Files
deepseek-harness/docs/cookbook/extension-cookbook.md
Tianyi Cui 140f818a42 refactor(events): remove the turn boundary mirror events
Complete the boundary-mirror removal begun with the step mirrors: drop
`agent/turn-start` and `agent/turn-end` from the agent event taxonomy. Turn and
step boundaries are now read exclusively off the durable `session/event` feed
(`turn/start`/`turn/end`/`step/start`/`step/end`) — there is no `agent/*` mirror
for any boundary.

- loop.ts: delete both turn emits; `closeTurn` loses its `emit` parameter and
  its now-unreachable idempotency guard (it is called exactly once per turn, on
  mutually exclusive normal/catch paths); `failTurn` loses the dead post-close
  branch that only a throwing turn-end LISTENER could reach.
- ui-stdio: render turn boundaries from `session/event`, recovering the short
  agent label from an `agent/created`→id map (the `turn/start` event carries only
  the turn number, and the session id is not reliably the agent id). ui-stdio is
  a disposable test REPL, so this migration retires the sole justification the
  event-domain-semantics RFC gave for KEEPING the turn mirrors.
- Tests: reason/turn-number collectors and the boundary-ordering test now read
  `session/event`; the throwing-turn-boundary-LISTENER tests are deleted (that
  code path no longer exists). A new test covers the outer-catch disposed branch
  via a pre-step listener that disposes-then-throws (the surviving real path).
- Docs: promote the "remove agent boundary mirror events" RFC to implemented
  (amended/narrowed — `agent/steering` is RETAINED, not a boundary mirror);
  update the event-domain-semantics + turn-enclosure RFCs, architecture.md, the
  cookbook, the ACP/agent/ui-stdio prose, and regenerate the cordis catalog.

`agent/steering` and `agent/stream-chunk` are explicitly out of scope (not
durable-boundary mirrors). ACP is unaffected — it already settles from the log's
`turn/end` + `agent/status`; snapshot goldens are byte-unchanged.
2026-07-02 03:26:45 +08:00

5.0 KiB

Cookbook: extension plugin shapes

The three plugin shapes you write against the harness extension surface, as illustrative snippets (elided imports and helper stubs — not copy-paste-complete). For the full step-by-step guides see adding a package, adding a tool, and adding an LLM adapter; for the seams these hook into see docs/architecture.md.

A tool plugin

A tool registers on ctx.tools. The annotated defineTool example (typed execute args, result shaping, the run_in_background pattern) lives in adding-a-tool.md — that guide is the source of truth for the tool shape. Raw JSON-Schema ToolDefinitions are also accepted by ctx.tools.register() directly (that is how MCP-sourced tools arrive); defineTool is the typed sugar for first-party tools.

A hook plugin (permission gate)

A hook wraps the tools/execute waterfall to veto or rewrite a call — the seam where sandbox, permission, and plan-mode plugins live.

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

declare function isAllowed(exec: ToolExecution): Promise<boolean>

export const name = 'permission-gate'

export function apply(ctx: Context) {
  ctx.on('tools/execute', async (exec, next) => {
    if (!(await isAllowed(exec))) {
      return {
        callId: exec.callId,
        content: [{ type: 'text', text: 'Denied by policy.' }],
        isError: true,
      }
    }
    return next()
  })
}

A UI plugin

A UI plugin consumes agent/stream-chunk and session events for rendering, and drives input back in via agent.send() / agent.steer().

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

declare function render(text: string): void
declare function onUserInput(handler: (text: string) => void): void

export const name = 'my-ui'
export const inject = ['agents']

export function apply(ctx: Context) {
  ctx.on('agent/stream-chunk', (agent, turn, step, chunk) => {
    if (chunk.type === 'text-delta') render(chunk.text)
  })
  onUserInput(text => ctx.agents.get(AgentId('main'))?.send([{ type: 'text', text }]))
}

A client-driver plugin (external protocol bridge)

A client driver is a UI plugin whose "user" is another program speaking a wire protocol rather than a human at a terminal. It owns the process's stdio (so it must run with no stdout logger — every non-protocol byte corrupts the stream), creates/resumes agents on demand through the dsh-agent factory seam, translates harness events (session/event, agent/*) into outbound protocol messages, and translates inbound requests back into agent.send() / agent.cancel(). Two harness-specific contracts make it correct: resolve each request exactly once off a settle signal (settle from the durable turn/end session event — the boundary is a session event, not an agent/* mirror — with agent/status as the fallback if a peer listener starved yours), and tear each agent down through its AgentHandle.dispose() (which stops the loop, awaits its exit, and unregisters), not just cancel() — disposal must reach quiescence, not merely request it.

packages/ui/acp is the worked example: it bridges the agent to the Agent Client Protocol (JSON-RPC over stdio) so Zed and other ACP editors can drive it. See its README for the full method surface and the deferred-permission-gate note.

import type { Context } from 'cordis'

export const name = 'my-protocol-bridge'
export const inject = ['agents', 'sessions', 'sessionPersistence']

export function apply(ctx: Context) {
  // Stream every logged assistant text/reasoning delta out to the client.
  ctx.on('session/event', (_session, event) => {
    if (event.type === 'assistant/chunk') {
      const chunk = event.data.chunk
      if (chunk.type === 'text-delta') {
        // sendToClient({ kind: 'message_chunk', text: chunk.text })
      }
    }
  })
  // Inbound "prompt": create/resume an agent and feed it; settle on turn end.
  // Teardown reaches quiescence via AgentHandle.dispose() (stop + await exit).
}

Runnable wirings

Three complete examples load their plugin trees from cordis.yml: examples/echo-agent (mock model + echo tool — the all-mock skeleton check, pnpm run demo:echo), examples/coding-agent (DeepSeek V4 + the bash tool suite — the real thing, pnpm run demo:coding), and examples/acp-agent (the same coding agent exposed as an ACP server over JSON-RPC stdio — the client-driver shape, pnpm run demo:acp). Each leaf is now just its swappable backends plus an app-package entry: the stdio demos load @deepseek-ai/dsh-stdio-agent, the ACP demo loads @deepseek-ai/dsh-acp-agent, and both app packages share the spine via the @deepseek-ai/dsh-agent-core bundle.