@genesislcap/ai-assistant 15.3.2 → 15.3.3-alpha-4899ddd.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/ai-assistant.api.json +391 -5
  2. package/dist/ai-assistant.d.ts +603 -6
  3. package/dist/chat-driver.cjs +285 -26
  4. package/dist/chat-driver.cjs.map +3 -3
  5. package/dist/chat-driver.mjs +285 -26
  6. package/dist/chat-driver.mjs.map +3 -3
  7. package/dist/custom-elements.json +236 -10
  8. package/dist/dts/channel/ai-activity-channel.d.ts +51 -1
  9. package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
  10. package/dist/dts/components/chat-driver/chat-driver.d.ts +99 -1
  11. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  12. package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +1 -1
  13. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +2 -0
  14. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +1 -0
  15. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -0
  16. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  17. package/dist/dts/main/blocked-state.test.d.ts +2 -0
  18. package/dist/dts/main/blocked-state.test.d.ts.map +1 -0
  19. package/dist/dts/main/main.d.ts +384 -6
  20. package/dist/dts/main/main.d.ts.map +1 -1
  21. package/dist/dts/main/main.styles.d.ts.map +1 -1
  22. package/dist/dts/main/main.styles.test.d.ts +2 -0
  23. package/dist/dts/main/main.styles.test.d.ts.map +1 -0
  24. package/dist/dts/main/main.template.d.ts +53 -0
  25. package/dist/dts/main/main.template.d.ts.map +1 -1
  26. package/dist/dts/state/ai-assistant-slice.d.ts +143 -6
  27. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  28. package/dist/dts/state/debug-event-log.d.ts +6 -1
  29. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  30. package/dist/dts/state/session-store.d.ts +10 -0
  31. package/dist/dts/state/session-store.d.ts.map +1 -1
  32. package/dist/esm/components/chat-driver/chat-driver.js +263 -21
  33. package/dist/esm/components/chat-driver/chat-driver.test.js +464 -1
  34. package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +312 -0
  35. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +89 -4
  36. package/dist/esm/main/blocked-state.test.js +913 -0
  37. package/dist/esm/main/main.js +680 -16
  38. package/dist/esm/main/main.styles.js +47 -0
  39. package/dist/esm/main/main.styles.test.js +86 -0
  40. package/dist/esm/main/main.template.js +121 -4
  41. package/dist/esm/state/ai-assistant-slice.js +128 -7
  42. package/dist/esm/state/ai-assistant-slice.test.js +138 -1
  43. package/dist/esm/state/debug-event-log.js +7 -2
  44. package/dist/esm/state/debug-event-log.test.js +49 -1
  45. package/dist/esm/state/persistence/session-snapshot.test.js +18 -0
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/docs/migration-GENC-1464.md +562 -0
  48. package/docs/sub_agent.md +20 -3
  49. package/package.json +17 -17
  50. package/src/channel/ai-activity-channel.ts +56 -2
  51. package/src/components/chat-driver/chat-driver.test.ts +549 -0
  52. package/src/components/chat-driver/chat-driver.ts +324 -14
  53. package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +438 -0
  54. package/src/components/orchestrating-driver/orchestrating-driver.ts +101 -6
  55. package/src/main/blocked-state.test.ts +1243 -0
  56. package/src/main/main.styles.test.ts +103 -0
  57. package/src/main/main.styles.ts +47 -0
  58. package/src/main/main.template.ts +131 -4
  59. package/src/main/main.ts +680 -10
  60. package/src/state/ai-assistant-slice.test.ts +215 -0
  61. package/src/state/ai-assistant-slice.ts +181 -8
  62. package/src/state/debug-event-log.test.ts +63 -0
  63. package/src/state/debug-event-log.ts +7 -2
  64. package/src/state/persistence/session-snapshot.test.ts +22 -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",
