乐于分享
好东西不私藏

DeepSeek Harness 源码-Agent 接口与注册表

DeepSeek Harness 源码-Agent 接口与注册表

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 边界被消费
}

这个接口设计非常精妙。注意 followupsteerinject 三个方法的区别:

三种消息类型的区别

🔹 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/createdemitAgent 完全配置并发布后触发
agent/disposedemitAgent 离开注册表
agent/statusemit状态切换 idle ⇄ running
agent/session-startemitSession 生命周期开始,首个注入点
agent/pre-stepwaterfall拦截/替换即将进入的 step 消息
agent/requestwaterfall替换 LLM 请求配置(provider/model)
agent/request-errorwaterfall模型请求失败恢复(重试策略)
agent/turn-stoppingserialTurn 即将关闭前的扩展点
agent/erroremitStep 或 Turn 错误通知

十、架构总结

本节剖析了 Agent 接口和注册表的源码设计。核心要点:

第 8 讲核心要点

🔹 Agent 接口零循环依赖 — 插件只依赖接口,不依赖具体循环实现,循环可替换

🔹 三步注册流程 — enter(插入)→ announce(广播)→ dispose(分离),每步都有防御性检查

🔹 工厂抽象 — Agent 创建委托给 AgentFactory,注册表只负责追踪和管理

🔹 发起者作用域 — AsyncLocalStorage 实现进程内异步调用链归属追踪

🔹 融合分发器 — Agent 主题与作用域载体绑定,事件自动按作用域过滤

🔹 邮箱系统 — followup/steer/inject 三种消息类型,分别进入不同队列

📚 系列导航

← 第 7 讲:Session 管理与事件日志

→ 第 9 讲:Agent Loop 核心循环

关注公众号「AI技术推荐官」获取更多源码解析内容