@kuralle-agents/hono-server 0.13.0 → 0.14.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 CHANGED
@@ -14,7 +14,7 @@ Peers: `@kuralle-agents/core`, `ai@^6`.
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`.
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 `StreamPart` JSON-SSE is opt-in via `?format=raw`.
18
18
 
19
19
  **Key exports:**
20
20
 
@@ -98,11 +98,11 @@ const server = serve({ fetch: app.fetch, port: 3000 });
98
98
  injectWebSocket(server);
99
99
  ```
100
100
 
101
- `POST /api/chat/sse` accepts `useChat`-shaped bodies (`{ messages: UIMessage[] }`) and returns a native `UIMessageStream`. No `HarnessStreamPart` → `UIMessageChunk` bridge required.
101
+ `POST /api/chat/sse` accepts `useChat`-shaped bodies (`{ messages: UIMessage[] }`) and returns a native `UIMessageStream`. No `StreamPart` → `UIMessageChunk` bridge required.
102
102
 
103
103
  ## Raw JSON-SSE (`?format=raw`)
104
104
 
105
- Non-UI consumers (curl, Studio, custom transports) that parsed raw `HarnessStreamPart` JSON from 0.4.x should append `?format=raw`:
105
+ Non-UI consumers (curl, Studio, custom transports) that parsed raw `StreamPart` JSON from 0.4.x should append `?format=raw`:
106
106
 
107
107
  ```bash
108
108
  curl -N -X POST 'http://localhost:3000/api/chat/sse?format=raw' \
@@ -117,7 +117,7 @@ Or use `createKuralleSseChatRouter` for a router that always emits raw JSON-SSE
117
117
  | Method | Path | Description |
118
118
  |--------|------|-------------|
119
119
  | `POST` | `/api/chat` | Single-turn JSON response |
120
- | `POST` | `/api/chat/sse` | **Default:** AI SDK `UIMessageStream` (`useChat`). **`?format=raw`:** legacy `HarnessStreamPart` JSON-SSE |
120
+ | `POST` | `/api/chat/sse` | **Default:** AI SDK `UIMessageStream` (`useChat`). **`?format=raw`:** legacy `StreamPart` JSON-SSE |
121
121
  | `POST` | `/api/chat/stream` | Chunked plain-text stream |
122
122
  | `GET` | `/agents/chat/:sessionId` | WebSocket widget endpoint |
123
123
  | `GET` | `/ws/:sessionId` | WebSocket turn endpoint |
@@ -139,7 +139,6 @@ createKuralleChatRouter({
139
139
  upgradeWebSocket,
140
140
  widgetWelcomeMode: 'static',
141
141
  widgetWelcomeMessage: "Hello, how can I help you today?",
142
- widgetWelcomeSuggestions: ['Check order status', 'Request a refund'],
143
142
  });
144
143
  ```
145
144
 
@@ -23,45 +23,29 @@ export function createKuralleSseChatRouter({ runtime, streamFilter: streamFilter
23
23
  input: body.message,
24
24
  userId: body.userId,
25
25
  });
26
- const responseStream = handle.toResponseStream('sse');
27
- const reader = responseStream.getReader();
28
26
  const encoder = new TextEncoder();
