.NET: [BREAKING] Subclass AgentThread so that different agents have their own threads with their own typed settings. (#798)

* Subclass AgentThread so that different agents have their own threads with their own typed settings.

* Address PR comment.

* Add unit tests for base abstract threads

* Fix style warning

* Fix stlying

* FIx and suppress warnings as needed.

* Remove covariant thread response types and fix some styling.

* Remove unecessary json property name attributes and make OrchestratingAgentThread private

* Fix break from merge from main.

* Fix formatting

* Fix deserialization bug in Memory sample

* Remove thread deletion from basic samples.

* Remove public constructors for thread subclasses and add more factory methods to concrete agent types.

* Update AgentProxy thread constructors to be internal as well.

* Revert AgentProxyThread to internal

* Change AIContextProvider to internal set

* Change conversation id and message store properties to internal set

* Update styling.

* Seal various thread types.

* Add thread type check for thread deletion

* Fix tests after latest merge from main

* Add thread type checks for thread deletion.

---------

Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
This commit is contained in:
westey
2025-09-23 11:30:06 +01:00
committed by GitHub
Unverified
parent 230cb083ce
commit 3571a7d321
68 changed files with 1774 additions and 613 deletions
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.AI.Agents;
/// <summary>
/// A base class for agent threads that operate entirely in memory without external storage.
/// </summary>
public abstract class InMemoryAgentThread : AgentThread
{
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryAgentThread"/> class.
/// </summary>
/// <param name="messageStore">An optional <see cref="InMemoryChatMessageStore"/> to use for storing chat messages. If null, a new instance will be created.</param>
protected InMemoryAgentThread(InMemoryChatMessageStore? messageStore = null)
{
this.MessageStore = messageStore ?? new InMemoryChatMessageStore();
}
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryAgentThread"/> class with the specified initial messages.
/// </summary>
/// <param name="messages">The messages to initialize the thread with.</param>
protected InMemoryAgentThread(IEnumerable<ChatMessage> messages)
{
this.MessageStore = new InMemoryChatMessageStore();
foreach (var message in messages)
{
this.MessageStore.Add(message);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryAgentThread"/> class from 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="messageStoreFactory">A factory function to create the <see cref="InMemoryChatMessageStore"/> from its serialized state.</param>
/// <exception cref="ArgumentException">The <paramref name="serializedThreadState"/> is not a JSON object.</exception>
/// <exception cref="JsonException">The <paramref name="serializedThreadState"/> is invalid or cannot be deserialized to the expected type.</exception>
protected InMemoryAgentThread(
JsonElement serializedThreadState,
JsonSerializerOptions? jsonSerializerOptions = null,
Func<JsonElement, JsonSerializerOptions?, InMemoryChatMessageStore>? messageStoreFactory = null)
{
if (serializedThreadState.ValueKind != JsonValueKind.Object)
{
throw new ArgumentException("The serialized thread state must be a JSON object.", nameof(serializedThreadState));
}
var state = JsonSerializer.Deserialize(
serializedThreadState,
AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(InMemoryAgentThreadState))) as InMemoryAgentThreadState;
this.MessageStore =
messageStoreFactory?.Invoke(state?.StoreState ?? default, jsonSerializerOptions) ??
new InMemoryChatMessageStore(state?.StoreState ?? default, jsonSerializerOptions);
}
/// <summary>
/// Gets or sets the <see cref="InMemoryChatMessageStore"/> used by this thread.
/// </summary>
public InMemoryChatMessageStore MessageStore { get; }
/// <summary>
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
/// </summary>
/// <param name="jsonSerializerOptions">The JSON serialization options to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
public override async Task<JsonElement> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
var storeState = await this.MessageStore.SerializeStateAsync(jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
var state = new InMemoryAgentThreadState
{
StoreState = storeState,
};
return JsonSerializer.SerializeToElement(state, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(InMemoryAgentThreadState)));
}
/// <inheritdoc />
protected internal override Task MessagesReceivedAsync(IEnumerable<ChatMessage> newMessages, CancellationToken cancellationToken = default)
=> this.MessageStore.AddMessagesAsync(newMessages, cancellationToken);
internal sealed class InMemoryAgentThreadState
{
public JsonElement? StoreState { get; set; }
}
}