Files
openmaic/OpenMAIC/tests/lib/chat/pi/director-compaction.test.ts
2026-08-16 14:58:47 +08:00

190 lines
6.6 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { createAssistantMessageEventStream, type AssistantMessage } from '@earendil-works/pi-ai';
import type { AgentMessage, StreamFn } from '@earendil-works/pi-agent-core';
import { createDirectorCompactionRuntime } from '@/lib/chat/pi/director-compaction';
import { toHistoryMessages } from '@/lib/chat/pi/prompts';
const EMPTY_USAGE = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
function summaryStream(summary: string, callCount: () => void): StreamFn {
return ((_model, _context, _options) => {
callCount();
const stream = createAssistantMessageEventStream();
const message: AssistantMessage = {
role: 'assistant',
content: [{ type: 'text', text: summary }],
api: 'test',
provider: 'test',
model: 'test-summary',
usage: EMPTY_USAGE,
stopReason: 'stop',
timestamp: Date.now(),
};
queueMicrotask(() => stream.push({ type: 'done', reason: 'stop', message }));
return stream;
}) as StreamFn;
}
function longHistory(): AgentMessage[] {
return Array.from({ length: 10 }, (_, index) => ({
role: 'user' as const,
content: `history-${index}: ${'important classroom evidence '.repeat(35)}`,
timestamp: index,
}));
}
function alternatingUiHistory() {
return Array.from({ length: 12 }, (_, index) => ({
id: `message-${index}`,
role: index % 2 === 0 ? ('user' as const) : ('assistant' as const),
parts: [
{
type: 'text' as const,
text: `turn-${index}: ${'important classroom evidence '.repeat(35)}`,
},
],
}));
}
describe('Pi Director compaction runtime', () => {
it('uses Pi session preparation and compaction, then retains new messages', async () => {
const onSummaryCall = vi.fn();
const runtime = createDirectorCompactionRuntime({
streamFn: summaryStream(
'Goal: preserve scene-2 evidence and https://example.test/source.',
onSummaryCall,
),
contextWindow: 600,
maxOutputTokens: 200,
settings: { enabled: true, reserveTokens: 160, keepRecentTokens: 120 },
});
const history = longHistory();
const compacted = await runtime.transformContext(history);
const trace = runtime.getTrace();
const event = trace.events[0];
expect(compacted[0]?.role).toBe('compactionSummary');
expect(compacted.length).toBeLessThan(history.length);
expect(onSummaryCall).toHaveBeenCalledTimes(1);
expect(trace).toMatchObject({
contextWindow: 600,
checkCount: 1,
triggerCount: 1,
failures: [],
});
expect(event).toBeDefined();
expect(event?.tokensBefore).toBeGreaterThan(event?.tokensAfter ?? Number.MAX_SAFE_INTEGER);
expect(event?.messagesBefore).toBeGreaterThan(event?.messagesAfter ?? Number.MAX_SAFE_INTEGER);
expect(event?.firstKeptEntryId).toBeTruthy();
expect(event?.summary).toContain('scene-2 evidence');
const tokenReduction = 1 - event!.tokensAfter / event!.tokensBefore;
const messageReduction = 1 - event!.messagesAfter / event!.messagesBefore;
expect(tokenReduction).toBeGreaterThan(0);
expect(messageReduction).toBeGreaterThan(0);
console.info(
'[COMPACTION-TRIGGER-01]',
JSON.stringify({
checkCount: trace.checkCount,
triggerCount: trace.triggerCount,
failures: trace.failures,
tokensBefore: event!.tokensBefore,
tokensAfter: event!.tokensAfter,
tokenReduction,
messagesBefore: event!.messagesBefore,
messagesAfter: event!.messagesAfter,
messageReduction,
firstKeptEntryId: event!.firstKeptEntryId,
summary: event!.summary,
}),
);
const nextMessage: AgentMessage = {
role: 'user',
content: 'NEW_FOLLOW_UP_AFTER_COMPACTION',
timestamp: 99,
};
const continued = await runtime.transformContext([...history, nextMessage]);
expect(continued[0]?.role).toBe('compactionSummary');
expect(
continued.some(
(message) =>
message.role === 'user' && message.content === 'NEW_FOLLOW_UP_AFTER_COMPACTION',
),
).toBe(true);
expect(onSummaryCall).toHaveBeenCalledTimes(1);
runtime.dispose();
});
it('does not compact a short history below the real threshold', async () => {
const onSummaryCall = vi.fn();
const runtime = createDirectorCompactionRuntime({
streamFn: summaryStream('unused', onSummaryCall),
contextWindow: 128_000,
});
const history: AgentMessage[] = [{ role: 'user', content: 'short question', timestamp: 1 }];
const transformed = await runtime.transformContext(history);
expect(transformed).toEqual(history);
expect(runtime.getTrace()).toMatchObject({ checkCount: 1, triggerCount: 0, events: [] });
expect(onSummaryCall).not.toHaveBeenCalled();
runtime.dispose();
});
it('compacts real alternating UI history even when imported assistant usage is zero', async () => {
const onSummaryCall = vi.fn();
const runtime = createDirectorCompactionRuntime({
streamFn: summaryStream(
'Preserve the current lesson goal and scene evidence.',
onSummaryCall,
),
contextWindow: 600,
maxOutputTokens: 200,
settings: { enabled: true, reserveTokens: 160, keepRecentTokens: 120 },
});
const history = toHistoryMessages(alternatingUiHistory(), null);
const compacted = await runtime.transformContext(history);
expect(compacted[0]?.role).toBe('compactionSummary');
expect(onSummaryCall).toHaveBeenCalled();
expect(runtime.getTrace()).toMatchObject({ checkCount: 1, triggerCount: 1, failures: [] });
expect(runtime.getTrace().events[0]?.tokensBefore).toBeGreaterThan(440);
expect(runtime.getTrace().events[0]?.tokensAfter).toBeLessThan(
runtime.getTrace().events[0]?.tokensBefore ?? 0,
);
runtime.dispose();
});
it('falls back to the default context window for invalid model metadata', () => {
const runtime = createDirectorCompactionRuntime({
streamFn: summaryStream('unused', vi.fn()),
contextWindow: Number.NaN,
});
expect(runtime.getTrace().contextWindow).toBe(128_000);
runtime.dispose();
});
it('allows the Director to retain all UI history before compaction', () => {
const messages = Array.from({ length: 20 }, (_, index) => ({
id: `message-${index}`,
role: 'user' as const,
parts: [{ type: 'text' as const, text: `message ${index}` }],
}));
expect(toHistoryMessages(messages)).toHaveLength(12);
expect(toHistoryMessages(messages, null)).toHaveLength(20);
});
});