Skip to content

.NET: Add factory delegate overload to MapAGUI for per-request agent resolution#6659

Open
Ashutosh0x wants to merge 2 commits into
microsoft:mainfrom
Ashutosh0x:feature/dynamic-agent-resolution-phase1
Open

.NET: Add factory delegate overload to MapAGUI for per-request agent resolution#6659
Ashutosh0x wants to merge 2 commits into
microsoft:mainfrom
Ashutosh0x:feature/dynamic-agent-resolution-phase1

Conversation

@Ashutosh0x

Copy link
Copy Markdown

Motivation and Context

This PR implements Phase 1 of ADR-0029 — the factory delegate approach recommended by @javiercn for dynamic per-request agent resolution.

The problem (confirmed in source code)

All three hosting channels currently resolve the AIAgent and AgentSessionStore once at startup via endpoints.ServiceProvider:

// AGUIEndpointRouteBuilderExtensions.cs:59 — agent captured at startup
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);

// AGUIEndpointRouteBuilderExtensions.cs:105 — session store captured at startup  
var agentSessionStore = endpoints.ServiceProvider.GetKeyedService<AgentSessionStore>(aiAgent.Name);

// AGUIEndpointRouteBuilderExtensions.cs:115 — both captured in closure
var hostAgent = new AIHostAgent(aiAgent, agentSessionStore);

This singleton-capture prevents:

The solution

New MapAGUI overload accepting a factory delegate:

app.MapAGUI(/agents/{agentId}, async (HttpContext context, CancellationToken ct) =>
{
    var agentId = context.GetRouteValue(agentId)?.ToString();
    return agentId switch
    {
        weather => weatherAgent,
        search => searchAgent,
        _ => null // 404
    };
});

Key implementation details:

  • Agent resolved per-request via factory delegate (not at startup)
  • AgentSessionStore resolved per-request from HttpContext.RequestServices (fixes singleton-capture)
  • IsolationKeyScopedAgentSessionStore wrapping applied per-request
  • Returns 404 when factory returns null, 400 for null input body
  • Full XML documentation with trust model cross-reference
  • 100% backward compatible — existing MapAGUI(pattern, agent) overloads unchanged

Design decisions

Following @javiercn's guidance on #6643:

  • Uses the familiar minimal API factory pattern (no new abstractions)
  • Per-request resolution via HttpContext.RequestServices (not IHttpContextAccessor)
  • Consistent with how ASP.NET Core handles AddDbContext, AddAuthentication, etc.

Scope

This PR covers AG-UI only as the first channel. OpenAI Responses and A2A have different architectures (service-based vs delegate-based) and will follow in subsequent PRs once the pattern is validated.

References

Contribution Checklist

cc @javiercn @DeagleGross @westey-m @rogerbarreto

…resolution

Add a new MapAGUI overload that accepts a
Func<HttpContext, CancellationToken, ValueTask<AIAgent?>> factory
delegate, enabling dynamic per-request agent resolution for
multi-tenant AG-UI hosting scenarios.

Key changes:
- New MapAGUI(pattern, agentFactory) overload that resolves agents
  per-request instead of capturing at startup
- AgentSessionStore resolved per-request from
  HttpContext.RequestServices (fixes singleton-capture bug)
- IsolationKeyScopedAgentSessionStore wrapping applied per-request
- Returns 404 when factory returns null, 400 for null input
- Full XML documentation with trust model cross-reference

This implements Phase 1 of ADR-0029 for the AG-UI channel,
following the approach recommended by @javiercn (Use(...) middleware
pattern with per-request resolution).

Fixes microsoft#2988, microsoft#3162
Related: microsoft#2343, ADR PR microsoft#6643
Copilot AI review requested due to automatic review settings June 22, 2026 11:22
@moonbox3 moonbox3 added the .NET Usage: [Issues, PRs], Target: .Net label Jun 22, 2026

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 additive AG-UI hosting API for per-request agent resolution in the .NET ASP.NET Core AG-UI endpoint builder, avoiding startup-time singleton capture and enabling multi-tenant/dynamic routing scenarios.

Changes:

  • Adds a new MapAGUI(pattern, Func<HttpContext, CancellationToken, ValueTask<AIAgent?>> agentFactory) overload for per-request agent selection.
  • Resolves AgentSessionStore and SessionIsolationKeyProvider from HttpContext.RequestServices (per-request scope) instead of endpoints.ServiceProvider.
  • Mirrors the existing AG-UI execution pipeline (run options, streaming, SSE result, and post-stream session save) for the factory-based overload.

Comment on lines +198 to +205
public static IEndpointConventionBuilder MapAGUI(
this IEndpointRouteBuilder endpoints,
[StringSyntax("route")] string pattern,
Func<HttpContext, CancellationToken, ValueTask<AIAgent?>> agentFactory)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentNullException.ThrowIfNull(agentFactory);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch! Pushed 0131d01 addressing both review items:

  1. Tests added — 3 new unit tests for the factory delegate overload:
    • MapAGUI_WithFactoryDelegate_MapsEndpoint_AtSpecifiedPattern
    • MapAGUI_WithNullFactory_ThrowsArgumentNullException
    • MapAGUI_WithFactoryDelegate_AndNullEndpoints_ThrowsArgumentNullException

These follow the same Moq + xUnit pattern used by the existing overload tests.

Comment on lines +206 to +214
return endpoints.MapPost(pattern, async ([FromBody] RunAgentInput? input, HttpContext context, CancellationToken cancellationToken) =>
{
if (input is null)
{
return Results.BadRequest();
}

// Resolve agent per-request via factory delegate
var aiAgent = await agentFactory(context, cancellationToken).ConfigureAwait(false);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed! Extracted the shared execution pipeline into a private ExecuteAgentRequestAsync helper method. Both overloads now call into it:

  • Static overload: resolves agent/session at startup, then calls ExecuteAgentRequestAsync
  • Factory overload: resolves agent/session per-request, then calls ExecuteAgentRequestAsync

This ensures the message conversion, run options, streaming pipeline, and session save logic stay in sync when AG-UI behavior changes.

Address Copilot review feedback:
- Extract shared AG-UI execution pipeline into private
  ExecuteAgentRequestAsync helper (eliminates code duplication
  between static and factory MapAGUI overloads)
- Add unit tests for factory delegate overload:
  * MapAGUI_WithFactoryDelegate_MapsEndpoint_AtSpecifiedPattern
  * MapAGUI_WithNullFactory_ThrowsArgumentNullException
  * MapAGUI_WithFactoryDelegate_AndNullEndpoints_ThrowsArgumentNullException
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.

3 participants