跳到主要内容

主循环与回合模型(agent-core)

30 秒导读: @oh-my-pi/pi-agent-core 是整个项目的心跳。它做一件事并把它做到极致: 反复地 向模型要一条回复、把回复里的工具调用跑掉、把结果塞回对话、再要下一条——直到 模型不再要工具、也没有排队的新消息为止。本章讲清这个循环的一圈("回合")长什么样、工具 怎么并发和被打断、中止(abort)时怎么收尾,以及一个贯穿全场的关键设计:内部用 AgentMessage, 只在真正调模型那一行才转成 provider 的线格式 Message

本章不讲:具体工具怎么实现(见 工具宇宙)、provider 流式与方言细节 (见 模型接入层)、上下文压缩 compaction(见 长程上下文治理)。


1. 这是什么(零基础也能懂)

  • 一句话定义: agent-core 是一个 异步生成器循环——喂进用户的一句话,吐出一串生命周期 事件(模型在说话、要调工具、工具跑完了、这一回合结束了……),循环体自己决定要不要再转一圈。

  • 它解决什么问题: 一个"会用工具的 AI"不是调一次模型就完事的。模型说"我要读这个文件", 你得真去读、把内容给它、它才能接着说"好,现在我改第 12 行"。谁来编排这个来回? 就是 agent-core。它是模型(嘴)和工具(手脚)之间那台不知疲倦的传送带。

  • 它能做什么:

    • 流式地把模型输出解析成结构化的 AssistantMessage(文本 / 思考 / 工具调用三种块)。
    • 把一条消息里的多个工具调用按并发规则调度执行,收集结果。
    • 把工具结果配对喂回,自动进入下一回合。
    • 中途插话(steering)、回合后追问(follow-up)、旁路通知(aside)的注入。
    • 干净地处理中止、超时、provider 报错、以及一类特定的协议泄漏(Harmony leak)。
  • 用起来什么样: 宿主几乎不直接碰循环,而是通过 Agent 类。最小用法:

    // 示意,非源码 —— 真实入口见 agent.ts:977 `prompt`
    const agent = new Agent({ /* model, tools, convertToLlm... */ });
    agent.subscribe(event => render(event)); // 订阅生命周期事件流
    await agent.prompt("把 README 里的拼写错误都改掉"); // 一句话,循环自己转到停

    prompt() 内部把这句话包成一条 user 消息,交给底层的 agentLoop(...),然后 for await 逐个事件更新内部状态、转发给订阅者(agent.ts:1194-1245)。

  • 一句话直觉: 把它想成 REPL(读-求值-打印循环),只不过"求值"是模型生成、"打印"是执行 工具。一圈叫一个 回合(turn);只要模型还在要工具或还有排队消息,REPL 就不退出。

本节不出现底层代码。记住一句话就行:agent-core = 模型与工具之间的传送带,一圈叫一个回合。


2. 顶层全景(它大概怎么转)

2.1 一个回合的数据流

先看一圈发生了什么。从左到右是时间顺序,虚线是"喂回去、再来一圈":

┌──────────────── 再来一圈(还有工具 / 还有排队消息)───────────────┐
│ │
用户/工具结果 ▼ │
─────────► ① 注入排队消息 ──► ② 转成线格式 ──► ③ 流式取一条 ──► ④ 有工具调用? │
(steering/aside) convertToLlm AssistantMessage stopReason 判定 │
(AgentMessage→Message) (边流边发事件) │ │
是 │ │ 否 │
▼ ▼ │
⑤ 执行工具 ⑥ 该停了吗? │
(并发+可打断) 查 follow-up │
│ │ │
└──────┴───────────┘
│ 无

agent_end

怎么读这张图: ②③④⑤ 是一个回合的四步;④ 判断"要不要继续",⑤ 跑完工具后必然回到 ① 再转;只有当模型不再要工具(⑥)且没有任何排队消息时,才落到 agent_end 收尾。

