@genesislcap/ai-assistant 15.14.0 → 15.14.2

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.
@@ -4963,6 +4963,15 @@
4963
4963
  "text": "BudgetDetail",
4964
4964
  "canonicalReference": "@genesislcap/ai-assistant!~BudgetDetail:type"
4965
4965
  },
4966
+ {
4967
+ "kind": "Content",
4968
+ "text": ";\n providerRefused?: "
4969
+ },
4970
+ {
4971
+ "kind": "Reference",
4972
+ "text": "ProviderRefusedDetail",
4973
+ "canonicalReference": "@genesislcap/ai-assistant!~ProviderRefusedDetail:type"
4974
+ },
4966
4975
  {
4967
4976
  "kind": "Content",
4968
4977
  "text": ";\n } | undefined"
@@ -4975,7 +4984,7 @@
4975
4984
  "isStatic": false,
4976
4985
  "returnTypeTokenRange": {
4977
4986
  "startIndex": 1,
4978
- "endIndex": 6
4987
+ "endIndex": 8
4979
4988
  },
4980
4989
  "releaseTag": "Beta",
4981
4990
  "isProtected": false,
@@ -5923,6 +5932,33 @@
5923
5932
  "endIndex": 3
5924
5933
  }
5925
5934
  },
5935
+ {
5936
+ "kind": "PropertySignature",
5937
+ "canonicalReference": "@genesislcap/ai-assistant!ChatDriverConfig#providerRefusedMessage:member",
5938
+ "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",
5939
+ "excerptTokens": [
5940
+ {
5941
+ "kind": "Content",
5942
+ "text": "providerRefusedMessage?: "
5943
+ },
5944
+ {
5945
+ "kind": "Content",
5946
+ "text": "string"
5947
+ },
5948
+ {
5949
+ "kind": "Content",
5950
+ "text": ";"
5951
+ }
5952
+ ],
5953
+ "isReadonly": false,
5954
+ "isOptional": true,
5955
+ "releaseTag": "Beta",
5956
+ "name": "providerRefusedMessage",
5957
+ "propertyTypeTokenRange": {
5958
+ "startIndex": 1,
5959
+ "endIndex": 2
5960
+ }
5961
+ },
5926
5962
  {
5927
5963
  "kind": "PropertySignature",
5928
5964
  "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
  /**
@@ -1712,6 +1748,7 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
1712
1748
  getSubAgentFailure(): {
1713
1749
  reason: SubAgentFailureReason;
1714
1750
  budget?: BudgetDetail;
1751
+ providerRefused?: ProviderRefusedDetail;
1715
1752
  } | undefined;
1716
1753
  /**
1717
1754
  * Record a sub-agent failure reason (first one wins). No-op for top-level
@@ -1980,6 +2017,19 @@ export declare interface ChatDriverConfig {
1980
2017
  * reversible change.
1981
2018
  */
1982
2019
  budgetExhaustedMessage?: string;
2020
+ /**
2021
+ * Overrides the transcript sentence shown when the upstream PROVIDER refuses the account
2022
+ * (GENC-1506) — see `DEFAULT_PROVIDER_REFUSED_MESSAGE`.
2023
+ *
2024
+ * Separate from `budgetExhaustedMessage` because the two conditions are separate: ours is a spend
2025
+ * cap a host administrator can raise, this is the vendor declining to serve us at all. A host that
2026
+ * white-labels one will usually want to white-label both, but conflating them into one field would
2027
+ * force identical copy on two situations with different remedies.
2028
+ *
2029
+ * Rarely needed: unlike the budget default, the shipped sentence names no vendor and no Genesis, so
2030
+ * it is already safe for a white-labelled deployment.
2031
+ */
2032
+ providerRefusedMessage?: string;
1983
2033
  }
1984
2034
 
1985
2035
  export { ChatFallback }
@@ -4410,6 +4460,11 @@ export declare type PopoutMode = 'expand' | 'collapse';
4410
4460
  */
