DeepSeek Harness 和 Pi 的 Agent Loop 循环结束判断架构细节解析

本篇文章代码基线采用 DeepSeek Harness 版本 0.1.2-alpha.1,提交时间为 2026 年 8 月 28 日;Pi 采用 853a80d2。这里的 Pi 指 pi-agent-core 中的官方 Agent Loop。

就循环究竟在什么时刻结束这个问题,很多 Agent 框架把结束判断写成一个条件:

while (hasToolCalls) {
  // ...
}

在 Demo 中这种循环也够用。进入生产环境后,你会发现会很多问题:

  • 模型没有调用工具,是否代表任务完成?
  • 工具执行完了,模型是否还需要解释结果?
  • 用户在工具执行期间发来 steering,应该进入当前轮还是下一轮?
  • 输出触达 token 上限后,工具参数还能不能执行?
  • 某个工具宣布任务完成,其他并行工具怎么办?
  • Agent 进入 idle 后,Goal Driver 能不能再次唤醒它?
  • Plan mode 退出,是结束当前循环,还是切换下一次请求的策略?
  • 进程崩溃后,日志里尚未闭合的 turn 应当如何解释?

DeepSeek Harness 和 Pi 的工程路线不同,或者说 DeepSeek Harness 更复杂,也更能满足复杂场景的需求。

DeepSeek Harness 将结束拆成多层状态,并把事件日志作为重建依据。Pi 保留了更扁平的循环结构,把 steering、follow-up 和 post-turn stop 暴露成回调。前者适合多插件、持久恢复和长期任务;后者适合轻量嵌入,控制路径也更容易读懂。

两种实现都能工作。它们承担的系统复杂度不同,结束语义自然不会相同。

五层边界

讨论 Agent Loop 时,我们需要先定义好「结束」,否则代码里的 completedturn_endagent_end 很容易被混为一谈。

DeepSeek Harness 实际存在五层边界。

第一层是模型请求结束。

Provider 流式输出完成,可能给出正常停止、工具调用、最大 token、错误或取消。这里的判断逻辑是:当前 HTTP 请求或模型流结束了吗?

第二层是 step 结束。

一个 step 包含一次模型调用,以及该响应发起的一整批工具执行。工具执行结束后,step 才算关闭。模型若调用了普通工具,系统通常还欠一次模型回访,因此 step 结束不代表 turn 结束。

第三层是 turn 结束。

DeepSeek Harness 中,一个 turn 可以包含多个 step,大概如下:

用户输入
→ 模型调用
→ 工具批次
→ 模型回访
→ 工具批次
→ 模型最终输出

以上为一个 turn。turn 结束需要同时满足两项条件:

  1. 模型不再欠一次回复;
  2. next-step inbox 没有待处理输入。

第四层是 driver activity 结束。

一个 driver 可以连续处理多个 turn。当前 turn 关闭后,如果 inbox 里还有普通 follow-up,driver 会直接打开下一 turn。只有队列耗尽,driver 才回到 idle。

第五层是长期工作流结束。

Goal Driver 可以监听 idle,检查持久 Goal 状态,再调用 followup() 开启新 turn。Plan mode 也可能跨越多个 turn 持续生效。因此,idle 只描述当前 activity;它无法回答长期目标是否完成。

Pi 的层级少一些。一次 assistant 响应与随后执行的工具批次,会对应一个 turn_end。工具回访模型时,Pi 再开启下一次 turn。内部队列耗尽后发出 agent_end,当前 invocation 至此结束。

从语义上看:

  • DeepSeek Harness 的 step 接近 Pi 的 turn;
  • DeepSeek Harness 的一次 driver activity 接近 Pi 的一次 Agent Loop invocation;
  • DeepSeek Harness 的 Goal Round 位于 driver activity 外层;
  • Pi 的长期 Goal 通常由宿主或扩展继续调度。

术语相似,粒度相差一层。如果日志消费端忽略这一点,统计出的 turn 数、工具往返次数和任务完成率都会失真。

驱动收敛

DeepSeek Harness 的入口:

while (await this.turn()) {
}

turn() 返回 true,driver 继续处理下一 turn;返回 false,driver 收敛;抛出异常时,异常会被 driver 边界容纳,随后进入 idle。

短代码背后有一套完整的 phase 状态机:

idle
maintenance
running

running 保存:

  • 当前 turn;
  • 当前 step;
  • AbortController
  • wakeRequested

maintenance 同样持有取消信号和唤醒锁存,只是对外状态仍显示为 idle。这种设计解决了一个经常被忽略的竞态:Agent 处于维护任务时收到新消息,新消息不能立即启动第二个 driver,也不能静默丢失。系统先将唤醒意图锁存,维护结束后检查 inbox,再决定是否重启。

取消路径也有类似处理。

如果一个 waking input 到达时,现有 activity 已经被 abort,这条消息会被重分类到 next-turn。它不能加入一个正在收敛的旧 turn。send() 在插入 inbox 前读取 aborted 状态,避免 inbox observer 触发重入取消后改变分类结果。

这段细节很像传统并发状态机,而不是常见的聊天循环。它处理的是输入归属问题:一条消息必须属于当前 step、下一 step 或下一 turn,重入事件不能改变已经作出的边界判断。

whenIdle() 也没有简单等待某个固定 Promise。它反复比较 activityDone

do {
  await (activity = this.activityDone)
} while (activity !== this.activityDone)

等待期间如果 activity 被新的 driver 替换,调用方会继续等待。否则 whenIdle() 可能在旧 driver 结束、新 driver 已经启动的窗口里提前返回。

在多插件环境中,idle 是一个可观察状态,也是一种同步承诺。这个承诺需要覆盖重启竞态,不能只看某次异步任务是否 resolve。

Turn 状态机

DeepSeek Harness 在领取输入之前写入 turn/start

pre-step 可能拒绝输入,系统提示组装可能抛错,首步消息也可能被插件改写为空。无论发生哪种情况,日志中都存在一个完整 turn,并由唯一的 turn/end 收口。

turn 内部维护两个变量:

  • turnEnds:当前已知的结束原因;
  • target:本轮 pre-step 应领取 next-turn 还是 next-step

第一个 step 从 next-turn 开始。后续 step 转向 next-step。普通用户 prompt 因此拥有独立 turn,工具附加上下文、运行时 context 和 steering 则可以进入当前 turn 的下一 step。

一次循环大致经历以下过程:

创建 step 编号
→ 从 inbox 领取消息
→ 组装系统提示和运行时 context
→ 执行 agent/pre-step
→ 记录 step/start
→ 写入 user/message
→ 调用模型并执行工具
→ 记录 step/end
→ 判断是否继续

如果 agent/pre-step 返回 reject,turn 以 blocked 结束,模型不会被调用。

如果首个 step 的消息为空,turn 以 completed 结束,也不会消耗模型请求。这覆盖了一个边缘场景:wakeup 已经打开 turn,随后消息被移除,或者 pre-step 插件把消息改写为空。边界既然已经建立,日志仍需闭合。

如果某个 step 已经产生结束结果,但下一次 pre-step 没有消息,循环退出。若仍有消息,就继续执行。

这里需要留意 turnEndsstepEnd 的关系。step() 可以返回:

  • completed
  • max-tokens
  • null

null 表示工具执行完毕,模型还欠一次回复。它是继续循环的协议状态,和错误、未知结果没有关系。

turn 的结束判断由「模型债务」与「消息债务」共同决定。模型债务来自 tool call,消息债务来自 inbox。只检查其中一项都会出错。

停止钩子

DeepSeek Harness 在 turn 准备收口时触发 agent/turn-stopping

这个扩展点没有返回 truefalse。插件如果希望 turn 继续,需要向 next-step 写入消息。hook 执行完成后,核心循环重新检查 inbox:

turn 已经可以结束
→ next-step 为空
→ 触发 agent/turn-stopping
→ 再次检查 next-step
→ 为空则关闭
→ 非空则进入下一 step

这是数据驱动的方式。

多个插件都能监听 stopping。如果采用 shouldContinue(): boolean,组合规则会立刻变得棘手:

  • 任意插件返回 true 就继续?
  • 所有插件都返回 true 才继续?
  • 后执行的插件能否覆盖先执行的结果?
  • 插件决定继续,却没有提供下一步输入,模型该看到什么?
  • 插件决定停止,但另一个插件已经排入 steering,谁的优先级更高?

DeepSeek Harness 用 inbox 消除了大部分布尔值合并问题(这个设计和 Golang 的竞态处理逻辑类似,使用类似消息队列来处理竞态)。继续执行必须产生一条可消费的数据。最终判定依赖队列状态,而非插件调用顺序。

当然,这也是需要代价的。

插件作者需要理解 inbox target、turn 边界和 wakeup 语义。一个简单的「再跑一次」操作,需要构造 UserMessage 并写入正确队列。框架学习成本高于直接回调。

这种成本在单体应用里可能显得繁琐。进入可恢复、多插件、可审计系统后,显式消息通常比隐式布尔控制更可靠。日志和调试工具能看到「为什么继续」,而不是只看到某个 hook 曾经返回 true。

Step 债务

step() 最关键的职责,是判断模型是否还欠一次回复。

模型流结束后,BlockAssembler 给出 finish 状态。如果 finish 是 error 或 aborted,系统先触发 agent/request-error waterfall。插件可以选择 retry。没有 retry 动作时,代码抛出结构化 LlmError

正常组装出 assistant message 后,判断顺序如下:

  1. finish 为 max-tokens,返回 max-tokens
  2. 没有 tool call,返回 completed
  3. 存在 tool call,执行整批工具;
  4. 工具批次声明 concluded,返回 completed
  5. 普通工具批次返回 null

顺序不能随便改。

最大 token 状态在工具解析之前返回,因此响应中即使出现 tool call,也不会被执行。截断输出可能包含不完整的 JSON 参数。某些流式解析器会尝试补全 JSON,补全后甚至能通过 schema 验证,但语义字段可能已经缺失。执行这类调用会产生真实副作用。

DeepSeek Harness 的选择偏保守:整轮以 max-tokens 收口,等待外部策略决定是否继续。

Pi 采用另一种处理。stopReason === "length" 且消息包含工具调用时,它会给所有工具生成错误结果,说明参数可能被截断,没有实际执行,然后把这些结果交回模型。模型有机会在下一 turn 重发完整调用。

两者的恢复能力不同。

