@fiodos/web-core 0.1.19 → 0.1.21
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/backendClient.js +3 -0
- package/dist/cjs/api/backendTelemetry.js +15 -0
- package/dist/cjs/controller/AgentController.js +80 -1
- package/dist/cjs/embed/global.d.ts +1 -1
- package/dist/cjs/orb/mountOrb.js +42 -8
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/embed/fiodos-embed.js +6 -4
- package/dist/esm/api/backendClient.js +3 -0
- package/dist/esm/api/backendTelemetry.js +15 -0
- package/dist/esm/controller/AgentController.js +80 -1
- package/dist/esm/embed/global.d.ts +1 -1
- package/dist/esm/orb/mountOrb.js +43 -9
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
|
@@ -147,6 +147,9 @@ export function createFiodosBackendClient(options) {
|
|
|
147
147
|
intent: request.lastStep.intent,
|
|
148
148
|
...(request.lastStep.label ? { label: request.lastStep.label } : {}),
|
|
149
149
|
success: request.lastStep.success,
|
|
150
|
+
// Technical error of a FAILED step: the backend renders it in the
|
|
151
|
+
// RECOVERY block so the model can diagnose and change path.
|
|
152
|
+
...(request.lastStep.error ? { error: request.lastStep.error } : {}),
|
|
150
153
|
// Bounded result payload of the executed step (product search
|
|
151
154
|
// results…): the backend renders it so the model can read real
|
|
152
155
|
// ids for the NEXT step's parameters.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Bound on the technical error string shipped with action_failed events. */
|
|
2
|
+
const MAX_ERROR_DETAIL_CHARS = 200;
|
|
1
3
|
function mapEvent(params) {
|
|
2
4
|
const intent = params.intentDetected ?? null;
|
|
3
5
|
const sessionId = params.sessionId ?? null;
|
|
@@ -13,6 +15,19 @@ function mapEvent(params) {
|
|
|
13
15
|
}
|
|
14
16
|
return events;
|
|
15
17
|
}
|
|
18
|
+
// Failures are the events that make field reports diagnosable: without
|
|
19
|
+
// them, "the orb said it couldn't do it" leaves the developer blind. Only
|
|
20
|
+
// the technical error string travels (HTTP status + path, template
|
|
21
|
+
// param…), never any message content.
|
|
22
|
+
case 'action_failed':
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
type: 'action_failed',
|
|
26
|
+
intent,
|
|
27
|
+
session_id: sessionId,
|
|
28
|
+
error: (params.errorDetail ?? '').slice(0, MAX_ERROR_DETAIL_CHARS) || null,
|
|
29
|
+
},
|
|
30
|
+
];
|
|
16
31
|
case 'action_confirmation_requested':
|
|
17
32
|
return [{ type: 'confirmation_requested', intent, session_id: sessionId }];
|
|
18
33
|
default:
|
|
@@ -25,6 +25,13 @@ import { createSpeechSession } from '../speech/speechSession.js';
|
|
|
25
25
|
import { classifyPendingReply, decideAction, } from '../core/turnEngine.js';
|
|
26
26
|
import { DEFAULT_AGENT_TIMINGS, DEFAULT_AGENT_CHAINING, } from '../config/types.js';
|
|
27
27
|
import { resolveUiMessages } from '../ui/messages.js';
|
|
28
|
+
/**
|
|
29
|
+
* How many FAILED-step recovery turns one chain may consume. A failed action
|
|
30
|
+
* (invented handle → 404, wrong id kind → 422, transient 5xx) no longer kills
|
|
31
|
+
* the task: the model gets the technical error back and picks a different
|
|
32
|
+
* path — bounded so a persistently failing action can never loop.
|
|
33
|
+
*/
|
|
34
|
+
const MAX_CHAIN_RECOVERIES = 2;
|
|
28
35
|
function randomSessionId() {
|
|
29
36
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
30
37
|
}
|
|
@@ -491,7 +498,11 @@ export class AgentController {
|
|
|
491
498
|
intentDetected: pending.intent,
|
|
492
499
|
sessionId: this.sessionId,
|
|
493
500
|
actionExecuted: { intent: pending.intent, confirmed: true },
|
|
501
|
+
...(!result.success ? { errorDetail: result.error ?? null } : {}),
|
|
494
502
|
});
|
|
503
|
+
if (!result.success) {
|
|
504
|
+
console.warn(`[fyodos] action "${pending.intent}" failed: ${result.error ?? 'unknown error'}`);
|
|
505
|
+
}
|
|
495
506
|
if (result.message) {
|
|
496
507
|
if (this.lastExchange)
|
|
497
508
|
this.lastExchange = { ...this.lastExchange, replyText: result.message };
|
|
@@ -518,6 +529,28 @@ export class AgentController {
|
|
|
518
529
|
step: chainState.step,
|
|
519
530
|
lastStep: chainState.pendingStep ?? chainState.lastStep,
|
|
520
531
|
visitedKeys: chainState.visitedKeys,
|
|
532
|
+
recoveries: chainState.recoveries,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
else if (!result.success && this.chaining.enabled) {
|
|
536
|
+
// The CONFIRMED action failed: same recovery contract as any other
|
|
537
|
+
// failed step — the model reads the technical error and takes a
|
|
538
|
+
// different path (bounded by the recovery budget). The user's
|
|
539
|
+
// confirmation covered THIS intent; a recovery that re-attempts it
|
|
540
|
+
// stages a NEW confirmation, so the human gate is never bypassed.
|
|
541
|
+
const failedStep = {
|
|
542
|
+
type: 'execute',
|
|
543
|
+
intent: pending.intent,
|
|
544
|
+
label: this.actionLabel(pending.intent),
|
|
545
|
+
success: false,
|
|
546
|
+
...(result.error ? { error: String(result.error).slice(0, 300) } : {}),
|
|
547
|
+
};
|
|
548
|
+
await this.runChainLoop({
|
|
549
|
+
userMessage: chainState.userMessage,
|
|
550
|
+
step: chainState.step,
|
|
551
|
+
lastStep: failedStep,
|
|
552
|
+
visitedKeys: chainState.visitedKeys,
|
|
553
|
+
recoveries: (chainState.recoveries ?? 0) + 1,
|
|
521
554
|
});
|
|
522
555
|
}
|
|
523
556
|
else if (!result.success) {
|
|
@@ -685,6 +718,7 @@ export class AgentController {
|
|
|
685
718
|
let pendingInProgress = false;
|
|
686
719
|
let pendingStep;
|
|
687
720
|
let pendingKey;
|
|
721
|
+
let stepFailed = false;
|
|
688
722
|
if (decision.kind === 'executed') {
|
|
689
723
|
const r = decision.result;
|
|
690
724
|
// REAL activity for the panel: this action actually just ran — the
|
|
@@ -719,6 +753,7 @@ export class AgentController {
|
|
|
719
753
|
intentDetected: turn.action.intent,
|
|
720
754
|
sessionId: this.sessionId,
|
|
721
755
|
actionExecuted: { intent: turn.action.intent, confirmed: false },
|
|
756
|
+
...(!r.success && !r.recoverable ? { errorDetail: r.error ?? null } : {}),
|
|
722
757
|
});
|
|
723
758
|
}
|
|
724
759
|
if (r.recoverable) {
|
|
@@ -741,6 +776,30 @@ export class AgentController {
|
|
|
741
776
|
outReply = this.messages.actionFailed;
|
|
742
777
|
outAudio = null;
|
|
743
778
|
}
|
|
779
|
+
if (!r.success) {
|
|
780
|
+
// Developer signal: the user only hears a generic failure phrase; the
|
|
781
|
+
// TECHNICAL cause (HTTP status + path, missing template param…) must
|
|
782
|
+
// be findable in the console or every field report is undebuggable.
|
|
783
|
+
console.warn(`[fyodos] action "${turn.action?.intent ?? '?'}" failed: ${r.error ?? 'unknown error'}`);
|
|
784
|
+
}
|
|
785
|
+
// RECOVERY signal: the step RAN and FAILED (not a recoverable guidance
|
|
786
|
+
// case). Feed the technical error back as a failed lastStep so the model
|
|
787
|
+
// can diagnose (invented handle → 404, wrong id kind → 422…) and take a
|
|
788
|
+
// DIFFERENT path, exactly like a human agent. The chain loop bounds how
|
|
789
|
+
// many of these turns run (MAX_CHAIN_RECOVERIES); task_status is
|
|
790
|
+
// irrelevant here — the model believed the step would succeed.
|
|
791
|
+
if (turn.action?.type === 'execute' && !r.success && !r.recoverable) {
|
|
792
|
+
const intent = turn.action.intent ?? '';
|
|
793
|
+
lastStep = {
|
|
794
|
+
type: 'execute',
|
|
795
|
+
intent,
|
|
796
|
+
label: this.actionLabel(intent),
|
|
797
|
+
success: false,
|
|
798
|
+
...(r.error ? { error: String(r.error).slice(0, 300) } : {}),
|
|
799
|
+
};
|
|
800
|
+
stepFailed = true;
|
|
801
|
+
canContinue = this.chaining.enabled;
|
|
802
|
+
}
|
|
744
803
|
// Chain signal: only a cleanly EXECUTED, non-recoverable step may advance,
|
|
745
804
|
// and only when the backend asked to continue (task_status=in_progress).
|
|
746
805
|
if (turn.action &&
|
|
@@ -816,6 +875,7 @@ export class AgentController {
|
|
|
816
875
|
pendingInProgress,
|
|
817
876
|
pendingStep,
|
|
818
877
|
pendingKey,
|
|
878
|
+
stepFailed,
|
|
819
879
|
};
|
|
820
880
|
}
|
|
821
881
|
/**
|
|
@@ -826,6 +886,7 @@ export class AgentController {
|
|
|
826
886
|
async runChainLoop(state) {
|
|
827
887
|
let step = state.step;
|
|
828
888
|
let lastStep = state.lastStep;
|
|
889
|
+
let recoveries = state.recoveries ?? 0;
|
|
829
890
|
const visitedKeys = state.visitedKeys;
|
|
830
891
|
// Keep the panel's live row visible for the WHOLE chain (including the
|
|
831
892
|
// dwell between steps), so the real narration never flickers away.
|
|
@@ -922,6 +983,7 @@ export class AgentController {
|
|
|
922
983
|
visitedKeys,
|
|
923
984
|
pendingKey: oc.pendingKey,
|
|
924
985
|
pendingStep: oc.pendingStep,
|
|
986
|
+
recoveries,
|
|
925
987
|
}
|
|
926
988
|
: null;
|
|
927
989
|
this.beginConfirmationVoiceWindow();
|
|
@@ -929,6 +991,21 @@ export class AgentController {
|
|
|
929
991
|
}
|
|
930
992
|
if (this.phase !== 'speaking')
|
|
931
993
|
this.setPhase('idle');
|
|
994
|
+
// Failed step: consume one recovery slot. The budget is what keeps a
|
|
995
|
+
// persistently failing action from looping; a chain that exhausts it
|
|
996
|
+
// ends with the (honest) failure reply already on screen.
|
|
997
|
+
if (oc.stepFailed) {
|
|
998
|
+
recoveries += 1;
|
|
999
|
+
if (recoveries > MAX_CHAIN_RECOVERIES) {
|
|
1000
|
+
this.telemetry.logEvent({
|
|
1001
|
+
eventType: 'agent_chain_aborted',
|
|
1002
|
+
sessionId: this.sessionId,
|
|
1003
|
+
chainStep: step,
|
|
1004
|
+
chainAbortReason: 'no_progress',
|
|
1005
|
+
});
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
932
1009
|
if (oc.actionKey && visitedKeys.has(oc.actionKey)) {
|
|
933
1010
|
this.telemetry.logEvent({
|
|
934
1011
|
eventType: 'agent_chain_aborted',
|
|
@@ -1111,7 +1188,8 @@ export class AgentController {
|
|
|
1111
1188
|
this.setPhase('idle');
|
|
1112
1189
|
}
|
|
1113
1190
|
// Kick off the autonomous chain when the first step executed cleanly and
|
|
1114
|
-
// the backend asked to continue (no sensitive confirmation pending)
|
|
1191
|
+
// the backend asked to continue (no sensitive confirmation pending) — or
|
|
1192
|
+
// when the first step FAILED and a recovery turn should diagnose it.
|
|
1115
1193
|
if (this.chaining.enabled && outcome.canContinue && !hasPending) {
|
|
1116
1194
|
this.telemetry.logEvent({
|
|
1117
1195
|
eventType: 'agent_chain_started',
|
|
@@ -1126,6 +1204,7 @@ export class AgentController {
|
|
|
1126
1204
|
step: 1,
|
|
1127
1205
|
lastStep: outcome.lastStep,
|
|
1128
1206
|
visitedKeys,
|
|
1207
|
+
recoveries: outcome.stepFailed ? 1 : 0,
|
|
1129
1208
|
});
|
|
1130
1209
|
}
|
|
1131
1210
|
// Final reply is on screen: the live narrative disappears (unless the
|
|
@@ -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.21";
|
|
31
31
|
readonly createFiodosAgent: typeof createFiodosAgent;
|
|
32
32
|
readonly mountOrb: typeof mountOrb;
|
|
33
33
|
readonly AgentController: typeof AgentController;
|
package/dist/esm/orb/mountOrb.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - CONFIRMATIONS: the same security model (manifest decides; strict phrase /
|
|
17
17
|
* deliberate tap for payment-grade; loose "yes" never resolves strict).
|
|
18
18
|
*/
|
|
19
|
-
import { DEFAULT_ORB_SCREEN_POSITION, createWebOrbHaptics, matchRouteIntent, resolveBubblePrompt,
|
|
19
|
+
import { DEFAULT_ORB_SCREEN_POSITION, createWebOrbHaptics, matchRouteIntent, resolveBubblePrompt, resolveBubbleTheme, resolveBubblePadding, bubbleShapeToCss, applyBubbleShapeCss, resolveOrbAnchorPx, resolveOrbDeployment, snapOrbScreenPositionToSide, viewportPixelsToOrbScreenPosition, fiodosInputInkColor, fiodosInputMutedInkColor, shouldHideInternalOrb, } from '@fiodos/core';
|
|
20
20
|
import { buildKeyboardChip, createOrbVisual, DEFAULT_ORB_APPEARANCE, } from './orbView.js';
|
|
21
21
|
import { watchPublishedConfig } from './publishedConfig.js';
|
|
22
22
|
import { createBubbleDictation, scrollTextInputToEnd } from '../speech/bubbleDictation.js';
|
|
@@ -54,6 +54,8 @@ const CSS = `
|
|
|
54
54
|
.fyodos-bubble-exchange:first-child .fyodos-bubble-user{padding-right:22px;}
|
|
55
55
|
.fyodos-bubble-user{flex:1;min-width:0;font-size:13px;line-height:18px;opacity:.75;}
|
|
56
56
|
.fyodos-bubble-reply{font-size:15px;line-height:21px;white-space:pre-wrap;}
|
|
57
|
+
.fyodos-bubble-greeting{font-size:15px;line-height:21px;white-space:pre-wrap;animation:fyodos-fade-in .18s ease-out;}
|
|
58
|
+
.fyodos-bubble--expanded .fyodos-bubble-greeting{font-size:17px;line-height:25px;}
|
|
57
59
|
.fyodos-bubble-busy{display:flex;gap:5px;padding:8px 14px;align-items:center;}
|
|
58
60
|
.fyodos-bubble-busy span{width:6px;height:6px;border-radius:50%;opacity:.5;animation:fyodos-blink 1s infinite;}
|
|
59
61
|
.fyodos-bubble-busy span:nth-child(2){animation-delay:.18s;}
|
|
@@ -207,13 +209,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
207
209
|
haptics.trigger('orbTap');
|
|
208
210
|
controller.primeAudio();
|
|
209
211
|
if (enableTextInput) {
|
|
210
|
-
|
|
211
|
-
openTextPanel
|
|
212
|
-
|
|
213
|
-
locale: currentBubbleLocale(),
|
|
214
|
-
messages: controller.messages,
|
|
215
|
-
})
|
|
216
|
-
: undefined);
|
|
212
|
+
// The bubble's question moves INTO the panel as an assistant greeting
|
|
213
|
+
// (openTextPanel shows it) — never pre-typed into the user's input.
|
|
214
|
+
openTextPanel();
|
|
217
215
|
return;
|
|
218
216
|
}
|
|
219
217
|
userTriedVoice = true;
|
|
@@ -455,7 +453,30 @@ export function mountOrb(controller, options = {}) {
|
|
|
455
453
|
loaderRow.style.display = 'none';
|
|
456
454
|
const exchangeList = document.createElement('div');
|
|
457
455
|
exchangeList.className = 'fyodos-bubble-exchanges';
|
|
458
|
-
|
|
456
|
+
// Screen-aware GREETING: the thought bubble's question, shown INSIDE the
|
|
457
|
+
// panel as an assistant message when it opens (tapping the bubble or opening
|
|
458
|
+
// the chat traditionally). Ephemeral by design: it never enters the
|
|
459
|
+
// conversation history and disappears the moment the user sends a message.
|
|
460
|
+
const greetingEl = document.createElement('div');
|
|
461
|
+
greetingEl.className = 'fyodos-bubble-greeting';
|
|
462
|
+
greetingEl.style.display = 'none';
|
|
463
|
+
exchangeScroll.append(loaderRow, exchangeList, greetingEl);
|
|
464
|
+
function showPanelGreeting() {
|
|
465
|
+
const prompt = resolveCurrentBubblePrompt();
|
|
466
|
+
if (!prompt) {
|
|
467
|
+
greetingEl.style.display = 'none';
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const t = { ...DEFAULT_MODAL_THEME, ...(modalTheme ?? {}) };
|
|
471
|
+
greetingEl.textContent = prompt;
|
|
472
|
+
greetingEl.style.color = t.titleColor;
|
|
473
|
+
greetingEl.style.display = '';
|
|
474
|
+
exchangeScroll.scrollTop = exchangeScroll.scrollHeight;
|
|
475
|
+
}
|
|
476
|
+
function hidePanelGreeting() {
|
|
477
|
+
if (greetingEl.style.display !== 'none')
|
|
478
|
+
greetingEl.style.display = 'none';
|
|
479
|
+
}
|
|
459
480
|
// Signature of the rendered list so state emits that do not change the
|
|
460
481
|
// transcript (volume, phase…) never re-render it nor fight the user's scroll.
|
|
461
482
|
let renderedSignature = '';
|
|
@@ -749,6 +770,8 @@ export function mountOrb(controller, options = {}) {
|
|
|
749
770
|
else {
|
|
750
771
|
bubbleDictation.stop();
|
|
751
772
|
syncMicButton();
|
|
773
|
+
// Ephemeral greeting never survives a close (recomputed on next open).
|
|
774
|
+
hidePanelGreeting();
|
|
752
775
|
// Collapse the paged history back to the initial page for the next open.
|
|
753
776
|
controller.resetBubbleWindow();
|
|
754
777
|
// Closing the panel returns the orb to idle (also clears mic fallback).
|
|
@@ -778,6 +801,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
778
801
|
syncMicButton();
|
|
779
802
|
haptics.trigger('send');
|
|
780
803
|
textInput.value = '';
|
|
804
|
+
// The greeting is a welcome, not part of the conversation: the user's
|
|
805
|
+
// first real message replaces it.
|
|
806
|
+
hidePanelGreeting();
|
|
781
807
|
// Silent turn: the reply is SHOWN in the bubble (not spoken), and the bubble
|
|
782
808
|
// STAYS OPEN. This is the unified, reference-matching behaviour.
|
|
783
809
|
void controller.submitTextTurn(value);
|
|
@@ -790,6 +816,11 @@ export function mountOrb(controller, options = {}) {
|
|
|
790
816
|
const draft = explicitSeed || cancelled || '';
|
|
791
817
|
textInput.value = draft;
|
|
792
818
|
setBubble(true);
|
|
819
|
+
// Welcome the user with this screen's bubble question as an assistant
|
|
820
|
+
// message (both open paths: bubble tap AND traditional open). It vanishes
|
|
821
|
+
// with the user's first message — never enters the history.
|
|
822
|
+
if (!draft)
|
|
823
|
+
showPanelGreeting();
|
|
793
824
|
// Only an explicit seed may auto-send; a restored cancelled message must
|
|
794
825
|
// wait for the user to edit/resend it.
|
|
795
826
|
if (autoSend && explicitSeed) {
|
|
@@ -1153,6 +1184,9 @@ export function mountOrb(controller, options = {}) {
|
|
|
1153
1184
|
syncActivityStep(rowBusy, state);
|
|
1154
1185
|
// Text bubble: show recent exchanges + busy state.
|
|
1155
1186
|
if (bubbleOpen) {
|
|
1187
|
+
// A turn in flight (typed OR voice) replaces the ephemeral greeting.
|
|
1188
|
+
if (state.isBusy)
|
|
1189
|
+
hidePanelGreeting();
|
|
1156
1190
|
loaderRow.style.display = state.loadingOlderExchanges ? 'flex' : 'none';
|
|
1157
1191
|
renderBubbleExchanges(state.recentExchanges);
|
|
1158
1192
|
exchangeScroll.style.display = 'flex';
|
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.21";
|
|
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.21';
|
|
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.21",
|
|
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": {
|