4411
4461
  export declare type ProviderInput = string | ((ctx: SystemPromptContext) => string | Promise<string>);
4412
4462
 
4463
+ /** The `providerRefused` payload on a `'provider-refused'` {@link ChatDriverResult} (GENC-1506). */
4464
+ declare type ProviderRefusedDetail = NonNullable<Extract<ChatDriverResult, {
4465
+ reason: 'done';
4466
+ }>['providerRefused']>;
4467
+
4413
4468
  /** Name reserved for the cross-agent handoff tool — injected by OrchestratingDriver. */
4414
4469
  export declare const REQUEST_CONTINUATION_TOOL = "request_continuation";
4415
4470
 
@@ -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,94 @@ 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 statedKind(payload) {
1153
+ const outer = typeof payload === "object" && payload !== null ? payload : {};
1154
+ const nested = typeof outer.details === "object" && outer.details !== null ? outer.details : {};
1155
+ for (const candidate of [outer.kind, nested.kind]) {
1156
+ if (candidate === "auth" || candidate === "spend")
1157
+ return candidate;
1158
+ }
1159
+ return void 0;
1160
+ }
1161
+ function kindFromEnvelopeType(payload) {
1162
+ const body = anthropicErrorBody(payload);
1163
+ if (!(body === null || body === void 0 ? void 0 : body.type))
1164
+ return void 0;
1165
+ if (ANTHROPIC_AUTH_TYPES.includes(body.type))
1166
+ return "auth";
1167
+ if (body.type === "billing_error")
1168
+ return "spend";
1169
+ return void 0;
1170
+ }
1171
+ function providerRefusalOf(vendor, code, payload) {
1172
+ var _a, _b;
1173
+ if (code === PROVIDER_REFUSED_CODE) {
1174
+ return (_b = (_a = statedKind(payload)) !== null && _a !== void 0 ? _a : kindFromEnvelopeType(payload)) !== null && _b !== void 0 ? _b : "spend";
1175
+ }
1176
+ if (vendor !== "anthropic")
1177
+ return void 0;
1178
+ const structural = kindFromEnvelopeType(payload);
1179
+ if (structural)
1180
+ return structural;
1181
+ const body = anthropicErrorBody(payload);
1182
+ if ((body === null || body === void 0 ? void 0 : body.type) === "invalid_request_error" && body.message) {
1183
+ const message = body.message;
1184
+ if (ANTHROPIC_SPEND_PHRASES.some((phrase) => phrase.test(message)))
1185
+ return "spend";
1186
+ }
1187
+ return void 0;
1188
+ }
1189
+ function providerRefusedFrom(vendorLabel, kind, upstreamStatus, payload, fallbackDetail) {
1190
+ var _a;
1191
+ const body = anthropicErrorBody(payload);
1192
+ 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);
1193
+ }
1194
+ function vendorOfTransportLabel(vendorLabel) {
1195
+ const entry = Object.entries(VENDOR_LABELS).find(([, label]) => label === vendorLabel);
1196
+ return entry === null || entry === void 0 ? void 0 : entry[0];
1197
+ }
1198
+
1108
1199
  // ../../foundation-ai/dist/esm/types/config.types.js
1109
1200
  var SUPPORTED_GEMINI_MODEL_IDS = [
1110
1201
  "gemini-2.5-pro",
@@ -1523,13 +1614,81 @@ function readFramedBody(stream, onChunk, vendorLabel, stallTimeout) {
1523
1614
  });
1524
1615
  }
1525
1616
 
