.NET: ADR for unified dynamic agent resolution across AG-UI, Responses, and A2A#6643
.NET: ADR for unified dynamic agent resolution across AG-UI, Responses, and A2A#6643Ashutosh0x wants to merge 3 commits into
Conversation
…s, and A2A Proposes IAgentResolver interface and factory-delegate overloads for MapAGUI, MapOpenAIResponses, and MapA2AHttpJson to enable per-request agent resolution. Addresses the cross-channel consistency requirement raised by @javiercn on PR microsoft#3162 and fixes the AgentSessionStore singleton-capture scoping issue identified by @halllo. Refs: microsoft#3162, microsoft#2988, microsoft#2343
There was a problem hiding this comment.
Pull request overview
This PR adds a new Architecture Decision Record (ADR) proposing a unified approach to resolve AIAgent instances dynamically per request across the three .NET hosting channels (AG-UI, OpenAI Responses, A2A), addressing current endpoint-registration-time singleton capture that blocks multi-tenant routing and breaks scoped session store usage.
Changes:
- Introduces ADR-0029 documenting the problem (startup-time agent/session-store capture) and proposing a dual API:
IAgentResolver(DI-based) plus factory delegate overloads. - Specifies per-request
AgentSessionStoreresolution viaHttpContext.RequestServicesto support scoped/transient registrations. - Documents expected error-handling behavior (404 on null resolution, 500 on exceptions, startup error if missing resolver).
| public static IServiceCollection AddAgentResolver<TResolver>(this IServiceCollection services) | ||
| where TResolver : class, IAgentResolver | ||
| { | ||
| services.AddHttpContextAccessor(); | ||
| services.AddSingleton<IAgentResolver, TResolver>(); | ||
| return services; | ||
| } |
|
cc @javiercn — This ADR directly addresses the cross-channel consistency requirement you raised on #3162:
I analyzed all three endpoint builders ( Would love your architectural feedback, particularly on:
Happy to iterate based on your guidance! |
|
I don't feel strongly about what we do in this regard, provided we are consistent and that other folks like @DeagleGross and @ReubenBond @rogerbarreto or @westey-m are happy with the approach. They might have different opinions on alternative ways in which this can be done/achieved. That said, the IHttpContextAccessor pattern is not the end of the world, especially if the framework provided it. You can just wrap the agent, or the frameworks could simply put the HttpContext instance on the additional properties of the options, and you get to run your resolution logic within a delegating handler which you can already create with Use(...) |
|
Thanks @javiercn — really appreciate the feedback and the direction! You're right that the Option A:
|
Addresses Copilot review - singleton resolver cannot take scoped dependencies (DbContext, repositories). Scoped registration enables per-request resolution which is the core intent of this ADR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed Copilot's review — pushed This ensures resolvers that depend on scoped services ( Thanks for the catch @copilot! |
- Add Option 4: Use(...) middleware + HttpContext on additional properties (Javier's recommended approach) - Restructure Decision Outcome as phased approach: Phase 1: Expose HttpContext + document Use(...) pattern (minimal) Phase 2: Factory delegates + IAgentResolver (if demand warrants) - Add Maintainer Feedback section with Javier's direct quote - Expand consulted list: @DeagleGross, @ReubenBond, @rogerbarreto, @westey-m (as requested by @javiercn) - Add reference to DeagleGross's A2A hosting work (microsoft#3732) This revision prioritizes the approach Javier suggested: minimal framework changes using established ASP.NET Core patterns rather than introducing new abstractions.
ADR Revised — Incorporating @javiercn's FeedbackUpdated the ADR (commit What Changed
The revision prioritizes minimal framework changes using established ASP.NET Core patterns (middleware pipeline, Would love input from the team on whether Phase 1 captures the right scope. Happy to iterate further! |
DeagleGross
left a comment
There was a problem hiding this comment.
Hey, thanks for filing and working on this! Recently not being an active contributor to agent-framework, but placed a couple of comments below. I am sure @westey-m can provide more insights here.
| Define a shared `IAgentResolver` interface in the hosting core: | ||
|
|
||
| ```csharp | ||
| public interface IAgentResolver |
There was a problem hiding this comment.
I dont see this being much different from resolving AIAgent from the registered instances in the DI container. Default implementation of this will do basically same GetRequiredService(agentname) or similar. Is there any specific use-case where this is helping to solve some problem?
There was a problem hiding this comment.
Great question @DeagleGross! You are right that a default IAgentResolver would basically do GetRequiredService. The key difference is when and how the agent name is determined:
Today (DI only): The agent is resolved once at startup in MapAGUI(pattern, agent). You can register multiple named agents, but there is no hook to select between them per-request based on route data.
Where dynamic resolution adds value:
- Route-based - /agents/{agentId} where agentId comes from the URL at request time
- Claims-based - Different tenants get different agents based on JWT claims
- Database-driven - Agent definitions stored in a DB, not statically in DI
That said, per @javiercn feedback, I have revised the ADR to deprioritize IAgentResolver in favor of Phase 1: expose HttpContext on options and use Use(...) middleware. This achieves the same result without a new abstraction. IAgentResolver is now Phase 2 only if community demand warrants it. Does that align with your thinking?
|
|
||
| ### Option 2: Factory delegate overloads (PR #3162 approach) | ||
|
|
||
| Add `MapXxx` overloads accepting `Func<HttpContext, CancellationToken, ValueTask<AIAgent?>>`: |
There was a problem hiding this comment.
This makes sense, but ideally for demo-purposes can you please share the sample where overloads today dont allow you to do X, and with the new overload you can do it? That will be a "proof" of why those overloads are important to exist
There was a problem hiding this comment.
Absolutely! Here is a concrete before/after:
Today — you CANNOT do this:
`csharp
// Goal: serve different agents based on URL
// e.g. GET /agents/weather/ag-ui vs GET /agents/search/ag-ui
// This does NOT work — agent is captured at startup:
app.MapAGUI(/agents/{agentId}, weatherAgent);
// ^ weatherAgent is always used, {agentId} route param is ignored
`
Workaround today — brittle HttpContextRoutingAgent:
csharp // You have to create a wrapper agent that overrides every method: public class RoutingAgent : AIAgent { private readonly IHttpContextAccessor _accessor; public override async Task<AgentResponse> RunAsync(...) { var ctx = _accessor.HttpContext; var id = ctx.GetRouteValue(agentId); var real = await _repo.GetAgent(id); return await real.RunAsync(...); // delegate everything } // Must also override: RunStreamingAsync, CreateSessionAsync, // GetSessionAsync, SerializeSession (not async!), etc. }
With factory delegate overload — clean and simple:
csharp app.MapAGUI(/agents/{agentId}, async (context, ct) => { var agentId = context.GetRouteValue(agentId)?.ToString(); return await agentRepo.GetAgentAsync(agentId, ct); }); // Session store also resolved per-request automatically
The core problem: today the agent is a startup-time constant, but multi-tenant platforms need it to be a request-time variable. The workaround requires duplicating the entire AIAgent surface area.
Phase 1 Implementation PR CreatedI've opened #6659 with the actual implementation of the factory delegate overload for MapAGUI, following the approach discussed here. ADR (this PR) → Implementation (#6659) The implementation resolves agents and session stores per-request from |
Motivation and Context
This ADR addresses the cross-channel consistency requirement raised by @javiercn on #3162:
All three hosting channel endpoint builders (MapAGUI, MapOpenAIResponses, MapA2AHttpJson) currently resolve the AIAgent instance once at endpoint registration time. This singleton-capture prevents dynamic per-request agent resolution needed for multi-tenant platforms.
@halllo further identified that AgentSessionStore is also singleton-captured, breaking scoped/transient DI registrations.
Description
Proposes an IAgentResolver interface in the hosting core plus factory-delegate overloads for all three channel types:
The ADR analyzes 3 options (HttpContextRoutingAgent workaround, factory delegates only, IAgentResolver + factory dual API) and recommends the dual API approach.
References
Contribution Checklist