Add a tool
A tool is a ChatTool (api/src/capabilities/types.ts): a provider-neutral definition the model sees, a present-tense label for the activity chip, and a run function that executes in the Worker.
const myTool: ChatTool = {
definition: { name, description, parameters }, // what the model sees
activity: (input) => 'Reading the …', // the chip while it runs
run: (input, { claim, today }) => ({ result }), // what it does
};
Where it goes
| The tool is… | Put it in |
|---|---|
| A general reader every capability benefits from | SHARED_TOOLS in api/src/capabilities/tools.ts |
| Specific to one capability's task | the capability's group file, listed in that capability's tools |
| Something that acts on the page, not the claim | tools.ts, with screenAction: true |
Either way it is offered on every turn. Placement only decides ownership and order.
The definition
definition: {
name: 'find_diary_gaps',
description: 'Deterministic check: lists every gap between consecutive diary entries longer than the threshold, with the open tasks due inside each gap. Call this first when reviewing handling pace.',
parameters: {
type: 'object',
properties: {
minDays: { type: 'number', description: 'Smallest gap to report, in days. Default 14.' },
},
},
},
- Name:
snake_caseverb phrase, unique across all tools. A test checks uniqueness. - Description: what it returns and when to call it. The model chooses tools from these descriptions; one that says "Call this first when…" gets called. Say "Deterministic" when it is, so the model trusts the arithmetic.
- Parameters: a JSON Schema object. Use
enumwherever the values are known: the shared section and panel lists exist for this. A parameterless tool uses{ type: 'object', properties: {} }.
The activity label
activity: (input) => `Reading ${SECTION_LABEL[String(input.section)] ?? 'the claim'}…`,
Present tense, ending in an ellipsis, a handler's words rather than the tool name: Checking the policy period and coverages…. It is shown while the tool runs and in the Prompt Lab trace.
The run function
run: (input, { claim, today }) => {
if (!claim.diary.length) return { result: 'No diary recorded on this claim.' };
// … compute
return { result: JSON.stringify({ today, gaps }) };
},
The rules:
- Read only the claim you are given. No fetches, no writes, no other claims, no globals.
- Use
todayfrom the context, nevernew Date(). Tests pin it; the date helpers indates.tswork onYYYY-MM-DDstrings. - Validate input defensively. The model can pass anything. Coerce or default it; never assume the type the schema asked for.
- Return compact JSON. The result stays in the model's context for the rest of the turn. Return what it needs to reason with, not the whole section again.
- Say plainly when there is nothing. "No diary recorded on this claim." is better than
[], which the model might over-read. - Throwing is allowed but not preferred.
runToolturns an exception intoTool failed: <message>, which the model can read. A plain result explaining the problem is better. - Screen actions return
clientActions. A screen-action tool returns{ result, clientActions: [{ kind: 'highlight_panel', panel }] }and nothing else happens server-side.
Test it
Unit-test the tool directly. The builders in api/test/ keep tests short:
import { claimWith, runJson, toolContext } from '../../test/builders';
import { diary } from '../../test/records';
it('reports a gap over the threshold', async () => {
const claim = claimWith({ diary: [diary({ id: 'D1', date: '2026-08-01', content: 'a' }), diary({ id: 'D2', date: '2026-08-30', content: 'b' })] });
const { gaps } = await runJson<{ gaps: unknown[] }>(findDiaryGaps, toolContext(claim, '2026-09-01'));
expect(gaps).toHaveLength(1);
});
Cover the empty case, the boundary of any threshold, and anything with dates or money. See Writing tests.
Then
- Mention the tool in the playbook that should use it ("Call find_diary_gaps first").
npm run docs:referenceto add it to the tools reference.