


























@@ -17,16 +17,20 @@ import type { EmbeddedRunAttemptResult } from "./run/types.js";
17171818let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent;
1919const DEEPSEEK_ERROR_MESSAGE = "429 deepseek rate limit";
20+type CurrentAttemptAssistantWithError = NonNullable<
21+EmbeddedRunAttemptResult["currentAttemptAssistant"]
22+> & { errorMessage: string };
20232124function isCurrentAttemptAssistant(
2225value: unknown,
23-): value is NonNullable<EmbeddedRunAttemptResult["currentAttemptAssistant"]> {
26+): value is CurrentAttemptAssistantWithError {
2427return (
2528typeof value === "object" &&
2629value !== null &&
2730"provider" in value &&
2831"model" in value &&
29-"errorMessage" in value
32+"errorMessage" in value &&
33+typeof value.errorMessage === "string"
3034);
3135}
3236@@ -130,16 +134,24 @@ describe("runEmbeddedPiAgent cross-provider fallback error handling", () => {
130134});
131135132136it("falls back to the session assistant when compaction removes the current attempt slice", async () => {
133-setupDeepseekFallbackErrorMatchers();
134137const getLastFormattedAssistant = captureFormattedAssistant();
138+const sameCandidateErrorMessage = "429 current candidate rate limit";
139+mockedIsFailoverAssistantError.mockImplementation((...args: unknown[]) => {
140+const assistant = args[0];
141+return isCurrentAttemptAssistant(assistant) && assistant.provider === "anthropic";
142+});
143+mockedIsRateLimitAssistantError.mockImplementation((...args: unknown[]) => {
144+const assistant = args[0];
145+return isCurrentAttemptAssistant(assistant) && assistant.provider === "anthropic";
146+});
135147mockedRunEmbeddedAttempt.mockResolvedValueOnce(
136148makeAttemptResult({
137149assistantTexts: [],
138150lastAssistant: makeAssistantMessageFixture({
139151stopReason: "error",
140-errorMessage: DEEPSEEK_ERROR_MESSAGE,
141-provider: "deepseek",
142-model: "deepseek-chat",
152+errorMessage: sameCandidateErrorMessage,
153+provider: "anthropic",
154+model: "test-model",
143155content: [],
144156}),
145157currentAttemptAssistant: undefined,
@@ -152,6 +164,119 @@ describe("runEmbeddedPiAgent cross-provider fallback error handling", () => {
152164config: makeCrossProviderFallbackConfig(),
153165});
154166155-await expectDeepseekFallbackError(promise, getLastFormattedAssistant);
167+await expect(promise).rejects.toBeInstanceOf(MockedFailoverError);
168+await expect(promise).rejects.toThrow(`anthropic/test-model: ${sameCandidateErrorMessage}`);
169+expect(mockedIsRateLimitAssistantError).toHaveBeenCalledTimes(1);
170+expect(getLastFormattedAssistant()).toMatchObject({
171+provider: "anthropic",
172+model: "test-model",
173+errorMessage: sameCandidateErrorMessage,
174+});
175+});
176+177+it("keeps PI-stamped session assistant errors for the current candidate after compaction", async () => {
178+const getLastFormattedAssistant = captureFormattedAssistant();
179+const sameCandidateErrorMessage = "429 current PI-stamped candidate rate limit";
180+mockedIsFailoverAssistantError.mockImplementation((...args: unknown[]) => {
181+const assistant = args[0];
182+return isCurrentAttemptAssistant(assistant) && assistant.provider === "pi";
183+});
184+mockedIsRateLimitAssistantError.mockImplementation((...args: unknown[]) => {
185+const assistant = args[0];
186+return isCurrentAttemptAssistant(assistant) && assistant.provider === "pi";
187+});
188+mockedRunEmbeddedAttempt.mockResolvedValueOnce(
189+makeAttemptResult({
190+assistantTexts: [],
191+lastAssistant: makeAssistantMessageFixture({
192+stopReason: "error",
193+errorMessage: sameCandidateErrorMessage,
194+provider: "pi",
195+model: "pi",
196+content: [],
197+}),
198+currentAttemptAssistant: undefined,
199+}),
200+);
201+202+const promise = runEmbeddedPiAgent({
203+ ...overflowBaseRunParams,
204+runId: "run-compaction-pi-stamped-fallback-error-context",
205+config: makeCrossProviderFallbackConfig(),
206+});
207+208+await expect(promise).rejects.toBeInstanceOf(MockedFailoverError);
209+await expect(promise).rejects.toThrow(sameCandidateErrorMessage);
210+expect(mockedIsRateLimitAssistantError).toHaveBeenCalledTimes(1);
211+const rateLimitCalls = mockedIsRateLimitAssistantError.mock.calls as unknown[][];
212+expect(rateLimitCalls.at(-1)?.[0]).toMatchObject({
213+provider: "pi",
214+model: "pi",
215+errorMessage: sameCandidateErrorMessage,
216+});
217+expect(getLastFormattedAssistant()).toMatchObject({
218+provider: "pi",
219+model: "pi",
220+errorMessage: sameCandidateErrorMessage,
221+});
222+});
223+224+it("does not reuse a prior provider session assistant when the current candidate times out", async () => {
225+const getLastFormattedAssistant = captureFormattedAssistant();
226+mockedRunEmbeddedAttempt.mockResolvedValueOnce(
227+makeAttemptResult({
228+assistantTexts: [],
229+timedOut: true,
230+lastAssistant: makeAssistantMessageFixture({
231+stopReason: "error",
232+errorMessage: "You exceeded your current OpenAI quota.",
233+provider: "openai-codex",
234+model: "gpt-5.4",
235+content: [],
236+}),
237+currentAttemptAssistant: undefined,
238+}),
239+);
240+241+const promise = runEmbeddedPiAgent({
242+ ...overflowBaseRunParams,
243+runId: "run-stale-session-assistant-timeout",
244+config: makeCrossProviderFallbackConfig(),
245+});
246+247+await expect(promise).rejects.toBeInstanceOf(MockedFailoverError);
248+await expect(promise).rejects.toThrow("LLM request timed out.");
249+await expect(promise).rejects.not.toThrow("OpenAI quota");
250+expect(getLastFormattedAssistant()).toBeUndefined();
251+});
252+253+it("does not reuse a prior provider session assistant for non-timeout failover", async () => {
254+mockedIsFailoverAssistantError.mockImplementation((...args: unknown[]) => {
255+const assistant = args[0];
256+return isCurrentAttemptAssistant(assistant) && assistant.errorMessage.includes("quota");
257+});
258+const getLastFormattedAssistant = captureFormattedAssistant();
259+mockedRunEmbeddedAttempt.mockResolvedValueOnce(
260+makeAttemptResult({
261+assistantTexts: [],
262+lastAssistant: makeAssistantMessageFixture({
263+stopReason: "error",
264+errorMessage: "You exceeded your current OpenAI quota.",
265+provider: "openai-codex",
266+model: "gpt-5.4",
267+content: [],
268+}),
269+currentAttemptAssistant: undefined,
270+}),
271+);
272+273+await runEmbeddedPiAgent({
274+ ...overflowBaseRunParams,
275+runId: "run-stale-session-assistant-non-timeout",
276+config: makeCrossProviderFallbackConfig(),
277+});
278+279+expect(mockedIsFailoverAssistantError).toHaveBeenCalledWith(undefined);
280+expect(getLastFormattedAssistant()).toBeUndefined();
156281});
157282});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。