에이전트 간 (A2A) 프로토콜
ADK-Rust는 네트워크 간 에이전트 통신을 위해 A2A Protocol v1.0.0을 구현합니다. 구현은 adk-server에 있으며 a2a-v1 기능 플래그 뒤에 있고, 모든 11개 JSON-RPC 작업, REST 바인딩, 에이전트 카드 검색, 버전 협상을 포함합니다. 사양이 허용하는 것보다 의미가 더 좁은 하나의 작업에 대해서는 Operation coverage를 참조하세요. 와이어 타입은 a2a-protocol-types에서 제공됩니다. 이는 @tomtom215의 Foundation 검증을 거친 Rust A2A SDK입니다(a2a-rust).
개요
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, IDs, metadata size) | §3.3 | ✅ |
Content-Type: application/a2a+json 응답에서 | §9 | ✅ |
| 첫 SSE 스트리밍 이벤트로서의 Task 객체 | §3.1.2 | ✅ |
| 멀티턴을 위한 컨텍스트 범위 Task 조회 | §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 엔드포인트(모든 11개 v1 작업; 작업 범위 참조)- 모든 작업을 위한 REST 라우트
- 모든 라우트에서의
A2A-Version헤더 협상
JSON-RPC 작업
모든 11개 A2A v1.0.0 작업이 지원됩니다:
| 메서드 | 설명 |
|---|---|
SendMessage | 메시지를 보내고, 작업을 생성/재개합니다 |
SendStreamingMessage | SendMessage와 동일하지만 SSE 스트림을 반환합니다 |
GetTask | ID로 작업을 검색 |
CancelTask | 실행 중인 작업을 취소 |
ListTasks | 필터링과 페이지 매김이 적용된 작업 목록 |
SubscribeToTask | SSE를 통해 작업 업데이트를 구독 |
CreateTaskPushNotificationConfig | 푸시 알림용 웹훅 등록 |
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로 전환한 뒤, 새 메시지를 history에 추가하고 처리를 계속합니다.
멱등성
같은 SendMessage 요청과 동일한 messageId가 중복되면, 다시 처리하지 않고 이전에 생성된 작업을 반환합니다. 이는 SendMessage와 SendStreamingMessage 모두에 적용됩니다.
푸시 알림 인증
클라이언트가 CreateTaskPushNotificationConfig를 통해 웹훅을 등록하면, 서버는 웹훅 전달에 다음 인증 헤더를 포함합니다:
Authorization: Bearer <credentials>—authentication필드에 bearer 자격 증명이 있을 때a2a-notification-token: <token>—token필드가 있을 때
두 헤더는 동시에 설정할 수 있습니다. SSRF 보호는 웹훅 URLs를 사설 IP 범위와 localhost에 대해 검증합니다.
입력 검증
모든 들어오는 요청은 처리 전에 검증됩니다:
| 검증 | 오류 |
|---|---|
| 부분이 0개인 메시지 | InvalidParams (-32602) |
| 비어 있거나 공백만 있는 messageId | InvalidParams (-32602) |
| messageId가 256자를 초과함 | InvalidParams (-32602) |
| 비어 있거나 공백만 있는 taskId | InvalidParams (-32602) |
| taskId가 256자를 초과함 | InvalidParams (-32602) |
| 메타데이터가 64 KB를 초과함 | InvalidParams (-32602) |
원격 에이전트 사용
원격 A2A 에이전트와 통신하려면 RemoteA2aAgent를 사용하세요:
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 discovery, SendMessage (실제 LLM를 사용하는 두 에이전트 모두), GetTask, ListTasks, CancelTask 오류 경로, SendStreamingMessage, push notification CRUD, GetExtendedAgentCard, version negotiation, 그리고 오류 경로.
모범 사례
- 기능을 정확하게 선언하기 — 에이전트가 실제로 지원하는 내용에 따라
streaming,pushNotifications를 설정하세요 - 긴 작업에는 스트리밍 사용하기 —
SendStreamingMessage는 클라이언트에 실시간 진행 상황을 제공합니다 - 다중 턴 흐름 처리하기 — 메시지 간 대화 상태를 유지하려면
contextId를 사용하세요 - 웹훅 URLs 검증하기 — SSRF 보호는 기본 제공되지만, 운영 환경에서는 HTTPS를 사용하세요
- 적절한 타임아웃 설정하기 — 원격 에이전트 호출에 대한 요청 타임아웃을 구성하세요
- 멱등성 사용하기 — 클라이언트는 동일한
messageId로SendMessage를 안전하게 재시도할 수 있습니다
관련
- LlmAgent — 에이전트 생성
- 다중 에이전트 시스템 — 하위 에이전트와 계층 구조
- 서버 배포 — 에이전트를 HTTP 서버로 실행하기
작업 범위
v1의 모든 11개 JSON-RPC 작업이 디스패치되고 구현됩니다.
| 작업 | 상태 |
|---|---|
SendMessage | 에이전트를 구동하고, 그 출력을 artifact로 기록합니다 |
SendStreamingMessage | 에이전트를 구동하고, 생성되는 대로 artifact 청크를 스트리밍합니다 |
GetTask, ListTasks | 전체 |
CancelTask | 전체 |
SubscribeToTask (tasks/resubscribe) | 스냅샷만 — 아래 참조 |
CreateTaskPushNotificationConfig, GetTaskPushNotificationConfig, ListTaskPushNotificationConfigs, DeleteTaskPushNotificationConfig | 전체 |
GetExtendedAgentCard | 전체 |
SubscribeToTask는 스냅샷입니다
이 작업은 task와 현재 상태를 반환한 다음 스트림을 닫습니다. 이후 업데이트를 전달하지 않으므로, 클라이언트는 진행 상황을 기다리기 위해 이를 기다리면 안 됩니다.
실시간 재연결에는 원래 요청보다 오래 지속되는 task별 이벤트 큐가 필요합니다. 참조 구현은 이를 A2A SDKs에서 가져오며 — adk-python와 adk-go 모두 tasks/resubscribe을 SDK의 큐 관리자에게 전적으로 위임하고, ADK 코드에서는 이를 구현하지 않습니다. 이 서버는 서버 런타임이 아니라 wire type을 제공하는 a2a-protocol-types 위에서 수작업으로 구성되었기 때문에, 아직 큐가 존재하지 않습니다.
라이브 업데이트가 필요할 때는 SendStreamingMessage를 사용하세요.
스트리밍 이벤트 계약
SendStreamingMessage는 에이전트 이벤트를 도착하는 대로 변환합니다:
| 에이전트 이벤트 | A2A 이벤트 |
|---|---|
| 첫 번째, 출력 전에 | Task, 그다음 TaskStatusUpdateEvent — Working |
내용, partial = true | TaskArtifactUpdateEvent — append, 마지막 청크 아님 |
내용, partial = false | TaskArtifactUpdateEvent — 최종 청크 |
| 스트림 종료 | TaskStatusUpdateEvent — Completed |
| 스트림 오류 | TaskStatusUpdateEvent — Failed |
모든 응답 청크는 하나의 아티팩트 ID를 공유하므로 클라이언트가 이를 다시 조합할 수 있습니다. 결합된 텍스트는
지속적으로 저장되므로, 나중에 GetTask가 반환하는 것은 스트리밍되었던 내용입니다. 이는 adk-python과 adk-go이 그들의 SDKs 위에서 구현하는 계약과 일치합니다.