Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/compaction-oversized-message.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

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.
5 changes: 5 additions & 0 deletions .changeset/compaction-strategy-interface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": minor
---

An agent can now choose how its history is compacted. Export a strategy from `agent/compaction.ts` with `defineCompaction` from `eve/compaction`: `shouldCompact` decides when, `compact` returns the replacement history, and eve keeps the events, memory recall, failure handling and billing. The default is unchanged and is available as `fixedTail()`.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Node and JS/TS builds
node_modules/
node_modules
dist/
/build/
coverage/
Expand All @@ -25,7 +25,7 @@ apps/benchmarks/evals/*/package.json
apps/benchmarks/evals/*/.eve-authoring-bootstrap.json
.extension-contracts-cache/
packages/eve/.workflow-vitest/
packages/eve/.generated/
packages/eve/.generated
packages/eve/src/extensions/code/extension/
packages/eve/src/extensions/git/extension/
packages/eve/src/computer-use/extension/
Expand Down
40 changes: 40 additions & 0 deletions docs/concepts/compaction-strategies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: "Compaction strategies"
description: "Replace how an agent shortens its history by exporting a strategy from compaction.ts."
---

By default eve compacts with the fixed-tail strategy: a summary of everything but the latest conversation, which stays word for word up to 20,000 tokens. See [Default harness](./default-harness). To compact differently, export a strategy from `agent/compaction.ts`.

```ts title="agent/compaction.ts"
import { defineCompaction } from "eve/compaction";

export default defineCompaction({
shouldCompact: (args) => args.inputTokens > args.threshold,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think should be ctx and we need to make sure the current model is exposed, as well as its context window if available. We can also use this opportunity to not require a context window if we want. right now we fail build without it, maybe that is a good thing im not 100% sure.

async compact(history) {
return history.slice(-6);
},
});
```

## What eve does and what the strategy does

eve measures the next request, decides when compaction can run, and owns everything around it: the `compaction.requested` and `compaction.completed` events, [memory](../memory) capture and recall, failure, and billing of the summary call. A strategy owns two decisions.

| Member | Decides |
| ----------------------- | ------------------------------------------------------------------------- |
| `shouldCompact(args)` | Whether the next request needs compacting, from the numbers eve measured. |
| `compact(history, ctx)` | The messages that replace `history`. |

`args` holds `inputTokens`, `requestEnvelopeTokens`, `threshold`, `recentWindowSize` and `event`, the eve event that prompted the check (today only `{ type: "step.started" }`; new triggers add members). It decides automatic compaction only: a user-requested compaction runs `compact` without calling it, and `ctx.forced` is `true` there. `ctx` carries the same four numbers (no `event`), plus `forced`. `history` is the conversation without memory records; eve places recalled memory ahead of the result, so a strategy never sees or reorders it. A user message you return needs a `kind`; build one with `createCompactionUserMessage(content)` from `eve/compaction`, for example to mark a checkpoint.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

threshold is very vague to me where does this come from what does it mean


`ctx` adds `summarize({ messages, system })`, which runs the compaction model and bills the session, `estimateTokens(messages)`, and, when the turn can be cancelled, an `abortSignal`. A strategy that never summarizes makes no model call.

The `modelId` on `compaction.requested` and `compaction.completed` is the id of the configured compaction model (`compaction.model`, or the agent's model when none is set), whether or not the strategy summarizes. It is not the id `summarize` resolved.

## Failure

Throw `CompactionOverLimitError` (exported from `eve/compaction`) when no history the strategy could return fits. Any thrown error fails the compaction the way a failed summary does.

## Reusing the default

`fixedTail()` from `eve/compaction` returns the default strategy, so a strategy can wrap it, for example to compact earlier than the threshold.
4 changes: 3 additions & 1 deletion docs/concepts/default-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The default harness is eve's built-in agent loop. It manages model calls, compac

## Compaction

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`:
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`:

```ts title="agent/agent.ts"
export default defineAgent({
Expand All @@ -24,6 +24,8 @@ input token count plus an estimate of new messages. A smaller character estimate
alone cannot satisfy compaction triggered by a higher provider count. If trimming
cannot free enough space, eve summarizes the older history.

To change how the history is shortened, export your own strategy from `compaction.ts`. See [Compaction strategies](./compaction-strategies).

First-class [memory](../memory) participates in a separate lifecycle. eve asks
providers to capture before compaction, excludes attributed recalled records
from the summarizer, keeps their canonical latest values, and recalls again
Expand Down
1 change: 1 addition & 0 deletions docs/concepts/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"sessions-runs-and-streaming",
"state",
"default-harness",
"compaction-strategies",
"built-in-tools",
"context-control",
"security-model"
Expand Down
1 change: 1 addition & 0 deletions docs/reference/agent-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Paths below are relative to the agent directory. Root agents can use every path;
| `hooks/` | Lifecycle and stream-event subscribers | Yes | Module-backed only; recursive directories are supported. |
| `skills/` | On-demand procedures and capability packs | Yes | Flat Markdown, module-backed skills, or packaged skills. |
| `lib/` | Shared authored helper code | Yes | Import-only; not copied into the sandbox. Helpers can import data files such as JSON from here. |
| `compaction.ts` | How the history is shortened | Yes | One strategy for the agent. See [Compaction strategies](/docs/concepts/compaction-strategies). |
| `memory.ts` or `memory/<name>.ts` | Cross-session memory | Yes | Provider-backed slots. See [Memory](/docs/memory). |
| `sandbox.ts` or `sandbox/sandbox.ts` | The agent's sandbox | Yes | The framework default applies when neither is authored. When `sandbox/` exists, only `sandbox/sandbox.ts` is read. |
| `sandbox/workspace/**` | Files seeded into the sandbox | Yes | Copied into the prepared environment; each new session sandbox starts with them in the workspace root. |
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/typescript-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default defineTool({
| `eveChannel`, `slackChannel`, and the other platforms | `eve/channels/<platform>` | `agent/channels/<platform>.ts` | [Channels](../channels/overview) |
| `defineSkill` | `eve/skills` | `agent/skills/<name>.ts` | [Skills](../skills) |
| `defineInstructions` | `eve/instructions` | `agent/instructions.ts` | [Instructions](../instructions) |
| `defineCompaction` | `eve/compaction` | `agent/compaction.ts` | [Compaction strategies](../concepts/compaction-strategies) |
| `defineMemory`, `defineMemoryProvider` | `eve/memory` | `agent/memory.ts` or `agent/memory/<slot>.ts` | [Memory](../memory) |
| `defineHook` | `eve/hooks` | `agent/hooks/<slug>.ts` | [Hooks](../guides/hooks) |
| `defineSchedule` | `eve/schedules` | `agent/schedules/<name>.ts` | [Schedules](../schedules) |
Expand Down Expand Up @@ -137,6 +138,7 @@ Tool definitions accept `availableInSubagents: false` to restrict the tool to to
| `eve/schedules` | `defineSchedule` |
| `eve/skills` | `defineSkill`, `defineDynamic` |
| `eve/instructions` | `defineInstructions`, `defineDynamic` |
| `eve/compaction` | `defineCompaction`, `fixedTail`, `CompactionOverLimitError`, strategy types |
| `eve/memory` | `defineMemory`, `defineMemoryProvider`, provider and lifecycle types |
| `eve/memory/scope` | `byPrincipal` and memory scope helpers |
| `eve/memory/file` | `fileMemory`, `inMemory`, and the conditional document backend contract |
Expand Down
17 changes: 12 additions & 5 deletions e2e/fixtures/agent-compaction-regressions/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,23 @@ const TEST_CONTEXT_WINDOW_TOKENS = 32_000;
// block, and 10 advertised tools; with nothing deferred and no skills or
// connections, the agent has no catalog tools) at 2,311.5
// tokens. This allowance sits 230.25 tokens below that estimate, the offset the
// cases have always run with, so the threshold leaves them a ~670-token history
// cases have always run with, so the threshold leaves them a ~4,770-token history
// budget. When eve's fixed instructions or tools change, re-measure: log what
// `estimateRequestEnvelope` (packages/eve/src/harness/request-envelope.ts)
// returns while `EVE_E2E_MODEL=mock pnpm exec eve eval --strict
// content-output-file-stub` runs in this fixture, subtract 230.25, and round up.
const TEST_REQUEST_ENVELOPE_TOKENS = 2_082;
// Fit the capped file-output exchange, while forcing the larger review and
// handoff reports into the assistant checkpoint consumed by the script.
const TEST_HISTORY_BUDGET_TOKENS = 900;
const COMPACTION_PRESSURE_USAGE = { inputTokens: TEST_REQUEST_ENVELOPE_TOKENS + 4_096 };
// History budget above the envelope. Compaction refuses a result that stays
// over it, so it must hold the assistant checkpoint (the review and handoff
// reports, about 3k tokens) plus the compaction prompt's own overhead; it
// must stay below the capped file-output exchange's weight (a 2000x2000 image,
// ~5k tokens) so that case still compacts.
const TEST_HISTORY_BUDGET_TOKENS = 5_000;
// Reported once a tool step has run: just over the budget, so the harness
// compacts, without claiming a history far bigger than the one it has.
const COMPACTION_PRESSURE_USAGE = {
inputTokens: TEST_REQUEST_ENVELOPE_TOKENS + TEST_HISTORY_BUDGET_TOKENS + 1,
};
const MAX_TOOL_CALLS = 10;

type RegressionCase = "content-output-file-stub" | "redundant-tool-calls" | "task-survival";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default defineEval({
"Alice is updating the library's reading-list application. Please review the repository and confirm these requirements for Bob's handoff.",
// Sized to a narrow window: long enough that a 280-char summarizer
// input cap would destroy the tail sentinel, but short enough that
// the task alone cannot cross the fixture's ~640-token history budget —
// the task alone cannot cross the fixture's history budget —
// otherwise compaction fires before the first model call and the
// mock never generates pressure.
[
Expand Down
18 changes: 16 additions & 2 deletions e2e/fixtures/agent-compaction-regressions/release-findings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const repositoryFindings = [
const repositoryNotes = [
"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.",
"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.",
"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.",
Expand All @@ -15,7 +15,7 @@ export const repositoryFindings = [
"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.",
];

export const handoffNotes = [
const handoffEntries = [
"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.",
"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.",
"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.",
Expand All @@ -31,3 +31,17 @@ export const handoffNotes = [
"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.",
"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.",
];

// Each record is about 4,400 tokens, nearly the whole history budget, so the
// task and a record never fit together: a compaction has to summarize the
// record into the checkpoint rather than keep it in the tail.
const RECORD_PASSES = 5;

function repeatPasses(notes: readonly string[]): string[] {
return Array.from({ length: RECORD_PASSES }, (_, pass) =>
notes.map((note) => (pass === 0 ? note : `${note} (Confirmed again in pass ${pass + 1}.)`)),
).flat();
}

export const repositoryFindings = repeatPasses(repositoryNotes);
export const handoffNotes = repeatPasses(handoffEntries);
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ export function assistantHasReport(
const escapedReference = reference.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const recordReference = new RegExp(`(?<![\\w/-])${escapedReference}(?![\\w/-])`);

// Only successful tool results introduce these references. Requiring their
// retention in assistant text proves they survived a checkpoint or trail.
// Only successful tool results introduce these references. Their presence in
// what the model is shown (a surviving tool result, or assistant text after a
// checkpoint or trail) proves they survived compaction.
return messages.some(
(message) => message.role === "assistant" && recordReference.test(message.text),
(message) =>
(message.role === "assistant" || message.role === "tool") &&
recordReference.test(message.text),
);
}
Loading
Loading