에이전트 간 (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메시지를 보내고, 작업을 생성/재개합니다
SendStreamingMessageSendMessage와 동일하지만 SSE 스트림을 반환합니다
GetTaskID로 작업을 검색
CancelTask실행 중인 작업을 취소
ListTasks필터링과 페이지 매김이 적용된 작업 목록
SubscribeToTaskSSE를 통해 작업 업데이트를 구독
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 스트림을 반환합니다:

  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로 전환한 뒤, 새 메시지를 history에 추가하고 처리를 계속합니다.

멱등성

같은 SendMessage 요청과 동일한 messageId가 중복되면, 다시 처리하지 않고 이전에 생성된 작업을 반환합니다. 이는 SendMessageSendStreamingMessage 모두에 적용됩니다.

푸시 알림 인증

클라이언트가 CreateTaskPushNotificationConfig를 통해 웹훅을 등록하면, 서버는 웹훅 전달에 다음 인증 헤더를 포함합니다:

  • Authorization: Bearer <credentials>authentication 필드에 bearer 자격 증명이 있을 때
  • a2a-notification-token: <token>token 필드가 있을 때

두 헤더는 동시에 설정할 수 있습니다. SSRF 보호는 웹훅 URLs를 사설 IP 범위와 localhost에 대해 검증합니다.

입력 검증

모든 들어오는 요청은 처리 전에 검증됩니다:

검증오류
부분이 0개인 메시지InvalidParams (-32602)
비어 있거나 공백만 있는 messageIdInvalidParams (-32602)
messageId가 256자를 초과함InvalidParams (-32602)
비어 있거나 공백만 있는 taskIdInvalidParams (-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-32001404
TaskNotCancelable-32002409
PushNotificationNotSupported-32003400
UnsupportedOperation-32004400
ContentTypeNotSupported-32005415
InvalidAgentResponse-32006502
VersionNotSupported-32009400
InvalidParams-32602400
MethodNotFound-32601404
내부-32603500

예제 실행

완전한 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, 그리고 오류 경로.

모범 사례

  1. 기능을 정확하게 선언하기 — 에이전트가 실제로 지원하는 내용에 따라 streaming, pushNotifications를 설정하세요
  2. 긴 작업에는 스트리밍 사용하기SendStreamingMessage는 클라이언트에 실시간 진행 상황을 제공합니다
  3. 다중 턴 흐름 처리하기 — 메시지 간 대화 상태를 유지하려면 contextId를 사용하세요
  4. 웹훅 URLs 검증하기 — SSRF 보호는 기본 제공되지만, 운영 환경에서는 HTTPS를 사용하세요
  5. 적절한 타임아웃 설정하기 — 원격 에이전트 호출에 대한 요청 타임아웃을 구성하세요
  6. 멱등성 사용하기 — 클라이언트는 동일한 messageIdSendMessage를 안전하게 재시도할 수 있습니다

이전: ← 서버 | 다음: 평가 →

작업 범위

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, 그다음 TaskStatusUpdateEventWorking
내용, partial = trueTaskArtifactUpdateEventappend, 마지막 청크 아님
내용, partial = falseTaskArtifactUpdateEvent — 최종 청크
스트림 종료TaskStatusUpdateEventCompleted
스트림 오류TaskStatusUpdateEventFailed

모든 응답 청크는 하나의 아티팩트 ID를 공유하므로 클라이언트가 이를 다시 조합할 수 있습니다. 결합된 텍스트는 지속적으로 저장되므로, 나중에 GetTask가 반환하는 것은 스트리밍되었던 내용입니다. 이는 adk-python과 adk-go이 그들의 SDKs 위에서 구현하는 계약과 일치합니다.

에이전트 간 (A2A) 프로토콜 - ADK-Rust 문서 | ADK-Rust