Description
After #6729 made all AgentSkillsProvider tools approval-required by default, the only
supported way to auto-approve them is ToolApprovalAgent + AllToolsAutoApprovalRule.
But ToolApprovalAgent re-invokes the whole inner agent once per approval batch, and
ChatClientAgent runs every AIContextProvider.InvokingAsync on each RunAsync. So a
turn that makes N tool rounds re-runs the entire context-provider stack N times.
In an app with non-trivial per-invocation providers (DB-backed memory, document RAG, a
summarization/compaction provider), this multiplies provider cost per round. A single
tool-heavy skill turn produced 70+ re-invocations and overran our streaming turn cap
(timeout). The same scenario also surfaces two related failure modes (see Additional Context):
a DisposeAsync that throws, and persisted dangling approval requests that permanently
break a reloaded session.
There is no way to auto-approve inline within FunctionInvokingChatClient's existing tool
loop — every approval forces a full re-invoke round-trip, regardless of how fast the rule
returns true.
What did you expect to happen?
Auto-approving skill tools should not change the cost model of a turn. With a "always
approve" rule, skill tools should execute inside FIC's normal multi-round loop — one pass
through the context providers per turn — exactly as they did before 1.11 when these tools
were not approval-gated.
Ideally one of:
- an option to auto-approve within FIC's tool loop (no per-round agent re-invoke), or
- a provider-level opt-out on
AgentSkillsProvider (the removed ScriptApproval left no way
to keep read-only tools un-gated), or
- scoping the default approval requirement to
run_skill_script only — load_skill /
read_skill_resource are read-only.
Steps to reproduce the issue
- Create a
ChatClientAgent with one or more AIContextProviders that do real
per-invocation work (e.g. a provider that increments a counter / runs a query in
InvokingAsync).
- Attach an
AgentSkillsProvider and wrap the agent with
.UseToolApproval(new ToolApprovalAgentOptions { AutoApprovalRules = [ToolApprovalAgent.AllToolsAutoApprovalRule] }).
- Run a skill/prompt that triggers several tool rounds.
- Observe that each tool round triggers a fresh
RunAsync, so the context provider's
InvokingAsync is invoked once per round instead of once per turn. With heavy providers,
turn latency ≈ rounds × provider cost.
Code Sample
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
// A context provider doing real per-invocation work. In a real app this is memory / RAG /
// summarization; here it just counts how many times it runs.
sealed class CountingContextProvider : AIContextProvider
{
public int Invocations;
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken ct = default)
{
Interlocked.Increment(ref Invocations);
// imagine an expensive DB query / summarization LLM call here
return ValueTask.FromResult(new AIContext());
}
}
var provider = new CountingContextProvider();
var skillsProvider = new AgentSkillsProvider(skillsDir, SubprocessScriptRunner.RunAsync);
AIAgent agent = chatClient
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Use the skill to complete the task." },
AIContextProviders = [provider, skillsProvider],
})
.AsBuilder()
.UseToolApproval(new ToolApprovalAgentOptions
{
AutoApprovalRules = [ToolApprovalAgent.AllToolsAutoApprovalRule],
})
.Build();
var session = await agent.CreateSessionAsync();
await agent.RunAsync("Run the skill end-to-end (it makes several tool calls).", session);
// EXPECTED: provider.Invocations == 1 (one provider pass for the turn)
// ACTUAL: provider.Invocations == number of tool rounds (one full pass PER approval round)
Console.WriteLine($"Context-provider passes this turn: {provider.Invocations}");
Error Messages / Stack Traces
# (Related) If an approval-gated turn is interrupted, dangling ToolApprovalRequestContent are
# persisted in serialized history. On reload FIC throws on EVERY subsequent turn, permanently
# breaking the session with no framework-level recovery:
ToolApprovalRequestContent found with FunctionCall.CallId(s) 'call_...' that have no matching ToolApprovalResponseContent.
Package Versions
Microsoft.Agents.AI: 1.11.1 Microsoft.Agents.AI.OpenAI: 1.11.1 Microsoft.Agents.AI.Workflows: 1.11.1 Microsoft.Agents.AI.CopilotStudio: 1.11.1-preview.260625.1 Microsoft.Extensions.AI: 10.6.0 Microsoft.Extensions.AI.Abstractions: 10.6.0
.NET Version
.NET 10
Additional Context
This report bundles three related observations from the same scenario (server-side streaming
chat agent, ~129 tools, AgentSkillsProvider with inline skills only — no run_skill_script,
plus DB-memory / document-RAG / compaction context providers). Happy to split into separate
issues if preferred:
-
(Primary, design) ToolApprovalAgent auto-approval re-invokes the agent per round, so
per-invocation AIContextProviders run once per round instead of once per turn → O(rounds)
overhead and timeouts on tool-heavy skills. No inline auto-approval path exists.
-
(Robustness) Persisted, unanswered ToolApprovalRequestContent permanently brick a
reloaded session (FIC hard-throws). Consider cleaning/tolerating dangling approval requests
on load instead of throwing.
Workaround we adopted: a DelegatingChatClient placed above FIC that strips the
ApprovalRequiredAIFunction marker so FIC runs skill tools inline (single provider pass, no
re-invoke), plus a load-time pass that drops dangling ToolApprovalRequestContent. It works,
but it relies on framework internals (GetService<ApprovalRequiredAIFunction>(), and
ChatClientAgent skipping its own FIC when one is already present in the pipeline), so it's
fragile across versions — which is why we're reporting this.
Description
After #6729 made all
AgentSkillsProvidertools approval-required by default, the onlysupported way to auto-approve them is
ToolApprovalAgent+AllToolsAutoApprovalRule.But
ToolApprovalAgentre-invokes the whole inner agent once per approval batch, andChatClientAgentruns everyAIContextProvider.InvokingAsyncon eachRunAsync. So aturn that makes N tool rounds re-runs the entire context-provider stack N times.
In an app with non-trivial per-invocation providers (DB-backed memory, document RAG, a
summarization/compaction provider), this multiplies provider cost per round. A single
tool-heavy skill turn produced 70+ re-invocations and overran our streaming turn cap
(timeout). The same scenario also surfaces two related failure modes (see Additional Context):
a
DisposeAsyncthat throws, and persisted dangling approval requests that permanentlybreak a reloaded session.
There is no way to auto-approve inline within FunctionInvokingChatClient's existing tool
loop — every approval forces a full re-invoke round-trip, regardless of how fast the rule
returns
true.What did you expect to happen?
Auto-approving skill tools should not change the cost model of a turn. With a "always
approve" rule, skill tools should execute inside FIC's normal multi-round loop — one pass
through the context providers per turn — exactly as they did before 1.11 when these tools
were not approval-gated.
Ideally one of:
AgentSkillsProvider(the removedScriptApprovalleft no wayto keep read-only tools un-gated), or
run_skill_scriptonly —load_skill/read_skill_resourceare read-only.Steps to reproduce the issue
ChatClientAgentwith one or moreAIContextProviders that do realper-invocation work (e.g. a provider that increments a counter / runs a query in
InvokingAsync).AgentSkillsProviderand wrap the agent with.UseToolApproval(new ToolApprovalAgentOptions { AutoApprovalRules = [ToolApprovalAgent.AllToolsAutoApprovalRule] }).RunAsync, so the context provider'sInvokingAsyncis invoked once per round instead of once per turn. With heavy providers,turn latency ≈ rounds × provider cost.
Code Sample
using Microsoft.Agents.AI; using Microsoft.Extensions.AI; // A context provider doing real per-invocation work. In a real app this is memory / RAG / // summarization; here it just counts how many times it runs. sealed class CountingContextProvider : AIContextProvider { public int Invocations; public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken ct = default) { Interlocked.Increment(ref Invocations); // imagine an expensive DB query / summarization LLM call here return ValueTask.FromResult(new AIContext()); } } var provider = new CountingContextProvider(); var skillsProvider = new AgentSkillsProvider(skillsDir, SubprocessScriptRunner.RunAsync); AIAgent agent = chatClient .AsAIAgent(new ChatClientAgentOptions { ChatOptions = new() { Instructions = "Use the skill to complete the task." }, AIContextProviders = [provider, skillsProvider], }) .AsBuilder() .UseToolApproval(new ToolApprovalAgentOptions { AutoApprovalRules = [ToolApprovalAgent.AllToolsAutoApprovalRule], }) .Build(); var session = await agent.CreateSessionAsync(); await agent.RunAsync("Run the skill end-to-end (it makes several tool calls).", session); // EXPECTED: provider.Invocations == 1 (one provider pass for the turn) // ACTUAL: provider.Invocations == number of tool rounds (one full pass PER approval round) Console.WriteLine($"Context-provider passes this turn: {provider.Invocations}");Error Messages / Stack Traces
Package Versions
Microsoft.Agents.AI: 1.11.1 Microsoft.Agents.AI.OpenAI: 1.11.1 Microsoft.Agents.AI.Workflows: 1.11.1 Microsoft.Agents.AI.CopilotStudio: 1.11.1-preview.260625.1 Microsoft.Extensions.AI: 10.6.0 Microsoft.Extensions.AI.Abstractions: 10.6.0
.NET Version
.NET 10
Additional Context
This report bundles three related observations from the same scenario (server-side streaming
chat agent, ~129 tools, AgentSkillsProvider with inline skills only — no run_skill_script,
plus DB-memory / document-RAG / compaction context providers). Happy to split into separate
issues if preferred:
(Primary, design)
ToolApprovalAgentauto-approval re-invokes the agent per round, soper-invocation
AIContextProviders run once per round instead of once per turn → O(rounds)overhead and timeouts on tool-heavy skills. No inline auto-approval path exists.
(Robustness) Persisted, unanswered
ToolApprovalRequestContentpermanently brick areloaded session (FIC hard-throws). Consider cleaning/tolerating dangling approval requests
on load instead of throwing.
Workaround we adopted: a
DelegatingChatClientplaced above FIC that strips theApprovalRequiredAIFunctionmarker so FIC runs skill tools inline (single provider pass, nore-invoke), plus a load-time pass that drops dangling
ToolApprovalRequestContent. It works,but it relies on framework internals (
GetService<ApprovalRequiredAIFunction>(), andChatClientAgentskipping its own FIC when one is already present in the pipeline), so it'sfragile across versions — which is why we're reporting this.