@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.
- package/dist/ai-assistant.api.json +5026 -645
- package/dist/ai-assistant.d.ts +802 -0
- package/dist/chat-driver.cjs +1487 -132
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +1480 -132
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +2779 -657
- package/dist/dts/autopilot/autopilot-session.d.ts +25 -0
- package/dist/dts/autopilot/autopilot-session.d.ts.map +1 -1
- package/dist/dts/chat-driver-node.d.ts +2 -0
- package/dist/dts/chat-driver-node.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts +15 -0
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +6 -0
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +7 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/interaction-controller.d.ts +12 -0
- package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -1
- package/dist/dts/headless/describe-interaction.d.ts +17 -0
- package/dist/dts/headless/describe-interaction.d.ts.map +1 -0
- package/dist/dts/headless/headless-debug.d.ts +28 -0
- package/dist/dts/headless/headless-debug.d.ts.map +1 -0
- package/dist/dts/headless/headless-persistence.d.ts +35 -0
- package/dist/dts/headless/headless-persistence.d.ts.map +1 -0
- package/dist/dts/headless/headless-session.d.ts +180 -0
- package/dist/dts/headless/headless-session.d.ts.map +1 -0
- package/dist/dts/headless/headless.types.d.ts +435 -0
- package/dist/dts/headless/headless.types.d.ts.map +1 -0
- package/dist/dts/headless/index.d.ts +5 -0
- package/dist/dts/headless/index.d.ts.map +1 -0
- package/dist/dts/headless-server/compact.d.ts +52 -0
- package/dist/dts/headless-server/compact.d.ts.map +1 -0
- package/dist/dts/headless-server/index.d.ts +23 -0
- package/dist/dts/headless-server/index.d.ts.map +1 -0
- package/dist/dts/headless-server/server.d.ts +13 -0
- package/dist/dts/headless-server/server.d.ts.map +1 -0
- package/dist/dts/headless-server/types.d.ts +210 -0
- package/dist/dts/headless-server/types.d.ts.map +1 -0
- package/dist/dts/index.d.ts +2 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts.map +1 -1
- 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 +5 -4
- package/dist/dts/state/decision-surface.d.ts.map +1 -1
- package/dist/dts/state/persistence/debug-log-assembly.d.ts +49 -0
- package/dist/dts/state/persistence/debug-log-assembly.d.ts.map +1 -0
- package/dist/dts/state/persistence/index.d.ts +1 -0
- package/dist/dts/state/persistence/index.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-persister.d.ts +7 -0
- package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
- package/dist/dts/types/interaction-surface.d.ts +30 -0
- package/dist/dts/types/interaction-surface.d.ts.map +1 -1
- package/dist/esm/autopilot/autopilot-session.js +31 -16
- package/dist/esm/chat-driver-node.js +8 -0
- package/dist/esm/components/ai-driver/base-driver-decorator.js +12 -0
- package/dist/esm/components/chat-driver/chat-driver.js +8 -0
- package/dist/esm/components/chat-driver/interaction-controller.js +66 -0
- package/dist/esm/headless/describe-interaction.js +55 -0
- package/dist/esm/headless/headless-debug.js +62 -0
- package/dist/esm/headless/headless-persistence.js +192 -0
- package/dist/esm/headless/headless-session.js +808 -0
- package/dist/esm/headless/headless.types.js +1 -0
- package/dist/esm/headless/index.js +3 -0
- package/dist/esm/headless-server/compact.js +160 -0
- package/dist/esm/headless-server/index.js +21 -0
- package/dist/esm/headless-server/server.js +746 -0
- package/dist/esm/headless-server/types.js +3 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/main/main.js +18 -37
- package/dist/esm/state/debug-event-log.js +6 -1
- package/dist/esm/state/decision-surface.js +75 -48
- package/dist/esm/state/persistence/debug-log-assembly.js +69 -0
- package/dist/esm/state/persistence/index.js +1 -0
- package/dist/esm/state/persistence/session-persister.js +16 -0
- package/dist/headless-server.cjs +853 -0
- package/dist/headless-server.cjs.map +7 -0
- package/dist/headless-server.mjs +819 -0
- package/dist/headless-server.mjs.map +7 -0
- package/package.json +28 -18
- package/scripts/build-headless-server-node.mjs +42 -0
- package/scripts/smoke-chat-driver-node.mjs +3 -0
- 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 = (
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
+
}
|