Agentic AI Masterclass Curated by Arunabh Bhattacharjee
Part IX - Appendices · Appendix B
B

Appendix B — Reference Implementation Tour

A guided read of the shipped code — twelve modules, offline tests, a standard-library runtime, and the limits each test actually establishes.

28 min read · Part IX - Appendices · Updated 9 November 2026

code/reagent/ is an executable teaching harness: twelve modules with a standard-library runtime, offline regression tests, and a traced demo that needs no API key or network access. The development installation adds pytest. The package demonstrates local control-flow contracts; it does not include production provider clients, operating-system isolation or complete crash recovery.

bash
cd code
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest -q          # run the current regression suite
python demo.py     # a full traced run against a scripted model

The excerpts omit surrounding code where marked; run the source files for complete examples.

B.1Why it has no dependencies#

Standard-library-only runtime dependencies keep external services behind replaceable interfaces.

The consequence people notice first is that the “model” in every test is a ScriptedModel replaying a fixed list of turns. That is not a limitation working around the absence of an API key — it is the design. It makes retries, guardrails, approvals, compaction, recorded-completion deduplication and exercised terminal outcomes assertable without a network call, an API key, or a flaky score. The suite runs quickly without live model calls; elapsed time depends on the environment.

python
class ScriptedModel:
    """A deterministic model that replays a fixed list of turns.

    This is the single most useful test double in an agent codebase. It makes
    the loop's control flow — retries, guardrails, approvals, compaction —
    assertable without a network call, an API key, or a flaky score.
    """

B.2Read it in this order#

This is a reading order, not a strict import ordering.

Module Chapters What it owns
types.py 3, 4 The values crossing every boundary; RunOutcome
model.py 2, 8 The ModelClient protocol, cost, test doubles
tools.py 13, 14 Tool definition, schema generation, registry, selection
policy.py 15, 19 The permission engine — allow / ask / deny
guardrails.py 20, 21 Input, action and output guardrails; budgets
state.py 5, 23 The append-only log state is folded from
trace.py 23 Spans shaped for OTel GenAI conventions
retry.py 22 Backoff, circuit breaker, compensating actions
memory.py 8, 9, 10 Context assembly, compaction, four memory kinds
skills.py 26 Procedural memory behind a verification gate
evals.py 24, 25 Outcome, trajectory, efficiency, rubric scoring
loop.py 4, 6, 7 The loop that calls all of the above — and nothing else

policy.py imports from tools.py because a permission decision needs a tool’s risk metadata. tools.py imports nothing from policy.py, because a tool must not be able to describe itself as permitted. That asymmetry is Chapter 3’s first boundary, expressed as an import graph.

loop.py — orchestration depends on everything; nothing depends on it SUBSYSTEMS memory.py skills.py evals.py guardrails.py policy.py retry.py MECHANISMS state.py tools.py trace.py FOUNDATION model.py types.py imports nothing from the package policy.py imports tools.py — a decision needs a tool’s risk metadata tools.py does not import policy.py; permission is decided outside the tool.
Figure B.1 - The module dependency graph. The asymmetry at the bottom is Chapter 3's first boundary expressed as an import rule.

B.3The four ideas the code exists to demonstrate#

The loop makes control decisions visible. loop.py concentrates admission, execution, observation and stopping in one inspectable module. The harness can enforce these mechanics; task correctness still requires evidence from tools, graders or accountable review.

Operated exits need names and tests. RunOutcome defines eight terminal states, but the enum alone does not establish a driver path for every value. For example, a declined approval normally becomes an observation so the run can continue. Inspect the exercised paths in tests/test_loop.py and add an explicit test for every terminal state your production adapter promises.

Recorded state is reconstructed by replay. RunState.replay folds an append-only local JSONL log, and open_run opens both fresh and existing runs. This centralises reconstruction, but the reference still has live/replay mismatches and an ambiguous remote-effect window, detailed below. An append-only API also does not make a local file tamper-proof.

The model proposes; the policy evaluates the declared call. Resource checks run before contextual rules, and a test asserts that a permissive rule cannot override a path denial. This guarantee covers the policy layer and inspected arguments. The package does not confine arbitrary Python code or prevent a tool from using undeclared resources; production enforcement belongs in scoped adapters and an external isolation boundary.

B.4The four bugs it found in itself#

Four historical defects illustrate why output inspection belongs beside automated tests. The September 2026 review found further issues, including cached-token double charging, double-counted trace cost, truncated responses reported as completion, unverified skill promotion, and repeated evaluations assigned an independent-binomial interval. Regression tests now cover those corrections. The historical examples below remain useful because they show how plausible plumbing can misrepresent its own behaviour.

1 · The schema that typed everything as a string#

tools.py derives JSON Schema from a function signature. The first version used inspect.signature and read param.annotation directly. Under from __future__ import annotations — which the test fixtures use, as most modern Python does — every annotation arrives as a string, so float became "float", matched nothing in the type table, and fell through to the "string" default.

Every tool still worked, because the fixture inputs happened to be usable; Python annotations do not perform runtime coercion. The model was simply being told that a monetary amount was text.

python
# Resolve annotations properly: under `from __future__ import annotations`
# every annotation arrives as a *string*, and a schema built from strings
# silently types every parameter as "string". This is the bug that makes
# a model pass "12.50" where a number was meant.
try:
    hints = typing.get_type_hints(fn)
except Exception:
    hints = {}

Test the generated schema, not just the function. Schema-generation errors can silently misdescribe every call.

2 and 3 · The trace that lied about causality#

The first tracer kept a stack of open spans in a list. Under parallel tool execution (Chapter 13), two worker threads pushed and popped that shared list, and the rendered tree showed tool.fetch_url nested inside tool.read_file — two independent operations displayed as a causal chain.

Moving to contextvars fixed that and immediately exposed the second bug: worker threads do not inherit context variables, so the parallel spans now had no parent at all and rendered as roots outside the run.

python
# A worker thread does not inherit the caller's contextvars, so the active
# span has to be carried across explicitly. Without this the trace shows
# parallel tools as unparented roots — the tree lies about what caused what.
for c, t in reads:
    snapshot = contextvars.copy_context()
    pending.append((c, t, pool.submit(snapshot.run, self._invoke_one, c, t)))

Both bugs were found by rendering the tree and reading it. Neither would have been caught by asserting that spans existed. The regression test now asserts the shape:

python
def test_parallel_tool_spans_are_parented_correctly(toolset, policy, tmp_path):
    """A trace whose parallel branches float free as roots is a trace that
    cannot answer 'what caused this'."""
    roots = [s for s in loop.tracer.spans if s.parent_id is None]
    assert len(roots) == 1 and roots[0].kind == "run"

4 · The test double that hid the phenomenon#

ScriptedModel estimated prompt tokens from message.content. Tool results live in message.tool_result.content, so tool observations contributed zero tokens, and the demo trace showed a context that never grew: in=342 on every single turn.

After correcting token accounting, the trace shows input growth (counts and timings vary by demo):

text
· agent.run                       10.1ms
  · chat                             0.0ms  in=342 out=36
  · tools.parallel                   4.1ms
    · tool.read_file                   0.2ms  allow
    · tool.fetch_url                   0.0ms  allow
  · chat                             0.0ms  in=435 out=6
  · chat                             0.0ms  in=482 out=10
  · chat                             0.0ms  in=515 out=10

A test double that misrepresents the behaviour under study can conceal defects.

DEFECT SYMPTOM HOW IT WAS FOUND 1 · schema typed everything as a string param.annotation returns "float", not float, under postponed annotation evaluation NONE. Every tool still worked; Fixture inputs worked. The model was told an amount was text. A test that asserted the GENERATED SCHEMA rather than the function. 2 · trace misreported causality a shared list of open spans, mutated by two worker threads at once fetch_url appeared NESTED inside read_file — two parallel operations shown as a chain. Rendering the tree and READING it. Existence tests did not check its causal structure. 3 · spans became orphans worker threads do not inherit contextvars, so the parent id was None Parallel spans rendered as ROOTS outside the run — a trace answering nothing. Exposed BY fixing bug 2. Regression test now asserts exactly one root. 4 · the test double hid the phenomenon token estimate read message.content, but tool results live in message.tool_result The demo showed in=342 on EVERY turn — a context that never grew. Reading the demo output and noticing a number that should have been rising. Why this appendix exists Four defects in a few hundred lines. Three invisible to any test that only asserted things existed. And number 4 is the worst kind: a test double that is wrong about the thing you are studying will make your system look correct — it hid the central phenomenon of three chapters.
Figure B.2 - Four historical defects in the teaching harness. These historical defects motivate semantic checks rather than existence-only assertions.

B.5Trace one proposed action through every boundary#

The fastest way to understand the package is to stop reading it module by module and follow one proposal all the way through. Take the fourth model turn in demo.py: Reference Agent proposes issue_credit(account="acme-9", amount=50.0). At that moment nothing has been credited. The proposal is a ToolCall, not a command, and AgentLoop._execute_calls() still has to turn it into an authorised, observed effect.

The path crosses eleven distinct checks or records. They are compact enough to miss when you scan the function, so read them as a protocol:

