@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.8.8-dev.202606090318.74794fe",
3
+ "version": "0.8.8-dev.202606091311.113d87f",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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 emergencyConfig = getConfig().compaction;
1225
- const emergencyResult = await runEmergencyCompaction({
1226
- conversationId: ctx.conversationId,
1227
- messages: ctx.messages,
1228
- provider: ctx.provider,
1229
- systemPrompt: ctx.systemPrompt,
1230
- tools: undefined,
1231
- compaction: emergencyConfig,
1232
- maxInputTokens: resolveCurrentMaxInputTokens(),
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