Skip to content

Commit 865ce80

Browse files
committed
Address review: validate strategy output inside the failure path, default once in maybeCompact, reuse moduleSourceRefSchema, resolve relative imports in rule 54, distinct compaction recall in the fixture, document event model id
Signed-off-by: Rui Conti <ruiconti@gmail.com>
1 parent 147cbf3 commit 865ce80

11 files changed

Lines changed: 94 additions & 33 deletions

File tree

‎docs/concepts/compaction-strategies.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ eve measures the next request, decides when compaction can run, and owns everyth
2525
| `shouldCompact(facts)` | Whether the next request needs compacting, from the numbers eve measured. |
2626
| `compact(history, ctx)` | The messages that replace `history`. |
2727

28-
`facts` holds `inputTokens`, `requestEnvelopeTokens`, `threshold`, `recentWindowSize` and `reason`. A user-requested compaction (`reason: "manual"`) runs whatever `shouldCompact` returns. `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.
28+
`facts` holds `inputTokens`, `requestEnvelopeTokens`, `threshold`, `recentWindowSize` and `reason`. `shouldCompact` decides automatic compaction only: a user-requested compaction (`reason: "manual"`) runs `compact` without calling it. `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.
2929

3030
`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.
3131

32+
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.
33+
3234
## Failure
3335

3436
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.

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ const model = mockModel({
1212
visible.includes("record evidence 0") ? "OLD_HISTORY_KEPT" : undefined,
1313
visible.includes("Summary of our conversation so far:") ? "SUMMARY_WRITTEN" : undefined,
1414
visible.split("PROFILE_VALUE=RECALLED").length === 2 ? undefined : "MEMORY_NOT_RECALLED_ONCE",
15+
visible.split("PROFILE_RECALLED_AFTER_COMPACTION=YES").length === 2
16+
? undefined
17+
: "COMPACTION_RECALL_MISSING",
1518
].filter((issue) => issue !== undefined);
1619
return issues.length === 0
1720
? "CUSTOM_STRATEGY_OK"

‎e2e/fixtures/agent-compaction-strategy/agent/memory/profile.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ export default defineMemory({
99
return { messages: [{ content: "PROFILE_VALUE=RECALLED", id: "profile" }] };
1010
},
1111
async "compaction.completed"() {
12-
return { messages: [{ content: "PROFILE_VALUE=RECALLED", id: "profile" }] };
12+
return {
13+
messages: [
14+
{ content: "PROFILE_RECALLED_AFTER_COMPACTION=YES", id: "profile-compaction" },
15+
],
16+
};
1317
},
1418
},
1519
},

‎packages/eve/.generated‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../eve-fix/packages/eve/.generated

‎packages/eve/node_modules‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../eve-fix/packages/eve/node_modules

‎packages/eve/src/compiler/manifest.ts‎

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -920,15 +920,6 @@ const compiledDynamicToolDefinitionSchema: z.ZodType<CompiledDynamicToolDefiniti
920920
})
921921
.strict();
922922

923-
const compiledCompactionDefinitionSchema: z.ZodType<CompiledCompactionDefinition> = z
924-
.object({
925-
exportName: z.string().optional(),
926-
logicalPath: z.string(),
927-
sourceId: z.string(),
928-
sourceKind: z.literal("module"),
929-
})
930-
.strict();
931-
932923
const compiledMemoryDefinitionSchema: z.ZodType<CompiledMemoryDefinition> = z
933924
.object({
934925
description: z.string().optional(),
@@ -1016,7 +1007,7 @@ const compiledAgentResourceFields = {
10161007
extensionMounts: z.array(compiledExtensionMountSchema).default([]),
10171008
hooks: z.array(compiledHookDefinitionSchema),
10181009
memories: z.array(compiledMemoryDefinitionSchema).default([]),
1019-
compaction: compiledCompactionDefinitionSchema.nullable().default(null),
1010+
compaction: moduleSourceRefSchema.nullable().default(null),
10201011
sandbox: compiledSandboxDefinitionSchema,
10211012
sandboxWorkspaces: z.array(compiledSandboxWorkspaceSchema),
10221013
schedules: z.array(compiledScheduleDefinitionSchema),
@@ -1124,7 +1115,7 @@ export const compiledAgentManifestSchema = z
11241115
dynamicTools: z.array(compiledDynamicToolDefinitionSchema).default([]),
11251116
hooks: z.array(compiledHookDefinitionSchema),
11261117
memories: z.array(compiledMemoryDefinitionSchema).default([]),
1127-
compaction: compiledCompactionDefinitionSchema.nullable().default(null),
1118+
compaction: moduleSourceRefSchema.nullable().default(null),
11281119
kind: z.literal(COMPILED_AGENT_MANIFEST_KIND),
11291120
remoteAgents: z.array(compiledRemoteAgentNodeSchema),
11301121
sandbox: compiledSandboxDefinitionSchema,

‎packages/eve/src/harness/compaction/default-strategy.ts‎

Lines changed: 0 additions & 5 deletions
This file was deleted.

‎packages/eve/src/harness/compaction/step.integration.test.ts‎

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ async function compactWith(
5757
compactionStrategy?: CompactionStrategy,
5858
options: {
5959
readonly compactionModelReference?: { readonly id: string };
60+
readonly events?: { readonly type: string; readonly data?: unknown }[];
6061
readonly resolveModel?: (reference: { readonly id: string }) => Promise<MockLanguageModelV3>;
6162
} = {},
6263
) {
@@ -79,7 +80,9 @@ async function compactWith(
7980
return createToolLoopHarness({
8081
compactOnly: true,
8182
compactionStrategy,
82-
handleEvent: async () => {},
83+
handleEvent: async (event) => {
84+
options.events?.push(event as { type: string; data?: unknown });
85+
},
8386
resolveModel: options.resolveModel ?? (async () => model),
8487
tools: new Map(),
8588
})(session, {});
@@ -207,6 +210,54 @@ describe("compaction strategy", () => {
207210
expect(result.session.history).toHaveLength(2);
208211
});
209212

213+
it("names the configured compaction model on its events, whether or not the strategy summarizes", async () => {
214+
const turnModel = new MockLanguageModelV3({ doStream: async () => summaryStream() });
215+
const strategy: CompactionStrategy = {
216+
compact: async (history) => history.slice(-2),
217+
shouldCompact: () => false,
218+
};
219+
const configured: { type: string; data?: unknown }[] = [];
220+
const fallback: { type: string; data?: unknown }[] = [];
221+
222+
await compactWith(turnModel, strategy, {
223+
compactionModelReference: { id: "other-model" },
224+
events: configured,
225+
resolveModel: async () => turnModel,
226+
});
227+
await compactWith(turnModel, strategy, { events: fallback });
228+
229+
const modelIds = (events: { type: string; data?: unknown }[]) =>
230+
events
231+
.filter((event) => event.type.startsWith("compaction."))
232+
.map((event) => [event.type, (event.data as { modelId?: string } | undefined)?.modelId]);
233+
expect(modelIds(configured)).toEqual([
234+
["compaction.requested", "other-model"],
235+
["compaction.completed", "other-model"],
236+
]);
237+
expect(modelIds(fallback)).toEqual([
238+
["compaction.requested", "summary-model"],
239+
["compaction.completed", "summary-model"],
240+
]);
241+
});
242+
243+
it("treats an invalid strategy result as a compaction failure that still counts the summary call", async () => {
244+
const strategy: CompactionStrategy = {
245+
compact: async (history, context) => {
246+
await context.summarize({ messages: [...history], system: "Summarize." });
247+
return [{ content: "no kind", role: "user" }] as never;
248+
},
249+
shouldCompact: () => false,
250+
};
251+
252+
const result = await compactWith(
253+
new MockLanguageModelV3({ doStream: async () => summaryStream() }),
254+
strategy,
255+
);
256+
257+
expect(result.session.history).toHaveLength(3);
258+
expect(getTurnUsageState(result.session.state)?.session).toMatchObject({ inputTokens: 120 });
259+
});
260+
210261
it("makes no model call when the strategy keeps the history without a summary", async () => {
211262
const doStream = vi.fn<MockLanguageModelV3["doStream"]>(async () => summaryStream());
212263

‎packages/eve/src/harness/compaction/step.ts‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import type {
3232
CompactionStrategy,
3333
} from "#harness/compaction/strategy.js";
3434
import { estimateTokens } from "#harness/token-estimate.js";
35-
import { defaultCompactionStrategy } from "#harness/compaction/default-strategy.js";
35+
import { fixedTail } from "#harness/compaction/strategies/fixed-tail/index.js";
3636
import { contextStorage } from "#context/container.js";
3737
import {
3838
createCompactionCompletedEvent,
@@ -97,7 +97,7 @@ export async function compactHistory(step: Step): Promise<StepResult> {
9797
resolveModel: config.resolveModel,
9898
runtimeIdentity: config.runtimeIdentity,
9999
session: step.session,
100-
strategy: config.compactionStrategy ?? defaultCompactionStrategy,
100+
strategy: config.compactionStrategy,
101101
telemetry: step.instrumentation?.telemetry(),
102102
traceContextHeaders: step.instrumentation?.gatewayTraceContextHeaders,
103103
});
@@ -155,7 +155,7 @@ export async function maybeCompact(input: {
155155
readonly resolveModel: ToolLoopHarnessConfig["resolveModel"];
156156
readonly runtimeIdentity?: ToolLoopHarnessConfig["runtimeIdentity"];
157157
readonly session: HarnessSession;
158-
readonly strategy: CompactionStrategy;
158+
readonly strategy?: CompactionStrategy;
159159
readonly telemetry?: TelemetryOptions;
160160
readonly traceContextHeaders?: () => Record<string, string> | undefined;
161161
}): Promise<{
@@ -166,6 +166,7 @@ export async function maybeCompact(input: {
166166
readonly session: HarnessSession;
167167
}> {
168168
const { emissionState, publish } = input;
169+
const strategy = input.strategy ?? fixedTail();
169170
let messages = input.messages;
170171
let session = input.session;
171172
const promptMessages = input.promptMessages ?? messages;
@@ -186,8 +187,7 @@ export async function maybeCompact(input: {
186187
threshold: session.compaction.threshold,
187188
};
188189
const needsSummary =
189-
input.force === true ||
190-
(projectedPromptMessages.length > 0 && input.strategy.shouldCompact(facts));
190+
input.force === true || (projectedPromptMessages.length > 0 && strategy.shouldCompact(facts));
191191
const needsMemoryCanonicalization = shouldCanonicalizeMemory(messages);
192192

193193
if (!needsSummary && !needsMemoryCanonicalization) {
@@ -260,7 +260,7 @@ export async function maybeCompact(input: {
260260
input.historyProjector?.({ messages: canonical.ordinary, state: session.state }) ??
261261
canonical.ordinary,
262262
);
263-
let compacted: readonly ModelMessage[] = ordinary;
263+
let compacted: HarnessModelMessage[] | undefined;
264264
let failure: { readonly error: unknown } | undefined;
265265
try {
266266
if (needsSummary) {
@@ -270,7 +270,8 @@ export async function maybeCompact(input: {
270270
estimateTokens,
271271
summarize,
272272
};
273-
compacted = await input.strategy.compact([...ordinary], context);
273+
const result = await strategy.compact([...ordinary], context);
274+
compacted = validateHarnessModelMessages([...canonical.memory, ...result]);
274275
}
275276
} catch (error) {
276277
failure = { error };
@@ -284,7 +285,7 @@ export async function maybeCompact(input: {
284285
if (failure !== undefined) {
285286
return { compacted: false, failure, messages: input.messages, session };
286287
}
287-
messages = validateHarnessModelMessages([...canonical.memory, ...compacted]);
288+
messages = compacted ?? validateHarnessModelMessages([...canonical.memory, ...ordinary]);
288289

289290
{
290291
const ctx = contextStorage.getStore();

‎packages/eve/src/harness/model-call/call.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ import { requireSessionModelReference, type StepResult } from "#harness/types.js
5353
import type { InstrumentationAttempt } from "#instrumentation/runtime.js";
5454
import { createLogger, logError } from "#internal/logging.js";
5555
import { CompactionOverLimitError } from "#harness/compaction/over-limit-error.js";
56-
import { defaultCompactionStrategy } from "#harness/compaction/default-strategy.js";
5756
import { maybeCompact } from "#harness/compaction/step.js";
5857
import { reportCompactionFailure } from "./failure.js";
5958
import { buildGatewayAttributionHeaders, withGatewayTraceContext } from "./model.js";
@@ -288,7 +287,7 @@ export class ModelCaller {
288287
resolveModel: config.resolveModel,
289288
runtimeIdentity: config.runtimeIdentity,
290289
session: step.session,
291-
strategy: config.compactionStrategy ?? defaultCompactionStrategy,
290+
strategy: config.compactionStrategy,
292291
telemetry: step.instrumentation?.telemetry(),
293292
traceContextHeaders: step.instrumentation?.gatewayTraceContextHeaders,
294293
});

0 commit comments

Comments
 (0)