fix: convert native tool history to text when switching to xml protocol

This commit is contained in:
Hannes Rudolph 2025-11-18 20:43:36 -07:00
parent 7c079453dc
commit a6cf7b1954

View file

@ -1114,10 +1114,20 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
// Handle protocol transitions
if (shouldUseXmlParser && !this.assistantMessageParser) {
// Switching from native → XML: create parser
this.assistantMessageParser = new AssistantMessageParser()
console.log(`[Task#${this.taskId}.${this.instanceId}] Switched native → xml: initialized XML parser`)
if (shouldUseXmlParser) {
// Switching to XML protocol
if (!this.assistantMessageParser) {
this.assistantMessageParser = new AssistantMessageParser()
console.log(`[Task#${this.taskId}.${this.instanceId}] Switched native → xml: initialized XML parser`)
}
// When switching to XML protocol, we must convert any existing native tool calls in history
// to text format. This is because XML-mode providers (like Gemini) typically don't accept
// native tool_use blocks when they are not configured with tools (which they aren't in XML mode).
if (this.apiConversationHistory.length > 0) {
this.apiConversationHistory = this.convertHistoryToText(this.apiConversationHistory, newProtocol)
await this.saveApiConversationHistory()
}
} else if (!shouldUseXmlParser && this.assistantMessageParser) {
// Switching from XML → native: remove parser
this.assistantMessageParser.reset()
@ -1520,37 +1530,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Only convert tool blocks to text for XML protocol
// For native protocol, the API expects proper tool_use/tool_result structure
if (!useNative) {
const conversationWithoutToolBlocks = existingApiConversationHistory.map((message) => {
if (Array.isArray(message.content)) {
const newContent = message.content.map((block) => {
if (block.type === "tool_use") {
// Format tool invocation based on protocol
const params = block.input as Record<string, any>
const formattedText = formatToolInvocation(block.name, params, protocol)
return {
type: "text",
text: formattedText,
} as Anthropic.Messages.TextBlockParam
} else if (block.type === "tool_result") {
// Convert block.content to text block array, removing images
const contentAsTextBlocks = Array.isArray(block.content)
? block.content.filter((item) => item.type === "text")
: [{ type: "text", text: block.content }]
const textContent = contentAsTextBlocks.map((item) => item.text).join("\n\n")
const toolName = findToolName(block.tool_use_id, existingApiConversationHistory)
return {
type: "text",
text: `[${toolName} Result]\n\n${textContent}`,
} as Anthropic.Messages.TextBlockParam
}
return block
})
return { ...message, content: newContent }
}
return message
})
existingApiConversationHistory = conversationWithoutToolBlocks
existingApiConversationHistory = this.convertHistoryToText(existingApiConversationHistory, protocol)
}
// FIXME: remove tool use blocks altogether
@ -3420,6 +3400,39 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
return checkpointDiff(this, options)
}
private convertHistoryToText(history: ApiMessage[], protocol: string): ApiMessage[] {
return history.map((message) => {
if (Array.isArray(message.content)) {
const newContent = message.content.map((block) => {
if (block.type === "tool_use") {
// Format tool invocation based on protocol
const params = block.input as Record<string, any>
const formattedText = formatToolInvocation(block.name, params, protocol as any)
return {
type: "text",
text: formattedText,
} as Anthropic.Messages.TextBlockParam
} else if (block.type === "tool_result") {
// Convert block.content to text block array, removing images
const contentAsTextBlocks = Array.isArray(block.content)
? block.content.filter((item) => item.type === "text")
: [{ type: "text", text: block.content }]
const textContent = contentAsTextBlocks.map((item) => item.text).join("\n\n")
const toolName = findToolName(block.tool_use_id, history)
return {
type: "text",
text: `[${toolName} Result]\n\n${textContent}`,
} as Anthropic.Messages.TextBlockParam
}
return block
})
return { ...message, content: newContent }
}
return message
})
}
// Metrics
public combineMessages(messages: ClineMessage[]) {