description: "The tool registry and execution pipeline for tool authors and maintainers registering, restricting, presenting, or debugging model-facing tools."
English | 中文
With dsh-tools, tool plugins register schemas and executors, and every model tool call runs through a guarded pipeline — allow/deny/ask policy, monotonic guards, around-dispatch wrappers, result inspection, definition-owned content finalization, and a final observe-only notification. The package also controls how tools are presented to the model: its mode config selects native function calling, PTC mode, or both, and one agent shadows that default for itself with presentAs. Tool authors use defineTool for typed parameter and output schemas, an optional cooperative timeout, parallel-safety classification, and optional UI presentation intents. Choose it as the registry for any capability you want the model to reach — schemas flow into prompt assembly automatically.
Mount dsh-tools wherever agents call tools: it provides ctx.tools, the registry every tool plugin registers into and the loop dispatches through. Registering a tool is enough to make it visible — the registry feeds its schemas into the system-prompt assembly automatically.
defineTool builds a typed tool definition: a model-facing name, description, and parameter schema, a canonical output declaration, and an execute body that returns only the declared JSON value. Model arguments are validated before execution; invalid input becomes a normal error result.
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 supports string, number, integer, boolean, null, array, object, author-only json, and exact-one oneOf; InferValue preserves exact types through 16 container levels before widening to JsonValue. A raw JSON Schema (JsonSchemaNode) is the wire-level counterpart shared with subagents, workflows, and MCP.
The mode config decides what the model sees: native (every visible schema), ptc (only run_code plus a generated SDK), or both.
- name: '@deepseek-ai/dsh-tools'
config:
mode: native
| Field | Default | Meaning |
|---|---|---|
mode |
native |
How visible tools are presented to the model: native, ptc, or both |
maxParallelSubCalls |
10 |
Concurrency cap for a run_code program's overlapping sub-calls; 1 restores strictly serial dispatch |
The generated configuration catalog is the exhaustive source for every accepted field. Non-native modes require a composed ctx.codeRuntime whose language has a registered SDK renderer; an agent preset selects its own presentation with dsh-agent-tool-presentation, and one agent can shadow the default with presentAs(mode).
ctx.tools.restrict(filter) applies an allow or deny mask to the global tools one agent inherits; masks intersect, scoped registrations stay visible, and the restriction lifts when disposed. ctx.tools.get(name, scope) resolves a tool as one scope sees it. A Host-local presenter consumer passes the calling agent when it must match the definition that executed. ctx.tools.schemas(scope) returns the visible schemas without the execute functions.
ctx.tools.guard(guard) registers a monotonic synchronous guard after the extensible tools/pre-execute waterfall: a returned reason denies the call, and no later listener can turn that denial back into permission. The pipeline's events give plugins more control — tools/pre-execute decides allow/deny/ask, tools/execute wraps dispatch for timeout or retry, tools/post-execute inspects or replaces the result, and tools/result observes the frozen final outcome.
A tool can retain pure presentCall() and presentResult() methods for Host-local consumers. The built-in Web Client does not consume those values. It selects a renderer through tool.call.toolview and derives card props from raw call arguments, result content, failure state, and persisted metadata. The Client-derived presentation decision owns this transport split.
The package-level contract is enough for most consumers; read these when you need the surrounding domain.
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 catalog. Agent-scoped restrictions, shadows, and extension registrations change that agent's end-tool set.
Fixed per-request cost proportional to the visible definitions. Restrictions that hide tools remove their entire schema cost for that agent.
Prefix-stable while visible definitions and their order are unchanged. Registration, disposal, or scoped restriction may invalidate reuse from the first changed schema token.
PTC 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 instructions identify generated declarations as program-only bindings. When the current bash parameter schema accepts the example arguments, they also show a complete run_code call around tools.bash(...). The tools:sdk section uses first-party order 5000. both exposes normal schemas and this PTC mode API; under ptc the prompt also carries the tools:ptc-only rule earlier in the first-party order, so the model reads which tools it may call before it reads what each one is for.
## 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. The declarations below are SDK bindings for this program. A declaration does not make its name a directly callable tool; only names supplied as separate tool schemas may be called directly. When no separate `bash` schema is supplied, invoke a declared `bash` binding inside `run_code`:
`run_code({ code: "return await tools.bash({ command: 'pwd', description: 'Show current directory' })", description: "Show current directory" })`
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 is program output. A successful tool result containing an image is attached after the run so you can inspect it on the next step; every other intermediate result stays out of the conversation, so extract just what you need.
Program-only SDK bindings:
Fixed per-request cost proportional to the visible definitions. PTC mode trades end-tool schemas for generated SDK text plus one transport schema rather than promising a universal reduction.
Prefix-stable while the PTC 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.
The loop retains model-emitted arguments and the registry's final content. Any thrown or denied call becomes exactly Error: <message>. PTC mode renders the outer program's printed lines and 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, while a successful image-bearing sub-result is appended after the outer result as source-attributed context.
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.
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
These limits define when the registry needs special care. They are current package constraints, not a task backlog.
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.timeoutMs on a definition is declarative only — the registry never enforces deadlines; enforcement requires the @deepseek-ai/dsh-tool-call-timeout-policy wrapper.mode: ptc/both rejects prompt assembly unless ctx.codeRuntime.language has a registered SDK renderer; within one agent no tool can be native-only while another is ptc-only.run_code output has the worker's configurable hard cap.run_code state is fresh per run — a persistent REPL-style kernel is rejected for the MVP, because cross-call state would be invisible to the log.