Skip to content

.NET: Bug Report: Cannot Serialize AgentThread with FunctionApprovalRequestContent #1318

@hexbit2

Description

@hexbit2

Bug Report: Cannot Serialize AgentThread with FunctionApprovalRequestContent

Summary

When using ApprovalRequiredAIFunction with AgentThread, attempting to serialize the thread while it contains a FunctionApprovalRequestContent message throws a NotSupportedException. This makes it impossible to persist conversations that use human-in-the-loop approval patterns, which breaks the documented workflow for persisted conversations.

The official documentation on persisted conversations demonstrates how to serialize and deserialize AgentThread objects to maintain conversation state across sessions. However, this feature is incompatible with ApprovalRequiredAIFunction, which is a core pattern documented in the function approvals tutorial.

Environment

  • Package: Microsoft.Agents.AI.OpenAI (latest version)
  • Target Framework: .NET 9.0
  • Model: Azure OpenAI gpt-4o-mini
  • Method: AgentThread.Serialize() + JsonSerializer.Serialize()

Steps to Reproduce

  1. Create an agent with functions wrapped in ApprovalRequiredAIFunction
  2. Create an AgentThread and invoke the agent with streaming
  3. Receive a FunctionApprovalRequestContent in the response stream
  4. Attempt to serialize the thread using the documented approach:
    JsonElement serializedThread = thread.Serialize();
    string json = JsonSerializer.Serialize(serializedThread, JsonSerializerOptions.Web);

Expected Behavior

The thread should serialize successfully, allowing the conversation (including pending approval requests) to be persisted and resumed later, as shown in the persisted conversation documentation.

Actual Behavior

Serialization throws:

System.NotSupportedException: Runtime type 'Microsoft.Extensions.AI.FunctionApprovalRequestContent' is not supported by polymorphic type 'Microsoft.Extensions.AI.AIContent'.
Path: $.Messages.Contents.

at System.Text.Json.ThrowHelper.ThrowNotSupportedException_RuntimeTypeNotSupported(Type baseType, Type runtimeType)
at System.Text.Json.Serialization.Metadata.PolymorphicTypeResolver.TryGetDerivedJsonTypeInfo(Type runtimeType, JsonTypeInfo& jsonTypeInfo, Object& typeDiscriminator)
at System.Text.Json.Serialization.JsonConverter.ResolvePolymorphicConverter(Object value, JsonTypeInfo jsonTypeInfo, JsonSerializerOptions options, WriteStack& state)

The FunctionApprovalRequestContent type is not registered in the polymorphic JSON serialization hierarchy for AIContent, making it impossible to serialize.

Full Reproduction Code

Tool Class

using System.ComponentModel;

namespace AgentFrameworkExperiments.Tools;

public class ProductivityTools
{
    [Description("Gets the current date and time")]
    public string GetCurrentTime()
    {
        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($"[SendEmail] To: {to}, Subject: {subject}");
        return $"Email sent successfully to {to}";
    }
}

Demo Code (REPRODUCES BUG)

#pragma warning disable MEAI001

using System.Text.Json;
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 Demo15_PersistedConversationWithApprovals
{
    public static async Task RunAsync()
    {
        var endpoint = new Uri("https://YOUR-ENDPOINT.openai.azure.com/");
        var credential = new AzureKeyCredential("YOUR-API-KEY");

        var tools = new ProductivityTools();

        // Both functions require approval
        var getTimeFunction = AIFunctionFactory.Create(tools.GetCurrentTime);
        var approvalRequiredGetTime = new ApprovalRequiredAIFunction(getTimeFunction);

        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]
            );

        string threadFilePath = "persisted_thread_with_approvals.json";
        AgentThread thread = agent.GetNewThread();

        Console.Write("User: ");
        string? userInput = Console.ReadLine();

        // Start streaming
        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");

        // At this point, thread contains FunctionApprovalRequestContent
        // Attempting to serialize now throws NotSupportedException

        Console.WriteLine("Attempting to serialize thread...");

        try
        {
            JsonElement serializedThread = thread.Serialize();
            string json = JsonSerializer.Serialize(serializedThread, JsonSerializerOptions.Web);
            await File.WriteAllTextAsync(threadFilePath, json);
            Console.WriteLine("✓ Success!");
        }
        catch (NotSupportedException ex)
        {
            Console.WriteLine($"❌ FAILED: {ex.Message}");
            // Output:
            // Runtime type 'Microsoft.Extensions.AI.FunctionApprovalRequestContent'
            // is not supported by polymorphic type 'Microsoft.Extensions.AI.AIContent'
        }
    }
}

Test Scenario

User input: "What time is it?"

Expected Result: Thread serializes successfully with pending approval request

Actual Result: NotSupportedException thrown during serialization

Impact

This bug makes it impossible to build production applications that combine:

  1. Persisted conversations (documented here)
  2. Human-in-the-loop approvals (documented here)

Real-World Use Cases Blocked:

  • Web APIs: Cannot save conversation state between HTTP requests when approvals are pending
  • Chatbots: Cannot persist user sessions when waiting for approval confirmations
  • Multi-turn workflows: Cannot checkpoint conversations that require human oversight
  • Disaster recovery: Cannot restore conversations that were interrupted during approval flows

Workarounds Attempted

❌ Workaround 1: Only serialize after approval completes

Problem: If the application crashes or user disconnects while approval is pending, conversation state is lost. This defeats the purpose of persistence.

❌ Workaround 2: Don't use ApprovalRequiredAIFunction

Problem: Removes safety guarantees for dangerous operations (sending emails, deleting data, financial transactions, etc.)

❌ Workaround 3: Custom serialization

Problem: AgentThread internal state is not fully accessible, making custom serialization unreliable and fragile.

Questions / Clarification Needed

If I'm misunderstanding how to properly persist threads with approval-required functions:

  1. Is there an alternative approach to serialize threads containing FunctionApprovalRequestContent?
  2. Is this by design? If so, what is the recommended pattern for persisting conversations with human-in-the-loop approvals?
  3. Should approval requests be transient? If yes, this limitation should be clearly documented in both tutorials.

The current documentation does not mention any incompatibility between these two core features, leading developers to assume they can be used together.

Suggested Fix

Option 1: Register FunctionApprovalRequestContent in polymorphic serialization

Add FunctionApprovalRequestContent (and FunctionApprovalResponseContent) to the JSON polymorphic type hierarchy for AIContent.

Option 2: Document the limitation

If approval requests are intentionally transient, clearly document in both tutorials that:

  • Threads cannot be serialized while approval requests are pending
  • Threads must wait for approval completion before persistence
  • Provide guidance on handling this in distributed/web scenarios

Option 3: Provide a workaround API

Add a method like thread.SerializeWithoutPendingApprovals() or thread.HasPendingApprovals() to give developers control over when serialization is safe.

Additional Context

I've created a comparison demo (Demo16) with auto-execute functions (no approvals), and that serializes successfully. This confirms the issue is specific to ApprovalRequiredAIFunction and not a general serialization problem.

Both features are prominently documented as core Agent Framework capabilities, so their incompatibility is unexpected and breaks reasonable developer expectations.

Request

Please either:

  1. Fix the serialization to support FunctionApprovalRequestContent, OR
  2. Clearly document this limitation and provide guidance on the recommended pattern for persisting conversations with approval workflows

Thank you for your time and consideration. If I've misunderstood something or there's an undocumented approach, please let me know!

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