DSHarness 系统拆解 固定基线 47f943859b · 36 已复核 / 0 撰写中 / 36 章
English
工具系统·第 16 章

工具定义原语与执行管线

schema、scope、execution mode、presentation 与 waterfall

已复核上游 47f943859b范围: 拆解 ToolDefinition、registry、快照、pre/guard/around/post/finalize/result 和 Code Mode 子调用。

结论:ToolRuntime 不是函数表,而是一条把不可信模型调用提交为可重放结果的事务边界

DeepSeek Harness 的工具核心同时承担五件事:解析每个 Agent scope 能看见什么;把定义投影成模型 schema;给一次调用分配不可伪造的执行 identity;把 pre-policy、审批、单调 guard、around wrapper、body、post-policy 和 tool-owned finalizer 排成固定管线;最后只发布一份 lossless、冻结的权威结果。Agent loop 才把顶层 call/result 写进 Session,UI presenter 则从同一耐久结果重建卡片。

最值得学习的不是“有 hooks”,而是每一层能改什么都不同:pre 不能改已记录参数;guard 只能拒绝;around 只能临时替换 signal 并包裹 body;post 可以替换 canonical value 或 model content;finalizer 只能改 content;result observer 什么都不能改。

生成控制流

仓库生成的执行图与生产源码一致地给出 pre → guards → around/body → post → normalize → finalize → frozen result 的顺序,并把 Session logging 与 UI projection 放在管线外的拥有者边界。TOOL-PIPELINE

1. 一个工具定义同时包含四份合同

合同字段消费者是否发给模型
调用合同namedescriptionparametersPrompt/Provider adapter
Canonical outputoutput.schemaoutput.render、可选 presentationMetaToolRuntime、Code Mode、UI replayOutput schema 不走 native schema;render 后 content 会进入历史
执行合同executetimeoutMsisConcurrencySafeRegistry 与外部 policy/scheduler
展示合同presentCallpresentResultfinalizeContentHost/UI 与提交末端Presenter 不发;final content 会进入模型历史
Definition shape

成功 body 必须返回 canonical lossless JSON value。Registry 用 output schema 验证它,再用纯 render(args,value) 生成 provider-neutral ContentBlocks;顶层调用还可生成耐久 JSON metadata。TOOL-DEFINITION

2. 模型 schema、执行 value、耐久 content 与 UI meta 是四种不同数据

model args string
→ parsed + lossless snapshot + frozen arguments
→ execute() returns canonical JSON value
→ output.schema validates value
→ output.render(args, value) produces model-facing ContentBlocks
→ presentationMeta(args, value) optionally produces replayable UI JSON

execution-local result: { value, content, meta? }
durable tool/result:     { content, isError, errorInfo?, meta? }
future model Surface:    content only
数据所有权

成功 ToolExecutionResult 在进程内携带 frozen value,但顶层 Session event 刻意只保存 content、failure state 与可选 meta。Canonical value 不可从日志保证恢复;UI 所需但文本无法无损表达的结构必须由 presentationMeta 明确投影。TOOL-RESULT-TYPESTOOL-CANONICAL-OUTPUTTOOL-LOOP-COMMIT

这让 Code Mode 可以把结构化 value 交给程序继续计算,同时主模型只看到渲染后的受控文本;也避免 UI 为了显示 diff、line numbers 或 web sources 去反向解析自然语言结果。

3. Schema DSL 是受约束的 JSON Schema 子集,不是完整规范

支持规则明确不做
Scalarstring/number/integer/boolean/null,类型匹配的 enum/constpattern、minimum、format 等关键字会被拒绝
Containerarray/items;object/properties/required/boolean additionalProperties显式 object 必须声明 openness,避免隐式默认
Unionexact-one oneOf,至少两个分支不接受与 oneOf 冲突的 sibling constraints
Any JSONauthor DSL 的 type: "json" 编译为无约束 node仍必须是 lossless JSON value
Annotationsdescription/title/default/examplesdefault 只做 annotation,不会自动填值
Fail loud

Raw schema walker 会拒绝未知或错位关键字、循环、稀疏/装饰数组、非法 required 与非 lossless annotations;编译和验证用显式任务栈而非递归调用栈。TOOL-SCHEMA-SUBSET

参数根由 DSL 编译成隐式 open object;嵌套显式 object 必须写 additionalProperties: true|false。因此“工具参数默认拒绝额外键”并不成立,除非作者在相应显式对象层选择 closed shape。

4. 只有 defineTool() 自动执行参数校验

Helper contract

defineTool() 编译参数与输出 schema,包装 execute,在 body 前调用 validator,并把路径化 violations 转为 ToolArgsError(code: INVALID_ARGS)。它还给 presenter 与 concurrency classifier 做 soft validation:旧日志参数不匹配时返回 undefined/false,而不是让 replay 或调度崩溃。TOOL-DEFINE-HELPER

直接注册边界

ToolRuntime.register() 会验证 output schema 和 timeout metadata,却不会把 parameters schema 应用于每次输入;执行创建阶段只做 lossless snapshot/freeze。直接构造原始 ToolDefinition 的第三方工具必须自己校验 arguments。模型可见 schema 是提示约束,不自动等于运行时输入 guard。TOOL-REGISTRATIONTOOL-PIPELINE-PREPARE

这不是第一方工具的缺口——它们使用 helper——但它是 extension author 的重要 trust boundary。把 raw registration 当作“registry 会替我 validate”会把任意 JSON 送进 body。

5. Scope view 先解析继承能力,再应用限制和本地 shadow

global tools
→ ancestor scope registrations (nearer shadows farther)
→ intersect every restriction along the scope chain
→ exact scope's own registrations (exempt from inherited filter)
→ reserved run_code transport when mode requires it
Visibility resolver

同一 layer 内重复 name 会失败;scoped registration 可 shadow inherited/global definition。Restriction 只能在 scoped context 注册,且只过滤继承能力;当前 scope 自己注册的 reporting/structured-output machinery 保留。多个 allow/deny filter 取交集,未知名字与 reserved transport fail loud。TOOL-SCOPE-VIEW

get()、model schema、SDK 和 dispatch 共用这个 view,降低“提示看见 A、执行却调用 B”的漂移。但定义与限制是 live effects:排队中的调用到真正 start 前仍可看到 registry change,这也是第 18 章重分类语义的基础。

6. Native、Code 与 Both 改的是模型入口,不是三套工具实现

ModeProvider toolsPrompt 增量直接执行边界
native全部 visible schemas普通 per-tool guidance模型直接调用 visible tool
code只有 reserved run_codecode-only rule + typed SDK模型直呼 native name 在 policy 前变成 UNKNOWN_TOOL;SDK subcall 可调用
bothvisible native schemas + run_codetyped SDK,无 code-only rule两种入口都可用
同源视图

每次 prompt assembly 从 calling scope 重新投影 name/description/parameters;只白名单这三个字段。Code SDK 额外拿 output schema 生成 typed return,而 run_code 被保留在 capability filter 之外。TOOL-PRESENTATION-MODESTOOL-SCHEMA-PROJECTION

在 code-only mode,native name 虽然用于 SDK catalog 仍“可见”,但 model-direct execution 会在 pre hooks、审批和 guards 之前确定性拒绝,避免 policy 误批准一个注定不能执行的入口;错误文本告诉模型应从 run_code 内调用。

7. 每次调用先获得不可伪造 identity,再进入 policy

字段来源用途
callIdProvider tool-call block / composite dispatcherSession call-result 关联
rootCallId根调用默认等于 callId,嵌套沿树传播整棵 composite execution 关联
tokenRegistry 新建的 same-process Symbol证明 nested parent 与 canonical result 属于本次 execution
agentAgent loop 显式提供,可选Scope routing、审批和 Session ownership
signalCaller 持有取消;around wrapper 的替换不能脱离它
Materialization

Registry 在 policy 前对 parsed arguments 做 lossless snapshot、detach、deep freeze;公开 execution 字段 readonly,最终 observer 收到前整个 execution object 再 freeze。Code Mode 只把 opaque parent token 交给 nested dispatcher,而不是泄漏可变 outer execution。TOOL-EXECUTION-IDENTITY

8. 完整管线:不同扩展点拥有不同权限

阶段允许做什么失败/拒绝后
tools/pre-executeallow / deny / ask;异步 waterfalldeny 形成 error result,仍可进 post
Approval seam把 ask 解析为 allowed-once 或具体 deny无 service、无 Agent、unavailable 都 fail closed
Monotonic guards同步返回首个 denial reason没有 allow 返回值,不能覆盖前一个 denial
tools/execute包裹下一层,替换本 wrapper lifetime 的 signalwrapper throw 直接变 final error
Tool body返回 canonical value;defer context;conclude turnbody throw 被规范化,仍进入 post
tools/post-executeaccept、替换 value 或 content、block、追加 contextthrow 变 final error
finalizeContent同步且只替换 content,覆盖成功与失败throw 变 final error,不再递归调用 finalizer
tools/result观察 frozen authoritative snapshotobserver failures 被记录并隔离
生产实现

Scheduler-facing prepare/dispatch/finalize/finish 只是把同一管线拆成“ordered policy”“可重叠 body”“ordered post/commit”三段;普通 ToolRuntime.execute() 仍把它们串成同一结果。TOOL-PIPELINE-PREPARETOOL-PIPELINE-DISPATCHTOOL-PIPELINE-FINALIZE

9. Pre-policy 与 Guard 刻意不能改 arguments

模型原始 argument string 已经作为 tool/call 写入日志,UI 也会用它展示 pending state。若 pre hook 可以替换 args,日志会声称执行了 A,body 实际收到 B,request replay 与审计都失真。因此 PreToolDecision 没有 rewrite arm。

单调授权

Waterfall 可选择 ask,但最终 allow 之后还要通过 global 再到 scope-chain 的每个 synchronous guard。Guard 只有“返回 reason 拒绝”或 abstain 两种结果;listener 顺序无法把已有 denial 重新提升为 permission。TOOL-DECISIONSTOOL-SCOPE-VIEW

审批的细节留给第 19 章;这里的核心性质是:可扩展 policy 可以提出允许意图,owner guard 仍有不可逆的最后拒绝权。

10. Around wrapper 可以换 signal,但不能切断 caller cancellation

tools/execute 适合 timeout、retry、metrics 等 around concerns。Wrapper 可以在委托期间替换 mutable exec.signal;ToolRuntime 在 body 前把这个 signal 与原 caller signal 融合,body settle 后移除 listeners 并恢复 wrapper view。

Quiescence

取消不会放弃已启动的 Promise。Registry 等 body 到达静止点,再把原本成功的结果替换为 ABORTED;body 尚未开始时使用 ABORTED_BEFORE_DISPATCH。同进程代码无法硬杀,工具作者必须传播 signal。TOOL-PIPELINE-DISPATCHTOOL-CANCELLATION

11. 成功 value 必须先过三道边界,才能成为 canonical result

1

Lossless snapshot

拒绝 undefined、BigInt、cycle、sparse array、非有限数、负零和 exotic graph。

2

Output schema

产生全部路径化 violations;不符合时转成 INVALID_TOOL_OUTPUT。

3

Pure projections

render 与可选 presentationMeta 的结果再次 snapshot;projection throw 也成为输出错误。

4

Per-execution mark

WeakMap 把 result 与 registry token 关联,防止 around wrapper 伪装成已验证 canonical result。

重新规范化

若 around wrapper 返回未经本 execution 标记的 success,Registry 只信它的 value,并重新走当前 tool 的 output validation/render;wrapper 自带 content 不会绕过 canonical projection。Error result 可被规范化保留。TOOL-CANONICAL-OUTPUT

12. Post-policy 可替换 value 或 content,但两者不能同时替换

Decision结果重新验证
accept 无替换保留 body/denial result,追加 contexts已有 canonical result 保持
accept {value}替换成功 canonical value重新跑 output schema、render、meta;失败结果不可变成 value
accept {content}只替换 model-facing blocks不重新解释 canonical value
block {feedback}转为 isError,feedback 既是 content 也是 message 来源只保留 blocker 明确附加的 contexts
控制权

Body deferred contexts 在 accept 时排在 post contexts 之前;block 会丢弃 body contexts,防止一个被政策否决的结果仍把隐藏 instruction 注入下一请求。TOOL-POST-POLICY

Content replacement 是刻意的 presentation/policy seam:它可以做脱敏、spill preview 或纠正反馈,而无需伪造新的 canonical value。代价是日志里的文本不再能证明 body 原始 render;需要 telemetry 或 tool-owned事件保留更早证据。

13. finalizeContent 是工具拥有的最后不变量

Finalizer 在调用开始时就从当时 visible definition 捕获,防止 arguments getter 或热重载在 snapshot 期间替换回调。它在所有 normalized outcome 上恰好尝试一次,包括 pre/around/post failure;它只能返回替换 content,不能改 isError、value、meta、contexts 或 concludesTurn。

提交末端

Registry 先把候选结果 materialize,运行 captured finalizer,再 materialize 一次并 deep-freeze;finalizer throw 会被转成普通 error result。随后 tools/result observer 只能看 frozen execution/result,任一 observer 同步或异步失败都不会改变调用返回。TOOL-PIPELINE-FINALIZE

为什么放最后

工具可用它保证“任何离开 registry 的 content 都满足本工具的最终格式/脱敏规则”,而通用 post-policy 仍不能越过它。相对地,错误 finalizer 会覆盖原错误文本为 finalizer 自身错误,审计时应结合更早日志/telemetry。

14. Additional context 和 conclude-turn 是结果携带的控制信号

Tool body 可调用 exec.deferContext(userMessage);post policy 也可附加 contexts。它们不在 body 中直接 append,而随最终 result 回到 Agent loop。Loop 先按模型顺序写完对应 tool/result,再把 contexts 放进 active-batch FIFO,下一 Step 才以拥有各自 source/meta 的 user messages 提交。

Turn control

exec.concludeTurn() 只会在最终成功结果上生成 concludesTurn:true;body 随后失败或 post block 时不会结束 Turn。Scheduler 只有在按模型顺序提交该结果后才汇总标记。TOOL-EXECUTION-IDENTITYTOOL-LOOP-COMMIT

这避免 side-channel context 插进 call/result 邻接中间,也让 composite transport 把 nested context/terminal signal 通过父结果显式转发,而不是直接突变父 Agent 的 inbox。

15. 顶层工具耐久性由 Agent loop 拥有,Registry 本身不写 Session

assistant/tool_call block
→ append tool/call (raw argument string)
→ ToolRuntime staged pipeline
→ append tool/result (final content + isError + error info + meta)
→ accept result.additionalContexts
→ next Step boundary
Commit ownership

Loop 在任何 pre-policy 之前先写 tool/call,保存 raw Provider arguments;即使 JSON 无效、被拒绝或 body 未启动,最终都能写一条成对 error result。Result 的 sourceEventSeqs 精确引用 call event。TOOL-LOOP-COMMIT

直接从其它进程内组件调用 ctx.tools.execute() 只得到 frozen result,不会自动产生顶层 Session events;若没有 Agent,审批 ask 也无法路由并 fail closed。Code Mode 是特例:outer run_code 仍由 loop 记账,bridge 另写 log-only nested dispatch events。

16. UI Presenter 是 provider-neutral render intent,并且必须可 replay

阶段可选 intent回退
Pending callgeneric / terminal / diffTool name + raw args 的 generic card
Completed resultgeneric / terminal / diff / search / read / web保留 pending title,渲染 raw result content
旧参数不匹配defineTool soft validation 返回 undefinedGeneric replay
Presenter throw / call 跨页缺失Host contain 或找不到 pairingEvent 仍发送,不带 view
Replay contract

Presenter 只能依赖 arguments 与耐久 result,因为 live UI 和历史 replay 都会调用它。Read line numbers、web sources 等不能从 model text 无损恢复的形状,必须先进入 durable meta,再由 presentResult 解释。TOOL-PRESENTATION-INTENTSTOOL-HOST-PRESENTER

