Skip to content

resource

Exact Spending Approval: Unit B Exercises

Study exact spending approval in Chapter 11. Unit B: diagnose a controlled failure, repair it and test a changed case in the student notebook.

Student edition · 90 minutes of dedicated work · 2026-09-09

Open in Colab Runs on Google Colab as it ships today, or on any local Python 3.12+ kernel.

This is one of two practical units for Chapter 11. Unit A constructs and connects the mechanism; Unit B investigates a controlled failure, repairs it and transfers the invariant. Each is a complete ninety-minute session, with its own setup and required conceptual introductions. Basic Python variables, conditions, loops, functions, lists and dictionaries are the starting knowledge. Libraries and specialized concepts used here are introduced below before the main task.

By the end you should be able to:

  1. Explain the chapter's mechanism using a prediction and an observed intermediate result.
  2. Repair the failure: Comparing only the next amount ignores money already reserved or spent. Propose real orders, approve them and inspect retained reservations. Reapproval must not reserve twice and refusal must leave the transaction unchanged.
  3. Solve apply the stricter cumulative spending ceiling using changed inputs and an independent expectation.
  4. Retain your implementation, failed/corrected observations, causal explanation and limits.
MinutesDedicated activityEvidence you produce
0–5State the problem and make a predictionInitial prediction in your own words
5–25Foundations and library examplesValues, explanations, revised predictions
25–35Trace setup and the main interfaceInput → learner function → observation
35–60Reproduce, diagnose and repairSource, visible checks and runtime evidence
60–80Implement and challenge the transfer taskFunction and a new counterexample
80–90Retrieve, explain and saveExit ticket and retained submission

Installation is preparation time. These are planning estimates, not measured completion times. Use the reference primers when a term is unfamiliar; in Unit B retrieve an explanation before re-reading it. Run All checks that the artifact executes. Unfinished student functions deliberately produce NEEDS_WORK. Keep your first attempt before opening answers.

This notebook belongs to the construction edition. Its supplied teaching runtime is embedded, so it can run without the textbook or another notebook. Where code uses REFERENCE_LESSON, that is the frozen runtime exercise identifier; the reader-facing chapter and saved unit identifiers use the current edition. Building against a supplied runtime is not proof that you have constructed all of its dependencies.

Run the self-contained setup

Open this notebook in Google Colab with the badge above, or use a local Python 3.12+ Jupyter kernel, with Pydantic 2. If needed, run %pip install "pydantic==2.13.4" once in a separate cell and restart the kernel. Package installation needs internet; the lesson itself needs no repository download, API key or prior notebook. The closing extension can use an OpenAI key from Colab's Secrets pane to put a live model behind the same tools; without a key it replays a recorded transcript and says so.

The collapsed cell contains 90 frozen teaching files. Base85 represents compressed bytes as text; zlib decompresses them; SHA-256 checks that the decoded files match this edition. These are supplied packaging operations, not learner algorithms. tempfile creates an isolated working copy; Path handles file locations; sys.path tells Python where the supplied modules live; a .pth file, the mechanism an editable install uses, tells reviewed local subprocesses the same. The code is available for inspection below and performs no package installation itself. The subsequent lesson teaches the libraries used by the mechanisms you will implement.

Run setup on every fresh kernel. It writes scratch runtime files separately from your retained practical-work/ch11-b folder. Rerunning setup restores the frozen support files and keeps your saved work. Restarting a kernel clears variables, not saved submission files. Source basis: Sovereign Agent 444c5f6. Some tasks use reviewed local subprocesses; they are not an OS sandbox.

Supplied offline setup and teaching files

The complete supplied setup cell is in the downloadable notebook. Run it there before attempting the cells below. This web edition omits only that compressed setup payload.

Commit to a prediction before the examples

python
prediction_notes = {
    "prediction": "Write the expected behavior before running the worked example.",
    "reason": "Name the input and rule behind that prediction.",
    "falsifier": "Name an observation that would prove the explanation wrong.",
    "revision": "After execution, explain what changed in your understanding.",
}

Reading the Python vocabulary used in this notebook

You need basic assignments, if, loops, functions, lists and dictionaries. The less familiar features used by the supplied code are introduced here. A library is reusable code that Python can import. The standard library ships with Python; Pydantic is an additional package. An import makes a name available, but does not mean that you have completed the exercise.

JSON is text for exchanging structured values. A Python dictionary is an in-memory object; the JSON representation is a string. Use json.dumps to encode and json.loads to decode. Decoding proves that text has valid JSON syntax, not that its fields match our business contract. Predict which of the following two decoded objects could describe a stock count.

python
import json

intro_data = {"sku": "MANGO", "count": 3}
intro_text = json.dumps(intro_data, sort_keys=True)
print(type(intro_data).__name__, type(intro_text).__name__, intro_text)
print(json.loads(intro_text))
print("Also valid JSON:", json.loads('["not", "a", "stock", "record"]'))
assert json.loads(intro_text) == intro_data

The first result is a dictionary; the second is a list. Before indexing a decoded object, check the shape that your function promises to accept. An exception interrupts the normal path. raise ValueError(...) refuses an invalid value; try/except lets a caller inspect that expected refusal. Catch the expected class, rather than turning every programming error into apparent success. finally runs cleanup even when an earlier operation raises.

An annotation, such as count: int, documents the expected type. It does not by itself enforce the type at runtime. A class defines a kind of object; an instance holds one object's data. @dataclass asks Python to generate routine construction and comparison methods from annotated fields. frozen=True prevents ordinary reassignment of the instance's fields; it does not make every object nested inside those fields immutable. A method is a function attached to a class; self refers to the instance receiving the call.

python
from dataclasses import dataclass


@dataclass(frozen=True)
class IntroObservation:
    operation: str
    count: int


intro_observation = IntroObservation("count-mango", 3)
print(intro_observation.operation, intro_observation.count)
assert intro_observation == IntroObservation("count-mango", 3)

A callback is a function passed to another function. This is how the classroom harness invokes your implementation. The argument candidate below is a function object; parentheses perform the call. Predict the two answers before execution, then trace the result to the callback.

python
def intro_apply(candidate, value):
    return {"input": value, "observed": candidate(value)}


def intro_double(value):
    return value * 2


print(intro_apply(intro_double, 3))
print(intro_apply(lambda value: value + 2, 3))
assert intro_apply(intro_double, 3)["observed"] == 6

lambda value: value + 2 is a small anonymous function. A closure is a function that retains access to values from its surrounding scope. It can bind a tool to a shop snapshot. A shallow copy duplicates only the outer container; copy.deepcopy also copies nested containers used in these fixtures. A set stores distinct values; required <= allowed asks whether every required item is allowed. frozenset is the corresponding immutable set. A tuple groups ordered values; (value,) is a one-item tuple, including the comma.

Paths and cleanup. Path represents a filesystem location. path / "file.json" constructs a child path; read_text and write_text read and write text. A context manager, used with with, manages entry and exit. A temporary-directory context removes its contents on exit. Save your submission outside temporary runtime directories. Reopening a file is different from reusing a Python variable: the former tests retained bytes, while the latter only tests this kernel.

python
from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as intro_folder:
    intro_path = Path(intro_folder) / "observation.json"
    intro_path.write_text(json.dumps(intro_data), encoding="utf-8")
    intro_reopened = json.loads(intro_path.read_text(encoding="utf-8"))
    assert intro_reopened == intro_data
    print("Read from a file:", intro_reopened)

Retrieval check: explain JSON versus a dictionary, annotation versus validation, class versus instance, and defining a callback versus invoking it. Change the callback above so an incorrect implementation visibly changes the observed output. This distinction will matter when grading your connected work. Reference: Python's JSON, dataclasses, and pathlib documentation.

Pydantic: turn an input dictionary into a checked object

Pydantic is an additional Python library for validating data. Its model is a class describing fields, not a neural network. Inherit from BaseModel, declare annotated fields, then call model_validate on incoming data. A field without a default is required. A field with a default can be omitted. The result is an instance whose values you read with dot notation.

python
from pydantic import BaseModel, ConfigDict, Field, ValidationError


class IntroCourseRequest(BaseModel):
    model_config = ConfigDict(strict=True, extra="forbid")
    name: str = Field(min_length=1)
    quantity: int = Field(gt=0, le=1000)
    note: str = ""


intro_request = IntroCourseRequest.model_validate({"name": "mango", "quantity": 4})
print(intro_request.name, intro_request.quantity, repr(intro_request.note))
assert intro_request.note == ""

The annotation says the field's type. Field supplies constraints: gt=0 means greater than zero, le=1000 means at most 1000, and min_length=1 excludes an empty name. ConfigDict sets model-wide behavior. strict=True rejects conversions for this integer field, including "4", 4.0 and True; extra="forbid" rejects undeclared keys. Pydantic can otherwise convert some compatible inputs, so choose this boundary deliberately rather than assuming every accepted input arrived in the expected type.

Predict which rule refuses each payload. ValidationError reports a failed contract. Its errors() entries contain loc, the field location, and type, the failure category. Catching that expected exception lets the notebook inspect the failure and continue.

python
intro_bad_requests = [
    {"name": "mango", "quantity": "4"},
    {"name": "mango", "quantity": True},
    {"name": "", "quantity": 4},
    {"name": "mango", "quantity": 0},
    {"name": "mango", "quantity": 4, "approved": True},
    {"quantity": 4},
]
for intro_bad_request in intro_bad_requests:
    try:
        IntroCourseRequest.model_validate(intro_bad_request)
    except ValidationError as intro_error:
        print([(item["loc"], item["type"]) for item in intro_error.errors(include_input=False)])
    else:
        raise AssertionError("An invalid input crossed the declared contract")

Use model_dump() for a Python dictionary, model_dump_json() for JSON text, and model_validate_json() to parse and validate JSON. model_json_schema() describes the contract; it is neither an instance's current values nor an invocation of the business handler.

python
intro_serialized = intro_request.model_dump_json()
intro_schema = IntroCourseRequest.model_json_schema()
assert IntroCourseRequest.model_validate_json(intro_serialized) == intro_request
print("Actual values:", intro_request.model_dump())
print("Quantity contract:", intro_schema["properties"]["quantity"])
assert intro_schema["properties"]["quantity"]["exclusiveMinimum"] == 0

Four is valid input to this schema even if the shop needs six. Pydantic checks the declared shape and constraints; the handler still needs authoritative stock, price and permission. Ordinary assignments to an existing instance are not automatically revalidated unless configured for assignment validation. This lesson validates new input at the boundary and uses the resulting values. Explain these limits before relying on a model object in a transaction or tool call.

Chapter 2's full introduction expands this pattern with a separate data-repair checkpoint. This notebook contains the required pattern here so prior Pydantic experience is not needed. References: models, fields, and strict mode.

SQLite from the first row to an atomic change

A dictionary disappears when its process ends. A database can retain records so a later process can resume from evidence. SQLite is an embedded database: Python's sqlite3 library opens a local database file without starting a separate database server. SQL is the language used to define, select and change its records. A table has named columns and rows. A primary key identifies a row; a query asks for rows satisfying a condition.

Start with a deliberately small preference table. Read the SQL as instructions: create the table, insert one named value, then select the value for one session. ? is a parameter placeholder; the values are passed separately so they are data, not SQL instructions. fetchone() returns one row or None; it does not guarantee that a matching row exists.

python
import sqlite3
from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as intro_sql_folder:
    intro_db_path = Path(intro_sql_folder) / "example.sqlite"
    intro_db = sqlite3.connect(intro_db_path, autocommit=True)
    intro_db.execute("CREATE TABLE preference (id INTEGER PRIMARY KEY, session TEXT, value TEXT)")
    intro_db.execute("INSERT INTO preference (session,value) VALUES (?,?)", ("lucy", "09:00"))
    intro_row = intro_db.execute(
        "SELECT value FROM preference WHERE session=?", ("lucy",)
    ).fetchone()
    print("Matching row:", intro_row)
    assert intro_row == ("09:00",)
    assert (
        intro_db.execute(
            "SELECT value FROM preference WHERE session=?", ("another-session",)
        ).fetchone()
        is None
    )
    intro_db.close()
    intro_reopen = sqlite3.connect(intro_db_path, autocommit=True)
    assert intro_reopen.execute("SELECT count(*) FROM preference").fetchone()[0] == 1
    intro_reopen.close()
print("The row survived closing and reopening its connection.")

Index [0] selects the first column of a returned tuple. sqlite3.Row is an alternative row factory that also permits named-column access. dict(row) then produces an ordinary dictionary. The book's Database wrapper supplies that configuration and its schema; the wrapper is course code, while sqlite3 is the standard library. You will use the public connection and transaction methods explained at the exercise boundary, rather than needing to reconstruct the wrapper.

Now consider a budget. Moving five cents from reserved to spent requires two values to change together. A transaction makes a group of local changes commit together or roll back together. The example explicitly controls SQL transactions with autocommit=True and SQL statements. BEGIN IMMEDIATE starts a write transaction; COMMIT keeps its changes; ROLLBACK discards them. Predict the row after the deliberately raised exception. Catching an error alone would not undo the first update; the rollback is the operation that restores the prior state.

python
intro_ledger = sqlite3.connect(":memory:", autocommit=True)
intro_ledger.execute(
    "CREATE TABLE budget (id INTEGER PRIMARY KEY, reserved INTEGER, spent INTEGER)"
)
intro_ledger.execute("INSERT INTO budget VALUES (1,5,0)")
intro_ledger.execute("BEGIN IMMEDIATE")
try:
    intro_ledger.execute("UPDATE budget SET reserved=0 WHERE id=1")
    raise ValueError("injected failure before the matching spend update")
except ValueError:
    intro_ledger.execute("ROLLBACK")
assert intro_ledger.execute("SELECT reserved,spent FROM budget").fetchone() == (5, 0)
intro_ledger.execute("BEGIN IMMEDIATE")
intro_ledger.execute("UPDATE budget SET reserved=0,spent=5 WHERE id=1")
intro_ledger.execute("COMMIT")
print(
    "After a complete change:", intro_ledger.execute("SELECT reserved,spent FROM budget").fetchone()
)
intro_ledger.close()

The literal :memory: creates a temporary database inside this connection; it is useful for the small experiment, but the earlier file example establishes persistence. Neither example proves that a remote supplier rolls back when the local transaction rolls back. An external operation has its own state and evidence.

SQL you will meet later. UPDATE ... SET ... WHERE ... changes selected rows. AND combines conditions. ORDER BY makes an ordering explicit; absent that clause, do not rely on row order. count(*) counts rows; sum(amount) totals a column and can be NULL on an empty input; coalesce(sum(amount),0) uses zero for that empty aggregate. A UNIQUE constraint rejects duplicate identities. GROUP BY status computes one aggregate per status.

An invariant is a condition that must remain true across operations, such as nonnegative reserved money. A snapshot is a consistent view at one point; two separate reads can describe different moments unless their transaction contract binds them. In the book, with db.immediate() groups related writes. It is a course-defined context manager with the commit/rollback purpose you just observed. Do not assume that an arbitrary with connection has identical behavior under every SQLite autocommit setting.

Your prediction: two workers both read ten remaining cents outside a transaction and each approve seven. Why can both believe the next order fits? Explain what must be checked together with the write. Then change the example's initial reserved amount and repeat the failure. Reference: Python's SQLite tutorial and transaction control.

Approval binds an actor to one exact proposal and bounded money

“You may order stock” leaves too many questions unanswered. Which product, quantity, supplier, price and expiration did Lucy approve? This chapter treats approval as a decision over an exact proposal and a cumulative budget. A proposal is structured intended work. A digest names its serialized bytes. An approval records authority to proceed under stated conditions. A receipt is later evidence from the supplier; approval is not that receipt.

Make a change visible before any execution

Two dictionaries can express the same keys in different insertion orders. Canonical serialization uses a stable key order and separators before hashing. SHA-256 maps bytes to a fingerprint useful for detecting change. It is not encryption and does not identify the approving person by itself. Predict whether changing quantity changes the digest, and whether reordering keys does.

python
import hashlib
import json


def intro_digest(proposal):
    encoded = json.dumps(proposal, sort_keys=True, separators=(",", ":"), allow_nan=False)
    return hashlib.sha256(encoded.encode("utf-8")).hexdigest()


intro_proposal = {"sku": "MANGO", "quantity": 4, "unit_cents": 325}
intro_reordered = {"unit_cents": 325, "quantity": 4, "sku": "MANGO"}
intro_changed = {**intro_proposal, "quantity": 5}
assert intro_digest(intro_proposal) == intro_digest(intro_reordered)
assert intro_digest(intro_proposal) != intro_digest(intro_changed)
print("Reordered keys match; a changed quantity requires a new decision.")

The ** syntax copies dictionary entries into a new dictionary before the explicit replacement. allow_nan=False refuses nonstandard non-finite numbers in this representation. The actual order proposal uses a validated schema and integer cents; hashing arbitrary malformed input would not make it a meaningful proposal.

Reserved and spent money answer different questions

Reserved money is committed to admitted work whose final outcome may still be pending. Spent money is supported by confirmed accepted outcomes in the local ledger. Available authority is the ceiling minus both. If 700 cents is spent and 900 reserved under a 2000-cents ceiling, a 500-cents order is too large even though 500 is smaller than 2000.

python
intro_spent, intro_reserved, intro_ceiling = 700, 900, 2000
for intro_addition in (0, 400, 401, 500):
    intro_exposure = intro_spent + intro_reserved + intro_addition
    print(
        intro_addition, "total exposure", intro_exposure, "allowed", intro_exposure <= intro_ceiling
    )
assert intro_spent + intro_reserved + 400 == intro_ceiling

Four hundred exactly fits; 401 does not. A repeated approval of the same already-reserved proposal must add zero, not reserve twice. This is idempotency at the local approval boundary: repeating the same admitted decision preserves its accounting effect. It does not mean that two distinct orders with the same price are the same order.

Check and reserve in one transaction

If two workers separately read the same available budget and then both reserve it, the combined result can exceed the ceiling. Validation must be tied to the write within the transaction that owns the current totals. The actual exercise uses db.immediate() for that local critical section. The installed account ceiling and the supplied policy ceiling both constrain authority; the effective ceiling is the smaller one. A caller cannot enlarge installed authority by sending a more generous policy object.

Authority also depends on the actor, expiration, current work and proposal state. A revoked or expired decision must not become valid because its amount is small. An uncertain supplier outcome must retain its reservation rather than being reset to a fresh draft. Revocation withdraws future authority; it cannot recall an external request already admitted and sent.

Translate the contract into cases

Write a table for a fresh draft, repeated approval, changed digest, one-cents overflow, unknown actor, expiration and uncertain outcome. For each, predict both the returned decision and the reserved/spent row after the attempt. Refusal with changed money is still a defect.

Unit A implements the complete approval path. Unit B removes part of the cumulative calculation; the observable over-admission exposes the bug. In the changed-policy exercise, reduce the supplied ceiling below the installed ceiling and keep the exact-fit case. A blanket refusal cannot satisfy both the positive and negative cases. The final explanation must name which evidence grants authority and which future evidence would settle the supplier outcome.

Choose an explicit starting point for this independent notebook

This Unit B runs without Unit A. By default it prepares a supplied reference starting point and labels its provenance. It is not evidence that you built Unit A. To investigate your own successful implementation, set LEARNER_HANDOFF to its saved path before running the cell. An invalid selected file refuses; it is never silently replaced with the reference.

