Skip to content

.NET: [Bug]: Add explicit closure semantics for dangling tool call / approval flows #7872

Description

@soul-soft

[Bug] Add unified closure semantics for pending tool calls and approvals

Requirement

The framework needs unified closure semantics for the tool call lifecycle, not only for approval-required tools.

The function loop is managing a paired lifecycle:

  • FunctionCallContent
  • ToolApprovalRequestContent
  • ToolApprovalResponseContent
  • FunctionResultContent

When a model emits a tool call, that lifecycle must eventually be closed, either by a result, a rejection, a cancellation, or another explicit terminal state. Today there is no clear framework-level rule for what happens when the request remains pending.

Why this is needed

Dangling tool work is unavoidable.

A reliable reproduction is:

  1. The model emits a FunctionCallContent.
  2. Before the tool lifecycle is completed, the user cancels the run or the process is closed.
  3. The same session is restored later.

This can happen with approval-required tools and with tools that do not require approval. For approval-required tools, the interruption may happen after ToolApprovalRequestContent is emitted but before the user approves or rejects it. For non-approval tools, the interruption may happen after FunctionCallContent is emitted but before FunctionResultContent is produced.

The host cannot fully prevent this because process termination, user cancellation, browser refresh, service restart, and interrupted streaming can all happen after the tool call has been produced but before it reaches a terminal state.

This is why I am marking it as a bug: the current state machine can leave a reproducible, user-visible pending tool state without a public, reliable closure path.

Drain and closure

If a session has in-progress tool calls or approval requests, the framework should drain them before accepting a new normal User message.

Drain does not conflict with closure. Drain is the higher-level requirement: all pending tool work must be resolved before the next user turn. The policy can vary:

  • Surface the pending approval requests again and continue asking the user.
  • Reject/close pending approval requests automatically according to a configured option.
  • Mark interrupted non-approval tool calls as cancelled or failed.
  • Expose pending tool work through a public API so the host can resolve it deliberately.

The important part is that pending tool work should not remain suspended indefinitely, and a new user turn should not enter the function loop while the previous tool lifecycle is still open.

Special case from #7862

#7862 is a special historical restore case.

In that case:

  • The persisted message history contains a raw FunctionCallContent.
  • The history did not convert that call into a host-facing ToolApprovalRequestContent.
  • The session can still contain pending approval state for that call.

So the restore path cannot rely on message history alone. If the session says there are in-progress approvals, the framework should drain them even if the historical messages do not contain matching approval request objects.

For example, when a new User message arrives after restore, the framework should first drain the pending approvals from the session. Depending on policy, that may mean surfacing the approval again or closing it as rejected.

Proposal

Please add a framework-level contract for pending tool closure:

  • A public way to enumerate pending tool calls and approvals from a session.
  • A public way to drain or clear pending tool work.
  • A configurable policy for what happens when a new user turn arrives while tool work is pending.
  • Consistent behavior across the function loop, approval binding, and workflow host.

Architecture note

I strongly recommend moving the core function loop and approval lifecycle handling from Microsoft.Extensions.AI into Microsoft.Agents.AI.

extensions follows the .NET core release cadence, while agents are evolving much faster. Keeping the core loop in extensions makes the agent pipeline harder to evolve.

It also creates a reverse dependency problem: pending tool and approval state belongs to Microsoft.Agents.AI, but the loop that needs to close that state is split across extensions. That forces extensions to depend on behavior it does not own, and leads to extra middleware layers that patch FunctionCallContent lifecycle issues.

Moving the core loop into Microsoft.Agents.AI would make the pipeline cleaner, reduce duplicated patching logic, and give users one coherent model for tool calls, approvals, and results.

Scope

This issue is not about UI and is not asking for automatic approval.

It is about defining how pending tool calls and approvals are drained and closed at the framework level.

Metadata

Metadata

Labels

.NETUsage: [Issues, PRs], Target: .NetagentsUsage: [Issues, PRs], Target: Single agentreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions