Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/core/assistant-message/NativeToolCallParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
])
}
})
})
})

Expand Down
7 changes: 5 additions & 2 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3556,8 +3556,11 @@ export class Task extends EventEmitter<TaskEvents> 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"),
Comment on lines +3559 to 3566
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Rawinput may be non-object 🐞 Bug ⛯ Reliability

rawInput is assigned directly from JSON.parse() and then preferred when persisting
tool_use.input into API conversation history. If a tool call’s arguments parse to a non-object
JSON value (array/string/number/null), history may contain an invalid tool_use.input shape and
later cause provider/API request failures when history is reused.
Agent Prompt
### Issue description
`rawInput` is set from `JSON.parse()` without ensuring the parsed value is a plain object. `Task` now prefers `rawInput` when constructing `tool_use` blocks for API conversation history. If parsed arguments are a non-object JSON value (e.g., `[]`, `"str"`, `null`), we can persist an invalid `tool_use.input` shape and potentially break subsequent API calls when replaying history.

### Issue Context
JSON allows non-object top-level values. The code currently assumes tool arguments are objects.

### Fix Focus Areas
- src/core/assistant-message/NativeToolCallParser.ts[613-616]
- src/core/assistant-message/NativeToolCallParser.ts[876-885]
- src/core/task/Task.ts[3559-3576]

### Notes
Implement a small `isPlainObject` check (or equivalent) and:
- Coerce `argsForProcessing` to `{}` when parsed value is not a plain object.
- Set `rawInput` only when the parsed value is a plain object.
- In `Task`, only prefer `rawInput` if it is a plain object; otherwise fall back to `nativeArgs`/`params`.
- Add a unit test covering a non-object parsed arguments payload to prevent regressions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Expand Down
6 changes: 6 additions & 0 deletions src/shared/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ export interface ToolUse<TName extends ToolName = ToolName> {
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<string, unknown>
}

/**
Expand Down