ReAct Loop 时间线映射与事件驱动设计
技术架构白皮书 · 2026年6月
| 层级 | 所在文件 | 核心特征 | 作用 |
|---|---|---|---|
| Agent Loop 层 | agent-loop.ts |
函数回调,直接参与执行逻辑 | 控制工具调用、转换消息格式、决定循环终止 |
| Agent 事件层 | agent.ts |
只读事件流 AgentEvent | 状态同步、UI 更新、日志记录 |
| Harness/Extension 层 | harness/ 目录 |
类型安全的拦截系统 HookEvent | 支持 block/patch/cancel,支持扩展 |
| 回调函数 | 触发时机 | 返回值语义 |
|---|---|---|
transformContext | 消息进入 LLM 前 | 修改后的 AgentMessage[] |
convertToLlm | 序列化为 LLM 请求前 | Message[] 格式转换 |
beforeToolCall | 工具调用执行前 | { block?: boolean } 可阻断 |
afterToolCall | 工具执行完成后 | { patch?: object } 可修改结果 |
prepareNextTurn | 每轮结束后 | AgentLoopTurnUpdate 修改上下文 |
shouldStopAfterTurn | 停止决策点 | boolean 是否终止循环 |
getApiKey | Provider 请求前 | 认证密钥 |
export type AgentEvent =
| { type: "agent_start" }
| { type: "agent_end"; messages: AgentMessage[] }
| { type: "turn_start" }
| { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] }
| { type: "message_start"; message: AgentMessage }
| { type: "message_update"; message: AgentMessage; ... }
| { type: "message_end"; message: AgentMessage } // → 触发 Session 写入
| { type: "tool_execution_start/update/end"; ... }
declare const HookResult: unique symbol;
interface HookEvent<TType extends string, TResult = void> {
type: TType;
readonly [HookResult]?: TResult; // 只在类型层面存在
}
type ResultOf<E> = E extends { readonly [HookResult]?: infer R } ? R : void;
| 时机 | 触发事件 | 写入方式 | 说明 |
|---|---|---|---|
| 消息完成时 | message_end |
立即写入 | 每完成一条消息(user/assistant/toolResult),立即 session.appendMessage() |
| 回合结束时 | turn_end |
批量刷新 | flushPendingSessionWrites() — 写入队列中的 model/thinking/tools 变更 |
| 空闲期操作 | phase = "idle" | 立即写入 | compaction、branch、model切换等结构性操作立即持久化 |
| 消息类型 | 触发时机 | 修改窗口 | 写入内容 | 数据结构 |
|---|---|---|---|---|
Prompt 消息role: "user" |
agent_start 后turn_start 时 |
无修改窗口message_start → message_end 连续触发 |
原始用户输入 | UserMessage {
role: "user";
content: string | (TextContent|ImageContent)[];
timestamp: number;
} |
Assistant 消息role: "assistant" |
LLM 流式响应 完成后 |
流式传输期间持续更新message_update × N流式结束后 message_end |
最终完整响应 (含 thinking/toolCalls) |
AssistantMessage {
role: "assistant";
content: (TextContent|ThinkingContent|ToolCall)[];
api: Api;
provider: string;
model: string;
usage: Usage;
stopReason: StopReason;
timestamp: number;
} |
ToolResult 消息role: "toolResult" |
工具执行 完成后 |
无修改窗口 工具返回后立即生成 |
工具执行结果 (可被 tool_result hook patch) |
ToolResultMessage {
role: "toolResult";
toolCallId: string;
toolName: string;
content: (TextContent|ImageContent)[];
details?: any;
isError: boolean;
timestamp: number;
} |
一旦 message_end 触发,消息内容即视为最终确定,立即写入 AppendOnlyLog,之后不再修改。三种消息的差异在于:
message_update 期间内容持续变化,只有 message_end 时才最终定型并写入tool_result hook 修改)Pi 提供以下官方修改入口,在消息写入前对其进行修改:
| 修改目标 | Hook / 回调 | 修改能力 | 代码示例 |
|---|---|---|---|
| System Prompt 系统指令 |
before_agent_start |
可修改 systemPrompt可注入初始 messages |
harness.on("before_agent_start", async (event) => {
return {
systemPrompt: event.systemPrompt + "\n请记住:简洁回答。",
messages: [{ role: "system", content: "上下文..." }]
};
}); |
| 进入 LLM 的消息列表 (裁剪/注入) |
context 或 transformContext |
可替换整个 messages 数组可添加/删除/修改任意消息 |
harness.on("context", async (event) => {
// 1. 裁剪过长历史
const trimmed = event.messages.slice(-20);
// 2. 注入系统提示
const modified = [
...trimmed,
{ role: "user", content: "请记住:简洁回答。" }
];
return { messages: modified };
}); |
| LLM 请求参数 | before_provider_requestbefore_provider_payload |
可修改 model、streamOptions 可修改序列化后的 payload |
harness.on("before_provider_request", async (event) => {
return {
model: cheaperModel, // 降级模型
streamOptions: { ... }
};
}); |
| ToolResult 内容 | tool_result |
可 patch content可修改 details、isError |
harness.on("tool_result", async (event) => {
if (event.toolName === "read_file") {
return {
content: "[截断] " + event.content.slice(0, 1000),
details: { truncated: true }
};
}
}); |
| 下一轮的上下文 | prepareNextTurn |
可替换整个 context.messages可切换 model/thinkingLevel |
agent.prepareNextTurn = async (ctx) => {
// 修改最后一条消息
const modified = [...ctx.newMessages];
modified[modified.length - 1].content += "[补充]";
return {
context: { ...ctx.context, messages: modified },
model: newModel
};
}; |
prepareNextTurn 替换整条 context.messages 数组不同修改入口对 Session 持久化的影响:
| 友好度 | 入口 | 层级 | Session 一致性 |
|---|---|---|---|
| ⭐⭐⭐ 最友好 | before_agent_starttool_result |
Harness | 修改后的内容正常走完 message_end → session.appendMessage(),Session 记录与实际执行完全一致 |
| ⭐⭐ 次友好 | contextprepareNextTurn |
Agent Loop / Harness | Session 存原始消息,执行看修改后的消息 —— 恢复时会得到与执行时不同的上下文,存在一致性风险 |
| ⭐ 不友好 | before_provider_payload |
Harness | 完全绕过 Session 记录,Session 存的是原始 payload,无法恢复实际发送的内容 |
before_agent_start(注入初始消息)和 tool_result(修改工具返回),避免使用 context 和 before_provider_payload 进行结构性修改。
type AgentHarnessPhase = "idle" | "turn" | "compaction" | "branch_summary" | "retry";
// "turn" 阶段:活跃操作期
if (this.phase === "turn") {
// 变更进入 pending 队列,不立即写入
this.pendingSessionWrites.push({ type: "model_change", ... });
}
// "idle" 阶段:可立即写入
if (this.phase === "idle") {
await this.session.appendModelChange(model.provider, model.id);
}
所有状态变更建模为不可变的 Entry,追加到 Session Tree:
type SessionTreeEntry =
| MessageEntry // 普通消息
| ModelChangeEntry // 模型切换
| ThinkingLevelChangeEntry // thinking 级别变更
| ActiveToolsChangeEntry // 工具集变更
| CompactionEntry // 压缩操作
| BranchSummaryEntry // 分支摘要
| LabelEntry // 标签
| CustomEntry; // 自定义数据
export function buildSessionContext(pathEntries: SessionTreeEntry[]): SessionContext {
// 从 entries 提取配置状态
for (const entry of pathEntries) {
if (entry.type === "thinking_level_change") {
thinkingLevel = entry.thinkingLevel;
} else if (entry.type === "model_change") {
model = { provider: entry.provider, modelId: entry.modelId };
} else if (entry.type === "active_tools_change") {
activeToolNames = [...entry.activeToolNames];
} else if (entry.type === "compaction") {
compaction = entry; // 记录压缩点
}
}
// 重建消息列表(处理 compaction 截断)
if (compaction) {
// 压缩点之前的消息被 summary 替代
messages.push(createCompactionSummaryMessage(...));
// 只保留 firstKeptEntryId 之后的 entries
for (const entry of keptEntries) appendMessage(entry);
} else {
for (const entry of pathEntries) appendMessage(entry);
}
return { messages, thinkingLevel, model, activeToolNames };
}
| 场景 | 默认策略 | 说明 |
|---|---|---|
| 未完成的 Agent Turn | 标记 interrupted |
保守策略,不自动重试,保留队列/pending writes |
| 未完成的 Provider 请求 | 标记 interrupted |
Provider streams 不可恢复,不自动重试 |
| 未完成的 Tool 调用 | 根据 metadata 决定 | 非幂等工具不重试,幂等/可重试工具可重新执行 |
| 缺失的 Active Tools | fail(可配置 drop/disable) |
工具 registry 必须提供兼容实现 |
半持久化架构:Session 存储可序列化的状态和配置(messages、model changes、tool changes),但工具实现、模型对象、钩子逻辑等运行时依赖必须由 Host App 重新注入。
// 未来的恢复 Builder(设计中)
const harness = await AgentHarness.builder()
.env(env)
.session(session) // 提供持久化 session
.model(defaultModel) // 提供默认模型
.tools(runtimeTools) // 提供运行时工具(必须)
.restore({ missingActiveTools: "fail" }); // 恢复策略
Extension 是 Pi 的用户级扩展机制,与三层 Hook 的关系如下:
| 层级 | Extension 可用性 | 使用方式 |
|---|---|---|
| Agent Loop 层 | ❌ 不可用 | 函数回调(transformContext, prepareNextTurn 等)仅供 Host App 内部使用 |
| Agent 事件层 | ✅ 可用(只读观察) | 通过 observe() 订阅 AgentEvent,用于 UI 更新、日志、监控 |
| Harness/Extension 层 | ✅ 主要使用 | 通过 on() 订阅 HookEvent,支持 block/patch/cancel 语义 |
| 能力类别 | API / 事件 | 操作范围 |
|---|---|---|
| 消息修改 (Session 写入前) |
before_agent_start |
修改 systemPrompt,注入初始 message |
context |
替换整个 messages 数组(裁剪/注入) |
|
tool_result |
patch content / details / isError |
|
input |
transform text/images,或标记 handled |
|
| 注册能力 | pi.registerTool() |
注册 LLM 可调用的工具(含自定义渲染) |
pi.registerCommand() |
注册用户命令(如 /my-cmd) |
|
pi.registerShortcut() |
注册键盘快捷键(如 ctrl+k) |
|
pi.registerFlag() |
注册 CLI 参数(如 --my-flag) |
|
| UI 交互 ( ctx.ui) |
select/confirm/input |
对话框交互(选择器、确认框、文本输入) |
setStatus/setWidget/setTheme |
状态栏、组件、主题控制 | |
notify/editor/pasteToEditor |
通知、多行编辑器、粘贴文本 | |
| Session 操作 ( ctx) |
getContextUsage/getSystemPrompt |
读取上下文用量、system prompt |
newSession/fork/navigateTree |
新建 session、分叉、树导航 | |
sendMessage/sendUserMessage |
发送自定义消息、用户消息 |
type ExtensionEvent =
// Session 生命周期
| ResourcesDiscoverEvent
| SessionStartEvent | SessionBeforeSwitchEvent | SessionBeforeForkEvent
| SessionBeforeCompactEvent | SessionCompactEvent | SessionShutdownEvent
| SessionBeforeTreeEvent | SessionTreeEvent
// Agent 执行(可修改的 HookEvent)
| ContextEvent // 修改消息列表
| BeforeProviderRequestEvent // 修改请求 payload
| BeforeAgentStartEvent // 修改 systemPrompt
| ToolCallEvent // 可 block,可 mutate input
| ToolResultEvent // 可 patch result
// Agent 执行(只读观察)
| AgentStartEvent | AgentEndEvent
| TurnStartEvent | TurnEndEvent
| MessageStartEvent | MessageUpdateEvent | MessageEndEvent
| ToolExecutionStartEvent | ToolExecutionUpdateEvent | ToolExecutionEndEvent
| AfterProviderResponseEvent
// 配置变更与用户输入
| ModelSelectEvent | ThinkingLevelSelectEvent
| UserBashEvent | InputEvent;
| 不能做的事 | 原因 |
|---|---|
| 修改已写入 Session 的 Prompt/Assistant 消息 | 设计上不可变,AppendOnlyLog 一旦写入不可更改 |
直接使用 transformContext, prepareNextTurn |
属于 Agent Loop 层,仅供 Host App 内部使用 |
访问原始 Agent 类实例 |
通过 ExtensionContext 封装的安全 API 访问 |
修改 message_update 期间的流式内容 |
AssistantMessage 流式期间不可修改,只有 message_end 后可观察 |
沙盒化扩展模型:Extension 只能通过类型安全的 Harness HookEvent 参与 Agent 生命周期,无法直接操作 Agent Loop 层的执行控制点。这种设计保证了:
Agent Loop 层 = "执行控制点" —— 同步调用,直接返回值影响执行,如 block 工具、修改消息格式、决定停止。
Agent 事件层 = "状态通知流" —— 异步事件,只读观察,如 UI 更新、日志、监控。message_end 触发 Session 写入,turn_end 触发 pending writes 批量刷新。
Harness/Extension 层 = "拦截与扩展系统" —— 类型安全的 HookEvent,支持 block/patch/cancel,与 Agent 事件并行但独立。
状态持久化 = "Append-Only Log" —— 每个消息和配置变更作为不可变 Entry 追加写入,通过归约(reduce)重建当前状态,实现崩溃恢复和会话重建。
一句话总结:Agent Loop 层控制"怎么执行",Agent 事件层通知"发生了什么"并驱动"何时写入",Harness 层决定"是否允许/如何修改",Session 层保证"状态可恢复" —— 四层交织形成完整的可信 Agent 架构。