
字节笔记本
2026年10月6日 · 约 12 分钟读完
DeepSeek Harness 核心子系统解析
在 DeepSeek Harness 的文档体系里,核心(core)子系统指 packages/core 下的六个包:session、system-prompt、tools、agent、agent-loop 与 scope。它们是每个组合启动时都会装载的最小集合:事件溯源的会话日志、系统提示词组装、工具注册表、agent 类型,以及驱动这一切的具体循环。官方文档用专门一页描述这组包的约定,本文把它整理成一篇导读,重点讲清三件事:agent 如何被创建与拥有,Agent 句柄的投递、取消与拦截约定,以及全仓库反复出现的两个类型模式。

一次轮次流经六个包
一轮对话在同一条流水线上走完六个包。agent-loop 里的 driver 先认领一条排队的提示词,在会话日志(ctx.sessions)上开启轮次;system-prompt(ctx.systemPrompt)接着组装请求前缀,并从日志派生消息历史;请求经 LLM seam 流式取回模型响应;工具调用由工具注册表(ctx.tools)分发执行;每个模型可见的事实再追加回日志,供下一步派生。循环搬运的对话词汇,比如 Message、ContentBlock 与 StreamChunk,则由 packages/llm 统一声明。
六个包的分工可以概括成一张表:
| 包 | 负责 | 暴露在 ctx 上 |
|---|---|---|
| session | 仅追加的 SessionEvent 日志与内存 store,唯一真源 | ctx.sessions |
| system-prompt | 提示词段落与工具 schema 组装 | ctx.systemPrompt |
| tools | 带作用域的工具注册表与受保护执行流水线 | ctx.tools |
| agent | Agent 接口、实时注册表、发起者作用域与 agent 事件 | ctx.agents |
| agent-loop | 实现公开 Agent 约定的具体 driver | ctx.agentLoop |
| scope | 构建按 agent 作用域的注册原语 | 无服务入口 |
scope 是其中唯一的非服务包:一个零依赖小库,只提供 createScope、scopeOf 与 scopeTarget 三个原语。它被有意放在模块图中 session 与 system-prompt 的下游,让两者都能消费它而不形成环。agent-loop 则是公开 Agent 约定的唯一具体实现,作为 harness 的默认产品循环存在,每个 driver 都在 ctx.agents.withInitiator() 内运行。扩展插件只依赖 agent 包,即便需要发起 agent 也不直接依赖 agent-loop,因此整条循环保持可替换。把这条主干接成可运行 agent 的默认组合,仓库给出了 agent-spine-demo 示例。
创建与所有权
消费方通过 ctx.agents 创建 agent:create 在调用方提供的会话标识下构建全新会话与 agent,resume 先加载持久会话再恢复,也可以经循环的声明式配置条目创建。编程式创建返回一个 AgentHandle,除 agent 本体外还带一个 dispose 方法。文档强调这个 disposer 是一种能力:在所有消费方里,只有持有句柄的创建者能拆掉这个 agent。dispose 会停掉循环、等待其退出、注销 agent、把会话从 store 移除,最后拆除 agent 的作用域世界。
创建是事务性的。CreateAgentOptions 携带新 agent 发布前所需的一切:会话元数据(已校验的工作目录、fork 谱系、seed 边界、来源分类与委派深度)、fork 用的可选 seed 回放前缀、按 agent 的选项、仅创建期有效的取消 signal,以及 setup 回调。setup 在两个标识都尚未发布时组装 agent 的作用域世界,凡经 agentCtx 注册的内容都先于 agent/created 事件与第一次提示词组装存在;setup 拒绝、commit 抛出或所有者提前释放资源,都会回滚整个事务,两个标识均不发布,不会留下半成品会话。
具体怎么造 agent 由工厂决定。循环通过 ctx.agents.setFactory 注册自己的工厂,消费方只面对 ctx.agents 这一个门面,无需依赖具体循环包;经配置创建的 agent 归循环自己的 fiber 所有,从不需要句柄。
Agent 句柄:投递、取消与状态
Agent 接口是每个插件(UI、钩子、编排器)面向编程的表面,暴露 id、options、session、inbox、status 与 agent 本地的 ctx。具体实现是 agent-loop 包的内部细节,循环之外没有任何组件依赖它。

对外统一的投递入口是 send,它直接暴露目标收件箱与是否唤醒两个维度;followup、steer 与 inject 是三个固定预设的别名。followup 排一条普通后续轮次并唤醒 driver;steer 提交中途引导,空闲的 driver 由此开轮,运行中的 driver 在下一个步骤边界消费;inject 把模型可见的上下文放进下一个 pre-step,不唤醒 driver,可能错过已经领取了批次的那一步。
收件箱是 agent 以持久投影形式拥有的两条有序待处理列表:next-turn 与 next-step。每个待处理项就是一条 UserMessage,由唯一的 MessageId 标识。append、prepend、replace、remove、clear、splice 与 claim 都会记录规范化的持久变更事件,并拒绝重复的待处理 id。claim 通过纯删除的 splice 取走拟进入步骤的批次,本身不发 discarded 通知,由循环另行逐条发出 claimed;整体队列的消费方靠持久 splice 重建两条列表,跟踪单条消息的消费方则用 inserted、claimed 与 discarded 三个精确通知。
取消同样有明确约定。cancel 会清掉排队与引导中的消息并中止活跃轮次,取消原因是 TypeScript 强约束的四种之一:user、parent、hook(带原因说明)与 disposed。keepInbox 选项可以保留未开始的待处理工作,只中止当前轮次。持久 turn/end 只保留粗粒度的 aborted 结果;要记录是谁请求了取消,应使用单独的持久事件,而不是让终态结果承担额外含义。
状态机刻意做得很小:AgentStatus 只有 idle 与 running 两个值,每次迁移都发出 agent/status。running 描述整个 driver 的排空区间,可能跨越连续的排队轮次,它不能证明某个轮次仍然打开。dispose 会把 agent 从注册表移除并发出 agent/disposed,但它不是一个终态 status 值。whenIdle 观察的是整个 agent,只有当调用方明确拥有从回执到空闲的这段区间时,才能把它称为一次 run;runMaintenance 则允许从真正的 idle 阶段运行一个非轮次维护任务,期间公开状态保持 idle。
四个拦截点
核心为插件预留了一组拦截点,各有明确的时机与权限。
agent/pre-step 是请求推导前唯一的串行监听器链。它的载荷携带独占的已领取批次、拟进入步骤的轮次与步骤坐标,以及当前轮次的取消 signal;监听器返回 reject 就不打开步骤,返回 enter 则给出进入步骤的完整消息批次,最终决策省略的已领取消息保持已删除,领取后才插入的输入留待后续处理。
agent/request 是替换冻结调用配置的 waterfall:await next 拿到机器本要使用的配置,返回替换值即可切换提供方或模型。但这条 waterfall 改不了消息,模型可见的内容必须走有日志的通道。
agent/request-error 在失败的模型步骤关闭之后、其轮次关闭之前运行,监听器仍可在失败轮次的 signal 存活期间修复持久状态或等待策略工作。认领恢复的监听器返回 retry 且不调用 next,默认的 undefined 让失败保持终态。
agent/turn-stopping 在轮次没有工具调用也没有新引导时运行,先于最后一次引导排空。监听器若反对关轮就调用 steer,机器重读收件箱:新引导会再跑一步,没有则关轮。文档强调这里由数据决定,监听器顺序无法改变结果;反向控制同样走数据,工具结果带上 concludesTurn 就在其步骤结束轮次,已提交的后续步骤工作不会被短路。
另外,agent/session-start 在第一次轮次前发出一次,携带会话生命周期为何开始的来源分类(全新启动、恢复、清空或压缩),插件用它配合 inject 播种模型可见的上下文。
全仓通用的两个类型模式
两个模式在每个子系统反复出现,文档只在核心页记录一次。
第一个是 Map 到派生联合:几乎所有可扩展的和类型都定义成以判别标签为键的接口,联合类型用 keyof 派生。插件通过声明合并添加变体,不需要改动拥有该类型的包:
interface ThingMap {
a: { kind: 'a' }
b: { kind: 'b' }
}
type ThingKind = keyof ThingMap
type Thing = ThingMap[keyof ThingMap]
// 插件不改拥有类型的包,直接声明合并扩展:
declare module '@deepseek-ai/dsh-llm' {
interface ContentBlockMap {
custom: { kind: 'custom' }
}
}文档列出六个规范 map:dsh-llm 的 ContentBlockMap、MessageSourceMap 与 FinishReasonMap,dsh-session 的 TurnTriggerMap、TurnEndReasonMap 与 SessionEventMap。仓库约定对这些标签一律用 switch 而不是链式 if,让每个分支自动窄化类型,拼错的标签直接编译失败。
第二个是品牌化 ID:跨包传递的 id 结构上是字符串,在类型层面却不可互换,把 SessionId 传给需要 CallId 的位置会编译报错。Branded 原语放在独立的纯类型包里,没有运行时代码,任何包都能品牌化自己的 id 而不引入无关依赖;比较、日志与 JSON 行为与普通字符串完全相同。两个核心 id 是 CallId(关联工具调用及其结果)与 SessionId(活跃 agent 与持久会话共享的标识)。
小结
核心子系统把 harness 的骨架压进六个包:日志是唯一真源,历史从日志派生;agent 的创建是带回滚的事务,句柄是能力;投递、取消与拦截全部落在类型化的显式约定上。想在 DeepSeek Harness 上写插件,这一页值得先读;顺手记住上面两个类型模式,后面每个子系统的文档里还会反复遇到它们。



