@genesislcap/ai-assistant 15.6.2 → 15.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-assistant.api.json +422 -6
- package/dist/ai-assistant.d.ts +654 -19
- 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 +289 -32
- 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/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.template.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 +435 -19
- 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/dts/utils/banked-usage-baselines.d.ts +51 -0
- package/dist/dts/utils/banked-usage-baselines.d.ts.map +1 -0
- 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/components/settings-modal/settings-modal.styles.js +63 -18
- package/dist/esm/components/settings-modal/settings-modal.template.js +36 -7
- package/dist/esm/main/blocked-state.test.js +969 -0
- package/dist/esm/main/cost-session-banking.test.js +238 -8
- package/dist/esm/main/main.js +826 -48
- package/dist/esm/main/main.styles.js +72 -0
- package/dist/esm/main/main.styles.test.js +99 -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/esm/utils/banked-usage-baselines.js +84 -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/components/settings-modal/settings-modal.styles.ts +63 -18
- package/src/components/settings-modal/settings-modal.template.ts +38 -7
- package/src/main/blocked-state.test.ts +1316 -0
- package/src/main/cost-session-banking.test.ts +283 -11
- package/src/main/main.styles.test.ts +130 -0
- package/src/main/main.styles.ts +72 -0
- package/src/main/main.template.ts +131 -4
- package/src/main/main.ts +827 -42
- 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/src/utils/banked-usage-baselines.ts +90 -0
package/dist/chat-driver.mjs
CHANGED
|
@@ -969,6 +969,67 @@ var GeminiProvider = class {
|
|
|
969
969
|
}
|
|
970
970
|
};
|
|
971
971
|
|
|
972
|
+
// ../../foundation-ai/dist/esm/transports/budget-exhausted-error.js
|
|
973
|
+
var BUDGET_EXCEEDED_CODE = "BUDGET_EXCEEDED";
|
|
974
|
+
var VENDOR_LABELS = Object.freeze({
|
|
975
|
+
anthropic: "Anthropic",
|
|
976
|
+
gemini: "Gemini",
|
|
977
|
+
openai: "OpenAI",
|
|
978
|
+
chrome: "Chrome"
|
|
979
|
+
});
|
|
980
|
+
var BUDGETED_VENDORS = Object.freeze([
|
|
981
|
+
"anthropic",
|
|
982
|
+
"gemini"
|
|
983
|
+
]);
|
|
984
|
+
var VENDOR_TYPE_BY_LABEL = new Map(Object.entries(VENDOR_LABELS).map(([type, label]) => [
|
|
985
|
+
label.toLowerCase(),
|
|
986
|
+
type
|
|
987
|
+
]));
|
|
988
|
+
function vendorTypeOfLabel(label) {
|
|
989
|
+
if (typeof label !== "string")
|
|
990
|
+
return void 0;
|
|
991
|
+
return VENDOR_TYPE_BY_LABEL.get(label.trim().toLowerCase());
|
|
992
|
+
}
|
|
993
|
+
var BUDGET_EXCEEDED_STATUS = 402;
|
|
994
|
+
var DEFAULT_BUDGET_EXHAUSTED_MESSAGE = "You've reached your AI usage limit. Contact your administrator to raise it.";
|
|
995
|
+
var BudgetExhaustedError = class extends Error {
|
|
996
|
+
constructor(vendorLabel, budgetUsd, spentUsd, detail, extra) {
|
|
997
|
+
super(`${vendorLabel} request refused: the AI usage budget is exhausted` + (budgetUsd != null ? ` (spent ${spentUsd != null ? `$${spentUsd}` : "an unknown amount"} of a $${budgetUsd} budget)` : "") + (detail ? `: ${detail}` : "") + ". Retrying will not clear this \u2014 the budget must be raised.");
|
|
998
|
+
this.vendorLabel = vendorLabel;
|
|
999
|
+
this.budgetUsd = budgetUsd;
|
|
1000
|
+
this.spentUsd = spentUsd;
|
|
1001
|
+
this.detail = detail;
|
|
1002
|
+
this.name = "BudgetExhaustedError";
|
|
1003
|
+
this.otherVendorAvailable = extra === null || extra === void 0 ? void 0 : extra.otherVendorAvailable;
|
|
1004
|
+
this.serverVendor = extra === null || extra === void 0 ? void 0 : extra.serverVendor;
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
var isBudgetRejection = (status, code) => status === BUDGET_EXCEEDED_STATUS || code === BUDGET_EXCEEDED_CODE;
|
|
1008
|
+
var parseJsonOrUndefined = (text) => {
|
|
1009
|
+
try {
|
|
1010
|
+
return JSON.parse(text);
|
|
1011
|
+
} catch (_a) {
|
|
1012
|
+
return void 0;
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
var codeOf = (payload) => {
|
|
1016
|
+
const code = payload === null || payload === void 0 ? void 0 : payload.code;
|
|
1017
|
+
return typeof code === "string" ? code : void 0;
|
|
1018
|
+
};
|
|
1019
|
+
var numberOrUndefined = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1020
|
+
var booleanOrUndefined = (value) => typeof value === "boolean" ? value : void 0;
|
|
1021
|
+
var stringOrUndefined = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
1022
|
+
function budgetExhaustedFrom(vendorLabel, payload, fallbackDetail) {
|
|
1023
|
+
var _a, _b, _c, _d;
|
|
1024
|
+
const body = typeof payload === "object" && payload !== null ? payload : {};
|
|
1025
|
+
const details = typeof body.details === "object" && body.details !== null ? body.details : {};
|
|
1026
|
+
const detail = typeof body.error === "string" ? body.error : fallbackDetail;
|
|
1027
|
+
return new BudgetExhaustedError(vendorLabel, (_a = numberOrUndefined(body.budgetUsd)) !== null && _a !== void 0 ? _a : numberOrUndefined(details.budgetUsd), (_b = numberOrUndefined(body.spentUsd)) !== null && _b !== void 0 ? _b : numberOrUndefined(details.spentUsd), detail, {
|
|
1028
|
+
otherVendorAvailable: (_c = booleanOrUndefined(body.otherVendorAvailable)) !== null && _c !== void 0 ? _c : booleanOrUndefined(details.otherVendorAvailable),
|
|
1029
|
+
serverVendor: (_d = stringOrUndefined(body.vendor)) !== null && _d !== void 0 ? _d : stringOrUndefined(details.vendor)
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
972
1033
|
// ../../foundation-ai/dist/esm/types/config.types.js
|
|
973
1034
|
var SUPPORTED_GEMINI_MODEL_IDS = [
|
|
974
1035
|
"gemini-2.5-pro",
|
|
@@ -1123,15 +1184,19 @@ function postWithRetry(options) {
|
|
|
1123
1184
|
signal: combinedSignal,
|
|
1124
1185
|
credentials
|
|
1125
1186
|
});
|
|
1126
|
-
if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
|
|
1127
|
-
logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
1128
|
-
clearTimeout(timeoutId);
|
|
1129
|
-
yield backoff(attempt);
|
|
1130
|
-
continue;
|
|
1131
|
-
}
|
|
1132
1187
|
if (!response.ok) {
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1188
|
+
const errText = yield response.text();
|
|
1189
|
+
const parsed = parseJsonOrUndefined(errText);
|
|
1190
|
+
if (isBudgetRejection(response.status, codeOf(parsed))) {
|
|
1191
|
+
throw budgetExhaustedFrom(vendorLabel, parsed, errText || void 0);
|
|
1192
|
+
}
|
|
1193
|
+
if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
|
|
1194
|
+
logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
1195
|
+
clearTimeout(timeoutId);
|
|
1196
|
+
yield backoff(attempt);
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
throw new Error(`${vendorLabel} request error ${response.status}: ${errText}`);
|
|
1135
1200
|
}
|
|
1136
1201
|
if (((_b = (_a = response.headers) === null || _a === void 0 ? void 0 : _a.get("content-type")) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes("application/x-ndjson")) && response.body) {
|
|
1137
1202
|
rearmStallTimer();
|
|
@@ -1141,6 +1206,9 @@ function postWithRetry(options) {
|
|
|
1141
1206
|
if (frame.code === "UPSTREAM_STALLED") {
|
|
1142
1207
|
throw new DOMException(`${vendorLabel} request stalled: ${(_c = frame.error) !== null && _c !== void 0 ? _c : "no upstream data"}`, "TimeoutError");
|
|
1143
1208
|
}
|
|
1209
|
+
if (isBudgetRejection(frame.status, frame.code)) {
|
|
1210
|
+
throw budgetExhaustedFrom(vendorLabel, frame);
|
|
1211
|
+
}
|
|
1144
1212
|
if (retryableStatuses.includes(frame.status) && attempt < MAX_RETRIES) {
|
|
1145
1213
|
logger.warn(`${vendorLabel}Transport: retryable status ${frame.status} (err frame), retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
1146
1214
|
clearTimeout(timeoutId);
|
|
@@ -1611,7 +1679,7 @@ ${att.content}` });
|
|
|
1611
1679
|
headers: requestHeaders,
|
|
1612
1680
|
body,
|
|
1613
1681
|
credentials,
|
|
1614
|
-
vendorLabel:
|
|
1682
|
+
vendorLabel: VENDOR_LABELS.anthropic,
|
|
1615
1683
|
retryableStatuses: _AnthropicTransport.RETRYABLE_STATUSES,
|
|
1616
1684
|
timeout: this.timeout,
|
|
1617
1685
|
stallTimeout: this.stallTimeout,
|
|
@@ -2279,7 +2347,7 @@ ${att.content}`
|
|
|
2279
2347
|
headers,
|
|
2280
2348
|
body: payload,
|
|
2281
2349
|
credentials,
|
|
2282
|
-
vendorLabel:
|
|
2350
|
+
vendorLabel: VENDOR_LABELS.gemini,
|
|
2283
2351
|
retryableStatuses: _GeminiTransport.RETRYABLE_STATUSES,
|
|
2284
2352
|
timeout: this.timeout,
|
|
2285
2353
|
stallTimeout: this.stallTimeout,
|
|
@@ -3029,6 +3097,19 @@ function accumulate(messages, into) {
|
|
|
3029
3097
|
var TOOL_FOLD_SYMBOL = Symbol("toolFold");
|
|
3030
3098
|
|
|
3031
3099
|
// src/components/chat-driver/chat-driver.ts
|
|
3100
|
+
var budgetDetailOf = (e) => {
|
|
3101
|
+
const vendor = vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor);
|
|
3102
|
+
if (e.budgetUsd == null && e.spentUsd == null && !vendor) return void 0;
|
|
3103
|
+
return {
|
|
3104
|
+
budgetUsd: e.budgetUsd,
|
|
3105
|
+
spentUsd: e.spentUsd,
|
|
3106
|
+
vendorLabel: e.vendorLabel,
|
|
3107
|
+
...vendor ? { vendor } : {},
|
|
3108
|
+
// Server-known truth the client cannot re-derive, so it rides all the way to
|
|
3109
|
+
// the banner rather than being re-guessed there from registry membership.
|
|
3110
|
+
...e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}
|
|
3111
|
+
};
|
|
3112
|
+
};
|
|
3032
3113
|
var DEFAULT_MAX_TOOL_ITERATIONS = 50;
|
|
3033
3114
|
var DEFAULT_MAX_FOLD_OPERATIONS = 5;
|
|
3034
3115
|
var DEFAULT_MAX_TURN_SNAPSHOTS = 400;
|
|
@@ -3211,6 +3292,22 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3211
3292
|
* picked up on the next turn.
|
|
3212
3293
|
*/
|
|
3213
3294
|
this.resolvedStatusCache = /* @__PURE__ */ new Map();
|
|
3295
|
+
/**
|
|
3296
|
+
* Set the moment a budget wall is observed anywhere in this turn — this
|
|
3297
|
+
* driver's own 402, or a sub-agent's (which surfaces here only as a
|
|
3298
|
+
* `'budget_exhausted'` tool result). Read at the top of the tool loop to end
|
|
3299
|
+
* the turn before issuing another model call that would hit the same wall.
|
|
3300
|
+
* Reset per turn alongside the other per-turn counters.
|
|
3301
|
+
*/
|
|
3302
|
+
this.budgetExhaustedThisTurn = false;
|
|
3303
|
+
/**
|
|
3304
|
+
* Whether this turn's budget wall came from a SUB-AGENT rather than this
|
|
3305
|
+
* driver's own request. Decides whether `lastResolvedProvider` is a valid
|
|
3306
|
+
* attribution fallback: for an own wall it is the refusing vendor, for a
|
|
3307
|
+
* child's wall it is the parent's vendor — the one known NOT to have refused.
|
|
3308
|
+
* Reset per turn alongside `budgetWallDetail`.
|
|
3309
|
+
*/
|
|
3310
|
+
this.budgetWallViaSubAgent = false;
|
|
3214
3311
|
const {
|
|
3215
3312
|
toolHandlers = {},
|
|
3216
3313
|
toolDefinitions = [],
|
|
@@ -3221,12 +3318,14 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3221
3318
|
condenseBatchCalls = 1,
|
|
3222
3319
|
maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
|
|
3223
3320
|
sessionKey = "",
|
|
3224
|
-
activityBus = NOOP_ACTIVITY_BUS
|
|
3321
|
+
activityBus = NOOP_ACTIVITY_BUS,
|
|
3322
|
+
budgetExhaustedMessage = DEFAULT_BUDGET_EXHAUSTED_MESSAGE
|
|
3225
3323
|
} = config;
|
|
3226
3324
|
this.maxToolIterations = maxToolIterations;
|
|
3227
3325
|
this.condenseBatchCalls = condenseBatchCalls;
|
|
3228
3326
|
this.sessionKey = sessionKey;
|
|
3229
3327
|
this.activityBus = activityBus;
|
|
3328
|
+
this.budgetExhaustedMessage = budgetExhaustedMessage;
|
|
3230
3329
|
if (typeof toolHandlers === "function") {
|
|
3231
3330
|
this.toolHandlersFactory = toolHandlers;
|
|
3232
3331
|
this.toolHandlers = {};
|
|
@@ -3333,8 +3432,56 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3333
3432
|
* (rather than set to `undefined`) so a happy-path result stays byte-identical to
|
|
3334
3433
|
* the historical `{ reason: 'done' }`.
|
|
3335
3434
|
*/
|
|
3336
|
-
turnDone(failureReason) {
|
|
3337
|
-
|
|
3435
|
+
turnDone(failureReason, budget) {
|
|
3436
|
+
if (!failureReason) return { reason: "done" };
|
|
3437
|
+
return budget ? { reason: "done", failureReason, budget } : { reason: "done", failureReason };
|
|
3438
|
+
}
|
|
3439
|
+
/**
|
|
3440
|
+
* Terminal budget outcome for a wall hit **outside** the tool loop — today,
|
|
3441
|
+
* `OrchestratingDriver`'s classification phase, which calls the provider
|
|
3442
|
+
* directly and so never enters `runToolLoop`.
|
|
3443
|
+
*
|
|
3444
|
+
* Does **not** publish `tool-loop-end`: no `tool-loop-start` was published for
|
|
3445
|
+
* the classify phase, and an unbalanced end would break start/end pairing for
|
|
3446
|
+
* subscribers that rely on it. The driver **return value** is what reports this
|
|
3447
|
+
* case — see `FoundationAiAssistant`'s latch, which reads both seams for
|
|
3448
|
+
* exactly this reason.
|
|
3449
|
+
*
|
|
3450
|
+
* The non-sub-agent tail of the in-loop `BudgetExhaustedError` branch lives
|
|
3451
|
+
* here so there is one copy of the log line, the debug-log entry, the
|
|
3452
|
+
* transcript bubble and the result shape rather than two that can drift.
|
|
3453
|
+
*
|
|
3454
|
+
* @param pendingUserMessage - a user message that has NOT yet been appended,
|
|
3455
|
+
* appended first so the answer does not end up replying to nothing. Only the
|
|
3456
|
+
* classification seam passes it: `OrchestratingDriver` dispatches the user's
|
|
3457
|
+
* text as an optimistic `history-updated` detail and leaves the real append
|
|
3458
|
+
* to `chatDriver.sendMessage`, which never runs when `classify()` throws — so
|
|
3459
|
+
* the bubble below would re-dispatch a history the user's own message was
|
|
3460
|
+
* never in, and it would vanish from the transcript on the next render. The
|
|
3461
|
+
* in-loop caller has already appended it and passes nothing.
|
|
3462
|
+
*
|
|
3463
|
+
* @internal
|
|
3464
|
+
*/
|
|
3465
|
+
reportBudgetExhausted(e, pendingUserMessage) {
|
|
3466
|
+
if (pendingUserMessage) this.appendToHistory(pendingUserMessage);
|
|
3467
|
+
this.budgetExhaustedThisTurn = true;
|
|
3468
|
+
logger2.error("ChatDriver: AI budget exhausted", e);
|
|
3469
|
+
recordTurnError(this.sessionKey, "budget-exhausted", {
|
|
3470
|
+
agent: this.activeAgentName,
|
|
3471
|
+
provider: this.lastResolvedProviderName,
|
|
3472
|
+
// The registry ALIAS (e.g. 'high') is what `provider` records; the vendor
|
|
3473
|
+
// is the thing a per-vendor budget is actually scoped to, and it was known
|
|
3474
|
+
// at both ends and discarded in the middle until now. Taken from the
|
|
3475
|
+
// refusing transport's label first — this method also serves the
|
|
3476
|
+
// classification seam, where `lastResolvedProvider` is the PREVIOUS turn's
|
|
3477
|
+
// vendor (or nothing), because classify runs against the registry default.
|
|
3478
|
+
vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
|
|
3479
|
+
budgetUsd: e.budgetUsd,
|
|
3480
|
+
spentUsd: e.spentUsd,
|
|
3481
|
+
isSubAgent: this.isSubAgent
|
|
3482
|
+
});
|
|
3483
|
+
this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
|
|
3484
|
+
return this.turnDone("budget-exhausted", budgetDetailOf(e));
|
|
3338
3485
|
}
|
|
3339
3486
|
/** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
|
|
3340
3487
|
static failureReasonOf(result) {
|
|
@@ -3344,10 +3491,28 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3344
3491
|
* Build the `tool-loop-end` event detail for a turn's result. A failure carries a
|
|
3345
3492
|
* `{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,
|
|
3346
3493
|
* kept byte-identical so subscribers see exactly what they always have.
|
|
3494
|
+
*
|
|
3495
|
+
* A budget failure additionally carries `vendor` — the concrete vendor
|
|
3496
|
+
* (`'anthropic'`/`'gemini'`) the walled turn resolved to, which the driver knows
|
|
3497
|
+
* and used to discard. Optional and additive: a subscriber reading only
|
|
3498
|
+
* `failureReason` is unaffected, a non-budget failure still emits the historical
|
|
3499
|
+
* `{ failureReason }` with no `vendor` key, and the value is a plain string so
|
|
3500
|
+
* the detail stays structured-cloneable for the cross-tab hop. It is the field a
|
|
3501
|
+
* per-vendor budget model needs and the one that would be awkward to retrofit.
|
|
3347
3502
|
*/
|
|
3348
|
-
|
|
3503
|
+
loopEndDetail(result) {
|
|
3349
3504
|
const failureReason = _ChatDriver.failureReasonOf(result);
|
|
3350
|
-
|
|
3505
|
+
if (!failureReason) return void 0;
|
|
3506
|
+
if (failureReason !== "budget-exhausted") {
|
|
3507
|
+
return { failureReason };
|
|
3508
|
+
}
|
|
3509
|
+
const budget = result.reason === "done" ? result.budget : void 0;
|
|
3510
|
+
const vendor = budget?.vendor ?? (this.budgetWallViaSubAgent ? void 0 : this.lastResolvedProvider);
|
|
3511
|
+
return {
|
|
3512
|
+
failureReason,
|
|
3513
|
+
...vendor ? { vendor } : {},
|
|
3514
|
+
...budget ? { budget } : {}
|
|
3515
|
+
};
|
|
3351
3516
|
}
|
|
3352
3517
|
/**
|
|
3353
3518
|
* Swap in a new agent's configuration. Called by OrchestratingDriver before
|
|
@@ -3533,9 +3698,9 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3533
3698
|
* under a separate session key, so recording here would orphan the event off
|
|
3534
3699
|
* the user-visible debug-log timeline.)
|
|
3535
3700
|
*/
|
|
3536
|
-
failSubAgent(reason) {
|
|
3701
|
+
failSubAgent(reason, budget) {
|
|
3537
3702
|
if (!this.isSubAgent || this.subAgentFailure) return;
|
|
3538
|
-
this.subAgentFailure = { reason };
|
|
3703
|
+
this.subAgentFailure = budget ? { reason, budget } : { reason };
|
|
3539
3704
|
}
|
|
3540
3705
|
/**
|
|
3541
3706
|
* Returns true if `releaseAgent` was called during the most recent turn.
|
|
@@ -3985,6 +4150,9 @@ Output format (strict):
|
|
|
3985
4150
|
this.subAgentCompletion = void 0;
|
|
3986
4151
|
this.subAgentFailure = void 0;
|
|
3987
4152
|
this.agentReleaseRequested = false;
|
|
4153
|
+
this.budgetExhaustedThisTurn = false;
|
|
4154
|
+
this.budgetWallDetail = void 0;
|
|
4155
|
+
this.budgetWallViaSubAgent = false;
|
|
3988
4156
|
this.appendToHistory({ role: "user", content: userInput, attachments });
|
|
3989
4157
|
this.turnStartedAt = Date.now();
|
|
3990
4158
|
recordMetaEvent(this.sessionKey, "turn.start", {
|
|
@@ -4019,7 +4187,7 @@ Output format (strict):
|
|
|
4019
4187
|
});
|
|
4020
4188
|
this.busy = false;
|
|
4021
4189
|
this.endTurn();
|
|
4022
|
-
this.activityBus.publish("tool-loop-end",
|
|
4190
|
+
this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
|
|
4023
4191
|
}
|
|
4024
4192
|
}
|
|
4025
4193
|
/**
|
|
@@ -4210,8 +4378,14 @@ Output format (strict):
|
|
|
4210
4378
|
recordMetaEvent(this.sessionKey, "subagent.completed", { agent: name });
|
|
4211
4379
|
return { outcome: { ok: true, result: completion.result }, trace };
|
|
4212
4380
|
}
|
|
4213
|
-
const
|
|
4381
|
+
const failure = child.getSubAgentFailure();
|
|
4382
|
+
const reason = failure?.reason ?? "max_iterations";
|
|
4214
4383
|
recordMetaEvent(this.sessionKey, "subagent.failed", { agent: name, reason });
|
|
4384
|
+
if (reason === "budget_exhausted") {
|
|
4385
|
+
this.budgetExhaustedThisTurn = true;
|
|
4386
|
+
this.budgetWallViaSubAgent = true;
|
|
4387
|
+
this.budgetWallDetail ??= failure?.budget;
|
|
4388
|
+
}
|
|
4215
4389
|
return { outcome: { ok: false, reason }, trace };
|
|
4216
4390
|
}
|
|
4217
4391
|
/**
|
|
@@ -4224,6 +4398,9 @@ Output format (strict):
|
|
|
4224
4398
|
this.beginTurn();
|
|
4225
4399
|
this.subAgentCompletion = void 0;
|
|
4226
4400
|
this.subAgentFailure = void 0;
|
|
4401
|
+
this.budgetExhaustedThisTurn = false;
|
|
4402
|
+
this.budgetWallDetail = void 0;
|
|
4403
|
+
this.budgetWallViaSubAgent = false;
|
|
4227
4404
|
this.turnStartedAt = Date.now();
|
|
4228
4405
|
recordMetaEvent(this.sessionKey, "turn.start", {
|
|
4229
4406
|
phase: "continueFromHistory",
|
|
@@ -4257,7 +4434,7 @@ Output format (strict):
|
|
|
4257
4434
|
});
|
|
4258
4435
|
this.busy = false;
|
|
4259
4436
|
this.endTurn();
|
|
4260
|
-
this.activityBus.publish("tool-loop-end",
|
|
4437
|
+
this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
|
|
4261
4438
|
}
|
|
4262
4439
|
}
|
|
4263
4440
|
// ---------------------------------------------------------------------------
|
|
@@ -4411,6 +4588,31 @@ Output format (strict):
|
|
|
4411
4588
|
if (this.turnController.signal.aborted) {
|
|
4412
4589
|
return this.completeAbortedTurn();
|
|
4413
4590
|
}
|
|
4591
|
+
if (this.budgetExhaustedThisTurn) {
|
|
4592
|
+
logger2.error("ChatDriver: ending the turn \u2014 a sub-agent hit the AI budget wall");
|
|
4593
|
+
recordTurnError(this.sessionKey, "budget-exhausted", {
|
|
4594
|
+
agent: this.activeAgentName,
|
|
4595
|
+
provider: this.lastResolvedProviderName,
|
|
4596
|
+
// The CHILD's vendor when it knew one, and NOTHING otherwise. On this
|
|
4597
|
+
// path the wall is definitionally the child's, and on a mixed registry
|
|
4598
|
+
// `lastResolvedProvider` is this driver's own vendor — the one known
|
|
4599
|
+
// NOT to have refused. An unattributable child wall must degrade to
|
|
4600
|
+
// the vendor-agnostic block (which `latchBlockedFrom` handles
|
|
4601
|
+
// fail-safe), never to a vendor that is known to be wrong: naming the
|
|
4602
|
+
// parent's vendor here walled BOTH — the child's via its own
|
|
4603
|
+
// tool-loop-end, the parent's via this event — and derived `blocked`
|
|
4604
|
+
// over headroom that still existed.
|
|
4605
|
+
vendor: this.budgetWallDetail?.vendor,
|
|
4606
|
+
via: "sub-agent",
|
|
4607
|
+
isSubAgent: this.isSubAgent
|
|
4608
|
+
});
|
|
4609
|
+
if (this.isSubAgent) {
|
|
4610
|
+
this.failSubAgent("budget_exhausted", this.budgetWallDetail);
|
|
4611
|
+
} else {
|
|
4612
|
+
this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
|
|
4613
|
+
}
|
|
4614
|
+
return this.turnDone("budget-exhausted", this.budgetWallDetail);
|
|
4615
|
+
}
|
|
4414
4616
|
const promptCtx = {
|
|
4415
4617
|
agentName: this.activeAgentName ?? "",
|
|
4416
4618
|
history: this.history,
|
|
@@ -4582,6 +4784,25 @@ ${tailBody}
|
|
|
4582
4784
|
}
|
|
4583
4785
|
return this.turnDone("response-truncated");
|
|
4584
4786
|
}
|
|
4787
|
+
if (e instanceof BudgetExhaustedError) {
|
|
4788
|
+
this.budgetExhaustedThisTurn = true;
|
|
4789
|
+
this.budgetWallDetail = budgetDetailOf(e);
|
|
4790
|
+
this.budgetWallViaSubAgent = false;
|
|
4791
|
+
if (this.isSubAgent) {
|
|
4792
|
+
logger2.error("ChatDriver: AI budget exhausted", e);
|
|
4793
|
+
recordTurnError(this.sessionKey, "budget-exhausted", {
|
|
4794
|
+
agent: this.activeAgentName,
|
|
4795
|
+
provider: this.lastResolvedProviderName,
|
|
4796
|
+
vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
|
|
4797
|
+
budgetUsd: e.budgetUsd,
|
|
4798
|
+
spentUsd: e.spentUsd,
|
|
4799
|
+
isSubAgent: true
|
|
4800
|
+
});
|
|
4801
|
+
this.failSubAgent("budget_exhausted", this.budgetWallDetail);
|
|
4802
|
+
return this.turnDone("budget-exhausted", this.budgetWallDetail);
|
|
4803
|
+
}
|
|
4804
|
+
return this.reportBudgetExhausted(e);
|
|
4805
|
+
}
|
|
4585
4806
|
if (e instanceof DOMException && e.name === "TimeoutError") {
|
|
4586
4807
|
logger2.error("ChatDriver: request timed out", e);
|
|
4587
4808
|
recordTurnError(this.sessionKey, "exception", {
|
|
@@ -5057,6 +5278,17 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5057
5278
|
* user stops. Reset at the start of each `sendMessage`.
|
|
5058
5279
|
*/
|
|
5059
5280
|
this.cancelled = false;
|
|
5281
|
+
/**
|
|
5282
|
+
* Whether the CURRENT turn's user message has reached the inner driver's
|
|
5283
|
+
* history. False through the pre-first-turn classify (where the only echo of
|
|
5284
|
+
* the message is an optimistic `history-updated` dispatch), true from the
|
|
5285
|
+
* moment `chatDriver.sendMessage` is entered — including through every later
|
|
5286
|
+
* handoff classify. Read by the budget-wall catch in `sendMessage` to decide
|
|
5287
|
+
* whether `reportBudgetExhausted` must append the message itself: appending
|
|
5288
|
+
* it when already appended duplicated it; not appending it when unappended
|
|
5289
|
+
* made it vanish. Reset at the top of each `runOrchestratedTurn`.
|
|
5290
|
+
*/
|
|
5291
|
+
this.userMessageAppended = false;
|
|
5060
5292
|
/**
|
|
5061
5293
|
* Sticky user pick from the picker (or the host's `setAgent` API). Only
|
|
5062
5294
|
* changes on explicit user action. Survives flow completion: when a stateful
|
|
@@ -5098,7 +5330,8 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5098
5330
|
maxFoldOperations: options.maxFoldOperations,
|
|
5099
5331
|
maxTurnSnapshots: options.maxTurnSnapshots,
|
|
5100
5332
|
sessionKey: this.sessionKey,
|
|
5101
|
-
activityBus: options.activityBus
|
|
5333
|
+
activityBus: options.activityBus,
|
|
5334
|
+
budgetExhaustedMessage: options.budgetExhaustedMessage
|
|
5102
5335
|
});
|
|
5103
5336
|
this.chatDriver.addEventListener("history-updated", (e) => {
|
|
5104
5337
|
this.dispatchEvent(new CustomEvent("history-updated", { detail: e.detail }));
|
|
@@ -5197,7 +5430,25 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5197
5430
|
return this.chatDriver.getSuggestions(history, prompt2, count, agentInfo);
|
|
5198
5431
|
}
|
|
5199
5432
|
async sendMessage(input, attachments) {
|
|
5433
|
+
try {
|
|
5434
|
+
return await this.runOrchestratedTurn(input, attachments);
|
|
5435
|
+
} catch (e) {
|
|
5436
|
+
if (e instanceof BudgetExhaustedError) {
|
|
5437
|
+
return this.chatDriver.reportBudgetExhausted(
|
|
5438
|
+
e,
|
|
5439
|
+
this.userMessageAppended ? void 0 : {
|
|
5440
|
+
role: "user",
|
|
5441
|
+
content: input,
|
|
5442
|
+
...attachments ? { attachments } : {}
|
|
5443
|
+
}
|
|
5444
|
+
);
|
|
5445
|
+
}
|
|
5446
|
+
throw e;
|
|
5447
|
+
}
|
|
5448
|
+
}
|
|
5449
|
+
async runOrchestratedTurn(input, attachments) {
|
|
5200
5450
|
this.cancelled = false;
|
|
5451
|
+
this.userMessageAppended = false;
|
|
5201
5452
|
const history = this.chatDriver.getHistory();
|
|
5202
5453
|
this.dispatchEvent(
|
|
5203
5454
|
new CustomEvent("history-updated", {
|
|
@@ -5211,6 +5462,7 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5211
5462
|
let handoffs = 0;
|
|
5212
5463
|
let handoffSummary = "";
|
|
5213
5464
|
let remainingTask = "";
|
|
5465
|
+
let lastResult;
|
|
5214
5466
|
while (true) {
|
|
5215
5467
|
if (this.cancelled) break;
|
|
5216
5468
|
await this.applyAgent(currentAgent);
|
|
@@ -5219,8 +5471,10 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5219
5471
|
const contextPrimer = handoffSummary ? [{ role: "user", content: `[Context from previous agent]: ${handoffSummary}` }] : [];
|
|
5220
5472
|
result = await this.chatDriver.continueFromHistory(contextPrimer);
|
|
5221
5473
|
} else {
|
|
5474
|
+
this.userMessageAppended = true;
|
|
5222
5475
|
result = await this.chatDriver.sendMessage(input, attachments);
|
|
5223
5476
|
}
|
|
5477
|
+
lastResult = result;
|
|
5224
5478
|
if (this.chatDriver.getAgentReleaseRequested()) {
|
|
5225
5479
|
await this.releaseActiveAgent();
|
|
5226
5480
|
break;
|
|
@@ -5242,7 +5496,7 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5242
5496
|
this.dispatchEvent(new CustomEvent("orchestrating-start"));
|
|
5243
5497
|
currentAgent = await this.classify(remainingTask, updatedHistory);
|
|
5244
5498
|
}
|
|
5245
|
-
return { reason: "done" };
|
|
5499
|
+
return lastResult?.reason === "done" ? lastResult : { reason: "done" };
|
|
5246
5500
|
}
|
|
5247
5501
|
async continueFromHistory(transientPrimer) {
|
|
5248
5502
|
return this.chatDriver.continueFromHistory(transientPrimer);
|
|
@@ -5425,6 +5679,7 @@ ${recentMessages}` : ""}`;
|
|
|
5425
5679
|
}
|
|
5426
5680
|
}
|
|
5427
5681
|
};
|
|
5682
|
+
let routedNoMatch = false;
|
|
5428
5683
|
for (let attempt = 0; attempt <= this.classifierRetries; attempt += 1) {
|
|
5429
5684
|
try {
|
|
5430
5685
|
const options = {
|
|
@@ -5444,8 +5699,10 @@ ${recentMessages}` : ""}`;
|
|
|
5444
5699
|
if (index >= 0 && index < this.specialists.length) {
|
|
5445
5700
|
return this.specialists[index];
|
|
5446
5701
|
}
|
|
5702
|
+
routedNoMatch = true;
|
|
5447
5703
|
break;
|
|
5448
5704
|
} catch (e) {
|
|
5705
|
+
if (e instanceof BudgetExhaustedError) throw e;
|
|
5449
5706
|
logger2.warn(`OrchestratingDriver: classifier attempt ${attempt + 1} failed:`, e);
|
|
5450
5707
|
if (attempt === this.classifierRetries) {
|
|
5451
5708
|
logger2.error("OrchestratingDriver: classifier failed after all retries, using fallback");
|
|
@@ -5453,10 +5710,12 @@ ${recentMessages}` : ""}`;
|
|
|
5453
5710
|
}
|
|
5454
5711
|
}
|
|
5455
5712
|
if (this.fallback) return this.fallback;
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5713
|
+
if (routedNoMatch) {
|
|
5714
|
+
const specialistNames = this.specialists.map((s) => s.name).join(", ");
|
|
5715
|
+
this.appendInlineMessage(
|
|
5716
|
+
`I'm not sure how to help with that. I can assist with: ${specialistNames}.`
|
|
5717
|
+
);
|
|
5718
|
+
}
|
|
5460
5719
|
return this.specialists[0];
|
|
5461
5720
|
}
|
|
5462
5721
|
/**
|