fix(agents): preserve string user content when merging turns

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
clawsweeper[bot]
2026-04-30 01:27:57 -07:00
committed by GitHub
parent e20147a1b6
commit 9061d1e4c3
3 changed files with 38 additions and 12 deletions

View File

@@ -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<AgentMessage, { role: "user" }>;
const current = {
role: "user",
content: "after",
timestamp: 2000,
} as Extract<AgentMessage, { role: "user" }>;
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",

View File

@@ -10,6 +10,10 @@ type AnthropicContentBlock = {
toolUseId?: string;
toolCallId?: string;
};
type UserContentBlock = Extract<
Extract<AgentMessage, { role: "user" }>["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<AgentMessage, { role: "user" }>,
): Extract<AgentMessage, { role: "user" }> {
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.

View File

@@ -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({