Skip to content

Python: progressive tool exposure via FunctionInvocationContext#6233

Open
eavanvalkenburg wants to merge 2 commits into
microsoft:mainfrom
eavanvalkenburg:python-progressive-tools-exposure-context
Open

Python: progressive tool exposure via FunctionInvocationContext#6233
eavanvalkenburg wants to merge 2 commits into
microsoft:mainfrom
eavanvalkenburg:python-progressive-tools-exposure-context

Conversation

@eavanvalkenburg
Copy link
Copy Markdown
Member

Motivation and Context

Frontloading a model with hundreds of tools hurts tool-selection accuracy, bloats context, and raises cost (a known concern). This PR enables progressive tool exposure: an agent can start with a small set of "loader" tools and let the model pull in additional tools on demand, within the same run.

This supersedes #3877 (and the earlier #3398). Huge thanks to @suneetnangia for the original work and for driving this feature — this PR reworks the same idea on top of the newer FunctionInvocationContext to make it first-class, as discussed in the review.

Description

Tools can now add or remove real FunctionTool schemas at runtime via the injected FunctionInvocationContext, instead of mutating a list smuggled through **kwargs.

  • FunctionInvocationContext gains a live tools list plus experimental add_tools() / remove_tools() helpers (feature id PROGRESSIVE_TOOLS). add_tools is marked @experimental.
  • Inputs are coerced with the framework's standard normalize_tools; duplicate-name handling reuses _append_unique_tools (same object is a no-op, a different object with a duplicate name raises ValueError). Outside a function-calling loop the helpers raise a clear RuntimeError.
  • The function-calling loop establishes a run-local, normalized tools list and threads it into the context at both invocation paths, so additions/removals take effect on the next iteration of the loop (in-flight tool calls in the current batch still execute).
  • Old **kwargs-based AdditiveToolsList / _framework_tools approach and its threading.Lock are removed.
  • New sample samples/02-agents/tools/dynamic_tool_exposure.py and a new samples/02-agents/tools/README.md index. The README documents that CodeAct providers (agent-framework-monty / agent-framework-hyperlight) are not covered by this mechanism — there the model only sees a single execute_code tool and host tools live inside the sandbox, so those providers use their own add_tools / remove_tool / clear_tools between runs.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

Add first-class progressive tool exposure to the Python core function-calling
loop. Tools can now add or remove real FunctionTool schemas at runtime via the
injected FunctionInvocationContext, taking effect on the next iteration of the
loop.

- FunctionInvocationContext gains a live `tools` list plus experimental
  `add_tools()` / `remove_tools()` helpers (feature: PROGRESSIVE_TOOLS).
- The function-calling loop establishes a run-local, normalized tools list and
  threads it into the context at both invocation paths so mutations propagate.
- Add a sample (dynamic_tool_exposure.py) and a tools samples README, including
  a note that CodeAct providers (Monty/Hyperlight) use their own provider-level
  tool management instead.

Supersedes microsoft#3877.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 1, 2026 09:18
@moonbox3 moonbox3 added documentation Improvements or additions to documentation python labels Jun 1, 2026
@moonbox3
Copy link
Copy Markdown
Contributor

moonbox3 commented Jun 1, 2026

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _feature_stage.py1651093%106, 152, 207, 218, 238, 276, 287, 293, 324, 352
   _middleware.py3941895%62, 65, 70, 353, 365, 906, 922, 924, 926, 1059, 1062, 1089, 1091, 1222, 1226, 1408, 1412, 1480
   _tools.py10177692%219–220, 395, 397, 410, 435–437, 445, 463, 477, 484, 491, 514, 516, 523, 531, 650, 685–687, 690–692, 694, 700, 752–754, 779, 805, 809, 847–849, 853, 875, 1018–1019, 1023, 1059, 1071, 1078–1081, 1102, 1106, 1110, 1124–1126, 1476, 1564, 1592, 1614, 1618, 1752, 1756, 1802, 1863–1864, 1967, 2020, 2040, 2042, 2098, 2161, 2354, 2417–2418, 2556–2557, 2624, 2629, 2636
TOTAL37492436088% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
7467 34 💤 0 ❌ 0 🔥 1m 42s ⏱️

