Description
While experimenting with checkpointing in Handoff Workflow, I tested several scenarios using the RestoreCheckpointAsync and ResumeStreamingAsync functions.
Ideally, when I load a checkpoint, it should resume or restore at this step and continue from there.
This works in a custom workflow, but I encountered several problems when trying to use checkpoints in Handoff.
For instance, when I resume the workflow, the WorkflowErrorEvent fires with the following message: "Expected exactly one update for key 'SharedState"
In situations where the AI requires tool approval and I use RestoreCheckpointAsync, the following error is thrown by my agentingStreamingFunctionMiddleware:
System.InvalidOperationException: 'ToolApprovalRequestContent found with FunctionCall.CallId(s) 'call_LaKjwuOiutjf280LhZmPTqHv' that have no matching ToolApprovalResponseContent.'
When I use ResumeStreamingAsync, it does not throw an exception, though the ExecutorFailed and WorkflowError events are fired.
[ExecutorFailed] weather_agent_933c703323e343f0a2480c3ec3707dc8: Cannot have multiple simultaneous conversations in Handoff Orchestration.
[WorkflowError] Error invoking handler for Microsoft.Agents.AI.Workflows.Specialized.HandoffState
I also tried using the HITL custom workflow as an agent, which created another problem. When I use checkpoints, my ChatClient Middleware throws an error: _'System.ClientModel.ClientResultException
HResult=0x80131500
Message=HTTP 400 (invalid_request_error: )
Parameter: messages.[3].role
An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_yjczbxhnnB9MzfKyWRAfCa4B'_
The custom AI combines the ChatProtocol Executor and the Guessing Game Example.
I also noticed that the chat messages contained many "User Null" elements and that the Tool_call ContentAI was missing.
I created a workaround by removing all the "User Null" elements, creating the missing "ToolMessages," and injecting them into the "ToolMessages." However, I don't believe the system is designed to function this way.
Code Sample
public static Workflow CreateHandoffWorkflow(AIAgent mainAgent, List<AIAgent> agents)
{
#pragma warning disable MAAIW001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
HandoffWorkflowBuilder workflowBuilder = AgentWorkflowBuilder.CreateHandoffBuilderWith(mainAgent)
.WithHandoffs(mainAgent, agents) // Triage can route to either specialist
.WithHandoffs(agents, mainAgent)
.WithToolCallFilteringBehavior(HandoffToolCallFilteringBehavior.None)
.EnableReturnToPrevious()
.EmitAgentResponseUpdateEvents();
#pragma warning restore MAAIW001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
Workflow workflow = workflowBuilder.Build();
WorkflowVisualizerTool.PrintAll(workflow);
return workflow;
}
public static async Task<(List<ChatMessage>, List<CheckpointInfo>?, CheckpointManager?)> RestoreWorkflowAsync(
Workflow workflow,
List<ChatMessage> messages,
string workflowName = "MasterAgent")
{
List<CheckpointInfo>? checkpoints = new();
CheckpointManager? checkpointManager = CheckpointManager.Default;
StreamingRun run = await InProcessExecution.RunStreamingAsync(
workflow,
messages,
checkpointManager: checkpointManager);
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"{workflowName} > ");
await HandleWorkflowAsync(run, messages, checkpoints);
Console.WriteLine();
Console.WriteLine("Restore Checkpoint");
Console.WriteLine($"Checkpoints: {checkpoints.Count}");
int index = checkpoints.Count / 2;
Console.WriteLine($"Middle Checkpoint {index}: {checkpoints[index].SessionId} (ID: {checkpoints[index].CheckpointId})");
Console.WriteLine("Will start from the middle");
Console.WriteLine();
Console.WriteLine();
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"{workflowName} > ");
await run.RestoreCheckpointAsync(checkpoints[index], CancellationToken.None);
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
await HandleWorkflowAsync(run, messages, checkpoints);
await run.DisposeAsync();
return (messages, checkpoints, checkpointManager);
}
public static async Task<(List<ChatMessage>, List<CheckpointInfo>?, CheckpointManager?)> RehydratingWorkflowAsync(
Workflow workflow,
List<ChatMessage> messages = null,
string workflowName = "MasterAgent")
{
CheckpointManager? checkpointManager = CheckpointManager.CreateInMemory();
List<CheckpointInfo>? checkpoints = new();
StreamingRun run = await InProcessExecution.RunStreamingAsync(
workflow,
messages,
checkpointManager: checkpointManager);
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"{workflowName} > ");
await HandleWorkflowAsync(run, messages, checkpoints);
Console.WriteLine();
Console.WriteLine("Restore Checkpoint");
Console.WriteLine($"Checkpoints: {checkpoints.Count}");
int index = checkpoints.Count / 2;
Console.WriteLine($"Middle Checkpoint {index}: {checkpoints[index].SessionId} (ID: {checkpoints[index].CheckpointId})");
Console.WriteLine("Will start from the middle");
Console.WriteLine();
Console.WriteLine();
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"{workflowName} > ");
await run.DisposeAsync();
run = await InProcessExecution.ResumeStreamingAsync(
workflow,
checkpoints[index],
checkpointManager: checkpointManager,
CancellationToken.None);
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
await HandleWorkflowAsync(run, messages, checkpoints);
await run.DisposeAsync();
return (messages, checkpoints, checkpointManager);
}
HandleWorkflowAsync is the simple "await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))" function.
Error Messages / Stack Traces
System.InvalidOperationException
HResult=0x80131509
Message=ToolApprovalRequestContent found with FunctionCall.CallId(s) 'call_nwH4mt3P0aJCQrNQ89iVB5bb' that have no matching ToolApprovalResponseContent.
Source=Microsoft.Extensions.AI
StackTrace:
at Microsoft.Shared.Diagnostics.Throw.InvalidOperationException(String message)
at Microsoft.Extensions.AI.FunctionInvokingChatClient.ExtractAndRemoveApprovalRequestsAndResponses(List`1 messages)
at Microsoft.Extensions.AI.FunctionInvokingChatClient.ProcessFunctionApprovalResponses(List`1 originalMessages, Boolean hasConversationId, String toolMessageId, String functionCallContentFallbackMessageId)
at Microsoft.Extensions.AI.FunctionInvokingChatClient.<GetStreamingResponseAsync>d__42.MoveNext()
at Microsoft.Extensions.AI.FunctionInvokingChatClient.<GetStreamingResponseAsync>d__42.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult(Int16 token)
at Microsoft.Extensions.AI.ConfigureOptionsChatClient.<GetStreamingResponseAsync>d__3.MoveNext()
at Microsoft.Extensions.AI.ConfigureOptionsChatClient.<GetStreamingResponseAsync>d__3.MoveNext()
at Microsoft.Extensions.AI.ConfigureOptionsChatClient.<GetStreamingResponseAsync>d__3.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult(Int16 token)
at Microsoft.Extensions.AI.ConfigureOptionsChatClient.<GetStreamingResponseAsync>d__3.MoveNext()
at Microsoft.Extensions.AI.ConfigureOptionsChatClient.<GetStreamingResponseAsync>d__3.MoveNext()
at Microsoft.Extensions.AI.ConfigureOptionsChatClient.<GetStreamingResponseAsync>d__3.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult(Int16 token)
at Microsoft.Extensions.AI.ConfigureOptionsChatClient.<GetStreamingResponseAsync>d__3.MoveNext()
at Microsoft.Extensions.AI.ConfigureOptionsChatClient.<GetStreamingResponseAsync>d__3.MoveNext()
at Microsoft.Extensions.AI.ConfigureOptionsChatClient.<GetStreamingResponseAsync>d__3.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult(Int16 token)
at Microsoft.Agents.AI.ChatClientAgent.<RunCoreStreamingAsync>d__29.MoveNext()
at Microsoft.Agents.AI.ChatClientAgent.<RunCoreStreamingAsync>d__29.MoveNext()
at Microsoft.Agents.AI.ChatClientAgent.<RunCoreStreamingAsync>d__29.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult(Int16 token)
at Microsoft.Agents.AI.AIAgent.<RunStreamingAsync>d__33.MoveNext()
at Microsoft.Agents.AI.AIAgent.<RunStreamingAsync>d__33.MoveNext()
at Microsoft.Agents.AI.AIAgent.<RunStreamingAsync>d__33.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult(Int16 token)
at Microsoft.Agents.AI.AIAgent.<RunStreamingAsync>d__33.MoveNext()
at Microsoft.Agents.AI.AIAgent.<RunStreamingAsync>d__33.MoveNext()
at Microsoft.Agents.AI.AIAgent.<RunStreamingAsync>d__33.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult(Int16 token)
at AgentTestSystem.Services.Middleware.MiddlewareClass.<AgentStreamingMiddleware>d__1.MoveNext() in D:\Entw\DevPhilip\COPA.AI\COPA.AI.Validation\COPA.AI_AgentTools\AgentTestSystem\Services\Middleware\Middleware.cs:line 47
at AgentTestSystem.Services.Middleware.MiddlewareClass.<AgentStreamingMiddleware>d__1.MoveNext() in D:\Entw\DevPhilip\COPA.AI\COPA.AI.Validation\COPA.AI_AgentTools\AgentTestSystem\Services\Middleware\Middleware.cs:line 47
at AgentTestSystem.Services.Middleware.MiddlewareClass.<AgentStreamingMiddleware>d__1.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult(Int16 token)
at Microsoft.Agents.AI.AIAgent.<RunStreamingAsync>d__33.MoveNext()
System.ClientModel.ClientResultException
HResult=0x80131500
Message=HTTP 400 (invalid_request_error: )
Parameter: messages.[3].role
An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_yjczbxhnnB9MzfKyWRAfCa4B
Source=OpenAI
StackTrace:
at OpenAI.ClientPipelineExtensions.<ProcessMessageAsync>d__0.MoveNext()
at System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable`1.ConfiguredValueTaskAwaiter.GetResult()
at OpenAI.Chat.ChatClient.<CompleteChatAsync>d__41.MoveNext()
at OpenAI.Chat.ChatClient.<CompleteChatAsync>d__15.MoveNext()
at Microsoft.Extensions.AI.OpenAIChatClient.<GetResponseAsync>d__6.MoveNext()
at AgentTestSystem.Services.Middleware.MiddlewareClass.<CustomChatClientMiddleware>d__3.MoveNext() in D:\Entw\DevPhilip\COPA.AI\COPA.AI.Validation\COPA.AI_AgentTools\AgentTestSystem\Services\Middleware\Middleware.cs:line 94
at Microsoft.Extensions.AI.AnonymousDelegatingChatClient.<<GetStreamingResponseAsync>g__GetStreamingResponseAsyncViaGetResponseAsync|6_0>d.MoveNext()
at Microsoft.Extensions.AI.AnonymousDelegatingChatClient.<<GetStreamingResponseAsync>g__GetStreamingResponseAsyncViaGetResponseAsync|6_0>d.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult(Int16 token)
at Microsoft.Extensions.AI.FunctionInvokingChatClient.<GetStreamingResponseAsync>d__42.MoveNext()
Package Versions
Microsoft.Agents.AI.Workflows" Version="1.3.0" , Microsoft.Agents.AI" Version="1.3.0"
.NET Version
.NET 10.0
Additional Context
No response
Description
While experimenting with checkpointing in Handoff Workflow, I tested several scenarios using the RestoreCheckpointAsync and ResumeStreamingAsync functions.
Ideally, when I load a checkpoint, it should resume or restore at this step and continue from there.
This works in a custom workflow, but I encountered several problems when trying to use checkpoints in Handoff.
For instance, when I resume the workflow, the WorkflowErrorEvent fires with the following message: "Expected exactly one update for key 'SharedState"
In situations where the AI requires tool approval and I use RestoreCheckpointAsync, the following error is thrown by my agentingStreamingFunctionMiddleware:
System.InvalidOperationException: 'ToolApprovalRequestContent found with FunctionCall.CallId(s) 'call_LaKjwuOiutjf280LhZmPTqHv' that have no matching ToolApprovalResponseContent.'
When I use ResumeStreamingAsync, it does not throw an exception, though the ExecutorFailed and WorkflowError events are fired.
[ExecutorFailed] weather_agent_933c703323e343f0a2480c3ec3707dc8: Cannot have multiple simultaneous conversations in Handoff Orchestration.
[WorkflowError] Error invoking handler for Microsoft.Agents.AI.Workflows.Specialized.HandoffState
I also tried using the HITL custom workflow as an agent, which created another problem. When I use checkpoints, my ChatClient Middleware throws an error: _'System.ClientModel.ClientResultException
HResult=0x80131500
Message=HTTP 400 (invalid_request_error: )
Parameter: messages.[3].role
An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_yjczbxhnnB9MzfKyWRAfCa4B'_
The custom AI combines the ChatProtocol Executor and the Guessing Game Example.
I also noticed that the chat messages contained many "User Null" elements and that the Tool_call ContentAI was missing.
I created a workaround by removing all the "User Null" elements, creating the missing "ToolMessages," and injecting them into the "ToolMessages." However, I don't believe the system is designed to function this way.
Code Sample
Error Messages / Stack Traces
Package Versions
Microsoft.Agents.AI.Workflows" Version="1.3.0" , Microsoft.Agents.AI" Version="1.3.0"
.NET Version
.NET 10.0
Additional Context
No response