fix(agent): clamped provider context images
Dropped oldest outgoing image blocks above the active provider budget so umans requests honor the shipped 10-image cap even when snapcompact is disabled. Added regression coverage for preserving text and newest images.\n\nFixes #3230
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import type {
|
||||
Context,
|
||||
DeveloperMessage,
|
||||
ImageContent,
|
||||
Model,
|
||||
TextContent,
|
||||
ToolResultMessage,
|
||||
UserMessage,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { providerImageBudget } from "@oh-my-pi/snapcompact";
|
||||
|
||||
function countImages(context: Context): number {
|
||||
let count = 0;
|
||||
for (const message of context.messages) {
|
||||
if (!Array.isArray(message.content)) continue;
|
||||
for (const part of message.content) {
|
||||
if (part.type === "image") count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function clampContent(
|
||||
content: readonly (TextContent | ImageContent)[],
|
||||
state: { remainingDrops: number },
|
||||
): (TextContent | ImageContent)[] | undefined {
|
||||
let changed = false;
|
||||
const clamped: (TextContent | ImageContent)[] = [];
|
||||
for (const part of content) {
|
||||
if (part.type === "image" && state.remainingDrops > 0) {
|
||||
state.remainingDrops--;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
clamped.push(part);
|
||||
}
|
||||
return changed ? clamped : undefined;
|
||||
}
|
||||
|
||||
function clampUserMessage(message: UserMessage, state: { remainingDrops: number }): UserMessage {
|
||||
if (!Array.isArray(message.content) || state.remainingDrops <= 0) return message;
|
||||
const content = clampContent(message.content, state);
|
||||
return content ? { ...message, content } : message;
|
||||
}
|
||||
|
||||
function clampDeveloperMessage(message: DeveloperMessage, state: { remainingDrops: number }): DeveloperMessage {
|
||||
if (!Array.isArray(message.content) || state.remainingDrops <= 0) return message;
|
||||
const content = clampContent(message.content, state);
|
||||
return content ? { ...message, content } : message;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/** Drops oldest transient image blocks so outgoing vision requests fit the active provider's image cap. */
|
||||
export function clampProviderContextImages(context: Context, model: Model): Context {
|
||||
if (!model.input.includes("image")) return context;
|
||||
const limit = providerImageBudget(model.provider);
|
||||
const totalImages = countImages(context);
|
||||
if (totalImages <= limit) return context;
|
||||
|
||||
const state = { remainingDrops: totalImages - limit };
|
||||
const messages = context.messages.map(message => {
|
||||
switch (message.role) {
|
||||
case "user":
|
||||
return clampUserMessage(message, state);
|
||||
case "developer":
|
||||
return clampDeveloperMessage(message, state);
|
||||
case "toolResult":
|
||||
return clampToolResultMessage(message, state);
|
||||
case "assistant":
|
||||
return message;
|
||||
}
|
||||
return message;
|
||||
});
|
||||
return { ...context, messages };
|
||||
}
|
||||
Reference in New Issue
Block a user