Python: progressive tool exposure via FunctionInvocationContext#6233
Python: progressive tool exposure via FunctionInvocationContext#6233eavanvalkenburg wants to merge 2 commits into
Conversation
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>
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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
FunctionInvocationContextwith a livetoolslist plusadd_tools()(experimental) andremove_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. |
There was a problem hiding this comment.
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
@experimentaldecorator 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 fromadd_toolsmutations within the same batch, and (3)remove_toolsin 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 freshFunctionToolinstance 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 callablesnormalize_tools()wraps them into a freshFunctionToolon every call, so duplicate detection (which relies on object identity) fails and the secondctx.add_tools(the_same_callable)raisesValueErrorinstead 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 underlyingfuncmatches 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>
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
FunctionInvocationContextto make it first-class, as discussed in the review.Description
Tools can now add or remove real
FunctionToolschemas at runtime via the injectedFunctionInvocationContext, instead of mutating a list smuggled through**kwargs.FunctionInvocationContextgains a livetoolslist plus experimentaladd_tools()/remove_tools()helpers (feature idPROGRESSIVE_TOOLS).add_toolsis marked@experimental.normalize_tools; duplicate-name handling reuses_append_unique_tools(same object is a no-op, a different object with a duplicate name raisesValueError). Outside a function-calling loop the helpers raise a clearRuntimeError.**kwargs-basedAdditiveToolsList/_framework_toolsapproach and itsthreading.Lockare removed.samples/02-agents/tools/dynamic_tool_exposure.pyand a newsamples/02-agents/tools/README.mdindex. The README documents that CodeAct providers (agent-framework-monty/agent-framework-hyperlight) are not covered by this mechanism — there the model only sees a singleexecute_codetool and host tools live inside the sandbox, so those providers use their ownadd_tools/remove_tool/clear_toolsbetween runs.Contribution Checklist