kimi-code/packages/agent-core-v2/test/microCompaction/micro.test.ts

996 lines
29 KiB
TypeScript

import type { ContentPart } from '#/app/llmProtocol/kosong';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { estimateTokensForMessages } from '#/_base/utils/tokens';
import { microCompactionFlag } from '#/agent/microCompaction/flag';
import { IFlagService } from '#/app/flag';
import { MASTER_ENV } from '#/app/flag/flagService';
import {
AGENT_WIRE_PROTOCOL_VERSION,
IAgentMicroCompactionService,
type PersistedWireRecord,
} from '#/index';
import { InMemoryWireRecordPersistence, testAgent, type TestAgentContext } from '../harness';
import { recordingTelemetry, type TelemetryRecord } from '../telemetry/stubs';
const CATALOGUED_PROVIDER = {
type: 'kimi',
apiKey: 'test-key',
model: 'kimi-code',
} as const;
const CATALOGUED_MODEL_CAPABILITIES = {
image_in: true,
video_in: true,
audio_in: false,
thinking: true,
tool_use: true,
max_context_tokens: 256_000,
} as const;
const MINUTE = 60 * 1000;
const DEFAULT_MARKER = '[Old tool result content cleared]';
const MICRO_COMPACTION_FLAG_ENV = getMicroCompactionFlagEnv();
describe('MicroCompaction', () => {
beforeEach(() => {
vi.stubEnv(MASTER_ENV, '0');
vi.stubEnv(MICRO_COMPACTION_FLAG_ENV, '1');
});
it('defaults the micro_compaction flag on', () => {
vi.unstubAllEnvs();
vi.stubEnv(MASTER_ENV, '0');
const ctx = testAgent();
expect(ctx.get(IFlagService).enabled('micro_compaction')).toBe(true);
});
it('truncates old tool results after cache miss', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 4,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * 60 * 1000,
minContextUsageRatio: 0,
},
},
});
vi.setSystemTime(0);
ctx.appendToolExchange();
ctx.appendToolExchange();
ctx.appendToolExchange();
expect(ctx.project()).toHaveLength(9);
vi.setSystemTime(61 * 60 * 1000);
(ctx.get(IAgentMicroCompactionService) as any).detect();
const messages = ctx.project();
expect(messages[2]).toMatchObject({
role: 'tool',
content: [{ type: 'text', text: DEFAULT_MARKER }],
});
expect(messages[5]).toMatchObject({
role: 'tool',
content: [{ type: 'text', text: 'lookup result' }],
});
expect(messages[8]).toMatchObject({
role: 'tool',
content: [{ type: 'text', text: 'lookup result' }],
});
});
it('does nothing before cache miss threshold', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 4,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * 60 * 1000,
},
},
});
vi.setSystemTime(0);
ctx.appendToolExchange();
ctx.appendToolExchange();
ctx.appendToolExchange();
vi.setSystemTime(30 * 60 * 1000);
const messages = ctx.project();
expect(hasMarker(messages)).toBe(false);
});
it('persists cutoff across calls until cache miss resets it', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 2,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * 60 * 1000,
minContextUsageRatio: 0,
},
},
});
vi.setSystemTime(0);
ctx.appendToolExchange();
ctx.appendToolExchange();
vi.setSystemTime(61 * 60 * 1000);
(ctx.get(IAgentMicroCompactionService) as any).detect();
const first = ctx.project();
expect(first[2]).toMatchObject({
role: 'tool',
content: [{ type: 'text', text: DEFAULT_MARKER }],
});
vi.setSystemTime(62 * 60 * 1000);
(ctx.get(IAgentMicroCompactionService) as any).detect();
const second = ctx.project();
expect(second[2]).toMatchObject({
role: 'tool',
content: [{ type: 'text', text: DEFAULT_MARKER }],
});
});
it('clears cutoff on reset', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 4,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * 60 * 1000,
},
},
});
vi.setSystemTime(0);
ctx.appendToolExchange();
ctx.appendToolExchange();
vi.setSystemTime(61 * 60 * 1000);
(ctx.get(IAgentMicroCompactionService) as any).reset();
const messages = ctx.project();
expect(hasMarker(messages)).toBe(false);
});
it('skips tool results below minContentTokens', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 2,
minContentTokens: 100,
cacheMissedThresholdMs: 60 * 60 * 1000,
},
},
});
vi.setSystemTime(0);
ctx.appendToolExchange();
ctx.appendToolExchange();
vi.setSystemTime(61 * 60 * 1000);
const messages = ctx.project();
expect(hasMarker(messages)).toBe(false);
});
it('skips non-tool messages', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 2,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * 60 * 1000,
},
},
});
vi.setSystemTime(0);
ctx.appendExchange(1, 'user one', 'assistant one', 10);
ctx.appendExchange(2, 'user two', 'assistant two', 10);
ctx.appendExchange(3, 'user three', 'assistant three', 10);
vi.setSystemTime(61 * 60 * 1000);
const messages = ctx.project();
expect(messages.every((m) => m.role === 'user' || m.role === 'assistant')).toBe(true);
expect(hasMarker(messages)).toBe(false);
});
it('clears cutoff on context clear', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 2,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * 60 * 1000,
},
},
});
vi.setSystemTime(0);
ctx.appendToolExchange();
ctx.appendToolExchange();
vi.setSystemTime(61 * 60 * 1000);
ctx.clearContext();
expect(ctx.project()).toHaveLength(0);
expect((ctx.get(IAgentMicroCompactionService) as any).lastAssistantAt).toBeNull();
});
it('sends truncated old tool results to the next model request without mutating history', async () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 4,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
minContextUsageRatio: 0,
},
},
});
ctx.configure({
provider: CATALOGUED_PROVIDER,
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
});
vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, { output: 'old result one' });
appendMicroToolExchange(ctx, 2, { output: 'middle result two' });
appendMicroToolExchange(ctx, 3, { output: 'recent result three' });
vi.setSystemTime(61 * MINUTE);
ctx.mockNextResponse({ type: 'text', text: 'done after micro compaction' });
await ctx.rpc.prompt({ input: [{ type: 'text', text: 'continue' }] });
await ctx.untilTurnEnd();
const call = ctx.llmCalls.at(-1);
expect(textOf(call?.history[2])).toBe(DEFAULT_MARKER);
expect(textOf(call?.history[5])).toBe(DEFAULT_MARKER);
expect(textOf(call?.history[8])).toBe('recent result three');
expect(textOf(ctx.context.get()[2])).toBe('old result one');
expect(textOf(ctx.context.get()[5])).toBe('middle result two');
expect(textOf(ctx.context.get()[8])).toBe('recent result three');
await ctx.expectResumeMatches();
});
it('restores lastAssistantAt from record time before applying cache-miss rules', async () => {
vi.useFakeTimers();
const assistantRecordTime = 2_000;
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 0,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
minContextUsageRatio: 0,
},
},
persistence: new InMemoryWireRecordPersistence(
resumeToolExchangeRecords(assistantRecordTime),
),
});
vi.setSystemTime(999_999);
await ctx.restorePersisted();
expect((ctx.get(IAgentMicroCompactionService) as any).lastAssistantAt).toBe(
assistantRecordTime,
);
vi.setSystemTime(assistantRecordTime + 30 * MINUTE);
expect(hasMarker(ctx.project())).toBe(false);
vi.setSystemTime(assistantRecordTime + 61 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER]);
});
it('preserves the restored cutoff when resuming before the next cache miss', async () => {
vi.useFakeTimers();
const persistence = new InMemoryWireRecordPersistence();
const config = {
keepRecentMessages: 2,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
minContextUsageRatio: 0,
};
const ctx = testAgent({
microCompaction: { config },
persistence,
});
vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, { output: 'result one' });
appendMicroToolExchange(ctx, 2, { output: 'result two' });
vi.setSystemTime(61 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, 'result two']);
await ctx.wireRecord.flush();
expect(lastMicroCompactionCutoff(persistence.records)).toBe(4);
vi.setSystemTime(62 * MINUTE);
appendMicroToolExchange(ctx, 3, { output: 'result three' });
await ctx.wireRecord.flush();
const resumed = testAgent({
microCompaction: { config },
persistence: new InMemoryWireRecordPersistence(cloneRecords(persistence.records)),
});
vi.setSystemTime(63 * MINUTE);
await resumed.restorePersisted();
expect((resumed.get(IAgentMicroCompactionService) as any).lastAssistantAt).toBe(62 * MINUTE);
expect(toolTexts(resumed.project())).toEqual([DEFAULT_MARKER, 'result two', 'result three']);
});
it('recomputes the restored cutoff when resuming after the cache-miss threshold', async () => {
vi.useFakeTimers();
const persistence = new InMemoryWireRecordPersistence();
const config = {
keepRecentMessages: 2,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
minContextUsageRatio: 0,
};
const ctx = testAgent({
microCompaction: { config },
persistence,
});
vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, { output: 'result one' });
appendMicroToolExchange(ctx, 2, { output: 'result two' });
vi.setSystemTime(61 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, 'result two']);
await ctx.wireRecord.flush();
expect(lastMicroCompactionCutoff(persistence.records)).toBe(4);
vi.setSystemTime(62 * MINUTE);
appendMicroToolExchange(ctx, 3, { output: 'result three' });
await ctx.wireRecord.flush();
const resumedPersistence = new InMemoryWireRecordPersistence(cloneRecords(persistence.records));
const resumed = testAgent({
microCompaction: { config },
persistence: resumedPersistence,
});
vi.setSystemTime(123 * MINUTE);
await resumed.restorePersisted();
expect((resumed.get(IAgentMicroCompactionService) as any).lastAssistantAt).toBe(62 * MINUTE);
(resumed.get(IAgentMicroCompactionService) as any).detect();
expect(toolTexts(resumed.project())).toEqual([DEFAULT_MARKER, DEFAULT_MARKER, 'result three']);
await resumed.wireRecord.flush();
expect(lastMicroCompactionCutoff(resumedPersistence.records)).toBe(7);
});
it('keeps an old cutoff while cache is warm and advances it on the next miss', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 2,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
minContextUsageRatio: 0,
},
},
});
vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, { output: 'result one' });
appendMicroToolExchange(ctx, 2, { output: 'result two' });
vi.setSystemTime(61 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, 'result two']);
vi.setSystemTime(62 * MINUTE);
appendMicroToolExchange(ctx, 3, { output: 'result three' });
vi.setSystemTime(63 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, 'result two', 'result three']);
vi.setSystemTime(123 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, DEFAULT_MARKER, 'result three']);
});
it('clamps cutoff when undo shortens the context', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 2,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
minContextUsageRatio: 0,
},
},
});
vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, { output: 'result one' });
appendMicroToolExchange(ctx, 2, { output: 'result two' });
appendMicroToolExchange(ctx, 3, { output: 'result three' });
vi.setSystemTime(61 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, DEFAULT_MARKER, 'result three']);
ctx.undoHistory(2);
appendMicroToolExchange(ctx, 4, { output: 'result four' });
expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, 'result four']);
});
it('tracks telemetry when a cache miss advances the micro_compaction cutoff', () => {
vi.useFakeTimers();
const records: TelemetryRecord[] = [];
const microCompaction = {
keepRecentMessages: 2,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
minContextUsageRatio: 0,
};
const ctx = testAgent({
telemetry: recordingTelemetry(records),
microCompaction: { config: microCompaction },
});
vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, { output: 'result one '.repeat(20) });
appendMicroToolExchange(ctx, 2, { output: 'result two '.repeat(20) });
appendMicroToolExchange(ctx, 3, { output: 'result three' });
vi.setSystemTime(61 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, DEFAULT_MARKER, 'result three']);
const event = singleTelemetryEvent(records, 'micro_compaction_finished');
expect(event.properties).toMatchObject({
keep_recent_messages: microCompaction.keepRecentMessages,
min_content_tokens: microCompaction.minContentTokens,
cache_missed_threshold_ms: microCompaction.cacheMissedThresholdMs,
min_context_usage_ratio: microCompaction.minContextUsageRatio,
truncated_marker: DEFAULT_MARKER,
previous_cutoff: 0,
cutoff: 7,
message_count: 9,
cache_age_ms: 61 * MINUTE,
truncated_tool_result_count: 2,
truncated_tool_result_tokens_before: expect.any(Number),
truncated_tool_result_tokens_after: expect.any(Number),
tokens_before: expect.any(Number),
tokens_after: expect.any(Number),
thinking_level: 'off',
});
expect(numberProperty(event, 'truncated_tool_result_tokens_before')).toBeGreaterThan(
numberProperty(event, 'truncated_tool_result_tokens_after'),
);
expect(numberProperty(event, 'tokens_before')).toBeGreaterThan(
numberProperty(event, 'tokens_after'),
);
expect(ctx.project()).toHaveLength(9);
expect(records.filter((record) => record.event === 'micro_compaction_finished')).toHaveLength(
1,
);
});
it('reports context token deltas from the previously compacted projection', () => {
vi.useFakeTimers();
const records: TelemetryRecord[] = [];
const microCompaction = {
keepRecentMessages: 2,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
minContextUsageRatio: 0,
};
const ctx = testAgent({
telemetry: recordingTelemetry(records),
microCompaction: { config: microCompaction },
});
vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, { output: 'result one '.repeat(20) });
appendMicroToolExchange(ctx, 2, { output: 'result two '.repeat(20) });
vi.setSystemTime(61 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
expect(toolTexts(ctx.project())).toEqual([DEFAULT_MARKER, 'result two '.repeat(20)]);
vi.setSystemTime(62 * MINUTE);
appendMicroToolExchange(ctx, 3, { output: 'result three' });
const expectedContextTokensBefore = estimateTokensForMessages(ctx.project());
vi.setSystemTime(123 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
const events = records.filter((record) => record.event === 'micro_compaction_finished');
expect(events).toHaveLength(2);
const secondEvent = events[1]!;
expect(secondEvent.properties).toMatchObject({
previous_cutoff: 4,
cutoff: 7,
truncated_tool_result_count: 2,
tokens_before: expectedContextTokensBefore,
tokens_after: estimateTokensForMessages(ctx.project()),
});
});
it('leaves context unchanged when the micro_compaction flag is disabled', () => {
vi.stubEnv(MICRO_COMPACTION_FLAG_ENV, '0');
vi.useFakeTimers();
const persistence = new InMemoryWireRecordPersistence();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 0,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
minContextUsageRatio: 0,
},
},
persistence,
});
vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, { output: 'result one' });
vi.setSystemTime(61 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
expect(toolTexts(ctx.project())).toEqual(['result one']);
expect(lastMicroCompactionCutoff(persistence.records)).toBeUndefined();
});
it('uses the custom marker at the minContentTokens boundary', () => {
vi.useFakeTimers();
const marker = '[tool output removed for test]';
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 0,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
truncatedMarker: marker,
minContextUsageRatio: 0,
},
},
});
vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, { output: 'abcd' });
vi.setSystemTime(61 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
expect(toolTexts(ctx.project())).toEqual([marker]);
expect(textOf(ctx.context.get()[2])).toBe('abcd');
});
it('keeps raw pending token accounting even when projection truncates tool output', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 0,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
minContextUsageRatio: 0,
},
},
});
ctx.configure();
vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, {
output: 'x'.repeat(400),
usageTokens: 50,
});
vi.setSystemTime(61 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
const rawPending = ctx.context.get().slice(-1);
const projectedPending = (ctx.get(IAgentMicroCompactionService) as any).compact(rawPending);
expect(textOf(projectedPending[0])).toBe(DEFAULT_MARKER);
expect(ctx.contextSize.getStatus().contextTokensWithPending).toBe(
ctx.contextSize.getStatus().contextTokens + estimateTokensForMessages(rawPending),
);
expect(ctx.contextSize.getStatus().contextTokensWithPending).toBeGreaterThan(
ctx.contextSize.getStatus().contextTokens + estimateTokensForMessages(projectedPending),
);
});
it('replaces rich error tool content while preserving context metadata before projection', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 0,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
minContextUsageRatio: 0,
},
},
});
vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, {
output: [
{ type: 'text', text: 'large rich output' },
{ type: 'video_url', videoUrl: { url: 'ms://video-1', id: 'video-1' } },
],
isError: true,
});
vi.setSystemTime(61 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
const compacted = (ctx.get(IAgentMicroCompactionService) as any).compact(ctx.context.get());
const tool = compacted.find((message: any) => message.role === 'tool');
expect(tool).toMatchObject({
role: 'tool',
toolCallId: 'call_micro_1',
isError: true,
content: [{ type: 'text', text: DEFAULT_MARKER }],
});
expect(tool?.content).toHaveLength(1);
});
it('does not truncate tool-shaped messages without a toolCallId', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 0,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
},
},
});
vi.setSystemTime(0);
ctx.context.splice(ctx.context.get().length, 0, [
{
role: 'assistant',
content: [{ type: 'text', text: 'assistant anchor' }],
toolCalls: [],
},
]);
vi.setSystemTime(61 * MINUTE);
ctx.context.splice(ctx.context.get().length, 0, [
{
role: 'tool',
content: [{ type: 'text', text: 'orphan tool-like output' }],
toolCalls: [],
},
]);
(ctx.get(IAgentMicroCompactionService) as any).detect();
const compacted = (ctx.get(IAgentMicroCompactionService) as any).compact(ctx.context.get());
expect(toolTexts(compacted)).toEqual(['orphan tool-like output']);
});
it('clears cutoff on full compaction', async () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 2,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * 60 * 1000,
},
},
});
ctx.configure({
provider: CATALOGUED_PROVIDER,
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
});
vi.setSystemTime(0);
ctx.appendExchange(1, 'old user', 'old assistant', 20);
ctx.appendExchange(2, 'recent user', 'recent assistant', 80);
vi.setSystemTime(61 * 60 * 1000);
const compacted = ctx.once('full_compaction.complete');
ctx.mockNextResponse({ type: 'text', text: 'Summary.' });
await ctx.rpc.beginCompaction({});
await compacted;
expect(ctx.project()).toHaveLength(1);
expect(ctx.project()[0]).toMatchObject({
role: 'assistant',
content: [{ type: 'text', text: 'Summary.' }],
});
});
it('does not apply when context usage is below minContextUsageRatio', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 0,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
minContextUsageRatio: 0.9,
},
},
});
ctx.configure({
provider: CATALOGUED_PROVIDER,
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
});
vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, { output: 'result one' });
vi.setSystemTime(61 * MINUTE);
const messages = ctx.project();
expect(hasMarker(messages)).toBe(false);
});
it('applies when context usage is above minContextUsageRatio', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 0,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * MINUTE,
minContextUsageRatio: 0.5,
},
},
});
ctx.configure({
provider: CATALOGUED_PROVIDER,
modelCapabilities: {
image_in: true,
video_in: true,
audio_in: false,
thinking: true,
tool_use: true,
max_context_tokens: 100,
},
});
vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, { output: 'x'.repeat(300) });
vi.setSystemTime(61 * MINUTE);
(ctx.get(IAgentMicroCompactionService) as any).detect();
const messages = ctx.project();
expect(hasMarker(messages)).toBe(true);
});
it('does not truncate when messages are fewer than keepRecentMessages', () => {
vi.useFakeTimers();
const ctx = testAgent({
microCompaction: {
config: {
keepRecentMessages: 20,
minContentTokens: 1,
cacheMissedThresholdMs: 60 * 60 * 1000,
},
},
});
vi.setSystemTime(0);
ctx.appendToolExchange();
ctx.appendToolExchange();
vi.setSystemTime(61 * 60 * 1000);
const messages = ctx.project();
expect(hasMarker(messages)).toBe(false);
});
});
afterEach(() => {
vi.useRealTimers();
vi.unstubAllEnvs();
});
interface MicroToolExchangeOptions {
readonly output?: string | ContentPart[] | undefined;
readonly isError?: boolean | undefined;
readonly usageTokens?: number | undefined;
}
function appendMicroToolExchange(
ctx: TestAgentContext,
index: number,
options: MicroToolExchangeOptions = {},
): void {
const toolCallId = `call_micro_${String(index)}`;
const output = options.output ?? `lookup result ${String(index)}`;
const usage =
options.usageTokens === undefined
? undefined
: {
inputOther: options.usageTokens - 1,
output: 1,
inputCacheRead: 0,
inputCacheCreation: 0,
};
ctx.appendUserMessage([{ type: 'text', text: `lookup ${String(index)}` }]);
ctx.context.splice(ctx.context.get().length, 0, [
{
role: 'assistant',
content: [{ type: 'text', text: `calling Lookup ${String(index)}` }],
toolCalls: [
{
type: 'function',
id: toolCallId,
name: 'Lookup',
arguments: JSON.stringify({ query: `item-${String(index)}` }),
},
],
},
]);
if (usage !== undefined) {
ctx.contextSize.measured(ctx.context.get(), [], usage);
}
ctx.context.splice(ctx.context.get().length, 0, [
{
role: 'tool',
content: typeof output === 'string' ? [{ type: 'text', text: output }] : [...output],
toolCalls: [],
toolCallId,
isError: options.isError,
},
]);
}
function resumeToolExchangeRecords(assistantRecordTime: number): PersistedWireRecord[] {
return [
{
type: 'metadata',
protocol_version: AGENT_WIRE_PROTOCOL_VERSION,
created_at: 1,
},
{
type: 'context.splice',
time: 1_000,
start: 0,
deleteCount: 0,
messages: [
{
role: 'user',
content: [{ type: 'text', text: 'lookup from restored session' }],
toolCalls: [],
origin: { kind: 'user' },
},
],
},
{
type: 'turn.launch',
time: assistantRecordTime,
turnId: 0,
origin: { kind: 'user' },
},
{
type: 'context.splice',
time: assistantRecordTime,
start: 1,
deleteCount: 0,
messages: [
{
role: 'assistant',
content: [{ type: 'text', text: 'calling restored Lookup' }],
toolCalls: [
{
type: 'function',
id: 'resume_micro_call',
name: 'Lookup',
arguments: JSON.stringify({ query: 'restored' }),
},
],
},
],
},
{
type: 'context.splice',
time: assistantRecordTime + 2,
start: 2,
deleteCount: 0,
messages: [
{
role: 'tool',
content: [{ type: 'text', text: 'restored lookup result' }],
toolCalls: [],
toolCallId: 'resume_micro_call',
},
],
},
] as PersistedWireRecord[];
}
function cloneRecords(records: readonly PersistedWireRecord[]): PersistedWireRecord[] {
return records.map((record) => structuredClone(record));
}
function lastMicroCompactionCutoff(records: readonly PersistedWireRecord[]): number | undefined {
return (records.findLast((record) => record.type === 'micro_compaction.apply') as any)?.cutoff;
}
function toolTexts(
messages: readonly { role: string; content?: readonly { type: string; text?: string }[] }[],
): string[] {
return messages.filter((message) => message.role === 'tool').map((message) => textOf(message));
}
function textOf(
message: { content?: readonly { type: string; text?: string }[] } | undefined,
): string {
return (
message?.content
?.map((part) => {
if (part.type === 'text') return part.text;
return '';
})
.join('') ?? ''
);
}
function hasMarker(
messages: readonly { role: string; content?: readonly { type: string; text?: string }[] }[],
): boolean {
return toolTexts(messages).includes(DEFAULT_MARKER);
}
function getMicroCompactionFlagEnv(): string {
return microCompactionFlag.env;
}
function singleTelemetryEvent(records: readonly TelemetryRecord[], event: string): TelemetryRecord {
const matches = records.filter((record) => record.event === event);
expect(matches).toHaveLength(1);
return matches[0]!;
}
function numberProperty(record: TelemetryRecord, key: string): number {
const value = record.properties?.[key];
expect(typeof value).toBe('number');
return value as number;
}