乐于分享
好东西不私藏

DeepSeek Harness 源码-Agent Loop 核心循环

DeepSeek Harness 源码-Agent Loop 核心循环

DeepSeek Harness 源码解析系列

第 9 讲:Agent Loop 核心循环

基于 DeepSeek Harness 源码 · 2026-08-24

一、本节概述

前几讲我们逐步拆解了 Harness 的整体架构、事件系统、Turn flow、依赖注入、Session 管理和 Agent 注册表。本节进入整个框架的核心引擎——ReactLoopAgent 驱动循环。这是整个 Harness 中唯一包含具体循环逻辑的包,其他所有行为都通过插件和事件扩展点注入。

源码位置:packages/core/agent-loop/,共 6 个核心文件:

agent-loop 包文件结构

🔹 index.ts(713 行)— AgentLoop 工厂服务,负责创建、恢复、销毁 Agent

🔹 agent.ts(496 行)— ReactLoopAgent 核心驱动,turn/step 循环

🔹 tool-calls.ts(289 行)— 工具调用调度器,串行/并行分组执行

🔹 runtime-context.ts(76 行)— 运行时上下文投影,动态上下文快照

🔹 constants.ts(6 行)— 默认并行工具调用数(10)

🔹 invariant.ts(63 行)— 运行时不变量检查,请求一致性校验

二、ReactLoopAgent:三阶段状态机

ReactLoopAgent 是整个循环的心脏。它维护一个内部 Phase 状态机,管理 Agent 从空闲到运行再到空闲的完整生命周期。先看它的三种状态定义:

📄 packages/core/agent-loop/src/agent.ts(第 38-46 行)

type Phase =
  | { kind: 'idle'; lastTurn: number }
  // ↑ 空闲状态:记录最后一个 turn 编号,用于下一个 turn 从 lastTurn+1 开始计数
  | {
    kind: 'maintenance'
    // ↑ 维护状态:允许后台任务(如压缩、清理)在不打开 turn 的情况下执行
    abort: AbortController
    // ↑ 维护任务也有独立的取消信号,可以中止正在进行的维护操作
    lastTurn: number
    // ↑ 维护期间 turn 计数不前进,保留 lastTurn 供恢复后使用
    wakeRequested: boolean
    // ↑ 维护期间如果有唤醒请求,标记为 true,维护结束后自动回放
  }
  | { kind: 'running'; abort: AbortController; turn: number; step: number; wakeRequested: boolean }
  // ↑ 运行状态:包含当前 turn 和 step 编号,独立的取消信号,以及挂起的唤醒标记

注意 maintenance 状态的设计意图。它让压缩(compaction)、清理等后台任务可以在 Agent 空闲时执行,但不打开新的 turn。如果维护期间收到用户消息,wakeRequested 标记确保维护结束后自动触发新的 turn。

三、kick() → turn() → step():三层循环

驱动循环的核心是 kick() 方法。它启动一个 while 循环,每轮调用 turn(),每个 turn 内部有多个 step()。这是典型的三层嵌套结构:

📄 packages/core/agent-loop/src/agent.ts(第 210-223 行)

private async kick(): Promise<void> {
  try {
    while (await this.turn()) {}
    // ↑ kick 是一个无限 while 循环,每次调用 turn()。turn() 返回 true 表示还有
    // 待处理消息(inbox.hasPending),返回 false 表示本轮对话结束,循环退出
  } catch (_error) {
    // ↑ 所有异常在这里被捕获——失败和取消在驱动边界被包含,不会向外传播
    // 错误已经在 throwError() 中通过 agent/error 事件报告了
  } finally {
    // ↑ 无论正常退出还是异常退出,都要恢复 idle 状态
    if (this.phase.kind === 'running') {
      const { turn, wakeRequested } = this.phase
      this.setPhase({ kind: 'idle', lastTurn: turn })
      // ↑ 将 running 状态切回 idle,保存最后一个 turn 编号
      if (wakeRequested && this.inbox.hasPending) this.wakeDriver()
      // ↑ 如果退出前有挂起的唤醒请求且邮箱仍有消息,重新启动驱动器
      // 这是"取消收敛唤醒锁存"机制:用户在中止后立刻发消息不会丢失
    }
  }
}

