@appilots/web-sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1389 @@
1
+ /** Source facts, not an executable locator or proof of a business outcome. */
2
+ interface ControlEvidence {
3
+ version: 1;
4
+ siteId: string;
5
+ component: string;
6
+ icons: string[];
7
+ handler?: string;
8
+ calls: string[];
9
+ argumentBindings: string[];
10
+ conditions: string[];
11
+ nativeConfirmation?: {
12
+ title?: string;
13
+ destructiveOption: boolean;
14
+ };
15
+ }
16
+ /** Receipt of the app callback actually invoked after user approval; not database proof. */
17
+ interface NativeConfirmationReceipt {
18
+ title: string;
19
+ message?: string;
20
+ buttonLabel?: string;
21
+ handlerCompleted: boolean;
22
+ }
23
+
24
+ /** Customer conversation/UI locales. Map text and host-app labels may use other languages. */
25
+ declare const SUPPORTED_APPILOTS_LOCALES: readonly ["pt-BR", "en", "es", "fr"];
26
+ type SupportedAppilotsLocale = (typeof SUPPORTED_APPILOTS_LOCALES)[number];
27
+
28
+ type MissionStatus = 'awaiting_approval' | 'running' | 'paused' | 'blocked' | 'completed' | 'cancelled';
29
+ interface MissionScope {
30
+ id: string;
31
+ revision: number;
32
+ operation: 'delete';
33
+ screen: string;
34
+ listId: string;
35
+ items: Array<{
36
+ key: string;
37
+ label: string;
38
+ }>;
39
+ expiresAt: number;
40
+ }
41
+ interface MissionView {
42
+ scope: MissionScope;
43
+ objective: string;
44
+ status: MissionStatus;
45
+ completed: number;
46
+ total: number;
47
+ reason?: string;
48
+ progressText: string;
49
+ }
50
+
51
+ /**
52
+ * Theme tokens — the source of truth for any color, font, radius or
53
+ * shadow used inside the SDK's chat UI. Components must read from these
54
+ * tokens (via {@link useAppilotsTheme}) rather than hardcoding values, so
55
+ * a developer who sets a `theme` prop or dashboard config can re-skin
56
+ * the whole chat without forking the SDK.
57
+ *
58
+ * BACKLOG 4.1.
59
+ *
60
+ * Adding a new token:
61
+ * 1. Add a field to `AppilotsThemeTokens` below.
62
+ * 2. Set a value in BOTH `defaultLight` and `defaultDark` (and ideally
63
+ * keep them visually consistent — e.g. dark text on light bg ↔
64
+ * light text on dark bg).
65
+ * 3. Use it in components via `useAppilotsTheme()`.
66
+ * 4. Mirror the field on `themeTokensSchema` in `@appilots/shared` if
67
+ * it should be settable from the dashboard.
68
+ *
69
+ * Removing a token is a breaking change — bump the SDK minor.
70
+ */
71
+ interface AppilotsThemeTokens {
72
+ /** Hex color values. The 8-digit form (#RRGGBBAA) is allowed. */
73
+ colors: {
74
+ /** Brand color — send button, FAB, header dot, links. */
75
+ primary: string;
76
+ /** Secondary brand accent — used sparingly (e.g. the human-agent/operator icon accent in message rows). */
77
+ secondary: string;
78
+ /** Page/modal background, behind the messages list. */
79
+ background: string;
80
+ /** Surface color for assistant message bubbles, input field, chips. */
81
+ surface: string;
82
+ /** Default text color (high contrast on background). */
83
+ text: string;
84
+ /** Muted text — placeholders, secondary labels, "Powered by". */
85
+ textSecondary: string;
86
+ /** 1px hairlines under header, around chips. */
87
+ border: string;
88
+ /** Success state — completed action checks in the breadcrumb (ActionBreadcrumb `success` tone). */
89
+ success: string;
90
+ /**
91
+ * Warning / caution state — drives the breadcrumb's `pending` tone
92
+ * (queued, not-yet-run actions). The default is a neutral gray, not
93
+ * amber, so it renders pixel-identical to the SDK's pre-4.2 hardcoded
94
+ * palette out of the box; set an amber/yellow here to make queued
95
+ * actions visually louder.
96
+ */
97
+ warning: string;
98
+ /** Error state — failed action rows (ActionBreadcrumb `failed` tone) and destructive badges/CTAs (confirm card, reject button). */
99
+ error: string;
100
+ /** Text color for chip/button labels (e.g. suggested prompts). */
101
+ buttonText: string;
102
+ /** Text color inside the assistant's message bubble (not the user's — that stays white on `primary`). */
103
+ bubbleText: string;
104
+ /**
105
+ * Text color inside the USER's message bubble (on `primary`). Optional
106
+ * mirror of `bubbleText` for the user side — falls back to white,
107
+ * matching the SDK's pre-4.2 hardcoded value, when unset.
108
+ */
109
+ userBubbleText: string;
110
+ };
111
+ typography: {
112
+ /** Font family applied to ALL chat text. Falls back to system. */
113
+ fontFamily: string;
114
+ fontSizeXs: number;
115
+ fontSizeSm: number;
116
+ fontSizeBase: number;
117
+ fontSizeLg: number;
118
+ };
119
+ radii: {
120
+ /** Tight corners — chips, small badges. */
121
+ sm: number;
122
+ /** Default — input field, send button, secondary buttons. */
123
+ md: number;
124
+ /** Generous — chat sheet, message bubbles. */
125
+ lg: number;
126
+ };
127
+ /** Optional drop-shadows (RN-shaped, applied via View style). */
128
+ shadows: {
129
+ /** Tells the developer's dark/light intent. Components don't read
130
+ * this directly — they read tokens — but `mode` is forwarded so
131
+ * status bar / image color choices can branch on it. */
132
+ appearance: 'light' | 'dark';
133
+ };
134
+ }
135
+ /**
136
+ * Deep-partial of the token tree — what a developer hands to the SDK
137
+ * via the `theme` prop or dashboard config. Every field optional;
138
+ * unspecified fields fall through to the default.
139
+ */
140
+ type PartialThemeTokens = {
141
+ colors?: Partial<AppilotsThemeTokens['colors']>;
142
+ typography?: Partial<AppilotsThemeTokens['typography']>;
143
+ radii?: Partial<AppilotsThemeTokens['radii']>;
144
+ shadows?: Partial<AppilotsThemeTokens['shadows']>;
145
+ };
146
+
147
+ /**
148
+ * SDK locale code. Canonical source of truth for `AppilotsLocale` — moved
149
+ * here (out of the React Native-coupled `i18n/I18nProvider`) so that
150
+ * platform-agnostic contract types (`RemotePersonalization.defaultLocale`)
151
+ * don't pull in a React/React Native import just for a string union type.
152
+ *
153
+ * `@appilots/sdk`'s `i18n/I18nProvider.tsx` re-exports this type from its
154
+ * original public path (`@appilots/sdk`'s `./i18n` barrel) so nothing
155
+ * downstream of the SDK's public API changes.
156
+ *
157
+ * Adding a locale = adding a bundle file in `@appilots/sdk`'s
158
+ * `i18n/bundles/` AND a branch in `i18n/I18nProvider.tsx`'s `BUNDLES`
159
+ * map AND a variant here. BACKLOG 4.1.
160
+ */
161
+ type AppilotsLocale = SupportedAppilotsLocale;
162
+
163
+ /**
164
+ * Core types for the Appilots SDK.
165
+ */
166
+
167
+ /**
168
+ * Payload of `GET /agent/personalization` — what the dev configured in
169
+ * the dashboard under Projects > Personalization. Fetched once on
170
+ * `AppilotsProvider` mount and applied by `AppilotsChat` as a fallback
171
+ * for any prop the dev did NOT set explicitly in code
172
+ * (prop > remote > SDK default).
173
+ *
174
+ * Only JSON-serializable values travel here: URLs/hex/booleans work,
175
+ * React nodes and local `require()` assets can only come via props.
176
+ * `null` means "not configured" — fall through to the next source.
177
+ */
178
+ interface RemotePersonalization {
179
+ assistantName: string | null;
180
+ /** Avatar URL shown next to assistant turns. */
181
+ assistantAvatar: string | null;
182
+ /** Emoji or image URL for the empty-state badge. */
183
+ emptyStateIcon: string | null;
184
+ welcomeMessage: string | null;
185
+ chatTitle: string | null;
186
+ poweredByVisible: boolean;
187
+ /**
188
+ * Partial token tree merged onto the SDK defaults. `mode` (when set)
189
+ * plays the role of the `themeMode` prop.
190
+ */
191
+ theme: (PartialThemeTokens & {
192
+ mode?: 'auto' | 'light' | 'dark';
193
+ }) | null;
194
+ defaultLocale: AppilotsLocale | null;
195
+ triggerButtonColor: string | null;
196
+ triggerButtonImageUrl: string | null;
197
+ }
198
+ type MessageRole = 'user' | 'assistant' | 'system' | 'human_agent';
199
+ interface ChatMessage {
200
+ id: string;
201
+ role: MessageRole;
202
+ content: string;
203
+ timestamp: number;
204
+ metadata?: Record<string, unknown>;
205
+ }
206
+ type AgentActionType = 'navigate' | 'form_fill' | 'ui_interaction' | 'scroll_list' | 'confirm' | 'custom';
207
+ interface AgentAction {
208
+ id: string;
209
+ type: AgentActionType;
210
+ payload: NavigationPayload | FormFillPayload | UIInteractionPayload | ScrollListPayload | Record<string, unknown>;
211
+ status: 'pending' | 'executing' | 'completed' | 'failed';
212
+ error?: string;
213
+ /** ID of the assistant message that triggered this action */
214
+ messageId?: string;
215
+ /**
216
+ * BACKLOG 2.2 — When the server flags an action as destructive,
217
+ * the SDK renders any preceding `confirm` action with the danger
218
+ * variant and expects the action to be gated by user approval.
219
+ */
220
+ destructive?: boolean;
221
+ }
222
+ interface NavigationPayload {
223
+ /** Absent only for `goBack`, which has no destination to name (#398). */
224
+ screenName?: string;
225
+ params?: Record<string, unknown>;
226
+ navigationAction: 'push' | 'navigate' | 'replace' | 'goBack' | 'reset';
227
+ /**
228
+ * Optional path of parent navigator route names for nested screens.
229
+ * E.g. ['HomeTab', 'VehiclesTab'] to reach a screen inside
230
+ * HomeTab → VehiclesTab → screenName.
231
+ *
232
+ * React Navigation requires nested navigation params:
233
+ * nav.navigate('HomeTab', { screen: 'VehiclesTab', params: { screen: 'VehicleCreate' } })
234
+ *
235
+ * The AI model provides this based on the MCP document's navigation graph.
236
+ */
237
+ path?: string[];
238
+ }
239
+ interface FormFillPayload {
240
+ screenName: string;
241
+ fields: FormFieldAction[];
242
+ /**
243
+ * If true, the handler presses the screen's primary submit button
244
+ * after all fields are filled. The submit button is resolved via
245
+ * `registerScreen({ actions: [{ type: 'submit', ... }] })` first,
246
+ * then via the fiber tree and a label heuristic.
247
+ *
248
+ * Aligned with the `submitAfterFill` field in the LLM tool schema
249
+ * (`agentTools.form_fill` in ai-relay.ts).
250
+ */
251
+ submitAfterFill?: boolean;
252
+ }
253
+ interface FormFieldAction {
254
+ fieldId: string;
255
+ fieldType: 'text' | 'select' | 'toggle' | 'date' | 'number' | 'custom';
256
+ value: unknown;
257
+ label?: string;
258
+ }
259
+ interface UIInteractionPayload {
260
+ componentId: string;
261
+ /** Canonical server/LLM identifier. `componentId` is kept as SDK alias. */
262
+ targetId?: string;
263
+ action: 'press' | 'longPress' | 'toggle' | 'scroll' | 'swipe' | 'focus' | 'set_value';
264
+ /**
265
+ * Numeric value for `set_value` (sliders / adjustable controls).
266
+ * The handler clamps to the slider's [min, max] and snaps to its step.
267
+ */
268
+ value?: number;
269
+ params?: Record<string, unknown>;
270
+ }
271
+ /**
272
+ * Scroll a list/collection so virtualized off-screen rows mount and
273
+ * show up in the next observation. Mirrors `scrollListPayloadSchema`
274
+ * in @appilots/shared. At least one of the fields should be present;
275
+ * the handler falls back to paging one viewport down when only a
276
+ * listId is given.
277
+ */
278
+ interface ScrollListPayload {
279
+ /** Runtime list id from the observation's "Lists visible" block. */
280
+ listId?: string;
281
+ /** 1-based index in the list's DATA to scroll to. */
282
+ toIndex?: number;
283
+ /** Coarse paging when no target index is known. */
284
+ direction?: 'up' | 'down';
285
+ }
286
+ interface AgentPermissions {
287
+ canNavigate: boolean;
288
+ canFillForms: boolean;
289
+ canInteractUI: boolean;
290
+ canSubmitForms: boolean;
291
+ allowedScreens?: string[];
292
+ blockedScreens?: string[];
293
+ allowedActions?: AgentActionType[];
294
+ }
295
+ type AppilotsEventType = 'agent:action:start'
296
+ /** Queue dispatch, after approval; distinct from a proposed action. */
297
+ | 'agent:action:executing' | 'agent:action:complete' | 'agent:action:error' | 'agent:message' | 'navigation:change' | 'chat:open' | 'chat:close' | 'chat:cancel' | 'chat:escalation:start' | 'chat:escalation:message' | 'chat:escalation:end' | 'sdk:introspection:unavailable';
298
+ interface AppilotsEvent {
299
+ type: AppilotsEventType;
300
+ timestamp: number;
301
+ data: Record<string, unknown>;
302
+ }
303
+ type AppilotsEventHandler = (event: AppilotsEvent) => void;
304
+ /**
305
+ * Client-side view of an open escalation. `resolved` never appears
306
+ * here — resolution clears the state (after surfacing a system
307
+ * message in the transcript).
308
+ */
309
+ interface EscalationState {
310
+ id: string;
311
+ status: 'pending' | 'active';
312
+ /** Set once an operator claims the conversation. */
313
+ operatorName?: string;
314
+ }
315
+
316
+ /** Optional host storage; only a session handle is stored, never screen contents. */
317
+ interface AppilotsSessionStorage {
318
+ getItem(key: string): Promise<string | null>;
319
+ setItem(key: string, value: string): Promise<void>;
320
+ removeItem(key: string): Promise<void>;
321
+ }
322
+ /**
323
+ * Mirrors `introspectionDiagnosticsSchema` on the wire. Declared here
324
+ * rather than imported so client-core stays free of the server's
325
+ * validator package — which means the two must be kept in step by hand.
326
+ * Adding a reason to one and not the other is a typecheck failure at
327
+ * the SDK, not a silent wire mismatch.
328
+ */
329
+ interface IntrospectionDiagnosticsInput {
330
+ captured: boolean;
331
+ failureReason?: 'sentinel-missing-internals' | 'never-mounted' | 'render-crashed' | 'names-mangled' | 'walk-recognized-nothing' | null;
332
+ reactVersion?: string | null;
333
+ failureCount?: number;
334
+ }
335
+ interface AppilotsClientOptions {
336
+ sessionStorage?: AppilotsSessionStorage;
337
+ projectId: string;
338
+ apiBaseUrl?: string;
339
+ apiKey?: string;
340
+ debug?: boolean;
341
+ timeout?: number;
342
+ headers?: Record<string, string>;
343
+ /**
344
+ * Version of the MCP doc bundled with this app build. The server
345
+ * compares this against the currently-active MCP doc for the
346
+ * project and emits a `mcp_version_mismatch` telemetry event when
347
+ * they differ — the canonical sign that the dev shipped an app
348
+ * update but forgot to regenerate/upload the MCP.
349
+ *
350
+ * Tip: import this from the MCP file the CLI generates, or read it
351
+ * from your app's package.json so it stays in lockstep with releases.
352
+ */
353
+ mcpVersion?: string;
354
+ /**
355
+ * Reports whether the platform binding could read the host app's UI
356
+ * tree. Called once per message; return `null` (or omit the option)
357
+ * when there is nothing to report.
358
+ *
359
+ * Only a FAILED reading is put on the wire. When introspection is
360
+ * dead every observation is empty but perfectly well-formed, so the
361
+ * server cannot tell a blind session from a user looking at empty
362
+ * screens — this is the only signal that distinguishes them.
363
+ */
364
+ introspectionReporter?: () => IntrospectionDiagnosticsInput | null | undefined;
365
+ /**
366
+ * The host app's version (e.g. from package.json or
367
+ * react-native-device-info). Sent as `X-App-Version` on every request
368
+ * so the server can match the compatible MCP document per app build —
369
+ * essential for OTA-updated fleets where multiple app versions coexist.
370
+ */
371
+ appVersion?: string;
372
+ /**
373
+ * The version of the SDK package driving this client, sent as
374
+ * `X-Appilots-Sdk-Version`. Each platform SDK passes its OWN published
375
+ * version (`@appilots/sdk`, `@appilots/web-sdk`); `@appilots/client-core`
376
+ * is private and never published, so its version means nothing to a
377
+ * customer and is only the fallback for a caller that constructs
378
+ * `AppilotsClient` directly.
379
+ *
380
+ * This is the server's only evidence of which SDKs are in the field,
381
+ * and therefore the only input to "is this field safe to tighten yet?"
382
+ * (issue #312). Leaving it wrong is not cosmetic: it makes the
383
+ * backward-compatibility promise in CLAUDE.md unverifiable.
384
+ */
385
+ sdkVersion?: string;
386
+ /**
387
+ * Who the app's current user is. `id` becomes the session's
388
+ * externalUserId; `name`/`identifiers` are upserted into the
389
+ * workspace's end-users registry (fire-and-forget identify after the
390
+ * session opens) so support operators see a person, not an anonymous
391
+ * id. Identifiers are dashboard-only — never sent to the LLM.
392
+ */
393
+ user?: AppilotsUser;
394
+ }
395
+ interface AppilotsUser {
396
+ /** The app's own user id (matches your backend). */
397
+ id: string;
398
+ name?: string;
399
+ /** Free-form lookup keys: { email, phone, cpf, ... }. Max 10. */
400
+ identifiers?: Record<string, string>;
401
+ }
402
+ interface SendMessageResponse {
403
+ mission?: MissionView;
404
+ replyLocale?: SupportedAppilotsLocale;
405
+ sessionId: string;
406
+ message: ChatMessage;
407
+ /** Summary message to show AFTER actions complete */
408
+ summaryMessage?: ChatMessage;
409
+ actions?: AgentAction[];
410
+ usage?: {
411
+ tokensUsed: number;
412
+ modelUsed: string;
413
+ latencyMs: number;
414
+ };
415
+ }
416
+ interface SendMessageStreamOptions {
417
+ /**
418
+ * Fired per SSE text-delta with the chunk and the accumulated text so
419
+ * far — render `fullText` directly; chunks may split words/markdown.
420
+ */
421
+ onTextDelta?: (chunk: string, fullText: string) => void;
422
+ /** Fired once when the server announces the session id. */
423
+ onSession?: (sessionId: string) => void;
424
+ onReplyLocale?: (locale: SupportedAppilotsLocale) => void;
425
+ /** Abort mid-generation (stop button). Rejects with name='AbortError'. */
426
+ signal?: AbortSignal;
427
+ }
428
+ /**
429
+ * The server refused the request because this workspace (or this end
430
+ * user) is going too fast — HTTP 429, `error.code = 'RATE_LIMITED'`.
431
+ * See `apps/api/src/common/services/relay-rate-limit.ts`.
432
+ *
433
+ * A distinct type for two reasons, both of which the plain `Error` the
434
+ * transport used to throw could not express:
435
+ *
436
+ * - It is a POLICY rejection, not a transport failure. On the streaming
437
+ * path that distinction decides whether the caller retries: a stream
438
+ * that dies before its first event is safely retried against the
439
+ * non-streaming endpoint, but retrying a 429 immediately is exactly
440
+ * the behaviour the limit exists to stop, and it earns a second 429.
441
+ * - It carries `retryAfterSeconds`, so a caller can say WHEN instead of
442
+ * "try again later", and can back off instead of spinning.
443
+ *
444
+ * `retryAfterSeconds` is read from the response envelope, falling back
445
+ * to the `Retry-After` header and then to 60 — a client that waits a
446
+ * minute is never wrong by much, and never zero.
447
+ */
448
+ declare class RateLimitedError extends Error {
449
+ readonly retryAfterSeconds: number;
450
+ constructor(message: string, retryAfterSeconds: number);
451
+ }
452
+ /** Runtime context sent with every user message. */
453
+ interface SendMessageContext {
454
+ missionProtocol?: 1;
455
+ missionId?: string;
456
+ /** Supported device language, independent of the app/map/visible UI locale. */
457
+ deviceLocale?: SupportedAppilotsLocale;
458
+ /**
459
+ * Which client platform this observation came from — `'react-native'`,
460
+ * `'web'`, `'android'`, `'ios'`, or any other client-defined string.
461
+ * The server treats an absent value as `'react-native'` (see
462
+ * `docs/agent-contract.md`'s "The `platform` field" section), so
463
+ * setting it is optional but forward-compatible: a client that sends
464
+ * it explicitly isn't relying on that default staying `'react-native'`
465
+ * forever.
466
+ */
467
+ platform?: string;
468
+ currentScreen?: string;
469
+ formState?: Record<string, unknown>;
470
+ /** Design-time screen metadata from registerScreen() or the MCP doc */
471
+ screenMetadata?: Record<string, unknown>;
472
+ /** Runtime React Navigation summary captured by the SDK */
473
+ navigationState?: {
474
+ currentRouteName?: string;
475
+ activePath?: string[];
476
+ rootRouteNames?: string[];
477
+ currentRouteNames?: string[];
478
+ routeNames?: string[];
479
+ canGoBack?: boolean;
480
+ };
481
+ /** Runtime snapshot from captureSnapshot() — what's actually visible */
482
+ snapshot?: Record<string, unknown>;
483
+ /** Legacy registry snapshot — kept as a fallback signal */
484
+ registeredComponents?: Array<{
485
+ id: string;
486
+ kind: 'field' | 'target' | 'toggle';
487
+ screen?: string;
488
+ label?: string;
489
+ }>;
490
+ /** Runtime list registry snapshot — collection metadata even when rows are virtualized. */
491
+ registeredLists?: Array<{
492
+ kind: 'list';
493
+ id: string;
494
+ component: string;
495
+ itemCount?: number;
496
+ label?: string;
497
+ screen?: string;
498
+ refreshing?: boolean;
499
+ empty?: boolean;
500
+ }>;
501
+ }
502
+
503
+ declare class AppilotsClient {
504
+ private readonly baseUrl;
505
+ private readonly projectId;
506
+ private readonly headers;
507
+ private readonly timeout;
508
+ private readonly debug;
509
+ private readonly mcpVersion;
510
+ private readonly introspectionReporter;
511
+ private readonly user;
512
+ private sessionId;
513
+ private mission;
514
+ private missionDispatchPaused;
515
+ private readonly sessionStorage;
516
+ private readonly sessionStorageKey;
517
+ private sessionLoad;
518
+ /** Guard so identify fires at most once per client instance. */
519
+ private identified;
520
+ constructor(options: AppilotsClientOptions);
521
+ /**
522
+ * A 404 on an `/agent/*` path is almost never a missing record — those
523
+ * routes are static. It means the request reached SOMETHING that is not
524
+ * this API, or reached it at the wrong mount point, and the server's own
525
+ * message ("Route not found") tells the integrator nothing about which.
526
+ *
527
+ * The symptom is unmistakable once you have seen it: every message
528
+ * fails, including a plain "hello" that needs no tool at all, because
529
+ * nothing ever reaches the agent. Left as-is it reads like the agent is
530
+ * broken.
531
+ */
532
+ private explainIfMisroutedBaseUrl;
533
+ private log;
534
+ private describeNonJsonResponse;
535
+ private request;
536
+ createSession(userId?: string, deviceInfo?: Record<string, unknown>): Promise<string>;
537
+ /**
538
+ * Upsert the configured user into the end-users registry. Fires once
539
+ * per client, after the first session opens; fire-and-forget — a
540
+ * failed identify must never break the chat.
541
+ */
542
+ private identifyConfiguredUser;
543
+ destroySession(): Promise<void>;
544
+ getSessionId(): string | null;
545
+ /**
546
+ * Wire fragment for introspection health. Empty on the happy path —
547
+ * a working client sends nothing, so the field costs a byte only
548
+ * when something is actually wrong. Never throws: a broken reporter
549
+ * must not take the message down with it.
550
+ */
551
+ private introspectionFragment;
552
+ sendMessage(content: string, context?: SendMessageContext): Promise<SendMessageResponse>;
553
+ /**
554
+ * Streaming variant of `sendMessage` — consumes the server's SSE
555
+ * endpoint (`/agent/message/stream`) and fires `onTextDelta` per chunk
556
+ * so the UI can render tokens as they arrive (OKR-008 KR1). Resolves
557
+ * with the same `SendMessageResponse` shape once the `done` event lands.
558
+ *
559
+ * Uses XMLHttpRequest because React Native's fetch does not expose
560
+ * `response.body` for incremental reads; RN's XHR delivers progressive
561
+ * `responseText`, which is the standard SSE transport on RN.
562
+ *
563
+ * Errors:
564
+ * - `StreamTransportError` — transport failed before ANY event; safe
565
+ * for the caller to fall back to the non-streaming endpoint.
566
+ * - `StreamAbortError` (name='AbortError') — options.signal fired;
567
+ * carries `partialContent` so the UI can keep what streamed.
568
+ */
569
+ sendMessageStream(content: string, context?: SendMessageContext, options?: SendMessageStreamOptions): Promise<SendMessageResponse>;
570
+ continueAgent(results: Array<{
571
+ actionId: string;
572
+ type: string;
573
+ success: boolean;
574
+ error?: string;
575
+ summary?: string;
576
+ /** BACKLOG 3.2 — structured failure diagnose, server uses for recovery */
577
+ diagnose?: {
578
+ category: 'component-not-found' | 'disabled' | 'validation' | 'network-5xx' | 'network-4xx' | 'screen-timeout' | 'ambiguous-target' | 'unknown';
579
+ visibleMessage?: string;
580
+ fieldId?: string;
581
+ targetId?: string;
582
+ screen?: string;
583
+ httpStatus?: number;
584
+ /** Candidate ids when resolution failed due to ambiguity. */
585
+ candidates?: string[];
586
+ recoverable?: boolean;
587
+ requiresUserInput?: boolean;
588
+ };
589
+ /** BACKLOG 3.2 — how many times this fingerprint has been retried (1 = first retry). */
590
+ recoveryAttempt?: number;
591
+ /** BACKLOG 3.2 — set when SDK has hit the per-action retry cap. */
592
+ recoveryExhausted?: boolean;
593
+ /**
594
+ * Set when a handler reported success but the expected visible
595
+ * side-effect did not happen, e.g. navigate returned but route stayed.
596
+ */
597
+ toolWithoutEffect?: boolean;
598
+ effect?: 'unknown' | 'none' | 'partial' | 'changed' | 'completed';
599
+ userVisibleStatusKey?: string;
600
+ }>, context: Record<string, unknown>, hop: number): Promise<SendMessageResponse & {
601
+ isDone: boolean;
602
+ hop: number;
603
+ }>;
604
+ private acceptMission;
605
+ getMission(): MissionView | null;
606
+ pauseMissionLocally(): void;
607
+ missionApprovalAllowed(action: AgentAction): boolean;
608
+ missionScopeForAction(action: AgentAction): MissionScope | undefined;
609
+ claimMissionAction(action: AgentAction, context: Record<string, unknown>): Promise<{
610
+ allowed: boolean;
611
+ scope: MissionScope;
612
+ expiresAt: number;
613
+ }>;
614
+ controlMission(command: 'observe' | 'pause' | 'resume' | 'cancel', context: Record<string, unknown>): Promise<SendMessageResponse & {
615
+ isDone: boolean;
616
+ hop: number;
617
+ }>;
618
+ private persistSession;
619
+ private loadStoredSession;
620
+ restoreMission(): Promise<MissionView | null>;
621
+ executeAction(actionId: string): Promise<AgentAction>;
622
+ /**
623
+ * Rewrite an action's type when the AI used a semantically-misclassified
624
+ * action type. The most common case: the LLM emits
625
+ * { type: 'ui_interaction', payload: { action: 'select', targetId, value } }
626
+ * to mean "set this field's value", which is really a form_fill. We detect
627
+ * that here and convert it before normalization, so the executor sees the
628
+ * right action shape and the field's setValue() is called instead of a
629
+ * meaningless press on a non-pressable component.
630
+ */
631
+ private rewriteActionTypeIfNeeded;
632
+ /**
633
+ * Normalize API tool call payloads to match SDK's expected payload types.
634
+ * The AI model may return slightly different field names than what the
635
+ * SDK executor expects, and may use vocabulary (e.g. "select", "scroll_to",
636
+ * "open") that the SDK doesn't natively support — we remap those to the
637
+ * nearest equivalent here so the executor never sees an "unknown action".
638
+ */
639
+ private normalizePayload;
640
+ /**
641
+ * Legacy per-action status report. The agentic chat loop reports via
642
+ * `continueAgent` (which carries diagnose + a fresh snapshot); this
643
+ * endpoint remains for apps driving actions manually through
644
+ * `useAppilotsActions`. `diagnose` is forwarded when provided so even
645
+ * the legacy path gives the server a failure category.
646
+ */
647
+ completeAction(actionId: string, success: boolean, error?: string, diagnose?: Record<string, unknown>): Promise<void>;
648
+ /**
649
+ * Fetch actions the server has persisted but that haven't been
650
+ * completed yet. Use after an SSE drop or a hard error to recover
651
+ * the batch without losing the actions the model already decided.
652
+ *
653
+ * Returns an empty array if there are no pending actions or if the
654
+ * call fails — never throws.
655
+ */
656
+ recoverPendingActions(sessionId?: string): Promise<AgentAction[]>;
657
+ /**
658
+ * Fetch dev-defined and empirically-popular prompts to seed the chat
659
+ * empty-state with one-tap chips. Cheap (cached server-side for 1h) but
660
+ * still a network hit, so callers should debounce around chat-open.
661
+ *
662
+ * @param screen Optional — when provided, response includes screen-scoped
663
+ * popular prompts in addition to globals.
664
+ */
665
+ getPopularPrompts(screen?: string, limit?: number): Promise<{
666
+ devDefined: string[];
667
+ screenPopular: string[];
668
+ globalPopular: string[];
669
+ computedAt: string;
670
+ }>;
671
+ /**
672
+ * Fetch the project's dashboard-configured personalization (theme,
673
+ * branding, FAB color/image, locale...). Called by `AppilotsProvider`
674
+ * once on mount — cheap (cached server-side for 1h) and applied by
675
+ * `AppilotsChat` as a fallback for props not set explicitly in code.
676
+ *
677
+ * Throws on network/HTTP failure — the provider treats any failure as
678
+ * "no remote config" and stays on props/defaults.
679
+ */
680
+ getPersonalization(): Promise<RemotePersonalization>;
681
+ /**
682
+ * Ask for a human operator. Idempotent: if the session already has
683
+ * an open escalation the server returns it instead of creating a
684
+ * duplicate. Creates the session first when the chat had none yet
685
+ * (the server sends it back and we adopt it, so the whole
686
+ * conversation stays under one sessionId).
687
+ */
688
+ requestEscalation(reason: 'user_requested' | 'agent_gave_up', note?: string): Promise<{
689
+ escalationId: string;
690
+ status: 'pending' | 'active';
691
+ }>;
692
+ /**
693
+ * Tell the server the user walked away from the conversation (chat
694
+ * cleared) so the escalation is closed instead of leaving the
695
+ * operator talking to nobody. Fire-and-forget: never throws — the
696
+ * local cleanup must proceed even if the network is gone.
697
+ */
698
+ abandonEscalation(escalationId: string): Promise<void>;
699
+ /** User message routed to the human operator (no LLM involved). */
700
+ sendEscalationMessage(escalationId: string, content: string): Promise<void>;
701
+ /**
702
+ * Poll fallback / reconnect catch-up: status + operator messages
703
+ * created after `since` (ISO timestamp of the last one we have).
704
+ */
705
+ getEscalationUpdates(escalationId: string, since?: string): Promise<{
706
+ status: 'pending' | 'active' | 'resolved';
707
+ messages: Array<{
708
+ id: string;
709
+ content: string;
710
+ createdAt: string;
711
+ }>;
712
+ }>;
713
+ /**
714
+ * Open the escalation SSE channel — operator replies + status
715
+ * changes pushed live. Same XHR transport as `sendMessageStream`
716
+ * (RN's fetch can't read a body incrementally).
717
+ *
718
+ * Long-lived by design: no idle watchdog — the server pings every
719
+ * 25s, and a genuinely dead connection surfaces as `onClose`, which
720
+ * the hook answers with a backoff reconnect + `since` catch-up.
721
+ * Returns a close function; closing never fires `onClose`.
722
+ */
723
+ openEscalationStream(escalationId: string, handlers: {
724
+ since?: string;
725
+ onHumanMessage: (message: {
726
+ id: string;
727
+ content: string;
728
+ createdAt: string;
729
+ }) => void;
730
+ onStatus: (status: 'pending' | 'active' | 'resolved', operatorName?: string) => void;
731
+ /** Transport ended (error or server close) — caller decides on reconnect. */
732
+ onClose: () => void;
733
+ }): () => void;
734
+ }
735
+
736
+ type ActionFailureCategory = 'component-not-found' | 'disabled' | 'validation' | 'network-5xx' | 'network-4xx' | 'screen-timeout' | 'ambiguous-target' | 'unknown';
737
+ interface ActionDiagnose {
738
+ category: ActionFailureCategory;
739
+ visibleMessage?: string;
740
+ fieldId?: string;
741
+ targetId?: string;
742
+ screen?: string;
743
+ httpStatus?: number;
744
+ candidates?: string[];
745
+ recoverable?: boolean;
746
+ requiresUserInput?: boolean;
747
+ }
748
+ /** Effect classification an executor can attach to a result. */
749
+ type ActionEffect = 'unknown' | 'none' | 'partial' | 'changed' | 'completed';
750
+ /** Terminal result of executing one action locally. */
751
+ interface ActionExecutionResult {
752
+ success: boolean;
753
+ error?: string;
754
+ diagnose?: ActionDiagnose;
755
+ effect?: ActionEffect;
756
+ userVisibleStatusKey?: string;
757
+ }
758
+ /** One entry of the `results` array POSTed to /agent/continue. */
759
+ interface TurnActionReport {
760
+ nativeConfirmation?: NativeConfirmationReceipt;
761
+ actionId: string;
762
+ type: string;
763
+ success: boolean;
764
+ error?: string;
765
+ summary?: string;
766
+ diagnose?: ActionDiagnose;
767
+ recoveryAttempt?: number;
768
+ recoveryExhausted?: boolean;
769
+ toolWithoutEffect?: boolean;
770
+ effect?: ActionEffect;
771
+ userVisibleStatusKey?: string;
772
+ }
773
+ type ChatLoadingStatusKey = 'thinking' | 'statusAnalyzing' | 'statusWaitingApp' | 'statusAdjusting';
774
+ /**
775
+ * Localized copy for escalation system messages, injected by the chat
776
+ * surface (the machine has no i18n access — same pattern as errorPrefix).
777
+ * When omitted, no system messages are appended but the state machine
778
+ * still works.
779
+ */
780
+ interface EscalationStrings {
781
+ /** Appended right after the escalation is created. */
782
+ requested: string;
783
+ /** Appended when an operator claims the conversation. */
784
+ connected: string;
785
+ /** Appended when the operator resolves — AI takes over again. */
786
+ resolved: string;
787
+ /** Content of the offer chip message appended when the agent gives up. */
788
+ offer: string;
789
+ }
790
+ interface ChatSessionOptions {
791
+ /** User-facing prefix for client-side failures before the relay can answer. */
792
+ errorPrefix?: string;
793
+ /**
794
+ * Stream assistant replies token-by-token via SSE (default true).
795
+ * When the streaming transport fails before any output, the machine
796
+ * falls back to the non-streaming endpoint transparently.
797
+ */
798
+ streaming?: boolean;
799
+ /** Localized escalation copy — see EscalationStrings. */
800
+ escalationStrings?: EscalationStrings;
801
+ }
802
+ interface ChatSessionState {
803
+ mission?: MissionView | null;
804
+ /** Current request has dispatched an app action; retained across continuation hops. */
805
+ hasStartedActing: boolean;
806
+ replyLocale: AppilotsLocale | null;
807
+ messages: ChatMessage[];
808
+ isLoading: boolean;
809
+ loadingStatusKey: ChatLoadingStatusKey;
810
+ error: string | null;
811
+ pendingActions: AgentAction[];
812
+ /**
813
+ * Open escalation, or null. While set, `sendMessage` routes to the
814
+ * human channel — the agent session itself is never interrupted.
815
+ */
816
+ escalation: EscalationState | null;
817
+ }
818
+ /** Everything the chat machine needs from the transport client. */
819
+ interface ChatSessionClient {
820
+ restoreMission?(): Promise<MissionView | null>;
821
+ getMission?(): MissionView | null;
822
+ pauseMissionLocally?(): void;
823
+ controlMission?(command: 'observe' | 'pause' | 'resume' | 'cancel', context: Record<string, unknown>): Promise<SendMessageResponse & {
824
+ isDone: boolean;
825
+ hop: number;
826
+ }>;
827
+ sendMessage(content: string, context?: SendMessageContext): Promise<SendMessageResponse>;
828
+ sendMessageStream(content: string, context?: SendMessageContext, options?: SendMessageStreamOptions): Promise<SendMessageResponse>;
829
+ continueAgent(results: TurnActionReport[], context: Record<string, unknown>, hop: number): Promise<SendMessageResponse & {
830
+ isDone: boolean;
831
+ hop: number;
832
+ }>;
833
+ requestEscalation(reason: 'user_requested' | 'agent_gave_up', note?: string): Promise<{
834
+ escalationId: string;
835
+ status: 'pending' | 'active';
836
+ }>;
837
+ abandonEscalation(escalationId: string): Promise<void>;
838
+ sendEscalationMessage(escalationId: string, content: string): Promise<void>;
839
+ openEscalationStream(escalationId: string, handlers: {
840
+ since?: string;
841
+ onHumanMessage: (message: {
842
+ id: string;
843
+ content: string;
844
+ createdAt: string;
845
+ }) => void;
846
+ onStatus: (status: 'pending' | 'active' | 'resolved', operatorName?: string) => void;
847
+ onClose: () => void;
848
+ }): () => void;
849
+ destroySession(): Promise<void> | void;
850
+ }
851
+ /** Input handed to the platform adapter after a turn's actions finished. */
852
+ interface SettleTurnInput {
853
+ /** Whether any action in this turn was a navigate. */
854
+ hadNavigate: boolean;
855
+ /** Screen observed when the turn started, BEFORE any navigate dispatched. */
856
+ preNavigateScreen: string | null;
857
+ /** Full route signature (name + params) captured pre-navigate. */
858
+ preNavigateSignature: string | null;
859
+ /** The turn's actions, in order. */
860
+ turnActions: AgentAction[];
861
+ /**
862
+ * Terminal execution results reported so far (actionId → outcome).
863
+ *
864
+ * `effect` é o veredito POR AÇÃO, medido com baseline tirado antes do
865
+ * press. Quem consome este input não pode contradizê-lo: a verificação
866
+ * de turno roda depois de tudo executado, então o baseline dela é
867
+ * estruturalmente posterior ao efeito que ela tenta observar.
868
+ */
869
+ results: Array<{
870
+ actionId: string;
871
+ type: string;
872
+ success: boolean;
873
+ effect?: ActionEffect;
874
+ }>;
875
+ /** componentId of the most recent press-like ui_interaction, if any. */
876
+ pressedComponentId: string | null;
877
+ /** screenName of the most recent navigate action, if any. */
878
+ navigateTargetScreen: string | null;
879
+ }
880
+ interface SettleTurnOutcome {
881
+ /** True when the platform gave up waiting for async work to finish. */
882
+ loadingPending: boolean;
883
+ /** Action ids that "succeeded" but produced no observable effect. */
884
+ toolWithoutEffectIds: Set<string>;
885
+ }
886
+ /**
887
+ * The imperative platform surface the chat machine calls into. React
888
+ * Native implements this with the fiber walker + React Navigation
889
+ * settling; web implements it with a DOM walker + history/router.
890
+ */
891
+ interface ChatPlatformAdapter {
892
+ /**
893
+ * Build the rich runtime context that travels with every agent turn:
894
+ * platform id, route name, screen metadata, UI snapshot, registries.
895
+ */
896
+ buildContext(extras?: {
897
+ loadingPending?: boolean;
898
+ /**
899
+ * This is the first observation of a NEW user request, so any
900
+ * cross-observation state the adapter keeps — today the baseline for
901
+ * `snapshot.delta` — starts over. Without it the first hop of a
902
+ * mission would report whatever the person did on their own since
903
+ * the last one as the effect of the agent's previous action.
904
+ */
905
+ newMission?: boolean;
906
+ }): Record<string, unknown>;
907
+ /** Current route name, or null when unknown. */
908
+ getCurrentScreen(): string | null;
909
+ /** Full route+params signature, or null when unknown. */
910
+ getCurrentScreenSignature(): string | null;
911
+ /**
912
+ * Wait for the UI to settle after a turn's actions executed, and
913
+ * detect actions that reported success but had no observable effect.
914
+ * Runs BEFORE the fresh context is captured.
915
+ */
916
+ settleTurn(input: SettleTurnInput): Promise<SettleTurnOutcome>;
917
+ }
918
+ interface ChatSessionMachineDeps {
919
+ client: ChatSessionClient;
920
+ adapter: ChatPlatformAdapter;
921
+ emit: (event: AppilotsEvent) => void;
922
+ /** Debug warn sink — defaults to console.warn. */
923
+ warn?: (message: string, ...args: unknown[]) => void;
924
+ }
925
+ /**
926
+ * Loose structural view of one declared screen action's metadata
927
+ * (mirrors the SDK's ScreenActionMetadata without importing it).
928
+ */
929
+ interface ScreenActionMetadataLike {
930
+ id: string;
931
+ label?: string;
932
+ requiresConfirmation?: boolean;
933
+ [key: string]: unknown;
934
+ }
935
+ /** Everything the action queue machine needs from the transport client. */
936
+ interface ActionQueueClient {
937
+ missionApprovalAllowed?(action: AgentAction): boolean;
938
+ missionScopeForAction?(action: AgentAction): MissionScope | undefined;
939
+ claimMissionAction?(action: AgentAction, context: Record<string, unknown>): Promise<{
940
+ allowed: boolean;
941
+ scope: MissionScope;
942
+ expiresAt: number;
943
+ }>;
944
+ completeAction(actionId: string, success: boolean, error?: string, diagnose?: Record<string, unknown>): Promise<void>;
945
+ }
946
+ /**
947
+ * The imperative platform surface the action queue calls into: actually
948
+ * executing an action against the live UI, plus the navigation probes
949
+ * used to sequence post-navigate actions.
950
+ */
951
+ interface ActionRunnerAdapter {
952
+ /** Fresh platform evidence for mission scope validation, before and after dispatch claims. */
953
+ buildMissionContext?(): Record<string, unknown>;
954
+ /**
955
+ * Execute one action against the platform UI. `confirmedDestructive`
956
+ * is true when a destructive confirm gating this action was already
957
+ * approved by the user — platforms use it to suppress their own
958
+ * native re-confirmation dialog (RN monkey-patches Alert.alert; web
959
+ * patches window.confirm).
960
+ */
961
+ execute(action: AgentAction, options: {
962
+ confirmedDestructive: boolean;
963
+ }): Promise<ActionExecutionResult>;
964
+ getCurrentScreen(): string | null;
965
+ getCurrentScreenSignature(): string | null;
966
+ /** Declared action metadata for a screen, if the app registered any. */
967
+ getScreenActionsMetadata(screen: string): Array<ScreenActionMetadataLike | string> | undefined;
968
+ /** Wait for a navigate to land before executing subsequent actions. */
969
+ waitForScreenSettle(options: {
970
+ expectingChange: boolean;
971
+ fromScreen: string | null;
972
+ fromSignature: string | null;
973
+ maxMs: number;
974
+ }): Promise<{
975
+ screen: string | null;
976
+ transitioned: boolean;
977
+ timedOut: boolean;
978
+ waitedMs: number;
979
+ }>;
980
+ }
981
+ interface ActionQueueMachineDeps {
982
+ client: ActionQueueClient;
983
+ adapter: ActionRunnerAdapter;
984
+ emit: (event: AppilotsEvent) => void;
985
+ warn?: (message: string, ...args: unknown[]) => void;
986
+ }
987
+ interface ActionQueueState {
988
+ actions: AgentAction[];
989
+ }
990
+
991
+ /**
992
+ * ChatSessionMachine — the platform-agnostic chat/turn/continuation
993
+ * state machine extracted from the RN `useAppilotsChat` hook.
994
+ *
995
+ * It owns: the message transcript, the pending-action list for the
996
+ * current turn, loading/error state, per-turn result bookkeeping, the
997
+ * recovery-attempt counters, the /agent/continue loop, and the human-
998
+ * escalation channel. Platforms plug in through `ChatPlatformAdapter`
999
+ * (context capture + post-action settling) and forward the shared
1000
+ * event bus into `handleEvent`.
1001
+ */
1002
+
1003
+ declare class ChatSessionMachine {
1004
+ private readonly deps;
1005
+ private options;
1006
+ private state;
1007
+ private readonly listeners;
1008
+ private messageIdCounter;
1009
+ private generation;
1010
+ private currentTurn;
1011
+ private continuationInFlight;
1012
+ private deviceLocale;
1013
+ private missionSuspended;
1014
+ /**
1015
+ * BACKLOG 3.2 — per-message-thread retry counter, keyed on action
1016
+ * fingerprint. Reset on every fresh `sendMessage`, persists across
1017
+ * /agent/continue hops within the same user turn so two retries of
1018
+ * the same action add up across hops.
1019
+ */
1020
+ private recoveryCountByFingerprint;
1021
+ /** Abort controller for the in-flight generation (cancelMessage). */
1022
+ private abortController;
1023
+ private escalationStreamClose;
1024
+ private escalationReconnectAttempt;
1025
+ private escalationReconnectTimer;
1026
+ /** createdAt of the newest operator message — reconnect catch-up cursor. */
1027
+ private escalationSince;
1028
+ /** Operator message ids already appended (SSE replay dedupe). */
1029
+ private seenHumanMessageIds;
1030
+ private disposed;
1031
+ constructor(deps: ChatSessionMachineDeps, options?: ChatSessionOptions);
1032
+ getState(): ChatSessionState;
1033
+ subscribeState(listener: () => void): () => void;
1034
+ /** Update mutable options (errorPrefix, streaming, escalation copy). */
1035
+ setOptions(options: ChatSessionOptions): void;
1036
+ /** Tear down timers and the escalation stream. Safe to call twice. */
1037
+ dispose(): void;
1038
+ private setState;
1039
+ private warn;
1040
+ private addAssistantMessage;
1041
+ private addMissionReply;
1042
+ private appendLocalSystemMessage;
1043
+ requestHuman(reason?: 'user_requested' | 'agent_gave_up'): Promise<void>;
1044
+ /**
1045
+ * Live channel: operator replies + status changes. Reopens with
1046
+ * capped exponential backoff (and a `since` catch-up cursor) when
1047
+ * the transport drops, for as long as the escalation stays open.
1048
+ */
1049
+ private openEscalationStream;
1050
+ private closeEscalationStream;
1051
+ /** Route a user message to the human channel while escalated. */
1052
+ private sendEscalationMessage;
1053
+ private beginTurn;
1054
+ private registerSynthesizedTurnAction;
1055
+ private runContinuation;
1056
+ handleEvent(event: AppilotsEvent): void;
1057
+ sendMessage(content: string): Promise<void>;
1058
+ /** Stop future work, keeping partial text and allowing an already dispatched action to settle. */
1059
+ cancelMessage(): void;
1060
+ restoreMission(): Promise<void>;
1061
+ pauseMission(): Promise<void>;
1062
+ resumeMission(): Promise<void>;
1063
+ cancelMission(): Promise<void>;
1064
+ private controlMission;
1065
+ clearMessages(): void;
1066
+ clearError(): void;
1067
+ }
1068
+
1069
+ /**
1070
+ * ActionQueueMachine — the platform-agnostic action queue extracted
1071
+ * from the RN `useAppilotsActions` hook.
1072
+ *
1073
+ * It owns: the action list and its status transitions, sequential
1074
+ * auto-execution, the confirm gate (server-injected and locally
1075
+ * synthesized), backend result reporting, and approve/reject. The
1076
+ * platform supplies actual execution and the navigation probes through
1077
+ * `ActionRunnerAdapter`.
1078
+ */
1079
+
1080
+ interface ActionQueueOptions {
1081
+ /**
1082
+ * If true, actions are executed automatically without user approval.
1083
+ * Confirm actions always require approval regardless of this setting.
1084
+ * @default false
1085
+ */
1086
+ autoExecute?: boolean;
1087
+ }
1088
+ declare class ActionQueueMachine {
1089
+ private readonly deps;
1090
+ private options;
1091
+ private state;
1092
+ private readonly listeners;
1093
+ /** Action ids already dispatched — never auto-executed twice. */
1094
+ private readonly processed;
1095
+ /** Guards against overlapping sequential runs. */
1096
+ private draining;
1097
+ private disposed;
1098
+ /** Set when new work arrives while a drain is in flight. */
1099
+ private drainRequested;
1100
+ /** Set while a drain is queued on the microtask queue. */
1101
+ private drainScheduled;
1102
+ constructor(deps: ActionQueueMachineDeps, options?: ActionQueueOptions);
1103
+ dispose(): void;
1104
+ getState(): ActionQueueState;
1105
+ subscribeState(listener: () => void): () => void;
1106
+ setOptions(options: ActionQueueOptions): void;
1107
+ private setActions;
1108
+ private updateActions;
1109
+ private warn;
1110
+ handleEvent(event: AppilotsEvent): void;
1111
+ /**
1112
+ * Kick the sequential executor. Safe to call at any time — it
1113
+ * coalesces while a drain is already running so a burst of arriving
1114
+ * actions produces exactly one in-order pass.
1115
+ *
1116
+ * The drain is deferred to a microtask, and that is load-bearing, not
1117
+ * a nicety. A turn's actions arrive as a synchronous burst of
1118
+ * `agent:action:start` events, and a confirm gating a destructive
1119
+ * action can arrive AFTER the action it protects. Draining
1120
+ * synchronously on the first event would execute that action while
1121
+ * its gate is still in flight — a delete running before the user
1122
+ * confirmed it. Deferring lets the whole burst land first, so the
1123
+ * gate is visible by the time anything executes. (The React hook this
1124
+ * was extracted from got the same guarantee for free, from state
1125
+ * batching before its effect ran.)
1126
+ */
1127
+ scheduleDrain(): void;
1128
+ private drain;
1129
+ /**
1130
+ * Client-side defense-in-depth confirm injection.
1131
+ *
1132
+ * When the server's permission filter MISSES a destructive action
1133
+ * (e.g. the MCP doc is stale and doesn't have the destructive flag
1134
+ * for that action, but the live screen metadata DOES have
1135
+ * `requiresConfirmation: true`), the action would otherwise execute
1136
+ * immediately with no user confirmation — exactly the "delete
1137
+ * executed before I confirmed" bug.
1138
+ *
1139
+ * Returns `true` when the target must NOT execute this pass (either
1140
+ * a synthetic confirm was injected, or one is already gating it).
1141
+ */
1142
+ private ensureLocalConfirmIfNeeded;
1143
+ /**
1144
+ * Execute pending actions in order so chained actions (e.g.
1145
+ * form_fill → press button) run in the correct sequence.
1146
+ */
1147
+ private executeSequentially;
1148
+ /**
1149
+ * A turn is an all-terminal contract: ChatSessionMachine cannot ask the
1150
+ * server for the next step until every action in the current batch has
1151
+ * reported a completion or an error. When an early action fails we must
1152
+ * not execute dependent actions, but leaving them `pending` deadlocks the
1153
+ * whole turn. Mark the untouched suffix as failed/skipped and emit the
1154
+ * same terminal event a real executor would have emitted.
1155
+ */
1156
+ private skipRemainingBatchActions;
1157
+ approveAction(actionId: string): Promise<boolean>;
1158
+ rejectAction(actionId: string): Promise<void>;
1159
+ get pendingActions(): AgentAction[];
1160
+ get executingActions(): AgentAction[];
1161
+ get completedActions(): AgentAction[];
1162
+ }
1163
+
1164
+ /**
1165
+ * Navigation adapter — the web answer to React Navigation.
1166
+ *
1167
+ * On the web there is no navigator object to interrogate, so the URL is
1168
+ * the source of truth: the path maps to a route name and its segments
1169
+ * to the `activePath` the agent contract expects. An app with a router
1170
+ * (React Router, TanStack Router, ...) can inject its own `navigate()`
1171
+ * and route table so navigation stays client-side and route names match
1172
+ * the MCP document; without one, the adapter falls back to
1173
+ * `history.pushState` and derives names from path segments.
1174
+ */
1175
+ interface WebNavigationState {
1176
+ currentRouteName?: string;
1177
+ activePath: string[];
1178
+ rootRouteNames: string[];
1179
+ currentRouteNames: string[];
1180
+ routeNames: string[];
1181
+ canGoBack?: boolean;
1182
+ /**
1183
+ * Screens this session has been on, oldest first. No `backStack`
1184
+ * counterpart: the browser exposes `history.length` and nothing about
1185
+ * its entries, so where back goes is unknowable here.
1186
+ */
1187
+ visited?: string[];
1188
+ }
1189
+ /** A route the app declared, mapping an agent-facing name to a path. */
1190
+ interface WebRoute {
1191
+ /** Route name as it appears in the MCP document (e.g. `VehicleList`). */
1192
+ name: string;
1193
+ /** URL path, optionally with `:param` segments (e.g. `/vehicles/:id`). */
1194
+ path: string;
1195
+ }
1196
+ interface WebNavigationAdapterOptions {
1197
+ /** Declared routes — the same names the MCP document uses. */
1198
+ routes?: WebRoute[];
1199
+ /**
1200
+ * The app's own navigation function. Injected by a router integration
1201
+ * so navigating stays client-side (no full page reload).
1202
+ */
1203
+ navigate?: (path: string, options?: {
1204
+ replace?: boolean;
1205
+ }) => void;
1206
+ /** Overrides `window` — used by tests and non-browser hosts. */
1207
+ window?: Window;
1208
+ }
1209
+ /**
1210
+ * Match a declared route pattern against a concrete path, returning the
1211
+ * captured params. `/vehicles/:id` vs `/vehicles/42` → `{ id: '42' }`.
1212
+ */
1213
+ declare function matchRoute(pattern: string, pathname: string): Record<string, string> | undefined;
1214
+ /** Fill `/vehicles/:id` with `{ id: 42 }` → `/vehicles/42`. */
1215
+ declare function buildPath(pattern: string, params?: Record<string, unknown>): string;
1216
+ declare class WebNavigationAdapter {
1217
+ private routes;
1218
+ private navigateFn?;
1219
+ private readonly win?;
1220
+ /** Bumped on every navigation so the settle loop can detect movement. */
1221
+ private listeners;
1222
+ constructor(options?: WebNavigationAdapterOptions);
1223
+ /** Register (or replace) the app's routes and navigate function. */
1224
+ configure(options: Pick<WebNavigationAdapterOptions, 'routes' | 'navigate'>): void;
1225
+ private get location();
1226
+ /**
1227
+ * The declared route matching the current URL, if any.
1228
+ *
1229
+ * When several patterns match, the most specific wins: a route with
1230
+ * fewer `:param` segments beats one with more. Without this,
1231
+ * declaration order decides, and `/vehicles/:id` would swallow
1232
+ * `/vehicles/new` — resolving the create screen to VehicleDetails
1233
+ * with `id: 'new'`.
1234
+ */
1235
+ private currentRoute;
1236
+ /**
1237
+ * Current route name: the declared route's name when the URL matches
1238
+ * one, else a name derived from the last path segment ('/' → 'Home').
1239
+ */
1240
+ /**
1241
+ * Screens this session has been on, oldest first.
1242
+ *
1243
+ * Recorded in `getCurrentScreen` rather than on a router event: the
1244
+ * adapter derives the route from the URL on demand and has no state
1245
+ * change to hook, and this method is called on every observation and
1246
+ * throughout settle polling, so a route that is up long enough to act
1247
+ * on is a route this sees.
1248
+ *
1249
+ * There is no web counterpart to `backStack`. The browser exposes
1250
+ * `history.length` and nothing else about its entries, so where back
1251
+ * goes is genuinely unknowable here — and the field stays absent
1252
+ * rather than guessed.
1253
+ */
1254
+ private visited;
1255
+ /** The visit log, oldest first. */
1256
+ getVisitedScreens(): string[];
1257
+ private recordVisit;
1258
+ getCurrentScreen(): string | null;
1259
+ private resolveCurrentScreen;
1260
+ /**
1261
+ * Route name plus its params — lets the settle loop detect a navigate
1262
+ * to the same route with different params (`/vehicles/41` →
1263
+ * `/vehicles/42`), which a name-only comparison would miss.
1264
+ */
1265
+ getCurrentScreenSignature(): string | null;
1266
+ /**
1267
+ * The `navigationState` block of the agent context. `activePath` is
1268
+ * the URL's segments mapped to route names — the web equivalent of a
1269
+ * nested navigator's active route chain.
1270
+ */
1271
+ getNavigationState(): WebNavigationState | null;
1272
+ /** Path for a route name, or undefined when it is not declared. */
1273
+ resolvePath(screenName: string, params?: Record<string, unknown>): string | undefined;
1274
+ /** True when the app declared this route name. */
1275
+ hasRoute(screenName: string): boolean;
1276
+ knownRouteNames(): string[];
1277
+ /**
1278
+ * Navigate to a path. Uses the app's own navigate function when one
1279
+ * was injected (keeping SPA routing intact); otherwise pushes onto
1280
+ * the History API and notifies listeners, so a router listening to
1281
+ * `popstate` still re-renders.
1282
+ */
1283
+ navigate(path: string, options?: {
1284
+ replace?: boolean;
1285
+ }): void;
1286
+ goBack(): void;
1287
+ canGoBack(): boolean;
1288
+ subscribe(listener: () => void): () => void;
1289
+ private notify;
1290
+ }
1291
+
1292
+ /**
1293
+ * AppilotsWebRuntime — wires the shared session machines to the web.
1294
+ *
1295
+ * This is the whole web client in one object: the transport, the two
1296
+ * client-core state machines, and the two platform adapters that give
1297
+ * them eyes (DOM snapshot) and hands (DOM dispatch). A React binding
1298
+ * sits on top of it, but nothing here needs React — a Vue or vanilla
1299
+ * app can use this class directly.
1300
+ */
1301
+
1302
+ /** Design-time metadata an app declares for a screen. */
1303
+ interface WebScreenMetadata {
1304
+ name: string;
1305
+ title?: string;
1306
+ description?: string;
1307
+ fields?: Array<Record<string, unknown>>;
1308
+ actions?: Array<ScreenActionMetadataLike | string>;
1309
+ }
1310
+ interface AppilotsWebRuntimeOptions {
1311
+ projectId: string;
1312
+ apiKey?: string;
1313
+ apiBaseUrl?: string;
1314
+ debug?: boolean;
1315
+ appVersion?: string;
1316
+ mcpVersion?: string;
1317
+ user?: {
1318
+ id: string;
1319
+ name?: string;
1320
+ identifiers?: Record<string, string>;
1321
+ };
1322
+ permissions?: Partial<AgentPermissions>;
1323
+ /** Declared routes — names must match the MCP document. */
1324
+ routes?: WebRoute[];
1325
+ /** The app's router navigate function, when it has one. */
1326
+ navigate?: WebNavigationAdapterOptions['navigate'];
1327
+ /** Screen metadata keyed by route name. */
1328
+ screens?: WebScreenMetadata[];
1329
+ /** Subtree to observe and act within. Defaults to the document. */
1330
+ root?: Element;
1331
+ /** Execute actions as they arrive, without per-action approval. */
1332
+ autoExecute?: boolean;
1333
+ /** Injected transport, for tests. */
1334
+ client?: AppilotsClient;
1335
+ chat?: ChatSessionOptions;
1336
+ }
1337
+ declare class AppilotsWebRuntime {
1338
+ readonly client: AppilotsClient;
1339
+ readonly navigation: WebNavigationAdapter;
1340
+ readonly chat: ChatSessionMachine;
1341
+ readonly actions: ActionQueueMachine;
1342
+ /**
1343
+ * Baseline for `snapshot.delta`. Per-runtime rather than per-module so
1344
+ * two runtimes on one page (a preview inside an app) cannot read each
1345
+ * other's previous screen.
1346
+ */
1347
+ private readonly deltaTracker;
1348
+ private readonly handlers;
1349
+ private readonly screens;
1350
+ private permissions?;
1351
+ private root?;
1352
+ private lifetime;
1353
+ constructor(options: AppilotsWebRuntimeOptions);
1354
+ subscribe(handler: AppilotsEventHandler): () => void;
1355
+ emit(event: AppilotsEvent): void;
1356
+ configureNavigation(options: Pick<WebNavigationAdapterOptions, 'routes' | 'navigate'>): void;
1357
+ registerScreen(metadata: WebScreenMetadata): void;
1358
+ setPermissions(permissions: Partial<AgentPermissions> | undefined): void;
1359
+ setRoot(root: Element | undefined): void;
1360
+ suspend(): void;
1361
+ resume(): void;
1362
+ dispose(): void;
1363
+ /**
1364
+ * The observation sent with every turn. `platform: 'web'` is explicit
1365
+ * on every request (including /agent/continue, which reuses this) so
1366
+ * the server applies its platform-neutral profile rather than the
1367
+ * React Native heuristics.
1368
+ */
1369
+ buildContext(extras?: {
1370
+ loadingPending?: boolean;
1371
+ newMission?: boolean;
1372
+ }): Record<string, unknown>;
1373
+ private createChatAdapter;
1374
+ private createActionRunner;
1375
+ /**
1376
+ * The declared actions for a screen, expanded with one entry per live
1377
+ * DOM element that points at a declaration via `data-appilots-action`.
1378
+ *
1379
+ * A list renders one delete button per row, each needing its own id to
1380
+ * be targetable, but all of them are the same declared action and must
1381
+ * inherit its `requiresConfirmation`. The confirm matcher is exact by
1382
+ * design (a fuzzy one fired destructive dialogs on plain navigation),
1383
+ * so we expand the declaration to the concrete ids instead of
1384
+ * loosening the match.
1385
+ */
1386
+ private screenActionsFor;
1387
+ }
1388
+
1389
+ export { type AgentAction as A, type ControlEvidence as C, type EscalationState as E, RateLimitedError as R, type ScreenActionMetadataLike as S, WebNavigationAdapter as W, type AppilotsLocale as a, type AgentPermissions as b, type AppilotsEvent as c, type ActionExecutionResult as d, type ActionDiagnose as e, ActionQueueMachine as f, type AgentActionType as g, AppilotsClient as h, type AppilotsEventHandler as i, AppilotsWebRuntime as j, type AppilotsWebRuntimeOptions as k, type ChatMessage as l, ChatSessionMachine as m, type ChatSessionOptions as n, type ChatSessionState as o, type EscalationStrings as p, type WebNavigationAdapterOptions as q, type WebNavigationState as r, type WebRoute as s, type WebScreenMetadata as t, buildPath as u, matchRoute as v };