图解 Pi AgentPi Agent, Illustratedv1.1.0
包Packages· agent-core
演示Slides
pi-agent-core已完成Donepackages/agent · v1.1.0

agent-core@earendil-works/pi-agent-core

通用 Agent:状态、事件循环、工具执行与钩子。General-purpose agent with transport abstraction, state management, and attachment support.

1.1.0
版本version
6
源文件src files
2,519
源码行src LOC
4
测试文件test files
83
测试用例test cases
1
导出路径export paths
依赖Depends onpi-ai被依赖Used bycoding-agent

这个包是什么What this package ispi-agent-core 是一个与“编码”无关的通用 Agent 内核:一个 Agent 类管状态和队列,一个 runLoop() 驱动“请求 → 工具 → 下一轮”,所有策略(上下文变换、权限、压缩、何时停)都通过 AgentLoopConfig 钩子注入。pi-agent-core is a general agent kernel that knows nothing about coding: an Agent class owns state and queues, runLoop() drives request → tools → next turn, and every policy (context transforms, permissions, compaction, when to stop) is injected through AgentLoopConfig hooks.

位于 pi-ai(模型与流)之上、pi-coding-agent(产品)之下。只有一个运行时依赖:pi-ai。Sits above pi-ai (models and streams) and below pi-coding-agent (the product). One runtime dependency: pi-ai.

01pi-agent-core

模块结构Module structure#

一句话结论Takeaway6 个源文件、单一导出入口:agent.ts(有状态的外壳)调用 agent-loop.ts(无状态的循环),两者共享 types.ts;proxy.ts 与 stream-fn.ts 提供可替换的 StreamFn。Six source files and a single export entry: agent.ts (the stateful shell) calls agent-loop.ts (the stateless loop), both share types.ts; proxy.ts and stream-fn.ts provide swappable StreamFns.

src/ 的 6 个文件、入口与对 pi-ai 的依赖The six files in src/, the entry, and what they use from pi-ai
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
pi-agent-core modulessrc/6 个文件6 files@earendil-works/pi-ai唯一的运行时依赖the only runtime dependencypi-coding-agentAgentSession · sdk.ts你的应用your appnew Agent({ streamFn })宿主运行时host runtimesetDefaultStreamFn(fn)test/4 个文件 · 77 个用例4 files · 77 casesindex.tsexport *613 Lagent.tsclass Agent · AgentOptionsPendingMessageQueue ×2(私有)PendingMessageQueue ×2 (private)949 Lagent-loop.tsagentLoop() · agentLoopContinue()runAgentLoop*() · runToolCall()20 Lstream-fn.tssetDefaultStreamFn()(导出)setDefaultStreamFn() (exported)getDefaultStreamFn()(内部)getDefaultStreamFn() (internal)393 Lproxy.tsstreamProxy(model, ctx, opts)ProxyAssistantMessageEvent539 Ltypes.tsAgentEvent · AgentTool · AgentStateAgentLoopConfig · StreamFn · hooksutils/transcript.tscreateInitialSystemMessage · getCurrentToolsgetToolStateChanges · toToolDeclarationutils/validation.tsvalidateToolArguments(tool, toolCall)TypeBox 模式校验 · 失败即抛出TypeBox schema check · throws on failureutils/event-stream.tsEventStream<T, R> → agentLoop() 的返回值EventStream<T, R> → what agentLoop() returnsAssistantMessageEventStream → streamProxy()types.tsModel · Message · Tool · UsageSimpleStreamOptions · TranscriptContextAgentagentLoopagentLoopContinuerunAgentLooprunAgentLoopContinuerunToolCallstreamProxysetDefaultStreamFn使用方CONSUMERS入口ENTRYrunAgentLoop()未传 streamFn 时when streamFn is omitted值导出Value exports+ 35 个导出类型+ 35 exported types主路径main pathimport / 再导出import / re-exportpi-agent-corepi-aipi-coding-agent其他other

要点Key points

  • index.ts 只有 5 行:export * 四个模块,再单独导出 setDefaultStreamFn(getDefaultStreamFn 不导出)。index.ts is five lines: export * from four modules plus setDefaultStreamFn alone (getDefaultStreamFn stays internal).
  • 循环本身不持有状态:agentLoop() 返回 EventStream<AgentEvent, AgentMessage[]>,runAgentLoop() 接受一个 emit 回调——Agent 用的是后者。The loop holds no state: agentLoop() returns an EventStream<AgentEvent, AgentMessage[]>, runAgentLoop() takes an emit callback — Agent uses the latter.
  • 对 pi-ai 的依赖集中在四处:transcript 工具(工具声明 / system 基线)、validateToolArguments、EventStream 与类型。The pi-ai dependency is concentrated in four places: transcript helpers (tool declarations, system baseline), validateToolArguments, EventStream, and types.

工作原理How it works

  1. 宿主构造 new Agent({ initialState, streamFn, convertToLlm, … })。The host constructs new Agent({ initialState, streamFn, convertToLlm, … }).
  2. prompt() / continue() 把当前状态拍成 AgentContext 快照,连同由选项组装的 AgentLoopConfig 交给 runAgentLoop()。prompt() / continue() snapshot state into an AgentContext and pass it, with an AgentLoopConfig assembled from the options, to runAgentLoop().
  3. 循环通过 emit 发出 AgentEvent;Agent.processEvents() 先更新自身状态,再依次 await 订阅者。The loop emits AgentEvents via emit; Agent.processEvents() updates its own state first, then awaits subscribers in order.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/agent/src/index.tsL4export *唯一入口the only entry point
