跳转至

31. agent-loop.ts:一次消息怎样跑成多轮 Agent

这个模块干什么

agent-loop.ts 是仓库的执行心脏:它把一条或多条 prompt 放入 context,流式请求 LLM,执行 assistant 请求的工具,再把结果喂回下一轮。 【packages/agent/src/agent-loop.ts:95-117】

它不保存全局 Agent 状态,只接收 context、config、abort signal、event sink 与 StreamFn,所以既能被 Agent 使用,也能被 Harness 直接调用。 【packages/agent/src/agent-loop.ts:95-102】 【packages/agent/src/harness/agent-harness.ts:613-622】

循环的难点不在“再请求一次模型”,而在保证流事件、工具结果、队列、停止条件和 session 刷新落在正确的时刻。 【packages/agent/src/agent-loop.ts:169-275】

stream-fn.ts 提供可选默认流,proxy.ts 把后端 SSE 适配回统一事件流,harness/ 则把这一核心接进持久会话和执行环境。 【packages/agent/src/stream-fn.ts:5-20】 【packages/agent/src/proxy.ts:82-99】 【packages/agent/src/harness/agent-harness.ts:442-498】

核心文件速查

文件 一句话职责 必读优先级
src/agent-loop.ts 驱动 LLM 流、工具批次、转向/后续队列与生命周期事件。 【packages/agent/src/agent-loop.ts:155-792】 ⭐⭐⭐
src/types.ts 规定 loop 可调用的 hooks、队列模式、工具和事件形状。 【packages/agent/src/types.ts:144-287】 【packages/agent/src/types.ts:379-437】 ⭐⭐⭐
src/agent.ts 将低层 event sink 变成状态归约加订阅者背压。 【packages/agent/src/agent.ts:398-468】 【packages/agent/src/agent.ts:522-576】 ⭐⭐⭐
src/stream-fn.ts 注册/读取默认 StreamFn,未配置时明确报错。 【packages/agent/src/stream-fn.ts:3-20】 ⭐⭐
src/proxy.ts POST 到 /api/stream,读取 SSE,并在客户端重建 partial assistant message。 【packages/agent/src/proxy.ts:116-232】 ⭐⭐
src/node.ts Node 专用入口:NodeExecutionEnv 加全部根 API。 【packages/agent/src/node.ts:1-2】
src/harness/agent-harness.ts 用 session 快照、provider hooks 与专属事件包装 runAgentLoop。 【packages/agent/src/harness/agent-harness.ts:354-498】 ⭐⭐
src/harness/session/session.ts 将树状 session entries 投影为下一轮的 AgentMessage context。 【packages/agent/src/harness/session/session.ts:59-148】 ⭐⭐
src/harness/messages.ts 注册 Harness 自定义消息,并实现它们到 LLM Message 的投影。 【packages/agent/src/harness/messages.ts:19-61】 【packages/agent/src/harness/messages.ts:120-164】
src/harness/types.ts 定义执行环境、session tree、Harness hooks 与稳定错误码。 【packages/agent/src/harness/types.ts:146-373】 【packages/agent/src/harness/types.ts:553-817】

精读

1. agentLoop() 是事件流外壳,runAgentLoop() 才是真执行

低层公开 API 分为返回 EventStreamagentLoop() / agentLoopContinue(),以及可被宿主 await 的 runAgentLoop() / runAgentLoopContinue()。 【packages/agent/src/agent-loop.ts:31-53】 【packages/agent/src/agent-loop.ts:95-143】

agentLoop() 立即建立 EventStream,后台启动 runAgentLoop(),每个 event 只做 stream.push(event),成功后以返回的新消息数组 stream.end(messages)。 【packages/agent/src/agent-loop.ts:31-53】

因此低层 EventStream 的消费者是观察者;若需要“消息落库后才能开始 tool preflight”的屏障,应通过 Agent 的 awaited event listener 接线。 【packages/agent/src/agent.ts:398-410】 【packages/agent/src/agent.ts:522-576】

agentLoopContinue() 拒绝空 context,也拒绝最后一条是 assistant 的 context。 【packages/agent/src/agent-loop.ts:64-92】

原因是下一次 provider request 的最后一条 LLM 消息必须是 user 或 toolResult;转换器可能改变 role,所以底层只能做 assistant 这一层可检查的防御。 【packages/agent/src/agent-loop.ts:56-63】

runAgentLoop() 先把 prompts 复制到 newMessages,同时创建一个 messages 为“旧 context 加 prompts”的 currentContext。 【packages/agent/src/agent-loop.ts:95-107】