2.2 部件一句话职责

部件干什么在哪
Agent有状态的门面:持有 AgentState、暴露 prompt/steer/abort,把 config 组装好交给循环agent.ts:329
agentLoop / agentLoopContinue循环的两个入口:一个带新 prompt 开场,一个从现有 context 续跑agent-loop.ts:299 / :340
runLoopBody真正的双层 while:外层管"停了又被唤醒",内层管"一回合接一回合"agent-loop.ts:700
streamAssistantResponse一个回合的"取回复"半场:转线格式 → 调 provider → 边流边发事件 → 返回一条 AssistantMessageagent-loop.ts:1122
executeToolCalls一个回合的"跑工具"半场:按并发规则调度、可被 steering 打断、容错收集结果agent-loop.ts:1638
EventStream循环与消费者之间的异步管道:push 事件、for await 消费、result() 拿终值packages/ai/src/utils/event-stream.ts:5

2.3 一条贯穿全场的暗线:两种消息

这是理解整章的钥匙,先点破:

  • AgentMessage——循环内部的"货币"。它是 Message(LLM 认识的)加上宿主自定义消息类型 的并集(types.ts:512)。UI 通知、状态条这类东西可以塞进来,循环照样传递,但模型看不到。
  • Message——provider 线格式,只有 user / assistant / toolResult 三种角色。

循环里从头到尾都用 AgentMessage;只有在即将调模型的那一行,才通过 convertToLlm 把它过滤/转换成 Message[](agent-loop.ts:1142)。这条边界是第 4 节的主题。


3. 回合模型(一个 turn 的边界)

3.1 什么是一个"回合"

一个回合 = 一条 assistant 响应 + 它触发的所有工具调用/结果。 这不是我的定义,是代码里 AgentEvent 注释的原话(types.ts:698)。事件上,回合由一对 turn_start / turn_end 括起来:

turn_start
├─ message_start/update/end (assistant 消息,流式)
├─ tool_execution_start/end (每个工具)
└─ message_start/end (每条 toolResult)
turn_end { message, toolResults }

turn_end 事件带着这一回合的 assistant 消息和它的工具结果一起发出(agent-loop.ts:412, emitTurnEnd),宿主可挂 onTurnEnd 钩子做每回合的收尾。

3.2 双层循环:内层跑回合,外层管"复活"

runLoopBody 的骨架是两个嵌套 while(agent-loop.ts:753:757):

外层 while(true): ← 管"agent 本要停了,但又来了新消息"
内层 while(hasMoreToolCalls || 有排队消息): ← 每转一圈 = 一个回合
注入排队消息 → 流式取 assistant → 判 stopReason → 跑工具/收尾 → emitTurnEnd
重新拉 steering,决定下一圈的 pendingMessages
(内层退出:模型不再要工具、也没排队消息)
onBeforeYield() → 再拉一次 late-steering / aside / follow-up
有?→ 设为 pendingMessages, continue(外层再转,复活内层)
无?→ break → endAgentStream

为什么要两层? 因为"agent 停下来"和"agent 结束"是两回事。内层跑完(模型不要工具了), agent 看起来要停;但用户可能在这空档里追问了一句(follow-up),或有个后台任务刚完成 (aside)。外层的职责就是在真正 break 之前再探一次队列,有货就把内层"复活"再转一轮 (agent-loop.ts:1080-1087)。

3.3 stopReason:决定要不要继续的开关

模型每条回复带一个 stopReason。循环据此决定"跑工具 / 停 / 报错收尾":

stopReason含义循环怎么做
toolUse停在工具调用上有工具就跑(hasMoreToolCalls = true)
stop(end_turn)正常结束仍然跑工具——见下方关键细节
stop + pause_turn非终止停顿(如 Codex 的进度播报)无工具时也重采样续跑,最多 8 次
length撞了 max_tokens不跑尾部工具(参数可能被截断),配占位结果
error / aborted出错 / 被中止立即给未完成工具补占位结果,收尾退出