三层循环的调用关系:

三层循环结构

🔹 kick() — 最外层:while 循环调用 turn(),直到 inbox 为空

🔹 turn() — 中间层:打开一个 turn,内部 while 循环执行多个 step

🔹 step() — 最内层:组装 prompt,调用 LLM,执行工具调用

四、turn():Turn 边界与 Step 循环

turn() 方法管理一个完整的 turn 生命周期。它先打开 turn 边界,然后在一个内部循环中执行多个 step,直到 turn 结束条件满足:

📄 packages/core/agent-loop/src/agent.ts(第 246-330 行)

private async turn(): Promise<boolean> {
  // ↑ 返回 true 表示 kick 应该继续(还有消息要处理),false 表示 turn 结束
  if (this.phase.kind !== 'running') {
    this.throwError(new Error(`agent "${this.id}": turn without driver reservation`))
  }
  // ↑ 前置检查:turn 只能在 running 阶段调用,防止并发调用
  const phase = this.phase
  const { signal } = phase.abort
  signal.throwIfAborted()
  // ↑ 检查取消信号:如果已被中止,立即抛出异常退出
  const turn = phase.turn + 1
  // ↑ turn 编号从 1 开始递增,每个 turn 对应一次用户请求的完整处理
  try {
    this.session.append('turn/start', { turn })
    // ↑ 在 Session 日志中记录 turn 开始事件,这是持久化的生命周期边界
  } catch (error: unknown) {
    this.throwError(error)
  }
  phase.turn = turn
  let turnEnds: TurnEndReason | null = null
  // ↑ turnEnds 记录 turn 的结束原因:completed / max-tokens / error / aborted / blocked
  let target: InboxTarget = 'next-turn'
  // ↑ 第一个 step 从 next-turn 队列取消息,后续 step 从 next-step 取
  try {
    while (true) {
      signal.throwIfAborted()
      // ↑ 每个 step 前都检查取消信号,实现协作式取消
      const step = phase.step + 1
      const decision = await this.preStep(target, { turn, step })
      // ↑ preStep 是关键的决策点:从 inbox 取消息、组装 prompt、运行 pre-step 钩子
      if (decision.kind === 'reject') {
        turnEnds = { kind: 'blocked' }
        return false
      }
      // ↑ 如果 pre-step 钩子返回 reject,turn 以 blocked 结束(例如权限被拒绝)
      if (turnEnds && decision.messages.length === 0) break
      // ↑ 如果 turn 已经有结束标记且当前 step 没有消息,退出 step 循环
      if (phase.step === 0 && decision.messages.length === 0) {
        turnEnds = { kind: 'completed' }
        return false
      }
      // ↑ 第一个 step 就没有消息(如唤醒消息被移除),直接以 completed 结束
      signal.throwIfAborted()
      this.session.append('step/start', { turn, step })
      phase.step = step
      try {
        for (const message of decision.messages) {
          this.session.append('user/message', message, { surfaceOp: 'append' })
          // ↑ 将用户消息追加到 Session 日志,surfaceOp: 'append' 表示 UI 可见
        }
        const stepEnd = await this.step(decision.assembly)
        // ↑ 执行核心 step:调用 LLM,处理工具调用
        if (turnEnds === null || turnEnds.kind !== 'max-tokens') turnEnds = stepEnd
        // ↑ max-tokens 是"粘性"的:一旦某个 step 触达 token 上限,后续 step
        // 正常完成也不能降级 turn 的结束原因
      } finally {
        this.session.append('step/end', { turn, step })
        // ↑ 无论 step 成功还是失败,都记录 step 结束事件
      }
      signal.throwIfAborted()
      if (turnEnds && this.inbox.nextStep.length === 0) {
        await this.dispatch.serial('agent/turn-stopping', { turn, signal })
        // ↑ turn 即将结束时,串行触发 turn-stopping 钩子
        // 插件可以在此取消 turn(例如达到 turn 预算上限)
        signal.throwIfAborted()
      }
      if (turnEnds && this.inbox.nextStep.length === 0) break
      // ↑ turn 结束条件:step 返回结束原因 + next-step 队列为空
      target = 'next-step'
      // ↑ 后续 step 从 next-step 队列取消息(steer/inject 消息)
    }
  } catch (error: unknown) {
    if (signal.aborted) {
      turnEnds = { kind: 'aborted', reason: signal.reason as AgentCancelCause }
      throw error
    }
    turnEnds = {
      kind: 'error',
      error: error instanceof LlmError
        ? error.failure
        : { message: errorChain(error), code: 'UNKNOWN' },
    }
    this.throwError(error)
  } finally {
    this.session.append('turn/end', { turn, reason: turnEnds! })
    // ↑ 记录 turn 结束事件,包含结束原因
  }
  if (!this.inbox.hasPending) return false
  // ↑ 如果邮箱空了,turn 循环结束,kick() 退出
  phase.abort = new AbortController()
  // ↑ 新 turn 创建新的取消信号,旧信号的锁存失效
  phase.wakeRequested = false
  phase.step = 0
  return true
  // ↑ 还有消息要处理,kick() 继续下一个 turn
}

五、step():LLM 调用与工具执行

step() 是最核心的执行单元。它组装请求、调用 LLM、处理流式响应、执行工具调用。整个 step 在 while(true) 循环中运行,支持错误重试:

📄 packages/core/agent-loop/src/agent.ts(第 332-401 行)

private async step(assembly: PromptAssembly): Promise<StepEndReason | null> {
  // ↑ 返回 null 表示 step 未完成(有工具调用需要继续),
  // 返回 completed 或 max-tokens 表示 step 结束
  const { turn, step, abort: { signal } } = this.phase
  signal.throwIfAborted()
  const system = renderPrompt(assembly)
  // ↑ 从组装好的 prompt assembly 渲染 system prompt 字符串

  while (true) {
    // ↑ while(true) 支持重试:LLM 调用失败时可以在循环内重试
    const { request, preparedCall } = await this.buildRequest(
      turn, step, assembly.tools, system, this.session.deriveMessages(), signal,
    )
    // ↑ buildRequest 组装完整的 LLM 请求:配置、消息、system prompt、工具列表
    const assembler = new BlockAssembler()
    // ↑ BlockAssembler 负责将流式 chunk 组装成完整的内容块
    const chunkSeqs: number[] = []
    // ↑ 记录每个 chunk 的事件序列号,用于后续 assistant/message 的回溯
    const stream = preparedCall?.stream(request) ?? this.loopCtx.llm.stream(request)
    // ↑ 如果有预制的调用(preparedCall),用它来流式请求;否则走标准 LLM 流
    // preparedCall 锁定了 adapter 注册,防止 HMR 热替换导致 adapter 混用
    for await (const chunk of stream) {
      signal.throwIfAborted()
      // ↑ 每个 chunk 到达时都检查取消信号
      chunkSeqs.push(this.session.append('assistant/chunk', { turn, step, chunk }).seq)
      // ↑ 每个流式 chunk 都记录到 Session 日志中
      assembler.push(chunk)
      // ↑ 将 chunk 推入组装器,逐步构建完整响应
    }
    signal.throwIfAborted()
    const finish = assembler.finish
    if (finish.kind === 'error' || finish.kind === 'aborted') {
      // ↑ 如果 LLM 返回错误或中止,走错误恢复路径
      const action = await this.dispatch.waterfall(
        'agent/request-error', {
          turn, step, provider: request.provider, failure: finish.failure,
          retryPolicy: preparedCall?.retryPolicy, signal,
        },
        () => Promise.resolve(undefined),
      )
      // ↑ 触发 agent/request-error 瀑布流钩子,插件可以决定重试或放弃
      // 重试策略来自 preparedCall 的 adapter 注册,保证一致性
      signal.throwIfAborted()
      if (action?.kind !== 'retry') {
        throw new LlmError(finish.failure.message, finish.failure.code, finish.failure)
      }
      continue
      // ↑ 如果钩子返回 retry,while 循环重新开始,重新调用 LLM
    }

    const message = createAssistantMessage({
      content: assembler.blocks(),
      source: { provider: request.provider, model: request.model, ... },
    })
    // ↑ 用组装好的内容块创建 assistant 消息
    this.session.append('assistant/message', { turn, step, message, ... },
      { surfaceOp: 'append', sourceEventSeqs: chunkSeqs })
    // ↑ 将完整的 assistant 消息写入 Session 日志,关联所有 chunk 的序列号

    if (finish.kind === 'max-tokens') return { kind: 'max-tokens' }
    // ↑ 触达 token 上限,step 结束

    const toolCalls = message.content.filter(block => block.type === 'tool-call')
    if (toolCalls.length === 0) return { kind: 'completed' }
    // ↑ 没有工具调用,step 正常完成

    const { concluded } = await executeToolCalls(
      this.loopCtx, turn, step, toolCalls, signal,
      context => this.inbox.splice('next-step', this.inbox.nextStep.length, 0, [context]),
    )
    // ↑ 执行工具调用,工具产生的额外上下文注入 next-step 队列
    return concluded ? { kind: 'completed' } : null
    // ↑ concluded=true 表示某个工具标记了 turn 结束,否则返回 null 继续 step
  }
}

六、工具调用调度器:串行屏障与并行池

executeToolCalls 是工具调用的调度引擎。它根据每个工具的并发模式(串行 exclusive / 并行 parallel)动态分组,使用有界滚动池控制并行度:

📄 packages/core/agent-loop/src/tool-calls.ts(第 59-101 行)

export async function executeToolCalls(
  ctx: Context, turn: number, step: number,
  toolCalls: ToolCallBlock[], signal: AbortSignal,
  acceptContext: (context: UserMessage) => void,
): Promise<{ concluded: boolean }> {
  const agent = ctx.agents.requireInitiator()
  const { session } = agent
  // ↑ 获取当前 Agent 和它的 Session,工具执行需要 Agent 上下文

  const planned: PlannedCall[] = toolCalls.map(block => ({
    block,
    exec: { callId: block.id, name: block.name,
      arguments: parseArguments(block.arguments), agent, signal },
  }))
  // ↑ 将模型返回的工具调用块解析为 PlannedCall,包含调用参数和 Agent 引用

  let next = 0
  let concluded = false
  while (next < planned.length) {
    // ↑ 遍历所有计划中的工具调用
    const first = planned[next]!
    const mode = ctx.tools.executionMode(first.exec).kind
    // ↑ 查询第一个调用的执行模式:'exclusive'(串行)或 'parallel'(可并行)
    const group = mode === 'parallel' ? planned.slice(next) : [first]
    // ↑ 关键设计:如果是并行模式,剩余所有调用作为一组;
    // 如果是串行模式,只有第一个调用单独作为一组(形成屏障)
    const outcome = await runGroup(ctx, turn, step, group, mode, signal, acceptContext)
    next += outcome.consumed
    // ↑ 跳过已处理的调用
    concluded ||= outcome.concluded
    // ↑ 如果任何工具标记了 turn 结束,累积 concluded 标记
    if (outcome.aborted) {
      // ↑ 如果发生中止,为剩余未执行的调用生成合成错误结果
      for (const call of planned.slice(next))
        appendSkippedToolCall(session, turn, step, call.block)
      return { concluded }
    }
  }
  return { concluded }
}

并行池的核心在 runGroup 函数中。它使用 maxParallelToolCalls(默认 10)限制并发度,同时保证模型顺序:

📄 packages/core/agent-loop/src/tool-calls.ts(第 121-246 行,核心片段)

