vanilla-agent 1.6.0 → 1.7.1
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/dist/index.cjs +7 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.global.js +29 -29
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -1
- package/package.json +8 -3
- package/src/client.test.ts +1 -1
- package/src/client.ts +18 -9
- package/src/index.ts +1 -0
- package/src/utils/formatting.test.ts +10 -4
- package/src/utils/formatting.ts +85 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Themeable, plugable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -30,13 +30,15 @@
|
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^20.12.7",
|
|
33
|
+
"@vitest/ui": "^4.0.9",
|
|
33
34
|
"eslint": "^8.57.0",
|
|
34
35
|
"eslint-config-prettier": "^9.1.0",
|
|
35
36
|
"postcss": "^8.4.38",
|
|
36
37
|
"rimraf": "^5.0.5",
|
|
37
38
|
"tailwindcss": "^3.4.10",
|
|
38
39
|
"tsup": "^8.0.1",
|
|
39
|
-
"typescript": "^5.4.5"
|
|
40
|
+
"typescript": "^5.4.5",
|
|
41
|
+
"vitest": "^4.0.9"
|
|
40
42
|
},
|
|
41
43
|
"engines": {
|
|
42
44
|
"node": ">=18.17.0"
|
|
@@ -58,6 +60,9 @@
|
|
|
58
60
|
"build:client": "tsup src/index.ts --format esm,cjs,iife --global-name AgentWidget --minify --sourcemap --splitting false --dts --loader \".css=text\"",
|
|
59
61
|
"build:installer": "tsup src/install.ts --format iife --global-name SiteAgentInstaller --out-dir dist --minify --sourcemap --no-splitting",
|
|
60
62
|
"lint": "eslint . --ext .ts",
|
|
61
|
-
"typecheck": "tsc --noEmit"
|
|
63
|
+
"typecheck": "tsc --noEmit",
|
|
64
|
+
"test": "vitest",
|
|
65
|
+
"test:ui": "vitest --ui",
|
|
66
|
+
"test:run": "vitest run"
|
|
62
67
|
}
|
|
63
68
|
}
|
package/src/client.test.ts
CHANGED
|
@@ -22,7 +22,7 @@ describe('AgentWidgetClient - JSON Streaming', () => {
|
|
|
22
22
|
'',
|
|
23
23
|
'data: {"type":"step_start","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","stepType":"prompt","index":1,"totalSteps":1,"startedAt":"2025-11-12T23:47:39.565Z"}',
|
|
24
24
|
'',
|
|
25
|
-
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"{
|
|
25
|
+
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":"{\\n"}',
|
|
26
26
|
'',
|
|
27
27
|
'data: {"type":"step_chunk","id":"step_01k9x5db72fzwvmdenryn0qm48","name":"Prompt 1","executionType":"prompt","index":2,"text":" "}',
|
|
28
28
|
'',
|
package/src/client.ts
CHANGED
|
@@ -640,6 +640,7 @@ export class AgentWidgetClient {
|
|
|
640
640
|
// Accumulate raw content for structured format parsing
|
|
641
641
|
const rawBuffer = rawContentBuffers.get(assistant.id) ?? "";
|
|
642
642
|
const accumulatedRaw = rawBuffer + chunk;
|
|
643
|
+
// Store raw content for action parsing, but NEVER set assistant.content to raw JSON
|
|
643
644
|
assistant.rawContent = accumulatedRaw;
|
|
644
645
|
|
|
645
646
|
// Use stream parser to parse
|
|
@@ -699,6 +700,7 @@ export class AgentWidgetClient {
|
|
|
699
700
|
}
|
|
700
701
|
}
|
|
701
702
|
// Otherwise wait for more chunks (incomplete structured format)
|
|
703
|
+
// Don't emit message if parser hasn't extracted text yet
|
|
702
704
|
}).catch(() => {
|
|
703
705
|
// On error, treat as plain text
|
|
704
706
|
assistant.content += chunk;
|
|
@@ -727,14 +729,12 @@ export class AgentWidgetClient {
|
|
|
727
729
|
emitMessage(assistant);
|
|
728
730
|
}
|
|
729
731
|
// Otherwise wait for more chunks (incomplete structured format)
|
|
732
|
+
// Don't emit message if parser hasn't extracted text yet
|
|
730
733
|
}
|
|
731
734
|
|
|
732
|
-
//
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
assistant.content = currentText;
|
|
736
|
-
emitMessage(assistant);
|
|
737
|
-
}
|
|
735
|
+
// IMPORTANT: Don't call getExtractedText() and emit messages here
|
|
736
|
+
// This was causing raw JSON to be displayed because getExtractedText()
|
|
737
|
+
// wasn't extracting the "text" field correctly during streaming
|
|
738
738
|
}
|
|
739
739
|
if (payload.isComplete) {
|
|
740
740
|
const finalContent = payload.result?.response ?? assistant.content;
|
|
@@ -820,15 +820,18 @@ export class AgentWidgetClient {
|
|
|
820
820
|
if (parser) {
|
|
821
821
|
// First check if parser already extracted text during streaming
|
|
822
822
|
const currentExtractedText = parser.getExtractedText();
|
|
823
|
+
const rawBuffer = rawContentBuffers.get(assistant.id);
|
|
824
|
+
const contentToProcess = rawBuffer ?? ensureStringContent(finalContent);
|
|
825
|
+
|
|
826
|
+
// Always set rawContent so action parsers can access the raw JSON
|
|
827
|
+
assistant.rawContent = contentToProcess;
|
|
828
|
+
|
|
823
829
|
if (currentExtractedText !== null && currentExtractedText.trim() !== "") {
|
|
824
830
|
// We already have extracted text from streaming - use it
|
|
825
831
|
assistant.content = currentExtractedText;
|
|
826
832
|
hasExtractedText = true;
|
|
827
833
|
} else {
|
|
828
834
|
// No extracted text yet - try to extract from final content
|
|
829
|
-
const rawBuffer = rawContentBuffers.get(assistant.id);
|
|
830
|
-
const contentToProcess = rawBuffer ?? ensureStringContent(finalContent);
|
|
831
|
-
assistant.rawContent = contentToProcess;
|
|
832
835
|
|
|
833
836
|
// Try fast path first
|
|
834
837
|
const extractedText = extractTextFromJson(contentToProcess);
|
|
@@ -887,6 +890,12 @@ export class AgentWidgetClient {
|
|
|
887
890
|
}
|
|
888
891
|
}
|
|
889
892
|
|
|
893
|
+
// Ensure rawContent is set even if there's no parser (for action parsing)
|
|
894
|
+
if (!assistant.rawContent) {
|
|
895
|
+
const rawBuffer = rawContentBuffers.get(assistant.id);
|
|
896
|
+
assistant.rawContent = rawBuffer ?? ensureStringContent(finalContent);
|
|
897
|
+
}
|
|
898
|
+
|
|
890
899
|
// Only show raw content if we never extracted any text and no buffer was used
|
|
891
900
|
if (!hasExtractedText && !rawContentBuffers.has(assistant.id)) {
|
|
892
901
|
// No extracted text and no streaming happened - show raw content
|
package/src/index.ts
CHANGED
|
@@ -45,8 +45,10 @@ describe("JSON Stream Parser", () => {
|
|
|
45
45
|
accumulatedContent += chunk;
|
|
46
46
|
const result = parser.processChunk(accumulatedContent);
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
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);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
// Also check getExtractedText
|
|
@@ -101,7 +103,9 @@ describe("JSON Stream Parser", () => {
|
|
|
101
103
|
const parser = createJsonStreamParser();
|
|
102
104
|
const result = parser.processChunk(completeJson);
|
|
103
105
|
|
|
104
|
-
|
|
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!");
|
|
105
109
|
expect(parser.getExtractedText()).toBe("Hello world!");
|
|
106
110
|
});
|
|
107
111
|
|
|
@@ -146,7 +150,9 @@ describe("JSON Stream Parser", () => {
|
|
|
146
150
|
for (const chunk of textChunks) {
|
|
147
151
|
accumulated += chunk;
|
|
148
152
|
const result = parser.processChunk(accumulated);
|
|
149
|
-
|
|
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);
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
// Should have many non-null results (incremental updates)
|
package/src/utils/formatting.ts
CHANGED
|
@@ -301,6 +301,91 @@ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
|
|
|
301
301
|
};
|
|
302
302
|
};
|
|
303
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Flexible JSON stream parser that can extract text from various field names.
|
|
306
|
+
* This parser looks for display text in multiple possible fields, making it
|
|
307
|
+
* compatible with different JSON response formats.
|
|
308
|
+
*
|
|
309
|
+
* @param textExtractor Optional function to extract display text from parsed JSON.
|
|
310
|
+
* If not provided, looks for common text fields.
|
|
311
|
+
*/
|
|
312
|
+
export const createFlexibleJsonStreamParser = (
|
|
313
|
+
textExtractor?: (parsed: any) => string | null
|
|
314
|
+
): AgentWidgetStreamParser => {
|
|
315
|
+
let extractedText: string | null = null;
|
|
316
|
+
let processedLength = 0;
|
|
317
|
+
|
|
318
|
+
// Default text extractor that handles common patterns
|
|
319
|
+
const defaultExtractor = (parsed: any): string | null => {
|
|
320
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
321
|
+
|
|
322
|
+
// Check for action-based text fields
|
|
323
|
+
if (parsed.action) {
|
|
324
|
+
switch (parsed.action) {
|
|
325
|
+
case 'nav_then_click':
|
|
326
|
+
return parsed.on_load_text || parsed.text || null;
|
|
327
|
+
case 'message':
|
|
328
|
+
case 'message_and_click':
|
|
329
|
+
case 'checkout':
|
|
330
|
+
return parsed.text || null;
|
|
331
|
+
default:
|
|
332
|
+
return parsed.text || parsed.display_text || parsed.message || null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Fallback to common text field names
|
|
337
|
+
return parsed.text || parsed.display_text || parsed.message || parsed.content || null;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const extractText = textExtractor || defaultExtractor;
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
getExtractedText: () => extractedText,
|
|
344
|
+
processChunk: (accumulatedContent: string): AgentWidgetStreamParserResult | string | null => {
|
|
345
|
+
// Validate that the accumulated content looks like JSON
|
|
346
|
+
const trimmed = accumulatedContent.trim();
|
|
347
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Skip if no new content
|
|
352
|
+
if (accumulatedContent.length <= processedLength) {
|
|
353
|
+
return extractedText !== null
|
|
354
|
+
? { text: extractedText, raw: accumulatedContent }
|
|
355
|
+
: null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
// Parse partial JSON - allow partial strings and objects
|
|
360
|
+
// STR | OBJ allows incomplete strings and objects during streaming
|
|
361
|
+
const parsed = parsePartialJson(accumulatedContent, STR | OBJ);
|
|
362
|
+
|
|
363
|
+
// Extract text using the provided or default extractor
|
|
364
|
+
const newText = extractText(parsed);
|
|
365
|
+
if (newText !== null) {
|
|
366
|
+
extractedText = newText;
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
// If parsing fails completely, keep the last extracted text
|
|
370
|
+
// This can happen with very malformed JSON
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Update processed length
|
|
374
|
+
processedLength = accumulatedContent.length;
|
|
375
|
+
|
|
376
|
+
// Always return the raw JSON for action parsing
|
|
377
|
+
// Text may be null during early streaming, that's ok
|
|
378
|
+
return {
|
|
379
|
+
text: extractedText || "",
|
|
380
|
+
raw: accumulatedContent
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
close: () => {
|
|
384
|
+
// No cleanup needed
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
|
|
304
389
|
/**
|
|
305
390
|
* XML stream parser.
|
|
306
391
|
* Extracts text from <text>...</text> tags in XML responses.
|