Skip to content

Python: fix: execute non-approval tools in mixed approval batches - #6400

Closed
King Star (jstar0) wants to merge 1 commit into
microsoft:mainfrom
jstar0:fix/mixed-tool-approval
Closed

Python: fix: execute non-approval tools in mixed approval batches#6400
King Star (jstar0) wants to merge 1 commit into
microsoft:mainfrom
jstar0:fix/mixed-tool-approval

Conversation

@jstar0

Copy link
Copy Markdown
Contributor

Summary

Fixes #6385.

When an assistant response contains a mixed batch of tool calls, the auto-invocation path currently uses one batch-level approval flag. If any tool in the batch requires approval, every function call is converted to a function_approval_request, including tools configured with approval_mode="never_require". That blocks non-sensitive tools behind the approval flow and prevents their normal results from being returned.

Changes

This change makes approval handling per function call:

  • Calls to tools with approval_mode="always_require" are returned as approval requests.
  • Calls that do not require approval continue through the normal execution path in the same batch.
  • Mixed results preserve batch order, so executed results and approval requests line up with the original function calls.
  • Executed tool results in a mixed approval batch are emitted as tool messages, while approval requests remain attached to the assistant message.
  • Updated the tool() documentation note to match the per-call behavior.

Verification

uv run pytest packages/core/tests/core/test_function_invocation_logic.py::test_mixed_batch_only_approval_tool_gets_wrapped
uv run pytest packages/core/tests/core/test_function_invocation_logic.py
uv run pytest packages/core/tests/workflow/test_agent_executor_tool_calls.py
uv run pytest packages/core/tests/core/test_tools.py packages/core/tests/core/test_middleware.py packages/core/tests/core/test_middleware_context_result.py packages/core/tests/core/test_middleware_with_agent.py packages/core/tests/core/test_middleware_with_chat.py
uv run ruff check packages/core/agent_framework/_tools.py packages/core/tests/core/test_function_invocation_logic.py
uv run pyright packages/core/agent_framework/_tools.py packages/core/tests/core/test_function_invocation_logic.py

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • Relevant unit tests pass, and I have added new tests where possible
  • Is this a breaking change? No

Copilot AI review requested due to automatic review settings June 8, 2026 16:03
@moonbox3 Evan Mattson (moonbox3) added the python Usage: [Issues, PRs], Target: Python label Jun 8, 2026
@github-actions github-actions Bot changed the title fix: execute non-approval tools in mixed approval batches Python: fix: execute non-approval tools in mixed approval batches Jun 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR updates function invocation behavior so that, when multiple tool calls are returned, only the tools configured to require approval are deferred for approval while non-approval tools execute normally.

Changes:

  • Adjust _try_execute_function_calls to defer only approval-required calls, execute the rest, and preserve original call ordering in returned Content.
  • Update response construction to append function_result contents as a separate "tool" message.
  • Add/adjust tests to validate mixed approval and execution behavior across streaming and non-streaming flows.

Reviewed changes

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

File Description
python/packages/core/tests/core/test_function_invocation_logic.py Adds a targeted regression test for mixed approval batching; updates scenario assertions to reflect new mixed execution/approval behavior.
python/packages/core/agent_framework/_tools.py Changes invocation logic to defer only approval-required tools; updates docstrings and response message shaping for function results.

Comment on lines 1699 to +1723
@@ -1708,36 +1706,25 @@ async def _try_execute_function_calls(
)
if fcc.type == "function_call" and fcc.name in approval_tools: # type: ignore[attr-defined]
logger.debug("Approval needed for function: %s", fcc.name)
approval_needed = True
break
deferred_results_by_index[idx] = Content.from_function_approval_request(
id=fcc.call_id, # type: ignore[arg-type, attr-defined]
function_call=fcc, # type: ignore[arg-type]
)
continue
if fcc.type == "function_call" and (fcc.name in declaration_only or fcc.name in additional_tool_names): # type: ignore[attr-defined]
declaration_only_flag = True
break
fcc.user_input_request = True
fcc.id = fcc.call_id
deferred_results_by_index[idx] = fcc
continue
if (
config.get("terminate_on_unknown_calls", False) and fcc.type == "function_call" and fcc.name not in tool_map # type: ignore[attr-defined]
):
raise KeyError(f'Error: Requested function "{fcc.name}" not found.') # type: ignore[attr-defined]
if approval_needed:
# approval can only be needed for Function Call Content, not Approval Responses.
logger.debug("Returning function_approval_request contents")
return (
[
Content.from_function_approval_request(id=fcc.call_id, function_call=fcc) # type: ignore[attr-defined, arg-type]
for fcc in function_calls
if fcc.type == "function_call"
],
False,
)
if declaration_only_flag:
# return the declaration only tools to the user, since we cannot execute them.
# Mark as user_input_request so AgentExecutor emits request_info events and pauses the workflow.
declaration_only_calls: list[Content] = []
for fcc in function_calls:
if fcc.type == "function_call":
fcc.user_input_request = True
fcc.id = fcc.call_id
declaration_only_calls.append(fcc)
return (declaration_only_calls, False)
function_calls_to_execute.append((idx, fcc))
Comment on lines 741 to +750
assert len(messages) == 3
assert messages[0].contents[0].type == "function_call"
assert messages[1].contents[0].type == "function_call"
# The approval request message contains both approval requests
assert len(messages[2].contents) == 2
assert all(c.type == "function_approval_request" for c in messages[2].contents)
assert exec_counter == 0 # Neither function executed yet
assert messages[2].contents[0].type == "function_result"
assert messages[2].contents[0].call_id == "1"
assert messages[2].contents[0].result == "Processed value1"
assert messages[2].contents[1].type == "function_approval_request"
assert messages[2].contents[1].function_call.name == "approval_func"
assert exec_counter == 1
Comment on lines 1714 to +1718
if fcc.type == "function_call" and (fcc.name in declaration_only or fcc.name in additional_tool_names): # type: ignore[attr-defined]
declaration_only_flag = True
break
fcc.user_input_request = True
fcc.id = fcc.call_id
deferred_results_by_index[idx] = fcc
continue
@jstar0

Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

response.messages[0].contents.extend(new_items)
else:
response.messages.append(Message(role="assistant", contents=new_items))
function_result_items = [fccr for fccr in function_call_results if fccr.type == "function_result"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we apply this same split to the streamed update path? This keeps non-streaming mixed approval responses role-correct, but _process_function_requests still returns the unsplit function_call_results, and the streaming loop emits them all with update_role == "assistant". Then stream.get_final_response() contains a function_result on an assistant message, so persisting it for approval continuation can send provider-invalid history. Should the streaming path yield the tool results as a separate role="tool" update before the approval request?

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _tools.py10267692%219–220, 397, 399, 412, 437–439, 447, 465, 479, 486, 493, 516, 518, 525, 533, 662, 696–698, 701–703, 705, 711, 762–764, 789, 815, 819, 857–859, 863, 885, 1028–1029, 1033, 1069, 1081, 1088–1091, 1112, 1116, 1120, 1134–1136, 1486, 1578, 1606, 1628, 1636, 1757, 1761, 1819, 1880–1881, 1984, 2037, 2057, 2059, 2117, 2183, 2376, 2439–2440, 2578–2579, 2646, 2651, 2658
TOTAL38452440388% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
7699 34 💤 0 ❌ 0 🔥 2m 0s ⏱️

@eavanvalkenburg

Copy link
Copy Markdown
Member

We have a different design for this in mind and will get that going this week, so closing this PR, thanks for the look King Star (@jstar0)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Mixed Tool Batch Applies Approval Wrapper To All Tool Calls

4 participants