@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
@@ -969,6 +969,67 @@ var GeminiProvider = class {
969
969
  }
970
970
  };
971
971
 
972
+ // ../../foundation-ai/dist/esm/transports/budget-exhausted-error.js
973
+ var BUDGET_EXCEEDED_CODE = "BUDGET_EXCEEDED";
974
+ var VENDOR_LABELS = Object.freeze({
975
+ anthropic: "Anthropic",
976
+ gemini: "Gemini",
977
+ openai: "OpenAI",
978
+ chrome: "Chrome"
979
+ });
980
+ var BUDGETED_VENDORS = Object.freeze([
981
+ "anthropic",
982
+ "gemini"
983
+ ]);
984
+ var VENDOR_TYPE_BY_LABEL = new Map(Object.entries(VENDOR_LABELS).map(([type, label]) => [
985
+ label.toLowerCase(),
986
+ type
987
+ ]));
988
+ function vendorTypeOfLabel(label) {
989
+ if (typeof label !== "string")
990
+ return void 0;
991
+ return VENDOR_TYPE_BY_LABEL.get(label.trim().toLowerCase());
992
+ }
993
+ var BUDGET_EXCEEDED_STATUS = 402;
994
+ var DEFAULT_BUDGET_EXHAUSTED_MESSAGE = "You've reached your AI usage limit. Contact your administrator to raise it.";
995
+ var BudgetExhaustedError = class extends Error {
996
+ constructor(vendorLabel, budgetUsd, spentUsd, detail, extra) {
997
+ super(`${vendorLabel} request refused: the AI usage budget is exhausted` + (budgetUsd != null ? ` (spent ${spentUsd != null ? `$${spentUsd}` : "an unknown amount"} of a $${budgetUsd} budget)` : "") + (detail ? `: ${detail}` : "") + ". Retrying will not clear this \u2014 the budget must be raised.");
998
+ this.vendorLabel = vendorLabel;
999
+ this.budgetUsd = budgetUsd;
1000
+ this.spentUsd = spentUsd;
1001
+ this.detail = detail;
1002
+ this.name = "BudgetExhaustedError";
1003
+ this.otherVendorAvailable = extra === null || extra === void 0 ? void 0 : extra.otherVendorAvailable;
1004
+ this.serverVendor = extra === null || extra === void 0 ? void 0 : extra.serverVendor;
1005
+ }
1006
+ };
1007
+ var isBudgetRejection = (status, code) => status === BUDGET_EXCEEDED_STATUS || code === BUDGET_EXCEEDED_CODE;
1008
+ var parseJsonOrUndefined = (text) => {
1009
+ try {
1010
+ return JSON.parse(text);
1011
+ } catch (_a) {
1012
+ return void 0;
1013
+ }
1014
+ };
1015
+ var codeOf = (payload) => {
1016
+ const code = payload === null || payload === void 0 ? void 0 : payload.code;
1017
+ return typeof code === "string" ? code : void 0;
1018
+ };
1019
+ var numberOrUndefined = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
1020
+ var booleanOrUndefined = (value) => typeof value === "boolean" ? value : void 0;
1021
+ var stringOrUndefined = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
1022
+ function budgetExhaustedFrom(vendorLabel, payload, fallbackDetail) {
1023
+ var _a, _b, _c, _d;
1024
+ const body = typeof payload === "object" && payload !== null ? payload : {};
1025
+ const details = typeof body.details === "object" && body.details !== null ? body.details : {};
1026
+ const detail = typeof body.error === "string" ? body.error : fallbackDetail;
1027
+ return new BudgetExhaustedError(vendorLabel, (_a = numberOrUndefined(body.budgetUsd)) !== null && _a !== void 0 ? _a : numberOrUndefined(details.budgetUsd), (_b = numberOrUndefined(body.spentUsd)) !== null && _b !== void 0 ? _b : numberOrUndefined(details.spentUsd), detail, {
1028
+ otherVendorAvailable: (_c = booleanOrUndefined(body.otherVendorAvailable)) !== null && _c !== void 0 ? _c : booleanOrUndefined(details.otherVendorAvailable),
1029
+ serverVendor: (_d = stringOrUndefined(body.vendor)) !== null && _d !== void 0 ? _d : stringOrUndefined(details.vendor)
1030
+ });
1031
+ }
1032
+
972
1033
  // ../../foundation-ai/dist/esm/types/config.types.js
