tool

js/ai/tool.ts

fino:ai/tool — validated tool definitions for agent tool calling.

A Tool wraps a model-visible name, description, fino:validate input schema, and executor. Agents convert the schema into provider-neutral JSON Schema tool definitions, validate model-supplied arguments with fino:validate, and return tool outputs as model-readable tool_result content.

Execution model

Tool executors receive ToolRunContext, including the abort signal, run id, step index, current messages, and optional MessageHistory. Validation failures and ordinary executor exceptions are converted into isError tool results by default so the model can repair its call. Set throwOnError when application code should fail the run instead.

Throw AbortError to cancel, or throw/use SuspendSignal to pause a durable session or workflow for external input such as approval. Suspension is a control-flow signal, not a failed tool result.

import { tool } from 'fino:ai/tool';
import { v } from 'fino:validate';

const lookup = tool({
  name: 'lookup_user',
  description: 'Look up a user by id.',
  parameters: v.object({ id: v.string().describe('User id') }),
  execute: async ({ id }: { id: string }, ctx) => {
    if (id === 'needs-approval') ctx.suspend({ reason: 'approval required', payload: { id } });
    return { content: `user:${id}` };
  },
});

Classes

class SuspendSignal extends Error {

Signals that execution should suspend instead of fail.

Throwing a SuspendSignal (directly or via ToolRunContext.suspend()) tells the surrounding agent, session, or workflow harness to pause the run and wait for external input — an approval decision, a human answer, a webhook — rather than treat the tool call as an error. Tool.invoke() recognizes it by name and always rethrows it, bypassing both the default isError conversion and throwOnError.

The message defaults to 'Suspended'. Attach a payload with whatever the resuming side needs to present or resolve the suspension.

import { SuspendSignal } from 'fino:ai/tool';

throw new SuspendSignal('needs approval', { action: 'delete', target: 'prod-db' });

Properties

payload?: unknown

Application data carried to whatever resumes or renders the suspension, such as the pending action shown in an approval UI.

Constructors

constructor(message?: string, payload?: unknown)

Create a suspension signal with an optional human-readable reason and an optional payload for the resuming side. The message defaults to 'Suspended'.

class Tool< Args = unknown, R extends ToolResult = ToolResult > extends Task<Args, R> {

Executable tool exposed to a model.

A Tool is a fino:task Task specialized for model tool calling: the input schema is normalized to JSON Schema at construction, output mode is fixed to text, and the executor receives a fully-populated ToolRunContext. Everything from Task is inherited — agents call invoke() to get the tool_result contract (validated arguments, errors folded into isError results), while toToolDefinition(), run(), and the CLI surface remain available. This means one Tool can be handed to an agent, mounted in an MCP server, or run directly by application code.

Arguments are validated against parameters before the executor runs; a validation failure produces an isError result whose message summarizes the failing paths so the model can correct itself.

import { Tool } from 'fino:ai/tool';
import { v } from 'fino:validate';

const weather = new Tool({
  name: 'get_weather',
  description: 'Get current weather for a city.',
  parameters: v.object({ city: v.string() }),
  execute: async ({ city }: { city: string }) => `Sunny in ${city}`,
});

// Direct execution, outside an agent loop:
const text = await weather.run({ city: 'Lisbon' });

// Agent-style invocation with the tool_result contract (agents build the
// context for you; shown here for a custom loop):
const result = await weather.invoke({ city: 'Lisbon' }, {
  signal: AbortSignal.timeout(5_000),
  toolCallId: 'call_1',
  step: 0,
  runId: 'run_1',
  messages: [],
  suspend() { throw new Error('no suspension support'); },
});
// → { content: 'Sunny in Lisbon' }

Constructors

constructor(opts: ToolOptions<Args, R>)

Create a tool from ToolOptions, normalizing parameters to plain JSON Schema and wiring the executor so it always receives a complete ToolRunContext with a working suspend().

Interfaces

interface ToolRunContext {

Runtime context passed to a tool executor.

Agents populate every field when they invoke a tool during a run. When the same tool is executed outside an agent loop (for example directly through Task.run()), the identifiers fall back to 'task'/0 and messages is empty, so executors can rely on the fields always being present.

import { tool } from 'fino:ai/tool';
import { v } from 'fino:validate';

const fetchPage = tool({
  name: 'fetch_page',
  description: 'Fetch a URL as text.',
  parameters: v.object({ url: v.string() }),
  execute: async ({ url }: { url: string }, ctx) => {
    const res = await fetch(url, { signal: ctx.signal });
    return { content: await res.text() };
  },
});

Properties

signal: AbortSignal

Aborts when the run is cancelled or, when timeoutMs is set, when the tool's own deadline elapses. Pass it to any cancellable I/O the executor performs.

toolCallId: string

Identifier of the model's tool-call block that triggered this execution, matching the id echoed back in the tool_result message.

step: number

Zero-based index of the agent step (model turn) this call belongs to.

runId: string

Identifier of the enclosing agent run, stable across all steps and tool calls within it.

messages: ModelMessage[]

Snapshot of the conversation messages at the time of the call, for tools that need to inspect prior context.

history?: MessageHistory

Durable conversation history from fino:ai/context when the run is backed by one; absent for ad-hoc runs.

Methods

suspend(opts?: { reason?: string; payload?: unknown; }): never

Pause the run for external input instead of returning a result.

Inside a durable session or workflow this delegates to the harness's own suspend mechanism; otherwise it throws a SuspendSignal carrying the given reason and payload. Either way it never returns.

interface ToolOptions< Args, R extends ToolResult = ToolResult > {

Definition used to create a Tool.

parameters accepts a fino:validate schema builder such as v.object({ id: v.string() }), or a raw JSON Schema object when adapting an external protocol. Builders are normalized to plain JSON Schema before being sent to providers.

import { tool } from 'fino:ai/tool';
import { v } from 'fino:validate';

const remove = tool({
  name: 'remove_file',
  description: 'Delete a file from the workspace.',
  parameters: v.object({ path: v.string().describe('Workspace-relative path') }),
  risk: 'destructive',
  sideEffects: true,
  requiresApproval: true,
  timeoutMs: 10_000,
  execute: async ({ path }: { path: string }) => `deleted ${path}`,
});

Properties

name: string

Model-visible tool name, sent to the provider in the tool definition and matched against the model's tool-call blocks.

description: string

Model-visible description telling the model what the tool does and when to call it. Quality here directly affects tool-selection accuracy.

parameters: SchemaLike<Args>

Input schema for model-supplied tool arguments.

Prefer fino:validate builders such as v.object({ id: v.string() }). Raw JSON Schema objects are accepted for protocol adapters and are normalized before being sent to providers.

execute: (args: Args, ctx: ToolRunContext) => R | Promise<R>

Executor called with validated, typed arguments and the run context.

Return a ToolResult (or a promise of one). Exceptions become isError tool results unless throwOnError is set; AbortError and SuspendSignal always propagate.

risk?: string

Human-readable risk category for approval UIs and audit logs.

sideEffects?: boolean

Whether the tool has external side effects.

requiresApproval?: boolean

Require an agent/session to suspend for application approval before this tool executes.

timeoutMs?: number

Maximum intended execution duration in milliseconds.

When set, execute receives a child abort signal and the invocation returns an error tool result if the timeout elapses before the executor settles.

throwOnError?: boolean

Rethrow validation failures and executor exceptions to the caller instead of converting them into isError tool results.

Defaults to false, which lets the model see failures and repair its call. Enable it when a tool failure should abort the application-level run. AbortError and SuspendSignal propagate regardless.

Types

type ToolResult = string | { content: ContentPart | ContentPart[] | string; isError?: boolean; }

Value returned by a tool executor.

A plain string is shorthand for a successful text result. The object form carries structured ContentParts (text, images, and so on) and may set isError: true to report a domain failure to the model as a failed tool_result without throwing — useful when the model should see the failure and retry with different arguments.

Functions

function tool< Args = unknown, R extends ToolResult = ToolResult >(opts: ToolOptions<Args, R>): Tool<Args, R>

Create a tool from a schema and executor.

Convenience factory for new Tool(opts). Argument types are inferred from the Args type parameter (or annotated on execute), so the executor body is fully typed.

import { tool } from 'fino:ai/tool';
import { v } from 'fino:validate';

const add = tool({
  name: 'add',
  description: 'Add two numbers.',
  parameters: v.object({ a: v.number(), b: v.number() }),
  execute: ({ a, b }: { a: number; b: number }) => String(a + b),
});

function toToolDefinition(t: Tool): ToolDefinition

Convert a Tool into the provider-neutral model tool definition shape.

Returns the name, description, and normalized JSON Schema parameters that fino:ai/model providers send to the model. Agents call this automatically for the tools they are given; use it directly when building a custom loop or wiring tools into an external protocol.

import { tool, toToolDefinition } from 'fino:ai/tool';
import { v } from 'fino:validate';

const search = tool({
  name: 'search',
  description: 'Search the web.',
  parameters: v.object({ query: v.string() }),
  execute: async ({ query }: { query: string }) => `results for ${query}`,
});

const def = toToolDefinition(search);
// → { name: 'search', description: 'Search the web.', parameters: { type: 'object', ... } }