-
Notifications
You must be signed in to change notification settings - Fork 549
Description
Bug Report: Invalid Message Sequence When Mixing ApprovalRequiredAIFunction and Auto-Execute Functions with RunStreamingAsync
Summary
When using RunStreamingAsync
with an agent that has both ApprovalRequiredAIFunction
and regular auto-execute AIFunction
tools, the thread's message store becomes corrupted. Auto-execute functions fail to add the Assistant's FunctionCallContent
message to the thread, resulting in an orphaned Tool message with FunctionResultContent
. This violates OpenAI's API contract and causes subsequent requests to fail with HTTP 400 error.
Environment
- Package:
Microsoft.Agents.AI.OpenAI
(latest) - Model: Azure OpenAI
gpt-4o-mini
- Method:
agent.RunStreamingAsync(message, thread)
Steps to Reproduce
-
Create an agent with two functions:
- Function A: Wrapped in
ApprovalRequiredAIFunction
(requires approval) - Function B: Regular
AIFunction
(auto-executes)
- Function A: Wrapped in
-
Use
RunStreamingAsync
with anAgentThread
to invoke functions in this sequence:- First, invoke Function A (approval-required) → Works correctly
- Approve and complete Function A → Works correctly
- Then, invoke Function B (auto-execute) → Bug occurs here
-
Inspect the thread's
MessageStore
after invoking Function B
Expected Behavior
When Function B (auto-execute) is invoked, the message sequence should be:
[N] User: "what time is it?"
[N+1] Assistant: FunctionCallContent (GetCurrentTime, call_xyz...)
[N+2] Tool: FunctionResultContent (call_xyz, "2025-10-08 05:17:47")
[N+3] Assistant: TextContent ("It's currently 5:17 AM...")
Actual Behavior
The Assistant's FunctionCallContent message is missing or null:
[18] User: "what time is it?"
[19] Assistant: null ⚠️ MISSING FunctionCallContent
[20] Tool: FunctionResultContent (call_xyz, "2025-10-08 05:17:47")
The function does execute (evidenced by the Tool message with result), but the preceding Assistant message is not properly recorded in the thread.
Impact
On the next user message, the agent attempts to send the corrupted message history to the LLM, resulting in:
HTTP 400 (invalid_request_error)
Parameter: messages.[N].role
Invalid parameter: messages with role 'tool' must be a response to a preceding message with 'tool_calls'.
This makes multi-turn conversations impossible when mixing approval and auto-execute functions.
Full Reproduction Code
Tool Classes
using System.ComponentModel;
namespace AgentFrameworkExperiments.Tools;
public class ProductivityTools
{
[Description("Gets the current date and time")]
public string GetCurrentTime()
{
Console.WriteLine("[ProductivityTools] GetCurrentTime called");
return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
[Description("Sends an email to a recipient")]
public string SendEmail(
[Description("The email address of the recipient")] string to,
[Description("The subject of the email")] string subject,
[Description("The body content of the email")] string body)
{
Console.WriteLine($"[ProductivityTools] SendEmail called");
Console.WriteLine($" To: {to}");
Console.WriteLine($" Subject: {subject}");
Console.WriteLine($" Body: {body}");
return $"Email sent successfully to {to} with subject '{subject}'";
}
}
Demo: Mixed Approval/Auto-Execute (REPRODUCES BUG)
#pragma warning disable MEAI001 // Type is for evaluation purposes only
using Azure.AI.OpenAI;
using Azure;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using AgentFrameworkExperiments.Tools;
namespace AgentFrameworkExperiments.Demos;
public static class Demo11_FunctionApprovals
{
public static async Task RunAsync()
{
Console.WriteLine("\n=== DEMO 11: Function Approvals with Streaming (MIXED) ===\n");
var endpoint = new Uri("https://YOUR-ENDPOINT.openai.azure.com/");
var credential = new AzureKeyCredential("YOUR-API-KEY");
var tools = new ProductivityTools();
// GetCurrentTime - No approval needed (safe read operation)
var getTimeFunction = AIFunctionFactory.Create(tools.GetCurrentTime);
// SendEmail - Requires approval (potentially dangerous write operation)
var sendEmailFunction = AIFunctionFactory.Create(tools.SendEmail);
var approvalRequiredSendEmail = new ApprovalRequiredAIFunction(sendEmailFunction);
AIAgent agent = new AzureOpenAIClient(endpoint, credential)
.GetChatClient("gpt-4o-mini")
.CreateAIAgent(
instructions: "You are a helpful productivity assistant. You can check the time and send emails.",
name: "ProductivityAgent",
tools: [getTimeFunction, approvalRequiredSendEmail]
);
AgentThread thread = agent.GetNewThread();
Console.WriteLine("Starting conversation loop. Type 'exit' to quit.\n");
while (true)
{
Console.Write("User: ");
string? userInput = Console.ReadLine();
if (string.IsNullOrWhiteSpace(userInput) || userInput.ToLower() == "exit")
{
break;
}
Console.WriteLine("\nAssistant: ");
bool hasApprovalRequest = false;
FunctionApprovalRequestContent? approvalRequest = null;
await foreach (var streamChunk in agent.RunStreamingAsync(userInput, thread))
{
if (!hasApprovalRequest && streamChunk.Contents != null)
{
approvalRequest = streamChunk.Contents
.OfType<FunctionApprovalRequestContent>()
.FirstOrDefault();
if (approvalRequest != null)
{
hasApprovalRequest = true;
}
}
if (!string.IsNullOrEmpty(streamChunk.Text))
{
Console.Write(streamChunk.Text);
}
}
Console.WriteLine("\n");
if (hasApprovalRequest && approvalRequest != null)
{
Console.WriteLine($"\n⚠️ APPROVAL REQUIRED:");
Console.WriteLine($"Function: {approvalRequest.FunctionCall.Name}");
Console.WriteLine($"Arguments: {approvalRequest.FunctionCall.Arguments}");
Console.Write("Approve this function call? (y/n): ");
string? approval = Console.ReadLine();
bool isApproved = approval?.ToLower() == "y";
Console.WriteLine(isApproved ? "✓ Approved" : "✗ Rejected");
var approvalMessage = new ChatMessage(
ChatRole.User,
[approvalRequest.CreateResponse(isApproved)]
);
Console.WriteLine("\nAssistant: ");
await foreach (var streamChunk in agent.RunStreamingAsync(approvalMessage, thread))
{
Console.Write(streamChunk.Text);
}
Console.WriteLine("\n");
}
}
Console.WriteLine("✓ Demo completed");
}
}
Test Scenario 1: Run the Mixed Demo
User inputs:
- "send email to [email protected] with subject 'Financial Reports' and body 'Please find attached...'"
- ✅ Works correctly - asks for approval
- "what time is it?"
- ❌ BUG OCCURS - Function executes but thread message [19] is null
- Any subsequent message
- ❌ HTTP 400 ERROR - Invalid message sequence
Comparison Demos
Demo11b: Both Functions Require Approval (WORKS CORRECTLY)
Only difference from Demo11:
// BOTH require approval
var getTimeFunction = AIFunctionFactory.Create(tools.GetCurrentTime);
var approvalRequiredGetTime = new ApprovalRequiredAIFunction(getTimeFunction); // ← Added approval
var sendEmailFunction = AIFunctionFactory.Create(tools.SendEmail);
var approvalRequiredSendEmail = new ApprovalRequiredAIFunction(sendEmailFunction);
AIAgent agent = new AzureOpenAIClient(endpoint, credential)
.GetChatClient("gpt-4o-mini")
.CreateAIAgent(
instructions: "You are a helpful productivity assistant.",
name: "ProductivityAgent",
tools: [approvalRequiredGetTime, approvalRequiredSendEmail] // ← Both require approval
);
Result: ✅ Works correctly - both functions properly add Assistant messages with FunctionApprovalRequestContent
Demo11c: Neither Function Requires Approval (WORKS CORRECTLY)
Only difference from Demo11:
// NEITHER require approval - both auto-execute
var getTimeFunction = AIFunctionFactory.Create(tools.GetCurrentTime);
var sendEmailFunction = AIFunctionFactory.Create(tools.SendEmail); // ← No approval wrapper
AIAgent agent = new AzureOpenAIClient(endpoint, credential)
.GetChatClient("gpt-4o-mini")
.CreateAIAgent(
instructions: "You are a helpful productivity assistant.",
name: "ProductivityAgent",
tools: [getTimeFunction, sendEmailFunction] // ← Both auto-execute
);
Result: ✅ Works correctly - both functions auto-execute and properly add Assistant messages with FunctionCallContent
Root Cause Hypothesis
When using RunStreamingAsync
with a thread:
ApprovalRequiredAIFunction
correctly adds Assistant message withFunctionApprovalRequestContent
to thread- Regular
AIFunction
executes the function but fails to add the Assistant message withFunctionCallContent
to thread when previously an ApprovalRequiredAIFunction was used in the same conversation - This creates an invalid message sequence where Tool message has no preceding function call
Workarounds Tested
✅ Works Correctly:
- Agent with ONLY
ApprovalRequiredAIFunction
tools (Demo11b) - Agent with ONLY regular
AIFunction
tools (Demo11c)
❌ Fails:
- Agent with mixed function types (Demo11)
Additional Notes
- Bug appears specific to
RunStreamingAsync
(not tested withRunAsync
) - The auto-execute function DOES run (we can see the Tool message with result), but the Assistant message is missing/null
- This only occurs after at least one approval-required function has been invoked in the conversation
- Both approval and auto-execute functions work correctly when used exclusively (not mixed)
Request
Please investigate why auto-execute functions fail to properly record FunctionCallContent
in the thread's message store when mixed with ApprovalRequiredAIFunction
in streaming scenarios.
