@genesislcap/ai-assistant 15.13.0 → 15.13.1-GENC-1506.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.
@@ -5923,6 +5923,33 @@
5923
5923
  "endIndex": 3
5924
5924
  }
5925
5925
  },
5926
+ {
5927
+ "kind": "PropertySignature",
5928
+ "canonicalReference": "@genesislcap/ai-assistant!ChatDriverConfig#providerRefusedMessage:member",
5929
+ "docComment": "/**\n * Overrides the transcript sentence shown when the upstream PROVIDER refuses the account (GENC-1506) — see `DEFAULT_PROVIDER_REFUSED_MESSAGE`.\n *\n * Separate from `budgetExhaustedMessage` because the two conditions are separate: ours is a spend cap a host administrator can raise, this is the vendor declining to serve us at all. A host that white-labels one will usually want to white-label both, but conflating them into one field would force identical copy on two situations with different remedies.\n *\n * Rarely needed: unlike the budget default, the shipped sentence names no vendor and no Genesis, so it is already safe for a white-labelled deployment.\n */\n",
5930
+ "excerptTokens": [
5931
+ {
5932
+ "kind": "Content",
5933
+ "text": "providerRefusedMessage?: "
5934
+ },
5935
+ {
5936
+ "kind": "Content",
5937
+ "text": "string"
5938
+ },
5939
+ {
5940
+ "kind": "Content",
5941
+ "text": ";"
5942
+ }
5943
+ ],
5944
+ "isReadonly": false,
5945
+ "isOptional": true,
5946
+ "releaseTag": "Beta",
5947
+ "name": "providerRefusedMessage",
5948
+ "propertyTypeTokenRange": {
5949
+ "startIndex": 1,
5950
+ "endIndex": 2
5951
+ }
5952
+ },
5926
5953
  {
5927
5954
  "kind": "PropertySignature",
5928
5955
  "canonicalReference": "@genesislcap/ai-assistant!ChatDriverConfig#sessionKey:member",
@@ -1524,6 +1524,24 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
1524
1524
  * and the bubble is composed per wall by {@link ChatDriver.budgetExhaustedBubble}.
1525
1525
  */
1526
1526
  private readonly budgetExhaustedMessageOverride?;
1527
+ /**
1528
+ * Host override for the provider-refusal sentence, from
1529
+ * `ChatDriverConfig.providerRefusedMessage`. `undefined` means no override, so the shipped default
1530
+ * is used verbatim.
1531
+ */
1532
+ private readonly providerRefusedMessageOverride?;
1533
+ /**
1534
+ * A provider refusal was observed this turn, so the tool loop must not call the model again
1535
+ * (GENC-1506).
1536
+ *
1537
+ * Exists for the SUB-AGENT path only, exactly like `budgetExhaustedThisTurn`: this driver's own
1538
+ * refusal returns straight out of the catch, whereas a child's refusal reaches the parent as a tool
1539
+ * result, and without this flag the loop would issue another call into the same wall — N batched
1540
+ * children costing N doomed calls plus a doomed parent one.
1541
+ */
1542
+ private providerRefusedThisTurn;
1543
+ /** The refusal detail latched off a sub-agent's failure, for the parent's own result. */
1544
+ private providerRefusedDetail?;
1527
1545
  /**
1528
1546
  * Set the moment a budget wall is observed anywhere in this turn — this
1529
1547
  * driver's own 402, or a sub-agent's (which surfaces here only as a
@@ -1629,6 +1647,24 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
1629
1647
  */
1630
1648
  private budgetExhaustedBubble;
1631
1649
  reportBudgetExhausted(e: BudgetExhaustedError, pendingUserMessage?: ChatMessage): ChatDriverResult;
1650
+ /**
1651
+ * The sentence shown when the upstream provider refuses (GENC-1506).
1652
+ *
1653
+ * Deliberately far simpler than `budgetExhaustedBubble`: no vendor name, no figures, no
1654
+ * switch-provider advice, and no branching on `kind`. The copy is cause-free by design — a user who
1655
+ * can see their own remaining spend must not be told about a limit, and we must not imply the bill
1656
+ * has gone unpaid — so there is nothing here to compose. A host override wins verbatim.
1657
+ */
1658
+ private providerRefusedBubble;
1659
+ /**
1660
+ * Terminal provider-refusal outcome. Mirrors `reportBudgetExhausted` so the two walls behave
1661
+ * identically from the caller's side, while keeping their diagnostics distinct.
1662
+ *
1663
+ * `kind` reaches the debug log and the result but never the transcript: it is what lets an operator
1664
+ * tell "top up the account" from "rotate the key", and with cause-free user copy this is the only
1665
+ * place that distinction survives.
1666
+ */
1667
+ private reportProviderRefused;
1632
1668
  /** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
1633
1669
  private static failureReasonOf;
1634
1670
  /**
@@ -1980,6 +2016,19 @@ export declare interface ChatDriverConfig {
1980
2016
  * reversible change.
1981
2017
  */
1982
2018
  budgetExhaustedMessage?: string;
2019
+ /**
2020
+ * Overrides the transcript sentence shown when the upstream PROVIDER refuses the account
2021
+ * (GENC-1506) — see `DEFAULT_PROVIDER_REFUSED_MESSAGE`.
2022
+ *
2023
+ * Separate from `budgetExhaustedMessage` because the two conditions are separate: ours is a spend
2024
+ * cap a host administrator can raise, this is the vendor declining to serve us at all. A host that
2025
+ * white-labels one will usually want to white-label both, but conflating them into one field would
2026
+ * force identical copy on two situations with different remedies.
2027
+ *
2028
+ * Rarely needed: unlike the budget default, the shipped sentence names no vendor and no Genesis, so
2029
+ * it is already safe for a white-labelled deployment.
2030
+ */
2031
+ providerRefusedMessage?: string;
1983
2032
  }
