Google A2A 프로토콜 깊게 파기

5장에서 의 윤곽을 잡았다. 이번 장에서는 의 실제 JSON 모양과 상태 머신을 끝까지 분해한다.


한 페이지 지도

의 등장인물은 다섯이다.

개념역할어디에 사는가
누구인가, 무엇을 할 수 있는가공개 URL의 JSON
처리 가능한 작업 한 가지Agent Card 안 배열
한 발화의 단위요청·응답 본문
스킬 호출의 작업 객체서버 상태
Task가 남긴 결과물Task 안

Card는 정적이다. Task와 Artifact는 동적이다. Message는 둘 사이를 흐른다.


Agent Card: 첫 만남의 명함

클라이언트가 가장 먼저 가져가는 건 다. 보통 https://example.com/.well-known/agent.json에서 받는다. Card 한 장에 목록과 스킴, 그리고 스트리밍· 같은 능력 플래그까지 들어있다.

# Verified against: https://a2a-protocol.org/latest/specification/
# Verified at: 2026-06-02
card = {
  "name": "Researcher",
  "description": "주제를 받아 보고서를 만든다.",
  "url": "https://example.com/a2a/v1",
  "version": "1.0.0",
  "capabilities": {
      "streaming": True,
      "pushNotifications": True,
  },
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["application/json"],
  "skills": [{
      "id": "research",
      "name": "Research Topic",
      "description": "주제 키워드를 보고서로 변환",
      "tags": ["research", "writing"],
  }],
  "securitySchemes": {"oauth": {"type": "oauth2"}},
}

Skill은 “메뉴판의 항목”

은 Card 안의 한 행이다. 사용자(또는 다른 에이전트)는 이 행을 보고 어떤 자격으로 를 만들 수 있는지를 판단한다.

각 스킬은 다음을 기술한다:

스킬이 곧 의 진입점이다.


Task 상태 머신

다이어그램 로딩…

이 강하게 보장한다. 다음과 같이 진행된다.

추가 옵션 상태: auth-required(인증 필요), rejected(거부). 모두 명시적이라는 점이 핵심이다. “사라진 Task”는 없다.


Message와 Part

에서 의 본문은 parts 배열이다. 한 메시지가 여러 종류의 데이터를 묶을 수 있다. 가 텍스트만 있을 거란 가정은 일찍 버리자.

# Verified against: https://a2a-protocol.org/latest/specification/
# Verified at: 2026-06-02
message = {
  "messageId": "msg-001",
  "role": "user",
  "contextId": "ctx-42",
  "parts": [
      {"text": "이 PDF 요약해줘.", "mediaType": "text/plain"},
      {"url": "https://example.com/doc.pdf", "mediaType": "application/pdf"},
  ],
}

파트 종류: text(문자열), data(구조화 JSON), url(파일 참조), raw(base64 바이트). 모든 파트에 mediaType을 달 수 있다.


message/send: 동기 요청

가장 단순한 입구다. 로 JSON-RPC message/send 메서드를 부르면 가 만들어진다. 의 가장 짧은 형태다.

# Verified against: https://a2a-protocol.org/latest/specification/
# Verified at: 2026-06-02
import httpx

async def send(card_url: str, text: str):
  body = {
      "jsonrpc": "2.0",
      "id": 1,
      "method": "message/send",
      "params": {
          "message": {
              "messageId": "m1",
              "role": "user",
              "parts": [{"text": text}],
          }
      },
  }
  async with httpx.AsyncClient() as c:
      r = await c.post(card_url, json=body)
      return r.json()  # → { result: { task: {...} } }

응답은 즉시 만들어진 Task 객체. 작업이 짧다면 그 자리에서 completed로 올 수도 있다.


message/stream: SSE 스트리밍

위에 얹은 부분 결과 채널이다. 진행 중 상태가 바뀔 때마다 이벤트가 푸시된다.

# Verified against: https://a2a-protocol.org/latest/specification/
# Verified at: 2026-06-02
import httpx, json

