



























@@ -49,10 +49,23 @@ vi.mock("../../agents/bootstrap-budget.js", () => ({
49495050vi.mock("../../agents/pi-embedded-helpers.js", () => ({
5151BILLING_ERROR_USER_MESSAGE: "billing",
52+formatRateLimitOrOverloadedErrorCopy: (message: string) => {
53+if (/model\s+(?:is\s+)?at capacity/i.test(message)) {
54+return "⚠️ Selected model is at capacity. Try a different model, or wait and retry.";
55+}
56+if (/rate.limit|too many requests|429/i.test(message)) {
57+return "⚠️ API rate limit reached. Please try again later.";
58+}
59+if (/overloaded/i.test(message)) {
60+return "The AI service is temporarily overloaded. Please try again in a moment.";
61+}
62+return undefined;
63+},
5264isCompactionFailureError: () => false,
5365isContextOverflowError: () => false,
5466isBillingErrorMessage: () => false,
5567isLikelyContextOverflowError: () => false,
68+isOverloadedErrorMessage: (message: string) => /overloaded|capacity/i.test(message),
5669isRateLimitErrorMessage: () => false,
5770isTransientHttpError: () => false,
5871sanitizeUserFacingText: (text?: string) => text ?? "",
@@ -410,6 +423,95 @@ describe("runAgentTurnWithFallback", () => {
410423expect(onToolResult.mock.calls[0]?.[0]?.text).toBeUndefined();
411424});
412425426+it("surfaces model capacity errors from no-text mid-turn failures", async () => {
427+state.runEmbeddedPiAgentMock.mockResolvedValueOnce({
428+payloads: [{ text: "thinking", isReasoning: true }],
429+meta: {
430+error: {
431+kind: "server_overloaded",
432+message: "Selected model is at capacity. Please try a different model.",
433+},
434+},
435+});
436+437+const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
438+const result = await runAgentTurnWithFallback({
439+commandBody: "hello",
440+followupRun: createFollowupRun(),
441+sessionCtx: {
442+Provider: "whatsapp",
443+MessageSid: "msg",
444+} as unknown as TemplateContext,
445+opts: {},
446+typingSignals: createMockTypingSignaler(),
447+blockReplyPipeline: null,
448+blockStreamingEnabled: false,
449+resolvedBlockStreamingBreak: "message_end",
450+applyReplyToMode: (payload) => payload,
451+shouldEmitToolResult: () => true,
452+shouldEmitToolOutput: () => false,
453+pendingToolTasks: new Set(),
454+resetSessionAfterCompactionFailure: async () => false,
455+resetSessionAfterRoleOrderingConflict: async () => false,
456+isHeartbeat: false,
457+sessionKey: "main",
458+getActiveSessionEntry: () => undefined,
459+resolvedVerboseLevel: "off",
460+});
461+462+expect(result.kind).toBe("success");
463+if (result.kind === "success") {
464+expect(result.runResult.payloads).toEqual([
465+{
466+text: "⚠️ Selected model is at capacity. Try a different model, or wait and retry.",
467+isError: true,
468+},
469+]);
470+}
471+});
472+473+it("surfaces model capacity errors from pre-reply CLI failures", async () => {
474+state.runWithModelFallbackMock.mockRejectedValueOnce(
475+new Error("Selected model is at capacity. Please try a different model."),
476+);
477+478+const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();
479+const followupRun = createFollowupRun();
480+followupRun.run.provider = "openai-codex";
481+followupRun.run.model = "gpt-5.5";
482+483+const result = await runAgentTurnWithFallback({
484+commandBody: "hello",
485+ followupRun,
486+sessionCtx: {
487+Provider: "whatsapp",
488+MessageSid: "msg",
489+} as unknown as TemplateContext,
490+opts: {},
491+typingSignals: createMockTypingSignaler(),
492+blockReplyPipeline: null,
493+blockStreamingEnabled: false,
494+resolvedBlockStreamingBreak: "message_end",
495+applyReplyToMode: (payload) => payload,
496+shouldEmitToolResult: () => true,
497+shouldEmitToolOutput: () => false,
498+pendingToolTasks: new Set(),
499+resetSessionAfterCompactionFailure: async () => false,
500+resetSessionAfterRoleOrderingConflict: async () => false,
501+isHeartbeat: false,
502+sessionKey: "main",
503+getActiveSessionEntry: () => undefined,
504+resolvedVerboseLevel: "off",
505+});
506+507+expect(result).toEqual({
508+kind: "final",
509+payload: {
510+text: "⚠️ Selected model is at capacity. Try a different model, or wait and retry.",
511+},
512+});
513+});
514+413515it("strips a glued leading NO_REPLY token from streamed tool results", async () => {
414516const onToolResult = vi.fn();
415517state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。