@genesislcap/ai-assistant 14.450.0 → 14.451.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 +85 -15
- package/dist/ai-assistant.d.ts +94 -3
- package/dist/dts/components/chat-driver/chat-driver.d.ts +7 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +48 -2
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/state/debug-event-log.d.ts +82 -0
- package/dist/dts/state/debug-event-log.d.ts.map +1 -0
- package/dist/esm/components/chat-driver/chat-driver.js +83 -4
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +19 -3
- package/dist/esm/main/main.js +149 -14
- package/dist/esm/state/debug-event-log.js +118 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -16
- package/src/components/chat-driver/chat-driver.ts +84 -8
- package/src/components/orchestrating-driver/orchestrating-driver.ts +19 -2
- package/src/main/main.ts +152 -6
- package/src/state/debug-event-log.ts +194 -0
package/src/main/main.ts
CHANGED
|
@@ -49,7 +49,13 @@ import { AiChatMarkdown } from '../components/chat-markdown/chat-markdown';
|
|
|
49
49
|
import { AiHaloOverlay } from '../components/halo-overlay';
|
|
50
50
|
import { OrchestratingDriver } from '../components/orchestrating-driver/orchestrating-driver';
|
|
51
51
|
import type { AgentConfig } from '../config/config';
|
|
52
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
recordMetaEvent,
|
|
54
|
+
getMetaEvents,
|
|
55
|
+
DEBUG_LOG_README,
|
|
56
|
+
type MetaEventType,
|
|
57
|
+
} from '../state/debug-event-log';
|
|
58
|
+
import { getOrCreateDriver, getDriver, deleteDriver } from '../state/driver-registry';
|
|
53
59
|
import { getSessionStore, hasSessionStore, type SessionStoreReturn } from '../state/session-store';
|
|
54
60
|
import {
|
|
55
61
|
AI_COLOUR_AMBER,
|
|
@@ -228,6 +234,11 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
228
234
|
set state(value: AiAssistantState) {
|
|
229
235
|
const prev = this._sessionRef?.store.aiAssistant.state;
|
|
230
236
|
this._sessionRef?.actions.aiAssistant.setState(value);
|
|
237
|
+
// Only record a real transition against a live session — a missing
|
|
238
|
+
// _sessionRef means setState no-op'd, so no transition actually happened.
|
|
239
|
+
if (this._sessionRef && prev !== value) {
|
|
240
|
+
this.logMeta('state.changed', { from: prev, to: value });
|
|
241
|
+
}
|
|
231
242
|
this.syncShowHalo();
|
|
232
243
|
// When the agent finishes (loading → !loading) the input row reappears (or
|
|
233
244
|
// becomes enabled) — refocus it so the user can type immediately, but only
|
|
@@ -354,6 +365,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
354
365
|
// Suggestions are scoped to the active routing — resetting forces a fresh
|
|
355
366
|
// fetch when the user pins/unpins so the prompts match the new agent.
|
|
356
367
|
if (previous !== value) {
|
|
368
|
+
this.logMeta(value ? 'agent.pinned' : 'agent.unpinned', { agent: value ?? previous });
|
|
357
369
|
this.suggestionsState = { status: 'idle' };
|
|
358
370
|
this.fetchSuggestions();
|
|
359
371
|
}
|
|
@@ -508,6 +520,12 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
508
520
|
private _userScrolledAway = false;
|
|
509
521
|
private _scrollListener?: () => void;
|
|
510
522
|
private static readonly SCROLL_BOTTOM_THRESHOLD_PX = 50;
|
|
523
|
+
/** Context-usage percentage that fires the one-shot `context.threshold-crossed` event. */
|
|
524
|
+
private static readonly CONTEXT_USAGE_WARN_PERCENT = 80;
|
|
525
|
+
/** Last `contextTokens` logged to the timeline — dedupes the `context.updated` event. */
|
|
526
|
+
private _lastLoggedContextTokens?: number;
|
|
527
|
+
/** Whether the one-shot ≥80% `context.threshold-crossed` event has fired this session. */
|
|
528
|
+
private _contextThresholdCrossed = false;
|
|
511
529
|
/**
|
|
512
530
|
* Interaction widgets that grow mid-lifecycle (e.g. expanding a "More info"
|
|
513
531
|
* panel, appending an SSE status row, revealing generated code) bubble a
|
|
@@ -648,6 +666,10 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
648
666
|
this.wireDriver();
|
|
649
667
|
if (history.length) this.driver.loadHistory([...history]);
|
|
650
668
|
this._driverAgentsKey = newKey;
|
|
669
|
+
this.logMeta('driver.created', {
|
|
670
|
+
reason: 'agents-changed',
|
|
671
|
+
driverKind: this.driver instanceof OrchestratingDriver ? 'orchestrating' : 'chat',
|
|
672
|
+
});
|
|
651
673
|
}
|
|
652
674
|
|
|
653
675
|
/** Returns a stable fingerprint for an agents array based on agent names and tool handler keys. */
|
|
@@ -741,6 +763,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
741
763
|
agent.maxToolIterations,
|
|
742
764
|
agent.maxFoldOperations,
|
|
743
765
|
agent.maxTurnSnapshots,
|
|
766
|
+
this.getStateKey() ?? '',
|
|
744
767
|
);
|
|
745
768
|
}
|
|
746
769
|
|
|
@@ -930,6 +953,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
930
953
|
}
|
|
931
954
|
}
|
|
932
955
|
|
|
956
|
+
const driverExisted = !!getDriver(key);
|
|
933
957
|
this.driver = getOrCreateDriver(key, () => this.createDriver());
|
|
934
958
|
this._driverAgentsKey = this.getAgentsKey(this.agents);
|
|
935
959
|
this.wireDriver();
|
|
@@ -939,9 +963,11 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
939
963
|
// panel, a collapse-mode element connects and wires to the same shared
|
|
940
964
|
// driver. Unwire this (hidden) instance on popout; re-wire on popin.
|
|
941
965
|
const unsubPopout = agenticActivityBus.subscribe('chat-popout', () => {
|
|
966
|
+
this.logMeta('driver.unwired', { reason: 'popout' });
|
|
942
967
|
this.unwireDriver();
|
|
943
968
|
});
|
|
944
969
|
const unsubPopin = agenticActivityBus.subscribe('chat-popin', () => {
|
|
970
|
+
this.logMeta('driver.wired', { reason: 'popin' });
|
|
945
971
|
this.wireDriver();
|
|
946
972
|
});
|
|
947
973
|
this.unsubBus = () => {
|
|
@@ -994,6 +1020,13 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
994
1020
|
// where state is loading and the input may be hidden or disabled).
|
|
995
1021
|
if (this.state !== 'loading') this.maybeAutoFocusChatInput();
|
|
996
1022
|
logger.debug('FoundationAiAssistant connected');
|
|
1023
|
+
this.logMeta('assistant.connected', {
|
|
1024
|
+
store: isNewStore ? 'created' : 'restored',
|
|
1025
|
+
restoredMessages: this.messages.length,
|
|
1026
|
+
driver: driverExisted ? 'reused' : 'created',
|
|
1027
|
+
driverKind: this.driver instanceof OrchestratingDriver ? 'orchestrating' : 'chat',
|
|
1028
|
+
driverBusy: this.driver?.isBusy() ?? false,
|
|
1029
|
+
});
|
|
997
1030
|
}
|
|
998
1031
|
|
|
999
1032
|
disconnectedCallback() {
|
|
@@ -1018,6 +1051,8 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1018
1051
|
// the panel re-mounts open.
|
|
1019
1052
|
this._agentPickerToggle.finalize();
|
|
1020
1053
|
this._settingsToggle.finalize();
|
|
1054
|
+
// Capture before clearing — `wasBusy` reads the driver, which is dropped below.
|
|
1055
|
+
this.logMeta('assistant.disconnected', { wasBusy: this.driver?.isBusy() ?? false });
|
|
1021
1056
|
// Clear local references only — driver and store stay in their registries.
|
|
1022
1057
|
this.driver = undefined;
|
|
1023
1058
|
this._sessionRef = undefined;
|
|
@@ -1101,6 +1136,34 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1101
1136
|
if (runningCost !== this.sessionCostUsd) {
|
|
1102
1137
|
this.sessionCostUsd = runningCost;
|
|
1103
1138
|
}
|
|
1139
|
+
// Record a context.updated meta event when the token count changes (≈once
|
|
1140
|
+
// per LLM call, as a new usage-bearing message arrives), plus a one-shot
|
|
1141
|
+
// threshold-crossed event the first time usage passes 80%.
|
|
1142
|
+
if (this.contextTokens != null && this.contextTokens !== this._lastLoggedContextTokens) {
|
|
1143
|
+
this._lastLoggedContextTokens = this.contextTokens;
|
|
1144
|
+
const usagePercent =
|
|
1145
|
+
this.contextLimit != null && this.contextLimit > 0
|
|
1146
|
+
? Math.round((this.contextTokens / this.contextLimit) * 100)
|
|
1147
|
+
: undefined;
|
|
1148
|
+
this.logMeta('context.updated', {
|
|
1149
|
+
tokens: this.contextTokens,
|
|
1150
|
+
limit: this.contextLimit,
|
|
1151
|
+
usagePercent,
|
|
1152
|
+
costUsd: this.sessionCostUsd,
|
|
1153
|
+
});
|
|
1154
|
+
if (
|
|
1155
|
+
usagePercent != null &&
|
|
1156
|
+
usagePercent >= FoundationAiAssistant.CONTEXT_USAGE_WARN_PERCENT &&
|
|
1157
|
+
!this._contextThresholdCrossed
|
|
1158
|
+
) {
|
|
1159
|
+
this._contextThresholdCrossed = true;
|
|
1160
|
+
this.logMeta('context.threshold-crossed', {
|
|
1161
|
+
usagePercent,
|
|
1162
|
+
tokens: this.contextTokens,
|
|
1163
|
+
limit: this.contextLimit,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1104
1167
|
// Publish halo-start whenever a new toolCalls message arrives.
|
|
1105
1168
|
if (this.showHalo !== 'no' && this.enabledAnimations?.includes('halo')) {
|
|
1106
1169
|
const last = this.messages[this.messages.length - 1];
|
|
@@ -1166,8 +1229,10 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1166
1229
|
/** Called when the user clicks the popout button. */
|
|
1167
1230
|
handlePopout(): void {
|
|
1168
1231
|
if (this.popoutMode === 'expand') {
|
|
1232
|
+
this.logMeta('assistant.popout');
|
|
1169
1233
|
agenticActivityBus.publish('chat-popout', undefined);
|
|
1170
1234
|
} else if (this.popoutMode === 'collapse') {
|
|
1235
|
+
this.logMeta('assistant.popin');
|
|
1171
1236
|
agenticActivityBus.publish('chat-popin', undefined);
|
|
1172
1237
|
}
|
|
1173
1238
|
}
|
|
@@ -1178,6 +1243,25 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1178
1243
|
return parts.length ? parts.join('::') : undefined;
|
|
1179
1244
|
}
|
|
1180
1245
|
|
|
1246
|
+
/**
|
|
1247
|
+
* Record a meta/lifecycle event onto this session's debug-log timeline
|
|
1248
|
+
* (exported via {@link FoundationAiAssistant.getDebugLog}). Every event
|
|
1249
|
+
* auto-carries `placement` so a session emitted from both the bubble and the
|
|
1250
|
+
* layout panel stays disambiguated. Keyed by `stateKey` so the timeline is
|
|
1251
|
+
* shared across pop-in/pop-out and survives driver rebuilds.
|
|
1252
|
+
*/
|
|
1253
|
+
private logMeta(type: MetaEventType, detail?: Record<string, unknown>): void {
|
|
1254
|
+
const key = this.getStateKey();
|
|
1255
|
+
if (!key) return;
|
|
1256
|
+
const placement =
|
|
1257
|
+
this.popoutMode === 'expand'
|
|
1258
|
+
? 'bubble'
|
|
1259
|
+
: this.popoutMode === 'collapse'
|
|
1260
|
+
? 'panel'
|
|
1261
|
+
: 'standalone';
|
|
1262
|
+
recordMetaEvent(key, type, { placement, ...detail });
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1181
1265
|
private readonly _settingsToggle = new AnimatedPanelToggle(
|
|
1182
1266
|
this,
|
|
1183
1267
|
'.settings-panel',
|
|
@@ -1189,6 +1273,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1189
1273
|
);
|
|
1190
1274
|
|
|
1191
1275
|
toggleSettings() {
|
|
1276
|
+
this.logMeta('panel.toggled', { panel: 'settings', open: !this.settingsOpen });
|
|
1192
1277
|
this._settingsToggle.toggle();
|
|
1193
1278
|
}
|
|
1194
1279
|
|
|
@@ -1203,6 +1288,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1203
1288
|
);
|
|
1204
1289
|
|
|
1205
1290
|
toggleAgentPicker() {
|
|
1291
|
+
this.logMeta('panel.toggled', { panel: 'agent-picker', open: !this.agentPickerOpen });
|
|
1206
1292
|
this._agentPickerToggle.toggle();
|
|
1207
1293
|
}
|
|
1208
1294
|
|
|
@@ -1337,8 +1423,60 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1337
1423
|
this.contextTokens != null && this.contextLimit != null && this.contextLimit > 0
|
|
1338
1424
|
? Math.round((this.contextTokens / this.contextLimit) * 100)
|
|
1339
1425
|
: undefined;
|
|
1426
|
+
const stateKey = this.getStateKey();
|
|
1427
|
+
|
|
1428
|
+
// Collapse repeated system prompts across consecutive turns. A stateful
|
|
1429
|
+
// agent's prompt changes between turns, but a stable agent repeats the same
|
|
1430
|
+
// (often multi-KB) prompt every turn — so replace a turn's `systemPrompt`
|
|
1431
|
+
// with a sentinel when it's byte-identical to the previous turn's. The
|
|
1432
|
+
// prompt is still shown in full whenever it changes, so prompt evolution
|
|
1433
|
+
// stays visible.
|
|
1434
|
+
let lastFullPrompt: string | undefined;
|
|
1435
|
+
let lastFullIndex = -1;
|
|
1436
|
+
const turns = (this.driver?.getTurnSnapshots?.() ?? []).map((t) => {
|
|
1437
|
+
let { systemPrompt } = t;
|
|
1438
|
+
if (systemPrompt != null && systemPrompt === lastFullPrompt) {
|
|
1439
|
+
systemPrompt = `<repeated — identical to turn ${lastFullIndex}>`;
|
|
1440
|
+
} else if (systemPrompt != null) {
|
|
1441
|
+
lastFullPrompt = systemPrompt;
|
|
1442
|
+
lastFullIndex = t.turnIndex;
|
|
1443
|
+
}
|
|
1444
|
+
return { kind: 'turn' as const, ...t, systemPrompt };
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
// Single chronological timeline. The conversation messages, per-LLM-call
|
|
1448
|
+
// turn snapshots, and lifecycle/structural meta events are stored separately
|
|
1449
|
+
// in memory (history on the driver, turn buffer on the driver, meta buffer
|
|
1450
|
+
// keyed by stateKey) but interleaved here so the exported log reads
|
|
1451
|
+
// top-to-bottom as one sequence — no cross-referencing parallel arrays.
|
|
1452
|
+
// Every entry is tagged `kind`. All three streams stamp ISO timestamps in
|
|
1453
|
+
// the same format, so a string compare orders them chronologically; ties
|
|
1454
|
+
// break by `kind` (event → turn → message: the cause, then the call it
|
|
1455
|
+
// triggered, then the output) and are otherwise stable within a stream.
|
|
1456
|
+
const KIND_RANK: Record<'event' | 'turn' | 'message', number> = {
|
|
1457
|
+
event: 0,
|
|
1458
|
+
turn: 1,
|
|
1459
|
+
message: 2,
|
|
1460
|
+
};
|
|
1461
|
+
const messages = this.driver?.getRawHistory?.() ?? this.messages;
|
|
1462
|
+
const timeline = [
|
|
1463
|
+
...messages.map((m) => ({ kind: 'message' as const, ...m })),
|
|
1464
|
+
...turns,
|
|
1465
|
+
...(stateKey ? getMetaEvents(stateKey) : []).map((e) => ({ kind: 'event' as const, ...e })),
|
|
1466
|
+
].sort((a, b) => {
|
|
1467
|
+
const ta = a.timestamp ?? '';
|
|
1468
|
+
const tb = b.timestamp ?? '';
|
|
1469
|
+
if (ta < tb) return -1;
|
|
1470
|
+
if (ta > tb) return 1;
|
|
1471
|
+
return KIND_RANK[a.kind] - KIND_RANK[b.kind];
|
|
1472
|
+
});
|
|
1340
1473
|
return {
|
|
1341
|
-
|
|
1474
|
+
// First key so it's at the top of the file — tells whoever opens the JSON
|
|
1475
|
+
// (often an AI agent) how to read the rest. See debug-event-log.ts.
|
|
1476
|
+
readme: DEBUG_LOG_README,
|
|
1477
|
+
// The whole session as one readable sequence. The conversation transcript
|
|
1478
|
+
// lives here as `kind: 'message'` entries rather than in a parallel block.
|
|
1479
|
+
timeline,
|
|
1342
1480
|
meta: {
|
|
1343
1481
|
timestamp,
|
|
1344
1482
|
host: window.location.host,
|
|
@@ -1380,9 +1518,6 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1380
1518
|
// Snapshot captured fresh at log-export time — reflects state NOW, which
|
|
1381
1519
|
// may have transitioned since the last LLM call.
|
|
1382
1520
|
activeDebugSnapshot,
|
|
1383
|
-
// Per-LLM-call timeline — pairs each turn with the prompt + tool surface
|
|
1384
|
-
// + agent state that drove it. Capped to the most recent N entries.
|
|
1385
|
-
turnSnapshots: this.driver?.getTurnSnapshots?.() ?? [],
|
|
1386
1521
|
debug: this.debugStateFactory?.(),
|
|
1387
1522
|
},
|
|
1388
1523
|
};
|
|
@@ -1466,6 +1601,10 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1466
1601
|
};
|
|
1467
1602
|
} catch (readError) {
|
|
1468
1603
|
logger.error('Failed to read file:', readError);
|
|
1604
|
+
this.logMeta('file.read-failed', {
|
|
1605
|
+
file: file.name,
|
|
1606
|
+
message: readError instanceof Error ? readError.message : String(readError),
|
|
1607
|
+
});
|
|
1469
1608
|
return { ok: false, message: `Failed to read "${file.name}".` };
|
|
1470
1609
|
}
|
|
1471
1610
|
}),
|
|
@@ -1486,7 +1625,13 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1486
1625
|
const files = Array.from(input.files ?? []);
|
|
1487
1626
|
input.value = '';
|
|
1488
1627
|
const { attachments, errors } = await this.processFiles(files);
|
|
1489
|
-
if (attachments.length)
|
|
1628
|
+
if (attachments.length) {
|
|
1629
|
+
this.attachments = [...this.attachments, ...attachments];
|
|
1630
|
+
this.logMeta('attachment.added', {
|
|
1631
|
+
count: attachments.length,
|
|
1632
|
+
names: attachments.map((a) => a.name),
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1490
1635
|
if (errors.length) this.attachmentErrors = [...this.attachmentErrors, ...errors];
|
|
1491
1636
|
}
|
|
1492
1637
|
|
|
@@ -1619,6 +1764,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1619
1764
|
if (generation !== this._suggestionsGeneration) return;
|
|
1620
1765
|
this.suggestionsState = { status: 'error', message: (e as Error).message };
|
|
1621
1766
|
logger.error('Failed to fetch suggestions:', e);
|
|
1767
|
+
this.logMeta('suggestions.failed', { message: (e as Error).message });
|
|
1622
1768
|
}
|
|
1623
1769
|
}
|
|
1624
1770
|
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level append-only timeline of meta/lifecycle events, keyed by session
|
|
3
|
+
* identity (the same `stateKey` space used by the driver registry and the
|
|
4
|
+
* session store).
|
|
5
|
+
*
|
|
6
|
+
* Why a module registry rather than the redux store or the driver:
|
|
7
|
+
* - **Survives the pop-in/pop-out element churn.** A single session can be
|
|
8
|
+
* emitted from up to two element instances (an `expand` bubble + a `collapse`
|
|
9
|
+
* panel) sharing one driver; keying by `stateKey` unifies them into one
|
|
10
|
+
* timeline.
|
|
11
|
+
* - **Survives driver rebuilds.** The driver is torn down and recreated when the
|
|
12
|
+
* host swaps the agents array (see `agentsChanged` / `deleteDriver`), so a
|
|
13
|
+
* driver-owned buffer would lose the timeline mid-session. The `stateKey`
|
|
14
|
+
* outlives the driver.
|
|
15
|
+
* - **Stays out of redux reactivity.** These appends never drive the UI, so
|
|
16
|
+
* there's no reason to pay immutable-reducer cost or flood the Redux DevTools
|
|
17
|
+
* action log with debug noise.
|
|
18
|
+
*
|
|
19
|
+
* Surfaced under `getDebugLog().meta.events`. Ring-buffered so a long-lived
|
|
20
|
+
* session can't grow the timeline unbounded.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Catalogue of meta event names. This is the documented surface — extend it as
|
|
27
|
+
* new events are wired in (Tier 2/3 lifecycle, interaction, provider events).
|
|
28
|
+
*/
|
|
29
|
+
export type MetaEventType =
|
|
30
|
+
// Element lifecycle + window placement
|
|
31
|
+
| 'assistant.connected'
|
|
32
|
+
| 'assistant.disconnected'
|
|
33
|
+
| 'assistant.popout'
|
|
34
|
+
| 'assistant.popin'
|
|
35
|
+
// Driver lifecycle / wiring
|
|
36
|
+
| 'driver.created'
|
|
37
|
+
| 'driver.wired'
|
|
38
|
+
| 'driver.unwired'
|
|
39
|
+
// Turn lifecycle
|
|
40
|
+
| 'state.changed'
|
|
41
|
+
| 'turn.start'
|
|
42
|
+
| 'turn.end'
|
|
43
|
+
| 'turn.error'
|
|
44
|
+
| 'tool.failed'
|
|
45
|
+
// Routing / providers
|
|
46
|
+
| 'agent.handoff'
|
|
47
|
+
| 'agent.pinned'
|
|
48
|
+
| 'agent.unpinned'
|
|
49
|
+
| 'provider.selected'
|
|
50
|
+
// Blocking interactions
|
|
51
|
+
| 'interaction.requested'
|
|
52
|
+
| 'interaction.resolved'
|
|
53
|
+
// Context / cost
|
|
54
|
+
| 'context.updated'
|
|
55
|
+
| 'context.threshold-crossed'
|
|
56
|
+
// UI + input
|
|
57
|
+
| 'panel.toggled'
|
|
58
|
+
| 'attachment.added'
|
|
59
|
+
| 'file.read-failed'
|
|
60
|
+
| 'suggestions.failed';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How much a reader should care about an event — lets a consumer (or an AI
|
|
64
|
+
* agent) filter the timeline: skip `low` UI/bookkeeping noise, skim `normal`
|
|
65
|
+
* session flow, or jump straight to `high` problem/limit signals when triaging.
|
|
66
|
+
*/
|
|
67
|
+
export type MetaEventImportance = 'high' | 'normal' | 'low';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Importance is intrinsic to the event *type*, not the instance, so it's
|
|
71
|
+
* defined once here and stamped automatically by {@link recordMetaEvent} — call
|
|
72
|
+
* sites never pass it. The `Record` is exhaustive: adding a `MetaEventType`
|
|
73
|
+
* without a level here is a compile error.
|
|
74
|
+
*
|
|
75
|
+
* - `high` — failures and hard limits you almost always want to see.
|
|
76
|
+
* - `normal` — meaningful session flow you read to follow what happened.
|
|
77
|
+
* - `low` — frequent UI / bookkeeping noise, usually safe to skip.
|
|
78
|
+
*/
|
|
79
|
+
export const META_EVENT_IMPORTANCE: Record<MetaEventType, MetaEventImportance> = {
|
|
80
|
+
'turn.error': 'high',
|
|
81
|
+
'tool.failed': 'high',
|
|
82
|
+
'file.read-failed': 'high',
|
|
83
|
+
'suggestions.failed': 'high',
|
|
84
|
+
'context.threshold-crossed': 'high',
|
|
85
|
+
|
|
86
|
+
'assistant.connected': 'normal',
|
|
87
|
+
'assistant.disconnected': 'normal',
|
|
88
|
+
'assistant.popout': 'normal',
|
|
89
|
+
'assistant.popin': 'normal',
|
|
90
|
+
'driver.created': 'normal',
|
|
91
|
+
'state.changed': 'normal',
|
|
92
|
+
'turn.start': 'normal',
|
|
93
|
+
'turn.end': 'normal',
|
|
94
|
+
'agent.handoff': 'normal',
|
|
95
|
+
'agent.pinned': 'normal',
|
|
96
|
+
'agent.unpinned': 'normal',
|
|
97
|
+
'provider.selected': 'normal',
|
|
98
|
+
'interaction.requested': 'normal',
|
|
99
|
+
'interaction.resolved': 'normal',
|
|
100
|
+
|
|
101
|
+
'driver.wired': 'low',
|
|
102
|
+
'driver.unwired': 'low',
|
|
103
|
+
'context.updated': 'low',
|
|
104
|
+
'panel.toggled': 'low',
|
|
105
|
+
'attachment.added': 'low',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** One entry in the meta-event timeline. */
|
|
109
|
+
export interface MetaEvent {
|
|
110
|
+
/** Monotonic counter across the session's lifetime (does not reset on driver rebuild). */
|
|
111
|
+
index: number;
|
|
112
|
+
/** ISO timestamp, matching the format used elsewhere in the debug log. */
|
|
113
|
+
timestamp: string;
|
|
114
|
+
/** Event name — see {@link MetaEventType}. */
|
|
115
|
+
type: MetaEventType;
|
|
116
|
+
/** How much to care about this event — see {@link META_EVENT_IMPORTANCE}. */
|
|
117
|
+
importance: MetaEventImportance;
|
|
118
|
+
/** Optional structured payload. */
|
|
119
|
+
detail?: Record<string, unknown>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Default ring-buffer cap. ~5× the turn-snapshot cap — entries are cheap. */
|
|
123
|
+
const DEFAULT_MAX_META_EVENTS = 200;
|
|
124
|
+
|
|
125
|
+
interface MetaEventBuffer {
|
|
126
|
+
events: MetaEvent[];
|
|
127
|
+
/** Next index to assign — kept separate from `events.length` so it stays monotonic after eviction. */
|
|
128
|
+
next: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const registry = new Map<string, MetaEventBuffer>();
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Append a meta event to the timeline for `key`. Evicts the oldest entry once
|
|
135
|
+
* the buffer exceeds {@link DEFAULT_MAX_META_EVENTS}. An empty `key` is bucketed
|
|
136
|
+
* under `''`, matching how drivers/stores handle an absent session identity, so
|
|
137
|
+
* callers never need to guard.
|
|
138
|
+
*/
|
|
139
|
+
export function recordMetaEvent(
|
|
140
|
+
key: string,
|
|
141
|
+
type: MetaEventType,
|
|
142
|
+
detail?: Record<string, unknown>,
|
|
143
|
+
): void {
|
|
144
|
+
let buffer = registry.get(key);
|
|
145
|
+
if (!buffer) {
|
|
146
|
+
buffer = { events: [], next: 0 };
|
|
147
|
+
registry.set(key, buffer);
|
|
148
|
+
}
|
|
149
|
+
buffer.events.push({
|
|
150
|
+
index: buffer.next,
|
|
151
|
+
timestamp: new Date().toISOString(),
|
|
152
|
+
type,
|
|
153
|
+
importance: META_EVENT_IMPORTANCE[type],
|
|
154
|
+
detail,
|
|
155
|
+
});
|
|
156
|
+
buffer.next += 1;
|
|
157
|
+
if (buffer.events.length > DEFAULT_MAX_META_EVENTS) {
|
|
158
|
+
buffer.events.shift();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Returns the meta-event timeline for `key`, or an empty array if none recorded. */
|
|
163
|
+
export function getMetaEvents(key: string): ReadonlyArray<MetaEvent> {
|
|
164
|
+
return registry.get(key)?.events ?? [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Human/agent-facing guide emitted as the first key of the exported debug log,
|
|
169
|
+
* so whoever opens the JSON (often an AI agent) knows how to read it without
|
|
170
|
+
* reverse-engineering the shape. Kept here next to the event catalogue it
|
|
171
|
+
* describes so the two stay in sync.
|
|
172
|
+
*/
|
|
173
|
+
export const DEBUG_LOG_README: readonly string[] = [
|
|
174
|
+
'This is an exported debug log for the Genesis AI assistant. Read it top-to-bottom.',
|
|
175
|
+
'`timeline` is the entire session as one array, already sorted chronologically by `timestamp` (ISO 8601). Every entry has a `kind`.',
|
|
176
|
+
"kind:'message' — the conversation. `role` is user/assistant/tool/system-event; `agentName` says which agent produced it; `toolCalls`/`toolResult`/`interaction` carry tool and widget activity; `inputTokens`/`outputTokens`/`cost` are per-message usage.",
|
|
177
|
+
"kind:'turn' — one LLM call. `systemPrompt` and `toolNames` are what the model saw. A systemPrompt of '<repeated — identical to turn N>' was byte-identical to turn N and de-duplicated; the full prompt is shown whenever it changes (often because a stateful agent advanced), so prompt evolution is visible.",
|
|
178
|
+
"kind:'turn'.`agentSnapshot` — the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.",
|
|
179
|
+
"kind:'event' — a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.",
|
|
180
|
+
"Each 'event' also has an `importance`: 'high' (failures/limits — turn.error, tool.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow — connects, turns, handoffs, agent/provider changes, interactions), or 'low' (skippable UI/bookkeeping noise — panel.toggled, attachment.added, driver.wired/unwired, context.updated). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. 'message' and 'turn' entries carry no importance — they are the substance, always read them.",
|
|
181
|
+
'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.error (a turn failed or hit a guardrail — see detail.reason), tool.failed (a tool threw), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), provider.selected (model/provider for the upcoming turns), interaction.requested/resolved (blocking user widgets — explain quiet gaps), context.updated/threshold-crossed (token + cost), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',
|
|
182
|
+
"`meta` holds context captured at export time: agentSummary (full agent configs), context (active model, token usage, session cost), activeDebugSnapshot (the active agent's `getDebugSnapshot()` taken fresh at export — reflects state NOW, which may have advanced beyond the last turn's agentSnapshot), debug (optional host-supplied debug state), host, and the export timestamp.",
|
|
183
|
+
'To debug a failure: find the last turn.error or tool.failed, then read upward for the user message, the turn(s), and the agent/provider/state events that led into it.',
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Removes all entries. Exposed for test isolation only — not part of the
|
|
188
|
+
* public API.
|
|
189
|
+
*
|
|
190
|
+
* @internal
|
|
191
|
+
*/
|
|
192
|
+
export function clearMetaEventRegistry(): void {
|
|
193
|
+
registry.clear();
|
|
194
|
+
}
|