DeepSeek Harness 保留了清晰的 durable turn reason,运维侧能准确识别 token ceiling。自动继续需要 Goal Driver、用户输入或其他插件参与。Pi 更倾向在当前 invocation 内自我修复,模型可能马上重发工具调用。

我在具有写操作的 Agent 中会选择 DeepSeek Harness 的策略。达到输出上限本身意味着模型状态不完整,自动延续可能重复前一批操作。只读研究型 Agent 可以采用 Pi 的方式,失败工具结果能减少一次人工干预。

粘性结果

DeepSeek Harness 的 max-tokens 具有粘性。

某个 step 达到上限后,插件仍可能在 turn-stopping 阶段补入 steering,让 turn 再执行几个 step。后续 step 即使正常完成,最终 turn/end 仍然保留 max-tokens

判断逻辑是:

if (
  turnEnds === null ||
  turnEnds.kind !== 'max-tokens'
)
  turnEnds = stepEnd

这里记录的是整个 turn 的质量,而非最后一次 step 的结果。

如果不做粘性处理,可能产生如下日志:

step 1: max-tokens
step 2: 插件要求继续
step 3: completed
turn/end: completed

消费端会丢失曾经发生的截断。监控系统无法统计 token ceiling,Goal Driver 也可能把这轮当成完整成功并继续自动调度。

把 turn reason 视为聚合结果后,合并规则就需要专门设计。DeepSeek Harness 当前只对 max-tokens 做了优先保留。错误和取消会直接离开主流程,不参与后续合并。

如果未来加入诸如 partialpolicy-warningbudget-exhausted 等状态,简单覆盖会越来越脆弱。更稳妥的方向是为 reason 建立显式优先级或累积 facts,再由投影层生成最终 reason。当前 union 规模尚小,现有实现足够清楚。

工具收口

DeepSeek Harness 允许工具通过结果上的 concludesTurn 宣布当前 turn 可以收口。

工具批次采用 OR 聚合:

任意一个已提交结果 concludesTurn === true
→ 整批 concluded === true

它表达的是「模型默认不再欠一次工具结果回访」。

concludesTurn 没有直接打断同批工具,也没有清空 next-step

  • 已启动的并行工具继续运行;
  • 结果按模型调用顺序提交;
  • additionalContexts 写入 next-step
  • 同期到达的 steering 也写入 next-step
  • 只要 next-step 非空,turn 仍会进入下一 step。

因此,工具结束能力属于软收口。它取消了自动回访债务,无法压过已经存在的消息债务。

这套语义适合权威型工具。例如某个交互工具已经取得用户最终决定,或者某个提交工具已经完成不可逆事务,模型没有必要再生成一段中间解释。工具可以建议收口,同时保留系统上下文和用户 steering 的优先级。

Pi 的工具终止采用 AND 聚合:

return finalizedCalls.length > 0 &&
  finalizedCalls.every(
    finalized => finalized.result.terminate === true
  )

只有非空批次中的每个 finalized result 都带有 terminate: true,整批才终止。

并行批次中,一个工具不应单方面吞掉其他工具结果。Pi 要求整批达成一致,语义更保守。DeepSeek Harness 允许一个权威工具宣布收口,扩展能力更强,也更依赖工具契约。

选择 OR 还是 AND,取决于 terminate 的业务含义。

如果它代表「任一工具发现全局终止条件」,OR 合理,比如用户取消、权限拒绝、事务已完成。

如果它代表「当前工具不需要模型处理自己的结果」,AND 更稳妥,因为其他工具可能仍需模型解释。

DeepSeek Harness 通过 next-step 上下文缓和了 OR 的侵略性,但无法覆盖所有情况。假设两个并行工具都成功,一个写数据库并 concludes,另一个返回一份需要模型分析的报告,又没有追加 context。模型回访会被跳过。工具注册规范需要限制哪些工具可以 conclude,不能把它当成普通便利字段。

顺序提交

DeepSeek Harness 的工具调度器还有一项容易被忽略的约束:执行可以并行,提交保持模型顺序。

调度器维护:

  • nextToStart
  • started
  • committed
  • inFlight
  • 按调用位置排列的 slots

并行工具完成时,只把结果放入对应 slot。commitReady()committed 开始,连续提交已经就绪的结果。后面的工具先完成,也要等待前面的 slot。

这会增加尾部等待时间,但能保证:

  • tool/result 顺序稳定;
  • additional context 顺序稳定;
  • replay 结果稳定;
  • 插件观察顺序稳定;
  • 同一模型响应在多次运行中的日志结构更一致。

模型生成了 A、B、C 三个调用。若 C 最先完成,直接提交 C 会让后续上下文受网络时序影响。对于事件溯源系统,这类非确定性会扩大恢复和测试成本。

Pi 的并行执行也使用 Promise.all 保留输入数组顺序,最终 tool result message 按原调用顺序发出。两套系统都接受并行执行带来的延迟收益,同时拒绝完成时序污染模型上下文。

DeepSeek Harness 额外支持动态重分类。并行池补充新任务前,会重新读取工具当前 execution mode。前一个工具可能修改注册表,使后续工具从 parallel 变成 exclusive。调度器会停止补充,等待当前池排空,再把 exclusive call 留给下一个 barrier。

这种灵活性会增加 scheduler 状态数量。适合「工具本身能改变工具环境」的 Harness。工具目录完全静态时,预先分组会更简单。

取消语义

工具执行期间的取消,比模型流取消复杂得多。

DeepSeek Harness 的原则可以概括成三条:

  1. 停止补充尚未启动的调用;
  2. 清空已经启动的调用;
  3. 为因取消而跳过的工具写入合成错误结果。

已启动工具可能已经产生副作用,框架不能假装它没有运行。调度器等待这些调用 settle,并按模型顺序提交结果和 additional contexts。

未启动工具则写入一对完整事件:

tool/call
tool/result(error: aborted before dispatch)

这样可以维持模型工具协议的配对关系。恢复和 replay 时,每个模型产生的 tool call 都有对应结果。

内部 scheduler failure 的处理更克制。它停止新 dispatch,等待已经启动的调用结束,然后抛出第一个失败。它不会为剩余调用伪造结果。因为 scheduler failure 与用户取消不同,系统无法确认未提交调用处于什么语义状态。伪造恢复结果可能掩盖调度器缺陷。

这里体现出事件日志的核心要求:日志不只服务 UI,它还是恢复协议。任何 synthetic event 都需要有确定语义。为了让数组长度好看而补事件,会污染后续决策。

Pi 的实现更偏运行时事件流。工具执行异常会被转换成 error tool result,随后继续进入上下文。取消时顺序模式会在每次工具后检查 signal,平行模式也会停止准备后续调用。它的核心目标是把本次 invocation 的 message stream 完整交给调用方,持久日志的闭合约束没有 DeepSeek Harness 那么强。

原因体系

DeepSeek Harness 的 TurnEndReason 包含六种核心结果:

  • completed
  • blocked
  • max-tokens
  • aborted
  • error
  • interrupted

这些状态回答的是整个 turn 如何结束。

completed 覆盖正常文本停止、工具要求收口,以及首步为空。它不承诺长期 Goal 完成。

blocked 来自 pre-step reject。输入可能已经被 inbox 领取,但策略层拒绝进入模型调用。driver 当前会停止,其他尚未领取的普通 prompt 可以继续留在队列。

max-tokens 表示至少一个 step 达到输出上限。它保留为 turn 级质量信号。

aborted 携带取消原因,包括 user、parent、hook 和 disposed。持久导入还可能出现 legacy cause。

error 保存结构化 LlmFailureLlmError 的信息原样保留,其他异常会被压平为 errorChain 文本,并使用 UNKNOWN code。

interrupted 由持久化恢复层产生。进程崩溃后,后端发现一个开放 turn,会用该状态闭合。live loop 不会主动写入它。

把 crash repair 与 runtime abort 分开很有必要。abort 表示程序收到了取消请求,并有机会执行清理;interrupted 表示生命周期突然消失。两者对副作用审计、重试决策和用户提示都有不同含义。

Pi 主要保留 assistant message 的 stopReason

  • stop
  • length
  • toolUse
  • error
  • aborted

它更接近最后一次模型调用的结束事实。DeepSeek Harness 的 reason 聚合了多 step turn 的结果。一个字段偏 provider,一个字段偏业务生命周期。

从监控角度看,Pi 的 stopReason 更利于分析模型行为;DeepSeek Harness 的 turn reason 更利于分析任务执行。生产系统通常两类数据都需要。DeepSeek Harness 通过 assistant/message、request events 和 turn/end 同时保留两层事实,日志体积也会更大。

Goal 外循环

长期 Goal 无法靠核心 turn 的 completed 判断。

DeepSeek Harness 采用外围 Round Driver。它监听 Agent 进入 idle,然后检查:

  • Agent 实例是否仍然有效;
  • inbox 是否存在竞争中的普通 prompt;
  • Goal 是否存在;
  • Goal phase 是否为 active;
  • 当前 activation 是否 armed;
  • 已启动 round 数是否低于上限。

满足条件后,Driver 构造一条来源为 goal 的 user message,并调用 agent.followup()。新消息进入 next-turn,打开全新的 turn。

时序如下:

turn/end(completed)
→ agent/status: idle
→ Goal Driver 读取持久状态
→ flush
→ followup(goal round)
→ turn/start

这种结构给每个 Goal Round 一个完整、独立、可恢复的生命周期。进程在两个 Round 之间退出,恢复时可以从最后一个闭合 turn 继续判断。一个覆盖几十轮的巨大 while 会让持久化边界、用户抢占和异常恢复都更困难。

Goal 还维护 phase 与 activation 两组状态。

持久 phase 描述业务状态:

  • active;
  • complete;
  • blocked;
  • paused。

进程内 activation 描述自动续行授权:

  • armed;
  • disarmed。

把两者拆开可以处理 resume 和 fork。一个持久 Goal 仍然 active,不代表新进程应该自动继续消耗预算。恢复后的 active Goal 默认 disarmed,需要人类再次授权。否则某次服务重启可能让多个历史任务同时复活。

达到最大 Round 数时,Goal 会进入 blocked,并记录 round-limit。max-tokens、Agent error、flush failure、driver failure 会 disarm。取消路径也会 pause 或 disarm,防止 idle 后被 Goal Driver 再次拉起。

这里采用了 fail-closed 策略。长期自治任务遇到状态不确定时停止续行。对成本敏感、有写权限的 Agent,这是合理默认值。自动重试可以放在更高层,由持久预算和幂等策略约束。

Goal 收尾

Goal 状态改为 complete 或 blocked 后,当前 turn 通常还会再调用一次模型。

update_goal 工具会修改持久状态,并通过 deferred context 注入收尾指令。工具结果与 <goal_complete><goal_blocked> context 进入下一 step,模型生成最终面向用户的说明。之后 turn 以 completed 结束。Agent 进入 idle,Goal Driver 读取到非 active phase,不再唤醒。

这解决了状态完成与用户可见输出之间的时间差。

如果 update_goal(complete) 直接 conclude turn,持久状态已经完成,用户界面可能只看到工具卡片,没有自然语言总结。若让模型先写总结再更新状态,模型或进程可能在总结期间失败,Goal 又会保持 active。

DeepSeek Harness 先提交状态,再执行展示收尾。恢复时即便缺少最终文本,Goal 也不会重复工作。用户体验层可以根据 complete 状态补一条提示,或者提供重新生成总结的操作。

代价是 complete 后多一次模型调用,也会增加少量 token 和延迟。我认为这笔成本合理。长期任务的最终输出属于交付结果,不能依赖 UI 猜测工具状态。

Pi 双循环

Pi 的 runLoop() 使用内外两层循环。

内层条件为:

hasMoreToolCalls || pendingMessages.length > 0

它处理工具回访和 steering。外层负责 Agent 本来要停止时的 follow-up。

一次内层迭代包括:

prepareNextTurn
→ 处理 pending steering
→ 调用模型
→ 执行工具
→ 发出 turn_end
→ shouldStopAfterTurn
→ 拉取新的 steering

内层耗尽后,系统调用 getFollowUpMessages()。存在 follow-up 时,将它们设置为 pending,重新进入内层。没有 follow-up 时发出 agent_end

Pi 提供三个高杠杆控制点:

  • getSteeringMessages()
  • getFollowUpMessages()
  • shouldStopAfterTurn()

这套接口很适合嵌入应用。

steering 处理当前 activity 内的即时干预;follow-up 处理 Agent 自然收口后的补充工作;shouldStopAfterTurn 提供宿主级截停。扩展作者不需要理解持久 inbox,也不需要创建新的状态投影。

复杂度被交给宿主。多个扩展共同提供 follow-up 时,需要上层决定合并方式。进程恢复后,回调内部的临时状态如何重建,也由应用实现。若不同扩展分别定义 Goal、Plan 和审批流程,结束语义会逐渐碎片化。

Pi core 的目标是提供一个小而可组合的运行时。它没有承诺统一的长期任务协议。这个边界和 DeepSeek Harness 的产品定位并不相同。

失败路径

Pi 遇到 assistant erroraborted 时,会发出 turn_endagent_end,然后结束当前 invocation。

DeepSeek Harness 会先区分流 finish error、显式 abort 和普通异常。

请求错误可以进入 agent/request-error waterfall。插件能够依据 provider、failure、retry policy 和 signal 决定 retry。重试发生在同一个 step 内,重新创建 assembler,再发一次请求。外部 turn 和 step 边界保持不变。

这种设计使日志更紧凑,但需要谨慎处理重复请求。前一次失败若已经在 provider 侧产生计费,session 中可能只有请求重试后的最终 assistant message。底层观测系统需要独立记录 attempt,不能只靠 conversation event log 统计模型调用次数。

Pi 通常由 stream implementation 或上层配置承担 retry。Agent Loop 接收到的 final message 已经带有 stopReason。职责划分更轻,调用方也更容易替换模型层。

DeepSeek Harness 将 request header、adapter defaults、request context 和 conversation surface 都纳入 session 语义,retry 与恢复自然更靠近 loop。架构更重,换来统一的请求重建能力。

请求序列

DeepSeek Harness 每次构建请求时都会比较当前 header 与 session 中的 baseline。

header 包含:

  • provider 和 model;
  • reasoning effort、max tokens 等调用配置;
  • adapter materialized defaults;
  • system prompt;
  • tool schemas。

首个 header 记录为 initial 或 resume。配置变化时记录 change。显式开始新消息序列或 surface replacement 后,会记录 series。

结束判断和 request series 没有直接控制关系,但它影响「同一个上下文连续体」的解释。Goal 的自动 Round 可以标记 startsRequestSeries: true,让每轮长期任务拥有独立请求序列。缓存、展示和重放层因此能识别边界。

如果只依靠 turn start 判断新序列,会把工具回访、用户 follow-up、Goal Round 和 compaction 后续请求全部归入一种语义。DeepSeek Harness 单独记录 series,是在为后续缓存和重建保留信息。

成本是日志事件更多,header equality 与 canonicalization 也必须稳定。工具 schema 排序、空字段处理或 adapter defaults 若不规范,可能制造大量无意义 change 事件。代码使用 canonical header 和 deep freeze,正是在控制这种漂移。

日志消费

消费 DeepSeek Harness 日志时,先问清业务方想判断哪一层结束。

判断一次模型调用完成,应读取 step 内的 assistant message、stream finish 和 usage。

判断 turn 是否闭合,应寻找配对的 turn/startturn/end。只有 assistant message 不够。pre-step reject、空 turn、异常和 crash repair 都可能产生不同结构。

判断当前 Agent 是否暂无活动,应读取 status 或等待 whenIdle()。最后一条 turn/end 仍不足以证明 idle,因为 driver 可能已经打开下一 turn。

判断长期 Goal 是否结束,应读取 Goal phase。complete、blocked 和 paused 都会停止自动 Round,业务含义各不相同。

判断 Plan 是否退出,应读取最新 plan/mode.active。看到 exit_plan_mode 调用,只能说明模型提出了退出请求。审批可能被拒绝,pending intent 也可能尚未在 pre-step 提交。

判断会话以后是否还会运行,需要观察 Agent 是否 disposed、是否从 registry 移除,以及外围 scheduler 是否仍能发送消息。idle 和 completed 都没有永久性。

Pi 的消费方式更简单:

  • assistant stopReason 描述模型调用;
  • turn_end 描述一次模型与工具批次;
  • agent_end 描述当前 invocation;
  • Goal、Plan 和工作流完成度读取具体扩展状态。

即使已经收到 agent_end,宿主仍然可以再次调用 Agent Loop。它同样不具备永久终止含义。

工程取舍

DeepSeek Harness 为结束判断支付了较高复杂度。

它需要:

  • phase 状态机;
  • inbox 分类;
  • wake latch;
  • 平衡的 turn 和 step 事件;
  • surface projection;
  • request header 重建;
  • crash repair;
  • Goal activation;
  • Plan boundary intent;
  • 有序工具提交。

这些机制会增加代码量、测试矩阵和插件开发门槛。一个简单聊天产品采用全套设计,投入可能超过收益。

它适合以下场景:

  • 会话需要跨进程恢复;
  • Agent 拥有高风险工具;
  • 多个插件会同时 steering;
  • 需要长期 Goal 与预算控制;
  • 日志需要审计和 replay;
  • Plan、审批和执行必须跨边界保持一致;
  • 子 Agent 和父 Agent 存在取消传播。

Pi 的循环更适合:

  • 单次 invocation 生命周期清晰;
  • 宿主已经拥有自己的持久化层;
  • 扩展数量有限;
  • 工作流由应用统一控制;
  • 希望快速接入多模型与工具;
  • 对 session event vocabulary 没有强约束。

Pi 的小核心减少了框架内部状态。业务一旦开始增加 Goal、审批、自动 follow-up、并发工具策略、恢复和预算,宿主会逐步补齐类似机制。复杂度没有消失,只是落在不同层。

团队选择框架时,不应只比较 Agent Loop 文件有多少行。要看系统准备把「继续运行的权力」放在哪里。

DeepSeek Harness 把权力分散给模型 finish、工具结果、inbox、生命周期 hook 和外围持久状态机,并通过事件日志协调。Pi 把权力集中在当前循环和几个宿主回调中,扩展层可以自行定义更高层协议。

设计建议

如果我们团队要自行实现 Agent Loop,我会保留以下原则。

第一,区分模型结束、工具批次结束、用户 turn 结束和长期任务结束。类型和事件名也要分开,避免一个 done 字段横跨所有层级。

第二,继续执行需要携带数据。插件要求再跑一轮时,应提交 steering 或 follow-up message。单独返回布尔值会让调试和组合越来越困难。

第三,工具调用后的模型回访应被建模为债务。普通工具结果产生一次回复债务,conclude 或 terminate 可以消除债务,steering 又会重新增加消息债务。用这套视角设计状态机,比堆叠 if (hasToolCalls) 更稳定。

第四,最大 token 需要独立结果,且不要执行疑似截断的工具参数。自动修复可以存在,但必须显式记录恢复过程。

第五,并行工具允许乱序完成,持久结果应按模型顺序提交。除非协议明确支持乱序 tool result,否则不要让网络时序改变上下文。

第六,取消必须区分已启动与未启动调用。已启动工具要排空或记录未知结果,未启动工具需要完整的 synthetic result,具体选择取决于日志协议。

第七,idle 不能充当长期完成信号。Goal phase、workflow state 和 Agent lifecycle 应拥有独立状态。

第八,模式切换要提交在请求边界。Plan、权限、模型路由和工具目录的变化,都不应在一个已开始的请求批次中途生效。

第九,恢复后的自动续行默认关闭。长期 Agent 会消耗资金,也可能修改外部系统。新进程接管旧任务时,应重新取得授权或租约。

第十,结束原因需要兼顾机器消费。completedblockedmax-tokensabortederrorinterrupted 这类结构化 union,比一段自然语言错误更适合监控、重试和审计。

架构落点

DeepSeek Harness 把结束判断分布在四个控制面上。

模型输出控制 step:还有没有工具回访债务。

inbox 控制 turn:还有没有当前轮待处理消息。

driver 控制 activity:队列耗尽后是否进入 idle。

Goal 与 Plan 控制工作流:idle 后是否开启新 Round,下一请求采用哪种策略。

Pi 将更多判断放在同一个运行循环里。工具调用和 pending steering 驱动内层循环,follow-up 驱动外层循环,shouldStopAfterTurn 提供宿主截停点,队列耗尽后发出 agent_end

