Community생산성 & 협업github.com

green-dalii/dsh-plugin-dev-skill

에이전트가 DeepSeek Harness(DSH) 플러그인을 올바르고 효율적이며 공식 규칙에 따라 개발할 수 있도록 하는 스킬 팩입니다.

dsh-plugin-dev-skill란 무엇인가요?

이 스킬은 DeepSeek Harness (DSH) 플러그인 개발을 위한 종합 가이드입니다. DSH가 Cordis 프레임워크 위에 구축된 플러그인 시스템이라는 핵심 개념부터 시작하여, 툴(tool) 플러그인, LLM 어댑터, 서비스, 이벤트 시스템 등 다양한 플러그인 유형을 개발하는 데 필요한 모든 것을 다룹니다. 주요 개념으로는 가역 효과(revertible effect), 반응적 공효과(reactive coeffect), Fiber(컴포넌트 인스턴스)가 있으며, 모든 등록은 반드시 `ctx`를 통해 이루어져야 하고, 종속성은 명시적으로 선언되어야 합니다. 또한, Schemastery를 사용한 설정 검증, 서비스 제공 및 소비 패턴, 이벤트 시스템, LLM 어댑터 개발, 플러그인 번들링 및 설치 방법까지 상세히 설명합니다. Claude Code, Cursor, Codex와 같은 모든 AI 에이전트와 호환됩니다.

지원 대상✓Claude Code✓Codex CLI~Cursor
npx skills add green-dalii/dsh-plugin-dev-skill

Installed? Explore more 생산성 & 협업 skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

DeepSeek Harness 플러그인 개발 스킬

어떤 Agent든 올바르고 효율적이며 규칙에 따라 DeepSeek Harness(DSH) 플러그인을 개발할 수 있도록 합니다. 이 스킬은 DSH 플러그인 개발을 위한 실무 가이드입니다: 먼저 멘탈 모델과 핵심 규칙을 제시하고, 바로 사용할 수 있는 코드 템플릿과 단계별 프로세스를 제공하며, 마지막으로 검증 체크리스트를 제시합니다. 심층적인 배경 지식은 References/ 디렉토리의 간결한 문서를 참조하세요.


0. 이 스킬을 사용해야 하는 경우

