Skip to content

Tools

Tools give agents the ability to call external functions. defineTool wraps a typed execute function with a Zod input schema so the model knows what to pass and what to expect back.

import { z } from 'zod';
import { defineTool } from '@kuralle-agents/core';
const echo = defineTool({
name: 'echo',
description: 'Echo back the provided text',
input: z.object({ text: z.string() }),
execute: async ({ text }) => ({ echoed: text }),
});

name and description go directly into the model’s tool schema. Keep descriptions accurate — the model uses them to decide when to call the tool.

input is a Zod object schema. Kuralle validates the model’s arguments against it before calling execute.

execute receives the validated input and returns any value. The return value is serialized and passed back to the model as the tool result.

There are two wiring steps — both are required:

tools: buildToolSet(...) — makes the tool model-visible. The model sees the schema and can decide to call it.

tools: { ... } — registers the executors on the agent so the runtime can resolve them by name (for example when a flow node calls a tool). Tool calls run through the runtime’s effect log, which gives exactly-once replay on retry.

import { defineAgent, buildToolSet } from '@kuralle-agents/core';
const tools = { echo };
const agent = defineAgent({
id: 'support',
instructions: 'Use the echo tool when asked.',
model: openai('gpt-4o-mini'),
tools: buildToolSet(tools), // model-visible
tools: tools, // durable executor
});

defineTool returns a Kuralle effect tool{ name, description, input, execute }, where input is a Standard Schema and execute is your durable handler. The model, however, speaks the Vercel AI SDK tool format. buildToolSet is the bridge between the two:

buildToolSet({ echo, lookup_order })
// → an AI SDK ToolSet: each entry has the tool's name, description, and
// inputSchema — the shape the model needs to know a tool exists and how to call it.

It copies each tool’s name, description, and input schema into an AI SDK ToolSet. It deliberately does not copy execute into the model-facing tool — the AI SDK entry is schema-only. The actual execution runs through the runtime, which routes every model-issued call through the durable effect log for exactly-once replay. (buildToolSet keeps each tool’s executor recoverable, so a model-issued call replays even before you register it in tools.)

That’s why the two fields are complementary rather than redundant:

  • tools: buildToolSet(tools)what the model can see and decide to call (name + description + input schema).
  • tools: toolsthe executor registry the runtime resolves by name (e.g. from flow nodes), and the fallback for tools not built with buildToolSet.

You author each tool once with defineTool; buildToolSet derives the model-facing view from it.

Every tool call is keyed in an append-only effect log. The runtime checks that key before calling execute — if the call already ran, it returns the logged result without calling execute again.

This means a payment tool won’t charge twice if the turn fails mid-execution, and a booking tool won’t double-book a slot.

const tools = {
lookup_order: lookupOrder,
cancel_order: cancelOrder,
create_ticket: createTicket,
};
const agent = defineAgent({
id: 'support',
instructions: '...',
model: openai('gpt-4o-mini'),
tools: tools,
});
echo-tool.ts
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { defineAgent, defineTool, createRuntime } from '@kuralle-agents/core';
// Define a tool with a Zod input schema and an async execute function
const echo = defineTool({
name: 'echo',
description: 'Echo back the provided text',
input: z.object({ text: z.string() }),
execute: async ({ text }) => ({ echoed: text }),
});
// Wire durable tools on the agent; flow nodes use buildToolSet for model-visible schema.
const agent = defineAgent({
id: 'support',
instructions: 'Use the echo tool when asked.',
model: openai('gpt-4o-mini'),
tools: { echo },
});
const runtime = createRuntime({ agents: [agent], defaultAgentId: 'support' });
const handle = runtime.run({ input: 'Echo "hello world"' });
for await (const part of handle.events) {
if (part.type === 'text-delta') process.stdout.write(part.delta);
if (part.type === 'done') console.log('\nDone.');
}
await handle;