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 Issues related to the .NET codebase 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 Issues related to the .NET codebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants