Files
deepseek-harness/packages/core/tools/README.zh.md
Tianyi Cui 76d95fdc4b docs: bring the zh README pairs along for the parallel-dispatch contract
master's bilingual README pairing (new since this branch forked) covers
packages/core/tools and packages/client/runtime, whose EN sides this PR
edits. Translate the scheduling-contract sentences (bridge pool, SDK
overlap line, loop cross-reference, codeDispatches lifecycle) into the zh
sides — including the verbatim shared model-facing block — and re-record
both pairing records.
2026-07-26 21:19:46 +08:00

27 KiB
Raw Blame History

dsh-tools

English | 中文

工具注册表与执行流水线。工具插件注册各自的 schema 和执行器agent loop智能体循环依次让每次调用经过 tools/pre-execute(可扩展的允许/拒绝门禁)→ 单调注册守卫 → tools/execute(供超时/重试/指标插件使用的环绕分发包装层)→ tools/post-execute(检查/替换结果、附加上下文)→ 由定义拥有的 finalizeContent 边界 → 仅观测的 tools/result 通知。注册表还负责决定如何向模型呈现其工具:mode 配置可以选择原生 Function Calling函数调用Code Mode,或同时选择两者。

服务:ToolRegistryctx 键:tools

配置

tools:
  mode: native   # native (default) | code | both

native 以函数定义的形式贡献可见工具。code 贡献保留的 run_code 传输和生成的 tools:sdk 段;both 同时贡献两种形式。不能注册、遮蔽、限制或移除该保留传输。非原生模式要求存在 TypeScript ctx.codeRuntime;如果 systemPrompt.toolOrder 条目指向当前模式未贡献的工具,系统会拒绝组装提示词。system-prompt/assemble 监听器可以替换注册表贡献;它返回的组装结果具有权威性,因此该监听器负责保留可用的 Code Mode 协议。

公开 API

  • ctx.tools.register(definition: ToolDefinition): () => void:注册一个受信任、带类型的同进程定义,其中必须包含规范的 output 声明。所在层由调用上下文的作用域决定普通插件上下文会全局注册agent 的 agent.ctx 只为该 agent 注册,并在此处遮蔽同名全局工具。同一层内名称重复会抛出;非原生模式还会拒绝保留的 run_code 传输名称。缺失或不受支持的输出声明,以及非正数或非有限的 timeoutMs,都会使注册失败。可选的同步 finalizeContent 回调会在调用开始时创建快照;在所有流水线结果规范化之后,它只能替换最终面向模型的内容,包括实体化其他结果字段时发现的错误。随调用 fiber 释放。
  • ctx.tools.restrict(filter):对全局工具应用 agent 作用域的允许/拒绝掩码;从普通上下文调用会抛出。筛选器在注册时创建快照;多个掩码取交集,随后再合并作用域本地工具。拒绝掩码会接纳后来出现且未点名的全局工具,而允许掩码会排除后来出现的名称。未知、本地或保留名称以及空筛选器都会被拒绝。这是实时可见性组合,不是权限边界;参见作用域安全非目标
  • ctx.tools.get(name: string, scope?: ScopeKey): ToolDefinition | undefined:按某个作用域所见的结果解析(应用遮蔽;被限制掉的全局工具视为不存在)。呈现器会传入发起调用的 agent使卡片与实际执行内容一致。
  • ctx.tools.schemas(scope?: ScopeKey): ToolSchema[]:返回该作用域可见的所有 schema不含 execute 函数)。已交付工具的 schema 收录在 docs/tool-catalog.md 中;该目录通过启动每个工具插件并采集此方法的结果生成(参见工具 schema 目录 Agent Noteagent 决策记录))。
  • ctx.tools.guard(guard: ToolGuard): () => void:在 tools/pre-execute 之后注册单调同步执行守卫:返回理由会拒绝调用,返回 undefined 则保持原决定。普通上下文守卫全局生效;agent.ctx 守卫只对该 agent 生效。后续 waterfall瀑布式事件监听器无法将守卫的拒绝重新变为允许。随调用 fiber 释放。
  • ctx.tools.execute(exec):以无损方式快照并冻结参数,分配不透明 token运行完整的策略分发结果流水线然后在最终观测前独立快照权威结果。无效参数会进入同一结果路径但不会到达策略或工具主体。环绕包装层只能替换 signal;注册表会在调用主体前立即重新融合调用方的原始信号。
  • ctx.tools.executionMode(exec):返回 parallel 的唯一条件是可见定义的 isConcurrencySafe(exec.arguments) 分类器恰好返回 true;未知、隐藏、未声明、无效或抛出异常的分类结果均为独占。

注入的服务

SystemPrompt:注册表通过 ctx.systemPrompt.tools() 自动将工具 schema 送入系统提示词组装。审批 seam 则按需使用(ctx.get('approval'),无静态注入):未部署该 seam 时仍会将询问退化为拒绝,而无论是否存在该 seam注册表都会保持活动。

取消

取消采用协作方式,并等待完全停稳。每次类型化调用都提供由调用方拥有的 AbortSignal;工具主体通过必填的只读 exec.signal 接收它,只有 tools/execute 包装层可以临时替换这个必填信号。注册表会在替换期间保留调用方取消,并且绝不会在已启动的同进程 Promise 尚未结算时提前返回。调用主体前发生的取消为 ABORTED_BEFORE_DISPATCH;调用后的取消只能把成功结果替换为 ABORTED。拒绝、包装层失败、工具失败、后置策略失败或超时拥有的 TOOL_TIMEOUT 仍保留更具体的结果。入口处已中止的调用会实体化并冻结参数,随后跳过所有策略和分发阶段,只发布一个结果。每个异步工具都必须观测或转发该信号,并且只能在自身拥有的工作停止后结算。工具取消 Agent Note 规定完整契约和强制终止边界。

实时事件

实时注册表流水线先经过 3 个可变换的 waterfall再经过由定义拥有的内容终结器最后到达仅观测的 tools/result 边界;注册表变更有意作为不过滤的共享状态通知。确切签名、分发 mode、作用域筛选和故障收容契约位于生成的 Cordis 事件目录,完整顺序则在生成的工具执行流水线中可视化。tools/result 是实时事件;名称相近的 tool/result 是 agent loop 随后追加的持久会话事件。

关键类型

  • ToolDefinitionToolSchema + 必填的 output { schema, render, presentationMeta? } + execute(args, exec),以及可选的最终内容回调、呈现回调、协作式 timeoutMs 和逐调用的 isConcurrencySafe(args) 分类器。主体只能返回输出 schema 声明的规范 JSON 值,并通过 exec.signal 协作停止。finalizeContent(exec, result) 对每个规范化结果都恰好运行一次,包括绕过后置策略的失败,并且只能替换 content;它必须是同步且对所有输入都有定义的函数。
  • ToolExecutionInput:调用方提供的调用描述:{ callId, name, arguments, signal, agent?, parent? }signal 必填且只读,调用方可以将外层执行的不透明 token 作为 parent 传入,但绝不能选择新执行自身的 token。
  • ToolExecutionToken:注册表分配的全新带品牌 Symbol。它只支持通过相等性进行关联,绝不会跨越模型、日志或 worker 边界。
  • ToolExecution:只读流水线视图:不可变的 { token, callId, name, arguments, signal, agent?, parent? };注册表会另行保留并重新融合调用方的原始信号。ToolDispatchExecution 是仅供 tools/execute 使用的视图,其必填信号可变,因此包装层可以替换并还原它,但不能删除它。嵌套调用的 parentToolExecutionToken,而不是执行对象。
  • ToolRunContext:传给工具主体的执行上下文,在 ToolExecution 基础上增加 deferContext(context)。组合工具借此把嵌套分发产生的上下文传递到外层结果,即使工具后来抛出或取消胜出也不例外;该方法绝不会立即注入上下文。
  • ToolExecutionResult:可辨识的执行局部结果。成功形态为 { isError:false, value:JsonValue, content, meta?, additionalContexts? };失败形态为 { isError:true, error:{ message, info? }, content, meta?, additionalContexts? },且不含值。调用身份保留在不可变的 ToolExecution 上。注册表会在呈现前快照、验证并冻结规范值,随后在最终观测前实体化持久呈现字段。ToolFailure.info 携带内部的 { name, code },用于表示 HarnessErroradditionalContexts 为循环在结果后的 FIFO 保留每个延迟或后置执行的 HookContext
  • PreToolDecision{kind:'allow'} | {kind:'deny', reason} | {kind:'ask', reason?}。该类型有意不提供输入改写;ask 在挂载 ctx.approval 时由它处理,否则退化为拒绝。
  • PostToolDecision:接受决定可以替换 contentvalue(不能同时替换),并可附加 additionalContexts;阻止决定会把反馈变成无值失败。替换内容会保留规范值和元数据。替换值会重新验证,并重新呈现内容/元数据。接受决定会先保留工具延迟的上下文,再附加决定上下文;阻止决定会丢弃工具延迟的上下文,只公开阻止决定显式提供的上下文。
  • ToolGuard(execution) => string | undefined;返回的字符串是最终单调拒绝理由,在可重排的前置执行 waterfall 之后、分发之前求值。
  • ToolCallView / ToolResultView:提供方无关、带 card 标签的呈现意图;工具通过 presentCall / presentResult 返回该意图,从而拥有 UI 呈现其自身调用的方式(参见「工具拥有的 UI 呈现」)。

扩展点

  • 工具插件调用 ctx.tools.register()schema 会自动流入组装结果。
  • tools/pre-execute 是可重排的允许/拒绝/询问门禁;ctx.tools.guard() 在其后添加单调的拥有方策略。
  • tools/execute 为超时、重试或指标环绕已经规范化的规范分发。包装层只能替换操作信号;包装层创作的成功结果会根据已解析工具的输出声明进行规范化。规范结果的来源属于一个不可变分发 token因此来自其他调用或工具的缓存结果会根据当前声明重新验证。
  • tools/post-execute 可以替换呈现内容、替换规范值、通过反馈阻止,或附加有序上下文。随后,定义可选的 finalizeContent 会在普通结果和外层流水线失败中维护其最终、仅涉及内容的不变式;tools/result 观测不可变的最终结果。内容替换不是保密边界:当编程消费方不得接收某个值时,应阻止或替换该值。
  • 确切签名与顺序位于生成的事件目录流水线中。
  • MCP 服务器:每个服务器使用一个插件;发现工具后,使用服务器的 schema 调用 ctx.tools.register()

类型化工具参数 schema

第一方插件作者可以使用本包导出的 defineTool() 辅助函数定义类型化工具参数 schema

import { readFile } from 'node:fs/promises'
import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

declare const ctx: Context

ctx.tools.register(defineTool({
  name: 'read_file',
  description: 'Read a file from disk.',
  parameters: {
    path: { type: 'string', required: true, description: 'Absolute file path' },
    offset: { type: 'number' },
    limit: { type: 'number' },
  },
  output: {
    schema: { type: 'string' },
    render: (_args, value) => [{ type: 'text', text: value }],
  },
  async execute(args, exec) {
    // args is typed: { path: string; offset?: number; limit?: number }
    return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
  },
}))

统一 schema DSL 使用 ParameterSchemaSpec 表示隐式开放参数对象,使用 ValueSchemaSpec 表示任意 JSON 值根。它支持 stringnumberintegerbooleannullarrayobject、仅供作者使用的 json,以及恰好匹配一个分支的 oneOf;标量 enum/const 值会接受类型正确性检查。每个显式 DSL 对象都声明 additionalProperties: true | false,而隐式参数根和原始 JSON Schema 保持标准的开放默认值。schema 记录只接受自身可枚举字符串键schema 数组必须是稠密普通数组。编译、验证、从注册表分离以及 schema 到 TypeScript 的呈现均使用显式工作栈,因此,对有效深层 schema 的运行时处理受内存而非调用栈限制;InferValue 在 16 层容器内保留精确类型,之后回退到 JsonValue,使 TypeScript 自身也保持栈安全。

defineTool 定义会在执行前验证模型参数,并把缺失必填值、基本类型错误、无效枚举成员和嵌套违规转换为 ToolArgsErrorINVALID_ARGS),进入普通错误结果路径。它还会根据 output.schema 推断主体返回类型和纯输出投影器;注册表在呈现前快照并验证返回的无损 JSON。隐式参数根是开放的显式对象只有在设置 additionalProperties: true 时才接受额外键,而没有声明属性的封闭对象只接受 {}。原始 JSON Schema 对象保持开放,除非显式设置 additionalProperties: false。系统不会应用默认值;没有 properties 的开放对象和没有 items 的数组只接受容器类型检查。通过原始方式注册的工具负责输入验证,但仍需声明输出,并由注册表强制校验输出。

有关详细信息,请参阅公开 API 中的 defineToolvalidateArgsToolArgsErrorValueSchemaSpecParameterSchemaSpecInferValueInferArgsvalueSchemaSpecToJsonSchemaparameterSchemaSpecToJsonSchema

可选的 timeoutMs 必须为正数且为有限值;它是策略元数据,不是模型可见的 schema。

可选的 isConcurrencySafe(args) 接收经过软验证的类型化参数。只有确切的 true 才允许并发分发/主体执行;无效输入和所有其他结果仍为独占。选择并发的主体不得改变父级拥有的状态;共享状态竞态必须具有交换性,否则必须安全拒绝。并行工具调用 Agent Note 规定完整安全契约。

强制执行的原始 JSON Schema 子集

JsonSchemaNode 是工具输出、Code Mode 生成、subagent 和工作流共享的原始对应类型。它允许任意 JSON 根、一个仅含 annotation 的无约束 JSON 节点,以及恰好匹配一个分支的 oneOfannotation 必须保持为无损 JSON。assertSupportedJsonSchema() 拒绝不受支持的构造,而 validateJsonSchemaValue() 返回带路径的违规信息。subagent 和工作流通过 assertObjectJsonSchema()ObjectJsonSchema 保留调用方定义的对象根要求,而不是依赖共享词汇的限制。

工具拥有的 UI 呈现

工具可以选择拥有纯 presentCall()presentResult() 呈现意图,使 UI 无需特殊处理工具名称:

  • 调用视图为 { card: 'generic', title, kind?, rawInput?, content?, locations? }{ card: 'terminal', title, description?, cwd? }{ card: 'diff', title, diffs, locations? }
  • 结果视图为 { card: 'generic', title?, content? }{ card: 'terminal', title?, output?, exitCode?, signal? }{ card: 'diff', title?, diffs }

返回 undefined 会选择通用回退。呈现器只依赖其参数和持久结果,因为 UI 会在实时流式输出和日志回放期间调用它们。output.presentationMeta(args, value) 为直接接口调用派生 JSON 元数据;该元数据随 tool/result 持久化并传回 presentResult,而规范值本身仍只存在于执行局部,绝不会回放。嵌套 Code 分发不会计算元数据。defineTool 会软验证较旧的日志参数并回退,而不会使回放崩溃。dsh-tool-bashdsh-tool-fs 是参考实现;规范输出 Agent Note 规定值/呈现拆分,呈现意图 Agent Note 规定卡片词汇。

Code Mode

codeboth 模式下,注册表为当前作用域公开保留的 run_code 传输和确定性的 TypeScript SDK只有程序的外层日志与返回值会重新进入模型上下文。SDK 为每个可见工具声明精确的 ToolArgsMapToolOutputMap 条目,每个绑定都会解析为该工具的规范 JSON 值。每个无损 JSON 绑定调用都会在原生调度契约下重新进入完整工具流水线(并发安全的调用最多可重叠 maxParallelSubCalls 个;独占调用单独运行并构成排序屏障),并在日志中与外层调用建立关联。拒绝及其他失败结果会以程序实际可见的 ToolCallError 进行 reject且只携带 toolNamemessageNative 内容和内部错误码留在 Code 契约之外。普通副作用不会回滚,子调用的 additionalContexts 会通过父结果延迟,以保持调用/结果相邻。运行结算会中止并 drain 尚未完成的绑定;运行时失败以 CodeRunFailedError 形式出现。参见 Code Mode 基础类型化返回契约代码运行时 seam。可以运行 pnpm run demo:code-mode 试用。

  • SDK 段tools:sdk,顺序 150一个惰性提示词段每次组装时都会重新生成 JsonValue、精确的 ToolArgsMap / ToolOutputMapToolNameToolCallError 声明、面向调用作用域可见最终能力的映射 tools 命名空间(特殊名称使用带引号的键),以及固定用法说明。其输出具有确定性:工具按字典序排列;工具集合不变时,文本逐字节相同(有利于前缀 cache。导出的代码生成器 jsonSchemaToTs 会处理统一 schema 的每种构造,并将不受支持的原始构造降级为 unknown,绝不会在提示词组装期间抛出。
  • 分发桥接层run_code 的 execute每个绑定调用都会在分发前快照为无损 JSONundefinedBigInt、循环、稀疏数组、-0 和特殊对象会使该次调用被拒绝),经由每次运行独有、复用原生并发契约的池调度——调用严格按提交顺序启动,连续的 isConcurrencySafe 调用最多可重叠经校验的 maxParallelSubCalls 配置个(默认 10设为 1 即恢复串行分发),被分类为独占的调用先排空池、单独运行并阻挡其后的调用——以外层执行的不透明 token 作为 parent,并经过完整的 pre-execute → guards → execute → post-execute → result 流水线。成功会返回策略处理后的最终规范值;失败以一条消息到达 worker并成为 ToolCallError(toolName, message)。每个已启动的子调用在进入流水线时记录一条 tool/code-dispatch-start 事件(确定性 id <parent>:code:<n>,按提交顺序编号),并以一条携带完整模型可见 content/isError 结果的 tool/code-dispatch 事件完结(采用 tool/result 词汇,因此 UI 会沿原生路径呈现子调用——这对事件的 time 字段承载每个子调用的计时);因 run 结算而被放弃的排队调用两者都不记录。deriveMessages() 既不公开这两个事件也不持久化规范值。token 关联让以提交为语义的观察器能够把内部成功延迟到最终 run_code 结果,而无需公开实时外层执行;普通工具副作用不会回滚。每个子调用的 additionalContexts 条目都会按分发顺序通过外层 ToolRunContext 延迟;循环只在父级 run_code 结果之后追加这些上下文,从而保持相邻关系,并且即使程序后来失败,也会保留各自的来源/元数据。
  • 结算纪律:桥接层拥有一次运行作用域的中止;该中止会跟随传入的外层信号,并在运行因任何原因结算时触发,因此预算耗尽会中止正在运行的子工具,而不会将其遗留。桥接层随后会在返回之前 drain 队列,使每个 tool/code-dispatch 都落在仍打开的轮次内。失败的运行会抛出 CodeRunFailedErrorcode: 'CODE_RUN_FAILED'message = 失败类型 + 已捕获日志),流水线会将其转换为模型可据以自我修正的结构化 isError
  • 结果边界:中间绑定值会完整跨越 worker 边界,且没有逐绑定字节上限。run_code 返回规范的 { logs: string[], result?: JsonValue };字符串原样呈现,其他所有存在的 JSON 根都通过栈安全的美化 JSON 遍历呈现,总缩进最多为 10 个字符(更深的子树保持紧凑),null 保持显式,而缺少 result 表示程序返回 undefined。worker 可配置的 maxOutputBytes(默认 64 MiB只应用于组合序列化后的外层日志数组、完成值或失败消息载荷固定的结果 envelope 语法和呈现空白不计入该账本。无效和超限的完成会明确失败,只有此外层结果可以使用普通 spill。

并行执行

agent loop 将连续的 parallel 调用归入有界滚动池,并把每个 exclusive 调用视为顺序屏障。只有分发主体会重叠策略、持久结果和上下文仍保持模型顺序。Code Mode 绑定通过桥接层自己的池复用同一套分类。并行工具调用 Agent Note 规定已交付声明及其原理。

模型体验

普通工具 schema

模型所见

在普通模式下,模型会看到每个可见定义的确切名称、描述和 JSON schema已交付定义记录在生成的工具包映射和 schema 章节中。agent 作用域的限制、遮蔽和扩展注册会改变该 agent 的最终工具集合。

Token 影响

每次请求的固定成本与可见定义成正比。隐藏工具的限制会为该 agent 移除其全部 schema 成本。

KV Cache 影响

只要可见定义及其顺序不变,前缀就保持稳定。注册、释放或作用域限制可能从第一个改变的 schema token 起使复用失效。

Code Mode schema 与系统提示词

模型所见

Code Mode 会公开生成的 run_code schema、下方 SDK 说明,以及生成的精确 declare const tools 块。both 会同时公开普通 schema 与此 Code Mode 接口。

Code Mode SDK 说明
## Writing code for run_code

Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:

- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.
- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.
- Emit results with `return` and/or `console.log(...)`. ONLY what you print or return comes back to you — intermediate tool results never enter the conversation, so extract just what you need.

The available tools:

Token 影响

每次请求的固定成本与可见定义成正比。Code Mode 使用生成的 SDK 文本加一个传输 schema 取代最终工具 schema但不承诺普遍减少成本。

KV Cache 影响

只要 Code Mode 选择、生成的 SDK、传输 schema 和可见工具集合不变,前缀就保持稳定。模式或筛选器变更可能从第一个改变的提示词或 schema token 起使复用失效。

工具调用历史与结果

模型所见

循环会保留模型发出的参数和注册表的最终内容。任何抛出或被拒绝的调用都会恰好变为 Error: <message>。Code Mode 只返回外层程序打印的行和呈现后的返回值;两者都为空时返回 (run_code completed with no output);失败时返回 Error: code run failed (<kind>): <message>,并根据是否存在已捕获内容,在其后附加 Captured output: 与捕获的行。内部分发事件只保留在日志中;后置执行监听器可以在结果之后追加带来源归属的上下文。

Token 影响

参数、结果和附加上下文取决于数据,并会重复发送直至压缩。隐藏工具的限制还会在模型可以调用这些工具之前移除其 schema。

KV Cache 影响

仅追加;新的可见内容位于可复用请求前缀之后,不会使现有 KV-cache 条目失效。

已知限制与暂缓工作

  • 并发策略不是事件 seamexecutionMode() 直接读取已解析的工具定义;插件只能在自身拥有的定义上声明分类器。
  • tools/pre-execute 有意不允许改写 exec.arguments:否则日志记录和呈现的参数会与实际运行内容失去同步;改写设计记录在拟议的 Agent Note中。
  • 调用方定义的 subagent 与工作流结构化输出仍要求对象根:这是消费方层面的守卫;共享 schema 词汇和工具输出支持任意 JSON 根。
  • 定义上的 timeoutMs 仅为声明:注册表绝不会强制执行截止时间;要强制执行,必须使用 @deepseek-ai/dsh-timeout-policy 包装层。
  • Code Mode 只支持 TypeScript且呈现模式在服务内统一mode: code/both 会拒绝组装提示词,除非 ctx.codeRuntime.language === 'typescript';作用域限制/遮蔽仍会选择每个 agent 的可见绑定,但不能让一个工具仅使用 Native而另一个仅使用 Code。
  • Code Mode 中间值只存在于执行局部,且没有字节上限:这些规范的类型化值无法从会话回放重建,并可能耗尽进程或 worker 内存;只有外层 run_code 输出受 worker 可配置的硬上限约束。每个子调用渲染后的 content 确实会原样记录在 tool/code-dispatch 中,不受字节上限约束,也不在 spill 策略范围内。因此,读取超大文件的程序会使会话日志增加等量字节(日志中的副本尚未接入 spill相关工作留待后续完成
  • 每次运行都会获得全新的 run_code 状态MVP 不采用持久 REPL 风格内核(跨调用状态不会出现在日志中);参见 Code Mode Agent Note