agent-loop.ts 精读agent-loop.ts Deep Read

文件路径:packages/agent/src/agent-loop.ts  |  695 行  |  PiMono 的循环引擎 File path: packages/agent/src/agent-loop.ts  |  695 lines  |  PiMono's loop engine

这个文件是什么:整个 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.

读代码前先看全貌:这个文件的结构Before reading code — file structure overview

文件从上到下分为 5 块: ┌─ 第 1-24 行 ──── 导入声明(不重要,跳过) │ ├─ 第 25-150 行 ── 入口函数(外部调用的接口,过一遍就行) │ agentLoop() / agentLoopContinue() / runAgentLoop() / runAgentLoopContinue() │ ├─ 第 152-246 行 ─ ⭐ runLoop() ← 核心中的核心,必须逐行搞懂 │ 这 90 行就是整个 agent 的骨架 │ ├─ 第 248-345 行 ─ streamAssistantResponse() ← 重要:怎么调 LLM │ 消息变换 → 格式转换 → 调用 → 处理流式响应 │ └─ 第 347-695 行 ─ 工具执行相关函数 ← 理解概念即可,不用逐行 prepare → execute → finalize 三步流程
The file splits into 5 sections from top to bottom: ┌─ Lines 1-24 ───── Import declarations (not important, skip) │ ├─ Lines 25-150 ─── Entry functions (external API, quick scan) │ agentLoop() / agentLoopContinue() / runAgentLoop() / runAgentLoopContinue() │ ├─ Lines 152-246 ── ⭐ runLoop() ← The core of the core, must understand line by line │ These 90 lines are the skeleton of the entire agent │ ├─ Lines 248-345 ── streamAssistantResponse() ← Important: how to call the LLM │ Message transform → format conversion → call → stream response handling │ └─ Lines 347-695 ── Tool execution functions ← Understand concepts, no need for line-by-line prepare → execute → finalize three-step flow

生活类比:Agent Loop 是什么Real-life analogy: What is an Agent Loop

想象一个只有脑子没有手的助手。你给他任务,他想了想说"我需要查日历"——但他自己查不了,得让你的系统替他查。系统查完告诉他结果,他又说"好,现在帮我发封邮件"。系统又替他发了。最后他说"搞定了"。

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.

用户消息 User message 发给 LLM Send to LLM LLM 回复 LLM response 要用工具? Use tools? No 返回结果 Return result Yes 执行工具 Execute tools 结果塞回对话历史 Feed results back 循 环 Loop

Agent Loop 循环逻辑Agent Loop logic flow

📦 第 1-24 行:导入声明 — 点击展开(不重要,了解即可)📦 Lines 1-24: Import declarations — click to expand (not important, just FYI)
1/** 2 * Agent loop that works with AgentMessage throughout. 3 * Transforms to Message[] only at the LLM call boundary. 4 */ 5 6import { 7 type AssistantMessage, 8 type Context, 9 EventStream, 10 streamSimple, 11 type ToolResultMessage, 12 validateToolArguments, 13} from "@mariozechner/pi-ai"; 14import type { 15 AgentContext, 16 AgentEvent, 17 AgentLoopConfig, 18 AgentMessage, 19 AgentTool, 20 AgentToolCall, 21 AgentToolResult, 22 StreamFn, 23} from "./types.js"; 24 25export type AgentEventSink = (event: AgentEvent) => Promise<void> | void;

简单说:这段从其他文件引入了需要的"零件"定义。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

🚪 第 26-150 行:入口函数 — 点击展开(过一遍即可,不是核心)🚪 Lines 26-150: Entry functions — click to expand (quick scan, not the core)
31export function agentLoop( 32 prompts: AgentMessage[], 33 context: AgentContext, 34 config: AgentLoopConfig, 35 signal?: AbortSignal, 36 streamFn?: StreamFn, 37): EventStream<AgentEvent, AgentMessage[]> { 38 const stream = createAgentStream(); 39 40 void runAgentLoop( 41 prompts, context, config, 42 async (event) => { stream.push(event); }, 43 signal, streamFn, 44 ).then((messages) => { stream.end(messages); }); 45 46 return stream; 47} 48 // ... agentLoopContinue() 类似结构,省略 ... 95export async function runAgentLoop( 96 prompts: AgentMessage[], 97 context: AgentContext, 98 config: AgentLoopConfig, 99 emit: AgentEventSink, 100 signal?: AbortSignal, 101 streamFn?: StreamFn, 102): Promise<AgentMessage[]> { 103 const newMessages: AgentMessage[] = [...prompts]; 104 const currentContext: AgentContext = { 105 ...context, 106 messages: [...context.messages, ...prompts], 107 }; 108 109 await emit({ type: "agent_start" }); 110 await emit({ type: "turn_start" }); 111 for (const prompt of prompts) { 112 await emit({ type: "message_start", message: prompt }); 113 await emit({ type: "message_end", message: prompt }); 114 } 115 116 await runLoop(currentContext, newMessages, config, signal, emit, streamFn); 117 return newMessages; 118}

