这个文件是什么:整个 agent 的"心脏"——一个循环函数。它不断地调 LLM、执行工具、把结果喂回去,直到任务完成。
What is this file: The "heart" of the entire agent — a loop function. It continuously calls the LLM, executes tools, feeds results back, until the task is complete.
阅读方式:左边是完整原始代码(保持文件原顺序),右边是逐段解读。绿色高亮 = 关键行。不重要的代码段可折叠。
How to read: Left column is the complete original code (in file order), right column is section-by-section explanation. Green highlight = key lines. Less important code sections are collapsible.
想象一个只有脑子没有手的助手。你给他任务,他想了想说"我需要查日历"——但他自己查不了,得让你的系统替他查。系统查完告诉他结果,他又说"好,现在帮我发封邮件"。系统又替他发了。最后他说"搞定了"。
Imagine an assistant who has a brain but no hands. You give them a task, they think and say "I need to check the calendar" — but they can't do it themselves, so your system checks for them. The system reports the result, then they say "OK, now send an email for me." The system sends it. Finally they say "All done."
角色对应:AI(LLM)= 助手的大脑,只负责想和决策;Agent Loop(你写的代码)= 助手的手脚,负责实际执行工具;用户 = 雇主,发起任务。
Role mapping: AI (LLM) = the assistant's brain, only thinks and decides; Agent Loop (your code) = the assistant's hands, actually executes tools; User = the employer, initiates tasks.
这个"大脑说要做什么 → 手脚去做 → 结果反馈给大脑 → 大脑继续想"的来回,就是 agent loop。
This back-and-forth of "brain says what to do → hands do it → results fed back to brain → brain thinks again" is the agent loop.
Agent Loop 循环逻辑Agent Loop logic flow
简单说:这段从其他文件引入了需要的"零件"定义。import 就像目录里列出"本章用到的概念"。你只需要记住几个关键名字:
• AgentContext = agent 的记忆(对话历史 + 系统提示 + 可用工具)
• AgentMessage = 对话里的一条消息
• AgentEvent = 循环中发生的事件(用于通知外部"我在干什么")
• StreamFn = 调用 LLM 的函数
In short: This section imports the required type definitions from other files. import is like a table of contents listing "concepts used in this chapter." Just remember a few key names:
• AgentContext = the agent's memory (conversation history + system prompt + available tools)
• AgentMessage = a single message in the conversation
• AgentEvent = events during the loop (notifying the outside "here's what I'm doing")
• StreamFn = the function that calls the LLM
简单说:这些是"外部调用入口"。产品代码通过 agentLoop() 或 runAgentLoop() 启动循环。它们做的事很简单:
1. 把用户的新消息加入上下文
2. 通知外部"循环开始了"(emit 事件)
3. 调用真正的核心函数 runLoop()
就像汽车的启动按钮——按下去,真正干活的是引擎(runLoop)。
In short: These are the "external entry points." Product code starts the loop via agentLoop() or runAgentLoop(). They do three simple things:
1. Add the user's new messages to the context
2. Notify the outside that "the loop has started" (emit events)
3. Call the real core function runLoop()
Like a car's start button — press it, and the engine (runLoop) does the actual work.
152/** 153 * Main loop logic shared by 154 * agentLoop and agentLoopContinue. 155 */ 156async function runLoop( 157 currentContext: AgentContext, 158 newMessages: AgentMessage[], 159 config: AgentLoopConfig, 160 signal: AbortSignal | undefined, 161 emit: AgentEventSink, 162 streamFn?: StreamFn, 163): Promise<void> {
这个函数接收 6 个参数:
This function takes 6 parameters:
163 let firstTurn = true; 164 // Check for steering messages at start 165 let pendingMessages: AgentMessage[] = 166 (await config.getSteeringMessages?.()) 167 || [];
firstTurn:标记"是不是第一轮",用于控制事件通知。
firstTurn: marks "is this the first turn," used to control event notifications.
pendingMessages:检查有没有"插队消息"——用户可能在等待期间打了新字。
pendingMessages: checks for "queued messages" — the user may have typed new input while waiting.
config.getSteeringMessages?.() 里的 ?. 表示"如果这个函数存在就调用,不存在就跳过"。|| [] 表示"如果结果是空的,就用空数组"。Syntax note: The ?. in config.getSteeringMessages?.() means "call this function if it exists, skip if not." || [] means "if the result is empty, use an empty array."168 // Outer loop: continues when queued 169 // follow-up messages arrive after 170 // agent would stop 171 while (true) { 172 let hasMoreToolCalls = true;
while (true) = 无限循环。只有遇到 break 或 return 才会退出。
while (true) = infinite loop. Only exits when it hits break or return.
外层循环的作用:当 agent 完成工作想停下来时,检查有没有用户追加的新任务(followUp)。如果有,就继续干活。
Purpose of the outer loop: when the agent finishes and wants to stop, check if the user has added new tasks (followUp). If so, keep working.
hasMoreToolCalls = true:初始设为 true,确保内层循环至少执行一次。
hasMoreToolCalls = true: initialized to true, ensuring the inner loop executes at least once.
173 174 // Inner loop: process tool calls 175 // and steering messages 176 while (hasMoreToolCalls 177 || pendingMessages.length > 0) {
循环条件:只要满足下面任一条件,就继续:
Loop condition: continues as long as either condition is met:
hasMoreToolCalls = AI 还需要用工具hasMoreToolCalls = the AI still needs to use toolspendingMessages.length > 0 = 有插队消息要处理pendingMessages.length > 0 = there are queued messages to process当两个都不满足时,内层循环退出 → AI 认为任务完成了。
When neither is satisfied, the inner loop exits → the AI considers the task complete.
178 if (!firstTurn) { 179 await emit({ type: "turn_start" }); 180 } else { 181 firstTurn = false; 182 } 183 184 // Process pending messages 185 if (pendingMessages.length > 0) { 186 for (const message of pendingMessages) { 187 await emit({ 188 type: "message_start", message 189 }); 190 await emit({ 191 type: "message_end", message 192 }); 193 currentContext.messages.push(message); 194 newMessages.push(message); 195 } 196 pendingMessages = []; 197 }
如果用户在 AI 工作期间发了新消息(steering),这里把它们加入"记忆"(currentContext),这样 AI 下次被调用时就能看到。
If the user sent new messages (steering) while the AI was working, this injects them into the "memory" (currentContext), so the AI can see them the next time it's called.
emit 通知外部"有新消息被注入了"。push 是把消息追加到列表末尾。
emit notifies the outside "new messages were injected." push appends the message to the end of the list.
198 199 // Stream assistant response 200 const message = await 201 streamAssistantResponse( 202 currentContext, config, 203 signal, emit, streamFn 204 ); 205 newMessages.push(message);
这是循环的第一个关键动作。
This is the first key action in the loop.
这一行代码的意思是:调用 streamAssistantResponse 这个函数,把 currentContext(所有对话历史)传给它,等它返回 AI 的回复。
This line calls the streamAssistantResponse function, passes it the currentContext (all conversation history), and waits for the AI's response.
"打包"在哪里?不在这里——这里只是"派任务"。具体的打包过程在 streamAssistantResponse 函数内部(往下滚到本页的下一个大段落就能看到)。那里面做了:
Where does the "packaging" happen? Not here — this just "dispatches the task." The actual packaging is inside streamAssistantResponse (scroll down to the next major section). Inside, it does:
AI 的回复可能是:
The AI's response could be:
206 207 if (message.stopReason === "error" 208 || message.stopReason === "aborted" 209 ) { 210 await emit({ 211 type: "turn_end", 212 message, toolResults: [] 213 }); 214 await emit({ 215 type: "agent_end", 216 messages: newMessages 217 }); 218 return; 219 }
如果 LLM 调用失败("error")或被用户主动取消("aborted"),整个循环立即结束。return 就是退出函数。
If the LLM call fails ("error") or the user actively cancels ("aborted"), the entire loop ends immediately. return exits the function.
220 221 // Check for tool calls 222 const toolCalls = message.content 223 .filter((c) => c.type === "toolCall"); 224 225 const toolResults: ToolResultMessage[] = []; 226 hasMoreToolCalls = false; 227 228 if (toolCalls.length > 0) { 229 const executedToolBatch = 230 await executeToolCalls( 231 currentContext, message, 232 config, signal, emit 233 ); 234 toolResults.push( 235 ...executedToolBatch.messages 236 ); 237 hasMoreToolCalls = 238 !executedToolBatch.terminate; 239 240 for (const result of toolResults) { 241 currentContext.messages.push(result); 242 newMessages.push(result); 243 } 244 }
这是决定"循环是否继续"的核心逻辑。
This is the core logic that decides "does the loop continue."
第 222-223 行:从 AI 的回复里找出所有"工具调用请求"。filter = 过滤出符合条件的项。
Lines 222-223: Extract all "tool call requests" from the AI's response. filter = select items matching the condition.
第 226 行:先假设"没有更多工具调用了"(= 循环该停了)。
Line 226: Assume "no more tool calls" by default (= the loop should stop).
第 228-244 行:如果确实有工具调用 →
Lines 228-244: If there are tool calls →
executeToolCalls)executeToolCalls)hasMoreToolCalls = !terminate:如果工具没说"终止",就设为 true → 循环继续hasMoreToolCalls = !terminate: if the tool didn't say "terminate," set to true → loop continues245 246 await emit({ 247 type: "turn_end", 248 message, toolResults 249 }); 250 251 if ( 252 await config.shouldStopAfterTurn?.( 253 { 254 message, toolResults, 255 context: currentContext, 256 newMessages, 257 } 258 ) 259 ) { 260 await emit({ 261 type: "agent_end", 262 messages: newMessages 263 }); 264 return; 265 } 266 267 pendingMessages = 268 (await config.getSteeringMessages?.()) 269 || []; 270 271 } // ← 内层循环结束inner loop ends
第 246-249 行:通知外部"这一轮结束了"。
Lines 246-249: Notify the outside "this turn is over."
第 251-265 行(灰色):停止条件 #2——可选的外部强制停止。产品可以在这里设规则,比如"最多 10 轮"、"token 用量超标就停"。
Lines 251-265 (dimmed): Stop condition #2 — optional external forced stop. Products can set rules here, like "max 10 turns" or "stop when token usage exceeds limit."
第 267-269 行:检查有没有新的插队消息,下一轮处理。
Lines 267-269: Check for new queued messages to process in the next turn.
shouldStopAfterTurn 是 Pi 的钩子。但"外部强制停止"的概念是通用的——Claude Code 也有最大轮次限制。Pi-Specific: shouldStopAfterTurn is a Pi hook. But the concept of "external forced stop" is universal — Claude Code also has a maximum turn limit.272 273 // Agent would stop here. 274 // Check for follow-up messages. 275 const followUpMessages = 276 (await config.getFollowUpMessages?.()) 277 || []; 278 if (followUpMessages.length > 0) { 279 pendingMessages = followUpMessages; 280 continue; 281 } 282 283 // No more messages, exit 284 break; 285 } 286 287 await emit({ 288 type: "agent_end", 289 messages: newMessages 290 }); 291}
内层循环结束后(AI 不再需要工具了),来到这里:
After the inner loop ends (the AI no longer needs tools), we arrive here:
continue(回到外层循环顶部,重新进内层)continue (go back to the top of the outer loop, re-enter the inner loop)break(退出外层循环,彻底结束)break (exit the outer loop, end completely)第 287-290 行:通知外部"agent 整个结束了"。
Lines 287-290: Notify the outside "the agent is completely done."
252async function streamAssistantResponse( 253 context: AgentContext, 254 config: AgentLoopConfig, 255 signal: AbortSignal | undefined, 256 emit: AgentEventSink, 257 streamFn?: StreamFn, 258): Promise<AssistantMessage> { 259 260 // 第一步:上下文变换Step 1: context transform 261 let messages = context.messages; 262 if (config.transformContext) { 263 messages = await config.transformContext( 264 messages, signal 265 ); 266 } 267 268 // 第二步:格式转换Step 2: format conversion 269 const llmMessages = 270 await config.convertToLlm(messages); 271 272 // 第三步:组装完整请求Step 3: assemble full request 273 const llmContext: Context = { 274 systemPrompt: context.systemPrompt, 275 messages: llmMessages, 276 tools: context.tools, 277 }; 278 279 const streamFunction = 280 streamFn || streamSimple; 281 282 // 解析 API keyresolve API key 283 const resolvedApiKey = 284 (config.getApiKey 285 ? await config.getApiKey( 286 config.model.provider 287 ) 288 : undefined) 289 || config.apiKey; 290 291 // 第四步:真正调用 LLMStep 4: actually call the LLM 292 const response = await streamFunction( 293 config.model, llmContext, { 294 ...config, 295 apiKey: resolvedApiKey, 296 signal, 297 });
不能直接把所有原始数据丢给 LLM——要先处理:
You can't just throw all raw data at the LLM — it needs processing first:
第一步 transformContext(260-266行):
对消息做"预处理"——可能是压缩旧对话、删掉不重要的消息、裁剪到窗口大小以内。
Step 1: transformContext (lines 260-266):
"Pre-process" the messages — compress old conversations, drop unimportant messages, trim to fit within the context window.
第二步 convertToLlm(268-270行):
格式转换——agent 内部记了很多额外信息(时间戳、元数据),LLM 不需要这些,转成 LLM API 要求的简洁格式。
Step 2: convertToLlm (lines 268-270):
Format conversion — the agent internally stores extra info (timestamps, metadata) that the LLM doesn't need. Convert to the lean format the LLM API requires.
第三步(272-277行):组装完整请求——系统提示词 + 对话 + 工具列表。
Step 3 (lines 272-277): Assemble the complete request — system prompt + conversation + tool list.
第四步(291-297行):真正发请求给 LLM。
Step 4 (lines 291-297): Actually send the request to the LLM.
298 299 // 第五步:处理流式响应Step 5: process streaming response 300 let partialMessage = null; 301 let addedPartial = false; 302 303 for await (const event of response) { 304 switch (event.type) { 305 case "start": 306 partialMessage = event.partial; 307 context.messages.push(partialMessage); 308 addedPartial = true; 309 await emit({ type: "message_start", 310 message: {...partialMessage} }); 311 break; 312 case "text_start": 313 case "text_delta": 314 // ... 更多事件类型more event types ... 315 // 更新消息内容update message content 316 await emit({ 317 type: "message_update", ... 318 }); 319 break; 320 case "done": 321 case "error": 322 const finalMessage = 323 await response.result(); 324 // 更新到最终版本update to final version 325 await emit({ 326 type: "message_end", 327 message: finalMessage 328 }); 329 return finalMessage; 330 } 331 } 332 // ... 收尾处理cleanup ... 333}
"流式"意味着 AI 的回答不是一次性全给你,而是一个字一个字蹦出来(就像你看 ChatGPT 打字的效果)。
"Streaming" means the AI's answer doesn't arrive all at once, but character by character (like watching ChatGPT type).
这段代码做的事:
What this code does:
你不需要记住这些细节。只需知道:这个函数最终返回一条完整的 AI 回复消息。
You don't need to memorize these details. Just know: this function ultimately returns one complete AI response message.
350async function executeToolCalls( 351 currentContext: AgentContext, 352 assistantMessage: AssistantMessage, 353 config: AgentLoopConfig, 354 signal: AbortSignal | undefined, 355 emit: AgentEventSink, 356): Promise<ExecutedToolCallBatch> { 357 358 const toolCalls = assistantMessage 359 .content 360 .filter((c) => c.type === "toolCall"); 361 362 // 判断是顺序执行还是并行执行decide sequential vs parallel execution 363 const hasSequentialToolCall = 364 toolCalls.some((tc) => 365 currentContext.tools?.find( 366 (t) => t.name === tc.name 367 )?.executionMode === "sequential" 368 ); 369 370 if (config.toolExecution === "sequential" 371 || hasSequentialToolCall) { 372 return executeToolCallsSequential(...); 373 } 374 return executeToolCallsParallel(...); 375}
AI 可能一次请求多个工具("同时读 3 个文件")。系统要决定:
The AI may request multiple tools at once ("read 3 files simultaneously"). The system must decide:
判断规则:如果某个工具自己标记了"我必须顺序执行"(比如写文件),或者全局配置了顺序模式,就走顺序路径。
Decision rule: if a tool marks itself as "I must run sequentially" (e.g., file writes), or if the global config is set to sequential mode, take the sequential path.
| 步骤Step | 函数Function | 做什么What it does | 类比Analogy |
|---|---|---|---|
| 1. 准备1. Prepare | prepareToolCall() |
找到工具、校验参数、问 beforeToolCall hook "允许吗?"Find the tool, validate arguments, ask the beforeToolCall hook "is this allowed?" | 助手说"我要用你的信用卡"→ 你先检查合不合理Assistant says "I need your credit card" → you check if it's reasonable first |
| 2. 执行2. Execute | executePreparedToolCall() |
真正运行工具代码Actually run the tool code | 你同意了,助手真的去刷卡You approved, the assistant actually swipes the card |
| 3. 收尾3. Finalize | finalizeExecutedToolCall() |
调 afterToolCall hook、格式化结果Call the afterToolCall hook, format the result | 刷完卡后你审查一下账单,确认没问题After swiping, you review the receipt to confirm everything's fine |
beforeToolCall hook 可以返回 { block: true } 阻止工具执行。beforeToolCall hook can return { block: true } to block tool execution.下面是完整代码(仅供参考,不必逐行阅读):Below is the full code (for reference only, no need to read line by line):
| 你看到 Claude Code 在做的事What you see Claude Code doing | 对应代码里的什么What it maps to in the code |
|---|---|
| AI 说"我要读这个文件" → 读了 → "我要编辑那个" → 编辑了 → "完成了"AI says "I'll read this file" → reads → "I'll edit that" → edits → "Done" | runLoop 内层循环的多次迭代Multiple iterations of runLoop's inner loop |
| 弹出"Allow Read file X?"对话框The "Allow Read file X?" dialog pops up | prepareToolCall 里的 beforeToolCall hookThe beforeToolCall hook inside prepareToolCall |
| AI 一个字一个字打出来AI types out character by character | streamAssistantResponse 里的流式事件处理Stream event handling in streamAssistantResponse |
| 你在 AI 工作时打字补充指令You type additional instructions while the AI works | steering 消息注入(pendingMessages)Steering message injection (pendingMessages) |
| AI 说"完成了"然后停下AI says "Done" and stops | toolCalls 为空 → hasMoreToolCalls = false → breaktoolCalls is empty → hasMoreToolCalls = false → break |