Files
deepseek-harness/docs/cookbook/extension-cookbook.zh.md
Turtle f290a8b851 refactor(cli)!: one shared base config with per-surface overlays
`dsh` shipped two config trees that were 43 rows the same: apps/cli/cordis.yml
composed web as 74 flat rows, while the TUI booted examples/tui-agent/cordis.yml
whose single `@deepseek-ai/dsh-tui-demo` row mounted twelve plugins behind a
twenty-key pass-through Config. Neither file was what its location claimed —
apps/cli hardcoded the "example" as the product default and the "demo" bundle
was the application — and every capability change had to be made twice.

- apps/cli/base.cordis.yml holds the 43 shared rows; tui.cordis.yml and
  web.cordis.yml are patch lists stating only what differs per surface
- overlays apply as SIBLING patch lists at one include level, because include
  patches never cross an include boundary. Precedence: base < surface <
  (--config | personal ~/.dsh/config.yaml) < launcher flag/profile patches
- `--config` now applies an overlay INSTEAD OF the personal one, so a demo or
  test tree never inherits the user's route; new `--config-replace` boots a file
  as the entire tree (the old `--config` behaviour). Both survive /resume
- vendor/include: index each `insert`ed row as it is added so a later patch can
  configure or disable it. Upstream built the id index once before the patch
  loop, leaving every surface-only row — the whole TUI front door — silently
  unpatchable from user config. Logged as local modification 8
- session identity moves to dsh-agent-loop's CONFIGURED_AGENT_IDENTITIES_KEY;
  dsh-tui's MAIN_SESSION_ID_KEY is deleted (only the bundle read it)
- delete examples/tui-agent, examples/cordis-agent, packages/examples/tui-demo;
  TUI tests → apps/cli/tests, cordis e2e → packages/cordis/tool-cordis/tests,
  examples/code-mode survives as an overlay leaf
- `dsh web` gains --config, threaded into AppCLIEntry as an extra overlay

Three latent defects surfaced and are fixed here: the TUI captured the optional
sessionQuery service once at construction and could permanently disable /resume
when it won the mount race; the session-store root silently reverted to a
project-local ./.sessions; --config-replace was dropped by the resume handoff.

Verified by booting each tree through the real Loader (TUI 55 entries, web 75,
zero unsettled) rather than reading YAML. All eight terminal snapshots replay
byte-identically; 14/14 PTY smoke, 112/112 snapshots, 25/25 doc-sync, hygiene
and lint clean.
2026-07-29 21:15:42 +08:00

10 KiB
Raw Blame History

实操手册:扩展插件形态

English | 中文

FIXME这篇重要指南尚未经过充分的人工设计审查请在首次发布前完成审查。

针对 harness 扩展表面编写的三种插件形态,以示意性代码片段呈现(省略了 import 和辅助桩——不可直接复制运行)。完整的分步指南见添加包package添加工具添加 LLM大语言模型适配器;这些插件所挂接的 seam 见 docs/architecture.md

工具插件

工具在 ctx.tools 上注册。带注解的 defineTool 示例(类型化的 execute 参数、结果塑形、run_in_background 模式)见 adding-a-tool.md——该指南是工具形态的真源。ctx.tools.register() 也直接接受原始 JSON-Schema ToolDefinitionMCP 来源的工具就是这样到达的);defineTool 是为第一方工具提供的类型化语法糖。

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

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

import type { Context } from '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() 将输入驱动回去。

import type { Context } from '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(),并根据持久的 turn/end 对每个请求恰好结算一次。通过 AgentHandle.dispose() 拆除 agent以使 dispose资源释放达到完全停稳。

packages/acp/acp 是仅面向自动化的完整示例:它通过 ACPAgent Client ProtocolJSON-RPC stdio 提供全新文本会话,发出已提交的助手文本,并为其拥有的 agent 注册一次性机器权限应答器。其 README 拥有精确的方法和生命周期契约。

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).
}

可运行的组装示例

可运行叶子从 examples/*/cordis.yml 加载各自的插件树;根目录的 demo:* 脚本和这些叶子目录是权威清单。交互式叶子使用 @deepseek-ai/dsh-tui,非交互式叶子使用 @deepseek-ai/dsh-cli-demoACP 叶子使用 @deepseek-ai/dsh-acp-demo,应用包共享 @deepseek-ai/dsh-agent-spine-demo

功能→机制映射

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

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

产品功能 插件机制
钩子系统(用户级 + 项目级) agent/session-startagent/prompt-submitagent/requesttools/pre-executetools/post-executeagent/turn-stopping 上的监听器waterfall seam 返回类型化决策,agent/turn-stopping 则可通过 steering 触发下一步;dsh-hooks-claude / dsh-hooks-codex 桥接器将钩子配置文件映射到这些 seam 上
/goal ctx.goals 管理持久状态,dsh-goal-session 通过公共 Agent 调度同会话回合,独立的命令/工具生产方分别提供人类/模型控制
/loop turn/end 会话事件上 followup() 下一次迭代;或强制继续
动态工作流 ctx.workflows + worker-thread 引擎 + workflow 工具;结构化的进程内子任务通过作用域化的 prompt/工具注册、单调工具守卫、最终 tools/result 提交(包括外层 run_code)和结构化输出执行的单调 concludeTurn() 标记来强制输出
排队消息 + steering中途引导 核心 Agent.followup() / Agent.steer()
上下文压缩context compaction自动 + 手动) ctx.compact seam + dsh-compact-basic;自动压力检查运行在串行 agent/step,规范化溢出恢复运行在 agent/request-error,手动调用方使用同一个压缩服务(压缩 Agent Note——面向模型的 /compact 消费方工具已推迟)
系统提示词可配置性 ctx.systemPrompt.section(),支持排序与作用域局部覆盖
AGENTS.md根目录 一个读取该文件的 section provider
AGENTS.md子目录按需触发+ 文件变更通知 从 watcher / tool-result 监听器调用 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 出口;强制约束留在独立的沙箱/审批轴上
子 agent 委派 ctx.subagents 提供方注册表(dsh-subagent-spawn/-fork/-acp+ dsh-tool-subagent 向模型暴露一个已配置的提供方
MCP 每个服务器一个插件:发现工具 → ctx.tools.register()
Skill技能 section + 工具注册;调用时通过 inject() 注入 skill 内容
记忆 section provider + 工具
定时任务cron 插件注册面向模型的调度工具;定时器触发 → 空闲时 followup(…, {source: {kind: 'cron', …}})/忙碌时 inject() 通知
UIGUICLI 输出 JSONL 监听 session/event(助手分片、边界、工具活动);输入 → followup()
遥测 / 可回放 trace session/event → JSONL回放 = sessions.create(id, { seed })
模型适配器 通过 registerAdapter 注册 LlmAdapter 子类(dsh-llm-deepseekdsh-llm-pi-ai
插件热重载 每个注册都是一个 ctx.effect → vendor 的 HMR热模块替换直接生效