DeepSeek Harness:把 Agent 做成可替换的运行时插件树

DeepSeek Harness 把 agent 运行时拆成一套可替换、可恢复、可审计、可产品化的插件系统。README 明确说它的核心架构是 "everything is a plugin",基于 Cordis,且当前仍是 developer preview,会有破坏性变更:README.md:L5-L23

它解决的问题是"怎么做一个可替换、可审计、可恢复、可分发的 agent 运行时"。模型、工具、会话日志、沙箱、审批、Web UI、SDK、子代理、工作流都被拆进同一棵 Cordis 插件树里;同一套 agent 能用 Web、headless、SDK、ACP 等入口运行,能力也能通过 profile、bundle、patch、preset 替换。

整体结构

DeepSeek Harness 可以先按三层理解。

第一层是启动与组合层。dsh 不直接启动一个写死的应用,而是读取 profile,把 bundle patch、用户 cordis.patch.yml、home patch、--patch overlay 和 telemetry 开关叠成最终 Cordis 配置树,然后 boot:profile-boot.ts:L1-L10profile-boot.ts:L131-L170。架构文档也明确说,dsh-base 是每个 profile 的第一层,提供 model adapters、tools、persistence、sandbox、approval policy、settings、credentials、telemetry;dsh-web-appdsh-headless 再分别叠出浏览器应用或一次性 runner:architecture.md:L15-L37

第二层是 agent 核心控制层。核心包分工清楚:session 管 append-only 事件日志,system-prompt 管 prompt section 和 tool schema 组装,tools 管工具注册与执行管线,agent 管 Agent 接口和 live event,agent-loop 是默认驱动,llm 是模型适配缝:architecture.md:L39-L51。一次交互被拆成 turn 和 step:领取 inbox 输入,组装 prompt 和工具 schema,经 agent/pre-step 决定是否进入模型请求,再 llm/stream,再执行工具,最后落 durable session events:architecture.md:L63-L90

第三层是能力扩展层。新增能力不应该塞进 loop,而是挂到明确 extension point 上:新增模型 provider 注册到 ctx.llm,新增模型可见能力注册到 ctx.tools,新增 UI 节点注册 ConversationNode,新增持久状态扩展 SessionEventMap,新增沙箱能力注册 ctx.sandbox backend:architecture.md:L104-L127。项目把这套扩展方式称为 capability seam:一个 seam 不只是 provider,而是 Service Definition、Service Provider、Consumer 三个角色一起设计:architecture.md:L98-L102