DeepSeek Harness 更接近一个事件溯源的 Agent Runtime。Pi 更接近一个可嵌入的 Agent 执行内核。

评价两者没啥意义,我们需要关注的是其系统边界。

如果产品只需要一次 prompt 驱动的工具循环,Pi 的结构更容易维护。若产品已经出现跨轮 Goal、审批、恢复、多插件 steering 和高风险工具,DeepSeek Harness 的分层会减少后期协议冲突。

Agent Loop 最难处理的部分,从来都不是让模型继续调用工具。真正消耗工程时间的是:它为什么继续、谁允许它继续、失败后还能不能继续、恢复后应不应该继续,以及日志能否解释它曾经做过的每一次选择。

以上。

DeepSeek Harness 的 Cordis 插件架构

DeepSeek Harness 把模型、工具、会话、沙箱、文件系统、Agent 循环、调度和 UI 都交给插件。

系统没有一块承载业务能力的固定内核,Cordis 只维护上下文、服务注册、事件分发、依赖激活和副作用回收;Agent 能做什么,由启动时挂入的一棵插件树决定。

传统的插件有注册容易,撤销困难;初始化容易,失败回滚困难;全局单例容易,局部覆盖困难等问题,而 Cordis 把这些困难收进了运行时语义,Harness 再用 session、agent 和 preset 的业务约束补齐它。

今天聊了一下 DeepSeek Harness 的核心 Cordis 架构以及 Cordis 所谓「时空可组合」,究竟怎样落到代码里,又为何适合 Agent Harness 这类持续变化的运行时。

插件的历史

插件架构的历史并不短。Eclipse 在二十多年前已经把 IDE 拆成插件、扩展点和扩展,宿主声明可扩展位置,其他插件通过清单贡献菜单、编辑器和处理逻辑。

OSGi 更进一步,给 bundle 配置安装、启动、停止、更新和卸载生命周期,再通过共享服务注册表完成发布、查找和绑定。服务离开注册表时,依赖方必须跟着处理动态变化。

今天看 Cordis,能找到这些设计的清晰痕迹:动态服务、生命周期、注册表、声明式依赖、事件通知都已有成熟先例。

Cordis 没有发明插件系统。

它简化并重新组合了这些概念,让它们适合 TypeScript 应用内的细粒度组装。OSGi 的部署单元是 bundle,模块、生命周期、服务和安全各有一层;Cordis 的执行单元是 Fiber,一个函数或一个 Service 子类就能成为插件。Eclipse 的扩展点通常由宿主定义结构化清单,Cordis 让服务和类型化事件直接成为扩展面。Agent 运行时里的工具、提示词片段、模型适配器和审批策略变化频繁,如果每次扩展都要引入重量级模块边界,团队很快会绕过框架,重新写回几个全局数组。

Cordis 官方仓库把自己定位为「Meta-Framework of Spatiotemporal Composability」。

从源码看,「元框架」表示它不规定 Agent、Web 服务或机器人该有什么组件,只提供构造框架所需的基础语义。DeepSeek Harness 在其上定义 sessionstoolsllmagents 等服务,又用这些服务组装产品。Cordis 与 Harness 的关系更接近运行时机制和领域框架,而非通用插件平台与插件集合。

DeepSeek Harness 还把 Cordis 源码直接放进 vendor/,固定在明确的上游提交,并将包名重映射到 @deepseek-ai 命名空间。

项目维护的本地修改包含 Fiber 重入卸载加固、配置更新事务、HMR 精确监听、延迟配置解析等。这增加了维护成本,也换来了框架层的可审计性。Agent 能执行 Shell、修改文件、访问网络,插件生命周期出错会留下进程、监听器、终端模式或权限状态。

此时依赖一个本地的代码白盒,会让人放心一些。

调用链路图

简单的调用图如下所示:

最小内核

Cordis 的根对象是 Context。它同时承担依赖容器、插件挂载入口和事件入口。

服务通过稳定名称出现在 ctx 上,例如 ctx.toolsctx.llmctx.sessions。插件声明 inject 后,Cordis 只有在所需服务可用时才激活它;服务消失,依赖插件也会进入卸载或等待状态。配置文件中条目的先后顺序因此不承担启动顺序,依赖关系才承担。

这种处理解决了传统插件系统的第一个顽疾:隐含初始化顺序。

常见实现会遍历插件数组,依次调用 init。当工具依赖文件系统、文件系统依赖沙箱、UI 又依赖会话时,数组顺序就变成一套没有类型、没有诊断的依赖图。后来插入一个插件,顺序约束可能跨越几十个文件。

Cordis 把需求写进 inject,Fiber 会为每项依赖保存当前实现;缺少依赖时保持 PENDING,依赖齐备后进入 LOADINGACTIVE。启动失败则进入 FAILED。状态机至少让故障有了准确位置。

Context 还是一个代理对象。

插件直接读取 ctx.tools 时,反射层会检查它是否声明过依赖,并沿 Fiber 父链解析实现。未声明就读取会抛错;声明了但当前上下文不可用,也会抛出另一类错误。这种约束防止依赖藏在任意函数深处。源码里依然提供 ctx.get(name) 读取可选服务,区别在于调用方显式接受服务可能不存在。

这里的「服务定义、服务提供方、消费方」三种角色被完整建模。比如文件系统能力不能只写一个接口,也不能只挂一个本地实现。定义方稳定调用协议,提供方接入本地目录或远程沙箱,消费方把能力变成模型可见工具。Harness 把这组关系称为 capability seam。替换沙箱时,Shell、PTY、LSP 只要依赖同一能力面,就能整体迁移到新的执行环境,消费方无需知道提供方运行在本机还是远端。

「一切皆插件」有一个前提是:一切产品能力皆由插件贡献,Cordis 自身仍保留插件得以存在的机制。上下文代理、Fiber 状态机、服务存储、事件总线和 Loader 属于元层。

这并不是系统里没有内核,更准确的说,应该是,内核不拥有模型、工具和循环等产品特权。

空间组合

同一个进程里,服务名称必须稳定,实例又不能全局唯一。两个 Agent 可能选择不同模型、不同工具集、不同 persona 和不同沙箱。如果 ctx.llm 永远指向一个全局对象,插件替换只发生在进程级,无法满足多会话并存。

Cordis 的 Context.isolate 为指定服务名创建一个 realm 标签。服务注册和查找都以该标签定位,同名服务可以在不同子上下文里各自存在。两个 isolate 调用传入同一标签时会加入同一 realm;使用新标签时彼此隔离。子上下文通过原型继承父上下文,隔离映射按层遮蔽,因此局部配置不需要复制整棵容器。

这解释了「空间可组合」的第一层:组合具有位置。插件挂在哪个上下文,决定它提供的服务、注册的监听器和持有的副作用在哪个范围可见。传统依赖注入容器也有 singleton、request、session 等 scope,Cordis 的差异在于作用域与插件树、事件过滤、资源所有权使用同一个 Context 表达。开发者无需在四套 API 之间同步身份。

DeepSeek Harness 又增加了 dsh-scope。它用不透明对象作为 scope key,维护父子关系,并创建带路由身份的事件 receiver。注册视图沿父链向下继承:Agent 能看到所属 preset 的提示词和工具。事件则沿链向上接纳:preset 级监听器能收到其下 Agent 的事件,兄弟 Agent 互不串线。这是第二层空间结构,解决的是领域身份,而非单个服务名的 realm。

为何需要两层?isolate 处理「哪个 tools 服务实例」,dsh-scope 处理「同一工具注册表里,哪些注册项对当前 Agent 可见」。前者适合替换提供方,后者适合对注册内容分层。把所有差异都做成独立服务实例,会放大内存和初始化成本;把所有差异都塞进一个全局注册表,又会让过滤规则散落在调用点。这两层模型把实例隔离和内容路由分开了。

Agent preset 是空间组合的完整应用。一个 preset 的 Cordis 配置会挂在一个长期存在的 scope 下,Agent 创建时把自己的 scope key 绑定到该 preset。相同 preset 的并发首次使用通过 single-flight 共享一次挂载,后续 Agent 复用这份组合。文件变化后,新会话加入新一代组合,已有会话保留原来的代际。这个策略避免会话运行中途突然更换工具或提示词,代价是旧代际要保留到整棵运行时退出,配置频繁变化时内存会按代际增长。

源码还专门审计 preset 子树是否把服务发布到了 root realm。发生这种泄漏时,第二个会话挂载同一 preset 会与第一个冲突,所谓会话级组合也会退化成进程全局状态。DeepSeek Harness 在发布 Agent 前拒绝这种配置。空间隔离若只靠约定,迟早会被一个没有 isolate 的 provider 穿透;运行时审计比文档警告可靠。

这套空间模型存在认知成本。插件作者要同时理解 Context 父链、服务 realm、业务 scope 父链和事件过滤方向(当然,在当前 Vibe Coding 盛行的时代,也可以作者不理解,直接让 AI 来搞)。

在认知不清楚的时候,常见的错误往往表现为某项能力「看不见」或意外泄漏,类型系统无法证明运行时挂载位置。DeepSeek Harness 用 preset 挂载审计、包级 invariant 和真实组合测试降低风险,却没有消除模型复杂度。团队若只需要单进程单 Agent,直接引入整套空间语义会显得过重。

时间组合

插件能挂载只是静态组合。运行期间服务上线、配置改变、插件失败或上下文销毁,系统还要回到一致状态。Cordis 用 Fiber 和 effect 管理这条时间轴。

每次插件应用都会产生一个 Fiber。Fiber 记录父上下文、原始配置、解析后配置、依赖实现快照、生命周期状态和 disposables。插件调用 ctx.effect() 注册副作用,effect 的执行结果返回 disposer。事件监听、服务提供、子插件和访问器最终都进入这套所有权体系。Fiber 卸载时按注册的逆序执行清理,并等待异步清理达到静止状态。

逆序本身不是一个特别要讲的事情,因为这是必须的。

若插件先启动子进程,再注册输出监听,最后暴露服务,销毁时应先撤销服务,停止新请求,再移除监听,最后结束进程。资源创建顺序的反向通常就是依赖安全的拆卸顺序。

传统插件常给出一个 deactivate() 钩子,把所有清理责任推给作者;漏掉一个定时器或事件监听,热加载几次便出现重复执行。Cordis 让每次注册同时产生撤销动作,框架持有所有权。

