Executive Briefing: A corporate treasury agent processes customer refund requests and vendor disbursements. Low-risk actions (checking invoices, reading order history) execute autonomously. High-risk disbursements ($> $500$) must pause graph execution using LangGraph's functional
interrupt(), persist thread state in PostgreSQL, await human operator review via an admin dashboard or CLI, and resume execution with either approval, rejection, or modified dollar amounts viaCommand(resume=...).
1. Enterprise Scenario & Requirements
Modern LangGraph v0.2+ replaces legacy exception-based interruptions with the functional interrupt() primitive:
- Tool-Level Interrupt: Build a
process_disbursementtool that checks transfer amount. If amount $> $500$, triggerinterrupt({"action": "disbursement", "amount": amount, "recipient": recipient}). - Persistent Checkpointing: Compile the graph with
PostgresSaver(orMemorySaverfor local tests) scoped bythread_id. - Suspended State Inspection: Demonstrate capturing the suspended state, reading the pending approval payload from outside the graph, and simulating human review.
- Resumption with Command: Resume execution by passing
Command(resume={"approved": True, "edited_amount": 450})tograph.stream().
2. Explicit Deliverables
disbursement_agent.py: Compiled LangGraph workflow with functionalinterrupt()trigger and checkpointer.admin_review_cli.py: Interactive CLI tool that lists paused threads, displays pending transaction details, and allows the operator to Approve, Reject, or Edit.test_hitl.py: Automated test suite verifying: (a) sub-$500 transactions execute without pausing, (b) large transactions pause cleanly at the interrupt node, (c) approval resumes and completes the transfer, and (d) rejection halts and notifies the customer.session_replay.log: Complete log of an interrupted session being resumed and completed.
3. Starter Code & Scaffold
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command
from typing import TypedDict, Annotated
import operator
class TreasuryState(TypedDict):
messages: Annotated[list, operator.add]
approved: bool
def disbursement_node(state: TreasuryState):
amount = 1250.0 # High stakes transaction
# Functional interrupt halts execution and passes payload to external caller
approval_response = interrupt({
"task": "authorize_disbursement",
"amount": amount,
"recipient": "Vendor-AcmeCorp"
})
if approval_response.get("approved"):
final_amt = approval_response.get("edited_amount", amount)
return {"approved": True, "messages": [f"Disbursed ${final_amt} successfully."]}
return {"approved": False, "messages": ["Disbursement rejected by compliance officer."]}
4. 100-Point Grading Rubric
| Criterion | Points | Excellent (25 pts) | Acceptable (15 pts) | Needs Work (5 pts) |
|---|---|---|---|---|
| Functional Interrupt Implementation | 25 pts | Uses modern interrupt() inside node/tool rather than deprecated NodeInterrupt exceptions. | Interruption works but uses legacy compile-time flags. | Uses manual input() prompt that blocks thread execution. |
| Checkpointer & Thread Scoping | 25 pts | Compiles graph with checkpointer; passes configurable={'thread_id': ...} on every call. | Checkpointer configured but thread IDs are hardcoded. | No checkpointer; state lost upon interruption. |
| Dynamic Resumption via Command | 25 pts | Resumes cleanly using Command(resume=...) and handles both approval and mid-flight edits. | Resumes on approval, but cannot accept edited payloads. | Fails to resume; requires restarting workflow from scratch. |
| Security & Compliance Policy | 25 pts | Read actions never blocked; all high-risk financial debits strictly require affirmative review. | Threshold logic works but allows zero-dollar bypass. | Arbitrary actions blocked without clear risk tiers. |