Skip to content

.NET: Bug Report [Potential] : Invalid Message Sequence When Mixing ApprovalRequiredAIFunction and Auto-Execute Functions with RunStreamingAsync #1295

@hexbit2

Description

@hexbit2

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

  1. Create an agent with two functions:

    • Function A: Wrapped in ApprovalRequiredAIFunction (requires approval)
    • Function B: Regular AIFunction (auto-executes)
  2. Use RunStreamingAsync with an AgentThread 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
  3. 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:

  1. "send email to [email protected] with subject 'Financial Reports' and body 'Please find attached...'"
    • ✅ Works correctly - asks for approval
  2. "what time is it?"
    • BUG OCCURS - Function executes but thread message [19] is null
  3. 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 with FunctionApprovalRequestContent to thread
  • Regular AIFunction executes the function but fails to add the Assistant message with FunctionCallContent 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 with RunAsync)
  • 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.

Image

Metadata

Metadata

Assignees

Labels

.NETagentsIssues related to single agents

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions