vanilla-agent 0.1.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 ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "vanilla-agent",
3
+ "version": "0.1.0",
4
+ "description": "Themeable, plugable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./widget.css": {
16
+ "import": "./widget.css",
17
+ "default": "./dist/widget.css"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "widget.css",
23
+ "src"
24
+ ],
25
+ "dependencies": {
26
+ "lucide": "^0.552.0",
27
+ "marked": "^12.0.2"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20.12.7",
31
+ "eslint": "^8.57.0",
32
+ "eslint-config-prettier": "^9.1.0",
33
+ "postcss": "^8.4.38",
34
+ "rimraf": "^5.0.5",
35
+ "tailwindcss": "^3.4.10",
36
+ "tsup": "^8.0.1",
37
+ "typescript": "^5.4.5"
38
+ },
39
+ "engines": {
40
+ "node": ">=18.17.0"
41
+ },
42
+ "license": "MIT",
43
+ "keywords": [
44
+ "chat",
45
+ "widget",
46
+ "streaming",
47
+ "typescript"
48
+ ],
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/becomevocal/chaty.git"
52
+ },
53
+ "scripts": {
54
+ "build": "rimraf dist && npm run build:styles && npm run build:client && npm run build:installer",
55
+ "build:styles": "node -e \"const fs=require('fs');fs.mkdirSync('dist',{recursive:true});fs.copyFileSync('src/styles/widget.css','dist/widget.css');\"",
56
+ "build:client": "tsup src/index.ts --format esm,cjs,iife --global-name ChatWidget --minify --sourcemap --splitting false --dts --loader \".css=text\"",
57
+ "build:installer": "tsup src/install.ts --format iife --global-name SiteAgentInstaller --out-dir dist --minify --sourcemap --no-splitting",
58
+ "lint": "eslint . --ext .ts",
59
+ "typecheck": "tsc --noEmit"
60
+ }
61
+ }
package/src/client.ts ADDED
@@ -0,0 +1,577 @@
1
+ import { ChatWidgetConfig, ChatWidgetMessage, ChatWidgetEvent } from "./types";
2
+
3
+ type DispatchOptions = {
4
+ messages: ChatWidgetMessage[];
5
+ signal?: AbortSignal;
6
+ };
7
+
8
+ type SSEHandler = (event: ChatWidgetEvent) => void;
9
+
10
+ const DEFAULT_ENDPOINT = "https://api.travrse.ai/v1/dispatch";
11
+
12
+ export class ChatWidgetClient {
13
+ private readonly apiUrl: string;
14
+ private readonly headers: Record<string, string>;
15
+ private readonly debug: boolean;
16
+
17
+ constructor(private config: ChatWidgetConfig = {}) {
18
+ this.apiUrl = config.apiUrl ?? DEFAULT_ENDPOINT;
19
+ this.headers = {
20
+ "Content-Type": "application/json",
21
+ ...config.headers
22
+ };
23
+ this.debug = Boolean(config.debug);
24
+ }
25
+
26
+ public async dispatch(options: DispatchOptions, onEvent: SSEHandler) {
27
+ const controller = new AbortController();
28
+ if (options.signal) {
29
+ options.signal.addEventListener("abort", () => controller.abort());
30
+ }
31
+
32
+ onEvent({ type: "status", status: "connecting" });
33
+
34
+ // Build simplified payload with just messages and optional flowId
35
+ // Sort by createdAt to ensure chronological order (not local sequence)
36
+ const body = {
37
+ messages: options.messages
38
+ .slice()
39
+ .sort((a, b) => {
40
+ const timeA = new Date(a.createdAt).getTime();
41
+ const timeB = new Date(b.createdAt).getTime();
42
+ return timeA - timeB;
43
+ })
44
+ .map((message) => ({
45
+ role: message.role,
46
+ content: message.content,
47
+ createdAt: message.createdAt
48
+ })),
49
+ ...(this.config.flowId && { flowId: this.config.flowId })
50
+ };
51
+
52
+ if (this.debug) {
53
+ // eslint-disable-next-line no-console
54
+ console.debug("[ChatWidgetClient] dispatch body", body);
55
+ }
56
+
57
+ const response = await fetch(this.apiUrl, {
58
+ method: "POST",
59
+ headers: this.headers,
60
+ body: JSON.stringify(body),
61
+ signal: controller.signal
62
+ });
63
+
64
+ if (!response.ok || !response.body) {
65
+ const error = new Error(
66
+ `Chat backend request failed: ${response.status} ${response.statusText}`
67
+ );
68
+ onEvent({ type: "error", error });
69
+ throw error;
70
+ }
71
+
72
+ onEvent({ type: "status", status: "connected" });
73
+ try {
74
+ await this.streamResponse(response.body, onEvent);
75
+ } finally {
76
+ onEvent({ type: "status", status: "idle" });
77
+ }
78
+ }
79
+
80
+ private async streamResponse(
81
+ body: ReadableStream<Uint8Array>,
82
+ onEvent: SSEHandler
83
+ ) {
84
+ const reader = body.getReader();
85
+ const decoder = new TextDecoder();
86
+ let buffer = "";
87
+
88
+ const baseSequence = Date.now();
89
+ let sequenceCounter = 0;
90
+ const nextSequence = () => baseSequence + sequenceCounter++;
91
+
92
+ const cloneMessage = (msg: ChatWidgetMessage): ChatWidgetMessage => {
93
+ const reasoning = msg.reasoning
94
+ ? {
95
+ ...msg.reasoning,
96
+ chunks: [...msg.reasoning.chunks]
97
+ }
98
+ : undefined;
99
+ const toolCall = msg.toolCall
100
+ ? {
101
+ ...msg.toolCall,
102
+ chunks: msg.toolCall.chunks ? [...msg.toolCall.chunks] : undefined
103
+ }
104
+ : undefined;
105
+ const tools = msg.tools
106
+ ? msg.tools.map((tool) => ({
107
+ ...tool,
108
+ chunks: tool.chunks ? [...tool.chunks] : undefined
109
+ }))
110
+ : undefined;
111
+
112
+ return {
113
+ ...msg,
114
+ reasoning,
115
+ toolCall,
116
+ tools
117
+ };
118
+ };
119
+
120
+ const emitMessage = (msg: ChatWidgetMessage) => {
121
+ onEvent({
122
+ type: "message",
123
+ message: cloneMessage(msg)
124
+ });
125
+ };
126
+
127
+ let assistantMessage: ChatWidgetMessage | null = null;
128
+ const reasoningMessages = new Map<string, ChatWidgetMessage>();
129
+ const toolMessages = new Map<string, ChatWidgetMessage>();
130
+ const reasoningContext = {
131
+ lastId: null as string | null,
132
+ byStep: new Map<string, string>()
133
+ };
134
+ const toolContext = {
135
+ lastId: null as string | null,
136
+ byCall: new Map<string, string>()
137
+ };
138
+
139
+ const normalizeKey = (value: unknown): string | null => {
140
+ if (value === null || value === undefined) return null;
141
+ try {
142
+ return String(value);
143
+ } catch (error) {
144
+ return null;
145
+ }
146
+ };
147
+
148
+ const getStepKey = (payload: Record<string, any>) =>
149
+ normalizeKey(
150
+ payload.stepId ??
151
+ payload.step_id ??
152
+ payload.step ??
153
+ payload.parentId ??
154
+ payload.flowStepId ??
155
+ payload.flow_step_id
156
+ );
157
+
158
+ const getToolCallKey = (payload: Record<string, any>) =>
159
+ normalizeKey(
160
+ payload.callId ??
161
+ payload.call_id ??
162
+ payload.requestId ??
163
+ payload.request_id ??
164
+ payload.toolCallId ??
165
+ payload.tool_call_id ??
166
+ payload.stepId ??
167
+ payload.step_id
168
+ );
169
+
170
+ const ensureAssistantMessage = () => {
171
+ if (assistantMessage) return assistantMessage;
172
+ assistantMessage = {
173
+ id: `assistant-${Date.now()}-${Math.random().toString(16).slice(2)}`,
174
+ role: "assistant",
175
+ content: "",
176
+ createdAt: new Date().toISOString(),
177
+ streaming: true,
178
+ variant: "assistant",
179
+ sequence: nextSequence()
180
+ };
181
+ emitMessage(assistantMessage);
182
+ return assistantMessage;
183
+ };
184
+
185
+ const trackReasoningId = (stepKey: string | null, id: string) => {
186
+ reasoningContext.lastId = id;
187
+ if (stepKey) {
188
+ reasoningContext.byStep.set(stepKey, id);
189
+ }
190
+ };
191
+
192
+ const resolveReasoningId = (
193
+ payload: Record<string, any>,
194
+ allowCreate: boolean
195
+ ): string | null => {
196
+ const rawId = payload.reasoningId ?? payload.id;
197
+ const stepKey = getStepKey(payload);
198
+ if (rawId) {
199
+ const resolved = String(rawId);
200
+ trackReasoningId(stepKey, resolved);
201
+ return resolved;
202
+ }
203
+ if (stepKey) {
204
+ const existing = reasoningContext.byStep.get(stepKey);
205
+ if (existing) {
206
+ reasoningContext.lastId = existing;
207
+ return existing;
208
+ }
209
+ }
210
+ if (reasoningContext.lastId && !allowCreate) {
211
+ return reasoningContext.lastId;
212
+ }
213
+ if (!allowCreate) {
214
+ return null;
215
+ }
216
+ const generated = `reason-${nextSequence()}`;
217
+ trackReasoningId(stepKey, generated);
218
+ return generated;
219
+ };
220
+
221
+ const ensureReasoningMessage = (reasoningId: string) => {
222
+ const existing = reasoningMessages.get(reasoningId);
223
+ if (existing) {
224
+ return existing;
225
+ }
226
+
227
+ const message: ChatWidgetMessage = {
228
+ id: `reason-${reasoningId}`,
229
+ role: "assistant",
230
+ content: "",
231
+ createdAt: new Date().toISOString(),
232
+ streaming: true,
233
+ variant: "reasoning",
234
+ sequence: nextSequence(),
235
+ reasoning: {
236
+ id: reasoningId,
237
+ status: "streaming",
238
+ chunks: []
239
+ }
240
+ };
241
+
242
+ reasoningMessages.set(reasoningId, message);
243
+ emitMessage(message);
244
+ return message;
245
+ };
246
+
247
+ const trackToolId = (callKey: string | null, id: string) => {
248
+ toolContext.lastId = id;
249
+ if (callKey) {
250
+ toolContext.byCall.set(callKey, id);
251
+ }
252
+ };
253
+
254
+ const resolveToolId = (
255
+ payload: Record<string, any>,
256
+ allowCreate: boolean
257
+ ): string | null => {
258
+ const rawId = payload.toolId ?? payload.id;
259
+ const callKey = getToolCallKey(payload);
260
+ if (rawId) {
261
+ const resolved = String(rawId);
262
+ trackToolId(callKey, resolved);
263
+ return resolved;
264
+ }
265
+ if (callKey) {
266
+ const existing = toolContext.byCall.get(callKey);
267
+ if (existing) {
268
+ toolContext.lastId = existing;
269
+ return existing;
270
+ }
271
+ }
272
+ if (toolContext.lastId && !allowCreate) {
273
+ return toolContext.lastId;
274
+ }
275
+ if (!allowCreate) {
276
+ return null;
277
+ }
278
+ const generated = `tool-${nextSequence()}`;
279
+ trackToolId(callKey, generated);
280
+ return generated;
281
+ };
282
+
283
+ const ensureToolMessage = (toolId: string) => {
284
+ const existing = toolMessages.get(toolId);
285
+ if (existing) {
286
+ return existing;
287
+ }
288
+
289
+ const message: ChatWidgetMessage = {
290
+ id: `tool-${toolId}`,
291
+ role: "assistant",
292
+ content: "",
293
+ createdAt: new Date().toISOString(),
294
+ streaming: true,
295
+ variant: "tool",
296
+ sequence: nextSequence(),
297
+ toolCall: {
298
+ id: toolId,
299
+ status: "pending"
300
+ }
301
+ };
302
+
303
+ toolMessages.set(toolId, message);
304
+ emitMessage(message);
305
+ return message;
306
+ };
307
+
308
+ const resolveTimestamp = (value: unknown) => {
309
+ if (typeof value === "number" && Number.isFinite(value)) {
310
+ return value;
311
+ }
312
+ if (typeof value === "string") {
313
+ const parsed = Number(value);
314
+ if (!Number.isNaN(parsed) && Number.isFinite(parsed)) {
315
+ return parsed;
316
+ }
317
+ const dateParsed = Date.parse(value);
318
+ if (!Number.isNaN(dateParsed)) {
319
+ return dateParsed;
320
+ }
321
+ }
322
+ return Date.now();
323
+ };
324
+
325
+ while (true) {
326
+ const { done, value } = await reader.read();
327
+ if (done) break;
328
+
329
+ buffer += decoder.decode(value, { stream: true });
330
+ const events = buffer.split("\n\n");
331
+ buffer = events.pop() ?? "";
332
+
333
+ for (const event of events) {
334
+ const lines = event.split("\n");
335
+ let eventType = "message";
336
+ let data = "";
337
+
338
+ for (const line of lines) {
339
+ if (line.startsWith("event:")) {
340
+ eventType = line.replace("event:", "").trim();
341
+ } else if (line.startsWith("data:")) {
342
+ data += line.replace("data:", "").trim();
343
+ }
344
+ }
345
+
346
+ if (!data) continue;
347
+ let payload: any;
348
+ try {
349
+ payload = JSON.parse(data);
350
+ } catch (error) {
351
+ onEvent({
352
+ type: "error",
353
+ error:
354
+ error instanceof Error
355
+ ? error
356
+ : new Error("Failed to parse chat stream payload")
357
+ });
358
+ continue;
359
+ }
360
+
361
+ const payloadType =
362
+ eventType !== "message" ? eventType : payload.type ?? "message";
363
+
364
+ if (payloadType === "reason_start") {
365
+ const reasoningId =
366
+ resolveReasoningId(payload, true) ?? `reason-${nextSequence()}`;
367
+ const reasoningMessage = ensureReasoningMessage(reasoningId);
368
+ reasoningMessage.reasoning = reasoningMessage.reasoning ?? {
369
+ id: reasoningId,
370
+ status: "streaming",
371
+ chunks: []
372
+ };
373
+ reasoningMessage.reasoning.startedAt =
374
+ reasoningMessage.reasoning.startedAt ??
375
+ resolveTimestamp(payload.startedAt ?? payload.timestamp);
376
+ reasoningMessage.reasoning.completedAt = undefined;
377
+ reasoningMessage.reasoning.durationMs = undefined;
378
+ reasoningMessage.streaming = true;
379
+ reasoningMessage.reasoning.status = "streaming";
380
+ emitMessage(reasoningMessage);
381
+ } else if (payloadType === "reason_chunk") {
382
+ const reasoningId =
383
+ resolveReasoningId(payload, false) ??
384
+ resolveReasoningId(payload, true) ??
385
+ `reason-${nextSequence()}`;
386
+ const reasoningMessage = ensureReasoningMessage(reasoningId);
387
+ reasoningMessage.reasoning = reasoningMessage.reasoning ?? {
388
+ id: reasoningId,
389
+ status: "streaming",
390
+ chunks: []
391
+ };
392
+ reasoningMessage.reasoning.startedAt =
393
+ reasoningMessage.reasoning.startedAt ??
394
+ resolveTimestamp(payload.startedAt ?? payload.timestamp);
395
+ const chunk =
396
+ payload.reasoningText ??
397
+ payload.text ??
398
+ payload.delta ??
399
+ "";
400
+ if (chunk && payload.hidden !== true) {
401
+ reasoningMessage.reasoning.chunks.push(String(chunk));
402
+ }
403
+ reasoningMessage.reasoning.status = payload.done ? "complete" : "streaming";
404
+ if (payload.done) {
405
+ reasoningMessage.reasoning.completedAt = resolveTimestamp(
406
+ payload.completedAt ?? payload.timestamp
407
+ );
408
+ const start = reasoningMessage.reasoning.startedAt ?? Date.now();
409
+ reasoningMessage.reasoning.durationMs = Math.max(
410
+ 0,
411
+ (reasoningMessage.reasoning.completedAt ?? Date.now()) - start
412
+ );
413
+ }
414
+ reasoningMessage.streaming = reasoningMessage.reasoning.status !== "complete";
415
+ emitMessage(reasoningMessage);
416
+ } else if (payloadType === "reason_complete") {
417
+ const reasoningId =
418
+ resolveReasoningId(payload, false) ??
419
+ resolveReasoningId(payload, true) ??
420
+ `reason-${nextSequence()}`;
421
+ const reasoningMessage = reasoningMessages.get(reasoningId);
422
+ if (reasoningMessage?.reasoning) {
423
+ reasoningMessage.reasoning.status = "complete";
424
+ reasoningMessage.reasoning.completedAt = resolveTimestamp(
425
+ payload.completedAt ?? payload.timestamp
426
+ );
427
+ const start = reasoningMessage.reasoning.startedAt ?? Date.now();
428
+ reasoningMessage.reasoning.durationMs = Math.max(
429
+ 0,
430
+ (reasoningMessage.reasoning.completedAt ?? Date.now()) - start
431
+ );
432
+ reasoningMessage.streaming = false;
433
+ emitMessage(reasoningMessage);
434
+ }
435
+ const stepKey = getStepKey(payload);
436
+ if (stepKey) {
437
+ reasoningContext.byStep.delete(stepKey);
438
+ }
439
+ } else if (payloadType === "tool_start") {
440
+ const toolId =
441
+ resolveToolId(payload, true) ?? `tool-${nextSequence()}`;
442
+ const toolMessage = ensureToolMessage(toolId);
443
+ const tool = toolMessage.toolCall ?? {
444
+ id: toolId,
445
+ status: "pending"
446
+ };
447
+ tool.name = payload.toolName ?? tool.name;
448
+ tool.status = "running";
449
+ if (payload.args !== undefined) {
450
+ tool.args = payload.args;
451
+ }
452
+ tool.startedAt =
453
+ tool.startedAt ??
454
+ resolveTimestamp(payload.startedAt ?? payload.timestamp);
455
+ tool.completedAt = undefined;
456
+ tool.durationMs = undefined;
457
+ toolMessage.toolCall = tool;
458
+ toolMessage.streaming = true;
459
+ emitMessage(toolMessage);
460
+ } else if (payloadType === "tool_chunk") {
461
+ const toolId =
462
+ resolveToolId(payload, false) ??
463
+ resolveToolId(payload, true) ??
464
+ `tool-${nextSequence()}`;
465
+ const toolMessage = ensureToolMessage(toolId);
466
+ const tool = toolMessage.toolCall ?? {
467
+ id: toolId,
468
+ status: "running"
469
+ };
470
+ tool.startedAt =
471
+ tool.startedAt ??
472
+ resolveTimestamp(payload.startedAt ?? payload.timestamp);
473
+ const chunkText =
474
+ payload.text ?? payload.delta ?? payload.message ?? "";
475
+ if (chunkText) {
476
+ tool.chunks = tool.chunks ?? [];
477
+ tool.chunks.push(String(chunkText));
478
+ }
479
+ tool.status = "running";
480
+ toolMessage.toolCall = tool;
481
+ toolMessage.streaming = true;
482
+ emitMessage(toolMessage);
483
+ } else if (payloadType === "tool_complete") {
484
+ const toolId =
485
+ resolveToolId(payload, false) ??
486
+ resolveToolId(payload, true) ??
487
+ `tool-${nextSequence()}`;
488
+ const toolMessage = ensureToolMessage(toolId);
489
+ const tool = toolMessage.toolCall ?? {
490
+ id: toolId,
491
+ status: "running"
492
+ };
493
+ tool.status = "complete";
494
+ if (payload.result !== undefined) {
495
+ tool.result = payload.result;
496
+ }
497
+ if (typeof payload.duration === "number") {
498
+ tool.duration = payload.duration;
499
+ }
500
+ tool.completedAt = resolveTimestamp(
501
+ payload.completedAt ?? payload.timestamp
502
+ );
503
+ if (typeof payload.duration === "number") {
504
+ tool.durationMs = payload.duration;
505
+ } else {
506
+ const start = tool.startedAt ?? Date.now();
507
+ tool.durationMs = Math.max(
508
+ 0,
509
+ (tool.completedAt ?? Date.now()) - start
510
+ );
511
+ }
512
+ toolMessage.toolCall = tool;
513
+ toolMessage.streaming = false;
514
+ emitMessage(toolMessage);
515
+ const callKey = getToolCallKey(payload);
516
+ if (callKey) {
517
+ toolContext.byCall.delete(callKey);
518
+ }
519
+ } else if (payloadType === "step_chunk") {
520
+ const assistant = ensureAssistantMessage();
521
+ const chunk = payload.text ?? payload.delta ?? payload.content ?? "";
522
+ if (chunk) {
523
+ assistant.content += chunk;
524
+ emitMessage(assistant);
525
+ }
526
+ if (payload.isComplete) {
527
+ const finalContent = payload.result?.response ?? assistant.content;
528
+ if (finalContent) {
529
+ assistant.content = finalContent;
530
+ assistant.streaming = false;
531
+ emitMessage(assistant);
532
+ }
533
+ }
534
+ } else if (payloadType === "step_complete") {
535
+ const finalContent = payload.result?.response;
536
+ const assistant = ensureAssistantMessage();
537
+ if (finalContent) {
538
+ assistant.content = finalContent;
539
+ assistant.streaming = false;
540
+ emitMessage(assistant);
541
+ } else {
542
+ // No final content, just mark as complete
543
+ assistant.streaming = false;
544
+ emitMessage(assistant);
545
+ }
546
+ } else if (payloadType === "flow_complete") {
547
+ const finalContent = payload.result?.response;
548
+ if (finalContent) {
549
+ const assistant = ensureAssistantMessage();
550
+ if (finalContent !== assistant.content) {
551
+ assistant.content = finalContent;
552
+ emitMessage(assistant);
553
+ }
554
+ assistant.streaming = false;
555
+ emitMessage(assistant);
556
+ } else {
557
+ const existingAssistant = assistantMessage;
558
+ if (existingAssistant) {
559
+ const assistantFinal = existingAssistant as ChatWidgetMessage;
560
+ assistantFinal.streaming = false;
561
+ emitMessage(assistantFinal);
562
+ }
563
+ }
564
+ onEvent({ type: "status", status: "idle" });
565
+ } else if (payloadType === "error" && payload.error) {
566
+ onEvent({
567
+ type: "error",
568
+ error:
569
+ payload.error instanceof Error
570
+ ? payload.error
571
+ : new Error(String(payload.error))
572
+ });
573
+ }
574
+ }
575
+ }
576
+ }
577
+ }