diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemResourceExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemResourceExtensions.cs
new file mode 100644
index 0000000000..c9e92dda60
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Converters/ItemResourceExtensions.cs
@@ -0,0 +1,344 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Hosting.OpenAI.Responses.Converters;
+
+///
+/// Extension methods for converting between ItemResource and ChatMessage.
+///
+internal static class ItemResourceExtensions
+{
+ ///
+ /// Converts an ItemResource to a ChatMessage.
+ ///
+ /// The ItemResource to convert.
+ /// A ChatMessage.
+ /// Thrown when the ItemResource type is not supported for conversion to ChatMessage.
+ public static ChatMessage ToChatMessage(this ItemResource itemResource)
+ {
+ if (itemResource is ResponsesMessageItemResource messageItem)
+ {
+ var content = messageItem switch
+ {
+ ResponsesAssistantMessageItemResource assistant => assistant.Content,
+ ResponsesUserMessageItemResource user => user.Content,
+ ResponsesSystemMessageItemResource system => system.Content,
+ ResponsesDeveloperMessageItemResource developer => developer.Content,
+ _ => throw new NotSupportedException($"Message item type {messageItem.GetType().Name} not supported")
+ };
+
+ // Convert ItemContent to AIContent using the existing converter
+ var aiContents = content
+ .Select(ItemContentConverter.ToAIContent)
+ .Where(c => c is not null)
+ .ToList();
+ return new ChatMessage(messageItem.Role, aiContents!);
+ }
+
+ if (itemResource is FunctionToolCallItemResource functionCall)
+ {
+ return new ChatMessage(ChatRole.Assistant, [
+ functionCall.ToFunctionCallContent()
+ ]);
+ }
+
+ if (itemResource is FunctionToolCallOutputItemResource functionOutput)
+ {
+ return new ChatMessage(ChatRole.Tool, [
+ functionOutput.ToFunctionResultContent()
+ ]);
+ }
+
+ throw new NotSupportedException($"ItemResource type {itemResource.GetType().Name} not supported for conversion to ChatMessage");
+ }
+
+ ///
+ /// Converts a ChatMessage to ItemResource objects.
+ /// This method requires an IdGenerator to create unique IDs for the generated resources.
+ ///
+ /// The chat message to convert.
+ /// The ID generator to use for creating IDs.
+ /// The JSON serializer options to use.
+ /// An enumerable of ItemResource objects.
+ public static IEnumerable ToItemResources(this ChatMessage message, IdGenerator idGenerator, JsonSerializerOptions jsonSerializerOptions)
+ {
+ return message.ToItemResource(idGenerator, jsonSerializerOptions);
+ }
+
+ ///
+ /// Converts a ChatMessage to a single ItemResource (message type) when it contains only message content.
+ /// For messages with function calls, use ToItemResources instead.
+ ///
+ /// The chat message to convert.
+ /// The ID to assign to the message resource.
+ /// A ResponsesMessageItemResource.
+ /// Thrown when the message contains function calls or function results.
+ public static ResponsesMessageItemResource ToMessageItemResource(this ChatMessage message, string id)
+ {
+ // Check if the message contains function calls or function results
+ if (message.Contents.Any(c => c is FunctionCallContent or FunctionResultContent))
+ {
+ throw new InvalidOperationException("Cannot convert a ChatMessage with function calls or function results to a single MessageItemResource. Use ToItemResources instead.");
+ }
+
+ // Convert all contents to ItemContent
+ var contents = message.Contents
+ .Select(ItemContentConverter.ToItemContent)
+ .Where(c => c is not null)
+ .ToList();
+
+ // Create the appropriate message item resource based on role
+ return message.Role.Value switch
+ {
+ "assistant" => new ResponsesAssistantMessageItemResource
+ {
+ Id = id,
+ Status = ResponsesMessageItemResourceStatus.Completed,
+ Content = contents!
+ },
+ "user" => new ResponsesUserMessageItemResource
+ {
+ Id = id,
+ Status = ResponsesMessageItemResourceStatus.Completed,
+ Content = contents!
+ },
+ "system" => new ResponsesSystemMessageItemResource
+ {
+ Id = id,
+ Status = ResponsesMessageItemResourceStatus.Completed,
+ Content = contents!
+ },
+ "developer" => new ResponsesDeveloperMessageItemResource
+ {
+ Id = id,
+ Status = ResponsesMessageItemResourceStatus.Completed,
+ Content = contents!
+ },
+ _ => new ResponsesAssistantMessageItemResource
+ {
+ Id = id,
+ Status = ResponsesMessageItemResourceStatus.Completed,
+ Content = contents!
+ }
+ };
+ }
+
+ ///
+ /// Converts multiple ItemResources to ChatMessages.
+ /// Adjacent message items with the same role will be combined into a single ChatMessage.
+ ///
+ /// The ItemResources to convert.
+ /// An enumerable of ChatMessages.
+ public static IEnumerable ToChatMessages(this IEnumerable itemResources)
+ {
+ ChatMessage? currentMessage = null;
+ var currentContents = new List();
+
+ foreach (var item in itemResources)
+ {
+ if (item is ResponsesMessageItemResource messageItem)
+ {
+ // Check if we should start a new message or continue the current one
+ if (currentMessage is not null && currentMessage.Role != messageItem.Role)
+ {
+ // Yield the current message and start a new one
+ yield return new ChatMessage(currentMessage.Role, [.. currentContents]);
+ currentContents = [];
+ }
+
+ // Add contents from this message item
+ var aiContents = messageItem switch
+ {
+ ResponsesAssistantMessageItemResource assistant => assistant.Content,
+ ResponsesUserMessageItemResource user => user.Content,
+ ResponsesSystemMessageItemResource system => system.Content,
+ ResponsesDeveloperMessageItemResource developer => developer.Content,
+ _ => []
+ };
+
+ foreach (var content in aiContents)
+ {
+ if (ItemContentConverter.ToAIContent(content) is { } aiContent)
+ {
+ currentContents.Add(aiContent);
+ }
+ }
+
+ // Initialize or update current message
+ currentMessage = new ChatMessage(messageItem.Role, [.. currentContents]);
+ }
+ else if (item is FunctionToolCallItemResource functionCall)
+ {
+ // Function calls are always from assistant
+ if (currentMessage is not null && currentMessage.Role != ChatRole.Assistant)
+ {
+ yield return new ChatMessage(currentMessage.Role, [.. currentContents]);
+ currentContents = [];
+ currentMessage = null;
+ }
+
+ currentContents.Add(functionCall.ToFunctionCallContent());
+ currentMessage = new ChatMessage(ChatRole.Assistant, [.. currentContents]);
+ }
+ else if (item is FunctionToolCallOutputItemResource functionOutput)
+ {
+ // Function outputs are always from tool role
+ if (currentMessage is not null && currentMessage.Role != ChatRole.Tool)
+ {
+ yield return new ChatMessage(currentMessage.Role, [.. currentContents]);
+ currentContents = [];
+ currentMessage = null;
+ }
+
+ currentContents.Add(functionOutput.ToFunctionResultContent());
+ currentMessage = new ChatMessage(ChatRole.Tool, [.. currentContents]);
+ }
+ }
+
+ // Yield the last message if any
+ if (currentMessage is not null && currentContents.Count > 0)
+ {
+ yield return new ChatMessage(currentMessage.Role, [.. currentContents]);
+ }
+ }
+
+ ///
+ /// Converts a FunctionToolCallItemResource to FunctionCallContent.
+ /// Uses the official Microsoft.Extensions.AI pattern via CreateFromParsedArguments to properly handle parsing errors.
+ ///
+ /// The function call item resource to convert.
+ /// A FunctionCallContent with properly parsed arguments. If parsing fails, the Exception property will be set.
+ public static FunctionCallContent ToFunctionCallContent(this FunctionToolCallItemResource functionCall)
+ {
+ // Use the same pattern as Microsoft.Extensions.AI.OpenAI's ParseCallContent method
+ // This properly handles parsing errors by setting the Exception property on FunctionCallContent
+ return FunctionCallContent.CreateFromParsedArguments(
+ functionCall.Arguments ?? "{}",
+ functionCall.CallId,
+ functionCall.Name,
+ static json => (IDictionary)JsonSerializer.Deserialize(json, OpenAIHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary)))!);
+ }
+
+ ///
+ /// Converts a FunctionToolCallOutputItemResource to FunctionResultContent.
+ ///
+ /// The function output item resource to convert.
+ /// A FunctionResultContent.
+ public static FunctionResultContent ToFunctionResultContent(this FunctionToolCallOutputItemResource functionOutput)
+ {
+ return new FunctionResultContent(functionOutput.CallId, functionOutput.Output);
+ }
+
+ ///
+ /// Converts a ChatMessage to ItemParam objects (input models without IDs).
+ /// This is useful for creating items in the Conversations API.
+ /// Filters out events that don't map well to ItemParams (e.g., messages with no convertible content).
+ ///
+ /// The chat message to convert.
+ /// An enumerable of ItemParam objects.
+ public static IEnumerable ToItemParams(this ChatMessage message)
+ {
+ // Separate function call/result contents from regular message contents
+ foreach (AIContent content in message.Contents)
+ {
+ switch (content)
+ {
+ case FunctionCallContent functionCallContent:
+ yield return new FunctionToolCallItemParam
+ {
+ CallId = functionCallContent.CallId,
+ Name = functionCallContent.Name,
+ Arguments = JsonSerializer.Serialize(
+ functionCallContent.Arguments,
+ OpenAIHostingJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary)))
+ };
+ break;
+
+ case FunctionResultContent functionResultContent:
+ string output = functionResultContent.Exception is not null
+ ? $"{functionResultContent.Exception.GetType().Name}(\"{functionResultContent.Exception.Message}\")"
+ : $"{functionResultContent.Result?.ToString() ?? "(null)"}";
+ yield return new FunctionToolCallOutputItemParam
+ {
+ CallId = functionResultContent.CallId,
+ Output = output
+ };
+ break;
+ }
+ }
+
+ // Convert regular message contents
+ List regularContents = [];
+ foreach (AIContent content in message.Contents)
+ {
+ if (content is not FunctionCallContent and not FunctionResultContent &&
+ ItemContentConverter.ToItemContent(content) is { } itemContent)
+ {
+ regularContents.Add(itemContent);
+ }
+ }
+
+ // Only create a message item if we have convertible contents
+ // This filters out messages that contain only non-convertible content (e.g., UsageContent)
+ if (regularContents.Count > 0)
+ {
+ InputMessageContent messageContent = InputMessageContent.FromContents(regularContents);
+
+ yield return message.Role.Value.ToUpperInvariant() switch
+ {
+ "USER" => new ResponsesUserMessageItemParam { Content = messageContent },
+ "ASSISTANT" => new ResponsesAssistantMessageItemParam { Content = messageContent },
+ "SYSTEM" => new ResponsesSystemMessageItemParam { Content = messageContent },
+ "DEVELOPER" => new ResponsesDeveloperMessageItemParam { Content = messageContent },
+ _ => new ResponsesUserMessageItemParam { Content = messageContent }
+ };
+ }
+ }
+
+ ///
+ /// Converts a ChatMessage to a single ItemParam (message type) when it contains only message content.
+ /// For messages with function calls, use ToItemParams instead.
+ ///
+ /// The chat message to convert.
+ /// A ResponsesMessageItemParam.
+ /// Thrown when the message contains function calls or function results.
+ public static ResponsesMessageItemParam ToMessageItemParam(this ChatMessage message)
+ {
+ // Check if the message contains function calls or function results
+ if (message.Contents.Any(c => c is FunctionCallContent or FunctionResultContent))
+ {
+ throw new InvalidOperationException("Cannot convert a ChatMessage with function calls or function results to a single MessageItemParam. Use ToItemParams instead.");
+ }
+
+ // Convert all contents to ItemContent
+ List contents = [];
+ foreach (AIContent content in message.Contents)
+ {
+ if (ItemContentConverter.ToItemContent(content) is { } itemContent)
+ {
+ contents.Add(itemContent);
+ }
+ }
+
+ // Create InputMessageContent
+ InputMessageContent messageContent = contents.Count > 0
+ ? InputMessageContent.FromContents(contents)
+ : InputMessageContent.FromText(message.Text ?? string.Empty);
+
+ // Create the appropriate message item param based on role
+ return message.Role.Value.ToUpperInvariant() switch
+ {
+ "USER" => new ResponsesUserMessageItemParam { Content = messageContent },
+ "ASSISTANT" => new ResponsesAssistantMessageItemParam { Content = messageContent },
+ "SYSTEM" => new ResponsesSystemMessageItemParam { Content = messageContent },
+ "DEVELOPER" => new ResponsesDeveloperMessageItemParam { Content = messageContent },
+ _ => new ResponsesUserMessageItemParam { Content = messageContent }
+ };
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs
index f90e47b070..e777bb6e16 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs
@@ -77,10 +77,14 @@ internal sealed class HostedAgentResponseExecutor : IResponseExecutor
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
string agentName = GetAgentName(request)!;
- AIAgent agent = this._serviceProvider.GetRequiredKeyedService(agentName);
+ var conversationId = request.Conversation?.Id;
+
+ var agent = this._serviceProvider.GetRequiredKeyedService(agentName);
+ var threadStore = this._serviceProvider.GetKeyedService(agent.Name);
+
var chatOptions = new ChatOptions
{
- ConversationId = request.Conversation?.Id,
+ ConversationId = conversationId,
Temperature = (float?)request.Temperature,
TopP = (float?)request.TopP,
MaxOutputTokens = request.MaxOutputTokens,
@@ -90,16 +94,27 @@ internal sealed class HostedAgentResponseExecutor : IResponseExecutor
var options = new ChatClientAgentRunOptions(chatOptions);
var messages = new List();
+ AgentThread? thread = default;
+ if (conversationId is not null && threadStore is not null)
+ {
+ thread = await threadStore.GetThreadAsync(agent, conversationId, cancellationToken).ConfigureAwait(false);
+ }
+
foreach (var inputMessage in request.Input.GetInputMessages())
{
messages.Add(inputMessage.ToChatMessage());
}
- await foreach (var streamingEvent in agent.RunStreamingAsync(messages, options: options, cancellationToken: cancellationToken)
+ await foreach (var streamingEvent in agent.RunStreamingAsync(messages, thread, options: options, cancellationToken: cancellationToken)
.ToStreamingResponseAsync(request, context, cancellationToken).ConfigureAwait(false))
{
yield return streamingEvent;
}
+
+ if (conversationId is not null && threadStore is not null && thread is not null)
+ {
+ await threadStore.SaveThreadAsync(agent, conversationId, thread, cancellationToken).ConfigureAwait(false);
+ }
}
///