다음 작업 중 하나라도 해당되면 반드시 이 스킬을 로드해야 합니다:

  • DeepSeek Harness용 플러그인(tool, LLM 어댑터, 서비스 제공자, 훅, UI, 프로토콜 브릿지 등) 작성, 수정 또는 디버깅
  • cordis.yml / cordis.patch.yml / dsh.profile / dsh.bundle 구성 작성 또는 수정
  • dsh plugin ... 명령어로 플러그인 설치, 제거, 패키징
  • DSH에서 ctx.tools, ctx.llm, ctx.agents, ctx.sessions 서비스 또는 tools/*, agent/*, session/event 이벤트 이해
  • DSH의 플러그인 모델(Cordis 프레임워크) 작동 방식 연구

먼저 이 SKILL.md 전체를 읽고 작업을 시작하세요. 작업을 시작하기 전에 프로젝트에 이미 존재하는 cordis.yml / profile / 관련 소스 코드를 읽고 기존 규칙을 확인하세요.


1. 멘탈 모델: DSH는 Cordis 위에 구축된 플러그인 시스템입니다

한 문장으로 요약: DeepSeek Harness는 모든 기능(도구, LLM 어댑터, 파일 접근, agent 루프 자체)이 플러그인(컴포넌트)이며, 공유 컨텍스트(context)에 마운트되는 Agent Harness SDK입니다.

세 가지 핵심 개념 (논문 "시공간적 구성성을 위한 프로그래밍 패러다임"에서 발췌, References/10-spatiotemporal.md 참조):

개념의미코드에서의 표현
가역 효과 (revertible effect)컴포넌트가 공유 환경을 변경할 때마다 역연산을 함께 저장하며, 런타임은 이를 추적하고 컴포넌트가 언로드될 때 LIFO 순서로 복원합니다.ctx.effect(() => () => cleanup); ctx.on(), ctx.tools.register() 등도 effect이며, 언로드 시 자동으로 취소됩니다.
반응적 공효과 (reactive coeffect)컴포넌트가 의존하는 서비스를 선언합니다. 서비스가 나타나거나, 사라지거나, 구현체가 변경되면 컴포넌트는 자동으로 활성화/비활성화/재로드됩니다.export const inject = ['tools']; 의존성이 사라지면 플러그인이 자동으로 dispose되고, 복구되면 자동으로 재로드됩니다.
Fiber (컴포넌트 인스턴스)로드된 각 플러그인 인스턴스는 fiber이며, 생명주기 상태 머신을 가집니다.PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED (apply에서 오류 발생 시 FAILED)

이로부터 도출되는 핵심 규칙:

  1. 모든 등록은 반드시 ctx를 통해 이루어져야 합니다 (ctx.on / ctx.tools.register / ctx.effect / ctx.plugin / ctx.provide). ctx를 통해 등록된 것은 언로드 시 자동으로 정리됩니다. 절대 수동으로 removeListener / clearInterval을 호출하지 마십시오.
  2. ctx를 통해 관리되지 않는 모든 리소스(네트워크 연결, 자체 타이머, 파일 핸들, watcher)는 반드시 ctx.effect(() => { ...; return () => cleanup })로 감싸야 합니다.
  3. 의존성은 반드시 선언해야 합니다 (inject). ctx.get()을 사용하여 필수 의존성을 탐지하지 마십시오. 로드 순서를 임의로 가정하지 마십시오. 순서는 의존성에 의해 결정되며 cordis.yml의 줄 순서가 아닙니다.
  4. 구성은 설정 가능해야 합니다: 배포 환경에 따라 값이 달라질 수 있는 모든 매개변수는 반드시 Config 필드여야 합니다. 하드코딩은 절대 허용되지 않습니다 (검증 기준: cordis.yml에서 코드 수정 없이 이 값을 변경할 수 있는가?).
  5. 구성 오류는 명확하게 알려야 합니다: Schemastery 스키마를 사용하여 로드 시 검증하고, 잘못된 구성은 즉시 로드 실패와 명확한 오류 메시지를 제공해야 합니다. 절대 비정상 상태로 실행하지 마십시오.
  6. 콜백은 재생/핫 리로드 가능해야 합니다: 표시 관련 콜백(output.render, presentCall, presentResult)은 반드시 순수 함수여야 합니다. 플러그인은 HMR에 의해 언제든지 언로드/재로드될 수 있으므로, 재로드에 걸쳐 유지되는 전역 상태에 의존하지 마십시오.

2. 환경 및 도구 체인

  • 플러그인은 TypeScript 모듈(JS도 가능)이며, Node.js ESM 환경에서 실행됩니다.
  • 핵심 의존성 패키지(모두 @deepseek-ai 스코프):
    • @deepseek-ai/cordis — 플러그인 프레임워크 (context, fiber, 이벤트, 레지스트리)
    • @deepseek-ai/schemastery — 구성 스키마 검증 (Standard Schema 구현)
    • @deepseek-ai/dsh-tools — defineTool 및 도구 레지스트리 서비스
    • @deepseek-ai/dsh-llm — LlmAdapter, StreamChunk, GenerateOptions, CallId
    • 각 능력 seam 패키지: dsh-shell, dsh-session, dsh-agent, dsh-system-prompt, dsh-fs, dsh-jobs, dsh-credentials 등 (전체 목록은 References/08-capability-layering.md 참조)
  • 두 가지 개발 진입점 (선택):
    • 소스 코드 체크아웃 (권장, 튜토리얼 시나리오): deepseek-ai/deepseek-harness 클론, pnpm install, pnpm dsh web --patch <cordis.yml>로 Web UI 시작, node --import tsx ../../vendor/cordis/bin.js로 순수 Cordis 튜토리얼 예제 실행.
    • 설치된 CLI: dsh --profile <name> ...으로 profile 시작, dsh plugin --profile <name> <pnpm args>로 플러그인 관리.

3. 플러그인의 최소 형태 (먼저 이것을 배우세요)

플러그인은 apply 함수를 내보내는 모듈입니다:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'my-plugin'   // 선택 사항, 진단 표시용

export function apply(ctx: Context) {
  // 여기에 능력 등록
}

cordis.yml 또는 patch를 통해 마운트:

- name: './src/my-plugin.ts'        # 상대 경로 (소스/개발) 또는 npm 패키지 이름 (설치 후)
  id: my-plugin                     # 안정적인 식별자, loader가 id로 구성 대조

세 가지 플러그인 형태

  1. 함수 형태 (기본 권장):
    export const name = 'greet-tool'
    export const inject = ['tools']
    export function apply(ctx: Context) { /* ... */ }
    
  2. 객체 형태: export default { name, inject, apply(ctx) {} }
  3. 클래스 형태 (외부에 서비스 제공 필요 시): export default class MyService extends Service { constructor(ctx) { super(ctx, 'myService') } }

