DeepSeek Harness Bluebook
Developer Guide

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. defineTool validates model-generated arguments against parameters before execute runs, so inside execute the 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.schema declares the canonical value type; execute returns only that value, which the registry snapshots, validates, freezes, and passes to output.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.

On this page