[A2A协议与实现-08]基于MAF的A2A Server设计与实现

前面的两篇文章通过简单例子简单介绍了如何利用一个ASP.NET Core应用来构建A2A Server,并分别采用JSON-RPCHTTP-JSON这两种底层传输协议将指定的AIAgent对外暴露成A2A服务,这篇文章主要从设计和实现的角度介绍背后的故事。

1. IA2ARequestHandler

A2A协议与实现-03:从Protobuf消息详解A2A协议四大核心操作介绍了A2A的四大核心操作,它们分别是:

  • Agent的调用 :通过发送SendMessageRequest消息远程调用承载与A2A Server中的Agent,具体性包括如下两种:
    • SendMessage :阻塞式调用,完整的响应以SendMessageResponse的形式返回;
    • SendStreamingMessage :流式调用,Agent生成的内容以StreamResponse的形式实时返回。
  • 任务管理 :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相关联的上下文标识符。这允许多个任务共享相同的上下文信息,促进协作和状态管理;
  • StatusAgentTask的当前状态,定义了任务的生命周期阶段。状态的变化可以触发不同的行为或通知;
  • 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变更通过如下所示的TaskStatusUpdateEventTaskArtifactUpdateEvent来描述。

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 : 如果SendMessageRequestContextId不为空,则返回此ID,否则创建一个GUID作为上下文ID;
  • ClientProvidedContextId :表示ContextId是否来源于SendMessageRequestContextId
  • 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 : 创建一个状态的SubmittedAgentTask,并通过调用EnqueueTaskAsync方法进入队列;
  • StartWorkAsync : 调用EnqueueStatusUpdateAsync方法通知任务状态变成Working
  • AddArtifactAsync :创建一个Artifact对象,并封装成TaskArtifactUpdateEvent,通过调用EnqueueArtifactUpdateAsync方法通知新的Artifact被添加;
  • CompleteAsync : 调用CompleteAsync方法终结输入流;
  • FailAsync :调用EnqueueStatusUpdateAsync方法通知后台任务状态变成Failed
  • CancelAsync :调用EnqueueStatusUpdateAsync方法通知后台任务状态变成CanceledIAgentHandlerCancelAsync方法调用的就是这个方法;
  • RejectAsync :调用EnqueueStatusUpdateAsync方法通知后台任务状态变成Rejected
  • RequireInputAsync :调用EnqueueStatusUpdateAsync方法通知后台任务状态变成InputRequired
  • RequireAuthAsync :调用EnqueueStatusUpdateAsync方法通知后台任务状态变成AuthRequired

2.3 A2AAgentHandler

真正用来执行AIAgentA2AAgentHandler是一个实现了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=trueRequestContextTask属性返回一个具体的AgentTask),那么具体的执行会围绕如何更新这个AgentTask
    • ContextId视为ConversationId,调用AIHostAgent对象的GetOrCreateSessionAsync方法创建或者提取AgentSession对象;
    • AgentTaskHistory属性中提取对话历史,并将每个消息转换成ChatMessage
    • 创建AgentRunOptions对象,并将AgentRunMode反映的针对后台任务的执行情况反映到AllowBackgroundResponses属性上;
    • ChatMessage列表、AgentSessionAgentRunOptions对象作为参数调用AIHostAgentRunAsync方法完成Agent的调用,并得到作为响应的AgentResponse
    • 调用AIHostAgent对象的SaveSessionAsync保存当前AgentSession
      • 如果AgentResponseContinuationToken属性为null,意味着Agent目前在前台运行,此时需要将响应消息的内容转换成A2A.Part类型,然后创建上面介绍的TaskUpdater对象并调用其AddArtifactAsync方法通知有新的Artifact被追加到后台任务。在调用CompleteAsync方法终结流程;
      • 否则,意味着Agent转入后台执行,此时会创建TaskUpdater对象并调用StartWorkAsync方法将后台任务状态设置为Working
  • 如果采用流式调用
    • ContextId视为ConversationId,调用AIHostAgent对象的GetOrCreateSessionAsync方法创建或者提取AgentSession对象;
    • RequestContextMessage转换成ChatMessage
    • 创建AgentRunOptions对象,并将RequestContextMetadata属性添加到AdditionalProperties中;
    • ChatMessage消息、AgentSessionAgentRunOptions对象作为参数调用AIHostAgent对象的RunStreamingAsync方法,并将实时接收的AgentResponseUpdate转换成A2A.Message类型,通过调用EnqueueMessageAsync方法添加到事件队列中;
    • 调用AIHostAgent对象的SaveSessionAsync保存当前AgentSession
  • 如果采用阻塞式调用
    • ContextId视为ConversationId,调用AIHostAgent对象的GetOrCreateSessionAsync方法创建或者提取AgentSession对象;
    • RequestContextMessage转换成ChatMessage
    • 创建AgentRunOptions对象,并将RequestContextMetadata属性添加到AdditionalProperties中;
    • ChatMessage消息、AgentSessionAgentRunOptions对象作为参数调用AIHostAgent对象的RunAsync方法,并得到作为响应的AgentResponse
    • 调用AIHostAgent对象的SaveSessionAsync保存当前AgentSession;
      • 如果AgentResponseContinuationToken属性为null,意味着Agent目前在前台运行,此时需要将响应消息的内容转换成A2A.Message,并作为参数调用EnqueueMessageAsync方法添加到事件队列中;
      • 否则,意味着Agent的执行尚未结束:
        • 创建TaskUpdater并调用SubmitAsync方法,针对新创建的ContextIdTaskId创建一个AgentTask对象添加到事件队列中;
        • 将响应消息转换成A2A.Message,并将其作为参数调用TaskUpdaterStartWorkAsync方法将后台任务的状态设置为Working

