Skip to content

Conversation

@DOsinga
Copy link
Collaborator

@DOsinga DOsinga commented Nov 24, 2025

Summary

Fix how we handle multiple tools, we need to interleave requests and responses, not bunch them up

Copilot AI review requested due to automatic review settings November 24, 2025 12:56
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the multi-tool calling logic to properly interleave tool requests and responses instead of bunching them together. The changes enable each tool request to have its own dedicated response message, creating a proper interleaved conversation pattern.

Key Changes:

  • Replaced a single shared message_tool_response with a HashMap mapping request IDs to individual response messages
  • Refactored tool execution to process tools sequentially, interleaving request/response pairs
  • Extracted handle_denied_tools as a separate helper function

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
crates/goose/src/agents/tool_execution.rs Updated handle_approval_tool_requests to accept a HashMap mapping request IDs to response messages, enabling individual response handling per tool request
crates/goose/src/agents/agent.rs Refactored reply logic to create individual response messages for each tool, process frontend tools sequentially, and build properly interleaved request/response message pairs

Ok(tool_futures)
}

async fn handle_denied_tools(permission_check_result: &&PermissionCheckResult, request_to_response_map: &HashMap<String, Arc<Mutex<Message>>>) {
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

Unnecessary double reference in parameter. Change permission_check_result: &&PermissionCheckResult to permission_check_result: &PermissionCheckResult.

Suggested change
async fn handle_denied_tools(permission_check_result: &&PermissionCheckResult, request_to_response_map: &HashMap<String, Arc<Mutex<Message>>>) {
async fn handle_denied_tools(permission_check_result: &PermissionCheckResult, request_to_response_map: &HashMap<String, Arc<Mutex<Message>>>) {

Copilot uses AI. Check for mistakes.
Comment on lines 1042 to 1052
for (idx, request) in frontend_requests.iter().enumerate() {
let requests = vec![request.clone()];
let mut frontend_tool_stream = self.handle_frontend_tool_requests(
&requests,
tool_response_messages[idx].clone(),
);

let mut frontend_tool_stream = self.handle_frontend_tool_requests(
&frontend_requests,
message_tool_response.clone(),
while let Some(msg) = frontend_tool_stream.try_next().await? {
yield AgentEvent::Message(msg);
}
}
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

Incorrect indentation: these lines should be indented to match the surrounding code context (should be indented one level deeper).

Copilot uses AI. Check for mistakes.
while let Some((request_id, item)) = combined.next().await {
if is_token_cancelled(&cancel_token) {
// Build the final interleaved messages
// Build the final interleaved messages
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

Duplicate comment on consecutive lines. Remove the duplicate.

Suggested change
// Build the final interleaved messages

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings November 24, 2025 13:13
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment on lines +1030 to +1032
.map(|_| Arc::new(Mutex::new(Message::user().with_id(
format!("msg_{}", Uuid::new_v4())
))))
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

[nitpick] Consider extracting message creation to a helper function. The closure creating Message::user().with_id(format!("msg_{}", Uuid::new_v4())) is duplicated at lines 1030-1032 when it could be a reusable function to improve maintainability.

Suggested change
.map(|_| Arc::new(Mutex::new(Message::user().with_id(
format!("msg_{}", Uuid::new_v4())
))))
.map(|_| Arc::new(Mutex::new(new_user_message_with_unique_id())))

Copilot uses AI. Check for mistakes.
Comment on lines 1040 to 1043
for (idx, request) in frontend_requests.iter().enumerate() {
let requests = vec![request.clone()];
let mut frontend_tool_stream = self.handle_frontend_tool_requests(
&requests,
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

Creating a single-element vector vec![request.clone()] for each iteration is inefficient. Consider refactoring handle_frontend_tool_requests to accept a single request instead of requiring a slice, or pass a slice of one element without cloning.

Suggested change
for (idx, request) in frontend_requests.iter().enumerate() {
let requests = vec![request.clone()];
let mut frontend_tool_stream = self.handle_frontend_tool_requests(
&requests,
for (idx, _request) in frontend_requests.iter().enumerate() {
let requests = &frontend_requests[idx..=idx];
let mut frontend_tool_stream = self.handle_frontend_tool_requests(
requests,

Copilot uses AI. Check for mistakes.
Comment on lines +1163 to +1173
for (idx, request) in frontend_requests.iter()
.chain(remaining_requests.iter()).enumerate() {
if request.tool_call.is_ok() {
let request_msg = Message::assistant()
.with_id(format!("msg_{}", Uuid::new_v4()))
.with_tool_request(request.id.clone(), request.tool_call.clone());
messages_to_add.push(request_msg);
let final_response = tool_response_messages[idx]
.lock().await.clone();
yield AgentEvent::Message(final_response.clone());
messages_to_add.push(final_response);
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

The indexing tool_response_messages[idx] assumes that the enumeration index matches the position in tool_response_messages, but this will fail if any requests have tool_call.is_err() since those requests are skipped but still increment idx. This could cause index out of bounds or incorrect message retrieval.

Suggested change
for (idx, request) in frontend_requests.iter()
.chain(remaining_requests.iter()).enumerate() {
if request.tool_call.is_ok() {
let request_msg = Message::assistant()
.with_id(format!("msg_{}", Uuid::new_v4()))
.with_tool_request(request.id.clone(), request.tool_call.clone());
messages_to_add.push(request_msg);
let final_response = tool_response_messages[idx]
.lock().await.clone();
yield AgentEvent::Message(final_response.clone());
messages_to_add.push(final_response);
let mut tool_response_idx = 0;
for request in frontend_requests.iter().chain(remaining_requests.iter()) {
if request.tool_call.is_ok() {
let request_msg = Message::assistant()
.with_id(format!("msg_{}", Uuid::new_v4()))
.with_tool_request(request.id.clone(), request.tool_call.clone());
messages_to_add.push(request_msg);
let final_response = tool_response_messages[tool_response_idx]
.lock().await.clone();
yield AgentEvent::Message(final_response.clone());
messages_to_add.push(final_response);
tool_response_idx += 1;

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment on lines +1164 to +1174
.chain(remaining_requests.iter()).enumerate() {
if request.tool_call.is_ok() {
let request_msg = Message::assistant()
.with_id(format!("msg_{}", Uuid::new_v4()))
.with_tool_request(request.id.clone(), request.tool_call.clone());
messages_to_add.push(request_msg);
let final_response = tool_response_messages[idx]
.lock().await.clone();
yield AgentEvent::Message(final_response.clone());
messages_to_add.push(final_response);
}
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

This loop processes all tool requests (frontend + remaining) but only emits tool_response_messages for requests where request.tool_call.is_ok(). If any request has tool_call.is_err(), the indexing into tool_response_messages[idx] will be off by one or more, causing a potential panic or mismatched response messages. The index should only increment for successful tool_calls, or filter should be applied before enumeration.

Suggested change
.chain(remaining_requests.iter()).enumerate() {
if request.tool_call.is_ok() {
let request_msg = Message::assistant()
.with_id(format!("msg_{}", Uuid::new_v4()))
.with_tool_request(request.id.clone(), request.tool_call.clone());
messages_to_add.push(request_msg);
let final_response = tool_response_messages[idx]
.lock().await.clone();
yield AgentEvent::Message(final_response.clone());
messages_to_add.push(final_response);
}
.chain(remaining_requests.iter())
.filter(|request| request.tool_call.is_ok())
.enumerate()
{
let request_msg = Message::assistant()
.with_id(format!("msg_{}", Uuid::new_v4()))
.with_tool_request(request.id.clone(), request.tool_call.clone());
messages_to_add.push(request_msg);
let final_response = tool_response_messages[idx]
.lock().await.clone();
yield AgentEvent::Message(final_response.clone());
messages_to_add.push(final_response);

Copilot uses AI. Check for mistakes.
Comment on lines +1035 to +1038
let mut request_to_response_map = HashMap::new();
for (idx, request) in frontend_requests.iter().chain(remaining_requests.iter()).enumerate() {
request_to_response_map.insert(request.id.clone(), tool_response_messages[idx].clone());
}
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

The request_to_response_map is populated using all requests (frontend_requests.iter().chain(remaining_requests.iter())), but tool_response_messages is created based on num_tool_requests which is the total count. If any requests have tool_call.is_err(), there's a mismatch: the map will have entries for all requests, but later code at line 1165 only processes requests where tool_call.is_ok(), leading to unused message objects and potential index misalignment when accessing tool_response_messages[idx].

Copilot uses AI. Check for mistakes.
Comment on lines 1040 to 1050
for (idx, request) in frontend_requests.iter().enumerate() {
let requests = vec![request.clone()];
let mut frontend_tool_stream = self.handle_frontend_tool_requests(
&requests,
tool_response_messages[idx].clone(),
);

// Process inspection results into permission decisions using the permission inspector
let permission_check_result = self.tool_inspection_manager
.process_inspection_results_with_permission_inspector(
&remaining_requests,
&inspection_results,
)
.unwrap_or_else(|| {
// Fallback if permission inspector not found - default to needs approval
let mut result = PermissionCheckResult {
approved: vec![],
needs_approval: vec![],
denied: vec![],
};
result.needs_approval.extend(remaining_requests.iter().cloned());
result
});

// Track extension requests for special handling
let mut enable_extension_request_ids = vec![];
for request in &remaining_requests {
if let Ok(tool_call) = &request.tool_call {
if tool_call.name == MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE {
enable_extension_request_ids.push(request.id.clone());
}
}
}
while let Some(msg) = frontend_tool_stream.try_next().await? {
yield AgentEvent::Message(msg);
}
}
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

[nitpick] Frontend tool requests are processed sequentially using individual indices into tool_response_messages, but this assumes frontend_requests starts at index 0 in the shared array. Since tool_response_messages contains entries for both frontend and remaining requests, and remaining requests start at frontend_requests.len(), the indexing here is correct but fragile. Consider documenting this assumption or using a more explicit indexing scheme.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

@alexhancock alexhancock left a comment

Choose a reason for hiding this comment

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

I don't understand the old code at all, but the new code makes sense to me

@DOsinga
Copy link
Collaborator Author

DOsinga commented Nov 24, 2025

In the old code we collected all the tool requests in one message and all the tool responses in another one and then appended those to the conversation, which doesn't work on at least databricks

Copilot AI review requested due to automatic review settings November 24, 2025 15:44
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment on lines +1173 to +1175
}
}
no_tools_called = false;
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

Indentation error: no_tools_called = false; should be at the same indentation level as the code inside the if let Some(response) block (line 1009), not outdented. This line logically belongs to the tool execution path.

Suggested change
}
}
no_tools_called = false;
no_tools_called = false;
}
}

Copilot uses AI. Check for mistakes.
messages_to_add.push(response.clone());
continue;
}

Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

The assistant response with tool requests is never added to messages_to_add. Line 1025 adds response.clone() when there are no tool requests, but when tools are called, the response containing the tool requests should be added before the individual tool request messages (line 1165-1167). Without this, the conversation history will be missing the assistant's response that triggered the tool calls.

Suggested change
// Always add the assistant's response that triggered the tool calls to the conversation history
messages_to_add.push(response.clone());

Copilot uses AI. Check for mistakes.
@DOsinga DOsinga merged commit 3e9d9c7 into main Nov 24, 2025
23 checks passed
@DOsinga DOsinga deleted the fix-multi-tool-calling branch November 24, 2025 15:59
michaelneale added a commit that referenced this pull request Nov 25, 2025
* main:
  docs: add DataHub MCP server extension documentation (#5769)
  docs: lowercase goose in remaining topics (#5861)
  docs: lowercase goose in getting-started and guides topics (#5857)
  Fix multi tool calling (#5855)
  fix(#5626 #5832): handle multiple content chunks & images better (#5839)
  chore: some old code hanging around, and mention configure cli (#5822)
  feat : add support for math / science symbology via katex (#5773)
  feat : add ability to see error message in toast (#5851)
kskarthik pushed a commit to kskarthik/goose that referenced this pull request Nov 25, 2025
kskarthik pushed a commit to kskarthik/goose that referenced this pull request Nov 26, 2025
Co-authored-by: Douwe Osinga <[email protected]>
Signed-off-by: Sai Karthik <[email protected]>
BlairAllan pushed a commit to BlairAllan/goose that referenced this pull request Nov 29, 2025
Co-authored-by: Douwe Osinga <[email protected]>
Signed-off-by: Blair Allan <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants