Registering Tools
Define tool schemas and execution logic with defineTool, then register on ctx.tools for the model to call
Model-facing tools let an agent do concrete work — read files, run commands, search the web. Use defineTool to define a tool's schema and execution, then register it on ctx.tools.
The minimal shape
import { readFile } from 'node:fs/promises'
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'read_file',
description: 'Read a file from disk.', // what the model sees
parameters: {
path: { type: 'string', required: true, description: 'Absolute path' },
limit: { type: 'number' }, // optional by default
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args, exec) {
// args is typed from the schema: { path: string; limit?: number }
return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
},
}))
}Registration is effect-based: disposing the plugin fiber unregisters the tool; the schema flows into system-prompt assembly automatically.
The execute() contract
- Args are validated for you.
defineToolvalidates model-generatedargumentsagainstparametersbeforeexecuteruns, so insideexecutethe args match the inferred type. You still hand-check constraints the schema cannot express (non-empty strings, positive numbers, cross-field rules). - Declare and return one canonical JSON value.
output.schemadeclares the canonical value type;executereturns only that value, which the registry snapshots, validates, freezes, and passes tooutput.render(args, value). Do not return content blocks from the body, or make callers parse prose for ids and fields. - Throwing or returning an invalid value means
isError. Throw for infrastructure failures; put a successful domain outcome in the canonical value even when it represents a non-ideal state. - Honor
exec.signal. Cancel in-flight work when it fires. - Registration borrows your readonly definition. Do not mutate the schema or replace callbacks after registration; to hot-swap a tool, dispose its owning effect and register the replacement.
Long-running work
For background execution, register the task with ctx.jobs.start(...) instead of blocking the tool body. A successful background branch returns a typed canonical handle such as { kind: 'background', jobId }; foreground work remains coupled to exec.signal.
Execution policy and observation
Do not build deployment policy into the tool; use the pipeline events instead:
tools/pre-execute— extensible allow/deny/ask policy.ctx.tools.guard()— a final monotonic deny that later listeners cannot undo.tools/execute— wrap dispatch with a deadline, retry, or metrics.tools/post-execute— replace presentation content or the return value, block the result, or attach model-facing context.tools/result— observe the immutable normalized outcome without changing it.
Tool UI cards
output.render returns model-facing content; the UI card is a separate concern declared through pure presentation projections and optional presentCall/presentResult methods. Both return a card-tagged render intent: generic (default), terminal (shell commands), diff (file changes), search, or web.
Hard rule: these methods must be pure functions of args (plus the result) — they run on live streaming and on session-log replay, so no I/O, no reading session state, and no clock/random.
Next steps
- To build a first tool step by step, see the add-a-tool recipe in the Cookbook.
- To understand the capability seam behind tools, return to Architecture.