Skip to content
36 changes: 29 additions & 7 deletions packages/agent-core/src/agent/compaction/micro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import type { ContentPart } from '@moonshot-ai/kosong';

import type { Agent } from '..';
import type { ContextMessage } from '../context';
import { estimateTokensForContentParts } from '../../utils/tokens';
import {
estimateTokensForContentParts,
estimateTokensForMessages,
} from '../../utils/tokens';

export interface MicroCompactionConfig {
keepRecentMessages: number;
Expand Down Expand Up @@ -65,9 +68,24 @@ export class MicroCompaction {
this.apply(nextCutoff);
if (previousCutoff !== nextCutoff) {
const effect = this.measureEffect(history, nextCutoff);
this.agent.telemetry.track('micro_compaction_applied', {
const previousEffect = this.measureEffect(history, previousCutoff);
const rawContextTokens = estimateTokensForMessages(history);
// Whole-context length before/after this cutoff change, mirroring the
// `tokensBefore`/`tokensAfter` fields on `compaction_finished` so the
// two compaction paths can be compared on the same axis.
const tokensBefore =
rawContextTokens -
previousEffect.truncatedToolResultTokensBefore +
previousEffect.truncatedToolResultTokensAfter;
const tokensAfter =
rawContextTokens -
effect.truncatedToolResultTokensBefore +
effect.truncatedToolResultTokensAfter;
this.agent.telemetry.track('micro_compaction_finished', {
...config,
...effect,
tokensBefore,
tokensAfter,
previous_cutoff: previousCutoff,
cutoff: nextCutoff,
message_count: history.length,
Expand Down Expand Up @@ -107,8 +125,8 @@ export class MicroCompaction {
) {
let markerTokenCount: number | undefined;
let truncatedToolResultCount = 0;
let beforeTokens = 0;
let afterTokens = 0;
let truncatedToolResultTokensBefore = 0;
let truncatedToolResultTokensAfter = 0;
for (let i = 0; i < messages.length && i < cutoff; i++) {
const message = messages[i];
if (message?.role !== 'tool' || message.toolCallId === undefined) continue;
Expand All @@ -120,9 +138,13 @@ export class MicroCompaction {
{ type: 'text', text: this.config.truncatedMarker },
]);
truncatedToolResultCount += 1;
beforeTokens += contentTokens;
afterTokens += markerTokenCount;
truncatedToolResultTokensBefore += contentTokens;
truncatedToolResultTokensAfter += markerTokenCount;
}
return { truncatedToolResultCount, beforeTokens, afterTokens };
return {
truncatedToolResultCount,
truncatedToolResultTokensBefore,
truncatedToolResultTokensAfter,
};
}
}
61 changes: 55 additions & 6 deletions packages/agent-core/test/agent/compaction/micro.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ describe('MicroCompaction', () => {
'result three',
]);

const event = singleTelemetryEvent(records, 'micro_compaction_applied');
const event = singleTelemetryEvent(records, 'micro_compaction_finished');
expect(event.properties).toMatchObject({
...microCompaction,
truncatedMarker: DEFAULT_MARKER,
Expand All @@ -478,15 +478,64 @@ describe('MicroCompaction', () => {
message_count: 9,
cache_age_ms: 61 * MINUTE,
truncatedToolResultCount: 2,
beforeTokens: expect.any(Number),
afterTokens: expect.any(Number),
truncatedToolResultTokensBefore: expect.any(Number),
truncatedToolResultTokensAfter: expect.any(Number),
tokensBefore: expect.any(Number),
tokensAfter: expect.any(Number),
});
expect(numberProperty(event, 'beforeTokens')).toBeGreaterThan(
numberProperty(event, 'afterTokens'),
expect(numberProperty(event, 'truncatedToolResultTokensBefore')).toBeGreaterThan(
numberProperty(event, 'truncatedToolResultTokensAfter'),
);
expect(numberProperty(event, 'tokensBefore')).toBeGreaterThan(
numberProperty(event, 'tokensAfter'),
);

expect(ctx.agent.context.messages).toHaveLength(9);
expect(records.filter((record) => record.event === 'micro_compaction_applied')).toHaveLength(1);
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,
});

vi.setSystemTime(0);
appendMicroToolExchange(ctx, 1, { output: 'result one '.repeat(20) });
appendMicroToolExchange(ctx, 2, { output: 'result two '.repeat(20) });

vi.setSystemTime(61 * MINUTE);
ctx.agent.microCompaction.detect();
expect(toolTexts(ctx.agent.context.messages)).toEqual([
DEFAULT_MARKER,
'result two '.repeat(20),
]);

vi.setSystemTime(62 * MINUTE);
appendMicroToolExchange(ctx, 3, { output: 'result three' });
const expectedContextTokensBefore = estimateTokensForMessages(ctx.agent.context.messages);

vi.setSystemTime(123 * MINUTE);
ctx.agent.microCompaction.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,
truncatedToolResultCount: 2,
tokensBefore: expectedContextTokensBefore,
tokensAfter: estimateTokensForMessages(ctx.agent.context.messages),
});
});

it('leaves context unchanged when the micro_compaction flag is disabled', () => {
Expand Down
Loading