diff --git a/src/core/assistant-message/NativeToolCallParser.ts b/src/core/assistant-message/NativeToolCallParser.ts index 87750314fa6..a16492873a9 100644 --- a/src/core/assistant-message/NativeToolCallParser.ts +++ b/src/core/assistant-message/NativeToolCallParser.ts @@ -879,6 +879,9 @@ export class NativeToolCallParser { params, partial: false, // Native tool calls are always complete when yielded nativeArgs, + // Preserve original args for API history to maintain format consistency + // This ensures line_ranges stays as [[1, 50]] instead of being converted to lineRanges + rawInput: args, } // Preserve original name for API history when an alias was used diff --git a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts index 0e81671cc15..575acc995a7 100644 --- a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts +++ b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts @@ -169,6 +169,49 @@ describe("NativeToolCallParser", () => { ]) } }) + + it("should preserve rawInput with original line_ranges format for API history", () => { + const toolCall = { + id: "toolu_123", + name: "read_file" as const, + arguments: JSON.stringify({ + files: [ + { + path: "src/core/task/Task.ts", + line_ranges: [ + [1920, 1990], + [2060, 2120], + ], + }, + ], + }), + } + + const result = NativeToolCallParser.parseToolCall(toolCall) + + expect(result).not.toBeNull() + expect(result?.type).toBe("tool_use") + if (result?.type === "tool_use") { + // Verify nativeArgs has converted format (lineRanges with objects) + const nativeArgs = result.nativeArgs as { + files: Array<{ path: string; lineRanges?: Array<{ start: number; end: number }> }> + } + expect(nativeArgs.files[0].lineRanges).toEqual([ + { start: 1920, end: 1990 }, + { start: 2060, end: 2120 }, + ]) + + // Verify rawInput preserves original format (line_ranges with tuples) + expect(result.rawInput).toBeDefined() + const rawInput = result.rawInput as { + files: Array<{ path: string; line_ranges?: Array<[number, number]> }> + } + expect(rawInput.files[0].line_ranges).toEqual([ + [1920, 1990], + [2060, 2120], + ]) + } + }) }) }) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 8db13bec82b..7f609b5b6ab 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -3556,8 +3556,11 @@ export class Task extends EventEmitter implements TaskLike { const toolUse = block as import("../../shared/tools").ToolUse const toolCallId = toolUse.id if (toolCallId) { - // nativeArgs is already in the correct API format for all tools - const input = toolUse.nativeArgs || toolUse.params + // Use rawInput to preserve original API format for history consistency. + // This ensures parameters like line_ranges stay as [[1, 50]] instead of + // being converted to lineRanges with object format [{ start: 1, end: 50 }]. + // Fall back to nativeArgs for tools that don't have rawInput, then to params for legacy. + const input = toolUse.rawInput || toolUse.nativeArgs || toolUse.params // Use originalName (alias) if present for API history consistency. // When tool aliases are used (e.g., "edit_file" -> "search_and_replace"), diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 4a2bf415925..4fe727ff39e 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -150,6 +150,12 @@ export interface ToolUse { toolUseId?: string // kilocode_change // nativeArgs is properly typed based on TName if it's in NativeToolArgs, otherwise never nativeArgs?: TName extends keyof NativeToolArgs ? NativeToolArgs[TName] : never + /** + * The raw input object from the API, preserving original parameter names and formats. + * Used for saving to conversation history to maintain API format consistency. + * For example, read_file keeps `line_ranges` as `[[1, 50]]` instead of converting to `lineRanges`. + */ + rawInput?: Record } /**