fix(agent): sanitize invalid tool call ids in context

This commit is contained in:
Jiang Bohan 2026-02-10 19:33:44 +08:00
parent 9d719c66af
commit e2d4803f8b
3 changed files with 51 additions and 1 deletions

View file

@ -38,6 +38,10 @@ import {
type SystemPromptMode,
} from "./system-prompt/index.js";
import type { AuthProfileFailureReason } from "./auth-profiles/index.js";
import {
sanitizeToolCallInputs,
sanitizeToolUseResultPairing,
} from "./session/session-transcript-repair.js";
// ============================================================
// Error classification for auth profile rotation
@ -181,6 +185,10 @@ export class Agent {
}
return this.currentApiKey;
},
transformContext: async (messages) => {
const sanitizedInputs = sanitizeToolCallInputs(messages);
return sanitizeToolUseResultPairing(sanitizedInputs);
},
});
// Load Agent Profile (if profileId is specified)

View file

@ -112,6 +112,28 @@ describe("sanitizeToolUseResultPairing", () => {
expect(out.some((m) => m.role === "toolResult")).toBe(false);
expect(out.map((m) => m.role)).toEqual(["user", "assistant"]);
});
it("drops tool results with empty tool call id", () => {
const input = [
{
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
},
{
role: "toolResult",
toolCallId: "",
toolName: "read",
content: [{ type: "text", text: "invalid id" }],
isError: true,
},
{ role: "user", content: "next" },
] as AgentMessage[];
const out = sanitizeToolUseResultPairing(input);
const toolResults = out.filter((m) => m.role === "toolResult") as Array<{ toolCallId?: string }>;
expect(toolResults).toHaveLength(1);
expect(toolResults[0]?.toolCallId).toBe("call_1");
});
});
describe("sanitizeToolCallInputs", () => {
@ -147,4 +169,20 @@ describe("sanitizeToolCallInputs", () => {
: [];
expect(types).toEqual(["text", "toolUse"]);
});
it("drops tool calls with empty id even when input exists", () => {
const input = [
{
role: "assistant",
content: [
{ type: "toolCall", id: "", name: "read", arguments: { path: "a" } },
{ type: "toolUse", id: " ", name: "exec", input: { cmd: "pwd" } },
],
},
{ role: "user", content: "hello" },
] as AgentMessage[];
const out = sanitizeToolCallInputs(input);
expect(out.map((m) => m.role)).toEqual(["user"]);
});
});

View file

@ -58,6 +58,10 @@ function hasToolCallInput(block: ToolCallBlock): boolean {
return hasInput || hasArguments;
}
function hasValidToolCallId(block: ToolCallBlock): boolean {
return typeof block.id === "string" && block.id.trim().length > 0;
}
function extractToolResultId(msg: Extract<AgentMessage, { role: "toolResult" }>): string | null {
const toolCallId = (msg as { toolCallId?: unknown }).toolCallId;
if (typeof toolCallId === "string" && toolCallId) {
@ -118,7 +122,7 @@ export function repairToolCallInputs(messages: AgentMessage[]): ToolCallInputRep
let droppedInMessage = 0;
for (const block of msg.content) {
if (isToolCallBlock(block) && !hasToolCallInput(block)) {
if (isToolCallBlock(block) && (!hasToolCallInput(block) || !hasValidToolCallId(block))) {
droppedToolCalls += 1;
droppedInMessage += 1;
changed = true;