它严格先发 agent_startturn_start,再为每个 prompt 各发一次 message_start / message_end。 【packages/agent/src/agent-loop.ts:109-116】

runAgentLoopContinue() 不创建 prompt 事件,newMessages 也从空数组开始,因此其最终结果只含这次 continuation 新产出的消息。 【packages/agent/src/agent-loop.ts:120-142】

createAgentStream()agent_end 作为终止事件,并把该事件的 messages 当作 stream result。 【packages/agent/src/agent-loop.ts:145-150】

2. 两层 while:内层继续“本次工作”,外层只等 follow-up

runLoop() 维护可替换的 currentContextconfig,并在真正进入循环前先 poll 一次 steering。 【packages/agent/src/agent-loop.ts:155-168】

外层 while (true) 的职责是:内层没有工作时,再检查 follow-up queue;它不是“每轮请求模型”的循环。 【packages/agent/src/agent-loop.ts:169-174】 【packages/agent/src/agent-loop.ts:262-274】

内层条件是 hasMoreToolCalls || pendingMessages.length > 0。 【packages/agent/src/agent-loop.ts:171-174】

hasMoreToolCalls 初始设为 true,使得没有工具的第一次 prompt 也至少请求一次 LLM。 【packages/agent/src/agent-loop.ts:170-174】

从第二次内层迭代开始才发新的 turn_start;初始那一轮已经由 runAgentLoop() 或 continuation 入口发过。 【packages/agent/src/agent-loop.ts:109-116】 【packages/agent/src/agent-loop.ts:175-179】

下面这段是“先把队列中的用户输入变成正式 transcript,再调模型”的顺序:

while (hasMoreToolCalls || pendingMessages.length > 0) {
    if (!firstTurn) {
        await emit({ type: "turn_start" });
    } else {
        firstTurn = false;
    }

    // Process pending messages (inject before next assistant response)
    if (pendingMessages.length > 0) {

来源:packages/agent/src/agent-loop.ts:174-182

pendingMessages 中每条消息都会先发 start/end,再同时 push 入 currentContext.messagesnewMessages。 【packages/agent/src/agent-loop.ts:181-190】

这解释了 steering 不是“打断当前工具”:它只会在上一轮 assistant 与工具已走完后的下一个 provider request 前出现。 【packages/agent/src/agent-loop.ts:224-260】

assistant message 的 error 或 aborted stop reason 会立刻得到 turn_endagent_end 并 return,不会尝试解析工具。 【packages/agent/src/agent-loop.ts:192-200】

正常完成后,loop 从 message.content 里过滤 type === "toolCall"。 【packages/agent/src/agent-loop.ts:202-207】

有工具时,tool result messages 会先补回 context/newMessages,随后才发本轮 turn_end。 【packages/agent/src/agent-loop.ts:205-224】

prepareNextTurn 位于 turn_end 之后,可以替换 context/model/thinking,并影响下一次模型调用。 【packages/agent/src/agent-loop.ts:224-245】

shouldStopAfterTurn 也在此处检查,返回 true 时先发 agent_end,不会再 poll steering 或 follow-up。 【packages/agent/src/agent-loop.ts:247-257】

否则 loop 在每轮末尾 poll steering;若取到消息,内层条件再次成立。 【packages/agent/src/agent-loop.ts:259-260】

只有“无自动工具续轮且无 steering”时才会 poll follow-up;有 follow-up 会回到外层下一次内层处理。 【packages/agent/src/agent-loop.ts:262-271】

3. LLM 边界:转换只影响请求,不覆写完整 transcript

streamAssistantResponse() 是唯一把 AgentMessage[] 变为 pi-ai Context 的地方。 【packages/agent/src/agent-loop.ts:277-302】

它先可选地调用 transformContext(context.messages, signal),再调用 convertToLlm(messages)。 【packages/agent/src/agent-loop.ts:288-295】

transformContext 的返回值放在局部变量 messages,没有赋回 context.messages;所以它是本次请求的视图变换,不自动改写长期 transcript。 【packages/agent/src/agent-loop.ts:288-302】

接着 loop 将原始 system prompt、转换后的 LLM messages、原 context 的 tools 组成 llmContext。 【packages/agent/src/agent-loop.ts:297-302】

动态 getApiKey(provider) 优先于 config 内已有 apiKey,适合在多轮工具执行后刷新短期 token。 【packages/agent/src/agent-loop.ts:304-312】

这一段正好定义了模型边界和 abort signal 的去向:

let messages = context.messages;
if (config.transformContext) {
    messages = await config.transformContext(messages, signal);
}

const llmMessages = await config.convertToLlm(messages);

const llmContext: Context = {
    systemPrompt: context.systemPrompt,
    messages: llmMessages,
    tools: context.tools,
};

来源:packages/agent/src/agent-loop.ts:288-302

stream 的 start event 把部分 assistant message push 进 context,并发 message_start 的浅复制。 【packages/agent/src/agent-loop.ts:317-324】

text、thinking、toolcall 的 start/delta/end event 都更新 context 中最后一条 partial message,并发 message_update。 【packages/agent/src/agent-loop.ts:326-344】

provider 发 doneerror 后,loop 必定调用 response.result() 获得 final assistant message。 【packages/agent/src/agent-loop.ts:346-358】

如果 provider 没发过 start,loop 仍会补 message_start,然后统一发 message_end,因此 event 消费者不必猜测 stream 是否从 start 开始。 【packages/agent/src/agent-loop.ts:346-371】

4. tool-call 解析:先拒绝不安全批次,再逐个 preflight

assistant 的 stop reason 为 "length" 时,所有 tool calls 都被拒绝,不会因某个 JSON 恰好能解析就执行。 【packages/agent/src/agent-loop.ts:207-215】

原因是流式 JSON 的 best-effort 解析可能产出看似合法但已截断的 arguments。 【packages/agent/src/agent-loop.ts:374-380】

拒绝路径仍为每一调用发 tool_execution_starttool_execution_end,再产生一条带明确错误文本的 tool result message。 【packages/agent/src/agent-loop.ts:381-405】

正常路径首先查找同名工具;找不到也不是 throw,而是 immediate error outcome。 【packages/agent/src/agent-loop.ts:600-614】

prepareArguments 时,它先处理原始 arguments,再交给 validateToolArguments 做 schema 校验。 【packages/agent/src/agent-loop.ts:586-598】 【packages/agent/src/agent-loop.ts:616-619】

只有校验之后才调用 beforeToolCall,因此权限层看到的是已验证参数而不是半截 JSON。 【packages/agent/src/agent-loop.ts:616-628】

const tool = currentContext.tools?.find((t) => t.name === toolCall.name);
if (!tool) {
    return {
        kind: "immediate",
        result: createErrorToolResult(`Tool ${toolCall.name} not found`),
        isError: true,
    };
}

try {
    const preparedToolCall = prepareToolCallArguments(tool, toolCall);
    const validatedArgs = validateToolArguments(tool, preparedToolCall);

来源:packages/agent/src/agent-loop.ts:607-619

hook 返回 block 会变成 error result;hook 后或返回 prepared 前发现 signal aborted 也会变成 "Operation aborted" 的 immediate result。 【packages/agent/src/agent-loop.ts:620-663】

参数转换、校验和 hook 抛出的异常同样被 catch 并转换为 error result,而不是撕裂整条 Agent 事件序列。 【packages/agent/src/agent-loop.ts:657-663】

5. 并发工具:preflight 串行,执行并行,持久化顺序稳定

若 config 是 "sequential",或本批任一目标工具声明 executionMode: "sequential",整批都走 sequential。 【packages/agent/src/agent-loop.ts:411-425】

sequential 路径为每条调用依次发 start、preflight、execute/finalize、发 execution end、发 tool-result message。 【packages/agent/src/agent-loop.ts:433-486】

sequential 在当前调用完成后若 signal 已 aborted 就 break,不再开始后续调用。 【packages/agent/src/agent-loop.ts:472-480】

parallel 路径仍逐条发 tool_execution_startprepareToolCall(),所以 permission/preflight 的顺序稳定而且可观察。 【packages/agent/src/agent-loop.ts:489-520】

preflight 为 immediate error 的调用会立即发 tool_execution_end,无需进入 Promise 批次。 【packages/agent/src/agent-loop.ts:507-519】

准备成功的调用被收集成 thunk;随后 Promise.all 同时启动它们。 【packages/agent/src/agent-loop.ts:522-542】

finalizedCalls.push(async () => {
    const executed = await executePreparedToolCall(preparation, signal, emit);
    const finalized = await finalizeExecutedToolCall(
        currentContext,
        assistantMessage,
        preparation,
        executed,
        config,
        signal,
    );
    await emitToolExecutionEnd(finalized, emit);
    return finalized;
});

来源:packages/agent/src/agent-loop.ts:522-534

因此 parallel 的 tool_execution_end 按实际完成时间出现,因为每个 thunk 在自己的结束时 emit。 【packages/agent/src/agent-loop.ts:522-534】

Promise.all 返回结果数组保留输入顺序,后面构造并发出 toolResult message 的循环也按该顺序走。 【packages/agent/src/agent-loop.ts:540-548】

这一区分很关键:UI 可以按完成时间刷新工具,写入对话和下一次模型请求则保持 assistant 原始 tool-call 顺序。 【packages/agent/src/agent-loop.ts:540-553】

每个 execute() 收到同一个 abort signal 和 update callback。 【packages/agent/src/agent-loop.ts:666-693】

工具通过 callback 给出的 partial result 会转换成 tool_execution_update event;所有已接收 update 会在工具终态前 await Promise.all(updateEvents)。 【packages/agent/src/agent-loop.ts:671-705】

(partialResult) => {
    if (!acceptingUpdates) return;
    updateEvents.push(
        Promise.resolve(
            emit({
                type: "tool_execution_update",
                toolCallId: prepared.toolCall.id,
                toolName: prepared.toolCall.name,
                args: prepared.toolCall.arguments,
                partialResult,
            }),
        ),
    );
},

来源:packages/agent/src/agent-loop.ts:679-692

工具 throw 会被 executePreparedToolCall() 变成 error result 和 isError: true。 【packages/agent/src/agent-loop.ts:697-703】

afterToolCall 在执行后、tool_execution_end 之前运行,能以字段替换方式修补最终 result 或其错误状态。 【packages/agent/src/agent-loop.ts:709-754】

一批只有在至少一个结果且每个 finalized result 都有 terminate === true 时才停止自动工具续轮。 【packages/agent/src/agent-loop.ts:582-584】

createToolResultMessage() 会把未类型化工具可能给出的空 content 规整为 [],并记录 tool call ID、工具名、details、usage、错误标记和新时间戳。 【packages/agent/src/agent-loop.ts:773-787】

6. abort 与 steering:一个是 signal,一个是排队消息

AbortSignal 被原样传进 stream function 的 options。 【packages/agent/src/agent-loop.ts:304-312】

同一 signal 也传进 beforeToolCallafterToolCall 和工具 execute();实际停止长任务仍取决于这些实现是否尊重 signal。 【packages/agent/src/agent-loop.ts:620-628】 【packages/agent/src/agent-loop.ts:675-679】 【packages/agent/src/agent-loop.ts:720-732】

模型流最终回报 stopReason === "aborted" 时,loop 走与 error 相同的短路结束路径。 【packages/agent/src/agent-loop.ts:196-200】

steeringgetSteeringMessages() 取得,直到一轮的 tool results 写入、turn_end、可选 next-turn hook 之后才注入。 【packages/agent/src/agent-loop.ts:218-260】

所以不要把 steering 当做“取消”API:它改变下一次 LLM 输入,abort 才是取消当前操作的通道。 【packages/agent/src/agent-loop.ts:259-274】 【packages/agent/src/agent.ts:306-314】

7. stream-fn.ts:默认值是宿主选择,不是 core 的 provider 选择

模块只维护一个私有 defaultStreamFn 变量。 【packages/agent/src/stream-fn.ts:1-3】

宿主调用 setDefaultStreamFn(streamFn) 可设置或清除该变量。 【packages/agent/src/stream-fn.ts:5-13】

getDefaultStreamFn() 在未配置时 throw,错误信息明确要求显式传入或先调用 setter。 【packages/agent/src/stream-fn.ts:15-20】

Agent 的构造函数和 runAgentLoop() 都可在缺少传入 streamFn 时使用这个 fallback。 【packages/agent/src/agent.ts:210-217】 【packages/agent/src/agent-loop.ts:116-117】

这避免 packages/agent 绑定 provider catalog 或兼容层;真正的模型请求仍由 pi-aiStreamFn 实现承担。 【packages/agent/src/stream-fn.ts:5-10】 【packages/agent/src/types.ts:18-32】

8. proxy.ts:SSE 瘦身后如何复原成标准流

ProxyAssistantMessageEvent 刻意去掉每个 delta event 的 partial 字段,以降低服务端到浏览器的带宽。 【packages/agent/src/proxy.ts:33-57】

streamProxy() 先按给定 model 建立一个空的 partial assistant message,再在异步任务中处理网络流。 【packages/agent/src/proxy.ts:116-137】 【packages/agent/src/proxy.ts:119-230】

请求是带 Bearer token 的 POST ${proxyUrl}/api/stream,body 发送 model、context 和经过白名单筛选的 serializable options。 【packages/agent/src/proxy.ts:101-114】 【packages/agent/src/proxy.ts:151-164】

响应读取器把 UTF-8 chunk 累积到 buffer,按换行切分,只处理 data: 开头的 SSE 行。 【packages/agent/src/proxy.ts:179-207】

abort handler 会取消已取得的 reader,fetch 本身也收到同一个 signal。 【packages/agent/src/proxy.ts:139-149】 【packages/agent/src/proxy.ts:151-164】

processProxyEvent() 按 content index 在本地更新 text、thinking 与 tool call block,然后回发带 partial 的统一 AssistantMessageEvent。 【packages/agent/src/proxy.ts:235-365】

工具参数的 delta 会累加临时 partialJson,用 parseStreamingJson 做尽力解析,并用复制 content block 的方式触发响应式观察者。 【packages/agent/src/proxy.ts:320-345】

case "toolcall_delta": {
    const content = partial.content[proxyEvent.contentIndex];
    if (content?.type === "toolCall") {
        (content as any).partialJson += proxyEvent.delta;
        content.arguments = parseStreamingJson((content as any).partialJson) || {};
        partial.content[proxyEvent.contentIndex] = { ...content };
        return {
            type: "toolcall_delta",
            contentIndex: proxyEvent.contentIndex,
            delta: proxyEvent.delta,
            partial,
        };
    }

来源:packages/agent/src/proxy.ts:320-333

done 更新 stop reason 与 usage;error 还写入 errorMessage,二者都成为标准终态 event。 【packages/agent/src/proxy.ts:350-359】

网络或解析异常会被 catch 成 error event:signal 已 aborted 时 reason 是 "aborted",否则是 "error"。 【packages/agent/src/proxy.ts:213-229】

9. node.tsharness/:loop 之外的产品化拼装层

根入口不含 Node 内建实现;node.ts 专门额外导出实现 ExecutionEnvNodeExecutionEnv。 【packages/agent/src/node.ts:1-2】 【packages/agent/src/harness/env/nodejs.ts:344-354】

ExecutionEnvFileSystemShell 的组合,所有文件系统操作都以 Result 形式返回预期失败而不是 reject。 【packages/agent/src/harness/types.ts:282-373】

NodeExecutionEnv.exec() 会解析 shell/cwd、接入 timeout 和 abort,并把 stdout/stderr chunk 回调给调用者。 【packages/agent/src/harness/env/nodejs.ts:364-497】

Harness 的内建工具 barrel 公开 createBashToolcreateEditToolcreateReadToolcreateWriteTool 及各自类型。 【packages/agent/src/harness/tools/index.ts:1-23】

harness/messages.ts 通过声明合并注册 bashExecutioncustombranchSummarycompactionSummary 四类 AgentMessage。 【packages/agent/src/harness/messages.ts:19-61】

convertToLlm() 会把 bash 运行记录、custom 消息和两类摘要投影为 user 文本,同时透传 LLM 原生三类消息。 【packages/agent/src/harness/messages.ts:120-164】

Session 以有 parentId 的 SessionTreeEntry 存储会话;message、model/thinking/tool 状态变更、压缩、分支摘要等都是 entry 类型。 【packages/agent/src/harness/types.ts:375-464】

buildSessionContext() 先导出沿当前 leaf 的状态,再应用 compaction transform,最后把 entries flatMap 为下一次 Agent context 的 messages。 【packages/agent/src/harness/session/session.ts:39-148】

AgentHarness.createTurnState() 每轮从 session 建 context、解析工具 context、快照资源/stream options,并计算 active tools 和 system prompt。 【packages/agent/src/harness/agent-harness.ts:354-387】

它的 loop config 把 Harness context hook 映射为 transformContext,把 tool_call / tool_result hook 映射为 core 的 before/after tool hooks。 【packages/agent/src/harness/agent-harness.ts:442-484】

它还在 prepareNextTurn 刷新待写 session entries 与 turn state,所以每个后续 provider request 都可看到更新后的会话与模型配置。 【packages/agent/src/harness/agent-harness.ts:485-497】

Harness 处理 core message_end 时 append session message,处理 turn_end 时 flush 并发 save point,处理 agent_end 时转 idle 后发 settled。 【packages/agent/src/harness/agent-harness.ts:538-565】

AgentHarness.executeTurn() 直接调用 runAgentLoop(),不是先构造 Agent,所以它拥有另一套 session-aware 生命周期接线。 【packages/agent/src/harness/agent-harness.ts:581-655】

压缩逻辑用最新 provider usage 加尾部估算 tokens,并在超过 contextWindow - reserveTokens 时建议压缩。 【packages/agent/src/harness/compaction/compaction.ts:231-266】

Skills loader 递归找 SKILL.md、尊重 ignore 文件、返回诊断而非让单个坏 skill 中断加载。 【packages/agent/src/harness/skills.ts:43-75】 【packages/agent/src/harness/skills.ts:103-174】

Prompt-template loader 则只从给定目录读取直接 .md 子项,或接受明确指定的 .md 文件。 【packages/agent/src/harness/prompt-templates.ts:24-61】

formatSkillsForSystemPrompt() 过滤 disableModelInvocation 的 skill,并以 XML 风格的 <available_skills> 块写进 system prompt。 【packages/agent/src/harness/system-prompt.ts:3-25】

数据流

下面追踪“用户让模型读取两个文件后解释差异”的完整一趟:

Agent.prompt("比较 a 与 b")
  -> normalizePromptInput()
  -> runPromptMessages()
  -> runAgentLoop(prompts, createContextSnapshot(), createLoopConfig(), processEvents, signal, streamFn)
  -> runLoop()
       -> streamAssistantResponse()
            -> transformContext?() -> convertToLlm()
            -> streamFunction(model, llmContext, { signal, ... })
            -> message_start / message_update* / message_end(assistant with toolCall×2)
       -> executeToolCalls()
            -> prepareToolCall() ×2
            -> executeToolCallsParallel()
            -> tool_execution_start ×2
            -> executePreparedToolCall() ×2
            -> tool_execution_update* / tool_execution_end ×2
            -> message_start/end(toolResult a, then toolResult b)
       -> turn_end
       -> streamAssistantResponse()  // 带两个 toolResult 的下一轮
            -> message_end(final assistant)
       -> turn_end -> agent_end
  -> Agent.processEvents()  // 每个 event 先更新 state,再 await listeners

prompt() 的字符串标准化和 runPromptMessages()runAgentLoop() 的接线在 Agent 中完成。 【packages/agent/src/agent.ts:336-347】 【packages/agent/src/agent.ts:379-412】

初始 prompt 事件和第一次进入 runLoop() 的调用顺序由 runAgentLoop() 固定。 【packages/agent/src/agent-loop.ts:95-117】

模型边界是 streamAssistantResponse() 的 transform、convert、streamFunction 三步。 【packages/agent/src/agent-loop.ts:281-312】

assistant 流中的 partial 与 final message 分别触发 update 和 end,因此 UI 能边收边画,session 可以只在 end 时持久化。 【packages/agent/src/agent-loop.ts:317-371】 【packages/agent/src/harness/agent-harness.ts:538-543】

工具批次由 executeToolCalls() 按全局/每工具 execution mode 分派;两个普通可并发工具走 parallel 路径。 【packages/agent/src/agent-loop.ts:411-425】

parallel 的 end event 可以按完成时间交错,但最终两条 toolResult messages 仍按 assistant 的 tool-call 位置进入下一轮 context。 【packages/agent/src/agent-loop.ts:522-553】

没有新的工具调用、steering 或 follow-up 后,loop 发 agent_endAgent 则在 listener 结算完才完成 prompt Promise。 【packages/agent/src/agent-loop.ts:259-275】 【packages/agent/src/agent.ts:471-493】

自测

  1. 为什么外层 while 只处理 follow-up,而第一次 LLM 调用由内层的初始 hasMoreToolCalls = true 触发? 【packages/agent/src/agent-loop.ts:169-174】 【packages/agent/src/agent-loop.ts:262-274】

  2. transformContext() 的结果为什么不会自动成为 context.messages 的新值? 【packages/agent/src/agent-loop.ts:288-302】

  3. parallel 模式下,为什么 tool_execution_end 和 tool result transcript 的排序可以不同? 【packages/agent/src/agent-loop.ts:522-553】

  4. 一条带 stopReason: "length" 的 assistant message 为什么不能执行其中任何 tool call? 【packages/agent/src/agent-loop.ts:207-215】 【packages/agent/src/agent-loop.ts:374-405】

  5. Harness 用哪三个 core event 完成消息持久化、save point 和 settled 通知? 【packages/agent/src/harness/agent-harness.ts:538-565】