async def stream(card_url: str, text: str):
  body = {
      "jsonrpc": "2.0",
      "id": 2,
      "method": "message/stream",
      "params": {"message": {
          "messageId": "m1", "role": "user",
          "parts": [{"text": text}],
      }},
  }
  async with httpx.AsyncClient(timeout=None) as c:
      async with c.stream("POST", card_url, json=body) as r:
          async for line in r.aiter_lines():
              if line.startswith("data: "):
                  yield json.loads(line[6:])

Push 알림: 진짜 비동기

스트리밍은 연결을 잡고 있는 모델이다. 장시간 작업(수 시간짜리 분석)은 그 연결을 유지하기 부담스럽다.

은 클라이언트가 webhook URL을 등록해두면, 서버가 Task 상태가 바뀔 때 그 URL로 알림을 보낸다. 클라이언트는 그동안 손을 놓아도 된다.

모드연결적합한 작업
한 번초 단위
유지분 단위
끊김시간 단위 이상

세 모드는 배타적이지 않다. 같은 Task가 stream + push를 동시에 노출할 수도 있다.


Artifact: 결과물의 컨테이너

completed로 갈 때 함께 따라오는 게 다. 대화라면, Artifact는 산출물이다.

# Verified against: https://a2a-protocol.org/latest/specification/
# Verified at: 2026-06-02
artifact = {
  "artifactId": "rep-001",
  "name": "report.md",
  "description": "Q2 시장 분석",
  "parts": [
      {"text": "# 시장 분석\n..."},
      {"url": "https://cdn.example.com/charts/q2.png",
       "mediaType": "image/png"},
  ],
}

같은 Task가 여러 Artifact를 가질 수 있다. 보고서 + 차트 + 원본 데이터 → 세 개.


tasks/get과 tasks/cancel

는 서버에 살아 있는 객체다. 클라이언트는 언제든 를 조회하거나 취소할 수 있다. 흐름에서는 이 두 메서드가 생명선이다.

# Verified against: https://a2a-protocol.org/latest/specification/
# Verified at: 2026-06-02
async def get_task(card_url: str, task_id: str):
  body = {"jsonrpc": "2.0", "id": 3,
          "method": "tasks/get", "params": {"id": task_id}}
  async with httpx.AsyncClient() as c:
      r = await c.post(card_url, json=body)
      return r.json()["result"]

async def cancel_task(card_url: str, task_id: str):
  body = {"jsonrpc": "2.0", "id": 4,
          "method": "tasks/cancel", "params": {"id": task_id}}
  async with httpx.AsyncClient() as c:
      r = await c.post(card_url, json=body)
      return r.json()["result"]

cancel요청이다. 서버가 거부할 수도 있다(이미 completed라면).


인증과 확장

securitySchemes의 입구다. OAuth2, API Key, mTLS 등 OpenAPI 시큐리티 표준을 그대로 따른다. 같은 카드가 여러 스킴을 동시에 노출할 수도 있다.

은 공식 스키마 외 벤더별 필드를 담는 자리다. 표준 외 메타는 메시지·태스크·카드의 metadata 필드 또는 extensions 영역으로 흘려보낸다. 클라이언트는 모르는 확장을 조용히 무시해야 한다(forward-compatibility).


언제 무엇을 쓰는가

케이스모드이유
단순 질의응답즉시 완료
코드 생성처럼 진행 표시 필요UX 즉시성
야간 배치자원 절약
모바일·끊김 잦은 클라이언트push + tasks/get 폴링연결 의존도 ↓

세 모드를 같은 위에서 동시에 운영해도 된다. 클라이언트의 능력에 맞춰 골라 쓰면 된다.


다음 챕터로

는 에이전트끼리의 약속이다. 그런데 비슷해 보이는 사촌이 있다 — 도구를 표준화한다. 같은 기능을 MCP 로 노출할지 A2A 로 노출할지, 다음 장에서 결정 기준을 정한다.