08 · Tools 全管道:参数、许可、执行、输出与并发¶
工具是 Agent 最直接影响外部世界的入口。DSH 不是把 {name,schema,callback} 直接交给模型后 invoke:它有 canonical JSON value、pure renderer、scope、permission、around-dispatch、post-policy、finalization 和结果通知。本章研究真实 ToolRuntime 与 defineTool,不把官方接口想象成 Pi ToolResult。
8.1 ToolDefinition 的完整职责¶
输入 schema 在 parameters;工具 execute(args,exec) 返回 canonical JSON value;output.schema 验证成功值,output.render(args,value) 返回 Native/model ContentBlocks,presentationMeta 可提供 direct top-level UI JSON。结果 {isError:false,value,content,…} 由 registry 构造,工具不能自己随手返回一份 result 当 canonical value。定义。
这保证 Native 调用读 content,PTC 程序内部调用读 value,两者共用工具逻辑。render 必须 pure,只依赖 validated args/value;不要在 render 再写数据库,也不要把 meta 内 UI diff 当作模型结果。presentCall/presentResult 读取 durable args/result,可以重复 replay,不应有副作用。
可选 metadata:timeoutMs 是 cooperative deadline,需要 timeout-policy plugin 实际执行;isConcurrencySafe 必须 pure true 才 opt-in overlap;deferLoading 标记 model schema loading;projectContent 是 post-policy 前内容投影;finalizeContent 是每个 normalized outcome 最后内容变换、必须 total。schema projection 只发 name/description/parameters/deferLoading,不把这些运行时 callback/timeout 暴露成模型参数。
8.2 defineTool 的参数和输出 DSL¶
defineTool 将 author-facing parameter property map 编译为隐式 object root;每个属性 required:true 表示必填,不写为可选。根 object 默认 open,不能假设所有未声明 extra fields 都被拒绝。嵌套 object 必须显式 additionalProperties true/false;value schema 的 json 节点表示任意受支持 JSON,编译为 annotation-only raw schema。DSL 编译。
compiler 用显式 task stack,避免深递归栈;seen 检查循环;支持有限的 raw JSON Schema subset,不是完整 ajv 所有 keyword。type inference 最多 16 container levels,之后降到 JsonValue;这只是 TS 推断上限,不表示程序输入限制同为 16。
defineTool 捕获 userExecute 等 callbacks 和编译 schema;execute wrapper 先 validate,错误 ToolArgsError(INVALID_ARGS),再调 typed userExecute。isConcurrencySafe 先 soft validate,invalid args 默认 exclusive;presenters 对旧日志 input soft validate,错了返回 undefined 给 generic UI,执行路径则严格拒绝。defineTool 全文。
概念性例子如下,完整可运行版本以实验目录锁定依赖和测试为准:
const summarize = defineTool({
name: 'summarize_record',
description: 'Summarize one already loaded record.',
parameters: { text: { type: 'string', required: true } },
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
isConcurrencySafe: () => true,
async execute({ text }, exec) {
exec.signal.throwIfAborted()
return text.slice(0, 120)
},
})
ctx.tools.register(summarize)
注意限制字符串 120 是这个教学工具的业务行为;生产中可调选择应变 Config,而不是塞固定值进框架插件。
8.3 register、scope、restrict 与 guard¶
register 在 effect 中写 global 或 calling-agent scoped layer;重复同层名称抛错,scoped 同名遮蔽 global;run_code 永久 reserved,不能因当前 native mode 未使用就占名。output 声明必须存在,schema 要受支持;timeoutMs positive finite。register。
restrict 必须 agent.ctx scoped context,global restriction 会伤所有 Agent 因此拒绝;空 {} no-op 也拒绝;allow/deny 名称检查已知工具。它掩蔽全局工具,与 scoped override 组合后构造 visible view;不是把注册器里全局 definition 物理删除。execution guard 是 waterfall 之后的 monotonic denial,各 guard 只能拒绝,不能 force-allow 另一个 guard 拒绝的调用。限制/guard。
8.4 第一步 createExecution:身份与 lossless 参数¶
从 input 捕获 callId/name/agent/parent/signal/rootCallId,分配 opaque Symbol token。rootCallId 缺省为自己 callId;PTC nested parent 只带 opaque token,不向观察者泄露外层 mutable exec。
arguments 经过 snapshotJsonValue 并 deepFreeze,pre-policy 不拥有改写参数入口。工具名、caller、token 是 readonly,around-dispatch 唯独允许换 signal;还记录原 callerSignal 与 bodyInvoked,以免 wrapper 通过换 signal 抹掉取消。createExecution。
projectContent/finalizeContent 在 snapshot 参数之前捕获,甚至 arguments getter 导致 registry callback 更新也不会换掉本次捕获的 callback。PTC mode 中“存在但 model-direct 不可调用的 native tool”在 policy 之前拒绝 UNKNOWN_TOOL,告诉模型必须从 run_code 内调用;真正未知名仍进入历史 dispatch UNKNOWN_TOOL 路径。参数无法 lossless snapshot 属于 final-result,不走正常 post-policy。pre-aborted 正常调用也直接 final error。
8.5 三层 waterfall 与不可跳过的收尾¶
图中是主要正常/denial 路径;create/pre/around 的 pipeline throw 会绕过 post-execute 直接 final-result。body throw、未知 tool 和 INVALID_ARGS 被 dispatchToolBody normalized 后一般仍进入 post-policy。不要宣称“每条失败都会走三层 waterfall”。完整 prepare/dispatch。
8.6 pre-execute:允许、拒绝、取消、询问¶
默认 next 返回 allow;direct deny(reason,info?) 不调用 next;cancel 是 canonical cancellation error;ask 调 approval seam,只有 allowed-once 变 allow。approval 服务缺少、没有 agent routing、channel unavailable、用户 rejected/cancelled 各给明确结果。ask 没服务不能自动同意。PreToolDecision,approval 映射。
pre-input 已 immutable,也已在原 tool/call raw args 里记录,因此 没有 rewrite args decision。旧知识里“在 tools/pre-execute 替换 exec.arguments 再执行”不属于当前 API。若产品需要先准备另一个参数,应该在模型/专用准备接口有日志解释,不能把已经记录给 UI 的调用偷换。
pre 返回 allow 后 guard 仍可能 deny;批准过程中 abort 后也复查 caller cancellation。deny/cancel 转 post-result 允许 post-policy 对反馈做处理,pipeline error 则 final-result。
8.7 execute:包装 body,不可取消原 caller 取消¶
tools/execute 默认 next 调 dispatchToolBody。wrapper 可以暂时将 exec.signal 换成 deadline signal,但 registry 在 body 开始时 fuse caller 与 wrapper,任意一方 abort 都传下去,settle 后去掉 relay listeners 并还原 wrapper signal。signal fusion。
resolveExecution 在 body 开始时重新读 registry,而不是 schema assembly 时永久捕获 definition;不可见或未知 ToolNotFoundError。bodyInvoked 在 tool.execute 前标记,然后 await body,snapshot/validate canonical output、render、meta。body throw normalized error,成功但 signal aborted 变 ABORTED。取消发生在 body 前是 ABORTED_BEFORE_DISPATCH。工具若返回 structured error,取消不会随便把全部错误身份覆盖。body 全文。
around wrapper 若直接返回自己 authored success result,normalizeDispatchResult 会按该 tool output contract 从 result.value 重验和重渲染,不能绕过 output schema。registry canonical result 用 WeakMap 的 exact token 证明,不是 result.isError=false 就信任。
8.8 output value、content、meta 与 canonical check¶
createSuccessResult 做 lossless snapshot value,validate output.schema,deepFreeze,然后 pure render 和 snapshotProjection。presentationMeta 只对无 parent 的 top-level direct call 求值。成功 value 可以是 object/array/string/scalar,不能是 Date、undefined、class 或函数。
canonicalResults 只标记具体 execution token 的 normalized result;同对象借到另一个 call 不算 canonical。materializeFinalResult 再对 content/meta/additionalContexts 复制冻结,成功 value 保留已有验证冻结身份,失败 result 没有 success-only concludesTurn/value。成功与 result normalization。
这不是“输出 JSON 就安全”:字段语义仍由业务工具检查,renderer 要正确,外部 side effect 要有自己幂等/事务。schema 只是保证读者、PTC、UI 与模型看到的 canonical 值都可表示并符合工具声明。
8.9 post-execute:结果变换与 corrective block¶
默认 next 返回 accept。accept 可以替换 content 或 value(二选一),附加 additionalContexts;不能同时带 value/content,失败结果也不能 replace value。value replacement 再走 output.schema/render;content replacement 不改 canonical value,使 PTC 程序值与模型展示可有 intentional 差别。
block(feedback) 把 outcome 变 error,只携带 blocking decision 明确提供的 contexts;工具 body defer 的 contexts 不保留在 blocked result。accept 合并 body contexts 和 decision contexts。post listener throw normalized final error。post 全文。
projectContent 在 post-policy 之前安装 execution-prepared content;policy 仍 authoritative。finalizeContent 位于后面 last-mile,只允许改 content,不能偷换 error/value/meta;作者契约要求 total 不抛,当前 registry 对意外 throw 仍尝试 error materialization,不能因此鼓励用 throw 做政策拒绝。
8.10 result 通知与 deferred context 的提交¶
finish 对 normalized result materialize,apply captured finalizer,再 materialize,freeze exec,然后 tools/result逐 listener contained;返回的 result 与观察者收到的 authoritative frozen snapshot 一致。普通 observer throw/reject 不改变 outcome。若更外层 internal dispatch itself throw,scheduler 仍有 step recovery。
exec.deferContext 保存 UserMessage 在当前 execution 的 result 上;loop 先写 tool/result,再将 additionalContexts 入 next-step inbox,下一 admitted request 才写 user/message。nested composite 必须把 inner result.additionalContexts 向外 ferried,否则会丢上下文;PTC transport 做这项事情。不能在每个并行 tool body 中直接把上下文乱序 append 到 model surface。结果通知,scheduler context。
exec.concludeTurn 记录 WeakSet 标记,只对 final success 生效;被 block/error/cancel 的执行不能终止一轮。nested authoritative success 要由 composite 明确转交,不能因为某 inner callback说“完成”就跳掉父工具其余工作。
8.11 并发是声明,也是运行时逐调用决策¶
默认 exclusive;定义 isConcurrencySafe(args) 返回真且没有 throw 才 parallel;defineTool invalid args 也 exclusive。registry executionMode read 最新 definition,因此一个模型消息中 unstarted tool 可能在前面工具更新 registry 后变成 barrier。
pool 只有 body/around-dispatch 并发,pre-policy、post-policy、result/context commit 都按 model order。parallel-safe 工具不应改 parent-owned状态;如果共享状态,其写入必须 commute 或 fail closed。代码不替你证明 read-only,也不会把 advisory flag 变成数据库串行事务。分类方法,scheduler。
8.12 PTC 与 main/release 的精确差异¶
native 发原 visible schemas;ptc 发 run_code 和 generated SDK,model-direct native 名拒绝,nested带parent的 SDK call仍走每一层工具许可/输出验证;both 发两种形式。timeout/ptc language renderer 缺配置会明确报错,不应偷偷降回 native。
rc.2 和 main 都要求 run_code 的 description 与 code。main 最新变动强调 description 在 code 前的 schema/说明展示顺序;这是生成顺序,不是 JSON 对象键顺序的语法强校验。main 去掉 ToolsInvariant companion,核心输入/输出验证仍保留,不能把删除诊断套件误读成“工具完全没有验证”。PTC 本体,DSL 与工具管道。
8.13 自建工具上线前需要验证什么¶
下面是自建工具上线前的建议验证清单:输入无效时业务 body 没执行;输出无效时 normalized error;ask 没 approval 时拒绝;caller cancel 无法被 wrapper signal 覆盖;timeout 结束时 body 真的收敛;并发结果顺序与 contexts 顺序;block 不保留 body-only contexts;conclude 只对 final success;scope override 和 restriction 真有效;卸载后贡献消失;从 durable replay 得到相同 presentation。
课程实验只覆盖其中一部分。examples/test/contracts.test.mjs 在真实 rc.2 Tools/Cordis 中验证输入拒绝、输出 schema 拒绝、pre-execute 拒绝不进入 body、post-execute block 不回滚副作用、已取消 signal 阻止 dispatch、waterfall 等待真实异步下游,以及服务/工具/prompt 的卸载与恢复。当前实验没有验证 ask 缺 approval、wrapper 与 caller signal 合并、运行中 timeout 收敛、并发 contexts/result 顺序、block 去除 body-only contexts、conclude 成功条件、scope override/restriction 或 durable replay presentation;这些结论在本章来自固定源码阅读,不能算作已运行测试。课程整体的实际测试数量、命令与日志以实验章和审查报告记录为准,不能把整体通过数量解释为本清单逐项通过。