@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.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);
|
|
@@ -1603,7 +1671,7 @@ ${att.content}` });
|
|
|
1603
1671
|
headers: requestHeaders,
|
|
1604
1672
|
body,
|
|
1605
1673
|
credentials,
|
|
1606
|
-
vendorLabel:
|
|
1674
|
+
vendorLabel: VENDOR_LABELS.anthropic,
|
|
1607
1675
|
retryableStatuses: _AnthropicTransport.RETRYABLE_STATUSES,
|
|
1608
1676
|
timeout: this.timeout,
|
|
1609
1677
|
stallTimeout: this.stallTimeout,
|
|
@@ -2252,7 +2320,7 @@ ${att.content}`
|
|
|
2252
2320
|
headers,
|
|
2253
2321
|
body: payload,
|
|
2254
2322
|
credentials,
|
|
2255
|
-
vendorLabel:
|
|
2323
|
+
vendorLabel: VENDOR_LABELS.gemini,
|
|
2256
2324
|
retryableStatuses: _GeminiTransport.RETRYABLE_STATUSES,
|
|
2257
2325
|
timeout: this.timeout,
|
|
2258
2326
|
stallTimeout: this.stallTimeout,
|
|
@@ -2954,6 +3022,19 @@ The above summarizes earlier parts of this conversation that were compacted. Con
|
|
|
2954
3022
|
var TOOL_FOLD_SYMBOL = Symbol("toolFold");
|
|
2955
3023
|
|
|
2956
3024
|
// src/components/chat-driver/chat-driver.ts
|
|
3025
|
+
var budgetDetailOf = (e) => {
|
|
3026
|
+
const vendor = vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor);
|
|
3027
|
+
if (e.budgetUsd == null && e.spentUsd == null && !vendor) return void 0;
|
|
3028
|
+
return {
|
|
3029
|
+
budgetUsd: e.budgetUsd,
|
|
3030
|
+
spentUsd: e.spentUsd,
|
|
3031
|
+
vendorLabel: e.vendorLabel,
|
|
3032
|
+
...vendor ? { vendor } : {},
|
|
3033
|
+
// Server-known truth the client cannot re-derive, so it rides all the way to
|
|
3034
|
+
// the banner rather than being re-guessed there from registry membership.
|
|
3035
|
+
...e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}
|
|
3036
|
+
};
|
|
3037
|
+
};
|
|
2957
3038
|
var DEFAULT_MAX_TOOL_ITERATIONS = 50;
|
|
2958
3039
|
var DEFAULT_MAX_FOLD_OPERATIONS = 5;
|
|
2959
3040
|
var DEFAULT_MAX_TURN_SNAPSHOTS = 400;
|
|
@@ -3136,6 +3217,22 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3136
3217
|
* picked up on the next turn.
|
|
3137
3218
|
*/
|
|
3138
3219
|
this.resolvedStatusCache = /* @__PURE__ */ new Map();
|
|
3220
|
+
/**
|
|
3221
|
+
* Set the moment a budget wall is observed anywhere in this turn — this
|
|
3222
|
+
* driver's own 402, or a sub-agent's (which surfaces here only as a
|
|
3223
|
+
* `'budget_exhausted'` tool result). Read at the top of the tool loop to end
|
|
3224
|
+
* the turn before issuing another model call that would hit the same wall.
|
|
3225
|
+
* Reset per turn alongside the other per-turn counters.
|
|
3226
|
+
*/
|
|
3227
|
+
this.budgetExhaustedThisTurn = false;
|
|
3228
|
+
/**
|
|
3229
|
+
* Whether this turn's budget wall came from a SUB-AGENT rather than this
|
|
3230
|
+
* driver's own request. Decides whether `lastResolvedProvider` is a valid
|
|
3231
|
+
* attribution fallback: for an own wall it is the refusing vendor, for a
|
|
3232
|
+
* child's wall it is the parent's vendor — the one known NOT to have refused.
|
|
3233
|
+
* Reset per turn alongside `budgetWallDetail`.
|
|
3234
|
+
*/
|
|
3235
|
+
this.budgetWallViaSubAgent = false;
|
|
3139
3236
|
const {
|
|
3140
3237
|
toolHandlers = {},
|
|
3141
3238
|
toolDefinitions = [],
|
|
@@ -3145,11 +3242,13 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3145
3242
|
maxFoldOperations = DEFAULT_MAX_FOLD_OPERATIONS,
|
|
3146
3243
|
maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
|
|
3147
3244
|
sessionKey = "",
|
|
3148
|
-
activityBus = NOOP_ACTIVITY_BUS
|
|
3245
|
+
activityBus = NOOP_ACTIVITY_BUS,
|
|
3246
|
+
budgetExhaustedMessage = DEFAULT_BUDGET_EXHAUSTED_MESSAGE
|
|
3149
3247
|
} = config;
|
|
3150
3248
|
this.maxToolIterations = maxToolIterations;
|
|
3151
3249
|
this.sessionKey = sessionKey;
|
|
3152
3250
|
this.activityBus = activityBus;
|
|
3251
|
+
this.budgetExhaustedMessage = budgetExhaustedMessage;
|
|
3153
3252
|
if (typeof toolHandlers === "function") {
|
|
3154
3253
|
this.toolHandlersFactory = toolHandlers;
|
|
3155
3254
|
this.toolHandlers = {};
|
|
@@ -3256,8 +3355,56 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3256
3355
|
* (rather than set to `undefined`) so a happy-path result stays byte-identical to
|
|
3257
3356
|
* the historical `{ reason: 'done' }`.
|
|
3258
3357
|
*/
|
|
3259
|
-
turnDone(failureReason) {
|
|
3260
|
-
|
|
3358
|
+
turnDone(failureReason, budget) {
|
|
3359
|
+
if (!failureReason) return { reason: "done" };
|
|
3360
|
+
return budget ? { reason: "done", failureReason, budget } : { reason: "done", failureReason };
|
|
3361
|
+
}
|
|
3362
|
+
/**
|
|
3363
|
+
* Terminal budget outcome for a wall hit **outside** the tool loop — today,
|
|
3364
|
+
* `OrchestratingDriver`'s classification phase, which calls the provider
|
|
3365
|
+
* directly and so never enters `runToolLoop`.
|
|
3366
|
+
*
|
|
3367
|
+
* Does **not** publish `tool-loop-end`: no `tool-loop-start` was published for
|
|
3368
|
+
* the classify phase, and an unbalanced end would break start/end pairing for
|
|
3369
|
+
* subscribers that rely on it. The driver **return value** is what reports this
|
|
3370
|
+
* case — see `FoundationAiAssistant`'s latch, which reads both seams for
|
|
3371
|
+
* exactly this reason.
|
|
3372
|
+
*
|
|
3373
|
+
* The non-sub-agent tail of the in-loop `BudgetExhaustedError` branch lives
|
|
3374
|
+
* here so there is one copy of the log line, the debug-log entry, the
|
|
3375
|
+
* transcript bubble and the result shape rather than two that can drift.
|
|
3376
|
+
*
|
|
3377
|
+
* @param pendingUserMessage - a user message that has NOT yet been appended,
|
|
3378
|
+
* appended first so the answer does not end up replying to nothing. Only the
|
|
3379
|
+
* classification seam passes it: `OrchestratingDriver` dispatches the user's
|
|
3380
|
+
* text as an optimistic `history-updated` detail and leaves the real append
|
|
3381
|
+
* to `chatDriver.sendMessage`, which never runs when `classify()` throws — so
|
|
3382
|
+
* the bubble below would re-dispatch a history the user's own message was
|
|
3383
|
+
* never in, and it would vanish from the transcript on the next render. The
|
|
3384
|
+
* in-loop caller has already appended it and passes nothing.
|
|
3385
|
+
*
|
|
3386
|
+
* @internal
|
|
3387
|
+
*/
|
|
3388
|
+
reportBudgetExhausted(e, pendingUserMessage) {
|
|
3389
|
+
if (pendingUserMessage) this.appendToHistory(pendingUserMessage);
|
|
3390
|
+
this.budgetExhaustedThisTurn = true;
|
|
3391
|
+
logger2.error("ChatDriver: AI budget exhausted", e);
|
|
3392
|
+
recordTurnError(this.sessionKey, "budget-exhausted", {
|
|
3393
|
+
agent: this.activeAgentName,
|
|
3394
|
+
provider: this.lastResolvedProviderName,
|
|
3395
|
+
// The registry ALIAS (e.g. 'high') is what `provider` records; the vendor
|
|
3396
|
+
// is the thing a per-vendor budget is actually scoped to, and it was known
|
|
3397
|
+
// at both ends and discarded in the middle until now. Taken from the
|
|
3398
|
+
// refusing transport's label first — this method also serves the
|
|
3399
|
+
// classification seam, where `lastResolvedProvider` is the PREVIOUS turn's
|
|
3400
|
+
// vendor (or nothing), because classify runs against the registry default.
|
|
3401
|
+
vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
|
|
3402
|
+
budgetUsd: e.budgetUsd,
|
|
3403
|
+
spentUsd: e.spentUsd,
|
|
3404
|
+
isSubAgent: this.isSubAgent
|
|
3405
|
+
});
|
|
3406
|
+
this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
|
|
3407
|
+
return this.turnDone("budget-exhausted", budgetDetailOf(e));
|
|
3261
3408
|
}
|
|
3262
3409
|
/** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
|
|
3263
3410
|
static failureReasonOf(result) {
|
|
@@ -3267,10 +3414,28 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3267
3414
|
* Build the `tool-loop-end` event detail for a turn's result. A failure carries a
|
|
3268
3415
|
* `{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,
|
|
3269
3416
|
* kept byte-identical so subscribers see exactly what they always have.
|
|
3417
|
+
*
|
|
3418
|
+
* A budget failure additionally carries `vendor` — the concrete vendor
|
|
3419
|
+
* (`'anthropic'`/`'gemini'`) the walled turn resolved to, which the driver knows
|
|
3420
|
+
* and used to discard. Optional and additive: a subscriber reading only
|
|
3421
|
+
* `failureReason` is unaffected, a non-budget failure still emits the historical
|
|
3422
|
+
* `{ failureReason }` with no `vendor` key, and the value is a plain string so
|
|
3423
|
+
* the detail stays structured-cloneable for the cross-tab hop. It is the field a
|
|
3424
|
+
* per-vendor budget model needs and the one that would be awkward to retrofit.
|
|
3270
3425
|
*/
|
|
3271
|
-
|
|
3426
|
+
loopEndDetail(result) {
|
|
3272
3427
|
const failureReason = _ChatDriver.failureReasonOf(result);
|
|
3273
|
-
|
|
3428
|
+
if (!failureReason) return void 0;
|
|
3429
|
+
if (failureReason !== "budget-exhausted") {
|
|
3430
|
+
return { failureReason };
|
|
3431
|
+
}
|
|
3432
|
+
const budget = result.reason === "done" ? result.budget : void 0;
|
|
3433
|
+
const vendor = budget?.vendor ?? (this.budgetWallViaSubAgent ? void 0 : this.lastResolvedProvider);
|
|
3434
|
+
return {
|
|
3435
|
+
failureReason,
|
|
3436
|
+
...vendor ? { vendor } : {},
|
|
3437
|
+
...budget ? { budget } : {}
|
|
3438
|
+
};
|
|
3274
3439
|
}
|
|
3275
3440
|
/**
|
|
3276
3441
|
* Swap in a new agent's configuration. Called by OrchestratingDriver before
|
|
@@ -3456,9 +3621,9 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
3456
3621
|
* under a separate session key, so recording here would orphan the event off
|
|
3457
3622
|
* the user-visible debug-log timeline.)
|
|
3458
3623
|
*/
|
|
3459
|
-
failSubAgent(reason) {
|
|
3624
|
+
failSubAgent(reason, budget) {
|
|
3460
3625
|
if (!this.isSubAgent || this.subAgentFailure) return;
|
|
3461
|
-
this.subAgentFailure = { reason };
|
|
3626
|
+
this.subAgentFailure = budget ? { reason, budget } : { reason };
|
|
3462
3627
|
}
|
|
3463
3628
|
/**
|
|
3464
3629
|
* Returns true if `releaseAgent` was called during the most recent turn.
|
|
@@ -3903,6 +4068,9 @@ Output format (strict):
|
|
|
3903
4068
|
this.subAgentCompletion = void 0;
|
|
3904
4069
|
this.subAgentFailure = void 0;
|
|
3905
4070
|
this.agentReleaseRequested = false;
|
|
4071
|
+
this.budgetExhaustedThisTurn = false;
|
|
4072
|
+
this.budgetWallDetail = void 0;
|
|
4073
|
+
this.budgetWallViaSubAgent = false;
|
|
3906
4074
|
this.appendToHistory({ role: "user", content: userInput, attachments });
|
|
3907
4075
|
this.turnStartedAt = Date.now();
|
|
3908
4076
|
recordMetaEvent(this.sessionKey, "turn.start", {
|
|
@@ -3937,7 +4105,7 @@ Output format (strict):
|
|
|
3937
4105
|
});
|
|
3938
4106
|
this.busy = false;
|
|
3939
4107
|
this.endTurn();
|
|
3940
|
-
this.activityBus.publish("tool-loop-end",
|
|
4108
|
+
this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
|
|
3941
4109
|
}
|
|
3942
4110
|
}
|
|
3943
4111
|
/**
|
|
@@ -4128,8 +4296,14 @@ Output format (strict):
|
|
|
4128
4296
|
recordMetaEvent(this.sessionKey, "subagent.completed", { agent: name });
|
|
4129
4297
|
return { outcome: { ok: true, result: completion.result }, trace };
|
|
4130
4298
|
}
|
|
4131
|
-
const
|
|
4299
|
+
const failure = child.getSubAgentFailure();
|
|
4300
|
+
const reason = failure?.reason ?? "max_iterations";
|
|
4132
4301
|
recordMetaEvent(this.sessionKey, "subagent.failed", { agent: name, reason });
|
|
4302
|
+
if (reason === "budget_exhausted") {
|
|
4303
|
+
this.budgetExhaustedThisTurn = true;
|
|
4304
|
+
this.budgetWallViaSubAgent = true;
|
|
4305
|
+
this.budgetWallDetail ??= failure?.budget;
|
|
4306
|
+
}
|
|
4133
4307
|
return { outcome: { ok: false, reason }, trace };
|
|
4134
4308
|
}
|
|
4135
4309
|
/**
|
|
@@ -4142,6 +4316,9 @@ Output format (strict):
|
|
|
4142
4316
|
this.beginTurn();
|
|
4143
4317
|
this.subAgentCompletion = void 0;
|
|
4144
4318
|
this.subAgentFailure = void 0;
|
|
4319
|
+
this.budgetExhaustedThisTurn = false;
|
|
4320
|
+
this.budgetWallDetail = void 0;
|
|
4321
|
+
this.budgetWallViaSubAgent = false;
|
|
4145
4322
|
this.turnStartedAt = Date.now();
|
|
4146
4323
|
recordMetaEvent(this.sessionKey, "turn.start", {
|
|
4147
4324
|
phase: "continueFromHistory",
|
|
@@ -4175,7 +4352,7 @@ Output format (strict):
|
|
|
4175
4352
|
});
|
|
4176
4353
|
this.busy = false;
|
|
4177
4354
|
this.endTurn();
|
|
4178
|
-
this.activityBus.publish("tool-loop-end",
|
|
4355
|
+
this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
|
|
4179
4356
|
}
|
|
4180
4357
|
}
|
|
4181
4358
|
// ---------------------------------------------------------------------------
|
|
@@ -4329,6 +4506,31 @@ Output format (strict):
|
|
|
4329
4506
|
if (this.turnController.signal.aborted) {
|
|
4330
4507
|
return this.completeAbortedTurn();
|
|
4331
4508
|
}
|
|
4509
|
+
if (this.budgetExhaustedThisTurn) {
|
|
4510
|
+
logger2.error("ChatDriver: ending the turn \u2014 a sub-agent hit the AI budget wall");
|
|
4511
|
+
recordTurnError(this.sessionKey, "budget-exhausted", {
|
|
4512
|
+
agent: this.activeAgentName,
|
|
4513
|
+
provider: this.lastResolvedProviderName,
|
|
4514
|
+
// The CHILD's vendor when it knew one, and NOTHING otherwise. On this
|
|
4515
|
+
// path the wall is definitionally the child's, and on a mixed registry
|
|
4516
|
+
// `lastResolvedProvider` is this driver's own vendor — the one known
|
|
4517
|
+
// NOT to have refused. An unattributable child wall must degrade to
|
|
4518
|
+
// the vendor-agnostic block (which `latchBlockedFrom` handles
|
|
4519
|
+
// fail-safe), never to a vendor that is known to be wrong: naming the
|
|
4520
|
+
// parent's vendor here walled BOTH — the child's via its own
|
|
4521
|
+
// tool-loop-end, the parent's via this event — and derived `blocked`
|
|
4522
|
+
// over headroom that still existed.
|
|
4523
|
+
vendor: this.budgetWallDetail?.vendor,
|
|
4524
|
+
via: "sub-agent",
|
|
4525
|
+
isSubAgent: this.isSubAgent
|
|
4526
|
+
});
|
|
4527
|
+
if (this.isSubAgent) {
|
|
4528
|
+
this.failSubAgent("budget_exhausted", this.budgetWallDetail);
|
|
4529
|
+
} else {
|
|
4530
|
+
this.appendToHistory({ role: "assistant", content: this.budgetExhaustedMessage });
|
|
4531
|
+
}
|
|
4532
|
+
return this.turnDone("budget-exhausted", this.budgetWallDetail);
|
|
4533
|
+
}
|
|
4332
4534
|
const promptCtx = {
|
|
4333
4535
|
agentName: this.activeAgentName ?? "",
|
|
4334
4536
|
history: this.history,
|
|
@@ -4499,6 +4701,25 @@ ${tailBody}
|
|
|
4499
4701
|
}
|
|
4500
4702
|
return this.turnDone("response-truncated");
|
|
4501
4703
|
}
|
|
4704
|
+
if (e instanceof BudgetExhaustedError) {
|
|
4705
|
+
this.budgetExhaustedThisTurn = true;
|
|
4706
|
+
this.budgetWallDetail = budgetDetailOf(e);
|
|
4707
|
+
this.budgetWallViaSubAgent = false;
|
|
4708
|
+
if (this.isSubAgent) {
|
|
4709
|
+
logger2.error("ChatDriver: AI budget exhausted", e);
|
|
4710
|
+
recordTurnError(this.sessionKey, "budget-exhausted", {
|
|
4711
|
+
agent: this.activeAgentName,
|
|
4712
|
+
provider: this.lastResolvedProviderName,
|
|
4713
|
+
vendor: vendorTypeOfLabel(e.vendorLabel) ?? vendorTypeOfLabel(e.serverVendor) ?? this.lastResolvedProvider,
|
|
4714
|
+
budgetUsd: e.budgetUsd,
|
|
4715
|
+
spentUsd: e.spentUsd,
|
|
4716
|
+
isSubAgent: true
|
|
4717
|
+
});
|
|
4718
|
+
this.failSubAgent("budget_exhausted", this.budgetWallDetail);
|
|
4719
|
+
return this.turnDone("budget-exhausted", this.budgetWallDetail);
|
|
4720
|
+
}
|
|
4721
|
+
return this.reportBudgetExhausted(e);
|
|
4722
|
+
}
|
|
4502
4723
|
if (e instanceof DOMException && e.name === "TimeoutError") {
|
|
4503
4724
|
logger2.error("ChatDriver: request timed out", e);
|
|
4504
4725
|
recordTurnError(this.sessionKey, "exception", {
|
|
@@ -4974,6 +5195,17 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
4974
5195
|
* user stops. Reset at the start of each `sendMessage`.
|
|
4975
5196
|
*/
|
|
4976
5197
|
this.cancelled = false;
|
|
5198
|
+
/**
|
|
5199
|
+
* Whether the CURRENT turn's user message has reached the inner driver's
|
|
5200
|
+
* history. False through the pre-first-turn classify (where the only echo of
|
|
5201
|
+
* the message is an optimistic `history-updated` dispatch), true from the
|
|
5202
|
+
* moment `chatDriver.sendMessage` is entered — including through every later
|
|
5203
|
+
* handoff classify. Read by the budget-wall catch in `sendMessage` to decide
|
|
5204
|
+
* whether `reportBudgetExhausted` must append the message itself: appending
|
|
5205
|
+
* it when already appended duplicated it; not appending it when unappended
|
|
5206
|
+
* made it vanish. Reset at the top of each `runOrchestratedTurn`.
|
|
5207
|
+
*/
|
|
5208
|
+
this.userMessageAppended = false;
|
|
4977
5209
|
/**
|
|
4978
5210
|
* Sticky user pick from the picker (or the host's `setAgent` API). Only
|
|
4979
5211
|
* changes on explicit user action. Survives flow completion: when a stateful
|
|
@@ -5015,7 +5247,8 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5015
5247
|
maxFoldOperations: options.maxFoldOperations,
|
|
5016
5248
|
maxTurnSnapshots: options.maxTurnSnapshots,
|
|
5017
5249
|
sessionKey: this.sessionKey,
|
|
5018
|
-
activityBus: options.activityBus
|
|
5250
|
+
activityBus: options.activityBus,
|
|
5251
|
+
budgetExhaustedMessage: options.budgetExhaustedMessage
|
|
5019
5252
|
});
|
|
5020
5253
|
this.chatDriver.addEventListener("history-updated", (e) => {
|
|
5021
5254
|
this.dispatchEvent(new CustomEvent("history-updated", { detail: e.detail }));
|
|
@@ -5114,7 +5347,25 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5114
5347
|
return this.chatDriver.getSuggestions(history, prompt2, count, agentInfo);
|
|
5115
5348
|
}
|
|
5116
5349
|
async sendMessage(input, attachments) {
|
|
5350
|
+
try {
|
|
5351
|
+
return await this.runOrchestratedTurn(input, attachments);
|
|
5352
|
+
} catch (e) {
|
|
5353
|
+
if (e instanceof BudgetExhaustedError) {
|
|
5354
|
+
return this.chatDriver.reportBudgetExhausted(
|
|
5355
|
+
e,
|
|
5356
|
+
this.userMessageAppended ? void 0 : {
|
|
5357
|
+
role: "user",
|
|
5358
|
+
content: input,
|
|
5359
|
+
...attachments ? { attachments } : {}
|
|
5360
|
+
}
|
|
5361
|
+
);
|
|
5362
|
+
}
|
|
5363
|
+
throw e;
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
async runOrchestratedTurn(input, attachments) {
|
|
5117
5367
|
this.cancelled = false;
|
|
5368
|
+
this.userMessageAppended = false;
|
|
5118
5369
|
const history = this.chatDriver.getHistory();
|
|
5119
5370
|
this.dispatchEvent(
|
|
5120
5371
|
new CustomEvent("history-updated", {
|
|
@@ -5128,6 +5379,7 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5128
5379
|
let handoffs = 0;
|
|
5129
5380
|
let handoffSummary = "";
|
|
5130
5381
|
let remainingTask = "";
|
|
5382
|
+
let lastResult;
|
|
5131
5383
|
while (true) {
|
|
5132
5384
|
if (this.cancelled) break;
|
|
5133
5385
|
await this.applyAgent(currentAgent);
|
|
@@ -5136,8 +5388,10 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5136
5388
|
const contextPrimer = handoffSummary ? [{ role: "user", content: `[Context from previous agent]: ${handoffSummary}` }] : [];
|
|
5137
5389
|
result = await this.chatDriver.continueFromHistory(contextPrimer);
|
|
5138
5390
|
} else {
|
|
5391
|
+
this.userMessageAppended = true;
|
|
5139
5392
|
result = await this.chatDriver.sendMessage(input, attachments);
|
|
5140
5393
|
}
|
|
5394
|
+
lastResult = result;
|
|
5141
5395
|
if (this.chatDriver.getAgentReleaseRequested()) {
|
|
5142
5396
|
await this.releaseActiveAgent();
|
|
5143
5397
|
break;
|
|
@@ -5159,7 +5413,7 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
5159
5413
|
this.dispatchEvent(new CustomEvent("orchestrating-start"));
|
|
5160
5414
|
currentAgent = await this.classify(remainingTask, updatedHistory);
|
|
5161
5415
|
}
|
|
5162
|
-
return { reason: "done" };
|
|
5416
|
+
return lastResult?.reason === "done" ? lastResult : { reason: "done" };
|
|
5163
5417
|
}
|
|
5164
5418
|
async continueFromHistory(transientPrimer) {
|
|
5165
5419
|
return this.chatDriver.continueFromHistory(transientPrimer);
|
|
@@ -5342,6 +5596,7 @@ ${recentMessages}` : ""}`;
|
|
|
5342
5596
|
}
|
|
5343
5597
|
}
|
|
5344
5598
|
};
|
|
5599
|
+
let routedNoMatch = false;
|
|
5345
5600
|
for (let attempt = 0; attempt <= this.classifierRetries; attempt += 1) {
|
|
5346
5601
|
try {
|
|
5347
5602
|
const options = {
|
|
@@ -5361,8 +5616,10 @@ ${recentMessages}` : ""}`;
|
|
|
5361
5616
|
if (index >= 0 && index < this.specialists.length) {
|
|
5362
5617
|
return this.specialists[index];
|
|
5363
5618
|
}
|
|
5619
|
+
routedNoMatch = true;
|
|
5364
5620
|
break;
|
|
5365
5621
|
} catch (e) {
|
|
5622
|
+
if (e instanceof BudgetExhaustedError) throw e;
|
|
5366
5623
|
logger2.warn(`OrchestratingDriver: classifier attempt ${attempt + 1} failed:`, e);
|
|
5367
5624
|
if (attempt === this.classifierRetries) {
|
|
5368
5625
|
logger2.error("OrchestratingDriver: classifier failed after all retries, using fallback");
|
|
@@ -5370,10 +5627,12 @@ ${recentMessages}` : ""}`;
|
|
|
5370
5627
|
}
|
|
5371
5628
|
}
|
|
5372
5629
|
if (this.fallback) return this.fallback;
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5630
|
+
if (routedNoMatch) {
|
|
5631
|
+
const specialistNames = this.specialists.map((s) => s.name).join(", ");
|
|
5632
|
+
this.appendInlineMessage(
|
|
5633
|
+
`I'm not sure how to help with that. I can assist with: ${specialistNames}.`
|
|
5634
|
+
);
|
|
5635
|
+
}
|
|
5377
5636
|
return this.specialists[0];
|
|
5378
5637
|
}
|
|
5379
5638
|
/**
|