Python: Add max_function_calls to FunctionInvocationConfiguration (#2329)#4175
Merged
eavanvalkenburg merged 7 commits intomicrosoft:mainfrom Feb 24, 2026
Merged
Conversation
Member
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new per-request function invocation limiter (max_function_calls) to the Python tool-invocation loop to better control total tool execution cost within a single get_response call.
Changes:
- Introduces
max_function_calls: int | NoneinFunctionInvocationConfiguration, including normalization/validation. - Tracks cumulative tool executions via
function_call_countfrom tool-processing steps and forcestool_choice="none"once the limit is reached. - Adds unit tests and a new sample demonstrating
max_iterations,max_function_calls, and per-toolmax_invocations.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
python/packages/core/agent_framework/_tools.py |
Adds max_function_calls, validation, tracking, and enforcement logic in non-streaming and streaming loops. |
python/packages/core/tests/core/test_function_invocation_logic.py |
Adds tests covering the new configuration and validation. |
python/samples/02-agents/tools/control_total_tool_executions.py |
New sample demonstrating all three limiting mechanisms with multiple scenarios. |
python/samples/01-get-started/06_host_your_agent.py |
Disables formatting/lint rule to preserve snippet tag layout. |
Comments suppressed due to low confidence (1)
python/packages/core/agent_framework/_tools.py:2117
total_function_callsis incremented for approval-executed tool calls, but themax_function_callscheck isn’t applied until after the subsequent model response’s tools are processed. If approvals alone exhaust the budget, the nextsuper_get_response(...)still runs withtool_choiceunchanged, allowing the model to request (and the loop to execute) more tool calls beyond the limit. Add a check immediately after updatingtotal_function_callsfromapproval_result(and similarly in_stream) to forcetool_choice="none"before the next model roundtrip when the budget is reached.
response = ChatResponse(messages=prepped_messages)
break
errors_in_a_row = approval_result["errors_in_a_row"]
total_function_calls += approval_result.get("function_call_count", 0)
response = await super_get_response(
messages=prepped_messages,
stream=False,
options=mutable_options,
**filtered_kwargs,
python/samples/02-agents/tools/control_total_tool_executions.py
Outdated
Show resolved
Hide resolved
…2329) Add a new per-request max_function_calls setting to FunctionInvocationConfiguration that limits the total number of individual function invocations across all iterations within a single get_response call. This complements max_iterations (which limits LLM roundtrips) by providing a hard cap on actual tool executions regardless of parallelism. - Add max_function_calls field to FunctionInvocationConfiguration (default: None/unlimited) - Track cumulative function call count in both streaming and non-streaming tool loops - Force tool_choice='none' when the limit is reached - Add validation in normalize_function_invocation_configuration - Improve docstrings for FunctionInvocationConfiguration, FunctionTool, and @tool to clarify semantics of max_iterations vs max_function_calls vs max_invocations - Add tests for parallel calls, single calls, unlimited mode, and config validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Showcases all three mechanisms for limiting tool executions: 1. max_iterations — caps LLM roundtrips 2. max_function_calls — caps total individual function invocations per request 3. max_invocations — lifetime cap on a specific tool instance Plus a combined scenario demonstrating defense in depth. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The XML snippet tags (# <create_agent> / # </create_agent>) are used for docs extraction and must stay adjacent to the code they wrap. Both ruff check (E305) and ruff format add blank lines after the function definition, pushing the closing tag away. Suppress with ruff: noqa: E305 and fmt: off. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… sample Show that wrapping the same callable with @tool multiple times creates independent FunctionTool instances with separate invocation counters, enabling per-agent max_invocations budgets for shared functions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The limit is checked after each batch of parallel calls completes, so the current batch always runs to completion even if it overshoots the limit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mple - Fix malformed Sphinx :attr: role in FunctionTool docstring — use plain backtick reference instead - Update sample to say 'best-effort cap' instead of 'hard cap' for max_function_calls, noting it's checked between iterations - Parametrize pattern is correct (fixture override, matching existing tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
5fcb84e to
452d1d4
Compare
TaoChenOSU
reviewed
Feb 23, 2026
TaoChenOSU
approved these changes
Feb 23, 2026
dmytrostruk
approved these changes
Feb 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Addresses #2329 — adds a new per-request
max_function_callssetting toFunctionInvocationConfigurationthat limits the total number of individual function invocations across all iterations within a singleget_responsecall.Motivation
max_iterationslimits LLM roundtrips, but each roundtrip can execute multiple tools in parallel. Settingmax_iterations=3could result in 30+ function executions. There was no way to cap the actual number of tool calls per request.Changes
Core (
_tools.py)max_function_calls: int | NoneonFunctionInvocationConfiguration(default:None= unlimited)_get_responseand_streamloops now track cumulative function call count viafunction_call_countonFunctionRequestResultmax_function_callsis reached, forcestool_choice="none"to get a final text responsenormalize_function_invocation_configurationvalidates the new field (must be ≥1 orNone)max_iterations— caps LLM roundtrips (each may invoke multiple tools in parallel)max_function_calls(new) — caps total individual function invocations per requestmax_invocations(on tool) — lifetime cap on a specific tool instanceTests
Nonemeans unlimited, config validationSample
samples/02-agents/tools/control_total_tool_executions.pyshowcasing all three mechanisms with 4 scenarios