DeepSeek Harness 的 vendor 版本进一步处理了重入场景:effect 在执行 setup 前先登记所有者包装;插件发布事件时,观察者可能同步卸载它;异步 cleanup 已经开始后,其他调用者仍能等待同一次清理;Fiber 处于 UNLOADING 时拒绝创建新 effect。这些代码看起来全是繁琐的边界处理,但为了保证生命周期并发的安全,一个都不能少。否则插件的热卸载就只是个玩具,没法真正落地。

依赖变化也属于时间组合。某个服务被提供后,反射层通知所有声明该依赖的 Fiber 重新检查;条件满足便激活。服务撤销后,依赖方会卸载并回到等待。OSGi 早已采用动态服务注册表,Cordis 的新意主要在于把动态依赖、作用域上下文和 effect 所有权压进一个很小的进程内模型。代价同样继承自 OSGi:任何持有服务引用越过生命周期的代码,都可能在提供方撤销后继续调用过期对象。Cordis 保存加载时的实现快照,却无法替业务代码管理逃逸引用。

Loader 把时间组合延伸到配置。DeepSeek Harness 的 profile 由多层 bundle patch 叠加:基础 bundle、模式 bundle、profile patch、home patch、命令行 overlay。patch 按 id 定位条目,配置采用整块替换。整块替换要求用户重述保留字段,使用上稍显笨重,却避免深度合并规则在数组、表达式和删除语义上制造歧义。

配置热更新时,Loader 先导入候选插件,再卸载旧实例并应用候选;候选失败会恢复旧插件或旧配置。Group 对一批子条目并发启动,收集全部结果,只要一项失败就删除新增项并重建旧配置。Include 读取候选文件、在副本上应用 patch、完成树协调后才提交缓存。用户 patch 语法错误或新插件启动失败时,最后一棵可用树继续运行。

这已经接近配置事务,却不能等同于数据库事务。

插件 effect 可能调用外部 API、创建远端资源、发送消息;disposer 只能做补偿,无法保证外部世界回滚。Loader 能保证自己管理的树和服务注册恢复,无法撤回所有不可逆副作用。插件开发规范仍要限制初始化期行为,把外部写入延迟到真正的业务请求,或者设计幂等键和补偿路径。

时间组合还带来可观的测试面积。挂载成功只是第一条路径,还要覆盖依赖晚到、依赖撤销、初始化失败、卸载重入、异步清理、热更新失败、回滚再次失败。DeepSeek Harness 要求注册项证明 disposal,产品可见插件还要通过真实 Loader 组合测试。这个成本无法靠框架消失,只能被框架集中暴露。相比线上出现幽灵监听器和半更新状态,还是愿意支付这部分测试费用。

事件契约

插件之间只靠服务调用,会把所有扩展都变成接口方法。宿主每增加一项策略,就要修改服务定义。Cordis 同时提供类型化事件,并区分 emitparallelserialbailwaterfall

DeepSeek Harness 最依赖的是 waterfall。它的监听器拿到 next,调用后把控制权交给下一层,不调用就截断链条。模型请求、工具执行和轮次控制都能被插件包裹。审批策略可以在工具执行前拒绝,重试插件可以包围模型流,日志插件可以观察前后状态。它与 Koa 一类中间件链相似,但事件名和 Context 过滤让同一个分发器覆盖多个能力域。

waterfall 也最容易出错。一个只想记录日志的监听器忘记调用 next(),整条能力链便被短路。DeepSeek Harness 把「必须调用 next()」写进项目级规则,并在事件文档里记录 dispatch mode。类型能保证参数,却很难保证 continuation 一定执行。代码评审和组合测试仍是主要防线。

