fix(btw): strip replayed tool calls from side-question context

This commit is contained in:
Nimrod Gutman
2026-04-10 12:48:30 +03:00
parent 2138273d63
commit cc5cb496ad
2 changed files with 98 additions and 3 deletions
+60
View File
@@ -581,4 +581,64 @@ describe("runBtwSideQuestion", () => {
expect.arrayContaining([expect.objectContaining({ role: "toolResult" })]),
);
});
it("strips assistant tool calls from BTW context so no-tool side questions stay tool-free", async () => {
getActiveEmbeddedRunSnapshotMock.mockReturnValue({
transcriptLeafId: "assistant-1",
messages: [
{
role: "user",
content: [{ type: "text", text: "seed" }],
timestamp: 1,
},
{
role: "assistant",
content: [
{ type: "text", text: "Let me check." },
{ type: "toolCall", id: "call_1", name: "read", arguments: { path: "README.md" } },
{ type: "toolUse", id: "call_legacy", name: "read", input: { path: "README.md" } },
],
provider: DEFAULT_PROVIDER,
api: "anthropic-messages",
model: DEFAULT_MODEL,
stopReason: "toolUse",
usage: {
input: 1,
output: 2,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 3,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
timestamp: 2,
},
],
});
mockDoneAnswer(MATH_ANSWER);
await runMathSideQuestion();
const [, context] = streamSimpleMock.mock.calls[0] ?? [];
expect(context).toMatchObject({
messages: [
expect.objectContaining({ role: "user" }),
expect.objectContaining({
role: "assistant",
content: [{ type: "text", text: "Let me check." }],
}),
expect.objectContaining({ role: "user" }),
],
});
expect(
(context as { messages?: Array<{ role?: string; content?: Array<{ type?: string }> }> })
.messages,
).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
role: "assistant",
content: expect.arrayContaining([expect.objectContaining({ type: "toolCall" })]),
}),
]),
);
});
});
+38 -3
View File
@@ -83,13 +83,48 @@ function buildBtwQuestionPrompt(question: string, inFlightPrompt?: string): stri
return lines.join("\n");
}
const BTW_TOOL_BLOCK_TYPES = new Set(["toolCall", "toolUse", "functionCall"]);
function sanitizeBtwAssistantMessage(
message: Extract<Message, { role: "assistant" }>,
): Extract<Message, { role: "assistant" }> | undefined {
const originalContent = Array.isArray(message.content) ? message.content : [];
const content = originalContent.filter((block) => {
if (!block || typeof block !== "object") {
return true;
}
return !BTW_TOOL_BLOCK_TYPES.has((block as { type?: unknown }).type as string);
});
if (content.length === originalContent.length) {
return message;
}
if (content.length === 0) {
return undefined;
}
return {
...message,
content,
};
}
function toSimpleContextMessages(messages: unknown[]): Message[] {
const contextMessages = messages.filter((message): message is Message => {
const contextMessages = messages.flatMap((message): Message[] => {
if (!message || typeof message !== "object") {
return false;
return [];
}
const role = (message as { role?: unknown }).role;
return role === "user" || role === "assistant";
if (role === "user") {
return [message as Extract<Message, { role: "user" }>];
}
if (role !== "assistant") {
return [];
}
// BTW is a no-tools path, so strip replay-only tool calls from assistant
// context before handing history to strict providers like Bedrock.
const sanitizedMessage = sanitizeBtwAssistantMessage(
message as Extract<Message, { role: "assistant" }>,
);
return sanitizedMessage ? [sanitizedMessage] : [];
});
return stripToolResultDetails(
contextMessages as Parameters<typeof stripToolResultDetails>[0],