- 人工智能
- AI Agent
- Agent 框架
- DeepSeek
【免费下载链接】deepseek-harness
DeepSeek Harness: Everything is a Plugin.
本篇文章围绕 DeepSeek Harness(deepseek-harness仓库)在packages/subagent目录下的子代理能力缝(capability seam)展开,核心讲解此前独立的ctx.subagentControl编排服务与ctx.subagents提供者契约合并为单一公共服务SubagentRuntime的架构决策、公开 API 契约、错误体系、内部续接管理器实现,以及面向模型的send_message/interrupt_agent适配器与backgroundMode配置语义。读完本文,你将理解为什么子代理系统只需要一个公共服务键,如何在部署中配置"一次性"与"可续接"两种后台运行策略,以及续接子代理的持久化、授权、取消与销毁语义在源码中的实际落点。
一、背景:为什么把控制服务并回子代理服务
在合并之前,DeepSeek Harness 将"可续接子代理(continuable child)"的编排逻辑放在独立的ctx.subagentControl服务中,它位于底层ctx.subagents提供者契约之上。这种拆分当时有三个动机:让提供者分发与 Jobs、持久化解耦;为模型适配器与人类适配器提供统一的编排契约;把"启停续接"从"底层提供者调用"中抽象出来。
但在实际使用中出现了结构性问题(详见 docs/subsystems/subagent.md 中的记录):
- 两个服务描述的是同一族能力:所有使用可续接子代理的调用方必须同时依赖两个服务,公共 API 多了一个键,暴露了调用方并不需要的架构区分;
- 提供者绑定型委托工具需要"猜策略":
tool-subagent这类工具只能从provider.resume推断续接策略,还要检查控制服务和send_message工具是否恰好被加载——同级插件的存在与否决定了执行语义; - 可选后续面耦合了启动:把"启动持久化工作"与"是否存在一个可选的后续投递工具"绑定,是不必要的耦合。
因此合并后的架构收敛为:SubagentRuntime是唯一的公共服务,@deepseek-ai/dsh-subagent-control独立包和ctx.subagentControl键被移除,续接实现退化为服务内部的私有管理器。
二、单一公共服务:SubagentRuntime的公开 API
合并后的服务在packages/subagent/subagent/src/index.ts中定义:
export class SubagentRuntime extends TypertRemoteService { // 普通一次性运行 async start(name: string, request: SubagentStartRequest): Promise<SubagentRun> // Task 支撑的可续接启动 async startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart> // 意图命名(intent-named)的后续消息投递 async followup(parent: Agent, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions): Promise<MessageId> // 中断一个存活的续接子代理的当前轮次 interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void // 子代理向其直接父级报告(delivery: 'quiet' | 'next-step') async reportFrom(child: Agent, content: ContentBlock[], options: SubagentReportOptions): Promise<MessageId> // 提供者注册/查询/枚举 registerProvider(provider: SubagentProvider): () => void getProvider(name: string): SubagentProvider | undefined list(): string[] }服务通过 Cordis 的declare module '@deepseek-ai/cordis'扩展了Context,挂载键为ctx.subagents。它还发布四个生命周期事件:subagent/provider-added、subagent/provider-removed、subagent/start与subagent/end(后者按委托父级做 scope 过滤分发)。
2.1start:普通的一次性委托
start(name, request)是经典的前台委托入口:按名字解析提供者,做能力校验与深度上限校验,生成one-shot模式的描述符(descriptor),然后调用provider.start(resolved)。提供者的 promise 兑现即发布与所有权移交边界——发布前失败不会有 run 可供调用方清理,也不会发出生命周期事件(见index.ts中start的实现与注释)。
2.2startContinuable:分配持久化子 id 并同步返回
startContinuable与start的区别在于所有权与时机契约不同:
- 它在
ChildLock临界区内预留持久化 child id、创建 Task、同步返回{ childId, messageId },而子代理的实际启动在 Task 内部继续推进; - 只有初始提示被子代理 inbox 接受才算成功;此前的任何失败都不带任何 id 拒绝,并完整回滚子代理;
- 普通
start则等待提供者发布,并移交一个持有者所有的 run。
折叠到单个start(用标志位或返回联合类型)会拓宽底层契约,因此保留了显式的startContinuable入口——这是文档中明确记录的取舍。
2.3followup:按 FIFO 顺序投递下一条消息
followup(parent, childId, content, options)是续接子代理的后续消息投递口:
- 常驻(resident)子代理直接由
Agent的 inbox 接收,唤醒处于waiting的 Activation; - 不在常驻状态的子代理则从持久化 Session **冷恢复(cold resume)**为一个新的 Activation;
Agentinbox 是唯一队列,因此每条被接受的消息只有一种可观察顺序;- 调用方取消(
AbortSignal)只拥有"inbox 接受之前"这段窗口,接受之后续接操作不再可取消。
三、统一的错误体系:SubagentError
合并后,服务与提供者共享一套错误类型,被移除的subagentControl不再保留独立的错误类。SubagentError继承自@deepseek-ai/dsh-llm的HarnessError,定义在 error.ts:
export class SubagentError extends HarnessError { constructor(message: string, code: string, options?: ErrorOptions) { super(message, code, options) this.name = 'SubagentError' } }从源码(index.ts、continuation.ts)可以归纳出稳定的错误码分类:
| 类别 | 错误码(code) | 触发场景 |
|---|---|---|
| 提供者查找 | NO_PROVIDER | 未注册该名字的提供者 |
| 提供者注册 | DUPLICATE_PROVIDER | 同名提供者重复注册 |
| 能力校验 | UNSUPPORTED_CAPABILITY | 请求了提供者不具备的agentOptions/outputSchema/depthLimit/toolFilter/persona能力,或续接模式要求prepareContinuable而提供者没有 |
| 续接可用性 | CONTINUATION_UNAVAILABLE | 未加载 agents 服务或 session-query 服务 |
| 持久化 | PERSISTENCE_UNAVAILABLE | 续接操作需要 session 持久化后端而未加载 |
| 授权 | UNAUTHORIZED | 调用方不是目标的精确存活直系父级/祖先、自中断、父级身份过期 |
| 取消/关闭 | CANCELLED、DRAINING、ACTIVATION_CLOSING | 列表读取被取消、管理器或父级树正在排水、Activation 正在销毁 |
| 续接路由 | NOT_RESUMABLE、DUPLICATE_CHILD | 目标无可用续接状态 / 持久化中已存在该 child id |
| 投递 | PARENT_UNAVAILABLE | 直接父级不在存活状态,报告或通知无法投递 |
| 销毁 | ACTIVATION_TEARDOWN_FAILED | Activation 销毁期间某一边界失败(聚合上报) |
错误码使调用方(如浏览器 RPC 层与工具层)可以精确区分"提供者不存在""能力缺失""未授权"与"持久化缺失",而不是把一切归结为内部错误。例如远程接口prompt会把UNAUTHORIZED翻译为subagent-unauthorized,把NOT_RESUMABLE翻译为subagent-not-resumable。
四、续接实现的内部架构:管理器而非注册表扩展
文档明确决策:续接实现保留为内部管理器(SubagentContinuationManager,见 continuation.ts),而不是扩展提供者注册表的核心状态。
4.1 依赖注入与生命周期
SubagentRuntime在构造函数中通过ctx.inject(['agents'], ...)创建管理器,因此注入的 Cordis 子 fiber 拥有 Task 完成监听器与 teardown 效果:
ctx.inject(['agents'], (childCtx: Context) => { const manager = new SubagentContinuationManager(childCtx, { prepareContinuable: (name, request) => this.prepareContinuable(name, request), observeActivation: (provider, childId, parent) => this.observeActivation(provider, childId, parent), }, this.setupRegistry) this.continuations = manager childCtx.effect(() => () => { /* 释放时清空槽位 */ }, 'subagents.continuationBinding()') })这意味着:
- 加载提供者注册表不需要 Jobs 或持久化——普通
start调用方可以完全脱离续接基础设施工作; - 管理器只在 Jobs 与 Agents 都可用时存在,每个续接操作在需要持久性的那一点才解析 session 持久化;
- 销毁该 fiber 会先取消并结算所有活跃续接,然后才释放它们的所有权关联(
drain()在释放结构作用域之前注册,保证逆序 unwind 时排水先于句柄释放)。
4.2 Activation:一次驻留期 = 一个存活子代理
续接子代理有一个持久化 Session 和至多一个进程内 Activation。关键语义(源码注释明确说明):
- Activation 不是请求、结果、取消或 Task 边界:它可以执行多个 FIFO 轮次,并在其派生的后代仍在运行时保持驻留;
Agentinbox 是唯一的轮次队列,管理器拥有驻留权,Agent循环拥有轮次排序与执行权;- 驻留状态由
stateOf()从 Agent 静默度与 owned-children 集合推导:running(有活跃许可/轮次/待唤醒消息)、waiting(静默但仍有未销毁子代理)、settled(静默且所有子代已销毁)。
注意Agent.status单独不足以判断驻留:在followup()接受消息与微任务真正准入之间存在idle窗口,因此管理器用accepted集合记录"已接受但尚未离队"的消息 id,防止过早判定为 settled。
4.3 冷恢复(cold resume)与授权
当对不存在 Activation 的子代理执行followup时,管理器走coldResume:通过 session-query 观察持久化 Session,先按持久化 header 的parentSession校验授权(只有持久化子代理的精确存活直系父级才能继续它),再折叠(fold)描述符,最后调用ctx.agents.resume()重建 Activation。冷恢复不会经过 subagent 提供者分发——持久化 Session 已包含初始前缀,描述符就是完整重建输入。
4.4 串行化、结算与销毁
ChildLock:每个持久化 child id 的操作(投递、结算、销毁)按提交顺序线性化,且单个临界区失败不会拒绝无关的后续调用方;- 结算通知:子代理结算后,管理器负责向持久化直接父级投递
subagent-settled通知(如 "Background subagent … finished and will do no further work unless you send it more.")。这份职责不能交给外部subagent/end监听器:该负载不携带父级信息,且此时子代理句柄已被销毁; - 销毁顺序:取消信号自顶向下传播(
cancel({ kind: 'parent' })),但句柄释放保持子优先(child-first),每个后代完成销毁后祖先才移除句柄;最终 session flush 是尽力而为,失败只记录日志; - 结算通知的送达策略:空闲父级收到一个普通轮次,忙碌父级被 steering 合并到下一个 step;父级自身已在 teardown 时不唤醒,只注入。
五、面向模型的适配器:@deepseek-ai/dsh-tool-subagent-control
独立服务包被移除后,可选的 tool-subagent-control/src/index.ts 包直接注入ctx.subagents,提供两个全局命名的工具,它们只是续接 API 的薄适配器,不做任何生命周期路由:
send_message:调用ctx.subagents.followup(parent, subagent_id, message, { source: { kind: 'coordinator', form: 'relay', senderSessionId: parent.id }, signal }),把消息排队为子代理的下一轮。返回messageId,不返回子代理的答复——失败即"消息未投递";interrupt_agent:调用ctx.subagents.interrupt(agent_id, { kind: 'ancestor', agent: caller }),只请求取消当前轮次。accepted: true只表示取消信号已受理,目标可能继续运行到观察到信号为止;已排队消息保持停放、子代理保持可用、对已完成目标是接受的无操作。
send_message是独立的适配器:加载与否既不启用也不禁用startContinuable。部署完全可以只通过 Task 工具启动并收集续接工作而不暴露send_message。
六、委托工具配置:backgroundMode是策略,provider.resume只是能力
@deepseek-ai/dsh-tool-subagent(tool-subagent/src/index.ts)的每个实例通过backgroundMode选择后台语义:
backgroundMode?: 'one-shot' | 'continuable' // 默认 'one-shot'one-shot(默认):调用默认前台执行,后台时持有一个普通 Task;continuable:调用默认后台执行,要求提供者具备prepareContinuable能力(方法存在即能力),返回持久化 child id。
该配置项是部署策略,而provider.resume只承担"已配置续接模式的能力检查"角色。因此在合并后的语义下:一个支持 resume 的提供者依然可以跑一次性后台工作。启动时若提供者缺少该能力,会以UNSUPPORTED_CAPABILITY在挂载阶段快速失败;而缺失 Jobs、Agents 或持久化,则仍推迟到第一个真正需要它们的操作才失败(见文档 Consequences 一节)。
工具的其他配置项包括provider(必填,选择ctx.subagents注册名)、toolName(默认subagent)、modelSelectionSettings、enableRunInBackground(默认true)、agentOptions、persona、toolFilter(allow/deny至少其一)与maxDepth(默认3,0禁止委托)。
七、备选方案与取舍(Alternatives considered)
文档记录了几条被否决的路径,理解它们有助于把握最终设计边界:
- 保留独立服务:依赖分离最强,但所有生产续接路径都要组合两个服务,多出的公共键暴露了调用方不需要的架构区分;内部管理器以不新增服务的方式保留了可选的 Task 与持久化依赖。
- 从
provider.resume推断续接模式:方法存在性正确描述冷恢复能力,但不能表达部署策略,会把所有可恢复提供者强制进续接后台语义,并把"兄弟插件缺失"变成运行时错误;显式工具配置把"选择"与"能力"分离。 - 注册续接访问/检查后续工具:注册表能把续接面是否存在告诉委托工具,但启动持久化工作不需要任何后续适配器——这会重新把 UI 组合编码进执行策略,以另一个名字重造兄弟依赖。
- 把普通与续接启动合并为一个方法:给
start加标志会返回"已发布的一次性 run"或"即时 Task 与 child 身份",削弱简单的所有权边界;保留startContinuable是更小的改动。
八、合并后的后果与迁移要点
- 服务拓扑少了一个公共键、少了一个包;原始提供者分发在无 Jobs、无持久化时依然可用;
- 续接模式在提供者挂载时快速失败(配置了续接而提供者无
resume);缺 Jobs/Agents/持久化则延迟到首个必需操作处失败; - 后续投递保持可选;
dsh-subagent包内部对 Jobs 与持久化感知,因此仍声明它们为可选 peer 依赖,即便普通start调用方并不需要; - 已有的续接竞态、授权、持久性、取消与"结算后销毁"(settle-then-dispose)语义保持不变,并由迁移后的
subagent测试套件(如 service.spec.ts、continuation.spec.ts、control.spec.ts)持续钉住。
对于在 DeepSeek Harness 上开发子代理能力的开发者,合并后的心智模型可以简化为一句:只面向ctx.subagents一个服务编程——start做一次性委托,startContinuable建立持久化续接子代理,followup/interrupt/reportFrom处理续接后的交互,send_message与interrupt_agent只是这些 API 的可选模型可见面,而续接生命周期本身由服务内部的 Task 与持久化感知管理器负责。
- 人工智能
- AI Agent
- Agent 框架
- DeepSeek
【免费下载链接】deepseek-harness
DeepSeek Harness: Everything is a Plugin.
相关推荐
DeepSeek Harness 可继续子代理会话:Activation 生命周期、单一 Inbox 续话与子优先销毁
DeepSeek Harness 可继续子代理会话:Activation 生命周期、单一 Inbox 续话与子优先销毁 本文聚焦 DeepSeek Harnes
人工智能AI AgentAgent 框架DeepSeekDeepSeek Harness 子代理继续执行操作的按意图 API 设计:SubagentRuntime 四大执行意图与单一持久性屏障
DeepSeek Harness 子代理继续执行操作的按意图 API 设计:SubagentRuntime 四大执行意图与单一持久性屏障 本篇技术指南以 Dee
人工智能AI AgentAgent 框架DeepSeekDeepSeek Harness 投递语义:Agent.send() 一消息一轮次的 FIFO 契约设计
DeepSeek Harness 投递语义:Agent.send 一消息一轮次的 FIFO 契约设计 本文讲解 DeepSeek Harness(一切皆插件的
人工智能AI AgentAgent 框架DeepSeek
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考