Files
deepseek-harness/docs/cordis-tutorial/07-into-the-harness.md
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.

Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.

The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.

Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:13 +08:00

4.5 KiB

7. Into the harness

English | 中文

This chapter registers a model-callable tool with the harness's tools service, executes it through the harness tool pipeline, and observes the result event. It remains keyless and does not call a model.

A tool plugin

Create greet-tool.ts in tmp/cordis-tutorial:

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

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

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

  // Drive one call through the real execution pipeline, standing in for
  // the model. CallId brands the correlation id a provider would issue.
  void (async () => {
    const result = await ctx.tools.execute({
      callId: CallId('demo-1'),
      name: 'greet',
      arguments: { name: 'Cordis' },
      signal: new AbortController().signal,
    })
    console.log('tool replied:', JSON.stringify(result.content))
  })()
}

Every pattern here is from the earlier chapters: inject: ['tools'] (chapter 3) holds the plugin until the tool registry exists; ctx.tools.register(...) attaches the registration disposer to the plugin (chapter 2), so unloading unregisters the tool. defineTool converts the parameters spec to the JSON Schema shown to the model, infers the type of args, and validates model-supplied arguments before execute runs. The tool returns the canonical value declared by output.schema; output.render separately produces the Native and durable result content.

An observer plugin

Create tool-logger.ts — a separate plugin that watches every tool call in the app through the harness's tools/result event:

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

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

export function apply(ctx: Context) {
  ctx.on('tools/result', (exec, result) => {
    const text = result.content
      .map(block => (block.type === 'text' ? block.text : ''))
      .join('')
    console.log(`[tool-logger] ${exec.name} -> ${text}`)
  })
}

The import type {} from '@deepseek-ai/dsh-tools' line pulls in the package's declaration merges so 'tools/result' and its payload are typed — the same move as chapter 4's stats.ts import, at package scale.

Compose and run

- name: '@deepseek-ai/dsh-system-prompt'
- name: '@deepseek-ai/dsh-tools'
- name: './tool-logger.ts'
- name: './greet-tool.ts'

@deepseek-ai/dsh-tools injects the systemPrompt service because tools contribute schemas to the system prompt, so the composition lists its provider too. Without it, the tools plugin remains PENDING as described in chapter 6.

node --import tsx ../../vendor/cordis/bin.js
[tool-logger] greet -> Hello, Cordis!
tool replied: [{"type":"text","text":"Hello, Cordis!"}]

The logger fired first: tools/result is emitted as part of result materialization, before execute's promise resolves to the caller. Neither of your plugins knows the other exists — the registry service and the event connect them.

From here to a full agent

A real agent is this composition plus more plugins: an LLM adapter, the agent loop, persistence, an entry point. Compare examples/headless-agent/cordis.yml — you can read every entry in it now. Add your greet-tool.ts to a copy of that file.

Where to go next:

  • Build a tool — more of defineTool, including presentation and richer schemas.
  • Three-layer capability design — how the harness structures replaceable capabilities.
  • The generated cordis-surface regions on the subsystem pages — everything you can inject and listen to, each on its owning page.
  • Architecture — the system map these plugins live in.