Stage Reference location What happens to the $50 proposal
1. Resolve ToolRegistry.get The name must identify a registered tool. An unknown name becomes an error observation; it never reaches Python dispatch.
2. Inspect the arguments action GuardrailPipeline The loop detector sees the canonical call signature and the secret scanner sees the arguments. A HALT ends the run; a BLOCK becomes an observation.
3. Check resources PermissionEngine.evaluate Path and network capabilities run before contextual rules. This call has neither a path nor a URL, so it advances.
4. Check the session and rules PermissionEngine.evaluate No prior session grant exists. The money rule matches issue_credit and returns ASK.
5. Tighten from tool metadata AgentLoop._execute_calls The tool is marked mutates=True, reversible=False. Even an ALLOW rule would be tightened to ASK below autonomy level 3 unless it came from the exact session grant.
6. Ask the injected approver The demo approver accepts $50 and rejects amounts over $100. The request and decision are separate events.
7. Admit the batch approved list Only after every proposed call has been vetted does execution begin. The model cannot smuggle an unchecked third call behind two harmless reads.
8. Deduplicate AgentLoop._invoke_one For a declared mutation, a stable key is computed from run id plus canonical call signature. A recorded completion returns its stored result without invoking the tool; ordinary reads execute again.
9. Execute under retry and trace Retrier.call inside tool.issue_credit span The tool runs with retry classification, a circuit breaker if one was supplied, and trace attributes naming the call.
10. Record completion TOOL_CALL_COMPLETED The result, signature and idempotency key enter the append-only log. Only successful mutations carry a completion key.
11. Vet and observe the result input guardrail, then _observe Successful returned content is checked, labelled trust="tool", and appended to the transcript and trajectory. Invocation errors are observed directly.

This order is the architecture. Moving any one step changes the safety property. Execute before policy and the policy becomes an audit comment. Ask before the absolute path and network checks and a human can accidentally widen a capability that is meant to be non-negotiable. Run calls while still vetting the batch and a later HALT arrives after earlier effects have already happened. Record completion before the tool returns and a crash can make a non-effect look committed. Put the returned text into context before scanning it and an indirect prompt injection has already crossed the trust boundary.

The implementation makes the ordering visible:

python
approved: list[tuple[ToolCall, Tool]] = []

for call in calls:
    tool = self.tools.get(call.name)
    ...
    if v := action_gr.worst(call):
        ...
    verdict = self.policy.evaluate(call, tool, self.policy_ctx)
    ...
    approved.append((call, tool))

for call, tool, result in self._invoke_all(approved):
    ...
    self._observe(result)

The two loops are a security boundary. The first loop performs total, sequential admission; the second performs effects. The reference does not cancel the whole batch merely because one call is denied. It gives the denied call back to the model as an error observation and later executes the calls that survived. What it guarantees is that no survivor starts before the whole proposal has been examined. If your product requires atomic batch semantics — all calls happen or none do — this loop does not provide them. You need a transaction owned by the receiving system or a saga with compensations (Chapter 22); running three Python functions inside one try block is not atomicity.

Execution then splits by declared effect, not by tool name. _invoke_all() partitions the approved calls into reads where mutates is false and writes where it is true. Multiple reads run in a thread pool under tools.parallel; writes run serially after the reads. The declaration is deliberately conservative. A tool that reads a queue by consuming a message is a mutation whatever its name says. A tool that submits a nominally read-only query to a metered service may be externally costly without mutating state. Production Reference Agent needs risk metadata that describes those consequences accurately; the reference shows where the decision lives, not how your organisation discovers the metadata.

MODEL OUTPUT — EVERY ITEM IS A PROPOSAL read_file(incident.md) fetch_url(runbook) issue_credit($50) proposed batch TOTAL ADMISSION — BEFORE ANY EFFECT 1 · registry known tool? 2 · action guardrail safe arguments? 3 · resource policy path / egress? 4 · contextual policy allow / ask / deny 5 · approval — only if ASK exact call granted? ASK unknown BLOCK / HALT DENY DENY declined RED BRANCHES: NO TOOL EFFECT — error observation, except HALT stops the run ALLOW YES EFFECT BOUNDARY — ADMITTED SUBSET ONLY 6 · idempotency mutation completed? 7 · invoke retry + trace 8 · completion event write success: key 9 · result guardrail successful content only MISS HIT: stored result; no new effect or completion event Observe; count new executions BLOCK uses a safe error replacement HALT → stop before observation completion remains in the durable log CONTINUE / BLOCK HALT errors bypass scan
Figure B.3 - Every call crosses registry, action guardrail, resource policy and contextual policy; only ASK proceeds to approval. Rejections produce no tool effect. Only a successful mutation completion carries an idempotency key and becomes a deduplication fact, while a result-guardrail HALT can stop the run after completion is logged but before _observe adds it to the live transcript.

There are also two different kinds of feedback. A denied path, an unknown tool and a declined approval are observations that did not execute. _observe(..., executed=False) puts the explanation into the transcript but keeps the trajectory honest. A fresh successful or failed invocation is an executed observation and adds the tool name to the live trajectory; replaying a cached mutation result must not count as another execution. That distinction matters to evaluation: read_file appearing in the model’s proposal does not prove the file was read, and a policy denial must not satisfy a must_call=("read_file",) requirement in evals.py.