关键细节一(为什么 stop 也跑工具): stopReason 是 provider 的元数据,不回到线上。 带着 tool_use 的回合,只要把 tool_results 追加上去续跑,无论它当初停在 tool_use 还是 end_turn 都被接受——交错思考的 Opus 就经常在 end_turn 下发工具调用。所以代码把 runnableStop = stopReason === "toolUse" || stopReason === "stop" 一视同仁(agent-loop.ts:930)。 唯一不能跑的是 length:尾部工具的参数可能是半截的,强行执行会用截断的 payload 造成破坏, 于是这些调用被丢弃并配一条解释性占位结果(agent-loop.ts:1000-1017、占位文案见 :2084)。

关键细节二(pause_turn 的重采样上限): 有的后端会一直"暂停但不结束",若无脑续跑就会 死循环。MAX_PAUSED_TURN_CONTINUATIONS = 8 给了个闸,且一旦某回合带了真工具调用就清零计数 (agent-loop.ts:85:1020-1035)。


4. AgentMessage ↔ Message:调用边界的那一次转换

这是全章工程含量最高的一处约束,单独成节。

4.1 转换只发生在一个地方

streamAssistantResponse 里,紧挨着调 provider 之前,有连续几行(agent-loop.ts:1136-1143):

// 示意,贴近源码 —— 见 agent-loop.ts:1136
let messages = context.messages; // 一路都是 AgentMessage[]
if (config.transformContext) messages = await config.transformContext(messages, signal);
const llmMessages = await config.convertToLlm(messages); // ← 唯一的 AgentMessage → Message
const normalizedMessages = normalizeMessagesForProvider(llmMessages, config.model);

重点看: convertToLlm 是这条边界的闸门。文件顶部注释把设计意图写死了——"Agent loop that works with AgentMessage throughout. Transforms to Message[] only at the LLM call boundary." (agent-loop.ts:2-3)。

4.2 默认转换器丢掉什么

默认的 defaultConvertToLlm 只保留能上线的三类,并额外滤掉一种"看着像对话、其实是终止错误" 的东西(agent.ts:60-65):

// 示意,非源码 —— 见 agent.ts:60 `defaultConvertToLlm`
return messages.filter(m => {
if (m.role === "assistant") return !isProviderRefusalMessage(m); // 拒答不回放
return m.role === "user" || m.role === "toolResult"; // 其余只留这两类
});

isProviderRefusalMessage 判的是 stopReason === "error"stopDetails.typerefusal / sensitive(replay-policy.ts:4)——这是 provider 的审核拒答,是终止错误,不该当成正常 对话再喂回去。宿主可传自己的 convertToLlm 覆盖(比如把自定义消息类型翻成 user 消息)。

4.3 转换之后还有两道"归一化"

转成 Message[] 只是第一步,上线前还有两处 provider 适配:

归一化做什么位置
normalizeMessagesForProviderCerebras 不吃 assistant 的 thinking 块 → 就地剥掉agent-loop.ts:507
normalizeTools给工具 schema 注入/裁剪:intent 追踪字段 i、按方言渲染示例、按需去描述省 tokenagent-loop.ts:578

agentLoopContinue 的入口有一条硬约束值得记:续跑时 context 的最后一条消息必须能转成 usertoolResult,否则 provider 会拒。代码在入口挡掉了 assistant 结尾的情况 (agent-loop.ts:350),但"能不能转"这件事没法在这里验证,因为 convertToLlm 每回合只调一次 (agent-loop.ts:340-348 的文档注释明说了这点)。


5. 工具执行:并发与阻断

executeToolCalls 是"跑工具"半场,它要同时兼顾:多个工具怎么排、跑一半能不能被打断、 第三方工具乱返回怎么办。

5.1 并发调度:shared 排排站,exclusive 独占