1617
+ // ../../foundation-ai/dist/esm/transports/retry-hints.js
1618
+ var MAX_SERVER_REQUESTED_BACKOFF_MS = 6e4;
1619
+ var MAX_TOTAL_SERVER_REQUESTED_BACKOFF_MS = 12e4;
1620
+ var SHOULD_RETRY_HEADER = "x-should-retry";
1621
+ var MS_PER_SECOND = 1e3;
1622
+ function shouldNotRetry(headers) {
1623
+ var _a;
1624
+ const value = (_a = headers === null || headers === void 0 ? void 0 : headers.get) === null || _a === void 0 ? void 0 : _a.call(headers, SHOULD_RETRY_HEADER);
1625
+ return typeof value === "string" && value.trim().toLowerCase() === "false";
1626
+ }
1627
+ function retryAfterMs(headers) {
1628
+ var _a;
1629
+ const raw = (_a = headers === null || headers === void 0 ? void 0 : headers.get) === null || _a === void 0 ? void 0 : _a.call(headers, "retry-after");
1630
+ if (typeof raw !== "string" || raw.trim() === "")
1631
+ return void 0;
1632
+ const seconds = Number(raw.trim());
1633
+ if (Number.isFinite(seconds))
1634
+ return Math.max(0, seconds * MS_PER_SECOND);
1635
+ const at = Date.parse(raw);
1636
+ if (Number.isNaN(at))
1637
+ return void 0;
1638
+ return Math.max(0, at - Date.now());
1639
+ }
1640
+ function retryInfoMs(payload) {
1641
+ const outer = typeof payload === "object" && payload !== null ? payload : {};
1642
+ const framed = typeof outer.details === "object" && outer.details !== null ? outer.details : void 0;
1643
+ for (const holder of [outer.error, framed === null || framed === void 0 ? void 0 : framed.error]) {
1644
+ const details = holder === null || holder === void 0 ? void 0 : holder.details;
1645
+ if (!Array.isArray(details))
1646
+ continue;
1647
+ for (const entry of details) {
1648
+ const type = entry === null || entry === void 0 ? void 0 : entry["@type"];
1649
+ if (typeof type !== "string" || !type.endsWith("google.rpc.RetryInfo"))
1650
+ continue;
1651
+ const delay = entry === null || entry === void 0 ? void 0 : entry.retryDelay;
1652
+ if (typeof delay !== "string")
1653
+ continue;
1654
+ const seconds = Number(delay.replace(/s$/, ""));
1655
+ if (Number.isFinite(seconds) && seconds >= 0)
1656
+ return seconds * MS_PER_SECOND;
1657
+ }
1658
+ }
1659
+ return void 0;
1660
+ }
1661
+ function resolveBackoffMs(attempt, baseMs, serverMs, serverAllowanceMs = Number.POSITIVE_INFINITY) {
1662
+ const ladder = baseMs * Math.pow(2, attempt);
1663
+ if (serverMs == null || !Number.isFinite(serverMs))
1664
+ return ladder;
1665
+ const honoured = Math.min(serverMs, MAX_SERVER_REQUESTED_BACKOFF_MS, Math.max(0, serverAllowanceMs));
1666
+ return Math.max(ladder, honoured);
1667
+ }
1668
+ function serverBackoffSpentMs(resolvedMs, ladderMs) {
1669
+ return Math.max(0, resolvedMs - ladderMs);
1670
+ }
1671
+
1526
1672
  // ../../foundation-ai/dist/esm/transports/post-with-retry.js
1527
1673
  var MAX_RETRIES = 5;