Successful returned content crosses the input pipeline after execution because it is entering the next model context. The name inp can mislead you into assuming it scanned the original user goal; in this loop it is applied to successful tool-returned Message objects labelled with tool trust. Invocation errors bypass that scan and enter as observations. The local errors are mostly harness-authored strings, but a production adapter may embed a remote response body inside an error. Normalise or scan that body before returning it; an HTTP status does not make its payload trusted. The final answer crosses the separate output pipeline. A production ingress adapter must still validate and normalise the initial user or webhook payload before calling run(). The reference’s guardrail split demonstrates where trust changes; it is not a complete perimeter for every ingress channel Reference Agent may serve.

The retry boundary is equally exact. Tool.invoke() converts expected ToolError and argument TypeError failures into results, so the model can repair or route around them. Retrier responds only to a raised RetryableError, and its default policy retries only TRANSIENT and RATE_LIMITED classes. That means adapter classification decides whether Reference Agent retries at all. Turning every provider exception into ToolError disables transport retry. Turning an HTTP 400 into RetryableError with its default TRANSIENT class spends up to four attempts on an invalid request; classifying it as INVALID records one attempt and returns the failure without retrying. Preserve the distinction when a local callable becomes a remote adapter, and record the class alongside the attempt count.

Finally, the approver is a synchronous callable. That keeps the control flow legible and the tests deterministic. It does not model an approval that sits for four hours, survives a deployment, expires, is delegated, or is answered from another device. A production approval service must persist the request, bind the decision to actor, tenant, tool, canonical arguments and policy version, and resume the same run after the decision. Keep the ASK verdict and the two audit events. Replace the in-process callback. That is the recurring rule of this implementation tour: preserve the seam and strengthen what sits behind it.

B.6Two tests that turned out to be wrong about the code#

test_budget_exhaustion_is_a_named_outcome originally used ten identical tool calls with a turn ceiling of three, and expected BUDGET_EXHAUSTED. It got LOOP_DETECTED — because the loop detector fires at the third identical call, before the turn ceiling is reached.

The code was right. A stuck agent should be reported as stuck, not as out-of-budget: both stop it, and only one names the actual problem. So the test was rewritten with distinct arguments, and a second test was added to pin the ordering deliberately:

python
def test_loop_detection_beats_the_turn_ceiling(toolset, policy, tmp_path):
    """Ordering matters: a stuck agent should be reported as stuck, not as
    out-of-budget. Both stop it, but only one names the actual problem."""

The second was test_cost_ceiling_stops_the_run, which expected a cost ceiling of effectively zero to stop a single-turn run. It did not, because ceilings are checked at turn boundaries, so the first turn always completes. Again the code was right and the expectation was wrong, and the corrected test states the semantics:

python
assert result.turns == 1, "the ceiling is enforced at the next turn boundary"

max_cost_usd=2 does not guarantee a bill under two dollars when the loop checks consumption only between turns.

B.7Read every green test as a bounded claim#

The suite is useful because it is fast, deterministic and unusually direct. It is dangerous if you turn “run the current regression suite” into “production ready.” A test proves only the boundary it actually crosses. The discipline in reading this repository is to state each test as a sentence with an explicit does not prove clause.

Test What the assertion proves What it does not prove
test_a_denied_path_never_reaches_the_tool A path outside the configured roots leaves the fixture counter at zero and produces a policy observation. Filesystem isolation against races, changed mounts, operating-system permissions or another process.
test_an_approved_irreversible_action_proceeds_and_is_remembered Two identical calls in one live run invoke the fixture once and retain a session grant. That two semantically equivalent calls with different argument representations deduplicate, or that a grant is safe across sessions.
test_a_resumed_run_does_not_repeat_a_side_effect A completion already present in a run log is folded into completed_calls, so a later run with the same id returns the stored result. A crash after the external effect but before TOOL_CALL_COMPLETED, or two workers resuming concurrently.
test_parallel_tool_spans_are_parented_correctly Two fixture reads have the tools.parallel span as parent and the trace has one root. Context propagation through an async runtime, a process pool, a queue or an external worker.
test_output_guardrail_withholds_a_leaking_answer One synthetic AWS-key-shaped string produces GUARDRAIL_STOP and is absent from the returned answer. Complete secret detection, encoded secrets, streaming leakage before a final buffer, or content-policy coverage.
test_every_run_ends_with_a_named_outcome The exercised happy path returns a RunOutcome value. That every exception, cancellation, worker loss and future exit added to the loop maps to the intended outcome.

