mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-10 22:41:14 +00:00
fix(vscode-lm): add tool call/result sequence validation
Fixes #10843 The VSCode Language Model API requires that when an Assistant message contains LanguageModelToolCallPart(s), the immediately next message MUST be a User message with LanguageModelToolResultPart(s) with matching callIds. This commit adds a validateAndRepairToolSequence function that: - Identifies assistant messages with tool calls - Checks that the next message is a user message with matching tool results - Adds placeholder ToolResultParts for any missing tool results - Creates user messages with placeholder results when no user message follows This fixes the 'Invalid request: Tool call part must be followed by a User message with a LanguageModelToolResultPart with a matching callId' error that occurs when using MCP servers (like JIRA/Confluence) with the vscode-lm provider in versions after 3.36.
This commit is contained in:
parent
9ab279ae42
commit
40d6b5e3c1
3 changed files with 323 additions and 3 deletions
|
|
@ -9,7 +9,11 @@ import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared
|
|||
import { normalizeToolSchema } from "../../utils/json-schema"
|
||||
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToVsCodeLmMessages, extractTextCountFromMessage } from "../transform/vscode-lm-format"
|
||||
import {
|
||||
convertToVsCodeLmMessages,
|
||||
extractTextCountFromMessage,
|
||||
validateAndRepairToolSequence,
|
||||
} from "../transform/vscode-lm-format"
|
||||
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
|
|
@ -367,9 +371,16 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
|
|||
}))
|
||||
|
||||
// Convert Anthropic messages to VS Code LM messages
|
||||
const convertedMessages = convertToVsCodeLmMessages(cleanedMessages)
|
||||
|
||||
// Validate and repair tool call/result sequences to ensure VSCode LM API requirements are met
|
||||
// The API requires that assistant messages with ToolCallParts are immediately followed by
|
||||
// user messages with matching ToolResultParts
|
||||
const validatedMessages = validateAndRepairToolSequence(convertedMessages)
|
||||
|
||||
const vsCodeLmMessages: vscode.LanguageModelChatMessage[] = [
|
||||
vscode.LanguageModelChatMessage.Assistant(systemPrompt),
|
||||
...convertToVsCodeLmMessages(cleanedMessages),
|
||||
...validatedMessages,
|
||||
]
|
||||
|
||||
// Initialize cancellation token for the request
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
import { convertToVsCodeLmMessages, convertToAnthropicRole, extractTextCountFromMessage } from "../vscode-lm-format"
|
||||
import {
|
||||
convertToVsCodeLmMessages,
|
||||
convertToAnthropicRole,
|
||||
extractTextCountFromMessage,
|
||||
validateAndRepairToolSequence,
|
||||
} from "../vscode-lm-format"
|
||||
|
||||
// Mock crypto using Vitest
|
||||
vitest.stubGlobal("crypto", {
|
||||
|
|
@ -346,3 +351,167 @@ describe("extractTextCountFromMessage", () => {
|
|||
expect(result).toBe("result-id")
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateAndRepairToolSequence", () => {
|
||||
it("should return empty array for empty input", () => {
|
||||
const result = validateAndRepairToolSequence([])
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it("should return messages unchanged when no tool calls present", () => {
|
||||
const mockTextPart = new (vitest.mocked(vscode).LanguageModelTextPart)("Hello")
|
||||
const messages = [
|
||||
{
|
||||
role: vitest.mocked(vscode).LanguageModelChatMessageRole.User,
|
||||
content: [mockTextPart],
|
||||
},
|
||||
{
|
||||
role: vitest.mocked(vscode).LanguageModelChatMessageRole.Assistant,
|
||||
content: [mockTextPart],
|
||||
},
|
||||
] as vscode.LanguageModelChatMessage[]
|
||||
|
||||
const result = validateAndRepairToolSequence(messages)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toBe(messages[0])
|
||||
expect(result[1]).toBe(messages[1])
|
||||
})
|
||||
|
||||
it("should return messages unchanged when tool call is followed by matching tool result", () => {
|
||||
const mockTextPart = new (vitest.mocked(vscode).LanguageModelTextPart)("I'll help")
|
||||
const mockToolCallPart = new (vitest.mocked(vscode).LanguageModelToolCallPart)("call-1", "test-tool", {})
|
||||
const mockToolResultPart = new (vitest.mocked(vscode).LanguageModelToolResultPart)("call-1", [
|
||||
new (vitest.mocked(vscode).LanguageModelTextPart)("result"),
|
||||
])
|
||||
|
||||
const assistantMessage = {
|
||||
role: vitest.mocked(vscode).LanguageModelChatMessageRole.Assistant,
|
||||
content: [mockTextPart, mockToolCallPart],
|
||||
} as vscode.LanguageModelChatMessage
|
||||
|
||||
const userMessage = {
|
||||
role: vitest.mocked(vscode).LanguageModelChatMessageRole.User,
|
||||
content: [mockToolResultPart],
|
||||
} as vscode.LanguageModelChatMessage
|
||||
|
||||
const messages = [assistantMessage, userMessage]
|
||||
|
||||
const result = validateAndRepairToolSequence(messages)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toBe(assistantMessage)
|
||||
expect(result[1]).toBe(userMessage)
|
||||
})
|
||||
|
||||
it("should add placeholder tool result when assistant tool call is not followed by user message", () => {
|
||||
const mockTextPart = new (vitest.mocked(vscode).LanguageModelTextPart)("I'll help")
|
||||
const mockToolCallPart = new (vitest.mocked(vscode).LanguageModelToolCallPart)("call-1", "test-tool", {})
|
||||
|
||||
const assistantMessage = {
|
||||
role: vitest.mocked(vscode).LanguageModelChatMessageRole.Assistant,
|
||||
content: [mockTextPart, mockToolCallPart],
|
||||
} as vscode.LanguageModelChatMessage
|
||||
|
||||
const messages = [assistantMessage]
|
||||
|
||||
const result = validateAndRepairToolSequence(messages)
|
||||
|
||||
// Should have original assistant message plus new user message with placeholder
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toBe(assistantMessage)
|
||||
expect(result[1].role).toBe(vitest.mocked(vscode).LanguageModelChatMessageRole.User)
|
||||
// The new user message should have a tool result part
|
||||
expect(result[1].content).toHaveLength(1)
|
||||
expect((result[1].content[0] as MockLanguageModelToolResultPart).callId).toBe("call-1")
|
||||
})
|
||||
|
||||
it("should add placeholder tool result when tool call is followed by user message without matching result", () => {
|
||||
const mockTextPart = new (vitest.mocked(vscode).LanguageModelTextPart)("I'll help")
|
||||
const mockToolCallPart = new (vitest.mocked(vscode).LanguageModelToolCallPart)("call-1", "test-tool", {})
|
||||
const mockUserTextPart = new (vitest.mocked(vscode).LanguageModelTextPart)("Continue please")
|
||||
|
||||
const assistantMessage = {
|
||||
role: vitest.mocked(vscode).LanguageModelChatMessageRole.Assistant,
|
||||
content: [mockTextPart, mockToolCallPart],
|
||||
} as vscode.LanguageModelChatMessage
|
||||
|
||||
const userMessage = {
|
||||
role: vitest.mocked(vscode).LanguageModelChatMessageRole.User,
|
||||
content: [mockUserTextPart],
|
||||
} as vscode.LanguageModelChatMessage
|
||||
|
||||
const messages = [assistantMessage, userMessage]
|
||||
|
||||
const result = validateAndRepairToolSequence(messages)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toBe(assistantMessage)
|
||||
// The repaired user message should have placeholder result prepended
|
||||
expect(result[1].content).toHaveLength(2)
|
||||
expect((result[1].content[0] as MockLanguageModelToolResultPart).callId).toBe("call-1")
|
||||
expect((result[1].content[1] as MockLanguageModelTextPart).value).toBe("Continue please")
|
||||
})
|
||||
|
||||
it("should handle multiple tool calls with partial matching results", () => {
|
||||
const mockTextPart = new (vitest.mocked(vscode).LanguageModelTextPart)("I'll help")
|
||||
const mockToolCallPart1 = new (vitest.mocked(vscode).LanguageModelToolCallPart)("call-1", "tool-1", {})
|
||||
const mockToolCallPart2 = new (vitest.mocked(vscode).LanguageModelToolCallPart)("call-2", "tool-2", {})
|
||||
const mockToolResultPart1 = new (vitest.mocked(vscode).LanguageModelToolResultPart)("call-1", [
|
||||
new (vitest.mocked(vscode).LanguageModelTextPart)("result-1"),
|
||||
])
|
||||
|
||||
const assistantMessage = {
|
||||
role: vitest.mocked(vscode).LanguageModelChatMessageRole.Assistant,
|
||||
content: [mockTextPart, mockToolCallPart1, mockToolCallPart2],
|
||||
} as vscode.LanguageModelChatMessage
|
||||
|
||||
const userMessage = {
|
||||
role: vitest.mocked(vscode).LanguageModelChatMessageRole.User,
|
||||
content: [mockToolResultPart1], // Only has result for call-1, missing call-2
|
||||
} as vscode.LanguageModelChatMessage
|
||||
|
||||
const messages = [assistantMessage, userMessage]
|
||||
|
||||
const result = validateAndRepairToolSequence(messages)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toBe(assistantMessage)
|
||||
// The repaired user message should have placeholder for call-2 prepended
|
||||
expect(result[1].content).toHaveLength(2)
|
||||
// Placeholder for missing call-2 should be first
|
||||
expect((result[1].content[0] as MockLanguageModelToolResultPart).callId).toBe("call-2")
|
||||
// Original result for call-1 should still be present
|
||||
expect((result[1].content[1] as MockLanguageModelToolResultPart).callId).toBe("call-1")
|
||||
})
|
||||
|
||||
it("should handle tool call followed by another assistant message", () => {
|
||||
const mockTextPart = new (vitest.mocked(vscode).LanguageModelTextPart)("I'll help")
|
||||
const mockToolCallPart = new (vitest.mocked(vscode).LanguageModelToolCallPart)("call-1", "test-tool", {})
|
||||
const mockTextPart2 = new (vitest.mocked(vscode).LanguageModelTextPart)("More assistant text")
|
||||
|
||||
const assistantMessage1 = {
|
||||
role: vitest.mocked(vscode).LanguageModelChatMessageRole.Assistant,
|
||||
content: [mockTextPart, mockToolCallPart],
|
||||
} as vscode.LanguageModelChatMessage
|
||||
|
||||
const assistantMessage2 = {
|
||||
role: vitest.mocked(vscode).LanguageModelChatMessageRole.Assistant,
|
||||
content: [mockTextPart2],
|
||||
} as vscode.LanguageModelChatMessage
|
||||
|
||||
const messages = [assistantMessage1, assistantMessage2]
|
||||
|
||||
const result = validateAndRepairToolSequence(messages)
|
||||
|
||||
// Should insert a user message with placeholder between the two assistant messages
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0]).toBe(assistantMessage1)
|
||||
expect(result[1].role).toBe(vitest.mocked(vscode).LanguageModelChatMessageRole.User)
|
||||
expect((result[1].content[0] as MockLanguageModelToolResultPart).callId).toBe("call-1")
|
||||
expect(result[2]).toBe(assistantMessage2)
|
||||
})
|
||||
|
||||
it("should handle null/undefined input gracefully", () => {
|
||||
expect(validateAndRepairToolSequence(null as any)).toEqual(null)
|
||||
expect(validateAndRepairToolSequence(undefined as any)).toEqual(undefined)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -194,3 +194,143 @@ export function extractTextCountFromMessage(message: vscode.LanguageModelChatMes
|
|||
}
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to extract tool call IDs from a message's content.
|
||||
* Returns an array of call IDs from LanguageModelToolCallPart instances.
|
||||
*/
|
||||
function getToolCallIds(message: vscode.LanguageModelChatMessage): string[] {
|
||||
const callIds: string[] = []
|
||||
if (Array.isArray(message.content)) {
|
||||
for (const part of message.content) {
|
||||
if (part instanceof vscode.LanguageModelToolCallPart) {
|
||||
callIds.push(part.callId)
|
||||
}
|
||||
}
|
||||
}
|
||||
return callIds
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to extract tool result IDs from a message's content.
|
||||
* Returns an array of call IDs from LanguageModelToolResultPart instances.
|
||||
*/
|
||||
function getToolResultIds(message: vscode.LanguageModelChatMessage): string[] {
|
||||
const callIds: string[] = []
|
||||
if (Array.isArray(message.content)) {
|
||||
for (const part of message.content) {
|
||||
if (part instanceof vscode.LanguageModelToolResultPart) {
|
||||
callIds.push(part.callId)
|
||||
}
|
||||
}
|
||||
}
|
||||
return callIds
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and repairs the tool call/result sequence in VSCode LM messages.
|
||||
*
|
||||
* The VSCode Language Model API requires that:
|
||||
* - When an Assistant message contains LanguageModelToolCallPart(s)
|
||||
* - The immediately next message MUST be a User message with LanguageModelToolResultPart(s)
|
||||
* with matching callIds
|
||||
*
|
||||
* This function:
|
||||
* 1. Identifies assistant messages with tool calls
|
||||
* 2. Checks that the next message is a user message with matching tool results
|
||||
* 3. If mismatches are found:
|
||||
* - Missing tool results: Adds placeholder ToolResultParts to the user message
|
||||
* - Orphaned tool results (no preceding tool call): Removes them
|
||||
* - No following user message: Creates one with the necessary tool results
|
||||
*
|
||||
* @param messages - Array of VSCode LanguageModelChatMessage to validate and repair
|
||||
* @returns The validated and repaired array of messages
|
||||
*/
|
||||
export function validateAndRepairToolSequence(
|
||||
messages: vscode.LanguageModelChatMessage[],
|
||||
): vscode.LanguageModelChatMessage[] {
|
||||
if (!messages || messages.length === 0) {
|
||||
return messages
|
||||
}
|
||||
|
||||
const repairedMessages: vscode.LanguageModelChatMessage[] = []
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const currentMessage = messages[i]
|
||||
const isAssistant = currentMessage.role === vscode.LanguageModelChatMessageRole.Assistant
|
||||
const toolCallIds = getToolCallIds(currentMessage)
|
||||
|
||||
// If this is an assistant message with tool calls
|
||||
if (isAssistant && toolCallIds.length > 0) {
|
||||
repairedMessages.push(currentMessage)
|
||||
|
||||
const nextMessage = messages[i + 1]
|
||||
const isNextUser = nextMessage?.role === vscode.LanguageModelChatMessageRole.User
|
||||
|
||||
if (isNextUser) {
|
||||
// Check if the next user message has matching tool results
|
||||
const toolResultIds = getToolResultIds(nextMessage)
|
||||
const missingResultIds = toolCallIds.filter((id) => !toolResultIds.includes(id))
|
||||
|
||||
if (missingResultIds.length > 0) {
|
||||
// Need to add placeholder tool results for missing IDs
|
||||
// Filter existing content to only include valid user message parts (TextPart and ToolResultPart)
|
||||
const existingContent: (vscode.LanguageModelTextPart | vscode.LanguageModelToolResultPart)[] = []
|
||||
if (Array.isArray(nextMessage.content)) {
|
||||
for (const part of nextMessage.content) {
|
||||
if (
|
||||
part instanceof vscode.LanguageModelTextPart ||
|
||||
part instanceof vscode.LanguageModelToolResultPart
|
||||
) {
|
||||
existingContent.push(part)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add placeholder results at the beginning (tool results come first)
|
||||
const placeholderResults = missingResultIds.map(
|
||||
(callId) =>
|
||||
new vscode.LanguageModelToolResultPart(callId, [
|
||||
new vscode.LanguageModelTextPart("[Tool result not available]"),
|
||||
]),
|
||||
)
|
||||
|
||||
// Create new user message with placeholders prepended
|
||||
const repairedUserMessage = vscode.LanguageModelChatMessage.User([
|
||||
...placeholderResults,
|
||||
...existingContent,
|
||||
])
|
||||
repairedMessages.push(repairedUserMessage)
|
||||
i++ // Skip the next message since we've processed it
|
||||
|
||||
console.warn(
|
||||
`Roo Code <Language Model API>: Added ${missingResultIds.length} placeholder tool result(s) for missing call IDs: ${missingResultIds.join(", ")}`,
|
||||
)
|
||||
} else {
|
||||
// All tool results are present, keep the message as-is
|
||||
repairedMessages.push(nextMessage)
|
||||
i++ // Skip the next message since we've processed it
|
||||
}
|
||||
} else {
|
||||
// No user message follows - create one with placeholder tool results
|
||||
const placeholderResults = toolCallIds.map(
|
||||
(callId) =>
|
||||
new vscode.LanguageModelToolResultPart(callId, [
|
||||
new vscode.LanguageModelTextPart("[Tool result not available]"),
|
||||
]),
|
||||
)
|
||||
const newUserMessage = vscode.LanguageModelChatMessage.User(placeholderResults)
|
||||
repairedMessages.push(newUserMessage)
|
||||
|
||||
console.warn(
|
||||
`Roo Code <Language Model API>: Created user message with ${toolCallIds.length} placeholder tool result(s) for call IDs: ${toolCallIds.join(", ")}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// For non-tool-call messages, just add them directly
|
||||
repairedMessages.push(currentMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return repairedMessages
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue