@genesislcap/ai-assistant 15.6.2 → 15.7.0-ts7.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 (100) hide show
  1. package/dist/ai-assistant.api.json +392 -6
  2. package/dist/ai-assistant.d.ts +616 -24
  3. package/dist/chat-driver.cjs +315 -46
  4. package/dist/chat-driver.cjs.map +3 -3
  5. package/dist/chat-driver.mjs +315 -46
  6. package/dist/chat-driver.mjs.map +3 -3
  7. package/dist/custom-elements.json +254 -10
  8. package/dist/dts/channel/ai-activity-bus.d.ts.map +1 -1
  9. package/dist/dts/channel/ai-activity-channel.d.ts +51 -1
  10. package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
  11. package/dist/dts/components/activity-halo/activity-halo.d.ts.map +1 -1
  12. package/dist/dts/components/agent-picker/agent-picker.d.ts.map +1 -1
  13. package/dist/dts/components/agent-picker/agent-picker.template.d.ts.map +1 -1
  14. package/dist/dts/components/chat-bubble/chat-bubble.d.ts.map +1 -1
  15. package/dist/dts/components/chat-driver/chat-driver.d.ts +99 -1
  16. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  17. package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +1 -1
  18. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
  19. package/dist/dts/components/chat-markdown/chat-markdown.d.ts.map +1 -1
  20. package/dist/dts/components/flowing-waves-indicator.d.ts.map +1 -1
  21. package/dist/dts/components/halo-overlay.d.ts.map +1 -1
  22. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +2 -0
  23. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +1 -0
  24. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -0
  25. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  26. package/dist/dts/components/popout-manager/popout-manager.d.ts.map +1 -1
  27. package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
  28. package/dist/dts/components/waves-indicator.d.ts.map +1 -1
  29. package/dist/dts/main/blocked-state.test.d.ts +2 -0
  30. package/dist/dts/main/blocked-state.test.d.ts.map +1 -0
  31. package/dist/dts/main/main.d.ts +394 -6
  32. package/dist/dts/main/main.d.ts.map +1 -1
  33. package/dist/dts/main/main.styles.d.ts.map +1 -1
  34. package/dist/dts/main/main.styles.test.d.ts +2 -0
  35. package/dist/dts/main/main.styles.test.d.ts.map +1 -0
  36. package/dist/dts/main/main.template.d.ts +53 -0
  37. package/dist/dts/main/main.template.d.ts.map +1 -1
  38. package/dist/dts/main/main.types.d.ts +2 -2
  39. package/dist/dts/main/main.types.d.ts.map +1 -1
  40. package/dist/dts/state/ai-assistant-slice.d.ts +163 -7
  41. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  42. package/dist/dts/state/debug-event-log.d.ts +6 -1
  43. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  44. package/dist/dts/state/persistence/session-persistence-provider.d.ts.map +1 -1
  45. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  46. package/dist/dts/state/session-store.d.ts +12 -1
  47. package/dist/dts/state/session-store.d.ts.map +1 -1
  48. package/dist/dts/suggestions/chat-suggestions.d.ts.map +1 -1
  49. package/dist/dts/utils/animated-panel-toggle.d.ts.map +1 -1
  50. package/dist/dts/utils/message-partition.d.ts.map +1 -1
  51. package/dist/esm/components/agent-picker/agent-picker.js +2 -1
  52. package/dist/esm/components/agent-picker/agent-picker.template.js +2 -2
  53. package/dist/esm/components/chat-bubble/chat-bubble.js +2 -1
  54. package/dist/esm/components/chat-driver/align-event-globals.js +1 -0
  55. package/dist/esm/components/chat-driver/chat-driver.js +281 -38
  56. package/dist/esm/components/chat-driver/chat-driver.test.js +471 -14
  57. package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +312 -0
  58. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +94 -9
  59. package/dist/esm/components/popout-manager/popout-manager.js +4 -1
  60. package/dist/esm/components/settings-modal/settings-modal.template.js +5 -8
  61. package/dist/esm/main/blocked-state.test.js +969 -0
  62. package/dist/esm/main/main.js +834 -104
  63. package/dist/esm/main/main.styles.js +47 -0
  64. package/dist/esm/main/main.styles.test.js +87 -0
  65. package/dist/esm/main/main.template.js +131 -20
  66. package/dist/esm/state/ai-assistant-slice.js +145 -7
  67. package/dist/esm/state/ai-assistant-slice.test.js +138 -1
  68. package/dist/esm/state/debug-event-log.js +9 -3
  69. package/dist/esm/state/debug-event-log.test.js +49 -1
  70. package/dist/esm/state/persistence/session-persister.js +13 -10
  71. package/dist/esm/state/persistence/session-snapshot.js +3 -2
  72. package/dist/esm/state/persistence/session-snapshot.test.js +18 -0
  73. package/dist/esm/utils/collect-session-models.test.js +2 -1
  74. package/dist/esm/utils/condense-history.js +6 -4
  75. package/dist/esm/utils/condense-history.test.js +15 -21
  76. package/dist/esm/utils/flatten-sub-agent-messages.js +4 -3
  77. package/dist/esm/utils/history-transform.js +2 -1
  78. package/dist/esm/utils/resolve-cost-history-config.js +4 -3
  79. package/dist/esm/utils/resolve-preference-baseline.js +2 -1
  80. package/dist/esm/utils/sum-usage.js +7 -6
  81. package/dist/tsconfig.tsbuildinfo +1 -1
  82. package/docs/migration-GENC-1464.md +562 -0
  83. package/docs/sub_agent.md +20 -3
  84. package/package.json +17 -17
  85. package/src/channel/ai-activity-channel.ts +56 -2
  86. package/src/components/chat-driver/chat-driver.test.ts +549 -0
  87. package/src/components/chat-driver/chat-driver.ts +324 -14
  88. package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +438 -0
  89. package/src/components/orchestrating-driver/orchestrating-driver.ts +101 -6
  90. package/src/main/blocked-state.test.ts +1316 -0
  91. package/src/main/main.styles.test.ts +103 -0
  92. package/src/main/main.styles.ts +47 -0
  93. package/src/main/main.template.ts +131 -4
  94. package/src/main/main.ts +704 -10
  95. package/src/state/ai-assistant-slice.test.ts +215 -0
  96. package/src/state/ai-assistant-slice.ts +218 -8
  97. package/src/state/debug-event-log.test.ts +63 -0
  98. package/src/state/debug-event-log.ts +7 -2
  99. package/src/state/persistence/session-snapshot.test.ts +22 -0
  100. package/tsconfig.json +1 -0
