Deployment
Kuralle agents run anywhere that runs Node.js, Bun, or Cloudflare Workers. The runtime has no external process dependencies — serve it with whatever HTTP framework you prefer.
Hono (Node.js or Bun)
Section titled “Hono (Node.js or Bun)”@kuralle-agents/hono-server mounts a full set of endpoints onto a Hono app with one call.
npm install @kuralle-agents/hono-server hono @hono/node-server @hono/node-wsimport { Hono } from 'hono';import { serve } from '@hono/node-server';import { createNodeWebSocket } from '@hono/node-ws';import { createRuntime, defineAgent } from '@kuralle-agents/core';import { createKuralleChatRouter } from '@kuralle-agents/hono-server';import { openai } from '@ai-sdk/openai';
const agent = defineAgent({ id: 'support', instructions: 'You are a helpful support agent.', model: openai('gpt-4o-mini'),});
const runtime = createRuntime({ agents: [agent], defaultAgentId: 'support' });
const app = new Hono();const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app });app.route('/', createKuralleChatRouter({ runtime, upgradeWebSocket }));
const server = serve({ fetch: app.fetch, port: 3000 });injectWebSocket(server);Bun doesn’t need @hono/node-server or createNodeWebSocket. Use Bun’s built-in WebSocket upgrade instead:
import { upgradeWebSocket } from 'hono/bun';
const app = new Hono();app.route('/', createKuralleChatRouter({ runtime, upgradeWebSocket }));
export default app;WhatsApp & messaging webhooks
Section titled “WhatsApp & messaging webhooks”To deploy a bot on WhatsApp / Instagram (rather than the web chat endpoints), mount createMessagingRouter from engagement() instead — it exposes the Meta webhook at /{platform}/webhook. A self-hostable reference that boots on Bun or Node with a real WhatsApp Cloud API number (bring your own token, no Embedded Signup) and an optional Redis WindowStore:
# bring-your-own number/token — set WHATSAPP_* env, then:bun run packages/kuralle-messaging-meta/examples/whatsapp-server/server.ts# webhook: https://<host>/messaging/whatsapp/webhookSee packages/kuralle-messaging-meta/examples/whatsapp-server/README.md and the Engagement guide.
Endpoints
Section titled “Endpoints”createKuralleChatRouter mounts these routes:
| Method | Path | Description |
|---|---|---|
| POST | /api/chat | Single-turn JSON response |
| POST | /api/chat/sse | AI SDK UIMessageStream (default, useChat-compatible). ?format=raw for legacy HarnessStreamPart JSON-SSE |
| POST | /api/chat/stream | Chunked text stream |
| GET | /agents/chat/:sessionId | WebSocket widget endpoint |
| GET | /ws/:sessionId | WebSocket turn endpoint |
| GET | /api/session/:id | Fetch session |
| DELETE | /api/session/:id | Delete session |
| GET | /health | Health check |
Cloudflare Workers
Section titled “Cloudflare Workers”@kuralle-agents/cf-agent runs Kuralle agents on Cloudflare Workers with Durable Objects. Subclass KuralleAgent, implement two methods, and Cloudflare handles SQLite persistence, multi-client sync, and stream resumability.
npm install @kuralle-agents/cf-agentimport { KuralleAgent } from '@kuralle-agents/cf-agent';import { defineAgent } from '@kuralle-agents/core';import { createOpenAI } from '@ai-sdk/openai';
interface Env { OPENAI_API_KEY: string;}
export class SupportAgent extends KuralleAgent<Env> { protected getAgents() { const openai = createOpenAI({ apiKey: this.env.OPENAI_API_KEY }); return [ defineAgent({ id: 'support', instructions: 'You are a helpful support agent.', model: openai('gpt-4o-mini'), }), ]; }
protected getDefaultAgentId() { return 'support'; }}
export default SupportAgent;Declare the Durable Object in wrangler.toml:
[[durable_objects.bindings]]name = "SUPPORT_AGENT"class_name = "SupportAgent"
[[migrations]]tag = "v1"new_sqlite_classes = ["SupportAgent"]Flows, tools, and derived routing all work identically on Cloudflare — the same defineAgent primitive, no runtime differences.
HTTP streaming
Section titled “HTTP streaming”Web (useChat, default)
Section titled “Web (useChat, default)”POST /api/chat/sse returns a native AI SDK UIMessageStream. A React client uses useChat with no bridge:
import { useChat } from '@ai-sdk/react';import type { KuralleUIMessage } from '@kuralle-agents/core';
const { messages } = useChat<KuralleUIMessage>({ api: '/api/chat/sse' });Kuralle flow/safety/interactive events arrive as data-kuralle-* parts — read persistent parts from message.parts, transient telemetry from useChat({ onData }). See ADR 0005 for the full mapping table.
Raw JSON-SSE (?format=raw)
Section titled “Raw JSON-SSE (?format=raw)”Non-UI consumers that parsed HarnessStreamPart JSON from 0.4.x append ?format=raw:
curl -N -X POST 'http://localhost:3000/api/chat/sse?format=raw' \ -H 'Content-Type: application/json' \ -d '{"message":"hello"}'Or use createKuralleSseChatRouter for a router that always emits raw JSON-SSE.
Direct TurnHandle piping
Section titled “Direct TurnHandle piping”Without createKuralleChatRouter, return a native stream for web clients:
app.post('/chat', async (c) => { const { input, sessionId } = await c.req.json(); const handle = runtime.run({ input, sessionId }); return handle.toUIMessageStreamResponse({ sessionId });});For raw HarnessStreamPart JSON-SSE (voice adapters, curl, custom transports):
return new Response(handle.toResponseStream('sse'), { headers: { 'Content-Type': 'text/event-stream; charset=utf-8' },});createKuralleChatRouter wires the native default on /api/chat/sse and also mounts sessions, WebSocket, and the full endpoint set.