Where governance becomes code
The leader track spends a whole session (a4) on WHY agents need human gates - irreversible actions, external sends, spend. Tonight is the HOW, and it is smaller than the leaders imagine: because b6's checkpointer already saves state at every step, "pause here and ask a human" is just a checkpoint nobody has resumed yet. No queues, no callback servers - one compile argument and three verbs: approve, edit, reject. Time travel falls out of the same machinery for free.
The approval gate 9 min live
Not every step deserves a gate. The rule: gate where the cost of a mistake exceeds the cost of a delay. Then the mechanics - pause BEFORE the risky node, surface the pending action, resume on a human verdict.
LiveWhy gates - the leader's rule arrives in code4 min▶
Until tonight DataDesk only READ things: CSVs, questions. Reads are cheap to get wrong - re-run and shrug. The moment an agent WRITES - files, tickets, emails, database rows, money - a wrong action has cleanup cost, and some have no cleanup at all. The rule your architecture should encode:
- Gate where mistake cost > delay cost. Saving a file over last month's report, sending anything external, spending anything - gated. Reformatting a draft the human will read anyway - not gated.
- The gate must show the ACTUAL action, not a summary of intent: the exact path, the exact text, the exact recipient. Approving a vibe is not approval.
- Rejection must be cheap. If saying no wastes the whole run, reviewers stop saying no. With checkpoints, a reject parks the thread - nothing upstream is lost.
The July 2025 Replit incident, revisited from the builder's chair. An agent with write access deleted a production database during a code freeze. Every postmortem take agreed on the same missing artifact: a pause between "the agent wants to run this" and "this ran". That pause is tonight's compile argument. The leader track teaches your executives to demand it; you are about to be the person who can say "it is already there".
LiveInterrupts and breakpoints - pause, surface, resume5 min▶
The static form: name the risky node at compile time. The run stops BEFORE it, mid-flight state saved:
- invoke(None, config) is the resume idiom: "no new input, continue from the checkpoint".
- update_state writes a correction into the checkpoint before resuming - the human is briefly a node in the graph.
- The pause has no timeout. Approve in three seconds or three days; the checkpoint waits in the database either way. That is why b6 was the prerequisite - no checkpointer, no gate.
Self-studyTwo altitudes: middleware HITL vs graph interrupts, and dynamic gates4 min read▶
- Same concept, two altitudes. At the create_agent level, b4's
HumanInTheLoopMiddlewaregates tool calls by name with approve/edit/reject policies - configuration, not construction. At the graph level, tonight's interrupts gate ANY node - including deterministic ones middleware never sees. Because create_agent runs ON LangGraph, both are the same checkpoint mechanics underneath; pick the altitude you are already building at. - Dynamic gates. A node can call
interrupt(payload)from INSIDE its own logic - pause only when THIS run needs a human (amount over threshold, confidence under threshold), surfacing exactly the payload the reviewer needs. Resuming feeds the human's answer back into the node viaCommand(resume=...). Static gates express policy ("all writes are reviewed"); dynamic gates express judgment ("this write looks risky"). - Where the human actually clicks. In production, the paused thread surfaces in your app: a Slack message, a review queue, an inbox row. The graph does not care - it sees only "resumed with a verdict". Your thread_id-mapping homework from b6 is exactly the plumbing this needs.
Time travel 5 min live
b6 taught you that history is a list of full snapshots. Tonight's second trick: any snapshot is a valid launch pad. Replay to reproduce, fork to explore.
LiveRewind: replay any past, fork any future5 min▶
Three moves, all built on get_state_history:
- Browse:
graph.get_state_history(cfg)yields every checkpoint of the thread, newest first - each with its own config handle. - Replay:
graph.invoke(None, past.config)re-executes from that snapshot - same state in, so you watch the same decisions unfold. Reproducing a flaky agent failure stops being folklore. - Fork:
graph.update_state(past.config, {...})returns a NEW config - a branch. Run it and you have two futures from one past, comparable side by side. The original history is untouched.
Self-studyDebugging with time travel - the incident workflow3 min read▶
The workflow that changes on-call life, step by step:
- Reproduce exactly. User reports "the agent gave a nonsense answer at 14:32". Pull the thread, list history, replay from the checkpoint before the bad step. No screenshots, no "cannot reproduce" - the state is the repro.
- Bisect the run. Replay from successively earlier checkpoints until the answer goes bad - now you know WHICH node's input was already poisoned versus which node did the poisoning.
- Fork the fix. Edit the poisoned state (or the prompt/tooling) on a fork and re-run the same future. Fix verified against the real failing case before you ship it - and the original run is preserved as evidence for the postmortem.
- Know the limits. Replay re-executes LIVE calls: a model may answer differently, a tool may hit changed data. Determinism grows with how much of the state you pin - another argument for small, explicit state.
DataDesk asks permission ★ 14 min · everyone builds
DataDesk's first WRITE action - saving a report file - arrives pre-gated. You will run the full triangle: approve and watch the file appear, reject and watch nothing happen, then edit the path mid-pause and approve the corrected write.
Create a scratch reports/ directory in your DataDesk project. Everything written tonight lands there and nowhere else - gates or not, blast-radius discipline stays.
Add the report flow to your graph: a draft node (model writes the summary text and proposes a path) and a save_report node (writes the file). Fixed edges: START → draft → save_report → END.
Compile with the gate: interrupt_before=["save_report"] plus your b6 checkpointer. Run "Save me a one-paragraph summary report of this week". It drafts... and stops. Print get_state(cfg).next - the pending node, visible.
Approve: inspect s.values["path"] and the text, then graph.invoke(None, cfg). The file appears in reports/. Reject: re-run on a fresh thread and simply do not resume - confirm no file was written. The no-op IS the feature.
Edit then approve: third run, pause, then graph.update_state(cfg, {"path": "reports/week-29-summary.md"}) and resume. The file lands at YOUR path, not the model's. You just acted as a node in the graph.
Rewind Tuesday ★ 8 min · build your own
Take a finished conversation, list its photo stack, relaunch from the middle of it twice - once as replay, once as a fork with a different question - and diff the two futures.
Use a thread with a few turns on it (this morning's gate run, or your b6 "phoebe-monday"). List its history and print one line per checkpoint: id prefix, what runs next, message count.
Pick a mid-conversation checkpoint - call it Tuesday. graph.invoke(None, past.config): the same future re-runs in front of you. Note anything that differs (live model calls may vary - that is a finding, not a bug).
Fork: update_state(past.config, ...) with a different question, capture the returned config, and run it. Two futures now exist from one past.
Compare the final answers of the original branch and the fork side by side. This diff - same history, one changed input - is the cleanest prompt-debugging instrument you own from tonight.
The flaky Friday agent, caught. A team's report agent produced a wrong total roughly once a week, never on demand. Once checkpointing landed, the on-call pulled the failing thread, bisected by replay, and found a tool returning a stale cache one node earlier than anyone suspected. Time-to-diagnosis went from "weeks of guessing" to one afternoon - not because anyone got smarter, but because the failure finally sat still.
Try it yourself - this week ◐ 30-45 min total
- Finish the full triangle if you did not complete it live: one approved file, one rejected no-op, one edited path. Keep all three transcripts - they are your governance demo for the a4 audience at work.
- Merge the gated report flow into datadesk proper as a fourth route ("report") off the b5 classifier. Confirm stats questions still flow ungated - selective gating is the design, not blanket gating.
- Make the gate dynamic (self-study 1c): move the pause INSIDE save_report with
interrupt(...), but only when the path already exists (an overwrite). New files save freely; overwrites ask. Resume withCommand(resume=...). - Run one bisect drill on any old thread: replay from three successively earlier checkpoints and write one sentence on where the answer changed. Speed matters less than doing it once before an incident makes you.
- Optional reading: the HITL + time-travel pages on docs.langchain.com, and b4's HumanInTheLoopMiddleware notes - now you can see the same checkpoint machinery under both.
Official sources covered
This track teaches from the official docs and the free LangChain Academy curricula (login required for lesson content; certificates stay with the Academy - all free). This page covers:
Three questions before you go 🎯 ◐ 90 seconds
1 · Which actions deserve an approval gate?
Blanket gating trains reviewers to rubber-stamp; no gating is the Replit incident. The rule is economic: gate where mistakes cost more than waiting does.
2 · A run is paused at interrupt_before=["save_report"]. What is the resume-as-approved idiom?
invoke(None, config) means "continue exactly where the thread paused". Editing first is update_state then the same call; rejecting is simply never making it.
3 · Forking a thread from a past checkpoint...
Git for conversations: update_state on a past checkpoint's config returns a branch config. Two futures, one preserved past - the debugging superpower of the session.