工具展示是从"模型请求调用某个工具"到"结果回到模型历史"的整条路径:schema 展示、分发、策略、受守护执行与事件记录。它横跨作用域工具注册表(packages/core/tools/src/index.ts,暴露为 ctx.tools)、循环的调度器(packages/core/agent-loop/src/tool-calls.ts)以及智能体平面的展示选择器(packages/core/agent-tool-presentation/src/index.ts)。
| 包 | 负责 |
|---|---|
@deepseek-ai/dsh-tools | ctx.tools——作用域工具注册表、defineTool、守卫、presentAs、restrict、执行流水线 |
@deepseek-ai/dsh-agent-loop | 计划/分发一个 step 工具调用并保持模型顺序的调度器 |
@deepseek-ai/dsh-agent-tool-presentation | "模型看到的工具是哪种形态"的选择器(native/code/both) |
@deepseek-ai/dsh-llm | ToolSchema、ToolCallBlock、createToolResultMessage |
对模型可见的 schema
每个已注册工具都是一个 ToolDefinition,其对模型可见的表面是声明于 packages/llm/llm/src/types.ts 的 ToolSchema:
export interface ToolSchema {
name: string
description: string
/** JSON Schema object for the arguments. */
parameters: Record<string, unknown>
}操作上交,ToolDefinition 加了 execute(args, exec) 主函数以及可选的最终内容与 UI 回调——但注册表的 schemas(scope) 方法只向模型投影 name、description 与 parameters(白名单),并通过 structuredClone 解绑参数对象,因此不会有任何运行时状态泄漏到提示。工具通过 defineTool DSL(packages/core/tools/src/schema.ts)编写,它把类型化 ValueSchemaSpec/ParameterSchemaSpec 编译成 JSON Schema 加生成的类型。
展示作用域工具与全局工具
工具 schema 经 systemPrompt.tools(provider) 到达提示。工具注册表只贡献该作用域可见的 schema——经受作用域限制过滤后的全局工具,加上作用域本地注册——并在遮蔽之后(作用域同名工具会为该作用域隐藏全局孪生)。这些 schema 如何渲染由展示模式决定:
| 模式 | 模型看到什么 |
|---|---|
native | 每个可见的 ToolSchema |
code | 仅保留的 run_code 传输 + 生成的 SDK(Code Mode) |
both | run_code 传输与本地 schema 都要 |
ctx.tools.presentAs(mode) 为挂载作用域(一行对应一个智能体预设的常驻挂载,而非一个会话)声明该模式。agent-tool-presentation 插件读取 mode 配置,然后直接 presentAs('native'),或等待 codeRuntime 服务后再声明代码模式。
执行流水线
注册表的流水线先运行 tools/pre-execute waterfall(钩子、权限、沙箱),再运行注册的单调守卫,然后是 tools/execute 与 tools/post-execute waterfall,最后是定义所有的 finalizeContent 与 tools/result 通知。流程的 ASCII 图:
助手消息 → tool-call 块
│ 为每个块计划一个 ToolExecutionInput
▼
[记录 tool/call] [UI presentCall(args)]
│
▼
tools/pre-execute waterfall (钩子、权限、沙箱)→ 允许 / 拒绝 / 询问
▼ 允许
注册的单调守卫(拒绝或弃权;身份受保护)
▼ 允许
tools/execute waterfall (超时、重试、指标包裹主体)
▼
注册工具的 execute() 主体
│ 工具自有事件:todo/write、fs/observed、hook/invoked、…
▼
tools/post-execute waterfall (接受 / 阻止 / 替换 / 加上下文)
▼
外层归一化(流水线/结果快照抛错 → isError)
▼
ToolDefinition.finalizeContent (最后一个仅内容不变量)
▼
tools/result(同步、冻结的权威结果)
▼
[记录 tool/result → 引用其 tool/call 的 seq] [UI presentResult(args, result)]
▼
工具批次落定 → additionalContexts FIFO → 注入 next-step user/messagectx.approval 在单调守卫之前解析询问类调用;不能被重排序的持有者策略保持为注册守卫。围绕分发的问题(超时)包装 tools/execute。注册表无损地快照候选结果,并在可见定义的快照式 finalizeContent 回调强制执行其同步的仅内容不变量之前归一化快照失败,因此 tools/result 总能观察到一份不可变的、无损 JSON 的结果。
循环的调度器与事件映射
executeToolCalls(位于 packages/core/agent-loop/src/tool-calls.ts)按活跃并发模式调度一个助手 step 的工具调用:独占调用形成屏障,并行调用使用一个以 ctx.agentLoop.config.maxParallelToolCalls 为上限的有界滚动池。尽管分发可以重叠,事件仍按模型顺序提交。到会话日志的持久映射:
function appendToolCall(session, turn, step, block): number {
const event = session.append('tool/call', { turn, step, callId: block.id, name: block.name, arguments: block.arguments })
return event.seq
}function appendToolResult(session, turn, step, block, result, callSeq): void {
const message = createToolResultMessage({ callId: block.id, content: result.content, isError: result.isError })
session.append('tool/result', {
turn, step, message,
...result.error?.info ? { error: result.error.info } : {},
...result.meta !== undefined ? { meta: result.meta } : {},
}, { surfaceOp: 'append', sourceEventSeqs: [callSeq] })
}模型 tool-call 块里的 callId 把 tool/call 与它的 tool/result 配对;结果在 sourceEventSeqs 中引用所属调用的 seq,保留了回放所需的相邻关系。结果可以携带 concludesTurn(让 turn 在该 step 处结束)与 additionalContexts(调度器的 acceptContext 把它们拼进 next-step inbox 作为 user/message)。取消时,跳过的模型调用会收到一条合成错误结果(TOOL_ABORTED_BEFORE_DISPATCH),从而保证回放仍然有效。
调度器表面与结果契约
把执行分成两个重叠阶段(有序策略,然后并发分发)的是工具注册表内部、由循环通过符号 TOOL_RUNTIME_SCHEDULER 访问的调度器。它的三个操作把各阶段串联起来:
interface ToolRuntimeScheduler {
/** Materialize input, run the ordered pre-execute/guard gate, and decide what stage follows. */
prepare(exec: ToolExecutionInput): Promise<ScheduledToolPreparation>
/** Run only the around-dispatch/body stage. */
dispatch(exec: ToolRunContext): Promise<ScheduledToolDispatch>
/** Run post-execute and definition-owned content finalization, then materialize and notify. */
finalize(exec: ToolRunContext, result: ToolExecutionResult): Promise<ToolExecutionResult>
/** Run definition-owned content finalization, then materialize and notify without post-execute. */
finish(exec: ToolRunContext, result: ToolExecutionResult): ToolExecutionResult
}prepare 返回一个阶段决定:dispatch 还需要主体,post-result 仍要走 tools/post-execute,而 final-result 已匹配失败语义并跳过它。tool-calls.ts 里的提交在结果是 post-result 时调用 finalize、在 final-result 时调用 finish——这正是 commitReady 路径上的两个选项。由此产出的 ToolExecutionResult 是成功(ToolExecutionSuccess:content + isError:false)与失败(ToolExecutionFailure)的联合,并且可以携带 additionalContexts、meta 与 concludesTurn。
并发按调用来回用 ctx.tools.executionMode(exec) 决定。独占调用形成屏障:调度器在开始前重新读取后续模式,因此注册表的变化可以在当前池排空后把批中某个调用提升为独占。并行调用使用有界滚动池(maxParallelToolCalls),其分发可以重叠,而策略、结果与结果上下文仍保持模型顺序。两种模式都尊重 signal 中止:已开始的调用排空并提交,未开始的调用收到合成结果。
限制过滤
tools.restrict(filter) 按交集为一个作用域过滤全局工具集。被过滤掉的全局工具既不出现在提示中,也拒绝执行——与一个从未存在的工具毫无区别。作用域本地注册在该过滤器之后合并。restrict({}) 因是无操作而被拒绝,保留的 run_code 传输名也不能直接限制(改而限制末端能力工具)。
真实事件
流水线经这些按作用域过滤的事件路由:
| 事件 | 分发 | 角色 |
|---|---|---|
tools/pre-execute | waterfall | 钩子、权限、沙箱的允许/拒绝/询问 |
tools/execute | waterfall | 围绕分发的超时 / 重试 / 指标 |
tools/post-execute | waterfall | 接受 / 阻止 / 替换 / 加上下文 |
tools/result | emit | 同步冻结的权威结果 |
session 的 tool/call / tool/result | 会话日志 | 持久化的调用/结果对 |
延伸阅读
- 智能体循环 —— 调用
executeToolCalls的step()循环。 - 会话管理 ——
tool/call/tool/result事件及其 surface 语义。 - 作用域系统 —— 作用域工具、遮蔽与按交集限制。
- 系统提示组装 —— 可见工具 schema 如何进入提示以及
toolOrder。 - 仓库源码:
packages/core/tools/src/index.ts、packages/core/agent-loop/src/tool-calls.ts、packages/core/agent-tool-presentation/src/index.ts。 - 官方脚手架:
docs/tool-execution-pipeline.md、docs/tool-catalog.md。