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 '@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-surfaceregions on the subsystem pages — everything you can inject and listen to, each on its owning page. - Architecture — the system map these plugins live in.