深色模式
02 · pi-agent-core — 有状态 Agent 运行时
包路径:
packages/agent(registry 名@earendil-works/pi-agent-core) 定位:在pi-ai之上构建"有状态 agent + 工具执行 + 事件流"。只有 6 个源文件,是理解 Pi agent loop 的最短路径。
1. 文件清单(全部 6 个,都该读)
| 文件 | 作用 |
|---|---|
src/index.ts | 导出 |
src/agent.ts | Agent 类(状态容器 + 生命周期 + 队列) |
src/agent-loop.ts | runAgentLoop / runLoop(loop 主逻辑、工具执行、事件发射) |
src/stream-fn.ts | StreamFn 默认实现 |
src/proxy.ts | 代理/封装辅助 |
src/types.ts | AgentMessage / AgentTool / AgentEvent / 各种 hook 类型 |
2. 两个核心类型(来自 types.ts + agent.ts)
AgentMessage:agent 内部用的消息,可含 UI 专用类型(通过声明合并扩展)。role 有system/user/assistant/toolResult,外加自定义。Message(来自pi-ai):LLM 只懂user/assistant/toolResult。
转换边界:
AgentMessage[]
├─ transformContext() (可选, AgentMessage[] → AgentMessage[]) 裁剪/注入上下文
└─ convertToLlm() (必选, AgentMessage[] → Message[]) 过滤 UI 消息、转标准格式
→ 交给 streamFn 发给 LLM1
2
3
4
2
3
4
默认 convertToLlm(agent.ts 的 defaultConvertToLlm)只是按 role 过滤,保留 system/user/assistant/toolResult。
3. Agent 类(agent.ts)
职责:持有当前 transcript(_state.messages),发射生命周期事件,执行工具,并提供 steering / follow-up 队列。
- 状态:
messages、tools、model、thinkingLevel、isStreaming、pendingToolCalls、errorMessage(getter/setter 拷贝顶层数组,避免外部篡改)。 - 事件订阅:
subscribe(listener)—— listener 收到AgentEvent与当前AbortSignal,在agent_end后才算 idle。 - 队列:
steer(msg):在当前 assistant turn 结束后立即注入(实时插话)。followUp(msg):仅在 agent 本应停止后运行(追加任务)。PendingMessageQueue支持one-at-a-time/all两种 drain 模式(steeringMode/followUpMode)。
- 生命周期方法:
prompt()(新消息)、continue()(从当前 transcript 续跑)、reset()(清空但保留 system 基线)、abort()、waitForIdle()。 - 钩子(经
AgentOptions注入):beforeToolCall/afterToolCall(权限检查、结果改写)、finishTurn(决定本轮是否结束 / 继续)、prepareRequest/prepareNextTurn(请求前/下轮前的上下文与模型调整)、transformContext/convertToLlm。
4. Loop 主逻辑(agent-loop.ts)
runAgentLoop → runLoop,核心是双层循环:
while (true) { // 外层:follow-up 消息到达后继续
while (toolCalls 或 pendingMessages) { // 内层:处理 tool call + steering
// 1. prepareNextTurn(如 compaction,耗时操作;其间可拾取 steering)
// 2. 处理 prepared + queued 消息,发射 message_start/end
// 3. prepareRequest(可改 model / context)
// 4. streamAssistantResponse(...) → 内部 convertToLlm 后调用 streamFn
// 5. 若 stopReason 为 error/aborted → 结束
// 6. 有 toolCall → executeToolCalls(并行/串行)
// 7. finishTurn 决策:action "end" → agent_end 返回;"continue" → 显式续跑
}
// 8. 本应停止:查 follow-up 队列,有则继续,否则退出
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
关键细节:
streamAssistantResponse:先transformContext(可选)再convertToLlm(必选),解析 apiKey(支持过期 token 重取),调用streamFunction,把流事件映射为message_start/update/end。- 工具执行:
toolExecution: "parallel" | "sequential";单个 tool 标executionMode: "sequential"也会强制串行。executeToolCallsParallel用Promise.all并发,executeToolCallsSequential逐个。 beforeToolCall/afterToolCall:每次工具调用都过这两道钩子(权限 / 改写结果)。runToolCall供"工具调用其它工具"时复用同一套钩子。- 截断保护:
stopReason === "length"时,所有 tool call 直接判失败(参数可能截断,不可执行),让模型重发。 declareToolChanges:每轮前计算context.tools(可执行的)与 transcript 中 system message 声明的(模型可调用的)之差,生成toolsAdded/toolsRemoved,保证 replay 始终等于可执行集合。- 错误不 reject:未知工具、校验失败、被拦截、抛异常,全部以
isError:true的 tool result 返回,loop 继续。
5. 事件流(结合 agent.ts 的 processEvents)
prompt()
├─ agent_start
├─ turn_start
├─ message_start (user)
├─ message_end (user)
├─ message_start (assistant) // 流式开始
├─ message_update (text/thinking/toolcall delta)
├─ message_end (assistant) // 完整响应
├─ tool_execution_start / _update / _end (每个工具调用)
├─ turn_end (message, toolResults)
└─ agent_end (messages) // 仅表示不再发事件;listener 未 settle 前不算 idle1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
6. 阅读清单
README.md(包内)的 Core Concepts + Event Flow(先看)。src/types.ts—— 吃透AgentMessage/AgentTool/AgentEvent/ hook 类型。src/agent.ts——Agent类、subscribe、steer/followUp、runWithLifecycle/processEvents。src/agent-loop.ts——runLoop双层循环、streamAssistantResponse、executeToolCalls*、before/afterToolCall、declareToolChanges。src/agent-loop.test.ts/agent.test.ts—— 用 faux provider 跑的最小示例,验证理解。
7. 自测
steer与followUp的语义区别?各自在什么时机被 drain?convertToLlm与transformContext分别解决什么问题?哪个是必选?- 一条 assistant 消息带 3 个并行 tool call,其中一个
terminate:true,loop 会怎样? stopReason:"length"时为什么不能直接执行 tool call?