29
27
  const filtered = new ReadableStream({
30
28
  async start(controller) {
31
29
  try {
32
- while (true) {
33
- const { done, value } = await reader.read();
34
- if (done)
35
- break;
36
- const chunk = new TextDecoder().decode(value);
37
- for (const line of chunk.split('\n\n')) {
38
- if (!line.startsWith('data: '))
39
- continue;
40
- const raw = line.slice('data: '.length);
41
- if (!raw.trim())
42
- continue;
43
- let part;
44
- try {
45
- part = JSON.parse(raw);
46
- }
47
- catch {
48
- controller.enqueue(value);
49
- continue;
50
- }
51
- if (!shouldEmit(part, streamFilter))
52
- continue;
53
- const safe = sanitizeForClient(part);
54
- const payload = `data: ${JSON.stringify(safe)}\n\n`;
55
- controller.enqueue(encoder.encode(payload));
56
- }
30
+ for await (const part of handle.events) {
31
+ if (!shouldEmit(part, streamFilter))
32
+ continue;
33
+ const safe = sanitizeForClient(part);
34
+ const payload = `data: ${JSON.stringify(safe)}\n\n`;
35
+ controller.enqueue(encoder.encode(payload));
57
36
  }
37
+ await handle;
58
38
  controller.close();
59
39
  }
60
40
  catch (error) {
61
41
  const message = streamFilter === 'all'
62
42
  ? error.message
63
43
  : 'An error occurred. Please try again.';
64
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'error', error: message })}\n\n`));
44
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({
45
+ channel: 'client',
46
+ type: 'error',
47
+ payload: { error: message },
48
+ })}\n\n`));
65
49
  controller.close();
66
50
  }