整体结构更像一张服务依赖网,而不是一条线性流水线。dsh 和 profile 负责装配,AgentLoop 是协调者;SystemPromptToolsLLMSession 都是被它调用的服务面,同时这些服务之间也有横向关系,例如 tool schema 会进入 prompt assembly,session event 会被 Web、SDK、persistence 消费。
#mermaid-svg-GnPzConMtTzeUxe1{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-GnPzConMtTzeUxe1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GnPzConMtTzeUxe1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GnPzConMtTzeUxe1 .error-icon{fill:#552222;}#mermaid-svg-GnPzConMtTzeUxe1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GnPzConMtTzeUxe1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GnPzConMtTzeUxe1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GnPzConMtTzeUxe1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GnPzConMtTzeUxe1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GnPzConMtTzeUxe1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GnPzConMtTzeUxe1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GnPzConMtTzeUxe1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GnPzConMtTzeUxe1 .marker.cross{stroke:#333333;}#mermaid-svg-GnPzConMtTzeUxe1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GnPzConMtTzeUxe1 p{margin:0;}#mermaid-svg-GnPzConMtTzeUxe1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GnPzConMtTzeUxe1 .cluster-label text{fill:#333;}#mermaid-svg-GnPzConMtTzeUxe1 .cluster-label span{color:#333;}#mermaid-svg-GnPzConMtTzeUxe1 .cluster-label span p{background-color:transparent;}#mermaid-svg-GnPzConMtTzeUxe1 .label text,#mermaid-svg-GnPzConMtTzeUxe1 span{fill:#333;color:#333;}#mermaid-svg-GnPzConMtTzeUxe1 .node rect,#mermaid-svg-GnPzConMtTzeUxe1 .node circle,#mermaid-svg-GnPzConMtTzeUxe1 .node ellipse,#mermaid-svg-GnPzConMtTzeUxe1 .node polygon,#mermaid-svg-GnPzConMtTzeUxe1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GnPzConMtTzeUxe1 .rough-node .label text,#mermaid-svg-GnPzConMtTzeUxe1 .node .label text,#mermaid-svg-GnPzConMtTzeUxe1 .image-shape .label,#mermaid-svg-GnPzConMtTzeUxe1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-GnPzConMtTzeUxe1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GnPzConMtTzeUxe1 .rough-node .label,#mermaid-svg-GnPzConMtTzeUxe1 .node .label,#mermaid-svg-GnPzConMtTzeUxe1 .image-shape .label,#mermaid-svg-GnPzConMtTzeUxe1 .icon-shape .label{text-align:center;}#mermaid-svg-GnPzConMtTzeUxe1 .node.clickable{cursor:pointer;}#mermaid-svg-GnPzConMtTzeUxe1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GnPzConMtTzeUxe1 .arrowheadPath{fill:#333333;}#mermaid-svg-GnPzConMtTzeUxe1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GnPzConMtTzeUxe1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GnPzConMtTzeUxe1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GnPzConMtTzeUxe1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GnPzConMtTzeUxe1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GnPzConMtTzeUxe1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GnPzConMtTzeUxe1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GnPzConMtTzeUxe1 .cluster text{fill:#333;}#mermaid-svg-GnPzConMtTzeUxe1 .cluster span{color:#333;}#mermaid-svg-GnPzConMtTzeUxe1 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-GnPzConMtTzeUxe1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GnPzConMtTzeUxe1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-GnPzConMtTzeUxe1 .icon-shape,#mermaid-svg-GnPzConMtTzeUxe1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GnPzConMtTzeUxe1 .icon-shape p,#mermaid-svg-GnPzConMtTzeUxe1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GnPzConMtTzeUxe1 .icon-shape .label rect,#mermaid-svg-GnPzConMtTzeUxe1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GnPzConMtTzeUxe1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GnPzConMtTzeUxe1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GnPzConMtTzeUxe1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-GnPzConMtTzeUxe1 .boot>*{fill:#eef2ff!important;stroke:#6366f1!important;color:#312e81!important;}#mermaid-svg-GnPzConMtTzeUxe1 .boot span{fill:#eef2ff!important;stroke:#6366f1!important;color:#312e81!important;}#mermaid-svg-GnPzConMtTzeUxe1 .boot tspan{fill:#312e81!important;}#mermaid-svg-GnPzConMtTzeUxe1 .drive>*{fill:#fef3c7!important;stroke:#d97706!important;color:#7c2d12!important;}#mermaid-svg-GnPzConMtTzeUxe1 .drive span{fill:#fef3c7!important;stroke:#d97706!important;color:#7c2d12!important;}#mermaid-svg-GnPzConMtTzeUxe1 .drive tspan{fill:#7c2d12!important;}#mermaid-svg-GnPzConMtTzeUxe1 .core>*{fill:#ecfdf5!important;stroke:#059669!important;color:#064e3b!important;}#mermaid-svg-GnPzConMtTzeUxe1 .core span{fill:#ecfdf5!important;stroke:#059669!important;color:#064e3b!important;}#mermaid-svg-GnPzConMtTzeUxe1 .core tspan{fill:#064e3b!important;}#mermaid-svg-GnPzConMtTzeUxe1 .side>*{fill:#f1f5f9!important;stroke:#64748b!important;color:#334155!important;}#mermaid-svg-GnPzConMtTzeUxe1 .side span{fill:#f1f5f9!important;stroke:#64748b!important;color:#334155!important;}#mermaid-svg-GnPzConMtTzeUxe1 .side tspan{fill:#334155!important;} 旁路消费
核心服务
协调层
装配层
装配
prepare and append
assemble
stream
execute
policy
contribute schemas
guard
assistant output
tool result
session event
session event
session event
装配
dsh CLI
profile and patch layers
Cordis plugin tree
Agent registry
AgentLoop service
SystemPrompt service
Tools registry
LLM service
SessionStore service
Sandbox and approval
Persistence and query
Web UI
SDK and automation

插件化到底扩展了什么

