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.
Setting the backbone LLM of your agent¶
These notebooks run on Ollama by default, the setup the book teaches.
If you do nothing, nothing changes: make_llm() starts a local Ollama
service when one isn't already running.
To use OpenAI or Anthropic instead:
- Install the extra:
uv sync --extra openaioruv sync --extra anthropic. - Export
OPENAI_API_KEYorANTHROPIC_API_KEYbefore launching Jupyter. - Set
LLM_PROVIDER=openai(oranthropic), or passprovider="openai"tomake_llm(). A key on its own never switches providers, so one exported for unrelated work cannot reroute you off the Ollama path.
The switch applies wherever a notebook builds its LLM with make_llm().
A notebook that constructs OllamaLLM directly stays on Ollama regardless
of these settings.
If you opted in but forgot to export the key, you will be prompted for it.
That is the safe path on hosted kernels, and it keeps the key out of the
saved notebook. Setting OLLAMA_API_KEY alone routes Ollama to Ollama
Cloud.
Caveat: the examples are tuned for qwen3. Output on gpt-5 or Claude will differ from what is printed in the book, and prompt-sensitive examples (the ch09 evaluator pattern, ch08 supervised trajectories) may behave noticeably differently.
# 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 time
import urllib.error
import urllib.request
from llm_agents_from_scratch.notebook_utils import make_llm
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 a URL¶
from llm_agents_from_scratch.a2a import A2AAgentSpec
spec = await A2AAgentSpec.from_url(
"http://localhost:9200",
timeout=120.0,
)
print(f"name: {spec.name}")
print(f"url: {spec.url}")
print(f"timeout: {spec.timeout}")
print(spec.agent_card)
name: crewai-hailstone
url: http://localhost:9200
timeout: 120.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"
}
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: Letting an LLMAgent Resume a Peer Task Parked in input_required¶
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
coordinator_llm = make_llm()
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.
from llm_agents_from_scratch import LLMAgent
from llm_agents_from_scratch.a2a import LLMAgentA2AExecutor, build_agent_card
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
demo_llm = make_llm()
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"
4a. 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,
)
4b. 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.
4c. 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: Chaining a Peer Agent with a Local Subagent¶
from llm_agents_from_scratch import LLMAgentBuilder
from llm_agents_from_scratch.subagents import SubAgentSpec
hailstone_subagent = SubAgentSpec(
name="hailstone",
description="Computes Hailstone sequences using next_number.",
builder=LLMAgentBuilder(
llm=coordinator_llm,
tools=[SimpleFunctionTool(func=next_number)],
),
max_steps=20,
)
coordinator = await (
LLMAgentBuilder()
.with_llm(coordinator_llm)
.with_a2a_agent(spec)
.with_subagent(hailstone_subagent)
.build()
)
chain_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 hailstone subagent to continue "
"the hailstone sequence from that number until it reaches 1. "
"Report the full sequence from 20 to 1."
),
)
chain_handler = coordinator.run(chain_task, max_steps=10)
chain_result = await chain_handler
print(chain_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: I need to make the following tool-calls:
{
"id_": "1ea6404b-b0e7-48eb-8ba3-0e774e5c4406",
"tool_name": "from_scratch__use_subag...[TRUNCATED]
INFO
(llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the tool call to the 'hailstone' subagent with the task: 'Continue the hailstone sequence starting from 16 until it reaches 1. Rep...[TRUNCATED]
INFO
(llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the tool call to the 'hailstone' subagent with the task: 'Continue the hailstone sequence starting from 16 until it reache...[TRUNCATED]
INFO
(llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: from_scratch__use_subagent
[hailstone]
INFO
(llm_agents_fs.LLMAgent) : 🚀 Starting task: Continue the hailstone sequence starting from 16 until it reaches 1. Report the full sequence from 16 to 1.
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Continue the hailstone sequence starting from 16 until it reaches 1. Report the full sequence from 16 to 1.
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: from_scratch__use_skill
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: <skill_content name="stop-at-one"> # Stop At One Compute a full sequence from a starting number down to 1 using the `next_num...[TRUNCATED]
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "68788c9a-13a6-49a2-996d-e9827af31c77",
"tool_name": "next_number",
"argu...[TRUNCATED]
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the tool call to compute the next number in the hailstone sequence starting from 16.
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the tool call to compute the next number in the hailstone sequence starting from 16.
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 8
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "a07cff17-1dcf-4f99-8d89-8ba8482c032e",
"tool_name": "next_number",
"argu...[TRUNCATED]
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the tool call to compute the next number in the hailstone sequence starting from 8.
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the tool call to compute the next number in the hailstone sequence starting from 8.
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 4
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "2d9e9b9c-f975-44c4-9d95-b203b6689b14",
"tool_name": "next_number",
"argu...[TRUNCATED]
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the tool call to compute the next number in the hailstone sequence starting from 4.
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the tool call to compute the next number in the hailstone sequence starting from 4.
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 2
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ✅ Step Result: I need to make the following tool-calls:
{
"id_": "1d163155-2db7-4fdd-9750-c70ffe1200ff",
"tool_name": "next_number",
"argu...[TRUNCATED]
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : 🧠 New Step: Execute the tool call to compute the next number in the hailstone sequence starting from 2.
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Execute the tool call to compute the next number in the hailstone sequence starting from 2.
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: next_number
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: 1
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : ✅ Step Result: The tool returned 1, which means we've reached the end of the sequence. Let me compile the full hailstone sequence from 16 to 1: **Ful...[TRUNCATED]
[hailstone]
INFO
(llm_agents_fs.TaskHandler) : No new step required.
[hailstone]
INFO
(llm_agents_fs.LLMAgent) : 🏁 Task completed: The tool returned 1, which means we've reached the end of the sequence. Let me compile the full hailstone sequence from 16 to 1: **...[TRUNCATED]
INFO
(llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: The tool returned 1, which means we've reached the end of the sequence. Let me compile the full hailstone sequence from 16 to ...[TRUNCATED]
INFO
(llm_agents_fs.TaskHandler) : ✅ Step Result: The hailstone subagent has returned the sequence from 16 to 1: **16 → 8 → 4 → 2 → 1** Now I need to combine this with the first part o...[TRUNCATED]
INFO
(llm_agents_fs.TaskHandler) : No new step required.
INFO
(llm_agents_fs.LLMAgent) : 🏁 Task completed: The hailstone subagent has returned the sequence from 16 to 1: **16 → 8 → 4 → 2 → 1** Now I need to combine this with the first par...[TRUNCATED]
The hailstone subagent has returned the sequence from 16 to 1: **16 → 8 → 4 → 2 → 1** Now I need to combine this with the first part of the sequence that the crewai-hailstone agent computed (20, 10, 5, 16) to report the full sequence from 20 to 1. The complete sequence is: - First part (from crewai-hailstone): 20 → 10 → 5 → 16 - Second part (from hailstone subagent): 16 → 8 → 4 → 2 → 1 Combining these (noting that 16 appears in both, so we don't duplicate it): **Full sequence from 20 to 1: 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1** The task is complete. The full hailstone sequence starting at 20 and ending at 1 is: **20, 10, 5, 16, 8, 4, 2, 1**
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