@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
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
# Migration Guide — GENC-1464 (AI budget exhausted: a first-class blocked state)
|
|
2
|
+
|
|
3
|
+
> **Additive — no migration required.** Nothing was removed, renamed, or given a new
|
|
4
|
+
> meaning. Every existing event name, public member, template `part`, and failure
|
|
5
|
+
> reason behaves exactly as before. Upgrade, and the only visible change is that a
|
|
6
|
+
> spend-cap rejection now produces a clear, locked UI instead of a generic apology. The one
|
|
7
|
+
> exception is copy: the default banner sentence now names the vendor that refused — see
|
|
8
|
+
> [Per-vendor budgets](#per-vendor-budgets).
|
|
9
|
+
>
|
|
10
|
+
> All surfaces below are `@beta`.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
When the ai-service proxy refuses a request because the AI-spend budget is used up, it
|
|
17
|
+
answers `HTTP 402` with `code: 'BUDGET_EXCEEDED'` (workstream C). Before this change the
|
|
18
|
+
client had no idea what that meant. A `402` fell through the transport as an untyped
|
|
19
|
+
`Error('Anthropic request error 402: …')`, landed in the chat driver's **transient-retry**
|
|
20
|
+
catch, and was re-issued up to `MAX_SETUP_TRANSPORT_RETRIES` times — three doomed requests
|
|
21
|
+
against a wall that no retry can clear — before the user finally got:
|
|
22
|
+
|
|
23
|
+
> Sorry, something went wrong on my end. Please try again in a moment.
|
|
24
|
+
|
|
25
|
+
Which is wrong twice over: nothing went wrong *on our end*, and trying again in a moment
|
|
26
|
+
will not help. Worse, the composer stayed enabled, so the user's next message hit the same
|
|
27
|
+
wall.
|
|
28
|
+
|
|
29
|
+
Budget exhaustion is now modelled for what it is: a **terminal, out-of-band condition**,
|
|
30
|
+
in the same family as `ResponseTruncatedError` — deterministic, not retryable, and fixable
|
|
31
|
+
only by a human raising the cap.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## What was added
|
|
36
|
+
|
|
37
|
+
### 1. `@genesislcap/foundation-ai`
|
|
38
|
+
|
|
39
|
+
| Addition | Kind | Notes |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `BudgetExhaustedError` | exported class | Fields: `vendorLabel`, `budgetUsd?`, `spentUsd?`, `detail?`, `otherVendorAvailable?`, `serverVendorLabel?` |
|
|
42
|
+
| `DEFAULT_BUDGET_EXHAUSTED_MESSAGE` | exported const | The one copy shown to the user, shared by the transcript bubble and the assistant's banner |
|
|
43
|
+
| `'budget-exhausted'` | new `TurnFailureReason` member | Rides `ChatDriverResult.failureReason` and the `tool-loop-end` activity-bus detail |
|
|
44
|
+
| `'budget_exhausted'` | new `SubAgentFailureReason` member | The `{ ok: false }` branch of `requestSubAgent`. **Terminal for the parent turn too** — see [`sub_agent.md`](./sub_agent.md) |
|
|
45
|
+
| `ChatDriverResult.budget` | new optional field on the `done` arm | `{ budgetUsd?, spentUsd?, vendorLabel, vendor?, otherVendorAvailable? }` — what the `402` reported, present on a budget failure that carried figures **or** an attributable vendor |
|
|
46
|
+
| `VENDOR_LABELS` / `vendorTypeOfLabel` | exported const + function | The vendor display names every transport stamps, and the reverse lookup that turns one back into an `AIProviderType` |
|
|
47
|
+
| `BUDGETED_VENDORS` / `isBudgetedVendor` | exported const + function | The vendors the proxy meters (`anthropic`, `gemini`) — i.e. the ones a wall can name, mirroring ai-service's `VENDORS`. `chrome` and `openai` are in `VENDOR_LABELS` for display only: Chrome runs on-device with no pot to exhaust, and the proxy refuses `openai` up front with `400 UNSUPPORTED_PROVIDER`, so neither is ever walled |
|
|
48
|
+
|
|
49
|
+
### Transport coverage
|
|
50
|
+
|
|
51
|
+
The classification is keyed on **status OR code** — the contracted `402`, or
|
|
52
|
+
`code: 'BUDGET_EXCEEDED'` under any status — and in every wire mode it runs
|
|
53
|
+
*before* the retryable-status ladder. Both keys matter: `status` is what the
|
|
54
|
+
workstream-C contract guarantees and the only key a plain-text rejection carries,
|
|
55
|
+
while `code` is the cheap defence against that contract drifting. `429` is in both
|
|
56
|
+
transports' retryable sets, so a proxy that ever mapped the refusal onto it would
|
|
57
|
+
otherwise reproduce the exact bug this change removes — five doomed requests and
|
|
58
|
+
~31s of backoff before an opaque failure.
|
|
59
|
+
|
|
60
|
+
- **Legacy JSON** — `402` (or the code under another status) → `BudgetExhaustedError`,
|
|
61
|
+
enriched from the body when it parses. An unparseable or plain-text `402` still
|
|
62
|
+
classifies, just without the extra fields.
|
|
63
|
+
- **NDJSON framed** — a terminal `{ t: 'err', status: 402, code: 'BUDGET_EXCEEDED', error,
|
|
64
|
+
details: { … } }` frame → the same typed throw, classified before the
|
|
65
|
+
ladder exactly like `UPSTREAM_STALLED`.
|
|
66
|
+
|
|
67
|
+
The fields read off the rejection, top-level in the legacy body and under `details` in
|
|
68
|
+
the framed one (`error` stays top-level in both). Each is narrowed independently, so a
|
|
69
|
+
`null` figure — which the proxy sends in preference to a misleading `0` — does not
|
|
70
|
+
suppress the fields beside it:
|
|
71
|
+
|
|
72
|
+
| field | used for |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `budgetUsd` | the cap in the banner's figures |
|
|
75
|
+
| `spentUsd` | the spend in the banner's figures |
|
|
76
|
+
| `error` | the proxy's human-readable detail |
|
|
77
|
+
| `vendor` | attribution when the transport's own label is unclaimed |
|
|
78
|
+
| `otherVendorAvailable` | whether "switch vendor" is honest advice at all |
|
|
79
|
+
|
|
80
|
+
`402` is deliberately **absent** from every transport's `RETRYABLE_STATUSES`, and a
|
|
81
|
+
regression test in each transport's suite asserts `fetch` is called exactly once, so it can
|
|
82
|
+
never quietly join the ladder. (`429` still retries — also pinned by a test.) A separate
|
|
83
|
+
`post-with-retry.test.ts` pins the *ordering* directly, by injecting a retryable set that
|
|
84
|
+
does contain `402`.
|
|
85
|
+
|
|
86
|
+
**Which transports are covered, precisely** — the detection lives in the shared
|
|
87
|
+
`postWithRetry` envelope, but not every transport delegates to it:
|
|
88
|
+
|
|
89
|
+
| Transport | Covered | Notes |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| `AnthropicTransport` | ✅ both `chat()` and `sendStructuredPrompt` | Goes through `postWithRetry` |
|
|
92
|
+
| `GeminiTransport` | ✅ both | Goes through `postWithRetry` |
|
|
93
|
+
| `ServerOpenAITransport` | ⚠️ typed, but invisible | Does its **own bare `fetch`**, so it classifies the `402` separately. It has no `chat()` path — it serves only `sendStructuredPrompt`, whose single caller `OpenAIProvider.interpretCriteria` catches everything and returns `null`. So a `402` there degrades smart-search **silently** and can **never** latch `blocked`, before or after this change. The typed throw improves the log line and helps a host that catches it directly. |
|
|
94
|
+
| `ChromePromptTransport`, `DefaultTransport` | n/a | Local / no-op; there is no proxy and nothing to classify |
|
|
95
|
+
|
|
96
|
+
Routing `ServerOpenAITransport` through `postWithRetry` wholesale was considered and
|
|
97
|
+
declined: it has no retry ladder, no stall timer and no framed mode, so that is a
|
|
98
|
+
transport rewrite rather than a fix.
|
|
99
|
+
|
|
100
|
+
### 2. `@genesislcap/ai-assistant`
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
| Addition | Kind |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `FoundationAiAssistant.blocked` | public getter/setter (`boolean`) |
|
|
106
|
+
| `FoundationAiAssistant.blockedReason` | public getter **and setter** (`string \| null`) |
|
|
107
|
+
| `FoundationAiAssistant.setBlocked(blocked, reason?)` | public method |
|
|
108
|
+
| `part="blocked-banner"` | new template part (always rendered — see [Accessibility](#accessibility)) |
|
|
109
|
+
| `blocked` / `blockedReason` + `setBlocked` | session-slice state and action |
|
|
110
|
+
| per-vendor blocking (`blockedVendors`, `setVendorBlocked`, …) | see [Per-vendor budgets](#per-vendor-budgets) |
|
|
111
|
+
| `ChatDriverConfig.budgetExhaustedMessage` | new optional field — overrides the transcript copy |
|
|
112
|
+
|
|
113
|
+
The chat driver gained a typed `BudgetExhaustedError` branch that sits **before** the
|
|
114
|
+
transient-retry catch. It does not retry; it appends `DEFAULT_BUDGET_EXHAUSTED_MESSAGE`
|
|
115
|
+
|
|
116
|
+
> You've reached your AI usage limit. Contact your administrator to raise it.
|
|
117
|
+
|
|
118
|
+
to the transcript (or calls `failSubAgent('budget_exhausted')` when running as a sub-agent),
|
|
119
|
+
and ends the turn with `failureReason: 'budget-exhausted'`.
|
|
120
|
+
|
|
121
|
+
That copy is deliberately **vendor-neutral**, because the same bundle ships to
|
|
122
|
+
white-labelled deployments. Both surfaces are overridable, and you should override
|
|
123
|
+
both together or neither: the banner via `setBlocked(true, reason)`, the transcript
|
|
124
|
+
bubble via `ChatDriverConfig.budgetExhaustedMessage` (the assistant element passes
|
|
125
|
+
your `blockedReason` through automatically, falling back to the shipped default).
|
|
126
|
+
Overriding only the banner used to leave a host reading its own wording directly
|
|
127
|
+
above the shipped default.
|
|
128
|
+
|
|
129
|
+
### `setBlocked` and the reason
|
|
130
|
+
|
|
131
|
+
`reason` distinguishes three cases, not two:
|
|
132
|
+
|
|
133
|
+
| Call | Effect |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `setBlocked(true, 'text')` | Blocks and sets the explanation |
|
|
136
|
+
| `setBlocked(true)` — reason **omitted** | Blocks and **keeps** any existing explanation |
|
|
137
|
+
| `setBlocked(true, null)` | Blocks and clears the explanation (banner falls back to the default) |
|
|
138
|
+
| `setBlocked(false)` | Unblocks, always clears the explanation, **and clears every per-vendor latch** |
|
|
139
|
+
|
|
140
|
+
The "omitted keeps" rule is load-bearing. The block has two writers — you, and the
|
|
141
|
+
driver's automatic latch — and the driver's write can land *after* yours: the
|
|
142
|
+
`tool-loop-end` bus event is published from the driver's `finally`, i.e. before
|
|
143
|
+
`sendMessage()` resolves. Treating an omitted reason as "clear it" meant a host
|
|
144
|
+
that set detailed copy from the bus had it replaced with generic copy a moment
|
|
145
|
+
later, every time.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## The blocked state
|
|
150
|
+
|
|
151
|
+
`blocked` follows the existing `compacting` / `restoring` pattern — session-slice-backed, so
|
|
152
|
+
it stays in sync across pop-in/pop-out — with **two deliberate differences**:
|
|
153
|
+
|
|
154
|
+
1. **It renders alongside the transcript, not instead of it.** `compacting` and `restoring`
|
|
155
|
+
replace the message list with a status view, which is right for a transient rewrite where
|
|
156
|
+
history is mid-flight. A block is indefinite, so hiding the conversation behind it would
|
|
157
|
+
strand the user's work behind a wall they cannot clear. Instead a persistent banner
|
|
158
|
+
(`part="blocked-banner"`) sits above the composer, with the transcript fully readable.
|
|
159
|
+
2. **It is latched.** Nothing in the element clears it — not a new turn, not
|
|
160
|
+
**Clear** / **New chat** (`resetSession` explicitly preserves it: starting a new chat does
|
|
161
|
+
not refill the budget), not a pop-in/out. It stays set until the host writes
|
|
162
|
+
`blocked = false`.
|
|
163
|
+
|
|
164
|
+
While blocked: the composer textarea and send button are disabled, the attach button is
|
|
165
|
+
disabled, the suggestion chips are hidden, the placeholder swaps to
|
|
166
|
+
`AI usage limit reached`, no suggestion requests are issued (each one would be a
|
|
167
|
+
guaranteed `402`), and `send()` / `submitMessage()` refuse. `submitMessage` returns
|
|
168
|
+
`{ ok: false, errors: [<the blocked reason>] }` rather than the transient
|
|
169
|
+
`'Assistant is busy'`, so a caller looping on "busy" doesn't spin forever.
|
|
170
|
+
|
|
171
|
+
> **Unrelated fix riding along.** The attach button now shares the text area's full
|
|
172
|
+
> disable gate. It previously checked only `busy`, so `compacting`, `restoring` **and**
|
|
173
|
+
> `blocked` were all missing from it — meaning files could be attached during a
|
|
174
|
+
> compaction or a restore and then never sent, which was already a bug before this
|
|
175
|
+
> feature existed. If you relied on attaching mid-compaction, that stops working.
|
|
176
|
+
|
|
177
|
+
### Scope and lifetime
|
|
178
|
+
|
|
179
|
+
`blocked` lives on the **session store**, so it is per-`stateKey` and **in memory**.
|
|
180
|
+
Two consequences that you must design around rather than discover:
|
|
181
|
+
|
|
182
|
+
- **It does not survive `switchSession`.** Switching away tears the outgoing
|
|
183
|
+
session's store down completely, so switching back yields a fresh, unblocked
|
|
184
|
+
store. The old session's value is *not* retained.
|
|
185
|
+
- **It does not survive a page reload.** It is deliberately **not** in the
|
|
186
|
+
persisted session snapshot. The snapshot is long-lived, so a persisted latch
|
|
187
|
+
would outlive an out-of-band budget raise with no in-element way to clear it —
|
|
188
|
+
the user would come back to a permanently dead composer against a budget that
|
|
189
|
+
has headroom. Re-deriving the state is strictly safer than remembering it.
|
|
190
|
+
|
|
191
|
+
So the durable source of truth is **your pre-flight** (Option B below), which
|
|
192
|
+
should run on mount *and* on every session switch. The driver latch is a safety
|
|
193
|
+
net for the first `402` within a live session, not a store.
|
|
194
|
+
|
|
195
|
+
One thing the latch *does* cross: the `tool-loop-end` topic it rides is forwarded
|
|
196
|
+
on the **tab-scoped** channel, so a wall hit in a popped-out window locks the main
|
|
197
|
+
window's composer immediately too. That is correct for a shared spend cap and
|
|
198
|
+
consistent with how the halo and loop events already propagate.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Per-vendor budgets
|
|
203
|
+
|
|
204
|
+
The AI-spend budget is **per vendor**: each vendor gets its own cap (the
|
|
205
|
+
ai-service side counts `SpentUsdAnthropic` / `SpentUsdGemini` alongside the
|
|
206
|
+
untouched combined `SpentUsd`, which is now reporting-only). Internal
|
|
207
|
+
(`@genesis.global`) users and local development stay unlimited on every vendor.
|
|
208
|
+
So one vendor can be exhausted while another still has headroom, and the
|
|
209
|
+
assistant models exactly that.
|
|
210
|
+
|
|
211
|
+
**This is not breaking.** `blockedVendors` starts empty, and while it is empty
|
|
212
|
+
`blocked` reads exactly as it always did.
|
|
213
|
+
|
|
214
|
+
### What was added
|
|
215
|
+
|
|
216
|
+
| Addition | Kind |
|
|
217
|
+
| --- | --- |
|
|
218
|
+
| `FoundationAiAssistant.blockedVendors` | public getter (`readonly AIProviderType[]`) — no setter |
|
|
219
|
+
| `FoundationAiAssistant.isVendorBlocked(vendor)` | public method |
|
|
220
|
+
| `FoundationAiAssistant.setVendorBlocked(vendor, blocked, reason?)` | public method — mirrors `setBlocked` |
|
|
221
|
+
| `FoundationAiAssistant.reachableVendors` | public getter (`readonly AIProviderType[]`) |
|
|
222
|
+
| `blockedVendors` / `blockedVendorReasons` + `setVendorBlocked` / `setBlockedReason` | session-slice state and actions |
|
|
223
|
+
| `ChatDriverResult.budget.vendor` | new optional field — the typed vendor beside the existing `vendorLabel` |
|
|
224
|
+
| `VENDOR_LABELS`, `vendorTypeOfLabel` | exported from `@genesislcap/foundation-ai` |
|
|
225
|
+
| `.is-visible` / `.is-partial` | banner classes — `is-visible` whenever the banner has copy, `is-partial` layered on top for the some-vendors-walled state |
|
|
226
|
+
|
|
227
|
+
### `blocked` is now derived
|
|
228
|
+
|
|
229
|
+
It reads **"every vendor the registry can currently reach is walled"**, plus the
|
|
230
|
+
vendor-agnostic flag. Concretely:
|
|
231
|
+
|
|
232
|
+
- Reading `blocked` is unchanged for you.
|
|
233
|
+
- Writing `blocked = true` / `setBlocked(true, …)` still sets the **vendor-agnostic**
|
|
234
|
+
block — one pot, which is what a single-budget host means. It does not populate
|
|
235
|
+
`blockedVendors`.
|
|
236
|
+
- Writing `blocked = false` / `setBlocked(false)` now **also clears every
|
|
237
|
+
per-vendor latch**. That is deliberate: it is what Option B's pre-flight already
|
|
238
|
+
meant, so Option B code is unchanged. A host asserting "the wall is gone" must
|
|
239
|
+
not be overruled by a driver latch it cannot see.
|
|
240
|
+
|
|
241
|
+
### There is deliberately no "which vendor will the next turn use?"
|
|
242
|
+
|
|
243
|
+
And there will not be one — no `peekVendorForNextTurn()`. The provider is
|
|
244
|
+
resolved **per turn and per agent**: `activeProviderInput` may be an async
|
|
245
|
+
function of the turn's context, `OrchestratingDriver` picks its agent with an LLM
|
|
246
|
+
`classify()` call, and sub-agents resolve their own. Any pre-turn answer would be
|
|
247
|
+
a guess that is wrong precisely on the multi-agent hosts per-vendor budgets exist
|
|
248
|
+
for. Resolving against the registry *default* alone was also rejected: with a
|
|
249
|
+
mixed registry it locks the composer in the wrong direction.
|
|
250
|
+
|
|
251
|
+
The element asks the answerable question instead — which vendors are
|
|
252
|
+
**reachable**, read from the provider statuses it already loads — and locks only
|
|
253
|
+
when all of them are walled.
|
|
254
|
+
|
|
255
|
+
The one place the exact vendor *is* knowable is suggestions: both suggestion
|
|
256
|
+
paths call `providerRegistry.default()`. Suggestion chips therefore hide (and no
|
|
257
|
+
request is issued) only when the **default** vendor is walled.
|
|
258
|
+
|
|
259
|
+
### Partial exhaustion: the composer stays live
|
|
260
|
+
|
|
261
|
+
While at least one reachable vendor has headroom the composer stays enabled, the
|
|
262
|
+
placeholder stays the host's, and `send`/`submitMessage` keep working. The banner
|
|
263
|
+
appears and names the walled vendor:
|
|
264
|
+
|
|
265
|
+
> Anthropic's AI usage limit is reached ($500.00 of $500.00). Switch to Gemini to keep going.
|
|
266
|
+
|
|
267
|
+
A turn is therefore **allowed to run and fail** if it happens to route to the
|
|
268
|
+
walled vendor. That is intentional and honest — there is no auto-switching,
|
|
269
|
+
because silently moving the user to another vendor would silently downgrade model
|
|
270
|
+
quality. The banner advises; the user (or the host) switches.
|
|
271
|
+
|
|
272
|
+
When every reachable vendor is walled the banner asks for a raise instead:
|
|
273
|
+
|
|
274
|
+
> Anthropic's AI usage limit is reached ($25.40 of $25.00). Contact your administrator to raise it.
|
|
275
|
+
|
|
276
|
+
…and with several walled vendors carrying no copy of their own, the figures do not
|
|
277
|
+
fit one sentence, so it names them:
|
|
278
|
+
|
|
279
|
+
> AI usage limits are reached for Anthropic and Gemini. Contact your administrator to raise them.
|
|
280
|
+
|
|
281
|
+
A host `blockedReason` still outranks all of it, and `setVendorBlocked(v, true,
|
|
282
|
+
'…')` supplies per-vendor copy that is composed into the sentence — **however many
|
|
283
|
+
vendors are walled**. As soon as any walled vendor carries copy, the banner reads
|
|
284
|
+
one statement per vendor instead of the compact list above, so nothing you set is
|
|
285
|
+
dropped when a second vendor goes:
|
|
286
|
+
|
|
287
|
+
> Anthropic spend is capped for this desk. Gemini's AI usage limit is reached. Contact your administrator to raise them.
|
|
288
|
+
|
|
289
|
+
A walled vendor with no copy falls back to the boilerplate sentence in that form
|
|
290
|
+
(`Gemini's …` above), so the set of statements always covers every vendor named.
|
|
291
|
+
|
|
292
|
+
The **transcript bubble** stays vendor-neutral. The assistant element passes
|
|
293
|
+
`blockedReason ?? DEFAULT_BUDGET_EXHAUSTED_MESSAGE` as
|
|
294
|
+
`ChatDriverConfig.budgetExhaustedMessage` — deliberately NOT the composed banner
|
|
295
|
+
copy. Only the vendor-agnostic latch writes `blockedReason`, and its wording
|
|
296
|
+
carries no switch clause, so the composed "switch to Gemini" advice is
|
|
297
|
+
structurally incapable of reaching the transcript. Per-vendor copy lives in
|
|
298
|
+
`blockedVendorReasons` and reaches the banner alone.
|
|
299
|
+
|
|
300
|
+
Ordering alone would not be enough to guarantee this: the driver reads the value
|
|
301
|
+
once at construction, but `getOrCreateDriver` keys on the agents list, so an
|
|
302
|
+
agents swap *after* a wall rebuilds the driver and would re-snapshot whatever the
|
|
303
|
+
banner said at that moment. The split is deliberate — the transcript is a
|
|
304
|
+
permanent record of the turn, and advice that stops being true the moment the user
|
|
305
|
+
switches vendor belongs on the banner.
|
|
306
|
+
|
|
307
|
+
> **Copy change.** With a recognised vendor label on the `402`, the default
|
|
308
|
+
> full-exhaustion banner now names the vendor ("Anthropic's AI usage limit is
|
|
309
|
+
> reached (…)") where it previously said "AI usage limit reached (…)". The generic
|
|
310
|
+
> `DEFAULT_BUDGET_EXHAUSTED_MESSAGE` fallback (no figures, no vendor) is unchanged,
|
|
311
|
+
> as is `BLOCKED_PLACEHOLDER`.
|
|
312
|
+
|
|
313
|
+
### Vendor-switch recovery is automatic — with one host obligation
|
|
314
|
+
|
|
315
|
+
Recovery is a **derivation**, not a mutation. Switch your registry to another
|
|
316
|
+
vendor and the observable fires, the provider statuses reload, the walled vendor
|
|
317
|
+
drops out of `reachableVendors`, and `blocked` goes false — with the latch left
|
|
318
|
+
intact, so switching back re-derives the block. No host code, and nothing can
|
|
319
|
+
unblock mid-turn.
|
|
320
|
+
|
|
321
|
+
**This requires an `ObservableAIProviderRegistry`** (i.e. `registerAIProviders` /
|
|
322
|
+
`MutableAIProviderRegistry`), which the element feature-detects. A host with an
|
|
323
|
+
immutable registry that changes vendors some other way must call
|
|
324
|
+
`setVendorBlocked(vendor, false)` itself. That is the single host obligation this
|
|
325
|
+
feature adds.
|
|
326
|
+
|
|
327
|
+
Also note the fail-safe: if your providers do not implement the optional
|
|
328
|
+
`AIProvider.getStatus()`, `reachableVendors` is empty and **any** wall blocks
|
|
329
|
+
everything — identical to the pre-per-vendor behaviour, but with no per-vendor
|
|
330
|
+
benefit. A `logger.warn` fires the first time that happens.
|
|
331
|
+
|
|
332
|
+
### Attribution, and when it degrades
|
|
333
|
+
|
|
334
|
+
A wall is attributed by reverse-looking-up the `vendorLabel` the refused
|
|
335
|
+
transport stamped, via the shared `VENDOR_LABELS` map (which the transports now
|
|
336
|
+
read their labels from, so the two cannot drift). Where that label is unclaimed —
|
|
337
|
+
a white-labelled or multiplexing gateway fronting several upstreams behind one
|
|
338
|
+
transport — the `402`'s own `vendor` is tried next. Only if neither resolves does
|
|
339
|
+
the wall degrade to the **vendor-agnostic** block — safe, and exactly the old
|
|
340
|
+
behaviour.
|
|
341
|
+
|
|
342
|
+
Attribution does **not** depend on the proxy reporting figures. A figure-less
|
|
343
|
+
`402` (a bare gateway rejection, a code-only body, or a framed error whose figures
|
|
344
|
+
are `null`) still carries the vendor, so the wall lands on the transport that was
|
|
345
|
+
refused rather than on whatever the driver resolved on the previous turn.
|
|
346
|
+
|
|
347
|
+
### `otherVendorAvailable`: the one thing only the server knows
|
|
348
|
+
|
|
349
|
+
The registry tells this element which vendors **exist**. Nothing client-side can
|
|
350
|
+
tell it which vendors still have **budget**. So when the `402` reports
|
|
351
|
+
`otherVendorAvailable: false`, that verdict overrules the inference: every other
|
|
352
|
+
*budgeted* vendor is walled in the same pass, the composer locks on the first
|
|
353
|
+
response instead of after a second doomed turn, and the banner stops advising a
|
|
354
|
+
switch to a vendor whose pot is equally spent.
|
|
355
|
+
|
|
356
|
+
The sweep runs over `BUDGETED_VENDORS`, **not** over `reachableVendors`. The
|
|
357
|
+
verdict is a statement about the proxy's own pots, so it must not be scoped to a
|
|
358
|
+
registry snapshot that loads asynchronously and may not have arrived yet — a wall
|
|
359
|
+
that beat the statuses home would otherwise discard the verdict entirely, with
|
|
360
|
+
nothing to re-run it afterwards.
|
|
361
|
+
|
|
362
|
+
One consequence for hosts: `blockedVendors` may therefore list a vendor your
|
|
363
|
+
registry cannot reach. That is deliberate and costs nothing —
|
|
364
|
+
|
|
365
|
+
- `blocked` only asks whether every **reachable** vendor is walled, and
|
|
366
|
+
- the banner reads the reachability-filtered list, so it still names only vendors
|
|
367
|
+
you can route to (see [The banner names only vendors you can
|
|
368
|
+
reach](#the-banner-names-only-vendors-you-can-reach)).
|
|
369
|
+
|
|
370
|
+
…but if you render `blockedVendors` yourself, filter it against your own registry
|
|
371
|
+
rather than showing it raw.
|
|
372
|
+
|
|
373
|
+
One boundary remains: only `BUDGETED_VENDORS` are swept. A Chrome provider is
|
|
374
|
+
on-device and unmetered, so `Switch to Chrome to keep going.` remains honest.
|
|
375
|
+
|
|
376
|
+
An **absent** `otherVendorAvailable` (an older proxy) is not a `false` — the
|
|
377
|
+
client is back to inferring, and behaviour is exactly as before.
|
|
378
|
+
|
|
379
|
+
### The banner names only vendors you can reach
|
|
380
|
+
|
|
381
|
+
Both the walled list the banner names and the free list it offers are filtered by
|
|
382
|
+
`reachableVendors`. A wall the registry has since moved away from cannot be hit,
|
|
383
|
+
so it is neither named nor announced — a host that ships Anthropic-only, walls,
|
|
384
|
+
then swaps to Gemini does not get "limits are reached for Anthropic and Gemini"
|
|
385
|
+
in front of a user who never had an Anthropic key. When nothing is reachable the
|
|
386
|
+
filter falls back to the unfiltered list, matching the fail-safe `blocked` takes
|
|
387
|
+
there.
|
|
388
|
+
|
|
389
|
+
### `'none'` is not a vendor
|
|
390
|
+
|
|
391
|
+
`setVendorBlocked('none', …)` is rejected with a `logger.warn` and does nothing.
|
|
392
|
+
`'none'` is the no-provider sentinel: latching it would render "none's AI usage
|
|
393
|
+
limit is reached" and could never be released by derivation, because `'none'`
|
|
394
|
+
never appears in `reachableVendors`. Use `setBlocked` for a vendor-agnostic block.
|
|
395
|
+
|
|
396
|
+
### Scope and lifetime are unchanged
|
|
397
|
+
|
|
398
|
+
The per-vendor latches live where the boolean lives: **in memory**, on the
|
|
399
|
+
session store, per-`stateKey`. They do not survive `switchSession`, they do not
|
|
400
|
+
survive a page reload, and they are deliberately absent from the persisted
|
|
401
|
+
snapshot. Your pre-flight remains the durable source of truth.
|
|
402
|
+
|
|
403
|
+
### Styling
|
|
404
|
+
|
|
405
|
+
`.blocked-banner.is-visible` carries every visual property and applies whenever
|
|
406
|
+
the banner has something to say — including while only **some** vendors are walled
|
|
407
|
+
(banner visible, composer live). It is named for visibility rather than for
|
|
408
|
+
`blocked` precisely because that partial state has `blocked === false`. Add
|
|
409
|
+
`.is-partial` if you want a distinct treatment for it; it is layered on top and
|
|
410
|
+
sets nothing but a softer border by default.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## How to adopt
|
|
415
|
+
|
|
416
|
+
### Option A — do nothing
|
|
417
|
+
|
|
418
|
+
The assistant latches `blocked` itself when a turn ends with `'budget-exhausted'`, so a
|
|
419
|
+
host that does no budget checking at all gets a correct locked UI the moment the first
|
|
420
|
+
`402` lands, with no code change. It latches from **two** seams, because neither covers
|
|
421
|
+
everything:
|
|
422
|
+
|
|
423
|
+
- the `tool-loop-end` activity-bus detail — which also fires for a **sub-agent's** wall,
|
|
424
|
+
for a turn this element did not start, and for an element swapped in mid-turn
|
|
425
|
+
(pop-in/pop-out), and which crosses to popout windows on the tab-scoped channel;
|
|
426
|
+
- the driver's **return value** — which covers the one case the bus cannot: a wall hit
|
|
427
|
+
during multi-agent **classification**, where no tool loop ran and so no
|
|
428
|
+
`tool-loop-end` was published.
|
|
429
|
+
|
|
430
|
+
Both go through one internal decision point, so the latch is idempotent and never
|
|
431
|
+
unblocks.
|
|
432
|
+
|
|
433
|
+
**What the banner says.** With no host reason set, the banner shows whatever figures the
|
|
434
|
+
proxy reported and names the vendor that refused — `Anthropic's AI usage limit is reached
|
|
435
|
+
($25.40 of $25.00). Contact your administrator to raise it.` — falling back to the generic
|
|
436
|
+
default copy when the `402` carried no figures and no recognisable vendor. When another
|
|
437
|
+
vendor still has headroom the closing sentence becomes `Switch to Gemini to keep going.`
|
|
438
|
+
instead; see [Per-vendor budgets](#per-vendor-budgets). If you want host-specific wording
|
|
439
|
+
(a link to your billing page, a named contact), that is Option B.
|
|
440
|
+
|
|
441
|
+
### Option B — pre-flight (required if the block must survive a reload or session switch)
|
|
442
|
+
|
|
443
|
+
Because the latch is in-memory and per-`stateKey` (see [Scope and lifetime](#scope-and-lifetime)),
|
|
444
|
+
this is the only way to get a correct locked UI **on mount** and **after a session
|
|
445
|
+
switch**. Run it in both places. It also blocks up front, before the user composes a
|
|
446
|
+
message that is guaranteed to fail:
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
async function applyBudgetGate(assistantEl: FoundationAiAssistant) {
|
|
450
|
+
const { withinBudget, budgetUsd, spentUsd } = await checkAiBudget();
|
|
451
|
+
assistantEl.setBlocked(
|
|
452
|
+
!withinBudget,
|
|
453
|
+
withinBudget
|
|
454
|
+
? null
|
|
455
|
+
: `AI usage limit reached ($${spentUsd} of $${budgetUsd}). Contact Genesis to increase it.`,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// on mount…
|
|
460
|
+
await applyBudgetGate(assistantEl);
|
|
461
|
+
// …and in your own session-switch handler
|
|
462
|
+
await applyBudgetGate(assistantEl);
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Pass no `reason` to use the default copy:
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
assistantEl.blocked = true;
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Release it once a later check shows headroom (unblocking always clears the reason, so a
|
|
472
|
+
stale explanation can never outlive the condition):
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
assistantEl.blocked = false;
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Option C — branch on the typed failure
|
|
479
|
+
|
|
480
|
+
For custom UI, telemetry, or a host-owned composer:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
import { BudgetExhaustedError } from '@genesislcap/foundation-ai';
|
|
484
|
+
|
|
485
|
+
// …from a driver result — the figures now ride along, so you do not have to
|
|
486
|
+
// catch the transport error to get them:
|
|
487
|
+
if (result.reason === 'done' && result.failureReason === 'budget-exhausted') {
|
|
488
|
+
showUpgradePrompt({ budgetUsd: result.budget?.budgetUsd, spentUsd: result.budget?.spentUsd });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// …or from the `tool-loop-end` activity-bus detail, which also carries the vendor:
|
|
492
|
+
agenticActivityBus.subscribe('tool-loop-end', (d) => {
|
|
493
|
+
if (d?.failureReason === 'budget-exhausted') showUpgradePrompt({ vendor: d.vendor });
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// …or directly around a transport/provider call:
|
|
497
|
+
try {
|
|
498
|
+
await provider.chat(history, input);
|
|
499
|
+
} catch (e) {
|
|
500
|
+
if (e instanceof BudgetExhaustedError) {
|
|
501
|
+
showUpgradePrompt({ budgetUsd: e.budgetUsd, spentUsd: e.spentUsd });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
> If you set `blocked` from the bus subscription, note it fires **before**
|
|
507
|
+
> `sendMessage()` resolves — the element's own latch runs afterwards and is a
|
|
508
|
+
> no-op while already blocked, so your copy is preserved.
|
|
509
|
+
|
|
510
|
+
Note that `TurnFailureReason` and `SubAgentFailureReason` are **open unions that grew**. If
|
|
511
|
+
you `switch` exhaustively over either with a `never` guard, TypeScript will now flag the new
|
|
512
|
+
member — that is the intended prompt to decide how your app should treat it, not a breaking
|
|
513
|
+
change to runtime behaviour.
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Styling the banner
|
|
518
|
+
|
|
519
|
+
The banner is themed from the design system's own tokens and is addressable as a part:
|
|
520
|
+
|
|
521
|
+
```css
|
|
522
|
+
foundation-ai-assistant::part(blocked-banner) {
|
|
523
|
+
border-color: var(--my-warning-color);
|
|
524
|
+
background: var(--my-warning-surface);
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
It carries `role="status"` — a standing condition the user can also read off the disabled
|
|
529
|
+
composer, not an interruption worth an `alert`.
|
|
530
|
+
|
|
531
|
+
### Accessibility
|
|
532
|
+
|
|
533
|
+
The banner element is **always in the DOM**, and only its text changes when `blocked`
|
|
534
|
+
flips. This is deliberate and load-bearing: `role="status"` announces *mutations* to a
|
|
535
|
+
region the assistive technology is already observing, and a region inserted into the DOM
|
|
536
|
+
already carrying its text is widely missed across NVDA, JAWS and VoiceOver. Keeping it
|
|
537
|
+
mounted makes the text change the observable event.
|
|
538
|
+
|
|
539
|
+
Consequences for hosts:
|
|
540
|
+
|
|
541
|
+
- `::part(blocked-banner)` now matches at all times. While unblocked the element is empty
|
|
542
|
+
and carries no padding, border or background, so it occupies **zero** space — every
|
|
543
|
+
visible property hangs off an internal `is-visible` class. If you style the part, put
|
|
544
|
+
your visual overrides where they only apply to the populated state (e.g. keep using the
|
|
545
|
+
border/background properties, which the empty state does not set) rather than adding
|
|
546
|
+
`min-height` or `padding` unconditionally.
|
|
547
|
+
- The composer's text area additionally carries `aria-disabled="true"` and an `aria-label`
|
|
548
|
+
containing the blocked reason while blocked, so the control's own accessible name states
|
|
549
|
+
why it is unavailable — a disabled control leaves the tab order, so its swapped
|
|
550
|
+
placeholder is never voiced.
|
|
551
|
+
- The banner has an `id`, and the text area, **send** button and **attach** button carry
|
|
552
|
+
`aria-describedby` pointing at it whenever the banner is visible — including during
|
|
553
|
+
**partial** exhaustion, where the composer is deliberately still enabled. That state has
|
|
554
|
+
no other accessible signal: `aria-disabled`/`aria-label` are keyed on the full block, and
|
|
555
|
+
a live composer keeps your own placeholder, so a screen-reader user would otherwise reach
|
|
556
|
+
the textarea with nothing to say the next turn might be refused. `role="status"` announces
|
|
557
|
+
the banner text when it changes; the description is what makes the same explanation
|
|
558
|
+
reachable afterwards, on demand, from the control it is about. It is also what gives
|
|
559
|
+
send/attach any explanation at all — they carried none in either state.
|
|
560
|
+
|
|
561
|
+
The id is shadow-DOM-scoped, so it cannot collide with your page. If you re-template the
|
|
562
|
+
composer, keep the reference: `aria-describedby` resolves same-root only.
|
package/docs/sub_agent.md
CHANGED
|
@@ -139,10 +139,22 @@ type SubAgentFailureReason =
|
|
|
139
139
|
| 'max_iterations'
|
|
140
140
|
| 'malformed_tool_call'
|
|
141
141
|
| 'empty_response'
|
|
142
|
-
| 'unknown_tool_limit'
|
|
142
|
+
| 'unknown_tool_limit'
|
|
143
|
+
| 'timeout'
|
|
144
|
+
| 'response_truncated'
|
|
145
|
+
| 'refusal'
|
|
146
|
+
| 'budget_exhausted';
|
|
143
147
|
```
|
|
144
148
|
|
|
145
|
-
Branch on `ok` in the calling handler
|
|
149
|
+
Branch on `ok` in the calling handler — but **the right recovery depends on which
|
|
150
|
+
reason you got**, and the two groups are opposites:
|
|
151
|
+
|
|
152
|
+
| Reason | Recovery |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `max_iterations`, `malformed_tool_call`, `empty_response`, `unknown_tool_limit`, `timeout` | **Retry or ask the user.** These are transient or prompt-design problems; a second attempt, a smaller task, or a clarifying question often succeeds. |
|
|
155
|
+
| `response_truncated` | **Do not retry as-is** — re-issuing the same request hits the same output-token cap. Retry only with a smaller step, or raise the provider's `maxTokens`. |
|
|
156
|
+
| `refusal` | **Terminal.** A safety-classifier decline is deterministic; the same request will be declined again. Return a terse error. |
|
|
157
|
+
| `budget_exhausted` | **Terminal, and already handled for you.** The AI-spend budget is gone and the proxy is refusing every request. Retrying is another `402`, and "ask the user" is asking someone who cannot fix it. Return a terse error and let the turn end — the parent driver ends the turn as soon as this outcome comes back, so your handler will not be called again this turn. |
|
|
146
158
|
|
|
147
159
|
```ts
|
|
148
160
|
const outcome = await context.requestSubAgent<ExtractedTrades>('trade_file_extractor', {
|
|
@@ -150,7 +162,12 @@ const outcome = await context.requestSubAgent<ExtractedTrades>('trade_file_extra
|
|
|
150
162
|
});
|
|
151
163
|
|
|
152
164
|
if (!outcome.ok) {
|
|
153
|
-
//
|
|
165
|
+
// Terminal reasons: report and stop. Retrying reproduces them, and for
|
|
166
|
+
// budget_exhausted the parent turn is ending anyway.
|
|
167
|
+
if (outcome.reason === 'budget_exhausted' || outcome.reason === 'refusal') {
|
|
168
|
+
return { error: `Extraction could not run (${outcome.reason}).` };
|
|
169
|
+
}
|
|
170
|
+
// Everything else is worth another attempt or a question to the user.
|
|
154
171
|
return { error: `Extraction didn't complete (${outcome.reason}).` };
|
|
155
172
|
}
|
|
156
173
|
const { rows } = outcome.result; // fully typed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genesislcap/ai-assistant",
|
|
3
3
|
"description": "Genesis AI Assistant micro-frontend",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.7.0",
|
|
5
5
|
"license": "SEE LICENSE IN license.txt",
|
|
6
6
|
"main": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/ai-assistant.d.ts",
|
|
@@ -73,26 +73,26 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@genesislcap/foundation-testing": "15.
|
|
77
|
-
"@genesislcap/genx": "15.
|
|
78
|
-
"@genesislcap/rollup-builder": "15.
|
|
79
|
-
"@genesislcap/ts-builder": "15.
|
|
80
|
-
"@genesislcap/uvu-playwright-builder": "15.
|
|
81
|
-
"@genesislcap/vite-builder": "15.
|
|
82
|
-
"@genesislcap/webpack-builder": "15.
|
|
76
|
+
"@genesislcap/foundation-testing": "15.7.0",
|
|
77
|
+
"@genesislcap/genx": "15.7.0",
|
|
78
|
+
"@genesislcap/rollup-builder": "15.7.0",
|
|
79
|
+
"@genesislcap/ts-builder": "15.7.0",
|
|
80
|
+
"@genesislcap/uvu-playwright-builder": "15.7.0",
|
|
81
|
+
"@genesislcap/vite-builder": "15.7.0",
|
|
82
|
+
"@genesislcap/webpack-builder": "15.7.0",
|
|
83
83
|
"@types/dompurify": "^3.0.5",
|
|
84
84
|
"@types/marked": "^5.0.2",
|
|
85
85
|
"esbuild": "0.25.12"
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
-
"@genesislcap/foundation-ai": "15.
|
|
89
|
-
"@genesislcap/foundation-logger": "15.
|
|
90
|
-
"@genesislcap/foundation-notifications": "15.
|
|
91
|
-
"@genesislcap/foundation-redux": "15.
|
|
92
|
-
"@genesislcap/foundation-ui": "15.
|
|
93
|
-
"@genesislcap/foundation-utils": "15.
|
|
94
|
-
"@genesislcap/rapid-design-system": "15.
|
|
95
|
-
"@genesislcap/web-core": "15.
|
|
88
|
+
"@genesislcap/foundation-ai": "15.7.0",
|
|
89
|
+
"@genesislcap/foundation-logger": "15.7.0",
|
|
90
|
+
"@genesislcap/foundation-notifications": "15.7.0",
|
|
91
|
+
"@genesislcap/foundation-redux": "15.7.0",
|
|
92
|
+
"@genesislcap/foundation-ui": "15.7.0",
|
|
93
|
+
"@genesislcap/foundation-utils": "15.7.0",
|
|
94
|
+
"@genesislcap/rapid-design-system": "15.7.0",
|
|
95
|
+
"@genesislcap/web-core": "15.7.0",
|
|
96
96
|
"dompurify": "^3.3.1",
|
|
97
97
|
"marked": "^17.0.3"
|
|
98
98
|
},
|
|
@@ -105,5 +105,5 @@
|
|
|
105
105
|
"access": "public"
|
|
106
106
|
},
|
|
107
107
|
"customElements": "dist/custom-elements.json",
|
|
108
|
-
"gitHead": "
|
|
108
|
+
"gitHead": "485a2cc9d4b8695ea2e57a9cc66cad55effb2184"
|
|
109
109
|
}
|