vanilla-agent 1.8.0 → 1.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-agent",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
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",
package/src/client.ts CHANGED
@@ -5,7 +5,11 @@ import {
5
5
  AgentWidgetStreamParser,
6
6
  AgentWidgetContextProvider,
7
7
  AgentWidgetRequestMiddleware,
8
- AgentWidgetRequestPayload
8
+ AgentWidgetRequestPayload,
9
+ AgentWidgetCustomFetch,
10
+ AgentWidgetSSEEventParser,
11
+ AgentWidgetHeadersFunction,
12
+ AgentWidgetSSEEventResult
9
13
  } from "./types";
10
14
  import {
11
15
  extractTextFromJson,
@@ -48,6 +52,9 @@ export class AgentWidgetClient {
48
52
  private readonly createStreamParser: () => AgentWidgetStreamParser;
49
53
  private readonly contextProviders: AgentWidgetContextProvider[];
50
54
  private readonly requestMiddleware?: AgentWidgetRequestMiddleware;
55
+ private readonly customFetch?: AgentWidgetCustomFetch;
56
+ private readonly parseSSEEvent?: AgentWidgetSSEEventParser;
57
+ private readonly getHeaders?: AgentWidgetHeadersFunction;
51
58
 
52
59
  constructor(private config: AgentWidgetConfig = {}) {
53
60
  this.apiUrl = config.apiUrl ?? DEFAULT_ENDPOINT;
@@ -60,6 +67,9 @@ export class AgentWidgetClient {
60
67
  this.createStreamParser = config.streamParser ?? getParserFromType(config.parserType);
61
68
  this.contextProviders = config.contextProviders ?? [];
62
69
  this.requestMiddleware = config.requestMiddleware;
70
+ this.customFetch = config.customFetch;
71
+ this.parseSSEEvent = config.parseSSEEvent;
72
+ this.getHeaders = config.getHeaders;
63
73
  }
64
74
 
65
75
  public async dispatch(options: DispatchOptions, onEvent: SSEHandler) {
@@ -70,19 +80,54 @@ export class AgentWidgetClient {
70
80
 
71
81
  onEvent({ type: "status", status: "connecting" });
72
82
 
73
- const body = await this.buildPayload(options.messages);
83
+ const payload = await this.buildPayload(options.messages);
74
84
 
75
85
  if (this.debug) {
76
86
  // eslint-disable-next-line no-console
77
- console.debug("[AgentWidgetClient] dispatch body", body);
87
+ console.debug("[AgentWidgetClient] dispatch payload", payload);
78
88
  }
79
89
 
80
- const response = await fetch(this.apiUrl, {
81
- method: "POST",
82
- headers: this.headers,
83
- body: JSON.stringify(body),
84
- signal: controller.signal
85
- });
90
+ // Build headers - merge static headers with dynamic headers if provided
91
+ let headers = { ...this.headers };
92
+ if (this.getHeaders) {
93
+ try {
94
+ const dynamicHeaders = await this.getHeaders();
95
+ headers = { ...headers, ...dynamicHeaders };
96
+ } catch (error) {
97
+ if (typeof console !== "undefined") {
98
+ // eslint-disable-next-line no-console
99
+ console.error("[AgentWidget] getHeaders error:", error);
100
+ }
101
+ }
102
+ }
103
+
104
+ // Use customFetch if provided, otherwise use default fetch
105
+ let response: Response;
106
+ if (this.customFetch) {
107
+ try {
108
+ response = await this.customFetch(
109
+ this.apiUrl,
110
+ {
111
+ method: "POST",
112
+ headers,
113
+ body: JSON.stringify(payload),
114
+ signal: controller.signal
115
+ },
116
+ payload
117
+ );
118
+ } catch (error) {
119
+ const err = error instanceof Error ? error : new Error(String(error));
120
+ onEvent({ type: "error", error: err });
121
+ throw err;
122
+ }
123
+ } else {
124
+ response = await fetch(this.apiUrl, {
125
+ method: "POST",
126
+ headers,
127
+ body: JSON.stringify(payload),
128
+ signal: controller.signal
129
+ });
130
+ }
86
131
 
87
132
  if (!response.ok || !response.body) {
88
133
  const error = new Error(
@@ -112,7 +157,7 @@ export class AgentWidgetClient {
112
157
  })
113
158
  .map((message) => ({
114
159
  role: message.role,
115
- content: message.content,
160
+ content: message.rawContent || message.content,
116
161
  createdAt: message.createdAt
117
162
  }));
118
163
 
@@ -167,6 +212,70 @@ export class AgentWidgetClient {
167
212
  return payload;
168
213
  }
169
214
 
215
+ /**
216
+ * Handle custom SSE event parsing via parseSSEEvent callback
217
+ * Returns true if event was handled, false otherwise
218
+ */
219
+ private async handleCustomSSEEvent(
220
+ payload: unknown,
221
+ onEvent: SSEHandler,
222
+ assistantMessageRef: { current: AgentWidgetMessage | null },
223
+ emitMessage: (msg: AgentWidgetMessage) => void,
224
+ nextSequence: () => number
225
+ ): Promise<boolean> {
226
+ if (!this.parseSSEEvent) return false;
227
+
228
+ try {
229
+ const result = await this.parseSSEEvent(payload);
230
+ if (result === null) return false; // Event should be ignored
231
+
232
+ const ensureAssistant = () => {
233
+ if (assistantMessageRef.current) return assistantMessageRef.current;
234
+ const msg: AgentWidgetMessage = {
235
+ id: `assistant-${Date.now()}-${Math.random().toString(16).slice(2)}`,
236
+ role: "assistant",
237
+ content: "",
238
+ createdAt: new Date().toISOString(),
239
+ streaming: true,
240
+ variant: "assistant",
241
+ sequence: nextSequence()
242
+ };
243
+ assistantMessageRef.current = msg;
244
+ emitMessage(msg);
245
+ return msg;
246
+ };
247
+
248
+ if (result.text !== undefined) {
249
+ const assistant = ensureAssistant();
250
+ assistant.content += result.text;
251
+ emitMessage(assistant);
252
+ }
253
+
254
+ if (result.done) {
255
+ if (assistantMessageRef.current) {
256
+ assistantMessageRef.current.streaming = false;
257
+ emitMessage(assistantMessageRef.current);
258
+ }
259
+ onEvent({ type: "status", status: "idle" });
260
+ }
261
+
262
+ if (result.error) {
263
+ onEvent({
264
+ type: "error",
265
+ error: new Error(result.error)
266
+ });
267
+ }
268
+
269
+ return true; // Event was handled
270
+ } catch (error) {
271
+ if (typeof console !== "undefined") {
272
+ // eslint-disable-next-line no-console
273
+ console.error("[AgentWidget] parseSSEEvent error:", error);
274
+ }
275
+ return false;
276
+ }
277
+ }
278
+
170
279
  private async streamResponse(
171
280
  body: ReadableStream<Uint8Array>,
172
281
  onEvent: SSEHandler
@@ -215,6 +324,8 @@ export class AgentWidgetClient {
215
324
  };
216
325
 
217
326
  let assistantMessage: AgentWidgetMessage | null = null;
327
+ // Reference to track assistant message for custom event handler
328
+ const assistantMessageRef = { current: null as AgentWidgetMessage | null };
218
329
  const reasoningMessages = new Map<string, AgentWidgetMessage>();
219
330
  const toolMessages = new Map<string, AgentWidgetMessage>();
220
331
  const reasoningContext = {
@@ -265,7 +376,6 @@ export class AgentWidgetClient {
265
376
  content: "",
266
377
  createdAt: new Date().toISOString(),
267
378
  streaming: true,
268
- variant: "assistant",
269
379
  sequence: nextSequence()
270
380
  };
271
381
  emitMessage(assistantMessage);
@@ -471,6 +581,24 @@ export class AgentWidgetClient {
471
581
  const payloadType =
472
582
  eventType !== "message" ? eventType : payload.type ?? "message";
473
583
 
584
+ // If custom SSE event parser is provided, try it first
585
+ if (this.parseSSEEvent) {
586
+ // Keep assistant message ref in sync
587
+ assistantMessageRef.current = assistantMessage;
588
+ const handled = await this.handleCustomSSEEvent(
589
+ payload,
590
+ onEvent,
591
+ assistantMessageRef,
592
+ emitMessage,
593
+ nextSequence
594
+ );
595
+ // Update assistantMessage from ref (in case it was created)
596
+ if (assistantMessageRef.current && !assistantMessage) {
597
+ assistantMessage = assistantMessageRef.current;
598
+ }
599
+ if (handled) continue; // Skip default handling if custom handler processed it
600
+ }
601
+
474
602
  if (payloadType === "reason_start") {
475
603
  const reasoningId =
476
604
  resolveReasoningId(payload, true) ?? `reason-${nextSequence()}`;
@@ -635,7 +763,8 @@ export class AgentWidgetClient {
635
763
  continue;
636
764
  }
637
765
  const assistant = ensureAssistantMessage();
638
- const chunk = payload.text ?? payload.delta ?? payload.content ?? "";
766
+ // Support various field names: text, delta, content, chunk (Travrse uses 'chunk')
767
+ const chunk = payload.text ?? payload.delta ?? payload.content ?? payload.chunk ?? "";
639
768
  if (chunk) {
640
769
  // Accumulate raw content for structured format parsing
641
770
  const rawBuffer = rawContentBuffers.get(assistant.id) ?? "";
@@ -167,3 +167,4 @@ export const enhanceWithForms = (
167
167
 
168
168
 
169
169
 
170
+
@@ -67,6 +67,8 @@ export interface PanelElements {
67
67
  clearChatButton: HTMLButtonElement | null;
68
68
  clearChatButtonWrapper: HTMLElement | null;
69
69
  iconHolder: HTMLElement;
70
+ headerTitle: HTMLElement;
71
+ headerSubtitle: HTMLElement;
70
72
  }
71
73
 
72
74
  export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelElements => {
@@ -789,7 +791,9 @@ export const buildPanel = (config?: AgentWidgetConfig, showClose = true): PanelE
789
791
  closeButtonWrapper,
790
792
  clearChatButton,
791
793
  clearChatButtonWrapper,
792
- iconHolder
794
+ iconHolder,
795
+ headerTitle: title,
796
+ headerSubtitle: subtitle
793
797
  };
794
798
  };
795
799
 
@@ -0,0 +1,87 @@
1
+ import { AgentWidgetConfig, AgentWidgetMessage } from "../types";
2
+
3
+ /**
4
+ * Context provided to component renderers
5
+ */
6
+ export interface ComponentContext {
7
+ message: AgentWidgetMessage;
8
+ config: AgentWidgetConfig;
9
+ /**
10
+ * Update component props during streaming
11
+ */
12
+ updateProps: (newProps: Record<string, unknown>) => void;
13
+ }
14
+
15
+ /**
16
+ * Component renderer function signature
17
+ */
18
+ export type ComponentRenderer = (
19
+ props: Record<string, unknown>,
20
+ context: ComponentContext
21
+ ) => HTMLElement;
22
+
23
+ /**
24
+ * Component registry for managing custom components
25
+ */
26
+ class ComponentRegistry {
27
+ private components: Map<string, ComponentRenderer> = new Map();
28
+
29
+ /**
30
+ * Register a custom component
31
+ */
32
+ register(name: string, renderer: ComponentRenderer): void {
33
+ if (this.components.has(name)) {
34
+ console.warn(`[ComponentRegistry] Component "${name}" is already registered. Overwriting.`);
35
+ }
36
+ this.components.set(name, renderer);
37
+ }
38
+
39
+ /**
40
+ * Unregister a component
41
+ */
42
+ unregister(name: string): void {
43
+ this.components.delete(name);
44
+ }
45
+
46
+ /**
47
+ * Get a component renderer by name
48
+ */
49
+ get(name: string): ComponentRenderer | undefined {
50
+ return this.components.get(name);
51
+ }
52
+
53
+ /**
54
+ * Check if a component is registered
55
+ */
56
+ has(name: string): boolean {
57
+ return this.components.has(name);
58
+ }
59
+
60
+ /**
61
+ * Get all registered component names
62
+ */
63
+ getAllNames(): string[] {
64
+ return Array.from(this.components.keys());
65
+ }
66
+
67
+ /**
68
+ * Clear all registered components
69
+ */
70
+ clear(): void {
71
+ this.components.clear();
72
+ }
73
+
74
+ /**
75
+ * Register multiple components at once
76
+ */
77
+ registerAll(components: Record<string, ComponentRenderer>): void {
78
+ Object.entries(components).forEach(([name, renderer]) => {
79
+ this.register(name, renderer);
80
+ });
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Global component registry instance
86
+ */
87
+ export const componentRegistry = new ComponentRegistry();
package/src/index.ts CHANGED
@@ -12,7 +12,12 @@ export type {
12
12
  AgentWidgetLauncherConfig,
13
13
  AgentWidgetEvent,
14
14
  AgentWidgetStreamParser,
15
- AgentWidgetStreamParserResult
15
+ AgentWidgetStreamParserResult,
16
+ AgentWidgetRequestPayload,
17
+ AgentWidgetCustomFetch,
18
+ AgentWidgetSSEEventParser,
19
+ AgentWidgetSSEEventResult,
20
+ AgentWidgetHeadersFunction
16
21
  } from "./types";
17
22
 
18
23
  export { initAgentWidgetFn as initAgentWidget };
@@ -36,7 +41,7 @@ export {
36
41
  escapeHtml,
37
42
  directivePostprocessor
38
43
  } from "./postprocessors";
39
- export {
44
+ export {
40
45
  createPlainTextParser,
41
46
  createJsonStreamParser,
42
47
  createFlexibleJsonStreamParser,
@@ -49,6 +54,21 @@ export type { AgentWidgetInitHandle };
49
54
  export type { AgentWidgetPlugin } from "./plugins/types";
50
55
  export { pluginRegistry } from "./plugins/registry";
51
56
 
57
+ // Component system exports
58
+ export { componentRegistry } from "./components/registry";
59
+ export type { ComponentRenderer, ComponentContext } from "./components/registry";
60
+ export {
61
+ createComponentStreamParser,
62
+ isComponentDirectiveType
63
+ } from "./utils/component-parser";
64
+ export type { ComponentDirective } from "./utils/component-parser";
65
+ export {
66
+ renderComponentDirective,
67
+ createComponentMiddleware,
68
+ hasComponentDirective,
69
+ extractComponentDirectiveFromMessage
70
+ } from "./utils/component-middleware";
71
+
52
72
  // Default configuration exports
53
73
  export { DEFAULT_WIDGET_CONFIG, mergeWithDefaults } from "./defaults";
54
74
 
@@ -74,3 +74,4 @@ export const pluginRegistry = new PluginRegistry();
74
74
 
75
75
 
76
76
 
77
+
@@ -92,3 +92,4 @@ export interface AgentWidgetPlugin {
92
92
 
93
93
 
94
94
 
95
+
package/src/types.ts CHANGED
@@ -339,10 +339,72 @@ export interface AgentWidgetStreamParser {
339
339
  }
340
340
 
341
341
 
342
+ /**
343
+ * Component renderer function signature for custom components
344
+ */
345
+ export type AgentWidgetComponentRenderer = (
346
+ props: Record<string, unknown>,
347
+ context: {
348
+ message: AgentWidgetMessage;
349
+ config: AgentWidgetConfig;
350
+ updateProps: (newProps: Record<string, unknown>) => void;
351
+ }
352
+ ) => HTMLElement;
353
+
354
+ /**
355
+ * Result from custom SSE event parser
356
+ */
357
+ export type AgentWidgetSSEEventResult = {
358
+ /** Text content to display */
359
+ text?: string;
360
+ /** Whether the stream is complete */
361
+ done?: boolean;
362
+ /** Error message if an error occurred */
363
+ error?: string;
364
+ } | null;
365
+
366
+ /**
367
+ * Custom SSE event parser function
368
+ * Allows transforming non-standard SSE event formats to vanilla-agent's expected format
369
+ */
370
+ export type AgentWidgetSSEEventParser = (
371
+ eventData: unknown
372
+ ) => AgentWidgetSSEEventResult | Promise<AgentWidgetSSEEventResult>;
373
+
374
+ /**
375
+ * Custom fetch function for full control over API requests
376
+ * Use this for custom authentication, request transformation, etc.
377
+ */
378
+ export type AgentWidgetCustomFetch = (
379
+ url: string,
380
+ init: RequestInit,
381
+ payload: AgentWidgetRequestPayload
382
+ ) => Promise<Response>;
383
+
384
+ /**
385
+ * Dynamic headers function - called before each request
386
+ */
387
+ export type AgentWidgetHeadersFunction = () => Record<string, string> | Promise<Record<string, string>>;
388
+
342
389
  export type AgentWidgetConfig = {
343
390
  apiUrl?: string;
344
391
  flowId?: string;
392
+ /**
393
+ * Static headers to include with each request.
394
+ * For dynamic headers (e.g., auth tokens), use `getHeaders` instead.
395
+ */
345
396
  headers?: Record<string, string>;
397
+ /**
398
+ * Dynamic headers function - called before each request.
399
+ * Useful for adding auth tokens that may change.
400
+ * @example
401
+ * ```typescript
402
+ * getHeaders: async () => ({
403
+ * 'Authorization': `Bearer ${await getAuthToken()}`
404
+ * })
405
+ * ```
406
+ */
407
+ getHeaders?: AgentWidgetHeadersFunction;
346
408
  copy?: {
347
409
  welcomeTitle?: string;
348
410
  welcomeSubtitle?: string;
@@ -374,6 +436,32 @@ export type AgentWidgetConfig = {
374
436
  actionParsers?: AgentWidgetActionParser[];
375
437
  actionHandlers?: AgentWidgetActionHandler[];
376
438
  storageAdapter?: AgentWidgetStorageAdapter;
439
+ /**
440
+ * Registry of custom components that can be rendered from JSON directives.
441
+ * Components are registered by name and can be invoked via JSON responses
442
+ * with the format: `{"component": "ComponentName", "props": {...}}`
443
+ *
444
+ * @example
445
+ * ```typescript
446
+ * config: {
447
+ * components: {
448
+ * ProductCard: (props, context) => {
449
+ * const card = document.createElement("div");
450
+ * card.innerHTML = `<h3>${props.title}</h3><p>$${props.price}</p>`;
451
+ * return card;
452
+ * }
453
+ * }
454
+ * }
455
+ * ```
456
+ */
457
+ components?: Record<string, AgentWidgetComponentRenderer>;
458
+ /**
459
+ * Enable component streaming. When true, component props will be updated
460
+ * incrementally as they stream in from the JSON response.
461
+ *
462
+ * @default true
463
+ */
464
+ enableComponentStreaming?: boolean;
377
465
  /**
378
466
  * Custom stream parser for extracting text from streaming structured responses.
379
467
  * Handles incremental parsing of JSON, XML, or other formats.
@@ -430,6 +518,68 @@ export type AgentWidgetConfig = {
430
518
  * ```
431
519
  */
432
520
  parserType?: "plain" | "json" | "regex-json" | "xml";
521
+ /**
522
+ * Custom fetch function for full control over API requests.
523
+ * Use this for custom authentication, request/response transformation, etc.
524
+ *
525
+ * When provided, this function is called instead of the default fetch.
526
+ * You receive the URL, RequestInit, and the payload that would be sent.
527
+ *
528
+ * @example
529
+ * ```typescript
530
+ * config: {
531
+ * customFetch: async (url, init, payload) => {
532
+ * // Transform request for your API format
533
+ * const myPayload = {
534
+ * flow: { id: 'my-flow-id' },
535
+ * messages: payload.messages,
536
+ * options: { stream_response: true }
537
+ * };
538
+ *
539
+ * // Add auth header
540
+ * const token = await getAuthToken();
541
+ *
542
+ * return fetch('/my-api/dispatch', {
543
+ * method: 'POST',
544
+ * headers: {
545
+ * 'Content-Type': 'application/json',
546
+ * 'Authorization': `Bearer ${token}`
547
+ * },
548
+ * body: JSON.stringify(myPayload),
549
+ * signal: init.signal
550
+ * });
551
+ * }
552
+ * }
553
+ * ```
554
+ */
555
+ customFetch?: AgentWidgetCustomFetch;
556
+ /**
557
+ * Custom SSE event parser for non-standard streaming response formats.
558
+ *
559
+ * Use this when your API returns SSE events in a different format than expected.
560
+ * Return `{ text }` for text chunks, `{ done: true }` for completion,
561
+ * `{ error }` for errors, or `null` to ignore the event.
562
+ *
563
+ * @example
564
+ * ```typescript
565
+ * // For Travrse API format
566
+ * config: {
567
+ * parseSSEEvent: (data) => {
568
+ * if (data.type === 'step_chunk' && data.chunk) {
569
+ * return { text: data.chunk };
570
+ * }
571
+ * if (data.type === 'flow_complete') {
572
+ * return { done: true };
573
+ * }
574
+ * if (data.type === 'step_error') {
575
+ * return { error: data.error };
576
+ * }
577
+ * return null; // Ignore other events
578
+ * }
579
+ * }
580
+ * ```
581
+ */
582
+ parseSSEEvent?: AgentWidgetSSEEventParser;
433
583
  };
434
584
 
435
585
  export type AgentWidgetMessageRole = "user" | "assistant" | "system";