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.

effectTools: { ... } — wires the durable executor. The runtime logs each call and result in the effect log, enabling 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
effectTools: 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 effectTools, which is what gives you the durable effect log and exactly-once replay.

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).
  • effectTools: toolshow the call actually runs (durable, logged, replay-safe).

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

Every tool call is written to an append-only effect log before execute is called. On retry, the runtime checks the log first — 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: buildToolSet(tools),
effectTools: tools,
});
echo-tool.ts
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { defineAgent, defineTool, createRuntime, buildToolSet } 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 it to an agent:
// tools: buildToolSet({ echo }) — makes it model-visible
// effectTools: { echo } — wires the durable executor
const agent = defineAgent({
id: 'support',
instructions: 'Use the echo tool when asked.',
model: openai('gpt-4o-mini'),
tools: buildToolSet({ echo }),
effectTools: { 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.text);
if (part.type === 'done') console.log('\nDone.');
}
await handle;