Cookbook: extension plugin shapes

Reference patterns for harness extensions. The snippets omit imports and helper implementations and are not copy-paste-complete. For concrete authoring paths, see the package checklist, first-tool tutorial, tool reference, and LLM adapter guide; the architecture owns the system and extension-point map.

A tool plugin

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

A hook plugin (permission-gate example)

This permission gate is one example of a hook plugin. It returns a typed decision from the tools/pre-execute gate to allow or deny a call; sandbox, permission, and plan-mode plugins can use this extension point. Hook plugins can intercept other extension points and are not inherently permission gates. A "native hook" is an ordinary Cordis plugin on an interception point; it needs no external protocol.

import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, 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/pre-execute', async (exec, next): Promise<PreToolDecision> => {
    if (!(await isAllowed(exec))) {
      return { kind: 'deny', reason: 'Denied by policy.' }
    }
    return next()
  })
}

This waterfall is the reorderable policy layer. Use ctx.tools.guard() when an invariant needs a monotonic final denial, tools/execute when a plugin must wrap the actual dispatch lifetime (timeouts/retries/metrics; only exec.signal is replaceable), tools/post-execute for explicit result transformation, and tools/result for contained observation of the immutable final outcome. The adding-a-tool guide gives the selection rule.

A UI plugin

A UI plugin renders from the session/event feed (the assistant token stream as assistant/chunk, plus turn/step boundaries and tool activity), and drives input back in via agent.followup() / agent.steer(). A browser plugin contributing a business row to the built-in Web Client instead registers a ConversationNodeDefinition and keyed Chat renderer; follow the Conversation Node guide.

import type { Context } from '@deepseek-ai/cordis'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'

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('session/event', (_session, event) => {
    if (event.type === 'assistant/chunk' && event.data.chunk.type === 'text-delta') {
      render(event.data.chunk.text)
    }
  })
  onUserInput(text => ctx.agents.get(SessionId('client-session'))?.followup(createUserMessage({
    content: [{ type: 'text', text }],
    source: { kind: 'user' },
  })))
}

An external protocol driver

A protocol driver adapts a wire peer to ctx.agents; it may serve a UI or an automation client. A stdio driver owns stdout, creates or resumes agents through the factory, and maps protocol requests to followup() or cancel(). A low-level prompt request returns its durable enqueue receipt; it does not acquire a result by correlating MessageId with turn/end. Publish whole-agent status separately. An automation method may wait from its receipt through the next idle and summarize that explicitly owned interval, while a UI normally keeps observing the open-ended event stream. Tear agents down with AgentHandle.dispose() so disposal reaches quiescence.

packages/acp/acp is the automation-only worked example: it exposes fresh text sessions over Agent Client Protocol JSON-RPC stdio, emits committed assistant text, and registers a one-shot machine permission answerer for agents it owns. Its README defines the exact methods, event order, and lifecycle contract.

import type { Context } from '@deepseek-ai/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, feed it, and return its enqueue receipt.
  // Whole-agent status is a separate notification; no turn end belongs to this prompt.
  // Teardown reaches quiescence via AgentHandle.dispose() (stop + await exit).
}

Runnable wirings