"loop、session 也是插件"容易误解。准确说法是:单个 loop 实例或单个 session 对象不是插件;提供 loop 能力和 session 能力的服务是 Cordis 插件

这里的"插件"可以理解成两件事:一是定义一个可被其它模块使用的服务面,例如 ctx.sessionsctx.agentLoopctx.agents;二是注册一组运行时行为,例如监听事件、发出事件、提供 factory、参与装配和卸载。它不等于"完全没有依赖"。DeepSeek Harness 里的 session 和 loop 是运行时强相关的:没有 session log,loop 没法生成可恢复的模型上下文;没有 loop,session 也只是一个事件日志容器。但它们不是互相把实现写死在一起,而是通过服务接口和事件协议连接。

比如默认 bundle 里,@deepseek-ai/dsh-session@deepseek-ai/dsh-agent-loop 都只是 cordis.patch.yml 里的插件 row:base/cordis.patch.yml:L24-L29base/cordis.patch.yml:L434-L439。这意味着它们不是被硬编码进主程序的全局单例,而是和 LLM、tools、sandbox、persistence 一样,通过 Cordis 装配、注入、卸载。

session 的实现方式是:SessionStore extends Service,构造时把自己注册为 ctx.sessionssession/src/index.ts:L786-L797。单个 Sessionctx.sessions.create()prepare()enter()announce() 创建和发布出来的运行对象,不是插件本身。Session.append() 负责把事件写入 append-only log,并通过 session/event 通知订阅者:session/src/index.ts:L570-L648。持久化没有写死在 SessionStore 里,源码注释明确说 persistence 是插件职责:订阅 session/event,在 session/flush 或 dispose 时落盘:session/src/index.ts:L1-L4session/src/index.ts:L786-L790

loop 的实现方式类似。AgentLoop extends Service implements AgentFactory,并显式声明自己依赖 agentssessionsllmtoolssystemPromptagent-loop/src/index.ts:L295-L298。构造时它把自己注册成 ctx.agents 的 factory,并给 system prompt 注入 providermodelcwd 变量:agent-loop/src/index.ts:L319-L353。真正跑 turn/step 的是它创建出来的 ReactLoopAgent,这个对象持有一个 Session,并把 agent 绑定到 scoped context:agent-loop/src/agent.ts:L63-L97

