Seven-chapter tutorial under docs/cordis-tutorial/ for agent developers new to Cordis: first plugin, lifecycle/effects, services, events, config, composition/HMR, and a final chapter registering a tool against real harness services. Every transcript was produced by running the chapter files in a gitignored tmp/ scratch directory. Published to both website locales as mirrored English pages under a new 'Cordis tutorial' develop-sidebar section; a Chinese pair can be added later without route changes.
4.3 KiB
7. Into the harness
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 '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' },
},
async execute(args) {
return [{ type: 'text', text: `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.
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 '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, a front end. 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 and, with a DEEPSEEK_API_KEY in the root .env, the model can actually call your tool.
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 services and events catalogs — everything you can inject and listen to.
- Architecture — the system map these plugins live in.