SourceTask supplies copied-source execution and handoff validation; RuntimeLab supplies the controlled failure experiment. Their public operations are introduced beside the main exercise. The artifact stores identity and observations; no variables from another kernel are required.

python
LEARNER_HANDOFF = None

Prepare and validate the supplied starting artifact

python
import json
import runpy
import shutil
import textwrap
from pathlib import Path

COURSE_INPUT = COURSE_WORK / "ch11-unit-a-handoff-v1.json"
if LEARNER_HANDOFF is not None:
    learner_input = Path(LEARNER_HANDOFF).expanduser().resolve()
    if not learner_input.is_file():
        raise FileNotFoundError("The selected learner handoff does not exist")
    if learner_input != COURSE_INPUT.resolve():
        shutil.copy2(learner_input, COURSE_INPUT)
    HANDOFF_ORIGIN = "LEARNER_SELECTED"
else:
    source_task_class = runpy.run_path(
        str(COURSE_ROOT / "book/always_on/exercises/source_tasks_v1.py")
    )["SourceTask"]
    reference_task = source_task_class(COURSE_ROOT, 8)
    try:
        reference_task.install(textwrap.dedent(reference_task.fragment))
        reference_observation = reference_task.visible("SUPPLIED_REFERENCE_START")
        if reference_observation["status"] != "PASS":
            raise RuntimeError("The supplied starting point did not pass its connection check")
        reference_task.save(COURSE_INPUT, reference_observation)
    finally:
        reference_task.close()
    HANDOFF_ORIGIN = "SUPPLIED_REFERENCE"
print("Starting evidence:", HANDOFF_ORIGIN)
print("The core task below validates the selected artifact before using it.")

Understand the supplied execution interface

The course runtime is provided so your implementation can be connected to real callers and storage. SourceTask(ROOT, chapter) makes a private copy. install(source) replaces only the declared function; visible() invokes the real chapter probe; save(path, result) retains a successful implementation and its evidence. load(path) checks the saved identities and hashes. inject_failure() changes the declared boundary; repair(fragment) replaces that broken fragment. close() removes the scratch copy after you retain evidence. These methods are supplied harness operations, not additional packages you must discover or install.

RuntimeLab provides the same copied-source failure experiment without the complete-function construction layer. Its run method records exit status, observations and the compared expectation. A subprocess log from an unfinished learner implementation is feedback about that implementation; it is not a successful connection. A syntax error in the notebook cell itself is a separate issue to fix. The task below names which interface it uses.

For direct-function units, the visible driver calls your callback without installing a source string. In either case, trace where your code is invoked. Supplied fixtures, database wrappers and replay models are labeled infrastructure; your own implementation and changed-case explanation are the evidence of learning.

Main practical: construct, connect and challenge

Comparing only the next amount ignores money already reserved or spent. This time you begin with your Unit A implementation and its saved evidence. Lucy receives simultaneous approvals whose promises exceed her total allowance.

Verify the handoff

The starting-point cell has selected the Unit A artifact explicitly. A selected learner handoff must validate; the default reference start is labeled separately. Run the setup and keep the runtime and implementation hashes in your submission.

python
import json
import os
import runpy
from pathlib import Path

ROOT = COURSE_ROOT

SourceTask = runpy.run_path(str(ROOT / "book/always_on/exercises/source_tasks_v1.py"))["SourceTask"]
REFERENCE_LESSON = 8
HANDOFF = Path("ch11-unit-a-handoff-v1.json")
handoff_status = "MISSING"
if HANDOFF.is_file():
    task = SourceTask(ROOT, REFERENCE_LESSON)
    try:
        handoff = task.load(HANDOFF)
        handoff_status = "VERIFIED"
        print("IMPLEMENTATION", handoff["implementation_sha256"])
    finally:
        task.close()
print("UNIT_A_HANDOFF", handoff_status)

Reproduce and diagnose

Predict the consequence of this injected boundary before executing it:

text
if addition > min(

The controlled mutation changes the same implementation you submitted. It refuses if the declared mutation boundary no longer occurs exactly once; inspect an alternative implementation with the instructor before adapting the experiment.

python
baseline = broken = None
if handoff_status == "VERIFIED":
    task = SourceTask(ROOT, REFERENCE_LESSON)
    try:
        task.load(HANDOFF)
        baseline = task.visible("YOUR_BASELINE")
        if baseline["status"] != "PASS":
            raise ValueError("Saved Unit A code no longer satisfies the visible contract")
        task.inject_failure()
        broken = task.run("INJECTED_FAILURE", expected=task.spec["expected_broken"])
        print("BEFORE", baseline["observation"])
        print("AFTER", broken["observation"])
    finally:
        task.close()
else:
    print("HANDOFF_REQUIRED: complete Unit A before performing Unit B")

State a diagnosis using those two observations. Name a test that would prove your diagnosis wrong. Propose real orders, approve them and inspect retained reservations. Reapproval must not reserve twice and refusal must leave the transaction unchanged.

Repair the boundary

Return the complete replacement for the injected fragment. Do not edit the oracle or print a desired observation. Repair the actual source. The starter keeps the defect so the learner outcome remains incomplete.

python
def repair_fragment():
    return "if addition > min("

Hint 1: the consequence

Lucy receives simultaneous approvals whose promises exceed her total allowance.

Hint 2: the evidence

Compare the two observations, then trace the changed field to approve in src/sovereign_agent/assistant_orders.py. Distinguish a schema refusal from a business-rule or authority refusal.

Hint 3: the design

Keep all authority and accounting checks in db.immediate; compute addition from prior state; compare spent plus reserved plus addition to the smaller ceiling.

python
def connect_repair(fragment):
    task = SourceTask(ROOT, REFERENCE_LESSON)
    try:
        task.load(HANDOFF)
        task.inject_failure()
        task.repair(fragment)
        return task.visible("YOUR_REPAIR")
    finally:
        task.close()


repair_result = None
if handoff_status == "VERIFIED":
    repair_result = connect_repair(repair_fragment())
    print("REPAIR", repair_result["status"], repair_result["observation"])
else:
    print("REPAIR_NOT_ATTEMPTED: missing Unit A evidence")

Transfer under a changed constraint

Test exact-limit acceptance, one-cents overflow, changed digest, stricter policy and repeated approval. Never call a real supplier.

Create a fresh task, load your handoff, inject the defect and apply your repair. Then change only the copied probe to exercise the new condition. Keep the actual observation and a prediction written beforehand. Explain why a visible-case lookup or a blanket refusal could pass the original example but fail this transfer.

The instructor's holdout applies your repair to a new copied runtime and checks both the positive case and the missing protection. An exact exception or changed state must cause a failure; no broad error is accepted as successful refusal.

Exit ticket

Submit the original handoff, baseline and broken observations, repair, transfer probe and results. State what Lucy would experience before and after the fix. Identify the guarantee that still requires separate evidence: An approval is bounded authority, not a supplier receipt.

python
passed = repair_result is not None and repair_result["status"] == "PASS"
exercise_report = {
    "unit": "ch11-b",
    "attempted": int(repair_result is not None),
    "completed": int(passed),
    "failed": int(repair_result is not None and not passed),
    "skipped": int(repair_result is None),
    "connection": "PASS" if passed else "NOT_READY",
    "handoff": handoff_status,
}
print("EXERCISE_REPORT=" + json.dumps(exercise_report, sort_keys=True))

Changed-constraint construction: Apply the stricter cumulative spending ceiling

Allow twenty minutes. Spend three minutes predicting, ten implementing and tracing, five on a new case of your own, and two explaining the surviving limitation. This is dedicated work, not an invitation to run a supplied answer. Both units revisit the same invariant after different core experiences; in Unit B, attempt this task from memory before consulting Unit A.

Implement transfer_check(spent, reserved, addition, installed, supplied). All values must be exact nonnegative integers; otherwise raise ValueError. Return whether spent + reserved + addition is at most min(installed, supplied). A repeated already-reserved approval passes addition=0. Do not interpret this arithmetic result as actor approval or supplier evidence.

Write your expected values before running the table. Keep one accepted case and one refusal. Your function is passed directly into the driver below. The driver copies inputs and checks they remain unchanged; it does not replace your implementation with the reference answer.

Hint 1: identify the authoritative inputs Name the source field for each output value. Which input changes while the rule remains the same?

Hint 2: choose the boundary cases Start with exact empty, exact equality and one value on each side of the boundary where valid. Do not add a special case for a visible product name or operation identity.

python
def transfer_check(spent, reserved, addition, installed, supplied):
    raise NotImplementedError("Compare cumulative exposure to both ceilings")
python
import copy
import json

TRANSFER_CASES = [
    ("exact fit", [700, 900, 400, 2000, 2500], True),
    ("one penny too many", [700, 900, 401, 2000, 2500], False),
    ("stricter supplied policy", [700, 900, 100, 2000, 1600], False),
    ("no double reservation", [700, 900, 0, 2000, 1600], True),
]


def same_transfer_value(actual, expected):
    if type(actual) is not type(expected):
        return False
    if isinstance(expected, dict):
        return actual.keys() == expected.keys() and all(
            same_transfer_value(actual[key], value) for key, value in expected.items()
        )
    if isinstance(expected, list):
        return len(actual) == len(expected) and all(
            same_transfer_value(a, e) for a, e in zip(actual, expected, strict=True)
        )
    return actual == expected


def run_transfer(candidate, cases):
    observations = []
    for label, arguments, expected in cases:
        supplied = copy.deepcopy(arguments)
        before = copy.deepcopy(supplied)
        raised = None
        try:
            actual = candidate(*supplied)
        except NotImplementedError:
            raised = "NotImplementedError"
            actual = {"unfinished": True}
        except Exception as error:
            raised = type(error).__name__
            actual = {"raises": raised}
        expects_error = isinstance(expected, dict) and set(expected) == {"raises"}
        correct = (
            raised == expected["raises"]
            if expects_error
            else (raised is None and same_transfer_value(actual, expected))
        )
        passed = correct and same_transfer_value(supplied, before)
        observations.append(
            {"case": label, "expected": expected, "observed": actual, "passed": passed}
        )
        print("PASS" if passed else "NEEDS_WORK", label, "expected", expected, "observed", actual)
    return observations


transfer_observations = run_transfer(transfer_check, TRANSFER_CASES)
TRANSFER_PASSED = all(row["passed"] for row in transfer_observations)
print("TRANSFER_STATUS", "PASS" if TRANSFER_PASSED else "NEEDS_WORK")

Design a counterexample and retrieve the mechanism

Add one new case with an independently calculated expected outcome to TRANSFER_CASES and rerun the driver. Change one condition at a time. Then deliberately replace your candidate with a constant answer in a temporary copy and show a case that rejects it. Restore your implementation. Explain why that counterexample is stronger than repeating the original example with a new name.

Without viewing the worked example, write the invariant in words and trace one observed value back to its input. Identify which part is a local fixture result and which claim would need a live provider, host or external-system observation. Keep a first attempt even if you used a hint.

Save your evidence and explain the result

Fill the prediction notes and your explanation before saving. Include the exact observed value, the input or retained row that caused it, your code's invocation point, one failed hypothesis, and the strongest claim the evidence still cannot support. A completed code cell alone does not earn explanation credit. Do not label reference-start behavior as your own Unit A construction.

Keep this edited notebook, the Markdown if used for notes, saved handoff files, and the JSON record below. Your work folder survives scratch cleanup and can be reopened in a new kernel. An instructor can ask for an unseen case after the visible checks; keep your implementation general.

python
explanation_notes = {
    "causal_trace": "Explain the input, learner invocation and observed result.",
    "failed_hypothesis": "Describe a prediction the evidence changed.",
    "remaining_limit": "Name the guarantee not established by this experiment.",
}
course_submission = {
    "unit": "ch11-b",
    "planned_minutes": 90,
    "starting_evidence": globals().get("HANDOFF_ORIGIN", "INDEPENDENT_UNIT_A"),
    "prediction": prediction_notes,
    "explanation": explanation_notes,
    "core_report": exercise_report,
    "transfer": transfer_observations,
    "explanation_review": "HUMAN_REVIEW_REQUIRED",
}
submission_path = COURSE_WORK / "ch11-b-submission-v1.json"
submission_path.write_text(
    json.dumps(course_submission, indent=2, sort_keys=True), encoding="utf-8"
)
print("Saved evidence:", submission_path)
print(
    "COURSE_REPORT="
    + json.dumps(
        {
            "unit": "ch11-b",
            "transfer_passed": TRANSFER_PASSED,
            "starting_evidence": course_submission["starting_evidence"],
            "edition": "student",
        },
        sort_keys=True,
    )
)

Extension: put a language model behind exact approval

Every propose and approve above was called by you, with a digest you copied by hand. In production the caller who proposes replenishment is a model, and the approval mechanism is the boundary that keeps money honest: an approval names one exact digest, reserves that amount once, and compares spent plus reserved plus the new amount against the smaller of the installed and supplied ceilings. This closing section puts a real model behind that boundary.

The mechanism is the Chapter 3 loop, agent_loop.run_loop: the model proposes a call, the dispatcher checks it against a Pydantic class, a before_write gate may refuse it, and the handler calls the same propose and approve from assistant_orders.py that you traced. Lucy has granted a standing automatic allowance of 1500 cents per order under a 2000 cents total, so the model may invoke approve with automatic=True and the record says AUTOMATIC. Beyond either number only Lucy's own /approve can act, and there is no tool for that. Nothing here sends an order to a supplier: an approval is bounded authority, not a receipt.

The runtime adds one file for this section, llm_lab.py. ScriptedModel replays turns written down in the notebook so the section runs identically for everyone; OpenAIModel sends the same messages to a hosted model when a key is present. The record always says which one ran.

Prediction: the next cell seeds Lucy's shop and prints four tool schemas. From the stock you know (vanilla 2 of 8 at 250 cents, strawberry 1 of 5 at 275 cents), write down both draft amounts and whether each fits the automatic allowance, then whether both fit the total together.

python
import hashlib
import json
import time
from pathlib import Path

from pydantic import Field

from reference_organizations.store.agent import (
    DraftArguments,
    NoArguments,
    StockArguments,
    seed_lucy,
    shop_dispatcher,
)
from sovereign_agent.agent_loop import Limits, run_loop
from sovereign_agent.assistant_orders import SpendingPolicy, approve, propose
from sovereign_agent.assistant_work import claim, enqueue
from sovereign_agent.database import Database
from sovereign_agent.llm_lab import ScriptedModel, describe, scripted_turn, tool_trace
from sovereign_agent.model_turn import ToolCall
from sovereign_agent.tool_dispatch import Dispatcher, ExecutableTool

APPROVAL_DB = COURSE_WORK / "ch11-llm-approval.sqlite"
TARGET = "lucy-local"
OPERATOR = "lucy"
SHOP_POLICY = SpendingPolicy(frozenset({OPERATOR}), total_cents=2000, automatic_order_cents=1500)


class ApprovalArguments(StockArguments):
    digest: str = Field(min_length=64, max_length=64, pattern=r"^[0-9a-f]{64}$")


def exact_digest(sku, quantity, unit_cost_cents, target=TARGET):
    """The same bytes `propose` hashes: the digest is a function of the exact proposal."""
    proposal = {
        "sku": sku,
        "quantity": quantity,
        "unit_cost_cents": unit_cost_cents,
        "supplier": "lucy-local",
        "currency": "USD",
    }
    encoded = json.dumps(proposal, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256((target + "\n" + encoded).encode()).hexdigest()


def open_shop(path):
    """A fresh seeded ledger with one claimed work item, so reruns observe the same rows."""
    for stale in (path, Path(f"{path}-wal"), Path(f"{path}-shm"), path.with_suffix(".authority")):
        stale.unlink(missing_ok=True)
    db = Database(path)
    seed_lucy(db)
    enqueue(db, "llm-lab:morning", "lucy", "Replenish low stock")
    work = claim(db, "notebook-worker", ttl=900)
    assert work is not None
    return db, work


def spending_row(db):
    row = db.connection.execute("SELECT * FROM assistant_spending WHERE id=1").fetchone()
    if row is None:
        return {"limit_cents": None, "reserved_cents": 0, "spent_cents": 0}
    return {k: row[k] for k in ("limit_cents", "reserved_cents", "spent_cents")}


def order_rows(db, work):
    rows = db.connection.execute(
        "SELECT json_extract(proposal,'$.sku') AS sku,amount,status,approval_basis,approved_by "
        "FROM assistant_orders WHERE work_id=? ORDER BY created,id",
        (work.id,),
    )
    return [dict(row) for row in rows]


def build_approval_tools(db, work, policy):
    """Two reading tools from the shop, two writing tools over the chapter's mechanism."""
    refusals = []
    reads = shop_dispatcher(db)

    def live_order(sku):
        return db.connection.execute(
            "SELECT id FROM assistant_orders WHERE work_id=? AND json_extract(proposal,'$.sku')=? "
            "AND status IN ('DRAFT','APPROVED') AND revoked=0",
            (work.id, sku),
        ).fetchone()

    def propose_order(args):
        checked = reads.invoke(
            ToolCall(id="validate-draft", name="draft_order", arguments=args.model_dump())
        )
        if not checked["ok"]:
            refusals.append(("propose_order", "proposal must match the current stock need"))
            raise ValueError("proposal must match the current stock need")
        try:
            identifier = propose(db, work, args.sku, args.quantity, target=TARGET)
        except (ValueError, PermissionError) as error:
            refusals.append(("propose_order", str(error)))
            raise
        row = db.connection.execute(
            "SELECT digest,amount,status FROM assistant_orders WHERE id=?", (identifier,)
        ).fetchone()
        return {"sku": args.sku, "quantity": args.quantity, **dict(row), "currency": "USD"}

    def approve_order(args):
        order = live_order(args.sku)
        try:
            approve(
                db,
                order["id"],
                args.digest,
                actor=OPERATOR,
                policy=policy,
                expires=time.time() + 3600,
                automatic=True,
            )
        except (ValueError, PermissionError) as error:
            refusals.append(("approve_order", str(error)))
            raise
        return {"sku": args.sku, "status": "APPROVED", **spending_row(db)}

    def spending(_):
        return {**spending_row(db), "orders": order_rows(db, work)}

    def before_write(call):
        # Approval is scoped to proposals this work item made; other work is not the model's.
        if call.name == "approve_order" and live_order(call.arguments.get("sku")) is None:
            refusals.append(("approve_order", "no live proposal for that product in this work"))
            raise PermissionError("no live proposal for that product in this work")

    tools = [
        reads.tools["list_stock"],
        reads.tools["supplier"],
        ExecutableTool(
            "propose_order",
            "Record an exact proposal for the quantity the shop needs. Never purchases.",
            DraftArguments,
            propose_order,
            consequential=True,
        ),
        ExecutableTool(
            "approve_order",
            "Approve one exact proposal by its digest under Lucy's automatic allowance.",
            ApprovalArguments,
            approve_order,
            consequential=True,
        ),
        ExecutableTool(
            "spending",
            "Read the spending ledger and every order of this work item.",
            NoArguments,
            spending,
        ),
    ]
    dispatcher = Dispatcher(
        tools, allowed=frozenset(tool.name for tool in tools), before_write=before_write
    )
    return dispatcher, refusals


approval_db, approval_work = open_shop(APPROVAL_DB)
approval_dispatcher, approval_refusals = build_approval_tools(
    approval_db, approval_work, SHOP_POLICY
)
for tool_schema in approval_dispatcher.schemas():
    function = tool_schema["function"]
    print(function["name"], "requires", function["parameters"].get("required", []))
    print("   ", function["description"])
print("Policy:", SHOP_POLICY)
VANILLA_DIGEST = exact_digest("SKU-VANILLA", 6, 250)
STRAWBERRY_DIGEST = exact_digest("SKU-STRAWBERRY", 4, 275)
print("Digest of the exact vanilla proposal:", VANILLA_DIGEST)
APPROVAL_MESSAGES = [
    {
        "role": "system",
        "content": (
            "You help Lucy run her ice cream shop. Read stock with the tools before proposing "
            "anything. Propose exactly the quantity the shop needs. You may approve a proposal "
            "under Lucy's automatic allowance by quoting the digest the proposal returned; if "
            "the shop refuses, leave that proposal as a draft for Lucy and say so. Finish with "
            "a short plain-words summary of what is approved, what is reserved and what waits."
        ),
    },
    {"role": "user", "content": "Vanilla and strawberry are low. Sort out the replenishment."},
]

The two reading tools are the shop's own list_stock and supplier from the embedded runtime; list_stock already counts approved orders as on_order, so an approval changes what the next read says is needed. propose_order reuses the shop's draft_order rule before it calls propose, so a quantity that is not the current need never reaches the ledger. The digest printed above was computed in the notebook from the proposal bytes alone, which is the whole point of exact approval: anyone holding the proposal can name it.

Run the loop against a recorded transcript

The turns below are what a model replied, written down. Turn one reads stock. Turn two proposes six vanilla and four strawberry. Turn three approves both by digest: the vanilla approval reserves 1500 cents, and the strawberry one asks the ledger to hold 1100 cents more. Turn four reads the spending ledger, and turn five answers.

Prediction: 1100 cents is under the 1500 cents automatic allowance. Is the strawberry approval accepted? Recall the order of checks in approve: operator, eligible proposal, uncertain retry, automatic allowance, then spent plus reserved plus addition against the ceiling. Write down what assistant_spending holds afterwards and which status each order has.

python
RECORDED_TURNS = [
    scripted_turn(calls=[{"name": "list_stock", "arguments": {}}]),
    scripted_turn(
        calls=[
            {"name": "propose_order", "arguments": {"sku": "SKU-VANILLA", "quantity": 6}},
            {"name": "propose_order", "arguments": {"sku": "SKU-STRAWBERRY", "quantity": 4}},
        ]
    ),
    scripted_turn(
        calls=[
            {
                "name": "approve_order",
                "arguments": {"sku": "SKU-VANILLA", "digest": VANILLA_DIGEST},
            },
            {
                "name": "approve_order",
                "arguments": {"sku": "SKU-STRAWBERRY", "digest": STRAWBERRY_DIGEST},
            },
        ]
    ),
    scripted_turn(calls=[{"name": "spending", "arguments": {}}]),
    scripted_turn(
        "Approved 6 vanilla at 250 cents, 1500 cents now reserved. The 4 strawberry proposal at "
        "1100 cents is "
        "recorded as a draft: together they would exceed the 2000 cents ceiling, so it waits for "
        "Lucy's own approval. Nothing has been sent to a supplier."
    ),
]
LOOP_LIMITS = Limits(model_calls=8, tool_calls=16, seconds=120, output_tokens=4096)

scripted_result = run_loop(
    ScriptedModel(RECORDED_TURNS), approval_dispatcher, APPROVAL_MESSAGES, limits=LOOP_LIMITS
)
print(describe(scripted_result, "scripted"))
scripted_trace = tool_trace(scripted_result)
scripted_spending = spending_row(approval_db)
scripted_orders = order_rows(approval_db, approval_work)
print("Refusals:", approval_refusals)
print("Spending:", scripted_spending)
for order in scripted_orders:
    print(order)
assert scripted_result.status == "COMPLETED"
assert [step["ok"] for step in scripted_trace] == [True, True, True, True, False, True]
assert scripted_trace[4]["result"] == "tool_failed"
assert approval_refusals == [("approve_order", "cumulative spending ceiling reached")]
# The digest the ledger minted is the one the notebook computed before the run.
assert scripted_trace[1]["result"]["digest"] == VANILLA_DIGEST
assert scripted_trace[1]["result"]["amount"] == 6 * 250
assert scripted_spending == {"limit_cents": 2000, "reserved_cents": 1500, "spent_cents": 0}
assert [(o["sku"], o["status"], o["approval_basis"]) for o in scripted_orders] == [
    ("SKU-VANILLA", "APPROVED", "AUTOMATIC"),
    ("SKU-STRAWBERRY", "DRAFT", "UNKNOWN"),
]
# Every observation in the transcript carries the identifier of the request it answers.
assert all(m.get("tool_call_id") for m in scripted_result.messages if m["role"] == "tool")

The strawberry approval fits the per-order allowance and still fails, because the ceiling is cumulative: 1500 already reserved plus 1100 is 2600, above 2000. The refusal reached the model as a plain tool_failed; the reason lives only in the notebook's ledger, since invoke never copies an error message into an observation. The transaction left the ledger unchanged: reserved stayed at 1500 and the strawberry row stayed a draft, which is what Lucy will see.

Run the same loop against a live model

Nothing changes except the model. build_model looks for an OPENAI_API_KEY in Colab's Secrets pane (the key icon in the left sidebar: add a secret with that name and switch on notebook access for it), then in the process environment for a local kernel. Without a key it returns the scripted model again and the record says so. A missing key is a normal condition, never a silent substitution.

The live model is gpt-5.1 through the openai library, which Colab ships preinstalled; on a local kernel run %pip install openai once. One run costs a few thousand tokens. The model receives the same five schemas and system message, on a fresh ledger. It may approve strawberry first and leave vanilla waiting, or approve nothing; what it cannot do is reserve more than the ceiling, reserve one proposal twice, or approve another work item's order, because those rules live in approve and in the gate.

Prediction: write which order a careful assistant should approve when only one fits, then run the cell and compare the model's judgment, or the recorded transcript when scripted.

python
from sovereign_agent.llm_lab import DEFAULT_MODEL, build_model, resolve_api_key

approval_db.close()
live_db, live_work = open_shop(APPROVAL_DB)
live_dispatcher, live_refusals = build_approval_tools(live_db, live_work, SHOP_POLICY)
chosen_model = build_model(RECORDED_TURNS, model=DEFAULT_MODEL)
print("Model source:", chosen_model.source, "| key found:", resolve_api_key() is not None)
live_result = run_loop(chosen_model, live_dispatcher, APPROVAL_MESSAGES, limits=LOOP_LIMITS)
print(describe(live_result, chosen_model.source))
print("Refusals:", live_refusals)
live_trace = tool_trace(live_result)
live_spending = spending_row(live_db)
live_orders = order_rows(live_db, live_work)
print("Spending:", live_spending)
for order in live_orders:
    print(order)
assert live_result.status in {"COMPLETED", "MODEL_CALL_LIMIT", "TOOL_LIMIT", "MODEL_FAILED"}
# Whatever the model did, the ledger obeys the chapter's arithmetic and authority rules.
approved = [order for order in live_orders if order["status"] == "APPROVED"]
assert live_spending["reserved_cents"] == sum(order["amount"] for order in approved)
assert live_spending["reserved_cents"] + live_spending["spent_cents"] <= SHOP_POLICY.total_cents
assert live_spending["spent_cents"] == 0  # nothing was ever sent to a supplier
for order in approved:
    assert order["amount"] <= SHOP_POLICY.automatic_order_cents
    assert (order["approval_basis"], order["approved_by"]) == ("AUTOMATIC", OPERATOR)
assert all(order["status"] in {"DRAFT", "APPROVED", "REVOKED"} for order in live_orders)
UNIT_COST = {"SKU-VANILLA": 250, "SKU-CHOCOLATE": 300, "SKU-STRAWBERRY": 275}
for step in live_trace:
    if step["tool"] == "propose_order" and step["ok"]:
        sku, quantity = step["arguments"]["sku"], step["arguments"]["quantity"]
        assert step["result"]["amount"] == quantity * UNIT_COST[sku], step

Challenge: the exact limit, one cents over, and a stricter supplied policy

The ceiling is data. Rerun the recorded transcript on a fresh ledger with total_cents=2600, which is exactly the two drafts together, and predict before running whether the second approval is accepted: the rule is "at most", not "below". Then rerun with total_cents=2599 and predict which refusal returns.

The next cell also calls approve twice more, by hand, on the 2600 cents ledger. Repeating the vanilla approval under the same policy must not reserve a second 1500 cents: addition is zero for an order that is already APPROVED. Repeating it under a stricter supplied policy of 2000 cents is refused even though nothing new is being reserved, because approve compares against the smaller of the installed and supplied ceilings. With a live key, rerun the live cell under the 2599 cents policy and explain what the model did after its second approval was refused.

In your notes, separate what the scripted run proves (the cumulative rule, the exact-limit boundary, no double reservation, the installed-versus-supplied minimum, transcript accounting) from what only a live run can show (the model's judgment). The saved record keeps the two apart.

python
def rerun_with(total_cents, name):
    db, work = open_shop(COURSE_WORK / f"ch11-llm-approval-{name}.sqlite")
    policy = SpendingPolicy(
        frozenset({OPERATOR}), total_cents=total_cents, automatic_order_cents=1500
    )
    dispatcher, refusals = build_approval_tools(db, work, policy)
    result = run_loop(
        ScriptedModel(RECORDED_TURNS), dispatcher, APPROVAL_MESSAGES, limits=LOOP_LIMITS
    )
    print(f"total_cents={total_cents}:", result.status, spending_row(db), "refusals:", refusals)
    return db, work, policy, refusals


exact_db, exact_work, exact_policy, exact_refusals = rerun_with(2600, "exact")
assert exact_refusals == [] and spending_row(exact_db)["reserved_cents"] == 2600
over_db, _, _, over_refusals = rerun_with(2599, "over")
assert over_refusals == [("approve_order", "cumulative spending ceiling reached")]
assert spending_row(over_db)["reserved_cents"] == 1500

vanilla_id = exact_db.connection.execute(
    "SELECT id FROM assistant_orders WHERE json_extract(proposal,'$.sku')='SKU-VANILLA'"
).fetchone()[0]
renewal = time.time() + 60
approve(exact_db, vanilla_id, VANILLA_DIGEST, actor=OPERATOR, policy=exact_policy, expires=renewal)
assert spending_row(exact_db)["reserved_cents"] == 2600  # reapproval reserved nothing new
stricter = SpendingPolicy(frozenset({OPERATOR}), total_cents=2000)
try:
    approve(exact_db, vanilla_id, VANILLA_DIGEST, actor=OPERATOR, policy=stricter, expires=renewal)
except PermissionError as error:
    stricter_refusal = str(error)
else:
    stricter_refusal = None
print("Stricter supplied policy:", stricter_refusal)
assert stricter_refusal == "cumulative spending ceiling reached"
assert spending_row(exact_db)["reserved_cents"] == 2600

llm_lab_report = {
    "unit": "ch11-b",
    "scripted": {
        "status": scripted_result.status,
        "tool_calls": scripted_result.tool_calls,
        "refused": [reason for _, reason in approval_refusals],
        "spending": scripted_spending,
        "orders": [(o["sku"], o["status"]) for o in scripted_orders],
    },
    "session": {
        "source": chosen_model.source,
        "model": DEFAULT_MODEL if chosen_model.source == "live" else None,
        "status": live_result.status,
        "model_calls": live_result.model_calls,
        "tool_calls": live_result.tool_calls,
        "refused": [reason for _, reason in live_refusals],
        "spending": live_spending,
        "orders": [(o["sku"], o["status"], o["approval_basis"]) for o in live_orders],
        "answer": live_result.answer[:400],
    },
    "ceilings": {
        "2600": {"refused": [r for _, r in exact_refusals], "reserved": 2600},
        "2599": {"refused": [r for _, r in over_refusals], "reserved": 1500},
        "reapproval_under_2000": stricter_refusal,
    },
}
llm_report_path = COURSE_WORK / "ch11-b-llm-lab-report-v1.json"
llm_report_path.write_text(json.dumps(llm_lab_report, indent=2, sort_keys=True), encoding="utf-8")
print("Saved evidence:", llm_report_path)
print("LLM_LAB_REPORT=" + json.dumps(llm_lab_report["session"], sort_keys=True))
for open_db in (live_db, exact_db, over_db):
    open_db.close()

Keep building with Prof Rod

Found this material through a colleague, classroom or shared download? Get the complete book at profrod.ai/book and join the Prof Rod learner community. Bring one result, one question or one failure you learned from. Share this resource with another learner and keep its source links with it so they can find the full course and future updates.