Skip to content

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.

  • AgentsdefineAgent with 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.
  • ToolsdefineTool with 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 — no routing.mode) picks the right specialist without leaking dispatch text to the user. handoffs transfer session context between agents.
  • RuntimecreateRuntime wires agents, sessions, and streaming. runtime.run() returns a TurnHandle: stream events with handle.events, await the result, pipe to HTTP with handle.toUIMessageStreamResponse() (AI SDK native, for useChat), or use handle.toResponseStream('sse') for raw StreamPart JSON-SSE.
  • Workspaces & Skills — mount read-only knowledge and writable scratch on Node, SQL, Cloudflare Durable Objects, D1, R2, or libSQL. Procedural SKILL.md content 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.

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.

Terminal window
npm install @kuralle-agents/core @ai-sdk/openai ai zod

Peer dependencies: ai@^6, zod. Bring your own provider package (@ai-sdk/openai, @ai-sdk/anthropic, etc.).

agent.ts
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:

Terminal window
OPENAI_API_KEY=sk-... npx tsx agent.ts