The human gate is a node in the graph, not a comment in the code.
A LangGraph state machine triages a CPA's inbox: classify, fetch grounded context, draft, then stop. The awaiting_approval node calls interrupt(), the checkpointer serializes the whole run, and nothing sends until a human resumes the thread. This page is generated from the artifacts: the topology is parsed out of src/lib/agent.ts, every row and quote comes from the committed promptfoo run evals/reports/eval.json.
the state machine, drawn from agent.ts
8 nodes, 3 conditional routers. One distinction worth naming: propose_slot and escalate_human set the state fieldtask_state = 'awaiting_approval' and then edge straight to END; only the reply path and the rag-refusal path cross the awaiting_approvalnode, which is where interrupt() actually fires.
the eval run: 10/10
10/10 scenarios pass77.1s total latency5 behavior classes
*Cost as recorded by the harness: the eval drives the agent through a custom exec provider, which reports no per-call token cost to promptfoo. The agent logs its own token spend separately.
classify_taskRouted down the default reply path. The graded run confirms the route: tool calls match: [rag].
fetch_contextOne rag call against the labor-law corpus (cs1); the citations it returns are the only sources the draft is allowed to cite.
draft_replyThe drafted reply, quoted from the run's output (1726 chars, truncated):
I only have three citations available in the provided context, and only [2] is directly relevant to your questions. I will answer what I can with that citation and flag where you will need additional verification, rather than invent sources.
awaiting_approvalinterrupt() parks the graph. The run ends here: task_state matches: awaiting_approval, and the tool-call list holds no gmail call. Nothing was sent.
the resume, exercised in the test suite rather than this eval run
The approval leg is covered by approval-gate.test.ts: it resumes the parked thread with new Command({ resume: { decision: 'approved', ... } }), the router sends the reply and the state reaches sent; a rejected decision routes back into draft_reply instead.
Two caveats stated plainly. Checkpoints live in LangGraph's MemorySaver, in process memory; the Postgres checkpointer is the documented deploy swap, and only the constructor changes. And every grading result in this run carries the line "faithfulness rubric gated off (< 30 scenarios)": the faithfulness rubric is deliberately gated off until the scenario set reaches 30, because at n=10 a single wrong citation would read as 90% faithfulness. Repository: backoffice-agent.