@@ -1,5 +1,5 @@
1
1
  import { __awaiter, __rest } from "tslib";
2
- import { isObservableAIProviderRegistry, MalformedFunctionCallError, ResponseTruncatedError, } from '@genesislcap/foundation-ai';
2
+ import { BudgetExhaustedError, DEFAULT_BUDGET_EXHAUSTED_MESSAGE, isObservableAIProviderRegistry, MalformedFunctionCallError, ResponseTruncatedError, vendorTypeOfLabel, } from '@genesislcap/foundation-ai';
3
3
  import { NOOP_ACTIVITY_BUS } from '../../channel/ai-activity-bus';
4
4
  import { resolveChatProvider } from '../../config/validate-providers';
5
5
  import { clearSession, getMetaEvents, mergeMetaEvents, recordMetaEvent, recordTurnError, recordTurnRetry, } from '../../state/debug-event-log';
@@ -9,6 +9,40 @@ import { applyHistoryCap, buildCompactionSummaryPrompt, findCompactionCut, norma
9
9
  import { logger } from '../../utils/logger';
10
10
  import { sumUsage } from '../../utils/sum-usage';
11
11
  import { TOOL_FOLD_SYMBOL } from '../../utils/tool-fold';
12
+ /**
13
+ * Lift the reportable facts off a {@link BudgetExhaustedError}, or `undefined`
14
+ * when it carries none.
15
+ *
16
+ * "None" means **no figures AND no attributable vendor** — not merely no
17
+ * figures. Dropping the whole object on a figure-less 402 also dropped
18
+ * `vendorLabel`, which is the authoritative attribution source, and left the
19
+ * element's latch falling back to the driver's last-resolved provider, i.e. the
20
+ * PREVIOUS turn's vendor. A turn-1 Gemini call followed by a turn-2 classifier
21
+ * refused by Anthropic with a figure-less 402 then walled *Gemini* and advised
22
+ * switching to the vendor that had actually run out. Three live shapes reach
23
+ * here with no figures — a bare gateway 402, a code-only body, and the framed
24
+ * err frame whose figures are `null` (which the proxy now prefers over a
25
+ * misleading `0`, so this path is getting more common, not less).
26
+ *
27
+ * The figures alone still decide whether banner copy is composed — see
28
+ * `formatBlockedReason`, which returns `undefined` for a figure-less budget so a
29
+ * host-set explanation survives the latch.
30
+ */
31
+ const budgetDetailOf = (e) => {
32
+ var _a;
33
+ // The typed vendor is derived from the LABEL, not from `lastResolvedProvider`:
34
+ // the label comes from the transport that was actually refused, whereas the
35
+ // last-resolved provider is stale on the classification seam (an orchestrated
36
+ // turn classifies against the registry default, which this driver may never
37
+ // have resolved). The proxy's own `vendor` is the fallback for a transport
38
+ // whose static label no vendor claims (a multiplexing or white-labelled
39
+ // gateway). Omitted rather than set to `undefined` when neither resolves, so
40
+ // "we know the vendor" stays testable by presence alone.
41
+ const vendor = (_a = vendorTypeOfLabel(e.vendorLabel)) !== null && _a !== void 0 ? _a : vendorTypeOfLabel(e.serverVendor);
42
+ if (e.budgetUsd == null && e.spentUsd == null && !vendor)
43
+ return undefined;
44
+ return Object.assign(Object.assign({ budgetUsd: e.budgetUsd, spentUsd: e.spentUsd, vendorLabel: e.vendorLabel }, (vendor ? { vendor } : {})), (e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}));
45
+ };
12
46
  const DEFAULT_MAX_TOOL_ITERATIONS = 50;
13
47
  const DEFAULT_MAX_FOLD_OPERATIONS = 5;
14
48
  // TODO: dedup system prompts in-memory to allow raising this cap much higher.
