From e10e1c895d511796fb795b24a9e992228c0851ad Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:31:10 -0500 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A4=96=20Sanitize=20malformed=20tool?= =?UTF-8?q?=20inputs=20before=20API=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes workspace errors when chat history contains corrupted tool inputs. The httpjail-coder workspace had a message where the AI generated malformed JSON that was stored as a string in the tool input field: input: '{"script" timeout_secs="10": "ls"}' This caused API errors: 'Input should be a valid dictionary' Solution: - Created sanitizeToolInputs() to replace invalid inputs (strings, null, arrays) with empty objects before sending to API - Integrated into AIService message processing pipeline - Original history remains unchanged, only API request is sanitized - Comprehensive test coverage including the actual problematic message Generated with `cmux` --- src/services/aiService.ts | 9 +- src/utils/messages/sanitizeToolInput.test.ts | 292 +++++++++++++++++++ src/utils/messages/sanitizeToolInput.ts | 61 ++++ 3 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 src/utils/messages/sanitizeToolInput.test.ts create mode 100644 src/utils/messages/sanitizeToolInput.ts diff --git a/src/services/aiService.ts b/src/services/aiService.ts index fe15e1ca2..eeca8cc94 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -3,6 +3,7 @@ import * as os from "os"; import { EventEmitter } from "events"; import { convertToModelMessages, type LanguageModel } from "ai"; import { applyToolOutputRedaction } from "@/utils/messages/applyToolOutputRedaction"; +import { sanitizeToolInputs } from "@/utils/messages/sanitizeToolInput"; import type { Result } from "@/types/result"; import { Ok, Err } from "@/types/result"; import type { WorkspaceMetadata } from "@/types/workspace"; @@ -461,10 +462,16 @@ export class AIService extends EventEmitter { const redactedForProvider = applyToolOutputRedaction(messagesWithModeContext); log.debug_obj(`${workspaceId}/2a_redacted_messages.json`, redactedForProvider); + // Sanitize tool inputs to ensure they are valid objects (not strings or arrays) + // This fixes cases where corrupted data in history has malformed tool inputs + // that would cause API errors like "Input should be a valid dictionary" + const sanitizedMessages = sanitizeToolInputs(redactedForProvider); + log.debug_obj(`${workspaceId}/2b_sanitized_messages.json`, sanitizedMessages); + // Convert CmuxMessage to ModelMessage format using Vercel AI SDK utility // Type assertion needed because CmuxMessage has custom tool parts for interrupted tools // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument - const modelMessages = convertToModelMessages(redactedForProvider as any); + const modelMessages = convertToModelMessages(sanitizedMessages as any); log.debug_obj(`${workspaceId}/2_model_messages.json`, modelMessages); // Apply ModelMessage transforms based on provider requirements diff --git a/src/utils/messages/sanitizeToolInput.test.ts b/src/utils/messages/sanitizeToolInput.test.ts new file mode 100644 index 000000000..9007df451 --- /dev/null +++ b/src/utils/messages/sanitizeToolInput.test.ts @@ -0,0 +1,292 @@ +import { describe, it, expect } from "@jest/globals"; +import type { CmuxMessage } from "@/types/message"; +import { sanitizeToolInputs } from "./sanitizeToolInput"; + +describe("sanitizeToolInput", () => { + it("should detect malformed JSON strings in tool input", () => { + // This reproduces the bug from httpjail-coder workspace + // When a tool input is a string instead of an object, the API will reject it + const message: CmuxMessage = { + id: "test-1", + role: "assistant", + parts: [ + { + type: "dynamic-tool", + toolCallId: "toolu_01test", + toolName: "bash", + state: "output-available", + // This is the malformed input from the actual chat - a string instead of object + input: '{"script" timeout_secs="10": "ls"}', + output: { + error: 'Invalid input for tool bash: JSON parsing failed', + }, + }, + ], + metadata: { + timestamp: Date.now(), + historySequence: 1, + }, + }; + + // The input field is a string, not an object + const toolPart = message.parts[0]; + if (toolPart.type === "dynamic-tool") { + expect(typeof toolPart.input).toBe("string"); + } + }); + + it("should detect string input (non-object) in tool calls", () => { + const message: CmuxMessage = { + id: "test-2", + role: "assistant", + parts: [ + { + type: "dynamic-tool", + toolCallId: "toolu_02test", + toolName: "bash", + state: "output-available", + // Input is a string instead of an object + input: "not an object", + output: { + error: "Invalid input", + }, + }, + ], + metadata: { + timestamp: Date.now(), + historySequence: 2, + }, + }; + + // The input field is a string, which would cause API errors + const toolPart = message.parts[0]; + if (toolPart.type === "dynamic-tool") { + expect(typeof toolPart.input).toBe("string"); + } + }); + + it("should handle valid tool input correctly", () => { + const message: CmuxMessage = { + id: "test-3", + role: "assistant", + parts: [ + { + type: "dynamic-tool", + toolCallId: "toolu_03test", + toolName: "bash", + state: "output-available", + // Valid input + input: { + script: "ls", + timeout_secs: 10, + }, + output: { + success: true, + output: "file1.txt\nfile2.txt", + }, + }, + ], + metadata: { + timestamp: Date.now(), + historySequence: 3, + }, + }; + + // Valid object input should pass through unchanged + const toolPart = message.parts[0]; + if (toolPart.type === "dynamic-tool") { + expect(typeof toolPart.input).toBe("object"); + expect(toolPart.input).toEqual({ script: "ls", timeout_secs: 10 }); + } + }); + + describe("sanitizeToolInputs", () => { + it("should handle the actual malformed message from httpjail-coder workspace", () => { + // This is the actual problematic message that caused the bug + const problematicMessage: CmuxMessage = { + id: "assistant-1761527027508-karjrpf3g", + role: "assistant", + metadata: { + historySequence: 1, + timestamp: 1761527027508, + partial: true, + }, + parts: [ + { + type: "text", + text: "I'll explore this repository.", + }, + { + type: "dynamic-tool", + toolCallId: "toolu_01DXeXp8oArG4PzT9rk4hz5c", + toolName: "bash", + state: "output-available", + // THIS IS THE MALFORMED INPUT - string instead of object + input: '{"script" timeout_secs="10": "ls"}', + output: { + error: "Invalid input for tool bash: JSON parsing failed", + }, + }, + ], + }; + + const sanitized = sanitizeToolInputs([problematicMessage]); + const sanitizedTool = sanitized[0].parts[1]; + + if (sanitizedTool.type === "dynamic-tool") { + // Should be converted to empty object + expect(sanitizedTool.input).toEqual({}); + } + }); + + it("should convert string inputs to empty objects", () => { + const messages: CmuxMessage[] = [ + { + id: "test-1", + role: "assistant", + parts: [ + { + type: "dynamic-tool", + toolCallId: "toolu_01test", + toolName: "bash", + state: "output-available", + input: "not an object", + output: { error: "Invalid input" }, + }, + ], + metadata: { timestamp: Date.now(), historySequence: 1 }, + }, + ]; + + const sanitized = sanitizeToolInputs(messages); + expect(sanitized[0].parts[0]).toMatchObject({ + type: "dynamic-tool", + input: {}, // Should be converted to empty object + }); + }); + + it("should keep valid object inputs unchanged", () => { + const messages: CmuxMessage[] = [ + { + id: "test-2", + role: "assistant", + parts: [ + { + type: "dynamic-tool", + toolCallId: "toolu_02test", + toolName: "bash", + state: "output-available", + input: { script: "ls", timeout_secs: 10 }, + output: { success: true }, + }, + ], + metadata: { timestamp: Date.now(), historySequence: 2 }, + }, + ]; + + const sanitized = sanitizeToolInputs(messages); + expect(sanitized[0].parts[0]).toMatchObject({ + type: "dynamic-tool", + input: { script: "ls", timeout_secs: 10 }, + }); + }); + + it("should not modify non-assistant messages", () => { + const messages: CmuxMessage[] = [ + { + id: "test-3", + role: "user", + parts: [{ type: "text", text: "Hello" }], + metadata: { timestamp: Date.now(), historySequence: 3 }, + }, + ]; + + const sanitized = sanitizeToolInputs(messages); + expect(sanitized).toEqual(messages); + }); + + it("should handle messages with multiple parts", () => { + const messages: CmuxMessage[] = [ + { + id: "test-4", + role: "assistant", + parts: [ + { type: "text", text: "Let me run this command" }, + { + type: "dynamic-tool", + toolCallId: "toolu_04test", + toolName: "bash", + state: "output-available", + input: "malformed", + output: { error: "bad" }, + }, + { type: "text", text: "Done" }, + ], + metadata: { timestamp: Date.now(), historySequence: 4 }, + }, + ]; + + const sanitized = sanitizeToolInputs(messages); + expect(sanitized[0].parts[1]).toMatchObject({ + type: "dynamic-tool", + input: {}, + }); + // Other parts should be unchanged + expect(sanitized[0].parts[0]).toEqual({ type: "text", text: "Let me run this command" }); + expect(sanitized[0].parts[2]).toEqual({ type: "text", text: "Done" }); + }); + + it("should handle null input", () => { + const messages: CmuxMessage[] = [ + { + id: "test-null", + role: "assistant", + parts: [ + { + type: "dynamic-tool", + toolCallId: "toolu_null", + toolName: "bash", + state: "output-available", + input: null as any, + output: { error: "Invalid" }, + }, + ], + metadata: { timestamp: Date.now(), historySequence: 1 }, + }, + ]; + + const sanitized = sanitizeToolInputs(messages); + const toolPart = sanitized[0].parts[0]; + if (toolPart.type === "dynamic-tool") { + expect(toolPart.input).toEqual({}); + } + }); + + it("should handle array input", () => { + const messages: CmuxMessage[] = [ + { + id: "test-array", + role: "assistant", + parts: [ + { + type: "dynamic-tool", + toolCallId: "toolu_array", + toolName: "bash", + state: "output-available", + input: ["not", "valid"] as any, + output: { error: "Invalid" }, + }, + ], + metadata: { timestamp: Date.now(), historySequence: 1 }, + }, + ]; + + const sanitized = sanitizeToolInputs(messages); + const toolPart = sanitized[0].parts[0]; + if (toolPart.type === "dynamic-tool") { + expect(toolPart.input).toEqual({}); + } + }); + }); +}); + diff --git a/src/utils/messages/sanitizeToolInput.ts b/src/utils/messages/sanitizeToolInput.ts new file mode 100644 index 000000000..991925d17 --- /dev/null +++ b/src/utils/messages/sanitizeToolInput.ts @@ -0,0 +1,61 @@ +import type { CmuxMessage, CmuxToolPart } from "@/types/message"; + +/** + * Sanitizes tool inputs in messages to ensure they are valid objects. + * + * The Anthropic API (and other LLM APIs) require tool inputs to be objects/dictionaries. + * However, if the model generates malformed JSON or if we have corrupted data in history, + * the input field might be a string instead of an object. + * + * This causes API errors like: "Input should be a valid dictionary" + * + * This function ensures all tool inputs are objects by converting non-object inputs + * to empty objects. This allows the conversation to continue even with corrupted history. + * + * @param messages - Messages to sanitize + * @returns New array with sanitized messages (original messages are not modified) + */ +export function sanitizeToolInputs(messages: CmuxMessage[]): CmuxMessage[] { + return messages.map((msg) => { + // Only process assistant messages with tool parts + if (msg.role !== "assistant") { + return msg; + } + + // Check if any parts need sanitization + const needsSanitization = msg.parts.some( + (part) => + part.type === "dynamic-tool" && + (typeof part.input !== "object" || part.input === null || Array.isArray(part.input)) + ); + + if (!needsSanitization) { + return msg; + } + + // Create new message with sanitized parts + return { + ...msg, + parts: msg.parts.map((part): typeof part => { + if (part.type !== "dynamic-tool") { + return part; + } + + // Sanitize the input if it's not a valid object + if ( + typeof part.input !== "object" || + part.input === null || + Array.isArray(part.input) + ) { + return { + ...part, + input: {}, // Replace with empty object + } as CmuxToolPart; + } + + return part; + }), + }; + }); +} + From 18b78d8833160f574d7d0c5a911b0d14ac7591e8 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:33:21 -0500 Subject: [PATCH 2/3] Fix linting and formatting issues --- src/utils/messages/sanitizeToolInput.test.ts | 7 ++++--- src/utils/messages/sanitizeToolInput.ts | 12 ++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/utils/messages/sanitizeToolInput.test.ts b/src/utils/messages/sanitizeToolInput.test.ts index 9007df451..a4103d0b4 100644 --- a/src/utils/messages/sanitizeToolInput.test.ts +++ b/src/utils/messages/sanitizeToolInput.test.ts @@ -18,7 +18,7 @@ describe("sanitizeToolInput", () => { // This is the malformed input from the actual chat - a string instead of object input: '{"script" timeout_secs="10": "ls"}', output: { - error: 'Invalid input for tool bash: JSON parsing failed', + error: "Invalid input for tool bash: JSON parsing failed", }, }, ], @@ -132,7 +132,7 @@ describe("sanitizeToolInput", () => { const sanitized = sanitizeToolInputs([problematicMessage]); const sanitizedTool = sanitized[0].parts[1]; - + if (sanitizedTool.type === "dynamic-tool") { // Should be converted to empty object expect(sanitizedTool.input).toEqual({}); @@ -247,6 +247,7 @@ describe("sanitizeToolInput", () => { toolCallId: "toolu_null", toolName: "bash", state: "output-available", + // eslint-disable-next-line @typescript-eslint/no-explicit-any input: null as any, output: { error: "Invalid" }, }, @@ -273,6 +274,7 @@ describe("sanitizeToolInput", () => { toolCallId: "toolu_array", toolName: "bash", state: "output-available", + // eslint-disable-next-line @typescript-eslint/no-explicit-any input: ["not", "valid"] as any, output: { error: "Invalid" }, }, @@ -289,4 +291,3 @@ describe("sanitizeToolInput", () => { }); }); }); - diff --git a/src/utils/messages/sanitizeToolInput.ts b/src/utils/messages/sanitizeToolInput.ts index 991925d17..bf9ca48a2 100644 --- a/src/utils/messages/sanitizeToolInput.ts +++ b/src/utils/messages/sanitizeToolInput.ts @@ -42,15 +42,12 @@ export function sanitizeToolInputs(messages: CmuxMessage[]): CmuxMessage[] { } // Sanitize the input if it's not a valid object - if ( - typeof part.input !== "object" || - part.input === null || - Array.isArray(part.input) - ) { - return { + if (typeof part.input !== "object" || part.input === null || Array.isArray(part.input)) { + const sanitized: CmuxToolPart = { ...part, input: {}, // Replace with empty object - } as CmuxToolPart; + }; + return sanitized; } return part; @@ -58,4 +55,3 @@ export function sanitizeToolInputs(messages: CmuxMessage[]): CmuxMessage[] { }; }); } - From 70491a287483ec217b2d2ece8a5dfc822a659c3e Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 26 Oct 2025 20:42:21 -0500 Subject: [PATCH 3/3] Remove useless tests that don't call our code --- src/utils/messages/sanitizeToolInput.test.ts | 391 +++++++------------ 1 file changed, 146 insertions(+), 245 deletions(-) diff --git a/src/utils/messages/sanitizeToolInput.test.ts b/src/utils/messages/sanitizeToolInput.test.ts index a4103d0b4..54b46e62a 100644 --- a/src/utils/messages/sanitizeToolInput.test.ts +++ b/src/utils/messages/sanitizeToolInput.test.ts @@ -2,292 +2,193 @@ import { describe, it, expect } from "@jest/globals"; import type { CmuxMessage } from "@/types/message"; import { sanitizeToolInputs } from "./sanitizeToolInput"; -describe("sanitizeToolInput", () => { - it("should detect malformed JSON strings in tool input", () => { - // This reproduces the bug from httpjail-coder workspace - // When a tool input is a string instead of an object, the API will reject it - const message: CmuxMessage = { - id: "test-1", +describe("sanitizeToolInputs", () => { + it("should handle the actual malformed message from httpjail-coder workspace", () => { + // This is the actual problematic message that caused the bug + const problematicMessage: CmuxMessage = { + id: "assistant-1761527027508-karjrpf3g", role: "assistant", - parts: [ - { - type: "dynamic-tool", - toolCallId: "toolu_01test", - toolName: "bash", - state: "output-available", - // This is the malformed input from the actual chat - a string instead of object - input: '{"script" timeout_secs="10": "ls"}', - output: { - error: "Invalid input for tool bash: JSON parsing failed", - }, - }, - ], metadata: { - timestamp: Date.now(), historySequence: 1, + timestamp: 1761527027508, + partial: true, }, - }; - - // The input field is a string, not an object - const toolPart = message.parts[0]; - if (toolPart.type === "dynamic-tool") { - expect(typeof toolPart.input).toBe("string"); - } - }); - - it("should detect string input (non-object) in tool calls", () => { - const message: CmuxMessage = { - id: "test-2", - role: "assistant", parts: [ { - type: "dynamic-tool", - toolCallId: "toolu_02test", - toolName: "bash", - state: "output-available", - // Input is a string instead of an object - input: "not an object", - output: { - error: "Invalid input", - }, + type: "text", + text: "I'll explore this repository.", }, - ], - metadata: { - timestamp: Date.now(), - historySequence: 2, - }, - }; - - // The input field is a string, which would cause API errors - const toolPart = message.parts[0]; - if (toolPart.type === "dynamic-tool") { - expect(typeof toolPart.input).toBe("string"); - } - }); - - it("should handle valid tool input correctly", () => { - const message: CmuxMessage = { - id: "test-3", - role: "assistant", - parts: [ { type: "dynamic-tool", - toolCallId: "toolu_03test", + toolCallId: "toolu_01DXeXp8oArG4PzT9rk4hz5c", toolName: "bash", state: "output-available", - // Valid input - input: { - script: "ls", - timeout_secs: 10, - }, + // THIS IS THE MALFORMED INPUT - string instead of object + input: '{"script" timeout_secs="10": "ls"}', output: { - success: true, - output: "file1.txt\nfile2.txt", + error: "Invalid input for tool bash: JSON parsing failed", }, }, ], - metadata: { - timestamp: Date.now(), - historySequence: 3, - }, }; - // Valid object input should pass through unchanged - const toolPart = message.parts[0]; - if (toolPart.type === "dynamic-tool") { - expect(typeof toolPart.input).toBe("object"); - expect(toolPart.input).toEqual({ script: "ls", timeout_secs: 10 }); + const sanitized = sanitizeToolInputs([problematicMessage]); + const sanitizedTool = sanitized[0].parts[1]; + + if (sanitizedTool.type === "dynamic-tool") { + // Should be converted to empty object + expect(sanitizedTool.input).toEqual({}); } }); - describe("sanitizeToolInputs", () => { - it("should handle the actual malformed message from httpjail-coder workspace", () => { - // This is the actual problematic message that caused the bug - const problematicMessage: CmuxMessage = { - id: "assistant-1761527027508-karjrpf3g", + it("should convert string inputs to empty objects", () => { + const messages: CmuxMessage[] = [ + { + id: "test-1", role: "assistant", - metadata: { - historySequence: 1, - timestamp: 1761527027508, - partial: true, - }, parts: [ - { - type: "text", - text: "I'll explore this repository.", - }, { type: "dynamic-tool", - toolCallId: "toolu_01DXeXp8oArG4PzT9rk4hz5c", + toolCallId: "toolu_01test", toolName: "bash", state: "output-available", - // THIS IS THE MALFORMED INPUT - string instead of object - input: '{"script" timeout_secs="10": "ls"}', - output: { - error: "Invalid input for tool bash: JSON parsing failed", - }, + input: "not an object", + output: { error: "Invalid input" }, }, ], - }; - - const sanitized = sanitizeToolInputs([problematicMessage]); - const sanitizedTool = sanitized[0].parts[1]; - - if (sanitizedTool.type === "dynamic-tool") { - // Should be converted to empty object - expect(sanitizedTool.input).toEqual({}); - } - }); - - it("should convert string inputs to empty objects", () => { - const messages: CmuxMessage[] = [ - { - id: "test-1", - role: "assistant", - parts: [ - { - type: "dynamic-tool", - toolCallId: "toolu_01test", - toolName: "bash", - state: "output-available", - input: "not an object", - output: { error: "Invalid input" }, - }, - ], - metadata: { timestamp: Date.now(), historySequence: 1 }, - }, - ]; + metadata: { timestamp: Date.now(), historySequence: 1 }, + }, + ]; - const sanitized = sanitizeToolInputs(messages); - expect(sanitized[0].parts[0]).toMatchObject({ - type: "dynamic-tool", - input: {}, // Should be converted to empty object - }); + const sanitized = sanitizeToolInputs(messages); + expect(sanitized[0].parts[0]).toMatchObject({ + type: "dynamic-tool", + input: {}, // Should be converted to empty object }); + }); - it("should keep valid object inputs unchanged", () => { - const messages: CmuxMessage[] = [ - { - id: "test-2", - role: "assistant", - parts: [ - { - type: "dynamic-tool", - toolCallId: "toolu_02test", - toolName: "bash", - state: "output-available", - input: { script: "ls", timeout_secs: 10 }, - output: { success: true }, - }, - ], - metadata: { timestamp: Date.now(), historySequence: 2 }, - }, - ]; + it("should keep valid object inputs unchanged", () => { + const messages: CmuxMessage[] = [ + { + id: "test-2", + role: "assistant", + parts: [ + { + type: "dynamic-tool", + toolCallId: "toolu_02test", + toolName: "bash", + state: "output-available", + input: { script: "ls", timeout_secs: 10 }, + output: { success: true }, + }, + ], + metadata: { timestamp: Date.now(), historySequence: 2 }, + }, + ]; - const sanitized = sanitizeToolInputs(messages); - expect(sanitized[0].parts[0]).toMatchObject({ - type: "dynamic-tool", - input: { script: "ls", timeout_secs: 10 }, - }); + const sanitized = sanitizeToolInputs(messages); + expect(sanitized[0].parts[0]).toMatchObject({ + type: "dynamic-tool", + input: { script: "ls", timeout_secs: 10 }, }); + }); - it("should not modify non-assistant messages", () => { - const messages: CmuxMessage[] = [ - { - id: "test-3", - role: "user", - parts: [{ type: "text", text: "Hello" }], - metadata: { timestamp: Date.now(), historySequence: 3 }, - }, - ]; + it("should not modify non-assistant messages", () => { + const messages: CmuxMessage[] = [ + { + id: "test-3", + role: "user", + parts: [{ type: "text", text: "Hello" }], + metadata: { timestamp: Date.now(), historySequence: 3 }, + }, + ]; - const sanitized = sanitizeToolInputs(messages); - expect(sanitized).toEqual(messages); - }); + const sanitized = sanitizeToolInputs(messages); + expect(sanitized).toEqual(messages); + }); - it("should handle messages with multiple parts", () => { - const messages: CmuxMessage[] = [ - { - id: "test-4", - role: "assistant", - parts: [ - { type: "text", text: "Let me run this command" }, - { - type: "dynamic-tool", - toolCallId: "toolu_04test", - toolName: "bash", - state: "output-available", - input: "malformed", - output: { error: "bad" }, - }, - { type: "text", text: "Done" }, - ], - metadata: { timestamp: Date.now(), historySequence: 4 }, - }, - ]; + it("should handle messages with multiple parts", () => { + const messages: CmuxMessage[] = [ + { + id: "test-4", + role: "assistant", + parts: [ + { type: "text", text: "Let me run this command" }, + { + type: "dynamic-tool", + toolCallId: "toolu_04test", + toolName: "bash", + state: "output-available", + input: "malformed", + output: { error: "bad" }, + }, + { type: "text", text: "Done" }, + ], + metadata: { timestamp: Date.now(), historySequence: 4 }, + }, + ]; - const sanitized = sanitizeToolInputs(messages); - expect(sanitized[0].parts[1]).toMatchObject({ - type: "dynamic-tool", - input: {}, - }); - // Other parts should be unchanged - expect(sanitized[0].parts[0]).toEqual({ type: "text", text: "Let me run this command" }); - expect(sanitized[0].parts[2]).toEqual({ type: "text", text: "Done" }); + const sanitized = sanitizeToolInputs(messages); + expect(sanitized[0].parts[1]).toMatchObject({ + type: "dynamic-tool", + input: {}, }); + // Other parts should be unchanged + expect(sanitized[0].parts[0]).toEqual({ type: "text", text: "Let me run this command" }); + expect(sanitized[0].parts[2]).toEqual({ type: "text", text: "Done" }); + }); - it("should handle null input", () => { - const messages: CmuxMessage[] = [ - { - id: "test-null", - role: "assistant", - parts: [ - { - type: "dynamic-tool", - toolCallId: "toolu_null", - toolName: "bash", - state: "output-available", - // eslint-disable-next-line @typescript-eslint/no-explicit-any - input: null as any, - output: { error: "Invalid" }, - }, - ], - metadata: { timestamp: Date.now(), historySequence: 1 }, - }, - ]; + it("should handle null input", () => { + const messages: CmuxMessage[] = [ + { + id: "test-null", + role: "assistant", + parts: [ + { + type: "dynamic-tool", + toolCallId: "toolu_null", + toolName: "bash", + state: "output-available", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + input: null as any, + output: { error: "Invalid" }, + }, + ], + metadata: { timestamp: Date.now(), historySequence: 1 }, + }, + ]; - const sanitized = sanitizeToolInputs(messages); - const toolPart = sanitized[0].parts[0]; - if (toolPart.type === "dynamic-tool") { - expect(toolPart.input).toEqual({}); - } - }); + const sanitized = sanitizeToolInputs(messages); + const toolPart = sanitized[0].parts[0]; + if (toolPart.type === "dynamic-tool") { + expect(toolPart.input).toEqual({}); + } + }); - it("should handle array input", () => { - const messages: CmuxMessage[] = [ - { - id: "test-array", - role: "assistant", - parts: [ - { - type: "dynamic-tool", - toolCallId: "toolu_array", - toolName: "bash", - state: "output-available", - // eslint-disable-next-line @typescript-eslint/no-explicit-any - input: ["not", "valid"] as any, - output: { error: "Invalid" }, - }, - ], - metadata: { timestamp: Date.now(), historySequence: 1 }, - }, - ]; + it("should handle array input", () => { + const messages: CmuxMessage[] = [ + { + id: "test-array", + role: "assistant", + parts: [ + { + type: "dynamic-tool", + toolCallId: "toolu_array", + toolName: "bash", + state: "output-available", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + input: ["not", "valid"] as any, + output: { error: "Invalid" }, + }, + ], + metadata: { timestamp: Date.now(), historySequence: 1 }, + }, + ]; - const sanitized = sanitizeToolInputs(messages); - const toolPart = sanitized[0].parts[0]; - if (toolPart.type === "dynamic-tool") { - expect(toolPart.input).toEqual({}); - } - }); + const sanitized = sanitizeToolInputs(messages); + const toolPart = sanitized[0].parts[0]; + if (toolPart.type === "dynamic-tool") { + expect(toolPart.input).toEqual({}); + } }); });