Agent-to-Agent(A2A)协议

ADK-Rust 在 adk-server 中通过 a2a-v1 功能标志实现了用于跨网络 agent 通信的 A2A Protocol v1.0.0。该实现涵盖全部 11 个 JSON-RPC 操作、REST 绑定、agent 卡发现和版本协商。有关其中一个语义范围比规范允许的范围更窄的操作,请参阅操作覆盖范围。线协议类型由 a2a-protocol-types 提供——由 @tomtom215 提供的经 Foundation 验证的 Rust A2A SDK(a2a-rust)。

概述

A2A 适用于以下场景:

  • 与第三方 agent 服务集成
  • 构建由专用 agent 组成的微服务架构
  • 启用跨语言 agent 通信(任何具有 A2A 客户端的语言)
  • 在 agent 系统之间实施正式契约

对于简单的内部组织,建议使用本地子 agent,而不是使用 A2A,以获得更好的性能。

v1.0.0 合规性

该实现完全符合 A2A Protocol v1.0.0 规范:

功能规范章节状态
带有能力声明的代理卡片§8✅
所有任务状态变更均使用 RFC 3339 时间戳§5.6.1✅
SendMessage 的消息 ID 幂等性§3.3.1✅
推送通知身份验证(Bearer + 令牌)§13.2✅
INPUT_REQUIRED 多轮恢复流程§3.4.3✅
输入验证(部分、ID、元数据大小)§3.3✅
响应中的 Content-Type: application/a2a+json§9✅
作为第一个 SSE 流式事件的任务对象§3.1.2✅
面向多轮交互的上下文范围任务查找§3.4.1✅
版本协商(A2A-Version 标头)§9.1✅
状态机验证(终止状态)§4.1.3✅

Agent 卡片

每个 A2A agent 都会在 /.well-known/agent-card.json 提供一个 agent 卡片,用于描述其功能、技能和支持的接口。

use adk_server::a2a::v1::card::build_v1_agent_card;
use a2a_protocol_types::{AgentCapabilities, AgentSkill};

let card = build_v1_agent_card(
    "my-agent",
    "A helpful research agent",
    "http://localhost:3001/jsonrpc",
    "1.0.0",
    vec![AgentSkill {
        id: "research".to_string(),
        name: "Research & Summarize".to_string(),
        description: "Researches topics and produces structured summaries".to_string(),
        tags: vec!["research".to_string()],
        examples: None,
        input_modes: None,
        output_modes: None,
        security_requirements: None,
    }],
    AgentCapabilities::none()
        .with_streaming(true)
        .with_push_notifications(true),
);

Agent 卡片包括:

  • Agent 名称、描述和版本
  • 支持的接口及其协议绑定和版本
  • 功能:streaming、pushNotifications、extendedAgentCard
  • 从 agent 配置派生的技能
  • 默认输入/输出模式

现在通过 AgentCapabilities 参数显式声明功能——不再使用硬编码的默认值。

通过 A2A v1 暴露 Agent

使用 LLM 集成构建完整的 A2A v1.0.0 服务器:

use std::sync::Arc;
use a2a_protocol_types::{AgentCapabilities, AgentSkill};
use adk_agent::LlmAgentBuilder;
use adk_server::a2a::v1::card::{CachedAgentCard, build_v1_agent_card};
use adk_server::a2a::v1::executor::V1Executor;
use adk_server::a2a::v1::jsonrpc_handler::jsonrpc_handler;
use adk_server::a2a::v1::push::NoOpPushNotificationSender;
use adk_server::a2a::v1::request_handler::RequestHandler;
use adk_server::a2a::v1::rest_handler::rest_router;
use adk_server::a2a::v1::task_store::InMemoryTaskStore;
use adk_server::a2a::v1::version::version_negotiation;
use adk_runner::RunnerConfig;
use adk_session::InMemorySessionService;
use axum::Router;
use axum::routing::post;
use tokio::sync::RwLock;

// 1. Create your agent
let model = adk_model::GeminiModel::new(&api_key, "gemini-3.7-flash")?;
let agent = LlmAgentBuilder::new("my-agent")
    .description("A helpful agent")
    .model(Arc::new(model))
    .instruction("You are a helpful assistant.")
    .build()?;

// 2. Set up A2A infrastructure
let task_store = Arc::new(InMemoryTaskStore::new());
let executor = Arc::new(V1Executor::new(task_store.clone()));
let push_sender = Arc::new(NoOpPushNotificationSender);

