@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.cjs
CHANGED
|
@@ -1016,6 +1016,67 @@ var GeminiProvider = class {
|
|
|
1016
1016
|
}
|
|
1017
1017
|
};
|
|
1018
1018
|
|
|
1019
|
+
// ../../foundation-ai/dist/esm/transports/budget-exhausted-error.js
|
|
1020
|
+
var BUDGET_EXCEEDED_CODE = "BUDGET_EXCEEDED";
|
|
1021
|
+
var VENDOR_LABELS = Object.freeze({
|
|
1022
|
+
anthropic: "Anthropic",
|
|
1023
|
+
gemini: "Gemini",
|
|
1024
|
+
openai: "OpenAI",
|
|
1025
|
+
chrome: "Chrome"
|
|
1026
|
+
});
|
|
1027
|
+
var BUDGETED_VENDORS = Object.freeze([
|
|
1028
|
+
"anthropic",
|
|
1029
|
+
"gemini"
|
|
1030
|
+
]);
|
|
1031
|
+
var VENDOR_TYPE_BY_LABEL = new Map(Object.entries(VENDOR_LABELS).map(([type, label]) => [
|
|
1032
|
+
label.toLowerCase(),
|
|
1033
|
+
type
|
|
1034
|
+
]));
|
|
1035
|
+
function vendorTypeOfLabel(label) {
|
|
1036
|
+
if (typeof label !== "string")
|
|
1037
|
+
return void 0;
|
|
1038
|
+
return VENDOR_TYPE_BY_LABEL.get(label.trim().toLowerCase());
|
|
1039
|
+
}
|
|
1040
|
+
var BUDGET_EXCEEDED_STATUS = 402;
|
|
1041
|
+
var DEFAULT_BUDGET_EXHAUSTED_MESSAGE = "You've reached your AI usage limit. Contact your administrator to raise it.";
|
|
1042
|
+
var BudgetExhaustedError = class extends Error {
|
|
1043
|
+
constructor(vendorLabel, budgetUsd, spentUsd, detail, extra) {
|
|
1044
|
+
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.");
|
|
1045
|
+
this.vendorLabel = vendorLabel;
|
|
1046
|
+
this.budgetUsd = budgetUsd;
|
|
1047
|
+
this.spentUsd = spentUsd;
|
|
1048
|
+
this.detail = detail;
|
|
1049
|
+
this.name = "BudgetExhaustedError";
|
|
1050
|
+
this.otherVendorAvailable = extra === null || extra === void 0 ? void 0 : extra.otherVendorAvailable;
|
|
1051
|
+
this.serverVendor = extra === null || extra === void 0 ? void 0 : extra.serverVendor;
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
var isBudgetRejection = (status, code) => status === BUDGET_EXCEEDED_STATUS || code === BUDGET_EXCEEDED_CODE;
|
|
1055
|
+
var parseJsonOrUndefined = (text) => {
|
|
1056
|
+
try {
|
|
1057
|
+
return JSON.parse(text);
|
|
1058
|
+
} catch (_a) {
|
|
1059
|
+
return void 0;
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
var codeOf = (payload) => {
|
|
1063
|
+
const code = payload === null || payload === void 0 ? void 0 : payload.code;
|
|
1064
|
+
return typeof code === "string" ? code : void 0;
|
|
1065
|
+
};
|
|
1066
|
+
var numberOrUndefined = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1067
|
+
var booleanOrUndefined = (value) => typeof value === "boolean" ? value : void 0;
|
|
1068
|
+
var stringOrUndefined = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
1069
|
+
function budgetExhaustedFrom(vendorLabel, payload, fallbackDetail) {
|
|
1070
|
+
var _a, _b, _c, _d;
|
|
1071
|
+
const body = typeof payload === "object" && payload !== null ? payload : {};
|
|
1072
|
+
const details = typeof body.details === "object" && body.details !== null ? body.details : {};
|
|
1073
|
+
const detail = typeof body.error === "string" ? body.error : fallbackDetail;
|
|
1074
|
+
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, {
|
|
1075
|
+
otherVendorAvailable: (_c = booleanOrUndefined(body.otherVendorAvailable)) !== null && _c !== void 0 ? _c : booleanOrUndefined(details.otherVendorAvailable),
|
|
1076
|
+
serverVendor: (_d = stringOrUndefined(body.vendor)) !== null && _d !== void 0 ? _d : stringOrUndefined(details.vendor)
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1019
1080
|
// ../../foundation-ai/dist/esm/types/config.types.js
|
|
1020
1081
|
var SUPPORTED_GEMINI_MODEL_IDS = [
|
|
1021
1082
|
"gemini-2.5-pro",
|
|
@@ -1170,15 +1231,19 @@ function postWithRetry(options) {
|
|
|
1170
1231
|
signal: combinedSignal,
|
|
1171
1232
|
credentials
|
|
1172
1233
|
});
|
|
1173
|
-
if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
|
|
1174
|
-
logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
1175
|
-
clearTimeout(timeoutId);
|
|
1176
|
-
yield backoff(attempt);
|
|
1177
|
-
continue;
|
|
1178
|
-
}
|
|
1179
1234
|
if (!response.ok) {
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1235
|
+
const errText = yield response.text();
|
|
1236
|
+
const parsed = parseJsonOrUndefined(errText);
|
|
1237
|
+
if (isBudgetRejection(response.status, codeOf(parsed))) {
|
|
1238
|
+
throw budgetExhaustedFrom(vendorLabel, parsed, errText || void 0);
|
|
1239
|
+
}
|
|
1240
|
+
if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
|
|
1241
|
+
logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
1242
|
+
clearTimeout(timeoutId);
|
|
1243
|
+
yield backoff(attempt);
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
throw new Error(`${vendorLabel} request error ${response.status}: ${errText}`);
|
|
1182
1247
|
}
|
|
1183
1248
|
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) {
|
|
1184
1249
|
rearmStallTimer();
|
|
@@ -1188,6 +1253,9 @@ function postWithRetry(options) {
|
|
|
1188
1253
|
if (frame.code === "UPSTREAM_STALLED") {
|
|
1189
1254
|
throw new DOMException(`${vendorLabel} request stalled: ${(_c = frame.error) !== null && _c !== void 0 ? _c : "no upstream data"}`, "TimeoutError");
|
|
1190
1255
|
}
|
|
1256
|
+
if (isBudgetRejection(frame.status, frame.code)) {
|
|
1257
|
+
throw budgetExhaustedFrom(vendorLabel, frame);
|
|
1258
|
+
}
|
|
1191
1259
|
if (retryableStatuses.includes(frame.status) && attempt < MAX_RETRIES) {
|
|
1192
1260
|
logger.warn(`${vendorLabel}Transport: retryable status ${frame.status} (err frame), retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
1193
1261
|
clearTimeout(timeoutId);
|
|
@@ -1658,7 +1726,7 @@ ${att.content}` });
|
|
|
1658
1726
|
headers: requestHeaders,
|
|
1659
1727
|
body,
|
|
1660
1728
|
credentials,
|
|
1661
|
-
vendorLabel:
|
|
1729
|
+
vendorLabel: VENDOR_LABELS.anthropic,
|
|
1662
1730
|
retryableStatuses: _AnthropicTransport.RETRYABLE_STATUSES,
|
|
1663
1731
|
timeout: this.timeout,
|
|
1664
1732
|
stallTimeout: this.stallTimeout,
|
|
@@ -2326,7 +2394,7 @@ ${att.content}`
|
|
|
2326
2394
|
headers,
|
|
2327
2395
|
body: payload,
|
|
2328
2396
|
credentials,
|
|
2329
|
-
vendorLabel:
|
|
2397
|
+
vendorLabel: VENDOR_LABELS.gemini,
|
|
2330
2398
|
retryableStatuses: _GeminiTransport.RETRYABLE_STATUSES,
|
|
2331
2399
|
timeout: this.timeout,
|
|
2332
2400
|
stallTimeout: this.stallTimeout,
|
|
@@ -3076,6 +3144,19 @@ function accumulate(messages, into) {
|
|
|
3076
3144
|
var TOOL_FOLD_SYMBOL = Symbol("toolFold");
|
|
3077
3145
|
|
|
3078
3146
|
// src/components/chat-driver/chat-driver.ts
|
|
3147
|
+
var budgetDetailOf = (e) => {
|
|
3148
|
+
const vendor = vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor);
|
|
3149
|
+
if (e.budgetUsd == null && e.spentUsd == null && !vendor) return void 0;
|
|
3150
|
+
return {
|
|
3151
|
+
budgetUsd: e.budgetUsd,
|
|
3152
|
+
spentUsd: e.spentUsd,
|
|
3153
|
+
vendorLabel: e.vendorLabel,
|
|
3154
|
+
...vendor ? { vendor } : {},
|
|
3155
|
+
// Server-known truth the client cannot re-derive, so it rides all the way to
|
|
3156
|
+
// the banner rather than being re-guessed there from registry membership.
|
|
3157
|
+
...e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}
|
|
3158
|
+
};
|
|
3159
|
+
};
|
|
3079
3160
|
var DEFAULT_MAX_TOOL_ITERATIONS = 50;
|
|
3080
3161
|
var DEFAULT_MAX_FOLD_OPERATIONS = 5;
|
|
3081
3162
|
var DEFAULT_MAX_TURN_SNAPSHOTS = 400;
|
|
@@ -3258,6 +3339,22 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3258
3339
|
* picked up on the next turn.
|
|
3259
3340
|
*/
|
|
3260
3341
|
this.resolvedStatusCache = /* @__PURE__ */ new Map();
|
|
3342
|
+
/**
|
|
3343
|
+
* Set the moment a budget wall is observed anywhere in this turn — this
|
|
3344
|
+
* driver's own 402, or a sub-agent's (which surfaces here only as a
|
|
3345
|
+
* `'budget_exhausted'` tool result). Read at the top of the tool loop to end
|
|
3346
|
+
* the turn before issuing another model call that would hit the same wall.
|
|
3347
|
+
* Reset per turn alongside the other per-turn counters.
|
|
3348
|
+
*/
|
|
3349
|
+
this.budgetExhaustedThisTurn = false;
|
|
3350
|
+
/**
|
|
3351
|
+
* Whether this turn's budget wall came from a SUB-AGENT rather than this
|
|
3352
|
+
* driver's own request. Decides whether `lastResolvedProvider` is a valid
|
|
3353
|
+
* attribution fallback: for an own wall it is the refusing vendor, for a
|
|
3354
|
+
* child's wall it is the parent's vendor — the one known NOT to have refused.
|
|
3355
|
+
* Reset per turn alongside `budgetWallDetail`.
|
|
3356
|
+
*/
|
|
3357
|
+
this.budgetWallViaSubAgent = false;
|
|
3261
3358
|
const {
|
|
3262
3359
|
toolHandlers = {},
|
|
3263
3360
|
toolDefinitions = [],
|
|
@@ -3268,12 +3365,14 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3268
3365
|
condenseBatchCalls = 1,
|
|
3269
3366
|
maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
|
|
3270
3367
|
sessionKey = "",
|
|
3271
|
-
activityBus = NOOP_ACTIVITY_BUS
|
|
3368
|
+
activityBus = NOOP_ACTIVITY_BUS,
|
|
3369
|
+
budgetExhaustedMessage = DEFAULT_BUDGET_EXHAUSTED_MESSAGE
|
|
3272
3370
|
} = config;
|
|
3273
3371
|
this.maxToolIterations = maxToolIterations;
|
|
3274
3372
|
this.condenseBatchCalls = condenseBatchCalls;
|
|
3275
3373
|
this.sessionKey = sessionKey;
|
|
3276
3374
|
this.activityBus = activityBus;
|
|
3375
|
+
this.budgetExhaustedMessage = budgetExhaustedMessage;
|
|
3277
3376
|
if (typeof toolHandlers === "function") {
|
|
3278
3377
|
this.toolHandlersFactory = toolHandlers;
|
|
3279
3378
|
this.toolHandlers = {};
|
|
@@ -3380,8 +3479,56 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3380
3479
|
* (rather than set to `undefined`) so a happy-path result stays byte-identical to
|
|
3381
3480
|
* the historical `{ reason: 'done' }`.
|
|
3382
3481
|
*/
|
|
3383
|
-
turnDone(failureReason) {
|
|
3384
|
-
|
|
3482
|
+
turnDone(failureReason, budget) {
|
|
3483
|
+
if (!failureReason) return { reason: "done" };
|
|
3484
|
+
return budget ? { reason: "done", failureReason, budget } : { reason: "done", failureReason };
|
|
3485
|
+
}
|
|
3486
|
+
/**
|
|
3487
|
+
* Terminal budget outcome for a wall hit **outside** the tool loop — today,
|
|
3488
|
+
* `OrchestratingDriver`'s classification phase, which calls the provider
|
|
3489
|
+
* directly and so never enters `runToolLoop`.
|
|
3490
|
+
*
|
|
3491
|
+
* Does **not** publish `tool-loop-end`: no `tool-loop-start` was published for
|
|
3492
|
+
* the classify phase, and an unbalanced end would break start/end pairing for
|
|
3493
|
+
* subscribers that rely on it. The driver **return value** is what reports this
|
|
3494
|
+
* case — see `FoundationAiAssistant`'s latch, which reads both seams for
|
|
3495
|
+
* exactly this reason.
|
|
3496
|
+
*
|
|
3497
|
+
* The non-sub-agent tail of the in-loop `BudgetExhaustedError` branch lives
|
|
3498
|
+
* here so there is one copy of the log line, the debug-log entry, the
|
|
3499
|
+
* transcript bubble and the result shape rather than two that can drift.
|
|
3500
|
+
*
|
|
3501
|
+
* @param pendingUserMessage - a user message that has NOT yet been appended,
|
|
3502
|
+
* appended first so the answer does not end up replying to nothing. Only the
|
|
3503
|
+
* classification seam passes it: `OrchestratingDriver` dispatches the user's
|
|
3504
|
+
* text as an optimistic `history-updated` detail and leaves the real append
|
|
3505
|
+
* to `chatDriver.sendMessage`, which never runs when `classify()` throws — so
|
|
3506
|
+
* the bubble below would re-dispatch a history the user's own message was
|
|
3507
|
+
* never in, and it would vanish from the transcript on the next render. The
|
|
3508
|
+
* in-loop caller has already appended it and passes nothing.
|
|
3509
|
+
*
|
|
3510
|
+
* @internal
|
|
3511
|
+
*/
|
|
3512
|
+
reportBudgetExhausted(e, pendingUserMessage) {
|
|
3513
|
+
if (pendingUserMessage) this.appendToHistory(pendingUserMessage);
|
|
3514
|
+
this.budgetExhaustedThisTurn = true;
|
|
3515
|
+
logger2.error("ChatDriver: AI budget exhausted", e);
|
|
3516
|
+
recordTurnError(this.sessionKey, "budget-exhausted", {
|
|
3517
|
+
agent: this.activeAgentName,
|
|
3518
|
+
provider: this.lastResolvedProviderName,
|
|
3519
|
+
// The registry ALIAS (e.g. 'high') is what `provider` records; the vendor
|
|
3520
|
+
// is the thing a per-vendor budget is actually scoped to, and it was known
|
|
3521
|
+
// at both ends and discarded in the middle until now. Taken from the
|
|
3522
|
+
// refusing transport's label first — this method also serves the
|
|
3523
|
+
// classification seam, where `lastResolvedProvider` is the PREVIOUS turn's
|
|
3524
|
+
// vendor (or nothing), because classify runs against the registry default.
|
|
3525
|
+
vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
|
|
3526
|
+
budgetUsd: e.budgetUsd,
|
|
3527
|
+
spentUsd: e.spentUsd,
|
|
3528
|
+
isSubAgent: this.isSubAgent
|
|
3529
|
+
});
|
|
3530
|
+
this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
|
|
3531
|
+
return this.turnDone("budget-exhausted", budgetDetailOf(e));
|
|
3385
3532
|
}
|
|
3386
3533
|
/** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
|
|
3387
3534
|
static failureReasonOf(result) {
|
|
@@ -3391,10 +3538,28 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3391
3538
|
* Build the `tool-loop-end` event detail for a turn's result. A failure carries a
|
|
3392
3539
|
* `{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,
|
|
3393
3540
|
* kept byte-identical so subscribers see exactly what they always have.
|
|
3541
|
+
*
|
|
3542
|
+
* A budget failure additionally carries `vendor` — the concrete vendor
|
|
3543
|
+
* (`'anthropic'`/`'gemini'`) the walled turn resolved to, which the driver knows
|
|
3544
|
+
* and used to discard. Optional and additive: a subscriber reading only
|
|
3545
|
+
* `failureReason` is unaffected, a non-budget failure still emits the historical
|
|
3546
|
+
* `{ failureReason }` with no `vendor` key, and the value is a plain string so
|
|
3547
|
+
* the detail stays structured-cloneable for the cross-tab hop. It is the field a
|
|
3548
|
+
* per-vendor budget model needs and the one that would be awkward to retrofit.
|
|
3394
3549
|
*/
|
|
3395
|
-
|
|
3550
|
+
loopEndDetail(result) {
|
|
3396
3551
|
const failureReason = _ChatDriver.failureReasonOf(result);
|
|
3397
|
-
|
|
3552
|
+
if (!failureReason) return void 0;
|
|
3553
|
+
if (failureReason !== "budget-exhausted") {
|
|
3554
|
+
return { failureReason };
|
|
3555
|
+
}
|
|
3556
|
+
const budget = result.reason === "done" ? result.budget : void 0;
|
|
3557
|
+
const vendor = budget?.vendor ?? (this.budgetWallViaSubAgent ? void 0 : this.lastResolvedProvider);
|
|
3558
|
+
return {
|
|
3559
|
+
failureReason,
|
|
3560
|
+
...vendor ? { vendor } : {},
|
|
3561
|
+
...budget ? { budget } : {}
|
|
3562
|
+
};
|
|
3398
3563
|
}
|
|
3399
3564
|
/**
|
|
3400
3565
|
* Swap in a new agent's configuration. Called by OrchestratingDriver before
|
|
@@ -3580,9 +3745,9 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3580
3745
|
* under a separate session key, so recording here would orphan the event off
|
|
3581
3746
|
* the user-visible debug-log timeline.)
|
|
3582
3747
|
*/
|
|
3583
|
-
failSubAgent(reason) {
|
|
3748
|
+
failSubAgent(reason, budget) {
|
|
3584
3749
|
if (!this.isSubAgent || this.subAgentFailure) return;
|
|
3585
|
-
this.subAgentFailure = { reason };
|
|
3750
|
+
this.subAgentFailure = budget ? { reason, budget } : { reason };
|
|
3586
3751
|
}
|
|
3587
3752
|
/**
|
|
3588
3753
|
* Returns true if `releaseAgent` was called during the most recent turn.
|
|
@@ -4032,6 +4197,9 @@ Output format (strict):
|
|
|
4032
4197
|
this.subAgentCompletion = void 0;
|
|
4033
4198
|
this.subAgentFailure = void 0;
|
|
4034
4199
|
this.agentReleaseRequested = false;
|
|
4200
|
+
this.budgetExhaustedThisTurn = false;
|
|
4201
|
+
this.budgetWallDetail = void 0;
|
|
4202
|
+
this.budgetWallViaSubAgent = false;
|
|
4035
4203
|
this.appendToHistory({ role: "user", content: userInput, attachments });
|
|
4036
4204
|
this.turnStartedAt = Date.now();
|
|
4037
4205
|
recordMetaEvent(this.sessionKey, "turn.start", {
|
|
@@ -4066,7 +4234,7 @@ Output format (strict):
|
|
|
4066
4234
|
});
|
|
4067
4235
|
this.busy = false;
|
|
4068
4236
|
this.endTurn();
|
|
4069
|
-
this.activityBus.publish("tool-loop-end",
|
|
4237
|
+
this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
|
|
4070
4238
|
}
|
|
4071
4239
|
}
|
|
4072
4240
|
/**
|
|
@@ -4257,8 +4425,14 @@ Output format (strict):
|
|
|
4257
4425
|
recordMetaEvent(this.sessionKey, "subagent.completed", { agent: name });
|
|
4258
4426
|
return { outcome: { ok: true, result: completion.result }, trace };
|
|
4259
4427
|
}
|
|
4260
|
-
const
|
|
4428
|
+
const failure = child.getSubAgentFailure();
|
|
4429
|
+
const reason = failure?.reason ?? "max_iterations";
|
|
4261
4430
|
recordMetaEvent(this.sessionKey, "subagent.failed", { agent: name, reason });
|
|
4431
|
+
if (reason === "budget_exhausted") {
|
|
4432
|
+
this.budgetExhaustedThisTurn = true;
|
|
4433
|
+
this.budgetWallViaSubAgent = true;
|
|
4434
|
+
this.budgetWallDetail ??= failure?.budget;
|
|
4435
|
+
}
|
|
4262
4436
|
return { outcome: { ok: false, reason }, trace };
|
|
4263
4437
|
}
|
|
4264
4438
|
/**
|
|
@@ -4271,6 +4445,9 @@ Output format (strict):
|
|
|
4271
4445
|
this.beginTurn();
|
|
4272
4446
|
this.subAgentCompletion = void 0;
|
|
4273
4447
|
this.subAgentFailure = void 0;
|
|
4448
|
+
this.budgetExhaustedThisTurn = false;
|
|
4449
|
+
this.budgetWallDetail = void 0;
|
|
4450
|
+
this.budgetWallViaSubAgent = false;
|
|
4274
4451
|
this.turnStartedAt = Date.now();
|
|
4275
4452
|
recordMetaEvent(this.sessionKey, "turn.start", {
|
|
4276
4453
|
phase: "continueFromHistory",
|
|
@@ -4304,7 +4481,7 @@ Output format (strict):
|
|
|
4304
4481
|
});
|
|
4305
4482
|
this.busy = false;
|
|
4306
4483
|
this.endTurn();
|
|
4307
|
-
this.activityBus.publish("tool-loop-end",
|
|
4484
|
+
this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
|
|
4308
4485
|
}
|
|
4309
4486
|
}
|
|
4310
4487
|
// ---------------------------------------------------------------------------
|
|
@@ -4458,6 +4635,31 @@ Output format (strict):
|
|
|
4458
4635
|
if (this.turnController.signal.aborted) {
|
|
4459
4636
|
return this.completeAbortedTurn();
|
|
4460
4637
|
}
|
|
4638
|
+
if (this.budgetExhaustedThisTurn) {
|
|
4639
|
+
logger2.error("ChatDriver: ending the turn \u2014 a sub-agent hit the AI budget wall");
|
|
4640
|
+
recordTurnError(this.sessionKey, "budget-exhausted", {
|
|
4641
|
+
agent: this.activeAgentName,
|
|
4642
|
+
provider: this.lastResolvedProviderName,
|
|
4643
|
+
// The CHILD's vendor when it knew one, and NOTHING otherwise. On this
|
|
4644
|
+
// path the wall is definitionally the child's, and on a mixed registry
|
|
4645
|
+
// `lastResolvedProvider` is this driver's own vendor — the one known
|
|
4646
|
+
// NOT to have refused. An unattributable child wall must degrade to
|
|
4647
|
+
// the vendor-agnostic block (which `latchBlockedFrom` handles
|
|
4648
|
+
// fail-safe), never to a vendor that is known to be wrong: naming the
|
|
4649
|
+
// parent's vendor here walled BOTH — the child's via its own
|
|
4650
|
+
// tool-loop-end, the parent's via this event — and derived `blocked`
|
|
4651
|
+
// over headroom that still existed.
|
|
4652
|
+
vendor: this.budgetWallDetail?.vendor,
|
|
4653
|
+
via: "sub-agent",
|
|
4654
|
+
isSubAgent: this.isSubAgent
|
|
4655
|
+
});
|
|
4656
|
+
if (this.isSubAgent) {
|
|
4657
|
+
this.failSubAgent("budget_exhausted", this.budgetWallDetail);
|
|
4658
|
+
} else {
|
|
4659
|
+
this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
|
|
4660
|
+
}
|
|
4661
|
+
return this.turnDone("budget-exhausted", this.budgetWallDetail);
|
|
4662
|
+
}
|
|
4461
4663
|
const promptCtx = {
|
|
4462
4664
|
agentName: this.activeAgentName ?? "",
|
|
4463
4665
|
history: this.history,
|
|
@@ -4629,6 +4831,25 @@ ${tailBody}
|
|
|
4629
4831
|
}
|
|
4630
4832
|
return this.turnDone("response-truncated");
|
|
4631
4833
|
}
|
|
4834
|
+
if (e instanceof BudgetExhaustedError) {
|
|
4835
|
+
this.budgetExhaustedThisTurn = true;
|
|
4836
|
+
this.budgetWallDetail = budgetDetailOf(e);
|
|
4837
|
+
this.budgetWallViaSubAgent = false;
|
|
4838
|
+
if (this.isSubAgent) {
|
|
4839
|
+
logger2.error("ChatDriver: AI budget exhausted", e);
|
|
4840
|
+
recordTurnError(this.sessionKey, "budget-exhausted", {
|
|
4841
|
+
agent: this.activeAgentName,
|
|
4842
|
+
provider: this.lastResolvedProviderName,
|
|
4843
|
+
vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
|
|
4844
|
+
budgetUsd: e.budgetUsd,
|
|
4845
|
+
spentUsd: e.spentUsd,
|
|
4846
|
+
isSubAgent: true
|
|
4847
|
+
});
|
|
4848
|
+
this.failSubAgent("budget_exhausted", this.budgetWallDetail);
|
|
4849
|
+
return this.turnDone("budget-exhausted", this.budgetWallDetail);
|
|
4850
|
+
}
|
|
4851
|
+
return this.reportBudgetExhausted(e);
|
|
4852
|
+
}
|
|
4632
4853
|
if (e instanceof DOMException && e.name === "TimeoutError") {
|
|
4633
4854
|
logger2.error("ChatDriver: request timed out", e);
|
|
4634
4855
|
recordTurnError(this.sessionKey, "exception", {
|
|
@@ -5104,6 +5325,17 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5104
5325
|
* user stops. Reset at the start of each `sendMessage`.
|
|
5105
5326
|
*/
|
|
5106
5327
|
this.cancelled = false;
|
|
5328
|
+
/**
|
|
5329
|
+
* Whether the CURRENT turn's user message has reached the inner driver's
|
|
5330
|
+
* history. False through the pre-first-turn classify (where the only echo of
|
|
5331
|
+
* the message is an optimistic `history-updated` dispatch), true from the
|
|
5332
|
+
* moment `chatDriver.sendMessage` is entered — including through every later
|
|
5333
|
+
* handoff classify. Read by the budget-wall catch in `sendMessage` to decide
|
|
5334
|
+
* whether `reportBudgetExhausted` must append the message itself: appending
|
|
5335
|
+
* it when already appended duplicated it; not appending it when unappended
|
|
5336
|
+
* made it vanish. Reset at the top of each `runOrchestratedTurn`.
|
|
5337
|
+
*/
|
|
5338
|
+
this.userMessageAppended = false;
|
|
5107
5339
|
/**
|
|
5108
5340
|
* Sticky user pick from the picker (or the host's `setAgent` API). Only
|
|
5109
5341
|
* changes on explicit user action. Survives flow completion: when a stateful
|
|
@@ -5145,7 +5377,8 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5145
5377
|
maxFoldOperations: options.maxFoldOperations,
|
|
5146
5378
|
maxTurnSnapshots: options.maxTurnSnapshots,
|
|
5147
5379
|
sessionKey: this.sessionKey,
|
|
5148
|
-
activityBus: options.activityBus
|
|
5380
|
+
activityBus: options.activityBus,
|
|
5381
|
+
budgetExhaustedMessage: options.budgetExhaustedMessage
|
|
5149
5382
|
});
|
|
5150
5383
|
this.chatDriver.addEventListener("history-updated", (e) => {
|
|
5151
5384
|
this.dispatchEvent(new CustomEvent("history-updated", { detail: e.detail }));
|
|
@@ -5244,7 +5477,25 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5244
5477
|
return this.chatDriver.getSuggestions(history, prompt2, count, agentInfo);
|
|
5245
5478
|
}
|
|
5246
5479
|
async sendMessage(input, attachments) {
|
|
5480
|
+
try {
|
|
5481
|
+
return await this.runOrchestratedTurn(input, attachments);
|
|
5482
|
+
} catch (e) {
|
|
5483
|
+
if (e instanceof BudgetExhaustedError) {
|
|
5484
|
+
return this.chatDriver.reportBudgetExhausted(
|
|
5485
|
+
e,
|
|
5486
|
+
this.userMessageAppended ? void 0 : {
|
|
5487
|
+
role: "user",
|
|
5488
|
+
content: input,
|
|
5489
|
+
...attachments ? { attachments } : {}
|
|
5490
|
+
}
|
|
5491
|
+
);
|
|
5492
|
+
}
|
|
5493
|
+
throw e;
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
async runOrchestratedTurn(input, attachments) {
|
|
5247
5497
|
this.cancelled = false;
|
|
5498
|
+
this.userMessageAppended = false;
|
|
5248
5499
|
const history = this.chatDriver.getHistory();
|
|
5249
5500
|
this.dispatchEvent(
|
|
5250
5501
|
new CustomEvent("history-updated", {
|
|
@@ -5258,6 +5509,7 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5258
5509
|
let handoffs = 0;
|
|
5259
5510
|
let handoffSummary = "";
|
|
5260
5511
|
let remainingTask = "";
|
|
5512
|
+
let lastResult;
|
|
5261
5513
|
while (true) {
|
|
5262
5514
|
if (this.cancelled) break;
|
|
5263
5515
|
await this.applyAgent(currentAgent);
|
|
@@ -5266,8 +5518,10 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5266
5518
|
const contextPrimer = handoffSummary ? [{ role: "user", content: `[Context from previous agent]: ${handoffSummary}` }] : [];
|
|
5267
5519
|
result = await this.chatDriver.continueFromHistory(contextPrimer);
|
|
5268
5520
|
} else {
|
|
5521
|
+
this.userMessageAppended = true;
|
|
5269
5522
|
result = await this.chatDriver.sendMessage(input, attachments);
|
|
5270
5523
|
}
|
|
5524
|
+
lastResult = result;
|
|
5271
5525
|
if (this.chatDriver.getAgentReleaseRequested()) {
|
|
5272
5526
|
await this.releaseActiveAgent();
|
|
5273
5527
|
break;
|
|
@@ -5289,7 +5543,7 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5289
5543
|
this.dispatchEvent(new CustomEvent("orchestrating-start"));
|
|
5290
5544
|
currentAgent = await this.classify(remainingTask, updatedHistory);
|
|
5291
5545
|
}
|
|
5292
|
-
return { reason: "done" };
|
|
5546
|
+
return lastResult?.reason === "done" ? lastResult : { reason: "done" };
|
|
5293
5547
|
}
|
|
5294
5548
|
async continueFromHistory(transientPrimer) {
|
|
5295
5549
|
return this.chatDriver.continueFromHistory(transientPrimer);
|
|
@@ -5472,6 +5726,7 @@ ${recentMessages}` : ""}`;
|
|
|
5472
5726
|
}
|
|
5473
5727
|
}
|
|
5474
5728
|
};
|
|
5729
|
+
let routedNoMatch = false;
|
|
5475
5730
|
for (let attempt = 0; attempt <= this.classifierRetries; attempt += 1) {
|
|
5476
5731
|
try {
|
|
5477
5732
|
const options = {
|
|
@@ -5491,8 +5746,10 @@ ${recentMessages}` : ""}`;
|
|
|
5491
5746
|
if (index >= 0 && index < this.specialists.length) {
|
|
5492
5747
|
return this.specialists[index];
|
|
5493
5748
|
}
|
|
5749
|
+
routedNoMatch = true;
|
|
5494
5750
|
break;
|
|
5495
5751
|
} catch (e) {
|
|
5752
|
+
if (e instanceof BudgetExhaustedError) throw e;
|
|
5496
5753
|
logger2.warn(`OrchestratingDriver: classifier attempt ${attempt + 1} failed:`, e);
|
|
5497
5754
|
if (attempt === this.classifierRetries) {
|
|
5498
5755
|
logger2.error("OrchestratingDriver: classifier failed after all retries, using fallback");
|
|
@@ -5500,10 +5757,12 @@ ${recentMessages}` : ""}`;
|
|
|
5500
5757
|
}
|
|
5501
5758
|
}
|
|
5502
5759
|
if (this.fallback) return this.fallback;
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5760
|
+
if (routedNoMatch) {
|
|
5761
|
+
const specialistNames = this.specialists.map((s) => s.name).join(", ");
|
|
5762
|
+
this.appendInlineMessage(
|
|
5763
|
+
`I'm not sure how to help with that. I can assist with: ${specialistNames}.`
|
|
5764
|
+
);
|
|
5765
|
+
}
|
|
5507
5766
|
return this.specialists[0];
|
|
5508
5767
|
}
|
|
5509
5768
|
/**
|