@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
|
@@ -151,6 +151,9 @@ function createFiodosBackendClient(options) {
|
|
|
151
151
|
intent: request.lastStep.intent,
|
|
152
152
|
...(request.lastStep.label ? { label: request.lastStep.label } : {}),
|
|
153
153
|
success: request.lastStep.success,
|
|
154
|
+
// Technical error of a FAILED step: the backend renders it in the
|
|
155
|
+
// RECOVERY block so the model can diagnose and change path.
|
|
156
|
+
...(request.lastStep.error ? { error: request.lastStep.error } : {}),
|
|
154
157
|
// Bounded result payload of the executed step (product search
|
|
155
158
|
// results…): the backend renders it so the model can read real
|
|
156
159
|
// ids for the NEXT step's parameters.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createFiodosTelemetry = createFiodosTelemetry;
|
|
4
|
+
/** Bound on the technical error string shipped with action_failed events. */
|
|
5
|
+
const MAX_ERROR_DETAIL_CHARS = 200;
|
|
4
6
|
function mapEvent(params) {
|
|
5
7
|
const intent = params.intentDetected ?? null;
|
|
6
8
|
const sessionId = params.sessionId ?? null;
|
|
@@ -16,6 +18,19 @@ function mapEvent(params) {
|
|
|
16
18
|
}
|
|
17
19
|
return events;
|
|
18
20
|
}
|
|
21
|
+
// Failures are the events that make field reports diagnosable: without
|
|
22
|
+
// them, "the orb said it couldn't do it" leaves the developer blind. Only
|
|
23
|
+
// the technical error string travels (HTTP status + path, template
|
|
24
|
+
// param…), never any message content.
|
|
25
|
+
case 'action_failed':
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
type: 'action_failed',
|
|
29
|
+
intent,
|
|
30
|
+
session_id: sessionId,
|
|
31
|
+
error: (params.errorDetail ?? '').slice(0, MAX_ERROR_DETAIL_CHARS) || null,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
19
34
|
case 'action_confirmation_requested':
|
|
20
35
|
return [{ type: 'confirmation_requested', intent, session_id: sessionId }];
|
|
21
36
|
default:
|
|
@@ -28,6 +28,13 @@ const speechSession_1 = require("../speech/speechSession");
|
|
|
28
28
|
const turnEngine_1 = require("../core/turnEngine");
|
|
29
29
|
const types_1 = require("../config/types");
|
|
30
30
|
const messages_1 = require("../ui/messages");
|
|
31
|
+
/**
|
|
32
|
+
* How many FAILED-step recovery turns one chain may consume. A failed action
|
|
33
|
+
* (invented handle → 404, wrong id kind → 422, transient 5xx) no longer kills
|
|
34
|
+
* the task: the model gets the technical error back and picks a different
|
|
35
|
+
* path — bounded so a persistently failing action can never loop.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_CHAIN_RECOVERIES = 2;
|
|
31
38
|
function randomSessionId() {
|
|
32
39
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
33
40
|
}
|
|
@@ -494,7 +501,11 @@ class AgentController {
|
|
|
494
501
|
intentDetected: pending.intent,
|
|
495
502
|
sessionId: this.sessionId,
|
|
496
503
|
actionExecuted: { intent: pending.intent, confirmed: true },
|
|
504
|
+
...(!result.success ? { errorDetail: result.error ?? null } : {}),
|
|
497
505
|
});
|
|
506
|
+
if (!result.success) {
|
|
507
|
+
console.warn(`[fyodos] action "${pending.intent}" failed: ${result.error ?? 'unknown error'}`);
|
|
508
|
+
}
|
|
498
509
|
if (result.message) {
|
|
499
510
|
if (this.lastExchange)
|
|
500
511
|
this.lastExchange = { ...this.lastExchange, replyText: result.message };
|
|
@@ -521,6 +532,28 @@ class AgentController {
|
|
|
521
532
|
step: chainState.step,
|
|
522
533
|
lastStep: chainState.pendingStep ?? chainState.lastStep,
|
|
523
534
|
visitedKeys: chainState.visitedKeys,
|
|
535
|
+
recoveries: chainState.recoveries,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
else if (!result.success && this.chaining.enabled) {
|
|
539
|
+
// The CONFIRMED action failed: same recovery contract as any other
|
|
540
|
+
// failed step — the model reads the technical error and takes a
|
|
541
|
+
// different path (bounded by the recovery budget). The user's
|
|
542
|
+
// confirmation covered THIS intent; a recovery that re-attempts it
|
|
543
|
+
// stages a NEW confirmation, so the human gate is never bypassed.
|
|
544
|
+
const failedStep = {
|
|
545
|
+
type: 'execute',
|
|
546
|
+
intent: pending.intent,
|
|
547
|
+
label: this.actionLabel(pending.intent),
|
|
548
|
+
success: false,
|
|
549
|
+
...(result.error ? { error: String(result.error).slice(0, 300) } : {}),
|
|
550
|
+
};
|
|
551
|
+
await this.runChainLoop({
|
|
552
|
+
userMessage: chainState.userMessage,
|
|
553
|
+
step: chainState.step,
|
|
554
|
+
lastStep: failedStep,
|
|
555
|
+
visitedKeys: chainState.visitedKeys,
|
|
556
|
+
recoveries: (chainState.recoveries ?? 0) + 1,
|
|
524
557
|
});
|
|
525
558
|
}
|
|
526
559
|
else if (!result.success) {
|
|
@@ -688,6 +721,7 @@ class AgentController {
|
|
|
688
721
|
let pendingInProgress = false;
|
|
689
722
|
let pendingStep;
|
|
690
723
|
let pendingKey;
|
|
724
|
+
let stepFailed = false;
|
|
691
725
|
if (decision.kind === 'executed') {
|
|
692
726
|
const r = decision.result;
|
|
693
727
|
// REAL activity for the panel: this action actually just ran — the
|
|
@@ -722,6 +756,7 @@ class AgentController {
|
|
|
722
756
|
intentDetected: turn.action.intent,
|
|
723
757
|
sessionId: this.sessionId,
|
|
724
758
|
actionExecuted: { intent: turn.action.intent, confirmed: false },
|
|
759
|
+
...(!r.success && !r.recoverable ? { errorDetail: r.error ?? null } : {}),
|
|
725
760
|
});
|
|
726
761
|
}
|
|
727
762
|
if (r.recoverable) {
|
|
@@ -744,6 +779,30 @@ class AgentController {
|
|
|
744
779
|
outReply = this.messages.actionFailed;
|
|
745
780
|
outAudio = null;
|
|
746
781
|
}
|
|
782
|
+
if (!r.success) {
|
|
783
|
+
// Developer signal: the user only hears a generic failure phrase; the
|
|
784
|
+
// TECHNICAL cause (HTTP status + path, missing template param…) must
|
|
785
|
+
// be findable in the console or every field report is undebuggable.
|
|
786
|
+
console.warn(`[fyodos] action "${turn.action?.intent ?? '?'}" failed: ${r.error ?? 'unknown error'}`);
|
|
787
|
+
}
|
|
788
|
+
// RECOVERY signal: the step RAN and FAILED (not a recoverable guidance
|
|
789
|
+
// case). Feed the technical error back as a failed lastStep so the model
|
|
790
|
+
// can diagnose (invented handle → 404, wrong id kind → 422…) and take a
|
|
791
|
+
// DIFFERENT path, exactly like a human agent. The chain loop bounds how
|
|
792
|
+
// many of these turns run (MAX_CHAIN_RECOVERIES); task_status is
|
|
793
|
+
// irrelevant here — the model believed the step would succeed.
|
|
794
|
+
if (turn.action?.type === 'execute' && !r.success && !r.recoverable) {
|
|
795
|
+
const intent = turn.action.intent ?? '';
|
|
796
|
+
lastStep = {
|
|
797
|
+
type: 'execute',
|
|
798
|
+
intent,
|
|
799
|
+
label: this.actionLabel(intent),
|
|
800
|
+
success: false,
|
|
801
|
+
...(r.error ? { error: String(r.error).slice(0, 300) } : {}),
|
|
802
|
+
};
|
|
803
|
+
stepFailed = true;
|
|
804
|
+
canContinue = this.chaining.enabled;
|
|
805
|
+
}
|
|
747
806
|
// Chain signal: only a cleanly EXECUTED, non-recoverable step may advance,
|
|
748
807
|
// and only when the backend asked to continue (task_status=in_progress).
|
|
749
808
|
if (turn.action &&
|
|
@@ -819,6 +878,7 @@ class AgentController {
|
|
|
819
878
|
pendingInProgress,
|
|
820
879
|
pendingStep,
|
|
821
880
|
pendingKey,
|
|
881
|
+
stepFailed,
|
|
822
882
|
};
|
|
823
883
|
}
|
|
824
884
|
/**
|
|
@@ -829,6 +889,7 @@ class AgentController {
|
|
|
829
889
|
async runChainLoop(state) {
|
|
830
890
|
let step = state.step;
|
|
831
891
|
let lastStep = state.lastStep;
|
|
892
|
+
let recoveries = state.recoveries ?? 0;
|
|
832
893
|
const visitedKeys = state.visitedKeys;
|
|
833
894
|
// Keep the panel's live row visible for the WHOLE chain (including the
|
|
834
895
|
// dwell between steps), so the real narration never flickers away.
|
|
@@ -925,6 +986,7 @@ class AgentController {
|
|
|
925
986
|
visitedKeys,
|
|
926
987
|
pendingKey: oc.pendingKey,
|
|
927
988
|
pendingStep: oc.pendingStep,
|
|
989
|
+
recoveries,
|
|
928
990
|
}
|
|
929
991
|
: null;
|
|
930
992
|
this.beginConfirmationVoiceWindow();
|
|
@@ -932,6 +994,21 @@ class AgentController {
|
|
|
932
994
|
}
|
|
933
995
|
if (this.phase !== 'speaking')
|
|
934
996
|
this.setPhase('idle');
|
|
997
|
+
// Failed step: consume one recovery slot. The budget is what keeps a
|
|
998
|
+
// persistently failing action from looping; a chain that exhausts it
|
|
999
|
+
// ends with the (honest) failure reply already on screen.
|
|
1000
|
+
if (oc.stepFailed) {
|
|
1001
|
+
recoveries += 1;
|
|
1002
|
+
if (recoveries > MAX_CHAIN_RECOVERIES) {
|
|
1003
|
+
this.telemetry.logEvent({
|
|
1004
|
+
eventType: 'agent_chain_aborted',
|
|
1005
|
+
sessionId: this.sessionId,
|
|
1006
|
+
chainStep: step,
|
|
1007
|
+
chainAbortReason: 'no_progress',
|
|
1008
|
+
});
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
935
1012
|
if (oc.actionKey && visitedKeys.has(oc.actionKey)) {
|
|
936
1013
|
this.telemetry.logEvent({
|
|
937
1014
|
eventType: 'agent_chain_aborted',
|
|
@@ -1114,7 +1191,8 @@ class AgentController {
|
|
|
1114
1191
|
this.setPhase('idle');
|
|
1115
1192
|
}
|
|
1116
1193
|
// Kick off the autonomous chain when the first step executed cleanly and
|
|
1117
|
-
// the backend asked to continue (no sensitive confirmation pending)
|
|
1194
|
+
// the backend asked to continue (no sensitive confirmation pending) — or
|
|
1195
|
+
// when the first step FAILED and a recovery turn should diagnose it.
|
|
1118
1196
|
if (this.chaining.enabled && outcome.canContinue && !hasPending) {
|
|
1119
1197
|
this.telemetry.logEvent({
|
|
1120
1198
|
eventType: 'agent_chain_started',
|
|
@@ -1129,6 +1207,7 @@ class AgentController {
|
|
|
1129
1207
|
step: 1,
|
|
1130
1208
|
lastStep: outcome.lastStep,
|
|
1131
1209
|
visitedKeys,
|
|
1210
|
+
recoveries: outcome.stepFailed ? 1 : 0,
|
|
1132
1211
|
});
|
|
1133
1212
|
}
|
|
1134
1213
|
// Final reply is on screen: the live narrative disappears (unless the
|
|
@@ -27,7 +27,7 @@ import { createScreenContextStore } from '../context/screenContextStore';
|
|
|
27
27
|
import { connectDynamicsFormContext, dynamicsFormSnapshot, normalizeDynamicsId } from './dynamics';
|
|
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/cjs/orb/mountOrb.js
CHANGED
|
@@ -57,6 +57,8 @@ const CSS = `
|
|
|
57
57
|
.fyodos-bubble-exchange:first-child .fyodos-bubble-user{padding-right:22px;}
|
|
58
58
|
.fyodos-bubble-user{flex:1;min-width:0;font-size:13px;line-height:18px;opacity:.75;}
|
|
59
59
|
.fyodos-bubble-reply{font-size:15px;line-height:21px;white-space:pre-wrap;}
|
|
60
|
+
.fyodos-bubble-greeting{font-size:15px;line-height:21px;white-space:pre-wrap;animation:fyodos-fade-in .18s ease-out;}
|
|
61
|
+
.fyodos-bubble--expanded .fyodos-bubble-greeting{font-size:17px;line-height:25px;}
|
|
60
62
|
.fyodos-bubble-busy{display:flex;gap:5px;padding:8px 14px;align-items:center;}
|
|
61
63
|
.fyodos-bubble-busy span{width:6px;height:6px;border-radius:50%;opacity:.5;animation:fyodos-blink 1s infinite;}
|
|
62
64
|
.fyodos-bubble-busy span:nth-child(2){animation-delay:.18s;}
|
|
@@ -210,13 +212,9 @@ function mountOrb(controller, options = {}) {
|
|
|
210
212
|
haptics.trigger('orbTap');
|
|
211
213
|
controller.primeAudio();
|
|
212
214
|
if (enableTextInput) {
|
|
213
|
-
|
|
214
|
-
openTextPanel
|
|
215
|
-
|
|
216
|
-
locale: currentBubbleLocale(),
|
|
217
|
-
messages: controller.messages,
|
|
218
|
-
})
|
|
219
|
-
: undefined);
|
|
215
|
+
// The bubble's question moves INTO the panel as an assistant greeting
|
|
216
|
+
// (openTextPanel shows it) — never pre-typed into the user's input.
|
|
217
|
+
openTextPanel();
|
|
220
218
|
return;
|
|
221
219
|
}
|
|
222
220
|
userTriedVoice = true;
|
|
@@ -458,7 +456,30 @@ function mountOrb(controller, options = {}) {
|
|
|
458
456
|
loaderRow.style.display = 'none';
|
|
459
457
|
const exchangeList = document.createElement('div');
|
|
460
458
|
exchangeList.className = 'fyodos-bubble-exchanges';
|
|
461
|
-
|
|
459
|
+
// Screen-aware GREETING: the thought bubble's question, shown INSIDE the
|
|
460
|
+
// panel as an assistant message when it opens (tapping the bubble or opening
|
|
461
|
+
// the chat traditionally). Ephemeral by design: it never enters the
|
|
462
|
+
// conversation history and disappears the moment the user sends a message.
|
|
463
|
+
const greetingEl = document.createElement('div');
|
|
464
|
+
greetingEl.className = 'fyodos-bubble-greeting';
|
|
465
|
+
greetingEl.style.display = 'none';
|
|
466
|
+
exchangeScroll.append(loaderRow, exchangeList, greetingEl);
|
|
467
|
+
function showPanelGreeting() {
|
|
468
|
+
const prompt = resolveCurrentBubblePrompt();
|
|
469
|
+
if (!prompt) {
|
|
470
|
+
greetingEl.style.display = 'none';
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const t = { ...DEFAULT_MODAL_THEME, ...(modalTheme ?? {}) };
|
|
474
|
+
greetingEl.textContent = prompt;
|
|
475
|
+
greetingEl.style.color = t.titleColor;
|
|
476
|
+
greetingEl.style.display = '';
|
|
477
|
+
exchangeScroll.scrollTop = exchangeScroll.scrollHeight;
|
|
478
|
+
}
|
|
479
|
+
function hidePanelGreeting() {
|
|
480
|
+
if (greetingEl.style.display !== 'none')
|
|
481
|
+
greetingEl.style.display = 'none';
|
|
482
|
+
}
|
|
462
483
|
// Signature of the rendered list so state emits that do not change the
|
|
463
484
|
// transcript (volume, phase…) never re-render it nor fight the user's scroll.
|
|
464
485
|
let renderedSignature = '';
|
|
@@ -752,6 +773,8 @@ function mountOrb(controller, options = {}) {
|
|
|
752
773
|
else {
|
|
753
774
|
bubbleDictation.stop();
|
|
754
775
|
syncMicButton();
|
|
776
|
+
// Ephemeral greeting never survives a close (recomputed on next open).
|
|
777
|
+
hidePanelGreeting();
|
|
755
778
|
// Collapse the paged history back to the initial page for the next open.
|
|
756
779
|
controller.resetBubbleWindow();
|
|
757
780
|
// Closing the panel returns the orb to idle (also clears mic fallback).
|
|
@@ -781,6 +804,9 @@ function mountOrb(controller, options = {}) {
|
|
|
781
804
|
syncMicButton();
|
|
782
805
|
haptics.trigger('send');
|
|
783
806
|
textInput.value = '';
|
|
807
|
+
// The greeting is a welcome, not part of the conversation: the user's
|
|
808
|
+
// first real message replaces it.
|
|
809
|
+
hidePanelGreeting();
|
|
784
810
|
// Silent turn: the reply is SHOWN in the bubble (not spoken), and the bubble
|
|
785
811
|
// STAYS OPEN. This is the unified, reference-matching behaviour.
|
|
786
812
|
void controller.submitTextTurn(value);
|
|
@@ -793,6 +819,11 @@ function mountOrb(controller, options = {}) {
|
|
|
793
819
|
const draft = explicitSeed || cancelled || '';
|
|
794
820
|
textInput.value = draft;
|
|
795
821
|
setBubble(true);
|
|
822
|
+
// Welcome the user with this screen's bubble question as an assistant
|
|
823
|
+
// message (both open paths: bubble tap AND traditional open). It vanishes
|
|
824
|
+
// with the user's first message — never enters the history.
|
|
825
|
+
if (!draft)
|
|
826
|
+
showPanelGreeting();
|
|
796
827
|
// Only an explicit seed may auto-send; a restored cancelled message must
|
|
797
828
|
// wait for the user to edit/resend it.
|
|
798
829
|
if (autoSend && explicitSeed) {
|
|
@@ -1156,6 +1187,9 @@ function mountOrb(controller, options = {}) {
|
|
|
1156
1187
|
syncActivityStep(rowBusy, state);
|
|
1157
1188
|
// Text bubble: show recent exchanges + busy state.
|
|
1158
1189
|
if (bubbleOpen) {
|
|
1190
|
+
// A turn in flight (typed OR voice) replaces the ephemeral greeting.
|
|
1191
|
+
if (state.isBusy)
|
|
1192
|
+
hidePanelGreeting();
|
|
1159
1193
|
loaderRow.style.display = state.loadingOlderExchanges ? 'flex' : 'none';
|
|
1160
1194
|
renderBubbleExchanges(state.recentExchanges);
|
|
1161
1195
|
exchangeScroll.style.display = 'flex';
|
package/dist/cjs/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/cjs/version.js
CHANGED
|
@@ -8,5 +8,5 @@ exports.SDK_PACKAGE = exports.SDK_VERSION = void 0;
|
|
|
8
8
|
* Auto-generated by scripts/sync-sdk-version.mjs from package.json. Do not edit
|
|
9
9
|
* by hand — change package.json `version` and re-run the sync/release script.
|
|
10
10
|
*/
|
|
11
|
-
exports.SDK_VERSION = '0.1.
|
|
11
|
+
exports.SDK_VERSION = '0.1.21';
|
|
12
12
|
exports.SDK_PACKAGE = '@fiodos/web-core';
|