Skip to content

tool()

Define a typed tool that an LLM can call.

tool() is an identity helper that infers types from your schema. It accepts a Tool spec and returns it unchanged — the value is in TypeScript inference.

Signature

ts
function tool<Input, Output>(spec: Tool<Input, Output>): Tool<Input, Output>

Tool spec

ts
type Tool<Input = unknown, Output = unknown> = {
  name: string;
  description: string;
  input: StandardSchemaV1<unknown, Input>;
  handler: (input: Input) => Promise<Output> | Output;
  permissions?: ToolPermissions;
  timeout?: number;
  jsonSchema?: Record<string, unknown>;
};

type ToolPermissions = {
  destructive?: boolean;
  scopes?: string[];
  network?: boolean;
  filesystem?: boolean;
  requireApproval?: boolean;
};
FieldTypeRequiredDescription
namestringYesTool name (sent to the model)
descriptionstringYesWhat the tool does (sent to the model)
inputStandardSchemaV1YesValidates and types the model's arguments
handler(input) => OutputYesExecutes the tool; may be async
permissionsToolPermissionsNoUsed by safety utilities
timeoutnumberNoMilliseconds before handler is aborted
jsonSchemaRecord<string, unknown>NoOverride JSON Schema sent to adapter (advanced)

Example

ts
import { tool } from 'flint';
import * as v from 'valibot';

const weatherTool = tool({
  name: 'get_weather',
  description: 'Get current weather for a location',
  input: v.object({
    location: v.string(),
    unit: v.optional(v.picklist(['celsius', 'fahrenheit']), 'celsius'),
  }),
  handler: async ({ location, unit }) => {
    // fetch real weather here
    return { location, temperature: 22, unit };
  },
});

ToolSpec reference

ts
type ToolSpec<Input, Output> = {
  name: string;
  description: string;
  input: StandardSchemaV1<unknown, Input>;
  handler: (input: Input) => Promise<Output> | Output;
  permissions?: ToolPermissions;
  timeout?: number;
  jsonSchema?: Record<string, unknown>;
};
FieldTypeDescription
namestringTool name sent to the LLM. Use snake_case. Must be unique within a call.
descriptionstringExplains to the LLM when and how to use the tool
inputStandardSchemaV1Input schema — Zod, Valibot, ArkType, or any Standard Schema library
handlerfunctionThe implementation. Receives typed, validated input.
permissionsToolPermissionsOptional permission metadata for requireApproval() and permissionedTools()
timeoutnumberMilliseconds before TimeoutError. Undefined = no timeout.
jsonSchemaRecord<string, unknown>Override the JSON Schema sent to the provider. Use when your schema library's output needs adjustment.

ToolPermissions

ts
type ToolPermissions = {
  destructive?: boolean;   // true if the tool modifies state irreversibly
  scopes?: string[];       // custom permission scope strings
  network?: boolean;       // true if the tool makes network requests
  filesystem?: boolean;    // true if the tool accesses the filesystem
  requireApproval?: boolean; // always require human approval
};

Permissions are metadata only — they don't restrict execution unless you use requireApproval() or permissionedTools() from the safety module.

Handler return types

The handler can return any serializable value. The agent loop serializes it to a tool result message:

ts
// Returning a string — used as-is
handler: () => 'success'

// Returning an object — JSON.stringify'd
handler: () => ({ count: 42, items: ['a', 'b'] })

// Returning a number
handler: ({ a, b }) => a + b

// Async handler
handler: async ({ url }) => {
  const res = await fetch(url);
  return res.text();
}

jsonSchema override

When your schema library generates JSON Schema that the LLM misinterprets, override with jsonSchema:

ts
const myTool = tool({
  name: 'search',
  description: 'Search for items',
  input: v.object({ query: v.string(), limit: v.optional(v.number()) }),
  handler: search,
  jsonSchema: {
    type: 'object',
    properties: {
      query: { type: 'string', description: 'The search query' },
      limit: { type: 'number', description: 'Max results (default 10)', default: 10 },
    },
    required: ['query'],
  },
});

Common mistakes

Tool names must match across calls

The LLM sends back the tool name exactly as you defined it. If you change a tool name between steps in a multi-turn conversation, the agent won't find the tool.

See also

Released under the MIT License.