Skip to content

Commit 5486906

Browse files
committed
feat: update tool choice to 'auto' and enhance tool call handling across multiple components
1 parent 3bd9877 commit 5486906

File tree

13 files changed

+249
-63
lines changed

13 files changed

+249
-63
lines changed

src/api/providers/lm-studio.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
9898
}
9999
if (toolCallEnabled) {
100100
params.tools = toolRegistry.generateFunctionCallSchemas(metadata.tools!, metadata.toolArgs)
101-
params.tool_choice = "required"
101+
params.tool_choice = "auto"
102102
}
103103

104104
if (this.options.lmStudioSpeculativeDecodingEnabled && this.options.lmStudioDraftModelId) {

src/api/providers/openai.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
169169
}
170170
if (toolCallEnabled) {
171171
requestOptions.tools = toolRegistry.generateFunctionCallSchemas(metadata.tools!, metadata.toolArgs)
172-
requestOptions.tool_choice = "required"
172+
requestOptions.tool_choice = "auto"
173173
}
174174

175175
// Add max_tokens if needed

src/api/providers/openrouter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
139139
}
140140
if (toolCallEnabled) {
141141
completionParams.tools = toolRegistry.generateFunctionCallSchemas(metadata.tools!, metadata.toolArgs!)
142-
completionParams.tool_choice = "required"
142+
completionParams.tool_choice = "auto"
143143
}
144144

145145
const stream = await this.client.chat.completions.create(completionParams)

src/core/assistant-message/AssistantMessageParser.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type ToolName, toolNames } from "@roo-code/types"
22
import { TextContent, ToolUse, ToolParamName, toolParamNames } from "../../shared/tools"
33
import { AssistantMessageContent } from "./parseAssistantMessage"
4+
import { ToolCallParam } from "../task/tool-call-helper"
45

