Tool
A function the model can call, with JSON-schema-typed arguments and a JSON-serializable result.
A Tool is a function the model can request by name. It is the primary mechanism by which an agent acts on the world: reading files, calling APIs, searching the web, executing code, editing documents, sending Slack messages.
If a Skill is what the model becomes, a Tool is what the model calls. That distinction is load-bearing — confusing the two is the recurring failure mode in agent libraries.
The interface
import type { ToolDefinition } from '@agentskit/core'
import type { JSONSchema7 } from 'json-schema'
export interface ToolDefinition {
name: string
description?: string
schema?: JSONSchema7
requiresConfirmation?: boolean
execute?: (
args: Record<string, unknown>,
context: ToolExecutionContext,
) => MaybePromise<unknown> | AsyncIterable<unknown>
init?: () => MaybePromise<void>
dispose?: () => MaybePromise<void>
tags?: string[]
category?: string
}Defining a tool
import type { ToolDefinition } from '@agentskit/core'
export const getWeather: ToolDefinition = {
name: 'get_weather',
description: 'Get the current weather for a city.',
schema: {
type: 'object',
properties: {
city: { type: 'string', description: 'City name, e.g. "Madrid"' },
},
required: ['city'],
},
async execute(args) {
const res = await fetch(`https://wttr.in/${args.city}?format=j1`)
return await res.json()
},
}The model sees name, description, and schema. The runtime validates args against schema before calling execute. Your function never receives malformed input.
Using built-in tools
import { webSearch, filesystem, shell } from '@agentskit/tools'
createRuntime({
adapter,
tools: [
webSearch(),
...filesystem({ basePath: './workspace' }),
shell({ allowed: ['ls', 'cat'] }),
],
})Confirmation gates
Set requiresConfirmation: true and the runtime will pause before executing, asking your onConfirm handler:
const dangerousTool: ToolDefinition = {
name: 'delete_file',
schema: { /* ... */ },
requiresConfirmation: true,
async execute(args) { /* ... */ },
}
createRuntime({
adapter,
tools: [dangerousTool],
onConfirm: async (call) => {
return await showConfirmationDialog(call)
},
})There is no timeout-based auto-approval. If the runtime requires confirmation but no onConfirm is configured, execution is refused. This matters for security-critical tools and is non-negotiable.
Streaming tool execution
Long-running tools can yield progress as an AsyncIterable:
const slowTool: ToolDefinition = {
name: 'process_dataset',
schema: { /* ... */ },
async *execute(args) {
yield 'Loading dataset...'
const data = await loadDataset(args.path)
yield `Processing ${data.length} rows...`
const result = await process(data)
return result // the last yielded value is the recorded result
},
}Consumers see partial values as informational; only the last one is recorded as the official result.
Declaration-only tools (MCP-friendly)
A tool without execute is a declaration — the model can call it, and the caller (typically a client or an MCP bridge) handles the actual execution. This is what makes the AgentsKit Tool contract a thin mapping over MCP's tool spec.
const remoteTool: ToolDefinition = {
name: 'lookup_customer',
description: 'Find a customer by email.',
schema: { type: 'object', properties: { email: { type: 'string' } } },
// no execute — runs elsewhere
}Type-safe tools with defineTool
Writing ToolDefinition objects directly types args as Record<string, unknown>, which means you must cast inside execute. The defineTool factory infers the args type directly from the JSON Schema so you get full type-safety and autocomplete at zero runtime cost.
import { defineTool } from '@agentskit/core'
const createTask = defineTool({
name: 'create_task',
description: 'Create a task in the project tracker.',
schema: {
type: 'object',
properties: {
title: { type: 'string' },
priority: { type: 'string', enum: ['low', 'medium', 'high'] },
dueDate: { type: 'string' },
},
required: ['title', 'priority'],
} as const, // <-- required for inference
async execute(args) {
// args.title → string (required, inferred)
// args.priority → string (required, inferred)
// args.dueDate → string | undefined (optional, inferred)
return await tracker.create({ title: args.title, priority: args.priority, dueDate: args.dueDate })
},
})The as const assertion on schema narrows the literal types so InferSchemaType can map them to TypeScript. Without it, args falls back to Record<string, unknown>.
Using the inferred type standalone
import type { InferSchemaType } from '@agentskit/core'
const schema = {
type: 'object',
properties: {
query: { type: 'string' },
limit: { type: 'integer' },
},
required: ['query'],
} as const
type SearchArgs = InferSchemaType<typeof schema>
// → { query: string; limit?: number }Zod-based tools with defineZodTool
If you already use Zod in your project, @agentskit/tools ships defineZodTool — a factory that types execute args from a Zod schema and also validates them at runtime via schema.parse.
Zod is not bundled — it must be installed as a peer dependency. You also supply the JSON Schema conversion yourself (e.g. via zod-to-json-schema), which keeps the Zod dependency fully optional.
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { defineZodTool } from '@agentskit/tools'
import type { JSONSchema7 } from 'json-schema'
const sendEmail = defineZodTool({
name: 'send_email',
description: 'Send an email to a recipient.',
schema: z.object({
to: z.string().email(),
subject: z.string(),
body: z.string(),
cc: z.string().email().optional(),
}),
toJsonSchema: (s) => zodToJsonSchema(s) as JSONSchema7,
async execute(args) {
// args.to → string (Zod-validated at runtime)
// args.subject → string
// args.body → string
// args.cc → string | undefined
await mailer.send({ to: args.to, subject: args.subject, body: args.body, cc: args.cc })
return { sent: true }
},
})defineZodTool wraps execute to call schema.parse(args) before your function runs. Invalid input raises a Zod ZodError — the runtime surfaces it as a tool error rather than crashing the agent.
Install peer dependencies
npm install zod zod-to-json-schemaWhen to use defineTool vs defineZodTool
defineTool | defineZodTool | |
|---|---|---|
| Deps | none (core only) | zod + zod-to-json-schema |
| Runtime validation | no | yes (via schema.parse) |
| Schema authoring | JSON Schema (as const) | Zod API |
| JSON Schema for adapter | you write it | converted automatically |
Use defineTool when you want zero extra dependencies and are comfortable with JSON Schema. Use defineZodTool when Zod is already in your stack and you want runtime validation as a safety net.
Common pitfalls
| Pitfall | What to do instead |
|---|---|
Returning a Date or Buffer from execute | Serialize: date.toISOString(), buffer.toString('base64') |
Throwing from execute on a recoverable error | Return { error: '...' } so the model can react and retry |
| Implementing your own confirmation timeout | Don't. Use requiresConfirmation + onConfirm properly |
Doing I/O at definition time (top-level await) | Move it into init() or execute() — see invariant T10 |
| Naming tools with spaces or special characters | Names must match ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$ |
Going deeper
The full list of invariants (twelve of them, T1–T12) is in ADR 0002 — Tool contract.