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 안의 한 행이다. 사용자(또는 다른 에이전트)는 이 행을 보고 어떤 자격으로 를 만들 수 있는지를 판단한다.
각 스킬은 다음을 기술한다:
id: 안정적 식별자. URL이나 로그에 그대로 박힌다.name/description: 사람용 설명. 이 라우팅 결정에 읽는다.tags: 검색·필터링 키.- (옵션)
inputModes/outputModes: 기본을 덮어쓸 때.
스킬이 곧 의 진입점이다.
Task 상태 머신
의 는 이 강하게 보장한다. 다음과 같이 진행된다.
submitted→ 요청을 받았다, 아직 시작 전.working→ 처리 중.input-required→ 사용자/호출자에게 추가 정보 요청, 일시 정지.completed→ 성공 종료.failed→ 오류 종료.canceled→ 외부 취소 명령.
추가 옵션 상태: 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 로 노출할지, 다음 장에서 결정 기준을 정한다.