从上面这个流程可以看出关于A2A针对后台任务的Agent调用实际上是借助AIAgent的后台响应特性实现的(AgentRunOptionsAllowBackgroundResponses配置选项为对应的开关),但是整个功能其实压根没有实现 。如果开启了AllowBackgroundResponses开关,并且LLM支持后台响应,那么调用RunAsync方法返回的AgentResponseContinuationToken可以用来判断是否真的采用后台任务在处理请求。如果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 调用请求的处理

实现在A2AServerSendMessageAsync体现了A2A Server针对阻塞式Agent调用请求的总体处理流程如下:

  • 根据提供的SendMessageResponse构建作为请求上下文的RequestConext对象;
  • 如果RequestConextIsContinuation为true(表示延续现有的任务)并且A2AServerOptionsAutoAppendHistory选项为true
    • ITaskStore存储提取代表当前任务的AgentTask对象;
    • RequestConext的消息添加到AgentTaskHistory属性代表的对话历史中;
    • 将新的AgentTask重新保存到ITaskStore存储;
  • 创建一个AgentEventQueue作为事件队列;
  • RequestContextAgentEventQueue对象作为参数调用IAgentHandlerExecuteAsync方法实现对Agent的调用;
  • 调用AgentEventQueueComplete方法表示输入终止;
  • 遍历AgentEventQueue的每个StreamResponse:
    • StreamResponse承载内容(StatusUpdateArtifactUpdate或者Message)应用到RequestContextTaskId标识的AgentTask上(如果不存在就什么都不做)
    • 如果作为返回值的SendMessageResponse尚未生成,且StreamResponse包含一个AgentTask(以后台任务执行Agent请求),针对这个AgentTask生成一个SendMessageResponse。迭代完成之后会将它作为返回值。
    • 如果作为返回值的SendMessageResponse尚未生成,且StreamResponse包含一个Message(直接返回结果),针对这个Message生成一个SendMessageResponse。迭代完成之后会将它作为返回值。

实现在A2AServerSendStreamingMessageAsync体现了A2A Server针对流式Agent调用 的处理流程与SendMessageAsync基本一致。差别在后面的步骤:

  • 虽然也是调用IAgentHandlerExecuteAsync方法,这里走的是流式调用的分支;
  • 在遍历AgentEventQueue的每个StreamResponse,会实时输出这个StreamResponse对象。

4. A2AJsonRpcProcessor & A2AHttpProcessor

由于IA2ARequestHandler接口是对A2A协议的表达,所以这个接口也是与具体的传输协议无关 。将IA2ARequestHandler与具体传输协议绑定是由A2AJsonRpcProcessorA2AHttpProcessor这两个内部类型来完成的,前者针对针对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方法的定义。可以看出最终用来处理请求的正式A2AJsonRpcProcessorProcessRequestAsync方法,而传入的则是从依赖注入容器中提取出来的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;
}
相关推荐
ZhengEnCi1 小时前
Pi Agent 深度解析:最小化编码 Agent 的哲学、争议与未来
agent
张反手2 小时前
一个能讲清的 Loop + Tool 小 demo
agent
wu8587734572 小时前
从 Prompt 到 Loop:拆解 AI 工程化四范式的演进逻辑与落地边界
人工智能·ai·prompt·aigc·ai编程
大龄码农有梦想3 小时前
Codex、Claude Code 等 AI 编程工具对软件工程的启发
人工智能·软件工程·agent·ai编程·ai agent·智能体·智能体平台
老猿AI洞察3 小时前
三万Token的底牌:Claude Opus 5提示词泄露,揭开AI“灵魂“的每一道锁
人工智能·ai
俊哥V3 小时前
每日 AI 研究简报 · 2026-07-26
人工智能·ai
Summer-Bright3 小时前
AI 芯片简报 07.21-07.24:NVIDIA Vera 亮剑、AMD 2nm GPU、Google 叛逃 CoWoS
大数据·人工智能·ai·自然语言处理·芯片·agi