跳转至

02 · 设计思想:把整个 Agent 产品变成可组合的插件树

本章以锁定的 main da00f7f… 为源码基准。发布快照是 dsh-v0.2.0-rc.2,仍然是预稳定 API;“最新 main”与“npm 发布版”是两套身份,不能混用配置。本章给出的架构解释来自源码和官方架构约定;Pi 对照使用上一课程锁定的 Pi v1.0.0,不把个人偏好写成性能结论。

2.1 先回答:Harness 到底是什么

模型负责产生内容和工具调用。Harness 负责决定模型看到什么、哪些调用能够执行、执行结束后哪些事实进入下一次请求,以及进程停止以后哪些事实还能恢复。DeepSeek Harness 将这些职责拆成 Cordis 服务、事件和效果,而不是把“一个 Agent 类”作为所有功能的中心。

AgentRegistry 提供 Agent 的公共注册和创建入口;AgentLoop 才是默认工厂和驱动器;SessionStore 是事件日志及派生历史;ToolRuntime 是工具注册和执行;SystemPrompt 负责组装;LlmRegistry 负责模型适配器。默认循环自己也作为插件提供 ctx.agentLoop,消费者通过 ctx.agents 使用它。因此换驱动器不是在工具实现里 monkey patch 私有方法,而是在组合层换提供工厂的插件。架构入口,Agent 公共服务,默认工厂。

flowchart TB P[Profile 与 Bundle 组合] --> C[Cordis Context 插件树] C --> A[AgentRegistry 公共句柄] C --> L[AgentLoop 默认驱动] C --> S[SessionStore 事件日志] C --> T[Tools 注册与执行策略] C --> M[LLM Adapter 路由] C --> Y[SystemPrompt 组装] A --> L L --> Y L --> M L --> T L --> S S --> V[纯投影与持久化消费者] T --> F[Filesystem 与 Shell 等能力]

“全部插件”不表示应用不需要启动器,也不表示每个模块都能够随意卸载。依赖会控制激活,使用过的消息解释器移除后会让历史拒绝继续派生,外部进程还需要归属与退出顺序。这是一种明确表达替换点和生命周期的架构,不是无约束的插件市场。

2.2 三种机制,各自解决一种问题

机制 例子 你新增功能时的问题
服务 ctx.tools、ctx.llm、ctx.sessions 谁提供能力接口,谁实现,谁使用?
事件 agent/pre-step、tools/pre-execute 在已有流程的哪个位置观察、拦截或包裹?
Effect ctx.effect、ctx.on、注册器 disposer 谁拥有注册和资源,卸载时如何回收?

服务定义、服务提供者、消费者构成一个 capability seam。文件系统接口和 Bash 工具不是同一个角色;让 Bash 在远端运行还要求 subprocess、filesystem 和 sandbox 指向同一个执行世界,不能只替换某一个路径字符串。否则模型读的是本机文件,执行却发生在另一台机器。把它们按能力家族拆分,是为了让替换贯穿产品的所有消费者。能力接缝说明。

事件也不是一套统一的“异步广播”。emit 不等待返回 Promise,parallel 等待全部监听器,serial 顺序执行且遇到非空 bail 值就停,waterfall 通过 next() 包裹下游。选错分发方式,会出现许可没有完成就执行、提交后观察器异常污染结果、或忘记 next() 把默认行为整段吞掉的问题。精确分发实现。

2.3 “模型可见必须可重建”是主约束

默认循环发送的消息来自 session.deriveMessages()。系统提示词进入 system/message,运行时上下文进入带来源的 user/message,工具集和配置进入 request/header,工具增删进入 developer/message,模型尝试与结果进入持久化结算事件。它不是先拼好任意 request 再尽力做日志,而是在真正请求之前提交足够的日志,再从日志构造请求。请求构造。

这带来四个直接收益:能够说明“模型为什么知道这个事实”;fork 能复制确切前缀;恢复不需要猜测内存里的提示词;人类对话、遥测与模型上下文可以从同一个事实源得到不同视图。这些是设计收益,不等于所有生产故障已经证明不存在。进程在流式尝试结算之前硬退出时,瞬时 chunks 尚未成为持久事实。Assistant 结算。

日志的 append-only 不等于模型永远看到全部日志。surfaceOp: replace 新增一个事件,替换当前 model surface 的节点范围;旧事件仍留在日志。这样压缩或工具输出重写不会擦掉人类已经看到的历史。Surface 实现。

2.4 组合层也是产品的一部分