서비스 제공이 필요해지기 전까지는 항상 함수 형태를 사용하세요.


4. Tool 개발 (가장 빈번한 작업)

도구는 ctx.tools에 등록하며, defineTool을 사용하여 정의합니다. 템플릿 (바로 복사해서 사용 가능):

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'my-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'my_tool',                                  // 모델이 보는 도구 이름 (snake_case 규칙)
    description: 'One sentence about what it does.',  // 모델이 보는 기능 설명, 구체적이고 언제 사용할지 명시
    parameters: {
      path: { type: 'string', required: true, description: '매개변수의 의미' },
      limit: { type: 'number' },                      // required가 없으면 선택 사항
    },
    output: {
      schema: { type: 'string' },                     // execute가 반환하는 표준 JSON 값 선언
      render: (_args, value) => [{ type: 'text', text: value }],  // 모델이 볼 수 있는 콘텐츠로 변환
    },
    async execute(args, exec) {
      // args는 스키마로 검증 및 타입 추론됨
      // exec.signal은 취소 신호, 장기 작업은 반드시 이에 응답해야 함
      return `result for ${args.path}`
    },
  }))
}

defineTool의 전체 계약 (반드시 준수)

  • parameters: 매개변수 스키 매핑, 루트는 암시적으로 열린 객체입니다. 각 속성은 required: true 가능. string/number/integer/boolean/null/array/object/oneOf 지원. 명시적 object 노드는 additionalProperties: true | false를 선언해야 합니다.
  • output.schema: execute가 반환하는 표준 JSON 값(객체/배열/스칼라/null 가능)을 선언합니다. 이는 프로그래매틱 호출자(Code Mode의 await tools.xxx())를 위한 API이며, 핸들과 필드를 직접 반환하도록 설계해야 합니다. 사람을 위한 설명은 output.render에 넣으세요.
  • execute(args, exec):
    • args는 런타임 검증을 거쳤지만, 스키마 DSL로 표현할 수 없는 제약(빈 문자열 아님, 양수, 교차 필드)은 직접 throw new Error(...)를 사용해야 합니다.
    • output.schema가 선언한 표준 값을 반환합니다. 콘텐츠 블록을 반환하지 마십시오. 호출자가 자연어에서 ID를 파싱하도록 강요하지 마십시오.
    • exec.signal을 준수하세요: 신호가 트리거되면 진행 중인 작업을 취소합니다.
    • 인프라 오류(예외 발생/잘못된 반환 값) → 결과는 isError로 표시됩니다. 성공했지만 결과가 만족스럽지 않은 경우(예: 0이 아닌 종료 코드) → 여전히 표준 값을 반환하고 render에서 설명하세요.
  • output.render(args, value): 순수 함수, 표준 값을 ContentBlock[](모델이 보는 텍스트)로 변환합니다. 여기서 절대 I/O, 세션 상태 읽기를 수행하지 마십시오.
  • 선택 사항 output.presentationMeta(args, value): 표준 값에서 재생 가능한 UI 메타데이터 파생 (순수 함수).
  • 선택 사항 presentCall(args) / presentResult(args, result): UI에서 도구의 카드 렌더링 의도 선언 (card: 'generic' | 'terminal' | 'diff' | 'search' | 'web' | 'read'). 반드시 순수 함수여야 하며 (실시간 스트림 및 세션 재생 중에 실행됨), I/O를 수행하지 않습니다.
  • timeoutMs, isConcurrencySafe, finalizeContent 등의 런타임 메타데이터를 모델이 볼 수 있는 스키마에 넣지 마십시오. 모델에 유출되지 않습니다.
  • 매개변수는 불변입니다: args를 읽기 전용으로 취급하세요. 등록 후에는 스키마를 변경하거나 콜백을 교체하지 마십시오.

장기 실행 작업 (백그라운드 작업)

