You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sequential Agent Workflow re-requesting HITL approvals on Session Resume
We are testing a sequential multi-agent workflow (AgentWorkflowBuilder.BuildSequential) consisting of a routing agent, summary agent, and an execution agent utilizing an ApprovalRequiredAIFunction.
We are running into two key issues with streaming and session state:
RunStreamingAsync yields multiple ToolApprovalRequestContent chunks sharing the same CallId, causing naive chunk counts to fail assertions.
Upon serializing, deserializing, and resuming the session with the user's approval response (CreateResponse(true)), the workflow restarts the sequence and prompts for the exact same tool approval a second time on top of executing the function.
How do we properly inject a tool approval response into a sequential workflow session so that the downstream execution agent consumes it rather than re-triggering the tool call?
Code:
// Create a sample function tool that the agent can use.
[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
var openAiClient = new OpenAIClient(credential: new ApiKeyCredential(ApiKey!),
options: new OpenAIClientOptions { Endpoint = new Uri(EndPoint) });
var chatClient = openAiClient.GetChatClient(Model).AsIChatClient();
var agent1 = new ChatClientAgent(chatClient, new ChatClientAgentOptions
{
Name = "greeting",
Description = "Interprets customer operations requests and routes toward execution.",
ChatOptions = new ChatOptions
{
Instructions = "Convert user requests into a concise execution handoff. Do not include disclaimers, capability limitations, or user-facing prose. You are unable to call any tools. Do not attempt to.",
ToolMode = ChatToolMode.None
}
});
var agent3 = new ChatClientAgent(chatClient, new ChatClientAgentOptions
{
Name = "summary bot",
Description = "summary assistant.",
ChatOptions = new ChatOptions
{
Instructions = "Return a single concise final answer using only execution results. Do not repeat content.",
ToolMode = ChatToolMode.None,
}
});
var agent2 = new ChatClientAgent(chatClient, new ChatClientAgentOptions
{
Name = "execution",
Description = "Executes governed customer operations using approved tools.",
ChatOptions = new ChatOptions
{
Instructions = "You are a governed customer service agent. Governance/HITL handles approvals outside the model.",
ToolMode = ChatToolMode.Auto,
Tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]
}
});
var agent = AgentWorkflowBuilder.BuildSequential("test-workflow", new List<AIAgent> { agent1, agent2, agent3 })
.AsAIAgent("weather-agent", "weather agent");
// Call the agent and check if there are any function approval requests to handle.
// For simplicity, we are assuming here that only function approvals are pending.
AgentSession session = await agent.CreateSessionAsync();
// For streaming use:
var updates = await agent.RunStreamingAsync("What is the weather like in Amsterdam?", session).ToListAsync();
List<ToolApprovalRequestContent> approvalRequests = updates.SelectMany(x => x.Contents).OfType<ToolApprovalRequestContent>().ToList();
var countsPerCallId = approvalRequests
.GroupBy(x => x.ToolCall.CallId)
.Select(g => new
{
CallId = g.Key,
Count = g.Count()
})
.ToList();
if (countsPerCallId.Count != 1)
{
throw new InvalidOperationException("Expected only one unique function call request.");
}
if (countsPerCallId[0].Count != 1)
{
throw new InvalidOperationException("Expected only one function call request.");
}
var message = string.Join("", updates
.SelectMany(m => m.Contents)
.OfType<TextContent>()
.Select(t => t.Text));
// serialise session
var serializedState = await agent.SerializeSessionAsync(session);
session = await agent.DeserializeSessionAsync(serializedState);
approvalRequests = approvalRequests.DistinctBy(x => x.ToolCall.CallId).ToList();
while (approvalRequests.Count > 0)
{
// Ask the user to approve each function call request.
List<ChatMessage> userInputResponses = approvalRequests
.ConvertAll(functionApprovalRequest =>
{
Console.WriteLine($"The agent would like to invoke the following function, please reply Y to approve: Name {((FunctionCallContent)functionApprovalRequest.ToolCall).Name}");
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
});
// For streaming use:
updates = await agent.RunStreamingAsync(userInputResponses, session).ToListAsync();
var errors = updates.SelectMany(x => x.Contents).OfType<ErrorContent>().ToList();
foreach (var item in errors)
{
Console.WriteLine(item.Message);
}
approvalRequests = updates.SelectMany(x => x.Contents).OfType<ToolApprovalRequestContent>().ToList();
if(approvalRequests.Count > 0)
{
throw new InvalidOperationException("Expected no further function call requests.");
}
message = string.Join("", updates
.SelectMany(m => m.Contents)
.OfType<TextContent>()
.Select(t => t.Text));
}
Console.WriteLine($"\nAgent: {message}");
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Sequential Agent Workflow re-requesting HITL approvals on Session Resume
We are testing a sequential multi-agent workflow (AgentWorkflowBuilder.BuildSequential) consisting of a routing agent, summary agent, and an execution agent utilizing an
ApprovalRequiredAIFunction.We are running into two key issues with streaming and session state:
RunStreamingAsyncyields multipleToolApprovalRequestContentchunks sharing the sameCallId, causing naive chunk counts to fail assertions.CreateResponse(true)), the workflow restarts the sequence and prompts for the exact same tool approval a second time on top of executing the function.How do we properly inject a tool approval response into a sequential workflow session so that the downstream execution agent consumes it rather than re-triggering the tool call?
Code:
All reactions