@genesislcap/ai-assistant 15.10.6 → 15.12.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 +226 -0
- package/dist/ai-assistant.d.ts +100 -3
- package/dist/chat-driver.cjs +540 -117
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +524 -116
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +439 -66
- package/dist/dts/chat-driver-node.d.ts +5 -2
- package/dist/dts/chat-driver-node.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +20 -3
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +2 -0
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +2 -0
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +1 -0
- package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts +1 -1
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +39 -2
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/config/define-stateful-agent.d.ts +15 -1
- package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +25 -0
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/react.d.ts +9 -8
- package/dist/dts/utils/strip-agent-handlers.d.ts +1 -1
- package/dist/dts/utils/sum-usage.d.ts +37 -4
- package/dist/dts/utils/sum-usage.d.ts.map +1 -1
- package/dist/dts/utils/usage-rows.d.ts +102 -0
- package/dist/dts/utils/usage-rows.d.ts.map +1 -0
- package/dist/dts/utils/usage-rows.test.d.ts +2 -0
- package/dist/dts/utils/usage-rows.test.d.ts.map +1 -0
- package/dist/esm/chat-driver-node.js +36 -1
- package/dist/esm/components/chat-driver/chat-driver.js +73 -10
- package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +137 -0
- package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +200 -0
- package/dist/esm/components/settings-modal/settings-modal.styles.js +22 -0
- package/dist/esm/components/settings-modal/settings-modal.template.js +25 -4
- package/dist/esm/components/settings-modal/settings-modal.template.test.js +1 -1
- package/dist/esm/config/define-stateful-agent.js +11 -0
- package/dist/esm/main/blocked-state.test.js +1 -0
- package/dist/esm/main/budget-meter.test.js +42 -0
- package/dist/esm/main/main.js +49 -0
- package/dist/esm/main/main.template.js +20 -1
- package/dist/esm/utils/strip-agent-handlers.js +1 -1
- package/dist/esm/utils/sum-usage.js +37 -4
- package/dist/esm/utils/usage-rows.js +90 -0
- package/dist/esm/utils/usage-rows.test.js +189 -0
- package/dist/react.cjs +11 -6
- package/dist/react.mjs +10 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -17
- package/src/chat-driver-node.ts +58 -0
- package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +185 -0
- package/src/components/chat-driver/chat-driver.trace-capture.test.ts +251 -0
- package/src/components/chat-driver/chat-driver.ts +90 -10
- package/src/components/settings-modal/settings-modal.styles.ts +22 -0
- package/src/components/settings-modal/settings-modal.template.test.ts +1 -1
- package/src/components/settings-modal/settings-modal.template.ts +27 -2
- package/src/config/config.ts +50 -1
- package/src/config/define-stateful-agent.ts +37 -0
- package/src/main/blocked-state.test.ts +1 -0
- package/src/main/budget-meter.test.ts +50 -0
- package/src/main/main.template.ts +19 -1
- package/src/main/main.ts +47 -0
- package/src/utils/strip-agent-handlers.ts +1 -1
- package/src/utils/sum-usage.ts +37 -4
- package/src/utils/usage-rows.test.ts +237 -0
- package/src/utils/usage-rows.ts +187 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-rows.test.d.ts","sourceRoot":"","sources":["../../../src/utils/usage-rows.test.ts"],"names":[],"mappings":""}
|
|
@@ -35,7 +35,42 @@ export { AnthropicProvider, AnthropicTransport, GeminiProvider, GeminiTransport,
|
|
|
35
35
|
// foundation-ai's REAL list instead of maintaining a parallel hardcoded copy that silently drifts
|
|
36
36
|
// as the platform adds models (a valid new model would otherwise look like a misconfiguration).
|
|
37
37
|
// GENC-1461, create PR #1644 review.
|
|
38
|
-
SUPPORTED_ANTHROPIC_MODEL_IDS, SUPPORTED_GEMINI_MODEL_IDS, isObservableAIProviderRegistry,
|
|
38
|
+
SUPPORTED_ANTHROPIC_MODEL_IDS, SUPPORTED_GEMINI_MODEL_IDS, isObservableAIProviderRegistry,
|
|
39
|
+
// Request pricing. Re-exported HERE, not left to a direct `@genesislcap/foundation-ai`
|
|
40
|
+
// import, for the same reason as everything above it: a headless host that adds its own
|
|
41
|
+
// foundation-ai dependency gets a second copy, and the driver's malformed/truncated
|
|
42
|
+
// handling does `instanceof` on the transports' error classes. Reaching the pricing
|
|
43
|
+
// should not cost a consumer that guarantee.
|
|
44
|
+
//
|
|
45
|
+
// These take RAW provider usage rather than a `ChatMessage` — see the foundation-ai
|
|
46
|
+
// module docs. They are for the path where nothing stamped a cost (a proxied or
|
|
47
|
+
// server-side call holding a usage block); when a message already carries `cost`, use
|
|
48
|
+
// it, because it was computed with the per-TTL cache split the message no longer has.
|
|
49
|
+
ANTHROPIC_CACHE_READ_MULTIPLIER, ANTHROPIC_CACHE_WRITE_1H_MULTIPLIER, ANTHROPIC_CACHE_WRITE_5M_MULTIPLIER, anthropicRatesFor, anthropicTokenCost, GEMINI_CACHED_INPUT_MULTIPLIER, GEMINI_LONG_CONTEXT_THRESHOLD, geminiRatesFor, geminiTokenCost,
|
|
50
|
+
// Resolves a model id to its vendor, so a caller holding only `message.model` can pick
|
|
51
|
+
// between the two cost functions above without maintaining its own model→vendor map.
|
|
52
|
+
vendorOfModel, } from '@genesislcap/foundation-ai';
|
|
53
|
+
// Usage/cost summation. A headless consumer attributing spend to a run has no other route to it:
|
|
54
|
+
// the transports' `getLifetimeCost()` is per-instance and lifetime-scoped, so a shared registry
|
|
55
|
+
// makes it unattributable, and re-deriving cost from token counts is wrong in the one direction
|
|
56
|
+
// that matters (`inputTokens` is total prompt size INCLUDING the cached part, so re-pricing charges
|
|
57
|
+
// every cached token at full rate). `sumUsage` reads the per-request cost the transports already
|
|
58
|
+
// stamped, and recurses into `toolCalls[].subAgentTrace` — where the entire spend of a delegating
|
|
59
|
+
// agent lives — and into `compaction.rolledUpUsage`, which is spend whose messages no longer exist.
|
|
60
|
+
// Both are invisible when omitted: the total is merely too low, which reads as a cheap run.
|
|
61
|
+
//
|
|
62
|
+
// `addUsage`/`emptyUsage` are the composition primitives for a conversation that CONTINUES across
|
|
63
|
+
// rounds — see the guidance on `sumUsage` itself. `totalTokens` exists because the four buckets of
|
|
64
|
+
// an `AggregateUsage` are disjoint and safe to add, which the per-message fields are NOT.
|
|
65
|
+
export { addUsage, emptyUsage, sumUsage, totalTokens } from './utils/sum-usage';
|
|
66
|
+
// Per-call projection, for the cases an aggregate cannot serve: usage rows for
|
|
67
|
+
// per-project attribution, a cost dashboard, or auditing which turns came back
|
|
68
|
+
// unpriced. `usageRows` is guaranteed to reconcile with `sumUsage` — including spend a
|
|
69
|
+
// compaction banked, which has no message of its own and which a naive per-message walk
|
|
70
|
+
// therefore drops while the total keeps it. Both take RAW history; `UsageRow` is
|
|
71
|
+
// deliberately not a `ChatMessage`, so passing rows back into `sumUsage` (which recurses
|
|
72
|
+
// itself, and would double-count) does not compile.
|
|
73
|
+
export { usageRows } from './utils/usage-rows';
|
|
39
74
|
// Debug-log harvesting (GENC-1461 unified diagnostics). A headless consumer collates its driver's
|
|
40
75
|
// log — `buildTimelineEntries({ turnSnapshots: driver.getTurnSnapshots(), messages:
|
|
41
76
|
// driver.getHistory(), metaEvents: getMetaEvents(sessionKey) })` → `DiagnosticEntry[]` — ships it to
|
|
@@ -584,6 +584,7 @@ export class ChatDriver extends EventTarget {
|
|
|
584
584
|
this.activeTemperatureInput = config.temperature;
|
|
585
585
|
this.activeToolChoiceInput = config.toolChoice;
|
|
586
586
|
this.activeCachePolicyInput = config.cachePolicy;
|
|
587
|
+
this.activeThinkingPolicyInput = config.thinkingPolicy;
|
|
587
588
|
this.activeTailContextInput = config.tailContext;
|
|
588
589
|
this.activeResponseSchemaInput = config.responseSchema;
|
|
589
590
|
this.activeFallbacks = config.fallbacks;
|
|
@@ -1403,15 +1404,29 @@ export class ChatDriver extends EventTarget {
|
|
|
1403
1404
|
* `condenseWhen` can register against the right call (it stamps the clocks
|
|
1404
1405
|
* straight off the driver). Absent for dispatch paths with no addressable tool
|
|
1405
1406
|
* call (e.g. the fold-close handler), where `condenseWhen` is a no-op.
|
|
1406
|
-
* @param traceCapture - Optional per-
|
|
1407
|
-
*
|
|
1408
|
-
* so parallel tool calls each capture their own
|
|
1407
|
+
* @param traceCapture - Optional per-tool-call accumulator. When provided, every
|
|
1408
|
+
* sub-agent call's trace is **appended** here rather than written to shared
|
|
1409
|
+
* instance state, so parallel tool calls each capture their own traces
|
|
1410
|
+
* independently.
|
|
1411
|
+
*
|
|
1412
|
+
* Deliberately a list of traces, not one slot. A single handler may call
|
|
1413
|
+
* `requestSubAgent` more than once — a code-driven scheduler dispatching a
|
|
1414
|
+
* dependency graph, a retry of a timed-out child, any fan-out helper — and a
|
|
1415
|
+
* single slot kept only the last, so every other child ran, was billed by the
|
|
1416
|
+
* provider, and then vanished from history. The loss was silent in the worst
|
|
1417
|
+
* way: `sumUsage` and `usageRows` both recurse into the trace, so they summed a
|
|
1418
|
+
* truncated input and still agreed with each other. Measured at 1 of 7 traces
|
|
1419
|
+
* kept on a seven-way fan-out, reporting ~2.3x under the real cost, with the
|
|
1420
|
+
* error growing as the fan-out widens.
|
|
1409
1421
|
*/
|
|
1410
1422
|
buildHandlerContext(activeToolCallId, traceCapture) {
|
|
1411
1423
|
return Object.assign(Object.assign({ requestInteraction: (componentName, data, options) => this.requestInteraction(componentName, data, options) }, (this.subAgentsMap.size > 0 && {
|
|
1412
1424
|
requestSubAgent: (name, options) => this.invokeSubAgent(name, options).then(({ outcome, trace }) => {
|
|
1413
|
-
|
|
1414
|
-
|
|
1425
|
+
// Append, never assign: see `traceCapture` on `buildHandlerContext`. Order
|
|
1426
|
+
// is completion order, which is fine — every message carries its own
|
|
1427
|
+
// timestamp and the timeline sorts on that.
|
|
1428
|
+
if (traceCapture && trace)
|
|
1429
|
+
traceCapture.traces.push(trace);
|
|
1415
1430
|
return outcome;
|
|
1416
1431
|
}),
|
|
1417
1432
|
})), { completeSubAgent: (result) => {
|
|
@@ -1908,6 +1923,18 @@ export class ChatDriver extends EventTarget {
|
|
|
1908
1923
|
// `iterations` because fold operations decrement iterations, which would incorrectly
|
|
1909
1924
|
// re-trigger the slice on subsequent calls after a fold open/close.
|
|
1910
1925
|
let firstLlmCall = !!currentInput;
|
|
1926
|
+
// Thinking posture for this WHOLE turn, resolved on the first model call and then held.
|
|
1927
|
+
//
|
|
1928
|
+
// Unlike temperature or toolChoice, this one cannot vary per iteration: a tool-use loop is
|
|
1929
|
+
// a single assistant turn, and Anthropic requires one thinking mode for its duration.
|
|
1930
|
+
// Toggling mid-loop does not error — the API silently disables thinking for that request
|
|
1931
|
+
// and strips blocks that would leave the turn structure invalid, so an `'auto' -> 'off'`
|
|
1932
|
+
// switch loses the reasoning continuity the opening call established while an
|
|
1933
|
+
// `'off' -> 'auto'` switch simply does not deliver the reasoning asked for. It also
|
|
1934
|
+
// invalidates the prompt cache, which costs more than the reasoning it was meant to save.
|
|
1935
|
+
// Resolved per USER turn instead, which is where the docs say to choose it.
|
|
1936
|
+
let pinnedThinkingPolicy;
|
|
1937
|
+
let thinkingPolicyPinned = false;
|
|
1911
1938
|
while (iterations < this.maxToolIterations) {
|
|
1912
1939
|
iterations += 1;
|
|
1913
1940
|
// Monotonic across the driver's life — the age clock. Unlike `iterations`,
|
|
@@ -2071,7 +2098,7 @@ export class ChatDriver extends EventTarget {
|
|
|
2071
2098
|
// provider is resolved — static value or a function of the turn context
|
|
2072
2099
|
// (which carries the live state for stateful agents). Resolved before the
|
|
2073
2100
|
// snapshot so the debug log records the exact request config the model saw.
|
|
2074
|
-
const [resolvedTemperature, resolvedToolChoice, resolvedCachePolicy, resolvedTailContext, resolvedResponseSchema,] =
|
|
2101
|
+
const [resolvedTemperature, resolvedToolChoice, resolvedCachePolicy, resolvedTailContext, resolvedResponseSchema, firstResolvedThinkingPolicy,] =
|
|
2075
2102
|
// oxlint-disable-next-line no-await-in-loop
|
|
2076
2103
|
yield Promise.all([
|
|
2077
2104
|
this.resolveTurnInput(this.activeTemperatureInput, promptCtx),
|
|
@@ -2079,7 +2106,16 @@ export class ChatDriver extends EventTarget {
|
|
|
2079
2106
|
this.resolveTurnInput(this.activeCachePolicyInput, promptCtx),
|
|
2080
2107
|
this.resolveTurnInput(this.activeTailContextInput, promptCtx),
|
|
2081
2108
|
this.resolveTurnInput(this.activeResponseSchemaInput, promptCtx),
|
|
2109
|
+
// Only consulted on the first iteration (see `pinnedThinkingPolicy`); resolved
|
|
2110
|
+
// alongside the others so a resolver still sees the same turn context.
|
|
2111
|
+
thinkingPolicyPinned
|
|
2112
|
+
? Promise.resolve(undefined)
|
|
2113
|
+
: this.resolveTurnInput(this.activeThinkingPolicyInput, promptCtx),
|
|
2082
2114
|
]);
|
|
2115
|
+
if (!thinkingPolicyPinned) {
|
|
2116
|
+
pinnedThinkingPolicy = firstResolvedThinkingPolicy;
|
|
2117
|
+
thinkingPolicyPinned = true;
|
|
2118
|
+
}
|
|
2083
2119
|
// The system prompt is always just the agent's resolved prompt — byte-stable, so it can be
|
|
2084
2120
|
// cached. The framework's volatile additions (fold suffix, retry nudge) and the agent's tail
|
|
2085
2121
|
// context all go to the framed tail: one uniform channel, no cache-scope branch. On a normal
|
|
@@ -2124,6 +2160,12 @@ export class ChatDriver extends EventTarget {
|
|
|
2124
2160
|
// Prompt-cache policy for this turn (Anthropic places breakpoints per scope; Gemini
|
|
2125
2161
|
// caches implicitly regardless). Undefined → no caching requested.
|
|
2126
2162
|
cachePolicy: resolvedCachePolicy,
|
|
2163
|
+
// Extended-thinking posture, pinned for the whole tool loop (one assistant turn) rather
|
|
2164
|
+
// than re-resolved per iteration — see `pinnedThinkingPolicy`. Undefined — unset, or the
|
|
2165
|
+
// resolver's answer for this turn — is NOT "off": it leaves the model on its own default,
|
|
2166
|
+
// so agents that never set this are priced exactly as before. Transports clamp models
|
|
2167
|
+
// that can't honour it.
|
|
2168
|
+
thinkingPolicy: pinnedThinkingPolicy,
|
|
2127
2169
|
// Framed volatile context injected at the message tail (never stored). Undefined → none.
|
|
2128
2170
|
tailContext,
|
|
2129
2171
|
// Structured-output schema for this turn (agent/state-resolved). When set, the transport
|
|
@@ -2303,8 +2345,15 @@ export class ChatDriver extends EventTarget {
|
|
|
2303
2345
|
// leaves the key off entirely rather than carrying it as `undefined`.
|
|
2304
2346
|
// JSON.stringify already drops undefined from the exported log, so this is
|
|
2305
2347
|
// chiefly about keeping the in-memory message shape honest.
|
|
2306
|
-
|
|
2348
|
+
// Fill, never overwrite — but still only when there is something to fill with.
|
|
2349
|
+
// A transport that already stamped a model knows something the driver does not:
|
|
2350
|
+
// `lastResolvedModel` is the model we ASKED for, so overwriting would relabel a
|
|
2351
|
+
// fallback-served turn as the requested model and misattribute its spend.
|
|
2352
|
+
// Written as a guard rather than `??=` because `??=` ASSIGNS undefined, which
|
|
2353
|
+
// would create the key and break the omit-when-unresolved contract above.
|
|
2354
|
+
if (response.model === undefined && this.lastResolvedModel !== undefined) {
|
|
2307
2355
|
response.model = this.lastResolvedModel;
|
|
2356
|
+
}
|
|
2308
2357
|
if (this.lastResolvedProvider !== undefined)
|
|
2309
2358
|
response.provider = this.lastResolvedProvider;
|
|
2310
2359
|
if (this.lastResolvedProviderName !== undefined) {
|
|
@@ -2517,15 +2566,26 @@ export class ChatDriver extends EventTarget {
|
|
|
2517
2566
|
}
|
|
2518
2567
|
return;
|
|
2519
2568
|
}
|
|
2520
|
-
// Real tool execution
|
|
2569
|
+
// Real tool execution.
|
|
2570
|
+
//
|
|
2571
|
+
// The accumulator is declared OUTSIDE the try so the catch can attach it
|
|
2572
|
+
// too: a handler that ran sub-agents and then threw (post-processing their
|
|
2573
|
+
// results failed, say) has already spent real money on children that
|
|
2574
|
+
// completed. Losing their traces on the error path would under-report the
|
|
2575
|
+
// run exactly as the single-slot bug did — and just as silently, since
|
|
2576
|
+
// `sumUsage` and `usageRows` would still agree with each other.
|
|
2577
|
+
const traceCapture = { traces: [] };
|
|
2578
|
+
const capturedTrace = () => traceCapture.traces.length ? traceCapture.traces.flat() : undefined;
|
|
2521
2579
|
try {
|
|
2522
|
-
const traceCapture = {};
|
|
2523
2580
|
const result = yield handler(tc.args, this.buildHandlerContext(tc.id, traceCapture));
|
|
2524
2581
|
const content = typeof result === 'string' ? result : JSON.stringify(result);
|
|
2525
2582
|
executedById.set(tc.id, {
|
|
2526
2583
|
toolCallId: tc.id,
|
|
2527
2584
|
content,
|
|
2528
|
-
|
|
2585
|
+
// Concatenated when a handler invoked several children, so none is lost.
|
|
2586
|
+
// Stays `undefined` when nothing was captured — readers key off presence,
|
|
2587
|
+
// and an empty array is a different claim from "no sub-agent ran".
|
|
2588
|
+
subAgentTrace: capturedTrace(),
|
|
2529
2589
|
});
|
|
2530
2590
|
anyRealToolExecuted = true;
|
|
2531
2591
|
}
|
|
@@ -2541,6 +2601,9 @@ export class ChatDriver extends EventTarget {
|
|
|
2541
2601
|
// Structured recovery hint so the model retries or routes around a tool
|
|
2542
2602
|
// failure instead of apologising and giving up.
|
|
2543
2603
|
content: `Tool error: ${e.message}\nRECOVERY: this tool failed once — you may retry it, or take a different valid action to make progress. Do NOT abandon the task, ask the user to rephrase, or claim you cannot make changes. If a planning tool failed, retry it or proceed with the information you already have.`,
|
|
2604
|
+
// Children that completed before the throw were still billed — keep
|
|
2605
|
+
// their traces so the run's cost stays whole.
|
|
2606
|
+
subAgentTrace: capturedTrace(),
|
|
2544
2607
|
});
|
|
2545
2608
|
anyRealToolExecuted = true; // treat errors as real work for fold op counting
|
|
2546
2609
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { __awaiter } from "tslib";
|
|
2
|
+
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
+
// Side-effect import — MUST come before `./chat-driver` so the driver subclasses
|
|
4
|
+
// jsdom's EventTarget rather than Node's native one. Mirrors chat-driver.test.ts.
|
|
5
|
+
import './align-event-globals';
|
|
6
|
+
import { ChatDriver } from './chat-driver';
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Per-turn `thinkingPolicy` resolution.
|
|
9
|
+
//
|
|
10
|
+
// The cost case for this option is the SHAPE of a tool loop, not a single call: the
|
|
11
|
+
// opening turn is a real decision and worth reasoning over, while the iterations that
|
|
12
|
+
// follow mostly pick the next tool from a narrow set — and on a thinking model each of
|
|
13
|
+
// those bills reasoning at the full output rate. That saving only exists if the
|
|
14
|
+
// resolver runs per iteration, so these tests assert the sequence across a loop, not
|
|
15
|
+
// just that one value arrives.
|
|
16
|
+
//
|
|
17
|
+
// The other half is the undefined case. Every agent written before this option leaves
|
|
18
|
+
// it unset, and a resolver may answer `undefined` on any given turn; both must reach
|
|
19
|
+
// the transport as `undefined` so the model keeps its own default. `undefined` reaching
|
|
20
|
+
// the wire as `'off'` would be a silent capability regression, and as `'auto'` a silent
|
|
21
|
+
// bill increase — so it is asserted explicitly rather than assumed.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/** Captures the policy seen on each model call; calls one tool, then finishes. */
|
|
24
|
+
const capturingProvider = () => {
|
|
25
|
+
const seen = [];
|
|
26
|
+
let turns = 0;
|
|
27
|
+
return {
|
|
28
|
+
seen,
|
|
29
|
+
chat: (_history, _userMessage, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
|
+
seen.push(options === null || options === void 0 ? void 0 : options.thinkingPolicy);
|
|
31
|
+
turns += 1;
|
|
32
|
+
// Two tool calls, so the loop runs three model calls in total — enough for a
|
|
33
|
+
// per-turn resolver to say something different on the later ones.
|
|
34
|
+
if (turns <= 2) {
|
|
35
|
+
return {
|
|
36
|
+
role: 'assistant',
|
|
37
|
+
content: '',
|
|
38
|
+
toolCalls: [{ id: `t${turns}`, name: 'step', args: {} }],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { role: 'assistant', content: 'done' };
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const makeRegistry = (provider) => ({
|
|
46
|
+
get: () => provider,
|
|
47
|
+
default: () => provider,
|
|
48
|
+
defaultName: () => 'test',
|
|
49
|
+
names: () => ['test'],
|
|
50
|
+
getStatus: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
|
|
51
|
+
listStatuses: () => __awaiter(void 0, void 0, void 0, function* () { return []; }),
|
|
52
|
+
});
|
|
53
|
+
const agent = (overrides) => (Object.assign({ name: 'worker', description: 'test agent', toolDefinitions: [
|
|
54
|
+
{ name: 'step', description: 'step', parameters: { type: 'object', properties: {} } },
|
|
55
|
+
], toolHandlers: { step: () => __awaiter(void 0, void 0, void 0, function* () { return 'stepped'; }) } }, overrides));
|
|
56
|
+
/** Run one user turn through a driver carrying `config`, and return the policies seen. */
|
|
57
|
+
const policiesFor = (config) => __awaiter(void 0, void 0, void 0, function* () {
|
|
58
|
+
const provider = capturingProvider();
|
|
59
|
+
const driver = new ChatDriver(makeRegistry(provider), {
|
|
60
|
+
maxToolIterations: 10,
|
|
61
|
+
maxFoldOperations: 5,
|
|
62
|
+
sessionKey: '',
|
|
63
|
+
});
|
|
64
|
+
driver.applyAgent(agent(config));
|
|
65
|
+
yield driver.sendMessage('go');
|
|
66
|
+
return provider.seen;
|
|
67
|
+
});
|
|
68
|
+
const suite = createLogicSuite('ChatDriver thinkingPolicy');
|
|
69
|
+
suite('leaves the policy undefined when the agent does not set one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
+
// The compatibility case: every existing agent. Undefined must reach the transport
|
|
71
|
+
// as undefined so each model keeps its own default posture — not silently coerced
|
|
72
|
+
// to 'off' (a capability regression) or 'auto' (a bill increase).
|
|
73
|
+
const seen = yield policiesFor({});
|
|
74
|
+
assert.ok(seen.length >= 3, `expected a multi-call loop, got ${seen.length}`);
|
|
75
|
+
assert.equal(seen.filter((p) => p !== undefined), [], 'no turn invents a policy');
|
|
76
|
+
}));
|
|
77
|
+
suite('applies a static policy to every turn of the loop', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
const seen = yield policiesFor({ thinkingPolicy: 'off' });
|
|
79
|
+
assert.ok(seen.length >= 3);
|
|
80
|
+
assert.equal([...new Set(seen)], ['off'], 'a static value is not just a first-turn setting');
|
|
81
|
+
}));
|
|
82
|
+
suite('pins the resolved policy for the whole tool loop', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
83
|
+
// A tool-use loop is ONE assistant turn and Anthropic requires a single thinking mode
|
|
84
|
+
// for its duration. Toggling part-way does not error — the API silently disables
|
|
85
|
+
// thinking for that request and strips blocks that would leave the turn structure
|
|
86
|
+
// invalid, so an 'auto' -> 'off' switch loses the continuity the opening call
|
|
87
|
+
// established and 'off' -> 'auto' never delivers the reasoning asked for. It also
|
|
88
|
+
// invalidates the prompt cache, costing more than the reasoning it meant to save.
|
|
89
|
+
//
|
|
90
|
+
// So a resolver that changes its mind mid-loop must NOT be honoured mid-loop. This
|
|
91
|
+
// asserts the opposite of what it looks like it should: the later values are ignored.
|
|
92
|
+
let call = 0;
|
|
93
|
+
const seen = yield policiesFor({
|
|
94
|
+
thinkingPolicy: () => {
|
|
95
|
+
call += 1;
|
|
96
|
+
return call === 1 ? 'auto' : 'off';
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
assert.ok(seen.length >= 3, `expected a multi-call loop, got ${seen.length}`);
|
|
100
|
+
assert.equal([...new Set(seen)], ['auto'], 'the first call decides; later resolutions do not take effect until the next user turn');
|
|
101
|
+
}));
|
|
102
|
+
suite('re-resolves on the next user turn', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
103
|
+
// The flip side: pinning is per turn, not for the driver's lifetime, so a state change
|
|
104
|
+
// between turns still lands.
|
|
105
|
+
const provider = capturingProvider();
|
|
106
|
+
const driver = new ChatDriver(makeRegistry(provider), {
|
|
107
|
+
maxToolIterations: 10,
|
|
108
|
+
maxFoldOperations: 5,
|
|
109
|
+
sessionKey: '',
|
|
110
|
+
});
|
|
111
|
+
let turn = 0;
|
|
112
|
+
driver.applyAgent(agent({
|
|
113
|
+
thinkingPolicy: () => (turn === 0 ? 'auto' : 'off'),
|
|
114
|
+
}));
|
|
115
|
+
yield driver.sendMessage('go');
|
|
116
|
+
const firstTurn = [...provider.seen];
|
|
117
|
+
turn = 1;
|
|
118
|
+
yield driver.sendMessage('again');
|
|
119
|
+
const secondTurn = provider.seen.slice(firstTurn.length);
|
|
120
|
+
assert.equal([...new Set(firstTurn)], ['auto'], 'turn one holds its posture');
|
|
121
|
+
assert.equal([...new Set(secondTurn)], ['off'], 'turn two picks up the new one');
|
|
122
|
+
}));
|
|
123
|
+
suite('passes undefined through when the resolver declines to choose', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
// A resolver may answer on some turns and not others. `undefined` is its third answer —
|
|
125
|
+
// "leave this model alone" — and must not be normalised into a value. Asserted across
|
|
126
|
+
// the whole loop because the first call's answer is the one that gets pinned.
|
|
127
|
+
const seen = yield policiesFor({ thinkingPolicy: () => undefined });
|
|
128
|
+
assert.ok(seen.length >= 3);
|
|
129
|
+
assert.equal(seen.filter((p) => p !== undefined), [], 'declining is not the same as choosing');
|
|
130
|
+
}));
|
|
131
|
+
suite('awaits an async resolver', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
132
|
+
const seen = yield policiesFor({
|
|
133
|
+
thinkingPolicy: () => __awaiter(void 0, void 0, void 0, function* () { return 'off'; }),
|
|
134
|
+
});
|
|
135
|
+
assert.equal([...new Set(seen)], ['off'], 'a promise is resolved, not passed through');
|
|
136
|
+
}));
|
|
137
|
+
suite.run();
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { __awaiter } from "tslib";
|
|
2
|
+
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
+
import { sumUsage } from '../../utils/sum-usage';
|
|
4
|
+
import { usageRows } from '../../utils/usage-rows';
|
|
5
|
+
// Side-effect import — MUST come before `./chat-driver` so the driver subclasses
|
|
6
|
+
// jsdom's EventTarget rather than Node's native one. Mirrors chat-driver.test.ts.
|
|
7
|
+
import './align-event-globals';
|
|
8
|
+
import { ChatDriver } from './chat-driver';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Sub-agent trace capture when ONE tool call invokes SEVERAL sub-agents.
|
|
11
|
+
//
|
|
12
|
+
// The capture slot used to be a single `{ trace?: ChatMessage[] }` per tool call,
|
|
13
|
+
// assigned by each `requestSubAgent`. That covers N tool calls × 1 sub-agent — the
|
|
14
|
+
// case its docblock named — but not 1 tool call × N sub-agents, which is what a
|
|
15
|
+
// code-driven scheduler or a retry produces. All but the last trace was dropped, so
|
|
16
|
+
// those children ran, were billed, and then had no record in history.
|
|
17
|
+
//
|
|
18
|
+
// The failure was invisible to every existing check: `sumUsage` and `usageRows` both
|
|
19
|
+
// recurse into `subAgentTrace`, so they summed a truncated input and still reconciled
|
|
20
|
+
// with each other perfectly. Measured at 1 of 7 traces kept on a seven-way fan-out,
|
|
21
|
+
// ~2.3x under the true cost.
|
|
22
|
+
//
|
|
23
|
+
// These tests assert the traces SURVIVE, which is upstream of any cost assertion.
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/** Answers by rule: concurrent children under one turn make a FIFO queue nondeterministic. */
|
|
26
|
+
const ruleProvider = () => {
|
|
27
|
+
let parentTurns = 0;
|
|
28
|
+
return {
|
|
29
|
+
chat: (_history, _userMessage, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
|
+
var _a;
|
|
31
|
+
const tools = ((_a = options === null || options === void 0 ? void 0 : options.tools) !== null && _a !== void 0 ? _a : []).map((t) => t.name);
|
|
32
|
+
// Child turn: finishes via its completion tool, and reports usage so the
|
|
33
|
+
// reconciliation assertions have something to add up.
|
|
34
|
+
if (tools.includes('work')) {
|
|
35
|
+
return {
|
|
36
|
+
role: 'assistant',
|
|
37
|
+
content: '',
|
|
38
|
+
model: 'claude-sonnet-5',
|
|
39
|
+
cost: 0.01,
|
|
40
|
+
inputTokens: 100,
|
|
41
|
+
outputTokens: 20,
|
|
42
|
+
toolCalls: [{ id: 'w1', name: 'work', args: {} }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (tools.includes('delegate') && parentTurns === 0) {
|
|
46
|
+
parentTurns += 1;
|
|
47
|
+
return {
|
|
48
|
+
role: 'assistant',
|
|
49
|
+
content: '',
|
|
50
|
+
model: 'claude-sonnet-5',
|
|
51
|
+
cost: 0.02,
|
|
52
|
+
inputTokens: 200,
|
|
53
|
+
outputTokens: 30,
|
|
54
|
+
toolCalls: [{ id: 'd0', name: 'delegate', args: {} }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { role: 'assistant', content: 'done', model: 'claude-sonnet-5' };
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const makeRegistry = (provider) => ({
|
|
62
|
+
get: () => provider,
|
|
63
|
+
default: () => provider,
|
|
64
|
+
defaultName: () => 'test',
|
|
65
|
+
names: () => ['test'],
|
|
66
|
+
getStatus: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
|
|
67
|
+
listStatuses: () => __awaiter(void 0, void 0, void 0, function* () { return []; }),
|
|
68
|
+
});
|
|
69
|
+
const agent = (overrides) => (Object.assign({ description: 'test agent' }, overrides));
|
|
70
|
+
/** A child that reports one unit of usage and completes. */
|
|
71
|
+
const worker = (name) => agent({
|
|
72
|
+
name,
|
|
73
|
+
toolDefinitions: [
|
|
74
|
+
{ name: 'work', description: 'work', parameters: { type: 'object', properties: {} } },
|
|
75
|
+
],
|
|
76
|
+
toolHandlers: {
|
|
77
|
+
work: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
var _a;
|
|
79
|
+
(_a = ctx.completeSubAgent) === null || _a === void 0 ? void 0 : _a.call(ctx, { ok: true });
|
|
80
|
+
return 'worked';
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
/**
|
|
85
|
+
* A parent whose single `delegate` tool call invokes `names` in turn — the shape a
|
|
86
|
+
* code-driven scheduler produces, and the one the old single slot truncated.
|
|
87
|
+
*/
|
|
88
|
+
const boss = (children, invoke) => agent({
|
|
89
|
+
name: 'boss',
|
|
90
|
+
subAgents: children,
|
|
91
|
+
toolDefinitions: [
|
|
92
|
+
{ name: 'delegate', description: 'delegate', parameters: { type: 'object', properties: {} } },
|
|
93
|
+
],
|
|
94
|
+
toolHandlers: {
|
|
95
|
+
delegate: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
96
|
+
yield Promise.all(invoke.map((n) => ctx.requestSubAgent(n, { task: 'go' })));
|
|
97
|
+
return 'delegated';
|
|
98
|
+
}),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const run = (config) => __awaiter(void 0, void 0, void 0, function* () {
|
|
102
|
+
const driver = new ChatDriver(makeRegistry(ruleProvider()), {
|
|
103
|
+
maxToolIterations: 20,
|
|
104
|
+
maxFoldOperations: 5,
|
|
105
|
+
sessionKey: '',
|
|
106
|
+
});
|
|
107
|
+
driver.applyAgent(config);
|
|
108
|
+
yield driver.sendMessage('go');
|
|
109
|
+
return driver.getHistory();
|
|
110
|
+
});
|
|
111
|
+
const tracesIn = (history) => history.flatMap((m) => { var _a; return ((_a = m.toolCalls) !== null && _a !== void 0 ? _a : []).flatMap((tc) => (tc.subAgentTrace ? [tc.subAgentTrace] : [])); });
|
|
112
|
+
const suite = createLogicSuite('ChatDriver sub-agent trace capture');
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
suite('keeps every trace when one tool call invokes three sub-agents', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
const names = ['gen_a', 'gen_b', 'gen_c'];
|
|
116
|
+
const history = yield run(boss(names.map(worker), names));
|
|
117
|
+
const traces = tracesIn(history);
|
|
118
|
+
assert.is(traces.length, 1, 'one tool call, so one concatenated trace');
|
|
119
|
+
// Each child contributes at least its own assistant turn. Before the fix this was
|
|
120
|
+
// one child's worth regardless of how many ran.
|
|
121
|
+
const seen = new Set(traces[0].map((m) => m.agentName).filter(Boolean));
|
|
122
|
+
assert.equal([...seen].sort(), names, `every invoked child must appear — got ${JSON.stringify([...seen])}`);
|
|
123
|
+
}));
|
|
124
|
+
suite('prices every child — usageRows emits a row per child and reconciles', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
125
|
+
// The reconciliation test extended to a fan-out. The single-child version passed
|
|
126
|
+
// before the fix AND after it, which is exactly why it did not catch this.
|
|
127
|
+
const names = ['gen_a', 'gen_b', 'gen_c'];
|
|
128
|
+
const history = yield run(boss(names.map(worker), names));
|
|
129
|
+
const rows = usageRows(history);
|
|
130
|
+
const childRows = rows.filter((r) => r.subAgentDepth === 1);
|
|
131
|
+
assert.is(childRows.length, names.length, 'one row per child that ran');
|
|
132
|
+
const rowTotal = rows.reduce((n, r) => { var _a, _b; return n + ((_a = r.costUsd) !== null && _a !== void 0 ? _a : 0) + ((_b = r.externalCostUsd) !== null && _b !== void 0 ? _b : 0); }, 0);
|
|
133
|
+
assert.ok(Math.abs(rowTotal - sumUsage(history).costUsd) < 1e-12, `rows ${rowTotal} vs sumUsage ${sumUsage(history).costUsd}`);
|
|
134
|
+
// Parent turn ($0.02) + three children ($0.01 each). Asserted as a number so a
|
|
135
|
+
// regression that silently drops a child fails here rather than only in the
|
|
136
|
+
// reconciliation above, which would still agree with a truncated input.
|
|
137
|
+
assert.ok(Math.abs(rowTotal - 0.05) < 1e-12, `expected 0.05, got ${rowTotal}`);
|
|
138
|
+
}));
|
|
139
|
+
suite('keeps both traces when the same sub-agent is invoked twice (a retry)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
140
|
+
// The retry shape: one handler, one child name, two invocations. Under the old slot
|
|
141
|
+
// the first attempt's trace was overwritten by the second.
|
|
142
|
+
const history = yield run(boss([worker('gen_a')], ['gen_a', 'gen_a']));
|
|
143
|
+
const traces = tracesIn(history);
|
|
144
|
+
assert.is(traces.length, 1);
|
|
145
|
+
const childTurns = traces[0].filter((m) => m.agentName === 'gen_a' && m.cost != null);
|
|
146
|
+
assert.is(childTurns.length, 2, 'both attempts survive, not just the last');
|
|
147
|
+
const rows = usageRows(history).filter((r) => r.subAgentDepth === 1);
|
|
148
|
+
assert.is(rows.length, 2, 'and both are priced');
|
|
149
|
+
}));
|
|
150
|
+
suite('keeps child traces when the parent handler throws after they ran', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
|
+
// Children that completed were billed by the provider. If the error path drops
|
|
152
|
+
// their traces, the run under-reports exactly as the single-slot bug did — and
|
|
153
|
+
// just as silently, since `sumUsage` and `usageRows` would still agree with each
|
|
154
|
+
// other over the truncated input. PR review.
|
|
155
|
+
const names = ['gen_a', 'gen_b'];
|
|
156
|
+
const parent = agent({
|
|
157
|
+
name: 'boss',
|
|
158
|
+
subAgents: names.map(worker),
|
|
159
|
+
toolDefinitions: [
|
|
160
|
+
{ name: 'delegate', description: 'delegate', parameters: { type: 'object', properties: {} } },
|
|
161
|
+
],
|
|
162
|
+
toolHandlers: {
|
|
163
|
+
delegate: (_args, ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
164
|
+
yield Promise.all(names.map((n) => ctx.requestSubAgent(n, { task: 'go' })));
|
|
165
|
+
throw new Error('post-processing the children failed');
|
|
166
|
+
}),
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
const history = yield run(parent);
|
|
170
|
+
const traces = tracesIn(history);
|
|
171
|
+
assert.is(traces.length, 1, 'the failed tool call still carries its trace');
|
|
172
|
+
const seen = [...new Set(traces[0].map((m) => m.agentName).filter(Boolean))].sort();
|
|
173
|
+
assert.equal(seen, names, 'both children survive the throw');
|
|
174
|
+
const childRows = usageRows(history).filter((r) => r.subAgentDepth === 1);
|
|
175
|
+
assert.is(childRows.length, 2, 'and both are still priced');
|
|
176
|
+
}));
|
|
177
|
+
suite('leaves subAgentTrace undefined when no sub-agent ran', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
|
+
var _a;
|
|
179
|
+
// Presence is the signal readers key off (`usage-rows.ts`, the UI's `when(...)`),
|
|
180
|
+
// so an empty array would be a different and wrong claim.
|
|
181
|
+
const history = yield run(agent({
|
|
182
|
+
name: 'boss',
|
|
183
|
+
subAgents: [worker('gen_a')],
|
|
184
|
+
toolDefinitions: [
|
|
185
|
+
{
|
|
186
|
+
name: 'delegate',
|
|
187
|
+
description: 'delegate',
|
|
188
|
+
parameters: { type: 'object', properties: {} },
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
toolHandlers: { delegate: () => __awaiter(void 0, void 0, void 0, function* () { return 'did it myself'; }) },
|
|
192
|
+
}));
|
|
193
|
+
for (const m of history) {
|
|
194
|
+
for (const tc of (_a = m.toolCalls) !== null && _a !== void 0 ? _a : []) {
|
|
195
|
+
assert.is(tc.subAgentTrace, undefined, 'not an empty array');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
assert.equal(usageRows(history).filter((r) => r.subAgentDepth === 1), [], 'no child rows');
|
|
199
|
+
}));
|
|
200
|
+
suite.run();
|
|
@@ -306,6 +306,28 @@ export const settingsModalStyles = css `
|
|
|
306
306
|
color: var(--neutral-foreground-rest);
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
/* Freshness footer (GENC-1464): refresh button + label under the vendor
|
|
310
|
+
rows, left-aligned — a footnote to the figures above it. The button's
|
|
311
|
+
inline padding is pinned and cancelled by the container margin so the
|
|
312
|
+
ICON GLYPH (not the button box) sits flush with the budget labels'
|
|
313
|
+
left edge, whatever the density tokens say. */
|
|
314
|
+
.settings-budget-toolbar {
|
|
315
|
+
display: flex;
|
|
316
|
+
justify-content: flex-start;
|
|
317
|
+
align-items: center;
|
|
318
|
+
gap: 4px;
|
|
319
|
+
margin-inline-start: -4px;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.settings-budget-refresh::part(control) {
|
|
323
|
+
padding-inline: 4px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.settings-budget-updated {
|
|
327
|
+
color: var(--neutral-foreground-hint);
|
|
328
|
+
font-size: 11px;
|
|
329
|
+
}
|
|
330
|
+
|
|
309
331
|
/*
|
|
310
332
|
* ONE rhythm for every meter row. The outer wrapper holds the context
|
|
311
333
|
* indicator and the budget meter; the inner one holds the budget meter's
|
|
@@ -120,7 +120,7 @@ const platformContextUsageTemplate = (progressTag) => html `
|
|
|
120
120
|
* the bar clamps — both computed in `settingsBudgetRows`, so the bindings here
|
|
121
121
|
* stay pure reads and the arithmetic is unit-testable.
|
|
122
122
|
*/
|
|
123
|
-
const platformBudgetUsageTemplate = (progressTag) => html `
|
|
123
|
+
const platformBudgetUsageTemplate = (progressTag, buttonTag, iconTag) => html `
|
|
124
124
|
${when((x) => x.settingsBudgetUsageVisible, html `
|
|
125
125
|
<div class="settings-budget-usage" part="budget-usage">
|
|
126
126
|
${repeat((x) => x.settingsBudgetRows, html `
|
|
@@ -135,6 +135,26 @@ const platformBudgetUsageTemplate = (progressTag) => html `
|
|
|
135
135
|
></${progressTag}>
|
|
136
136
|
</div>
|
|
137
137
|
`)}
|
|
138
|
+
<!--
|
|
139
|
+
Freshness footer (GENC-1464): the rows above update per turn and at
|
|
140
|
+
the wall, which reads as "stale" to anyone watching the modal — the
|
|
141
|
+
label says when the figures arrived, the button asks the host for new
|
|
142
|
+
ones (see FoundationAiAssistant.requestBudgetRefresh). Below the rows
|
|
143
|
+
and left-aligned, reading as a footnote to the figures it describes.
|
|
144
|
+
-->
|
|
145
|
+
<div class="settings-budget-toolbar" part="budget-toolbar">
|
|
146
|
+
<${buttonTag}
|
|
147
|
+
appearance="stealth"
|
|
148
|
+
class="settings-budget-refresh"
|
|
149
|
+
part="budget-refresh"
|
|
150
|
+
title="Refresh budget figures"
|
|
151
|
+
aria-label="Refresh budget figures"
|
|
152
|
+
@click=${(x) => x.requestBudgetRefresh()}
|
|
153
|
+
>
|
|
154
|
+
<${iconTag} name="arrows-rotate"></${iconTag}>
|
|
155
|
+
</${buttonTag}>
|
|
156
|
+
<span class="settings-budget-updated">${(x) => x.settingsBudgetUpdatedLabel}</span>
|
|
157
|
+
</div>
|
|
138
158
|
</div>
|
|
139
159
|
`)}
|
|
140
160
|
`;
|
|
@@ -159,10 +179,11 @@ const platformBudgetUsageTemplate = (progressTag) => html `
|
|
|
159
179
|
*
|
|
160
180
|
* @internal
|
|
161
181
|
*/
|
|
162
|
-
export const settingsUsageMetersTemplate = (progressTag) => html `
|
|
182
|
+
export const settingsUsageMetersTemplate = (progressTag, buttonTag, iconTag) => html `
|
|
163
183
|
${when((x) => x.settingsContextUsageVisible || x.settingsBudgetUsageVisible, html `
|
|
164
184
|
<div class="settings-usage-meters">
|
|
165
|
-
${platformContextUsageTemplate(progressTag)}
|
|
185
|
+
${platformContextUsageTemplate(progressTag)}
|
|
186
|
+
${platformBudgetUsageTemplate(progressTag, buttonTag, iconTag)}
|
|
166
187
|
</div>
|
|
167
188
|
`)}
|
|
168
189
|
`;
|
|
@@ -511,7 +532,7 @@ export const SettingsModalTemplate = (designSystemPrefix) => {
|
|
|
511
532
|
</h3>
|
|
512
533
|
<div class="settings-modal-section-body">
|
|
513
534
|
<slot name="settings-model" ${slotted('settingsModelSlotted')}></slot>
|
|
514
|
-
${settingsUsageMetersTemplate(progressTag)}
|
|
535
|
+
${settingsUsageMetersTemplate(progressTag, buttonTag, iconTag)}
|
|
515
536
|
</div>
|
|
516
537
|
</section>
|
|
517
538
|
<!--
|
|
@@ -25,7 +25,7 @@ FoundationAiAssistant;
|
|
|
25
25
|
// only), for the reason budget-meter.test.ts gives: connecting subscribes to
|
|
26
26
|
// `agenticActivityBus` and leaves the runner's event loop open.
|
|
27
27
|
const Suite = createLogicSuite('settings-modal usage meters structure');
|
|
28
|
-
const template = settingsUsageMetersTemplate('test-progress');
|
|
28
|
+
const template = settingsUsageMetersTemplate('test-progress', 'test-button', 'test-icon');
|
|
29
29
|
let storeSeq = 0;
|
|
30
30
|
/** A fresh (unconnected) element wired to its own real session store. */
|
|
31
31
|
function element() {
|