67
51
  finally {
package/dist/index.d.ts CHANGED
@@ -1,18 +1,12 @@
1
1
  import { Hono } from 'hono';
2
2
  import type { Context, Handler } from 'hono';
3
- import type { HarnessStreamPart, RuntimeLike } from '@kuralle-agents/core';
3
+ import type { StreamPart, RuntimeLike } from '@kuralle-agents/core';
4
4
  export { createOpenAICompatRouter, type OpenAICompatRuntime, type CreateOpenAICompatRouterOptions, type ChatCompletionsRequest, } from './openaiCompat.js';
5
5
  export { shouldEmit, sanitizeForClient, type StreamEventFilter, } from './streamFilter.js';
6
6
  export { createKuralleSseChatRouter, type KuralleSseChatRouterOptions } from './chatRouter.js';
7
7
  import { type StreamEventFilter } from './streamFilter.js';
8
- type FlowStreamPart = {
9
- type: string;
10
- id?: string;
11
- delta?: string;
12
- error?: string;
13
- };
14
8
  type FlowRouterManager = {
15
- process: (input: string) => AsyncGenerator<FlowStreamPart>;
9
+ process: (input: string) => AsyncGenerator<StreamPart>;
16
10
  currentNodeName: string;
17
11
  nodeHistory: string[];
18
12
  hasEnded: boolean;
@@ -57,6 +51,17 @@ export type FlowResponse = {
57
51
  export type KuralleWebSocket = {
58
52
  send: (data: string) => void;
59
53
  };
54
+ export type WebSocketTransportFrame = {
55
+ type: 'connected';
56
+ sessionId: string;
57
+ timestamp: string;
58
+ } | {
59
+ type: 'cancelled';
60
+ sessionId: string;
61
+ timestamp: string;
62
+ } | {
63
+ type: 'pong';
64
+ };
60
65
  export type WebSocketEvent = {
61
66
  data?: unknown;
62
67
  };
@@ -92,12 +97,8 @@ export type KuralleChatRouterOptions = {
92
97
  */
93
98
  widgetWelcomeMessage?: string;
94
99
  /**
95
- * Optional static quick-reply suggestions for the initial widget state.
96
- */
97
- widgetWelcomeSuggestions?: string[];
98
- /**
99
- * Controls which HarnessStreamPart events are sent to external clients (SSE + widget/flow WebSockets).
100
- * - `'safe'`: only user-facing events (text-delta, done, sanitized error, suggested-questions, input).
100
+ * Controls which StreamPart events are sent to external clients (SSE + widget/flow WebSockets).
101
+ * - `'safe'`: events classified as `client` by core, with errors sanitized.
101
102
  * - `'all'`: full stream (Studio / dev tooling).
102
103
  * - function: custom predicate; return true to emit.
103
104
  * @default 'safe'
@@ -108,7 +109,8 @@ export type KuralleRouterOptions = {
108
109
  flowManager: FlowRouterManager;
109
110
  sessionId: string;
110
111
  upgradeWebSocket?: UpgradeWebSocket;
112
+ streamFilter?: StreamEventFilter;
111
113
  };
112
- export declare const createKuralleChatRouter: ({ runtime, upgradeWebSocket, widgetWelcomeMode, sendWidgetWelcomeMessage, widgetWelcomeMessage, widgetWelcomeSuggestions, streamFilter: streamFilterOption, }: KuralleChatRouterOptions) => Hono;
113
- export type { HarnessStreamPart };
114
- export declare const createKuralleRouter: ({ flowManager, sessionId, upgradeWebSocket, }: KuralleRouterOptions) => Hono;
114
+ export declare const createKuralleChatRouter: ({ runtime, upgradeWebSocket, widgetWelcomeMode, sendWidgetWelcomeMessage, widgetWelcomeMessage, streamFilter: streamFilterOption, }: KuralleChatRouterOptions) => Hono;
115
+ export type { StreamPart };
116
+ export declare const createKuralleRouter: ({ flowManager, sessionId, upgradeWebSocket, streamFilter: streamFilterOption, }: KuralleRouterOptions) => Hono;
package/dist/index.js CHANGED
@@ -84,13 +84,13 @@ const collectResponse = async (runtime, message, sessionId, userId) => {
84
84
  let resolvedSessionId = sessionId ?? '';
85
85
  for await (const part of iterateRuntimeParts(runtime, { input: message, sessionId, userId })) {
86
86
  if (part.type === 'text-delta') {
87
- response += part.delta;
87
+ response += part.payload.delta;
88
88
  }
89
89
  if (part.type === 'error') {
90
- throw new Error(part.error);
90
+ throw new Error(part.payload.error);
91
91
  }
92
92
  if (part.type === 'done') {
93
- resolvedSessionId = part.sessionId;
93
+ resolvedSessionId = part.payload.sessionId;
94
94
  }
95
95
  }
96
96
  return {
@@ -104,6 +104,19 @@ const sendSSEPart = async (stream, part, filter) => {
104
104
  const safe = sanitizeForClient(part);
105
105
  await stream.writeSSE({ event: safe.type, data: JSON.stringify(safe) });
106
106
  };
107
+ const sendStreamPartToWs = (ws, part, filter) => {
108
+ if (!shouldEmit(part, filter))
109
+ return;
110
+ ws.send(JSON.stringify(sanitizeForClient(part)));
111
+ };
112
+ const sendTransportFrameToWs = (ws, frame) => {
113
+ ws.send(JSON.stringify(frame));
114
+ };
115
+ const errorPart = (message) => ({
116
+ channel: 'client',
117
+ type: 'error',
118
+ payload: { error: message },
119
+ });
107
120
  const formatSessionResponse = (session) => ({
108
121
  sessionId: session.id,
109
122
  currentAgent: session.currentAgent ?? session.activeAgentId,
@@ -165,15 +178,11 @@ const resolveWidgetWelcomeMode = (mode, sendWidgetWelcomeMessage, widgetWelcomeM
165
178
  return 'static';
166
179
  return 'model';
167
180
  };
168
- export const createKuralleChatRouter = ({ runtime, upgradeWebSocket, widgetWelcomeMode, sendWidgetWelcomeMessage = true, widgetWelcomeMessage, widgetWelcomeSuggestions, streamFilter: streamFilterOption, }) => {
181
+ export const createKuralleChatRouter = ({ runtime, upgradeWebSocket, widgetWelcomeMode, sendWidgetWelcomeMessage = true, widgetWelcomeMessage, streamFilter: streamFilterOption, }) => {
169
182
  const streamFilter = streamFilterOption ?? 'safe';
170
183
  const app = new Hono();
171
184
  const effectiveWelcomeMode = resolveWidgetWelcomeMode(widgetWelcomeMode, sendWidgetWelcomeMessage, widgetWelcomeMessage);
172
- const sendHarnessPartToWs = (ws, part) => {
173
- if (!shouldEmit(part, streamFilter))
174
- return;
175
- ws.send(JSON.stringify(sanitizeForClient(part)));
176
- };
185
+ const sendHarnessPartToWs = (ws, part) => sendStreamPartToWs(ws, part, streamFilter);
177
186
  app.get('/health', (c) => c.json({ status: 'ok' }));
178
187
  app.post('/api/chat', async (c) => {
179
188
  const body = await parseJsonBody(c);
@@ -211,7 +220,7 @@ export const createKuralleChatRouter = ({ runtime, upgradeWebSocket, widgetWelco
211
220
  userId: body.userId,
212
221
  })) {
213
222
  if (part.type === 'text-delta') {
214
- controller.enqueue(encoder.encode(part.delta));
223
+ controller.enqueue(encoder.encode(part.payload.delta));
215
224
  }
216
225
  }
217
226
  }
@@ -441,39 +450,38 @@ export const createKuralleChatRouter = ({ runtime, upgradeWebSocket, widgetWelco
441
450
  onOpen: async (_event, ws) => {
442
451
  debug(`[Kuralle] New widget connection: ${sessionId}`);
443
452
  // Send connected message
444
- ws.send(JSON.stringify({
453
+ sendTransportFrameToWs(ws, {
445
454
  type: 'connected',
446
455
  sessionId,
447
456
  timestamp: new Date().toISOString(),
448
- }));
457
+ });
449
458
  if (effectiveWelcomeMode !== 'off') {
450
459
  const staticWelcome = widgetWelcomeMessage?.trim();
451
460
  if (effectiveWelcomeMode === 'static') {
452
461
  const welcomeId = crypto.randomUUID();
453
- ws.send(JSON.stringify({ type: 'text-start', id: welcomeId }));
454
- ws.send(JSON.stringify({
462
+ sendHarnessPartToWs(ws, {
463
+ channel: 'client',
464
+ type: 'text-start',
465
+ payload: { id: welcomeId },
466
+ });
467
+ sendHarnessPartToWs(ws, {
468
+ channel: 'client',
455
469
  type: 'text-delta',
456
- id: welcomeId,
457
- delta: staticWelcome || 'Hello! How can I help you today?',
458
- }));
459
- ws.send(JSON.stringify({ type: 'text-end', id: welcomeId }));
460
- const suggestions = (widgetWelcomeSuggestions ?? [])
461
- .filter((item) => typeof item === 'string')
462
- .map((item) => item.trim())
463
- .filter(Boolean)
464
- .slice(0, 3);
465
- if (suggestions.length > 0) {
466
- ws.send(JSON.stringify({
467
- type: 'suggested-questions',
468
- suggestions,
469
- isPartial: false,
470
- }));
471
- }
472
- ws.send(JSON.stringify({
470
+ payload: {
471
+ id: welcomeId,
472
+ delta: staticWelcome || 'Hello! How can I help you today?',
473
+ },
474
+ });
475
+ sendHarnessPartToWs(ws, {
476
+ channel: 'client',
477
+ type: 'text-end',
478
+ payload: { id: welcomeId },
479
+ });
480
+ sendHarnessPartToWs(ws, {
481
+ channel: 'client',
473
482
  type: 'done',
474
- sessionId,
475
- timestamp: new Date().toISOString(),
476
- }));
483
+ payload: { sessionId },
484
+ });
477
485
  }
478
486
  else {
479
487
  // Optionally start conversation with a generated greeting.
@@ -491,18 +499,29 @@ export const createKuralleChatRouter = ({ runtime, upgradeWebSocket, widgetWelco
491
499
  console.error(`[Kuralle] Failed to send greeting for session ${sessionId}:`, error);
492
500
  // Send a simple greeting if streaming fails
493
501
  const fallbackId = crypto.randomUUID();
494
- ws.send(JSON.stringify({ type: 'text-start', id: fallbackId }));
495
- ws.send(JSON.stringify({
502
+ sendHarnessPartToWs(ws, {
503
+ channel: 'client',
504
+ type: 'text-start',
505
+ payload: { id: fallbackId },
506
+ });
507
+ sendHarnessPartToWs(ws, {
508
+ channel: 'client',
496
509
  type: 'text-delta',
497
- id: fallbackId,
498
- delta: 'Hello! How can I help you today?',
499
- }));
500
- ws.send(JSON.stringify({ type: 'text-end', id: fallbackId }));
501
- ws.send(JSON.stringify({
510
+ payload: {
511
+ id: fallbackId,
512
+ delta: 'Hello! How can I help you today?',
513
+ },
514
+ });
515
+ sendHarnessPartToWs(ws, {
516
+ channel: 'client',
517
+ type: 'text-end',
518
+ payload: { id: fallbackId },
519
+ });
520
+ sendHarnessPartToWs(ws, {
521
+ channel: 'client',
502
522
  type: 'done',
503
- sessionId,
504
- timestamp: new Date().toISOString(),
505
- }));
523
+ payload: { sessionId },
524
+ });
506
525
  }
507
526
  }
508
527
  }
@@ -513,7 +532,7 @@ export const createKuralleChatRouter = ({ runtime, upgradeWebSocket, widgetWelco
513
532
  // Handle the widget's message format
514
533
  const input = payload.message;
515
534
  if (typeof input !== 'string' || !input.trim()) {
516
- ws.send(JSON.stringify({ type: 'error', error: 'message content required' }));
535
+ sendHarnessPartToWs(ws, errorPart('message content required'));
517
536
  return;
518
537
  }
519
538
  for await (const part of iterateRuntimeParts(runtime, {
@@ -526,12 +545,9 @@ export const createKuralleChatRouter = ({ runtime, upgradeWebSocket, widgetWelco
526
545
  }
527
546
  catch (error) {
528
547
  console.error('[Kuralle] Widget WebSocket error:', error);
529
- ws.send(JSON.stringify({
530
- type: 'error',
531
- error: streamFilter === 'all'
532
- ? error.message
533
- : 'An error occurred. Please try again.',
534
- }));
548
+ sendHarnessPartToWs(ws, errorPart(streamFilter === 'all'
549
+ ? error.message
550
+ : 'An error occurred. Please try again.'));
535
551
  }
536
552
  },
537
553
  };
@@ -542,11 +558,11 @@ export const createKuralleChatRouter = ({ runtime, upgradeWebSocket, widgetWelco
542
558
  const sessionId = c.req.param('sessionId') ?? '';
543
559
  return {
544
560
  onOpen(_event, ws) {
545
- ws.send(JSON.stringify({
561
+ sendTransportFrameToWs(ws, {
546
562
  type: 'connected',
547
563
  sessionId,
548
564
  timestamp: new Date().toISOString(),
549
- }));
565
+ });
550
566
  },
551
567
  async onMessage(event, ws) {
552
568
  try {
@@ -554,17 +570,17 @@ export const createKuralleChatRouter = ({ runtime, upgradeWebSocket, widgetWelco
554
570
  // Handle cancellation (barge-in)
555
571
  if (payload.type === 'cancel') {
556
572
  runtime.abortSession(sessionId, payload.reason ?? 'User interrupted');
557
- ws.send(JSON.stringify({
573
+ sendTransportFrameToWs(ws, {
558
574
  type: 'cancelled',
559
575
  sessionId,
560
576
  timestamp: new Date().toISOString(),
561
- }));
577
+ });
562
578
  return;
563
579
  }
564
580
  const input = payload.content ?? payload.message;
565
581
  if (payload.type === 'message') {
566
582
  if (typeof input !== 'string' || !input.trim()) {
567
- ws.send(JSON.stringify({ type: 'error', error: 'message content required' }));
583
+ sendHarnessPartToWs(ws, errorPart('message content required'));
568
584
  return;
569
585
  }
570
586
  for await (const part of iterateRuntimeParts(runtime, {
@@ -577,17 +593,14 @@ export const createKuralleChatRouter = ({ runtime, upgradeWebSocket, widgetWelco
577
593
  return;
578
594
  }
579
595
  if (payload.type === 'ping') {
580
- ws.send(JSON.stringify({ type: 'pong' }));
596
+ sendTransportFrameToWs(ws, { type: 'pong' });
581
597
  }
582
598
  }
583
599
  catch (error) {
584
600
  console.error('[Kuralle] Flow WebSocket error:', error);
585
- ws.send(JSON.stringify({
586
- type: 'error',
587
- error: streamFilter === 'all'
588
- ? error.message
589
- : 'An error occurred. Please try again.',
590
- }));
601
+ sendHarnessPartToWs(ws, errorPart(streamFilter === 'all'
602
+ ? error.message
603
+ : 'An error occurred. Please try again.'));
591
604
  }
592
605
  },
593
606
  };
@@ -605,10 +618,10 @@ const collectFlowResponse = async (flowManager, message) => {
605
618
  let response = '';
606
619
  for await (const part of flowManager.process(message)) {
607
620
  if (part.type === 'text-delta') {
608
- response += part.delta;
621
+ response += part.payload.delta;
609
622
  }
610
623
  if (part.type === 'error') {
611
- throw new Error(part.error);
624
+ throw new Error(part.payload.error);
612
625
  }
613
626
  }
614
627
  return {
@@ -616,8 +629,9 @@ const collectFlowResponse = async (flowManager, message) => {
616
629
  hasEnded: flowManager.hasEnded,
617
630
  };
618
631
  };
619
- export const createKuralleRouter = ({ flowManager, sessionId, upgradeWebSocket, }) => {
632
+ export const createKuralleRouter = ({ flowManager, sessionId, upgradeWebSocket, streamFilter: streamFilterOption, }) => {
620
633
  const app = new Hono();
634
+ const streamFilter = streamFilterOption ?? 'safe';
621
635
  app.get('/health', (c) => c.json({ status: 'ok' }));
622
636
  app.get('/info', (c) => c.json({
623
637
  sessionId,
@@ -670,7 +684,7 @@ export const createKuralleRouter = ({ flowManager, sessionId, upgradeWebSocket,
670
684
  try {
671
685
  for await (const part of flowManager.process(input)) {
672
686
  if (part.type === 'text-delta') {
673
- controller.enqueue(encoder.encode(part.delta));
687
+ controller.enqueue(encoder.encode(part.payload.delta));
674
688
  }
675
689
  }
676
690
  }
@@ -713,36 +727,8 @@ export const createKuralleRouter = ({ flowManager, sessionId, upgradeWebSocket,
713
727
  }
714
728
  });
715
729
  }
716
- async function* flowHarnessParts() {
717
- for await (const part of flowManager.process(input)) {
718
- if (part.type === 'text-start' ||
719
- part.type === 'text-delta' ||
720
- part.type === 'text-end' ||
721
- part.type === 'text-cancel' ||
722
- part.type === 'tool-call' ||
723
- part.type === 'tool-result' ||
724
- part.type === 'node-enter' ||
725
- part.type === 'node-exit' ||
726
- part.type === 'flow-enter' ||
727
- part.type === 'flow-transition' ||
728
- part.type === 'flow-end' ||
729
- part.type === 'handoff' ||
730
- part.type === 'interactive' ||
731
- part.type === 'safety-blocked' ||
732
- part.type === 'pipeline-validation-block' ||
733
- part.type === 'conversation-outcome' ||
734
- part.type === 'interrupted' ||
735
- part.type === 'paused' ||
736
- part.type === 'custom' ||
737
- part.type === 'error' ||
738
- part.type === 'done' ||
739
- part.type === 'turn-end') {
740
- yield part;
741
- }
742
- }
743
- }
744
730
  return createUIMessageStreamResponse({
745
- stream: harnessToUIMessageStream(flowHarnessParts(), {
731
+ stream: harnessToUIMessageStream(flowManager.process(input), {
746
732
  sessionId: bodySessionId ?? sessionId,
747
733
  }),
748
734
  });
@@ -752,11 +738,11 @@ export const createKuralleRouter = ({ flowManager, sessionId, upgradeWebSocket,
752
738
  onOpen(_event, ws) {
753
739
  // Generate sessionId for this connection
754
740
  const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
755
- ws.send(JSON.stringify({
741
+ sendTransportFrameToWs(ws, {
756
742
  type: 'connected',
757
743
  sessionId,
758
744
  timestamp: new Date().toISOString(),
759
- }));
745
+ });
760
746
  // Note: Automatic greeting would require async, but this is sync
761
747
  // The greeting will be sent on first message instead
762
748
  },
@@ -766,7 +752,7 @@ export const createKuralleRouter = ({ flowManager, sessionId, upgradeWebSocket,
766
752
  const input = payload.content ?? payload.message;
767
753
  if (payload.type === 'message') {
768
754
  if (typeof input !== 'string' || !input.trim()) {
769
- ws.send(JSON.stringify({ type: 'error', error: 'message content required' }));
755
+ sendStreamPartToWs(ws, errorPart('message content required'), streamFilter);
770
756
  return;
771
757
  }
772
758
  // For the first message, prepend a greeting
@@ -776,19 +762,16 @@ export const createKuralleRouter = ({ flowManager, sessionId, upgradeWebSocket,
776
762
  fullInput = greeting + input;
777
763
  }
778
764
  for await (const part of flowManager.process(fullInput)) {
779
- ws.send(JSON.stringify(part));
765
+ sendStreamPartToWs(ws, part, streamFilter);
780
766
  }
781
767
  return;
782
768
  }
783
769
  if (payload.type === 'ping') {
784
- ws.send(JSON.stringify({ type: 'pong' }));
770
+ sendTransportFrameToWs(ws, { type: 'pong' });
785
771
  }
786
772
  }
787
773
  catch (error) {
788
- ws.send(JSON.stringify({
789
- type: 'error',
790
- error: error.message,
791
- }));
774
+ sendStreamPartToWs(ws, errorPart(error.message), streamFilter);
792
775
  }
793
776
  }
794
777
  })));
@@ -175,16 +175,18 @@ async function collectTurn(handle, clientTools) {
175
175
  if (stopEarly)
176
176
  continue;
177
177
  if (part.type === 'text-delta') {
178
- text += part.delta;
178
+ text += part.payload.delta;
179
179
  }
180
180
  else if (part.type === 'tool-call') {
181
- if (!clientTools.has(part.toolName))
181
+ if (!clientTools.has(part.payload.toolName))
182
182
  continue;
183
- const argsStr = typeof part.args === 'string' ? part.args : JSON.stringify(part.args ?? {});
183
+ const argsStr = typeof part.payload.args === 'string'
184
+ ? part.payload.args
185
+ : JSON.stringify(part.payload.args ?? {});
184
186
  toolCalls.push({
185
187
  index: toolCalls.length,
186
- id: part.toolCallId ?? `call_${crypto.randomBytes(6).toString('hex')}`,
187
- name: part.toolName,
188
+ id: part.payload.toolCallId ?? `call_${crypto.randomBytes(6).toString('hex')}`,
189
+ name: part.payload.toolName,
188
190
  argsBuffer: argsStr,
189
191
  nameSent: false,
190
192
  argsSentLength: 0,
@@ -283,19 +285,21 @@ async function handleChatCompletions(c, body, opts) {
283
285
  if (stopEarly)
284
286
  continue;
285
287
  if (part.type === 'text-delta') {
286
- if (!part.delta)
288
+ if (!part.payload.delta)
287
289
  continue;
288
- completionText += part.delta;
289
- await writeChunk({ content: part.delta });
290
+ completionText += part.payload.delta;
291
+ await writeChunk({ content: part.payload.delta });
290
292
  }
291
293
  else if (part.type === 'tool-call') {
292
- if (!clientToolSet.has(part.toolName))
294
+ if (!clientToolSet.has(part.payload.toolName))
293
295
  continue;
294
- const argsStr = typeof part.args === 'string' ? part.args : JSON.stringify(part.args ?? {});
296
+ const argsStr = typeof part.payload.args === 'string'
297
+ ? part.payload.args
298
+ : JSON.stringify(part.payload.args ?? {});
295
299
  const acc = {
296
300
  index: pendingToolCalls.length,
297
- id: part.toolCallId ?? `call_${crypto.randomBytes(6).toString('hex')}`,
298
- name: part.toolName,
301
+ id: part.payload.toolCallId ?? `call_${crypto.randomBytes(6).toString('hex')}`,
302
+ name: part.payload.toolName,
299
303
  argsBuffer: argsStr,
300
304
  nameSent: false,
301
305
  argsSentLength: 0,
@@ -1,13 +1,4 @@
1
- export type StreamEventFilter = 'safe' | 'all' | ((part: {
2
- type: string;
3
- }) => boolean);
4
- export declare function shouldEmit(part: {
5
- type: string;
6
- }, filter: StreamEventFilter): boolean;
7
- export declare function sanitizeForClient(part: {
8
- type: string;
9
- error?: string;
10
- }): {
11
- type: string;
12
- error?: string;
13
- };
1
+ import type { StreamPart } from '@kuralle-agents/core';
2
+ export type StreamEventFilter = 'safe' | 'all' | ((part: StreamPart) => boolean);
3
+ export declare function shouldEmit(part: StreamPart, filter: StreamEventFilter): boolean;
4
+ export declare function sanitizeForClient(part: StreamPart): StreamPart;
@@ -1,28 +1,18 @@
1
- const SAFE_EVENT_TYPES = new Set([
2
- 'text-start',
3
- 'text-delta',
4
- 'text-end',
5
- 'text-cancel',
6
- 'text-clear',
7
- 'done',
8
- 'error',
9
- 'suggested-questions',
10
- 'conversation-outcome',
11
- 'knowledge-citation',
12
- 'knowledge-no-results',
13
- 'input',
14
- ]);
1
+ import { PART_CHANNEL } from '@kuralle-agents/core';
15
2
  export function shouldEmit(part, filter) {
16
3
  if (filter === 'all')
17
4
  return true;
18
5
  if (typeof filter === 'function')
19
6
  return filter(part);
20
- return SAFE_EVENT_TYPES.has(part.type);
7
+ return PART_CHANNEL[part.type] === 'client';
21
8
  }
22
9
  export function sanitizeForClient(part) {
23
10
  if (part.type === 'error') {
24
- console.error('[Kuralle] Client-facing error suppressed:', part.error);
25
- return { type: 'error', error: 'An error occurred. Please try again.' };
11
+ console.error('[Kuralle] Client-facing error suppressed:', part.payload.error);
12
+ return {
13
+ ...part,
14
+ payload: { error: 'An error occurred. Please try again.' },
15
+ };
26
16
  }
27
17
  return part;
28
18
  }
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/kuralle/kuralle-agents.git",
7
7
  "directory": "packages/hono-server"
8
8
  },
9
- "version": "0.13.0",
9
+ "version": "0.14.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.13.0"
23
+ "@kuralle-agents/core": "0.14.0"
24
24
  },
25
25
  "peerDependencies": {
26
- "@kuralle-agents/core": "0.13.0"
26
+ "@kuralle-agents/core": "0.14.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@ai-sdk/openai": "^3.0.0",