Skip to main content

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 fromSHARED_TOOLS in api/src/capabilities/tools.ts
Specific to one capability's taskthe capability's group file, listed in that capability's tools
Something that acts on the page, not the claimtools.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_case verb 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 enum wherever 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:

  1. Read only the claim you are given. No fetches, no writes, no other claims, no globals.
  2. Use today from the context, never new Date(). Tests pin it; the date helpers in dates.ts work on YYYY-MM-DD strings.
  3. Validate input defensively. The model can pass anything. Coerce or default it; never assume the type the schema asked for.
  4. 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.
  5. Say plainly when there is nothing. "No diary recorded on this claim." is better than [], which the model might over-read.
  6. Throwing is allowed but not preferred. runTool turns an exception into Tool failed: <message>, which the model can read. A plain result explaining the problem is better.
  7. 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:reference to add it to the tools reference.