That third row deserves special attention because it is the easiest one to over-read. The test’s comment says “The process dies,” but the first run() call actually returns normally with a completed tool event and a finished run event. The second loop then resumes the same run id. This is a valid test for replaying a recorded completion. It is not a fault-injection test for dying inside _invoke_one().

Imagine Reference Agent issuing the $50 credit from the demo against a real billing service. The failure sequence is precise:

  1. The harness computes key K and appends TOOL_CALL_STARTED.
  2. The billing service accepts the credit and commits it.
  3. The response reaches the adapter, but the worker is killed before TOOL_CALL_COMPLETED is appended.
  4. A replacement worker folds the log. It sees no successful completion for K, so completed_calls has no entry.
  5. The model proposes the same credit, or recovery replays it, and the local check does not suppress the invocation.
  6. Unless the billing receiver deduplicates K, the customer receives a second credit.

The green test and the duplicate effect can coexist because they concern different crash points. Chapter 5 develops the production repair: transmit the same key to a receiver that durably deduplicates it, reconcile an ambiguous started-without-completed action through an authoritative read, and enforce single-writer ownership of a run. The appendix adds the code-reading lesson: turn a test name into a timeline before you use it as evidence. If the timeline contains no injected stop between effect and record, it says nothing about that interval.

Apply the same method to guardrails. The default secret scanner has four regular-expression families. Its tests prove those patterns and their placement in action and output pipelines. They do not make pattern matching a general exfiltration defence; the docstring says the structural controls are the real boundary. Apply it to memory: keyword overlap, confidence and recency are tested mechanics, not proof that the recalled fact is correct. Apply it to evaluation: an unparseable judge response becomes a failure, but a parseable, confidently wrong judgement remains possible.

This is why three defects in the earlier section were found by looking at rendered output. Existence assertions are necessary and insufficient. A span can exist with the wrong parent. A token counter can return a number that never grows. A schema can be valid JSON and still tell the model that every number is text. Add semantic assertions at the boundary where the generated artefact will be consumed:

  • inspect the exact schema the model receives, not only the decorated Python function;
  • assert trace parent-child relationships, not only span counts;
  • assert monotonic context growth when observations accumulate, not only that usage is positive;
  • inject a stop at each durable boundary, not only before and after the whole run;
  • run a concurrent-resume test against the production event store, not only two sequential Python objects;
  • confirm every security test can fail after deliberately removing the protection.

The last item is a test of the test. If changing the path policy to allow /etc leaves test_a_denied_path_never_reaches_the_tool green, the fixture is not exercising the capability you think it is. A red control is especially important for tenant isolation and policy tests, where a bad fixture can give you perfect confidence in a path that never reached the sensitive resource.

A passing reference suite is evidence about the teaching implementation

The offline tests pin the loop’s local contracts. They do not certify a provider adapter, an external API, a distributed event store, a sandbox or your tenant model, because none of those components is present. Carry the contract tests to each production replacement and add failure injection at the new boundary. Never inherit the confidence without inheriting — and extending — the evidence.

B.8The demo#

demo.py runs a full agent turn sequence against a scripted model, and exercises several local boundaries. Its stable operational summary is:

text
outcome     completed
turns       7
trajectory  read_file → fetch_url → issue_credit → write_file
credits     ['acme-9:50.0']   ← one mock entry, though the model asked twice

The executable output also reports estimated tokens, illustrative cost, event count and a trace. Those values depend on the current fixture; they are not an invoice or evidence that a live customer was credited. The final response identifies the runbook evidence and says no production remediation ran.

Parallel reads. Turn one issued read_file and fetch_url together; both executed concurrently under tools.parallel because neither declares mutates.

A path denial. Turn two attempted /etc/passwd. PathPolicy denied it before the tool ran, and the model received Denied by policy (path.outside): ... Do not retry this call; find another route or ask the user.

An egress policy. runbooks.internal.corp is on the allowlist; the demo exercises the allowed host. Separate policy tests exercise denied hosts.

A human declining. A $500 credit exceeded the stand-in approver’s limit and came back as A human declined this action: over my approval limit — route to the billing lead. An observation, not an exception.

Idempotency. The model requested the same $50 credit twice. CHARGES has one entry, because the second call’s idempotency key was already in completed_calls.

No repeated approval. The trace shows the human asked once, not twice — because a session grant for that exact call suppresses the re-ask, which is Chapter 19’s approval-fatigue defence in three lines of loop.py.

The demo also proposes a reusable skill. Its structural checks can pass, but the default gate has no behavioural verifier, so the proposal remains PROPOSED and is not retrievable. Supply fresh host-run trials before promotion; a proposal cannot attest to its own correctness.

B.9What is deliberately absent#

Real provider clients. Implement ModelClient with authentication, endpoint-specific message conversion, timeouts, usage mapping and contract tests.