17. 失败归一化按“body 是否启动”和“失败在哪一层”分类

失败点是否过 post最终典型 code / content
Code-only direct collapseUNKNOWN_TOOL,并给出 run_code 路由
Arguments 非 lossless JSON普通 error result;body 未启动
Pre deny / approval deny / guard reasonError: reason;post 可 block/替换 content
Pre/guard throw异常归一化为 final error
Unknown tool / body throw / invalid output结构化 HarnessError info(若有)+ Error text
Around wrapper throwfinal error
Post throw已在 post 失败finalizer 仍运行,然后发布 error
Finalizer throw不再进入 postfinalizer error 成为权威结果
Caller abort取决于到达阶段ABORTED_BEFORE_DISPATCH 或 ABORTED

errorMessage() 还对 hostile thrown value 做 total fallback,防止错误规范化自身再 throw。成功与失败最终都必须成为 lossless frozen snapshot,调用方无需用 exception control flow 区分普通工具失败。TOOL-DECISIONSTOOL-CANCELLATION

18. Runtime invariant 证明顺序与冻结,但不证明业务策略正确

Companion checks

Invariant 监听内部 dispatch,要求同一 execution 的 pre → execute → post 顺序合法,tools/result 时 execution/result/content 已冻结且 identity 非空;对 Code Mode nested events 还检查 root/parent lineage 与 open Turn enclosure。TOOL-RUNTIME-INVARIANT

它不会验证某个 raw tool 是否真的按 parameters schema 校验输入、timeout wrapper 是否加载、isConcurrencySafe 声明是否业务上安全、presenter 是否纯,也不会判断 policy 给出的 allow/deny 是否符合组织规则。Invariant 保护协议形状,不替代 capability owner 的策略测试。

19. 已确认的边界、动态窗口与设计取舍

发现分类影响
Raw ToolDefinition 输入不由 registry schema 自动校验Extension trust boundary第三方作者必须自己 validate,或使用 defineTool
timeoutMs 需要独立 policyComposition boundary漏挂 wrapper 时没有 deadline
Pre-policy 不能改 arguments有意审计取舍要“修复”输入只能拒绝并让模型重试
Canonical value 不进入 durable log数据最小化/Code Mode 取舍Replay 只能恢复 content/meta,不能恢复结构化中间值
Code runtime 在 prompt assembly 与 execution 分别读取已记录动态窗口若热切换到另一语言,程序可能按旧 SDK 写、在新 runtime 跑
Definition/registry 是 live effects动态组合取舍排队后、启动前 capability 可消失或重分类
同进程 body 只能 cooperative cancel执行模型限制忽略 signal 的 body 会延迟 quiescence
Code runtime window

源码明确注释:assembly 和 run_code execution 分别读取当前 runtime;单一稳定 backend 时无害,热重载跨语言时可能发生 schema/runtime mismatch,绑定到 request 被推迟到出现第二 backend 后。TOOL-CODE-RUNTIME-WINDOW

20. 设计评价与验证状态

选择收益代价
Canonical value 与 model content 分离结构化编排、受控上下文、UI meta 各自清晰Replay 不能重获执行期 value
能力 view 与 policy pipeline 分离同一实现可按 Agent scope 组合和限制Live registry 使时间边界更复杂
每阶段单独变换权限扩展性不会变成任意重写作者必须理解哪类 hook 解决哪类问题
Lossless snapshot + frozen final result日志、observer 与 replay 不受后续突变大 JSON graph 有显著复制成本
Caller-owned durable loggingRegistry 可用于 agentless/composite 场景直接调用者若需要审计必须自己建立 carrier
本章验证范围

本章交叉核对了 ToolDefinition、schema DSL/raw validator、scope layers、presentation modes、execution identity、全部 pipeline stages、Agent-loop top-level commit、Host presenter fallback 与 tools invariant,并阅读了生成执行图和核心 README。Upstream 依赖未安装,因此没有声称测试在本机通过;并行池、审批策略与 Code Mode worker 分别在后续专章继续展开。

我的学习体会

内容仅自动保存到当前浏览器,不上传、不进入仓库。你可以导出 Markdown 自行归档。