run_in_background을 지원하려면:

const jobs = ctx.get('jobs')
if (!jobs) throw new Error('background jobs unavailable: load @deepseek-ai/dsh-jobs')
return {
  kind: 'background',
  jobId: jobs.start({
    kind: 'my-work',
    label: args.command,
    ...exec.agent ? { owner: exec.agent } : {},
    run: () => { /* { cancel, done, readOutput? } 반환 */ },
  }),
}

핵심: jobId를 발행한 후에는 작업 자체의 취소 신호(job_kill / owner dispose가 관리)를 사용하고, 더 이상 exec.signal을 사용하지 마십시오. 성공 출력 스키마는 { kind: 'background', jobId }와 같은 표준 핸들을 전달할 수 있어야 합니다. References/03-tools.md 및 @deepseek-ai/dsh-tool-bash 구현을 참조하세요.

도구 실행 전략 (확장 포인트, 필요에 따라 사용)

확장 포인트모드용도
tools/pre-executewaterfall허용/거부/질문 전략 (권한 게이트); {kind:'allow'} / {kind:'deny',reason} / {kind:'ask'} 반환
ctx.tools.guard()단조 가드최종 거부, 이후 리스너가 취소 불가능 (reason을 반환하면 거부)
tools/executewaterfall디스패치 래핑: 타임아웃/재시도/메트릭; exec.signal을 대체할 수 있지만 제거할 수 없음
tools/post-executewaterfall콘텐츠/값 대체, 결과 차단, 모델 컨텍스트 첨부
tools/resultemit읽기 전용 관찰, 불변 최종 결과 (로그, 감사)

전략을 도구 내부에 빌드인하지 마십시오. 배포 전략을 훅 플러그인(일반 Cordis 플러그인이 이러한 이벤트를 수신)에 넣으세요(예: 권한 게이트 예시는 References/11-cookbook.md 참조).


5. 플러그인 구성 (Schemastery)

플러그인은 Config(동일한 이름의 TS 인터페이스 + Schemastery 스키마)를 내보내며, Cordis는 apply 전에 검증하고 기본값을 채웁니다:

import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'

export interface Config {
  greeting: string
  maxRetries: number
  verbose?: boolean
}

export const Config: Schema<Config> = Schema.object({
  greeting: Schema.string().default('Hello'),
  maxRetries: Schema.number().default(3),
  verbose: Schema.boolean().default(false),
})

export function apply(ctx: Context, config: Config) {
  // config는 완전하고 검증된 상태임이 보장됨
}

cordis.yml에서 구성 전달:

- name: './src/my-plugin.ts'
  config:
    greeting: 'Hi there'
    maxRetries: 5

핵심 사항:

  • Config는 반드시 Standard Schema 객체여야 합니다 (Schemastery로 구축). 일반 객체를 내보낼 수 없습니다.
  • 자주 사용되는 생성자: Schema.string().required() / .default(x), Schema.number(), Schema.boolean(), Schema.array(String), Schema.union(['a','b']), Schema.object({...}) 중첩.
  • !!js 표현식은 로드 시 평가를 지원합니다 (예: greeting: !!js process.env.GREETING ?? 'Hello'); !!js는 config 및 disabled 필드에만 유효합니다.
  • 구성 변경은 HMR을 트리거합니다: 이전 인스턴스 언로드 (등록 자동 정리) → 새 인스턴스 로드. 플러그인 외부에서 구성을 캐시하지 마십시오.

6. 서비스 및 의존성 (서비스 개발)

서비스 사용 (소비자)

export const inject = ['tools']        // 필수 의존성: 충족되지 않으면 로드되지 않음
export function apply(ctx: Context) {
  ctx.tools.register(/* ... */)        // apply 시점에 이미 준비됨이 보장됨
}

선택적 의존성: inject를 작성하지 않고, 사용 시 ctx.get('service')로 탐지합니다 (undefined 반환 시 제공자가 없음을 의미).

서비스 제공 (제공자)

Service 하위 클래스 사용 (클래스 형태 플러그인):

import { Service, type Context } from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis' {
  interface Context {
    metrics: MetricsService     // 선언 병합: ctx.metrics에 타입 부여
  }
}

export default class MetricsService extends Service {
  static inject = ['llm']       // 서비스도 다른 서비스에 의존 가능
  constructor(ctx: Context) {
    super(ctx, 'metrics')       // 'metrics'는 서비스 이름
  }
  record(event: string, value: number) { /* ... */ }
}

export const name = 'metrics'
export function apply(ctx: Context) {
  ctx.plugin(MetricsService)
}

저수준 API (자주 사용되지는 않지만 알아야 함): ctx.provide(name, value)로 서비스 구현체 등록 (effect, 언로드 시 자동 제거), ctx.set/get으로 저장소 읽기/쓰기, ctx.accessor로 계산된 속성 정의, ctx.mixin으로 서비스 멤버를 ctx에 마운트.

서비스 동작 의미 (이해하고 암기)

  • inject는 일회성 시작 검사가 아닙니다: 실행 중 필수 서비스가 사라지면 → 의존하는 플러그인이 자동으로 dispose되고; 서비스가 복구되면 → 자동으로 재로드됩니다. 이는 소비자가 언로드된 서비스에 대한 참조를 절대 유지하지 않도록 보장합니다.
  • 서비스 이름은 플랫 글로벌 네임스페이스입니다: tools, llm 등은 이미 사용 중입니다. 고유 서비스 이름은 식별 가능한 접두사를 사용하세요 (예: myCap, metrics).
  • 서비스 격리: cordis.yml에서 group + isolate를 사용하여 다른 플러그인 그룹이 동일한 서비스 이름의 다른 인스턴스를 보도록 할 수 있습니다 (예: 두 그룹이 각각 다른 timeout으로 구성된 shell 제공자).

7. 이벤트 시스템 (플러그인 간 통신)

디스패치 모드 (이벤트 이름의 일부, 반드시 올바르게 선택)

모드호출의미
emitctx.emit(name, ...args)동기 브로드캐스트, 대기하지 않고 반환 값을 수집하지 않음
parallelawait ctx.parallel(...)모든 리스너가 동시에 실행되고 완료될 때까지 대기
serialawait ctx.serial(...)순서대로 실행 및 대기, 첫 번째 null/false/undefined가 아닌 반환 값이 승리하고 중지
bailctx.bail(...)serial의 동기 버전
waterfallawait ctx.waterfall(name, ...args, next)미들웨어 래핑; 리스너는 next를 받고 하류 반환 값을 래핑

waterfall 핵심 규칙: 관찰/주석만 하는 리스너는 반드시 next()를 호출해야 합니다. next()를 호출하지 않고 직접 반환 = 의도적 단락 (거부/차단). next() 호출을 잊으면 하류 기본 동작이 조용히 사라집니다.

타입 안전 이벤트

declare module '@deepseek-ai/cordis' {
  interface Events {
    'my-plugin/ready': (payload: { id: string }) => void
    'my-plugin/check': (input: string) => boolean | undefined
  }
}

이벤트 명명 규칙 namespace/action (예: agent/step, tools/result, session/event). 리스너는 ctx.on()으로 등록하면 플러그인 언로드 시 자동으로 정리됩니다.

자주 사용되는 Harness 이벤트 (개발 중 자주 접함)

  • tools/pre-execute / tools/execute / tools/post-execute / tools/result — 도구 파이프라인 (위 표 참조)
  • agent/* (agent/request, agent/pre-step, agent/session-start, agent/turn-stopping 등) — agent 생명주기 및 요청 구성
  • session/event — 지속적인 세션 이벤트 스트림 (turn/*, step/*, tool/call, tool/result, assistant/chunk는 모두 여기의 event.type이며, 동일한 이름의 Cordis 이벤트가 아님)
  • approval/request — 승인 waterfall
  • system-prompt/assemble — 시스템 프롬프트 전체 조립 (권위 있는 반환, 리스너는 기존 기여를 보존할 책임이 있음)

전체 서명/트리거 모드는 각 하위 시스템 페이지에서 생성된 cordis-surface 블록을 참조하세요 (References/08-capability-layering.md에 안내가 있음).


8. LLM 어댑터 (새 모델 제공자 연결)

어댑터 = LlmAdapter를 상속하고 stream()을 구현하는 클래스로, Harness 제공자 무관 요청을 특정 API 호출로 변환하고 다시 StreamChunk 조각으로 변환합니다.

import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'

class MyAdapter extends LlmAdapter {
  async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
    // 1. options.messages → 제공자 형식
    // 2. 스트리밍 API 호출 (반드시 options.signal 전달)
    // 3. 응답 → StreamChunk 시퀀스
  }
}

export const name = 'my-llm-adapter'
export const inject = ['llm']
export const Config = Schema.object({
  apiKey: Schema.string().required(),
  providers: Schema.array(String).required(),
})

export function apply(ctx: Context, config: Config) {
  ctx.llm.registerAdapter(config.providers, new MyAdapter(config.apiKey))
}

StreamChunk 프로토콜 (엄격히 준수)

async function* chunks(): AsyncIterable<StreamChunk> {
  yield { type: 'block-start', index: 0, blockType: 'text' }
  yield { type: 'text-delta', index: 0, text: 'Hello' }
  yield { type: 'block-end', index: 0, block: { type: 'text', text: 'Hello world' } }
  // 도구 호출 블록:
  yield { type: 'block-start', index: 1, blockType: 'tool-call' }
  yield { type: 'tool-call-delta', index: 1, id: CallId('c1'), name: 'bash', argumentsDelta: '{"command":"ls"}' }
  yield { type: 'block-end', index: 1, block: { type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{"command":"ls"}' } }
  yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }   // finish 전에 반드시 위치
  yield { type: 'finish', reason: { kind: 'stop' } }                        // 반드시 마지막
}

규칙: 각 block-start에는 해당하는 block-end가 있어야 합니다. index는 0부터 증가합니다. tool-call의 arguments는 전체가 원시 JSON 문자열입니다. finish는 마지막 블록입니다. usage는 finish 전에 위치합니다.

오류 및 메타데이터

  • 전송/프로토콜 오류: throw new LlmError(msg, 'STABLE_CODE') (안정적인 코드 포함), 일반 Error의 자동 변환에 의존하지 마십시오.
  • 제공자가 GenerateOptions의 특정 필드를 지원하지 않는 경우: throw new LlmError(..., 'UNSUPPORTED'), 절대 조용히 버리지 마십시오.
  • 각 HTTP 요청에 attributionHeaders()를 병합하고 options.signal을 전달합니다.
  • 선택 사항: resolveModel() (제공자/모델 ID + 선택적 context/reasoning 메타데이터 반환) 및 listModels() (모델 옵션 공개) 재정의.

전체 튜토리얼은 References/05-llm-adapter.md를 참조하세요. 참조 구현: packages/llm/llm-deepseek (OpenAI 호환 SSE) 및 packages/llm/llm-pi-ai.


9. 능력 계층화 (세 가지 역할, 교체 가능한 능력의 디자인 패턴)

능력에 교체 가능한 제공자가 필요한 경우 (예: Bash 실행), 세 가지 역할로 분할하고 다른 패키지에 배치합니다:

  1. Service Definition (예: dsh-shell): Cordis 서비스 인터페이스 + Request/Result 타입 정의. abstract class MyCapService extends Service { super(ctx,'myCap'); abstract execute(req): Promise<res> }
  2. Service Provider (예: dsh-bash-local): 인터페이스 구현. class MyCapLocal extends MyCapService { ... }; apply(ctx){ ctx.plugin(MyCapLocal) }
  3. Consumer (예: dsh-tool-bash): 능력을 모델 도구로 노출. inject: ['tools','myCap']

의존성 방향: Provider는 Definition에 의존하고, Consumer는 Definition에 의존하며, Provider와 Consumer는 서로 의존하지 않습니다. 제공자를 교체하려면 cordis.yml의 한 줄만 변경하면 되며, Definition과 Consumer는 변경할 필요가 없습니다.

예방적 분할은 하지 마십시오: 역할이 독립적으로 진화해야 할 때만 패키지를 분할하세요. 간단한 도구 플러그인은 하나의 패키지로 충분합니다.


10. 패키징 및 설치 (플러그인을 사용자에게 전달)

두 가지 개념

  • 번들 (bundle): 구성 레이어가 포함된 npm 패키지로, manifest가 dsh.bundle을 선언합니다. 작성자가 작성하고 배포하는 것입니다.
  • 프로필 (profile): $DSH_HOME/profiles/<name>에 위치한 시작 가능한 번들로, manifest가 dsh.profile을 선언합니다 (순서가 있는 bundles 목록 포함). 사용자가 시작하는 것입니다.

번들 패키징

hello-plugin/
├── package.json       # dsh.bundle 선언
├── cordis.patch.yml   # 레이어 내용
└── index.js           # 플러그인 모듈
// package.json
{
  "name": "dsh-hello-plugin",
  "version": "0.1.0",
  "type": "module",
  "main": "index.js",
  "files": ["index.js", "cordis.patch.yml"],
  "dsh": { "bundle": { "patch": "cordis.patch.yml" } }
}

cordis.patch.yml (cordis.yml에 병합될 레이어):

- name: './node_modules/.store/dsh-hello-plugin/index.js'   # 설치 후 절대 경로
  id: hello
  config:
    greeting: 'World'

Profile 구조

$DSH_HOME/
└── profiles/
    └── my-app/
        ├── dsh.profile        # manifest: bundles 목록 + 루트 구성
        └── cordis.yml         # 최종 조립된 Cordis 구성 (번들의 패치가 병합됨)

dsh.profile:

dsh:
  profile:
    bundles: ['dsh-hello-plugin@^0.1']

CLI 설치 명령어:

dsh plugin --profile my-app add dsh-hello-plugin@latest

내부적으로 DSH는 번들을 npm에 게시된 패키지로 처리하고, 의존성을 설치하며, cordis.patch.yml을 cordis.yml에 병합합니다.


11. 권장 개발 워크플로우

  1. 저장소 체크아웃 및 환경 확인: git clone https://github.com/deepseek-ai/deepseek-harness.git && cd deepseek-harness && pnpm install
  2. 기존 플러그인 및 구문 이해: cordis.yml, 기존 플러그인 소스 (예: plugins/*) 읽기
  3. 멘탈 모델 숙지: 위 1절의 핵심 개념 검토
  4. 플러그인 작성: 올바른 주형(함수/객체/클래스) 사용, inject 및 Config 선언, ctx를 통해 등록
  5. 로컬에서 테스트: 출처 1번 진입점 (pnpm dsh web --patch your-plugin.yml) 또는 dsh --profile dev 사용
  6. 검증: 아래 검증 체크리스트 통과
  7. 공유를 위한 패키징: 번들 구조 생성, npm publish 또는 내부 레지스트리 게시

12. 검증 체크리스트

플러그인을 "완료"로 표시하기 전에 이 체크리스트를 실행하세요.

기본 사항

  • 모든 부작용 등록이 ctx.on / ctx.effect / ctx.plugin / ctx.tools.register 등을 통해 이루어졌습니까?
  • 자체 관리 리소스(타이머/소켓/파일)가 ctx.effect로 래핑되었습니까?
  • inject가 필수 의존성을 정확히 선언했습니까 (ctx.get() 탐지 아님)?
  • Config가 모든 환경별 값을 포함하며 하드코딩이 없습니까?
  • Config는 Schemastery 객체이며, 검증 실패 시 로드가 실패합니까?

도구 전용

  • execute()는 exec.signal을 존중하며 취소 가능합니까?
  • 장기 실행 명령어 (os/명령어/파일)는 exec.signal을 확인합니까? run_in_background에 jobs를 사용합니까?
  • output.schema는 프로그래매틱 사용을 위해 표준 값을 선언합니까?
  • output.render()는 순수 함수이며 I/O가 없습니까?
  • presentCall/presentResult (있는 경우)는 순수 함수이며 I/O가 없습니까?
  • args는 읽기 전용으로 처리됩니까 (함수 본문 내에서 변경하지 않음)?
  • parameters의 유형과 설명은 모델이 올바르게 호출하기에 충분히 풍부합니까?

서비스 전용

  • Service 하위 클래스가 super(ctx, 'serviceName')를 호출합니까?
  • 제공자와 소비자가 Definition 패키지에만 의존하고 서로에게 의존하지 않습니까?
  • 서비스 이름에 충돌을 피하기 위한 접두사가 있습니까?

LLM 어댑터 전용

  • stream()이 정확한 StreamChunk 프로토콜을 따릅니까 (block-start/end 쌍, finish 마지막)?
  • 지원되지 않는 GenerateOptions 필드가 조용히 무시되지 않고 UNSUPPORTED를 던집니까?
  • 오류가 안정적인 코드와 함께 LlmError를 던집니까?
  • options.signal이 HTTP 클라이언트에 전달됩니까?

관련 스킬