前两章讲了 Event 和 Tool。本章回答更实用的问题:
我想改 Agent 行为,应该选哪个扩展点?
关键不是“哪个 Event 看起来最接近”,而是你想修改的数据处于哪一层。
1. 先按数据层选择
flowchart TD
Q{"要改什么?"}
Q -->|用户原始输入| I["input"]
Q -->|本次系统提示| B["before_agent_start"]
Q -->|每次模型消息| C["context"]
Q -->|HTTP Header| H["before_provider_headers"]
Q -->|序列化 Payload| P["before_provider_request"]
Q -->|工具许可| T["tool_call"]
Q -->|模型/Thinking| M["Runtime Action"]
Q -->|压缩| X["compact + session_before_compact"]
Q -->|会话状态| S["appendEntry"]
越靠后修改,越接近 Provider/Tool 的最终形态;越靠前,越能保持高层语义。
2. 修改 System Prompt
对每个用户 Run 增加规则:
pi.on("before_agent_start", (event) => ({
systemPrompt:
event.systemPrompt +
"\n\nUse concise, evidence-backed answers.",
}));
它覆盖当前用户提交触发的整个 Agent Run,包括其中多个 Tool-loop Turn。多个 Handler 链式 处理;下一次用户提交/Agent Run 没有 Override 时恢复 Base Prompt。
若规则来自静态应用配置,直接构建 Base System Prompt 更简单;Extension 适合按项目、 用户输入或运行状态动态改变。
3. 修改用户输入
input 看见 Skill/Template 展开前的 Raw Text:
pi.on("input", (event) => {
if (!event.text.startsWith("?brief ")) {
return { action: "continue" };
}
return {
action: "transform",
text: `请简短回答:${event.text.slice(7)}`,
images: event.images,
};
});
返回 Handled 可完全绕过 Agent。Extension Command 比 Input 更早;命中 Command 时 Input 不会触发。
4. 增加或过滤 Context
每次模型调用前:
pi.on("context", (event) => ({
messages: [
{
role: "custom",
customType: "runtime-facts",
content: "Current incident: INC-42",
display: false,
timestamp: Date.now(),
},
...event.messages,
],
}));
Context Event 的修改不自动写 Session。适合:
- 临时 Runtime Fact;
- 对模型隐藏某些 Custom Message;
- Redact 发送给 Provider 的内容;
- 控制本次 Context Window。
需要审计和恢复的业务状态,应另写 Session Entry。
5. before_agent_start 与 context 怎么选
| 需求 | 选择 |
|---|---|
| 根据用户这次 Prompt 增加规则 | before_agent_start |
| 每一轮 Tool Loop 前都重新计算 | context |
| 修改 System Prompt | before_agent_start |
| 修改 Message Array | context |
| 写入可持久化 Custom Message | before_agent_start 返回 message |
| 仅影响 Provider 可见 Context | context |
一次 Agent Run 可有多个 LLM Turn,所以 context 可能比 before_agent_start 多次触发。
6. 修改 Provider Request
增加 Trace Header:
pi.on("before_provider_headers", (event, ctx) => {
event.headers["x-session-id"] =
ctx.sessionManager.getSessionId();
});
修改 Payload:
pi.on("before_provider_request", (event) => {
const payload = event.payload as Record<string, unknown>;
return { ...payload, temperature: 0 };
});
Payload Shape 由 Provider/API 决定,不是统一稳定 Schema。能在 System Prompt、Context 或 Provider Config 解决的问题,不要无谓地下沉到 Payload Rewrite。
7. 添加请求头的边界
Header Handler 原地修改:
event.headers.authorization = null; // 删除
它适合 Trace、Gateway Attribution 或受控 Header。不要把 Secret 写进日志。
Provider Retry 可能复用同一 Headers,不重新触发 Hook;不能用它实现“每次 Retry 都生成 不同 Nonce”的假设。
8. 切换模型
绑定完成后:
const model = ctx.modelRegistry.find(
"provider",
"model-id",
);
if (model) {
const changed = await pi.setModel(model);
if (!changed) {
// 没有可用认证
}
}
setModel() 返回 Boolean;缺少 API Key 时为 False。只有模型实际变化时才触发
model_select;若切换导致 Clamp 后的有效 Thinking Level 也实际变化,才另触发
thinking_level_select。
模型对象应来自当前 Model Registry,不要凭字符串构造缺字段对象。
9. 控制 Thinking Level
pi.setThinkingLevel("high");
Runner 委托 AgentSession;实际 Level 会按 Model Capability Clamp。需要显示最终值:
pi.getThinkingLevel();
thinking_level_select 是通知,不是可取消 Event。
10. 自动触发 Compaction
pi.on("agent_settled", (_event, ctx) => {
const usage = ctx.getContextUsage();
if (
typeof usage?.percent === "number" &&
usage.percent > 80
) {
ctx.compact({
customInstructions: "Preserve decisions and tool evidence.",
});
}
});
ctx.compact() 不返回待 Await 的 Compaction Result。用 onComplete/onError Callback 或
session_compact Event 观察结果。
必须防递归:Compaction 后的事件链不应无条件再次触发 Compaction。
11. 自定义 Compaction
session_before_compact 可以:
- Cancel;
- 返回完整
compaction; - 观察 Reason、WillRetry、Signal;
- 使用 Branch Entries 和 Preparation。
多个 Extension 都返回 Custom Compaction 时会形成 Owner 冲突。生产应用应只启用一个 Compaction Provider,其他 Extension 只观察。
12. 添加自定义会话数据
pi.appendEntry("desktop-policy", {
schemaVersion: 1,
mode: "readonly",
});
Custom Entry:
- 写入当前 Session;
- 不进入模型 Context;
- 可在同一持久化 Session Resume 时重建状态;
- 不自动跨无关 New Session。
State Schema 要 Versioned;Reload 会重跑 Factory,闭包状态需要从 Entry 恢复。
13. 任务完成后执行操作
发送通知、刷新文件索引或更新外部状态,应监听:
pi.on("agent_settled", async () => {
await updateTaskStatus();
});
不是 agent_end:后者之后可能 Retry、Auto-compaction Retry 或处理 Follow-up。
仍要保证 Idempotency。Extension 触发的新 Run 可能再次 Settled;使用 Run/Session Entry 或 业务 Idempotency Key 去重。
14. 实现 Read-only Mode
最小方案:
const READ_ONLY_TOOLS = ["read", "grep", "find", "ls"];
let mode: "normal" | "readonly" = "normal";
pi.registerCommand("readonly", {
description: "Enable read-only mode",
handler: async (_args, ctx) => {
await ctx.waitForIdle();
mode = "readonly";
pi.setActiveTools(READ_ONLY_TOOLS);
pi.appendEntry("desktop-mode", {
mode: "readonly",
});
},
});
再加最终 Gate:
pi.on("tool_call", (event) => {
if (mode !== "readonly") return;
if (!READ_ONLY_TOOLS.includes(event.toolName)) {
return {
block: true,
reason: "Read-only mode blocks this tool.",
};
}
});
在 session_start 中还应从当前 Branch 的 desktop-mode Entry 恢复 mode;切回 Normal 时
同时恢复此前保存的 Active Tool Set。这里的 Closure State 只服务当前 Extension Instance。
只改 Prompt 不够;只改 Active Set 也不能撤回本 Turn 已生成的 Call。最终执行 Backend 还要 做 Filesystem/Network Policy。
15. 实现 Plan Mode
Plan Mode 通常需要三层:
System Prompt:只分析和规划
Active Tools:只保留读取/搜索
Final Policy:拒绝写入和执行
Extension 可用 Command 切换 Mode、保存 Entry、修改 Tool Set,并在
before_agent_start 追加 Plan Instructions。
Pi 没有一个通用的“PlanMode=true” Core Flag。这里是由现有扩展点组合出的产品模式。
16. Mode 状态机
stateDiagram-v2
[*] --> Normal
Normal --> Plan: command / policy
Plan --> ReadOnly: lock
ReadOnly --> Normal: explicit unlock
state Plan {
[*] --> ReadTools
ReadTools --> ProducePlan
}
每次切换:
- 等待 Idle;
- 持久化 Mode;
- 更新 Active Tools;
- 更新 UI Store;
- 下一次用户提交/Agent Run 由 before_agent_start 注入规则;
- Final Policy 按当前 Mode 检查。
17. 修改 Tool Result
Redaction:
pi.on("tool_result", (event) => ({
content: redactSecrets(event.content),
}));
它发生在 Tool Execute 后、模型继续前。适合保护 Provider Context,但不能撤销 Tool 已经 执行的副作用,也不一定清除 Backend/Console 中已记录的数据。
18. 添加 Custom Message
before_agent_start 返回 Message 会:
- 形成 Session 中的 Custom Message;
- 送入 Agent Context;
- 可控制 Display。
pi.sendMessage() 可以选择:
- 是否 Trigger Turn;
- Streaming 时 Steer/FollowUp/NextTurn。
发送消息可能形成递归 Event Chain,必须标记 CustomType 并检查来源。
19. 为需求选 Hook 的检查表
- 修改对象是 Prompt、Message、Payload、Tool,还是 Session?
- 要持久化还是只影响一次 Provider Call?
- 要观察、修改、Block,还是启动新动作?
- 同一 Run 会触发几次?
- 多个 Extension 时如何合并?
- Handler Error 应 Fail Open 还是 Fail Closed?
- 是否需要 Signal/Timeout?
- 是否会递归触发自己?
- 是否有最终宿主安全检查?
20. 常见误区
“所有 Prompt 修改都放 context”
不对。System Prompt 用 before_agent_start 更直接;Message Array 才用 Context。
“before_provider_request 有稳定通用 Payload”
错误。它取决于 Provider API。
“agent_end 就代表业务完成”
不一定。用 agent_settled,并做 Idempotency。
“Plan Mode 只是一句 System Prompt”
不安全。还要 Tool Set 与 Final Policy。
“appendEntry 会自动把 Mode 告诉模型”
错误。Custom Entry 不进入 LLM Context;需要另行注入。
21. 本章小结
- 扩展点应按被修改的数据层选择;
- Input 改 Raw Text,before_agent_start 改当前 Agent Run 的 System Prompt,Context 改每次 LLM Messages;
- Headers 与 Provider Payload 是更底层、Provider-sensitive 的 Hook;
- Model 与 Thinking 通过 Runtime Action 改变;只有对应有效值实际变化时才发 Notification;
- Compaction Action、Before Event 和 After Event 分别负责触发、定制和观察;
- Custom Entry 保存当前 Session 状态,但不进入模型 Context;
- 任务真正稳定结束看 agent_settled,并保证 Side Effect Idempotent;
- Read-only/Plan Mode 是 Prompt、Active Tools 与 Final Policy 的组合;
- Tool Result Redaction 不能撤销已发生副作用;
- 所有会启动新 Run 的 Handler 都要防递归。
22. 自测
- before_agent_start 与 context 各适合修改什么?
- Provider Payload Rewrite 为什么应谨慎使用?
- setModel 返回 False 代表什么?
- ctx.compact 怎样观察 Completion/Error?
- appendEntry 为什么不能单独让模型知道 Mode?
- 任务完成通知为什么用 agent_settled?
- Read-only Mode 至少需要哪三层?
- Plan Mode 是 Pi 内置 Flag 吗?
- Tool Result Redaction 能否撤销 Tool 副作用?
- 怎样避免 Extension 自己触发无限 Run?