@genesislcap/ai-assistant 15.86.1 → 15.87.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.
Files changed (84) hide show
  1. package/dist/ai-assistant.api.json +5026 -645
  2. package/dist/ai-assistant.d.ts +802 -0
  3. package/dist/chat-driver.cjs +1487 -132
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +1480 -132
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +2779 -657
  8. package/dist/dts/autopilot/autopilot-session.d.ts +25 -0
  9. package/dist/dts/autopilot/autopilot-session.d.ts.map +1 -1
  10. package/dist/dts/chat-driver-node.d.ts +2 -0
  11. package/dist/dts/chat-driver-node.d.ts.map +1 -1
  12. package/dist/dts/components/ai-driver/ai-driver.d.ts +15 -0
  13. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  14. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +6 -0
  15. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
  16. package/dist/dts/components/chat-driver/chat-driver.d.ts +7 -0
  17. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  18. package/dist/dts/components/chat-driver/interaction-controller.d.ts +12 -0
  19. package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -1
  20. package/dist/dts/headless/describe-interaction.d.ts +17 -0
  21. package/dist/dts/headless/describe-interaction.d.ts.map +1 -0
  22. package/dist/dts/headless/headless-debug.d.ts +28 -0
  23. package/dist/dts/headless/headless-debug.d.ts.map +1 -0
  24. package/dist/dts/headless/headless-persistence.d.ts +35 -0
  25. package/dist/dts/headless/headless-persistence.d.ts.map +1 -0
  26. package/dist/dts/headless/headless-session.d.ts +180 -0
  27. package/dist/dts/headless/headless-session.d.ts.map +1 -0
  28. package/dist/dts/headless/headless.types.d.ts +435 -0
  29. package/dist/dts/headless/headless.types.d.ts.map +1 -0
  30. package/dist/dts/headless/index.d.ts +5 -0
  31. package/dist/dts/headless/index.d.ts.map +1 -0
  32. package/dist/dts/headless-server/compact.d.ts +52 -0
  33. package/dist/dts/headless-server/compact.d.ts.map +1 -0
  34. package/dist/dts/headless-server/index.d.ts +23 -0
  35. package/dist/dts/headless-server/index.d.ts.map +1 -0
  36. package/dist/dts/headless-server/server.d.ts +13 -0
  37. package/dist/dts/headless-server/server.d.ts.map +1 -0
  38. package/dist/dts/headless-server/types.d.ts +210 -0
  39. package/dist/dts/headless-server/types.d.ts.map +1 -0
  40. package/dist/dts/index.d.ts +2 -0
  41. package/dist/dts/index.d.ts.map +1 -1
  42. package/dist/dts/main/main.d.ts.map +1 -1
  43. package/dist/dts/state/debug-event-log.d.ts +1 -1
  44. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  45. package/dist/dts/state/decision-surface.d.ts +5 -4
  46. package/dist/dts/state/decision-surface.d.ts.map +1 -1
  47. package/dist/dts/state/persistence/debug-log-assembly.d.ts +49 -0
  48. package/dist/dts/state/persistence/debug-log-assembly.d.ts.map +1 -0
  49. package/dist/dts/state/persistence/index.d.ts +1 -0
  50. package/dist/dts/state/persistence/index.d.ts.map +1 -1
  51. package/dist/dts/state/persistence/session-persister.d.ts +7 -0
  52. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  53. package/dist/dts/types/interaction-surface.d.ts +30 -0
  54. package/dist/dts/types/interaction-surface.d.ts.map +1 -1
  55. package/dist/esm/autopilot/autopilot-session.js +31 -16
  56. package/dist/esm/chat-driver-node.js +8 -0
  57. package/dist/esm/components/ai-driver/base-driver-decorator.js +12 -0
  58. package/dist/esm/components/chat-driver/chat-driver.js +8 -0
  59. package/dist/esm/components/chat-driver/interaction-controller.js +66 -0
  60. package/dist/esm/headless/describe-interaction.js +55 -0
  61. package/dist/esm/headless/headless-debug.js +62 -0
  62. package/dist/esm/headless/headless-persistence.js +192 -0
  63. package/dist/esm/headless/headless-session.js +808 -0
  64. package/dist/esm/headless/headless.types.js +1 -0
  65. package/dist/esm/headless/index.js +3 -0
  66. package/dist/esm/headless-server/compact.js +160 -0
  67. package/dist/esm/headless-server/index.js +21 -0
  68. package/dist/esm/headless-server/server.js +746 -0
  69. package/dist/esm/headless-server/types.js +3 -0
  70. package/dist/esm/index.js +3 -0
  71. package/dist/esm/main/main.js +18 -37
  72. package/dist/esm/state/debug-event-log.js +6 -1
  73. package/dist/esm/state/decision-surface.js +75 -48
  74. package/dist/esm/state/persistence/debug-log-assembly.js +69 -0
  75. package/dist/esm/state/persistence/index.js +1 -0
  76. package/dist/esm/state/persistence/session-persister.js +16 -0
  77. package/dist/headless-server.cjs +853 -0
  78. package/dist/headless-server.cjs.map +7 -0
  79. package/dist/headless-server.mjs +819 -0
  80. package/dist/headless-server.mjs.map +7 -0
  81. package/package.json +28 -18
  82. package/scripts/build-headless-server-node.mjs +42 -0
  83. package/scripts/smoke-chat-driver-node.mjs +3 -0
  84. package/scripts/smoke-headless-server-node.mjs +112 -0
@@ -0,0 +1,808 @@
1
+ import { __awaiter } from "tslib";
2
+ import { byNumberOf, fieldsExample, invokeArgsFor } from '../autopilot/autopilot-session';
3
+ import { capRead, visibleTranscript } from '../autopilot/decision-message';
4
+ import { recordMetaEvent } from '../state/debug-event-log';
5
+ import { checkArgs, unavailableReason } from '../state/decision-surface';
6
+ import { logger } from '../utils/logger';
7
+ import { sumUsage } from '../utils/sum-usage';
8
+ import { describeInteraction } from './describe-interaction';
9
+ import { headlessCurrentDebugLog } from './headless-debug';
10
+ /** The holder id the session's interceptor is installed under: it observes, and never claims. */
11
+ const HEADLESS_INTERCEPTOR_ID = 'headless';
12
+ /**
13
+ * Stands in for a claimant when the transcript is rendered: the session is nobody's delegate, so
14
+ * every message reads as the person sees it, and none is dropped as "its own".
15
+ */
16
+ const NOT_A_CLAIMANT = HEADLESS_INTERCEPTOR_ID;
17
+ /** How long one `message` line or a turn's text may run, in characters. */
18
+ const LINE_CAP_CHARS = 4000;
19
+ /** How many events the log keeps; the oldest go first. A `wait` cursor older than that resumes at the oldest. */
20
+ const MAX_EVENTS = 5000;
21
+ /** How often an open turn is checked for having ended, when there is no bus to say so. */
22
+ const TURN_POLL_MS = 50;
23
+ /** The longest delay a timer holds: a larger one fires at once, with a warning. */
24
+ const MAX_TIMER_MS = 2147483647;
25
+ /** `text` for a log line, cut to the cap, with its full length when it was. */
26
+ const capped = (text) => text.length > LINE_CAP_CHARS
27
+ ? { text: `${text.slice(0, LINE_CAP_CHARS)}…`, chars: text.length }
28
+ : { text };
29
+ /** A wait's `maxWaitMs` as a timer can take it: not a number is 0, and it is cut to what a timer holds. */
30
+ const timerMs = (ms) => Number.isNaN(Number(ms)) ? 0 : Math.min(Math.max(0, Number(ms)), MAX_TIMER_MS);
31
+ const messageOf = (e) => (e instanceof Error ? e.message : String(e));
32
+ /** Whether `value` has the shape of an autopilot: what `AutopilotHost` asks for, less its copy. */
33
+ const isAutopilotLike = (value) => {
34
+ const v = value;
35
+ return (!!v &&
36
+ typeof v === 'object' &&
37
+ typeof v.subscribe === 'function' &&
38
+ typeof v.pause === 'function' &&
39
+ typeof v.resume === 'function' &&
40
+ typeof v.stop === 'function' &&
41
+ typeof v.state === 'object' &&
42
+ v.state !== null);
43
+ };
44
+ /**
45
+ * A driver with no screen: a way for a host with no browser (a coding agent behind a small HTTP
46
+ * server, a test) to run a whole assistant journey as the person (GENC-1660).
47
+ *
48
+ * It watches the driver for interactions and turns the conversation into an append-only log of
49
+ * plain-data events (see {@link HeadlessEvent}): `decision` when the person has something to
50
+ * answer, `working` when someone else has it, `settled`, the visible `message`s, the end of each
51
+ * `turn`, the autopilot's state, and the host's own `probe`. The caller reads the log with
52
+ * {@link HeadlessSession.wait}, a long poll cursored by `seq`, so nothing is lost between calls.
53
+ * It answers with {@link HeadlessSession.act}, which runs a widget's action as a click would, with
54
+ * the same numbered options the autopilot is shown.
55
+ *
56
+ * It only observes: its interceptor never claims an interaction, so an autopilot on the same
57
+ * driver works as it always does, and what the session reports as a `decision` is what the autopilot
58
+ * has handed to the person. Plain classes only: no DOM, no FAST.
59
+ *
60
+ * @beta
61
+ */
62
+ export class HeadlessSession {
63
+ constructor(driver, options = {}) {
64
+ var _a, _b, _c, _d, _e, _f, _g;
65
+ this.driver = driver;
66
+ this.options = options;
67
+ this.events = [];
68
+ this.seq = 0;
69
+ this.tracked = new Map();
70
+ /** What the session did to an interaction itself, for `settled` to say when the driver reports it. */
71
+ this.origin = new Map();
72
+ this.waiters = new Set();
73
+ this.teardown = [];
74
+ this.lastProbe = {};
75
+ this.lastAutopilot = '';
76
+ this.disposed = false;
77
+ if (typeof driver.addInteractionInterceptor !== 'function') {
78
+ throw new Error('HeadlessSession needs a driver that supports interaction interceptors (addInteractionInterceptor), and this one does not.');
79
+ }
80
+ this.sessionKey = (_c = (_a = options.sessionKey) !== null && _a !== void 0 ? _a : (_b = options.persistence) === null || _b === void 0 ? void 0 : _b.stateKey) !== null && _c !== void 0 ? _c : '';
81
+ this.allowRaw = (_d = options.allowRaw) !== null && _d !== void 0 ? _d : true;
82
+ this.clock = (_e = options.clock) !== null && _e !== void 0 ? _e : Date.now;
83
+ this.seen = this.history().length;
84
+ this.base = this.seen;
85
+ this.lastProbe = this.readProbe();
86
+ this.teardown.push(driver.addInteractionInterceptor({
87
+ id: HEADLESS_INTERCEPTOR_ID,
88
+ onInteractionOpened: (opened) => {
89
+ this.onOpened(opened);
90
+ // An observer: it never claims.
91
+ return undefined;
92
+ },
93
+ }));
94
+ this.listen('history-updated', () => this.onHistory());
95
+ this.listen('interaction-settled', (e) => this.onSettled(e
96
+ .detail));
97
+ // Turns end whoever started them: heard on the bus when the host shares it, else seen by
98
+ // watching the history and asking the driver whether it is busy.
99
+ if (options.activityBus) {
100
+ this.teardown.push(options.activityBus.subscribe('tool-loop-start', () => this.observe()), options.activityBus.subscribe('tool-loop-end', (detail) => {
101
+ if ((detail === null || detail === void 0 ? void 0 : detail.failureReason) && this.open)
102
+ this.open.failureReason = detail.failureReason;
103
+ this.observe();
104
+ }));
105
+ }
106
+ const autopilot = (_f = options.autopilot) !== null && _f !== void 0 ? _f : (isAutopilotLike(driver) ? driver : undefined);
107
+ this.autopilotHost = autopilot;
108
+ if (autopilot) {
109
+ this.lastAutopilot = this.autopilotSignature();
110
+ this.teardown.push(autopilot.subscribe(() => this.onAutopilot()));
111
+ }
112
+ // After the listeners above, so the persister sees every change the log does.
113
+ this.persistence = (_g = options.persistence) === null || _g === void 0 ? void 0 : _g.attach({ driver, sessionKey: this.sessionKey });
114
+ recordMetaEvent(this.sessionKey, 'headless.attached', options.label !== undefined ? { label: options.label } : undefined);
115
+ }
116
+ // ---------------------------------------------------------------------------------------------
117
+ // The calls
118
+ // ---------------------------------------------------------------------------------------------
119
+ /**
120
+ * Send the person's message, and start the turn. Returns as soon as the turn has started, not
121
+ * when it ends: a turn includes every interaction parked on the person, so it can run as long as
122
+ * the journey. Its end is the `turn` event.
123
+ *
124
+ * Refused, never thrown, when the message is empty, or a turn is already running.
125
+ */
126
+ send(text, options = {}) {
127
+ var _a;
128
+ if (this.disposed)
129
+ return { ok: false, error: 'This session has been disposed.' };
130
+ if (typeof text !== 'string' || !text.trim()) {
131
+ return { ok: false, error: 'There is nothing to send: the message is empty.' };
132
+ }
133
+ if (this.turn || this.driver.isBusy()) {
134
+ return {
135
+ ok: false,
136
+ error: 'A turn is already running. Wait for it to end (the turn event), or answer what it is waiting on.',
137
+ };
138
+ }
139
+ // Idle, but its turn not yet reported: report it before this one begins.
140
+ this.closeTurn();
141
+ const start = this.history().length;
142
+ this.open = { start };
143
+ this.startPolling();
144
+ (_a = this.persistence) === null || _a === void 0 ? void 0 : _a.notePrompt(text);
145
+ let running;
146
+ try {
147
+ running = this.driver
148
+ .sendMessage(text, options.attachments, options.agent !== undefined ? { agent: options.agent } : undefined)
149
+ .then((result) => {
150
+ this.turn = undefined;
151
+ if (this.open) {
152
+ this.open.reason = result.reason;
153
+ this.open.failureReason = result.failureReason;
154
+ }
155
+ this.observe();
156
+ }, (e) => {
157
+ this.turn = undefined;
158
+ this.log({ type: 'error', where: 'turn', message: messageOf(e) });
159
+ if (this.open)
160
+ this.open.reason = 'error';
161
+ this.observe();
162
+ });
163
+ }
164
+ catch (e) {
165
+ this.open = undefined;
166
+ this.stopPolling();
167
+ return { ok: false, error: messageOf(e) };
168
+ }
169
+ this.turn = running;
170
+ return { ok: true };
171
+ }
172
+ /**
173
+ * Run one of a pending interaction's actions as the person, as a click would (the answer is
174
+ * credited `answeredBy: 'user'`). `action` is a number from the description's actions, or an
175
+ * action's name; `args` carry the text, the options (numbers or labels) or the form's fields it
176
+ * takes. Refused, with the reason and what is enabled, when a claimant holds the interaction,
177
+ * the action is unknown or disabled, or its arguments do not fit: nothing runs then.
178
+ *
179
+ * The result is the driver's `InvokeResult`, plus `description` of the interaction when it is
180
+ * still open.
181
+ */
182
+ act(interactionId_1, action_1) {
183
+ return __awaiter(this, arguments, void 0, function* (interactionId, action, args = {}) {
184
+ var _a;
185
+ if (typeof this.driver.invokeAsUser !== 'function') {
186
+ return {
187
+ ok: false,
188
+ error: 'no-surface',
189
+ message: 'This driver cannot run an action as the person (it has no invokeAsUser).',
190
+ };
191
+ }
192
+ const t = this.tracked.get(interactionId);
193
+ const requested = String(action !== null && action !== void 0 ? action : '').trim();
194
+ let name = requested;
195
+ let invokeArgs = args;
196
+ if (t === null || t === void 0 ? void 0 : t.ctx.surface) {
197
+ const surface = t.ctx.surface;
198
+ // The numbers as the description shows them now: the list's order is stable, so this is the
199
+ // list the caller read.
200
+ const numbering = this.describe(t)
201
+ .actions.filter((a) => a.n !== undefined)
202
+ .map((a) => a.name);
203
+ const byNumber = byNumberOf(numbering);
204
+ name = (_a = byNumber(requested)) !== null && _a !== void 0 ? _a : requested;
205
+ const listed = surface.actions().find((a) => a.name === name);
206
+ const built = invokeArgsFor(listed, args, byNumber);
207
+ if (!built.ok) {
208
+ return {
209
+ ok: false,
210
+ error: 'invalid-args',
211
+ message: `fields is not valid JSON. ${fieldsExample(name, listed === null || listed === void 0 ? void 0 : listed.argsSchema)}`,
212
+ };
213
+ }
214
+ invokeArgs = built.args;
215
+ }
216
+ const result = yield this.driver.invokeAsUser(interactionId, name, invokeArgs);
217
+ if (t)
218
+ this.evaluate(t);
219
+ const still = this.tracked.get(interactionId);
220
+ return still && !still.settled ? Object.assign(Object.assign({}, result), { description: this.describe(still) }) : result;
221
+ });
222
+ }
223
+ /**
224
+ * Settle a pending interaction raw, with `result` as the widget would have resolved it,
225
+ * bypassing its actions and everything they check: an escape hatch for a widget with no
226
+ * controller, or one that cannot be acted on. Logs `settled` with `raw: true` and records
227
+ * `headless.raw-resolved`. Refused when `allowRaw` is `false`, when the interaction is not
228
+ * pending, or when a claimant holds it: as for `act`, the person takes it back first.
229
+ */
230
+ resolve(interactionId, result) {
231
+ if (!this.allowRaw) {
232
+ return { ok: false, error: 'Settling an interaction raw is not allowed in this session.' };
233
+ }
234
+ const t = this.tracked.get(interactionId);
235
+ if (!t || t.settled) {
236
+ return { ok: false, error: `No pending interaction "${interactionId}".` };
237
+ }
238
+ if (t.ctx.heldBy !== 'user') {
239
+ return {
240
+ ok: false,
241
+ error: 'Another agent holds this interaction, so it was not resolved. The person has to take it back first, for example by pausing the autopilot.',
242
+ };
243
+ }
244
+ this.origin.set(interactionId, { raw: true });
245
+ try {
246
+ this.driver.resolveInteraction(interactionId, result);
247
+ }
248
+ catch (e) {
249
+ this.origin.delete(interactionId);
250
+ return { ok: false, error: messageOf(e) };
251
+ }
252
+ recordMetaEvent(this.sessionKey, 'headless.raw-resolved', {
253
+ interactionId,
254
+ component: t.componentName,
255
+ });
256
+ return { ok: true };
257
+ }
258
+ /**
259
+ * Read from a pending interaction without changing it: one of its views, or (with `tool`) one of
260
+ * the widget tools given to the session. The text is capped. A view the surface does not list is
261
+ * refused, naming those it does, as the autopilot's `widget_read` does.
262
+ */
263
+ read(interactionId_1) {
264
+ return __awaiter(this, arguments, void 0, function* (interactionId, request = {}) {
265
+ var _a, _b;
266
+ const t = this.tracked.get(interactionId);
267
+ if (!t || t.settled)
268
+ return { ok: false, error: `No pending interaction "${interactionId}".` };
269
+ if (request.tool !== undefined && request.view !== undefined) {
270
+ return { ok: false, error: 'Ask for a view or a tool, not both.' };
271
+ }
272
+ if (request.tool !== undefined)
273
+ return this.readTool(t, request.tool, request.args);
274
+ const { surface } = t.ctx;
275
+ if (!surface)
276
+ return { ok: false, error: 'This widget has nothing to read: it has no surface.' };
277
+ // A blank view is no view, as for the autopilot.
278
+ const view = typeof request.view === 'string' && request.view.trim() ? request.view.trim() : undefined;
279
+ const names = Object.keys((_a = surface.views) !== null && _a !== void 0 ? _a : {});
280
+ if (view !== undefined && names.length && !names.includes(view)) {
281
+ return { ok: false, error: `No view "${view}" here. Views: ${names.join(', ')}.` };
282
+ }
283
+ let state;
284
+ try {
285
+ state = surface.read ? surface.read(view) : surface.facts();
286
+ }
287
+ catch (e) {
288
+ return { ok: false, error: `Could not read: ${messageOf(e)}` };
289
+ }
290
+ if (state === undefined) {
291
+ return {
292
+ ok: false,
293
+ error: `Nothing to read without a view.${names.length ? ` Views: ${names.join(', ')}.` : ''}`,
294
+ };
295
+ }
296
+ return { ok: true, content: capRead((_b = JSON.stringify(state)) !== null && _b !== void 0 ? _b : 'null') };
297
+ });
298
+ }
299
+ /**
300
+ * Wait for the log to say something, then return what it said. Resolves as soon as an event in
301
+ * `until` is in the log after `since` (immediately, when one already is), or at `maxWaitMs` with
302
+ * status `'working'`. Either way it returns every event after `since`, and `nextSeq`, the cursor
303
+ * for the next call: poll with it and nothing is lost.
304
+ */
305
+ wait(request) {
306
+ var _a;
307
+ const since = (_a = request.since) !== null && _a !== void 0 ? _a : 0;
308
+ const { until } = request;
309
+ if (this.events.some((e) => e.seq > since && wanted(e, until))) {
310
+ return Promise.resolve(this.waited(since, 'ready'));
311
+ }
312
+ if (this.disposed)
313
+ return Promise.resolve(this.waited(since, 'closed'));
314
+ const { signal } = request;
315
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted)
316
+ return Promise.resolve(this.waited(since, 'working'));
317
+ return new Promise((resolve) => {
318
+ const onAbort = () => waiter.finish('working');
319
+ const waiter = {
320
+ since,
321
+ until,
322
+ finish: (status) => {
323
+ clearTimeout(timer);
324
+ signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
325
+ this.waiters.delete(waiter);
326
+ resolve(this.waited(since, status));
327
+ },
328
+ };
329
+ this.waiters.add(waiter);
330
+ const timer = setTimeout(() => waiter.finish('working'), timerMs(request.maxWaitMs));
331
+ signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
332
+ });
333
+ }
334
+ /** Every event logged after `since` (default: all the log keeps), without waiting. */
335
+ eventsSince(since = 0) {
336
+ return this.events.filter((e) => e.seq > since);
337
+ }
338
+ /**
339
+ * What is going on now: whether a turn runs, the interaction the person should answer first (and
340
+ * the others open behind it), the autopilot, what it has cost, and the host's probe.
341
+ */
342
+ state() {
343
+ var _a;
344
+ const open = [...this.tracked.values()].filter((t) => t.armed && !t.settled);
345
+ const described = open.map((t) => ({ t, d: this.describe(t) }));
346
+ const mine = described.find(({ d }) => d.heldBy === 'user' && d.ready);
347
+ const others = described
348
+ .filter((entry) => entry !== mine)
349
+ .map(({ d }) => ({
350
+ interactionId: d.interactionId,
351
+ component: d.component,
352
+ heldBy: d.heldBy,
353
+ ready: d.ready,
354
+ }));
355
+ const host = this.autopilotHost;
356
+ return Object.assign(Object.assign({ busy: this.turn !== undefined || this.driver.isBusy() || (host === null || host === void 0 ? void 0 : host.state.working) === true, pending: (_a = mine === null || mine === void 0 ? void 0 : mine.d) !== null && _a !== void 0 ? _a : null, others }, (host
357
+ ? {
358
+ autopilot: {
359
+ mode: host.state.mode,
360
+ status: host.state.status,
361
+ needsUser: host.state.needsUser,
362
+ working: host.state.working,
363
+ },
364
+ }
365
+ : {})), { cost: sumUsage(this.history()), probe: this.readProbe(), lastSeq: this.seq });
366
+ }
367
+ /**
368
+ * Save the session now: the snapshot, and the diagnostics appended since the last flush, rather
369
+ * than at the next throttled save. A no-op without `persistence`.
370
+ */
371
+ flush() {
372
+ return __awaiter(this, void 0, void 0, function* () {
373
+ var _a;
374
+ yield ((_a = this.persistence) === null || _a === void 0 ? void 0 : _a.flush());
375
+ });
376
+ }
377
+ /**
378
+ * The debug log, the document the browser's Download gives. With `persistence`, the log of the
379
+ * whole session stitched from the stored stream with a fresh `meta-snapshot` (`lifetimeDebugLog`,
380
+ * the same code); without, the current page's, from `buildDiagnosticEntries`. Never throws.
381
+ */
382
+ debugLog() {
383
+ if (this.persistence)
384
+ return this.persistence.debugLog();
385
+ return Promise.resolve(headlessCurrentDebugLog(this.driver, this.sessionKey, this.options.debug));
386
+ }
387
+ /** Stop the running turn, when the driver can. */
388
+ stop() {
389
+ if (typeof this.driver.cancel !== 'function') {
390
+ return { ok: false, error: 'This driver cannot cancel a turn.' };
391
+ }
392
+ this.driver.cancel();
393
+ return { ok: true };
394
+ }
395
+ /**
396
+ * Pause, resume or stop the autopilot, when there is one. Pausing hands what it holds to the
397
+ * person, which then appears as a `decision`.
398
+ *
399
+ * `'keepPaused'` is the hand-back banner's "Keep paused": it turns the autopilot's own hand-back
400
+ * of one interaction into an ordinary pause, so it does not take the question back once it is
401
+ * answered. `interactionId` names the interaction handed back; omitted, it is the one the
402
+ * autopilot's own pause is waiting on (`state().autopilot`'s hand-back, the `decision` it left
403
+ * with the person). Refused, with why, when the autopilot has handed nothing back, when
404
+ * `interactionId` is not the one it handed back, or when the host cannot keep paused; the host's
405
+ * own `keepPaused` would do nothing in those cases without saying so.
406
+ */
407
+ autopilot(mode, interactionId) {
408
+ const host = this.autopilotHost;
409
+ if (!host)
410
+ return { ok: false, error: 'There is no autopilot on this driver.' };
411
+ try {
412
+ if (mode === 'keepPaused')
413
+ return this.keepPaused(host, interactionId);
414
+ host[mode]();
415
+ }
416
+ catch (e) {
417
+ return { ok: false, error: messageOf(e) };
418
+ }
419
+ return { ok: true };
420
+ }
421
+ keepPaused(host, interactionId) {
422
+ if (typeof host.keepPaused !== 'function') {
423
+ return { ok: false, error: 'This autopilot cannot keep paused.' };
424
+ }
425
+ const pause = host.state.pause;
426
+ if ((pause === null || pause === void 0 ? void 0 : pause.scope) !== 'this-interaction' || !pause.interactionId) {
427
+ return {
428
+ ok: false,
429
+ error: 'The autopilot has not handed a question back, so there is nothing to keep paused.',
430
+ };
431
+ }
432
+ const handedBack = pause.interactionId;
433
+ if (interactionId !== undefined && interactionId !== handedBack) {
434
+ return {
435
+ ok: false,
436
+ error: `The autopilot handed back ${handedBack}, not ${interactionId}.`,
437
+ };
438
+ }
439
+ host.keepPaused(handedBack);
440
+ return { ok: true };
441
+ }
442
+ /** Detach: remove the interceptor and every subscription, and release anyone waiting. */
443
+ dispose() {
444
+ var _a;
445
+ if (this.disposed)
446
+ return;
447
+ this.disposed = true;
448
+ for (const t of this.tracked.values())
449
+ this.release(t);
450
+ clearTimeout(this.quiesceTimer);
451
+ this.stopPolling();
452
+ (_a = this.persistence) === null || _a === void 0 ? void 0 : _a.dispose();
453
+ for (const off of this.teardown.splice(0)) {
454
+ try {
455
+ off();
456
+ }
457
+ catch (e) {
458
+ logger.warn('HeadlessSession: a teardown threw:', e);
459
+ }
460
+ }
461
+ for (const waiter of this.waiters)
462
+ waiter.finish('closed');
463
+ }
464
+ // ---------------------------------------------------------------------------------------------
465
+ // Watching the driver
466
+ // ---------------------------------------------------------------------------------------------
467
+ listen(type, handler) {
468
+ this.driver.addEventListener(type, handler);
469
+ this.teardown.push(() => this.driver.removeEventListener(type, handler));
470
+ }
471
+ history() {
472
+ var _a, _b, _c;
473
+ return (_c = (_b = (_a = this.driver).getRawHistory) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : [];
474
+ }
475
+ /** An interaction opened: watch it. Who holds it is not decided yet, so it is reported later. */
476
+ onOpened(opened) {
477
+ if (this.disposed)
478
+ return;
479
+ const { ctx } = opened;
480
+ const t = {
481
+ id: opened.interactionId,
482
+ ctx,
483
+ componentName: opened.componentName,
484
+ data: opened.data,
485
+ ownerPath: opened.ownerPath,
486
+ unsubs: [],
487
+ armed: false,
488
+ settled: false,
489
+ autoTried: false,
490
+ };
491
+ this.tracked.set(t.id, t);
492
+ // Holder changes (a claimant handing back), working and readiness all notify.
493
+ t.unsubs.push(ctx.subscribe(() => this.evaluate(t)));
494
+ t.unsubs.push(ctx.onSurfaceReady(() => this.evaluate(t)));
495
+ ctx.onDispose(() => this.onEnded(t));
496
+ // After the open has completed: the interceptors after this one may yet claim it, and the
497
+ // driver holds a resolution made before its message exists, but is not relied on here.
498
+ queueMicrotask(() => {
499
+ if (this.disposed || t.settled)
500
+ return;
501
+ t.armed = true;
502
+ this.evaluate(t);
503
+ });
504
+ }
505
+ /** The driver says an interaction settled, by whatever route. */
506
+ onSettled(detail) {
507
+ const t = this.tracked.get(detail.interactionId);
508
+ if (!t || t.settled)
509
+ return;
510
+ const origin = this.origin.get(t.id);
511
+ this.settle(t, Object.assign(Object.assign(Object.assign(Object.assign({}, (detail.answeredBy ? { answeredBy: detail.answeredBy } : {})), (detail.resolvedItself ? { resolvedItself: true } : {})), ((origin === null || origin === void 0 ? void 0 : origin.auto) ? { auto: true } : {})), ((origin === null || origin === void 0 ? void 0 : origin.raw) ? { raw: true } : {})));
512
+ }
513
+ /** The interaction's context was disposed: if nothing said it settled, say it did. */
514
+ onEnded(t) {
515
+ if (!t.settled)
516
+ this.settle(t, {});
517
+ }
518
+ settle(t, how) {
519
+ t.settled = true;
520
+ if (!this.disposed) {
521
+ this.log(Object.assign({ type: 'settled', interactionId: t.id, component: t.componentName }, how));
522
+ }
523
+ this.release(t);
524
+ }
525
+ release(t) {
526
+ for (const off of t.unsubs.splice(0)) {
527
+ try {
528
+ off();
529
+ }
530
+ catch (_a) {
531
+ // Already gone with its context.
532
+ }
533
+ }
534
+ t.settled = true;
535
+ this.tracked.delete(t.id);
536
+ this.origin.delete(t.id);
537
+ }
538
+ /** New messages in the driver's history: report what the person would see of them. */
539
+ onHistory() {
540
+ if (this.disposed)
541
+ return;
542
+ this.reportMessages();
543
+ this.observe();
544
+ }
545
+ reportMessages() {
546
+ const history = this.history();
547
+ // Shorter than before: a compaction replaced it. Carry on from its end.
548
+ if (history.length < this.seen) {
549
+ this.seen = history.length;
550
+ return;
551
+ }
552
+ const fresh = history.slice(this.seen);
553
+ this.seen = history.length;
554
+ // Interactions are reported as decisions and settlements, not as lines.
555
+ const lines = visibleTranscript(fresh.filter((m) => !m.interaction), { claimant: NOT_A_CLAIMANT });
556
+ for (const line of lines)
557
+ this.log(Object.assign({ type: 'message' }, capped(line)));
558
+ }
559
+ onAutopilot() {
560
+ var _a;
561
+ if (this.disposed || !this.autopilotHost)
562
+ return;
563
+ const signature = this.autopilotSignature();
564
+ if (signature === this.lastAutopilot)
565
+ return;
566
+ this.lastAutopilot = signature;
567
+ const { state } = this.autopilotHost;
568
+ this.log(Object.assign({ type: 'autopilot', mode: state.mode, status: state.status, needsUser: state.needsUser, working: state.working }, (((_a = state.pause) === null || _a === void 0 ? void 0 : _a.reason) ? { reason: state.pause.reason } : {})));
569
+ }
570
+ autopilotSignature() {
571
+ var _a;
572
+ const s = (_a = this.autopilotHost) === null || _a === void 0 ? void 0 : _a.state;
573
+ return s ? JSON.stringify([s.mode, s.status, s.needsUser, s.working]) : '';
574
+ }
575
+ // ---------------------------------------------------------------------------------------------
576
+ // Reporting an interaction
577
+ // ---------------------------------------------------------------------------------------------
578
+ describe(t) {
579
+ return describeInteraction({ ctx: t.ctx, componentName: t.componentName, data: t.data });
580
+ }
581
+ /**
582
+ * Report where an interaction stands, when that has changed. A `decision` when the person holds
583
+ * it and can act; `working` when a claimant holds it or it is not ready yet. A `decision` is
584
+ * logged again only when its description changed materially: who holds it, whether it is ready
585
+ * or has a controller, or which actions are enabled. The text and facts alone do not count.
586
+ */
587
+ evaluate(t) {
588
+ var _a, _b;
589
+ if (this.disposed || !t.armed || t.settled)
590
+ return;
591
+ if (this.autoSettle(t))
592
+ return;
593
+ let d;
594
+ try {
595
+ d = this.describe(t);
596
+ }
597
+ catch (e) {
598
+ this.log({ type: 'error', where: 'describe', message: messageOf(e) });
599
+ return;
600
+ }
601
+ if (d.heldBy === 'user' && d.ready) {
602
+ const sig = JSON.stringify([d.controller, d.actions.map((a) => [a.name, a.enabled])]);
603
+ if (((_a = t.last) === null || _a === void 0 ? void 0 : _a.kind) === 'decision' && t.last.sig === sig)
604
+ return;
605
+ t.last = { kind: 'decision', sig };
606
+ this.log(Object.assign(Object.assign({ type: 'decision' }, d), (t.ownerPath.length ? { ownerPath: t.ownerPath } : {})));
607
+ return;
608
+ }
609
+ const sig = `${d.heldBy}|${d.ready}`;
610
+ if (((_b = t.last) === null || _b === void 0 ? void 0 : _b.kind) === 'working' && t.last.sig === sig)
611
+ return;
612
+ t.last = { kind: 'working', sig };
613
+ this.log({
614
+ type: 'working',
615
+ interactionId: t.id,
616
+ component: t.componentName,
617
+ heldBy: d.heldBy,
618
+ });
619
+ }
620
+ /**
621
+ * Settle a widget that settles itself in a browser, as it would on mounting. Once, and only when
622
+ * the person holds it. Returns whether it settled it.
623
+ */
624
+ autoSettle(t) {
625
+ var _a;
626
+ const result = (_a = this.options.autoSettle) === null || _a === void 0 ? void 0 : _a[t.componentName];
627
+ if (!result || t.autoTried || t.ctx.heldBy !== 'user')
628
+ return false;
629
+ t.autoTried = true;
630
+ try {
631
+ const value = result(t.data);
632
+ this.origin.set(t.id, { auto: true });
633
+ this.driver.resolveInteraction(t.id, value);
634
+ return true;
635
+ }
636
+ catch (e) {
637
+ this.origin.delete(t.id);
638
+ this.log({
639
+ type: 'error',
640
+ where: 'auto-settle',
641
+ message: `${t.componentName}: ${messageOf(e)}`,
642
+ });
643
+ return false;
644
+ }
645
+ }
646
+ readTool(t, name, args) {
647
+ return __awaiter(this, void 0, void 0, function* () {
648
+ var _a, _b;
649
+ const sets = ((_a = this.options.widgetTools) !== null && _a !== void 0 ? _a : []).filter((s) => s.componentName === t.componentName);
650
+ const names = sets.flatMap((s) => Object.keys(s.tools)).sort();
651
+ const spec = sets
652
+ .map((s) => (Object.prototype.hasOwnProperty.call(s.tools, name) ? s.tools[name] : undefined))
653
+ .find((s) => s !== undefined);
654
+ if (!spec) {
655
+ return {
656
+ ok: false,
657
+ error: names.length
658
+ ? `No tool "${name}" for the "${t.componentName}" widget. Tools: ${names.join(', ')}.`
659
+ : `The "${t.componentName}" widget has no tools.`,
660
+ };
661
+ }
662
+ const { controller } = t.ctx;
663
+ if (controller === undefined) {
664
+ return { ok: false, error: `The "${t.componentName}" widget has nothing to read from.` };
665
+ }
666
+ const why = unavailableReason(spec.unavailable, controller);
667
+ if (why !== undefined)
668
+ return { ok: false, error: `Not available right now: ${name}: ${why}.` };
669
+ const problem = checkArgs(args !== null && args !== void 0 ? args : {}, spec.definition.parameters);
670
+ if (problem)
671
+ return { ok: false, error: `Not done: ${name}: ${problem}` };
672
+ try {
673
+ const output = yield spec.run(controller, args !== null && args !== void 0 ? args : {});
674
+ return {
675
+ ok: true,
676
+ content: capRead(typeof output === 'string' ? output : ((_b = JSON.stringify(output)) !== null && _b !== void 0 ? _b : 'null')),
677
+ };
678
+ }
679
+ catch (e) {
680
+ return { ok: false, error: `Could not read: ${messageOf(e)}` };
681
+ }
682
+ });
683
+ }
684
+ // ---------------------------------------------------------------------------------------------
685
+ // The log
686
+ // ---------------------------------------------------------------------------------------------
687
+ /**
688
+ * Notice a turn begin or end, whoever started it. The driver being busy (or a `send` in flight)
689
+ * is a turn; once it has been idle for a beat the turn is over and is reported, once. The beat
690
+ * covers the gaps inside one turn, such as a hand-off continuing it, which are a microtask long.
691
+ * A turn that begins the instant another ends, with no idle between, is reported as one.
692
+ */
693
+ observe() {
694
+ if (this.disposed)
695
+ return;
696
+ if (this.turn || this.driver.isBusy()) {
697
+ clearTimeout(this.quiesceTimer);
698
+ this.quiesceTimer = undefined;
699
+ if (!this.open) {
700
+ this.open = { start: this.base };
701
+ this.startPolling();
702
+ }
703
+ return;
704
+ }
705
+ if (this.open && this.quiesceTimer === undefined) {
706
+ this.quiesceTimer = setTimeout(() => {
707
+ this.quiesceTimer = undefined;
708
+ this.closeTurn();
709
+ }, 0);
710
+ }
711
+ }
712
+ /** Report the open turn as ended, unless the driver is busy again. */
713
+ closeTurn() {
714
+ var _a;
715
+ const { open } = this;
716
+ if (this.disposed || !open || this.turn || this.driver.isBusy())
717
+ return;
718
+ clearTimeout(this.quiesceTimer);
719
+ this.quiesceTimer = undefined;
720
+ this.open = undefined;
721
+ this.stopPolling();
722
+ this.logTurn((_a = open.reason) !== null && _a !== void 0 ? _a : 'done', open.failureReason, open.start);
723
+ this.base = this.history().length;
724
+ }
725
+ /** Without a shared bus nothing says a turn ended, so while one is open the driver is asked. */
726
+ startPolling() {
727
+ var _a, _b;
728
+ if (this.options.activityBus || this.poller !== undefined)
729
+ return;
730
+ this.poller = setInterval(() => this.observe(), TURN_POLL_MS);
731
+ (_b = (_a = this.poller).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
732
+ }
733
+ stopPolling() {
734
+ if (this.poller !== undefined)
735
+ clearInterval(this.poller);
736
+ this.poller = undefined;
737
+ }
738
+ logTurn(reason, failureReason, start) {
739
+ const text = finalText(this.history().slice(start));
740
+ this.log(Object.assign(Object.assign({ type: 'turn', reason }, (failureReason ? { failureReason } : {})), (text !== undefined ? capped(text) : {})));
741
+ }
742
+ log(event) {
743
+ this.append(event);
744
+ this.checkProbe();
745
+ }
746
+ append(event) {
747
+ this.seq += 1;
748
+ const entry = Object.assign(Object.assign({}, event), { seq: this.seq, at: new Date(this.clock()).toISOString() });
749
+ this.events.push(entry);
750
+ if (this.events.length > MAX_EVENTS)
751
+ this.events.splice(0, this.events.length - MAX_EVENTS);
752
+ for (const waiter of this.waiters) {
753
+ if (wanted(entry, waiter.until))
754
+ waiter.finish('ready');
755
+ }
756
+ }
757
+ readProbe() {
758
+ var _a, _b;
759
+ try {
760
+ const read = (_b = (_a = this.options).probe) === null || _b === void 0 ? void 0 : _b.call(_a);
761
+ return read ? Object.assign({}, read) : {};
762
+ }
763
+ catch (e) {
764
+ logger.warn('HeadlessSession: the probe threw:', e);
765
+ return {};
766
+ }
767
+ }
768
+ /** Read the probe again; a change in any top-level value is a `probe` event. */
769
+ checkProbe() {
770
+ if (!this.options.probe || this.disposed)
771
+ return;
772
+ const now = this.readProbe();
773
+ const keys = new Set([...Object.keys(this.lastProbe), ...Object.keys(now)]);
774
+ const changed = [...keys].filter((k) => JSON.stringify(this.lastProbe[k]) !== JSON.stringify(now[k]));
775
+ this.lastProbe = now;
776
+ if (changed.length)
777
+ this.append({ type: 'probe', probe: now, changed });
778
+ }
779
+ waited(since, status) {
780
+ return { events: this.eventsSince(since), status, nextSeq: this.seq };
781
+ }
782
+ }
783
+ /** Whether `event` ends a wait on `until`; by default, anything that needs the caller. */
784
+ function wanted(event, until) {
785
+ if (until)
786
+ return until.includes(event.type);
787
+ return (event.type === 'decision' ||
788
+ event.type === 'turn' ||
789
+ event.type === 'error' ||
790
+ (event.type === 'autopilot' && event.needsUser));
791
+ }
792
+ /** What the assistant last said that the person sees: its final text for the turn, if any. */
793
+ function finalText(messages) {
794
+ var _a, _b;
795
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
796
+ const m = messages[i];
797
+ if (m.role === 'assistant' &&
798
+ !m.interaction &&
799
+ !((_a = m.toolCalls) === null || _a === void 0 ? void 0 : _a.length) &&
800
+ !m.thinking &&
801
+ m.category !== 'reasoning' &&
802
+ m.category !== 'narration' &&
803
+ ((_b = m.content) === null || _b === void 0 ? void 0 : _b.trim())) {
804
+ return m.content;
805
+ }
806
+ }
807
+ return undefined;
808
+ }