fix(agents): cap embedded runner retry loop

This commit is contained in:
Peter Steinberger
2026-02-21 15:35:45 +01:00
parent 352b5262da
commit b25d3652e7
4 changed files with 66 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
import "./run.overflow-compaction.mocks.shared.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { pickFallbackThinkingLevel } from "../pi-embedded-helpers.js";
import { compactEmbeddedPiSessionDirect } from "./compact.js";
import { runEmbeddedPiAgent } from "./run.js";
import { makeAttemptResult, mockOverflowRetrySuccess } from "./run.overflow-compaction.fixture.js";
@@ -16,6 +17,7 @@ const mockedSessionLikelyHasOversizedToolResults = vi.mocked(sessionLikelyHasOve
const mockedTruncateOversizedToolResultsInSession = vi.mocked(
truncateOversizedToolResultsInSession,
);
const mockedPickFallbackThinkingLevel = vi.mocked(pickFallbackThinkingLevel);
describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
beforeEach(() => {
@@ -106,4 +108,29 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(4);
expect(result.meta.error?.kind).toBe("context_overflow");
});
it("returns retry_limit when repeated retries never converge", async () => {
mockedRunEmbeddedAttempt.mockReset();
mockedCompactDirect.mockReset();
mockedPickFallbackThinkingLevel.mockReset();
mockedRunEmbeddedAttempt.mockResolvedValue(
makeAttemptResult({ promptError: new Error("unsupported reasoning mode") }),
);
mockedPickFallbackThinkingLevel.mockReturnValue("low");
const result = await runEmbeddedPiAgent({
sessionId: "test-session",
sessionKey: "test-key",
sessionFile: "/tmp/session.json",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 30000,
runId: "run-1",
});
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(24);
expect(mockedCompactDirect).not.toHaveBeenCalled();
expect(result.meta.error?.kind).toBe("retry_limit");
expect(result.payloads?.[0]?.isError).toBe(true);
});
});

View File

@@ -102,6 +102,9 @@ function createCompactionDiagId(): string {
return `ovf-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
// Defensive guard for the outer run loop across all retry branches.
const MAX_RUN_RETRY_ITERATIONS = 24;
const hasUsageValues = (
usage: ReturnType<typeof normalizeUsage>,
): usage is NonNullable<ReturnType<typeof normalizeUsage>> =>
@@ -475,13 +478,42 @@ export async function runEmbeddedPiAgent(
}
const MAX_OVERFLOW_COMPACTION_ATTEMPTS = 3;
const MAX_RUN_LOOP_ITERATIONS = MAX_RUN_RETRY_ITERATIONS;
let overflowCompactionAttempts = 0;
let toolResultTruncationAttempted = false;
const usageAccumulator = createUsageAccumulator();
let lastRunPromptUsage: ReturnType<typeof normalizeUsage> | undefined;
let autoCompactionCount = 0;
let runLoopIterations = 0;
try {
while (true) {
if (runLoopIterations >= MAX_RUN_LOOP_ITERATIONS) {
const message = `Exceeded retry limit after ${runLoopIterations} attempts.`;
log.error(
`[run-retry-limit] sessionKey=${params.sessionKey ?? params.sessionId} ` +
`provider=${provider}/${modelId} attempts=${runLoopIterations}`,
);
return {
payloads: [
{
text:
"Request failed after repeated internal retries. " +
"Please try again, or use /new to start a fresh session.",
isError: true,
},
],
meta: {
durationMs: Date.now() - started,
agentMeta: {
sessionId: params.sessionId,
provider,
model: model.id,
},
error: { kind: "retry_limit", message },
},
};
}
runLoopIterations += 1;
attemptedThinking.add(thinkLevel);
await fs.mkdir(resolvedWorkspace, { recursive: true });

View File

@@ -36,7 +36,12 @@ export type EmbeddedPiRunMeta = {
aborted?: boolean;
systemPromptReport?: SessionSystemPromptReport;
error?: {
kind: "context_overflow" | "compaction_failure" | "role_ordering" | "image_size";
kind:
| "context_overflow"
| "compaction_failure"
| "role_ordering"
| "image_size"
| "retry_limit";
message: string;
};
/** Stop reason for the agent run (e.g., "completed", "tool_calls"). */