async function runGroup(...) {
  const { maxParallelToolCalls } = ctx.agentLoop.config
  // ↑ 从 AgentLoop 配置读取最大并行度,默认 10
  const slots: (Slot | undefined)[] = group.map(() => undefined)
  // ↑ 每个调用一个 slot,等待结果填入
  const callSeqs: number[] = group.map(() => -1)
  // ↑ 记录每个调用的事件序列号,结果需要引用调用的 seq
  let nextToStart = 0
  let committed = 0
  // ↑ committed 只沿模型顺序推进,保证结果按模型返回的顺序提交

  const commitReady = async (): Promise<void> => {
    while (committed < group.length) {
      const slot = slots[committed]
      if (slot === undefined) break
      // ↑ 只提交模型顺序上连续的已完成 slot
      const result = slot.needsPost
        ? await ctx.tools[TOOL_RUNTIME_SCHEDULER].finalize(slot.exec, slot.result)
        : ctx.tools[TOOL_RUNTIME_SCHEDULER].finish(slot.exec, slot.result)
      // ↑ 需要后处理的工具走 finalize,直接结果走 finish
      appendToolResult(session, turn, step, call!.block, result, callSeqs[committed]!)
      // ↑ 将结果追加到 Session 日志
      for (const context of result.additionalContexts ?? []) acceptContext(context)
      // ↑ 工具产生的额外上下文(如文件变更通知)注入下一步
      concluded ||= result.concludesTurn === true
      committed++
    }
  }

  const fillPool = async (): Promise<void> => {
    while (!aborted && nextToStart < group.length && inFlight.size < maxParallelToolCalls) {
      // ↑ 填充条件:未中止 + 还有调用 + 飞行中数量 < 并行上限
      const nextCall = group[nextToStart]!
      if (nextToStart > 0 && mode === 'parallel'
        && ctx.tools.executionMode(nextCall.exec).kind !== 'parallel') break
      // ↑ 重新分类检查:如果后续调用不是并行模式,停止填充形成新屏障
      await startCall(nextToStart)
      nextToStart++
      throwSchedulerFailure()
      await commitReady()
      // ↑ 启动一个调用后立即尝试提交已完成的,保持模型顺序
    }
  }

  try {
    await fillPool()
    while (inFlight.size > 0) {
      const settledIndex = await Promise.race(inFlight.values())
      // ↑ 等待任意一个飞行中的调用完成
      inFlight.delete(settledIndex)
      await commitReady()
      // ↑ 提交已完成的调用
      if (signal.aborted) aborted = true
      await fillPool()
      // ↑ 空出槽位后继续填充,实现滚动池
    }
  } catch (error: unknown) {
    schedulerFailure ??= { error }
    await Promise.allSettled(inFlight.values())
    throw schedulerFailure.error
  }
  // ↑ 调度器故障时等待所有已启动的调用完成,但不伪造结果
}

七、AgentLoop 工厂服务:创建与恢复

AgentLoop 是 Cordis 服务,负责 Agent 的创建、恢复和销毁。它实现了 AgentFactory 接口,管理工厂级所有权:

📄 packages/core/agent-loop/src/index.ts(第 296-382 行)

export class AgentLoop extends Service implements AgentFactory {
  static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt']
  // ↑ 注入 5 个核心服务:Agent 注册表、Session 管理、LLM、工具、系统提示

  constructor(ctx: Context, config: Config) {
    super(ctx, 'agentLoop')
    // ↑ 注册为 ctx.agentLoop,其他插件通过 ctx.agentLoop 访问

    this.config = {
      ...config,
      agents: applyLauncherIdentities(config.agents, ctx.get(CONFIGURED_AGENT_IDENTITIES_KEY)),
      // ↑ 应用启动器指定的身份覆盖配置中的 sessionId
      get maxParallelToolCalls() {
        return source().maxParallelToolCalls
      },
      // ↑ 使用 getter 而非固定值:用户修改设置后下一个工具组立即生效,
      // 不需要重启
    }

    validateConfiguredAgents(this.config.agents)
    // ↑ 验证配置:sessionId 和 resumeSessionId 互斥,无重复身份

    this.ownership = new FactoryOwnership(ctx.fiber)
    // ↑ 工厂级所有权管理器:跟踪活跃 Agent 和启动任务
    this.runtime = { ctx }
    // ↑ 保存工厂的依赖上下文,防止调用者影子覆盖

    ctx.effect(() => () => this.ownership.dispose(), 'agentLoop.transactions()')
    // ↑ 注册销毁效果:工厂卸载时清理所有 Agent
    ctx.effect(() => ctx.agents.setFactory(this), 'agentLoop.setFactory()')
    // ↑ 将自己注册为 Agent 工厂,插件通过 ctx.agents 创建 Agent

    ctx.systemPrompt.variable('provider', context => context.agent?.options.provider)
    ctx.systemPrompt.variable('model', context => context.agent?.options.model)
    ctx.systemPrompt.variable('cwd', context => context.agent?.session.header.cwd)
    // ↑ 注册三个 prompt 变量:provider、model、cwd,在渲染 system prompt 时动态填充

    for (const { id, sessionId, cwd, resumeSessionId, ...options } of this.config.agents) {
      // ↑ 遍历配置的 Agent 条目,逐个创建或恢复
      if (resumeSessionId === undefined || resumeSessionId === '') {
        const configuredId = sessionId ?? SessionId(`${id}-session-${randomUUID()}`)
        // ↑ 没有指定 sessionId 时生成随机 UUID,保证唯一性
        const persistence = sessionId === undefined ? undefined : ctx.get('sessionPersistence')
        if (persistence === undefined) {
          this.create(configuredId, options, meta)
        } else {
          this.restoreOrCreateConfigured(ctx, persistence, configuredId, options, meta)
          // ↑ 有持久化后端时先尝试恢复,失败再创建新的
        }
      } else {
        // ↑ resumeSessionId 路径:从持久化存储加载已有会话
        ctx.effect(() => {
          void this.resumeWith(ctx, childCtx.sessionPersistence, { resumeSessionId, agentOptions: options })
        }, `agentLoop.resume(${id})`)
      }
    }
  }
}