@@ -1123,15 +1184,19 @@ function postWithRetry(options) {
1123
1184
  signal: combinedSignal,
1124
1185
  credentials
1125
1186
  });
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
1187
  if (!response.ok) {
1133
- const err = yield response.text();
1134
- throw new Error(`${vendorLabel} request error ${response.status}: ${err}`);
1188
+ const errText = yield response.text();
1189
+ const parsed = parseJsonOrUndefined(errText);
1190
+ if (isBudgetRejection(response.status, codeOf(parsed))) {
1191
+ throw budgetExhaustedFrom(vendorLabel, parsed, errText || void 0);
1192
+ }
1193
+ if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
1194
+ logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1195
+ clearTimeout(timeoutId);
1196
+ yield backoff(attempt);
1197
+ continue;
1198
+ }
1199
+ throw new Error(`${vendorLabel} request error ${response.status}: ${errText}`);
1135
1200
  }
1136
1201
  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
1202
  rearmStallTimer();
@@ -1141,6 +1206,9 @@ function postWithRetry(options) {
1141
1206
  if (frame.code === "UPSTREAM_STALLED") {
1142
1207
  throw new DOMException(`${vendorLabel} request stalled: ${(_c = frame.error) !== null && _c !== void 0 ? _c : "no upstream data"}`, "TimeoutError");
1143
1208
  }
1209
+ if (isBudgetRejection(frame.status, frame.code)) {
1210
+ throw budgetExhaustedFrom(vendorLabel, frame);
1211
+ }
1144
1212
  if (retryableStatuses.includes(frame.status) && attempt < MAX_RETRIES) {
1145
1213
  logger.warn(`${vendorLabel}Transport: retryable status ${frame.status} (err frame), retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1146
1214
  clearTimeout(timeoutId);
@@ -1603,7 +1671,7 @@ ${att.content}` });
1603
1671
  headers: requestHeaders,
1604
1672
  body,
1605
1673
  credentials,
1606
- vendorLabel: "Anthropic",
1674
+ vendorLabel: VENDOR_LABELS.anthropic,
1607
1675
  retryableStatuses: _AnthropicTransport.RETRYABLE_STATUSES,
1608
1676
  timeout: this.timeout,
1609
1677
  stallTimeout: this.stallTimeout,
@@ -2252,7 +2320,7 @@ ${att.content}`
2252
2320
  headers,
2253
2321
  body: payload,
2254
2322
  credentials,
2255
- vendorLabel: "Gemini",
2323
+ vendorLabel: VENDOR_LABELS.gemini,
2256
2324
  retryableStatuses: _GeminiTransport.RETRYABLE_STATUSES,
2257
2325
  timeout: this.timeout,
2258
2326
  stallTimeout: this.stallTimeout,
@@ -2954,6 +3022,19 @@ The above summarizes earlier parts of this conversation that were compacted. Con
2954
3022
  var TOOL_FOLD_SYMBOL = Symbol("toolFold");
2955
3023
 
2956
3024
  // src/components/chat-driver/chat-driver.ts
3025
+ var budgetDetailOf = (e) => {
3026
+ const vendor = vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor);
3027
+ if (e.budgetUsd == null && e.spentUsd == null && !vendor) return void 0;
3028
+ return {
3029
+ budgetUsd: e.budgetUsd,
3030
+ spentUsd: e.spentUsd,
3031
+ vendorLabel: e.vendorLabel,
3032
+ ...vendor ? { vendor } : {},
3033
+ // Server-known truth the client cannot re-derive, so it rides all the way to
3034
+ // the banner rather than being re-guessed there from registry membership.
3035
+ ...e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}
3036
+ };
3037
+ };
2957
3038
  var DEFAULT_MAX_TOOL_ITERATIONS = 50;
2958
3039
  var DEFAULT_MAX_FOLD_OPERATIONS = 5;
2959
3040
  var DEFAULT_MAX_TURN_SNAPSHOTS = 400;
@@ -3136,6 +3217,22 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3136
3217
  * picked up on the next turn.
3137
3218
  */
3138
3219
  this.resolvedStatusCache = /* @__PURE__ */ new Map();
3220
+ /**
3221
+ * Set the moment a budget wall is observed anywhere in this turn — this
3222
+ * driver's own 402, or a sub-agent's (which surfaces here only as a
3223
+ * `'budget_exhausted'` tool result). Read at the top of the tool loop to end
3224
+ * the turn before issuing another model call that would hit the same wall.
3225
+ * Reset per turn alongside the other per-turn counters.
3226
+ */
3227
+ this.budgetExhaustedThisTurn = false;
3228
+ /**
3229
+ * Whether this turn's budget wall came from a SUB-AGENT rather than this
3230
+ * driver's own request. Decides whether `lastResolvedProvider` is a valid
3231
+ * attribution fallback: for an own wall it is the refusing vendor, for a
3232
+ * child's wall it is the parent's vendor — the one known NOT to have refused.
3233
+ * Reset per turn alongside `budgetWallDetail`.
3234
+ */
3235
+ this.budgetWallViaSubAgent = false;
3139
3236
  const {
3140
3237
  toolHandlers = {},
3141
3238
  toolDefinitions = [],
@@ -3145,11 +3242,13 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3145
3242
  maxFoldOperations = DEFAULT_MAX_FOLD_OPERATIONS,
3146
3243
  maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
3147
3244
  sessionKey = "",
3148
- activityBus = NOOP_ACTIVITY_BUS
3245
+ activityBus = NOOP_ACTIVITY_BUS,
3246
+ budgetExhaustedMessage = DEFAULT_BUDGET_EXHAUSTED_MESSAGE
3149
3247
  } = config;
3150
3248
  this.maxToolIterations = maxToolIterations;
3151
3249
  this.sessionKey = sessionKey;
3152
3250
  this.activityBus = activityBus;
3251
+ this.budgetExhaustedMessage = budgetExhaustedMessage;
3153
3252
  if (typeof toolHandlers === "function") {
3154
3253
  this.toolHandlersFactory = toolHandlers;
3155
3254
  this.toolHandlers = {};
@@ -3256,8 +3355,56 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3256
3355
  * (rather than set to `undefined`) so a happy-path result stays byte-identical to
3257
3356
  * the historical `{ reason: 'done' }`.
3258
3357
  */
3259
- turnDone(failureReason) {
3260
- return failureReason ? { reason: "done", failureReason } : { reason: "done" };
3358
+ turnDone(failureReason, budget) {
3359
+ if (!failureReason) return { reason: "done" };
3360
+ return budget ? { reason: "done", failureReason, budget } : { reason: "done", failureReason };
3361
+ }
3362
+ /**
3363
+ * Terminal budget outcome for a wall hit **outside** the tool loop — today,
3364
+ * `OrchestratingDriver`'s classification phase, which calls the provider
3365
+ * directly and so never enters `runToolLoop`.
3366
+ *
3367
+ * Does **not** publish `tool-loop-end`: no `tool-loop-start` was published for
3368
+ * the classify phase, and an unbalanced end would break start/end pairing for
3369
+ * subscribers that rely on it. The driver **return value** is what reports this
3370
+ * case — see `FoundationAiAssistant`'s latch, which reads both seams for
3371
+ * exactly this reason.
3372
+ *
3373
+ * The non-sub-agent tail of the in-loop `BudgetExhaustedError` branch lives
3374
+ * here so there is one copy of the log line, the debug-log entry, the
3375
+ * transcript bubble and the result shape rather than two that can drift.
3376
+ *
3377
+ * @param pendingUserMessage - a user message that has NOT yet been appended,
3378
+ * appended first so the answer does not end up replying to nothing. Only the
3379
+ * classification seam passes it: `OrchestratingDriver` dispatches the user's
3380
+ * text as an optimistic `history-updated` detail and leaves the real append
3381
+ * to `chatDriver.sendMessage`, which never runs when `classify()` throws — so
3382
+ * the bubble below would re-dispatch a history the user's own message was
3383
+ * never in, and it would vanish from the transcript on the next render. The
3384
+ * in-loop caller has already appended it and passes nothing.
3385
+ *
3386
+ * @internal
3387
+ */
3388
+ reportBudgetExhausted(e, pendingUserMessage) {
3389
+ if (pendingUserMessage) this.appendToHistory(pendingUserMessage);
3390
+ this.budgetExhaustedThisTurn = true;
3391
+ logger2.error("ChatDriver: AI budget exhausted", e);
3392
+ recordTurnError(this.sessionKey, "budget-exhausted", {
3393
+ agent: this.activeAgentName,
3394
+ provider: this.lastResolvedProviderName,
3395
+ // The registry ALIAS (e.g. 'high') is what `provider` records; the vendor
3396
+ // is the thing a per-vendor budget is actually scoped to, and it was known
3397
+ // at both ends and discarded in the middle until now. Taken from the
3398
+ // refusing transport's label first — this method also serves the
3399
+ // classification seam, where `lastResolvedProvider` is the PREVIOUS turn's
3400
+ // vendor (or nothing), because classify runs against the registry default.
3401
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
3402
+ budgetUsd: e.budgetUsd,
3403
+ spentUsd: e.spentUsd,
3404
+ isSubAgent: this.isSubAgent
3405
+ });
3406
+ this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
3407
+ return this.turnDone("budget-exhausted", budgetDetailOf(e));
3261
3408
  }
3262
3409
  /** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
3263
3410
  static failureReasonOf(result) {
@@ -3267,10 +3414,28 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3267
3414
  * Build the `tool-loop-end` event detail for a turn's result. A failure carries a
3268
3415
  * `{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,
3269
3416
  * kept byte-identical so subscribers see exactly what they always have.
3417
+ *
3418
+ * A budget failure additionally carries `vendor` — the concrete vendor
3419
+ * (`'anthropic'`/`'gemini'`) the walled turn resolved to, which the driver knows
3420
+ * and used to discard. Optional and additive: a subscriber reading only
3421
+ * `failureReason` is unaffected, a non-budget failure still emits the historical
3422
+ * `{ failureReason }` with no `vendor` key, and the value is a plain string so
3423
+ * the detail stays structured-cloneable for the cross-tab hop. It is the field a
3424
+ * per-vendor budget model needs and the one that would be awkward to retrofit.
3270
3425
  */
3271
- static loopEndDetail(result) {
3426
+ loopEndDetail(result) {
3272
3427
  const failureReason = _ChatDriver.failureReasonOf(result);
3273
- return failureReason ? { failureReason } : void 0;
3428
+ if (!failureReason) return void 0;
3429
+ if (failureReason !== "budget-exhausted") {
3430
+ return { failureReason };
3431
+ }
3432
+ const budget = result.reason === "done" ? result.budget : void 0;
3433
+ const vendor = budget?.vendor ?? (this.budgetWallViaSubAgent ? void 0 : this.lastResolvedProvider);
3434
+ return {
3435
+ failureReason,
3436
+ ...vendor ? { vendor } : {},
3437
+ ...budget ? { budget } : {}
3438
+ };
3274
3439
  }
3275
3440
  /**
3276
3441
  * Swap in a new agent's configuration. Called by OrchestratingDriver before
@@ -3456,9 +3621,9 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3456
3621
  * under a separate session key, so recording here would orphan the event off
3457
3622
  * the user-visible debug-log timeline.)
3458
3623
  */
3459
- failSubAgent(reason) {
3624
+ failSubAgent(reason, budget) {
3460
3625
  if (!this.isSubAgent || this.subAgentFailure) return;
3461
- this.subAgentFailure = { reason };
3626
+ this.subAgentFailure = budget ? { reason, budget } : { reason };
3462
3627
  }
3463
3628
  /**
3464
3629
  * Returns true if `releaseAgent` was called during the most recent turn.
@@ -3903,6 +4068,9 @@ Output format (strict):
3903
4068
  this.subAgentCompletion = void 0;
3904
4069
  this.subAgentFailure = void 0;
3905
4070
  this.agentReleaseRequested = false;
4071
+ this.budgetExhaustedThisTurn = false;
4072
+ this.budgetWallDetail = void 0;
4073
+ this.budgetWallViaSubAgent = false;
3906
4074
  this.appendToHistory({ role: "user", content: userInput, attachments });
3907
4075
  this.turnStartedAt = Date.now();
3908
4076
  recordMetaEvent(this.sessionKey, "turn.start", {
@@ -3937,7 +4105,7 @@ Output format (strict):
3937
4105
  });
3938
4106
  this.busy = false;
3939
4107
  this.endTurn();
3940
- this.activityBus.publish("tool-loop-end", _ChatDriver.loopEndDetail(result));
4108
+ this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
3941
4109
  }
3942
4110
  }
3943
4111
  /**
@@ -4128,8 +4296,14 @@ Output format (strict):
4128
4296
  recordMetaEvent(this.sessionKey, "subagent.completed", { agent: name });
4129
4297
  return { outcome: { ok: true, result: completion.result }, trace };
4130
4298
  }
4131
- const reason = child.getSubAgentFailure()?.reason ?? "max_iterations";
4299
+ const failure = child.getSubAgentFailure();
4300
+ const reason = failure?.reason ?? "max_iterations";
4132
4301
  recordMetaEvent(this.sessionKey, "subagent.failed", { agent: name, reason });
4302
+ if (reason === "budget_exhausted") {
4303
+ this.budgetExhaustedThisTurn = true;
4304
+ this.budgetWallViaSubAgent = true;
4305
+ this.budgetWallDetail ??= failure?.budget;
4306
+ }
4133
4307
  return { outcome: { ok: false, reason }, trace };
4134
4308
  }
4135
4309
  /**
@@ -4142,6 +4316,9 @@ Output format (strict):
4142
4316
  this.beginTurn();
4143
4317
  this.subAgentCompletion = void 0;
4144
4318
  this.subAgentFailure = void 0;
4319
+ this.budgetExhaustedThisTurn = false;
4320
+ this.budgetWallDetail = void 0;
4321
+ this.budgetWallViaSubAgent = false;
4145
4322
  this.turnStartedAt = Date.now();
4146
4323
  recordMetaEvent(this.sessionKey, "turn.start", {
4147
4324
  phase: "continueFromHistory",
@@ -4175,7 +4352,7 @@ Output format (strict):
4175
4352
  });
4176
4353
  this.busy = false;
4177
4354
  this.endTurn();
4178
- this.activityBus.publish("tool-loop-end", _ChatDriver.loopEndDetail(result));
4355
+ this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
4179
4356
  }
4180
4357
  }
4181
4358
  // ---------------------------------------------------------------------------
@@ -4329,6 +4506,31 @@ Output format (strict):
4329
4506
  if (this.turnController.signal.aborted) {
4330
4507
  return this.completeAbortedTurn();
4331
4508
  }
4509
+ if (this.budgetExhaustedThisTurn) {
4510
+ logger2.error("ChatDriver: ending the turn \u2014 a sub-agent hit the AI budget wall");
4511
+ recordTurnError(this.sessionKey, "budget-exhausted", {
4512
+ agent: this.activeAgentName,
4513
+ provider: this.lastResolvedProviderName,
4514
+ // The CHILD's vendor when it knew one, and NOTHING otherwise. On this
4515
+ // path the wall is definitionally the child's, and on a mixed registry
4516
+ // `lastResolvedProvider` is this driver's own vendor — the one known
4517
+ // NOT to have refused. An unattributable child wall must degrade to
4518
+ // the vendor-agnostic block (which `latchBlockedFrom` handles
4519
+ // fail-safe), never to a vendor that is known to be wrong: naming the
4520
+ // parent's vendor here walled BOTH — the child's via its own
4521
+ // tool-loop-end, the parent's via this event — and derived `blocked`
4522
+ // over headroom that still existed.
4523
+ vendor: this.budgetWallDetail?.vendor,
4524
+ via: "sub-agent",
4525
+ isSubAgent: this.isSubAgent
4526
+ });
4527
+ if (this.isSubAgent) {
4528
+ this.failSubAgent("budget_exhausted", this.budgetWallDetail);
4529
+ } else {
4530
+ this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
4531
+ }
4532
+ return this.turnDone("budget-exhausted", this.budgetWallDetail);
4533
+ }
4332
4534
  const promptCtx = {
4333
4535
  agentName: this.activeAgentName ?? "",
4334
4536
  history: this.history,
@@ -4499,6 +4701,25 @@ ${tailBody}
4499
4701
  }
4500
4702
  return this.turnDone("response-truncated");
4501
4703
  }
4704
+ if (e instanceof BudgetExhaustedError) {
4705
+ this.budgetExhaustedThisTurn = true;
4706
+ this.budgetWallDetail = budgetDetailOf(e);
4707
+ this.budgetWallViaSubAgent = false;
4708
+ if (this.isSubAgent) {
4709
+ logger2.error("ChatDriver: AI budget exhausted", e);
4710
+ recordTurnError(this.sessionKey, "budget-exhausted", {
4711
+ agent: this.activeAgentName,
4712
+ provider: this.lastResolvedProviderName,
4713
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
4714
+ budgetUsd: e.budgetUsd,
4715
+ spentUsd: e.spentUsd,
4716
+ isSubAgent: true
4717
+ });
4718
+ this.failSubAgent("budget_exhausted", this.budgetWallDetail);
4719
+ return this.turnDone("budget-exhausted", this.budgetWallDetail);
4720
+ }
4721
+ return this.reportBudgetExhausted(e);
4722
+ }
4502
4723
  if (e instanceof DOMException && e.name === "TimeoutError") {
4503
4724
  logger2.error("ChatDriver: request timed out", e);
4504
4725
  recordTurnError(this.sessionKey, "exception", {
@@ -4974,6 +5195,17 @@ var OrchestratingDriver = class extends EventTarget {
4974
5195
  * user stops. Reset at the start of each `sendMessage`.
4975
5196
  */
4976
5197
  this.cancelled = false;
5198
+ /**
5199
+ * Whether the CURRENT turn's user message has reached the inner driver's
5200
+ * history. False through the pre-first-turn classify (where the only echo of
5201
+ * the message is an optimistic `history-updated` dispatch), true from the
5202
+ * moment `chatDriver.sendMessage` is entered — including through every later
5203
+ * handoff classify. Read by the budget-wall catch in `sendMessage` to decide
5204
+ * whether `reportBudgetExhausted` must append the message itself: appending
5205
+ * it when already appended duplicated it; not appending it when unappended
5206
+ * made it vanish. Reset at the top of each `runOrchestratedTurn`.
5207
+ */
5208
+ this.userMessageAppended = false;
4977
5209
  /**
4978
5210
  * Sticky user pick from the picker (or the host's `setAgent` API). Only
4979
5211
  * changes on explicit user action. Survives flow completion: when a stateful
@@ -5015,7 +5247,8 @@ var OrchestratingDriver = class extends EventTarget {
5015
5247
  maxFoldOperations: options.maxFoldOperations,
5016
5248
  maxTurnSnapshots: options.maxTurnSnapshots,
5017
5249
  sessionKey: this.sessionKey,
5018
- activityBus: options.activityBus
5250
+ activityBus: options.activityBus,
5251
+ budgetExhaustedMessage: options.budgetExhaustedMessage
5019
5252
  });
5020
5253
  this.chatDriver.addEventListener("history-updated", (e) => {
5021
5254
  this.dispatchEvent(new CustomEvent("history-updated", { detail: e.detail }));
@@ -5114,7 +5347,25 @@ var OrchestratingDriver = class extends EventTarget {
5114
5347
  return this.chatDriver.getSuggestions(history, prompt2, count, agentInfo);
5115
5348
  }
5116
5349
  async sendMessage(input, attachments) {
5350
+ try {
5351
+ return await this.runOrchestratedTurn(input, attachments);
5352
+ } catch (e) {
5353
+ if (e instanceof BudgetExhaustedError) {
5354
+ return this.chatDriver.reportBudgetExhausted(
5355
+ e,
5356
+ this.userMessageAppended ? void 0 : {
5357
+ role: "user",
5358
+ content: input,
5359
+ ...attachments ? { attachments } : {}
5360
+ }
5361
+ );
5362
+ }
5363
+ throw e;
5364
+ }
5365
+ }
5366
+ async runOrchestratedTurn(input, attachments) {
5117
5367
  this.cancelled = false;
5368
+ this.userMessageAppended = false;
5118
5369
  const history = this.chatDriver.getHistory();
5119
5370
  this.dispatchEvent(
5120
5371
  new CustomEvent("history-updated", {
@@ -5128,6 +5379,7 @@ var OrchestratingDriver = class extends EventTarget {
5128
5379
  let handoffs = 0;
5129
5380
  let handoffSummary = "";
5130
5381
  let remainingTask = "";
5382
+ let lastResult;
5131
5383
  while (true) {
5132
5384
  if (this.cancelled) break;
5133
5385
  await this.applyAgent(currentAgent);
@@ -5136,8 +5388,10 @@ var OrchestratingDriver = class extends EventTarget {
5136
5388
  const contextPrimer = handoffSummary ? [{ role: "user", content: `[Context from previous agent]: ${handoffSummary}` }] : [];
5137
5389
  result = await this.chatDriver.continueFromHistory(contextPrimer);
5138
5390
  } else {
5391
+ this.userMessageAppended = true;
5139
5392
  result = await this.chatDriver.sendMessage(input, attachments);
5140
5393
  }
5394
+ lastResult = result;
5141
5395
  if (this.chatDriver.getAgentReleaseRequested()) {
5142
5396
  await this.releaseActiveAgent();
5143
5397
  break;
@@ -5159,7 +5413,7 @@ var OrchestratingDriver = class extends EventTarget {
5159
5413
  this.dispatchEvent(new CustomEvent("orchestrating-start"));
5160
5414
  currentAgent = await this.classify(remainingTask, updatedHistory);
5161
5415
  }
5162
- return { reason: "done" };
5416
+ return lastResult?.reason === "done" ? lastResult : { reason: "done" };
5163
5417
  }
5164
5418
  async continueFromHistory(transientPrimer) {
5165
5419
  return this.chatDriver.continueFromHistory(transientPrimer);
@@ -5342,6 +5596,7 @@ ${recentMessages}` : ""}`;
5342
5596
  }
5343
5597
  }
5344
5598
  };
5599
+ let routedNoMatch = false;
5345
5600
  for (let attempt = 0; attempt <= this.classifierRetries; attempt += 1) {
5346
5601
  try {
5347
5602
  const options = {
@@ -5361,8 +5616,10 @@ ${recentMessages}` : ""}`;
5361
5616
  if (index >= 0 && index < this.specialists.length) {
5362
5617
  return this.specialists[index];
5363
5618
  }
5619
+ routedNoMatch = true;
5364
5620
  break;
5365
5621
  } catch (e) {
5622
+ if (e instanceof BudgetExhaustedError) throw e;
5366
5623
  logger2.warn(`OrchestratingDriver: classifier attempt ${attempt + 1} failed:`, e);
5367
5624
  if (attempt === this.classifierRetries) {
5368
5625
  logger2.error("OrchestratingDriver: classifier failed after all retries, using fallback");
@@ -5370,10 +5627,12 @@ ${recentMessages}` : ""}`;
5370
5627
  }
5371
5628
  }
5372
5629
  if (this.fallback) return this.fallback;
5373
- const specialistNames = this.specialists.map((s) => s.name).join(", ");
5374
- this.appendInlineMessage(
5375
- `I'm not sure how to help with that. I can assist with: ${specialistNames}.`
5376
- );
5630
+ if (routedNoMatch) {
5631
+ const specialistNames = this.specialists.map((s) => s.name).join(", ");
5632
+ this.appendInlineMessage(
5633
+ `I'm not sure how to help with that. I can assist with: ${specialistNames}.`
5634
+ );
5635
+ }
5377
5636
  return this.specialists[0];
5378
5637
  }
5379
5638
  /**