A vector index and embedding model. MemoryStore.retrieve uses keyword scoring; replace its retrieval implementation for semantic search.

A sandbox runtime. Enforce isolation outside the agent process (Chapter 16).

A real summariser. TruncatingSummarizer keeps heads and tails. It exists to prove the plumbing of compaction; you substitute a model call and nothing around it changes.

A web server, queue and database for production deployment (Chapter 29).

B.10Replace one seam at a time#

The phrase “starting skeleton” can suggest a weekend in which you replace the scripted model, the JSONL log, keyword memory and in-process tools together. That produces a system whose first failure has four plausible causes and no baseline. The safer route is a sequence of substitutions in which the deterministic harness stays as the control.

Start by recording a small contract suite against the unmodified reference. Include at least one case for each terminal outcome you intend to operate, one allowed and one denied call per resource class, one approval, one duplicate side effect, one parallel read batch, one compaction and one result blocked on the way back into context. Save the RunResult, event types and normalised span shape for each case. Do not snapshot random ids or wall-clock durations; compare the semantics.

Then replace one boundary and make the same cases pass:

Seam Reference component Production replacement must add Acceptance signal
Model synchronous ModelClient.complete; ScriptedModel Provider authentication, timeouts, streaming assembly, rate-limit classification, usage reconciliation and provider-specific message conversion The same scripted contract remains green; provider canaries produce the same named outcomes and complete tool-call objects.
Durable state one JSONL file per run, immediate flush and optional fsync Atomic append, monotonic sequence allocation, schema versioning, tenant partitioning, retention, encryption and a single-writer lease Every log prefix replays deterministically; forced worker loss produces one ordered continuation.
Tools in-process Python callables Network adapters or isolated workers, credential scoping, receiver-side idempotency, deadlines, payload limits and authoritative status lookup Denied calls never arrive; ambiguous effects reconcile; repeated logical keys return the original result.
Approval synchronous callable Durable requests, actor authentication, expiry, delegation, argument digest and policy-version binding A decision after process restart resumes the same call, and an altered call requires a new decision.
Trace in-memory spans and terminal renderer OTLP export, sampling, redaction, correlation across queues and retention controls Trace parents or links preserve causality across services; errors, policy decisions and cost totals remain observable under the chosen sampling policy.
Memory in-memory keyword scoring Tenant-safe persistence, indexed retrieval, deletion, provenance updates, correction handling and retrieval evaluation Cross-tenant probes return zero hits; stale or corrected facts stop appearing; retrieval quality is measured on labelled queries.
Sandbox absent A separate constrained runtime with resource ceilings, network policy, artefact transfer and teardown Escape tests fail closed; time, memory, process and egress ceilings are observed under load.

The order of those replacements should follow the direction of evidence. A provider adapter is usually first because it turns the scripted decisions into real, variable decisions while leaving every action local and reversible. Keep the temporary JSONL store, fixture tools and in-memory trace. When a canary differs from the script, the new adapter and model are the only new variables. Record both the provider’s raw finish reason and the normalised StopReason; otherwise a truncated tool call can masquerade as a model choosing to stop.

Move durable state next, but test it with ScriptedModel. Feed exactly the same turns while killing workers after RUN_STARTED, after MODEL_TURN, after TOOL_CALL_STARTED, after the external effect, after TOOL_CALL_COMPLETED and during RUN_FINISHED. Define a semantic projection for comparison rather than assuming the current reference’s live object and replayed object are already identical. They are not: live _observe() appends a tool name to state.trajectory, while replay appends the stored canonical signature; replay sets finished and outcome from RUN_FINISHED, while the live path returns those values in RunResult without setting the two state fields; and denied, declined or unknown-tool observations change the live transcript without a durable observation event, so replay cannot reconstruct them.

Treat those mismatches as reference limitations to repair, not semantics to copy. A production projection should choose one stable trajectory representation, apply terminal fields consistently on live and replay paths, and durably record every observation with whether execution occurred. Until that repair exists, compare a normalised projection that deliberately maps signatures to tool names, takes terminal status from RunResult on the live side, and excludes non-durable rejection observations with an explicit gap report. Then compare goal, messages, turns, usage, cost, completed-call map, compaction count, normalised trajectory and terminal status at every log prefix. The result should be equivalent to the uninterrupted projection or explicitly uncertain at the remote-effect boundary. “The run recovered” is not an assertion.

Then externalise tools one risk class at a time. Read-only calls establish authentication, deadlines and result-shape conversion without risking an effect. Reversible writes establish receiver keys, status lookup and compensation. Irreversible writes come last, after durable approval and ambiguous-outcome handling exist. Preserve the ToolResult rule that expected failures are observations. If a remote 400 becomes an uncaught SDK exception, the new adapter has erased the model’s chance to repair its arguments and changed a local tool failure into a harness ERROR.

