@genesislcap/ai-assistant 15.6.2 → 15.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/ai-assistant.api.json +422 -6
  2. package/dist/ai-assistant.d.ts +654 -19
  3. package/dist/chat-driver.cjs +285 -26
  4. package/dist/chat-driver.cjs.map +3 -3
  5. package/dist/chat-driver.mjs +285 -26
  6. package/dist/chat-driver.mjs.map +3 -3
  7. package/dist/custom-elements.json +289 -32
  8. package/dist/dts/channel/ai-activity-channel.d.ts +51 -1
  9. package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
  10. package/dist/dts/components/chat-driver/chat-driver.d.ts +99 -1
  11. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  12. package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +1 -1
  13. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +2 -0
  14. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +1 -0
  15. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -0
  16. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  17. package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
  18. package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
  19. package/dist/dts/main/blocked-state.test.d.ts +2 -0
  20. package/dist/dts/main/blocked-state.test.d.ts.map +1 -0
  21. package/dist/dts/main/main.d.ts +435 -19
  22. package/dist/dts/main/main.d.ts.map +1 -1
  23. package/dist/dts/main/main.styles.d.ts.map +1 -1
  24. package/dist/dts/main/main.styles.test.d.ts +2 -0
  25. package/dist/dts/main/main.styles.test.d.ts.map +1 -0
  26. package/dist/dts/main/main.template.d.ts +53 -0
  27. package/dist/dts/main/main.template.d.ts.map +1 -1
  28. package/dist/dts/state/ai-assistant-slice.d.ts +162 -6
  29. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  30. package/dist/dts/state/debug-event-log.d.ts +6 -1
  31. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  32. package/dist/dts/state/session-store.d.ts +11 -0
  33. package/dist/dts/state/session-store.d.ts.map +1 -1
  34. package/dist/dts/utils/banked-usage-baselines.d.ts +51 -0
  35. package/dist/dts/utils/banked-usage-baselines.d.ts.map +1 -0
  36. package/dist/esm/components/chat-driver/chat-driver.js +263 -21
  37. package/dist/esm/components/chat-driver/chat-driver.test.js +464 -1
  38. package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +312 -0
  39. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +89 -4
  40. package/dist/esm/components/settings-modal/settings-modal.styles.js +63 -18
  41. package/dist/esm/components/settings-modal/settings-modal.template.js +36 -7
  42. package/dist/esm/main/blocked-state.test.js +969 -0
  43. package/dist/esm/main/cost-session-banking.test.js +238 -8
  44. package/dist/esm/main/main.js +826 -48
  45. package/dist/esm/main/main.styles.js +72 -0
  46. package/dist/esm/main/main.styles.test.js +99 -0
  47. package/dist/esm/main/main.template.js +121 -4
  48. package/dist/esm/state/ai-assistant-slice.js +145 -7
  49. package/dist/esm/state/ai-assistant-slice.test.js +138 -1
  50. package/dist/esm/state/debug-event-log.js +7 -2
  51. package/dist/esm/state/debug-event-log.test.js +49 -1
  52. package/dist/esm/state/persistence/session-snapshot.test.js +18 -0
  53. package/dist/esm/utils/banked-usage-baselines.js +84 -0
  54. package/dist/tsconfig.tsbuildinfo +1 -1
  55. package/docs/migration-GENC-1464.md +562 -0
  56. package/docs/sub_agent.md +20 -3
  57. package/package.json +17 -17
  58. package/src/channel/ai-activity-channel.ts +56 -2
  59. package/src/components/chat-driver/chat-driver.test.ts +549 -0
  60. package/src/components/chat-driver/chat-driver.ts +324 -14
  61. package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +438 -0
  62. package/src/components/orchestrating-driver/orchestrating-driver.ts +101 -6
  63. package/src/components/settings-modal/settings-modal.styles.ts +63 -18
  64. package/src/components/settings-modal/settings-modal.template.ts +38 -7
  65. package/src/main/blocked-state.test.ts +1316 -0
  66. package/src/main/cost-session-banking.test.ts +283 -11
  67. package/src/main/main.styles.test.ts +130 -0
  68. package/src/main/main.styles.ts +72 -0
  69. package/src/main/main.template.ts +131 -4
  70. package/src/main/main.ts +827 -42
  71. package/src/state/ai-assistant-slice.test.ts +215 -0
  72. package/src/state/ai-assistant-slice.ts +218 -8
  73. package/src/state/debug-event-log.test.ts +63 -0
  74. package/src/state/debug-event-log.ts +7 -2
  75. package/src/state/persistence/session-snapshot.test.ts +22 -0
  76. package/src/utils/banked-usage-baselines.ts +90 -0
