diff --git a/dotnet/samples/A2AClientServer/A2AServer/Program.cs b/dotnet/samples/A2AClientServer/A2AServer/Program.cs
index e4f619dc54..bd344c46c8 100644
--- a/dotnet/samples/A2AClientServer/A2AServer/Program.cs
+++ b/dotnet/samples/A2AClientServer/A2AServer/Program.cs
@@ -104,7 +104,10 @@ else
throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentId must be provided");
}
-var a2aTaskManager = app.MapA2A(hostA2AAgent, path: "/", agentCard: hostA2AAgentCard);
-app.MapWellKnownAgentCard(a2aTaskManager, "/");
+var a2aTaskManager = app.MapA2A(
+ hostA2AAgent,
+ path: "/",
+ agentCard: hostA2AAgentCard,
+ taskManager => app.MapWellKnownAgentCard(taskManager, "/"));
await app.RunAsync();
diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs
index da27df46e8..571b07b1d5 100644
--- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs
+++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs
@@ -24,7 +24,8 @@ builder.AddAIAgent(
"pirate",
instructions: "You are a pirate. Speak like a pirate",
description: "An agent that speaks like a pirate.",
- chatClientServiceKey: "chat-model");
+ chatClientServiceKey: "chat-model")
+ .WithInMemoryThreadStore();
builder.AddAIAgent("knights-and-knaves", (sp, key) =>
{
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/EndpointRouteBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/EndpointRouteBuilderExtensions.cs
index 85ef1e3e2c..cae9801148 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/EndpointRouteBuilderExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/EndpointRouteBuilderExtensions.cs
@@ -1,8 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using A2A;
using A2A.AspNetCore;
using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Hosting.A2A;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
@@ -23,10 +25,21 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
/// The name of the agent to use for A2A protocol integration.
/// The route group to use for A2A endpoints.
/// Configured for A2A integration.
- public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path)
+ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path)
+ => endpoints.MapA2A(agentName, path, _ => { });
+
+ ///
+ /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
+ ///
+ /// The to add the A2A endpoints to.
+ /// The name of the agent to use for A2A protocol integration.
+ /// The route group to use for A2A endpoints.
+ /// The callback to configure .
+ /// Configured for A2A integration.
+ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, Action configureTaskManager)
{
var agent = endpoints.ServiceProvider.GetRequiredKeyedService(agentName);
- return endpoints.MapA2A(agent, path);
+ return endpoints.MapA2A(agent, path, configureTaskManager);
}
///
@@ -42,10 +55,27 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
/// Curated Registries (Catalog-Based Discovery)
/// discovery mechanism.
///
- public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard)
+ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard)
+ => endpoints.MapA2A(agentName, path, agentCard, _ => { });
+
+ ///
+ /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
+ ///
+ /// The to add the A2A endpoints to.
+ /// The name of the agent to use for A2A protocol integration.
+ /// The route group to use for A2A endpoints.
+ /// Agent card info to return on query.
+ /// The callback to configure .
+ /// Configured for A2A integration.
+ ///
+ /// This method can be used to access A2A agents that support the
+ /// Curated Registries (Catalog-Based Discovery)
+ /// discovery mechanism.
+ ///
+ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard, Action configureTaskManager)
{
var agent = endpoints.ServiceProvider.GetRequiredKeyedService(agentName);
- return endpoints.MapA2A(agent, path, agentCard);
+ return endpoints.MapA2A(agent, path, agentCard, configureTaskManager);
}
///
@@ -55,11 +85,26 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
/// The agent to use for A2A protocol integration.
/// The route group to use for A2A endpoints.
/// Configured for A2A integration.
- public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
+ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
+ => endpoints.MapA2A(agent, path, _ => { });
+
+ ///
+ /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
+ ///
+ /// The to add the A2A endpoints to.
+ /// The agent to use for A2A protocol integration.
+ /// The route group to use for A2A endpoints.
+ /// The callback to configure .
+ /// Configured for A2A integration.
+ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, Action configureTaskManager)
{
var loggerFactory = endpoints.ServiceProvider.GetRequiredService();
- var taskManager = agent.MapA2A(loggerFactory: loggerFactory);
- return endpoints.MapA2A(taskManager, path);
+ var agentThreadStore = endpoints.ServiceProvider.GetKeyedService(agent.Name);
+ var taskManager = agent.MapA2A(loggerFactory: loggerFactory, agentThreadStore: agentThreadStore);
+ var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
+
+ configureTaskManager(taskManager);
+ return endpointConventionBuilder;
}
///
@@ -75,11 +120,33 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
/// Curated Registries (Catalog-Based Discovery)
/// discovery mechanism.
///
- public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard)
+ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard)
+ => endpoints.MapA2A(agent, path, agentCard, _ => { });
+
+ ///
+ /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
+ ///
+ /// The to add the A2A endpoints to.
+ /// The agent to use for A2A protocol integration.
+ /// The route group to use for A2A endpoints.
+ /// Agent card info to return on query.
+ /// The callback to configure .
+ /// Configured for A2A integration.
+ ///
+ /// This method can be used to access A2A agents that support the
+ /// Curated Registries (Catalog-Based Discovery)
+ /// discovery mechanism.
+ ///
+ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard, Action configureTaskManager)
{
var loggerFactory = endpoints.ServiceProvider.GetRequiredService();
- var taskManager = agent.MapA2A(agentCard: agentCard, loggerFactory: loggerFactory);
- return endpoints.MapA2A(taskManager, path);
+ var agentThreadStore = endpoints.ServiceProvider.GetKeyedService(agent.Name);
+ var taskManager = agent.MapA2A(agentCard: agentCard, agentThreadStore: agentThreadStore, loggerFactory: loggerFactory);
+ var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
+
+ configureTaskManager(taskManager);
+
+ return endpointConventionBuilder;
}
///
@@ -90,14 +157,12 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
/// Pre-configured A2A TaskManager to use for A2A endpoints handling.
/// The route group to use for A2A endpoints.
/// Configured for A2A integration.
- public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, TaskManager taskManager, string path)
+ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, ITaskManager taskManager, string path)
{
// note: current SDK version registers multiple `.well-known/agent.json` handlers here.
// it makes app return HTTP 500, but will be fixed once new A2A SDK is released.
// see https://github.com/microsoft/agent-framework/issues/476 for details
A2ARouteBuilderExtensions.MapA2A(endpoints, taskManager, path);
- endpoints.MapHttpA2A(taskManager, path);
-
- return taskManager;
+ return endpoints.MapHttpA2A(taskManager, path);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs
index 81d33b5506..43376d8fb2 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs
@@ -20,29 +20,37 @@ public static class AIAgentExtensions
/// Agent to attach A2A messaging processing capabilities to.
/// Instance of to configure for A2A messaging. New instance will be created if not passed.
/// The logger factory to use for creating instances.
+ /// The store to store thread contents and metadata.
/// The configured .
- public static TaskManager MapA2A(
+ public static ITaskManager MapA2A(
this AIAgent agent,
- TaskManager? taskManager = null,
- ILoggerFactory? loggerFactory = null)
+ ITaskManager? taskManager = null,
+ ILoggerFactory? loggerFactory = null,
+ AgentThreadStore? agentThreadStore = null)
{
ArgumentNullException.ThrowIfNull(agent);
ArgumentNullException.ThrowIfNull(agent.Name);
- taskManager ??= new();
+ var hostAgent = new AIHostAgent(
+ innerAgent: agent,
+ threadStore: agentThreadStore ?? new NoopAgentThreadStore());
+ taskManager ??= new TaskManager();
taskManager.OnMessageReceived += OnMessageReceivedAsync;
-
return taskManager;
async Task OnMessageReceivedAsync(MessageSendParams messageSendParams, CancellationToken cancellationToken)
{
- var response = await agent.RunAsync(
- messageSendParams.ToChatMessages(),
- cancellationToken: cancellationToken).ConfigureAwait(false);
var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N");
- var parts = response.Messages.ToParts();
+ var thread = await hostAgent.GetOrCreateThreadAsync(contextId, cancellationToken).ConfigureAwait(false);
+ var response = await hostAgent.RunAsync(
+ messageSendParams.ToChatMessages(),
+ thread: thread,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ await hostAgent.SaveThreadAsync(contextId, thread, cancellationToken).ConfigureAwait(false);
+ var parts = response.Messages.ToParts();
return new AgentMessage
{
MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
@@ -60,14 +68,16 @@ public static class AIAgentExtensions
/// The agent card to return on query.
/// Instance of to configure for A2A messaging. New instance will be created if not passed.
/// The logger factory to use for creating instances.
+ /// The store to store thread contents and metadata.
/// The configured .
- public static TaskManager MapA2A(
+ public static ITaskManager MapA2A(
this AIAgent agent,
AgentCard agentCard,
- TaskManager? taskManager = null,
- ILoggerFactory? loggerFactory = null)
+ ITaskManager? taskManager = null,
+ ILoggerFactory? loggerFactory = null,
+ AgentThreadStore? agentThreadStore = null)
{
- taskManager = agent.MapA2A(taskManager, loggerFactory);
+ taskManager = agent.MapA2A(taskManager, loggerFactory, agentThreadStore);
taskManager.OnAgentCardQuery += (context, query) =>
{
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AIHostAgent.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AIHostAgent.cs
new file mode 100644
index 0000000000..c11a630ffe
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting/AIHostAgent.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.Hosting;
+
+///
+/// Provides a hosting wrapper around an that adds thread persistence capabilities
+/// for server-hosted scenarios where conversations need to be restored across requests.
+///
+///
+///
+/// wraps an existing agent implementation and adds the ability to
+/// persist and restore conversation threads using an .
+///
+///
+/// This wrapper enables thread persistence without requiring type-specific knowledge of the thread type,
+/// as all thread operations work through the base abstraction.
+///
+///
+public class AIHostAgent : DelegatingAIAgent
+{
+ private readonly AgentThreadStore _threadStore;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The underlying agent implementation to wrap.
+ /// The thread store to use for persisting conversation state.
+ ///
+ /// or is .
+ ///
+ public AIHostAgent(AIAgent innerAgent, AgentThreadStore threadStore)
+ : base(innerAgent)
+ {
+ this._threadStore = Throw.IfNull(threadStore);
+ }
+
+ ///
+ /// Gets an existing agent thread for the specified conversation, or creates a new one if none exists.
+ ///
+ /// The unique identifier of the conversation for which to retrieve or create the agent thread. Cannot be null,
+ /// empty, or consist only of white-space characters.
+ /// A cancellation token that can be used to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation. The task result contains the agent thread associated with the
+ /// specified conversation. If no thread exists, a new thread is created and returned.
+ public ValueTask GetOrCreateThreadAsync(string conversationId, CancellationToken cancellationToken = default)
+ {
+ _ = Throw.IfNullOrWhitespace(conversationId);
+
+ return this._threadStore.GetThreadAsync(this.InnerAgent, conversationId, cancellationToken);
+ }
+
+ ///
+ /// Persists a conversation thread to the thread store.
+ ///
+ /// The unique identifier for the conversation.
+ /// The thread to persist.
+ /// The to monitor for cancellation requests.
+ /// A task that represents the asynchronous save operation.
+ /// is null or whitespace.
+ /// is .
+ public ValueTask SaveThreadAsync(string conversationId, AgentThread thread, CancellationToken cancellationToken = default)
+ {
+ _ = Throw.IfNullOrWhitespace(conversationId);
+ _ = Throw.IfNull(thread);
+
+ return this._threadStore.SaveThreadAsync(this.InnerAgent, conversationId, thread, cancellationToken);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs
new file mode 100644
index 0000000000..902c54ebe9
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.Hosting;
+
+///
+/// Provides extension methods for configuring .
+///
+public static class HostedAgentBuilderExtensions
+{
+ ///
+ /// Configures the host agent builder to use an in-memory thread store for agent thread management.
+ ///
+ /// The host agent builder to configure with the in-memory thread store.
+ /// The same instance, configured to use an in-memory thread store.
+ public static IHostedAgentBuilder WithInMemoryThreadStore(this IHostedAgentBuilder builder)
+ {
+ builder.ServiceCollection.AddKeyedSingleton(builder.Name, new InMemoryAgentThreadStore());
+ return builder;
+ }
+
+ ///
+ /// Registers the specified agent thread store with the host agent builder, enabling thread-specific storage for
+ /// agent operations.
+ ///
+ /// The host agent builder to configure with the thread store. Cannot be null.
+ /// The agent thread store instance to register. Cannot be null.
+ /// The same host agent builder instance, allowing for method chaining.
+ public static IHostedAgentBuilder WithThreadStore(this IHostedAgentBuilder builder, AgentThreadStore store)
+ {
+ builder.ServiceCollection.AddKeyedSingleton(builder.Name, store);
+ return builder;
+ }
+
+ ///
+ /// Configures the host agent builder to use a custom thread store implementation for agent threads.
+ ///
+ /// The host agent builder to configure.
+ /// A factory function that creates an agent thread store instance using the provided service provider and agent
+ /// name.
+ /// The same host agent builder instance, enabling further configuration.
+ public static IHostedAgentBuilder WithThreadStore(this IHostedAgentBuilder builder, Func createAgentThreadStore)
+ {
+ builder.ServiceCollection.AddKeyedSingleton(builder.Name, (sp, key) =>
+ {
+ Throw.IfNull(key);
+ var keyString = key as string;
+ Throw.IfNullOrEmpty(keyString);
+ var store = createAgentThreadStore(sp, keyString);
+ if (store is null)
+ {
+ throw new InvalidOperationException($"The agent thread store factory did not return a valid {nameof(AgentThreadStore)} instance for key '{keyString}'.");
+ }
+
+ return store;
+ });
+ return builder;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/IAgentThreadStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/IAgentThreadStore.cs
new file mode 100644
index 0000000000..f95999dde3
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting/IAgentThreadStore.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Agents.AI.Hosting;
+
+///
+/// Defines the contract for storing and retrieving agent conversation threads.
+///
+///
+/// Implementations of this interface enable persistent storage of conversation threads,
+/// allowing conversations to be resumed across HTTP requests, application restarts,
+/// or different service instances in hosted scenarios.
+///
+public abstract class AgentThreadStore
+{
+ ///
+ /// Saves a serialized agent thread to persistent storage.
+ ///
+ /// The agent that owns this thread.
+ /// The unique identifier for the conversation/thread.
+ /// The thread to save.
+ /// The to monitor for cancellation requests.
+ /// A task that represents the asynchronous save operation.
+ public abstract ValueTask SaveThreadAsync(
+ AIAgent agent,
+ string conversationId,
+ AgentThread thread,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Retrieves a serialized agent thread from persistent storage.
+ ///
+ /// The agent that owns this thread.
+ /// The unique identifier for the conversation/thread to retrieve.
+ /// The to monitor for cancellation requests.
+ ///
+ /// A task that represents the asynchronous retrieval operation.
+ /// The task result contains the serialized thread state, or if not found.
+ ///
+ public abstract ValueTask GetThreadAsync(
+ AIAgent agent,
+ string conversationId,
+ CancellationToken cancellationToken = default);
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentThreadStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentThreadStore.cs
new file mode 100644
index 0000000000..74bbe279fb
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentThreadStore.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Concurrent;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Agents.AI.Hosting;
+
+///
+/// Provides an in-memory implementation of for development and testing scenarios.
+///
+///
+///
+/// This implementation stores threads in memory using a concurrent dictionary and is suitable for:
+///
+/// - Single-instance development scenarios
+/// - Testing and prototyping
+/// - Scenarios where thread persistence across restarts is not required
+///
+///
+///
+/// Warning: All stored threads will be lost when the application restarts.
+/// For production use with multiple instances or persistence across restarts, use a durable storage implementation
+/// such as Redis, SQL Server, or Azure Cosmos DB.
+///
+///
+public sealed class InMemoryAgentThreadStore : AgentThreadStore
+{
+ private readonly ConcurrentDictionary _threads = new();
+
+ ///
+ public override ValueTask SaveThreadAsync(AIAgent agent, string conversationId, AgentThread thread, CancellationToken cancellationToken = default)
+ {
+ var key = GetKey(conversationId, agent.Id);
+ this._threads[key] = thread.Serialize();
+ return default;
+ }
+
+ ///
+ public override ValueTask GetThreadAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default)
+ {
+ var key = GetKey(conversationId, agent.Id);
+ JsonElement? threadContent = this._threads.TryGetValue(key, out var existingThread) ? existingThread : null;
+
+ return threadContent switch
+ {
+ null => new ValueTask(agent.GetNewThread()),
+ _ => new ValueTask(agent.DeserializeThread(threadContent.Value)),
+ };
+ }
+
+ private static string GetKey(string conversationId, string agentId) => $"{agentId}:{conversationId}";
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/NoopAgentThreadStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/NoopAgentThreadStore.cs
new file mode 100644
index 0000000000..c94489d0b0
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting/NoopAgentThreadStore.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Agents.AI.Hosting;
+
+///
+/// This store implementation does not have any store under the hood and operates with empty threads.
+/// It is the "noop" store, and could be used if you are keeping the thread contents on the client side for example.
+///
+public sealed class NoopAgentThreadStore : AgentThreadStore
+{
+ ///
+ public override ValueTask SaveThreadAsync(AIAgent agent, string conversationId, AgentThread thread, CancellationToken cancellationToken = default)
+ {
+ return new ValueTask();
+ }
+
+ ///
+ public override ValueTask GetThreadAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default)
+ {
+ return new ValueTask(agent.GetNewThread());
+ }
+}