56
/**
67
* Parser for assistant messages. Maintains state between chunks
@@ -51,7 +52,7 @@ export class AssistantMessageParser {
5152
* Process a new chunk of text and update the parser state.
5253
* @param chunk The new chunk of text to process.
5354
*/
54-
public processChunk(chunk: string): AssistantMessageContent[] {
55+
public processChunk(chunk: string, toolCallParam?: ToolCallParam): AssistantMessageContent[] {
5556
if (this.accumulator.length + chunk.length > this.MAX_ACCUMULATOR_SIZE) {
5657
throw new Error("Assistant message exceeds maximum allowed size")
5758
}
@@ -174,6 +175,11 @@ export class AssistantMessageParser {
174175
name: extractedToolName as ToolName,
175176
params: {},
176177
partial: true,
178+
toolUseId: toolCallParam && toolCallParam.toolUserId ? toolCallParam.toolUserId : undefined,
179+
toolUseParam:
180+
toolCallParam && toolCallParam?.anthropicContent
181+
? toolCallParam?.anthropicContent
182+
: undefined,
177183
}
178184

179185
this.currentToolUseStartIndex = this.accumulator.length

src/core/assistant-message/parseAssistantMessage.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { type ToolName, toolNames } from "@roo-code/types"
22

33
import { TextContent, ToolUse, ToolParamName, toolParamNames } from "../../shared/tools"
4+
import { ToolCallParam } from "../task/tool-call-helper"
45

56
export type AssistantMessageContent = TextContent | ToolUse
67

7-
export function parseAssistantMessage(assistantMessage: string): AssistantMessageContent[] {
8+
export function parseAssistantMessage(
9+
assistantMessage: string,
10+
toolCallParam?: ToolCallParam,
11+
): AssistantMessageContent[] {
812
let contentBlocks: AssistantMessageContent[] = []
913
let currentTextContent: TextContent | undefined = undefined
1014
let currentTextContentStartIndex = 0
@@ -103,6 +107,9 @@ export function parseAssistantMessage(assistantMessage: string): AssistantMessag
103107
name: toolUseOpeningTag.slice(1, -1) as ToolName,
104108
params: {},
105109
partial: true,
110+
toolUseId: toolCallParam && toolCallParam.toolUserId ? toolCallParam.toolUserId : undefined,
111+
toolUseParam:
112+
toolCallParam && toolCallParam?.anthropicContent ? toolCallParam?.anthropicContent : undefined,
106113
}
107114

108115
currentToolUseStartIndex = accumulator.length

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { Task } from "../task/Task"
3333
import { codebaseSearchTool } from "../tools/codebaseSearchTool"
3434
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
3535
import { applyDiffToolLegacy } from "../tools/applyDiffTool"
36+
import Anthropic from "@anthropic-ai/sdk"
3637

3738
/**
3839
* Processes and presents assistant message content to the user interface.
@@ -61,6 +62,7 @@ export async function presentAssistantMessage(cline: Task) {
6162
return
6263
}
6364

65+
const toolCallEnabled = cline.apiConfiguration?.toolCallEnabled
6466
cline.presentAssistantMessageLocked = true
6567
cline.presentAssistantMessageHasPendingUpdates = false
6668

@@ -245,12 +247,28 @@ export async function presentAssistantMessage(cline: Task) {
245247
}
246248

247249
const pushToolResult = (content: ToolResponse) => {
248-
cline.userMessageContent.push({ type: "text", text: `${toolDescription()} Result:` })
249-
250+
const newUserMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = [
251+
{ type: "text", text: `${toolDescription()} Result:` },
252+
]
250253
if (typeof content === "string") {
251-
cline.userMessageContent.push({ type: "text", text: content || "(tool did not return anything)" })
254+
newUserMessages.push({ type: "text", text: content || "(tool did not return anything)" })
255+
} else {
256+
newUserMessages.push(...content)
257+
}
258+
259+
if (toolCallEnabled) {
260+
const lastToolUseMessage = cline.assistantMessageContent.find((msg) => msg.type === "tool_use")
261+
if (lastToolUseMessage && lastToolUseMessage.toolUseId) {
262+
const toolUseId = lastToolUseMessage.toolUseId
263+
const toolMessage: Anthropic.ToolResultBlockParam = {
264+
tool_use_id: toolUseId,
265+
type: "tool_result",
266+
content: newUserMessages,
267+
}
268+
cline.userMessageContent.push(toolMessage)
269+
}
252270
} else {
253-
cline.userMessageContent.push(...content)
271+
cline.userMessageContent.push(...newUserMessages)
254272
}
255273

256274
// Once a tool result has been collected, ignore all other tool
@@ -429,7 +447,7 @@ export async function presentAssistantMessage(cline: Task) {
429447
)
430448
}
431449

432-
if (isMultiFileApplyDiffEnabled || cline.apiConfiguration.toolCallEnabled === true) {
450+
if (isMultiFileApplyDiffEnabled || toolCallEnabled) {
433451
await checkpointSaveAndMark(cline)
434452
await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
435453
} else {

src/core/prompts/sections/tool-use-guidelines.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { CodeIndexManager } from "../../../services/code-index/manager"
2+
import { SystemPromptSettings } from "../types"
23

3-
export function getToolUseGuidelinesSection(codeIndexManager?: CodeIndexManager): string {
4+
export function getToolUseGuidelinesSection(
5+
codeIndexManager?: CodeIndexManager,
6+
settings?: SystemPromptSettings,
7+
): string {
48
const isCodebaseSearchAvailable =
59
codeIndexManager &&
610
codeIndexManager.isFeatureEnabled &&
@@ -34,7 +38,9 @@ export function getToolUseGuidelinesSection(codeIndexManager?: CodeIndexManager)
3438
guidelinesList.push(
3539
`${itemNumber++}. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.`,
3640
)
37-
guidelinesList.push(`${itemNumber++}. Formulate your tool use using the XML format specified for each tool.`)
41+
if (settings?.toolCallEnabled !== true) {
42+
guidelinesList.push(`${itemNumber++}. Formulate your tool use using the XML format specified for each tool.`)
43+
}
3844
guidelinesList.push(`${itemNumber++}. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
3945
- Information about whether the tool succeeded or failed, along with any reasons for failure.
4046
- Linter errors that may have arisen due to the changes you made, which you'll need to address.

src/core/prompts/system.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ ${getToolDescriptionsForMode(
108108
enableMcpServerCreation,
109109
)}
110110
111-
${getToolUseGuidelinesSection(codeIndexManager)}
111+
${getToolUseGuidelinesSection(codeIndexManager, settings)}
112112
113113
${mcpServersSection}
114114

src/core/task/Task.ts

Lines changed: 73 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ import { getMessagesSinceLastSummary, summarizeConversation } from "../condense"
108108
import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning"
109109
import { restoreTodoListForTask } from "../tools/updateTodoListTool"
110110
import { AutoApprovalHandler } from "./AutoApprovalHandler"
111-
import { StreamingToolCallProcessor, handleOpenaiToolCallStreaming } from "./tool-call-helper"
111+
import { StreamingToolCallProcessor, ToolCallParam, handleOpenaiToolCallStreaming } from "./tool-call-helper"
112112
import { ToolArgs } from "../prompts/tools/types"
113113

114114
const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes
@@ -272,7 +272,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
272272
assistantMessageContent: AssistantMessageContent[] = []
273273
presentAssistantMessageLocked = false
274274
presentAssistantMessageHasPendingUpdates = false
275-
userMessageContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
275+
userMessageContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolResultBlockParam)[] = []
276276
userMessageContentReady = false
277277
didRejectTool = false
278278
didAlreadyUseTool = false
@@ -1211,41 +1211,41 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
12111211
// Make sure that the api conversation history can be resumed by the API,
12121212
// even if it goes out of sync with cline messages.
12131213
let existingApiConversationHistory: ApiMessage[] = await this.getSavedApiConversationHistory()
1214-
1215-
// v2.0 xml tags refactor caveat: since we don't use tools anymore, we need to replace all tool use blocks with a text block since the API disallows conversations with tool uses and no tool schema
1216-
const conversationWithoutToolBlocks = existingApiConversationHistory.map((message) => {
1217-
if (Array.isArray(message.content)) {
1218-
const newContent = message.content.map((block) => {
1219-
if (block.type === "tool_use") {
1220-
// It's important we convert to the new tool schema
1221-
// format so the model doesn't get confused about how to
1222-
// invoke tools.
1223-
const inputAsXml = Object.entries(block.input as Record<string, string>)
1224-
.map(([key, value]) => `<${key}>\n${value}\n</${key}>`)
1225-
.join("\n")
1226-
return {
1227-
type: "text",
1228-
text: `<${block.name}>\n${inputAsXml}\n</${block.name}>`,
1229-
} as Anthropic.Messages.TextBlockParam
1230-
} else if (block.type === "tool_result") {
1231-
// Convert block.content to text block array, removing images
1232-
const contentAsTextBlocks = Array.isArray(block.content)
1233-
? block.content.filter((item) => item.type === "text")
1234-
: [{ type: "text", text: block.content }]
1235-
const textContent = contentAsTextBlocks.map((item) => item.text).join("\n\n")
1236-
const toolName = findToolName(block.tool_use_id, existingApiConversationHistory)
1237-
return {
1238-
type: "text",
1239-
text: `[${toolName} Result]\n\n${textContent}`,
1240-
} as Anthropic.Messages.TextBlockParam
1241-
}
1242-
return block
1243-
})
1244-
return { ...message, content: newContent }
1245-
}
1246-
return message
1247-
})
1248-
existingApiConversationHistory = conversationWithoutToolBlocks
1214+
if (this.apiConfiguration.toolCallEnabled !== true) {
1215+
const conversationWithoutToolBlocks = existingApiConversationHistory.map((message) => {
1216+
if (Array.isArray(message.content)) {
1217+
const newContent = message.content.map((block) => {
1218+
if (block.type === "tool_use") {
1219+
// It's important we convert to the new tool schema
1220+
// format so the model doesn't get confused about how to
1221+
// invoke tools.
1222+
const inputAsXml = Object.entries(block.input as Record<string, string>)
1223+
.map(([key, value]) => `<${key}>\n${value}\n</${key}>`)
1224+
.join("\n")
1225+
return {
1226+
type: "text",
1227+
text: `<${block.name}>\n${inputAsXml}\n</${block.name}>`,
1228+
} as Anthropic.Messages.TextBlockParam
1229+
} else if (block.type === "tool_result") {
1230+
// Convert block.content to text block array, removing images
1231+
const contentAsTextBlocks = Array.isArray(block.content)
1232+
? block.content.filter((item) => item.type === "text")
1233+
: [{ type: "text", text: block.content }]
1234+
const textContent = contentAsTextBlocks.map((item) => item.text).join("\n\n")
1235+
const toolName = findToolName(block.tool_use_id, existingApiConversationHistory)
1236+
return {
1237+
type: "text",
1238+
text: `[${toolName} Result]\n\n${textContent}`,
1239+
} as Anthropic.Messages.TextBlockParam
1240+
}
1241+
return block
1242+
})
1243+
return { ...message, content: newContent }
1244+
}
1245+
return message
1246+
})
1247+
existingApiConversationHistory = conversationWithoutToolBlocks
1248+
}
12491249

12501250
// FIXME: remove tool use blocks altogether
12511251

@@ -1794,13 +1794,15 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
17941794
case "text":
17951795
case "tool_call": {
17961796
let chunkContent
1797+
let toolParam: ToolCallParam | undefined
17971798
if (chunk.type == "tool_call") {
1798-
chunkContent =
1799+
toolParam =
17991800
handleOpenaiToolCallStreaming(
18001801
this.streamingToolCallProcessor,
18011802
chunk.toolCalls,
18021803
chunk.toolCallType,
18031804
) ?? ""
1805+
chunkContent = toolParam.chunkContent
18041806
} else {
18051807
chunkContent = chunk.text
18061808
}
@@ -1809,11 +1811,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
18091811
// Parse raw assistant message chunk into content blocks.
18101812
const prevLength = this.assistantMessageContent.length
18111813
if (this.isAssistantMessageParserEnabled && this.assistantMessageParser) {
1812-
this.assistantMessageContent =
1813-
this.assistantMessageParser.processChunk(chunkContent)
1814+
this.assistantMessageContent = this.assistantMessageParser.processChunk(
1815+
chunkContent,
1816+
toolParam,
1817+
)
18141818
} else {
18151819
// Use the old parsing method when experiment is disabled
1816-
this.assistantMessageContent = parseAssistantMessage(assistantMessage)
1820+
this.assistantMessageContent = parseAssistantMessage(assistantMessage, toolParam)
18171821
}
18181822

18191823
if (this.assistantMessageContent.length > prevLength) {
@@ -2107,10 +2111,34 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
21072111
let didEndLoop = false
21082112

21092113
if (assistantMessage.length > 0) {
2110-
await this.addToApiConversationHistory({
2111-
role: "assistant",
2112-
content: [{ type: "text", text: assistantMessage }],
2113-
})
2114+
if (this.apiConfiguration.toolCallEnabled !== true) {
2115+
await this.addToApiConversationHistory({
2116+
role: "assistant",
2117+
content: [{ type: "text", text: assistantMessage }],
2118+
})
2119+
} else {
2120+
for (const block of this.assistantMessageContent) {
2121+
if (block.type === "text" && block.content) {
2122+
await this.addToApiConversationHistory({
2123+
role: "assistant",
2124+
content: [{ type: "text", text: block.content }],
2125+
})
2126+
}
2127+
if (block.type === "tool_use" && block.toolUseId && block.toolUseParam) {
2128+
await this.addToApiConversationHistory({
2129+
role: "assistant",
2130+
content: [
2131+
{
2132+
type: "tool_use",
2133+
id: block.toolUseId,
2134+
name: block.name,
2135+
input: block.toolUseParam.input,
2136+
},
2137+
],
2138+
})
2139+
}
2140+
}
2141+
}
21142142

21152143
TelemetryService.instance.captureConversationMessage(this.taskId, "assistant")
21162144

src/core/task/__tests__/tool-call-helper.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ describe("handleOpenaiToolCallStreaming", () => {
306306
it("should delegate to processor.processChunk", () => {
307307
const processor = new StreamingToolCallProcessor()
308308
const chunk = [{ index: 0, id: "1", function: { name: "echo", arguments: '{"msg":"hi"}' } }]
309-
const xml = handleOpenaiToolCallStreaming(processor, chunk, "openai")
309+
const xml = handleOpenaiToolCallStreaming(processor, chunk, "openai").chunkContent
310310
expect(xml).toContain("<echo>")
311311
expect(xml).toContain("<msg>hi</msg>")
312312
})

0 commit comments

Comments
 (0)