Appearance
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:
- 把
prompts加入context.messages - 发出这些新消息的
message_start/message_end - 启动 loop
- 返回
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流程可能是:
- user message 进入 context
- LLM 返回 assistant message,包含
list_files和read_file两个 tool calls - loop 执行工具
- 生成两条
toolResultmessages 并加入 context finishTurn()没有要求结束- 没有新的 steering messages
- 因为还有工具结果需要模型处理,进入下一轮
- LLM 看到工具结果,返回最终文本
- 这条 assistant message 没有 tool calls
- 没有 steering 或 follow-up messages
- 发出
agent_end
LLM 请求边界
streamAssistantResponse() 是模型请求边界。
流程:
text
transformContext()
-> convertToLlm()
-> normalizeContext()
-> getApiKey()
-> streamFn()
-> 消费 provider events
-> 组装 AssistantMessage
-> 发出 message_start / message_update / message_end有两个关键点:
- loop 内部始终使用
AgentMessage[] - 只有在请求 LLM 前才通过
convertToLlm()转成 provider 能理解的Message[]
这允许应用层定义自定义消息类型,然后在转换阶段过滤或映射它们。
错误和中止
如果 assistant message 的 stopReason 是:
erroraborted
loop 会结束本次 run,不再执行工具,也不再轮询队列。
事件顺序仍然是:
text
turn_end
agent_end工具声明
context.tools 是运行时真正可以执行的工具。
但模型能调用哪些工具,是由 transcript 里的 system messages 声明的。
declareToolChanges() 会在请求前比较:
text
transcript 中已声明的工具
vs
context.tools 中可执行的工具如果有差异,就生成或更新 system message,加入:
toolsAddedtoolsRemoved
例如新增了一个 search 工具,下一轮请求前会出现类似语义的声明:
text
toolsAdded: ["search"]这样 provider transcript 可以持续表达当前工具集合。
工具执行
executeToolCalls() 负责执行一条 assistant message 里的所有 tool calls。
执行模式有两种:
sequentialparallel
默认是 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 不是完全无序:
- 按原始 tool call 顺序做 preflight
- preflight 包括查找工具、参数准备、校验和
beforeToolCall - 允许执行的工具并发运行
tool_execution_end按完成顺序发出- 最终
toolResultmessages 按原始 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() 可以改写:
contentdetailsstructuredContentisErrorusageterminate
事件
主要事件结构:
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建议阅读顺序:
packages/agent/src/types.tsAgentLoopConfigAgentContextAgentEvent
packages/agent/src/agent-loop.tsrunAgentLoop()runLoop()streamAssistantResponse()executeToolCalls*()
packages/agent/test/agent-loop.test.ts- 工具调用
- parallel 事件顺序
- steering messages
finishTurnprepareNextTurn- terminate 语义