Skip to content

CH 20 · defineTool: Craft a Tool for the Agent

Word count~2,430 wordsTime~20 minPrereqCH 19 (Three Plugin Forms)LevelReproducible

Chapter Goal

In CH 18 and 19, the plugins we wrote only logged — that was just to verify "the plugin is loaded". This chapter writes the truly valuable thing in plugins: tools.

Tools are the Agent's "hands": the model says something, the code you write is called, does real work, and sends the result back to the model. In the previous chapters you've been using dsh's built-in tools (read files, run commands, search); this chapter we craft one ourselves, let the model actually reach in and call it.

Tools Are the Soul of Plugins

Recall what you use every day: dsh's Agent can read files, write files, run commands — that's tools (capabilities registered in ctx.tools). The model itself can only "talk"; tools let it "act".

The minimum skeleton of a tool plugin:

js
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx) {
  ctx.tools.register(defineTool({
    // The four parts of a tool, broken down one by one below
  }))
}

inject: ['tools'] declares "I want to use the tool registry", learned in CH 19; ctx.tools.register(...) mounts a tool into the registry.

The Four-Piece of defineTool

defineTool takes an object that tells dsh "what's this tool called, when should it be used, what parameters does it need, how does it do its work". A tool = a "hiring JD for the Agent":

FieldMeaningAn analogy
nameThe tool's name, the model uses it to callJob title
descriptionTell the model "what does this tool do, when should it be used"Job responsibilities
parametersDeclare which parameters are required, which are optionalMaterials to submit
executeThe function that actually does the workWork after onboarding

Look at a complete minimum tool (same as the official tutorial, slightly translated):

js
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet by name',
    parameters: {
      name: { type: 'string', required: true, description: 'Name of the person to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

Four key points:

  • parameters is written in JSON Schema: type: 'string' declares the parameter type, required: true declares it's required. The framework automatically validates the parameters the model passes; non-compliant ones error out — you don't need to manually type-check in execute.
  • execute(args) is the function that actually does the work: args has been validated, just use it confidently. It returns a "canonical value" (a string here).
  • output.schema declares the shape of the return value, output.render converts the return value to content the model can see (a text block here). The return value first exists in canonical form, the rendering layer is responsible for "translating" it to the model.
  • description is extremely important: the model uses it to judge "should I use this tool right now". Write it clearly, write it specifically, and the model will know when to call it.

Hands-on: Write a greet Tool, Make the Model Actually Call It

In a workspace where you want to put plugins, create a directory:

powershell
New-Item -ItemType Directory -Path "tool-demo\src" -Force

Step 1: Install Dependencies

defineTool comes from @deepseek-ai/dsh-tools, install it in the plugin directory first:

powershell
cd "E:\software-workspace\DeepSeek harness demo\tool-demo"   # replace with your directory
npm init -y
npm install @deepseek-ai/dsh-tools

Remember to add a line "type": "module" in package.json (CH 19 mentioned, without it there'll be a bunch of warnings).

Step 2: Write the Tool

Create tool-demo\src\greet.js, with the same greet tool as above. I added a log line in execute to easily confirm in the terminal that it was really called:

js
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet by name',
    parameters: {
      name: { type: 'string', required: true, description: 'Name of the person to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      console.log('[greet] called with', args.name)
      return `Hello, ${args.name}!`
    },
  }))
}

Step 3: Declare and Verify

Create tool-demo\cordis.yml (replace the path with your own, remember %20 for spaces):

yaml
- insert:
    - id: greet
      name: 'file:///E:/your-workspace/tool-demo/src/greet.js'

Use headless to run once, let the model actually call it:

powershell
cd your-workspace-directory
dsh --profile headless --patch "./tool-demo/cordis.yml" "You must call the greet tool, say hi to Ada, then tell me verbatim what the tool returned"

In the terminal output you'll see:

text
[greet] called with Ada

This is the evidence that execute was actually called by the model — your code was really executed by the model reaching in. And the model's reply will contain the tool's returned Hello, Ada!.

The yellow [greet] called with Ada in the terminal is the log of execute being actually called by the model; the Hello, Ada! below is the result the tool returns to the model.

Let dsh Do It: One Prompt Does It

The tool-writing flow is exactly the same as writing a plugin, dsh can do it itself, and it knows defineTool's fields and how the schema should be written better than you.

Send this prompt directly in the Web UI's input box:

text
In my current workspace, help me write a tool plugin: use defineTool to define the simplest tool (with one required parameter, returning some text in execute). First read the dsh official docs to understand defineTool's fields and parameter validation rules, implement per the official spec, then run a headless to have the model actually call it, and finally tell me the result and where the files are.

It will go look up the docs itself, install dependencies, write the tool, verify the model actually called it. You just verify.

This is the result of me actually running this prompt: it read the docs itself, built the three pieces of the plugin (package.json declaring bundle, defineTool entry index.js, a one-line insert cordis.patch.yml), and even discovered that Windows paths with spaces would break the install command parsing, and proactively moved the plugin to a no-spaces path before installing — it hit the pitfall, and worked around it itself.

After running, it also organized the pitfalls into a list: plugin export form requirements, inject must be explicitly declared, workspace packages need build artifacts to run, path-spaces pitfall.

Common Pitfalls

ProblemWhat's happeningHow to handle
Reports Cannot find package '@deepseek-ai/dsh-tools'Plugin directory didn't install dependenciesnpm install @deepseek-ai/dsh-tools
Model never calls your tooldescription is unclear, model doesn't know when to use itMake description specific: "Use when user requests X"
Parameters not passed correctlyschema and model understanding don't matchWrite clear description for each parameter in parameters
Tool reported a parameter errorModel passed illegal parametersMark required items with required: true, write the type correctly
Model called it but result is wrongLogic in execute is wrongAdd console.log in execute to debug, check the logs

What you learned in this chapter

You pass if you can complete the items below:

  • [ ] State the defineTool four-piece: name / description / parameters / execute
  • [ ] Know that parameters is JSON Schema, the framework automatically validates parameters
  • [ ] Write a tool plugin and register it to ctx.tools
  • [ ] Use headless to verify the model actually called your tool
  • [ ] Know that description decides when the model uses your tool

Open Source · MIT · Community Driven