woopcode docs
v1.0.0 GitHub

Adding a tool

A tool is an object with a name, a description, a parameter list, and an execute function. Everything else — schema generation, dispatch, error handling, the docs — follows from registering it.

The shape#

export interface Tool {  name: string;  description: string;  parameters: ToolParameter[];  execute(    args: Record<string, unknown>,    signal?: AbortSignal,  ): Promise<string>;} export interface ToolParameter {  name: string;  description: string;  required: boolean;  type?: "string" | "number" | "array";}

execute returns a string. That string is what the model sees, so it is part of the interface, not a log line.

Write it#

tools/countLines.ts

import type { Tool } from "../config/types";import { resolveWorkspacePath } from "./workspace"; export const countLinesTool: Tool = {  name: "count_lines",  description: "Counts the lines in a file.",  parameters: [    { name: "path", description: "Path to the file", required: true },  ],   async execute(args) {    const requested = args.path as string;    if (!requested) throw Error("File path is required");     // Never join paths by hand. This resolves symlinks before checking    // containment, which is what stops a link being used to escape.    const path = await resolveWorkspacePath(requested, { mustExist: true });    const text = await Bun.file(path).text();     return `${text.split("\n").length} lines`;  },};

Register it#

tools/index.ts

export const toolRegistry: Tool[] = [  // ...  countLinesTool,];

That is the whole wiring. The registry is what the provider schema, the dispatcher, and the docs all read.

Classify its effect#

The registry does not record whether a tool writes — approval is enforced in the tool and in the policy, not declared as metadata. The docs need to know, so there is one table:

site/scripts/extract.ts

const EFFECT: Record<string, "read" | "write" | "shell" | "ask"> = {  // ...  count_lines: "read",};

A tool missing from that table shows up as unclassified and bun run docs:extract warns about it — it is not silently documented as safe.

Regenerate the docs data#

bun run docs:extract

Commit the updated site/src/docs/surface.json. Nothing in docs/ states a tool name, parameter, or count in prose, so the tools page, the counts, and the tables all pick up your tool with no page edits. bun run docs:check fails if you forget.

Rules worth following#

Throw for bad input. Errors are returned to the agent as results, so it can correct a path and retry. Write the message for the model: File <path> does not exist is actionable, ENOENT is not.

Use resolveWorkspacePath for every path. It is the workspace boundary.

Bound your output. Tool results are truncated at 4,000 characters before reaching the model. Truncate deliberately, with a notice, rather than being cut mid-structure — read_file is the example to copy.

Respect the signal. Long-running work should check the AbortSignal so CtrlC stops it.

Keep the description short and literal. It is prompt text, and the model chooses tools from it.

Test it#

bun test

Tool tests live in packages/tests/tools/. Cover the happy path, a missing required argument, and a path outside the workspace.

Next#

Added in woopcode@0.6.0