简单说:这些是"外部调用入口"。产品代码通过 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-246 行:runLoop() — 核心循环引擎⭐ Lines 152-246: runLoop() — Core Loop Engine

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> {

函数签名:循环需要什么"原料"Function signature: what "ingredients" the loop needs

这个函数接收 6 个参数:

This function takes 6 parameters:

  • currentContext — agent 的"记忆":包含所有对话历史、系统提示词、可用工具列表
  • currentContext — the agent's "memory": contains all conversation history, system prompt, and available tools
  • newMessages — 收集器:这次循环新产生的消息都存到这里
  • newMessages — collector: all new messages produced during this loop are stored here
  • config — 配置:模型选择、各种 hook 函数、消息队列
  • config — configuration: model selection, various hook functions, message queue
  • signal — 取消按钮:外部可以随时按下它中断循环
  • signal — cancel button: the outside can press it anytime to interrupt the loop
  • emit — 广播器:每发生一件事就通知外面(UI 靠这个显示进度)
  • emit — broadcaster: notifies the outside every time something happens (the UI uses this to show progress)
  • streamFn — 调 LLM 的函数(可以替换成不同的 AI 模型)
  • streamFn — the function that calls the LLM (can be swapped for different AI models)
所有 agent 框架都有的:"记忆" + "取消机制" + "事件通知"。名字不同,本质一样。Universal to all agent frameworks: "memory" + "cancellation mechanism" + "event notification." Different names, same essence.
163  let firstTurn = true;
164  // Check for steering messages at start
165  let pendingMessages: AgentMessage[] =
166    (await config.getSteeringMessages?.())
167    || [];

初始化Initialization

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;

外层循环开始Outer loop begins

while (true) = 无限循环。只有遇到 breakreturn 才会退出。

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) {

⭐ 内层循环 = Agent Loop 本体⭐ Inner loop = the Agent Loop itself

循环条件:只要满足下面任一条件,就继续:

Loop condition: continues as long as either condition is met:

  • hasMoreToolCalls = AI 还需要用工具
  • hasMoreToolCalls = the AI still needs to use tools
  • pendingMessages.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.

这就是 agent loop 的核心判断:AI 有工具要用 → 继续;AI 没话说了 → 停。所有 agent 框架都是这个逻辑。This is the core decision of the agent loop: AI has tools to use → continue; AI has nothing more to say → stop. Every agent framework follows this logic.
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      }

步骤 A:注入插队消息Step A: Inject queued messages

如果用户在 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.

Pi 特有:这个"插队"机制不是所有框架都有。但"用户能在 agent 工作时干预"的需求是通用的。Pi-Specific: This "message queuing" mechanism isn't in every framework. But the need for "users to intervene while the agent is working" is universal.
198
199      // Stream assistant response
200      const message = await
201        streamAssistantResponse(
202          currentContext, config,
203          signal, emit, streamFn
204        );
205      newMessages.push(message);

⭐ 步骤 B:调用 LLM⭐ Step B: Call the LLM

这是循环的第一个关键动作。

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:

  1. transformContext — 压缩/裁剪消息("行李太多,先扔掉不重要的")
  2. transformContext — compress/trim messages ("too much luggage, discard the unimportant stuff")
  3. convertToLlm — 格式转换("把行李按航空公司要求重新打包")
  4. convertToLlm — format conversion ("repack luggage per airline requirements")
  5. 组装 systemPrompt + messages + tools("全部装箱")
  6. Assemble systemPrompt + messages + tools ("box everything up")
  7. 调用 LLM API("发货")
  8. Call the LLM API ("ship it")

AI 的回复可能是:

The AI's response could be:

  • 纯文字回答("你好,我帮你总结完了")
  • A text-only answer ("Hi, I've finished the summary for you")
  • 工具调用请求("我需要读一个文件")
  • A tool call request ("I need to read a file")
  • 错误(网络断了、API 报错)
  • An error (network down, API error)
通用模式:每个 agent 框架都有这一步——"把上下文发给 LLM,等回复"。这是循环的心跳。Universal Pattern: Every agent framework has this step — "send context to the LLM, wait for a response." This is the loop's heartbeat.
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      }

