@fiodos/web-core 0.1.15 → 0.1.18
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/api/anonUserId.d.ts +19 -0
- package/dist/cjs/api/anonUserId.js +63 -0
- package/dist/cjs/api/backendClient.js +4 -0
- package/dist/cjs/api/clientBootstrap.d.ts +1 -0
- package/dist/cjs/api/clientBootstrap.js +15 -4
- package/dist/cjs/controller/AgentController.js +18 -0
- package/dist/cjs/dropin/createFiodosAgent.js +6 -2
- package/dist/cjs/embed/global.d.ts +1 -1
- package/dist/cjs/embed/global.js +4 -0
- package/dist/cjs/orb/mountOrb.js +38 -0
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/embed/fiodos-embed.js +4 -4
- package/dist/esm/api/anonUserId.d.ts +19 -0
- package/dist/esm/api/anonUserId.js +59 -0
- package/dist/esm/api/backendClient.js +4 -0
- package/dist/esm/api/clientBootstrap.d.ts +1 -0
- package/dist/esm/api/clientBootstrap.js +14 -4
- package/dist/esm/controller/AgentController.js +19 -1
- package/dist/esm/dropin/createFiodosAgent.js +6 -2
- package/dist/esm/embed/global.d.ts +1 -1
- package/dist/esm/embed/global.js +4 -0
- package/dist/esm/orb/mountOrb.js +38 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable per-browser ANONYMOUS caller id, sent as `x-user-id` when the host
|
|
3
|
+
* app provides no getUserId (public storefronts, marketing sites…).
|
|
4
|
+
*
|
|
5
|
+
* Why: the backend's fairness rate-limit buckets anonymous callers by source
|
|
6
|
+
* IP. Behind shopper NATs / proxies that means MANY real visitors share one
|
|
7
|
+
* bucket and an active session can starve itself into 429 ("Too many
|
|
8
|
+
* requests"). A random per-browser id gives each visitor their own fairness
|
|
9
|
+
* bucket without identifying anyone: it is random, local, never derived from
|
|
10
|
+
* personal data, and never used for conversation identity (that stays 'anon').
|
|
11
|
+
*/
|
|
12
|
+
/** Returns the stable anonymous caller id for this browser (creates it once). */
|
|
13
|
+
export declare function getAnonCallerId(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Wraps the host's getUserId so anonymous visitors still send a stable
|
|
16
|
+
* per-browser caller id for rate-limit fairness. The host's real user id
|
|
17
|
+
* always wins when present.
|
|
18
|
+
*/
|
|
19
|
+
export declare function withAnonCallerFallback(getUserId?: () => string | null): () => string | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable per-browser ANONYMOUS caller id, sent as `x-user-id` when the host
|
|
3
|
+
* app provides no getUserId (public storefronts, marketing sites…).
|
|
4
|
+
*
|
|
5
|
+
* Why: the backend's fairness rate-limit buckets anonymous callers by source
|
|
6
|
+
* IP. Behind shopper NATs / proxies that means MANY real visitors share one
|
|
7
|
+
* bucket and an active session can starve itself into 429 ("Too many
|
|
8
|
+
* requests"). A random per-browser id gives each visitor their own fairness
|
|
9
|
+
* bucket without identifying anyone: it is random, local, never derived from
|
|
10
|
+
* personal data, and never used for conversation identity (that stays 'anon').
|
|
11
|
+
*/
|
|
12
|
+
const STORAGE_KEY = 'fyodos:anon-caller:v1';
|
|
13
|
+
let inMemoryId = null;
|
|
14
|
+
function randomId() {
|
|
15
|
+
try {
|
|
16
|
+
const c = globalThis.crypto;
|
|
17
|
+
if (c?.randomUUID)
|
|
18
|
+
return `anon-${c.randomUUID()}`;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
/* fall through */
|
|
22
|
+
}
|
|
23
|
+
return `anon-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
|
24
|
+
}
|
|
25
|
+
/** Returns the stable anonymous caller id for this browser (creates it once). */
|
|
26
|
+
export function getAnonCallerId() {
|
|
27
|
+
if (inMemoryId)
|
|
28
|
+
return inMemoryId;
|
|
29
|
+
try {
|
|
30
|
+
const stored = window.localStorage.getItem(STORAGE_KEY);
|
|
31
|
+
if (stored) {
|
|
32
|
+
inMemoryId = stored;
|
|
33
|
+
return stored;
|
|
34
|
+
}
|
|
35
|
+
const fresh = randomId();
|
|
36
|
+
window.localStorage.setItem(STORAGE_KEY, fresh);
|
|
37
|
+
inMemoryId = fresh;
|
|
38
|
+
return fresh;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Private mode / storage blocked: keep a per-page-load id (still better
|
|
42
|
+
// than sharing the IP bucket with every other visitor).
|
|
43
|
+
inMemoryId = inMemoryId ?? randomId();
|
|
44
|
+
return inMemoryId;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Wraps the host's getUserId so anonymous visitors still send a stable
|
|
49
|
+
* per-browser caller id for rate-limit fairness. The host's real user id
|
|
50
|
+
* always wins when present.
|
|
51
|
+
*/
|
|
52
|
+
export function withAnonCallerFallback(getUserId) {
|
|
53
|
+
return () => {
|
|
54
|
+
const real = getUserId?.();
|
|
55
|
+
if (real)
|
|
56
|
+
return real;
|
|
57
|
+
return getAnonCallerId();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -141,6 +141,10 @@ export function createFiodosBackendClient(options) {
|
|
|
141
141
|
intent: request.lastStep.intent,
|
|
142
142
|
...(request.lastStep.label ? { label: request.lastStep.label } : {}),
|
|
143
143
|
success: request.lastStep.success,
|
|
144
|
+
// Bounded result payload of the executed step (product search
|
|
145
|
+
// results…): the backend renders it so the model can read real
|
|
146
|
+
// ids for the NEXT step's parameters.
|
|
147
|
+
...(request.lastStep.data ? { data: request.lastStep.data } : {}),
|
|
144
148
|
};
|
|
145
149
|
}
|
|
146
150
|
}
|
|
@@ -33,6 +33,7 @@ export declare function fetchClientManifest(opts: {
|
|
|
33
33
|
baseUrl: string;
|
|
34
34
|
apiKey: string;
|
|
35
35
|
}): Promise<ManifestBootstrapResult>;
|
|
36
|
+
export declare function markEmbedDistribution(): void;
|
|
36
37
|
/**
|
|
37
38
|
* Best-effort heartbeat proving the orb booted. Never throws and never blocks
|
|
38
39
|
* the orb — failures are swallowed (the dashboard simply won't flip to ✓).
|
|
@@ -37,11 +37,21 @@ export async function fetchClientManifest(opts) {
|
|
|
37
37
|
}
|
|
38
38
|
return { status: 'ready', manifest: data.manifest };
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Set by the embed bundle entrypoint (dist/embed/fiodos-embed.js). Embeds are
|
|
42
|
+
* loaded from a CDN `@latest` URL (Shopify themes, Dynamics…), so they update
|
|
43
|
+
* THEMSELVES — the heartbeat marks them so the dashboard never shows an
|
|
44
|
+
* "update available / npm install" notice the developer cannot act on.
|
|
45
|
+
*/
|
|
46
|
+
let embedDistribution = false;
|
|
47
|
+
export function markEmbedDistribution() {
|
|
48
|
+
embedDistribution = true;
|
|
49
|
+
}
|
|
40
50
|
function detectPlatform() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return 'web';
|
|
51
|
+
const mobile = typeof navigator !== 'undefined' && /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent);
|
|
52
|
+
if (embedDistribution)
|
|
53
|
+
return mobile ? 'web-embed-mobile' : 'web-embed';
|
|
54
|
+
return mobile ? 'web-mobile' : 'web';
|
|
45
55
|
}
|
|
46
56
|
/**
|
|
47
57
|
* Best-effort heartbeat proving the orb booted. Never throws and never blocks
|
|
@@ -15,7 +15,7 @@
|
|
|
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, clearPersistedConversation, conversationIdentityKey, 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, trimChainStepData, validateManifest, } from '@fiodos/core';
|
|
19
19
|
import { createWebCredentialAutofillAdapter } from '../adapters/webCredentialAutofillAdapter.js';
|
|
20
20
|
import { AgentApiError } from '../api/errors.js';
|
|
21
21
|
import { formatAgentTurnError } from '../api/formatTurnError.js';
|
|
@@ -713,9 +713,22 @@ export class AgentController {
|
|
|
713
713
|
/* keep the assistant's guiding reply; never speak a refusal. */
|
|
714
714
|
}
|
|
715
715
|
else if (r.message) {
|
|
716
|
+
// FEEDBACK GUARANTEE: the handler's outcome message (success,
|
|
717
|
+
// idempotent or failure) is the ground truth that the action ran;
|
|
718
|
+
// speak it instead of the LLM's optimistic reply.
|
|
716
719
|
outReply = r.message;
|
|
717
720
|
outAudio = null;
|
|
718
721
|
}
|
|
722
|
+
else if (!r.success) {
|
|
723
|
+
// Same guarantee for a handler that failed WITHOUT setting `message`
|
|
724
|
+
// (e.g. a declarative Shopify/OData `execution` call that hit a
|
|
725
|
+
// network/HTTP/template error — apiActions.ts never throws, so
|
|
726
|
+
// runManifestAction's catch never runs either). Without this, the
|
|
727
|
+
// LLM's pre-action narration ("Adding X to your cart…") stays on
|
|
728
|
+
// screen as if it had happened — a silent false success.
|
|
729
|
+
outReply = this.messages.actionFailed;
|
|
730
|
+
outAudio = null;
|
|
731
|
+
}
|
|
719
732
|
// Chain signal: only a cleanly EXECUTED, non-recoverable step may advance,
|
|
720
733
|
// and only when the backend asked to continue (task_status=in_progress).
|
|
721
734
|
if (turn.action &&
|
|
@@ -723,11 +736,16 @@ export class AgentController {
|
|
|
723
736
|
r.success &&
|
|
724
737
|
!r.recoverable) {
|
|
725
738
|
const intent = turn.action.intent ?? '';
|
|
739
|
+
// Feed the step's RESULT DATA back to the next chain turn (bounded):
|
|
740
|
+
// it is how the model reads real technical values (variant ids, line
|
|
741
|
+
// numbers) out of a data action instead of inventing them.
|
|
742
|
+
const chainData = trimChainStepData(r.data);
|
|
726
743
|
lastStep = {
|
|
727
744
|
type: turn.action.type,
|
|
728
745
|
intent,
|
|
729
746
|
label: turn.action.type === 'navigate' ? this.routeLabel(intent) : this.actionLabel(intent),
|
|
730
747
|
success: true,
|
|
748
|
+
...(chainData ? { data: chainData } : {}),
|
|
731
749
|
};
|
|
732
750
|
actionKey = `${turn.action.type}:${intent}`;
|
|
733
751
|
canContinue = turn.taskStatus === 'in_progress';
|
|
@@ -21,6 +21,7 @@ import { AgentController } from '../controller/AgentController.js';
|
|
|
21
21
|
import { mountOrb } from '../orb/mountOrb.js';
|
|
22
22
|
import { showOrbDevError } from '../orb/devErrorBadge.js';
|
|
23
23
|
import { DEFAULT_WEB_API_URL, fetchClientManifest, sendOrbSeen } from '../api/clientBootstrap.js';
|
|
24
|
+
import { withAnonCallerFallback } from '../api/anonUserId.js';
|
|
24
25
|
function defaultNavigate(route) {
|
|
25
26
|
if (typeof window !== 'undefined' && window.location)
|
|
26
27
|
window.location.assign(route);
|
|
@@ -60,15 +61,18 @@ function assemble(options, manifest, baseUrl) {
|
|
|
60
61
|
const fallback = browserLocale();
|
|
61
62
|
const locale = resolveAgentLocale(options);
|
|
62
63
|
const sttLocale = options.sttLocale ?? options.locale ?? fallback.sttLocale;
|
|
64
|
+
// Rate-limit fairness: anonymous visitors still send a stable per-browser
|
|
65
|
+
// caller id (never used for conversation identity — that stays 'anon').
|
|
66
|
+
const callerId = withAnonCallerFallback(options.getUserId);
|
|
63
67
|
const backend = createFiodosBackendClient({
|
|
64
68
|
baseUrl,
|
|
65
69
|
apiKey: options.apiKey,
|
|
66
|
-
getUserId:
|
|
70
|
+
getUserId: callerId,
|
|
67
71
|
});
|
|
68
72
|
const telemetry = createFiodosTelemetry({
|
|
69
73
|
baseUrl,
|
|
70
74
|
apiKey: options.apiKey,
|
|
71
|
-
getUserId:
|
|
75
|
+
getUserId: callerId,
|
|
72
76
|
});
|
|
73
77
|
const config = {
|
|
74
78
|
manifest,
|
|
@@ -27,7 +27,7 @@ import { createScreenContextStore } from '../context/screenContextStore.js';
|
|
|
27
27
|
import { connectDynamicsFormContext, dynamicsFormSnapshot, normalizeDynamicsId } from './dynamics.js';
|
|
28
28
|
import { buildApiActionRegistries, validateManifest } from '@fiodos/core';
|
|
29
29
|
declare const api: {
|
|
30
|
-
readonly version: "0.1.
|
|
30
|
+
readonly version: "0.1.18";
|
|
31
31
|
readonly createFiodosAgent: typeof createFiodosAgent;
|
|
32
32
|
readonly mountOrb: typeof mountOrb;
|
|
33
33
|
readonly AgentController: typeof AgentController;
|
package/dist/esm/embed/global.js
CHANGED
|
@@ -26,7 +26,11 @@ import { AgentController } from '../controller/AgentController.js';
|
|
|
26
26
|
import { createScreenContextStore } from '../context/screenContextStore.js';
|
|
27
27
|
import { connectDynamicsFormContext, dynamicsFormSnapshot, normalizeDynamicsId, } from './dynamics.js';
|
|
28
28
|
import { SDK_VERSION } from '../version.js';
|
|
29
|
+
import { markEmbedDistribution } from '../api/clientBootstrap.js';
|
|
29
30
|
import { buildApiActionRegistries, validateManifest } from '@fiodos/core';
|
|
31
|
+
// This bundle is served from a CDN `@latest` URL and updates itself: mark the
|
|
32
|
+
// heartbeat so the dashboard never shows an npm update command for it.
|
|
33
|
+
markEmbedDistribution();
|
|
30
34
|
const api = {
|
|
31
35
|
version: SDK_VERSION,
|
|
32
36
|
createFiodosAgent,
|
package/dist/esm/orb/mountOrb.js
CHANGED
|
@@ -707,13 +707,44 @@ export function mountOrb(controller, options = {}) {
|
|
|
707
707
|
// Live-activity line: white (reply color), italic — not the faint gray.
|
|
708
708
|
busyStep.style.color = t.titleColor;
|
|
709
709
|
}
|
|
710
|
+
// The panel's open state survives FULL PAGE LOADS within the tab session:
|
|
711
|
+
// on multi-page hosts (Shopify storefronts…) every agent navigation reloads
|
|
712
|
+
// the document, and without this the conversation panel closed mid-dialogue
|
|
713
|
+
// on every step. sessionStorage is per-tab and cleared when the tab closes,
|
|
714
|
+
// so a fresh visit never starts with the panel open.
|
|
715
|
+
const panelOpenKey = `fyodos:panel-open:${controller.config.manifest.appId}`;
|
|
716
|
+
function persistPanelOpen(open) {
|
|
717
|
+
try {
|
|
718
|
+
if (open)
|
|
719
|
+
window.sessionStorage.setItem(panelOpenKey, '1');
|
|
720
|
+
else
|
|
721
|
+
window.sessionStorage.removeItem(panelOpenKey);
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
/* storage unavailable (private mode) — persistence is best-effort */
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function wasPanelOpen() {
|
|
728
|
+
try {
|
|
729
|
+
return window.sessionStorage.getItem(panelOpenKey) === '1';
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
710
735
|
function setBubble(open) {
|
|
711
736
|
bubbleOpen = open;
|
|
712
737
|
bubble.style.display = open ? 'flex' : 'none';
|
|
738
|
+
persistPanelOpen(open);
|
|
713
739
|
if (open) {
|
|
714
740
|
applyBubbleTheme();
|
|
715
741
|
positionBubble();
|
|
716
742
|
textInput.focus();
|
|
743
|
+
// Paint the CURRENT conversation immediately: exchanges are only
|
|
744
|
+
// rendered inside render() while the panel is open, so without this
|
|
745
|
+
// the restored history stayed invisible (an apparently stuck panel)
|
|
746
|
+
// until the next controller event — i.e. until the user typed.
|
|
747
|
+
render(controller.getState());
|
|
717
748
|
}
|
|
718
749
|
else {
|
|
719
750
|
bubbleDictation.stop();
|
|
@@ -1156,6 +1187,13 @@ export function mountOrb(controller, options = {}) {
|
|
|
1156
1187
|
}
|
|
1157
1188
|
const unsubscribe = controller.subscribe(render);
|
|
1158
1189
|
render(controller.getState());
|
|
1190
|
+
// Re-open the conversation panel after a full page load when it was open
|
|
1191
|
+
// before (MPA hosts reload on every navigation): the dialogue visually
|
|
1192
|
+
// continues where it was — the persisted conversation restores async in the
|
|
1193
|
+
// controller and repaints via emit → render once loaded.
|
|
1194
|
+
if (enableTextInput && wasPanelOpen()) {
|
|
1195
|
+
setBubble(true);
|
|
1196
|
+
}
|
|
1159
1197
|
// Track the current route AND the app's live locale so the thought bubble
|
|
1160
1198
|
// re-resolves its CTA on navigation and on language switches. No universal
|
|
1161
1199
|
// navigation/i18n event exists, so poll + listen to popstate / visibility
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
|
|
6
6
|
* by hand — change package.json `version` and re-run the sync/release script.
|
|
7
7
|
*/
|
|
8
|
-
export declare const SDK_VERSION = "0.1.
|
|
8
|
+
export declare const SDK_VERSION = "0.1.18";
|
|
9
9
|
export declare const SDK_PACKAGE = "@fiodos/web-core";
|
package/dist/esm/version.js
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
|
|
6
6
|
* by hand — change package.json `version` and re-run the sync/release script.
|
|
7
7
|
*/
|
|
8
|
-
export const SDK_VERSION = '0.1.
|
|
8
|
+
export const SDK_VERSION = '0.1.18';
|
|
9
9
|
export const SDK_PACKAGE = '@fiodos/web-core';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/web-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Framework-agnostic browser layer for the Fiodos agent: a vanilla AgentController (orchestrator), DOM adapters (navigation/voice/storage), HTTP client and decision engine over @fiodos/core. Shared base for @fiodos/vue, @fiodos/svelte and @fiodos/angular (no framework imports).",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"publishConfig": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"prepublishOnly": "node ../../scripts/sync-sdk-version.mjs && npm run build"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@fiodos/core": "^0.1.
|
|
33
|
+
"@fiodos/core": "^0.1.13"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^22.0.0",
|