에이전트 간(A2A) 프로토콜
ADK-Rust는 네트워크 간 에이전트 통신을 위해 A2A Protocol v1.0.0을 구현합니다. 구현은 adk-server에서 a2a-v1 기능 플래그 뒤에 제공되며, 11개의 JSON-RPC 작업, REST 바인딩, 에이전트 카드 검색 및 버전 협상을 모두 지원합니다. 사양에서 허용하는 범위보다 의미가 좁은 하나의 작업에 대해서는 작업 지원 범위를 참조하세요. 와이어 타입은 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 + 토큰) | §13.2 | ✅ |
| INPUT_REQUIRED 다중 턴 재개 흐름 | §3.4.3 | ✅ |
| 입력 유효성 검사(parts, IDs, 메타데이터 크기) | §3.3 | ✅ |
응답의 Content-Type: application/a2a+json | §9 | ✅ |
| 스트리밍 이벤트에서 첫 번째로 전송되는 작업 객체 SSE | §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-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 캐싱이 적용된 에이전트 카드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"}]
}
}
}
응답에는 상태, 기록, 아티팩트가 포함된 작업 객체가 포함됩니다. 응답은 Content-Type: application/a2a+json을 사용합니다.
SendStreamingMessage
SendMessage과 동일한 요청 형식입니다. 다음을 포함하는 SSE 스트림을 반환합니다.
- 첫 번째 이벤트는 완전한
Task객체입니다(사양 §3.1.2에 따름). - 이후 이벤트는
TaskStatusUpdateEvent입니다(작업 중, 완료 등). - 아티팩트 이벤트는
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을 통해 웹훅을 등록하면 서버는 웹훅 전달 시 인증 헤더를 포함합니다.
Authorization: Bearer <credentials>—authentication필드에 bearer 자격 증명이 있는 경우a2a-notification-token: <token>—token필드가 있는 경우
두 헤더는 동시에 설정할 수 있습니다. SSRF 보호는 비공개 IP 범위 및 localhost에 대해 웹훅 URLs을 검증합니다.
입력 검증
처리 전에 모든 수신 요청의 유효성을 검증합니다:
| 유효성 검사 | 오류 |
|---|---|
| 부분이 0개인 메시지 | InvalidParams (-32602) |
| 비어 있거나 공백만 있는 messageId | InvalidParams (-32602) |
| 256자를 초과하는 messageId | InvalidParams (-32602) |
| 비어 있거나 공백만 있는 taskId | InvalidParams (-32602) |
| 256자를 초과하는 taskId | InvalidParams (-32602) |
| 64KB를 초과하는 메타데이터 | 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 예제 agent 두 개가 포함되어 있습니다.
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
client는 agent card 검색, SendMessage (실제 LLM를 사용하는 두 agent 모두), GetTask, ListTasks, CancelTask 오류 경로, SendStreamingMessage, push notification CRUD, GetExtendedAgentCard, 버전 협상 및 오류 경로를 검증합니다.
모범 사례
- 기능을 정확하게 선언 — agent가 실제로 지원하는 기능에 따라
streaming,pushNotifications을 설정합니다. - 장시간 작업에는 streaming 사용 —
SendStreamingMessage는 client에 실시간 진행 상황을 제공합니다. - 다중 turn 흐름 처리 — 메시지 간 conversation state를 유지하려면
contextId을 사용합니다. - webhook URLs 검증 — SSRF 보호 기능이 기본 제공되지만, production에서는 HTTPS를 사용합니다.
- 적절한 timeout 설정 — remote agent 호출에 대한 request timeout을 구성합니다.
- idempotency 사용 — client는 동일한
messageId와 함께SendMessage을 안전하게 재시도할 수 있습니다.
관련 항목
- LlmAgent — agent 생성
- Multi-Agent Systems — sub-agent 및 계층 구조
- Server Deployment — agent를 HTTP server로 실행
이전: ← Server | 다음: Evaluation →
작업 범위
v1 JSON-RPC의 11개 operation이 모두 dispatch되고 구현되어 있습니다.
| 작업 | 상태 |
|---|---|
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 |
하나의 응답에 포함된 모든 청크는 아티팩트 ID를 공유하므로 클라이언트가 이를 재조립할 수 있습니다. 결합된 텍스트는 지속되므로 이후 GetTask는 스트리밍된 내용을 반환합니다. 이는 adk-python 및 adk-go이 해당 SDKs을 통해 구현하는 계약과 일치합니다.