停止条件 #1:出错 / 被取消Stop condition #1: Error / Aborted

如果 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.

通用模式:所有 agent 都有这个安全阀。出错了不能继续瞎跑。Universal Pattern: Every agent has this safety valve. Can't keep running blindly after an error.
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      }

⭐ 步骤 C:检查并执行工具⭐ Step C: Check and execute tools

这是决定"循环是否继续"的核心逻辑。

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 →

  1. 执行这些工具(executeToolCalls
  2. Execute the tools (executeToolCalls)
  3. 收集执行结果
  4. Collect execution results
  5. hasMoreToolCalls = !terminate:如果工具没说"终止",就设为 true → 循环继续
  6. hasMoreToolCalls = !terminate: if the tool didn't say "terminate," set to true → loop continues
  7. 把工具结果加入"记忆"(下次 AI 能看到结果)
  8. Add tool results to "memory" (the AI can see results next time)
核心判断(通用模式)
AI 回复里有工具调用 → 执行 → 结果喂回去 → 继续循环
AI 回复里没有工具调用 → 循环结束(AI 给出了最终答案)

这就是 agent 和普通聊天的本质区别。
Core decision (Universal Pattern):
AI response has tool calls → execute → feed results back → continue loop
AI response has no tool calls → loop ends (AI gave its final answer)

This is the fundamental difference between an agent and regular chat.
245
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

步骤 D:一轮结束,检查是否强制停止Step D: Turn ends, check for forced stop

第 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.

Pi 特有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}

外层循环:真正的结束Outer loop: the real ending

内层循环结束后(AI 不再需要工具了),来到这里:

After the inner loop ends (the AI no longer needs tools), we arrive here:

  1. 检查有没有 followUp 消息(用户追加的新任务)
  2. Check for followUp messages (new tasks the user added)
  3. 如果有 → continue(回到外层循环顶部,重新进内层)
  4. If yes → continue (go back to the top of the outer loop, re-enter the inner loop)
  5. 如果没有 → break(退出外层循环,彻底结束)
  6. If no → break (exit the outer loop, end completely)

第 287-290 行:通知外部"agent 整个结束了"。

Lines 287-290: Notify the outside "the agent is completely done."

总结:循环的三个停止条件
1. 出错/取消 → 立即 return
2. 外部强制 shouldStopAfterTurn → return
3. AI 没工具要用 + 没 followUp → break

条件 1 和 3 所有 agent 都有。条件 2 是产品层面的安全阀。
Summary: three stop conditions for the loop
1. Error/cancelled → immediate return
2. External forced stop shouldStopAfterTurn → return
3. AI has no tools to use + no followUp → break

Conditions 1 and 3 exist in every agent. Condition 2 is a product-level safety valve.

第 252-345 行:streamAssistantResponse() — 怎么调用 LLMLines 252-345: streamAssistantResponse() — How to call the LLM

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 前的"两步变换管道"The "two-step transform pipeline" before calling the LLM

不能直接把所有原始数据丢给 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.

通用模式:所有 agent 在调 LLM 前都有"变换管道"。Claude Code 有更复杂的 9 步管道,但核心思路一样。Universal Pattern: Every agent has a "transform pipeline" before calling the LLM. Claude Code has a more complex 9-step pipeline, but the core idea is the same.
和知识地图的连接:这就是"② 上下文工程"的具体实现位置。Claude Code 的实现更复杂,但核心思路相同。Connection to the knowledge map: This is where "② Context Engineering" is concretely implemented. Claude Code's implementation is more complex, but the core approach is the same.
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}

流式响应处理(灰色 = 细节不重要)Stream response handling (dimmed = details not important)

"流式"意味着 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:

  1. 收到第一个字 → 通知外部"AI 开始说话了"
  2. First character received → notify outside "the AI started speaking"
  3. 每收到新内容 → 通知外部"AI 又说了一些"
  4. New content received → notify outside "the AI said more"
  5. AI 说完了 → 通知外部"消息完成",返回完整消息
  6. AI finished → notify outside "message complete," return the full message

你不需要记住这些细节。只需知道:这个函数最终返回一条完整的 AI 回复消息。

You don't need to memorize these details. Just know: this function ultimately returns one complete AI response message.

你的体验:Claude Code 里 AI 一个字一个字打出来——就是这个"流式"机制在工作。Your experience: In Claude Code, the AI types out character by character — that's this "streaming" mechanism at work.

第 350-695 行:工具执行相关函数Lines 350-695: Tool execution functions

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}

工具执行入口:并行 vs 顺序Tool execution entry: parallel vs sequential