packages/agent/src/agent.tsL188Agent状态、队列、生命周期state, queues, lifecycle
packages/agent/src/agent-loop.tsL102runAgentLoop循环入口(emit 版本)loop entry (emit flavour)
packages/agent/src/stream-fn.tsL11setDefaultStreamFn全局默认 StreamFnglobal default StreamFn
packages/agent/src/types.tsL193AgentLoopConfig所有钩子的定义every hook's definition
02pi-agent-core

公共 APIPublic API#

一句话结论Takeaway只有一个导入路径 @earendil-works/pi-agent-core。日常只需要 Agent;需要自己管理状态时直接用 agentLoop() / runAgentLoop();runToolCall() 让宿主在循环之外复用同一套工具管道。There is a single import path, @earendil-works/pi-agent-core. Day to day you only need Agent; manage state yourself with agentLoop() / runAgentLoop(); runToolCall() lets a host reuse the same tool pipeline outside the loop.

由构建脚本扫描 export 声明生成,每个符号都链接到 v1.1.0 中的定义行。getDefaultStreamFn 未从 index.ts 导出。Generated by scanning export declarations at build time; each symbol links to its definition at v1.1.0. getDefaultStreamFn is not re-exported from index.ts.

03pi-agent-core

运行生命周期Run lifecycle#

一句话结论Takeaway同一时刻最多一个活动运行:runWithLifecycle() 建立 AbortController 与 isStreaming,执行失败会被转换成一条合成的 error 助手消息而不是向上抛出,finishRun() 总在最后清理。At most one active run at a time: runWithLifecycle() sets up the AbortController and isStreaming; executor failures become a synthesized error assistant message instead of propagating, and finishRun() always cleans up.

Agent 的运行状态机与守卫The Agent run state machine and its guards
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
Agent run state machine守卫与约束(agent.ts)GUARDS (agent.ts)abort()中止信号 → 流与工具signal → stream & tools空闲IdleactiveRun = undefinedrunWithLifecycle()isStreaming = truenew AbortController()流式响应Streaming responsestreamingMessage = partialmessage_start · _update收尾Settlingawait agent_end 监听器await agent_end listenersisStreaming 仍为 trueisStreaming still true执行工具Executing toolspendingToolCalls ∪ {id}tool_execution_start · _endfinishRun()isStreaming = false清空状态 · resolve 等待者clear state · resolve waitershandleRunFailure()合成 error 助手消息synthesize error messagemessage_* · turn_end · agent_endprompt · continuetoolCallturn_startagent_end监听器全部完成listeners settledexecutor 抛出executor throwsfinally再次空闲 · waitForIdle() 返回idle again · waitForIdle() resolvesprompt()运行中调用 → 抛错,提示改用 steer() / followUp()while running → throws: use steer() / followUp()reset()运行中调用 → 抛错;空闲时保留 system 基线、清空队列throws while running; idle: keeps the system baseline, clears queuescontinue()末条为 assistant → 先排空 steering,再 follow-up,否则抛错last = assistant → drain steering, else follow-ups, else throwsubscribe()监听器按订阅顺序 await,并拿到本次运行的 signallisteners awaited in order, each gets the run signal12345

要点Key points

  • prompt() 在运行中调用会抛错,提示改用 steer() 或 followUp()——这是唯一的并发入口约束。prompt() throws while a run is active, pointing at steer() or followUp() — the single concurrency rule at the entry.
  • isStreaming 在 agent_end 监听器全部完成后才变回 false,所以 waitForIdle() 返回时 UI 一定已经处理完最后的事件。isStreaming turns false only after all agent_end listeners settle, so when waitForIdle() resolves the UI has handled the final events.
  • handleRunFailure() 依次发出 message_start/end(error 消息)、turn_end、agent_end,订阅者看到的事件序列与正常结束同构。handleRunFailure() emits message_start/end (the error message), turn_end and agent_end, so subscribers see the same event shape as a normal ending.
  • reset() 运行中抛错;空闲时清空消息与队列,但保留 system 基线消息。reset() throws while running; when idle it clears messages and queues but keeps the system baseline message.

工作原理How it works

  1. prompt(input) 把字符串 / 图像规范化成 user 消息,调用 runPromptMessages()。prompt(input) normalizes string / images into user messages and calls runPromptMessages().
  2. runWithLifecycle(executor) 创建 AbortController,置 isStreaming = true,执行 executor(signal)。runWithLifecycle(executor) creates an AbortController, sets isStreaming = true, runs executor(signal).
  3. 异常 → handleRunFailure(error, aborted);stopReason 取 aborted 或 error。On exception → handleRunFailure(error, aborted); stopReason becomes aborted or error.
  4. finally → finishRun():isStreaming = false、清 streamingMessage / pendingToolCalls、resolve 空闲等待者。finally → finishRun(): isStreaming = false, clear streamingMessage / pendingToolCalls, resolve idle waiters.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/agent/src/agent.tsL373prompt()入口与并发检查entry and concurrency check
packages/agent/src/agent.tsL384continue()从现有上下文恢复resume from existing context
packages/agent/src/agent.tsL507runWithLifecycle()AbortController 与 isStreamingAbortController and isStreaming
packages/agent/src/agent.tsL532handleRunFailure()合成 error 消息synthesize the error message
packages/agent/src/agent.tsL550finishRun()清理并唤醒等待者cleanup and wake waiters
packages/agent/src/agent.tsL355reset()保留 system 基线keeps the system baseline
04pi-agent-core

Agent 循环Agent loop#

一句话结论TakeawayrunLoop 是两层嵌套循环:内层处理工具调用与 steering 消息,外层在 follow-up 消息到达时重新启动。runLoop is two nested loops: the inner one drains tool calls and steering messages, the outer one restarts when follow-up messages arrive.

本节沿用第一版的图(原位于总览页),已按 v1.1.0 重新核对。This section keeps its first-edition diagram (formerly on the overview page), re-checked against v1.1.0.

