mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
401a552735
* feat: Add DelegatingAgentSessionStore Add helper for decorator pattern for AgentSessionStore * feat: Add UserIdentityScopedSessionStore Add support for using the ASP.Net Core ambient `ClaimsIdentity` User, along with a user-specified claim type to scope the session store based on authenticated identity. * fix: Harden scope mapping * fix: Add UserIdentityScopeSessionStoreOptions to avoid future breaking changes * Split UserIdentityScopedSessionStore into a separate IsolationKeyProvider and IsolationKeyScopedSessionStore * Add GetService<>() capabilities to interrogate AgentSessionStore delegation chain * Harden default for A2A hosting by using an IsolationKeyScopedAgentSessionStore when no store is available. * Pipe isolation through Hosting helper extension methods * Add comment to samples about adding SessionIsolationKeyProvider * Fix isolation key provider nullability semantics * fix A2A defaults * fixup * remove unneeded keyProvider requirement test * Add trust-model XML docs to AgentSessionStore, InMemoryAgentSessionStore, MapAGUI, A2A entry points Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/e466c53a-faad-40a8-8b5f-83cf0dce0b1d Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com> * fix: Switch ClaimsBasedIsolationKeyProvider to be Singleton * matches HttpContextAccessor and related MAF services * release: Ensure new project is in the release filter * fixup: Integraitaon tests --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>
214 lines
11 KiB
C#
214 lines
11 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using A2A;
|
|
using Microsoft.Agents.AI;
|
|
using Microsoft.Agents.AI.Hosting;
|
|
using Microsoft.Agents.AI.Hosting.A2A;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Shared.DiagnosticIds;
|
|
|
|
namespace Microsoft.Extensions.DependencyInjection;
|
|
|
|
/// <summary>
|
|
/// Provides extension methods for registering A2A server instances in the dependency injection container.
|
|
/// </summary>
|
|
[Experimental(DiagnosticIds.Experiments.AIResponseContinuations)]
|
|
public static class A2AServerServiceCollectionExtensions
|
|
{
|
|
/// <summary>
|
|
/// Registers an <see cref="A2AServer"/> in the dependency injection container, keyed by the agent name
|
|
/// specified in the <paramref name="agentBuilder"/>. This method only registers the server; to expose it
|
|
/// as an HTTP endpoint, call one of the <c>MapA2AHttpJson</c> or <c>MapA2AJsonRpc</c> endpoint mapping
|
|
/// methods during application startup.
|
|
/// </summary>
|
|
/// <param name="agentBuilder">The agent builder whose name identifies the agent.</param>
|
|
/// <param name="configureOptions">An optional callback to configure <see cref="A2AServerRegistrationOptions"/>.</param>
|
|
/// <returns>The <paramref name="agentBuilder"/> for chaining.</returns>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <strong>Trust model.</strong> The A2A <c>contextId</c> arrives from the wire
|
|
/// and is treated as a chain-resume identifier — <em>not</em> as an authorization
|
|
/// token. The <see cref="AgentSessionStore"/> contract carries no principal/owner
|
|
/// dimension, so when a persistent store is registered any caller who knows or
|
|
/// guesses another caller's <c>contextId</c> can resume that other caller's
|
|
/// persisted thread. Hosts that serve more than one user must compose a principal
|
|
/// dimension into the lookup key — typically by calling
|
|
/// <c>UseClaimsBasedSessionIsolation(...)</c> from
|
|
/// <c>Microsoft.Agents.AI.Hosting.AspNetCore</c> (or by registering a custom
|
|
/// <see cref="SessionIsolationKeyProvider"/>). When no isolation provider is
|
|
/// registered, behavior is unchanged — the bare <c>contextId</c> is used as the
|
|
/// conversation identifier, which is appropriate for first-run / single-user /
|
|
/// prototyping scenarios but unsafe for multi-user hosts.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static IHostedAgentBuilder AddA2AServer(this IHostedAgentBuilder agentBuilder, Action<A2AServerRegistrationOptions>? configureOptions = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(agentBuilder);
|
|
|
|
agentBuilder.ServiceCollection.AddA2AServer(agentBuilder.Name, configureOptions);
|
|
|
|
return agentBuilder;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers an <see cref="A2AServer"/> in the dependency injection container, keyed by the specified
|
|
/// agent name. This method only registers the server; to expose it as an HTTP endpoint, call one of the
|
|
/// <c>MapA2AHttpJson</c> or <c>MapA2AJsonRpc</c> endpoint mapping methods during application startup.
|
|
/// </summary>
|
|
/// <param name="builder">The host application builder to configure.</param>
|
|
/// <param name="agentName">The name of the agent to create an A2A server for.</param>
|
|
/// <param name="configureOptions">An optional callback to configure <see cref="A2AServerRegistrationOptions"/>.</param>
|
|
/// <returns>The <paramref name="builder"/> for chaining.</returns>
|
|
/// <remarks>
|
|
/// See the trust-model remarks on <see cref="AddA2AServer(IHostedAgentBuilder, Action{A2AServerRegistrationOptions}?)"/>
|
|
/// for guidance on multi-user hosts (the wire <c>contextId</c> is a chain-resume
|
|
/// identifier, not an authorization token; multi-user hosts must compose a
|
|
/// principal dimension via <c>UseClaimsBasedSessionIsolation(...)</c> or a custom
|
|
/// <see cref="SessionIsolationKeyProvider"/>).
|
|
/// </remarks>
|
|
public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, string agentName, Action<A2AServerRegistrationOptions>? configureOptions = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(builder);
|
|
|
|
builder.Services.AddA2AServer(agentName, configureOptions);
|
|
|
|
return builder;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers an <see cref="A2AServer"/> in the dependency injection container for the specified
|
|
/// <see cref="AIAgent"/> instance, keyed by the agent's <see cref="AIAgent.Name"/>. This method only
|
|
/// registers the server; to expose it as an HTTP endpoint, call one of the <c>MapA2AHttpJson</c> or
|
|
/// <c>MapA2AJsonRpc</c> endpoint mapping methods during application startup.
|
|
/// </summary>
|
|
/// <param name="builder">The host application builder to configure.</param>
|
|
/// <param name="agent">The agent instance to create an A2A server for.</param>
|
|
/// <param name="configureOptions">An optional callback to configure <see cref="A2AServerRegistrationOptions"/>.</param>
|
|
/// <returns>The <paramref name="builder"/> for chaining.</returns>
|
|
/// <remarks>
|
|
/// See the trust-model remarks on <see cref="AddA2AServer(IHostedAgentBuilder, Action{A2AServerRegistrationOptions}?)"/>
|
|
/// for guidance on multi-user hosts (the wire <c>contextId</c> is a chain-resume
|
|
/// identifier, not an authorization token; multi-user hosts must compose a
|
|
/// principal dimension via <c>UseClaimsBasedSessionIsolation(...)</c> or a custom
|
|
/// <see cref="SessionIsolationKeyProvider"/>).
|
|
/// </remarks>
|
|
public static IHostApplicationBuilder AddA2AServer(this IHostApplicationBuilder builder, AIAgent agent, Action<A2AServerRegistrationOptions>? configureOptions = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(builder);
|
|
|
|
builder.Services.AddA2AServer(agent, configureOptions);
|
|
|
|
return builder;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers an <see cref="A2AServer"/> in the dependency injection container, keyed by the specified
|
|
/// agent name. This method only registers the server; to expose it as an HTTP endpoint, call one of the
|
|
/// <c>MapA2AHttpJson</c> or <c>MapA2AJsonRpc</c> endpoint mapping methods during application startup.
|
|
/// </summary>
|
|
/// <param name="services">The service collection to add the A2A server to.</param>
|
|
/// <param name="agentName">The name of the agent to create an A2A server for.</param>
|
|
/// <param name="configureOptions">An optional callback to configure <see cref="A2AServerRegistrationOptions"/>.</param>
|
|
/// <returns>The <paramref name="services"/> for chaining.</returns>
|
|
/// <remarks>
|
|
/// See the trust-model remarks on <see cref="AddA2AServer(IHostedAgentBuilder, Action{A2AServerRegistrationOptions}?)"/>
|
|
/// for guidance on multi-user hosts (the wire <c>contextId</c> is a chain-resume
|
|
/// identifier, not an authorization token; multi-user hosts must compose a
|
|
/// principal dimension via <c>UseClaimsBasedSessionIsolation(...)</c> or a custom
|
|
/// <see cref="SessionIsolationKeyProvider"/>).
|
|
/// </remarks>
|
|
public static IServiceCollection AddA2AServer(this IServiceCollection services, string agentName, Action<A2AServerRegistrationOptions>? configureOptions = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(services);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(agentName);
|
|
|
|
A2AServerRegistrationOptions? options = null;
|
|
if (configureOptions is not null)
|
|
{
|
|
options = new A2AServerRegistrationOptions();
|
|
configureOptions(options);
|
|
}
|
|
|
|
services.AddKeyedSingleton(agentName, (sp, _) =>
|
|
{
|
|
var agent = sp.GetRequiredKeyedService<AIAgent>(agentName);
|
|
return CreateA2AServer(sp, agent, options);
|
|
});
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers an <see cref="A2AServer"/> in the dependency injection container for the specified
|
|
/// <see cref="AIAgent"/> instance, keyed by the agent's <see cref="AIAgent.Name"/>. This method only
|
|
/// registers the server; to expose it as an HTTP endpoint, call one of the <c>MapA2AHttpJson</c> or
|
|
/// <c>MapA2AJsonRpc</c> endpoint mapping methods during application startup.
|
|
/// </summary>
|
|
/// <param name="services">The service collection to add the A2A server to.</param>
|
|
/// <param name="agent">The agent instance to create an A2A server for.</param>
|
|
/// <param name="configureOptions">An optional callback to configure <see cref="A2AServerRegistrationOptions"/>.</param>
|
|
/// <returns>The <paramref name="services"/> for chaining.</returns>
|
|
/// <remarks>
|
|
/// See the trust-model remarks on <see cref="AddA2AServer(IHostedAgentBuilder, Action{A2AServerRegistrationOptions}?)"/>
|
|
/// for guidance on multi-user hosts (the wire <c>contextId</c> is a chain-resume
|
|
/// identifier, not an authorization token; multi-user hosts must compose a
|
|
/// principal dimension via <c>UseClaimsBasedSessionIsolation(...)</c> or a custom
|
|
/// <see cref="SessionIsolationKeyProvider"/>).
|
|
/// </remarks>
|
|
public static IServiceCollection AddA2AServer(this IServiceCollection services, AIAgent agent, Action<A2AServerRegistrationOptions>? configureOptions = null)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(services);
|
|
ArgumentNullException.ThrowIfNull(agent);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(agent.Name, nameof(agent) + "." + nameof(agent.Name));
|
|
|
|
A2AServerRegistrationOptions? options = null;
|
|
if (configureOptions is not null)
|
|
{
|
|
options = new A2AServerRegistrationOptions();
|
|
configureOptions(options);
|
|
}
|
|
|
|
services.AddKeyedSingleton(agent.Name, (sp, _) => CreateA2AServer(sp, agent, options));
|
|
|
|
return services;
|
|
}
|
|
|
|
private static A2AServer CreateA2AServer(IServiceProvider serviceProvider, AIAgent agent, A2AServerRegistrationOptions? options)
|
|
{
|
|
var agentHandler = serviceProvider.GetKeyedService<IAgentHandler>(agent.Name);
|
|
if (agentHandler is null)
|
|
{
|
|
var agentSessionStore = serviceProvider.GetKeyedService<AgentSessionStore>(agent.Name);
|
|
var runMode = options?.AgentRunMode ?? AgentRunMode.DisallowBackground;
|
|
|
|
// Ensure that we have an IsolationKeyScopedAgentSessionStore registered.
|
|
var isolationKeyProvider = serviceProvider.GetService<SessionIsolationKeyProvider>();
|
|
if (agentSessionStore?.GetService<IsolationKeyScopedAgentSessionStore>() is null)
|
|
{
|
|
agentSessionStore ??= new InMemoryAgentSessionStore();
|
|
agentSessionStore = new IsolationKeyScopedAgentSessionStore(agentSessionStore, isolationKeyProvider, new() { Strict = isolationKeyProvider != null });
|
|
}
|
|
|
|
var hostAgent = new AIHostAgent(
|
|
innerAgent: agent,
|
|
sessionStore: agentSessionStore);
|
|
|
|
agentHandler = new A2AAgentHandler(hostAgent, runMode);
|
|
}
|
|
|
|
var loggerFactory = serviceProvider.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
|
|
var taskStore = serviceProvider.GetKeyedService<ITaskStore>(agent.Name) ?? new InMemoryTaskStore();
|
|
|
|
return new A2AServer(
|
|
agentHandler,
|
|
taskStore,
|
|
new ChannelEventNotifier(),
|
|
loggerFactory.CreateLogger<A2AServer>(),
|
|
options?.ServerOptions);
|
|
}
|
|
}
|