@genesislcap/ai-assistant 15.60.0 → 15.62.0
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/ai-assistant.api.json +18231 -11171
- package/dist/ai-assistant.d.ts +1235 -9
- package/dist/chat-driver.cjs +658 -50
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +657 -50
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +4419 -377
- package/dist/dts/autopilot/autopilot-driver.d.ts +278 -0
- package/dist/dts/autopilot/autopilot-driver.d.ts.map +1 -0
- package/dist/dts/autopilot/autopilot-session.d.ts +163 -0
- package/dist/dts/autopilot/autopilot-session.d.ts.map +1 -0
- package/dist/dts/autopilot/autopilot-tools.d.ts +15 -0
- package/dist/dts/autopilot/autopilot-tools.d.ts.map +1 -0
- package/dist/dts/autopilot/decision-message.d.ts +62 -0
- package/dist/dts/autopilot/decision-message.d.ts.map +1 -0
- package/dist/dts/autopilot/hand-back/hand-back.controller.d.ts +69 -0
- package/dist/dts/autopilot/hand-back/hand-back.controller.d.ts.map +1 -0
- package/dist/dts/autopilot/hand-back/hand-back.d.ts +22 -0
- package/dist/dts/autopilot/hand-back/hand-back.d.ts.map +1 -0
- package/dist/dts/autopilot/index.d.ts +4 -0
- package/dist/dts/autopilot/index.d.ts.map +1 -0
- package/dist/dts/channel/ai-activity-bus.d.ts.map +1 -1
- package/dist/dts/channel/ai-activity-channel.d.ts +17 -0
- package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
- package/dist/dts/channel/autopilot-needs-user.d.ts +8 -0
- package/dist/dts/channel/autopilot-needs-user.d.ts.map +1 -0
- package/dist/dts/components/agent-picker/agent-picker.constants.d.ts +10 -0
- package/dist/dts/components/agent-picker/agent-picker.constants.d.ts.map +1 -1
- package/dist/dts/components/agent-picker/agent-picker.d.ts +4 -0
- package/dist/dts/components/agent-picker/agent-picker.d.ts.map +1 -1
- package/dist/dts/components/agent-picker/agent-picker.template.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts +34 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +3 -1
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
- package/dist/dts/components/chat-bubble/chat-bubble.d.ts +21 -0
- package/dist/dts/components/chat-bubble/chat-bubble.d.ts.map +1 -1
- package/dist/dts/components/chat-bubble/chat-bubble.styles.d.ts.map +1 -1
- package/dist/dts/components/chat-bubble/chat-bubble.template.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +5 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/interaction-controller.d.ts +27 -3
- package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts +10 -2
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +9 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +183 -0
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/config/define-stateful-agent.d.ts +31 -1
- package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
- package/dist/dts/index.d.ts +7 -3
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +128 -5
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts +2 -2
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/react.d.ts +10 -0
- package/dist/dts/state/debug-event-log.d.ts +1 -1
- package/dist/dts/state/debug-event-log.d.ts.map +1 -1
- package/dist/dts/state/decision-surface.d.ts +46 -0
- package/dist/dts/state/decision-surface.d.ts.map +1 -0
- package/dist/dts/state/interaction-context.d.ts +21 -1
- package/dist/dts/state/interaction-context.d.ts.map +1 -1
- package/dist/dts/state/interaction-registry.d.ts +63 -0
- package/dist/dts/state/interaction-registry.d.ts.map +1 -0
- package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-snapshot.d.ts +21 -0
- package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
- package/dist/dts/state/run-steps.d.ts +66 -0
- package/dist/dts/state/run-steps.d.ts.map +1 -0
- package/dist/dts/types/ai-chat-widget.d.ts +7 -1
- package/dist/dts/types/ai-chat-widget.d.ts.map +1 -1
- package/dist/dts/types/interaction-context.d.ts +66 -1
- package/dist/dts/types/interaction-context.d.ts.map +1 -1
- package/dist/dts/types/interaction-interceptor.d.ts +60 -2
- package/dist/dts/types/interaction-interceptor.d.ts.map +1 -1
- package/dist/dts/types/interaction-surface.d.ts +195 -0
- package/dist/dts/types/interaction-surface.d.ts.map +1 -0
- package/dist/dts/utils/context-tokens.d.ts +6 -0
- package/dist/dts/utils/context-tokens.d.ts.map +1 -1
- package/dist/dts/utils/delegated-message.d.ts +13 -0
- package/dist/dts/utils/delegated-message.d.ts.map +1 -1
- package/dist/dts/utils/history-transform.d.ts.map +1 -1
- package/dist/dts/utils/preserved-user-messages.d.ts +21 -0
- package/dist/dts/utils/preserved-user-messages.d.ts.map +1 -0
- package/dist/esm/autopilot/autopilot-driver.js +906 -0
- package/dist/esm/autopilot/autopilot-session.js +449 -0
- package/dist/esm/autopilot/autopilot-tools.js +68 -0
- package/dist/esm/autopilot/decision-message.js +206 -0
- package/dist/esm/autopilot/hand-back/hand-back.controller.js +47 -0
- package/dist/esm/autopilot/hand-back/hand-back.js +129 -0
- package/dist/esm/autopilot/index.js +4 -0
- package/dist/esm/channel/ai-activity-bus.js +7 -0
- package/dist/esm/channel/autopilot-needs-user.js +26 -0
- package/dist/esm/components/agent-picker/agent-picker.constants.js +10 -0
- package/dist/esm/components/agent-picker/agent-picker.js +11 -1
- package/dist/esm/components/agent-picker/agent-picker.template.js +2 -4
- package/dist/esm/components/ai-driver/ai-driver.js +2 -0
- package/dist/esm/components/ai-driver/base-driver-decorator.js +8 -0
- package/dist/esm/components/chat-bubble/chat-bubble.js +45 -0
- package/dist/esm/components/chat-bubble/chat-bubble.styles.js +26 -0
- package/dist/esm/components/chat-bubble/chat-bubble.template.js +13 -0
- package/dist/esm/components/chat-driver/chat-driver.js +53 -18
- package/dist/esm/components/chat-driver/interaction-controller.js +297 -12
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.js +16 -2
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +28 -11
- package/dist/esm/config/define-stateful-agent.js +34 -29
- package/dist/esm/index.js +3 -0
- package/dist/esm/main/main.js +334 -15
- package/dist/esm/main/main.styles.js +126 -0
- package/dist/esm/main/main.template.js +116 -11
- package/dist/esm/state/debug-event-log.js +28 -1
- package/dist/esm/state/decision-surface.js +209 -0
- package/dist/esm/state/interaction-context.js +89 -1
- package/dist/esm/state/interaction-registry.js +33 -0
- package/dist/esm/state/persistence/session-persister.js +23 -11
- package/dist/esm/state/persistence/session-snapshot.js +1 -1
- package/dist/esm/state/run-steps.js +77 -0
- package/dist/esm/types/interaction-surface.js +1 -0
- package/dist/esm/utils/context-tokens.js +23 -9
- package/dist/esm/utils/delegated-message.js +15 -0
- package/dist/esm/utils/history-transform.js +18 -7
- package/dist/esm/utils/preserved-user-messages.js +31 -0
- package/dist/react.cjs +7 -0
- package/dist/react.mjs +6 -0
- package/docs/styling.md +1 -1
- package/package.json +17 -17
- package/sandbox/README.md +38 -2
- package/sandbox/autopilot.ts +358 -0
- package/sandbox/controls.ts +24 -1
- package/sandbox/fixtures.ts +2 -0
- package/sandbox/sandbox.ts +188 -1
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
import { __awaiter } from "tslib";
|
|
2
|
+
import { BaseDriverDecorator } from '../components/ai-driver/base-driver-decorator';
|
|
3
|
+
import { recordMetaEvent } from '../state/debug-event-log';
|
|
4
|
+
import { getInteractionRegistration, registerInteraction } from '../state/interaction-registry';
|
|
5
|
+
import { logger } from '../utils/logger';
|
|
6
|
+
import { AutopilotSession, positiveOr } from './autopilot-session';
|
|
7
|
+
import { AUTOPILOT_HAND_BACK, HandBackController, handBackAsText, } from './hand-back/hand-back.controller';
|
|
8
|
+
/**
|
|
9
|
+
* The holder id the autopilot claims interactions as.
|
|
10
|
+
*
|
|
11
|
+
* @beta
|
|
12
|
+
*/
|
|
13
|
+
export const AUTOPILOT_CLAIMANT = 'autopilot';
|
|
14
|
+
/** How the working agent is told about a decision when the config gives no phrase. */
|
|
15
|
+
const DEFAULT_DECISION_FRAMING = "Decided on the user's behalf";
|
|
16
|
+
/** How a written answer sent on the user's behalf is framed, when the config gives no phrase. */
|
|
17
|
+
const DEFAULT_REPLY_FRAMING = "Answered on the user's behalf.";
|
|
18
|
+
/**
|
|
19
|
+
* Written answers the autopilot may send in a row, each answering a question the working agent
|
|
20
|
+
* ended its turn on, before the next question goes to the person instead: a loop between two agents
|
|
21
|
+
* talking to each other is a failure, not a conversation.
|
|
22
|
+
*/
|
|
23
|
+
const MAX_CONSECUTIVE_REPLIES = 2;
|
|
24
|
+
/** The transient note that carries the working agent on, after the person said to. */
|
|
25
|
+
const CARRY_ON_PRIMER = '[The user asked you to carry on with the plan on their behalf.]';
|
|
26
|
+
/** Turn failures that are walls with their own UI, not something to offer to retry. */
|
|
27
|
+
const WALLS = new Set([
|
|
28
|
+
'budget-exhausted',
|
|
29
|
+
'provider-refused',
|
|
30
|
+
'ai-service-error',
|
|
31
|
+
]);
|
|
32
|
+
/** Written answers per step before `reply` is withdrawn, unless the config says otherwise. */
|
|
33
|
+
const DEFAULT_MAX_REFINEMENTS = 2;
|
|
34
|
+
/** How long a claimed interaction may go quiet before the watchdog hands it back. */
|
|
35
|
+
const DEFAULT_WATCHDOG_MS = 600000; // 10 minutes
|
|
36
|
+
/** How many times the watchdog looks within one window. */
|
|
37
|
+
const WATCHDOG_CHECKS_PER_WINDOW = 4;
|
|
38
|
+
const DEFAULT_COPY = {
|
|
39
|
+
running: 'Working on your behalf',
|
|
40
|
+
paused: 'Paused',
|
|
41
|
+
selfPaused: "Waiting for your answer: I'll carry on after this",
|
|
42
|
+
handBackLead: 'I need your input on this one:',
|
|
43
|
+
toggleLabel: 'Autopilot',
|
|
44
|
+
description: "Answers the assistant's questions on your behalf, so the work keeps going.",
|
|
45
|
+
pauseLabel: 'Pause autopilot',
|
|
46
|
+
resumeLabel: 'Resume autopilot',
|
|
47
|
+
resumeDecideLabel: 'Resume autopilot and force it to decide this one',
|
|
48
|
+
keepPausedLabel: 'Keep paused',
|
|
49
|
+
markLabel: 'Done in autopilot mode',
|
|
50
|
+
checkInMessage: "I've done a lot in one go, so I've stopped to check in. Shall I carry on?",
|
|
51
|
+
carryOnLabel: 'Carry on',
|
|
52
|
+
tooLongMessage: 'This conversation has grown too long for me to carry on as it is.',
|
|
53
|
+
compactLabel: 'Compact and carry on',
|
|
54
|
+
failedMessage: 'Something went wrong on that last step.',
|
|
55
|
+
tryAgainLabel: 'Try again',
|
|
56
|
+
sendLabel: 'Send',
|
|
57
|
+
budgetReason: 'This one is taking more working out than I expected, so over to you.',
|
|
58
|
+
errorReason: 'Something went wrong while I was deciding this one.',
|
|
59
|
+
noDecisionReason: "I couldn't settle this one.",
|
|
60
|
+
watchdogReason: 'This is taking longer than expected, so I need you to look at it.',
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Autopilot (GENC-1660): a driver decorator that answers the questions a working agent asks the
|
|
64
|
+
* user, on their behalf, grounded in the agent's autopilot config (for bootstrap, the brief).
|
|
65
|
+
*
|
|
66
|
+
* Neither side calls the other. The working agent asks as it always does; this driver claims the
|
|
67
|
+
* interactions its policy allows, while it is running for that flow, and answers each in one short
|
|
68
|
+
* turn on the flow's `AutopilotSession`. It plugs in only through the generic extension points
|
|
69
|
+
* — an interaction interceptor, a handler capability and the flow lifecycle — so the core knows
|
|
70
|
+
* nothing about it, and it wraps any `AiDriver`.
|
|
71
|
+
*
|
|
72
|
+
* @beta
|
|
73
|
+
*/
|
|
74
|
+
export class AutopilotDriver extends BaseDriverDecorator {
|
|
75
|
+
constructor(inner, options) {
|
|
76
|
+
var _a, _b, _c;
|
|
77
|
+
super(inner);
|
|
78
|
+
this.listeners = new Set();
|
|
79
|
+
this.sessions = new Map();
|
|
80
|
+
/** The flow the mode applies to: set when it is first switched, cleared when that flow ends. */
|
|
81
|
+
this.onFor = null;
|
|
82
|
+
this.mode = 'paused';
|
|
83
|
+
/** What sessions that have ended did, kept for the debug log. */
|
|
84
|
+
this.endedDiagnostics = [];
|
|
85
|
+
/** Written answers sent in a row; see `MAX_CONSECUTIVE_REPLIES`. */
|
|
86
|
+
this.consecutiveReplies = 0;
|
|
87
|
+
/**
|
|
88
|
+
* Bumped on every pause and resume, and when its sessions end. A written answer is sent only if
|
|
89
|
+
* none came between asking for it and its landing: one started before a pause is stale after it,
|
|
90
|
+
* even once resumed.
|
|
91
|
+
*/
|
|
92
|
+
this.generation = 0;
|
|
93
|
+
/** Working turns this driver has started that have not ended yet; see `working`. */
|
|
94
|
+
this.turnsInFlight = 0;
|
|
95
|
+
/** Written questions being answered right now; see `working`. */
|
|
96
|
+
this.answersInFlight = 0;
|
|
97
|
+
this.agents = options.agents;
|
|
98
|
+
this.providerRegistry = options.providerRegistry;
|
|
99
|
+
this.sessionKey = (_a = options.sessionKey) !== null && _a !== void 0 ? _a : '';
|
|
100
|
+
// The turn-level hand-back is a widget like any other: registered, so it has a controller and a
|
|
101
|
+
// text form. Once per app, whichever autopilot gets there first.
|
|
102
|
+
if (!getInteractionRegistration(AUTOPILOT_HAND_BACK)) {
|
|
103
|
+
registerInteraction(AUTOPILOT_HAND_BACK, {
|
|
104
|
+
controller: (data, ctx) => new HandBackController(data, ctx),
|
|
105
|
+
asText: handBackAsText,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const framing = () => {
|
|
109
|
+
var _a;
|
|
110
|
+
const owner = this.flowOwner();
|
|
111
|
+
return (owner && ((_a = this.bindingFor(owner)) === null || _a === void 0 ? void 0 : _a.config.decisionFraming)) || DEFAULT_DECISION_FRAMING;
|
|
112
|
+
};
|
|
113
|
+
// Unset means the platform's own wording.
|
|
114
|
+
const editFraming = () => {
|
|
115
|
+
var _a;
|
|
116
|
+
const owner = this.flowOwner();
|
|
117
|
+
return (owner && ((_a = this.bindingFor(owner)) === null || _a === void 0 ? void 0 : _a.config.editFraming)) || undefined;
|
|
118
|
+
};
|
|
119
|
+
// Every hook on the inner driver is undone by `detach()`, so this decorator can be dropped
|
|
120
|
+
// while the driver it wrapped lives on.
|
|
121
|
+
const removeInterceptor = (_b = inner.addInteractionInterceptor) === null || _b === void 0 ? void 0 : _b.call(inner, {
|
|
122
|
+
id: AUTOPILOT_CLAIMANT,
|
|
123
|
+
// Read when a claimed interaction settles: the framing of the flow the autopilot serves.
|
|
124
|
+
get decisionFraming() {
|
|
125
|
+
return framing();
|
|
126
|
+
},
|
|
127
|
+
// Read when the person finishes an interaction the autopilot had changed first.
|
|
128
|
+
get editFraming() {
|
|
129
|
+
return editFraming();
|
|
130
|
+
},
|
|
131
|
+
onInteractionOpened: (opened) => this.onInteractionOpened(opened),
|
|
132
|
+
});
|
|
133
|
+
if (removeInterceptor)
|
|
134
|
+
this.onDetach(removeInterceptor);
|
|
135
|
+
const removeCapability = (_c = inner.provideHandlerCapability) === null || _c === void 0 ? void 0 : _c.call(inner, AUTOPILOT_CLAIMANT, (scope) => this.capabilityFor(scope));
|
|
136
|
+
if (removeCapability)
|
|
137
|
+
this.onDetach(removeCapability);
|
|
138
|
+
const onFlowOwnerChanged = (e) => this.onFlowOwnerChanged(e.detail);
|
|
139
|
+
// Availability can change with the agent's state (a brief being approved), which nothing
|
|
140
|
+
// announces; the transcript moving is the cheapest reliable nudge to re-read it.
|
|
141
|
+
const onHistoryUpdated = () => this.notify();
|
|
142
|
+
inner.addEventListener('flow-owner-changed', onFlowOwnerChanged);
|
|
143
|
+
inner.addEventListener('history-updated', onHistoryUpdated);
|
|
144
|
+
this.onDetach(() => {
|
|
145
|
+
this.disarmWatchdog();
|
|
146
|
+
inner.removeEventListener('flow-owner-changed', onFlowOwnerChanged);
|
|
147
|
+
inner.removeEventListener('history-updated', onHistoryUpdated);
|
|
148
|
+
this.releaseHeld();
|
|
149
|
+
this.endSessions();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// -----------------------------------------------------------------------------------------------
|
|
153
|
+
// AutopilotHost
|
|
154
|
+
// -----------------------------------------------------------------------------------------------
|
|
155
|
+
get state() {
|
|
156
|
+
var _a;
|
|
157
|
+
const owner = this.flowOwner();
|
|
158
|
+
const working = this.turnsInFlight > 0 || this.answersInFlight > 0 || this.inner.isBusy();
|
|
159
|
+
if (!owner || !this.isAvailable(owner)) {
|
|
160
|
+
return { mode: 'unavailable', status: '', needsUser: false, working };
|
|
161
|
+
}
|
|
162
|
+
const mode = this.onFor === owner ? this.mode : 'paused';
|
|
163
|
+
const copy = this.copyFor(owner);
|
|
164
|
+
const selfPaused = ((_a = this.pauseState) === null || _a === void 0 ? void 0 : _a.scope) === 'this-interaction';
|
|
165
|
+
return Object.assign(Object.assign({ mode }, (this.pauseState ? { pause: this.pauseState } : {})), { status: selfPaused ? copy.selfPaused : mode === 'running' ? copy.running : copy.paused, needsUser: selfPaused || !!this.turnHandBack, working });
|
|
166
|
+
}
|
|
167
|
+
get copy() {
|
|
168
|
+
return this.copyFor(this.flowOwner());
|
|
169
|
+
}
|
|
170
|
+
subscribe(listener) {
|
|
171
|
+
this.listeners.add(listener);
|
|
172
|
+
return () => {
|
|
173
|
+
this.listeners.delete(listener);
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
pause() {
|
|
177
|
+
var _a, _b;
|
|
178
|
+
const owner = this.flowOwner();
|
|
179
|
+
if (!owner || !this.isAvailable(owner))
|
|
180
|
+
return;
|
|
181
|
+
this.onFor = owner;
|
|
182
|
+
this.mode = 'paused';
|
|
183
|
+
this.pauseState = { scope: 'until-resumed' };
|
|
184
|
+
this.generation += 1;
|
|
185
|
+
const held = this.held;
|
|
186
|
+
if (held && held.opened.ctx.heldBy === AUTOPILOT_CLAIMANT) {
|
|
187
|
+
held.handedOver = 'pause';
|
|
188
|
+
(_a = this.sessions.get(held.agentName)) === null || _a === void 0 ? void 0 : _a.abort();
|
|
189
|
+
held.opened.hold.set('user');
|
|
190
|
+
}
|
|
191
|
+
// A written question it is answering is the person's now: its answer is stopped, and would be
|
|
192
|
+
// dropped if it landed anyway (see `generation`). They answer by typing.
|
|
193
|
+
if (this.answersInFlight > 0)
|
|
194
|
+
(_b = this.sessions.get(owner)) === null || _b === void 0 ? void 0 : _b.abort();
|
|
195
|
+
this.record('autopilot.paused', { scope: 'until-resumed', flow: owner });
|
|
196
|
+
this.notify();
|
|
197
|
+
}
|
|
198
|
+
resume() {
|
|
199
|
+
var _a;
|
|
200
|
+
const owner = this.flowOwner();
|
|
201
|
+
if (!owner || !this.isAvailable(owner))
|
|
202
|
+
return;
|
|
203
|
+
const wasSelfPaused = ((_a = this.pauseState) === null || _a === void 0 ? void 0 : _a.scope) === 'this-interaction';
|
|
204
|
+
this.onFor = owner;
|
|
205
|
+
this.mode = 'running';
|
|
206
|
+
this.pauseState = undefined;
|
|
207
|
+
this.generation += 1;
|
|
208
|
+
this.record('autopilot.resumed', { flow: owner, decideYourself: wasSelfPaused });
|
|
209
|
+
const held = this.held;
|
|
210
|
+
if (held === null || held === void 0 ? void 0 : held.handedOver) {
|
|
211
|
+
// Pausing handed it over, so take it back; a self-pause the person resumed means "decide it
|
|
212
|
+
// yourself", so take it back without the option to hand it back again.
|
|
213
|
+
held.handedOver = undefined;
|
|
214
|
+
if (held.opened.hold.set('claimant')) {
|
|
215
|
+
// Watched again, as when it was first claimed: the watchdog stops for good once it hands
|
|
216
|
+
// an interaction back, and a decision that stalled once may well stall again.
|
|
217
|
+
this.armWatchdog(held);
|
|
218
|
+
this.decide(held, { allowHandBack: !wasSelfPaused });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else if (!held && !this.turnHandBack && !this.inner.isBusy()) {
|
|
222
|
+
// Resumed with the working agent idle — its turn ended waiting for the person — so carry it
|
|
223
|
+
// on, or nothing would happen until they typed: answer the question it ended on, if it ended
|
|
224
|
+
// on one nobody answered, or tell it to carry on. Only on a live Resume, never on a restore.
|
|
225
|
+
const question = this.lastQuestion();
|
|
226
|
+
if (question) {
|
|
227
|
+
this.answerInBackground(owner, question);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
void this.continueFromHistory([{ role: 'user', content: CARRY_ON_PRIMER }]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
this.notify();
|
|
234
|
+
}
|
|
235
|
+
stop() {
|
|
236
|
+
var _a, _b;
|
|
237
|
+
(_b = (_a = this.inner).cancel) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
238
|
+
this.pause();
|
|
239
|
+
}
|
|
240
|
+
// -----------------------------------------------------------------------------------------------
|
|
241
|
+
// Persistence (§4.8): the mode is saved with the session and restored with it
|
|
242
|
+
// -----------------------------------------------------------------------------------------------
|
|
243
|
+
/** The mode to save with the session, while it is set for the current flow. */
|
|
244
|
+
getPersistedMode() {
|
|
245
|
+
const owner = this.flowOwner();
|
|
246
|
+
if (!owner || this.onFor !== owner)
|
|
247
|
+
return undefined;
|
|
248
|
+
return { onFor: owner, mode: this.mode };
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Bring back a saved mode. It applies once its flow is the owner again — at once if it already
|
|
252
|
+
* is, otherwise when the restored flow re-activates. Restoring starts nothing: the autopilot acts
|
|
253
|
+
* only on live events, such as the working agent's own resume asking a question (§4.10, S3).
|
|
254
|
+
*/
|
|
255
|
+
restorePersistedMode(saved) {
|
|
256
|
+
this.restored = saved;
|
|
257
|
+
const owner = this.flowOwner();
|
|
258
|
+
if (owner)
|
|
259
|
+
this.adoptRestored(owner);
|
|
260
|
+
}
|
|
261
|
+
adoptRestored(owner) {
|
|
262
|
+
const saved = this.restored;
|
|
263
|
+
if (!saved)
|
|
264
|
+
return;
|
|
265
|
+
this.restored = undefined;
|
|
266
|
+
// Another flow took over first, so the saved mode has nothing left to apply to.
|
|
267
|
+
if (saved.onFor !== owner)
|
|
268
|
+
return;
|
|
269
|
+
this.onFor = owner;
|
|
270
|
+
this.mode = saved.mode;
|
|
271
|
+
this.pauseState = undefined;
|
|
272
|
+
this.record(saved.mode === 'running' ? 'autopilot.resumed' : 'autopilot.paused', {
|
|
273
|
+
trigger: 'restored',
|
|
274
|
+
flow: owner,
|
|
275
|
+
});
|
|
276
|
+
this.notify();
|
|
277
|
+
}
|
|
278
|
+
keepPaused(interactionId) {
|
|
279
|
+
var _a;
|
|
280
|
+
if (((_a = this.pauseState) === null || _a === void 0 ? void 0 : _a.scope) !== 'this-interaction')
|
|
281
|
+
return;
|
|
282
|
+
if (this.pauseState.interactionId !== interactionId)
|
|
283
|
+
return;
|
|
284
|
+
this.generation += 1;
|
|
285
|
+
this.mode = 'paused';
|
|
286
|
+
this.pauseState = { scope: 'until-resumed' };
|
|
287
|
+
if (this.held)
|
|
288
|
+
this.held.handedOver = 'pause';
|
|
289
|
+
this.record('autopilot.paused', { scope: 'until-resumed', trigger: 'keep-paused' });
|
|
290
|
+
this.notify();
|
|
291
|
+
}
|
|
292
|
+
// -----------------------------------------------------------------------------------------------
|
|
293
|
+
// Turns: how each working turn ended decides what the autopilot does next (§4.2, §4.10)
|
|
294
|
+
// -----------------------------------------------------------------------------------------------
|
|
295
|
+
sendMessage(input, attachments, options) {
|
|
296
|
+
const _super = Object.create(null, {
|
|
297
|
+
sendMessage: { get: () => super.sendMessage }
|
|
298
|
+
});
|
|
299
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
300
|
+
// A message the person typed starts a fresh run of answers on their behalf.
|
|
301
|
+
if (!(options === null || options === void 0 ? void 0 : options.delegate))
|
|
302
|
+
this.consecutiveReplies = 0;
|
|
303
|
+
const result = yield this.whileWorking(() => _super.sendMessage.call(this, input, attachments, options));
|
|
304
|
+
this.afterTurn(result);
|
|
305
|
+
return result;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
continueFromHistory(transientPrimer, options) {
|
|
309
|
+
const _super = Object.create(null, {
|
|
310
|
+
continueFromHistory: { get: () => super.continueFromHistory }
|
|
311
|
+
});
|
|
312
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
313
|
+
const result = yield this.whileWorking(() => _super.continueFromHistory.call(this, transientPrimer, options));
|
|
314
|
+
this.afterTurn(result);
|
|
315
|
+
return result;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
resumeRestoredFlow(resumeNote, agent) {
|
|
319
|
+
const _super = Object.create(null, {
|
|
320
|
+
resumeRestoredFlow: { get: () => super.resumeRestoredFlow }
|
|
321
|
+
});
|
|
322
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
323
|
+
const result = yield this.whileWorking(() => _super.resumeRestoredFlow.call(this, resumeNote, agent));
|
|
324
|
+
if (result)
|
|
325
|
+
this.afterTurn(result);
|
|
326
|
+
else
|
|
327
|
+
this.notify();
|
|
328
|
+
return result;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Run a working turn, counted for `working` from the moment it is asked for: the wrapped driver
|
|
333
|
+
* may not say it is busy until some way in. The caller notifies once it has acted on the end.
|
|
334
|
+
*/
|
|
335
|
+
whileWorking(run) {
|
|
336
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
337
|
+
this.turnsInFlight += 1;
|
|
338
|
+
this.notify();
|
|
339
|
+
let ended = false;
|
|
340
|
+
try {
|
|
341
|
+
const result = yield run();
|
|
342
|
+
ended = true;
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
this.turnsInFlight -= 1;
|
|
347
|
+
if (!ended)
|
|
348
|
+
this.notify();
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Act on how a working turn ended, without holding up whoever awaited it, then notify: idle
|
|
354
|
+
* unless that started something (an answer, a hand-back), which it does before this returns.
|
|
355
|
+
*/
|
|
356
|
+
afterTurn(result) {
|
|
357
|
+
void this.onTurnEnded(result).catch((e) => {
|
|
358
|
+
logger.error('AutopilotDriver: handling the end of a turn failed:', e);
|
|
359
|
+
this.record('autopilot.error', { message: e instanceof Error ? e.message : String(e) });
|
|
360
|
+
});
|
|
361
|
+
this.notify();
|
|
362
|
+
}
|
|
363
|
+
onTurnEnded(result) {
|
|
364
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
365
|
+
const owner = this.flowOwner();
|
|
366
|
+
// Nothing to do unless the autopilot is running for a flow that is still going.
|
|
367
|
+
if (!owner || this.onFor !== owner || this.mode !== 'running' || this.pauseState)
|
|
368
|
+
return;
|
|
369
|
+
if (this.held || this.turnHandBack || this.inner.isBusy())
|
|
370
|
+
return;
|
|
371
|
+
const failure = result.reason === 'done' ? result.failureReason : undefined;
|
|
372
|
+
if (failure) {
|
|
373
|
+
// A wall (the budget, a refused account) keeps its own UI and is not the person's to retry.
|
|
374
|
+
if (WALLS.has(failure))
|
|
375
|
+
return;
|
|
376
|
+
yield this.presentHandBack(failure === 'max-iterations'
|
|
377
|
+
? 'max-iterations'
|
|
378
|
+
: failure === 'context-exhausted'
|
|
379
|
+
? 'context-exhausted'
|
|
380
|
+
: 'failure');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// Ended cleanly with the flow still running: the working agent is waiting for the person to type.
|
|
384
|
+
const question = this.lastQuestion();
|
|
385
|
+
if (!question)
|
|
386
|
+
return;
|
|
387
|
+
if (this.consecutiveReplies >= MAX_CONSECUTIVE_REPLIES) {
|
|
388
|
+
yield this.presentHandBack('question', { question });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
yield this.answer(owner, question);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
/** {@link AutopilotDriver.answer}, from a caller that does not wait for it. */
|
|
395
|
+
answerInBackground(owner, question) {
|
|
396
|
+
void this.answer(owner, question).catch((e) => {
|
|
397
|
+
logger.error('AutopilotDriver: answering a question failed:', e);
|
|
398
|
+
this.record('autopilot.error', { message: e instanceof Error ? e.message : String(e) });
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Answer a written question the working agent ended its turn on, and send the answer on the
|
|
403
|
+
* user's behalf, with what writing it cost booked on that message (§4.2).
|
|
404
|
+
*
|
|
405
|
+
* Sent only if the question still stands when the answer lands: no pause or resume since it was
|
|
406
|
+
* asked (Pause stops it, and the question is the person's to answer by typing), the flow still
|
|
407
|
+
* running, the working agent idle, and the question still the last thing it said. Otherwise the
|
|
408
|
+
* answer goes nowhere — not sent, not counted, not recorded as acted — and its cost is recorded as
|
|
409
|
+
* dropped, as it is whenever no reply is sent to carry it.
|
|
410
|
+
*/
|
|
411
|
+
answer(owner, question) {
|
|
412
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
413
|
+
var _a, _b, _c;
|
|
414
|
+
const session = this.sessionFor(owner);
|
|
415
|
+
if (!session)
|
|
416
|
+
return;
|
|
417
|
+
const generation = this.generation;
|
|
418
|
+
const stands = () => this.generation === generation &&
|
|
419
|
+
this.flowOwner() === owner &&
|
|
420
|
+
this.onFor === owner &&
|
|
421
|
+
this.mode === 'running' &&
|
|
422
|
+
!this.pauseState &&
|
|
423
|
+
!this.inner.isBusy() &&
|
|
424
|
+
this.lastQuestion() === question;
|
|
425
|
+
this.answersInFlight += 1;
|
|
426
|
+
this.notify();
|
|
427
|
+
let outcome;
|
|
428
|
+
try {
|
|
429
|
+
outcome = yield session.answerQuestion(question, {
|
|
430
|
+
allowHandBack: true,
|
|
431
|
+
stillWanted: stands,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
catch (e) {
|
|
435
|
+
this.answersInFlight -= 1;
|
|
436
|
+
this.notify();
|
|
437
|
+
throw e;
|
|
438
|
+
}
|
|
439
|
+
// Not notified yet: what follows (a reply, a hand-back, or nothing) notifies.
|
|
440
|
+
this.answersInFlight -= 1;
|
|
441
|
+
const cost = (_a = outcome.costUsd) !== null && _a !== void 0 ? _a : 0;
|
|
442
|
+
if (!stands()) {
|
|
443
|
+
this.dropCost(cost, 'stale');
|
|
444
|
+
this.notify();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (outcome.kind === 'replied') {
|
|
448
|
+
this.consecutiveReplies += 1;
|
|
449
|
+
this.record('autopilot.acted', { action: 'reply', question: true });
|
|
450
|
+
const framing = (_c = (_b = this.bindingFor(owner)) === null || _b === void 0 ? void 0 : _b.config.decisionFraming) !== null && _c !== void 0 ? _c : DEFAULT_REPLY_FRAMING;
|
|
451
|
+
yield this.sendMessage(outcome.text, undefined, {
|
|
452
|
+
delegate: AUTOPILOT_CLAIMANT,
|
|
453
|
+
delegateFraming: framing,
|
|
454
|
+
externalCostUsd: cost,
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
this.dropCost(cost, outcome.kind);
|
|
459
|
+
if (outcome.kind === 'handed-back') {
|
|
460
|
+
yield this.presentHandBack('question', { question, reason: outcome.reason });
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
this.notify();
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Spend on a written answer that no reply carries into the session total — it was handed back,
|
|
469
|
+
* stopped, or went stale — recorded so it is never lost silently.
|
|
470
|
+
*/
|
|
471
|
+
dropCost(costUsd, why) {
|
|
472
|
+
if (costUsd > 0)
|
|
473
|
+
this.record('autopilot.cost-dropped', { costUsd, why });
|
|
474
|
+
}
|
|
475
|
+
/** The working agent's last visible words, when its turn ended on them. */
|
|
476
|
+
lastQuestion() {
|
|
477
|
+
var _a, _b, _c, _d;
|
|
478
|
+
const history = (_c = (_b = (_a = this.inner).getRawHistory) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : [];
|
|
479
|
+
const last = [...history].reverse().find((m) => m.role !== 'system-event' && !m.thinking);
|
|
480
|
+
if (!last || last.role !== 'assistant' || last.interaction || last.delegate)
|
|
481
|
+
return undefined;
|
|
482
|
+
if (last.excludeFromProvider ||
|
|
483
|
+
last.category === 'reasoning' ||
|
|
484
|
+
last.category === 'narration') {
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
return ((_d = last.content) === null || _d === void 0 ? void 0 : _d.trim()) || undefined;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* The turn-level hand-back (§4.10): a working turn ended needing the person and there is no
|
|
491
|
+
* widget to hand over, so the autopilot shows its own, `userOnly`, and acts on the choice.
|
|
492
|
+
*/
|
|
493
|
+
presentHandBack(why_1) {
|
|
494
|
+
return __awaiter(this, arguments, void 0, function* (why, opts = {}) {
|
|
495
|
+
var _a, _b, _c, _d;
|
|
496
|
+
if (!this.inner.presentInteraction)
|
|
497
|
+
return;
|
|
498
|
+
const copy = this.copy;
|
|
499
|
+
const pauseOption = { id: 'pause', label: copy.pauseLabel };
|
|
500
|
+
const data = why === 'max-iterations'
|
|
501
|
+
? {
|
|
502
|
+
message: copy.checkInMessage,
|
|
503
|
+
options: [{ id: 'carry-on', label: copy.carryOnLabel }, pauseOption],
|
|
504
|
+
}
|
|
505
|
+
: why === 'context-exhausted'
|
|
506
|
+
? {
|
|
507
|
+
message: copy.tooLongMessage,
|
|
508
|
+
options: [{ id: 'compact', label: copy.compactLabel }, pauseOption],
|
|
509
|
+
}
|
|
510
|
+
: why === 'failure'
|
|
511
|
+
? {
|
|
512
|
+
message: copy.failedMessage,
|
|
513
|
+
options: [{ id: 'try-again', label: copy.tryAgainLabel }, pauseOption],
|
|
514
|
+
}
|
|
515
|
+
: {
|
|
516
|
+
message: opts.reason ? `${copy.handBackLead} ${opts.reason}` : copy.handBackLead,
|
|
517
|
+
question: opts.question,
|
|
518
|
+
freeText: true,
|
|
519
|
+
sendLabel: copy.sendLabel,
|
|
520
|
+
options: [pauseOption],
|
|
521
|
+
};
|
|
522
|
+
this.turnHandBack = { why };
|
|
523
|
+
this.consecutiveReplies = 0;
|
|
524
|
+
this.notify();
|
|
525
|
+
let result;
|
|
526
|
+
try {
|
|
527
|
+
result = yield this.inner.presentInteraction(AUTOPILOT_HAND_BACK, data, {
|
|
528
|
+
userOnly: true,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
finally {
|
|
532
|
+
this.turnHandBack = undefined;
|
|
533
|
+
this.notify();
|
|
534
|
+
}
|
|
535
|
+
const choice = (_a = result === null || result === void 0 ? void 0 : result.payload) === null || _a === void 0 ? void 0 : _a.choice;
|
|
536
|
+
this.record('autopilot.handback-presented', Object.assign({ why, options: data.options.map((o) => o.id) }, (choice ? { choice } : {})));
|
|
537
|
+
if (!choice)
|
|
538
|
+
return;
|
|
539
|
+
if (choice === 'pause') {
|
|
540
|
+
this.pause();
|
|
541
|
+
}
|
|
542
|
+
else if (choice === 'reply') {
|
|
543
|
+
// The person's own words: an ordinary message, not one sent on their behalf.
|
|
544
|
+
yield this.sendMessage((_b = result.payload.text) !== null && _b !== void 0 ? _b : '');
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
if (choice === 'compact')
|
|
548
|
+
yield ((_d = (_c = this.inner).compact) === null || _d === void 0 ? void 0 : _d.call(_c));
|
|
549
|
+
yield this.continueFromHistory([{ role: 'user', content: CARRY_ON_PRIMER }]);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The inner driver's diagnostics, then everything the autopilot did (§4.8): diagnostics compose
|
|
555
|
+
* down the decorator stack, so they reach the downloaded and the persisted debug log through the
|
|
556
|
+
* host's existing external-diagnostics channel.
|
|
557
|
+
*/
|
|
558
|
+
getExternalDiagnostics() {
|
|
559
|
+
return [
|
|
560
|
+
...super.getExternalDiagnostics(),
|
|
561
|
+
...this.endedDiagnostics,
|
|
562
|
+
...[...this.sessions.values()].flatMap((session) => session.diagnostics()),
|
|
563
|
+
];
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Close every session, keeping what it did for the debug log. An answer one was still writing is
|
|
567
|
+
* stale from here: it is never sent, and its cost is recorded as dropped when it lands.
|
|
568
|
+
*/
|
|
569
|
+
endSessions() {
|
|
570
|
+
this.generation += 1;
|
|
571
|
+
for (const session of this.sessions.values()) {
|
|
572
|
+
this.endedDiagnostics.push(...session.diagnostics());
|
|
573
|
+
session.dispose();
|
|
574
|
+
}
|
|
575
|
+
this.sessions.clear();
|
|
576
|
+
}
|
|
577
|
+
dispose() {
|
|
578
|
+
const _super = Object.create(null, {
|
|
579
|
+
dispose: { get: () => super.dispose }
|
|
580
|
+
});
|
|
581
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
582
|
+
this.endSessions();
|
|
583
|
+
yield _super.dispose.call(this);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
// -----------------------------------------------------------------------------------------------
|
|
587
|
+
// Claiming and deciding
|
|
588
|
+
// -----------------------------------------------------------------------------------------------
|
|
589
|
+
onInteractionOpened(opened) {
|
|
590
|
+
var _a, _b;
|
|
591
|
+
const decline = (reason) => {
|
|
592
|
+
this.record('autopilot.declined', {
|
|
593
|
+
interactionId: opened.interactionId,
|
|
594
|
+
component: opened.componentName,
|
|
595
|
+
reason,
|
|
596
|
+
});
|
|
597
|
+
return undefined;
|
|
598
|
+
};
|
|
599
|
+
const owner = this.flowOwner();
|
|
600
|
+
if (!owner || this.onFor !== owner || this.mode !== 'running')
|
|
601
|
+
return decline('paused');
|
|
602
|
+
if (opened.ownerPath[0] !== owner)
|
|
603
|
+
return decline('other-flow');
|
|
604
|
+
// The innermost agent on the path that declares an autopilot answers for it: sub-agents
|
|
605
|
+
// inherit their flow's.
|
|
606
|
+
const agentName = [...opened.ownerPath].reverse().find((name) => this.bindingFor(name));
|
|
607
|
+
const binding = agentName ? this.bindingFor(agentName) : undefined;
|
|
608
|
+
if (!agentName || !binding)
|
|
609
|
+
return decline('no-autopilot');
|
|
610
|
+
if (!this.isAvailable(owner))
|
|
611
|
+
return decline('unavailable');
|
|
612
|
+
if (opened.userOnly)
|
|
613
|
+
return decline('userOnly');
|
|
614
|
+
if (((_b = (_a = binding.config.widgetPolicy) === null || _a === void 0 ? void 0 : _a[opened.componentName]) !== null && _b !== void 0 ? _b : 'human-first') !== 'allowed') {
|
|
615
|
+
return decline('policy');
|
|
616
|
+
}
|
|
617
|
+
if (opened.ctx.controller === undefined)
|
|
618
|
+
return decline('no-controller');
|
|
619
|
+
const held = { opened, agentName };
|
|
620
|
+
this.held = held;
|
|
621
|
+
opened.ctx.onDispose(() => this.onSettled(held));
|
|
622
|
+
this.record('autopilot.claimed', {
|
|
623
|
+
interactionId: opened.interactionId,
|
|
624
|
+
component: opened.componentName,
|
|
625
|
+
ownerPath: opened.ownerPath,
|
|
626
|
+
});
|
|
627
|
+
this.armWatchdog(held);
|
|
628
|
+
// The claim is granted only once this returns, so act on it after.
|
|
629
|
+
queueMicrotask(() => {
|
|
630
|
+
opened.ctx.onSurfaceReady(() => {
|
|
631
|
+
if (this.held === held && opened.ctx.heldBy === AUTOPILOT_CLAIMANT) {
|
|
632
|
+
this.decide(held, { allowHandBack: true });
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
return 'claim';
|
|
637
|
+
}
|
|
638
|
+
/** Run one decision on the flow's session, and act on how it ended. */
|
|
639
|
+
decide(held, options) {
|
|
640
|
+
const session = this.sessionFor(held.agentName);
|
|
641
|
+
if (!session)
|
|
642
|
+
return;
|
|
643
|
+
const key = this.stepKey(held);
|
|
644
|
+
session
|
|
645
|
+
.decide(held.opened, Object.assign(Object.assign({}, options), { allowReply: this.mayRefine(held, key), onReply: () => this.countRefinement(held, key) }))
|
|
646
|
+
.then((outcome) => this.onDecided(held, outcome))
|
|
647
|
+
.catch((e) => {
|
|
648
|
+
logger.error('AutopilotDriver: a decision failed:', e);
|
|
649
|
+
this.record('autopilot.error', {
|
|
650
|
+
interactionId: held.opened.interactionId,
|
|
651
|
+
message: e instanceof Error ? e.message : String(e),
|
|
652
|
+
});
|
|
653
|
+
this.handBack(held, this.copyFor(held.agentName).errorReason, 'error');
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
/** The step a decision belongs to, for the refinement limit (§4.4). */
|
|
657
|
+
stepKey(held) {
|
|
658
|
+
var _a, _b, _c;
|
|
659
|
+
const { stepKey } = (_b = (_a = this.bindingFor(held.agentName)) === null || _a === void 0 ? void 0 : _a.config) !== null && _b !== void 0 ? _b : {};
|
|
660
|
+
const state = (_c = this.bindingFor(held.agentName)) === null || _c === void 0 ? void 0 : _c.state();
|
|
661
|
+
try {
|
|
662
|
+
if (stepKey && state !== undefined)
|
|
663
|
+
return stepKey({ state });
|
|
664
|
+
}
|
|
665
|
+
catch (e) {
|
|
666
|
+
logger.warn('AutopilotDriver: stepKey threw; using the component name:', e);
|
|
667
|
+
}
|
|
668
|
+
return held.opened.componentName;
|
|
669
|
+
}
|
|
670
|
+
/** Whether a written answer is still allowed on this step. The count resets when the step changes. */
|
|
671
|
+
mayRefine(held, key) {
|
|
672
|
+
var _a;
|
|
673
|
+
if (((_a = this.refinements) === null || _a === void 0 ? void 0 : _a.key) !== key)
|
|
674
|
+
this.refinements = { key, count: 0 };
|
|
675
|
+
return this.refinements.count < this.refinementLimit(held);
|
|
676
|
+
}
|
|
677
|
+
countRefinement(held, key) {
|
|
678
|
+
var _a;
|
|
679
|
+
if (((_a = this.refinements) === null || _a === void 0 ? void 0 : _a.key) !== key)
|
|
680
|
+
this.refinements = { key, count: 0 };
|
|
681
|
+
this.refinements.count += 1;
|
|
682
|
+
const limit = this.refinementLimit(held);
|
|
683
|
+
this.record('autopilot.refinement', {
|
|
684
|
+
interactionId: held.opened.interactionId,
|
|
685
|
+
stepKey: key,
|
|
686
|
+
count: this.refinements.count,
|
|
687
|
+
limitReached: this.refinements.count >= limit,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
/** Written answers allowed per step: the config's when it is a whole number `>= 0`. */
|
|
691
|
+
refinementLimit(held) {
|
|
692
|
+
var _a;
|
|
693
|
+
const limit = (_a = this.bindingFor(held.agentName)) === null || _a === void 0 ? void 0 : _a.config.maxRefinements;
|
|
694
|
+
return typeof limit === 'number' && Number.isFinite(limit) && limit >= 0
|
|
695
|
+
? limit
|
|
696
|
+
: DEFAULT_MAX_REFINEMENTS;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* The watchdog (§4.2, critique S5): a claimed interaction whose work has gone quiet for longer
|
|
700
|
+
* than `watchdogMs` is handed back rather than left to hang silently. Quiet is either no surface
|
|
701
|
+
* ready to act on and no heartbeat, or the autopilot's own decision still running with no model
|
|
702
|
+
* call landing and no heartbeat — a stalled model call, which nothing else times out. It checks a
|
|
703
|
+
* few times per window, and stops when the interaction settles.
|
|
704
|
+
*/
|
|
705
|
+
armWatchdog(held) {
|
|
706
|
+
var _a;
|
|
707
|
+
this.disarmWatchdog();
|
|
708
|
+
const windowMs = positiveOr((_a = this.bindingFor(held.agentName)) === null || _a === void 0 ? void 0 : _a.config.watchdogMs, DEFAULT_WATCHDOG_MS);
|
|
709
|
+
const check = () => {
|
|
710
|
+
var _a, _b;
|
|
711
|
+
this.watchdog = undefined;
|
|
712
|
+
if (this.held !== held)
|
|
713
|
+
return;
|
|
714
|
+
const { ctx } = held.opened;
|
|
715
|
+
const deciding = (_a = this.sessions
|
|
716
|
+
.get(held.agentName)) === null || _a === void 0 ? void 0 : _a.decisionProgressAt(held.opened.interactionId);
|
|
717
|
+
const lastSign = Math.max(ctx.lastActivityAt, deciding !== null && deciding !== void 0 ? deciding : 0);
|
|
718
|
+
const quiet = Date.now() - lastSign > windowMs;
|
|
719
|
+
const waiting = deciding !== undefined || !((_b = ctx.surface) === null || _b === void 0 ? void 0 : _b.isReady());
|
|
720
|
+
if (ctx.heldBy === AUTOPILOT_CLAIMANT && waiting && quiet) {
|
|
721
|
+
this.handBack(held, this.copyFor(held.agentName).watchdogReason, 'watchdog');
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
this.watchdog = setTimeout(check, Math.max(1, Math.floor(windowMs / WATCHDOG_CHECKS_PER_WINDOW)));
|
|
725
|
+
};
|
|
726
|
+
this.watchdog = setTimeout(check, Math.max(1, Math.floor(windowMs / WATCHDOG_CHECKS_PER_WINDOW)));
|
|
727
|
+
}
|
|
728
|
+
disarmWatchdog() {
|
|
729
|
+
if (this.watchdog !== undefined)
|
|
730
|
+
clearTimeout(this.watchdog);
|
|
731
|
+
this.watchdog = undefined;
|
|
732
|
+
}
|
|
733
|
+
onDecided(held, outcome) {
|
|
734
|
+
if (outcome.kind !== 'handed-back')
|
|
735
|
+
return;
|
|
736
|
+
this.handBack(held, outcome.reason, outcome.trigger);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* The autopilot's self-pause, for one interaction (§4.10): the person gets it, with the reason as
|
|
740
|
+
* the banner, and the autopilot carries on by itself once they answer. Only while it is still
|
|
741
|
+
* the autopilot's to hand back — not settled, not already with the person — and its decision
|
|
742
|
+
* stops with it, so a decision still running spends nothing more on an interaction it no longer
|
|
743
|
+
* holds. Every caller (a decision's own hand-back, a failed decision, the watchdog) gets both.
|
|
744
|
+
*/
|
|
745
|
+
handBack(held, reason, trigger) {
|
|
746
|
+
var _a;
|
|
747
|
+
if (this.held !== held || held.opened.ctx.heldBy !== AUTOPILOT_CLAIMANT)
|
|
748
|
+
return;
|
|
749
|
+
(_a = this.sessions.get(held.agentName)) === null || _a === void 0 ? void 0 : _a.abort();
|
|
750
|
+
held.handedOver = 'self';
|
|
751
|
+
held.opened.hold.set('user');
|
|
752
|
+
this.pauseState = {
|
|
753
|
+
scope: 'this-interaction',
|
|
754
|
+
interactionId: held.opened.interactionId,
|
|
755
|
+
reason,
|
|
756
|
+
};
|
|
757
|
+
this.record('autopilot.handed-back', {
|
|
758
|
+
interactionId: held.opened.interactionId,
|
|
759
|
+
reason,
|
|
760
|
+
trigger,
|
|
761
|
+
});
|
|
762
|
+
this.notify();
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* On detach: hand a claimed interaction the autopilot still holds to the person, stopping its
|
|
766
|
+
* decision, so the driver it wrapped lives on with nothing held by an autopilot that is gone.
|
|
767
|
+
*/
|
|
768
|
+
releaseHeld() {
|
|
769
|
+
var _a;
|
|
770
|
+
const held = this.held;
|
|
771
|
+
this.held = undefined;
|
|
772
|
+
if (!held || held.opened.ctx.heldBy !== AUTOPILOT_CLAIMANT)
|
|
773
|
+
return;
|
|
774
|
+
(_a = this.sessions.get(held.agentName)) === null || _a === void 0 ? void 0 : _a.abort();
|
|
775
|
+
held.opened.hold.set('user');
|
|
776
|
+
this.record('autopilot.handed-back', {
|
|
777
|
+
interactionId: held.opened.interactionId,
|
|
778
|
+
trigger: 'detached',
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
/** A claimed interaction settled, whoever settled it. */
|
|
782
|
+
onSettled(held) {
|
|
783
|
+
var _a, _b;
|
|
784
|
+
if (this.held !== held)
|
|
785
|
+
return;
|
|
786
|
+
this.held = undefined;
|
|
787
|
+
this.disarmWatchdog();
|
|
788
|
+
(_a = this.sessions.get(held.agentName)) === null || _a === void 0 ? void 0 : _a.abort();
|
|
789
|
+
// The autopilot's own hand-back ends when the person answers: it carries on by itself.
|
|
790
|
+
if (((_b = this.pauseState) === null || _b === void 0 ? void 0 : _b.scope) === 'this-interaction' &&
|
|
791
|
+
this.pauseState.interactionId === held.opened.interactionId) {
|
|
792
|
+
this.pauseState = undefined;
|
|
793
|
+
this.record('autopilot.resumed', { trigger: 'answered', flow: this.onFor });
|
|
794
|
+
}
|
|
795
|
+
this.notify();
|
|
796
|
+
}
|
|
797
|
+
onFlowOwnerChanged(owner) {
|
|
798
|
+
if (owner === this.onFor)
|
|
799
|
+
return;
|
|
800
|
+
this.refinements = undefined;
|
|
801
|
+
// "On" is scoped to the flow it was set for: a new flow, or none, starts paused.
|
|
802
|
+
this.onFor = null;
|
|
803
|
+
this.mode = 'paused';
|
|
804
|
+
this.pauseState = undefined;
|
|
805
|
+
this.endSessions();
|
|
806
|
+
if (owner)
|
|
807
|
+
this.adoptRestored(owner);
|
|
808
|
+
this.notify();
|
|
809
|
+
}
|
|
810
|
+
capabilityFor(scope) {
|
|
811
|
+
const owner = scope.flowOwner;
|
|
812
|
+
if (!owner || !this.bindingFor(owner))
|
|
813
|
+
return undefined;
|
|
814
|
+
const currentMode = () => {
|
|
815
|
+
if (!this.isAvailable(owner))
|
|
816
|
+
return 'unavailable';
|
|
817
|
+
return this.onFor === owner ? this.mode : 'paused';
|
|
818
|
+
};
|
|
819
|
+
return {
|
|
820
|
+
get mode() {
|
|
821
|
+
return currentMode();
|
|
822
|
+
},
|
|
823
|
+
setMode: (mode) => {
|
|
824
|
+
if (this.flowOwner() !== owner)
|
|
825
|
+
return;
|
|
826
|
+
if (mode === 'running')
|
|
827
|
+
this.resume();
|
|
828
|
+
else
|
|
829
|
+
this.pause();
|
|
830
|
+
},
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
// -----------------------------------------------------------------------------------------------
|
|
834
|
+
// Helpers
|
|
835
|
+
// -----------------------------------------------------------------------------------------------
|
|
836
|
+
flowOwner() {
|
|
837
|
+
var _a, _b, _c;
|
|
838
|
+
return (_c = (_b = (_a = this.inner).getFlowOwner) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : null;
|
|
839
|
+
}
|
|
840
|
+
bindingFor(agentName) {
|
|
841
|
+
var _a;
|
|
842
|
+
return (_a = this.agents.find((a) => a.name === agentName)) === null || _a === void 0 ? void 0 : _a.autopilot;
|
|
843
|
+
}
|
|
844
|
+
isAvailable(owner) {
|
|
845
|
+
const binding = this.bindingFor(owner);
|
|
846
|
+
if (!binding)
|
|
847
|
+
return false;
|
|
848
|
+
const { availableWhen } = binding.config;
|
|
849
|
+
if (!availableWhen)
|
|
850
|
+
return true;
|
|
851
|
+
const state = binding.state();
|
|
852
|
+
if (state === undefined)
|
|
853
|
+
return false;
|
|
854
|
+
try {
|
|
855
|
+
return availableWhen({ state });
|
|
856
|
+
}
|
|
857
|
+
catch (e) {
|
|
858
|
+
logger.warn('AutopilotDriver: availableWhen threw; treating as unavailable:', e);
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
copyFor(owner) {
|
|
863
|
+
var _a;
|
|
864
|
+
const custom = owner ? (_a = this.bindingFor(owner)) === null || _a === void 0 ? void 0 : _a.config.copy : undefined;
|
|
865
|
+
return Object.assign(Object.assign({}, DEFAULT_COPY), custom);
|
|
866
|
+
}
|
|
867
|
+
sessionFor(agentName) {
|
|
868
|
+
const existing = this.sessions.get(agentName);
|
|
869
|
+
if (existing)
|
|
870
|
+
return existing;
|
|
871
|
+
const binding = this.bindingFor(agentName);
|
|
872
|
+
if (!binding)
|
|
873
|
+
return undefined;
|
|
874
|
+
const session = new AutopilotSession({
|
|
875
|
+
agentName,
|
|
876
|
+
binding,
|
|
877
|
+
providerRegistry: this.providerRegistry,
|
|
878
|
+
sessionKey: this.sessionKey,
|
|
879
|
+
working: this.inner,
|
|
880
|
+
claimant: AUTOPILOT_CLAIMANT,
|
|
881
|
+
copy: this.copyFor(agentName),
|
|
882
|
+
});
|
|
883
|
+
this.sessions.set(agentName, session);
|
|
884
|
+
return session;
|
|
885
|
+
}
|
|
886
|
+
record(type, detail) {
|
|
887
|
+
recordMetaEvent(this.sessionKey, type, detail);
|
|
888
|
+
}
|
|
889
|
+
notify() {
|
|
890
|
+
for (const listener of this.listeners) {
|
|
891
|
+
try {
|
|
892
|
+
listener();
|
|
893
|
+
}
|
|
894
|
+
catch (e) {
|
|
895
|
+
logger.error('AutopilotDriver: a subscriber threw', e);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* The autopilot a host installed, if `driver` is one (GENC-1660). The host installs it outermost,
|
|
902
|
+
* so this is the driver it holds; nothing about the autopilot travels through `AiDriver`.
|
|
903
|
+
*
|
|
904
|
+
* @internal
|
|
905
|
+
*/
|
|
906
|
+
export const autopilotOf = (driver) => driver instanceof AutopilotDriver ? driver : undefined;
|