Approval follows the event store because a decision that can outlive a process needs something durable to wait on. Store an immutable digest of the proposed tool name and canonical arguments with the request. On resume, recompute the digest before accepting the decision. A grant for issue_credit(acme-9, 50.0) must not authorise issue_credit(acme-9, 500.0), even if a UI grouped them under the same friendly sentence. Also store the actor and tenant from PolicyContext; a bare boolean cannot answer who approved which customer’s effect.

Only after those paths are stable should you distribute execution. A queue introduces delivery duplication, delayed messages, cancellation races and a second trace-context boundary. The local thread-pool test cannot cover any of them. Carry the run id, tool-call id, idempotency key, parent span id, deadline and tenant as an authenticated work envelope. Make the worker return a result keyed by the logical call rather than by one delivery attempt. Then test two deliveries of the same envelope and a late result arriving after the run has already ended.

This sequence is not a universal calendar. It is a way to keep causality. If your first requirement is sandboxed code execution, the sandbox may arrive before a live provider. The invariant is that each change has one new failure surface, a retained control and an acceptance signal. “We replaced the whole reference stack and the demo still completes” is one observation with too many explanations.

Do not delete the fake when the real adapter arrives. ScriptedModel remains the control-plane test double. The in-memory tracer remains useful for unit tests even when production exports OTLP. A temporary-directory event log still makes replay tests fast after production state moves to a database. The reference component and its production replacement serve different tests; replacing the former in the test suite makes every result slower and introduces a failure source into tests that are trying to isolate the loop.

Use a small numeric gate to separate parity from improvement. Suppose your contract set has 40 cases and you run each five times against a non-deterministic provider: that is 200 run results. The reference or previous release passes 190, or 95.0%. The new adapter passes 186, or 93.0%. The difference is four results, two percentage points. There are only forty base-case clusters. The aggregate margins cannot establish a significant change or identify exactly four regressions: B may fix some cases while breaking more. Retain all paired outcomes, group repeats by case and inspect every discordant trace before drawing a conclusion.

For illustration, suppose the paired inspection finds four regressions and no fixes. If three are truncated streamed tool calls and one is a provider timeout misclassified as fatal, the model did not regress. Fix response assembly and failure classification, then repeat the same 200-run gate. If the failures are incorrect tool selections with complete schemas and clean transport, the model or the visible tool set becomes a plausible cause. The trace attributes already separate input tokens, output tokens, cached tokens, selected-tool count, context utilisation, finish reason and retry attempts. Preserve those attributes through the adapter so the comparison remains answerable.

The arithmetic also exposes cost before broad rollout. If the five repeats average three model turns, the gate makes 600 provider calls. At an illustrative $0.018 per turn, one gate costs $10.80: 40 cases × 5 repeats × 3 turns × $0.018. Run it ten times during a migration and you have spent $108. That may be trivial; the point is to know it before a continuous-integration job runs on every commit. Keep the deterministic tests as the per-change gate, schedule the provider suite at a cadence its cost and rate limits support, and require it before release.

Do the same arithmetic for recovery, not just quality. In an illustrative week of 50,000 Reference Agent runs, suppose 0.4% are interrupted after at least one model turn: 200 resumptions. If 15 of those logs contain a started mutation without a completion, the ambiguous-action rate is 15 ÷ 50,000 = 0.03% of all runs but 15 ÷ 200 = 7.5% of resumptions. The first percentage looks negligible; the second tells the on-call engineer that roughly one in thirteen recoveries needs reconciliation. Report both denominators. Then split the fifteen by receiver-idempotent, authoritatively queryable, reversible and human-only actions. That split determines whether the next investment belongs in adapters, recovery automation or approval operations.

Instrument substitution versions explicitly. A trace should say which model-adapter version, event-schema version, policy version, tool-adapter version and context-assembly version produced the run. Without those labels, a before-and-after comparison collapses several deployments into one bucket and sends you back to anecdotes. Avoid putting raw prompts, secrets or full tool payloads into the labels; use stable version identifiers and keep sensitive evidence behind controlled links. The reference tracer demonstrates attribute placement. Your production telemetry policy decides what may leave the tenant boundary.

The same one-seam rule applies to capability. Do not add a real model and simultaneously expose fifty production tools because the model can now choose them. Start with the reference registry behaviour: all tools are visible at twelve or fewer; beyond twelve, select() returns a narrowed view based on a deliberately simple keyword score. Replace that selector only after you have labelled tool-choice cases. The code itself warns that the threshold is arbitrary and should rise until selection accuracy falls. Treat 12 as a teaching default, not a discovered constant.

