@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.
- package/dist/ai-assistant.api.json +391 -5
- package/dist/ai-assistant.d.ts +613 -6
- package/dist/chat-driver.cjs +285 -26
- package/dist/chat-driver.cjs.map +3 -3
- package/dist/chat-driver.mjs +285 -26
- package/dist/chat-driver.mjs.map +3 -3
- package/dist/custom-elements.json +254 -10
- package/dist/dts/channel/ai-activity-channel.d.ts +51 -1
- package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +99 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +2 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +1 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/main/blocked-state.test.d.ts +2 -0
- package/dist/dts/main/blocked-state.test.d.ts.map +1 -0
- package/dist/dts/main/main.d.ts +394 -6
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.styles.test.d.ts +2 -0
- package/dist/dts/main/main.styles.test.d.ts.map +1 -0
- package/dist/dts/main/main.template.d.ts +53 -0
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +162 -6
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/debug-event-log.d.ts +6 -1
- package/dist/dts/state/debug-event-log.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +11 -0
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +263 -21
- package/dist/esm/components/chat-driver/chat-driver.test.js +464 -1
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +312 -0
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +89 -4
- package/dist/esm/main/blocked-state.test.js +969 -0
- package/dist/esm/main/main.js +704 -16
- package/dist/esm/main/main.styles.js +47 -0
- package/dist/esm/main/main.styles.test.js +86 -0
- package/dist/esm/main/main.template.js +121 -4
- package/dist/esm/state/ai-assistant-slice.js +145 -7
- package/dist/esm/state/ai-assistant-slice.test.js +138 -1
- package/dist/esm/state/debug-event-log.js +7 -2
- package/dist/esm/state/debug-event-log.test.js +49 -1
- package/dist/esm/state/persistence/session-snapshot.test.js +18 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/migration-GENC-1464.md +562 -0
- package/docs/sub_agent.md +20 -3
- package/package.json +17 -17
- package/src/channel/ai-activity-channel.ts +56 -2
- package/src/components/chat-driver/chat-driver.test.ts +549 -0
- package/src/components/chat-driver/chat-driver.ts +324 -14
- package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +438 -0
- package/src/components/orchestrating-driver/orchestrating-driver.ts +101 -6
- package/src/main/blocked-state.test.ts +1316 -0
- package/src/main/main.styles.test.ts +103 -0
- package/src/main/main.styles.ts +47 -0
- package/src/main/main.template.ts +131 -4
- package/src/main/main.ts +704 -10
- package/src/state/ai-assistant-slice.test.ts +215 -0
- package/src/state/ai-assistant-slice.ts +218 -8
- package/src/state/debug-event-log.test.ts +63 -0
- package/src/state/debug-event-log.ts +7 -2
- package/src/state/persistence/session-snapshot.test.ts +22 -0
package/dist/ai-assistant.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AggregateUsage } from '@genesislcap/foundation-ai';
|
|
|
3
3
|
import { AIProviderRegistry } from '@genesislcap/foundation-ai';
|
|
4
4
|
import type { AIProviderRegistryStatusEntry } from '@genesislcap/foundation-ai';
|
|
5
5
|
import type { AIProviderType } from '@genesislcap/foundation-ai';
|
|
6
|
+
import { BudgetExhaustedError } from '@genesislcap/foundation-ai';
|
|
6
7
|
import type { CachePolicy } from '@genesislcap/foundation-ai';
|
|
7
8
|
import type { ChatAttachment } from '@genesislcap/foundation-ai';
|
|
8
9
|
import type { ChatConfig } from '@genesislcap/foundation-ai';
|
|
@@ -219,9 +220,59 @@ export declare interface AgenticActivityEvents {
|
|
|
219
220
|
* driver dispose, and an agent handoff the detail is `undefined` — the historical shape,
|
|
220
221
|
* kept byte-identical so subscribers that only care about the boundary can keep ignoring
|
|
221
222
|
* it. Structured-cloneable so it survives the cross-tab BroadcastChannel hop.
|
|
223
|
+
*
|
|
224
|
+
* A `'budget-exhausted'` failure additionally carries `vendor` — the concrete
|
|
225
|
+
* vendor the walled turn resolved to (GENC-1464), which is NOT the same thing as
|
|
226
|
+
* the registry alias recorded on the debug-log entry. Optional and additive: a
|
|
227
|
+
* subscriber reading only `failureReason` is unaffected, and every other
|
|
228
|
+
* failure still emits the historical `{ failureReason }` with no `vendor` key.
|
|
229
|
+
* It exists so a later per-vendor budget model is an additive change rather
|
|
230
|
+
* than a retrofit — the vendor is known at the transport and at the driver, and
|
|
231
|
+
* was previously discarded between them.
|
|
232
|
+
*
|
|
233
|
+
* Note this topic is forwarded on the tab-scoped channel, so a wall hit in a
|
|
234
|
+
* popped-out window reaches the main window's subscribers too — which is why
|
|
235
|
+
* the assistant's blocked latch fires there as well. Correct for a shared spend
|
|
236
|
+
* cap; see `docs/migration-GENC-1464.md` §Scope.
|
|
222
237
|
*/
|
|
223
238
|
'tool-loop-end': {
|
|
224
239
|
failureReason?: TurnFailureReason;
|
|
240
|
+
vendor?: AIProviderType;
|
|
241
|
+
/**
|
|
242
|
+
* Proxy-reported spend figures, present only on `budget-exhausted`. Carried
|
|
243
|
+
* here because this event — not the driver's return value — is what latches
|
|
244
|
+
* the blocked state for a wall hit INSIDE the tool loop (the common case):
|
|
245
|
+
* the publish happens in `sendMessage`'s `finally`, so it lands before the
|
|
246
|
+
* return-value seam and wins the latch. Without these the banner would fall
|
|
247
|
+
* back to the generic copy in exactly the path the figures were added for.
|
|
248
|
+
* Plain numbers + string keep the detail structured-cloneable for the
|
|
249
|
+
* cross-tab hop.
|
|
250
|
+
*/
|
|
251
|
+
budget?: {
|
|
252
|
+
budgetUsd?: number;
|
|
253
|
+
spentUsd?: number;
|
|
254
|
+
vendorLabel: string;
|
|
255
|
+
/**
|
|
256
|
+
* The refusing vendor as a typed value: normalised from `vendorLabel`
|
|
257
|
+
* where a vendor claims that label, otherwise from the proxy's own
|
|
258
|
+
* `vendor` field on the 402 — so it can disagree with `vendorLabel`,
|
|
259
|
+
* which is what keeps attribution working behind a white-labelled or
|
|
260
|
+
* multiplexing gateway.
|
|
261
|
+
*
|
|
262
|
+
* Distinct from the detail's top-level `vendor` (the driver's
|
|
263
|
+
* last-resolved provider): this one comes from the transport that was
|
|
264
|
+
* actually refused, so it is the one a per-vendor latch trusts first.
|
|
265
|
+
*/
|
|
266
|
+
vendor?: AIProviderType;
|
|
267
|
+
/**
|
|
268
|
+
* The proxy's verdict on whether any OTHER vendor it meters still has
|
|
269
|
+
* headroom. The one fact here a subscriber cannot work out for itself:
|
|
270
|
+
* the registry says which vendors EXIST, never which still have
|
|
271
|
+
* budget. A `false` is what stops the banner advising a switch to a
|
|
272
|
+
* vendor that is equally spent.
|
|
273
|
+
*/
|
|
274
|
+
otherVendorAvailable?: boolean;
|
|
275
|
+
};
|
|
225
276
|
} | undefined;
|
|
226
277
|
/**
|
|
227
278
|
* Fired when a tool handler hands a widget to the user mid-loop and parks awaiting it
|
|
@@ -1053,6 +1104,45 @@ declare interface BaseAgentConfig {
|
|
|
1053
1104
|
notResumableMessage?: string;
|
|
1054
1105
|
}
|
|
1055
1106
|
|
|
1107
|
+
/**
|
|
1108
|
+
* Id of the blocked banner, referenced by the composer controls'
|
|
1109
|
+
* `aria-describedby`. Shadow-DOM-scoped, so a fixed string cannot collide with
|
|
1110
|
+
* the host page — and IDREF resolution is same-root, which is exactly where both
|
|
1111
|
+
* ends of this reference live.
|
|
1112
|
+
*
|
|
1113
|
+
* @internal
|
|
1114
|
+
*/
|
|
1115
|
+
export declare const BLOCKED_BANNER_ID = "blocked-banner";
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Class list for the banner below, joined rather than interpolated so an
|
|
1119
|
+
* inapplicable modifier contributes nothing. Two interpolations directly in the
|
|
1120
|
+
* attribute emitted `class="blocked-banner "` in the common (unblocked) case —
|
|
1121
|
+
* harmless to the browser, but it shows up in every DOM snapshot and every
|
|
1122
|
+
* innerHTML assertion a host writes against this element.
|
|
1123
|
+
*
|
|
1124
|
+
* The two modifiers name what they actually gate, which is why neither is
|
|
1125
|
+
* `is-blocked`: `is-visible` means the banner has something to say, and that
|
|
1126
|
+
* includes PARTIAL exhaustion — one vendor walled, composer still live, `blocked`
|
|
1127
|
+
* false. `is-partial` then softens the treatment for exactly that case. Naming
|
|
1128
|
+
* the first after `blocked` read as a contradiction beside the second, and made
|
|
1129
|
+
* the styles say `.blocked-banner.is-blocked` to mean "visible".
|
|
1130
|
+
*
|
|
1131
|
+
* Exported for the unit test that pins the attribute; not part of the element
|
|
1132
|
+
* API.
|
|
1133
|
+
*
|
|
1134
|
+
* @internal
|
|
1135
|
+
*/
|
|
1136
|
+
export declare const blockedBannerClasses: (x: FoundationAiAssistant) => string;
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* The `budget` payload carried on a `'budget-exhausted'` {@link ChatDriverResult}.
|
|
1140
|
+
* Derived from the type rather than restated so the two cannot drift.
|
|
1141
|
+
*/
|
|
1142
|
+
declare type BudgetDetail = NonNullable<Extract<ChatDriverResult, {
|
|
1143
|
+
reason: 'done';
|
|
1144
|
+
}>['budget']>;
|
|
1145
|
+
|
|
1056
1146
|
export { CachePolicy }
|
|
1057
1147
|
|
|
1058
1148
|
/**
|
|
@@ -1235,6 +1325,10 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1235
1325
|
* Set when a sub-agent's tool loop ends without `completeSubAgent` being
|
|
1236
1326
|
* called. Read by the parent's `invokeSubAgent` to build the `{ ok: false }`
|
|
1237
1327
|
* branch of `requestSubAgent`. Only ever set when `isSubAgent` is true.
|
|
1328
|
+
*
|
|
1329
|
+
* `budget` rides along on a `'budget_exhausted'` failure so the parent inherits
|
|
1330
|
+
* the child's ATTRIBUTION, not just the fact of a wall — see
|
|
1331
|
+
* `budgetWallDetail`.
|
|
1238
1332
|
*/
|
|
1239
1333
|
private subAgentFailure;
|
|
1240
1334
|
/**
|
|
@@ -1383,6 +1477,39 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1383
1477
|
private readonly sessionKey;
|
|
1384
1478
|
/** Injected activity bus; defaults to a no-op off-browser (Node/tests/headless). */
|
|
1385
1479
|
private readonly activityBus;
|
|
1480
|
+
/** Transcript copy for a budget wall — see `ChatDriverConfig.budgetExhaustedMessage`. */
|
|
1481
|
+
private readonly budgetExhaustedMessage;
|
|
1482
|
+
/**
|
|
1483
|
+
* Set the moment a budget wall is observed anywhere in this turn — this
|
|
1484
|
+
* driver's own 402, or a sub-agent's (which surfaces here only as a
|
|
1485
|
+
* `'budget_exhausted'` tool result). Read at the top of the tool loop to end
|
|
1486
|
+
* the turn before issuing another model call that would hit the same wall.
|
|
1487
|
+
* Reset per turn alongside the other per-turn counters.
|
|
1488
|
+
*/
|
|
1489
|
+
private budgetExhaustedThisTurn;
|
|
1490
|
+
/**
|
|
1491
|
+
* The refusing vendor's own attribution for the wall `budgetExhaustedThisTurn`
|
|
1492
|
+
* records, when it was knowable. Kept SEPARATE from the flag rather than
|
|
1493
|
+
* replacing it: a figure-less 402 from a transport no vendor claims yields no
|
|
1494
|
+
* detail at all (`budgetDetailOf` returns `undefined`), and folding the two
|
|
1495
|
+
* would make that case stop ending the turn.
|
|
1496
|
+
*
|
|
1497
|
+
* It matters most for a sub-agent's wall. The child can sit on a different
|
|
1498
|
+
* vendor from its parent — `applyAgent` reads `config.provider` — so without
|
|
1499
|
+
* this the parent's short-circuit reports `lastResolvedProvider`, i.e. the one
|
|
1500
|
+
* vendor that did NOT refuse. Under a mixed registry that walls Gemini because
|
|
1501
|
+
* an Anthropic child 402'd, and if those are the only two reachable vendors the
|
|
1502
|
+
* host then derives `blocked` and locks a composer that still had headroom.
|
|
1503
|
+
*/
|
|
1504
|
+
private budgetWallDetail?;
|
|
1505
|
+
/**
|
|
1506
|
+
* Whether this turn's budget wall came from a SUB-AGENT rather than this
|
|
1507
|
+
* driver's own request. Decides whether `lastResolvedProvider` is a valid
|
|
1508
|
+
* attribution fallback: for an own wall it is the refusing vendor, for a
|
|
1509
|
+
* child's wall it is the parent's vendor — the one known NOT to have refused.
|
|
1510
|
+
* Reset per turn alongside `budgetWallDetail`.
|
|
1511
|
+
*/
|
|
1512
|
+
private budgetWallViaSubAgent;
|
|
1386
1513
|
constructor(providerRegistry: AIProviderRegistry, config?: ChatDriverConfig);
|
|
1387
1514
|
/**
|
|
1388
1515
|
* Tear down the driver: aborts the lifecycle signal so any in-flight provider
|
|
@@ -1429,14 +1556,49 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1429
1556
|
* the historical `{ reason: 'done' }`.
|
|
1430
1557
|
*/
|
|
1431
1558
|
private turnDone;
|
|
1559
|
+
/**
|
|
1560
|
+
* Terminal budget outcome for a wall hit **outside** the tool loop — today,
|
|
1561
|
+
* `OrchestratingDriver`'s classification phase, which calls the provider
|
|
1562
|
+
* directly and so never enters `runToolLoop`.
|
|
1563
|
+
*
|
|
1564
|
+
* Does **not** publish `tool-loop-end`: no `tool-loop-start` was published for
|
|
1565
|
+
* the classify phase, and an unbalanced end would break start/end pairing for
|
|
1566
|
+
* subscribers that rely on it. The driver **return value** is what reports this
|
|
1567
|
+
* case — see `FoundationAiAssistant`'s latch, which reads both seams for
|
|
1568
|
+
* exactly this reason.
|
|
1569
|
+
*
|
|
1570
|
+
* The non-sub-agent tail of the in-loop `BudgetExhaustedError` branch lives
|
|
1571
|
+
* here so there is one copy of the log line, the debug-log entry, the
|
|
1572
|
+
* transcript bubble and the result shape rather than two that can drift.
|
|
1573
|
+
*
|
|
1574
|
+
* @param pendingUserMessage - a user message that has NOT yet been appended,
|
|
1575
|
+
* appended first so the answer does not end up replying to nothing. Only the
|
|
1576
|
+
* classification seam passes it: `OrchestratingDriver` dispatches the user's
|
|
1577
|
+
* text as an optimistic `history-updated` detail and leaves the real append
|
|
1578
|
+
* to `chatDriver.sendMessage`, which never runs when `classify()` throws — so
|
|
1579
|
+
* the bubble below would re-dispatch a history the user's own message was
|
|
1580
|
+
* never in, and it would vanish from the transcript on the next render. The
|
|
1581
|
+
* in-loop caller has already appended it and passes nothing.
|
|
1582
|
+
*
|
|
1583
|
+
* @internal
|
|
1584
|
+
*/
|
|
1585
|
+
reportBudgetExhausted(e: BudgetExhaustedError, pendingUserMessage?: ChatMessage): ChatDriverResult;
|
|
1432
1586
|
/** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
|
|
1433
1587
|
private static failureReasonOf;
|
|
1434
1588
|
/**
|
|
1435
1589
|
* Build the `tool-loop-end` event detail for a turn's result. A failure carries a
|
|
1436
1590
|
* `{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,
|
|
1437
1591
|
* kept byte-identical so subscribers see exactly what they always have.
|
|
1592
|
+
*
|
|
1593
|
+
* A budget failure additionally carries `vendor` — the concrete vendor
|
|
1594
|
+
* (`'anthropic'`/`'gemini'`) the walled turn resolved to, which the driver knows
|
|
1595
|
+
* and used to discard. Optional and additive: a subscriber reading only
|
|
1596
|
+
* `failureReason` is unaffected, a non-budget failure still emits the historical
|
|
1597
|
+
* `{ failureReason }` with no `vendor` key, and the value is a plain string so
|
|
1598
|
+
* the detail stays structured-cloneable for the cross-tab hop. It is the field a
|
|
1599
|
+
* per-vendor budget model needs and the one that would be awkward to retrofit.
|
|
1438
1600
|
*/
|
|
1439
|
-
private
|
|
1601
|
+
private loopEndDetail;
|
|
1440
1602
|
/**
|
|
1441
1603
|
* Swap in a new agent's configuration. Called by OrchestratingDriver before
|
|
1442
1604
|
* each specialist turn so the shared driver runs with the right tools and prompt.
|
|
@@ -1503,6 +1665,7 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1503
1665
|
*/
|
|
1504
1666
|
getSubAgentFailure(): {
|
|
1505
1667
|
reason: SubAgentFailureReason;
|
|
1668
|
+
budget?: BudgetDetail;
|
|
1506
1669
|
} | undefined;
|
|
1507
1670
|
/**
|
|
1508
1671
|
* Record a sub-agent failure reason (first one wins). No-op for top-level
|
|
@@ -1735,6 +1898,22 @@ export declare interface ChatDriverConfig {
|
|
|
1735
1898
|
* it defaults to {@link NOOP_ACTIVITY_BUS} so no `BroadcastChannel` is ever opened.
|
|
1736
1899
|
*/
|
|
1737
1900
|
activityBus?: ActivityBus;
|
|
1901
|
+
/**
|
|
1902
|
+
* Transcript copy appended when the AI-spend budget wall is hit (GENC-1464).
|
|
1903
|
+
* Defaults to `DEFAULT_BUDGET_EXHAUSTED_MESSAGE`.
|
|
1904
|
+
*
|
|
1905
|
+
* The assistant element already lets a host override the blocked **banner**
|
|
1906
|
+
* via `setBlocked(true, reason)`; without this the transcript **bubble** stayed
|
|
1907
|
+
* on the default, so a white-labelled host got its own copy in the banner and
|
|
1908
|
+
* the shipped default directly below it. Passing the same effective copy here
|
|
1909
|
+
* keeps the two surfaces saying one thing.
|
|
1910
|
+
*
|
|
1911
|
+
* Deliberately a driver-config field rather than something read off a chat
|
|
1912
|
+
* config: `ChatDriver` has no `chatConfig` and is used standalone (see
|
|
1913
|
+
* `chat-driver-node`), so threading one in would be a much larger and less
|
|
1914
|
+
* reversible change.
|
|
1915
|
+
*/
|
|
1916
|
+
budgetExhaustedMessage?: string;
|
|
1738
1917
|
}
|
|
1739
1918
|
|
|
1740
1919
|
export { ChatFallback }
|
|
@@ -1766,6 +1945,31 @@ declare type ChatInteractionEventsMap = {
|
|
|
1766
1945
|
|
|
1767
1946
|
export { ChatToolChoice }
|
|
1768
1947
|
|
|
1948
|
+
/**
|
|
1949
|
+
* `aria-describedby` for the composer's textarea, send button and attach button:
|
|
1950
|
+
* the banner's id whenever the banner has something to say, otherwise `null` (so
|
|
1951
|
+
* the attribute is omitted rather than emitted empty).
|
|
1952
|
+
*
|
|
1953
|
+
* Keyed on `bannerVisible`, NOT on `blocked`, and that is the point. The
|
|
1954
|
+
* PARTIAL state — some vendor walled, composer still live — is the state this
|
|
1955
|
+
* feature exists to create, and it was the one state with no accessible
|
|
1956
|
+
* explanation at all: `aria-disabled` and `aria-label` bind only on `blocked`, and
|
|
1957
|
+
* a live composer keeps the host's own placeholder, so a screen-reader user
|
|
1958
|
+
* arriving at the textarea heard "Type a message" with no hint that the next turn
|
|
1959
|
+
* might be refused. The banner's `role="status"` announces the text when it
|
|
1960
|
+
* CHANGES; this is what makes the same explanation reachable afterwards, on
|
|
1961
|
+
* demand, from the control it is about.
|
|
1962
|
+
*
|
|
1963
|
+
* Applied in the fully blocked state too, where it is additive: the `aria-label`
|
|
1964
|
+
* there states the reason as the control's name, and this restates it as its
|
|
1965
|
+
* description for the send/attach buttons, which carry neither.
|
|
1966
|
+
*
|
|
1967
|
+
* Exported for the unit test that pins it; not part of the element API.
|
|
1968
|
+
*
|
|
1969
|
+
* @internal
|
|
1970
|
+
*/
|
|
1971
|
+
export declare const composerDescribedBy: (x: FoundationAiAssistant) => string | null;
|
|
1972
|
+
|
|
1769
1973
|
/** A model used during a recorded cost session. */
|
|
1770
1974
|
export declare interface CostSessionModelEntry {
|
|
1771
1975
|
model: string;
|
|
@@ -1997,6 +2201,24 @@ export declare interface FallbackAgentConfig extends BaseAgentConfig {
|
|
|
1997
2201
|
description?: never;
|
|
1998
2202
|
}
|
|
1999
2203
|
|
|
2204
|
+
/**
|
|
2205
|
+
* Banner copy built from the figures a 402 actually reported, or `undefined`
|
|
2206
|
+
* when the proxy sent none.
|
|
2207
|
+
*
|
|
2208
|
+
* `undefined` is the meaningful return, not a fallback: paired with the
|
|
2209
|
+
* slice's "a re-latch without a reason keeps the existing explanation" rule, it
|
|
2210
|
+
* makes a figureless driver latch leave a host-supplied reason intact rather
|
|
2211
|
+
* than blanking it back to the default at the exact moment the wall is hit.
|
|
2212
|
+
*
|
|
2213
|
+
* Exported for the unit test that pins the formatting; not part of the element
|
|
2214
|
+
* API.
|
|
2215
|
+
*
|
|
2216
|
+
* @internal
|
|
2217
|
+
*/
|
|
2218
|
+
export declare function formatBlockedReason(budget?: Extract<ChatDriverResult, {
|
|
2219
|
+
reason: 'done';
|
|
2220
|
+
}>['budget'], vendor?: AIProviderType): string | undefined;
|
|
2221
|
+
|
|
2000
2222
|
/**
|
|
2001
2223
|
* Foundation AI Assistant component.
|
|
2002
2224
|
*
|
|
@@ -2127,13 +2349,22 @@ export declare class FoundationAiAssistant extends GenesisElement {
|
|
|
2127
2349
|
get busy(): boolean;
|
|
2128
2350
|
/**
|
|
2129
2351
|
* True when a new send must be refused: a turn is running (`busy`), a page-reload
|
|
2130
|
-
* restore is loading history that would clobber it (`restoring`),
|
|
2131
|
-
* compaction is rewriting history (`compacting`)
|
|
2132
|
-
*
|
|
2133
|
-
*
|
|
2134
|
-
*
|
|
2352
|
+
* restore is loading history that would clobber it (`restoring`), a manual
|
|
2353
|
+
* compaction is rewriting history (`compacting`), or a backend condition has
|
|
2354
|
+
* locked the assistant outright (`blocked` — e.g. an exhausted AI budget).
|
|
2355
|
+
* Mirrors the composer's `?disabled` gate so a **programmatic**
|
|
2356
|
+
* `send`/`submitMessage` (e.g. a host's custom input while the built-in composer
|
|
2357
|
+
* is hidden) can't slip past it and have its message wiped when the
|
|
2358
|
+
* restored/compacted history lands (GENC-1351 §6).
|
|
2135
2359
|
*/
|
|
2136
2360
|
private get sendBlocked();
|
|
2361
|
+
/**
|
|
2362
|
+
* Why a programmatic send was refused, for `submitMessage`'s `errors`. A
|
|
2363
|
+
* backend block is called out distinctly because — unlike the transient
|
|
2364
|
+
* "busy" cases — waiting and retrying will never clear it, and a caller
|
|
2365
|
+
* looping on "Assistant is busy" would spin forever.
|
|
2366
|
+
*/
|
|
2367
|
+
private sendRefusalReason;
|
|
2137
2368
|
/**
|
|
2138
2369
|
* Re-runs `agentsChanged` if the live `agents` array no longer matches the
|
|
2139
2370
|
* fingerprint of the currently installed driver. Used to apply swaps that
|
|
@@ -2204,6 +2435,366 @@ export declare class FoundationAiAssistant extends GenesisElement {
|
|
|
2204
2435
|
*/
|
|
2205
2436
|
get restoring(): boolean;
|
|
2206
2437
|
set restoring(value: boolean);
|
|
2438
|
+
/**
|
|
2439
|
+
* Whether the assistant is blocked by a backend condition and cannot send —
|
|
2440
|
+
* today, an exhausted AI-spend budget (GENC-1464). While true the composer is
|
|
2441
|
+
* disabled, `send`/`submitMessage` refuse, suggestions stop being fetched, and
|
|
2442
|
+
* a persistent banner (`part="blocked-banner"`) sits above the composer
|
|
2443
|
+
* explaining why. The transcript stays visible and scrollable throughout —
|
|
2444
|
+
* unlike `compacting` and `restoring`, this does not replace the conversation.
|
|
2445
|
+
*
|
|
2446
|
+
* **Latched.** Nothing in the element clears it: not a new turn, not "Clear"
|
|
2447
|
+
* / "New chat", not a pop-in/out. It stays set until the host writes
|
|
2448
|
+
* `false` — because nothing the user can do inside the assistant refills a
|
|
2449
|
+
* budget. Hosts typically set it from their own pre-flight budget check on
|
|
2450
|
+
* mount, and clear it once a later check shows headroom again.
|
|
2451
|
+
*
|
|
2452
|
+
* The driver also latches it automatically when a turn ends with the
|
|
2453
|
+
* `'budget-exhausted'` failure reason — off the `tool-loop-end` activity bus
|
|
2454
|
+
* (which also covers a sub-agent wall, a turn this element did not start, and
|
|
2455
|
+
* an element swapped in mid-turn) and off the driver's return value (which
|
|
2456
|
+
* covers a wall hit during multi-agent classification, where no tool loop ran
|
|
2457
|
+
* and so no bus event fires). So a host that does no pre-flight at all still
|
|
2458
|
+
* gets a correct locked UI the moment the first 402 lands. The bus topic is
|
|
2459
|
+
* tab-scoped, so a wall hit in a popped-out window latches the main window
|
|
2460
|
+
* too — correct for a shared spend cap.
|
|
2461
|
+
*
|
|
2462
|
+
* **Lifetime — read this before relying on it as your source of truth.** The
|
|
2463
|
+
* latch is in-memory and per-`stateKey`:
|
|
2464
|
+
*
|
|
2465
|
+
* - It does **not** survive `switchSession`. Switching away tears the outgoing
|
|
2466
|
+
* session's store down entirely, so switching back yields a fresh, unblocked
|
|
2467
|
+
* store.
|
|
2468
|
+
* - It does **not** survive a page reload. It is deliberately absent from the
|
|
2469
|
+
* persisted session snapshot: the snapshot is long-lived, so a persisted
|
|
2470
|
+
* latch would outlive an out-of-band budget raise with no in-element way to
|
|
2471
|
+
* clear it — a stale lock is worse than re-deriving the state.
|
|
2472
|
+
*
|
|
2473
|
+
* The durable source of truth is therefore the **host's pre-flight**, which
|
|
2474
|
+
* should run on mount and on every session switch. See
|
|
2475
|
+
* `docs/migration-GENC-1464.md` §"How to adopt", Option B.
|
|
2476
|
+
*
|
|
2477
|
+
* @beta
|
|
2478
|
+
*/
|
|
2479
|
+
get blocked(): boolean;
|
|
2480
|
+
set blocked(value: boolean);
|
|
2481
|
+
/**
|
|
2482
|
+
* The distinct vendors the registry can currently reach, from the provider
|
|
2483
|
+
* statuses this element already loads on connect and refreshes on every
|
|
2484
|
+
* observable-registry change.
|
|
2485
|
+
*
|
|
2486
|
+
* This is the element's answer to "which vendor would the next turn use" — a
|
|
2487
|
+
* question that has **no** correct answer and deliberately gets no API. The
|
|
2488
|
+
* provider is resolved per turn AND per agent: `activeProviderInput` may be an
|
|
2489
|
+
* async function of the turn's context, an orchestrated turn picks its agent
|
|
2490
|
+
* with an LLM `classify()` call, and sub-agents resolve their own providers.
|
|
2491
|
+
* Any pre-turn "peek" would therefore be a guess that is wrong precisely on the
|
|
2492
|
+
* multi-agent hosts per-vendor budgets exist for.
|
|
2493
|
+
*
|
|
2494
|
+
* Asking instead which vendors are REACHABLE is answerable, synchronous, and
|
|
2495
|
+
* free — and it is enough: the composer must stay live while any reachable
|
|
2496
|
+
* vendor has headroom, and must lock when none does. It also makes vendor-switch
|
|
2497
|
+
* recovery a derivation rather than a mutation: a host that swaps its registry
|
|
2498
|
+
* to another vendor fires the observable, the statuses reload, the walled vendor
|
|
2499
|
+
* drops out of this set, and `blocked` goes false with every latch left intact.
|
|
2500
|
+
*
|
|
2501
|
+
* @beta
|
|
2502
|
+
*/
|
|
2503
|
+
get reachableVendors(): readonly AIProviderType[];
|
|
2504
|
+
/**
|
|
2505
|
+
* Vendors currently walled by the AI-spend budget, in the order they were
|
|
2506
|
+
* walled. Empty for a host that never adopts the per-vendor API.
|
|
2507
|
+
*
|
|
2508
|
+
* May include a vendor this registry cannot reach — either because the registry
|
|
2509
|
+
* moved on after the wall was latched, or because a 402's
|
|
2510
|
+
* `otherVendorAvailable: false` walls every vendor the proxy meters (which is
|
|
2511
|
+
* what that verdict is a statement about; see
|
|
2512
|
+
* {@link FoundationAiAssistant.latchBlockedFrom}). Neither costs anything
|
|
2513
|
+
* internally — {@link FoundationAiAssistant.blocked} asks only about the
|
|
2514
|
+
* reachable set, and the banner reads the reachability-filtered
|
|
2515
|
+
* {@link FoundationAiAssistant.relevantBlockedVendors} — but a host rendering
|
|
2516
|
+
* this list itself should filter it against its own registry.
|
|
2517
|
+
*
|
|
2518
|
+
* Read-only on purpose: a settable array would let a host write a partial list
|
|
2519
|
+
* and silently orphan the per-vendor banner copy.
|
|
2520
|
+
* {@link FoundationAiAssistant.setVendorBlocked} is the write path.
|
|
2521
|
+
*
|
|
2522
|
+
* @beta
|
|
2523
|
+
*/
|
|
2524
|
+
get blockedVendors(): readonly AIProviderType[];
|
|
2525
|
+
/**
|
|
2526
|
+
* Whether this specific vendor's budget is walled — regardless of whether any
|
|
2527
|
+
* other vendor still has headroom.
|
|
2528
|
+
*
|
|
2529
|
+
* @beta
|
|
2530
|
+
*/
|
|
2531
|
+
isVendorBlocked(vendor: AIProviderType): boolean;
|
|
2532
|
+
/**
|
|
2533
|
+
* Wall (or release) one vendor's budget, optionally with banner copy for it.
|
|
2534
|
+
* The per-vendor mirror of {@link FoundationAiAssistant.setBlocked}.
|
|
2535
|
+
*
|
|
2536
|
+
* Walling a vendor does **not** on its own disable the composer: while another
|
|
2537
|
+
* reachable vendor has headroom the assistant stays usable and the banner tells
|
|
2538
|
+
* the user to switch. Only when every reachable vendor is walled does `blocked`
|
|
2539
|
+
* become true.
|
|
2540
|
+
*
|
|
2541
|
+
* `reason` is composed into the banner sentence for that vendor, **however many
|
|
2542
|
+
* vendors are walled** — see {@link FoundationAiAssistant.effectiveBlockedReason}.
|
|
2543
|
+
* It is per-vendor copy, not a whole-banner override; {@link FoundationAiAssistant.blockedReason}
|
|
2544
|
+
* is the override. Omitting it keeps whatever explanation that vendor already
|
|
2545
|
+
* carried, so a driver latch landing after a host one cannot blank it.
|
|
2546
|
+
*
|
|
2547
|
+
* `'none'` is rejected with a warning rather than accepted: it is the "no
|
|
2548
|
+
* provider configured" sentinel, not a vendor. Latching it would put the raw
|
|
2549
|
+
* sentinel in the banner ("none's AI usage limit is reached") and could never
|
|
2550
|
+
* be undone by derivation, because `'none'` never appears in
|
|
2551
|
+
* {@link FoundationAiAssistant.reachableVendors} — so `blocked` could never
|
|
2552
|
+
* become derivable from it either. Use {@link FoundationAiAssistant.setBlocked}
|
|
2553
|
+
* for a vendor-agnostic block.
|
|
2554
|
+
*
|
|
2555
|
+
* @beta
|
|
2556
|
+
*/
|
|
2557
|
+
/** Whether this vendor's wall came from the sweep alone — see the slice's `sweptVendors`. */
|
|
2558
|
+
private isVendorSwept;
|
|
2559
|
+
setVendorBlocked(vendor: AIProviderType, blocked: boolean, reason?: string | null): void;
|
|
2560
|
+
/**
|
|
2561
|
+
* The walled vendors the user can still be routed to — i.e.
|
|
2562
|
+
* {@link FoundationAiAssistant.blockedVendors} narrowed to the reachable set,
|
|
2563
|
+
* which is the only set the banner may name.
|
|
2564
|
+
*
|
|
2565
|
+
* A wall the registry can no longer reach is not news: it cannot be hit, and
|
|
2566
|
+
* naming it puts a vendor in front of the user that is not theirs. The concrete
|
|
2567
|
+
* failure this exists to stop: a host ships Anthropic-only, Anthropic walls, the
|
|
2568
|
+
* host swaps its registry to Gemini, Gemini walls — and the banner reads "AI
|
|
2569
|
+
* usage limits are reached for Anthropic and Gemini" to a user who has never
|
|
2570
|
+
* had an Anthropic key. The spurious second name also flips the copy onto the
|
|
2571
|
+
* plural branch, so even the closing sentence is wrong.
|
|
2572
|
+
*
|
|
2573
|
+
* When nothing is reachable YET (the statuses are still loading — the exact
|
|
2574
|
+
* window the pre-flight and the cross-tab bus latch into), the fallback keeps
|
|
2575
|
+
* every wall for LOCKING (`blocked` still derives true) but names only the
|
|
2576
|
+
* vendors walled by a REFUSAL, dropping the ones the `otherVendorAvailable`
|
|
2577
|
+
* sweep added. A refusal is a fact about a vendor this user just used; a sweep
|
|
2578
|
+
* entry is the proxy's headroom verdict about a vendor the host may not even
|
|
2579
|
+
* ship — naming it here reproduced the exact failure above from the other
|
|
2580
|
+
* direction ("Gemini's AI usage limit is reached" to an Anthropic-only user),
|
|
2581
|
+
* and flipped the copy onto the plural branch with it. Self-corrects when the
|
|
2582
|
+
* statuses land: from then on reachability, not provenance, decides.
|
|
2583
|
+
*
|
|
2584
|
+
* @internal
|
|
2585
|
+
*/
|
|
2586
|
+
private get relevantBlockedVendors();
|
|
2587
|
+
/**
|
|
2588
|
+
* Whether the blocked banner has anything to say — a vendor-agnostic block, OR
|
|
2589
|
+
* at least one walled vendor the registry can still reach. Broader than
|
|
2590
|
+
* {@link FoundationAiAssistant.blocked} on purpose: partial exhaustion leaves
|
|
2591
|
+
* the composer live but still needs to be announced, because the next turn may
|
|
2592
|
+
* route to the walled vendor and fail.
|
|
2593
|
+
*
|
|
2594
|
+
* Reads the reachability-filtered list for the same reason the copy does — a
|
|
2595
|
+
* host that has swapped its registry away from the walled vendor has nothing
|
|
2596
|
+
* left to announce, and would otherwise get a banner that falls through to the
|
|
2597
|
+
* generic default copy over a perfectly usable composer.
|
|
2598
|
+
*
|
|
2599
|
+
* @internal
|
|
2600
|
+
*/
|
|
2601
|
+
get bannerVisible(): boolean;
|
|
2602
|
+
/**
|
|
2603
|
+
* Whether a suggestions fetch would hit a wall.
|
|
2604
|
+
*
|
|
2605
|
+
* Unlike a chat turn, this one CAN be resolved exactly: both suggestion paths
|
|
2606
|
+
* go to the registry **default** and never to a per-agent override
|
|
2607
|
+
* (`ChatDriver.getSuggestions` calls `providerRegistry.default()`, and
|
|
2608
|
+
* `OrchestratingDriver.getSuggestions` just delegates to it). So the vendor a
|
|
2609
|
+
* suggestions call would use is knowable, and this is the one place in the
|
|
2610
|
+
* feature where that is true.
|
|
2611
|
+
*
|
|
2612
|
+
* Same rationale as the guard it replaces: a doomed suggestions call burns a
|
|
2613
|
+
* `suggestions.failed` meta event and parks a raw transport error in
|
|
2614
|
+
* `suggestionsState` for hosts to find.
|
|
2615
|
+
*
|
|
2616
|
+
* @internal
|
|
2617
|
+
*/
|
|
2618
|
+
get suggestionsBlocked(): boolean;
|
|
2619
|
+
/**
|
|
2620
|
+
* Explanation shown in the blocked banner, or `null` for the element's default
|
|
2621
|
+
* copy. Only meaningful while {@link FoundationAiAssistant.blocked} is true;
|
|
2622
|
+
* setting `blocked = false` clears it.
|
|
2623
|
+
*
|
|
2624
|
+
* Writable, and symmetric with every other store-backed accessor on this
|
|
2625
|
+
* class: writing it re-latches with the CURRENT `blocked` value, so it changes
|
|
2626
|
+
* the copy without disturbing the flag. Assigning `null` clears the
|
|
2627
|
+
* explanation while staying blocked (the banner falls back to the default
|
|
2628
|
+
* copy). {@link FoundationAiAssistant.setBlocked} remains the way to write
|
|
2629
|
+
* both in one atomic action.
|
|
2630
|
+
*
|
|
2631
|
+
* @beta
|
|
2632
|
+
*/
|
|
2633
|
+
get blockedReason(): string | null;
|
|
2634
|
+
set blockedReason(value: string | null);
|
|
2635
|
+
/**
|
|
2636
|
+
* Latch the backend block off a turn outcome. The single decision point for
|
|
2637
|
+
* both latch sites (the `tool-loop-end` bus subscription and `send()`'s
|
|
2638
|
+
* return-value check), so the rule cannot diverge between them.
|
|
2639
|
+
*
|
|
2640
|
+
* Idempotent and one-way **per vendor**: it never unblocks, and it never
|
|
2641
|
+
* re-writes an existing wall. That guard is load-bearing, not just an
|
|
2642
|
+
* optimisation — the bus fires from the driver's `finally`, i.e. BEFORE
|
|
2643
|
+
* `sendMessage()` resolves, so a host subscribed to `tool-loop-end` (what the
|
|
2644
|
+
* migration guide's Option C recommends) sets its own detailed reason and the
|
|
2645
|
+
* return-value latch would otherwise land a moment later and blank it.
|
|
2646
|
+
*
|
|
2647
|
+
* The guard being per-vendor rather than global is the whole difference. A
|
|
2648
|
+
* single global "already blocked, do nothing" would swallow a second vendor's
|
|
2649
|
+
* wall — so a session walled on Anthropic could never record that Gemini went
|
|
2650
|
+
* too, and under a mixed registry an Anthropic-only wall would lock a composer
|
|
2651
|
+
* that Gemini could still serve.
|
|
2652
|
+
*
|
|
2653
|
+
* **Which field is authoritative**, in order:
|
|
2654
|
+
*
|
|
2655
|
+
* 1. `budget.vendorLabel` — stamped by the transport that was actually refused,
|
|
2656
|
+
* so it can never be stale. But it is a static per-transport string, so a
|
|
2657
|
+
* white-labelled or multiplexing gateway fronting several upstreams leaves
|
|
2658
|
+
* it unclaimed by any vendor.
|
|
2659
|
+
* 2. `budget.vendor` — which the driver resolved from the label where it could,
|
|
2660
|
+
* and otherwise from the proxy's own `vendor` field on the 402. It is
|
|
2661
|
+
* therefore NOT simply `vendorLabel` normalised, and the two can disagree;
|
|
2662
|
+
* that fallback is the only attribution on offer for the gateway case above.
|
|
2663
|
+
* 3. `vendorHint` — the bus detail's top-level vendor, i.e. the driver's
|
|
2664
|
+
* last-resolved provider. Last resort because it CAN be stale: an
|
|
2665
|
+
* orchestrated turn classifies against the registry default, a provider the
|
|
2666
|
+
* chat driver may never have resolved, so its last-resolved provider there is
|
|
2667
|
+
* the previous turn's vendor or nothing at all.
|
|
2668
|
+
*
|
|
2669
|
+
* A wall that names no recognised vendor falls back to the vendor-agnostic
|
|
2670
|
+
* block — fail-safe, and identical to the pre-per-vendor behaviour.
|
|
2671
|
+
*
|
|
2672
|
+
* **`otherVendorAvailable: false` is server-known truth and outranks every
|
|
2673
|
+
* inference made here.** The proxy meters the pots, so only it can say whether
|
|
2674
|
+
* anything else has headroom; this element can only observe which vendors the
|
|
2675
|
+
* registry can REACH, which says nothing about their remaining spend. So when
|
|
2676
|
+
* the proxy says no, every other *budgeted* vendor is walled in the same pass.
|
|
2677
|
+
* That is what makes `blocked` derive true — locking the composer on the first
|
|
2678
|
+
* response instead of after a second doomed turn — and what empties the "free"
|
|
2679
|
+
* set the banner would otherwise have advised switching to.
|
|
2680
|
+
*
|
|
2681
|
+
* The sweep runs over `BUDGETED_VENDORS`, deliberately **not** over
|
|
2682
|
+
* {@link FoundationAiAssistant.reachableVendors}. Reachability is loaded
|
|
2683
|
+
* asynchronously (`activateSession` kicks off `loadProviderStatuses()` and does
|
|
2684
|
+
* not await it), so a wall landing before the statuses resolve would have swept
|
|
2685
|
+
* an empty set — silently discarding the one fact on the 402 the client cannot
|
|
2686
|
+
* re-derive, with nothing to re-run it when the statuses arrived. Worse, it let
|
|
2687
|
+
* the verdict effectively un-latch: the single wall derived `blocked` only
|
|
2688
|
+
* while the reachable set was empty, so the moment the statuses landed the
|
|
2689
|
+
* composer came back to life against a budget the proxy had already said was
|
|
2690
|
+
* gone. Sweeping the metered vendor list instead makes the verdict independent
|
|
2691
|
+
* of load order and of registry membership, which is exactly what it is: a
|
|
2692
|
+
* statement about the proxy's pots, not about this user's registry.
|
|
2693
|
+
*
|
|
2694
|
+
* Walling a vendor the registry cannot reach costs nothing: `blocked` only asks
|
|
2695
|
+
* whether every REACHABLE vendor is walled, and the banner reads
|
|
2696
|
+
* {@link FoundationAiAssistant.relevantBlockedVendors}, which filters the walls
|
|
2697
|
+
* back down to the reachable set before naming any of them.
|
|
2698
|
+
*
|
|
2699
|
+
* One boundary remains on the sweep: it walls only vendors the proxy meters,
|
|
2700
|
+
* enforced structurally by iterating `BUDGETED_VENDORS` itself rather than
|
|
2701
|
+
* filtering a wider set through a predicate. Chrome runs on-device with no pot
|
|
2702
|
+
* to exhaust, so "no other vendor has budget" is not a statement about it, and
|
|
2703
|
+
* "Switch to Chrome to keep going." stays honest advice.
|
|
2704
|
+
*
|
|
2705
|
+
* The sweep runs last but sits OUTSIDE the per-vendor idempotence guard, and
|
|
2706
|
+
* both halves of that matter. Last, so the vendor that actually refused this
|
|
2707
|
+
* turn heads the wall order the multi-vendor banner reads. Outside the guard,
|
|
2708
|
+
* because the verdict can arrive on a LATER 402 for a vendor that is already
|
|
2709
|
+
* walled — turn 1 walls Anthropic while Gemini still has headroom, turn 2 walls
|
|
2710
|
+
* it again and reports that Gemini has since gone too — and an early return
|
|
2711
|
+
* would drop exactly the news the second turn was there to deliver.
|
|
2712
|
+
*
|
|
2713
|
+
* @param reason - the turn's failure reason; anything but `'budget-exhausted'` is ignored.
|
|
2714
|
+
* @param ref - session store to write through; defaults to the live one. `send()`
|
|
2715
|
+
* passes the ref it captured before its awaits, since a lifecycle event during the
|
|
2716
|
+
* turn may already have cleared `_sessionRef`.
|
|
2717
|
+
* @param budget - what the 402 reported: figures when the proxy sent any, the
|
|
2718
|
+
* refusing vendor, and its `otherVendorAvailable` verdict.
|
|
2719
|
+
* @param vendorHint - the `tool-loop-end` detail's top-level vendor, used only
|
|
2720
|
+
* when the budget payload names none.
|
|
2721
|
+
*
|
|
2722
|
+
* @internal
|
|
2723
|
+
*/
|
|
2724
|
+
private latchBlockedFrom;
|
|
2725
|
+
/**
|
|
2726
|
+
* Block or unblock the assistant, optionally with custom banner copy.
|
|
2727
|
+
* Equivalent to the {@link FoundationAiAssistant.blocked} setter, plus the
|
|
2728
|
+
* reason in one atomic write.
|
|
2729
|
+
*
|
|
2730
|
+
* Blocking writes the **vendor-agnostic** block — what a host with one budget
|
|
2731
|
+
* pot means — and does not populate
|
|
2732
|
+
* {@link FoundationAiAssistant.blockedVendors}. Unblocking clears the
|
|
2733
|
+
* per-vendor walls as well, so a host asserting "the wall is gone" after its
|
|
2734
|
+
* own pre-flight cannot be silently overruled by a driver latch it never saw.
|
|
2735
|
+
*
|
|
2736
|
+
* @beta
|
|
2737
|
+
*/
|
|
2738
|
+
setBlocked(blocked: boolean, reason?: string | null): void;
|
|
2739
|
+
/**
|
|
2740
|
+
* Copy shown in the blocked banner, in ascending specificity:
|
|
2741
|
+
*
|
|
2742
|
+
* 1. the host's `blockedReason` — a whole-banner override, unchanged;
|
|
2743
|
+
* 2. copy composed from the walled vendors (their per-vendor reasons when set,
|
|
2744
|
+
* otherwise their names) plus the action that is actually available;
|
|
2745
|
+
* 3. the default budget-exhaustion message.
|
|
2746
|
+
*
|
|
2747
|
+
* The action clause is computed here rather than stored, because whether
|
|
2748
|
+
* "switch vendor" or "contact your administrator" is honest depends on whether
|
|
2749
|
+
* another reachable vendor still has headroom — which changes when the registry
|
|
2750
|
+
* changes, long after the wall was latched. Telling a user to switch to a
|
|
2751
|
+
* vendor that is also exhausted is worse than saying nothing.
|
|
2752
|
+
*
|
|
2753
|
+
* The `free` set is a client-side inference — "reachable and not known to be
|
|
2754
|
+
* walled" — and it is only ever allowed to be one. Where the proxy has told us
|
|
2755
|
+
* otherwise (`otherVendorAvailable: false`) the latch has already walled those
|
|
2756
|
+
* vendors, so they are not in `free` here and no "switch to X" clause can be
|
|
2757
|
+
* composed from them. Both lists are filtered by reachability, so the copy
|
|
2758
|
+
* names only vendors this user's registry can actually route to.
|
|
2759
|
+
*
|
|
2760
|
+
* **Per-vendor copy is composed however many vendors are walled**, which is
|
|
2761
|
+
* what {@link FoundationAiAssistant.setVendorBlocked} and
|
|
2762
|
+
* `docs/migration-GENC-1464.md` both promise. It previously reached only the
|
|
2763
|
+
* single-vendor branch, so a host that had carefully set copy for each of its
|
|
2764
|
+
* vendors watched all of it vanish the moment a second one walled — replaced by
|
|
2765
|
+
* generic copy, at the exact moment the situation got worse.
|
|
2766
|
+
*
|
|
2767
|
+
* The compact "AI usage limits are reached for A and B." list is kept for the
|
|
2768
|
+
* case it was written for: NO walled vendor carries copy, so a statement per
|
|
2769
|
+
* vendor would just repeat one boilerplate sentence per name. As soon as any of
|
|
2770
|
+
* them does carry copy, the branch lists one statement per vendor instead —
|
|
2771
|
+
* falling back to that same boilerplate for the ones that have none, so the
|
|
2772
|
+
* sentence set stays complete rather than silently naming a subset.
|
|
2773
|
+
*
|
|
2774
|
+
* @internal
|
|
2775
|
+
*/
|
|
2776
|
+
get effectiveBlockedReason(): string;
|
|
2777
|
+
/**
|
|
2778
|
+
* Copy the driver writes into the TRANSCRIPT when a turn hits the budget wall.
|
|
2779
|
+
*
|
|
2780
|
+
* A host-set `blockedReason` still wins, so a white-labelled host does not read
|
|
2781
|
+
* its own explanation in the banner and the shipped default directly below it —
|
|
2782
|
+
* that is the whole reason the driver takes this at all.
|
|
2783
|
+
*
|
|
2784
|
+
* What it deliberately is NOT is
|
|
2785
|
+
* {@link FoundationAiAssistant.effectiveBlockedReason}. That getter
|
|
2786
|
+
* composes advice — `"…Switch to Gemini to keep going."` — which is true only
|
|
2787
|
+
* while that vendor has headroom, and the transcript is a permanent record. The
|
|
2788
|
+
* driver reads this once at CONSTRUCTION, and `getOrCreateDriver` keys on the
|
|
2789
|
+
* agents list, so an agents swap *after* a wall re-runs `createDriver` and would
|
|
2790
|
+
* freeze that sentence into every later turn's bubble. Reading the latch's own
|
|
2791
|
+
* copy instead is structurally incapable of carrying the advice: the only
|
|
2792
|
+
* writer of `blockedReason` is the vendor-agnostic branch of
|
|
2793
|
+
* `latchBlockedFrom`, whose `formatBlockedReason(budget)` form has no switch
|
|
2794
|
+
* clause. Per-vendor copy lives in `blockedVendorReasons` and reaches the
|
|
2795
|
+
* banner alone.
|
|
2796
|
+
*/
|
|
2797
|
+
private get transcriptBudgetExhaustedMessage();
|
|
2207
2798
|
/**
|
|
2208
2799
|
* Name of the agent the user has pinned via the agent picker. `null` means
|
|
2209
2800
|
* automatic routing (Auto). Persisted on the session store, so it survives
|
|
@@ -2448,6 +3039,8 @@ export declare class FoundationAiAssistant extends GenesisElement {
|
|
|
2448
3039
|
private driverCleanup?;
|
|
2449
3040
|
private loadingTimer;
|
|
2450
3041
|
private unsubBus?;
|
|
3042
|
+
/** Unsubscribe handle for the `tool-loop-end` budget latch (GENC-1464). */
|
|
3043
|
+
private _unsubBudgetLatch?;
|
|
2451
3044
|
/** Unsubscribe handle for the provider-registry change listener (observable registries only). */
|
|
2452
3045
|
private unsubProviderRegistry?;
|
|
2453
3046
|
/** Unsubscribe handle for {@link (AssistantAppSettingsProvider:interface).subscribe}. */
|
|
@@ -3316,6 +3909,17 @@ export declare class OrchestratingDriver extends EventTarget implements AiDriver
|
|
|
3316
3909
|
* user stops. Reset at the start of each `sendMessage`.
|
|
3317
3910
|
*/
|
|
3318
3911
|
private cancelled;
|
|
3912
|
+
/**
|
|
3913
|
+
* Whether the CURRENT turn's user message has reached the inner driver's
|
|
3914
|
+
* history. False through the pre-first-turn classify (where the only echo of
|
|
3915
|
+
* the message is an optimistic `history-updated` dispatch), true from the
|
|
3916
|
+
* moment `chatDriver.sendMessage` is entered — including through every later
|
|
3917
|
+
* handoff classify. Read by the budget-wall catch in `sendMessage` to decide
|
|
3918
|
+
* whether `reportBudgetExhausted` must append the message itself: appending
|
|
3919
|
+
* it when already appended duplicated it; not appending it when unappended
|
|
3920
|
+
* made it vanish. Reset at the top of each `runOrchestratedTurn`.
|
|
3921
|
+
*/
|
|
3922
|
+
private userMessageAppended;
|
|
3319
3923
|
/**
|
|
3320
3924
|
* Sticky user pick from the picker (or the host's `setAgent` API). Only
|
|
3321
3925
|
* changes on explicit user action. Survives flow completion: when a stateful
|
|
@@ -3349,6 +3953,8 @@ export declare class OrchestratingDriver extends EventTarget implements AiDriver
|
|
|
3349
3953
|
maxTurnSnapshots?: number;
|
|
3350
3954
|
/** Activity bus passed through to the inner ChatDriver (browser host injects the singleton). */
|
|
3351
3955
|
activityBus?: ActivityBus;
|
|
3956
|
+
/** Budget-wall transcript copy, passed through to the inner ChatDriver (GENC-1464). */
|
|
3957
|
+
budgetExhaustedMessage?: string;
|
|
3352
3958
|
});
|
|
3353
3959
|
resolveInteraction(interactionId: string, result: unknown): void;
|
|
3354
3960
|
getInteractionContext(interactionId: string): InteractionContext | undefined;
|
|
@@ -3381,6 +3987,7 @@ export declare class OrchestratingDriver extends EventTarget implements AiDriver
|
|
|
3381
3987
|
getExternalDiagnostics(): ReadonlyArray<DiagnosticEntry>;
|
|
3382
3988
|
getSuggestions(history: ChatMessage[], prompt: string, count: number, allAgentInfo?: AllAgentSummary[]): Promise<string[]>;
|
|
3383
3989
|
sendMessage(input: string, attachments?: ChatAttachment[]): Promise<ChatDriverResult>;
|
|
3990
|
+
private runOrchestratedTurn;
|
|
3384
3991
|
continueFromHistory(transientPrimer?: ChatMessage[]): Promise<ChatDriverResult>;
|
|
3385
3992
|
/** {@inheritDoc AiDriver.primeRestoredAgentState} */
|
|
3386
3993
|
primeRestoredAgentState(snapshots: Record<string, unknown>): void;
|