@fiodos/web-core 0.1.11 → 0.1.14

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.
Files changed (42) hide show
  1. package/dist/cjs/adapters/webCredentialAutofillAdapter.d.ts +16 -0
  2. package/dist/cjs/adapters/webCredentialAutofillAdapter.js +47 -0
  3. package/dist/cjs/api/backendClient.js +14 -0
  4. package/dist/cjs/config/types.d.ts +21 -2
  5. package/dist/cjs/controller/AgentController.d.ts +35 -1
  6. package/dist/cjs/controller/AgentController.js +123 -3
  7. package/dist/cjs/dropin/createFiodosAgent.d.ts +32 -1
  8. package/dist/cjs/dropin/createFiodosAgent.js +39 -1
  9. package/dist/cjs/embed/dynamics.d.ts +61 -0
  10. package/dist/cjs/embed/dynamics.js +173 -0
  11. package/dist/cjs/embed/global.d.ts +45 -0
  12. package/dist/cjs/embed/global.js +48 -0
  13. package/dist/cjs/index.d.ts +3 -0
  14. package/dist/cjs/index.js +8 -1
  15. package/dist/cjs/orb/mountOrb.js +111 -18
  16. package/dist/cjs/orb/orbView.js +40 -7
  17. package/dist/cjs/orb/publishedConfig.d.ts +4 -2
  18. package/dist/cjs/orb/publishedConfig.js +56 -7
  19. package/dist/cjs/version.d.ts +1 -1
  20. package/dist/cjs/version.js +1 -1
  21. package/dist/embed/fiodos-embed.js +66 -0
  22. package/dist/esm/adapters/webCredentialAutofillAdapter.d.ts +16 -0
  23. package/dist/esm/adapters/webCredentialAutofillAdapter.js +44 -0
  24. package/dist/esm/api/backendClient.js +14 -0
  25. package/dist/esm/config/types.d.ts +21 -2
  26. package/dist/esm/controller/AgentController.d.ts +35 -1
  27. package/dist/esm/controller/AgentController.js +124 -4
  28. package/dist/esm/dropin/createFiodosAgent.d.ts +32 -1
  29. package/dist/esm/dropin/createFiodosAgent.js +39 -1
  30. package/dist/esm/embed/dynamics.d.ts +61 -0
  31. package/dist/esm/embed/dynamics.js +168 -0
  32. package/dist/esm/embed/global.d.ts +45 -0
  33. package/dist/esm/embed/global.js +46 -0
  34. package/dist/esm/index.d.ts +3 -0
  35. package/dist/esm/index.js +3 -0
  36. package/dist/esm/orb/mountOrb.js +112 -19
  37. package/dist/esm/orb/orbView.js +41 -8
  38. package/dist/esm/orb/publishedConfig.d.ts +4 -2
  39. package/dist/esm/orb/publishedConfig.js +56 -7
  40. package/dist/esm/version.d.ts +1 -1
  41. package/dist/esm/version.js +1 -1
  42. package/package.json +5 -3
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Web implementation of @fiodos/core's CredentialAutofillAdapter, on top of
3
+ * the Credential Management API (`navigator.credentials.get({ password: true,
4
+ * mediation: 'required' })`). The BROWSER owns the account-picker / user
5
+ * verification UI; this module only receives the outcome:
6
+ *
7
+ * - A `PasswordCredential` → 'success' with the saved username + password.
8
+ * - `null` (user dismissed the picker, or no saved credential and the browser
9
+ * chose not to prompt) → 'dismissed' (graceful fallback, never an error).
10
+ * - No API / SecurityError (insecure context, iframe policy) → 'unavailable'.
11
+ *
12
+ * The credentials never touch storage, logs or the network from here: they go
13
+ * straight into the app's own login handler parameters, one call, then gone.
14
+ */
15
+ import type { CredentialAutofillAdapter } from '@fiodos/core';
16
+ export declare function createWebCredentialAutofillAdapter(): CredentialAutofillAdapter;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createWebCredentialAutofillAdapter = createWebCredentialAutofillAdapter;
4
+ function credentialsContainer() {
5
+ if (typeof navigator === 'undefined')
6
+ return null;
7
+ const nav = navigator;
8
+ return nav.credentials && typeof nav.credentials.get === 'function' ? nav.credentials : null;
9
+ }
10
+ function passwordCredentialSupported() {
11
+ if (typeof window === 'undefined')
12
+ return false;
13
+ const w = window;
14
+ // The Credential Management password store needs both the constructor and a
15
+ // secure context; feature-detect instead of sniffing the browser.
16
+ return typeof w.PasswordCredential === 'function' && w.isSecureContext !== false;
17
+ }
18
+ function createWebCredentialAutofillAdapter() {
19
+ return {
20
+ isSupported() {
21
+ return credentialsContainer() !== null && passwordCredentialSupported();
22
+ },
23
+ async requestCredentials() {
24
+ const container = credentialsContainer();
25
+ if (!container || !passwordCredentialSupported())
26
+ return { status: 'unavailable' };
27
+ let raw;
28
+ try {
29
+ // mediation 'required': ALWAYS show the browser's account picker — the
30
+ // user explicitly asked to sign in, so a visible, cancellable choice is
31
+ // the honest UX (and the only one that lets them pick among accounts).
32
+ raw = await container.get({ password: true, mediation: 'required' });
33
+ }
34
+ catch {
35
+ // SecurityError (permissions policy / insecure frame), NotSupportedError…
36
+ return { status: 'unavailable' };
37
+ }
38
+ const cred = raw;
39
+ if (!cred)
40
+ return { status: 'dismissed' };
41
+ if (cred.type === 'password' && cred.id && typeof cred.password === 'string' && cred.password) {
42
+ return { status: 'success', username: cred.id, password: cred.password };
43
+ }
44
+ return { status: 'unavailable' };
45
+ },
46
+ };
47
+ }
@@ -119,6 +119,20 @@ function createFiodosBackendClient(options) {
119
119
  if (request.sessionId?.trim()) {
120
120
  body.session_id = request.sessionId.trim();
121
121
  }
122
+ // Device capability flags (retrocompatible: old backends ignore them).
123
+ if (request.platformCapabilities?.credentialAutofill) {
124
+ body.platform_capabilities = { credential_autofill: true };
125
+ }
126
+ // Live session verdict for audience filtering (retrocompatible: absent
127
+ // when the host wired no session source; old backends ignore it).
128
+ if (request.sessionContext) {
129
+ body.session_context = {
130
+ authenticated: request.sessionContext.authenticated,
131
+ ...(request.sessionContext.capabilities
132
+ ? { capabilities: request.sessionContext.capabilities }
133
+ : {}),
134
+ };
135
+ }
122
136
  // Autonomous-chain continuation metadata (backward compatible: absent on a
123
137
  // normal turn; an old backend simply ignores these fields).
124
138
  if (request.isContinuation) {
@@ -3,7 +3,7 @@
3
3
  * Mirrors @fiodos/react's FiodosWebAgentConfig (same adapters, same lifecycle).
4
4
  * Shared by every framework binding through the AgentController.
5
5
  */
6
- import type { ActionRegistries, AgentBackendClient, AgentMessages, AppManifest, ConfirmationLexicon, MessageCatalog, NavigationAdapter, Sanitizer, StorageAdapter, TelemetryAdapter, VoiceAdapter } from '@fiodos/core';
6
+ import type { ActionRegistries, AgentBackendClient, AgentMessages, AppManifest, ConfirmationLexicon, CredentialAutofillAdapter, MessageCatalog, NavigationAdapter, Sanitizer, StorageAdapter, TelemetryAdapter, VoiceAdapter } from '@fiodos/core';
7
7
  import type { ResolveUiMessagesOptions, WebUiMessages } from '../ui/messages';
8
8
  export type AgentPhase = 'idle' | 'listening' | 'thinking' | 'speaking' | 'confirming';
9
9
  export interface AgentTimings {
@@ -55,7 +55,26 @@ export interface FiodosWebAgentConfig {
55
55
  storage: StorageAdapter;
56
56
  telemetry?: TelemetryAdapter;
57
57
  /** Host auth check, required by manifest actions declaring requiresAuth. */
58
- isAuthenticated?: () => boolean;
58
+ isAuthenticated?: () => boolean | null | undefined;
59
+ /**
60
+ * Fine-grained session capability flags from the wired session bridge
61
+ * (audience map: e.g. { has_payment_method: true }). Sent with each turn so
62
+ * the backend shows this user only THEIR face of the orb. Boolean flags
63
+ * only — never user data. null/undefined = unknown (nothing is filtered).
64
+ */
65
+ getSessionCapabilities?: () => Record<string, boolean> | null | undefined;
66
+ /**
67
+ * Platform saved-credential service powering the built-in SMART SIGN-IN
68
+ * flow (browser password manager). Defaults to the web Credential
69
+ * Management adapter; pass `null` to disable the feature entirely.
70
+ */
71
+ credentialAutofill?: CredentialAutofillAdapter | null;
72
+ /**
73
+ * Host end-user identity. Conversation memory is namespaced per identity
74
+ * (hashed LOCALLY, never sent by this module): logout/account switches swap
75
+ * to that identity's own conversation instead of leaking the previous one.
76
+ */
77
+ getUserId?: () => string | null;
59
78
  messages?: {
60
79
  catalogs?: Record<string, MessageCatalog>;
61
80
  overrides?: Partial<AgentMessages>;
@@ -74,6 +74,7 @@ export declare class AgentController {
74
74
  private readonly telemetry;
75
75
  private readonly consent;
76
76
  private readonly executor;
77
+ private readonly credentialAutofill;
77
78
  private readonly timings;
78
79
  private readonly chaining;
79
80
  private readonly ttsLocale;
@@ -89,7 +90,7 @@ export declare class AgentController {
89
90
  private consentModalVisible;
90
91
  private readonly sessionId;
91
92
  private sessionStarted;
92
- private readonly conversation;
93
+ private conversation;
93
94
  private pending;
94
95
  private confirmationReasked;
95
96
  private confirmationVoiceActive;
@@ -116,6 +117,10 @@ export declare class AgentController {
116
117
  /** Persistence scope: internal orbs keep their memory under a separate key
117
118
  * so a public orb sharing the same appId can never read it. */
118
119
  private conversationScope;
120
+ /** Current identity segment (LOCAL hash of the host's getUserId; never sent
121
+ * anywhere). The conversation belongs to (app, scope, identity). */
122
+ private conversationIdentity;
123
+ private removeIdentityListeners;
119
124
  private loadingOlderExchanges;
120
125
  private loadOlderTimer;
121
126
  private readonly listeners;
@@ -123,6 +128,22 @@ export declare class AgentController {
123
128
  constructor(config: FiodosWebAgentConfig);
124
129
  private restorePersistedConversation;
125
130
  private persistConversation;
131
+ /**
132
+ * Re-evaluates the host identity and, when it changed, swaps conversations:
133
+ * the outgoing identity's session is saved under ITS private key (so a user
134
+ * who logs back in recovers it, honouring "never clear"), all in-memory
135
+ * state and UI are wiped, and the incoming identity's own session (usually
136
+ * empty) is restored. An anonymous visitor after a logout always starts
137
+ * clean — the previous user's transcript is never inherited.
138
+ */
139
+ private syncConversationIdentity;
140
+ /**
141
+ * Wipes the CURRENT identity's conversation: live memory, panel and the
142
+ * persisted blob. Hosts can call it from their logout for a hard wipe (by
143
+ * default a logout only parks the conversation under the user's private
144
+ * local key).
145
+ */
146
+ resetConversation(): void;
126
147
  getState(): AgentState;
127
148
  /** Update the REAL live activity and re-render (no-op when unchanged). */
128
149
  private setActivity;
@@ -171,6 +192,19 @@ export declare class AgentController {
171
192
  confirmPendingAction(): Promise<void>;
172
193
  cancelPendingAction(): void;
173
194
  private handleConfirmationTranscript;
195
+ /**
196
+ * Device capability flags advertised on each turn (never user data). Fresh
197
+ * per turn: cheap, and honest if the environment changes mid-session.
198
+ */
199
+ private platformCapabilities;
200
+ /**
201
+ * Live session verdict advertised on each turn (audience map — never user
202
+ * data): whether the user is signed in plus the app's boolean capability
203
+ * flags. Fresh per turn so login/logout mid-session is honest. Undefined
204
+ * when the host wired neither source (older installs — backend filters
205
+ * nothing, exactly today's behavior).
206
+ */
207
+ private sessionContext;
174
208
  private routeLabel;
175
209
  private actionLabel;
176
210
  /**
@@ -19,6 +19,7 @@ exports.AgentController = void 0;
19
19
  * - The agent only ever executes actions declared in the manifest.
20
20
  */
21
21
  const core_1 = require("@fiodos/core");
22
+ const webCredentialAutofillAdapter_1 = require("../adapters/webCredentialAutofillAdapter");
22
23
  const errors_1 = require("../api/errors");
23
24
  const formatTurnError_1 = require("../api/formatTurnError");
24
25
  const backendClient_1 = require("../api/backendClient");
@@ -43,6 +44,7 @@ class AgentController {
43
44
  this.consentModalVisible = false;
44
45
  this.sessionId = randomSessionId();
45
46
  this.sessionStarted = false;
47
+ // Replaced wholesale on identity swaps / explicit resets.
46
48
  this.conversation = (0, core_1.createConversationSession)();
47
49
  this.pending = null;
48
50
  this.confirmationReasked = false;
@@ -72,6 +74,7 @@ class AgentController {
72
74
  /** Persistence scope: internal orbs keep their memory under a separate key
73
75
  * so a public orb sharing the same appId can never read it. */
74
76
  this.conversationScope = 'public';
77
+ this.removeIdentityListeners = null;
75
78
  this.loadingOlderExchanges = false;
76
79
  this.loadOlderTimer = null;
77
80
  this.listeners = new Set();
@@ -92,12 +95,19 @@ class AgentController {
92
95
  appId: config.manifest.appId,
93
96
  version: config.consentVersion,
94
97
  });
98
+ // SMART SIGN-IN ships by default on web (browser password manager);
99
+ // hosts can override the adapter or pass null to opt out entirely.
100
+ this.credentialAutofill =
101
+ config.credentialAutofill === undefined
102
+ ? (0, webCredentialAutofillAdapter_1.createWebCredentialAutofillAdapter)()
103
+ : config.credentialAutofill;
95
104
  this.executor = (0, core_1.createActionExecutor)({
96
105
  manifest: config.manifest,
97
106
  registries: config.registries,
98
107
  navigation: config.navigation,
99
108
  messages: this.messages,
100
109
  isAuthenticated: config.isAuthenticated,
110
+ credentialAutofill: this.credentialAutofill,
101
111
  });
102
112
  this.timings = { ...types_1.DEFAULT_AGENT_TIMINGS, ...config.timings };
103
113
  // Chaining OFF by default; clamp to the same safe bounds as react/react-native.
@@ -131,15 +141,29 @@ class AgentController {
131
141
  });
132
142
  // Re-emit combined state whenever the speech machine advances.
133
143
  this.unsubscribeSpeech = this.speech.subscribe(() => this.emit());
144
+ this.conversationIdentity = (0, core_1.conversationIdentityKey)(config.getUserId?.());
134
145
  // Rehydrate the previous conversation (page reloads): the persisted blob
135
146
  // carries its own retention rule, so expiry is decided correctly even
136
147
  // before the published config arrives. Best-effort and non-blocking.
137
148
  void this.restorePersistedConversation();
149
+ // Logout/login usually happens on another screen: re-check identity when
150
+ // the tab regains focus or becomes visible again, besides before every turn.
151
+ if (typeof window !== 'undefined') {
152
+ const check = () => void this.syncConversationIdentity();
153
+ window.addEventListener('focus', check);
154
+ document.addEventListener('visibilitychange', check);
155
+ this.removeIdentityListeners = () => {
156
+ window.removeEventListener('focus', check);
157
+ document.removeEventListener('visibilitychange', check);
158
+ };
159
+ }
138
160
  }
139
161
  // ── Conversation persistence (survives page reloads) ───────────────────────
140
162
  async restorePersistedConversation() {
141
- const restored = await (0, core_1.restoreConversationSession)(this.config.storage, this.config.manifest.appId, Date.now(), this.conversationScope);
142
- if (!restored)
163
+ const identityAtStart = this.conversationIdentity;
164
+ const restored = await (0, core_1.restoreConversationSession)(this.config.storage, this.config.manifest.appId, Date.now(), this.conversationScope, identityAtStart);
165
+ // The identity may have changed again while the read resolved.
166
+ if (!restored || this.conversationIdentity !== identityAtStart)
143
167
  return;
144
168
  // Never clobber turns the user already made while the restore resolved.
145
169
  if (this.conversation.turns.length > 0 || this.conversation.uiArchive?.length)
@@ -151,7 +175,43 @@ class AgentController {
151
175
  this.emit();
152
176
  }
153
177
  persistConversation() {
154
- void (0, core_1.persistConversationSession)(this.config.storage, this.config.manifest.appId, this.conversation, this.conversationRetentionMinutes, this.conversationScope);
178
+ void (0, core_1.persistConversationSession)(this.config.storage, this.config.manifest.appId, this.conversation, this.conversationRetentionMinutes, this.conversationScope, this.conversationIdentity);
179
+ }
180
+ /**
181
+ * Re-evaluates the host identity and, when it changed, swaps conversations:
182
+ * the outgoing identity's session is saved under ITS private key (so a user
183
+ * who logs back in recovers it, honouring "never clear"), all in-memory
184
+ * state and UI are wiped, and the incoming identity's own session (usually
185
+ * empty) is restored. An anonymous visitor after a logout always starts
186
+ * clean — the previous user's transcript is never inherited.
187
+ */
188
+ syncConversationIdentity() {
189
+ const next = (0, core_1.conversationIdentityKey)(this.config.getUserId?.());
190
+ if (next === this.conversationIdentity)
191
+ return false;
192
+ // 1. Park the outgoing identity's session under its own key.
193
+ this.persistConversation();
194
+ // 2. Wipe every trace from memory and the panel.
195
+ this.conversation = (0, core_1.createConversationSession)();
196
+ this.conversationIdentity = next;
197
+ this.lastExchange = null;
198
+ this.emit();
199
+ // 3. Bring in the new identity's own session, if any.
200
+ void this.restorePersistedConversation();
201
+ return true;
202
+ }
203
+ /**
204
+ * Wipes the CURRENT identity's conversation: live memory, panel and the
205
+ * persisted blob. Hosts can call it from their logout for a hard wipe (by
206
+ * default a logout only parks the conversation under the user's private
207
+ * local key).
208
+ */
209
+ resetConversation() {
210
+ this.cancelAll();
211
+ this.conversation = (0, core_1.createConversationSession)();
212
+ this.lastExchange = null;
213
+ void (0, core_1.clearPersistedConversation)(this.config.storage, this.config.manifest.appId, this.conversationScope, this.conversationIdentity);
214
+ this.emit();
155
215
  }
156
216
  // ── Subscription / state ───────────────────────────────────────────────────
157
217
  getState() {
@@ -532,6 +592,57 @@ class AgentController {
532
592
  this.beginConfirmationVoiceWindow();
533
593
  }
534
594
  }
595
+ /**
596
+ * Device capability flags advertised on each turn (never user data). Fresh
597
+ * per turn: cheap, and honest if the environment changes mid-session.
598
+ */
599
+ platformCapabilities() {
600
+ let autofill = false;
601
+ try {
602
+ autofill = this.credentialAutofill?.isSupported() === true;
603
+ }
604
+ catch {
605
+ autofill = false;
606
+ }
607
+ return autofill ? { credentialAutofill: true } : undefined;
608
+ }
609
+ /**
610
+ * Live session verdict advertised on each turn (audience map — never user
611
+ * data): whether the user is signed in plus the app's boolean capability
612
+ * flags. Fresh per turn so login/logout mid-session is honest. Undefined
613
+ * when the host wired neither source (older installs — backend filters
614
+ * nothing, exactly today's behavior).
615
+ */
616
+ sessionContext() {
617
+ const { isAuthenticated, getSessionCapabilities } = this.config;
618
+ if (!isAuthenticated && !getSessionCapabilities)
619
+ return undefined;
620
+ let authenticated = null;
621
+ try {
622
+ const v = isAuthenticated?.();
623
+ authenticated = v == null ? null : Boolean(v);
624
+ }
625
+ catch {
626
+ authenticated = null;
627
+ }
628
+ let capabilities;
629
+ try {
630
+ const raw = getSessionCapabilities?.();
631
+ if (raw && typeof raw === 'object') {
632
+ const clean = {};
633
+ for (const [k, v] of Object.entries(raw)) {
634
+ if (typeof v === 'boolean')
635
+ clean[k] = v;
636
+ }
637
+ if (Object.keys(clean).length > 0)
638
+ capabilities = clean;
639
+ }
640
+ }
641
+ catch {
642
+ capabilities = undefined;
643
+ }
644
+ return { authenticated, ...(capabilities ? { capabilities } : {}) };
645
+ }
535
646
  routeLabel(intent) {
536
647
  return this.config.manifest.routes.find((r) => r.intent === intent)?.label;
537
648
  }
@@ -717,6 +828,8 @@ class AgentController {
717
828
  isContinuation: true,
718
829
  chainStep: step,
719
830
  lastStep,
831
+ platformCapabilities: this.platformCapabilities(),
832
+ sessionContext: this.sessionContext(),
720
833
  ...(0, backendClient_1.buildManifestPayload)(this.config.manifest),
721
834
  }, { signal: controller.signal });
722
835
  oc = await this.applyOutcome(turn, routeNow, snap?.context ?? {}, state.userMessage);
@@ -820,6 +933,9 @@ class AgentController {
820
933
  this.setPhase('idle');
821
934
  return;
822
935
  }
936
+ // Identity gate: if the host user changed since the last turn (login,
937
+ // logout, account switch), swap conversations BEFORE recording anything.
938
+ this.syncConversationIdentity();
823
939
  this.setPhase('thinking');
824
940
  // REAL activity: the request is genuinely in flight from this moment.
825
941
  this.setActivity({ kind: 'thinking' });
@@ -871,6 +987,8 @@ class AgentController {
871
987
  conversationHistory: conv.history,
872
988
  conversationSummary: conv.summary ?? undefined,
873
989
  sessionId: this.sessionId,
990
+ platformCapabilities: this.platformCapabilities(),
991
+ sessionContext: this.sessionContext(),
874
992
  ...(0, backendClient_1.buildManifestPayload)(this.config.manifest),
875
993
  }, { signal: controller.signal });
876
994
  outcome = await this.applyOutcome(turn, currentRoute, snapshot?.context ?? {}, text);
@@ -1112,6 +1230,8 @@ class AgentController {
1112
1230
  }
1113
1231
  dispose() {
1114
1232
  this.cancelAll();
1233
+ this.removeIdentityListeners?.();
1234
+ this.removeIdentityListeners = null;
1115
1235
  if (this.loadOlderTimer) {
1116
1236
  clearTimeout(this.loadOlderTimer);
1117
1237
  this.loadOlderTimer = null;
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { type ActionRegistries, type AppManifest } from '@fiodos/core';
15
15
  import { AgentController } from '../controller/AgentController';
16
+ import type { AgentScreenSnapshot } from '../context/screenContextStore';
16
17
  import { type MountOrbOptions, type MountedOrb } from '../orb/mountOrb';
17
18
  import type { FiodosWebAgentConfig } from '../config/types';
18
19
  export interface CreateFiodosAgentOptions {
@@ -37,8 +38,22 @@ export interface CreateFiodosAgentOptions {
37
38
  sttLocale?: string;
38
39
  /** Host router navigate delegate. Defaults to location.assign. */
39
40
  navigate?: (route: string) => void;
41
+ /**
42
+ * Current route/screen resolver for the agent's screen awareness. Defaults
43
+ * to location.pathname + search. Apps whose screens are CLIENT STATE (no URL
44
+ * change) pass the generated `fyodosGetCurrentRoute` here so the orb knows
45
+ * which screen the user is on (see screen-truth wiring).
46
+ */
47
+ getCurrentRoute?: () => string | null;
40
48
  /** Host auth check for actions requiring auth. */
41
- isAuthenticated?: () => boolean;
49
+ isAuthenticated?: () => boolean | null | undefined;
50
+ /**
51
+ * Fine-grained session capability flags (audience map, e.g.
52
+ * { has_payment_method: true }) from the generated session wiring. Boolean
53
+ * flags only; sent with each turn so the orb shows this user only THEIR
54
+ * available actions.
55
+ */
56
+ getSessionCapabilities?: () => Record<string, boolean> | null | undefined;
42
57
  /** Voice id forwarded to the backend TTS. */
43
58
  ttsVoice?: string | (() => string | undefined);
44
59
  /** End-user id for rate limiting / telemetry correlation. */
@@ -59,6 +74,22 @@ export interface FiodosAgentHandle {
59
74
  orb: MountedOrb | null;
60
75
  /** Resolves once the controller is ready (immediately on the manual path). */
61
76
  ready: Promise<AgentController | null>;
77
+ /**
78
+ * Wipes the current identity's conversation (live memory + persisted blob).
79
+ * Call it from the app's logout for a hard wipe; no-op until ready.
80
+ */
81
+ resetConversation(): void;
82
+ /**
83
+ * Publishes WHAT THE USER IS SEEING so 'execute' actions can target it —
84
+ * the embed-host equivalent of the React `useAgentScreenContext` hook.
85
+ * Hosts without our SDK on screens (Dynamics web resources, WinCC CWCs)
86
+ * call this whenever their form/record/view changes. On the auto-fetch
87
+ * path, calls made before the controller is ready are kept and applied on
88
+ * ready, so hosts don't need to await `ready` themselves.
89
+ */
90
+ setScreenContext(snapshot: AgentScreenSnapshot): void;
91
+ /** Clears a previously published snapshot (e.g. the record was closed). */
92
+ clearScreenContext(): void;
62
93
  destroy(): void;
63
94
  }
64
95
  export declare function createFiodosAgent(options: CreateFiodosAgentOptions): FiodosAgentHandle;
@@ -68,13 +68,18 @@ function assemble(options, manifest, baseUrl) {
68
68
  manifest,
69
69
  locale,
70
70
  sttLocale,
71
- navigation: (0, webNavigationAdapter_1.createWebNavigationAdapter)({ navigate: options.navigate ?? defaultNavigate }),
71
+ navigation: (0, webNavigationAdapter_1.createWebNavigationAdapter)({
72
+ navigate: options.navigate ?? defaultNavigate,
73
+ getCurrentRoute: options.getCurrentRoute,
74
+ }),
72
75
  registries: options.registries ?? { handlers: {}, idempotencyCheckers: {} },
73
76
  backend,
74
77
  voice: (0, webVoiceAdapter_1.createWebVoiceAdapter)(),
75
78
  storage: (0, webStorageAdapter_1.createWebStorageAdapter)(),
76
79
  telemetry,
77
80
  isAuthenticated: options.isAuthenticated,
81
+ getSessionCapabilities: options.getSessionCapabilities,
82
+ getUserId: options.getUserId,
78
83
  ttsVoice: options.ttsVoice,
79
84
  ...options.configOverrides,
80
85
  };
@@ -85,6 +90,9 @@ function assemble(options, manifest, baseUrl) {
85
90
  function createFiodosAgent(options) {
86
91
  const baseUrl = (options.baseUrl ?? clientBootstrap_1.DEFAULT_WEB_API_URL).replace(/\/$/, '');
87
92
  const shouldMount = options.mount !== false;
93
+ // One stable owner id per handle: the host is "one screen" from the store's
94
+ // point of view, so consecutive setScreenContext calls replace each other.
95
+ const contextOwner = Symbol('fiodos-embed-host');
88
96
  // ── Manual path: manifest provided → fully synchronous (back-compat). ───────
89
97
  if (options.manifest) {
90
98
  const controller = assemble(options, options.manifest, baseUrl);
@@ -97,6 +105,15 @@ function createFiodosAgent(options) {
97
105
  controller,
98
106
  orb,
99
107
  ready: Promise.resolve(controller),
108
+ resetConversation() {
109
+ controller.resetConversation();
110
+ },
111
+ setScreenContext(snapshot) {
112
+ controller.screenStore.set(contextOwner, snapshot);
113
+ },
114
+ clearScreenContext() {
115
+ controller.screenStore.clear(contextOwner);
116
+ },
100
117
  destroy() {
101
118
  orb?.unmount();
102
119
  controller.dispose();
@@ -105,10 +122,28 @@ function createFiodosAgent(options) {
105
122
  return handle;
106
123
  }
107
124
  // ── Auto path (React parity): fetch manifest by apiKey, then assemble. ──────
125
+ // Context published before the controller resolves is buffered (last one
126
+ // wins) and applied on ready — hosts fire form events without awaiting us.
127
+ let pendingSnapshot;
108
128
  const handle = {
109
129
  controller: null,
110
130
  orb: null,
111
131
  ready: Promise.resolve(null),
132
+ resetConversation() {
133
+ handle.controller?.resetConversation();
134
+ },
135
+ setScreenContext(snapshot) {
136
+ if (handle.controller)
137
+ handle.controller.screenStore.set(contextOwner, snapshot);
138
+ else
139
+ pendingSnapshot = snapshot;
140
+ },
141
+ clearScreenContext() {
142
+ if (handle.controller)
143
+ handle.controller.screenStore.clear(contextOwner);
144
+ else
145
+ pendingSnapshot = null;
146
+ },
112
147
  destroy() {
113
148
  cancelled = true;
114
149
  handle.orb?.unmount();
@@ -144,6 +179,9 @@ function createFiodosAgent(options) {
144
179
  return null;
145
180
  }
146
181
  handle.controller = controller;
182
+ if (pendingSnapshot)
183
+ controller.screenStore.set(contextOwner, pendingSnapshot);
184
+ pendingSnapshot = undefined;
147
185
  handle.orb = shouldMount ? (0, mountOrb_1.mountOrb)(controller, options.orb) : null;
148
186
  if (shouldMount && options.apiKey) {
149
187
  void (0, clientBootstrap_1.sendOrbSeen)({ baseUrl, apiKey: options.apiKey });
@@ -0,0 +1,61 @@
1
+ import type { AgentScreenSnapshot } from '../context/screenContextStore';
2
+ interface XrmAttributeLike {
3
+ getName(): string;
4
+ getValue(): unknown;
5
+ /** Present on optionset/lookup attributes; preferred over raw values. */
6
+ getText?(): string | string[] | null;
7
+ }
8
+ interface XrmEntityLike {
9
+ getEntityName(): string;
10
+ /** Record GUID, usually brace-wrapped: "{1D2C...}". Empty on create forms. */
11
+ getId(): string;
12
+ getPrimaryAttributeValue?(): string | null;
13
+ attributes?: {
14
+ forEach(cb: (attribute: XrmAttributeLike) => void): void;
15
+ };
16
+ /** Model-driven apps expose save events here (Xrm ≥ 9.x). */
17
+ addOnPostSave?(handler: () => void): void;
18
+ removeOnPostSave?(handler: () => void): void;
19
+ }
20
+ export interface DynamicsFormContextLike {
21
+ data: {
22
+ entity: XrmEntityLike;
23
+ };
24
+ ui?: {
25
+ getFormType?(): number;
26
+ };
27
+ }
28
+ /** The subset of FiodosAgentHandle the bridge needs (keeps this module cheap to test). */
29
+ export interface ScreenContextSink {
30
+ setScreenContext(snapshot: AgentScreenSnapshot): void;
31
+ clearScreenContext(): void;
32
+ }
33
+ export interface DynamicsSnapshotOptions {
34
+ /**
35
+ * Logical names of the attributes to expose to the agent. When omitted, the
36
+ * first `maxAttributes` non-empty scalar attributes are included.
37
+ */
38
+ attributes?: string[];
39
+ /** Cap when no allowlist is given. Default 12. */
40
+ maxAttributes?: number;
41
+ }
42
+ /** "{1D2C…}" → "1d2c…" (Web API URLs want bare lowercase GUIDs). */
43
+ export declare function normalizeDynamicsId(rawId: string): string;
44
+ /**
45
+ * Builds the AgentScreenSnapshot for the current form state, or null when
46
+ * there is nothing referenceable yet (create form: no record id).
47
+ */
48
+ export declare function dynamicsFormSnapshot(formContext: DynamicsFormContextLike, options?: DynamicsSnapshotOptions): AgentScreenSnapshot | null;
49
+ export interface DynamicsContextBridge {
50
+ /** Re-reads the form and republishes the snapshot (wire to OnChange/OnSave). */
51
+ refresh(): void;
52
+ /** Stops publishing and clears the agent's screen context. */
53
+ disconnect(): void;
54
+ }
55
+ /**
56
+ * Publishes the form snapshot now, republishes after every save (when the
57
+ * form exposes OnPostSave), and clears the context on disconnect. Call it from
58
+ * the form's OnLoad handler.
59
+ */
60
+ export declare function connectDynamicsFormContext(agent: ScreenContextSink, formContext: DynamicsFormContextLike, options?: DynamicsSnapshotOptions): DynamicsContextBridge;
61
+ export {};