Skip to content

Commit 181caed

Browse files
authored
fix(eve): stop compacting on every step when the current message fills the context (#5290)
Signed-off-by: Rui Conti <ruiconti@gmail.com>
1 parent de1b9aa commit 181caed

18 files changed

Lines changed: 1164 additions & 112 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eve": patch
3+
---
4+
5+
Compaction no longer re-runs on every step when the current user message is most of the context window. After a compaction, eve keeps at most 20,000 tokens of the latest conversation word for word next to the summary, with tool results shortened to 2,000 characters; a current message larger than that keeps its start and end, and the cut-out middle is summarized. A compaction that cannot bring the history under the threshold now fails the session, without retrying the step, with an error that says to start a new session or send a smaller message.

‎docs/concepts/default-harness.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ The default harness is eve's built-in agent loop. It manages model calls, compac
77

88
## Compaction
99

10-
The harness keeps a long session from overflowing the model's context window. Before comparing the conversation with `thresholdPercent` (`0.9` by default), it adds the estimated fixed envelope of the checkpoint prompt used for compaction. It then summarizes the older turns and keeps going. The prompt asks the compaction model to distinguish completed progress and decisions from remaining work and to retain the constraints, preferences, data, and references needed to continue. When eve compacts again, it passes the previous checkpoint separately and without the transcript's per-message truncation, then replaces it with the updated checkpoint. The summary uses the active turn model unless you override it. Tune when and how it kicks in under [`compaction`](../agent-config#compaction) in `agent.ts`:
10+
The harness keeps a long session from overflowing the model's context window. Before comparing the conversation with `thresholdPercent` (`0.9` by default), it adds the estimated fixed envelope of the checkpoint prompt used for compaction. It then summarizes the older turns and keeps going. The prompt asks the compaction model to distinguish completed progress and decisions from remaining work and to retain the constraints, preferences, data, and references needed to continue. When eve compacts again, it passes the previous checkpoint separately and without the transcript's per-message truncation, then replaces it with the updated checkpoint. The summary uses the active turn model unless you override it. After compacting, the harness keeps the most recent messages word for word next to the summary, up to 20,000 tokens (or half of the threshold, if that is smaller), with tool results shortened to 2,000 characters. The current user message is always part of that, so when it alone is larger than the budget the harness keeps only its start and its end, with a note where the middle was cut, and summarizes the cut-out middle with the older turns. If a compaction cannot bring the history under the threshold, the session fails with an error instead of compacting again, and the step is not retried: start a new session or send a smaller message. Tune when and how it kicks in under [`compaction`](../agent-config#compaction) in `agent.ts`:
1111

1212
```ts title="agent/agent.ts"
1313
export default defineAgent({

‎e2e/fixtures/agent-compaction-regressions/agent/agent.ts‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,23 @@ const TEST_CONTEXT_WINDOW_TOKENS = 32_000;
2020
// block, and 10 advertised tools; with nothing deferred and no skills or
2121
// connections, the agent has no catalog tools) at 2,311.5
2222
// tokens. This allowance sits 230.25 tokens below that estimate, the offset the
23-
// cases have always run with, so the threshold leaves them a ~670-token history
23+
// cases have always run with, so the threshold leaves them a ~4,770-token history
2424
// budget. When eve's fixed instructions or tools change, re-measure: log what
2525
// `estimateRequestEnvelope` (packages/eve/src/harness/request-envelope.ts)
2626
// returns while `EVE_E2E_MODEL=mock pnpm exec eve eval --strict
2727
// content-output-file-stub` runs in this fixture, subtract 230.25, and round up.
2828
const TEST_REQUEST_ENVELOPE_TOKENS = 2_082;
29-
// Fit the capped file-output exchange, while forcing the larger review and
30-
// handoff reports into the assistant checkpoint consumed by the script.
31-
const TEST_HISTORY_BUDGET_TOKENS = 900;
32-
const COMPACTION_PRESSURE_USAGE = { inputTokens: TEST_REQUEST_ENVELOPE_TOKENS + 4_096 };
29+
// History budget above the envelope. Compaction refuses a result that stays
30+
// over it, so it must hold the assistant checkpoint (the review and handoff
31+
// reports, about 3k tokens) plus the compaction prompt's own overhead; it
32+
// must stay below the capped file-output exchange's weight (a 2000x2000 image,
33+
// ~5k tokens) so that case still compacts.
34+
const TEST_HISTORY_BUDGET_TOKENS = 5_000;
35+
// Reported once a tool step has run: just over the budget, so the harness
36+
// compacts, without claiming a history far bigger than the one it has.
37+
const COMPACTION_PRESSURE_USAGE = {
38+
inputTokens: TEST_REQUEST_ENVELOPE_TOKENS + TEST_HISTORY_BUDGET_TOKENS + 1,
39+
};
3340
const MAX_TOOL_CALLS = 10;
3441

3542
type RegressionCase = "content-output-file-stub" | "redundant-tool-calls" | "task-survival";

‎e2e/fixtures/agent-compaction-regressions/evals/task-survival.eval.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default defineEval({
1212
"Alice is updating the library's reading-list application. Please review the repository and confirm these requirements for Bob's handoff.",
1313
// Sized to a narrow window: long enough that a 280-char summarizer
1414
// input cap would destroy the tail sentinel, but short enough that
15-
// the task alone cannot cross the fixture's ~640-token history budget —
15+
// the task alone cannot cross the fixture's history budget —
1616
// otherwise compaction fires before the first model call and the
1717
// mock never generates pressure.
1818
[

‎e2e/fixtures/agent-compaction-regressions/release-findings.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const repositoryFindings = [
1+
const repositoryNotes = [
22
"Application startup loads configuration before registering the catalog, cart, checkout, and order routes. An invalid currency setting prevents the application from accepting requests and identifies the setting that needs correction.",
33
"The catalog exposes product identifiers, display names, prices in cents, and availability. Product identifiers remain stable when a display name changes, so a saved cart continues to identify the intended product.",
44
"Catalog searches return an empty collection when nothing matches. The storefront distinguishes that state from a failed search and offers a clear way to remove filters without losing the customer's current cart.",
@@ -15,7 +15,7 @@ export const repositoryFindings = [
1515
"The review covers startup, catalog, cart, checkout, saved orders, and notifications. Release validation should now concentrate on the purchase and cancellation journeys, including retries, without repeating this repository inspection.",
1616
];
1717

18-
export const handoffNotes = [
18+
const handoffEntries = [
1919
"Repository review is complete for application startup, catalog browsing, cart updates, checkout, order history, and notifications. Carry these findings into release validation; the next reviewer should not repeat the completed inspection.",
2020
"Start release validation with a catalog containing available and unavailable products. Confirm that filters show an empty state when appropriate and that clearing a filter restores the product list while preserving the current cart contents.",
2121
"Exercise cart updates with a new product, an existing product, and removal of the final item. Include invalid quantities and confirm that rejected updates leave the last valid cart visible, with an explanation tied to the offending input.",
@@ -31,3 +31,17 @@ export const handoffNotes = [
3131
"Keep unresolved release checks distinct from completed review work in the handoff. If validation exposes a new defect, record the new observation and affected journey rather than reopening the repository inspection without additional evidence.",
3232
"After the release checks are recorded, publish the concise handoff with the completed review result and checkpoint status. Link each recorded release check to the journey it covers so later reviewers can trace every result back to this plan.",
3333
];
34+
35+
// Each record is about 4,400 tokens, nearly the whole history budget, so the
36+
// task and a record never fit together: a compaction has to summarize the
37+
// record into the checkpoint rather than keep it in the tail.
38+
const RECORD_PASSES = 5;
39+
40+
function repeatPasses(notes: readonly string[]): string[] {
41+
return Array.from({ length: RECORD_PASSES }, (_, pass) =>
42+
notes.map((note) => (pass === 0 ? note : `${note} (Confirmed again in pass ${pass + 1}.)`)),
43+
).flat();
44+
}
45+
46+
export const repositoryFindings = repeatPasses(repositoryNotes);
47+
export const handoffNotes = repeatPasses(handoffEntries);

‎e2e/fixtures/agent-compaction-regressions/report-evidence.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ export function assistantHasReport(
77
const escapedReference = reference.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
88
const recordReference = new RegExp(`(?<![\\w/-])${escapedReference}(?![\\w/-])`);
99

10-
// Only successful tool results introduce these references. Requiring their
11-
// retention in assistant text proves they survived a checkpoint or trail.
10+
// Only successful tool results introduce these references. Their presence in
11+
// what the model is shown (a surviving tool result, or assistant text after a
12+
// checkpoint or trail) proves they survived compaction.
1213
return messages.some(
13-
(message) => message.role === "assistant" && recordReference.test(message.text),
14+
(message) =>
15+
(message.role === "assistant" || message.role === "tool") &&
16+
recordReference.test(message.text),
1417
);
1518
}

‎packages/eve/src/execution/catalog/step-catalog.integration.test.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -479,11 +479,17 @@ describe("step catalog in the harness (real AI SDK)", () => {
479479
const historyBeforeCompaction = driver.session.history;
480480

481481
// Compaction replaces history; the next request starts a fresh baseline.
482-
// Any real history exceeds a threshold of 1, so the next step compacts.
482+
// Compaction refuses a result that stays over the threshold, so the
483+
// threshold must leave room for the summary. Old filler, appended after
484+
// the last provider-measured prompt, pushes the history over it.
483485
const compactionStep = mark();
484486
driver.session = {
485487
...driver.session,
486-
compaction: { ...driver.session.compaction, threshold: 1 },
488+
compaction: { ...driver.session.compaction, threshold: 2_000 },
489+
history: [
490+
...driver.session.history,
491+
{ content: "Earlier context. ".repeat(1_000), kind: "user", role: "user" },
492+
],
487493
};
488494
reply(text("Ready for the next request."));
489495
await drive({ message: "Alice asks what is next." });

0 commit comments

Comments
 (0)