# Build and test a bounded AI agent

Original LockedIn Labs training. All people, tenants and records are synthetic.
These files execute on your machine. No API key, customer data, model inference
or external sending is used by any supplied lab.

Use the downloads on the [free lab page](https://training.lockedinlabs.ai/training/guides/agent-engineering#lab-files-title), or the Academy course's Practice section. Use a disposable
working directory. Python labs require Python 3.11 or later; the delegation
coordinator and MCP labs require Node.js 24. Never put real source documents or
credentials in these examples.

## 0. Choose a lab and prepare your workspace

This is an original hands-on route for forward-deployed engineers who can run a
terminal command and read a small Python function. If you are new to coding,
first trace the supplied inputs, state changes and refusal decisions on paper;
label that work unexecuted. Python labs need Python 3.11 or later and a text
editor. Use `python3 --version` to check your interpreter before starting.

Use the lab library on the free page to filter by runtime and open the exact
walkthrough for your chosen file. Download the files you need and save them
in a new disposable folder. The evaluation lab imports the workflow file from
that same folder, so keep both original filenames. You do not need an account,
API key or package installation for the standard-library labs.

| Start with | Preparation | Evidence to keep |
| --- | --- | --- |
| Durable workflow reference | Python standard library; one `.py` file | A paused draft, a fresh-process read and a bound decision. |
| Scoped retrieval | Python standard library; standalone file | Authorized evidence, content-edit invalidation and revoked access. |
| Regression comparison | Workflow and evaluation `.py` files together | Reference, broken-candidate and missing-case reports. |
| MCP tool admission | Node.js 24; standalone `.mjs` file | Definition change, current grant and argument/result boundary decisions. |
| MCP stdio compatibility | Node.js 24; standalone file; owned child process | Correlated wire frames, a useful tool result and protocol/tool refusals. |
| Agent canary evidence | Python standard library; standalone file | Segment comparisons, incomplete evidence and a non-authorizing review recommendation. |
| Shared delegation | Node.js 24; standalone `.mjs` file | Consumed attempts, freed slots and late-result refusal. |
| Worker recovery | Python standard library; actual SIGKILL extension on macOS/Linux | A committed lease after process death, bounded reclaim and current receipt. |
| LangGraph SQLite restart | Separate environment with three pinned packages | The same real graph checkpoint inspected and resumed in fresh processes. |
| LangGraph wiring | Separate virtual environment and pinned package | An observed interrupt and scripted resume; in-memory only. |
| LangChain tool loop | Separate virtual environment and pinned packages | Proposed tool, dispatched result and stop reason. |

Choose one lab rather than running every command at once. The sections below
contain the commands in order. Start with the standard-library workflow, inspect
one refusal, then compare the regression candidates. Framework installation
requires normal package-registry access; installed examples run without provider
inference. They use real framework orchestration with scripted proposals.

If Python reports that a file is missing, check your working directory and the
original filename. If the evaluator cannot import its reference, put both files
beside each other. A deliberately blocked candidate exits 2; preserve its JSON
instead of treating that exit as a package failure. Never fix a refusal by
removing the relevant case or using a real customer's records.

## 1. Run the durable workflow reference

The reference uses only the Python standard library. Run:

```sh
python3 fde-agent-engineering-lab.py test
python3 fde-agent-engineering-lab.py demo --db field-desk.sqlite
python3 fde-agent-engineering-lab.py inspect --db field-desk.sqlite
python3 fde-agent-engineering-lab.py review --db field-desk.sqlite --decision accepted
python3 fde-agent-engineering-lab.py inspect --db field-desk.sqlite
```

The test command exits nonzero on failure. Retain your actual output rather than
copying an expected transcript. A successful run exercises nine cases: useful
authorized work, foreign source denial, foreign run denial, restart and request
replay, conflicting request reuse, decision replay/conflict, changed proposal,
changed source and withdrawn source.

`demo` creates an `awaiting_review` record. Every following command is a fresh
process reading the same SQLite file. `review` records only acceptance or
rejection of that synthetic draft. It does not send a response. Repeat the same
accepted decision and verify it is stable. Attempt `--decision rejected` after
acceptance and inspect the conflict. For a foreign-tenant read, use:

```sh
python3 fde-agent-engineering-lab.py inspect --db field-desk.sqlite --tenant south
```

That command must refuse. This CLI is not an authentication service: a deployed
host must derive tenant identity from an authenticated session and authorize
reviewers before invoking the domain methods. Its SQLite transaction is a local
reference, not evidence about a distributed fleet.

## 1b. Build current, authorized retrieval context

Download `fde-retrieval-context-lab.py` from the free lab page. This is a
standalone standard-library exercise; it does not import the workflow file.
Predict the two retrieval outcomes before running:

```sh
python3 fde-retrieval-context-lab.py test
python3 fde-retrieval-context-lab.py demo
```

The demo contrasts a deliberately flawed global-top-one/post-filter comparison
with a builder that checks current source authorization before chunking and
ranking. A foreign document contains more query-term occurrences, so the flawed
comparison throws away its only result even though an authorized passage exists.
The scoped builder preserves the useful task and keeps the foreign text out.
All records are synthetic; the CLI principal stands in for trusted host context,
not an authentication service. Real storage adapters must enforce the same scope.

This baseline ranks exact term frequency over paragraph chunks. It is not an
embedding index or an answer generator. Inspect the returned tenant, source,
version, paragraph locator and content digest. Then compare an unchanged cache
hit with a text edit that keeps the old version number and with a revoked grant.
The cache compares the entire currently eligible source snapshot, not only the
previously selected hits, so newly permitted evidence can invalidate an empty
lookup. This intentionally rescans eligible text; it is not a performance claim.
The process-local dictionary has no expiry, eviction or deletion API. Revocation
prevents reuse through the rechecked boundary but does not erase every old cache
entry. Design and test retention separately before storing any real content.

The eight bundled tests also cover source withdrawal, copied result mutation,
identity-scoped reuse, duplicate source identity, new evidence after an empty
lookup and a character budget that keeps whole chunks. The ceiling covers
selected evidence text, not metadata, tokens or a future model request. A
`withheld_budget` result is distinct from `no_matching_context`; neither proves
there is no answer in the corpus. The `context_ready` state means selected
lexical evidence is present, not that a generated answer is supported.

Keep your actual JSON, a source revision and one new case the lexical baseline
misses. Explain what a semantic retriever would need to improve, and what must
remain invariant when replacing the ranker. The lab labels evidence untrusted
but does not call a model or test prompt-injection resistance. Add inference,
answer-support, hostile-source and receiving-engineer checks separately before
making those claims.

### Compare retrieval quality on a fixed packet

On the free lab page, use the interactive comparison to choose a candidate,
inspect a case and predict its interpretation. The browser displays supplied
local observations; use the command below to gather your own report.
Use **Download receiving brief** in that comparison to keep the selected report,
all case states and source snapshots in an editable Markdown worksheet. Fill in
your own explanation and receiver challenge, then add actual local output when
you run the lab. Preparing the worksheet does not mark a run or a course complete.

Use the same standalone file; no packages, embeddings or provider key are needed:

```sh
python3 fde-retrieval-context-lab.py evaluate --candidate scoped
python3 fde-retrieval-context-lab.py evaluate --candidate postfilter
python3 fde-retrieval-context-lab.py evaluate --candidate refuse-all
python3 fde-retrieval-context-lab.py evaluate --split development
python3 fde-retrieval-context-lab.py evaluate --split transfer
```

The eight visible synthetic cases have authored paragraph relevance labels.
Inspect `evaluation_cases()` in your downloaded file before changing a candidate.
Four development cases cover foreign ranking dominance, two required passages,
withdrawal and revoked access. Four transfer cases cover different terminology,
a withdrawn distractor, a foreign-only source and evidence that cannot fit its
character budget. “Transfer” is a practice partition, not an independent hidden
test. Candidates retrieve without receiving the relevance labels.

Each report retains case IDs, source snapshot identities, required and returned
passages, character ceilings, per-query scores and explicit denominators.
The context ceiling is two chunks; the flawed post-filter candidate still takes
one global result before checking access. The same whole-chunk character budget
is applied to its output. Do not feed that flawed comparison into model context.

| Field | How to interpret it |
| --- | --- |
| `mean_recall_at_2` | For each executed answerable query, distinct required passages found divided by required passages; average those query recalls. |
| `mrr_at_2` | Average the reciprocal of the first relevant rank within two results; an answerable miss contributes zero. |
| `answerable_planned`, `answerable_executed` | Keep the original population separate from the queries actually run. |
| `correct_empty`, `unanswerable_executed` | Count the supplied empty-evidence cases separately; they do not raise answerable recall. |
| `invalid_evidence`, `context_budget_violations` | Current source/identity/content checks and context ceilings remain independent of relevance. |
| `unrun`, `complete` | Name omissions. Complete means all selected cases ran, not that the candidate is ready to deploy. |

On the complete supplied packet the scoped candidate has a recall sum of 3 over
five answerable queries: mean recall 0.6. The three empty-evidence cases return
empty. Development mean recall is 1 across two answerable queries; transfer is
1/3 across three. Keep the terminology miss and budget withholding in that latter
denominator. These values describe this visible local packet only.

In D2 the post-filter candidate finds one of two required passages at rank 1.
That query has recall 0.5 and reciprocal rank 1. Its complete-packet means are
0.1 recall and 0.2 reciprocal rank. An early useful hit is not complete evidence.
The refuse-all control gets the three empty cases right but has zero answerable
recall. Source identity includes the tenant, version, locator and content digest;
stale or altered text cannot earn a valid hit, and duplicates do not multiply recall.

Now deliberately leave a miss unrun:

```sh
python3 fde-retrieval-context-lab.py evaluate --candidate scoped --omit-case T1-different-terminology
```

This produces JSON and intentionally exits 2. The mean becomes 0.75 over four
executed answerable queries, but `complete` is false and the omitted case stays
in `unrun`. That higher displayed average is not a retrieval improvement.
Omit every answerable case and the metric denominator is zero: its value is
null, not perfect. Keep the original complete reports before making a change.

Ask a receiver to add a new terminology query and an independently chosen
relevant paragraph, then explain a miss without tuning the expected label to
your candidate. Retain the case-level report and propose a new ranking or
chunking experiment. This exercise evaluates a small lexical/context baseline,
not semantic retrieval, answer support, a live model, prompt-injection resistance
or enterprise performance. Relevance labels may be incomplete in real corpora;
a representative evaluation needs separate labeling and sampling evidence.
[Ranked retrieval evaluation](https://nlp.stanford.edu/IR-book/html/htmledition/evaluation-of-ranked-retrieval-results-1.html)
and the [information-retrieval evaluator reference](https://sbert.net/docs/package_reference/sentence_transformer/evaluation.html#sentence_transformers.evaluation.InformationRetrievalEvaluator)
provide further metric context; this lab uses the explicit denominator policy above.

## 1c. Recover an abandoned worker lease

Download `fde-worker-recovery-lab.py` from the free lab page. It is a standalone
standard-library exercise using only temporary synthetic SQLite records.
Predict whether the abandoned worker may record a completion before running:

```sh
python3 fde-worker-recovery-lab.py test
python3 fde-worker-recovery-lab.py demo
```

The demo records a queued admission, worker A’s lease, a database-connection
restart and worker B’s reclaim at lease expiry. B receives fencing number 2;
A’s attempt to finish with number 1 is rejected as `lease_lost`. B records the
current synthetic result. The timestamps are injected clock ticks, not observed
latency or a process-kill experiment. The admission is not a completion, and a
finished worker record is not a human acceptance or an external-effect receipt.

Inspect the original eight queue tests as well as your actual demo JSON. They cover bound
request replay, per-tenant capacity, lease expiry, stale completion, cancellation,
checkpoint-version compatibility, persisted attempt limits and stable result
receipts. An unchanged request replay cannot resurrect a cancelled job; an old
worker cannot claim a declared newer checkpoint format. The format marker is a
simple gate, not state migration or complete deserialization evidence.

Write the operator’s next action for each record:

| Observation | Next safe decision to explain |
| --- | --- |
| A worker has a live lease | Keep a second worker from stealing it. |
| A lease expires and another worker reclaims it | Preserve the increasing fencing number; reject stale completion. |
| Cancellation invalidates the lease | Stop local acceptance; investigate any effect already in flight. |
| The attempt budget is exhausted | Retain the record and investigate; do not silently replenish attempts. |
| Rolled-back code cannot read the format marker | Keep work unclaimed until compatible handling is established. |

The last stored `running` state with an expired lease is not evidence that a
business action failed. This lab has no external effects. Its fencing check
protects only its completion row; an external destination needs idempotency,
supported fencing or reconciliation of uncertain outcomes. Cancellation does
not kill the old process or undo an already-issued request. Do not infer
at-most-once business execution from a local completion refusal.

The synthetic tenant and worker arguments stand in for trusted host context.
Before exposing these methods through a service, authenticate and authorize
callers, derive tenant scope and supply an authoritative clock. Fencing numbers
are concurrency versions, not credentials. The local writer transaction is not
a distributed queue, fleet-concurrency test, deadline controller or a deployed
checkpoint backend. Keep those limitations in the receiving-team runbook.

### Run a real local process-kill rehearsal

On **macOS or Linux**, use the same standalone file with Python 3.11 or later:

```sh
python3 fde-worker-recovery-lab.py crash-demo
```

This command creates a temporary synthetic database, starts its own child worker
and waits for a flushed `lease_committed` event. That worker has committed the
lease and is idle with its SQLite connection open. The parent kills only that
owned child with SIGKILL, waits for its exit and reopens the database. It does
not accept a process ID or connect to your application database. The internal
worker entry refuses an existing file. Temporary records are removed after the
report is assembled; keep your JSON output separately if you need it later.

Predict each field before running. Compare your actual report with this worksheet:

| Field | Observation to explain |
| --- | --- |
| `boundary.event`, `worker_exit` | `lease_committed`, then `-9` on the supported POSIX run. |
| `boundary.job`, `after_kill` | Identical stored lease; still `running`, with a null result digest. |
| `integrity_check` | SQLite returns `ok` for this local after-commit experiment. |
| `before_expiry`, `attempts_before_reclaim` | `lease_busy` at injected tick 4; one attempt remains consumed. |
| `reclaimed` | Worker B holds fence 2 at injected tick 5; two attempts are consumed. |
| `reconstructed_stale_submission` | A synthetic replay of A's old lease is refused as `lease_lost`. |
| `finished`, `receipt_replay_equal` | B records the synthetic result; reading the same receipt is stable. |

The stale submission is reconstructed by the parent. It is **not** a message
sent by the killed worker. The clock ticks are injected, so this is not a
measurement of expiry delay or recovery time. The ten bundled tests include the
real process-boundary check and preservation of an existing file; those two
process tests explicitly skip on unsupported systems. On Windows use `demo`
and record this SIGKILL rehearsal as unrun. The command uses no model, network,
customer records, sending action or authenticated service.

Keep the distinction between a process killed **after commit while idle** and
a kill inside a transaction, a host power failure, a storage outage or a remote
queue failover. This exercise does not establish those properties or prove
at-most-once external effects. [Python documents the POSIX kill and return-code
behavior](https://docs.python.org/3/library/subprocess.html#subprocess.Popen.kill);
[SQLite describes its commit boundaries and storage assumptions](https://www.sqlite.org/atomiccommit.html).

For a receiving task, ask another engineer to identify the observation that
permits reclaim, explain why the failed pre-expiry attempt spends no budget and
propose one still-unrun failure experiment. Keep the original JSON with that
operator note; do not relabel a proposed experiment as executed evidence.

### Compete for one job from separate processes

Use Python 3.11 or later and the same standalone worker file. Predict the
consumed attempt count before running:

```sh
python3 fde-worker-recovery-lab.py race-demo
```

The command admits one synthetic R1 job in its own temporary database and
starts two fresh Python processes. Each opens a separate connection before a
shared starting gate releases both. Either worker may win; the gate does not
guarantee overlapping execution or a particular ordering. Inspect your actual
report instead of assigning worker A the winning role in advance.

| Report field | Observation to explain |
| --- | --- |
| `workers` | Two distinct process IDs; one `claimed`, one `lease_busy`. |
| Winning `fence` | 1: only one claim was admitted. |
| `after_claims.attempts` | 1, not 2: the refused contender did not consume another attempt. |
| `after_claims.state` | `running`; `result_digest` is still null. A lease is not completion. |
| `integrity_check` | `ok` on the local file after both processes exit. |

The current claim method begins a writer transaction **before** reading the
job, checking its lease and updating its ownership. A process-local lock would
not coordinate these separate workers. The second writer must inspect the
committed lease and refuse while it is live. SQLite's bounded lock wait can
instead fail with a database error under other contention; that is different
from the domain's `lease_busy` refusal. Do not translate a storage failure into
permission to run. [SQLite transaction control](https://www.sqlite.org/lang_transaction.html)
explains the single-writer boundary and `BEGIN IMMEDIATE` behavior.

Receiving task: give a peer the JSON without your explanation. Have them identify
the current owner, consumed attempts and evidence still missing before calling
the job complete. Then inspect the sequential expiry demo: explain why reclaim
at injected tick 5 may increment the fence, while a competing claim at tick 0
may not. Keep this local observation separate from fleet capacity, clock
agreement, lock-timeout recovery, network partitions and external effects.
No throughput, distributed execution or authenticated worker identity is tested.

## 1d. Inspect a shared delegation budget

Use the interactive trace on the free lab page before running the coordinator.
Choose a case, predict the next decision, then use **Next event** to inspect the
supplied observation. **Previous** and **Restart trace** let you revisit the
reasoning. The browser replays recorded synthetic output; it does not dispatch
workers, run a model or grade your work.

Download `fde-delegation-lab.mjs`, check Node.js 24 and run:

```sh
node --version
node fde-delegation-lab.mjs supported
node fde-delegation-lab.mjs failed-retry
node fde-delegation-lab.mjs source-withdrawn
node fde-delegation-lab.mjs terminal-manual
node fde-delegation-lab.mjs exact-deadline
node fde-delegation-lab.mjs --all
```

The final command prints 13 scenario records, one JSON object per line. Exit 0
means the replay ran; it does not mean every proposed action was permitted.
No packages, API keys or network calls are needed. Keep the actual output.

In `failed-retry`, D1 and D2 consume both attempts. D1 returns at 400 virtual ms;
D2 fails at 450. At that point, attempts consumed is **2** and in flight is **0**.
Failure releases a slot without refunding the shared allowance. D3 is refused
at 600. Its message at 650 is `unknown-attempt`, not proof of a third execution.

`candidate-held` is intermediate. In `supported`, review rechecks the current
source before producing a draft for operator review; nothing is sent. In
`source-withdrawn`, the candidate arrives before withdrawal, but the later review
cannot use it. In `terminal-manual`, the explicit manual decision closes the
request before the result arrives. In `exact-deadline`, an event at 1,000 virtual
ms is too late. These timestamps are injected inputs, not latency measurements.

For a changed-constraint experiment, use a disposable copy and change only
`maxAttempts: 2` to `maxAttempts: 3`. Predict and rerun `failed-retry` and
`foreign-scope`. More budget can admit a retry; it cannot authorize foreign scope,
revive a withdrawn source or reopen the terminal manual route. Ask a receiver
to explain those boundaries from the recorded events.

The [existing delegation workbook practice](/training/guides/workbook#optional-lab-delegate-agent-work-without-multiplying-authority-30-minutes)
contains the fuller worker packet, conflicting references and receiving contract.
Keep that material beside this trace. The local coordinator is single-process
and scripted: it does not establish atomic distributed reservation, authenticated
worker messages, real cancellation or model quality. A deployment needs isolated
integration evidence for those boundaries.

## 1e. Recover a receiver after its response is lost

**25-minute local lab · Python 3.11+ · No packages or provider key**

An agent worker can lose its connection after a destination commits an action.
Restarting that worker does not establish whether the action happened. Download
`fde-receiver-recovery-lab.py` from the lab chooser. Before running it, predict
the effect and receipt counts for each boundary below.

```sh
python3 fde-receiver-recovery-lab.py test
python3 fde-receiver-recovery-lab.py demo
```

The parent starts only children of this lab in a temporary synthetic workspace.
Each child exits abruptly at an explicitly named SQL boundary. Exit 24 occurs
after both inserts but before commit; exit 23 occurs after commit but before
printing a response. Exit 25 belongs to a deliberately broken receiver that
commits its effect separately from its receipt. These codes are lab markers,
not HTTP status codes. The temporary files are removed after the report.

| Boundary | Before retry: effects / receipts | After retry: effects / receipts |
| --- | --- | --- |
| Before shared commit | 0 / 0 | 1 / 1 |
| After shared commit, response missing | 1 / 1 | 1 / 1 |
| Broken: effect committed alone | 1 / 0 | 2 / 1 |

These are expected local observations; keep the actual JSON from your own run.
A missing response means the caller has no receipt. It does **not** mean the
destination has no effect. Inspect `before_retry` before reading `after_retry`.
For the shared transaction, `BEGIN IMMEDIATE` serializes the local writer check;
the unique tenant/request key binds a canonical payload digest to an effect ID.
The effect and receipt commit together. Retrying the same key and payload returns
the stored ID. A different payload conflicts; a new key can create another effect.
This is request replay protection, not business-level duplicate detection.

Inspect `test_distinct_processes_racing_same_key_share_one_effect`: two actual
processes target one local database and receive the same receipt. Replays still
check the current synthetic work-order grant. A key, digest or tenant name is not
an authentication credential. In a deployed host, derive tenant context from
verified identity and authorize the specific action; do not trust an agent's
argument to supply either authority or scope.

**Receiving task.** Explain the broken receiver's `1 / 0` state to another
engineer, then show the retry that produces `2 / 1`. Ask them to identify the
transaction boundary that repairs it. Propose one changed-payload experiment,
run it, retain the conflict and unchanged effect count, and record their actual
challenge and your revision. Keep a separate unrun register for remote service
failure, database failover, retention expiry and a crash inside commit.

The row in `effects` is the entire synthetic effect. No email, payment, real tool
call, model or network request runs. This is abrupt local process exit around
commit, not a power-loss test or a distributed exactly-once guarantee. A local
transaction cannot atomically include an unrelated remote service. That service
needs its own supported idempotency contract, stable operation identity and an
authorized outcome lookup or reconciliation path. Deleting receipts or choosing
a fresh key to clear an uncertain outcome can duplicate work. Set retention and
reconciliation policy before adapting the pattern to a real receiver.

Implementation references: Python's [explicit transaction control](https://docs.python.org/3/library/sqlite3.html#transaction-control)
and SQLite's [atomic commit boundary](https://www.sqlite.org/atomiccommit.html).
The lab uses explicit SQL with `isolation_level=None`; it does not depend on the
newer Python `autocommit` connection argument.

## 2. Run a real LangGraph interrupt/resume graph

Keep the framework experiment separate from the standard-library reference.
In a new isolated virtual environment, install LangGraph from its normal Python
package registry, then record the actual resolved versions:

```sh
python3 -m venv .venv
. .venv/bin/activate
python -m pip install "langgraph==1.2.16"
python -m pip freeze > requirements.lock.txt
python fde-langgraph-lab.py
```

On Windows activate with `.venv\Scripts\activate`. Keep the lock file with your
practice evidence; use it to reproduce the same environment. The top-level package is pinned to the version used for this walkthrough;
transitive versions are captured by your freeze output. It is not a complete
pre-resolved lock manifest. The supplied graph was run with Python 3.12 and
LangGraph 1.2.16 on 17 October 2026.
Follow the linked upstream documentation if your installed version differs.

The graph uses StateGraph, InMemorySaver, interrupt and Command. It asserts that
the first invocation waits for review, then scripts an accepted resume. This
checks graph wiring with a deterministic drafting node. It is neither a live
model evaluation nor a real human-review session. InMemorySaver does not survive
a process ending. The SQLite reference illustrates a separate durable design;
it is not presented as a LangGraph checkpoint adapter.

## 2a. Resume a real LangGraph after process exit

Download `fde-langgraph-durable-lab.py`. This uses the real LangGraph StateGraph,
interrupt and SqliteSaver APIs. It is separate from both the earlier in-memory
walkthrough and the standard-library SQLite reference. Use a new disposable
working folder and isolated environment:

```sh
python3 -m venv .venv-graph
.venv-graph/bin/python -m pip install "langgraph==1.2.16" "langgraph-checkpoint==4.3.0" "langgraph-checkpoint-sqlite==3.1.2"
.venv-graph/bin/python -m pip freeze > langgraph-requirements.lock.txt
.venv-graph/bin/python fde-langgraph-durable-lab.py test
.venv-graph/bin/python fde-langgraph-durable-lab.py demo --db graph-demo.sqlite
```

On Windows replace `.venv-graph/bin/python` with
`.venv-graph\Scripts\python.exe`. Package installation needs normal registry
access; installed lab commands need no model key and refuse socket connections.
The three package versions above were used with Python 3.12.14 on 18 October
2026. Retain your own resolved lock output and observations.

Fourteen tests check real process-separated persistence, scope naming, refused
resume, sequential terminal replay/conflict, rejection, per-request proposal
binding and preservation of an existing file. The scripted demo uses four child
processes: start, inspect, accept and repeat the decision. Compare these fields:

| Observation | What it establishes locally |
| --- | --- |
| First process: awaiting_review, next review, interrupted true | A pending graph checkpoint exists. |
| Second process: same digest and pending state | A fresh graph instance reads the saved work. |
| Third process: accepted, empty next, interrupted false | The scripted bound decision resumed this local graph. |
| Fourth process: decision-replayed | The sequential host guard returns the recorded decision. |

The demo requires a new filename. An existing `graph-demo.sqlite` is preserved
with `demo-needs-new-file`; inspect it or choose another disposable filename.
Do not reuse a real application database. For manual work, run `start` and
`inspect` against another new local file:

```sh
.venv-graph/bin/python fde-langgraph-durable-lab.py start --db graph-practice.sqlite
.venv-graph/bin/python fde-langgraph-durable-lab.py inspect --db graph-practice.sqlite
```

Copy the `digest` from your actual inspect JSON. Replace the literal
`PASTE_OBSERVED_DIGEST` below with that value; it is a proposal fingerprint,
not a credential. Leaving the placeholder in place must refuse the resume.

```sh
.venv-graph/bin/python fde-langgraph-durable-lab.py resume --db graph-practice.sqlite --decision accepted --digest PASTE_OBSERVED_DIGEST
```

Before acceptance, try a wrong digest, add `--source-version 4`, or add
`--deny-review`. Each must return a named refusal with exit 2 and preserve the
pending checkpoint. The flags simulate trusted host source/reviewer state;
they are not authentication. Start a second request with `--request R2` and
show that the R1 digest cannot approve it. Then accept the correct proposal,
repeat the decision and try the conflicting rejected decision. Retain the
actual outputs rather than counting every process exit as completed work.

The [LangGraph interrupt guide](https://docs.langchain.com/oss/python/langgraph/interrupts)
explains that the interrupted node restarts from its beginning. This lab puts
no external effect before interrupt. Its code enables strict checkpoint
MessagePack handling and disables inherited remote tracing. Keep the SQLite
file private and never load an arbitrary checkpoint database.

Normal process exit is not a hard-kill, power-loss, concurrency or migration
experiment. The sequential host checks are not a distributed compare-and-set;
thread naming is not tenant authentication. File access, reviewer authorization,
retention, encryption, backup and schema compatibility require separate deployed
controls. No message is sent, and the CLI's scripted accepted value is not an
observed independent human review or a business outcome.

### Recover a failed node before resuming review

**20-minute extension · Same pinned environment and file, Edition 2 or later**

A node failure is not a human-review interrupt. Run the new scripted failure
rehearsal against another fresh disposable database:

```sh
.venv-graph/bin/python fde-langgraph-durable-lab.py failure-demo --db graph-failure.sqlite
```

Five separate processes start with a deliberate review-node exception, inspect
the checkpoint, retry the failed node, supply a bound decision and replay it.
The exception simulates an unavailable dependency; no external outage occurs.
The graph, exception propagation, checkpoint storage and process boundaries are
real local execution. Compare the actual fields before explaining the result:

| Process | Node calls in this process | Pending failure | Human interrupt |
| --- | --- | --- | --- |
| Deliberate failure | draft, review | true | false |
| Fresh inspection | none | true | false |
| Retry without a new input | review | false | true |
| Bound accepted resume | review | false | false |
| Same decision replay | none | false | false |

The failure occurs after the completed draft step has checkpointed. Retrying
uses `graph.invoke(None, config)` on the same thread, not a new input or a
human decision. The observed draft digest must remain unchanged. In these
pinned versions, `task_error_nodes` still contains review when retry reaches
interrupt. That retained metadata is not itself an instruction to retry again.
Inspect `interrupted`, `next` and `pending_failure` together. The latter is a
host interpretation for this sequential teaching graph, not a general SDK API.
Even the stored application string `awaiting_review` precedes the actual
interrupt, so it cannot by itself distinguish these states.

Try the sequence manually in a new file. The first command deliberately exits 2;
this is an observed failure, not a passing execution. Copy the digest from your
actual inspect output into each placeholder below (a fingerprint, not a secret):

```sh
.venv-graph/bin/python fde-langgraph-durable-lab.py start --db graph-retry.sqlite --fail-before-review
.venv-graph/bin/python fde-langgraph-durable-lab.py inspect --db graph-retry.sqlite
.venv-graph/bin/python fde-langgraph-durable-lab.py retry --db graph-retry.sqlite --digest PASTE_OBSERVED_DIGEST
.venv-graph/bin/python fde-langgraph-durable-lab.py resume --db graph-retry.sqlite --decision accepted --digest PASTE_OBSERVED_DIGEST
```

Before retry, try `resume`: it must return `retry-required`. Try a wrong digest,
`--source-version 4` or `--deny-review` with retry; each refuses without running
a node. After retry reaches an interrupt, retry must return `not-failed` and leave
it waiting. Retry never supplies a human decision. Review-node entry appears
again when the bound decision resumes because code before interrupt re-enters;
keep irreversible effects out of that region. No provider or sending tool runs.

**Receiving task.** Have another engineer explain why the error metadata after
retry does not justify another automatic attempt. Retain all five observations,
the unchanged digest and per-process node calls. Ask them to distinguish a
completed earlier graph step from side effects inside a failed node. A node's
own partial external work may repeat on retry; this exercise does not make that
work atomic or establish a distributed exactly-once guarantee. Keep automatic
retry policy, real dependency outages, concurrency, cancellation and crash recovery
inside a node separately unrun. Consult the current [checkpointer guide](https://docs.langchain.com/oss/python/langgraph/checkpointers)
for step/task persistence; retain the actual package versions beside this result.

### Recover a parallel step without repeating its completed branch

**20-minute extension · Edition 3 · Same pinned packages**

The sequential failure exercise finishes the draft in an earlier graph step.
This extension schedules draft and policy as sibling branches in the **same**
step and joins them before review. Policy deliberately raises; draft completes.
No real policy service, model or external action runs. Predict whether the
completed draft needs to run again, then use another new disposable file:

```sh
.venv-graph/bin/python fde-langgraph-durable-lab.py parallel-demo --db graph-parallel.sqlite
```

Four fresh processes fail the policy branch, inspect saved work, retry and
supply a bound review decision. The local SDK schedules the sibling branches;
this exercise does not measure wall-clock overlap, throughput or cost. First-call
ordering can vary. Retain the actual JSON and compare these observations:

| Process | Node calls in this process | What to inspect |
| --- | --- | --- |
| Scripted branch failure | draft + policy, order unspecified | task_results.draft true; policy error; no review interrupt |
| Fresh inspection | none | Same draft digest and saved draft task result |
| Retry same thread with no new input | policy, review | Draft does not repeat; policy checked; review interrupted |
| Bound accepted resume | review | Accepted state and no next node |

`get_state` exposes the saved draft output while the sibling policy task still
has an error. Visible draft text, digest or `awaiting_review` status does not
establish that the joined step is complete. Inspect `task_results`,
`task_error_nodes`, `policy_status`, `next` and `interrupted` together. The
`checked` policy value is a deterministic teaching marker, not an authorization,
safety or compliance assessment. The review node will not proceed until that
branch returns its marker, and interrupt still requires a separate decision.

For a manual rehearsal, use a different fresh filename. The first command
intentionally exits 2. Copy your actual inspect digest into the placeholders:

```sh
.venv-graph/bin/python fde-langgraph-durable-lab.py parallel-fail --db graph-parallel-practice.sqlite
.venv-graph/bin/python fde-langgraph-durable-lab.py parallel-inspect --db graph-parallel-practice.sqlite
.venv-graph/bin/python fde-langgraph-durable-lab.py parallel-retry --db graph-parallel-practice.sqlite --digest PASTE_OBSERVED_DIGEST
.venv-graph/bin/python fde-langgraph-durable-lab.py parallel-resume --db graph-parallel-practice.sqlite --decision accepted --digest PASTE_OBSERVED_DIGEST
```

Before retry, attempt parallel-resume; it refuses with not-awaiting-review.
Try a wrong digest, `--source-version 4` or `--deny-review` on parallel-retry;
each refuses without discarding the completed draft. After retry reaches
interrupt, a second parallel-retry returns not-failed. Replaying the same bound
decision returns the recorded result; a conflicting later decision is refused.
The parallel graph uses a distinct synthetic thread name from the sequential
graph. That separates this exercise's state, not permissions; trusted host
identity and access to the checkpoint file still need a real deployment boundary.

**Receiving task.** Ask another engineer to identify the saved result before
retry, then show that the fresh process calls policy and review without draft.
Have them explain why starting a new thread or providing a new input would be
a different operation, and why completing both branches still does not grant
human acceptance. Keep their actual challenge and your revision with the JSON.
The SDK persists completed task writes for recovery; it does not undo or make
an external effect inside a failed branch atomic. Do not clear the checkpoint
to hide a failure. Multi-host coordination, real policy service failures,
cancellation, reducer conflicts, schema upgrades, power loss and external
exactly-once behavior remain separately unrun. The [checkpointer guide](https://docs.langchain.com/oss/python/langgraph/checkpointers)
explains pending task writes; the fields above are observations of this pinned
local experiment.

## 2b. Run the actual LangChain tool-calling loop offline

Download `fde-langchain-agent-lab.py` from the free lab page or the LangChain
lesson's Practice section. In a new disposable folder, create a separate
virtual environment before installing the pinned packages. Record all resolved
versions; this walkthrough does not require the earlier LangGraph setup:

```sh
python3 -m venv .venv-chain
. .venv-chain/bin/activate
python -m pip install "langchain==1.4.6" "langgraph==1.2.16"
python -m pip freeze > requirements.lock.txt
python fde-langchain-agent-lab.py test
python fde-langchain-agent-lab.py supported
python fde-langchain-agent-lab.py foreign-source
python fde-langchain-agent-lab.py authority-override
python fde-langchain-agent-lab.py unknown-tool
python fde-langchain-agent-lab.py spin
```

On Windows activate with `.venv-chain\Scripts\activate` instead.

Five tests passed with Python 3.12 and those top-level package versions on
17 October 2026. Retain your own actual result. The tests also refuse socket
connections, and the script disables inherited remote tracing. Package
installation needs registry access; the installed lab needs no provider key.

This uses the real LangChain `create_agent` orchestration and a deterministic
`BaseChatModel` adapter that proposes each supplied tool call. It is not an
inference run or a measure of model quality. The ordinary trajectory should be
`input`, `tool-proposal`, `tool-result`, `answer`. The scoped business lookup
admits P1 and withholds the foreign P9 text. The strict tool schema rejects an
added tenant field before lookup; an ungranted sender is never dispatched.

`spin` intentionally reaches the graph step limit, reports `incomplete` and
exits with status 2. This is an expected refusal, not an installation failure.
A finished graph can still produce a withheld answer. Do not turn either state
into an accepted business outcome. The step ceiling is not an elapsed-time,
token, distributed-work or external-service budget.

Inspect the source and change a supplied proposal. Add a test that keeps the
ordinary request useful while proving the new refusal. A future provider-backed
adapter requires a separate environment configuration, model-quality evaluation
and execution evidence; do not relabel these scripted results as model results.

## 2c. Evaluate useful work, stale approval and missing evidence

Download `fde-agent-evaluation-lab.py` and `fde-agent-engineering-lab.py` into
the same disposable directory. This exercise uses the Python standard library;
no package installation, model account or network connection is needed.
Use Python 3.11 or later, and record `python3 --version` with your output.

Predict the failed case IDs before running:

```sh
python3 fde-agent-evaluation-lab.py test
python3 fde-agent-evaluation-lab.py run --candidate reference
python3 fde-agent-evaluation-lab.py run --candidate refuse-all
python3 fde-agent-evaluation-lab.py run --candidate stale-review
python3 fde-agent-evaluation-lab.py run --candidate reference --omit-case withdrawn-source
```

The first command verifies the evaluator, not a learner's competence. Each run
calls the reference's real domain methods against new temporary synthetic
SQLite databases. The two intentionally broken subclasses are teaching copies:
`refuse-all` refuses useful work; `stale-review` skips the current-source check
before acceptance. They are never proposed production changes.

Inspect the actual report rather than copying these expectations:

| Candidate | What the declared contract should expose |
| --- | --- |
| reference | Utility 1/1, boundary 4/4, recovery 1/1; all six cases observed. |
| refuse-all | Foreign-source denial passes, but useful drafting fails; setup-dependent cases report failed observations. |
| stale-review | Useful drafting and replay pass; changed and withdrawn source acceptance fail. |
| reference with omission | Five executed of six planned; withdrawn-source remains unrun, so the local decision is blocked. |

Exit 0 means `local_contract_met`. The mutants and omission example deliberately
exit 2 with `blocked`; this is the exercise's expected result, not an installer
error. The JSON keeps utility, boundary and recovery denominators separate and
lists each failed or unrun case. Duplicate and unknown case IDs are rejected.
Unexpected exceptions become failed observations with their class name only;
source bodies, database paths and exception messages are not exported.

Change one real requirement before changing code. For example, the current
contract does not authenticate reviewers or test revocation between pause and
resume. Define that adapter and its acceptance cases rather than treating the
existing six passes as evidence for it. Add a new case without removing a failed
case from the denominator. Keep predictions, observed JSON, source revision and
unrun work in the handover. These are deterministic local observations, not
inference-quality scores, customer outcomes or permission to release a service.

## 2d. Check MCP tool admission

This standalone lab practices the host policy around a Model Context Protocol
(MCP) tool. It uses MCP-shaped descriptor fixtures, not a running MCP server or
SDK. No packages, network, credentials or model account are needed. Download
`fde-mcp-contract-lab.mjs`, check Node.js 24, then run:

```sh
node --version
node fde-mcp-contract-lab.mjs test
node fde-mcp-contract-lab.mjs supported
node fde-mcp-contract-lab.mjs changed-description
node fde-mcp-contract-lab.mjs forged-scope
node fde-mcp-contract-lab.mjs hint-is-not-authority
node fde-mcp-contract-lab.mjs demo
```

The test command runs nine checks and exits nonzero on a failed assertion.
`demo` prints 15 scenario records. Scenario commands exit 0 when a replay runs,
including a refused call; inspect `code` and `dispatches`, not just the exit
status. Unknown selectors and extra arguments exit 2.

The [MCP tools specification](https://modelcontextprotocol.io/specification/2026-07-28/server/tools)
defines discovery, tool schemas and calls. Annotations from untrusted servers
are untrusted. Input validation and access controls remain implementation
responsibilities. This exercise adds an original local host-admission policy;
its digest review is not claimed to be a mandated MCP consent mechanism.

Predict these distinctions before reading your actual output:

| Case | Boundary to investigate |
| --- | --- |
| supported | One useful P1 lookup; returned excerpt remains untrusted data. |
| changed-description / changed-schema / changed-output-schema / changed-annotations | Reviewed definition changed; zero business dispatches. |
| forged-scope | Extra tenant field rejected before dispatch. |
| foreign-record | One business lookup; unavailable without foreign text. |
| revoked-grant / changed-connection / changed-principal / expired-approval | Existing synthetic review does not renew current authority. |
| hint-is-not-authority | A read-only annotation cannot grant a sending callable. |
| withdrawn-source | A valid ID cannot recover withdrawn content. |
| malformed-result / oversized-result | Returned context withholds invalid or oversized output. |

The host's connection key, principal, tenant, approval and clock are trusted
synthetic inputs. A real adapter must establish them independently of model
arguments and self-reported server names. The local fingerprint covers the
reviewed JSON definition, with stable object-key ordering. It is not a signature,
endpoint authentication, code attestation or proof that the tool is safe.

The business callable rechecks current scope even when called without the
adapter. Argument and result checks are handwritten for exactly the authored
contract; they are not a general JSON Schema validator. The result ceiling counts
serialized UTF-8 bytes, not characters or tokens, and applies after the local
lookup. It cannot cap transport allocation, tool duration, invocation rate or
other external costs.

In a disposable copy, remove only the digest-comparison line. Run `test` again
and retain the failed changed-definition observation. Restore the check and show
that the ordinary lookup still works. Ask a receiver why increasing an output
budget cannot authorize a foreign source or a sender.

Finally, add one new case and its expected dispatch count. Identify the real MCP
transport/version negotiation, OAuth, revocation during a remote call, deadlines
and rate controls still to test separately. Valid returned text remains
untrusted: this lab makes no inference call and does not measure prompt-injection
resistance, factual quality or human approval.

### Trace a source instruction through a second tool proposal

Edition 2 adds a two-call host-boundary rehearsal:

```sh
node fde-mcp-contract-lab.mjs followup-demo
```

Each record first looks up a valid P1 source whose excerpt contains an injected
instruction to change to south scope and send a response. That first result is
schema-valid, within the byte limit and context-ready. Its trust remains
untrusted-source-data. The lab does not detect, remove or neutralize the text.
The next proposal is an authored fixture, not a model response. This experiment
tests application enforcement after source retrieval, not model resistance to
prompt injection.

| Follow-up | First lookup | Second proposal and observation |
| --- | --- | --- |
| ordinary-lookup | One dispatch, untrusted excerpt | A normal P1 lookup remains admitted; the returned excerpt is still untrusted. |
| scope-claim | Same accepted source | An extra tenant field is invalid-arguments with zero further business dispatches. |
| send-claim | Same accepted source | send_response is tool-not-allowed with zero further business dispatches. |
| revoked-between | Same accepted source | Trusted fixture permission is revoked before the next dispatch; access-denied retains zero further business calls. |

Explain the two dispatch counts separately. Returning a source is not approving
its instructions. The host's north scope never comes from the excerpt or the
second proposal. Revocation changes the trusted host fixture, not a model-owned
flag. The source is allowed to contain unsafe instructions without gaining any
application authority; a schema-valid response is not a safe instruction channel.

Have a receiver predict the sender refusal before running the command. In a
disposable copy, add an approved-looking field to the second proposal and retain
the argument-shape refusal alongside the ordinary lookup. Identify where a real
host derives identity and where a remote result could arrive after permission
changes. Keep model behavior, semantic injection detection, remote in-flight
revocation and external-effect cancellation unrun. No network, provider, message,
credential or remote tool is used by this continuation.

## 2e. Shape an agent trace before exporting it

**20-minute observability lab · Python 3.11+ · No packages or provider key**

Use the browser exercise above to predict a case and inspect its recorded JSON.
Changing the case or trying again clears your prediction; it does not run the
projector or save progress. Then gather your own output with the file below.

Download `fde-agent-trace-lab.py`. This standalone file projects application
events through a deliberately closed schema before they could reach a logger.
It does not install an OpenTelemetry SDK or export anything. Predict which
events retain useful diagnostics before running:

```sh
python3 fde-agent-trace-lab.py test
python3 fde-agent-trace-lab.py demo
```

The demo processes eight visible synthetic cases and reports every case ID.
T1–T3 project: admission keeps a source version, a completed tool call keeps its
bounded name/outcome/timing, and a model's readiness claim stays labeled
`model_proposal`. That claim is not an application receipt or human approval.
T4–T8 refuse: source text, an attempted tenant override, an exception message,
a Boolean used as elapsed time and an unbounded tool name. Inspect your output;
these are deterministic local observations, not model or learner scores.
Tool statuses and elapsed milliseconds are supplied inputs, not measured calls.

| Boundary | Explain the implementation choice |
| --- | --- |
| Host context | The caller supplies a synthetic tenant and run separately; event fields cannot override them. Real authentication remains outside this lab. |
| Field contract | Exact keys and approved scalar values only. Extra nested fields refuse the event, rather than silently exporting their contents. |
| Evidence origin | Trusted host code chooses the event kind; a model cannot label its own claim an application observation. |
| Failure | A fixed code crosses the boundary. Raw exception messages and rejected values are not echoed into the demo report. |
| Size | Compact serialized UTF-8 bytes are checked before release, with a default ceiling of 256. This does not limit ingestion memory or telemetry volume. |

The approved diagnostic fields remain useful: an operator can correlate R1,
inspect a withheld tool outcome or identify a timeout without reading a source
body. Refusing every event would discard that useful path. Conversely, removing
payload text from the viewer after a logger already persisted it is too late
for this boundary. Call the projector before logging, tracing or exception
reporting, and test those real sinks separately when adapting the exercise.

In a disposable copy, add a `source_locator` diagnostic. Define its allowed
shape before changing the schema, keep a useful case, and add a case that tries
to put source text or a personal identifier into that field. Do not solve the
exercise by accepting arbitrary strings or removing the refused cases. Ask a
receiver to reproduce a useful tool event and explain the model-claim label
without your driving. Keep their question, your revision and actual output.

The synthetic tenant enum and run pattern are teaching fixtures. Even a valid
opaque identifier can be linkable or sensitive in a real environment. This is
not general PII detection, redaction of arbitrary text, a logging SDK or a
compliance assessment. No exporter configuration, retention, access policy,
deletion, sampling, distributed correlation or production sink is tested.
The current [OpenTelemetry GenAI content-capture guidance](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md#capturing-instructions-inputs-and-outputs)
recommends leaving instructions, inputs and outputs uncaptured by default.
Its conventions are developing; the event format here is an original application
exercise and does not claim to implement those conventions.

## 2f. Inspect an MCP stdio compatibility exchange

### Choose the wire profile before writing the adapter

Download [the stdio compatibility lab](https://training.lockedinlabs.ai/templates/fde-mcp-stdio-lab.mjs). This is an actual parent/child-process exchange over standard streams, pinned to **MCP 2025-11-25**. It uses the handshake model: `initialize`, a matching version and tools capability, then `notifications/initialized`, `tools/list` and `tools/call`. The client waits for each response before issuing the next request. It does not implement the newer profile or automatic fallback.

Version choice matters in field integration. The [2026-07-28 versioning source](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/versioning.mdx) describes a different model: version and capabilities travel with each request, without this initialization handshake. Its compatibility guidance distinguishes legacy, modern and dual-era implementations. An engineer maintaining a handshake-based integration needs an explicit supported-version contract; passing this older-profile lab does not establish compatibility with a modern endpoint. Sources checked 18 October 2026.

Use the pinned [lifecycle](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle), [stdio transport](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#stdio) and [tool messages](https://modelcontextprotocol.io/specification/2025-11-25/server/tools) as protocol references. This teaching implementation supports one authored tool and one pending request. It is not an MCP SDK, general schema validator, complete protocol implementation or interoperability certification.

### Run two processes and read the boundary

Save the one file in a new disposable folder. Node.js 24 and permission to start your own child process are required. No packages, model, socket, provider key or customer records are used. The child inherits no environment variables from the parent. The program always launches its own file with the current Node executable; it accepts no arbitrary command or remote endpoint.

```sh
node --version
node fde-mcp-stdio-lab.mjs test
node fde-mcp-stdio-lab.mjs supported
node fde-mcp-stdio-lab.mjs foreign-record
node fde-mcp-stdio-lab.mjs forged-scope
node fde-mcp-stdio-lab.mjs demo
```

The supported transcript contains four sent methods in order. The initialized notification has no request ID and receives no response. Each request has an incrementing ID; each accepted response must match the pending request. The peer writes a diagnostic to stderr, while stdout carries newline-delimited JSON frames. The client records only diagnostic presence, message identities and the synthetic result. `peerExited: true` confirms the owned child was closed for that run; it says nothing about remote lifecycle management.

The successful result remains `untrusted-tool-content`. A server-supplied name is not authenticated identity, and an available tool is not a grant to read another tenant. This peer has a fixed synthetic north scope and can return only the P1 teaching record. There is no sign-in or real authorization service in the lab. Keep the earlier host-admission lab beside it: transport correctness and application authority are separate obligations.

### Diagnose the failure at the right layer

| Case | Expected observation | Receiving-engineer explanation |
| --- | --- | --- |
| `supported` | `received` with a synthetic excerpt | The selected wire path returned data; a person has not accepted its use. |
| `foreign-record` | `tool-refused` / `tool-unavailable` | A valid tool invocation can refuse a resource without a protocol failure. |
| `forged-scope` | `protocol-refused`, JSON-RPC code `-32602` | An added model-controlled tenant field violates the one authored argument contract. |
| `protocol-error` | JSON-RPC code `-32601` | A protocol error is distinct from `result.isError` in a tool execution response. |
| `wrong-id` | `unexpected-response-id` before a call | Never attach an unrelated response to the current operation. |
| `missing-capability` / `unsupported-version` | Stop before discovery/calling | Do not silently assume a tool capability or change the agreed profile. |
| `before-ready` | `discovery-protocol-error` | For this pinned handshake profile, omitting the notification leaves the peer unready. |
| `stdout-noise` / `oversized-frame` | `non-json-stdout` / `frame-budget` | A friendly status line on stdout breaks framing; the teaching client also limits its receive buffer. |
| `timeout` | `request-timeout` and a closed child | The local deadline limits waiting; it does not prove a remote action was cancelled. |

Failures use fixed codes rather than copying malformed bytes. The 4 KiB receive limit and one-request-at-a-time contract are deliberately narrow; this client does not implement multiplexing, server notifications, pagination, reconnect, cancellation messages, HTTP/OAuth, server authentication, comprehensive output validation or a deployment sandbox. The deadline closes the owned local peer; do not substitute it for distributed cancellation semantics.

### Extend and hand over the integration decision

First predict `wrong-id` and `before-ready`, then compare actual transcripts. In a disposable copy, add a second response with a valid-looking but unrelated ID and preserve the refusal. Restore the useful P1 path after the change. Explain what must change before this client could accept legitimate server notifications without confusing them with responses.

Give a receiver the selected protocol revision, commands, actual output, tool/resource refusals and the remaining integration work. Ask them to distinguish a received source, a refused tool, a broken protocol exchange and an accepted business outcome. A separate SDK/modern-profile experiment needs its own package versions and evidence; do not relabel this compatibility lab as that experiment.

## 2g. Evaluate agent canary evidence before widening access

### Define the comparison before looking at the aggregate

Download [the canary evidence lab](https://training.lockedinlabs.ai/templates/fde-canary-evidence-lab.py). It evaluates supplied synthetic observations; it does not deploy, route traffic, call a model or decide a person's qualification. All acceptance labels are authored inputs. No supplied packet is evidence of a customer result or an actual human review.

A candidate can look healthy in a service-wide average while a smaller workflow deteriorates. The [SRE canarying chapter](https://sre.google/workbook/canarying-releases/) explains why control/canary separation, representative exposure and aligned measurement windows matter. It also distinguishes artificial load tests from observing a real canary. This exercise is a local analysis of fictional packets, not a canary deployment. Reference checked 19 October 2026 UTC.

The local policy uses two fixed workflow segments, routine and exception. It requires at least 20 supplied observations per segment and cohort, permits at most a five-percentage-point error increase or accepted-work decrease, and limits the candidate response-latency p95 to 1.5 times the control. These are deliberately authored teaching thresholds, not SLO recommendations, statistical confidence bounds or a claim that twenty samples establish readiness. In a real release, choose and document appropriate criteria before exposure.

The packet declares admitted counts independently of its observation rows. The evaluator reconciles them; a missing row is not a successful request. It rejects duplicate IDs, inconsistent scalar types, impossible accepted/error combinations and unexpected segments. Windows must agree and each observation must fall inside the shared half-open interval. This validates the toy packet's consistency, not the authenticity of its counters, assignment, versions, labels or source system.

### Run the cases and identify the decision layer

Save the standalone file in a disposable folder. Python 3.11 or later is required, with no packages or provider configuration.

```sh
python3 --version
python3 fde-canary-evidence-lab.py test
python3 fde-canary-evidence-lab.py baseline-ready
python3 fde-canary-evidence-lab.py aggregate-green-segment-red
python3 fde-canary-evidence-lab.py all-refusal
python3 fde-canary-evidence-lab.py missing-observation
python3 fde-canary-evidence-lab.py demo
```

Every output retains `origin: synthetic-teaching-packet` and `release_authorized: false`. A ready-for-human-review result only says this packet met this exercise's criteria. It neither grants production access nor recommends bypassing the accountable release decision.

| Packet | What to retain | What the engineer should explain |
| --- | --- | --- |
| `baseline-ready` | Four cohort/segment counts and two p95 comparisons | Useful supplied outcomes met the authored criteria; no production release was authorized. |
| `aggregate-green-segment-red` | Candidate errors 7/120 versus control 4/120; exception errors 6/20 versus 2/20 | The aggregate increase is 2.5 percentage points, but the exception increase is 20 points. The segment regression stops the recommendation. |
| `all-refusal` | Candidate errors are zero and accepted work is zero | A system that refuses useful work cannot win by having no recorded execution errors. |
| `missing-observation` | Declared admission and observed row counts disagree | Hold for evidence; do not silently drop the missing record or infer success. |
| `missing-segment` / `small-sample` | Missing or below-minimum segment coverage | Hold even if the included rows look healthy. The threshold is teaching policy, not statistical power. |
| `mixed-window` | Different candidate/control intervals | Do not compare a previous incident window with the new candidate's window as if they were concurrent. |
| `latency-regression` | The exception segment's response-latency p95 | Error rate alone does not describe acceptable operation. These response durations exclude human review/correction effort. |
| `safety-event` | One supplied critical event and the stop reason | Do not average away an explicit safety trigger or excuse it because the sample is small. |
| `duplicate-observation` / `malformed-scalar` | A hold with an integrity reason | Duplicate data and a Boolean used as an integer are not ordinary healthy observations. |

Accepted counts and response latency remain separate. A production field trial also needs the receiving operator's acceptance criteria, review/correction effort, queue delay, attempt costs and manual fallback. The two supplied segments do not establish representativeness for unmodeled tasks, tenants, loads or times of day. There is no randomization, causal inference, confidence interval, authenticated telemetry ingestion or actual rollout in this file.

### Change one criterion and transfer the decision

Predict the aggregate-green/segment-red result before running it. Have a receiver compute both denominators from the JSON rather than reading only the status. In a disposable copy, add a third segment and show why both admission and observation contracts must change; do not let the unknown segment disappear from the comparison. Then add a new required observation field and preserve invalid/missing cases alongside the useful baseline.

Record your policy revision, synthetic packet, commands, actual output, hold/stop reasons and untested receiving-environment assumptions. Name the person who would authorize exposure in a real rollout. Keep the manual fallback and checkpoint-version recovery plan beside that decision. This evaluator is teaching evidence for a discussion, not a production promotion or rollback controller.

## 3. Make a meaningful change

Choose one extension. Keep a working baseline and record your changed source:

- Add a source withdrawal test before the model context boundary.
- Add an authenticated reviewer adapter over the domain method. Test revocation
  between pause and resume; do not trust a browser-supplied tenant or role.
- Add a bounded LangChain model adapter with a single read-only tool. Keep the
  model configuration outside the prompt. Record any unrun provider steps.
- Propose a durable LangGraph checkpointer, then test crash/resume and replay in
  that implementation. Do not relabel the in-memory run as durable.
- Add a simulated unknown external-effect outcome and a reconciliation record.
  Never add a real sending integration just to make the walkthrough impressive.

## 4. Hand it over

For a structured receiving conversation, use the free [agent-system receiving
defense](https://training.lockedinlabs.ai/training/guides/receiving-review#optional-agent-system-receiving-defense-25-minutes).
Bring one useful run, one boundary failure and a changed-condition decision; the
receiver should be able to continue one available task from your packet.


Submit the contract, state diagram, source revision, environment versions,
observed commands/output, one failing case and its repair, known limits and the
operator recovery note. Ask a peer to reproduce an authorized request and
diagnose a source-change refusal without you driving. Keep scripted outcomes,
observed local results and unrun deployment work clearly separated.

Further reading: https://docs.langchain.com/oss/python/langchain/agents and
https://docs.langchain.com/oss/python/langgraph/overview. These are technical
references, not a partnership or a credential.