runLoop():内层循环与外层循环runLoop(): inner and outer loop
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
Agent loop消息队列MESSAGE QUEUESrunLoop()prompt() / continue()Steering 队列Steering queueagent.steer()Follow-up 队列Follow-up queueagent.followUp()注入待处理消息Inject pending messagesprepared + steering → message_start/endstreamAssistantResponse()transformContext → convertToLlm → streamFnexecuteToolCalls()sequential | parallel · length → 全部失败sequential | parallel · length → all failfinishTurn() · turn_end决策 end | continue,再轮询 steeringdecision end | continue, then poll steeringgetFollowUpMessages()Agent 本该停止时才检查checked only when the agent would stopagent_end运行结束,回到 idlerun finished → idle用户在 Agent 运行时输入typed while the agent runs模式:one-at-a-time(默认)| allmode: one-at-a-time (default) | all有工具调用tool calls无工具调用no tool callsprepareNextTurn() · turn_start仍有工具调用 / steeringtool calls or steering left轮询poll都没有neither决策 = enddecision = end无 follow-upnone queuederror / aborted外层循环:follow-up 到达(或 finishTurn → continue)outer loop: follow-ups arrived (or finishTurn → continue)内层inner循环loop012345

要点Key points

  • Steering:Agent 运行中用户插入的消息,在当前轮的工具执行完成后、下一次 LLM 请求之前注入。Steering messages typed while the agent runs are injected after the current turn's tools finish, before the next LLM request.
  • Follow-up:只在 Agent 本该停止时才投递,触发外层循环。Follow-up messages are delivered only when the agent would otherwise stop, and drive the outer loop.
  • stopReason 为 length 时,该消息中所有工具调用直接判为失败,因为参数可能被截断。On stopReason: "length" every tool call in that message fails, because its arguments may be truncated.
  • finishTurn 可返回 end(立即结束)或 continue(在无新消息时追加一次仅含上下文的轮次)。finishTurn can return end (stop now) or continue (run one more context-only turn when nothing else is queued).

工作原理How it works

  1. 开始时先轮询一次 getSteeringMessages(),因为用户可能在等待时已经输入。It first polls getSteeringMessages(), since the user may have typed while waiting.
  2. 从第二轮起调用 prepareNextTurn();coding agent 在这里做阈值压缩并刷新工具清单。From the second turn it calls prepareNextTurn(); the coding agent compacts at threshold and refreshes the tool loadout here.
  3. prepareRequest() 可覆盖 model 与 thinking level,然后 streamAssistantResponse() 依次执行 transformContext → convertToLlm → getApiKey → streamFn。prepareRequest() may override model and thinking level; then streamAssistantResponse() runs transformContext → convertToLlm → getApiKey → streamFn.
  4. 若所有工具结果都带 terminate: true,内层循环在本轮后停止。If every tool result carries terminate: true, the inner loop stops after this turn.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/agent/src/agent-loop.tsL163runLoop()双层循环本体the two nested loops
packages/agent/src/agent-loop.tsL381streamAssistantResponse()上下文转换与流式请求context transform and streaming call
packages/agent/src/types.tsL193AgentLoopConfig所有循环钩子的类型types for every loop hook
packages/coding-agent/src/core/agent-session.tsL888finishTurncoding agent 的轮次收尾coding agent's end-of-turn hook
05pi-agent-core

钩子调用顺序Hook call order#

一句话结论Takeaway一轮 = 一次 LLM 响应 + 它请求的工具。钩子的调用顺序是固定的:prepareRequest → transformContext → convertToLlm → getApiKey → streamFn,工具侧 prepareArguments → validate → beforeToolCall → execute → afterToolCall,轮末 finishTurn → getSteeringMessages → prepareNextTurn。A turn = one LLM response + the tools it asks for. Hooks fire in a fixed order: prepareRequest → transformContext → convertToLlm → getApiKey → streamFn; for tools prepareArguments → validate → beforeToolCall → execute → afterToolCall; at turn end finishTurn → getSteeringMessages → prepareNextTurn.

两次请求 + 一次工具调用中的钩子与事件顺序Hook and event order across two requests and one tool call
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
Hook call order in one runrunLoop()AgentLoopConfig 钩子AgentLoopConfig hooksstreamFn · pi-aiAgentToolemit → AgentEventagent_start · turn_start · message_*(user)message_start (assistant)message_update ×nmessage_end (assistant)tool_execution_starttool_execution_update ×ntool_execution_end · message_*(toolResult)turn_endturn_startmessage_start … message_endturn_end · agent_end1 · 开始1 · Start2 · 请求 12 · Request 13 · 工具3 · Tools4 · 轮次结束4 · End of turn5 · 请求 2 → 结束5 · Request 2 → enddeclareToolChanges(prompts)getSteeringMessages()prepareRequest(req, signal)transformContext(messages)convertToLlm(messages)getApiKey(provider)streamFn(model, normalizeContext(llm), opts)text_* · thinking_* · toolcall_*done(toolUse)prepareArguments() → validatebeforeToolCall(ctx, signal)execute(id, args, signal, onUpdate)onUpdate(partial) … resultafterToolCall(ctx, signal)finishTurn(turn, signal)getSteeringMessages()prepareNextTurn(turn)prepareRequest → … → getApiKeystreamFn(…)done(stop)finishTurn → getFollowUpMessageserror / aborted → finishTurn → turn_end → agent_end,直接退出error / aborted → finishTurn → turn_end → agent_end, hard exit123456789101112131415161718192021调用call返回 / 流事件return / stream event1顺序编号,颜色 = 发起方order · colour = caller

要点Key points

  • transformContext 作用于 AgentMessage[](可含自定义消息),convertToLlm 再把它们转成 pi-ai 的 Message[]——压缩、裁剪放前者,格式转换放后者。transformContext works on AgentMessage[] (custom messages included); convertToLlm then turns them into pi-ai Message[] — pruning belongs in the former, format conversion in the latter.
  • getApiKey 每次请求都调用,让短期 OAuth token 在长时间工具执行期间也能刷新。getApiKey is called on every request, so short-lived OAuth tokens can refresh during long tool runs.
  • finishTurn 返回 { action: "end" } 可以在本轮后直接结束(跳过 steering 轮询);prepareNextTurn 可以替换模型、工具、上下文。finishTurn returning { action: "end" } stops after this turn (skipping the steering poll); prepareNextTurn can swap model, tools or context.
  • 工具集合变化时,declareToolChanges 在发送前插入带 toolsAdded/Removed 的 system 消息,保证 transcript 自描述。When the tool set changes, declareToolChanges inserts a system message with toolsAdded/Removed before sending, keeping the transcript self-describing.

工作原理How it works

  1. 开始:发 agent_start、turn_start,把提示消息作为 message_start/end 发出。Start: emit agent_start, turn_start, then the prompt messages as message_start/end.
  2. 请求:streamAssistantResponse() 依次调用请求侧钩子,把 pi-ai 事件映射为 message_*。Request: streamAssistantResponse() runs the request-side hooks and maps pi-ai events onto message_*.
  3. 工具:每个 toolCall 经 prepareToolCall() → executePreparedToolCall() → finalizeExecutedToolCall()。Tools: each toolCall goes through prepareToolCall() → executePreparedToolCall() → finalizeExecutedToolCall().
  4. 轮末:turn_end,轮询 steering;有消息或有工具结果就进入下一轮,否则检查 follow-up,最后 agent_end。Turn end: turn_end, poll steering; continue if there are messages or tool results, else check follow-ups, then agent_end.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/agent/src/agent-loop.tsL163runLoop()内外两层循环the inner and outer loop
packages/agent/src/agent-loop.tsL381streamAssistantResponse()请求侧钩子与事件映射request hooks and event mapping
packages/agent/src/agent-loop.tsL333declareToolChanges()工具变化写入 transcripttool changes into the transcript
packages/agent/src/agent-loop.tsL708prepareToolCall()查找 · 参数 · 校验 · beforeToolCallfind · args · validate · beforeToolCall
packages/agent/src/agent-loop.tsL859finalizeExecutedToolCall()afterToolCall 字段合并afterToolCall field merge
packages/agent/src/types.tsL155FinishTurnend | continueend | continue
06pi-agent-core

工具调用执行Tool call execution#

一句话结论Takeaway每个工具调用都走同一条管道:查找 → 参数准备 → schema 校验 → beforeToolCall → 执行 → afterToolCall;任何失败都变成错误结果,而不是异常。Every tool call runs one pipeline — lookup → prepare → schema validation → beforeToolCall → execute → afterToolCall — and every failure becomes an error result, never a throw.

本节沿用第一版的图(原位于总览页),已按 v1.1.0 重新核对。This section keeps its first-edition diagram (formerly on the overview page), re-checked against v1.1.0.

单个工具调用的处理管道The pipeline for one tool call
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
Tool call execution扩展(经 ExtensionRunner)EXTENSIONS (via ExtensionRunner)一批工具调用A BATCH OF TOOL CALLSassistant 消息含多个 toolCall 时when one assistant message has several tool calls查找工具Find tooltools.find(name)准备参数Prepare argsprepareArguments()校验ValidatevalidateToolArguments()beforeToolCall()ext · tool_calltool.execute()(…, signal, onUpdate)afterToolCall()ext · tool_result完成Finishtool_execution_endtoolCall来自 assistant 消息from assistant messageToolResultMessagemessage_start / endtool_call可阻止 · blockmay blocktool_result可改写结果may rewrite resulttool_execution_update流式输出 · 例如 bashstreamed output, e.g. bashcreateErrorToolResult(message)isError: true —— 失败是结果,不是异常isError: true — a failure is a result, never a throwexecute 抛错execute throws→ isError 结果→ isError resulttool_execution_startonUpdate未找到not found抛错throws不合法invalidblock / abort跳过 afterToolCallskips afterToolCallsequentialparallelprep 1exec 1msg 1prep 2exec 2msg 2prep 3exec 3msg 3prep 1prep 2prep 3并发执行 · 每个完成即发 tool_execution_endrun concurrently · tool_execution_end as each finishesmsg 1msg 2msg 3结果按原顺序发出results in source order任一工具声明 executionMode: "sequential" → 整批串行any tool with executionMode: "sequential" serializes the batch1234567

要点Key points

  • 参数用工具的 TypeBox schema 通过 validateToolArguments() 校验。Arguments are validated against the tool's TypeBox schema via validateToolArguments().
  • coding agent 把 beforeToolCall / afterToolCall 映射为扩展事件 tool_call(可阻止)和 tool_result(可改写结果)。The coding agent maps beforeToolCall / afterToolCall to the extension events tool_call (can block) and tool_result (can rewrite).
  • 模式为 ToolExecutionMode = "sequential" | "parallel";只要批次中有一个工具声明 executionMode: "sequential",整批就串行。ToolExecutionMode = "sequential" | "parallel"; if any tool in the batch declares executionMode: "sequential", the whole batch runs sequentially.
  • runToolCall() 对外导出,供调用其他工具的工具复用同一套钩子(例如权限检查)。runToolCall() is exported so tools that call other tools reuse the same hooks (e.g. permission checks).

工作原理How it works

  1. 并行模式下,准备阶段(含 beforeToolCall)按源顺序逐个进行,执行阶段并发。In parallel mode, preparation (including beforeToolCall) runs one by one in source order; execution runs concurrently.
  2. tool_execution_end 在每个工具完成时发出;ToolResultMessage 则按原始顺序统一发出。tool_execution_end fires as each tool finishes; ToolResultMessages are emitted afterwards in the original order.
  3. 执行中的 onUpdate 回调被转成 tool_execution_update 事件,用于流式显示 bash 输出等。onUpdate during execution becomes tool_execution_update events, used to stream bash output and similar.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/agent/src/agent-loop.tsL708prepareToolCall()查找、校验与 beforeToolCalllookup, validation, beforeToolCall
packages/agent/src/agent-loop.tsL586executeToolCallsParallel()并发执行与有序结果concurrent execution, ordered results
packages/agent/src/agent-loop.tsL811runToolCall()可复用的单次调用reusable single call
packages/coding-agent/src/core/agent-session.tsL668emitToolCall接入扩展事件bridges to extension events
packages/coding-agent/src/core/tools/index.tsL95ToolName8 个内置工具the 8 built-in tools
07pi-agent-core

工具批的并发与顺序Tool batches: concurrency and order#

一句话结论Takeaway默认并行:准备阶段按顺序(含 beforeToolCall),执行并发,afterToolCall 后逐个发 tool_execution_end,所有结果到齐再按原顺序写 toolResult。任一工具声明 executionMode: "sequential" 时整批退化为顺序执行。Parallel by default: preparation runs in order (including beforeToolCall), execution is concurrent, tool_execution_end fires as each finalizes, and toolResults are written in source order once all settle. If any tool declares executionMode: "sequential" the whole batch runs sequentially.

并行与顺序模式的时间线(示意,时长为示例)Parallel vs sequential timelines (illustrative durations)
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
Timing of a tool batchparallel(默认)parallel (default)准备按顺序 · 执行并发 · 结果按原顺序prepare in order · execute concurrently · results in source ordersequentialtoolExecution = "sequential",或批内任一工具声明 executionMode: "sequential"toolExecution = "sequential", or any tool in the batch declares executionMode: "sequential"A · readB · bashC · grepprepexecute · 120 msprepexecute · 380 mspreptool_execution_end #1tool_execution_end #2tool_execution_end #3消息messagesmsg Amsg Bmsg CPromise.all 之后按 A → B → C 发出after Promise.all · in A → B → C order0 ms100 ms200 ms300 ms400 ms500 ms600 ms700 ms800 msA · read120 msB · bash380 msC · grep≈ 770 ms:每个调用 start → … → message 后才开始下一个≈ 770 ms: each call finishes start → … → message before the next0 ms100 ms200 ms300 ms400 ms500 ms600 ms700 ms800 ms立即结果(未找到 / 校验失败 / 被阻止)在准备阶段就发 tool_execution_end;中止后剩余调用得到 "Operation aborted"immediate outcomes (not found / invalid / blocked) emit tool_execution_end during prepare; after abort the rest get "Operation aborted"terminate:只有批内每个最终结果都设 terminate: true,循环才在本批后停止terminate: the loop stops after the batch only if every finalized result sets terminate: true准备:查找 · prepareArguments · 校验 · beforeToolCall;以及 afterToolCallprepare: find · prepareArguments · validate · beforeToolCall; and afterToolCalltool.execute()ToolResultMessage

要点Key points

  • 立即结果(未知工具、参数校验失败、被 beforeToolCall 阻止)不进入执行,直接得到 error 结果。Immediate outcomes (unknown tool, failed validation, blocked by beforeToolCall) skip execution and get an error result directly.
  • 1.1.0 新增:每个结果带 durationMs(performance.now() 计时),出现在 AgentToolCallOutcome 与 tool_execution_end 上。New in 1.1.0: every result carries durationMs (timed with performance.now()), on AgentToolCallOutcome and tool_execution_end.
  • 只有批内每个最终结果都 terminate: true 时,循环才会在本批后停止——一个工具不能单方面结束对话。The loop stops after a batch only if every finalized result has terminate: true — a single tool can't end the conversation on its own.
  • 输出被截断(stopReason: "length")时,不执行任何工具调用,而是为每个调用写入失败结果。When output was truncated (stopReason: "length"), no tool call runs; each gets a failure result instead.

工作原理How it works

  1. executeToolCalls() 根据 config.toolExecution 与各工具的 executionMode 选择顺序或并行。executeToolCalls() picks sequential or parallel from config.toolExecution and each tool's executionMode.
  2. 并行:逐个 prepareToolCall(),把可执行的放入 Promise.all。Parallel: prepareToolCall() one by one, putting runnable calls into Promise.all.
  3. 每个完成后 finalizeExecutedToolCall() 并立即发 tool_execution_end。Each completion is finalized and immediately emits tool_execution_end.
  4. 全部完成后按原顺序 createToolResultMessage() 并发出消息事件。After all settle, createToolResultMessage() in source order and emit the message events.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/agent/src/agent-loop.tsL508executeToolCalls()选择执行模式chooses the execution mode
packages/agent/src/agent-loop.tsL586executeToolCallsParallel()并行批the parallel batch
packages/agent/src/agent-loop.tsL530executeToolCallsSequential()顺序批the sequential batch
packages/agent/src/agent-loop.tsL690shouldTerminateToolBatch()全体 terminate 才停止stop only if all terminate
packages/agent/src/agent-loop.tsL478failToolCallsFromTruncatedMessage()截断时的处理handling truncation
packages/agent/src/agent-loop.tsL811runToolCall()无事件地复用同一管道reuse the pipeline without events
08pi-agent-core

AgentEvent 事件流The AgentEvent stream#

一句话结论Takeaway所有可观察的行为都是事件:运行、轮次、消息、工具执行四个层次,严格嵌套。pi-ai 的 12 种流事件在这一层被压缩成 message_start / update / end 三种。Everything observable is an event, in four strictly nested levels: run, turn, message, tool execution. pi-ai's 12 stream event types collapse into message_start / update / end here.

AgentEvent 的嵌套、归约器与 pi-ai 事件映射AgentEvent nesting, the reducer, and the mapping from pi-ai events
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
AgentEvent streamagent_start … agent_end一次运行one runturn_start … turn_end第 1 轮:一次响应 + 其工具turn 1 · one response + its toolsturn_start … turn_end第 2 轮:最终回答turn 2 · final answerAgent.processEvents()先归约,再 await 监听器reduce, then await listenerspi-ai 事件 → AgentEvent(streamAssistantResponse)pi-ai events → AgentEvent (streamAssistantResponse)message_startmessage_endmessage_startmessage_update ×nmessage_endtool_execution_starttool_execution_update ×ntool_execution_endmessage_startmessage_endmessage_startmessage_update ×nmessage_endmessage_start · _updatemessage_endtool_execution_starttool_execution_endturn_endagent_endstarttext_starttext_deltatext_endthinking_startthinking_deltathinking_endtoolcall_starttoolcall_deltatoolcall_enddoneerrormessage_startpush(partial)message_update{ message: {...partial}, assistantMessageEvent }message_endresult() + thinkingLeveluser 提示user promptassistant每个工具调用per tool calltoolResultassistantstopReason: "stop"→ streamingMessage = message→ messages.push(message)→ pendingToolCalls ∪ {id}→ pendingToolCalls ∖ {id}→ errorMessage ← message.errorMessage→ streamingMessage = undefinedpendingToolCalls 每次替换为新 Set(便于 UI 比较)pendingToolCalls is replaced with a new Set each time若流在 start 之前就以 error 结束,循环会补发 message_start;最终消息替换上下文中的 partialif the stream ends before start, the loop emits message_start itself; the final message replaces the partial in context映射maps touser 消息user messageassistant 消息assistant message工具执行tool executiontoolResult 消息toolResult message

要点Key points

  • message_update 同时携带 assistantMessageEvent(原始 pi-ai 事件),UI 可以按 delta 精细渲染。message_update also carries assistantMessageEvent (the raw pi-ai event), so a UI can render per delta.
  • 并行工具模式下 tool_execution_end 按完成顺序发出,但 toolResult 消息按调用的原始顺序发出。In parallel tool mode tool_execution_end fires in completion order, but toolResult messages are emitted in source order.
  • Agent 的归约器在调用订阅者之前更新状态,所以监听器里读 agent.state 总是最新的。Agent's reducer updates state before calling subscribers, so agent.state read inside a listener is always current.

工作原理How it works

  1. 流开始(start):把 partial 推入上下文并发 message_start。Stream start (start): push the partial into context and emit message_start.
  2. 每个 delta:用新的 partial 替换上下文末尾,发 message_update。Each delta: replace the context tail with the new partial and emit message_update.
  3. done / error:取 result(),盖上 thinkingLevel,发 message_end。done / error: take result(), stamp thinkingLevel, emit message_end.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/agent/src/types.tsL516AgentEvent全部事件联合类型the full event union
packages/agent/src/agent.tsL565processEvents()状态归约器the state reducer
packages/agent/src/agent.tsL266subscribe()按顺序 await 的监听器listeners awaited in order
packages/agent/src/agent-loop.tsL153createAgentStream()agentLoop() 的 EventStreamthe EventStream behind agentLoop()
09pi-coding-agent

Steering 与 Follow-upSteering and follow-up#

一句话结论Takeaway运行中用户的新输入有两种语义:steer() 在当前轮工具结束后、下一次请求前插入;followUp() 等到 Agent 本来要停下时才开始新一轮。两个队列都可以一次取一条或全部。New input during a run has two meanings: steer() lands after the current turn's tools and before the next request; followUp() waits until the agent would otherwise stop. Each queue drains one-at-a-time or all at once.

两个队列、三个 steering 轮询点与 follow-upTwo queues, three steering poll points, and follow-up
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
Steering and follow-up queuessteeringQueuePendingMessageQueuefollowUpQueuePendingMessageQueue队列 API(agent.ts)QUEUE API (agent.ts)agent.steer(msg)运行中插话nudge mid-runagent.followUp(msg)做完之后再…after you finish…m1m2m3m1m2m3① 运行开始① run start初次轮询initial poll② turn_end 之后② after turn_end除非 finishTurn → endunless finishTurn → end③ prepareNextTurn之后 · 仅当 ② 为空after it · only if ② empty④ 本该停止时④ would stopgetFollowUpMessages()steer()followUp()clearSteeringQueue()clearFollowUpQueue()clearAllQueues()hasQueuedMessages()peekQueuedMessages()steeringModefollowUpMode← enqueue()one-at-a-time(默认)→ drain() 取 [m1]one-at-a-time (default) → drain() takes [m1]all → drain() 取 [m1, m2, m3]all → drain() takes [m1, m2, m3]← enqueue()one-at-a-time(默认)→ drain() 取 [m1]one-at-a-time (default) → drain() takes [m1]all → drain() 取 [m1, m2, m3]all → drain() takes [m1, m2, m3]getSteeringMessages()外层循环重新开始restarts the outer loop请求 + 工具request + toolsturn_end下一轮next turn一次运行的时间轴timeline of one runcontinue() 从 assistant 末尾恢复且有 steering 时,① 会被跳过一次(skipInitialSteeringPoll)continue() resuming from an assistant tail with steering skips ① once (skipInitialSteeringPoll)peekQueuedMessages() 优先返回 steering,否则 follow-up;reset() 会清空两个队列peekQueuedMessages() returns steering first, else follow-ups; reset() clears both

