From eaee471878de646d44cae6645a864658a655b07b Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Tue, 24 Jun 2025 20:53:05 -0700
Subject: [PATCH 01/12] Added PersistentAgentsChatClient implementation
---
eng/Packages.Data.props | 1 +
.../src/Azure.AI.Agents.Persistent.csproj | 1 +
.../src/Custom/PersistentAgentsChatClient.cs | 433 ++++++++++++++++++
.../PersistentAgentsClientExtensions.cs | 29 ++
4 files changed, 464 insertions(+)
create mode 100644 sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs
create mode 100644 sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsClientExtensions.cs
diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props
index 4704e8b2023b..6c63a6f06003 100644
--- a/eng/Packages.Data.props
+++ b/eng/Packages.Data.props
@@ -103,6 +103,7 @@
+
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/src/Azure.AI.Agents.Persistent.csproj b/sdk/ai/Azure.AI.Agents.Persistent/src/Azure.AI.Agents.Persistent.csproj
index f4ca45b7112b..b59d74f0e552 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/src/Azure.AI.Agents.Persistent.csproj
+++ b/sdk/ai/Azure.AI.Agents.Persistent/src/Azure.AI.Agents.Persistent.csproj
@@ -16,6 +16,7 @@
+
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs
new file mode 100644
index 000000000000..77cb1d085d96
--- /dev/null
+++ b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs
@@ -0,0 +1,433 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#nullable enable
+#pragma warning disable AZC0003, AZC0004, AZC0007, AZC0015
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+
+namespace Azure.AI.Agents.Persistent
+{
+ /// Represents an for an Azure.AI.Agents.Persistent .
+ public partial class PersistentAgentsChatClient : IChatClient
+ {
+ /// The name of the chat client provider.
+ private const string ProviderName = "azure";
+
+ /// The underlying .
+ private readonly PersistentAgentsClient? _client;
+
+ /// Metadata for the client.
+ private readonly ChatClientMetadata? _metadata;
+
+ /// The ID of the agent to use.
+ private readonly string? _agentId;
+
+ /// The thread ID to use if none is supplied in .
+ private readonly string? _threadId;
+
+ /// Initializes a new instance of the class for the specified .
+ public PersistentAgentsChatClient(PersistentAgentsClient client, string agentId, string? threadId)
+ {
+ Argument.AssertNotNull(client, nameof(client));
+ Argument.AssertNotNullOrWhiteSpace(agentId, nameof(agentId));
+
+ _client = client;
+ _agentId = agentId;
+ _threadId = threadId;
+
+ _metadata = new(ProviderName);
+ }
+
+ protected PersistentAgentsChatClient() { }
+
+ ///
+ public object? GetService(Type serviceType, object? serviceKey = null) =>
+ serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) :
+ serviceKey is not null ? null :
+ serviceType == typeof(ChatClientMetadata) ? _metadata :
+ serviceType == typeof(PersistentAgentsClient) ? _client :
+ serviceType.IsInstanceOfType(this) ? this :
+ null;
+
+ ///
+ public Task GetResponseAsync(
+ IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) =>
+ GetStreamingResponseAsync(messages, options, cancellationToken).ToChatResponseAsync(cancellationToken);
+
+ ///
+ public async IAsyncEnumerable GetStreamingResponseAsync(
+ IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Argument.AssertNotNull(messages, nameof(messages));
+
+ // Extract necessary state from messages and options.
+ (ThreadAndRunOptions runOptions, List? toolResults) = CreateRunOptions(messages, options);
+
+ // Get the thread ID.
+ string? threadId = options?.ConversationId ?? _threadId;
+ if (threadId is null && toolResults is not null)
+ {
+ throw new ArgumentException("No thread ID was provided, but chat messages includes tool results.", nameof(messages));
+ }
+
+ // Get any active run ID for this thread.
+ ThreadRun? threadRun = null;
+ if (threadId is not null)
+ {
+ await foreach (ThreadRun? run in _client!.Runs.GetRunsAsync(threadId, limit: 1, ListSortOrder.Descending, cancellationToken: cancellationToken).ConfigureAwait(false))
+ {
+ if (run.Status != RunStatus.Completed && run.Status != RunStatus.Cancelled && run.Status != RunStatus.Failed && run.Status != RunStatus.Expired)
+ {
+ threadRun = run;
+ break;
+ }
+ }
+ }
+
+ // Submit the request.
+ IAsyncEnumerable updates;
+ if (threadRun is not null &&
+ ConvertFunctionResultsToToolOutput(toolResults, out List? toolOutputs) is { } toolRunId &&
+ toolRunId == threadRun.Id)
+ {
+ // There's an active run and we have tool results to submit, so submit the results and continue streaming.
+ // This is going to ignore any additional messages in the run options, as we are only submitting tool outputs,
+ // but there doesn't appear to be a way to submit additional messages, and having such additional messages is rare.
+ updates = _client!.Runs.SubmitToolOutputsToStreamAsync(threadRun, toolOutputs, cancellationToken);
+ }
+ else
+ {
+ if (threadId is null)
+ {
+ // No thread ID was provided, so create a new thread.
+ PersistentAgentThread thread = await _client!.Threads.CreateThreadAsync(runOptions.ThreadOptions.Messages, runOptions.ToolResources, runOptions.Metadata, cancellationToken).ConfigureAwait(false);
+ runOptions.ThreadOptions.Messages.Clear();
+ threadId = thread.Id;
+ }
+ else if (threadRun is not null)
+ {
+ // There was an active run; we need to cancel it before starting a new run.
+ await _client!.Runs.CancelRunAsync(threadId, threadRun.Id, cancellationToken).ConfigureAwait(false);
+ threadRun = null;
+ }
+
+ // Now create a new run and stream the results.
+ updates = _client!.Runs.CreateRunStreamingAsync(
+ threadId: threadId,
+ agentId: _agentId,
+ overrideModelName: runOptions?.OverrideModelName,
+ overrideInstructions: runOptions?.OverrideInstructions,
+ additionalInstructions: null,
+ additionalMessages: runOptions?.ThreadOptions.Messages,
+ overrideTools: runOptions?.OverrideTools,
+ temperature: runOptions?.Temperature,
+ topP: runOptions?.TopP,
+ maxPromptTokens: runOptions?.MaxPromptTokens,
+ maxCompletionTokens: runOptions?.MaxCompletionTokens,
+ truncationStrategy: runOptions?.TruncationStrategy,
+ toolChoice: runOptions?.ToolChoice,
+ responseFormat: runOptions?.ResponseFormat,
+ parallelToolCalls: runOptions?.ParallelToolCalls,
+ metadata: runOptions?.Metadata,
+ cancellationToken);
+ }
+
+ // Process each update.
+ string? responseId = null;
+ await foreach (StreamingUpdate? update in updates.ConfigureAwait(false))
+ {
+ switch (update)
+ {
+ case ThreadUpdate tu:
+ threadId ??= tu.Value.Id;
+ goto default;
+
+ case RunUpdate ru:
+ threadId ??= ru.Value.ThreadId;
+ responseId ??= ru.Value.Id;
+
+ ChatResponseUpdate ruUpdate = new()
+ {
+ AuthorName = ru.Value.AssistantId,
+ ConversationId = threadId,
+ CreatedAt = ru.Value.CreatedAt,
+ MessageId = responseId,
+ ModelId = ru.Value.Model,
+ RawRepresentation = ru,
+ ResponseId = responseId,
+ Role = ChatRole.Assistant,
+ };
+
+ if (ru.Value.Usage is { } usage)
+ {
+ ruUpdate.Contents.Add(new UsageContent(new()
+ {
+ InputTokenCount = usage.PromptTokens,
+ OutputTokenCount = usage.CompletionTokens,
+ TotalTokenCount = usage.TotalTokens,
+ }));
+ }
+
+ if (ru is RequiredActionUpdate rau && rau.ToolCallId is string toolCallId && rau.FunctionName is string functionName)
+ {
+ ruUpdate.Contents.Add(
+ new FunctionCallContent(
+ JsonSerializer.Serialize([ru.Value.Id, toolCallId], AgentsChatClientJsonContext.Default.StringArray),
+ functionName,
+ JsonSerializer.Deserialize(rau.FunctionArguments, AgentsChatClientJsonContext.Default.IDictionaryStringObject)!));
+ }
+
+ yield return ruUpdate;
+ break;
+
+ case MessageContentUpdate mcu:
+ yield return new(mcu.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant, mcu.Text)
+ {
+ ConversationId = threadId,
+ MessageId = responseId,
+ RawRepresentation = mcu,
+ ResponseId = responseId,
+ };
+ break;
+
+ default:
+ yield return new ChatResponseUpdate
+ {
+ ConversationId = threadId,
+ MessageId = responseId,
+ RawRepresentation = update,
+ ResponseId = responseId,
+ Role = ChatRole.Assistant,
+ };
+ break;
+ }
+ }
+ }
+
+ ///
+ public void Dispose() { }
+
+ ///
+ /// Creates the to use for the request and extracts any function result contents
+ /// that need to be submitted as tool results.
+ ///
+ private (ThreadAndRunOptions RunOptions, List? ToolResults) CreateRunOptions(
+ IEnumerable messages, ChatOptions? options)
+ {
+ // Create the options instance to populate, either a fresh or using one the caller provides.
+ ThreadAndRunOptions runOptions =
+ options?.RawRepresentationFactory?.Invoke(this) as ThreadAndRunOptions ??
+ new();
+
+ // Populate the run options from the ChatOptions, if provided.
+ if (options is not null)
+ {
+ runOptions.MaxCompletionTokens ??= options.MaxOutputTokens;
+ runOptions.OverrideModelName ??= options.ModelId;
+ runOptions.TopP ??= options.TopP;
+ runOptions.Temperature ??= options.Temperature;
+ runOptions.ParallelToolCalls ??= options.AllowMultipleToolCalls;
+ // Ignored: options.TopK, options.FrequencyPenalty, options.Seed, options.StopSequences
+
+ if (options.Tools is { Count: > 0 } tools)
+ {
+ // If the caller has provided any tool overrides, we'll assume they don't want to use the agent's tools.
+ // But if they haven't, the only way we can provide our tools is via an override, whereas we'd really like to
+ // just add them. To handle that, we'll get all of the agent's tools and add them to the override list
+ // along with our tools.
+ IList toolDefinitions = runOptions.OverrideTools is not null ? [.. runOptions.OverrideTools] : [];
+
+ // TODO: When moved to Azure.AI.Agents.Persistent, merge agent tools with override tools, in similar way like here:
+ // https://github.com/dotnet/extensions/blob/694b95ef75c6bd9de00ef761dadae4e70ee8739f/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs#L263-L279
+
+ // The caller can provide tools in the supplied ThreadAndRunOptions. Augment it with any supplied via ChatOptions.Tools.
+ foreach (AITool tool in tools)
+ {
+ switch (tool)
+ {
+ case AIFunction aiFunction:
+ toolDefinitions.Add(new FunctionToolDefinition(
+ aiFunction.Name,
+ aiFunction.Description,
+ BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(aiFunction.JsonSchema, AgentsChatClientJsonContext.Default.JsonElement))));
+ break;
+
+ case HostedCodeInterpreterTool:
+ toolDefinitions.Add(new CodeInterpreterToolDefinition());
+ break;
+
+ case HostedWebSearchTool webSearch when webSearch.AdditionalProperties?.TryGetValue("connectionId", out object? connectionId) is true:
+ toolDefinitions.Add(new BingGroundingToolDefinition(new BingGroundingSearchToolParameters([new BingGroundingSearchConfiguration(connectionId!.ToString())])));
+ break;
+ }
+ }
+
+ if (toolDefinitions.Count > 0)
+ {
+ runOptions.OverrideTools = toolDefinitions;
+ }
+ }
+
+ // Store the tool mode, if relevant.
+ if (runOptions.ToolChoice is null)
+ {
+ switch (options.ToolMode)
+ {
+ case NoneChatToolMode:
+ runOptions.ToolChoice = BinaryData.FromString("none");
+ break;
+
+ case RequiredChatToolMode required:
+ runOptions.ToolChoice = required.RequiredFunctionName is string functionName ?
+ BinaryData.FromString($$"""{"type": "function", "function": {"name": "{{functionName}}"} }""") :
+ BinaryData.FromString("required");
+ break;
+ }
+ }
+
+ // Store the response format, if relevant.
+ if (runOptions.ResponseFormat is null)
+ {
+ if (options.ResponseFormat is ChatResponseFormatJson jsonFormat)
+ {
+ runOptions.ResponseFormat = jsonFormat.Schema is { } schema ?
+ BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(new()
+ {
+ ["type"] = "json_schema",
+ ["json_schema"] = JsonSerializer.SerializeToNode(schema, AgentsChatClientJsonContext.Default.JsonNode),
+ }, AgentsChatClientJsonContext.Default.JsonObject)) :
+ BinaryData.FromString("""{ "type": "json_object" }""");
+ }
+ }
+ }
+
+ // Process ChatMessages. System messages are turned into additional instructions.
+ // All other messages are added 1:1, treating assistant messages as agent messages
+ // and everything else as user messages.
+ StringBuilder? instructions = null;
+ List? functionResults = null;
+
+ runOptions.ThreadOptions ??= new();
+
+ foreach (ChatMessage chatMessage in messages)
+ {
+ List messageContents = [];
+
+ if (chatMessage.Role == ChatRole.System ||
+ chatMessage.Role == new ChatRole("developer"))
+ {
+ instructions ??= new();
+ foreach (TextContent textContent in chatMessage.Contents.OfType())
+ {
+ _ = instructions.Append(textContent);
+ }
+
+ continue;
+ }
+
+ foreach (AIContent content in chatMessage.Contents)
+ {
+ switch (content)
+ {
+ case TextContent text:
+ messageContents.Add(new MessageInputTextBlock(text.Text));
+ break;
+
+ case DataContent image when image.HasTopLevelMediaType("image"):
+ messageContents.Add(new MessageInputImageUriBlock(new MessageImageUriParam(image.Uri)));
+ break;
+
+ case UriContent image when image.HasTopLevelMediaType("image"):
+ messageContents.Add(new MessageInputImageUriBlock(new MessageImageUriParam(image.Uri.AbsoluteUri)));
+ break;
+
+ case FunctionResultContent result:
+ (functionResults ??= []).Add(result);
+ break;
+
+ default:
+ if (content.RawRepresentation is MessageInputContentBlock rawContent)
+ {
+ messageContents.Add(rawContent);
+ }
+ break;
+ }
+ }
+
+ if (messageContents.Count > 0)
+ {
+ runOptions.ThreadOptions.Messages.Add(new ThreadMessageOptions(
+ chatMessage.Role == ChatRole.Assistant ? MessageRole.Agent : MessageRole.User,
+ messageContents));
+ }
+ }
+
+ if (instructions is not null)
+ {
+ runOptions.OverrideInstructions = instructions.ToString();
+ }
+
+ return (runOptions, functionResults);
+ }
+
+ /// Convert instances to instances.
+ /// The tool results to process.
+ /// The generated list of tool outputs, if any could be created.
+ /// The run ID associated with the corresponding function call requests.
+ private static string? ConvertFunctionResultsToToolOutput(List? toolResults, out List? toolOutputs)
+ {
+ string? runId = null;
+ toolOutputs = null;
+ if (toolResults?.Count > 0)
+ {
+ foreach (FunctionResultContent frc in toolResults)
+ {
+ // When creating the FunctionCallContext, we created it with a CallId == [runId, callId].
+ // We need to extract the run ID and ensure that the ToolOutput we send back to Azure
+ // is only the call ID.
+ string[]? runAndCallIDs;
+ try
+ {
+ runAndCallIDs = JsonSerializer.Deserialize(frc.CallId, AgentsChatClientJsonContext.Default.StringArray);
+ }
+ catch
+ {
+ continue;
+ }
+
+ if (runAndCallIDs is null ||
+ runAndCallIDs.Length != 2 ||
+ string.IsNullOrWhiteSpace(runAndCallIDs[0]) || // run ID
+ string.IsNullOrWhiteSpace(runAndCallIDs[1]) || // call ID
+ (runId is not null && runId != runAndCallIDs[0]))
+ {
+ continue;
+ }
+
+ runId = runAndCallIDs[0];
+ (toolOutputs ??= []).Add(new(runAndCallIDs[1], frc.Result?.ToString() ?? string.Empty));
+ }
+ }
+
+ return runId;
+ }
+
+ [JsonSerializable(typeof(JsonElement))]
+ [JsonSerializable(typeof(JsonNode))]
+ [JsonSerializable(typeof(JsonObject))]
+ [JsonSerializable(typeof(string[]))]
+ [JsonSerializable(typeof(IDictionary))]
+ private sealed partial class AgentsChatClientJsonContext : JsonSerializerContext;
+ }
+}
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsClientExtensions.cs b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsClientExtensions.cs
new file mode 100644
index 000000000000..aaa1a4ae45cd
--- /dev/null
+++ b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsClientExtensions.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#nullable enable
+
+using Microsoft.Extensions.AI;
+
+namespace Azure.AI.Agents.Persistent
+{
+ ///
+ /// Provides extension methods for .
+ ///
+ public static class PersistentAgentsClientExtensions
+ {
+ ///
+ /// Creates an for a client for interacting with a specific agent.
+ ///
+ /// The instance to be accessed as an .
+ /// The unique identifier of the agent with which to interact.
+ ///
+ /// An optional existing thread identifier for the chat session. This serves as a default, and may be overridden per call to
+ /// or via the
+ /// property. If not thread ID is provided via either mechanism, a new thread will be created for the request.
+ ///
+ /// An instance configured to interact with the specified agent and thread.
+ public static IChatClient AsIChatClient(this PersistentAgentsClient client, string agentId, string? threadId = null) =>
+ new PersistentAgentsChatClient(client, agentId, threadId);
+ }
+}
From 893a3df73ca9d45859f4e6ecfdb5ccfc2bf9e29d Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Wed, 25 Jun 2025 18:49:59 -0700
Subject: [PATCH 02/12] Added tests
---
.../src/Custom/PersistentAgentsChatClient.cs | 48 ++-
.../PersistentAgentsClientExtensions.cs | 6 +-
.../tests/PersistentAgentsChatClientTests.cs | 351 ++++++++++++++++++
3 files changed, 387 insertions(+), 18 deletions(-)
create mode 100644 sdk/ai/Azure.AI.Agents.Persistent/tests/PersistentAgentsChatClientTests.cs
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs
index 77cb1d085d96..5892e7ab753e 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs
+++ b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs
@@ -2,7 +2,7 @@
// Licensed under the MIT License.
#nullable enable
-#pragma warning disable AZC0003, AZC0004, AZC0007, AZC0015
+#pragma warning disable AZC0004, AZC0007, AZC0015
using System;
using System.Collections.Generic;
@@ -34,17 +34,20 @@ public partial class PersistentAgentsChatClient : IChatClient
private readonly string? _agentId;
/// The thread ID to use if none is supplied in .
- private readonly string? _threadId;
+ private readonly string? _defaultThreadId;
+
+ /// List of tools associated with the agent.
+ private IReadOnlyList? _agentTools;
/// Initializes a new instance of the class for the specified .
- public PersistentAgentsChatClient(PersistentAgentsClient client, string agentId, string? threadId)
+ public PersistentAgentsChatClient(PersistentAgentsClient client, string agentId, string? defaultThreadId = null)
{
Argument.AssertNotNull(client, nameof(client));
Argument.AssertNotNullOrWhiteSpace(agentId, nameof(agentId));
_client = client;
_agentId = agentId;
- _threadId = threadId;
+ _defaultThreadId = defaultThreadId;
_metadata = new(ProviderName);
}
@@ -52,7 +55,7 @@ public PersistentAgentsChatClient(PersistentAgentsClient client, string agentId,
protected PersistentAgentsChatClient() { }
///
- public object? GetService(Type serviceType, object? serviceKey = null) =>
+ public virtual object? GetService(Type serviceType, object? serviceKey = null) =>
serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) :
serviceKey is not null ? null :
serviceType == typeof(ChatClientMetadata) ? _metadata :
@@ -61,21 +64,22 @@ protected PersistentAgentsChatClient() { }
null;
///
- public Task GetResponseAsync(
+ public virtual Task GetResponseAsync(
IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) =>
GetStreamingResponseAsync(messages, options, cancellationToken).ToChatResponseAsync(cancellationToken);
///
- public async IAsyncEnumerable GetStreamingResponseAsync(
+ public virtual async IAsyncEnumerable GetStreamingResponseAsync(
IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
Argument.AssertNotNull(messages, nameof(messages));
// Extract necessary state from messages and options.
- (ThreadAndRunOptions runOptions, List? toolResults) = CreateRunOptions(messages, options);
+ (ThreadAndRunOptions runOptions, List? toolResults) =
+ await CreateRunOptionsAsync(messages, options, cancellationToken).ConfigureAwait(false);
// Get the thread ID.
- string? threadId = options?.ConversationId ?? _threadId;
+ string? threadId = options?.ConversationId ?? _defaultThreadId;
if (threadId is null && toolResults is not null)
{
throw new ArgumentException("No thread ID was provided, but chat messages includes tool results.", nameof(messages));
@@ -222,8 +226,8 @@ public void Dispose() { }
/// Creates the to use for the request and extracts any function result contents
/// that need to be submitted as tool results.
///
- private (ThreadAndRunOptions RunOptions, List? ToolResults) CreateRunOptions(
- IEnumerable messages, ChatOptions? options)
+ private async ValueTask<(ThreadAndRunOptions RunOptions, List? ToolResults)> CreateRunOptionsAsync(
+ IEnumerable messages, ChatOptions? options, CancellationToken cancellationToken)
{
// Create the options instance to populate, either a fresh or using one the caller provides.
ThreadAndRunOptions runOptions =
@@ -242,16 +246,30 @@ public void Dispose() { }
if (options.Tools is { Count: > 0 } tools)
{
+ List toolDefinitions = [];
+
// If the caller has provided any tool overrides, we'll assume they don't want to use the agent's tools.
// But if they haven't, the only way we can provide our tools is via an override, whereas we'd really like to
// just add them. To handle that, we'll get all of the agent's tools and add them to the override list
// along with our tools.
- IList toolDefinitions = runOptions.OverrideTools is not null ? [.. runOptions.OverrideTools] : [];
+ if (runOptions.OverrideTools is null || !runOptions.OverrideTools.Any())
+ {
+ if (_agentTools is null)
+ {
+ PersistentAgent agent = await _client!.Administration.GetAgentAsync(_agentId, cancellationToken).ConfigureAwait(false);
+ _agentTools = agent.Tools;
+ }
- // TODO: When moved to Azure.AI.Agents.Persistent, merge agent tools with override tools, in similar way like here:
- // https://github.com/dotnet/extensions/blob/694b95ef75c6bd9de00ef761dadae4e70ee8739f/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs#L263-L279
+ toolDefinitions.AddRange(_agentTools);
+ }
+
+ // The caller can provide tools in the supplied ThreadAndRunOptions.
+ if (runOptions.OverrideTools is not null)
+ {
+ toolDefinitions.AddRange(runOptions.OverrideTools);
+ }
- // The caller can provide tools in the supplied ThreadAndRunOptions. Augment it with any supplied via ChatOptions.Tools.
+ // Now add the tools from ChatOptions.Tools.
foreach (AITool tool in tools)
{
switch (tool)
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsClientExtensions.cs b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsClientExtensions.cs
index aaa1a4ae45cd..af1fd5c899ba 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsClientExtensions.cs
+++ b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsClientExtensions.cs
@@ -17,13 +17,13 @@ public static class PersistentAgentsClientExtensions
///
/// The instance to be accessed as an .
/// The unique identifier of the agent with which to interact.
- ///
+ ///
/// An optional existing thread identifier for the chat session. This serves as a default, and may be overridden per call to
/// or via the
/// property. If not thread ID is provided via either mechanism, a new thread will be created for the request.
///
/// An instance configured to interact with the specified agent and thread.
- public static IChatClient AsIChatClient(this PersistentAgentsClient client, string agentId, string? threadId = null) =>
- new PersistentAgentsChatClient(client, agentId, threadId);
+ public static IChatClient AsIChatClient(this PersistentAgentsClient client, string agentId, string? defaultThreadId = null) =>
+ new PersistentAgentsChatClient(client, agentId, defaultThreadId);
}
}
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/tests/PersistentAgentsChatClientTests.cs b/sdk/ai/Azure.AI.Agents.Persistent/tests/PersistentAgentsChatClientTests.cs
new file mode 100644
index 000000000000..eae805a412df
--- /dev/null
+++ b/sdk/ai/Azure.AI.Agents.Persistent/tests/PersistentAgentsChatClientTests.cs
@@ -0,0 +1,351 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Azure.Core.TestFramework;
+using Azure.Identity;
+using Microsoft.Extensions.AI;
+using NUnit.Framework;
+
+namespace Azure.AI.Agents.Persistent.Tests
+{
+ public class PersistentAgentsChatClientTests : RecordedTestBase
+ {
+ private const string AGENT_NAME = "cs_e2e_tests_chat_client";
+ private const string STREAMING_CONSTRAINT = "The test framework does not support iteration of stream in Sync mode.";
+
+ private string _agentId;
+ private string _threadId;
+
+ public PersistentAgentsChatClientTests(bool isAsync) : base(isAsync)
+ {
+ TestDiagnostics = false;
+ }
+
+ #region Enumerations
+ public enum ChatOptionsTestType
+ {
+ Default,
+ WithTools,
+ WithResponseFormat
+ }
+ #endregion
+
+ [SetUp]
+ public async Task Setup()
+ {
+ using IDisposable _ = SetTestSwitch();
+ PersistentAgentsClient client = GetClient();
+ PersistentAgent agent = await client.Administration.CreateAgentAsync(
+ model: "gpt-4.1",
+ name: AGENT_NAME,
+ instructions: "You are a helpful chat agent."
+ );
+
+ _agentId = agent.Id;
+
+ PersistentAgentThread thread = await client.Threads.CreateThreadAsync();
+
+ _threadId = thread.Id;
+ }
+
+ [RecordedTest]
+ public async Task TestGetResponseAsync()
+ {
+ using IDisposable _ = SetTestSwitch();
+ PersistentAgentsClient client = GetClient();
+ PersistentAgentsChatClient chatClient = new(client, _agentId, _threadId);
+
+ List messages = [];
+ messages.Add(new ChatMessage(ChatRole.User, [new TextContent("Hello, tell me a joke")]));
+
+ ChatResponse response = await chatClient.GetResponseAsync(messages);
+
+ Assert.IsNotNull(response);
+ Assert.IsNotNull(response.Messages);
+ Assert.GreaterOrEqual(response.Messages.Count, 1);
+ Assert.AreEqual(ChatRole.Assistant, response.Messages[0].Role);
+ Assert.IsNotNull(response.ConversationId);
+ }
+
+ [RecordedTest]
+ [TestCase(ChatOptionsTestType.Default)]
+ [TestCase(ChatOptionsTestType.WithTools)]
+ [TestCase(ChatOptionsTestType.WithResponseFormat)]
+ public async Task TestGetStreamingResponseAsync(ChatOptionsTestType optionsType)
+ {
+ if (!IsAsync)
+ {
+ Assert.Inconclusive(STREAMING_CONSTRAINT);
+ }
+
+ using IDisposable _ = SetTestSwitch();
+ PersistentAgentsClient client = GetClient();
+ PersistentAgentsChatClient chatClient = new(client, _agentId, _threadId);
+
+ ChatOptions options = null;
+ if (optionsType == ChatOptionsTestType.WithTools)
+ {
+ options = new ChatOptions
+ {
+ Tools = [AIFunctionFactory.Create(() => "It's 80 degrees and sunny.", "GetWeather")],
+ ToolMode = ChatToolMode.Auto
+ };
+ }
+ else if (optionsType == ChatOptionsTestType.WithResponseFormat)
+ {
+ options = new ChatOptions
+ {
+ ResponseFormat = ChatResponseFormat.Json
+ };
+ }
+
+ List messages = [new ChatMessage(ChatRole.User, [new TextContent("What's the weather like? Respond in JSON.")])];
+ bool receivedUpdate = false;
+
+ await foreach (ChatResponseUpdate update in chatClient.GetStreamingResponseAsync(messages, options))
+ {
+ Assert.IsNotNull(update);
+ Assert.IsNotNull(update.ConversationId);
+ if (update.Contents.Any(c => (optionsType == ChatOptionsTestType.WithTools && c is FunctionCallContent) || c is TextContent))
+ {
+ receivedUpdate = true;
+ }
+ }
+
+ Assert.IsTrue(receivedUpdate, "No valid streaming update received.");
+ }
+
+ [RecordedTest]
+ public async Task TestSubmitToolOutputs()
+ {
+ using IDisposable _ = SetTestSwitch();
+ PersistentAgentsClient client = GetClient();
+ FunctionToolDefinition tool = new(
+ name: "GetFavouriteWord",
+ description: "Gets the favourite word of a person.",
+ parameters: BinaryData.FromObjectAsJson(new
+ {
+ Type = "object",
+ Properties = new { Name = new { Type = "string", Description = "Person's name" } },
+ Required = new[] { "name" }
+ }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
+ );
+
+ PersistentAgent agent = await client.Administration.CreateAgentAsync(
+ model: "gpt-4.1",
+ name: AGENT_NAME,
+ instructions: "Use the provided function to answer questions.",
+ tools: [tool]
+ );
+
+ PersistentAgentThread thread = await client.Threads.CreateThreadAsync();
+
+ PersistentAgentsChatClient chatClient = new(client, agent.Id, thread.Id);
+
+ await client.Messages.CreateMessageAsync(thread.Id, MessageRole.User, "What's Mike's favourite word?");
+
+ ThreadRun run = await client.Runs.CreateRunAsync(thread.Id, agent.Id);
+ do
+ {
+ await Task.Delay(500);
+ run = await client.Runs.GetRunAsync(thread.Id, run.Id);
+ } while (run.Status == RunStatus.Queued || run.Status == RunStatus.InProgress);
+
+ if (run.Status == RunStatus.RequiresAction && run.RequiredAction is SubmitToolOutputsAction action)
+ {
+ List messages = [];
+ foreach (RequiredToolCall toolCall in action.ToolCalls)
+ {
+ if (toolCall is RequiredFunctionToolCall functionCall)
+ {
+ string[] callIds = [run.Id, functionCall.Id];
+ messages.Add(new ChatMessage(ChatRole.Tool, [new FunctionResultContent(JsonSerializer.Serialize(callIds), "bar")]));
+ }
+ }
+
+ ChatResponse response = await chatClient.GetResponseAsync(messages, new ChatOptions { ConversationId = thread.Id });
+ Assert.IsNotNull(response);
+ Assert.GreaterOrEqual(response.Messages.Count, 1);
+ Assert.IsTrue(response.Messages[0].Contents.Any(c => c is TextContent tc && tc.Text.Contains("bar")));
+ }
+ else
+ {
+ Assert.Fail("Run did not require tool action.");
+ }
+ }
+
+ [RecordedTest]
+ public async Task TestChatOptionsTools()
+ {
+ using IDisposable _ = SetTestSwitch();
+ PersistentAgentsClient client = GetClient();
+
+ FunctionToolDefinition wordTool = new(
+ name: "GetFavouriteWord",
+ description: "Gets the favourite word of a person.",
+ parameters: BinaryData.FromObjectAsJson(new
+ {
+ Type = "object",
+ Properties = new { Name = new { Type = "string", Description = "Person's name" } },
+ Required = new[] { "name" }
+ }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
+ );
+
+ // First tool is registered on agent level.
+ PersistentAgent agent = await client.Administration.CreateAgentAsync(
+ model: "gpt-4.1",
+ name: AGENT_NAME,
+ instructions: "Use the provided function to answer questions.",
+ tools: [wordTool]
+ );
+
+ // Second tool is registered per request.
+ ChatOptions chatOptions = new()
+ {
+ Tools = [AIFunctionFactory.Create(() => "It's 80 degrees and sunny.", "GetWeather")],
+ ToolMode = ChatToolMode.Auto
+ };
+
+ PersistentAgentThread thread = await client.Threads.CreateThreadAsync();
+
+ PersistentAgentsChatClient chatClient = new(client, agent.Id, thread.Id);
+
+ List messages = [];
+ messages.Add(new ChatMessage(ChatRole.User, [new TextContent("What's Mike's favourite word and current weather in Seattle?")]));
+
+ ChatResponse response = await chatClient.GetResponseAsync(messages, chatOptions);
+
+ Assert.IsNotNull(response);
+ Assert.IsNotNull(response.Messages);
+ Assert.GreaterOrEqual(response.Messages.Count, 1);
+ Assert.AreEqual(ChatRole.Assistant, response.Messages[0].Role);
+
+ List functionNames = [.. response.Messages[0].Contents
+ .OfType()
+ .Select(c => c.Name)];
+
+ Assert.Contains("GetFavouriteWord", functionNames);
+ Assert.Contains("GetWeather", functionNames);
+ }
+
+ [RecordedTest]
+ public void TestGetService()
+ {
+ using IDisposable _ = SetTestSwitch();
+ PersistentAgentsClient client = GetClient();
+ PersistentAgentsChatClient chatClient = new(client, _agentId, _threadId);
+
+ Assert.IsNotNull(chatClient.GetService(typeof(ChatClientMetadata)));
+ Assert.IsNotNull(chatClient.GetService(typeof(PersistentAgentsClient)));
+ Assert.IsNotNull(chatClient.GetService(typeof(PersistentAgentsChatClient)));
+ Assert.IsNull(chatClient.GetService(typeof(string)));
+ Assert.Throws(() => chatClient.GetService(null));
+ }
+
+ #region Helpers
+ private class CompositeDisposable : IDisposable
+ {
+ private readonly List _disposables = [];
+
+ public CompositeDisposable(params IDisposable[] disposables)
+ {
+ for (int i = 0; i < disposables.Length; i++)
+ {
+ _disposables.Add(disposables[i]);
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (IDisposable d in _disposables)
+ {
+ d?.Dispose();
+ }
+ }
+ }
+
+ private static CompositeDisposable SetTestSwitch()
+ {
+ return new CompositeDisposable(
+ new TestAppContextSwitch(new()
+ {
+ { PersistantAgensConstants.UseOldConnectionString, true.ToString() }
+ }));
+ }
+
+ private PersistentAgentsClient GetClient()
+ {
+ var connectionString = TestEnvironment.PROJECT_CONNECTION_STRING;
+ PersistentAgentsAdministrationClientOptions opts = InstrumentClientOptions(new PersistentAgentsAdministrationClientOptions());
+ PersistentAgentsAdministrationClient admClient;
+
+ if (Mode == RecordedTestMode.Playback)
+ {
+ admClient = InstrumentClient(new PersistentAgentsAdministrationClient(connectionString, new MockCredential(), opts));
+ return new PersistentAgentsClient(admClient);
+ }
+
+ var cli = Environment.GetEnvironmentVariable("USE_CLI_CREDENTIAL");
+ if (!string.IsNullOrEmpty(cli) && string.Compare(cli, "true", StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ admClient = InstrumentClient(new PersistentAgentsAdministrationClient(connectionString, new AzureCliCredential(), opts));
+ }
+ else
+ {
+ admClient = InstrumentClient(new PersistentAgentsAdministrationClient(connectionString, new DefaultAzureCredential(), opts));
+ }
+
+ return new PersistentAgentsClient(admClient);
+ }
+
+ #endregion
+
+ #region Cleanup
+ [TearDown]
+ public void Cleanup()
+ {
+ DirectoryInfo tempDir = new(Path.Combine(Path.GetTempPath(), "cs_e2e_temp_dir"));
+ if (tempDir.Exists)
+ {
+ tempDir.Delete(true);
+ }
+
+ if (Mode == RecordedTestMode.Playback)
+ return;
+
+ PersistentAgentsClient client;
+ var cli = Environment.GetEnvironmentVariable("USE_CLI_CREDENTIAL");
+ if (!string.IsNullOrEmpty(cli) && string.Compare(cli, "true", StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ client = new PersistentAgentsClient(TestEnvironment.PROJECT_ENDPOINT, new AzureCliCredential());
+ }
+ else
+ {
+ client = new PersistentAgentsClient(TestEnvironment.PROJECT_ENDPOINT, new DefaultAzureCredential());
+ }
+
+ // Remove agent
+ Pageable agents = client.Administration.GetAgents();
+ foreach (PersistentAgent agent in agents)
+ {
+ if (agent.Name.StartsWith(AGENT_NAME))
+ client.Administration.DeleteAgent(agent.Id);
+ }
+
+ // Remove thread
+ Pageable threads = client.Threads.GetThreads();
+ foreach (PersistentAgentThread thread in threads)
+ {
+ if (thread.Id == _threadId)
+ client.Threads.DeleteThread(thread.Id);
+ }
+ }
+ #endregion
+ }
+}
From d5f38a4de70e502c1937582da16b7a3d50a403eb Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Wed, 25 Jun 2025 19:11:03 -0700
Subject: [PATCH 03/12] Added sample
---
.../Sample_PersistentAgents_As_IChatClient.cs | 51 +++++++++++++++++++
1 file changed, 51 insertions(+)
create mode 100644 sdk/ai/Azure.AI.Agents.Persistent/tests/Samples/Sample_PersistentAgents_As_IChatClient.cs
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/tests/Samples/Sample_PersistentAgents_As_IChatClient.cs b/sdk/ai/Azure.AI.Agents.Persistent/tests/Samples/Sample_PersistentAgents_As_IChatClient.cs
new file mode 100644
index 000000000000..09f2bce0ccc1
--- /dev/null
+++ b/sdk/ai/Azure.AI.Agents.Persistent/tests/Samples/Sample_PersistentAgents_As_IChatClient.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#nullable disable
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Azure.Core.TestFramework;
+using Microsoft.Extensions.AI;
+using NUnit.Framework;
+
+namespace Azure.AI.Agents.Persistent.Tests;
+
+public partial class Sample_PersistentAgents_As_IChatClient : SamplesBase
+{
+ [Test]
+ [AsyncOnly]
+ public async Task PersistentAgentsAsIChatClient()
+ {
+ #region Snippet:PersistentAgentsAsIChatClient_CreateClient
+#if SNIPPET
+ var projectEndpoint = System.Environment.GetEnvironmentVariable("PROJECT_ENDPOINT");
+ var modelDeploymentName = System.Environment.GetEnvironmentVariable("MODEL_DEPLOYMENT_NAME");
+#else
+ var projectEndpoint = TestEnvironment.PROJECT_ENDPOINT;
+ var modelDeploymentName = TestEnvironment.MODELDEPLOYMENTNAME;
+#endif
+ PersistentAgentsClient client = new(projectEndpoint, new DefaultAzureCredential());
+ #endregion
+ #region Snippet:PersistentAgentsAsIChatClient_CreateAgentAsIChatClient
+ PersistentAgent agent = await client.Administration.CreateAgentAsync(
+ model: modelDeploymentName,
+ name: "my-agent",
+ instructions: "You are a helpful agent.");
+
+ PersistentAgentThread thread = await client.Threads.CreateThreadAsync();
+
+ IChatClient chatClient = client.AsIChatClient(agent.Id, thread.Id);
+ #endregion
+ #region Snippet:PersistentAgentsAsIChatClient_GetResponseAsync
+ ChatResponse response = await chatClient.GetResponseAsync([new ChatMessage(ChatRole.User, [new TextContent("Hello, tell me a joke")])]);
+
+ Console.WriteLine(string.Join(Environment.NewLine, response.Messages.Select(c => c.Text)));
+ #endregion
+ #region Snippet:PersistentAgentsAsIChatClient_Cleanup
+ await client.Threads.DeleteThreadAsync(thread.Id);
+ await client.Administration.DeleteAgentAsync(agent.Id);
+ #endregion
+ }
+}
From c8e2f461a8bd2e06bf16d536a0ab665c47e4e5c4 Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Wed, 25 Jun 2025 19:15:58 -0700
Subject: [PATCH 04/12] Public API additions
---
.../api/Azure.AI.Agents.Persistent.net8.0.cs | 13 +++++++++++++
.../Azure.AI.Agents.Persistent.netstandard2.0.cs | 13 +++++++++++++
2 files changed, 26 insertions(+)
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.net8.0.cs b/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.net8.0.cs
index fee03fc84fa8..6d227cd3b4ba 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.net8.0.cs
+++ b/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.net8.0.cs
@@ -1145,6 +1145,15 @@ public enum ServiceVersion
V2025_05_15_Preview = 3,
}
}
+ public partial class PersistentAgentsChatClient : Microsoft.Extensions.AI.IChatClient, System.IDisposable
+ {
+ protected PersistentAgentsChatClient() { }
+ public PersistentAgentsChatClient(Azure.AI.Agents.Persistent.PersistentAgentsClient client, string agentId, string? defaultThreadId = null) { }
+ public void Dispose() { }
+ public virtual System.Threading.Tasks.Task GetResponseAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
+ public virtual object? GetService(System.Type serviceType, object? serviceKey = null) { throw null; }
+ public virtual System.Collections.Generic.IAsyncEnumerable GetStreamingResponseAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options = null, [System.Runtime.CompilerServices.EnumeratorCancellationAttribute] System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
+ }
public partial class PersistentAgentsClient
{
protected PersistentAgentsClient() { }
@@ -1159,6 +1168,10 @@ public PersistentAgentsClient(string endpoint, Azure.Core.TokenCredential creden
public virtual Azure.Response CreateThreadAndRun(string assistantId, Azure.AI.Agents.Persistent.ThreadAndRunOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual System.Threading.Tasks.Task> CreateThreadAndRunAsync(string assistantId, Azure.AI.Agents.Persistent.ThreadAndRunOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
+ public static partial class PersistentAgentsClientExtensions
+ {
+ public static Microsoft.Extensions.AI.IChatClient AsIChatClient(this Azure.AI.Agents.Persistent.PersistentAgentsClient client, string agentId, string? defaultThreadId = null) { throw null; }
+ }
public static partial class PersistentAgentsExtensions
{
public static Azure.AI.Agents.Persistent.PersistentAgentsClient GetPersistentAgentsClient(this System.ClientModel.Primitives.ClientConnectionProvider provider) { throw null; }
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.netstandard2.0.cs b/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.netstandard2.0.cs
index d34c42b9eab7..dbb434f72501 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.netstandard2.0.cs
+++ b/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.netstandard2.0.cs
@@ -1145,6 +1145,15 @@ public enum ServiceVersion
V2025_05_15_Preview = 3,
}
}
+ public partial class PersistentAgentsChatClient : Microsoft.Extensions.AI.IChatClient, System.IDisposable
+ {
+ protected PersistentAgentsChatClient() { }
+ public PersistentAgentsChatClient(Azure.AI.Agents.Persistent.PersistentAgentsClient client, string agentId, string? defaultThreadId = null) { }
+ public void Dispose() { }
+ public virtual System.Threading.Tasks.Task GetResponseAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
+ public virtual object? GetService(System.Type serviceType, object? serviceKey = null) { throw null; }
+ public virtual System.Collections.Generic.IAsyncEnumerable GetStreamingResponseAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options = null, [System.Runtime.CompilerServices.EnumeratorCancellationAttribute] System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
+ }
public partial class PersistentAgentsClient
{
protected PersistentAgentsClient() { }
@@ -1159,6 +1168,10 @@ public PersistentAgentsClient(string endpoint, Azure.Core.TokenCredential creden
public virtual Azure.Response CreateThreadAndRun(string assistantId, Azure.AI.Agents.Persistent.ThreadAndRunOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual System.Threading.Tasks.Task> CreateThreadAndRunAsync(string assistantId, Azure.AI.Agents.Persistent.ThreadAndRunOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
+ public static partial class PersistentAgentsClientExtensions
+ {
+ public static Microsoft.Extensions.AI.IChatClient AsIChatClient(this Azure.AI.Agents.Persistent.PersistentAgentsClient client, string agentId, string? defaultThreadId = null) { throw null; }
+ }
public static partial class PersistentAgentsExtensions
{
public static Azure.AI.Agents.Persistent.PersistentAgentsClient GetPersistentAgentsClient(this System.ClientModel.Primitives.ClientConnectionProvider provider) { throw null; }
From d10f507a7ae134da807bfb95d17bdcf01b505aa2 Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Wed, 25 Jun 2025 19:34:54 -0700
Subject: [PATCH 05/12] Update
sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsClientExtensions.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../src/Custom/PersistentAgentsClientExtensions.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsClientExtensions.cs b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsClientExtensions.cs
index af1fd5c899ba..f4320e3b9f47 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsClientExtensions.cs
+++ b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsClientExtensions.cs
@@ -20,7 +20,7 @@ public static class PersistentAgentsClientExtensions
///
/// An optional existing thread identifier for the chat session. This serves as a default, and may be overridden per call to
/// or via the
- /// property. If not thread ID is provided via either mechanism, a new thread will be created for the request.
+ /// property. If no thread ID is provided via either mechanism, a new thread will be created for the request.
///
/// An instance configured to interact with the specified agent and thread.
public static IChatClient AsIChatClient(this PersistentAgentsClient client, string agentId, string? defaultThreadId = null) =>
From 1c0204266b78a80ef133aa927d9356af780cf5f4 Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Wed, 25 Jun 2025 19:36:21 -0700
Subject: [PATCH 06/12] Fixed typo
---
.../src/Custom/PersistentAgentsAdministrationClient.cs | 4 ++--
...rsistantAgensConstants.cs => PersistentAgentsConstants.cs} | 2 +-
.../tests/PersistentAgentsChatClientTests.cs | 2 +-
.../Azure.AI.Agents.Persistent/tests/PersistentAgentsTests.cs | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
rename sdk/ai/Azure.AI.Agents.Persistent/src/Custom/{PersistantAgensConstants.cs => PersistentAgentsConstants.cs} (88%)
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsAdministrationClient.cs b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsAdministrationClient.cs
index 3feb39566db3..9efb13472b23 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsAdministrationClient.cs
+++ b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsAdministrationClient.cs
@@ -20,8 +20,8 @@ namespace Azure.AI.Agents.Persistent
public partial class PersistentAgentsAdministrationClient
{
private static readonly bool s_is_test_run = AppContextSwitchHelper.GetConfigValue(
- PersistantAgensConstants.UseOldConnectionString,
- PersistantAgensConstants.UseOldConnectionStringEnvVar);
+ PersistentAgentsConstants.UseOldConnectionString,
+ PersistentAgentsConstants.UseOldConnectionStringEnvVar);
/// The ClientDiagnostics is used to provide tracing support for the client library.
internal virtual ClientDiagnostics ClientDiagnostics { get; }
// TODO: Replace project connections string by PROJECT_ENDPOINT when 1DP will be available.
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistantAgensConstants.cs b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsConstants.cs
similarity index 88%
rename from sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistantAgensConstants.cs
rename to sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsConstants.cs
index d0bb692acb80..f93503fc41de 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistantAgensConstants.cs
+++ b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsConstants.cs
@@ -3,7 +3,7 @@
namespace Azure.AI.Agents.Persistent
{
- internal class PersistantAgensConstants
+ internal class PersistentAgentsConstants
{
public const string UseOldConnectionString = "Azure.AI.Agents.Persistent.Internal.UseConnectionString";
public const string UseOldConnectionStringEnvVar = "_IS_TEST_RUN";
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/tests/PersistentAgentsChatClientTests.cs b/sdk/ai/Azure.AI.Agents.Persistent/tests/PersistentAgentsChatClientTests.cs
index eae805a412df..68b0343b5927 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/tests/PersistentAgentsChatClientTests.cs
+++ b/sdk/ai/Azure.AI.Agents.Persistent/tests/PersistentAgentsChatClientTests.cs
@@ -275,7 +275,7 @@ private static CompositeDisposable SetTestSwitch()
return new CompositeDisposable(
new TestAppContextSwitch(new()
{
- { PersistantAgensConstants.UseOldConnectionString, true.ToString() }
+ { PersistentAgentsConstants.UseOldConnectionString, true.ToString() }
}));
}
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/tests/PersistentAgentsTests.cs b/sdk/ai/Azure.AI.Agents.Persistent/tests/PersistentAgentsTests.cs
index cead4a3bb908..0a5152c3080c 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/tests/PersistentAgentsTests.cs
+++ b/sdk/ai/Azure.AI.Agents.Persistent/tests/PersistentAgentsTests.cs
@@ -56,7 +56,7 @@ public IDisposable SetTestSwitch()
{
return new CompositeDisposable(
new TestAppContextSwitch(new() {
- { PersistantAgensConstants.UseOldConnectionString, true.ToString() },
+ { PersistentAgentsConstants.UseOldConnectionString, true.ToString() },
}));
}
From b222b1e2d3a210dfeb8f809ea729c38463aaa3ee Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Thu, 26 Jun 2025 08:36:04 -0700
Subject: [PATCH 07/12] Addressed PR feedback
---
.../api/Azure.AI.Agents.Persistent.net8.0.cs | 9 ---------
.../api/Azure.AI.Agents.Persistent.netstandard2.0.cs | 9 ---------
.../src/Custom/PersistentAgentsChatClient.cs | 3 +--
3 files changed, 1 insertion(+), 20 deletions(-)
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.net8.0.cs b/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.net8.0.cs
index 6d227cd3b4ba..d3463eec9d27 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.net8.0.cs
+++ b/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.net8.0.cs
@@ -1145,15 +1145,6 @@ public enum ServiceVersion
V2025_05_15_Preview = 3,
}
}
- public partial class PersistentAgentsChatClient : Microsoft.Extensions.AI.IChatClient, System.IDisposable
- {
- protected PersistentAgentsChatClient() { }
- public PersistentAgentsChatClient(Azure.AI.Agents.Persistent.PersistentAgentsClient client, string agentId, string? defaultThreadId = null) { }
- public void Dispose() { }
- public virtual System.Threading.Tasks.Task GetResponseAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
- public virtual object? GetService(System.Type serviceType, object? serviceKey = null) { throw null; }
- public virtual System.Collections.Generic.IAsyncEnumerable GetStreamingResponseAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options = null, [System.Runtime.CompilerServices.EnumeratorCancellationAttribute] System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
- }
public partial class PersistentAgentsClient
{
protected PersistentAgentsClient() { }
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.netstandard2.0.cs b/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.netstandard2.0.cs
index dbb434f72501..f0887f272c48 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.netstandard2.0.cs
+++ b/sdk/ai/Azure.AI.Agents.Persistent/api/Azure.AI.Agents.Persistent.netstandard2.0.cs
@@ -1145,15 +1145,6 @@ public enum ServiceVersion
V2025_05_15_Preview = 3,
}
}
- public partial class PersistentAgentsChatClient : Microsoft.Extensions.AI.IChatClient, System.IDisposable
- {
- protected PersistentAgentsChatClient() { }
- public PersistentAgentsChatClient(Azure.AI.Agents.Persistent.PersistentAgentsClient client, string agentId, string? defaultThreadId = null) { }
- public void Dispose() { }
- public virtual System.Threading.Tasks.Task GetResponseAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
- public virtual object? GetService(System.Type serviceType, object? serviceKey = null) { throw null; }
- public virtual System.Collections.Generic.IAsyncEnumerable GetStreamingResponseAsync(System.Collections.Generic.IEnumerable messages, Microsoft.Extensions.AI.ChatOptions? options = null, [System.Runtime.CompilerServices.EnumeratorCancellationAttribute] System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
- }
public partial class PersistentAgentsClient
{
protected PersistentAgentsClient() { }
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs
index 5892e7ab753e..37c98a515e3e 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs
+++ b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT License.
#nullable enable
-#pragma warning disable AZC0004, AZC0007, AZC0015
using System;
using System.Collections.Generic;
@@ -19,7 +18,7 @@
namespace Azure.AI.Agents.Persistent
{
/// Represents an for an Azure.AI.Agents.Persistent .
- public partial class PersistentAgentsChatClient : IChatClient
+ internal partial class PersistentAgentsChatClient : IChatClient
{
/// The name of the chat client provider.
private const string ProviderName = "azure";
From f45c76de6bc637cc7c395a86982114d004ceb9e1 Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Fri, 27 Jun 2025 12:31:05 -0700
Subject: [PATCH 08/12] Added conditional block for dependency
---
eng/Packages.Data.props | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props
index 6c63a6f06003..f042979ea290 100644
--- a/eng/Packages.Data.props
+++ b/eng/Packages.Data.props
@@ -103,7 +103,6 @@
-
@@ -207,6 +206,10 @@
+
+
+
+
From c4c711e56e167e8939fd946b47e8c469b8763542 Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Mon, 30 Jun 2025 09:02:27 -0700
Subject: [PATCH 09/12] Small improvements
---
.../src/Custom/PersistentAgentsChatClient.cs | 26 +++++++++----------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs
index 37c98a515e3e..16a406842241 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs
+++ b/sdk/ai/Azure.AI.Agents.Persistent/src/Custom/PersistentAgentsChatClient.cs
@@ -129,20 +129,20 @@ public virtual async IAsyncEnumerable GetStreamingResponseAs
updates = _client!.Runs.CreateRunStreamingAsync(
threadId: threadId,
agentId: _agentId,
- overrideModelName: runOptions?.OverrideModelName,
- overrideInstructions: runOptions?.OverrideInstructions,
+ overrideModelName: runOptions.OverrideModelName,
+ overrideInstructions: runOptions.OverrideInstructions,
additionalInstructions: null,
- additionalMessages: runOptions?.ThreadOptions.Messages,
- overrideTools: runOptions?.OverrideTools,
- temperature: runOptions?.Temperature,
- topP: runOptions?.TopP,
- maxPromptTokens: runOptions?.MaxPromptTokens,
- maxCompletionTokens: runOptions?.MaxCompletionTokens,
- truncationStrategy: runOptions?.TruncationStrategy,
- toolChoice: runOptions?.ToolChoice,
- responseFormat: runOptions?.ResponseFormat,
- parallelToolCalls: runOptions?.ParallelToolCalls,
- metadata: runOptions?.Metadata,
+ additionalMessages: runOptions.ThreadOptions.Messages,
+ overrideTools: runOptions.OverrideTools,
+ temperature: runOptions.Temperature,
+ topP: runOptions.TopP,
+ maxPromptTokens: runOptions.MaxPromptTokens,
+ maxCompletionTokens: runOptions.MaxCompletionTokens,
+ truncationStrategy: runOptions.TruncationStrategy,
+ toolChoice: runOptions.ToolChoice,
+ responseFormat: runOptions.ResponseFormat,
+ parallelToolCalls: runOptions.ParallelToolCalls,
+ metadata: runOptions.Metadata,
cancellationToken);
}
From 11669bdc168924b78f057f452de6408560fc4d1a Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Tue, 1 Jul 2025 16:05:36 -0700
Subject: [PATCH 10/12] Updated assets tag
---
sdk/ai/Azure.AI.Agents.Persistent/assets.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/assets.json b/sdk/ai/Azure.AI.Agents.Persistent/assets.json
index c32106030257..0c04871a0dfa 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/assets.json
+++ b/sdk/ai/Azure.AI.Agents.Persistent/assets.json
@@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "net",
"TagPrefix": "net/ai/Azure.AI.Agents.Persistent",
- "Tag": "net/ai/Azure.AI.Agents.Persistent_f7ce2c53dc"
+ "Tag": "net/ai/Azure.AI.Agents.Persistent_6b97e0f0c7"
}
From 136c3ac1cb8fe4eabb653983bb3f7f5ad6450f13 Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Tue, 1 Jul 2025 16:33:16 -0700
Subject: [PATCH 11/12] Updated assets tag
---
sdk/ai/Azure.AI.Agents.Persistent/assets.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/assets.json b/sdk/ai/Azure.AI.Agents.Persistent/assets.json
index 0c04871a0dfa..919c761dddae 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/assets.json
+++ b/sdk/ai/Azure.AI.Agents.Persistent/assets.json
@@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "net",
"TagPrefix": "net/ai/Azure.AI.Agents.Persistent",
- "Tag": "net/ai/Azure.AI.Agents.Persistent_6b97e0f0c7"
+ "Tag": "net/ai/Azure.AI.Agents.Persistent_42e1171bc4"
}
From 0461245bd70f455086ecb68a2f702166ee3bc122 Mon Sep 17 00:00:00 2001
From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Date: Tue, 1 Jul 2025 17:01:39 -0700
Subject: [PATCH 12/12] Updated assets tag
---
sdk/ai/Azure.AI.Agents.Persistent/assets.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sdk/ai/Azure.AI.Agents.Persistent/assets.json b/sdk/ai/Azure.AI.Agents.Persistent/assets.json
index 919c761dddae..28d5c1f1e9b5 100644
--- a/sdk/ai/Azure.AI.Agents.Persistent/assets.json
+++ b/sdk/ai/Azure.AI.Agents.Persistent/assets.json
@@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "net",
"TagPrefix": "net/ai/Azure.AI.Agents.Persistent",
- "Tag": "net/ai/Azure.AI.Agents.Persistent_42e1171bc4"
+ "Tag": "net/ai/Azure.AI.Agents.Persistent_44b68ac951"
}