Repository navigation
Description
Description
Summary
When a ToolApprovalRequestContent is persisted via CheckpointManager.CreateJson(...) (a JsonCheckpointStore-based store) during a HITL workflow and then restored on resume, ToolApprovalRequestContent.ToolCall appears to lose its concrete FunctionCallContent type and come back as something that no longer satisfies is FunctionCallContent in FunctionInvokingChatClient.ExtractAndRemoveApprovalRequestsAndResponses.
Because that method matches with
case ToolApprovalRequestContent tarc
when tarc.ToolCall is FunctionCallContent { InformationalOnly: false }:the TARC is silently skipped, the call id is never added to approvalRequestCallIds, and the downstream validation / invocation logic does not handle the approval as expected.
Environment
Microsoft.Agents.AI/Microsoft.Agents.AI.Workflows1.1 (.NET 10)Microsoft.Extensions.AI10.5- Storage: custom
JsonCheckpointStoresubclass persisting the JSON to SQL Server (Dapper). TheCheckpointManageris created viaCheckpointManager.CreateJson(store); no customJsonSerializerOptionspassed. - Flow:
workflow.AsAIAgent()is not used in the affected path; we drive the run directly withInProcessExecution.RunStreamingAsync(workflow, input, CheckpointManager, sessionId, ct)and resume withInProcessExecution.ResumeStreamingAsync(...). - HITL mechanism:
ApprovalRequiredAIFunction-wrapped tools inside aChatClientAgentnode that is bound into the workflow (pattern "B" in the published GroupChatToolApproval sample).
Expected behavior
After a checkpoint → resume cycle, ToolApprovalRequestContent.ToolCall restored through JsonCheckpointStore should remain an instance of the original concrete ToolCallContent subclass (FunctionCallContent), so that FunctionInvokingChatClient’s pattern matching continues to work unchanged.
Actual behavior
After resume, the ToolCall instance returned by request.TryGetDataAs<ToolApprovalRequestContent>() no longer matches is FunctionCallContent. As a result the approval pair is not extracted by FICC and the run either:
- raises
InvalidOperationException: ToolApprovalRequestContent found with FunctionCall.CallId(s) '…' that have no matching ToolApprovalResponseContent.(if only the TARC side degrades), or - passes the approval through unprocessed and causes OpenAI Responses to reject with
No tool output found for function call call_…in the subsequent turn.
Reproduction outline
- Build a workflow where a
ChatClientAgentnode is bound intoWorkflowBuilderand uses anApprovalRequiredAIFunctiontool. - Run the workflow with durable checkpointing:
var checkpointManager = CheckpointManager.CreateJson(myJsonCheckpointStore); await foreach (var evt in InProcessExecution.RunStreamingAsync( workflow, input, checkpointManager, sessionId, ct).WatchStreamAsync(ct)) { ... }
- On
RequestInfoEvent, persist the envelope, terminate the run. - Later, resume:
var run = await InProcessExecution.ResumeStreamingAsync( workflow, checkpointManager, sessionId, ct); var request = /* the resumed RequestInfoEvent.Request */; var tarc = request.TryGetDataAs<ToolApprovalRequestContent>(); Console.WriteLine(tarc!.ToolCall?.GetType().FullName); // Observed: NOT FunctionCallContent
- Build a
ToolApprovalResponseContentwith the restoredtarc.ToolCallverbatim and send it back viarun.SendResponseAsync(request.CreateResponse(taResp)). - FICC’s
ExtractAndRemoveApprovalRequestsAndResponsesfails to pick up the pair.
(I can produce a minimal standalone repro against a fake IChatClient if useful — let me know.)
Likely root cause (hypothesis)
ToolApprovalRequestContent.ToolCall is declared as the abstract base ToolCallContent. System.Text.Json requires polymorphic metadata ([JsonPolymorphic] + [JsonDerivedType] or equivalent TypeInfoResolver) to round-trip an abstract property through its concrete subtype. Unless JsonCheckpointStore is configured with AIJsonUtilities.DefaultOptions (or an equivalent resolver that knows about FunctionCallContent), ToolCall serializes as the base shape and deserializes back as the base type.
Two things to confirm in the repo:
- Whether
AIContent/ToolCallContent/FunctionCallContentcurrently carry[JsonPolymorphic]+[JsonDerivedType(typeof(FunctionCallContent), "...")]— or are only known to the resolver bundled inAIJsonUtilities.DefaultOptions. - What
JsonSerializerOptionsthe defaultCheckpointManager.CreateJson(...)uses. If it does not pull fromAIJsonUtilities.DefaultOptionsby default, the polymorphic information forAIContent-derived members is effectively lost for any custom store.
Workaround currently in use
In our resume handler we reconstruct the concrete type before building the response:
var toolCall = tarc.ToolCall is FunctionCallContent fcc
? fcc
: new FunctionCallContent(tarc.ToolCall!.CallId, /*name*/..., /*args*/...);
var response = new ToolApprovalResponseContent(tarc.RequestId, approved, toolCall);This is fragile (we must know the original Name / Arguments to rebuild the FCC) and only masks the serializer-level problem.
Asks
- Is this a known limitation of the JSON checkpoint path for
AIContent-valued properties whose declared type is abstract? If so, documenting it would help. - Can
CheckpointManager.CreateJson(...)default to (or transparently compose with)AIJsonUtilities.DefaultOptionsso thatAIContentpolymorphism is preserved out of the box forJsonCheckpointStoreimplementations? - Alternatively, can
ToolCallContent/AIContentbe annotated with[JsonPolymorphic]+[JsonDerivedType(...)]so the polymorphism travels with the type itself regardless of the resolver? - If the official guidance is "you must pass your own
JsonSerializerOptions", please document that explicitly next toCheckpointManager.CreateJsonand the HITL samples. The current GroupChatToolApproval sample works because it does not checkpoint across process boundaries; the failure only appears under durable resume.
Code Sample
Error Messages / Stack Traces
Package Versions
Microsoft.Agents.AI 1.1.0, Microsoft.Agents.AI.Workflows 1.1.0
.NET Version
.NET 10
Additional Context
No response
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
lokitoth commentedon May 1, 2026
Most likely this is due to a lack of appropriate
[JsonDerivedType]annotation somewhere. Thanks for filing the issue, awata (@aeras3637).lokitoth commentedon May 5, 2026
Looking at the MEAI types, it does have
[JsonDerivedType(typeof(FunctionCallContent), "functionCall")]set on it, and has had it that way for some time, so it is unlikely to be the root cause.Expanding a little on how the checkpoint serialization works with JSON backends, we rely on
JsonMarshaller(here) to manage the translation. It takes an optionalJsonSerializationOptionsprovided by the user (you can inject this when callingCheckpointManager.CreateJson(ICheckpointStore<JsonElement> store, JsonSerializerOptions? customOptions = null)), but this is used only if the internal one (viaWorkflowsJsonUtilities, which chains toAgentAbstractionsJsonUtilities, which then chains to theAIJsonUtilitiesclass).So the fact that it still loses the type information is strange.
Add issue #5350 repro tests + root-cause validation plan
Add maximal E2E ChatClientAgent + ApprovalRequiredAIFunction repro for …
lokitoth commentedon May 19, 2026
Hey awata (@aeras3637) - I spent some time trying to reproduce the issue but have not been able to. Here is a set of unit tests that I believe capture the issue as you described, with successively more machinery being exercised.
Could you check if you are still running into the issue described? If you are, let's see if we can figure out where I did not quite do the same thing that you are.
aeras3637 commentedon May 20, 2026
Thank you for looking into this.
I also created a minimal repro and tested the checkpoint / resume flow through
CheckpointManager.CreateJson(...)andJsonCheckpointStore, without our application-specific implementation involved.As a result, I confirmed that in the standard workflow checkpoint / resume path,
ToolApprovalRequestContent.ToolCallis still restored asFunctionCallContentafter resume.I also tested this with the current package versions and with versions close to the ones we were using at the time, and I could not reproduce the concrete type being lost on the framework side.
After further investigation, the issue turned out to be in our application-side HITL resume response generation.
In our implementation, we rebuild
FunctionCallContentbefore sending the approval response, because we need to add application-specific arguments such asrequestId,responseAction, andpayload.That code did not validate strongly enough that the request-side
ToolCallwas actually aFunctionCallContent. If it was ever handled as an unexpectedToolCallContent, it could produce a response that lost the original function name and arguments.So the problem was most likely not that the framework lost the concrete type during checkpoint restore. Instead, we appear to have misdiagnosed an issue in our own resume response generation as a framework checkpoint serialization issue.
On our side, I have made the following changes:
FunctionCallContentis preserved across framework checkpoint / resumeToolCallis not aFunctionCallContentBased on your investigation and the repro results above, I agree that this does not appear to be a framework bug.
Sorry for opening this issue before isolating the application-specific part of the behavior. Thank you again for spending time trying to reproduce it.
Please feel free to close this issue as an application-side implementation problem.
lokitoth commentedon May 21, 2026
No worries; investigation discovered an unrelated issue that we'll be fixing :)
Going to close this out now, but please reopen if this issue reappears.