@genesislcap/ai-assistant 15.3.2 → 15.3.3-alpha-4899ddd.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-assistant.api.json +391 -5
- package/dist/ai-assistant.d.ts +603 -6
- package/dist/chat-driver.cjs +285 -26
- package/dist/chat-driver.cjs.map +3 -3
- package/dist/chat-driver.mjs +285 -26
- package/dist/chat-driver.mjs.map +3 -3
- package/dist/custom-elements.json +236 -10
- package/dist/dts/channel/ai-activity-channel.d.ts +51 -1
- package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +99 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +2 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +1 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/main/blocked-state.test.d.ts +2 -0
- package/dist/dts/main/blocked-state.test.d.ts.map +1 -0
- package/dist/dts/main/main.d.ts +384 -6
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.styles.test.d.ts +2 -0
- package/dist/dts/main/main.styles.test.d.ts.map +1 -0
- package/dist/dts/main/main.template.d.ts +53 -0
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +143 -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 +10 -0
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +263 -21
- package/dist/esm/components/chat-driver/chat-driver.test.js +464 -1
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +312 -0
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +89 -4
- package/dist/esm/main/blocked-state.test.js +913 -0
- package/dist/esm/main/main.js +680 -16
- package/dist/esm/main/main.styles.js +47 -0
- package/dist/esm/main/main.styles.test.js +86 -0
- package/dist/esm/main/main.template.js +121 -4
- package/dist/esm/state/ai-assistant-slice.js +128 -7
- package/dist/esm/state/ai-assistant-slice.test.js +138 -1
- package/dist/esm/state/debug-event-log.js +7 -2
- package/dist/esm/state/debug-event-log.test.js +49 -1
- package/dist/esm/state/persistence/session-snapshot.test.js +18 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/migration-GENC-1464.md +562 -0
- package/docs/sub_agent.md +20 -3
- package/package.json +17 -17
- package/src/channel/ai-activity-channel.ts +56 -2
- package/src/components/chat-driver/chat-driver.test.ts +549 -0
- package/src/components/chat-driver/chat-driver.ts +324 -14
- package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +438 -0
- package/src/components/orchestrating-driver/orchestrating-driver.ts +101 -6
- package/src/main/blocked-state.test.ts +1243 -0
- package/src/main/main.styles.test.ts +103 -0
- package/src/main/main.styles.ts +47 -0
- package/src/main/main.template.ts +131 -4
- package/src/main/main.ts +680 -10
- package/src/state/ai-assistant-slice.test.ts +215 -0
- package/src/state/ai-assistant-slice.ts +181 -8
- package/src/state/debug-event-log.test.ts +63 -0
- package/src/state/debug-event-log.ts +7 -2
- package/src/state/persistence/session-snapshot.test.ts +22 -0
package/dist/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);
|
|
@@ -1650,7 +1718,7 @@ ${att.content}` });
|
|
|
1650
1718
|
headers: requestHeaders,
|
|
1651
1719
|
body,
|
|
1652
1720
|
credentials,
|
|
1653
|
-
vendorLabel:
|
|
1721
|
+
vendorLabel: VENDOR_LABELS.anthropic,
|
|
1654
1722
|
retryableStatuses: _AnthropicTransport.RETRYABLE_STATUSES,
|
|
1655
1723
|
timeout: this.timeout,
|
|
1656
1724
|
stallTimeout: this.stallTimeout,
|
|
@@ -2299,7 +2367,7 @@ ${att.content}`
|
|
|
2299
2367
|
headers,
|
|
2300
2368
|
body: payload,
|
|
2301
2369
|
credentials,
|
|
2302
|
-
vendorLabel:
|
|
2370
|
+
vendorLabel: VENDOR_LABELS.gemini,
|
|
2303
2371
|
retryableStatuses: _GeminiTransport.RETRYABLE_STATUSES,
|
|
2304
2372
|
timeout: this.timeout,
|
|
2305
2373
|
stallTimeout: this.stallTimeout,
|
|
@@ -3001,6 +3069,19 @@ The above summarizes earlier parts of this conversation that were compacted. Con
|
|
|
3001
3069
|
var TOOL_FOLD_SYMBOL = Symbol("toolFold");
|
|
3002
3070
|
|
|
3003
3071
|
// src/components/chat-driver/chat-driver.ts
|
|
3072
|
+
var budgetDetailOf = (e) => {
|
|
3073
|
+
const vendor = vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor);
|
|
3074
|
+
if (e.budgetUsd == null && e.spentUsd == null && !vendor) return void 0;
|
|
3075
|
+
return {
|
|
3076
|
+
budgetUsd: e.budgetUsd,
|
|
3077
|
+
spentUsd: e.spentUsd,
|
|
3078
|
+
vendorLabel: e.vendorLabel,
|
|
3079
|
+
...vendor ? { vendor } : {},
|
|
3080
|
+
// Server-known truth the client cannot re-derive, so it rides all the way to
|
|
3081
|
+
// the banner rather than being re-guessed there from registry membership.
|
|
3082
|
+
...e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}
|
|
3083
|
+
};
|
|
3084
|
+
};
|
|
3004
3085
|
var DEFAULT_MAX_TOOL_ITERATIONS = 50;
|
|
3005
3086
|
var DEFAULT_MAX_FOLD_OPERATIONS = 5;
|
|
3006
3087
|
var DEFAULT_MAX_TURN_SNAPSHOTS = 400;
|
|
@@ -3183,6 +3264,22 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3183
3264
|
* picked up on the next turn.
|
|
3184
3265
|
*/
|
|
3185
3266
|
this.resolvedStatusCache = /* @__PURE__ */ new Map();
|
|
3267
|
+
/**
|
|
3268
|
+
* Set the moment a budget wall is observed anywhere in this turn — this
|
|
3269
|
+
* driver's own 402, or a sub-agent's (which surfaces here only as a
|
|
3270
|
+
* `'budget_exhausted'` tool result). Read at the top of the tool loop to end
|
|
3271
|
+
* the turn before issuing another model call that would hit the same wall.
|
|
3272
|
+
* Reset per turn alongside the other per-turn counters.
|
|
3273
|
+
*/
|
|
3274
|
+
this.budgetExhaustedThisTurn = false;
|
|
3275
|
+
/**
|
|
3276
|
+
* Whether this turn's budget wall came from a SUB-AGENT rather than this
|
|
3277
|
+
* driver's own request. Decides whether `lastResolvedProvider` is a valid
|
|
3278
|
+
* attribution fallback: for an own wall it is the refusing vendor, for a
|
|
3279
|
+
* child's wall it is the parent's vendor — the one known NOT to have refused.
|
|
3280
|
+
* Reset per turn alongside `budgetWallDetail`.
|
|
3281
|
+
*/
|
|
3282
|
+
this.budgetWallViaSubAgent = false;
|
|
3186
3283
|
const {
|
|
3187
3284
|
toolHandlers = {},
|
|
3188
3285
|
toolDefinitions = [],
|
|
@@ -3192,11 +3289,13 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3192
3289
|
maxFoldOperations = DEFAULT_MAX_FOLD_OPERATIONS,
|
|
3193
3290
|
maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
|
|
3194
3291
|
sessionKey = "",
|
|
3195
|
-
activityBus = NOOP_ACTIVITY_BUS
|
|
3292
|
+
activityBus = NOOP_ACTIVITY_BUS,
|
|
3293
|
+
budgetExhaustedMessage = DEFAULT_BUDGET_EXHAUSTED_MESSAGE
|
|
3196
3294
|
} = config;
|
|
3197
3295
|
this.maxToolIterations = maxToolIterations;
|
|
3198
3296
|
this.sessionKey = sessionKey;
|
|
3199
3297
|
this.activityBus = activityBus;
|
|
3298
|
+
this.budgetExhaustedMessage = budgetExhaustedMessage;
|
|
3200
3299
|
if (typeof toolHandlers === "function") {
|
|
3201
3300
|
this.toolHandlersFactory = toolHandlers;
|
|
3202
3301
|
this.toolHandlers = {};
|
|
@@ -3303,8 +3402,56 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3303
3402
|
* (rather than set to `undefined`) so a happy-path result stays byte-identical to
|
|
3304
3403
|
* the historical `{ reason: 'done' }`.
|
|
3305
3404
|
*/
|
|
3306
|
-
turnDone(failureReason) {
|
|
3307
|
-
|
|
3405
|
+
turnDone(failureReason, budget) {
|
|
3406
|
+
if (!failureReason) return { reason: "done" };
|
|
3407
|
+
return budget ? { reason: "done", failureReason, budget } : { reason: "done", failureReason };
|
|
3408
|
+
}
|
|
3409
|
+
/**
|
|
3410
|
+
* Terminal budget outcome for a wall hit **outside** the tool loop — today,
|
|
3411
|
+
* `OrchestratingDriver`'s classification phase, which calls the provider
|
|
3412
|
+
* directly and so never enters `runToolLoop`.
|
|
3413
|
+
*
|
|
3414
|
+
* Does **not** publish `tool-loop-end`: no `tool-loop-start` was published for
|
|
3415
|
+
* the classify phase, and an unbalanced end would break start/end pairing for
|
|
3416
|
+
* subscribers that rely on it. The driver **return value** is what reports this
|
|
3417
|
+
* case — see `FoundationAiAssistant`'s latch, which reads both seams for
|
|
3418
|
+
* exactly this reason.
|
|
3419
|
+
*
|
|
3420
|
+
* The non-sub-agent tail of the in-loop `BudgetExhaustedError` branch lives
|
|
3421
|
+
* here so there is one copy of the log line, the debug-log entry, the
|
|
3422
|
+
* transcript bubble and the result shape rather than two that can drift.
|
|
3423
|
+
*
|
|
3424
|
+
* @param pendingUserMessage - a user message that has NOT yet been appended,
|
|
3425
|
+
* appended first so the answer does not end up replying to nothing. Only the
|
|
3426
|
+
* classification seam passes it: `OrchestratingDriver` dispatches the user's
|
|
3427
|
+
* text as an optimistic `history-updated` detail and leaves the real append
|
|
3428
|
+
* to `chatDriver.sendMessage`, which never runs when `classify()` throws — so
|
|
3429
|
+
* the bubble below would re-dispatch a history the user's own message was
|
|
3430
|
+
* never in, and it would vanish from the transcript on the next render. The
|
|
3431
|
+
* in-loop caller has already appended it and passes nothing.
|
|
3432
|
+
*
|
|
3433
|
+
* @internal
|
|
3434
|
+
*/
|
|
3435
|
+
reportBudgetExhausted(e, pendingUserMessage) {
|
|
3436
|
+
if (pendingUserMessage) this.appendToHistory(pendingUserMessage);
|
|
3437
|
+
this.budgetExhaustedThisTurn = true;
|
|
3438
|
+
logger2.error("ChatDriver: AI budget exhausted", e);
|
|
3439
|
+
recordTurnError(this.sessionKey, "budget-exhausted", {
|
|
3440
|
+
agent: this.activeAgentName,
|
|
3441
|
+
provider: this.lastResolvedProviderName,
|
|
3442
|
+
// The registry ALIAS (e.g. 'high') is what `provider` records; the vendor
|
|
3443
|
+
// is the thing a per-vendor budget is actually scoped to, and it was known
|
|
3444
|
+
// at both ends and discarded in the middle until now. Taken from the
|
|
3445
|
+
// refusing transport's label first — this method also serves the
|
|
3446
|
+
// classification seam, where `lastResolvedProvider` is the PREVIOUS turn's
|
|
3447
|
+
// vendor (or nothing), because classify runs against the registry default.
|
|
3448
|
+
vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
|
|
3449
|
+
budgetUsd: e.budgetUsd,
|
|
3450
|
+
spentUsd: e.spentUsd,
|
|
3451
|
+
isSubAgent: this.isSubAgent
|
|
3452
|
+
});
|
|
3453
|
+
this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
|
|
3454
|
+
return this.turnDone("budget-exhausted", budgetDetailOf(e));
|
|
3308
3455
|
}
|
|
3309
3456
|
/** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
|
|
3310
3457
|
static failureReasonOf(result) {
|
|
@@ -3314,10 +3461,28 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3314
3461
|
* Build the `tool-loop-end` event detail for a turn's result. A failure carries a
|
|
3315
3462
|
* `{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,
|
|
3316
3463
|
* kept byte-identical so subscribers see exactly what they always have.
|
|
3464
|
+
*
|
|
3465
|
+
* A budget failure additionally carries `vendor` — the concrete vendor
|
|
3466
|
+
* (`'anthropic'`/`'gemini'`) the walled turn resolved to, which the driver knows
|
|
3467
|
+
* and used to discard. Optional and additive: a subscriber reading only
|
|
3468
|
+
* `failureReason` is unaffected, a non-budget failure still emits the historical
|
|
3469
|
+
* `{ failureReason }` with no `vendor` key, and the value is a plain string so
|
|
3470
|
+
* the detail stays structured-cloneable for the cross-tab hop. It is the field a
|
|
3471
|
+
* per-vendor budget model needs and the one that would be awkward to retrofit.
|
|
3317
3472
|
*/
|
|
3318
|
-
|
|
3473
|
+
loopEndDetail(result) {
|
|
3319
3474
|
const failureReason = _ChatDriver.failureReasonOf(result);
|
|
3320
|
-
|
|
3475
|
+
if (!failureReason) return void 0;
|
|
3476
|
+
if (failureReason !== "budget-exhausted") {
|
|
3477
|
+
return { failureReason };
|
|
3478
|
+
}
|
|
3479
|
+
const budget = result.reason === "done" ? result.budget : void 0;
|
|
3480
|
+
const vendor = budget?.vendor ?? (this.budgetWallViaSubAgent ? void 0 : this.lastResolvedProvider);
|
|
3481
|
+
return {
|
|
3482
|
+
failureReason,
|
|
3483
|
+
...vendor ? { vendor } : {},
|
|
3484
|
+
...budget ? { budget } : {}
|
|
3485
|
+
};
|
|
3321
3486
|
}
|
|
3322
3487
|
/**
|
|
3323
3488
|
* Swap in a new agent's configuration. Called by OrchestratingDriver before
|
|
@@ -3503,9 +3668,9 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3503
3668
|
* under a separate session key, so recording here would orphan the event off
|
|
3504
3669
|
* the user-visible debug-log timeline.)
|
|
3505
3670
|
*/
|
|
3506
|
-
failSubAgent(reason) {
|
|
3671
|
+
failSubAgent(reason, budget) {
|
|
3507
3672
|
if (!this.isSubAgent || this.subAgentFailure) return;
|
|
3508
|
-
this.subAgentFailure = { reason };
|
|
3673
|
+
this.subAgentFailure = budget ? { reason, budget } : { reason };
|
|
3509
3674
|
}
|
|
3510
3675
|
/**
|
|
3511
3676
|
* Returns true if `releaseAgent` was called during the most recent turn.
|
|
@@ -3950,6 +4115,9 @@ Output format (strict):
|
|
|
3950
4115
|
this.subAgentCompletion = void 0;
|
|
3951
4116
|
this.subAgentFailure = void 0;
|
|
3952
4117
|
this.agentReleaseRequested = false;
|
|
4118
|
+
this.budgetExhaustedThisTurn = false;
|
|
4119
|
+
this.budgetWallDetail = void 0;
|
|
4120
|
+
this.budgetWallViaSubAgent = false;
|
|
3953
4121
|
this.appendToHistory({ role: "user", content: userInput, attachments });
|
|
3954
4122
|
this.turnStartedAt = Date.now();
|
|
3955
4123
|
recordMetaEvent(this.sessionKey, "turn.start", {
|
|
@@ -3984,7 +4152,7 @@ Output format (strict):
|
|
|
3984
4152
|
});
|
|
3985
4153
|
this.busy = false;
|
|
3986
4154
|
this.endTurn();
|
|
3987
|
-
this.activityBus.publish("tool-loop-end",
|
|
4155
|
+
this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
|
|
3988
4156
|
}
|
|
3989
4157
|
}
|
|
3990
4158
|
/**
|
|
@@ -4175,8 +4343,14 @@ Output format (strict):
|
|
|
4175
4343
|
recordMetaEvent(this.sessionKey, "subagent.completed", { agent: name });
|
|
4176
4344
|
return { outcome: { ok: true, result: completion.result }, trace };
|
|
4177
4345
|
}
|
|
4178
|
-
const
|
|
4346
|
+
const failure = child.getSubAgentFailure();
|
|
4347
|
+
const reason = failure?.reason ?? "max_iterations";
|
|
4179
4348
|
recordMetaEvent(this.sessionKey, "subagent.failed", { agent: name, reason });
|
|
4349
|
+
if (reason === "budget_exhausted") {
|
|
4350
|
+
this.budgetExhaustedThisTurn = true;
|
|
4351
|
+
this.budgetWallViaSubAgent = true;
|
|
4352
|
+
this.budgetWallDetail ??= failure?.budget;
|
|
4353
|
+
}
|
|
4180
4354
|
return { outcome: { ok: false, reason }, trace };
|
|
4181
4355
|
}
|
|
4182
4356
|
/**
|
|
@@ -4189,6 +4363,9 @@ Output format (strict):
|
|
|
4189
4363
|
this.beginTurn();
|
|
4190
4364
|
this.subAgentCompletion = void 0;
|
|
4191
4365
|
this.subAgentFailure = void 0;
|
|
4366
|
+
this.budgetExhaustedThisTurn = false;
|
|
4367
|
+
this.budgetWallDetail = void 0;
|
|
4368
|
+
this.budgetWallViaSubAgent = false;
|
|
4192
4369
|
this.turnStartedAt = Date.now();
|
|
4193
4370
|
recordMetaEvent(this.sessionKey, "turn.start", {
|
|
4194
4371
|
phase: "continueFromHistory",
|
|
@@ -4222,7 +4399,7 @@ Output format (strict):
|
|
|
4222
4399
|
});
|
|
4223
4400
|
this.busy = false;
|
|
4224
4401
|
this.endTurn();
|
|
4225
|
-
this.activityBus.publish("tool-loop-end",
|
|
4402
|
+
this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
|
|
4226
4403
|
}
|
|
4227
4404
|
}
|
|
4228
4405
|
// ---------------------------------------------------------------------------
|
|
@@ -4376,6 +4553,31 @@ Output format (strict):
|
|
|
4376
4553
|
if (this.turnController.signal.aborted) {
|
|
4377
4554
|
return this.completeAbortedTurn();
|
|
4378
4555
|
}
|
|
4556
|
+
if (this.budgetExhaustedThisTurn) {
|
|
4557
|
+
logger2.error("ChatDriver: ending the turn \u2014 a sub-agent hit the AI budget wall");
|
|
4558
|
+
recordTurnError(this.sessionKey, "budget-exhausted", {
|
|
4559
|
+
agent: this.activeAgentName,
|
|
4560
|
+
provider: this.lastResolvedProviderName,
|
|
4561
|
+
// The CHILD's vendor when it knew one, and NOTHING otherwise. On this
|
|
4562
|
+
// path the wall is definitionally the child's, and on a mixed registry
|
|
4563
|
+
// `lastResolvedProvider` is this driver's own vendor — the one known
|
|
4564
|
+
// NOT to have refused. An unattributable child wall must degrade to
|
|
4565
|
+
// the vendor-agnostic block (which `latchBlockedFrom` handles
|
|
4566
|
+
// fail-safe), never to a vendor that is known to be wrong: naming the
|
|
4567
|
+
// parent's vendor here walled BOTH — the child's via its own
|
|
4568
|
+
// tool-loop-end, the parent's via this event — and derived `blocked`
|
|
4569
|
+
// over headroom that still existed.
|
|
4570
|
+
vendor: this.budgetWallDetail?.vendor,
|
|
4571
|
+
via: "sub-agent",
|
|
4572
|
+
isSubAgent: this.isSubAgent
|
|
4573
|
+
});
|
|
4574
|
+
if (this.isSubAgent) {
|
|
4575
|
+
this.failSubAgent("budget_exhausted", this.budgetWallDetail);
|
|
4576
|
+
} else {
|
|
4577
|
+
this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
|
|
4578
|
+
}
|
|
4579
|
+
return this.turnDone("budget-exhausted", this.budgetWallDetail);
|
|
4580
|
+
}
|
|
4379
4581
|
const promptCtx = {
|
|
4380
4582
|
agentName: this.activeAgentName ?? "",
|
|
4381
4583
|
history: this.history,
|
|
@@ -4546,6 +4748,25 @@ ${tailBody}
|
|
|
4546
4748
|
}
|
|
4547
4749
|
return this.turnDone("response-truncated");
|
|
4548
4750
|
}
|
|
4751
|
+
if (e instanceof BudgetExhaustedError) {
|
|
4752
|
+
this.budgetExhaustedThisTurn = true;
|
|
4753
|
+
this.budgetWallDetail = budgetDetailOf(e);
|
|
4754
|
+
this.budgetWallViaSubAgent = false;
|
|
4755
|
+
if (this.isSubAgent) {
|
|
4756
|
+
logger2.error("ChatDriver: AI budget exhausted", e);
|
|
4757
|
+
recordTurnError(this.sessionKey, "budget-exhausted", {
|
|
4758
|
+
agent: this.activeAgentName,
|
|
4759
|
+
provider: this.lastResolvedProviderName,
|
|
4760
|
+
vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
|
|
4761
|
+
budgetUsd: e.budgetUsd,
|
|
4762
|
+
spentUsd: e.spentUsd,
|
|
4763
|
+
isSubAgent: true
|
|
4764
|
+
});
|
|
4765
|
+
this.failSubAgent("budget_exhausted", this.budgetWallDetail);
|
|
4766
|
+
return this.turnDone("budget-exhausted", this.budgetWallDetail);
|
|
4767
|
+
}
|
|
4768
|
+
return this.reportBudgetExhausted(e);
|
|
4769
|
+
}
|
|
4549
4770
|
if (e instanceof DOMException && e.name === "TimeoutError") {
|
|
4550
4771
|
logger2.error("ChatDriver: request timed out", e);
|
|
4551
4772
|
recordTurnError(this.sessionKey, "exception", {
|
|
@@ -5021,6 +5242,17 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5021
5242
|
* user stops. Reset at the start of each `sendMessage`.
|
|
5022
5243
|
*/
|
|
5023
5244
|
this.cancelled = false;
|
|
5245
|
+
/**
|
|
5246
|
+
* Whether the CURRENT turn's user message has reached the inner driver's
|
|
5247
|
+
* history. False through the pre-first-turn classify (where the only echo of
|
|
5248
|
+
* the message is an optimistic `history-updated` dispatch), true from the
|
|
5249
|
+
* moment `chatDriver.sendMessage` is entered — including through every later
|
|
5250
|
+
* handoff classify. Read by the budget-wall catch in `sendMessage` to decide
|
|
5251
|
+
* whether `reportBudgetExhausted` must append the message itself: appending
|
|
5252
|
+
* it when already appended duplicated it; not appending it when unappended
|
|
5253
|
+
* made it vanish. Reset at the top of each `runOrchestratedTurn`.
|
|
5254
|
+
*/
|
|
5255
|
+
this.userMessageAppended = false;
|
|
5024
5256
|
/**
|
|
5025
5257
|
* Sticky user pick from the picker (or the host's `setAgent` API). Only
|
|
5026
5258
|
* changes on explicit user action. Survives flow completion: when a stateful
|
|
@@ -5062,7 +5294,8 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5062
5294
|
maxFoldOperations: options.maxFoldOperations,
|
|
5063
5295
|
maxTurnSnapshots: options.maxTurnSnapshots,
|
|
5064
5296
|
sessionKey: this.sessionKey,
|
|
5065
|
-
activityBus: options.activityBus
|
|
5297
|
+
activityBus: options.activityBus,
|
|
5298
|
+
budgetExhaustedMessage: options.budgetExhaustedMessage
|
|
5066
5299
|
});
|
|
5067
5300
|
this.chatDriver.addEventListener("history-updated", (e) => {
|
|
5068
5301
|
this.dispatchEvent(new CustomEvent("history-updated", { detail: e.detail }));
|
|
@@ -5161,7 +5394,25 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5161
5394
|
return this.chatDriver.getSuggestions(history, prompt2, count, agentInfo);
|
|
5162
5395
|
}
|
|
5163
5396
|
async sendMessage(input, attachments) {
|
|
5397
|
+
try {
|
|
5398
|
+
return await this.runOrchestratedTurn(input, attachments);
|
|
5399
|
+
} catch (e) {
|
|
5400
|
+
if (e instanceof BudgetExhaustedError) {
|
|
5401
|
+
return this.chatDriver.reportBudgetExhausted(
|
|
5402
|
+
e,
|
|
5403
|
+
this.userMessageAppended ? void 0 : {
|
|
5404
|
+
role: "user",
|
|
5405
|
+
content: input,
|
|
5406
|
+
...attachments ? { attachments } : {}
|
|
5407
|
+
}
|
|
5408
|
+
);
|
|
5409
|
+
}
|
|
5410
|
+
throw e;
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
async runOrchestratedTurn(input, attachments) {
|
|
5164
5414
|
this.cancelled = false;
|
|
5415
|
+
this.userMessageAppended = false;
|
|
5165
5416
|
const history = this.chatDriver.getHistory();
|
|
5166
5417
|
this.dispatchEvent(
|
|
5167
5418
|
new CustomEvent("history-updated", {
|
|
@@ -5175,6 +5426,7 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5175
5426
|
let handoffs = 0;
|
|
5176
5427
|
let handoffSummary = "";
|
|
5177
5428
|
let remainingTask = "";
|
|
5429
|
+
let lastResult;
|
|
5178
5430
|
while (true) {
|
|
5179
5431
|
if (this.cancelled) break;
|
|
5180
5432
|
await this.applyAgent(currentAgent);
|
|
@@ -5183,8 +5435,10 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5183
5435
|
const contextPrimer = handoffSummary ? [{ role: "user", content: `[Context from previous agent]: ${handoffSummary}` }] : [];
|
|
5184
5436
|
result = await this.chatDriver.continueFromHistory(contextPrimer);
|
|
5185
5437
|
} else {
|
|
5438
|
+
this.userMessageAppended = true;
|
|
5186
5439
|
result = await this.chatDriver.sendMessage(input, attachments);
|
|
5187
5440
|
}
|
|
5441
|
+
lastResult = result;
|
|
5188
5442
|
if (this.chatDriver.getAgentReleaseRequested()) {
|
|
5189
5443
|
await this.releaseActiveAgent();
|
|
5190
5444
|
break;
|
|
@@ -5206,7 +5460,7 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5206
5460
|
this.dispatchEvent(new CustomEvent("orchestrating-start"));
|
|
5207
5461
|
currentAgent = await this.classify(remainingTask, updatedHistory);
|
|
5208
5462
|
}
|
|
5209
|
-
return { reason: "done" };
|
|
5463
|
+
return lastResult?.reason === "done" ? lastResult : { reason: "done" };
|
|
5210
5464
|
}
|
|
5211
5465
|
async continueFromHistory(transientPrimer) {
|
|
5212
5466
|
return this.chatDriver.continueFromHistory(transientPrimer);
|
|
@@ -5389,6 +5643,7 @@ ${recentMessages}` : ""}`;
|
|
|
5389
5643
|
}
|
|
5390
5644
|
}
|
|
5391
5645
|
};
|
|
5646
|
+
let routedNoMatch = false;
|
|
5392
5647
|
for (let attempt = 0; attempt <= this.classifierRetries; attempt += 1) {
|
|
5393
5648
|
try {
|
|
5394
5649
|
const options = {
|
|
@@ -5408,8 +5663,10 @@ ${recentMessages}` : ""}`;
|
|
|
5408
5663
|
if (index >= 0 && index < this.specialists.length) {
|
|
5409
5664
|
return this.specialists[index];
|
|
5410
5665
|
}
|
|
5666
|
+
routedNoMatch = true;
|
|
5411
5667
|
break;
|
|
5412
5668
|
} catch (e) {
|
|
5669
|
+
if (e instanceof BudgetExhaustedError) throw e;
|
|
5413
5670
|
logger2.warn(`OrchestratingDriver: classifier attempt ${attempt + 1} failed:`, e);
|
|
5414
5671
|
if (attempt === this.classifierRetries) {
|
|
5415
5672
|
logger2.error("OrchestratingDriver: classifier failed after all retries, using fallback");
|
|
@@ -5417,10 +5674,12 @@ ${recentMessages}` : ""}`;
|
|
|
5417
5674
|
}
|
|
5418
5675
|
}
|
|
5419
5676
|
if (this.fallback) return this.fallback;
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5677
|
+
if (routedNoMatch) {
|
|
5678
|
+
const specialistNames = this.specialists.map((s) => s.name).join(", ");
|
|
5679
|
+
this.appendInlineMessage(
|
|
5680
|
+
`I'm not sure how to help with that. I can assist with: ${specialistNames}.`
|
|
5681
|
+
);
|
|
5682
|
+
}
|
|
5424
5683
|
return this.specialists[0];
|
|
5425
5684
|
}
|
|
5426
5685
|
/**
|