Copy link
Copy Markdown
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 introduces progressive tool exposure for the Python agent framework by making the per-run tool list available via FunctionInvocationContext, allowing tools to add/remove tools dynamically during a single function-calling loop.

Changes:

  • Extend FunctionInvocationContext with a live tools list plus add_tools() (experimental) and remove_tools() helpers.
  • Establish a run-local, normalized mutable tools list in the function-calling loop and thread it into tool invocation contexts.
  • Add comprehensive tests and new samples/docs demonstrating dynamic tool exposure.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
python/packages/core/agent_framework/_middleware.py Adds live tools list + progressive tool exposure helpers on FunctionInvocationContext.
python/packages/core/agent_framework/_tools.py Threads run-local tools list through invocation path; normalizes tools once per run.
python/packages/core/agent_framework/_feature_stage.py Adds ExperimentalFeature.PROGRESSIVE_TOOLS.
python/packages/core/tests/core/test_function_invocation_logic.py Adds tests validating add/remove behavior across iterations and streaming/middleware paths.
python/packages/core/AGENTS.md Documents progressive tool exposure via FunctionInvocationContext.
python/samples/02-agents/tools/dynamic_tool_exposure.py New sample demonstrating on-demand tool loading.
python/samples/02-agents/tools/README.md Adds index entry and guidance for progressive tool exposure + CodeAct note.

Comment thread python/packages/core/agent_framework/_tools.py
Comment thread python/packages/core/agent_framework/_middleware.py
Comment thread python/samples/02-agents/tools/dynamic_tool_exposure.py
Comment thread python/samples/02-agents/tools/dynamic_tool_exposure.py
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Automated Code Review

Reviewers: 4 | Confidence: 79%

✓ Correctness

The implementation is correct and well-structured. The run-local mutable tools list is properly created via normalize_tools (protecting the caller's original), shared as the same list object with both mutable_options['tools'] (seen by the model) and the FunctionInvocationContext (seen by tools), and the tool_map is rebuilt each iteration. asyncio.gather concurrency is safe because list mutations in add_tools/remove_tools are synchronous (no await points). The only issue found is a missing @experimental decorator on remove_tools, inconsistent with the PR description which states both helpers are experimental.

✓ Security Reliability

This PR implements progressive tool exposure cleanly with appropriate safety guards. The shared mutable tools list is safe under asyncio's cooperative concurrency because add_tools/remove_tools are synchronous (no await points during mutation). The caller's list is protected via normalize_tools creating a fresh copy, approval modes are preserved for dynamically added tools, and proper RuntimeError/ValueError guards exist at trust boundaries. No injection risks, resource leaks, or unhandled failure modes were identified.

✓ Test Coverage

The test suite for progressive tool exposure is thorough with 15+ tests covering the core happy paths, error cases, middleware integration, streaming, and approval workflows. However, there are a few documented behaviors and method signatures that lack direct test coverage: (1) passing a list/sequence to add_tools (tested only with single tools despite the signature accepting sequences and the sample using a list), (2) the documented guarantee that in-flight batch calls are isolated from add_tools mutations within the same batch, and (3) remove_tools in the streaming path.

✗ Design Approach

I found one design-level issue in the new progressive tool exposure API: FunctionInvocationContext.add_tools() does not actually preserve its documented duplicate-handling semantics for plain callables, because each add re-wraps the callable into a fresh FunctionTool instance before duplicate detection. That means a supported input shape can fail unexpectedly when the same loader runs twice.

Flagged Issues

  • FunctionInvocationContext.add_tools() documents that re-adding the same tool/callable is a no-op, but for plain callables normalize_tools() wraps them into a fresh FunctionTool on every call, so duplicate detection (which relies on object identity) fails and the second ctx.add_tools(the_same_callable) raises ValueError instead of being idempotent (_middleware.py:320, _tools.py:969-970, _tools.py:911-914).

Suggestions

  • Preserve callable identity across add_tools() calls for plain callables (e.g., by checking whether an existing tool's underlying func matches the incoming callable before raising), so the documented 'same object is a no-op' semantics apply equally to all supported input types.

Automated review by eavanvalkenburg's agents

Address review feedback: factorial and fibonacci now return an error
message for negative n instead of producing incorrect results.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@eavanvalkenburg eavanvalkenburg enabled auto-merge June 1, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants