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) |
이로부터 도출되는 핵심 규칙:
- 모든 등록은 반드시
ctx를 통해 이루어져야 합니다 (ctx.on/ctx.tools.register/ctx.effect/ctx.plugin/ctx.provide).ctx를 통해 등록된 것은 언로드 시 자동으로 정리됩니다. 절대 수동으로 removeListener / clearInterval을 호출하지 마십시오. ctx를 통해 관리되지 않는 모든 리소스(네트워크 연결, 자체 타이머, 파일 핸들, watcher)는 반드시ctx.effect(() => { ...; return () => cleanup })로 감싸야 합니다.- 의존성은 반드시 선언해야 합니다 (
inject).ctx.get()을 사용하여 필수 의존성을 탐지하지 마십시오. 로드 순서를 임의로 가정하지 마십시오. 순서는 의존성에 의해 결정되며cordis.yml의 줄 순서가 아닙니다. - 구성은 설정 가능해야 합니다: 배포 환경에 따라 값이 달라질 수 있는 모든 매개변수는 반드시
Config필드여야 합니다. 하드코딩은 절대 허용되지 않습니다 (검증 기준:cordis.yml에서 코드 수정 없이 이 값을 변경할 수 있는가?). - 구성 오류는 명확하게 알려야 합니다: Schemastery 스키마를 사용하여 로드 시 검증하고, 잘못된 구성은 즉시 로드 실패와 명확한 오류 메시지를 제공해야 합니다. 절대 비정상 상태로 실행하지 마십시오.
- 콜백은 재생/핫 리로드 가능해야 합니다: 표시 관련 콜백(
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로 구성 대조
세 가지 플러그인 형태
- 함수 형태 (기본 권장):
export const name = 'greet-tool' export const inject = ['tools'] export function apply(ctx: Context) { /* ... */ } - 객체 형태:
export default { name, inject, apply(ctx) {} } - 클래스 형태 (외부에 서비스 제공 필요 시):
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에서 설명하세요.
- args는 런타임 검증을 거쳤지만, 스키마 DSL로 표현할 수 없는 제약(빈 문자열 아님, 양수, 교차 필드)은 직접
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-execute | waterfall | 허용/거부/질문 전략 (권한 게이트); {kind:'allow'} / {kind:'deny',reason} / {kind:'ask'} 반환 |
ctx.tools.guard() | 단조 가드 | 최종 거부, 이후 리스너가 취소 불가능 (reason을 반환하면 거부) |
tools/execute | waterfall | 디스패치 래핑: 타임아웃/재시도/메트릭; exec.signal을 대체할 수 있지만 제거할 수 없음 |
tools/post-execute | waterfall | 콘텐츠/값 대체, 결과 차단, 모델 컨텍스트 첨부 |
tools/result | emit | 읽기 전용 관찰, 불변 최종 결과 (로그, 감사) |
전략을 도구 내부에 빌드인하지 마십시오. 배포 전략을 훅 플러그인(일반 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. 이벤트 시스템 (플러그인 간 통신)
디스패치 모드 (이벤트 이름의 일부, 반드시 올바르게 선택)
| 모드 | 호출 | 의미 |
|---|---|---|
emit | ctx.emit(name, ...args) | 동기 브로드캐스트, 대기하지 않고 반환 값을 수집하지 않음 |
parallel | await ctx.parallel(...) | 모든 리스너가 동시에 실행되고 완료될 때까지 대기 |
serial | await ctx.serial(...) | 순서대로 실행 및 대기, 첫 번째 null/false/undefined가 아닌 반환 값이 승리하고 중지 |
bail | ctx.bail(...) | serial의 동기 버전 |
waterfall | await 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— 승인 waterfallsystem-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 실행), 세 가지 역할로 분할하고 다른 패키지에 배치합니다:
- Service Definition (예:
dsh-shell): Cordis 서비스 인터페이스 + Request/Result 타입 정의.abstract class MyCapService extends Service { super(ctx,'myCap'); abstract execute(req): Promise<res> } - Service Provider (예:
dsh-bash-local): 인터페이스 구현.class MyCapLocal extends MyCapService { ... };apply(ctx){ ctx.plugin(MyCapLocal) } - 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. 권장 개발 워크플로우
- 저장소 체크아웃 및 환경 확인:
git clone https://github.com/deepseek-ai/deepseek-harness.git && cd deepseek-harness && pnpm install - 기존 플러그인 및 구문 이해:
cordis.yml, 기존 플러그인 소스 (예:plugins/*) 읽기 - 멘탈 모델 숙지: 위 1절의 핵심 개념 검토
- 플러그인 작성: 올바른 주형(함수/객체/클래스) 사용,
inject및Config선언,ctx를 통해 등록 - 로컬에서 테스트: 출처 1번 진입점 (
pnpm dsh web --patch your-plugin.yml) 또는dsh --profile dev사용 - 검증: 아래 검증 체크리스트 통과
- 공유를 위한 패키징: 번들 구조 생성,
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 클라이언트에 전달됩니까?