エージェント間(A2A)プロトコル
ADK-Rust は、クロスネットワークのエージェント通信のために A2A Protocol v1.0.0 を実装しています。実装は adk-server 内で a2a-v1 機能フラグの背後にあり、11 個すべての JSON-RPC オペレーション、REST バインディング、エージェントカードの検出、およびバージョン交渉をカバーしています。仕様が許容するよりもセマンティクスが狭い唯一のオペレーションについては、Operation coverage を参照してください。ワイヤ型は a2a-protocol-types によって提供されます。これは、@tomtom215(a2a-rust)による、Foundation に検証された Rust の A2A SDK です。
概要
A2A は次のような場合に有用です:
- サードパーティのエージェントサービスと統合する場合
- 専門化されたエージェントを持つマイクロサービスアーキテクチャを構築する場合
- クロス言語のエージェント通信を可能にする場合(A2A クライアントを持つ任意の言語)
- エージェントシステム間で正式な契約を強制する場合
単純な内部編成であれば、A2A の代わりにローカルのサブエージェントを使うと、より良いパフォーマンスが得られます。
v1.0.0 への準拠
この実装は、A2A Protocol v1.0.0 仕様に完全に準拠しています:
| 機能 | 仕様セクション | 状態 |
|---|---|---|
| 機能宣言付きのエージェントカード | §8 | ✅ |
| すべてのタスク状態変更における RFC 3339 タイムスタンプ | §5.6.1 | ✅ |
SendMessage のメッセージ ID の冪等性 | §3.3.1 | ✅ |
| プッシュ通知の認証(Bearer + token) | §13.2 | ✅ |
| INPUT_REQUIRED のマルチターン再開フロー | §3.4.3 | ✅ |
| 入力検証(parts、ID、metadata サイズ) | §3.3 | ✅ |
Content-Type: application/a2a+json に対する応答 | §9 | ✅ |
| 最初の SSE ストリーミングイベントとしての Task オブジェクト | §3.1.2 | ✅ |
| マルチターン向けのコンテキストスコープ付きタスク検索 | §3.4.1 | ✅ |
バージョンネゴシエーション(A2A-Version ヘッダー) | §9.1 | ✅ |
| 状態マシンの検証(終端状態) | §4.1.3 | ✅ |
エージェントカード
すべてのA2Aエージェントは、/.well-known/agent-card.jsonにその機能、スキル、およびサポートされるインターフェースを記述したエージェントカードを公開します。
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),
);
エージェントカードには以下が含まれます:
- エージェント名、説明、バージョン
- プロトコルバインディングとバージョンを含むサポートされるインターフェース
- 機能:
streaming、pushNotifications、extendedAgentCard - エージェント設定から派生したスキル
- デフォルトの入力/出力モード
機能は現在、AgentCapabilitiesパラメータを通じて明示的に宣言されます。ハードコードされたデフォルトはもうありません。
A2A v1 によるエージェントの公開
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-2.5-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 キャッシュ付きのエージェントカードPOST /jsonrpc— JSON-RPC エンドポイント(v1 の 11 のすべてのオペレーション。 Operation coverage を参照)- すべてのオペレーション用の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"}]
}
}
}
レスポンスには、status、history、artifacts を含む Task オブジェクトが含まれます。レスポンスは Content-Type: application/a2a+json を使用します。
SendStreamingMessage
SendMessage と同じリクエスト形式です。次の内容を含む SSE ストリームを返します:
- 最初のイベントは完全な
Taskオブジェクトです(仕様 §3.1.2 に準拠) - 以降のイベントは
TaskStatusUpdateEventです(Working、Completed など) - アーティファクトのイベントは
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 保護は、webhook URLs がプライベート IP 範囲および localhost に対して有効であるかを検証します。
入力検証
すべての受信リクエストは、処理前に検証されます:
| 検証 | エラー |
|---|---|
| 部分がゼロのメッセージ | InvalidParams (-32602) |
| 空または空白のみの messageId | InvalidParams (-32602) |
| messageId が 256 文字を超える | InvalidParams (-32602) |
| taskId が空、または空白のみ | InvalidParams (-32602) |
| taskId が 256 文字を超える | InvalidParams (-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 | -32001 | 404 |
| TaskNotCancelable | -32002 | 409 |
| PushNotificationNotSupported | -32003 | 400 |
| UnsupportedOperation | -32004 | 400 |
| ContentTypeNotSupported | -32005 | 415 |
| InvalidAgentResponse | -32006 | 502 |
| VersionNotSupported | -32009 | 400 |
| InvalidParams | -32602 | 400 |
| MethodNotFound | -32601 | 404 |
| 内部 | -32603 | 500 |
サンプルの実行
完全な A2A v1.0.0 のサンプルエージェントが 2 つ含まれています:
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
クライアントは以下を検証します: agent card の検出、SendMessage(実際の LLM を持つ両方のエージェント)、GetTask、ListTasks、CancelTask のエラーパス、SendStreamingMessage、push notification CRUD、GetExtendedAgentCard、バージョン交渉、およびエラーパス。
ベストプラクティス
- 能力を正確に宣言する — エージェントが実際にサポートする内容に基づいて
streaming、pushNotificationsを設定する - 長い操作にはストリーミングを使う —
SendStreamingMessageはクライアントにリアルタイムの進行状況を提供する - 複数ターンのフローを処理する — メッセージ間で会話状態を維持するために
contextIdを使う - Webhook の URLs を検証する — SSRF 保護は組み込みだが、本番環境では HTTPS を使う
- 適切なタイムアウトを設定する — リモートエージェント呼び出しのリクエストタイムアウトを構成する
- 冪等性を使う — クライアントは同じ
messageIdでSendMessageを安全に再試行できる
関連
- LlmAgent — エージェントの作成
- Multi-Agent Systems — サブエージェントと階層
- Server Deployment — エージェントを HTTP サーバーとして実行する
前: ← Server | 次: Evaluation →
オペレーションのカバレッジ
v1 の JSON-RPC オペレーション 11 個すべてがディスパッチされ、実装されています。
| 操作 | ステータス |
|---|---|
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 = true | TaskArtifactUpdateEvent — append、最後のチャンクではない |
内容, partial = false | TaskArtifactUpdateEvent — 最終チャンク |
| ストリーム終了 | TaskStatusUpdateEvent — Completed |
| ストリームエラー | TaskStatusUpdateEvent — Failed |
すべての1回の応答のチャンクは、クライアントが再構成できるように同じアーティファクトIDを共有します。連結されたテキストは永続化されるため、後のGetTaskはストリームされた内容を返します。これは、adk-pythonとadk-goがそれらのSDKs上で実装する契約に一致します。