@kuralle-agents/hono-server 0.4.1 → 0.6.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.
- package/README.md +72 -14
- package/dist/index.js +95 -35
- 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
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
101
|
+
`POST /api/chat/sse` accepts `useChat`-shaped bodies (`{ messages: UIMessage[] }`) and returns a native `UIMessageStream`. No `HarnessStreamPart` → `UIMessageChunk` bridge required.
|
|
52
102
|
|
|
53
|
-
|
|
54
|
-
import { upgradeWebSocket } from 'hono/bun';
|
|
103
|
+
## Raw JSON-SSE (`?format=raw`)
|
|
55
104
|
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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` |
|
|
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
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
await
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
await
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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.
|
|
9
|
+
"version": "0.6.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.
|
|
23
|
+
"@kuralle-agents/core": "0.6.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@kuralle-agents/core": "0.
|
|
26
|
+
"@kuralle-agents/core": "0.6.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@ai-sdk/openai": "^3.0.0",
|