AI 可能一次请求多个工具("同时读 3 个文件")。系统要决定:

The AI may request multiple tools at once ("read 3 files simultaneously"). The system must decide:

  • 并行:3 个同时执行(快,默认)
  • Parallel: execute all 3 simultaneously (fast, default)
  • 顺序:一个完了再执行下一个(慢但安全)
  • Sequential: finish one before starting the next (slower but safer)

判断规则:如果某个工具自己标记了"我必须顺序执行"(比如写文件),或者全局配置了顺序模式,就走顺序路径。

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.

通用模式:并行 vs 顺序的选择所有框架都要做。Claude Code 的 Read 并行执行,Write 顺序执行——同一个道理。Universal Pattern: The parallel vs sequential choice exists in every framework. In Claude Code, Read runs in parallel, Write runs sequentially — same principle.
🔧 第 376-695 行:工具执行的具体实现 — 点击展开(理解三步流程即可,不用逐行读)🔧 Lines 376-695: Tool execution implementation — click to expand (understand the 3-step flow, no need to read line by line)

每个工具调用经过三步:Each tool call goes through three steps:

步骤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
权限拦截点:在第 1 步"准备"阶段,beforeToolCall hook 可以返回 { block: true } 阻止工具执行。
这就是 Claude Code 弹出"Allow Read file X?"对话框的底层机制。
Permission intercept point: In step 1 "Prepare," the beforeToolCall hook can return { block: true } to block tool execution.
This is the underlying mechanism behind Claude Code's "Allow Read file X?" dialog.

下面是完整代码(仅供参考,不必逐行阅读):Below is the full code (for reference only, no need to read line by line):

372async function executeToolCallsSequential( 373 currentContext, assistantMessage, toolCalls, config, signal, emit, 374): Promise<ExecutedToolCallBatch> { 375 const finalizedCalls = []; 376 const messages = []; 377 378 for (const toolCall of toolCalls) { 379 await emit({ type: "tool_execution_start", toolCallId: toolCall.id, ... }); 380 381 const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal); 382 383 if (preparation.kind === "immediate") { 384 // 准备阶段就被拦截了(参数错误/权限不允许) 385 finalized = { toolCall, result: preparation.result, isError: preparation.isError }; 386 } else { 387 // 真正执行 388 const executed = await executePreparedToolCall(preparation, signal, emit); 389 finalized = await finalizeExecutedToolCall(..., preparation, executed, config, signal); 390 } 391 392 await emitToolExecutionEnd(finalized, emit); 393 const toolResultMessage = createToolResultMessage(finalized); 394 await emitToolResultMessage(toolResultMessage, emit); 395 messages.push(toolResultMessage); 396 } 397 398 return { messages, terminate: shouldTerminateToolBatch(finalizedCalls) }; 399} 400 // ... prepareToolCall、executePreparedToolCall、finalizeExecutedToolCall ... // ... 省略 300 行具体实现 ... // 概念已在上面的三步表格中说明

读完这个文件,你应该带走的认知Key takeaways from this file

1. Agent = 一个循环,不是一次问答 └─ 调 LLM → 有工具调用?→ 是 → 执行 → 结果喂回 → 再调 LLM → 否 → 结束 2. 停止条件有三层保护 ├─ 出错/取消 → 立即停 ├─ 外部强制(max turns / token limit) → 安全阀 └─ AI 自然完成(没有更多工具调用) → 正常结束 3. 调 LLM 前有"变换管道" └─ 原始消息 → transformContext(压缩/裁剪)→ convertToLlm(格式转换)→ 发给 LLM 4. 工具执行前有"权限关卡" └─ beforeToolCall → 可以 block → Claude Code 的"Allow?"就是这个 5. 事件系统让外部观察循环状态 └─ turn_start / message_start / tool_execution_start / ... / agent_end
1. Agent = a loop, not a single Q&A └─ Call LLM → Tool calls? → Yes → Execute → Feed results back → Call LLM again → No → End 2. Three layers of stop protection ├─ Error/cancel → stop immediately ├─ External forced (max turns / token limit) → safety valve └─ AI naturally finished (no more tool calls) → normal end 3. "Transform pipeline" before calling the LLM └─ Raw messages → transformContext (compress/trim) → convertToLlm (format) → send to LLM 4. "Permission gate" before tool execution └─ beforeToolCall → can block → Claude Code's "Allow?" is exactly this 5. Event system lets the outside observe loop state └─ turn_start / message_start / tool_execution_start / ... / agent_end

和你日常体验的对应Mapping to your daily experience

你看到 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