diff --git a/.changeset/loud-bears-hide.md b/.changeset/loud-bears-hide.md new file mode 100644 index 00000000000..e306936200f --- /dev/null +++ b/.changeset/loud-bears-hide.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Improved handling of tool calls in the API conversation history diff --git a/packages/types/src/kilocode/native-function-calling.ts b/packages/types/src/kilocode/native-function-calling.ts index 471d88a8112..647f8a8e6dc 100644 --- a/packages/types/src/kilocode/native-function-calling.ts +++ b/packages/types/src/kilocode/native-function-calling.ts @@ -21,7 +21,7 @@ export const nativeFunctionCallingProviders = [ "human-relay", ] satisfies ProviderName[] as ProviderName[] -const modelsDefaultingToNativeFunctionCalls = ["anthropic/claude-haiku-4.5"] +const modelsDefaultingToJsonKeywords = ["claude-haiku-4.5", "claude-haiku-4-5"] export function getActiveToolUseStyle(settings: ProviderSettings | undefined): ToolUseStyle { if ( @@ -33,8 +33,8 @@ export function getActiveToolUseStyle(settings: ProviderSettings | undefined): T if (settings.toolStyle) { return settings.toolStyle } - const model = getModelId(settings) - if (model && modelsDefaultingToNativeFunctionCalls.includes(model)) { + const model = getModelId(settings)?.toLowerCase() + if (model && modelsDefaultingToJsonKeywords.some((keyword) => model.includes(keyword))) { return "json" } return "xml" diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index a4a63fa8d4e..0216005e675 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -240,12 +240,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH if (this.providerName == "KiloCode" && isAnyRecognizedKiloCodeError(error)) { throw error } - const rawError = safeJsonParse(error?.error?.metadata?.raw) as { error?: OpenAI.ErrorObject } | undefined - if (rawError?.error?.message) { - throw new Error(`${this.providerName} error: ${rawError.error.message}`) - } + throw new Error(makeOpenRouterErrorReadable(error)) // kilocode_change end - throw handleOpenAIError(error, this.providerName) } let lastUsage: CompletionUsage | undefined = undefined @@ -514,8 +510,13 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH // kilocode_change start function makeOpenRouterErrorReadable(error: any) { + const metadata = error?.error?.metadata as { raw?: string; provider_name?: string } | undefined + const parsedJson = safeJsonParse(metadata?.raw) + const rawError = parsedJson as { error?: string & { message?: string }; detail?: string } | undefined + if (error?.code !== 429 && error?.code !== 418) { - return `OpenRouter API Error: ${error?.message || error}` + const errorMessage = rawError?.error?.message ?? rawError?.error ?? rawError?.detail ?? error?.message + throw new Error(`${metadata?.provider_name ?? "Provider"} error: ${errorMessage ?? "unknown error"}`) } try { diff --git a/src/api/providers/utils/openai-error-handler.ts b/src/api/providers/utils/openai-error-handler.ts index 4096535d9e9..90be81f7c43 100644 --- a/src/api/providers/utils/openai-error-handler.ts +++ b/src/api/providers/utils/openai-error-handler.ts @@ -4,7 +4,6 @@ */ import i18n from "../../../i18n/setup" -import { isAnyRecognizedKiloCodeError } from "../../../shared/kilocode/errorUtils" /** * Handles OpenAI client errors and transforms them into user-friendly messages diff --git a/src/core/assistant-message/AssistantMessageParser.ts b/src/core/assistant-message/AssistantMessageParser.ts index 68d34f078e3..94aed9ca4aa 100644 --- a/src/core/assistant-message/AssistantMessageParser.ts +++ b/src/core/assistant-message/AssistantMessageParser.ts @@ -2,6 +2,7 @@ import { type ToolName, toolNames } from "@roo-code/types" import { TextContent, ToolUse, ToolParamName, toolParamNames } from "../../shared/tools" import { AssistantMessageContent } from "./parseAssistantMessage" import { NativeToolCall, parseDoubleEncodedParams } from "./kilocode/native-tool-call" +import Anthropic from "@anthropic-ai/sdk" // kilocode_change /** * Parser for assistant messages. Maintains state between chunks @@ -75,7 +76,7 @@ export class AssistantMessageParser { * @param toolCalls Array of native tool call objects (may be partial during streaming). We * currently set parallel_tool_calls to false, so in theory there should only be 1 call. */ - public processNativeToolCalls(toolCalls: NativeToolCall[]): void { + public *processNativeToolCalls(toolCalls: NativeToolCall[]): Generator { for (const toolCall of toolCalls) { // Determine the tracking key // If we have an index, use that to look up or store the id @@ -187,6 +188,7 @@ export class AssistantMessageParser { name: toolName as ToolName, params: parsedArgs, partial: false, // Now complete after accumulation + toolUseId: accumulatedCall.id, } // Add the tool use to content blocks @@ -195,6 +197,13 @@ export class AssistantMessageParser { // Mark this tool call as processed this.processedNativeToolCallIds.add(toolCallId) this.nativeToolCallsAccumulator.delete(toolCallId) + + yield { + type: "tool_use", + name: toolUse.name, + id: toolUse.toolUseId ?? "", + input: toolUse.params, + } } } } diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 290efebbace..38409fe5107 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -42,6 +42,7 @@ import { codebaseSearchTool } from "../tools/codebaseSearchTool" import { experiments, EXPERIMENT_IDS } from "../../shared/experiments" import { applyDiffToolLegacy } from "../tools/applyDiffTool" import { yieldPromise } from "../kilocode" +import Anthropic from "@anthropic-ai/sdk" // kilocode_change /** * Processes and presents assistant message content to the user interface. @@ -60,7 +61,7 @@ import { yieldPromise } from "../kilocode" * as it becomes available. */ -export async function presentAssistantMessage(cline: Task, recursionDepth: number = 0 /*kilocode_change*/) { +export async function presentAssistantMessage(cline: Task) { if (cline.abort) { throw new Error(`[Task#presentAssistantMessage] task ${cline.taskId}.${cline.instanceId} aborted`) } @@ -247,16 +248,26 @@ export async function presentAssistantMessage(cline: Task, recursionDepth: numbe } } + const pushToolResult_withToolUseId_kilocode = ( + ...items: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] + ) => { + if (block.toolUseId) { + cline.userMessageContent.push({ type: "tool_result", tool_use_id: block.toolUseId, content: items }) + } else { + cline.userMessageContent.push(...items) + } + } + if (cline.didRejectTool) { // Ignore any tool content after user has rejected tool once. if (!block.partial) { - cline.userMessageContent.push({ + pushToolResult_withToolUseId_kilocode({ type: "text", text: `Skipping tool ${toolDescription()} due to user rejecting a previous tool.`, }) } else { // Partial tool after user rejected a previous tool. - cline.userMessageContent.push({ + pushToolResult_withToolUseId_kilocode({ type: "text", text: `Tool ${toolDescription()} was interrupted and not executed due to user rejecting a previous tool.`, }) @@ -267,7 +278,7 @@ export async function presentAssistantMessage(cline: Task, recursionDepth: numbe if (cline.didAlreadyUseTool) { // Ignore any content after a tool has already been used. - cline.userMessageContent.push({ + pushToolResult_withToolUseId_kilocode({ type: "text", text: `Tool [${block.name}] was not executed because a tool has already been used in this message. Only one tool may be used per message. You must assess the first tool's result before proceeding to use the next tool.`, }) @@ -276,13 +287,17 @@ export async function presentAssistantMessage(cline: Task, recursionDepth: numbe } const pushToolResult = (content: ToolResponse) => { - cline.userMessageContent.push({ type: "text", text: `${toolDescription()} Result:` }) + // kilocode_change start + const items = new Array() + items.push({ type: "text", text: `${toolDescription()} Result:` }) if (typeof content === "string") { - cline.userMessageContent.push({ type: "text", text: content || "(tool did not return anything)" }) + items.push({ type: "text", text: content || "(tool did not return anything)" }) } else { - cline.userMessageContent.push(...content) + items.push(...content) } + pushToolResult_withToolUseId_kilocode(...items) + // kilocode_change end // Once a tool result has been collected, ignore all other tool // uses since we should only ever present one tool result per @@ -414,7 +429,7 @@ export async function presentAssistantMessage(cline: Task, recursionDepth: numbe if (response === "messageResponse") { // Add user feedback to userContent. - cline.userMessageContent.push( + pushToolResult_withToolUseId_kilocode( { type: "text" as const, text: `Tool repetition limit reached. User feedback: ${text}`, @@ -637,7 +652,7 @@ export async function presentAssistantMessage(cline: Task, recursionDepth: numbe // this function ourselves. // kilocode_change start: prevent excessive recursion await yieldPromise() - await presentAssistantMessage(cline, recursionDepth + 1) + await presentAssistantMessage(cline) // kilocode_change end return } @@ -647,7 +662,7 @@ export async function presentAssistantMessage(cline: Task, recursionDepth: numbe if (cline.presentAssistantMessageHasPendingUpdates) { // kilocode_change start: prevent excessive recursion await yieldPromise() - await presentAssistantMessage(cline, recursionDepth + 1) + await presentAssistantMessage(cline) // kilocode_change end } } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 82090a1409f..423bfb39ebe 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -300,7 +300,11 @@ export class Task extends EventEmitter implements TaskLike { assistantMessageContent: AssistantMessageContent[] = [] presentAssistantMessageLocked = false presentAssistantMessageHasPendingUpdates = false - userMessageContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = [] + userMessageContent: ( + | Anthropic.TextBlockParam + | Anthropic.ImageBlockParam + | Anthropic.ToolResultBlockParam // kilocode_change + )[] = [] userMessageContentReady = false didRejectTool = false didAlreadyUseTool = false @@ -2004,6 +2008,7 @@ export class Task extends EventEmitter implements TaskLike { // limit error, which gets thrown on the first chunk). const stream = this.attemptApiRequest() let assistantMessage = "" + let assistantToolUses = new Array() // kilocode_change let reasoningMessage = "" let pendingGroundingSources: GroundingSource[] = [] this.isStreaming = true @@ -2056,7 +2061,11 @@ export class Task extends EventEmitter implements TaskLike { case "native_tool_calls": { // Handle native OpenAI-format tool calls // Process native tool calls through the parser - this.assistantMessageParser.processNativeToolCalls(chunk.toolCalls) + for (const toolUse of this.assistantMessageParser.processNativeToolCalls( + chunk.toolCalls, + )) { + assistantToolUses.push(toolUse) + } // Update content blocks after processing native tool calls const prevLength = this.assistantMessageContent.length @@ -2407,12 +2416,7 @@ export class Task extends EventEmitter implements TaskLike { // able to save the assistant's response. let didEndLoop = false - // kilocode_change start: Check for tool use before determining if response is empty - const didToolUse = this.assistantMessageContent.some((block) => block.type === "tool_use") - // kilocode_change end - - if (assistantMessage.length > 0 || didToolUse) { - // kilocode_change: also check for tool use + if (assistantMessage.length > 0 || assistantToolUses.length > 0 /* kilocode_change */) { // Display grounding sources to the user if they exist if (pendingGroundingSources.length > 0) { const citationLinks = pendingGroundingSources.map((source, i) => `[${i + 1}](${source.url})`) @@ -2423,10 +2427,17 @@ export class Task extends EventEmitter implements TaskLike { }) } + // kilocode_change start: also add tool calls to history + const assistantMessageContent = new Array() + if (assistantMessage) { + assistantMessageContent.push({ type: "text", text: assistantMessage }) + } + assistantMessageContent.push(...assistantToolUses) await this.addToApiConversationHistory({ role: "assistant", - content: [{ type: "text", text: assistantMessage }], + content: assistantMessageContent, }) + // kilocode_change end TelemetryService.instance.captureConversationMessage(this.taskId, "assistant") diff --git a/src/core/tools/attemptCompletionTool.ts b/src/core/tools/attemptCompletionTool.ts index 878352f951d..7f3544a11b7 100644 --- a/src/core/tools/attemptCompletionTool.ts +++ b/src/core/tools/attemptCompletionTool.ts @@ -159,6 +159,18 @@ export async function attemptCompletionTool( }) toolResults.push(...formatResponse.imageBlocks(images)) + + // kilocode_change start + if (block.toolUseId) { + cline.userMessageContent.push({ + type: "tool_result", + tool_use_id: block.toolUseId, + content: [{ type: "text", text: `${toolDescription()} Result:` }, ...toolResults], + }) + return + } + // kilocode_change end + cline.userMessageContent.push({ type: "text", text: `${toolDescription()} Result:` }) cline.userMessageContent.push(...toolResults) diff --git a/src/core/tools/kilocode.ts b/src/core/tools/kilocode.ts index 4695f90eed0..5b5fddccd40 100644 --- a/src/core/tools/kilocode.ts +++ b/src/core/tools/kilocode.ts @@ -49,3 +49,53 @@ export async function blockFileReadWhenTooLarge(task: Task, relPath: string, con xmlContent: `${relPath}${errorMsg}`, } } + +type FileEntry = { + path?: string + lineRanges?: { + start: number + end: number + }[] +} + +export function parseNativeFiles(nativeFiles: { path?: string; line_ranges?: string[] }[]) { + const fileEntries = new Array() + for (const file of nativeFiles) { + if (!file.path) continue + + const fileEntry: FileEntry = { + path: file.path, + lineRanges: [], + } + + // Handle line_ranges array from native format + if (file.line_ranges && Array.isArray(file.line_ranges)) { + for (const range of file.line_ranges) { + const match = String(range).match(/(\d+)-(\d+)/) + if (match) { + const [, start, end] = match.map(Number) + if (!isNaN(start) && !isNaN(end)) { + fileEntry.lineRanges?.push({ start, end }) + } + } + } + } + fileEntries.push(fileEntry) + } + return fileEntries +} + +export function getNativeReadFileToolDescription(blockName: string, files: FileEntry[]) { + const paths = files.map((file) => file.path) + if (paths.length === 0) { + return `[${blockName} with no valid paths]` + } else if (paths.length === 1) { + // Modified part for single file + return `[${blockName} for '${paths[0]}'. Reading multiple files at once is more efficient for the LLM. If other files are relevant to your current task, please read them simultaneously.]` + } else if (paths.length <= 3) { + const pathList = paths.map((p) => `'${p}'`).join(", ") + return `[${blockName} for ${pathList}]` + } else { + return `[${blockName} for ${paths.length} files]` + } +} diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index f11959a4555..2edc372e894 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -14,7 +14,7 @@ import { readLines } from "../../integrations/misc/read-lines" import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../integrations/misc/extract-text" import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter" import { parseXml } from "../../utils/xml" -import { blockFileReadWhenTooLarge } from "./kilocode" +import { blockFileReadWhenTooLarge, getNativeReadFileToolDescription, parseNativeFiles } from "./kilocode" import { DEFAULT_MAX_IMAGE_FILE_SIZE_MB, DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB, @@ -26,7 +26,11 @@ import { export function getReadFileToolDescription(blockName: string, blockParams: any): string { // Handle both single path and multiple files via args - if (blockParams.args) { + // kilocode_change start + if (blockParams.files && Array.isArray(blockParams.files)) { + return getNativeReadFileToolDescription(blockName, parseNativeFiles(blockParams.files)) + // kilocode_change end + } else if (blockParams.args) { try { const parsed = parseXml(blockParams.args) as any const files = Array.isArray(parsed.file) ? parsed.file : [parsed.file].filter(Boolean) @@ -131,28 +135,7 @@ export async function readFileTool( // kilocode_change start // Handle native JSON format first (from OpenAI-style tool calls) if (nativeFiles && Array.isArray(nativeFiles)) { - for (const file of nativeFiles) { - if (!file.path) continue - - const fileEntry: FileEntry = { - path: file.path, - lineRanges: [], - } - - // Handle line_ranges array from native format - if (file.line_ranges && Array.isArray(file.line_ranges)) { - for (const range of file.line_ranges) { - const match = String(range).match(/(\d+)-(\d+)/) - if (match) { - const [, start, end] = match.map(Number) - if (!isNaN(start) && !isNaN(end)) { - fileEntry.lineRanges?.push({ start, end }) - } - } - } - } - fileEntries.push(fileEntry) - } + fileEntries.push(...parseNativeFiles(nativeFiles)) // kilocode_change end } else if (argsXmlTag) { // Parse file entries from XML (new multi-file format) diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 0440829de77..8f994e8b251 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -85,6 +85,7 @@ export interface ToolUse { // params is a partial record, allowing only some or none of the possible parameters to be used params: Partial> partial: boolean + toolUseId?: string // kilocode_change } export interface ExecuteCommandToolUse extends ToolUse {