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

Allow agents to replace the default compaction summary instructions with `defineAgent({ compaction: { prompt } })`.
14 changes: 14 additions & 0 deletions docs/agent-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,20 @@ export default defineAgent({
});
```

Set `prompt` when a checkpoint must preserve domain-specific context. It replaces eve's default summary instructions. eve still supplies the previous checkpoint and conversation transcript:

```ts title="agent/agent.ts" {3,8}
import { defineAgent } from "eve";

const compactionPrompt = `Create a handoff summary for the next model.
Preserve unresolved customer questions and exact quoted requirements.`;

export default defineAgent({
model: "anthropic/claude-opus-4.8",
compaction: { prompt: compactionPrompt },
});
```

See [Default harness](./concepts/default-harness#compaction) for how the loop applies it.

## Runtime limits
Expand Down
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,9 @@ 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 checkpoint prompt envelope. It then summarizes the older turns and continues the session.

The default prompt preserves completed progress, decisions, remaining work, constraints, data, and references. You can replace those instructions with `compaction.prompt`. eve supplies the previous checkpoint and conversation transcript separately. The summary uses the active turn model unless you override it. Configure these settings under [`compaction`](../agent-config#compaction) in `agent.ts`:

```ts title="agent/agent.ts"
export default defineAgent({
Expand Down
3 changes: 2 additions & 1 deletion packages/eve/src/compiler/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ const compiledAgentWorkflowDefinitionSchema = z
const compiledAgentCompactionDefinitionSchema: z.ZodType<CompiledAgentCompactionDefinition> = z
.object({
model: compiledRuntimeModelReferenceSchema.optional(),
prompt: z.string().optional(),
thresholdPercent: z.number().finite().min(0).max(1).optional(),
})
.strict();
Expand Down Expand Up @@ -810,11 +811,11 @@ export function createCompiledAgentNodeManifest(input: {
: [...input.config.build.externalDependencies],
},
compaction: {
...input.config.compaction,
model:
input.config.compaction?.model === undefined
? undefined
: cloneCompiledRuntimeModelReference(input.config.compaction.model),
thresholdPercent: input.config.compaction?.thresholdPercent,
},
description: input.config.description,
dynamicModel:
Expand Down
27 changes: 12 additions & 15 deletions packages/eve/src/compiler/normalize-agent-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,18 @@ export async function compileAgentConfig(
sourcePath: configModulePath,
value: authoredModel,
});
const compaction: {
model?: CompiledRuntimeModelReference;
thresholdPercent?: number;
} = {};
const {
model: authoredCompactionModel,
modelContextWindowTokens: compactionModelContextWindowTokens,
...authoredCompaction
} = definition.compaction ?? {};
const compaction: Mutable<NonNullable<CompiledAgentDefinition["compaction"]>> = {
...authoredCompaction,
};

const compiledConfig: {
build?: CompiledAgentDefinition["build"];
compaction: {
model?: CompiledRuntimeModelReference;
thresholdPercent?: number;
};
compaction: NonNullable<CompiledAgentDefinition["compaction"]>;
description?: string;
dynamicModel?: CompiledAgentDefinition["dynamicModel"];
experimental?: CompiledAgentDefinition["experimental"];
Expand Down Expand Up @@ -148,22 +149,18 @@ export async function compileAgentConfig(
};
}

if (definition.compaction?.model !== undefined) {
if (authoredCompactionModel !== undefined) {
compaction.model = await normalizeAuthoredModelReference({
modelCatalog: context.modelCatalog,
purpose: "the compaction summary model",
contextWindowTokens: definition.compaction.modelContextWindowTokens,
contextWindowTokens: compactionModelContextWindowTokens,
providerOptions: definition.modelOptions?.providerOptions,
source: configModule,
sourcePath: configModulePath,
value: definition.compaction.model,
value: authoredCompactionModel,
});
}

if (definition.compaction?.thresholdPercent !== undefined) {
compaction.thresholdPercent = definition.compaction.thresholdPercent;
}

return compiledConfig;
}

Expand Down
3 changes: 0 additions & 3 deletions packages/eve/src/execution/create-session-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ export async function createSessionStep(input: {
// delegating parent: a child may narrow what its parent granted, never widen
// it. Root runs have no inherited limits, so their configured values apply.
const session = createSession({
compactionOverrides: {
thresholdPercent: effectiveAgent.thresholdPercent,
},
continuationToken: input.continuationToken,
limits: {
// Inherited token limits are the parent's remaining quota share at
Expand Down
3 changes: 0 additions & 3 deletions packages/eve/src/execution/dispatch-runtime-actions-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,6 @@ export async function dispatchRuntimeActionsStep(input: {
const bundle = ctx.require(BundleKey);
const effectiveAgent = resolveEffectiveAgentRuntime(bundle, ctx);
const session = hydrateDurableSession({
compactionOverrides: {
thresholdPercent: effectiveAgent.thresholdPercent,
},
durable: durableSession,
turnAgent: effectiveAgent.turnAgent,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,6 @@ export async function dispatchWorkflowRuntimeActionsStep(input: {
}

const session = hydrateDurableSession({
compactionOverrides: {
thresholdPercent: effectiveAgent.thresholdPercent,
},
durable: durableSession,
turnAgent: effectiveAgent.turnAgent,
});
Expand Down
36 changes: 24 additions & 12 deletions packages/eve/src/execution/effective-agent-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,29 @@ import { describe, expect, it } from "vitest";
import { ContextContainer } from "#context/container.js";
import { DynamicSubagentAgentConfigKey } from "#context/keys.js";
import { resolveEffectiveAgentRuntime } from "#execution/effective-agent-config.js";
import { normalizeDynamicSubagentAgentConfig } from "#runtime/subagents/dynamic-agent-config.js";

describe("resolveEffectiveAgentRuntime", () => {
it("applies the selected subagent model and runtime settings", () => {
const ctx = new ContextContainer();
ctx.set(DynamicSubagentAgentConfigKey, {
compaction: {
model: { id: "anthropic/claude-sonnet-4.5" },
thresholdPercent: 0.75,
},
description: "Perform deep research.",
limits: { sessionTimeoutMs: 120_000 },
model: { id: "anthropic/claude-opus-4.6" },
reasoning: "high",
});
ctx.set(
DynamicSubagentAgentConfigKey,
normalizeDynamicSubagentAgentConfig({
name: "researcher",
value: {
compaction: {
model: "anthropic/claude-sonnet-4.5",
modelContextWindowTokens: 100_000,
prompt: "Preserve citations.",
thresholdPercent: 0.75,
},
description: "Perform deep research.",
limits: { sessionTimeoutMs: 120_000 },
model: "anthropic/claude-opus-4.6",
reasoning: "high",
},
}),
);
const tools = [{ name: "search" }];

const effective = resolveEffectiveAgentRuntime(
Expand All @@ -40,9 +49,12 @@ describe("resolveEffectiveAgentRuntime", () => {

expect(effective).toMatchObject({
limits: { sessionTimeoutMs: 120_000 },
thresholdPercent: 0.75,
turnAgent: {
compactionModel: { id: "anthropic/claude-sonnet-4.5" },
compaction: {
model: { contextWindowTokens: 100_000, id: "anthropic/claude-sonnet-4.5" },
prompt: "Preserve citations.",
thresholdPercent: 0.75,
},
model: { id: "anthropic/claude-opus-4.6" },
reasoning: "high",
},
Expand Down
5 changes: 1 addition & 4 deletions packages/eve/src/execution/effective-agent-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { DynamicSubagentAgentConfig } from "#runtime/subagents/dynamic-agen

export interface EffectiveAgentRuntime {
readonly limits?: AgentLimitsDefinition;
readonly thresholdPercent?: number;
readonly turnAgent: RuntimeTurnAgent;
}

Expand All @@ -25,17 +24,15 @@ export function resolveEffectiveAgentRuntimeFromConfig(
if (config === undefined) {
return {
limits: bundle.resolvedAgent.config.limits,
thresholdPercent: bundle.resolvedAgent.config.compaction?.thresholdPercent,
turnAgent: bundle.turnAgent,
};
}

return {
limits: config.limits,
thresholdPercent: config.compaction?.thresholdPercent,
turnAgent: {
...bundle.turnAgent,
compactionModel: config.compaction?.model,
compaction: config.compaction,
dynamicModel: undefined,
model: config.model,
outputSchema: config.outputSchema,
Expand Down
72 changes: 60 additions & 12 deletions packages/eve/src/execution/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ function createTestTurnAgent(overrides?: Partial<RuntimeTurnAgent>): RuntimeTurn
return {
id: "test-agent",
instructions: ["You are a helpful assistant.", "Be concise."],
compactionModel: {
id: "summary-model",
},
compaction: { model: { id: "summary-model" } },
model: { id: "test-model" },
tools: [
{
Expand Down Expand Up @@ -49,6 +47,18 @@ describe("createCompactionConfig", () => {
});
});

it("preserves an authored compaction prompt", () => {
expect(
createCompactionConfig({
prompt: "Preserve every unresolved customer question.",
}),
).toEqual({
prompt: "Preserve every unresolved customer question.",
recentWindowSize: 10,
threshold: 100_000,
});
});

it("uses the authored threshold percent when provided", () => {
expect(
createCompactionConfig({
Expand Down Expand Up @@ -161,13 +171,13 @@ describe("createSession", () => {
});
});

it("honors compactionOverrides.thresholdPercent", () => {
it("honors the turn agent compaction threshold", () => {
const session = createSession({
compactionOverrides: { thresholdPercent: 0.5 },
continuationToken: "root-token",
sessionId: "sess-root",
turnAgent: createTestTurnAgent({
model: { id: "test-model", contextWindowTokens: 200_000 },
compaction: { thresholdPercent: 0.5 },
model: { contextWindowTokens: 200_000, id: "test-model" },
}),
});

Expand All @@ -177,6 +187,48 @@ describe("createSession", () => {
});
});

it("rebuilds the compaction prompt while preserving durable accounting", () => {
const created = createSession({
continuationToken: "root-token",
sessionId: "sess-root",
turnAgent: createTestTurnAgent({ compaction: { prompt: "Initial prompt" } }),
});
const session = {
...created,
compaction: {
...created.compaction,
lastKnownInputTokens: 500,
lastKnownPromptMessageCount: 2,
},
};

const durable = projectToDurableSession(session);
const hydrated = hydrateDurableSession({
durable,
turnAgent: createTestTurnAgent({ compaction: { prompt: "Hydrated prompt" } }),
});
const refreshed = refreshSessionFromTurnAgent({
session: hydrated,
turnAgent: createTestTurnAgent({ compaction: { prompt: "Updated prompt" } }),
});

expect(created.compaction.prompt).toBe("Initial prompt");
expect(durable.compaction).toEqual({
lastKnownInputTokens: 500,
lastKnownPromptMessageCount: 2,
});
expect(hydrated.compaction).toMatchObject({
lastKnownInputTokens: 500,
lastKnownPromptMessageCount: 2,
prompt: "Hydrated prompt",
});
expect(refreshed.compaction).toMatchObject({
lastKnownInputTokens: 500,
lastKnownPromptMessageCount: 2,
prompt: "Updated prompt",
});
});

it("copies the compaction model into the refreshed session", () => {
const session = createSession({
continuationToken: "root-token",
Expand All @@ -187,9 +239,7 @@ describe("createSession", () => {
const refreshed = refreshSessionFromTurnAgent({
session,
turnAgent: createTestTurnAgent({
compactionModel: {
id: "updated-summary-model",
},
compaction: { model: { id: "updated-summary-model" } },
}),
});

Expand Down Expand Up @@ -445,9 +495,6 @@ describe("refreshSessionFromTurnAgent", () => {
}),
});
const refreshed = refreshSessionFromTurnAgent({
compactionOverrides: {
thresholdPercent: 0.5,
},
session: {
...session,
compaction: {
Expand All @@ -457,6 +504,7 @@ describe("refreshSessionFromTurnAgent", () => {
},
},
turnAgent: createTestTurnAgent({
compaction: { thresholdPercent: 0.5 },
model: { contextWindowTokens: 200_000, id: "updated-model" },
}),
});
Expand Down
Loading
Loading