Skip to content
Merged
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/calm-compaction-checkpoints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Compaction now reserves room for its checkpoint prompt before reaching the configured threshold. The prompt asks the compaction model to distinguish completed work from remaining work, and later compactions receive the previous checkpoint intact instead of truncating it with ordinary transcript text.
2 changes: 1 addition & 1 deletion docs/agent-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ which levels are available and how they map to provider-native settings. Use

## Compaction

Compaction summarizes older turns as you approach the context window. It's on by default, so you only tune when it kicks in. Lower `thresholdPercent` to compact sooner:
Compaction summarizes older turns as you approach the context window. It's on by default, so you only tune when it kicks in. eve adds the estimated fixed checkpoint-prompt envelope to the trigger count, so compaction starts sooner than the conversation-only estimate. Lower `thresholdPercent` to compact sooner:

```ts title="agent/agent.ts"
export default defineAgent({
Expand Down
2 changes: 1 addition & 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. Once the conversation crosses a fraction of the window (`thresholdPercent`, `0.9` by default), it summarizes the older turns into a compact form and keeps going. 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. Tune when and how it kicks in under [`compaction`](../agent-config#compaction) in `agent.ts`:

```ts title="agent/agent.ts"
export default defineAgent({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "@eve/e2e-compaction-regression-shared/tools/advance-checkpoint";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "@eve/e2e-compaction-regression-shared/tools/advance-checkpoint";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "@eve/e2e-compaction-regression-shared/tools/advance-checkpoint";
1 change: 1 addition & 0 deletions e2e/fixtures/compaction-regression-shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"exports": {
"./agent": "./src/agent.ts",
"./evals": "./src/evals.ts",
"./tools/advance-checkpoint": "./src/advance-checkpoint.ts",
"./tools/inspect-repository": "./src/inspect-repository.ts",
"./tools/perform-source-analysis": "./src/perform-source-analysis.ts"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineState } from "eve/context";
import { defineTool } from "eve/tools";
import { z } from "zod";

import { SECOND_CHECKPOINT_MARKER } from "./constants";

const invocationCount = defineState("compaction-regression.advance-checkpoint", () => 0);
const modelFamilySchema = z.enum(["gpt-5.6", "opus-4.8", "sonnet-5"]);

export default defineTool({
description:
"Test-only second-compaction trigger tool. Records a completed fixture work unit and adds enough evidence for the harness to cross the compaction threshold again.",
inputSchema: z.object({
modelFamily: modelFamilySchema,
regressionCase: z.enum(["redundant-tool-calls", "stale-todo-work"]),
}),
async execute(input) {
const attempt = invocationCount.get() + 1;
invocationCount.update(() => attempt);

return {
checkpointMarker: SECOND_CHECKPOINT_MARKER,
completed: true,
modelFamily: input.modelFamily,
regressionCase: input.regressionCase,
attempt,
evidencePadding: "second checkpoint evidence ".repeat(100),
};
},
});
37 changes: 31 additions & 6 deletions e2e/fixtures/compaction-regression-shared/src/agent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { defineAgent, type AgentDefinition } from "eve";
import { mockModel, type MockModelRequest } from "eve/evals";

import { SECOND_CHECKPOINT_MARKER } from "./constants";

const TEST_CONTEXT_WINDOW_TOKENS = 32_000;
const MAX_TOOL_CALLS = 10;

Expand All @@ -16,6 +18,7 @@ export function createCompactionRegressionAgent(input: {
readonly modelFamily: ModelFamily;
}): AgentDefinition {
let activeRegression: ActiveRegression | undefined;
const checkpointAdvanceCallCounts = new Map<RegressionCase, number>();
const toolCallCounts = new Map<RegressionCase, number>();
const taskModel = mockModel({
modelId: `compaction-regression-task-model-${input.modelFamily}`,
Expand All @@ -26,6 +29,7 @@ export function createCompactionRegressionAgent(input: {
activeRegression?.regressionCase !== initialRegression.regressionCase
) {
activeRegression = initialRegression;
checkpointAdvanceCallCounts.set(initialRegression.regressionCase, 0);
toolCallCounts.set(initialRegression.regressionCase, 0);
}

Expand All @@ -36,8 +40,32 @@ export function createCompactionRegressionAgent(input: {
const regression = activeRegression;
const marker = completionMarker(regression.regressionCase);

if (checkpointProvesCompletion(request.messages, marker)) {
return `Done: ${marker}`;
// These are fixture markers, not compaction protocol fields. `marker` records the
// regression work tool; `SECOND_CHECKPOINT_MARKER` records the test-only tool
// whose output makes the harness cross the compaction threshold a second time.
if (checkpointContains(request.messages, marker)) {
if (checkpointContains(request.messages, SECOND_CHECKPOINT_MARKER)) {
return `Done: ${marker}; ${SECOND_CHECKPOINT_MARKER}`;
}

const advanceCalls = checkpointAdvanceCallCounts.get(regression.regressionCase) ?? 0;
if (advanceCalls >= MAX_TOOL_CALLS) {
return `Hard stop after ${MAX_TOOL_CALLS} checkpoint advances: ${marker}`;
}

checkpointAdvanceCallCounts.set(regression.regressionCase, advanceCalls + 1);
return {
toolCalls: [
{
id: `advance-checkpoint-${advanceCalls + 1}`,
input: {
modelFamily: input.modelFamily,
regressionCase: regression.regressionCase,
},
name: "advance-checkpoint",
},
],
};
}

const completedCalls = toolCallCounts.get(regression.regressionCase) ?? 0;
Expand Down Expand Up @@ -110,10 +138,7 @@ function completionMarker(regressionCase: RegressionCase): string {
: "SOURCE_ANALYSIS_COMPLETE";
}

function checkpointProvesCompletion(
messages: MockModelRequest["messages"],
marker: string,
): boolean {
function checkpointContains(messages: MockModelRequest["messages"], marker: string): boolean {
return messages.some((message, index) => {
if (message.role !== "user" || message.text !== "Summary of our conversation so far:") {
return false;
Expand Down
2 changes: 2 additions & 0 deletions e2e/fixtures/compaction-regression-shared/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** Fixture evidence emitted by the test-only second-compaction trigger tool. */
export const SECOND_CHECKPOINT_MARKER = "SECOND_CHECKPOINT_READY";
15 changes: 13 additions & 2 deletions e2e/fixtures/compaction-regression-shared/src/evals.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { EveEvalContext } from "eve/evals";

import type { ModelFamily } from "./agent";
import { SECOND_CHECKPOINT_MARKER } from "./constants";

export async function testRedundantToolCalls(
t: EveEvalContext,
Expand All @@ -22,8 +23,13 @@ export async function testRedundantToolCalls(
input: { scope: "repository" },
output: { completed: true, completionMarker: "REPOSITORY_INSPECTION_COMPLETE" },
});
t.event("compaction.completed", { count: (count) => count >= 1 });
t.calledTool("advance-checkpoint", {
count: 1,
output: { checkpointMarker: SECOND_CHECKPOINT_MARKER, completed: true },
});
t.event("compaction.completed", { count: 2 });
t.messageIncludes("REPOSITORY_INSPECTION_COMPLETE");
t.messageIncludes(SECOND_CHECKPOINT_MARKER);
}

export async function testStaleTodoWork(
Expand All @@ -46,6 +52,11 @@ export async function testStaleTodoWork(
count: 1,
output: { completed: true, workUnit: "source-analysis" },
});
t.event("compaction.completed", { count: (count) => count >= 1 });
t.calledTool("advance-checkpoint", {
count: 1,
output: { checkpointMarker: SECOND_CHECKPOINT_MARKER, completed: true },
});
t.event("compaction.completed", { count: 2 });
t.messageIncludes("SOURCE_ANALYSIS_COMPLETE");
t.messageIncludes(SECOND_CHECKPOINT_MARKER);
}
61 changes: 61 additions & 0 deletions packages/eve/src/harness/compaction-prompt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { ModelMessage } from "ai";
import { describe, expect, it } from "vitest";

import { COMPACTION_PROMPT_ENVELOPE, createCompactionPrompt } from "#harness/compaction-prompt.js";

describe("createCompactionPrompt", () => {
it("preserves the previous checkpoint without applying transcript truncation", () => {
const markerAfterTextLimit = "CRITICAL_STATE_AFTER_280_CHARACTERS";
const previousCheckpoint = `${"completed work ".repeat(24)}${markerAfterTextLimit}`;

const result = createCompactionPrompt({
messages: [{ content: "New evidence", role: "user" }],
previousCheckpoint,
});

expect(result.system).toBe(COMPACTION_PROMPT_ENVELOPE.system);
expect(result.prompt).toContain(`<previous-checkpoint>\n${previousCheckpoint}`);
expect(result.prompt).toContain(markerAfterTextLimit);
});

it("summarizes structured tool messages without dumping raw JSON", () => {
const messages: ModelMessage[] = [
{
content: [
{
input: { query: "debug" },
toolCallId: "call-1",
toolName: "search",
type: "tool-call",
},
],
role: "assistant",
},
{
content: [
{
output: {
type: "json",
value: ["alpha", "beta", "gamma", "delta"],
},
toolCallId: "call-1",
toolName: "search",
type: "tool-result",
},
],
role: "tool",
},
];

const result = createCompactionPrompt({ messages, previousCheckpoint: undefined });

expect(result.prompt).toContain("Conversation transcript:");
expect(result.prompt).toContain("### assistant");
expect(result.prompt).toContain("Called search with object(query=debug)");
expect(result.prompt).toContain(
"Tool search returned object(type=json, value=array(4: alpha, beta, gamma, …))",
);
expect(result.prompt).not.toContain('{"query"');
expect(result.prompt).not.toContain('{"items"');
});
});
Loading
Loading