Skip to content

Commit 1f1833b

Browse files
committed
refactor: centralize task tool protocol locking in resolveToolProtocol
- Add optional 'lockedProtocol' parameter to resolveToolProtocol() that takes absolute precedence over all other resolution logic (precedence level 0) - Update all task-context call sites to pass task.taskToolProtocol as third param - Fix API providers (anthropic, openrouter, requesty) to use metadata.toolProtocol when available, ensuring consistent protocol throughout task lifetime - Add tests for lockedProtocol parameter This ensures that once a task's tool protocol is locked at creation/resumption, all protocol checks throughout the task's execution respect the locked value, even if user settings change mid-task.
1 parent d92d729 commit 1f1833b

File tree

12 files changed

+528
-83
lines changed

12 files changed

+528
-83
lines changed

packages/types/src/history.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ export const historyItemSchema = z.object({
1919
size: z.number().optional(),
2020
workspace: z.string().optional(),
2121
mode: z.string().optional(),
22+
/**
23+
* The tool protocol used by this task. Once a task uses tools with a specific
24+
* protocol (XML or Native), it is permanently locked to that protocol.
25+
*
26+
* - "xml": Tool calls are parsed from XML text (no tool IDs)
27+
* - "native": Tool calls come as tool_call chunks with IDs
28+
*
29+
* This ensures task resumption works correctly even when NTC settings change.
30+
*/
31+
toolProtocol: z.enum(["xml", "native"]).optional(),
2232
status: z.enum(["active", "completed", "delegated"]).optional(),
2333
delegatedToId: z.string().optional(), // Last child this parent delegated to
2434
childIds: z.array(z.string()).optional(), // All children spawned by this task

src/api/providers/anthropic.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
7373
// Enable native tools by default using resolveToolProtocol (which checks model's defaultToolProtocol)
7474
// This matches OpenRouter's approach of always including tools when provided
7575
// Also exclude tools when tool_choice is "none" since that means "don't use tools"
76+
// IMPORTANT: Use metadata.toolProtocol if provided (task's locked protocol) for consistency
7677
const model = this.getModel()
77-
const toolProtocol = resolveToolProtocol(this.options, model.info)
78+
const toolProtocol = resolveToolProtocol(this.options, model.info, metadata?.toolProtocol)
7879
const shouldIncludeNativeTools =
7980
metadata?.tools &&
8081
metadata.tools.length > 0 &&

src/api/providers/openrouter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
244244
}
245245

246246
// Process reasoning_details when switching models to Gemini for native tool call compatibility
247-
const toolProtocol = resolveToolProtocol(this.options, model.info)
247+
// IMPORTANT: Use metadata.toolProtocol if provided (task's locked protocol) for consistency
248+
const toolProtocol = resolveToolProtocol(this.options, model.info, metadata?.toolProtocol)
248249
const isNativeProtocol = toolProtocol === TOOL_PROTOCOL.NATIVE
249250
const isGemini = modelId.startsWith("google/gemini")
250251

src/api/providers/requesty.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
139139
: undefined
140140

141141
// Check if native tool protocol is enabled
142-
const toolProtocol = resolveToolProtocol(this.options, info)
142+
// IMPORTANT: Use metadata.toolProtocol if provided (task's locked protocol) for consistency
143+
const toolProtocol = resolveToolProtocol(this.options, info, metadata?.toolProtocol)
143144
const useNativeTools = toolProtocol === TOOL_PROTOCOL.NATIVE
144145

145146
const completionParams: RequestyChatCompletionParamsStreaming = {

src/core/environment/getEnvironmentDetails.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,12 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
236236
language: language ?? formatLanguage(vscode.env.language),
237237
})
238238

239-
// Resolve and add tool protocol information
239+
// Use the task's locked tool protocol for consistent environment details.
240+
// This ensures the model sees the same tool format it was started with,
241+
// even if user settings have changed. Fall back to resolving fresh if
242+
// the task hasn't been fully initialized yet (shouldn't happen in practice).
240243
const modelInfo = cline.api.getModel().info
241-
const toolProtocol = resolveToolProtocol(state?.apiConfiguration ?? {}, modelInfo)
244+
const toolProtocol = resolveToolProtocol(state?.apiConfiguration ?? {}, modelInfo, cline.taskToolProtocol)
242245

243246
details += `\n\n# Current Mode\n`
244247
details += `<slug>${currentMode}</slug>\n`

src/core/task-persistence/taskMetadata.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import NodeCache from "node-cache"
22
import getFolderSize from "get-folder-size"
33

4-
import type { ClineMessage, HistoryItem } from "@roo-code/types"
4+
import type { ClineMessage, HistoryItem, ToolProtocol } from "@roo-code/types"
55

66
import { combineApiRequests } from "../../shared/combineApiRequests"
77
import { combineCommandSequences } from "../../shared/combineCommandSequences"
@@ -23,6 +23,11 @@ export type TaskMetadataOptions = {
2323
mode?: string
2424
/** Initial status for the task (e.g., "active" for child tasks) */
2525
initialStatus?: "active" | "delegated" | "completed"
26+
/**
27+
* The tool protocol locked to this task. Once set, the task will
28+
* continue using this protocol even if user settings change.
29+
*/
30+
toolProtocol?: ToolProtocol
2631
}
2732

2833
export async function taskMetadata({
@@ -35,6 +40,7 @@ export async function taskMetadata({
3540
workspace,
3641
mode,
3742
initialStatus,
43+
toolProtocol,
3844
}: TaskMetadataOptions) {
3945
const taskDir = await getTaskDirectoryPath(globalStoragePath, id)
4046

@@ -90,6 +96,8 @@ export async function taskMetadata({
9096
// initialStatus is included when provided (e.g., "active" for child tasks)
9197
// to ensure the status is set from the very first save, avoiding race conditions
9298
// where attempt_completion might run before a separate status update.
99+
// toolProtocol is persisted to ensure tasks resume with the correct protocol
100+
// even if user settings have changed.
93101
const historyItem: HistoryItem = {
94102
id,
95103
rootTaskId,
@@ -107,6 +115,7 @@ export async function taskMetadata({
107115
size: taskDirSize,
108116
workspace,
109117
mode,
118+
...(toolProtocol && { toolProtocol }),
110119
...(initialStatus && { status: initialStatus }),
111120
}
112121

0 commit comments

Comments
 (0)