fix(combo/fusion): flatten Anthropic-style tool messages in panel calls
flattenToolHistory only recognized OpenAI-style tool calls. Anthropic-compatible clients (Claude Code, /v1/messages) send tool invocations and results as tool_use/tool_result blocks inside the message content array. Since panel calls strip the tools definitions (#1859), the panel expert models received structured tool history without schemas, causing them to fail or misbehave (leading to empty responses and 503 errors). Extend flattenToolHistory to recognize and flatten tool_use and tool_result blocks in the content array into prose text, keeping panel expert execution robust and independent of the client API format. PR decolua/9router#1910 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -31,6 +31,29 @@ function flattenToolHistory(messages) {
|
||||
const base = extractTextContent(rest.content) || (typeof rest.content === "string" ? rest.content : "");
|
||||
return { ...rest, content: `${base}${base ? "\n" : ""}${TOOL_CALL_PREFIX}${names}]` };
|
||||
}
|
||||
if (Array.isArray(msg.content)) {
|
||||
const hasToolUse = msg.content.some((c) => c.type === "tool_use");
|
||||
const hasToolResult = msg.content.some((c) => c.type === "tool_result");
|
||||
if (hasToolUse || hasToolResult) {
|
||||
const textParts = [];
|
||||
const toolNames = [];
|
||||
const toolResults = [];
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "text" && block.text) textParts.push(block.text);
|
||||
if (block.type === "tool_use") toolNames.push(block.name || "tool");
|
||||
if (block.type === "tool_result") toolResults.push(extractTextContent(block.content) || String(block.content ?? ""));
|
||||
}
|
||||
const { ...rest } = msg;
|
||||
let newContent = textParts.join("\n");
|
||||
if (toolNames.length > 0) {
|
||||
newContent = `${newContent}${newContent ? "\n" : ""}${TOOL_CALL_PREFIX}${toolNames.join(", ")}]`;
|
||||
}
|
||||
if (toolResults.length > 0) {
|
||||
newContent = `${newContent}${newContent ? "\n" : ""}${TOOL_RESULT_PREFIX}${toolResults.join("\n")}]`;
|
||||
}
|
||||
return { ...rest, content: newContent };
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -182,4 +182,35 @@ describe("fusion combo", () => {
|
||||
expect(judgeBody.messages[1].tool_calls).toBeDefined();
|
||||
expect(judgeBody.messages[2].role).toBe("tool");
|
||||
});
|
||||
|
||||
it("flattens Anthropic-style tool_use and tool_result blocks in arrays", async () => {
|
||||
const handleSingleModel = vi.fn(async () => okResponse("ans"));
|
||||
await handleFusionChat({
|
||||
body: {
|
||||
messages: [
|
||||
{ role: "user", content: "do it" },
|
||||
{ role: "assistant", content: [{ type: "text", text: "ok" }, { type: "tool_use", id: "t1", name: "run" }] },
|
||||
{ role: "user", content: [{ type: "tool_result", tool_use_id: "t1", content: "done" }] }
|
||||
],
|
||||
tools: [{ name: "run", description: "d" }]
|
||||
},
|
||||
models: ["p/a", "p/b"],
|
||||
handleSingleModel,
|
||||
log,
|
||||
judgeModel: "p/judge"
|
||||
});
|
||||
|
||||
const panelCalls = handleSingleModel.mock.calls.filter(([,, isPanel]) => isPanel === true);
|
||||
expect(panelCalls.length).toBe(2);
|
||||
const panelBody = panelCalls[0][0];
|
||||
|
||||
expect(panelBody.tools).toBeUndefined();
|
||||
expect(panelBody.messages.length).toBe(3);
|
||||
|
||||
// Flattened tool_use
|
||||
expect(panelBody.messages[1].content).toBe("ok\n[Called tools: run]");
|
||||
|
||||
// Flattened tool_result
|
||||
expect(panelBody.messages[2].content).toBe("[Tool result: done]");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user