运行中的 dsh 由 profile 和多个有顺序的 bundle 层组成,用户 patch 和命令行 overlay 位于更高层。官方支持的 Node 应用启动走 dsh --profile …;不能把源码测试里 new Context() 的样例包装成另一个官方应用 bin。启动规则和分层。

自己的 Agent 有三个不同层次:

  1. 改产品组合:选 bundle,增删工具和 provider,给自己的 profile 提供 prompt 与配置。
  2. 增加行为插件:在工具、模型或日志服务旁挂载新插件,用事件和注册器贡献能力。
  3. 做另一种运行载体:通过支持的 SDK/ACP 投影已有 loop,自己实现 UI 或业务宿主。

如果需求是“工具完成后补一段文件变化说明”,使用 exec.deferContext;如果需求是“下一请求拒绝发送敏感文件”,在能够解释日志变化的 seam 做策略;如果需求是“相同 Session 换一组工具和提示词”,在创建 setup 或 preset 中组合。只有现有 seam 无法表达语义时才考虑改循环,并且需要更新两套 SDK 与持久化语义。

2.5 scope 与 isolate 不要混为一谈

DSH scope 标记注册归属、工具/提示词层次和事件筛选;Cordis isolate('tools') 为服务名称创建独立 realm。前者让 Agent A 覆盖全局工具却继续使用同一个 Tools 服务;后者让某一插件子树使用另一整个 Tools 服务提供者。

agent.ctx 中注册一个名为 lookup 的工具,可遮蔽全局同名定义;给 preset 挂载一个新的 ctx.fs provider 则需要对应的 isolate realm,否则同一服务名重复提供会抛错。scoped 工具限制也不是进程 sandbox;进程隔离是另一项 capability。scope 原语,服务独立 realm。

2.6 与 Pi v1.0.0 的真实差异

决策 Pi v1.0.0 DeepSeek Harness 锁定 main
扩展中心 小的 Agent Core,CodingAgent SDK 和 extensions 增加产品能力 整个产品由 Cordis 插件组成,默认 loop 也是 provider
组合语言 SDK 构造、settings、extensions、资源加载 profiles/bundles 与 Cordis plugin tree/patch/isolate
工具结果 工具内容与 details;CodeMode 还有自己的应用状态约定 canonical JSON value 加独立 output.schema/render,Native 与 PTC 共用
上下文依据 message/context 管道加 session tree 的产品管理 model-visible 与 append-only Session、surface、请求 header 构成强约束
分支 单个 session 文件中的 parent/leaf 树及产品分支动作 child Session 拷贝事件前缀、记录 lineage 与 inherited cut
执行扩展 Agent 事件与 extension hooks typed Cordis events,明确 emit/parallel/serial/waterfall
资源清理 Agent/Session、扩展管理与工具宿主分别拥有生命周期 Fiber/effect 统一注册归属,并在复合拥有者中安排 quiescence
程序调用工具 Pi CodeMode/runtime 做自己的变量、执行与应用语义 PTC run_code/SDK transport,内部仍走完整工具策略管道

这并不是“小而简单”与“大而正确”的评判。Pi 适合先掌握模型-工具循环,再嵌入自己的业务;DSH 倾向于把 Session、宿主、能力切换、动态产品组合一起建模。代码体量、依赖数量和生命周期复杂度也随之增加。你需要多种能力提供者、多人协调、持久化恢复和可配置产品时,DSH 的接缝更直接;只需要一个明确工具集的脚本型 Agent 时,这些层次可能构成额外成本。Pi 源码的对应依据见上一课程设计章,这里的对照不声称做过性能基准。

2.7 哪些理念不能直接变成保证

ctx.effect() 为你管理清理,但不保证异步 disposer 成功;同进程工具不支持强制杀掉不合作的 Promise;JSON 日志能够恢复模型历史,不保证外部写入与日志提交原子化;pure projection 是编写者必须遵守的要求,不代表所有 TypeScript 输入都逐字段运行时验证。

自己的插件应该明确:哪个事件是事实,哪个通知仅实时;哪个 Promise 必须等待;哪个注册器本身已经提供 effect;故障之后能否重试副作用;哪些持久化记录使未来 reader 必须加载同一个解释器。后面03、05、07、08逐一回答。

2.8 版本边界

rc.2 与 main 的核心 loop agent.ts、inbox、tool scheduler、runtime-context、Cordis 源码在本次两快照对比中没有行为差异。main 删除了多个 invariant companion 实现与导出;不应从 rc.2 的诊断插件文档推断 main 还有同样的插件。main 另外把 run_code 描述和 schema 字段显示顺序改为 description 在 code 前,仍需要两参数。普通 Session/surface 差异主要为注释和诊断标记清理;具体发布矩阵以版本章为准。