@guideai/react-native 0.0.0-stage → 0.2.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,3592 @@
1
+ import { useContext, createContext, useState, useRef, useEffect, useCallback } from "react";
2
+ const DRAWING_URL = /^https:\/\/res\.cloudinary\.com\/[\w-]+\/image\/upload\//;
3
+ const IMAGE_BLOCK = /```image[ \t]*\n([\s\S]*?)```/g;
4
+ function headlessImages(text) {
5
+ const images = [];
6
+ const out = (text || "").replace(IMAGE_BLOCK, (_block, raw) => {
7
+ let data;
8
+ try {
9
+ data = JSON.parse(raw.trim());
10
+ } catch {
11
+ return "";
12
+ }
13
+ if (typeof data.url !== "string" || !DRAWING_URL.test(data.url)) return "";
14
+ const image = {
15
+ url: data.url,
16
+ svg: typeof data.svg === "string" && DRAWING_URL.test(data.svg) ? data.svg : null,
17
+ alt: typeof data.alt === "string" ? data.alt.slice(0, 200) : "",
18
+ width: Math.round(Number(data.width)) || 1600,
19
+ height: Math.round(Number(data.height)) || 900
20
+ };
21
+ images.push(image);
22
+ return `![${image.alt.replace(/[[\]]/g, "")}](${image.url})`;
23
+ });
24
+ return { text: out, images };
25
+ }
26
+ const NAME_PREFIXES = [
27
+ /^api[\s_-]*v?\d*[\s_-]*/i,
28
+ /^(get|fetch|list|read|retrieve|show|query|search|find|lookup|look up)[\s_-]+/i,
29
+ /^(create|add|post|new|insert|register)[\s_-]+/i,
30
+ /^(update|patch|put|edit|modify|set)[\s_-]+/i,
31
+ /^(delete|remove|destroy|archive)[\s_-]+/i,
32
+ /^(check|verify|validate|count|send|export|download)[\s_-]+/i
33
+ ];
34
+ const METHOD_VERBS = {
35
+ GET: "Retrieving",
36
+ POST: "Creating",
37
+ PUT: "Updating",
38
+ PATCH: "Updating",
39
+ DELETE: "Removing"
40
+ };
41
+ const NAME_VERBS = [
42
+ { pattern: /^(search|find|lookup|look_up|query)/i, verb: "Looking up" },
43
+ { pattern: /^(list|get_all|getall)/i, verb: "Retrieving" },
44
+ { pattern: /^(count|stats|statistics|summary|analytics|metrics|usage|report)/i, verb: "Gathering" },
45
+ { pattern: /^(check|verify|validate|status)/i, verb: "Checking" },
46
+ { pattern: /^(create|add|new|insert|register)/i, verb: "Creating" },
47
+ { pattern: /^(update|edit|modify|set|patch)/i, verb: "Updating" },
48
+ { pattern: /^(delete|remove|destroy|archive)/i, verb: "Removing" },
49
+ { pattern: /^(send|email|notify|invite)/i, verb: "Sending" },
50
+ { pattern: /^(export|download)/i, verb: "Preparing" }
51
+ ];
52
+ const SELF_DESCRIBING = /\b(information|details|data|summary|stats|statistics|activity|history|settings|status|usage|overview|report|list|metrics|analytics|breakdown)\b/i;
53
+ function humanizeName(name) {
54
+ let out = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_\-/]+/g, " ").replace(/\bapi\s*v?\d+\b/gi, " ").replace(/\s+(get|post|put|patch|delete)$/i, "").replace(/\s+/g, " ").trim().toLowerCase();
55
+ for (const prefix of NAME_PREFIXES) {
56
+ out = out.replace(prefix, "");
57
+ }
58
+ const kept = [];
59
+ for (const word of out.trim().split(" ").reverse()) {
60
+ const stem = word.replace(/s$/, "");
61
+ if (!word || stem === "id" || stem === "uuid") continue;
62
+ if (kept.some((k) => k.replace(/s$/, "") === stem)) continue;
63
+ kept.push(word);
64
+ }
65
+ return kept.reverse().join(" ");
66
+ }
67
+ function pickVerb(toolName, noun, method) {
68
+ const bare = toolName.replace(NAME_PREFIXES[0], "");
69
+ for (const { pattern, verb } of NAME_VERBS) {
70
+ if (pattern.test(bare)) return verb;
71
+ }
72
+ for (const { pattern, verb } of NAME_VERBS) {
73
+ if (pattern.test(noun)) return verb;
74
+ }
75
+ return METHOD_VERBS[method.toUpperCase()] || "Retrieving";
76
+ }
77
+ function describeToolCall(toolCall) {
78
+ const supplied = (toolCall.label || "").trim();
79
+ if (supplied) return supplied;
80
+ let noun = humanizeName(toolCall.tool || "");
81
+ if (!noun) return "Retrieving information";
82
+ const verb = pickVerb(toolCall.tool || "", noun, toolCall.method || "GET");
83
+ const ownScope = /\s+(me|self|current|mine)$/.test(noun);
84
+ if (ownScope) noun = `your ${noun.replace(/\s+(me|self|current|mine)$/, "")}`;
85
+ if (SELF_DESCRIBING.test(noun) || ownScope) return `${verb} ${noun}`;
86
+ const singular = noun.replace(/([a-z])(?<!s|u|i)s$/, "$1");
87
+ return `${verb} ${singular} information`;
88
+ }
89
+ function describeToolBatch(toolCalls) {
90
+ if (toolCalls.length === 0) return "Working on it";
91
+ if (toolCalls.length === 1) return describeToolCall(toolCalls[0]);
92
+ return `Gathering information from ${toolCalls.length} sources`;
93
+ }
94
+ class MemoryKVStore {
95
+ constructor() {
96
+ this.entries = /* @__PURE__ */ new Map();
97
+ }
98
+ getItem(key) {
99
+ const value = this.entries.get(key);
100
+ return value === void 0 ? null : value;
101
+ }
102
+ setItem(key, value) {
103
+ this.entries.set(key, String(value));
104
+ }
105
+ removeItem(key) {
106
+ this.entries.delete(key);
107
+ }
108
+ /** Test convenience — not part of the KVStore contract. */
109
+ clear() {
110
+ this.entries.clear();
111
+ }
112
+ }
113
+ const MAX_MESSAGE_LENGTH = 2e3;
114
+ const MAX_TOOL_RESULT_SIZE = 5e4;
115
+ const VALID_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "CLIENT"]);
116
+ const INJECTION_PATTERN = /<script|javascript:|on\w+\s*=|union\s+select|drop\s+table|insert\s+into|delete\s+from/i;
117
+ const CREDENTIAL_PATTERN$1 = /sk[_-]live[_-]\w{10,}|pk[_-]live[_-]\w{10,}|Bearer\s+\S{20,}/g;
118
+ const THINKING_BLOCK_PATTERN = /<thinking\b[^>]*>[\s\S]*?<\/thinking>/gi;
119
+ const THINKING_LINE_PATTERN = /^\s*<\/?thinking\b[^>]*>\s*$/gim;
120
+ const GUIDE_FENCE_PATTERN = /```(?:guide|guideai-guide)\s*\n?[\s\S]*?(?:```|$)/gi;
121
+ const GENERIC_SELECTORS = /* @__PURE__ */ new Set([
122
+ "a",
123
+ "button",
124
+ "input",
125
+ "select",
126
+ "textarea",
127
+ "div",
128
+ "span",
129
+ "li",
130
+ "p",
131
+ "form",
132
+ "table",
133
+ "img",
134
+ "label"
135
+ ]);
136
+ class HarnessValidator {
137
+ constructor() {
138
+ this.registeredTools = [];
139
+ }
140
+ /** Update the list of registered tools (called after harness init). */
141
+ setRegisteredTools(tools) {
142
+ this.registeredTools = tools;
143
+ }
144
+ // ----------------------------------------------------------------
145
+ // Input validation
146
+ // ----------------------------------------------------------------
147
+ /**
148
+ * Validate and sanitize user input before sending to the backend.
149
+ *
150
+ * Checks:
151
+ * - Not empty
152
+ * - Within max length
153
+ * - No obvious injection patterns
154
+ */
155
+ validateUserInput(question) {
156
+ if (!question || !question.trim()) {
157
+ return { valid: false, error: "Message cannot be empty." };
158
+ }
159
+ const trimmed = question.trim();
160
+ if (trimmed.length > MAX_MESSAGE_LENGTH) {
161
+ return {
162
+ valid: true,
163
+ sanitized: trimmed.slice(0, MAX_MESSAGE_LENGTH)
164
+ };
165
+ }
166
+ if (INJECTION_PATTERN.test(trimmed)) {
167
+ console.warn("[GuideAI] Potential injection pattern detected in user input.");
168
+ }
169
+ return { valid: true, sanitized: trimmed };
170
+ }
171
+ // ----------------------------------------------------------------
172
+ // Tool call validation
173
+ // ----------------------------------------------------------------
174
+ /**
175
+ * Validate a tool call response from the backend before execution.
176
+ *
177
+ * Checks:
178
+ * - Required fields present (tool, method, path)
179
+ * - Method is a valid HTTP method
180
+ * - Tool exists in the registered tools list
181
+ * - Path looks valid (starts with /)
182
+ */
183
+ validateToolCall(toolCall) {
184
+ if (!toolCall) {
185
+ return { valid: false, error: "Tool call is missing." };
186
+ }
187
+ if (!toolCall.tool || typeof toolCall.tool !== "string") {
188
+ return { valid: false, error: 'Tool call missing "tool" name.' };
189
+ }
190
+ if (!toolCall.method || typeof toolCall.method !== "string") {
191
+ return { valid: false, error: 'Tool call missing "method".' };
192
+ }
193
+ const method = toolCall.method.toUpperCase();
194
+ if (!VALID_METHODS.has(method)) {
195
+ return {
196
+ valid: false,
197
+ error: `Invalid tool method "${toolCall.method}". Expected one of: ${[...VALID_METHODS].join(", ")}.`
198
+ };
199
+ }
200
+ if (!toolCall.path || typeof toolCall.path !== "string") {
201
+ return { valid: false, error: 'Tool call missing "path".' };
202
+ }
203
+ if (!toolCall.path.startsWith("/")) {
204
+ return {
205
+ valid: false,
206
+ error: `Tool path must start with "/". Got: "${toolCall.path}".`
207
+ };
208
+ }
209
+ if (this.registeredTools.length > 0) {
210
+ const found = this.registeredTools.some(
211
+ (t) => t.name === toolCall.tool
212
+ );
213
+ if (!found) {
214
+ return {
215
+ valid: false,
216
+ error: `Tool "${toolCall.tool}" is not registered. Available: ${this.registeredTools.map((t) => t.name).slice(0, 5).join(", ")}...`
217
+ };
218
+ }
219
+ }
220
+ return { valid: true };
221
+ }
222
+ // ----------------------------------------------------------------
223
+ // Response validation
224
+ // ----------------------------------------------------------------
225
+ /**
226
+ * Validate and sanitize the backend response before showing to user.
227
+ *
228
+ * - Strip leaked credentials from answer text
229
+ * - Validate response structure
230
+ */
231
+ validateResponse(response) {
232
+ if (!response || typeof response !== "object") {
233
+ return { valid: false, error: "Invalid response from server." };
234
+ }
235
+ const resp = response;
236
+ if (typeof resp.answer !== "string") {
237
+ return { valid: false, error: 'Response missing "answer" field.' };
238
+ }
239
+ const sanitizedAnswer = this.sanitizeAnswer(resp.answer);
240
+ if (sanitizedAnswer !== resp.answer) {
241
+ console.warn("[GuideAI] Sanitized hidden reasoning or leaked credentials from response.");
242
+ }
243
+ return { valid: true, sanitized: sanitizedAnswer };
244
+ }
245
+ /**
246
+ * Sanitize the answer text — strip leaked credentials, hidden reasoning
247
+ * and machine payload the visitor should never read.
248
+ */
249
+ sanitizeAnswer(answer) {
250
+ return answer.replace(THINKING_BLOCK_PATTERN, "").replace(THINKING_LINE_PATTERN, "").replace(GUIDE_FENCE_PATTERN, "").replace(CREDENTIAL_PATTERN$1, "[REDACTED]").trim();
251
+ }
252
+ // ----------------------------------------------------------------
253
+ // Tool result validation
254
+ // ----------------------------------------------------------------
255
+ /**
256
+ * Validate a tool execution result before sending to the format endpoint.
257
+ *
258
+ * - Check structure
259
+ * - Truncate oversized results
260
+ * - Strip credential-looking values
261
+ */
262
+ validateToolResult(result) {
263
+ if (!result) {
264
+ return { valid: false, error: "Tool result is missing." };
265
+ }
266
+ if (typeof result.success !== "boolean") {
267
+ return { valid: false, error: 'Tool result missing "success" field.' };
268
+ }
269
+ if (result.data) {
270
+ const serialized = JSON.stringify(result.data);
271
+ if (serialized.length > MAX_TOOL_RESULT_SIZE) {
272
+ result.data = this._truncateData(result.data);
273
+ }
274
+ }
275
+ return { valid: true };
276
+ }
277
+ /**
278
+ * Enhanced tool result validation — classifies errors and detects empty-success.
279
+ *
280
+ * Returns errorType for contextual UI messages and warning for
281
+ * success-but-empty scenarios.
282
+ */
283
+ validateToolResultEnhanced(result) {
284
+ if (!result) {
285
+ return { valid: false, error: "Tool result is missing.", errorType: "unknown" };
286
+ }
287
+ if (typeof result.success !== "boolean") {
288
+ return { valid: false, error: 'Tool result missing "success" field.', errorType: "unknown" };
289
+ }
290
+ if (!result.success) {
291
+ const errorType = this._classifyError(result);
292
+ const friendlyMsg = this._friendlyErrorMessage(errorType);
293
+ const raw = (result.error || "").trim();
294
+ return {
295
+ valid: false,
296
+ error: friendlyMsg,
297
+ errorType,
298
+ detail: raw ? `${friendlyMsg} (${raw})` : friendlyMsg
299
+ };
300
+ }
301
+ if (this._isEmptyData(result.data)) {
302
+ return {
303
+ valid: true,
304
+ warning: "That went through, but there was nothing to show for it yet."
305
+ };
306
+ }
307
+ if (result.data) {
308
+ const serialized = JSON.stringify(result.data);
309
+ if (serialized.length > MAX_TOOL_RESULT_SIZE) {
310
+ result.data = this._truncateData(result.data);
311
+ }
312
+ }
313
+ return { valid: true };
314
+ }
315
+ // ----------------------------------------------------------------
316
+ // Formatted response validation
317
+ // ----------------------------------------------------------------
318
+ /**
319
+ * Validate that an LLM-formatted answer is substantial and relevant.
320
+ * Returns the answer as-is if OK, or a fallback if not.
321
+ */
322
+ validateFormattedResponse(answer, toolResult) {
323
+ if (!answer || answer.trim().length < 15) {
324
+ return "I retrieved the data but couldn't generate a complete summary. Please try again.";
325
+ }
326
+ const refusalPatterns = /^(i cannot|i'm unable|i don't have|sorry, i can't)/i;
327
+ if (refusalPatterns.test(answer.trim()) && (toolResult == null ? void 0 : toolResult.success) && toolResult.data) {
328
+ return "The data was fetched successfully. Please rephrase your question for a better summary.";
329
+ }
330
+ return this.sanitizeAnswer(answer);
331
+ }
332
+ // ----------------------------------------------------------------
333
+ // Guide step validation
334
+ // ----------------------------------------------------------------
335
+ /**
336
+ * Validate guide steps before rendering the walkthrough button.
337
+ * Ensures steps have minimum required fields and catches generic selectors.
338
+ */
339
+ validateGuideSteps(steps) {
340
+ if (!steps || steps.length === 0) {
341
+ return { valid: false, steps: [], warnings: ["No guide steps returned."] };
342
+ }
343
+ const warnings = [];
344
+ const validSteps = [];
345
+ for (let i = 0; i < steps.length; i++) {
346
+ const step = steps[i];
347
+ const title = step.title;
348
+ const body = step.body;
349
+ const selector = step.target_selector;
350
+ const action = step.action;
351
+ if (!title && !body) {
352
+ warnings.push(`Step ${i + 1} has no title or body — skipped.`);
353
+ continue;
354
+ }
355
+ if (selector && GENERIC_SELECTORS.has(selector.trim())) {
356
+ warnings.push(
357
+ `Step ${i + 1} has a generic selector "${selector}" — it may not highlight correctly.`
358
+ );
359
+ step.target_selector = void 0;
360
+ if (action && action !== "none") {
361
+ step.action = "none";
362
+ }
363
+ }
364
+ validSteps.push(step);
365
+ }
366
+ return {
367
+ valid: validSteps.length > 0,
368
+ steps: validSteps,
369
+ warnings
370
+ };
371
+ }
372
+ // ----------------------------------------------------------------
373
+ // Private helpers
374
+ // ----------------------------------------------------------------
375
+ _classifyError(result) {
376
+ const status = result.status;
377
+ const error = (result.error || "").toLowerCase();
378
+ if (status === 401 || status === 403) return "auth";
379
+ if (status === 404) return "not_found";
380
+ if (status === 429) return "rate_limit";
381
+ if (status && status >= 500) return "server";
382
+ if (status === 400 || status === 422) return "validation";
383
+ if (error.includes("timed out") || error.includes("timeout")) return "timeout";
384
+ if (error.includes("connect") || error.includes("network")) return "connection";
385
+ if (status && status >= 400) return "client";
386
+ return result.error_type || "unknown";
387
+ }
388
+ /**
389
+ * What a person reads when a request fails.
390
+ *
391
+ * No status codes, no endpoint names, no field paths — those belong to the
392
+ * model (see `detail`), which can act on them. A person can only be told,
393
+ * plainly, that the thing did not work and roughly why.
394
+ */
395
+ _friendlyErrorMessage(errorType) {
396
+ switch (errorType) {
397
+ case "auth":
398
+ return "Your session has expired. Please log in again and retry.";
399
+ case "not_found":
400
+ return "I couldn't find that record. It may have been removed.";
401
+ case "rate_limit":
402
+ return "Too many requests right now. Please try again in a moment.";
403
+ case "timeout":
404
+ return "That took too long to respond. Please try again.";
405
+ case "connection":
406
+ return "I couldn't reach the service. Please check your connection and try again.";
407
+ case "server":
408
+ return "The service ran into a problem. Please try again shortly.";
409
+ case "validation":
410
+ return "Some of the details in that request weren't accepted.";
411
+ default:
412
+ return "I wasn't able to complete that request.";
413
+ }
414
+ }
415
+ _isEmptyData(data) {
416
+ if (data === null || data === void 0) return true;
417
+ if (Array.isArray(data)) return data.length === 0;
418
+ if (typeof data === "string") return data.trim().length === 0;
419
+ if (typeof data === "object") {
420
+ const obj = data;
421
+ const keys = Object.keys(obj).filter((k) => !k.startsWith("_"));
422
+ if (keys.length === 0) return true;
423
+ return keys.every((k) => {
424
+ const v = obj[k];
425
+ return v === null || v === void 0 || v === "" || Array.isArray(v) && v.length === 0 || typeof v === "object" && v !== null && Object.keys(v).length === 0;
426
+ });
427
+ }
428
+ return false;
429
+ }
430
+ /**
431
+ * Truncate large data payloads to prevent token waste at the format endpoint.
432
+ */
433
+ _truncateData(data) {
434
+ if (Array.isArray(data)) {
435
+ return data.slice(0, 25);
436
+ }
437
+ if (data && typeof data === "object") {
438
+ const obj = data;
439
+ for (const key of ["data", "results", "items", "records", "rows"]) {
440
+ if (Array.isArray(obj[key])) {
441
+ return {
442
+ ...obj,
443
+ [key]: obj[key].slice(0, 25),
444
+ _truncated: true
445
+ };
446
+ }
447
+ }
448
+ }
449
+ return data;
450
+ }
451
+ }
452
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
453
+ const DEFAULT_MAX_MESSAGES = 50;
454
+ const SENSITIVE_KEY_PATTERN = /^(password|passwd|secret|token|api[_-]?key|admin[_-]?key|access[_-]?key|private[_-]?key|authorization|ssn|credit[_-]?card|cvv|cvc|pin)$/i;
455
+ const CREDENTIAL_PATTERN = /sk[_-]live[_-]\w{10,}|pk[_-]live[_-]\w{10,}|Bearer\s+\S{20,}/g;
456
+ class ConversationMemory {
457
+ /**
458
+ * @param store Where history lives. Defaults to memory so core never
459
+ * reaches for a global; the web SDK passes its localStorage adapter.
460
+ */
461
+ constructor(siteId, ttlMs, maxMessages, store = new MemoryKVStore()) {
462
+ this.listener = null;
463
+ this.store = store;
464
+ this.storageKey = `guideai_chat_${siteId}`;
465
+ this.ttlMs = ttlMs != null ? ttlMs : DEFAULT_TTL_MS;
466
+ this.maxMessages = maxMessages != null ? maxMessages : DEFAULT_MAX_MESSAGES;
467
+ }
468
+ /** Get all messages, checking TTL. Returns empty array if expired. */
469
+ getMessages() {
470
+ try {
471
+ const raw = this.store.getItem(this.storageKey);
472
+ if (!raw) return [];
473
+ const data = JSON.parse(raw);
474
+ if (Date.now() - data.updated_at > this.ttlMs) {
475
+ this.clear();
476
+ return [];
477
+ }
478
+ return data.messages || [];
479
+ } catch {
480
+ return [];
481
+ }
482
+ }
483
+ /** Add a message and persist to the store. Auto-generates an ID if not provided. */
484
+ addMessage(message) {
485
+ var _a;
486
+ if (!message.id) {
487
+ message.id = `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
488
+ }
489
+ const messages = this.getMessages();
490
+ messages.push(message);
491
+ const trimmed = messages.slice(-this.maxMessages);
492
+ this._save(trimmed);
493
+ try {
494
+ (_a = this.listener) == null ? void 0 : _a.call(this, message);
495
+ } catch {
496
+ }
497
+ }
498
+ /** Called with every message added from now on; null stops it. */
499
+ onMessage(listener) {
500
+ this.listener = listener;
501
+ }
502
+ /**
503
+ * Get recent messages formatted for LLM context.
504
+ * Includes tool result summaries so the LLM knows what data was
505
+ * previously returned and can make informed follow-up tool calls.
506
+ * Caps total payload to ~4000 chars to stay within token budget.
507
+ */
508
+ getRecentForLLM(count = 10) {
509
+ const messages = this.getMessages();
510
+ const MAX_MSG_CHARS = 500;
511
+ const MAX_TOOL_RESULT_CHARS = 800;
512
+ const MAX_TOTAL_CHARS = 4e3;
513
+ const recent = messages.slice(-count).map((msg) => {
514
+ var _a;
515
+ let content = msg.content;
516
+ if (msg.role === "assistant" && content.length > MAX_MSG_CHARS) {
517
+ content = content.slice(0, MAX_MSG_CHARS) + "...";
518
+ }
519
+ if (((_a = msg.toolResult) == null ? void 0 : _a.success) && msg.toolResult.data) {
520
+ const summary = this._summarizeToolResult(msg.toolResult.data, MAX_TOOL_RESULT_CHARS);
521
+ if (summary) {
522
+ content += `
523
+
524
+ [Tool result data: ${summary}]`;
525
+ }
526
+ }
527
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
528
+ const callSummary = msg.toolCalls.map((tc) => {
529
+ const route = tc.method && tc.path ? ` ${tc.method} ${tc.path}` : "";
530
+ const detail = tc.summary ? ` - ${tc.summary}` : "";
531
+ return `${tc.tool}${route}: ${tc.success ? "ok" : "failed"}${detail}`;
532
+ }).join("; ");
533
+ content += `
534
+ [Tool calls: ${callSummary}]`;
535
+ }
536
+ content = content.replace(CREDENTIAL_PATTERN, "[REDACTED]");
537
+ return { role: msg.role, content };
538
+ });
539
+ let total = recent.reduce((sum, m) => sum + m.content.length, 0);
540
+ while (recent.length > 2 && total > MAX_TOTAL_CHARS) {
541
+ const removed = recent.shift();
542
+ total -= removed.content.length;
543
+ }
544
+ return recent;
545
+ }
546
+ /**
547
+ * Create a compact summary of tool result data for LLM context.
548
+ * Extracts key fields, array lengths, and available resource names.
549
+ */
550
+ _summarizeToolResult(data, maxChars) {
551
+ try {
552
+ const cleaned = this._stripSensitiveFields(data);
553
+ const json = JSON.stringify(cleaned);
554
+ if (json.length <= maxChars) {
555
+ return json;
556
+ }
557
+ if (Array.isArray(cleaned)) {
558
+ const total = cleaned.length;
559
+ const pickFields = (item) => {
560
+ if (typeof item === "object" && item !== null) {
561
+ const obj = item;
562
+ const picked = {};
563
+ for (const key of ["name", "id", "title", "path", "endpoint", "url", "type", "method", "label", "slug", "status"]) {
564
+ if (key in obj) picked[key] = obj[key];
565
+ }
566
+ return Object.keys(picked).length > 0 ? picked : Object.keys(obj).slice(0, 4).reduce((acc, k) => ({ ...acc, [k]: obj[k] }), {});
567
+ }
568
+ return item;
569
+ };
570
+ const compactItems = cleaned.map(pickFields);
571
+ let itemsToShow = compactItems;
572
+ let summary = JSON.stringify({ total_count: total, items: itemsToShow });
573
+ if (summary.length > maxChars) {
574
+ let lo = 1, hi = compactItems.length;
575
+ while (lo < hi) {
576
+ const mid = Math.ceil((lo + hi) / 2);
577
+ const test = JSON.stringify({ total_count: total, items_shown: mid, items: compactItems.slice(0, mid) });
578
+ if (test.length <= maxChars) {
579
+ lo = mid;
580
+ } else {
581
+ hi = mid - 1;
582
+ }
583
+ }
584
+ itemsToShow = compactItems.slice(0, lo);
585
+ summary = JSON.stringify({ total_count: total, items_shown: lo, items: itemsToShow });
586
+ }
587
+ return summary.length <= maxChars ? summary : summary.slice(0, maxChars) + "...";
588
+ }
589
+ if (typeof cleaned === "object" && cleaned !== null) {
590
+ const keys = Object.keys(cleaned);
591
+ const summary = JSON.stringify({ keys, preview: json.slice(0, maxChars - 100) });
592
+ return summary.slice(0, maxChars);
593
+ }
594
+ return json.slice(0, maxChars);
595
+ } catch {
596
+ return "";
597
+ }
598
+ }
599
+ /** Recursively strip fields with sensitive-looking keys (defense-in-depth). */
600
+ _stripSensitiveFields(data, depth = 0) {
601
+ if (depth > 5) return data;
602
+ if (Array.isArray(data)) {
603
+ return data.slice(0, 50).map((item) => this._stripSensitiveFields(item, depth + 1));
604
+ }
605
+ if (typeof data === "object" && data !== null) {
606
+ const result = {};
607
+ for (const [key, value] of Object.entries(data)) {
608
+ result[key] = SENSITIVE_KEY_PATTERN.test(key) ? "[REDACTED]" : this._stripSensitiveFields(value, depth + 1);
609
+ }
610
+ return result;
611
+ }
612
+ return data;
613
+ }
614
+ /** Clear all conversation history. */
615
+ clear() {
616
+ try {
617
+ this.store.removeItem(this.storageKey);
618
+ } catch {
619
+ }
620
+ }
621
+ /** Check if there is any conversation history. */
622
+ hasHistory() {
623
+ return this.getMessages().length > 0;
624
+ }
625
+ _save(messages) {
626
+ try {
627
+ const data = {
628
+ messages,
629
+ updated_at: Date.now()
630
+ };
631
+ this.store.setItem(this.storageKey, JSON.stringify(data));
632
+ } catch {
633
+ }
634
+ }
635
+ }
636
+ function encodeParam(value) {
637
+ return encodeURIComponent(value).replace(/%20/g, "+").replace(/[!'()~]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
638
+ }
639
+ const MAX_ERROR_DETAIL_CHARS = 400;
640
+ const MAX_ERROR_DETAIL_ITEMS = 5;
641
+ class ToolExecutor {
642
+ /**
643
+ * @param transport How this platform reaches the customer's API.
644
+ * Defaults to the browser's, so existing callers are unaffected.
645
+ */
646
+ constructor(authConfig, baseUrl, credentials, transport) {
647
+ this.credentials = credentials;
648
+ this.transport = transport;
649
+ this.authConfig = authConfig;
650
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
651
+ }
652
+ /** Read the user's auth token from the configured browser storage. */
653
+ getAuthToken() {
654
+ var _a, _b;
655
+ if (this.authConfig.type === "none") return null;
656
+ try {
657
+ const supplied = this.authConfig.type === "bearer" ? (_b = (_a = this.credentials).authToken) == null ? void 0 : _b.call(_a) : null;
658
+ if (supplied) return supplied;
659
+ let raw = null;
660
+ const storageKey = this.authConfig.key;
661
+ if (storageKey && this.authConfig.source) {
662
+ raw = this.credentials.read(this.authConfig.source, storageKey);
663
+ }
664
+ if (raw) {
665
+ const token = this._extractToken(raw);
666
+ if (token) return token;
667
+ }
668
+ return null;
669
+ } catch {
670
+ return null;
671
+ }
672
+ }
673
+ /**
674
+ * Extract the actual JWT from a raw storage value.
675
+ *
676
+ * Handles all common storage formats automatically:
677
+ * - Plain JWT string: "eyJhbGciOi..."
678
+ * - JSON with known field: {"access_token": "eyJ..."} or {"token": "eyJ..."}
679
+ * - JSON with any string field that looks like a JWT
680
+ *
681
+ * If token_field is configured, it takes priority and extracts that specific field.
682
+ */
683
+ _extractToken(raw) {
684
+ if (raw.startsWith("eyJ")) return raw;
685
+ try {
686
+ const parsed = JSON.parse(raw);
687
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
688
+ if (this.authConfig.token_field) {
689
+ const value = this._readObjectPath(parsed, this.authConfig.token_field);
690
+ return typeof value === "string" ? value : null;
691
+ }
692
+ const commonFields = ["access_token", "token", "jwt", "id_token", "auth_token", "accessToken", "idToken", "authToken"];
693
+ for (const field of commonFields) {
694
+ if (typeof parsed[field] === "string" && parsed[field]) {
695
+ return parsed[field];
696
+ }
697
+ }
698
+ for (const value of Object.values(parsed)) {
699
+ if (typeof value === "string" && value.startsWith("eyJ")) {
700
+ return value;
701
+ }
702
+ }
703
+ }
704
+ if (typeof parsed === "string") return parsed;
705
+ } catch {
706
+ }
707
+ return raw;
708
+ }
709
+ /** Execute an API tool call client-side using the user's auth. */
710
+ async execute(toolCall) {
711
+ try {
712
+ const method = toolCall.method.toUpperCase();
713
+ const unresolved = [
714
+ ...this._unresolvedPathParams(toolCall),
715
+ ...this._unresolvedHeaders(toolCall)
716
+ ];
717
+ if (unresolved.length > 0) {
718
+ return {
719
+ success: false,
720
+ error: `Missing ${unresolved.join(", ")} — this needs data from another request first.`
721
+ };
722
+ }
723
+ const url = this._buildUrl(toolCall);
724
+ const headers = this._buildHeaders(toolCall);
725
+ let body;
726
+ if (toolCall.body && method !== "GET") {
727
+ body = JSON.stringify(toolCall.body);
728
+ } else if (toolCall.params && method !== "GET") {
729
+ body = JSON.stringify(toolCall.params);
730
+ }
731
+ const response = await this.transport.request({
732
+ url,
733
+ // The tool's own verb, not a coerced one: rewriting a DELETE as a
734
+ // POST would silently change what the customer's API is asked to do.
735
+ method,
736
+ headers,
737
+ body
738
+ });
739
+ let data;
740
+ const contentType = response.headers.get("content-type") || "";
741
+ if (contentType.includes("application/json")) {
742
+ data = await response.json();
743
+ } else {
744
+ data = await response.text();
745
+ }
746
+ const ok = response.status >= 200 && response.status < 300;
747
+ return {
748
+ success: ok,
749
+ data,
750
+ status: response.status,
751
+ error: ok ? void 0 : response.status === 401 || response.status === 403 ? "Your session has expired. Please refresh the page or log in again." : this._describeHttpError(response.status, data)
752
+ };
753
+ } catch (err) {
754
+ return {
755
+ success: false,
756
+ error: err instanceof Error ? err.message : "Unknown error executing tool call"
757
+ };
758
+ }
759
+ }
760
+ /**
761
+ * Turn a failed response into an error the model can act on.
762
+ *
763
+ * "HTTP 422: Unprocessable Entity" tells nobody which field was wrong, so a
764
+ * rejected write used to end the chain with a status line the user could do
765
+ * nothing with. The body almost always names the problem — FastAPI puts a
766
+ * loc/msg list in `detail`, most other APIs use `message`, `error` or an
767
+ * `errors` array — so surface that alongside the status.
768
+ */
769
+ _describeHttpError(status, data) {
770
+ const base = `HTTP ${status}`;
771
+ const detail = this._extractErrorDetail(data);
772
+ return detail ? `${base} — ${detail}` : base;
773
+ }
774
+ _extractErrorDetail(data) {
775
+ if (typeof data === "string") {
776
+ const text = data.trim();
777
+ return text ? text.slice(0, MAX_ERROR_DETAIL_CHARS) : null;
778
+ }
779
+ if (!data || typeof data !== "object") return null;
780
+ const body = data;
781
+ const parts = [];
782
+ for (const key of ["detail", "message", "error", "errors", "error_description"]) {
783
+ const value = body[key];
784
+ if (value === void 0 || value === null) continue;
785
+ if (typeof value === "string") {
786
+ if (value.trim()) parts.push(value.trim());
787
+ } else if (Array.isArray(value)) {
788
+ for (const entry of value.slice(0, MAX_ERROR_DETAIL_ITEMS)) {
789
+ const described = this._describeFieldError(entry);
790
+ if (described) parts.push(described);
791
+ }
792
+ } else if (typeof value === "object") {
793
+ for (const [field, msg] of Object.entries(value).slice(0, MAX_ERROR_DETAIL_ITEMS)) {
794
+ const text = Array.isArray(msg) ? msg.join(", ") : String(msg);
795
+ parts.push(`${field}: ${text}`);
796
+ }
797
+ }
798
+ if (parts.length > 0) break;
799
+ }
800
+ if (parts.length === 0) return null;
801
+ return parts.join("; ").slice(0, MAX_ERROR_DETAIL_CHARS);
802
+ }
803
+ /** One entry of a validation-error list, e.g. FastAPI's {loc, msg, type}. */
804
+ _describeFieldError(entry) {
805
+ var _a, _b, _c, _d, _e;
806
+ if (typeof entry === "string") return entry.trim() || null;
807
+ if (!entry || typeof entry !== "object") return null;
808
+ const item = entry;
809
+ const message = (_c = (_b = (_a = item.msg) != null ? _a : item.message) != null ? _b : item.error) != null ? _c : item.detail;
810
+ const location = Array.isArray(item.loc) ? item.loc.filter((part) => part !== "body").join(".") : (_e = (_d = item.field) != null ? _d : item.param) != null ? _e : item.name;
811
+ if (!message) return location ? `${location}: invalid` : null;
812
+ return location ? `${location}: ${message}` : String(message);
813
+ }
814
+ /**
815
+ * Execute several independent tool calls concurrently.
816
+ *
817
+ * The model batches only calls that do not depend on each other's results,
818
+ * so a batch of four costs roughly one call's wall-clock time instead of
819
+ * four sequential round trips. Results come back in request order.
820
+ *
821
+ * A rejected promise is impossible here — `execute` already converts thrown
822
+ * errors into a failed ToolExecutionResult — but the settled form is used
823
+ * anyway so one exception can never discard the whole batch.
824
+ */
825
+ async executeBatch(toolCalls, onEach) {
826
+ const settled = await Promise.allSettled(
827
+ toolCalls.map(async (toolCall) => {
828
+ const result = await this.execute(toolCall);
829
+ onEach == null ? void 0 : onEach(toolCall, result);
830
+ return result;
831
+ })
832
+ );
833
+ return settled.map((outcome, i) => {
834
+ if (outcome.status === "fulfilled") return outcome.value;
835
+ const result = {
836
+ success: false,
837
+ error: outcome.reason instanceof Error ? outcome.reason.message : "Unknown error executing tool call"
838
+ };
839
+ onEach == null ? void 0 : onEach(toolCalls[i], result);
840
+ return result;
841
+ });
842
+ }
843
+ _buildHeaders(toolCall) {
844
+ var _a;
845
+ const headers = {
846
+ "Content-Type": "application/json",
847
+ Accept: "application/json"
848
+ };
849
+ const blocked = /* @__PURE__ */ new Set([
850
+ "authorization",
851
+ "cookie",
852
+ "host",
853
+ "origin",
854
+ "referer",
855
+ "x-forwarded-for",
856
+ "x-real-ip",
857
+ "content-length",
858
+ "transfer-encoding",
859
+ "connection"
860
+ ]);
861
+ const canonical = /* @__PURE__ */ new Map();
862
+ const mergeHeader = (name, value) => {
863
+ const lower = name.toLowerCase();
864
+ if (!value || blocked.has(lower)) return;
865
+ const existing = canonical.get(lower);
866
+ if (existing !== void 0 && existing !== name) delete headers[existing];
867
+ canonical.set(lower, name);
868
+ headers[name] = value;
869
+ };
870
+ for (const name of Object.keys(headers)) canonical.set(name.toLowerCase(), name);
871
+ if (this.authConfig.type === "none") ;
872
+ else if (this.authConfig.type === "bearer") {
873
+ const token = this.getAuthToken();
874
+ if (!token) {
875
+ throw new Error("No auth token found in browser storage. The user may not be logged in.");
876
+ }
877
+ headers.Authorization = `Bearer ${token}`;
878
+ } else if (this.authConfig.type === "api_key" && this.authConfig.key && this.authConfig.header) {
879
+ mergeHeader(this.authConfig.header, this.authConfig.key);
880
+ }
881
+ if (this.authConfig.headers && typeof this.authConfig.headers === "object") {
882
+ for (const [name, rawValue] of Object.entries(this.authConfig.headers)) {
883
+ mergeHeader(name, this._resolveHeaderValue(rawValue));
884
+ }
885
+ }
886
+ if (toolCall.headers && typeof toolCall.headers === "object") {
887
+ for (const [name, value] of Object.entries(toolCall.headers)) {
888
+ mergeHeader(name, value);
889
+ }
890
+ }
891
+ for (const declared of (_a = this.authConfig.app_headers) != null ? _a : []) {
892
+ if (!(declared == null ? void 0 : declared.name)) continue;
893
+ let value = null;
894
+ try {
895
+ value = this.credentials.read("app", declared.name);
896
+ } catch {
897
+ value = null;
898
+ }
899
+ mergeHeader(declared.name, value);
900
+ }
901
+ return headers;
902
+ }
903
+ _resolveHeaderValue(value) {
904
+ if (typeof value === "string") return value;
905
+ let raw = null;
906
+ try {
907
+ raw = this.credentials.read(value.source, value.key);
908
+ } catch {
909
+ return null;
910
+ }
911
+ if (!raw) return null;
912
+ if (value.token_field) {
913
+ try {
914
+ const parsed = JSON.parse(raw);
915
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
916
+ const selected = this._readObjectPath(parsed, value.token_field);
917
+ return typeof selected === "string" ? selected : null;
918
+ }
919
+ } catch {
920
+ return raw;
921
+ }
922
+ }
923
+ return this._extractToken(raw) || raw;
924
+ }
925
+ /**
926
+ * Path template segments that `_buildUrl` would leave unsubstituted.
927
+ *
928
+ * A value that is itself a placeholder ("{id}", "<site_id>", "TBD") counts
929
+ * as unresolved too — substituting it would produce a URL that looks valid
930
+ * but addresses nothing.
931
+ */
932
+ _unresolvedPathParams(toolCall) {
933
+ const segments = (toolCall.path || "").match(/\{(\w+)\}/g) || [];
934
+ if (segments.length === 0) return [];
935
+ const params = toolCall.params || {};
936
+ const placeholder = /^\s*(?:[{<[].*[}>\]]|[$:].+|tbd|todo|placeholder|unknown|none|null|n\/a)\s*$/i;
937
+ return segments.map((segment) => segment.slice(1, -1)).filter((name) => {
938
+ const value = params[name];
939
+ if (value === void 0 || value === null || value === "") return true;
940
+ return typeof value === "string" && placeholder.test(value);
941
+ });
942
+ }
943
+ /**
944
+ * Headers whose value is still a placeholder the model never filled in.
945
+ *
946
+ * Worse than an unresolved path segment: a scoping header like X-Site-Id
947
+ * sent as "{site_id}" does not 404, it answers about whatever the default
948
+ * record is. Failing the call here is what makes the mistake visible.
949
+ */
950
+ _unresolvedHeaders(toolCall) {
951
+ const headers = toolCall.headers;
952
+ if (!headers) return [];
953
+ const placeholder = /^\s*(?:[{<[].*[}>\]]|[$:].+|tbd|todo|placeholder|unknown|none|null|n\/a)\s*$/i;
954
+ return Object.entries(headers).filter(([, value]) => typeof value === "string" && placeholder.test(value)).map(([name]) => name);
955
+ }
956
+ _buildUrl(toolCall) {
957
+ let path = toolCall.path;
958
+ const method = toolCall.method.toUpperCase();
959
+ if (toolCall.params) {
960
+ for (const [key, value] of Object.entries(toolCall.params)) {
961
+ const placeholder = `{${key}}`;
962
+ if (path.includes(placeholder)) {
963
+ path = path.replace(placeholder, encodeURIComponent(String(value)));
964
+ }
965
+ }
966
+ }
967
+ let url;
968
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path)) {
969
+ url = path;
970
+ } else {
971
+ const origin = this._origin(this.baseUrl);
972
+ url = path.startsWith("/") ? `${origin}${path}` : `${this.baseUrl}/${path}`;
973
+ }
974
+ if (toolCall.params && method === "GET") {
975
+ const pairs = [];
976
+ for (const [key, value] of Object.entries(toolCall.params)) {
977
+ if (!toolCall.path.includes(`{${key}}`)) {
978
+ pairs.push(`${encodeParam(key)}=${encodeParam(String(value))}`);
979
+ }
980
+ }
981
+ if (pairs.length > 0) {
982
+ url += (url.includes("?") ? "&" : "?") + pairs.join("&");
983
+ }
984
+ }
985
+ return url;
986
+ }
987
+ /** Scheme + authority of a base URL, without its path. */
988
+ _origin(base) {
989
+ const match = /^([a-z][a-z0-9+.-]*:\/\/[^/]+)/i.exec(base);
990
+ return match ? match[1] : base;
991
+ }
992
+ _readObjectPath(source, path) {
993
+ const segments = path.split(".").map((segment) => segment.trim()).filter(Boolean);
994
+ let current = source;
995
+ for (const segment of segments) {
996
+ if (!current || typeof current !== "object" || Array.isArray(current)) {
997
+ return void 0;
998
+ }
999
+ current = current[segment];
1000
+ }
1001
+ return current;
1002
+ }
1003
+ }
1004
+ class AuthError extends Error {
1005
+ constructor(status) {
1006
+ super("Your session has expired. Please refresh the page or log in again.");
1007
+ this.name = "AuthError";
1008
+ this.status = status;
1009
+ }
1010
+ }
1011
+ class UsageLimitError extends Error {
1012
+ constructor(detail) {
1013
+ super(detail.message);
1014
+ this.name = "UsageLimitError";
1015
+ this.status = 429;
1016
+ this.limitType = detail.limit_type || "unknown";
1017
+ this.plan = detail.plan || "unknown";
1018
+ this.current = detail.current || 0;
1019
+ this.limit = detail.limit || 0;
1020
+ this.upgradeHint = detail.upgrade_hint || "";
1021
+ }
1022
+ }
1023
+ class PlanAccessError extends Error {
1024
+ constructor(detail) {
1025
+ super(detail.message || "This feature is not available on the current plan.");
1026
+ this.status = 402;
1027
+ this.name = "PlanAccessError";
1028
+ this.feature = detail.feature || "unknown";
1029
+ this.plan = detail.plan || "unknown";
1030
+ this.upgradeHint = detail.upgrade_hint || "";
1031
+ }
1032
+ }
1033
+ const _j = (parts) => parts.join("/");
1034
+ const _v = ["", "api", "v1"];
1035
+ const EP = {
1036
+ feedback: () => _j([..._v, "ml", "feedback"]),
1037
+ weights: () => _j([..._v, "ml", "weights"]),
1038
+ modelInfo: () => _j([..._v, "ml", "model-info"]),
1039
+ guides: () => _j([..._v, "guides", "published"]),
1040
+ guidesRecord: () => _j([..._v, "guides", "record"]),
1041
+ events: () => _j([..._v, "events"]),
1042
+ chatAsk: () => _j([..._v, "chat", "ask"]),
1043
+ chatExecuteTool: () => _j([..._v, "chat", "execute-tool"]),
1044
+ onboardingCurrent: (userId) => `${_j([..._v, "onboarding", "current"])}?user_id=${encodeURIComponent(userId)}`,
1045
+ onboardingObserve: () => _j([..._v, "onboarding", "observe"]),
1046
+ onboardingDismiss: () => _j([..._v, "onboarding", "dismiss"]),
1047
+ onboardingEnroll: () => _j([..._v, "onboarding", "enroll"]),
1048
+ onboardingSkip: () => _j([..._v, "onboarding", "skip"]),
1049
+ onboardingComplete: () => _j([..._v, "onboarding", "complete"]),
1050
+ onboardingWatched: () => _j([..._v, "onboarding", "watched"]),
1051
+ demoSearch: () => _j([..._v, "demos", "search"]),
1052
+ chatRoute: () => _j([..._v, "chat", "route"]),
1053
+ chatOpener: () => _j([..._v, "chat", "opener"]),
1054
+ chatOpenerSpeech: () => _j([..._v, "chat", "opener", "speech"]),
1055
+ chatIntro: () => _j([..._v, "chat", "intro"]),
1056
+ chatEscalate: () => _j([..._v, "chat", "escalate"]),
1057
+ chatEscalationConfig: () => _j([..._v, "chat", "escalate", "config"]),
1058
+ chatConversationWebhook: () => _j([..._v, "chat", "conversation-webhook"]),
1059
+ chatFeedback: () => _j([..._v, "chat", "feedback"]),
1060
+ coachScreen: () => _j([..._v, "chat", "coach", "screen"]),
1061
+ coachAsk: () => _j([..._v, "chat", "coach", "ask"]),
1062
+ coachExplain: () => _j([..._v, "chat", "coach", "explain"]),
1063
+ coachReplan: () => _j([..._v, "chat", "coach", "replan"]),
1064
+ coachConfirm: () => _j([..._v, "chat", "coach", "confirm"]),
1065
+ coachSpeech: () => _j([..._v, "chat", "coach", "speech"]),
1066
+ paymentCheckout: () => _j([..._v, "connectors", "payments", "checkout"]),
1067
+ chatContinue: () => _j([..._v, "chat", "continue-tool-chain"]),
1068
+ /** A Do it run: plan a screen, act, look again (the agent service). */
1069
+ doItRuns: () => _j([..._v, "guide-executions"]),
1070
+ doItRun: (runId, op) => _j([..._v, "guide-executions", encodeURIComponent(runId), ...op ? [op] : []]),
1071
+ chatFormat: () => _j([..._v, "chat", "format-tool-result"]),
1072
+ copilotTools: () => _j([..._v, "copilot-tools", "active"]),
1073
+ copilotStream: () => _j([..._v, "copilot", "stream"]),
1074
+ resolveScore: () => _j([..._v, "resolve", "score"]),
1075
+ resolveLocate: () => _j([..._v, "resolve", "locate"]),
1076
+ resolveRoute: () => _j([..._v, "resolve", "route"]),
1077
+ resolveGraph: () => _j([..._v, "resolve", "graph"]),
1078
+ visitors: () => _j([..._v, "visitors", "register"]),
1079
+ announcements: () => _j([..._v, "announcements"]),
1080
+ pageScan: () => _j([..._v, "page-scan", "scan"]),
1081
+ pageScanReveal: () => _j([..._v, "page-scan", "reveal"]),
1082
+ supportStatus: () => _j([..._v, "support", "public", "status"]),
1083
+ supportConvs: () => _j([..._v, "support", "public", "conversations"]),
1084
+ supportMsgs: (conversationId) => _j([..._v, "support", "public", "conversations", conversationId, "messages"]),
1085
+ realtimeSession: () => _j([..._v, "realtime", "session"]),
1086
+ realtimeTranscriptionSession: () => _j([..._v, "realtime", "transcription", "session"]),
1087
+ realtimeSpeech: () => _j([..._v, "realtime", "speech"]),
1088
+ listenSession: () => _j([..._v, "realtime", "listen", "session"]),
1089
+ speakSession: () => _j([..._v, "realtime", "speak", "session"]),
1090
+ liveSession: () => _j([..._v, "realtime", "live", "session"]),
1091
+ // v=2: neutral voice codes (stream / pipeline / live / live-rtc).
1092
+ voiceCapabilities: () => `${_j([..._v, "realtime", "capabilities"])}?v=2`,
1093
+ liveVoiceMinute: () => _j([..._v, "realtime", "live", "minute"]),
1094
+ batchVoiceTranscribe: () => _j([..._v, "realtime", "batch", "transcribe"]),
1095
+ documentFormSuggestions: () => _j([..._v, "realtime", "document", "form-suggestions"])
1096
+ };
1097
+ const MAX_CHAIN_ROUNDS = 5;
1098
+ const MAX_TOTAL_TOOL_CALLS = 10;
1099
+ const MAX_REPAIR_ROUNDS = 1;
1100
+ const REPAIRABLE_ERROR_TYPES = /* @__PURE__ */ new Set(["validation", "client", "not_found"]);
1101
+ function isServerTool(toolCall) {
1102
+ return (toolCall.method || "").toUpperCase() === "MCP";
1103
+ }
1104
+ class CopilotHarness {
1105
+ /**
1106
+ * @param tools How this platform actually touches the screen. Defaults to
1107
+ * the browser registry so every existing caller is unaffected; a mobile
1108
+ * runtime passes one that dispatches native effects instead. The loop
1109
+ * below never learns which it is holding.
1110
+ */
1111
+ constructor(api, config, tools, storage, transport, credentials) {
1112
+ this.toolExecutor = null;
1113
+ this.activeTools = null;
1114
+ this.initialized = false;
1115
+ this.lastPlanFingerprint = null;
1116
+ this.api = api;
1117
+ this.config = config;
1118
+ this.tools = tools;
1119
+ this.transport = transport;
1120
+ this.credentials = credentials;
1121
+ this.memory = new ConversationMemory(
1122
+ config.siteId,
1123
+ config.memoryTtlMs,
1124
+ config.maxMemoryMessages,
1125
+ storage
1126
+ );
1127
+ this.validator = new HarnessValidator();
1128
+ this.validator.setRegisteredTools(this.tools.copilotDefinitions());
1129
+ }
1130
+ /** Hear every turn the conversation records (typed, spoken, replies). */
1131
+ onMessageRecorded(listener) {
1132
+ this.memory.onMessage(listener);
1133
+ }
1134
+ /** Initialize the harness — fetch active tools for the site. */
1135
+ async initialize() {
1136
+ if (this.initialized) return this.isEnabled();
1137
+ try {
1138
+ const response = await this.api.get(
1139
+ EP.copilotTools()
1140
+ );
1141
+ if (response && response.tools && response.tools.length > 0) {
1142
+ this.activeTools = response;
1143
+ this.validator.setRegisteredTools([
1144
+ ...response.tools,
1145
+ ...this.tools.copilotDefinitions()
1146
+ ]);
1147
+ this.toolExecutor = new ToolExecutor(
1148
+ response.auth_config,
1149
+ response.base_url,
1150
+ this.credentials,
1151
+ this.transport
1152
+ );
1153
+ }
1154
+ } catch {
1155
+ }
1156
+ this.initialized = true;
1157
+ return this.isEnabled();
1158
+ }
1159
+ /**
1160
+ * Register the listener that renders the assistant's plan.
1161
+ *
1162
+ * Set on the harness rather than passed per message: `processMessage` is
1163
+ * already called positionally from several places, and a plan belongs to
1164
+ * the UI for the whole session, not to one call.
1165
+ */
1166
+ setPlanCallback(callback) {
1167
+ this.onPlan = callback;
1168
+ }
1169
+ /**
1170
+ * Values the host page supplies for tool inputs.
1171
+ *
1172
+ * Sent with every turn, including chain rounds — a later round builds calls
1173
+ * too, and dropping it there is how a chain that started on the right
1174
+ * record finishes on the default one.
1175
+ */
1176
+ /** Bind to the SDK's analytics session. Carries no message content. */
1177
+ setAnalyticsSessionId(sessionId) {
1178
+ this.config.analyticsSessionId = sessionId;
1179
+ }
1180
+ /** Read the analytics bundle's ids on every question. Ids only. */
1181
+ setAnalyticsIds(read) {
1182
+ this.config.analyticsIds = read;
1183
+ }
1184
+ setToolContext(values) {
1185
+ this.toolContext = values && Object.keys(values).length > 0 ? values : void 0;
1186
+ }
1187
+ /**
1188
+ * The app's tool inputs as the API names them. A value declared in Copilot
1189
+ * Tools as filling other inputs (app key `course` fills `content_id`) is
1190
+ * sent under those names too; one the app set by that name already wins.
1191
+ */
1192
+ contextForTools() {
1193
+ var _a, _b, _c, _d;
1194
+ if (!this.toolContext) return void 0;
1195
+ const out = { ...this.toolContext };
1196
+ for (const input of (_c = (_b = (_a = this.activeTools) == null ? void 0 : _a.auth_config) == null ? void 0 : _b.app_inputs) != null ? _c : []) {
1197
+ const value = (input == null ? void 0 : input.key) ? this.toolContext[input.key] : void 0;
1198
+ if (value === void 0) continue;
1199
+ for (const name of (_d = input.fills) != null ? _d : []) {
1200
+ if (name && !(name in out)) out[name] = value;
1201
+ }
1202
+ }
1203
+ return out;
1204
+ }
1205
+ /** Whether copilot action tools are available for this site. */
1206
+ isEnabled() {
1207
+ return this.tools.definitions().length > 0 || this.activeTools !== null && this.activeTools.tools.length > 0;
1208
+ }
1209
+ /**
1210
+ * Process a user message through the harness.
1211
+ *
1212
+ * 1. Sends question + history + page context to backend /ask
1213
+ * 2. If backend returns SSE stream (agent loop running), reads events in real-time
1214
+ * 3. Calls onStatus callback with progress updates during the loop
1215
+ * 4. Returns the final response when the stream completes
1216
+ * 5. For non-tool queries, returns JSON response directly
1217
+ */
1218
+ async processMessage(question, pageUrl, pageSnapshot, chatMode, onStatus, onTextDelta, onToolCallEvent, abortSignal, userContext, onboardingUserId) {
1219
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j2, _k;
1220
+ const inputCheck = this.validator.validateUserInput(question);
1221
+ if (!inputCheck.valid) {
1222
+ return this._errorResponse(inputCheck.error || "Invalid input.");
1223
+ }
1224
+ const sanitizedQuestion = inputCheck.sanitized || question;
1225
+ this.lastPlanFingerprint = null;
1226
+ this.currentPlan = void 0;
1227
+ this.memory.addMessage({
1228
+ role: "user",
1229
+ content: sanitizedQuestion,
1230
+ timestamp: Date.now()
1231
+ });
1232
+ const conversationHistory = this.memory.getRecentForLLM(10);
1233
+ const mayForwardToken = ((_b = (_a = this.activeTools) == null ? void 0 : _a.auth_config) == null ? void 0 : _b.forward_user_token) === true;
1234
+ const userToken = mayForwardToken ? (_d = (_c = this.toolExecutor) == null ? void 0 : _c.getAuthToken()) != null ? _d : null : null;
1235
+ let analytics = {};
1236
+ try {
1237
+ analytics = (_g = (_f = (_e = this.config).analyticsIds) == null ? void 0 : _f.call(_e)) != null ? _g : {};
1238
+ } catch {
1239
+ }
1240
+ const requestBody = {
1241
+ question: sanitizedQuestion,
1242
+ page_url: pageUrl,
1243
+ session_id: analytics.session_id || this.config.analyticsSessionId || void 0,
1244
+ distinct_id: analytics.distinct_id || void 0,
1245
+ page_snapshot: pageSnapshot,
1246
+ chat_mode: chatMode || "guidance",
1247
+ assistant_label: ((_h = this.config.assistantLabel) == null ? void 0 : _h.trim()) || void 0,
1248
+ delegate_instruction: ((_i = this.config.delegationInstruction) == null ? void 0 : _i.trim().slice(0, 1e3)) || void 0,
1249
+ conversation_history: conversationHistory,
1250
+ stream: this.api.supportsStreaming,
1251
+ user_token: userToken || void 0,
1252
+ prefer_client_tool_execution: !!this.toolExecutor || this.tools.definitions().length > 0,
1253
+ client_tools: chatMode === "assistant" ? this.tools.definitions() : void 0,
1254
+ user_context: userContext,
1255
+ onboarding_user_id: onboardingUserId,
1256
+ tool_context: this.contextForTools(),
1257
+ client_capabilities: ((_j2 = this.config.clientCapabilities) == null ? void 0 : _j2.length) ? this.config.clientCapabilities : void 0
1258
+ };
1259
+ let rawResponse;
1260
+ try {
1261
+ rawResponse = await this.api.postRaw(EP.chatAsk(), requestBody, abortSignal);
1262
+ } catch (err) {
1263
+ if (abortSignal == null ? void 0 : abortSignal.aborted) {
1264
+ return this._errorResponse("Response cancelled.");
1265
+ }
1266
+ if (err instanceof AuthError) {
1267
+ return this._errorResponse(err.message);
1268
+ }
1269
+ if (err instanceof UsageLimitError || err instanceof PlanAccessError) {
1270
+ throw err;
1271
+ }
1272
+ return this._errorResponse("Failed to get response from server.");
1273
+ }
1274
+ const contentType = rawResponse.headers.get("content-type") || "";
1275
+ if (contentType.includes("text/event-stream")) {
1276
+ return this._readSSEStream(
1277
+ rawResponse,
1278
+ sanitizedQuestion,
1279
+ onStatus,
1280
+ onTextDelta,
1281
+ onToolCallEvent,
1282
+ abortSignal
1283
+ );
1284
+ }
1285
+ let response;
1286
+ try {
1287
+ response = await rawResponse.json();
1288
+ } catch {
1289
+ return this._errorResponse("Failed to parse server response.");
1290
+ }
1291
+ if (onTextDelta && response.answer) {
1292
+ this._emitAnswerText(response.answer, onTextDelta, abortSignal);
1293
+ }
1294
+ const harnessResponse = this._handleJsonResponse(response, chatMode);
1295
+ if (harnessResponse.autoExecuteToolCall && harnessResponse.pendingToolCall) {
1296
+ return this._executePendingToolCall(
1297
+ (_k = harnessResponse.pendingToolCalls) != null ? _k : [harnessResponse.pendingToolCall],
1298
+ sanitizedQuestion,
1299
+ onStatus,
1300
+ onTextDelta,
1301
+ onToolCallEvent
1302
+ );
1303
+ }
1304
+ return harnessResponse;
1305
+ }
1306
+ /**
1307
+ * Execute a confirmed tool call after user approval.
1308
+ * Called when user clicks "Yes, do it" for write/destructive actions.
1309
+ * Executes in the browser when possible, then formats the result with LLM.
1310
+ */
1311
+ async executeToolCall(toolCall, originalQuestion, onStatus, onTextDelta) {
1312
+ if (!this.canExecuteTool(toolCall)) {
1313
+ const errorMsg = "Tool execution is unavailable in this browser context.";
1314
+ this.memory.addMessage({
1315
+ role: "assistant",
1316
+ content: errorMsg,
1317
+ timestamp: Date.now()
1318
+ });
1319
+ return {
1320
+ mode: "answer",
1321
+ answer: errorMsg,
1322
+ toolResult: {
1323
+ success: false,
1324
+ error: errorMsg
1325
+ },
1326
+ autoExecute: false
1327
+ };
1328
+ }
1329
+ const result = await this.executeLocalTool(toolCall);
1330
+ const resultCheck = this.validator.validateToolResultEnhanced(result);
1331
+ if (!resultCheck.valid) {
1332
+ if (REPAIRABLE_ERROR_TYPES.has(resultCheck.errorType || "")) {
1333
+ onStatus == null ? void 0 : onStatus("That did not go through — adjusting the request...");
1334
+ const repaired = await this._continueToolChain(
1335
+ toolCall.tool,
1336
+ { error: resultCheck.detail || result.error || resultCheck.error },
1337
+ originalQuestion,
1338
+ [{
1339
+ tool_name: toolCall.tool,
1340
+ tool_result: { error: resultCheck.detail || result.error || resultCheck.error }
1341
+ }],
1342
+ onStatus,
1343
+ onTextDelta
1344
+ );
1345
+ if (repaired.pendingToolCall || repaired.answer) {
1346
+ return repaired;
1347
+ }
1348
+ }
1349
+ const errorMsg = resultCheck.error || "Tool execution failed.";
1350
+ this.memory.addMessage({
1351
+ role: "assistant",
1352
+ content: errorMsg,
1353
+ toolResult: result,
1354
+ timestamp: Date.now()
1355
+ });
1356
+ return {
1357
+ mode: "answer",
1358
+ answer: errorMsg,
1359
+ toolResult: result,
1360
+ autoExecute: false
1361
+ };
1362
+ }
1363
+ if (resultCheck.warning) {
1364
+ this.memory.addMessage({
1365
+ role: "assistant",
1366
+ content: resultCheck.warning,
1367
+ toolResult: result,
1368
+ timestamp: Date.now()
1369
+ });
1370
+ return {
1371
+ mode: "answer",
1372
+ answer: resultCheck.warning,
1373
+ toolResult: result,
1374
+ autoExecute: false
1375
+ };
1376
+ }
1377
+ return this._formatToolResult(
1378
+ toolCall.tool,
1379
+ result.data,
1380
+ originalQuestion,
1381
+ onStatus,
1382
+ onTextDelta
1383
+ );
1384
+ }
1385
+ /** Alias for executeToolCall. */
1386
+ async confirmAndExecute(toolCall, originalQuestion) {
1387
+ return this.executeToolCall(toolCall, originalQuestion);
1388
+ }
1389
+ /** Get conversation history for display in chat. */
1390
+ getConversationHistory() {
1391
+ return this.memory.getMessages();
1392
+ }
1393
+ /** Recent redacted context for semantic conversational follow-ups. */
1394
+ getRecentConversationContext(count = 8) {
1395
+ return this.memory.getRecentForLLM(count);
1396
+ }
1397
+ /** Persist a completed Realtime turn so voice and text share one history. */
1398
+ recordRealtimeTurn(role, content) {
1399
+ const normalized = content.trim().slice(0, 4e3);
1400
+ if (!normalized) return;
1401
+ this.memory.addMessage({ role, content: normalized, timestamp: Date.now() });
1402
+ }
1403
+ /** Check if there is existing conversation history. */
1404
+ hasHistory() {
1405
+ return this.memory.hasHistory();
1406
+ }
1407
+ /** Clear conversation history. */
1408
+ clearHistory() {
1409
+ this.memory.clear();
1410
+ }
1411
+ // ----------------------------------------------------------------
1412
+ // Private methods
1413
+ // ----------------------------------------------------------------
1414
+ /**
1415
+ * Read SSE events from the backend agent loop stream.
1416
+ * Supports incremental text streaming, tool call display events, and cancellation.
1417
+ * Updates status via callback and resolves with the final response.
1418
+ */
1419
+ async _readSSEStream(rawResponse, originalQuestion, onStatus, onTextDelta, onToolCallEvent, abortSignal) {
1420
+ var _a, _b;
1421
+ if (!rawResponse.stream) {
1422
+ return this._handleJsonResponse(
1423
+ await rawResponse.json(),
1424
+ void 0
1425
+ );
1426
+ }
1427
+ let buffer = "";
1428
+ let finalResponse = null;
1429
+ let accumulatedText = "";
1430
+ const toolCallRecords = [];
1431
+ try {
1432
+ for await (const chunk of rawResponse.stream) {
1433
+ if (abortSignal == null ? void 0 : abortSignal.aborted) break;
1434
+ buffer += chunk;
1435
+ const eventBlocks = buffer.split("\n\n");
1436
+ buffer = eventBlocks.pop() || "";
1437
+ for (const block of eventBlocks) {
1438
+ const trimmedBlock = block.trim();
1439
+ if (!trimmedBlock) continue;
1440
+ for (const rawLine of trimmedBlock.split("\n")) {
1441
+ const line2 = rawLine.trim();
1442
+ if (!line2.startsWith("data: ")) continue;
1443
+ const jsonStr = line2.slice(6);
1444
+ let event;
1445
+ try {
1446
+ event = JSON.parse(jsonStr);
1447
+ } catch {
1448
+ continue;
1449
+ }
1450
+ switch (event.type) {
1451
+ case "status":
1452
+ onStatus == null ? void 0 : onStatus(event.message || "Processing...");
1453
+ break;
1454
+ case "text_delta":
1455
+ if (event.delta) {
1456
+ accumulatedText += event.delta;
1457
+ onTextDelta == null ? void 0 : onTextDelta(event.delta);
1458
+ }
1459
+ break;
1460
+ case "tool_call_start":
1461
+ if (event.tool) {
1462
+ onToolCallEvent == null ? void 0 : onToolCallEvent({
1463
+ type: "start",
1464
+ tool: event.tool,
1465
+ description: event.description
1466
+ });
1467
+ }
1468
+ onStatus == null ? void 0 : onStatus(event.description ? `${event.description}...` : "Looking up data...");
1469
+ break;
1470
+ case "tool_call_done":
1471
+ if (event.tool) {
1472
+ toolCallRecords.push({
1473
+ tool: event.tool,
1474
+ description: event.description || "",
1475
+ success: (_a = event.success) != null ? _a : true,
1476
+ summary: event.summary
1477
+ });
1478
+ onToolCallEvent == null ? void 0 : onToolCallEvent({
1479
+ type: "done",
1480
+ tool: event.tool,
1481
+ success: event.success,
1482
+ summary: event.summary
1483
+ });
1484
+ }
1485
+ break;
1486
+ case "tool_result":
1487
+ onStatus == null ? void 0 : onStatus(
1488
+ event.success ? `Got data from ${(event.tool || "").replace(/_/g, " ").slice(0, 40)}...` : "Tool call failed."
1489
+ );
1490
+ break;
1491
+ case "error":
1492
+ finalResponse = {
1493
+ mode: "answer",
1494
+ answer: event.message || "An error occurred.",
1495
+ autoExecute: false
1496
+ };
1497
+ break;
1498
+ case "done":
1499
+ if (event.response) {
1500
+ finalResponse = this._parseStreamedResponse(event.response);
1501
+ if (!finalResponse.answer && accumulatedText) {
1502
+ const stripped = this._stripToolCallArtifacts(accumulatedText);
1503
+ finalResponse.answer = stripped || "Here is the result of your request.";
1504
+ }
1505
+ }
1506
+ break;
1507
+ }
1508
+ }
1509
+ }
1510
+ }
1511
+ } catch (err) {
1512
+ if (abortSignal == null ? void 0 : abortSignal.aborted) {
1513
+ if (accumulatedText) {
1514
+ const stripped = this._stripToolCallArtifacts(accumulatedText);
1515
+ const partialResponse = {
1516
+ mode: "answer",
1517
+ answer: stripped || "Response cancelled.",
1518
+ autoExecute: false
1519
+ };
1520
+ this.memory.addMessage({
1521
+ role: "assistant",
1522
+ content: accumulatedText,
1523
+ toolCalls: toolCallRecords.length > 0 ? toolCallRecords : void 0,
1524
+ timestamp: Date.now()
1525
+ });
1526
+ return partialResponse;
1527
+ }
1528
+ return this._errorResponse("Response cancelled.");
1529
+ }
1530
+ console.error("[GuideAI] SSE stream error:", err);
1531
+ }
1532
+ if (!finalResponse && accumulatedText) {
1533
+ const stripped = this._stripToolCallArtifacts(accumulatedText);
1534
+ finalResponse = {
1535
+ mode: "answer",
1536
+ answer: stripped || "Here is the result of your request.",
1537
+ autoExecute: false
1538
+ };
1539
+ }
1540
+ if (finalResponse) {
1541
+ if (finalResponse.autoExecuteToolCall && finalResponse.pendingToolCall) {
1542
+ return this._executePendingToolCall(
1543
+ (_b = finalResponse.pendingToolCalls) != null ? _b : [finalResponse.pendingToolCall],
1544
+ originalQuestion,
1545
+ onStatus,
1546
+ onTextDelta,
1547
+ onToolCallEvent
1548
+ );
1549
+ }
1550
+ this.memory.addMessage({
1551
+ role: "assistant",
1552
+ content: finalResponse.answer,
1553
+ toolResult: finalResponse.toolResult,
1554
+ toolCalls: toolCallRecords.length > 0 ? toolCallRecords : void 0,
1555
+ guideSteps: finalResponse.guideSteps,
1556
+ guideTitle: finalResponse.guideTitle,
1557
+ navigation: finalResponse.navigation,
1558
+ mode: finalResponse.mode,
1559
+ confirmationNeeded: finalResponse.confirmationNeeded,
1560
+ pendingToolCall: finalResponse.pendingToolCall,
1561
+ demo: finalResponse.demo,
1562
+ timestamp: Date.now()
1563
+ });
1564
+ return finalResponse;
1565
+ }
1566
+ return this._errorResponse("Agent loop completed but no response was received.");
1567
+ }
1568
+ /**
1569
+ * Parse the ChatAskResponse from a streamed "done" event into HarnessResponse.
1570
+ */
1571
+ /**
1572
+ * Strip tool_call artifacts from streamed text so raw JSON is never shown.
1573
+ *
1574
+ * Handles:
1575
+ * - Fenced code blocks: ```tool_call\n{...}```
1576
+ * - Bare JSON objects with a "tool" key: {"tool":"list_guides","params":{}}
1577
+ * - Standalone mode: lines (mode: execute)
1578
+ */
1579
+ _stripToolCallArtifacts(text) {
1580
+ let stripped = text.replace(/<thinking\b[^>]*>[\s\S]*?<\/thinking>/gi, "").replace(/^\s*<\/?thinking\b[^>]*>\s*$/gim, "");
1581
+ stripped = stripped.replace(/```tool_\w+\s*\n[\s\S]*?```/g, "").replace(/```tool_\w+[\s\S]*?```/g, "");
1582
+ let result = "";
1583
+ let i = 0;
1584
+ while (i < stripped.length) {
1585
+ if (stripped[i] === "{") {
1586
+ let depth = 0;
1587
+ let j = i;
1588
+ while (j < stripped.length) {
1589
+ if (stripped[j] === "{") depth++;
1590
+ else if (stripped[j] === "}") {
1591
+ depth--;
1592
+ if (depth === 0) break;
1593
+ }
1594
+ j++;
1595
+ }
1596
+ if (depth === 0 && j < stripped.length) {
1597
+ const block = stripped.slice(i, j + 1);
1598
+ let parsed = null;
1599
+ try {
1600
+ parsed = JSON.parse(block);
1601
+ } catch {
1602
+ if (block.includes("{{")) {
1603
+ try {
1604
+ parsed = JSON.parse(block.replace(/\{\{/g, "{").replace(/\}\}/g, "}"));
1605
+ } catch {
1606
+ }
1607
+ }
1608
+ }
1609
+ if (parsed && typeof parsed === "object" && "tool" in parsed) {
1610
+ i = j + 1;
1611
+ continue;
1612
+ }
1613
+ }
1614
+ }
1615
+ result += stripped[i];
1616
+ i++;
1617
+ }
1618
+ result = result.replace(/^\s*mode:\s*\w+\s*$/gm, "");
1619
+ return result.trim();
1620
+ }
1621
+ _parseStreamedResponse(response) {
1622
+ var _a, _b, _c, _d, _e;
1623
+ const respCheck = this.validator.validateResponse(response);
1624
+ const answer = respCheck.sanitized || response.answer || "";
1625
+ if ((_a = response.tool_call) == null ? void 0 : _a.requires_confirm) {
1626
+ return {
1627
+ mode: "answer",
1628
+ answer,
1629
+ confirmationNeeded: true,
1630
+ pendingToolCall: response.tool_call,
1631
+ autoExecute: false
1632
+ };
1633
+ }
1634
+ if (response.tool_call) {
1635
+ return {
1636
+ mode: "answer",
1637
+ answer,
1638
+ pendingToolCall: response.tool_call,
1639
+ pendingToolCalls: ((_b = response.tool_calls) == null ? void 0 : _b.length) ? response.tool_calls : [response.tool_call],
1640
+ autoExecuteToolCall: true,
1641
+ autoExecute: false
1642
+ };
1643
+ }
1644
+ let guideSteps = response.guide_steps || void 0;
1645
+ if (guideSteps && guideSteps.length > 0) {
1646
+ const stepCheck = this.validator.validateGuideSteps(guideSteps);
1647
+ guideSteps = stepCheck.valid ? stepCheck.steps : void 0;
1648
+ }
1649
+ let toolResult;
1650
+ if (response.tool_result) {
1651
+ toolResult = {
1652
+ success: (_c = response.tool_result.success) != null ? _c : true,
1653
+ data: response.tool_result.data,
1654
+ error: response.tool_result.error,
1655
+ status: response.tool_result.status
1656
+ };
1657
+ }
1658
+ return {
1659
+ mode: response.mode || "answer",
1660
+ answer,
1661
+ intent: response.intent,
1662
+ intentReason: response.intent_reason || void 0,
1663
+ guideSteps,
1664
+ guideTitle: response.guide_title || void 0,
1665
+ navigation: response.navigation || void 0,
1666
+ toolResult,
1667
+ autoExecute: false,
1668
+ // disabled — never auto-click/scroll/fill on user's page
1669
+ escalation: response.escalation || void 0,
1670
+ demo: response.demo || void 0,
1671
+ point: response.point || void 0,
1672
+ actions: ((_d = response.actions) == null ? void 0 : _d.length) ? response.actions : void 0,
1673
+ payment: response.payment || void 0,
1674
+ coach: response.coach || void 0,
1675
+ quickReplies: ((_e = response.quick_replies) == null ? void 0 : _e.length) ? response.quick_replies : void 0
1676
+ };
1677
+ }
1678
+ /**
1679
+ * Handle a regular JSON response (non-streaming).
1680
+ */
1681
+ /**
1682
+ * Surface a plan to the UI once per chain.
1683
+ *
1684
+ * The chain revisits the server on every round, and a mid-chain response
1685
+ * may restate the plan. Re-announcing an identical plan would redraw it and
1686
+ * make the assistant look like it had started over, so only a changed plan
1687
+ * is emitted.
1688
+ */
1689
+ _announcePlan(plan) {
1690
+ var _a;
1691
+ if (!plan || !Array.isArray(plan.steps) || plan.steps.length === 0) {
1692
+ return void 0;
1693
+ }
1694
+ const fingerprint = JSON.stringify(plan.steps.map((s) => [s.title, s.tool]));
1695
+ if (fingerprint !== this.lastPlanFingerprint) {
1696
+ this.lastPlanFingerprint = fingerprint;
1697
+ this.currentPlan = plan;
1698
+ (_a = this.onPlan) == null ? void 0 : _a.call(this, plan);
1699
+ }
1700
+ return plan;
1701
+ }
1702
+ _handleJsonResponse(response, chatMode, toolResult) {
1703
+ var _a, _b, _c, _d;
1704
+ const respCheck = this.validator.validateResponse(response);
1705
+ if (!respCheck.valid) {
1706
+ return this._errorResponse(respCheck.error || "Invalid server response.");
1707
+ }
1708
+ const answer = respCheck.sanitized || response.answer;
1709
+ const plan = (_a = this._announcePlan(response.plan)) != null ? _a : this.currentPlan;
1710
+ if (response.tool_call && response.tool_call.requires_confirm) {
1711
+ return {
1712
+ mode: "answer",
1713
+ answer,
1714
+ plan,
1715
+ confirmationNeeded: true,
1716
+ pendingToolCall: response.tool_call,
1717
+ autoExecute: false
1718
+ };
1719
+ }
1720
+ if (response.tool_call) {
1721
+ return {
1722
+ mode: "answer",
1723
+ answer,
1724
+ plan,
1725
+ pendingToolCall: response.tool_call,
1726
+ pendingToolCalls: ((_b = response.tool_calls) == null ? void 0 : _b.length) ? response.tool_calls : [response.tool_call],
1727
+ autoExecuteToolCall: true,
1728
+ autoExecute: false
1729
+ };
1730
+ }
1731
+ let guideSteps = response.guide_steps || void 0;
1732
+ if (guideSteps && guideSteps.length > 0) {
1733
+ const stepCheck = this.validator.validateGuideSteps(guideSteps);
1734
+ if (stepCheck.warnings.length > 0) {
1735
+ console.warn("[GuideAI] Guide step warnings:", stepCheck.warnings);
1736
+ }
1737
+ guideSteps = stepCheck.valid ? stepCheck.steps : void 0;
1738
+ }
1739
+ const harnessResult = {
1740
+ mode: response.mode || "guide",
1741
+ answer,
1742
+ plan,
1743
+ intent: response.intent,
1744
+ intentReason: response.intent_reason || void 0,
1745
+ guideSteps,
1746
+ guideTitle: response.guide_title || void 0,
1747
+ navigation: response.navigation || void 0,
1748
+ toolResult,
1749
+ autoExecute: false,
1750
+ // disabled — never auto-click/scroll/fill on user's page
1751
+ escalation: response.escalation || void 0,
1752
+ demo: response.demo || void 0,
1753
+ point: response.point || void 0,
1754
+ actions: ((_c = response.actions) == null ? void 0 : _c.length) ? response.actions : void 0,
1755
+ payment: response.payment || void 0,
1756
+ coach: response.coach || void 0,
1757
+ quickReplies: ((_d = response.quick_replies) == null ? void 0 : _d.length) ? response.quick_replies : void 0
1758
+ };
1759
+ this.memory.addMessage({
1760
+ role: "assistant",
1761
+ content: answer,
1762
+ toolResult,
1763
+ guideSteps: harnessResult.guideSteps,
1764
+ guideTitle: harnessResult.guideTitle,
1765
+ navigation: harnessResult.navigation,
1766
+ mode: harnessResult.mode,
1767
+ escalation: harnessResult.escalation,
1768
+ demo: harnessResult.demo,
1769
+ timestamp: Date.now()
1770
+ });
1771
+ return harnessResult;
1772
+ }
1773
+ async _continueToolChain(latestToolName, latestToolResult, originalQuestion, toolHistory, onStatus, onTextDelta, latestBatch, budget) {
1774
+ var _a;
1775
+ const conversationHistory = this.memory.getRecentForLLM(6);
1776
+ if (onStatus) {
1777
+ onStatus(
1778
+ (budget == null ? void 0 : budget.forceAnswer) ? "Writing up what I found..." : latestBatch && latestBatch.length > 1 ? "Compiling the information together..." : "Reviewing what came back..."
1779
+ );
1780
+ }
1781
+ const response = await this.api.post(
1782
+ EP.chatContinue(),
1783
+ {
1784
+ original_question: originalQuestion,
1785
+ latest_tool_name: latestToolName,
1786
+ latest_tool_result: latestToolResult,
1787
+ latest_batch: latestBatch != null ? latestBatch : [],
1788
+ conversation_history: conversationHistory,
1789
+ tool_history: toolHistory,
1790
+ tools_remaining: budget == null ? void 0 : budget.toolsRemaining,
1791
+ force_answer: (_a = budget == null ? void 0 : budget.forceAnswer) != null ? _a : false,
1792
+ tool_context: this.contextForTools(),
1793
+ client_tools: this.tools.definitions()
1794
+ }
1795
+ );
1796
+ const harnessResponse = this._handleJsonResponse(
1797
+ response,
1798
+ "assistant",
1799
+ { success: true, data: latestToolResult }
1800
+ );
1801
+ if (!harnessResponse.autoExecuteToolCall && onTextDelta && harnessResponse.answer) {
1802
+ this._emitAnswerText(harnessResponse.answer, onTextDelta);
1803
+ }
1804
+ return harnessResponse;
1805
+ }
1806
+ /**
1807
+ * Remove calls that have already been made in this chain.
1808
+ *
1809
+ * The model is told not to repeat itself, and mostly does not, but a long
1810
+ * chain occasionally re-requests an endpoint it already has — which costs a
1811
+ * request and a model round trip to learn nothing new.
1812
+ */
1813
+ _dropRepeats(toolCalls, executed, callKey) {
1814
+ return toolCalls.filter((call) => !executed.has(callKey(call)));
1815
+ }
1816
+ /**
1817
+ * Run a client-executed tool chain.
1818
+ *
1819
+ * Each round the model may request either one tool or a batch of
1820
+ * independent tools. A batch is executed concurrently, so the wall-clock
1821
+ * cost of gathering four unrelated resources is one request plus one model
1822
+ * round trip, rather than four of each. Dependent calls still arrive one at
1823
+ * a time — the model only batches what it can run without waiting.
1824
+ */
1825
+ async _runClientToolChain(initialToolCalls, originalQuestion, onStatus, onTextDelta, onToolCallEvent) {
1826
+ var _a;
1827
+ if (!this.hasAnyLocalTools()) {
1828
+ return this._errorResponse("Tool execution is unavailable in this browser context.");
1829
+ }
1830
+ const toolHistory = [];
1831
+ const executed = /* @__PURE__ */ new Set();
1832
+ const callKey = (call) => {
1833
+ var _a2, _b;
1834
+ return `${call.tool}:${JSON.stringify((_a2 = call.params) != null ? _a2 : {})}:${JSON.stringify((_b = call.body) != null ? _b : {})}`;
1835
+ };
1836
+ let currentBatch = this._dropRepeats(initialToolCalls, executed, callKey);
1837
+ let toolsRun = 0;
1838
+ let repairsUsed = 0;
1839
+ for (let iteration = 0; currentBatch.length > 0 && iteration < MAX_CHAIN_ROUNDS; iteration += 1) {
1840
+ if (toolsRun + currentBatch.length > MAX_TOTAL_TOOL_CALLS) {
1841
+ currentBatch = currentBatch.slice(0, Math.max(0, MAX_TOTAL_TOOL_CALLS - toolsRun));
1842
+ if (currentBatch.length === 0) break;
1843
+ }
1844
+ currentBatch.forEach((call) => executed.add(callKey(call)));
1845
+ toolsRun += currentBatch.length;
1846
+ const results = await this.executeLocalToolBatch(
1847
+ currentBatch,
1848
+ (toolCall, result) => {
1849
+ const check = this.validator.validateToolResultEnhanced(result);
1850
+ onToolCallEvent == null ? void 0 : onToolCallEvent({
1851
+ type: "done",
1852
+ tool: toolCall.tool,
1853
+ success: check.valid,
1854
+ // The card's badge already shows Complete/Error and its title
1855
+ // already shows the label, so only send extra detail worth
1856
+ // reading — a warning, or why it failed.
1857
+ summary: check.valid ? check.warning : check.error || "Failed"
1858
+ });
1859
+ }
1860
+ );
1861
+ const batchSteps = [];
1862
+ const failures = [];
1863
+ let anyRepairable = false;
1864
+ let lastGoodResult;
1865
+ results.forEach((rawResult, i) => {
1866
+ const toolCall = currentBatch[i];
1867
+ const check = this.validator.validateToolResultEnhanced(rawResult);
1868
+ if (!check.valid) {
1869
+ failures.push(check.error || "Tool execution failed.");
1870
+ if (REPAIRABLE_ERROR_TYPES.has(check.errorType || "")) anyRepairable = true;
1871
+ batchSteps.push({
1872
+ tool_name: toolCall.tool,
1873
+ tool_result: {
1874
+ error: check.detail || rawResult.error || check.error || "Tool execution failed."
1875
+ }
1876
+ });
1877
+ return;
1878
+ }
1879
+ lastGoodResult = rawResult;
1880
+ batchSteps.push({ tool_name: toolCall.tool, tool_result: rawResult.data });
1881
+ });
1882
+ const allFailed = failures.length === results.length;
1883
+ const canRepair = allFailed && anyRepairable && repairsUsed < MAX_REPAIR_ROUNDS;
1884
+ if (allFailed && !canRepair) {
1885
+ const errorMsg = failures[0] || "Tool execution failed.";
1886
+ this.memory.addMessage({
1887
+ role: "assistant",
1888
+ content: errorMsg,
1889
+ toolResult: results[0],
1890
+ timestamp: Date.now()
1891
+ });
1892
+ if (onTextDelta) {
1893
+ onTextDelta(errorMsg);
1894
+ }
1895
+ return {
1896
+ mode: "answer",
1897
+ answer: errorMsg,
1898
+ toolResult: results[0],
1899
+ autoExecute: false
1900
+ };
1901
+ }
1902
+ if (canRepair) {
1903
+ repairsUsed += 1;
1904
+ onStatus == null ? void 0 : onStatus("That did not go through — adjusting and trying again...");
1905
+ }
1906
+ toolHistory.push(...batchSteps);
1907
+ const latestStep = batchSteps[batchSteps.length - 1];
1908
+ const toolsRemaining = Math.max(0, MAX_TOTAL_TOOL_CALLS - toolsRun);
1909
+ const forceAnswer = toolsRemaining === 0 || iteration === MAX_CHAIN_ROUNDS - 1;
1910
+ const continued = await this._continueToolChain(
1911
+ latestStep.tool_name,
1912
+ latestStep.tool_result,
1913
+ originalQuestion,
1914
+ toolHistory,
1915
+ onStatus,
1916
+ onTextDelta,
1917
+ batchSteps,
1918
+ { toolsRemaining, forceAnswer }
1919
+ );
1920
+ const nextBatch = this._dropRepeats(
1921
+ (_a = continued.pendingToolCalls) != null ? _a : continued.pendingToolCall ? [continued.pendingToolCall] : [],
1922
+ executed,
1923
+ callKey
1924
+ );
1925
+ if (continued.autoExecuteToolCall && nextBatch.length > 0) {
1926
+ currentBatch = nextBatch;
1927
+ onStatus == null ? void 0 : onStatus(describeToolBatch(currentBatch));
1928
+ for (const toolCall of currentBatch) {
1929
+ onToolCallEvent == null ? void 0 : onToolCallEvent({
1930
+ type: "start",
1931
+ tool: toolCall.tool,
1932
+ description: describeToolCall(toolCall)
1933
+ });
1934
+ }
1935
+ continue;
1936
+ }
1937
+ if (!continued.toolResult && lastGoodResult) {
1938
+ continued.toolResult = lastGoodResult;
1939
+ }
1940
+ return continued;
1941
+ }
1942
+ if (toolHistory.length > 0) {
1943
+ const last = toolHistory[toolHistory.length - 1];
1944
+ return this._continueToolChain(
1945
+ last.tool_name,
1946
+ last.tool_result,
1947
+ originalQuestion,
1948
+ toolHistory,
1949
+ onStatus,
1950
+ onTextDelta,
1951
+ void 0,
1952
+ { toolsRemaining: 0, forceAnswer: true }
1953
+ );
1954
+ }
1955
+ const fallback = "I gathered several data points but hit the maximum number of tool steps.";
1956
+ this.memory.addMessage({
1957
+ role: "assistant",
1958
+ content: fallback,
1959
+ timestamp: Date.now()
1960
+ });
1961
+ return {
1962
+ mode: "answer",
1963
+ answer: fallback,
1964
+ autoExecute: false
1965
+ };
1966
+ }
1967
+ hasAnyLocalTools() {
1968
+ return !!this.toolExecutor || this.tools.definitions().length > 0;
1969
+ }
1970
+ canExecuteTool(toolCall) {
1971
+ if (isServerTool(toolCall)) return true;
1972
+ if (this.tools.canExecute(toolCall.tool)) {
1973
+ return this.tools.canExecuteCall(toolCall);
1974
+ }
1975
+ return !!this.toolExecutor;
1976
+ }
1977
+ executeLocalTool(toolCall) {
1978
+ if (isServerTool(toolCall)) return this.executeOnServer(toolCall);
1979
+ if (this.tools.canExecute(toolCall.tool)) {
1980
+ return this.tools.execute(toolCall);
1981
+ }
1982
+ if (this.toolExecutor) return this.toolExecutor.execute(toolCall);
1983
+ return Promise.resolve({
1984
+ success: false,
1985
+ error: "Tool execution is unavailable in this browser context.",
1986
+ error_type: "tool_unavailable"
1987
+ });
1988
+ }
1989
+ /**
1990
+ * An MCP tool, after the user approved it: we cannot call it — it is
1991
+ * reached from GuideAI's network with a secret the client never holds —
1992
+ * so the backend runs it, told who the user is by their token.
1993
+ */
1994
+ async executeOnServer(toolCall) {
1995
+ var _a, _b;
1996
+ try {
1997
+ return await this.api.post(EP.chatExecuteTool(), {
1998
+ tool: toolCall.tool,
1999
+ method: toolCall.method,
2000
+ path: toolCall.path,
2001
+ params: toolCall.params,
2002
+ body: toolCall.body,
2003
+ user_token: (_b = (_a = this.toolExecutor) == null ? void 0 : _a.getAuthToken()) != null ? _b : void 0
2004
+ });
2005
+ } catch (error) {
2006
+ return {
2007
+ success: false,
2008
+ error: error instanceof Error ? error.message : "The tool could not be run.",
2009
+ error_type: "tool_unavailable"
2010
+ };
2011
+ }
2012
+ }
2013
+ executeLocalToolBatch(toolCalls, onComplete) {
2014
+ return Promise.all(toolCalls.map(async (toolCall) => {
2015
+ const result = await this.executeLocalTool(toolCall);
2016
+ onComplete == null ? void 0 : onComplete(toolCall, result);
2017
+ return result;
2018
+ }));
2019
+ }
2020
+ async _formatToolResult(toolName, toolResult, originalQuestion, onStatus, onTextDelta) {
2021
+ const conversationHistory = this.memory.getRecentForLLM(3);
2022
+ try {
2023
+ if (onStatus) {
2024
+ onStatus("Processing results...");
2025
+ }
2026
+ const rawResponse = await this.api.postRaw(
2027
+ EP.chatFormat(),
2028
+ {
2029
+ tool_name: toolName,
2030
+ tool_result: toolResult,
2031
+ original_question: originalQuestion,
2032
+ conversation_history: conversationHistory,
2033
+ stream: this.api.supportsStreaming
2034
+ }
2035
+ );
2036
+ const contentType = rawResponse.headers.get("content-type") || "";
2037
+ if (contentType.includes("text/event-stream")) {
2038
+ return this._readFormatToolResultStream(
2039
+ rawResponse,
2040
+ toolResult,
2041
+ onStatus,
2042
+ onTextDelta
2043
+ );
2044
+ }
2045
+ const formatted = await rawResponse.json();
2046
+ const rawAnswer = (formatted == null ? void 0 : formatted.answer) || "Here is the result of your request.";
2047
+ const answer = this.validator.validateFormattedResponse(
2048
+ rawAnswer,
2049
+ { success: true, data: toolResult }
2050
+ );
2051
+ const navigation = (formatted == null ? void 0 : formatted.navigation) || void 0;
2052
+ const guideSteps = (formatted == null ? void 0 : formatted.guide_steps) || void 0;
2053
+ const guideTitle = (formatted == null ? void 0 : formatted.guide_title) || void 0;
2054
+ const mode = (formatted == null ? void 0 : formatted.mode) === "execute" ? "execute" : "answer";
2055
+ this.memory.addMessage({
2056
+ role: "assistant",
2057
+ content: answer,
2058
+ toolResult: { success: true, data: toolResult },
2059
+ guideSteps,
2060
+ guideTitle,
2061
+ navigation,
2062
+ mode,
2063
+ timestamp: Date.now()
2064
+ });
2065
+ return {
2066
+ mode,
2067
+ answer,
2068
+ guideSteps,
2069
+ guideTitle,
2070
+ navigation,
2071
+ toolResult: { success: true, data: toolResult },
2072
+ autoExecute: false
2073
+ };
2074
+ } catch {
2075
+ const fallback = "I fetched the data successfully but had trouble formatting it.";
2076
+ this.memory.addMessage({
2077
+ role: "assistant",
2078
+ content: fallback,
2079
+ timestamp: Date.now()
2080
+ });
2081
+ return {
2082
+ mode: "answer",
2083
+ answer: fallback,
2084
+ toolResult: { success: true, data: toolResult },
2085
+ autoExecute: false
2086
+ };
2087
+ }
2088
+ }
2089
+ async _readFormatToolResultStream(rawResponse, toolResult, onStatus, onTextDelta) {
2090
+ var _a;
2091
+ let buffer = "";
2092
+ let accumulatedText = "";
2093
+ let finalResponse = null;
2094
+ let streamError = null;
2095
+ if (!rawResponse.stream) {
2096
+ try {
2097
+ finalResponse = await rawResponse.json();
2098
+ } catch {
2099
+ streamError = "I got the data but had trouble formatting it.";
2100
+ }
2101
+ }
2102
+ try {
2103
+ for await (const chunk of (_a = rawResponse.stream) != null ? _a : []) {
2104
+ buffer += chunk;
2105
+ const eventBlocks = buffer.split("\n\n");
2106
+ buffer = eventBlocks.pop() || "";
2107
+ for (const block of eventBlocks) {
2108
+ const trimmedBlock = block.trim();
2109
+ if (!trimmedBlock) continue;
2110
+ for (const rawLine of trimmedBlock.split("\n")) {
2111
+ const line2 = rawLine.trim();
2112
+ if (!line2.startsWith("data: ")) continue;
2113
+ let event;
2114
+ try {
2115
+ event = JSON.parse(line2.slice(6));
2116
+ } catch {
2117
+ continue;
2118
+ }
2119
+ switch (event.type) {
2120
+ case "status":
2121
+ onStatus == null ? void 0 : onStatus(event.message || "Processing results...");
2122
+ break;
2123
+ case "text_delta":
2124
+ if (event.delta) {
2125
+ accumulatedText += event.delta;
2126
+ onTextDelta == null ? void 0 : onTextDelta(event.delta);
2127
+ }
2128
+ break;
2129
+ case "error":
2130
+ streamError = event.message || "I got the data but had trouble formatting it.";
2131
+ break;
2132
+ case "done":
2133
+ if (event.format_response) {
2134
+ finalResponse = event.format_response;
2135
+ }
2136
+ break;
2137
+ }
2138
+ }
2139
+ }
2140
+ }
2141
+ } catch {
2142
+ streamError = "I got the data but had trouble formatting it.";
2143
+ }
2144
+ if (streamError) {
2145
+ this.memory.addMessage({
2146
+ role: "assistant",
2147
+ content: streamError,
2148
+ timestamp: Date.now()
2149
+ });
2150
+ return {
2151
+ mode: "answer",
2152
+ answer: streamError,
2153
+ toolResult: { success: true, data: toolResult },
2154
+ autoExecute: false
2155
+ };
2156
+ }
2157
+ const rawAnswer = (finalResponse == null ? void 0 : finalResponse.answer) || accumulatedText || "Here is the result of your request.";
2158
+ const answer = this.validator.validateFormattedResponse(
2159
+ rawAnswer,
2160
+ { success: true, data: toolResult }
2161
+ );
2162
+ const navigation = (finalResponse == null ? void 0 : finalResponse.navigation) || void 0;
2163
+ const guideSteps = (finalResponse == null ? void 0 : finalResponse.guide_steps) || void 0;
2164
+ const guideTitle = (finalResponse == null ? void 0 : finalResponse.guide_title) || void 0;
2165
+ const mode = (finalResponse == null ? void 0 : finalResponse.mode) === "execute" ? "execute" : "answer";
2166
+ this.memory.addMessage({
2167
+ role: "assistant",
2168
+ content: answer,
2169
+ toolResult: { success: true, data: toolResult },
2170
+ guideSteps,
2171
+ guideTitle,
2172
+ navigation,
2173
+ mode,
2174
+ timestamp: Date.now()
2175
+ });
2176
+ return {
2177
+ mode,
2178
+ answer,
2179
+ guideSteps,
2180
+ guideTitle,
2181
+ navigation,
2182
+ toolResult: { success: true, data: toolResult },
2183
+ autoExecute: false
2184
+ };
2185
+ }
2186
+ async _executePendingToolCall(toolCalls, originalQuestion, onStatus, onTextDelta, onToolCallEvent) {
2187
+ if (toolCalls.length === 0) {
2188
+ return this._errorResponse("No tool call to execute.");
2189
+ }
2190
+ onStatus == null ? void 0 : onStatus(describeToolBatch(toolCalls));
2191
+ for (const toolCall of toolCalls) {
2192
+ onToolCallEvent == null ? void 0 : onToolCallEvent({
2193
+ type: "start",
2194
+ tool: toolCall.tool,
2195
+ description: describeToolCall(toolCall)
2196
+ });
2197
+ }
2198
+ const result = await this._runClientToolChain(
2199
+ toolCalls,
2200
+ originalQuestion,
2201
+ onStatus,
2202
+ onTextDelta,
2203
+ onToolCallEvent
2204
+ );
2205
+ if (!result.toolResult) {
2206
+ onToolCallEvent == null ? void 0 : onToolCallEvent({
2207
+ type: "done",
2208
+ tool: toolCalls[0].tool,
2209
+ success: false,
2210
+ summary: result.answer || "Failed"
2211
+ });
2212
+ }
2213
+ return result;
2214
+ }
2215
+ /**
2216
+ * Emit a fully-buffered answer to the UI.
2217
+ *
2218
+ * This used to fake a typing effect by releasing one word every 12ms. The
2219
+ * response has already arrived in full by this point, so that delay was pure
2220
+ * added latency — a 250-word answer sat behind three seconds of animation
2221
+ * after the data was on the client. Real streaming still arrives token by
2222
+ * token over SSE; a buffered answer is shown as soon as we have it.
2223
+ */
2224
+ _emitAnswerText(text, onTextDelta, abortSignal) {
2225
+ if (abortSignal == null ? void 0 : abortSignal.aborted) return;
2226
+ onTextDelta(text);
2227
+ }
2228
+ _errorResponse(message) {
2229
+ return {
2230
+ mode: "guide",
2231
+ answer: message,
2232
+ autoExecute: false
2233
+ };
2234
+ }
2235
+ }
2236
+ const REQUEST_ORIGIN_HEADER = "x-guideai-request-origin";
2237
+ function parseHeaders(raw) {
2238
+ const map = /* @__PURE__ */ new Map();
2239
+ for (const line2 of (raw != null ? raw : "").split(/\r?\n/)) {
2240
+ const colon = line2.indexOf(":");
2241
+ if (colon <= 0) continue;
2242
+ map.set(line2.slice(0, colon).trim().toLowerCase(), line2.slice(colon + 1).trim());
2243
+ }
2244
+ return { get: (name) => {
2245
+ var _a;
2246
+ return (_a = map.get(name.toLowerCase())) != null ? _a : null;
2247
+ } };
2248
+ }
2249
+ const RESPONSE_START_TIMEOUT_MS = 2e4;
2250
+ class RNHttpTransport {
2251
+ constructor(options = {}) {
2252
+ this.options = options;
2253
+ this.supportsStreaming = typeof XMLHttpRequest !== "undefined";
2254
+ }
2255
+ request(req) {
2256
+ return new Promise((resolve, reject) => {
2257
+ var _a;
2258
+ const xhr = new XMLHttpRequest();
2259
+ xhr.open(req.method, req.url);
2260
+ const headers = { ...req.headers };
2261
+ if (this.options.requestOrigin && !headers[REQUEST_ORIGIN_HEADER]) {
2262
+ headers[REQUEST_ORIGIN_HEADER] = this.options.requestOrigin;
2263
+ }
2264
+ for (const [name, value] of Object.entries(headers)) {
2265
+ xhr.setRequestHeader(name, value);
2266
+ }
2267
+ const doneCallbacks = [];
2268
+ let delivered = 0;
2269
+ let pending = [];
2270
+ let finished = false;
2271
+ let failure2 = null;
2272
+ let wake = null;
2273
+ const push = (chunk) => {
2274
+ if (chunk) pending.push(chunk);
2275
+ wake == null ? void 0 : wake();
2276
+ wake = null;
2277
+ };
2278
+ const drainResponseText = () => {
2279
+ const text = xhr.responseText;
2280
+ if (text.length > delivered) {
2281
+ const slice = text.slice(delivered);
2282
+ delivered = text.length;
2283
+ push(slice);
2284
+ }
2285
+ };
2286
+ let startTimer = null;
2287
+ const clearStartTimer = () => {
2288
+ if (startTimer === null) return;
2289
+ clearTimeout(startTimer);
2290
+ startTimer = null;
2291
+ };
2292
+ const fail = (error) => {
2293
+ clearStartTimer();
2294
+ if (finished) return;
2295
+ failure2 = error;
2296
+ finished = true;
2297
+ push("");
2298
+ reject(error);
2299
+ };
2300
+ let resolved = false;
2301
+ const resolveOnce = () => {
2302
+ if (resolved) return;
2303
+ resolved = true;
2304
+ clearStartTimer();
2305
+ const stream = {
2306
+ async *[Symbol.asyncIterator]() {
2307
+ while (true) {
2308
+ if (pending.length) {
2309
+ const batch = pending;
2310
+ pending = [];
2311
+ for (const chunk of batch) yield chunk;
2312
+ continue;
2313
+ }
2314
+ if (failure2) throw failure2;
2315
+ if (finished) return;
2316
+ await new Promise((wakeUp) => {
2317
+ wake = wakeUp;
2318
+ });
2319
+ }
2320
+ }
2321
+ };
2322
+ const whenDone = new Promise((done, error) => {
2323
+ const settle = () => {
2324
+ if (failure2) error(failure2);
2325
+ else done(xhr.responseText);
2326
+ };
2327
+ if (finished) settle();
2328
+ else doneCallbacks.push(settle);
2329
+ });
2330
+ resolve({
2331
+ status: xhr.status,
2332
+ headers: parseHeaders(xhr.getAllResponseHeaders()),
2333
+ json: async () => JSON.parse(await whenDone),
2334
+ text: () => whenDone,
2335
+ stream
2336
+ });
2337
+ };
2338
+ xhr.onreadystatechange = () => {
2339
+ if (xhr.readyState === 2) {
2340
+ resolveOnce();
2341
+ } else if (xhr.readyState === 3) {
2342
+ resolveOnce();
2343
+ drainResponseText();
2344
+ } else if (xhr.readyState === 4) {
2345
+ resolveOnce();
2346
+ drainResponseText();
2347
+ finished = true;
2348
+ push("");
2349
+ for (const callback of doneCallbacks) callback();
2350
+ }
2351
+ };
2352
+ xhr.onerror = () => fail(new Error(`Network request failed: ${req.method} ${req.url}`));
2353
+ xhr.ontimeout = () => fail(new Error(`Request timed out: ${req.url}`));
2354
+ xhr.onabort = () => fail(new Error(`Request aborted: ${req.url}`));
2355
+ if (req.signal) {
2356
+ if (req.signal.aborted) {
2357
+ fail(new Error(`Request aborted: ${req.url}`));
2358
+ return;
2359
+ }
2360
+ }
2361
+ startTimer = setTimeout(() => {
2362
+ startTimer = null;
2363
+ fail(new Error(
2364
+ `No response after ${RESPONSE_START_TIMEOUT_MS}ms: ${req.method} ${req.url}`
2365
+ ));
2366
+ try {
2367
+ xhr.abort();
2368
+ } catch {
2369
+ }
2370
+ }, RESPONSE_START_TIMEOUT_MS);
2371
+ xhr.send((_a = req.body) != null ? _a : null);
2372
+ });
2373
+ }
2374
+ }
2375
+ class NativeKVStore {
2376
+ constructor(store) {
2377
+ this.store = store;
2378
+ }
2379
+ getItem(key) {
2380
+ var _a;
2381
+ return (_a = this.store.getString(key)) != null ? _a : null;
2382
+ }
2383
+ setItem(key, value) {
2384
+ this.store.set(key, value);
2385
+ }
2386
+ removeItem(key) {
2387
+ this.store.delete(key);
2388
+ }
2389
+ }
2390
+ class MemoryStore {
2391
+ constructor() {
2392
+ this.map = /* @__PURE__ */ new Map();
2393
+ }
2394
+ getItem(key) {
2395
+ var _a;
2396
+ return (_a = this.map.get(key)) != null ? _a : null;
2397
+ }
2398
+ setItem(key, value) {
2399
+ this.map.set(key, value);
2400
+ }
2401
+ removeItem(key) {
2402
+ this.map.delete(key);
2403
+ }
2404
+ }
2405
+ class RNCredentialSource {
2406
+ constructor(storage, session) {
2407
+ this.storage = storage;
2408
+ this.session = session;
2409
+ this.hostToken = null;
2410
+ this.appValues = /* @__PURE__ */ new Map();
2411
+ }
2412
+ /**
2413
+ * Set by the runtime from `getAuthToken()` before every turn, so the
2414
+ * synchronous read inside a tool call sees the host's current login.
2415
+ */
2416
+ setAuthToken(token) {
2417
+ this.hostToken = (token == null ? void 0 : token.trim()) || null;
2418
+ }
2419
+ authToken() {
2420
+ return this.hostToken;
2421
+ }
2422
+ /** Set from `getRequestHeaders()` before every turn. */
2423
+ setAppValues(values) {
2424
+ this.appValues = new Map(Object.entries(values).map(([k, v]) => [k.toLowerCase(), v]));
2425
+ }
2426
+ read(source, key) {
2427
+ var _a;
2428
+ switch (source) {
2429
+ case "localStorage":
2430
+ return this.storage.getItem(key);
2431
+ case "sessionStorage":
2432
+ return this.session.getItem(key);
2433
+ // A header the toolset declares as supplied by the app.
2434
+ case "app":
2435
+ return (_a = this.appValues.get(key.toLowerCase())) != null ? _a : null;
2436
+ // No cookie jar on a device. Saying so is the honest answer.
2437
+ case "cookie":
2438
+ return null;
2439
+ default:
2440
+ return null;
2441
+ }
2442
+ }
2443
+ }
2444
+ function rnPlatform(options = {}) {
2445
+ const storage = options.store ? new NativeKVStore(options.store) : new MemoryStore();
2446
+ const session = new MemoryStore();
2447
+ return { storage, session, credentials: new RNCredentialSource(storage, session) };
2448
+ }
2449
+ const RESERVED_HEADERS = /* @__PURE__ */ new Set(["authorization", "host", "content-length", "cookie"]);
2450
+ class HostAuth {
2451
+ constructor(credentials, options = {}) {
2452
+ this.credentials = credentials;
2453
+ this.options = options;
2454
+ this.pendingRenewal = null;
2455
+ }
2456
+ get enabled() {
2457
+ return Boolean(this.options.getAuthToken);
2458
+ }
2459
+ current() {
2460
+ return this.credentials.authToken();
2461
+ }
2462
+ /** Read the host's token and extra headers now. Never throws. */
2463
+ async refresh() {
2464
+ var _a;
2465
+ if (this.options.getRequestHeaders) {
2466
+ let raw = null;
2467
+ try {
2468
+ raw = await this.options.getRequestHeaders();
2469
+ } catch (error) {
2470
+ console.warn("[GuideAI] getRequestHeaders failed; sending none:", error);
2471
+ }
2472
+ const clean = {};
2473
+ for (const [name, value] of Object.entries(raw != null ? raw : {})) {
2474
+ if (/^[A-Za-z0-9-]{1,64}$/.test(name) && !RESERVED_HEADERS.has(name.toLowerCase()) && typeof value === "string" && value && !/[\r\n]/.test(value)) {
2475
+ clean[name] = value;
2476
+ }
2477
+ }
2478
+ this.credentials.setAppValues(clean);
2479
+ }
2480
+ if (!this.options.getAuthToken) return null;
2481
+ let token = null;
2482
+ try {
2483
+ token = ((_a = await this.options.getAuthToken()) == null ? void 0 : _a.trim()) || null;
2484
+ } catch (error) {
2485
+ console.warn("[GuideAI] getAuthToken failed; continuing signed out:", error);
2486
+ }
2487
+ this.credentials.setAuthToken(token);
2488
+ return token;
2489
+ }
2490
+ /**
2491
+ * A new token after a 401, or null when there is none to be had.
2492
+ *
2493
+ * Shared while in flight: a batch of parallel tool calls that all hit an
2494
+ * expired token triggers one refresh, not one per call.
2495
+ */
2496
+ renew() {
2497
+ var _a;
2498
+ if (!this.pendingRenewal) {
2499
+ const ask = (_a = this.options.onAuthExpired) != null ? _a : this.options.getAuthToken;
2500
+ this.pendingRenewal = (async () => {
2501
+ var _a2;
2502
+ let token = null;
2503
+ try {
2504
+ token = ask ? ((_a2 = await ask()) == null ? void 0 : _a2.trim()) || null : null;
2505
+ } catch (error) {
2506
+ console.warn("[GuideAI] onAuthExpired failed:", error);
2507
+ }
2508
+ this.credentials.setAuthToken(token);
2509
+ return token;
2510
+ })().finally(() => {
2511
+ this.pendingRenewal = null;
2512
+ });
2513
+ }
2514
+ return this.pendingRenewal;
2515
+ }
2516
+ }
2517
+ function bearerOf(headers) {
2518
+ const name = Object.keys(headers).find((key) => key.toLowerCase() === "authorization");
2519
+ const value = name ? headers[name] : "";
2520
+ return (value == null ? void 0 : value.startsWith("Bearer ")) ? value.slice(7) : null;
2521
+ }
2522
+ class HostAuthTransport {
2523
+ constructor(inner, auth) {
2524
+ this.inner = inner;
2525
+ this.auth = auth;
2526
+ }
2527
+ get supportsStreaming() {
2528
+ return this.inner.supportsStreaming;
2529
+ }
2530
+ async request(req) {
2531
+ const response = await this.inner.request(req);
2532
+ if (response.status !== 401 || !this.auth.enabled) return response;
2533
+ const sent = bearerOf(req.headers);
2534
+ if (!sent || sent !== this.auth.current()) return response;
2535
+ const renewed = await this.auth.renew();
2536
+ if (!renewed || renewed === sent) return response;
2537
+ const headers = { ...req.headers };
2538
+ for (const key of Object.keys(headers)) {
2539
+ if (key.toLowerCase() === "authorization") delete headers[key];
2540
+ }
2541
+ headers.Authorization = `Bearer ${renewed}`;
2542
+ return this.inner.request({ ...req, headers });
2543
+ }
2544
+ }
2545
+ const KIND_TO_TAG = {
2546
+ button: "button",
2547
+ link: "a",
2548
+ input: "input",
2549
+ text: "p",
2550
+ toggle: "input",
2551
+ image: "img"
2552
+ };
2553
+ class ScreenRegistry {
2554
+ constructor() {
2555
+ this.elements = /* @__PURE__ */ new Map();
2556
+ }
2557
+ register(element, handlers = {}) {
2558
+ this.elements.set(element.id, { ...element, handlers });
2559
+ return () => {
2560
+ var _a;
2561
+ if (((_a = this.elements.get(element.id)) == null ? void 0 : _a.handlers) === handlers) {
2562
+ this.elements.delete(element.id);
2563
+ }
2564
+ };
2565
+ }
2566
+ get(id) {
2567
+ return this.elements.get(id);
2568
+ }
2569
+ /** Resolve by id first, then by visible label — the model may send either. */
2570
+ find(request) {
2571
+ var _a, _b, _c;
2572
+ if (request.selector) {
2573
+ const byId = this.elements.get(request.selector);
2574
+ if (byId) return byId;
2575
+ }
2576
+ const name = ((_b = (_a = request.name) != null ? _a : request.selector) != null ? _b : "").trim().toLowerCase();
2577
+ if (!name) return void 0;
2578
+ for (const element of this.elements.values()) {
2579
+ if (element.label.trim().toLowerCase() === name) return element;
2580
+ if (((_c = element.accessibilityLabel) == null ? void 0 : _c.trim().toLowerCase()) === name) return element;
2581
+ }
2582
+ return void 0;
2583
+ }
2584
+ all() {
2585
+ return Array.from(this.elements.values());
2586
+ }
2587
+ clear() {
2588
+ this.elements.clear();
2589
+ }
2590
+ }
2591
+ function toPageElement(element) {
2592
+ var _a;
2593
+ const tag = KIND_TO_TAG[(_a = element.kind) != null ? _a : "button"];
2594
+ return {
2595
+ tag,
2596
+ text: element.label,
2597
+ selector: element.id,
2598
+ ...element.accessibilityLabel ? { aria_label: element.accessibilityLabel } : {},
2599
+ ...element.value !== void 0 ? { value: element.value } : {},
2600
+ ...element.placeholder ? { placeholder: element.placeholder } : {},
2601
+ ...element.hidden ? { hidden: true } : {},
2602
+ ...element.sectionTitle ? { section_title: element.sectionTitle } : {}
2603
+ };
2604
+ }
2605
+ function buildSnapshot(input) {
2606
+ const elements = input.registry.all().map(toPageElement);
2607
+ return {
2608
+ url: input.path,
2609
+ title: input.title,
2610
+ elements
2611
+ };
2612
+ }
2613
+ const CLIENT_PATH_PREFIX = "/__guideai/client/";
2614
+ const elementProperties = {
2615
+ selector: {
2616
+ type: "string",
2617
+ description: "An element id from the current screen snapshot."
2618
+ },
2619
+ name: {
2620
+ type: "string",
2621
+ description: "Visible text or accessible name when a reliable id is unavailable."
2622
+ }
2623
+ };
2624
+ const RN_AGENT_TOOL_DEFINITIONS = [
2625
+ {
2626
+ name: "guideai_fill_field",
2627
+ description: "Fill one non-sensitive field on the current screen using its id, label, or accessible name.",
2628
+ consequential: true,
2629
+ parameters: {
2630
+ type: "object",
2631
+ properties: {
2632
+ fieldName: { type: "string", description: "Label, accessible name, or placeholder of the field." },
2633
+ selector: elementProperties.selector,
2634
+ value: { type: "string", description: "The value the user explicitly asked to enter." }
2635
+ },
2636
+ required: ["value"],
2637
+ additionalProperties: false
2638
+ }
2639
+ },
2640
+ {
2641
+ name: "guideai_fill_form",
2642
+ description: "Fill several non-sensitive fields on the current screen in order and report each outcome.",
2643
+ consequential: true,
2644
+ parameters: {
2645
+ type: "object",
2646
+ properties: {
2647
+ fields: {
2648
+ type: "array",
2649
+ maxItems: 25,
2650
+ items: {
2651
+ type: "object",
2652
+ properties: {
2653
+ fieldName: { type: "string" },
2654
+ selector: { type: "string" },
2655
+ value: { type: "string" }
2656
+ },
2657
+ required: ["value"]
2658
+ }
2659
+ }
2660
+ },
2661
+ required: ["fields"],
2662
+ additionalProperties: false
2663
+ }
2664
+ },
2665
+ {
2666
+ name: "guideai_scroll_to_element",
2667
+ description: "Scroll an element on the current screen into view.",
2668
+ parameters: { type: "object", properties: elementProperties, additionalProperties: false }
2669
+ },
2670
+ {
2671
+ name: "guideai_click_element",
2672
+ description: "Tap one enabled control on the current screen after user confirmation.",
2673
+ consequential: true,
2674
+ parameters: { type: "object", properties: elementProperties, additionalProperties: false }
2675
+ },
2676
+ {
2677
+ name: "guideai_navigate",
2678
+ description: "Navigate to another screen in the app.",
2679
+ consequential: true,
2680
+ parameters: {
2681
+ type: "object",
2682
+ properties: { url: { type: "string", description: "The destination screen name." } },
2683
+ required: ["url"],
2684
+ additionalProperties: false
2685
+ }
2686
+ },
2687
+ {
2688
+ name: "guideai_go_back",
2689
+ description: "Go back one screen in the app navigation stack.",
2690
+ consequential: true,
2691
+ parameters: { type: "object", properties: {}, additionalProperties: false }
2692
+ }
2693
+ ];
2694
+ function failure(error, errorType = "action_failed") {
2695
+ return { success: false, error, error_type: errorType };
2696
+ }
2697
+ function stringArg(value) {
2698
+ return typeof value === "string" && value.trim() ? value : void 0;
2699
+ }
2700
+ class RNAgentToolRegistry {
2701
+ constructor(options) {
2702
+ this.options = options;
2703
+ this.queue = Promise.resolve();
2704
+ }
2705
+ definitions() {
2706
+ const available = RN_AGENT_TOOL_DEFINITIONS.filter((tool) => {
2707
+ if (tool.name === "guideai_navigate" || tool.name === "guideai_go_back") {
2708
+ return Boolean(this.options.navigation);
2709
+ }
2710
+ return true;
2711
+ });
2712
+ return available.map((tool) => ({ ...tool, parameters: { ...tool.parameters } }));
2713
+ }
2714
+ copilotDefinitions() {
2715
+ return this.definitions().map((tool) => ({
2716
+ name: tool.name,
2717
+ description: tool.description,
2718
+ method: "CLIENT",
2719
+ path: `${CLIENT_PATH_PREFIX}${tool.name}`,
2720
+ body_schema: tool.parameters,
2721
+ risk_level: tool.consequential ? "write" : "read"
2722
+ }));
2723
+ }
2724
+ canExecute(name) {
2725
+ return this.definitions().some((tool) => tool.name === name);
2726
+ }
2727
+ canExecuteCall(toolCall) {
2728
+ return toolCall.method.toUpperCase() === "CLIENT" && toolCall.path === `${CLIENT_PATH_PREFIX}${toolCall.tool}` && this.canExecute(toolCall.tool);
2729
+ }
2730
+ execute(toolCall) {
2731
+ var _a, _b;
2732
+ if (!this.canExecuteCall(toolCall)) {
2733
+ return Promise.resolve(failure("Native tool is not registered.", "tool_not_found"));
2734
+ }
2735
+ const args = { ...(_a = toolCall.params) != null ? _a : {}, ...(_b = toolCall.body) != null ? _b : {} };
2736
+ const run = async () => this.executeNow(toolCall.tool, args);
2737
+ const result = this.queue.then(run, run);
2738
+ this.queue = result.then(() => void 0, () => void 0);
2739
+ return result;
2740
+ }
2741
+ executeBatch(toolCalls, onComplete) {
2742
+ return Promise.all(toolCalls.map(async (toolCall) => {
2743
+ const result = await this.execute(toolCall);
2744
+ onComplete == null ? void 0 : onComplete(toolCall, result);
2745
+ return result;
2746
+ }));
2747
+ }
2748
+ async fillOne(field) {
2749
+ var _a, _b;
2750
+ const label = (_b = (_a = field.fieldName) != null ? _a : field.selector) != null ? _b : "field";
2751
+ const registry = this.options.getRegistry();
2752
+ if (!registry) return { field: label, filled: false, error: "no_active_screen" };
2753
+ const element = registry.find({ selector: field.selector, name: field.fieldName });
2754
+ if (!element) return { field: label, filled: false, error: "element_not_found" };
2755
+ if (!element.handlers.setValue) {
2756
+ return { field: label, filled: false, error: "element_not_fillable" };
2757
+ }
2758
+ await element.handlers.setValue(field.value);
2759
+ return { field: element.label, filled: true };
2760
+ }
2761
+ async executeNow(name, args) {
2762
+ var _a;
2763
+ try {
2764
+ const registry = this.options.getRegistry();
2765
+ switch (name) {
2766
+ case "guideai_fill_field": {
2767
+ const outcome = await this.fillOne({
2768
+ fieldName: stringArg(args.fieldName),
2769
+ selector: stringArg(args.selector),
2770
+ value: String((_a = args.value) != null ? _a : "")
2771
+ });
2772
+ return { success: outcome.filled, data: outcome, error: outcome.error };
2773
+ }
2774
+ case "guideai_fill_form": {
2775
+ const requested = Array.isArray(args.fields) ? args.fields.slice(0, 25) : [];
2776
+ const outcomes = [];
2777
+ for (let index = 0; index < requested.length; index += 1) {
2778
+ const entry = requested[index];
2779
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
2780
+ outcomes.push({ index, field: `field ${index + 1}`, filled: false, error: "invalid_field" });
2781
+ continue;
2782
+ }
2783
+ if (entry.value == null) {
2784
+ outcomes.push({ index, field: `field ${index + 1}`, filled: false, error: "value_required" });
2785
+ continue;
2786
+ }
2787
+ outcomes.push({ index, ...await this.fillOne({
2788
+ fieldName: stringArg(entry.fieldName),
2789
+ selector: stringArg(entry.selector),
2790
+ value: String(entry.value)
2791
+ }) });
2792
+ }
2793
+ const filledCount = outcomes.filter((outcome) => outcome.filled).length;
2794
+ return {
2795
+ // Matches the browser registry: the tool completed even when a
2796
+ // field was refused. The model needs per-field outcomes to
2797
+ // explain a partial result rather than a transport error.
2798
+ success: outcomes.length > 0,
2799
+ data: { success: outcomes.length > 0 && filledCount === outcomes.length, filledCount, outcomes },
2800
+ error: outcomes.length === 0 ? "fields_required" : void 0
2801
+ };
2802
+ }
2803
+ case "guideai_click_element": {
2804
+ if (!registry) return failure("no_active_screen", "element_not_found");
2805
+ const element = registry.find({ selector: stringArg(args.selector), name: stringArg(args.name) });
2806
+ if (!element) return failure("element_not_found", "element_not_found");
2807
+ if (!element.handlers.press) return failure("element_not_pressable");
2808
+ await element.handlers.press();
2809
+ return { success: true, data: { pressed: element.label } };
2810
+ }
2811
+ case "guideai_scroll_to_element": {
2812
+ if (!registry) return failure("no_active_screen", "element_not_found");
2813
+ const element = registry.find({ selector: stringArg(args.selector), name: stringArg(args.name) });
2814
+ if (!element) return failure("element_not_found", "element_not_found");
2815
+ if (!element.handlers.scrollTo) return failure("element_not_scrollable");
2816
+ await element.handlers.scrollTo();
2817
+ return { success: true, data: { scrolledTo: element.label } };
2818
+ }
2819
+ case "guideai_navigate": {
2820
+ const destination = stringArg(args.url);
2821
+ if (!destination) return failure("url_required");
2822
+ if (!this.options.navigation) return failure("navigation_unavailable");
2823
+ await this.options.navigation.navigate(destination);
2824
+ return { success: true, data: { navigatedTo: destination } };
2825
+ }
2826
+ case "guideai_go_back": {
2827
+ const navigation = this.options.navigation;
2828
+ if (!navigation) return failure("navigation_unavailable");
2829
+ if (navigation.canGoBack && !navigation.canGoBack()) {
2830
+ return failure("no_history");
2831
+ }
2832
+ await navigation.goBack();
2833
+ return { success: true, data: { wentBack: true } };
2834
+ }
2835
+ default:
2836
+ return failure(`Unknown native tool: ${name}`, "tool_not_found");
2837
+ }
2838
+ } catch (error) {
2839
+ return failure(error instanceof Error ? error.message : String(error));
2840
+ }
2841
+ }
2842
+ }
2843
+ class GuideAIRuntime {
2844
+ constructor(options = {}) {
2845
+ const platform = rnPlatform({ store: options.store });
2846
+ this.storage = platform.storage;
2847
+ this.credentials = platform.credentials;
2848
+ this.auth = new HostAuth(this.credentials, options.auth);
2849
+ this.transport = new HostAuthTransport(
2850
+ new RNHttpTransport({ requestOrigin: options.requestOrigin }),
2851
+ this.auth
2852
+ );
2853
+ this.tools = new RNAgentToolRegistry({
2854
+ getRegistry: () => {
2855
+ var _a;
2856
+ return (_a = this.active) == null ? void 0 : _a.registry;
2857
+ },
2858
+ navigation: options.navigation
2859
+ });
2860
+ }
2861
+ /**
2862
+ * Called on every navigation. Replacing the whole screen (rather than
2863
+ * merging) is deliberate: elements from the previous screen are gone,
2864
+ * and leaving them visible to the model would have it offer controls
2865
+ * that are no longer mounted.
2866
+ */
2867
+ setActiveScreen(screen) {
2868
+ this.active = screen;
2869
+ }
2870
+ getActiveScreen() {
2871
+ return this.active;
2872
+ }
2873
+ /**
2874
+ * What the host passes as the page snapshot on each turn.
2875
+ *
2876
+ * Returns undefined when no screen is registered — not an empty
2877
+ * snapshot. The difference matters: an empty snapshot asserts "this
2878
+ * screen has no controls", while undefined says "nothing was declared",
2879
+ * and only the second lets the backend fall back to route-level
2880
+ * guidance rather than concluding the screen is bare.
2881
+ */
2882
+ captureSnapshot() {
2883
+ if (!this.active) return void 0;
2884
+ return buildSnapshot({
2885
+ path: this.active.path,
2886
+ title: this.active.title,
2887
+ registry: this.active.registry
2888
+ });
2889
+ }
2890
+ }
2891
+ const DEFAULT_TIMEOUT_MS = 3e4;
2892
+ async function raiseForStatus(response, path) {
2893
+ var _a, _b, _c, _d, _e;
2894
+ if (response.status >= 200 && response.status < 300) return;
2895
+ if (response.status === 401 || response.status === 403) {
2896
+ throw new AuthError(response.status);
2897
+ }
2898
+ let detail = {};
2899
+ try {
2900
+ detail = (_a = await response.json()) != null ? _a : {};
2901
+ } catch {
2902
+ detail = {};
2903
+ }
2904
+ if (response.status === 402) {
2905
+ throw new PlanAccessError({
2906
+ feature: String((_b = detail.feature) != null ? _b : "assistant"),
2907
+ plan: String((_c = detail.plan) != null ? _c : "unknown"),
2908
+ message: typeof detail.message === "string" ? detail.message : void 0
2909
+ });
2910
+ }
2911
+ if (response.status === 429) {
2912
+ throw new UsageLimitError({
2913
+ message: typeof detail.message === "string" ? detail.message : "Usage limit reached",
2914
+ limit_type: String((_d = detail.limit_type) != null ? _d : "requests"),
2915
+ plan: String((_e = detail.plan) != null ? _e : "unknown")
2916
+ });
2917
+ }
2918
+ throw new Error(`HTTP ${response.status} for ${path}`);
2919
+ }
2920
+ class UploadError extends Error {
2921
+ constructor(status, message) {
2922
+ super(message);
2923
+ this.status = status;
2924
+ this.name = "UploadError";
2925
+ }
2926
+ }
2927
+ class RNApiClient {
2928
+ constructor(options) {
2929
+ var _a;
2930
+ this.options = options;
2931
+ this.transport = new RNHttpTransport({ requestOrigin: options.requestOrigin });
2932
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
2933
+ this.timeoutMs = (_a = options.timeoutMs) != null ? _a : DEFAULT_TIMEOUT_MS;
2934
+ }
2935
+ get supportsStreaming() {
2936
+ return this.transport.supportsStreaming;
2937
+ }
2938
+ headers() {
2939
+ const headers = {
2940
+ "content-type": "application/json",
2941
+ "x-api-key": this.options.publicKey
2942
+ };
2943
+ if (this.options.siteId) headers["x-site-id"] = this.options.siteId;
2944
+ if (this.options.agent) headers["x-guideai-agent"] = this.options.agent;
2945
+ return headers;
2946
+ }
2947
+ url(path) {
2948
+ return path.startsWith("http") ? path : `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
2949
+ }
2950
+ /**
2951
+ * Time budget for JSON calls.
2952
+ *
2953
+ * Core's AbortSignal is declared with only `aborted` — it never
2954
+ * originates cancellation — so the deadline is enforced here by racing
2955
+ * rather than by constructing a controller inside the loop.
2956
+ */
2957
+ async withTimeout(work, timeoutMs, path) {
2958
+ let timer;
2959
+ try {
2960
+ return await Promise.race([
2961
+ work,
2962
+ new Promise((_, reject) => {
2963
+ timer = setTimeout(
2964
+ () => reject(new Error(`Request timed out after ${timeoutMs}ms: ${path}`)),
2965
+ timeoutMs
2966
+ );
2967
+ })
2968
+ ]);
2969
+ } finally {
2970
+ if (timer !== void 0) clearTimeout(timer);
2971
+ }
2972
+ }
2973
+ async get(path, timeoutMs) {
2974
+ const request = async () => {
2975
+ const response = await this.transport.request({
2976
+ url: this.url(path),
2977
+ method: "GET",
2978
+ headers: this.headers()
2979
+ });
2980
+ await raiseForStatus(response, path);
2981
+ return response.json();
2982
+ };
2983
+ return this.withTimeout(request(), timeoutMs != null ? timeoutMs : this.timeoutMs, path);
2984
+ }
2985
+ async post(path, body, timeoutMs) {
2986
+ const request = async () => {
2987
+ const response = await this.transport.request({
2988
+ url: this.url(path),
2989
+ method: "POST",
2990
+ headers: this.headers(),
2991
+ body: JSON.stringify(body != null ? body : {})
2992
+ });
2993
+ await raiseForStatus(response, path);
2994
+ return response.json();
2995
+ };
2996
+ return this.withTimeout(request(), timeoutMs != null ? timeoutMs : this.timeoutMs, path);
2997
+ }
2998
+ /**
2999
+ * Upload a file the device already wrote (a voice recording) as
3000
+ * multipart. XHR rather than fetch: it is the transport this package
3001
+ * already trusts on RN, and it takes RN's `{ uri, name, type }` parts.
3002
+ */
3003
+ async postFile(path, field, file, fields = {}, timeoutMs) {
3004
+ const form = new FormData();
3005
+ form.append(field, file);
3006
+ for (const [name, value] of Object.entries(fields)) form.append(name, value);
3007
+ const headers = this.headers();
3008
+ delete headers["content-type"];
3009
+ if (this.options.requestOrigin) headers[REQUEST_ORIGIN_HEADER] = this.options.requestOrigin;
3010
+ const request = new Promise((resolve, reject) => {
3011
+ const xhr = new XMLHttpRequest();
3012
+ xhr.open("POST", this.url(path));
3013
+ for (const [name, value] of Object.entries(headers)) xhr.setRequestHeader(name, value);
3014
+ xhr.onreadystatechange = () => {
3015
+ if (xhr.readyState === 4) resolve({ status: xhr.status, body: xhr.responseText });
3016
+ };
3017
+ xhr.onerror = () => reject(new Error(`Network error for ${path}`));
3018
+ xhr.send(form);
3019
+ });
3020
+ const { status, body } = await this.withTimeout(request, timeoutMs != null ? timeoutMs : this.timeoutMs, path);
3021
+ let parsed = null;
3022
+ try {
3023
+ parsed = body ? JSON.parse(body) : null;
3024
+ } catch {
3025
+ parsed = null;
3026
+ }
3027
+ if (status < 200 || status >= 300) {
3028
+ const detail = parsed == null ? void 0 : parsed.detail;
3029
+ throw new UploadError(status, typeof detail === "string" ? detail : `HTTP ${status} for ${path}`);
3030
+ }
3031
+ return parsed;
3032
+ }
3033
+ async postRaw(path, body, signal) {
3034
+ const response = await this.transport.request({
3035
+ url: this.url(path),
3036
+ method: "POST",
3037
+ headers: this.headers(),
3038
+ body: JSON.stringify(body != null ? body : {}),
3039
+ signal
3040
+ });
3041
+ await raiseForStatus(response, path);
3042
+ return response;
3043
+ }
3044
+ }
3045
+ const RuntimeContext = createContext(void 0);
3046
+ const RegistryContext = createContext(void 0);
3047
+ let moduleRuntime;
3048
+ function configureGuideAI(runtime) {
3049
+ moduleRuntime = runtime;
3050
+ }
3051
+ function useGuideAIRuntime() {
3052
+ const fromContext = useContext(RuntimeContext);
3053
+ const runtime = fromContext != null ? fromContext : moduleRuntime;
3054
+ if (!runtime) {
3055
+ throw new Error(
3056
+ "GuideAI is not configured. Call configureGuideAI(new GuideAIRuntime({...})) at app startup, or wrap your app in <GuideAIProvider>."
3057
+ );
3058
+ }
3059
+ return runtime;
3060
+ }
3061
+ function useScreenRegistry() {
3062
+ return useContext(RegistryContext);
3063
+ }
3064
+ function navigationBridge(source) {
3065
+ const resolve = () => typeof source === "function" ? source() : source;
3066
+ return {
3067
+ navigate(screen) {
3068
+ const ref = resolve();
3069
+ if (!ref || ref.isReady && !ref.isReady()) {
3070
+ throw new Error("navigation_not_ready");
3071
+ }
3072
+ ref.navigate(screen);
3073
+ },
3074
+ goBack() {
3075
+ const ref = resolve();
3076
+ if (!ref || ref.isReady && !ref.isReady()) {
3077
+ throw new Error("navigation_not_ready");
3078
+ }
3079
+ ref.goBack();
3080
+ },
3081
+ canGoBack() {
3082
+ const ref = resolve();
3083
+ if (!ref || ref.isReady && !ref.isReady()) return false;
3084
+ return ref.canGoBack ? ref.canGoBack() : true;
3085
+ }
3086
+ };
3087
+ }
3088
+ const stamp = () => (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
3089
+ let seq = 0;
3090
+ const next = () => seq += 1;
3091
+ function dump(label, value) {
3092
+ let text;
3093
+ try {
3094
+ text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
3095
+ } catch {
3096
+ text = String(value);
3097
+ }
3098
+ console.log(`${label}
3099
+ ${text}`);
3100
+ }
3101
+ function line(icon, text) {
3102
+ console.log(`[${stamp()}] ${icon} ${text}`);
3103
+ }
3104
+ function traceApi(api) {
3105
+ return {
3106
+ supportsStreaming: api.supportsStreaming,
3107
+ async get(path, timeoutMs) {
3108
+ const id = next();
3109
+ line("→", `#${id} GET ${path}`);
3110
+ try {
3111
+ const result = await api.get(path, timeoutMs);
3112
+ dump(`[${stamp()}] ← #${id} GET ${path} responded:`, result);
3113
+ return result;
3114
+ } catch (error) {
3115
+ line("✗", `#${id} GET ${path} FAILED: ${describeError(error)}`);
3116
+ throw error;
3117
+ }
3118
+ },
3119
+ async post(path, body, timeoutMs) {
3120
+ const id = next();
3121
+ dump(`[${stamp()}] → #${id} POST ${path} body:`, body);
3122
+ try {
3123
+ const result = await api.post(path, body, timeoutMs);
3124
+ dump(`[${stamp()}] ← #${id} POST ${path} responded:`, result);
3125
+ return result;
3126
+ } catch (error) {
3127
+ line("✗", `#${id} POST ${path} FAILED: ${describeError(error)}`);
3128
+ throw error;
3129
+ }
3130
+ },
3131
+ async postRaw(path, body, signal) {
3132
+ var _a;
3133
+ const id = next();
3134
+ dump(`[${stamp()}] → #${id} POST ${path} body:`, body);
3135
+ try {
3136
+ const response = await api.postRaw(path, body, signal);
3137
+ line("←", `#${id} ${response.status} ${path} (${(_a = response.headers.get("content-type")) != null ? _a : "no content-type"})`);
3138
+ if (response.stream) {
3139
+ return { ...response, stream: teeStream(response.stream, id) };
3140
+ }
3141
+ return response;
3142
+ } catch (error) {
3143
+ line("✗", `#${id} POST ${path} FAILED: ${describeError(error)}`);
3144
+ throw error;
3145
+ }
3146
+ }
3147
+ };
3148
+ }
3149
+ function teeStream(stream, id) {
3150
+ return {
3151
+ async *[Symbol.asyncIterator]() {
3152
+ var _a;
3153
+ let buffer = "";
3154
+ for await (const chunk of stream) {
3155
+ buffer += chunk;
3156
+ const parts = buffer.split("\n\n");
3157
+ buffer = (_a = parts.pop()) != null ? _a : "";
3158
+ for (const frame of parts) {
3159
+ const trimmed = frame.trim();
3160
+ if (trimmed) console.log(`[${stamp()}] ⋯ #${id} frame: ${trimmed}`);
3161
+ }
3162
+ yield chunk;
3163
+ }
3164
+ if (buffer.trim()) console.log(`[${stamp()}] ⋯ #${id} frame: ${buffer.trim()}`);
3165
+ line("⋯", `#${id} stream ended`);
3166
+ }
3167
+ };
3168
+ }
3169
+ function traceTransport(transport) {
3170
+ return {
3171
+ supportsStreaming: transport.supportsStreaming,
3172
+ async request(req) {
3173
+ var _a;
3174
+ const id = next();
3175
+ console.log(
3176
+ `[${stamp()}] ⇢ #${id} ${req.method} ${req.url}
3177
+ headers: ${JSON.stringify(req.headers)}
3178
+ body: ${(_a = req.body) != null ? _a : "(none)"}`
3179
+ );
3180
+ try {
3181
+ const response = await transport.request(req);
3182
+ line("⇠", `#${id} ${response.status} ${req.url}`);
3183
+ return response;
3184
+ } catch (error) {
3185
+ line("✗", `#${id} ${req.method} ${req.url} FAILED: ${describeError(error)}`);
3186
+ throw error;
3187
+ }
3188
+ }
3189
+ };
3190
+ }
3191
+ function traceTools(tools) {
3192
+ const report = (call, result) => {
3193
+ var _a;
3194
+ if (result.success) {
3195
+ dump(`[${stamp()}] ✓ executed ${call.tool} →`, result.data);
3196
+ } else {
3197
+ line("✗", `${call.tool} FAILED: ${(_a = result.error) != null ? _a : "unknown"}${result.error_type ? ` (${result.error_type})` : ""}`);
3198
+ }
3199
+ };
3200
+ return {
3201
+ definitions: () => tools.definitions(),
3202
+ copilotDefinitions: () => tools.copilotDefinitions(),
3203
+ canExecute: (name) => tools.canExecute(name),
3204
+ canExecuteCall: (call) => tools.canExecuteCall(call),
3205
+ execute: async (call) => {
3206
+ dump(`[${stamp()}] ⚙ TOOL CALL ${call.tool} (${call.method} ${call.path}):`, {
3207
+ params: call.params,
3208
+ body: call.body,
3209
+ requires_confirm: call.requires_confirm
3210
+ });
3211
+ const result = await tools.execute(call);
3212
+ report(call, result);
3213
+ return result;
3214
+ },
3215
+ executeBatch: async (calls, onComplete) => {
3216
+ line("⚙", `BATCH of ${calls.length}: ${calls.map((c) => c.tool).join(", ")}`);
3217
+ return tools.executeBatch(calls, (call, result) => {
3218
+ report(call, result);
3219
+ onComplete == null ? void 0 : onComplete(call, result);
3220
+ });
3221
+ },
3222
+ destroy: tools.destroy ? () => {
3223
+ var _a;
3224
+ return (_a = tools.destroy) == null ? void 0 : _a.call(tools);
3225
+ } : void 0
3226
+ };
3227
+ }
3228
+ function describeError(error) {
3229
+ if (error instanceof Error) {
3230
+ return `${error.name}: ${error.message}`;
3231
+ }
3232
+ return String(error);
3233
+ }
3234
+ function traceUserMessage(text) {
3235
+ console.log(`
3236
+ ${"═".repeat(60)}`);
3237
+ console.log(`[${stamp()}] 👤 YOU: ${text}`);
3238
+ console.log("═".repeat(60));
3239
+ }
3240
+ function traceAssistantAnswer(text, mode) {
3241
+ console.log(`[${stamp()}] 🤖 ASSISTANT${mode ? ` (${mode})` : ""}:
3242
+ ${text}`);
3243
+ console.log(`${"═".repeat(60)}
3244
+ `);
3245
+ }
3246
+ const DEFAULT_BASE_URL = "https://cdn.3guideai.com";
3247
+ let configured = null;
3248
+ function configure(config) {
3249
+ var _a, _b, _c, _d, _e, _f;
3250
+ const debug = (_a = config.debug) != null ? _a : false;
3251
+ const baseApi = new RNApiClient({
3252
+ baseUrl: (_b = config.baseUrl) != null ? _b : DEFAULT_BASE_URL,
3253
+ publicKey: config.apiKey,
3254
+ requestOrigin: config.origin,
3255
+ agent: ((_c = config.agent) == null ? void 0 : _c.trim()) || void 0
3256
+ });
3257
+ const runtime = new GuideAIRuntime({
3258
+ requestOrigin: config.origin,
3259
+ store: config.store,
3260
+ auth: {
3261
+ getAuthToken: config.getAuthToken,
3262
+ onAuthExpired: config.onAuthExpired,
3263
+ getRequestHeaders: config.getRequestHeaders
3264
+ },
3265
+ navigation: config.navigation ? navigationBridge(
3266
+ typeof config.navigation === "function" ? config.navigation : () => config.navigation
3267
+ ) : void 0
3268
+ });
3269
+ configureGuideAI(runtime);
3270
+ const harness = new CopilotHarness(
3271
+ debug ? traceApi(baseApi) : baseApi,
3272
+ {
3273
+ // The site id is resolved server-side from the API key; the
3274
+ // harness only needs a stable string to key its local memory.
3275
+ // Per agent too: a tutor and a course builder in one app must not
3276
+ // read each other's conversation.
3277
+ siteId: ((_d = config.agent) == null ? void 0 : _d.trim()) ? `${config.apiKey}:${config.agent.trim()}` : config.apiKey,
3278
+ assistantLabel: config.assistantName
3279
+ },
3280
+ debug ? traceTools(runtime.tools) : runtime.tools,
3281
+ runtime.storage,
3282
+ debug ? traceTransport(runtime.transport) : runtime.transport,
3283
+ runtime.credentials
3284
+ );
3285
+ if (config.toolContext) harness.setToolContext(config.toolContext);
3286
+ configured = {
3287
+ runtime,
3288
+ harness,
3289
+ api: baseApi,
3290
+ debug,
3291
+ visitorId: (_e = config.visitorId) != null ? _e : null,
3292
+ profile: (_f = config.userProfile) != null ? _f : null
3293
+ };
3294
+ }
3295
+ function identify(userId) {
3296
+ if (!configured) throw new Error("Call GuideAI.configure() before identify().");
3297
+ configured.visitorId = (userId == null ? void 0 : userId.trim()) || null;
3298
+ }
3299
+ function setToolContext(values) {
3300
+ if (!configured) throw new Error("Call GuideAI.configure() before setToolContext().");
3301
+ configured.harness.setToolContext(values != null ? values : void 0);
3302
+ }
3303
+ function setUserProfile(profile) {
3304
+ if (!configured) throw new Error("Call GuideAI.configure() before setUserProfile().");
3305
+ configured.profile = profile;
3306
+ }
3307
+ function requireConfigured() {
3308
+ if (!configured) {
3309
+ throw new Error(
3310
+ "GuideAI is not configured. Call GuideAI.configure({ apiKey, origin }) at app startup, before rendering <GuideAIChat />."
3311
+ );
3312
+ }
3313
+ return configured;
3314
+ }
3315
+ function isConfigured() {
3316
+ return configured !== null;
3317
+ }
3318
+ function reset() {
3319
+ configured = null;
3320
+ }
3321
+ const GuideAI = { configure, identify, setUserProfile, setToolContext, isConfigured, reset };
3322
+ const AUDIO_TYPES = {
3323
+ m4a: "audio/m4a",
3324
+ mp4: "audio/mp4",
3325
+ aac: "audio/aac",
3326
+ caf: "audio/x-caf",
3327
+ wav: "audio/wav",
3328
+ mp3: "audio/mpeg",
3329
+ webm: "audio/webm",
3330
+ ogg: "audio/ogg",
3331
+ "3gp": "audio/3gpp"
3332
+ };
3333
+ function audioType(uri) {
3334
+ var _a, _b;
3335
+ const extension = ((_a = uri.split("?")[0].split(".").pop()) != null ? _a : "").toLowerCase();
3336
+ const type = (_b = AUDIO_TYPES[extension]) != null ? _b : "audio/m4a";
3337
+ return { type, name: `voice.${AUDIO_TYPES[extension] ? extension : "m4a"}` };
3338
+ }
3339
+ function replyFields(result, streamed) {
3340
+ var _a, _b;
3341
+ const { text, images } = headlessImages(answerText(result, streamed));
3342
+ const call = result.confirmationNeeded ? result.pendingToolCall : void 0;
3343
+ return {
3344
+ content: text,
3345
+ images: images.length ? images : void 0,
3346
+ suggestions: ((_a = result.quickReplies) == null ? void 0 : _a.length) ? result.quickReplies.slice(0, 4) : void 0,
3347
+ pendingAction: call ? { label: ((_b = call.label) == null ? void 0 : _b.trim()) || describeToolCall(call), tool: call.tool } : void 0
3348
+ };
3349
+ }
3350
+ let nextId = 0;
3351
+ const nextMessageId = () => `m${nextId += 1}`;
3352
+ function answerText(result, fallback) {
3353
+ var _a, _b;
3354
+ const answer = ((_a = result.answer) != null ? _a : "").trim();
3355
+ if (answer) return answer;
3356
+ const first = (_b = result.guideSteps) == null ? void 0 : _b[0];
3357
+ const body = typeof (first == null ? void 0 : first.body) === "string" ? first.body.trim() : "";
3358
+ if (body) return body;
3359
+ const title = typeof (first == null ? void 0 : first.title) === "string" ? first.title.trim() : "";
3360
+ if (title) return title;
3361
+ return fallback || "Done.";
3362
+ }
3363
+ const FAILURE_MESSAGE = "I'm sorry, I can't complete your request right now. Would you like to talk to a human?";
3364
+ function useGuideAI(options = {}) {
3365
+ var _a, _b;
3366
+ const runtime = useGuideAIRuntime();
3367
+ const configured2 = requireConfigured();
3368
+ const harness = (_a = options.harness) != null ? _a : configured2.harness;
3369
+ const trace = (_b = options.trace) != null ? _b : configured2.debug;
3370
+ const mode = "assistant";
3371
+ const [messages, setMessages] = useState([]);
3372
+ const [input, setInput] = useState("");
3373
+ const [busy, setBusy] = useState(false);
3374
+ const [status, setStatus] = useState(null);
3375
+ const [transcribing, setTranscribing] = useState(false);
3376
+ const lastQuestion = useRef(null);
3377
+ const pending = useRef(/* @__PURE__ */ new Map());
3378
+ const mounted = useRef(true);
3379
+ useEffect(() => () => {
3380
+ mounted.current = false;
3381
+ }, []);
3382
+ useEffect(() => {
3383
+ if (typeof harness.hasHistory !== "function" || typeof harness.getConversationHistory !== "function") return;
3384
+ if (!harness.hasHistory()) return;
3385
+ setMessages((previous) => {
3386
+ if (previous.length > 0) return previous;
3387
+ return harness.getConversationHistory().filter((entry) => typeof entry.content === "string" && entry.content.trim()).map((entry, index) => {
3388
+ var _a2;
3389
+ return {
3390
+ id: (_a2 = entry.id) != null ? _a2 : `h${index}`,
3391
+ role: entry.role,
3392
+ content: entry.content
3393
+ };
3394
+ });
3395
+ });
3396
+ }, [harness]);
3397
+ const initialized = useRef(null);
3398
+ const ensureInitialized = useCallback(() => {
3399
+ if (!initialized.current) initialized.current = harness.initialize();
3400
+ return initialized.current;
3401
+ }, [harness]);
3402
+ const runTurn = useCallback(async (replyId, question, work) => {
3403
+ var _a2, _b2;
3404
+ setBusy(true);
3405
+ setStatus(null);
3406
+ let streamed = "";
3407
+ const appendDelta = (delta) => {
3408
+ streamed += delta;
3409
+ if (!mounted.current) return;
3410
+ setMessages((prev) => prev.map((message) => message.id === replyId ? { ...message, content: message.content + delta } : message));
3411
+ };
3412
+ try {
3413
+ await ((_a2 = runtime.auth) == null ? void 0 : _a2.refresh());
3414
+ const result = await work((next2) => {
3415
+ if (mounted.current) setStatus(next2);
3416
+ }, appendDelta);
3417
+ if (trace) traceAssistantAnswer((_b2 = result.answer) != null ? _b2 : "(empty)", result.mode);
3418
+ if (result.confirmationNeeded && result.pendingToolCall) {
3419
+ pending.current.set(replyId, { call: result.pendingToolCall, question });
3420
+ }
3421
+ if (!mounted.current) return;
3422
+ setMessages((prev) => prev.map((message) => message.id === replyId ? { ...message, ...replyFields(result, streamed), streaming: false } : message));
3423
+ } catch (error) {
3424
+ console.log("[GuideAI] ✗ turn failed:", error);
3425
+ if (error instanceof Error && error.stack) {
3426
+ console.log("[GuideAI] stack:", error.stack);
3427
+ }
3428
+ if (!mounted.current) return;
3429
+ setMessages((prev) => prev.map((message) => message.id === replyId ? {
3430
+ ...message,
3431
+ content: FAILURE_MESSAGE,
3432
+ streaming: false,
3433
+ error: true
3434
+ } : message));
3435
+ } finally {
3436
+ if (mounted.current) {
3437
+ setBusy(false);
3438
+ setStatus(null);
3439
+ }
3440
+ }
3441
+ }, [runtime, trace]);
3442
+ const send = useCallback(async (question) => {
3443
+ const text = (question != null ? question : input).trim();
3444
+ if (!text || busy) return;
3445
+ lastQuestion.current = text;
3446
+ if (question === void 0) setInput("");
3447
+ if (trace) traceUserMessage(text);
3448
+ const replyId = nextMessageId();
3449
+ setMessages((prev) => [
3450
+ ...prev,
3451
+ { id: nextMessageId(), role: "user", content: text },
3452
+ { id: replyId, role: "assistant", content: "", streaming: true }
3453
+ ]);
3454
+ await runTurn(replyId, text, async (onStatus, onDelta) => {
3455
+ await ensureInitialized();
3456
+ const screen = runtime.getActiveScreen();
3457
+ const { profile, visitorId } = requireConfigured();
3458
+ return harness.processMessage(
3459
+ text,
3460
+ screen == null ? void 0 : screen.path,
3461
+ runtime.captureSnapshot(),
3462
+ mode,
3463
+ onStatus,
3464
+ onDelta,
3465
+ void 0,
3466
+ void 0,
3467
+ // Who is asking, as the app knows them; the agent pitches to it.
3468
+ profile ? { profile } : void 0,
3469
+ visitorId != null ? visitorId : void 0
3470
+ );
3471
+ });
3472
+ }, [busy, input, harness, mode, runtime, ensureInitialized, trace, runTurn]);
3473
+ const retry = useCallback(async () => {
3474
+ if (!lastQuestion.current || busy) return;
3475
+ setMessages((prev) => prev.slice(0, -2));
3476
+ await send(lastQuestion.current);
3477
+ }, [busy, send]);
3478
+ const clear = useCallback(() => {
3479
+ setMessages([]);
3480
+ lastQuestion.current = null;
3481
+ pending.current.clear();
3482
+ }, []);
3483
+ const settleAction = useCallback((messageId) => {
3484
+ const action = pending.current.get(messageId);
3485
+ pending.current.delete(messageId);
3486
+ setMessages((prev) => prev.map((message) => message.id === messageId ? { ...message, pendingAction: void 0 } : message));
3487
+ return action;
3488
+ }, []);
3489
+ const confirm = useCallback(async (messageId) => {
3490
+ if (busy) return;
3491
+ const action = settleAction(messageId);
3492
+ if (!action) return;
3493
+ const replyId = nextMessageId();
3494
+ setMessages((prev) => [...prev, { id: replyId, role: "assistant", content: "", streaming: true }]);
3495
+ await runTurn(replyId, action.question, async (onStatus, onDelta) => {
3496
+ await ensureInitialized();
3497
+ return harness.executeToolCall(action.call, action.question, onStatus, onDelta);
3498
+ });
3499
+ }, [busy, settleAction, runTurn, ensureInitialized, harness]);
3500
+ const cancel = useCallback((messageId) => {
3501
+ if (!settleAction(messageId)) return;
3502
+ setMessages((prev) => [...prev, { id: nextMessageId(), role: "assistant", content: "Okay, I haven't changed anything." }]);
3503
+ }, [settleAction]);
3504
+ const sendVoice = useCallback(async (recording) => {
3505
+ var _a2;
3506
+ if (busy || transcribing) return { text: null, error: "Wait for the current reply to finish." };
3507
+ const guessed = audioType(recording.uri);
3508
+ setTranscribing(true);
3509
+ try {
3510
+ const result = await requireConfigured().api.postFile(
3511
+ EP.batchVoiceTranscribe(),
3512
+ "audio",
3513
+ { uri: recording.uri, name: guessed.name, type: recording.mimeType || guessed.type },
3514
+ { language_code: recording.languageCode || "en-US" },
3515
+ 45e3
3516
+ );
3517
+ const heard = ((_a2 = result == null ? void 0 : result.text) == null ? void 0 : _a2.trim()) || "";
3518
+ if (!heard) return { text: null, error: "I didn't catch that. Please try again." };
3519
+ if (mounted.current) setTranscribing(false);
3520
+ await send(heard);
3521
+ return { text: heard };
3522
+ } catch (error) {
3523
+ console.log("[GuideAI] ✗ voice transcription failed:", error);
3524
+ const message = error instanceof UploadError && error.status === 422 ? error.message : "I couldn't process that recording. Please try again or type your message.";
3525
+ return { text: null, error: message };
3526
+ } finally {
3527
+ if (mounted.current) setTranscribing(false);
3528
+ }
3529
+ }, [busy, transcribing, send]);
3530
+ return {
3531
+ messages,
3532
+ input,
3533
+ setInput,
3534
+ send,
3535
+ busy,
3536
+ canSend: Boolean(input.trim()) && !busy,
3537
+ status,
3538
+ retry,
3539
+ clear,
3540
+ confirm,
3541
+ cancel,
3542
+ sendVoice,
3543
+ transcribing
3544
+ };
3545
+ }
3546
+ function useOnboarding() {
3547
+ const runtime = useGuideAIRuntime();
3548
+ const [checklist, setChecklist] = useState(null);
3549
+ const [loading, setLoading] = useState(true);
3550
+ const refresh = useCallback(async () => {
3551
+ var _a;
3552
+ const { api, visitorId } = requireConfigured();
3553
+ if (!visitorId) {
3554
+ setChecklist(null);
3555
+ setLoading(false);
3556
+ return;
3557
+ }
3558
+ try {
3559
+ const current = await api.get(EP.onboardingCurrent(visitorId));
3560
+ const screen = (_a = runtime.getActiveScreen()) == null ? void 0 : _a.path;
3561
+ const updated = current && !current.completed && !current.dismissed && screen ? await api.post(EP.onboardingObserve(), { user_id: visitorId, screen }) : current;
3562
+ setChecklist(updated);
3563
+ } catch {
3564
+ } finally {
3565
+ setLoading(false);
3566
+ }
3567
+ }, [runtime]);
3568
+ const dismiss = useCallback(async () => {
3569
+ const { api, visitorId } = requireConfigured();
3570
+ if (!visitorId) return;
3571
+ const updated = await api.post(EP.onboardingDismiss(), { user_id: visitorId });
3572
+ setChecklist(updated);
3573
+ }, []);
3574
+ useEffect(() => {
3575
+ void refresh();
3576
+ const timer = setInterval(() => void refresh(), 5e3);
3577
+ return () => clearInterval(timer);
3578
+ }, [refresh]);
3579
+ return { checklist, loading, refresh, dismiss };
3580
+ }
3581
+ export {
3582
+ EP as E,
3583
+ GuideAI as G,
3584
+ RegistryContext as R,
3585
+ ScreenRegistry as S,
3586
+ useScreenRegistry as a,
3587
+ useGuideAI as b,
3588
+ useOnboarding as c,
3589
+ requireConfigured as r,
3590
+ useGuideAIRuntime as u
3591
+ };
3592
+ //# sourceMappingURL=useOnboarding-DAEGoYSs.js.map