一条 assistant 消息可能带多个工具调用。每个工具用 concurrency 声明自己的排法(types.ts:606):

  • "shared"(默认)——可以和别的 shared 工具并肩跑
  • "exclusive"——独占:它得等前面所有在跑的跑完,后面的也得等它。
  • 函数形式——按(未校验的)原始参数动态决定;解析器抛错就退回安全的 exclusive

调度逻辑是一段紧凑的"栅栏"编排(agent-loop.ts:2007-2031):

shared A ─┐
lastExclusive ─────────┼─► (并肩) exclusive C 等 A、B 都完
shared B ─┘ │

之后的 shared 又基于 C 起跑

每个 exclusive 任务 Promise.all([lastExclusive, ...sharedTasks]) 等齐前面所有人,然后成为新的 lastExclusive 并清空 shared 批次;shared 任务只等 lastExclusive。最后 Promise.allSettled(tasks) 等全批落定(agent-loop.ts:2047)。

5.2 打断:steering 如何截停一批工具

用户中途插话(steering)时,不该傻等整批工具跑完。机制分两层:

  1. 每个工具跑完后探一次队列——checkSteering()(agent-loop.ts:1688)。若队列非空,就 abort 一个内部steeringAbortController,后续未开跑的工具被标记 skipped(runTool 开头的 interruptState.triggered 短路,:1757)。注意它用的是非消费式 peek(hasSteeringMessages), 消息的所有权仍归队列,直到外层到达注入边界才真正出队。

  2. 对"只是在等"的工具,跑的过程中也轮询——若某工具声明了 interruptible: true(如 job 轮询), 一个 250ms 的定时器(STEERING_INTERRUPT_POLL_MS,:114)会周期性调 checkSteering,让 abort 信号提前把等待截短,而不是干等到工具自己的窗口耗尽(agent-loop.ts:2039-2050)。

设计克制之处: 只有纯等待、且干净响应 abort 的工具才该标 interruptible——因为 abort 一 个正在写文件的工具会留下半截副作用。文档在 AgentTool.interruptible 的注释里反复强调了这点 (types.ts:609-617)。

5.3 容错:第三方工具乱返回怎么办

工具接口是有类型的,但 MCP / 扩展 / 用户自写工具运行期可能违约(返回没有 content 数组、块形状 非法……)。把这种脏结果直接存进会话文件,重载时就会崩。coerceToolResult唯一入口把它掰回 合法形状(agent-loop.ts:230):

  • content 不是数组 → 换成一条 "invalid result" 错误文本,标 isError
  • 逐块校验,非法块计数并塞一条说明。
  • Anthropic 不接受 is_error: true 且内容为空的 tool_result → 补一句兜底文案(:280)。

afterToolCall 钩子返回的结果同样过一遍 coerceToolResult,因为它也是不可信的用户代码 (agent-loop.ts:1945)。

5.4 软性工具要求:提醒→升级,别急着强制

有时宿主想让模型"在干别的之前先调某个工具"(比如先 resolve 一个待决动作)。直接把 tool_choice 设成强制会作废 provider 的消息缓存,代价高。于是有了 SoftToolRequirement (types.ts:59)——一套"先礼后兵"的生命周期:

阶段行为依据
新 id 首次激活注入一次 reminder 消息,tool_choice 保持 autoagent-loop.ts:804-814
模型听话(只调了要求的工具)什么都不改,零缓存失效calledOnlyRequiredTool,:942
模型不听(调了别的 / 摆烂)不执行那些"绕路"工具,配 skipped 结果,下一回合强制 tool_choice:951-981
强制若干次仍不满足MAX_SOFT_TOOL_ESCALATIONS = 3 兜底抛错,防死循环:93:952

一个细节:硬 tool_choice 若和软要求冲突(禁用工具 none、或强制了别的工具),软门就让位—— hardToolChoiceBlocks 判这件事(agent-loop.ts:101)。


