桌面 Agent 现在要增加数据库查询能力:
模型可以执行只读 SQL;任何写操作必须进入审批,所有调用都要留下审计记录。
这一需求会串起 Tool Definition、Schema、Wrapper、Active Tool、Event Gate 和 Audit。
1. Tool 从哪里进入
flowchart LR
F["Extension Factory"] --> D["ToolDefinition"]
D --> G["RegisteredTool<br/>+ SourceInfo"]
G --> W["wrapRegisteredTool"]
W --> A["AgentTool"]
A --> R["AgentSession Tool Registry"]
R --> S["Agent State Active Tools"]
S --> M["下一次模型请求"]
pi.registerTool() 会刷新 Registry;启动时 Extension Tool 默认进入 Active Set,运行期间
新注册的 Tool 通常也会自动激活,并从下一次模型请求可用。Allow/Exclude 或宿主显式
Active Set 仍可能限制它。
2. defineTool 做什么
const queryDatabase = defineTool({
name: "query_database",
label: "Query database",
description: "Run SQL. Write operations require approval.",
parameters: Type.Object({
sql: Type.String(),
}),
async execute(_id, params, signal, onUpdate, ctx) {
// ...
},
});
defineTool() 主要保留 TypeBox Parameter 的 Type Inference,尤其当 Definition 先赋给变量或
放进 Array 时。它不会注册 Tool,也不会增加运行时 Security。
真正注册仍是:
pi.registerTool(queryDatabase);
3. ToolDefinition 的核心字段
| 字段 | 谁使用 |
|---|---|
name |
模型 Tool Call、Registry Key |
label |
宿主展示 |
description |
模型理解能力 |
parameters |
Tool Argument Schema |
execute |
实际执行 |
executionMode |
Parallel / Sequential |
prepareArguments |
Validation 前兼容转换 |
promptSnippet |
Active 时的默认 Prompt Tool Section |
promptGuidelines |
Active 时的默认 Prompt Guideline |
constrainedSampling |
Provider 侧 Tool Sampling 请求 |
课程不使用自定义 TUI Renderer;桌面 UI 从 AgentSession Event 建立自己的 Tool Card。
4. 参数从模型到 execute
模型 Raw Arguments
→ prepareArguments(可选)
→ TypeBox Validation
→ beforeToolCall / tool_call
→ execute(validatedArgs)
一个重要边界:tool_call Handler 可以在 Validation 后原地改 Input,Core 不再次校验。
因此审批后若还有不可信 Handler,最终执行参数仍可能变化。
桌面 Agent 的最后一道 Policy 应位于受控的最终执行层,检查真正交给 Backend 的参数。
5. 完整数据库 Tool:默认只读,写操作需 Capability
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { defineTool } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function databaseExtension(pi: ExtensionAPI) {
pi.registerTool(
defineTool({
name: "query_database",
label: "Query database",
description: "Run SQL. Write operations require approval.",
parameters: Type.Object({
sql: Type.String({ minLength: 1 }),
}),
executionMode: "sequential",
async execute(toolCallId, { sql }, signal, onUpdate) {
const capability = consumeQueryCapability(
toolCallId,
canonicalizeSql(sql),
);
assertAuthorizedSql(sql, capability);
onUpdate?.({
content: [{ type: "text", text: "Querying…" }],
details: { phase: "query" },
});
return await runAuthorizedQuery(sql, signal);
},
}),
);
}
这些 Capability、SQL 校验与 Query Backend 都是宿主业务实现,不是 Pi 内置函数。只读 SQL
可获得隐式 Read Capability;写 SQL 只有在审批 Gate 为同一
toolCallId + canonical sql 记录一次性 Capability 后才通过 Backend Final Check。
6. execute 的五个参数
toolCallId
params
signal
onUpdate
ctx
- ID:关联 Event、UI 和 Audit;
- Params:Schema Validation 后的参数,但可能已被 Hook 修改;
- Signal:当前 Run 的 Abort Signal;
- onUpdate:发送 Partial Result;
- Context:当前 CWD、Model、Session Read View、Abort/Compact 等。
Tool 应把 Signal 继续传给 Database Driver、Fetch 或 Child Process。只接收 Signal 而不转发, 用户中断仍无法停止底层工作。
7. Tool Result 的形状
return {
content: [
{ type: "text", text: JSON.stringify(rows) },
],
details: {
rowCount: rows.length,
durationMs,
},
};
content会形成给模型的 Tool Result;details主要给应用层/Renderer/Event 使用;usage可记录 Tool 内嵌模型调用;terminate可参与终止整个 Tool Batch 的判断;- Wrapper 还可能添加
addedToolNames。
不要把 Secret 放入 Content;它会进入模型 Context 和 Session Message。
8. Progress Update
onUpdate(partialResult) 产生 tool_execution_update:
sequenceDiagram
participant T as Tool
participant C as Agent Core
participant U as Desktop Store
T->>C: onUpdate(progress)
C->>U: tool_execution_update
T->>C: final result
C->>U: tool_execution_end
Execute 返回后,Core 不再接受迟到 Update,并等待已经发出的 Update Event 完成。桌面 Store
应按 toolCallId 合并 Partial State,而不是把每次 Update 当新 Tool。
9. RegisteredTool 与 Wrapper
pi.registerTool() 保存:
{
definition,
sourceInfo: extension.sourceInfo,
}
wrapRegisteredTool():
- 调用通用
wrapToolDefinition(); - 在执行时注入
runner.createContext(); - 比较执行前后 Active Tool Set;
- 若执行后只新增 Active Tool、没有移除任何旧 Active Tool,将 Name 写进 Result 的
addedToolNames;若同一次变化移除了旧 Tool,则不添加该字段。
Tool Call/Result Interception 不在这个 Wrapper 中,而在 AgentSession 安装到 Agent Core 的 Before/After Tool Hook 中。
10. 动态启用和禁用
const current = pi.getActiveTools();
pi.setActiveTools(
current.filter((name) => name !== "query_database"),
);
setActiveToolsByName():
- 只保留 Registry 中存在的 Name;
- Unknown Name 静默忽略;
- 替换 Agent State Tools;
- 按新 Tool Set 重建 Base System Prompt;
- 改变从下一 Agent Turn 生效。
如果当前 Turn 已经把 Tool Schema 发给模型,动态禁用不能撤回已经生成并进入 Preflight 的 Call;最终 Policy 仍要检查。
11. Allowed 与 Excluded Tool
AgentSession Config 可提供:
allowedToolNames;excludedToolNames。
Registry Refresh 时,Tool 必须在 Allowlist(若存在)且不在 Exclude Set。这个过滤同时作用 于 Built-in、Extension 与 SDK Custom Tool。
Extension 的 setActiveTools() 只能在已经被配置允许的 Registry 内选择,不能绕过宿主
Allow/Exclude。
12. 同名 Tool 的覆盖顺序
内部 Registry 先放 Built-in,再放 Extension Tool,最后放 SDK customTools。Map 后写覆盖:
SDK customTools
> Extension Tool
> Built-in Tool
而多个 Extension 之间先注册者胜出;Resource Loader 为跨 Extension 同名加诊断。
因此注册一个名为 read 的 Extension Tool 可以替换 Built-in Read 的执行 Definition,但
不是自动调用原实现的 Decorator。其他展示层可能按自己的规则回退 Built-in Metadata/
Renderer,不能把执行覆盖泛化成所有层都完整替换。
13. 两种“包装”
Event 包装
tool_call → 前置检查/改参数
原 Tool execute
tool_result → 脱敏/补信息
不需要获得原 Tool Execute Reference,适合 Approval、Audit、Redaction。
同名替换
注册相同 Name
→ Registry 使用 Custom Definition
适合完全提供新 Backend。ExtensionAPI 的 getAllTools() 返回 Metadata,不提供原
execute,所以不能假设同名 Definition 自动帮你调用 Built-in。
14. 数据库审批 Gate
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "query_database") return;
const sql =
typeof event.input.sql === "string"
? event.input.sql
: "";
if (isReadOnlySql(sql)) return;
if (!ctx.hasUI) {
return {
block: true,
reason: "Write query requires interactive approval.",
};
}
const approved = await ctx.ui.confirm(
"Database write",
sql,
{ signal: ctx.signal, timeout: 30_000 },
);
if (!approved) {
return { block: true, reason: "Rejected by user." };
}
grantOneShotQueryCapability(
event.toolCallId,
canonicalizeSql(sql),
);
});
在桌面应用中,ExtensionUIContext 必须由宿主适配为桌面 Dialog;本章不依赖 Pi TUI。
审批后,最终 Database Backend 仍应验证 SQL/Capability。UI Approval 是用户意图,不是 Parser 或 Authorization。
15. 审批内容必须绑定最终参数
Approval Record 至少保存:
toolCallId
toolName
canonical arguments hash
decision
user/session
timestamp
policy version
真正执行前再计算 Canonical Argument Hash。若与批准版本不一致,重新审批或 Block。
这是桌面宿主的安全设计,不是 Pi 已内置的 Approval Record Schema。
16. Audit Event
观察三类 Event:
pi.on("tool_execution_start", recordStart);
pi.on("tool_execution_update", recordProgress);
pi.on("tool_execution_end", recordEnd);
还可监听:
tool_call:当前 Handler 所见的已校验 Input,后续 Handler 仍可能修改;tool_result:实际执行后的 Input 与送回模型前 Result;Block、查找或校验失败时没有该 Extension Event;- Extension Error Listener:记录 Runner 捕获的普通/Result Handler Error;
tool_callHandler Error 直接传播,不会由emitToolCall()发给这个 Listener。
最终参数的权威审计应位于最后一个受控 Gate 或 Backend 执行层;tool_result.input 只能做
执行后的补充记录。tool_execution_start.args 是 Prepare/Validation 前的 Raw Arguments。
审计日志应对 Secret 和大 Result 做 Redaction/Truncation。不要因为“审计”复制一份完整 Credential。
17. Tool Error
以下情况产生 Error Tool Result:
- Name 不存在;
- Argument Preparation/Validation 失败;
- Before Tool Hook Block 或抛错;
- Execute 抛错;
- Agent Core After Tool Hook 抛错;
- 执行前检测到 Abort,或 Tool 自己响应 Signal 并抛错。
Extension 的 tool_result Handler 抛错会被 Runner 记录并隔离,保留当前 Result,不会自动
变成 isError=true。自定义 Tool 成功 Return 一个写着 { error: ... } 的 Details,也不会
自动设置 isError。需要失败语义就让 Execute Throw,或让成功运行的 Result Handler明确
返回 isError: true。
若 Tool 忽略 Signal 并最终成功返回,Core 不能强行抢占它;执行期 Abort 是否成为 Error 取决于底层是否真正响应取消。
18. Sequential 与 Parallel
Definition 可设置:
executionMode: "sequential"
若 Batch 中任何 Tool 标记 Sequential,整个 Batch 走 Sequential;Agent Loop Config 也可 全局指定 Sequential。否则默认 Parallel。
数据库 Transaction、会修改共享文件或依赖兄弟 Result 的 Tool,应慎重选择 Sequential。 但 Sequential 只规定这一 Batch 的执行顺序,不自动提供 Transaction Rollback。
19. Prompt 中怎样出现 Custom Tool
Active Tool 的完整 Schema 通过 Provider Request 提供。coding-agent 的 Base System Prompt 还可使用:
promptSnippet:Available Tools 的一行说明;promptGuidelines:Guidelines Bullet。
未设置 Snippet 的 Custom Tool 不出现在默认 Prompt 的那段文本中,但仍可通过 Provider Tool Schema 被模型调用。只有 Active Tool 的 Prompt Metadata 会参与构建。
20. 一个完整链路
sequenceDiagram
participant M as Model
participant C as Agent Core
participant G as Approval Gate
participant T as Database Tool
participant A as Audit
M->>C: query_database(sql)
C->>A: execution_start
C->>G: tool_call(current mutable input)
G-->>C: allow/block
C->>T: execute(params, signal, update)
T->>A: execution_update
T-->>C: result
C->>C: tool_result patches
C->>A: execution_end
C-->>M: ToolResult Message
21. 常见误区
“defineTool 会自动注册”
错误。它帮助类型推导,仍要 registerTool()。
“Schema Validation 后参数不会再变”
错误。tool_call 可以改,而且不重新校验。
“禁用 Tool 能撤销本 Turn 已生成的调用”
错误。Active Set 主要影响后续 Turn;Final Policy 仍要检查。
“同名 Tool 自动包装 Built-in”
错误。它是替换;Event 才是非侵入式前后包装。
“Approval 通过就等于 SQL 安全”
错误。Backend 仍需 Parser、Authorization 和 Final Argument Binding。
22. 本章小结
- defineTool 保留 Parameter Inference,不负责注册或安全;
- ToolDefinition 经 RegisteredTool、Wrapper 和 Registry 成为 AgentTool;
- Argument 先 Prepare/Validate,再经过可变的 tool_call Chain;
- Execute 要转发 Abort Signal,并可通过 onUpdate 报 Progress;
- Result Content 进入模型 Context,Details 服务应用层;
- Active Tool 改变会重建 Prompt,并从下一 Turn 生效;
- Allowed/Excluded 是宿主 Registry 边界;
- SDK Custom > Extension > Built-in(执行 Definition);Extension 间 First Wins;
- Event 包装与同名替换是两种不同机制;
- 审批要绑定最终 Canonical Arguments,Backend 做最终 Policy;
- Execution/Call/Result Event 共同构成审计链;
- Sequential 不等于 Transaction,也不提供副作用回滚。
23. 自测
- defineTool 与 registerTool 分别做什么?
- Tool Argument 在哪个阶段校验,之后还能否被修改?
- RegisteredTool 为什么保存 SourceInfo?
- onUpdate 怎样映射到桌面状态?
- setActiveTools 对 Unknown Name 怎样处理?
- SDK Custom、Extension 与 Built-in 同名时谁胜出?
- Event 包装与同名替换有什么区别?
- Approval 为什么要绑定 Canonical Argument Hash?
- Tool 成功返回带 error 字段的 Details 会自动 isError 吗?
- Sequential Tool 能否自动回滚之前的副作用?