Module 8: Project The Reservo Multi Agent System
One Blackboard for the Whole System
Description
The three pieces from lessons 03, 04, and 05 —pipeline, fan-out, handoff— each resolve one
complete sub-task. This lesson wires in the system's last structural piece, Blackboard (Module
6), with no change at all, and confirms something Module 6 already established but that's worth
seeing, once more, over this capstone's three new demos: not every pattern writes to the shared
state equally.
You'll run, in a single run, Luis's piece (pipeline, DOES write), Marta's piece (fan-out, does NOT
write), and Valentina's piece (handoff, also doesn't write) — and you'll see, with the
Blackboard's WRITE_SEQ as witness, that the sequence number doesn't advance a single time
during Marta's and Valentina's pieces.
Connection to the module
Blackboard, WriteLogEntry, and WRITE_SEQ are exactly the same class from Module 6 — same
fields (member, room, tier, hours, price_cents, booking_id, log), same write
method. This lesson adds nothing to it — it reuses the three pieces already wired in from lessons
03, 04, and 05 to confirm, with all three running in the same process, the "who writes" rule M6
established with Ana's request. Lesson 08 comes back to this exact same Blackboard for the
complete Demo C.
Analogy: the kitchen whiteboard, one shift with three different tables
M6's kitchen whiteboard doesn't fill up with everything that happens in the restaurant — only with what the rest of the kitchen might need later: which table ordered what, whether it's confirmed. This lesson is one full shift with three differently shaped tables: Luis's table, which ends in a real booking (that DOES go on the whiteboard); Marta's table, which only asked to compare prices and ask about a general policy (none of that is a booking, none of it goes on the whiteboard); and Valentina's table, which only quoted without confirming (that doesn't go on the whiteboard either, not yet).
Worked example: three members, one Blackboard
import ast
import concurrent.futures
import itertools
from dataclasses import dataclass, field
import reservo_tools as rt
def dispatch_parallel(tool_use_blocks, tools):
with concurrent.futures.ThreadPoolExecutor(max_workers=len(tool_use_blocks)) as pool:
futures = [pool.submit(tools[b["name"]], **b["input"]) for b in tool_use_blocks]
results = [f.result() for f in futures]
return [
{"type": "tool_result", "tool_use_id": b["id"], "content": str(r)}
for b, r in zip(tool_use_blocks, results)
]
def run_agent_parallel(question, model_script, tools, max_iterations=10):
messages = [{"role": "user", "content": question}]
for step in range(max_iterations):
turn = model_script[step]
messages.append({"role": "assistant", "content": turn["content"]})
if turn["stop_reason"] != "tool_use":
return turn, messages
tool_result_blocks = dispatch_parallel(turn["content"], tools)
messages.append({"role": "user", "content": tool_result_blocks})
raise RuntimeError(f"max_iterations reached ({max_iterations})")
def search_docs(query):
q = query.lower()
if "cancel" in q:
return "[cancellation-policy] You can cancel at no charge up until 2 hours before the reserved time."
if "no" in q and ("present" in q or "show" in q or "arrive" in q):
return ("[no-show-policy] If you don't show up for a confirmed booking and don't "
"cancel at least 2 hours in advance, Reservo charges 50% of the quoted "
"price as a no-show fee.")
return "No relevant policy was found for that question."
SPECIALISTS = {
"booking_agent": {"tools": {"get_quote": rt.get_quote, "book_room": rt.book_room}},
"policy_agent": {"tools": {"search_docs": search_docs}},
"pricing_agent": {"tools": {"get_quote": rt.get_quote}},
}
def run_specialist(name, task, model_script):
tools = SPECIALISTS[name]["tools"]
return run_agent_parallel(task, model_script, tools)
# ---- M3: pipeline, unchanged (lesson 03) ----
@dataclass
class PipelineStage:
kind: str
name: str
label: str
def last_tool_result(history):
for m in reversed(history):
if isinstance(m["content"], list):
for b in m["content"]:
if b["type"] == "tool_result":
try:
return ast.literal_eval(b["content"])
except (ValueError, SyntaxError):
return b["content"]
return None
def build_stage_task(stage, payload):
if stage.kind == "quote":
return f"Quote {payload['room']} {payload['tier']} {payload['hours']}h."
if stage.kind == "validate_policy":
return (f"What's the cancellation policy for a {payload['hours']}h booking "
f"of {payload['room']}, before confirming it?")
if stage.kind == "confirm":
if payload.get("cleared_to_book"):
return (f"Book {payload['room']} {payload['tier']} {payload['hours']}h "
f"for {payload['member']} -- the cancellation policy was already validated.")
return (f"Book {payload['room']} {payload['tier']} {payload['hours']}h "
f"for {payload['member']}.")
raise ValueError(f"don't know how to build the task for stage {stage.kind!r}")
def extract_payload(stage, history, payload):
new_payload = dict(payload)
result = last_tool_result(history)
if stage.kind == "quote":
new_payload["price_cents"] = result["price_cents"]
elif stage.kind == "validate_policy":
new_payload["cancellation_policy"] = result
new_payload["cleared_to_book"] = True
elif stage.kind == "confirm":
new_payload["booking_id"] = result["booking_id"]
new_payload["confirmed"] = result["confirmed"]
return new_payload
def run_pipeline(stages, model_scripts, initial_payload):
payload = dict(initial_payload)
trace = []
for i, stage in enumerate(stages):
task = build_stage_task(stage, payload)
final, history = run_specialist(stage.name, task, model_scripts[i])
payload = extract_payload(stage, history, payload)
trace.append({"stage": i + 1, "label": stage.label, "history": history})
return payload, trace
# ---- M5: handoff, unchanged (lesson 05) ----
HANDOFF_TOOL_NAME = "handoff_to_specialist"
@dataclass
class HandoffPackage:
sender: str
receiver: str
reason: str
task: str
context: dict = field(default_factory=dict)
def run_agent_with_handoff(question, model_script, tools, self_name, max_iterations=10):
messages = [{"role": "user", "content": question}]
for step in range(max_iterations):
turn = model_script[step]
messages.append({"role": "assistant", "content": turn["content"]})
if turn["stop_reason"] != "tool_use":
return turn, messages, None
block = turn["content"][0]
if block["name"] == HANDOFF_TOOL_NAME:
inp = block["input"]
package = HandoffPackage(
sender=self_name, receiver=inp["receiver"], reason=inp["reason"],
task=inp["task"], context=inp.get("context", {}),
)
return None, messages, package
tool_result_blocks = dispatch_parallel(turn["content"], tools)
messages.append({"role": "user", "content": tool_result_blocks})
raise RuntimeError(f"max_iterations reached ({max_iterations})")
def run_specialist_with_handoff(name, task, model_script):
tools = SPECIALISTS[name]["tools"]
return run_agent_with_handoff(task, model_script, tools, self_name=name)
def run_with_handoff(name, task, model_scripts):
final, history, package = run_specialist_with_handoff(name, task, model_scripts[name])
trace = [{"agent": name, "history": history, "package": package}]
if package is None:
return final, trace
receiver_final, receiver_history, receiver_package = run_specialist_with_handoff(
package.receiver, package.task, model_scripts[package.receiver],
)
trace.append({"agent": package.receiver, "history": receiver_history, "package": receiver_package})
return receiver_final, trace
# ---- M6: Blackboard, unchanged ----
WRITE_SEQ = itertools.count(1)
@dataclass
class WriteLogEntry:
seq: int
writer: str
field: str
value: object
@dataclass
class Blackboard:
member: str | None = None
room: str | None = None
tier: str | None = None
hours: int | None = None
price_cents: int | None = None
booking_id: int | None = None
log: list = field(default_factory=list)
def write(self, writer, **fields):
for key, value in fields.items():
setattr(self, key, value)
self.log.append(
WriteLogEntry(seq=next(WRITE_SEQ), writer=writer, field=key, value=value)
)
bb = Blackboard()
print("=== 1) Luis (pipeline): booking_agent DOES write -- there's a real booking ===")
PIPELINE_STAGES = [
PipelineStage(kind="quote", name="booking_agent", label="quote"),
PipelineStage(kind="validate_policy", name="policy_agent", label="validate the cancellation policy"),
PipelineStage(kind="confirm", name="booking_agent", label="confirm the booking"),
]
INITIAL_PAYLOAD = {"room": "Studio", "tier": "pro", "hours": 3, "member": "Luis"}
script_quote = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 3}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": "Studio pro 3h costs 9600 cents."}]},
]
script_policy = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "cancellation policy"}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": (
"You can cancel at no charge up until 2 hours before the reserved time. "
"There's nothing stopping this from being confirmed.")}]},
]
script_confirm = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Studio", "tier": "pro", "hours": 3, "member": "Luis"}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": "I booked Studio pro 3h for Luis (confirmation #1)."}]},
]
bb.write("supervisor", member="Luis")
payload, _ = run_pipeline(PIPELINE_STAGES, [script_quote, script_policy, script_confirm], INITIAL_PAYLOAD)
bb.write("booking_agent", room=payload["room"], tier=payload["tier"],
hours=payload["hours"], price_cents=payload["price_cents"])
bb.write("booking_agent", booking_id=payload["booking_id"])
print("Blackboard after Luis:", bb)
print()
print("=== 2) Marta (fan-out): neither pricing_agent nor policy_agent write -- they're comparisons, not a booking ===")
seq_before_marta = bb.log[-1].seq
script_pricing = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote", "input": {"room": "Studio", "tier": "pro", "hours": 2}},
{"type": "tool_use", "id": "toolu_02", "name": "get_quote", "input": {"room": "Boardroom", "tier": "pro", "hours": 2}},
]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Studio pro 2h: 6400 cents. Boardroom pro 2h: 12800 cents. Studio is the cheaper option of the two."}]},
]
run_specialist("pricing_agent", "Compare Studio and Boardroom pro 2h.", script_pricing)
print(f"seq of the last write BEFORE Marta: {seq_before_marta} -- after her fan-out: {bb.log[-1].seq} (no change)")
print()
print("=== 3) Valentina (handoff): also doesn't write -- the context stays local to the answer, not the Blackboard ===")
script_workshop_booking = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 4}}]},
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_02", "name": HANDOFF_TOOL_NAME,
"input": {"receiver": "policy_agent", "reason": "no-show question, outside my expertise",
"task": "what happens if a member doesn't show up for a confirmed booking?",
"context": {"room": "Studio", "tier": "pro", "hours": 4, "price_cents": 12800}}}]},
]
script_workshop_policy = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "what happens if i do not show up for my reservation"}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": (
"If you don't show up for a confirmed booking and don't cancel at least 2 hours in advance, "
"Reservo charges 50% of the quoted price as a no-show fee.")}]},
]
run_with_handoff("booking_agent", "Quote Studio pro 4h. What happens if I don't show up?",
{"booking_agent": script_workshop_booking, "policy_agent": script_workshop_policy})
print(f"seq of the last write after Valentina: {bb.log[-1].seq} (no change again)")
print()
print("--- the Blackboard's full log (one run, 3 different members) ---")
for entry in bb.log:
print(f" #{entry.seq} {entry.writer:<14} wrote {entry.field}={entry.value!r}")
What to expect:
=== 1) Luis (pipeline): booking_agent DOES write -- there's a real booking ===
Blackboard after Luis: Blackboard(member='Luis', room='Studio', tier='pro', hours=3, price_cents=9600, booking_id=1, log=[WriteLogEntry(seq=1, writer='supervisor', field='member', value='Luis'), WriteLogEntry(seq=2, writer='booking_agent', field='room', value='Studio'), WriteLogEntry(seq=3, writer='booking_agent', field='tier', value='pro'), WriteLogEntry(seq=4, writer='booking_agent', field='hours', value=3), WriteLogEntry(seq=5, writer='booking_agent', field='price_cents', value=9600), WriteLogEntry(seq=6, writer='booking_agent', field='booking_id', value=1)])
=== 2) Marta (fan-out): neither pricing_agent nor policy_agent write -- they're comparisons, not a booking ===
seq of the last write BEFORE Marta: 6 -- after her fan-out: 6 (no change)
=== 3) Valentina (handoff): also doesn't write -- the context stays local to the answer, not the Blackboard ===
seq of the last write after Valentina: 6 (no change again)
--- the Blackboard's full log (one run, 3 different members) ---
#1 supervisor wrote member='Luis'
#2 booking_agent wrote room='Studio'
#3 booking_agent wrote tier='pro'
#4 booking_agent wrote hours=3
#5 booking_agent wrote price_cents=9600
#6 booking_agent wrote booking_id=1
Six entries in the log, all from Luis's table — neither Marta nor Valentina add a single line,
even though both ran real specialists, with real tool calls, within the same process. The
Blackboard's seq is an honest witness to this: if Marta or Valentina had written anything,
bb.log[-1].seq would have advanced past 6 — and it doesn't, confirming in code what this
lesson's prose claims.
The criterion's question 3, applied three times
Does any data this sub-task produces get needed LATER in the same run, by
another part of the system we don't yet know who's going to read it?
Luis (pipeline) -> YES. The confirmed booking (room, tier, hours, price_cents,
booking_id) is a FACT of the system: if later another
part of Reservo needs to know whether Luis has an
active booking, that data has to be available without
anyone having to ask booking_agent again.
Marta (fan-out) -> NO. A price comparison and a general policy question
are information Marta consumes ONCE, in her answer --
there's no "system fact" another part needs later.
Nobody's going to ask "what did pricing_agent quote
Marta?" in another run.
Valentina (handoff) -> NOT YET. Quoting without confirming is information for
ONE answer, not a persistent fact -- if Valentina
later DOES confirm that booking (as she does in
lesson 08's full Demo C), THAT part of her request
does write to the Blackboard, with a different
pipeline Track.
The rule isn't "pipeline always writes, fan-out and handoff never do" — it's more precise than that: what gets written is whatever produces a confirmed fact the rest of the system might need later, regardless of which pattern produced it. In this lesson, the only sub-task that produces such a fact is Luis's confirmed booking — that's a coincidence of these three particular demos, not a fixed rule of the pattern. Lesson 08's Demo C confirms it: its pipeline track (Valentina's Focus booking) DOES write, while its fan-out track and its handoff track, again, don't.
Common mistakes
-
Concluding "fan-out and handoff never write to the Blackboard." It's not a rule of the pattern — it's a consequence of what each sub-task produces. A fan-out that DID confirm a booking (say, two independent bookings resolved at once) SHOULD write each one; this lesson doesn't show that because neither Marta's nor Valentina's piece confirms anything, not because the mechanism prevents it.
-
Writing to the Blackboard INSIDE a fan-out's parallel section. Although this lesson has no case that needs it, M6 L07's rule still stands for when it does: any write happens AFTER the
ThreadPoolExecutorcloses, never during, to avoid race conditions over the sameBlackboardobject. -
Thinking
WRITE_SEQresets with each newBlackboard(). As M7 L07 Exercise 2 already confirmed,WRITE_SEQis a process-level counter, not an instance-level one — if this lesson created a secondBlackboard()after the first, its first entry would continue from7, not from1.
Exercises
Exercise 1: Add a fourth table that DOES write, with fan-out (Easy)
Design a fourth piece —a new member, "Pedro"— whose request is a fan-out of TWO independent
bookings (for example, "Book Focus basic 1h for me, and separately book Boardroom basic 1h for my
colleague"), and confirm the Blackboard DOES receive writes from both, even though they run at
the same time.
See solution
# run_tracks_parallel: the same function from M4/M7, reused unchanged
# (you already wired it in in this module's lesson 04).
def run_tracks_parallel(jobs):
results = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=len(jobs)) as pool:
future_to_key = {pool.submit(fn): key for key, fn in jobs.items()}
for future in concurrent.futures.as_completed(future_to_key):
key = future_to_key[future]
results[key] = future.result()
return results
def job_book_focus_pedro():
script = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Focus", "tier": "basic", "hours": 1, "member": "Pedro"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "I booked Focus basic 1h for Pedro."}]},
]
return run_specialist("booking_agent", "Book Focus basic 1h for Pedro.", script)
def job_book_boardroom_colleague():
script = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Boardroom", "tier": "basic", "hours": 1, "member": "Pedro's colleague"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "I booked Boardroom basic 1h for Pedro's colleague."}]},
]
return run_specialist("booking_agent", "Book Boardroom basic 1h for the colleague.", script)
results_pedro = run_tracks_parallel({"focus": job_book_focus_pedro, "boardroom": job_book_boardroom_colleague})
bb.write("supervisor", member="Pedro")
for key in sorted(results_pedro):
final, history = results_pedro[key]
tool_result = next(
b["content"] for m in history if isinstance(m["content"], list)
for b in m["content"] if b["type"] == "tool_result"
)
print(f" [{key}] tool_result: {tool_result}")
Explanation (no need to print the full Blackboard): every job_* in this solution DOES call
book_room — unlike pricing_agent in Demo B, these two fan-out branches produce real bookings,
so —outside the parallel section, after run_tracks_parallel returns— each one should get written
to the Blackboard with its own bb.write("booking_agent", ...), confirming that "fan-out doesn't
write" was never a rule of the mechanism, only of what Demos A, B, and C in particular produce.
Exercise 2: Why does supervisor write member BEFORE running any specialist? (Medium)
In this lesson's three pieces, bb.write("supervisor", member=...) happens before running
booking_agent, policy_agent, or pricing_agent. Without running code, explain why this order
matters, using M6 L07's reasoning about Ana's request.
See solution
Because member is data the system knows from the moment the request arrives —it doesn't
depend on any specialist's result— so writing it first leaves the Blackboard in a useful state
even if, for some reason, none of the tracks finished running (a tool that fails, max_iterations
reached). If the order were reversed —writing member AFTER running the specialists—, any failure
mid-way would leave a Blackboard that doesn't even know who made the request, a worse state to
debug than an incomplete one that at least has the most basic piece of data already present. It's
the same reason M7 L07 wrote member as the whole Ana run's very first write, before opening the
parallel tracks.
Exercise 3: Design a request where the HANDOFF track should write (Hard)
This lesson showed Valentina's handoff doesn't write because it ends in a quote, not a
confirmation. Design, in one sentence, a Reservo request where a handoff WOULD end up producing a
fact worth writing to the Blackboard — and explain exactly which field(s) you'd write and why.
See solution
One valid example: "Book Focus pro 2h for me -- and if there's any availability problem, ask
someone else what options I have." If booking_agent, mid-way through trying to book, hit a
situation outside its usual expertise (say, a special overbooking policy that lives in
policy_agent) and handed off the turn, but the chain's final result DID end in a confirmed
booking (policy_agent authorizes the exception and someone —the flow itself, not necessarily
policy_agent— confirms the booking), then that result would have exactly the same fields worth
writing as Luis's pipeline: room, tier, hours, price_cents, booking_id. The criterion's
question 3 rule doesn't distinguish by PATTERN —it distinguishes by whether the final result is a
confirmed fact the rest of the system might need later—, and a handoff that ends in a real
confirmation meets that condition exactly the same way a pipeline does.
Summary and next step
Blackboard, with no change since Module 6, still only logs what another part of the system might need later — confirmed over three new members in a single run.- The rule isn't "the pattern decides whether it writes" — it's "the result decides": Luis's confirmed booking writes six entries; Marta's comparison and Valentina's unconfirmed quote, none.
WRITE_SEQstayed at6throughout Marta's and Valentina's entire pieces — the code witness that neither of them wrote anything.
Next lesson: 07 — Measuring the Coordination Cost of the Complete System. This module's only genuinely new piece: a function that counts calls, hops, and rounds for any combination of tracks — tested against Demos A and B.
Additional resources
- Anthropic — Multi-agent research system — A real system where only the facts another component needs later get persisted to shared state, not every intermediate step.
- Python — Dataclasses — The structure behind
BlackboardandWriteLogEntry, unchanged since Module 6. - Python —
itertools.count— The deterministic counter behindWRITE_SEQand_booking_ids, the reason this module never needsrandomoruuid4. - Anthropic — Building effective agents — The principle of keeping shared state minimal and explicit, the same discipline this lesson confirms over three new cases.