Runnable leaves load their plugin trees from examples/*/cordis.yml; the root demo:* scripts and those leaf directories are the authoritative inventory. The product dsh launcher owns Web and one-shot headless execution, ACP leaves use @deepseek-ai/dsh-acp-demo, and JSON-RPC leaves use @deepseek-ai/dsh-sdk-jsonrpc-demo. The headless snapshot leaf mounts @deepseek-ai/dsh-agent-spine-demo and JSONL persistence explicitly, then drives them through an example-owned test fixture rather than a shipped app package.

The feature → mechanism map

Every product feature maps to a listener on a documented extension point — the microkernel claim made checkable (microkernel Agent Note). No row modifies the loop.

system-prompt/assemble is an expert cooperative whole-assembly transform: its returned assembly is authoritative, so listener authors own preserving active Code Mode and structured-output protocol contributions. Prefer ctx.tools.restrict() for tool filtering that must stay aligned across presentation, lookup, and execution.

Product feature Plugin mechanism
Hook system (user + project level) listeners on agent/session-start, agent/pre-step, agent/request, tools/pre-execute, tools/post-execute, and agent/turn-stopping; the waterfalls return typed decisions, while agent/turn-stopping may steer another step; the dsh-hooks-claude-code / dsh-hooks-codex bridges map hook config files onto these extension points
/goal ctx.goals owns durable state, dsh-goal-round-driver schedules same-session rounds through the public Agent, and separate command/tool producers expose human/model control
/loop on the turn/end session event, followup() the next iteration; or force-continue
Dynamic workflow ctx.workflowEngine + the worker-thread engine + the workflow tool; structured in-process children enforce output with scoped prompt/tool registrations, a monotonic tool guard, final tools/result commit (including enclosing run_code), and the structured-output execution's monotonic concludeTurn() marker
Queued + steering messages core Agent.followup() / Agent.steer()
Context compaction (auto + manual) the ctx.compaction seam + dsh-compaction-basic; automatic pressure runs on serial agent/pre-step, canonical overflow recovery runs on agent/request-error, and manual callers use the same compact service (compaction Agent Note)
System prompt configurability ctx.systemPrompt.section() with ordering and scope-local shadowing
AGENTS.md (root) a section provider reading the file
AGENTS.md (subdir, on-touch) + file-change notices agent.inject() from a watcher / tool-result listener
Built-in tools ctx.tools.register(); schemas flow into the assembly automatically — the dsh-tool-* families (bash, fs, web, subagent, todo) are the shipped examples
ToolSearch / progressive disclosure replace a scoped ctx.tools.restrict() registration as the visible set changes; the registry keeps presentation, lookup, and execution aligned
Tool deadline / retry / metrics wrap core dispatch with tools/execute; a wrapper may replace exec.signal, delegate, and inspect the normalized result in one lexical lifetime
Final tool-result metrics / audit / capture observe immutable authoritative outcomes with tools/result; use tools/post-execute instead only when the plugin must transform the result or attach context
Monotonic terminal turn policy call ToolExecution.concludeTurn() from the successful terminal tool; later tool calls in the same response remain guardable, and the loop stops after the step
Subprocess sandbox (landlock / sandbox-exec) use a ctx.sandbox backend through dsh-bash-sandbox; use tools/pre-execute for capability-level denial
Permission system / AskUserQuestion return ask from tools/pre-execute and answer through ctx.approval; register a separate model-facing ask tool for ordinary user questions
Plan mode @deepseek-ai/dsh-plan-mode — logged plan/mode state, the plan:policy guidance section, /plan [message] entry, /plan off direct exit, and the user-reviewed exit_plan_mode exit; enforcement stays on the independent sandbox/approval axes
Sub-agent delegation the ctx.subagents provider registry (dsh-subagent-spawn-in-process/-fork/-acp/-codex/-claude-code/-dsh-sdk) + dsh-tool-subagent exposing one configured provider to the model
MCP one plugin per server: discover tools → ctx.tools.register()
Skills section + tool registration; inject() skill content on invocation
Memory section provider + tool
Scheduled tasks (cron) a plugin registers model-callable scheduling tools; timer fires → followup(…, {source: {kind: 'cron', …}}) when idle / inject() notification when busy
UI (GUI; CLI emits JSONL) listen session/event (assistant chunks, boundaries, tool activity); input → followup()
Web Client Chat business node register a ConversationNodeDefinition and conversation.chat.node keyed renderer
SessionTelemetryBackend / replayable trace session/event → JSONL; replay = sessions.create(id, { seed })
Model adapters LlmAdapter subclass via registerAdapter (dsh-llm-deepseek, dsh-llm-pi-ai)
Plugin hot-reload every registration is a ctx.effect → vendored HMR just works

实操手册:扩展插件形态

harness 扩展的参考模式。代码片段省略了 import 和辅助实现,无法直接复制运行。具体编写路径见包检查清单第一个工具教程工具参考LLM(大语言模型)适配器指南;系统与扩展点映射由架构文档负责。

工具插件

工具在 ctx.tools 上注册。带注解的 defineTool 示例(类型化的 execute 参数、结果构造、run_in_background 模式)见 adding-a-tool.md——该指南是工具定义的真源。ctx.tools.register() 也直接接受原始 JSON Schema ToolDefinition(MCP 来源的工具就是这样到达的);defineTool 是第一方工具使用的类型化辅助函数。

钩子插件(以权限门禁为例)

这个权限门禁是钩子插件的一个示例。它从 tools/pre-execute 门禁返回一个类型化的决策,用于允许或拒绝一次调用;沙箱、权限和 plan-mode 插件都可以使用该扩展点。钩子插件也可以拦截其他扩展点,本身并不等同于权限门禁。「原生钩子」是在拦截点上运行的普通 Cordis 插件,不需要外部协议。

import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, 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/pre-execute', async (exec, next): Promise<PreToolDecision> => {
    if (!(await isAllowed(exec))) {
      return { kind: 'deny', reason: 'Denied by policy.' }
    }
    return next()
  })
}

这个 waterfall(瀑布式事件)是可重排的策略层。当不变式需要单调的最终拒绝时使用 ctx.tools.guard();当插件需要包裹实际分发生命周期时(超时/重试/指标;仅 exec.signal 可替换)使用 tools/execute;显式结果变换使用 tools/post-execute;对不可变最终结果的受限观察使用 tools/result。选择规则见添加工具指南

UI 插件

UI 插件从 session/event 事件流渲染(助手 token 流以 assistant/chunk 形式到达,加上轮次/步骤边界与工具活动),并通过 agent.followup() / agent.steer() 将输入驱动回去。如果浏览器插件要向内建 Web Client 贡献业务行,则应注册 ConversationNodeDefinition 与 keyed Chat renderer;具体步骤见 Conversation Node 指南

import type { Context } from '@deepseek-ai/cordis'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'

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('session/event', (_session, event) => {
    if (event.type === 'assistant/chunk' && event.data.chunk.type === 'text-delta') {
      render(event.data.chunk.text)
    }
  })
  onUserInput(text => ctx.agents.get(SessionId('client-session'))?.followup(createUserMessage({
    content: [{ type: 'text', text }],
    source: { kind: 'user' },
  })))
}

外部协议驱动

协议驱动将协议对端接入 ctx.agents;它可以服务于 UI 或自动化客户端。stdio 驱动拥有 stdout,通过工厂创建或恢复 agent(智能体),并将协议请求映射为 followup()cancel()。底层提示词请求返回其持久入队回执;它不会通过关联 MessageIdturn/end 获得结果。整个 agent 的状态应单独发布。自动化方法可以从回执等待到下一次 idle,并概括这一显式拥有的区间;UI 通常则会持续观察开放式事件流。通过 AgentHandle.dispose() 拆除 agent,以使 dispose(资源释放)达到完全停稳。

packages/acp/acp 是仅面向自动化的完整示例:它通过 ACP(Agent Client Protocol)JSON-RPC stdio 提供全新文本会话,发出已提交的助手文本,并为其拥有的 agent 注册一次性机器权限应答器。其 README 定义确切的方法、事件顺序和生命周期约定。

import type { Context } from '@deepseek-ai/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, feed it, and return its enqueue receipt.
  // Whole-agent status is a separate notification; no turn end belongs to this prompt.
  // Teardown reaches quiescence via AgentHandle.dispose() (stop + await exit).
}

可运行的组装示例

可运行叶子从 examples/*/cordis.yml 加载各自的插件树;根目录的 demo:* 脚本和这些叶子目录是权威清单。产品 dsh 启动器负责 Web 和一次性 headless 执行,ACP 叶子使用 @deepseek-ai/dsh-acp-demo,JSON-RPC 叶子使用 @deepseek-ai/dsh-sdk-jsonrpc-demo。headless 快照叶节点显式挂载 @deepseek-ai/dsh-agent-spine-demo 和 JSONL 持久化,再通过示例自有的测试 fixture(测试前置数据)驱动这些组件,而不是通过已交付的 app 包。