1528
1674
  function postWithRetry(options) {
1529
1675
  return __awaiter(this, void 0, void 0, function* () {
1530
1676
  var _a, _b, _c, _d, _e, _f, _g;
1531
1677
  const { url, headers, body, credentials, vendorLabel, retryableStatuses, timeout, stallTimeout, backoffBaseMs, signal } = options;
1532
- const backoff = (attempt) => abortableDelay(backoffBaseMs * (1 << attempt), signal);
1678
+ const vendor = vendorOfTransportLabel(vendorLabel);
1679
+ let serverBackoffSpent = 0;
1680
+ const backoff = (attempt, serverMs) => {
1681
+ const ladderMs = backoffBaseMs * Math.pow(2, attempt);
1682
+ const allowanceMs = MAX_TOTAL_SERVER_REQUESTED_BACKOFF_MS - serverBackoffSpent;
1683
+ const waitMs = resolveBackoffMs(attempt, backoffBaseMs, serverMs, allowanceMs);
1684
+ serverBackoffSpent += serverBackoffSpentMs(waitMs, ladderMs);
1685
+ if (serverMs != null && waitMs > ladderMs) {
1686
+ logger.warn(`${vendorLabel}Transport: honouring the server's requested retry delay (${Math.round(waitMs)}ms, asked ${Math.round(serverMs)}ms)`);
1687
+ } else if (serverMs != null && serverMs > ladderMs) {
1688
+ logger.warn(`${vendorLabel}Transport: declining the server's requested retry delay (asked ${Math.round(serverMs)}ms; the ${MAX_TOTAL_SERVER_REQUESTED_BACKOFF_MS}ms sequence allowance is spent) \u2014 using the ladder's ${Math.round(waitMs)}ms`);
1689
+ }
1690
+ return abortableDelay(waitMs, signal);
1691
+ };
1533
1692
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
1534
1693
  const timeoutController = new AbortController();
1535
1694
  const startedAt = performance.now();
@@ -1563,13 +1722,18 @@ function postWithRetry(options) {
1563
1722
  if (!response.ok) {
1564
1723
  const errText = yield response.text();
1565
1724
  const parsed = parseJsonOrUndefined(errText);
1725
+ const refusal = providerRefusalOf(vendor, codeOf(parsed), parsed);
1726
+ if (refusal) {
1727
+ throw providerRefusedFrom(vendorLabel, refusal, response.status, parsed, errText || void 0);
1728
+ }
1566
1729
  if (isBudgetRejection(response.status, codeOf(parsed))) {
1567
1730
  throw budgetExhaustedFrom(vendorLabel, parsed, errText || void 0);
1568
1731
  }
1569
- if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
1732
+ const refusedRetry = shouldNotRetry(response.headers);
1733
+ if (retryableStatuses.includes(response.status) && !refusedRetry && attempt < MAX_RETRIES) {
1570
1734
  logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1571
1735
  clearTimeout(timeoutId);
1572
- yield backoff(attempt);
1736
+ yield backoff(attempt, retryAfterMs(response.headers) || retryInfoMs(parsed));
1573
1737
  continue;
1574
1738
  }
1575
1739
  throw new Error(`${vendorLabel} request error ${response.status}: ${errText}`);
@@ -1582,13 +1746,17 @@ function postWithRetry(options) {
1582
1746
  if (frame.code === "UPSTREAM_STALLED") {
1583
1747
  throw new DOMException(`${vendorLabel} request stalled: ${(_f = frame.error) !== null && _f !== void 0 ? _f : "no upstream data"}`, "TimeoutError");
1584
1748
  }
1749
+ const framedRefusal = providerRefusalOf(vendor, frame.code, frame);
1750
+ if (framedRefusal) {
1751
+ throw providerRefusedFrom(vendorLabel, framedRefusal, frame.status, frame, frame.error);
1752
+ }
1585
1753
  if (isBudgetRejection(frame.status, frame.code)) {
1586
1754
  throw budgetExhaustedFrom(vendorLabel, frame);
1587
1755
  }
1588
1756
  if (retryableStatuses.includes(frame.status) && attempt < MAX_RETRIES) {
1589
1757
  logger.warn(`${vendorLabel}Transport: retryable status ${frame.status} (err frame), retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
1590
1758
  clearTimeout(timeoutId);
1591
- yield backoff(attempt);
1759
+ yield backoff(attempt, retryInfoMs(frame));
1592
1760
  continue;
1593
1761
  }
1594
1762
  throw new Error(`${vendorLabel} request error ${frame.status}: ${(_g = frame.error) !== null && _g !== void 0 ? _g : JSON.stringify(frame.details)}`);
@@ -3671,6 +3839,12 @@ function accumulate(messages, into) {
3671
3839
  var TOOL_FOLD_SYMBOL = Symbol("toolFold");
3672
3840
 
3673
3841
  // src/components/chat-driver/chat-driver.ts
3842
+ var providerRefusedDetailOf = (e) => ({
3843
+ vendorLabel: e.vendorLabel,
3844
+ kind: e.kind,
3845
+ ...e.upstreamStatus != null ? { upstreamStatus: e.upstreamStatus } : {},
3846
+ ...e.upstreamType ? { upstreamType: e.upstreamType } : {}
3847
+ });
3674
3848
  var budgetDetailOf = (e) => {
3675
3849
  const vendor = vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor);
3676
3850
  if (e.budgetUsd == null && e.spentUsd == null && !vendor) return void 0;
@@ -3866,6 +4040,16 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3866
4040
  * picked up on the next turn.
3867
4041
  */
3868
4042
  this.resolvedStatusCache = /* @__PURE__ */ new Map();
4043
+ /**
4044
+ * A provider refusal was observed this turn, so the tool loop must not call the model again
4045
+ * (GENC-1506).
4046
+ *
4047
+ * Exists for the SUB-AGENT path only, exactly like `budgetExhaustedThisTurn`: this driver's own
4048
+ * refusal returns straight out of the catch, whereas a child's refusal reaches the parent as a tool
4049
+ * result, and without this flag the loop would issue another call into the same wall — N batched
4050
+ * children costing N doomed calls plus a doomed parent one.
4051
+ */
4052
+ this.providerRefusedThisTurn = false;
3869
4053
  /**
3870
4054
  * Set the moment a budget wall is observed anywhere in this turn — this
3871
4055
  * driver's own 402, or a sub-agent's (which surfaces here only as a
@@ -3893,13 +4077,15 @@ var ChatDriver = class _ChatDriver extends EventTarget {
3893
4077
  maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
3894
4078
  sessionKey = "",
3895
4079
  activityBus = NOOP_ACTIVITY_BUS,
3896
- budgetExhaustedMessage
4080
+ budgetExhaustedMessage,
4081
+ providerRefusedMessage
3897
4082
  } = config;
3898
4083
  this.maxToolIterations = maxToolIterations;
3899
4084
  this.condenseBatchCalls = condenseBatchCalls;
3900
4085
  this.sessionKey = sessionKey;
3901
4086
  this.activityBus = activityBus;
3902
4087
  this.budgetExhaustedMessageOverride = budgetExhaustedMessage;
4088
+ this.providerRefusedMessageOverride = providerRefusedMessage;
3903
4089
  if (typeof toolHandlers === "function") {
3904
4090
  this.toolHandlersFactory = toolHandlers;
3905
4091
  this.toolHandlers = {};
@@ -4006,9 +4192,14 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4006
4192
  * (rather than set to `undefined`) so a happy-path result stays byte-identical to
4007
4193
  * the historical `{ reason: 'done' }`.
4008
4194
  */
4009
- turnDone(failureReason, budget) {
4195
+ turnDone(failureReason, budget, providerRefused) {
4010
4196
  if (!failureReason) return { reason: "done" };
4011
- return budget ? { reason: "done", failureReason, budget } : { reason: "done", failureReason };
4197
+ return {
4198
+ reason: "done",
4199
+ failureReason,
4200
+ ...budget ? { budget } : {},
4201
+ ...providerRefused ? { providerRefused } : {}
4202
+ };
4012
4203
  }
4013
4204
  /**
4014
4205
  * Terminal budget outcome for a wall hit **outside** the tool loop — today,
@@ -4063,6 +4254,42 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4063
4254
  this.appendToHistory({ role: "assistant", content: this.budgetExhaustedBubble(e) });
4064
4255
  return this.turnDone("budget-exhausted", budgetDetailOf(e));
4065
4256
  }
4257
+ /**
4258
+ * The sentence shown when the upstream provider refuses (GENC-1506).
4259
+ *
4260
+ * Deliberately far simpler than `budgetExhaustedBubble`: no vendor name, no figures, no
4261
+ * switch-provider advice, and no branching on `kind`. The copy is cause-free by design — a user who
4262
+ * can see their own remaining spend must not be told about a limit, and we must not imply the bill
4263
+ * has gone unpaid — so there is nothing here to compose. A host override wins verbatim.
4264
+ */
4265
+ providerRefusedBubble() {
4266
+ return this.providerRefusedMessageOverride ?? DEFAULT_PROVIDER_REFUSED_MESSAGE;
4267
+ }
4268
+ /**
4269
+ * Terminal provider-refusal outcome. Mirrors `reportBudgetExhausted` so the two walls behave
4270
+ * identically from the caller's side, while keeping their diagnostics distinct.
4271
+ *
4272
+ * `kind` reaches the debug log and the result but never the transcript: it is what lets an operator
4273
+ * tell "top up the account" from "rotate the key", and with cause-free user copy this is the only
4274
+ * place that distinction survives.
4275
+ */
4276
+ reportProviderRefused(e) {
4277
+ const detail = providerRefusedDetailOf(e);
4278
+ this.providerRefusedThisTurn = true;
4279
+ this.providerRefusedDetail = detail;
4280
+ logger2.error("ChatDriver: provider refused the request", e);
4281
+ recordTurnError(this.sessionKey, "provider-refused", {
4282
+ agent: this.activeAgentName,
4283
+ provider: this.lastResolvedProviderName,
4284
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? this.lastResolvedProvider,
4285
+ kind: e.kind,
4286
+ upstreamStatus: e.upstreamStatus,
4287
+ upstreamType: e.upstreamType,
4288
+ isSubAgent: this.isSubAgent
4289
+ });
4290
+ this.appendToHistory({ role: "assistant", content: this.providerRefusedBubble() });
4291
+ return this.turnDone("provider-refused", void 0, detail);
4292
+ }
4066
4293
  /** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
4067
4294
  static failureReasonOf(result) {
4068
4295
  return result.reason === "done" ? result.failureReason : void 0;
@@ -4286,9 +4513,13 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4286
4513
  * under a separate session key, so recording here would orphan the event off
4287
4514
  * the user-visible debug-log timeline.)
4288
4515
  */
4289
- failSubAgent(reason, budget) {
4516
+ failSubAgent(reason, budget, providerRefused) {
4290
4517
  if (!this.isSubAgent || this.subAgentFailure) return;
4291
- this.subAgentFailure = budget ? { reason, budget } : { reason };
4518
+ this.subAgentFailure = {
4519
+ reason,
4520
+ ...budget ? { budget } : {},
4521
+ ...providerRefused ? { providerRefused } : {}
4522
+ };
4292
4523
  }
4293
4524
  /**
4294
4525
  * Returns true if `releaseAgent` was called during the most recent turn.
@@ -4747,6 +4978,8 @@ Output format (strict):
4747
4978
  this.budgetExhaustedThisTurn = false;
4748
4979
  this.budgetWallDetail = void 0;
4749
4980
  this.budgetWallViaSubAgent = false;
4981
+ this.providerRefusedThisTurn = false;
4982
+ this.providerRefusedDetail = void 0;
4750
4983
  this.appendToHistory({ role: "user", content: userInput, attachments });
4751
4984
  this.turnStartedAt = Date.now();
4752
4985
  recordMetaEvent(this.sessionKey, "turn.start", {
@@ -4991,6 +5224,10 @@ Output format (strict):
4991
5224
  this.budgetWallViaSubAgent = true;
4992
5225
  this.budgetWallDetail ??= failure?.budget;
4993
5226
  }
5227
+ if (reason === "provider_refused") {
5228
+ this.providerRefusedThisTurn = true;
5229
+ this.providerRefusedDetail ??= failure?.providerRefused;
5230
+ }
4994
5231
  return { outcome: { ok: false, reason }, trace };
4995
5232
  }
4996
5233
  /**
@@ -5006,6 +5243,8 @@ Output format (strict):
5006
5243
  this.budgetExhaustedThisTurn = false;
5007
5244
  this.budgetWallDetail = void 0;
5008
5245
  this.budgetWallViaSubAgent = false;
5246
+ this.providerRefusedThisTurn = false;
5247
+ this.providerRefusedDetail = void 0;
5009
5248
  this.turnStartedAt = Date.now();
5010
5249
  recordMetaEvent(this.sessionKey, "turn.start", {
5011
5250
  phase: "continueFromHistory",
@@ -5195,6 +5434,22 @@ Output format (strict):
5195
5434
  if (this.turnController.signal.aborted) {
5196
5435
  return this.completeAbortedTurn();
5197
5436
  }
5437
+ if (this.providerRefusedThisTurn) {
5438
+ logger2.error("ChatDriver: ending the turn \u2014 a sub-agent hit the provider wall");
5439
+ recordTurnError(this.sessionKey, "provider-refused", {
5440
+ agent: this.activeAgentName,
5441
+ provider: this.lastResolvedProviderName,
5442
+ kind: this.providerRefusedDetail?.kind,
5443
+ via: "sub-agent",
5444
+ isSubAgent: this.isSubAgent
5445
+ });
5446
+ if (this.isSubAgent) {
5447
+ this.failSubAgent("provider_refused", void 0, this.providerRefusedDetail);
5448
+ } else {
5449
+ this.appendToHistory({ role: "assistant", content: this.providerRefusedBubble() });
5450
+ }
5451
+ return this.turnDone("provider-refused", void 0, this.providerRefusedDetail);
5452
+ }
5198
5453
  if (this.budgetExhaustedThisTurn) {
5199
5454
  logger2.error("ChatDriver: ending the turn \u2014 a sub-agent hit the AI budget wall");
5200
5455
  recordTurnError(this.sessionKey, "budget-exhausted", {
@@ -5422,6 +5677,24 @@ ${tailBody}
5422
5677
  }
5423
5678
  return this.turnDone("response-truncated");
5424
5679
  }
5680
+ if (e instanceof ProviderRefusedError) {
5681
+ if (this.isSubAgent) {
5682
+ logger2.error("ChatDriver: provider refused the request", e);
5683
+ recordTurnError(this.sessionKey, "provider-refused", {
5684
+ agent: this.activeAgentName,
5685
+ provider: this.lastResolvedProviderName,
5686
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? this.lastResolvedProvider,
5687
+ kind: e.kind,
5688
+ upstreamStatus: e.upstreamStatus,
5689
+ upstreamType: e.upstreamType,
5690
+ isSubAgent: true
5691
+ });
5692
+ const detail = providerRefusedDetailOf(e);
5693
+ this.failSubAgent("provider_refused", void 0, detail);
5694
+ return this.turnDone("provider-refused", void 0, detail);
5695
+ }
5696
+ return this.reportProviderRefused(e);
5697
+ }
5425
5698
  if (e instanceof BudgetExhaustedError) {
5426
5699
  this.budgetExhaustedThisTurn = true;
5427
5700
  this.budgetWallDetail = budgetDetailOf(e);
@@ -6779,6 +7052,7 @@ function withFreshMetaSnapshot(stored, fresh) {
6779
7052
  AnthropicProvider,
6780
7053
  AnthropicTransport,
6781
7054
  ChatDriver,
7055
+ DEFAULT_PROVIDER_REFUSED_MESSAGE,
6782
7056
  GEMINI_CACHED_INPUT_MULTIPLIER,
6783
7057
  GEMINI_LONG_CONTEXT_THRESHOLD,
6784
7058
  GeminiProvider,
@@ -6786,6 +7060,8 @@ function withFreshMetaSnapshot(stored, fresh) {
6786
7060
  MutableAIProviderRegistry,
6787
7061
  NOOP_ACTIVITY_BUS,
6788
7062
  OrchestratingDriver,
7063
+ PROVIDER_REFUSED_CODE,
7064
+ ProviderRefusedError,
6789
7065
  REQUEST_CONTINUATION_TOOL,
6790
7066
  SUPPORTED_ANTHROPIC_MODEL_IDS,
6791
7067
  SUPPORTED_GEMINI_MODEL_IDS,