@kuralle-agents/cf-agent 0.3.20 → 0.4.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.
@@ -15,4 +15,14 @@ import type { StreamAdapterConfig } from './types.js';
15
15
  * CF's applyChunkToParts() then builds UIMessage parts from these chunks.
16
16
  * CF handles persistence, broadcasting, and resumability.
17
17
  */
18
+ type SSETextState = {
19
+ open: boolean;
20
+ canceledIds: Set<string>;
21
+ };
18
22
  export declare function createSSEResponse(stream: AsyncGenerator<HarnessStreamPart>, config?: StreamAdapterConfig): Response;
23
+ /**
24
+ * Convert a single HarnessStreamPart to SSE data lines.
25
+ * Returns 0+ formatted SSE lines ("data: {...}\n\n").
26
+ */
27
+ export declare function convertToSSELines(part: HarnessStreamPart, config: StreamAdapterConfig, textState: SSETextState): string[];
28
+ export {};
@@ -1,38 +1,18 @@
1
1
  import { DEFAULT_STREAM_CONFIG } from './types.js';
2
- /**
3
- * Convert Kuralle's HarnessStreamPart generator into an SSE Response
4
- * that CF's AIChatAgent._reply() can parse.
5
- *
6
- * CF's _streamSSEReply reads lines in the format:
7
- * data: {"type":"text-start"}\n\n
8
- * data: {"type":"text-delta","delta":"Hello"}\n\n
9
- * data: {"type":"tool-input-available","toolCallId":"...","toolName":"...","input":{...}}\n\n
10
- * data: {"type":"tool-output-available","toolCallId":"...","output":{...}}\n\n
11
- * data: {"type":"data-handoff","data":{...}}\n\n
12
- * data: {"type":"text-end"}\n\n
13
- *
14
- * CF's applyChunkToParts() then builds UIMessage parts from these chunks.
15
- * CF handles persistence, broadcasting, and resumability.
16
- */
17
2
  export function createSSEResponse(stream, config = DEFAULT_STREAM_CONFIG) {
18
3
  const encoder = new TextEncoder();
19
- let textStarted = false;
4
+ const textState = { open: false, canceledIds: new Set() };
20
5
  const body = new ReadableStream({
21
6
  async start(controller) {
22
7
  try {
23
8
  for await (const part of stream) {
24
- const lines = convertToSSELines(part, config, textStarted);
9
+ const lines = convertToSSELines(part, config, textState);
25
10
  for (const line of lines) {
26
- // Track whether we've emitted text-start
27
- if (line.includes('"text-start"'))
28
- textStarted = true;
29
- if (line.includes('"text-end"'))
30
- textStarted = false;
31
11
  controller.enqueue(encoder.encode(line));
32
12
  }
33
13
  }
34
14
  // Ensure text is closed if still open
35
- if (textStarted) {
15
+ if (textState.open) {
36
16
  controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'text-end' })}\n\n`));
37
17
  }
38
18
  controller.close();
@@ -53,23 +33,39 @@ export function createSSEResponse(stream, config = DEFAULT_STREAM_CONFIG) {
53
33
  * Convert a single HarnessStreamPart to SSE data lines.
54
34
  * Returns 0+ formatted SSE lines ("data: {...}\n\n").
55
35
  */
56
- function convertToSSELines(part, config, textStarted) {
36
+ export function convertToSSELines(part, config, textState) {
57
37
  const lines = [];
58
38
  const sse = (obj) => `data: ${JSON.stringify(obj)}\n\n`;
39
+ const closeTextSegment = () => {
40
+ if (!textState.open)
41
+ return;
42
+ lines.push(sse({ type: 'text-end' }));
43
+ textState.open = false;
44
+ };
59
45
  switch (part.type) {
46
+ case 'text-start':
47
+ break;
48
+ case 'text-end':
49
+ closeTextSegment();
50
+ break;
51
+ case 'text-cancel': {
52
+ textState.canceledIds.add(part.id);
53
+ closeTextSegment();
54
+ break;
55
+ }
60
56
  case 'text-delta': {
61
- // Emit text-start on first text chunk
62
- if (!textStarted) {
57
+ if (textState.canceledIds.has(part.id)) {
58
+ break;
59
+ }
60
+ if (!textState.open) {
63
61
  lines.push(sse({ type: 'text-start' }));
62
+ textState.open = true;
64
63
  }
65
- lines.push(sse({ type: 'text-delta', delta: part.text }));
64
+ lines.push(sse({ type: 'text-delta', delta: part.delta }));
66
65
  break;
67
66
  }
68
67
  case 'tool-call': {
69
- // Close text before tool if open
70
- if (textStarted) {
71
- lines.push(sse({ type: 'text-end' }));
72
- }
68
+ closeTextSegment();
73
69
  // CF expects tool-input-available with toolCallId, toolName, input
74
70
  lines.push(sse({
75
71
  type: 'tool-input-available',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuralle-agents/cf-agent",
3
- "version": "0.3.20",
3
+ "version": "0.4.0",
4
4
  "description": "Kuralle agent integration for Cloudflare Workers with AIChatAgent",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,18 +22,18 @@
22
22
  "@cloudflare/ai-chat": "^0.1.8",
23
23
  "ai": "^6.0.90",
24
24
  "zod-to-json-schema": "^3.24.0",
25
- "@kuralle-agents/core": "0.3.20",
26
- "@kuralle-agents/realtime-audio": "0.3.20"
25
+ "@kuralle-agents/core": "0.4.0",
26
+ "@kuralle-agents/realtime-audio": "0.4.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "agents": "^0.11.5",
30
30
  "zod": "^3.0.0",
31
- "@kuralle-agents/voice-protocol": "0.3.20"
31
+ "@kuralle-agents/voice-protocol": "0.4.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "agents": "^0.11.5",
35
35
  "typescript": "^5.7.0",
36
- "@kuralle-agents/voice-protocol": "0.3.20"
36
+ "@kuralle-agents/voice-protocol": "0.4.0"
37
37
  },
38
38
  "engines": {
39
39
  "node": ">=20"
@@ -57,7 +57,7 @@
57
57
  "prebuild": "rm -rf dist",
58
58
  "build": "tsc",
59
59
  "dev": "tsc --watch",
60
- "test": "bun test src/voice/__tests__",
60
+ "test": "bun test src/voice/__tests__ src/__tests__",
61
61
  "typecheck": "tsc --noEmit",
62
62
  "clean": "rm -rf dist"
63
63
  }