1import { assert } from "@effect/vitest";
2import type { ProviderReplayTranscript } from "@t3tools/contracts";
3
4import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
5import {
6 assertBaseProjection,
7 assertSemanticProjectionIntegrity,
8 projectionFor,
9} from "../shared.ts";
10import { PI_COMPACTION_MARKER } from "./input.ts";
11
12function field(value: unknown, key: string): unknown {
13 return typeof value === "object" && value !== null ? Reflect.get(value, key) : undefined;
14}
15
16/**
17 * `/compact` reaches Pi as the RPC `compact` command, never as a prompt, with
18 * any trailing text as custom instructions. A compaction Pi refuses still
19 * settles its run and shows a failed compaction row; a successful one carries
20 * Pi's own summary and token counts, and the next turn recalls the marker.
21 */
22export function assertPiCompactionOutput(
23 result: OrchestratorV2ScenarioResult,
24 transcript: ProviderReplayTranscript,
25) {
26 const outbound = transcript.entries.flatMap((entry) =>
27 entry.type === "expect_outbound" ? [entry.frame] : [],
28 );
29 assert.isFalse(
30 outbound.some(
31 (frame) =>
32 field(frame, "type") === "prompt" && String(field(frame, "message")).startsWith("/compact"),
33 ),
34 "/compact must not be sent as a prompt",
35 );
36 assert.deepEqual(
37 outbound.filter((frame) => field(frame, "type") === "compact"),
38 [
39 { type: "compact", customInstructions: `keep the opaque marker ${PI_COMPACTION_MARKER}` },
40 { type: "compact" },
41 ],
42 );
43 const compactionEnds = transcript.entries
44 .flatMap((entry) => (entry.type === "emit_inbound" ? [entry.frame] : []))
45 .filter((frame) => field(frame, "type") === "compaction_end");
46 assert.lengthOf(compactionEnds, 2);
47 const [refused, compacted] = compactionEnds;
48 // Pi omits `result` on failure rather than sending null.
49 assert.isFalse(typeof refused === "object" && refused !== null && "result" in refused);
50 const compactionResult = field(compacted, "result");
51 assert.isDefined(compactionResult, "the second compaction must succeed");
52
53 assertBaseProjection({
54 result,
55 transcript,
56 runCount: 6,
57 runStatuses: ["completed", "completed", "completed", "completed", "completed", "completed"],
58 });
59 const projection = projectionFor(result, transcript.scenario);
60 assertSemanticProjectionIntegrity(projection);
61 const compactionRuns = [projection.runs[1], projection.runs[4]];
62 const compactions = projection.turnItems.filter((item) => item.type === "compaction");
63 assert.deepEqual(
64 compactions.map((item) => [item.runId, item.status, item.title]),
65 [
66 [compactionRuns[0]?.id ?? null, "failed", "Context compaction failed"],
67 [compactionRuns[1]?.id ?? null, "completed", "Context compacted"],
68 ],
69 );
70 assert.equal(compactions[0]?.summary, field(refused, "errorMessage"));
71 assert.equal(compactions[1]?.summary, field(compactionResult, "summary"));
72 assert.equal(compactions[1]?.beforeTokenCount, field(compactionResult, "tokensBefore"));
73 assert.equal(compactions[1]?.afterTokenCount, field(compactionResult, "estimatedTokensAfter"));
74 assert.isFalse(
75 projection.turnItems.some(
76 (item) =>
77 compactionRuns.some((run) => run?.id === item.runId) && item.type === "assistant_message",
78 ),
79 "compaction must not run a model turn",
80 );
81 // Right after compacting, Pi reports context usage as unknown (tokens: null);
82 // the meter keeps Pi's own post-compaction estimate instead of going blank.
83 const compactionTurn = projection.providerTurns.find(
84 (turn) => turn.runAttemptId === compactionRuns[1]?.activeAttemptId,
85 );
86 assert.equal(
87 compactionTurn?.tokenUsage?.usedTokens,
88 field(compactionResult, "estimatedTokensAfter"),
89 );
90
91 const recall = projection.turnItems.findLast((item) => item.type === "assistant_message");
92 assert.include(recall?.text, PI_COMPACTION_MARKER);
93}