Skip to main content

Adding a Tool

Adding to BOR’s tool surface is two steps: a handler module and a registry entry. The prompt docs, the executor, and both LLM harnesses pick it up automatically.

1. Write the handler

Create (or extend) a module under server/protocol/handlers/. Export an execute(block, ctx) that returns the standard result shape:
// server/protocol/handlers/myfeature.js
export async function executeMyTool(block, ctx) {
  // block.attrs   — tag attributes
  // block.body    — inner text (CDATA-unwrapped)
  // block.children — typed sub-tags (if your tool uses them)
  // ctx           — per-request context: audit(), emit(), signal, loadConfig(), …

  const target = (block.attrs?.target || '').trim();
  if (!target) {
    return { ok: false, event: 'tool_error', payload: { tag: 'my_tool', reason: 'target required' }, llmEcho: 'my_tool: target required' };
  }

  // …do the work…
  await ctx.audit({ kind: 'my_tool', target });

  return {
    ok: true,
    event: 'my_tool',                 // SSE event name → the presence
    payload: { target, result: '…' }, // card data for the bubble
    llmEcho: `Did the thing to ${target}.`, // what the model reads next pass
    // llmMedia: [{ type: 'image_url', url, mediaType, label }]  // optional images for the model
  };
}

The result contract

FieldGoes toPurpose
okexecutorsuccess/failure (retries on transient failure).
event + payloadthe presencerendered as a card via SSE.
llmEchothe modela compact, model-facing result string.
llmMediathe modeloptional image blocks (e.g. a screenshot).
Keep llmEcho compact — it’s read every following pass. Never throw; return a tool_error result.

2. Register it

Add one entry to TAGS in server/protocol/registry.js:
my_tool: {
  description: "What it does. This text is rendered into the system prompt, so be precise about when and how to use it.",
  params: {
    attrs: { target: 'required. the thing to act on.' },
    body: 'optional text payload',
    // children: { … }  // if your tool takes typed sub-tags
  },
  examples: [`<my_tool target="x"/>`],
  voiced: false,        // does its run surface in the bubble?
  feedsBack: true,      // does its result trigger another model pass? (usually yes)
  execute: myfeature.executeMyTool,
},
Import your module at the top of registry.js (import * as myfeature from './handlers/myfeature.js'). That’s it. buildToolDocs() renders the new tool into the system prompt automatically, the executor invokes your handler, and the new harness shims it.

3. (Optional) render a custom card

If you want a richer bubble card than the default activity row, handle your event in v2/host/presence.js:
  • Add a TOOL_META entry (my_tool: ['Label', 'subtitle', '◆']).
  • Add a tag_start branch (open a card) and a result-event branch (finalize it) in handleChatEvent.
  • Add a card type + renderer, and styles in presence.css.
The browser, terminal, editor, and wait cards are all examples of this pattern.

Notes & gotchas

  • Children & attributes. The new-harness shim treats children as flat string properties; tools whose children carry XML attributes need handler-side schema overrides. Prefer attributes for scalars.
  • Opaque content tags. If your tool’s body is file-like content, the parser already handles CDATA robustly — anchor on your own close tag, not CDATA. See The protocol.
  • Audit. Anything that mutates state should call ctx.audit(...).
  • Safety. If your tool runs shell or writes files, route through the validator / path guards.

Checklist

  • Handler returns { ok, event, payload, llmEcho } (+ optional llmMedia).
  • Registry entry with description, params, examples, voiced, feedsBack, execute.
  • Module imported in registry.js.
  • (Optional) presence card + CSS.
  • node --check the changed files.