6. abort 语义:中止时怎么干净收尾

中止是这类循环最容易出 bug 的地方。核心难点:API 要求 tool_use 和 tool_result 必须配对—— 模型说要调 3 个工具,回放时就必须有 3 条结果,否则续跑直接被 provider 拒。

6.1 三种"停"和各自的收尾

触发怎么来的收尾动作
外部 abortagent.abort(reason)AbortController合成一条 aborted 的 assistant 消息,给未完成工具补占位结果
deadline 超时config.deadline 到点,内部 AbortController每个检查点 isDeadlineExceeded 提前 endAgentStream
provider 报错流里来了 error 事件stopReason === "error",同样补占位结果后退出

error / aborted 的分支在 agent-loop.ts:886:先把消息里的工具调用滤出来,逐个用 createAbortedToolResult 造占位结果(:2074),塞进 context 维持配对,再 emitTurnEnd + agent_end 退出。

6.2 abort 竞速:一次注册,反复复用

流式读取时,循环要同时等"下一个事件"和"abort 信号"谁先到。天真的写法是每次 next()Promise.withResolvers + 加/删监听器——太浪费。这里改成整条流只注册一次 abort 监听,拿一个 abortRacePromise 复用给每一次 responseIterator.next()(agent-loop.ts:1329-1353)。abort 赢了 就走 finishAbortedStream,尝试取消 provider、合成 aborted 消息、结算 span。

6.3 只有"完整"的工具调用能活过 abort

被中止时,流到一半的工具调用参数是半截的,带着不完整的 provider id,回放会炸。 retainCompletedToolCalls 只保留那些已经收到 toolcall_end 的调用(其 id 进了 completedToolCallIds 集合),把没完成的丢掉,并打上 stream_interrupted_after_content 标记 (agent-loop.ts:1499)。

6.4 abort 原因怎么透出

agent.abort("用户按了 Esc") 传的字符串,或一个非 AbortErrorError,会被 abortReasonText 提取出来挂到合成消息的 errorMessage 上;裸 abort()(reason 是默认的 AbortError DOMException)则退回通用文案 "Request was aborted"(agent-loop.ts:1576)。

6.5 中止后 steering 队列不清空

有个反直觉但正确的设计:外部 abort 时排空 steering 队列(agent-loop.ts:1045-1051)。 因为若在这里出队,消息会落在一个"马上就要 abort 的模型调用"之前——消息进了历史,agent 却永远 不回应。正确做法是把队列留着,让 session 层 abort 后重新续跑时,再把队列送进一个新的 run。


7. 状态模型:循环之外的三样东西

循环本身几乎无状态(参数进、事件出)。有状态的东西挂在 Agent 和几个辅助结构上。

7.1 AgentState:门面持有的一切

AgentState(types.ts:517)是宿主眼里的"当前会话":systemPrompt、model、tools、messages、 是否在流式、待决工具集合、错误。Agent 把它设为私有字段 #state(agent.ts:330),对外只读 get state(),改动都走命名方法(setModelappendMessagesteer……)。

Agent.#runLoop 每次跑之前,把 #state 里的东西装进一个 AgentLoopConfig 和一份 context 快照 (context.messagesslice() 出来的副本,agent.ts:1078-1082),再交给 agentLoop。循环产出的 事件流回来,又逐个更新回 #state(agent.ts:1198-1245)。"config/context 进 → 事件出 → state 更新" 是单向的,循环从不直接改宿主状态。

7.2 两条队列:steer(插话)与 followUp(追问)

队列何时投递入队方法出队策略
steering回合中间,打断当前工具批次后agent.steer(m)(:860)all 一次全给 / one-at-a-time 每回合一条
followUpagent 本要停下时才处理agent.followUp(m)(:868)同上

