The previous article covered MCP: an Agent connects to tool services via a standard protocol. Tools are passive — they wait to be called, execute, return a result.
But some scenarios require delegating to another Agent with autonomous decision-making, not just a tool:
When these three Agents need to collaborate, how do they communicate? Who knows who exists? How do they hand off work?
This is the problem A2A (Agent-to-Agent) Protocol solves.
MCP: Agent ←→ Tools/Data (vertical integration, Agent invokes tools)
A2A: Agent ←→ Agent (horizontal collaboration, Agent delegates to Agent)
Each Agent publishes an AgentCard
describing its capabilities:
from a2a.types import AgentCard, AgentSkill
research_card = AgentCard()
research_card.name = "research-agent"
research_card.description = "Gathers factual background on technical topics"
skill = AgentSkill()
skill.id = "research"
skill.name = "Research"
skill.description = "Collect key facts on a topic"
skill.tags.extend(["research", "facts"])
research_card.skills.append(skill)
Key AgentCard fields: name
, description
, skills
(each skill has tags
for discovery).
Think of it as the Agent equivalent of OpenAPI Spec — a machine-readable capability declaration that humans can also read.
Agents don't pass function calls to each other — they pass Task
objects:
from a2a.types import Task, TaskState, TaskStatus, Message, Part, Role
task = Task()
task.id = str(uuid.uuid4())
task.status.state = TaskState.TASK_STATE_SUBMITTED
msg = Message()
msg.role = Role.ROLE_USER
part = Part(); part.text = "Should I use Python or Go?"
msg.parts.append(part)
task.history.append(msg)
Task
has a lifecycle: SUBMITTED → WORKING → COMPLETED / FAILED
. On completion, the Agent appends the result as a ROLE_AGENT
Message.
AgentRegistry
stores all registered AgentCards and supports discovery by tag:
class AgentRegistry:
def register(self, card: AgentCard, handler: Callable[[Task], Task]) -> None:
self._agents[card.name] = AgentEntry(card=card, handler=handler)
def discover(self, tag: str) -> list[AgentCard]:
"""Return all agents whose skills include the given tag."""
...
def delegate(self, agent_name: str, input_text: str) -> Task:
"""Create a Task and execute it via the registered handler."""
...
Without a protocol, the orchestrator calls three Python functions directly:
def direct_orchestrator(question: str) -> str:
research = research_agent_fn(question) # hard dependency
analysis = analysis_agent_fn(research) # hard dependency
answer = writing_agent_fn(analysis) # hard dependency
return answer
Real execution output:
→ calling research_agent (direct)
→ calling analysis_agent (direct)
→ calling writing_agent (direct)
Answer: Choose Python if you need rapid development with broad libraries.
Select Go if performance and concurrency are critical...
Works perfectly. The problem is structural: the orchestrator has three hardcoded function references. Replace any one Agent, and you must edit the orchestrator. If the orchestrator lives in a different service, that means a cross-service code change.
Register three Agents, each with distinct skill tags:
[registry] registered: research-agent
[registry] registered: analysis-agent
[registry] registered: writing-agent
Discovery test:
researchers = registry.discover("research")
writers = registry.discover("writing")
The orchestrator never writes an agent name — it discovers by tag:
def a2a_orchestrator(question: str) -> str:
researchers = registry.discover("research")
t1 = registry.delegate(researchers[0].name, question)
analysts = registry.discover("analysis")
t2 = registry.delegate(analysts[0].name, task_output(t1))
writers = registry.discover("writing")
t3 = registry.delegate(writers[0].name, task_output(t2))
return task_output(t3)
Real execution output:
→ delegating to research-agent (discovered via tag)
→ delegating to analysis-agent (discovered via tag)
→ delegating to writing-agent (discovered via tag)
Answer: Choose Python for rapid development; Go for high-throughput performance...
The critical difference: the orchestrator code contains no agent names. Register a writing-agent-v2
with the same writing
tag, and the orchestrator discovers and uses it immediately — zero code changes.
A2A's most powerful use case: the LLM reads the AgentCard catalog and decides which Agents to call and in what order.
Show the LLM the agent catalog:
Available agents:
research-agent: Gathers factual background [skills: Research(research, facts)]
analysis-agent: Analyzes research notes [skills: Analysis(analysis, tradeoffs)]
writing-agent: Composes technical prose [skills: Writing(writing, prose)]
LLM outputs an execution plan:
["research-agent", "analysis-agent", "writing-agent"]
Execute the plan:
Executing 3 agents:
→ delegating to research-agent
→ delegating to analysis-agent
→ delegating to writing-agent
Final answer: Choose Python for rapid development; Go for high-throughput...
This is A2A's end state: no pre-configured orchestrator pipeline. The LLM reads the task requirements and the AgentCard descriptions, then plans the collaboration chain at runtime.
Dimension MCP A2A
──────────────────────────────────────────────────────────────────────
Problem solved Agent ↔ Tool/Data Agent ↔ Agent
Discovery list_tools() (tool catalog) discover() (Agent registry)
Unit of work Tool call (sync) Task (async-ready)
Coupling Agent invokes tool directly Orchestrator delegates task
Other end Passive tool service Autonomous Agent with logic
Cross-service Tool is an independent proc Agent is an independent service
Who decides Agent (tool use) Orchestrator (delegation)
Complete four-way selection guide:
Scenario Recommended
──────────────────────────────────────────────────────────────────────
Same codebase, call target known Direct function call
Agent needs external tools MCP (tools as service)
Agent delegates to specialist Agents A2A (agents as service)
Cross-org large-scale Agent network ANP (decentralized discovery)
AgentCard Design
description
: one sentence about what the Agent is good at — the LLM reads it for routing decisionsskill.tags
: use semantically clear tags (research
, analysis
, writing
), not version numbers or internal IDsAgentCard
should be machine-readable and human-readable (think OpenAPI style)Task Design
Task.id
: use UUID for tracking and idempotent retryhistory
(Message chain) for context passing — don't concatenate everything into part.text
ROLE_USER
(input) from ROLE_AGENT
(output) messagesRegistry and Discovery
LLM-Driven Routing
Five core takeaways:
Up next: Agent Evaluation Framework — how to systematically test Agents, which metrics matter, and how to use DeepEval.
Find more useful knowledge and interesting products on my Homepage