@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
@@ -1016,6 +1016,67 @@ var GeminiProvider = class {
1016
1016
  }
1017
1017
  };
1018
1018
 
1019
+ // ../../foundation-ai/dist/esm/transports/budget-exhausted-error.js
1020
+ var BUDGET_EXCEEDED_CODE = "BUDGET_EXCEEDED";
1021
+ var VENDOR_LABELS = Object.freeze({
1022
+ anthropic: "Anthropic",
1023
+ gemini: "Gemini",
1024
+ openai: "OpenAI",
1025
+ chrome: "Chrome"
1026
+ });
1027
+ var BUDGETED_VENDORS = Object.freeze([
1028
+ "anthropic",
1029
+ "gemini"
1030
+ ]);
1031
+ var VENDOR_TYPE_BY_LABEL = new Map(Object.entries(VENDOR_LABELS).map(([type, label]) => [
1032
+ label.toLowerCase(),
1033
+ type
1034
+ ]));
1035
+ function vendorTypeOfLabel(label) {
1036
+ if (typeof label !== "string")
1037
+ return void 0;
1038
+ return VENDOR_TYPE_BY_LABEL.get(label.trim().toLowerCase());
1039
+ }
1040
+ var BUDGET_EXCEEDED_STATUS = 402;
1041
+ var DEFAULT_BUDGET_EXHAUSTED_MESSAGE = "You've reached your AI usage limit. Contact your administrator to raise it.";
1042
+ var BudgetExhaustedError = class extends Error {
1043
+ constructor(vendorLabel, budgetUsd, spentUsd, detail, extra) {
1044
+ 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.");
1045
+ this.vendorLabel = vendorLabel;
1046
+ this.budgetUsd = budgetUsd;
1047
+ this.spentUsd = spentUsd;
1048
+ this.detail = detail;
1049
+ this.name = "BudgetExhaustedError";
1050
+ this.otherVendorAvailable = extra === null || extra === void 0 ? void 0 : extra.otherVendorAvailable;
1051
+ this.serverVendor = extra === null || extra === void 0 ? void 0 : extra.serverVendor;
1052
+ }
1053
+ };
1054
+ var isBudgetRejection = (status, code) => status === BUDGET_EXCEEDED_STATUS || code === BUDGET_EXCEEDED_CODE;
1055
+ var parseJsonOrUndefined = (text) => {
1056
+ try {
1057
+ return JSON.parse(text);
1058
+ } catch (_a) {
1059
+ return void 0;
1060
+ }
1061
+ };
1062
+ var codeOf = (payload) => {
1063
+ const code = payload === null || payload === void 0 ? void 0 : payload.code;
1064
+ return typeof code === "string" ? code : void 0;
1065
+ };
1066
+ var numberOrUndefined = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
1067
+ var booleanOrUndefined = (value) => typeof value === "boolean" ? value : void 0;
1068
+ var stringOrUndefined = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
1069
+ function budgetExhaustedFrom(vendorLabel, payload, fallbackDetail) {
1070
+ var _a, _b, _c, _d;
1071
+ const body = typeof payload === "object" && payload !== null ? payload : {};
1072
+ const details = typeof body.details === "object" && body.details !== null ? body.details : {};
1073
+ const detail = typeof body.error === "string" ? body.error : fallbackDetail;
1074
+ 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, {
1075
+ otherVendorAvailable: (_c = booleanOrUndefined(body.otherVendorAvailable)) !== null && _c !== void 0 ? _c : booleanOrUndefined(details.otherVendorAvailable),
1076
+ serverVendor: (_d = stringOrUndefined(body.vendor)) !== null && _d !== void 0 ? _d : stringOrUndefined(details.vendor)
1077
+ });
1078
+ }
1079
+
1019
1080
  // ../../foundation-ai/dist/esm/types/config.types.js
1020
1081
  var SUPPORTED_GEMINI_MODEL_IDS = [
1021
1082
  "gemini-2.5-pro",
@@ -1149,7 +1210,8 @@ function readFramedBody(stream, onChunk, vendorLabel, stallTimeout) {
1149
1210
  var MAX_RETRIES = 5;
1150
1211
  function postWithRetry(options) {
1151
1212
  return __awaiter(this, void 0, void 0, function* () {
1152
- var _a, _b, _c, _d;
1213
+ var _a, _b;
1214
+ var _c, _d;
1153
1215
  const { url, headers, body, credentials, vendorLabel, retryableStatuses, timeout, stallTimeout, backoffBaseMs, signal } = options;
1154
1216
  const backoff = (attempt) => abortableDelay(backoffBaseMs * (1 << attempt), signal);
1155
1217
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
@@ -1170,15 +1232,19 @@ function postWithRetry(options) {
1170
1232
  signal: combinedSignal,
1171
1233
  credentials
1172
1234
  });
1173
- if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
1174
- logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1175
- clearTimeout(timeoutId);
1176
- yield backoff(attempt);
1177
- continue;
1178
- }
1179
1235
  if (!response.ok) {
1180
- const err = yield response.text();
1181
- throw new Error(`${vendorLabel} request error ${response.status}: ${err}`);
1236
+ const errText = yield response.text();
1237
+ const parsed = parseJsonOrUndefined(errText);
1238
+ if (isBudgetRejection(response.status, codeOf(parsed))) {
1239
+ throw budgetExhaustedFrom(vendorLabel, parsed, errText || void 0);
1240
+ }
1241
+ if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
1242
+ logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1243
+ clearTimeout(timeoutId);
1244
+ yield backoff(attempt);
1245
+ continue;
1246
+ }
1247
+ throw new Error(`${vendorLabel} request error ${response.status}: ${errText}`);
1182
1248
  }
1183
1249
  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) {
1184
1250
  rearmStallTimer();
@@ -1188,6 +1254,9 @@ function postWithRetry(options) {
1188
1254
  if (frame.code === "UPSTREAM_STALLED") {
1189
1255
  throw new DOMException(`${vendorLabel} request stalled: ${(_c = frame.error) !== null && _c !== void 0 ? _c : "no upstream data"}`, "TimeoutError");
1190
1256
  }
1257
+ if (isBudgetRejection(frame.status, frame.code)) {
1258
+ throw budgetExhaustedFrom(vendorLabel, frame);
1259
+ }
1191
1260
  if (retryableStatuses.includes(frame.status) && attempt < MAX_RETRIES) {
1192
1261
  logger.warn(`${vendorLabel}Transport: retryable status ${frame.status} (err frame), retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1193
1262
  clearTimeout(timeoutId);
@@ -1504,7 +1573,8 @@ var AnthropicTransport = class _AnthropicTransport {
1504
1573
  * the payload tidy.
1505
1574
  */
1506
1575
  toAnthropicMessages(history, userMessage, attachments) {
1507
- var _a, _b, _c;
1576
+ var _a, _b;
1577
+ var _c;
1508
1578
  const messages = [];
1509
1579
  const pushBlock = (role, block) => {
1510
1580
  const last = messages[messages.length - 1];
@@ -1534,13 +1604,13 @@ var AnthropicTransport = class _AnthropicTransport {
1534
1604
  type: "tool_use",
1535
1605
  id: tc.id,
1536
1606
  name: tc.name,
1537
- input: (_b = tc.args) !== null && _b !== void 0 ? _b : {}
1607
+ input: (_c = tc.args) !== null && _c !== void 0 ? _c : {}
1538
1608
  });
1539
1609
  }
1540
1610
  continue;
1541
1611
  }
1542
1612
  const role = msg.role === "user" ? "user" : "assistant";
1543
- if (role === "user" && ((_c = msg.attachments) === null || _c === void 0 ? void 0 : _c.length)) {
1613
+ if (role === "user" && ((_b = msg.attachments) === null || _b === void 0 ? void 0 : _b.length)) {
1544
1614
  pushBlock(role, { type: "text", text: msg.content });
1545
1615
  for (const att of msg.attachments) {
1546
1616
  pushBlock(role, { type: "text", text: `[File: ${att.name}]
@@ -1658,7 +1728,7 @@ ${att.content}` });
1658
1728
  headers: requestHeaders,
1659
1729
  body,
1660
1730
  credentials,
1661
- vendorLabel: "Anthropic",
1731
+ vendorLabel: VENDOR_LABELS.anthropic,
1662
1732
  retryableStatuses: _AnthropicTransport.RETRYABLE_STATUSES,
1663
1733
  timeout: this.timeout,
1664
1734
  stallTimeout: this.stallTimeout,
@@ -2016,7 +2086,8 @@ var GeminiTransport = class _GeminiTransport {
2016
2086
  // ── ChatTransport (multi-turn chat) ────────────────────────────────────
2017
2087
  sendChatMessage(history, userMessage, options) {
2018
2088
  return __awaiter(this, void 0, void 0, function* () {
2019
- var _a, _b;
2089
+ var _a;
2090
+ var _b;
2020
2091
  const contents = this.toGeminiContents(history, userMessage, options === null || options === void 0 ? void 0 : options.attachments);
2021
2092
  if (options === null || options === void 0 ? void 0 : options.tailContext) {
2022
2093
  contents.push({ role: "user", parts: [{ text: options.tailContext }] });
@@ -2080,7 +2151,8 @@ var GeminiTransport = class _GeminiTransport {
2080
2151
  return totalCost;
2081
2152
  }
2082
2153
  toGeminiContents(history, userMessage, attachments) {
2083
- var _a, _b, _c;
2154
+ var _a, _b;
2155
+ var _c;
2084
2156
  const contents = [];
2085
2157
  const toolCallNameById = /* @__PURE__ */ new Map();
2086
2158
  for (const msg of history) {
@@ -2088,7 +2160,7 @@ var GeminiTransport = class _GeminiTransport {
2088
2160
  continue;
2089
2161
  const role = msg.role === "user" ? "user" : "model";
2090
2162
  if (msg.toolResult) {
2091
- const fnName = (_a = toolCallNameById.get(msg.toolResult.toolCallId)) !== null && _a !== void 0 ? _a : msg.toolResult.toolCallId;
2163
+ const fnName = (_c = toolCallNameById.get(msg.toolResult.toolCallId)) !== null && _c !== void 0 ? _c : msg.toolResult.toolCallId;
2092
2164
  contents.push({
2093
2165
  role: "user",
2094
2166
  parts: [
@@ -2100,7 +2172,7 @@ var GeminiTransport = class _GeminiTransport {
2100
2172
  }
2101
2173
  ]
2102
2174
  });
2103
- } else if ((_b = msg.toolCalls) === null || _b === void 0 ? void 0 : _b.length) {
2175
+ } else if ((_a = msg.toolCalls) === null || _a === void 0 ? void 0 : _a.length) {
2104
2176
  for (const tc of msg.toolCalls) {
2105
2177
  toolCallNameById.set(tc.id, tc.name);
2106
2178
  }
@@ -2119,7 +2191,7 @@ var GeminiTransport = class _GeminiTransport {
2119
2191
  return part;
2120
2192
  })
2121
2193
  });
2122
- } else if (role === "user" && ((_c = msg.attachments) === null || _c === void 0 ? void 0 : _c.length)) {
2194
+ } else if (role === "user" && ((_b = msg.attachments) === null || _b === void 0 ? void 0 : _b.length)) {
2123
2195
  const msgAttachmentParts = msg.attachments.map((att) => ({
2124
2196
  text: `[File: ${att.name}]
2125
2197
  ${att.content}`
@@ -2172,11 +2244,12 @@ ${att.content}`
2172
2244
  };
2173
2245
  }
2174
2246
  fromGeminiResponse(response, offeredToolNames = /* @__PURE__ */ new Set()) {
2175
- var _a, _b, _c, _d;
2247
+ var _a, _b;
2248
+ var _c, _d;
2176
2249
  const { inputTokens, outputTokens, thoughtsTokens, cacheReadTokens, cost } = this.usageFromGemini(response.usageMetadata);
2177
2250
  const candidates = response === null || response === void 0 ? void 0 : response.candidates;
2178
2251
  const firstCandidate = candidates === null || candidates === void 0 ? void 0 : candidates[0];
2179
- 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 : [];
2252
+ 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 : [];
2180
2253
  const toolCalls = [];
2181
2254
  const thoughtParts = [];
2182
2255
  const textParts = [];
@@ -2189,7 +2262,7 @@ ${att.content}`
2189
2262
  toolCalls.push({
2190
2263
  id: crypto.randomUUID(),
2191
2264
  name: part.functionCall.name,
2192
- args: (_c = part.functionCall.args) !== null && _c !== void 0 ? _c : {},
2265
+ args: (_d = part.functionCall.args) !== null && _d !== void 0 ? _d : {},
2193
2266
  providerMetadata: { [GEMINI_PROVIDER_KEY]: state }
2194
2267
  });
2195
2268
  } else if (part.thought && part.text) {
@@ -2227,7 +2300,7 @@ ${att.content}`
2227
2300
  base.cacheReadTokens = cacheReadTokens;
2228
2301
  if (cost != null)
2229
2302
  base.cost = cost;
2230
- const blockReason = (_d = response.promptFeedback) === null || _d === void 0 ? void 0 : _d.blockReason;
2303
+ const blockReason = (_b = response.promptFeedback) === null || _b === void 0 ? void 0 : _b.blockReason;
2231
2304
  if (finishReason != null || thoughtsTokens != null || blockReason != null) {
2232
2305
  base.responseMeta = {
2233
2306
  finishReason,
@@ -2273,9 +2346,10 @@ ${att.content}`
2273
2346
  * without answering" signature.
2274
2347
  */
2275
2348
  logAbnormalResponse(response, parsed) {
2276
- var _a, _b, _c;
2277
- const usage = (_a = response.usageMetadata) !== null && _a !== void 0 ? _a : {};
2278
- const candidate = (_c = (_b = response.candidates) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : {};
2349
+ var _a;
2350
+ var _b, _c;
2351
+ const usage = (_b = response.usageMetadata) !== null && _b !== void 0 ? _b : {};
2352
+ const candidate = (_c = (_a = response.candidates) === null || _a === void 0 ? void 0 : _a[0]) !== null && _c !== void 0 ? _c : {};
2279
2353
  logger.warn("GeminiTransport: blank/non-STOP response", {
2280
2354
  model: this.model,
2281
2355
  finishReason: parsed.finishReason,
@@ -2326,7 +2400,7 @@ ${att.content}`
2326
2400
  headers,
2327
2401
  body: payload,
2328
2402
  credentials,
2329
- vendorLabel: "Gemini",
2403
+ vendorLabel: VENDOR_LABELS.gemini,
2330
2404
  retryableStatuses: _GeminiTransport.RETRYABLE_STATUSES,
2331
2405
  timeout: this.timeout,
2332
2406
  stallTimeout: this.stallTimeout,
@@ -2379,7 +2453,8 @@ var EmptyAIProviderRegistry = class _EmptyAIProviderRegistry {
2379
2453
  }
2380
2454
  getStatus(name) {
2381
2455
  return __awaiter(this, void 0, void 0, function* () {
2382
- var _a, _b, _c;
2456
+ var _a, _b;
2457
+ var _c;
2383
2458
  if (name !== void 0 && name !== _EmptyAIProviderRegistry.DEFAULT_NAME)
2384
2459
  return null;
2385
2460
  return (_c = yield (_b = (_a = this.noopProvider).getStatus) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : null;
@@ -2387,7 +2462,8 @@ var EmptyAIProviderRegistry = class _EmptyAIProviderRegistry {
2387
2462
  }
2388
2463
  listStatuses() {
2389
2464
  return __awaiter(this, void 0, void 0, function* () {
2390
- var _a, _b, _c;
2465
+ var _a, _b;
2466
+ var _c;
2391
2467
  const status = (_c = yield (_b = (_a = this.noopProvider).getStatus) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : null;
2392
2468
  return [{ name: _EmptyAIProviderRegistry.DEFAULT_NAME, isDefault: true, status }];
2393
2469
  });
@@ -2431,7 +2507,8 @@ var MutableAIProviderRegistry = class _MutableAIProviderRegistry {
2431
2507
  }
2432
2508
  getStatus(name) {
2433
2509
  return __awaiter(this, void 0, void 0, function* () {
2434
- var _a, _b;
2510
+ var _a;
2511
+ var _b;
2435
2512
  const provider = name === void 0 ? this.default() : this.providers.get(name);
2436
2513
  if (!provider)
2437
2514
  return null;
@@ -2442,7 +2519,8 @@ var MutableAIProviderRegistry = class _MutableAIProviderRegistry {
2442
2519
  return __awaiter(this, void 0, void 0, function* () {
2443
2520
  const entries = [...this.providers.entries()];
2444
2521
  return Promise.all(entries.map((_a) => __awaiter(this, [_a], void 0, function* ([name, provider]) {
2445
- var _b, _c;
2522
+ var _b;
2523
+ var _c;
2446
2524
  return {
2447
2525
  name,
2448
2526
  isDefault: name === this._defaultName,
@@ -3076,6 +3154,19 @@ function accumulate(messages, into) {
3076
3154
  var TOOL_FOLD_SYMBOL = Symbol("toolFold");
3077
3155
 
3078
3156
  // src/components/chat-driver/chat-driver.ts
3157
+ var budgetDetailOf = (e) => {
3158
+ const vendor = vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor);
3159
+ if (e.budgetUsd == null && e.spentUsd == null && !vendor) return void 0;
3160
+ return {
3161
+ budgetUsd: e.budgetUsd,
3162
+ spentUsd: e.spentUsd,
3163
+ vendorLabel: e.vendorLabel,
3164
+ ...vendor ? { vendor } : {},
3165
+ // Server-known truth the client cannot re-derive, so it rides all the way to
3166
+ // the banner rather than being re-guessed there from registry membership.
3167
+ ...e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}
3168
+ };
3169
+ };
3079
3170
  var DEFAULT_MAX_TOOL_ITERATIONS = 50;
3080
3171
  var DEFAULT_MAX_FOLD_OPERATIONS = 5;
3081
3172
  var DEFAULT_MAX_TURN_SNAPSHOTS = 400;
@@ -3258,6 +3349,22 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3258
3349
  * picked up on the next turn.
3259
3350
  */
3260
3351
  this.resolvedStatusCache = /* @__PURE__ */ new Map();
3352
+ /**
3353
+ * Set the moment a budget wall is observed anywhere in this turn — this
3354
+ * driver's own 402, or a sub-agent's (which surfaces here only as a
3355
+ * `'budget_exhausted'` tool result). Read at the top of the tool loop to end
3356
+ * the turn before issuing another model call that would hit the same wall.
3357
+ * Reset per turn alongside the other per-turn counters.
3358
+ */
3359
+ this.budgetExhaustedThisTurn = false;
3360
+ /**
3361
+ * Whether this turn's budget wall came from a SUB-AGENT rather than this
3362
+ * driver's own request. Decides whether `lastResolvedProvider` is a valid
3363
+ * attribution fallback: for an own wall it is the refusing vendor, for a
3364
+ * child's wall it is the parent's vendor — the one known NOT to have refused.
3365
+ * Reset per turn alongside `budgetWallDetail`.
3366
+ */
3367
+ this.budgetWallViaSubAgent = false;
3261
3368
  const {
3262
3369
  toolHandlers = {},
3263
3370
  toolDefinitions = [],
@@ -3268,12 +3375,14 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3268
3375
  condenseBatchCalls = 1,
3269
3376
  maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
3270
3377
  sessionKey = "",
3271
- activityBus = NOOP_ACTIVITY_BUS
3378
+ activityBus = NOOP_ACTIVITY_BUS,
3379
+ budgetExhaustedMessage = DEFAULT_BUDGET_EXHAUSTED_MESSAGE
3272
3380
  } = config;
3273
3381
  this.maxToolIterations = maxToolIterations;
3274
3382
  this.condenseBatchCalls = condenseBatchCalls;
3275
3383
  this.sessionKey = sessionKey;
3276
3384
  this.activityBus = activityBus;
3385
+ this.budgetExhaustedMessage = budgetExhaustedMessage;
3277
3386
  if (typeof toolHandlers === "function") {
3278
3387
  this.toolHandlersFactory = toolHandlers;
3279
3388
  this.toolHandlers = {};
@@ -3380,8 +3489,56 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3380
3489
  * (rather than set to `undefined`) so a happy-path result stays byte-identical to
3381
3490
  * the historical `{ reason: 'done' }`.
3382
3491
  */
3383
- turnDone(failureReason) {
3384
- return failureReason ? { reason: "done", failureReason } : { reason: "done" };
3492
+ turnDone(failureReason, budget) {
3493
+ if (!failureReason) return { reason: "done" };
3494
+ return budget ? { reason: "done", failureReason, budget } : { reason: "done", failureReason };
3495
+ }
3496
+ /**
3497
+ * Terminal budget outcome for a wall hit **outside** the tool loop — today,
3498
+ * `OrchestratingDriver`'s classification phase, which calls the provider
3499
+ * directly and so never enters `runToolLoop`.
3500
+ *
3501
+ * Does **not** publish `tool-loop-end`: no `tool-loop-start` was published for
3502
+ * the classify phase, and an unbalanced end would break start/end pairing for
3503
+ * subscribers that rely on it. The driver **return value** is what reports this
3504
+ * case — see `FoundationAiAssistant`'s latch, which reads both seams for
3505
+ * exactly this reason.
3506
+ *
3507
+ * The non-sub-agent tail of the in-loop `BudgetExhaustedError` branch lives
3508
+ * here so there is one copy of the log line, the debug-log entry, the
3509
+ * transcript bubble and the result shape rather than two that can drift.
3510
+ *
3511
+ * @param pendingUserMessage - a user message that has NOT yet been appended,
3512
+ * appended first so the answer does not end up replying to nothing. Only the
3513
+ * classification seam passes it: `OrchestratingDriver` dispatches the user's
3514
+ * text as an optimistic `history-updated` detail and leaves the real append
3515
+ * to `chatDriver.sendMessage`, which never runs when `classify()` throws — so
3516
+ * the bubble below would re-dispatch a history the user's own message was
3517
+ * never in, and it would vanish from the transcript on the next render. The
3518
+ * in-loop caller has already appended it and passes nothing.
3519
+ *
3520
+ * @internal
3521
+ */
3522
+ reportBudgetExhausted(e, pendingUserMessage) {
3523
+ if (pendingUserMessage) this.appendToHistory(pendingUserMessage);
3524
+ this.budgetExhaustedThisTurn = true;
3525
+ logger2.error("ChatDriver: AI budget exhausted", e);
3526
+ recordTurnError(this.sessionKey, "budget-exhausted", {
3527
+ agent: this.activeAgentName,
3528
+ provider: this.lastResolvedProviderName,
3529
+ // The registry ALIAS (e.g. 'high') is what `provider` records; the vendor
3530
+ // is the thing a per-vendor budget is actually scoped to, and it was known
3531
+ // at both ends and discarded in the middle until now. Taken from the
3532
+ // refusing transport's label first — this method also serves the
3533
+ // classification seam, where `lastResolvedProvider` is the PREVIOUS turn's
3534
+ // vendor (or nothing), because classify runs against the registry default.
3535
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
3536
+ budgetUsd: e.budgetUsd,
3537
+ spentUsd: e.spentUsd,
3538
+ isSubAgent: this.isSubAgent
3539
+ });
3540
+ this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
3541
+ return this.turnDone("budget-exhausted", budgetDetailOf(e));
3385
3542
  }
3386
3543
  /** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
3387
3544
  static failureReasonOf(result) {
@@ -3391,10 +3548,28 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3391
3548
  * Build the `tool-loop-end` event detail for a turn's result. A failure carries a
3392
3549
  * `{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,
3393
3550
  * kept byte-identical so subscribers see exactly what they always have.
3551
+ *
3552
+ * A budget failure additionally carries `vendor` — the concrete vendor
3553
+ * (`'anthropic'`/`'gemini'`) the walled turn resolved to, which the driver knows
3554
+ * and used to discard. Optional and additive: a subscriber reading only
3555
+ * `failureReason` is unaffected, a non-budget failure still emits the historical
3556
+ * `{ failureReason }` with no `vendor` key, and the value is a plain string so
3557
+ * the detail stays structured-cloneable for the cross-tab hop. It is the field a
3558
+ * per-vendor budget model needs and the one that would be awkward to retrofit.
3394
3559
  */
3395
- static loopEndDetail(result) {
3560
+ loopEndDetail(result) {
3396
3561
  const failureReason = _ChatDriver.failureReasonOf(result);
3397
- return failureReason ? { failureReason } : void 0;
3562
+ if (!failureReason) return void 0;
3563
+ if (failureReason !== "budget-exhausted") {
3564
+ return { failureReason };
3565
+ }
3566
+ const budget = result.reason === "done" ? result.budget : void 0;
3567
+ const vendor = budget?.vendor ?? (this.budgetWallViaSubAgent ? void 0 : this.lastResolvedProvider);
3568
+ return {
3569
+ failureReason,
3570
+ ...vendor ? { vendor } : {},
3571
+ ...budget ? { budget } : {}
3572
+ };
3398
3573
  }
3399
3574
  /**
3400
3575
  * Swap in a new agent's configuration. Called by OrchestratingDriver before
@@ -3580,9 +3755,9 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3580
3755
  * under a separate session key, so recording here would orphan the event off
3581
3756
  * the user-visible debug-log timeline.)
3582
3757
  */
3583
- failSubAgent(reason) {
3758
+ failSubAgent(reason, budget) {
3584
3759
  if (!this.isSubAgent || this.subAgentFailure) return;
3585
- this.subAgentFailure = { reason };
3760
+ this.subAgentFailure = budget ? { reason, budget } : { reason };
3586
3761
  }
3587
3762
  /**
3588
3763
  * Returns true if `releaseAgent` was called during the most recent turn.
@@ -4032,6 +4207,9 @@ Output format (strict):
4032
4207
  this.subAgentCompletion = void 0;
4033
4208
  this.subAgentFailure = void 0;
4034
4209
  this.agentReleaseRequested = false;
4210
+ this.budgetExhaustedThisTurn = false;
4211
+ this.budgetWallDetail = void 0;
4212
+ this.budgetWallViaSubAgent = false;
4035
4213
  this.appendToHistory({ role: "user", content: userInput, attachments });
4036
4214
  this.turnStartedAt = Date.now();
4037
4215
  recordMetaEvent(this.sessionKey, "turn.start", {
@@ -4066,7 +4244,7 @@ Output format (strict):
4066
4244
  });
4067
4245
  this.busy = false;
4068
4246
  this.endTurn();
4069
- this.activityBus.publish("tool-loop-end", _ChatDriver.loopEndDetail(result));
4247
+ this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
4070
4248
  }
4071
4249
  }
4072
4250
  /**
@@ -4257,8 +4435,14 @@ Output format (strict):
4257
4435
  recordMetaEvent(this.sessionKey, "subagent.completed", { agent: name });
4258
4436
  return { outcome: { ok: true, result: completion.result }, trace };
4259
4437
  }
4260
- const reason = child.getSubAgentFailure()?.reason ?? "max_iterations";
4438
+ const failure = child.getSubAgentFailure();
4439
+ const reason = failure?.reason ?? "max_iterations";
4261
4440
  recordMetaEvent(this.sessionKey, "subagent.failed", { agent: name, reason });
4441
+ if (reason === "budget_exhausted") {
4442
+ this.budgetExhaustedThisTurn = true;
4443
+ this.budgetWallViaSubAgent = true;
4444
+ this.budgetWallDetail ??= failure?.budget;
4445
+ }
4262
4446
  return { outcome: { ok: false, reason }, trace };
4263
4447
  }
4264
4448
  /**
@@ -4271,6 +4455,9 @@ Output format (strict):
4271
4455
  this.beginTurn();
4272
4456
  this.subAgentCompletion = void 0;
4273
4457
  this.subAgentFailure = void 0;
4458
+ this.budgetExhaustedThisTurn = false;
4459
+ this.budgetWallDetail = void 0;
4460
+ this.budgetWallViaSubAgent = false;
4274
4461
  this.turnStartedAt = Date.now();
4275
4462
  recordMetaEvent(this.sessionKey, "turn.start", {
4276
4463
  phase: "continueFromHistory",
@@ -4304,7 +4491,7 @@ Output format (strict):
4304
4491
  });
4305
4492
  this.busy = false;
4306
4493
  this.endTurn();
4307
- this.activityBus.publish("tool-loop-end", _ChatDriver.loopEndDetail(result));
4494
+ this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
4308
4495
  }
4309
4496
  }
4310
4497
  // ---------------------------------------------------------------------------
@@ -4458,6 +4645,31 @@ Output format (strict):
4458
4645
  if (this.turnController.signal.aborted) {
4459
4646
  return this.completeAbortedTurn();
4460
4647
  }
4648
+ if (this.budgetExhaustedThisTurn) {
4649
+ logger2.error("ChatDriver: ending the turn \u2014 a sub-agent hit the AI budget wall");
4650
+ recordTurnError(this.sessionKey, "budget-exhausted", {
4651
+ agent: this.activeAgentName,
4652
+ provider: this.lastResolvedProviderName,
4653
+ // The CHILD's vendor when it knew one, and NOTHING otherwise. On this
4654
+ // path the wall is definitionally the child's, and on a mixed registry
4655
+ // `lastResolvedProvider` is this driver's own vendor — the one known
4656
+ // NOT to have refused. An unattributable child wall must degrade to
4657
+ // the vendor-agnostic block (which `latchBlockedFrom` handles
4658
+ // fail-safe), never to a vendor that is known to be wrong: naming the
4659
+ // parent's vendor here walled BOTH — the child's via its own
4660
+ // tool-loop-end, the parent's via this event — and derived `blocked`
4661
+ // over headroom that still existed.
4662
+ vendor: this.budgetWallDetail?.vendor,
4663
+ via: "sub-agent",
4664
+ isSubAgent: this.isSubAgent
4665
+ });
4666
+ if (this.isSubAgent) {
4667
+ this.failSubAgent("budget_exhausted", this.budgetWallDetail);
4668
+ } else {
4669
+ this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
4670
+ }
4671
+ return this.turnDone("budget-exhausted", this.budgetWallDetail);
4672
+ }
4461
4673
  const promptCtx = {
4462
4674
  agentName: this.activeAgentName ?? "",
4463
4675
  history: this.history,
@@ -4629,6 +4841,25 @@ ${tailBody}
4629
4841
  }
4630
4842
  return this.turnDone("response-truncated");
4631
4843
  }
4844
+ if (e instanceof BudgetExhaustedError) {
4845
+ this.budgetExhaustedThisTurn = true;
4846
+ this.budgetWallDetail = budgetDetailOf(e);
4847
+ this.budgetWallViaSubAgent = false;
4848
+ if (this.isSubAgent) {
4849
+ logger2.error("ChatDriver: AI budget exhausted", e);
4850
+ recordTurnError(this.sessionKey, "budget-exhausted", {
4851
+ agent: this.activeAgentName,
4852
+ provider: this.lastResolvedProviderName,
4853
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
4854
+ budgetUsd: e.budgetUsd,
4855
+ spentUsd: e.spentUsd,
4856
+ isSubAgent: true
4857
+ });
4858
+ this.failSubAgent("budget_exhausted", this.budgetWallDetail);
4859
+ return this.turnDone("budget-exhausted", this.budgetWallDetail);
4860
+ }
4861
+ return this.reportBudgetExhausted(e);
4862
+ }
4632
4863
  if (e instanceof DOMException && e.name === "TimeoutError") {
4633
4864
  logger2.error("ChatDriver: request timed out", e);
4634
4865
  recordTurnError(this.sessionKey, "exception", {
@@ -5104,6 +5335,17 @@ var OrchestratingDriver = class extends EventTarget {
5104
5335
  * user stops. Reset at the start of each `sendMessage`.
5105
5336
  */
5106
5337
  this.cancelled = false;
5338
+ /**
5339
+ * Whether the CURRENT turn's user message has reached the inner driver's
5340
+ * history. False through the pre-first-turn classify (where the only echo of
5341
+ * the message is an optimistic `history-updated` dispatch), true from the
5342
+ * moment `chatDriver.sendMessage` is entered — including through every later
5343
+ * handoff classify. Read by the budget-wall catch in `sendMessage` to decide
5344
+ * whether `reportBudgetExhausted` must append the message itself: appending
5345
+ * it when already appended duplicated it; not appending it when unappended
5346
+ * made it vanish. Reset at the top of each `runOrchestratedTurn`.
5347
+ */
5348
+ this.userMessageAppended = false;
5107
5349
  /**
5108
5350
  * Sticky user pick from the picker (or the host's `setAgent` API). Only
5109
5351
  * changes on explicit user action. Survives flow completion: when a stateful
@@ -5145,7 +5387,8 @@ var OrchestratingDriver = class extends EventTarget {
5145
5387
  maxFoldOperations: options.maxFoldOperations,
5146
5388
  maxTurnSnapshots: options.maxTurnSnapshots,
5147
5389
  sessionKey: this.sessionKey,
5148
- activityBus: options.activityBus
5390
+ activityBus: options.activityBus,
5391
+ budgetExhaustedMessage: options.budgetExhaustedMessage
5149
5392
  });
5150
5393
  this.chatDriver.addEventListener("history-updated", (e) => {
5151
5394
  this.dispatchEvent(new CustomEvent("history-updated", { detail: e.detail }));
@@ -5244,7 +5487,25 @@ var OrchestratingDriver = class extends EventTarget {
5244
5487
  return this.chatDriver.getSuggestions(history, prompt2, count, agentInfo);
5245
5488
  }
5246
5489
  async sendMessage(input, attachments) {
5490
+ try {
5491
+ return await this.runOrchestratedTurn(input, attachments);
5492
+ } catch (e) {
5493
+ if (e instanceof BudgetExhaustedError) {
5494
+ return this.chatDriver.reportBudgetExhausted(
5495
+ e,
5496
+ this.userMessageAppended ? void 0 : {
5497
+ role: "user",
5498
+ content: input,
5499
+ ...attachments ? { attachments } : {}
5500
+ }
5501
+ );
5502
+ }
5503
+ throw e;
5504
+ }
5505
+ }
5506
+ async runOrchestratedTurn(input, attachments) {
5247
5507
  this.cancelled = false;
5508
+ this.userMessageAppended = false;
5248
5509
  const history = this.chatDriver.getHistory();
5249
5510
  this.dispatchEvent(
5250
5511
  new CustomEvent("history-updated", {
@@ -5258,6 +5519,7 @@ var OrchestratingDriver = class extends EventTarget {
5258
5519
  let handoffs = 0;
5259
5520
  let handoffSummary = "";
5260
5521
  let remainingTask = "";
5522
+ let lastResult;
5261
5523
  while (true) {
5262
5524
  if (this.cancelled) break;
5263
5525
  await this.applyAgent(currentAgent);
@@ -5266,8 +5528,10 @@ var OrchestratingDriver = class extends EventTarget {
5266
5528
  const contextPrimer = handoffSummary ? [{ role: "user", content: `[Context from previous agent]: ${handoffSummary}` }] : [];
5267
5529
  result = await this.chatDriver.continueFromHistory(contextPrimer);
5268
5530
  } else {
5531
+ this.userMessageAppended = true;
5269
5532
  result = await this.chatDriver.sendMessage(input, attachments);
5270
5533
  }
5534
+ lastResult = result;
5271
5535
  if (this.chatDriver.getAgentReleaseRequested()) {
5272
5536
  await this.releaseActiveAgent();
5273
5537
  break;
@@ -5289,7 +5553,7 @@ var OrchestratingDriver = class extends EventTarget {
5289
5553
  this.dispatchEvent(new CustomEvent("orchestrating-start"));
5290
5554
  currentAgent = await this.classify(remainingTask, updatedHistory);
5291
5555
  }
5292
- return { reason: "done" };
5556
+ return lastResult?.reason === "done" ? lastResult : { reason: "done" };
5293
5557
  }
5294
5558
  async continueFromHistory(transientPrimer) {
5295
5559
  return this.chatDriver.continueFromHistory(transientPrimer);
@@ -5472,6 +5736,7 @@ ${recentMessages}` : ""}`;
5472
5736
  }
5473
5737
  }
5474
5738
  };
5739
+ let routedNoMatch = false;
5475
5740
  for (let attempt = 0; attempt <= this.classifierRetries; attempt += 1) {
5476
5741
  try {
5477
5742
  const options = {
@@ -5491,8 +5756,10 @@ ${recentMessages}` : ""}`;
5491
5756
  if (index >= 0 && index < this.specialists.length) {
5492
5757
  return this.specialists[index];
5493
5758
  }
5759
+ routedNoMatch = true;
5494
5760
  break;
5495
5761
  } catch (e) {
5762
+ if (e instanceof BudgetExhaustedError) throw e;
5496
5763
  logger2.warn(`OrchestratingDriver: classifier attempt ${attempt + 1} failed:`, e);
5497
5764
  if (attempt === this.classifierRetries) {
5498
5765
  logger2.error("OrchestratingDriver: classifier failed after all retries, using fallback");
@@ -5500,10 +5767,12 @@ ${recentMessages}` : ""}`;
5500
5767
  }
5501
5768
  }
5502
5769
  if (this.fallback) return this.fallback;
5503
- const specialistNames = this.specialists.map((s) => s.name).join(", ");
5504
- this.appendInlineMessage(
5505
- `I'm not sure how to help with that. I can assist with: ${specialistNames}.`
5506
- );
5770
+ if (routedNoMatch) {
5771
+ const specialistNames = this.specialists.map((s) => s.name).join(", ");
5772
+ this.appendInlineMessage(
5773
+ `I'm not sure how to help with that. I can assist with: ${specialistNames}.`
5774
+ );
5775
+ }
5507
5776
  return this.specialists[0];
5508
5777
  }
5509
5778
  /**