.NET: API to manage AgentThreads in hosting scenarios (#1520)

* skeleton

* wip

* rename + fix tests

* implement workflow tests

* fix comments

* Update dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fixes

* proto

* fix worfklow build logic

* build it / no reflection / no generics / extensions on aiagent

* rollback + new overload on workflow builder

* address PR comments

* fix build

* take from main

* correct based on latest API

* apply suggestion

* address PR comments x1

* address PR comments 2

* renames

* *With*

* refactor api a bit

* refactor + merge main + apply suggestions

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Korolev Dmitry
2025-10-24 13:17:25 +02:00
committed by GitHub
Unverified
parent 31701dbb92
commit 73eb00b37b
9 changed files with 369 additions and 30 deletions
@@ -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();
@@ -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) =>
{
@@ -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
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
/// <param name="path">The route group to use for A2A endpoints.</param>
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
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, _ => { });
/// <summary>
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
/// <param name="path">The route group to use for A2A endpoints.</param>
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, Action<ITaskManager> configureTaskManager)
{
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
return endpoints.MapA2A(agent, path);
return endpoints.MapA2A(agent, path, configureTaskManager);
}
/// <summary>
@@ -42,10 +55,27 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
/// discovery mechanism.
/// </remarks>
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, _ => { });
/// <summary>
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
/// <param name="path">The route group to use for A2A endpoints.</param>
/// <param name="agentCard">Agent card info to return on query.</param>
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
/// <remarks>
/// This method can be used to access A2A agents that support the
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
/// discovery mechanism.
/// </remarks>
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager)
{
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
return endpoints.MapA2A(agent, path, agentCard);
return endpoints.MapA2A(agent, path, agentCard, configureTaskManager);
}
/// <summary>
@@ -55,11 +85,26 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
/// <param name="agent">The agent to use for A2A protocol integration.</param>
/// <param name="path">The route group to use for A2A endpoints.</param>
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
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, _ => { });
/// <summary>
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agent">The agent to use for A2A protocol integration.</param>
/// <param name="path">The route group to use for A2A endpoints.</param>
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, Action<ITaskManager> configureTaskManager)
{
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
var taskManager = agent.MapA2A(loggerFactory: loggerFactory);
return endpoints.MapA2A(taskManager, path);
var agentThreadStore = endpoints.ServiceProvider.GetKeyedService<AgentThreadStore>(agent.Name);
var taskManager = agent.MapA2A(loggerFactory: loggerFactory, agentThreadStore: agentThreadStore);
var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
configureTaskManager(taskManager);
return endpointConventionBuilder;
}
/// <summary>
@@ -75,11 +120,33 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
/// discovery mechanism.
/// </remarks>
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, _ => { });
/// <summary>
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
/// <param name="agent">The agent to use for A2A protocol integration.</param>
/// <param name="path">The route group to use for A2A endpoints.</param>
/// <param name="agentCard">Agent card info to return on query.</param>
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
/// <remarks>
/// This method can be used to access A2A agents that support the
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
/// discovery mechanism.
/// </remarks>
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager)
{
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
var taskManager = agent.MapA2A(agentCard: agentCard, loggerFactory: loggerFactory);
return endpoints.MapA2A(taskManager, path);
var agentThreadStore = endpoints.ServiceProvider.GetKeyedService<AgentThreadStore>(agent.Name);
var taskManager = agent.MapA2A(agentCard: agentCard, agentThreadStore: agentThreadStore, loggerFactory: loggerFactory);
var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
configureTaskManager(taskManager);
return endpointConventionBuilder;
}
/// <summary>
@@ -90,14 +157,12 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
/// <param name="taskManager">Pre-configured A2A TaskManager to use for A2A endpoints handling.</param>
/// <param name="path">The route group to use for A2A endpoints.</param>
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
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);
}
}
@@ -20,29 +20,37 @@ public static class AIAgentExtensions
/// <param name="agent">Agent to attach A2A messaging processing capabilities to.</param>
/// <param name="taskManager">Instance of <see cref="TaskManager"/> to configure for A2A messaging. New instance will be created if not passed.</param>
/// <param name="loggerFactory">The logger factory to use for creating <see cref="ILogger"/> instances.</param>
/// <param name="agentThreadStore">The store to store thread contents and metadata.</param>
/// <returns>The configured <see cref="TaskManager"/>.</returns>
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<A2AResponse> 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
/// <param name="agentCard">The agent card to return on query.</param>
/// <param name="taskManager">Instance of <see cref="TaskManager"/> to configure for A2A messaging. New instance will be created if not passed.</param>
/// <param name="loggerFactory">The logger factory to use for creating <see cref="ILogger"/> instances.</param>
/// <param name="agentThreadStore">The store to store thread contents and metadata.</param>
/// <returns>The configured <see cref="TaskManager"/>.</returns>
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) =>
{
@@ -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;
/// <summary>
/// Provides a hosting wrapper around an <see cref="AIAgent"/> that adds thread persistence capabilities
/// for server-hosted scenarios where conversations need to be restored across requests.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="AIHostAgent"/> wraps an existing agent implementation and adds the ability to
/// persist and restore conversation threads using an <see cref="AgentThreadStore"/>.
/// </para>
/// <para>
/// This wrapper enables thread persistence without requiring type-specific knowledge of the thread type,
/// as all thread operations work through the base <see cref="AgentThread"/> abstraction.
/// </para>
/// </remarks>
public class AIHostAgent : DelegatingAIAgent
{
private readonly AgentThreadStore _threadStore;
/// <summary>
/// Initializes a new instance of the <see cref="AIHostAgent"/> class.
/// </summary>
/// <param name="innerAgent">The underlying agent implementation to wrap.</param>
/// <param name="threadStore">The thread store to use for persisting conversation state.</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="innerAgent"/> or <paramref name="threadStore"/> is <see langword="null"/>.
/// </exception>
public AIHostAgent(AIAgent innerAgent, AgentThreadStore threadStore)
: base(innerAgent)
{
this._threadStore = Throw.IfNull(threadStore);
}
/// <summary>
/// Gets an existing agent thread for the specified conversation, or creates a new one if none exists.
/// </summary>
/// <param name="conversationId">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.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <returns>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.</returns>
public ValueTask<AgentThread> GetOrCreateThreadAsync(string conversationId, CancellationToken cancellationToken = default)
{
_ = Throw.IfNullOrWhitespace(conversationId);
return this._threadStore.GetThreadAsync(this.InnerAgent, conversationId, cancellationToken);
}
/// <summary>
/// Persists a conversation thread to the thread store.
/// </summary>
/// <param name="conversationId">The unique identifier for the conversation.</param>
/// <param name="thread">The thread to persist.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
/// <exception cref="ArgumentException"><paramref name="conversationId"/> is null or whitespace.</exception>
/// <exception cref="ArgumentNullException"><paramref name="thread"/> is <see langword="null"/>.</exception>
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);
}
}
@@ -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;
/// <summary>
/// Provides extension methods for configuring <see cref="AIAgent"/>.
/// </summary>
public static class HostedAgentBuilderExtensions
{
/// <summary>
/// Configures the host agent builder to use an in-memory thread store for agent thread management.
/// </summary>
/// <param name="builder">The host agent builder to configure with the in-memory thread store.</param>
/// <returns>The same <paramref name="builder"/> instance, configured to use an in-memory thread store.</returns>
public static IHostedAgentBuilder WithInMemoryThreadStore(this IHostedAgentBuilder builder)
{
builder.ServiceCollection.AddKeyedSingleton<AgentThreadStore>(builder.Name, new InMemoryAgentThreadStore());
return builder;
}
/// <summary>
/// Registers the specified agent thread store with the host agent builder, enabling thread-specific storage for
/// agent operations.
/// </summary>
/// <param name="builder">The host agent builder to configure with the thread store. Cannot be null.</param>
/// <param name="store">The agent thread store instance to register. Cannot be null.</param>
/// <returns>The same host agent builder instance, allowing for method chaining.</returns>
public static IHostedAgentBuilder WithThreadStore(this IHostedAgentBuilder builder, AgentThreadStore store)
{
builder.ServiceCollection.AddKeyedSingleton(builder.Name, store);
return builder;
}
/// <summary>
/// Configures the host agent builder to use a custom thread store implementation for agent threads.
/// </summary>
/// <param name="builder">The host agent builder to configure.</param>
/// <param name="createAgentThreadStore">A factory function that creates an agent thread store instance using the provided service provider and agent
/// name.</param>
/// <returns>The same host agent builder instance, enabling further configuration.</returns>
public static IHostedAgentBuilder WithThreadStore(this IHostedAgentBuilder builder, Func<IServiceProvider, string, AgentThreadStore> 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;
}
}
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.Hosting;
/// <summary>
/// Defines the contract for storing and retrieving agent conversation threads.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public abstract class AgentThreadStore
{
/// <summary>
/// Saves a serialized agent thread to persistent storage.
/// </summary>
/// <param name="agent">The agent that owns this thread.</param>
/// <param name="conversationId">The unique identifier for the conversation/thread.</param>
/// <param name="thread">The thread to save.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
public abstract ValueTask SaveThreadAsync(
AIAgent agent,
string conversationId,
AgentThread thread,
CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a serialized agent thread from persistent storage.
/// </summary>
/// <param name="agent">The agent that owns this thread.</param>
/// <param name="conversationId">The unique identifier for the conversation/thread to retrieve.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>
/// A task that represents the asynchronous retrieval operation.
/// The task result contains the serialized thread state, or <see langword="null"/> if not found.
/// </returns>
public abstract ValueTask<AgentThread> GetThreadAsync(
AIAgent agent,
string conversationId,
CancellationToken cancellationToken = default);
}
@@ -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;
/// <summary>
/// Provides an in-memory implementation of <see cref="AgentThreadStore"/> for development and testing scenarios.
/// </summary>
/// <remarks>
/// <para>
/// This implementation stores threads in memory using a concurrent dictionary and is suitable for:
/// <list type="bullet">
/// <item><description>Single-instance development scenarios</description></item>
/// <item><description>Testing and prototyping</description></item>
/// <item><description>Scenarios where thread persistence across restarts is not required</description></item>
/// </list>
/// </para>
/// <para>
/// <strong>Warning:</strong> 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.
/// </para>
/// </remarks>
public sealed class InMemoryAgentThreadStore : AgentThreadStore
{
private readonly ConcurrentDictionary<string, JsonElement> _threads = new();
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public override ValueTask<AgentThread> 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<AgentThread>(agent.GetNewThread()),
_ => new ValueTask<AgentThread>(agent.DeserializeThread(threadContent.Value)),
};
}
private static string GetKey(string conversationId, string agentId) => $"{agentId}:{conversationId}";
}
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.Hosting;
/// <summary>
/// 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.
/// </summary>
public sealed class NoopAgentThreadStore : AgentThreadStore
{
/// <inheritdoc/>
public override ValueTask SaveThreadAsync(AIAgent agent, string conversationId, AgentThread thread, CancellationToken cancellationToken = default)
{
return new ValueTask();
}
/// <inheritdoc/>
public override ValueTask<AgentThread> GetThreadAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default)
{
return new ValueTask<AgentThread>(agent.GetNewThread());
}
}