973
1034
  var SUPPORTED_GEMINI_MODEL_IDS = [
974
1035
  "gemini-2.5-pro",
@@ -1102,7 +1163,8 @@ function readFramedBody(stream, onChunk, vendorLabel, stallTimeout) {
1102
1163
  var MAX_RETRIES = 5;
1103
1164
  function postWithRetry(options) {
1104
1165
  return __awaiter(this, void 0, void 0, function* () {
1105
- var _a, _b, _c, _d;
1166
+ var _a, _b;
1167
+ var _c, _d;
1106
1168
  const { url, headers, body, credentials, vendorLabel, retryableStatuses, timeout, stallTimeout, backoffBaseMs, signal } = options;
1107
1169
  const backoff = (attempt) => abortableDelay(backoffBaseMs * (1 << attempt), signal);
1108
1170
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
@@ -1123,15 +1185,19 @@ function postWithRetry(options) {
1123
1185
  signal: combinedSignal,
1124
1186
  credentials
1125
1187
  });
1126
- if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
1127
- logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1128
- clearTimeout(timeoutId);
1129
- yield backoff(attempt);
1130
- continue;
1131
- }
1132
1188
  if (!response.ok) {
1133
- const err = yield response.text();
1134
- throw new Error(`${vendorLabel} request error ${response.status}: ${err}`);
1189
+ const errText = yield response.text();
1190
+ const parsed = parseJsonOrUndefined(errText);
1191
+ if (isBudgetRejection(response.status, codeOf(parsed))) {
1192
+ throw budgetExhaustedFrom(vendorLabel, parsed, errText || void 0);
1193
+ }
1194
+ if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
1195
+ logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1196
+ clearTimeout(timeoutId);
1197
+ yield backoff(attempt);
1198
+ continue;
1199
+ }
1200
+ throw new Error(`${vendorLabel} request error ${response.status}: ${errText}`);
1135
1201
  }
1136
1202
  if (((_b = (_a = response.headers) === null || _a === void 0 ? void 0 : _a.get("content-type")) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes("application/x-ndjson")) && response.body) {
1137
1203
  rearmStallTimer();
@@ -1141,6 +1207,9 @@ function postWithRetry(options) {
1141
1207
  if (frame.code === "UPSTREAM_STALLED") {
1142
1208
  throw new DOMException(`${vendorLabel} request stalled: ${(_c = frame.error) !== null && _c !== void 0 ? _c : "no upstream data"}`, "TimeoutError");
1143
1209
  }
1210
+ if (isBudgetRejection(frame.status, frame.code)) {
1211
+ throw budgetExhaustedFrom(vendorLabel, frame);
1212
+ }
1144
1213
  if (retryableStatuses.includes(frame.status) && attempt < MAX_RETRIES) {
1145
1214
  logger.warn(`${vendorLabel}Transport: retryable status ${frame.status} (err frame), retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1146
1215
  clearTimeout(timeoutId);
@@ -1457,7 +1526,8 @@ var AnthropicTransport = class _AnthropicTransport {
1457
1526
  * the payload tidy.
1458
1527
  */
1459
1528
  toAnthropicMessages(history, userMessage, attachments) {
1460
- var _a, _b, _c;
1529
+ var _a, _b;
1530
+ var _c;
1461
1531
  const messages = [];
1462
1532
  const pushBlock = (role, block) => {
1463
1533
  const last = messages[messages.length - 1];
@@ -1487,13 +1557,13 @@ var AnthropicTransport = class _AnthropicTransport {
1487
1557
  type: "tool_use",
1488
1558
  id: tc.id,
1489
1559
  name: tc.name,
1490
- input: (_b = tc.args) !== null && _b !== void 0 ? _b : {}
1560
+ input: (_c = tc.args) !== null && _c !== void 0 ? _c : {}
1491
1561
  });
1492
1562
  }
1493
1563
  continue;
1494
1564
  }
1495
1565
  const role = msg.role === "user" ? "user" : "assistant";
1496
- if (role === "user" && ((_c = msg.attachments) === null || _c === void 0 ? void 0 : _c.length)) {
1566
+ if (role === "user" && ((_b = msg.attachments) === null || _b === void 0 ? void 0 : _b.length)) {
1497
1567
  pushBlock(role, { type: "text", text: msg.content });
1498
1568
  for (const att of msg.attachments) {
1499
1569
  pushBlock(role, { type: "text", text: `[File: ${att.name}]
@@ -1611,7 +1681,7 @@ ${att.content}` });
1611
1681
  headers: requestHeaders,
1612
1682
  body,
1613
1683
  credentials,
1614
- vendorLabel: "Anthropic",
1684
+ vendorLabel: VENDOR_LABELS.anthropic,
1615
1685
  retryableStatuses: _AnthropicTransport.RETRYABLE_STATUSES,
1616
1686
  timeout: this.timeout,
1617
1687
  stallTimeout: this.stallTimeout,
@@ -1969,7 +2039,8 @@ var GeminiTransport = class _GeminiTransport {
1969
2039
  // ── ChatTransport (multi-turn chat) ────────────────────────────────────
1970
2040
  sendChatMessage(history, userMessage, options) {
1971
2041
  return __awaiter(this, void 0, void 0, function* () {
1972
- var _a, _b;
2042
+ var _a;
2043
+ var _b;
1973
2044
  const contents = this.toGeminiContents(history, userMessage, options === null || options === void 0 ? void 0 : options.attachments);
1974
2045
  if (options === null || options === void 0 ? void 0 : options.tailContext) {
1975
2046
  contents.push({ role: "user", parts: [{ text: options.tailContext }] });
@@ -2033,7 +2104,8 @@ var GeminiTransport = class _GeminiTransport {
2033
2104
  return totalCost;
2034
2105
  }
2035
2106
  toGeminiContents(history, userMessage, attachments) {
2036
- var _a, _b, _c;
2107
+ var _a, _b;
2108
+ var _c;
2037
2109
  const contents = [];
2038
2110
  const toolCallNameById = /* @__PURE__ */ new Map();
2039
2111
  for (const msg of history) {
@@ -2041,7 +2113,7 @@ var GeminiTransport = class _GeminiTransport {
2041
2113
  continue;
2042
2114
  const role = msg.role === "user" ? "user" : "model";
2043
2115
  if (msg.toolResult) {
2044
- const fnName = (_a = toolCallNameById.get(msg.toolResult.toolCallId)) !== null && _a !== void 0 ? _a : msg.toolResult.toolCallId;
2116
+ const fnName = (_c = toolCallNameById.get(msg.toolResult.toolCallId)) !== null && _c !== void 0 ? _c : msg.toolResult.toolCallId;
2045
2117
  contents.push({
2046
2118
  role: "user",
2047
2119
  parts: [
@@ -2053,7 +2125,7 @@ var GeminiTransport = class _GeminiTransport {
2053
2125
  }
2054
2126
  ]
2055
2127
  });
2056
- } else if ((_b = msg.toolCalls) === null || _b === void 0 ? void 0 : _b.length) {
2128
+ } else if ((_a = msg.toolCalls) === null || _a === void 0 ? void 0 : _a.length) {
2057
2129
  for (const tc of msg.toolCalls) {
2058
2130
  toolCallNameById.set(tc.id, tc.name);
2059
2131
  }
@@ -2072,7 +2144,7 @@ var GeminiTransport = class _GeminiTransport {
2072
2144
  return part;
2073
2145
  })
2074
2146
  });
2075
- } else if (role === "user" && ((_c = msg.attachments) === null || _c === void 0 ? void 0 : _c.length)) {
2147
+ } else if (role === "user" && ((_b = msg.attachments) === null || _b === void 0 ? void 0 : _b.length)) {
2076
2148
  const msgAttachmentParts = msg.attachments.map((att) => ({
2077
2149
  text: `[File: ${att.name}]
2078
2150
  ${att.content}`
@@ -2125,11 +2197,12 @@ ${att.content}`
2125
2197
  };
2126
2198
  }
2127
2199
  fromGeminiResponse(response, offeredToolNames = /* @__PURE__ */ new Set()) {
2128
- var _a, _b, _c, _d;
2200
+ var _a, _b;
2201
+ var _c, _d;
2129
2202
  const { inputTokens, outputTokens, thoughtsTokens, cacheReadTokens, cost } = this.usageFromGemini(response.usageMetadata);
2130
2203
  const candidates = response === null || response === void 0 ? void 0 : response.candidates;
2131
2204
  const firstCandidate = candidates === null || candidates === void 0 ? void 0 : candidates[0];
2132
- const parts = (_b = (_a = firstCandidate === null || firstCandidate === void 0 ? void 0 : firstCandidate.content) === null || _a === void 0 ? void 0 : _a.parts) !== null && _b !== void 0 ? _b : [];
2205
+ const parts = (_c = (_a = firstCandidate === null || firstCandidate === void 0 ? void 0 : firstCandidate.content) === null || _a === void 0 ? void 0 : _a.parts) !== null && _c !== void 0 ? _c : [];
2133
2206
  const toolCalls = [];
2134
2207
  const thoughtParts = [];
2135
2208
  const textParts = [];
@@ -2142,7 +2215,7 @@ ${att.content}`
2142
2215
  toolCalls.push({
2143
2216
  id: crypto.randomUUID(),
2144
2217
  name: part.functionCall.name,
2145
- args: (_c = part.functionCall.args) !== null && _c !== void 0 ? _c : {},
2218
+ args: (_d = part.functionCall.args) !== null && _d !== void 0 ? _d : {},
2146
2219
  providerMetadata: { [GEMINI_PROVIDER_KEY]: state }
2147
2220
  });
2148
2221
  } else if (part.thought && part.text) {
@@ -2180,7 +2253,7 @@ ${att.content}`
2180
2253
  base.cacheReadTokens = cacheReadTokens;
2181
2254
  if (cost != null)
2182
2255
  base.cost = cost;
2183
- const blockReason = (_d = response.promptFeedback) === null || _d === void 0 ? void 0 : _d.blockReason;
2256
+ const blockReason = (_b = response.promptFeedback) === null || _b === void 0 ? void 0 : _b.blockReason;
2184
2257
  if (finishReason != null || thoughtsTokens != null || blockReason != null) {
2185
2258
  base.responseMeta = {
2186
2259
  finishReason,
@@ -2226,9 +2299,10 @@ ${att.content}`
2226
2299
  * without answering" signature.
2227
2300
  */
2228
2301
  logAbnormalResponse(response, parsed) {
2229
- var _a, _b, _c;
2230
- const usage = (_a = response.usageMetadata) !== null && _a !== void 0 ? _a : {};
2231
- const candidate = (_c = (_b = response.candidates) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : {};
2302
+ var _a;
2303
+ var _b, _c;
2304
+ const usage = (_b = response.usageMetadata) !== null && _b !== void 0 ? _b : {};
2305
+ const candidate = (_c = (_a = response.candidates) === null || _a === void 0 ? void 0 : _a[0]) !== null && _c !== void 0 ? _c : {};
2232
2306
  logger.warn("GeminiTransport: blank/non-STOP response", {
2233
2307
  model: this.model,
2234
2308
  finishReason: parsed.finishReason,
@@ -2279,7 +2353,7 @@ ${att.content}`
2279
2353
  headers,
2280
2354
  body: payload,
2281
2355
  credentials,
2282
- vendorLabel: "Gemini",
2356
+ vendorLabel: VENDOR_LABELS.gemini,
2283
2357
  retryableStatuses: _GeminiTransport.RETRYABLE_STATUSES,
2284
2358
  timeout: this.timeout,
2285
2359
  stallTimeout: this.stallTimeout,
@@ -2332,7 +2406,8 @@ var EmptyAIProviderRegistry = class _EmptyAIProviderRegistry {
2332
2406
  }
2333
2407
  getStatus(name) {
2334
2408
  return __awaiter(this, void 0, void 0, function* () {
2335
- var _a, _b, _c;
2409
+ var _a, _b;
2410
+ var _c;
2336
2411
  if (name !== void 0 && name !== _EmptyAIProviderRegistry.DEFAULT_NAME)
2337
2412
  return null;
2338
2413
  return (_c = yield (_b = (_a = this.noopProvider).getStatus) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : null;
@@ -2340,7 +2415,8 @@ var EmptyAIProviderRegistry = class _EmptyAIProviderRegistry {
2340
2415
  }
2341
2416
  listStatuses() {
2342
2417
  return __awaiter(this, void 0, void 0, function* () {
2343
- var _a, _b, _c;
2418
+ var _a, _b;
2419
+ var _c;
2344
2420
  const status = (_c = yield (_b = (_a = this.noopProvider).getStatus) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : null;
2345
2421
  return [{ name: _EmptyAIProviderRegistry.DEFAULT_NAME, isDefault: true, status }];
2346
2422
  });
@@ -2384,7 +2460,8 @@ var MutableAIProviderRegistry = class _MutableAIProviderRegistry {
2384
2460
  }
2385
2461
  getStatus(name) {
2386
2462
  return __awaiter(this, void 0, void 0, function* () {
2387
- var _a, _b;
2463
+ var _a;
2464
+ var _b;
2388
2465
  const provider = name === void 0 ? this.default() : this.providers.get(name);
2389
2466
  if (!provider)
2390
2467
  return null;
@@ -2395,7 +2472,8 @@ var MutableAIProviderRegistry = class _MutableAIProviderRegistry {
2395
2472
  return __awaiter(this, void 0, void 0, function* () {
2396
2473
  const entries = [...this.providers.entries()];
2397
2474
  return Promise.all(entries.map((_a) => __awaiter(this, [_a], void 0, function* ([name, provider]) {
2398
- var _b, _c;
2475
+ var _b;
2476
+ var _c;
2399
2477
  return {
2400
2478
  name,
2401
2479
  isDefault: name === this._defaultName,
@@ -3029,6 +3107,19 @@ function accumulate(messages, into) {
3029
3107
  var TOOL_FOLD_SYMBOL = Symbol("toolFold");
3030
3108
 
3031
3109
  // src/components/chat-driver/chat-driver.ts
3110
+ var budgetDetailOf = (e) => {
3111
+ const vendor = vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor);
3112
+ if (e.budgetUsd == null && e.spentUsd == null && !vendor) return void 0;
3113
+ return {
3114
+ budgetUsd: e.budgetUsd,
3115
+ spentUsd: e.spentUsd,
3116
+ vendorLabel: e.vendorLabel,
3117
+ ...vendor ? { vendor } : {},
3118
+ // Server-known truth the client cannot re-derive, so it rides all the way to
3119
+ // the banner rather than being re-guessed there from registry membership.
3120
+ ...e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}
3121
+ };
3122
+ };
3032
3123
  var DEFAULT_MAX_TOOL_ITERATIONS = 50;
3033
3124
  var DEFAULT_MAX_FOLD_OPERATIONS = 5;
3034
3125
  var DEFAULT_MAX_TURN_SNAPSHOTS = 400;
@@ -3211,6 +3302,22 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3211
3302
  * picked up on the next turn.
3212
3303
  */
3213
3304
  this.resolvedStatusCache = /* @__PURE__ */ new Map();
3305
+ /**
3306
+ * Set the moment a budget wall is observed anywhere in this turn — this
3307
+ * driver's own 402, or a sub-agent's (which surfaces here only as a
3308
+ * `'budget_exhausted'` tool result). Read at the top of the tool loop to end
3309
+ * the turn before issuing another model call that would hit the same wall.
3310
+ * Reset per turn alongside the other per-turn counters.
3311
+ */
3312
+ this.budgetExhaustedThisTurn = false;
3313
+ /**
3314
+ * Whether this turn's budget wall came from a SUB-AGENT rather than this
3315
+ * driver's own request. Decides whether `lastResolvedProvider` is a valid
3316
+ * attribution fallback: for an own wall it is the refusing vendor, for a
3317
+ * child's wall it is the parent's vendor — the one known NOT to have refused.
3318
+ * Reset per turn alongside `budgetWallDetail`.
3319
+ */
3320
+ this.budgetWallViaSubAgent = false;
3214
3321
  const {
3215
3322
  toolHandlers = {},
3216
3323
  toolDefinitions = [],
@@ -3221,12 +3328,14 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3221
3328
  condenseBatchCalls = 1,
3222
3329
  maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
3223
3330
  sessionKey = "",
3224
- activityBus = NOOP_ACTIVITY_BUS
3331
+ activityBus = NOOP_ACTIVITY_BUS,
3332
+ budgetExhaustedMessage = DEFAULT_BUDGET_EXHAUSTED_MESSAGE
3225
3333
  } = config;
3226
3334
  this.maxToolIterations = maxToolIterations;
3227
3335
  this.condenseBatchCalls = condenseBatchCalls;
3228
3336
  this.sessionKey = sessionKey;
3229
3337
  this.activityBus = activityBus;
3338
+ this.budgetExhaustedMessage = budgetExhaustedMessage;
3230
3339
  if (typeof toolHandlers === "function") {
3231
3340
  this.toolHandlersFactory = toolHandlers;
3232
3341
  this.toolHandlers = {};
@@ -3333,8 +3442,56 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3333
3442
  * (rather than set to `undefined`) so a happy-path result stays byte-identical to
3334
3443
  * the historical `{ reason: 'done' }`.
3335
3444
  */
3336
- turnDone(failureReason) {
3337
- return failureReason ? { reason: "done", failureReason } : { reason: "done" };
3445
+ turnDone(failureReason, budget) {
3446
+ if (!failureReason) return { reason: "done" };
3447
+ return budget ? { reason: "done", failureReason, budget } : { reason: "done", failureReason };
3448
+ }
3449
+ /**
3450
+ * Terminal budget outcome for a wall hit **outside** the tool loop — today,
3451
+ * `OrchestratingDriver`'s classification phase, which calls the provider
3452
+ * directly and so never enters `runToolLoop`.
3453
+ *
3454
+ * Does **not** publish `tool-loop-end`: no `tool-loop-start` was published for
3455
+ * the classify phase, and an unbalanced end would break start/end pairing for
3456
+ * subscribers that rely on it. The driver **return value** is what reports this
3457
+ * case — see `FoundationAiAssistant`'s latch, which reads both seams for
3458
+ * exactly this reason.
3459
+ *
3460
+ * The non-sub-agent tail of the in-loop `BudgetExhaustedError` branch lives
3461
+ * here so there is one copy of the log line, the debug-log entry, the
3462
+ * transcript bubble and the result shape rather than two that can drift.
3463
+ *
3464
+ * @param pendingUserMessage - a user message that has NOT yet been appended,
3465
+ * appended first so the answer does not end up replying to nothing. Only the
3466
+ * classification seam passes it: `OrchestratingDriver` dispatches the user's
3467
+ * text as an optimistic `history-updated` detail and leaves the real append
3468
+ * to `chatDriver.sendMessage`, which never runs when `classify()` throws — so
3469
+ * the bubble below would re-dispatch a history the user's own message was
3470
+ * never in, and it would vanish from the transcript on the next render. The
3471
+ * in-loop caller has already appended it and passes nothing.
3472
+ *
3473
+ * @internal
3474
+ */
3475
+ reportBudgetExhausted(e, pendingUserMessage) {
3476
+ if (pendingUserMessage) this.appendToHistory(pendingUserMessage);
3477
+ this.budgetExhaustedThisTurn = true;
3478
+ logger2.error("ChatDriver: AI budget exhausted", e);
3479
+ recordTurnError(this.sessionKey, "budget-exhausted", {
3480
+ agent: this.activeAgentName,
3481
+ provider: this.lastResolvedProviderName,
3482
+ // The registry ALIAS (e.g. 'high') is what `provider` records; the vendor
3483
+ // is the thing a per-vendor budget is actually scoped to, and it was known
3484
+ // at both ends and discarded in the middle until now. Taken from the
3485
+ // refusing transport's label first — this method also serves the
3486
+ // classification seam, where `lastResolvedProvider` is the PREVIOUS turn's
3487
+ // vendor (or nothing), because classify runs against the registry default.
3488
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
3489
+ budgetUsd: e.budgetUsd,
3490
+ spentUsd: e.spentUsd,
3491
+ isSubAgent: this.isSubAgent
3492
+ });
3493
+ this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
3494
+ return this.turnDone("budget-exhausted", budgetDetailOf(e));
3338
3495
  }
3339
3496
  /** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
3340
3497
  static failureReasonOf(result) {
@@ -3344,10 +3501,28 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3344
3501
  * Build the `tool-loop-end` event detail for a turn's result. A failure carries a
3345
3502
  * `{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,
3346
3503
  * kept byte-identical so subscribers see exactly what they always have.
3504
+ *
3505
+ * A budget failure additionally carries `vendor` — the concrete vendor
3506
+ * (`'anthropic'`/`'gemini'`) the walled turn resolved to, which the driver knows
3507
+ * and used to discard. Optional and additive: a subscriber reading only
3508
+ * `failureReason` is unaffected, a non-budget failure still emits the historical
3509
+ * `{ failureReason }` with no `vendor` key, and the value is a plain string so
3510
+ * the detail stays structured-cloneable for the cross-tab hop. It is the field a
3511
+ * per-vendor budget model needs and the one that would be awkward to retrofit.
3347
3512
  */
3348
- static loopEndDetail(result) {
3513
+ loopEndDetail(result) {
3349
3514
  const failureReason = _ChatDriver.failureReasonOf(result);
3350
- return failureReason ? { failureReason } : void 0;
3515
+ if (!failureReason) return void 0;
3516
+ if (failureReason !== "budget-exhausted") {
3517
+ return { failureReason };
3518
+ }
3519
+ const budget = result.reason === "done" ? result.budget : void 0;
3520
+ const vendor = budget?.vendor ?? (this.budgetWallViaSubAgent ? void 0 : this.lastResolvedProvider);
3521
+ return {
3522
+ failureReason,
3523
+ ...vendor ? { vendor } : {},
3524
+ ...budget ? { budget } : {}
3525
+ };
3351
3526
  }
3352
3527
  /**
3353
3528
  * Swap in a new agent's configuration. Called by OrchestratingDriver before
@@ -3533,9 +3708,9 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3533
3708
  * under a separate session key, so recording here would orphan the event off
3534
3709
  * the user-visible debug-log timeline.)
3535
3710
  */
3536
- failSubAgent(reason) {
3711
+ failSubAgent(reason, budget) {
3537
3712
  if (!this.isSubAgent || this.subAgentFailure) return;
3538
- this.subAgentFailure = { reason };
3713
+ this.subAgentFailure = budget ? { reason, budget } : { reason };
3539
3714
  }
3540
3715
  /**
3541
3716
  * Returns true if `releaseAgent` was called during the most recent turn.
@@ -3985,6 +4160,9 @@ Output format (strict):
3985
4160
  this.subAgentCompletion = void 0;
3986
4161
  this.subAgentFailure = void 0;
3987
4162
  this.agentReleaseRequested = false;
4163
+ this.budgetExhaustedThisTurn = false;
4164
+ this.budgetWallDetail = void 0;
4165
+ this.budgetWallViaSubAgent = false;
3988
4166
  this.appendToHistory({ role: "user", content: userInput, attachments });
3989
4167
  this.turnStartedAt = Date.now();
3990
4168
  recordMetaEvent(this.sessionKey, "turn.start", {
@@ -4019,7 +4197,7 @@ Output format (strict):
4019
4197
  });
4020
4198
  this.busy = false;
4021
4199
  this.endTurn();
4022
- this.activityBus.publish("tool-loop-end", _ChatDriver.loopEndDetail(result));
4200
+ this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
4023
4201
  }
4024
4202
  }
4025
4203
  /**
@@ -4210,8 +4388,14 @@ Output format (strict):
4210
4388
  recordMetaEvent(this.sessionKey, "subagent.completed", { agent: name });
4211
4389
  return { outcome: { ok: true, result: completion.result }, trace };
4212
4390
  }
4213
- const reason = child.getSubAgentFailure()?.reason ?? "max_iterations";
4391
+ const failure = child.getSubAgentFailure();
4392
+ const reason = failure?.reason ?? "max_iterations";
4214
4393
  recordMetaEvent(this.sessionKey, "subagent.failed", { agent: name, reason });
4394
+ if (reason === "budget_exhausted") {
4395
+ this.budgetExhaustedThisTurn = true;
4396
+ this.budgetWallViaSubAgent = true;
4397
+ this.budgetWallDetail ??= failure?.budget;
4398
+ }
4215
4399
  return { outcome: { ok: false, reason }, trace };
4216
4400
  }
4217
4401
  /**
@@ -4224,6 +4408,9 @@ Output format (strict):
4224
4408
  this.beginTurn();
4225
4409
  this.subAgentCompletion = void 0;
4226
4410
  this.subAgentFailure = void 0;
4411
+ this.budgetExhaustedThisTurn = false;
4412
+ this.budgetWallDetail = void 0;
4413
+ this.budgetWallViaSubAgent = false;
4227
4414
  this.turnStartedAt = Date.now();
4228
4415
  recordMetaEvent(this.sessionKey, "turn.start", {
4229
4416
  phase: "continueFromHistory",
@@ -4257,7 +4444,7 @@ Output format (strict):
4257
4444
  });
4258
4445
  this.busy = false;
4259
4446
  this.endTurn();
4260
- this.activityBus.publish("tool-loop-end", _ChatDriver.loopEndDetail(result));
4447
+ this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
4261
4448
  }
4262
4449
  }
4263
4450
  // ---------------------------------------------------------------------------
@@ -4411,6 +4598,31 @@ Output format (strict):
4411
4598
  if (this.turnController.signal.aborted) {
4412
4599
  return this.completeAbortedTurn();
4413
4600
  }
4601
+ if (this.budgetExhaustedThisTurn) {
4602
+ logger2.error("ChatDriver: ending the turn \u2014 a sub-agent hit the AI budget wall");
4603
+ recordTurnError(this.sessionKey, "budget-exhausted", {
4604
+ agent: this.activeAgentName,
4605
+ provider: this.lastResolvedProviderName,
4606
+ // The CHILD's vendor when it knew one, and NOTHING otherwise. On this
4607
+ // path the wall is definitionally the child's, and on a mixed registry
4608
+ // `lastResolvedProvider` is this driver's own vendor — the one known
4609
+ // NOT to have refused. An unattributable child wall must degrade to
4610
+ // the vendor-agnostic block (which `latchBlockedFrom` handles
4611
+ // fail-safe), never to a vendor that is known to be wrong: naming the
4612
+ // parent's vendor here walled BOTH — the child's via its own
4613
+ // tool-loop-end, the parent's via this event — and derived `blocked`
4614
+ // over headroom that still existed.
4615
+ vendor: this.budgetWallDetail?.vendor,
4616
+ via: "sub-agent",
4617
+ isSubAgent: this.isSubAgent
4618
+ });
4619
+ if (this.isSubAgent) {
4620
+ this.failSubAgent("budget_exhausted", this.budgetWallDetail);
4621
+ } else {
4622
+ this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
4623
+ }
4624
+ return this.turnDone("budget-exhausted", this.budgetWallDetail);
4625
+ }
4414
4626
  const promptCtx = {
4415
4627
  agentName: this.activeAgentName ?? "",
4416
4628
  history: this.history,
@@ -4582,6 +4794,25 @@ ${tailBody}
4582
4794
  }
4583
4795
  return this.turnDone("response-truncated");
4584
4796
  }
4797
+ if (e instanceof BudgetExhaustedError) {
4798
+ this.budgetExhaustedThisTurn = true;
4799
+ this.budgetWallDetail = budgetDetailOf(e);
4800
+ this.budgetWallViaSubAgent = false;
4801
+ if (this.isSubAgent) {
4802
+ logger2.error("ChatDriver: AI budget exhausted", e);
4803
+ recordTurnError(this.sessionKey, "budget-exhausted", {
4804
+ agent: this.activeAgentName,
4805
+ provider: this.lastResolvedProviderName,
4806
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
4807
+ budgetUsd: e.budgetUsd,
4808
+ spentUsd: e.spentUsd,
4809
+ isSubAgent: true
4810
+ });
4811
+ this.failSubAgent("budget_exhausted", this.budgetWallDetail);
4812
+ return this.turnDone("budget-exhausted", this.budgetWallDetail);
4813
+ }
4814
+ return this.reportBudgetExhausted(e);
4815
+ }
4585
4816
  if (e instanceof DOMException && e.name === "TimeoutError") {
4586
4817
  logger2.error("ChatDriver: request timed out", e);
4587
4818
  recordTurnError(this.sessionKey, "exception", {
@@ -5057,6 +5288,17 @@ var OrchestratingDriver = class extends EventTarget {
5057
5288
  * user stops. Reset at the start of each `sendMessage`.
5058
5289
  */
5059
5290
  this.cancelled = false;
5291
+ /**
5292
+ * Whether the CURRENT turn's user message has reached the inner driver's
5293
+ * history. False through the pre-first-turn classify (where the only echo of
5294
+ * the message is an optimistic `history-updated` dispatch), true from the
5295
+ * moment `chatDriver.sendMessage` is entered — including through every later
5296
+ * handoff classify. Read by the budget-wall catch in `sendMessage` to decide
5297
+ * whether `reportBudgetExhausted` must append the message itself: appending
5298
+ * it when already appended duplicated it; not appending it when unappended
5299
+ * made it vanish. Reset at the top of each `runOrchestratedTurn`.
5300
+ */
5301
+ this.userMessageAppended = false;
5060
5302
  /**
5061
5303
  * Sticky user pick from the picker (or the host's `setAgent` API). Only
5062
5304
  * changes on explicit user action. Survives flow completion: when a stateful
@@ -5098,7 +5340,8 @@ var OrchestratingDriver = class extends EventTarget {
5098
5340
  maxFoldOperations: options.maxFoldOperations,
5099
5341
  maxTurnSnapshots: options.maxTurnSnapshots,
5100
5342
  sessionKey: this.sessionKey,
5101
- activityBus: options.activityBus
5343
+ activityBus: options.activityBus,
5344
+ budgetExhaustedMessage: options.budgetExhaustedMessage
5102
5345
  });
5103
5346
  this.chatDriver.addEventListener("history-updated", (e) => {
5104
5347
  this.dispatchEvent(new CustomEvent("history-updated", { detail: e.detail }));
@@ -5197,7 +5440,25 @@ var OrchestratingDriver = class extends EventTarget {
5197
5440
  return this.chatDriver.getSuggestions(history, prompt2, count, agentInfo);
5198
5441
  }
5199
5442
  async sendMessage(input, attachments) {
5443
+ try {
5444
+ return await this.runOrchestratedTurn(input, attachments);
5445
+ } catch (e) {
5446
+ if (e instanceof BudgetExhaustedError) {
5447
+ return this.chatDriver.reportBudgetExhausted(
5448
+ e,
5449
+ this.userMessageAppended ? void 0 : {
5450
+ role: "user",
5451
+ content: input,
5452
+ ...attachments ? { attachments } : {}
5453
+ }
5454
+ );
5455
+ }
5456
+ throw e;
5457
+ }
5458
+ }
5459
+ async runOrchestratedTurn(input, attachments) {
5200
5460
  this.cancelled = false;
5461
+ this.userMessageAppended = false;
5201
5462
  const history = this.chatDriver.getHistory();
5202
5463
  this.dispatchEvent(
5203
5464
  new CustomEvent("history-updated", {
@@ -5211,6 +5472,7 @@ var OrchestratingDriver = class extends EventTarget {
5211
5472
  let handoffs = 0;
5212
5473
  let handoffSummary = "";
5213
5474
  let remainingTask = "";
5475
+ let lastResult;
5214
5476
  while (true) {
5215
5477
  if (this.cancelled) break;
5216
5478
  await this.applyAgent(currentAgent);
@@ -5219,8 +5481,10 @@ var OrchestratingDriver = class extends EventTarget {
5219
5481
  const contextPrimer = handoffSummary ? [{ role: "user", content: `[Context from previous agent]: ${handoffSummary}` }] : [];
5220
5482
  result = await this.chatDriver.continueFromHistory(contextPrimer);
5221
5483
  } else {
5484
+ this.userMessageAppended = true;
5222
5485
  result = await this.chatDriver.sendMessage(input, attachments);
5223
5486
  }
5487
+ lastResult = result;
5224
5488
  if (this.chatDriver.getAgentReleaseRequested()) {
5225
5489
  await this.releaseActiveAgent();
5226
5490
  break;
@@ -5242,7 +5506,7 @@ var OrchestratingDriver = class extends EventTarget {
5242
5506
  this.dispatchEvent(new CustomEvent("orchestrating-start"));
5243
5507
  currentAgent = await this.classify(remainingTask, updatedHistory);
5244
5508
  }
5245
- return { reason: "done" };
5509
+ return lastResult?.reason === "done" ? lastResult : { reason: "done" };
5246
5510
  }
5247
5511
  async continueFromHistory(transientPrimer) {
5248
5512
  return this.chatDriver.continueFromHistory(transientPrimer);
@@ -5425,6 +5689,7 @@ ${recentMessages}` : ""}`;
5425
5689
  }
5426
5690
  }
5427
5691
  };
5692
+ let routedNoMatch = false;
5428
5693
  for (let attempt = 0; attempt <= this.classifierRetries; attempt += 1) {
5429
5694
  try {
5430
5695
  const options = {
@@ -5444,8 +5709,10 @@ ${recentMessages}` : ""}`;
5444
5709
  if (index >= 0 && index < this.specialists.length) {
5445
5710
  return this.specialists[index];
5446
5711
  }
5712
+ routedNoMatch = true;
5447
5713
  break;
5448
5714
  } catch (e) {
5715
+ if (e instanceof BudgetExhaustedError) throw e;
5449
5716
  logger2.warn(`OrchestratingDriver: classifier attempt ${attempt + 1} failed:`, e);
5450
5717
  if (attempt === this.classifierRetries) {
5451
5718
  logger2.error("OrchestratingDriver: classifier failed after all retries, using fallback");
@@ -5453,10 +5720,12 @@ ${recentMessages}` : ""}`;
5453
5720
  }
5454
5721
  }
5455
5722
  if (this.fallback) return this.fallback;
5456
- const specialistNames = this.specialists.map((s) => s.name).join(", ");
5457
- this.appendInlineMessage(
5458
- `I'm not sure how to help with that. I can assist with: ${specialistNames}.`
5459
- );
5723
+ if (routedNoMatch) {
5724
+ const specialistNames = this.specialists.map((s) => s.name).join(", ");
5725
+ this.appendInlineMessage(
5726
+ `I'm not sure how to help with that. I can assist with: ${specialistNames}.`
5727
+ );
5728
+ }
5460
5729
  return this.specialists[0];
5461
5730
  }
5462
5731
  /**