Chapter 10 — Assembling Interoperable MAS with A2A¶
Setup Instructions¶
To ensure you have the required dependencies to run this notebook, you'll need to have our llm-agents-from-scratch framework installed on the running Jupyter kernel. To do this, you can launch this notebook with the following command while within the project's root directory:
uv run --with jupyter jupyter lab
Alternatively, if you just want to use the published version of llm-agents-from-scratch without local development, you can install it from PyPi by uncommenting the cell below.
# Uncomment the line below to install `llm-agents-from-scratch` from PyPi
# !pip install llm-agents-from-scratch
Running an Ollama service¶
To execute the code provided in this notebook, you'll need to have Ollama installed on your local machine and have its LLM hosting service running. To download Ollama, follow the instructions found on this page: https://ollama.com/download. After downloading and installing Ollama, you can start a service by opening a terminal and running the command ollama serve.
import os, shutil, subprocess, time, urllib.request, urllib.error
def ensure_ollama(host="http://localhost:11434", timeout=15):
"""Start Ollama if not already running and wait until responsive."""
def _up():
try:
urllib.request.urlopen(f"{host}/api/tags", timeout=1)
return True
except (urllib.error.URLError, ConnectionError, TimeoutError):
return False
if _up():
return print(f"✓ Ollama already running at {host}")
# Lightning persistent path first, then standard locations
ollama_path = shutil.which("ollama")
if ollama_path is None:
for candidate in [
"/teamspace/studios/this_studio/.local/bin/ollama",
"/usr/local/bin/ollama",
"/usr/bin/ollama",
]:
if os.path.exists(candidate):
ollama_path = candidate
break
if ollama_path is None:
raise RuntimeError(
"Could not find the ollama binary. Install with: "
"curl -fsSL https://ollama.com/install.sh | sh"
)
print(f"Starting Ollama server ({ollama_path})...")
subprocess.Popen(
[ollama_path, "serve"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
deadline = time.time() + timeout
while time.time() < deadline:
if _up():
return print(f"✓ Ollama up and running at {host}")
time.sleep(0.5)
raise RuntimeError(f"Ollama did not start within {timeout}s")
use_cloud = "OLLAMA_API_KEY" in os.environ
ensure_ollama() if not use_cloud else print("✓ Using Ollama Cloud")
✓ Ollama already running at http://localhost:11434
The CrewAI Hailstone A2A server¶
The examples in this notebook demonstrate the framework's A2A integration using a toy peer that exposes the Hailstone sequence as an A2A skill, built with a different agent stack (CrewAI) than the rest of this repo. This is intentional: it's a genuine external peer, not another LLMAgent.
Unlike the MCP Hailstone server (Chapter 5), which speaks stdio and is spawned per-session automatically by MCPToolProvider, A2A peers are standalone HTTP services, and a client just points at a URL. This notebook launches the CrewAI Hailstone agent as a background process so it's reachable below.
Important: Run this notebook from within the project's root directory.
The code for the A2A server is located at: https://github.com/nerdai/llm-agents-from-scratch/tree/main/extra/a2a-crewai-hailstone
import subprocess, time, urllib.request, urllib.error
from pathlib import Path
def ensure_a2a_crewai_hailstone(
host="http://localhost:9200",
timeout=15,
):
"""Start the CrewAI Hailstone A2A server if not already running.
Returns the Popen handle if this call started the server, or
None if it was already running (so we know not to tear it down).
"""
def _up():
try:
urllib.request.urlopen(
f"{host}/.well-known/agent-card.json",
timeout=1,
)
return True
except (urllib.error.URLError, ConnectionError, TimeoutError):
return False
if _up():
print(f"✓ CrewAI Hailstone A2A server already running at {host}")
return None
server_path = Path.cwd().parent / "extra/a2a-crewai-hailstone"
print(f"Starting CrewAI Hailstone A2A server at {host}...")
process = subprocess.Popen(
["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "9200"],
cwd=server_path,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
deadline = time.time() + timeout
while time.time() < deadline:
if _up():
print(f"✓ CrewAI Hailstone A2A server up at {host}")
return process
time.sleep(0.5)
process.terminate()
raise RuntimeError(f"A2A server did not start within {timeout}s")
a2a_server_process = ensure_a2a_crewai_hailstone()
Starting CrewAI Hailstone A2A server at http://localhost:9200...
✓ CrewAI Hailstone A2A server up at http://localhost:9200
The from-scratch Hailstone A2A server¶
A second peer, alongside the CrewAI one, this time using this framework's own LLMAgent instead of a different stack. It's a standalone app under extra/a2a-from-scratch-hailstone/, not something built inline in this notebook: a genuine A2A peer is remote, an out-of-process black box with its own log stream, not a co-routine sharing the caller's process. This notebook launches it as a background process the same way as the CrewAI peer, with its own console-formatted logs captured to from_scratch_hailstone_server.log so its log stream stays genuinely separate from the coordinator's.
The code for this A2A server is located at: https://github.com/nerdai/llm-agents-from-scratch/blob/main/extra/a2a-from-scratch-hailstone/main.py
import subprocess, time, urllib.request, urllib.error
from pathlib import Path
from urllib.parse import urlparse
def ensure_from_scratch_hailstone(
host="http://127.0.0.1:9300",
timeout=15,
log_path="from_scratch_hailstone_server.log",
):
"""Start the from-scratch Hailstone A2A server if not already running.
Returns the Popen handle if this call started the server, or
None if it was already running (so we know not to tear it down).
Runs in its own session (start_new_session=True) so Cleanup can
kill the whole process group -- confirmed live that terminating
only the `uv run` wrapper process can leave the actual uvicorn
process orphaned and still bound to the port, the same way the
CrewAI helper's Popen handle could in principle (untested there
since it hasn't come up, but the risk is the same).
"""
def _up():
try:
with urllib.request.urlopen(
f"{host}/.well-known/agent-card.json",
timeout=1,
):
pass
return True
except (urllib.error.URLError, ConnectionError, TimeoutError):
return False
if _up():
print(f"✓ from-scratch-hailstone A2A server already running at {host}")
return None
server_path = Path.cwd().parent / "extra/a2a-from-scratch-hailstone"
port = urlparse(host).port or 80
print(f"Starting from-scratch-hailstone A2A server at {host}...")
with open(log_path, "w") as log_file:
process = subprocess.Popen(
["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", str(port)],
cwd=server_path,
stdout=log_file,
stderr=subprocess.STDOUT,
start_new_session=True,
)
deadline = time.time() + timeout
while time.time() < deadline:
if _up():
print(f"✓ from-scratch-hailstone A2A server up at {host}")
return process
time.sleep(0.5)
stop_from_scratch_hailstone(process)
raise RuntimeError(f"A2A server did not start within {timeout}s")
def stop_from_scratch_hailstone(process):
"""Kills the whole process group started by ensure_from_scratch_hailstone().
`process.terminate()` alone only signals the `uv run` wrapper --
confirmed live to leave the actual uvicorn process orphaned and
still bound to the port. `start_new_session=True` above put the
whole tree in its own process group, so signaling that group
(negative pid) reaches every descendant. Falls back to SIGKILL
if the group doesn't exit promptly on SIGTERM.
"""
import os
import signal
def _signal(sig):
try:
os.killpg(os.getpgid(process.pid), sig)
except ProcessLookupError:
pass
_signal(signal.SIGTERM)
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
_signal(signal.SIGKILL)
process.wait(timeout=5)
own_server_process = ensure_from_scratch_hailstone()
Starting from-scratch-hailstone A2A server at http://127.0.0.1:9300...
✓ from-scratch-hailstone A2A server up at http://127.0.0.1:9300
Examples¶
Example 1: Constructing an A2AAgentSpec from an Agent Card / URL¶
1a. From a URL¶
from llm_agents_from_scratch.a2a import A2AAgentSpec
spec = await A2AAgentSpec.from_url("http://localhost:9200")
print(f"name: {spec.name}")
print(f"url: {spec.url}")
print(f"timeout: {spec.timeout}")
print(spec.agent_card)
/home/nerdai/Projects/llm-agents-from-scratch/.venv/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
name: crewai-hailstone
url: http://localhost:9200
timeout: 60.0
name: "crewai-hailstone"
description: "Computes the full Hailstone (Collatz) sequence for a positive integer via a CrewAI agent."
supported_interfaces {
url: "http://localhost:9200"
protocol_binding: "JSONRPC"
protocol_version: "1.0"
}
version: "0.1.0"
capabilities {
streaming: false
}
default_input_modes: "text/plain"
default_output_modes: "text/plain"
skills {
id: "hailstone_sequence"
name: "hailstone_sequence"
description: "Compute the full hailstone sequence for a positive integer x, repeatedly applying x / 2 (if even) or 3x + 1 (if odd) until reaching 1. Asks for clarification if the task doesn\'t name a starting integer."
tags: "math"
}
1b. From an Agent Card¶
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill
from a2a.utils.constants import PROTOCOL_VERSION_1_0, TransportProtocol
# hand-built, no network call -- useful for tests, fixtures, or a peer
# whose card details you already know ahead of time
hand_built_card = AgentCard(
name="crewai-hailstone",
description="Computes the full Hailstone (Collatz) sequence.",
supported_interfaces=[
AgentInterface(
url="http://localhost:9200",
protocol_binding=TransportProtocol.JSONRPC,
protocol_version=PROTOCOL_VERSION_1_0,
),
],
version="0.1.0",
capabilities=AgentCapabilities(streaming=False),
default_input_modes=["text/plain"],
default_output_modes=["text/plain"],
skills=[
AgentSkill(
id="hailstone_sequence",
name="hailstone_sequence",
description="Compute the full hailstone sequence for x.",
tags=["math"],
),
],
)
spec_hand_built = A2AAgentSpec.from_agent_card(agent_card=hand_built_card)
print(f"spec name: {spec_hand_built.name}")
print(f"spec url: {spec_hand_built.url}")
print(f"spec catalog:\n{spec_hand_built.catalog()}")
spec name: crewai-hailstone
spec url: http://localhost:9200
spec catalog:
<a2a_agent>
<name>crewai-hailstone</name>
<description>Computes the full Hailstone (Collatz) sequence.</description>
<a2a_skills>
<a2a_skill>hailstone_sequence</a2a_skill>
</a2a_skills>
</a2a_agent>
Example 2: Building and Manually Calling UseA2AAgentTool¶
from llm_agents_from_scratch.a2a import UseA2AAgentTool
from llm_agents_from_scratch.data_structures import ToolCall
tool = UseA2AAgentTool(a2a_agents_registry={spec.name: spec})
tool_call = ToolCall(
tool_name="from_scratch__use_a2a_agent",
arguments={
"name": spec.name,
"task": "Give me the hailstone sequence starting at 4.",
},
)
result = await tool(tool_call=tool_call)
print(result.content)
4,2,1
Example 3: Resuming a Peer Task Parked in input_required¶
CrewAIHailstoneExecutor's ambiguity check is a plain regex for a digit -- a spelled-out number like "four" triggers TASK_STATE_INPUT_REQUIRED organically, not as a CrewAI judgment call.
3a. Manually Inspecting the Wrapped Response¶
from llm_agents_from_scratch.a2a import UseA2AAgentTool
from llm_agents_from_scratch.data_structures import ToolCall
tool = UseA2AAgentTool(a2a_agents_registry={spec.name: spec})
ambiguous_call = ToolCall(
tool_name="from_scratch__use_a2a_agent",
arguments={
"name": spec.name,
"task": "Compute the hailstone sequence starting at the number four.",
},
)
input_required_result = await tool(tool_call=ambiguous_call)
print(input_required_result.content)
The A2A agent 'crewai-hailstone' needs more information before it can continue: Which positive integer should I start the hailstone sequence from? To continue, call `from_scratch__use_a2a_agent` again with name='crewai-hailstone', task_id='b6562cab-54b5-4934-b386-634c07e02370', and `task` set to the requested information. This resumes the same remote task rather than starting a new one.
3b. Letting an LLMAgent Resume the Task¶
Needs a larger model than the rest of this notebook -- smaller local models (e.g. qwen3:14b) were observed to narrate making the resume call without ever actually issuing it. Requires ollama signin or OLLAMA_API_KEY set.
from llm_agents_from_scratch import LLMAgentBuilder
from llm_agents_from_scratch.data_structures import Task
from llm_agents_from_scratch.llms import OllamaLLM
coordinator_host = "https://ollama.com" if use_cloud else None
coordinator_model = "qwen3.5:397b-cloud" if use_cloud else "qwen3:14b"
coordinator_llm = OllamaLLM(
host=coordinator_host,
model=coordinator_model,
think=False,
json_prompt_mode=use_cloud,
)
coordinator = await (
LLMAgentBuilder()
.with_llm(coordinator_llm)
.with_a2a_agent(spec)
.build()
)
resume_task = Task(
instruction=(
"Pass the following task to the crewai-hailstone peer agent, "
"and ensure it completes the task successfully. "
"Task: Compute the hailstone sequence starting at the number four."
),
)
handler = coordinator.run(resume_task, max_steps=6)
result = await handler
print(result.content)
The hailstone sequence starting at the number 4 is: 4, 2, 1.
handler.rollout confirms the coordinator genuinely made both calls -- the ambiguous dispatch with the word "four", the peer's input_required question, and the resumed call with the digit 4 -- not a lucky single guess:
print(handler.rollout)
=== Task Step Start ===
💬 assistant: My current instruction is 'Pass the following task to the crewai-hailstone peer agent, and ensure it completes the task successfully. Task: Compute the hailstone sequence starting at the number four.'
💬 assistant: I need to make the following tool call(s):
{
"id_": "92ea669f-43c2-4863-8241-a443c543b4da",
"tool_name": "from_scratch__use_a2a_agent",
"arguments": {
"name": "crewai-hailstone",
"task": "Compute the hailstone sequence starting at the number four."
}
}.
🔧 tool: {
"tool_call_id": "92ea669f-43c2-4863-8241-a443c543b4da",
"content": "The A2A agent 'crewai-hailstone' needs more information before it can continue:\n\nWhich positive integer should I start the hailstone sequence from?\n\nTo continue, call `from_scratch__use_a2a_agent` again with name='crewai-hailstone', task_id='09967e28-722d-4c11-b8a1-379b18d3816d', and `task` set to the requested information. This resumes the same remote task rather than starting a new one.",
"error": false
}
💬 assistant: {"name": "from_scratch__use_a2a_agent", "arguments": {"name":"crewai-hailstone","task_id":"09967e28-722d-4c11-b8a1-379b18d3816d","task":"4"}}
=== Task Step End ===
=== Task Step Start ===
💬 assistant: My current instruction is 'The assistant should continue by providing the number '4' as the starting point for the hailstone sequence to the 'crewai-hailstone' agent.'
💬 assistant: I need to call the `from_scratch__use_a2a_agent` tool with the name 'crewai-hailstone' and the task set to '4' to provide the starting point for the hailstone sequence.
=== Task Step End ===
=== Task Step Start ===
💬 assistant: My current instruction is 'I need to call the `from_scratch__use_a2a_agent` tool with the name 'crewai-hailstone' and the task set to '4' to provide the starting point for the hailstone sequence.'
💬 assistant: I need to make the following tool call(s):
{
"id_": "92a53230-8653-41eb-bdc9-01dd7e38db82",
"tool_name": "from_scratch__use_a2a_agent",
"arguments": {
"name": "crewai-hailstone",
"task": "4"
}
}.
🔧 tool: {
"tool_call_id": "92a53230-8653-41eb-bdc9-01dd7e38db82",
"content": "4,2,1",
"error": false
}
💬 assistant: The hailstone sequence starting at the number 4 is: 4, 2, 1.
=== Task Step End ===
Example 4: Standing Up LLMAgentA2AExecutor as an A2A Server¶
First, purely for demonstration, builds the same LLMAgentA2AExecutor + build_agent_card() construction extra/a2a-from-scratch-hailstone/main.py uses to actually serve this agent (already running, started above) -- nothing here is wired into a running server. The rest of this example dispatches to that real server, a genuinely separate OS process, no different from dispatching to the CrewAI peer in Examples 1-2.
4a. Building LLMAgentA2AExecutor and build_agent_card()¶
from llm_agents_from_scratch import LLMAgent
from llm_agents_from_scratch.a2a import LLMAgentA2AExecutor, build_agent_card
from llm_agents_from_scratch.llms import OllamaLLM
from llm_agents_from_scratch.tools import SimpleFunctionTool
def next_number(x: int) -> int:
if x % 2 == 0:
return x // 2
return 3 * x + 1
model = "qwen3.5:397b-cloud" if use_cloud else "qwen3:14b"
demo_host = "https://ollama.com" if use_cloud else None
demo_llm = OllamaLLM(host=demo_host, model=model, think=False, json_prompt_mode=use_cloud)
hailstone_agent = LLMAgent(llm=demo_llm, tools=[SimpleFunctionTool(func=next_number)])
demo_executor = LLMAgentA2AExecutor(agent=hailstone_agent)
demo_card = build_agent_card(
name="from-scratch-hailstone",
description="Computes the full Hailstone (Collatz) sequence for a positive integer.",
url="http://127.0.0.1:9300",
)
print(demo_card)
name: "from-scratch-hailstone"
description: "Computes the full Hailstone (Collatz) sequence for a positive integer."
supported_interfaces {
url: "http://127.0.0.1:9300"
protocol_binding: "JSONRPC"
protocol_version: "1.0"
}
version: "0.1.0"
capabilities {
streaming: false
}
default_input_modes: "text/plain"
default_output_modes: "text/plain"
4b. Discovering the Server¶
from llm_agents_from_scratch.a2a import A2AAgentSpec
own_spec = await A2AAgentSpec.from_url(
"http://127.0.0.1:9300",
timeout=120.0,
)
4c. Dispatching via the Coordinator¶
Reuses the coordinator from Example 3 rather than dispatching manually -- registering the new peer is just adding it to a2a_agents_registry, same dict with_a2a_agent() builds. Console logging is enabled here so the coordinator's own step-by-step logs print live, right in this process -- genuinely separate from the served agent's own log stream in from_scratch_hailstone_server.log, seen next.
import logging
from llm_agents_from_scratch.data_structures import Task
from llm_agents_from_scratch.logger import enable_console_logging
enable_console_logging(logging.INFO)
coordinator.a2a_agents_registry[own_spec.name] = own_spec
hailstone_task = Task(
instruction=(
"Ask the from-scratch-hailstone peer agent to compute the "
"hailstone sequence starting at 8, until it reaches 1."
),
)
hailstone_handler = coordinator.run(hailstone_task, max_steps=15)
hailstone_result = await hailstone_handler
print(hailstone_result.content)
INFO
(llm_agents_fs.LLMAgent) : 🚀 Starting task: Ask the from-scratch-hailstone peer agent to compute the hailstone sequence starting at 8, until it reaches 1.
INFO
(llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Ask the from-scratch-hailstone peer agent to compute the hailstone sequence starting at 8, until it reaches 1.
INFO
(llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: from_scratch__use_a2a_agent
INFO
(llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: The next number in the hailstone sequence starting at 2 is 1. The sequence has now reached 1, so the computation is complete. ...[TRUNCATED]
INFO
(llm_agents_fs.TaskHandler) : ✅ Step Result: The hailstone sequence starting at 8 is: 8, 4, 2, 1. The computation is complete as it has reached 1.
INFO
(llm_agents_fs.TaskHandler) : No new step required.
INFO
(llm_agents_fs.LLMAgent) : 🏁 Task completed: The hailstone sequence starting at 8 is: 8, 4, 2, 1. The computation is complete as it has reached 1.
The hailstone sequence starting at 8 is: 8, 4, 2, 1. The computation is complete as it has reached 1.
4d. The Server's Own Log Stream¶
Two different processes, two different log streams: the coordinator's logs just printed above came from this notebook's own kernel process; the server's own logs went to from_scratch_hailstone_server.log the whole time, in its own process.
print(open("from_scratch_hailstone_server.log").read())
warning
:
`VIRTUAL_ENV=/home/nerdai/Projects/llm-agents-from-scratch/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
Uninstalled
1 package
in 0.26ms
Installed
1 package
in 0.95ms
INFO: Started server process [338576] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:9300 (Press CTRL+C to quit) INFO: 127.0.0.1:38922 - "GET /.well-known/agent-card.json HTTP/1.1" 200 OK INFO: 127.0.0.1:38750 - "GET /.well-known/agent-card.json HTTP/1.1" 200 OK INFO (llm_agents_fs.LLMAgent) : 🚀 Starting task: Compute the hailstone sequence starting at 8, until it reaches 1. INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Compute the hailstone sequence starting at 8, until it reaches 1. INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to call the next_number tool with x=8. INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the next_number tool with x=8. INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the next_number tool with x=8. INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 4 INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: The next number in the hailstone sequence starting at 8 is 4. I will now compute the subsequent numbers in the sequence until it reache...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Compute the next number in the hailstone sequence starting at 4. INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Compute the next number in the hailstone sequence starting at 4. INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to call the next_number tool with x=4. INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the next_number tool with x=4. INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the next_number tool with x=4. INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 2 INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: The next number in the hailstone sequence starting at 4 is 2. I will now compute the subsequent numbers in the sequence until it reache...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Compute the next number in the hailstone sequence starting at 2. INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Compute the next number in the hailstone sequence starting at 2. INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: I need to call the next_number tool with x=2. INFO (llm_agents_fs.TaskHandler) : 🧠 New Step: Call the next_number tool with x=2. INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the next_number tool with x=2. INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 1 INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: The next number in the hailstone sequence starting at 2 is 1. The sequence has now reached 1, so the computation is complete. The full ...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : No new step required. INFO (llm_agents_fs.LLMAgent) : 🏁 Task completed: The next number in the hailstone sequence starting at 2 is 1. The sequence has now reached 1, so the computation is complete. The fu...[TRUNCATED] INFO: 127.0.0.1:60898 - "POST / HTTP/1.1" 200 OK
Example 5: A Relay Hailstone Task¶
relay_task = Task(
instruction=(
"Ask the crewai-hailstone peer agent to compute the hailstone "
"sequence starting at 20, run 3 steps then stop. Take the last "
"number it reports, then ask the from-scratch-hailstone peer "
"agent to continue the hailstone sequence from that number "
"until it reaches 1. Report the full sequence from 20 to 1."
),
)
relay_handler = coordinator.run(relay_task, max_steps=10)
relay_result = await relay_handler
print(relay_result.content)
INFO
(llm_agents_fs.LLMAgent) : 🚀 Starting task: Ask the crewai-hailstone peer agent to compute the hailstone sequence starting at 20, run 3 steps then stop. Take the last number it ...[TRUNCATED]
INFO
(llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Ask the crewai-hailstone peer agent to compute the hailstone sequence starting at 20, run 3 steps then stop. Take the last number ...[TRUNCATED]
INFO
(llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: from_scratch__use_a2a_agent
INFO
(llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 20,10,5,16
INFO
(llm_agents_fs.TaskHandler) : ✅ Step Result: The last number reported by the crewai-hailstone agent is 16. I will now ask the from-scratch-hailstone agent to continue the hailstone...[TRUNCATED]
INFO
(llm_agents_fs.TaskHandler) : 🧠 New Step: Call the from-scratch-hailstone agent with the task: 'Continue the hailstone sequence from 16 until it reaches 1.'
INFO
(llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Call the from-scratch-hailstone agent with the task: 'Continue the hailstone sequence from 16 until it reaches 1.'
INFO
(llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: from_scratch__use_a2a_agent
INFO
(llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: Starting with 16, I will continue the hailstone sequence step by step until it reaches 1. 1. 16 is even, so the next number ...[TRUNCATED]
INFO
(llm_agents_fs.TaskHandler) : ✅ Step Result: The full hailstone sequence from 20 to 1 is: 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1.
INFO
(llm_agents_fs.TaskHandler) : No new step required.
INFO
(llm_agents_fs.LLMAgent) : 🏁 Task completed: The full hailstone sequence from 20 to 1 is: 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1.
The full hailstone sequence from 20 to 1 is: 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1.
print(relay_handler.rollout)
=== Task Step Start ===
💬 assistant: My current instruction is 'Ask the crewai-hailstone peer agent to compute the hailstone sequence starting at 20, run 3 steps then stop. Take the last number it reports, then ask the from-scratch-hailstone peer agent to continue the hailstone sequence from that number until it reaches 1. Report the full sequence from 20 to 1.'
💬 assistant: I need to make the following tool call(s):
{
"id_": "f5fea94c-ab0e-4534-9307-758395519367",
"tool_name": "from_scratch__use_a2a_agent",
"arguments": {
"name": "crewai-hailstone",
"task": "Compute the hailstone sequence starting at 20, run 3 steps then stop."
}
}.
🔧 tool: {
"tool_call_id": "f5fea94c-ab0e-4534-9307-758395519367",
"content": "20,10,5,16",
"error": false
}
💬 assistant: The last number reported by the crewai-hailstone agent is 16. I will now ask the from-scratch-hailstone agent to continue the hailstone sequence from 16 until it reaches 1.
=== Task Step End ===
=== Task Step Start ===
💬 assistant: My current instruction is 'Call the from-scratch-hailstone agent with the task: 'Continue the hailstone sequence from 16 until it reaches 1.''
💬 assistant: I need to make the following tool call(s):
{
"id_": "b60e378e-5ba1-4828-bafa-57dccda17c0a",
"tool_name": "from_scratch__use_a2a_agent",
"arguments": {
"name": "from-scratch-hailstone",
"task": "Continue the hailstone sequence from 16 until it reaches 1."
}
}.
🔧 tool: {
"tool_call_id": "b60e378e-5ba1-4828-bafa-57dccda17c0a",
"content": "Starting with 16, I will continue the hailstone sequence step by step until it reaches 1. \n\n1. 16 is even, so the next number is $16 / 2 = 8$.\n2. 8 is even, so the next number is $8 / 2 = 4$.\n3. 4 is even, so the next number is $4 / 2 = 2$.\n4. 2 is even, so the next number is $2 / 2 = 1$.\n\nThe sequence from 16 until it reaches 1 is: 16 → 8 → 4 → 2 → 1.",
"error": false
}
💬 assistant: The full hailstone sequence from 20 to 1 is: 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1.
=== Task Step End ===
Cleanup¶
import subprocess
if a2a_server_process is not None:
a2a_server_process.terminate()
try:
a2a_server_process.wait(timeout=5)
except subprocess.TimeoutExpired:
a2a_server_process.kill()
a2a_server_process.wait(timeout=5)
print("✓ Stopped CrewAI Hailstone A2A server")
else:
print("A2A server wasn't started by this notebook -- leaving it running")
if own_server_process is not None:
stop_from_scratch_hailstone(own_server_process)
print("✓ Stopped from-scratch-hailstone A2A server")
else:
print(
"from-scratch-hailstone A2A server wasn't started by "
"this notebook -- leaving it running",
)
✓ Stopped CrewAI Hailstone A2A server
✓ Stopped from-scratch-hailstone A2A server