@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.
- package/dist/cjs/adapters/webCredentialAutofillAdapter.d.ts +16 -0
- package/dist/cjs/adapters/webCredentialAutofillAdapter.js +47 -0
- package/dist/cjs/api/backendClient.js +14 -0
- package/dist/cjs/config/types.d.ts +21 -2
- package/dist/cjs/controller/AgentController.d.ts +35 -1
- package/dist/cjs/controller/AgentController.js +123 -3
- package/dist/cjs/dropin/createFiodosAgent.d.ts +32 -1
- package/dist/cjs/dropin/createFiodosAgent.js +39 -1
- package/dist/cjs/embed/dynamics.d.ts +61 -0
- package/dist/cjs/embed/dynamics.js +173 -0
- package/dist/cjs/embed/global.d.ts +45 -0
- package/dist/cjs/embed/global.js +48 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/orb/mountOrb.js +111 -18
- package/dist/cjs/orb/orbView.js +40 -7
- package/dist/cjs/orb/publishedConfig.d.ts +4 -2
- package/dist/cjs/orb/publishedConfig.js +56 -7
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/embed/fiodos-embed.js +66 -0
- package/dist/esm/adapters/webCredentialAutofillAdapter.d.ts +16 -0
- package/dist/esm/adapters/webCredentialAutofillAdapter.js +44 -0
- package/dist/esm/api/backendClient.js +14 -0
- package/dist/esm/config/types.d.ts +21 -2
- package/dist/esm/controller/AgentController.d.ts +35 -1
- package/dist/esm/controller/AgentController.js +124 -4
- package/dist/esm/dropin/createFiodosAgent.d.ts +32 -1
- package/dist/esm/dropin/createFiodosAgent.js +39 -1
- package/dist/esm/embed/dynamics.d.ts +61 -0
- package/dist/esm/embed/dynamics.js +168 -0
- package/dist/esm/embed/global.d.ts +45 -0
- package/dist/esm/embed/global.js +46 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/orb/mountOrb.js +112 -19
- package/dist/esm/orb/orbView.js +41 -8
- package/dist/esm/orb/publishedConfig.d.ts +4 -2
- package/dist/esm/orb/publishedConfig.js +56 -7
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +5 -3
|
@@ -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.js';
|
|
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
|
|
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
|
/**
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* deliberate modal tap; a loose "yes" never confirms them.
|
|
16
16
|
* - The agent only ever executes actions declared in the manifest.
|
|
17
17
|
*/
|
|
18
|
-
import { combineContexts, createActionExecutor, createConsentManager, createSafeTelemetry, createSanitizer, buildBubbleExchangeWindow, BUBBLE_PAGE_EXCHANGES, conversationRetentionOptions, createConversationSession, persistConversationSession, restoreConversationSession, defaultRouteContextText, formatMessage, prepareConversationForTurn, recordConversationExchange, resolveConfirmationLexicon, resolveMessages, validateManifest, } from '@fiodos/core';
|
|
18
|
+
import { combineContexts, createActionExecutor, createConsentManager, createSafeTelemetry, createSanitizer, buildBubbleExchangeWindow, BUBBLE_PAGE_EXCHANGES, clearPersistedConversation, conversationIdentityKey, conversationRetentionOptions, createConversationSession, persistConversationSession, restoreConversationSession, defaultRouteContextText, formatMessage, prepareConversationForTurn, recordConversationExchange, resolveConfirmationLexicon, resolveMessages, validateManifest, } from '@fiodos/core';
|
|
19
|
+
import { createWebCredentialAutofillAdapter } from '../adapters/webCredentialAutofillAdapter.js';
|
|
19
20
|
import { AgentApiError } from '../api/errors.js';
|
|
20
21
|
import { formatAgentTurnError } from '../api/formatTurnError.js';
|
|
21
22
|
import { buildManifestPayload } from '../api/backendClient.js';
|
|
@@ -40,6 +41,7 @@ export class AgentController {
|
|
|
40
41
|
this.consentModalVisible = false;
|
|
41
42
|
this.sessionId = randomSessionId();
|
|
42
43
|
this.sessionStarted = false;
|
|
44
|
+
// Replaced wholesale on identity swaps / explicit resets.
|
|
43
45
|
this.conversation = createConversationSession();
|
|
44
46
|
this.pending = null;
|
|
45
47
|
this.confirmationReasked = false;
|
|
@@ -69,6 +71,7 @@ export class AgentController {
|
|
|
69
71
|
/** Persistence scope: internal orbs keep their memory under a separate key
|
|
70
72
|
* so a public orb sharing the same appId can never read it. */
|
|
71
73
|
this.conversationScope = 'public';
|
|
74
|
+
this.removeIdentityListeners = null;
|
|
72
75
|
this.loadingOlderExchanges = false;
|
|
73
76
|
this.loadOlderTimer = null;
|
|
74
77
|
this.listeners = new Set();
|
|
@@ -89,12 +92,19 @@ export class AgentController {
|
|
|
89
92
|
appId: config.manifest.appId,
|
|
90
93
|
version: config.consentVersion,
|
|
91
94
|
});
|
|
95
|
+
// SMART SIGN-IN ships by default on web (browser password manager);
|
|
96
|
+
// hosts can override the adapter or pass null to opt out entirely.
|
|
97
|
+
this.credentialAutofill =
|
|
98
|
+
config.credentialAutofill === undefined
|
|
99
|
+
? createWebCredentialAutofillAdapter()
|
|
100
|
+
: config.credentialAutofill;
|
|
92
101
|
this.executor = createActionExecutor({
|
|
93
102
|
manifest: config.manifest,
|
|
94
103
|
registries: config.registries,
|
|
95
104
|
navigation: config.navigation,
|
|
96
105
|
messages: this.messages,
|
|
97
106
|
isAuthenticated: config.isAuthenticated,
|
|
107
|
+
credentialAutofill: this.credentialAutofill,
|
|
98
108
|
});
|
|
99
109
|
this.timings = { ...DEFAULT_AGENT_TIMINGS, ...config.timings };
|
|
100
110
|
// Chaining OFF by default; clamp to the same safe bounds as react/react-native.
|
|
@@ -128,15 +138,29 @@ export class AgentController {
|
|
|
128
138
|
});
|
|
129
139
|
// Re-emit combined state whenever the speech machine advances.
|
|
130
140
|
this.unsubscribeSpeech = this.speech.subscribe(() => this.emit());
|
|
141
|
+
this.conversationIdentity = conversationIdentityKey(config.getUserId?.());
|
|
131
142
|
// Rehydrate the previous conversation (page reloads): the persisted blob
|
|
132
143
|
// carries its own retention rule, so expiry is decided correctly even
|
|
133
144
|
// before the published config arrives. Best-effort and non-blocking.
|
|
134
145
|
void this.restorePersistedConversation();
|
|
146
|
+
// Logout/login usually happens on another screen: re-check identity when
|
|
147
|
+
// the tab regains focus or becomes visible again, besides before every turn.
|
|
148
|
+
if (typeof window !== 'undefined') {
|
|
149
|
+
const check = () => void this.syncConversationIdentity();
|
|
150
|
+
window.addEventListener('focus', check);
|
|
151
|
+
document.addEventListener('visibilitychange', check);
|
|
152
|
+
this.removeIdentityListeners = () => {
|
|
153
|
+
window.removeEventListener('focus', check);
|
|
154
|
+
document.removeEventListener('visibilitychange', check);
|
|
155
|
+
};
|
|
156
|
+
}
|
|
135
157
|
}
|
|
136
158
|
// ── Conversation persistence (survives page reloads) ───────────────────────
|
|
137
159
|
async restorePersistedConversation() {
|
|
138
|
-
const
|
|
139
|
-
|
|
160
|
+
const identityAtStart = this.conversationIdentity;
|
|
161
|
+
const restored = await restoreConversationSession(this.config.storage, this.config.manifest.appId, Date.now(), this.conversationScope, identityAtStart);
|
|
162
|
+
// The identity may have changed again while the read resolved.
|
|
163
|
+
if (!restored || this.conversationIdentity !== identityAtStart)
|
|
140
164
|
return;
|
|
141
165
|
// Never clobber turns the user already made while the restore resolved.
|
|
142
166
|
if (this.conversation.turns.length > 0 || this.conversation.uiArchive?.length)
|
|
@@ -148,7 +172,43 @@ export class AgentController {
|
|
|
148
172
|
this.emit();
|
|
149
173
|
}
|
|
150
174
|
persistConversation() {
|
|
151
|
-
void persistConversationSession(this.config.storage, this.config.manifest.appId, this.conversation, this.conversationRetentionMinutes, this.conversationScope);
|
|
175
|
+
void persistConversationSession(this.config.storage, this.config.manifest.appId, this.conversation, this.conversationRetentionMinutes, this.conversationScope, this.conversationIdentity);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Re-evaluates the host identity and, when it changed, swaps conversations:
|
|
179
|
+
* the outgoing identity's session is saved under ITS private key (so a user
|
|
180
|
+
* who logs back in recovers it, honouring "never clear"), all in-memory
|
|
181
|
+
* state and UI are wiped, and the incoming identity's own session (usually
|
|
182
|
+
* empty) is restored. An anonymous visitor after a logout always starts
|
|
183
|
+
* clean — the previous user's transcript is never inherited.
|
|
184
|
+
*/
|
|
185
|
+
syncConversationIdentity() {
|
|
186
|
+
const next = conversationIdentityKey(this.config.getUserId?.());
|
|
187
|
+
if (next === this.conversationIdentity)
|
|
188
|
+
return false;
|
|
189
|
+
// 1. Park the outgoing identity's session under its own key.
|
|
190
|
+
this.persistConversation();
|
|
191
|
+
// 2. Wipe every trace from memory and the panel.
|
|
192
|
+
this.conversation = createConversationSession();
|
|
193
|
+
this.conversationIdentity = next;
|
|
194
|
+
this.lastExchange = null;
|
|
195
|
+
this.emit();
|
|
196
|
+
// 3. Bring in the new identity's own session, if any.
|
|
197
|
+
void this.restorePersistedConversation();
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Wipes the CURRENT identity's conversation: live memory, panel and the
|
|
202
|
+
* persisted blob. Hosts can call it from their logout for a hard wipe (by
|
|
203
|
+
* default a logout only parks the conversation under the user's private
|
|
204
|
+
* local key).
|
|
205
|
+
*/
|
|
206
|
+
resetConversation() {
|
|
207
|
+
this.cancelAll();
|
|
208
|
+
this.conversation = createConversationSession();
|
|
209
|
+
this.lastExchange = null;
|
|
210
|
+
void clearPersistedConversation(this.config.storage, this.config.manifest.appId, this.conversationScope, this.conversationIdentity);
|
|
211
|
+
this.emit();
|
|
152
212
|
}
|
|
153
213
|
// ── Subscription / state ───────────────────────────────────────────────────
|
|
154
214
|
getState() {
|
|
@@ -529,6 +589,57 @@ export class AgentController {
|
|
|
529
589
|
this.beginConfirmationVoiceWindow();
|
|
530
590
|
}
|
|
531
591
|
}
|
|
592
|
+
/**
|
|
593
|
+
* Device capability flags advertised on each turn (never user data). Fresh
|
|
594
|
+
* per turn: cheap, and honest if the environment changes mid-session.
|
|
595
|
+
*/
|
|
596
|
+
platformCapabilities() {
|
|
597
|
+
let autofill = false;
|
|
598
|
+
try {
|
|
599
|
+
autofill = this.credentialAutofill?.isSupported() === true;
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
autofill = false;
|
|
603
|
+
}
|
|
604
|
+
return autofill ? { credentialAutofill: true } : undefined;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Live session verdict advertised on each turn (audience map — never user
|
|
608
|
+
* data): whether the user is signed in plus the app's boolean capability
|
|
609
|
+
* flags. Fresh per turn so login/logout mid-session is honest. Undefined
|
|
610
|
+
* when the host wired neither source (older installs — backend filters
|
|
611
|
+
* nothing, exactly today's behavior).
|
|
612
|
+
*/
|
|
613
|
+
sessionContext() {
|
|
614
|
+
const { isAuthenticated, getSessionCapabilities } = this.config;
|
|
615
|
+
if (!isAuthenticated && !getSessionCapabilities)
|
|
616
|
+
return undefined;
|
|
617
|
+
let authenticated = null;
|
|
618
|
+
try {
|
|
619
|
+
const v = isAuthenticated?.();
|
|
620
|
+
authenticated = v == null ? null : Boolean(v);
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
authenticated = null;
|
|
624
|
+
}
|
|
625
|
+
let capabilities;
|
|
626
|
+
try {
|
|
627
|
+
const raw = getSessionCapabilities?.();
|
|
628
|
+
if (raw && typeof raw === 'object') {
|
|
629
|
+
const clean = {};
|
|
630
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
631
|
+
if (typeof v === 'boolean')
|
|
632
|
+
clean[k] = v;
|
|
633
|
+
}
|
|
634
|
+
if (Object.keys(clean).length > 0)
|
|
635
|
+
capabilities = clean;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
capabilities = undefined;
|
|
640
|
+
}
|
|
641
|
+
return { authenticated, ...(capabilities ? { capabilities } : {}) };
|
|
642
|
+
}
|
|
532
643
|
routeLabel(intent) {
|
|
533
644
|
return this.config.manifest.routes.find((r) => r.intent === intent)?.label;
|
|
534
645
|
}
|
|
@@ -714,6 +825,8 @@ export class AgentController {
|
|
|
714
825
|
isContinuation: true,
|
|
715
826
|
chainStep: step,
|
|
716
827
|
lastStep,
|
|
828
|
+
platformCapabilities: this.platformCapabilities(),
|
|
829
|
+
sessionContext: this.sessionContext(),
|
|
717
830
|
...buildManifestPayload(this.config.manifest),
|
|
718
831
|
}, { signal: controller.signal });
|
|
719
832
|
oc = await this.applyOutcome(turn, routeNow, snap?.context ?? {}, state.userMessage);
|
|
@@ -817,6 +930,9 @@ export class AgentController {
|
|
|
817
930
|
this.setPhase('idle');
|
|
818
931
|
return;
|
|
819
932
|
}
|
|
933
|
+
// Identity gate: if the host user changed since the last turn (login,
|
|
934
|
+
// logout, account switch), swap conversations BEFORE recording anything.
|
|
935
|
+
this.syncConversationIdentity();
|
|
820
936
|
this.setPhase('thinking');
|
|
821
937
|
// REAL activity: the request is genuinely in flight from this moment.
|
|
822
938
|
this.setActivity({ kind: 'thinking' });
|
|
@@ -868,6 +984,8 @@ export class AgentController {
|
|
|
868
984
|
conversationHistory: conv.history,
|
|
869
985
|
conversationSummary: conv.summary ?? undefined,
|
|
870
986
|
sessionId: this.sessionId,
|
|
987
|
+
platformCapabilities: this.platformCapabilities(),
|
|
988
|
+
sessionContext: this.sessionContext(),
|
|
871
989
|
...buildManifestPayload(this.config.manifest),
|
|
872
990
|
}, { signal: controller.signal });
|
|
873
991
|
outcome = await this.applyOutcome(turn, currentRoute, snapshot?.context ?? {}, text);
|
|
@@ -1109,6 +1227,8 @@ export class AgentController {
|
|
|
1109
1227
|
}
|
|
1110
1228
|
dispose() {
|
|
1111
1229
|
this.cancelAll();
|
|
1230
|
+
this.removeIdentityListeners?.();
|
|
1231
|
+
this.removeIdentityListeners = null;
|
|
1112
1232
|
if (this.loadOlderTimer) {
|
|
1113
1233
|
clearTimeout(this.loadOlderTimer);
|
|
1114
1234
|
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.js';
|
|
16
|
+
import type { AgentScreenSnapshot } from '../context/screenContextStore.js';
|
|
16
17
|
import { type MountOrbOptions, type MountedOrb } from '../orb/mountOrb.js';
|
|
17
18
|
import type { FiodosWebAgentConfig } from '../config/types.js';
|
|
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;
|
|
@@ -65,13 +65,18 @@ function assemble(options, manifest, baseUrl) {
|
|
|
65
65
|
manifest,
|
|
66
66
|
locale,
|
|
67
67
|
sttLocale,
|
|
68
|
-
navigation: createWebNavigationAdapter({
|
|
68
|
+
navigation: createWebNavigationAdapter({
|
|
69
|
+
navigate: options.navigate ?? defaultNavigate,
|
|
70
|
+
getCurrentRoute: options.getCurrentRoute,
|
|
71
|
+
}),
|
|
69
72
|
registries: options.registries ?? { handlers: {}, idempotencyCheckers: {} },
|
|
70
73
|
backend,
|
|
71
74
|
voice: createWebVoiceAdapter(),
|
|
72
75
|
storage: createWebStorageAdapter(),
|
|
73
76
|
telemetry,
|
|
74
77
|
isAuthenticated: options.isAuthenticated,
|
|
78
|
+
getSessionCapabilities: options.getSessionCapabilities,
|
|
79
|
+
getUserId: options.getUserId,
|
|
75
80
|
ttsVoice: options.ttsVoice,
|
|
76
81
|
...options.configOverrides,
|
|
77
82
|
};
|
|
@@ -82,6 +87,9 @@ function assemble(options, manifest, baseUrl) {
|
|
|
82
87
|
export function createFiodosAgent(options) {
|
|
83
88
|
const baseUrl = (options.baseUrl ?? DEFAULT_WEB_API_URL).replace(/\/$/, '');
|
|
84
89
|
const shouldMount = options.mount !== false;
|
|
90
|
+
// One stable owner id per handle: the host is "one screen" from the store's
|
|
91
|
+
// point of view, so consecutive setScreenContext calls replace each other.
|
|
92
|
+
const contextOwner = Symbol('fiodos-embed-host');
|
|
85
93
|
// ── Manual path: manifest provided → fully synchronous (back-compat). ───────
|
|
86
94
|
if (options.manifest) {
|
|
87
95
|
const controller = assemble(options, options.manifest, baseUrl);
|
|
@@ -94,6 +102,15 @@ export function createFiodosAgent(options) {
|
|
|
94
102
|
controller,
|
|
95
103
|
orb,
|
|
96
104
|
ready: Promise.resolve(controller),
|
|
105
|
+
resetConversation() {
|
|
106
|
+
controller.resetConversation();
|
|
107
|
+
},
|
|
108
|
+
setScreenContext(snapshot) {
|
|
109
|
+
controller.screenStore.set(contextOwner, snapshot);
|
|
110
|
+
},
|
|
111
|
+
clearScreenContext() {
|
|
112
|
+
controller.screenStore.clear(contextOwner);
|
|
113
|
+
},
|
|
97
114
|
destroy() {
|
|
98
115
|
orb?.unmount();
|
|
99
116
|
controller.dispose();
|
|
@@ -102,10 +119,28 @@ export function createFiodosAgent(options) {
|
|
|
102
119
|
return handle;
|
|
103
120
|
}
|
|
104
121
|
// ── Auto path (React parity): fetch manifest by apiKey, then assemble. ──────
|
|
122
|
+
// Context published before the controller resolves is buffered (last one
|
|
123
|
+
// wins) and applied on ready — hosts fire form events without awaiting us.
|
|
124
|
+
let pendingSnapshot;
|
|
105
125
|
const handle = {
|
|
106
126
|
controller: null,
|
|
107
127
|
orb: null,
|
|
108
128
|
ready: Promise.resolve(null),
|
|
129
|
+
resetConversation() {
|
|
130
|
+
handle.controller?.resetConversation();
|
|
131
|
+
},
|
|
132
|
+
setScreenContext(snapshot) {
|
|
133
|
+
if (handle.controller)
|
|
134
|
+
handle.controller.screenStore.set(contextOwner, snapshot);
|
|
135
|
+
else
|
|
136
|
+
pendingSnapshot = snapshot;
|
|
137
|
+
},
|
|
138
|
+
clearScreenContext() {
|
|
139
|
+
if (handle.controller)
|
|
140
|
+
handle.controller.screenStore.clear(contextOwner);
|
|
141
|
+
else
|
|
142
|
+
pendingSnapshot = null;
|
|
143
|
+
},
|
|
109
144
|
destroy() {
|
|
110
145
|
cancelled = true;
|
|
111
146
|
handle.orb?.unmount();
|
|
@@ -141,6 +176,9 @@ export function createFiodosAgent(options) {
|
|
|
141
176
|
return null;
|
|
142
177
|
}
|
|
143
178
|
handle.controller = controller;
|
|
179
|
+
if (pendingSnapshot)
|
|
180
|
+
controller.screenStore.set(contextOwner, pendingSnapshot);
|
|
181
|
+
pendingSnapshot = undefined;
|
|
144
182
|
handle.orb = shouldMount ? mountOrb(controller, options.orb) : null;
|
|
145
183
|
if (shouldMount && options.apiKey) {
|
|
146
184
|
void sendOrbSeen({ baseUrl, apiKey: options.apiKey });
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AgentScreenSnapshot } from '../context/screenContextStore.js';
|
|
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 {};
|