要点Key points

  • 默认模式都是 one-at-a-time:每个轮询点只取一条,剩下的留到下一轮,避免一次塞入大量插话。Both default to one-at-a-time: each poll takes one message and leaves the rest for later turns, so a burst of nudges doesn't arrive at once.
  • steering 有三个轮询点:运行开始、turn_end 之后、prepareNextTurn 之后(仅当前一次为空)。Steering is polled at three points: run start, after turn_end, and after prepareNextTurn (only if the previous poll was empty).
  • continue() 遇到末尾是 assistant 时会先消费 steering,否则消费 follow-up,两者都空则抛错。continue() with an assistant tail consumes steering first, then follow-ups, and throws if both are empty.

工作原理How it works

  1. steer(m) / followUp(m) 只是 enqueue,不会打断正在进行的流或工具。steer(m) / followUp(m) just enqueue; they never interrupt the in-flight stream or tool.
  2. 循环通过 getSteeringMessages / getFollowUpMessages 钩子拉取,Agent 用两个私有 PendingMessageQueue 实现它们。The loop pulls through the getSteeringMessages / getFollowUpMessages hooks, which Agent implements with two private PendingMessageQueues.
  3. 取到的消息作为 user 消息写入上下文并发出 message_start/end,然后开始新一轮请求。Drained messages enter the context as user messages with message_start/end, then a new request starts.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/agent/src/agent.tsL143PendingMessageQueueone-at-a-time | allone-at-a-time | all
packages/agent/src/agent.tsL299steer()运行中插话nudge mid-run
packages/agent/src/agent.tsL304followUp()停下之后再做after it stops
packages/agent/src/types.tsL55QueueMode队列模式queue mode
10pi-agent-core

StreamFn 与代理StreamFn and the proxy#

一句话结论TakeawayAgent 从不直接依赖某个 provider:它只调用一个 StreamFn。可以传入 models.streamSimple,也可以用 streamProxy 把请求转发到你自己的服务端——客户端从 SSE 事件重建 partial,得到与直连相同的 AssistantMessageEventStream。The agent never depends on a provider directly: it only calls a StreamFn. Pass models.streamSimple, or use streamProxy to forward requests to your own server — the client rebuilds the partial from SSE events and gets the same AssistantMessageEventStream as a direct call.

StreamFn 的解析与 streamProxy 的往返How the StreamFn is resolved, and the streamProxy round trip
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
StreamFn and the proxy streamAgentOptions.streamFn构造时传入passed at constructionagent.streamFunction传给 runAgentLoop()handed to runAgentLoop()getDefaultStreamFn()未设置时抛错throws if none setsetDefaultStreamFn()由宿主安装installed by a hostStreamFn(model, TranscriptContext, SimpleStreamOptions?)→ AssistantMessageEventStream | Promise<…>约定:不抛异常 · 失败编码为 error 事件 + stopReason "error" / "aborted"contract: never throw · failures = error event + stopReason "error" / "aborted"models.streamSimplepi-ai Models · coding-agent 用 ModelRuntimepi-ai Models · coding-agent uses ModelRuntimestreamProxy(model, ctx, opts)proxy.ts · { authToken, proxyUrl }POST {proxyUrl}/api/streamAuthorization: Bearer <authToken>你的代理服务your proxy server运行 pi-airuns pi-aidata: {event}SSE · 无 partialSSE · no partialprocessProxyEvent(e, partial)在客户端重建 partialrebuild partial client-sideAssistantMessageEventStream与直连相同 · 带 durationMs(1.1.0)same as direct · gets durationMs (1.1.0)??直接调用direct经服务端via a serverstreambody: { model, context, options }options = 11 serializable fields:temperature · samplingParams · maxTokensreasoning · cacheRetention · sessionIdheaders · metadata · transportthinkingBudgets · maxRetryDelayMs失败 → error 事件:failures → error event:!response.ok → "Proxy error: …" · 无 done/error 的 EOF → "Connection closed by proxy server…" · abort → reader.cancel() → aborted!response.ok → "Proxy error: …" · EOF without done/error → "Connection closed by proxy server…" · abort → reader.cancel() → aborted1234

要点Key points

  • 解析顺序:AgentOptions.streamFn → setDefaultStreamFn() 安装的全局默认 → 都没有则抛出 “No default stream function configured”。Resolution: AgentOptions.streamFn → the global default from setDefaultStreamFn() → otherwise throw “No default stream function configured”.
  • 1.1.0 破坏性变更:streamProxy 与所有 StreamFn 必须返回 AssistantMessageEventStream(以便统一计时 durationMs)。Breaking in 1.1.0: streamProxy and every StreamFn must return AssistantMessageEventStream (so durationMs is timed uniformly).
  • 代理只序列化 11 个可序列化选项;服务端发的事件不含 partial,以节省带宽。The proxy serializes only 11 serializable options; server events omit the partial to save bandwidth.

工作原理How it works

  1. streamProxy(model, ctx, { proxyUrl, authToken }) 向 {proxyUrl}/api/stream 发 POST。streamProxy(model, ctx, { proxyUrl, authToken }) POSTs to {proxyUrl}/api/stream.
  2. 读取 SSE data: 行,processProxyEvent() 把每个事件应用到本地 partial 上并推入流。Read SSE data: lines; processProxyEvent() applies each event to the local partial and pushes it.
  3. HTTP 错误、EOF 无终止事件、abort 都被编码成 error 事件,不会抛出。HTTP errors, EOF without a terminal event, and aborts are all encoded as error events — never thrown.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/agent/src/proxy.tsL107streamProxy()代理 StreamFnthe proxy StreamFn
packages/agent/src/proxy.tsL257processProxyEvent()客户端重建 partialrebuild the partial client-side
packages/agent/src/proxy.tsL91buildProxyRequestOptions()可序列化选项serializable options
packages/agent/src/stream-fn.tsL15getDefaultStreamFn()未配置时抛错throws when unset
packages/agent/src/types.tsL33StreamFn不抛异常的约定the never-throw contract
11pi-agent-core

数据类型Data types#

一句话结论TakeawayAgentMessage 是 pi-ai Message 与可声明合并的 CustomAgentMessages 的联合:宿主可以把自己的消息类型(如 artifact、通知)放进会话,只要 convertToLlm 知道如何把它们转成 LLM 消息。AgentMessage is the union of pi-ai Message and the declaration-mergeable CustomAgentMessages: hosts can put their own message kinds (artifacts, notifications) into the session, as long as convertToLlm knows how to turn them into LLM messages.

核心类型与它们的关系Core types and how they relate
← 左右滑动查看,点击放大 →← swipe to scroll, tap to enlarge →
pi-agent-core data types«interface»AgentState+systemPrompt (readonly)+model · thinkingLevel+tools · messages (copy on set)+isStreaming · streamingMessage?+pendingToolCalls: Set<string>+errorMessage?«interface»AgentContext+messages: AgentMessage[]+tools?: AgentTool[]«union»AgentMessage|Message (pi-ai)|CustomAgentMessages[K]«merge»CustomAgentMessages声明合并扩展extend by declaration merging如 artifact · notificatione.g. artifact · notification«pi-ai»Message|SystemMessage · UserMessage|AssistantMessage|ToolResultMessage«extends Tool»AgentTool+name · description · parameters+label · outputSchema?+executionMode? · replay?+prepareArguments?(args)+execute(id, params, signal, onUpdate) → Promise<Result>AgentToolResult<T>+content: (Text | Image)[]+details: T+structuredContent? · usage?+isError? · terminate?AgentToolCallOutcome+toolCall · result · isError+durationMs? (1.1.0)«interface»AgentLoopConfig+…SimpleStreamOptions+model+convertToLlm (required)+transformContext? · getApiKey?+prepareRequest? · prepareNextTurn?+finishTurn? → end | continue+beforeToolCall? · afterToolCall?+getSteeringMessages?+getFollowUpMessages?+toolExecution? = "parallel"BeforeToolCallResult+block? · reason?+terminate?AfterToolCallResult+content? · details? · isError?+usage? · terminate?+structuredContent?快照snapshottoolsexecute 返回execute returns钩子返回hooks return持有 / 返回owns / returns派生 / 联合成员derived / union memberpi-agent-core由宿主提供provided by the host钩子结果hook resultspi-ai

要点Key points

  • AgentState 的 tools 与 messages 赋值时复制数组;pendingToolCalls 每次替换为新的 Set,方便 UI 做引用比较。AgentState.tools and messages copy on assignment; pendingToolCalls is replaced with a new Set each time so UIs can compare by reference.
  • AgentTool 继承 pi-ai Tool,增加 label、execute()、可选 prepareArguments() 与 executionMode。AgentTool extends pi-ai Tool with label, execute(), optional prepareArguments() and executionMode.
  • AfterToolCallResult 按字段合并:替换 content 但不给 structuredContent 时,旧的 structuredContent 会被丢弃,避免两者不一致。AfterToolCallResult merges field by field: replacing content without structuredContent drops the old structuredContent, so the two never disagree.

工作原理How it works

  1. 扩展消息类型:declare module "@earendil-works/pi-agent-core" { interface CustomAgentMessages { … } }。Extend messages: declare module "@earendil-works/pi-agent-core" { interface CustomAgentMessages { … } }.
  2. 工具返回 AgentToolResult<T>:content 给模型,details 给 UI / 日志。Tools return AgentToolResult<T>: content for the model, details for UI / logs.

关键文件Key files · v1.1.0

文件File符号Symbol作用Role
packages/agent/src/types.tsL382AgentState公开状态public state
packages/agent/src/types.tsL365CustomAgentMessages声明合并扩展点declaration-merge extension point
packages/agent/src/types.tsL466AgentTool工具定义tool definition
packages/agent/src/types.tsL424AgentToolResultcontent + detailscontent + details
packages/agent/src/types.tsL449AgentToolCallOutcome含 durationMswith durationMs
packages/agent/src/types.tsL66BeforeToolCallResultblock / terminateblock / terminate
12pi-agent-core

扩展点Extension points#

streamFn替换模型调用:直连、代理、录制回放、测试桩。Swap the model call: direct, proxy, record/replay, test stubs.
convertToLlm · transformContext自定义消息 → LLM 消息;压缩、裁剪、注入上下文。Custom messages → LLM messages; compaction, pruning, context injection.
beforeToolCall · afterToolCall权限审批、阻止、改写结果、终止。Approval, blocking, rewriting results, terminating.
prepareRequest · prepareNextTurn · finishTurn每轮改模型 / 工具 / 选项,决定是否继续。Change model / tools / options per turn and decide whether to continue.
getApiKey每次请求取凭据(短期 OAuth token)。Fetch credentials per request (short-lived OAuth tokens).
CustomAgentMessages声明合并,增加会话里的消息种类。Declaration merging adds message kinds to the session.
AgentTool.prepareArguments · executionMode修正模型给出的参数;强制顺序执行。Repair model-supplied args; force sequential execution.
subscribe()按顺序 await 的事件监听器,拿到运行的 AbortSignal。In-order awaited listeners that receive the run's AbortSignal.
13pi-agent-core

测试Tests#

一句话结论Takeaway4 个测试文件覆盖循环(钩子顺序、队列、并行工具、截断)、Agent 生命周期、端到端与代理流。Four test files cover the loop (hook order, queues, parallel tools, truncation), the Agent lifecycle, end-to-end runs and the proxy stream.

用例数 = 源码中 it( / test( 的出现次数(静态统计,不运行测试)。Cases = occurrences of it( / test( in the source (static count; tests were not run).

14pi-agent-core

交叉链接Cross-links#