




























@@ -0,0 +1,249 @@
1+import { beforeAll, beforeEach, describe, expect, it } from "vitest";
2+import type {
3+diagnosticSessionStates as DiagnosticSessionStatesType,
4+getDiagnosticSessionState as GetDiagnosticSessionStateType,
5+SessionState,
6+} from "../../logging/diagnostic-session-state.js";
7+import type { hashToolCall as HashToolCallType } from "../tool-loop-detection.js";
8+import type { PostCompactionLoopPersistedError as PostCompactionLoopPersistedErrorType } from "./post-compaction-loop-guard.js";
9+import {
10+makeAttemptResult,
11+makeCompactionSuccess,
12+makeOverflowError,
13+} from "./run.overflow-compaction.fixture.js";
14+import {
15+loadRunOverflowCompactionHarness,
16+mockedCompactDirect,
17+mockedContextEngine,
18+mockedIsCompactionFailureError,
19+mockedIsLikelyContextOverflowError,
20+mockedLog,
21+mockedRunEmbeddedAttempt,
22+mockedSessionLikelyHasOversizedToolResults,
23+mockedTruncateOversizedToolResultsInSession,
24+overflowBaseRunParams as baseParams,
25+} from "./run.overflow-compaction.harness.js";
26+27+let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent;
28+// These need to be imported AFTER loadRunOverflowCompactionHarness so that
29+// they reference the same module instances the (re-imported) runner uses.
30+// vi.resetModules() inside the harness invalidates any earlier import.
31+let diagnosticSessionStates: typeof DiagnosticSessionStatesType;
32+let getDiagnosticSessionState: typeof GetDiagnosticSessionStateType;
33+let hashToolCall: typeof HashToolCallType;
34+let PostCompactionLoopPersistedError: typeof PostCompactionLoopPersistedErrorType;
35+36+function recordToolOutcome(
37+state: SessionState,
38+toolName: string,
39+toolParams: unknown,
40+resultHash: string,
41+runId?: string,
42+): void {
43+if (!state.toolCallHistory) {
44+state.toolCallHistory = [];
45+}
46+state.toolCallHistory.push({
47+ toolName,
48+argsHash: hashToolCall(toolName, toolParams),
49+ resultHash,
50+timestamp: Date.now(),
51+ ...(runId ? { runId } : {}),
52+});
53+}
54+55+describe("post-compaction loop guard wired into runEmbeddedPiAgent", () => {
56+beforeAll(async () => {
57+({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness());
58+// Re-import after the harness reset so we share module instances with
59+// the runner. The runner imports both modules through its own graph.
60+({ diagnosticSessionStates, getDiagnosticSessionState } =
61+await import("../../logging/diagnostic-session-state.js"));
62+({ hashToolCall } = await import("../tool-loop-detection.js"));
63+({ PostCompactionLoopPersistedError } = await import("./post-compaction-loop-guard.js"));
64+});
65+66+beforeEach(() => {
67+diagnosticSessionStates.clear();
68+mockedRunEmbeddedAttempt.mockReset();
69+mockedCompactDirect.mockReset();
70+mockedSessionLikelyHasOversizedToolResults.mockReset();
71+mockedTruncateOversizedToolResultsInSession.mockReset();
72+mockedContextEngine.info.ownsCompaction = false;
73+mockedLog.debug.mockReset();
74+mockedLog.info.mockReset();
75+mockedLog.warn.mockReset();
76+mockedLog.error.mockReset();
77+mockedLog.isEnabled.mockReset();
78+mockedLog.isEnabled.mockReturnValue(false);
79+mockedIsCompactionFailureError.mockImplementation((msg?: string) => {
80+if (!msg) {
81+return false;
82+}
83+const lower = msg.toLowerCase();
84+return lower.includes("request_too_large") && lower.includes("summarization failed");
85+});
86+mockedIsLikelyContextOverflowError.mockImplementation((msg?: string) => {
87+if (!msg) {
88+return false;
89+}
90+const lower = msg.toLowerCase();
91+return (
92+lower.includes("request_too_large") ||
93+lower.includes("request size exceeds") ||
94+lower.includes("context window exceeded") ||
95+lower.includes("prompt too large")
96+);
97+});
98+mockedCompactDirect.mockResolvedValue({
99+ok: false,
100+compacted: false,
101+reason: "nothing to compact",
102+});
103+mockedSessionLikelyHasOversizedToolResults.mockReturnValue(false);
104+mockedTruncateOversizedToolResultsInSession.mockResolvedValue({
105+truncated: false,
106+truncatedCount: 0,
107+reason: "no oversized tool results",
108+});
109+});
110+111+it("aborts the run with PostCompactionLoopPersistedError when identical (tool, args, result) repeats windowSize times after compaction", async () => {
112+const overflowError = makeOverflowError();
113+const sessionState = getDiagnosticSessionState({
114+sessionKey: baseParams.sessionKey,
115+sessionId: baseParams.sessionId,
116+});
117+118+// Attempt 1: overflow → triggers compaction.
119+mockedRunEmbeddedAttempt.mockImplementationOnce(async () =>
120+makeAttemptResult({ promptError: overflowError }),
121+);
122+// Attempt 2: post-compaction. The wrapped tool layer would have
123+// recorded `windowSize` identical (tool, args, result) outcomes during
124+// this single attempt. The runner's after-attempt guard observation
125+// sees all three at once, accumulates matches, and aborts on the third.
126+mockedRunEmbeddedAttempt.mockImplementationOnce(async () => {
127+for (let i = 0; i < 3; i += 1) {
128+recordToolOutcome(
129+sessionState,
130+"gateway",
131+{ action: "lookup", path: "x" },
132+"identical-result",
133+baseParams.runId,
134+);
135+}
136+return makeAttemptResult({
137+promptError: null,
138+toolMetas: [{ toolName: "gateway" }, { toolName: "gateway" }, { toolName: "gateway" }],
139+});
140+});
141+142+mockedCompactDirect.mockResolvedValueOnce(
143+makeCompactionSuccess({
144+summary: "Compacted session",
145+firstKeptEntryId: "entry-5",
146+tokensBefore: 150000,
147+}),
148+);
149+150+await expect(runEmbeddedPiAgent(baseParams)).rejects.toBeInstanceOf(
151+PostCompactionLoopPersistedError,
152+);
153+154+expect(mockedCompactDirect).toHaveBeenCalledTimes(1);
155+expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
156+});
157+158+it("does not abort when the result hash changes across post-compaction attempts (progress was made)", async () => {
159+const overflowError = makeOverflowError();
160+const sessionState = getDiagnosticSessionState({
161+sessionKey: baseParams.sessionKey,
162+sessionId: baseParams.sessionId,
163+});
164+165+// Attempt 1: overflow → triggers compaction.
166+mockedRunEmbeddedAttempt.mockImplementationOnce(async () =>
167+makeAttemptResult({ promptError: overflowError }),
168+);
169+// Attempt 2 (post-compaction): identical args, but DIFFERENT result hash
170+// each time. Only one further attempt is needed since the runner exits
171+// on a successful prompt with no further retry trigger.
172+let callCounter = 0;
173+mockedRunEmbeddedAttempt.mockImplementationOnce(async () => {
174+callCounter += 1;
175+recordToolOutcome(
176+sessionState,
177+"gateway",
178+{ action: "lookup", path: "x" },
179+`result-${callCounter}`,
180+baseParams.runId,
181+);
182+return makeAttemptResult({
183+promptError: null,
184+toolMetas: [{ toolName: "gateway" }],
185+});
186+});
187+188+mockedCompactDirect.mockResolvedValueOnce(
189+makeCompactionSuccess({
190+summary: "Compacted session",
191+firstKeptEntryId: "entry-5",
192+tokensBefore: 150000,
193+}),
194+);
195+196+const result = await runEmbeddedPiAgent(baseParams);
197+expect(result.meta.error).toBeUndefined();
198+expect(mockedCompactDirect).toHaveBeenCalledTimes(1);
199+expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
200+});
201+202+it("disarms after windowSize observations regardless of match, so later identical calls do not abort", async () => {
203+// Use windowSize: 2 so the guard disarms after 2 observations.
204+const overflowError = makeOverflowError();
205+const sessionState = getDiagnosticSessionState({
206+sessionKey: baseParams.sessionKey,
207+sessionId: baseParams.sessionId,
208+});
209+210+// Attempt 1: overflow → triggers compaction.
211+mockedRunEmbeddedAttempt.mockImplementationOnce(async () =>
212+makeAttemptResult({ promptError: overflowError }),
213+);
214+// Attempt 2 (post-compaction): two distinct records → window full,
215+// guard disarms with no abort. We then append more identical records
216+// afterwards in this test to confirm they are not observed by the guard.
217+mockedRunEmbeddedAttempt.mockImplementationOnce(async () => {
218+recordToolOutcome(sessionState, "read", { path: "/a" }, "ra", baseParams.runId);
219+recordToolOutcome(sessionState, "write", { path: "/b" }, "rb", baseParams.runId);
220+return makeAttemptResult({
221+promptError: null,
222+toolMetas: [{ toolName: "read" }, { toolName: "write" }],
223+});
224+});
225+226+mockedCompactDirect.mockResolvedValueOnce(
227+makeCompactionSuccess({
228+summary: "Compacted session",
229+firstKeptEntryId: "entry-5",
230+tokensBefore: 150000,
231+}),
232+);
233+234+const result = await runEmbeddedPiAgent({
235+ ...baseParams,
236+config: {
237+tools: {
238+loopDetection: {
239+postCompactionGuard: { enabled: true, windowSize: 2 },
240+},
241+},
242+} as never,
243+});
244+245+expect(result.meta.error).toBeUndefined();
246+expect(mockedCompactDirect).toHaveBeenCalledTimes(1);
247+expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
248+});
249+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。