Skip to content

.NET: Add AsIChatClient extension to expose any AIAgent as an IChatClient - #7687

Open
Tomas Rampas (tomas-rampas) wants to merge 3 commits into
microsoft:mainfrom
tomas-rampas:issue-3496-asichatclient
Open

.NET: Add AsIChatClient extension to expose any AIAgent as an IChatClient#7687
Tomas Rampas (tomas-rampas) wants to merge 3 commits into
microsoft:mainfrom
tomas-rampas:issue-3496-asichatclient

Conversation

@tomas-rampas

@tomas-rampas Tomas Rampas (tomas-rampas) commented Aug 16, 2026

Copy link
Copy Markdown

Motivation & Context

More and more .NET APIs accept Microsoft.Extensions.AI.IChatClient. This change lets any AIAgent be used wherever an IChatClient is accepted — the motivating scenario from the issue thread is using an agent as the LLM behind Microsoft.Extensions.AI.Evaluation judges. Implements the proposal I claimed on #3496 (API shape posted there for early feedback).

Description & Review Guide

  • What are the major changes?

    • New extension method AIAgentExtensions.AsIChatClient(this AIAgent agent, AgentSession? session = null) in Microsoft.Agents.AI — mirrors the sibling AsAIFunction(..., AgentSession?) and the AsIChatClient naming already used by the provider-client adapters in this repo.
    • New internal sealed class AIAgentChatClient : IChatClient (src/Microsoft.Agents.AI/ChatClient/AIAgentChatClient.cs):
      • GetResponseAsyncagent.RunAsync(...) → the existing AgentResponse.AsChatResponse() converter (raw ChatResponse pass-through preserved, so ConversationId/usage survive for ChatClientAgent).
      • GetStreamingResponseAsync validates eagerly (throws before enumeration), then streams via a private [EnumeratorCancellation] iterator using the singular AsChatResponseUpdate() converter, so WithCancellation(...) tokens are honored.
      • ChatOptions are carried through ChatClientAgentRunOptions (honored by ChatClientAgent, gracefully ignored by agents that don't understand them); ResponseFormat is additionally copied onto the base AgentRunOptions so structured output (GetResponseAsync<T>) works for every agent type.
      • GetService: unkeyed IChatClient requests return the adapter (preserving the full agent pipeline — instructions, tools, context providers); everything else forwards to the agent; ChatClientMetadata is synthesized as a last-resort fallback.
      • Dispose is a no-op; the caller owns the agent lifetime.
    • 24 unit tests (tests/Microsoft.Agents.AI.UnitTests/AIAgentChatClientTests.cs), including ChatClientAgent end-to-end (instructions/tools merge, ConversationId round-trip), M.E.AI structured-output through the adapter, GetService precedence pinned against a real ChatClientAgent, and cancellation propagation on both paths (mutation-tested: breaking token forwarding fails 4 tests).
  • What is the impact of these changes? Purely additive: one new public method, no modified lines in existing code. Release build passes Package Validation with zero CP diagnostics. Default usage is stateless per call (full history each request); an optional bound session enables stateful use with documented caveats (no concurrent use, don't share across users).

  • What do you want reviewers to focus on?

    1. Should this API carry [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]? Microsoft.Agents.AI is GA-validated (baseline 1.0.0), and the adapter has documented sharp edges (background/continuation responses unsupported; non-ChatClientAgent agents ignore most ChatOptions). The closest structural analogue (ChatStrategyExtensions.AsChatReducer) is gated; the sibling AsAIFunction is not. Happy to add the attribute if you prefer to let the shape settle — please advise.
    2. ChatOptions.ContinuationToken is passed through rather than rejected up front; raw tokens don't round-trip for ChatClientAgent (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.
    3. ChatOptions pass-through means callers can add tools / append instructions for agents honoring ChatClientAgentRunOptions — same capability the agent holder already has via RunAsync; the remarks point untrusted-caller scenarios at the RejectRequestSettings/RunOptionsFactory pattern from Microsoft.Agents.AI.Hosting.OpenAI.

    Offered as follow-ups (kept out to keep this PR small): a sample mirroring Agent_Step09_AsFunctionTool showing an agent as an M.E.AI.Evaluation judge; additional tests (cancelled-token → OperationCanceledException end-to-end, exception propagation unwrapped, caller ChatOptions non-mutation regression).

Related Issue

Fixes #3496

No other open PR exists for this issue.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an adapter allowing any .NET AIAgent to be consumed as an IChatClient.

Changes:

  • Adds the AsIChatClient extension 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.

…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
@tomas-rampas

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@microsoft-github-policy-service agree

@rogerbarreto

Copy link
Copy Markdown
Member

Not all AIAgents may be compatible with a IChatClient contract, ideally this should be limited to ChatClientAgents that already expose its internal IChatClient via the GetService<IChatClient>() method.

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.

@tomas-rampas

Copy link
Copy Markdown
Author

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 [Experimental].

But I don't think GetService<IChatClient>() can cover what #3496 asks for, for two reasons:

  1. For agents other than ChatClientAgent it just returns null - base AIAgent.GetService only returns the agent itself (AIAgent.cs:118-124), and for example GitHubCopilotAgent does not expose any inner IChatClient. The motivating scenario in the issue is using Copilot SDK agents as Microsoft.Extensions.AI.Evaluation judges, so exactly these agents need it most.

  2. For ChatClientAgent it returns the inner client (ChatClientAgent.cs:408), so agent instructions, agent-level tools, context providers and session handling are all skipped. It is a useful escape hatch, but it is a different operation - GetService unwraps the agent, while AsIChatClient() runs the whole agent behind the IChatClient interface, same way as AsAIFunction() does it for tools.

If you prefer to keep the surface constrained while the shape settles, I can add [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)], or narrow the extension to ChatClientAgent only - but the second option would not cover the Copilot SDK case. Shyam N (@shyamnamboodiripad) does your evaluation scenario need also non-ChatClientAgent agents? Whatever direction the area owners prefer, the change is small on my side.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

.NET Usage: [Issues, PRs], Target: .Net

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET: [Feature]: AIAgent.AsIChatClient

3 participants