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

  1. Thinking bb.price_cents still equals 6000 after 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 a Blackboard.

  2. Using history_of to make runtime decisions. The log's purpose is auditing after the run finished (or mid-run, to debug) — no specialist in this module consults history_of to decide what to do; all of them read the field directly (bb.price_cents), which always gives the current value.

  3. Forgetting that two writes to the same field, from the same agent, aren't an error. Unlike writing to a field with a different writer for 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.

  4. Thinking you need a corrected=True field or something similar to mark a correction. No — the log already implies it: if history_of(field) has more than one entry, there was a correction; the number of entries and their order are all the information you need.

  5. Running this example in a process that already had bookings. If book_room doesn't return booking_id: 1, the interpreter already ran book_room earlier 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 filtering bb.log by hand every time.
  • A write never 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

  1. Python — dataclasses — The module behind WriteLogEntry, with history_of filtering by each entry's field.
  2. 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.
  3. Python — List comprehensions — The mechanism behind history_of, filtering self.log by a simple predicate.
  4. Python — Value equality (==) — The comparison a stricter version of write could use to distinguish a reaffirmation from a real change, discussed in Exercise 3.