@vellumai/assistant 0.8.8-dev.202606090318.74794fe → 0.8.8-dev.202606091311.113d87f
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/package.json
CHANGED
|
@@ -56,6 +56,11 @@ let runCalls: Array<{
|
|
|
56
56
|
let runResults: CompactionRunResult[] = [];
|
|
57
57
|
const estimateReturns: number[] = [];
|
|
58
58
|
|
|
59
|
+
// Captured arguments from each runEmergencyCompaction invocation, so the
|
|
60
|
+
// emergencyCompact tests can assert the manager fills in the fields it owns.
|
|
61
|
+
let emergencyCalls: Record<string, unknown>[] = [];
|
|
62
|
+
let emergencyResult: CompactionRunResult | undefined;
|
|
63
|
+
|
|
59
64
|
mock.module("../context/token-estimator.js", () => ({
|
|
60
65
|
estimatePromptTokens: (): number => {
|
|
61
66
|
const next = estimateReturns.shift();
|
|
@@ -92,6 +97,15 @@ mock.module("../context/compactor.js", () => ({
|
|
|
92
97
|
}
|
|
93
98
|
return result;
|
|
94
99
|
},
|
|
100
|
+
runEmergencyCompaction: async (
|
|
101
|
+
args: Record<string, unknown>,
|
|
102
|
+
): Promise<CompactionRunResult> => {
|
|
103
|
+
emergencyCalls.push(args);
|
|
104
|
+
if (!emergencyResult) {
|
|
105
|
+
throw new Error("emergencyResult not seeded");
|
|
106
|
+
}
|
|
107
|
+
return emergencyResult;
|
|
108
|
+
},
|
|
95
109
|
}));
|
|
96
110
|
|
|
97
111
|
import { ContextWindowManager } from "../plugins/defaults/compaction/window-manager.js";
|
|
@@ -142,24 +156,28 @@ function compactResult(
|
|
|
142
156
|
};
|
|
143
157
|
}
|
|
144
158
|
|
|
159
|
+
function makeConfig(maxAttempts: number = 3) {
|
|
160
|
+
return {
|
|
161
|
+
enabled: true,
|
|
162
|
+
maxInputTokens: 200_000,
|
|
163
|
+
targetBudgetRatio: 0.3,
|
|
164
|
+
compactThreshold: 0.8,
|
|
165
|
+
summaryBudgetRatio: 0.05,
|
|
166
|
+
overflowRecovery: {
|
|
167
|
+
enabled: true,
|
|
168
|
+
safetyMarginRatio: 0.05,
|
|
169
|
+
maxAttempts,
|
|
170
|
+
interactiveLatestTurnCompression: "summarize" as const,
|
|
171
|
+
nonInteractiveLatestTurnCompression: "summarize" as const,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
145
176
|
function buildManager(maxAttempts: number = 3): ContextWindowManager {
|
|
146
177
|
return new ContextWindowManager({
|
|
147
178
|
provider: makeProvider(),
|
|
148
179
|
systemPrompt: "you are a test assistant",
|
|
149
|
-
config:
|
|
150
|
-
enabled: true,
|
|
151
|
-
maxInputTokens: 200_000,
|
|
152
|
-
targetBudgetRatio: 0.3,
|
|
153
|
-
compactThreshold: 0.8,
|
|
154
|
-
summaryBudgetRatio: 0.05,
|
|
155
|
-
overflowRecovery: {
|
|
156
|
-
enabled: true,
|
|
157
|
-
safetyMarginRatio: 0.05,
|
|
158
|
-
maxAttempts,
|
|
159
|
-
interactiveLatestTurnCompression: "summarize",
|
|
160
|
-
nonInteractiveLatestTurnCompression: "summarize",
|
|
161
|
-
},
|
|
162
|
-
},
|
|
180
|
+
config: makeConfig(maxAttempts),
|
|
163
181
|
conversationId: "conv-test",
|
|
164
182
|
});
|
|
165
183
|
}
|
|
@@ -289,3 +307,91 @@ describe("ContextWindowManager.maybeCompact retry budget", () => {
|
|
|
289
307
|
expect(result.estimatedInputTokens).toBe(165_000);
|
|
290
308
|
});
|
|
291
309
|
});
|
|
310
|
+
|
|
311
|
+
describe("ContextWindowManager.emergencyCompact", () => {
|
|
312
|
+
beforeEach(() => {
|
|
313
|
+
emergencyCalls = [];
|
|
314
|
+
emergencyResult = undefined;
|
|
315
|
+
estimateReturns.length = 0;
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("supplies manager-owned fields and forwards only overflow inputs", async () => {
|
|
319
|
+
/**
|
|
320
|
+
* The manager owns provider, system prompt, token budget, conversation
|
|
321
|
+
* id, compaction config, and non-persisted prefix count; the caller
|
|
322
|
+
* provides only the overflow-specific inputs.
|
|
323
|
+
*/
|
|
324
|
+
// GIVEN a manager seeded with a non-persisted prefix and a canned result
|
|
325
|
+
emergencyResult = compactResult({
|
|
326
|
+
compacted: true,
|
|
327
|
+
compactedMessages: 40,
|
|
328
|
+
summaryText: "emergency summary",
|
|
329
|
+
});
|
|
330
|
+
const manager = buildManager();
|
|
331
|
+
manager.seedNonPersistedPrefix(3);
|
|
332
|
+
const messages = makeMessages(8);
|
|
333
|
+
const signal = new AbortController().signal;
|
|
334
|
+
|
|
335
|
+
// WHEN emergency compaction runs
|
|
336
|
+
const result = await manager.emergencyCompact(
|
|
337
|
+
messages,
|
|
338
|
+
{ previousEstimatedInputTokens: 242_201, overrideProfile: "fast" },
|
|
339
|
+
signal,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// THEN the manager fills its owned fields and forwards the caller inputs
|
|
343
|
+
expect(emergencyCalls.length).toBe(1);
|
|
344
|
+
const args = emergencyCalls[0]!;
|
|
345
|
+
expect(args.conversationId).toBe("conv-test");
|
|
346
|
+
expect(args.systemPrompt).toBe("you are a test assistant");
|
|
347
|
+
expect(args.maxInputTokens).toBe(200_000);
|
|
348
|
+
expect(args.nonPersistedPrefixCount).toBe(3);
|
|
349
|
+
expect(args.tools).toBeUndefined();
|
|
350
|
+
expect(args.force).toBe(true);
|
|
351
|
+
expect(args.signal).toBe(signal);
|
|
352
|
+
expect(args.messages).toBe(messages);
|
|
353
|
+
expect(args.previousEstimatedInputTokens).toBe(242_201);
|
|
354
|
+
expect(args.overrideProfile).toBe("fast");
|
|
355
|
+
expect((args.provider as { name: string }).name).toBe("mock-provider");
|
|
356
|
+
expect(result.summaryText).toBe("emergency summary");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("defaults overrideProfile to null when omitted", async () => {
|
|
360
|
+
/**
|
|
361
|
+
* Callers that don't resolve a per-conversation profile get an explicit
|
|
362
|
+
* null forwarded to the summary call.
|
|
363
|
+
*/
|
|
364
|
+
// GIVEN a manager and a canned result
|
|
365
|
+
emergencyResult = compactResult({ compacted: true });
|
|
366
|
+
const manager = buildManager();
|
|
367
|
+
|
|
368
|
+
// WHEN emergency compaction runs without an override profile
|
|
369
|
+
await manager.emergencyCompact(makeMessages(6), {
|
|
370
|
+
previousEstimatedInputTokens: 210_000,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// THEN overrideProfile is forwarded as null
|
|
374
|
+
expect(emergencyCalls[0]!.overrideProfile).toBeNull();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("throws when the manager has no conversation id", async () => {
|
|
378
|
+
/**
|
|
379
|
+
* Emergency compaction needs a conversation id for attachment and
|
|
380
|
+
* timestamp lookups; a manager without one cannot run it.
|
|
381
|
+
*/
|
|
382
|
+
// GIVEN a manager constructed without a conversation id
|
|
383
|
+
const manager = new ContextWindowManager({
|
|
384
|
+
provider: makeProvider(),
|
|
385
|
+
systemPrompt: "you are a test assistant",
|
|
386
|
+
config: makeConfig(),
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// WHEN/THEN emergency compaction rejects without invoking the compactor
|
|
390
|
+
await expect(
|
|
391
|
+
manager.emergencyCompact(makeMessages(4), {
|
|
392
|
+
previousEstimatedInputTokens: 210_000,
|
|
393
|
+
}),
|
|
394
|
+
).rejects.toThrow(/conversationId/);
|
|
395
|
+
expect(emergencyCalls.length).toBe(0);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
import { getConfig } from "../config/loader.js";
|
|
34
34
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
35
35
|
import type { ContextWindowConfig } from "../config/types.js";
|
|
36
|
-
import { runEmergencyCompaction } from "../context/compactor.js";
|
|
37
36
|
import {
|
|
38
37
|
derefToolResultReReads,
|
|
39
38
|
postTurnTruncateToolResults,
|
|
@@ -1221,22 +1220,15 @@ export async function runAgentLoopImpl(
|
|
|
1221
1220
|
// compressing history. Falls through to reducer tiers on failure.
|
|
1222
1221
|
{
|
|
1223
1222
|
try {
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
previousEstimatedInputTokens: estimatedTokensAtOverflow,
|
|
1234
|
-
force: true,
|
|
1235
|
-
signal: abortController.signal,
|
|
1236
|
-
overrideProfile: resolveCurrentOverrideProfile() ?? null,
|
|
1237
|
-
nonPersistedPrefixCount:
|
|
1238
|
-
ctx.contextWindowManager.nonPersistedPrefixCount,
|
|
1239
|
-
});
|
|
1223
|
+
const emergencyResult =
|
|
1224
|
+
await ctx.contextWindowManager.emergencyCompact(
|
|
1225
|
+
ctx.messages,
|
|
1226
|
+
{
|
|
1227
|
+
previousEstimatedInputTokens: estimatedTokensAtOverflow,
|
|
1228
|
+
overrideProfile: resolveCurrentOverrideProfile() ?? null,
|
|
1229
|
+
},
|
|
1230
|
+
abortController.signal,
|
|
1231
|
+
);
|
|
1240
1232
|
if (emergencyResult.compacted) {
|
|
1241
1233
|
rlog.info(
|
|
1242
1234
|
{
|
|
@@ -20,7 +20,9 @@ import type { LLMCallSite } from "../../../config/schemas/llm.js";
|
|
|
20
20
|
import type { ContextWindowConfig } from "../../../config/types.js";
|
|
21
21
|
import {
|
|
22
22
|
type CompactionRunArgs,
|
|
23
|
+
type CompactionRunResult,
|
|
23
24
|
runAssistantDrivenCompaction,
|
|
25
|
+
runEmergencyCompaction,
|
|
24
26
|
} from "../../../context/compactor.js";
|
|
25
27
|
import { estimatePromptTokens } from "../../../context/token-estimator.js";
|
|
26
28
|
import type {
|
|
@@ -118,6 +120,21 @@ export interface ContextWindowCompactOptions {
|
|
|
118
120
|
actorTrustClass?: TrustClass;
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
export interface EmergencyCompactOptions {
|
|
124
|
+
/**
|
|
125
|
+
* The provider-reported estimate at the overflow that triggered recovery.
|
|
126
|
+
* Sizing the emergency summary needs the actual rejected token count, not
|
|
127
|
+
* the manager's pre-send estimate (which under-counted, hence the
|
|
128
|
+
* rejection).
|
|
129
|
+
*/
|
|
130
|
+
previousEstimatedInputTokens: number;
|
|
131
|
+
/**
|
|
132
|
+
* Per-conversation inference-profile override forwarded to the summary
|
|
133
|
+
* call.
|
|
134
|
+
*/
|
|
135
|
+
overrideProfile?: string | null;
|
|
136
|
+
}
|
|
137
|
+
|
|
121
138
|
export interface ContextWindowManagerOptions {
|
|
122
139
|
provider: Provider;
|
|
123
140
|
systemPrompt: string | (() => string);
|
|
@@ -428,6 +445,45 @@ export class ContextWindowManager {
|
|
|
428
445
|
return { ...result, estimatedInputTokens, exhausted: true };
|
|
429
446
|
}
|
|
430
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Emergency mid-turn compaction — summarize everything before the last
|
|
450
|
+
* tool_use/tool_result pair and let the agent continue with
|
|
451
|
+
* `[summary, last_tool_call, last_tool_result]`. Used as a recovery rung
|
|
452
|
+
* after the provider rejects a turn for context overflow. The manager
|
|
453
|
+
* supplies the provider, system prompt, token budget, conversation id, and
|
|
454
|
+
* non-persisted prefix count it already owns; the caller provides only the
|
|
455
|
+
* overflow-specific inputs.
|
|
456
|
+
*/
|
|
457
|
+
async emergencyCompact(
|
|
458
|
+
messages: Message[],
|
|
459
|
+
options: EmergencyCompactOptions,
|
|
460
|
+
signal?: AbortSignal,
|
|
461
|
+
): Promise<CompactionRunResult> {
|
|
462
|
+
if (this.conversationId == null) {
|
|
463
|
+
throw new Error(
|
|
464
|
+
"ContextWindowManager has no conversationId — cannot run emergency compaction",
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
return await runEmergencyCompaction({
|
|
469
|
+
conversationId: this.conversationId,
|
|
470
|
+
messages,
|
|
471
|
+
provider: this.provider,
|
|
472
|
+
systemPrompt: this.systemPrompt,
|
|
473
|
+
tools: undefined,
|
|
474
|
+
compaction: this.resolveCompactionConfig(),
|
|
475
|
+
maxInputTokens: this.config.maxInputTokens,
|
|
476
|
+
previousEstimatedInputTokens: options.previousEstimatedInputTokens,
|
|
477
|
+
force: true,
|
|
478
|
+
signal,
|
|
479
|
+
overrideProfile: options.overrideProfile ?? null,
|
|
480
|
+
nonPersistedPrefixCount: this._nonPersistedPrefixCount,
|
|
481
|
+
});
|
|
482
|
+
} finally {
|
|
483
|
+
this.clearSystemPromptCache();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
431
487
|
/**
|
|
432
488
|
* Recompute the prompt-token estimate against the post-compaction
|
|
433
489
|
* message array. The compactor returns a conservative placeholder; the
|