diff --git a/docs/specs/004-python-function-calling-loop.md b/docs/specs/004-python-function-calling-loop.md
index 71dc8f59140..dc57a607cd6 100644
--- a/docs/specs/004-python-function-calling-loop.md
+++ b/docs/specs/004-python-function-calling-loop.md
@@ -500,6 +500,15 @@ that manually replay messages own the equivalent rule: do not resend an approval
approval `Message`, approval `Content`, or an earlier returned response.
- Approval-time `UserInputRequiredException` and `MiddlewareTermination` return immediately without another model
call.
+- `Agent.as_tool()` keeps child function approvals inside the delegated invocation. A child
+ `ToolApprovalMiddleware` may resolve them through runtime `auto_approval_rules`; any unresolved child function
+ approval does not enter the caller's approval state or model transcript. An approval-only response fails the
+ agent-tool invocation; a mixed response preserves its non-approval user-input requests while discarding the child
+ approval continuation. Interactive, delayed, or durable approval belongs in a workflow. When
+ `propagate_session=True`, child application-state changes merge back into the parent while framework approval and
+ invocation-budget state remain isolated, including approval queues stored under custom child middleware
+ `source_id` values. Parent and child `ToolApprovalMiddleware` instances must use distinct `source_id` values; an
+ overlap fails before the child runs.
### Approval control content
@@ -578,6 +587,7 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Mixed approved/rejected batch | Every call gets one correctly correlated terminal result. | `packages/core/tests/core/test_function_invocation_logic.py::test_rejected_approval` |
| Persisted approval replay | Resume executes with the prior call available. | `test_persisted_approval_messages_replay_correctly` |
| Hosted approval pass-through | Hosted requests/responses are bound to the recorded provider request and are not processed as local calls. | `test_hosted_tool_approval_response`, `test_hosted_mcp_approval_response_passthrough`, `test_session_approval_binding_reconstructs_hosted_response`, `test_mixed_local_and_hosted_approval_flow` |
+| Agent-tool child approval | A child `ToolApprovalMiddleware` can auto-approve runtime tool requests inside one delegated invocation; unresolved child approvals execute nothing and do not enter the caller's approval state or dispatch a same-named parent tool, including with propagated application state. Mixed batches preserve non-approval user-input requests. Shared parent and child state rejects overlapping `ToolApprovalMiddleware.source_id` values before running the child; parent approval state is neither visible nor mutable inside the child, and custom child approval queues do not leak into later delegations. | `packages/core/tests/core/test_agents.py::test_chat_agent_as_tool_auto_approves_child_tool_with_middleware`, `test_chat_agent_as_tool_fails_closed_for_unresolved_child_approval`, `test_chat_agent_as_tool_child_approval_does_not_dispatch_same_named_parent_tool`, `test_chat_agent_as_tool_preserves_non_approval_requests_from_mixed_child_batch`, `test_chat_agent_as_tool_shared_session_requires_distinct_tool_approval_source_ids`, `test_chat_agent_as_tool_isolates_custom_parent_approval_state`, `test_chat_agent_as_tool_approved_delegation_does_not_confuse_framework_approval_state`, `test_chat_agent_as_tool_does_not_restore_custom_approval_queue_on_fresh_delegation` |
| Approval-time user input | Every user-input request from one approved execution returns in order with assistant role and no extra model call; the execution consumes one call-budget unit. | `packages/core/tests/core/test_harness_tool_approval.py::test_approval_resume_returns_all_user_input_requests_without_another_model_call`, `packages/core/tests/core/test_function_invocation_logic.py::test_approval_resume_user_input_counts_toward_function_call_budget` |
| Mixed terminal result and follow-up input | Completed siblings remain tool-role while only follow-up input requests use assistant-role messages/updates. | `packages/core/tests/core/test_function_invocation_logic.py::test_approval_resume_separates_terminal_results_from_follow_up_requests`, `packages/openai/tests/openai/test_openai_chat_completion_client.py::test_mixed_approval_resume_roles_serialize_function_result_as_tool`, `packages/core/tests/core/test_harness_tool_approval.py::test_dynamic_policy_approval_partitions_safe_sibling_result_roles` |
| Approval-time middleware termination | Terminal result returns with no extra model call in either response mode. | `packages/core/tests/core/test_function_invocation_logic.py::test_approval_resume_honors_middleware_termination` |
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 4a37e210fb6..f429405f190 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -109,7 +109,7 @@
-
+
diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props
index 60e890e60c9..dfeb2feba25 100644
--- a/dotnet/nuget/nuget-package.props
+++ b/dotnet/nuget/nuget-package.props
@@ -1,14 +1,14 @@
- 1.21.0
+ 1.22.0
1
- 260911
+ 260918
$(VersionPrefix)-rc$(RCNumber)
$(VersionPrefix)-$(VersionSuffix).$(DateSuffix).1
$(VersionPrefix)-preview.$(DateSuffix).1
$(VersionPrefix)
- 1.21.0
+ $(VersionPrefix)
Debug;Release;Publish
true
diff --git a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs
index 11b0dbc62bf..5950cf1debd 100644
--- a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs
@@ -188,7 +188,7 @@ protected override async IAsyncEnumerable RunCoreStreamingA
{
copilotSession = await this._copilotClient.ResumeSessionAsync(
typedSession.SessionId,
- this.CreateResumeConfig(),
+ ToResumeSessionConfig(this._sessionConfig),
cancellationToken).ConfigureAwait(false);
}
else
@@ -306,11 +306,6 @@ private async Task EnsureClientStartedAsync(CancellationToken cancellationToken)
await this._copilotClient.StartAsync(cancellationToken).ConfigureAwait(false);
}
- private ResumeSessionConfig CreateResumeConfig()
- {
- return CopyResumeSessionConfig(this._sessionConfig);
- }
-
///
/// Copies all supported properties from a source into a new instance,
/// preserving from the source (defaulting to true if unset).
@@ -323,34 +318,96 @@ internal static SessionConfig CopySessionConfig(SessionConfig source)
}
///
- /// Copies all supported properties from a source into a new
+ /// Converts a source into a new
/// , preserving
/// from the source (defaulting to true if unset).
///
- internal static ResumeSessionConfig CopyResumeSessionConfig(SessionConfig? source)
+ internal static ResumeSessionConfig ToResumeSessionConfig(SessionConfig? source)
{
+ // ResumeSessionConfig is a separate SDK type, so resumed sessions need every shared setting projected explicitly.
return new ResumeSessionConfig
{
- Model = source?.Model,
- ReasoningEffort = source?.ReasoningEffort,
- ReasoningSummary = source?.ReasoningSummary,
- ContextTier = source?.ContextTier,
- Tools = source?.Tools,
- SystemMessage = source?.SystemMessage,
+ AdditionalDirectories = source?.AdditionalDirectories,
+ Agent = source?.Agent,
+ AskUserVariant = source?.AskUserVariant,
+ AuthClientIdMetadataUrl = source?.AuthClientIdMetadataUrl,
AvailableTools = source?.AvailableTools,
- ExcludedTools = source?.ExcludedTools,
- Provider = source?.Provider,
- OnPermissionRequest = source?.OnPermissionRequest,
- OnUserInputRequest = source?.OnUserInputRequest,
- Hooks = source?.Hooks,
- WorkingDirectory = source?.WorkingDirectory,
+ CanvasHandler = source?.CanvasHandler,
+ CanvasProvider = source?.CanvasProvider,
+ Canvases = source?.Canvases,
+ Capi = source?.Capi,
+ ClientName = source?.ClientName,
+ CoauthorEnabled = source?.CoauthorEnabled,
+ Commands = source?.Commands,
ConfigDirectory = source?.ConfigDirectory,
- McpServers = source?.McpServers,
+ ContextTier = source?.ContextTier,
+ CreateSessionFsProvider = source?.CreateSessionFsProvider,
CustomAgents = source?.CustomAgents,
- SkillDirectories = source?.SkillDirectories,
+ CustomAgentsLocalOnly = source?.CustomAgentsLocalOnly,
+ DefaultAgent = source?.DefaultAgent,
+ DisabledMcpServers = source?.DisabledMcpServers,
DisabledSkills = source?.DisabledSkills,
+ EmbeddingCacheStorage = source?.EmbeddingCacheStorage,
+ EnableCitations = source?.EnableCitations,
+ EnableConfigDiscovery = source?.EnableConfigDiscovery,
+ EnableExperimentalMode = source?.EnableExperimentalMode,
+ EnableFileChangeTracking = source?.EnableFileChangeTracking,
+ EnableFileHooks = source?.EnableFileHooks,
+ EnableHostGitOperations = source?.EnableHostGitOperations,
+ EnableManagedSettings = source?.EnableManagedSettings,
+ EnableMcpApps = source?.EnableMcpApps ?? default,
+ EnableOnDemandInstructionDiscovery = source?.EnableOnDemandInstructionDiscovery,
+ EnableSessionStore = source?.EnableSessionStore,
+ EnableSessionTelemetry = source?.EnableSessionTelemetry,
+ EnableSkills = source?.EnableSkills,
+ ExcludedBuiltInAgents = source?.ExcludedBuiltInAgents,
+ ExcludedTools = source?.ExcludedTools,
+ ExpAssignments = source?.ExpAssignments,
+ ExtensionInfo = source?.ExtensionInfo,
+ ExtensionSdkPath = source?.ExtensionSdkPath,
+ FeatureFlags = source?.FeatureFlags,
+ GitHubMcpToolConfig = source?.GitHubMcpToolConfig,
+ GitHubToken = source?.GitHubToken,
+ GitHubTokenProvider = source?.GitHubTokenProvider,
+ Hooks = source?.Hooks,
+ IncludeSubAgentStreamingEvents = source?.IncludeSubAgentStreamingEvents ?? default,
+ IncludedBuiltinSkills = source?.IncludedBuiltinSkills,
InfiniteSessions = source?.InfiniteSessions,
- Streaming = source?.Streaming ?? true
+ InstructionDirectories = source?.InstructionDirectories,
+ LargeOutput = source?.LargeOutput,
+ ManagedSettings = source?.ManagedSettings,
+ ManageScheduleEnabled = source?.ManageScheduleEnabled,
+ McpOAuthTokenStorage = source?.McpOAuthTokenStorage,
+ McpServers = source?.McpServers,
+ Memory = source?.Memory,
+ Model = source?.Model,
+ ModelCapabilities = source?.ModelCapabilities,
+ Models = source?.Models,
+ OnAutoModeSwitchRequest = source?.OnAutoModeSwitchRequest,
+ OnElicitationRequest = source?.OnElicitationRequest,
+ OnEvent = source?.OnEvent,
+ OnExitPlanModeRequest = source?.OnExitPlanModeRequest,
+ OnMcpAuthRequest = source?.OnMcpAuthRequest,
+ OnPermissionRequest = source?.OnPermissionRequest,
+ OnUserInputRequest = source?.OnUserInputRequest,
+ OrganizationCustomInstructions = source?.OrganizationCustomInstructions,
+ PluginDirectories = source?.PluginDirectories,
+ Provider = source?.Provider,
+ Providers = source?.Providers,
+ ReasoningEffort = source?.ReasoningEffort,
+ ReasoningSummary = source?.ReasoningSummary,
+ RemoteSession = source?.RemoteSession,
+ RequestCanvasRenderer = source?.RequestCanvasRenderer,
+ RequestExtensions = source?.RequestExtensions,
+ SessionLimits = source?.SessionLimits,
+ SkillDirectories = source?.SkillDirectories,
+ SkipCustomInstructions = source?.SkipCustomInstructions,
+ SkipEmbeddingRetrieval = source?.SkipEmbeddingRetrieval,
+ Streaming = source?.Streaming ?? true,
+ SystemMessage = source?.SystemMessage,
+ ToolSearch = source?.ToolSearch,
+ Tools = source?.Tools,
+ WorkingDirectory = source?.WorkingDirectory,
};
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Tools.Shell/ShellPolicy.cs b/dotnet/src/Microsoft.Agents.AI.Tools.Shell/ShellPolicy.cs
index 7950ed0674d..1d0d2577066 100644
--- a/dotnet/src/Microsoft.Agents.AI.Tools.Shell/ShellPolicy.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Tools.Shell/ShellPolicy.cs
@@ -141,6 +141,14 @@ public bool Equals(ShellPolicyOutcome other) =>
///
public sealed class ShellPolicy
{
+ ///
+ /// Ceiling on any single pattern match. Policy patterns are operator-authored, but the
+ /// commands they filter are model-generated, so a pattern that backtracks catastrophically
+ /// would stall the authorization path itself. The timeout makes that failure bounded and
+ /// recoverable rather than a hang.
+ ///
+ private static readonly TimeSpan s_patternMatchTimeout = TimeSpan.FromSeconds(1);
+
private readonly IReadOnlyList _denyList;
private readonly IReadOnlyList? _allowList;
private readonly Func? _custom;
@@ -170,11 +178,11 @@ public ShellPolicy(
Func? custom = null)
{
this._denyList = denyList?
- .Select(pattern => new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase))
+ .Select(pattern => new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase, s_patternMatchTimeout))
.ToArray() ?? Array.Empty();
this._allowList = allowList?
- .Select(pattern => new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase))
+ .Select(pattern => new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase, s_patternMatchTimeout))
.ToArray();
this._custom = custom;
@@ -200,15 +208,36 @@ public ShellPolicyOutcome Evaluate(ShellRequest request)
foreach (var deny in this._denyList)
{
- if (deny.IsMatch(command))
+ // A deny pattern that cannot be evaluated in time fails closed: an
+ // unevaluated rule must never read as "this command is fine".
+ try
{
- return ShellPolicyOutcome.Deny($"matched deny pattern: {deny}");
+ if (deny.IsMatch(command))
+ {
+ return ShellPolicyOutcome.Deny($"matched deny pattern: {deny}");
+ }
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ return ShellPolicyOutcome.Deny($"deny pattern could not be evaluated in time: {deny}");
}
}
if (this._allowList is not null)
{
- var matched = this._allowList.Any(allow => allow.IsMatch(command));
+ var matched = this._allowList.Any(allow =>
+ {
+ // A timed-out allow pattern is treated as a non-match, so it can
+ // never be the thing that grants permission.
+ try
+ {
+ return allow.IsMatch(command);
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ return false;
+ }
+ });
if (!matched)
{
return ShellPolicyOutcome.Deny("command does not match allow list");
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs
index fcca066848e..699b98c2e02 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
@@ -134,16 +133,6 @@ internal static IChatClient WithDefaultAgentMiddleware(this IChatClient chatClie
// so FICC emits execute_tool spans on the agent source.
chatBuilder.Use(innerClient => new DeferredOpenTelemetryChatClient(innerClient));
- var agentChatClient = chatBuilder.Build(services);
-
- if (options?.ChatOptions?.Tools is { Count: > 0 })
- {
- // When tools are provided in the constructor, set the tools for the whole lifecycle of the chat client
- var functionService = agentChatClient.GetService();
- Debug.Assert(functionService is not null, "FunctionInvokingChatClient should be registered in the chat client.");
- functionService!.AdditionalTools = options.ChatOptions.Tools;
- }
-
- return agentChatClient;
+ return chatBuilder.Build(services);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs b/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs
index 13f15ff1e9f..5fdadda9264 100644
--- a/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs
@@ -2,7 +2,8 @@
using System;
using System.Collections.Generic;
-using System.Linq;
+using System.Collections.ObjectModel;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -10,7 +11,7 @@
namespace Microsoft.Agents.AI;
///
-/// Internal agent decorator that adds function invocation middleware logic.
+/// Internal agent wrapper that gives callbacks control over function calls.
///
internal sealed class FunctionInvocationDelegatingAgent : DelegatingAIAgent
{
@@ -27,11 +28,12 @@ protected override Task RunCoreAsync(IEnumerable mes
protected override IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
=> this.InnerAgent.RunStreamingAsync(messages, session, this.AgentRunOptionsWithFunctionMiddleware(options), cancellationToken);
- // Decorate options to add the middleware function
- private AgentRunOptions? AgentRunOptionsWithFunctionMiddleware(AgentRunOptions? options)
+ // Work on a per-run copy so adding callback support does not change options that the caller may reuse.
+ private ChatClientAgentRunOptions AgentRunOptionsWithFunctionMiddleware(AgentRunOptions? options)
{
if (options is null || options.GetType() == typeof(AgentRunOptions))
{
+ // Plain agent options cannot hold a chat-client factory, so copy their shared values to chat-specific options.
options = new ChatClientAgentRunOptions()
{
ResponseFormat = options?.ResponseFormat,
@@ -40,6 +42,10 @@ protected override IAsyncEnumerable RunCoreStreamingAsync(I
AdditionalProperties = options?.AdditionalProperties,
};
}
+ else if (options is ChatClientAgentRunOptions chatOptions)
+ {
+ options = chatOptions.Clone();
+ }
if (options is not ChatClientAgentRunOptions aco)
{
@@ -47,42 +53,205 @@ protected override IAsyncEnumerable RunCoreStreamingAsync(I
}
var originalFactory = aco.ChatClientFactory;
- aco.ChatClientFactory = chatClient =>
+ // Apply the original factory before adding our wrapper so even a replacement client keeps these callbacks.
+ aco.ChatClientFactory = chatClient => FunctionMiddlewarePreservingChatClient.Build(chatClient, originalFactory, this);
+
+ return aco;
+ }
+
+ ///
+ /// Preserves the function middleware chain when tools are added or replaced during a run.
+ ///
+ private sealed class FunctionMiddlewarePreservingChatClient(
+ IChatClient innerClient, FunctionInvocationDelegatingAgent[] middlewareChain) : DelegatingChatClient(innerClient)
+ {
+ // Concurrent runs must not combine the callbacks collected while their clients are built.
+ private static readonly AsyncLocal s_buildScope = new();
+ private readonly FunctionInvocationDelegatingAgent[] _middlewareChain = middlewareChain;
+
+ internal static FunctionMiddlewarePreservingChatClient Build(
+ IChatClient chatClient, Func? originalFactory, FunctionInvocationDelegatingAgent middleware)
+ {
+ var previous = s_buildScope.Value;
+ // Nested agent wrappers build one client synchronously. Share their list only within the same run.
+ var scope = previous is not null && ReferenceEquals(previous.RunContext, CurrentRunContext)
+ ? previous
+ : new PipelineBuildScope(CurrentRunContext);
+ // Function wrapping reverses this list, so insert at the front to keep callbacks in registration order.
+ scope.Middleware.Insert(0, middleware);
+ s_buildScope.Value = scope;
+ try
+ {
+ var builder = chatClient.AsBuilder();
+ if (originalFactory is not null)
+ {
+ builder.Use(originalFactory);
+ }
+
+ return new FunctionMiddlewarePreservingChatClient(builder.Build(), [.. scope.Middleware]);
+ }
+ finally
+ {
+ // Restore the previous list so a later client build cannot reuse callbacks from this one.
+ s_buildScope.Value = previous;
+ }
+ }
+
+ public override async Task GetResponseAsync(
+ IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
+ => await this.InnerClient.GetResponseAsync(messages, this.ConfigureOptions(options), cancellationToken).ConfigureAwait(false);
+
+ public override async IAsyncEnumerable GetStreamingResponseAsync(
+ IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- var builder = chatClient.AsBuilder();
+ await foreach (var update in this.InnerClient.GetStreamingResponseAsync(messages, this.ConfigureOptions(options), cancellationToken).ConfigureAwait(false))
+ {
+ yield return update;
+ }
+ }
+
+ private ChatOptions ConfigureOptions(ChatOptions? options)
+ {
+ // Each request gets its own options object, leaving caller-owned options unchanged.
+ options = options?.Clone() ?? new();
+ if (options.Tools is { } tools)
+ {
+ // This collection also wraps functions that are added or replaced later in the same run.
+ options.Tools = new MiddlewareEnabledTools(tools, this._middlewareChain);
+ }
+
+ return options;
+ }
+
+ private sealed class PipelineBuildScope(AgentRunContext? runContext)
+ {
+ internal AgentRunContext? RunContext { get; } = runContext;
+ internal List Middleware { get; } = [];
+ }
+ }
+
+ private sealed class MiddlewareEnabledTools : Collection
+ {
+ internal MiddlewareEnabledTools(IList tools, FunctionInvocationDelegatingAgent[] middleware)
+ {
+ this.MiddlewareChain = middleware;
+ // Add also wraps the functions already present, just as it will wrap functions added later.
+ foreach (var tool in tools)
+ {
+ this.Add(tool);
+ }
+ }
+
+ internal FunctionInvocationDelegatingAgent[] MiddlewareChain { get; }
- if (originalFactory is not null)
+ internal static void ApplyTo(ChatOptions options, FunctionInvocationDelegatingAgent[] middleware)
+ {
+ // Keep the current collection only when it already uses this exact callback list.
+ if (options.Tools is { } tools &&
+ (tools is not MiddlewareEnabledTools existing || !ReferenceEquals(existing.MiddlewareChain, middleware)))
{
- builder.Use(originalFactory);
+ options.Tools = new MiddlewareEnabledTools(tools, middleware);
}
+ }
- return builder.ConfigureOptions(co
- => co.Tools = co.Tools?.Select(tool => tool is AIFunction aiFunction
- ? new MiddlewareEnabledFunction(this.InnerAgent, aiFunction, this._delegateFunc)
- : tool)
- .ToList())
- .Build();
- };
+ // Route later additions and replacements through the same callback wrapping.
+ protected override void InsertItem(int index, AITool item) => base.InsertItem(index, this.Wrap(item));
- return options;
+ protected override void SetItem(int index, AITool item) => base.SetItem(index, this.Wrap(item));
+
+ private AITool Wrap(AITool tool)
+ {
+ if (tool is AIFunction function)
+ {
+ foreach (var middleware in this.MiddlewareChain)
+ {
+ function = MiddlewareEnabledFunction.Wrap(function, middleware, this.MiddlewareChain);
+ }
+
+ return function;
+ }
+
+ return tool;
+ }
}
- private sealed class MiddlewareEnabledFunction(AIAgent innerAgent, AIFunction innerFunction, Func>, CancellationToken, ValueTask