Skip to content

Commit b517b5d

Browse files
Merge branch 'main' into gok/feat/nvidia
2 parents 116a1aa + 73eb00b commit b517b5d

23 files changed

Lines changed: 645 additions & 307 deletions

File tree

dotnet/samples/A2AClientServer/A2AServer/Program.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@ You specialize in handling queries related to logistics.
104104
throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentId must be provided");
105105
}
106106

107-
var a2aTaskManager = app.MapA2A(hostA2AAgent, path: "/", agentCard: hostA2AAgentCard);
108-
app.MapWellKnownAgentCard(a2aTaskManager, "/");
107+
var a2aTaskManager = app.MapA2A(
108+
hostA2AAgent,
109+
path: "/",
110+
agentCard: hostA2AAgentCard,
111+
taskManager => app.MapWellKnownAgentCard(taskManager, "/"));
109112

110113
await app.RunAsync();

dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"pirate",
2525
instructions: "You are a pirate. Speak like a pirate",
2626
description: "An agent that speaks like a pirate.",
27-
chatClientServiceKey: "chat-model");
27+
chatClientServiceKey: "chat-model")
28+
.WithInMemoryThreadStore();
2829

2930
builder.AddAIAgent("knights-and-knaves", (sp, key) =>
3031
{

dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/EndpointRouteBuilderExtensions.cs

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3+
using System;
34
using A2A;
45
using A2A.AspNetCore;
56
using Microsoft.Agents.AI;
7+
using Microsoft.Agents.AI.Hosting;
68
using Microsoft.Agents.AI.Hosting.A2A;
79
using Microsoft.AspNetCore.Builder;
810
using Microsoft.AspNetCore.Routing;
@@ -23,10 +25,21 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
2325
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
2426
/// <param name="path">The route group to use for A2A endpoints.</param>
2527
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
26-
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path)
28+
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path)
29+
=> endpoints.MapA2A(agentName, path, _ => { });
30+
31+
/// <summary>
32+
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
33+
/// </summary>
34+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
35+
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
36+
/// <param name="path">The route group to use for A2A endpoints.</param>
37+
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
38+
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
39+
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, Action<ITaskManager> configureTaskManager)
2740
{
2841
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
29-
return endpoints.MapA2A(agent, path);
42+
return endpoints.MapA2A(agent, path, configureTaskManager);
3043
}
3144

3245
/// <summary>
@@ -42,10 +55,27 @@ public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, string a
4255
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
4356
/// discovery mechanism.
4457
/// </remarks>
45-
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard)
58+
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard)
59+
=> endpoints.MapA2A(agentName, path, agentCard, _ => { });
60+
61+
/// <summary>
62+
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
63+
/// </summary>
64+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
65+
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
66+
/// <param name="path">The route group to use for A2A endpoints.</param>
67+
/// <param name="agentCard">Agent card info to return on query.</param>
68+
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
69+
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
70+
/// <remarks>
71+
/// This method can be used to access A2A agents that support the
72+
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
73+
/// discovery mechanism.
74+
/// </remarks>
75+
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager)
4676
{
4777
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
48-
return endpoints.MapA2A(agent, path, agentCard);
78+
return endpoints.MapA2A(agent, path, agentCard, configureTaskManager);
4979
}
5080

5181
/// <summary>
@@ -55,11 +85,26 @@ public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, string a
5585
/// <param name="agent">The agent to use for A2A protocol integration.</param>
5686
/// <param name="path">The route group to use for A2A endpoints.</param>
5787
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
58-
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
88+
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
89+
=> endpoints.MapA2A(agent, path, _ => { });
90+
91+
/// <summary>
92+
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
93+
/// </summary>
94+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
95+
/// <param name="agent">The agent to use for A2A protocol integration.</param>
96+
/// <param name="path">The route group to use for A2A endpoints.</param>
97+
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
98+
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
99+
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, Action<ITaskManager> configureTaskManager)
59100
{
60101
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
61-
var taskManager = agent.MapA2A(loggerFactory: loggerFactory);
62-
return endpoints.MapA2A(taskManager, path);
102+
var agentThreadStore = endpoints.ServiceProvider.GetKeyedService<AgentThreadStore>(agent.Name);
103+
var taskManager = agent.MapA2A(loggerFactory: loggerFactory, agentThreadStore: agentThreadStore);
104+
var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
105+
106+
configureTaskManager(taskManager);
107+
return endpointConventionBuilder;
63108
}
64109

65110
/// <summary>
@@ -75,11 +120,33 @@ public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, AIAgent
75120
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
76121
/// discovery mechanism.
77122
/// </remarks>
78-
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard)
123+
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard)
124+
=> endpoints.MapA2A(agent, path, agentCard, _ => { });
125+
126+
/// <summary>
127+
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
128+
/// </summary>
129+
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
130+
/// <param name="agent">The agent to use for A2A protocol integration.</param>
131+
/// <param name="path">The route group to use for A2A endpoints.</param>
132+
/// <param name="agentCard">Agent card info to return on query.</param>
133+
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
134+
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
135+
/// <remarks>
136+
/// This method can be used to access A2A agents that support the
137+
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
138+
/// discovery mechanism.
139+
/// </remarks>
140+
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager)
79141
{
80142
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
81-
var taskManager = agent.MapA2A(agentCard: agentCard, loggerFactory: loggerFactory);
82-
return endpoints.MapA2A(taskManager, path);
143+
var agentThreadStore = endpoints.ServiceProvider.GetKeyedService<AgentThreadStore>(agent.Name);
144+
var taskManager = agent.MapA2A(agentCard: agentCard, agentThreadStore: agentThreadStore, loggerFactory: loggerFactory);
145+
var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
146+
147+
configureTaskManager(taskManager);
148+
149+
return endpointConventionBuilder;
83150
}
84151

85152
/// <summary>
@@ -90,14 +157,12 @@ public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, AIAgent
90157
/// <param name="taskManager">Pre-configured A2A TaskManager to use for A2A endpoints handling.</param>
91158
/// <param name="path">The route group to use for A2A endpoints.</param>
92159
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
93-
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, TaskManager taskManager, string path)
160+
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, ITaskManager taskManager, string path)
94161
{
95162
// note: current SDK version registers multiple `.well-known/agent.json` handlers here.
96163
// it makes app return HTTP 500, but will be fixed once new A2A SDK is released.
97164
// see https://github.com/microsoft/agent-framework/issues/476 for details
98165
A2ARouteBuilderExtensions.MapA2A(endpoints, taskManager, path);
99-
endpoints.MapHttpA2A(taskManager, path);
100-
101-
return taskManager;
166+
return endpoints.MapHttpA2A(taskManager, path);
102167
}
103168
}

dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,37 @@ public static class AIAgentExtensions
2020
/// <param name="agent">Agent to attach A2A messaging processing capabilities to.</param>
2121
/// <param name="taskManager">Instance of <see cref="TaskManager"/> to configure for A2A messaging. New instance will be created if not passed.</param>
2222
/// <param name="loggerFactory">The logger factory to use for creating <see cref="ILogger"/> instances.</param>
23+
/// <param name="agentThreadStore">The store to store thread contents and metadata.</param>
2324
/// <returns>The configured <see cref="TaskManager"/>.</returns>
24-
public static TaskManager MapA2A(
25+
public static ITaskManager MapA2A(
2526
this AIAgent agent,
26-
TaskManager? taskManager = null,
27-
ILoggerFactory? loggerFactory = null)
27+
ITaskManager? taskManager = null,
28+
ILoggerFactory? loggerFactory = null,
29+
AgentThreadStore? agentThreadStore = null)
2830
{
2931
ArgumentNullException.ThrowIfNull(agent);
3032
ArgumentNullException.ThrowIfNull(agent.Name);
3133

32-
taskManager ??= new();
34+
var hostAgent = new AIHostAgent(
35+
innerAgent: agent,
36+
threadStore: agentThreadStore ?? new NoopAgentThreadStore());
3337

38+
taskManager ??= new TaskManager();
3439
taskManager.OnMessageReceived += OnMessageReceivedAsync;
35-
3640
return taskManager;
3741

3842
async Task<A2AResponse> OnMessageReceivedAsync(MessageSendParams messageSendParams, CancellationToken cancellationToken)
3943
{
40-
var response = await agent.RunAsync(
44+
var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N");
45+
var thread = await hostAgent.GetOrCreateThreadAsync(contextId, cancellationToken).ConfigureAwait(false);
46+
47+
var response = await hostAgent.RunAsync(
4148
messageSendParams.ToChatMessages(),
49+
thread: thread,
4250
cancellationToken: cancellationToken).ConfigureAwait(false);
43-
var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N");
44-
var parts = response.Messages.ToParts();
4551

52+
await hostAgent.SaveThreadAsync(contextId, thread, cancellationToken).ConfigureAwait(false);
53+
var parts = response.Messages.ToParts();
4654
return new AgentMessage
4755
{
4856
MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
@@ -60,14 +68,16 @@ async Task<A2AResponse> OnMessageReceivedAsync(MessageSendParams messageSendPara
6068
/// <param name="agentCard">The agent card to return on query.</param>
6169
/// <param name="taskManager">Instance of <see cref="TaskManager"/> to configure for A2A messaging. New instance will be created if not passed.</param>
6270
/// <param name="loggerFactory">The logger factory to use for creating <see cref="ILogger"/> instances.</param>
71+
/// <param name="agentThreadStore">The store to store thread contents and metadata.</param>
6372
/// <returns>The configured <see cref="TaskManager"/>.</returns>
64-
public static TaskManager MapA2A(
73+
public static ITaskManager MapA2A(
6574
this AIAgent agent,
6675
AgentCard agentCard,
67-
TaskManager? taskManager = null,
68-
ILoggerFactory? loggerFactory = null)
76+
ITaskManager? taskManager = null,
77+
ILoggerFactory? loggerFactory = null,
78+
AgentThreadStore? agentThreadStore = null)
6979
{
70-
taskManager = agent.MapA2A(taskManager, loggerFactory);
80+
taskManager = agent.MapA2A(taskManager, loggerFactory, agentThreadStore);
7181

7282
taskManager.OnAgentCardQuery += (context, query) =>
7383
{
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.Shared.Diagnostics;
7+
8+
namespace Microsoft.Agents.AI.Hosting;
9+
10+
/// <summary>
11+
/// Provides a hosting wrapper around an <see cref="AIAgent"/> that adds thread persistence capabilities
12+
/// for server-hosted scenarios where conversations need to be restored across requests.
13+
/// </summary>
14+
/// <remarks>
15+
/// <para>
16+
/// <see cref="AIHostAgent"/> wraps an existing agent implementation and adds the ability to
17+
/// persist and restore conversation threads using an <see cref="AgentThreadStore"/>.
18+
/// </para>
19+
/// <para>
20+
/// This wrapper enables thread persistence without requiring type-specific knowledge of the thread type,
21+
/// as all thread operations work through the base <see cref="AgentThread"/> abstraction.
22+
/// </para>
23+
/// </remarks>
24+
public class AIHostAgent : DelegatingAIAgent
25+
{
26+
private readonly AgentThreadStore _threadStore;
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="AIHostAgent"/> class.
30+
/// </summary>
31+
/// <param name="innerAgent">The underlying agent implementation to wrap.</param>
32+
/// <param name="threadStore">The thread store to use for persisting conversation state.</param>
33+
/// <exception cref="ArgumentNullException">
34+
/// <paramref name="innerAgent"/> or <paramref name="threadStore"/> is <see langword="null"/>.
35+
/// </exception>
36+
public AIHostAgent(AIAgent innerAgent, AgentThreadStore threadStore)
37+
: base(innerAgent)
38+
{
39+
this._threadStore = Throw.IfNull(threadStore);
40+
}
41+
42+
/// <summary>
43+
/// Gets an existing agent thread for the specified conversation, or creates a new one if none exists.
44+
/// </summary>
45+
/// <param name="conversationId">The unique identifier of the conversation for which to retrieve or create the agent thread. Cannot be null,
46+
/// empty, or consist only of white-space characters.</param>
47+
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
48+
/// <returns>A task that represents the asynchronous operation. The task result contains the agent thread associated with the
49+
/// specified conversation. If no thread exists, a new thread is created and returned.</returns>
50+
public ValueTask<AgentThread> GetOrCreateThreadAsync(string conversationId, CancellationToken cancellationToken = default)
51+
{
52+
_ = Throw.IfNullOrWhitespace(conversationId);
53+
54+
return this._threadStore.GetThreadAsync(this.InnerAgent, conversationId, cancellationToken);
55+
}
56+
57+
/// <summary>
58+
/// Persists a conversation thread to the thread store.
59+
/// </summary>
60+
/// <param name="conversationId">The unique identifier for the conversation.</param>
61+
/// <param name="thread">The thread to persist.</param>
62+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
63+
/// <returns>A task that represents the asynchronous save operation.</returns>
64+
/// <exception cref="ArgumentException"><paramref name="conversationId"/> is null or whitespace.</exception>
65+
/// <exception cref="ArgumentNullException"><paramref name="thread"/> is <see langword="null"/>.</exception>
66+
public ValueTask SaveThreadAsync(string conversationId, AgentThread thread, CancellationToken cancellationToken = default)
67+
{
68+
_ = Throw.IfNullOrWhitespace(conversationId);
69+
_ = Throw.IfNull(thread);
70+
71+
return this._threadStore.SaveThreadAsync(this.InnerAgent, conversationId, thread, cancellationToken);
72+
}
73+
}

0 commit comments

Comments
 (0)