Skip to content

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.

@kuralle-agents/hono-server mounts a full set of endpoints onto a Hono app with one call.

Terminal window
npm install @kuralle-agents/hono-server hono @hono/node-server @hono/node-ws
server.ts
import { 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;

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:

Terminal window
# 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/webhook

See packages/kuralle-messaging-meta/examples/whatsapp-server/README.md and the Engagement guide.

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 |

@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.

Terminal window
npm install @kuralle-agents/cf-agent
import { 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.

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.

Non-UI consumers that parsed HarnessStreamPart JSON from 0.4.x append ?format=raw:

Terminal window
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.

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.