vanilla-agent 1.5.0 → 1.6.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.
@@ -0,0 +1,228 @@
1
+ import type {
2
+ AgentWidgetActionContext,
3
+ AgentWidgetActionEventPayload,
4
+ AgentWidgetActionHandler,
5
+ AgentWidgetActionHandlerResult,
6
+ AgentWidgetActionParser,
7
+ AgentWidgetParsedAction,
8
+ AgentWidgetControllerEventMap,
9
+ AgentWidgetMessage
10
+ } from "../types";
11
+
12
+ type ActionManagerProcessContext = {
13
+ text: string;
14
+ message: AgentWidgetMessage;
15
+ streaming: boolean;
16
+ raw?: string;
17
+ };
18
+
19
+ type ActionManagerOptions = {
20
+ parsers: AgentWidgetActionParser[];
21
+ handlers: AgentWidgetActionHandler[];
22
+ getMetadata: () => Record<string, unknown>;
23
+ updateMetadata: (
24
+ updater: (prev: Record<string, unknown>) => Record<string, unknown>
25
+ ) => void;
26
+ emit: <K extends keyof AgentWidgetControllerEventMap>(
27
+ event: K,
28
+ payload: AgentWidgetControllerEventMap[K]
29
+ ) => void;
30
+ documentRef: Document | null;
31
+ };
32
+
33
+ const stripCodeFence = (value: string) => {
34
+ const match = value.match(/```(?:json)?\s*([\s\S]*?)```/i);
35
+ return match ? match[1] : value;
36
+ };
37
+
38
+ const extractJsonObject = (value: string) => {
39
+ const trimmed = value.trim();
40
+ const start = trimmed.indexOf("{");
41
+ if (start === -1) return null;
42
+
43
+ let depth = 0;
44
+ for (let i = start; i < trimmed.length; i += 1) {
45
+ const char = trimmed[i];
46
+ if (char === "{") depth += 1;
47
+ if (char === "}") {
48
+ depth -= 1;
49
+ if (depth === 0) {
50
+ return trimmed.slice(start, i + 1);
51
+ }
52
+ }
53
+ }
54
+ return null;
55
+ };
56
+
57
+ export const defaultJsonActionParser: AgentWidgetActionParser = ({ text }) => {
58
+ if (!text) return null;
59
+ if (!text.includes("{")) return null;
60
+
61
+ try {
62
+ const withoutFence = stripCodeFence(text);
63
+ const jsonBody = extractJsonObject(withoutFence);
64
+ if (!jsonBody) return null;
65
+ const parsed = JSON.parse(jsonBody);
66
+ if (!parsed || typeof parsed !== "object" || !parsed.action) {
67
+ return null;
68
+ }
69
+ const { action, ...payload } = parsed;
70
+ return {
71
+ type: String(action),
72
+ payload,
73
+ raw: parsed
74
+ };
75
+ } catch {
76
+ return null;
77
+ }
78
+ };
79
+
80
+ const asString = (value: unknown) =>
81
+ typeof value === "string" ? value : value == null ? "" : String(value);
82
+
83
+ export const defaultActionHandlers: Record<
84
+ string,
85
+ AgentWidgetActionHandler
86
+ > = {
87
+ message: (action) => {
88
+ if (action.type !== "message") return;
89
+ const text = asString((action.payload as Record<string, unknown>).text);
90
+ return {
91
+ handled: true,
92
+ displayText: text
93
+ };
94
+ },
95
+ messageAndClick: (action, context) => {
96
+ if (action.type !== "message_and_click") return;
97
+ const payload = action.payload as Record<string, unknown>;
98
+ const selector = asString(payload.element);
99
+ if (selector && context.document?.querySelector) {
100
+ const element = context.document.querySelector<HTMLElement>(selector);
101
+ if (element) {
102
+ setTimeout(() => {
103
+ element.click();
104
+ }, 400);
105
+ } else if (typeof console !== "undefined") {
106
+ // eslint-disable-next-line no-console
107
+ console.warn("[AgentWidget] Element not found for selector:", selector);
108
+ }
109
+ }
110
+ return {
111
+ handled: true,
112
+ displayText: asString(payload.text)
113
+ };
114
+ }
115
+ };
116
+
117
+ const ensureArrayOfStrings = (value: unknown): string[] => {
118
+ if (Array.isArray(value)) {
119
+ return value.map((entry) => String(entry));
120
+ }
121
+ return [];
122
+ };
123
+
124
+ export const createActionManager = (options: ActionManagerOptions) => {
125
+ let processedIds = new Set(
126
+ ensureArrayOfStrings(options.getMetadata().processedActionMessageIds)
127
+ );
128
+
129
+ const syncFromMetadata = () => {
130
+ processedIds = new Set(
131
+ ensureArrayOfStrings(options.getMetadata().processedActionMessageIds)
132
+ );
133
+ };
134
+
135
+ const persistProcessedIds = () => {
136
+ const latestIds = Array.from(processedIds);
137
+ options.updateMetadata((prev) => ({
138
+ ...prev,
139
+ processedActionMessageIds: latestIds
140
+ }));
141
+ };
142
+
143
+ const process = (context: ActionManagerProcessContext): string | null => {
144
+ if (
145
+ context.streaming ||
146
+ context.message.role !== "assistant" ||
147
+ !context.text ||
148
+ processedIds.has(context.message.id)
149
+ ) {
150
+ return null;
151
+ }
152
+
153
+ const parseSource =
154
+ (typeof context.raw === "string" && context.raw) ||
155
+ (typeof context.message.rawContent === "string" &&
156
+ context.message.rawContent) ||
157
+ (typeof context.text === "string" && context.text) ||
158
+ null;
159
+
160
+ if (
161
+ !parseSource &&
162
+ typeof context.text === "string" &&
163
+ context.text.trim().startsWith("{") &&
164
+ typeof console !== "undefined"
165
+ ) {
166
+ // eslint-disable-next-line no-console
167
+ console.warn(
168
+ "[AgentWidget] Structured response detected but no raw payload was provided. Ensure your stream parser returns { text, raw }."
169
+ );
170
+ }
171
+
172
+ const action = parseSource
173
+ ? options.parsers.reduce<AgentWidgetParsedAction | null>(
174
+ (acc, parser) =>
175
+ acc || parser?.({ text: parseSource, message: context.message }) || null,
176
+ null
177
+ )
178
+ : null;
179
+
180
+ if (!action) {
181
+ return null;
182
+ }
183
+
184
+ processedIds.add(context.message.id);
185
+ persistProcessedIds();
186
+
187
+ const eventPayload: AgentWidgetActionEventPayload = {
188
+ action,
189
+ message: context.message
190
+ };
191
+ options.emit("action:detected", eventPayload);
192
+
193
+ for (const handler of options.handlers) {
194
+ if (!handler) continue;
195
+ try {
196
+ const handlerResult = handler(action, {
197
+ message: context.message,
198
+ metadata: options.getMetadata(),
199
+ updateMetadata: options.updateMetadata,
200
+ document: options.documentRef
201
+ } as AgentWidgetActionContext) as AgentWidgetActionHandlerResult | void;
202
+
203
+ if (!handlerResult) continue;
204
+
205
+ if (handlerResult.displayText !== undefined && handlerResult.handled) {
206
+ return handlerResult.displayText;
207
+ }
208
+
209
+ if (handlerResult.handled) {
210
+ return "";
211
+ }
212
+ } catch (error) {
213
+ if (typeof console !== "undefined") {
214
+ // eslint-disable-next-line no-console
215
+ console.error("[AgentWidget] Action handler error:", error);
216
+ }
217
+ }
218
+ }
219
+
220
+ return "";
221
+ };
222
+
223
+ return {
224
+ process,
225
+ syncFromMetadata
226
+ };
227
+ };
228
+
@@ -0,0 +1,41 @@
1
+ type Handler<T> = (payload: T) => void;
2
+
3
+ export type EventUnsubscribe = () => void;
4
+
5
+ export const createEventBus = <EventMap extends Record<string, any>>() => {
6
+ const listeners = new Map<keyof EventMap, Set<Handler<any>>>();
7
+
8
+ const on = <K extends keyof EventMap>(
9
+ event: K,
10
+ handler: Handler<EventMap[K]>
11
+ ): EventUnsubscribe => {
12
+ if (!listeners.has(event)) {
13
+ listeners.set(event, new Set());
14
+ }
15
+ listeners.get(event)!.add(handler as Handler<any>);
16
+ return () => off(event, handler);
17
+ };
18
+
19
+ const off = <K extends keyof EventMap>(
20
+ event: K,
21
+ handler: Handler<EventMap[K]>
22
+ ) => {
23
+ listeners.get(event)?.delete(handler as Handler<any>);
24
+ };
25
+
26
+ const emit = <K extends keyof EventMap>(event: K, payload: EventMap[K]) => {
27
+ listeners.get(event)?.forEach((handler) => {
28
+ try {
29
+ handler(payload);
30
+ } catch (error) {
31
+ if (typeof console !== "undefined") {
32
+ // eslint-disable-next-line no-console
33
+ console.error("[AgentWidget] Event handler error:", error);
34
+ }
35
+ }
36
+ });
37
+ };
38
+
39
+ return { on, off, emit };
40
+ };
41
+
@@ -140,7 +140,9 @@ const createRegexJsonParserInternal = (): {
140
140
  processChunk: async (accumulatedContent: string): Promise<AgentWidgetStreamParserResult | string | null> => {
141
141
  // Skip if no new content
142
142
  if (accumulatedContent.length <= processedLength) {
143
- return extractedText ? { text: extractedText, raw: accumulatedContent } : extractedText;
143
+ return extractedText !== null
144
+ ? { text: extractedText, raw: accumulatedContent }
145
+ : null;
144
146
  }
145
147
 
146
148
  // Validate that the accumulated content looks like valid JSON
@@ -159,10 +161,14 @@ const createRegexJsonParserInternal = (): {
159
161
  processedLength = accumulatedContent.length;
160
162
 
161
163
  // Return both the extracted text and raw JSON
162
- return extractedText ? {
163
- text: extractedText,
164
- raw: accumulatedContent
165
- } : extractedText;
164
+ if (extractedText !== null) {
165
+ return {
166
+ text: extractedText,
167
+ raw: accumulatedContent
168
+ };
169
+ }
170
+
171
+ return null;
166
172
  },
167
173
  close: async () => {
168
174
  // No cleanup needed for regex-based parser
@@ -257,7 +263,9 @@ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
257
263
 
258
264
  // Skip if no new content
259
265
  if (accumulatedContent.length <= processedLength) {
260
- return extractedText ? { text: extractedText, raw: accumulatedContent } : extractedText;
266
+ return extractedText !== null
267
+ ? { text: extractedText, raw: accumulatedContent }
268
+ : null;
261
269
  }
262
270
 
263
271
  try {
@@ -278,10 +286,14 @@ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
278
286
  processedLength = accumulatedContent.length;
279
287
 
280
288
  // Return both the extracted text and raw JSON
281
- return extractedText ? {
282
- text: extractedText,
283
- raw: accumulatedContent
284
- } : extractedText;
289
+ if (extractedText !== null) {
290
+ return {
291
+ text: extractedText,
292
+ raw: accumulatedContent
293
+ };
294
+ }
295
+
296
+ return null;
285
297
  },
286
298
  close: () => {
287
299
  // No cleanup needed
@@ -0,0 +1,72 @@
1
+ import type {
2
+ AgentWidgetMessage,
3
+ AgentWidgetStorageAdapter,
4
+ AgentWidgetStoredState
5
+ } from "../types";
6
+
7
+ const safeJsonParse = (value: string | null) => {
8
+ if (!value) return null;
9
+ try {
10
+ return JSON.parse(value);
11
+ } catch (error) {
12
+ if (typeof console !== "undefined") {
13
+ // eslint-disable-next-line no-console
14
+ console.error("[AgentWidget] Failed to parse stored state:", error);
15
+ }
16
+ return null;
17
+ }
18
+ };
19
+
20
+ const sanitizeMessages = (messages: AgentWidgetMessage[]) =>
21
+ messages.map((message) => ({
22
+ ...message,
23
+ streaming: false
24
+ }));
25
+
26
+ export const createLocalStorageAdapter = (
27
+ key = "vanilla-agent-state"
28
+ ): AgentWidgetStorageAdapter => {
29
+ const getStorage = () => {
30
+ if (typeof window === "undefined" || !window.localStorage) {
31
+ return null;
32
+ }
33
+ return window.localStorage;
34
+ };
35
+
36
+ return {
37
+ load: () => {
38
+ const storage = getStorage();
39
+ if (!storage) return null;
40
+ return safeJsonParse(storage.getItem(key));
41
+ },
42
+ save: (state: AgentWidgetStoredState) => {
43
+ const storage = getStorage();
44
+ if (!storage) return;
45
+ try {
46
+ const payload: AgentWidgetStoredState = {
47
+ ...state,
48
+ messages: state.messages ? sanitizeMessages(state.messages) : undefined
49
+ };
50
+ storage.setItem(key, JSON.stringify(payload));
51
+ } catch (error) {
52
+ if (typeof console !== "undefined") {
53
+ // eslint-disable-next-line no-console
54
+ console.error("[AgentWidget] Failed to persist state:", error);
55
+ }
56
+ }
57
+ },
58
+ clear: () => {
59
+ const storage = getStorage();
60
+ if (!storage) return;
61
+ try {
62
+ storage.removeItem(key);
63
+ } catch (error) {
64
+ if (typeof console !== "undefined") {
65
+ // eslint-disable-next-line no-console
66
+ console.error("[AgentWidget] Failed to clear stored state:", error);
67
+ }
68
+ }
69
+ }
70
+ };
71
+ };
72
+