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技术推荐官」获取更多源码解析内容
夜雨聆风