Kuralle
Kuralle
Build production agents with typed procedures, durable effects, enforced policy, portable workspaces, and the Pi model/tool loop.
Kuralle is a TypeScript framework for agents that have to survive contact with real systems. The model can reason flexibly; typed flows own procedures, policy owns authority, and the durable journal owns consequential effects.
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 — model-reasoned routing (
routes/agents, derived from agent shape — norouting.mode) 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, pipe to HTTP withhandle.toUIMessageStreamResponse()(AI SDK native, foruseChat), or usehandle.toResponseStream('sse')for rawStreamPartJSON-SSE. - Workspaces & Skills — mount read-only knowledge and writable scratch on Node, SQL, Cloudflare Durable Objects, D1, R2, or libSQL. Procedural
SKILL.mdcontent discloses progressively and is content-hashed into traces. - Drivers — Pi is the recommended model/tool loop; Kuralle stays the outer authority for tools, flows, policy, sessions, streaming, and recovery. The AI SDK driver remains the portability baseline.
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.
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. Routing, multi-agent composition, and session persistence are all derived from these.
Production boundaries stay in code. A prompt cannot authorize a refund, mutate a repository, or choose its own tenant. Policy decides allow/ask/deny, workspaces enforce mounts, and human approval suspends durably before the effect.
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 } 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: { echo },});
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.payload.delta); if (part.type === 'done') sessionId = part.payload.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