@genesislcap/ai-assistant 15.6.2 → 15.7.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 +613 -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 +254 -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 +394 -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 +162 -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 +11 -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 +969 -0
  37. package/dist/esm/main/main.js +704 -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 +145 -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 +1316 -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 +704 -10
  60. package/src/state/ai-assistant-slice.test.ts +215 -0
  61. package/src/state/ai-assistant-slice.ts +218 -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
@@ -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",
@@ -1170,15 +1231,19 @@ function postWithRetry(options) {
1170
1231
  signal: combinedSignal,
1171
1232
  credentials
1172
1233
  });
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
1234
  if (!response.ok) {
1180
- const err = yield response.text();
1181
- throw new Error(`${vendorLabel} request error ${response.status}: ${err}`);
1235
+ const errText = yield response.text();
1236
+ const parsed = parseJsonOrUndefined(errText);
1237
+ if (isBudgetRejection(response.status, codeOf(parsed))) {
1238
+ throw budgetExhaustedFrom(vendorLabel, parsed, errText || void 0);
1239
+ }
1240
+ if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
1241
+ logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1242
+ clearTimeout(timeoutId);
1243
+ yield backoff(attempt);
1244
+ continue;
1245
+ }
1246
+ throw new Error(`${vendorLabel} request error ${response.status}: ${errText}`);
1182
1247
  }
1183
1248
  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
1249
  rearmStallTimer();
@@ -1188,6 +1253,9 @@ function postWithRetry(options) {
1188
1253
  if (frame.code === "UPSTREAM_STALLED") {
1189
1254
  throw new DOMException(`${vendorLabel} request stalled: ${(_c = frame.error) !== null && _c !== void 0 ? _c : "no upstream data"}`, "TimeoutError");
1190
1255
  }
1256
+ if (isBudgetRejection(frame.status, frame.code)) {
1257
+ throw budgetExhaustedFrom(vendorLabel, frame);
1258
+ }
1191
1259
  if (retryableStatuses.includes(frame.status) && attempt < MAX_RETRIES) {
1192
1260
  logger.warn(`${vendorLabel}Transport: retryable status ${frame.status} (err frame), retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1193
1261
  clearTimeout(timeoutId);
@@ -1658,7 +1726,7 @@ ${att.content}` });
1658
1726
  headers: requestHeaders,
1659
1727
  body,
1660
1728
  credentials,
1661
- vendorLabel: "Anthropic",
1729
+ vendorLabel: VENDOR_LABELS.anthropic,
1662
1730
  retryableStatuses: _AnthropicTransport.RETRYABLE_STATUSES,
1663
1731
  timeout: this.timeout,
1664
1732
  stallTimeout: this.stallTimeout,
@@ -2326,7 +2394,7 @@ ${att.content}`
2326
2394
  headers,
2327
2395
  body: payload,
2328
2396
  credentials,
2329
- vendorLabel: "Gemini",
2397
+ vendorLabel: VENDOR_LABELS.gemini,
2330
2398
  retryableStatuses: _GeminiTransport.RETRYABLE_STATUSES,
2331
2399
  timeout: this.timeout,
2332
2400
  stallTimeout: this.stallTimeout,
@@ -3076,6 +3144,19 @@ function accumulate(messages, into) {
3076
3144
  var TOOL_FOLD_SYMBOL = Symbol("toolFold");
3077
3145
 
3078
3146
  // src/components/chat-driver/chat-driver.ts
3147
+ var budgetDetailOf = (e) => {
3148
+ const vendor = vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor);
3149
+ if (e.budgetUsd == null && e.spentUsd == null && !vendor) return void 0;
3150
+ return {
3151
+ budgetUsd: e.budgetUsd,
3152
+ spentUsd: e.spentUsd,
3153
+ vendorLabel: e.vendorLabel,
3154
+ ...vendor ? { vendor } : {},
3155
+ // Server-known truth the client cannot re-derive, so it rides all the way to
3156
+ // the banner rather than being re-guessed there from registry membership.
3157
+ ...e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}
3158
+ };
3159
+ };
3079
3160
  var DEFAULT_MAX_TOOL_ITERATIONS = 50;
3080
3161
  var DEFAULT_MAX_FOLD_OPERATIONS = 5;
3081
3162
  var DEFAULT_MAX_TURN_SNAPSHOTS = 400;
@@ -3258,6 +3339,22 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3258
3339
  * picked up on the next turn.
3259
3340
  */
3260
3341
  this.resolvedStatusCache = /* @__PURE__ */ new Map();
3342
+ /**
3343
+ * Set the moment a budget wall is observed anywhere in this turn — this
3344
+ * driver's own 402, or a sub-agent's (which surfaces here only as a
3345
+ * `'budget_exhausted'` tool result). Read at the top of the tool loop to end
3346
+ * the turn before issuing another model call that would hit the same wall.
3347
+ * Reset per turn alongside the other per-turn counters.
3348
+ */
3349
+ this.budgetExhaustedThisTurn = false;
3350
+ /**
3351
+ * Whether this turn's budget wall came from a SUB-AGENT rather than this
3352
+ * driver's own request. Decides whether `lastResolvedProvider` is a valid
3353
+ * attribution fallback: for an own wall it is the refusing vendor, for a
3354
+ * child's wall it is the parent's vendor — the one known NOT to have refused.
3355
+ * Reset per turn alongside `budgetWallDetail`.
3356
+ */
3357
+ this.budgetWallViaSubAgent = false;
3261
3358
  const {
3262
3359
  toolHandlers = {},
3263
3360
  toolDefinitions = [],
@@ -3268,12 +3365,14 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3268
3365
  condenseBatchCalls = 1,
3269
3366
  maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
3270
3367
  sessionKey = "",
3271
- activityBus = NOOP_ACTIVITY_BUS
3368
+ activityBus = NOOP_ACTIVITY_BUS,
3369
+ budgetExhaustedMessage = DEFAULT_BUDGET_EXHAUSTED_MESSAGE
3272
3370
  } = config;
3273
3371
  this.maxToolIterations = maxToolIterations;
3274
3372
  this.condenseBatchCalls = condenseBatchCalls;
3275
3373
  this.sessionKey = sessionKey;
3276
3374
  this.activityBus = activityBus;
3375
+ this.budgetExhaustedMessage = budgetExhaustedMessage;
3277
3376
  if (typeof toolHandlers === "function") {
3278
3377
  this.toolHandlersFactory = toolHandlers;
3279
3378
  this.toolHandlers = {};
@@ -3380,8 +3479,56 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3380
3479
  * (rather than set to `undefined`) so a happy-path result stays byte-identical to
3381
3480
  * the historical `{ reason: 'done' }`.
3382
3481
  */
3383
- turnDone(failureReason) {
3384
- return failureReason ? { reason: "done", failureReason } : { reason: "done" };
3482
+ turnDone(failureReason, budget) {
3483
+ if (!failureReason) return { reason: "done" };
3484
+ return budget ? { reason: "done", failureReason, budget } : { reason: "done", failureReason };
3485
+ }
3486
+ /**
3487
+ * Terminal budget outcome for a wall hit **outside** the tool loop — today,
3488
+ * `OrchestratingDriver`'s classification phase, which calls the provider
3489
+ * directly and so never enters `runToolLoop`.
3490
+ *
3491
+ * Does **not** publish `tool-loop-end`: no `tool-loop-start` was published for
3492
+ * the classify phase, and an unbalanced end would break start/end pairing for
3493
+ * subscribers that rely on it. The driver **return value** is what reports this
3494
+ * case — see `FoundationAiAssistant`'s latch, which reads both seams for
3495
+ * exactly this reason.
3496
+ *
3497
+ * The non-sub-agent tail of the in-loop `BudgetExhaustedError` branch lives
3498
+ * here so there is one copy of the log line, the debug-log entry, the
3499
+ * transcript bubble and the result shape rather than two that can drift.
3500
+ *
3501
+ * @param pendingUserMessage - a user message that has NOT yet been appended,
3502
+ * appended first so the answer does not end up replying to nothing. Only the
3503
+ * classification seam passes it: `OrchestratingDriver` dispatches the user's
3504
+ * text as an optimistic `history-updated` detail and leaves the real append
3505
+ * to `chatDriver.sendMessage`, which never runs when `classify()` throws — so
3506
+ * the bubble below would re-dispatch a history the user's own message was
3507
+ * never in, and it would vanish from the transcript on the next render. The
3508
+ * in-loop caller has already appended it and passes nothing.
3509
+ *
3510
+ * @internal
3511
+ */
3512
+ reportBudgetExhausted(e, pendingUserMessage) {
3513
+ if (pendingUserMessage) this.appendToHistory(pendingUserMessage);
3514
+ this.budgetExhaustedThisTurn = true;
3515
+ logger2.error("ChatDriver: AI budget exhausted", e);
3516
+ recordTurnError(this.sessionKey, "budget-exhausted", {
3517
+ agent: this.activeAgentName,
3518
+ provider: this.lastResolvedProviderName,
3519
+ // The registry ALIAS (e.g. 'high') is what `provider` records; the vendor
3520
+ // is the thing a per-vendor budget is actually scoped to, and it was known
3521
+ // at both ends and discarded in the middle until now. Taken from the
3522
+ // refusing transport's label first — this method also serves the
3523
+ // classification seam, where `lastResolvedProvider` is the PREVIOUS turn's
3524
+ // vendor (or nothing), because classify runs against the registry default.
3525
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
3526
+ budgetUsd: e.budgetUsd,
3527
+ spentUsd: e.spentUsd,
3528
+ isSubAgent: this.isSubAgent
3529
+ });
3530
+ this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
3531
+ return this.turnDone("budget-exhausted", budgetDetailOf(e));
3385
3532
  }
3386
3533
  /** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
3387
3534
  static failureReasonOf(result) {
@@ -3391,10 +3538,28 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3391
3538
  * Build the `tool-loop-end` event detail for a turn's result. A failure carries a
3392
3539
  * `{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,
3393
3540
  * kept byte-identical so subscribers see exactly what they always have.
3541
+ *
3542
+ * A budget failure additionally carries `vendor` — the concrete vendor
3543
+ * (`'anthropic'`/`'gemini'`) the walled turn resolved to, which the driver knows
3544
+ * and used to discard. Optional and additive: a subscriber reading only
3545
+ * `failureReason` is unaffected, a non-budget failure still emits the historical
3546
+ * `{ failureReason }` with no `vendor` key, and the value is a plain string so
3547
+ * the detail stays structured-cloneable for the cross-tab hop. It is the field a
3548
+ * per-vendor budget model needs and the one that would be awkward to retrofit.
3394
3549
  */
3395
- static loopEndDetail(result) {
3550
+ loopEndDetail(result) {
3396
3551
  const failureReason = _ChatDriver.failureReasonOf(result);
3397
- return failureReason ? { failureReason } : void 0;
3552
+ if (!failureReason) return void 0;
3553
+ if (failureReason !== "budget-exhausted") {
3554
+ return { failureReason };
3555
+ }
3556
+ const budget = result.reason === "done" ? result.budget : void 0;
3557
+ const vendor = budget?.vendor ?? (this.budgetWallViaSubAgent ? void 0 : this.lastResolvedProvider);
3558
+ return {
3559
+ failureReason,
3560
+ ...vendor ? { vendor } : {},
3561
+ ...budget ? { budget } : {}
3562
+ };
3398
3563
  }
3399
3564
  /**
3400
3565
  * Swap in a new agent's configuration. Called by OrchestratingDriver before
@@ -3580,9 +3745,9 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3580
3745
  * under a separate session key, so recording here would orphan the event off
3581
3746
  * the user-visible debug-log timeline.)
3582
3747
  */
3583
- failSubAgent(reason) {
3748
+ failSubAgent(reason, budget) {
3584
3749
  if (!this.isSubAgent || this.subAgentFailure) return;
3585
- this.subAgentFailure = { reason };
3750
+ this.subAgentFailure = budget ? { reason, budget } : { reason };
3586
3751
  }
3587
3752
  /**
3588
3753
  * Returns true if `releaseAgent` was called during the most recent turn.
@@ -4032,6 +4197,9 @@ Output format (strict):
4032
4197
  this.subAgentCompletion = void 0;
4033
4198
  this.subAgentFailure = void 0;
4034
4199
  this.agentReleaseRequested = false;
4200
+ this.budgetExhaustedThisTurn = false;
4201
+ this.budgetWallDetail = void 0;
4202
+ this.budgetWallViaSubAgent = false;
4035
4203
  this.appendToHistory({ role: "user", content: userInput, attachments });
4036
4204
  this.turnStartedAt = Date.now();
4037
4205
  recordMetaEvent(this.sessionKey, "turn.start", {
@@ -4066,7 +4234,7 @@ Output format (strict):
4066
4234
  });
4067
4235
  this.busy = false;
4068
4236
  this.endTurn();
4069
- this.activityBus.publish("tool-loop-end", _ChatDriver.loopEndDetail(result));
4237
+ this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
4070
4238
  }
4071
4239
  }
4072
4240
  /**
@@ -4257,8 +4425,14 @@ Output format (strict):
4257
4425
  recordMetaEvent(this.sessionKey, "subagent.completed", { agent: name });
4258
4426
  return { outcome: { ok: true, result: completion.result }, trace };
4259
4427
  }
4260
- const reason = child.getSubAgentFailure()?.reason ?? "max_iterations";
4428
+ const failure = child.getSubAgentFailure();
4429
+ const reason = failure?.reason ?? "max_iterations";
4261
4430
  recordMetaEvent(this.sessionKey, "subagent.failed", { agent: name, reason });
4431
+ if (reason === "budget_exhausted") {
4432
+ this.budgetExhaustedThisTurn = true;
4433
+ this.budgetWallViaSubAgent = true;
4434
+ this.budgetWallDetail ??= failure?.budget;
4435
+ }
4262
4436
  return { outcome: { ok: false, reason }, trace };
4263
4437
  }
4264
4438
  /**
@@ -4271,6 +4445,9 @@ Output format (strict):
4271
4445
  this.beginTurn();
4272
4446
  this.subAgentCompletion = void 0;
4273
4447
  this.subAgentFailure = void 0;
4448
+ this.budgetExhaustedThisTurn = false;
4449
+ this.budgetWallDetail = void 0;
4450
+ this.budgetWallViaSubAgent = false;
4274
4451
  this.turnStartedAt = Date.now();
4275
4452
  recordMetaEvent(this.sessionKey, "turn.start", {
4276
4453
  phase: "continueFromHistory",
@@ -4304,7 +4481,7 @@ Output format (strict):
4304
4481
  });
4305
4482
  this.busy = false;
4306
4483
  this.endTurn();
4307
- this.activityBus.publish("tool-loop-end", _ChatDriver.loopEndDetail(result));
4484
+ this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
4308
4485
  }
4309
4486
  }
4310
4487
  // ---------------------------------------------------------------------------
@@ -4458,6 +4635,31 @@ Output format (strict):
4458
4635
  if (this.turnController.signal.aborted) {
4459
4636
  return this.completeAbortedTurn();
4460
4637
  }
4638
+ if (this.budgetExhaustedThisTurn) {
4639
+ logger2.error("ChatDriver: ending the turn \u2014 a sub-agent hit the AI budget wall");
4640
+ recordTurnError(this.sessionKey, "budget-exhausted", {
4641
+ agent: this.activeAgentName,
4642
+ provider: this.lastResolvedProviderName,
4643
+ // The CHILD's vendor when it knew one, and NOTHING otherwise. On this
4644
+ // path the wall is definitionally the child's, and on a mixed registry
4645
+ // `lastResolvedProvider` is this driver's own vendor — the one known
4646
+ // NOT to have refused. An unattributable child wall must degrade to
4647
+ // the vendor-agnostic block (which `latchBlockedFrom` handles
4648
+ // fail-safe), never to a vendor that is known to be wrong: naming the
4649
+ // parent's vendor here walled BOTH — the child's via its own
4650
+ // tool-loop-end, the parent's via this event — and derived `blocked`
4651
+ // over headroom that still existed.
4652
+ vendor: this.budgetWallDetail?.vendor,
4653
+ via: "sub-agent",
4654
+ isSubAgent: this.isSubAgent
4655
+ });
4656
+ if (this.isSubAgent) {
4657
+ this.failSubAgent("budget_exhausted", this.budgetWallDetail);
4658
+ } else {
4659
+ this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
4660
+ }
4661
+ return this.turnDone("budget-exhausted", this.budgetWallDetail);
4662
+ }
4461
4663
  const promptCtx = {
4462
4664
  agentName: this.activeAgentName ?? "",
4463
4665
  history: this.history,
@@ -4629,6 +4831,25 @@ ${tailBody}
4629
4831
  }
4630
4832
  return this.turnDone("response-truncated");
4631
4833
  }
4834
+ if (e instanceof BudgetExhaustedError) {
4835
+ this.budgetExhaustedThisTurn = true;
4836
+ this.budgetWallDetail = budgetDetailOf(e);
4837
+ this.budgetWallViaSubAgent = false;
4838
+ if (this.isSubAgent) {
4839
+ logger2.error("ChatDriver: AI budget exhausted", e);
4840
+ recordTurnError(this.sessionKey, "budget-exhausted", {
4841
+ agent: this.activeAgentName,
4842
+ provider: this.lastResolvedProviderName,
4843
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
4844
+ budgetUsd: e.budgetUsd,
4845
+ spentUsd: e.spentUsd,
4846
+ isSubAgent: true
4847
+ });
4848
+ this.failSubAgent("budget_exhausted", this.budgetWallDetail);
4849
+ return this.turnDone("budget-exhausted", this.budgetWallDetail);
4850
+ }
4851
+ return this.reportBudgetExhausted(e);
4852
+ }
4632
4853
  if (e instanceof DOMException && e.name === "TimeoutError") {
4633
4854
  logger2.error("ChatDriver: request timed out", e);
4634
4855
  recordTurnError(this.sessionKey, "exception", {
@@ -5104,6 +5325,17 @@ var OrchestratingDriver = class extends EventTarget {
5104
5325
  * user stops. Reset at the start of each `sendMessage`.
5105
5326
  */
5106
5327
  this.cancelled = false;
5328
+ /**
5329
+ * Whether the CURRENT turn's user message has reached the inner driver's
5330
+ * history. False through the pre-first-turn classify (where the only echo of
5331
+ * the message is an optimistic `history-updated` dispatch), true from the
5332
+ * moment `chatDriver.sendMessage` is entered — including through every later
5333
+ * handoff classify. Read by the budget-wall catch in `sendMessage` to decide
5334
+ * whether `reportBudgetExhausted` must append the message itself: appending
5335
+ * it when already appended duplicated it; not appending it when unappended
5336
+ * made it vanish. Reset at the top of each `runOrchestratedTurn`.
5337
+ */
5338
+ this.userMessageAppended = false;
5107
5339
  /**
5108
5340
  * Sticky user pick from the picker (or the host's `setAgent` API). Only
5109
5341
  * changes on explicit user action. Survives flow completion: when a stateful
@@ -5145,7 +5377,8 @@ var OrchestratingDriver = class extends EventTarget {
5145
5377
  maxFoldOperations: options.maxFoldOperations,
5146
5378
  maxTurnSnapshots: options.maxTurnSnapshots,
5147
5379
  sessionKey: this.sessionKey,
5148
- activityBus: options.activityBus
5380
+ activityBus: options.activityBus,
5381
+ budgetExhaustedMessage: options.budgetExhaustedMessage
5149
5382
  });
5150
5383
  this.chatDriver.addEventListener("history-updated", (e) => {
5151
5384
  this.dispatchEvent(new CustomEvent("history-updated", { detail: e.detail }));
@@ -5244,7 +5477,25 @@ var OrchestratingDriver = class extends EventTarget {
5244
5477
  return this.chatDriver.getSuggestions(history, prompt2, count, agentInfo);
5245
5478
  }
5246
5479
  async sendMessage(input, attachments) {
5480
+ try {
5481
+ return await this.runOrchestratedTurn(input, attachments);
5482
+ } catch (e) {
5483
+ if (e instanceof BudgetExhaustedError) {
5484
+ return this.chatDriver.reportBudgetExhausted(
5485
+ e,
5486
+ this.userMessageAppended ? void 0 : {
5487
+ role: "user",
5488
+ content: input,
5489
+ ...attachments ? { attachments } : {}
5490
+ }
5491
+ );
5492
+ }
5493
+ throw e;
5494
+ }
5495
+ }
5496
+ async runOrchestratedTurn(input, attachments) {
5247
5497
  this.cancelled = false;
5498
+ this.userMessageAppended = false;
5248
5499
  const history = this.chatDriver.getHistory();
5249
5500
  this.dispatchEvent(
5250
5501
  new CustomEvent("history-updated", {
@@ -5258,6 +5509,7 @@ var OrchestratingDriver = class extends EventTarget {
5258
5509
  let handoffs = 0;
5259
5510
  let handoffSummary = "";
5260
5511
  let remainingTask = "";
5512
+ let lastResult;
5261
5513
  while (true) {
5262
5514
  if (this.cancelled) break;
5263
5515
  await this.applyAgent(currentAgent);
@@ -5266,8 +5518,10 @@ var OrchestratingDriver = class extends EventTarget {
5266
5518
  const contextPrimer = handoffSummary ? [{ role: "user", content: `[Context from previous agent]: ${handoffSummary}` }] : [];
5267
5519
  result = await this.chatDriver.continueFromHistory(contextPrimer);
5268
5520
  } else {
5521
+ this.userMessageAppended = true;
5269
5522
  result = await this.chatDriver.sendMessage(input, attachments);
5270
5523
  }
5524
+ lastResult = result;
5271
5525
  if (this.chatDriver.getAgentReleaseRequested()) {
5272
5526
  await this.releaseActiveAgent();
5273
5527
  break;
@@ -5289,7 +5543,7 @@ var OrchestratingDriver = class extends EventTarget {
5289
5543
  this.dispatchEvent(new CustomEvent("orchestrating-start"));
5290
5544
  currentAgent = await this.classify(remainingTask, updatedHistory);
5291
5545
  }
5292
- return { reason: "done" };
5546
+ return lastResult?.reason === "done" ? lastResult : { reason: "done" };
5293
5547
  }
5294
5548
  async continueFromHistory(transientPrimer) {
5295
5549
  return this.chatDriver.continueFromHistory(transientPrimer);
@@ -5472,6 +5726,7 @@ ${recentMessages}` : ""}`;
5472
5726
  }
5473
5727
  }
5474
5728
  };
5729
+ let routedNoMatch = false;
5475
5730
  for (let attempt = 0; attempt <= this.classifierRetries; attempt += 1) {
5476
5731
  try {
5477
5732
  const options = {
@@ -5491,8 +5746,10 @@ ${recentMessages}` : ""}`;
5491
5746
  if (index >= 0 && index < this.specialists.length) {
5492
5747
  return this.specialists[index];
5493
5748
  }
5749
+ routedNoMatch = true;
5494
5750
  break;
5495
5751
  } catch (e) {
5752
+ if (e instanceof BudgetExhaustedError) throw e;
5496
5753
  logger2.warn(`OrchestratingDriver: classifier attempt ${attempt + 1} failed:`, e);
5497
5754
  if (attempt === this.classifierRetries) {
5498
5755
  logger2.error("OrchestratingDriver: classifier failed after all retries, using fallback");
@@ -5500,10 +5757,12 @@ ${recentMessages}` : ""}`;
5500
5757
  }
5501
5758
  }
5502
5759
  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
- );
5760
+ if (routedNoMatch) {
5761
+ const specialistNames = this.specialists.map((s) => s.name).join(", ");
5762
+ this.appendInlineMessage(
5763
+ `I'm not sure how to help with that. I can assist with: ${specialistNames}.`
5764
+ );
5765
+ }
5507
5766
  return this.specialists[0];
5508
5767
  }
5509
5768
  /**