事件的持久性也被刻意分层。agent/*tools/* 事件用于活跃运行时的拦截;会话事件追加到日志,承担恢复、fork、UI 回放和模型历史投影。DeepSeek Harness 规定「模型可见即已记录」:进入模型请求的输入必须能从 session log 重建。插件若偷偷修改 prompt 却不产生会话事实,重放结果会漂移,问题也无法审计。

这条约束说明,一切皆插件并不意味着一切皆事件。直接能力调用放进 Service,策略和拦截放进实时事件,需要持久化的事实进入 session log。三类通信各自承担调用、扩展和历史。将它们混成一个全局 event bus,短期代码更少,长期会失去时序语义和数据权威。

Agent 适配

Agent Harness 比普通 Web 服务更需要动态组合。模型供应商会变,工具权限随工作区变化,子 Agent 的执行环境可能与父 Agent 不同,UI 还要消费同一份流式过程。如果主循环直接 import 每个能力,任何替换都会改循环;循环逐渐成为依赖最多、风险最高的文件。

DeepSeek Harness 的默认 agent loop 仍然存在,但它也是一个服务提供方。循环读取 session log 生成历史,组装插件贡献的 prompt 和 tool schema,通过 agent/request 进入模型适配器,再经过工具流水线记录结果。扩展点围绕步骤、请求、工具和停止阶段布置。新功能通常挂到这些事件或注册表,项目规则甚至要求修改 agent loop 时同步更新架构文档。

我赞成这种限制。循环承担控制流,频繁承载产品策略后会迅速腐化。压缩上下文、重试、审批、工具超时、计划模式、子 Agent 调度都可以有自己的生命周期与配置。它们进入循环的接口必须少且稳定。插件化不会自动获得解耦,真正起作用的是 DeepSeek Harness 为能力选择了明确的 seam,并拒绝消费方特有逻辑污染定义层。

UI 作为插件也有实际意义。Web 和 headless profile 在共享 base bundle 上增加不同条目,后者可以完全不启动服务器。UI 驱动 ctx.agents,订阅 session/event 渲染状态,没必要成为循环的一部分。服务端、CLI、ACP 和浏览器界面可以共享会话及 Agent 语义,同时保留自己的传输和展示逻辑。

插件树还提供自省基础。Fiber 保留名称、状态、依赖和 effect 元数据,DeepSeek Harness 能实现查看、挂载、卸载自身插件的能力。对 Agent 而言,这比普通应用更敏感:模型可以通过工具改变运行时,错误配置可能直接扩大权限。源码中的 sandbox 和 approval 仍是独立能力,插件架构没有天然安全性。自修改必须受工具权限、配置校验和作用域审计约束。

架构代价

第一项代价是启动与运行时开销。每个插件产生 Fiber,服务访问经过 Proxy 和反射解析,事件分发要执行作用域过滤,注册项还要保存 disposer 与诊断元数据。对于 LLM Agent,单次模型请求通常以百毫秒到秒计,这些 JavaScript 调度开销很难成为主要瓶颈;高频 token chunk、文件扫描或终端字节流若全部穿过通用事件总线,成本会被放大。DeepSeek Harness 把流式模型输出定义为能力语义,但大量数据处理仍应留在具体 provider 内,事件只承载必要扩展点。

第二项代价是故障面扩大。插件初始化可以同步抛错、异步拒绝、等待缺失服务,也可能在 disposer 中失败。Group 并发启动缩短时间,却会带来多个兄弟同时失败的 AggregateError。DeepSeek Harness 的 boot 会等待 Loader 结算,审计所有启用条目是否加载与激活,失败时先 dispose 部分构造的上下文,再退出。少做任何一步,都可能让终端停留在 raw mode,或者让后台 watcher 继续运行。

第三项代价是配置成为编程接口。cordis.yml 支持 !!js 表达式,条目可以按环境禁用,配置会在依赖激活后求值。灵活性很高,静态分析能力随之下降。表达式读取服务时,求值时机与上下文位置都会影响结果。DeepSeek Harness 只允许 configdisabled 使用插值,其他元数据保持字面量,并让错误尽早暴露。这仍要求运维人员理解插件依赖和 patch 覆盖语义,配置文件已经超出普通 YAML 参数表的复杂度。

第四项代价是生态兼容。Cordis 的服务名和事件类型提供了源码级协议,插件版本升级仍可能修改配置、事件 payload 或生命周期假设。DeepSeek Harness 目前处于预发布阶段,仓库规则允许直接拒绝旧磁盘格式,也没有承诺 session 格式兼容。现在的可组合性主要服务于同一发行版内的替换和扩展,尚不能推导出跨版本插件 ABI 稳定。

第五项代价是组织治理。一切都能成为插件后,团队容易把每段十几行逻辑都拆成包,形成依赖图膨胀、文档分散和测试启动缓慢。DeepSeek Harness 用 package 分组、Service Definition/Provider/Consumer 角色、每包 README、运行时 invariant 和真实组合测试控制边界。这些规范本身就是成本。小团队若没有维护扩展生态或多 profile 的需求,模块化函数加显式依赖可能更合适。

历史对照

从 OSGi 看 Cordis,动态服务和生命周期联动已有先例。服务注册、发现、撤销后触发依赖变化,这条主线几乎一致。Cordis 值得学习的新点,是把资源清理统一成 effect,并让子插件、服务、监听器都归属于同一个 Fiber。OSGi 的 bundle 生命周期更完整,也更重;Cordis 选择应用内细粒度对象,失去了类加载隔离、标准版本解析和安全层,获得了低门槛组合。

从 Eclipse 看 DeepSeek Harness,profile 与 bundle patch 很像部署时组装,Service 和事件类似扩展点。差异在于 Eclipse 扩展通常围绕稳定宿主能力,DeepSeek Harness 连 agent loop 和 UI 都可替换,核心插件与第三方插件共享同一挂载机制。这个平权减少了特权内核,风险也更集中到协议治理:循环能被替换,不代表任何替代循环都遵守 session log、权限和工具时序约束。

从依赖注入容器看,Cordis 的服务解析并不陌生。新的组合来自 DI 与生命周期的绑定。普通容器负责构造对象,定时器、监听器和子进程仍由业务代码清理;Cordis 把注册动作收敛成 effect。空间 scope 与时间 ownership 同时存在,插件才能在某个 Agent 范围内挂载,并随该 Agent 完整撤销。

从微内核看,DeepSeek Harness 的内核确实很轻,但其 Loader 和配置协调已经承担不少平台职责。它要解析模块、等待依赖、处理 HMR、执行事务式更新、保存最后可用树、输出诊断。这提醒我,微内核减少的是业务特权,不会减少生命周期复杂度。能力越动态,内核对失败语义的要求越高。

Cordis 的贡献可以概括为一种紧凑的组合坐标:Context 决定插件位于哪里,Fiber 决定插件在何时有效,Service 负责直接能力,Event 负责横切协作,effect 负责撤销。DeepSeek Harness 在这个坐标系上加入 Agent scope、持久会话事件和配置事务,使其能承载多会话、多 profile 与热更新。

工程取舍

当我想要用类似架构时,会先确认三个条件,基于这三个条件判断来是否采用。

其一,能力确实需要独立替换,至少存在两个生产提供方或明确的外部扩展需求。

其二,同一进程需要多套组合并存,或者运行期间需要可靠更新。

其三,团队愿意为卸载、回滚和真实组合测试持续付费。

缺少这些条件,插件框架容易沦为复杂的工厂模式。

能力切分要从消费方开始。先列出谁调用、调用时需要哪些稳定语义,再设计 Service Definition;随后实现 provider,最后把面向模型的 schema、展示和错误文本留给 consumer。接口只有一个内部调用者时,保留私有闭包,不急着升格为公共 service。可替换性必须由真实替代者证明。

所有注册都要有明确所有者。注册工具、提示词片段、适配器、监听器时同步返回 disposer;卸载测试要观察注册项确实消失。涉及异步资源时,dispose 完成的定义要包含子进程退出、队列排空和 watcher 关闭,不能只发出取消信号。DeepSeek Harness 对 Fiber quiescence 的处理值得照搬。

配置更新要区分内存一致性和外部副作用。Loader 可以恢复旧树,插件初始化若已经创建云资源,回滚需要业务补偿。规则可更保守一些:挂载阶段只做校验和本地注册,外部写操作放到带幂等标识的运行阶段。确实要在初始化创建资源时,必须让 disposer 能识别部分完成状态。

作用域要控制在两种以内。实例隔离与注册可见性已经足够表达多数 Agent 场景,再增加租户、请求、工作区、角色四套独立 scope,调试成本会失控,需要根据实际业务需求和必要性来判断。当需要新增维度时,先判断它属于服务实例选择、注册过滤、持久数据权限还是请求参数。很多所谓新 scope,其实只是一次显式参数传递。

事件也要克制。需要唯一返回值的调用用 Service,需要持久恢复的事实写 session log,需要多个插件观察或包裹的阶段才用事件。waterfall 必须把短路当成公共协议,监听器顺序也要有测试。事件名虽然能降低导入耦合,却会增加时序耦合;后者通常更难排查。

小结

DeepSeek Harness 选择 Cordis,解决的主要矛盾是 Agent 能力变化速度与运行时一致性之间的冲突。模型、工具、沙箱和 UI 都可以替换并不稀奇;这些能力可以在局部作用域内共存,能随依赖变化启停,能在配置失败后保留旧树,卸载时还能回收监听器、服务和子插件,才构成可用的插件架构。

它也没有抹平工程风险。Proxy 隐藏了查找路径,双层 scope 增加理解成本,热更新无法回滚任意外部副作用,预发布阶段也缺少跨版本兼容承诺。团队采用这套思路时,应复制它的生命周期纪律和能力边界,别只复制「一切皆插件」的目录结构。

源码给我们一些启发,把插件设计的评审顺序倒过来:先问如何撤销,再问如何注册;先证明局部挂载不会泄漏,再讨论全局复用;先定义失败后保留哪一棵树,再讨论热更新速度。做到这些,时空可组合才是一组可以验证的运行时语义。

这套架构个人理解是想往 Agent 的实时「自生长」,通过 AI 的能力在使用过程中让自己更强大。

逼逼这么多,主要还是在这个过程中学习一下,说实话,有了 DeepSeek Harness,想自己开发一个专属的 Agent ,快捷方便了很多,eg且这是 MIT 协议的。

以上。

参考资料

 

从 Pi 到 DSH:Agent Harness 如何从「可扩展」走向「自生长」

大模型能力持续提升之后,Agent 系统的竞争焦点正在发生变化。

过去,我们主要关心模型能不能理解任务、调用工具、编写代码。现在,更重要的问题逐渐变成:模型之外的系统,能否为 Agent 提供一个可以持续变化的运行环境?

这个运行环境通常被称为 Agent Harness

Harness 负责把模型、提示词、工具、记忆、上下文、权限和外部环境连接起来。它决定模型能够看到什么、调用什么、保存什么,以及一次行动会产生怎样的系统影响。

从这个角度看,Agent 的能力并不只来自模型,用一个公式来表达,大概如下:

Agent 能力
=
模型能力
× 上下文组织
× 工具系统
× 运行时结构
× 生命周期治理

如果 Harness 是固定的,那么即使模型越来越强,Agent 的能力边界仍然主要由开发者预先决定。

如果 Harness 可以扩展,用户和开发者就能不断为 Agent 增加工具、技能和工作流。

但如果我们希望 Agent 进一步具备「自生长」能力,仅仅提供扩展接口还不够。系统还必须允许 Agent:

  1. 发现自己的能力缺口;
  2. 生成或引入候选能力;
  3. 在运行时安全地激活这些能力;
  4. 验证变化是否有效;
  5. 在失败时完整回滚;
  6. 将成功经验沉淀为可复用组件;
  7. 淘汰已经失效或长期无用的能力。

Pi 与 DSH 可以被视为这条演进路径上的两个阶段。

Pi 回答的是:

如何构建一个足够小、足够开放,可以持续扩展的 Agent Harness?

DSH 进一步追问:

如何让 Agent 不只是使用外部提供的扩展,而是安全地参与自身能力结构的形成?

这不是从一种插件格式切换到另一种插件格式,而是 Agent Harness 从开放扩展平台能力生长环境的转变。

1. 什么是 Agent Harness

一个最简单的 Agent,可以被表示为:

Model
  ↓
Prompt
  ↓
Tool Call
  ↓
Environment

模型接收上下文,生成工具调用,再根据工具结果继续推理。

但在真实系统里,Agent 还需要处理更多问题:

  • 当前有哪些工具可用?
  • 工具由谁注册?
  • 不同任务应该使用什么模型?
  • 哪些信息应该进入上下文?
  • 哪些记忆需要跨会话保存?
  • 如何限制文件、网络和进程权限?
  • 工具调用失败后如何恢复?
  • 新能力如何加载?
  • 组件移除后如何清理副作用?
  • 不同组件之间的依赖如何表达?

于是,Agent 的实际结构更接近于:

                ┌─────────────┐
                │    Model    │
                └──────┬──────┘
                       │
                ┌──────▼──────┐
                │ Agent Loop  │
                └──────┬──────┘
                       │
       ┌───────────────┼────────────────┐
       │               │                │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│    Tools    │ │    Memory   │ │   Context   │
└─────────────┘ └─────────────┘ └─────────────┘
       │               │                │
       └───────────────┼────────────────┘
                       │
                ┌──────▼──────┐
                │ Environment │
                └─────────────┘

Harness 并不是模型外面的一层简单包装。

它实际上规定了 Agent 的:

  • 能力边界;
  • 行为边界;
  • 状态边界;
  • 权限边界;
  • 演化边界。

因此,当我们讨论 Agent 是否可以扩展、重构甚至自生长时,本质上讨论的不是模型参数如何变化,而是 Harness 能否安全地改变自身结构

2. Pi:以最小内核换取最大扩展空间

Pi 的核心思想非常明确:

不要预先替用户决定完整工作流,而是提供足够小的基础能力,让用户自己构建需要的 Harness。

Pi 将自己定义为一个最小化的 Agent Harness,并通过 Extensions、Skills、Prompt Templates、Themes 和 Packages 提供扩展能力。它允许用户修改工具、命令、模型提供商、工作流、上下文处理和终端界面,也支持将这些资源打包后通过 npm 或 Git 分发。(pi.dev)

这背后是一种「原语优先」的设计哲学:

不是:
内置所有功能

而是:
提供少量原语
+
开放扩展接口
+
允许用户自行组合

2.1 最小化不是能力不足

传统 Agent 产品通常倾向于内置更多功能:

  • 规划模式;
  • 子 Agent;
  • 权限确认;
  • MCP 集成;
  • 后台任务;
  • 长期记忆;
  • 沙箱;
  • Todo 系统。

Pi 选择了另一条路线:这些能力不一定需要成为内核的一部分,它们可以通过扩展或外部环境实现。

Pi 当前提供文件读取、文件修改、内容搜索和命令执行等内置工具,同时允许通过命令行配置工具集合,也允许 Extension 注册新的工具或覆盖现有工具。(pi.dev)

这种设计的意义把稳定的内核和变化的能力分开:

稳定部分:
模型调用、Agent Loop、会话、基础工具、扩展 API

变化部分:
工具、技能、上下文、工作流、权限策略、界面、模型路由

只要扩展接口足够开放,功能就不必全部固化在核心代码中。

2.2 Pi 的四类扩展资源

从 Harness 的角度看,Pi 的可扩展性主要体现在以下几个层面。

2.2.1 Extensions:改变运行时行为

Extension 是可以进入 Agent 运行时的代码模块。

它可以:

  • 注册工具和命令;
  • 监听工具调用;
  • 拦截或修改行为;
  • 改变上下文;
  • 切换模型;
  • 增加权限检查;
  • 实现自定义 UI;
  • 覆盖内置工具;
  • 连接外部系统。

因此,Extension 改变的不只是模型“知道什么”,还可以直接改变 Harness“如何运行”。

2.2.2 Skills:注入按需能力

Skill 更接近一种可复用的过程知识。

它可以告诉 Agent:

  • 在什么情况下使用某种方法;
  • 如何执行特定工作流;
  • 如何调用外部命令;
  • 如何处理某类项目;
  • 如何检查输出质量。

Pi 将 Skill 设计为按需加载的能力包,使 Agent 不必在每次会话开始时把所有知识都放进上下文。(pi.dev)

2.2.3 Prompt Templates:固化交互模式

Prompt Template 将常见任务封装成可复用提示词,例如:

  • 代码审查;
  • 架构分析;
  • 发布检查;
  • 测试生成;
  • 故障排查。

它改变的是任务入口和交互结构。

2.2.4 Packages:分发完整能力

Package 可以将 Extensions、Skills、Prompt Templates 和 Themes 组合起来,通过 npm、Git 或本地路径安装。Pi 还支持临时加载 Package,使组件只对当前运行生效。(github.com)

这使 Pi 不只是一个 Agent,也成为一个 Harness 能力分发平台:

Pi Package
├── Extensions
├── Skills
├── Prompts
└── Themes

2.3 Pi 的关键突破:Agent 可以参与修改 Harness

Pi 最值得关注的地方,不只是「插件多」。

它的官方描述明确鼓励用户让 Pi 编写自己需要的扩展,并在修改后通过重新加载继续当前工作。(pi.dev)

这意味着一种新的工作方式正在出现:

Agent 发现缺少某项能力
        ↓
Agent 编写 Extension
        ↓
用户检查或确认
        ↓
重新加载 Harness
        ↓
Agent 使用新增能力继续任务

过去,Agent 与开发工具之间的关系通常是:

人类开发 Harness
Agent 使用 Harness

在 Pi 中,它开始变成:

人类定义目标
Agent 修改 Harness
Agent 使用修改后的 Harness

这是从「可扩展」走向「自生长」的重要一步。

但是,这还不等于完整的自生长。

3. 行为自治不等于结构自治

理解 Pi 与 DSH 的关系,需要区分两种不同的自治。

3.1 行为自治

行为自治指 Agent 可以在已有能力范围内自主决定:

  • 下一步做什么;
  • 调用哪个工具;
  • 如何拆解任务;
  • 是否修改文件;
  • 是否运行测试;
  • 如何根据结果调整计划。

此时,Agent 的选择空间是动态的,但能力集合通常是预先确定的。

固定工具集合
    ↓
Agent 自主选择工具
    ↓
完成任务

3.2 结构自治

结构自治指 Agent 可以改变自己的能力结构:

  • 创建新工具;
  • 安装新组件;
  • 替换 Memory;
  • 修改上下文策略;
  • 调整模型 Router;
  • 增加权限规则;
  • 组合新的执行流程;
  • 卸载无效组件。
当前能力结构
    ↓
Agent 发现能力缺口
    ↓
生成候选组件
    ↓
改变 Harness
    ↓
形成新的能力结构

Pi 已经为结构自治打开了入口。

但它的扩展机制主要回答的是:

如何把一项新能力加入系统?

真正的自生长还必须回答:

  • 新能力应该在什么条件下激活?
  • 它依赖哪些已有能力?
  • 它失败后能否完整退出?
  • 它注册的工具和监听器由谁撤销?
  • 如何判断变化带来了正收益?
  • 如何防止不同扩展相互冲突?
  • 如何把一次临时变化沉淀成长期能力?
  • 如何淘汰不再适用的组件?

这些问题将我们从扩展系统带到了组件运行时。

4. DSH:为 Harness 自生长建立运行时基础

本文讨论的 DSH,不只是另一个插件系统。

它试图将 Agent Harness 表达为一个可以在运行中不断组合和重组的组件系统。

其基本结构可以概括为:

Component
=
Context
+
Effect
+
Cleanup
+
Coeffects

这四个概念分别回答四个问题:

概念 回答的问题
Context 组件可以看到和使用什么?
Effect 组件加入系统后会产生什么影响?
Cleanup 组件退出时如何撤销影响?
Coeffects 组件依赖哪些外部能力或其他组件?

DSH 的目标不是让所有模块都能任意修改系统,而是让变化成为一种显式、可追踪、可逆的运行时操作

4.1 Context:为能力提供明确边界

一个组件不能直接访问所有系统状态。

它应该通过 Context 获取所需能力,例如:

Context
├── Model Registry
├── Tool Registry
├── Memory
├── Event Bus
├── Session
├── Logger
├── Storage
├── Permission
└── Environment

Context 的第一层作用是解耦。

组件不需要知道某个能力的具体实现,只需要知道运行环境向它提供了什么。

例如,一个模型路由组件可能只依赖:

Model Registry
Task Metadata
Usage Metrics

一个长期记忆组件可能只依赖:

Session Events
Embedding Service
Storage
Context Injector

一个工具组件则可能只依赖:

Tool Registry
Credentials
Network
Logger

Context 的第二层作用是限制能力边界。

如果组件只能通过 Context 访问环境,那么系统就可以决定:

  • 哪些资源向它开放;
  • 哪些能力只读;
  • 哪些调用需要权限;
  • 哪些状态不能跨组件共享;
  • 哪些操作只能在 Sandbox 中执行。

因此,Context 不只是依赖注入,也是一种能力安全模型:

组件能做什么
≈
Context 向它暴露什么

4.2 Effect 与 Cleanup:让变化可以被撤销

当组件加入 Harness 时,通常会产生副作用。

例如:

  • 注册一个工具;
  • 添加一个事件监听器;
  • 打开数据库连接;
  • 启动后台任务;
  • 修改模型路由;
  • 向上下文注入内容;
  • 挂载文件目录;
  • 注册快捷键;
  • 创建缓存;
  • 改变权限策略。

这些变化可以统一理解为 Effect:

Component
    ↓
Effect
    ↓
Harness 状态发生变化

问题在于,大多数插件系统只强调如何创建 Effect,却没有把撤销 Effect 当作同等重要的能力。

于是,组件卸载后可能出现:

  • 工具仍然留在 Registry 中;
  • 事件监听器重复注册;
  • 后台任务继续运行;
  • 网络连接没有关闭;
  • 临时文件没有删除;
  • Context 中残留失效状态;
  • 新旧路由规则同时生效。

在 DSH 中,Effect 应该返回对应的 Cleanup:

Effect(Context) → Cleanup

例如:

function activate(context{
  const unregister = context.tools.register(myTool)

  return function cleanup() {
    unregister()
  }
}

更复杂的组件可能产生多个副作用:

function activate(context{
  const stopTool = registerTool(context)
  const stopListener = registerListener(context)
  const stopWorker = startWorker(context)
  const closeConnection = connectDatabase(context)

  return async function cleanup() {
    await stopWorker()
    stopListener()
    stopTool()
    await closeConnection()
  }
}

这使组件具备完整生命周期:

创建
  ↓
激活
  ↓
运行
  ↓
停用
  ↓
清理

4.3 时间可组合性:让 Agent 有资格试错

Effect 与 Cleanup 提供的是一种时间可组合性

一个组件可以在某个时刻进入系统,也可以在另一个时刻完整退出:

t₀:Harness 原始状态

t₁:组件进入
    → 注册工具
    → 启动服务
    → 注入上下文

t₂:组件运行

t₃:组件退出
    → 移除工具
    → 停止服务
    → 撤销上下文

t₄:Harness 恢复到可预测状态

这对普通插件系统很重要,但对自生长系统更重要。

因为自生长必然包含试错。

Agent 创建的新工具、新路由器、新记忆策略,不可能每次都正确。如果一次失败的尝试不能被完整撤销,那么每次实验都会向系统中留下残余状态。

最终,所谓“生长”就会退化成不可控的复杂度积累。

因此,自生长的基本过程不应该是:

生成组件
  ↓
永久安装

而应该是:

生成候选组件
      ↓
执行受控 Effect
      ↓
验证运行结果
   ┌──┴──┐
 成功    失败
  │       │
保留   Cleanup

Cleanup 的意义不只是“支持热卸载”。

它赋予 Agent 一种更关键的能力:

在不永久污染系统的前提下试验新的自身结构。

4.4 Coeffects:让依赖关系成为运行时的一部分

时间可组合性解决的是组件何时进入、何时退出。

但 Agent Harness 的组件并不是彼此孤立的。

如果依赖关系只是组件内部的隐式假设,运行时就无法回答:

  • 组件现在能否启动?
  • 它缺少什么能力?
  • 某个依赖消失后,哪些组件应该停用?
  • 替换一个 Provider 会影响哪些组件?
  • 哪些组件可以被安全卸载?
  • 一个候选组件是否与当前环境兼容?

Coeffects 用于显式表达组件所需的外部条件:

Effect:
组件对系统做什么

Coeffects:
组件需要系统提供什么

例如:

const vectorMemory = {
  coeffects: [
    "embedding-model",
    "vector-storage",
    "session-events"
  ],

  effect(context) {
    // 激活 Memory
    return cleanup
  }
}

运行时可以根据当前 Context 判断组件是否具备激活条件:

依赖全部满足
    ↓
组件激活

依赖不完整
    ↓
组件保持未激活

依赖运行中消失
    ↓
执行 Cleanup

这使 Harness 不再只是一个插件列表,而是一个动态依赖图:

Credential
    ↓
GitHub Provider
    ↓
GitHub Tool
    ↓
Repository Agent

或者:

Embedding Model ─┐
                 ├→ Vector Memory → Context Injector
Vector Storage ──┘

4.5 空间可组合性:让生长不是无序堆积

Coeffects 提供的是一种空间可组合性

组件不是按照固定顺序硬编码在系统中,而是根据能力和依赖形成运行时拓扑。

当一个能力出现时,依赖它的组件可以被激活;当这个能力消失时,相关组件可以被停用:

Sandbox 出现
    ↓
Code Executor 激活
    ↓
Test Runner 激活
    ↓
Coding Agent 获得安全执行能力

如果 Sandbox 被移除:

Sandbox 消失
    ↓
Code Executor Cleanup
    ↓
Test Runner Cleanup
    ↓
相关工具从 Agent 能力集合中撤销

这与传统插件系统有一个关键区别。

传统插件系统更像:

安装了什么
=
系统拥有什么

而动态组件运行时更像:

当前可用能力
=
当前组件
× 当前依赖
× 当前环境
× 当前权限

这使 Harness 的结构可以随着任务和环境变化,而不是不断向全局系统中永久添加模块。

真正的生长,不是组件数量持续增加,而是系统能够形成更适合当前任务的能力结构。

4.6 从动态重组到自生长闭环

Context、Effect、Cleanup 和 Coeffects 为动态重组提供了基础。

但必须强调:

动态重组是自生长的必要条件,不是自生长的全部。

完整的自生长至少需要五个阶段。

4.6.1 第一阶段:发现能力缺口

Agent 首先要判断自己缺少什么。

例如:

  • 当前没有访问某类数据的工具;
  • 现有 Tool 无法处理特定协议;
  • Memory 无法支持长期任务;
  • 当前模型不适合某类输入;
  • 缺少项目专用的验证流程;
  • 当前执行环境不满足安全要求;
  • 工具调用成本或延迟过高。

能力缺口不能只由「工具调用失败」判断。

它还可能来自:

  • 任务规划;
  • 运行指标;
  • 用户反馈;
  • 测试结果;
  • 历史失败记录;
  • 现有能力清单;
  • 环境变化。

因此,自生长的起点不是生成代码,而是:

目标要求
-
当前能力
=
能力缺口

4.6.2 第二阶段:生成或引入候选能力

识别缺口后,Agent 可以选择不同方式补齐能力:

  • 编写新的 Tool;
  • 生成新的 Skill;
  • 安装已有 Package;
  • 包装外部 API;
  • 组合已有组件;
  • 替换 Memory Provider;
  • 调整模型 Router;
  • 创建新的工作流;
  • 启动一个专用子 Agent;
  • 为现有组件增加适配层。

这里的关键词是「候选」。

Agent 生成的组件不能因为能够编译或加载,就自动成为系统的永久能力。

生成成功
≠
能力有效
≠
适合长期保留

4.6.3 第三阶段:在受控环境中激活

候选组件应该先进入受限 Context。

系统需要明确:

  • 它可以访问哪些目录;
  • 可以调用哪些网络服务;
  • 是否可以读取凭证;
  • 可以注册哪些工具;
  • 能否修改全局状态;
  • 资源消耗上限是多少;
  • 运行时间是否受限;
  • 哪些副作用必须登记。

这一点尤其重要,因为扩展代码通常拥有较高权限。

以 Pi 为例,其官方安全文档明确说明,内置工具和 Extension 默认继承启动 Pi 的进程权限;第三方 Package 可能执行代码或指示模型执行操作,因此需要代码审查,并在需要时依赖容器或虚拟化提供真正的隔离边界。(github.com)

如果 Agent 开始自行生成和加载组件,权限问题只会更加突出。

所以,自生长必须建立在受控运行时上,而不能建立在「默认信任所有新代码」之上。

4.6.4 第四阶段:验证、保留或回滚

候选能力激活后,系统需要对它进行验证。

验证可以包括:

  • 能否完成目标任务;
  • 是否通过单元测试;
  • 是否破坏已有功能;
  • 是否产生未声明的副作用;
  • 是否扩大了不必要的权限;
  • 是否降低了任务成本;
  • 是否提高了成功率;
  • 是否造成明显的上下文膨胀;
  • 是否与现有组件冲突;
  • Cleanup 是否完整。

验证结果通常有三种:

验证通过
    ↓
保留或固化

部分通过
    ↓
修改后重新试验

验证失败
    ↓
Cleanup + 回滚

因此,自生长不是一次性的代码生成,而是一个选择过程:

Generate
→ Activate
→ Observe
→ Evaluate
→ Select

没有评估的生成,只是变化。

经过评估和选择的变化,才可能成为生长。

4.6.5 第五阶段:沉淀、复用与淘汰

一次任务中的成功,不代表组件值得永久保留。

系统还需要决定:

  • 这项能力是否跨会话有效?
  • 它适用于哪些任务?
  • 应该在什么条件下重新激活?
  • 它依赖哪些版本?
  • 它的权限范围是什么?
  • 它由谁生成?
  • 经过了哪些测试?
  • 是否允许自动升级?
  • 何时应该重新评估?
  • 何时应该被淘汰?

因此,一个可复用组件除了代码,还需要附带元数据:

Component
├── Implementation
├── Capability Description
├── Coeffects
├── Permissions
├── Validation Records
├── Version
├── Provenance
├── Metrics
├── Effect
└── Cleanup

真正的自生长闭环应当是:

发现缺口
  ↓
生成候选能力
  ↓
隔离激活
  ↓
观察与验证
  ↓
保留或回滚
  ↓
沉淀为可复用组件
  ↓
持续评估
  ↓
升级或淘汰

5. Pi 与 DSH 的核心差异

Pi 与 DSH 并不是简单的替代关系。

它们关注的是两个不同层次的问题。

维度 Pi DSH
核心目标 构建开放、可定制的 Agent Harness 构建支持动态变化的组件运行时
能力来源 Extensions、Skills、Prompts、Packages 运行时组件及其依赖关系
主要变化发起者 用户、开发者,也可以由 Agent 辅助 用户、系统或 Agent
组件组织方式 扩展资源与 Package 动态依赖图
激活方式 启动加载、临时加载或重新加载 根据 Context 与 Coeffects 激活
生命周期 以扩展加载和配置为中心 Effect 与 Cleanup 对称管理
依赖治理 主要由扩展代码和包依赖处理 将运行时能力依赖显式化
失败处理 依赖扩展实现、进程或会话边界 通过 Cleanup 支持组件级回滚
演化方向 从固定产品走向开放平台 从开放平台走向受控自生长
核心价值 让 Harness 容易被改变 让 Harness 可以安全地持续改变

可以把两者的关系概括为:

Pi:
稳定核心
+
开放扩展点
+
能力分发机制

DSH:
组件化 Context
+
显式 Coeffects
+
可逆 Effect
+
动态生命周期

Pi 解决了「能力能否加入」的问题。

DSH 更关注「能力如何进入、如何协作、如何退出,以及如何在运行时形成新的结构」。

6. 从可扩展到自生长,中间还差什么

可以把 Agent Harness 的演进分为三个阶段。

6.1 第一阶段:固定能力

模型
+
固定提示词
+
固定工具

系统能够执行任务,但能力边界在设计阶段已经确定。

如果缺少能力,只能由开发者修改产品代码并重新发布。

6.2 第二阶段:开放扩展

稳定核心
+
Extensions
+
Skills
+
Packages
+
开放 API

用户和开发者可以持续增加能力。

Agent 也可以帮助编写扩展,但扩展的发现、安装、验证和维护通常仍然依赖人工流程。

Pi 代表了这一阶段的重要方向:Harness 不再是一个封闭产品,而成为可以根据个人工作流持续定制的平台。

6.3 第三阶段:受控自生长

稳定内核
+
能力缺口识别
+
候选组件生成
+
动态组件运行时
+
显式依赖
+
可逆副作用
+
隔离验证
+
自动回滚
+
能力沉淀
+
持续淘汰

Agent 不再只是等待外部为它安装能力。

它可以根据任务识别不足,提出新的能力结构,在受控环境中试验,并根据结果决定保留、修改还是撤销。

这时,Agent 的角色从能力消费者变成了能力形成过程的参与者。

7. 自生长不等于无限增长

「自生长」并不是 Agent 不断为自己增加工具和代码。

无限增加不是生长,而是熵增。

一个只会添加能力、不会清理能力的系统,最终会出现:

  • 工具数量不断膨胀;
  • 相似能力重复实现;
  • 组件版本相互冲突;
  • 权限范围持续扩大;
  • 上下文被大量描述占据;
  • 路由规则越来越难以理解;
  • 无效监听器和后台任务持续运行;
  • Agent 无法判断应该使用哪个工具;
  • 系统行为逐渐不可预测。

因此,自生长必须同时包含四种能力:

增加
+
选择
+
清理
+
遗忘

可以进一步表示为:

可持续生长
=
生成新能力
-
淘汰无效能力
+
重组已有能力

这也是 Cleanup 和生命周期管理的重要性所在。

真正成熟的 Agent 不仅要知道「我还需要什么」,还要知道:

  • 什么已经不需要了;
  • 什么正在产生负面影响;
  • 什么应该暂时停用;
  • 什么可以被更简单的组件替代;
  • 什么经验只适用于过去的环境。

遗忘不是生长的反面。

受控遗忘本身就是生长的一部分。

9. Pi 与 DSH 不是替代,而是演进

Pi 的价值在于证明了一件事:

Agent Harness 不需要是一个功能固定的封闭产品。

通过开放 Extension、Skill、Prompt 和 Package,Harness 可以被用户重新塑造,也可以让 Agent 辅助编写自身需要的扩展。

这已经比传统 Agent 产品向前迈出了一大步。

DSH 所代表的方向,则是在此基础上进一步抽象:

如果 Agent 可以创建新能力,那么这些新能力应该如何成为运行时的一等公民?

答案不能只是把更多代码放进扩展目录。

系统还需要:

  • 明确组件所处的 Context;
  • 显式声明组件依赖的 Coeffects;
  • 管理组件产生的 Effect;
  • 在组件退出时执行 Cleanup;
  • 根据环境变化重新计算能力拓扑;
  • 对候选能力进行验证;
  • 将有效能力沉淀下来;
  • 将失效能力安全淘汰。

因此,二者可以形成一种递进关系:

Pi:
让 Harness 可以被修改

        ↓

DSH:
让修改具有显式生命周期

        ↓

自生长 Agent:
让 Agent 可以发起、验证并沉淀修改

从这个角度看,DSH 不是对 Pi 的否定。

它是在 Pi 所代表的开放扩展思路上,将“变化”进一步提升为运行时的核心抽象。

10. Harness 正在成为 Agent 的生长环境

如果说 Pi 的核心贡献,是把 Agent 从一个封闭产品变成一个可编程、可扩展的平台,那么 DSH 所探索的下一步,就是让 Agent 逐渐成为自身能力变化的参与者。

它不只是使用已有工具,还可以:

  • 发现能力缺口;
  • 创建候选组件;
  • 调整能力组合;
  • 在受控环境中试验;
  • 根据结果保留或回滚;
  • 将成功经验沉淀为长期能力。

但自生长不等于无限增加。

一个只会安装新工具、写入新记忆、叠加新策略的 Agent,最终只会被自己积累的复杂度拖垮。

真正可持续的生长,必须同时包含:

发现
→ 生成
→ 激活
→ 验证
→ 选择
→ 清理
→ 沉淀
→ 淘汰

Pi 回答了:

如何让 Agent Harness 可以被持续扩展?

DSH 则进一步追问:

如何让 Agent 安全地参与自身能力的形成?

从 Pi 到 DSH,变化的不只是扩展机制,而是 Harness 的角色。

它正在从承载工具与插件的工程框架,转变为支持能力试验、筛选、重组和沉淀的运行环境。

未来的 Agent Harness 可能不再只是模型与工具之间的连接层。

它还将成为 Agent 管理自身变化的基础设施:

  • 允许新能力出现;
  • 阻止错误变化扩散;
  • 保存经过验证的经验;
  • 清除已经失效的结构;
  • 在保持稳定内核的同时持续重组外围能力。

只有当 Agent 既能增加能力,也能验证、回滚、清理和遗忘时,它才真正具备从「可扩展的软件」走向「可持续生长的系统」的可能。

以上。