# 逐行精读:`agent-loop.ts` —— 核心循环(Pi 的心脏) > 源文件:`pi-main/packages/agent/src/agent-loop.ts`(792 行) > > 前置阅读:[01-types-and-protocol.md](./01-types-and-protocol.md)(types.ts 逐行精读)。 > 循环只是在操作那些类型——不懂类型就读不懂循环。 ## 文件结构总览 ``` agent-loop.ts (792 行) ├── 公共入口(L1-151) │ ├── agentLoop() —— 带 prompt 启动 │ ├── agentLoopContinue() —— 不加 prompt 续跑(用于重试) │ └── createAgentStream() —— EventStream 工厂 ├── 核心循环 runLoop()(L155-275)⭐⭐⭐ │ ├── 外层 while:follow-up 循环 │ └── 内层 while:steering + 工具循环 ├── streamAssistantResponse()(L281-374)⭐⭐ 边界转换 ├── 工具执行(L383-755) │ ├── failToolCallsFromTruncatedMessage() —— 截断时全部失败 │ ├── executeToolCalls() —— 模式选择 │ ├── executeToolCallsSequential() —— 顺序执行 │ ├── executeToolCallsParallel() —— 并发执行 │ ├── prepareToolCall() —— 验证 + before hook │ ├── executePreparedToolCall() —— 执行 + 流式更新 │ └── finalizeExecutedToolCall() —— after hook └── 辅助函数(L757-792) ├── createErrorToolResult() ├── emitToolExecutionEnd() ├── createToolResultMessage() └── emitToolResultMessage() ``` --- ## 一、公共入口(L1-151) ### `agentLoop` —— 带 prompt 启动(L34-118) ```ts // L34-101(签名) export function agentLoop( prompts: AgentMessage[], context: AgentContext, config: AgentLoopConfig, signal?: AbortSignal, streamFn?: StreamFn, ): EventStream { const stream = createAgentStream(); void runAgentLoop(prompts, context, config, async (event) => stream.push(event), signal, streamFn) .then((messages) => stream.end(messages)); return stream; } ``` **关键模式:立即返回 EventStream,后台异步推事件**。`void runAgentLoop(...).then(...)` 启动一个不等待的 promise,事件通过 `stream.push` 推给消费者。这让调用方能边迭代事件边让循环跑——**生产者/消费者解耦**。 ```ts // L103-117(runAgentLoop 主体) async function runAgentLoop(...): Promise { const newMessages: AgentMessage[] = [...prompts]; // 本次 run 产生的所有新消息 const currentContext: AgentContext = { ...context, messages: [...context.messages, ...prompts], // 上下文 = 原有 + 新 prompt }; await emit({ type: "agent_start" }); // L109 生命周期开始 await emit({ type: "turn_start" }); // L110 for (const prompt of prompts) { // L111-114 为每个 prompt 发 message 事件 await emit({ type: "message_start", message: prompt }); await emit({ type: "message_end", message: prompt }); } await runLoop(currentContext, newMessages, config, signal, emit, streamFn); // L116 ← 核心循环 return newMessages; // L117 返回本次产生的新消息 } ``` **`newMessages` 是只追加的列表**——本次 run 产生的所有新消息(prompt + assistant + toolResult + steering + follow-up)。它和 `currentContext.messages`(完整 transcript)是两个不同的东西。 ### `agentLoopContinue` —— 续跑(L120-143) ```ts // L127-133 两个前置校验 if (context.messages.length === 0) throw new Error("Cannot continue: no messages in context"); if (context.messages[last].role === "assistant") throw new Error("Cannot continue from assistant"); ``` **为什么最后一条不能是 assistant?** 因为继续循环意味着要调 LLM,而 LLM 调用要求最后一条是 user 或 toolResult(Anthropic/OpenAI 的协议约束)。最后一条是 assistant 时"继续"无意义——你应该用 `prompt` 加新消息。 ### `createAgentStream`(L145-150) ```ts function createAgentStream(): EventStream { return new EventStream( (event) => event.type === "agent_end", // 完成检测器 (event) => event.type === "agent_end" ? event.messages : [], // 结果提取器 ); } ``` `EventStream` 的两个回调:① 判断哪个事件是"完成"事件;② 从完成事件提取最终结果 `R`。这里完成事件是 `agent_end`,结果是它的 `messages`。 --- ## 二、核心循环 `runLoop`(L155-275)⭐⭐⭐ 这是整个文件最重要的一段。**双层循环 + 四个停止条件**。 ### 函数签名与初始状态(L155-167) ```ts async function runLoop( initialContext: AgentContext, newMessages: AgentMessage[], initialConfig: AgentLoopConfig, signal: AbortSignal | undefined, emit: AgentEventSink, streamFn?: StreamFn, ): Promise { let currentContext = initialContext; // 可变:prepareNextTurn 会替换 let config = initialConfig; // 可变:prepareNextTurn 会替换 model/reasoning let firstTurn = true; // L167 启动时就检查 steering(用户可能在等待时已输入) let pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || []; ``` **`currentContext` 和 `config` 都是 `let`**——`prepareNextTurn` 可以整体替换它们。这是"每轮重建上下文"的实现基础。 **L167 的 `getSteeringMessages` 在循环开始就调用**——用户可能在 `agentLoop` 返回 EventStream 后、循环真正开始前就输入了 steering 消息。这是为了不丢消息。 ### 双层循环骨架(L170-272) ```ts // L170 外层 while:follow-up 循环 while (true) { let hasMoreToolCalls = true; // L174 内层 while:steering + 工具循环 while (hasMoreToolCalls || pendingMessages.length > 0) { ``` **为什么是双层?** - **内层**:处理"一个连贯的工作流"——有工具调用就继续,有 steering 就注入。 - **外层**:当内层要退出(没工具调用也没 steering 了),再检查 follow-up——"任务做完了,还有别的吗?"有就重启内层。 这种分层让 steering(紧急插话)和 follow-up(后续任务)的语义清晰分离。 ### 内层循环体(L175-260) #### ① Turn 标记与消息注入(L175-190) ```ts if (!firstTurn) { await emit({ type: "turn_start" }); // L176 非首轮才发 turn_start } else { firstTurn = false; // 首轮的 turn_start 已在 runAgentLoop 发过 } // L182-190 注入 pendingMessages(steering 或 follow-up) if (pendingMessages.length > 0) { for (const message of pendingMessages) { await emit({ type: "message_start", message }); await emit({ type: "message_end", message }); currentContext.messages.push(message); // 加入 transcript newMessages.push(message); // 加入本次 run 的新消息 } pendingMessages = []; // 清空 } ``` **steering/follow-up 消息在调 LLM **之前**注入**——模型会在下一轮看到它们。 #### ② 流式接收 assistant 响应(L193-200) ```ts const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn); newMessages.push(message); // L196-200 错误/中止:直接结束 if (message.stopReason === "error" || message.stopReason === "aborted") { await emit({ type: "turn_end", message, toolResults: [] }); await emit({ type: "agent_end", messages: newMessages }); return; } ``` **这是停止条件①**:provider 返回 error/aborted。注意仍然发 `turn_end` 和 `agent_end`——保证事件序列完整。 #### ③ 提取并执行工具调用(L202-222) ```ts const toolCalls = message.content.filter((c) => c.type === "toolCall"); const toolResults: ToolResultMessage[] = []; hasMoreToolCalls = false; // L206 默认假设不继续 if (toolCalls.length > 0) { // L208-214 关键分支:截断时全部失败 const executedToolBatch = message.stopReason === "length" ? await failToolCallsFromTruncatedMessage(toolCalls, emit) : await executeToolCalls(currentContext, message, config, signal, emit); toolResults.push(...executedToolBatch.messages); hasMoreToolCalls = !executedToolBatch.terminate; // L216 terminate 决定是否继续 for (const result of toolResults) { currentContext.messages.push(result); // 工具结果入 transcript newMessages.push(result); } } ``` **`length` 停止原因的特殊处理**(L208-214)——这是停止条件的一个微妙点: 为什么截断时要全部失败?因为流式 tool call 的参数是用**增量 JSON 解析**(`parseStreamingJson`)拼出来的。如果输出被 token 限制截断,拼出来的 JSON 可能恰好能解析、能通过 schema 验证,但**内容是残缺的**——执行它会有不可预测的后果。所以宁可全部报错让模型重新发。 `hasMoreToolCalls = !executedToolBatch.terminate`(L216)——**只有批内所有结果都 `terminate: true` 才停止**(见 `shouldTerminateToolBatch`,L584-586)。 #### ④ prepareNextTurn + shouldStopAfterTurn(L224-259) ```ts await emit({ type: "turn_end", message, toolResults }); // L226-245 prepareNextTurn:每轮后重建上下文 const nextTurnContext = { message, toolResults, context: currentContext, newMessages }; const nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext); if (nextTurnSnapshot) { currentContext = nextTurnSnapshot.context ?? currentContext; // 替换上下文 config = { ...config, model: nextTurnSnapshot.model ?? config.model, // 替换模型 reasoning: nextTurnSnapshot.thinkingLevel === undefined // 替换思考级别 ? config.reasoning : nextTurnSnapshot.thinkingLevel === "off" ? undefined : nextTurnSnapshot.thinkingLevel, }; } // L247-257 停止条件②:shouldStopAfterTurn if (await config.shouldStopAfterTurn?.({ message, toolResults, context: currentContext, newMessages })) { await emit({ type: "agent_end", messages: newMessages }); return; } pendingMessages = (await config.getSteeringMessages?.()) || []; // L259 再 poll steering } // 内层 while 结束 ``` **`prepareNextTurn` 是 harness 层动态上下文的钩子**(L226-245)。harness 在这里:从 session 重载消息、重算系统提示(可含 cwd/git status)、重建工具列表。模型切换、上下文压缩、工具动态加载都通过这个生效。 **L247-257 是停止条件②**:`shouldStopAfterTurn` 返回 true。注意它**在 steering poll 之前**——如果要求停止,就不 poll steering/follow-up 了(L255 直接 return)。用途:上下文快满了提前停。 **L259 再次 poll steering**——内层循环每次 turn 结束都检查。这就是 steering 的"紧急插话"语义。 ### 外层循环:follow-up(L262-271) ```ts // 内层退出,agent 本来要停了 const followUpMessages = (await config.getFollowUpMessages?.()) || []; // L263 if (followUpMessages.length > 0) { pendingMessages = followUpMessages; // L266 设为 pending,内层会注入 continue; // L267 重启内层 } break; // L271 真的没东西了,退出 } await emit({ type: "agent_end", messages: newMessages }); // L274 最终结束 } ``` **follow-up 只在内层"本来要退出"时才检查**——这是它和 steering 的本质区别。steering 是打断(内层每次都 poll),follow-up 是续接(内层要停才问)。 ### 停止条件总结 | # | 条件 | 触发位置 | |---|---|---| | ① | provider 返回 `error`/`aborted` | L196 | | ② | `shouldStopAfterTurn` 返回 true | L247 | | ③ | 没工具调用、没 steering、没 follow-up(自然结束) | 内层退出 + 外层 break | | ④ | 工具批全部 `terminate: true`(通过 `hasMoreToolCalls=false`) | L216 + 内层退出 | **截断(length)不是停止条件**——它只是让工具全部失败,循环继续(模型会看到错误,重发工具调用)。 --- ## 三、`streamAssistantResponse` —— 边界转换(L281-374)⭐⭐ **这是 AgentMessage↔Message 唯一的翻译点**,也是循环和 provider 的接口。 ### 三步转换(L288-314) ```ts async function streamAssistantResponse(...): Promise { // 第①步:可选的上下文变换(AgentMessage → AgentMessage) let messages = context.messages; if (config.transformContext) { messages = await config.transformContext(messages, signal); // 剪枝/注入 } // 第②步:转换成 LLM 消息(AgentMessage → Message[]) const llmMessages = await config.convertToLlm(messages); // 第③步:构建 LLM 上下文 const llmContext: Context = { systemPrompt: context.systemPrompt, messages: llmMessages, tools: context.tools, }; const streamFunction = streamFn || streamSimple; // L304 可注入自定义 streamFn // 动态解析 API key(处理过期 token) const resolvedApiKey = (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey; // 调 provider const response = await streamFunction(config.model, llmContext, { ...config, apiKey: resolvedApiKey, signal }); ``` **为什么 `transformContext` 在 `convertToLlm` 之前?** 因为 `transformContext` 操作 `AgentMessage[]`(应用级,可剪枝自定义消息、注入外部上下文),而 `convertToLlm` 是格式转换(AgentMessage → Message)。顺序是"先处理应用语义,再转换协议格式"。 ### 流式累加(L316-373)—— 流式消息是"活"的 ```ts let partialMessage: AssistantMessage | null = null; let addedPartial = false; // 标记是否已 push 到 context.messages for await (const event of response) { switch (event.type) { case "start": // L321-326 partialMessage = event.partial; context.messages.push(partialMessage); // ★ 立即入栈 addedPartial = true; await emit({ type: "message_start", message: { ...partialMessage } }); break; case "text_start": case "text_delta": case "text_end": case "thinking_start": case "thinking_delta": case "thinking_end": case "toolcall_start": case "toolcall_delta": case "toolcall_end": // L337-346 所有 delta 事件统一处理 if (partialMessage) { partialMessage = event.partial; context.messages[context.messages.length - 1] = partialMessage; // ★ 原地替换最后一个 await emit({ type: "message_update", assistantMessageEvent: event, message: { ...partialMessage } }); } break; case "done": case "error": { // L348-361 const finalMessage = await response.result(); if (addedPartial) { context.messages[last] = finalMessage; // 替换为最终版 } else { context.messages.push(finalMessage); // 没 push 过就 push } if (!addedPartial) { await emit({ type: "message_start", message: { ...finalMessage } }); // 补发 message_start } await emit({ type: "message_end", message: finalMessage }); return finalMessage; } } } // L365-373 流自然结束(没 done/error 事件)的兜底 const finalMessage = await response.result(); if (addedPartial) context.messages[last] = finalMessage; else { context.messages.push(finalMessage); await emit({ type: "message_start", message: { ...finalMessage } }); } await emit({ type: "message_end", message: finalMessage }); return finalMessage; } ``` **核心设计:流式消息在流式过程中就躺在 transcript 末尾**。 - `start` 事件 → push 空的 partial 消息 - 每个 delta 事件 → 用新 partial **原地替换**最后一个元素 - `done`/`error` → 用最终消息替换 这让 UI 能实时看到流式输出(通过 `message_update` 事件),而 transcript 始终保持"当前最新状态"。 **`addedPartial` 标志**(L317)处理一个边界情况:如果 provider 没发 `start` 事件直接发 `done`(某些 provider 的行为),那 transcript 里还没有 partial,需要补发 `message_start`。 **L365-373 的兜底**:如果流自然耗尽(for-await 结束)但没收到 `done`/`error`,用 `response.result()` 取最终消息——保证总有最终消息入栈。这是健壮性设计。 **为什么 emit 时用 `{ ...partialMessage }` 浅拷贝?**(L325, L343)因为 partial 后续会被替换,emit 出去的应该是那一刻的快照,避免消费者持有的是会被原地修改的引用。 --- ## 四、工具执行(L383-755) ### 截断失败(L383-408) ```ts async function failToolCallsFromTruncatedMessage(toolCalls, emit): Promise { const messages: ToolResultMessage[] = []; for (const toolCall of toolCalls) { await emit({ type: "tool_execution_start", toolCallId: toolCall.id, toolName: toolCall.name, args: toolCall.arguments }); const finalized: FinalizedToolCallOutcome = { toolCall, result: createErrorToolResult(`...arguments may be truncated. Re-issue the tool call...`), isError: true, }; await emitToolExecutionEnd(finalized, emit); const toolResultMessage = createToolResultMessage(finalized); await emitToolResultMessage(toolResultMessage, emit); messages.push(toolResultMessage); } return { messages, terminate: false }; // 不终止——让模型看到错误重发 } ``` **模拟完整的工具执行事件序列**(start → end → message),但结果永远是错误。`terminate: false` 让循环继续——模型会看到"参数可能截断"的错误,重新发工具调用。 ### 模式选择(L413-428) ```ts async function executeToolCalls(...): Promise { const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall"); // 只要有一个工具声明了 sequential,整个批次就退化为 sequential const hasSequentialToolCall = toolCalls.some((tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === "sequential"); if (config.toolExecution === "sequential" || hasSequentialToolCall) { return executeToolCallsSequential(...); } return executeToolCallsParallel(...); } ``` **保守策略**:批内有一个 sequential 工具就全部顺序执行。因为 sequential 通常意味着有副作用(如写文件),并发可能冲突。 ### 顺序执行(L435-489) ```ts async function executeToolCallsSequential(...): Promise { const finalizedCalls: FinalizedToolCallOutcome[] = []; const messages: ToolResultMessage[] = []; for (const toolCall of toolCalls) { // 一个一个来 await emit({ type: "tool_execution_start", ... }); const preparation = await prepareToolCall(...); // 验证 + before hook let finalized: FinalizedToolCallOutcome; if (preparation.kind === "immediate") { // 验证失败/blocked/未找到 → 直接出结果 finalized = { toolCall, result: preparation.result, isError: preparation.isError }; } else { const executed = await executePreparedToolCall(...); // 执行 finalized = await finalizeExecutedToolCall(...); // after hook } await emitToolExecutionEnd(finalized, emit); // 立即发 end const toolResultMessage = createToolResultMessage(finalized); await emitToolResultMessage(toolResultMessage, emit); // 立即发 toolResult message finalizedCalls.push(finalized); messages.push(toolResultMessage); if (signal?.aborted) break; // 中止则跳出 } return { messages, terminate: shouldTerminateToolBatch(finalizedCalls) }; } ``` **顺序模式的特点:start→execute→end→message 一个工具完整走完才下一个**。中止会 break,已完成的工具结果保留。 ### 并发执行(L491-556)—— 精妙之处 ```ts async function executeToolCallsParallel(...): Promise { const finalizedCalls: FinalizedToolCallEntry[] = []; // 注意类型是 Entry(可能是函数) // 第一阶段:顺序 prepare(含 before hook) for (const toolCall of toolCalls) { await emit({ type: "tool_execution_start", ... }); const preparation = await prepareToolCall(...); if (preparation.kind === "immediate") { // immediate 直接 finalize + 发 end const finalized = { toolCall, result: preparation.result, isError: preparation.isError }; await emitToolExecutionEnd(finalized, emit); finalizedCalls.push(finalized); // 直接存结果 if (signal?.aborted) break; continue; } // prepared → 存一个 thunk(延迟执行) finalizedCalls.push(async () => { const executed = await executePreparedToolCall(preparation, signal, emit); const finalized = await finalizeExecutedToolCall(...); await emitToolExecutionEnd(finalized, emit); // end 按完成顺序发 return finalized; }); if (signal?.aborted) break; } // 第二阶段:并发执行所有 thunk const orderedFinalizedCalls = await Promise.all( finalizedCalls.map((entry) => (typeof entry === "function" ? entry() : Promise.resolve(entry))), ); // 第三阶段:toolResult message 按 assistant 源顺序发(不是完成顺序!) const messages: ToolResultMessage[] = []; for (const finalized of orderedFinalizedCalls) { const toolResultMessage = createToolResultMessage(finalized); await emitToolResultMessage(toolResultMessage, emit); messages.push(toolResultMessage); } return { messages, terminate: shouldTerminateToolBatch(orderedFinalizedCalls) }; } ``` **并发模式的三个阶段**: 1. **顺序 prepare**:所有工具的 `prepareToolCall`(含 before hook)顺序跑。为什么顺序?因为 before hook 可能 block,且 prepare 可能修改共享状态。 2. **并发 execute**:prepared 的工具存成 thunk,`Promise.all` 并发执行。`tool_execution_end` 按**完成顺序**发(每个 thunk 完成时立即发)。 3. **顺序 emit message**:`toolResult` 的 `message_start/end` 按**源顺序**发(`orderedFinalizedCalls` 保持原顺序)。 **`FinalizedToolCallEntry` 是联合类型**(L582): ```ts type FinalizedToolCallEntry = FinalizedToolCallOutcome | (() => Promise); ``` immediate 的存结果对象,prepared 的存 thunk。第二阶段用 `typeof entry === "function"` 区分。 **为什么 end 按完成顺序、message 按源顺序?** 因为 `tool_execution_end` 是给 UI 看的(哪个工具先做完先显示),而 toolResult message 是给**下一轮 LLM**看的(要和 assistant 的 toolCall 顺序对应,否则模型会困惑)。 ### `prepareToolCall` —— 验证 + before hook(L602-666) ```ts async function prepareToolCall(...): Promise { const tool = currentContext.tools?.find((t) => t.name === toolCall.name); if (!tool) { // L610 工具未找到 return { kind: "immediate", result: createErrorToolResult(`Tool ${toolCall.name} not found`), isError: true }; } try { const preparedToolCall = prepareToolCallArguments(tool, toolCall); // L619 prepareArguments shim const validatedArgs = validateToolArguments(tool, preparedToolCall); // L620 schema 验证 if (config.beforeToolCall) { // L621 before hook const beforeResult = await config.beforeToolCall( { assistantMessage, toolCall, args: validatedArgs, context: currentContext }, signal); if (signal?.aborted) return { kind: "immediate", result: createErrorToolResult("Operation aborted"), isError: true }; if (beforeResult?.block) { // L638 block → 错误结果 return { kind: "immediate", result: createErrorToolResult(beforeResult.reason || "Tool execution was blocked"), isError: true }; } } if (signal?.aborted) return { kind: "immediate", result: createErrorToolResult("Operation aborted"), isError: true }; return { kind: "prepared", toolCall, tool, args: validatedArgs }; // L653 通过 → 执行 } catch (error) { // L659 验证抛错 → 错误结果 return { kind: "immediate", result: createErrorToolResult(error.message), isError: true }; } } ``` **三道关卡**:① 工具存在性 ② `prepareArguments`(LLM 输出规整)+ `validateToolArguments`(schema 验证)③ `beforeToolCall` hook(权限/安全)。 **返回 `immediate` 的情况**:工具未找到、参数验证失败、before hook block、已中止。这些都跳过执行,直接产生错误结果——**错误被编码成结果,而非抛出**。 ### `executePreparedToolCall` —— 执行 + 流式更新(L668-709) ```ts async function executePreparedToolCall(prepared, signal, emit): Promise { const updateEvents: Promise[] = []; let acceptingUpdates = true; // 更新开关 try { const result = await prepared.tool.execute( prepared.toolCall.id, prepared.args as never, signal, (partialResult) => { // onUpdate 回调 if (!acceptingUpdates) return; // settle 后的调用忽略 updateEvents.push(Promise.resolve(emit({ type: "tool_execution_update", ... partialResult }))); }, ); acceptingUpdates = false; // 执行完成,关闭更新 await Promise.all(updateEvents); // 等待所有 update 事件 flush return { result, isError: false }; } catch (error) { acceptingUpdates = false; await Promise.all(updateEvents); // 错误时也 flush 更新 return { result: createErrorToolResult(error.message), isError: true }; } finally { acceptingUpdates = false; // 确保 finally 也关闭 } } ``` **`onUpdate` 回调的作用域**:只在 `execute()` promise 未 settle 时有效。`acceptingUpdates` 三处置 false(成功/失败/finally),保证 settle 后的调用被忽略。 **`updateEvents` 收集机制**:update 回调里不直接 await emit(回调是同步的),而是把 emit 的 promise 推入数组,execute 完成后统一 `Promise.all` flush。这避免了回调内的异步竞态。 **错误处理**(L699-705):工具 execute 抛错 → `createErrorToolResult` → `isError: true`。**错误编码成结果,绝不抛出**。 ### `finalizeExecutedToolCall` —— after hook(L711-755) ```ts async function finalizeExecutedToolCall(...): Promise { let result = executed.result; let isError = executed.isError; if (config.afterToolCall) { try { const afterResult = await config.afterToolCall( { assistantMessage, toolCall: prepared.toolCall, args: prepared.args, result, isError, context: currentContext }, signal); if (afterResult) { // 字段级覆盖,无深度合并 result = { ...result, content: afterResult.content ?? result.content, // 给了就替换 details: afterResult.details ?? result.details, terminate: afterResult.terminate ?? result.terminate, }; isError = afterResult.isError ?? isError; } } catch (error) { // after hook 抛错 → 错误结果 result = createErrorToolResult(error.message); isError = true; } } return { toolCall: prepared.toolCall, result, isError }; } ``` **after hook 的字段级覆盖**(L736-742):`content`/`details`/`terminate` 用 `??`(给了就替换,没给保持原值)。`isError` 同理。**无深度合并**——这是有意的,避免意外的部分覆盖导致状态不一致。 用途:脱敏(改写 content)、强制标记错误(`isError: true`)、审计日志(读取 details)。 --- ## 五、辅助函数(L757-792) ```ts // L757-762 错误结果工厂 function createErrorToolResult(message: string): AgentToolResult { return { content: [{ type: "text", text: message }], details: {} }; } // L774-787 构造 ToolResultMessage function createToolResultMessage(finalized: FinalizedToolCallOutcome): ToolResultMessage { return { role: "toolResult", toolCallId: finalized.toolCall.id, toolName: finalized.toolCall.name, content: finalized.result.content ?? [], // L781 null 归一化(JS 扩展工具可能无 content) details: finalized.result.details, ...(finalized.result.addedToolNames?.length ? { addedToolNames: finalized.result.addedToolNames } : {}), isError: finalized.isError, timestamp: Date.now(), }; } ``` **L778-781 的 null 归一化**:注释说"untyped tools (JS extensions) can return results without content"。因为 JS 扩展工具不走 Rust 的强类型,可能返回 `content: null`。这里 `?? []` 保证 null 永不进入 session 历史或 provider payload——防御性编程。 **`addedToolNames` 条件展开**(L783):只在非空时加入字段,避免序列化出空的 `addedToolNames: []`。 --- ## 六、设计模式总结 读完 792 行,提炼出 agent-loop.ts 的核心设计模式: ### 1. 错误编码而非抛出(贯穿全文) - `StreamFn` 契约:不 throw,错误进 stream - `prepareToolCall`:验证失败返回 `immediate` 错误结果 - `executePreparedToolCall`:工具抛错 → `createErrorToolResult` - `finalizeExecutedToolCall`:after hook 抛错 → 错误结果 **所有错误都变成 `ToolResultMessage(isError: true)`**,循环永远不会因为工具错误而中断事件序列。 ### 2. 生产者-消费者解耦(L34-49) `agentLoop` 立即返回 EventStream,后台 `void` promise 推事件。消费者 `for await` 拉事件,生产者跑循环。 ### 3. 流式消息"活性"(L321-361) partial 消息在流式过程中就躺在 transcript 末尾,delta 原地替换。UI 实时可见,transcript 始终最新。 ### 4. 双层循环分离语义(L170-272) - 内层:steering(打断)+ 工具调用 - 外层:follow-up(续接) 两种"中途加消息"的语义清晰区分。 ### 5. 边界唯一转换(L288-295) `convertToLlm` 是 AgentMessage→Message 的唯一桥梁。`transformContext`(AgentMessage→AgentMessage)在它之前。 ### 6. 顺序 vs 并发的精巧设计(L435-556) - sequential:完整流水线一个一个来 - parallel:prepare 顺序、execute 并发、message 按源序 ### 7. 字段级覆盖无深度合并(L736-742) after hook 的覆盖是"给了就整体替换,没给就保持",避免部分覆盖导致状态不一致。 ### 8. 防御性归一化(L781) 外部来源(JS 扩展)的 `content: null` 被 `?? []` 归一化,保证内部不变量。 --- ## 七、纯 std 实现启示 读完循环逻辑,对 focus 实现的关键启示: 1. **循环本身是纯的状态机**——`runLoop` 只是 `while` + 条件判断 + 函数调用,零 I/O。纯 std 完全可实现。 2. **`StreamFn` 可以是 trait**: ```rust trait StreamFn: Send { fn stream(&self, model: &Model, ctx: &Context, opts: &StreamOptions) -> Pin + Send>>; } ``` 或者更简单——用同步迭代器/channel。核心循环不关心 stream 怎么来。 3. **EventStream 可用 `mpsc::Receiver` 替代**: ```rust let (tx, rx) = mpsc::channel(); // 后台线程:循环跑,tx.send(event) // 主线程:rx.iter() 消费 ``` Rust 的 std::sync::mpsc 完全够用。或者用 crossbeam(但那不是 std)。 4. **并发工具执行可用 `std::thread` + `JoinHandle`**:顺序 prepare,spawn 线程并发 execute,join 收集结果。 5. **AgentMessage→Message 转换是纯函数**——不依赖任何外部库。 **结论**:agent-loop.ts 的逻辑可以几乎 1:1 移植到纯 std Rust,唯一的妥协是 async(可以用同步阻塞 + 线程替代)和 JSON(手写极简解析器或认栽用 serde)。 --- 至此,Pi 的核心两层(类型 + 循环)已完整解读。配合 [00-pi-architecture-overview.md](./00-pi-architecture-overview.md) 的全景,你已具备从零实现一个 agent 框架所需的全部知识。