@@ -1,4 +1,5 @@
1
1
  import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
+ import { getBankedBaseline, resetBankedBaselines, setBankedBaseline, } from '../utils/banked-usage-baselines';
2
3
  import { clearCostSessionHistory, loadCostSessionHistory, } from '../utils/cost-session-history';
3
4
  import { emptyUsage } from '../utils/sum-usage';
4
5
  import { FoundationAiAssistant } from './main';
@@ -28,9 +29,17 @@ const usage = (costUsd, outputTokens = 100) => (Object.assign(Object.assign({},
28
29
  /**
29
30
  * An unconnected element with just enough session state wired for `finalizeCostSession`:
30
31
  * the live usage it projects, the transcript it titles from, and the key it upserts against.
32
+ *
33
+ * Banked baselines are module state keyed by session (so remounts share one), which means they
34
+ * outlive an element. Cleared by default, because every suite but the remount ones is modelling
35
+ * a *fresh page load* — and because leaving them would leak one suite's baseline into the next
36
+ * under the shared `PROJECT` key. Pass `remount: true` to model a new element joining a session
37
+ * that has already banked, which is the case this file exists to pin.
31
38
  */
32
39
  function element(options) {
33
40
  var _a, _b, _c;
41
+ if (!options.remount)
42
+ resetBankedBaselines();
34
43
  const el = document.createElement('foundation-ai-assistant');
35
44
  const messages = [{ role: 'user', content: 'build me a blotter' }];
36
45
  const internals = el;
@@ -267,10 +276,10 @@ Suite('an unreadable ledger blocks writes instead of replacing it with what we c
267
276
  clearCostSessionHistory(scope);
268
277
  });
269
278
  Suite('deleting a provider row whose id differs from its projectKey still drops the memo', () => {
270
- // `_bankedFor` holds a projectKey; the delete action passes a row id. A host provider can assign
271
- // any id, so comparing across the two namespaces skipped the reset and the next turn resurrected
272
- // the deleted total. Reachable when a provider load lands AFTER a finalize has banked: our own
273
- // upsert would otherwise have rewritten the row's id to the projectKey.
279
+ // Baselines are keyed by projectKey; the delete action passes a row id. A host provider can
280
+ // assign any id, so deleting by the wrong namespace left the baseline in place and the next
281
+ // turn resurrected the deleted total. Reachable when a provider load lands AFTER a finalize has
282
+ // banked: our own upsert would otherwise have rewritten the row's id to the projectKey.
274
283
  const scope = 'ledger-remove-id';
275
284
  clearCostSessionHistory(scope);
276
285
  const el = element({
@@ -279,16 +288,221 @@ Suite('deleting a provider row whose id differs from its projectKey still drops
279
288
  rows: [row({ id: 'host-assigned-uuid', projectKey: PROJECT, usage: usage(10) })],
280
289
  persistenceActive: false,
281
290
  });
282
- // Memo already populated for this project, as it would be after a turn.
283
- const internals = el;
284
- internals._bankedFor = PROJECT;
285
- internals._bankedUsage = usage(10);
291
+ // Baseline already resolved for this project, as it would be after a turn — set directly
292
+ // rather than by finalizing, because our own upsert would rewrite the row's id to the
293
+ // projectKey and the id mismatch under test would vanish with it.
294
+ setBankedBaseline(PROJECT, usage(10));
286
295
  el.removeCostSessionFromHistory('host-assigned-uuid');
287
296
  assert.is(el.costSessionHistory.length, 0);
288
297
  el.finalizeCostSession();
289
298
  assert.is(el.costSessionHistory[0].usage.costUsd, 2, 'deleted total must not come back');
290
299
  clearCostSessionHistory(scope);
291
300
  });
301
+ Suite('a remount mid-session does not re-bank what the row already holds', () => {
302
+ var _a;
303
+ // The reported regression (GENC-1475). Reproduces the field report exactly: with persistence
304
+ // off, the host remounted the assistant part-way through a session (a layout re-render — the
305
+ // debug log showed seven connects in one session) and the LIFETIME figure jumped by the whole
306
+ // session-to-date while the live figure kept climbing normally.
307
+ //
308
+ // The new element joins a live session: same key, same transcript, and rows that already
309
+ // contain the previous instance's upsert. An element-scoped baseline re-derived here and, with
310
+ // no restore to defer to, banked `row.usage` — which already included this very transcript.
311
+ const scope = 'bank-remount';
312
+ clearCostSessionHistory(scope);
313
+ const first = element({
314
+ scope,
315
+ live: usage(6),
316
+ rows: [row({ usage: usage(1), banked: usage(1) })],
317
+ persistenceActive: false,
318
+ });
319
+ first.finalizeCostSession();
320
+ assert.is(first.costSessionHistory[0].usage.costUsd, 7, '1 banked + 6 live');
321
+ // Remount: a brand new element wired to the same session, handed the rows as they now stand.
322
+ const second = element({
323
+ scope,
324
+ live: usage(6),
325
+ rows: first.costSessionHistory,
326
+ persistenceActive: false,
327
+ remount: true,
328
+ });
329
+ second.finalizeCostSession();
330
+ assert.is(second.costSessionHistory[0].usage.costUsd, 7, 'remount must not inflate the row');
331
+ assert.is((_a = second.costSessionHistory[0].banked) === null || _a === void 0 ? void 0 : _a.costUsd, 1, 'baseline unchanged by the remount');
332
+ // And the session keeps accruing normally afterwards, on top of the SAME baseline.
333
+ const third = element({
334
+ scope,
335
+ live: usage(9),
336
+ rows: second.costSessionHistory,
337
+ persistenceActive: false,
338
+ remount: true,
339
+ });
340
+ third.finalizeCostSession();
341
+ assert.is(third.costSessionHistory[0].usage.costUsd, 10, '1 banked + 9 live');
342
+ clearCostSessionHistory(scope);
343
+ });
344
+ Suite('a page reload still absorbs the prior total after a remount banked nothing new', () => {
345
+ // The other half of the pairing: dropping the baseline on remount is wrong, but keeping it
346
+ // across a *page load* would be too — the transcript starts empty there (persistence off), so
347
+ // the row's whole total must be banked. `resetBankedBaselines` stands in for the realm dying.
348
+ const scope = 'bank-reload';
349
+ clearCostSessionHistory(scope);
350
+ const before = element({
351
+ scope,
352
+ live: usage(6),
353
+ rows: [row({ usage: usage(1), banked: usage(1) })],
354
+ persistenceActive: false,
355
+ });
356
+ before.finalizeCostSession();
357
+ assert.is(before.costSessionHistory[0].usage.costUsd, 7);
358
+ const afterReload = element({
359
+ scope,
360
+ live: usage(2),
361
+ rows: before.costSessionHistory,
362
+ persistenceActive: false,
363
+ });
364
+ afterReload.finalizeCostSession();
365
+ assert.is(afterReload.costSessionHistory[0].usage.costUsd, 9, 'prior 7 banked, this load adds 2');
366
+ clearCostSessionHistory(scope);
367
+ });
368
+ Suite('clearing the transcript re-baselines instead of discarding spend', () => {
369
+ var _a;
370
+ // Clear empties the transcript, so the row's total now predates what is on screen and has to be
371
+ // banked wholesale. Merely dropping the baseline would re-derive through `persistenceActive`,
372
+ // which with persistence ON carries the row's OLD banked figure forward and silently loses
373
+ // every turn between that baseline and the clear.
374
+ const scope = 'bank-clear-session';
375
+ clearCostSessionHistory(scope);
376
+ const el = element({
377
+ scope,
378
+ live: usage(6),
379
+ rows: [row({ usage: usage(10), banked: usage(4) })],
380
+ persistenceActive: true,
381
+ });
382
+ el.finalizeCostSession();
383
+ assert.is(el.costSessionHistory[0].usage.costUsd, 10);
384
+ // `clearSession` itself tears down and rebuilds the driver, which an unconnected element has
385
+ // none of — so drive the one step under test. `clearSession` reads the live figure before
386
+ // `resetSession` zeroes it and passes it in; the 6 here is that capture.
387
+ const priv = el;
388
+ priv.rebaselineOnClearedTranscript(PROJECT, usage(6));
389
+ // A fresh transcript worth 3, on top of the whole 10 the project had accrued.
390
+ priv._sessionRef.store.aiAssistant.sessionUsage = usage(3);
391
+ el.finalizeCostSession();
392
+ assert.is(el.costSessionHistory[0].usage.costUsd, 13, 'pre-clear spend kept, new turn added');
393
+ assert.is((_a = el.costSessionHistory[0].banked) === null || _a === void 0 ? void 0 : _a.costUsd, 10, 'whole pre-clear total banked');
394
+ clearCostSessionHistory(scope);
395
+ });
396
+ Suite('clearing while the ledger is still loading must not pin a baseline', () => {
397
+ var _a;
398
+ // Review case. A stamped baseline is sticky — `bankedUsageFor` short-circuits on any stored
399
+ // entry and a zero usage is a *resolved* value there. On the provider path the row list is empty
400
+ // and `_costHistoryLoaded` false while the read is in flight, so a clear landing in that window
401
+ // used to pin a baseline derived from no rows at all. The load arriving afterwards with the
402
+ // project's real total was then never consulted, and the next write replaced its row with
403
+ // `banked: 0` — the project's whole history gone.
404
+ const scope = 'bank-clear-unloaded';
405
+ clearCostSessionHistory(scope);
406
+ const el = element({ scope, live: usage(5), loaded: false });
407
+ const priv = el;
408
+ // Clear arrives before the provider read lands: rows are empty and nothing is known yet.
409
+ priv.rebaselineOnClearedTranscript(PROJECT, usage(5));
410
+ assert.is(getBankedBaseline(PROJECT), undefined, 'nothing pinned from a ledger we cannot see');
411
+ // The read lands with this project's real history, and a turn follows.
412
+ el.costSessionHistory = [row({ usage: usage(42), banked: usage(40) })];
413
+ priv._costHistoryLoaded = true;
414
+ priv._sessionRef.store.aiAssistant.sessionUsage = usage(1);
415
+ el.finalizeCostSession();
416
+ assert.is(el.costSessionHistory[0].usage.costUsd, 43, "the project's 42 survives the race");
417
+ assert.is((_a = el.costSessionHistory[0].banked) === null || _a === void 0 ? void 0 : _a.costUsd, 42, 'baseline re-derived from real rows');
418
+ clearCostSessionHistory(scope);
419
+ });
420
+ Suite('clearing banks a transcript the row has not caught up with', () => {
421
+ var _a;
422
+ // Review case: without this, the suite passed identically against the OLD derivation
423
+ // (`existing?.usage`), because everywhere else the row already agreed with baseline + live.
424
+ //
425
+ // The formulas only diverge when the row is STALE relative to the transcript — which is exactly
426
+ // what the new one exists for, since `finalizeCostSession` skips its write while the ledger is
427
+ // unloaded and while the session has no usage. No finalize here, so the row lags by 3.
428
+ const scope = 'bank-clear-stale-row';
429
+ clearCostSessionHistory(scope);
430
+ const el = element({
431
+ scope,
432
+ live: usage(3),
433
+ rows: [row({ usage: usage(42), banked: usage(40) })],
434
+ persistenceActive: false,
435
+ });
436
+ const priv = el;
437
+ priv.rebaselineOnClearedTranscript(PROJECT, usage(3));
438
+ // 42 from the row plus the 3 the discarded transcript proved. The old row-read banked 42 flat.
439
+ assert.is((_a = getBankedBaseline(PROJECT)) === null || _a === void 0 ? void 0 : _a.costUsd, 45, 'the stale 3 is banked, not dropped');
440
+ priv._sessionRef.store.aiAssistant.sessionUsage = usage(1);
441
+ el.finalizeCostSession();
442
+ assert.is(el.costSessionHistory[0].usage.costUsd, 46, '45 banked + the new turn');
443
+ clearCostSessionHistory(scope);
444
+ });
445
+ Suite('a memoised baseline survives a clear during a reload instead of being dropped', () => {
446
+ var _a, _b;
447
+ // Review case. The guard is broader than the derivation needs: once a baseline is memoised the
448
+ // new figure is determined without any rows, so deleting it there throws away something correct.
449
+ // Concretely — row at usage 120 / banked 90, baseline memoised at 90, 30 on screen — dropping it
450
+ // made the next turn re-derive `row.banked` and rewrite the row BACKWARDS from 120 to 95.
451
+ const scope = 'bank-clear-memoised';
452
+ clearCostSessionHistory(scope);
453
+ const el = element({
454
+ scope,
455
+ live: usage(30),
456
+ rows: [row({ usage: usage(120), banked: usage(90) })],
457
+ persistenceActive: true,
458
+ });
459
+ el.finalizeCostSession();
460
+ assert.is((_a = getBankedBaseline(PROJECT)) === null || _a === void 0 ? void 0 : _a.costUsd, 90, 'baseline memoised off the restored row');
461
+ // A chatConfig rebind puts the ledger back in flight, and the clear lands inside that window.
462
+ const priv = el;
463
+ priv._costHistoryLoaded = false;
464
+ priv.rebaselineOnClearedTranscript(PROJECT, usage(30));
465
+ assert.is((_b = getBankedBaseline(PROJECT)) === null || _b === void 0 ? void 0 : _b.costUsd, 120, '90 kept and the discarded 30 added');
466
+ priv._costHistoryLoaded = true;
467
+ priv._sessionRef.store.aiAssistant.sessionUsage = usage(5);
468
+ el.finalizeCostSession();
469
+ assert.is(el.costSessionHistory[0].usage.costUsd, 125, 'row moves forward, never backwards');
470
+ clearCostSessionHistory(scope);
471
+ });
472
+ Suite('clearing with the ledger disabled pins nothing, even though it reads as loaded', () => {
473
+ // Review case. `reloadCostSessionHistory` sets `_costHistoryLoaded` TRUE over a deliberately
474
+ // empty list when `enabled` is false — "nothing will be loaded", not "rows are known" — so the
475
+ // flag alone let a clear pin a baseline from a ledger that was never read. A host enabling
476
+ // history afterwards (an async setting, a feature flag) then had its real row buried by it.
477
+ const scope = 'bank-clear-disabled';
478
+ clearCostSessionHistory(scope);
479
+ const el = element({ scope, live: usage(3) });
480
+ el.chatConfig = { costHistory: { enabled: false, scope } };
481
+ const priv = el;
482
+ assert.is(priv._costHistoryLoaded, true, 'reads as loaded despite nothing being known');
483
+ priv.rebaselineOnClearedTranscript(PROJECT, usage(3));
484
+ assert.is(getBankedBaseline(PROJECT), undefined, 'nothing pinned while the ledger is off');
485
+ // History switched on afterwards, and this project turns out to have real spend.
486
+ el.chatConfig = { costHistory: { enabled: true, scope, title: 'Trade Blotter' } };
487
+ el.costSessionHistory = [row({ usage: usage(50), banked: usage(50) })];
488
+ priv._costHistoryLoaded = true;
489
+ priv._sessionRef.store.aiAssistant.sessionUsage = usage(1);
490
+ el.finalizeCostSession();
491
+ assert.is(el.costSessionHistory[0].usage.costUsd, 51, "the project's 50 is not buried");
492
+ clearCostSessionHistory(scope);
493
+ });
494
+ Suite('a host slotting its own build history does not have to enable our ledger', () => {
495
+ // Review case. `enabled` governs the BUILT-IN ledger and defaults false, while the Usage tab
496
+ // shows for showCost/showTokens which default true. Once the section's ?hidden binding became
497
+ // effective, short-circuiting on `enabled` above the slot check hid a host's own markup — and
498
+ // the slot is the only thing in that container.
499
+ const el = element({ scope: 'slotted-history', live: usage(1) });
500
+ el.chatConfig = { costHistory: { enabled: false } };
501
+ assert.is(el.settingsCostHistorySectionVisible, false, 'nothing slotted, ledger off → hidden');
502
+ el.settingsCostHistorySlotted = [document.createElement('div')];
503
+ assert.is(el.settingsCostHistorySectionVisible, true, 'slotted content shows without `enabled`');
504
+ assert.is(el.settingsCostHistoryHeadingVisible, true, 'and the heading comes with it');
505
+ });
292
506
  Suite('hides the lifetime section when no state key at all can produce a row', () => {
293
507
  const el = element({ scope: 'ledger-nokey', live: usage(3) });
294
508
  assert.is(el.settingsCostLifetimeVisible, true, 'visible with a key');
@@ -305,4 +519,20 @@ Suite('labels the live figures by whether the transcript is authoritative', () =
305
519
  const without = element({ scope: 'bank-label', live: usage(1), persistenceActive: false });
306
520
  assert.is(without.liveUsageIsProjectLifetime, false, 'not restored → this session only');
307
521
  });
522
+ Suite('a stored baseline is isolated from the row and the caller it passed through', () => {
523
+ var _a, _b;
524
+ // A baseline arrives as a reference into a ledger row (`resolveBankedUsage` returns `row.banked`
525
+ // or `row.usage`) and is read back onto the NEXT row's `banked` field, so an uncloned map entry
526
+ // is reachable from page-global state, two rows, and any host handed the row list. Mutating
527
+ // either end must not move the stored figure, or a project's baseline is silently rewritten for
528
+ // the rest of the page.
529
+ resetBankedBaselines();
530
+ const source = usage(10);
531
+ setBankedBaseline(PROJECT, source);
532
+ source.costUsd = 999;
533
+ assert.is((_a = getBankedBaseline(PROJECT)) === null || _a === void 0 ? void 0 : _a.costUsd, 10, 'mutating the source cannot reach the map');
534
+ const readBack = getBankedBaseline(PROJECT);
535
+ readBack.costUsd = 777;
536
+ assert.is((_b = getBankedBaseline(PROJECT)) === null || _b === void 0 ? void 0 : _b.costUsd, 10, 'mutating a read copy cannot reach the map');
537
+ });
308
538
  Suite.run();