agentskit.js
Concepts

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-schema

When to use defineTool vs defineZodTool

defineTooldefineZodTool
Depsnone (core only)zod + zod-to-json-schema
Runtime validationnoyes (via schema.parse)
Schema authoringJSON Schema (as const)Zod API
JSON Schema for adapteryou write itconverted 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

PitfallWhat to do instead
Returning a Date or Buffer from executeSerialize: date.toISOString(), buffer.toString('base64')
Throwing from execute on a recoverable errorReturn { error: '...' } so the model can react and retry
Implementing your own confirmation timeoutDon'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 charactersNames 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.

✎ Edit this page on GitHub·Found a problem? Open an issue →·How to contribute →

On this page