diff --git a/.changeset/green-ducks-applaud.md b/.changeset/green-ducks-applaud.md new file mode 100644 index 0000000000..35a6e70ba0 --- /dev/null +++ b/.changeset/green-ducks-applaud.md @@ -0,0 +1,5 @@ +--- +"cline": patch +--- + +Canonicalize `attempt_completion` tool args by mapping `response` to `result` in the central tool executor to prevent intermittent missing-parameter retries with native parallel tool calling. diff --git a/src/core/task/ToolExecutor.ts b/src/core/task/ToolExecutor.ts index a9e0253627..ba38b594f1 100644 --- a/src/core/task/ToolExecutor.ts +++ b/src/core/task/ToolExecutor.ts @@ -54,6 +54,15 @@ import { createUIHelpers } from "./tools/types/UIHelpers" import { ToolDisplayUtils } from "./tools/utils/ToolDisplayUtils" import { ToolResultUtils } from "./tools/utils/ToolResultUtils" +export function canonicalizeAttemptCompletionParams(block: ToolUse): boolean { + if (block.name === ClineDefaultTool.ATTEMPT && !block.params?.result && typeof block.params?.response === "string") { + block.params.result = block.params.response + return true + } + + return false +} + export class ToolExecutor { private autoApprover: AutoApprove private coordinator: ToolExecutorCoordinator @@ -360,6 +369,7 @@ export class ToolExecutor { if (!this.coordinator.has(block.name)) { return false // Tool not handled by coordinator } + canonicalizeAttemptCompletionParams(block) const config = this.asToolConfig() diff --git a/src/core/task/__tests__/ToolExecutor.canonicalization.test.ts b/src/core/task/__tests__/ToolExecutor.canonicalization.test.ts new file mode 100644 index 0000000000..2de3d3c019 --- /dev/null +++ b/src/core/task/__tests__/ToolExecutor.canonicalization.test.ts @@ -0,0 +1,58 @@ +import { strict as assert } from "node:assert" +import { ClineDefaultTool } from "@shared/tools" +import { describe, it } from "mocha" +import type { ToolUse } from "../../assistant-message" +import { canonicalizeAttemptCompletionParams } from "../ToolExecutor" + +describe("ToolExecutor canonicalization", () => { + it("canonicalizes attempt_completion response into result", () => { + const block: ToolUse = { + type: "tool_use", + name: ClineDefaultTool.ATTEMPT, + params: { + response: "final answer from response field", + task_progress: "- [x] done", + }, + partial: false, + } + + const didCanonicalize = canonicalizeAttemptCompletionParams(block) + + assert.equal(didCanonicalize, true) + assert.equal(block.params.result, "final answer from response field") + assert.equal(block.params.response, "final answer from response field") + }) + + it("does not canonicalize when attempt_completion already has result", () => { + const block: ToolUse = { + type: "tool_use", + name: ClineDefaultTool.ATTEMPT, + params: { + result: "already canonical", + response: "extra text", + }, + partial: false, + } + + const didCanonicalize = canonicalizeAttemptCompletionParams(block) + + assert.equal(didCanonicalize, false) + assert.equal(block.params.result, "already canonical") + }) + + it("does not canonicalize non-attempt tools", () => { + const block: ToolUse = { + type: "tool_use", + name: ClineDefaultTool.ACT_MODE, + params: { + response: "act mode response", + }, + partial: false, + } + + const didCanonicalize = canonicalizeAttemptCompletionParams(block) + + assert.equal(didCanonicalize, false) + assert.equal(block.params.result, undefined) + }) +})