























@@ -2,7 +2,12 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core";
22import type { ImageContent } from "@mariozechner/pi-ai";
33import { describe, expect, it } from "vitest";
44import { castAgentMessage } from "../../test-helpers/agent-message-fixtures.js";
5-import { PRUNED_HISTORY_IMAGE_MARKER, pruneProcessedHistoryImages } from "./history-image-prune.js";
5+import {
6+PRUNED_HISTORY_IMAGE_MARKER,
7+PRUNED_HISTORY_MEDIA_REFERENCE_MARKER,
8+installHistoryImagePruneContextTransform,
9+pruneProcessedHistoryImages,
10+} from "./history-image-prune.js";
611712function expectArrayMessageContent(
813message: AgentMessage | undefined,
@@ -18,22 +23,37 @@ function expectPrunedImageMessage(
1823messages: AgentMessage[],
1924errorMessage: string,
2025): Array<{ type: string; text?: string; data?: string }> {
21-const didMutate = pruneProcessedHistoryImages(messages);
22-expect(didMutate).toBe(true);
23-const content = expectArrayMessageContent(messages[0], errorMessage);
26+const pruned = pruneProcessedHistoryImages(messages);
27+expect(pruned).not.toBeNull();
28+expect(pruned).not.toBe(messages);
29+const content = expectArrayMessageContent(pruned?.[0], errorMessage);
2430expect(content).toHaveLength(2);
2531expect(content[1]).toMatchObject({ type: "text", text: PRUNED_HISTORY_IMAGE_MARKER });
2632return content;
2733}
28342935function expectImageMessagePreserved(messages: AgentMessage[], errorMessage: string) {
30-const didMutate = pruneProcessedHistoryImages(messages);
36+const pruned = pruneProcessedHistoryImages(messages);
313732-expect(didMutate).toBe(false);
38+expect(pruned).toBeNull();
3339const content = expectArrayMessageContent(messages[0], errorMessage);
3440expect(content[1]).toMatchObject({ type: "image", data: "abc" });
3541}
364243+function oldEnoughTail(): AgentMessage[] {
44+const assistantTurn = () => castAgentMessage({ role: "assistant", content: "ack" });
45+const userText = () => castAgentMessage({ role: "user", content: "more" });
46+return [
47+assistantTurn(),
48+userText(),
49+assistantTurn(),
50+userText(),
51+assistantTurn(),
52+userText(),
53+assistantTurn(),
54+];
55+}
56+3757describe("pruneProcessedHistoryImages", () => {
3858const image: ImageContent = { type: "image", data: "abc", mimeType: "image/png" };
3959const assistantTurn = () => castAgentMessage({ role: "assistant", content: "ack" });
@@ -58,6 +78,90 @@ describe("pruneProcessedHistoryImages", () => {
5878expect(content[0]?.type).toBe("text");
5979});
608081+it("scrubs old media attachment markers from text blocks", () => {
82+const messages: AgentMessage[] = [
83+castAgentMessage({
84+role: "user",
85+content: [
86+{
87+type: "text",
88+text: [
89+"old image",
90+"[media attached: media://inbound/old.png]",
91+"[media attached 1/2: /tmp/old photo.jpeg (image/jpeg) | https://example.test/img]",
92+"[Image: source: /Users/me/Pictures/old.jpg]",
93+].join("\n"),
94+},
95+{ ...image },
96+],
97+}),
98+ ...oldEnoughTail(),
99+];
100+101+const pruned = pruneProcessedHistoryImages(messages);
102+103+expect(pruned).not.toBeNull();
104+const content = expectArrayMessageContent(pruned?.[0], "expected user array content");
105+expect(content[0]?.text).toBe(
106+[
107+"old image",
108+PRUNED_HISTORY_MEDIA_REFERENCE_MARKER,
109+PRUNED_HISTORY_MEDIA_REFERENCE_MARKER,
110+PRUNED_HISTORY_MEDIA_REFERENCE_MARKER,
111+].join("\n"),
112+);
113+expect(content[1]).toMatchObject({ type: "text", text: PRUNED_HISTORY_IMAGE_MARKER });
114+const originalContent = expectArrayMessageContent(
115+messages[0],
116+"expected original user content",
117+);
118+expect(originalContent[0]?.text).toContain("[media attached: media://inbound/old.png]");
119+expect(originalContent[1]).toMatchObject({ type: "image", data: "abc" });
120+});
121+122+it("scrubs old media attachment markers from string content without image blocks", () => {
123+const messages: AgentMessage[] = [
124+castAgentMessage({
125+role: "user",
126+content: "please remember [media attached: media://inbound/stale-image.png]",
127+}),
128+ ...oldEnoughTail(),
129+];
130+131+const pruned = pruneProcessedHistoryImages(messages);
132+133+expect(pruned).not.toBeNull();
134+const firstUser = pruned?.[0] as Extract<AgentMessage, { role: "user" }> | undefined;
135+expect(firstUser?.content).toBe(`please remember ${PRUNED_HISTORY_MEDIA_REFERENCE_MARKER}`);
136+const originalUser = messages[0] as Extract<AgentMessage, { role: "user" }> | undefined;
137+expect(originalUser?.content).toBe(
138+"please remember [media attached: media://inbound/stale-image.png]",
139+);
140+});
141+142+it("scrubs bare old inbound media URIs from tool results", () => {
143+const messages: AgentMessage[] = [
144+castAgentMessage({
145+role: "toolResult",
146+toolName: "memory_search",
147+content: "previous media://inbound/stale-screenshot.png result",
148+}),
149+ ...oldEnoughTail(),
150+];
151+152+const pruned = pruneProcessedHistoryImages(messages);
153+154+expect(pruned).not.toBeNull();
155+const toolResult = pruned?.[0] as Extract<AgentMessage, { role: "toolResult" }> | undefined;
156+expect(toolResult?.content).toBe(`previous ${PRUNED_HISTORY_MEDIA_REFERENCE_MARKER} result`);
157+const originalToolResult = messages[0] as
158+| Extract<AgentMessage, { role: "toolResult" }>
159+| undefined;
160+expect(originalToolResult?.content).toBe(
161+"previous media://inbound/stale-screenshot.png result",
162+);
163+});
164+61165it("keeps image blocks that belong to the third-most-recent assistant turn", () => {
62166const messages: AgentMessage[] = [
63167castAgentMessage({
@@ -74,6 +178,33 @@ describe("pruneProcessedHistoryImages", () => {
74178expectImageMessagePreserved(messages, "expected user array content");
75179});
76180181+it("preserves recent media attachment markers", () => {
182+const messages: AgentMessage[] = [
183+castAgentMessage({
184+role: "user",
185+content: [
186+{
187+type: "text",
188+text: "recent [media attached: media://inbound/current.png]",
189+},
190+{ ...image },
191+],
192+}),
193+assistantTurn(),
194+userText(),
195+assistantTurn(),
196+userText(),
197+assistantTurn(),
198+];
199+200+const pruned = pruneProcessedHistoryImages(messages);
201+202+expect(pruned).toBeNull();
203+const content = expectArrayMessageContent(messages[0], "expected user array content");
204+expect(content[0]?.text).toBe("recent [media attached: media://inbound/current.png]");
205+expect(content[1]).toMatchObject({ type: "image", data: "abc" });
206+});
207+77208it("does not count multiple assistant messages from one tool loop as separate turns", () => {
78209const messages: AgentMessage[] = [
79210castAgentMessage({
@@ -108,9 +239,9 @@ describe("pruneProcessedHistoryImages", () => {
108239}),
109240];
110241111-const didMutate = pruneProcessedHistoryImages(messages);
242+const pruned = pruneProcessedHistoryImages(messages);
112243113-expect(didMutate).toBe(false);
244+expect(pruned).toBeNull();
114245const content = expectArrayMessageContent(messages[0], "expected user array content");
115246expect(content).toHaveLength(2);
116247expect(content[1]).toMatchObject({ type: "image", data: "abc" });
@@ -153,14 +284,20 @@ describe("pruneProcessedHistoryImages", () => {
153284assistantTurn(),
154285];
155286156-const didMutate = pruneProcessedHistoryImages(messages);
157-expect(didMutate).toBe(true);
287+const pruned = pruneProcessedHistoryImages(messages);
288+expect(pruned).not.toBeNull();
158289159-const oldContent = expectArrayMessageContent(messages[0], "expected old user content");
290+const oldContent = expectArrayMessageContent(pruned?.[0], "expected old user content");
160291expect(oldContent[1]).toMatchObject({ type: "text", text: PRUNED_HISTORY_IMAGE_MARKER });
161292162-const recentContent = expectArrayMessageContent(messages[6], "expected recent user content");
293+const recentContent = expectArrayMessageContent(pruned?.[6], "expected recent user content");
163294expect(recentContent[1]).toMatchObject({ type: "image", data: "abc" });
295+296+const originalOldContent = expectArrayMessageContent(
297+messages[0],
298+"expected original old user content",
299+);
300+expect(originalOldContent[1]).toMatchObject({ type: "image", data: "abc" });
164301});
165302166303it("does not change messages when no assistant turn exists", () => {
@@ -171,10 +308,65 @@ describe("pruneProcessedHistoryImages", () => {
171308}),
172309];
173310174-const didMutate = pruneProcessedHistoryImages(messages);
311+const pruned = pruneProcessedHistoryImages(messages);
175312176-expect(didMutate).toBe(false);
313+expect(pruned).toBeNull();
177314const firstUser = messages[0] as Extract<AgentMessage, { role: "user" }> | undefined;
178315expect(firstUser?.content).toBe("noop");
179316});
180317});
318+319+describe("installHistoryImagePruneContextTransform", () => {
320+const image: ImageContent = { type: "image", data: "abc", mimeType: "image/png" };
321+322+it("prunes the provider replay view after an existing context transform", async () => {
323+const messages: AgentMessage[] = [
324+castAgentMessage({ role: "user", content: "fresh prompt" }),
325+ ...oldEnoughTail(),
326+];
327+const transformedMessages: AgentMessage[] = [
328+castAgentMessage({
329+role: "user",
330+content: [
331+{
332+type: "text",
333+text: "stale [media attached: media://inbound/old.png]",
334+},
335+{ ...image },
336+],
337+}),
338+ ...oldEnoughTail(),
339+];
340+const originalTransformContext = async (
341+inputMessages: AgentMessage[],
342+_signal?: AbortSignal,
343+) => {
344+expect(inputMessages).toBe(messages);
345+return transformedMessages;
346+};
347+const agent = { transformContext: originalTransformContext };
348+349+const restore = installHistoryImagePruneContextTransform(agent);
350+const replayMessages = await agent.transformContext(messages, new AbortController().signal);
351+352+expect(replayMessages).not.toBe(transformedMessages);
353+const replayContent = expectArrayMessageContent(
354+replayMessages[0],
355+"expected replay user array content",
356+);
357+expect(replayContent[0]?.text).toBe(`stale ${PRUNED_HISTORY_MEDIA_REFERENCE_MARKER}`);
358+expect(replayContent[1]).toMatchObject({
359+type: "text",
360+text: PRUNED_HISTORY_IMAGE_MARKER,
361+});
362+const originalContent = expectArrayMessageContent(
363+transformedMessages[0],
364+"expected original transformed content",
365+);
366+expect(originalContent[0]?.text).toContain("media://inbound/old.png");
367+expect(originalContent[1]).toMatchObject({ type: "image", data: "abc" });
368+369+restore();
370+expect(agent.transformContext).toBe(originalTransformContext);
371+});
372+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。