diff --git a/src/agents/pi-embedded-helpers.validate-turns.test.ts b/src/agents/pi-embedded-helpers.validate-turns.test.ts index 9e20ad0d48f..3847d5322dd 100644 --- a/src/agents/pi-embedded-helpers.validate-turns.test.ts +++ b/src/agents/pi-embedded-helpers.validate-turns.test.ts @@ -357,6 +357,26 @@ describe("mergeConsecutiveUserTurns", () => { expect(merged.timestamp).toBe(2000); }); + it("preserves string content while merging content", () => { + const previous = { + role: "user", + content: "before", + timestamp: 1000, + } as Extract; + const current = { + role: "user", + content: "after", + timestamp: 2000, + } as Extract; + + const merged = mergeConsecutiveUserTurns(previous, current); + + expect(merged.content).toEqual([ + { type: "text", text: "before" }, + { type: "text", text: "after" }, + ]); + }); + it("backfills timestamp from earlier message when missing", () => { const previous = { role: "user", diff --git a/src/agents/pi-embedded-helpers/turns.ts b/src/agents/pi-embedded-helpers/turns.ts index c64efbe4ff9..5c3871f4984 100644 --- a/src/agents/pi-embedded-helpers/turns.ts +++ b/src/agents/pi-embedded-helpers/turns.ts @@ -10,6 +10,10 @@ type AnthropicContentBlock = { toolUseId?: string; toolCallId?: string; }; +type UserContentBlock = Extract< + Extract["content"], + readonly unknown[] +>[number]; function isToolCallBlock(block: AnthropicContentBlock): boolean { return block.type === "toolUse" || block.type === "toolCall" || block.type === "functionCall"; @@ -350,8 +354,8 @@ export function mergeConsecutiveUserTurns( current: Extract, ): Extract { const mergedContent = [ - ...(Array.isArray(previous.content) ? previous.content : []), - ...(Array.isArray(current.content) ? current.content : []), + ...normalizeUserContentForMerge(previous.content), + ...normalizeUserContentForMerge(current.content), ]; return { @@ -361,6 +365,16 @@ export function mergeConsecutiveUserTurns( }; } +function normalizeUserContentForMerge(content: unknown): UserContentBlock[] { + if (Array.isArray(content)) { + return content as UserContentBlock[]; + } + if (typeof content === "string") { + return [{ type: "text", text: content }]; + } + return []; +} + /** * Validates and fixes conversation turn sequences for Anthropic API. * Anthropic requires strict alternating user→assistant pattern. diff --git a/src/agents/pi-embedded-runner.sanitize-session-history.test.ts b/src/agents/pi-embedded-runner.sanitize-session-history.test.ts index 85d8a6e3309..1e0c6653360 100644 --- a/src/agents/pi-embedded-runner.sanitize-session-history.test.ts +++ b/src/agents/pi-embedded-runner.sanitize-session-history.test.ts @@ -1141,17 +1141,9 @@ describe("sanitizeSessionHistory", () => { "```", ].join("\n"); const messages = castAgentMessages([ - { - role: "user", - content: [{ type: "text", text: "First" }], - timestamp: nextTimestamp(), - }, + makeUserMessage("First"), makeAssistantMessage([{ type: "text", text: metadataOnlyText }]), - { - role: "user", - content: [{ type: "text", text: "Second" }], - timestamp: nextTimestamp(), - }, + makeUserMessage("Second"), ]); const sanitized = await sanitizeSessionHistory({