所以它们的关系不是"完全解耦",而是"职责分离"。SessionStore 不知道模型、工具、prompt 怎么运行,它只保证事件日志、surface projection、session lifecycle 和 session/event firehose。AgentLoop 知道如何推进 turn/step,但它不自己实现 session 存储,也不自己实现持久化、模型适配、工具注册、prompt 组装;这些都从 ctx.sessionsctx.llmctx.toolsctx.systemPrompt 取。loop 对 session 有依赖,session 对 loop 没有依赖;其它能力通过监听 session/eventagent/* 事件接入。这就是"耦合在协议上,而不是耦合在实现上"。

把一轮 agent 请求拆开看,能看到各个插件具体怎么插拔。

关键点是:AgentLoop 当然要和 LLM、tools、session 互动,它就是协调者 。插件化不是让这些模块彼此不知道对方存在,而是把互动固定在 ctx.* 服务接口上。主路径上,AgentLoop 会主动调用 ctx.sessionsctx.systemPromptctx.llmctx.tools;旁路上,session event 再被 persistence、UI、telemetry、projection 消费。

system-prompttools 的关系尤其容易误读。模型请求里的 prompt 必须包含"模型能用哪些工具"的信息,否则模型无法合法地产生 tool call;但 tool schema 的所有权不在 system-prompt,而在 tools registry。真实边界是:工具插件向 ctx.tools 注册 schema 和 executor,tools registry 再通过 ctx.systemPrompt.tools() 把当前 scope 可见的 schema 贡献给 prompt assembly。system-prompt 负责把 persona、runtime context、prompt variables、tool schemas 组装成一次模型请求的 PromptAssembly;它可以单独开发组装机制,但不能脱离 tool schema 这个输入面来理解模型请求。
#mermaid-svg-FcofoEZsDgDxLaeQ{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-FcofoEZsDgDxLaeQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FcofoEZsDgDxLaeQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FcofoEZsDgDxLaeQ .error-icon{fill:#552222;}#mermaid-svg-FcofoEZsDgDxLaeQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FcofoEZsDgDxLaeQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FcofoEZsDgDxLaeQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FcofoEZsDgDxLaeQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FcofoEZsDgDxLaeQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FcofoEZsDgDxLaeQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FcofoEZsDgDxLaeQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FcofoEZsDgDxLaeQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FcofoEZsDgDxLaeQ .marker.cross{stroke:#333333;}#mermaid-svg-FcofoEZsDgDxLaeQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FcofoEZsDgDxLaeQ p{margin:0;}#mermaid-svg-FcofoEZsDgDxLaeQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FcofoEZsDgDxLaeQ .cluster-label text{fill:#333;}#mermaid-svg-FcofoEZsDgDxLaeQ .cluster-label span{color:#333;}#mermaid-svg-FcofoEZsDgDxLaeQ .cluster-label span p{background-color:transparent;}#mermaid-svg-FcofoEZsDgDxLaeQ .label text,#mermaid-svg-FcofoEZsDgDxLaeQ span{fill:#333;color:#333;}#mermaid-svg-FcofoEZsDgDxLaeQ .node rect,#mermaid-svg-FcofoEZsDgDxLaeQ .node circle,#mermaid-svg-FcofoEZsDgDxLaeQ .node ellipse,#mermaid-svg-FcofoEZsDgDxLaeQ .node polygon,#mermaid-svg-FcofoEZsDgDxLaeQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FcofoEZsDgDxLaeQ .rough-node .label text,#mermaid-svg-FcofoEZsDgDxLaeQ .node .label text,#mermaid-svg-FcofoEZsDgDxLaeQ .image-shape .label,#mermaid-svg-FcofoEZsDgDxLaeQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-FcofoEZsDgDxLaeQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FcofoEZsDgDxLaeQ .rough-node .label,#mermaid-svg-FcofoEZsDgDxLaeQ .node .label,#mermaid-svg-FcofoEZsDgDxLaeQ .image-shape .label,#mermaid-svg-FcofoEZsDgDxLaeQ .icon-shape .label{text-align:center;}#mermaid-svg-FcofoEZsDgDxLaeQ .node.clickable{cursor:pointer;}#mermaid-svg-FcofoEZsDgDxLaeQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FcofoEZsDgDxLaeQ .arrowheadPath{fill:#333333;}#mermaid-svg-FcofoEZsDgDxLaeQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FcofoEZsDgDxLaeQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FcofoEZsDgDxLaeQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FcofoEZsDgDxLaeQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FcofoEZsDgDxLaeQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FcofoEZsDgDxLaeQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FcofoEZsDgDxLaeQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FcofoEZsDgDxLaeQ .cluster text{fill:#333;}#mermaid-svg-FcofoEZsDgDxLaeQ .cluster span{color:#333;}#mermaid-svg-FcofoEZsDgDxLaeQ 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-FcofoEZsDgDxLaeQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FcofoEZsDgDxLaeQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-FcofoEZsDgDxLaeQ .icon-shape,#mermaid-svg-FcofoEZsDgDxLaeQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FcofoEZsDgDxLaeQ .icon-shape p,#mermaid-svg-FcofoEZsDgDxLaeQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FcofoEZsDgDxLaeQ .icon-shape .label rect,#mermaid-svg-FcofoEZsDgDxLaeQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FcofoEZsDgDxLaeQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FcofoEZsDgDxLaeQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FcofoEZsDgDxLaeQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} yes
no
User input
AgentLoop
ctx.sessions prepare session
append turn start and step start
emit session event
Persistence UI Telemetry Projection
ctx.systemPrompt assemble prompt and tool schemas
ctx.llm stream model request
assistant text and tool calls
append assistant message
emit session event
has tool calls
ctx.tools execute calls
tool guarded pipeline
finalize tool result and render intent
append tool result
emit session event
append step end and turn end
ctx.sessions flush
emit final event

对应的近似伪代码是:

ts 复制代码
async function runOneTurn(input: UserInput) {
  // 1. loop 使用 session 插件,不自己实现日志和持久化。
  const session = await ctx.sessions.prepare({ input })
  await session.append({ type: 'turn/start' })

  while (shouldContinue(session)) {
    await session.append({ type: 'step/start' })

    // 2. loop 使用 prompt 插件,不自己拼 persona/context/tool schema。
    // tool schema 来自 tools registry 贡献给 systemPrompt 的 provider。
    const prompt = await ctx.systemPrompt.assemble({
      session,
      variables: { provider, model, cwd },
    })

    // 3. loop 使用 LLM 插件,不知道具体 provider 协议。
    const assistant = await collect(
      ctx.llm.stream({
        messages: prompt.messages,
        tools: prompt.tools,
      }),
    )
    await session.append({ type: 'assistant/message', message: assistant })

    // 4. loop 使用 tools 插件,不直接调用具体工具实现。
    for (const call of assistant.toolCalls) {
      const result = await ctx.tools.execute(call, {
        session,
        agent,
        policyContext,
      })
      await session.append({ type: 'tool/result', call, result })
    }

    await session.append({ type: 'step/end' })
  }

  await session.append({ type: 'turn/end' })
  await ctx.sessions.flush(session.id)
}

这段伪代码里有两个层次:主链是 AgentLoop -> ctx.systemPrompt / ctx.llm / ctx.tools / ctx.sessions 的直接调用;扩展链是 Session.append() 产生 session/event,persistence、UI、telemetry、projection 等插件订阅事件。也就是说,AgentLoop 和 LLM、tool、session 并没有"少互动",只是互动对象不是具体实现类,而是 Cordis service。

  1. 组合插件dsh 读取 profile,把 bundle、profile patch、用户 patch、命令行 overlay 叠成 Cordis 配置。这里的"插拔"是最外层的:一个能力是否存在,先取决于配置树里有没有对应 row。默认 base 里就把 llmsessiontoolssystem-promptagent-loopfs-sandbox 等 row 装进去:base/cordis.patch.yml:L24-L29base/cordis.patch.yml:L420-L444
  2. Session 插件@deepseek-ai/dsh-session 提供 ctx.sessions。它的能力不是"跑 agent",而是创建/发布/查询/fork/flush session,维护 append-only log,把新事件通过 session/event 发出去。持久化、搜索、telemetry、Web replay 都不需要嵌进 SessionStore,只要订阅这个事件流或接 session/flush
  3. Persistence / Query / Projection 插件@deepseek-ai/dsh-session-persistence-jsonl@deepseek-ai/dsh-session-query-sqlite@deepseek-ai/dsh-session-projection 都是接在 session 后面的插件。它们消费 session log,但不改变 loop。换 JSONL/SQLite/其它后端,理论上换的是 persistence row;session 的事件协议和 loop 写日志的动作不变:base/cordis.patch.yml:L98-L127
  4. Agent Registry 插件@deepseek-ai/dsh-agent 提供 agent registry 和 ctx.agents 这类 agent 生命周期接口。它本身不决定怎么跑模型循环,而是让某个 factory 接管"创建/恢复 agent"。这就是 agent-loop 能被做成插件的前提:registry 定义入口,具体 driver 可以由 factory 提供。
  5. Agent Loop 插件@deepseek-ai/dsh-agent-loop 提供 ctx.agentLoop,同时把自己挂成 ctx.agents 的 factory。创建 agent 时,它先通过 ctx.sessions.prepare() 准备 session,再创建 ReactLoopAgent,最后把 agent 和 session 发布到 registry:agent-loop/src/index.ts:L580-L640。这层只负责 turn/step 驱动:打开 turn、claim inbox、组装 prompt、调模型、跑工具、写 session events。
  6. System Prompt 插件@deepseek-ai/dsh-system-prompt 提供 ctx.systemPrompt。它不是"纯文本 prompt 模板",而是模型请求组装器:把 persona、插件贡献的 prompt section、动态 context、prompt variables、工具 schemas 组装成一次 PromptAssemblyPromptAssembly 的结构就是 { sections, tools, variables },文档明确说 tool schemas 是 assembly 的一部分,因为"告诉模型它能做什么"必须和 prompt 保持一致:system-prompt README:L23-L35AgentLoop 不自己拼 prompt,而是在每个 step 调 ctx.systemPrompt.assemble();其它插件可以贡献 section、变量、tool provider,或拦截 system-prompt/assemble
  7. LLM 插件@deepseek-ai/dsh-llm 定义 ctx.llm 这条模型调用缝,@deepseek-ai/dsh-llm-pi-ai@deepseek-ai/dsh-llm-deepseek 这类 provider 插件注册具体 adapter。AgentLoop 只发起 provider-neutral 的 stream 请求,不需要知道 DeepSeek、Pi、Anthropic 兼容协议怎么序列化。换模型后端,换的是 adapter 插件或配置 row。
  8. Tools 插件@deepseek-ai/dsh-tools 提供 ctx.tools,工具包通过 ctx.tools.register() 注册 schema、执行函数、输出渲染和 UI render intent。它自动把工具 schema 贡献给 ctx.systemPrompt.tools(),所以每次 prompt assembly 都能拿到当前 agent scope 可见的工具定义;执行时再把一次调用包进 tools/pre-execute -> guard -> tools/execute -> tools/post-execute -> finalizeContent -> tools/result 管线:tools README:L5-L31。所以加工具、加审批、加 timeout、加 metrics,都不需要改 loop;但 prompt assembly 必须消费 tools registry 贡献的 schema。
  9. Capability seam 插件 :Web 是典型例子。@deepseek-ai/dsh-web 定义 ctx.web 服务和 provider 选择;web-search-exaweb-search-perplexityweb-fetch-http 注册具体 provider;@deepseek-ai/dsh-tool-web 才把它包装成模型可见的 web_search / web_fetch 工具:web READMEtool-web README。这就是 Service Definition / Provider / Consumer 三段式:换 provider 不改工具 schema,换模型可见工具不改 provider。
  10. UI / Web 插件:Web UI 不直接读一个"当前对象状态",而是消费 session event / projection 和工具 render intent。工具自己声明 call/result 怎么展示,UI 只认 card vocabulary。这样新增工具时,工具插件带上 presentation,UI 不需要为每个工具硬编码分支。

这套设计的扩展含义是:插拔不是把一段代码动态 import 进来那么简单,而是每个能力都有明确的服务面和消费关系。AgentLoop 同时依赖 ctx.sessionsctx.systemPromptctx.llmctx.tools,但它只依赖这些协议,不持有它们的实现。要加工具,注册到 ctx.tools;要换模型,注册到 ctx.llm;要加持久化,订阅 session/event;要加 Web 搜索,先注册 ctx.web provider,再用 tool 插件暴露给模型;要换 driver,提供新的 AgentFactory。这就是"插件树"的实际含义。

核心不变量

这个项目最重要的不变量是:model-visible means logged。任何会进入模型请求的内容,都必须能从 session log 重建:architecture.md:L92-L96。这解释了为什么项目大量设计围绕 event sourcing、projection、snapshot、replay、fixture 和 invariant:日志不是附属记录,而是模型上下文、UI 回放、fork、resume、telemetry、persistence 的共同事实源。

和其他 harness 的差异

如果只看原子能力,DeepSeek Harness 并不稀奇:插件、session memory、tool calling、多 agent、Web UI、sandbox、SDK 都是常见能力。它的差异不在"有这些东西",而在这些东西被提升成运行时的一等约束,并被同一套插件生命周期和 session log 贯穿。

维度 常见 harness DeepSeek Harness 的差异
插件 插件多是工具、模型、回调扩展 几乎所有能力都是插件服务,包括 agent loop、session store、tools、LLM adapter、Web UI、SDK、sandbox
状态 memory 或 checkpoint 多用于恢复流程 append-only session log 是模型上下文、UI replay、fork、resume、telemetry、persistence 的共同事实源
工具 function calling 加 executor 工具有完整管线:pre-execute、guard、execute、post-execute、finalize、result,并区分 canonical value、model content、UI render intent
配置 代码配置或 YAML 启动 profile、bundle、patch、preset 是产品机制,用户能替换任意 row,而不是改源码
能力替换 替换单个 provider capability seam 要同时考虑定义、实现、消费端;换执行世界时可以成组替换 fs、shell、lsp、subprocess
UI UI 读当前运行状态 Web UI 从 session/event replay,工具卡片由工具声明 render intent,减少 UI hardcode 工具名
安全/权限 工具层加权限判断 sandbox policy、approval、tool guard、durable policy context 进入同一执行与日志模型
测试 单测加少量 e2e 强制真实入口、snapshot、Web browser replay、real API smoke,强调"验证世界,不验证自报"

工具系统能体现这种差异。它不是直接 execute(),而是 tools/pre-execute -> guard -> tools/execute -> tools/post-execute -> finalizeContent -> tools/result,并且工具 schema、执行结果、UI render intent、Code Mode SDK 都有明确边界:tools README:L5-L27tools README:L107-L120。这让工具不只是"模型能调用的函数",而是同时服务模型上下文、权限策略、执行治理、UI 呈现和日志回放的产品单元。

所以更准确的对比不是"它有没有插件、有没有日志、有没有工具",而是:

  1. LangChain / LangGraph 更像编排框架,强在 chain、graph 和集成生态;DeepSeek Harness 更像 agent 产品运行时,强在配置层、日志一致性、工具政策、Web/SDK/持久化一体化。
  2. AutoGen / CrewAI 更强调多 agent 协作范式;DeepSeek Harness 更强调每个 agent 的生命周期、会话事实源、工具治理和产品表面。
  3. OpenAI Agents SDK 更贴 provider-native agent loop、tool 和 handoff;DeepSeek Harness 更 provider-neutral,也更强调插件树和本地运行时。
  4. Claude Code / Codex 这类产品是成熟 coding agent;DeepSeek Harness 更像可被二次开发的 harness 基座。

Agency / Taste / Quality

  1. Agency:痛点具体,不是框架练习。它定义的问题是"可组合、可审计、可替换的 agent runtime",覆盖真实 agent 产品会遇到的模型、工具、沙箱、审批、Web、SDK、持久化、回放问题。
  2. Taste:项目非常 opinionated。它选择 Cordis 插件树、append-only session log、capability seams、profile/bundle patch layer,而不是一个大核心类。它也明确选择"不做":例如新行为不改 loop,Web composition 文本不在浏览器里编辑,SDK 不负责项目脚手架。
  3. Quality :质量策略不是堆单测。测试文档要求 per-file 100% coverage、真实入口测试、keyless snapshot、Web browser snapshot、真实 API e2e,并强调"验证世界,不验证自报输出":testing.md:L7-L19testing.md:L27-L49

主要代价

  1. 学习成本高:理解它必须先理解 Cordis、plugin lifecycle、scope、event waterfall、session surface。
  2. 改动成本高:很多行为需要同步更新 README、JSDoc、generated catalog、snapshot、Agent Note。
  3. 预发布阶段兼容性弱:根文档明确允许破坏性变更,适合快速打基础,但外部插件生态会承压。
  4. 抽象密度高:capability seam 很强,但新人容易在 Service Definition / Provider / Consumer / bundle / preset / profile 之间迷路。
相关推荐
刀锋00011 小时前
从0到1手搓生产级 AI Agent:LangGraph 1.2 + LangChain 1.3 保姆级实战(全部代码已跑通)
人工智能·python·langchain·ai agent·langgraph
水如烟1 小时前
孤能子视角:因果论——方向、锁定与必然感:关系场中归因链的生成语法
人工智能
老兵发新帖1 小时前
OSD和视频流接口随机出现net::ERR_CONNECTION_RESET问题分析总结
人工智能
海兰1 小时前
【开源工具】BlueKing Lite —— AI 原生的轻量运维平台(二)
运维·人工智能·开源
御风之翼_唤星者1 小时前
LoadFramePackModel模块报错bad escape
python·ai
自学机械人的小白ing1 小时前
深度学习系统学习
人工智能·深度学习·学习
HyperAI超神经1 小时前
128K长上下文+智能体强化训练!LFM2.5-2.6B解锁端侧大模型高效部署;DETR用Transformer斩断NMS与Anchor,重塑目标检测
人工智能·深度学习·目标检测·计算机视觉·数据集·transformer
学习日记5251 小时前
AI 工程实战:一套可复用的提示词库与质量门禁,如何让 AI 辅助研发「可验证、可沉淀」
人工智能·prompt
雪隐1 小时前
个人电脑玩AI-16让5060 Ti给你打工——5060Ti 16G 跑 MiniMax-Music-3:从下载到 60s 出歌的全流程
前端·人工智能·后端