@genesislcap/ai-assistant 15.19.6 → 15.20.0-alpha.3
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 +707 -69
- package/dist/ai-assistant.d.ts +443 -25
- package/dist/chat-driver.cjs +440 -34
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +440 -34
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +709 -47
- package/dist/dts/components/ai-driver/ai-driver.d.ts +40 -7
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +81 -2
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +23 -3
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +44 -0
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +187 -5
- 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.template.d.ts.map +1 -1
- package/dist/dts/state/persistence/diagnostics-cursors.d.ts +7 -2
- package/dist/dts/state/persistence/diagnostics-cursors.d.ts.map +1 -1
- package/dist/dts/state/persistence/diagnostics.d.ts +30 -0
- package/dist/dts/state/persistence/diagnostics.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-persister.d.ts +18 -0
- package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
- package/dist/dts/utils/condense-history.d.ts +35 -0
- package/dist/dts/utils/condense-history.d.ts.map +1 -1
- package/dist/dts/utils/context-tokens.d.ts +156 -0
- package/dist/dts/utils/context-tokens.d.ts.map +1 -0
- package/dist/dts/utils/history-transform.d.ts +76 -14
- package/dist/dts/utils/history-transform.d.ts.map +1 -1
- package/dist/dts/utils/resolve-context-budget.d.ts +98 -0
- package/dist/dts/utils/resolve-context-budget.d.ts.map +1 -0
- package/dist/esm/components/chat-driver/chat-driver.js +200 -35
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +28 -4
- package/dist/esm/main/main.js +400 -24
- package/dist/esm/main/main.styles.js +128 -0
- package/dist/esm/main/main.template.js +64 -29
- package/dist/esm/state/debug-event-log.js +1 -1
- package/dist/esm/state/persistence/diagnostics-cursors.js +1 -1
- package/dist/esm/state/persistence/diagnostics.js +49 -0
- package/dist/esm/state/persistence/session-persister.js +38 -18
- package/dist/esm/utils/condense-history.js +125 -26
- package/dist/esm/utils/context-tokens.js +339 -0
- package/dist/esm/utils/history-transform.js +101 -19
- package/dist/esm/utils/resolve-context-budget.js +84 -0
- package/package.json +16 -16
- package/sandbox/README.md +93 -4
- package/sandbox/controls.ts +77 -10
- package/sandbox/fixtures.ts +163 -6
- package/sandbox/sandbox.css +54 -1
- package/sandbox/sandbox.ts +384 -7
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-context-budget.d.ts","sourceRoot":"","sources":["../../../src/utils/resolve-context-budget.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAO7D,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,0BAA0B,EAAE,CAAC;AAEtC;;;;GAIG;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C,6DAA6D;AAC7D,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,OAAQ,CAAC;AAEjD,iEAAiE;AACjE,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;OAOG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,8CAA8C;AAC9C,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,UAAU,EAAE,UAAU,CAAC;IACvB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,kBAAkB,GAAG,qBAAqB,CAmDrF"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { __awaiter, __rest } from "tslib";
|
|
2
|
-
import { BudgetExhaustedError, DEFAULT_BUDGET_EXHAUSTED_MESSAGE, DEFAULT_PROVIDER_REFUSED_MESSAGE, ProviderRefusedError, isObservableAIProviderRegistry, MalformedFunctionCallError, ResponseTruncatedError, vendorTypeOfLabel, } from '@genesislcap/foundation-ai';
|
|
2
|
+
import { BudgetExhaustedError, DEFAULT_BUDGET_EXHAUSTED_MESSAGE, DEFAULT_PROVIDER_REFUSED_MESSAGE, ContextOverflowError, DEFAULT_CONTEXT_OVERFLOW_MESSAGE, ProviderRefusedError, isObservableAIProviderRegistry, MalformedFunctionCallError, ResponseTruncatedError, vendorTypeOfLabel, } from '@genesislcap/foundation-ai';
|
|
3
3
|
import { NOOP_ACTIVITY_BUS } from '../../channel/ai-activity-bus';
|
|
4
4
|
import { resolveChatProvider } from '../../config/validate-providers';
|
|
5
5
|
import { clearSession, getMetaEvents, mergeMetaEvents, recordMetaEvent, recordTurnError, recordTurnRetry, } from '../../state/debug-event-log';
|
|
6
6
|
import { createInteractionContext, } from '../../state/interaction-context';
|
|
7
|
-
import { applyCondensation } from '../../utils/condense-history';
|
|
8
|
-
import {
|
|
7
|
+
import { applyCondensation, condenseForPersistence, } from '../../utils/condense-history';
|
|
8
|
+
import { estimateRequestTokens } from '../../utils/context-tokens';
|
|
9
|
+
import { applyHistoryCap, buildCompactionSummaryPrompt, planCompaction, normalizeForProvider, } from '../../utils/history-transform';
|
|
9
10
|
import { logger } from '../../utils/logger';
|
|
10
11
|
import { messageUsage, sumUsage } from '../../utils/sum-usage';
|
|
11
12
|
import { TOOL_FOLD_SYMBOL } from '../../utils/tool-fold';
|
|
@@ -115,16 +116,6 @@ const HARVESTED_SUBAGENT_EVENTS = new Set([
|
|
|
115
116
|
export const REQUEST_CONTINUATION_TOOL = 'request_continuation';
|
|
116
117
|
/** Paired in history for each `request_continuation` so tool_calls stay balanced for the provider. */
|
|
117
118
|
const HANDOFF_TOOL_RESULT_PLACEHOLDER = 'Handoff to another specialist — routing continues on the next turn.';
|
|
118
|
-
/**
|
|
119
|
-
* Plain TS class that drives a multi-turn chat conversation, including the tool-call loop.
|
|
120
|
-
* Owned by `FoundationAiAssistant` — created in `connectedCallback`, torn down in `disconnectedCallback`.
|
|
121
|
-
*
|
|
122
|
-
* Dispatches `'history-updated'` events on itself so the owning element can observe changes.
|
|
123
|
-
*
|
|
124
|
-
* @fires history-updated - Fired whenever the in-memory chat history changes (append, tool loop, interaction resolution, post-resolve external cost) with the full history snapshot. detail: `ReadonlyArray<ChatMessage>`
|
|
125
|
-
*
|
|
126
|
-
* @beta
|
|
127
|
-
*/
|
|
128
119
|
export class ChatDriver extends EventTarget {
|
|
129
120
|
constructor(providerRegistry, config = {}) {
|
|
130
121
|
super();
|
|
@@ -755,6 +746,12 @@ export class ChatDriver extends EventTarget {
|
|
|
755
746
|
const status = yield this.resolveStatusForProvider(resolvedName, provider);
|
|
756
747
|
this.lastResolvedModel = status.model;
|
|
757
748
|
this.lastResolvedProvider = status.provider;
|
|
749
|
+
// The window of the provider THIS call will go to. Read here rather than
|
|
750
|
+
// pushed down by the host, because the host cannot know it until the later
|
|
751
|
+
// `provider-changed` event — so a per-agent provider switch would otherwise
|
|
752
|
+
// have its first call guarded by the previous model's limit, refusing a valid
|
|
753
|
+
// turn on the way up and waving an oversized one through on the way down.
|
|
754
|
+
this.lastResolvedContextLimit = status.contextLimit;
|
|
758
755
|
if (resolvedName !== this.lastDispatchedProviderName) {
|
|
759
756
|
this.lastDispatchedProviderName = resolvedName;
|
|
760
757
|
recordMetaEvent(this.sessionKey, 'provider.selected', {
|
|
@@ -788,7 +785,13 @@ export class ChatDriver extends EventTarget {
|
|
|
788
785
|
try {
|
|
789
786
|
const resolved = yield ((_a = provider.getStatus) === null || _a === void 0 ? void 0 : _a.call(provider));
|
|
790
787
|
if (resolved) {
|
|
791
|
-
status = {
|
|
788
|
+
status = {
|
|
789
|
+
model: resolved.model,
|
|
790
|
+
provider: resolved.provider,
|
|
791
|
+
// Carried so the mid-loop guard can measure against the window of the
|
|
792
|
+
// provider this call actually resolved to (GENC-1567).
|
|
793
|
+
contextLimit: resolved.contextLimit,
|
|
794
|
+
};
|
|
792
795
|
}
|
|
793
796
|
}
|
|
794
797
|
catch (_b) {
|
|
@@ -962,9 +965,62 @@ export class ChatDriver extends EventTarget {
|
|
|
962
965
|
return snapshot;
|
|
963
966
|
}
|
|
964
967
|
/**
|
|
965
|
-
*
|
|
966
|
-
*
|
|
968
|
+
* Set the mid-loop context guard. Its margin is deliberately far smaller than
|
|
969
|
+
* the reserve that blocks NEW turns: that reserve exists so an accepted turn can
|
|
970
|
+
* spend it, and a guard set at the same line would kill every turn that used
|
|
971
|
+
* the headroom it was given.
|
|
967
972
|
*/
|
|
973
|
+
setContextGuard(policy) {
|
|
974
|
+
this.contextGuard = policy && policy.marginTokens > 0 ? policy : undefined;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Whether issuing `requestHistory` would run the context window out.
|
|
978
|
+
*
|
|
979
|
+
* Inert unless a margin has been set AND the resolved provider reports a
|
|
980
|
+
* window — with no window there is nothing to measure against, and inventing
|
|
981
|
+
* one to refuse a request is worse than letting the provider answer.
|
|
982
|
+
*
|
|
983
|
+
* Stopping here is strictly better than letting the request go: the provider
|
|
984
|
+
* would reject an oversized prompt outright, leaving a transcript still too
|
|
985
|
+
* large to retry and no explanation the user can act on, whereas ending the
|
|
986
|
+
* turn keeps history intact so compaction is still available and the work so
|
|
987
|
+
* far is not lost.
|
|
988
|
+
*/
|
|
989
|
+
contextExhausted(requestHistory, pendingInput) {
|
|
990
|
+
var _a, _b;
|
|
991
|
+
const guard = this.contextGuard;
|
|
992
|
+
if (!guard)
|
|
993
|
+
return undefined;
|
|
994
|
+
// The resolved provider's own window, falling back to the host's assumption
|
|
995
|
+
// only when the provider reports none. Without the fallback a host that knows
|
|
996
|
+
// its window but uses a provider that does not report it got a UI gate and no
|
|
997
|
+
// running-turn guard — the fallback protecting the composer and nothing else.
|
|
998
|
+
const limit = (_a = this.lastResolvedContextLimit) !== null && _a !== void 0 ? _a : guard.fallbackLimit;
|
|
999
|
+
if (limit == null || limit <= 0)
|
|
1000
|
+
return undefined;
|
|
1001
|
+
const threshold = Math.max(0, limit - guard.marginTokens);
|
|
1002
|
+
// The first call of a turn passes the user's text and attachments SEPARATELY
|
|
1003
|
+
// (`sendMessage` appended them to history, and `baseHistory` drops that last
|
|
1004
|
+
// message precisely because they are supplied out of band). Measuring only
|
|
1005
|
+
// `requestHistory` therefore omitted them — so a large paste or a set of
|
|
1006
|
+
// images, the very payloads most likely to overrun a window, were invisible to
|
|
1007
|
+
// the guard that exists to catch them.
|
|
1008
|
+
const request = pendingInput && (pendingInput.content || ((_b = pendingInput.attachments) === null || _b === void 0 ? void 0 : _b.length))
|
|
1009
|
+
? [
|
|
1010
|
+
...requestHistory,
|
|
1011
|
+
{
|
|
1012
|
+
role: 'user',
|
|
1013
|
+
content: pendingInput.content,
|
|
1014
|
+
attachments: pendingInput.attachments,
|
|
1015
|
+
},
|
|
1016
|
+
]
|
|
1017
|
+
: requestHistory;
|
|
1018
|
+
const estimated = estimateRequestTokens(this.history, request);
|
|
1019
|
+
if (estimated < threshold)
|
|
1020
|
+
return undefined;
|
|
1021
|
+
logger.error(`ChatDriver: ending the turn — the request would fill the context window (~${estimated} of ${limit})`);
|
|
1022
|
+
return { estimated, threshold };
|
|
1023
|
+
}
|
|
968
1024
|
setProviderHistoryTransform(transform) {
|
|
969
1025
|
this.providerHistoryTransform = transform;
|
|
970
1026
|
}
|
|
@@ -978,6 +1034,26 @@ export class ChatDriver extends EventTarget {
|
|
|
978
1034
|
getActiveFoldNames() {
|
|
979
1035
|
return this.foldStack.map((f) => f.foldName);
|
|
980
1036
|
}
|
|
1037
|
+
/**
|
|
1038
|
+
* The transcript as it should be **stored**: every tool payload the model has already stopped
|
|
1039
|
+
* seeing replaced by the same stub it was sent (GENC-1566). Live history is untouched — this
|
|
1040
|
+
* returns a copy.
|
|
1041
|
+
*
|
|
1042
|
+
* Condensation deliberately spares stored history, because the live transcript and the debug
|
|
1043
|
+
* export both read it. A host writing that transcript to durable storage inherits the other
|
|
1044
|
+
* half of that decision: it stores payloads the driver has already retired, on every autosave,
|
|
1045
|
+
* for the life of the session. Measured on one small Create session — 58,187 chars, 7.4% of a
|
|
1046
|
+
* 790 KB session object, across five payloads worth 14,434 tokens of context, written 86 times.
|
|
1047
|
+
*
|
|
1048
|
+
* Only already-collapsed payloads are touched, so this can never store a stub for something the
|
|
1049
|
+
* model can still see, and it has no side effects (see `condenseForPersistence`, which is
|
|
1050
|
+
* internal). A host that wants the unredacted transcript still calls
|
|
1051
|
+
* {@link ChatDriver.getRawHistory}, and a diagnostics stream that keeps full payloads is
|
|
1052
|
+
* unaffected — which is what makes the elision recoverable.
|
|
1053
|
+
*/
|
|
1054
|
+
getHistoryForPersistence() {
|
|
1055
|
+
return condenseForPersistence([...this.history], this.condensePolicies);
|
|
1056
|
+
}
|
|
981
1057
|
getSuggestions(history, prompt, count, allAgentInfo) {
|
|
982
1058
|
return __awaiter(this, void 0, void 0, function* () {
|
|
983
1059
|
// Suggestions are an out-of-turn UI helper, not bound to any single agent —
|
|
@@ -1085,8 +1161,19 @@ export class ChatDriver extends EventTarget {
|
|
|
1085
1161
|
* turns exists behind a clean boundary. Uses the same `history` `compact()`
|
|
1086
1162
|
* acts on, so the UI's gate can't disagree with the action (GENC-1351 follow-up).
|
|
1087
1163
|
*/
|
|
1088
|
-
canCompact() {
|
|
1089
|
-
return
|
|
1164
|
+
canCompact(options) {
|
|
1165
|
+
return this.getCompactionPlan(options) != null;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* {@inheritDoc AiDriver.getCompactionPlan}
|
|
1169
|
+
*
|
|
1170
|
+
* Runs against the driver's own `history` — the exact list `compact()` acts on
|
|
1171
|
+
* — so a projection and the compaction it describes can never be computed from
|
|
1172
|
+
* different transcripts (the GENC-1351 follow-up that first made `canCompact`
|
|
1173
|
+
* read `history` rather than a mirrored copy).
|
|
1174
|
+
*/
|
|
1175
|
+
getCompactionPlan(options) {
|
|
1176
|
+
return planCompaction(this.history, options);
|
|
1090
1177
|
}
|
|
1091
1178
|
/**
|
|
1092
1179
|
* Destructively compact older turns into a single `compacted-summary` message
|
|
@@ -1097,9 +1184,9 @@ export class ChatDriver extends EventTarget {
|
|
|
1097
1184
|
* the new one. Returns the created summary message, or `null` when there is
|
|
1098
1185
|
* nothing worth compacting or the default provider cannot summarize.
|
|
1099
1186
|
*/
|
|
1100
|
-
compact() {
|
|
1187
|
+
compact(options) {
|
|
1101
1188
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1102
|
-
var _a, _b, _c, _d, _e, _f;
|
|
1189
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1103
1190
|
if (this.busy)
|
|
1104
1191
|
return null;
|
|
1105
1192
|
const defaultProvider = this.providerRegistry.default();
|
|
@@ -1108,7 +1195,10 @@ export class ChatDriver extends EventTarget {
|
|
|
1108
1195
|
return null;
|
|
1109
1196
|
}
|
|
1110
1197
|
const history = this.history;
|
|
1111
|
-
|
|
1198
|
+
// Same plan the affordance was gated on — including its worth-it floor, so a
|
|
1199
|
+
// compaction that would reclaim too little is declined here as well as being
|
|
1200
|
+
// greyed out in the UI.
|
|
1201
|
+
const cut = (_a = planCompaction(history, options)) === null || _a === void 0 ? void 0 : _a.cut;
|
|
1112
1202
|
if (cut == null)
|
|
1113
1203
|
return null;
|
|
1114
1204
|
const toCompact = history.slice(0, cut);
|
|
@@ -1122,7 +1212,7 @@ export class ChatDriver extends EventTarget {
|
|
|
1122
1212
|
const failMessage = "Sorry, I couldn't compact the conversation just now. Nothing was changed — please try again.";
|
|
1123
1213
|
let summaryText;
|
|
1124
1214
|
try {
|
|
1125
|
-
summaryText = (
|
|
1215
|
+
summaryText = (_c = (_b = (yield defaultProvider.prompt(userMessage, { systemPrompt }))) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : '';
|
|
1126
1216
|
}
|
|
1127
1217
|
catch (e) {
|
|
1128
1218
|
logger.warn(`ChatDriver: compaction summary failed: ${e instanceof Error ? e.message : e}`);
|
|
@@ -1141,7 +1231,7 @@ export class ChatDriver extends EventTarget {
|
|
|
1141
1231
|
break;
|
|
1142
1232
|
}
|
|
1143
1233
|
}
|
|
1144
|
-
const status = yield ((
|
|
1234
|
+
const status = yield ((_d = defaultProvider.getStatus) === null || _d === void 0 ? void 0 : _d.call(defaultProvider));
|
|
1145
1235
|
const createdAt = new Date().toISOString();
|
|
1146
1236
|
const summaryMessage = {
|
|
1147
1237
|
role: 'compacted-summary',
|
|
@@ -1150,10 +1240,10 @@ export class ChatDriver extends EventTarget {
|
|
|
1150
1240
|
// for the oldest messages and sits at the head of history, so this keeps it
|
|
1151
1241
|
// sorted to the head in any timestamp-ordered view (the debug timeline) and
|
|
1152
1242
|
// history time-monotonic. Its true creation time is in compaction.createdAt.
|
|
1153
|
-
timestamp: (
|
|
1243
|
+
timestamp: (_f = (_e = toCompact[0]) === null || _e === void 0 ? void 0 : _e.timestamp) !== null && _f !== void 0 ? _f : createdAt,
|
|
1154
1244
|
compaction: {
|
|
1155
1245
|
compactedCount: toCompact.length,
|
|
1156
|
-
coveredThroughTimestamp: (
|
|
1246
|
+
coveredThroughTimestamp: (_g = toCompact[toCompact.length - 1]) === null || _g === void 0 ? void 0 : _g.timestamp,
|
|
1157
1247
|
tokensBefore,
|
|
1158
1248
|
model: status === null || status === void 0 ? void 0 : status.model,
|
|
1159
1249
|
createdAt,
|
|
@@ -1701,6 +1791,11 @@ export class ChatDriver extends EventTarget {
|
|
|
1701
1791
|
// Mark before the first turn so the child forces tool use and reports a
|
|
1702
1792
|
// typed failure (rather than user-facing text) if it never completes.
|
|
1703
1793
|
child.markAsSubAgent();
|
|
1794
|
+
// The guard is policy, and a sub-agent runs the same tool loop against the
|
|
1795
|
+
// same window. Without this every child was inert — and the
|
|
1796
|
+
// `context_exhausted` sub-agent failure this feature added was unreachable by
|
|
1797
|
+
// any normal path.
|
|
1798
|
+
child.setContextGuard(this.contextGuard);
|
|
1704
1799
|
// Propagate disposal: if this (parent) driver is torn down while the
|
|
1705
1800
|
// sub-agent is mid-flight, dispose the child too so its in-flight request
|
|
1706
1801
|
// aborts. Detached in the `finally` below once the sub-agent completes.
|
|
@@ -2085,7 +2180,7 @@ export class ChatDriver extends EventTarget {
|
|
|
2085
2180
|
// oxlint-disable-next-line complexity
|
|
2086
2181
|
runToolLoop(userInput, attachments, transientPrimer) {
|
|
2087
2182
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2088
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
2183
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
|
|
2089
2184
|
if (!this.systemPrompt) {
|
|
2090
2185
|
logger.warn('ChatDriver: no systemPrompt set. The assistant will have no instructions — provide a systemPrompt via agents config or the foundation-ai-assistant property.');
|
|
2091
2186
|
}
|
|
@@ -2399,6 +2494,43 @@ export class ChatDriver extends EventTarget {
|
|
|
2399
2494
|
turnSnapshot.provider = this.lastResolvedProvider;
|
|
2400
2495
|
if (this.lastResolvedModel !== undefined)
|
|
2401
2496
|
turnSnapshot.model = this.lastResolvedModel;
|
|
2497
|
+
// The context wall (GENC-1567) — checked HERE, at the last possible moment,
|
|
2498
|
+
// rather than at the top of the iteration where it started life.
|
|
2499
|
+
//
|
|
2500
|
+
// Two things are only knowable at this point, and getting either wrong
|
|
2501
|
+
// ended valid turns. `historyForCall` is what will actually be sent, after
|
|
2502
|
+
// `condenseWhen` has stubbed spent tool payloads and per-agent masking has
|
|
2503
|
+
// blanked another agent's — stored history deliberately keeps them in full,
|
|
2504
|
+
// so measuring it refused turns whose real request was a fraction of the
|
|
2505
|
+
// size. And `lastResolvedContextLimit` is the window of the provider THIS
|
|
2506
|
+
// call resolved to, which on an agent switch is not the one the host knew
|
|
2507
|
+
// about when it last pushed a threshold down.
|
|
2508
|
+
const contextStop = this.contextExhausted(historyForCall, {
|
|
2509
|
+
content: userInputForCall,
|
|
2510
|
+
attachments: attachmentsForCall,
|
|
2511
|
+
});
|
|
2512
|
+
if (contextStop) {
|
|
2513
|
+
recordTurnError(this.sessionKey, 'context-exhausted', {
|
|
2514
|
+
agent: this.activeAgentName,
|
|
2515
|
+
provider: this.lastResolvedProviderName,
|
|
2516
|
+
contextTokens: contextStop.estimated,
|
|
2517
|
+
guard: contextStop.threshold,
|
|
2518
|
+
limit: this.lastResolvedContextLimit,
|
|
2519
|
+
iterations,
|
|
2520
|
+
isSubAgent: this.isSubAgent,
|
|
2521
|
+
});
|
|
2522
|
+
if (this.isSubAgent) {
|
|
2523
|
+
this.failSubAgent('context_exhausted');
|
|
2524
|
+
}
|
|
2525
|
+
else {
|
|
2526
|
+
this.appendToHistory({
|
|
2527
|
+
role: 'assistant',
|
|
2528
|
+
content: 'I had to stop here — this conversation has filled the available context. ' +
|
|
2529
|
+
'Compact it to free up room, then ask me to continue.',
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
return this.turnDone('context-exhausted');
|
|
2533
|
+
}
|
|
2402
2534
|
let response;
|
|
2403
2535
|
try {
|
|
2404
2536
|
// oxlint-disable-next-line no-await-in-loop
|
|
@@ -2484,13 +2616,46 @@ export class ChatDriver extends EventTarget {
|
|
|
2484
2616
|
// error is an untyped transport failure, gets re-issued MAX_SETUP_TRANSPORT_RETRIES times
|
|
2485
2617
|
// against a wall that cannot move, and then surfaces as "something went wrong on my end" —
|
|
2486
2618
|
// wrong twice over, since nothing went wrong on our end and trying again will not help.
|
|
2619
|
+
// The REQUEST wall (GENC-1567) — the prompt outgrew the window. Placed
|
|
2620
|
+
// beside the account walls and before the transient-retry step for the
|
|
2621
|
+
// same reason: re-issuing an oversized prompt cannot make it fit, and
|
|
2622
|
+
// without this it surfaces as "something went wrong on my end" when in
|
|
2623
|
+
// fact the user has a clear, effective remedy.
|
|
2624
|
+
//
|
|
2625
|
+
// A backstop rather than the main defence — the mid-loop guard should
|
|
2626
|
+
// normally stop the turn before a request this large is built. It is
|
|
2627
|
+
// reached when the provider reports no context window (nothing for the
|
|
2628
|
+
// guard to measure against), when the estimate ran under, or when a
|
|
2629
|
+
// single request was already too large.
|
|
2630
|
+
if (e instanceof ContextOverflowError) {
|
|
2631
|
+
logger.error('ChatDriver: the prompt exceeded the model context window', e);
|
|
2632
|
+
recordTurnError(this.sessionKey, 'context-exhausted', {
|
|
2633
|
+
agent: this.activeAgentName,
|
|
2634
|
+
provider: this.lastResolvedProviderName,
|
|
2635
|
+
vendor: (_e = vendorTypeOfLabel(e.vendorLabel)) !== null && _e !== void 0 ? _e : this.lastResolvedProvider,
|
|
2636
|
+
promptTokens: e.promptTokens,
|
|
2637
|
+
limitTokens: e.limitTokens,
|
|
2638
|
+
via: 'provider',
|
|
2639
|
+
isSubAgent: this.isSubAgent,
|
|
2640
|
+
});
|
|
2641
|
+
if (this.isSubAgent) {
|
|
2642
|
+
this.failSubAgent('context_exhausted');
|
|
2643
|
+
}
|
|
2644
|
+
else {
|
|
2645
|
+
this.appendToHistory({
|
|
2646
|
+
role: 'assistant',
|
|
2647
|
+
content: DEFAULT_CONTEXT_OVERFLOW_MESSAGE,
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
return this.turnDone('context-exhausted');
|
|
2651
|
+
}
|
|
2487
2652
|
if (e instanceof ProviderRefusedError) {
|
|
2488
2653
|
if (this.isSubAgent) {
|
|
2489
2654
|
logger.error('ChatDriver: provider refused the request', e);
|
|
2490
2655
|
recordTurnError(this.sessionKey, 'provider-refused', {
|
|
2491
2656
|
agent: this.activeAgentName,
|
|
2492
2657
|
provider: this.lastResolvedProviderName,
|
|
2493
|
-
vendor: (
|
|
2658
|
+
vendor: (_f = vendorTypeOfLabel(e.vendorLabel)) !== null && _f !== void 0 ? _f : this.lastResolvedProvider,
|
|
2494
2659
|
kind: e.kind,
|
|
2495
2660
|
upstreamStatus: e.upstreamStatus,
|
|
2496
2661
|
upstreamType: e.upstreamType,
|
|
@@ -2524,7 +2689,7 @@ export class ChatDriver extends EventTarget {
|
|
|
2524
2689
|
recordTurnError(this.sessionKey, 'budget-exhausted', {
|
|
2525
2690
|
agent: this.activeAgentName,
|
|
2526
2691
|
provider: this.lastResolvedProviderName,
|
|
2527
|
-
vendor: (
|
|
2692
|
+
vendor: (_h = (_g = vendorTypeOfLabel(e.vendorLabel)) !== null && _g !== void 0 ? _g : vendorTypeOfLabel(e.serverVendor)) !== null && _h !== void 0 ? _h : this.lastResolvedProvider,
|
|
2528
2693
|
budgetUsd: e.budgetUsd,
|
|
2529
2694
|
spentUsd: e.spentUsd,
|
|
2530
2695
|
isSubAgent: true,
|
|
@@ -2622,15 +2787,15 @@ export class ChatDriver extends EventTarget {
|
|
|
2622
2787
|
// fallback chain answered on a different model than the one we asked for.
|
|
2623
2788
|
if (response.model !== undefined)
|
|
2624
2789
|
turnSnapshot.model = response.model;
|
|
2625
|
-
const isThinkingStep = response.content && ((
|
|
2626
|
-
const isEmptyResponse = !((
|
|
2790
|
+
const isThinkingStep = response.content && ((_j = response.toolCalls) === null || _j === void 0 ? void 0 : _j.length);
|
|
2791
|
+
const isEmptyResponse = !((_k = response.content) === null || _k === void 0 ? void 0 : _k.trim()) && !((_l = response.toolCalls) === null || _l === void 0 ? void 0 : _l.length);
|
|
2627
2792
|
// A pre-output refusal (safety-classifier decline, e.g. Fable 5 `stop_reason: 'refusal'`)
|
|
2628
2793
|
// comes back with empty content, so it looks like a blank response — but it is deterministic:
|
|
2629
2794
|
// retrying re-sends the identical request and refuses again, burning up to
|
|
2630
2795
|
// MAX_EMPTY_RESPONSE_RETRIES turns on the most expensive models for the same outcome, ending
|
|
2631
2796
|
// in the misleading "blank response" message. Treat it as a terminal, non-retried failure with
|
|
2632
2797
|
// its own reason and message. (GENC-1461)
|
|
2633
|
-
const isRefusal = ((
|
|
2798
|
+
const isRefusal = ((_m = response.responseMeta) === null || _m === void 0 ? void 0 : _m.finishReason) === 'refusal';
|
|
2634
2799
|
if (isEmptyResponse) {
|
|
2635
2800
|
emptyResponseAttempts += 1;
|
|
2636
2801
|
if (!isRefusal && emptyResponseAttempts < MAX_EMPTY_RESPONSE_RETRIES) {
|
|
@@ -2704,7 +2869,7 @@ export class ChatDriver extends EventTarget {
|
|
|
2704
2869
|
emptyResponseAttempts = 0;
|
|
2705
2870
|
malformedAttempts = 0;
|
|
2706
2871
|
setupTransportAttempts = 0;
|
|
2707
|
-
if (!((
|
|
2872
|
+
if (!((_o = response.toolCalls) === null || _o === void 0 ? void 0 : _o.length)) {
|
|
2708
2873
|
break;
|
|
2709
2874
|
}
|
|
2710
2875
|
const [toolCalls, systemCalls] = response.toolCalls.reduce((acc, tc) => {
|
|
@@ -2934,7 +3099,7 @@ export class ChatDriver extends EventTarget {
|
|
|
2934
3099
|
// The response was appended before execution — find it and annotate.
|
|
2935
3100
|
let tcMsgIdx = -1;
|
|
2936
3101
|
for (let i = this.history.length - 1; i >= 0; i -= 1) {
|
|
2937
|
-
if (this.history[i].role === 'assistant' && ((
|
|
3102
|
+
if (this.history[i].role === 'assistant' && ((_p = this.history[i].toolCalls) === null || _p === void 0 ? void 0 : _p.length)) {
|
|
2938
3103
|
tcMsgIdx = i;
|
|
2939
3104
|
break;
|
|
2940
3105
|
}
|
|
@@ -2972,7 +3137,7 @@ export class ChatDriver extends EventTarget {
|
|
|
2972
3137
|
const unknownTools = [
|
|
2973
3138
|
...new Set([
|
|
2974
3139
|
...this.recentUnknownToolNames,
|
|
2975
|
-
...((
|
|
3140
|
+
...((_q = response.toolCalls) !== null && _q !== void 0 ? _q : [])
|
|
2976
3141
|
.filter((tc) => unknownToolIds.has(tc.id))
|
|
2977
3142
|
.map((tc) => tc.name),
|
|
2978
3143
|
]),
|
|
@@ -2984,7 +3149,7 @@ export class ChatDriver extends EventTarget {
|
|
|
2984
3149
|
const staleTools = [
|
|
2985
3150
|
...new Set([
|
|
2986
3151
|
...this.recentStaleToolNames,
|
|
2987
|
-
...((
|
|
3152
|
+
...((_r = response.toolCalls) !== null && _r !== void 0 ? _r : [])
|
|
2988
3153
|
.filter((tc) => staleToolIds.has(tc.id))
|
|
2989
3154
|
.map((tc) => tc.name),
|
|
2990
3155
|
]),
|
|
@@ -203,13 +203,37 @@ export class OrchestratingDriver extends EventTarget {
|
|
|
203
203
|
getRawHistory() {
|
|
204
204
|
return this.chatDriver.getHistory();
|
|
205
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Delegates to the inner {@link ChatDriver} — condensation policies are registered there, so it
|
|
208
|
+
* is the only object that knows which payloads the model has stopped seeing (GENC-1566).
|
|
209
|
+
*
|
|
210
|
+
* **This forward is the whole feature, for any multi-agent host.** `AiDriver` declares
|
|
211
|
+
* `getHistoryForPersistence` optional, and `SessionPersister` calls it through `?.`, falling back
|
|
212
|
+
* to {@link OrchestratingDriver.getRawHistory}. This class does NOT extend `ChatDriver` — it
|
|
213
|
+
* wraps one — so without an explicit forward the optional call resolves to `undefined` and every
|
|
214
|
+
* host on an orchestrating driver silently keeps persisting the full payloads. Measured exactly
|
|
215
|
+
* that way on a Create run: 10 `context.condensed` events fired, and the session still stored
|
|
216
|
+
* the two largest `read_context` results at 26,907 and 23,060 chars. The method existed, the
|
|
217
|
+
* tests passed, and the feature did nothing.
|
|
218
|
+
*/
|
|
219
|
+
getHistoryForPersistence() {
|
|
220
|
+
return this.chatDriver.getHistoryForPersistence();
|
|
221
|
+
}
|
|
222
|
+
/** {@inheritDoc AiDriver.setContextGuard} */
|
|
223
|
+
setContextGuard(policy) {
|
|
224
|
+
this.chatDriver.setContextGuard(policy);
|
|
225
|
+
}
|
|
206
226
|
/** {@inheritDoc AiDriver.compact} */
|
|
207
|
-
compact() {
|
|
208
|
-
return this.chatDriver.compact();
|
|
227
|
+
compact(options) {
|
|
228
|
+
return this.chatDriver.compact(options);
|
|
209
229
|
}
|
|
210
230
|
/** {@inheritDoc AiDriver.canCompact} */
|
|
211
|
-
canCompact() {
|
|
212
|
-
return this.chatDriver.canCompact();
|
|
231
|
+
canCompact(options) {
|
|
232
|
+
return this.chatDriver.canCompact(options);
|
|
233
|
+
}
|
|
234
|
+
/** {@inheritDoc AiDriver.getCompactionPlan} */
|
|
235
|
+
getCompactionPlan(options) {
|
|
236
|
+
return this.chatDriver.getCompactionPlan(options);
|
|
213
237
|
}
|
|
214
238
|
/** Delegates to the inner {@link ChatDriver} — turns are captured there. */
|
|
215
239
|
getTurnSnapshots() {
|