.NET: Promote FoundryChatClient to public, add file/vector-store helpers and ToPromptAgentAsync converter#5940
Conversation
- Replace AzureAIProjectChatClient and AzureAIProjectResponsesChatClient with a single internal sealed FoundryChatClient that covers three modes (pure responses, server-side agent reference, hosted agent endpoint).
- Rename AzureAIProjectChatClientExtensions to AIProjectClientExtensions to reflect that it extends AIProjectClient.
- All four AsAIAgent extension overloads and both FoundryAgent constructors now construct FoundryChatClient internally so the microsoft.foundry telemetry tag is uniform across paths.
- Introduce AgentFrameworkUserAgentPolicy that stamps agent-framework-dotnet/{version} on outbound requests, mirroring the Python agent-framework-python/{version} contract.
- Delete the Foundry-local MeaiUserAgentPolicy duplicate; rely on MEAI 10.5.1 to stamp MEAI/{version} automatically.
- HostedAgentUserAgentPolicy keeps the combined foundry-hosting/agent-framework-dotnet/{version} segment (Python parity) and upgrades the bare segment in place to avoid duplication.
- Tests reorganized: FoundryChatClientTests, AIProjectClientExtensionsTests, AgentFrameworkUserAgentPolicyTests, MeaiAutoUserAgentVerificationTests, plus in-place upgrade unit tests in HostedOutboundUserAgentTests.
…d ToPromptAgentAsync converter - Promote FoundryChatClient from internal sealed to public sealed for Python parity, so .NET developers can hold and pass a FoundryChatClient directly the way Python developers do. - Mode 3 (hosted agent endpoint) now materializes an AIProjectClient from the parsed project root, making GetService<AIProjectClient>() non-null across all three construction modes. This eliminates the per-mode asymmetry that previously hid project-level helpers from agents constructed via an agent endpoint URL. - Add four new instance methods on FoundryChatClient mirroring Python's spec: UploadFileAsync, DeleteFileAsync, CreateVectorStoreAsync (bundles upload + create + wait), DeleteVectorStoreAsync. Single overload each, path-only inputs to start; additional overloads can be added later without breaking callers. All are Experimental, consistent with the rest of the Foundry package. - Add ToPromptAgentAsync extension methods on ChatClientAgent and FoundryAgent for the agent-to-prompt-agent converter described in the Foundry spec. Mode 1 (responses API) synthesizes a DeclarativeAgentDefinition from the agent's ChatOptions; mode 2 (server-side agent reference, version, or record) returns the cached or freshly fetched Definition; mode 3 throws InvalidOperationException because no local definition exists to convert. - Strict AITool to ResponseTool mapping for mode 1: AIFunction becomes CreateFunctionTool with the function's JSON schema; AITool instances that wrap a ResponseTool unwrap via GetService(typeof(ResponseTool)); anything else throws InvalidOperationException naming the offending tool type. Matches the Python spec's unsupported-tools-raise-ValueError contract. - New unit tests: FoundryChatClientVectorStoreTests (22 tests covering all four helpers across the three FoundryChatClient construction modes plus validation and cancellation), FoundryPromptAgentConverterTests (16 tests covering both extension entry points across mode 1 synthesis, mode 2 cached and fetched paths, all failure modes, and a Python-parity guard asserting both extensions produce equivalent definitions for equivalent inputs), plus four new tests in FoundryChatClientTests for the mode 3 AIProjectClient materialization.
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 79%
✓ Correctness
No actionable issues found in this dimension.
✓ Security Reliability
The PR is well-structured with proper input validation, idempotency guards, and graceful fallbacks. Two reliability observations: (1) CreateVectorStoreAsync does not clean up previously-uploaded files if a mid-sequence upload fails, leaving orphaned server resources; (2) In mode 3 (hosted agent endpoint), the materialized AIProjectClient for file/vector-store operations does not propagate the caller's clientOptions (Transport, RetryPolicy, NetworkTimeout), creating an inconsistency with how CreateProjectLevelOpenAIClientFromAgentEndpoint carefully propagates those settings. Neither is a security vulnerability, but the options propagation gap could cause unexpected behavior in testing scenarios.
✓ Test Coverage
No actionable issues found in this dimension.
✗ Design Approach
I found two design-level regressions in the new
FoundryChatClient: hosted-endpoint helper calls are built on a project client that drops the caller’s configured pipeline/options, andCreateVectorStoreAsyncdoes not implement the wait-until-ready behavior that its own contract promises. I found one design issue in the new mode-3 helper surface: the tests explicitly acknowledge that hosted-agent file uploads run through a separately materializedAIProjectClientwith the default transport, which means the new file/vector-store APIs would bypass caller-suplied transport and pipeline settings on that construction path. The existing repo already has a better pattern for agent-endpoint scenarios that preserves those settings when moving between per-agent and project-level clients.
Automated review by rogerbarreto's agents
| var aiProjectClient = new AIProjectClient(projectRoot, credential); | ||
|
|
||
| return new HostedAgentEndpointInner(chatClient, perAgentClient, aiProjectClient, agentName); | ||
| } | ||
|
|
||
| /// <summary>Best-effort registration of <see cref="AgentFrameworkUserAgentPolicy"/> via the MEAI <see cref="OpenAIRequestPolicies"/> hook with at-most-once dedup per pipeline.</summary> |
There was a problem hiding this comment.
This hosted-endpoint path creates new AIProjectClient(projectRoot, credential) with default options only, while CreateProjectLevelOpenAIClientFromAgentEndpoint ~20 lines below carefully copies Transport, RetryPolicy, NetworkTimeout, and UserAgentApplicationId. Because UploadFileAsync/CreateVectorStoreAsync route through this unconfigured client (GetOpenAIFileClient()/GetVectorStoreClient()), they silently ignore the caller's pipeline settings. This contradicts the forwarding contract validated in FoundryAgentTests.cs:687-717. Build this AIProjectClient from a copied options bag that mirrors the relevant settings from clientOptions.
| } | ||
|
|
||
| var options = new VectorStoreCreationOptions | ||
| { |
There was a problem hiding this comment.
If an upload in this loop throws (e.g., file 3 of 5 not found or network failure), previously-uploaded files are orphaned on the server with no cleanup. Consider wrapping the loop in try/catch with best-effort DeleteFileAsync calls for already-uploaded IDs, or at minimum document the non-transactional semantics in the XML doc.
| } | ||
|
|
||
| var vectorStoreClient = this.GetVectorStoreClient(); | ||
| var result = await vectorStoreClient.CreateVectorStoreAsync(options, cancellationToken).ConfigureAwait(false); |
There was a problem hiding this comment.
The XML doc contract (lines 234-242) says this helper 'waits until the store is fully ready' and returns 'The created and fully-ready VectorStore', but the implementation just calls CreateVectorStoreAsync(...) and returns immediately. A caller can receive a store that is still indexing, defeating the purpose of the one-call helper. This needs an explicit wait/poll step on the created vector store before returning.
| { | ||
| // Regression-prevention for Q2 mode-3 materialization: file uploads must work on mode 3 | ||
| // because the materialized AIProjectClient is now reachable. We cannot fully exercise the | ||
| // wire here (mode 3's AIProjectClient is built internally with a real DefaultTransport), |
There was a problem hiding this comment.
This comment surfaces a real design gap: mode-3 file/vector-store operations route through a hidden AIProjectClient with DefaultTransport, bypassing the caller's custom pipeline. The repo already has the correct pattern—reuse a supplied project client for agent endpoints (AzureAIProjectChatClientExtensions.cs:69-103) or copy per-agent options onto a project-scoped client (FoundryAgent.cs:431-455). The new helper surface should follow the same approach.
There was a problem hiding this comment.
Pull request overview
This PR consolidates Foundry .NET chat-client plumbing around FoundryChatClient, adds prompt-agent conversion helpers, and expands Foundry file/vector-store helper coverage with tests.
Changes:
- Replaces the older AzureAIProject chat-client decorators with
FoundryChatClientand adds agent-framework User-Agent stamping. - Adds
ToPromptAgentAsyncconversion extensions for Foundry-backed agents. - Adds file/vector-store helper APIs and broad unit coverage for construction modes, user-agent behavior, and conversion paths.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
dotnet/src/Microsoft.Agents.AI.Foundry/FoundryChatClient.cs |
New unified Foundry chat client plus file/vector-store helpers. |
dotnet/src/Microsoft.Agents.AI.Foundry/FoundryPromptAgentConverter.cs |
Shared implementation for prompt-agent conversion. |
dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgentExtensions.cs |
Adds FoundryAgent.ToPromptAgentAsync. |
dotnet/src/Microsoft.Agents.AI.Foundry/ChatClientAgentFoundryExtensions.cs |
Adds ChatClientAgent.ToPromptAgentAsync. |
dotnet/src/Microsoft.Agents.AI.Foundry/AgentFrameworkUserAgentPolicy.cs |
Adds agent-framework User-Agent policy. |
dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs |
Routes constructors through FoundryChatClient. |
dotnet/src/Microsoft.Agents.AI.Foundry/AIProjectClientExtensions.cs |
Updates extension implementation to use FoundryChatClient. |
dotnet/src/Microsoft.Agents.AI.Foundry/RequestOptionsExtensions.cs |
Removes local MEAI User-Agent policy. |
dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClient.cs |
Removes replaced decorator. |
dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectResponsesChatClient.cs |
Removes replaced decorator. |
dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/HostedAgentUserAgentPolicy.cs |
Updates hosted User-Agent upgrade behavior. |
dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryChatClientTests.cs |
Adds coverage for FoundryChatClient modes and services. |
dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryChatClientVectorStoreTests.cs |
Adds file/vector-store helper tests. |
dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryPromptAgentConverterTests.cs |
Adds prompt-agent converter tests. |
dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AgentFrameworkUserAgentPolicyTests.cs |
Adds User-Agent policy tests. |
dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/MeaiAutoUserAgentVerificationTests.cs |
Adds MEAI auto-stamping regression test. |
dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentTests.cs |
Updates FoundryAgent user-agent and metadata tests. |
dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AIProjectClientExtensionsTests.cs |
Updates extension tests for new chat-client wrapper. |
dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/RequestOptionsExtensionsTests.cs |
Removes obsolete local MEAI policy tests. |
dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientTests.cs |
Removes tests for deleted decorator. |
dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostedOutboundUserAgentTests.cs |
Extends hosted User-Agent tests. |
dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentStructuredOutputRunTests.cs |
Updates skipped-test text to FoundryChatClient. |
Comments suppressed due to low confidence (4)
dotnet/src/Microsoft.Agents.AI.Foundry/AIProjectClientExtensions.cs:26
- Renaming this public extension class is a breaking API change for callers that invoke the extension methods statically (for example,
AzureAIProjectChatClientExtensions.AsAIAgent(...)) or reference the type in source/binary compatibility checks. Keep the old public type (for example as an obsolete forwarding partial class) if the PR is intended to be additive.
dotnet/src/Microsoft.Agents.AI.Foundry/FoundryChatClient.cs:45 - These public XML docs still describe FoundryChatClient as intentionally internal and say promotion is deferred, which is now false for the newly public type. This will publish misleading API documentation for consumers.
/// The type is intentionally <see langword="internal"/> for now. The public surface remains the
/// existing <c>FoundryAgent</c> and <c>AsAIAgent</c> shapes; promotion is deferred until external
/// callers express need.
dotnet/src/Microsoft.Agents.AI.Foundry/FoundryPromptAgentConverter.cs:81
- Use American English in comments: change "synthesise" to "synthesize".
// Mode 1 (pure responses): synthesise from ChatOptions.
dotnet/src/Microsoft.Agents.AI.Foundry/FoundryPromptAgentConverter.cs:90
- Use American English in user-facing error messages: change "synthesise" to "synthesize".
"ToPromptAgentAsync requires a model id on the agent's ChatOptions to synthesise a prompt agent definition.");
| IChatClient chatClient = new FoundryChatClient(agentEndpoint, credential, clientOptions); | ||
| var agentName = ((FoundryChatClient)chatClient).HostedAgentName!; |
| // depend on this. RBAC for those calls is at the project level; if the supplied | ||
| // credential lacks project-scope permissions, the SDK surfaces a clean 401/403 at | ||
| // call time. | ||
| var aiProjectClient = new AIProjectClient(projectRoot, credential); |
| return result.Value; | ||
| } | ||
|
|
| var record = await aiProjectClient.AgentAdministrationClient | ||
| .GetAgentAsync(agentReference.Name, cancellationToken) | ||
| .ConfigureAwait(false); | ||
| return record.Value.GetLatestVersion().Definition; |
| var idx = existing!.IndexOf(BareAgentFrameworkPrefix, StringComparison.Ordinal); | ||
| if (idx >= 0) | ||
| { | ||
| var end = existing.IndexOf(' ', idx); | ||
| if (end < 0) | ||
| { | ||
| end = existing.Length; | ||
| } | ||
|
|
||
| var replaced = string.Concat(existing.AsSpan(0, idx), s_supplementValue.AsSpan(), existing.AsSpan(end)); |
| [Fact] | ||
| public async Task UploadFileAsync_Mode3_UploadsViaMaterializedProjectClientAsync() | ||
| { | ||
| // Regression-prevention for Q2 mode-3 materialization: file uploads must work on mode 3 | ||
| // because the materialized AIProjectClient is now reachable. We cannot fully exercise the | ||
| // wire here (mode 3's AIProjectClient is built internally with a real DefaultTransport), | ||
| // so we assert the call surfaces with the expected exception family (auth/network) and | ||
| // NOT InvalidOperationException complaining about a missing AIProjectClient. | ||
| var chatClient = CreateMode3(); | ||
| var path = MakeTempFile(); | ||
| try | ||
| { | ||
| var ex = await Assert.ThrowsAnyAsync<Exception>(() => | ||
| chatClient.UploadFileAsync(path, FileUploadPurpose.Assistants, CancellationToken.None)); | ||
| Assert.IsNotType<InvalidOperationException>(ex); | ||
| } | ||
| finally { File.Delete(path); } |
| /// <summary> | ||
| /// Internal Foundry chat-client decorator that unifies the three Foundry chat-client construction | ||
| /// modes (pure responses, server-side agent reference, hosted agent endpoint) behind a single | ||
| /// type and centralises Foundry-specific concerns: <c>microsoft.foundry</c> telemetry tagging, |
| /// <see cref="IChatClient.GetService(Type, object?)"/>): | ||
| /// </para> | ||
| /// <list type="bullet"> | ||
| /// <item><description><b>Mode 1 (pure responses)</b>: synthesise a <see cref="DeclarativeAgentDefinition"/> from the agent's <see cref="ChatOptions"/>.</description></item> |
Motivation and Context
This PR adds the .NET equivalent of the Foundry "Agent Framework to Prompt Agents" spec (foundrysdk_specs/specs/agent-framework-to-prompt-agents/spec.md). Today there is no .NET way to take an agent built and tested locally and publish it as a server-side Foundry prompt agent without manually restating the entire definition; this PR closes that gap and adds the file/vector-store helper surface that Python's spec also calls for, matching the Python developer experience.
Description
Two changes ship together because they share infrastructure on the same
FoundryChatClient:FoundryChatClientpromoted to public sealed so .NET developers can hold and pass the type directly, mirroring Python. Mode 3 (hosted agent endpoint URL) now materializes anAIProjectClientfrom the parsed project root, eliminating the per-mode asymmetry that previously hid project-level operations from agents constructed via an agent endpoint URL.GetService<AIProjectClient>()returns a non-null instance for all three construction modes after this change.Four new instance methods on
FoundryChatClientmirroring Python's spec:UploadFileAsync,DeleteFileAsync,CreateVectorStoreAsync(bundles upload + create + wait into one call),DeleteVectorStoreAsync. Single overload each, path-only inputs to start; additional overloads can be added later without breaking callers. Available uniformly across all three construction modes.ToPromptAgentAsyncextension methods on bothChatClientAgent(in namespaceMicrosoft.Agents.AI.Foundry) andFoundryAgent. Returns aProjectsAgentDefinitionready to pass toAgentAdministrationClient.CreateAgentVersionAsync. Mode 1 synthesizes a freshDeclarativeAgentDefinitionfrom the agent'sChatOptions(model id, instructions, temperature, top-p, tools); mode 2 returns the cached or freshly fetchedProjectsAgentVersion.Definition(the AgentReference-only path fetches the latest version from the service); mode 3 throwsInvalidOperationExceptionbecause no local definition exists to convert.Strict AITool to ResponseTool mapping for mode 1 synthesis:
AIFunctionbecomesResponseTool.CreateFunctionToolcarrying the function's JSON schema; AITool instances that wrap a ResponseTool (everything produced byFoundryAIToolfactories) unwrap viaGetService(typeof(ResponseTool)); anything else throwsInvalidOperationExceptionnaming the offending tool type. Matches Python's "unsupported tool types raise ValueError" contract.All new public types and members carry the
[Experimental]attribute consistent with the rest of theMicrosoft.Agents.AI.Foundrypackage.The first commit in this PR (
Consolidate Foundry chat client decorators into FoundryChatClient) is the prerequisite consolidation work that unified the previousAzureAIProjectChatClientandAzureAIProjectResponsesChatClientdecorators into the singleFoundryChatClienttype now being promoted here, and aligns the User-Agent contract with Python by addingagent-framework-dotnet/{version}stamping and preserving the combinedfoundry-hosting/agent-framework-dotnet/{version}segment when hosted.Verification
dotnet build --tl:off --warnaserroron all five changed projects: 0 errors, 0 warnings.dotnet format --verify-no-changes --verbosity diagnosticvia WSL2 + Docker (mcr.microsoft.com/dotnet/sdk:10.0, the CI image): clean on all five projects.Foundry.IntegrationTestsfull suite against the configured project endpoint): 67 total, 44 passed, 0 failed, 23 intentionally skipped. Matches the baselineorigin/mainexactly. Zero regressions.Contribution Checklist
FoundryChatClientwasinternaland is nowpublic sealed; existing callers see no change. All other surfaces are additive.