@@ -230,11 +264,28 @@ export class ChatDriver extends EventTarget {
230
264
  * picked up on the next turn.
231
265
  */
232
266
  this.resolvedStatusCache = new Map();
233
- const { toolHandlers = {}, toolDefinitions = [], systemPrompt, primerHistory, maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS, maxFoldOperations = DEFAULT_MAX_FOLD_OPERATIONS, condenseBatchCalls = 1, maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS, sessionKey = '', activityBus = NOOP_ACTIVITY_BUS, } = config;
267
+ /**
268
+ * Set the moment a budget wall is observed anywhere in this turn — this
269
+ * driver's own 402, or a sub-agent's (which surfaces here only as a
270
+ * `'budget_exhausted'` tool result). Read at the top of the tool loop to end
271
+ * the turn before issuing another model call that would hit the same wall.
272
+ * Reset per turn alongside the other per-turn counters.
273
+ */
274
+ this.budgetExhaustedThisTurn = false;
275
+ /**
276
+ * Whether this turn's budget wall came from a SUB-AGENT rather than this
277
+ * driver's own request. Decides whether `lastResolvedProvider` is a valid
278
+ * attribution fallback: for an own wall it is the refusing vendor, for a
279
+ * child's wall it is the parent's vendor — the one known NOT to have refused.
280
+ * Reset per turn alongside `budgetWallDetail`.
281
+ */
282
+ this.budgetWallViaSubAgent = false;
283
+ const { toolHandlers = {}, toolDefinitions = [], systemPrompt, primerHistory, maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS, maxFoldOperations = DEFAULT_MAX_FOLD_OPERATIONS, condenseBatchCalls = 1, maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS, sessionKey = '', activityBus = NOOP_ACTIVITY_BUS, budgetExhaustedMessage = DEFAULT_BUDGET_EXHAUSTED_MESSAGE, } = config;
234
284
  this.maxToolIterations = maxToolIterations;
235
285
  this.condenseBatchCalls = condenseBatchCalls;
236
286
  this.sessionKey = sessionKey;
237
287
  this.activityBus = activityBus;
288
+ this.budgetExhaustedMessage = budgetExhaustedMessage;
238
289
  if (typeof toolHandlers === 'function') {
239
290
  this.toolHandlersFactory = toolHandlers;
240
291
  this.toolHandlers = {};
@@ -364,8 +415,61 @@ export class ChatDriver extends EventTarget {
364
415
  * (rather than set to `undefined`) so a happy-path result stays byte-identical to
365
416
  * the historical `{ reason: 'done' }`.
366
417
  */
367
- turnDone(failureReason) {
368
- return failureReason ? { reason: 'done', failureReason } : { reason: 'done' };
418
+ turnDone(failureReason, budget) {
419
+ if (!failureReason)
420
+ return { reason: 'done' };
421
+ // `budget` is likewise omitted rather than set to `undefined`, so a non-budget
422
+ // failure's shape is unchanged for a consumer that structurally compares it.
423
+ return budget ? { reason: 'done', failureReason, budget } : { reason: 'done', failureReason };
424
+ }
425
+ /**
426
+ * Terminal budget outcome for a wall hit **outside** the tool loop — today,
427
+ * `OrchestratingDriver`'s classification phase, which calls the provider
428
+ * directly and so never enters `runToolLoop`.
429
+ *
430
+ * Does **not** publish `tool-loop-end`: no `tool-loop-start` was published for
431
+ * the classify phase, and an unbalanced end would break start/end pairing for
432
+ * subscribers that rely on it. The driver **return value** is what reports this
433
+ * case — see `FoundationAiAssistant`'s latch, which reads both seams for
434
+ * exactly this reason.
435
+ *
436
+ * The non-sub-agent tail of the in-loop `BudgetExhaustedError` branch lives
437
+ * here so there is one copy of the log line, the debug-log entry, the
438
+ * transcript bubble and the result shape rather than two that can drift.
439
+ *
440
+ * @param pendingUserMessage - a user message that has NOT yet been appended,
441
+ * appended first so the answer does not end up replying to nothing. Only the
442
+ * classification seam passes it: `OrchestratingDriver` dispatches the user's
443
+ * text as an optimistic `history-updated` detail and leaves the real append
444
+ * to `chatDriver.sendMessage`, which never runs when `classify()` throws — so
445
+ * the bubble below would re-dispatch a history the user's own message was
446
+ * never in, and it would vanish from the transcript on the next render. The
447
+ * in-loop caller has already appended it and passes nothing.
448
+ *
449
+ * @internal
450
+ */
451
+ reportBudgetExhausted(e, pendingUserMessage) {
452
+ var _a, _b;
453
+ if (pendingUserMessage)
454
+ this.appendToHistory(pendingUserMessage);
455
+ this.budgetExhaustedThisTurn = true;
456
+ logger.error('ChatDriver: AI budget exhausted', e);
457
+ recordTurnError(this.sessionKey, 'budget-exhausted', {
458
+ agent: this.activeAgentName,
459
+ provider: this.lastResolvedProviderName,
460
+ // The registry ALIAS (e.g. 'high') is what `provider` records; the vendor
461
+ // is the thing a per-vendor budget is actually scoped to, and it was known
462
+ // at both ends and discarded in the middle until now. Taken from the
463
+ // refusing transport's label first — this method also serves the
464
+ // classification seam, where `lastResolvedProvider` is the PREVIOUS turn's
465
+ // vendor (or nothing), because classify runs against the registry default.
466
+ vendor: (_b = (_a = vendorTypeOfLabel(e.vendorLabel)) !== null && _a !== void 0 ? _a : vendorTypeOfLabel(e.serverVendor)) !== null && _b !== void 0 ? _b : this.lastResolvedProvider,
467
+ budgetUsd: e.budgetUsd,
468
+ spentUsd: e.spentUsd,
469
+ isSubAgent: this.isSubAgent,
470
+ });
471
+ this.appendToHistory({ role: 'assistant', content: this.budgetExhaustedMessage });
472
+ return this.turnDone('budget-exhausted', budgetDetailOf(e));
369
473
  }
370
474
  /** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
371
475
  static failureReasonOf(result) {
@@ -375,10 +479,39 @@ export class ChatDriver extends EventTarget {
375
479
  * Build the `tool-loop-end` event detail for a turn's result. A failure carries a
376
480
  * `{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,
377
481
  * kept byte-identical so subscribers see exactly what they always have.
482
+ *
483
+ * A budget failure additionally carries `vendor` — the concrete vendor
484
+ * (`'anthropic'`/`'gemini'`) the walled turn resolved to, which the driver knows
485
+ * and used to discard. Optional and additive: a subscriber reading only
486
+ * `failureReason` is unaffected, a non-budget failure still emits the historical
487
+ * `{ failureReason }` with no `vendor` key, and the value is a plain string so
488
+ * the detail stays structured-cloneable for the cross-tab hop. It is the field a
489
+ * per-vendor budget model needs and the one that would be awkward to retrofit.
378
490
  */
379
- static loopEndDetail(result) {
491
+ loopEndDetail(result) {
492
+ var _a;
380
493
  const failureReason = ChatDriver.failureReasonOf(result);
381
- return failureReason ? { failureReason } : undefined;
494
+ if (!failureReason)
495
+ return undefined;
496
+ if (failureReason !== 'budget-exhausted') {
497
+ return { failureReason };
498
+ }
499
+ // The figures ride the event, not just the return value: this publish happens in
500
+ // sendMessage's `finally`, so for an in-loop wall it reaches the host's latch FIRST
501
+ // and the return-value seam is then a no-op (the latch is idempotent). Omitted
502
+ // entirely — never set to undefined — when the proxy sent no figures.
503
+ const budget = result.reason === 'done' ? result.budget : undefined;
504
+ // Prefer the refusing transport's own attribution over the driver's
505
+ // last-resolved provider, for the same staleness reason as `budgetDetailOf`.
506
+ //
507
+ // The fallback is legitimate ONLY for this driver's own wall — its resolved
508
+ // provider IS the refuser then. When the wall came from a SUB-AGENT
509
+ // (`budgetWallViaSubAgent`), the refuser is the child's vendor, and on a
510
+ // mixed registry `lastResolvedProvider` is the one vendor known NOT to have
511
+ // refused; an unattributable child wall degrades to the vendor-agnostic
512
+ // event instead, which the host's latch handles fail-safe.
513
+ const vendor = (_a = budget === null || budget === void 0 ? void 0 : budget.vendor) !== null && _a !== void 0 ? _a : (this.budgetWallViaSubAgent ? undefined : this.lastResolvedProvider);
514
+ return Object.assign(Object.assign({ failureReason }, (vendor ? { vendor } : {})), (budget ? { budget } : {}));
382
515
  }
383
516
  /**
384
517
  * Swap in a new agent's configuration. Called by OrchestratingDriver before
@@ -546,7 +679,7 @@ export class ChatDriver extends EventTarget {
546
679
  return this.resolvedStatusCache.get(name);
547
680
  let status = {};
548
681
  try {
549
- const resolved = yield ((_a = provider.getStatus) === null || _a === void 0 ? void 0 : _a.call(provider));
682
+ const resolved = yield (_a = provider.getStatus) === null || _a === void 0 ? void 0 : _a.call(provider);
550
683
  if (resolved) {
551
684
  status = { model: resolved.model, provider: resolved.provider };
552
685
  }
@@ -603,10 +736,12 @@ export class ChatDriver extends EventTarget {
603
736
  * under a separate session key, so recording here would orphan the event off
604
737
  * the user-visible debug-log timeline.)
605
738
  */
606
- failSubAgent(reason) {
739
+ failSubAgent(reason, budget) {
607
740
  if (!this.isSubAgent || this.subAgentFailure)
608
741
  return;
609
- this.subAgentFailure = { reason };
742
+ // Omitted rather than set to `undefined` so a non-budget failure's shape is
743
+ // unchanged for a structural comparison, matching `turnDone`.
744
+ this.subAgentFailure = budget ? { reason, budget } : { reason };
610
745
  }
611
746
  /**
612
747
  * Returns true if `releaseAgent` was called during the most recent turn.
@@ -790,13 +925,10 @@ export class ChatDriver extends EventTarget {
790
925
  }
791
926
  else {
792
927
  const conversationContext = history
793
- .filter((m) => {
794
- var _a, _b;
795
- return (m.role === 'user' || m.role === 'assistant') &&
796
- !((_a = m.toolCalls) === null || _a === void 0 ? void 0 : _a.length) &&
797
- !m.thinking &&
798
- !!((_b = m.content) === null || _b === void 0 ? void 0 : _b.trim());
799
- })
928
+ .filter((m) => { var _a, _b; return (m.role === 'user' || m.role === 'assistant') &&
929
+ !((_a = m.toolCalls) === null || _a === void 0 ? void 0 : _a.length) &&
930
+ !m.thinking &&
931
+ !!((_b = m.content) === null || _b === void 0 ? void 0 : _b.trim()); })
800
932
  .slice(-SUGGESTIONS_HISTORY_WINDOW)
801
933
  .map((m) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`)
802
934
  .join('\n');
@@ -851,7 +983,8 @@ export class ChatDriver extends EventTarget {
851
983
  */
852
984
  compact() {
853
985
  return __awaiter(this, void 0, void 0, function* () {
854
- var _a, _b, _c, _d, _e, _f;
986
+ var _a, _b, _c, _d;
987
+ var _e, _f;
855
988
  if (this.busy)
856
989
  return null;
857
990
  const defaultProvider = this.providerRegistry.default();
@@ -874,7 +1007,7 @@ export class ChatDriver extends EventTarget {
874
1007
  const failMessage = "Sorry, I couldn't compact the conversation just now. Nothing was changed — please try again.";
875
1008
  let summaryText;
876
1009
  try {
877
- summaryText = (_b = (_a = (yield defaultProvider.prompt(userMessage, { systemPrompt }))) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : '';
1010
+ summaryText = (_e = (_a = (yield defaultProvider.prompt(userMessage, { systemPrompt }))) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _e !== void 0 ? _e : '';
878
1011
  }
879
1012
  catch (e) {
880
1013
  logger.warn(`ChatDriver: compaction summary failed: ${e instanceof Error ? e.message : e}`);
@@ -893,7 +1026,7 @@ export class ChatDriver extends EventTarget {
893
1026
  break;
894
1027
  }
895
1028
  }
896
- const status = yield ((_c = defaultProvider.getStatus) === null || _c === void 0 ? void 0 : _c.call(defaultProvider));
1029
+ const status = yield (_b = defaultProvider.getStatus) === null || _b === void 0 ? void 0 : _b.call(defaultProvider);
897
1030
  const createdAt = new Date().toISOString();
898
1031
  const summaryMessage = {
899
1032
  role: 'compacted-summary',
@@ -902,10 +1035,10 @@ export class ChatDriver extends EventTarget {
902
1035
  // for the oldest messages and sits at the head of history, so this keeps it
903
1036
  // sorted to the head in any timestamp-ordered view (the debug timeline) and
904
1037
  // history time-monotonic. Its true creation time is in compaction.createdAt.
905
- timestamp: (_e = (_d = toCompact[0]) === null || _d === void 0 ? void 0 : _d.timestamp) !== null && _e !== void 0 ? _e : createdAt,
1038
+ timestamp: (_f = (_c = toCompact[0]) === null || _c === void 0 ? void 0 : _c.timestamp) !== null && _f !== void 0 ? _f : createdAt,
906
1039
  compaction: {
907
1040
  compactedCount: toCompact.length,
908
- coveredThroughTimestamp: (_f = toCompact[toCompact.length - 1]) === null || _f === void 0 ? void 0 : _f.timestamp,
1041
+ coveredThroughTimestamp: (_d = toCompact[toCompact.length - 1]) === null || _d === void 0 ? void 0 : _d.timestamp,
909
1042
  tokensBefore,
910
1043
  model: status === null || status === void 0 ? void 0 : status.model,
911
1044
  createdAt,
@@ -1016,7 +1149,8 @@ export class ChatDriver extends EventTarget {
1016
1149
  * Marks the interaction message as resolved so it renders read-only on re-render.
1017
1150
  */
1018
1151
  resolveInteraction(interactionId, result) {
1019
- var _a, _b;
1152
+ var _a;
1153
+ var _b;
1020
1154
  const interaction = this.pendingInteractions.get(interactionId);
1021
1155
  if (interaction) {
1022
1156
  // Clear the timeout so a user resolution doesn't later fire a stale
@@ -1109,7 +1243,7 @@ export class ChatDriver extends EventTarget {
1109
1243
  ? result.costUsd
1110
1244
  : undefined;
1111
1245
  this.history[idx] = Object.assign(Object.assign(Object.assign({}, this.history[idx]), { interaction: Object.assign(Object.assign({}, this.history[idx].interaction), { resolved: result }) }), (costUsd != null
1112
- ? { externalCostUsd: ((_a = this.history[idx].externalCostUsd) !== null && _a !== void 0 ? _a : 0) + costUsd }
1246
+ ? { externalCostUsd: ((_b = this.history[idx].externalCostUsd) !== null && _b !== void 0 ? _b : 0) + costUsd }
1113
1247
  : {}));
1114
1248
  this.dispatchEvent(new CustomEvent('history-updated', {
1115
1249
  detail: this.history,
@@ -1137,7 +1271,7 @@ export class ChatDriver extends EventTarget {
1137
1271
  this.pendingInteractions.delete(interactionId);
1138
1272
  // Tear down the live context on RESOLVE (not on element unmount) — this closes
1139
1273
  // any sockets/timers the widget parked on it via `onDispose`. (GENC-1390)
1140
- (_b = this.interactionContexts.get(interactionId)) === null || _b === void 0 ? void 0 : _b.dispose();
1274
+ (_a = this.interactionContexts.get(interactionId)) === null || _a === void 0 ? void 0 : _a.dispose();
1141
1275
  this.interactionContexts.delete(interactionId);
1142
1276
  }
1143
1277
  else {
@@ -1191,6 +1325,9 @@ export class ChatDriver extends EventTarget {
1191
1325
  this.subAgentCompletion = undefined;
1192
1326
  this.subAgentFailure = undefined;
1193
1327
  this.agentReleaseRequested = false;
1328
+ this.budgetExhaustedThisTurn = false;
1329
+ this.budgetWallDetail = undefined;
1330
+ this.budgetWallViaSubAgent = false;
1194
1331
  this.appendToHistory({ role: 'user', content: userInput, attachments });
1195
1332
  this.turnStartedAt = Date.now();
1196
1333
  recordMetaEvent(this.sessionKey, 'turn.start', {
@@ -1228,7 +1365,7 @@ export class ChatDriver extends EventTarget {
1228
1365
  });
1229
1366
  this.busy = false;
1230
1367
  this.endTurn();
1231
- this.activityBus.publish('tool-loop-end', ChatDriver.loopEndDetail(result));
1368
+ this.activityBus.publish('tool-loop-end', this.loopEndDetail(result));
1232
1369
  }
1233
1370
  });
1234
1371
  }
@@ -1322,7 +1459,8 @@ export class ChatDriver extends EventTarget {
1322
1459
  */
1323
1460
  invokeSubAgent(name, options) {
1324
1461
  return __awaiter(this, void 0, void 0, function* () {
1325
- var _a, _b, _c, _d, _e;
1462
+ var _a;
1463
+ var _b, _c, _d, _e;
1326
1464
  const subConfig = this.subAgentsMap.get(name);
1327
1465
  if (!subConfig) {
1328
1466
  const available = [...this.subAgentsMap.keys()].join(', ') || '(none)';
@@ -1491,12 +1629,35 @@ export class ChatDriver extends EventTarget {
1491
1629
  // provider ignored forced tool use and returned text). The previous
1492
1630
  // final-text fallback is intentionally gone — sub-agents return a
1493
1631
  // structured outcome only, and the parent handler decides how to recover.
1494
- const reason = (_e = (_d = child.getSubAgentFailure()) === null || _d === void 0 ? void 0 : _d.reason) !== null && _e !== void 0 ? _e : 'max_iterations';
1632
+ const failure = child.getSubAgentFailure();
1633
+ const reason = (_d = failure === null || failure === void 0 ? void 0 : failure.reason) !== null && _d !== void 0 ? _d : 'max_iterations';
1495
1634
  // Record under THIS (parent) driver's session so the failure lands on the
1496
1635
  // user-visible debug-log timeline — the child ran under its own session key.
1497
1636
  // This is also the only telemetry for the defensive default above, where the
1498
1637
  // child's loop ended without recording an explicit failure reason.
1499
1638
  recordMetaEvent(this.sessionKey, 'subagent.failed', { agent: name, reason });
1639
+ // A child that hit the budget wall walls this driver too — the cap is shared,
1640
+ // and the parent's very next model call would 402 as well. Flagged (not
1641
+ // thrown) because the handler's return value is still appended as a tool
1642
+ // result: the loop reads the flag before the next provider call and ends the
1643
+ // turn there, which is what makes the `'budget_exhausted'` doc's "terminal
1644
+ // for the parent too" actually true.
1645
+ //
1646
+ // The child's attribution is inherited with it: the vendor that refused is
1647
+ // the child's, which under a mixed registry is not this driver's. See
1648
+ // `budgetWallDetail` for what mis-attributing it costs.
1649
+ //
1650
+ // FIRST attribution wins (`??=`). With batched delegations, child A walling
1651
+ // WITH attribution can be followed by child B walling WITHOUT one (a
1652
+ // figure-less 402 behind a transport label no vendor claims); a plain
1653
+ // assignment reset the field to `undefined` and the short-circuit's
1654
+ // fallback then named the parent's own vendor — the one known NOT to have
1655
+ // refused.
1656
+ if (reason === 'budget_exhausted') {
1657
+ this.budgetExhaustedThisTurn = true;
1658
+ this.budgetWallViaSubAgent = true;
1659
+ (_e = this.budgetWallDetail) !== null && _e !== void 0 ? _e : (this.budgetWallDetail = failure === null || failure === void 0 ? void 0 : failure.budget);
1660
+ }
1500
1661
  return { outcome: { ok: false, reason }, trace };
1501
1662
  });
1502
1663
  }
@@ -1512,6 +1673,9 @@ export class ChatDriver extends EventTarget {
1512
1673
  this.beginTurn();
1513
1674
  this.subAgentCompletion = undefined;
1514
1675
  this.subAgentFailure = undefined;
1676
+ this.budgetExhaustedThisTurn = false;
1677
+ this.budgetWallDetail = undefined;
1678
+ this.budgetWallViaSubAgent = false;
1515
1679
  this.turnStartedAt = Date.now();
1516
1680
  recordMetaEvent(this.sessionKey, 'turn.start', {
1517
1681
  phase: 'continueFromHistory',
@@ -1548,7 +1712,7 @@ export class ChatDriver extends EventTarget {
1548
1712
  });
1549
1713
  this.busy = false;
1550
1714
  this.endTurn();
1551
- this.activityBus.publish('tool-loop-end', ChatDriver.loopEndDetail(result));
1715
+ this.activityBus.publish('tool-loop-end', this.loopEndDetail(result));
1552
1716
  }
1553
1717
  });
1554
1718
  }
@@ -1702,7 +1866,8 @@ export class ChatDriver extends EventTarget {
1702
1866
  // oxlint-disable-next-line complexity
1703
1867
  runToolLoop(userInput, attachments, transientPrimer) {
1704
1868
  return __awaiter(this, void 0, void 0, function* () {
1705
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
1869
+ var _a, _b, _c, _d, _e, _f, _g;
1870
+ var _h, _j, _k, _l, _m, _o;
1706
1871
  if (!this.systemPrompt) {
1707
1872
  logger.warn('ChatDriver: no systemPrompt set. The assistant will have no instructions — provide a systemPrompt via agents config or the foundation-ai-assistant property.');
1708
1873
  }
@@ -1732,8 +1897,48 @@ export class ChatDriver extends EventTarget {
1732
1897
  if (this.turnController.signal.aborted) {
1733
1898
  return this.completeAbortedTurn();
1734
1899
  }
1900
+ // A budget wall observed earlier this turn ends it HERE, before another
1901
+ // model call. Reachable only via a sub-agent (this driver's own 402
1902
+ // returns straight out of the catch below) — `invokeSubAgent` sets the
1903
+ // flag, its `{ ok: false, reason: 'budget_exhausted' }` outcome is
1904
+ // appended as a tool result, and without this the loop would call the
1905
+ // provider again into the same wall: N batched sub-agent calls otherwise
1906
+ // cost N doomed children AND a doomed parent call.
1907
+ //
1908
+ // Scoped strictly to the budget reason: every other SubAgentFailureReason
1909
+ // is something the parent can legitimately recover from, so those still
1910
+ // let the loop continue.
1911
+ if (this.budgetExhaustedThisTurn) {
1912
+ logger.error('ChatDriver: ending the turn — a sub-agent hit the AI budget wall');
1913
+ recordTurnError(this.sessionKey, 'budget-exhausted', {
1914
+ agent: this.activeAgentName,
1915
+ provider: this.lastResolvedProviderName,
1916
+ // The CHILD's vendor when it knew one, and NOTHING otherwise. On this
1917
+ // path the wall is definitionally the child's, and on a mixed registry
1918
+ // `lastResolvedProvider` is this driver's own vendor — the one known
1919
+ // NOT to have refused. An unattributable child wall must degrade to
1920
+ // the vendor-agnostic block (which `latchBlockedFrom` handles
1921
+ // fail-safe), never to a vendor that is known to be wrong: naming the
1922
+ // parent's vendor here walled BOTH — the child's via its own
1923
+ // tool-loop-end, the parent's via this event — and derived `blocked`
1924
+ // over headroom that still existed.
1925
+ vendor: (_a = this.budgetWallDetail) === null || _a === void 0 ? void 0 : _a.vendor,
1926
+ via: 'sub-agent',
1927
+ isSubAgent: this.isSubAgent,
1928
+ });
1929
+ if (this.isSubAgent) {
1930
+ this.failSubAgent('budget_exhausted', this.budgetWallDetail);
1931
+ }
1932
+ else {
1933
+ this.appendToHistory({ role: 'assistant', content: this.budgetExhaustedMessage });
1934
+ }
1935
+ // Carried onto the result so `loopEndDetail` publishes the refusing
1936
+ // vendor rather than falling through to `lastResolvedProvider`, and the
1937
+ // host's latch walls the vendor that actually ran out.
1938
+ return this.turnDone('budget-exhausted', this.budgetWallDetail);
1939
+ }
1735
1940
  const promptCtx = {
1736
- agentName: (_a = this.activeAgentName) !== null && _a !== void 0 ? _a : '',
1941
+ agentName: (_h = this.activeAgentName) !== null && _h !== void 0 ? _h : '',
1737
1942
  history: this.history,
1738
1943
  turnIndex: iterations - 1,
1739
1944
  signal: this.turnController.signal,
@@ -1806,7 +2011,7 @@ export class ChatDriver extends EventTarget {
1806
2011
  // loses all summarized context. `normalizeForProvider` only touches
1807
2012
  // `compacted-summary`, so it's a safe pass-through for everything else.
1808
2013
  const primer = normalizeForProvider([
1809
- ...((_b = this.primerHistory) !== null && _b !== void 0 ? _b : []),
2014
+ ...((_j = this.primerHistory) !== null && _j !== void 0 ? _j : []),
1810
2015
  ...(transientPrimer !== null && transientPrimer !== void 0 ? transientPrimer : []),
1811
2016
  ]);
1812
2017
  const baseHistory = firstLlmCall ? this.history.slice(0, -1) : this.history;
@@ -1972,6 +2177,44 @@ export class ChatDriver extends EventTarget {
1972
2177
  }
1973
2178
  return this.turnDone('response-truncated');
1974
2179
  }
2180
+ // The AI-spend budget is gone and the proxy refused the request (HTTP
2181
+ // 402). Terminal in the strongest sense available to us: unlike a
2182
+ // truncation — which a smaller request would get past — *no* request
2183
+ // succeeds until someone raises the budget out of band. So there is no
2184
+ // retry here and no "try again" in the copy; the turn ends and the host
2185
+ // locks the composer off the `'budget-exhausted'` failure reason (see
2186
+ // `FoundationAiAssistant.blocked`).
2187
+ if (e instanceof BudgetExhaustedError) {
2188
+ // Flagged as well as returned: a sub-agent's wall reaches the PARENT
2189
+ // only as a tool result, and the parent must not issue another model
2190
+ // call after it (see the short-circuit at the top of this loop).
2191
+ this.budgetExhaustedThisTurn = true;
2192
+ this.budgetWallDetail = budgetDetailOf(e);
2193
+ // Paired with the detail everywhere it is written: this is the
2194
+ // driver's OWN wall, so the sub-agent marker must not survive from an
2195
+ // earlier child and suppress the `lastResolvedProvider` fallback that
2196
+ // is legitimate here. Unreachable today (the short-circuit fires
2197
+ // before a second wall can land in one turn) — kept structural so the
2198
+ // pairing does not depend on that ordering staying true.
2199
+ this.budgetWallViaSubAgent = false;
2200
+ if (this.isSubAgent) {
2201
+ logger.error('ChatDriver: AI budget exhausted', e);
2202
+ recordTurnError(this.sessionKey, 'budget-exhausted', {
2203
+ agent: this.activeAgentName,
2204
+ provider: this.lastResolvedProviderName,
2205
+ vendor: (_l = (_k = vendorTypeOfLabel(e.vendorLabel)) !== null && _k !== void 0 ? _k : vendorTypeOfLabel(e.serverVendor)) !== null && _l !== void 0 ? _l : this.lastResolvedProvider,
2206
+ budgetUsd: e.budgetUsd,
2207
+ spentUsd: e.spentUsd,
2208
+ isSubAgent: true,
2209
+ });
2210
+ // Bubble a typed failure to the parent instead of speaking to the
2211
+ // user, carrying the attribution so the parent walls the vendor that
2212
+ // actually refused rather than its own.
2213
+ this.failSubAgent('budget_exhausted', this.budgetWallDetail);
2214
+ return this.turnDone('budget-exhausted', this.budgetWallDetail);
2215
+ }
2216
+ return this.reportBudgetExhausted(e);
2217
+ }
1975
2218
  // A request timeout from the transport (tagged `TimeoutError`) is not a
1976
2219
  // bug on our end — surface it distinctly instead of letting it fall
1977
2220
  // through to the generic "something went wrong" catch. No auto-retry:
@@ -2037,15 +2280,15 @@ export class ChatDriver extends EventTarget {
2037
2280
  if (this.lastResolvedProviderName !== undefined) {
2038
2281
  response.providerName = this.lastResolvedProviderName;
2039
2282
  }
2040
- const isThinkingStep = response.content && ((_c = response.toolCalls) === null || _c === void 0 ? void 0 : _c.length);
2041
- const isEmptyResponse = !((_d = response.content) === null || _d === void 0 ? void 0 : _d.trim()) && !((_e = response.toolCalls) === null || _e === void 0 ? void 0 : _e.length);
2283
+ const isThinkingStep = response.content && ((_b = response.toolCalls) === null || _b === void 0 ? void 0 : _b.length);
2284
+ const isEmptyResponse = !((_c = response.content) === null || _c === void 0 ? void 0 : _c.trim()) && !((_d = response.toolCalls) === null || _d === void 0 ? void 0 : _d.length);
2042
2285
  // A pre-output refusal (safety-classifier decline, e.g. Fable 5 `stop_reason: 'refusal'`)
2043
2286
  // comes back with empty content, so it looks like a blank response — but it is deterministic:
2044
2287
  // retrying re-sends the identical request and refuses again, burning up to
2045
2288
  // MAX_EMPTY_RESPONSE_RETRIES turns on the most expensive models for the same outcome, ending
2046
2289
  // in the misleading "blank response" message. Treat it as a terminal, non-retried failure with
2047
2290
  // its own reason and message. (GENC-1461)
2048
- const isRefusal = ((_f = response.responseMeta) === null || _f === void 0 ? void 0 : _f.finishReason) === 'refusal';
2291
+ const isRefusal = ((_e = response.responseMeta) === null || _e === void 0 ? void 0 : _e.finishReason) === 'refusal';
2049
2292
  if (isEmptyResponse) {
2050
2293
  emptyResponseAttempts += 1;
2051
2294
  if (!isRefusal && emptyResponseAttempts < MAX_EMPTY_RESPONSE_RETRIES) {
@@ -2104,7 +2347,7 @@ export class ChatDriver extends EventTarget {
2104
2347
  emptyResponseAttempts = 0;
2105
2348
  malformedAttempts = 0;
2106
2349
  setupTransportAttempts = 0;
2107
- if (!((_g = response.toolCalls) === null || _g === void 0 ? void 0 : _g.length)) {
2350
+ if (!((_f = response.toolCalls) === null || _f === void 0 ? void 0 : _f.length)) {
2108
2351
  break;
2109
2352
  }
2110
2353
  const [toolCalls, systemCalls] = response.toolCalls.reduce((acc, tc) => {
@@ -2310,7 +2553,7 @@ export class ChatDriver extends EventTarget {
2310
2553
  // The response was appended before execution — find it and annotate.
2311
2554
  let tcMsgIdx = -1;
2312
2555
  for (let i = this.history.length - 1; i >= 0; i -= 1) {
2313
- if (this.history[i].role === 'assistant' && ((_h = this.history[i].toolCalls) === null || _h === void 0 ? void 0 : _h.length)) {
2556
+ if (this.history[i].role === 'assistant' && ((_g = this.history[i].toolCalls) === null || _g === void 0 ? void 0 : _g.length)) {
2314
2557
  tcMsgIdx = i;
2315
2558
  break;
2316
2559
  }
@@ -2348,7 +2591,7 @@ export class ChatDriver extends EventTarget {
2348
2591
  const unknownTools = [
2349
2592
  ...new Set([
2350
2593
  ...this.recentUnknownToolNames,
2351
- ...((_j = response.toolCalls) !== null && _j !== void 0 ? _j : [])
2594
+ ...((_m = response.toolCalls) !== null && _m !== void 0 ? _m : [])
2352
2595
  .filter((tc) => unknownToolIds.has(tc.id))
2353
2596
  .map((tc) => tc.name),
2354
2597
  ]),
@@ -2360,7 +2603,7 @@ export class ChatDriver extends EventTarget {
2360
2603
  const staleTools = [
2361
2604
  ...new Set([
2362
2605
  ...this.recentStaleToolNames,
2363
- ...((_k = response.toolCalls) !== null && _k !== void 0 ? _k : [])
2606
+ ...((_o = response.toolCalls) !== null && _o !== void 0 ? _o : [])
2364
2607
  .filter((tc) => staleToolIds.has(tc.id))
2365
2608
  .map((tc) => tc.name),
2366
2609
  ]),