两条队列都是 Agent 私有数组,循环通过 config 上的 getSteeringMessages / getFollowUpMessages 回调去拉(agent.ts:1177-1186)。这样队列的真相只有一份(在 agent-core),session 层用非消费式 的 peekSteeringQueue 派生显示与计数(agent.ts:893)。

7.3 append-only-context:让 provider 缓存少失效

长会话每回合都把整段历史重新序列化上线,provider 的前缀缓存(DeepSeek/Anthropic 的 KV cache)就 反复失效、反复重算。AppendOnlyContextManager(append-only-context.ts:169)专治这个:

  • StablePrefix——system prompt + 工具规格算一次就冻住,除非指纹变了(:50)。
  • AppendOnlyLog——消息只增不改,靠 syncMessages 每回合把新增的尾巴 append 上去(:207)。

最巧的是 syncMessages 的"就地改写"分支:当某条历史消息被改写(裁剪、去图、transformContext 重渲染),早期版本会清空整条日志 → 本地后端每回合被迫重算 ~40k token 的 prefix(issue #3406)。 现在它按逐条 digest 找最长稳定前缀,只从分叉点之后重发,让 KV cache 一直热到分叉点 (append-only-context.ts:216-237#longestStablePrefix at :277)。这属于本章的边界——深入见 长程上下文治理

7.4 run-collector:一次 run 的账本

若开了 telemetry,AgentRunCollector(run-collector.ts:147)在循环跑的过程中缓冲每次 chat、每个 工具的记录,最后折叠成一个 AgentRunSummary + AgentRunCoverage,随 agent_end 事件带出 (agent-loop.ts:386 buildAgentEndEvent)。它记的是:各 stopReason 计数、每个工具的 ok/error/skipped/blocked/aborted、token 用量、成本、以及"声明了但没被调用的工具"覆盖率。

有两条绕过 span 的 skip 路径要它直接补记账:pre-run 就被中止的工具、以及批次尾扫时"从没产出 结果消息"的工具——都走公开的 recordSkippedTool(agent-loop.ts:902:2055-2065)。另外, beforeToolCall 返回 { block: true } 时抛的 ToolCallBlockedError(run-collector.ts:619)让 span 能把终态标成 blocked 而非混同普通异常。


8. 巧妙之处(可借鉴的技术)

  1. "AgentMessage 全程、只在调用边界转 Message" —— 让循环能承载 UI-only 消息、拒答标记等 非线上内容,而不污染 provider 请求。转换闸门集中在一处(agent-loop.ts:1142),改一个地方就改了 全部行为。

  2. stopReason 不上线,所以 end_turntool_use 一视同仁 —— 一句被 live Anthropic API 验证过 的观察,省掉了一整类"模型在 end_turn 下发工具"的兼容分支(agent-loop.ts:915-930 的长注释)。

  3. 单次注册的 abort 竞速 —— 把每事件的 withResolvers/监听器增删,降成整条流一次注册、复用同 一个 promise(agent-loop.ts:1329)。热路径上的实打实优化。

  4. 软性工具要求"先提醒后强制" —— 承认"改 tool_choice = 作废缓存"这个真实代价,用一次内联提醒 把"听话"的常见情况变成零成本,只在模型不听时才付强制的代价(types.ts:59agent-loop.ts:942-981)。

  5. 最长稳定前缀 diff —— 用逐条 digest 找分叉点,把"改一条历史消息 = 全量重算"降成"只重算分叉 之后",直接回应了一个真实 issue(#3406,append-only-context.ts:277)。

  6. 占位结果维持 tool_use/tool_result 配对 —— 无论中止、超时、还是 length 截断,都给每个悬空的 工具调用补一条结果消息,保证下一次回放不被 provider 拒(createAbortedToolResult,:2074)。


9. 边界与局限(诚实说)

  • agentLoopContinue 的最后一条消息约束无法在入口验证 —— 它必须能转成 user/toolResult, 否则 provider 拒;但因为 convertToLlm 每回合只调一次,入口只能挡掉 assistant 结尾这一种显式 情况,其余靠调用方自觉(agent-loop.ts:340-348)。

  • 软要求的强制上限是"防御性"的,不是常态 —— MAX_SOFT_TOOL_ESCALATIONS = 3 到顶会抛错终止 整个 run(agent-loop.ts:952)。强制 tool_choice 本应保证工具被调,走到这一步说明模型行为异常。

  • pause_turn 续跑上限 8 次同理 —— 撞上限后就不再续,把控制权交回;一个永不停止 pause 的后端 会被这个闸挡住而非无限自旋(agent-loop.ts:85)。

  • 中止时半截工具调用一律丢弃 —— 只有到达 toolcall_end 的调用能存活(retainCompletedToolCalls, :1499)。这是安全取舍:半截参数不可信,宁可丢。

  • telemetry 关掉时循环零 tracer 开销 —— telemetry 字段为 undefined 时,buildAgentEndEvent 直接返回裸事件,不做任何 collector 快照(agent-loop.ts:391)。功能诚实地按需付费。

  • 本章不涉及:in-band 方言如何把工具调用编/解码(第 2 章)、 工具本身的 FS 形状接口(第 3 章)、compaction 与转向(第 6 章)。


10. 代码地图(导航索引)

按符号名 grep 比按行号更抗漂移。下表列的是真实符号:

主题文件路径符号名
循环入口(新 prompt)packages/agent/src/agent-loop.tsagentLoop
循环入口(续跑)packages/agent/src/agent-loop.tsagentLoopContinue
双层 while 主体packages/agent/src/agent-loop.tsrunLoopBody
取回复半场(转线格式+流式)packages/agent/src/agent-loop.tsstreamAssistantResponse
跑工具半场(并发+打断+容错)packages/agent/src/agent-loop.tsexecuteToolCalls
单个工具执行packages/agent/src/agent-loop.tsrunTool
steering 打断探测packages/agent/src/agent-loop.tscheckSteering
工具结果容错归一packages/agent/src/agent-loop.tscoerceToolResult
中止/超时占位结果packages/agent/src/agent-loop.tscreateAbortedToolResult
半截工具调用剔除packages/agent/src/agent-loop.tsretainCompletedToolCalls
abort 原因提取packages/agent/src/agent-loop.tsabortReasonText
线格式归一(Cerebras thinking)packages/agent/src/agent-loop.tsnormalizeMessagesForProvider
工具 schema 归一(intent/示例/裁剪)packages/agent/src/agent-loop.tsnormalizeTools
pause_turn 续跑上限packages/agent/src/agent-loop.tsMAX_PAUSED_TURN_CONTINUATIONS
有状态门面 / prompt 入口packages/agent/src/agent.tsAgent, prompt, #runLoop
默认线格式转换器packages/agent/src/agent.tsdefaultConvertToLlm
插话 / 追问入队packages/agent/src/agent.tssteer, followUp
消息/状态/事件类型packages/agent/src/types.tsAgentMessage, AgentState, AgentEvent
工具接口(并发/可打断/intent)packages/agent/src/types.tsAgentTool
软性工具要求packages/agent/src/types.tsSoftToolRequirement, isSoftToolRequirement
拒答识别(不回放)packages/agent/src/replay-policy.tsisProviderRefusalMessage
run 账本 / 被阻断错误packages/agent/src/run-collector.tsAgentRunCollector, ToolCallBlockedError
稳定前缀 + 追加日志packages/agent/src/append-only-context.tsAppendOnlyContextManager, syncMessages
thinking 档位packages/agent/src/thinking.tsThinkingLevel
事件流管道packages/ai/src/utils/event-stream.tsEventStream

同组其它章: 总览与阅读地图 · 模型接入层与方言 · 工具宇宙 · hashline 编辑语言 · 原生核心 · 长程上下文治理与转向