DeepSeek Harness Bluebook
Developer Guide

Cookbook

Hands-on recipes: add a workspace package, add a model tool, and wire an LLM adapter

Three hands-on recipes: add a workspace package, add a model tool, and wire an LLM adapter. Each requires a source checkout (see the run-from-source part of Installation).

Recipe 1: add a workspace package

1. Create the package

packages/<group>/<pkg>/
  package.json     # copy from packages/core/tools, adjust name/description/deps
  tsconfig.json    # extends ../../../tsconfig.base.json, rootDir src, outDir lib/types
  src/index.ts     # service default export, or plugin (name/inject/apply/Config)
  README.md        # service API, events, extension points, and design notes

Key package.json invariants: private: true, a version matching the root package.json, type: module, @deepseek-ai/cordis in both peerDependencies and devDependencies (same range), and @deepseek-ai/schemastery in dependencies (it is a runtime validator).

2. Register it in the root configs

  • Add { "path": "./packages/<group>/<pkg>" } to the references of tsconfig.host.json (Host) or tsconfig.client.json (Client). An ordinary package belongs to exactly one aggregate, never both.
  • A client plugin package additionally declares dsh.client and extends tsconfig.base.client.json.

3. Decide the package topology

Split a swappable capability into Service Definition / Service Provider / Consumer packages when the roles evolve independently; a single-purpose plugin stays one package. Name the stable current responsibility, not the first implementation or a future expansion.

4. Verify

pnpm install
pnpm run constraints && pnpm run typecheck && pnpm run lint
pnpm run build && pnpm run hygiene

Recipe 2: add a model tool

Create a greet tool. Replace the plugin file with:

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

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

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

inject makes Cordis wait for the tool registry; defineTool infers and validates args from parameters.

pnpm dsh web --patch ./scratch-plugin/cordis.yml

Open http://127.0.0.1:3080 and ask Use the greet tool to greet Ada. — the model can call greet. For the full tool contract, see Registering Tools.

Recipe 3: wire an LLM adapter

An adapter translates the harness's provider-neutral request into a provider API call and the response back into harness chunks:

import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'

class MyAdapter extends LlmAdapter {
  private apiKey: string

  constructor(apiKey: string) {
    super()
    this.apiKey = apiKey
  }

  async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
    // 1. Convert options.messages to the provider format.
    // 2. Call the streaming API.
    // 3. Convert the response into StreamChunk values.
  }
}

export interface Config {
  apiKey: string
  providers: string[]
}

export const Config: Schema<Config> = Schema.object({
  apiKey: Schema.string().required(),
  providers: Schema.array(Schema.string()).required(),
})

export const name = 'my-llm-adapter'
export const inject = ['llm']

export function apply(ctx: Context, config: Config) {
  ctx.llm.registerAdapter(config.providers, new MyAdapter(config.apiKey))
}

Hard streaming rules: every block-start has a matching block-end; index starts at 0 and identifies content-block order; tool-call arguments are raw JSON strings end-to-end; emit usage before finish; finish is the final chunk. For a field the provider cannot honor, throw LlmError rather than silently dropping it.

Use it from cordis.yml:

- id: my-llm
  name: './src/my-llm-adapter.ts'
  config:
    apiKey: !!js process.env.MY_API_KEY
    providers:
      - my-provider

- id: agent-loop
  name: '@deepseek-ai/dsh-agent-loop'
  config:
    agents:
      - id: main
        provider: my-provider
        model: my-model-v1

Reference implementations in the repository: packages/llm/llm-deepseek (direct HTTP) and packages/llm/llm-pi-ai (wrapping an LLM library).

Next steps

On this page