// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Streaming;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses;
///
/// Extension methods for .
///
internal static class AgentRunResponseUpdateExtensions
{
///
/// Converts a stream of to stream of .
///
/// The agent run response updates.
/// The create response request.
/// The agent invocation context.
/// The cancellation token.
/// A stream of response events.
public static async IAsyncEnumerable ToStreamingResponseAsync(
this IAsyncEnumerable updates,
CreateResponse request,
AgentInvocationContext context,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var seq = new SequenceNumber();
var createdAt = DateTimeOffset.UtcNow;
var latestUsage = ResponseUsage.Zero;
yield return new StreamingResponseCreated { SequenceNumber = seq.Increment(), Response = CreateResponse(status: ResponseStatus.InProgress) };
yield return new StreamingResponseInProgress { SequenceNumber = seq.Increment(), Response = CreateResponse(status: ResponseStatus.InProgress) };
var outputIndex = 0;
List items = [];
var updateEnumerator = updates.GetAsyncEnumerator(cancellationToken);
await using var _ = updateEnumerator.ConfigureAwait(false);
// Track active item IDs by executor ID to pair invoked/completed/failed events
Dictionary executorItemIds = [];
AgentRunResponseUpdate? previousUpdate = null;
StreamingEventGenerator? generator = null;
while (await updateEnumerator.MoveNextAsync().ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
var update = updateEnumerator.Current;
// Special-case for agent framework workflow events.
if (update.RawRepresentation is WorkflowEvent workflowEvent)
{
// Convert executor events to standard OpenAI output_item events
if (workflowEvent is ExecutorInvokedEvent invokedEvent)
{
var itemId = IdGenerator.NewId(prefix: "item");
// Store the item ID for this executor so we can reuse it for completion/failure
executorItemIds[invokedEvent.ExecutorId] = itemId;
var item = new ExecutorActionItemResource
{
Id = itemId,
ExecutorId = invokedEvent.ExecutorId,
Status = "in_progress",
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
yield return new StreamingOutputItemAdded
{
SequenceNumber = seq.Increment(),
OutputIndex = outputIndex,
Item = item
};
}
else if (workflowEvent is ExecutorCompletedEvent completedEvent)
{
// Reuse the item ID from the invoked event, or generate a new one if not found
var itemId = executorItemIds.TryGetValue(completedEvent.ExecutorId, out var existingId)
? existingId
: IdGenerator.NewId(prefix: "item");
// Remove from tracking as this executor run is now complete
executorItemIds.Remove(completedEvent.ExecutorId);
JsonElement? resultData = null;
if (completedEvent.Data != null && JsonSerializer.IsReflectionEnabledByDefault)
{
resultData = JsonSerializer.SerializeToElement(
completedEvent.Data,
OpenAIHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
}
var item = new ExecutorActionItemResource
{
Id = itemId,
ExecutorId = completedEvent.ExecutorId,
Status = "completed",
Result = resultData,
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
yield return new StreamingOutputItemDone
{
SequenceNumber = seq.Increment(),
OutputIndex = outputIndex,
Item = item
};
}
else if (workflowEvent is ExecutorFailedEvent failedEvent)
{
// Reuse the item ID from the invoked event, or generate a new one if not found
var itemId = executorItemIds.TryGetValue(failedEvent.ExecutorId, out var existingId)
? existingId
: IdGenerator.NewId(prefix: "item");
// Remove from tracking as this executor run has now failed
executorItemIds.Remove(failedEvent.ExecutorId);
var item = new ExecutorActionItemResource
{
Id = itemId,
ExecutorId = failedEvent.ExecutorId,
Status = "failed",
Error = failedEvent.Data?.ToString(),
CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
yield return new StreamingOutputItemDone
{
SequenceNumber = seq.Increment(),
OutputIndex = outputIndex,
Item = item
};
}
else
{
// For other workflow events (not executor-specific), keep the old format as fallback
yield return CreateWorkflowEventResponse(workflowEvent, seq.Increment(), outputIndex);
}
continue;
}
if (!IsSameMessage(update, previousUpdate))
{
// Finalize the current generator when moving to a new message.
foreach (var evt in generator?.Complete() ?? [])
{
OnEvent(evt);
yield return evt;
}
generator = null;
outputIndex++;
previousUpdate = update;
}
using var contentEnumerator = update.Contents.GetEnumerator();
while (contentEnumerator.MoveNext())
{
var content = contentEnumerator.Current;
// Usage content is handled separately.
if (content is UsageContent usageContent && usageContent.Details != null)
{
latestUsage += usageContent.Details.ToResponseUsage();
continue;
}
// Create a new generator if there is no existing one or the existing one does not support the content.
if (generator?.IsSupported(content) != true)
{
// Finalize the current generator, if there is one.
foreach (var evt in generator?.Complete() ?? [])
{
OnEvent(evt);
yield return evt;
}
// Increment output index when switching generators
if (generator is not null)
{
outputIndex++;
}
// Create a new generator based on the content type.
generator = content switch
{
TextContent => new AssistantMessageEventGenerator(context.IdGenerator, seq, outputIndex),
TextReasoningContent => new TextReasoningContentEventGenerator(context.IdGenerator, seq, outputIndex),
FunctionCallContent => new FunctionCallEventGenerator(context.IdGenerator, seq, outputIndex, context.JsonSerializerOptions),
FunctionResultContent => new FunctionResultEventGenerator(context.IdGenerator, seq, outputIndex),
FunctionApprovalRequestContent => new FunctionApprovalRequestEventGenerator(context.IdGenerator, seq, outputIndex, context.JsonSerializerOptions),
FunctionApprovalResponseContent => new FunctionApprovalResponseEventGenerator(context.IdGenerator, seq, outputIndex),
ErrorContent => new ErrorContentEventGenerator(context.IdGenerator, seq, outputIndex),
UriContent uriContent when uriContent.HasTopLevelMediaType("image") => new ImageContentEventGenerator(context.IdGenerator, seq, outputIndex),
DataContent dataContent when dataContent.HasTopLevelMediaType("image") => new ImageContentEventGenerator(context.IdGenerator, seq, outputIndex),
DataContent dataContent when dataContent.HasTopLevelMediaType("audio") => new AudioContentEventGenerator(context.IdGenerator, seq, outputIndex),
HostedFileContent => new HostedFileContentEventGenerator(context.IdGenerator, seq, outputIndex),
DataContent => new FileContentEventGenerator(context.IdGenerator, seq, outputIndex),
_ => null
};
// If no generator could be created, skip this content.
if (generator is null)
{
continue;
}
}
foreach (var evt in generator.ProcessContent(content))
{
OnEvent(evt);
yield return evt;
}
}
}
// Finalize the active generator.
foreach (var evt in generator?.Complete() ?? [])
{
OnEvent(evt);
yield return evt;
}
yield return new StreamingResponseCompleted { SequenceNumber = seq.Increment(), Response = CreateResponse(status: ResponseStatus.Completed, outputs: items) };
void OnEvent(StreamingResponseEvent evt)
{
if (evt is StreamingOutputItemDone itemDone)
{
items.Add(itemDone.Item);
}
}
Response CreateResponse(ResponseStatus status = ResponseStatus.Completed, IEnumerable? outputs = null)
{
return new Response
{
Agent = request.Agent?.ToAgentId(),
Background = request.Background,
Conversation = request.Conversation ?? new ConversationReference { Id = context.ConversationId },
CreatedAt = createdAt.ToUnixTimeSeconds(),
Error = null,
Id = context.ResponseId,
Instructions = request.Instructions,
MaxOutputTokens = request.MaxOutputTokens,
MaxToolCalls = request.MaxToolCalls,
Metadata = request.Metadata != null ? new Dictionary(request.Metadata) : [],
Model = request.Model,
Output = outputs?.ToList() ?? [],
ParallelToolCalls = request.ParallelToolCalls ?? true,
PreviousResponseId = request.PreviousResponseId,
Prompt = request.Prompt,
PromptCacheKey = request.PromptCacheKey,
Reasoning = request.Reasoning,
SafetyIdentifier = request.SafetyIdentifier,
ServiceTier = request.ServiceTier,
Status = status,
Store = request.Store ?? true,
Temperature = request.Temperature ?? 1.0,
Text = request.Text,
ToolChoice = request.ToolChoice,
Tools = [.. request.Tools ?? []],
TopLogprobs = request.TopLogprobs,
TopP = request.TopP ?? 1.0,
Truncation = request.Truncation,
Usage = latestUsage,
#pragma warning disable CS0618 // Type or member is obsolete
User = request.User,
#pragma warning restore CS0618 // Type or member is obsolete
};
}
}
private static bool IsSameMessage(AgentRunResponseUpdate? first, AgentRunResponseUpdate? second)
{
return IsSameValue(first?.MessageId, second?.MessageId)
&& IsSameValue(first?.AuthorName, second?.AuthorName)
&& IsSameRole(first?.Role, second?.Role);
static bool IsSameValue(string? str1, string? str2) =>
str1 is not { Length: > 0 } || str2 is not { Length: > 0 } || str1 == str2;
static bool IsSameRole(ChatRole? value1, ChatRole? value2) =>
!value1.HasValue || !value2.HasValue || value1.Value == value2.Value;
}
private static StreamingWorkflowEventComplete CreateWorkflowEventResponse(WorkflowEvent workflowEvent, int sequenceNumber, int outputIndex)
{
// Extract executor_id if this is an ExecutorEvent
string? executorId = null;
if (workflowEvent is ExecutorEvent execEvent)
{
executorId = execEvent.ExecutorId;
}
JsonElement eventData;
if (JsonSerializer.IsReflectionEnabledByDefault)
{
JsonElement? dataElement = null;
if (workflowEvent.Data is not null)
{
dataElement = JsonSerializer.SerializeToElement(workflowEvent.Data, OpenAIHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
}
var eventDataObj = new WorkflowEventData
{
EventType = workflowEvent.GetType().Name,
Data = dataElement,
ExecutorId = executorId,
Timestamp = DateTime.UtcNow.ToString("O")
};
eventData = JsonSerializer.SerializeToElement(eventDataObj, OpenAIHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(WorkflowEventData)));
}
else
{
eventData = JsonSerializer.SerializeToElement(
"Unsupported. Workflow event serialization is currently only supported when JsonSerializer.IsReflectionEnabledByDefault is true.",
OpenAIHostingJsonContext.Default.String);
}
// Create the properly typed streaming workflow event
return new StreamingWorkflowEventComplete
{
SequenceNumber = sequenceNumber,
OutputIndex = outputIndex,
Data = eventData,
ExecutorId = executorId,
ItemId = IdGenerator.NewId(prefix: "wf", stringLength: 8, delimiter: "")
};
}
}