Repository navigation
Description
[Bug] MAF approval-required function call is persisted as a dangling FunctionCallContent and pending approval state has no public read API
Reconstructed draft based on the original issue context. This is not guaranteed to be a byte-for-byte copy of the previous text.
Problem
When an approval-required function tool is called, MAF/FICC can persist the public conversation history with a raw FunctionCallContent, while the corresponding pending approval state is stored separately in session metadata.
This creates a practical restore problem for durable host applications:
- The visible message history says the assistant emitted a
FunctionCallContent. - The session state says there is a pending approval request for that tool call.
- The public message history does not contain a host-facing
ToolApprovalRequestContent. - There is no public API to enumerate the pending approval requests from the restored session.
As a result, the host cannot reliably restore the approval UI from public state alone.
Reproduction
- Configure an approval-required function tool, for example
get_weather. - Ask the agent a question that causes the model to call that tool.
- The run stops and waits for user approval.
- Before the user approves or rejects the tool call, close or refresh the conversation.
- Later, restore the same historical conversation/session.
At this point, the host must restore the approval UI and should not allow normal user input before the approval is resolved.
Persisted history example
The stored user message is ordinary text:
{
"role": "user",
"contents": [
{
"$type": "text",
"text": "北京的天气"
}
]
}The stored assistant message contains reasoning/text and a raw FunctionCallContent:
{
"role": "assistant",
"contents": [
{
"$type": "reasoning",
"text": "The user asks about the weather in Beijing. I should call the get_weather tool. It requires user approval before execution, but let me try."
},
{
"$type": "text",
"text": ""
},
{
"$type": "functionCall",
"name": "get_weather",
"arguments": {
"location": "北京",
"unit": "celsius"
},
"informationalOnly": false,
"callId": "call_00_vigxDpknXBrXBTnynu6P8614"
}
]
}The problem is that durable message history does not contain a ToolApprovalRequestContent that the host can render directly.
Why this is a bug
For a host application with durable sessions, pending approval is not an internal implementation detail. It is user-visible conversation state.
The user is waiting on a decision: approve or reject the tool call. If the host cannot restore that state, the conversation is left in an invalid state:
- If the host allows a new normal user message, the previous function call remains unresolved.
- If the host wants to restore the approval UI, it has no public way to enumerate the pending request.
- If the host reads private serialized state, the implementation becomes fragile.
This means the framework exposes a durable conversation state that cannot be interpreted reliably through public contracts.
Current workaround
The workaround is to inspect private session state and reconstruct the approval UI manually.
In practice, the relevant pending approval state may appear in internal state such as:
stateBag._pendingApprovalRequestsstateBag._sessionThreadState.queuedApprovalRequests
This works as a defensive workaround, but it is not a stable contract. Field names and serialized shapes are private implementation details.
Expected behavior
MAF should provide one of the following:
- Persist approval-required tool calls as stable
ToolApprovalRequestContentin chat history. - Expose a public API to enumerate pending approvals from a restored session.
- Document a stable contract that maps persisted
FunctionCallContentto the pending approval request id.
With that, the host can restore the approval UI, block normal user input while pending approvals exist, and submit an approval/rejection response safely.
Desired restored flow
After restoring a historical session:
- The host asks MAF whether the session has pending approvals.
- If pending approvals exist, the host renders approval UI instead of accepting normal user input.
- The user approves or rejects.
- The host sends the corresponding
ToolApprovalResponseContent. - MAF/FICC resumes the original tool lifecycle, executes or rejects the function, and produces the corresponding result.
Scope
This issue is specifically about durable restore of pending approval state.
It is not about changing approval policy, and it is not asking for automatic approval.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
atty57 commentedon Aug 25, 2026
I'd like to take this on, starting with option 2 (a public API to enumerate pending approvals from a restored session).
The state is already there —
ApprovalResponseBindingChatClientrecords every model-originatedToolApprovalRequestContentinto the session state bag under the internal_pendingApprovalRequestskey, and consumes the entry once a matchingToolApprovalResponseContentis bound. So a host reading that key is reading correct state; it just isn't a supported contract, which is exactly the fragility described here.My proposal is a small additive extension method that reads the same recorded state:
This lets a host, after
DeserializeSessionAsync, detect that the conversation is paused on an approval, restore its approval UI, and submit by request id — removing the need for a host-side mirror like_sessionThreadState.queuedApprovalRequests.Deliberately not in scope, since they look like maintainer design calls rather than something to land unsolicited:
ToolApprovalRequestContentinto durable chat history instead of a bareFunctionCallContent) changes what goes into message history and affects replay for every provider — happy to follow up on it separately if you'd like it, but it seems worth agreeing on the direction first.Draft PR incoming. If you'd rather see a different shape for the API (or a different option entirely), I'm happy to rework it.
soul-soft commentedon Aug 26, 2026
I agree with this direction. This feels like a conservative fix because it does not force one recovery policy on every
host. Instead, it gives developers a supported way to decide how to handle pending approvals after restore.
The important part is that it opens a unified semantic: pending approval is durable, user-visible conversation state,
and hosts should be able to discover and resolve it through public APIs instead of parsing private session metadata.
soul-soft commentedon Aug 26, 2026
One more API design consideration: server-side approval endpoints usually receive only a
requestIdfrom the client, then need to create the matching approval response.Because of that, I wonder whether pending approvals should have a small session-level management surface instead of only a one-off enumeration helper. For example, the session could expose an approval-oriented API that supports:
RequestIdThe exact shape does not need to be this, but conceptually something like:
This would make the API more sustainable if approval lifecycle management grows beyond restore-only scenarios. It would also let a server reconstruct the correct
ToolApprovalResponseContentwithout parsing private session metadata or maintaining a separate host-side mirror of pending requests.westey-m commentedon Aug 28, 2026
1448376744 (@soul-soft), have you considered just storing the last output from the agent when closing the application, so that it can be restored when re-loading the application, and using that restore the UI?
The last response from the agent would contain the ToolApprovalRequestContent items required to render the approval UI again.
Also, are you using per-service-call persistence? I'm assuming you are storing chat history in the session?
soul-soft commentedon Aug 28, 2026
Yes, I am using per-service-call persistence. The reason is conversation continuity: my app supports interruption, so
I need the session/chat state to remain consistent across interrupted and resumed flows.
And yes, storing the last agent output does sound like a good direction for another related problem I have: when an
interruption happens, how should the host surface unresolved regular function calls that are not approval requests?
The question I want to clarify here is: what is the recommended way to store the last output from the agent? Is there
a public API or pattern for capturing that output in a form that can later be restored and used to rebuild the UI?
westey-m commentedon Aug 31, 2026
1448376744 (@soul-soft), thanks for the additional information. The reason I asked about per-service-call persistence is because this means that chat history is persisted below FunctionInvokingChatClient. If per-service-call persistence is off, approval requests and responses are actually persisted in chat history, since then chat history is persisted above FunctionInvokingChatClient.
Regardless though, separately maintaining a UX friendly chat history is not complex. It is just a
List<ChatMessage>, which would need both the user input and agent output added to it. This can be serialized directly using a JsonSerializer. AllChatMessageinstances are fully serializable.I do think you raise a valid point about wanting to just fetch the public facing chat history from the agent, since in theory it should already have it somewhere. In practice though, the agent doesn't always have access to the chat history. E.g. if the ChatClientAgent is using the responses service with store=true, then the ChatClientAgent has no chat history at all, it only stores an id in the session that identifies the chat history stored in the responses service.
Service-stored chat history can also (for some services) be modified via service endpoints, without the agent's involvement. So any copy of ChatHistory that we maintain in the session may become un-synced, if a developer was making such changes.
We currently also don't have any abstraction or implementations for accessing service-stored chat history for all services, so the agent is unable to natively retrieve the chat history from such a service. Agent Framework relies on the service using it's own chat history, service side, by just pre-pending it on to new messages.
Therefore, the recommendation right now, is to maintain your own UX friendly chat history outside of the agent.
mo3in commentedon Sep 20, 2026
Adding another durable-host data point from a .NET 1.22 integration. I am not yet claiming the same root cause, but the scenario is close enough to be useful here.
Our stack is:
Observed behavior:
ToolApprovalRequestContent.AgentSessionis serialized and persisted successfully.RUN_ERROR/StreamingError.Important comparison: we also have a compatibility canary using stock
MapAGUIServer+ serializedAgentSessionStorewhere approval interrupt -> serialize -> restore -> approve works and executes exactly once.We are currently isolating whether the divergence appears when adding the durable custom
ChatHistoryProvider, the custom hosting route, or their interaction with approval response binding.Diagnostic matrix:
I'll follow up with a minimal reproduction if this proves to be the same durable-approval/history-state issue rather than a bug in our custom route.
westey-m commentedon Sep 22, 2026
Closing this issue as the main ask has been addressed.