Skip to content

Agent Loop ​

Agent loop 的实现主要在 packages/agent/src/agent-loop.ts。它负责把一次用户输入组织成多轮模型请求和工具执行,并把过程以事件形式暴露给 UI。

心智模型 ​

text
用户消息
  -> 组装上下文并声明工具
  -> 请求 LLM
  -> 得到 assistant message
  -> 如果有 tool calls,执行工具并追加 toolResult
  -> 如果需要下一轮,回到 LLM
  -> 否则结束

一次 run 从 agent_start 开始,到 agent_end 结束。

一次 turn 是:

text
一条 assistant message + 这条消息产生的所有 toolResult messages

所以一个 run 可以包含多个 turn。

入口 ​

文件里有两个对外入口:

agentLoop() ​

ts
agentLoop(prompts, context, config, signal, streamFn)

用于新 prompt:

  1. 把 prompts 加入 context.messages
  2. 发出这些新消息的 message_start / message_end
  3. 启动 loop
  4. 返回 EventStream<AgentEvent, AgentMessage[]>

agentLoopContinue() ​

ts
agentLoopContinue(context, config, signal, streamFn)

用于从已有上下文继续,不新增用户消息。典型场景是 retry 或恢复上下文。

约束:

  • context.messages 不能为空
  • 最后一条消息不能是 assistant
  • 最后一条消息必须能被 convertToLlm() 转成 provider 接受的 user 或 toolResult 消息

底层共享函数是 runLoop()。

主循环 ​

runLoop() 的简化逻辑如下:

ts
while (true) {
  let hasMoreToolCalls = true;

  while (hasMoreToolCalls || pendingMessages.length > 0) {
    // 1. 如果上一轮已完成,调用 prepareNextTurn()
    // 2. 注入 steering messages
    // 3. 调用 prepareRequest()
    // 4. 请求 LLM
    // 5. 处理 assistant message
    // 6. 如果有 tool calls,执行工具
    // 7. 调用 finishTurn()
    // 8. 轮询 steering messages
  }

  // 本来可以自然结束
  // 检查 follow-up messages
  // 如果有,继续下一轮
  // 否则退出
}

一次典型 turn ​

假设用户输入:

text
列出当前目录的文件,然后读 README

流程可能是:

  1. user message 进入 context
  2. LLM 返回 assistant message,包含 list_files 和 read_file 两个 tool calls
  3. loop 执行工具
  4. 生成两条 toolResult messages 并加入 context
  5. finishTurn() 没有要求结束
  6. 没有新的 steering messages
  7. 因为还有工具结果需要模型处理,进入下一轮
  8. LLM 看到工具结果,返回最终文本
  9. 这条 assistant message 没有 tool calls
  10. 没有 steering 或 follow-up messages
  11. 发出 agent_end

LLM 请求边界 ​

streamAssistantResponse() 是模型请求边界。

流程:

text
transformContext()
  -> convertToLlm()
  -> normalizeContext()
  -> getApiKey()
  -> streamFn()
  -> 消费 provider events
  -> 组装 AssistantMessage
  -> 发出 message_start / message_update / message_end

有两个关键点:

  1. loop 内部始终使用 AgentMessage[]
  2. 只有在请求 LLM 前才通过 convertToLlm() 转成 provider 能理解的 Message[]

这允许应用层定义自定义消息类型,然后在转换阶段过滤或映射它们。

错误和中止 ​

如果 assistant message 的 stopReason 是:

  • error
  • aborted

loop 会结束本次 run,不再执行工具,也不再轮询队列。

事件顺序仍然是:

text
turn_end
agent_end

工具声明 ​

context.tools 是运行时真正可以执行的工具。

但模型能调用哪些工具,是由 transcript 里的 system messages 声明的。

declareToolChanges() 会在请求前比较:

text
transcript 中已声明的工具
vs
context.tools 中可执行的工具

如果有差异,就生成或更新 system message,加入:

  • toolsAdded
  • toolsRemoved

例如新增了一个 search 工具,下一轮请求前会出现类似语义的声明:

text
toolsAdded: ["search"]

这样 provider transcript 可以持续表达当前工具集合。

工具执行 ​

executeToolCalls() 负责执行一条 assistant message 里的所有 tool calls。

执行模式有两种:

  • sequential
  • parallel

默认是 parallel。

如果这批工具里有任何一个声明了:

ts
executionMode: "sequential"

整批工具都会退回 sequential 执行。

单个工具的流程 ​

text
查找 tool
  -> prepareArguments()
  -> validateToolArguments()
  -> beforeToolCall()
  -> tool.execute()
  -> afterToolCall()
  -> 生成 ToolResultMessage

工具执行失败不会让 loop 抛异常。失败会转换成 isError: true 的 tool result,让模型看到错误并有机会修正。

Sequential ​

text
tool 1 prepare -> execute -> finalize
tool 2 prepare -> execute -> finalize

每个工具完整结束后才处理下一个。

Parallel ​

Parallel 不是完全无序:

  1. 按原始 tool call 顺序做 preflight
  2. preflight 包括查找工具、参数准备、校验和 beforeToolCall
  3. 允许执行的工具并发运行
  4. tool_execution_end 按完成顺序发出
  5. 最终 toolResult messages 按原始 tool call 顺序加入 context

举例:

text
assistant 请求:toolA、toolB

toolA 执行 100ms
toolB 执行 10ms

事件顺序:
tool_execution_start toolA
tool_execution_start toolB
tool_execution_end   toolB
tool_execution_end   toolA

context 里的顺序:
toolResult toolA
toolResult toolB

截断响应 ​

如果 assistant message 的 stopReason 是 length,说明输出被 token limit 截断。

这时工具参数可能不完整。即使某些参数看起来能通过校验,也可能实际缺少字段。

所以 loop 不会执行这些工具,而是把每个 tool call 都变成错误 result,提示模型重新发出完整调用。

提前结束 ​

工具结果可以带:

ts
terminate: true

规则是:只有当前工具批次里的所有 finalized results 都设置 terminate: true,loop 才会在这一批工具后停止。

如果批次里有一个 result 没有设置,loop 会继续。

消息注入 ​

Agent loop 支持两类运行中消息。

Steering messages ​

getSteeringMessages() 用于用户在 agent 工作时插入新指令。

例如:

text
用户:修复这个 bug
agent 开始调用工具
用户:不要改测试文件

第二条消息就是 steering message。

轮询时机:

  • run 开始时
  • 每个 assistant turn 的工具执行完成后
  • prepareNextTurn() 运行后

注意:

  • 当前 assistant message 的 tool calls 不会被跳过
  • steering messages 会在下一次 LLM 请求前加入 context

Follow-up messages ​

getFollowUpMessages() 用于 agent 本来要结束时排队的新任务。

例如:

text
用户:跑测试
agent 完成
队列里已有:再跑 lint

轮询时机:

  • 没有更多 tool calls
  • 没有 steering messages
  • run 本来要退出

如果有 follow-up messages,loop 会继续一个新的 turn。

配置 ​

AgentLoopConfig 在 packages/agent/src/types.ts。

必填 ​

ts
model: Model<any>;
convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;

convertToLlm() 决定哪些应用层消息会真正发给 provider。

上下文转换 ​

ts
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;

在 convertToLlm() 之前执行。

适合:

  • 裁剪历史
  • 上下文窗口管理
  • 注入外部上下文

每轮前替换状态 ​

ts
prepareNextTurn?: (context: PrepareNextTurnContext) => AgentLoopTurnUpdate | undefined;

在下一轮开始前调用。

可以返回:

ts
{
  context?: AgentContext;
  messages?: AgentMessage[];
  model?: Model<any>;
  thinkingLevel?: ThinkingLevel;
}

适合 compaction、模型切换、动态指令注入。

请求前调整 ​

ts
prepareRequest?: (request: PrepareRequestContext) => AgentRequestUpdate | undefined;

在每次 conversational LLM 请求前调用。

与 prepareNextTurn() 的区别:

  • prepareNextTurn() 面向下一轮
  • prepareRequest() 面向即将发生的这次请求

Turn 决策 ​

ts
finishTurn?: (turn: AgentTurnContext) => AgentTurnDecision | undefined;

在一轮 assistant message 和所有 toolResults 完成后调用。

返回:

ts
{ action: "end" }

会结束 run。

返回:

ts
{ action: "continue" }

会保证还有下一次 provider request。

如果既有自然需要的请求,比如 tool results,又有显式 continue,自然请求会满足这次继续,不会额外多一次请求。

工具 hooks ​

ts
beforeToolCall?: (context: BeforeToolCallContext) => Promise<BeforeToolCallResult | undefined>;
afterToolCall?: (context: AfterToolCallContext) => Promise<AfterToolCallResult | undefined>;

beforeToolCall() 可以:

  • 检查权限
  • 修改已验证参数
  • 阻止执行

afterToolCall() 可以改写:

  • content
  • details
  • structuredContent
  • isError
  • usage
  • terminate

事件 ​

主要事件结构:

text
agent_start
  turn_start
    message_start
    message_update
    message_end
    tool_execution_start
    tool_execution_update
    tool_execution_end
    message_start
    message_end
  turn_end
agent_end

含义:

  • agent_start / agent_end:一次 run 的开始和结束
  • turn_start / turn_end:一次 assistant turn 的开始和结束
  • message_start / message_update / message_end:消息生命周期
  • tool_execution_*:工具执行生命周期

agent_end.messages 是本次 loop 新产生的 AgentMessage[]。

注意:底层的 agentLoop() 和 agentLoopContinue() 返回的事件流是观察型的。它保持事件顺序,但不会等待消费者的 async event handler 完全 settle 后才继续后续生产阶段。如果需要事件处理成为 barrier,应使用 Agent 类。

嵌套工具调用 ​

runToolCall() 暴露给需要嵌套调用工具的场景。

它会走与模型发起的 tool call 相同的流程:

text
prepareArguments
  -> validateToolArguments
  -> beforeToolCall
  -> execute
  -> afterToolCall

但它:

  • 不发事件
  • 不添加 message

这样嵌套调用也会经过权限检查等 hooks。

小结 ​

Agent loop 可以理解为一个可配置、可中断、可观察的状态机:

text
context + tools + model
  -> assistant response
  -> tool results
  -> steering / follow-up
  -> next response
  -> agent_end

建议阅读顺序:

  1. packages/agent/src/types.ts

    • AgentLoopConfig
    • AgentContext
    • AgentEvent
  2. packages/agent/src/agent-loop.ts

    • runAgentLoop()
    • runLoop()
    • streamAssistantResponse()
    • executeToolCalls*()
  3. packages/agent/test/agent-loop.test.ts

    • 工具调用
    • parallel 事件顺序
    • steering messages
    • finishTurn
    • prepareNextTurn
    • terminate 语义