@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
@@ -100,6 +100,32 @@ export function parseFields(fields) {
100
100
  return { ok: false };
101
101
  }
102
102
  }
103
+ /**
104
+ * A function reading a number as the action it stands for in the list last shown (`numbering`, as
105
+ * {@link renderActions} returns it): `'2'` is `numbering[1]`; anything that is not a whole number,
106
+ * or is out of range, stands for nothing. Shared by `widget_act` and the headless driver, so "2"
107
+ * means the same option to both.
108
+ */
109
+ export const byNumberOf = (numbering) => (value) => /^\d+$/.test(value.trim()) ? numbering[Number(value.trim()) - 1] : undefined;
110
+ /**
111
+ * The arguments `widget_act` hands to `invoke` for `listed`, from what was asked. An action taking
112
+ * one free-text argument gets `text`, as `reply` does; a form gets its `fields`, parsed leniently
113
+ * ({@link parseFields}; not ok when they do not parse); anything else its options (a number mapped
114
+ * to its name by `byNumber`) and text, as given. Pure, and shared with the headless driver, so
115
+ * both build the same call from the same words.
116
+ */
117
+ export function invokeArgsFor(listed, given, byNumber) {
118
+ if ((listed === null || listed === void 0 ? void 0 : listed.args) === 'text')
119
+ return { ok: true, args: { text: given.text } };
120
+ if (given.fields !== undefined && given.fields !== null && given.fields !== '') {
121
+ const parsed = parseFields(given.fields);
122
+ return parsed.ok ? { ok: true, args: parsed.value } : { ok: false };
123
+ }
124
+ return {
125
+ ok: true,
126
+ args: Object.assign(Object.assign({}, (given.options ? { options: given.options.map((o) => { var _a; return (_a = byNumber(o)) !== null && _a !== void 0 ? _a : o; }) } : {})), (given.text !== undefined ? { text: given.text } : {})),
127
+ };
128
+ }
103
129
  /**
104
130
  * How to send `fields` for `action`, as a correctly escaped example: the whole `widget_act` call,
105
131
  * with `fields` a JSON object inside a string, built from the action's schema where it has one.
@@ -567,7 +593,7 @@ export class AutopilotSession {
567
593
  }
568
594
  const opened = current.opened;
569
595
  // A number names an option in the list last sent; a name is a platform-owned action.
570
- const byNumber = (value) => /^\d+$/.test(value.trim()) ? current.numbering[Number(value.trim()) - 1] : undefined;
596
+ const byNumber = byNumberOf(current.numbering);
571
597
  const name = (_b = byNumber(String(action !== null && action !== void 0 ? action : ''))) !== null && _b !== void 0 ? _b : String(action !== null && action !== void 0 ? action : '').trim();
572
598
  const listed = (_c = opened.ctx.surface) === null || _c === void 0 ? void 0 : _c.actions().find((a) => a.name === name);
573
599
  // Past the refinement limit, a refining action is withdrawn: not listed, and refused here.
@@ -575,22 +601,11 @@ export class AutopilotSession {
575
601
  const what = name === 'reply' ? 'a written answer' : `"${name}"`;
576
602
  return `Not done: ${what} is not available for this one any more; choose another action or hand it back.\n${relist()}`;
577
603
  }
578
- // An action taking one free-text argument gets `text`, as `reply` does; a form gets its
579
- // `fields`, parsed leniently; anything else its options and text, as given.
580
- let invokeArgs;
581
- if ((listed === null || listed === void 0 ? void 0 : listed.args) === 'text') {
582
- invokeArgs = { text };
583
- }
584
- else if (fields !== undefined && fields !== null && fields !== '') {
585
- const parsed = parseFields(fields);
586
- if (!parsed.ok) {
587
- return `Not done: fields is not valid JSON. ${fieldsExample(name, listed === null || listed === void 0 ? void 0 : listed.argsSchema)}\n${relist()}`;
588
- }
589
- invokeArgs = parsed.value;
590
- }
591
- else {
592
- invokeArgs = Object.assign(Object.assign({}, (options ? { options: options.map((o) => { var _a; return (_a = byNumber(o)) !== null && _a !== void 0 ? _a : o; }) } : {})), (text !== undefined ? { text } : {}));
604
+ const built = invokeArgsFor(listed, { options, text, fields }, byNumber);
605
+ if (!built.ok) {
606
+ return `Not done: fields is not valid JSON. ${fieldsExample(name, listed === null || listed === void 0 ? void 0 : listed.argsSchema)}\n${relist()}`;
593
607
  }
608
+ const invokeArgs = built.args;
594
609
  const result = yield opened.hold.invoke(name, invokeArgs, { note });
595
610
  if (!result.ok) {
596
611
  return `Not done: ${result.message}\n${relist()}`;
@@ -23,6 +23,11 @@ export * from './channel/ai-activity-bus';
23
23
  export * from './config/config';
24
24
  export * from './config/define-stateful-agent';
25
25
  export * from './config/fallback-agents';
26
+ // The headless session: a way for a host with no browser to run a whole assistant journey as the
27
+ // person. DOM-free like everything above: plain classes over the driver, `decision-message` (text
28
+ // only) and `autopilot-session`'s pure argument helpers; the autopilot's FAST hand-back view is
29
+ // not in its closure, and `AutopilotHost` is imported for its type alone.
30
+ export * from './headless';
26
31
  // Provider-construction surface, re-exported from `@genesislcap/foundation-ai` so a headless
27
32
  // consumer builds its registry from the SAME bundled foundation-ai instance the driver uses. That
28
33
  // shared identity matters: `ChatDriver`'s malformed/truncated handling does `instanceof` on the
@@ -113,3 +118,6 @@ export { buildTimelineEntries } from './state/persistence/build-timeline-entries
113
118
  // and cost figures are as old as that. Pass a fresh snapshot of your own through this before
114
119
  // `assembleDebugLog`, or state in your own output that the totals are historical.
115
120
  export { assembleDebugLog, withFreshMetaSnapshot } from './state/persistence/diagnostics';
121
+ // The two derivations of a debug log the browser's element uses (and the headless session), so a
122
+ // host assembling its own log does not write a second one. DOM-free.
123
+ export { buildDiagnosticEntries, lifetimeDebugLog } from './state/persistence/debug-log-assembly';
@@ -176,6 +176,18 @@ export class BaseDriverDecorator extends EventTarget {
176
176
  var _a, _b, _c;
177
177
  return (_c = (_b = (_a = this.inner).addInteractionInterceptor) === null || _b === void 0 ? void 0 : _b.call(_a, interceptor)) !== null && _c !== void 0 ? _c : (() => undefined);
178
178
  }
179
+ /**
180
+ * Forwarded: the interaction belongs to the driver that opened it. An inner driver that cannot
181
+ * run actions answers as if the interaction had ended.
182
+ */
183
+ invokeAsUser(interactionId, action, args) {
184
+ var _a, _b, _c;
185
+ return ((_c = (_b = (_a = this.inner).invokeAsUser) === null || _b === void 0 ? void 0 : _b.call(_a, interactionId, action, args)) !== null && _c !== void 0 ? _c : Promise.resolve({
186
+ ok: false,
187
+ error: 'not-held',
188
+ message: 'This interaction has already ended.',
189
+ }));
190
+ }
179
191
  presentInteraction(componentName, data, options) {
180
192
  var _a, _b, _c;
181
193
  return ((_c = (_b = (_a = this.inner).presentInteraction) === null || _b === void 0 ? void 0 : _b.call(_a, componentName, data, options)) !== null && _c !== void 0 ? _c : Promise.reject(new Error('presentInteraction: the inner driver cannot show interactions.')));
@@ -1852,6 +1852,14 @@ export class ChatDriver extends EventTarget {
1852
1852
  addInteractionInterceptor(interceptor) {
1853
1853
  return this.interactions.addInterceptor(interceptor);
1854
1854
  }
1855
+ /**
1856
+ * {@inheritDoc AiDriver.invokeAsUser}
1857
+ *
1858
+ * @beta
1859
+ */
1860
+ invokeAsUser(interactionId, action, args) {
1861
+ return this.interactions.invokeAsUser(interactionId, action, args);
1862
+ }
1855
1863
  /**
1856
1864
  * {@inheritDoc AiDriver.presentInteraction}
1857
1865
  *
@@ -774,6 +774,72 @@ export class InteractionController {
774
774
  (_f = this.contexts.get(interactionId)) === null || _f === void 0 ? void 0 : _f.dispose();
775
775
  this.contexts.delete(interactionId);
776
776
  }
777
+ /**
778
+ * See `ChatDriver.invokeAsUser`: `hold.invoke`'s sibling for the person (GENC-1660).
779
+ *
780
+ * Runs the action the way the widget's own button does, and records nothing against a claimant:
781
+ * no `settledBy`, no `decision`. So `whoAnswered` credits a settle to the person
782
+ * (`answeredBy: 'user'`), exactly as a click would be. Refused unless the person holds the
783
+ * interaction: a claimant that does is not pushed aside, the person takes it back first. An
784
+ * editing action leaves no edit record either — those mark what a claimant changed on the
785
+ * person's behalf, and this is the person's own change.
786
+ */
787
+ invokeAsUser(interactionId, action, args) {
788
+ return __awaiter(this, void 0, void 0, function* () {
789
+ var _a;
790
+ const pending = this.pending.get(interactionId);
791
+ const handle = this.contexts.get(interactionId);
792
+ if (!pending || !handle) {
793
+ return { ok: false, error: 'not-held', message: 'This interaction has already ended.' };
794
+ }
795
+ if (handle.context.heldBy !== 'user') {
796
+ return {
797
+ ok: false,
798
+ error: 'not-held',
799
+ message: 'Another agent holds this interaction, so it was not acted on. The person has to take it back first, for example by pausing the autopilot.',
800
+ };
801
+ }
802
+ const { surface } = handle;
803
+ if (!surface) {
804
+ return { ok: false, error: 'no-surface', message: 'Nothing has been exposed here yet.' };
805
+ }
806
+ // One action at a time, as for a claimant: a second run would start while the first is still
807
+ // in flight. Refused, with nothing run.
808
+ if (pending.running) {
809
+ return Object.assign(Object.assign({ ok: false }, BUSY), { actions: surface.actions() });
810
+ }
811
+ const prepared = surface.prepare(action, args);
812
+ if ('error' in prepared) {
813
+ return Object.assign(Object.assign({ ok: false }, prepared), { actions: surface.actions() });
814
+ }
815
+ const token = {};
816
+ pending.running = { token, finishes: prepared.finishes, note: undefined };
817
+ let output;
818
+ try {
819
+ const ran = prepared.run();
820
+ output = isThenable(ran) ? yield ran : ran;
821
+ }
822
+ catch (e) {
823
+ logger.warn(`InteractionController: action "${action}" threw:`, e);
824
+ return { ok: false, error: 'threw', message: e instanceof Error ? e.message : String(e) };
825
+ }
826
+ finally {
827
+ if (((_a = pending.running) === null || _a === void 0 ? void 0 : _a.token) === token)
828
+ pending.running = undefined;
829
+ }
830
+ // Settled, whichever way it got there: the answer is the person's regardless, so unlike a
831
+ // claimant's run there is no run token to tell this run's settle from another's. One held back
832
+ // until its message is appended (`early`) is settled too, as soon as it is.
833
+ const finished = !this.pending.has(interactionId) || pending.early !== undefined;
834
+ recordMetaEvent(this.deps.sessionKey, 'interaction.invoked', {
835
+ interactionId,
836
+ by: 'user',
837
+ action,
838
+ finished,
839
+ });
840
+ return Object.assign({ ok: true, finished, actions: finished ? [] : surface.actions() }, (output !== undefined && !finished ? { output } : {}));
841
+ });
842
+ }
777
843
  /**
778
844
  * See `ChatDriver.recordExternalCost`.
779
845
  *
@@ -0,0 +1,55 @@
1
+ import { renderDecision, widgetAsText } from '../autopilot/decision-message';
2
+ /** An action, as a model reads it: only what is set, so the record stays small. */
3
+ const describeAction = (action, n) => (Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (n !== undefined ? { n } : {})), { name: action.name }), (action.label !== undefined ? { label: action.label } : {})), (action.description !== undefined ? { description: action.description } : {})), { enabled: action.enabled }), (action.unavailable !== undefined ? { unavailable: action.unavailable } : {})), (action.args !== undefined ? { args: action.args } : {})), (action.argsSchema !== undefined ? { argsSchema: action.argsSchema } : {})), { finishes: action.finishes }), (action.refines ? { refines: true } : {})));
4
+ /**
5
+ * What the person is being asked, as a compact JSON-serialisable record a model can read and
6
+ * answer (GENC-1660): the same text the autopilot is sent for the decision (`renderDecision`,
7
+ * so there is one rendering of a decision, not two), and its actions as data, numbered as that text
8
+ * numbers them, so "1" means the same option here and there.
9
+ *
10
+ * With no surface — a widget with no controller, or one that has not exposed anything yet — there
11
+ * is nothing to act on: `controller` is `false` (or `true` and not `ready`, while a controller that
12
+ * will expose a surface has not yet), `actions` is empty, and `text` is the widget as text alone.
13
+ *
14
+ * Reads the surface fresh each time, so it describes the widget as it is now.
15
+ *
16
+ * @beta
17
+ */
18
+ export function describeInteraction(input) {
19
+ const { ctx, componentName, data } = input;
20
+ const base = {
21
+ interactionId: ctx.interactionId,
22
+ component: componentName,
23
+ heldBy: ctx.heldBy,
24
+ };
25
+ const { surface } = ctx;
26
+ if (!surface) {
27
+ const hasController = ctx.controller !== undefined;
28
+ return Object.assign(Object.assign({}, base), { controller: hasController,
29
+ // A controller that has not exposed its surface yet will: wait for it. Without one, the
30
+ // widget is all there is.
31
+ ready: !hasController, text: widgetAsText(componentName, data), actions: [] });
32
+ }
33
+ const rendered = renderDecision({
34
+ interactionId: ctx.interactionId,
35
+ componentName,
36
+ data,
37
+ surface,
38
+ });
39
+ const actions = surface.actions().map((a) => {
40
+ const index = rendered.numbering.indexOf(a.name);
41
+ // Only a plain option is numbered, as `renderActions` does; a full-form action of the same
42
+ // name is called by name.
43
+ return describeAction(a, index >= 0 && a.description === undefined ? index + 1 : undefined);
44
+ });
45
+ let facts;
46
+ try {
47
+ const read = surface.facts();
48
+ if (Object.keys(read).length)
49
+ facts = Object.assign({}, read);
50
+ }
51
+ catch (_a) {
52
+ // A facts reader that throws must not hide the decision.
53
+ }
54
+ return Object.assign(Object.assign(Object.assign(Object.assign({}, base), { controller: true, ready: surface.isReady(), text: rendered.text, actions }), (surface.views && Object.keys(surface.views).length ? { views: Object.assign({}, surface.views) } : {})), (facts ? { facts } : {}));
55
+ }
@@ -0,0 +1,62 @@
1
+ import { ChatDriver } from '../components/chat-driver/chat-driver';
2
+ import { buildMetaSnapshot } from '../main/meta-snapshot';
3
+ import { DEBUG_LOG_README } from '../state/debug-event-log';
4
+ import { buildDiagnosticEntries } from '../state/persistence/debug-log-assembly';
5
+ import { assembleDebugLog, } from '../state/persistence/diagnostics';
6
+ import { estimateContextTokens, isContextMeasurable } from '../utils/context-tokens';
7
+ import { stripAgentHandlers } from '../utils/strip-agent-handlers';
8
+ import { sumUsage, totalTokens } from '../utils/sum-usage';
9
+ /** What `meta.host` says when the host did not name itself. */
10
+ const DEFAULT_HOST = 'headless';
11
+ /**
12
+ * The `meta-snapshot` entry for right now, gathered the way the assistant element gathers it
13
+ * (`buildMetaSnapshot` does the assembly; this reads what it reports): the live agent comes from
14
+ * the DRIVER, the transcript from its raw history, and the figures from the transcript, so what a
15
+ * headless host logs is what the browser would. The context window and model, which the element
16
+ * learns from its provider registry, are the host's to say.
17
+ *
18
+ * @beta
19
+ */
20
+ export function headlessMetaSnapshot(driver, options = {}) {
21
+ var _a, _b, _c, _d, _e;
22
+ const history = (_b = (_a = driver.getRawHistory) === null || _a === void 0 ? void 0 : _a.call(driver)) !== null && _b !== void 0 ? _b : [];
23
+ const usage = sumUsage(history);
24
+ const live = driver.getActiveAgent();
25
+ const context = (_c = options.context) === null || _c === void 0 ? void 0 : _c.call(options);
26
+ return buildMetaSnapshot({
27
+ agents: options.agents ? [...options.agents()] : undefined,
28
+ activeAgent: live ? stripAgentHandlers(live) : undefined,
29
+ liveActiveAgent: live,
30
+ activeFoldStack: driver instanceof ChatDriver ? driver.getActiveFoldNames() : undefined,
31
+ activeModel: context === null || context === void 0 ? void 0 : context.model,
32
+ contextTokens: isContextMeasurable(history) ? estimateContextTokens(history) : undefined,
33
+ contextLimit: context === null || context === void 0 ? void 0 : context.contextLimit,
34
+ sessionCostUsd: usage.costUsd,
35
+ sessionTokensConsumed: totalTokens(usage),
36
+ sessionUsage: usage,
37
+ debugState: (_d = options.debugState) === null || _d === void 0 ? void 0 : _d.call(options),
38
+ host: (_e = options.host) !== null && _e !== void 0 ? _e : DEFAULT_HOST,
39
+ });
40
+ }
41
+ /**
42
+ * The diagnostic entries for the session so far, from the same `buildDiagnosticEntries` the
43
+ * browser's element and its persister use.
44
+ *
45
+ * @beta
46
+ */
47
+ export function headlessDiagnosticEntries(driver, stateKey, options = {}) {
48
+ return buildDiagnosticEntries({
49
+ driver,
50
+ stateKey,
51
+ metaSnapshot: () => headlessMetaSnapshot(driver, options),
52
+ });
53
+ }
54
+ /**
55
+ * The current page's debug log: what `HeadlessSession.debugLog()` returns when nothing is
56
+ * persisted, and the fallback when the stored stream cannot be read.
57
+ *
58
+ * @beta
59
+ */
60
+ export function headlessCurrentDebugLog(driver, stateKey, options = {}) {
61
+ return assembleDebugLog(headlessDiagnosticEntries(driver, stateKey, options), DEBUG_LOG_README);
62
+ }
@@ -0,0 +1,192 @@
1
+ import { __awaiter } from "tslib";
2
+ import { lifetimeDebugLog } from '../state/persistence/debug-log-assembly';
3
+ import { deletePersister, getOrCreatePersister } from '../state/persistence/persister-registry';
4
+ import { SessionPersister } from '../state/persistence/session-persister';
5
+ import { lastUserMessageIndex } from '../state/persistence/session-snapshot';
6
+ import { getSessionStore } from '../state/session-store';
7
+ import { estimateContextTokens, isContextMeasurable } from '../utils/context-tokens';
8
+ import { stripAgentHandlers } from '../utils/strip-agent-handlers';
9
+ import { sumUsage } from '../utils/sum-usage';
10
+ import { headlessCurrentDebugLog, headlessDiagnosticEntries, headlessMetaSnapshot, } from './headless-debug';
11
+ /**
12
+ * The session persister, fed from a driver with no element to feed it. Holds what the persister
13
+ * reads of the session store (the transcript, the agent, the pin and the flow owner, the usage and
14
+ * the provider) current from the driver's events, and asks it to save on the events on which the
15
+ * browser's element does: a transcript change, spend recorded after the fact, a checkpoint, a
16
+ * hand-off. Only what the persister needs: none of the element's UI state.
17
+ */
18
+ class Binding {
19
+ constructor(driver, options) {
20
+ this.driver = driver;
21
+ this.options = options;
22
+ this.ref = getSessionStore(this.options.stateKey);
23
+ this.teardown = [];
24
+ this.disposed = false;
25
+ const deps = {
26
+ stateKey: options.stateKey,
27
+ getPersistence: () => ({ enabled: true, provider: options.provider }),
28
+ getAgentNames: () => { var _a, _b; return ((_b = (_a = options.agents) === null || _a === void 0 ? void 0 : _a.call(options)) !== null && _b !== void 0 ? _b : []).map((a) => a.name); },
29
+ getDriver: () => driver,
30
+ getStore: () => this.ref,
31
+ getDiagnosticEntries: () => headlessDiagnosticEntries(driver, options.stateKey, options),
32
+ };
33
+ this.persister = getOrCreatePersister(options.stateKey, () => new SessionPersister(deps));
34
+ this.persister.rebindDeps(deps);
35
+ this.syncAll();
36
+ const on = (type, handler) => {
37
+ driver.addEventListener(type, handler);
38
+ this.teardown.push(() => driver.removeEventListener(type, handler));
39
+ };
40
+ // The element's `wireDriver` triggers, on the same events.
41
+ on('history-updated', () => {
42
+ this.syncTranscript();
43
+ this.persister.scheduleSave();
44
+ this.persister.scheduleDiagnostics();
45
+ });
46
+ on('external-cost-recorded', () => {
47
+ this.syncUsage();
48
+ this.persister.scheduleSave();
49
+ this.persister.scheduleDiagnostics();
50
+ });
51
+ on('checkpoint-saved', () => this.persister.scheduleSave());
52
+ on('agent-changed', (e) => {
53
+ const agent = e.detail;
54
+ this.ref.actions.aiAssistant.setActiveAgent(agent ? stripAgentHandlers(agent) : undefined);
55
+ });
56
+ on('pinned-changed', (e) => {
57
+ const name = e.detail;
58
+ if (this.ref.store.aiAssistant.pinnedAgentName !== name) {
59
+ this.ref.actions.aiAssistant.setPinnedAgentName(name);
60
+ }
61
+ });
62
+ on('flow-owner-changed', (e) => this.onFlowOwnerChanged(e.detail));
63
+ on('provider-changed', (e) => {
64
+ this.ref.actions.aiAssistant.setActiveProviderName(e.detail.name);
65
+ this.syncContext();
66
+ });
67
+ // A hand-off is part of the session, and saved — except one ended as `cleared`.
68
+ for (const type of ['handoff-started', 'handoff-returned', 'handoff-unwound']) {
69
+ on(type, (e) => {
70
+ var _a, _b;
71
+ if (((_b = (_a = e.detail) === null || _a === void 0 ? void 0 : _a.outcome) === null || _b === void 0 ? void 0 : _b.kind) !== 'cleared') {
72
+ this.persister.scheduleSave();
73
+ }
74
+ });
75
+ }
76
+ }
77
+ notePrompt(text) {
78
+ this.pendingPrompt = text;
79
+ }
80
+ flush() {
81
+ return __awaiter(this, void 0, void 0, function* () {
82
+ if (this.disposed)
83
+ return;
84
+ this.syncAll();
85
+ yield this.persister.saveNow();
86
+ yield this.persister.flushDiagnostics();
87
+ });
88
+ }
89
+ debugLog() {
90
+ const { driver, options } = this;
91
+ return lifetimeDebugLog({
92
+ provider: options.provider,
93
+ stateKey: options.stateKey,
94
+ // Land the current delta first, so the log includes it.
95
+ flush: () => this.persister.flushDiagnostics(),
96
+ metaSnapshot: () => headlessMetaSnapshot(driver, options),
97
+ fallback: () => headlessCurrentDebugLog(driver, options.stateKey, options),
98
+ });
99
+ }
100
+ dispose() {
101
+ if (this.disposed)
102
+ return;
103
+ this.disposed = true;
104
+ for (const off of this.teardown.splice(0))
105
+ off();
106
+ // Timers only: the saved session stays where it is.
107
+ deletePersister(this.options.stateKey);
108
+ }
109
+ /**
110
+ * Mirror the flow owner, and the prompt that started the flow, as the element does: a
111
+ * non-resumable tombstone hands that prompt back on return (GENC-1351 §5.9). `flow-owner-changed`
112
+ * fires while a turn is being routed, BEFORE its message is in history, so the prompt the session
113
+ * was told of wins over the last user message in history, which would be the one before.
114
+ */
115
+ onFlowOwnerChanged(name) {
116
+ var _a, _b, _c, _d;
117
+ const { actions, store } = this.ref;
118
+ if (store.aiAssistant.flowOwnerAgentName !== name) {
119
+ actions.aiAssistant.setFlowOwnerAgentName(name);
120
+ }
121
+ if (!name) {
122
+ actions.aiAssistant.setFlowActivationPrompt(null);
123
+ return;
124
+ }
125
+ const history = (_c = (_b = (_a = this.driver).getRawHistory) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : [];
126
+ const prompt = this.pendingPrompt || ((_d = history[lastUserMessageIndex(history)]) === null || _d === void 0 ? void 0 : _d.content);
127
+ if (prompt)
128
+ actions.aiAssistant.setFlowActivationPrompt(prompt);
129
+ }
130
+ syncAll() {
131
+ var _a, _b, _c;
132
+ const { actions } = this.ref;
133
+ const live = this.driver.getActiveAgent();
134
+ actions.aiAssistant.setActiveAgent(live ? stripAgentHandlers(live) : undefined);
135
+ actions.aiAssistant.setActiveProviderName(this.driver.getActiveProviderName());
136
+ const owner = (_c = (_b = (_a = this.driver).getFlowOwner) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : null;
137
+ if (owner !== this.ref.store.aiAssistant.flowOwnerAgentName)
138
+ this.onFlowOwnerChanged(owner);
139
+ this.syncTranscript();
140
+ }
141
+ /** The transcript and what is derived from it, as the element's `messages` setter does. */
142
+ syncTranscript() {
143
+ var _a, _b, _c;
144
+ const history = (_c = (_b = (_a = this.driver).getRawHistory) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : [];
145
+ const { actions } = this.ref;
146
+ actions.aiAssistant.setMessages([...history]);
147
+ // Estimated, not the last raw measurement: see the element's `handleMessagesUpdate`.
148
+ if (isContextMeasurable(history)) {
149
+ actions.aiAssistant.setContextTokens(estimateContextTokens(history));
150
+ }
151
+ this.syncUsage();
152
+ this.syncContext();
153
+ }
154
+ syncUsage() {
155
+ var _a, _b, _c;
156
+ const usage = sumUsage((_c = (_b = (_a = this.driver).getRawHistory) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : []);
157
+ this.ref.actions.aiAssistant.setSessionUsage(usage);
158
+ }
159
+ syncContext() {
160
+ var _a, _b;
161
+ const context = (_b = (_a = this.options).context) === null || _b === void 0 ? void 0 : _b.call(_a);
162
+ const { actions } = this.ref;
163
+ actions.aiAssistant.setContextLimit(context === null || context === void 0 ? void 0 : context.contextLimit);
164
+ actions.aiAssistant.setActiveModel(context === null || context === void 0 ? void 0 : context.model);
165
+ }
166
+ }
167
+ /**
168
+ * Persistence for a `HeadlessSession`: it writes exactly what the browser writes — the
169
+ * `sessions/<key>` snapshot and the `diag/<key>` stream — through the platform's
170
+ * `SessionPersister` and the host's `SessionPersistenceProvider`, and builds the lifetime debug log
171
+ * from the same code as the browser's Download.
172
+ *
173
+ * Exported from the package's main entry only, NOT from the Node entry (`./chat-driver`): it
174
+ * reaches the session store, and through it `@genesislcap/foundation-redux`, which are not
175
+ * Node-clean. The `persister` it makes is the registry's, so a host can restore through
176
+ * `getPersister(stateKey)`.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * const session = new HeadlessSession(driver, {
181
+ * persistence: headlessPersistence({ provider, stateKey: 'project-1', agents: () => agents }),
182
+ * });
183
+ * ```
184
+ *
185
+ * @beta
186
+ */
187
+ export function headlessPersistence(options) {
188
+ return {
189
+ stateKey: options.stateKey,
190
+ attach: (host) => new Binding(host.driver, options),
191
+ };
192
+ }