Module 6: Shared State and the Blackboard Pattern
The Write Log
Description
Lessons 03 and 04 used bb.log in passing, to show the order writes happened in. This lesson stops
on the log itself: why it exists, what it guarantees, and what it reveals that the Blackboard's
current state —the six fields with their latest value— can't reveal on its own. The central case:
Ana changes rooms mid-run. booking_agent quotes Focus first, and then quotes Studio again,
correcting what it had already written. The Blackboard's final state only shows Studio — but the
log keeps the full correction, with both quotes, who wrote them, and in what order.
Without the log, that correction would disappear without a trace: anyone looking at the
Blackboard afterward would see room='Studio' as if that had always been the choice, with no way
to know Focus was there first. With the log, the correction is an auditable fact, not a lost
detail.
Connection to the module
This lesson extends Blackboard from lessons 02-04 with history_of, a new method that reads the
log filtered by field. It doesn't change write or any earlier structure. Lesson 07 cites the full
log of a four-role run again; lesson 08 uses it to diagnose the mini-project's "done wrong"
scenario.
Analogy: the medical history, not just today's diagnosis
A well-kept medical chart doesn't just say "the patient has X" — it says when X was diagnosed, what
the chart said before that diagnosis, and who wrote it. If a doctor corrects an earlier diagnosis,
that correction gets added to the chart, it doesn't erase what was there before: a treatment
already given based on the old diagnosis is still part of the patient's real history, even though
today we know that diagnosis was wrong. The Blackboard's log works the same way: it never erases
an earlier entry, it only adds the correction on top.
Worked example: Ana changes rooms mid-run
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})")
SPECIALISTS = {
"booking_agent": {
"tools": {
"list_rooms": rt.list_rooms, "get_quote": rt.get_quote,
"book_room": rt.book_room, "cancel_booking": rt.cancel_booking,
},
},
}
def run_specialist(name, task, model_script):
tools = SPECIALISTS[name]["tools"]
return run_agent_parallel(task, model_script, tools)
def all_tool_results(history):
import ast
results = []
for m in history:
if isinstance(m["content"], list):
for b in m["content"]:
if b["type"] == "tool_result":
try:
results.append(ast.literal_eval(b["content"]))
except (ValueError, SyntaxError):
results.append(b["content"])
return results
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)
)
def history_of(self, field_name):
"""Every log entry for ONE field, in order -- lets you see how
that value changed throughout the run, not just its current
state."""
return [e for e in self.log if e.field == field_name]
bb = Blackboard()
bb.write("supervisor", member="Ana")
print("--- booking_agent quotes Focus pro 3h and writes ---")
bb.write("booking_agent", room="Focus", tier="pro", hours=3, price_cents=6000)
print(f"room={bb.room!r} tier={bb.tier!r} hours={bb.hours!r} price_cents={bb.price_cents!r}")
print()
print("--- Ana changes rooms mid-run: 'Studio would be better, same duration' ---")
print("booking_agent quotes again and writes again -- SAME fields, new values")
bb.write("booking_agent", room="Studio", tier="pro", hours=3, price_cents=9600)
print(f"room={bb.room!r} tier={bb.tier!r} hours={bb.hours!r} price_cents={bb.price_cents!r}")
print()
print("--- booking_agent confirms the Studio booking ---")
model_script_book_studio = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Studio", "tier": "pro", "hours": 3, "member": "Ana"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "I booked Studio pro 3h for Ana (confirmation #1), 9600 cents."}]},
]
_, history_book = run_specialist("booking_agent", "Book Studio pro 3h for Ana.", model_script_book_studio)
(booking_result,) = all_tool_results(history_book)
bb.write("booking_agent", booking_id=booking_result["booking_id"])
print()
print("--- FINAL Blackboard state: only the latest value of each field ---")
print(f"room={bb.room!r} tier={bb.tier!r} hours={bb.hours!r} price_cents={bb.price_cents!r} booking_id={bb.booking_id!r}")
print()
print("--- the full log: the CORRECTION stays visible, even though the current state already covers it ---")
for entry in bb.log:
print(f" #{entry.seq} {entry.writer:<14} wrote {entry.field}={entry.value!r}")
print()
print("--- auditing ONE field with history_of: how price_cents changed during the run ---")
for entry in bb.history_of("price_cents"):
print(f" #{entry.seq} {entry.writer} set price_cents={entry.value}")
What to expect:
--- booking_agent quotes Focus pro 3h and writes ---
room='Focus' tier='pro' hours=3 price_cents=6000
--- Ana changes rooms mid-run: 'Studio would be better, same duration' ---
booking_agent quotes again and writes again -- SAME fields, new values
room='Studio' tier='pro' hours=3 price_cents=9600
--- booking_agent confirms the Studio booking ---
--- FINAL Blackboard state: only the latest value of each field ---
room='Studio' tier='pro' hours=3 price_cents=9600 booking_id=1
--- the full log: the CORRECTION stays visible, even though the current state already covers it ---
#1 supervisor wrote member='Ana'
#2 booking_agent wrote room='Focus'
#3 booking_agent wrote tier='pro'
#4 booking_agent wrote hours=3
#5 booking_agent wrote price_cents=6000
#6 booking_agent wrote room='Studio'
#7 booking_agent wrote tier='pro'
#8 booking_agent wrote hours=3
#9 booking_agent wrote price_cents=9600
#10 booking_agent wrote booking_id=1
--- auditing ONE field with history_of: how price_cents changed during the run ---
#5 booking_agent set price_cents=6000
#9 booking_agent set price_cents=9600
Read the two final sections carefully. The state (bb.room, bb.price_cents, etc.) only tells
you where everything ended up: Studio, 9600 cents. If someone asked you "why is the final price
9600 and not 6000?", the state alone can't answer that — you need the log. With
history_of("price_cents"), the answer is complete: booking_agent wrote 6000 first (#5), and
the same booking_agent corrected it to 9600 later (#9). No other write touched that field in
between — the correction came from the same agent, not interference from another.
Why the log never erases, only adds
Blackboard.write has no mechanism to "replace" a log entry — every call adds new entries, without
touching earlier ones. This is a deliberate design decision: if write overwrote the previous
price_cents entry instead of adding a new one, Ana's correction would disappear from the history
exactly as it disappears from the state — losing the only source that lets you reconstruct,
afterward, that there was a correction at all.
The cost of this decision is that the log grows without bound during a run — every write adds
entries, never reduces them. For a single Reservo request's run, that growth is trivial (dozens of
entries, not thousands). A system running huge numbers of requests over the same process, without
ever resetting the Blackboard, would need to think about this — but this guide never reuses a
Blackboard across runs (lesson 08 confirms it with a case where reusing it wrong is exactly
the mistake).
Common mistakes
-
Thinking
bb.price_centsstill equals6000after the correction. No — the state always reflects the most recent write; only the log, not the state, keeps the previous value. Confusing these two is the most common mistake when working with aBlackboard. -
Using
history_ofto make runtime decisions. The log's purpose is auditing after the run finished (or mid-run, to debug) — no specialist in this module consultshistory_ofto decide what to do; all of them read the field directly (bb.price_cents), which always gives the current value. -
Forgetting that two
writes to the same field, from the same agent, aren't an error. Unlike writing to a field with a differentwriterfor no clear reason (a signal of possible conflict, which lesson 06 touches on), the same agent correcting its own data in the same run is exactly this lesson's case — legitimate, and the log documents it unambiguously. -
Thinking you need a
corrected=Truefield or something similar to mark a correction. No — the log already implies it: ifhistory_of(field)has more than one entry, there was a correction; the number of entries and their order are all the information you need. -
Running this example in a process that already had bookings. If
book_roomdoesn't returnbooking_id: 1, the interpreter already ranbook_roomearlier in the same session.
Exercises
Exercise 1: Confirm your own run (Easy)
Run the worked example yourself and confirm that history_of("room") shows the two entries
(Focus, then Studio), in that order, with the correct sequence numbers.
See solution
for entry in bb.history_of("room"):
print(f" #{entry.seq} {entry.writer} set room={entry.value!r}")
Expected output:
#2 booking_agent set room='Focus'
#6 booking_agent set room='Studio'
If your output matches, you confirmed the log keeps both room writes in the correct order — the
same verification you already did with price_cents in the worked example.
Exercise 2: How many fields had NO correction at all? (Medium)
On the worked example's final Blackboard, count how many of the six fields have exactly one
entry in their history_of (no correction), versus how many have more than one.
See solution
ALL_FIELDS_L05 = ["member", "room", "tier", "hours", "price_cents", "booking_id"]
for f in ALL_FIELDS_L05:
n = len(bb.history_of(f))
print(f"{f:12} -> {n} write(s)")
Expected output:
member -> 1 write(s)
room -> 2 write(s)
tier -> 2 write(s)
hours -> 2 write(s)
price_cents -> 2 write(s)
booking_id -> 1 write(s)
Explanation: member and booking_id each got written once (the member didn't change, and the
booking was confirmed only once, at the end, already for Studio). room, tier, hours, and
price_cents each have two entries because all four got rewritten together, in the same write,
when Ana changed rooms — confirming what you already saw in lesson 02's Exercise 2: a write with
several fields leaves one log entry per field, not a single one.
Exercise 3: A field that "changes" to the same value (Hard)
Run bb.write("booking_agent", tier="pro") one more time, on the worked example's final bb —with
the same "pro" value it already had—. Does the log add a new entry? Should
Blackboard.write, as built in this lesson, distinguish between "writing a different value" and
"writing the same value again"?
See solution
before_count = len(bb.history_of("tier"))
bb.write("booking_agent", tier="pro")
after_count = len(bb.history_of("tier"))
print(f"'tier' entries before: {before_count}, after: {after_count}")
for entry in bb.history_of("tier"):
print(f" #{entry.seq} {entry.writer} set tier={entry.value!r}")
Expected output:
'tier' entries before: 2, after: 3
#3 booking_agent set tier='pro'
#7 booking_agent set tier='pro'
#11 booking_agent set tier='pro'
Explanation: yes, the log adds a third entry, even though the value didn't change —
Blackboard.write, as built in this lesson, logs every call to write for that field, without
comparing the new value against the previous one. This is a valid design decision, not a bug: for
this guide's purposes (auditing who touched each field, and when), someone "reaffirming" a value
without changing it is also useful information — for example, it confirms that booking_agent went
through that field again in a second write, even if the result was the same. A stricter version of
write could compare getattr(self, key) == value before adding an entry, but that would change
the log's purpose: from "every time someone wrote" to "every time something actually changed" — a
different decision, one this lesson doesn't make.
Summary and next step
- The
Blackboard's state (bb.room,bb.price_cents, etc.) always reflects the most recent write; the log keeps the full history, including corrections the state already covers. history_of(field_name)filters the log by a single field, letting you see how that value changed throughout the run — without that method, reconstructing a field's history would require filteringbb.logby hand every time.- A
writenever erases or replaces earlier log entries — it only adds. That's the property that makes it possible to audit corrections after they happened. - The log records every call to
write, even if the new value equals the previous one — an explicit design decision, not an oversight.
Next lesson: 06 — The trade-off measured: visibility vs. isolation. We count, with real
numbers, how much of the full Blackboard each agent sees, compared with the minimum a tailored
handoff package would need.
Additional resources
- Python —
dataclasses— The module behindWriteLogEntry, withhistory_offiltering by each entry'sfield. - Anthropic — Multi-agent research system — A real system where being able to reconstruct which sub-agent produced each piece of data, and when, is part of how unexpected results get debugged.
- Python — List comprehensions — The mechanism behind
history_of, filteringself.logby a simple predicate. - Python — Value equality (
==) — The comparison a stricter version ofwritecould use to distinguish a reaffirmation from a real change, discussed in Exercise 3.