// 3. Build agent card with capabilities
let card = build_v1_agent_card(
    "my-agent", "A helpful agent",
    "http://localhost:3001/jsonrpc", "1.0.0",
    vec![/* skills */],
    AgentCapabilities::none().with_streaming(true),
);
let cached_card = Arc::new(RwLock::new(CachedAgentCard::new(card)));

// 4. Create runner config for LLM invocation
let session_service = Arc::new(InMemorySessionService::new());
let runner_config = Arc::new(RunnerConfig {
    app_name: "my-agent".to_string(),
    agent: Arc::new(agent),
    session_service,
    artifact_service: None,
    memory_service: None,
    plugin_manager: None,
    run_config: None,
    compaction_config: None,
    context_cache_config: None,
    cache_capable: None,
    request_context: None,
    cancellation_token: None,
});

// 5. Wire up the handler and routes
let handler = Arc::new(RequestHandler::with_runner(
    executor, task_store, push_sender, cached_card, runner_config,
));

let app = Router::new()
    .route("/jsonrpc", post(jsonrpc_handler))
    .with_state(handler.clone())
    .merge(rest_router(handler))
    .layer(axum::middleware::from_fn(version_negotiation));

// 6. Serve
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await?;
axum::serve(listener, app).await?;

这会暴露:

  • GET /.well-known/agent-card.json — 支持 ETag 缓存的 agent 卡片
  • POST /jsonrpc — JSON-RPC 端点(全部 11 个 v1 操作;请参阅操作覆盖范围)
  • REST 路由,用于所有操作
  • A2A-Version 标头协商,适用于所有路由

JSON-RPC 操作

支持全部 11 个 A2A v1.0.0 操作:

方法描述
SendMessage发送消息,创建/恢复任务
SendStreamingMessage与 SendMessage 相同,但返回 SSE 流
GetTask按 ID 检索任务
CancelTask取消正在运行的任务
ListTasks使用筛选和分页列出任务
SubscribeToTask通过 SSE 订阅任务更新
CreateTaskPushNotificationConfig注册用于推送通知的 webhook
GetTaskPushNotificationConfig获取推送通知配置
ListTaskPushNotificationConfigs列出任务的推送配置
DeleteTaskPushNotificationConfig移除推送通知配置
GetExtendedAgentCard获取扩展代理卡

SendMessage

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "msg-123",
      "role": "ROLE_USER",
      "parts": [{"text": "Research quantum computing"}]
    }
  }
}

响应包含一个带有状态、历史记录和工件的任务对象。响应使用 Content-Type: application/a2a+json。

SendStreamingMessage

请求格式与 SendMessage 相同。返回一个 SSE 流,其中:

  1. 第一个事件是完整的 Task 对象(依据规范 §3.1.2)
  2. 后续事件是 TaskStatusUpdateEvent(Working、Completed 等)
  3. 工件事件是 TaskArtifactUpdateEvent

多轮对话

当任务进入 INPUT_REQUIRED 状态时,使用相同的 contextId 发送后续消息以恢复任务:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "msg-456",
      "role": "ROLE_USER",
      "contextId": "ctx-original",
      "parts": [{"text": "Yes, include more details on error correction"}]
    }
  }
}

处理程序会自动通过 contextId 查找现有任务,将其从 INPUT_REQUIRED 转换为 Working,将新消息追加到历史记录,并继续处理。

幂等性

具有相同 messageId 的重复 SendMessage 请求会返回先前创建的任务,而不会重新处理。这同时适用于 SendMessage 和 SendStreamingMessage。

推送通知身份验证

当客户端通过 CreateTaskPushNotificationConfig 注册 webhook 时,服务器会在 webhook 传递中包含身份验证标头:

  • Authorization: Bearer <credentials> — 当 authentication 字段包含 bearer 凭据时
  • a2a-notification-token: <token> — 当存在 token 字段时

两个标头可以同时设置。SSRF 防护会根据私有 IP 范围和 localhost 验证 webhook URLs。

输入验证

所有传入请求都会在处理前经过验证:

验证错误
包含零个部分的消息InvalidParams (-32602)
为空或仅包含空白字符的 messageIdInvalidParams (-32602)
超过 256 个字符的 messageIdInvalidParams (-32602)
为空或仅包含空白字符的 taskIdInvalidParams (-32602)
超过 256 个字符的 taskIdInvalidParams (-32602)
超过 64 KB 的元数据InvalidParams (-32602)

使用远程代理

使用 RemoteA2aAgent 与远程 A2A 代理通信:

use adk_server::a2a::RemoteA2aAgent;

let remote_agent = RemoteA2aAgent::builder("prime_checker")
    .description("Checks if numbers are prime")
    .agent_url("http://localhost:8001")
    .build()?;

