Skip to content

Python: Add max_function_calls to FunctionInvocationConfiguration (#2329)#4175

Merged
eavanvalkenburg merged 7 commits intomicrosoft:mainfrom
eavanvalkenburg:feature/max-function-calls-2329
Feb 24, 2026
Merged

Python: Add max_function_calls to FunctionInvocationConfiguration (#2329)#4175
eavanvalkenburg merged 7 commits intomicrosoft:mainfrom
eavanvalkenburg:feature/max-function-calls-2329

Conversation

@eavanvalkenburg
Copy link
Member

@eavanvalkenburg eavanvalkenburg commented Feb 23, 2026

Summary

Addresses #2329 — adds 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.

Motivation

max_iterations limits LLM roundtrips, but each roundtrip can execute multiple tools in parallel. Setting max_iterations=3 could result in 30+ function executions. There was no way to cap the actual number of tool calls per request.

Changes

Core (_tools.py)

  • New field: max_function_calls: int | None on FunctionInvocationConfiguration (default: None = unlimited)
  • Tracking: Both _get_response and _stream loops now track cumulative function call count via function_call_count on FunctionRequestResult
  • Enforcement: When max_function_calls is reached, forces tool_choice="none" to get a final text response
  • Validation: normalize_function_invocation_configuration validates the new field (must be ≥1 or None)
  • Improved docstrings: Clarified semantics of all three control mechanisms:
    • max_iterations — caps LLM roundtrips (each may invoke multiple tools in parallel)
    • max_function_calls (new) — caps total individual function invocations per request
    • max_invocations (on tool) — lifetime cap on a specific tool instance

Tests

  • 4 new tests: parallel calls limit, single calls limit, None means unlimited, config validation

Sample

  • New samples/02-agents/tools/control_total_tool_executions.py showcasing all three mechanisms with 4 scenarios

Copilot AI review requested due to automatic review settings February 23, 2026 11:44
@markwallace-microsoft
Copy link
Member

markwallace-microsoft commented Feb 23, 2026

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _tools.py8769489%166–167, 322, 324, 342–344, 351, 369, 383, 390, 397, 413, 415, 422, 459, 484, 488, 505–507, 554–556, 578, 632, 654, 717–723, 759, 770–781, 803–805, 810, 814, 828–830, 869, 938, 948, 958, 1014, 1045, 1064, 1342, 1399, 1419, 1490–1494, 1616, 1620, 1644, 1670, 1672, 1688, 1690, 1775, 1805, 1825, 1827, 1880, 1943, 2134–2135, 2187, 2200, 2210–2211, 2249–2250, 2311, 2317, 2324
TOTAL22169347684% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
4269 240 💤 0 ❌ 0 🔥 1m 16s ⏱️

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

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 | None in FunctionInvocationConfiguration, including normalization/validation.
  • Tracks cumulative tool executions via function_call_count from tool-processing steps and forces tool_choice="none" once the limit is reached.
  • Adds unit tests and a new sample demonstrating max_iterations, max_function_calls, and per-tool max_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_calls is incremented for approval-executed tool calls, but the max_function_calls check isn’t applied until after the subsequent model response’s tools are processed. If approvals alone exhaust the budget, the next super_get_response(...) still runs with tool_choice unchanged, allowing the model to request (and the loop to execute) more tool calls beyond the limit. Add a check immediately after updating total_function_calls from approval_result (and similarly in _stream) to force tool_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,

eavanvalkenburg and others added 6 commits February 23, 2026 13:22
…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>
@eavanvalkenburg eavanvalkenburg force-pushed the feature/max-function-calls-2329 branch from 5fcb84e to 452d1d4 Compare February 23, 2026 12:22
@eavanvalkenburg eavanvalkenburg added this pull request to the merge queue Feb 24, 2026
Merged via the queue into microsoft:main with commit 55398e2 Feb 24, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants