Tool Definition Primitives and the Execution Pipeline
Schemas, scope, execution modes, presentation, and waterfalls
Conclusion: ToolRuntime Is Not a Function Table; It Is the Transaction Boundary That Commits Untrusted Model Calls into Replayable Results
DeepSeek Harness's tool core has five simultaneous responsibilities: resolve what each Agent scope can see; project definitions into model schemas; mint an unforgeable execution identity for every call; order pre-policy, approval, monotonic guards, around wrappers, the body, post-policy, and a tool-owned finalizer; and publish exactly one lossless, frozen authoritative result. The Agent loop owns durable top-level call/result events, while UI presenters reconstruct cards from the same durable outcome.
The important property is not merely “it has hooks.” Each layer has a different mutation budget: pre-policy cannot rewrite already-logged arguments; guards may only deny; around wrappers may temporarily replace the signal and wrap the body; post-policy may replace the canonical value or model content; the finalizer may change only content; result observers may change nothing.
The generated execution graph and production source agree on pre → guards → around/body → post → normalization → finalization → frozen result, with Session logging and UI projection at owning boundaries outside the registry pipeline. TOOL-PIPELINE
1. One Tool Definition Contains Four Contracts
| Contract | Fields | Consumer | Sent to the model? |
|---|---|---|---|
| Invocation | name, description, parameters | Prompt and Provider adapter | Yes |
| Canonical output | output.schema, output.render, optional presentationMeta | ToolRuntime, Code Mode, UI replay | The output schema is not a native tool schema; rendered content enters history |
| Execution | execute, timeoutMs, isConcurrencySafe | Registry and external policy/scheduler | No |
| Presentation | presentCall, presentResult, finalizeContent | Host/UI and final commit stage | Presenters are hidden; finalized content enters model history |
A successful body must return a canonical lossless JSON value. The registry validates it with the output schema, then calls pure render(args,value) to produce provider-neutral ContentBlocks; a direct top-level call may also project durable JSON metadata. TOOL-DEFINITION
2. Model Schema, Execution Value, Durable Content, and UI Metadata Are Different Data
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
A successful in-process ToolExecutionResult carries a frozen value, but the top-level Session event deliberately persists only content, failure state, and optional metadata. The canonical value is not guaranteed reconstructable from the log. UI structure that cannot be losslessly recovered from prose must be projected explicitly through presentationMeta. TOOL-RESULT-TYPESTOOL-CANONICAL-OUTPUTTOOL-LOOP-COMMIT
This lets Code Mode pass a structured value back to a program for further computation while the main model sees only controlled rendered text. It also prevents a UI from reverse-parsing natural-language results to recover diffs, line numbers, or web sources.
3. The Schema DSL Is an Enforced JSON Schema Subset, Not the Complete Standard
| Supported | Rule | Explicitly not done |
|---|---|---|
| Scalars | string/number/integer/boolean/null and type-correct enum/const | Keywords such as pattern, minimum, and format are rejected |
| Containers | array/items; object/properties/required/boolean additionalProperties | An explicit object must declare openness to avoid an implicit default |
| Union | Exact-one oneOf with at least two branches | Conflicting sibling constraints beside oneOf are rejected |
| Any JSON | The author DSL's type: "json" compiles to an unconstrained node | The value must still be lossless JSON |
| Annotations | description/title/default/examples | Defaults are annotations and are never applied |
The raw-schema walker rejects unknown or misplaced keywords, cycles, sparse or decorated arrays, invalid required lists, and non-lossless annotations. Compilation and validation use explicit task stacks instead of recursive JavaScript calls. TOOL-SCHEMA-SUBSET
The parameter root compiles as an implicit open object; every nested explicit object must state additionalProperties: true|false. “Tool parameters reject extra keys by default” is therefore false unless the author closes the relevant explicit object.
4. Only defineTool() Automatically Validates Arguments
defineTool() compiles parameter and output schemas, wraps execute, runs validation before the body, and turns path-qualified violations into ToolArgsError(code: INVALID_ARGS). It also softly validates presenters and the concurrency classifier: mismatched historical arguments return undefined or false instead of crashing replay or scheduling. TOOL-DEFINE-HELPER
ToolRuntime.register() validates the output schema and timeout metadata but does not apply the parameters schema to each invocation. Execution creation only takes a lossless snapshot and freezes arguments. A third-party raw ToolDefinition must validate its own input. A model-visible schema is a prompting contract, not automatically a runtime input guard. TOOL-REGISTRATIONTOOL-PIPELINE-PREPARE
This is not a gap in first-party tools, which use the helper, but it is an important extension-author trust boundary. Treating raw registration as “the registry validates for me” sends arbitrary JSON into the body.
5. Scope Resolution Builds Inherited Capability, Then Applies Restrictions and Local Shadowing
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
A duplicate name in one layer fails; a scoped registration may shadow an inherited or global definition. Restrictions require a scoped context and filter inherited capability only, preserving the exact scope's reporting and structured-output machinery. Multiple allow/deny filters intersect, and unknown names or the reserved transport fail loudly. TOOL-SCOPE-VIEW
get(), model schemas, the generated SDK, and dispatch share this view, reducing “the prompt showed A but execution selected B” drift. Definitions and restrictions are live effects, however: a queued call can observe a registry change before it actually starts. Chapter 18 follows that reclassification behavior.
6. Native, Code, and Both Change the Model Entry Point, Not the Tool Implementations
| Mode | Provider tools | Prompt addition | Direct-execution boundary |
|---|---|---|---|
native | All visible schemas | Ordinary per-tool guidance | The model directly calls a visible tool |
code | Only reserved run_code | Code-only rule plus typed SDK | A direct native name becomes UNKNOWN_TOOL before policy; an SDK subcall may invoke it |
both | Visible native schemas plus run_code | Typed SDK without the code-only rule | Both entry forms are callable |
Every prompt assembly reprojects name, description, and parameters for the calling scope and allowlists only those three fields. The Code SDK additionally consumes output schemas for typed returns, while run_code stays outside capability filtering. TOOL-PRESENTATION-MODESTOOL-SCHEMA-PROJECTION
In code-only mode, native names remain visible to the SDK catalog, but a model-direct execution is deterministically rejected before pre hooks, approval, and guards. Policy cannot accidentally approve an entry point that can never run, and the error tells the model to call it inside run_code.
7. Every Call Receives an Unforgeable Identity Before Policy
| Field | Source | Purpose |
|---|---|---|
callId | Provider tool-call block or composite dispatcher | Session call/result correlation |
rootCallId | Defaults to callId for a root and propagates down a nested tree | Correlate one composite execution tree |
token | A same-process Symbol minted by the registry | Prove nested parentage and that a canonical result belongs to this execution |
agent | Explicitly supplied by the Agent loop; optional | Scope routing, approval, and Session ownership |
signal | Owned by the caller | Cancellation that an around wrapper cannot detach |
Before policy, the registry losslessly snapshots, detaches, and deep-freezes parsed arguments. Public execution fields are readonly, and the complete execution object is frozen before final observers receive it. Code Mode gives a nested dispatcher only the opaque parent token rather than exposing the mutable outer execution. TOOL-EXECUTION-IDENTITY
8. The Complete Pipeline Gives Each Extension Point a Different Authority
| Stage | Permitted action | After failure or denial |
|---|---|---|
tools/pre-execute | allow / deny / ask through an async waterfall | A denial becomes an error result that can still reach post |
| Approval seam | Resolve ask into allowed-once or a specific denial | No service, no Agent, or unavailable channel fails closed |
| Monotonic guards | Synchronously return the first denial reason | No allow return exists, so another guard cannot reverse denial |
tools/execute | Wrap the next stage and replace the signal for this wrapper's lifetime | A wrapper throw becomes a final error |
| Tool body | Return a canonical value, defer context, conclude a Turn | A body throw is normalized and still reaches post |
tools/post-execute | Accept, replace value or content, block, add context | A throw becomes a final error |
finalizeContent | Synchronously replace content only, for successes and failures | A throw becomes a final error; the finalizer is not called recursively |
tools/result | Observe the frozen authoritative snapshot | Observer failures are logged and contained |
The scheduler-facing prepare/dispatch/finalize/finish view merely splits this pipeline into ordered policy, overlap-eligible body work, and ordered post/commit. Ordinary ToolRuntime.execute() still composes them into one result. TOOL-PIPELINE-PREPARETOOL-PIPELINE-DISPATCHTOOL-PIPELINE-FINALIZE
9. Pre-Policy and Guards Deliberately Cannot Rewrite Arguments
The model's raw argument string has already been appended as tool/call, and the UI uses it for the pending card. If a pre hook could replace arguments, the log would claim A ran while the body received B, breaking replay and audit. PreToolDecision therefore has no rewrite arm.
The waterfall may request approval, but a final allow must still pass every synchronous guard from global through the scope chain. A guard can return a denial reason or abstain; listener order cannot elevate an existing denial back into permission. TOOL-DECISIONSTOOL-SCOPE-VIEW
Chapter 19 covers approval in detail. The core property here is that extensible policy may propose an allow, while an owner guard retains an irreversible final veto.
10. An Around Wrapper May Replace the Signal but Cannot Sever Caller Cancellation
tools/execute hosts around concerns such as timeouts, retries, and metrics. A wrapper may replace mutable exec.signal while delegating. Before entering the body, ToolRuntime fuses that signal with the original caller signal; after settlement it removes listeners and restores the wrapper view.
Cancellation never abandons a started Promise. The registry waits for the body to settle, then replaces an otherwise successful result with ABORTED; if the body never began, it uses ABORTED_BEFORE_DISPATCH. Same-process code cannot be hard-killed, so tool authors must forward the signal. TOOL-PIPELINE-DISPATCHTOOL-CANCELLATION
11. A Successful Value Crosses Three Boundaries Before Becoming Canonical
Lossless snapshot
Reject undefined, BigInt, cycles, sparse arrays, non-finite numbers, negative zero, and exotic graphs.
Output schema
Produce all path-qualified violations; failure becomes INVALID_TOOL_OUTPUT.
Pure projections
Snapshot render and optional presentationMeta again; a projection throw is also an output error.
Per-execution mark
A WeakMap ties the result to the registry token, preventing an around wrapper from impersonating a validated result.
If an around wrapper returns a success not marked for this execution, the registry trusts only its value and reruns the current tool's output validation and rendering. Wrapper-authored content cannot bypass the canonical projection. Error results are normalized separately. TOOL-CANONICAL-OUTPUT
12. Post-Policy May Replace Value or Content, but Never Both at Once
| Decision | Outcome | Revalidation |
|---|---|---|
accept without replacement | Keep body or denial result and append contexts | The existing canonical result remains |
accept {value} | Replace a successful canonical value | Rerun output schema, render, and metadata; a failure cannot become a value |
accept {content} | Replace model-facing blocks only | Do not reinterpret the canonical value |
block {feedback} | Produce isError; feedback supplies content and the message | Keep only contexts explicitly supplied by the blocker |
Body-deferred contexts precede post-policy contexts on accept. Block discards body contexts so a policy-rejected result cannot still inject a hidden instruction into the next request. TOOL-POST-POLICY
Content replacement is a deliberate presentation and policy seam. It supports redaction, spill previews, or corrective feedback without fabricating a new canonical value. The cost is that durable text no longer proves the body's original rendering; earlier evidence needs telemetry or tool-owned events.
13. finalizeContent Is the Tool-Owned Last Invariant
The finalizer is captured from the then-visible definition when the call starts, preventing an argument getter or hot reload from replacing it during snapshotting. It is attempted exactly once for every normalized outcome, including pre, around, and post failures. It may return replacement content only; it cannot alter isError, value, metadata, contexts, or concludesTurn.
The registry materializes the candidate, runs the captured finalizer, materializes again, and deep-freezes the outcome. A finalizer throw becomes an ordinary error result. tools/result observers then receive frozen execution and result objects; synchronous or asynchronous observer failure cannot alter the returned outcome. TOOL-PIPELINE-FINALIZE
A tool can guarantee that every content value leaving the registry satisfies its own final format or redaction rule, beyond generic post-policy. Conversely, a faulty finalizer replaces the original error text with its own error, so audit may require earlier telemetry or events.
14. Additional Context and Conclude-Turn Are Control Signals Carried by the Result
A body may call exec.deferContext(userMessage), and post-policy may attach contexts. Neither appends directly from inside the body. They return with the final result to the Agent loop. The loop writes the corresponding tool/result first, then places contexts into the active-batch FIFO; the next Step commits them as user messages retaining their own source and metadata.
exec.concludeTurn() creates concludesTurn:true only on a final successful result. If the body later fails or post-policy blocks it, the Turn is not concluded. The scheduler aggregates the marker only after committing that result in model order. TOOL-EXECUTION-IDENTITYTOOL-LOOP-COMMIT
This keeps side-channel context out of call/result adjacency and forces a composite transport to ferry nested context or terminal intent through its parent result rather than mutating the parent Agent's inbox directly.
15. The Agent Loop Owns Top-Level Durability; the Registry Itself Does Not Write the 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
The loop appends tool/call before any pre-policy, preserving raw Provider arguments. Invalid JSON, denial, or a body that never starts can still receive one paired error result. The result's sourceEventSeqs cites the exact call event. TOOL-LOOP-COMMIT
An in-process component that calls ctx.tools.execute() directly receives a frozen result but does not automatically create top-level Session events. An Agent-less call also cannot route approval and fails closed on ask. Code Mode is the exception: the loop logs the outer run_code, while its bridge appends separate log-only nested dispatch events.
16. UI Presenters Emit Provider-Neutral Render Intent and Must Survive Replay
| Phase | Optional intent | Fallback |
|---|---|---|
| Pending call | generic / terminal / diff | A generic card with tool name and raw arguments |
| Completed result | generic / terminal / diff / search / read / web | Keep the pending title and render raw result content |
| Historical arguments no longer validate | defineTool soft validation returns undefined | Generic replay |
| Presenter throws or call is missing across a page | The Host contains it or cannot pair it | The event still ships without a view |
A presenter may depend only on arguments and the durable result because both live UI and historical replay invoke it. Shapes such as read line numbers or web sources that cannot be reconstructed from model text must first enter durable metadata and then be interpreted by presentResult. TOOL-PRESENTATION-INTENTSTOOL-HOST-PRESENTER
17. Failure Normalization Depends on Whether the Body Started and Which Layer Failed
| Failure point | Runs post? | Typical final code / content |
|---|---|---|
| Code-only direct collapse | No | UNKNOWN_TOOL with a route through run_code |
| Arguments are not lossless JSON | No | Ordinary error result; body never starts |
| Pre denial / approval denial / guard reason | Yes | Error: reason; post may block or replace content |
| Pre or guard throws | No | The exception is normalized as a final error |
| Unknown tool / body throw / invalid output | Yes | Structured HarnessError info when present plus Error text |
| Around wrapper throws | No | Final error |
| Post throws | It failed inside post | The finalizer still runs, then publishes an error |
| Finalizer throws | Does not re-enter post | The finalizer error becomes authoritative |
| Caller aborts | Depends on reached stage | ABORTED_BEFORE_DISPATCH or ABORTED |
errorMessage() also has a total fallback for hostile thrown values, preventing error normalization from throwing again. Success and failure both end as lossless frozen snapshots, so ordinary tool failures do not require exception control flow at the caller. TOOL-DECISIONSTOOL-CANCELLATION
18. The Runtime Invariant Proves Ordering and Freezing, Not Business-Policy Correctness
The invariant observes internal dispatch, requires legal pre → execute → post ordering for one execution, and checks that execution, result, and content are frozen with nonempty identity at tools/result. For nested Code Mode events it also checks root/parent lineage and open-Turn enclosure. TOOL-RUNTIME-INVARIANT
It does not prove that a raw tool validates against its parameter schema, that the timeout wrapper is mounted, that an isConcurrencySafe claim is operationally true, that a presenter is pure, or that policy's allow/deny decision matches an organization's rules. The invariant protects protocol shape; it does not replace capability-owner policy tests.
19. Confirmed Boundaries, Dynamic Windows, and Design Tradeoffs
| Finding | Classification | Consequence |
|---|---|---|
| Raw ToolDefinition inputs are not automatically schema-validated by the registry | Extension trust boundary | Third-party authors must validate or use defineTool |
timeoutMs requires a separate policy | Composition boundary | A missing wrapper means no deadline |
| Pre-policy cannot rewrite arguments | Deliberate audit tradeoff | “Repairing” input requires denial and model retry |
| Canonical values do not enter the durable log | Data-minimization and Code Mode tradeoff | Replay recovers content and metadata, not structured intermediates |
| The Code runtime is read separately during prompt assembly and execution | Documented dynamic window | A cross-language hot swap could run code written for the old SDK in the new runtime |
| Definitions and registry entries are live effects | Dynamic-composition tradeoff | A capability can disappear or be reclassified after queueing and before start |
| Same-process bodies support cooperative cancellation only | Execution-model limitation | A body that ignores the signal delays quiescence |
Source comments explicitly state that assembly and run_code execution read the current runtime separately. This is harmless with one stable backend, but a cross-language hot reload can create a schema/runtime mismatch; binding the runtime to the request is deferred until a second backend makes the case testable. TOOL-CODE-RUNTIME-WINDOW
20. Design Assessment and Verification Status
| Choice | Benefit | Cost |
|---|---|---|
| Separate canonical value from model content | Clear structured orchestration, controlled context, and UI metadata | Replay cannot recover the execution-time value |
| Separate capability view from the policy pipeline | One implementation composes and restricts per Agent scope | A live registry makes temporal boundaries more complex |
| Give every stage a distinct mutation budget | Extensibility does not become arbitrary rewriting | Authors must know which hook solves which concern |
| Lossless snapshots and a frozen final result | Logs, observers, and replay resist later mutation | Large JSON graphs incur material copying cost |
| Caller-owned durable logging | The registry works for Agent-less and composite scenarios | Direct callers that need audit must provide their own carrier |
This chapter cross-checked ToolDefinition, the schema DSL and raw validator, scope layers, presentation modes, execution identity, every pipeline stage, Agent-loop top-level commit, Host presenter fallback, and the tools invariant, plus the generated execution graph and core README. Upstream dependencies are not installed, so no upstream test execution is claimed. Later chapters cover the parallel pool, approval policy, and the Code Mode worker in depth.
My Learning Notes
Autosaved only in this browser. Nothing is uploaded or committed. Export Markdown whenever you want to keep a copy.