@kuralle-agents/hono-server 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +72 -14
  2. package/dist/index.js +95 -35
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -5,24 +5,74 @@ Hono router that exposes a Kuralle `Runtime` over HTTP, SSE, and WebSocket.
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install @kuralle-agents/hono-server @kuralle-agents/core
8
+ npm install @kuralle-agents/hono-server @kuralle-agents/core ai
9
9
  ```
10
10
 
11
- Peer: `@kuralle-agents/core`.
11
+ Peers: `@kuralle-agents/core`, `ai@^6`.
12
12
 
13
13
  ## What it does
14
14
 
15
15
  `createKuralleChatRouter` mounts a complete set of endpoints onto a Hono app — HTTP chat, SSE streaming, WebSocket widget, session management, audit, CSAT, and a manual compression trigger — wired to any `RuntimeLike` instance.
16
16
 
17
+ **As of 0.5.0, `POST /api/chat/sse` defaults to an AI SDK `UIMessageStream`** — a `useChat` client works with zero bridge code. Raw `HarnessStreamPart` JSON-SSE is opt-in via `?format=raw`.
18
+
17
19
  **Key exports:**
18
20
 
19
- - **`createKuralleChatRouter`** — full router: chat, SSE, WebSocket, session, outcome, audit endpoints.
20
- - **`createKuralleSseChatRouter`** — SSE-only variant for simpler setups.
21
+ - **`createKuralleChatRouter`** — full router: chat, SSE (native default), WebSocket, session, outcome, audit endpoints.
22
+ - **`createKuralleSseChatRouter`** — raw JSON-SSE only (explicit legacy wire).
21
23
  - **`createOpenAICompatRouter`** — OpenAI-compatible `/v1/chat/completions` endpoint.
22
24
  - **`createKuralleRouter`** — standalone router for flow-manager instances.
23
25
  - **`shouldEmit` / `sanitizeForClient`** — stream event filter utilities.
24
26
 
25
- ## Usage
27
+ ## Web client (`useChat`, no bridge)
28
+
29
+ ```tsx
30
+ 'use client';
31
+
32
+ import { useChat } from '@ai-sdk/react';
33
+ import type { KuralleUIMessage } from '@kuralle-agents/core';
34
+
35
+ export function Chat() {
36
+ const { messages, sendMessage } = useChat<KuralleUIMessage>({
37
+ api: '/api/chat/sse',
38
+ onData: (part) => {
39
+ // transient telemetry (node/flow/control) — not persisted to message.parts
40
+ if (part.type === 'data-kuralle-node') {
41
+ console.log('node:', part.data);
42
+ }
43
+ },
44
+ });
45
+
46
+ return (
47
+ <div>
48
+ {messages.map((m) => (
49
+ <div key={m.id}>
50
+ {m.parts.map((part, i) => {
51
+ if (part.type === 'text') return <span key={i}>{part.text}</span>;
52
+ if (part.type === 'data-kuralle-safety') {
53
+ return <div key={i}>Blocked: {part.data.userFacingMessage}</div>;
54
+ }
55
+ if (part.type === 'data-kuralle-interactive') {
56
+ return (
57
+ <div key={i}>
58
+ {part.data.options.map((o) => (
59
+ <button key={o.value} onClick={() => sendMessage({ text: o.value })}>
60
+ {o.label}
61
+ </button>
62
+ ))}
63
+ </div>
64
+ );
65
+ }
66
+ return null;
67
+ })}
68
+ </div>
69
+ ))}
70
+ </div>
71
+ );
72
+ }
73
+ ```
74
+
75
+ Server:
26
76
 
27
77
  ```ts
28
78
  import { Hono } from 'hono';
@@ -48,22 +98,27 @@ const server = serve({ fetch: app.fetch, port: 3000 });
48
98
  injectWebSocket(server);
49
99
  ```
50
100
 
51
- **Bun:**
101
+ `POST /api/chat/sse` accepts `useChat`-shaped bodies (`{ messages: UIMessage[] }`) and returns a native `UIMessageStream`. No `HarnessStreamPart` → `UIMessageChunk` bridge required.
52
102
 
53
- ```ts
54
- import { upgradeWebSocket } from 'hono/bun';
103
+ ## Raw JSON-SSE (`?format=raw`)
55
104
 
56
- app.route('/', createKuralleChatRouter({ runtime, upgradeWebSocket }));
57
- export default app;
105
+ Non-UI consumers (curl, Studio, custom transports) that parsed raw `HarnessStreamPart` JSON from 0.4.x should append `?format=raw`:
106
+
107
+ ```bash
108
+ curl -N -X POST 'http://localhost:3000/api/chat/sse?format=raw' \
109
+ -H 'Content-Type: application/json' \
110
+ -d '{"message":"hello","sessionId":"demo"}'
58
111
  ```
59
112
 
60
- ## Endpoints (createKuralleChatRouter)
113
+ Or use `createKuralleSseChatRouter` for a router that always emits raw JSON-SSE on `/api/chat/sse`.
114
+
115
+ ## Endpoints (`createKuralleChatRouter`)
61
116
 
62
117
  | Method | Path | Description |
63
118
  |--------|------|-------------|
64
119
  | `POST` | `/api/chat` | Single-turn JSON response |
65
- | `POST` | `/api/chat/sse` | SSE streaming response |
66
- | `POST` | `/api/chat/stream` | Chunked text stream |
120
+ | `POST` | `/api/chat/sse` | **Default:** AI SDK `UIMessageStream` (`useChat`). **`?format=raw`:** legacy `HarnessStreamPart` JSON-SSE |
121
+ | `POST` | `/api/chat/stream` | Chunked plain-text stream |
67
122
  | `GET` | `/agents/chat/:sessionId` | WebSocket widget endpoint |
68
123
  | `GET` | `/ws/:sessionId` | WebSocket turn endpoint |
69
124
  | `GET` | `/api/session/:id` | Fetch session |
@@ -100,7 +155,10 @@ createKuralleChatRouter({
100
155
  });
101
156
  ```
102
157
 
158
+ Applies to raw JSON-SSE (`?format=raw`) and WebSocket paths. The native `UIMessageStream` default passes through the adapter unfiltered.
159
+
103
160
  ## Related
104
161
 
105
- - [`@kuralle-agents/core`](https://www.npmjs.com/package/@kuralle-agents/core) — runtime and agent primitives.
162
+ - [`@kuralle-agents/core`](https://www.npmjs.com/package/@kuralle-agents/core) — runtime, `harnessToUIMessageStream`, `KuralleUIMessage`.
106
163
  - [`@kuralle-agents/cf-agent`](https://www.npmjs.com/package/@kuralle-agents/cf-agent) — Cloudflare Workers variant.
164
+ - `docs/adr/0005-ai-sdk-native-uimessage-default.md` — decision record and `data-kuralle-*` mapping table.
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
+ import { createUIMessageStreamResponse } from 'ai';
1
2
  import { Hono } from 'hono';
2
3
  import { streamSSE } from 'hono/streaming';
4
+ import { harnessToUIMessageStream } from '@kuralle-agents/core';
3
5
  // PR-20 — re-export the OpenAI-compatible router. Lets consumers do:
4
6
  // import { createOpenAICompatRouter } from '@kuralle-agents/hono-server';
5
7
  export { createOpenAICompatRouter, } from './openaiCompat.js';
@@ -15,6 +17,7 @@ const parseJsonBody = async (c) => {
15
17
  return null;
16
18
  }
17
19
  };
20
+ const wantsRawStreamFormat = (c) => c.req.query('format') === 'raw';
18
21
  const extractInputFromBody = (body) => {
19
22
  if (typeof body.message === 'string') {
20
23
  return { input: body.message, sessionId: body.sessionId };
@@ -23,9 +26,15 @@ const extractInputFromBody = (body) => {
23
26
  const messages = body.messages;
24
27
  const lastMessage = messages[messages.length - 1];
25
28
  if (lastMessage.parts && Array.isArray(lastMessage.parts)) {
26
- const textPart = lastMessage.parts.find((p) => p.type === 'text');
27
- const input = textPart?.text || '';
28
- const sessionId = typeof body.id === 'string' ? body.id : undefined;
29
+ const input = lastMessage.parts
30
+ .filter((p) => p.type === 'text')
31
+ .map((p) => p.text ?? '')
32
+ .join('');
33
+ const sessionId = typeof body.sessionId === 'string'
34
+ ? body.sessionId
35
+ : typeof body.id === 'string'
36
+ ? body.id
37
+ : undefined;
29
38
  return { input, sessionId };
30
39
  }
31
40
  }
@@ -202,30 +211,42 @@ export const createKuralleChatRouter = ({ runtime, upgradeWebSocket, widgetWelco
202
211
  });
203
212
  app.post('/api/chat/sse', async (c) => {
204
213
  const body = await parseJsonBody(c);
205
- if (!body || !body.message) {
214
+ if (!body) {
215
+ return c.json({ error: 'invalid body' }, 400);
216
+ }
217
+ const { input, sessionId: bodySessionId } = extractInputFromBody(body);
218
+ if (!input) {
206
219
  return c.json({ error: 'message required' }, 400);
207
220
  }
208
- return streamSSE(c, async (stream) => {
209
- try {
210
- for await (const part of iterateRuntimeParts(runtime, {
211
- input: body.message,
212
- sessionId: body.sessionId,
213
- userId: body.userId,
214
- })) {
215
- await sendSSEPart(stream, part, streamFilter);
221
+ const sessionId = bodySessionId ??
222
+ (typeof body.sessionId === 'string' ? body.sessionId : undefined) ??
223
+ crypto.randomUUID();
224
+ const userId = typeof body.userId === 'string' ? body.userId : undefined;
225
+ if (wantsRawStreamFormat(c)) {
226
+ return streamSSE(c, async (stream) => {
227
+ try {
228
+ for await (const part of iterateRuntimeParts(runtime, {
229
+ input,
230
+ sessionId,
231
+ userId,
232
+ })) {
233
+ await sendSSEPart(stream, part, streamFilter);
234
+ }
216
235
  }
217
- }
218
- catch (error) {
219
- console.error('[Kuralle] SSE stream error:', error);
220
- const message = streamFilter === 'all'
221
- ? error.message
222
- : 'An error occurred. Please try again.';
223
- await stream.writeSSE({
224
- event: 'error',
225
- data: JSON.stringify({ error: message }),
226
- });
227
- }
228
- });
236
+ catch (error) {
237
+ console.error('[Kuralle] SSE stream error:', error);
238
+ const message = streamFilter === 'all'
239
+ ? error.message
240
+ : 'An error occurred. Please try again.';
241
+ await stream.writeSSE({
242
+ event: 'error',
243
+ data: JSON.stringify({ error: message }),
244
+ });
245
+ }
246
+ });
247
+ }
248
+ const handle = runtime.run({ input, sessionId, userId });
249
+ return handle.toUIMessageStreamResponse({ sessionId });
229
250
  });
230
251
  app.post('/api/chat/resume', async (c) => {
231
252
  const body = await parseJsonBody(c);
@@ -647,21 +668,60 @@ export const createKuralleRouter = ({ flowManager, sessionId, upgradeWebSocket,
647
668
  });
648
669
  app.post('/api/flow/sse', async (c) => {
649
670
  const body = await parseJsonBody(c);
650
- if (!body || !body.message) {
671
+ if (!body) {
672
+ return c.json({ error: 'invalid body' }, 400);
673
+ }
674
+ const { input, sessionId: bodySessionId } = extractInputFromBody(body);
675
+ if (!input) {
651
676
  return c.json({ error: 'message required' }, 400);
652
677
  }
653
- return streamSSE(c, async (stream) => {
654
- try {
655
- for await (const part of flowManager.process(body.message)) {
656
- await sendFlowSSEPart(stream, part);
678
+ if (wantsRawStreamFormat(c)) {
679
+ return streamSSE(c, async (stream) => {
680
+ try {
681
+ for await (const part of flowManager.process(input)) {
682
+ await sendFlowSSEPart(stream, part);
683
+ }
684
+ }
685
+ catch (error) {
686
+ await stream.writeSSE({
687
+ event: 'error',
688
+ data: JSON.stringify({ error: error.message }),
689
+ });
690
+ }
691
+ });
692
+ }
693
+ async function* flowHarnessParts() {
694
+ for await (const part of flowManager.process(input)) {
695
+ if (part.type === 'text-start' ||
696
+ part.type === 'text-delta' ||
697
+ part.type === 'text-end' ||
698
+ part.type === 'text-cancel' ||
699
+ part.type === 'tool-call' ||
700
+ part.type === 'tool-result' ||
701
+ part.type === 'node-enter' ||
702
+ part.type === 'node-exit' ||
703
+ part.type === 'flow-enter' ||
704
+ part.type === 'flow-transition' ||
705
+ part.type === 'flow-end' ||
706
+ part.type === 'handoff' ||
707
+ part.type === 'interactive' ||
708
+ part.type === 'safety-blocked' ||
709
+ part.type === 'pipeline-validation-block' ||
710
+ part.type === 'conversation-outcome' ||
711
+ part.type === 'interrupted' ||
712
+ part.type === 'paused' ||
713
+ part.type === 'custom' ||
714
+ part.type === 'error' ||
715
+ part.type === 'done' ||
716
+ part.type === 'turn-end') {
717
+ yield part;
657
718
  }
658
719
  }
659
- catch (error) {
660
- await stream.writeSSE({
661
- event: 'error',
662
- data: JSON.stringify({ error: error.message }),
663
- });
664
- }
720
+ }
721
+ return createUIMessageStreamResponse({
722
+ stream: harnessToUIMessageStream(flowHarnessParts(), {
723
+ sessionId: bodySessionId ?? sessionId,
724
+ }),
665
725
  });
666
726
  });
667
727
  if (upgradeWebSocket) {
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/kuralle/kuralle-agents.git",
7
7
  "directory": "packages/kuralle-hono-server"
8
8
  },
9
- "version": "0.4.1",
9
+ "version": "0.5.0",
10
10
  "description": "Hono router for hosting Kuralle createRuntime() over HTTP, SSE, and WebSocket",
11
11
  "type": "module",
12
12
  "main": "dist/index.js",
@@ -20,10 +20,10 @@
20
20
  "dependencies": {
21
21
  "ai": "^6.0.0",
22
22
  "hono": "^4.0.0",
23
- "@kuralle-agents/core": "0.4.1"
23
+ "@kuralle-agents/core": "0.5.0"
24
24
  },
25
25
  "peerDependencies": {
26
- "@kuralle-agents/core": "0.4.1"
26
+ "@kuralle-agents/core": "0.5.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@ai-sdk/openai": "^3.0.0",