.NET: [BREAKING] Change GetNewThread and DeserializeThread to async (#3152)

* Change GetNewThread and DeserializeThread plus ChatMessageStore and AIContextProvider Factories to async

* Merge fixes
This commit is contained in:
westey
2026-01-12 11:25:51 +00:00
committed by GitHub
Unverified
parent 6e3bc219e0
commit bb6ecd9c71
113 changed files with 538 additions and 477 deletions
@@ -42,7 +42,7 @@ public static class Program
// Create the Host agent
var hostAgent = new HostClientAgent(loggerFactory);
await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(";"));
AgentThread thread = hostAgent.Agent!.GetNewThread();
AgentThread thread = await hostAgent.Agent!.GetNewThreadAsync(cancellationToken);
try
{
while (true)
@@ -88,7 +88,7 @@ public static class Program
description: "AG-UI Client Agent",
tools: [changeBackground, readClientClimateSensors]);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync(cancellationToken);
List<ChatMessage> messages = [new(ChatRole.System, "You are a helpful assistant.")];
try
{
@@ -19,7 +19,7 @@ public static class FunctionTriggers
public static async Task<string> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{
DurableAIAgent writer = context.GetAgent("WriterAgent");
AgentThread writerThread = writer.GetNewThread();
AgentThread writerThread = await writer.GetNewThreadAsync();
AgentRunResponse<TextResponse> initial = await writer.RunAsync<TextResponse>(
message: "Write a concise inspirational sentence about learning.",
@@ -21,7 +21,7 @@ public static class FunctionTriggers
// Get the spam detection agent
DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent");
AgentThread spamThread = spamDetectionAgent.GetNewThread();
AgentThread spamThread = await spamDetectionAgent.GetNewThreadAsync();
// Step 1: Check if the email is spam
AgentRunResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
@@ -43,7 +43,7 @@ public static class FunctionTriggers
// Generate and send response for legitimate email
DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent");
AgentThread emailThread = emailAssistantAgent.GetNewThread();
AgentThread emailThread = await emailAssistantAgent.GetNewThreadAsync();
AgentRunResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
message:
@@ -24,7 +24,7 @@ public static class FunctionTriggers
// Get the writer agent
DurableAIAgent writerAgent = context.GetAgent("WriterAgent");
AgentThread writerThread = writerAgent.GetNewThread();
AgentThread writerThread = await writerAgent.GetNewThreadAsync();
// Set initial status
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
@@ -20,7 +20,7 @@ public static class FunctionTriggers
// Get the writer agent
DurableAIAgent writerAgent = context.GetAgent("Writer");
AgentThread writerThread = writerAgent.GetNewThread();
AgentThread writerThread = await writerAgent.GetNewThreadAsync();
// Set initial status
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
@@ -95,7 +95,7 @@ public sealed class FunctionTriggers
AIAgent agentProxy = durableClient.AsDurableAgentProxy(context, "TravelPlanner");
// Create a new agent thread
AgentThread thread = agentProxy.GetNewThread();
AgentThread thread = await agentProxy.GetNewThreadAsync(cancellationToken);
string agentSessionId = thread.GetService<AgentSessionId>().ToString();
this._logger.LogInformation("Creating new agent session: {AgentSessionId}", agentSessionId);
@@ -16,7 +16,7 @@ AgentCard agentCard = await agentCardResolver.GetAgentCardAsync();
// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
AIAgent agent = agentCard.GetAIAgent();
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
// Start the initial run with a long-running task.
AgentRunResponse response = await agent.RunAsync("Conduct a comprehensive analysis of quantum computing applications in cryptography, including recent breakthroughs, implementation challenges, and future roadmap. Please include diagrams and visual representations to illustrate complex concepts.", thread);
@@ -20,7 +20,7 @@ AIAgent agent = chatClient.CreateAIAgent(
name: "agui-client",
description: "AG-UI Client Agent");
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helpful assistant.")
@@ -20,7 +20,7 @@ AIAgent agent = chatClient.CreateAIAgent(
name: "agui-client",
description: "AG-UI Client Agent");
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helpful assistant.")
@@ -33,7 +33,7 @@ AIAgent agent = chatClient.CreateAIAgent(
description: "AG-UI Client Agent",
tools: frontendTools);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helpful assistant.")
@@ -30,7 +30,7 @@ JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web)
};
StatefulAgent<AgentState> agent = new(baseAgent, jsonOptions, new AgentState());
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helpful recipe assistant.")
@@ -128,7 +128,7 @@ var agent = new ChatClientAgent(instrumentedChatClient,
.UseOpenTelemetry(SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
.Build();
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
appLogger.LogInformation("Agent created successfully with ID: {AgentId}", agent.Id);
@@ -31,7 +31,7 @@ AIAgent agent2 = await persistentAgentsClient.CreateAIAgentAsync(
instructions: JokerInstructions);
// You can then invoke the agent like any other AIAgent.
AgentThread thread = agent1.GetNewThread();
AgentThread thread = await agent1.GetNewThreadAsync();
Console.WriteLine(await agent1.RunAsync("Tell me a joke about a pirate.", thread));
// Cleanup for sample purposes.
@@ -40,7 +40,7 @@ var latestAgentVersion = jokerAgentLatest.GetService<AgentVersion>()!;
Console.WriteLine($"Latest agent version id: {latestAgentVersion.Id}");
// Once you have the AIAgent, you can invoke it like any other AIAgent.
AgentThread thread = jokerAgentLatest.GetNewThread();
AgentThread thread = await jokerAgentLatest.GetNewThreadAsync();
Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate.", thread));
// This will use the same thread to continue the conversation.
@@ -28,16 +28,16 @@ namespace SampleApp
{
public override string? Name => "UpperCaseParrotAgent";
public override AgentThread GetNewThread()
=> new CustomAgentThread();
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
=> new(new CustomAgentThread());
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
=> new CustomAgentThread(serializedThread, jsonSerializerOptions);
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new CustomAgentThread(serializedThread, jsonSerializerOptions));
protected override async Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
// Create a thread if the user didn't supply one.
thread ??= this.GetNewThread();
thread ??= await this.GetNewThreadAsync(cancellationToken);
if (thread is not CustomAgentThread typedThread)
{
@@ -69,7 +69,7 @@ namespace SampleApp
protected override async IAsyncEnumerable<AgentRunResponseUpdate> RunCoreStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Create a thread if the user didn't supply one.
thread ??= this.GetNewThread();
thread ??= await this.GetNewThreadAsync(cancellationToken);
if (thread is not CustomAgentThread typedThread)
{
@@ -33,7 +33,7 @@ AIAgent agent2 = await assistantClient.CreateAIAgentAsync(
instructions: JokerInstructions);
// You can invoke the agent like any other AIAgent.
AgentThread thread = agent1.GetNewThread();
AgentThread thread = await agent1.GetNewThreadAsync();
Console.WriteLine(await agent1.RunAsync("Tell me a joke about a pirate.", thread));
// Cleanup for sample purposes.
@@ -26,11 +26,11 @@ AIAgent agent = new AnthropicClient { APIKey = apiKey }
.CreateAIAgent(model: model, instructions: AssistantInstructions, name: AssistantName, tools: [tool]);
// Non-streaming agent interaction with function tools.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", thread));
// Streaming agent interaction with function tools.
thread = agent.GetNewThread();
thread = await agent.GetNewThreadAsync();
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync("What is the weather like in Amsterdam?", thread))
{
Console.WriteLine(update);
@@ -34,7 +34,7 @@ AIAgent agent = new AzureOpenAIClient(
{
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
AIContextProviderFactory = (ctx) => new ChatHistoryMemoryProvider(
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(new ChatHistoryMemoryProvider(
vectorStore,
collectionName: "chathistory",
vectorDimensions: 3072,
@@ -43,18 +43,18 @@ AIAgent agent = new AzureOpenAIClient(
storageScope: new() { UserId = "UID1", ThreadId = new Guid().ToString() },
// Configure the scope which would be used to search for relevant prior messages.
// In this case, we are searching for any messages for the user across all threads.
searchScope: new() { UserId = "UID1" })
searchScope: new() { UserId = "UID1" }))
});
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
// Run the agent with the thread that stores conversation history in the vector store.
Console.WriteLine(await agent.RunAsync("I like jokes about Pirates. Tell me a joke about a pirate.", thread));
// Start a second thread. Since we configured the search scope to be across all threads for the user,
// the agent should remember that the user likes pirate jokes.
AgentThread thread2 = agent.GetNewThread();
AgentThread thread2 = await agent.GetNewThreadAsync();
// Run the agent with the second thread.
Console.WriteLine(await agent.RunAsync("Tell me a joke that I might like.", thread2));
@@ -31,16 +31,16 @@ AIAgent agent = new AzureOpenAIClient(
.CreateAIAgent(new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(ctx.SerializedState.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined
// If each thread should have its own Mem0 scope, you can create a new id per thread here:
// ? new Mem0Provider(mem0HttpClient, new Mem0ProviderScope() { ThreadId = Guid.NewGuid().ToString() })
// In this case we are storing memories scoped by application and user instead so that memories are retained across threads.
? new Mem0Provider(mem0HttpClient, new Mem0ProviderScope() { ApplicationId = "getting-started-agents", UserId = "sample-user" })
// For cases where we are restoring from serialized state:
: new Mem0Provider(mem0HttpClient, ctx.SerializedState, ctx.JsonSerializerOptions)
: new Mem0Provider(mem0HttpClient, ctx.SerializedState, ctx.JsonSerializerOptions))
});
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
// Clear any existing memories for this scope to demonstrate fresh behavior.
Mem0Provider mem0Provider = thread.GetService<Mem0Provider>()!;
@@ -56,9 +56,9 @@ Console.WriteLine(await agent.RunAsync("What do you already know about my upcomi
Console.WriteLine("\n>> Serialize and deserialize the thread to demonstrate persisted state\n");
JsonElement serializedThread = thread.Serialize();
AgentThread restoredThread = agent.DeserializeThread(serializedThread);
AgentThread restoredThread = await agent.DeserializeThreadAsync(serializedThread);
Console.WriteLine(await agent.RunAsync("Can you recap the personal details you remember?", restoredThread));
Console.WriteLine("\n>> Start a new thread that shares the same Mem0 scope\n");
AgentThread newThread = agent.GetNewThread();
AgentThread newThread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("Summarize what you already know about me.", newThread));
@@ -33,11 +33,11 @@ ChatClient chatClient = new AzureOpenAIClient(
AIAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = "You are a friendly assistant. Always address the user by their name." },
AIContextProviderFactory = ctx => new UserInfoMemory(chatClient.AsIChatClient(), ctx.SerializedState, ctx.JsonSerializerOptions)
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(new UserInfoMemory(chatClient.AsIChatClient(), ctx.SerializedState, ctx.JsonSerializerOptions))
});
// Create a new thread for the conversation.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(">> Use thread with blank memory\n");
@@ -52,7 +52,7 @@ var threadElement = thread.Serialize();
Console.WriteLine("\n>> Use deserialized thread with previously created memories\n");
// Later we can deserialize the thread and continue the conversation with the previous memory component state.
var deserializedThread = agent.DeserializeThread(threadElement);
var deserializedThread = await agent.DeserializeThreadAsync(threadElement);
Console.WriteLine(await agent.RunAsync("What is my name and age?", deserializedThread));
Console.WriteLine("\n>> Read memories from memory component\n");
@@ -68,7 +68,7 @@ Console.WriteLine("\n>> Use new thread with previously created memories\n");
// It is also possible to set the memories in a memory component on an individual thread.
// This is useful if we want to start a new thread, but have it share the same memories as a previous thread.
var newThread = agent.GetNewThread();
var newThread = await agent.GetNewThreadAsync();
if (userInfo is not null && newThread.GetService<UserInfoMemory>() is UserInfoMemory newThreadMemory)
{
newThreadMemory.UserInfo = userInfo;
@@ -30,7 +30,7 @@ using JsonDocument createConversationResultAsJson = JsonDocument.Parse(createCon
string conversationId = createConversationResultAsJson.RootElement.GetProperty("id"u8)!.GetString()!;
// Create a thread for the conversation - this enables conversation state management for subsequent turns
AgentThread thread = agent.GetNewThread(conversationId);
AgentThread thread = await agent.GetNewThreadAsync(conversationId);
Console.WriteLine("=== Multi-turn Conversation Demo ===\n");
@@ -33,7 +33,7 @@ The `AgentThread` works with `ChatClientAgentRunOptions` to link the agent to a
ChatClientAgentRunOptions agentRunOptions = new() { ChatOptions = new ChatOptions() { ConversationId = conversationId } };
// Create a thread for the conversation
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
// First call links the thread to the conversation
ChatCompletion firstResponse = await agent.RunAsync([firstMessage], thread, agentRunOptions);
@@ -59,7 +59,7 @@ foreach (ClientResult result in getConversationItemsResults.GetRawPages())
1. **Create an OpenAI Client**: Initialize an `OpenAIClient` with your API key
2. **Create a Conversation**: Use `ConversationClient` to create a server-side conversation
3. **Create an Agent**: Initialize an `OpenAIResponseClientAgent` with the desired model and instructions
4. **Create a Thread**: Call `agent.GetNewThread()` to create a new conversation thread
4. **Create a Thread**: Call `agent.GetNewThreadAsync()` to create a new conversation thread
5. **Link Thread to Conversation**: Pass `ChatClientAgentRunOptions` with the `ConversationId` on the first call
6. **Send Messages**: Subsequent calls to `agent.RunAsync()` only need the thread - context is maintained
7. **Cleanup**: Delete the conversation when done using `conversationClient.DeleteConversation()`
@@ -62,15 +62,15 @@ AIAgent agent = azureOpenAIClient
.CreateAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions),
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)),
// Since we are using ChatCompletion which stores chat history locally, we can also add a message removal policy
// that removes messages produced by the TextSearchProvider before they are added to the chat history, so that
// we don't bloat chat history with all the search result messages.
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(ctx.SerializedState, ctx.JsonSerializerOptions)
.WithAIContextProviderMessageRemoval(),
ChatMessageStoreFactory = (ctx, ct) => new ValueTask<ChatMessageStore>(new InMemoryChatMessageStore(ctx.SerializedState, ctx.JsonSerializerOptions)
.WithAIContextProviderMessageRemoval()),
});
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(">> Asking about returns\n");
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", thread));
@@ -71,10 +71,10 @@ AIAgent agent = azureOpenAIClient
.CreateAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief." },
AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions))
});
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(">> Asking about SK threads\n");
Console.WriteLine(await agent.RunAsync("Hi! How do I create a thread in Semantic Kernel?", thread));
@@ -29,10 +29,10 @@ AIAgent agent = new AzureOpenAIClient(
.CreateAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
AIContextProviderFactory = ctx => new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
AIContextProviderFactory = (ctx, ct) => new ValueTask<AIContextProvider>(new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions))
});
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(">> Asking about returns\n");
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", thread));
@@ -43,7 +43,7 @@ AIAgent agent = await aiProjectClient
instructions: "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
tools: [fileSearchTool]);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(">> Asking about returns\n");
Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", thread));
@@ -17,12 +17,12 @@ AIAgent agent = new AzureOpenAIClient(
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent with a multi-turn conversation, where the context is preserved in the thread object.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
Console.WriteLine(await agent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", thread));
// Invoke the agent with a multi-turn conversation and streaming, where the context is preserved in the thread object.
thread = agent.GetNewThread();
thread = await agent.GetNewThreadAsync();
await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate.", thread))
{
Console.WriteLine(update);
@@ -30,7 +30,7 @@ AIAgent agent = new AzureOpenAIClient(
.CreateAIAgent(instructions: "You are a helpful assistant", tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]);
// Call the agent and check if there are any user input requests to handle.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
var response = await agent.RunAsync("What is the weather like in Amsterdam?", thread);
var userInputRequests = response.UserInputRequests.ToList();
@@ -19,7 +19,7 @@ AIAgent agent = new AzureOpenAIClient(
.CreateAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
// Run the agent with a new thread.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
@@ -35,7 +35,7 @@ await File.WriteAllTextAsync(tempFilePath, JsonSerializer.Serialize(serializedTh
JsonElement reloadedSerializedThread = JsonElement.Parse(await File.ReadAllTextAsync(tempFilePath));
// Deserialize the thread state after loading from storage.
AgentThread resumedThread = agent.DeserializeThread(reloadedSerializedThread);
AgentThread resumedThread = await agent.DeserializeThreadAsync(reloadedSerializedThread);
// Run the agent again with the resumed thread.
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));
@@ -31,17 +31,15 @@ AIAgent agent = new AzureOpenAIClient(
{
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
ChatMessageStoreFactory = ctx =>
{
ChatMessageStoreFactory = (ctx, ct) => new ValueTask<ChatMessageStore>(
// Create a new chat message store for this agent that stores the messages in a vector store.
// Each thread must get its own copy of the VectorChatMessageStore, since the store
// also contains the id that the thread is stored under.
return new VectorChatMessageStore(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions);
}
new VectorChatMessageStore(vectorStore, ctx.SerializedState, ctx.JsonSerializerOptions))
});
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
// Run the agent with the thread that stores conversation history in the vector store.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
@@ -58,7 +56,7 @@ Console.WriteLine(JsonSerializer.Serialize(serializedThread, new JsonSerializerO
// and loaded again later.
// Deserialize the thread state after loading from storage.
AgentThread resumedThread = agent.DeserializeThread(serializedThread);
AgentThread resumedThread = await agent.DeserializeThreadAsync(serializedThread);
// Run the agent with the thread that stores conversation history in the vector store a second time.
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));
@@ -49,7 +49,7 @@ internal sealed class SampleService(AIAgent agent, IHostApplicationLifetime appL
public async Task StartAsync(CancellationToken cancellationToken)
{
// Create a thread that will be used for the entirety of the service lifetime so that the user can ask follow up questions.
this._thread = agent.GetNewThread();
this._thread = await agent.GetNewThreadAsync(cancellationToken);
_ = this.RunAsync(appLifetime.ApplicationStopping);
}
@@ -22,7 +22,7 @@ ChatMessage message = new(ChatRole.User, [
new UriContent("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "image/jpeg")
]);
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await foreach (var update in agent.RunStreamingAsync(message, thread))
{
@@ -32,7 +32,7 @@ AIAgent agent = new AzureOpenAIClient(
// Enable background responses (only supported by {Azure}OpenAI Responses at this time).
AgentRunOptions options = new() { AllowBackgroundResponses = true };
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
// Start the initial run.
AgentRunResponse response = await agent.RunAsync("Write a very long novel about a team of astronauts exploring an uncharted galaxy.", thread, options);
@@ -44,10 +44,10 @@ while (response.ContinuationToken is not null)
await Task.Delay(TimeSpan.FromSeconds(10));
RestoreAgentState(agent, out thread, out ResponseContinuationToken? continuationToken);
var (restoredThread, continuationToken) = await RestoreAgentState(agent);
options.ContinuationToken = continuationToken;
response = await agent.RunAsync(thread, options);
response = await agent.RunAsync(restoredThread, options);
}
Console.WriteLine(response.Text);
@@ -58,13 +58,15 @@ void PersistAgentState(AgentThread thread, ResponseContinuationToken? continuati
stateStore["continuationToken"] = JsonSerializer.SerializeToElement(continuationToken, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
}
void RestoreAgentState(AIAgent agent, out AgentThread thread, out ResponseContinuationToken? continuationToken)
async Task<(AgentThread Thread, ResponseContinuationToken? ContinuationToken)> RestoreAgentState(AIAgent agent)
{
JsonElement serializedThread = stateStore["thread"] ?? throw new InvalidOperationException("No serialized thread found in state store.");
JsonElement? serializedToken = stateStore["continuationToken"];
thread = agent.DeserializeThread(serializedThread);
continuationToken = (ResponseContinuationToken?)serializedToken?.Deserialize(AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
AgentThread thread = await agent.DeserializeThreadAsync(serializedThread);
ResponseContinuationToken? continuationToken = (ResponseContinuationToken?)serializedToken?.Deserialize(AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
return (thread, continuationToken);
}
[Description("Researches relevant space facts and scientific information for writing a science fiction novel")]
@@ -45,7 +45,7 @@ var middlewareEnabledAgent = originalAgent
.Use(GuardrailMiddleware, null)
.Build();
var thread = middlewareEnabledAgent.GetNewThread();
var thread = await middlewareEnabledAgent.GetNewThreadAsync();
Console.WriteLine("\n\n=== Example 1: Wording Guardrail ===");
var guardRailedResponse = await middlewareEnabledAgent.RunAsync("Tell me something harmful.");
@@ -24,10 +24,10 @@ AIAgent agent = new AzureOpenAIClient(
{
ChatOptions = new() { Instructions = "You are good at telling jokes." },
Name = "Joker",
ChatMessageStoreFactory = ctx => new InMemoryChatMessageStore(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions)
ChatMessageStoreFactory = (ctx, ct) => new ValueTask<ChatMessageStore>(new InMemoryChatMessageStore(new MessageCountingChatReducer(2), ctx.SerializedState, ctx.JsonSerializerOptions))
});
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
@@ -19,7 +19,7 @@ AIAgent agent = new AzureOpenAIClient(
// Enable background responses (only supported by OpenAI Responses at this time).
AgentRunOptions options = new() { AllowBackgroundResponses = true };
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
// Start the initial run.
AgentRunResponse response = await agent.RunAsync("Write a very long novel about otters in space.", thread, options);
@@ -41,7 +41,7 @@ Console.WriteLine(response.Text);
// Reset options and thread for streaming.
options = new() { AllowBackgroundResponses = true };
thread = agent.GetNewThread();
thread = await agent.GetNewThreadAsync();
AgentRunResponseUpdate? lastReceivedUpdate = null;
// Start streaming.
@@ -39,7 +39,7 @@ Console.WriteLine();
try
{
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
await foreach (var response in agent.RunStreamingAsync(Task, thread))
{
@@ -26,12 +26,12 @@ AgentVersion agentVersion = aiProjectClient.Agents.CreateAgentVersion(agentName:
AIAgent jokerAgent = aiProjectClient.GetAIAgent(agentVersion);
// Invoke the agent with a multi-turn conversation, where the context is preserved in the thread object.
AgentThread thread = jokerAgent.GetNewThread();
AgentThread thread = await jokerAgent.GetNewThreadAsync();
Console.WriteLine(await jokerAgent.RunAsync("Tell me a joke about a pirate.", thread));
Console.WriteLine(await jokerAgent.RunAsync("Now add some emojis to the joke and tell it in the voice of a pirate's parrot.", thread));
// Invoke the agent with a multi-turn conversation and streaming, where the context is preserved in the thread object.
thread = jokerAgent.GetNewThread();
thread = await jokerAgent.GetNewThreadAsync();
await foreach (AgentRunResponseUpdate update in jokerAgent.RunStreamingAsync("Tell me a joke about a pirate.", thread))
{
Console.WriteLine(update);
@@ -37,11 +37,11 @@ var newAgent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, mod
var existingAgent = await aiProjectClient.GetAIAgentAsync(name: AssistantName, tools: [tool]);
// Non-streaming agent interaction with function tools.
AgentThread thread = existingAgent.GetNewThread();
AgentThread thread = await existingAgent.GetNewThreadAsync();
Console.WriteLine(await existingAgent.RunAsync("What is the weather like in Amsterdam?", thread));
// Streaming agent interaction with function tools.
thread = existingAgent.GetNewThread();
thread = await existingAgent.GetNewThreadAsync();
await foreach (AgentRunResponseUpdate update in existingAgent.RunStreamingAsync("What is the weather like in Amsterdam?", thread))
{
Console.WriteLine(update);
@@ -32,7 +32,7 @@ AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, mo
// Call the agent with approval-required function tools.
// The agent will request approval before invoking the function.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
AgentRunResponse response = await agent.RunAsync("What is the weather like in Amsterdam?", thread);
// Check if there are any user input requests (approvals needed).
@@ -19,7 +19,7 @@ AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential(
AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: JokerName, model: deploymentName, instructions: JokerInstructions);
// Start a new thread for the agent conversation.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
// Run the agent with a new thread.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
@@ -35,7 +35,7 @@ await File.WriteAllTextAsync(tempFilePath, JsonSerializer.Serialize(serializedTh
JsonElement reloadedSerializedThread = JsonElement.Parse(await File.ReadAllTextAsync(tempFilePath))!;
// Deserialize the thread state after loading from storage.
AgentThread resumedThread = agent.DeserializeThread(reloadedSerializedThread);
AgentThread resumedThread = await agent.DeserializeThreadAsync(reloadedSerializedThread);
// Run the agent again with the resumed thread.
Console.WriteLine(await agent.RunAsync("Now tell the same joke in the voice of a pirate, and add some emojis to the joke.", resumedThread));
@@ -38,11 +38,11 @@ AIAgent agent = aiProjectClient.CreateAIAgent(name: JokerName, model: deployment
.Build();
// Invoke the agent and output the text result.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
// Invoke the agent with streaming support.
thread = agent.GetNewThread();
thread = await agent.GetNewThreadAsync();
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync("Tell me a joke about a pirate.", thread))
{
Console.WriteLine(update);
@@ -42,7 +42,7 @@ internal sealed class SampleService(AIProjectClient client, AIAgent agent, IHost
public async Task StartAsync(CancellationToken cancellationToken)
{
// Create a thread that will be used for the entirety of the service lifetime so that the user can ask follow up questions.
this._thread = agent.GetNewThread();
this._thread = await agent.GetNewThreadAsync(cancellationToken);
_ = this.RunAsync(appLifetime.ApplicationStopping);
}
@@ -24,7 +24,7 @@ ChatMessage message = new(ChatRole.User, [
new DataContent(File.ReadAllBytes("assets/walkway.jpg"), "image/jpeg")
]);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(message, thread))
{
@@ -39,7 +39,7 @@ AIAgent agent = aiProjectClient.CreateAIAgent(
tools: [weatherAgent.AsAIFunction()]);
// Invoke the agent and output the text result.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", thread));
// Cleanup by agent name removes the agent versions created.
@@ -49,7 +49,7 @@ AIAgent middlewareEnabledAgent = originalAgent
.Use(GuardrailMiddleware, null)
.Build();
AgentThread thread = middlewareEnabledAgent.GetNewThread();
AgentThread thread = await middlewareEnabledAgent.GetNewThreadAsync();
Console.WriteLine("\n\n=== Example 1: Wording Guardrail ===");
AgentRunResponse guardRailedResponse = await middlewareEnabledAgent.RunAsync("Tell me something harmful.");
@@ -42,7 +42,7 @@ AIAgent agent = aiProjectClient.CreateAIAgent(
services: serviceProvider);
// Invoke the agent and output the text result.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("Tell me current time and weather in Seattle.", thread));
// Cleanup by agent name removes the agent version created.
@@ -83,7 +83,7 @@ internal sealed class Program
AllowBackgroundResponses = true,
};
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage message = new(ChatRole.User, [
new TextContent("I need you to help me search for 'OpenAI news'. Please type 'OpenAI news' and submit the search. Once you see search results, the task is complete."),
@@ -42,7 +42,7 @@ AIAgent agent = await persistentAgentsClient.CreateAIAgentAsync(
});
// You can then invoke the agent like any other AIAgent.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", thread));
// Cleanup for sample purposes.
@@ -75,7 +75,7 @@ AIAgent agentWithRequiredApproval = await persistentAgentsClient.CreateAIAgentAs
});
// You can then invoke the agent like any other AIAgent.
var threadWithRequiredApproval = agentWithRequiredApproval.GetNewThread();
var threadWithRequiredApproval = await agentWithRequiredApproval.GetNewThreadAsync();
var response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", threadWithRequiredApproval);
var userInputRequests = response.UserInputRequests.ToList();
@@ -37,7 +37,7 @@ AIAgent agent = new AzureOpenAIClient(
tools: [mcpTool]);
// You can then invoke the agent like any other AIAgent.
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
Console.WriteLine(await agent.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", thread));
// **** MCP Tool with Approval Required ****
@@ -64,7 +64,7 @@ AIAgent agentWithRequiredApproval = new AzureOpenAIClient(
tools: [mcpToolWithApproval]);
// You can then invoke the agent like any other AIAgent.
var threadWithRequiredApproval = agentWithRequiredApproval.GetNewThread();
var threadWithRequiredApproval = await agentWithRequiredApproval.GetNewThreadAsync();
var response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", threadWithRequiredApproval);
var userInputRequests = response.UserInputRequests.ToList();
@@ -109,7 +109,7 @@ internal sealed class SloganGeneratedEvent(SloganResult sloganResult) : Workflow
internal sealed class SloganWriterExecutor : Executor
{
private readonly AIAgent _agent;
private readonly AgentThread _thread;
private AgentThread? _thread;
/// <summary>
/// Initializes a new instance of the <see cref="SloganWriterExecutor"/> class.
@@ -128,7 +128,6 @@ internal sealed class SloganWriterExecutor : Executor
};
this._agent = new ChatClientAgent(chatClient, agentOptions);
this._thread = this._agent.GetNewThread();
}
protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) =>
@@ -137,6 +136,8 @@ internal sealed class SloganWriterExecutor : Executor
public async ValueTask<SloganResult> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._thread ??= await this._agent.GetNewThreadAsync(cancellationToken);
var result = await this._agent.RunAsync(message, this._thread, cancellationToken: cancellationToken);
var sloganResult = JsonSerializer.Deserialize<SloganResult>(result.Text) ?? throw new InvalidOperationException("Failed to deserialize slogan result.");
@@ -179,7 +180,7 @@ internal sealed class FeedbackEvent(FeedbackResult feedbackResult) : WorkflowEve
internal sealed class FeedbackExecutor : Executor<SloganResult>
{
private readonly AIAgent _agent;
private readonly AgentThread _thread;
private AgentThread? _thread;
public int MinimumRating { get; init; } = 8;
@@ -204,11 +205,12 @@ internal sealed class FeedbackExecutor : Executor<SloganResult>
};
this._agent = new ChatClientAgent(chatClient, agentOptions);
this._thread = this._agent.GetNewThread();
}
public override async ValueTask HandleAsync(SloganResult message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
this._thread ??= await this._agent.GetNewThreadAsync(cancellationToken);
var sloganMessage = $"""
Here is a slogan for the task '{message.Task}':
Slogan: {message.Slogan}
@@ -37,7 +37,7 @@ public static class Program
// Create the workflow and turn it into an agent
var workflow = WorkflowFactory.BuildWorkflow(chatClient);
var agent = workflow.AsAgent("workflow-agent", "Workflow Agent");
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
// Start an interactive loop to interact with the workflow as if it were an agent
while (true)
@@ -47,7 +47,7 @@ internal sealed class Program
AIAgent agent = aiProjectClient.GetAIAgent(agentVersion);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
ProjectConversation conversation =
await aiProjectClient
@@ -90,7 +90,7 @@ public static class Program
{
EnableSensitiveData = true // enable sensitive data at the agent level such as prompts and responses
};
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
// Start an interactive loop to interact with the workflow as if it were an agent
while (true)
@@ -43,8 +43,8 @@ internal sealed class AFAgentApplication : AgentApplication
// Deserialize the conversation history into an AgentThread, or create a new one if none exists.
AgentThread agentThread = threadElementStart.ValueKind is not JsonValueKind.Undefined and not JsonValueKind.Null
? this._agent.DeserializeThread(threadElementStart, JsonUtilities.DefaultOptions)
: this._agent.GetNewThread();
? await this._agent.DeserializeThreadAsync(threadElementStart, JsonUtilities.DefaultOptions, cancellationToken)
: await this._agent.GetNewThreadAsync(cancellationToken);
ChatMessage chatMessage = HandleUserInput(turnContext);
+11 -11
View File
@@ -52,27 +52,27 @@ public sealed class A2AAgent : AIAgent
}
/// <inheritdoc/>
public sealed override AgentThread GetNewThread()
=> new A2AAgentThread();
public sealed override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
=> new(new A2AAgentThread());
/// <summary>
/// Get a new <see cref="AgentThread"/> instance using an existing context id, to continue that conversation.
/// </summary>
/// <param name="contextId">The context id to continue.</param>
/// <returns>A new <see cref="AgentThread"/> instance.</returns>
public AgentThread GetNewThread(string contextId)
=> new A2AAgentThread() { ContextId = contextId };
/// <returns>A value task representing the asynchronous operation. The task result contains a new <see cref="AgentThread"/> instance.</returns>
public ValueTask<AgentThread> GetNewThreadAsync(string contextId)
=> new(new A2AAgentThread() { ContextId = contextId });
/// <inheritdoc/>
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
=> new A2AAgentThread(serializedThread, jsonSerializerOptions);
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new A2AAgentThread(serializedThread, jsonSerializerOptions));
/// <inheritdoc/>
protected override async Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(messages);
A2AAgentThread typedThread = this.GetA2AThread(thread, options);
A2AAgentThread typedThread = await this.GetA2AThreadAsync(thread, options, cancellationToken).ConfigureAwait(false);
this._logger.LogA2AAgentInvokingAgent(nameof(RunAsync), this.Id, this.Name);
@@ -139,7 +139,7 @@ public sealed class A2AAgent : AIAgent
{
_ = Throw.IfNull(messages);
A2AAgentThread typedThread = this.GetA2AThread(thread, options);
A2AAgentThread typedThread = await this.GetA2AThreadAsync(thread, options, cancellationToken).ConfigureAwait(false);
this._logger.LogA2AAgentInvokingAgent(nameof(RunStreamingAsync), this.Id, this.Name);
@@ -211,7 +211,7 @@ public sealed class A2AAgent : AIAgent
/// <inheritdoc/>
public override string? Description => this._description;
private A2AAgentThread GetA2AThread(AgentThread? thread, AgentRunOptions? options)
private async ValueTask<A2AAgentThread> GetA2AThreadAsync(AgentThread? thread, AgentRunOptions? options, CancellationToken cancellationToken)
{
// Aligning with other agent implementations that support background responses, where
// a thread is required for background responses to prevent inconsistent experience
@@ -221,7 +221,7 @@ public sealed class A2AAgent : AIAgent
throw new InvalidOperationException("A thread must be provided when AllowBackgroundResponses is enabled.");
}
thread ??= this.GetNewThread();
thread ??= await this.GetNewThreadAsync(cancellationToken).ConfigureAwait(false);
if (thread is not A2AAgentThread typedThread)
{
@@ -105,7 +105,8 @@ public abstract class AIAgent
/// <summary>
/// Creates a new conversation thread that is compatible with this agent.
/// </summary>
/// <returns>A new <see cref="AgentThread"/> instance ready for use with this agent.</returns>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains a new <see cref="AgentThread"/> instance ready for use with this agent.</returns>
/// <remarks>
/// <para>
/// This method creates a fresh conversation thread that can be used to maintain state
@@ -118,14 +119,15 @@ public abstract class AIAgent
/// may be deferred until first use to optimize performance.
/// </para>
/// </remarks>
public abstract AgentThread GetNewThread();
public abstract ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Deserializes an agent thread from its JSON serialized representation.
/// </summary>
/// <param name="serializedThread">A <see cref="JsonElement"/> containing the serialized thread state.</param>
/// <param name="jsonSerializerOptions">Optional settings to customize the deserialization process.</param>
/// <returns>A restored <see cref="AgentThread"/> instance with the state from <paramref name="serializedThread"/>.</returns>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains a restored <see cref="AgentThread"/> instance with the state from <paramref name="serializedThread"/>.</returns>
/// <exception cref="ArgumentException">The <paramref name="serializedThread"/> is not in the expected format.</exception>
/// <exception cref="JsonException">The serialized data is invalid or cannot be deserialized.</exception>
/// <remarks>
@@ -133,7 +135,7 @@ public abstract class AIAgent
/// allowing conversations to resume across application restarts or be migrated between
/// different agent instances.
/// </remarks>
public abstract AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null);
public abstract ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default);
/// <summary>
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the thread.
@@ -26,8 +26,8 @@ namespace Microsoft.Agents.AI;
/// <item><description>Chat history reduction, e.g. where messages needs to be summarized or truncated to reduce the size.</description></item>
/// </list>
/// An <see cref="AgentThread"/> is always constructed by an <see cref="AIAgent"/> so that the <see cref="AIAgent"/>
/// can attach any necessary behaviors to the <see cref="AgentThread"/>. See the <see cref="AIAgent.GetNewThread()"/>
/// and <see cref="AIAgent.DeserializeThread(JsonElement, JsonSerializerOptions?)"/> methods for more information.
/// can attach any necessary behaviors to the <see cref="AgentThread"/>. See the <see cref="AIAgent.GetNewThreadAsync(System.Threading.CancellationToken)"/>
/// and <see cref="AIAgent.DeserializeThreadAsync(JsonElement, JsonSerializerOptions?, System.Threading.CancellationToken)"/> methods for more information.
/// </para>
/// <para>
/// Because of these behaviors, an <see cref="AgentThread"/> may not be reusable across different agents, since each agent
@@ -37,13 +37,13 @@ namespace Microsoft.Agents.AI;
/// To support conversations that may need to survive application restarts or separate service requests, an <see cref="AgentThread"/> can be serialized
/// and deserialized, so that it can be saved in a persistent store.
/// The <see cref="AgentThread"/> provides the <see cref="Serialize(JsonSerializerOptions?)"/> method to serialize the thread to a
/// <see cref="JsonElement"/> and the <see cref="AIAgent.DeserializeThread(JsonElement, JsonSerializerOptions?)"/> method
/// <see cref="JsonElement"/> and the <see cref="AIAgent.DeserializeThreadAsync(JsonElement, JsonSerializerOptions?, System.Threading.CancellationToken)"/> method
/// can be used to deserialize the thread.
/// </para>
/// </remarks>
/// <seealso cref="AIAgent"/>
/// <seealso cref="AIAgent.GetNewThread()"/>
/// <seealso cref="AIAgent.DeserializeThread(JsonElement, JsonSerializerOptions?)"/>
/// <seealso cref="AIAgent.GetNewThreadAsync(System.Threading.CancellationToken)"/>
/// <seealso cref="AIAgent.DeserializeThreadAsync(JsonElement, JsonSerializerOptions?, System.Threading.CancellationToken)"/>
public abstract class AgentThread
{
/// <summary>
@@ -74,11 +74,11 @@ public abstract class DelegatingAIAgent : AIAgent
}
/// <inheritdoc />
public override AgentThread GetNewThread() => this.InnerAgent.GetNewThread();
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default) => this.InnerAgent.GetNewThreadAsync(cancellationToken);
/// <inheritdoc />
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
=> this.InnerAgent.DeserializeThread(serializedThread, jsonSerializerOptions);
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> this.InnerAgent.DeserializeThreadAsync(serializedThread, jsonSerializerOptions, cancellationToken);
/// <inheritdoc />
protected override Task<AgentRunResponse> RunCoreAsync(
@@ -42,20 +42,20 @@ public class CopilotStudioAgent : AIAgent
}
/// <inheritdoc/>
public sealed override AgentThread GetNewThread()
=> new CopilotStudioAgentThread();
public sealed override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
=> new(new CopilotStudioAgentThread());
/// <summary>
/// Get a new <see cref="AgentThread"/> instance using an existing conversation id, to continue that conversation.
/// </summary>
/// <param name="conversationId">The conversation id to continue.</param>
/// <returns>A new <see cref="AgentThread"/> instance.</returns>
public AgentThread GetNewThread(string conversationId)
=> new CopilotStudioAgentThread() { ConversationId = conversationId };
public ValueTask<AgentThread> GetNewThreadAsync(string conversationId)
=> new(new CopilotStudioAgentThread() { ConversationId = conversationId });
/// <inheritdoc/>
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
=> new CopilotStudioAgentThread(serializedThread, jsonSerializerOptions);
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new CopilotStudioAgentThread(serializedThread, jsonSerializerOptions));
/// <inheritdoc/>
protected override async Task<AgentRunResponse> RunCoreAsync(
@@ -68,7 +68,7 @@ public class CopilotStudioAgent : AIAgent
// Ensure that we have a valid thread to work with.
// If the thread ID is null, we need to start a new conversation and set the thread ID accordingly.
thread ??= this.GetNewThread();
thread ??= await this.GetNewThreadAsync(cancellationToken).ConfigureAwait(false);
if (thread is not CopilotStudioAgentThread typedThread)
{
throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used.");
@@ -106,7 +106,8 @@ public class CopilotStudioAgent : AIAgent
// Ensure that we have a valid thread to work with.
// If the thread ID is null, we need to start a new conversation and set the thread ID accordingly.
thread ??= this.GetNewThread();
thread ??= await this.GetNewThreadAsync(cancellationToken).ConfigureAwait(false);
if (thread is not CopilotStudioAgentThread typedThread)
{
throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used.");
@@ -2,6 +2,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Azure.Identity;
using Microsoft.Azure.Cosmos;
@@ -35,7 +36,7 @@ public static class CosmosDBChatExtensions
throw new ArgumentNullException(nameof(options));
}
options.ChatMessageStoreFactory = context => new CosmosChatMessageStore(connectionString, databaseId, containerId);
options.ChatMessageStoreFactory = (context, ct) => new ValueTask<ChatMessageStore>(new CosmosChatMessageStore(connectionString, databaseId, containerId));
return options;
}
@@ -62,7 +63,7 @@ public static class CosmosDBChatExtensions
throw new ArgumentNullException(nameof(options));
}
options.ChatMessageStoreFactory = context => new CosmosChatMessageStore(accountEndpoint, new DefaultAzureCredential(), databaseId, containerId);
options.ChatMessageStoreFactory = (context, ct) => new ValueTask<ChatMessageStore>(new CosmosChatMessageStore(accountEndpoint, new DefaultAzureCredential(), databaseId, containerId));
return options;
}
@@ -89,7 +90,7 @@ public static class CosmosDBChatExtensions
throw new ArgumentNullException(nameof(options));
}
options.ChatMessageStoreFactory = context => new CosmosChatMessageStore(cosmosClient, databaseId, containerId);
options.ChatMessageStoreFactory = (context, ct) => new ValueTask<ChatMessageStore>(new CosmosChatMessageStore(cosmosClient, databaseId, containerId));
return options;
}
}
@@ -67,7 +67,7 @@ internal class AgentEntity(IServiceProvider services, CancellationToken cancella
// Start the agent response stream
IAsyncEnumerable<AgentRunResponseUpdate> responseStream = agentWrapper.RunStreamingAsync(
this.State.Data.ConversationHistory.SelectMany(e => e.Messages).Select(m => m.ToChatMessage()),
agentWrapper.GetNewThread(),
await agentWrapper.GetNewThreadAsync(cancellationToken).ConfigureAwait(false),
options: null,
this._cancellationToken);
@@ -32,11 +32,12 @@ public sealed class DurableAIAgent : AIAgent
/// <summary>
/// Creates a new agent thread for this agent using a random session ID.
/// </summary>
/// <returns>A new agent thread.</returns>
public override AgentThread GetNewThread()
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains a new agent thread.</returns>
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
{
AgentSessionId sessionId = this._context.NewAgentSessionId(this._agentName);
return new DurableAgentThread(sessionId);
return ValueTask.FromResult<AgentThread>(new DurableAgentThread(sessionId));
}
/// <summary>
@@ -44,12 +45,13 @@ public sealed class DurableAIAgent : AIAgent
/// </summary>
/// <param name="serializedThread">The serialized thread data.</param>
/// <param name="jsonSerializerOptions">Optional JSON serializer options.</param>
/// <returns>The deserialized agent thread.</returns>
public override AgentThread DeserializeThread(
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A value task that represents the asynchronous operation. The task result contains the deserialized agent thread.</returns>
public override ValueTask<AgentThread> DeserializeThreadAsync(
JsonElement serializedThread,
JsonSerializerOptions? jsonSerializerOptions = null)
JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
return DurableAgentThread.Deserialize(serializedThread, jsonSerializerOptions);
return ValueTask.FromResult<AgentThread>(DurableAgentThread.Deserialize(serializedThread, jsonSerializerOptions));
}
/// <summary>
@@ -74,12 +76,12 @@ public sealed class DurableAIAgent : AIAgent
throw new NotSupportedException("Cancellation is not supported for durable agents.");
}
thread ??= this.GetNewThread();
thread ??= await this.GetNewThreadAsync(cancellationToken).ConfigureAwait(false);
if (thread is not DurableAgentThread durableThread)
{
throw new ArgumentException(
"The provided thread is not valid for a durable agent. " +
"Create a new thread using GetNewThread or provide a thread previously created by this agent.",
"Create a new thread using GetNewThreadAsync or provide a thread previously created by this agent.",
paramName: nameof(thread));
}
@@ -11,16 +11,16 @@ internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient)
public override string? Name { get; } = name;
public override AgentThread DeserializeThread(
public override ValueTask<AgentThread> DeserializeThreadAsync(
JsonElement serializedThread,
JsonSerializerOptions? jsonSerializerOptions = null)
JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
return DurableAgentThread.Deserialize(serializedThread, jsonSerializerOptions);
return ValueTask.FromResult<AgentThread>(DurableAgentThread.Deserialize(serializedThread, jsonSerializerOptions));
}
public override AgentThread GetNewThread()
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
{
return new DurableAgentThread(AgentSessionId.WithRandomKey(this.Name!));
return ValueTask.FromResult<AgentThread>(new DurableAgentThread(AgentSessionId.WithRandomKey(this.Name!)));
}
protected override async Task<AgentRunResponse> RunCoreAsync(
@@ -29,7 +29,7 @@ internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient)
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
thread ??= this.GetNewThread();
thread ??= await this.GetNewThreadAsync(cancellationToken).ConfigureAwait(false);
if (thread is not DurableAgentThread durableThread)
{
throw new ArgumentException(
@@ -74,7 +74,7 @@ public static async Task<string> SpamDetectionOrchestration(
// Get the spam detection agent
DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent");
AgentThread spamThread = spamDetectionAgent.GetNewThread();
AgentThread spamThread = await spamDetectionAgent.GetNewThreadAsync();
// Step 1: Check if the email is spam
AgentRunResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
@@ -97,7 +97,7 @@ public static async Task<string> SpamDetectionOrchestration(
{
// Generate and send response for legitimate email
DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent");
AgentThread emailThread = emailAssistantAgent.GetNewThread();
AgentThread emailThread = await emailAssistantAgent.GetNewThreadAsync();
AgentRunResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
message:
@@ -38,15 +38,15 @@ public sealed class InMemoryAgentThreadStore : AgentThreadStore
}
/// <inheritdoc/>
public override ValueTask<AgentThread> GetThreadAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default)
public override async 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)),
null => await agent.GetNewThreadAsync(cancellationToken).ConfigureAwait(false),
_ => await agent.DeserializeThreadAsync(threadContent.Value, cancellationToken: cancellationToken).ConfigureAwait(false),
};
}
@@ -20,6 +20,6 @@ public sealed class NoopAgentThreadStore : AgentThreadStore
/// <inheritdoc/>
public override ValueTask<AgentThread> GetThreadAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default)
{
return new ValueTask<AgentThread>(agent.GetNewThread());
return agent.GetNewThreadAsync(cancellationToken);
}
}
@@ -30,15 +30,15 @@ internal class PurviewAgent : AIAgent, IDisposable
}
/// <inheritdoc/>
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
return this._innerAgent.DeserializeThread(serializedThread, jsonSerializerOptions);
return this._innerAgent.DeserializeThreadAsync(serializedThread, jsonSerializerOptions, cancellationToken);
}
/// <inheritdoc/>
public override AgentThread GetNewThread()
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
{
return this._innerAgent.GetNewThread();
return this._innerAgent.GetNewThreadAsync(cancellationToken);
}
/// <inheritdoc/>
@@ -20,8 +20,8 @@ internal sealed class AIAgentHostExecutor : ChatProtocolExecutor
this._emitEvents = emitEvents;
}
private AgentThread EnsureThread(IWorkflowContext context) =>
this._thread ??= this._agent.GetNewThread();
private async Task<AgentThread> EnsureThreadAsync(IWorkflowContext context, CancellationToken cancellationToken) =>
this._thread ??= await this._agent.GetNewThreadAsync(cancellationToken).ConfigureAwait(false);
private const string ThreadStateKey = nameof(_thread);
protected internal override async ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
@@ -43,7 +43,7 @@ internal sealed class AIAgentHostExecutor : ChatProtocolExecutor
JsonElement? threadValue = await context.ReadStateAsync<JsonElement?>(ThreadStateKey, cancellationToken: cancellationToken).ConfigureAwait(false);
if (threadValue.HasValue)
{
this._thread = this._agent.DeserializeThread(threadValue.Value);
this._thread = await this._agent.DeserializeThreadAsync(threadValue.Value, cancellationToken: cancellationToken).ConfigureAwait(false);
}
await base.OnCheckpointRestoredAsync(context, cancellationToken).ConfigureAwait(false);
@@ -54,7 +54,10 @@ internal sealed class AIAgentHostExecutor : ChatProtocolExecutor
if (emitEvents ?? this._emitEvents)
{
// Run the agent in streaming mode only when agent run update events are to be emitted.
IAsyncEnumerable<AgentRunResponseUpdate> agentStream = this._agent.RunStreamingAsync(messages, this.EnsureThread(context), cancellationToken: cancellationToken);
IAsyncEnumerable<AgentRunResponseUpdate> agentStream = this._agent.RunStreamingAsync(
messages,
await this.EnsureThreadAsync(context, cancellationToken).ConfigureAwait(false),
cancellationToken: cancellationToken);
List<AgentRunResponseUpdate> updates = [];
@@ -74,7 +77,10 @@ internal sealed class AIAgentHostExecutor : ChatProtocolExecutor
else
{
// Otherwise, run the agent in non-streaming mode.
AgentRunResponse response = await this._agent.RunAsync(messages, this.EnsureThread(context), cancellationToken: cancellationToken).ConfigureAwait(false);
AgentRunResponse response = await this._agent.RunAsync(
messages,
await this.EnsureThreadAsync(context, cancellationToken).ConfigureAwait(false),
cancellationToken: cancellationToken).ConfigureAwait(false);
await context.SendMessageAsync(response.Messages, cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
@@ -63,14 +63,15 @@ internal sealed class WorkflowHostAgent : AIAgent
protocol.ThrowIfNotChatProtocol();
}
public override AgentThread GetNewThread() => new WorkflowThread(this._workflow, this.GenerateNewId(), this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails);
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
=> new(new WorkflowThread(this._workflow, this.GenerateNewId(), this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails));
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
=> new WorkflowThread(this._workflow, serializedThread, this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails, jsonSerializerOptions);
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> new(new WorkflowThread(this._workflow, serializedThread, this._executionEnvironment, this._checkpointManager, this._includeExceptionDetails, jsonSerializerOptions));
private ValueTask<WorkflowThread> UpdateThreadAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, CancellationToken cancellationToken = default)
private async ValueTask<WorkflowThread> UpdateThreadAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, CancellationToken cancellationToken = default)
{
thread ??= this.GetNewThread();
thread ??= await this.GetNewThreadAsync(cancellationToken).ConfigureAwait(false);
if (thread is not WorkflowThread workflowThread)
{
@@ -80,7 +81,7 @@ internal sealed class WorkflowHostAgent : AIAgent
// For workflow threads, messages are added directly via the internal AddMessages method
// The MessageStore methods are used for agent invocation scenarios
workflowThread.MessageStore.AddMessages(messages);
return new ValueTask<WorkflowThread>(workflowThread);
return workflowThread;
}
protected override async
@@ -283,7 +283,7 @@ public sealed partial class ChatClientAgent : AIAgent
// We can derive the type of supported thread from whether we have a conversation id,
// so let's update it and set the conversation id for the service thread case.
this.UpdateThreadWithTypeAndConversationId(safeThread, chatResponse.ConversationId);
await this.UpdateThreadWithTypeAndConversationIdAsync(safeThread, chatResponse.ConversationId, cancellationToken).ConfigureAwait(false);
// To avoid inconsistent state we only notify the thread of the input messages if no error occurs after the initial request.
await NotifyMessageStoreOfNewMessagesAsync(safeThread, GetInputMessages(inputMessages, continuationToken), chatMessageStoreMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
@@ -302,19 +302,30 @@ public sealed partial class ChatClientAgent : AIAgent
: this.ChatClient.GetService(serviceType, serviceKey));
/// <inheritdoc/>
public override AgentThread GetNewThread()
=> new ChatClientAgentThread
public override async ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
{
ChatMessageStore? messageStore = this._agentOptions?.ChatMessageStoreFactory is not null
? await this._agentOptions.ChatMessageStoreFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false)
: null;
AIContextProvider? contextProvider = this._agentOptions?.AIContextProviderFactory is not null
? await this._agentOptions.AIContextProviderFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false)
: null;
return new ChatClientAgentThread
{
MessageStore = this._agentOptions?.ChatMessageStoreFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }),
AIContextProvider = this._agentOptions?.AIContextProviderFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null })
MessageStore = messageStore,
AIContextProvider = contextProvider
};
}
/// <summary>
/// Creates a new agent thread instance using an existing conversation identifier to continue that conversation.
/// </summary>
/// <param name="conversationId">The identifier of an existing conversation to continue.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>
/// A new <see cref="AgentThread"/> instance configured to work with the specified conversation.
/// A value task representing the asynchronous operation. The task result contains a new <see cref="AgentThread"/> instance configured to work with the specified conversation.
/// </returns>
/// <remarks>
/// <para>
@@ -326,19 +337,26 @@ public sealed partial class ChatClientAgent : AIAgent
/// instances that support server-side conversation storage through their underlying <see cref="IChatClient"/>.
/// </para>
/// </remarks>
public AgentThread GetNewThread(string conversationId)
=> new ChatClientAgentThread()
public async ValueTask<AgentThread> GetNewThreadAsync(string conversationId, CancellationToken cancellationToken = default)
{
AIContextProvider? contextProvider = this._agentOptions?.AIContextProviderFactory is not null
? await this._agentOptions.AIContextProviderFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false)
: null;
return new ChatClientAgentThread()
{
ConversationId = conversationId,
AIContextProvider = this._agentOptions?.AIContextProviderFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null })
AIContextProvider = contextProvider
};
}
/// <summary>
/// Creates a new agent thread instance using an existing <see cref="ChatMessageStore"/> to continue a conversation.
/// </summary>
/// <param name="chatMessageStore">The <see cref="ChatMessageStore"/> instance to use for managing the conversation's message history.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>
/// A new <see cref="AgentThread"/> instance configured to work with the provided <paramref name="chatMessageStore"/>.
/// A value task representing the asynchronous operation. The task result contains a new <see cref="AgentThread"/> instance configured to work with the provided <paramref name="chatMessageStore"/>.
/// </returns>
/// <remarks>
/// <para>
@@ -347,36 +365,43 @@ public sealed partial class ChatClientAgent : AIAgent
/// with a <see cref="ChatMessageStore"/> may not be compatible with these services.
/// </para>
/// <para>
/// Where a service requires server-side conversation storage, use <see cref="GetNewThread(string)"/>.
/// Where a service requires server-side conversation storage, use <see cref="GetNewThreadAsync(string, CancellationToken)"/>.
/// </para>
/// <para>
/// If the agent detects, during the first run, that the underlying AI service requires server-side conversation storage,
/// the thread will throw an exception to indicate that it cannot continue using the provided <see cref="ChatMessageStore"/>.
/// </para>
/// </remarks>
public AgentThread GetNewThread(ChatMessageStore chatMessageStore)
=> new ChatClientAgentThread()
public async ValueTask<AgentThread> GetNewThreadAsync(ChatMessageStore chatMessageStore, CancellationToken cancellationToken = default)
{
AIContextProvider? contextProvider = this._agentOptions?.AIContextProviderFactory is not null
? await this._agentOptions.AIContextProviderFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false)
: null;
return new ChatClientAgentThread()
{
MessageStore = Throw.IfNull(chatMessageStore),
AIContextProvider = this._agentOptions?.AIContextProviderFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null })
AIContextProvider = contextProvider
};
}
/// <inheritdoc/>
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
public override async ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
Func<JsonElement, JsonSerializerOptions?, ChatMessageStore>? chatMessageStoreFactory = this._agentOptions?.ChatMessageStoreFactory is null ?
Func<JsonElement, JsonSerializerOptions?, CancellationToken, ValueTask<ChatMessageStore>>? chatMessageStoreFactory = this._agentOptions?.ChatMessageStoreFactory is null ?
null :
(jse, jso) => this._agentOptions.ChatMessageStoreFactory.Invoke(new() { SerializedState = jse, JsonSerializerOptions = jso });
(jse, jso, ct) => this._agentOptions.ChatMessageStoreFactory.Invoke(new() { SerializedState = jse, JsonSerializerOptions = jso }, ct);
Func<JsonElement, JsonSerializerOptions?, AIContextProvider>? aiContextProviderFactory = this._agentOptions?.AIContextProviderFactory is null ?
Func<JsonElement, JsonSerializerOptions?, CancellationToken, ValueTask<AIContextProvider>>? aiContextProviderFactory = this._agentOptions?.AIContextProviderFactory is null ?
null :
(jse, jso) => this._agentOptions.AIContextProviderFactory.Invoke(new() { SerializedState = jse, JsonSerializerOptions = jso });
(jse, jso, ct) => this._agentOptions.AIContextProviderFactory.Invoke(new() { SerializedState = jse, JsonSerializerOptions = jso }, ct);
return new ChatClientAgentThread(
return await ChatClientAgentThread.DeserializeAsync(
serializedThread,
jsonSerializerOptions,
chatMessageStoreFactory,
aiContextProviderFactory);
aiContextProviderFactory,
cancellationToken).ConfigureAwait(false);
}
#region Private
@@ -426,7 +451,7 @@ public sealed partial class ChatClientAgent : AIAgent
// We can derive the type of supported thread from whether we have a conversation id,
// so let's update it and set the conversation id for the service thread case.
this.UpdateThreadWithTypeAndConversationId(safeThread, chatResponse.ConversationId);
await this.UpdateThreadWithTypeAndConversationIdAsync(safeThread, chatResponse.ConversationId, cancellationToken).ConfigureAwait(false);
// Ensure that the author name is set for each message in the response.
foreach (ChatMessage chatResponseMessage in chatResponse.Messages)
@@ -653,7 +678,7 @@ public sealed partial class ChatClientAgent : AIAgent
throw new InvalidOperationException("A thread must be provided when continuing a background response with a continuation token.");
}
thread ??= this.GetNewThread();
thread ??= await this.GetNewThreadAsync(cancellationToken).ConfigureAwait(false);
if (thread is not ChatClientAgentThread typedThread)
{
throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used.");
@@ -735,13 +760,13 @@ public sealed partial class ChatClientAgent : AIAgent
return (typedThread, chatOptions, inputMessagesForChatClient, aiContextProviderMessages, chatMessageStoreMessages, continuationToken);
}
private void UpdateThreadWithTypeAndConversationId(ChatClientAgentThread thread, string? responseConversationId)
private async Task UpdateThreadWithTypeAndConversationIdAsync(ChatClientAgentThread thread, string? responseConversationId, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(responseConversationId) && !string.IsNullOrWhiteSpace(thread.ConversationId))
{
// We were passed a thread that is service managed, but we got no conversation id back from the chat client,
// meaning the service doesn't support service managed threads, so the thread cannot be used with this service.
throw new InvalidOperationException("Service did not return a valid conversation id when using a service managed thread.");
// We were passed an AgentThread that has an id for service managed chat history, but we got no conversation id back from the chat client,
// meaning the service doesn't support service managed chat history, so the thread cannot be used with this service.
throw new InvalidOperationException("Service did not return a valid conversation id when using an AgentThread with service managed chat history.");
}
if (!string.IsNullOrWhiteSpace(responseConversationId))
@@ -752,10 +777,12 @@ public sealed partial class ChatClientAgent : AIAgent
}
else
{
// If the service doesn't use service side thread storage (i.e. we got no id back from invocation), and
// If the service doesn't use service side chat history storage (i.e. we got no id back from invocation), and
// the thread has no MessageStore yet, we should update the thread with the custom MessageStore or
// default InMemoryMessageStore so that it has somewhere to store the chat history.
thread.MessageStore ??= this._agentOptions?.ChatMessageStoreFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }) ?? new InMemoryChatMessageStore();
thread.MessageStore ??= this._agentOptions?.ChatMessageStoreFactory is not null
? await this._agentOptions.ChatMessageStoreFactory.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }, cancellationToken).ConfigureAwait(false)
: new InMemoryChatMessageStore();
}
}
@@ -2,6 +2,8 @@
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
@@ -40,14 +42,14 @@ public sealed class ChatClientAgentOptions
/// Gets or sets a factory function to create an instance of <see cref="ChatMessageStore"/>
/// which will be used to store chat messages for this agent.
/// </summary>
public Func<ChatMessageStoreFactoryContext, ChatMessageStore>? ChatMessageStoreFactory { get; set; }
public Func<ChatMessageStoreFactoryContext, CancellationToken, ValueTask<ChatMessageStore>>? ChatMessageStoreFactory { get; set; }
/// <summary>
/// Gets or sets a factory function to create an instance of <see cref="AIContextProvider"/>
/// which will be used to create a context provider for each new thread, and can then
/// provide additional context for each agent run.
/// </summary>
public Func<AIContextProviderFactoryContext, AIContextProvider>? AIContextProviderFactory { get; set; }
public Func<AIContextProviderFactoryContext, CancellationToken, ValueTask<AIContextProvider>>? AIContextProviderFactory { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to use the provided <see cref="IChatClient"/> instance as is,
@@ -3,6 +3,8 @@
using System;
using System.Diagnostics;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
@@ -22,48 +24,6 @@ public sealed class ChatClientAgentThread : AgentThread
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChatClientAgentThread"/> class from previously serialized state.
/// </summary>
/// <param name="serializedThreadState">A <see cref="JsonElement"/> representing the serialized state of the thread.</param>
/// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param>
/// <param name="chatMessageStoreFactory">
/// An optional factory function to create a custom <see cref="ChatMessageStore"/> from its serialized state.
/// If not provided, the default in-memory message store will be used.
/// </param>
/// <param name="aiContextProviderFactory">
/// An optional factory function to create a custom <see cref="AIContextProvider"/> from its serialized state.
/// If not provided, no context provider will be configured.
/// </param>
internal ChatClientAgentThread(
JsonElement serializedThreadState,
JsonSerializerOptions? jsonSerializerOptions = null,
Func<JsonElement, JsonSerializerOptions?, ChatMessageStore>? chatMessageStoreFactory = null,
Func<JsonElement, JsonSerializerOptions?, AIContextProvider>? aiContextProviderFactory = null)
{
if (serializedThreadState.ValueKind != JsonValueKind.Object)
{
throw new ArgumentException("The serialized thread state must be a JSON object.", nameof(serializedThreadState));
}
var state = serializedThreadState.Deserialize(
AgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ThreadState))) as ThreadState;
this.AIContextProvider = aiContextProviderFactory?.Invoke(state?.AIContextProviderState ?? default, jsonSerializerOptions);
if (state?.ConversationId is string threadId)
{
this.ConversationId = threadId;
// Since we have an ID, we should not have a chat message store and we can return here.
return;
}
this._messageStore =
chatMessageStoreFactory?.Invoke(state?.StoreState ?? default, jsonSerializerOptions) ??
new InMemoryChatMessageStore(state?.StoreState ?? default, jsonSerializerOptions); // default to an in-memory store
}
/// <summary>
/// Gets or sets the ID of the underlying service thread to support cases where the chat history is stored by the agent service.
/// </summary>
@@ -152,6 +112,58 @@ public sealed class ChatClientAgentThread : AgentThread
/// </summary>
public AIContextProvider? AIContextProvider { get; internal set; }
/// <summary>
/// Creates a new instance of the <see cref="ChatClientAgentThread"/> class from previously serialized state.
/// </summary>
/// <param name="serializedThreadState">A <see cref="JsonElement"/> representing the serialized state of the thread.</param>
/// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param>
/// <param name="chatMessageStoreFactory">
/// An optional factory function to create a custom <see cref="ChatMessageStore"/> from its serialized state.
/// If not provided, the default in-memory message store will be used.
/// </param>
/// <param name="aiContextProviderFactory">
/// An optional factory function to create a custom <see cref="AIContextProvider"/> from its serialized state.
/// If not provided, no context provider will be configured.
/// </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation. The task result contains the deserialized <see cref="ChatClientAgentThread"/>.</returns>
internal static async Task<ChatClientAgentThread> DeserializeAsync(
JsonElement serializedThreadState,
JsonSerializerOptions? jsonSerializerOptions = null,
Func<JsonElement, JsonSerializerOptions?, CancellationToken, ValueTask<ChatMessageStore>>? chatMessageStoreFactory = null,
Func<JsonElement, JsonSerializerOptions?, CancellationToken, ValueTask<AIContextProvider>>? aiContextProviderFactory = null,
CancellationToken cancellationToken = default)
{
if (serializedThreadState.ValueKind != JsonValueKind.Object)
{
throw new ArgumentException("The serialized thread state must be a JSON object.", nameof(serializedThreadState));
}
var state = serializedThreadState.Deserialize(
AgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ThreadState))) as ThreadState;
var thread = new ChatClientAgentThread();
thread.AIContextProvider = aiContextProviderFactory is not null
? await aiContextProviderFactory.Invoke(state?.AIContextProviderState ?? default, jsonSerializerOptions, cancellationToken).ConfigureAwait(false)
: null;
if (state?.ConversationId is string threadId)
{
thread.ConversationId = threadId;
// Since we have an ID, we should not have a chat message store and we can return here.
return thread;
}
thread._messageStore =
chatMessageStoreFactory is not null
? await chatMessageStoreFactory.Invoke(state?.StoreState ?? default, jsonSerializerOptions, cancellationToken).ConfigureAwait(false)
: new InMemoryChatMessageStore(state?.StoreState ?? default, jsonSerializerOptions); // default to an in-memory store
return thread;
}
/// <inheritdoc/>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
@@ -22,7 +22,7 @@ public abstract class ChatClientAgentRunStreamingTests<TAgentFixture>(Func<TAgen
{
// Arrange
var agent = await this.Fixture.CreateChatClientAgentAsync(instructions: "Always respond with 'Computer says no', even if there was no user input.");
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await using var agentCleanup = new AgentCleanup(agent, this.Fixture);
await using var threadCleanup = new ThreadCleanup(thread, this.Fixture);
@@ -53,7 +53,7 @@ public abstract class ChatClientAgentRunStreamingTests<TAgentFixture>(Func<TAgen
AIFunctionFactory.Create(MenuPlugin.GetSpecials),
AIFunctionFactory.Create(MenuPlugin.GetItemPrice)
]);
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
foreach (var questionAndAnswer in questionsAndAnswers)
{
@@ -21,7 +21,7 @@ public abstract class ChatClientAgentRunTests<TAgentFixture>(Func<TAgentFixture>
{
// Arrange
var agent = await this.Fixture.CreateChatClientAgentAsync(instructions: "ALWAYS RESPOND WITH 'Computer says no', even if there was no user input.");
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await using var agentCleanup = new AgentCleanup(agent, this.Fixture);
await using var threadCleanup = new ThreadCleanup(thread, this.Fixture);
@@ -53,7 +53,7 @@ public abstract class ChatClientAgentRunTests<TAgentFixture>(Func<TAgentFixture>
AIFunctionFactory.Create(MenuPlugin.GetSpecials),
AIFunctionFactory.Create(MenuPlugin.GetItemPrice)
]);
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
foreach (var questionAndAnswer in questionsAndAnswers)
{
@@ -24,7 +24,7 @@ public abstract class RunStreamingTests<TAgentFixture>(Func<TAgentFixture> creat
{
// Arrange
var agent = this.Fixture.Agent;
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
// Act
@@ -36,7 +36,7 @@ public abstract class RunStreamingTests<TAgentFixture>(Func<TAgentFixture> creat
{
// Arrange
var agent = this.Fixture.Agent;
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
// Act
@@ -52,7 +52,7 @@ public abstract class RunStreamingTests<TAgentFixture>(Func<TAgentFixture> creat
{
// Arrange
var agent = this.Fixture.Agent;
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
// Act
@@ -68,7 +68,7 @@ public abstract class RunStreamingTests<TAgentFixture>(Func<TAgentFixture> creat
{
// Arrange
var agent = this.Fixture.Agent;
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
// Act
@@ -92,7 +92,7 @@ public abstract class RunStreamingTests<TAgentFixture>(Func<TAgentFixture> creat
const string Q1 = "What is the capital of France.";
const string Q2 = "And Austria?";
var agent = this.Fixture.Agent;
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
// Act
@@ -24,7 +24,7 @@ public abstract class RunTests<TAgentFixture>(Func<TAgentFixture> createAgentFix
{
// Arrange
var agent = this.Fixture.Agent;
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
// Act
@@ -39,7 +39,7 @@ public abstract class RunTests<TAgentFixture>(Func<TAgentFixture> createAgentFix
{
// Arrange
var agent = this.Fixture.Agent;
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
// Act
@@ -57,7 +57,7 @@ public abstract class RunTests<TAgentFixture>(Func<TAgentFixture> createAgentFix
{
// Arrange
var agent = this.Fixture.Agent;
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
// Act
@@ -74,7 +74,7 @@ public abstract class RunTests<TAgentFixture>(Func<TAgentFixture> createAgentFix
{
// Arrange
var agent = this.Fixture.Agent;
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
// Act
@@ -99,7 +99,7 @@ public abstract class RunTests<TAgentFixture>(Func<TAgentFixture> createAgentFix
const string Q1 = "What is the capital of France.";
const string Q2 = "And Austria?";
var agent = this.Fixture.Agent;
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
// Act
@@ -148,7 +148,7 @@ public sealed class A2AAgentTests : IDisposable
new(ChatRole.User, "Test message")
};
var thread = this._agent.GetNewThread();
var thread = await this._agent.GetNewThreadAsync();
// Act
await this._agent.RunAsync(inputMessages, thread);
@@ -168,7 +168,7 @@ public sealed class A2AAgentTests : IDisposable
new(ChatRole.User, "Test message")
};
var thread = this._agent.GetNewThread();
var thread = await this._agent.GetNewThreadAsync();
var a2aThread = (A2AAgentThread)thread;
a2aThread.ContextId = "existing-context-id";
@@ -201,7 +201,7 @@ public sealed class A2AAgentTests : IDisposable
ContextId = "different-context"
};
var thread = this._agent.GetNewThread();
var thread = await this._agent.GetNewThreadAsync();
var a2aThread = (A2AAgentThread)thread;
a2aThread.ContextId = "existing-context-id";
@@ -272,7 +272,7 @@ public sealed class A2AAgentTests : IDisposable
ContextId = "new-stream-context"
};
var thread = this._agent.GetNewThread();
var thread = await this._agent.GetNewThreadAsync();
// Act
await foreach (var _ in this._agent.RunStreamingAsync(inputMessages, thread))
@@ -296,7 +296,7 @@ public sealed class A2AAgentTests : IDisposable
this._handler.StreamingResponseToReturn = new AgentMessage();
var thread = this._agent.GetNewThread();
var thread = await this._agent.GetNewThreadAsync();
var a2aThread = (A2AAgentThread)thread;
a2aThread.ContextId = "existing-context-id";
@@ -316,7 +316,7 @@ public sealed class A2AAgentTests : IDisposable
public async Task RunStreamingAsync_WithThreadHavingDifferentContextId_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
var thread = this._agent.GetNewThread();
var thread = await this._agent.GetNewThreadAsync();
var a2aThread = (A2AAgentThread)thread;
a2aThread.ContextId = "existing-context-id";
@@ -440,7 +440,7 @@ public sealed class A2AAgentTests : IDisposable
Parts = [new TextPart { Text = "Response to task" }]
};
var thread = (A2AAgentThread)this._agent.GetNewThread();
var thread = (A2AAgentThread)await this._agent.GetNewThreadAsync();
thread.TaskId = "task-123";
var inputMessage = new ChatMessage(ChatRole.User, "Please make the background transparent");
@@ -466,7 +466,7 @@ public sealed class A2AAgentTests : IDisposable
Status = new() { State = TaskState.Submitted }
};
var thread = this._agent.GetNewThread();
var thread = await this._agent.GetNewThreadAsync();
// Act
await this._agent.RunAsync("Start a task", thread);
@@ -492,7 +492,7 @@ public sealed class A2AAgentTests : IDisposable
}
};
var thread = this._agent.GetNewThread();
var thread = await this._agent.GetNewThreadAsync();
// Act
var result = await this._agent.RunAsync("Start a long-running task", thread);
@@ -586,7 +586,7 @@ public sealed class A2AAgentTests : IDisposable
Parts = [new TextPart { Text = "Response to task" }]
};
var thread = (A2AAgentThread)this._agent.GetNewThread();
var thread = (A2AAgentThread)await this._agent.GetNewThreadAsync();
thread.TaskId = "task-123";
// Act
@@ -613,7 +613,7 @@ public sealed class A2AAgentTests : IDisposable
Status = new() { State = TaskState.Submitted }
};
var thread = this._agent.GetNewThread();
var thread = await this._agent.GetNewThreadAsync();
// Act
await foreach (var _ in this._agent.RunStreamingAsync("Start a task", thread))
@@ -686,7 +686,7 @@ public sealed class A2AAgentTests : IDisposable
]
};
var thread = this._agent.GetNewThread();
var thread = await this._agent.GetNewThreadAsync();
// Act
var updates = new List<AgentRunResponseUpdate>();
@@ -725,7 +725,7 @@ public sealed class A2AAgentTests : IDisposable
Status = new() { State = TaskState.Working }
};
var thread = this._agent.GetNewThread();
var thread = await this._agent.GetNewThreadAsync();
// Act
var updates = new List<AgentRunResponseUpdate>();
@@ -768,7 +768,7 @@ public sealed class A2AAgentTests : IDisposable
}
};
var thread = this._agent.GetNewThread();
var thread = await this._agent.GetNewThreadAsync();
// Act
var updates = new List<AgentRunResponseUpdate>();
@@ -228,7 +228,7 @@ public sealed class AGUIAgentTests
var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "agent1", description: "Test agent", tools: []);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
List<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Hello")];
// Act
@@ -244,17 +244,17 @@ public sealed class AGUIAgentTests
}
[Fact]
public void DeserializeThread_WithValidState_ReturnsChatClientAgentThread()
public async Task DeserializeThread_WithValidState_ReturnsChatClientAgentThreadAsync()
{
// Arrange
using var httpClient = new HttpClient();
var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "agent1", description: "Test agent", tools: []);
AgentThread originalThread = agent.GetNewThread();
AgentThread originalThread = await agent.GetNewThreadAsync();
JsonElement serialized = originalThread.Serialize();
// Act
AgentThread deserialized = agent.DeserializeThread(serialized);
AgentThread deserialized = await agent.DeserializeThreadAsync(serialized);
// Assert
Assert.NotNull(deserialized);
@@ -487,7 +487,7 @@ public sealed class AGUIAgentTests
var chatClient = new AGUIChatClient(httpClient, "http://localhost/agent", null, AGUIJsonSerializerContext.Default.Options);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "agent1", description: "Test agent", tools: [testTool]);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
List<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Test")];
// Act
@@ -378,10 +378,10 @@ public class AIAgentTests
protected override string? IdCore { get; }
public override AgentThread GetNewThread()
public override async ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
public override async ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override Task<AgentRunResponse> RunCoreAsync(
@@ -35,7 +35,7 @@ public class DelegatingAIAgentTests
this._innerAgentMock.Protected().SetupGet<string>("IdCore").Returns("test-agent-id");
this._innerAgentMock.Setup(x => x.Name).Returns("Test Agent");
this._innerAgentMock.Setup(x => x.Description).Returns("Test Description");
this._innerAgentMock.Setup(x => x.GetNewThread()).Returns(this._testThread);
this._innerAgentMock.Setup(x => x.GetNewThreadAsync()).ReturnsAsync(this._testThread);
this._innerAgentMock
.Protected()
@@ -132,17 +132,17 @@ public class DelegatingAIAgentTests
#region Method Delegation Tests
/// <summary>
/// Verify that GetNewThread delegates to inner agent.
/// Verify that GetNewThreadAsync delegates to inner agent.
/// </summary>
[Fact]
public void GetNewThread_DelegatesToInnerAgent()
public async Task GetNewThreadAsync_DelegatesToInnerAgentAsync()
{
// Act
var thread = this._delegatingAgent.GetNewThread();
var thread = await this._delegatingAgent.GetNewThreadAsync();
// Assert
Assert.Same(this._testThread, thread);
this._innerAgentMock.Verify(x => x.GetNewThread(), Times.Once);
this._innerAgentMock.Verify(x => x.GetNewThreadAsync(), Times.Once);
}
/// <summary>
@@ -53,7 +53,7 @@ public class AzureAIProjectChatClientTests
});
// Act
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await agent.RunAsync("Hello", thread);
Assert.True(requestTriggered);
@@ -102,7 +102,7 @@ public class AzureAIProjectChatClientTests
});
// Act
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await agent.RunAsync("Hello", thread, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "conv_12345" } });
Assert.True(requestTriggered);
@@ -151,7 +151,7 @@ public class AzureAIProjectChatClientTests
});
// Act
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await agent.RunAsync("Hello", thread, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "conv_12345" } });
Assert.True(requestTriggered);
@@ -200,7 +200,7 @@ public class AzureAIProjectChatClientTests
});
// Act
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
await agent.RunAsync("Hello", thread, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "resp_0888a" } });
Assert.True(requestTriggered);
@@ -66,12 +66,12 @@ public sealed class AggregatorPromptAgentFactoryTests
private sealed class TestAgent : AIAgent
{
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public override AgentThread GetNewThread()
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
@@ -51,7 +51,7 @@ public sealed class AgentEntityTests(ITestOutputHelper outputHelper) : IDisposab
// A proxy agent is needed to call the hosted test agent
AIAgent simpleAgentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services);
AgentThread thread = simpleAgentProxy.GetNewThread();
AgentThread thread = await simpleAgentProxy.GetNewThreadAsync(this.TestTimeoutToken);
DurableTaskClient client = testHelper.GetClient();
@@ -98,7 +98,7 @@ public sealed class AgentEntityTests(ITestOutputHelper outputHelper) : IDisposab
// A proxy agent is needed to call the hosted test agent
AIAgent simpleAgentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services);
AgentThread thread = simpleAgentProxy.GetNewThread();
AgentThread thread = await simpleAgentProxy.GetNewThreadAsync(this.TestTimeoutToken);
DurableTaskClient client = testHelper.GetClient();
@@ -184,7 +184,7 @@ public sealed class AgentEntityTests(ITestOutputHelper outputHelper) : IDisposab
public override async Task<string> RunAsync(TaskOrchestrationContext context, string input)
{
DurableAIAgent writer = context.GetAgent("TestAgent");
AgentThread writerThread = writer.GetNewThread();
AgentThread writerThread = await writer.GetNewThreadAsync();
await writer.RunAsync(
message: context.GetInput<string>()!,
@@ -51,7 +51,7 @@ public sealed class ExternalClientTests(ITestOutputHelper outputHelper) : IDispo
AIAgent simpleAgentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services);
// Act: send a prompt to the agent and wait for a response
AgentThread thread = simpleAgentProxy.GetNewThread();
AgentThread thread = await simpleAgentProxy.GetNewThreadAsync(this.TestTimeoutToken);
await simpleAgentProxy.RunAsync(
message: "Hello!",
thread,
@@ -156,7 +156,7 @@ public sealed class ExternalClientTests(ITestOutputHelper outputHelper) : IDispo
{
// 1. Get agent and create a session
DurableAIAgent agent = context.GetAgent("SimpleAgent");
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync(this.TestTimeoutToken);
// 2. Call an agent and tell it my name
await agent.RunAsync($"My name is {name}.", thread);
@@ -194,7 +194,7 @@ public sealed class ExternalClientTests(ITestOutputHelper outputHelper) : IDispo
AIAgent workflowManagerAgentProxy = testHelper.Services.GetDurableAgentProxy("WorkflowAgent");
// Act: send a prompt to the agent
AgentThread thread = workflowManagerAgentProxy.GetNewThread();
AgentThread thread = await workflowManagerAgentProxy.GetNewThreadAsync(this.TestTimeoutToken);
await workflowManagerAgentProxy.RunAsync(
message: "Start a greeting workflow for \"John Doe\".",
thread,
@@ -55,7 +55,7 @@ public sealed class TimeToLiveTests(ITestOutputHelper outputHelper) : IDisposabl
});
AIAgent agentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services);
AgentThread thread = agentProxy.GetNewThread();
AgentThread thread = await agentProxy.GetNewThreadAsync(this.TestTimeoutToken);
DurableTaskClient client = testHelper.GetClient();
AgentSessionId sessionId = thread.GetService<AgentSessionId>();
@@ -120,7 +120,7 @@ public sealed class TimeToLiveTests(ITestOutputHelper outputHelper) : IDisposabl
});
AIAgent agentProxy = simpleAgent.AsDurableAgentProxy(testHelper.Services);
AgentThread thread = agentProxy.GetNewThread();
AgentThread thread = await agentProxy.GetNewThreadAsync(this.TestTimeoutToken);
DurableTaskClient client = testHelper.GetClient();
AgentSessionId sessionId = thread.GetService<AgentSessionId>();
@@ -175,7 +175,7 @@ public sealed class AIAgentExtensionsTests
{
Mock<AIAgent> agentMock = new() { CallBase = true };
agentMock.SetupGet(x => x.Name).Returns("TestAgent");
agentMock.Setup(x => x.GetNewThread()).Returns(new TestAgentThread());
agentMock.Setup(x => x.GetNewThreadAsync()).ReturnsAsync(new TestAgentThread());
agentMock
.Protected()
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
@@ -194,7 +194,7 @@ public sealed class AIAgentExtensionsTests
{
Mock<AIAgent> agentMock = new() { CallBase = true };
agentMock.SetupGet(x => x.Name).Returns("TestAgent");
agentMock.Setup(x => x.GetNewThread()).Returns(new TestAgentThread());
agentMock.Setup(x => x.GetNewThreadAsync()).ReturnsAsync(new TestAgentThread());
agentMock
.Protected()
.Setup<Task<AgentRunResponse>>("RunCoreAsync",
@@ -31,7 +31,7 @@ public sealed class BasicStreamingTests : IAsyncDisposable
await this.SetupTestServerAsync();
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []);
ChatClientAgentThread thread = (ChatClientAgentThread)agent.GetNewThread();
ChatClientAgentThread thread = (ChatClientAgentThread)await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "hello");
List<AgentRunResponseUpdate> updates = [];
@@ -62,7 +62,7 @@ public sealed class BasicStreamingTests : IAsyncDisposable
await this.SetupTestServerAsync();
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []);
ChatClientAgentThread thread = (ChatClientAgentThread)agent.GetNewThread();
ChatClientAgentThread thread = (ChatClientAgentThread)await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "test");
List<AgentRunResponseUpdate> updates = [];
@@ -106,7 +106,7 @@ public sealed class BasicStreamingTests : IAsyncDisposable
await this.SetupTestServerAsync();
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []);
ChatClientAgentThread thread = (ChatClientAgentThread)agent.GetNewThread();
ChatClientAgentThread thread = (ChatClientAgentThread)await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "hello");
// Act
@@ -125,7 +125,7 @@ public sealed class BasicStreamingTests : IAsyncDisposable
await this.SetupTestServerAsync();
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []);
ChatClientAgentThread chatClientThread = (ChatClientAgentThread)agent.GetNewThread();
ChatClientAgentThread chatClientThread = (ChatClientAgentThread)await agent.GetNewThreadAsync();
ChatMessage firstUserMessage = new(ChatRole.User, "First question");
// Act - First turn
@@ -169,7 +169,7 @@ public sealed class BasicStreamingTests : IAsyncDisposable
await this.SetupTestServerAsync(useMultiMessageAgent: true);
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []);
ChatClientAgentThread chatClientThread = (ChatClientAgentThread)agent.GetNewThread();
ChatClientAgentThread chatClientThread = (ChatClientAgentThread)await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "Tell me a story");
List<AgentRunResponseUpdate> updates = [];
@@ -201,7 +201,7 @@ public sealed class BasicStreamingTests : IAsyncDisposable
await this.SetupTestServerAsync();
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []);
ChatClientAgentThread chatClientThread = (ChatClientAgentThread)agent.GetNewThread();
ChatClientAgentThread chatClientThread = (ChatClientAgentThread)await agent.GetNewThreadAsync();
// Multiple user messages sent in one turn
ChatMessage[] userMessages =
@@ -280,15 +280,11 @@ internal sealed class FakeChatClientAgent : AIAgent
public override string? Description => "A fake agent for testing";
public override AgentThread GetNewThread()
{
return new FakeInMemoryAgentThread();
}
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentThread());
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
{
return new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions);
}
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions));
protected override async Task<AgentRunResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
@@ -348,15 +344,11 @@ internal sealed class FakeMultiMessageAgent : AIAgent
public override string? Description => "A fake agent that sends multiple messages for testing";
public override AgentThread GetNewThread()
{
return new FakeInMemoryAgentThread();
}
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentThread());
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
{
return new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions);
}
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions));
protected override async Task<AgentRunResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
@@ -334,12 +334,11 @@ internal sealed class FakeForwardedPropsAgent : AIAgent
await Task.CompletedTask;
}
public override AgentThread GetNewThread() => new FakeInMemoryAgentThread();
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentThread());
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
{
return new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions);
}
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions));
private sealed class FakeInMemoryAgentThread : InMemoryAgentThread
{
@@ -34,7 +34,7 @@ public sealed class SharedStateTests : IAsyncDisposable
await this.SetupTestServerAsync(fakeAgent);
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []);
ChatClientAgentThread thread = (ChatClientAgentThread)agent.GetNewThread();
ChatClientAgentThread thread = (ChatClientAgentThread)await agent.GetNewThreadAsync();
string stateJson = JsonSerializer.Serialize(initialState);
byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson);
@@ -77,7 +77,7 @@ public sealed class SharedStateTests : IAsyncDisposable
await this.SetupTestServerAsync(fakeAgent);
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []);
ChatClientAgentThread thread = (ChatClientAgentThread)agent.GetNewThread();
ChatClientAgentThread thread = (ChatClientAgentThread)await agent.GetNewThreadAsync();
string stateJson = JsonSerializer.Serialize(initialState);
byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson);
@@ -119,7 +119,7 @@ public sealed class SharedStateTests : IAsyncDisposable
await this.SetupTestServerAsync(fakeAgent);
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []);
ChatClientAgentThread thread = (ChatClientAgentThread)agent.GetNewThread();
ChatClientAgentThread thread = (ChatClientAgentThread)await agent.GetNewThreadAsync();
string stateJson = JsonSerializer.Serialize(complexState);
byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson);
@@ -159,7 +159,7 @@ public sealed class SharedStateTests : IAsyncDisposable
await this.SetupTestServerAsync(fakeAgent);
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []);
ChatClientAgentThread thread = (ChatClientAgentThread)agent.GetNewThread();
ChatClientAgentThread thread = (ChatClientAgentThread)await agent.GetNewThreadAsync();
string stateJson = JsonSerializer.Serialize(initialState);
byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson);
@@ -210,7 +210,7 @@ public sealed class SharedStateTests : IAsyncDisposable
await this.SetupTestServerAsync(fakeAgent);
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []);
ChatClientAgentThread thread = (ChatClientAgentThread)agent.GetNewThread();
ChatClientAgentThread thread = (ChatClientAgentThread)await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "hello");
@@ -243,7 +243,7 @@ public sealed class SharedStateTests : IAsyncDisposable
await this.SetupTestServerAsync(fakeAgent);
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []);
ChatClientAgentThread thread = (ChatClientAgentThread)agent.GetNewThread();
ChatClientAgentThread thread = (ChatClientAgentThread)await agent.GetNewThreadAsync();
string stateJson = JsonSerializer.Serialize(emptyState);
byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson);
@@ -280,7 +280,7 @@ public sealed class SharedStateTests : IAsyncDisposable
await this.SetupTestServerAsync(fakeAgent);
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Sample assistant", tools: []);
ChatClientAgentThread thread = (ChatClientAgentThread)agent.GetNewThread();
ChatClientAgentThread thread = (ChatClientAgentThread)await agent.GetNewThreadAsync();
string stateJson = JsonSerializer.Serialize(initialState);
byte[] stateBytes = System.Text.Encoding.UTF8.GetBytes(stateJson);
@@ -417,12 +417,11 @@ internal sealed class FakeStateAgent : AIAgent
await Task.CompletedTask;
}
public override AgentThread GetNewThread() => new FakeInMemoryAgentThread();
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentThread());
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
{
return new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions);
}
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new FakeInMemoryAgentThread(serializedThread, jsonSerializerOptions));
private sealed class FakeInMemoryAgentThread : InMemoryAgentThread
{
@@ -45,7 +45,7 @@ public sealed class ToolCallingTests : IAsyncDisposable
await this.SetupTestServerAsync(serverTools: [serverTool]);
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "Call the server function");
List<AgentRunResponseUpdate> updates = [];
@@ -93,7 +93,7 @@ public sealed class ToolCallingTests : IAsyncDisposable
await this.SetupTestServerAsync(serverTools: [getWeatherTool, getTimeTool]);
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "What's the weather and time?");
List<AgentRunResponseUpdate> updates = [];
@@ -134,7 +134,7 @@ public sealed class ToolCallingTests : IAsyncDisposable
await this.SetupTestServerAsync();
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: [clientTool]);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "Call the client function");
List<AgentRunResponseUpdate> updates = [];
@@ -182,7 +182,7 @@ public sealed class ToolCallingTests : IAsyncDisposable
await this.SetupTestServerAsync();
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: [calculateTool, formatTool]);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "Calculate 5 + 3 and format 'hello'");
List<AgentRunResponseUpdate> updates = [];
@@ -233,7 +233,7 @@ public sealed class ToolCallingTests : IAsyncDisposable
await this.SetupTestServerAsync(serverTools: [serverTool]);
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: [clientTool]);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "Get both server and client data");
List<AgentRunResponseUpdate> updates = [];
@@ -298,7 +298,7 @@ public sealed class ToolCallingTests : IAsyncDisposable
await this.SetupTestServerAsync(serverTools: [testTool]);
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "Call the test function");
List<AgentRunResponseUpdate> updates = [];
@@ -342,7 +342,7 @@ public sealed class ToolCallingTests : IAsyncDisposable
await this.SetupTestServerAsync(serverTools: [func1, func2], triggerParallelCalls: true);
var chatClient = new AGUIChatClient(this._client!, "", null);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "Call both functions in parallel");
List<AgentRunResponseUpdate> updates = [];
@@ -428,7 +428,7 @@ public sealed class ToolCallingTests : IAsyncDisposable
await this.SetupTestServerAsync(serverTools: [serverTool], jsonSerializerOptions: ServerJsonContext.Default.Options);
var chatClient = new AGUIChatClient(this._client!, "", null, ServerJsonContext.Default.Options);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: []);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "Get server forecast for Seattle for 5 days");
List<AgentRunResponseUpdate> updates = [];
@@ -474,7 +474,7 @@ public sealed class ToolCallingTests : IAsyncDisposable
await this.SetupTestServerAsync();
var chatClient = new AGUIChatClient(this._client!, "", null, ClientJsonContext.Default.Options);
AIAgent agent = chatClient.CreateAIAgent(instructions: null, name: "assistant", description: "Test assistant", tools: [clientTool]);
AgentThread thread = agent.GetNewThread();
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage userMessage = new(ChatRole.User, "Get client forecast for Portland with hourly data");
List<AgentRunResponseUpdate> updates = [];
@@ -425,10 +425,11 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests
public override string? Description => "Agent that produces multiple text chunks";
public override AgentThread GetNewThread() => new TestInMemoryAgentThread();
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default) =>
new(new TestInMemoryAgentThread());
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) =>
new TestInMemoryAgentThread(serializedThread, jsonSerializerOptions);
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new TestInMemoryAgentThread(serializedThread, jsonSerializerOptions));
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
@@ -514,10 +515,11 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests
public override string? Description => "Test agent";
public override AgentThread GetNewThread() => new TestInMemoryAgentThread();
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default) =>
new(new TestInMemoryAgentThread());
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) =>
new TestInMemoryAgentThread(serializedThread, jsonSerializerOptions);
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
new(new TestInMemoryAgentThread(serializedThread, jsonSerializerOptions));
protected override Task<AgentRunResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
{
@@ -11,11 +11,11 @@ internal sealed class TestAgent(string name, string description) : AIAgent
public override string? Description => description;
public override AgentThread GetNewThread() => new DummyAgentThread();
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default) => new(new DummyAgentThread());
public override AgentThread DeserializeThread(
public override ValueTask<AgentThread> DeserializeThreadAsync(
JsonElement serializedThread,
JsonSerializerOptions? jsonSerializerOptions = null) => new DummyAgentThread();
JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => new(new DummyAgentThread());
protected override Task<AgentRunResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
@@ -324,10 +324,10 @@ public class AgentExtensionsTests
this._exceptionToThrow = exceptionToThrow;
}
public override AgentThread GetNewThread()
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public override string? Name { get; }
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
@@ -115,12 +117,11 @@ public class ChatClientAgentOptionsTests
const string Description = "Test description";
var tools = new List<AITool> { AIFunctionFactory.Create(() => "test") };
static ChatMessageStore ChatMessageStoreFactory(
ChatClientAgentOptions.ChatMessageStoreFactoryContext ctx) => new Mock<ChatMessageStore>().Object;
static ValueTask<ChatMessageStore> ChatMessageStoreFactoryAsync(
ChatClientAgentOptions.ChatMessageStoreFactoryContext ctx, CancellationToken ct) => new(new Mock<ChatMessageStore>().Object);
static AIContextProvider AIContextProviderFactory(
ChatClientAgentOptions.AIContextProviderFactoryContext ctx) =>
new Mock<AIContextProvider>().Object;
static ValueTask<AIContextProvider> AIContextProviderFactoryAsync(
ChatClientAgentOptions.AIContextProviderFactoryContext ctx, CancellationToken ct) => new(new Mock<AIContextProvider>().Object);
var original = new ChatClientAgentOptions()
{
@@ -128,8 +129,8 @@ public class ChatClientAgentOptionsTests
Description = Description,
ChatOptions = new() { Tools = tools },
Id = "test-id",
ChatMessageStoreFactory = ChatMessageStoreFactory,
AIContextProviderFactory = AIContextProviderFactory
ChatMessageStoreFactory = ChatMessageStoreFactoryAsync,
AIContextProviderFactory = AIContextProviderFactoryAsync
};
// Act
@@ -241,8 +241,8 @@ public partial class ChatClientAgentTests
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
// Create a thread using the agent's GetNewThread method
var thread = agent.GetNewThread();
// Create a thread using the agent's GetNewThreadAsync method
var thread = await agent.GetNewThreadAsync();
// Act
await agent.RunAsync([new(ChatRole.User, "new message")], thread: thread);
@@ -438,8 +438,8 @@ public partial class ChatClientAgentTests
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, ChatMessageStore>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(new InMemoryChatMessageStore());
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, CancellationToken, ValueTask<ChatMessageStore>>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>())).ReturnsAsync(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
@@ -447,7 +447,7 @@ public partial class ChatClientAgentTests
});
// Act
ChatClientAgentThread? thread = agent.GetNewThread() as ChatClientAgentThread;
ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
await agent.RunAsync([new(ChatRole.User, "test")], thread);
// Assert
@@ -455,7 +455,7 @@ public partial class ChatClientAgentTests
Assert.Equal(2, messageStore.Count);
Assert.Equal("test", messageStore[0].Text);
Assert.Equal("response", messageStore[1].Text);
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>()), Times.Once);
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>()), Times.Once);
}
/// <summary>
@@ -477,7 +477,7 @@ public partial class ChatClientAgentTests
});
// Act
ChatClientAgentThread? thread = agent.GetNewThread() as ChatClientAgentThread;
ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
await agent.RunAsync([new(ChatRole.User, "test")], thread);
// Assert
@@ -509,8 +509,8 @@ public partial class ChatClientAgentTests
It.IsAny<ChatMessageStore.InvokedContext>(),
It.IsAny<CancellationToken>())).Returns(new ValueTask());
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, ChatMessageStore>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(mockChatMessageStore.Object);
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, CancellationToken, ValueTask<ChatMessageStore>>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>())).ReturnsAsync(mockChatMessageStore.Object);
ChatClientAgent agent = new(mockService.Object, options: new()
{
@@ -519,7 +519,7 @@ public partial class ChatClientAgentTests
});
// Act
ChatClientAgentThread? thread = agent.GetNewThread() as ChatClientAgentThread;
ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
await agent.RunAsync([new(ChatRole.User, "test")], thread);
// Assert
@@ -538,7 +538,7 @@ public partial class ChatClientAgentTests
It.Is<ChatMessageStore.InvokedContext>(x => x.RequestMessages.Count() == 1 && x.ChatMessageStoreMessages.Count() == 1 && x.ResponseMessages!.Count() == 1),
It.IsAny<CancellationToken>()),
Times.Once);
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>()), Times.Once);
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>()), Times.Once);
}
/// <summary>
@@ -557,8 +557,8 @@ public partial class ChatClientAgentTests
Mock<ChatMessageStore> mockChatMessageStore = new();
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, ChatMessageStore>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(mockChatMessageStore.Object);
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, CancellationToken, ValueTask<ChatMessageStore>>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>())).ReturnsAsync(mockChatMessageStore.Object);
ChatClientAgent agent = new(mockService.Object, options: new()
{
@@ -567,7 +567,7 @@ public partial class ChatClientAgentTests
});
// Act
ChatClientAgentThread? thread = agent.GetNewThread() as ChatClientAgentThread;
ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync([new(ChatRole.User, "test")], thread));
// Assert
@@ -576,7 +576,7 @@ public partial class ChatClientAgentTests
It.Is<ChatMessageStore.InvokedContext>(x => x.RequestMessages.Count() == 1 && x.ResponseMessages == null && x.InvokeException!.Message == "Test Error"),
It.IsAny<CancellationToken>()),
Times.Once);
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>()), Times.Once);
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>()), Times.Once);
}
/// <summary>
@@ -592,8 +592,8 @@ public partial class ChatClientAgentTests
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, ChatMessageStore>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(new InMemoryChatMessageStore());
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, CancellationToken, ValueTask<ChatMessageStore>>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>())).ReturnsAsync(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
@@ -601,7 +601,7 @@ public partial class ChatClientAgentTests
});
// Act & Assert
ChatClientAgentThread? thread = agent.GetNewThread() as ChatClientAgentThread;
ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync([new(ChatRole.User, "test")], thread));
Assert.Equal("Only the ConversationId or MessageStore may be set, but not both and switching from one to another is not supported.", exception.Message);
}
@@ -649,10 +649,10 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = (_, _) => new(mockProvider.Object), ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
var thread = agent.GetNewThread() as ChatClientAgentThread;
var thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
await agent.RunAsync(requestMessages, thread);
// Assert
@@ -711,7 +711,7 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = (_, _) => new(mockProvider.Object), ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(requestMessages));
@@ -757,7 +757,7 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AIContext());
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = _ => mockProvider.Object, ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
ChatClientAgent agent = new(mockService.Object, options: new() { AIContextProviderFactory = (_, _) => new(mockProvider.Object), ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] } });
// Act
await agent.RunAsync([new(ChatRole.User, "user message")]);
@@ -2010,8 +2010,8 @@ public partial class ChatClientAgentTests
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(ToAsyncEnumerableAsync(returnUpdates));
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, ChatMessageStore>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(new InMemoryChatMessageStore());
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, CancellationToken, ValueTask<ChatMessageStore>>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>())).ReturnsAsync(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
@@ -2019,7 +2019,7 @@ public partial class ChatClientAgentTests
});
// Act
ChatClientAgentThread? thread = agent.GetNewThread() as ChatClientAgentThread;
ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
await agent.RunStreamingAsync([new(ChatRole.User, "test")], thread).ToListAsync();
// Assert
@@ -2027,7 +2027,7 @@ public partial class ChatClientAgentTests
Assert.Equal(2, messageStore.Count);
Assert.Equal("test", messageStore[0].Text);
Assert.Equal("what?", messageStore[1].Text);
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>()), Times.Once);
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>()), Times.Once);
}
/// <summary>
@@ -2048,8 +2048,8 @@ public partial class ChatClientAgentTests
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(ToAsyncEnumerableAsync(returnUpdates));
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, ChatMessageStore>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>())).Returns(new InMemoryChatMessageStore());
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, CancellationToken, ValueTask<ChatMessageStore>>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>())).ReturnsAsync(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
@@ -2057,7 +2057,7 @@ public partial class ChatClientAgentTests
});
// Act & Assert
ChatClientAgentThread? thread = agent.GetNewThread() as ChatClientAgentThread;
ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await agent.RunStreamingAsync([new(ChatRole.User, "test")], thread).ToListAsync());
Assert.Equal("Only the ConversationId or MessageStore may be set, but not both and switching from one to another is not supported.", exception.Message);
}
@@ -2105,10 +2105,16 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] }, AIContextProviderFactory = _ => mockProvider.Object });
ChatClientAgent agent = new(
mockService.Object,
options: new()
{
ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] },
AIContextProviderFactory = (_, _) => new(mockProvider.Object)
});
// Act
var thread = agent.GetNewThread() as ChatClientAgentThread;
var thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
var updates = agent.RunStreamingAsync(requestMessages, thread);
_ = await updates.ToAgentRunResponseAsync();
@@ -2168,7 +2174,13 @@ public partial class ChatClientAgentTests
.Setup(p => p.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Returns(new ValueTask());
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] }, AIContextProviderFactory = _ => mockProvider.Object });
ChatClientAgent agent = new(
mockService.Object,
options: new()
{
ChatOptions = new() { Instructions = "base instructions", Tools = [AIFunctionFactory.Create(() => { }, "base function")] },
AIContextProviderFactory = (_, _) => new(mockProvider.Object)
});
// Act
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
@@ -93,7 +93,7 @@ public class ChatClientAgentThreadTests
#region Deserialize Tests
[Fact]
public async Task VerifyDeserializeConstructorWithMessagesAsync()
public async Task VerifyDeserializeWithMessagesAsync()
{
// Arrange
var json = JsonSerializer.Deserialize("""
@@ -103,7 +103,7 @@ public class ChatClientAgentThreadTests
""", TestJsonSerializerContext.Default.JsonElement);
// Act.
var thread = new ChatClientAgentThread(json);
var thread = await ChatClientAgentThread.DeserializeAsync(json);
// Assert
Assert.Null(thread.ConversationId);
@@ -115,7 +115,7 @@ public class ChatClientAgentThreadTests
}
[Fact]
public async Task VerifyDeserializeConstructorWithIdAsync()
public async Task VerifyDeserializeWithIdAsync()
{
// Arrange
var json = JsonSerializer.Deserialize("""
@@ -125,7 +125,7 @@ public class ChatClientAgentThreadTests
""", TestJsonSerializerContext.Default.JsonElement);
// Act
var thread = new ChatClientAgentThread(json);
var thread = await ChatClientAgentThread.DeserializeAsync(json);
// Assert
Assert.Equal("TestConvId", thread.ConversationId);
@@ -133,7 +133,7 @@ public class ChatClientAgentThreadTests
}
[Fact]
public async Task VerifyDeserializeConstructorWithAIContextProviderAsync()
public async Task VerifyDeserializeWithAIContextProviderAsync()
{
// Arrange
var json = JsonSerializer.Deserialize("""
@@ -145,7 +145,7 @@ public class ChatClientAgentThreadTests
Mock<AIContextProvider> mockProvider = new();
// Act
var thread = new ChatClientAgentThread(json, aiContextProviderFactory: (_, _) => mockProvider.Object);
var thread = await ChatClientAgentThread.DeserializeAsync(json, aiContextProviderFactory: (_, _, _) => new(mockProvider.Object));
// Assert
Assert.Null(thread.MessageStore);
@@ -153,14 +153,14 @@ public class ChatClientAgentThreadTests
}
[Fact]
public async Task DeserializeContructorWithInvalidJsonThrowsAsync()
public async Task DeserializeWithInvalidJsonThrowsAsync()
{
// Arrange
var invalidJson = JsonSerializer.Deserialize("[42]", TestJsonSerializerContext.Default.JsonElement);
var thread = new ChatClientAgentThread();
// Act & Assert
Assert.Throws<ArgumentException>(() => new ChatClientAgentThread(invalidJson));
await Assert.ThrowsAsync<ArgumentException>(() => ChatClientAgentThread.DeserializeAsync(invalidJson));
}
#endregion Deserialize Tests
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
@@ -12,7 +13,7 @@ namespace Microsoft.Agents.AI.UnitTests.ChatClient;
public class ChatClientAgent_DeserializeThreadTests
{
[Fact]
public void DeserializeThread_UsesAIContextProviderFactory_IfProvided()
public async Task DeserializeThread_UsesAIContextProviderFactory_IfProvidedAsync()
{
// Arrange
var mockChatClient = new Mock<IChatClient>();
@@ -21,10 +22,10 @@ public class ChatClientAgent_DeserializeThreadTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" },
AIContextProviderFactory = _ =>
AIContextProviderFactory = (_, _) =>
{
factoryCalled = true;
return mockContextProvider.Object;
return new ValueTask<AIContextProvider>(mockContextProvider.Object);
}
});
@@ -35,7 +36,7 @@ public class ChatClientAgent_DeserializeThreadTests
""", TestJsonSerializerContext.Default.JsonElement);
// Act
var thread = agent.DeserializeThread(json);
var thread = await agent.DeserializeThreadAsync(json);
// Assert
Assert.True(factoryCalled, "AIContextProviderFactory was not called.");
@@ -45,7 +46,7 @@ public class ChatClientAgent_DeserializeThreadTests
}
[Fact]
public void DeserializeThread_UsesChatMessageStoreFactory_IfProvided()
public async Task DeserializeThread_UsesChatMessageStoreFactory_IfProvidedAsync()
{
// Arrange
var mockChatClient = new Mock<IChatClient>();
@@ -54,10 +55,10 @@ public class ChatClientAgent_DeserializeThreadTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" },
ChatMessageStoreFactory = _ =>
ChatMessageStoreFactory = (_, _) =>
{
factoryCalled = true;
return mockMessageStore.Object;
return new ValueTask<ChatMessageStore>(mockMessageStore.Object);
}
});
@@ -68,7 +69,7 @@ public class ChatClientAgent_DeserializeThreadTests
""", TestJsonSerializerContext.Default.JsonElement);
// Act
var thread = agent.DeserializeThread(json);
var thread = await agent.DeserializeThreadAsync(json);
// Assert
Assert.True(factoryCalled, "ChatMessageStoreFactory was not called.");
@@ -1,17 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests.ChatClient;
/// <summary>
/// Contains unit tests for the ChatClientAgent.GetNewThread methods.
/// Contains unit tests for the ChatClientAgent.GetNewThreadAsync methods.
/// </summary>
public class ChatClientAgent_GetNewThreadTests
{
[Fact]
public void GetNewThread_UsesAIContextProviderFactory_IfProvided()
public async Task GetNewThread_UsesAIContextProviderFactory_IfProvidedAsync()
{
// Arrange
var mockChatClient = new Mock<IChatClient>();
@@ -20,15 +21,15 @@ public class ChatClientAgent_GetNewThreadTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" },
AIContextProviderFactory = _ =>
AIContextProviderFactory = (_, _) =>
{
factoryCalled = true;
return mockContextProvider.Object;
return new ValueTask<AIContextProvider>(mockContextProvider.Object);
}
});
// Act
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
// Assert
Assert.True(factoryCalled, "AIContextProviderFactory was not called.");
@@ -38,7 +39,7 @@ public class ChatClientAgent_GetNewThreadTests
}
[Fact]
public void GetNewThread_UsesChatMessageStoreFactory_IfProvided()
public async Task GetNewThread_UsesChatMessageStoreFactory_IfProvidedAsync()
{
// Arrange
var mockChatClient = new Mock<IChatClient>();
@@ -47,15 +48,15 @@ public class ChatClientAgent_GetNewThreadTests
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" },
ChatMessageStoreFactory = _ =>
ChatMessageStoreFactory = (_, _) =>
{
factoryCalled = true;
return mockMessageStore.Object;
return new ValueTask<ChatMessageStore>(mockMessageStore.Object);
}
});
// Act
var thread = agent.GetNewThread();
var thread = await agent.GetNewThreadAsync();
// Assert
Assert.True(factoryCalled, "ChatMessageStoreFactory was not called.");
@@ -65,7 +66,7 @@ public class ChatClientAgent_GetNewThreadTests
}
[Fact]
public void GetNewThread_UsesChatMessageStore_FromTypedOverload()
public async Task GetNewThread_UsesChatMessageStore_FromTypedOverloadAsync()
{
// Arrange
var mockChatClient = new Mock<IChatClient>();
@@ -73,7 +74,7 @@ public class ChatClientAgent_GetNewThreadTests
var agent = new ChatClientAgent(mockChatClient.Object);
// Act
var thread = agent.GetNewThread(mockMessageStore.Object);
var thread = await agent.GetNewThreadAsync(mockMessageStore.Object);
// Assert
Assert.IsType<ChatClientAgentThread>(thread);
@@ -82,7 +83,7 @@ public class ChatClientAgent_GetNewThreadTests
}
[Fact]
public void GetNewThread_UsesConversationId_FromTypedOverload()
public async Task GetNewThread_UsesConversationId_FromTypedOverloadAsync()
{
// Arrange
var mockChatClient = new Mock<IChatClient>();
@@ -90,7 +91,7 @@ public class ChatClientAgent_GetNewThreadTests
var agent = new ChatClientAgent(mockChatClient.Object);
// Act
var thread = agent.GetNewThread(TestConversationId);
var thread = await agent.GetNewThreadAsync(TestConversationId);
// Assert
Assert.IsType<ChatClientAgentThread>(thread);

Some files were not shown because too many files have changed in this diff Show More