@genesislcap/ai-assistant 15.26.0 → 15.27.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 +150 -7
- package/dist/ai-assistant.d.ts +82 -99
- package/dist/chat-driver.cjs +460 -373
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +460 -373
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +682 -202
- package/dist/dts/components/chat-driver/chat-driver.d.ts +28 -200
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/interaction-controller.d.ts +99 -0
- package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/turn-recorder.d.ts +191 -0
- package/dist/dts/components/chat-driver/turn-recorder.d.ts.map +1 -0
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts +20 -0
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +32 -1
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts +19 -0
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +7 -1
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-persistence-provider.d.ts +5 -0
- package/dist/dts/state/persistence/session-persistence-provider.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +1 -0
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/dts/utils/format-message-timestamp.d.ts +77 -0
- package/dist/dts/utils/format-message-timestamp.d.ts.map +1 -0
- package/dist/dts/utils/resolve-preference-baseline.d.ts +18 -1
- package/dist/dts/utils/resolve-preference-baseline.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +68 -404
- package/dist/esm/components/chat-driver/interaction-controller.js +235 -0
- package/dist/esm/components/chat-driver/turn-recorder.js +206 -0
- package/dist/esm/components/settings-modal/settings-modal.template.js +53 -2
- package/dist/esm/main/main.js +80 -17
- package/dist/esm/main/main.styles.js +75 -0
- package/dist/esm/main/main.template.js +43 -2
- package/dist/esm/state/ai-assistant-slice.js +5 -0
- package/dist/esm/utils/format-message-timestamp.js +126 -0
- package/dist/esm/utils/resolve-preference-baseline.js +54 -2
- package/package.json +16 -16
- package/sandbox/README.md +45 -2
- package/sandbox/sandbox.ts +95 -3
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { recordMetaEvent } from '../../state/debug-event-log';
|
|
2
|
+
import { createInteractionContext, } from '../../state/interaction-context';
|
|
3
|
+
import { logger } from '../../utils/logger';
|
|
4
|
+
/**
|
|
5
|
+
* Custom UI interactions: parking a tool handler on a widget, and settling it when the user (or a
|
|
6
|
+
* timeout) answers.
|
|
7
|
+
*
|
|
8
|
+
* Owns the pending call and its live context, which are created and destroyed together. Everything
|
|
9
|
+
* else it needs is the driver's history and event target, reached through `InteractionDeps`.
|
|
10
|
+
*/
|
|
11
|
+
export class InteractionController {
|
|
12
|
+
constructor(deps) {
|
|
13
|
+
this.deps = deps;
|
|
14
|
+
this.pending = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* Live per-interaction contexts (GENC-1390), keyed by interactionId. Created in `request`
|
|
17
|
+
* alongside the pending entry, disposed + removed in `resolve`. Non-serialisable (sockets,
|
|
18
|
+
* timers, subscriber sets), so they live here — not on the DOM, not in the persisted store — and
|
|
19
|
+
* therefore survive the element remounts (layout reflow/drag, transcript re-render) that this
|
|
20
|
+
* feature exists to make harmless. At most one is live at a time (see the
|
|
21
|
+
* one-interaction-at-a-time guard in `request`).
|
|
22
|
+
*/
|
|
23
|
+
this.contexts = new Map();
|
|
24
|
+
}
|
|
25
|
+
/** See `ChatDriver.setHostInteractionRequester`. */
|
|
26
|
+
setHostRequester(fn) {
|
|
27
|
+
this.hostRequester = fn;
|
|
28
|
+
}
|
|
29
|
+
/** See `ChatDriver.requestInteraction`. */
|
|
30
|
+
request(componentName, data, options) {
|
|
31
|
+
if (this.hostRequester) {
|
|
32
|
+
return this.hostRequester(componentName, data, options);
|
|
33
|
+
}
|
|
34
|
+
if (this.pending.size > 0) {
|
|
35
|
+
throw new Error('requestInteraction: another user interaction is already in flight. ' +
|
|
36
|
+
'Only one interaction may be active at a time — sequence them in a single tool handler ' +
|
|
37
|
+
'rather than spawning widgets from parallel sub-agents or parallel tool calls.');
|
|
38
|
+
}
|
|
39
|
+
const interactionId = crypto.randomUUID();
|
|
40
|
+
const chatInputDuringExecution = options === null || options === void 0 ? void 0 : options.chatInputDuringExecution;
|
|
41
|
+
const timeoutMs = options === null || options === void 0 ? void 0 : options.timeoutMs;
|
|
42
|
+
const presentation = options === null || options === void 0 ? void 0 : options.presentation;
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
this.pending.set(interactionId, {
|
|
45
|
+
resolve,
|
|
46
|
+
reject,
|
|
47
|
+
overrideId: chatInputDuringExecution ? interactionId : undefined,
|
|
48
|
+
// Resolve the interaction with a timeout result if the user does not
|
|
49
|
+
// respond in time. `resolve` runs the same teardown a normal resolution
|
|
50
|
+
// does (marks the widget read-only, reverts any input override) and
|
|
51
|
+
// clears any still-pending handle.
|
|
52
|
+
timeoutHandle: timeoutMs != null
|
|
53
|
+
? setTimeout(() => {
|
|
54
|
+
this.resolve(interactionId, {
|
|
55
|
+
status: 'timeout',
|
|
56
|
+
message: `Interaction timed out after ${timeoutMs}ms.`,
|
|
57
|
+
});
|
|
58
|
+
}, timeoutMs)
|
|
59
|
+
: undefined,
|
|
60
|
+
});
|
|
61
|
+
recordMetaEvent(this.deps.sessionKey, 'interaction.requested', {
|
|
62
|
+
interactionId,
|
|
63
|
+
component: componentName,
|
|
64
|
+
agent: this.deps.activeAgentName(),
|
|
65
|
+
});
|
|
66
|
+
// Signal the park boundary on the activity bus so turn-aware UI can tell
|
|
67
|
+
// "actively computing" from "parked awaiting the user" — the latter is a
|
|
68
|
+
// safe window for actions disallowed mid-request (e.g. switching provider
|
|
69
|
+
// during a long journey step). Paired with `interaction-resolved`.
|
|
70
|
+
this.deps.activityBus.publish('interaction-requested', undefined);
|
|
71
|
+
if (chatInputDuringExecution) {
|
|
72
|
+
this.deps.emit(new CustomEvent('interaction-start', {
|
|
73
|
+
detail: { interactionId, chatInputDuringExecution },
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
// Create the live context (GENC-1390) BEFORE the widget can mount, so the
|
|
77
|
+
// wrapper finds it when it builds the pending `interaction` descriptor.
|
|
78
|
+
this.contexts.set(interactionId, createInteractionContext(interactionId));
|
|
79
|
+
this.deps.appendToHistory({
|
|
80
|
+
role: 'assistant',
|
|
81
|
+
content: '',
|
|
82
|
+
interaction: Object.assign({ interactionId,
|
|
83
|
+
componentName,
|
|
84
|
+
data }, (presentation ? { presentation } : {})),
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* See `ChatDriver.resolveInteraction`. Warns and returns when the id is unknown — a widget can
|
|
90
|
+
* report twice, and the second report has nothing left to settle.
|
|
91
|
+
*/
|
|
92
|
+
resolve(interactionId, result) {
|
|
93
|
+
var _a;
|
|
94
|
+
const interaction = this.pending.get(interactionId);
|
|
95
|
+
if (!interaction) {
|
|
96
|
+
logger.warn(`Interaction with ID ${interactionId} not found.`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Clear the timeout so a user resolution doesn't later fire a stale
|
|
100
|
+
// timeout (and a timeout firing here clears its own already-fired handle).
|
|
101
|
+
if (interaction.timeoutHandle)
|
|
102
|
+
clearTimeout(interaction.timeoutHandle);
|
|
103
|
+
// Harvest any out-of-band diagnostic entries the widget collated (e.g. from a server-side
|
|
104
|
+
// ChatDriver) so the host's debug log + persisted diagnostics include them. Independent of the
|
|
105
|
+
// message patched below — they describe the widget's own work, not this message.
|
|
106
|
+
this.deps.foldDiagnostics(interactionId, result === null || result === void 0 ? void 0 : result.diagnostics);
|
|
107
|
+
// Fold any widget-reported external (non-LLM) cost onto the message, so
|
|
108
|
+
// the host's `sumCosts` recompute includes it in the session total.
|
|
109
|
+
// Accumulates defensively in case a widget reports more than once.
|
|
110
|
+
const costUsd = typeof (result === null || result === void 0 ? void 0 : result.costUsd) === 'number' && Number.isFinite(result.costUsd) && result.costUsd > 0
|
|
111
|
+
? result.costUsd
|
|
112
|
+
: undefined;
|
|
113
|
+
const patched = this.deps.patchInteractionMessage(interactionId, (message) => {
|
|
114
|
+
var _a;
|
|
115
|
+
return (Object.assign(Object.assign(Object.assign({}, message), { interaction: Object.assign(Object.assign({}, message.interaction), { resolved: result }) }), (costUsd != null ? { externalCostUsd: ((_a = message.externalCostUsd) !== null && _a !== void 0 ? _a : 0) + costUsd } : {})));
|
|
116
|
+
});
|
|
117
|
+
if (patched)
|
|
118
|
+
this.deps.notifyHistoryUpdated();
|
|
119
|
+
// A widget may opt into echoing its outcome on the user's side of the
|
|
120
|
+
// chat by returning `userMessage`. Appended as a display-only
|
|
121
|
+
// `'synthetic-user'` message: it is tracked in history (and the debug log)
|
|
122
|
+
// and rendered like a user message, but transports never serialize it —
|
|
123
|
+
// the model already learns the outcome from this handler's return value.
|
|
124
|
+
// Appended before `interaction.resolve` so it precedes the tool result and
|
|
125
|
+
// any follow-up turn the resumed handler produces.
|
|
126
|
+
if (typeof (result === null || result === void 0 ? void 0 : result.userMessage) === 'string' && result.userMessage.trim()) {
|
|
127
|
+
this.deps.appendToHistory({ role: 'synthetic-user', content: result.userMessage });
|
|
128
|
+
}
|
|
129
|
+
if (interaction.overrideId) {
|
|
130
|
+
this.deps.emit(new CustomEvent('interaction-stop', { detail: { interactionId } }));
|
|
131
|
+
}
|
|
132
|
+
recordMetaEvent(this.deps.sessionKey, 'interaction.resolved', { interactionId });
|
|
133
|
+
// The park is ending and the loop is about to resume computing — paired
|
|
134
|
+
// with `interaction-requested`. Fires for every resolution path (user
|
|
135
|
+
// completion, timeout, cancellation), since all route through here.
|
|
136
|
+
this.deps.activityBus.publish('interaction-resolved', undefined);
|
|
137
|
+
interaction.resolve(result);
|
|
138
|
+
this.pending.delete(interactionId);
|
|
139
|
+
// Tear down the live context on RESOLVE (not on element unmount) — this closes
|
|
140
|
+
// any sockets/timers the widget parked on it via `onDispose`. (GENC-1390)
|
|
141
|
+
(_a = this.contexts.get(interactionId)) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
142
|
+
this.contexts.delete(interactionId);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* See `ChatDriver.recordExternalCost`.
|
|
146
|
+
*
|
|
147
|
+
* **Why this exists separately from `resolve`.** A widget's spend reaches the session total only
|
|
148
|
+
* through `InteractionResult.costUsd`, which is read exactly once — `resolve` does its whole job
|
|
149
|
+
* against a live pending entry and then deletes it. That is correct for the resolution itself,
|
|
150
|
+
* but it assumes a widget stops costing money when it resolves, and several deliberately do not:
|
|
151
|
+
* a widget that resolves EARLY so the assistant stops waiting (options shown, a preview
|
|
152
|
+
* rendered) may stay interactive for minutes afterwards and do more paid work on the user's
|
|
153
|
+
* behalf. Re-emitting `interaction-completed` does not help — with the interaction gone from the
|
|
154
|
+
* map the call is a silent no-op — so that spend had nowhere to go and was simply missing from
|
|
155
|
+
* the figure shown to the user.
|
|
156
|
+
*
|
|
157
|
+
* Touches only `externalCostUsd`: the recorded `interaction.resolved` payload is left exactly as
|
|
158
|
+
* the resolution wrote it, because this is not a second resolution and must not read like one.
|
|
159
|
+
*/
|
|
160
|
+
recordExternalCost(interactionId, costUsd) {
|
|
161
|
+
const drop = (reason) => {
|
|
162
|
+
recordMetaEvent(this.deps.sessionKey, 'interaction.external-cost-dropped', {
|
|
163
|
+
interactionId,
|
|
164
|
+
costUsd,
|
|
165
|
+
reason,
|
|
166
|
+
});
|
|
167
|
+
return false;
|
|
168
|
+
};
|
|
169
|
+
if (!interactionId)
|
|
170
|
+
return drop('no interactionId');
|
|
171
|
+
if (typeof costUsd !== 'number' || !Number.isFinite(costUsd) || costUsd <= 0) {
|
|
172
|
+
return drop('not a positive finite amount');
|
|
173
|
+
}
|
|
174
|
+
const patched = this.deps.patchInteractionMessage(interactionId, (message) => {
|
|
175
|
+
var _a;
|
|
176
|
+
return (Object.assign(Object.assign({}, message), { externalCostUsd: ((_a = message.externalCostUsd) !== null && _a !== void 0 ? _a : 0) + costUsd }));
|
|
177
|
+
});
|
|
178
|
+
// The message is gone (trimmed or condensed): the spend is real and already billed, so it is
|
|
179
|
+
// recorded as dropped rather than silently discarded — an invisible gap in the session total
|
|
180
|
+
// is exactly what this whole channel exists to close.
|
|
181
|
+
if (!patched)
|
|
182
|
+
return drop('interaction message no longer in history');
|
|
183
|
+
recordMetaEvent(this.deps.sessionKey, 'interaction.external-cost', { interactionId, costUsd });
|
|
184
|
+
// A NARROW event, deliberately not `history-updated`.
|
|
185
|
+
//
|
|
186
|
+
// The host answers `history-updated` by reassigning `messages`, which re-evaluates
|
|
187
|
+
// `activeInteractionRow` — and the trailing interaction slot renders through a
|
|
188
|
+
// `repeat(..., { recycle: false })`, so every view in it is disposed and rebuilt regardless of
|
|
189
|
+
// item identity. That is correct for a PENDING interaction (GENC-1429: a recycled wrapper
|
|
190
|
+
// carries the previous interaction's truthy `resolved` and swallows the next completion), and
|
|
191
|
+
// survivable because `buildInteraction` hands a remounted pending widget the same driver-owned
|
|
192
|
+
// `interactionContext`.
|
|
193
|
+
//
|
|
194
|
+
// A RESOLVED one has no such context — `resolve` disposed and deleted it — so announcing this
|
|
195
|
+
// on `history-updated` would tear down and blank the very widget that just reported, in
|
|
196
|
+
// exactly the flow this channel exists for (a user tweaking an option after the interaction
|
|
197
|
+
// resolved). Bookkeeping about a finished turn must stay invisible, so the host recomputes its
|
|
198
|
+
// totals from this driver's history and saves, without touching what is rendered. Reviewer
|
|
199
|
+
// catch on #2474 — confirmed against the layout widget, whose tweak result went off screen
|
|
200
|
+
// because the rebuild raced the session write that would have restored it.
|
|
201
|
+
this.deps.emit(new CustomEvent('external-cost-recorded', {
|
|
202
|
+
detail: { interactionId, costUsd },
|
|
203
|
+
}));
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
/** See `ChatDriver.getInteractionContext`. */
|
|
207
|
+
context(interactionId) {
|
|
208
|
+
var _a;
|
|
209
|
+
return (_a = this.contexts.get(interactionId)) === null || _a === void 0 ? void 0 : _a.context;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Resolve every in-flight interaction with a `'timeout'` result, closing its
|
|
213
|
+
* widget read-only. Used when a sub-agent that owns the open widget times out:
|
|
214
|
+
* the widget is parked in *this* driver's map (interactions resolve on the
|
|
215
|
+
* host, not the child), so cancelling it here unblocks the sub-agent handler
|
|
216
|
+
* awaiting it. The "one interaction at a time" invariant means there is at
|
|
217
|
+
* most one.
|
|
218
|
+
*/
|
|
219
|
+
cancelAll(message) {
|
|
220
|
+
// Snapshot the keys: `resolve` deletes from the map it would otherwise be iterating live.
|
|
221
|
+
for (const interactionId of [...this.pending.keys()]) {
|
|
222
|
+
this.resolve(interactionId, { status: 'timeout', message });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Drop any live interaction contexts on driver teardown. Normally empty (a context is created
|
|
227
|
+
* and destroyed with its pending interaction, and dispose is not reached mid-interaction);
|
|
228
|
+
* disposed defensively so a stray context can never leak an open socket/timer.
|
|
229
|
+
*/
|
|
230
|
+
disposeContexts() {
|
|
231
|
+
for (const handle of this.contexts.values())
|
|
232
|
+
handle.dispose();
|
|
233
|
+
this.contexts.clear();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { __rest } from "tslib";
|
|
2
|
+
import { mergeMetaEvents, recordMetaEvent } from '../../state/debug-event-log';
|
|
3
|
+
/**
|
|
4
|
+
* Everything the exported debug log draws from one driver: its own per-LLM-call snapshots, the
|
|
5
|
+
* ones forwarded up from sub-agents, and the diagnostics harvested from resolved interactions.
|
|
6
|
+
*
|
|
7
|
+
* All three feed the same timeline and all three are keyed in this driver's turn space, which is
|
|
8
|
+
* why they share an owner — a forwarded child turn and a harvested server turn are both re-labelled
|
|
9
|
+
* against the counter kept here.
|
|
10
|
+
*/
|
|
11
|
+
export class TurnRecorder {
|
|
12
|
+
constructor(sessionKey, maxSnapshots) {
|
|
13
|
+
this.sessionKey = sessionKey;
|
|
14
|
+
this.maxSnapshots = maxSnapshots;
|
|
15
|
+
/**
|
|
16
|
+
* Ring buffer of per-LLM-call snapshots, capped at `maxSnapshots`; older entries drop off as
|
|
17
|
+
* new ones arrive.
|
|
18
|
+
*/
|
|
19
|
+
this.buffer = [];
|
|
20
|
+
/** Monotonic counter that survives agent swaps — useful for cross-referencing with history. */
|
|
21
|
+
this.turnIndex = 0;
|
|
22
|
+
/**
|
|
23
|
+
* `turn` + `message` diagnostic entries harvested from an out-of-band source. `event` entries
|
|
24
|
+
* are NOT kept here — they are folded into the session's meta-event registry so they inherit
|
|
25
|
+
* fresh host indices, and reach the log through `getMetaEvents`. Append-only, and never cleared:
|
|
26
|
+
* it lives with the recorder, so a new chat (fresh driver) starts empty while a compaction (same
|
|
27
|
+
* instance) keeps it. (GENC-1461.)
|
|
28
|
+
*/
|
|
29
|
+
this.externalEntries = [];
|
|
30
|
+
/**
|
|
31
|
+
* Monotonic count of external-diagnostics batches folded this driver-lifetime. Namespaces each
|
|
32
|
+
* batch's re-keyed `turn` entries so distinct server generations in one chat session can't
|
|
33
|
+
* collide on `turnIndex` in the forward-capture persister (which dedups turns by that key).
|
|
34
|
+
*/
|
|
35
|
+
this.externalBatches = 0;
|
|
36
|
+
}
|
|
37
|
+
/** Point at the incoming agent's `getDebugSnapshot`, or clear it when the agent supplies none. */
|
|
38
|
+
setSnapshotter(snapshotter) {
|
|
39
|
+
this.snapshotter = snapshotter;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The per-turn snapshots captured so far — what the LLM saw on each turn: system prompt, tool
|
|
43
|
+
* surface, and agent-supplied state (e.g. a machine snapshot).
|
|
44
|
+
*/
|
|
45
|
+
snapshots() {
|
|
46
|
+
return this.buffer;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The harvested `turn` + `message` diagnostic entries. Opaque, and chronologically sorted by the
|
|
50
|
+
* host at assemble time.
|
|
51
|
+
*/
|
|
52
|
+
externalDiagnostics() {
|
|
53
|
+
return this.externalEntries;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Push one snapshot to the ring buffer. Called inside `runToolLoop` just
|
|
57
|
+
* before each LLM call — that's the latest point where the prompt, tool
|
|
58
|
+
* surface, and agent state line up with what the model is about to see.
|
|
59
|
+
*
|
|
60
|
+
* Returns the pushed object so the caller can back-fill what only the response
|
|
61
|
+
* knows (`usage`). Mutating it after the fact is safe whether or not the ring
|
|
62
|
+
* buffer has since evicted it — an evicted snapshot is simply no longer exported.
|
|
63
|
+
*/
|
|
64
|
+
record(turn) {
|
|
65
|
+
let agentSnapshot;
|
|
66
|
+
if (this.snapshotter) {
|
|
67
|
+
try {
|
|
68
|
+
agentSnapshot = this.snapshotter();
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
// A snapshotter throwing must not derail the LLM call — capture the
|
|
72
|
+
// error string in place of the snapshot so the export still shows
|
|
73
|
+
// *something* happened.
|
|
74
|
+
agentSnapshot = `<getDebugSnapshot threw: ${e instanceof Error ? e.message : String(e)}>`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const turnIndex = String(this.turnIndex);
|
|
78
|
+
this.turnIndex += 1;
|
|
79
|
+
const snapshot = Object.assign(Object.assign({}, turn), { turnIndex, timestamp: new Date().toISOString(), agentSnapshot });
|
|
80
|
+
this.buffer.push(snapshot);
|
|
81
|
+
this.trim();
|
|
82
|
+
return snapshot;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Merge a sub-agent's turn snapshots into this driver's buffer so they surface
|
|
86
|
+
* as `kind:'turn'` entries in the exported debug log. The child runs as a
|
|
87
|
+
* separate, discarded driver, so its snapshots would otherwise be lost. Each is
|
|
88
|
+
* re-labelled under the parent turn that activated the sub-agent: the child's
|
|
89
|
+
* own (numeric) turns become `"<parentTurn>-1"`, `"-2"`, … (1-based, in order);
|
|
90
|
+
* any already-forwarded grand-child labels (strings) have their leading segment
|
|
91
|
+
* remapped the same way, so nesting composes (`"5-2"` → `"5-2-1"`).
|
|
92
|
+
*
|
|
93
|
+
* Note: two sub-agents invoked in the *same* parent turn share the prefix, so
|
|
94
|
+
* their labels can repeat — `agentName` on each snapshot disambiguates them.
|
|
95
|
+
*/
|
|
96
|
+
forwardSubAgent(childSnapshots) {
|
|
97
|
+
var _a;
|
|
98
|
+
if (childSnapshots.length === 0)
|
|
99
|
+
return;
|
|
100
|
+
// The activating parent turn = the most recent snapshot this driver recorded
|
|
101
|
+
// before entering the tool handler that invoked the sub-agent.
|
|
102
|
+
const parentTurn = Math.max(0, this.turnIndex - 1);
|
|
103
|
+
const ownTurnLabel = new Map();
|
|
104
|
+
let ownPos = 0;
|
|
105
|
+
for (const snap of childSnapshots) {
|
|
106
|
+
let turnIndex;
|
|
107
|
+
if (!snap.turnIndex.includes('-')) {
|
|
108
|
+
// The child's own turn (a bare counter) → number it under the parent turn.
|
|
109
|
+
ownPos += 1;
|
|
110
|
+
turnIndex = `${parentTurn}-${ownPos}`;
|
|
111
|
+
ownTurnLabel.set(snap.turnIndex, turnIndex);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// An already-forwarded grand-child label — remap its leading segment.
|
|
115
|
+
const [lead, ...rest] = snap.turnIndex.split('-');
|
|
116
|
+
const leadLabel = (_a = ownTurnLabel.get(lead)) !== null && _a !== void 0 ? _a : `${parentTurn}-${lead}`;
|
|
117
|
+
turnIndex = [leadLabel, ...rest].join('-');
|
|
118
|
+
}
|
|
119
|
+
this.buffer.push(Object.assign(Object.assign({}, snap), { turnIndex }));
|
|
120
|
+
}
|
|
121
|
+
this.trim();
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Fold a widget's out-of-band diagnostics into this session — typically a server-side
|
|
125
|
+
* ChatDriver's collated log, arriving on `InteractionResult.diagnostics` — so the host's debug
|
|
126
|
+
* log and persisted diagnostics stream include them. Independent of the interaction's own
|
|
127
|
+
* message: these describe the widget's work, not that message. A non-array or empty harvest is
|
|
128
|
+
* a no-op.
|
|
129
|
+
*/
|
|
130
|
+
foldDiagnostics(interactionId, harvested) {
|
|
131
|
+
// The harvested entries carry the SOURCE engine's own index space (0-based turns/events), which
|
|
132
|
+
// would collide with this host's cursors in the forward-capture persister — turns dedup by
|
|
133
|
+
// `turnIndex`, events by an `index` high-water mark — so foreign entries get silently dropped
|
|
134
|
+
// from the persisted stream. Route each kind through the host's own collision-safe machinery,
|
|
135
|
+
// mirroring how a sub-agent's log is folded in (see `invokeSubAgent`):
|
|
136
|
+
// • `event` → `mergeMetaEvents`, which re-indexes them onto this session's monotonic event
|
|
137
|
+
// counter (clearing the high-water) while preserving their timestamps; they then
|
|
138
|
+
// reach the log via `getMetaEvents` in `buildTimelineEntries`, not this buffer.
|
|
139
|
+
// • `turn` → re-key `turnIndex` to the turn's local position within a per-batch namespace,
|
|
140
|
+
// so turns stay unique even when one interaction folds in several independent
|
|
141
|
+
// server runs that each number their own turns from 0 (a consolidator carries a
|
|
142
|
+
// consolidator-code run AND a table-code run), and never collide with the host's
|
|
143
|
+
// own bare-integer turn keys.
|
|
144
|
+
// • `message` → kept as-is; the persister already keys messages by content, so they survive.
|
|
145
|
+
// TODO(GENC-1461, option 2 / streaming): expose an imperative `appendDiagnostics(entries)` on
|
|
146
|
+
// the handler context (see buildHandlerContext) that runs this same decomposition as SSE ticks
|
|
147
|
+
// arrive, so a streaming widget can surface its external engine's log live rather than only at
|
|
148
|
+
// resolve time. Batch-at-resolve (here) is the current, non-streaming path.
|
|
149
|
+
if (!Array.isArray(harvested) || !harvested.length)
|
|
150
|
+
return;
|
|
151
|
+
const batch = (this.externalBatches += 1);
|
|
152
|
+
const externalEvents = [];
|
|
153
|
+
let turnCount = 0;
|
|
154
|
+
let messageCount = 0;
|
|
155
|
+
for (const raw of harvested) {
|
|
156
|
+
// The harvest is external and typed `unknown[]`, so an element could be a primitive, null,
|
|
157
|
+
// or an object without a string `kind`. Guard before reading `.kind` so a malformed entry
|
|
158
|
+
// can't throw here (and gets skipped rather than derailing the whole fold).
|
|
159
|
+
if (!raw || typeof raw !== 'object' || typeof raw.kind !== 'string') {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const entry = raw;
|
|
163
|
+
if (entry.kind === 'event') {
|
|
164
|
+
const { kind: _kind } = entry, event = __rest(entry, ["kind"]);
|
|
165
|
+
externalEvents.push(event);
|
|
166
|
+
}
|
|
167
|
+
else if (entry.kind === 'turn') {
|
|
168
|
+
// Local position within THIS harvest, not the source engine's own `turnIndex`: one
|
|
169
|
+
// interaction can carry several independent server runs (a consolidator folds in its
|
|
170
|
+
// consolidator-code run and its table-code run), each numbering its turns from 0, so
|
|
171
|
+
// keying by the source index would collide them. The local position keeps every turn key
|
|
172
|
+
// distinct within the batch, so the persister's per-`turnIndex` dedup keeps them all.
|
|
173
|
+
this.externalEntries.push(Object.assign(Object.assign({}, entry), { turnIndex: `server-generation.${batch}:${turnCount}` }));
|
|
174
|
+
turnCount += 1;
|
|
175
|
+
}
|
|
176
|
+
else if (entry.kind === 'message') {
|
|
177
|
+
messageCount += 1;
|
|
178
|
+
this.externalEntries.push(entry);
|
|
179
|
+
}
|
|
180
|
+
// Any other kind is intentionally dropped, not folded. A sibling's `buildTimelineEntries`
|
|
181
|
+
// only ever emits message/turn/event; `meta-snapshot` is host/DOM-owned (latest-wins on
|
|
182
|
+
// reassembly, plus its own persister dedup), so folding a foreign one would pollute this
|
|
183
|
+
// host's timeline and meta cursor. Restricting to the three real kinds also keeps the
|
|
184
|
+
// manifest counts accurate (nothing miscounted as a `message`).
|
|
185
|
+
}
|
|
186
|
+
if (externalEvents.length)
|
|
187
|
+
mergeMetaEvents(this.sessionKey, externalEvents);
|
|
188
|
+
// Manifest: one provenance marker for the fold itself — records that an out-of-band engine's
|
|
189
|
+
// log crossed into this session and how much of each kind, so a regression that silently
|
|
190
|
+
// drops a generation's diagnostics (e.g. a new path that forgets to thread them across the
|
|
191
|
+
// boundary) surfaces as a zero/absent manifest rather than only by diffing a download. Also
|
|
192
|
+
// the natural seed for the streaming PR's open/close bracket. Stamped at resolve, so it
|
|
193
|
+
// closes the block on the sorted timeline. (GENC-1461.)
|
|
194
|
+
recordMetaEvent(this.sessionKey, 'external-diagnostics.folded', {
|
|
195
|
+
interactionId,
|
|
196
|
+
batch,
|
|
197
|
+
counts: { turn: turnCount, event: externalEvents.length, message: messageCount },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/** Hold the ring buffer at its cap, dropping oldest-first. */
|
|
201
|
+
trim() {
|
|
202
|
+
while (this.buffer.length > this.maxSnapshots) {
|
|
203
|
+
this.buffer.shift();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -31,6 +31,54 @@ const toggleAnimationEnabled = (host, value, enabled) => {
|
|
|
31
31
|
host.setEnabledAnimations(next);
|
|
32
32
|
};
|
|
33
33
|
const readSwitchChecked = (event) => event.target.checked;
|
|
34
|
+
/**
|
|
35
|
+
* The timestamp modes, in the order they read as an increasing amount of chrome.
|
|
36
|
+
*
|
|
37
|
+
* Keyed by {@link TimestampDisplay} rather than listed as an array of `{ value, label }`,
|
|
38
|
+
* so adding a mode to the union is a compile error here instead of a picker that silently
|
|
39
|
+
* cannot reach it.
|
|
40
|
+
*/
|
|
41
|
+
const TIMESTAMP_MODE_LABELS = {
|
|
42
|
+
off: 'Off',
|
|
43
|
+
hover: 'On hover',
|
|
44
|
+
always: 'Always',
|
|
45
|
+
};
|
|
46
|
+
const TIMESTAMP_MODE_OPTIONS = Object.keys(TIMESTAMP_MODE_LABELS).map((value) => ({ value, label: TIMESTAMP_MODE_LABELS[value] }));
|
|
47
|
+
/**
|
|
48
|
+
* Three-way picker for {@link TimestampDisplay} (GENC-1257).
|
|
49
|
+
*
|
|
50
|
+
* The design system's segmented control rather than the `switch` every other row uses,
|
|
51
|
+
* because the setting genuinely has three states — and rather than hand-rolled buttons
|
|
52
|
+
* carrying `role="radio"`, because ARIA roles supply semantics but never behaviour:
|
|
53
|
+
* promising a radio group means owning roving `tabindex` and arrow-key navigation.
|
|
54
|
+
*
|
|
55
|
+
* `SegmentedControl extends FASTRadioGroup` (and `SegmentedItem extends FASTRadio`), so
|
|
56
|
+
* this IS the DS radio group — it inherits the whole keyboard pattern — just skinned as
|
|
57
|
+
* segments rather than dots, which is the right affordance for three short mode labels.
|
|
58
|
+
*
|
|
59
|
+
* The usual reason to avoid a DS control here does not apply. `RadioGroup.valueChanged`
|
|
60
|
+
* emits `change` whenever `value` is written — including the write this template's own
|
|
61
|
+
* binding performs on render, exactly the phantom-change footgun the DS switch has
|
|
62
|
+
* (GENC-1351 #1). It is harmless because the `timestampDisplay` setter guards on
|
|
63
|
+
* `preferenceUnchanged`, as every preference setter does, so a render-time echo returns early
|
|
64
|
+
* and never reaches the store or marks preferences user-changed.
|
|
65
|
+
*/
|
|
66
|
+
export const timestampModePickerTemplate = (segmentedControlTag, segmentedItemTag) => html `
|
|
67
|
+
<${segmentedControlTag}
|
|
68
|
+
part="toggle-timestamps"
|
|
69
|
+
aria-label="Show timestamps"
|
|
70
|
+
:value=${(x) => x.timestampDisplay}
|
|
71
|
+
@change=${(x, c) => {
|
|
72
|
+
x.timestampDisplay = c.event.target.value;
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
${repeat(() => TIMESTAMP_MODE_OPTIONS, html `
|
|
76
|
+
<${segmentedItemTag} part="timestamp-mode-option" value="${(option) => option.value}"
|
|
77
|
+
>${(option) => option.label}</${segmentedItemTag}
|
|
78
|
+
>
|
|
79
|
+
`)}
|
|
80
|
+
</${segmentedControlTag}>
|
|
81
|
+
`;
|
|
34
82
|
/** Inline animation picker — categories and options always visible (no multiselect popup). */
|
|
35
83
|
const animationsPickerTemplate = (switchTag) => html `
|
|
36
84
|
<div class="settings-animation-list" part="toggle-animations">
|
|
@@ -198,7 +246,7 @@ export const settingsUsageMetersTemplate = (progressTag, buttonTag, iconTag) =>
|
|
|
198
246
|
</div>
|
|
199
247
|
`)}
|
|
200
248
|
`;
|
|
201
|
-
const chatBotSettingsTemplate = (switchTag, buttonTag) => html `
|
|
249
|
+
const chatBotSettingsTemplate = (switchTag, buttonTag, segmentedControlTag, segmentedItemTag) => html `
|
|
202
250
|
${when((x) => { var _a; return ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showToolCalls) != null; }, chatBotToggleRow('Show tool calls', "Display each tool the chatbot invokes — file reads, searches, edits and commands — inline as it works, so you can follow exactly what it's doing behind the scenes.", switchTag, 'toggle-tool-calls', (x) => x.showToolCalls, (x, c) => {
|
|
203
251
|
x.showToolCalls = readSwitchChecked(c.event);
|
|
204
252
|
}))}
|
|
@@ -211,6 +259,7 @@ const chatBotSettingsTemplate = (switchTag, buttonTag) => html `
|
|
|
211
259
|
${when((x) => { var _a; return ((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showAgentSwitchIndicator) != null; }, chatBotToggleRow('Show delegation', 'Show in the chat window when tasks are delegated to different agents.', switchTag, 'toggle-agent-switch', (x) => x.showAgentSwitchIndicator, (x, c) => {
|
|
212
260
|
x.showAgentSwitchIndicator = readSwitchChecked(c.event);
|
|
213
261
|
}))}
|
|
262
|
+
${settingsToggleRow('Show timestamps', 'Show when each message was sent, next to the sender name. Screen readers announce the full date in either visible mode.', 'toggle-timestamps', timestampModePickerTemplate(segmentedControlTag, segmentedItemTag))}
|
|
214
263
|
${when((x) => x.persistenceToggleable, chatBotToggleRow('Remember chat', 'Save this conversation so it can be restored after a page refresh. Turning this off deletes the saved copy.', switchTag, 'toggle-remember-chat', (x) => x.persistenceActive, (x, c) => {
|
|
215
264
|
x.setPersistenceEnabled(readSwitchChecked(c.event));
|
|
216
265
|
}))}
|
|
@@ -526,6 +575,8 @@ export const SettingsModalTemplate = (designSystemPrefix) => {
|
|
|
526
575
|
const tabsTag = `${designSystemPrefix}-tabs`;
|
|
527
576
|
const tabTag = `${designSystemPrefix}-tab`;
|
|
528
577
|
const tabPanelTag = `${designSystemPrefix}-tab-panel`;
|
|
578
|
+
const segmentedControlTag = `${designSystemPrefix}-segmented-control`;
|
|
579
|
+
const segmentedItemTag = `${designSystemPrefix}-segmented-item`;
|
|
529
580
|
const settingsTabPanel = html `
|
|
530
581
|
<${tabPanelTag}
|
|
531
582
|
slot="tabpanel"
|
|
@@ -595,7 +646,7 @@ export const SettingsModalTemplate = (designSystemPrefix) => {
|
|
|
595
646
|
</h3>
|
|
596
647
|
<p class="settings-modal-section-note">These options only control what you see in the chat window and do not control model behaviour.</p>
|
|
597
648
|
<div class="settings-modal-section-body">
|
|
598
|
-
${chatBotSettingsTemplate(switchTag, buttonTag)}
|
|
649
|
+
${chatBotSettingsTemplate(switchTag, buttonTag, segmentedControlTag, segmentedItemTag)}
|
|
599
650
|
</div>
|
|
600
651
|
</section>
|
|
601
652
|
</div>
|