// Use as a sub-agent in a local agent hierarchy
let root_agent = LlmAgentBuilder::new("root")
    .model(Arc::new(model))
    .sub_agent(Arc::new(remote_agent))
    .build()?;

A2A 客户端

用于直接进行协议级通信:

use adk_server::a2a::client::v1_client::A2aV1Client;

// Discover agent card
let card = A2aV1Client::resolve_agent_card("http://localhost:3001").await?;
let client = A2aV1Client::new(card);

// Send message
let task = client.send_message(message).await?;

// Get task
let task = client.get_task(&task_id, Some(10)).await?;

// List tasks
let tasks = client.list_tasks(None, None, None, None).await?;

// Cancel task
client.cancel_task(&task_id).await?;

// Streaming
let response = client.send_streaming_message(message).await?;

// Push notification CRUD
let config = client.create_push_notification_config(config).await?;
client.delete_push_notification_config(&task_id, &config_id).await?;

错误处理

A2A 错误同时映射到 JSON-RPC 代码和 HTTP 状态码:

错误JSON-RPC 代码HTTP 状态
TaskNotFound-32001404
TaskNotCancelable-32002409
PushNotificationNotSupported-32003400
UnsupportedOperation-32004400
ContentTypeNotSupported-32005415
InvalidAgentResponse-32006502
VersionNotSupported-32009400
InvalidParams-32602400
MethodNotFound-32601404
内部-32603500

运行示例

其中包含两个完整的 A2A v1.0.0 示例代理:

cargo run --manifest-path examples/a2a-research-agent/Cargo.toml
cargo run --manifest-path examples/a2a-writing-agent/Cargo.toml --bin a2a-writing-agent
cargo run --manifest-path examples/a2a-writing-agent/Cargo.toml --bin client

客户端验证:代理卡片发现、SendMessage(两个代理都使用真实的 LLM)、GetTask、ListTasks、CancelTask 错误路径、SendStreamingMessage、推送通知 CRUD、GetExtendedAgentCard、版本协商以及错误路径。

最佳实践

  1. 准确声明能力 — 根据代理实际支持的功能设置 streaming、pushNotifications
  2. 对长时间运行的操作使用流式传输 — SendStreamingMessage 可向客户端提供实时进度
  3. 处理多轮流程 — 使用 contextId 在消息之间维护对话状态
  4. 验证 webhook URLs — 内置 SSRF 防护,但应在生产环境中使用 HTTPS
  5. 设置适当的超时时间 — 为远程代理调用配置请求超时
  6. 使用幂等性 — 客户端可以使用相同的 messageId 安全地重试 SendMessage

上一页:← 服务器 | 下一页:评估 →

操作覆盖范围

所有 11 个 v1 JSON-RPC 操作均已分派并实现。

操作状态
SendMessage驱动代理,并将其输出记录为工件
SendStreamingMessage驱动代理,并在工件块生成时对其进行流式传输
GetTask, ListTasks完整
CancelTask完整
SubscribeToTask (tasks/resubscribe)仅快照 — 见下文
CreateTaskPushNotificationConfig, GetTaskPushNotificationConfig, ListTaskPushNotificationConfigs, DeleteTaskPushNotificationConfig完整
GetExtendedAgentCard完整

SubscribeToTask 是快照

该操作返回任务及其当前状态,然后关闭流。它不会传递后续更新,因此客户端不得等待它来获取进度。

实时重新连接需要一个生命周期长于发起请求的、按任务划分的事件队列。参考实现从其 A2A SDKs 中获取该队列 — adk-python 和 adk-go 都将 tasks/resubscribe 完全委托给 SDK 的队列管理器,二者都没有在 ADK 代码中实现该功能。 此服务器基于 a2a-protocol-types 手动实现,而后者提供的是线路类型而非服务器运行时,因此队列目前尚不存在。

需要实时更新时,请使用 SendStreamingMessage。

流式事件契约

SendStreamingMessage 会在代理事件到达时对其进行转换:

代理事件A2A 事件
首先,在输出之前Task,然后 TaskStatusUpdateEvent — Working
内容,partial = trueTaskArtifactUpdateEvent — append,不是最后一个块
内容,partial = falseTaskArtifactUpdateEvent — 最后一块
流结束TaskStatusUpdateEvent — Completed
流错误TaskStatusUpdateEvent — Failed

同一响应的所有分块共享一个工件 ID。拼接后的文本会被持久化,因此后续的 GetTask 会返回已流式传输的内容。这与 adk-python 和 adk-go 通过其 SDKs 实现的契约一致。

Agent-to-Agent(A2A)协议 - ADK-Rust 文档 | ADK-Rust