Roo-Code/src/utils/resolveToolProtocol.ts
Daniel 3e0d9c65e8
feat: lock task tool protocol for consistent task resumption (#10192)
Co-authored-by: Roo Code <roomote@roocode.com>
2025-12-18 11:54:26 -08:00

112 lines
4 KiB
TypeScript

import { ToolProtocol, TOOL_PROTOCOL } from "@roo-code/types"
import type { ProviderSettings, ModelInfo } from "@roo-code/types"
import type { Anthropic } from "@anthropic-ai/sdk"
import { findLast, findLastIndex } from "../shared/array"
/**
* Represents an API message in the conversation history.
* This is a minimal type definition for the detection function.
*/
type ApiMessageForDetection = Anthropic.MessageParam & {
ts?: number
}
/**
* Resolve the effective tool protocol based on the precedence hierarchy:
*
* 0. Locked Protocol (task-level lock, if provided - highest priority)
* 1. User Preference - Per-Profile (explicit profile setting)
* 2. Model Default (defaultToolProtocol in ModelInfo)
* 3. Native Fallback (final fallback)
*
* Then check support: if protocol is "native" but model doesn't support it, use XML.
*
* @param providerSettings - The provider settings for the current profile
* @param modelInfo - Optional model information containing capabilities
* @param lockedProtocol - Optional task-locked protocol that takes absolute precedence
* @returns The resolved tool protocol (either "xml" or "native")
*/
export function resolveToolProtocol(
providerSettings: ProviderSettings,
modelInfo?: ModelInfo,
lockedProtocol?: ToolProtocol,
): ToolProtocol {
// 0. Locked Protocol - task-level lock takes absolute precedence
// This ensures tasks continue using their original protocol even if settings change
if (lockedProtocol) {
return lockedProtocol
}
// If model doesn't support native tools, return XML immediately
// Treat undefined as unsupported (only allow native when explicitly true)
if (modelInfo?.supportsNativeTools !== true) {
return TOOL_PROTOCOL.XML
}
// 1. User Preference - Per-Profile (explicit profile setting, highest priority)
if (providerSettings.toolProtocol) {
return providerSettings.toolProtocol
}
// 2. Model Default - model's preferred protocol
if (modelInfo?.defaultToolProtocol) {
return modelInfo.defaultToolProtocol
}
// 3. Native Fallback
return TOOL_PROTOCOL.NATIVE
}
/**
* Detect the tool protocol used in an existing conversation history.
*
* This function scans the API conversation history for tool_use blocks
* and determines which protocol was used based on their structure:
*
* - Native protocol: tool_use blocks ALWAYS have an `id` field
* - XML protocol: tool_use blocks NEVER have an `id` field
*
* This is critical for task resumption: if a task previously used tools
* with a specific protocol, we must continue using that protocol even
* if the user's NTC settings have changed.
*
* The function searches from the most recent message backwards to find
* the last tool call, which represents the task's current protocol state.
*
* @param messages - The API conversation history to scan
* @returns The detected protocol, or undefined if no tool calls were found
*/
export function detectToolProtocolFromHistory(messages: ApiMessageForDetection[]): ToolProtocol | undefined {
// Find the last assistant message that contains a tool_use block
const lastAssistantWithTool = findLast(messages, (message) => {
if (message.role !== "assistant") {
return false
}
const content = message.content
if (!Array.isArray(content)) {
return false
}
return content.some((block) => block.type === "tool_use")
})
if (!lastAssistantWithTool) {
return undefined
}
// Find the last tool_use block in that message's content
const content = lastAssistantWithTool.content as Anthropic.ContentBlock[]
const lastToolUseIndex = findLastIndex(content, (block) => block.type === "tool_use")
if (lastToolUseIndex === -1) {
return undefined
}
const lastToolUse = content[lastToolUseIndex]
// The presence or absence of `id` determines the protocol:
// - Native protocol tool calls ALWAYS have an ID (set when parsed from tool_call chunks)
// - XML protocol tool calls NEVER have an ID (parsed from XML text)
// This pattern is used in presentAssistantMessage.ts:497-500
const hasId = "id" in lastToolUse && !!lastToolUse.id
return hasId ? TOOL_PROTOCOL.NATIVE : TOOL_PROTOCOL.XML
}