Productionising the memory path requires the same honesty. MemoryStore.retrieve() is keyword-scored, TruncatingSummarizer preserves message heads and tails, and verified skills live in an in-memory library unless explicitly saved. Those are executable demonstrations of interfaces and policies. They are not claims that lexical retrieval, truncation or a JSON file meets Reference Agent’s quality, privacy or concurrency requirements. Replace each mechanism behind its method, replay the contract suite, and add a labelled evaluation for the new promise. A vector index without a retrieval evaluation is merely a more expensive unmeasured function.

Preserve the contract, replace the mechanism

A production substitution is complete when the original local contract tests still pass, the new boundary has failure-injection tests for its own failure modes, and traces let you distinguish adapter failure from model failure. A larger implementation that erases the seam has moved Reference Agent backwards even if the happy-path demo still works.

B.11Using it#

Three ways it is useful beyond reading.

As a reference for one subsystem. If you are building a permission engine, read policy.py and its tests — the ordered evaluation, the three-valued verdict, and the test asserting a rule cannot widen a resource denial are the load-bearing parts, and they transfer to any language.

As a test-harness pattern. ScriptedModel plus the terminal-outcome tests is a pattern worth copying wholesale into your own codebase, whatever it is written in. The ability to force your loop down important exit paths deterministically, changes how much you can safely change.

As a starting skeleton. Fork it, add one adapter for your provider, replace MemoryStore.retrieve with your index, and you have the spine from Appendix A’s phase one with the tests already written.

The tests worth copying

Whatever language you build in, these are the tests that catch the defects this book describes. Each one is a few lines.

  1. One test per terminal outcome, forced with a scripted model. Any outcome you cannot force is one you cannot reason about.
  2. Resume without repeating a confirmed side effect. Replay a recorded completion and assert no duplicate. Add a separate crash-before-completion test that requires receiver idempotency, reconciliation or an explicit uncertain outcome.
  3. A rule cannot widen a resource denial. A permissive ALLOW rule plus a path outside the allowlist must still deny.
  4. The default verdict is never ALLOW. A one-line invariant that decays silently during a hurried release.
  5. Trace shape: one root, and parallel spans parented to their batch.
  6. The generated schema, not the function — types, required fields, enums, descriptions, and that risk metadata never leaks into it.
  7. Goal survives compaction byte-identically. Assert the string, not its meaning.
  8. A retry does not happen on a non-retryable class, with an injected clock so the test is instant.
  9. Tenant A cannot read tenant B — and confirm the test can fail.
  10. An unparseable judge response scores as failure, not as a pass.

Key Takeaways

  • The reference runtime uses only the standard library; pytest is a development dependency. Fast offline tests encourage frequent verification of its local contracts.
  • Module dependency order is a consequence of Chapter 3’s boundaries: policy imports tools because a decision needs risk metadata, and tools never imports policy because a tool must not describe itself as permitted.
  • Four real defects, three found by looking at output rather than by a failing test: a schema that typed every parameter as a string under postponed annotations, a shared span stack that misreported parallel causality, worker threads that did not inherit trace context, and a test double that hid context growth entirely.
  • Two tests turned out to be wrong about correct code, and both taught something worth pinning: loop detection should beat the turn ceiling because it names the real problem, and ceilings are enforced at turn boundaries so a cost limit does not bound a single turn.
  • What is absent is named rather than faked: provider clients, a vector index, a sandbox, a real summariser, a server. Each omission has a docstring identifying the substitution point.
  • The ten tests in this appendix are the ones that catch the defects the book describes, and they transfer to any language.
  • A proposed tool call crosses total admission, policy, approval, idempotency, execution, result guardrails and observation in a deliberate order; preserving that order matters more than preserving the Python.
  • A green test is a bounded claim. State what it does not prove, inject failures at the uncovered boundary, and verify that security tests turn red when their protection is removed.
  • Productionise one seam at a time while the deterministic harness remains the control, then measure parity before trying to improve quality.

Hands-on

Run it, then break it 1. Clone, install, run pytest -q and python demo.py. Read the rendered trace and match each span to the code that created it. 2. Remove the contextvars.copy_context() line and re-run the demo. Watch the parallel spans detach into roots — that is what a trace lying about causality looks like. 3. Delete the typing.get_type_hints call and re-run the tool tests. The schema assertion should fail. Without that assertion, the defect could survive a happy-path tool test. 4. Add a ninth terminal outcome to RunOutcome and write the test that forces it. Notice how much of the loop you had to understand to do it — that is the loop being small enough to reason about. 5. Port the ten tests from this appendix into your own codebase, in your own language. That is the smallest useful thing you can take from this book, and it is not small. 6. Choose one test from the bounded-claim table. Write its event timeline, mark the interval it does not exercise, then add a fault injection at that exact interval. 7. Wrap a real model client behind ModelClient without removing ScriptedModel. Run the same contract cases against both and classify every difference as model, adapter, policy or infrastructure before changing a prompt.