功能→机制映射

每个产品功能都映射到一个文档化扩展点上的监听器——微内核声明由此可验证(微内核 Agent Note)。没有任何一行修改循环本身。

system-prompt/assemble 是一个专家协作式的整体装配变换:其返回的装配结果具有权威性,因此监听器作者有责任保留活跃的 Code Mode 和结构化输出协议的贡献。对于需要在展示、查找和执行之间保持对齐的工具过滤,优先使用 ctx.tools.restrict()

产品功能 插件机制
钩子系统(用户级 + 项目级) agent/session-startagent/pre-stepagent/requesttools/pre-executetools/post-executeagent/turn-stopping 上的监听器;waterfall 返回类型化决策,agent/turn-stopping 则可通过 steering(中途引导)触发下一步;dsh-hooks-claude-code / dsh-hooks-codex 桥接器将钩子配置文件映射到这些扩展点上
/goal ctx.goals 管理持久状态,dsh-goal-round-driver 通过公共 Agent 调度同会话 Round,独立的命令/工具生产方分别提供人类/模型控制
/loop turn/end 会话事件上 followup() 下一次迭代;或强制继续
动态工作流 ctx.workflowEngine + worker-thread 引擎 + workflow 工具;结构化的进程内子任务通过作用域化的提示词/工具注册、单调工具守卫、最终 tools/result 提交(包括外层 run_code)和结构化输出执行的单调 concludeTurn() 标记来强制输出
排队消息 + steering 核心 Agent.followup() / Agent.steer()
上下文压缩(context compaction)(自动 + 手动) ctx.compaction seam + dsh-compaction-basic;自动压力检查运行在串行 agent/pre-step,标准的溢出恢复机制运行在 agent/request-error,手动调用方使用同一个压缩服务(压缩 Agent Note
系统提示词可配置性 ctx.systemPrompt.section(),支持排序与作用域局部覆盖
AGENTS.md(根目录) 一个读取该文件的 section 提供方
AGENTS.md(子目录,按需触发)+ 文件变更通知 从 watcher / 工具结果监听器调用 agent.inject()
内置工具 ctx.tools.register();schema 自动流入装配——dsh-tool-* 系列(bash、fs、web、subagent、todo)是已交付的示例
ToolSearch / 渐进式披露 当可见集变化时替换一个作用域化的 ctx.tools.restrict() 注册;注册表保持展示、查找和执行三者对齐
工具截止时间 / 重试 / 指标 tools/execute 包裹核心分发;包装层可替换 exec.signal、委托执行,并在同一词法生命周期内检视规范化结果
最终工具结果指标 / 审计 / 捕获 tools/result 观察不可变的权威结果;仅当插件需要变换结果或附加上下文时才使用 tools/post-execute
单调终端轮次策略 从成功的终端工具调用 ToolExecution.concludeTurn();同一响应中后续工具调用仍可由守卫阻止,循环在该步骤后停止
子进程沙箱(landlock / sandbox-exec) 通过 dsh-bash-sandbox 使用 ctx.sandbox 后端;能力级别的拒绝使用 tools/pre-execute
权限系统 / AskUserQuestion tools/pre-execute 返回 ask 并通过 ctx.approval 应答;为普通用户提问注册一个独立的面向模型的 ask 工具
Plan mode @deepseek-ai/dsh-plan-mode:落日志的 plan/mode 状态、plan:policy 引导段、/plan [message] 入口、/plan off 直接退出,以及经用户评审的 exit_plan_mode 出口;强制约束留在独立的沙箱/审批轴上
subagent 委派 ctx.subagents 提供方注册表(dsh-subagent-spawn-in-process/-fork/-acp/-codex/-claude-code/-dsh-sdk)+ dsh-tool-subagent 向模型暴露一个已配置的提供方
MCP 每个服务器一个插件:发现工具 → ctx.tools.register()
skill(技能) section + 工具注册;调用时通过 inject() 注入 skill 内容
记忆 section 提供方 + 工具
定时任务(cron) 插件注册面向模型的调度工具;定时器触发 → 空闲时 followup(…, {source: {kind: 'cron', …}})/忙碌时 inject() 通知
UI(GUI;CLI(命令行界面)输出 JSONL) 监听 session/event(助手分片、边界、工具活动);输入 → followup()
Web Client Chat 业务节点 注册 ConversationNodeDefinitionconversation.chat.node keyed renderer
遥测 / 可回放 trace session/event → JSONL;回放 = sessions.create(id, { seed })
模型适配器 通过 registerAdapter 注册 LlmAdapter 子类(dsh-llm-deepseekdsh-llm-pi-ai
插件热重载 每个注册都是一个 ctx.effect → 随仓库提供的 HMR(热模块替换)直接生效