1984
2033
 
1985
2034
  export { ChatFallback }
@@ -2314,9 +2363,9 @@ export declare interface FallbackAgentConfig extends BaseAgentConfig {
2314
2363
  *
2315
2364
  * @internal
2316
2365
  */
2317
- export declare function formatBlockedReason(budget?: Extract<ChatDriverResult, {
2366
+ export declare function formatBlockedReason(budget?: Partial<Extract<ChatDriverResult, {
2318
2367
  reason: 'done';
2319
- }>['budget'], vendor?: AIProviderType): string | undefined;
2368
+ }>['budget']>, vendor?: AIProviderType): string | undefined;
2320
2369
 
2321
2370
  /**
2322
2371
  * Foundation AI Assistant component.
@@ -2778,11 +2827,18 @@ export declare class FoundationAiAssistant extends GenesisElement {
2778
2827
  */
2779
2828
  private get relevantBlockedVendors();
2780
2829
  /**
2781
- * Whether the blocked banner has anything to say — a vendor-agnostic block, OR
2782
- * at least one walled vendor the registry can still reach. Broader than
2783
- * {@link FoundationAiAssistant.blocked} on purpose: partial exhaustion leaves
2784
- * the composer live but still needs to be announced, because the next turn may
2785
- * route to the walled vendor and fail.
2830
+ * Whether the blocked banner has anything to say — a vendor-agnostic block, or
2831
+ * a partial wall that the NEXT send would actually hit.
2832
+ *
2833
+ * Partial exhaustion is announced only while the ACTIVE vendor (the registry
2834
+ * default the next send resolves to) is among the walled ones. A user who has
2835
+ * switched to a vendor with headroom has already acted on the advice —
2836
+ * keeping the banner up past that point nagged about a vendor they just left
2837
+ * (GENC-1464 tester feedback), while switching BACK to the walled vendor
2838
+ * brings the banner straight back. Two conservative fallbacks keep it
2839
+ * fail-informative: while the statuses are still loading the active vendor is
2840
+ * unknowable, so any reachable-and-walled vendor still announces; and a
2841
+ * vendor-agnostic block announces always, exactly as before.
2786
2842
  *
2787
2843
  * Reads the reachability-filtered list for the same reason the copy does — a
2788
2844
  * host that has swapped its registry away from the walled vendor has nothing
@@ -2809,6 +2865,15 @@ export declare class FoundationAiAssistant extends GenesisElement {
2809
2865
  * @internal
2810
2866
  */
2811
2867
  get suggestionsBlocked(): boolean;
2868
+ /**
2869
+ * The vendor the NEXT send resolves to — the registry default's provider, as
2870
+ * reported through `providerStatuses`. `undefined` while the statuses are
2871
+ * still loading (or when the default reports the `'none'` sentinel), which
2872
+ * every consumer treats conservatively.
2873
+ *
2874
+ * @internal
2875
+ */
2876
+ private get activeVendor();
2812
2877
  /**
2813
2878
  * Explanation shown in the blocked banner, or `null` for the element's default
2814
2879
  * copy. Only meaningful while {@link FoundationAiAssistant.blocked} is true;
@@ -2957,12 +3022,14 @@ export declare class FoundationAiAssistant extends GenesisElement {
2957
3022
  * vendors watched all of it vanish the moment a second one walled — replaced by
2958
3023
  * generic copy, at the exact moment the situation got worse.
2959
3024
  *
2960
- * The compact "AI usage limits are reached for A and B." list is kept for the
2961
- * case it was written for: NO walled vendor carries copy, so a statement per
2962
- * vendor would just repeat one boilerplate sentence per name. As soon as any of
2963
- * them does carry copy, the branch lists one statement per vendor instead —
2964
- * falling back to that same boilerplate for the ones that have none, so the
2965
- * sentence set stays complete rather than silently naming a subset.
3025
+ * The compact list is the DEFAULT shape for several walls, now carrying each
3026
+ * vendor's figures: "AI usage limits are reached for Gemini ($62.34 of $50.00)
3027
+ * and Anthropic ($31.10 of $30.00)." A driver latch stores no copy of its own
3028
+ * (it stores the figures, which this composes from), so that shape is what the
3029
+ * real budget flow produces. As soon as any walled vendor carries HOST copy,
3030
+ * which cannot be folded into a list item, the branch lists one statement per
3031
+ * vendor instead — falling back to composed copy for the ones that have none, so
3032
+ * the sentence set stays complete rather than silently naming a subset.
2966
3033
  *
2967
3034
  * @internal
2968
3035
  */
@@ -26,6 +26,7 @@ __export(chat_driver_node_exports, {
26
26
  AnthropicProvider: () => AnthropicProvider,
27
27
  AnthropicTransport: () => AnthropicTransport,
28
28
  ChatDriver: () => ChatDriver,
29
+ DEFAULT_PROVIDER_REFUSED_MESSAGE: () => DEFAULT_PROVIDER_REFUSED_MESSAGE,
29
30
  GEMINI_CACHED_INPUT_MULTIPLIER: () => GEMINI_CACHED_INPUT_MULTIPLIER,
30
31
  GEMINI_LONG_CONTEXT_THRESHOLD: () => GEMINI_LONG_CONTEXT_THRESHOLD,
31
32
  GeminiProvider: () => GeminiProvider,
@@ -33,6 +34,8 @@ __export(chat_driver_node_exports, {
33
34
  MutableAIProviderRegistry: () => MutableAIProviderRegistry,
34
35
  NOOP_ACTIVITY_BUS: () => NOOP_ACTIVITY_BUS,
35
36
  OrchestratingDriver: () => OrchestratingDriver,
37
+ PROVIDER_REFUSED_CODE: () => PROVIDER_REFUSED_CODE,
38
+ ProviderRefusedError: () => ProviderRefusedError,
36
39
  REQUEST_CONTINUATION_TOOL: () => REQUEST_CONTINUATION_TOOL,
37
40
  SUPPORTED_ANTHROPIC_MODEL_IDS: () => SUPPORTED_ANTHROPIC_MODEL_IDS,
38
41
  SUPPORTED_GEMINI_MODEL_IDS: () => SUPPORTED_GEMINI_MODEL_IDS,
@@ -1105,6 +1108,78 @@ function budgetExhaustedFrom(vendorLabel, payload, fallbackDetail) {
1105
1108
  });
1106
1109
  }
1107
1110
 
1111
+ // ../../foundation-ai/dist/esm/transports/provider-refused.js
1112
+ var PROVIDER_REFUSED_CODE = "PROVIDER_REFUSED";
1113
+ var DEFAULT_PROVIDER_REFUSED_MESSAGE = "AI requests are currently being refused by the provider. This isn't related to your account or your usage and needs no action from you \u2014 please contact your support team so it can be restored.";
1114
+ var ProviderRefusedError = class extends Error {
1115
+ constructor(vendorLabel, kind, upstreamStatus, upstreamType, detail) {
1116
+ super(`${vendorLabel} refused the request (${kind})` + (upstreamStatus != null ? ` \u2014 HTTP ${upstreamStatus}` : "") + (upstreamType ? ` ${upstreamType}` : "") + (detail ? `: ${detail}` : "") + ". Retrying will not clear this \u2014 the provider account must be fixed.");
1117
+ this.vendorLabel = vendorLabel;
1118
+ this.kind = kind;
1119
+ this.upstreamStatus = upstreamStatus;
1120
+ this.upstreamType = upstreamType;
1121
+ this.detail = detail;
1122
+ this.name = "ProviderRefusedError";
1123
+ }
1124
+ };
1125
+ var ANTHROPIC_SPEND_PHRASES = Object.freeze([
1126
+ /credit balance/i,
1127
+ /usage limit/i,
1128
+ /spend limit/i,
1129
+ /insufficient (credit|funds|balance)/i
1130
+ ]);
1131
+ var ANTHROPIC_AUTH_TYPES = Object.freeze([
1132
+ "authentication_error",
1133
+ "permission_error"
1134
+ ]);
1135
+ function anthropicErrorBody(payload) {
1136
+ var _a;
1137
+ const outer = typeof payload === "object" && payload !== null ? payload : {};
1138
+ for (const candidate of [
1139
+ outer.error,
1140
+ (_a = outer.details) === null || _a === void 0 ? void 0 : _a.error
1141
+ ]) {
1142
+ if (typeof candidate === "object" && candidate !== null) {
1143
+ const { type, message } = candidate;
1144
+ return {
1145
+ type: typeof type === "string" ? type : void 0,
1146
+ message: typeof message === "string" ? message : void 0
1147
+ };
1148
+ }
1149
+ }
1150
+ return void 0;
1151
+ }
1152
+ function providerRefusalOf(vendor, code, payload) {
1153
+ if (code === PROVIDER_REFUSED_CODE) {
1154
+ const kind = payload === null || payload === void 0 ? void 0 : payload.kind;
1155
+ return kind === "auth" ? "auth" : "spend";
1156
+ }
1157
+ if (vendor !== "anthropic")
1158
+ return void 0;
1159
+ const body = anthropicErrorBody(payload);
1160
+ if (!(body === null || body === void 0 ? void 0 : body.type))
1161
+ return void 0;
1162
+ if (ANTHROPIC_AUTH_TYPES.includes(body.type))
1163
+ return "auth";
1164
+ if (body.type === "billing_error")
1165
+ return "spend";
1166
+ if (body.type === "invalid_request_error" && body.message) {
1167
+ const message = body.message;
1168
+ if (ANTHROPIC_SPEND_PHRASES.some((phrase) => phrase.test(message)))
1169
+ return "spend";
1170
+ }
1171
+ return void 0;
1172
+ }
1173
+ function providerRefusedFrom(vendorLabel, kind, upstreamStatus, payload, fallbackDetail) {
1174
+ var _a;
1175
+ const body = anthropicErrorBody(payload);
1176
+ return new ProviderRefusedError(vendorLabel, kind, upstreamStatus, body === null || body === void 0 ? void 0 : body.type, (_a = body === null || body === void 0 ? void 0 : body.message) !== null && _a !== void 0 ? _a : fallbackDetail);
1177
+ }
1178
+ function vendorOfTransportLabel(vendorLabel) {
1179
+ const entry = Object.entries(VENDOR_LABELS).find(([, label]) => label === vendorLabel);
1180
+ return entry === null || entry === void 0 ? void 0 : entry[0];
1181
+ }
1182
+
1108
1183
  // ../../foundation-ai/dist/esm/types/config.types.js
1109
1184
  var SUPPORTED_GEMINI_MODEL_IDS = [
1110
1185
  "gemini-2.5-pro",
@@ -1523,13 +1598,70 @@ function readFramedBody(stream, onChunk, vendorLabel, stallTimeout) {
1523
1598
  });
1524
1599
  }
1525
1600
 
1601
+ // ../../foundation-ai/dist/esm/transports/retry-hints.js
1602
+ var MAX_SERVER_REQUESTED_BACKOFF_MS = 6e4;
1603
+ var SHOULD_RETRY_HEADER = "x-should-retry";
1604
+ var MS_PER_SECOND = 1e3;
1605
+ function shouldNotRetry(headers) {
1606
+ var _a;
1607
+ const value = (_a = headers === null || headers === void 0 ? void 0 : headers.get) === null || _a === void 0 ? void 0 : _a.call(headers, SHOULD_RETRY_HEADER);
1608
+ return typeof value === "string" && value.trim().toLowerCase() === "false";
1609
+ }
1610
+ function retryAfterMs(headers) {
1611
+ var _a;
1612
+ const raw = (_a = headers === null || headers === void 0 ? void 0 : headers.get) === null || _a === void 0 ? void 0 : _a.call(headers, "retry-after");
1613
+ if (typeof raw !== "string" || raw.trim() === "")
1614
+ return void 0;
1615
+ const seconds = Number(raw.trim());
1616
+ if (Number.isFinite(seconds))
1617
+ return Math.max(0, seconds * MS_PER_SECOND);
1618
+ const at = Date.parse(raw);
1619
+ if (Number.isNaN(at))
1620
+ return void 0;
1621
+ return Math.max(0, at - Date.now());
1622
+ }
1623
+ function retryInfoMs(payload) {
1624
+ const outer = typeof payload === "object" && payload !== null ? payload : {};
1625
+ const framed = typeof outer.details === "object" && outer.details !== null ? outer.details : void 0;
1626
+ for (const holder of [outer.error, framed === null || framed === void 0 ? void 0 : framed.error]) {
1627
+ const details = holder === null || holder === void 0 ? void 0 : holder.details;
1628
+ if (!Array.isArray(details))
1629
+ continue;
1630
+ for (const entry of details) {
1631
+ const type = entry === null || entry === void 0 ? void 0 : entry["@type"];
1632
+ if (typeof type !== "string" || !type.endsWith("google.rpc.RetryInfo"))
1633
+ continue;
1634
+ const delay = entry.retryDelay;
1635
+ if (typeof delay !== "string")
1636
+ continue;
1637
+ const seconds = Number(delay.replace(/s$/, ""));
1638
+ if (Number.isFinite(seconds) && seconds >= 0)
1639
+ return seconds * MS_PER_SECOND;
1640
+ }
1641
+ }
1642
+ return void 0;
1643
+ }
1644
+ function resolveBackoffMs(attempt, baseMs, serverMs) {
1645
+ const ladder = baseMs * Math.pow(2, attempt);
1646
+ if (serverMs == null || !Number.isFinite(serverMs))
1647
+ return ladder;
1648
+ return Math.max(ladder, Math.min(serverMs, MAX_SERVER_REQUESTED_BACKOFF_MS));
1649
+ }
1650
+
1526
1651
  // ../../foundation-ai/dist/esm/transports/post-with-retry.js
1527
1652
  var MAX_RETRIES = 5;
1528
1653
  function postWithRetry(options) {
1529
1654
  return __awaiter(this, void 0, void 0, function* () {
1530
- var _a, _b, _c, _d, _e, _f, _g;
1655
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1531
1656
  const { url, headers, body, credentials, vendorLabel, retryableStatuses, timeout, stallTimeout, backoffBaseMs, signal } = options;
1532
- const backoff = (attempt) => abortableDelay(backoffBaseMs * (1 << attempt), signal);
1657
+ const vendor = vendorOfTransportLabel(vendorLabel);
1658
+ const backoff = (attempt, serverMs) => {
1659
+ const waitMs = resolveBackoffMs(attempt, backoffBaseMs, serverMs);
1660
+ if (serverMs != null && waitMs > backoffBaseMs * Math.pow(2, attempt)) {
1661
+ logger.warn(`${vendorLabel}Transport: honouring the server's requested retry delay (${Math.round(waitMs)}ms, asked ${Math.round(serverMs)}ms)`);
1662
+ }
1663
+ return abortableDelay(waitMs, signal);
1664
+ };
1533
1665
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
1534
1666
  const timeoutController = new AbortController();
1535
1667
  const startedAt = performance.now();
@@ -1563,24 +1695,33 @@ function postWithRetry(options) {
1563
1695
  if (!response.ok) {
1564
1696
  const errText = yield response.text();
1565
1697
  const parsed = parseJsonOrUndefined(errText);
1698
+ const refusal = providerRefusalOf(vendor, codeOf(parsed), parsed);
1699
+ if (refusal) {
1700
+ throw providerRefusedFrom(vendorLabel, refusal, response.status, parsed, errText || void 0);
1701
+ }
1566
1702
  if (isBudgetRejection(response.status, codeOf(parsed))) {
1567
1703
  throw budgetExhaustedFrom(vendorLabel, parsed, errText || void 0);
1568
1704
  }
1569
- if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
1705
+ const refusedRetry = shouldNotRetry(response.headers);
1706
+ if (retryableStatuses.includes(response.status) && !refusedRetry && attempt < MAX_RETRIES) {
1570
1707
  logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1571
1708
  clearTimeout(timeoutId);
1572
- yield backoff(attempt);
1709
+ yield backoff(attempt, (_d = retryAfterMs(response.headers)) !== null && _d !== void 0 ? _d : retryInfoMs(parsed));
1573
1710
  continue;
1574
1711
  }
1575
1712
  throw new Error(`${vendorLabel} request error ${response.status}: ${errText}`);
1576
1713
  }
1577
- if (((_e = (_d = response.headers) === null || _d === void 0 ? void 0 : _d.get("content-type")) === null || _e === void 0 ? void 0 : _e.toLowerCase().includes("application/x-ndjson")) && response.body) {
1714
+ if (((_f = (_e = response.headers) === null || _e === void 0 ? void 0 : _e.get("content-type")) === null || _f === void 0 ? void 0 : _f.toLowerCase().includes("application/x-ndjson")) && response.body) {
1578
1715
  rearmStallTimer();
1579
1716
  const frame = yield readFramedBody(response.body, rearmStallTimer, vendorLabel, stallTimeout);
1580
1717
  if (frame.t === "ok")
1581
1718
  return frame.body;
1582
1719
  if (frame.code === "UPSTREAM_STALLED") {
1583
- throw new DOMException(`${vendorLabel} request stalled: ${(_f = frame.error) !== null && _f !== void 0 ? _f : "no upstream data"}`, "TimeoutError");
1720
+ throw new DOMException(`${vendorLabel} request stalled: ${(_g = frame.error) !== null && _g !== void 0 ? _g : "no upstream data"}`, "TimeoutError");
1721
+ }
1722
+ const framedRefusal = providerRefusalOf(vendor, frame.code, frame);
1723
+ if (framedRefusal) {
1724
+ throw providerRefusedFrom(vendorLabel, framedRefusal, frame.status, frame, frame.error);
1584
1725
  }
1585
1726
  if (isBudgetRejection(frame.status, frame.code)) {
1586
1727
  throw budgetExhaustedFrom(vendorLabel, frame);
@@ -1588,10 +1729,10 @@ function postWithRetry(options) {
1588
1729
  if (retryableStatuses.includes(frame.status) && attempt < MAX_RETRIES) {
1589
1730
  logger.warn(`${vendorLabel}Transport: retryable status ${frame.status} (err frame), retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1590
1731
  clearTimeout(timeoutId);
1591
- yield backoff(attempt);
1732
+ yield backoff(attempt, retryInfoMs(frame));
1592
1733
  continue;
1593
1734
  }
1594
- throw new Error(`${vendorLabel} request error ${frame.status}: ${(_g = frame.error) !== null && _g !== void 0 ? _g : JSON.stringify(frame.details)}`);
1735
+ throw new Error(`${vendorLabel} request error ${frame.status}: ${(_h = frame.error) !== null && _h !== void 0 ? _h : JSON.stringify(frame.details)}`);
1595
1736
  }
1596
1737
  return yield response.json();
1597
1738
  } catch (e) {
@@ -3671,6 +3812,12 @@ function accumulate(messages, into) {
3671
3812
  var TOOL_FOLD_SYMBOL = Symbol("toolFold");
3672
3813
 
3673
3814
  // src/components/chat-driver/chat-driver.ts
3815
+ var providerRefusedDetailOf = (e) => ({
3816
+ vendorLabel: e.vendorLabel,
3817
+ kind: e.kind,
3818
+ ...e.upstreamStatus != null ? { upstreamStatus: e.upstreamStatus } : {},
3819
+ ...e.upstreamType ? { upstreamType: e.upstreamType } : {}
3820
+ });
3674
3821
  var budgetDetailOf = (e) => {
3675
3822
  const vendor = vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor);
3676
3823
  if (e.budgetUsd == null && e.spentUsd == null && !vendor) return void 0;
@@ -3866,6 +4013,16 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3866
4013
  * picked up on the next turn.
3867
4014
  */
3868
4015
  this.resolvedStatusCache = /* @__PURE__ */ new Map();
4016
+ /**
4017
+ * A provider refusal was observed this turn, so the tool loop must not call the model again
4018
+ * (GENC-1506).
4019
+ *
4020
+ * Exists for the SUB-AGENT path only, exactly like `budgetExhaustedThisTurn`: this driver's own
4021
+ * refusal returns straight out of the catch, whereas a child's refusal reaches the parent as a tool
4022
+ * result, and without this flag the loop would issue another call into the same wall — N batched
4023
+ * children costing N doomed calls plus a doomed parent one.
4024
+ */
4025
+ this.providerRefusedThisTurn = false;
3869
4026
  /**
3870
4027
  * Set the moment a budget wall is observed anywhere in this turn — this
3871
4028
  * driver's own 402, or a sub-agent's (which surfaces here only as a
@@ -3893,13 +4050,15 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3893
4050
  maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
3894
4051
  sessionKey = "",
3895
4052
  activityBus = NOOP_ACTIVITY_BUS,
3896
- budgetExhaustedMessage
4053
+ budgetExhaustedMessage,
4054
+ providerRefusedMessage
3897
4055
  } = config;
3898
4056
  this.maxToolIterations = maxToolIterations;
3899
4057
  this.condenseBatchCalls = condenseBatchCalls;
3900
4058
  this.sessionKey = sessionKey;
3901
4059
  this.activityBus = activityBus;
3902
4060
  this.budgetExhaustedMessageOverride = budgetExhaustedMessage;
4061
+ this.providerRefusedMessageOverride = providerRefusedMessage;
3903
4062
  if (typeof toolHandlers === "function") {
3904
4063
  this.toolHandlersFactory = toolHandlers;
3905
4064
  this.toolHandlers = {};
@@ -4006,9 +4165,14 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4006
4165
  * (rather than set to `undefined`) so a happy-path result stays byte-identical to
4007
4166
  * the historical `{ reason: 'done' }`.
4008
4167
  */
4009
- turnDone(failureReason, budget) {
4168
+ turnDone(failureReason, budget, providerRefused) {
4010
4169
  if (!failureReason) return { reason: "done" };
4011
- return budget ? { reason: "done", failureReason, budget } : { reason: "done", failureReason };
4170
+ return {
4171
+ reason: "done",
4172
+ failureReason,
4173
+ ...budget ? { budget } : {},
4174
+ ...providerRefused ? { providerRefused } : {}
4175
+ };
4012
4176
  }
4013
4177
  /**
4014
4178
  * Terminal budget outcome for a wall hit **outside** the tool loop — today,
@@ -4063,6 +4227,42 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4063
4227
  this.appendToHistory({ role: "assistant", content: this.budgetExhaustedBubble(e) });
4064
4228
  return this.turnDone("budget-exhausted", budgetDetailOf(e));
4065
4229
  }
4230
+ /**
4231
+ * The sentence shown when the upstream provider refuses (GENC-1506).
4232
+ *
4233
+ * Deliberately far simpler than `budgetExhaustedBubble`: no vendor name, no figures, no
4234
+ * switch-provider advice, and no branching on `kind`. The copy is cause-free by design — a user who
4235
+ * can see their own remaining spend must not be told about a limit, and we must not imply the bill
4236
+ * has gone unpaid — so there is nothing here to compose. A host override wins verbatim.
4237
+ */
4238
+ providerRefusedBubble() {
4239
+ return this.providerRefusedMessageOverride ?? DEFAULT_PROVIDER_REFUSED_MESSAGE;
4240
+ }
4241
+ /**
4242
+ * Terminal provider-refusal outcome. Mirrors `reportBudgetExhausted` so the two walls behave
4243
+ * identically from the caller's side, while keeping their diagnostics distinct.
4244
+ *
4245
+ * `kind` reaches the debug log and the result but never the transcript: it is what lets an operator
4246
+ * tell "top up the account" from "rotate the key", and with cause-free user copy this is the only
4247
+ * place that distinction survives.
4248
+ */
4249
+ reportProviderRefused(e) {
4250
+ const detail = providerRefusedDetailOf(e);
4251
+ this.providerRefusedThisTurn = true;
4252
+ this.providerRefusedDetail = detail;
4253
+ logger2.error("ChatDriver: provider refused the request", e);
4254
+ recordTurnError(this.sessionKey, "provider-refused", {
4255
+ agent: this.activeAgentName,
4256
+ provider: this.lastResolvedProviderName,
4257
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? this.lastResolvedProvider,
4258
+ kind: e.kind,
4259
+ upstreamStatus: e.upstreamStatus,
4260
+ upstreamType: e.upstreamType,
4261
+ isSubAgent: this.isSubAgent
4262
+ });
4263
+ this.appendToHistory({ role: "assistant", content: this.providerRefusedBubble() });
4264
+ return this.turnDone("provider-refused", void 0, detail);
4265
+ }
4066
4266
  /** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
4067
4267
  static failureReasonOf(result) {
4068
4268
  return result.reason === "done" ? result.failureReason : void 0;
@@ -4747,6 +4947,8 @@ Output format (strict):
4747
4947
  this.budgetExhaustedThisTurn = false;
4748
4948
  this.budgetWallDetail = void 0;
4749
4949
  this.budgetWallViaSubAgent = false;
4950
+ this.providerRefusedThisTurn = false;
4951
+ this.providerRefusedDetail = void 0;
4750
4952
  this.appendToHistory({ role: "user", content: userInput, attachments });
4751
4953
  this.turnStartedAt = Date.now();
4752
4954
  recordMetaEvent(this.sessionKey, "turn.start", {
@@ -5006,6 +5208,8 @@ Output format (strict):
5006
5208
  this.budgetExhaustedThisTurn = false;
5007
5209
  this.budgetWallDetail = void 0;
5008
5210
  this.budgetWallViaSubAgent = false;
5211
+ this.providerRefusedThisTurn = false;
5212
+ this.providerRefusedDetail = void 0;
5009
5213
  this.turnStartedAt = Date.now();
5010
5214
  recordMetaEvent(this.sessionKey, "turn.start", {
5011
5215
  phase: "continueFromHistory",
@@ -5195,6 +5399,22 @@ Output format (strict):
5195
5399
  if (this.turnController.signal.aborted) {
5196
5400
  return this.completeAbortedTurn();
5197
5401
  }
5402
+ if (this.providerRefusedThisTurn) {
5403
+ logger2.error("ChatDriver: ending the turn \u2014 a sub-agent hit the provider wall");
5404
+ recordTurnError(this.sessionKey, "provider-refused", {
5405
+ agent: this.activeAgentName,
5406
+ provider: this.lastResolvedProviderName,
5407
+ kind: this.providerRefusedDetail?.kind,
5408
+ via: "sub-agent",
5409
+ isSubAgent: this.isSubAgent
5410
+ });
5411
+ if (this.isSubAgent) {
5412
+ this.failSubAgent("provider_refused");
5413
+ } else {
5414
+ this.appendToHistory({ role: "assistant", content: this.providerRefusedBubble() });
5415
+ }
5416
+ return this.turnDone("provider-refused", void 0, this.providerRefusedDetail);
5417
+ }
5198
5418
  if (this.budgetExhaustedThisTurn) {
5199
5419
  logger2.error("ChatDriver: ending the turn \u2014 a sub-agent hit the AI budget wall");
5200
5420
  recordTurnError(this.sessionKey, "budget-exhausted", {
@@ -5422,6 +5642,25 @@ ${tailBody}
5422
5642
  }
5423
5643
  return this.turnDone("response-truncated");
5424
5644
  }
5645
+ if (e instanceof ProviderRefusedError) {
5646
+ if (this.isSubAgent) {
5647
+ logger2.error("ChatDriver: provider refused the request", e);
5648
+ recordTurnError(this.sessionKey, "provider-refused", {
5649
+ agent: this.activeAgentName,
5650
+ provider: this.lastResolvedProviderName,
5651
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? this.lastResolvedProvider,
5652
+ kind: e.kind,
5653
+ upstreamStatus: e.upstreamStatus,
5654
+ upstreamType: e.upstreamType,
5655
+ isSubAgent: true
5656
+ });
5657
+ this.providerRefusedThisTurn = true;
5658
+ this.providerRefusedDetail = providerRefusedDetailOf(e);
5659
+ this.failSubAgent("provider_refused");
5660
+ return this.turnDone("provider-refused", void 0, this.providerRefusedDetail);
5661
+ }
5662
+ return this.reportProviderRefused(e);
5663
+ }
5425
5664
  if (e instanceof BudgetExhaustedError) {
5426
5665
  this.budgetExhaustedThisTurn = true;
5427
5666
  this.budgetWallDetail = budgetDetailOf(e);
@@ -6779,6 +7018,7 @@ function withFreshMetaSnapshot(stored, fresh) {
6779
7018
  AnthropicProvider,
6780
7019
  AnthropicTransport,
6781
7020
  ChatDriver,
7021
+ DEFAULT_PROVIDER_REFUSED_MESSAGE,
6782
7022
  GEMINI_CACHED_INPUT_MULTIPLIER,
6783
7023
  GEMINI_LONG_CONTEXT_THRESHOLD,
6784
7024
  GeminiProvider,
@@ -6786,6 +7026,8 @@ function withFreshMetaSnapshot(stored, fresh) {
6786
7026
  MutableAIProviderRegistry,
6787
7027
  NOOP_ACTIVITY_BUS,
6788
7028
  OrchestratingDriver,
7029
+ PROVIDER_REFUSED_CODE,
7030
+ ProviderRefusedError,
6789
7031
  REQUEST_CONTINUATION_TOOL,
6790
7032
  SUPPORTED_ANTHROPIC_MODEL_IDS,
6791
7033
  SUPPORTED_GEMINI_MODEL_IDS,