@runtypelabs/persona 1.36.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 (61) hide show
  1. package/README.md +1080 -0
  2. package/dist/index.cjs +140 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +2626 -0
  5. package/dist/index.d.ts +2626 -0
  6. package/dist/index.global.js +1843 -0
  7. package/dist/index.global.js.map +1 -0
  8. package/dist/index.js +140 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/install.global.js +2 -0
  11. package/dist/install.global.js.map +1 -0
  12. package/dist/widget.css +1627 -0
  13. package/package.json +79 -0
  14. package/src/@types/idiomorph.d.ts +37 -0
  15. package/src/client.test.ts +387 -0
  16. package/src/client.ts +1589 -0
  17. package/src/components/composer-builder.ts +530 -0
  18. package/src/components/feedback.ts +379 -0
  19. package/src/components/forms.ts +170 -0
  20. package/src/components/header-builder.ts +455 -0
  21. package/src/components/header-layouts.ts +303 -0
  22. package/src/components/launcher.ts +193 -0
  23. package/src/components/message-bubble.ts +528 -0
  24. package/src/components/messages.ts +54 -0
  25. package/src/components/panel.ts +204 -0
  26. package/src/components/reasoning-bubble.ts +144 -0
  27. package/src/components/registry.ts +87 -0
  28. package/src/components/suggestions.ts +97 -0
  29. package/src/components/tool-bubble.ts +288 -0
  30. package/src/defaults.ts +321 -0
  31. package/src/index.ts +175 -0
  32. package/src/install.ts +284 -0
  33. package/src/plugins/registry.ts +77 -0
  34. package/src/plugins/types.ts +95 -0
  35. package/src/postprocessors.ts +194 -0
  36. package/src/runtime/init.ts +162 -0
  37. package/src/session.ts +376 -0
  38. package/src/styles/tailwind.css +20 -0
  39. package/src/styles/widget.css +1627 -0
  40. package/src/types.ts +1635 -0
  41. package/src/ui.ts +3341 -0
  42. package/src/utils/actions.ts +227 -0
  43. package/src/utils/attachment-manager.ts +384 -0
  44. package/src/utils/code-generators.test.ts +500 -0
  45. package/src/utils/code-generators.ts +1806 -0
  46. package/src/utils/component-middleware.ts +137 -0
  47. package/src/utils/component-parser.ts +119 -0
  48. package/src/utils/constants.ts +16 -0
  49. package/src/utils/content.ts +306 -0
  50. package/src/utils/dom.ts +25 -0
  51. package/src/utils/events.ts +41 -0
  52. package/src/utils/formatting.test.ts +166 -0
  53. package/src/utils/formatting.ts +470 -0
  54. package/src/utils/icons.ts +92 -0
  55. package/src/utils/message-id.ts +37 -0
  56. package/src/utils/morph.ts +36 -0
  57. package/src/utils/positioning.ts +17 -0
  58. package/src/utils/storage.ts +72 -0
  59. package/src/utils/theme.ts +105 -0
  60. package/src/widget.css +1 -0
  61. package/widget.css +1 -0
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createJsonStreamParser } from "./formatting";
3
+
4
+ describe("JSON Stream Parser", () => {
5
+ it("should extract text field incrementally as JSON streams in", () => {
6
+ // Simulate the actual stream chunks from the user's example
7
+ const chunks = [
8
+ '{\n',
9
+ ' ',
10
+ ' "',
11
+ 'action',
12
+ '":',
13
+ ' "',
14
+ 'message',
15
+ '",\n',
16
+ ' ',
17
+ ' "',
18
+ 'text',
19
+ '":',
20
+ ' "',
21
+ 'You\'re',
22
+ ' welcome',
23
+ '!',
24
+ ' Enjoy',
25
+ ' your',
26
+ ' browsing',
27
+ ',',
28
+ ' and',
29
+ ' I\'m',
30
+ ' here',
31
+ ' if',
32
+ ' you',
33
+ ' need',
34
+ ' anything',
35
+ '!"\n',
36
+ '}'
37
+ ];
38
+
39
+ const parser = createJsonStreamParser();
40
+ let accumulatedContent = "";
41
+ const extractedTexts: string[] = [];
42
+
43
+ // Process each chunk incrementally
44
+ for (const chunk of chunks) {
45
+ accumulatedContent += chunk;
46
+ const result = parser.processChunk(accumulatedContent);
47
+
48
+ // Extract text from result (can be string or object with text property)
49
+ const text = typeof result === 'string' ? result : result?.text ?? null;
50
+ if (text !== null) {
51
+ extractedTexts.push(text);
52
+ }
53
+
54
+ // Also check getExtractedText
55
+ const currentText = parser.getExtractedText();
56
+ if (currentText !== null && !extractedTexts.includes(currentText)) {
57
+ extractedTexts.push(currentText);
58
+ }
59
+ }
60
+
61
+ // Verify that we extracted text progressively
62
+ expect(extractedTexts.length).toBeGreaterThan(5); // Should have many incremental updates
63
+
64
+ // The final extracted text should be the complete text value
65
+ const finalText = parser.getExtractedText();
66
+ expect(finalText).toBe("You're welcome! Enjoy your browsing, and I'm here if you need anything!");
67
+
68
+ // Verify intermediate extractions show progressive text
69
+ // The text should start appearing once the "text" field value starts streaming
70
+ const hasPartialText = extractedTexts.some(text =>
71
+ text.includes("You're") || text.includes("welcome")
72
+ );
73
+ expect(hasPartialText).toBe(true);
74
+ });
75
+
76
+ it("should handle incomplete JSON gracefully", () => {
77
+ const chunks = [
78
+ '{\n',
79
+ ' "action": "message",\n',
80
+ ' "text": "',
81
+ 'Hello',
82
+ ' ',
83
+ 'world'
84
+ // Note: No closing quote or brace
85
+ ];
86
+
87
+ const parser = createJsonStreamParser();
88
+ let accumulated = "";
89
+
90
+ for (const chunk of chunks) {
91
+ accumulated += chunk;
92
+ parser.processChunk(accumulated);
93
+ }
94
+
95
+ // Should still extract partial text
96
+ const result = parser.getExtractedText();
97
+ expect(result).toBe("Hello world");
98
+ });
99
+
100
+ it("should handle complete JSON in one chunk", () => {
101
+ const completeJson = '{"action": "message", "text": "Hello world!"}';
102
+
103
+ const parser = createJsonStreamParser();
104
+ const result = parser.processChunk(completeJson);
105
+
106
+ // Extract text from result (can be string or object with text property)
107
+ const text = typeof result === 'string' ? result : result?.text ?? null;
108
+ expect(text).toBe("Hello world!");
109
+ expect(parser.getExtractedText()).toBe("Hello world!");
110
+ });
111
+
112
+ it("should handle the exact stream format from user example", () => {
113
+ // Extract just the text chunks from the SSE stream
114
+ const textChunks = [
115
+ '{\n',
116
+ ' ',
117
+ ' "',
118
+ 'action',
119
+ '":',
120
+ ' "',
121
+ 'message',
122
+ '",\n',
123
+ ' ',
124
+ ' "',
125
+ 'text',
126
+ '":',
127
+ ' "',
128
+ 'You\'re',
129
+ ' welcome',
130
+ '!',
131
+ ' Enjoy',
132
+ ' your',
133
+ ' browsing',
134
+ ',',
135
+ ' and',
136
+ ' I\'m',
137
+ ' here',
138
+ ' if',
139
+ ' you',
140
+ ' need',
141
+ ' anything',
142
+ '!"\n',
143
+ '}'
144
+ ];
145
+
146
+ const parser = createJsonStreamParser();
147
+ let accumulated = "";
148
+ const allExtractedTexts: (string | null)[] = [];
149
+
150
+ for (const chunk of textChunks) {
151
+ accumulated += chunk;
152
+ const result = parser.processChunk(accumulated);
153
+ // Extract text from result (can be string or object with text property)
154
+ const text = typeof result === 'string' ? result : result?.text ?? null;
155
+ allExtractedTexts.push(text);
156
+ }
157
+
158
+ // Should have many non-null results (incremental updates)
159
+ const nonNullResults = allExtractedTexts.filter(r => r !== null);
160
+ expect(nonNullResults.length).toBeGreaterThan(10);
161
+
162
+ // Final result should be the complete text
163
+ const finalResult = parser.getExtractedText();
164
+ expect(finalResult).toBe("You're welcome! Enjoy your browsing, and I'm here if you need anything!");
165
+ });
166
+ });
@@ -0,0 +1,470 @@
1
+ import { AgentWidgetReasoning, AgentWidgetToolCall, AgentWidgetStreamParser, AgentWidgetStreamParserResult } from "../types";
2
+ import { parse as parsePartialJson, STR, OBJ } from "partial-json";
3
+
4
+ /**
5
+ * Unescapes JSON string escape sequences that LLMs often double-escape.
6
+ * Converts literal \n, \r, \t sequences to actual control characters.
7
+ */
8
+ const unescapeJsonString = (str: string): string => {
9
+ return str
10
+ .replace(/\\n/g, '\n')
11
+ .replace(/\\r/g, '\r')
12
+ .replace(/\\t/g, '\t')
13
+ .replace(/\\"/g, '"')
14
+ .replace(/\\\\/g, '\\');
15
+ };
16
+
17
+ export const formatUnknownValue = (value: unknown): string => {
18
+ if (value === null) return "null";
19
+ if (value === undefined) return "";
20
+ if (typeof value === "string") return value;
21
+ if (typeof value === "number" || typeof value === "boolean") {
22
+ return String(value);
23
+ }
24
+ try {
25
+ return JSON.stringify(value, null, 2);
26
+ } catch (error) {
27
+ return String(value);
28
+ }
29
+ };
30
+
31
+ export const formatReasoningDuration = (reasoning: AgentWidgetReasoning) => {
32
+ const end = reasoning.completedAt ?? Date.now();
33
+ const start = reasoning.startedAt ?? end;
34
+ const durationMs =
35
+ reasoning.durationMs !== undefined
36
+ ? reasoning.durationMs
37
+ : Math.max(0, end - start);
38
+ const seconds = durationMs / 1000;
39
+ if (seconds < 0.1) {
40
+ return "Thought for <0.1 seconds";
41
+ }
42
+ const formatted =
43
+ seconds >= 10
44
+ ? Math.round(seconds).toString()
45
+ : seconds.toFixed(1).replace(/\.0$/, "");
46
+ return `Thought for ${formatted} seconds`;
47
+ };
48
+
49
+ export const describeReasonStatus = (reasoning: AgentWidgetReasoning) => {
50
+ if (reasoning.status === "complete") return formatReasoningDuration(reasoning);
51
+ if (reasoning.status === "pending") return "Waiting";
52
+ return "";
53
+ };
54
+
55
+ export const formatToolDuration = (tool: AgentWidgetToolCall) => {
56
+ const durationMs =
57
+ typeof tool.duration === "number"
58
+ ? tool.duration
59
+ : typeof tool.durationMs === "number"
60
+ ? tool.durationMs
61
+ : Math.max(
62
+ 0,
63
+ (tool.completedAt ?? Date.now()) -
64
+ (tool.startedAt ?? tool.completedAt ?? Date.now())
65
+ );
66
+ const seconds = durationMs / 1000;
67
+ if (seconds < 0.1) {
68
+ return "Used tool for <0.1 seconds";
69
+ }
70
+ const formatted =
71
+ seconds >= 10
72
+ ? Math.round(seconds).toString()
73
+ : seconds.toFixed(1).replace(/\.0$/, "");
74
+ return `Used tool for ${formatted} seconds`;
75
+ };
76
+
77
+ export const describeToolStatus = (status: AgentWidgetToolCall["status"]) => {
78
+ if (status === "complete") return "";
79
+ if (status === "pending") return "Starting";
80
+ return "Running";
81
+ };
82
+
83
+ export const describeToolTitle = (tool: AgentWidgetToolCall) => {
84
+ if (tool.status === "complete") {
85
+ return formatToolDuration(tool);
86
+ }
87
+ return "Using tool...";
88
+ };
89
+
90
+ /**
91
+ * Creates a regex-based parser for extracting text from JSON streams.
92
+ * This is a simpler alternative to schema-stream that uses regex to extract
93
+ * the 'text' field incrementally as JSON streams in.
94
+ *
95
+ * This can be used as an alternative parser option.
96
+ */
97
+ const createRegexJsonParserInternal = (): {
98
+ processChunk(accumulatedContent: string): Promise<AgentWidgetStreamParserResult | string | null>;
99
+ getExtractedText(): string | null;
100
+ close?(): Promise<void>;
101
+ } => {
102
+ let extractedText: string | null = null;
103
+ let processedLength = 0;
104
+
105
+ // Regex-based extraction for incremental JSON parsing
106
+ const extractTextFromIncompleteJson = (jsonString: string): string | null => {
107
+ // Look for "text": "value" pattern, handling incomplete strings
108
+ // Match: "text": " followed by any characters (including incomplete)
109
+ const textFieldRegex = /"text"\s*:\s*"((?:[^"\\]|\\.|")*?)"/;
110
+ const match = jsonString.match(textFieldRegex);
111
+
112
+ if (match && match[1]) {
113
+ // Unescape the string value
114
+ try {
115
+ // Replace escaped characters
116
+ let unescaped = match[1]
117
+ .replace(/\\n/g, '\n')
118
+ .replace(/\\r/g, '\r')
119
+ .replace(/\\t/g, '\t')
120
+ .replace(/\\"/g, '"')
121
+ .replace(/\\\\/g, '\\');
122
+ return unescaped;
123
+ } catch {
124
+ return match[1];
125
+ }
126
+ }
127
+
128
+ // Also try to match incomplete text field (text field that hasn't closed yet)
129
+ // Look for "text": " followed by content that may not be closed
130
+ const incompleteTextFieldRegex = /"text"\s*:\s*"((?:[^"\\]|\\.)*)/;
131
+ const incompleteMatch = jsonString.match(incompleteTextFieldRegex);
132
+
133
+ if (incompleteMatch && incompleteMatch[1]) {
134
+ // Unescape the partial string value
135
+ try {
136
+ let unescaped = incompleteMatch[1]
137
+ .replace(/\\n/g, '\n')
138
+ .replace(/\\r/g, '\r')
139
+ .replace(/\\t/g, '\t')
140
+ .replace(/\\"/g, '"')
141
+ .replace(/\\\\/g, '\\');
142
+ return unescaped;
143
+ } catch {
144
+ return incompleteMatch[1];
145
+ }
146
+ }
147
+
148
+ return null;
149
+ };
150
+
151
+ return {
152
+ getExtractedText: () => extractedText,
153
+ processChunk: async (accumulatedContent: string): Promise<AgentWidgetStreamParserResult | string | null> => {
154
+ // Skip if no new content
155
+ if (accumulatedContent.length <= processedLength) {
156
+ return extractedText !== null
157
+ ? { text: extractedText, raw: accumulatedContent }
158
+ : null;
159
+ }
160
+
161
+ // Validate that the accumulated content looks like valid JSON
162
+ const trimmed = accumulatedContent.trim();
163
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
164
+ return null;
165
+ }
166
+
167
+ // Try to extract text field using regex
168
+ const extracted = extractTextFromIncompleteJson(accumulatedContent);
169
+ if (extracted !== null) {
170
+ extractedText = extracted;
171
+ }
172
+
173
+ // Update processed length
174
+ processedLength = accumulatedContent.length;
175
+
176
+ // Return both the extracted text and raw JSON
177
+ if (extractedText !== null) {
178
+ return {
179
+ text: extractedText,
180
+ raw: accumulatedContent
181
+ };
182
+ }
183
+
184
+ return null;
185
+ },
186
+ close: async () => {
187
+ // No cleanup needed for regex-based parser
188
+ }
189
+ };
190
+ };
191
+
192
+ /**
193
+ * Extracts the text field from JSON (works with partial JSON during streaming).
194
+ * For complete JSON, uses fast path. For incomplete JSON, returns null (use stateful parser in client.ts).
195
+ *
196
+ * @param jsonString - The JSON string (can be partial/incomplete during streaming)
197
+ * @returns The extracted text value, or null if not found or invalid
198
+ */
199
+ export const extractTextFromJson = (jsonString: string): string | null => {
200
+ try {
201
+ // Try to parse complete JSON first (fast path)
202
+ const parsed = JSON.parse(jsonString);
203
+ if (parsed && typeof parsed === "object" && typeof parsed.text === "string") {
204
+ return parsed.text;
205
+ }
206
+ } catch {
207
+ // For incomplete JSON, return null - use stateful parser in client.ts
208
+ return null;
209
+ }
210
+ return null;
211
+ };
212
+
213
+ /**
214
+ * Plain text parser - passes through text as-is without any parsing.
215
+ * This is the default parser.
216
+ */
217
+ export const createPlainTextParser = (): AgentWidgetStreamParser => {
218
+ const parser: AgentWidgetStreamParser = {
219
+ processChunk: (accumulatedContent: string): string | null => {
220
+ // Always return null to indicate this isn't a structured format
221
+ // Content will be displayed as plain text
222
+ return null;
223
+ },
224
+ getExtractedText: (): string | null => {
225
+ return null;
226
+ }
227
+ };
228
+ // Mark this as a plain text parser
229
+ (parser as any).__isPlainTextParser = true;
230
+ return parser;
231
+ };
232
+
233
+ /**
234
+ * JSON parser using regex-based extraction.
235
+ * Extracts the 'text' field from JSON responses using regex patterns.
236
+ * This is a simpler regex-based alternative to createJsonStreamParser.
237
+ * Less robust for complex/malformed JSON but has no external dependencies.
238
+ */
239
+ export const createRegexJsonParser = (): AgentWidgetStreamParser => {
240
+ const regexParser = createRegexJsonParserInternal();
241
+
242
+ return {
243
+ processChunk: async (accumulatedContent: string): Promise<AgentWidgetStreamParserResult | string | null> => {
244
+ // Only process if it looks like JSON
245
+ const trimmed = accumulatedContent.trim();
246
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
247
+ return null;
248
+ }
249
+ return regexParser.processChunk(accumulatedContent);
250
+ },
251
+ getExtractedText: regexParser.getExtractedText.bind(regexParser),
252
+ close: regexParser.close?.bind(regexParser)
253
+ };
254
+ };
255
+
256
+ /**
257
+ * JSON stream parser using partial-json library.
258
+ * Extracts the 'text' field from JSON responses using the partial-json library,
259
+ * which is specifically designed for parsing incomplete JSON from LLMs.
260
+ * This is the recommended parser as it's more robust than regex.
261
+ *
262
+ * Library: https://github.com/promplate/partial-json-parser-js
263
+ */
264
+ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
265
+ let extractedText: string | null = null;
266
+ let processedLength = 0;
267
+
268
+ return {
269
+ getExtractedText: () => extractedText,
270
+ processChunk: (accumulatedContent: string): AgentWidgetStreamParserResult | string | null => {
271
+ // Validate that the accumulated content looks like JSON
272
+ const trimmed = accumulatedContent.trim();
273
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
274
+ return null;
275
+ }
276
+
277
+ // Skip if no new content
278
+ if (accumulatedContent.length <= processedLength) {
279
+ return extractedText !== null || extractedText === ""
280
+ ? { text: extractedText || "", raw: accumulatedContent }
281
+ : null;
282
+ }
283
+
284
+ try {
285
+ // Parse partial JSON - allow partial strings and objects
286
+ // STR | OBJ allows incomplete strings and objects during streaming
287
+ const parsed = parsePartialJson(accumulatedContent, STR | OBJ);
288
+
289
+ if (parsed && typeof parsed === "object") {
290
+ // Check for component directives - extract text if present for combined text+component
291
+ if (parsed.component && typeof parsed.component === "string") {
292
+ // For component directives, extract text if present, otherwise empty
293
+ extractedText = typeof parsed.text === "string" ? unescapeJsonString(parsed.text) : "";
294
+ }
295
+ // Check for form directives - these also don't have text fields
296
+ else if (parsed.type === "init" && parsed.form) {
297
+ // For form directives, return empty - they're handled by form postprocessor
298
+ extractedText = "";
299
+ }
300
+ // Extract text field if available
301
+ else if (typeof parsed.text === "string") {
302
+ extractedText = unescapeJsonString(parsed.text);
303
+ }
304
+ }
305
+ } catch (error) {
306
+ // If parsing fails completely, keep the last extracted text
307
+ // This can happen with very malformed JSON
308
+ }
309
+
310
+ // Update processed length
311
+ processedLength = accumulatedContent.length;
312
+
313
+ // Always return raw JSON for component/form directive detection
314
+ // Return empty string for text if it's a component/form directive
315
+ if (extractedText !== null) {
316
+ return {
317
+ text: extractedText,
318
+ raw: accumulatedContent
319
+ };
320
+ }
321
+
322
+ return null;
323
+ },
324
+ close: () => {
325
+ // No cleanup needed
326
+ }
327
+ };
328
+ };
329
+
330
+ /**
331
+ * Flexible JSON stream parser that can extract text from various field names.
332
+ * This parser looks for display text in multiple possible fields, making it
333
+ * compatible with different JSON response formats.
334
+ *
335
+ * @param textExtractor Optional function to extract display text from parsed JSON.
336
+ * If not provided, looks for common text fields.
337
+ */
338
+ export const createFlexibleJsonStreamParser = (
339
+ textExtractor?: (parsed: any) => string | null
340
+ ): AgentWidgetStreamParser => {
341
+ let extractedText: string | null = null;
342
+ let processedLength = 0;
343
+
344
+ // Default text extractor that handles common patterns
345
+ const defaultExtractor = (parsed: any): string | null => {
346
+ if (!parsed || typeof parsed !== "object") return null;
347
+
348
+ // Helper to safely extract and unescape text
349
+ const getText = (value: any): string | null => {
350
+ return typeof value === "string" ? unescapeJsonString(value) : null;
351
+ };
352
+
353
+ // Check for component directives - extract text if present for combined text+component
354
+ if (parsed.component && typeof parsed.component === "string") {
355
+ // For component directives, extract text if present, otherwise empty
356
+ return typeof parsed.text === "string" ? unescapeJsonString(parsed.text) : "";
357
+ }
358
+
359
+ // Check for form directives - these also don't have text fields
360
+ if (parsed.type === "init" && parsed.form) {
361
+ // For form directives, return empty - they're handled by form postprocessor
362
+ return "";
363
+ }
364
+
365
+ // Check for action-based text fields
366
+ if (parsed.action) {
367
+ switch (parsed.action) {
368
+ case 'nav_then_click':
369
+ return getText(parsed.on_load_text) || getText(parsed.text) || null;
370
+ case 'message':
371
+ case 'message_and_click':
372
+ case 'checkout':
373
+ return getText(parsed.text) || null;
374
+ default:
375
+ return getText(parsed.text) || getText(parsed.display_text) || getText(parsed.message) || null;
376
+ }
377
+ }
378
+
379
+ // Fallback to common text field names
380
+ return getText(parsed.text) || getText(parsed.display_text) || getText(parsed.message) || getText(parsed.content) || null;
381
+ };
382
+
383
+ const extractText = textExtractor || defaultExtractor;
384
+
385
+ return {
386
+ getExtractedText: () => extractedText,
387
+ processChunk: (accumulatedContent: string): AgentWidgetStreamParserResult | string | null => {
388
+ // Validate that the accumulated content looks like JSON
389
+ const trimmed = accumulatedContent.trim();
390
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
391
+ return null;
392
+ }
393
+
394
+ // Skip if no new content
395
+ if (accumulatedContent.length <= processedLength) {
396
+ return extractedText !== null
397
+ ? { text: extractedText, raw: accumulatedContent }
398
+ : null;
399
+ }
400
+
401
+ try {
402
+ // Parse partial JSON - allow partial strings and objects
403
+ // STR | OBJ allows incomplete strings and objects during streaming
404
+ const parsed = parsePartialJson(accumulatedContent, STR | OBJ);
405
+
406
+ // Extract text using the provided or default extractor
407
+ const newText = extractText(parsed);
408
+ if (newText !== null) {
409
+ extractedText = newText;
410
+ }
411
+ } catch (error) {
412
+ // If parsing fails completely, keep the last extracted text
413
+ // This can happen with very malformed JSON
414
+ }
415
+
416
+ // Update processed length
417
+ processedLength = accumulatedContent.length;
418
+
419
+ // Always return the raw JSON for action parsing and component detection
420
+ // Text may be null or empty for component/form directives, that's ok
421
+ return {
422
+ text: extractedText || "",
423
+ raw: accumulatedContent
424
+ };
425
+ },
426
+ close: () => {
427
+ // No cleanup needed
428
+ }
429
+ };
430
+ };
431
+
432
+ /**
433
+ * XML stream parser.
434
+ * Extracts text from <text>...</text> tags in XML responses.
435
+ */
436
+ export const createXmlParser = (): AgentWidgetStreamParser => {
437
+ let extractedText: string | null = null;
438
+
439
+ return {
440
+ processChunk: (accumulatedContent: string): AgentWidgetStreamParserResult | string | null => {
441
+ // Return null if not XML format
442
+ const trimmed = accumulatedContent.trim();
443
+ if (!trimmed.startsWith('<')) {
444
+ return null;
445
+ }
446
+
447
+ // Extract text from <text>...</text> tags
448
+ // Handle both <text>content</text> and <text attr="value">content</text>
449
+ const match = accumulatedContent.match(/<text[^>]*>([\s\S]*?)<\/text>/);
450
+ if (match && match[1]) {
451
+ extractedText = match[1];
452
+ // For XML, we typically don't need the raw content for middleware
453
+ // but we can include it for consistency
454
+ return { text: extractedText, raw: accumulatedContent };
455
+ }
456
+
457
+ return null;
458
+ },
459
+ getExtractedText: (): string | null => {
460
+ return extractedText;
461
+ }
462
+ };
463
+ };
464
+
465
+
466
+
467
+
468
+
469
+
470
+