Overview
dsh-tools
Source-level overviewTool registry and execution pipeline. Tool plugins register their schemas and executors; the agent loop executes each call through tools/pre-execute (the extensible allow/deny gate) → monotonic registered guards → tools/execute (an around-dispatch wrapper for timeout/retry/metrics plugins) → tools/post-execute (inspect/replace the result, attach context) → the definition-owned finalizeContent boundary → the observe-only tools/result notification. The registry also owns HOW its tools are presented to the model — its mode config selects native function calling, Code Mode, or both, and one agent shadows that default for itself with presentAs.Collapse technical overview
tools/pre-execute (the extensible allow/deny gate) → monotonic registered guards → tools/execute (an around-dispatch wrapper for timeout/retry/metrics plugins) → tools/post-execute (inspect/replace the result, attach context) → the definition-owned finalizeContent boundary → the observe-only tools/result notification. The registry also owns HOW its tools are presented to the model — its mode config selects native function calling, Code Mode, or both, and one agent shadows that default for itself with presentAs.This is an atomic module already shipped with Harness, not a standalone profile layer.
Capabilities
What it contributes
README / EN
Package documentation
dsh-tools
English | 中文
Tool registry and execution pipeline. Tool plugins register their schemas and executors; the agent loop executes each call through tools/pre-execute (the extensible allow/deny gate) → monotonic registered guards → tools/execute (an around-dispatch wrapper for timeout/retry/metrics plugins) → tools/post-execute (inspect/replace the result, attach context) → the definition-owned finalizeContent boundary → the observe-only tools/result notification. The registry also owns HOW its tools are presented to the model — its mode config selects native function calling, Code Mode, or both, and one agent shadows that default for itself with presentAs.
Service: ToolRuntime (ctx key: tools)
Config
tools:
mode: native # native (default) | code | both
native contributes visible tools as function definitions. code contributes the reserved run_code transport, the generated tools:sdk section, and the tools:code-only rule stating that only run_code may be called directly — which the executor then enforces, resolving a model-direct call naming any other tool to UNKNOWN_TOOL before policy runs; both contributes both forms and states no such rule, because its native calls do execute. This is the default for agents that declare none of their own — an agent preset selects its own with dsh-agent-tool-presentation. The reserved transport cannot be registered, shadowed, restricted, or removed, and its name is reserved whatever the configured mode, because any agent may select a code mode. Non-native modes require a ctx.codeRuntime whose language has a registered SDK renderer — TypeScript ships via dsh-code-runtime-worker-thread; a Python renderer is built in and drives any runtime that reports language: 'python' (a first-party dsh-code-runtime-python backend is delivered separately). A runtime language with no renderer fails prompt assembly loudly, and a systemPrompt.toolOrder entry for a tool the mode does not contribute rejects prompt assembly. A system-prompt/assemble listener may replace the registry's contributions; its returned assembly is authoritative, so that listener owns preserving a usable Code Mode protocol.
Public API
ctx.tools.register(definition: ToolDefinition): () => voidRegister a trusted typed same-process definition with a mandatory canonicaloutputdeclaration. The layer is the calling context's scope: a plain plugin context registers globally; an agent'sagent.ctxregisters for that agent alone, shadowing a same-named global tool there. Duplicate names within one layer throw; non-native modes also reject the reservedrun_codetransport name. Missing or unsupported output declarations and a non-positive or non-finitetimeoutMsfail at registration. The optional synchronousfinalizeContentcallback is snapshotted when a call starts and may replace only final model-facing content after every pipeline outcome is normalized, including an error discovered while materializing another result field. Disposed with the calling fiber.ctx.tools.presentAs(mode: ToolPresentationMode): () => voidselects this agent's model-facing presentation, shadowing themodeconfig for that agent alone; it throws from a plain context (a process-wide presentation is the config field) and from a second declaration in the same scope. A code mode also registers that agent's owntools:sdksection. The catalog is unchanged —schemas(agent)still reports the agent's capabilities; only the assembly's tools collapse. Disposed with the calling fiber.ctx.tools.restrict(filter)applies an agent-scoped allow/deny mask to global tools and throws from a plain context. The filter is snapshotted at registration; multiple masks intersect and scope-local tools merge afterwards. Deny masks admit later unnamed globals, while allow masks exclude later names. Unknown, local, or reserved names and empty filters reject. This is live visibility composition, not an authority boundary; see the scope security non-goal.ctx.tools.get(name: string, scope?: ScopeKey): ToolDefinition | undefinedResolution as one scope sees it (shadowing applied; a restricted-away global reads as absent) — presenters pass the calling agent so the card matches what executed.ctx.tools.schemas(scope?: ScopeKey): ToolSchema[]Schemas of everything the scope can see (without theexecutefunctions). The shipped tools' schemas are catalogued in docs/tool-catalog.md, generated by booting each tool plugin and harvesting this method (see the tool-schema-catalog Agent Note).ctx.tools.guard(guard: ToolGuard): () => voidRegister a monotonic synchronous execution guard aftertools/pre-execute: returning a reason denies the call, whileundefinedleaves it unchanged. A plain-context guard applies globally; anagent.ctxguard applies only to that agent. Later waterfall listeners cannot turn a guard denial back into permission. Disposed with the calling fiber.ctx.tools.execute(exec)losslessly snapshots and freezes arguments, assigns an opaque token, runs the complete policy/dispatch/result pipeline, then independently snapshots the authoritative outcome before final observation. Invalid arguments use the same result path without reaching policy or the body. Around wrappers may replace onlysignal; the registry re-fuses the original caller signal immediately before the body.ctx.tools.executionMode(exec)returnsparallelonly when the visible definition'sisConcurrencySafe(exec.arguments)classifier returns exactlytrue; unknown, hidden, undeclared, invalid, or throwing classifications are exclusive.
Injected services
SystemPrompt — the registry automatically feeds its tool schemas into the system-prompt assembly via ctx.systemPrompt.tools(). The approval seam is consumed opportunistically instead (ctx.get('approval'), no static inject): a deployment without it keeps the ask→deny degrade, and the registry stays active either way.
Cancellation
Cancellation is cooperative and quiescent. Every typed invocation supplies a caller-owned AbortSignal; tool bodies receive it as required readonly exec.signal, while only tools/execute wrappers may temporarily replace the required signal. The registry preserves caller cancellation through replacement and never races away from a started same-process promise. Cancellation before body invocation is ABORTED_BEFORE_DISPATCH; cancellation after invocation can replace only a successful outcome with ABORTED. A denial, wrapper failure, tool failure, post-policy failure, or timeout-owned TOOL_TIMEOUT remains more specific. A pre-aborted entry materializes and freezes arguments, then skips every policy and dispatch phase and publishes one result. Every async tool must observe or forward the signal and settle only after owned work stops. The tool-cancellation Agent Note owns the full contract and hard-termination limit.
Live events
The live registry pipeline has three transformable waterfalls, then the definition-owned content finalizer, then the observe-only tools/result event; registry changes are deliberately unfiltered shared-state notifications. Exact signatures, dispatch modes, scope filtering, and failure containment contracts live in the generated region of tools.md, while the complete ordering is visualized in the generated tool execution pipeline. tools/result is live; the similarly named tool/result is the durable session event the agent loop appends afterwards.
Key types
ToolDefinition—ToolSchema+ mandatoryoutput { schema, render, presentationMeta? }+execute(args, exec), optional final-content and presentation callbacks, cooperativetimeoutMs, and optional per-callisConcurrencySafe(args)classification. A body returns only the canonical JSON value declared by the output schema and cooperatively stops throughexec.signal.finalizeContent(exec, result)runs exactly once for every normalized result, including failures that bypass post-policy, and can replace onlycontent; it must be synchronous and total.ToolExecutionInput— the caller-supplied call description:{ callId, name, arguments, signal, agent?, parent? };signalis required and readonly, callers may pass an enclosing execution's opaque token asparent, and callers never choose the new execution's own token.ToolExecutionToken— a fresh brandedSymbolassigned by the registry. It supports equality correlation only and never crosses a model, log, or worker boundary.ToolExecution— the readonly pipeline view: immutable{ token, callId, name, arguments, signal, agent?, parent? }; the registry separately retains and re-fuses the original caller signal.ToolDispatchExecutionis thetools/execute-only view whose required signal is mutable, so a wrapper may replace and restore it but cannot delete it. A nested call'sparentis aToolExecutionToken, not an execution object.ToolRunContext— the execution passed to a tool body, extendingToolExecutionwithdeferContext(context). It defers one context until the tool's final result reaches the loop — typically a nested-dispatch context ferried by a composite tool, or a fresh plugin-sourced instruction minted by a leaf tool (tool-goal's wrap-up) — even when the tool later throws or cancellation wins; it never injects immediately.ToolExecutionResult— discriminated execution-local outcome. Success is{ isError:false, value:JsonValue, content, meta?, additionalContexts? }; failure is{ isError:true, error:{ message, info? }, content, meta?, additionalContexts? }and has no value. Call identity stays on the immutableToolExecution. The registry snapshots, validates, and freezes the canonical value before rendering, then materializes the durable presentation fields before final observation.ToolFailure.infocarries an internal{ name, code }for aHarnessError;additionalContextspreserves every deferred or post-execute identifiedUserMessagefor the loop's post-result FIFO.PreToolDecision—{kind:'allow'}|{kind:'deny', reason}|{kind:'ask', reason?}. Input rewrite is deliberately not offered;askis serviced byctx.approvalwhen mounted and otherwise degrades to deny.PostToolDecision— accept may replacecontentorvalue, never both, and may attachadditionalContexts; block turns feedback into a valueless failure. Content replacement preserves the canonical value and metadata. Value replacement is revalidated and rerenders content/metadata. Accept preserves tool-deferred contexts before decision contexts; block discards tool-deferred contexts and exposes only contexts explicitly supplied by the blocking decision.ToolGuard—(execution) => string | undefined; the returned string is a final monotonic denial reason evaluated after the reorderable pre-execute waterfall and before dispatch.ToolCallView/ToolResultView— provider-neutralcard-tagged render intents a tool returns frompresentCall/presentResultto own how a UI renders ITS calls (see "Tool-owned UI presentation").
Extension points
- Tool plugins call
ctx.tools.register()— schemas flow into the assembly automatically. tools/pre-executeis the reorderable allow/deny/ask gate;ctx.tools.guard()adds monotonic owner policy after it.tools/executewraps normalized canonical dispatch for timeout, retry, or metrics. Wrappers may replace only the operational signal; a wrapper-authored success is normalized through the resolved tool's output declaration. Each canonical result belongs to one immutable dispatch token, so a cached result from another call or tool is revalidated under the active declaration.tools/post-executemay replace presentation content, replace the canonical value, block with feedback, or attach ordered contexts. A definition's optionalfinalizeContentthen owns its last content-only invariant across normal results and outer pipeline failures;tools/resultobserves the immutable final outcome. Content replacement is not a confidentiality boundary: block or replace the value when programmatic consumers must not receive it.- Exact signatures and ordering live in the generated region of tools.md and pipeline.
- MCP servers: one plugin per server, discover tools, call
ctx.tools.register()with the server's schemas.
Typed tool parameter schemas
First-party plugin authors can use the defineTool() helper (exported from this package) for typed tool parameter schemas:
import { readFile } from 'node:fs/promises'
import type { Context } from '@deepseek-ai/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 })
},
}))
The unified schema DSL uses ParameterSchemaSpec for the implicit open parameter object and ValueSchemaSpec for any JSON-value root. It supports string, number, integer, boolean, null, array, object, author-only json, and exact-one oneOf; scalar enum/const values are type-correct. Every explicit DSL object declares additionalProperties: true | false, while the implicit parameter root and raw JSON Schema keep the standard open default. Schema records accept only own enumerable string keys, and schema arrays must be dense ordinary arrays. Compilation, validation, registry detachment, and schema-to-TypeScript rendering use explicit work stacks, so runtime processing of valid deep schemas is memory-bounded rather than call-stack-bounded; InferValue preserves exact types through 16 container levels and then falls back to JsonValue so TypeScript itself remains stack-safe.
A defineTool definition validates model arguments before execution and turns missing required values, wrong primitives, invalid enum members, and nested violations into ToolArgsError (INVALID_ARGS) for the normal error-result path. It also infers the body return and pure output projectors from output.schema; the registry snapshots and validates the returned lossless JSON before presentation. The implicit parameter root is open; an explicit object accepts extra keys only with additionalProperties: true, and a closed object with no declared properties accepts only {}. Raw JSON Schema objects remain open unless they explicitly set additionalProperties: false. Defaults are not applied; open objects without properties and arrays without items receive only a container type check. Raw-registered tools own input validation but still declare and receive registry-enforced output.
See defineTool, validateArgs, ToolArgsError, ValueSchemaSpec, ParameterSchemaSpec, InferValue, InferArgs, valueSchemaSpecToJsonSchema, and parameterSchemaSpecToJsonSchema in the public API for details.
Optional timeoutMs must be positive and finite; it is policy metadata, not model-visible schema.
Optional isConcurrencySafe(args) receives typed, softly validated arguments. Exact true permits concurrent dispatch/body execution; invalid input and all other outcomes remain exclusive. Opted-in bodies do not mutate parent-owned state, and shared-state races must commute or fail closed. The parallel tool-call Agent Note owns the full safety contract.
Enforced raw JSON Schema subset
JsonSchemaNode is the raw counterpart shared by tool outputs, Code Mode generation, subagents, and workflows. It permits any JSON root, an annotation-only unconstrained JSON node, and exact-one oneOf; annotations must remain lossless JSON. assertSupportedJsonSchema() rejects unsupported constructs, while validateJsonSchemaValue() returns path-qualified violations. Subagents and workflows retain their caller-defined object-root requirement through assertObjectJsonSchema() and ObjectJsonSchema, not through a limitation in the shared vocabulary.
Tool-owned UI presentation
Tools optionally own pure presentCall() and presentResult() render intents, so UIs do not special-case tool names:
- Call views are
{ card: 'generic', title, kind?, rawInput?, content?, locations? },{ card: 'terminal', title, description?, cwd? }, or{ card: 'diff', title, diffs, locations? }. - Result views are
{ card: 'generic', title?, content? },{ card: 'terminal', title?, output?, exitCode?, signal? },{ card: 'diff', title?, diffs },{ card: 'search', shape, title?, truncated, total, … }(a completed discovery search — grouped-by-file matches forshape: 'matches'(grep) or a flat path list forshape: 'paths'(glob), withtruncated/totalso a UI never presents a capped result as complete; the view carries no result text and a search has nocard: 'search'call-time analogue),{ card: 'read', title?, path, offset, lines, totalLines, lang?, content? }(a completed file read → a line-numbered, optionally syntax-highlighted code view;offsetis the 1-based first line the window requested, kept even whenlinesis empty;linesis{ number, text }[]keeping each file line number, andcontentis the envelope-stripped text a UI without read support falls back to), or{ card: 'web', kind: 'search' | 'fetch', title?, … }(a completed web retrieval; thekindarms carry the structured search sources or the fetch summary, and a UI without thewebcapability falls back to the raw result content).
Returning undefined selects generic fallback. Presenters depend only on their arguments and the durable result because UIs call them during live streaming and log replay. output.presentationMeta(args, value) derives JSON metadata for direct top-level calls; that metadata persists with tool/result and returns to presentResult, while the canonical value itself remains execution-local and is never replayed. Nested Code dispatches do not compute metadata. defineTool soft-validates older logged arguments and falls back instead of crashing replay. dsh-tool-bash and dsh-tool-fs are the reference implementations; the canonical-output Agent Note owns the value/presentation split and the render-intent Agent Note owns card vocabulary.
Code Mode
Under code or both, the registry exposes the reserved run_code transport and a deterministic SDK for the current scope, generated in the loaded runtime's language — the registry selects the renderer by ctx.codeRuntime.language (typescript → the TypeScript SDK below, python → the Python SDK). Only the program's outer logs and return value re-enter model context. The SDK declares exact per-tool argument and canonical-output types for every visible tool (ToolArgsMap/ToolOutputMap in TypeScript, named TypedDicts in Python), and each binding resolves to the tool's canonical JSON value. Each lossless-JSON binding call re-enters the complete tool pipeline under the native scheduling contract (concurrency-safe calls may overlap up to maxParallelSubCalls; exclusive calls run alone as ordering barriers) with logged correlation to the outer call. Denials and other failed results reject with the real program-visible ToolCallError carrying only toolName and message; Native content and internal error codes stay outside the Code contract. Ordinary side effects are not rolled back, and sub-call additionalContexts are deferred through the parent result to preserve call/result adjacency. Run settlement aborts and drains outstanding bindings; runtime failures surface as CodeRunFailedError.
Under code — not both — the transport is also the only entry the model may use: a model-direct call naming any other visible tool resolves to UNKNOWN_TOOL at execution creation, before tools/pre-execute, approval ask, and guards, so nothing observes or approves a call that can only fail. The denial names the route back (only \run_code` is callable directly — call `` from inside a `run_code` program instead), because the same prompt declares that tool and a bare unknown toolreads as a broken deployment. SDK sub-dispatches carry the outer execution'sparenttoken and are exempt, so programs keep every binding the SDK declared. See the [executor-collapse note](../../../.agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.md), the [Code Mode foundation](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md), [typed-return contract](../../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md), and [code-runtime seam](../../code-runtime/README.md). Trypnpm run demo:code-mode`.
- The SDK section (
tools:sdk, order 150): a lazy prompt section regenerating the language-appropriate SDK text at each assembly. In the TypeScript flavor it emitsJsonValue, exactToolArgsMap/ToolOutputMap,ToolName, theToolCallErrordeclaration, and a mappedtoolsnamespace for the calling scope's visible end capabilities (exotic names via quoted keys), plus fixed usage instructions; the Python flavor (ctx.codeRuntime.language === 'python') emits the equivalent namedTypedDicts and atoolsobject with matching usage instructions. Deterministic — lexicographic tool order, byte-identical text for an unchanged tool set (prefix-cache-friendly). Both codegens are exported and never throw during prompt assembly:jsonSchemaToTshandles every unified schema construct and degrades unsupported raw constructs tounknown;jsonSchemaToPydoes the same, degrading toAny(and a whole object todict[str, Any]when a field name is not a legalTypedDictattribute, or whenever it is called outside the SDK render, which supplies the naming context aTypedDictdeclaration needs). - The dispatch bridge (
run_code's execute): every binding call is snapshotted as lossless JSON before dispatch (undefined,BigInt, cycles, sparse arrays,-0, and exotic objects reject that one call), scheduled through a per-run pool that reuses the native concurrency contract — calls start strictly in submission order, consecutiveisConcurrencySafecalls overlap up to the validatedmaxParallelSubCallsconfig (default 10;1restores serial dispatch), and an exclusive-classified call drains the pool, runs alone, and bars later calls — given the outer execution's opaque token asparent, and run through the complete pre-execute → guards → execute → post-execute → result pipeline. A success returns the final canonical value after policy; a failure reaches the worker as one message and becomesToolCallError(toolName, message). Each started sub-call logs atool/code-dispatch-startevent (deterministic id<parent>:code:<n>, numbered by submission) at pipeline entry and settles with onetool/code-dispatchevent carrying the complete model-facingcontent/isErroroutcome (thetool/resultvocabulary, so UIs render sub-calls through the native path — the pair'stimefields carry per-sub-call timing); a queued call abandoned by run settlement logs neither.deriveMessages()surfaces neither event nor persists the canonical value. Token correlation lets commit-style observers defer an inner success until the finalrun_coderesult without exposing the live outer execution; ordinary tool side effects are not rolled back. Every sub-calladditionalContextsentry is deferred through the outerToolRunContextin dispatch order; the loop appends those contexts only after the parentrun_coderesult, preserving adjacency and retaining each source/meta even when the program later fails. - Settlement discipline: the bridge owns a run-scoped abort that follows the outer signal in and fires when the run settles for any reason, so a budget expiry aborts an in-flight sub-tool instead of orphaning it; the bridge then drains its queue BEFORE returning, so every
tool/code-dispatchlands inside the open turn. A failed run throwsCodeRunFailedError(code: 'CODE_RUN_FAILED', message = the failure kind + captured logs), which the pipeline converts to a structuredisErrorthe model self-corrects from. - Result size: intermediate binding values cross the worker process whole and have no per-binding byte cap.
run_codereturns canonical{ logs: string[], result?: JsonValue }; strings render raw, every other present JSON root renders through a stack-safe pretty JSON traversal whose total indentation is capped at ten characters (deeper subtrees stay compact),nullremains explicit, and absentresultmeans the program returnedundefined. The worker's configurablemaxOutputBytes(default 64 MiB) applies only to the combined serialized outer log-array, completion-value, or failure-message payloads; fixed result-envelope syntax and presentation whitespace are outside that limit. Invalid and over-limit completions fail explicitly, and only this outer result is eligible for ordinary spill.
Parallel execution
The agent loop groups consecutive parallel calls into a bounded rolling pool and treats each exclusive call as an ordering barrier. Only dispatch/body overlaps; policy, durable results, and context retain model order. Code Mode bindings reuse the same classification through the bridge's own pool. The parallel tool-call Agent Note owns the shipped declarations and rationale.
Model Experience
Normal tool schemas
What the model sees
In normal mode the model sees each visible definition's exact name, description, and JSON schema; the shipped definitions are recorded in the generated tool package map and schema sections. Agent-scoped restrictions, shadows, and extension registrations change that agent's end-tool set.
Token effect
Fixed per-request cost proportional to the visible definitions. Restrictions that hide tools remove their entire schema cost for that agent.
KV Cache effect
Prefix-stable while visible definitions and their order are unchanged. Registration, disposal, or scoped restriction may invalidate reuse from the first changed schema token.
Code Mode schema and system prompt
What the model sees
Code Mode exposes the generated run_code schema, the SDK instructions below, and the generated exact SDK block for the loaded runtime's language (the TypeScript declare const tools block, or the Python tools declaration). both exposes normal schemas and this Code Mode API. Under code the prompt also carries the tools:code-only rule, ordered ahead of the per-tool guidance band so the model reads which tools it may call before it reads what each one is for; both renders it empty. The instructions and SDK block match the loaded runtime's language; the TypeScript version (via dsh-code-runtime-worker-thread) is shown below, and the Python version (for any runtime reporting language: 'python') has the same operations and types in Python syntax (await tools.name(args), subscript access for exotic names, print(...) and top-level return).
Code Mode SDK instructions
## Writing code for run_code
`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. 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 effect
Fixed per-request cost proportional to the visible definitions. Code Mode trades end-tool schemas for generated SDK text plus one transport schema rather than promising a universal reduction.
KV Cache effect
Prefix-stable while the Code Mode selection, generated SDK, transport schema, and visible tool set are unchanged. Mode or filter changes may invalidate reuse from the first changed prompt or schema token.
Tool-call history and results
What the model sees
The loop retains model-emitted arguments and the registry's final content. Any thrown or denied call becomes exactly Error: <message>. Code Mode returns only the outer program's printed lines and rendered return value, (run_code completed with no output) when both are empty, or Error: code run failed (<kind>): <message> followed conditionally by Captured output: and the captured lines. Inner dispatch events stay log-only; post-execute listeners may append source-attributed context after the result.
Token effect
Arguments, results, and additional context are data-dependent and resent until compaction. Restrictions that hide tools also remove their schemas before the model can call them.
KV Cache effect
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
Known Limitations and Deferred Work
- Concurrency policy is not an event gate —
executionMode()reads the resolved tool definition directly; plugins can only declare a classifier on definitions they own. tools/pre-executedeliberately cannot rewriteexec.arguments— logged and rendered args would desync from what ran; the rewrite design is a proposed Agent Note.- Caller-defined subagent and workflow structured outputs remain object-rooted — this is a consumer-level guard; the shared schema vocabulary and tool outputs support every JSON root.
timeoutMson a definition is declarative only — the registry never enforces deadlines; enforcement requires the@deepseek-ai/dsh-tool-call-timeout-policywrapper.- Code Mode's SDK language follows the one loaded runtime, and a presentation is per agent rather than per tool —
mode: code/bothrejects prompt assembly unlessctx.codeRuntime.languagehas a registered SDK renderer (TypeScript or Python); scoped restrictions/shadows andpresentAschoose each agent's visible bindings and their form, but within one agent no tool can be native-only while another is code-only. - Code Mode intermediate values are execution-local and unbounded by bytes — the canonical typed values cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer
run_codeoutput has the worker's configurable hard cap. The durable log copy of each sub-call IS bounded: thetools/code-dispatch-logwaterfall lets the spill policy replace an oversizedtool/code-dispatchcontent with a preview + locator (rationale). run_codestate is fresh per run — a persistent REPL-style kernel is rejected for the MVP (cross-call state would be invisible to the log); see the Code Mode Agent Note.
LIMITATIONS
Known limitations
- **Concurrency policy is not an event gate** — `executionMode()` reads the resolved tool definition directly; plugins can only declare a classifier on definitions they own. - **`tools/pre-execute` deliberately cannot rewrite `exec.arguments`** — logged and rendered args would desync from what ran; the rewrite design is [a proposed Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md). - **Caller-defined subagent and workflow structured outputs remain object-rooted** — this is a consumer-level guard; the shared schema vocabulary and tool outputs support every JSON root. - **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-tool-call-timeout-policy` wrapper. - **Code Mode's SDK language follows the one loaded runtime, and a presentation is per agent rather than per tool** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language` has a registered SDK renderer (TypeScript or Python); scoped restrictions/shadows and `presentAs` choose each agent's visible bindings and their form, but within one agent no tool can be native-only while another is code-only. - **Code Mode intermediate values are execution-local and unbounded by bytes** — the canonical typed values cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer `run_code` output has the worker's configurable hard cap. The durable log copy of each sub-call IS bounded: the `tools/code-dispatch-log` waterfall lets the spill policy replace an oversized `tool/code-dispatch` content with a preview + locator ([rationale](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md)). - **`run_code` state is fresh per run** — a persistent REPL-style kernel is rejected for the MVP (cross-call state would be invisible to the log); see [the Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md).
