@guideai/react-native 0.0.0-stage → 0.1.0

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