diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs
index 1a55624f3d..be66e9e635 100644
--- a/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Agents.AI;
+using Microsoft.Extensions.Logging;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Extensions.AI;
@@ -32,11 +34,19 @@ public static class ChatClientHarnessExtensions
/// additional context providers, and chat history provider.
/// When , the agent uses built-in default settings.
///
+ ///
+ /// Optional logger factory for creating loggers used by the agent and its components.
+ ///
+ ///
+ /// Optional service provider for resolving dependencies required by AI functions and other agent components.
+ ///
/// A new instance.
public static HarnessAgent AsHarnessAgent(
this IChatClient chatClient,
int maxContextWindowTokens,
int maxOutputTokens,
- HarnessAgentOptions? options = null) =>
- new(chatClient, maxContextWindowTokens, maxOutputTokens, options);
+ HarnessAgentOptions? options = null,
+ ILoggerFactory? loggerFactory = null,
+ IServiceProvider? services = null) =>
+ new(chatClient, maxContextWindowTokens, maxOutputTokens, options, loggerFactory, services);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs
index ef1af05513..139f47db3c 100644
--- a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs
@@ -10,6 +10,7 @@ using Microsoft.Agents.AI.Compaction;
using Microsoft.Agents.AI.Tools.Shell;
#endif
using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
@@ -105,6 +106,12 @@ public sealed class HarnessAgent : DelegatingAIAgent
/// additional context providers, and chat history provider.
/// When , the agent uses built-in default settings.
///
+ ///
+ /// Optional logger factory for creating loggers used by the agent and its components.
+ ///
+ ///
+ /// Optional service provider for resolving dependencies required by AI functions and other agent components.
+ ///
///
/// is .
///
@@ -112,18 +119,20 @@ public sealed class HarnessAgent : DelegatingAIAgent
/// is not positive, or
/// is negative or greater than or equal to .
///
- public HarnessAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options = null)
+ public HarnessAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options = null, ILoggerFactory? loggerFactory = null, IServiceProvider? services = null)
: base(BuildAgent(
Throw.IfNull(chatClient),
maxContextWindowTokens,
maxOutputTokens,
- options))
+ options,
+ loggerFactory,
+ services))
{
}
- private static AIAgent BuildAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options)
+ private static AIAgent BuildAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options, ILoggerFactory? loggerFactory, IServiceProvider? services)
{
- ChatClientAgent innerAgent = BuildInnerAgent(chatClient, maxContextWindowTokens, maxOutputTokens, options);
+ ChatClientAgent innerAgent = BuildInnerAgent(chatClient, maxContextWindowTokens, maxOutputTokens, options, loggerFactory, services);
AIAgentBuilder builder = innerAgent.AsBuilder();
@@ -137,10 +146,10 @@ public sealed class HarnessAgent : DelegatingAIAgent
builder.UseOpenTelemetry(sourceName: options?.OpenTelemetrySourceName);
}
- return builder.Build();
+ return builder.Build(services);
}
- private static ChatClientAgent BuildInnerAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options)
+ private static ChatClientAgent BuildInnerAgent(IChatClient chatClient, int maxContextWindowTokens, int maxOutputTokens, HarnessAgentOptions? options, ILoggerFactory? loggerFactory, IServiceProvider? services)
{
var compactionStrategy = new ContextWindowCompactionStrategy(
maxContextWindowTokens: maxContextWindowTokens,
@@ -165,13 +174,13 @@ public sealed class HarnessAgent : DelegatingAIAgent
ChatOptions chatOptions = BuildChatOptions(options, instructions, maxOutputTokens);
- var compactionProvider = new CompactionProvider(compactionStrategy);
+ var compactionProvider = new CompactionProvider(compactionStrategy, loggerFactory: loggerFactory);
- IEnumerable contextProviders = BuildContextProviders(options);
+ IEnumerable contextProviders = BuildContextProviders(options, loggerFactory);
return chatClient
.AsBuilder()
- .UseFunctionInvocation(configure: options?.MaximumIterationsPerRequest is int maxIterations
+ .UseFunctionInvocation(loggerFactory, configure: options?.MaximumIterationsPerRequest is int maxIterations
? ficc => ficc.MaximumIterationsPerRequest = maxIterations
: null)
.UseMessageInjection()
@@ -189,7 +198,9 @@ public sealed class HarnessAgent : DelegatingAIAgent
RequirePerServiceCallChatHistoryPersistence = true,
WarnOnChatHistoryProviderConflict = false,
ThrowOnChatHistoryProviderConflict = false,
- });
+ },
+ loggerFactory,
+ services);
}
private static ChatOptions BuildChatOptions(HarnessAgentOptions? options, string instructions, int maxOutputTokens)
@@ -215,7 +226,7 @@ public sealed class HarnessAgent : DelegatingAIAgent
return result;
}
- private static List BuildContextProviders(HarnessAgentOptions? options)
+ private static List BuildContextProviders(HarnessAgentOptions? options, ILoggerFactory? loggerFactory)
{
var providers = new List();
@@ -255,8 +266,8 @@ public sealed class HarnessAgent : DelegatingAIAgent
if (options?.DisableAgentSkillsProvider is not true)
{
AgentSkillsProvider skillsProvider = options?.AgentSkillsSource is AgentSkillsSource source
- ? new AgentSkillsProvider(source)
- : new AgentSkillsProvider(Directory.GetCurrentDirectory());
+ ? new AgentSkillsProvider(source, loggerFactory: loggerFactory)
+ : new AgentSkillsProvider(Directory.GetCurrentDirectory(), loggerFactory: loggerFactory);
providers.Add(skillsProvider);
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs
index 4f08209bd8..2711fa1458 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Microsoft.Agents.AI.Tools.Shell;
#endif
using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
@@ -1460,4 +1461,131 @@ public class HarnessAgentTests
#endregion
#endif
+
+ #region LoggerFactory and ServiceProvider
+
+ ///
+ /// Verify that the constructor succeeds when loggerFactory is provided.
+ ///
+ [Fact]
+ public void Constructor_SucceedsWithLoggerFactory()
+ {
+ // Arrange
+ var chatClient = new Mock().Object;
+ var loggerFactory = new Mock().Object;
+
+ // Act
+ var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory);
+
+ // Assert
+ Assert.NotNull(agent);
+ }
+
+ ///
+ /// Verify that the constructor succeeds when serviceProvider is provided.
+ ///
+ [Fact]
+ public void Constructor_SucceedsWithServiceProvider()
+ {
+ // Arrange
+ var chatClient = new Mock().Object;
+ var services = new Mock().Object;
+
+ // Act
+ var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), services: services);
+
+ // Assert
+ Assert.NotNull(agent);
+ }
+
+ ///
+ /// Verify that the constructor succeeds when both loggerFactory and serviceProvider are provided.
+ ///
+ [Fact]
+ public void Constructor_SucceedsWithLoggerFactoryAndServiceProvider()
+ {
+ // Arrange
+ var chatClient = new Mock().Object;
+ var loggerFactory = new Mock().Object;
+ var services = new Mock().Object;
+
+ // Act
+ var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory, services);
+
+ // Assert
+ Assert.NotNull(agent);
+ }
+
+ ///
+ /// Verify that AsHarnessAgent extension method accepts loggerFactory and serviceProvider.
+ ///
+ [Fact]
+ public void AsHarnessAgent_SucceedsWithLoggerFactoryAndServiceProvider()
+ {
+ // Arrange
+ var chatClient = new Mock().Object;
+ var loggerFactory = new Mock().Object;
+ var services = new Mock().Object;
+
+ // Act
+ var agent = chatClient.AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), loggerFactory, services);
+
+ // Assert
+ Assert.NotNull(agent);
+ }
+
+ ///
+ /// Verify that ILoggerFactory is threaded to downstream components by confirming CreateLogger is called.
+ ///
+ [Fact]
+ public void Constructor_LoggerFactoryIsUsedByDownstreamComponents()
+ {
+ // Arrange
+ var chatClient = new Mock().Object;
+ var mockLoggerFactory = new Mock();
+ mockLoggerFactory
+ .Setup(lf => lf.CreateLogger(It.IsAny()))
+ .Returns(new Mock().Object);
+
+ // Act — use options that leave CompactionProvider and AgentSkillsProvider enabled
+ var options = new HarnessAgentOptions
+ {
+ DisableToolApproval = true,
+ DisableOpenTelemetry = true,
+ DisableFileMemory = true,
+ DisableFileAccess = true,
+ DisableWebSearch = true,
+ DisableTodoProvider = true,
+ DisableAgentModeProvider = true,
+ };
+ var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options, mockLoggerFactory.Object);
+
+ // Assert — CreateLogger should have been called by one or more downstream components
+ Assert.NotNull(agent);
+ mockLoggerFactory.Verify(lf => lf.CreateLogger(It.IsAny()), Times.AtLeastOnce());
+ }
+
+ ///
+ /// Verify that IServiceProvider is propagated through the agent pipeline by confirming
+ /// it is queried during agent construction.
+ ///
+ [Fact]
+ public void Constructor_ServiceProviderIsQueriedDuringBuild()
+ {
+ // Arrange
+ var chatClient = new Mock().Object;
+ var mockServices = new Mock();
+ mockServices
+ .Setup(sp => sp.GetService(It.IsAny()))
+ .Returns(null!);
+
+ // Act
+ var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions(), services: mockServices.Object);
+
+ // Assert — the service provider should have been queried during pipeline construction
+ Assert.NotNull(agent);
+ mockServices.Verify(sp => sp.GetService(It.IsAny()), Times.AtLeastOnce());
+ }
+
+ #endregion
}
diff --git a/python/packages/core/AGENTS.md b/python/packages/core/AGENTS.md
index ffb6b3e2c5..a0f4c81b56 100644
--- a/python/packages/core/AGENTS.md
+++ b/python/packages/core/AGENTS.md
@@ -56,7 +56,7 @@ agent_framework/
- **`AgentMiddleware`** - Intercepts agent `run()` calls
- **`ChatMiddleware`** - Intercepts chat client `get_response()` calls
- **`FunctionMiddleware`** - Intercepts function/tool invocations
-- **`AgentContext`** / **`ChatContext`** / **`FunctionInvocationContext`** - Context objects passed through middleware
+- **`AgentContext`** / **`ChatContext`** / **`FunctionInvocationContext`** - Context objects passed through middleware. A tool can declare a `FunctionInvocationContext` parameter to receive it; `context.tools` is the live, mutable tools list for the run, and `context.add_tools(...)` / `context.remove_tools(...)` enable progressive tool exposure (changes apply on the next function-calling iteration).
### Sessions (`_sessions.py`)
diff --git a/python/packages/core/agent_framework/_feature_stage.py b/python/packages/core/agent_framework/_feature_stage.py
index dfa9cb6343..455c61bb7e 100644
--- a/python/packages/core/agent_framework/_feature_stage.py
+++ b/python/packages/core/agent_framework/_feature_stage.py
@@ -58,6 +58,7 @@ class ExperimentalFeature(str, Enum):
FOUNDRY_PREVIEW_TOOLS = "FOUNDRY_PREVIEW_TOOLS"
FUNCTIONAL_WORKFLOWS = "FUNCTIONAL_WORKFLOWS"
HARNESS = "HARNESS"
+ PROGRESSIVE_TOOLS = "PROGRESSIVE_TOOLS"
SKILLS = "SKILLS"
TO_PROMPT_AGENT = "TO_PROMPT_AGENT"
diff --git a/python/packages/core/agent_framework/_middleware.py b/python/packages/core/agent_framework/_middleware.py
index 4f90030368..2eddb08a07 100644
--- a/python/packages/core/agent_framework/_middleware.py
+++ b/python/packages/core/agent_framework/_middleware.py
@@ -11,6 +11,7 @@ from enum import Enum
from typing import TYPE_CHECKING, Any, Generic, Literal, TypeAlias, cast, overload
from ._clients import SupportsChatGetResponse
+from ._feature_stage import ExperimentalFeature, experimental
from ._types import (
AgentResponse,
AgentResponseUpdate,
@@ -214,6 +215,12 @@ class FunctionInvocationContext:
result: Function execution result. Can be observed after calling ``call_next()``
to see the actual execution result or can be set to override the execution result.
kwargs: Additional runtime keyword arguments forwarded to the function invocation.
+ tools: The live, mutable list of tools available to the model for the current
+ agent run, or ``None`` when the function is invoked outside of a
+ function-calling loop (for example via ``FunctionTool.invoke`` directly).
+ Tools can add or remove tools during execution using :meth:`add_tools`
+ and :meth:`remove_tools` (progressive tool exposure). Mutations take
+ effect on the **next** model iteration, not the in-flight batch.
Examples:
.. code-block:: python
@@ -232,6 +239,18 @@ class FunctionInvocationContext:
# Continue execution
await call_next()
+
+ Progressive tool exposure from inside a tool:
+
+ .. code-block:: python
+
+ from agent_framework import FunctionInvocationContext, tool
+
+
+ @tool(approval_mode="never_require")
+ def load_math_tools(ctx: FunctionInvocationContext) -> str:
+ ctx.add_tools([factorial, fibonacci])
+ return "Math tools are now available."
"""
def __init__(
@@ -242,6 +261,7 @@ class FunctionInvocationContext:
metadata: Mapping[str, Any] | None = None,
result: Any = None,
kwargs: Mapping[str, Any] | None = None,
+ tools: list[ToolTypes] | None = None,
) -> None:
"""Initialize the FunctionInvocationContext.
@@ -252,6 +272,9 @@ class FunctionInvocationContext:
metadata: Metadata dictionary for sharing data between function middleware.
result: Function execution result.
kwargs: Additional runtime keyword arguments forwarded to the function invocation.
+ tools: The live, mutable list of tools for the current agent run. When provided,
+ this is the same list object the model sees on the next iteration, so
+ appending or removing tools changes the model's available tools.
"""
self.function = function
self.arguments = arguments
@@ -259,6 +282,96 @@ class FunctionInvocationContext:
self.metadata: dict[str, Any] = dict(metadata) if metadata is not None else {}
self.result = result
self.kwargs: dict[str, Any] = dict(kwargs) if kwargs is not None else {}
+ self.tools = tools
+
+ @experimental(feature_id=ExperimentalFeature.PROGRESSIVE_TOOLS)
+ def add_tools(
+ self,
+ tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]],
+ ) -> None:
+ """Add one or more tools to the current agent run (progressive tool exposure).
+
+ Callable inputs are converted to :class:`FunctionTool`, and tool collections are
+ flattened, using the same normalization as the rest of the framework. Added tools
+ become available to the model on the **next** iteration of the function-calling
+ loop; they do not affect tool calls already requested in the in-flight batch.
+
+ Adding a tool whose name already exists is a no-op when it is the same object, and
+ raises ``ValueError`` when it is a different object with a duplicate name.
+
+ Args:
+ tools: A single tool/callable or a sequence of tools/callables to add.
+
+ Raises:
+ RuntimeError: If the context has no live tools list (for example when the
+ function is invoked outside of a function-calling loop).
+ ValueError: If a different tool with a duplicate name is added.
+ """
+ from ._tools import _append_unique_tools, normalize_tools # type: ignore[reportPrivateUsage]
+
+ if self.tools is None:
+ raise RuntimeError(
+ "Cannot add tools: this FunctionInvocationContext is not bound to a live "
+ "agent run. add_tools is only available for functions invoked within an "
+ "agent's function-calling loop."
+ )
+ # Validate the whole batch against a throwaway copy first, so a duplicate-name
+ # clash partway through the batch raises before the live tool list is mutated
+ # (all-or-nothing semantics).
+ merged = _append_unique_tools(list(self.tools), normalize_tools(tools))
+ self.tools[:] = merged
+
+ @experimental(feature_id=ExperimentalFeature.PROGRESSIVE_TOOLS)
+ def remove_tools(
+ self,
+ tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | str | Sequence[str],
+ ) -> None:
+ """Remove one or more tools from the current agent run (progressive tool exposure).
+
+ Tools may be specified by name, by tool object, or by the original callable. Names
+ that are not currently present are ignored. Removals take effect on the **next**
+ iteration of the function-calling loop; tool calls already requested in the
+ in-flight batch still execute.
+
+ Args:
+ tools: A tool name, tool/callable, or a sequence of any of these to remove.
+
+ Raises:
+ RuntimeError: If the context has no live tools list (for example when the
+ function is invoked outside of a function-calling loop).
+ """
+ from ._tools import _get_tool_name, normalize_tools # type: ignore[reportPrivateUsage]
+
+ if self.tools is None:
+ raise RuntimeError(
+ "Cannot remove tools: this FunctionInvocationContext is not bound to a live "
+ "agent run. remove_tools is only available for functions invoked within an "
+ "agent's function-calling loop."
+ )
+
+ names_to_remove: set[str] = set()
+ raw_items: list[Any]
+ if isinstance(tools, str):
+ raw_items = [tools]
+ elif isinstance(tools, Sequence) and not isinstance(tools, (bytes, bytearray)):
+ raw_items = list(cast("Sequence[Any]", tools))
+ else:
+ raw_items = [tools]
+ for item in raw_items:
+ if isinstance(item, str):
+ names_to_remove.add(item)
+ continue
+ for normalized in normalize_tools(item):
+ if name := _get_tool_name(normalized): # type: ignore[reportPrivateUsage]
+ names_to_remove.add(name)
+
+ if not names_to_remove:
+ return
+ self.tools[:] = [
+ tool
+ for tool in self.tools
+ if _get_tool_name(tool) not in names_to_remove # type: ignore[reportPrivateUsage]
+ ]
class ChatContext:
diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py
index 93722a8987..5237cf62ba 100644
--- a/python/packages/core/agent_framework/_tools.py
+++ b/python/packages/core/agent_framework/_tools.py
@@ -1418,6 +1418,7 @@ async def _auto_invoke_function(
sequence_index: int | None = None,
request_index: int | None = None,
middleware_pipeline: FunctionMiddlewarePipeline | None = None,
+ live_tools: list[ToolTypes] | None = None,
) -> Content:
"""Invoke a function call requested by the agent, applying middleware that is defined.
@@ -1432,6 +1433,8 @@ async def _auto_invoke_function(
sequence_index: The index of the function call in the sequence.
request_index: The index of the request iteration.
middleware_pipeline: Optional middleware pipeline to apply during execution.
+ live_tools: The live, mutable tools list for the current agent run, exposed on
+ the FunctionInvocationContext so tools can add/remove tools at runtime.
Returns:
The function result content.
@@ -1523,6 +1526,7 @@ async def _auto_invoke_function(
arguments=args,
session=invocation_session,
kwargs=runtime_kwargs.copy(),
+ tools=live_tools,
)
function_result = await tool.invoke(
arguments=args,
@@ -1537,6 +1541,10 @@ async def _auto_invoke_function(
except UserInputRequiredException:
raise
except Exception as exc:
+ logger.warning(
+ f"Function '{tool.name}' raised an exception; returning an error result to the "
+ f"model. Set include_detailed_errors=True for the full detail. Exception: {exc!r}"
+ )
message = "Error: Function failed."
if config.get("include_detailed_errors", False):
message = f"{message} Exception: {exc}"
@@ -1552,6 +1560,7 @@ async def _auto_invoke_function(
arguments=args,
session=invocation_session,
kwargs=runtime_kwargs.copy(),
+ tools=live_tools,
)
call_id = function_call_content.call_id
@@ -1608,6 +1617,10 @@ async def _auto_invoke_function(
except UserInputRequiredException:
raise
except Exception as exc:
+ logger.warning(
+ f"Function '{tool.name}' raised an exception; returning an error result to the "
+ f"model. Set include_detailed_errors=True for the full detail. Exception: {exc!r}"
+ )
message = "Error: Function failed."
if config.get("include_detailed_errors", False):
message = f"{message} Exception: {exc}"
@@ -1659,6 +1672,9 @@ async def _try_execute_function_calls(
from ._types import Content
tool_map = _get_tool_map(tools)
+ # The live tools list (when tools is the run-local list) is exposed on the
+ # FunctionInvocationContext so tools can add/remove tools during the run.
+ live_tools: list[ToolTypes] | None = cast("list[ToolTypes]", tools) if isinstance(tools, list) else None
approval_tools = [tool_name for tool_name, tool in tool_map.items() if tool.approval_mode == "always_require"]
logger.debug(
"_try_execute_function_calls: tool_map keys=%s, approval_tools=%s",
@@ -1733,6 +1749,7 @@ async def _try_execute_function_calls(
request_index=attempt_idx,
middleware_pipeline=middleware_pipeline,
config=config,
+ live_tools=live_tools,
)
return (result, False)
except MiddlewareTermination as exc:
@@ -2371,6 +2388,13 @@ class FunctionInvocationLayer(Generic[OptionsCoT]):
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=filtered_kwargs,
)
+ # Establish a single, run-local mutable tools list so that tools can add or remove
+ # tools during the run (progressive tool exposure). A fresh list is created via
+ # normalize_tools so the caller's original tools container is never mutated, while
+ # the same list object is shared with the model (options["tools"]) and the tool map
+ # rebuilt on every loop iteration.
+ if mutable_options.get("tools"):
+ mutable_options["tools"] = normalize_tools(mutable_options["tools"])
if not stream:
async def _get_response() -> ChatResponse[Any]:
diff --git a/python/packages/core/tests/core/test_function_invocation_logic.py b/python/packages/core/tests/core/test_function_invocation_logic.py
index 3d20a26080..f96ca99ad5 100644
--- a/python/packages/core/tests/core/test_function_invocation_logic.py
+++ b/python/packages/core/tests/core/test_function_invocation_logic.py
@@ -3975,3 +3975,425 @@ async def test_user_input_request_empty_contents_returns_fallback(chat_client_ba
]
assert len(function_results) >= 1
assert any("user input" in (fr.result or "").lower() for fr in function_results)
+
+
+# region Progressive tool exposure (FunctionInvocationContext.add_tools / remove_tools)
+
+
+def _pte_function_call_response(call_id: str, name: str, arguments: str = "{}") -> ChatResponse:
+ return ChatResponse(
+ messages=Message(
+ role="assistant",
+ contents=[Content.from_function_call(call_id=call_id, name=name, arguments=arguments)],
+ )
+ )
+
+
+def _pte_text_response(text: str = "done") -> ChatResponse:
+ return ChatResponse(messages=Message(role="assistant", contents=[text]))
+
+
+@tool(name="factorial", approval_mode="never_require")
+def _pte_factorial(n: int) -> int:
+ """Compute the factorial of n."""
+ result = 1
+ for value in range(2, n + 1):
+ result *= value
+ return result
+
+
+async def test_context_exposes_live_tools(chat_client_base: SupportsChatGetResponse):
+ from agent_framework import FunctionTool
+
+ seen_names: list[str] = []
+
+ @tool(name="inspect_tools", approval_mode="never_require")
+ def inspect_tools(ctx: FunctionInvocationContext) -> str:
+ assert ctx.tools is not None
+ seen_names.extend(t.name for t in ctx.tools if isinstance(t, FunctionTool))
+ return "inspected"
+
+ chat_client_base.run_responses = [
+ _pte_function_call_response("1", "inspect_tools"),
+ _pte_text_response(),
+ ]
+ await chat_client_base.get_response(
+ [Message(role="user", contents=["hi"])],
+ options={"tool_choice": "auto", "tools": [inspect_tools]},
+ )
+ assert "inspect_tools" in seen_names
+
+
+async def test_add_tools_available_next_iteration(chat_client_base: SupportsChatGetResponse):
+ exec_counter = 0
+
+ @tool(name="factorial", approval_mode="never_require")
+ def factorial(n: int) -> int:
+ nonlocal exec_counter
+ exec_counter += 1
+ return 120
+
+ @tool(name="load_math", approval_mode="never_require")
+ def load_math(ctx: FunctionInvocationContext) -> str:
+ ctx.add_tools(factorial)
+ return "math tools loaded"
+
+ chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
+ chat_client_base.run_responses = [
+ _pte_function_call_response("1", "load_math"),
+ _pte_function_call_response("2", "factorial", '{"n": 5}'),
+ _pte_text_response(),
+ ]
+ response = await chat_client_base.get_response(
+ [Message(role="user", contents=["compute 5!"])],
+ options={"tool_choice": "auto", "tools": [load_math]},
+ )
+ assert exec_counter == 1
+ assert response.messages[-1].text == "done"
+
+
+async def test_add_tools_model_sees_added_tools_in_options(chat_client_base: SupportsChatGetResponse):
+ from agent_framework import FunctionTool
+
+ recorded: list[list[str]] = []
+ client_cls = type(chat_client_base)
+ original = client_cls._get_non_streaming_response
+
+ async def recording(self: Any, *, messages: Any, options: dict[str, Any], **kwargs: Any) -> ChatResponse:
+ tools = options.get("tools") or []
+ recorded.append([t.name for t in tools if isinstance(t, FunctionTool)])
+ return await original(self, messages=messages, options=options, **kwargs)
+
+ @tool(name="load_math", approval_mode="never_require")
+ def load_math(ctx: FunctionInvocationContext) -> str:
+ ctx.add_tools(_pte_factorial)
+ return "loaded"
+
+ chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
+ chat_client_base.run_responses = [
+ _pte_function_call_response("1", "load_math"),
+ _pte_function_call_response("2", "factorial", '{"n": 5}'),
+ _pte_text_response(),
+ ]
+ monkey = pytest.MonkeyPatch()
+ monkey.setattr(client_cls, "_get_non_streaming_response", recording)
+ try:
+ await chat_client_base.get_response(
+ [Message(role="user", contents=["compute 5!"])],
+ options={"tool_choice": "auto", "tools": [load_math]},
+ )
+ finally:
+ monkey.undo()
+
+ assert recorded[0] == ["load_math"]
+ assert "factorial" in recorded[1]
+
+
+async def test_remove_tools_next_iteration(chat_client_base: SupportsChatGetResponse):
+ from agent_framework import FunctionTool
+
+ recorded: list[list[str]] = []
+ client_cls = type(chat_client_base)
+ original = client_cls._get_non_streaming_response
+
+ async def recording(self: Any, *, messages: Any, options: dict[str, Any], **kwargs: Any) -> ChatResponse:
+ tools = options.get("tools") or []
+ recorded.append([t.name for t in tools if isinstance(t, FunctionTool)])
+ return await original(self, messages=messages, options=options, **kwargs)
+
+ @tool(name="get_weather", approval_mode="never_require")
+ def get_weather(location: str) -> str:
+ return "sunny"
+
+ @tool(name="drop_weather", approval_mode="never_require")
+ def drop_weather(ctx: FunctionInvocationContext) -> str:
+ ctx.remove_tools("get_weather")
+ return "removed"
+
+ chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
+ chat_client_base.run_responses = [
+ _pte_function_call_response("1", "drop_weather"),
+ _pte_text_response(),
+ ]
+ monkey = pytest.MonkeyPatch()
+ monkey.setattr(client_cls, "_get_non_streaming_response", recording)
+ try:
+ await chat_client_base.get_response(
+ [Message(role="user", contents=["hi"])],
+ options={"tool_choice": "auto", "tools": [get_weather, drop_weather]},
+ )
+ finally:
+ monkey.undo()
+
+ assert set(recorded[0]) == {"get_weather", "drop_weather"}
+ assert "get_weather" not in recorded[1]
+
+
+async def test_add_tools_does_not_mutate_caller_tools_list(chat_client_base: SupportsChatGetResponse):
+ @tool(name="load_math", approval_mode="never_require")
+ def load_math(ctx: FunctionInvocationContext) -> str:
+ ctx.add_tools(_pte_factorial)
+ return "loaded"
+
+ original_tools: list[Any] = [load_math]
+ chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
+ chat_client_base.run_responses = [
+ _pte_function_call_response("1", "load_math"),
+ _pte_text_response(),
+ ]
+ await chat_client_base.get_response(
+ [Message(role="user", contents=["hi"])],
+ options={"tool_choice": "auto", "tools": original_tools},
+ )
+ assert original_tools == [load_math]
+
+
+async def test_add_tools_persists_across_iterations(chat_client_base: SupportsChatGetResponse):
+ from agent_framework import FunctionTool
+
+ recorded: list[list[str]] = []
+ client_cls = type(chat_client_base)
+ original = client_cls._get_non_streaming_response
+
+ async def recording(self: Any, *, messages: Any, options: dict[str, Any], **kwargs: Any) -> ChatResponse:
+ tools = options.get("tools") or []
+ recorded.append([t.name for t in tools if isinstance(t, FunctionTool)])
+ return await original(self, messages=messages, options=options, **kwargs)
+
+ @tool(name="load_math", approval_mode="never_require")
+ def load_math(ctx: FunctionInvocationContext) -> str:
+ ctx.add_tools(_pte_factorial)
+ return "loaded"
+
+ chat_client_base.function_invocation_configuration["max_iterations"] = 4 # type: ignore[attr-defined]
+ chat_client_base.run_responses = [
+ _pte_function_call_response("1", "load_math"),
+ _pte_function_call_response("2", "factorial", '{"n": 5}'),
+ _pte_function_call_response("3", "factorial", '{"n": 3}'),
+ _pte_text_response(),
+ ]
+ monkey = pytest.MonkeyPatch()
+ monkey.setattr(client_cls, "_get_non_streaming_response", recording)
+ try:
+ await chat_client_base.get_response(
+ [Message(role="user", contents=["hi"])],
+ options={"tool_choice": "auto", "tools": [load_math]},
+ )
+ finally:
+ monkey.undo()
+
+ assert "factorial" in recorded[1]
+ assert "factorial" in recorded[2]
+
+
+async def test_add_tools_through_function_middleware(chat_client_base: SupportsChatGetResponse):
+ exec_counter = 0
+
+ class PassthroughMiddleware(FunctionMiddleware):
+ async def process(self, context: FunctionInvocationContext, call_next: Any) -> None:
+ await call_next()
+
+ @tool(name="factorial", approval_mode="never_require")
+ def factorial(n: int) -> int:
+ nonlocal exec_counter
+ exec_counter += 1
+ return 120
+
+ @tool(name="load_math", approval_mode="never_require")
+ def load_math(ctx: FunctionInvocationContext) -> str:
+ ctx.add_tools(factorial)
+ return "loaded"
+
+ chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
+ chat_client_base.run_responses = [
+ _pte_function_call_response("1", "load_math"),
+ _pte_function_call_response("2", "factorial", '{"n": 5}'),
+ _pte_text_response(),
+ ]
+ await chat_client_base.get_response(
+ [Message(role="user", contents=["hi"])],
+ options={"tool_choice": "auto", "tools": [load_math]},
+ middleware=[PassthroughMiddleware()],
+ )
+ assert exec_counter == 1
+
+
+async def test_add_tools_with_approval_required_tool(chat_client_base: SupportsChatGetResponse):
+ @tool(name="secure_tool", approval_mode="always_require")
+ def secure_tool(value: str) -> str:
+ return f"secure: {value}"
+
+ @tool(name="load_secure", approval_mode="never_require")
+ def load_secure(ctx: FunctionInvocationContext) -> str:
+ ctx.add_tools(secure_tool)
+ return "loaded"
+
+ chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
+ chat_client_base.run_responses = [
+ _pte_function_call_response("1", "load_secure"),
+ _pte_function_call_response("2", "secure_tool", '{"value": "x"}'),
+ _pte_text_response(),
+ ]
+ response = await chat_client_base.get_response(
+ [Message(role="user", contents=["hi"])],
+ options={"tool_choice": "auto", "tools": [load_secure]},
+ )
+ assert any(item.type == "function_approval_request" for msg in response.messages for item in msg.contents)
+
+
+async def test_add_tools_accepts_plain_callable(chat_client_base: SupportsChatGetResponse):
+ exec_counter = 0
+
+ def plain_factorial(n: int) -> int:
+ """Compute factorial."""
+ nonlocal exec_counter
+ exec_counter += 1
+ return 120
+
+ @tool(name="load_math", approval_mode="never_require")
+ def load_math(ctx: FunctionInvocationContext) -> str:
+ ctx.add_tools(plain_factorial)
+ return "loaded"
+
+ chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
+ chat_client_base.run_responses = [
+ _pte_function_call_response("1", "load_math"),
+ _pte_function_call_response("2", "plain_factorial", '{"n": 5}'),
+ _pte_text_response(),
+ ]
+ await chat_client_base.get_response(
+ [Message(role="user", contents=["hi"])],
+ options={"tool_choice": "auto", "tools": [load_math]},
+ )
+ assert exec_counter == 1
+
+
+async def test_add_tools_streaming(chat_client_base: SupportsChatGetResponse):
+ exec_counter = 0
+
+ @tool(name="factorial", approval_mode="never_require")
+ def factorial(n: int) -> int:
+ nonlocal exec_counter
+ exec_counter += 1
+ return 120
+
+ @tool(name="load_math", approval_mode="never_require")
+ def load_math(ctx: FunctionInvocationContext) -> str:
+ ctx.add_tools(factorial)
+ return "loaded"
+
+ chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
+ chat_client_base.streaming_responses = [
+ [
+ ChatResponseUpdate(
+ contents=[Content.from_function_call(call_id="1", name="load_math", arguments="{}")],
+ role="assistant",
+ )
+ ],
+ [
+ ChatResponseUpdate(
+ contents=[Content.from_function_call(call_id="2", name="factorial", arguments='{"n": 5}')],
+ role="assistant",
+ )
+ ],
+ [ChatResponseUpdate(contents=[Content.from_text("done")], role="assistant", finish_reason="stop")],
+ ]
+ async for _ in chat_client_base.get_response(
+ [Message(role="user", contents=["hi"])],
+ stream=True,
+ options={"tool_choice": "auto", "tools": [load_math]},
+ ):
+ pass
+ assert exec_counter == 1
+
+
+def test_add_tools_duplicate_same_object_is_noop():
+ @tool(name="dup", approval_mode="never_require")
+ def dup(x: int) -> int:
+ return x
+
+ ctx = FunctionInvocationContext(function=dup, arguments={}, tools=[dup])
+ ctx.add_tools(dup)
+ assert ctx.tools is not None
+ assert len(ctx.tools) == 1
+
+
+def test_add_tools_duplicate_name_different_object_raises():
+ @tool(name="dup", approval_mode="never_require")
+ def dup_a(x: int) -> int:
+ return x
+
+ @tool(name="dup", approval_mode="never_require")
+ def dup_b(x: int) -> int:
+ return x
+
+ ctx = FunctionInvocationContext(function=dup_a, arguments={}, tools=[dup_a])
+ with pytest.raises(ValueError):
+ ctx.add_tools(dup_b)
+
+
+def test_add_tools_batch_with_duplicate_is_atomic():
+ """A duplicate-name clash partway through a batch must leave the live list unchanged."""
+
+ @tool(name="existing", approval_mode="never_require")
+ def existing(x: int) -> int:
+ return x
+
+ @tool(name="fresh", approval_mode="never_require")
+ def fresh(x: int) -> int:
+ return x
+
+ @tool(name="existing", approval_mode="never_require")
+ def clashing(x: int) -> int:
+ return x
+
+ ctx = FunctionInvocationContext(function=existing, arguments={}, tools=[existing])
+ with pytest.raises(ValueError):
+ ctx.add_tools([fresh, clashing])
+ assert ctx.tools is not None
+ # The valid "fresh" tool must not have been committed before the clash raised.
+ assert ctx.tools == [existing]
+
+
+def test_remove_tools_by_name_and_object():
+ @tool(name="a", approval_mode="never_require")
+ def a(x: int) -> int:
+ return x
+
+ @tool(name="b", approval_mode="never_require")
+ def b(x: int) -> int:
+ return x
+
+ ctx = FunctionInvocationContext(function=a, arguments={}, tools=[a, b])
+ ctx.remove_tools("a")
+ assert ctx.tools is not None
+ assert [t.name for t in ctx.tools] == ["b"]
+ ctx.remove_tools(b)
+ assert ctx.tools == []
+
+
+def test_remove_tools_unknown_name_is_noop():
+ @tool(name="a", approval_mode="never_require")
+ def a(x: int) -> int:
+ return x
+
+ ctx = FunctionInvocationContext(function=a, arguments={}, tools=[a])
+ ctx.remove_tools("nonexistent")
+ assert ctx.tools is not None
+ assert [t.name for t in ctx.tools] == ["a"]
+
+
+def test_progressive_tools_helpers_raise_without_live_tools():
+ @tool(name="a", approval_mode="never_require")
+ def a(x: int) -> int:
+ return x
+
+ ctx = FunctionInvocationContext(function=a, arguments={})
+ assert ctx.tools is None
+ with pytest.raises(RuntimeError):
+ ctx.add_tools(a)
+ with pytest.raises(RuntimeError):
+ ctx.remove_tools("a")
+
+
+# endregion
diff --git a/python/samples/02-agents/tools/README.md b/python/samples/02-agents/tools/README.md
new file mode 100644
index 0000000000..ad07d86ecd
--- /dev/null
+++ b/python/samples/02-agents/tools/README.md
@@ -0,0 +1,75 @@
+# Tools
+
+Samples that show how to define, configure, and control function tools for an
+agent — from basic declarations to approvals, invocation limits, session
+injection, and dynamic (progressive) tool exposure.
+
+## Function tools
+
+| File | Demonstrates |
+|------|--------------|
+| [`function_tool_with_explicit_schema.py`](function_tool_with_explicit_schema.py) | Defining a tool with an explicit JSON schema. |
+| [`function_tool_declaration_only.py`](function_tool_declaration_only.py) | A declaration-only tool (schema without a local implementation). |
+| [`function_tool_with_kwargs.py`](function_tool_with_kwargs.py) | Passing extra keyword arguments into a tool. |
+| [`function_tool_from_dict_with_dependency_injection.py`](function_tool_from_dict_with_dependency_injection.py) | Dependency injection into a tool defined from a dict. |
+| [`function_tool_with_session_injection.py`](function_tool_with_session_injection.py) | Injecting the session into a tool. |
+| [`tool_in_class.py`](tool_in_class.py) | Using a method on a class as a tool. |
+| [`agent_as_tool_with_session_propagation.py`](agent_as_tool_with_session_propagation.py) | Exposing an agent as a tool with session propagation. |
+
+## Approvals & invocation control
+
+| File | Demonstrates |
+|------|--------------|
+| [`function_tool_with_approval.py`](function_tool_with_approval.py) | Requiring human approval before a tool runs. |
+| [`function_tool_with_approval_and_sessions.py`](function_tool_with_approval_and_sessions.py) | Tool approvals combined with sessions. |
+| [`function_invocation_configuration.py`](function_invocation_configuration.py) | Configuring function-invocation settings (e.g. max iterations). |
+| [`control_total_tool_executions.py`](control_total_tool_executions.py) | All the ways to cap how many times tools run. |
+| [`function_tool_with_max_invocations.py`](function_tool_with_max_invocations.py) | Limiting the number of invocations per tool. |
+| [`function_tool_with_max_exceptions.py`](function_tool_with_max_exceptions.py) | Limiting the number of exceptions a tool may raise. |
+| [`function_tool_recover_from_failures.py`](function_tool_recover_from_failures.py) | Returning errors so the agent can recover from tool failures. |
+
+## Progressive tool exposure (dynamic loading)
+
+| File | Demonstrates |
+|------|--------------|
+| [`dynamic_tool_exposure.py`](dynamic_tool_exposure.py) | A "loader" tool that adds more tools at runtime via `FunctionInvocationContext`. |
+
+Frontloading a model with hundreds of tools hurts tool-selection accuracy,
+bloats context, and raises cost. Instead, start with a small set of loader
+tools and let the model pull in more on demand. Inside a tool, the injected
+`ctx: FunctionInvocationContext` exposes a live `ctx.tools` list plus
+`ctx.add_tools(...)` / `ctx.remove_tools(...)` helpers. Tools added or removed
+take effect on the **next iteration** of the function-calling loop.
+
+> [!NOTE]
+> Progressive tool exposure applies to the standard function-calling loop. It
+> does **not** apply to CodeAct providers (`agent-framework-monty`,
+> `agent-framework-hyperlight`). In CodeAct the model only sees a single
+> `execute_code` tool, and host tools are exposed *inside the sandbox* as typed
+> Python functions rather than as model tool-schemas. Host tools there are
+> invoked without a `FunctionInvocationContext`, so `ctx.add_tools()` is not
+> available; the helpers fail fast with a clear `RuntimeError` instead of
+> silently doing nothing. To change a CodeAct agent's tool set, use the
+> provider's own `add_tools` / `remove_tool` / `clear_tools` methods (applied
+> between runs). The recommended provider-driven path for Monty and Hyperlight
+> is shown in [`../context_providers/code_act/`](../context_providers/code_act/)
+> ([`code_act.py`](../context_providers/code_act/code_act.py) for Hyperlight,
+> [`monty_code_act.py`](../context_providers/code_act/monty_code_act.py) for
+> Monty).
+
+## Local shell & code interpreters
+
+| Path | Demonstrates |
+|------|--------------|
+| [`local_shell_with_allowlist.py`](local_shell_with_allowlist.py) | `LocalShellTool` restricted by a strict command allow-list. |
+| [`local_shell_with_environment_provider.py`](local_shell_with_environment_provider.py) | `LocalShellTool` wired with a `ShellEnvironmentProvider`. |
+| [`local_code_interpreter/`](local_code_interpreter/) | Hyperlight-backed sandboxed code interpreter (standalone tool — *extra* pattern). |
+| [`monty_code_interpreter/`](monty_code_interpreter/) | Monty-backed sandboxed code interpreter (standalone tool — *extra* pattern). |
+
+> [!TIP]
+> The `local_code_interpreter/` and `monty_code_interpreter/` samples show the
+> standalone-tool wiring and are provided as *extra* reference. For most
+> Monty/Hyperlight use cases the **recommended** path is the provider-driven
+> CodeAct setup in
+> [`../context_providers/code_act/`](../context_providers/code_act/), which adds
+> dynamic tool / capability management.
diff --git a/python/samples/02-agents/tools/dynamic_tool_exposure.py b/python/samples/02-agents/tools/dynamic_tool_exposure.py
new file mode 100644
index 0000000000..2886399486
--- /dev/null
+++ b/python/samples/02-agents/tools/dynamic_tool_exposure.py
@@ -0,0 +1,79 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import asyncio
+from typing import Annotated
+
+from agent_framework import Agent, FunctionInvocationContext, tool
+from agent_framework.openai import OpenAIChatClient
+from dotenv import load_dotenv
+from pydantic import Field
+
+# Load environment variables from .env file
+load_dotenv()
+
+"""
+Dynamic Tool Exposure (Progressive Tool Loading) Example
+
+This example demonstrates "progressive tool exposure": a tool that adds more tools to
+the agent at runtime, in the same run, via ``FunctionInvocationContext``.
+
+Frontloading a model with hundreds of tools hurts tool-selection accuracy, bloats
+context, and raises cost. Instead, you can start with a small set of "loader" tools and
+let the model pull in additional tools on demand. Tools added with ``ctx.add_tools(...)``
+(or removed with ``ctx.remove_tools(...)``) become available to the model on the next
+iteration of the function-calling loop.
+"""
+
+
+# These math tools are not registered on the agent up front. They are added on demand by
+# the ``load_math_tools`` tool below, and only then become callable by the model.
+@tool(approval_mode="never_require")
+def factorial(n: Annotated[int, Field(description="A non-negative integer.")]) -> str:
+ """Compute the factorial of n."""
+ if n < 0:
+ return "Error: n must be a non-negative integer."
+ result = 1
+ for value in range(2, n + 1):
+ result *= value
+ return f"{n}! = {result}"
+
+
+@tool(approval_mode="never_require")
+def fibonacci(n: Annotated[int, Field(description="The 0-based index in the Fibonacci sequence.")]) -> str:
+ """Compute the n-th Fibonacci number."""
+ if n < 0:
+ return "Error: n must be a non-negative integer."
+ a, b = 0, 1
+ for _ in range(n):
+ a, b = b, a + b
+ return f"fib({n}) = {a}"
+
+
+# The only tool the agent starts with. When called, it exposes the math tools above so the
+# model can use them on the next turn. Note the ``ctx`` parameter is injected by the
+# framework and is not visible to the model.
+@tool(approval_mode="never_require")
+def load_math_tools(ctx: FunctionInvocationContext) -> str:
+ """Load additional math tools (factorial, fibonacci) so they can be used."""
+ ctx.add_tools([factorial, fibonacci])
+ return "Loaded math tools: factorial, fibonacci. You can now call them."
+
+
+async def main() -> None:
+ agent = Agent(
+ client=OpenAIChatClient(),
+ name="MathAgent",
+ instructions=(
+ "You are a math assistant. If you need math capabilities that are not yet "
+ "available, call load_math_tools first, then use the newly available tools."
+ ),
+ tools=[load_math_tools],
+ )
+
+ # The agent starts with only ``load_math_tools``. To answer the question it must first
+ # load the math tools, then call ``factorial`` on the next iteration.
+ print(f"Agent: {await agent.run('What is 5 factorial?')}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/python/uv.lock b/python/uv.lock
index 1f816411e3..676413c1cc 100644
--- a/python/uv.lock
+++ b/python/uv.lock
@@ -562,7 +562,7 @@ requires-dist = [
{ name = "agent-framework-core", editable = "packages/core" },
{ name = "azure-ai-agentserver-core", specifier = ">=2.0.0b3,<3" },
{ name = "azure-ai-agentserver-invocations", specifier = ">=1.0.0b3,<2" },
- { name = "azure-ai-agentserver-responses", specifier = ">=1.0.0b5,<2" },
+ { name = "azure-ai-agentserver-responses", specifier = ">=1.0.0b7,<2" },
]
[[package]]
@@ -1171,19 +1171,18 @@ wheels = [
[[package]]
name = "azure-ai-agentserver-core"
-version = "2.0.0b3"
+version = "2.0.0b5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "azure-monitor-opentelemetry-exporter", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "hypercorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "microsoft-opentelemetry", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
- { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/84/29/1a9606d5252b02d77070a1b633dd0c26fe65a0f4a0fb0cfdaa751e2ed458/azure_ai_agentserver_core-2.0.0b3.tar.gz", hash = "sha256:e295b19a65d53c513929f52f0862bbb815cc9e9fc29d2a2825452f3136260123", size = 42573, upload-time = "2026-04-23T04:13:16.717Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a5/06/7c88b6506d26ee625a967cef762e6a155ed7ab8812f3f1e45ec1a950b8ae/azure_ai_agentserver_core-2.0.0b5.tar.gz", hash = "sha256:f03dc737351e5d847e9fc18c5b78b261436de368f1317a0c29957cc2179c37d1", size = 46273, upload-time = "2026-05-25T12:48:01.739Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7f/9b/1fc87c05b55821f33c46c5e8a3b97a573aa2fc4bff387e75cca1a87800b4/azure_ai_agentserver_core-2.0.0b3-py3-none-any.whl", hash = "sha256:5ef921eb9fd9c0f15682fe930320fae50dccfa915d7518f9a16d99014bbcb3cb", size = 29127, upload-time = "2026-04-23T04:13:17.976Z" },
+ { url = "https://files.pythonhosted.org/packages/68/80/a43a269601512793b220c36dc0864b44d806b969dbfe14f1ecc3b5f5202b/azure_ai_agentserver_core-2.0.0b5-py3-none-any.whl", hash = "sha256:0d00c298892e2ff466b32235d5d9c55b57054f0e8fcedb0726eacd7684e1aa89", size = 31521, upload-time = "2026-05-25T12:48:03.072Z" },
]
[[package]]
@@ -1200,7 +1199,7 @@ wheels = [
[[package]]
name = "azure-ai-agentserver-responses"
-version = "1.0.0b5"
+version = "1.0.0b7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -1208,9 +1207,9 @@ dependencies = [
{ name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e6/27/3ecb7fe704ff8764199bfbe4cc1e584a520a9affe042470d9d50b6e1e73a/azure_ai_agentserver_responses-1.0.0b5.tar.gz", hash = "sha256:0b627b810359c792ea7b6fa6782abaf6df32d9bc9e5a569ad722afcffd0ce8d9", size = 410908, upload-time = "2026-04-23T04:31:15.414Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/53/febb6f3453f5dc1e0b6dc47d4e5198b64605d1f83c847255946f74bc300e/azure_ai_agentserver_responses-1.0.0b7.tar.gz", hash = "sha256:2f67cdfc0219cb0ab86800dadb1cfdb40ab4aa0413dae7ffa5ea4ea84eec3eb0", size = 419032, upload-time = "2026-05-25T12:48:38.81Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/44/91/1e5c0d7ce95ca8b022e69e4ca6b23e413fc2d57f0191429c4633e02213d2/azure_ai_agentserver_responses-1.0.0b5-py3-none-any.whl", hash = "sha256:4c2a6ab56e71eeb330aa52b7cb2cc71b8ec6b5bbe0e7dc84310f2c7fbda393a3", size = 268362, upload-time = "2026-04-23T04:31:17.014Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/94/48825357e009f7db3b6b5d0a9344a7ab3304e32f06f50328b2393e3b06cb/azure_ai_agentserver_responses-1.0.0b7-py3-none-any.whl", hash = "sha256:efb5271f24a297bacde9769359308e54e870f66ad4d3b4826ae97a77e40e94d4", size = 268063, upload-time = "2026-05-25T12:48:40.817Z" },
]
[[package]]
@@ -3887,6 +3886,41 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/1b/543ddaa2daf8593911a02a07a6a78366d4a6a0053ec86a557c19fa97b60e/microsoft_agents_hosting_core-0.3.1-py3-none-any.whl", hash = "sha256:a4b41556b15321b74f539c5a0a89f70955459b7ec57e9e4b24e61bba27f1cbbc", size = 94573, upload-time = "2025-09-09T23:19:53.855Z" },
]
+[[package]]
+name = "microsoft-opentelemetry"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "azure-core-tracing-opentelemetry", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "azure-monitor-opentelemetry-exporter", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-exporter-otlp-proto-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation-django", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation-fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation-flask", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation-httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation-logging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation-openai-agents-v2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation-openai-v2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation-psycopg2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation-requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation-urllib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation-urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-resource-detector-azure", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-util-genai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "pyjwt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a2/cf/74885d07d38e225b84b63a8a2720de846e518fe4c7e89457f4c150a9c7d5/microsoft_opentelemetry-1.3.2.tar.gz", hash = "sha256:d36f31731740170624b53f370358a9700f503bb4f9bd25c7f81c0c88c66f511c", size = 178031, upload-time = "2026-05-29T22:05:53.442Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/8d/6960be61c8fe236fef730b0cae1d97a1898f62355b2d6679ef46abe1e4be/microsoft_opentelemetry-1.3.2-py3-none-any.whl", hash = "sha256:65292474ce7efee115f671457188e92edc4a8d432fad163e49e504155be66ae5", size = 198419, upload-time = "2026-05-29T22:05:54.849Z" },
+]
+
[[package]]
name = "mistralai"
version = "2.4.2"
@@ -4633,6 +4667,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/41/619f3530324a58491f2d20f216a10dd7393629b29db4610dda642a27f4ed/opentelemetry_instrumentation_flask-0.61b0-py3-none-any.whl", hash = "sha256:e8ce474d7ce543bfbbb3e93f8a6f8263348af9d7b45502f387420cf3afa71253", size = 15996, upload-time = "2026-03-04T14:19:31.304Z" },
]
+[[package]]
+name = "opentelemetry-instrumentation-httpx"
+version = "0.61b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-util-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cd/2a/e2becd55e33c29d1d9ef76e2579040ed1951cb33bacba259f6aff2fdd2a6/opentelemetry_instrumentation_httpx-0.61b0.tar.gz", hash = "sha256:6569ec097946c5551c2a4252f74c98666addd1bf047c1dde6b4ef426719ff8dd", size = 24104, upload-time = "2026-03-04T14:20:34.752Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/88/dde310dce56e2d85cf1a09507f5888544955309edc4b8d22971d6d3d1417/opentelemetry_instrumentation_httpx-0.61b0-py3-none-any.whl", hash = "sha256:dee05c93a6593a5dc3ae5d9d5c01df8b4e2c5d02e49275e5558534ee46343d5e", size = 17198, upload-time = "2026-03-04T14:19:33.585Z" },
+]
+
[[package]]
name = "opentelemetry-instrumentation-logging"
version = "0.61b0"
@@ -4646,6 +4696,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/0e/2137db5239cc5e564495549a4d11488a7af9b48fc76520a0eea20e69ddae/opentelemetry_instrumentation_logging-0.61b0-py3-none-any.whl", hash = "sha256:6d87e5ded6a0128d775d41511f8380910a1b610671081d16efb05ac3711c0074", size = 17076, upload-time = "2026-03-04T14:19:36.765Z" },
]
+[[package]]
+name = "opentelemetry-instrumentation-openai-agents-v2"
+version = "0.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-util-genai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/00/15/b6a303454d2800d772cdebc490c1d598d06d0e541619db80195eb9ea85c6/opentelemetry_instrumentation_openai_agents_v2-0.1.0.tar.gz", hash = "sha256:1033f4b261ce07f65d197ac0e9c499302c805eae987a6cc4e7f99bb279363477", size = 22423, upload-time = "2025-10-15T19:04:59.912Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cd/0a/b6f47734e1d7f936cbc52ef8e673d3e08d9c3c8a13d9549c03f978758076/opentelemetry_instrumentation_openai_agents_v2-0.1.0-py3-none-any.whl", hash = "sha256:e4e3dfba32bd6eeee0624eca9be54341ab7cc4f7a3bb895354f2f9d6f7afe2f3", size = 25002, upload-time = "2025-10-15T19:04:58.562Z" },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-openai-v2"
+version = "2.3b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/4e/21f8cd16ccb471dd217ed85eb817796a10c4f2718ae2c91e752a57180cf0/opentelemetry_instrumentation_openai_v2-2.3b0.tar.gz", hash = "sha256:5de9d70cc9536eea1fe48ea016e0c5f25735fa9a13709076a64b20657fadb6ba", size = 170838, upload-time = "2025-12-24T13:20:58.33Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/02/7ff0a9282520592772a356dd39d1559f3726610ccc3854a2f598b756c66f/opentelemetry_instrumentation_openai_v2-2.3b0-py3-none-any.whl", hash = "sha256:c6aca87be0da0289ea1d8167fea4b0f227ea5ef0e90496e2822121e47340d36a", size = 18053, upload-time = "2025-12-24T13:20:57.233Z" },
+]
+
[[package]]
name = "opentelemetry-instrumentation-psycopg2"
version = "0.61b0"
@@ -4772,6 +4851,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" },
]
+[[package]]
+name = "opentelemetry-util-genai"
+version = "0.3b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a2/d8/4dd2fb622d26ec45b10ef63eb87fd512f5d7467c7bd35ce390629bd6dff8/opentelemetry_util_genai-0.3b0.tar.gz", hash = "sha256:83e127789a9ad615b8ca65f05fc36955a67ce257b06142bfd46159a3b7ed73d3", size = 31800, upload-time = "2026-02-20T16:16:14.807Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/e5/fada54909e445d7b4007f8b96221d571999efeab9446f3127cc1cebe5e07/opentelemetry_util_genai-0.3b0-py3-none-any.whl", hash = "sha256:ebc2b01bcb891ddc7218452470d189d3321cd742653299ff8e7de45debcfb986", size = 28426, upload-time = "2026-02-20T16:16:12.027Z" },
+]
+
[[package]]
name = "opentelemetry-util-http"
version = "0.61b0"