From 474fa31832b00fd6cde9f56923b949795f1931de Mon Sep 17 00:00:00 2001 From: Ashutosh0x Date: Sat, 20 Jun 2026 14:51:16 +0530 Subject: [PATCH 1/3] .NET: ADR for unified dynamic agent resolution across AG-UI, Responses, 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 #3162 and fixes the AgentSessionStore singleton-capture scoping issue identified by @halllo. Refs: #3162, #2988, #2343 --- .../0029-unified-dynamic-agent-resolution.md | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 docs/decisions/0029-unified-dynamic-agent-resolution.md diff --git a/docs/decisions/0029-unified-dynamic-agent-resolution.md b/docs/decisions/0029-unified-dynamic-agent-resolution.md new file mode 100644 index 0000000000..0c60df2d6d --- /dev/null +++ b/docs/decisions/0029-unified-dynamic-agent-resolution.md @@ -0,0 +1,223 @@ +--- +status: proposed +contact: Ashutosh0x +date: 2026-06-20 +consulted: "@javiercn, @TheEagleByte, @halllo, @Davidlmkh" +--- + +# Unified Dynamic Agent Resolution Across AG-UI, OpenAI Responses, and A2A Endpoints + +## Context and Problem Statement + +All three hosting channel endpoint builders (`MapAGUI`, `MapOpenAIResponses`, `MapA2AHttpJson`) currently resolve the `AIAgent` instance and its `AgentSessionStore` **once at endpoint registration time** (app startup). This singleton-capture pattern prevents dynamic per-request agent resolution, which is required for multi-tenant agent platforms where route parameters (e.g., `/agents/{agentId}`) determine which agent handles each request. + +Community contributors have proposed factory-delegate overloads for AG-UI (#3162, #2343), but the maintainers (@javiercn) correctly noted that this pattern must be applied consistently across all three channel types simultaneously — not just AG-UI. + +### Current singleton-capture pattern (all three channels) + +```csharp +// AG-UI — AGUIEndpointRouteBuilderExtensions.cs:105 +var agentSessionStore = endpoints.ServiceProvider.GetKeyedService(aiAgent.Name); +var hostAgent = new AIHostAgent(aiAgent, agentSessionStore); + +// Responses — EndpointRouteBuilderExtensions.Responses.cs:71 +var executor = new AIAgentResponseExecutor(agent); + +// A2A — A2AEndpointRouteBuilderExtensions.cs:69 +var a2aServer = endpoints.ServiceProvider.GetKeyedService(agentName); +``` + +### Pain points identified by the community + +1. **No dynamic routing** — Cannot serve different agents based on route parameters (#2988, #3162) +2. **AgentSessionStore scoping broken** — Even scoped/transient session stores are captured as singletons at startup (@halllo, #3162 comment) +3. **HttpContextRoutingAgent workaround is brittle** — Requires overriding 5+ methods, couples agent logic to HTTP transport, `SerializeSession()` was not async (@halllo, @TheEagleByte) +4. **Forces per-agent infrastructure** — Without dynamic resolution, multi-agent hosts need separate endpoints per agent or fork the repo (@Davidlmkh) + +## Decision Drivers + +- **Consistency**: The same pattern must work across AG-UI, OpenAI Responses, and A2A +- **ASP.NET Core idiom**: Follow established patterns (`AddDbContext()`, `AddAuthentication()`, minimal API delegates) +- **Separation of concerns**: Agent resolution is a routing/infrastructure concern, not an agent behavior concern +- **Backward compatibility**: Existing single-agent `MapXxx(pattern, agent)` overloads must continue to work +- **Session store scoping**: Must support per-request session store resolution, not singleton capture + +## Considered Options + +### Option 1: HttpContextRoutingAgent (current workaround) + +A delegating `AIAgent` subclass that uses `IHttpContextAccessor` to resolve the real agent at runtime. + +**Pros**: Works today without framework changes +**Cons**: Couples agents to HTTP, requires overriding all virtual methods, session store still singleton-captured, reduces cohesion + +### Option 2: Factory delegate overloads (PR #3162 approach) + +Add `MapXxx` overloads accepting `Func>`: + +```csharp +app.MapAGUI("/agents/{agentId}", async (context, ct) => +{ + var agentId = context.GetRouteValue("agentId")?.ToString(); + return await agentRepository.GetAgentByIdAsync(agentId, ct); +}); +``` + +**Pros**: Familiar minimal API pattern, maximum flexibility +**Cons**: Channel-specific implementation needed for each `MapXxx` + +### Option 3: `IAgentResolver` interface with DI registration (recommended) + +Define a shared `IAgentResolver` interface in the hosting core: + +```csharp +public interface IAgentResolver +{ + ValueTask ResolveAgentAsync(HttpContext context, CancellationToken cancellationToken = default); +} +``` + +Registration: +```csharp +builder.Services.AddAgentResolver(); +``` + +Endpoint mapping (no agent parameter): +```csharp +app.MapAGUI("/agents/{agentId}"); +app.MapOpenAIResponses("/agents/{agentId}/v1/responses"); +app.MapA2AHttpJson("/agents/{agentId}/a2a"); +``` + +Each `MapXxx` overload resolves the agent per-request via `IAgentResolver` from DI. + +**Pros**: Single interface for all channels, DI-native, testable, session stores resolved per-request +**Cons**: New abstraction to learn + +## Decision Outcome + +Chosen option: **Option 3 (IAgentResolver) combined with Option 2 (factory delegate)** as the dual-API approach. + +### Implementation plan + +#### 1. Shared `IAgentResolver` interface (new, in `Microsoft.Agents.AI.Hosting`) + +```csharp +namespace Microsoft.Agents.AI.Hosting; + +/// +/// Resolves an dynamically at request time. +/// Implementations can use route data, headers, claims, or any +/// request-scoped information to select the appropriate agent. +/// +public interface IAgentResolver +{ + /// + /// Resolves an agent for the current request. + /// Returns null if no agent matches, causing a 404 response. + /// + ValueTask ResolveAgentAsync( + HttpContext context, + CancellationToken cancellationToken = default); +} +``` + +#### 2. DI registration helper (new, in each hosting package) + +```csharp +public static IServiceCollection AddAgentResolver(this IServiceCollection services) + where TResolver : class, IAgentResolver +{ + services.AddHttpContextAccessor(); + services.AddSingleton(); + return services; +} +``` + +#### 3. Per-request agent + session store resolution + +The key fix: resolve `AgentSessionStore` per-request from `HttpContext.RequestServices` instead of capturing at startup: + +```csharp +// Inside the endpoint delegate (per-request): +var agent = await resolver.ResolveAgentAsync(context, cancellationToken); +if (agent is null) return Results.NotFound(); + +// Resolve session store from the request's DI scope (not app-level) +var agentSessionStore = context.RequestServices.GetKeyedService(agent.Name) + ?? new NoopAgentSessionStore(); +``` + +#### 4. Factory delegate overloads (convenience API) + +```csharp +// AG-UI +public static IEndpointConventionBuilder MapAGUI( + this IEndpointRouteBuilder endpoints, + string pattern, + Func> agentFactory); + +// OpenAI Responses +public static IEndpointConventionBuilder MapOpenAIResponses( + this IEndpointRouteBuilder endpoints, + string? responsesPath, + Func> agentFactory); + +// A2A +public static IEndpointConventionBuilder MapA2AHttpJson( + this IEndpointRouteBuilder endpoints, + string path, + Func> agentFactory); +``` + +#### 5. Error handling + +| Scenario | Behavior | +|:---|:---| +| Factory/resolver returns `null` | 404 Not Found | +| Factory/resolver throws | 500 Internal Server Error + log | +| No resolver registered, no agent parameter | `InvalidOperationException` at startup | + +### Usage examples + +#### Route-based multi-tenant + +```csharp +builder.Services.AddAgentResolver(); + +app.MapAGUI("/agents/{agentId}"); +app.MapOpenAIResponses("/agents/{agentId}/v1/responses"); +app.MapA2AHttpJson("/agents/{agentId}/a2a"); + +public class RouteBasedAgentResolver(IAgentRepository repo) : IAgentResolver +{ + public async ValueTask ResolveAgentAsync(HttpContext context, CancellationToken ct) + { + var agentId = context.GetRouteValue("agentId")?.ToString(); + return string.IsNullOrEmpty(agentId) ? null : await repo.GetAgentAsync(agentId, ct); + } +} +``` + +#### Inline factory (quick prototyping) + +```csharp +app.MapAGUI("/agents/{agentId}", async (context, ct) => +{ + var agentId = context.GetRouteValue("agentId")?.ToString(); + return agentId switch + { + "weather" => weatherAgent, + "search" => searchAgent, + _ => null // 404 + }; +}); +``` + +## References + +- #3162 — .NET: Support dynamic agent resolution in AG-UI endpoints (@TheEagleByte) +- #2343 — Earlier dynamic resolution PR (@halllo) +- #2988 — Original issue requesting dynamic agent selection +- @javiercn's comment: "this needs to be applied not only to AG-UI but to Open AI responses and A2A" +- @halllo's `AgentSessionStore` scoping analysis (Feb 2026) From ee49e2b59fdea15fd1174bf1dfbc731a68e69ddc Mon Sep 17 00:00:00 2001 From: Ashutosh0x Date: Sat, 20 Jun 2026 16:09:04 +0530 Subject: [PATCH 2/3] fix: register IAgentResolver as scoped instead of singleton 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> --- docs/decisions/0029-unified-dynamic-agent-resolution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/decisions/0029-unified-dynamic-agent-resolution.md b/docs/decisions/0029-unified-dynamic-agent-resolution.md index 0c60df2d6d..fb76359ee2 100644 --- a/docs/decisions/0029-unified-dynamic-agent-resolution.md +++ b/docs/decisions/0029-unified-dynamic-agent-resolution.md @@ -129,7 +129,7 @@ public static IServiceCollection AddAgentResolver(this IServiceCollec where TResolver : class, IAgentResolver { services.AddHttpContextAccessor(); - services.AddSingleton(); + services.AddScoped(); return services; } ``` From a04a40feafac3418ce7dba008b20e749c1a150e8 Mon Sep 17 00:00:00 2001 From: Ashutosh0x Date: Sat, 20 Jun 2026 16:41:45 +0530 Subject: [PATCH 3/3] refactor: revise ADR based on @javiercn's architectural feedback - 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 (#3732) This revision prioritizes the approach Javier suggested: minimal framework changes using established ASP.NET Core patterns rather than introducing new abstractions. --- .../0029-unified-dynamic-agent-resolution.md | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/docs/decisions/0029-unified-dynamic-agent-resolution.md b/docs/decisions/0029-unified-dynamic-agent-resolution.md index fb76359ee2..4056ca0d8d 100644 --- a/docs/decisions/0029-unified-dynamic-agent-resolution.md +++ b/docs/decisions/0029-unified-dynamic-agent-resolution.md @@ -2,7 +2,8 @@ status: proposed contact: Ashutosh0x date: 2026-06-20 -consulted: "@javiercn, @TheEagleByte, @halllo, @Davidlmkh" +consulted: "@javiercn, @DeagleGross, @ReubenBond, @rogerbarreto, @westey-m, @TheEagleByte, @halllo, @Davidlmkh" +revised: 2026-06-20T16:40:00+05:30 --- # Unified Dynamic Agent Resolution Across AG-UI, OpenAI Responses, and A2A Endpoints @@ -66,6 +67,47 @@ app.MapAGUI("/agents/{agentId}", async (context, ct) => **Pros**: Familiar minimal API pattern, maximum flexibility **Cons**: Channel-specific implementation needed for each `MapXxx` +### Option 4: Middleware pipeline with `Use(...)` + HttpContext on additional properties (@javiercn's suggestion) + +Surface `HttpContext` on the agent options' additional properties, and use the existing ASP.NET Core middleware pipeline (`Use(...)`) for resolution logic: + +```csharp +app.MapAGUI("/agents/{agentId}", defaultAgent) + .Use(async (context, next) => + { + var httpContext = context.HttpContext; + var agentId = httpContext.GetRouteValue("agentId")?.ToString(); + + // Resolve the actual agent from DI or a repository + var resolver = httpContext.RequestServices.GetRequiredService(); + var resolvedAgent = await resolver.GetAgentAsync(agentId); + + // Place the resolved agent on HttpContext.Items for downstream use + httpContext.Items["ResolvedAgent"] = resolvedAgent; + + // Also resolve session store per-request (fixes singleton-capture) + var sessionStore = httpContext.RequestServices + .GetKeyedService(resolvedAgent?.Name); + httpContext.Items["AgentSessionStore"] = sessionStore; + + await next(context); + }); +``` + +Alternatively, the framework could expose `HttpContext` on the options/additional properties directly, allowing resolution within a custom `DelegatingHandler` via `Use(...)`: + +```csharp +// Framework-level change: expose HttpContext in agent options +public class AgentRequestOptions +{ + public HttpContext? HttpContext { get; set; } + public IDictionary AdditionalProperties { get; } = new Dictionary(); +} +``` + +**Pros**: No new abstractions needed, uses existing ASP.NET Core middleware pipeline, developers already know this pattern, `IHttpContextAccessor` is well-established +**Cons**: Initial `agent` parameter still required at registration (even if overridden at runtime), resolution logic duplicated unless wrapped in shared middleware + ### Option 3: `IAgentResolver` interface with DI registration (recommended) Define a shared `IAgentResolver` interface in the hosting core: @@ -94,9 +136,32 @@ Each `MapXxx` overload resolves the agent per-request via `IAgentResolver` from **Pros**: Single interface for all channels, DI-native, testable, session stores resolved per-request **Cons**: New abstraction to learn +## Maintainer Feedback + +> **@javiercn** (June 20, 2026): "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. 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(...)`" + +Key takeaways from maintainer feedback: +1. **Consistency across channels** is the hard requirement — the specific mechanism is flexible +2. **`IHttpContextAccessor` + `Use(...)`** is an acceptable lightweight approach +3. **Exposing `HttpContext` on options/additional properties** would be a minimal framework change that unblocks dynamic resolution +4. Additional maintainer input requested from @DeagleGross (A2A hosting), @ReubenBond (Orleans/distributed), @rogerbarreto (.NET core), @westey-m (VectorData/Agent Framework) + ## Decision Outcome -Chosen option: **Option 3 (IAgentResolver) combined with Option 2 (factory delegate)** as the dual-API approach. +Revised recommendation: **Phased approach** incorporating @javiercn's feedback. + +### Phase 1 (minimal, non-breaking) — Recommended immediate action + +- Expose `HttpContext` on the options/additional properties for all three channels +- Document the `Use(...)` middleware pattern for per-request agent resolution +- Fix per-request `AgentSessionStore` resolution from `HttpContext.RequestServices` + +This requires minimal framework changes and uses patterns ASP.NET Core developers already know. + +### Phase 2 (convenience, if community demand warrants) + +- Add factory delegate overloads for `MapXxx` (Option 2) +- Consider `IAgentResolver` as an optional DI-registered convenience layer (Option 3) ### Implementation plan @@ -219,5 +284,7 @@ app.MapAGUI("/agents/{agentId}", async (context, ct) => - #3162 — .NET: Support dynamic agent resolution in AG-UI endpoints (@TheEagleByte) - #2343 — Earlier dynamic resolution PR (@halllo) - #2988 — Original issue requesting dynamic agent selection -- @javiercn's comment: "this needs to be applied not only to AG-UI but to Open AI responses and A2A" +- @javiercn's [feedback on PR #6643](https://github.com/microsoft/agent-framework/pull/6643#issuecomment-4757378039): recommending `Use(...)` middleware + HttpContext on additional properties +- @javiercn's comment on #3162: "this needs to be applied not only to AG-UI but to Open AI responses and A2A" - @halllo's `AgentSessionStore` scoping analysis (Feb 2026) +- @DeagleGross's A2A hosting task support (#3732) — relevant for A2A channel consistency