Agent 类与生命周期
Agent 是 @mariozechner/pi-agent-core 对外的有状态入口。下层是裸的 runAgentLoop 循环,上层是 AgentSession 那样的编排层。Agent 在中间负责:持有 transcript 和工具表、管 activeRun 生命周期、维护 steering/followUp 两条待办队列、把 AgentOptions 翻译成 AgentLoopConfig、用 runWithLifecycle 包住真正的循环调用,并通过 subscribe 把事件发给外部 listener。可以把它理解成「循环之上的状态壳」,循环本身不存任何状态。
职责
- 持有状态:
AgentState包含messages/tools/model/thinkingLevel/isStreaming/streamingMessage/pendingToolCalls/errorMessage,构造时由createMutableAgentState初始化。见packages/agent/src/agent.ts:158-188。 - 队列管理:
steeringQueue和followUpQueue各自一个PendingMessageQueue,mode 取"one-at-a-time"或"all"。见packages/agent/src/agent.ts:113-144与packages/agent/src/agent.ts:200-280。 - 生命周期钩子:
beforeToolCall/afterToolCall在构造时存下,装进createLoopConfig传给底层循环。见packages/agent/src/agent.ts:410-436。 - 运行封装:
runWithLifecycle负责创建AbortController、设isStreaming、catch 异常走handleRunFailure、finally里finishRun。见packages/agent/src/agent.ts:438-486。 - 入口三件套:
prompt收新输入、continue从末尾接续、steer/followUp排队。见packages/agent/src/agent.ts:312-353和packages/agent/src/agent.ts:252-259。
设计动机
为什么不直接让上层调 runAgentLoop?因为循环本身是纯函数式的——传进去什么 context,它就跑什么,跑完返回 newMessages,不存任何东西。真实编码助手需要「跨轮的 transcript、流式期间继续打字要排队、abort 要有唯一信号源、失败要把 errorMessage 写进 state」。把这些塞进 runLoop 会让循环变重且不可复用,放到上层又会在 print/rpc/TUI 三种模式重复。Agent 抽出来当唯一状态壳,循环只管跑,状态只管存。
队列分两条而不是一条,是因为 steering(本轮插话,要在下一轮 assistant 回复前注入)和 followUp(本该结束时再起一轮)语义不同——steering 在内层 while 里 getSteeringMessages 拉,followUp 在外层 while 末尾 getFollowUpMessages 拉,见 双层 while 主循环。PendingMessageQueue 的 "all"/"one-at-a-time" mode 让调用方决定一次排空还是只取一条。
关键文件
packages/agent/src/agent.ts:113-144—PendingMessageQueue:enqueue/drain/clear,drain在"all"模式全取,否则只取首条。packages/agent/src/agent.ts:158-207—class Agent字段与构造器,默认streamFn指向streamSimple,toolExecution默认"parallel"。packages/agent/src/agent.ts:312-323—prompt重载:支持AgentMessage、AgentMessage[]、string + images,内部走normalizePromptInput→runPromptMessages。packages/agent/src/agent.ts:355-372—normalizePromptInput:字符串+图片转成带timestamp的 userAgentMessage。packages/agent/src/agent.ts:374-400—runPromptMessages/runContinuation分别调runAgentLoop/runAgentLoopContinue,都包在runWithLifecycle里。packages/agent/src/agent.ts:410-436—createLoopConfig:把实例字段拼成AgentLoopConfig,getSteeringMessages/getFollowUpMessages闭包到队列的drain。packages/agent/src/agent.ts:438-486—runWithLifecycle+handleRunFailure+finishRun。
构造器把可替换件全装上,streamFn 缺省指向 streamSimple:
// packages/agent/src/agent.ts:190-207
constructor(options: AgentOptions = {}) {
this._state = createMutableAgentState(options.initialState);
this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
this.transformContext = options.transformContext;
this.streamFn = options.streamFn ?? streamSimple;
// ... getApiKey / onPayload / onResponse / beforeToolCall / afterToolCall ...
this.steeringQueue = new PendingMessageQueue(options.steeringMode ?? "one-at-a-time");
this.followUpQueue = new PendingMessageQueue(options.followUpMode ?? "one-at-a-time");
this.transport = options.transport ?? "auto";
this.toolExecution = options.toolExecution ?? "parallel";
}createLoopConfig 把队列的 drain 闭包成异步函数,循环通过这两个回调拉消息:
// packages/agent/src/agent.ts:427-434
getSteeringMessages: async () => {
if (skipInitialSteeringPoll) {
skipInitialSteeringPoll = false;
return [];
}
return this.steeringQueue.drain();
},
getFollowUpMessages: async () => this.followUpQueue.drain(),runWithLifecycle 是所有运行入口的公共壳,出错走 handleRunFailure 把错误写进 state 再发 agent_end:
// packages/agent/src/agent.ts:454-461
try {
await executor(abortController.signal);
} catch (error) {
await this.handleRunFailure(error, abortController.signal.aborted);
} finally {
this.finishRun();
}数据流
prompt(text) 的生命周期:
边界与失败
- 并发 prompt 拒绝:
prompt/continue在activeRun已存在时直接抛错,不静默排队。调用方应改走steer/followUp,见packages/agent/src/agent.ts:316-320。 - continue 角色校验:末条消息若是
assistant,优先消费 steering/followUp 队列;两者都空时抛 "Cannot continue from message role: assistant",见packages/agent/src/agent.ts:336-350。 - 失败不丢消息:
handleRunFailure把错误包成一条stopReason: "aborted"|"error"的 assistantAgentMessage追加到 transcript,UI 能直接渲染,见packages/agent/src/agent.ts:463-478。 - reset 清干净:
reset清 messages、流式状态、pendingToolCalls、errorMessage,也清两条队列,见packages/agent/src/agent.ts:301-310。 - abort 单一源:
abort()只调activeRun.abortController.abort(),循环和 stream 都监听同一个 signal,无需多路径协调,见packages/agent/src/agent.ts:287-290。
小结
Agent 把 runAgentLoop 包成有状态对象:持有 transcript、管两条队列、装生命周期钩子、统一 abort 信号。往上看是 AgentSession 那种编排层,往下看是 双层 while 主循环。生命周期钩子如何被工具执行路径消费,看 工具执行 sequential/parallel;状态字段和 AgentMessage/AgentEvent 形状,看 类型契约。