DeepSeek Harness 源码解析系列
第 8 讲:Agent 接口与注册表
基于 DeepSeek Harness 源码 · 2026-08-23
一、本节概述
前几讲我们了解了 Harness 的整体架构、事件系统、Turn flow 和依赖注入。本节聚焦 Agent 接口 与 AgentRegistry 注册表 —— 这是整个 Agent 运行时的心脏。每个插件(UI、Hooks、编排器)都通过这个接口编程,而注册表负责管理所有活跃 Agent 的生命周期。
源码位置:packages/core/agent/,核心文件 index.ts(706 行)定义了 AgentRegistry 服务类,runtime-types.ts(292 行)定义了 Agent 公开接口和全部 agent/* 事件词汇表。
二、Agent 接口:零循环依赖的抽象
Agent 接口是整个框架最关键的设计之一。它定义了 Agent 的"外观",但不包含任何具体的循环实现 —— 这意味着 循环是可替换的。所有插件只依赖这个接口,不依赖具体的 dsh-agent-loop 包。
📄 packages/core/agent/src/runtime-types.ts(第 64-144 行)
/** Public live-agent handle. */
export interface Agent {
/** The single identity shared with {@link session}. */
readonly id: SessionId
// ↑ Agent 的唯一标识,与 Session 共用同一个 ID,保证 Agent 和 Session 一一对应
/** The provider route and model this agent's requests use. */
readonly options: AgentOptions
// ↑ 包含 provider(供应商路由)、model(模型 ID)、maxTokens(最大输出 token 数)
/** The live session this agent drives; its log is the durable source of truth. */
readonly session: Session
// ↑ Agent 驱动的 Session 对象,Session 的事件日志是唯一的持久化事实来源
/** The agent-owned projection of durable pending work. */
readonly inbox: Inbox
// ↑ 邮箱系统:投影持久化的待处理消息,分为 next-turn 和 next-step 两个队列
/** The current lifecycle state, mirrored on every `agent/status` transition. */
readonly status: AgentStatus
// ↑ 生命周期状态:'idle'(空闲)或 'running'(运行中),每次状态切换都会触发事件
/** Agent-scoped context; its contributions are agent-local, unwind on disposal. */
readonly ctx: Context
// ↑ Agent 作用域上下文:在此注册的工具/提示/监听器只属于这个 Agent,销毁时自动清理
/** Clear queued and steering work — unless `keepInbox` — and abort the active turn. */
cancel(cause: AgentCancelCause, options?: CancelOptions): void
// ↑ 取消当前操作:默认清空邮箱,keepInbox=true 时保留待处理消息只中止当前 turn
/** Resolve after the current whole-agent activity reaches quiescence. */
whenIdle(): Promise
// ↑ 等待 Agent 完全空闲:包括当前驱动器和所有维护任务都完成
/** Run one non-turn maintenance task from the true idle phase. */
runMaintenance (task: (signal: AbortSignal) => Promise ): Promise
// ↑ 在空闲阶段运行维护任务:不打开 turn,状态保持 idle,适合后台清理等工作
/** Route identified input to an inbox boundary and optionally wake the driver. */
send(message: UserMessage, target: InboxTarget, wakeup: boolean): void
// ↑ 通用发送方法:将消息送入指定邮箱边界(next-turn 或 next-step),可选择唤醒驱动器
/** Queue an ordinary follow-up turn and wake the driver. */
followup(message: UserMessage): void
// ↑ 排队一个普通 follow-up turn:消息成为独立 turn 的唯一消息,并唤醒驱动器
/** Submit steering for the nearest step. */
steer(message: UserMessage): void
// ↑ 提交驾驶指令:如果驱动器空闲则立即启动 turn,否则在下一个 step 边界消费
/** Queue model-facing context for the next pre-step without waking the driver. */
inject(message: UserMessage): void
// ↑ 注入模型上下文:不唤醒驱动器,消息在下一个 pre-step 边界被消费
}
这个接口设计非常精妙。注意 followup、steer、inject 三个方法的区别:
三种消息类型的区别
🔹 followup() — 新 turn:消息进入 next-turn 队列,唤醒驱动器,适合用户的新请求
🔹 steer() — 驾驶指令:消息进入 next-step 队列,唤醒驱动器,适合在运行中改变方向
🔹 inject() — 静默注入:消息进入 next-step 队列,不唤醒驱动器,适合补充上下文
三、AgentRegistry:活跃 Agent 管理中心
AgentRegistry 是注册在 ctx.agents 上的服务,负责跟踪所有活跃的 Agent 实例。它继承自 Cordis 的 Service 基类,内部使用一个 Map 存储 Agent 条目。
📄 packages/core/agent/src/index.ts(第 256-298 行)
export class AgentRegistry extends Service {
// 继承 Cordis Service,获得生命周期管理和依赖注入能力
private store = new Map ()
// ↑ 核心存储:用 SessionId 做键,存储每个 Agent 的完整生命周期状态
private factory: FactorySlot | undefined
// ↑ 工厂槽位:延迟初始化,由 agent-loop 插件注册具体创建逻辑
private readonly initiators = new AsyncLocalStorage ()
// ↑ 异步本地存储:存储"发起者 Agent",实现进程内异步调用链的归属追踪
private readonly initiatorRuns = new AsyncLocalStorage ()
// ↑ 发起者运行追踪:记录嵌套的发起者边界链,用于正确清理
private initiatorState: 'active' | 'closing' | 'disposed' = 'active'
// ↑ 发起者状态机:三态控制新边界的接受/拒绝
private activeInitiatorRuns = 0
// ↑ 活跃发起者运行计数:用于等待所有异步边界完成
private initiatorDrain: PromiseWithResolvers | undefined
// ↑ 排空信号:当计数归零时 resolve,通知清理逻辑可以继续
private initiatorDisposal: Promise | undefined
// ↑ 清理 Promise:幂等化,确保 dispose 只执行一次
constructor(ctx: Context) {
super(ctx, 'agents')
// ↑ 注册服务名为 'agents',ctx.agents 指向此实例
ctx.inject(['typert'], (typeCtx) => {
// ↑ 依赖 typert 服务(RPC 类型系统),注册 Agent 的类型查找规则
typeCtx.typert.lookups.register('agent', {
// ↑ 让 RPC 调用可以通过 agentId(SessionId)查找到 Agent 实例
parameter: 'agent',
wire: 'agentId',
hostTypeSymbol: '@deepseek-ai/dsh-agent#Agent',
wireTypeSymbol: '@deepseek-ai/dsh-session/types#SessionId',
resolve: sessionId => this.get(sessionId),
// ↑ 解析函数:通过 ID 从 store 查找对应的 Agent
})
})
ctx.accessor('agent', { get: () => undefined })
// ↑ 注册 ctx.agent 访问器,默认返回 undefined;每个 Agent.ctx 会用自己的属性覆盖
ctx.on('internal/status', (fiber) => {
// ↑ 监听 Fiber 状态变化:当承载 AgentRegistry 的 Fiber 开始卸载时关闭发起者
if (fiber.state === FiberState.UNLOADING && this.hasLifecycleAncestor(fiber)) {
this.closeInitiators()
// ↑ 阻止新的发起者边界,但允许已开始的异步操作完成
}
})
ctx.effect(function* (this: AgentRegistry) {
// ↑ 注册可逆效果:Fiber 卸载时按顺序执行清理
yield () => this.disposeInitiators()
yield () => { this.closeInitiators() }
}.bind(this), 'agents.initiatorLifecycle()')
}
构造函数做了三件事:
AgentRegistry 构造函数的三项初始化
🔹 Typert 注册 — 让 RPC 网关能通过 agentId 查找 Agent,实现远程调用
🔹 ctx.agent 访问器 — 默认 undefined,每个 Agent 的 ctx 会用自己的属性覆盖(own property 优先于 proxy)
🔹 可逆效果注册 — Fiber 卸载时自动关闭发起者边界,确保资源正确释放
四、Agent 条目与注册流程
每个 Agent 在注册表中对应一个 AgentEntry,包含完整生命周期状态。注册分为两步:enter()(插入但不广播)和 announce()(广播创建事件)。
📄 packages/core/agent/src/index.ts(第 221-231 行)
/** All mutable lifecycle state for one exact registry entry. */
interface AgentEntry {
readonly id: SessionId
// ↑ Agent 的唯一标识,与 Session 共用
readonly agent: Agent
// ↑ Agent 实例本身
/** Runtime creator-agent ownership; independent of durable session lineage. */
readonly owner: Agent | undefined
// ↑ 运行时创建者:哪个 Agent 创建了这个 Agent(父子关系),与持久化的 Session 谱系独立
readonly carrier: Scoped
// ↑ 作用域载体:用于事件分发时的作用域过滤,确保只有相关监听器收到通知
announced: boolean
// ↑ 是否已广播创建事件:防止重复广播
announcing: boolean
// ↑ 是否正在广播中:防止递归创建第二个生命周期边
detachRequested: boolean
// ↑ 是否在广播期间请求了分离:同步监听器要求销毁时延迟到广播完成后执行
}
📄 packages/core/agent/src/index.ts(第 474-508 行)enter() 方法
enter(agent: Agent, owner: Agent | undefined): () => void {
const id = agent.id
// ↑ 获取 Agent ID,用于后续查找和存储
if (id !== agent.session.id) {
// ↑ 一致性校验:Agent ID 必须与 Session ID 完全一致
throw new Error(`agent id "${id}" does not match session id "${agent.session.id}"`)
}
const carrier = scopeTarget(agent, agent)
// ↑ 创建作用域载体:agent 既是作用域键也是主题,事件分发时据此过滤
if (this.store.has(id)) throw new Error(`agent "${id}" is already registered`)
// ↑ 碰撞检测:同一个 ID 不能重复注册,并发创建时只有一个能成功
const entry: AgentEntry = {
id, agent, owner, carrier,
announced: false, announcing: false, detachRequested: false,
}
// ↑ 构建条目,所有状态标志初始化为 false
this.store.set(id, entry)
// ↑ 存入 Map,此时 Agent 已注册但尚未广播
let entered = true
// ↑ 单标记变量:确保 detach 是幂等的(只执行一次)
const detach = (): void => {
// ↑ 返回分离闭包:拥有者调用此闭包来销毁这个 Agent
if (!entered) return
// ↑ 幂等检查:已经分离过则直接返回
entered = false
if (entry.announcing) {
// ↑ 如果正在广播创建事件,不能立即分离(会破坏同步监听器的执行)
entry.detachRequested = true
// ↑ 标记为"请求分离",等广播完成后再执行
return
}
this.detachEntered(entry)
// ↑ 直接执行分离:从 store 移除,如果已广播则发出 disposed 事件
}
return detach
}
这段代码的设计非常严谨。关键设计点:
注册流程的防御性设计
🔹 ID 一致性校验 — Agent ID 必须等于 Session ID,保证一一对应关系
🔹 碰撞检测 — 同一 ID 不能重复注册,并发场景下只有一个能成功进入
🔹 延迟分离 — 广播期间收到分离请求时,延迟到所有同步监听器执行完毕后再分离
🔹 幂等分离 — detach 闭包只能生效一次,防止重复销毁
五、广播与分离机制
announce() 方法负责广播 agent/created 事件,detachEntered() 负责安全地移除 Agent 并广播 agent/disposed。
📄 packages/core/agent/src/index.ts(第 549-576 行)announce() 方法
announce(agent: Agent): void {
const entry = this.store.get(agent.id)
// ↑ 从 store 查找对应的条目
if (entry === undefined || entry.agent !== agent) {
// ↑ 双重校验:条目存在且存储的 agent 引用与传入的一致
throw new Error(`agent "${agent.id}" is not live in this registry`)
}
if (entry.announced || entry.announcing) {
// ↑ 防止重复广播:已经广播过或正在广播都拒绝
throw new Error(`agent "${entry.id}" was already announced`)
}
entry.announcing = true
// ↑ 先标记"正在广播",防止监听器递归创建第二个生命周期边
entry.announced = true
// ↑ 再标记"已广播",即使监听器抛出异常也不会回滚这个标志
const args: unknown[] = [entry.carrier, 'agent/created', { agent: entry.agent }]
// ↑ 构建事件参数:载体 + 事件名 + 载荷
try {
for (const callback of this.ctx.events.dispatch('emit', args)) {
// ↑ 遍历所有 emit 模式的监听器回调
const returned: unknown = callback(...args)
// ↑ 同步调用监听器
void Promise.resolve(returned).catch((error: unknown) => {
// ↑ 如果监听器返回 Promise 且被拒绝,捕获并记录警告而不阻塞其他监听器
this.ctx.logger.warn(`agent "${entry.id}": agent/created listener rejected: ${String(error)}`)
})
}
} finally {
entry.announcing = false
// ↑ 广播完成,清除 announcing 标记
if (entry.detachRequested) this.detachEntered(entry)
// ↑ 如果广播期间有分离请求,现在安全执行
}
}
📄 packages/core/agent/src/index.ts(第 512-540 行)detachEntered() 和 emitDisposed()
private detachEntered(entry: AgentEntry): void {
entry.detachRequested = false
// ↑ 清除分离请求标记
if (this.store.get(entry.id) !== entry) return
// ↑ 关键安全检查:store 中该 ID 对应的条目必须还是同一个对象
// ↑ 如果已经被替换(比如并发创建的新 Agent),则不执行删除
this.store.delete(entry.id)
// ↑ 从 Map 中移除该条目
if (!entry.announced) return
// ↑ 如果从未广播过创建事件,也不广播销毁事件(生命周期不完整)
this.emitDisposed(entry)
// ↑ 广播 agent/disposed 事件
}
private emitDisposed(entry: AgentEntry): void {
const args: unknown[] = [entry.carrier, 'agent/disposed', { agent: entry.agent }]
// ↑ 构建销毁事件参数,使用条目创建时的稳定载体
for (const callback of this.ctx.events.dispatch('emit', args)) {
try {
const returned: unknown = callback(...args)
// ↑ 同步调用销毁监听器
void Promise.resolve(returned).catch((error: unknown) => {
// ↑ 异步拒绝也捕获并记录,不阻塞其他监听器
this.ctx.logger.warn(`agent "${entry.id}": agent/disposed listener rejected: ${String(error)}`)
})
} catch (error: unknown) {
// ↑ 同步异常也捕获,确保所有监听器都被调用
this.ctx.logger.warn(`agent "${entry.id}": agent/disposed listener threw: ${String(error)}`)
}
}
}
六、发起者作用域:异步调用链归属
Harness 使用 AsyncLocalStorage 实现了进程内的"发起者 Agent"追踪。当一个 Agent 创建子 Agent 时,子 Agent 的异步操作链携带子 Agent 的归属,而父 Agent 的操作链在子操作返回后自动恢复父归属。
📄 packages/core/agent/src/index.ts(第 640-670 行)runWithInitiator() 核心实现
private runWithInitiator (agent: Agent | undefined, operation: () => T): T {
if (this.initiatorState !== 'active') throw new Error(DISPOSED_INITIATOR_MESSAGE)
// ↑ 状态检查:如果发起者系统已关闭/销毁,拒绝新的边界
const run: InitiatorRun = {
active: true,
parent: this.initiatorRuns.getStore(),
// ↑ 记录父级运行,形成嵌套链:子边界知道它的父是谁
}
this.activeInitiatorRuns += 1
// ↑ 活跃运行计数 +1:用于追踪还有多少异步边界未完成
let result: T
try {
result = this.initiatorRuns.run(run, () => this.initiators.run(agent, operation))
// ↑ 关键:先用 initiatorRuns.run() 建立运行追踪边界,
// ↑ 再用 initiators.run() 建立发起者归属边界,然后执行操作
// ↑ 两层 AsyncLocalStorage 嵌套:外层追踪运行生命周期,内层存储发起者 Agent
} catch (error: unknown) {
this.releaseInitiatorRun(run)
// ↑ 同步异常时立即释放运行计数
throw error
}
if (isPromise(result)) {
// ↑ 如果操作返回 Promise(异步操作),需要等 Promise 结算后再释放
void Promise.prototype.then.call(
result,
() => { this.releaseInitiatorRun(run) },
// ↑ Promise resolve 时释放
() => { this.releaseInitiatorRun(run) },
// ↑ Promise reject 时也释放
)
} else {
this.releaseInitiatorRun(run)
// ↑ 同步操作直接释放
}
return result
}
七、工厂 API:Agent 创建与恢复
Agent 的实际创建由 AgentFactory 接口抽象,具体实现在 dsh-agent-loop 包中。注册表只暴露 create() 和 resume() 方法,将调用委托给已注册的工厂。
📄 packages/core/agent/src/index.ts(第 405-430 行)
async create(options: CreateAgentOptions): Promise {
const ownerCtx = this.ctx
// ↑ 当前上下文作为拥有者上下文
const { target } = this.requireFactory()
// ↑ 获取已注册的工厂,未注册则抛出异常
const receiver = getTraceable(ownerCtx, target)
// ↑ 重新追踪:确保工厂调用的效果绑定到调用者的上下文,而不是注册时的上下文
return Reflect.apply(target.createAgent, receiver, [ownerCtx, options])
// ↑ 通过 Reflect.apply 调用工厂的 createAgent,receiver 作为 this,保证 Cordis 追踪正确
}
async resume(options: ResumeAgentOptions): Promise {
const ownerCtx = this.ctx
const { target } = this.requireFactory()
const receiver = getTraceable(ownerCtx, target)
// ↑ 同样的追踪逻辑:resume 也需要正确的上下文绑定
return Reflect.apply(target.resume, receiver, [ownerCtx, options])
}
📄 packages/core/agent/src/index.ts(第 80-133 行)CreateAgentOptions 结构
export interface CreateAgentOptions {
readonly sessionId: SessionId
// ↑ 会话 ID:Agent 和 Session 共享的唯一标识
readonly meta?: {
readonly cwd?: string
// ↑ 工作目录:Agent 执行命令时的根目录
readonly parentSession?: SessionId
// ↑ 父会话:fork 场景下的谱系追踪
readonly seedLength?: number
// ↑ 种子长度:会话历史中不可修改的前缀长度
readonly origin?: 'subagent'
// ↑ 来源标记:subagent 场景下标识子 Agent
readonly delegationDepth?: number
// ↑ 委托深度:限制递归委托的层数,防止无限嵌套
readonly agentPreset?: string
// ↑ Agent 预设:用于组合 Agent 配置的预设名称
}
readonly seed?: readonly SessionEvent[]
// ↑ 种子事件:fork 时携带的父会话历史前缀
readonly agentOptions?: AgentOptions
// ↑ Agent 选项:provider、model、maxTokens
readonly signal?: AbortSignal
// ↑ 创建期取消信号:只在创建过程中有效,创建完成后分离
readonly setup?: AgentSetup
// ↑ 设置回调:在 Agent 发布前执行的组合逻辑,可以注册工具/监听器等
}
八、事件分发器:作用域安全的事件系统
dispatch.ts 提供了融合分发器,将 Agent 主题与作用域载体绑定在一起,确保事件只分发给正确的监听器。
📄 packages/core/agent/src/dispatch.ts(第 107-149 行)
export function agentEvents(ctx: Context, agent: Agent,
carrier: Scoped = agentCarrier(agent)): AgentEventDispatch {
// ↑ 构建融合分发器:将 Agent 主题和作用域载体绑定
// ↑ carrier 有默认值,调用者也可以传入预构建的载体避免重复分配
const fused = (payload: PayloadRest ): PayloadOf =>
({ ...payload, agent } as PayloadOf )
// ↑ 融合函数:自动将 agent 注入载荷,调用者只需传递除 agent 外的字段
// ↑ agent 字段最后合并,确保即使调用者传了 agent 也会被覆盖(防止主题篡改)
return {
emit(name, payload) {
// ↑ 普通广播:火后即忘,不阻塞
const args: unknown[] = [carrier, name, fused(payload)]
const callbacks = ctx.events.dispatch('emit', args)
// ↑ 获取所有 emit 模式的监听器
for (const callback of callbacks) {
try {
const returned: unknown = callback(...args)
// ↑ 同步调用
void Promise.resolve(returned).catch((error: unknown) => {
// ↑ 异步拒绝捕获并记录,不阻塞后续监听器
ctx.logger.warn(`agent event "${name}" listener rejected: ${String(error)}`)
})
} catch (error: unknown) {
// ↑ 同步异常捕获,确保所有监听器都被调用
ctx.logger.warn(`agent event "${name}" listener threw: ${String(error)}`)
}
}
},
async serial(name, payload) {
// ↑ 串行分发:按顺序 await 每个监听器,支持 bail-out
const serial = ctx.serial as (thisArg: Scoped , name: string, ...args: unknown[]) => Promise
return await serial(carrier, name, fused(payload))
},
waterfall(name, payload, ...rest) {
// ↑ 瀑布分发:洋葱模型,每个监听器可以调用 next() 委托给下一个
const waterfall = ctx.waterfall as (thisArg: Scoped , name: string, ...args: unknown[]) => never
return waterfall(carrier, name, fused(payload), ...rest)
},
}
}
九、Agent 事件词汇表
runtime-types.ts 通过 TypeScript 声明合并定义了完整的 agent/* 事件词汇表,涵盖生命周期、拦截点和错误通知。
| 事件名 | 模式 | 用途 |
|---|---|---|
agent/created | emit | Agent 完全配置并发布后触发 |
agent/disposed | emit | Agent 离开注册表 |
agent/status | emit | 状态切换 idle ⇄ running |
agent/session-start | emit | Session 生命周期开始,首个注入点 |
agent/pre-step | waterfall | 拦截/替换即将进入的 step 消息 |
agent/request | waterfall | 替换 LLM 请求配置(provider/model) |
agent/request-error | waterfall | 模型请求失败恢复(重试策略) |
agent/turn-stopping | serial | Turn 即将关闭前的扩展点 |
agent/error | emit | Step 或 Turn 错误通知 |
十、架构总结
本节剖析了 Agent 接口和注册表的源码设计。核心要点:
第 8 讲核心要点
🔹 Agent 接口零循环依赖 — 插件只依赖接口,不依赖具体循环实现,循环可替换
🔹 三步注册流程 — enter(插入)→ announce(广播)→ dispose(分离),每步都有防御性检查
🔹 工厂抽象 — Agent 创建委托给 AgentFactory,注册表只负责追踪和管理
🔹 发起者作用域 — AsyncLocalStorage 实现进程内异步调用链归属追踪
🔹 融合分发器 — Agent 主题与作用域载体绑定,事件自动按作用域过滤
🔹 邮箱系统 — followup/steer/inject 三种消息类型,分别进入不同队列
📚 系列导航
← 第 7 讲:Session 管理与事件日志
→ 第 9 讲:Agent Loop 核心循环
关注公众号「AI技术推荐官」获取更多源码解析内容
夜雨聆风