Skip to content

Defining tools ​

A tool is a typed function your agent can call. Drop one file per tool into the agent's tools/ directory, each default-exporting a defineTool():

ts
// tools/search-tickets.ts
import { z } from "zod";
import { defineTool } from "@toren-run/core";

export default defineTool({
  name: "search_tickets",
  description: "Search the support ticket index by free text.",
  input: z.object({ query: z.string(), limit: z.number().default(10) }),
  effects: "none",
  idempotency: "keyed",
  approval: "never",
  handler: async ({ query, limit }, ctx) => {
    const res = await fetch(`https://tickets.internal/search?q=${encodeURIComponent(query)}&n=${limit}`, {
      headers: { authorization: `Bearer ${ctx.env.TICKETS_API_KEY}` },
    });
    return JSON.stringify(await res.json());
  },
});

Every field is part of the durability contract, not decoration:

  • input is a Zod schema. The model's arguments are validated before your handler runs, and the schema becomes the JSON Schema the model sees.
  • effects declares what the tool touches: none (pure lookup), sandbox (writes something recoverable), or external (emails, payments, anything the outside world sees). The runtime uses this to decide what is safe to replay.
  • idempotency: "keyed" means the call is recorded in the event log under a deterministic key, so a crashed and resumed task never executes it twice. Use "none" only for pure reads where a duplicate call is harmless.
  • approval gates the call on a human: "never", "always", or a predicate over the parsed args ((args) => args.amount > 100). A gated call parks the run at zero compute until someone approves it in the console, CLI, or API. See Approvals.
  • ctx.env holds the values your agent declared in agent.yaml under env:. Handlers never read raw process.env; declared variables are validated at startup and fail fast with the full missing list. Never pass secrets in a run's input, because inputs live in the event log forever by design.

The handler returns a string (JSON-encode structured results). Whatever it returns is recorded in the event log, so a resumed run replays the recorded result instead of calling the tool again.

Budgets: timeoutMs and maxAttempts ​

Two optional fields keep one bad tool call from taking a whole run down with it:

ts
export default defineTool({
  name: "fetch_report",
  // ...
  timeoutMs: 30_000,   // a call running longer than this returns a timeout error to the model
  maxAttempts: 2,      // after a worker died mid-call this many times, fail closed instead of re-running
  handler: async (args, ctx) => { /* ... */ },
});
  • timeoutMs races the handler against a timer. On timeout the call completes with an error result (tool fetch_report timed out after 30000ms) that the model sees and can act on, exactly like a thrown error. The run keeps its accounting and continues. The handler's promise cannot be cancelled, so a truly stuck handler is abandoned, not killed; put the real cancellation in the handler (an AbortSignal on the HTTP call, a timeout on the child process).
  • maxAttempts bounds the crash window. When a worker dies after ToolCallStarted and before ToolCallCompleted, the resumed run re-runs the call under the same idempotency key. With maxAttempts: 2, the third such start fails closed with an error result instead of running again. Without it the documented at-least-once behavior stands.

Both sit in the same declaration as effects, idempotency, and approval, and both are per tool: the run-level budgets are limits.maxAttemptsPerTask and limits.maxWallClockMin in agent.yaml. The runtime owns the hard deadline and the final cancel; the tool owns how fast it gives up.

Built-in tools ​

Some tools are common enough to ship in the box. Declare them by name in agent.yaml and skip the handler:

yaml
builtin_tools: [web_search]

The full set, each documented on its own page:

  • Web search (web_search): Tavily-backed live search. Needs TAVILY_API_KEY, which the loader folds into the agent's required env automatically.
  • Database (sql_query): read-only SQL against SQL_DATABASE_URL.
  • File parsing (read_attachment): paged reads of attached pdf/docx/xlsx/text files.
  • Background runs (run_process, check_run): spawn one of the agent's named processes as a durable run and poll it.

bash is deliberately not declarable here: listing it in builtin_tools is a startup error pointing you to sandbox: true, which grants bash plus the workspace file tools together (see Sandbox).

  • Database (sql_query): read-only SQL access to a database; needs SQL_DATABASE_URL.
  • File parsing (read_attachment): paged access to attached files: PDF, docx, xlsx, and text formats, parsed once at upload.

The sandbox is its own switch: sandbox: true grants bash plus the workspace file tools. A builtin whose name collides with one of your own tools is a startup error, never a silent override.