前面的两篇文章通过简单例子简单介绍了如何利用一个ASP.NET Core应用来构建A2A Server,并分别采用JSON-RPC和HTTP-JSON这两种底层传输协议将指定的AIAgent对外暴露成A2A服务,这篇文章主要从设计和实现的角度介绍背后的故事。
1. IA2ARequestHandler
A2A协议与实现-03:从Protobuf消息详解A2A协议四大核心操作介绍了A2A的四大核心操作,它们分别是:
- Agent的调用 :通过发送
SendMessageRequest消息远程调用承载与A2A Server中的Agent,具体性包括如下两种:- SendMessage :阻塞式调用,完整的响应以
SendMessageResponse的形式返回; - SendStreamingMessage :流式调用,Agent生成的内容以
StreamResponse的形式实时返回。
- SendMessage :阻塞式调用,完整的响应以
- 任务管理 :A2A利用后台任务的形式解决长时间执行的问题。即通过创建一个后台任务处理Agent调用请求,将任务返回给客户端由其跟踪任务的进度并得到最终的结果,具体的操作包括:
- ListTasks:根据条件检索任务列表;
- GetTask:根据指定的ID提取对应的任务;
- CancelTask:取消执行指定的任务;
- SubscribeToTask:订阅指定的任务,并接收任务的响应流。
- 推送通知配置管理 :为了解决Agent长时间执行导致连接断开的问题,A2A支持基于Webhook 针对任务状态的反向推送。A2A定义了
TaskPushNotificationConfig来对此进行配置,并提供如下的操作管理此配置:- CreateTaskPushNotificationConfig:创建配置;
- GetTaskPushNotificationConfig:提取指定配置;
- ListTaskPushNotificationConfigs:检索配置;
- DeleteTaskPushNotificationConfig:删除指定配置。
- AgentCard扩展 :用于展示更详细的身份信息、专有工具集或品牌展示。为了隐私保护,这些丰富的信息只有在对方通过身份验证后才会展示,操作名称为GetAgentCard。
A2A官方.NET SDK(NuGet包为A2A)将上述四组操作转换成如下这个IA2ARequestHandler接口。由于基于后台任务对Agent调用请求的处理目前还半成品 ,所以建立在后台任务上的推送通知配置管理和AgentCard扩展压根就没有实现。,所以本文主要介绍Agent调用涉及的两个操作。
csharp
public interface IA2ARequestHandler
{
// 1. Agent调用
Task<SendMessageResponse> SendMessageAsync(
SendMessageRequest request,
CancellationToken cancellationToken = default);
IAsyncEnumerable<StreamResponse> SendStreamingMessageAsync(
SendMessageRequest request,
CancellationToken cancellationToken = default);
//2. 任务管理
Task<AgentTask> GetTaskAsync(
GetTaskRequest request,
CancellationToken cancellationToken = default);
Task<ListTasksResponse> ListTasksAsync(
ListTasksRequest request,
CancellationToken cancellationToken = default);
Task<AgentTask> CancelTaskAsync(
CancelTaskRequest request,
CancellationToken cancellationToken = default);
IAsyncEnumerable<StreamResponse> SubscribeToTaskAsync(
SubscribeToTaskRequest request,
CancellationToken cancellationToken = default);
// 3. 任务推送通知配置管理
Task<TaskPushNotificationConfig> CreateTaskPushNotificationConfigAsync(
CreateTaskPushNotificationConfigRequest request,
CancellationToken cancellationToken = default);
Task<TaskPushNotificationConfig> GetTaskPushNotificationConfigAsync(
GetTaskPushNotificationConfigRequest request,
CancellationToken cancellationToken = default);
Task<ListTaskPushNotificationConfigResponse> ListTaskPushNotificationConfigAsync(
ListTaskPushNotificationConfigRequest request,
CancellationToken cancellationToken = default);
Task DeleteTaskPushNotificationConfigAsync(
DeleteTaskPushNotificationConfigRequest request,
CancellationToken cancellationToken = default);
// 4. AgentCard扩展
Task<AgentCard> GetExtendedAgentCardAsync(
GetExtendedAgentCardRequest request,
CancellationToken cancellationToken = default);
}
1.1 SendMessageRequest
针对Agent的调用,不论是阻塞式调用还是流式调用,都是使用如下这个SendMessageRequest作为输入请求。
csharp
public sealed class SendMessageRequest
{
public string? Tenant { get; set; }
public Message Message { get; set; };
public SendMessageConfiguration? Configuration { get; set; }
public Dictionary<string, JsonElement>? Metadata { get; set; }
}
三个属性成员说明如下:
- Tenant:租户名称(A2A采用多租户设计解决企业级场景下多客户隔离、多环境托管以及复杂的资源路由问题);
- Message :作为请求内容的消息,可以理解为MEAI的
ChatMessage; - Configuration :提供用于控制服务端处理Agent调用行为的配置,比如是否支持后台任务模式或者基于WebHook的通知推送等;
- Metadata: 提供额外的元数据。
A2A通信的核心内容均定义在如下所示的Message类型中:
csharp
public sealed class Message
{
public Role Role { get; set; }
public List<Part> Parts { get; set; } = new List<Part>();
public string MessageId { get; set; } = string.Empty;
public string? ContextId { get; set; }
public string? TaskId { get; set; }
public List<string>? ReferenceTaskIds { get; set; }
public List<string>? Extensions { get; set; }
public Dictionary<string, JsonElement>? Metadata { get; set; }
}
public enum Role
{
Unspecified,
User,
Agent
}
具体属性成员说明如下:
- MessageId:每条消息的唯一标识符,通常由发送方生成。它对于跟踪对话历史、调试和日志记录非常重要;
- ContextId:可选字段,用于将消息关联到特定的上下文或会话中。这对于多轮对话或需要状态管理的交互非常有用;
- TaskId:可选字段,用于将消息关联到特定的任务或操作中。这对于长时间运行的任务或需要异步处理的场景非常有用;
- Role:消息的角色,指明这条消息是由用户还是Agent发送的。这有助于接收方理解消息的来源和意图;
- Parts:消息的内容部分。每个Part可以包含不同类型的数据(文本、图像、结构化数据等),使得消息能够支持多模态交互;
- Metadata:一个结构化的键值对集合,用于携带额外的上下文信息。这些信息可以用于路由、优先级设置、调试等目的;
- Extensions:一个字符串列表,指明这条消息使用了哪些协议扩展。这允许系统在不修改核心协议的情况下,添加新的功能或特性;
- ReferenceTaskIds:一个字符串列表,包含与这条消息相关的任务ID。这对于跟踪和管理复杂的工作流非常有用,尤其是在涉及多个Agent协作的场景中。
SendMessageRequest的Configuration返回具有如下定义的SendMessageConfiguration对象:
csharp
public sealed class SendMessageConfiguration
{
public List<string>? AcceptedOutputModes { get; set; }
public PushNotificationConfig? PushNotificationConfig { get; set; }
public int? HistoryLength { get; set; }
public bool ReturnImmediately { get; set; }
}
具体配置成员包括:
- AcceptedOutputModes :一个字符串列表,指明发送方希望接收的输出媒体类型。例如,如果发送方只想要文本响应,它可能会设置为
["text/plain"];如果它也能处理图像响应,则可能设置为["text/plain", "image/png"]。这有助于接收Agent根据发送方的能力和偏好来生成适当格式的响应; - PushNotificationConfig :如果发送方希望在任务完成时接收推送通知,可以通过这个字段提供相关配置。这允许Agent在处理完任务后主动通知发送方,而不需要发送方一直等待连接;
- HistoryLength:指明发送方希望接收的消息历史记录的长度。比如,如果设置为5,接收方在响应时会附带最近5条相关的消息历史,这对于需要上下文理解的交互非常有用。
- ReturnImmediately :一个布尔值,指示发送方是否希望接收方立即返回响应,而不是等待整个处理过程完成。如果设置为
true,接收方可以先返回一个确认消息(例如,任务已接受),然后在后续的消息中提供处理结果。这对于需要快速反馈的交互非常有用。
用于配置基于任务的通知推送配置的PushNotificationConfig类型定义如下:
csharp
public sealed class PushNotificationConfig
{
public string? Id { get; set; }
public string Url { get; set; }
public AuthenticationInfo? Authentication { get; set; }
public string? Token { get; set; }
}
public sealed class AuthenticationInfo
{
public string Scheme { get; set; }
public string? Credentials { get; set; }
}
具体配置成员包括:
- Id:推送通知配置的唯一标识符,通常由发送方生成。它对于跟踪和管理推送通知非常重要;
- Url:接收推送通知的URL。当Agent完成任务时,它将向这个URL发送HTTP POST请求,通知发送方任务的完成情况;
- Token:用于验证推送通知的安全令牌。Agent在发送通知时会包含这个令牌,接收方可以使用它来验证通知的真实性,防止伪造的通知攻击;
- Authentication :一个
AuthenticationInfo对象,包含了用于认证推送通知的相关信息。这可以包括认证方案(如Bearer Token、OAuth2等)和相关的凭证信息,确保推送通知的安全传输。
1.2 SendMessageResponse
如果采用后台任务的形式处理Agent调用请求,服务端会返回代表此后台任务的AgentTask对象,否则将最终的结果以Message对象予以响应。所以标识阻塞式调用响应的SendMessageResponse要么通过Task属性返回AgentTask,要么通过Message属性返回包含结果的消息。具体属于那种情况可通过PayloadCase属性返回的SendMessageResponseCase配置来确定。
csharp
public sealed class SendMessageResponse
{
public AgentTask? Task { get; set; }
public Message? Message { get; set; }
public SendMessageResponseCase PayloadCase{ get;}
}
public enum SendMessageResponseCase
{
None,
Task,
Message
}
表示后台任务的AgentTask类型定义如下:
csharp
public sealed class AgentTask
{
public string Id { get; set; }
public string ContextId { get; set; }
public TaskStatus Status { get; set; }
public List<Message>? History { get; set; }
public List<Artifact>? Artifacts { get; set; }
public Dictionary<string, JsonElement>? Metadata { get; set; }
}
后台任务可以提供如下信息:
- Id :每个
AgentTask的唯一标识符,通常由创建它的Agent生成。它对于跟踪、引用和管理任务非常重要; - ContextId :与
AgentTask相关联的上下文标识符。这允许多个任务共享相同的上下文信息,促进协作和状态管理; - Status :
AgentTask的当前状态,定义了任务的生命周期阶段。状态的变化可以触发不同的行为或通知; - Artifacts :与
AgentTask相关的产出物列表。这些AgentTask代表了任务执行过程中生成的结果或中间成果,可以是文本、图像、结构化数据等多种形式; - History :与
AgentTask相关的消息历史记录。这些消息记录了任务执行过程中所有的交互,包括客户端和Agent之间的通信。这对于调试、审计和理解任务执行过程非常有用; - Metadata :一个结构化的键值对集合,用于携带与这个
AgentTask相关的上下文信息。这些信息可以用于分类、搜索、调试等目的;
1.3 StreamResponse
对于流式调用实时返回的StreamResponse,如果不采用后台任务模式,则利用Message返回实时生成内容。反之除了首次返回代表后台任务的AgentTask,后续会返回针对任务的状态变化或者针对Artifact的更新。StreamResponse对应这四种情况下哪一种,由PayloadCase返回的StreamResponseCase枚举决定。
csharp
public sealed class StreamResponse
{
public AgentTask? Task { get; set; }
public Message? Message { get; set; }
public TaskStatusUpdateEvent? StatusUpdate { get; set; }
public TaskArtifactUpdateEvent? ArtifactUpdate { get; set; }
public StreamResponseCase PayloadCase{ get; }
}
public enum StreamResponseCase
{
None,
Task,
Message,
StatusUpdate,
ArtifactUpdate
}
针对后台任务状态的改变和Artifact变更通过如下所示的TaskStatusUpdateEvent和TaskArtifactUpdateEvent来描述。
csharp
public sealed class TaskStatusUpdateEvent
{
public string TaskId { get; set; } = string.Empty;
public string ContextId { get; set; } = string.Empty;
public TaskStatus Status { get; set; } = new TaskStatus();
public Dictionary<string, JsonElement>? Metadata { get; set; }
}
public sealed class TaskArtifactUpdateEvent
{
public string TaskId { get; set; }
public string ContextId { get; set; }
public Artifact Artifact { get; set; }
public bool Append { get; set; }
public bool LastChunk { get; set; }
public Dictionary<string, JsonElement>? Metadata { get; set; }
}
2. IAgentHandler
A2A最终调用承载的AIAgent是由注册的IAgentHandler对象完成的,具体体现在该接口定义的ExecuteAsync方法上。另一个CancelAsync方法用于取消执行指定上下文涉及的后台任务。
csharp
public interface IAgentHandler
{
Task ExecuteAsync(
RequestContext context,
AgentEventQueue eventQueue,
CancellationToken cancellationToken);
async Task CancelAsync(
RequestContext context,
AgentEventQueue eventQueue,
CancellationToken cancellationToken)
{
await new TaskUpdater(eventQueue, context.TaskId, context.ContextId).CancelAsync(cancellationToken);
}
}
2.1 RequestContext
当A2A Server接收到外界针对Agent调用请求(阻塞式或者流式调用)后,会创建一个RequestContext对象作为当前的处理请求的上下文。由于两种调用方式传入的输入都是SendMessageRequest,所以可以说这个请求上下文就是根据SendMessageRequest对象创建的。
csharp
public sealed class RequestContext
{
public required Message Message { get; init; }
public AgentTask? Task { get; init; }
public required string TaskId { get; init; }
public required string ContextId { get; init; }
public bool ClientProvidedContextId { get; init; }
public required bool StreamingResponse { get; init; }
public SendMessageConfiguration? Configuration { get; init; }
public Dictionary<string, JsonElement>? Metadata { get; init; }
public string? UserText => Message.Parts.FirstOrDefault((Part p) => p.Text != null)?.Text;
public bool IsContinuation => Task != null;
}
每个上下文成员的作用的来源说明如下:
- Message :表示调用Agent传入的消息,来源于
SendMessageRequest的同名属性; - Task : 如果
SendMessageRequest的TaskId不为空,会从本地任务存储中提取对应的AgentTask来初始化此属性; - TaskId :如果
SendMessageRequest的TaskId不为空,则返回此ID,否则创建一个GUID作为任务ID; - ContextId : 如果
SendMessageRequest的ContextId不为空,则返回此ID,否则创建一个GUID作为上下文ID; - ClientProvidedContextId :表示
ContextId是否来源于SendMessageRequest的ContextId; - StreamingResponse:表示式阻塞式调用还是流式调用;
- Configuration :来源于
SendMessageRequest的同名属性; - Metadata :来源于
SendMessageRequest的同名属性; - UserText :
Message第一个Part对象(如果存在)的文本; - IsContinuation :用来判断当前请求是否在继续执行一个已经存在的任务),如果
Task不为null,返回true,否则返回false。
2.2 AgentEventQueue
通过调用ExecuteAsync方法执行Agent时,不管是否为流式调用,任何形式的产出都被封装到StreamResponse对象,并置于eventQueue参数表示的时间队列。该事件队列通过如下这个AgentEventQueue类型表示,具体的队列本质上是一个BoundedChannel<StreamResponse>对象,默认容量为16,支持多线程写,并在队列满了之后会阻塞等待。AgentEventQueue实现了IAsyncEnumerable<StreamResponse>接口,所以可以异步读取写入的StreamResponse对象。
csharp
public sealed class AgentEventQueue : IAsyncEnumerable<StreamResponse>
{
private readonly Channel<StreamResponse> _channel;
public AgentEventQueue(int capacity = 16);
public AgentEventQueue(BoundedChannelOptions options);
public ValueTask WriteAsync(StreamResponse response, CancellationToken cancellationToken = default);
public ValueTask EnqueueTaskAsync(AgentTask task, CancellationToken cancellationToken = default);
public ValueTask EnqueueMessageAsync(Message message, CancellationToken cancellationToken = default);
public ValueTask EnqueueStatusUpdateAsync(TaskStatusUpdateEvent update, CancellationToken cancellationToken = default);
public ValueTask EnqueueArtifactUpdateAsync(TaskArtifactUpdateEvent update, CancellationToken cancellationToken = default);
public void Complete(Exception? exception = null);
public async IAsyncEnumerator<StreamResponse> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
除了定义一个WriteAsync方法写入创建好的StreamResponse对象之外,AgentEventQueue还定义了四个**Enqueue-**为前缀的方法:
- EnqueueMessageAsync: 写入生成的内容;
- EnqueueTaskAsync: 写入创建的后台任务;
- EnqueueStatusUpdateAsync: 写入任务状态的变化;
- EnqueueArtifactUpdateAsync:写入附加在任务上的Artifact变更;
针对这个AgentEventQueue对象表示的事件队列的操作可以进一步通过如下这个TaskUpdater来完成。顾名思义,TaskUpdater用来将针对AgentTask的更新事件写入队列。我们创建该对象的时候传入AgentEventQueue对象,和后台任务和上下文的ID。
csharp
public sealed class TaskUpdater(AgentEventQueue eventQueue, string taskId, string contextId)
{
public string TaskId { get; }
public string ContextId { get; }
public ValueTask SubmitAsync(
Dictionary<string, JsonElement>? metadata = null,
CancellationToken cancellationToken = default);
public ValueTask StartWorkAsync(
Message? message = null,
Dictionary<string, JsonElement>? metadata = null,
CancellationToken cancellationToken = default);
public ValueTask AddArtifactAsync(
IReadOnlyList<Part> parts,
string? artifactId = null,
string? name = null,
string? description = null,
bool lastChunk = true,
bool append = false,
Dictionary<string, JsonElement>? metadata = null,
CancellationToken cancellationToken = default);
public async ValueTask CompleteAsync(
Message? message = null,
Dictionary<string, JsonElement>? metadata = null,
CancellationToken cancellationToken = default);
}
public async ValueTask FailAsync(
Message? message = null,
Dictionary<string, JsonElement>? metadata = null,
CancellationToken cancellationToken = default);
public async ValueTask CancelAsync(
Dictionary<string, JsonElement>? metadata = null,
CancellationToken cancellationToken = default);
public async ValueTask RejectAsync(
Message? message = null,
Dictionary<string, JsonElement>? metadata = null,
CancellationToken cancellationToken = default);
public async ValueTask RequireInputAsync(
Message message,
Dictionary<string, JsonElement>? metadata = null,
CancellationToken cancellationToken = default);
public async ValueTask RequireAuthAsync(
Message? message = null,
Dictionary<string, JsonElement>? metadata = null,
CancellationToken cancellationToken = default);
}
各个方法采用如的方式操作队列:
- SubmitAsync : 创建一个状态的
Submitted的AgentTask,并通过调用EnqueueTaskAsync方法进入队列; - StartWorkAsync : 调用
EnqueueStatusUpdateAsync方法通知任务状态变成Working; - AddArtifactAsync :创建一个
Artifact对象,并封装成TaskArtifactUpdateEvent,通过调用EnqueueArtifactUpdateAsync方法通知新的Artifact被添加; - CompleteAsync : 调用
CompleteAsync方法终结输入流; - FailAsync :调用
EnqueueStatusUpdateAsync方法通知后台任务状态变成Failed; - CancelAsync :调用
EnqueueStatusUpdateAsync方法通知后台任务状态变成Canceled。IAgentHandler的CancelAsync方法调用的就是这个方法; - RejectAsync :调用
EnqueueStatusUpdateAsync方法通知后台任务状态变成Rejected; - RequireInputAsync :调用
EnqueueStatusUpdateAsync方法通知后台任务状态变成InputRequired; - RequireAuthAsync :调用
EnqueueStatusUpdateAsync方法通知后台任务状态变成AuthRequired;
2.3 A2AAgentHandler
真正用来执行AIAgent的A2AAgentHandler是一个实现了IAgentHandler接口的内部类型。构造函数传入的第一个参数就是根据AIAgent进一步封装成的AIHostAgent(主要是提供一个AgentSessionStore对象来存储Session状态)。
csharp
internal sealed class A2AAgentHandler : IAgentHandler
{
public A2AAgentHandler(AIHostAgent hostAgent, AgentRunMode runMode);
public Task ExecuteAsync(RequestContext context, AgentEventQueue eventQueue, CancellationToken cancellationToken);
public async Task CancelAsync(RequestContext context, AgentEventQueue eventQueue, CancellationToken cancellationToken);
}
public sealed class AgentRunMode : IEquatable<AgentRunMode>
{
public static AgentRunMode DisallowBackground ();
public static AgentRunMode AllowBackgroundIfSupported();
public static AgentRunMode AllowBackgroundWhen(
Func<A2ARunDecisionContext, CancellationToken, ValueTask<bool>> runInBackground);
private AgentRunMode(string value, Func<A2ARunDecisionContext, CancellationToken, ValueTask<bool>>? runInBackground = null);
}
构造函数的第二个参数类型为AgentRunMode,用来表示Agent执行的模式。所谓执行模式,本质上就是是否支持以后台任务的方式处理Agent调用。由于构造函数被设置为私有,所以我们只能通过三个静态方法创建固定三类AgentRunMode:
- DisallowBackground:不允许;
- AllowBackgroundIfSupported:允许;
- AllowBackgroundWhen:满足条件时允许。
实现在ExecuteAsync方法中针对Agent的执行采用如下的流程,具体分为三个分支:
- 如果
IsContinuation=true(RequestContext的Task属性返回一个具体的AgentTask),那么具体的执行会围绕如何更新这个AgentTask:- 将
ContextId视为ConversationId,调用AIHostAgent对象的GetOrCreateSessionAsync方法创建或者提取AgentSession对象; - 从
AgentTask的History属性中提取对话历史,并将每个消息转换成ChatMessage; - 创建
AgentRunOptions对象,并将AgentRunMode反映的针对后台任务的执行情况反映到AllowBackgroundResponses属性上; - 将
ChatMessage列表、AgentSession和AgentRunOptions对象作为参数调用AIHostAgent的RunAsync方法完成Agent的调用,并得到作为响应的AgentResponse; - 调用
AIHostAgent对象的SaveSessionAsync保存当前AgentSession;- 如果
AgentResponse的ContinuationToken属性为null,意味着Agent目前在前台运行,此时需要将响应消息的内容转换成A2A.Part类型,然后创建上面介绍的TaskUpdater对象并调用其AddArtifactAsync方法通知有新的Artifact被追加到后台任务。在调用CompleteAsync方法终结流程; - 否则,意味着Agent转入后台执行,此时会创建
TaskUpdater对象并调用StartWorkAsync方法将后台任务状态设置为Working。
- 如果
- 将
- 如果采用流式调用 :
- 将
ContextId视为ConversationId,调用AIHostAgent对象的GetOrCreateSessionAsync方法创建或者提取AgentSession对象; - 将
RequestContext的Message转换成ChatMessage; - 创建
AgentRunOptions对象,并将RequestContext的Metadata属性添加到AdditionalProperties中; - 将
ChatMessage消息、AgentSession和AgentRunOptions对象作为参数调用AIHostAgent对象的RunStreamingAsync方法,并将实时接收的AgentResponseUpdate转换成A2A.Message类型,通过调用EnqueueMessageAsync方法添加到事件队列中; - 调用
AIHostAgent对象的SaveSessionAsync保存当前AgentSession;
- 将
- 如果采用阻塞式调用 :
- 将
ContextId视为ConversationId,调用AIHostAgent对象的GetOrCreateSessionAsync方法创建或者提取AgentSession对象; - 将
RequestContext的Message转换成ChatMessage; - 创建
AgentRunOptions对象,并将RequestContext的Metadata属性添加到AdditionalProperties中; - 将
ChatMessage消息、AgentSession和AgentRunOptions对象作为参数调用AIHostAgent对象的RunAsync方法,并得到作为响应的AgentResponse; - 调用AIHostAgent对象的SaveSessionAsync保存当前AgentSession;
- 如果
AgentResponse的ContinuationToken属性为null,意味着Agent目前在前台运行,此时需要将响应消息的内容转换成A2A.Message,并作为参数调用EnqueueMessageAsync方法添加到事件队列中; - 否则,意味着Agent的执行尚未结束:
- 创建
TaskUpdater并调用SubmitAsync方法,针对新创建的ContextId和TaskId创建一个AgentTask对象添加到事件队列中; - 将响应消息转换成
A2A.Message,并将其作为参数调用TaskUpdater的StartWorkAsync方法将后台任务的状态设置为Working。
- 创建
- 如果
- 将
从上面这个流程可以看出关于A2A针对后台任务的Agent调用实际上是借助AIAgent的后台响应特性实现的(AgentRunOptions的AllowBackgroundResponses配置选项为对应的开关),但是整个功能其实压根没有实现 。如果开启了AllowBackgroundResponses开关,并且LLM支持后台响应,那么调用RunAsync方法返回的AgentResponse的ContinuationToken可以用来判断是否真的采用后台任务在处理请求。如果ContinuationToken不为null,我们需要利用它轮询发起后续的调用 。很显然,上述的流程并没有这么做,它仅仅创建了一个AgentTask并将状态设置成Working而已。如果我们去轮询这个任务的状态,会发现它的状态永远是Working。
3. A2AServer
如果说IA2ARequestHandler接口是对A2A协议 的表达,那么如下这个A2AServer就是对这个协议的实现 。正如开篇介绍的,建立在后台任务之上的通知推送和AgentCard扩展特性压根就没有实现,所以对应的实现方法直接抛出A2AException异常。
csharp
public class A2AServer : IA2ARequestHandler, IAsyncDisposable
{
public A2AServer(IAgentHandler handler, ITaskStore taskStore,
ChannelEventNotifier notifier, ILogger<A2AServer> logger,
A2AServerOptions? options = null);
public virtual async Task<SendMessageResponse> SendMessageAsync(
SendMessageRequest request,
CancellationToken cancellationToken = default);
public virtual async IAsyncEnumerable<StreamResponse> SendStreamingMessageAsync(
SendMessageRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken = default);
public virtual async Task<AgentTask> GetTaskAsync(
GetTaskRequest request,
ancellationToken cancellationToken = default);
public virtual async Task<ListTasksResponse> ListTasksAsync(
ListTasksRequest request,
CancellationToken cancellationToken = default);
public virtual async Task<AgentTask> CancelTaskAsync(
CancelTaskRequest request, CancellationToken cancellationToken = default);
public virtual async IAsyncEnumerable<StreamResponse> SubscribeToTaskAsync(
SubscribeToTaskRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken = default);
public virtual Task<TaskPushNotificationConfig> CreateTaskPushNotificationConfigAsync(
CreateTaskPushNotificationConfigRequest request,
CancellationToken cancellationToken = default)
=> throw new A2AException("Push notifications not supported.", A2AErrorCode.PushNotificationNotSupported);
public virtual Task<TaskPushNotificationConfig> GetTaskPushNotificationConfigAsync(
GetTaskPushNotificationConfigRequest request,
CancellationToken cancellationToken = default)
=> throw new A2AException("Push notifications not supported.", A2AErrorCode.PushNotificationNotSupported);
public virtual Task<ListTaskPushNotificationConfigResponse> ListTaskPushNotificationConfigAsync(
ListTaskPushNotificationConfigRequest request,
CancellationToken cancellationToken = default)
=> throw new A2AException("Push notifications not supported.", A2AErrorCode.PushNotificationNotSupported);
public virtual Task DeleteTaskPushNotificationConfigAsync(
DeleteTaskPushNotificationConfigRequest request,
CancellationToken cancellationToken = default)
=> throw new A2AException("Push notifications not supported.", A2AErrorCode.PushNotificationNotSupported);
public virtual Task<AgentCard> GetExtendedAgentCardAsync(
GetExtendedAgentCardRequest request,
CancellationToken cancellationToken = default)
=> throw new A2AException("Extended agent card not configured.", A2AErrorCode.ExtendedAgentCardNotConfigured);
}
在正式介绍A2AServer针对Agent调用以及任务管理相关方法的实现之前,我们先来介绍构造函数的参数类型。第一个参数类型IAgentHandler,我们已经知道它用来执行Agent,我们现在关注其余两个。
3.1 ITaskStore
顾名思义,ITaskStore接口用来存储表示后台任务的AgentTask对象。如下面的代码所示,该接口定义了四个方法用来实现针对AgentTask的提取、存储、删除和检索。实际上A2AServer基于任务管理的几个方法最终就是调用这些方法实现的。SDK提供了如下这个基于内存字典作为存储的默认实现类型InMemoryTaskStore。
csharp
public interface ITaskStore
{
Task<AgentTask?> GetTaskAsync(string taskId, CancellationToken cancellationToken = default);
Task SaveTaskAsync(string taskId, AgentTask task, CancellationToken cancellationToken = default);
Task DeleteTaskAsync(string taskId, CancellationToken cancellationToken = default);
Task<ListTasksResponse> ListTasksAsync(ListTasksRequest request, CancellationToken cancellationToken = default);
}
public sealed class InMemoryTaskStore : ITaskStore
{
...
}
3.2 A2AServerOptions
作为A2AServer配置选项类的A2AServerOptions只定义了唯一的属性AutoAppendHistory,用来决定在后续对话的请求中,系统是否自动将用户刚刚发送的新消息追加到该任务的历史记录中,相当于是由服务端来管理Session。
csharp
public sealed class A2AServerOptions
{
public bool AutoAppendHistory { get; set; } = true;
}
3.3 调用请求的处理
实现在A2AServer的SendMessageAsync体现了A2A Server针对阻塞式Agent调用请求的总体处理流程如下:
- 根据提供的
SendMessageResponse构建作为请求上下文的RequestConext对象; - 如果
RequestConext的IsContinuation为true(表示延续现有的任务)并且A2AServerOptions的AutoAppendHistory选项为true:- 从
ITaskStore存储提取代表当前任务的AgentTask对象; - 将
RequestConext的消息添加到AgentTask的History属性代表的对话历史中; - 将新的
AgentTask重新保存到ITaskStore存储;
- 从
- 创建一个
AgentEventQueue作为事件队列; - 将
RequestContext和AgentEventQueue对象作为参数调用IAgentHandler的ExecuteAsync方法实现对Agent的调用; - 调用
AgentEventQueue的Complete方法表示输入终止; - 遍历
AgentEventQueue的每个StreamResponse:- 将
StreamResponse承载内容(StatusUpdate、ArtifactUpdate或者Message)应用到RequestContext的TaskId标识的AgentTask上(如果不存在就什么都不做) - 如果作为返回值的
SendMessageResponse尚未生成,且StreamResponse包含一个AgentTask(以后台任务执行Agent请求),针对这个AgentTask生成一个SendMessageResponse。迭代完成之后会将它作为返回值。 - 如果作为返回值的
SendMessageResponse尚未生成,且StreamResponse包含一个Message(直接返回结果),针对这个Message生成一个SendMessageResponse。迭代完成之后会将它作为返回值。
- 将
实现在A2AServer的SendStreamingMessageAsync体现了A2A Server针对流式Agent调用 的处理流程与SendMessageAsync基本一致。差别在后面的步骤:
- 虽然也是调用
IAgentHandler的ExecuteAsync方法,这里走的是流式调用的分支; - 在遍历
AgentEventQueue的每个StreamResponse,会实时输出这个StreamResponse对象。
4. A2AJsonRpcProcessor & A2AHttpProcessor
由于IA2ARequestHandler接口是对A2A协议的表达,所以这个接口也是与具体的传输协议无关 。将IA2ARequestHandler与具体传输协议绑定是由A2AJsonRpcProcessor和A2AHttpProcessor这两个内部类型来完成的,前者针对针对JSON-RPC,后者针对HTTP-JSON。这两个Processor定义的绝大部分方法的第一个参数都是IA2ARequestHandler对象,也就是这两个Processor分别完成与传输协议 相关部分的工作,核心的处理还是交给IA2ARequestHandler对象来完成。
csharp
public static class A2AJsonRpcProcessor
{
internal static async Task<IResult> ProcessRequestAsync(
IA2ARequestHandler requestHandler,
HttpRequest request,
CancellationToken cancellationToken);
}
internal static class A2AHttpProcessor
{
internal static Task<IResult> GetTaskAsync(IA2ARequestHandler requestHandler, ILogger logger, string id, int? historyLength, string? metadata, CancellationToken cancellationToken);
internal static Task<IResult> CancelTaskAsync(IA2ARequestHandler requestHandler, ILogger logger, string id, CancellationToken cancellationToken);
internal static Task<IResult> SendMessageAsync(IA2ARequestHandler requestHandler, ILogger logger, SendMessageRequest sendRequest, CancellationToken cancellationToken);
internal static IResult SendMessageStream(IA2ARequestHandler requestHandler, ILogger logger, SendMessageRequest sendRequest, CancellationToken cancellationToken);
internal static IResult SubscribeToTask(IA2ARequestHandler requestHandler, ILogger logger, string id, CancellationToken cancellationToken);
internal static Task<IResult> GetAgentCardRestAsync(IA2ARequestHandler requestHandler, ILogger logger, AgentCard agentCard, CancellationToken cancellationToken);
internal static Task<IResult> GetTaskRestAsync(IA2ARequestHandler requestHandler, ILogger logger, string id, int? historyLength, CancellationToken cancellationToken);
internal static Task<IResult> CancelTaskRestAsync(IA2ARequestHandler requestHandler, ILogger logger, string id, CancellationToken cancellationToken);
internal static Task<IResult> SendMessageRestAsync(IA2ARequestHandler requestHandler, ILogger logger, SendMessageRequest request, CancellationToken cancellationToken);
internal static IResult SendMessageStreamRest(IA2ARequestHandler requestHandler, ILogger logger, SendMessageRequest request, CancellationToken cancellationToken);
internal static IResult SubscribeToTaskRest(IA2ARequestHandler requestHandler, ILogger logger, string id, CancellationToken cancellationToken);
internal static Task<IResult> ListTasksRestAsync(IA2ARequestHandler requestHandler, ILogger logger, string? contextId, string? status, int? pageSize, string? pageToken, int? historyLength, CancellationToken cancellationToken);
internal static Task<IResult> GetExtendedAgentCardRestAsync(IA2ARequestHandler requestHandler, ILogger logger, CancellationToken cancellationToken);
internal static Task<IResult> CreatePushNotificationConfigRestAsync(IA2ARequestHandler requestHandler, ILogger logger, string taskId, PushNotificationConfig config, CancellationToken cancellationToken);
internal static Task<IResult> ListPushNotificationConfigRestAsync(IA2ARequestHandler requestHandler, ILogger logger, string taskId, int? pageSize, string? pageToken, CancellationToken cancellationToken);
internal static Task<IResult> GetPushNotificationConfigRestAsync(IA2ARequestHandler requestHandler, ILogger logger, string taskId, string configId, CancellationToken cancellationToken);
internal static Task<IResult> DeletePushNotificationConfigRestAsync(IA2ARequestHandler requestHandler, ILogger logger, string taskId, string configId, CancellationToken cancellationToken);
}
5. 路由注册
如下是用于注册基于JSON-RPC路由的MapA2AJsonRpc方法的定义。可以看出最终用来处理请求的正式A2AJsonRpcProcessor的ProcessRequestAsync方法,而传入的则是从依赖注入容器中提取出来的A2AServer对象。如果针对IA2ARequestHandler接口有其他的实现,则可以直接调用MapA2A方法注册路由。
csharp
public static IEndpointConventionBuilder MapA2AJsonRpc(
this IEndpointRouteBuilder endpoints,
AIAgent agent,
string path)
=> endpoints.MapA2AJsonRpc(agent.Name, path);
public static IEndpointConventionBuilder MapA2AJsonRpc(
this IEndpointRouteBuilder endpoints,
string agentName,
string path)
{
var a2aServer = endpoints.ServiceProvider.GetKeyedService<A2AServer>(agentName)
return endpoints.MapA2A(a2aServer, path);
}
public static IEndpointConventionBuilder MapA2A(
this IEndpointRouteBuilder endpoints,
IA2ARequestHandler requestHandler,
[StringSyntax("Route")] string path)
{
var routeGroup = endpoints.MapGroup("");
routeGroup.MapPost(path, (HttpRequest request, CancellationToken cancellationToken)
=> A2AJsonRpcProcessor.ProcessRequestAsync(requestHandler, request, cancellationToken));
return routeGroup;
}
如下是用于注册基于HTTP-JSON路由的MapA2AHttpJson方法的定义。可以看出它最终为A2A的各种操作针对不同的路径注册了路由,路由处理器调用的正是定于在A2AHttpProcessor中的各种方法,而作为第一个参数传入的依然是A2AServer对象。如果如果针对IA2ARequestHandler接口有其他的实现,则可以直接调用MapHttpA2A方法注册路由。
csharp
public static IEndpointConventionBuilder MapA2AHttpJson(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
=>endpoints.MapA2AHttpJson(agent.Name, path);
public static IEndpointConventionBuilder MapA2AHttpJson(this IEndpointRouteBuilder endpoints, string agentName, string path)
{
var a2aServer = endpoints.ServiceProvider.GetKeyedService<A2AServer>(agentName)
var stubAgentCard = new AgentCard { Name = "A2A Agent" };
return endpoints.MapHttpA2A(a2aServer, stubAgentCard, path);
}
public static IEndpointConventionBuilder MapHttpA2A(
this IEndpointRouteBuilder endpoints,
IA2ARequestHandler requestHandler,
AgentCard agentCard,
[StringSyntax("Route")] string path = "")
{
var routeGroup = endpoints.MapGroup(path);
var logger = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("A2A.REST");
// Agent card (SDK convenience endpoint, not part of the A2A spec Section 11.3)
routeGroup.MapGet("/card", (CancellationToken ct)
=> A2AHttpProcessor.GetAgentCardRestAsync(requestHandler, logger, agentCard, ct));
// Task operations
routeGroup.MapGet("/tasks/{id}", (string id, [FromQuery] int? historyLength, CancellationToken ct)
=> A2AHttpProcessor.GetTaskRestAsync(requestHandler, logger, id, historyLength, ct));
routeGroup.MapPost("/tasks/{id}:cancel", (string id, CancellationToken ct)
=> A2AHttpProcessor.CancelTaskRestAsync(requestHandler, logger, id, ct));
routeGroup.MapPost("/tasks/{id}:subscribe", (string id, CancellationToken ct)
=> A2AHttpProcessor.SubscribeToTaskRest(requestHandler, logger, id, ct));
routeGroup.MapGet("/tasks", ([FromQuery] string? contextId, [FromQuery] string? status,
[FromQuery] int? pageSize, [FromQuery] string? pageToken, [FromQuery] int? historyLength,
CancellationToken ct)
=> A2AHttpProcessor.ListTasksRestAsync(requestHandler, logger, contextId, status, pageSize, pageToken,
historyLength, ct));
// Message operations
routeGroup.MapPost("/message:send", ([FromBody] SendMessageRequest request, CancellationToken ct)
=> A2AHttpProcessor.SendMessageRestAsync(requestHandler, logger, request, ct));
routeGroup.MapPost("/message:stream", ([FromBody] SendMessageRequest request, CancellationToken ct)
=> A2AHttpProcessor.SendMessageStreamRest(requestHandler, logger, request, ct));
// Push notification config operations
routeGroup.MapPost("/tasks/{id}/pushNotificationConfigs",
(string id, [FromBody] PushNotificationConfig config, CancellationToken ct)
=> A2AHttpProcessor.CreatePushNotificationConfigRestAsync(requestHandler, logger, id, config, ct));
routeGroup.MapGet("/tasks/{id}/pushNotificationConfigs",
(string id, [FromQuery] int? pageSize, [FromQuery] string? pageToken, CancellationToken ct)
=> A2AHttpProcessor.ListPushNotificationConfigRestAsync(requestHandler, logger, id, pageSize, pageToken, ct));
routeGroup.MapGet("/tasks/{id}/pushNotificationConfigs/{configId}",
(string id, string configId, CancellationToken ct)
=> A2AHttpProcessor.GetPushNotificationConfigRestAsync(requestHandler, logger, id, configId, ct));
routeGroup.MapDelete("/tasks/{id}/pushNotificationConfigs/{configId}",
(string id, string configId, CancellationToken ct)
=> A2AHttpProcessor.DeletePushNotificationConfigRestAsync(requestHandler, logger, id, configId, ct));
// Extended agent card
routeGroup.MapGet("/extendedAgentCard", (CancellationToken ct)
=> A2AHttpProcessor.GetExtendedAgentCardRestAsync(requestHandler, logger, ct));
return routeGroup;
}
6. 服务注册
不论是调用MapA2AJsonRpc方法还是MapA2AHttpJson,都需要从依赖注入容器中提取A2AServer对象,对应的服务注册由如下这个AddA2AServer扩展方法进行注册。
csharp
public static IServiceCollection AddA2AServer(
this IServiceCollection services,
string agentName,
Action<A2AServerRegistrationOptions>? configureOptions = null)
{
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;
}