Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
didToolFailInCurrentTurn = false
didCompleteReadingStream = false
assistantMessageParser?: AssistantMessageParser
// XML fallback parser for native protocol - detects and parses XML tool calls in text when model outputs them
private xmlFallbackParser?: AssistantMessageParser
private providerProfileChangeListener?: (config: { name: string; provider?: string }) => void

// Native tool call streaming state (track which index each tool is at)
Expand Down Expand Up @@ -2535,6 +2537,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
)
const shouldUseXmlParser = streamProtocol === "xml"

// Initialize XML fallback parser for native protocol
// This allows detecting XML tool calls in text when models output them despite native tools being enabled
if (!shouldUseXmlParser) {
this.xmlFallbackParser = new AssistantMessageParser()
} else {
this.xmlFallbackParser = undefined
}

// Yields only if the first chunk is successful, otherwise will
// allow the user to retry the request (most likely due to rate
// limit error, which gets thrown on the first chunk).
Expand Down Expand Up @@ -2789,6 +2799,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.userMessageContentReady = false
}

// Also feed text to XML fallback parser to detect XML tool calls
// in case the model outputs them despite native tools being enabled
if (this.xmlFallbackParser) {
this.xmlFallbackParser.processChunk(chunk.text)
}

// Present content to user
presentAssistantMessage(this)
}
Expand Down Expand Up @@ -3141,6 +3157,41 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.assistantMessageContent = parsedBlocks
}

// XML Fallback for Native Protocol:
// If we're in native protocol mode but no native tool_call chunks were received,
// check if the XML fallback parser found any tool calls in the text stream.
// This handles models that output XML tool calls despite native tools being enabled.
if (!shouldUseXmlParser && this.xmlFallbackParser) {
this.xmlFallbackParser.finalizeContentBlocks()
const fallbackBlocks = this.xmlFallbackParser.getContentBlocks()

// Check if fallback parser found any tool uses
const fallbackHasToolUses = fallbackBlocks.some(
(block) => block.type === "tool_use" || block.type === "mcp_tool_use",
)

// Check if native protocol already found tool uses
const nativeHasToolUses = this.assistantMessageContent.some(
(block) => block.type === "tool_use" || block.type === "mcp_tool_use",
)

// If native protocol didn't find tools but XML fallback did, use the fallback results
if (!nativeHasToolUses && fallbackHasToolUses) {
console.log(
`[Task#${this.taskId}] XML fallback detected tool calls in native protocol text stream. Using fallback parser results.`,
)
this.assistantMessageContent = fallbackBlocks
// Mark that we have new content to process
this.userMessageContentReady = false
Comment on lines +3178 to +3185
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the XML fallback parser is triggered, the parsed tool blocks will lack the id property that native protocol requires. The AssistantMessageParser creates tool_use blocks without IDs, but later in the code (around line 3289), tool uses are only added to assistantContent for API history if toolCallId is truthy. This means when XML fallback is used, the tool calls won't be recorded in the API conversation history, which could cause API errors on subsequent requests since tool_use blocks in assistant messages expect corresponding tool_result blocks.

Consider generating synthetic IDs for fallback-parsed tool uses (e.g., using crypto.randomUUID()) after the fallback is triggered:

Suggested change
// If native protocol didn't find tools but XML fallback did, use the fallback results
if (!nativeHasToolUses && fallbackHasToolUses) {
console.log(
`[Task#${this.taskId}] XML fallback detected tool calls in native protocol text stream. Using fallback parser results.`,
)
this.assistantMessageContent = fallbackBlocks
// Mark that we have new content to process
this.userMessageContentReady = false
// If native protocol didn't find tools but XML fallback did, use the fallback results
if (!nativeHasToolUses && fallbackHasToolUses) {
console.log(
`[Task#${this.taskId}] XML fallback detected tool calls in native protocol text stream. Using fallback parser results.`,
)
// Assign synthetic IDs to fallback tool uses since XML parser doesn't generate them
// but native protocol requires IDs for API history tracking
for (const block of fallbackBlocks) {
if ((block.type === "tool_use" || block.type === "mcp_tool_use") && !(block as any).id) {
(block as any).id = crypto.randomUUID()
}
}
this.assistantMessageContent = fallbackBlocks

Fix it with Roo Code or mention @roomote and request a fix.

// Present the fallback tool calls
presentAssistantMessage(this)
}

// Reset the fallback parser for next request
this.xmlFallbackParser.reset()
this.xmlFallbackParser = undefined
}

// Present any partial blocks that were just completed
// For XML protocol: includes both text and tool_use blocks parsed from the text stream
// For native protocol: tool_use blocks were already presented during streaming via
Expand Down
Loading