工具插件注册 schema 与执行器;agent loop 通过 tools/pre-execute → guards → execute → post-execute 执行每次调用。
注册表同时决定工具如何呈现给模型:原生 function calling / Code Mode / 两者。
ctx.tools
10 个 src 文件 · 5800 行(仓库最大核心包)
Service 类插件(默认导出 ToolRuntime)
Config: mode(native|code|both) · maxParallelSubCalls
| 项 | 内容 |
|---|---|
| Service | ToolRuntime extends Service,super(ctx, 'tools');static inject = ['systemPrompt'] —— 自动把工具 schema 喂进提示词组装(index.ts:788,832) |
| Config | mode: 模型呈现方式(native 默认 | code 只送 run_code+SDK | both);maxParallelSubCalls: Code Mode 程序内并发子调用上限(默认 10,1 = 严格串行) |
| 内部接口 | TOOL_RUNTIME_SCHEDULER(unique symbol):agent-loop 并行调度器专用 —— prepare/dispatch/finalize/finish 四段 |
| 错误码 | UNKNOWN_TOOL · INVALID_TOOL_OUTPUT · INVALID_ARGS · UNSUPPORTED_SCHEMA · CODE_RUN_FAILED · ABORTED · ABORTED_BEFORE_DISPATCH |
register(definition: ToolDefinition): () => void presentAs(mode: ToolPresentationMode): () => void // 仅 scoped ctx restrict(filter: ToolRestriction): () => void // 仅 scoped ctx get(name, scope?): ToolDefinition | undefined schemas(scope?): ToolSchema[] // 提示词组装喂给模型 guard(guard: ToolGuard): () => void execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
extends ToolSchema // name / description / parameters output: ToolOutputDefinition // 强制:{ schema, render(args, value), presentationMeta? } execute(args, exec): Promise<unknown> // 只返回 canonical lossless-JSON 值 finalizeContent?(exec, result) // 同步、总函数、只许替换 content timeoutMs?: number // 声明式协作超时预算(永不上模型) isConcurrencySafe?(args): boolean // 仅精确 true 才并发 presentCall?(args): ToolCallView | undefined presentResult?(args, result): ToolResultView | undefined // 纯函数,直播与回放都会调用
ToolExecutionInput:{ callId, rootCallId?, name, arguments, agent?, parent?, signal } —— signal 必填只读ToolExecutionToken:brand 过的 Symbol,仅同进程内做相等关联,永不过模型/日志/worker 边界ToolRunContext:body 收到的上下文,额外 deferContext(context) 与 concludeTurn()ToolExecutionResult:判别联合 —— {isError:false, value, content, meta?, additionalContexts?, concludesTurn?} / {isError:true, error}ToolRestriction:{ allow?: string[], deny?: string[] };决策:allow | deny(reason) | ask(reason?)| 事件 | mode | 签名与 next() 语义 |
|---|---|---|
tools/pre-execute | waterfall | 可重排的 allow/deny/ask 门;next() 委托为 allow;ask 缺 approval 服务时退化为 deny |
tools/execute | waterfall | around-dispatch 包装(超时/重试/指标);next() 返回规范化结果;wrapper 只能换 exec.signal |
tools/post-execute | waterfall | accept / 换 content 或 value / block(反馈变无值失败)/ 附上下文;抛出的工具失败也到达此瀑布 |
tools/code-dispatch-log | waterfall | 只改 run_code 子分派的持久化日志副本;监听器抛错被包含,回退原样记录 |
tools/result | emit | 观察冻结的最终结果;live 事件,别与持久化 tool/result 混淆 |
tools/change | emit | 注册/注销/scope 限制变化;故意不 scope 过滤的全局通知 |
全部经 ctx.waterfall(scopeTarget(this, exec.agent), …) 分派 —— agent-scoped 监听器只收到该 agent 的调用。持久化会话事件(tool/code-dispatch-start / tool/code-dispatch)在 src/types.ts,deriveMessages() 忽略它们 → 子调用永不重入模型上下文。
ToolCancellationState = { callerSignal, bodyInvoked }。body 前 → ABORTED_BEFORE_DISPATCH;
body 后成功结果 → ABORTED;denial/失败/超时保持更具体的结果。从不 abandon 已启动的 promise。
预中止入口:物化+冻结参数后跳过一切政策与分派。
ToolLayer:命名表 tools(同层重名抛错)+ 匿名表 restrictions/guards + 单值单元格 modeview(scope) 是唯一真相源(get/schemas/执行解析共用):inherited = global + 祖先链(近层覆盖远层);整条链每一层都 admits 才可见restrict 只接受 scoped ctx(全局调用抛错);多个 mask 取交集;deny 自动遮掉后来注册的全局工具,allow 排除后来注册的新名 —— 过滤是live 判断,每次 view 重新评估。不是安全/权威边界presentAs 只接受 scoped ctx;非 native 模式同时注册该 agent 的 tools:sdk + tools:code-only prompt section;modeFor 沿链最近者胜| 模块 | 职责 |
|---|---|
schema.ts | 作者面向的 DSL:ValueSchemaSpec(9 种节点)+ ParameterSchemaSpec(隐式开放对象根)+ defineTool(类型推断 + 执行前校验,非法 → ToolArgsError)+ InferValue/InferArgs(16 层容器后回退 JsonValue) |
json-schema.ts | 强制 JSON Schema 子集(工具输出、Code Mode、subagent、workflow 共用);不支持的关键词拒绝而非忽略;校验是总函数、返回路径化违规 |
ts-types.ts | schema → TypeScript 类型文本;生成完整 tools:sdk 节;任何构造退化 unknown,绝不 throw |
py-types.ts | schema → Python 类型文本(TypedDict / Protocol);退化 Any |
defineTool 的三处软校验:execute 前硬校验(ToolArgsError);presentCall/presentResult/isConcurrencySafe 对旧日志回放参数软校验、失败回退 undefined/false 而非崩溃(schema.ts:594-615)。
RUN_CODE_NAME = 'run_code' 无条件保留 —— 任何 agent 可能自选 code mode,不能注册/影子/restrict/移除maxParallelSubCalls;exclusive 的 barrier 覆盖到 post-execute 完成abandon() 拒绝且不记录 start 事件subCallId = <callId>:code:<n>;binding 用 null-prototype 对象承载 __proto__ 等名字;程序只能绑定自己 agent 可见的工具
调用视图 ToolCallView 与结果视图 ToolResultView 是 card 标签联合:generic | terminal | diff | search | read | web(+locations)。
presenter 是纯函数,UI 在直播与日志回放两处调用;结构信息(读文件行、web 来源)经 output.presentationMeta(args, value) 投影成 JSON,
随 tool/result 持久化、回到 presentResult 读回 —— canonical value 本身永不重放。
工具的 UI 渲染意图是设计的一部分,前置决定,别事后补。
监听 internal/dispatch 拦截所有事件分发:阶段单调性(pre-execute 不重复、execute 必跟在 pre 后、post 必跟在 pre/execute 后、result 收尾);tools/result 时 exec/result/content 必须 frozen;code-dispatch 必须落在开放 turn 内且 subCallId 归属稳定;既有会话在 seed 时全量回放验证。
defineTool(参数/返回值类型推断 + 自动校验);或裸 ctx.tools.register(rawDefinition)(自己校验输入,output 声明与校验仍是注册表强制的)ParameterSchemaSpec(隐式开放对象根,required: true 按属性标注);输出用 ValueSchemaSpec;必写 output.render(args, value) 纯投影exec.signal;可选 timeoutMs(正有限)、isConcurrencySafe(精确 true 才并发)presentCall/presentResult 返回 card 视图,不返回则 generic 回退(标题=工具名)docs/cookbook/adding-a-tool.md| 需求 | 机制 | 约束 |
|---|---|---|
| 允许 / 拒绝 / 询问 | tools/pre-execute 瀑布 或 ctx.tools.guard() | guard 单调(返回 reason 即拒绝,无法翻案);不能重写 exec.arguments(会与日志/渲染脱节) |
| 超时 / 重试 / 指标 | tools/execute 瀑布 wrapper | 只能换 exec.signal;调用身份不可变;wrapper 自产结果按该调用的 output 声明重新规范化 |
| 改呈现 / 换值 / 附上下文 | tools/post-execute | accept 换 content 或 value(二选一);block 把反馈变无值失败;上下文顺序:工具 deferred 在前、决定上下文在后 |
| 最后一道 content 不变量 | ToolDefinition.finalizeContent | 同步、总函数、每结果恰好一次(含绕过 post-execute 的失败)、只换 content |
| 只观察 | tools/result | 只读冻结快照;监听器抛错被包含 |
| 改 Code Mode 子分派的日志副本 | tools/code-dispatch-log | 程序值/模型结果不受影响;抛错回退原样 |
agent.ctx 上 ctx.tools.restrict({ allow: [...] }) 或 { deny: [...] } —— 只过滤继承面,不伤该 scope 自身注册;多 mask 交集agent.ctx.tools.register(...) 同名 shadow 全局,该 agent 独享agent.ctx.tools.presentAs('code') 只改该 agent 的模型呈现(catalog 不变)UNKNOWN_TOOLToolCallView(presentCall)与 ToolResultView(presentResult);参考 dsh-tool-bash(terminal 卡)、dsh-tool-fs(diff/read 卡)output.presentationMeta 投影成 JSON 持久化,再在 presentResult 读回snapshotJsonValue;undefined/BigInt/循环/-0/exotic 对象各自拒绝timeoutMs 是声明式:注册表不 enforce;没挂 timeout-policy wrapper 就无超时isConcurrencySafe 精确 true 才 parallel;未知/隐藏/抛错全部 fail-closed 为 exclusivetools/result(live)≠ tool/result(durable);tool/code-dispatch* 是 log-only,不 derive 模型消息CodeSdkLanguage + SDK_RENDERERS + RUN_CODE_FLAVORS + renderer 四联编辑;原生工具的并行是 agent-loop 自己的滚动池