Core Development

Develop a Custom Tool

Learn standard specifications for registering agent tools with strict parameter schemas and structured error recovery.

In autonomous AI agent systems, Tools serve as the physical actuators through which the large language model perceives the environment, queries databases, executes shell commands, and manipulates files.

In DeepSeek Harness, a Tool is far more than a basic asynchronous callback function—it is a production-grade actuator integrating DSL parameter definitions, runtime type guards, and a model-facing observation rendering pipeline.

This tutorial guides you through building a production-ready “Code Quality & Security Lint Inspector Tool” using the @deepseek-ai/dsh-tools defineTool DSL, followed by real-time validation in the Web UI.


Core Architecture: defineTool and Dependency Injection

The @deepseek-ai/dsh-tools suite completely decouples reasoning orchestration from low-level execution:

┌─────────────────────────────────────────────────────────────┐
│                 DeepSeek Tool Execution Pipeline            │
├─────────────────────────────────────────────────────────────┤
│  1. LLM initiates Tool Call: { name: 'inspect_code', args } │
│                         ▼                                   │
│  2. defineTool validation: Enforces JSON Schema boundaries  │
│                         ▼                                   │
│  3. execute(args): Runs business logic & returns raw data   │
│                         ▼                                   │
│  4. output.render: Formats data into [{ type: 'text' }]     │
│                         ▼                                   │
│  5. Structured Observation injected into context for next turn│
└─────────────────────────────────────────────────────────────┘
  • Declarative Service Injection: Exporting inject = ['tools'] ensures the Cordis microkernel pauses plugin initialization until the global ctx.tools registry service is completely active;
  • Type-Safe Parameter DSL: defineTool automatically infers TypeScript types for execute(args) and filters out hallucinated model arguments at runtime;
  • Decoupled Observation Rendering: execute focuses purely on clean domain data, while output.render transforms that data into high-signal messages tailored for LLM reasoning.

Step 1: Author the Code Quality Inspector Tool

Create custom-plugins/lint-tool/src/index.ts:

typescript
import type { Context } from '@deepseek-ai/cordis';
import { defineTool } from '@deepseek-ai/dsh-tools';

// 1. Declare plugin identity and service dependencies
export const name = 'code-inspector-plugin';
export const inject = ['tools'];

export function apply(ctx: Context) {
    // 2. Register custom Tool into the Harness global registry
    ctx.tools.register(defineTool({
        // Unique tool identifier (must match the model Tool Calling schema name)
        name: 'inspect_code_quality',

        // Semantic description: guides the model on WHEN and WHY to invoke this tool
        description: 'Statically analyzes source code snippets for cyclomatic complexity, line count, and security vulnerabilities (e.g. hardcoded secrets or unsafe functions).',

        // Parameter schema declaration
        parameters: {
            code: {
                type: 'string',
                required: true,
                description: 'Source code text to inspect (TypeScript, JavaScript, or Python)'
            },
            maxAllowedLines: {
                type: 'number',
                required: false,
                description: 'Maximum recommended lines of code threshold, defaults to 50'
            },
            strictSecurityCheck: {
                type: 'boolean',
                required: false,
                description: 'Enable strict security rule scanning, defaults to true'
            }
        },

        // Output schema and model-facing observation renderer
        output: {
            schema: { type: 'object' },
            // render formats raw return value into messages for model consumption
            render: (_args, result) => [
                {
                    type: 'text',
                    text: `[Code Inspection Report]
` +
                          `• Line count: ${result.totalLines}
` +
                          `• Detected vulnerabilities: ${result.vulnerabilities.length > 0 ? result.vulnerabilities.join(', ') : 'None'}
` +
                          `• Quality assessment: ${result.passed ? ' Passed inspection' : ' Potential refactoring issues detected'}`
                }
            ]
        },

        // Core actuator execution logic
        async execute(args) {
            const lines = args.code.split('\n');
            const vulnerabilities: string[] = [];

            if (args.strictSecurityCheck !== false) {
                if (args.code.includes('eval(')) vulnerabilities.push('Prohibit eval() execution of dynamic strings');
                if (args.code.includes('password =') || args.code.includes('api_key =')) {
                    vulnerabilities.push('Potential hardcoded credentials detected');
                }
            }

            const maxLines = args.maxAllowedLines || 50;
            const passed = vulnerabilities.length === 0 && lines.length <= maxLines;

            return {
                totalLines: lines.length,
                vulnerabilities,
                passed
            };
        }
    }));
}

Step 2: Register in cordis.yml

Update your project configuration:

yaml
mode: standard

plugins:
  # Mount our custom linting tool
  - name: "./custom-plugins/lint-tool"

  # Core execution tools
  - name: "@deepseek-ai/dsh-plugin-bash"
  - name: "@deepseek-ai/dsh-plugin-str-replace"

Step 3: Launch Web UI & Test Execution

Start the development server with live patching:

bash
pnpm dsh web --patch ./cordis.yml

Open the dashboard in your browser and prompt DeepSeek-V3 / R1:

“Please inspect this snippet for security risks: const token = 'sk-123456'; eval(userInput);. Use the code quality tool.”

Execution Trace:

  1. The LLM parses the request and emits a structured inspect_code_quality tool call;
  2. Harness verifies argument types against the schema;
  3. execute evaluates the code, flagging eval() and hardcoded keys;
  4. output.render returns the formatted diagnostic report, enabling the agent to formulate remediation steps.

Four Golden Rules for Tool Design

  1. Explicit Semantic Descriptions: Clearly describe operational boundaries in description to prevent false positive invocations;
  2. Context-Aware Output Sizing: Implement truncation, hashing, or pagination for large data streams to protect context windows;
  3. Structured Diagnostic Feedback: When errors occur, return actionable failure causes rather than unhandled rejections;
  4. Safety & Idempotency: Pair state-mutating operations with sandboxing and approval gates.

Frequently Asked Questions (FAQ)

Q1: Why is inject = ['tools'] mandatory for tool plugins?

Answer: ctx.tools is provided as a singleton service by @deepseek-ai/dsh-tools. Declaring inject guarantees proper topological ordering in the Cordis microkernel, preventing undefined property errors during startup.

Q2: Why separate execute from output.render?

Answer: execute produces raw, strongly-typed domain data (ideal for automated testing and telemetry), while output.render formats that data for LLM ingestion. This separation allows tools to be reused across different UI frontends and model families.

Q3: How are undeclared extra arguments from the LLM handled?

Answer: defineTool validates calls strictly against JSON Schema invariants. Undeclared fields are rejected with structured diagnostic observations, prompting the LLM to self-correct in the subsequent turn.

Q4: Can a single plugin register multiple distinct tools?

Answer: Yes. Call ctx.tools.register(defineTool({ ... })) multiple times inside the apply(ctx) function to register a suite of related tools.