Skip to content

.NET: [Bug]: AgentSkillsProvider approval-by-default + ToolApprovalAgent re-invoke causes O(rounds) context-provider overhead and timeouts. #6825

Closed

Description

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

  1. 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).
  2. Attach an AgentSkillsProvider and wrap the agent with
    .UseToolApproval(new ToolApprovalAgentOptions { AutoApprovalRules = [ToolApprovalAgent.AllToolsAutoApprovalRule] }).
  3. Run a skill/prompt that triggers several tool rounds.
  4. 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:

  1. (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.

  2. (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.

Activity

  1. changed the title [-].NET: [Bug]: AgentSkillsProvider approval-by-default + ToolApprovalAgent re-invoke causes O(rounds) context-provider overhead, timeouts, and a DisposeAsync throw[/-] [+].NET: [Bug]: AgentSkillsProvider approval-by-default + ToolApprovalAgent re-invoke causes O(rounds) context-provider overhead, timeouts.[/+] on Jun 30, 2026
  2. changed the title [-].NET: [Bug]: AgentSkillsProvider approval-by-default + ToolApprovalAgent re-invoke causes O(rounds) context-provider overhead, timeouts.[/-] [+].NET: [Bug]: AgentSkillsProvider approval-by-default + ToolApprovalAgent re-invoke causes O(rounds) context-provider overhead and timeouts.[/+] on Jun 30, 2026
  3. removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

.NETUsage: [Issues, PRs], Target: .Net

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions