.NET: Add AsIChatClient extension to expose an AIAgent as an IChatClient - #7687
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an adapter allowing any .NET AIAgent to be consumed as an IChatClient.
Changes:
- Adds the
AsIChatClientextension with session and usage guidance. - Implements response conversion, streaming, cancellation, options, metadata, and service forwarding.
- Adds comprehensive unit and integration-style coverage.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
dotnet/src/Microsoft.Agents.AI/AgentExtensions.cs |
Exposes the new public extension method. |
dotnet/src/Microsoft.Agents.AI/ChatClient/AIAgentChatClient.cs |
Implements the agent-to-chat-client adapter. |
dotnet/tests/Microsoft.Agents.AI.UnitTests/AIAgentChatClientTests.cs |
Tests adapter behavior and integration. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
d009b77 to
23aca4f
Compare
@microsoft-github-policy-service agree |
|
Not all So, given the requirement in the PR, the proper way would be. var anyAiAgent = ...
var chatClient = anyAIAgent.GetService<IChatClient>();
// use the chat client from here. |
|
Thanks Roger Barreto (@rogerbarreto), the compatibility concern is fair - that's also why the remarks document what each agent type honors, and why I asked in the PR description if this should go under But I don't think
If you prefer to keep the surface constrained while the shape settles, I can add |
…ient Adds AIAgentExtensions.AsIChatClient(this AIAgent, AgentSession? = null), backed by an internal AIAgentChatClient adapter, so any agent can be used where Microsoft.Extensions.AI.IChatClient is accepted (e.g. as an evaluation judge in Microsoft.Extensions.AI.Evaluation). - Maps GetResponseAsync/GetStreamingResponseAsync to RunAsync/ RunStreamingAsync, reusing the AgentResponse converters; streaming honors WithCancellation via EnumeratorCancellation. - Carries ChatOptions through ChatClientAgentRunOptions; ResponseFormat is also copied to the base AgentRunOptions so structured output works for non-ChatClient agents. - GetService returns the adapter for unkeyed IChatClient requests (preserving the full agent pipeline), forwards everything else to the agent, and synthesizes ChatClientMetadata as a fallback. - Stateless per call by default; optional bound session mirrors AsAIFunction semantics. - 24 unit tests incl. ChatClientAgent end-to-end, structured output, GetService precedence, and cancellation propagation. Addresses microsoft#3496
The repo's own ChatClientExtensions is declared in the Microsoft.Extensions.AI namespace, so the fully-qualified cref never disambiguated anything; CI's dotnet format (SDK 10.0.400) flags it.
33329bb to
03e9539
Compare
|
Thanks for the contribution Tomas Rampas (@tomas-rampas). This looks good! |
Motivation & Context
More and more .NET APIs accept
Microsoft.Extensions.AI.IChatClient. This change lets anAIAgentbe used wherever anIChatClientis accepted — the motivating scenario from the issue thread is using an agent as the LLM behindMicrosoft.Extensions.AI.Evaluationjudges. Implements the proposal I claimed on #3496 (API shape posted there for early feedback).Description & Review Guide
What are the major changes?
AIAgentExtensions.AsIChatClient(this AIAgent agent, AgentSession? session = null, string? conversationId = null, bool allowNonChatClientAgents = false)inMicrosoft.Agents.AI, marked[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]per review — mirrors the siblingAsAIFunction(..., AgentSession?)and theAsIChatClientnaming already used by the provider-client adapters in this repo. The optionalconversationIdsets the id a session-bound client reports (validated non-blank, non-reserved); otherwise a per-instance id is generated.InvalidOperationExceptionunless the agent is aChatClientAgentor exposes one through an unkeyedGetService<ChatClientAgent>()request, which is exactly the "honorsChatClientAgentRunOptions" question the adapter depends on. Decorators built onDelegatingAIAgentforward the request, so they pass.allowNonChatClientAgents: truewraps any other agent (A2A, Copilot Studio, GitHub Copilot, workflow-hosted, custom) with the limitations spelled out in the parameter doc: onlyChatOptions.ResponseFormatsurvives, the rest ofChatOptionsis ignored, and the client is only as faithful as the agent's ownRunAsync. The Purview middleware agent does not forwardGetService, so it needs the opt-in for now.internal sealed class AIAgentChatClient : IChatClient(src/Microsoft.Agents.AI/ChatClient/AIAgentChatClient.cs):GetResponseAsync→agent.RunAsync(...)→ the existingAgentResponse.AsChatResponse()converter (rawChatResponsepass-through preserved in stateless mode when the raw response carries no conversation id, so usage and identity survive forChatClientAgent).GetStreamingResponseAsyncvalidates eagerly (throws before enumeration), then streams via a private[EnumeratorCancellation]iterator using the singularAsChatResponseUpdate()converter, soWithCancellation(...)tokens are honored.ChatClientAgent.CreateSessionAsync(conversationId).ChatOptions.ConversationIdthrowsInvalidOperationException, a blank one is treated as absent, and an id carried by the raw response is cleared on a copy. The client therefore never hands out an id it would reject, which matters becauseChatClientAgent(viaAsAIAgent()),FunctionInvokingChatClientandMessageInjectingChatClientall send a reported id back on the next call.ChatOptionsare carried throughChatClientAgentRunOptions(honored byChatClientAgent, ignored by agents that don't understand them);ResponseFormatis additionally copied onto the baseAgentRunOptionsso structured output (GetResponseAsync<T>) works for every agent type.GetService: unkeyedIChatClientrequests return the adapter (preserving the full agent pipeline — instructions, tools, context providers); everything else forwards to the agent;ChatClientMetadatais synthesized as a last-resort fallback.Disposeis a no-op; the caller owns the agent lifetime.tests/Microsoft.Agents.AI.UnitTests/AIAgentChatClientTests.cs, run on net10.0 and net472), includingChatClientAgentend-to-end (instructions/tools merge, the configured id reported over a service-managed conversation), the guard against real, decorated and fake agents, the stateless id rule on both paths including theAsIChatClient().AsAIAgent()two-turn round trip, M.E.AI structured-output through the adapter,GetServiceprecedence pinned against a realChatClientAgent, cancellation propagation on both paths, and a reflection guard that fails the build if a future M.E.AIChatResponsemember is missed by the copy used for id stamping (key guards mutation-tested).What is the impact of these changes? Purely additive: one new public method (gated
[Experimental]), declared in the fivePublicAPI.Unshipped.txtbaselines, no modified lines in existing code. The parameter added in the latest revision is source-compatible; it is binary-breaking only for code compiled against an earlier revision of this PR, which nothing shipped is, so no breaking-change label. Release build passes Package Validation with zero CP diagnostics. By default only chat-client-backed agents are accepted; other agents need an explicit opt-in. Default usage is stateless per call (full history each request); an optional bound session enables stateful use that signals history storage per theIChatClientcontract, with documented caveats (one in-flight request at a time, don't share across users).What do you want reviewers to focus on?
Should this API carryResolved — marked[Experimental]?[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]per westey (@westey-m)'s review.Guard for non-chat-client agents?Resolved — on by default withallowNonChatClientAgents: trueopt-out per Roger Barreto (@rogerbarreto)'s review; the probe isGetService<ChatClientAgent>().ChatOptions.ContinuationTokenis passed through rather than rejected up front; raw tokens don't round-trip forChatClientAgent(its token validation fails loudly). This is a deliberate choice — documented as unsupported in the remarks — so a future agent that accepts raw tokens isn't blocked. Can switch to fail-fast if preferred.ChatOptionspass-through means callers can add tools / append instructions for agents honoringChatClientAgentRunOptions— same capability the agent holder already has viaRunAsync; the remarks point untrusted-caller scenarios at theRejectRequestSettings/RunOptionsFactorypattern fromMicrosoft.Agents.AI.Hosting.OpenAI.Offered as follow-ups (kept out to keep this PR small): a sample mirroring
Agent_Step09_AsFunctionToolshowing an agent as an M.E.AI.Evaluation judge; additional tests (cancelled-token →OperationCanceledExceptionend-to-end, exception propagation unwrapped); a small issue forPurviewAgentnot forwardingGetService.Related Issue
Fixes #3496
No other open PR exists for this issue.
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.