
DeepSeek Harness 全景解剖:从第一次启动到"一切皆插件"的运行时组装
dsh web 看上去像一个启动网页的命令;真正值得追问的是:源码中为什么找不到一个不可替换的"Web Agent Core"?答案不在 Web UI,而在启动时构成的 Cordis plugin tree。UI 只是其中一个 Bundle 加入的表层,Agent、会话、模型适配、工具、Sandbox 与 Approval 也都以同样的方式进入树。
先把研究对象放对位置
模型会生成文本和 tool-call block;它不保存项目会话、不能自行打开文件,也不拥有操作系统权限。Agent Harness 是两者之间的承载运行时:保存任务事实、组装每次请求可见的 Context 与工具 schema、把调用送往真实执行环境,并在边界处施加策略。因而,浏览器不是 Harness 的定义;换成 Headless runner、SDK server 或 ACP transport,核心问题仍然相同。
在本基线中,dsh 是"profile launcher"。dsh web 是 --profile web 的别名;dsh --profile headless "..." 则装配一条一次性运行的 Headless surface。这一结论来自 apps/cli/src/args.ts,并以 pnpm dsh --help 的实际输出验证。CLI 只消费自己的 profile、patch 与 dump 参数;它遇到未知 token 后,把余下参数原样交给已经装配出的 app plugin。这个分界让新 surface 自己拥有命令行,而不必修改 launcher。
四个层次,四个所有者
Cordis 是此仓库的组装框架:Plugin 在共享 ctx 上提供 service、订阅 typed event、注册 effect;plugin 卸载时注册随 effect 反向撤销。"一切皆插件"不是"所有文件都可选",而是没有一个通过修改 Loop 才能替换的特权核心。
| 实体 | 谁拥有它 | 回答的问题 |
|---|---|---|
| Plugin | 代码包 | 我注册/提供什么能力? |
| Bundle | 可发布包的 dsh.bundle |
我贡献哪一层 Cordis patch? |
| Profile | $DSH_HOME/profiles/<name> |
这次运行选择哪些 Bundle、安装哪些 out-of-tree 依赖? |
| Patch | Profile、home 或命令行 | 对既有 row 做何种最终替换/插入? |
最容易出错的是把 Bundle 当成 Profile。Bundle 是可分发的"贡献层";Profile 是用户启动的命名组合。二者都使用 package.json 中的 dsh 字段,但前者声明 patch 文件,后者声明有序 bundles 列表。Patch 也不是深度 merge:同一 id 的替换应重述该 row 所需的完整 config。这是安全而清晰的配置契约,不是 YAML 技巧。
Boot:从空树到可运行产品
apps/cli/src/profile-boot.ts 中的 composeProfile() 把 Bundle patch、profile patch、home patch 与 --patch overlays 依次组成 row;随后 boot() 在一个空 root config 上装载它。实际优先级为:
#mermaid-svg-P0x96Kl11s6EcN3I{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-P0x96Kl11s6EcN3I .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-P0x96Kl11s6EcN3I .error-icon{fill:#552222;}#mermaid-svg-P0x96Kl11s6EcN3I .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-P0x96Kl11s6EcN3I .marker{fill:#333333;stroke:#333333;}#mermaid-svg-P0x96Kl11s6EcN3I .marker.cross{stroke:#333333;}#mermaid-svg-P0x96Kl11s6EcN3I svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-P0x96Kl11s6EcN3I p{margin:0;}#mermaid-svg-P0x96Kl11s6EcN3I .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-P0x96Kl11s6EcN3I .cluster-label text{fill:#333;}#mermaid-svg-P0x96Kl11s6EcN3I .cluster-label span{color:#333;}#mermaid-svg-P0x96Kl11s6EcN3I .cluster-label span p{background-color:transparent;}#mermaid-svg-P0x96Kl11s6EcN3I .label text,#mermaid-svg-P0x96Kl11s6EcN3I span{fill:#333;color:#333;}#mermaid-svg-P0x96Kl11s6EcN3I .node rect,#mermaid-svg-P0x96Kl11s6EcN3I .node circle,#mermaid-svg-P0x96Kl11s6EcN3I .node ellipse,#mermaid-svg-P0x96Kl11s6EcN3I .node polygon,#mermaid-svg-P0x96Kl11s6EcN3I .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-P0x96Kl11s6EcN3I .rough-node .label text,#mermaid-svg-P0x96Kl11s6EcN3I .node .label text,#mermaid-svg-P0x96Kl11s6EcN3I .image-shape .label,#mermaid-svg-P0x96Kl11s6EcN3I .icon-shape .label{text-anchor:middle;}#mermaid-svg-P0x96Kl11s6EcN3I .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-P0x96Kl11s6EcN3I .rough-node .label,#mermaid-svg-P0x96Kl11s6EcN3I .node .label,#mermaid-svg-P0x96Kl11s6EcN3I .image-shape .label,#mermaid-svg-P0x96Kl11s6EcN3I .icon-shape .label{text-align:center;}#mermaid-svg-P0x96Kl11s6EcN3I .node.clickable{cursor:pointer;}#mermaid-svg-P0x96Kl11s6EcN3I .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-P0x96Kl11s6EcN3I .arrowheadPath{fill:#333333;}#mermaid-svg-P0x96Kl11s6EcN3I .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-P0x96Kl11s6EcN3I .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-P0x96Kl11s6EcN3I .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P0x96Kl11s6EcN3I .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-P0x96Kl11s6EcN3I .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P0x96Kl11s6EcN3I .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-P0x96Kl11s6EcN3I .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-P0x96Kl11s6EcN3I .cluster text{fill:#333;}#mermaid-svg-P0x96Kl11s6EcN3I .cluster span{color:#333;}#mermaid-svg-P0x96Kl11s6EcN3I div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-P0x96Kl11s6EcN3I .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-P0x96Kl11s6EcN3I rect.text{fill:none;stroke-width:0;}#mermaid-svg-P0x96Kl11s6EcN3I .icon-shape,#mermaid-svg-P0x96Kl11s6EcN3I .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P0x96Kl11s6EcN3I .icon-shape p,#mermaid-svg-P0x96Kl11s6EcN3I .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-P0x96Kl11s6EcN3I .icon-shape .label rect,#mermaid-svg-P0x96Kl11s6EcN3I .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P0x96Kl11s6EcN3I .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-P0x96Kl11s6EcN3I .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-P0x96Kl11s6EcN3I :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Profile bundles (listed order)
Profile cordis.patch.yml
$DSH_HOME/cordis.patch.yml
--patch overlays (argv order)
Effective Cordis plugin tree
后层可以替换早层的同 id row;命令行 patch 因此适合作为可复现实验的显式覆盖,而不是偷偷改安装文件。Profile boot 还将 launcher 的环境快照和 app arguments 作为服务提供给树,并监听两个用户 patch 文件以允许 config-only reload。它不理解某个 Agent 的行为。
我在 Linux x86_64 / Node 24.14.0 上执行:
sh
DSH_HOME="$(mktemp -d)" pnpm dsh --profile headless --dump-default-config
命令以 0 退出,输出 333 行。其前部可直接看到 llm、session、agent、session-persistence-jsonl、subprocess、sandbox、sandbox-policy、approval、permission 与工具 rows。这不是"Headless 少了运行时",而是 Headless Bundle 换掉了 surface。第二次以 --patch ../article-1-overlay.yml --dump-config 运行亦以 0 退出,且输出将 tool-web 标记为由该 overlay patch;这验证了层的可观察合成。
dsh web 实际启动什么
最准确的回答不是一个固定对象名称,而是"web Profile 的有效 plugin tree"。dsh-base 给出共同的基础层;dsh-web-app 加入 browser application;dsh-headless 则加入一次性 runner、没有 server。请把 "Web UI" 视作一个可替换 consumer,而不是 Agent Harness 的 owner。
这种设计有一个工程后果:要改变工具、模型、执行 provider 或 UI,先问它属于哪一个 row 和哪一个 service,再决定是在 profile、bundle 还是 patch 改动。修改 dsh web 的启动代码通常是最后选项。
可复现起点
源码路径要求 Node 满足根 package.json 的 engines,使用 pnpm。发布包与本次基线没有同一可核验 git head:registry 的当前包为 0.1.0-rc.6,而冻结源码为 rc.5;因此本文没有将前者的行为冒充为基线证据。本文环境中 node-pty 的原生构建因归档权限失败,故仅验证了不加载 PTY 的 CLI/dump 路径。真实 Web boot 还需要完成官方要求的 build artifacts。
下一篇不再讨论树怎样形成,而问一个更关键的问题:树已经装好后,为什么一句"运行测试"可以在一次 Turn 中发出多次模型请求、持久化多组事实,并最终触及真实环境?
DeepSeek Harness Runtime 深潜:一次 Agent 任务如何从模型推理走向真实世界
用户只说"修复测试",为什么不是一次 completion 就结束?因为 Harness 把"完成一个用户任务"和"发出一次模型请求"分成了 Turn 与 Step。这个区分不是命名偏好,而是日志、重试、工具执行与外部控制能否正确推理的边界。
从 Inbox 到 Turn,再到 Step
Agent 有一个 Inbox。followup() 写入 next-turn 并唤醒 driver;steer() 写入 next-step 并唤醒;inject() 也写入 next-step,但不自行唤醒。ReactLoopAgent 在 agent-loop/src/agent.ts 先持久化 turn/start,原子地 claim 输入,再经过 agent/pre-step waterfall。该 hook 可以拒绝或重写进入模型的 message;因此一个已开始 Turn 可以不花任何 Step 便结束,日志仍能记录尝试。
一个 Step 是一次请求、一次 streaming completion 与其工具批次;同一 Turn 在工具结果或新的 next-step 输入出现时继续下一 Step。核心顺序如下:
Tools LLM Session log Agent Loop Tools LLM Session log Agent Loop #mermaid-svg-6BQ7wjn9D7yw7ciH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6BQ7wjn9D7yw7ciH .error-icon{fill:#552222;}#mermaid-svg-6BQ7wjn9D7yw7ciH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6BQ7wjn9D7yw7ciH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6BQ7wjn9D7yw7ciH .marker.cross{stroke:#333333;}#mermaid-svg-6BQ7wjn9D7yw7ciH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6BQ7wjn9D7yw7ciH p{margin:0;}#mermaid-svg-6BQ7wjn9D7yw7ciH .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6BQ7wjn9D7yw7ciH text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6BQ7wjn9D7yw7ciH .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-6BQ7wjn9D7yw7ciH .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-6BQ7wjn9D7yw7ciH #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-6BQ7wjn9D7yw7ciH .sequenceNumber{fill:white;}#mermaid-svg-6BQ7wjn9D7yw7ciH #sequencenumber{fill:#333;}#mermaid-svg-6BQ7wjn9D7yw7ciH #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-6BQ7wjn9D7yw7ciH .messageText{fill:#333;stroke:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6BQ7wjn9D7yw7ciH .labelText,#mermaid-svg-6BQ7wjn9D7yw7ciH .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .loopText,#mermaid-svg-6BQ7wjn9D7yw7ciH .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6BQ7wjn9D7yw7ciH .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-6BQ7wjn9D7yw7ciH .noteText,#mermaid-svg-6BQ7wjn9D7yw7ciH .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6BQ7wjn9D7yw7ciH .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6BQ7wjn9D7yw7ciH .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6BQ7wjn9D7yw7ciH .actorPopupMenu{position:absolute;}#mermaid-svg-6BQ7wjn9D7yw7ciH .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-6BQ7wjn9D7yw7ciH .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6BQ7wjn9D7yw7ciH .actor-man circle,#mermaid-svg-6BQ7wjn9D7yw7ciH line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-6BQ7wjn9D7yw7ciH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} turn/start, step/start, user/messageassembled requestassistant chunks / messageassistant/message, tool/callguarded execution pipelinefinalized resulttool/result, step/endnext Step when work is owedturn/end
仓库的 JSON-RPC Bash snapshot 给出可核验的具体轨迹:turn/start(1) 与 step/start(1,1) 后,模型产生 bash call;tool/call 的 seq 63 在执行前记录,tool/result 的 seq 64 用 sourceEventSeqs:[63] 链接它;随后 step/end,同一 Turn 的 step/start(1,2) 再请求模型,最终 turn/end。这证明"一条任务 = 一个模型调用"是错误的运行时模型。该轨迹是当前测试 fixture 证据,而非本文环境向外部模型的实测。
SessionEvent 为什么是事实来源
Session 是 append-only log;deriveMessages() 只从带 surfaceOp 的 message-producing event 折叠出模型历史。chunk、turn boundary 这类 raw 事件留在日志中以服务重放/UI,却不会被误塞回 prompt。Loop 在请求前调用 this.session.deriveMessages();可选 invariant 又从 log 重建请求边界。由此得到仓库明确的不变量:Model-visible means logged。
这带来一个很实际的设计约束:若你的 plugin 想向模型增加持久上下文,不应偷改内存 messages array;应定义并记录相应的 SessionEvent,再从日志投影。这样 resume、fork、transcript、telemetry 与 UI 都可以从同一事实流工作。反过来,实时状态应在 agent/* 事件表达,不能误称为会话事实。
Tool pipeline:日志先于副作用
工具并非模型直接调用 shell。Loop 先把模型生成的调用持久化,再让 ctx.tools 驱动 pipeline:
#mermaid-svg-MsnotyhtH0EGCnXd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MsnotyhtH0EGCnXd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MsnotyhtH0EGCnXd .error-icon{fill:#552222;}#mermaid-svg-MsnotyhtH0EGCnXd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MsnotyhtH0EGCnXd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MsnotyhtH0EGCnXd .marker.cross{stroke:#333333;}#mermaid-svg-MsnotyhtH0EGCnXd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MsnotyhtH0EGCnXd p{margin:0;}#mermaid-svg-MsnotyhtH0EGCnXd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MsnotyhtH0EGCnXd .cluster-label text{fill:#333;}#mermaid-svg-MsnotyhtH0EGCnXd .cluster-label span{color:#333;}#mermaid-svg-MsnotyhtH0EGCnXd .cluster-label span p{background-color:transparent;}#mermaid-svg-MsnotyhtH0EGCnXd .label text,#mermaid-svg-MsnotyhtH0EGCnXd span{fill:#333;color:#333;}#mermaid-svg-MsnotyhtH0EGCnXd .node rect,#mermaid-svg-MsnotyhtH0EGCnXd .node circle,#mermaid-svg-MsnotyhtH0EGCnXd .node ellipse,#mermaid-svg-MsnotyhtH0EGCnXd .node polygon,#mermaid-svg-MsnotyhtH0EGCnXd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MsnotyhtH0EGCnXd .rough-node .label text,#mermaid-svg-MsnotyhtH0EGCnXd .node .label text,#mermaid-svg-MsnotyhtH0EGCnXd .image-shape .label,#mermaid-svg-MsnotyhtH0EGCnXd .icon-shape .label{text-anchor:middle;}#mermaid-svg-MsnotyhtH0EGCnXd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MsnotyhtH0EGCnXd .rough-node .label,#mermaid-svg-MsnotyhtH0EGCnXd .node .label,#mermaid-svg-MsnotyhtH0EGCnXd .image-shape .label,#mermaid-svg-MsnotyhtH0EGCnXd .icon-shape .label{text-align:center;}#mermaid-svg-MsnotyhtH0EGCnXd .node.clickable{cursor:pointer;}#mermaid-svg-MsnotyhtH0EGCnXd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MsnotyhtH0EGCnXd .arrowheadPath{fill:#333333;}#mermaid-svg-MsnotyhtH0EGCnXd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MsnotyhtH0EGCnXd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MsnotyhtH0EGCnXd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MsnotyhtH0EGCnXd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MsnotyhtH0EGCnXd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MsnotyhtH0EGCnXd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MsnotyhtH0EGCnXd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MsnotyhtH0EGCnXd .cluster text{fill:#333;}#mermaid-svg-MsnotyhtH0EGCnXd .cluster span{color:#333;}#mermaid-svg-MsnotyhtH0EGCnXd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MsnotyhtH0EGCnXd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MsnotyhtH0EGCnXd rect.text{fill:none;stroke-width:0;}#mermaid-svg-MsnotyhtH0EGCnXd .icon-shape,#mermaid-svg-MsnotyhtH0EGCnXd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MsnotyhtH0EGCnXd .icon-shape p,#mermaid-svg-MsnotyhtH0EGCnXd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MsnotyhtH0EGCnXd .icon-shape .label rect,#mermaid-svg-MsnotyhtH0EGCnXd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MsnotyhtH0EGCnXd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MsnotyhtH0EGCnXd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MsnotyhtH0EGCnXd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} assistant tool-call block
persist tool/call
tools/pre-execute waterfall
monotonic guards
tools/execute waterfall + body
tools/post-execute waterfall
finalizeContent + tools/result
persist tool/result
tools/pre-execute、tools/execute、tools/post-execute 都是 waterfall:listener 必须以 next() 委托后续链,否则它是在有意短路。pre 可 allow/deny/ask;随后 registered guard 只能收紧、不能放宽既有 owner policy;execute 可包裹调度(timeout、metrics 等);post 可接受、阻断、替换输出或添加 context。最终 finalizeContent 强制 content-only 合约,tools/result 只观察冻结的权威结果。335 项 CLI/Agent Loop 测试在本环境通过,其中 tool invariant 明确检查这三个阶段的次序。
Capability Seam 与 Execution World
所谓 Capability Seam 不是"有一个 interface"就成立。完整的 seam 有三角:Definition 声明 ctx service 合约;Provider 实现它;Consumer(常见为模型可见 Tool)只依赖合约。文件系统、subprocess、sandbox、shell/PTY 的关键含义在于:多个 consumer 可共享一个 execution world。替换 fs/subprocess provider 为远端 sandbox,并不自动迁移 session 或模型;它只移动由该 provider 承担的文件/进程操作。
这也是为何"新增能力"要先问:谁定义、谁提供、谁消费、什么会持久化?只写一个 Tool body 而没有稳定 provider 边界,往往是在制造不可替换的耦合。
安全边界不能合并成一个词
| 层 | 控制什么 | 不控制什么 |
|---|---|---|
| Tool exposed to model | 模型能请求哪些能力 | 请求是否批准、OS 是否允许 |
| Permission policy | 预设把 mode 映射到策略 | 它不是内核隔离 |
| Approval | 某次 ask 的人类决定 | 不是长期 OS capability |
| Sandbox | provider 对文件/进程的约束 | 当前 SandboxMode 不表示网络/进程政策 |
| OS / credentials | 进程实际拥有的权限与密钥 | Harness 不能凭空收回宿主已授予的一切 |
默认 headless config 在本次 dump 中将 workspace-write 配为 sandbox workspace-write + approval ask,danger-full-access 配为 approval never。这描述 composition,而不是"有 Sandbox 就安全"。Python minimal example 明确使用 danger-full-access,只应在一次性 checkout/container 中运行。
本地没有 API credential,故真实模型 + tool 的端到端路径未 runtime-verified;不要把 snapshot 或源码推断写成"实测模型"。但执行顺序、日志事实与 hook 合约由架构文档、实现、fixture 与通过的测试共同覆盖。
下一篇把这些稳定边界变成行动准则:新增一个能力时何时写 Plugin、何时写 Adapter,以及怎样把同一个 runtime 放进脚本、服务或编辑器生态。
从理解到掌控:扩展 DeepSeek Harness,并将 Agent Runtime 嵌入真实工程系统
如果要增加一个模型工具,为什么不直接在 Agent Loop 的 if 分支里接入?因为 Loop 的职责是驱动 Turn/Step;把某个业务能力塞进去,会让每一个 product composition 都承受它的依赖、权限与失败语义。仓库的边界更严格:新行为通过 Plugin、service、event 或 Tool registry 进入;Loop 不为它改形状。
先选择扩展点
| 需求 | 首选机制 | 不应误用为 |
|---|---|---|
| 新模型可见能力 | Tool Plugin 注册到 ctx.tools |
修改 Agent Loop |
| 新文件/进程实现 | Capability Provider | 复制每个 Tool |
| 拦截一次 tool call | tools/* waterfall / guard |
在 Tool body 中硬编码全局政策 |
| 新 provider 协议 | LLM Adapter | 仅填一个 provider 名称 |
| 新可安装产品层 | Bundle | Profile 本身 |
| 非交互运行 | Headless / SDK server | Web UI 自动化 |
一个最小 plugin 只是导出 apply(ctx);Cordis 在加载时调用它。注册返回的是 effect,plugin unload 后相应注册撤销。这是 out-of-tree code 可以与 in-tree package 同等参与树的原因。生产插件不应只 console.log:它至少要明确 inject 依赖、注册何种能力、失败时是否阻断调用、以及是否需要 durable event。
Tool:把业务副作用放进已存在的通道
官方 adding-a-tool 指南要求通过 defineTool/ctx.tools 注册 definition。definition 提供可供模型 schema 化的参数、execute 的 canonical value,以及 model-facing output.render;UI card 则是独立的纯 presentation projection。不要在 presenter 中读文件、取时间或做 I/O:它还会在 Session log replay 中运行。
ts
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'project-metadata-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'project_metadata',
description: 'Return controlled project metadata.',
// Parameter/output schemas must follow the baseline tool cookbook.
async execute(_args, _signal) {
return { value: { revision: 'replace-with-real-provider' } }
},
}))
}
这是结构示意,不能原样当成可运行文件:当前版本的 schema 与 return contract 需从 docs/cookbook/adding-a-tool.md 复制完整定义,并为输入、拒绝、执行错误与输出固定测试。真正的 non-toy case 是"受控项目元数据":Tool 只消费一个注入的 metadata provider,权限/approval 仍在通用 pipeline,持久化仍由 Loop 的 tool/call/tool/result 完成。这样没有新增一个绕开审计的直接 shell 通道。
Bundle 是分发,Profile 是选择
当 plugin 要交给别人安装,写 Bundle:包内 dsh.bundle 指向 cordis.patch.yml,该 patch insert plugin rows。用户再以 dsh plugin --profile demo add <package> 将 Bundle 放入该 Profile 的有序列表。Profile 仍可用自己的 cordis.patch.yml 覆盖 Bundle,home 与 --patch 层仍在其上。Git 安装与 npm 包不同:Git 安装的是 source,若其 prepare script 获允会在用户机器执行代码;应只信任源码并 pin commit,或分发已构建 tarball/npm artifact。
Provider config 不等于 LLM Adapter
"换模型"有两种完全不同的改动。若已注册 Adapter 认识新 provider route/model id,只需要 composition 中的 provider/model/credential config。若需把 Harness 的 GenerateOptions 翻译为一种新上游协议,则实现 LlmAdapter.stream(),将 provider response 还原为严格 StreamChunk 序列,再调用 ctx.llm.registerAdapter()。Adapter 必须遵守 usage 在 finish 前、finish 后无 chunk、tool arguments 保持 raw JSON、错误转为稳定 code、传播 abort signal 等契约。只改 DEEPSEEK_BASE_URL 并不会实现另一种 streaming dialect。
选择嵌入边界
#mermaid-svg-G072Bm0mR3XVRmPD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-G072Bm0mR3XVRmPD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-G072Bm0mR3XVRmPD .error-icon{fill:#552222;}#mermaid-svg-G072Bm0mR3XVRmPD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-G072Bm0mR3XVRmPD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-G072Bm0mR3XVRmPD .marker.cross{stroke:#333333;}#mermaid-svg-G072Bm0mR3XVRmPD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-G072Bm0mR3XVRmPD p{margin:0;}#mermaid-svg-G072Bm0mR3XVRmPD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-G072Bm0mR3XVRmPD .cluster-label text{fill:#333;}#mermaid-svg-G072Bm0mR3XVRmPD .cluster-label span{color:#333;}#mermaid-svg-G072Bm0mR3XVRmPD .cluster-label span p{background-color:transparent;}#mermaid-svg-G072Bm0mR3XVRmPD .label text,#mermaid-svg-G072Bm0mR3XVRmPD span{fill:#333;color:#333;}#mermaid-svg-G072Bm0mR3XVRmPD .node rect,#mermaid-svg-G072Bm0mR3XVRmPD .node circle,#mermaid-svg-G072Bm0mR3XVRmPD .node ellipse,#mermaid-svg-G072Bm0mR3XVRmPD .node polygon,#mermaid-svg-G072Bm0mR3XVRmPD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-G072Bm0mR3XVRmPD .rough-node .label text,#mermaid-svg-G072Bm0mR3XVRmPD .node .label text,#mermaid-svg-G072Bm0mR3XVRmPD .image-shape .label,#mermaid-svg-G072Bm0mR3XVRmPD .icon-shape .label{text-anchor:middle;}#mermaid-svg-G072Bm0mR3XVRmPD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-G072Bm0mR3XVRmPD .rough-node .label,#mermaid-svg-G072Bm0mR3XVRmPD .node .label,#mermaid-svg-G072Bm0mR3XVRmPD .image-shape .label,#mermaid-svg-G072Bm0mR3XVRmPD .icon-shape .label{text-align:center;}#mermaid-svg-G072Bm0mR3XVRmPD .node.clickable{cursor:pointer;}#mermaid-svg-G072Bm0mR3XVRmPD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-G072Bm0mR3XVRmPD .arrowheadPath{fill:#333333;}#mermaid-svg-G072Bm0mR3XVRmPD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-G072Bm0mR3XVRmPD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-G072Bm0mR3XVRmPD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G072Bm0mR3XVRmPD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-G072Bm0mR3XVRmPD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G072Bm0mR3XVRmPD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-G072Bm0mR3XVRmPD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-G072Bm0mR3XVRmPD .cluster text{fill:#333;}#mermaid-svg-G072Bm0mR3XVRmPD .cluster span{color:#333;}#mermaid-svg-G072Bm0mR3XVRmPD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-G072Bm0mR3XVRmPD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-G072Bm0mR3XVRmPD rect.text{fill:none;stroke-width:0;}#mermaid-svg-G072Bm0mR3XVRmPD .icon-shape,#mermaid-svg-G072Bm0mR3XVRmPD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G072Bm0mR3XVRmPD .icon-shape p,#mermaid-svg-G072Bm0mR3XVRmPD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-G072Bm0mR3XVRmPD .icon-shape .label rect,#mermaid-svg-G072Bm0mR3XVRmPD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G072Bm0mR3XVRmPD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-G072Bm0mR3XVRmPD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-G072Bm0mR3XVRmPD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 谁驱动 runtime?
CLI one-shot
Own process
External agent client
Web client business call
dsh --profile headless
TypeScript/Python SDK over stdio JSON-RPC
ACP server
Typert API Gateway
- Headless :
dsh --profile headless "job"创建 fresh persisted session、打印最终文本并退出;适合 shell automation。 - SDK JSON-RPC :TypeScript
DeepSeekHarness或 PythonDeepSeekHarness管理一个子进程。客户端只驱动 runtime;cordis.yml仍拥有组成。Python SDK 文档要求 Python 3.10+、受支持平台与可修改的隔离 workspace;其 bundled runtime 不要求系统 Node。 - ACP:面向 Agent Client Protocol 的 automation server。它创建 fresh agents,走 stdin/stdout JSON-RPC,只发 committed assistant text,并可处理 bridge-owned one-shot permission request;不等于 UI、transcript 或 editor protocol。
- API Gateway :Web host/client 的 unary Remote service 层。
@Remote/@RemoteScope描述可跨 connection/api调用的方法;session event stream 不应伪装为 Remote unary call。
这四条路可并存:它们是入口/transport,不是对同一 capability 的重复实现。SDK 进程关闭、ACP client disconnect 与 Cordis unload 都必须归结到明确的 agent/session dispose 生命周期。
上线前的最小检查
- 指出 capability 的 Definition、Provider、Consumer 与 owner;不要只贴 Tool 名字。
- 为模型可见或可重放事实设计
SessionEvent,而非临时数组。 - 明确 Tool exposure、policy、approval、sandbox、OS/credential 的五层责任。
- 固定
cordis.yml、plugin 版本、cwd、credential 来源与日志目录;危险 composition 只在 disposable 环境运行。 - 用真实或 mock provider 跑 tool case,读取 JSONL 的 call/result/Step;没有 credential 时只报告 source/test verification。
在本研究容器中,本地 out-of-tree plugin 的真实长驻 boot 和 Python SDK 未完成 runtime verification:前者受 source build/PTY 原生依赖限制,后者还需要 credential 与 bundled wheel。本文不以教程文本替代实验。已经验证的部分是 CLI composition dump 以及 335 项核心测试;其余结论按照文档/源码等级陈述。
读到这里,面对陌生 package 的正确起手式不是"它是不是核心",而是:它定义什么 capability?谁提供?谁消费?何时运行?什么持久化?哪些 hook 能替换它?失败落在哪里?这正是把 Harness 从"能运行"变成"能嵌入、能修改"的方法。