diff --git a/packages/coding-agent/src/session/provider-image-budget.ts b/packages/coding-agent/src/session/provider-image-budget.ts index 1840ac4a9..ade57c48c 100644 --- a/packages/coding-agent/src/session/provider-image-budget.ts +++ b/packages/coding-agent/src/session/provider-image-budget.ts @@ -9,6 +9,11 @@ import type { } from "@oh-my-pi/pi-ai"; import { providerImageBudget } from "@oh-my-pi/snapcompact"; +const TOOL_RESULT_IMAGE_OMISSION: TextContent = { + type: "text", + text: "[image omitted: provider image limit]", +}; + function countImages(context: Context): number { let count = 0; for (const message of context.messages) { @@ -52,7 +57,8 @@ function clampDeveloperMessage(message: DeveloperMessage, state: { remainingDrop function clampToolResultMessage(message: ToolResultMessage, state: { remainingDrops: number }): ToolResultMessage { if (state.remainingDrops <= 0) return message; const content = clampContent(message.content, state); - return content ? { ...message, content } : message; + if (!content) return message; + return { ...message, content: content.length > 0 ? content : [TOOL_RESULT_IMAGE_OMISSION] }; } /** Drops oldest transient image blocks so outgoing vision requests fit the active provider's image cap. */ diff --git a/packages/coding-agent/test/session/provider-image-budget.test.ts b/packages/coding-agent/test/session/provider-image-budget.test.ts index c50cef215..eefafd2b6 100644 --- a/packages/coding-agent/test/session/provider-image-budget.test.ts +++ b/packages/coding-agent/test/session/provider-image-budget.test.ts @@ -69,6 +69,28 @@ describe("provider context image budgets", () => { expect(imageData(context)).toEqual(Array.from({ length: 31 }, (_, index) => `image-${index}`)); }); + it("keeps image-only tool results meaningful when every image block is dropped", () => { + const context: Context = { + systemPrompt: [], + tools: [], + messages: Array.from({ length: 11 }, (_, index) => ({ + role: "toolResult", + toolCallId: `call-${index}`, + toolName: "inspect_image", + content: [image(`image-${index}`)], + isError: false, + timestamp: index, + })), + }; + + const clamped = clampProviderContextImages(context, UMANS_MODEL); + const firstMessage = clamped.messages[0]; + + expect(imageData(clamped)).toEqual(Array.from({ length: 10 }, (_, index) => `image-${index + 1}`)); + expect(firstMessage?.role).toBe("toolResult"); + expect(firstMessage?.content).toEqual([text("[image omitted: provider image limit]")]); + }); + it("preserves context identity when the provider cap is not exceeded", () => { const context: Context = { systemPrompt: [],