Kuralle
Kuralle
Build conversational agents with structured flows, routing, and durable tool execution. The same agent runs over text or voice.
Get startedKuralle is a TypeScript framework for building conversational AI agents — text and voice — with structured flows, routing, and durable tool execution.
Most agent frameworks give you a prompt and a tool-call loop. That’s fine until the conversation has steps — collect these fields, confirm, then book. Kuralle puts those steps in a flow: a small graph of typed nodes with real control flow, so your procedure lives in code you can test, not in a sprawling system prompt.
One tagless primitive (defineAgent) derives its behavior from the fields you set. Add flows to get a structured flow agent. Add routes to get a router. Add agents to compose them. The runtime, session management, and streaming stay the same across all three.
Primitives
Section titled “Primitives”- Agents —
defineAgentwith instructions and tools. Behavior is derived from what you populate, not a type flag. - Flows — node graphs (
reply,collect,action,decide) where each node returns its next transition. Your SOP becomes a typed state machine you didn’t have to hand-write. - Tools —
defineToolwith a Zod input schema and an async executor. Every tool effect is logged so a retried turn never double-executes. - Routing & Handoffs — structured routing (
routes) picks the right specialist without leaking dispatch text to the user.handoffstransfer session context between agents. - Runtime —
createRuntimewires agents, sessions, and streaming.runtime.run()returns aTurnHandle: stream events withhandle.events, await the result, or pipe to HTTP withhandle.toResponseStream('sse').
Why Kuralle
Section titled “Why Kuralle”Procedures belong in flows, not prompts. A booking or intake procedure becomes a handful of typed nodes you can test — not a long prompt you hope the model follows every turn.
One agent config, text and voice. The same defineAgent runs over chat text and over provider-native realtime voice. You don’t maintain two stacks.
Durable tool execution. Every defineTool call is logged in an append-only effect log. Retries replay against the log — a payment tool doesn’t charge twice, a booking tool doesn’t double-book.
Few primitives, composed. defineAgent, defineFlow, defineTool, createRuntime. That’s the core API. Structured routing, multi-agent composition, and session persistence are all derived from these.
Installation
Section titled “Installation”npm install @kuralle-agents/core @ai-sdk/openai ai zodPeer dependencies: ai@^6, zod. Bring your own provider package (@ai-sdk/openai, @ai-sdk/anthropic, etc.).
Your first agent
Section titled “Your first agent”import { openai } from '@ai-sdk/openai';import { z } from 'zod';import { defineAgent, defineTool, createRuntime, buildToolSet } 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 }),});
const agent = defineAgent({ id: 'support', name: 'Support Agent', instructions: 'Helpful support agent. Use the echo tool when asked.', model: openai('gpt-4o-mini'), tools: buildToolSet({ echo }), // make the tool model-visible effectTools: { echo }, // wire the durable executor});
const runtime = createRuntime({ agents: [agent], defaultAgentId: 'support',});
let sessionId: string | undefined;
async function chat(input: string) { const handle = runtime.run({ input, sessionId }); for await (const part of handle.events) { // events is a property, not a method if (part.type === 'text-delta') process.stdout.write(part.text); if (part.type === 'done') sessionId = part.sessionId; } await handle;}
await chat('Use echo to say "hello"');await chat('What did I just ask you to do?');Run it:
OPENAI_API_KEY=sk-... npx tsx agent.ts