八、FactoryOwnership:安全的生命周期管理

FactoryOwnership 是工厂级的生命周期控制器。它确保在工厂卸载时所有 Agent 被正确清理:

📄 packages/core/agent-loop/src/index.ts(第 40-90 行)

class FactoryOwnership {
  private accepting = true
  // ↑ 接受新 Agent 的开关,dispose 时关闭
  private readonly teardown = new AbortController()
  // ↑ 工厂级取消信号,dispose 时触发
  private readonly inactive = Promise.withResolvers<void>()
  // ↑ 非活跃状态的 Promise,用于 waitWhileActive 提前退出
  private readonly liveAgents = new Set<() => Promise<void>>()
  // ↑ 跟踪所有活跃 Agent 的销毁函数
  private startupTasks = new Set<Promise<void>>()
  // ↑ 跟踪启动任务,防止工厂在启动完成前被销毁

  isActive(): boolean {
    return this.accepting && !INACTIVE_STATES.has(this.fiber.state)
    // ↑ 双重检查:工厂接受新请求 AND fiber 未处于卸载/销毁/失败状态
  }

  track(dispose: () => Promise<void>): () => void {
    this.liveAgents.add(dispose)
    return () => { this.liveAgents.delete(dispose) }
    // ↑ 注册 Agent 销毁函数,返回取消注册的清理函数
  }

  async dispose(): Promise<void> {
    this.accepting = false
    this.teardown.abort(new Error('agent loop is not active'))
    // ↑ 触发所有等待中的操作取消
    this.inactive.resolve()
    // ↑ 让 waitWhileActive 提前退出
    await Promise.all([
      ...[...this.liveAgents].map(dispose => dispose()),
      // ↑ 销毁所有活跃 Agent
      ...this.startupTasks,
      // ↑ 等待所有启动任务完成
    ])
  }
}

九、运行时不变量:请求一致性检查

invariant.ts 定义了可选的运行时检查。它拦截每个 LLM 请求,验证请求与 Session 日志的一致性:

📄 packages/core/agent-loop/src/invariant.ts(第 19-55 行)

const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
  ctx.on('llm/stream', (options: GenerateOptions, next) => {
    if (!isAgentLoopRequest(options)) return next()
    // ↑ 只检查由 agent-loop 构建的请求,跳过外部直接调用

    if (!Object.isFrozen(options)) fail('a loop-built request must be frozen')
    // ↑ 请求必须是冻结的,防止下游修改

    if (options.sessionId === undefined) fail('a loop-built request must carry a session id')
    const session = ctx.sessions.get(options.sessionId)
    if (!session) fail(`a loop-built request must carry a live session id`)
    // ↑ 请求必须关联一个活跃的 Session

    if (!events.some(event => event.type === 'step/start')) {
      return fail('a loop-built request with no step/start in its session log')
    }
    // ↑ Session 日志必须有 step/start 事件,证明请求来自合法的 step

    const expected = session.deriveMessages()
    if (JSON.stringify(options.messages) !== JSON.stringify(expected)) {
      fail(`llm request diverges from the dispatch-time durable derivation`)
    }
    // ↑ 核心检查:请求中的消息必须与从 Session 日志推导出的消息完全一致
    // 这保证了"日志是唯一事实来源"的架构原则

    const headerMatches = options.model === header.config.model
      && options.system === header.system
      && options.temperature === header.config.temperature
      // ↑ 请求参数必须与折叠的请求头完全匹配
    if (!headerMatches) {
      fail(`llm request diverges from the folded request header`)
    }

    return next()
  }, { global: true, prepend: true })
  // ↑ prepend=true 确保这个检查在任何其他监听器之前运行
}, { inject: ['sessions'] })

十、运行时上下文投影

RuntimeContextProjection 管理动态运行上下文的快照。它确保相同的上下文不重复写入日志:

📄 packages/core/agent-loop/src/runtime-context.ts(第 25-76 行)

export class RuntimeContextProjection {
  private retained: { seq: number; text: string | undefined } | null | undefined
  // ↑ retained 有三种状态:
  // undefined = 从未有过快照;null = 快照被清除;对象 = 当前保留的快照

  constructor(ctx: Context, session: Session) {
    // 构造时从后向前扫描 Session 事件,找到最后一个活跃的 runtime-context 快照
    for (let index = session.events.length - 1; index >= 0; index -= 1) {
      const event = session.events[index]
      if (event?.type !== 'user/message' || !isOwned(event.data)) continue
      this.retained ??= null
      if (surface.has(event.seq)) {
        this.retained = { seq: event.seq, text: textOf(event.data) }
        break
      }
    }
    // ↑ 从后向前扫描是因为被替换(compaction)的事件在 surface 上被移除,
    // 但仍在 events 数组中。找到最后一个仍在 surface 上的快照

    ctx.on('session/event', (subject, event) => {
      if (event.type === 'user/message' && isOwned(event.data)) {
        this.retained = { seq: event.seq, text: textOf(event.data) }
      } else if (this.retained && isReplacementSurfaceEvent(event)
        && event.sourceEventSeqs?.includes(this.retained.seq) === true) {
        this.retained = null
        // ↑ 如果快照被替换事件覆盖,标记为 null(无有效快照)
      }
    })
  }

  project(current: string, sections: readonly ContextSnapshotSection[]): UserMessage | undefined {
    if (this.retained === undefined && current.length === 0) return
    // ↑ 从未有过快照且当前为空,不需要写入
    const snapshot = current.length === 0 ? CLEARED : current
    if (this.retained?.text === snapshot) return
    // ↑ 如果快照内容与当前相同,跳过写入(避免重复)
    return createUserMessage({ content: [{ type: 'text', text: snapshot }], ... })
  }
}

十一、架构总结

agent-loop 是整个 Harness 中唯一包含具体循环逻辑的包。它的设计遵循了几个关键原则:

Agent Loop 核心设计原则

🔹 日志即事实 — 所有状态变化通过 Session 事件日志记录,请求从日志推导

🔹 三层循环 — kick() → turn() → step(),每层有清晰的边界和退出条件

🔹 协作式取消 — AbortSignal 贯穿所有异步操作,每个 chunk/step 前检查

🔹 滚动并行池 — 工具调用按模式分组,有界并行 + 模型顺序提交

🔹 回滚安全 — 创建是事务性的,失败时所有资源自动回滚

🔹 插件扩展 — 所有行为通过 agent/* 事件扩展,循环本身不含业务逻辑

十二、与前后讲的关联

本节拆解了 Agent Loop 的核心循环机制。下一讲(第 10 讲)将分析工具注册与执行管道,深入 dsh-tools 包,了解工具如何被注册、策略如何执行、结果如何返回。工具管道是 Agent Loop 的"手脚"——Loop 决定什么时候调用工具,而工具管道决定工具调用实际发生什么。

📚 系列导航

← 第 8 讲:Agent 接口与注册表

→ 第 10 讲:工具注册与执行管道

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