Skip to content

Commit e635728

Browse files
Add A2A Protocol tutorial (16-a2a-protocol)
1 parent 84bec9c commit e635728

4 files changed

Lines changed: 400 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# A2A Protocol — Agent-to-Agent Communication
2+
3+
This tutorial shows how to use the **A2A (Agent-to-Agent) Protocol** to expose Strands agents as network services and connect them together. You'll learn to wrap agents as servers, invoke them remotely, discover their capabilities, stream responses, and compose remote agents as tools.
4+
5+
## Tutorial Details
6+
7+
| Information | Details |
8+
|----------------------|--------------------------------------------------------------------------|
9+
| **Strands Features** | `A2AServer`, `A2AAgent`, `A2AClientToolProvider`, agent cards, streaming |
10+
| **Agent Pattern** | Multi-agent via network protocol (server ↔ client) |
11+
| **Tools** | `calculator` from strands-agents-tools |
12+
| **Model** | Amazon Nova Lite on Amazon Bedrock |
13+
14+
## How It Works
15+
16+
1. **A2AServer** wraps any Strands `Agent` and serves it over HTTP with an auto-generated agent card
17+
2. **A2AAgent** acts as a client that discovers and invokes remote agents
18+
3. The agent card at `/.well-known/agent-card.json` advertises capabilities, skills, and streaming support
19+
4. **A2AClientToolProvider** wraps remote agents as callable tools for an orchestrator agent
20+
21+
## Prerequisites
22+
23+
- Python 3.10 or later
24+
- AWS account with [Amazon Bedrock](https://aws.amazon.com/bedrock/) access configured
25+
- Basic familiarity with Strands Agents — see [01-first-agent](../01-first-agent/) if needed
26+
27+
## Tutorial Structure
28+
29+
```
30+
16-a2a-protocol/
31+
├── README.md
32+
├── requirements.txt
33+
└── a2a-protocol.ipynb
34+
```
35+
36+
| File | Description |
37+
|------|-------------|
38+
| [a2a-protocol.ipynb](./a2a-protocol.ipynb) | Step-by-step notebook covering server setup, client invocation, agent card discovery, streaming, and tool composition |
39+
40+
## What You'll Learn
41+
42+
- **A2AServer**: wrap a Strands agent and serve it over HTTP
43+
- **A2AAgent**: invoke a remote agent and get results
44+
- **Agent card discovery**: inspect capabilities, skills (auto-derived from tools), and streaming support
45+
- **Streaming patterns**: consume `A2AStreamEvent` for real-time responses
46+
- **A2AClientToolProvider**: compose remote agents as tools for an orchestrator agent
47+
48+
## Installation
49+
50+
Install the required dependencies:
51+
52+
```bash
53+
pip install -r requirements.txt
54+
```
55+
56+
Then open [a2a-protocol.ipynb](./a2a-protocol.ipynb) from the `16-a2a-protocol/` directory.
57+
58+
## Related Tutorials
59+
60+
- [10-agents-as-tools](../10-agents-as-tools/) — composing agents locally (A2A extends this over the network)
61+
- [04-streaming](../04-streaming/) — streaming fundamentals that A2A builds on
62+
- [11-swarm](../11-swarm/) — alternative multi-agent pattern using swarms
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# 16 — A2A Protocol: Agent-to-Agent Communication\n",
8+
"\n",
9+
"The **A2A (Agent-to-Agent) Protocol** lets you expose any Strands agent as a network service and invoke it from other agents or clients. This enables multi-agent architectures where agents run as independent services and communicate over HTTP.\n",
10+
"\n",
11+
"In this tutorial you will:\n",
12+
"1. Wrap a Strands agent as an **A2A server**\n",
13+
"2. Invoke it remotely with **A2AAgent**\n",
14+
"3. Discover its capabilities via the **agent card**\n",
15+
"4. Stream responses in real time\n",
16+
"5. Compose remote agents as **tools** for an orchestrator"
17+
]
18+
},
19+
{
20+
"cell_type": "markdown",
21+
"metadata": {},
22+
"source": [
23+
"## Prerequisites\n",
24+
"\n",
25+
"You need AWS credentials configured for Amazon Bedrock access. See [01-first-agent](../01-first-agent/) if you haven't set this up."
26+
]
27+
},
28+
{
29+
"cell_type": "code",
30+
"execution_count": null,
31+
"metadata": {},
32+
"outputs": [],
33+
"source": [
34+
"%pip install -U 'strands-agents[a2a]' 'strands-agents-tools[a2a_client]' strands-agents-tools -q"
35+
]
36+
},
37+
{
38+
"cell_type": "markdown",
39+
"metadata": {},
40+
"source": [
41+
"## Section 1: Introduction — What is A2A?\n",
42+
"\n",
43+
"When agents need to collaborate, they can run as separate services and talk to each other over the network. The A2A protocol provides:\n",
44+
"\n",
45+
"- **A2AServer** — wraps any `Agent` and serves it over HTTP\n",
46+
"- **Agent cards** — JSON metadata at `/.well-known/agent-card.json` describing capabilities\n",
47+
"- **A2AAgent** — a client that discovers and invokes remote agents\n",
48+
"- **A2AClientToolProvider** — wraps remote agents as tools for other agents\n",
49+
"\n",
50+
"This means you can build a calculator agent, a research agent, and a writing agent as independent services, then have an orchestrator agent call whichever one it needs."
51+
]
52+
},
53+
{
54+
"cell_type": "markdown",
55+
"metadata": {},
56+
"source": [
57+
"## Section 2: A2AServer — Wrapping an Agent as a Service\n",
58+
"\n",
59+
"Let's create a simple calculator agent and serve it over A2A. The server automatically generates an agent card from the agent's `name` and `description`."
60+
]
61+
},
62+
{
63+
"cell_type": "code",
64+
"execution_count": null,
65+
"metadata": {},
66+
"outputs": [],
67+
"source": [
68+
"from strands import Agent, tool\n",
69+
"from strands.models import BedrockModel\n",
70+
"\n",
71+
"\n",
72+
"@tool\n",
73+
"def calculator(expression: str) -> str:\n",
74+
" \"\"\"Evaluate a mathematical expression and return the result.\n",
75+
"\n",
76+
" Args:\n",
77+
" expression: A mathematical expression to evaluate (e.g., '2 + 2', '10 ** 6').\n",
78+
" \"\"\"\n",
79+
" try:\n",
80+
" result = eval(expression, {\"__builtins__\": {}}, {})\n",
81+
" return str(result)\n",
82+
" except Exception as e:\n",
83+
" return f\"Error: {e}\"\n",
84+
"\n",
85+
"\n",
86+
"model = BedrockModel(model_id=\"us.amazon.nova-lite-v1:0\")\n",
87+
"\n",
88+
"calculator_agent = Agent(\n",
89+
" model=model,\n",
90+
" tools=[calculator],\n",
91+
" name=\"Calculator Agent\",\n",
92+
" description=\"An agent that evaluates mathematical expressions.\",\n",
93+
" callback_handler=None,\n",
94+
")\n",
95+
"\n",
96+
"print(f\"Agent created: {calculator_agent.name}\")\n",
97+
"print(f\"Tools: {[t.__name__ if hasattr(t, '__name__') else str(t) for t in calculator_agent.tool_names]}\")"
98+
]
99+
},
100+
{
101+
"cell_type": "markdown",
102+
"metadata": {},
103+
"source": [
104+
"Now start the A2A server in a background thread so we can continue using this notebook:"
105+
]
106+
},
107+
{
108+
"cell_type": "code",
109+
"execution_count": null,
110+
"metadata": {},
111+
"outputs": [],
112+
"source": [
113+
"import threading\n",
114+
"import time\n",
115+
"\n",
116+
"from strands.multiagent.a2a import A2AServer\n",
117+
"\n",
118+
"a2a_server = A2AServer(agent=calculator_agent, port=9000, enable_a2a_compliant_streaming=True)\n",
119+
"\n",
120+
"server_thread = threading.Thread(target=a2a_server.serve, daemon=True)\n",
121+
"server_thread.start()\n",
122+
"time.sleep(2) # wait for server to start\n",
123+
"\n",
124+
"print(\"A2A server running at http://127.0.0.1:9000\")\n",
125+
"print(\"Agent card available at http://127.0.0.1:9000/.well-known/agent-card.json\")"
126+
]
127+
},
128+
{
129+
"cell_type": "markdown",
130+
"metadata": {},
131+
"source": [
132+
"## Section 3: A2AAgent — Invoking a Remote Agent\n",
133+
"\n",
134+
"`A2AAgent` is the client side. Point it at the server endpoint and call it just like a local agent — it returns an `AgentResult`."
135+
]
136+
},
137+
{
138+
"cell_type": "code",
139+
"execution_count": null,
140+
"metadata": {},
141+
"outputs": [],
142+
"source": [
143+
"from strands.agent.a2a_agent import A2AAgent\n",
144+
"\n",
145+
"a2a_client = A2AAgent(endpoint=\"http://127.0.0.1:9000\")\n",
146+
"\n",
147+
"result = a2a_client(\"What is 10 ** 6?\")\n",
148+
"print(\"Response:\", result.message)"
149+
]
150+
},
151+
{
152+
"cell_type": "markdown",
153+
"metadata": {},
154+
"source": [
155+
"The remote agent received the message, used its `calculator` tool, and returned the result — all over HTTP. Let's try a more complex expression:"
156+
]
157+
},
158+
{
159+
"cell_type": "code",
160+
"execution_count": null,
161+
"metadata": {},
162+
"outputs": [],
163+
"source": [
164+
"result = a2a_client(\"Calculate the square root of 144 and then multiply it by 7\")\n",
165+
"print(\"Response:\", result.message)"
166+
]
167+
},
168+
{
169+
"cell_type": "markdown",
170+
"metadata": {},
171+
"source": [
172+
"## Section 4: Agent Card Discovery\n",
173+
"\n",
174+
"Every A2A server publishes an **agent card** — a JSON document describing the agent's name, description, capabilities, and skills. Skills are auto-derived from the agent's tools.\n",
175+
"\n",
176+
"Let's fetch and inspect it:"
177+
]
178+
},
179+
{
180+
"cell_type": "code",
181+
"execution_count": null,
182+
"metadata": {},
183+
"outputs": [],
184+
"source": [
185+
"import json\n",
186+
"import urllib.request\n",
187+
"\n",
188+
"card_url = \"http://127.0.0.1:9000/.well-known/agent-card.json\"\n",
189+
"with urllib.request.urlopen(card_url) as resp:\n",
190+
" agent_card = json.loads(resp.read())\n",
191+
"\n",
192+
"print(\"=== Agent Card ===\")\n",
193+
"print(json.dumps(agent_card, indent=2))"
194+
]
195+
},
196+
{
197+
"cell_type": "markdown",
198+
"metadata": {},
199+
"source": [
200+
"Key fields in the agent card:\n",
201+
"- **name** / **description** — from the `Agent` constructor\n",
202+
"- **skills** — auto-generated from the agent's tools (each tool becomes a skill)\n",
203+
"- **capabilities** — indicates streaming support and other features\n",
204+
"\n",
205+
"This is how clients discover what a remote agent can do before invoking it."
206+
]
207+
},
208+
{
209+
"cell_type": "markdown",
210+
"metadata": {},
211+
"source": [
212+
"## Section 5: Streaming Patterns\n",
213+
"\n",
214+
"For long-running tasks, you can stream responses as they're generated. The `stream_async` method yields `A2AStreamEvent` objects:"
215+
]
216+
},
217+
{
218+
"cell_type": "code",
219+
"execution_count": null,
220+
"metadata": {},
221+
"outputs": [],
222+
"source": [
223+
"import asyncio\n",
224+
"\n",
225+
"\n",
226+
"async def stream_example():\n",
227+
" print(\"Streaming response:\")\n",
228+
" print(\"-\" * 40)\n",
229+
" async for event in a2a_client.stream_async(\n",
230+
" \"Explain step by step how to calculate 25 * 48\"\n",
231+
" ):\n",
232+
" if \"data\" in event:\n",
233+
" print(event[\"data\"], end=\"\", flush=True)\n",
234+
" print(\"\\n\" + \"-\" * 40)\n",
235+
" print(\"Stream complete.\")\n",
236+
"\n",
237+
"\n",
238+
"await stream_example()"
239+
]
240+
},
241+
{
242+
"cell_type": "markdown",
243+
"metadata": {},
244+
"source": [
245+
"Streaming is enabled by default on the server. Each event contains a chunk of the response text, letting you display results progressively in a UI or CLI."
246+
]
247+
},
248+
{
249+
"cell_type": "markdown",
250+
"metadata": {},
251+
"source": [
252+
"## Section 6: A2AClientToolProvider — Remote Agents as Tools\n",
253+
"\n",
254+
"The most powerful pattern: wrap remote A2A agents as **tools** that another agent can call. `A2AClientToolProvider` discovers agents from their URLs and exposes them as callable tools.\n",
255+
"\n",
256+
"This lets you build an orchestrator that delegates to specialized remote agents:"
257+
]
258+
},
259+
{
260+
"cell_type": "code",
261+
"execution_count": null,
262+
"metadata": {},
263+
"outputs": [],
264+
"source": [
265+
"from strands_tools.a2a_client import A2AClientToolProvider\n",
266+
"\n",
267+
"# Discover remote agents and wrap them as tools\n",
268+
"provider = A2AClientToolProvider(known_agent_urls=[\"http://127.0.0.1:9000\"])\n",
269+
"\n",
270+
"print(f\"Discovered {len(provider.tools)} remote agent tool(s):\")\n",
271+
"for t in provider.tools:\n",
272+
" name = t.__name__ if hasattr(t, \"__name__\") else str(t)\n",
273+
" print(f\" - {name}\")"
274+
]
275+
},
276+
{
277+
"cell_type": "code",
278+
"execution_count": null,
279+
"metadata": {},
280+
"outputs": [],
281+
"source": [
282+
"# Create an orchestrator agent that uses the remote calculator as a tool\n",
283+
"orchestrator = Agent(\n",
284+
" model=model,\n",
285+
" tools=provider.tools,\n",
286+
" system_prompt=\"You are a helpful assistant. Use the available tools to answer questions.\",\n",
287+
" callback_handler=None,\n",
288+
")\n",
289+
"\n",
290+
"result = orchestrator(\"What is 123 * 456?\")\n",
291+
"print(\"Orchestrator response:\", result.message)"
292+
]
293+
},
294+
{
295+
"cell_type": "markdown",
296+
"metadata": {},
297+
"source": [
298+
"The orchestrator doesn't know how to do math itself — it delegates to the remote Calculator Agent via A2A, gets the result, and presents it to the user."
299+
]
300+
},
301+
{
302+
"cell_type": "markdown",
303+
"metadata": {},
304+
"source": [
305+
"## What You Learned\n",
306+
"\n",
307+
"- **A2AServer** wraps any Strands agent and serves it over HTTP with an auto-generated agent card\n",
308+
"- **A2AAgent** invokes remote agents and returns `AgentResult` just like local calls\n",
309+
"- **Agent cards** at `/.well-known/agent-card.json` advertise capabilities and skills\n",
310+
"- **Streaming** with `stream_async` delivers responses progressively\n",
311+
"- **A2AClientToolProvider** wraps remote agents as tools, enabling orchestrator patterns\n",
312+
"\n",
313+
"### Next Steps\n",
314+
"\n",
315+
"- Deploy A2A servers as containers or Lambda functions (see [02-deploy](../../02-deploy/))\n",
316+
"- Combine with [11-swarm](../11-swarm/) for dynamic multi-agent teams\n",
317+
"- Add [05-guardrails](../05-guardrails/) to your A2A agents for content filtering"
318+
]
319+
}
320+
],
321+
"metadata": {
322+
"kernelspec": {
323+
"display_name": "Python 3",
324+
"language": "python",
325+
"name": "python3"
326+
},
327+
"language_info": {
328+
"name": "python",
329+
"version": "3.10.0"
330+
}
331+
},
332+
"nbformat": 4,
333+
"nbformat_minor": 4
334+
}

0 commit comments

Comments
 (0)