fix(kiro): merge adjacent user history turns after role normalization (#2105)

Merged automatically
This commit is contained in:
Gioxa
2026-05-10 10:00:08 +07:00
committed by GitHub
parent 7f0da3d0b2
commit 149d13cb9c
2 changed files with 79 additions and 1 deletions

View File

@@ -288,7 +288,50 @@ function convertMessages(messages, tools, model) {
}
});
return { history, currentMessage };
// Kiro expects history to alternate between user and assistant turns. After
// normalizing `system`/`tool` roles into `userInputMessage`, the history can
// contain adjacent user turns, which Kiro can reject. Merge consecutive
// `userInputMessage` entries by concatenating their content and preserving
// any attached `userInputMessageContext` (e.g. accumulated toolResults).
//
// Why this is not redundant with the `flushPending` grouping in the main
// loop: the assistant branch resets `currentRole = null` after emitting
// `toolUses`. Any following `tool` role (normalized to user) and a
// subsequent `user` role therefore each open their own flush, producing
// two adjacent `userInputMessage` entries in history. This pass collapses
// those.
const mergedHistory: typeof history = [];
for (const item of history) {
const previous = mergedHistory[mergedHistory.length - 1];
if (item.userInputMessage && previous?.userInputMessage) {
const previousContent = previous.userInputMessage.content || "";
const currentContent = item.userInputMessage.content || "";
previous.userInputMessage.content = previousContent
? `${previousContent}\n\n${currentContent}`
: currentContent;
if (item.userInputMessage.userInputMessageContext) {
const previousContext = previous.userInputMessage.userInputMessageContext || {};
const nextContext = item.userInputMessage.userInputMessageContext;
const mergedContext: Record<string, unknown> = { ...previousContext };
for (const [key, value] of Object.entries(nextContext)) {
const existing = (previousContext as Record<string, unknown>)[key];
if (Array.isArray(existing) && Array.isArray(value)) {
mergedContext[key] = [...existing, ...value];
} else {
mergedContext[key] = value;
}
}
previous.userInputMessage.userInputMessageContext = mergedContext;
}
} else {
mergedHistory.push(item);
}
}
return { history: mergedHistory, currentMessage };
}
/**

View File

@@ -254,3 +254,38 @@ test("OpenAI -> Kiro still returns a valid payload for minimal requests", () =>
);
assert.equal(result.conversationState.currentMessage.userInputMessage.modelId, "claude-sonnet-4");
});
test("OpenAI -> Kiro merges adjacent user history turns after role normalization", () => {
const result = buildKiroPayload(
"claude-sonnet-4",
{
messages: [
{ role: "system", content: "System rules" },
{ role: "user", content: "First question" },
{ role: "assistant", content: "Answer 1" },
{ role: "tool", tool_call_id: "call_orphan", content: "tool log" },
{ role: "user", content: "Follow-up" },
],
},
false,
null
);
const history = result.conversationState.history as Array<{
userInputMessage?: { content: string };
assistantResponseMessage?: { content: string };
}>;
for (let i = 1; i < history.length; i++) {
assert.equal(
Boolean(history[i - 1].userInputMessage) && Boolean(history[i].userInputMessage),
false,
"history should not contain adjacent userInputMessage turns"
);
}
const firstUser = history[0].userInputMessage;
assert.ok(firstUser, "first history turn should be a user turn");
assert.equal(firstUser.content, "System rules\n\nFirst question");
assert.equal(history[1].assistantResponseMessage?.content, "Answer 1");
});