diff --git a/dotnet/src/Agents/Abstractions/Agent.cs b/dotnet/src/Agents/Abstractions/Agent.cs
index a742d7ce6767..1dc1461f122f 100644
--- a/dotnet/src/Agents/Abstractions/Agent.cs
+++ b/dotnet/src/Agents/Abstractions/Agent.cs
@@ -48,19 +48,35 @@ public abstract class Agent
///
/// The message to pass to the agent.
/// The conversation thread to continue with this invocation. If not provided, creates a new thread.
- /// Optional arguments to pass to the agents's invocation, including any .
- /// The containing services, plugins, and other state for use by the agent.
/// Optional parameters for agent invocation.
/// The to monitor for cancellation requests. The default is .
/// An async list of response items that each contain a and an .
///
/// To continue this thread in the future, use an returned in one of the response items.
///
- public abstract IAsyncEnumerable> InvokeAsync(
+ public virtual IAsyncEnumerable> InvokeAsync(
ChatMessageContent message,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
+ AgentInvokeOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ return this.InvokeAsync(new[] { message }, thread, options, cancellationToken);
+ }
+
+ ///
+ /// Invoke the agent with the provided message and arguments.
+ ///
+ /// The messages to pass to the agent.
+ /// The conversation thread to continue with this invocation. If not provided, creates a new thread.
+ /// Optional parameters for agent invocation.
+ /// The to monitor for cancellation requests. The default is .
+ /// An async list of response items that each contain a and an .
+ ///
+ /// To continue this thread in the future, use an returned in one of the response items.
+ ///
+ public abstract IAsyncEnumerable> InvokeAsync(
+ ICollection messages,
+ AgentThread? thread = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default);
@@ -69,19 +85,35 @@ public abstract IAsyncEnumerable> InvokeAs
///
/// The message to pass to the agent.
/// The conversation thread to continue with this invocation. If not provided, creates a new thread.
- /// Optional arguments to pass to the agents's invocation, including any .
- /// The containing services, plugins, and other state for use by the agent.
/// Optional parameters for agent invocation.
/// The to monitor for cancellation requests. The default is .
/// An async list of response items that each contain a and an .
///
/// To continue this thread in the future, use an returned in one of the response items.
///
- public abstract IAsyncEnumerable> InvokeStreamingAsync(
+ public virtual IAsyncEnumerable> InvokeStreamingAsync(
ChatMessageContent message,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
+ AgentInvokeOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ return this.InvokeStreamingAsync(new[] { message }, thread, options, cancellationToken);
+ }
+
+ ///
+ /// Invoke the agent with the provided message and arguments.
+ ///
+ /// The messages to pass to the agent.
+ /// The conversation thread to continue with this invocation. If not provided, creates a new thread.
+ /// Optional parameters for agent invocation.
+ /// The to monitor for cancellation requests. The default is .
+ /// An async list of response items that each contain a and an .
+ ///
+ /// To continue this thread in the future, use an returned in one of the response items.
+ ///
+ public abstract IAsyncEnumerable> InvokeStreamingAsync(
+ ICollection messages,
+ AgentThread? thread = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default);
@@ -143,14 +175,14 @@ public abstract IAsyncEnumerable>
/// Ensures that the thread exists, is of the expected type, and is active, plus adds the provided message to the thread.
///
/// The expected type of the thead.
- /// The message to add to the thread once it is setup.
+ /// The messages to add to the thread once it is setup.
/// The thread to create if it's null, validate it's type if not null, and start if it is not active.
/// A callback to use to construct the thread if it's null.
/// The to monitor for cancellation requests. The default is .
/// An async task that completes once all update are complete.
///
protected async Task EnsureThreadExistsWithMessageAsync(
- ChatMessageContent message,
+ ICollection messages,
AgentThread? thread,
Func constructThread,
CancellationToken cancellationToken)
@@ -168,8 +200,11 @@ protected async Task EnsureThreadExistsWithMessageAsync
public class AgentInvokeOptions
{
+ ///
+ /// Gets or sets optional arguments to pass to the agent's invocation, including any
+ ///
+ public KernelArguments? KernelArguments { get; init; } = null;
+
+ ///
+ /// Gets or sets the containing services, plugins, and other state for use by the agent
+ ///
+ public Kernel? Kernel { get; init; } = null;
+
///
/// Gets or sets any instructions, in addition to those that were provided to the agent
/// initially, that need to be added to the prompt for this invocation only.
diff --git a/dotnet/src/Agents/Abstractions/AggregatorAgent.cs b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
index e71b7ff03f1a..d37cfd0590dc 100644
--- a/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
+++ b/dotnet/src/Agents/Abstractions/AggregatorAgent.cs
@@ -46,10 +46,8 @@ public sealed class AggregatorAgent(Func chatProvider) : Agent
///
public override IAsyncEnumerable> InvokeAsync(
- ChatMessageContent message,
+ ICollection messages,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
@@ -59,10 +57,8 @@ public override IAsyncEnumerable> InvokeAs
///
public override IAsyncEnumerable> InvokeStreamingAsync(
- ChatMessageContent message,
+ ICollection messages,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
index 0ee773037885..548fa1bea0f0 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
@@ -143,17 +143,15 @@ public IAsyncEnumerable InvokeAsync(
///
public override async IAsyncEnumerable> InvokeAsync(
- ChatMessageContent message,
+ ICollection messages,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
AgentInvokeOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- Verify.NotNull(message);
+ Verify.NotNull(messages);
var azureAIAgentThread = await this.EnsureThreadExistsWithMessageAsync(
- message,
+ messages,
thread,
() => new AzureAIAgentThread(this.Client),
cancellationToken).ConfigureAwait(false);
@@ -168,8 +166,8 @@ public override async IAsyncEnumerable> In
var invokeResults = this.InvokeAsync(
azureAIAgentThread.Id!,
internalOptions,
- this.MergeArguments(arguments),
- kernel ?? this.Kernel,
+ this.MergeArguments(options?.KernelArguments),
+ options?.Kernel ?? this.Kernel,
cancellationToken);
// Notify the thread of new messages and return them to the caller.
@@ -221,17 +219,15 @@ async IAsyncEnumerable InternalInvokeAsync()
///
public async override IAsyncEnumerable> InvokeStreamingAsync(
- ChatMessageContent message,
+ ICollection messages,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
AgentInvokeOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- Verify.NotNull(message);
+ Verify.NotNull(messages);
var azureAIAgentThread = await this.EnsureThreadExistsWithMessageAsync(
- message,
+ messages,
thread,
() => new AzureAIAgentThread(this.Client),
cancellationToken).ConfigureAwait(false);
@@ -247,8 +243,8 @@ public async override IAsyncEnumerable
/// Represents a conversation thread for an Azure AI agent.
///
-public class AzureAIAgentThread : AgentThread
+public sealed class AzureAIAgentThread : AgentThread
{
private readonly AgentsClient _client;
private string? _id = null;
+ private bool _isDeleted = false;
///
/// Initializes a new instance of the class.
@@ -48,6 +51,11 @@ public AzureAIAgentThread(AgentsClient client, string id)
///
public override async Task CreateAsync(CancellationToken cancellationToken = default)
{
+ if (this._isDeleted)
+ {
+ throw new InvalidOperationException("This thread has been deleted and cannot be recreated.");
+ }
+
if (this._id is not null)
{
return this._id;
@@ -62,21 +70,39 @@ public override async Task CreateAsync(CancellationToken cancellationTok
///
public override async Task DeleteAsync(CancellationToken cancellationToken = default)
{
+ if (this._isDeleted)
+ {
+ return;
+ }
+
if (this._id is null)
{
- throw new InvalidOperationException("This thread cannot be ended, since it has not been started.");
+ throw new InvalidOperationException("This thread cannot be deleted, since it has not been created.");
}
- await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false);
- this._id = null;
+ try
+ {
+ await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false);
+ }
+ catch (RequestFailedException ex) when (ex.Status == 404)
+ {
+ // Do nothing, since the thread was already deleted.
+ }
+
+ this._isDeleted = true;
}
///
public override async Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
{
+ if (this._isDeleted)
+ {
+ throw new InvalidOperationException("This thread has been deleted and cannot be used anymore.");
+ }
+
if (this._id is null)
{
- throw new InvalidOperationException("Messages cannot be added to this thread, since the thread has not been started.");
+ await this.CreateAsync(cancellationToken).ConfigureAwait(false);
}
// If the message was generated by this agent, it is already in the thread and we shouldn't add it again.
@@ -87,13 +113,21 @@ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, Canc
}
///
- public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default)
+ public async IAsyncEnumerable GetMessagesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
+ if (this._isDeleted)
+ {
+ throw new InvalidOperationException("This thread has been deleted and cannot be used anymore.");
+ }
+
if (this._id is null)
{
- throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread has not been started.");
+ await this.CreateAsync(cancellationToken).ConfigureAwait(false);
}
- return AgentThreadActions.GetMessagesAsync(this._client, this._id!, ListSortOrder.Ascending, cancellationToken);
+ await foreach (var message in AgentThreadActions.GetMessagesAsync(this._client, this._id!, ListSortOrder.Ascending, cancellationToken).ConfigureAwait(false))
+ {
+ yield return message;
+ }
}
}
diff --git a/dotnet/src/Agents/Bedrock/BedrockAgent.cs b/dotnet/src/Agents/Bedrock/BedrockAgent.cs
index d5cdf0e6255d..a3762f00bcb1 100644
--- a/dotnet/src/Agents/Bedrock/BedrockAgent.cs
+++ b/dotnet/src/Agents/Bedrock/BedrockAgent.cs
@@ -75,10 +75,8 @@ public static string CreateSessionId()
///
public override IAsyncEnumerable> InvokeAsync(
- ChatMessageContent message,
+ ICollection messages,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
@@ -88,10 +86,8 @@ public override IAsyncEnumerable> InvokeAs
///
public override IAsyncEnumerable> InvokeStreamingAsync(
- ChatMessageContent message,
+ ICollection messages,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
index ddbe0bb5ac5c..271770646632 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -59,17 +59,15 @@ public ChatCompletionAgent(
///
public override async IAsyncEnumerable> InvokeAsync(
- ChatMessageContent message,
+ ICollection messages,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
AgentInvokeOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- Verify.NotNull(message);
+ Verify.NotNull(messages);
var chatHistoryAgentThread = await this.EnsureThreadExistsWithMessageAsync(
- message,
+ messages,
thread,
() => new ChatHistoryAgentThread(),
cancellationToken).ConfigureAwait(false);
@@ -84,8 +82,8 @@ public override async IAsyncEnumerable> In
var invokeResults = this.InternalInvokeAsync(
agentName,
chatHistory,
- this.MergeArguments(arguments),
- kernel ?? this.Kernel,
+ this.MergeArguments(options?.KernelArguments),
+ options?.Kernel ?? this.Kernel,
options?.AdditionalInstructions,
cancellationToken);
@@ -114,17 +112,15 @@ public override IAsyncEnumerable InvokeAsync(
///
public override async IAsyncEnumerable> InvokeStreamingAsync(
- ChatMessageContent message,
+ ICollection messages,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
AgentInvokeOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- Verify.NotNull(message);
+ Verify.NotNull(messages);
var chatHistoryAgentThread = await this.EnsureThreadExistsWithMessageAsync(
- message,
+ messages,
thread,
() => new ChatHistoryAgentThread(),
cancellationToken).ConfigureAwait(false);
@@ -140,8 +136,8 @@ public override async IAsyncEnumerable chatHistoryAgentThread.OnNewMessageAsync(newMessage),
- this.MergeArguments(arguments),
- kernel ?? this.Kernel,
+ this.MergeArguments(options?.KernelArguments),
+ options?.Kernel ?? this.Kernel,
options?.AdditionalInstructions,
cancellationToken);
diff --git a/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs b/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
index cb13ead90e99..889cae3b1f92 100644
--- a/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
+++ b/dotnet/src/Agents/Core/ChatHistoryAgentThread.cs
@@ -2,7 +2,7 @@
using System;
using System.Collections.Generic;
-using System.Linq;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel.ChatCompletion;
@@ -12,10 +12,11 @@ namespace Microsoft.SemanticKernel.Agents;
///
/// Represents a conversation thread based on an instance of that is maanged inside this class.
///
-public class ChatHistoryAgentThread : AgentThread
+public sealed class ChatHistoryAgentThread : AgentThread
{
private readonly ChatHistory _chatHistory = new();
private string? _id = null;
+ private bool _isDeleted = false;
///
/// Initializes a new instance of the class.
@@ -42,6 +43,11 @@ public ChatHistoryAgentThread(ChatHistory chatHistory, string? id = null)
///
public override Task CreateAsync(CancellationToken cancellationToken = default)
{
+ if (this._isDeleted)
+ {
+ throw new InvalidOperationException("This thread has been deleted and cannot be recreated.");
+ }
+
if (this._id is not null)
{
return Task.FromResult(this._id);
@@ -55,37 +61,54 @@ public override Task CreateAsync(CancellationToken cancellationToken = d
///
public override Task DeleteAsync(CancellationToken cancellationToken = default)
{
+ if (this._isDeleted)
+ {
+ return Task.CompletedTask;
+ }
+
if (this._id is null)
{
- throw new InvalidOperationException("This thread cannot be ended, since it has not been started.");
+ throw new InvalidOperationException("This thread cannot be deleted, since it has not been created.");
}
this._chatHistory.Clear();
- this._id = null;
+ this._isDeleted = true;
return Task.CompletedTask;
}
///
- public override Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
+ public async override Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
{
+ if (this._isDeleted)
+ {
+ throw new InvalidOperationException("This thread has been deleted and cannot be used anymore.");
+ }
+
if (this._id is null)
{
- throw new InvalidOperationException("Messages cannot be added to this thread, since the thread has not been started.");
+ await this.CreateAsync(cancellationToken).ConfigureAwait(false);
}
this._chatHistory.Add(newMessage);
- return Task.CompletedTask;
}
///
- public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default)
+ public async IAsyncEnumerable GetMessagesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
+ if (this._isDeleted)
+ {
+ throw new InvalidOperationException("This thread has been deleted and cannot be used anymore.");
+ }
+
if (this._id is null)
{
- throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread has not been started.");
+ await this.CreateAsync(cancellationToken).ConfigureAwait(false);
}
- return this._chatHistory.ToAsyncEnumerable();
+ foreach (var message in this._chatHistory)
+ {
+ yield return message;
+ }
}
}
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
index e16d130d6e46..48aba8edf415 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
@@ -362,17 +362,15 @@ public async Task DeleteAsync(CancellationToken cancellationToken = defaul
///
public override async IAsyncEnumerable> InvokeAsync(
- ChatMessageContent message,
+ ICollection messages,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
AgentInvokeOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- Verify.NotNull(message);
+ Verify.NotNull(messages);
var openAIAssistantAgentThread = await this.EnsureThreadExistsWithMessageAsync(
- message,
+ messages,
thread,
() => new OpenAIAssistantAgentThread(this.Client),
cancellationToken).ConfigureAwait(false);
@@ -387,8 +385,8 @@ public override async IAsyncEnumerable> In
var invokeResults = this.InvokeAsync(
openAIAssistantAgentThread.Id!,
internalOptions,
- this.MergeArguments(arguments),
- kernel ?? this.Kernel,
+ this.MergeArguments(options?.KernelArguments),
+ options?.Kernel ?? this.Kernel,
cancellationToken);
// Notify the thread of new messages and return them to the caller.
@@ -458,17 +456,15 @@ async IAsyncEnumerable InternalInvokeAsync()
///
public async override IAsyncEnumerable> InvokeStreamingAsync(
- ChatMessageContent message,
+ ICollection messages,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
AgentInvokeOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- Verify.NotNull(message);
+ Verify.NotNull(messages);
var openAIAssistantAgentThread = await this.EnsureThreadExistsWithMessageAsync(
- message,
+ messages,
thread,
() => new OpenAIAssistantAgentThread(this.Client),
cancellationToken).ConfigureAwait(false);
@@ -484,8 +480,8 @@ public async override IAsyncEnumerable
/// Represents a conversation thread for an Open AI Assistant agent.
///
-public class OpenAIAssistantAgentThread : AgentThread
+public sealed class OpenAIAssistantAgentThread : AgentThread
{
private readonly AssistantClient _client;
private string? _id = null;
+ private bool _isDeleted = false;
///
/// Initializes a new instance of the class.
@@ -48,6 +50,11 @@ public OpenAIAssistantAgentThread(AssistantClient client, string id)
///
public override async Task CreateAsync(CancellationToken cancellationToken = default)
{
+ if (this._isDeleted)
+ {
+ throw new InvalidOperationException("This thread has been deleted and cannot be recreated.");
+ }
+
if (this._id is not null)
{
return this._id;
@@ -62,21 +69,32 @@ public override async Task CreateAsync(CancellationToken cancellationTok
///
public override async Task DeleteAsync(CancellationToken cancellationToken = default)
{
+ if (this._isDeleted)
+ {
+ return;
+ }
+
if (this._id is null)
{
- throw new InvalidOperationException("This thread cannot be ended, since it has not been started.");
+ throw new InvalidOperationException("This thread cannot be deleted, since it has not been created.");
}
await this._client.DeleteThreadAsync(this._id, cancellationToken).ConfigureAwait(false);
- this._id = null;
+
+ this._isDeleted = true;
}
///
public override async Task OnNewMessageAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
{
+ if (this._isDeleted)
+ {
+ throw new InvalidOperationException("This thread has been deleted and cannot be used anymore.");
+ }
+
if (this._id is null)
{
- throw new InvalidOperationException("Messages cannot be added to this thread, since the thread has not been started.");
+ await this.CreateAsync(cancellationToken).ConfigureAwait(false);
}
// If the message was generated by this agent, it is already in the thread and we shouldn't add it again.
@@ -87,13 +105,21 @@ public override async Task OnNewMessageAsync(ChatMessageContent newMessage, Canc
}
///
- public IAsyncEnumerable GetMessagesAsync(CancellationToken cancellationToken = default)
+ public async IAsyncEnumerable GetMessagesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
+ if (this._isDeleted)
+ {
+ throw new InvalidOperationException("This thread has been deleted and cannot be used anymore.");
+ }
+
if (this._id is null)
{
- throw new InvalidOperationException("The messages for this thread cannot be retrieved, since the thread has not been started.");
+ await this.CreateAsync(cancellationToken).ConfigureAwait(false);
}
- return AssistantThreadActions.GetMessagesAsync(this._client, this._id!, MessageCollectionOrder.Ascending, cancellationToken);
+ await foreach (var message in AssistantThreadActions.GetMessagesAsync(this._client, this._id!, MessageCollectionOrder.Ascending, cancellationToken).ConfigureAwait(false))
+ {
+ yield return message;
+ }
}
}
diff --git a/dotnet/src/Agents/UnitTests/MockAgent.cs b/dotnet/src/Agents/UnitTests/MockAgent.cs
index b88ba37a3aa1..72bb3ed30509 100644
--- a/dotnet/src/Agents/UnitTests/MockAgent.cs
+++ b/dotnet/src/Agents/UnitTests/MockAgent.cs
@@ -20,10 +20,8 @@ internal sealed class MockAgent : ChatHistoryKernelAgent
public IReadOnlyList Response { get; set; } = [];
public override IAsyncEnumerable> InvokeAsync(
- ChatMessageContent message,
+ ICollection messages,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
@@ -44,10 +42,8 @@ public override IAsyncEnumerable InvokeAsync(
///
public override IAsyncEnumerable> InvokeStreamingAsync(
- ChatMessageContent message,
+ ICollection messages,
AgentThread? thread = null,
- KernelArguments? arguments = null,
- Kernel? kernel = null,
AgentInvokeOptions? options = null,
CancellationToken cancellationToken = default)
{
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/AgentThreadTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/AgentThreadTests.cs
new file mode 100644
index 000000000000..1b71751eb35e
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/AgentThreadTests.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Xunit;
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AgentThreadConformance;
+
+public abstract class AgentThreadTests(Func createAgentFixture) : IAsyncLifetime
+{
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+ private AgentFixture _agentFixture;
+#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+
+ protected AgentFixture Fixture => this._agentFixture;
+
+ [Fact]
+ public async Task DeletingThreadTwiceDoesNotThrowAsync()
+ {
+ await this.Fixture.AgentThread.CreateAsync();
+
+ await this.Fixture.AgentThread.DeleteAsync();
+ await this.Fixture.AgentThread.DeleteAsync();
+ }
+
+ [Fact]
+ public async Task UsingThreadAfterDeleteThrowsAsync()
+ {
+ await this.Fixture.AgentThread.CreateAsync();
+ await this.Fixture.AgentThread.DeleteAsync();
+
+ await Assert.ThrowsAsync(async () => await this.Fixture.AgentThread.CreateAsync());
+ await Assert.ThrowsAsync(async () => await this.Fixture.AgentThread.OnNewMessageAsync(new ChatMessageContent()));
+ }
+
+ [Fact]
+ public async Task DeleteThreadBeforeCreateThrowsAsync()
+ {
+ await Assert.ThrowsAsync(async () => await this.Fixture.AgentThread.DeleteAsync());
+ }
+
+ [Fact]
+ public async Task UsingThreadbeforeCreateCreatesAsync()
+ {
+ await this.Fixture.AgentThread.OnNewMessageAsync(new ChatMessageContent());
+ Assert.NotNull(this.Fixture.AgentThread.Id);
+ }
+
+ public Task InitializeAsync()
+ {
+ this._agentFixture = createAgentFixture();
+ return this._agentFixture.InitializeAsync();
+ }
+
+ public Task DisposeAsync()
+ {
+ return this._agentFixture.DisposeAsync();
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/AzureAIAgentThreadTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/AzureAIAgentThreadTests.cs
new file mode 100644
index 000000000000..db2c20495580
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/AzureAIAgentThreadTests.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AgentThreadConformance;
+
+public class AzureAIAgentThreadTests() : AgentThreadTests(() => new AzureAIAgentFixture())
+{
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/ChatCompletionAgentThreadTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/ChatCompletionAgentThreadTests.cs
new file mode 100644
index 000000000000..6c1b29269a98
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/ChatCompletionAgentThreadTests.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AgentThreadConformance;
+
+public class ChatCompletionAgentThreadTests() : AgentThreadTests(() => new ChatCompletionAgentFixture())
+{
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/OpenAIAssistantAgentThreadTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/OpenAIAssistantAgentThreadTests.cs
new file mode 100644
index 000000000000..63aadf8aa54c
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentThreadConformance/OpenAIAssistantAgentThreadTests.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AgentThreadConformance;
+
+public class OpenAIAssistantAgentThreadTests() : AgentThreadTests(() => new OpenAIAssistantAgentFixture())
+{
+}
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs
index e944d506af2a..9d61ff73e866 100644
--- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
+using Azure;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
@@ -49,7 +50,13 @@ public override async Task DisposeAsync()
{
if (this._thread!.Id is not null)
{
- await this._agentsClient!.DeleteThreadAsync(this._thread!.Id);
+ try
+ {
+ await this._agentsClient!.DeleteThreadAsync(this._thread!.Id);
+ }
+ catch (RequestFailedException ex) when (ex.Status == 404)
+ {
+ }
}
await this._agentsClient!.DeleteAgentAsync(this._aiAgent!.Id);
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/InvokeTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/InvokeTests.cs
index 1cee519982b4..3b1afcd796b9 100644
--- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/InvokeTests.cs
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/InvokeTests.cs
@@ -11,7 +11,7 @@
namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance;
///
-/// Base test class for testing the method of agents.
+/// Base test class for testing the method of agents.
/// Each agent type should have its own derived class.
///
public abstract class InvokeTests(Func createAgentFixture) : IAsyncLifetime
diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs
index c62d913a4b0f..9fdce790f7f5 100644
--- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs
+++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.ClientModel;
using System.Threading.Tasks;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
@@ -48,7 +49,13 @@ public override async Task DisposeAsync()
{
if (this._thread!.Id is not null)
{
- await this._assistantClient!.DeleteThreadAsync(this._thread!.Id);
+ try
+ {
+ await this._assistantClient!.DeleteThreadAsync(this._thread!.Id);
+ }
+ catch (ClientResultException ex) when (ex.Status == 404)
+ {
+ }
}
await this._assistantClient!.DeleteAssistantAsync(this._assistant!.Id);