[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:
- The model emits a
FunctionCallContent.
- Before the tool lifecycle is completed, the user cancels the run or the process is closed.
- 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.
[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:
FunctionCallContentToolApprovalRequestContentToolApprovalResponseContentFunctionResultContentWhen 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:
FunctionCallContent.This can happen with approval-required tools and with tools that do not require approval. For approval-required tools, the interruption may happen after
ToolApprovalRequestContentis emitted but before the user approves or rejects it. For non-approval tools, the interruption may happen afterFunctionCallContentis emitted but beforeFunctionResultContentis 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
Usermessage.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:
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
#7862is a special historical restore case.In that case:
FunctionCallContent.ToolApprovalRequestContent.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
Usermessage 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:
Architecture note
I strongly recommend moving the core function loop and approval lifecycle handling from
Microsoft.Extensions.AIintoMicrosoft.Agents.AI.extensionsfollows the .NET core release cadence, while agents are evolving much faster. Keeping the core loop inextensionsmakes 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 acrossextensions. That forcesextensionsto depend on behavior it does not own, and leads to extra middleware layers that patchFunctionCallContentlifecycle issues.Moving the core loop into
Microsoft.Agents.AIwould 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.