Agent State Machine
The LangGraph StateGraph for the medication-adherence agent. The agent
is a short, mostly-linear pipeline of graph nodes, not a multi-state
conversational machine: one /chat turn flows through it once.
The default build has six nodes:
intake -> guardrail_pre -> [retrieve_context] -> generate_response -> guardrail_post -> closing. retrieve_context exists only when both a
vector store and an embedder are injected (the RAG path); with neither,
the graph degrades to a three-node shape (intake -> guardrail_pre -> generate_response -> guardrail_post -> closing, retrieval absent).
Two real branch points exist:
- A conditional edge after
guardrail_preskipsretrieve_contextand routes straight togenerate_responsewheneverguardrail_prealready attached a failing pre-guardrail decision (input-validation, scope, or escalation). The rejected turn would never use retrieved context, so embedding it is skipped (no wasted billable embedder call). generate_responseshort-circuits the LLM call to a deterministic template on three conditions: a failing escalation decision (emits the locale-aware escalation template), a failing input-validation / scope decision (emits a locale-aware refusal), or arefusal-on-no-matchdecision fromretrieve_context(emits the no-context refusal). These are in-node branches, not graph edges; a short-circuited turn still flows through every downstream node.
When the graph is built with HITL enabled, a review_response node is
inserted between generate_response and guardrail_post. It calls
assess_review_need (a deterministic, total pure function over guardrail
decisions); when a high-risk-but-not-acute draft is detected (unverified
citation, missing citation on a RAG turn, or persona drift) it calls
LangGraph interrupt() to pause the graph for human sign-off. The
/chat handler returns status="paused_for_review"; the human resumes
through POST /chat/resume, which delivers a Command(resume=...) that
re-enters review_response and applies the decision (approve / edit /
reject). A clear verdict makes the node a no-op and the graph flows on
exactly as the six-node graph does. Acute red flags never reach this
pause: they are short-circuited upstream in guardrail_pre.
See ADR-0001 for the orchestration rationale, c4-component.md for the node-and-module decomposition, and request-sequence.md for the single-turn interaction flow.
The Agent Execution Graph in the demo single-page app (ADR-0009) is a live, in-browser visualization of this same topology: it draws the real compiled node set and edges shown below and lights each node as a turn streams. The streaming emission path and visualization did not change the agent graph, so this diagram remains the authoritative topology reference and the live graph must match it.
Node graph (default build)
Section titled “Node graph (default build)”stateDiagram-v2
[*] --> intake
intake --> guardrail_pre
state "intake" as intake
state "guardrail_pre" as guardrail_pre
state "retrieve_context (RAG path only)" as retrieve_context
state "generate_response" as generate_response
state "guardrail_post" as guardrail_post
state "closing" as closing
state route_pre <<choice>>
guardrail_pre --> route_pre
route_pre --> retrieve_context : clean turn, RAG enabled
route_pre --> generate_response : pre-guardrail failure (skip retrieval)
retrieve_context --> generate_response
generate_response --> guardrail_post
guardrail_post --> closing
closing --> [*]
note right of guardrail_pre
input-validation, PII redaction,
escalation detection, rule-based
scope, optional judge-backed scope.
A failing decision is carried
forward on the state.
end note
note right of generate_response
In-node short-circuit (no LLM call):
failing escalation -> escalation template,
failing input-validation/scope -> refusal,
refusal-on-no-match -> no-context refusal.
Otherwise calls the LLM and runs the
citation check.
end note
Node graph with HITL enabled
Section titled “Node graph with HITL enabled”stateDiagram-v2
[*] --> intake
intake --> guardrail_pre
state "intake" as intake
state "guardrail_pre" as guardrail_pre
state "retrieve_context (RAG path only)" as retrieve_context
state "generate_response" as generate_response
state "review_response (interrupt HITL)" as review_response
state "guardrail_post" as guardrail_post
state "closing" as closing
state route_pre <<choice>>
guardrail_pre --> route_pre
route_pre --> retrieve_context : clean turn, RAG enabled
route_pre --> generate_response : pre-guardrail failure (skip retrieval)
retrieve_context --> generate_response
generate_response --> review_response
review_response --> guardrail_post
guardrail_post --> closing
closing --> [*]
note right of review_response
assess_review_need(state):
clear verdict -> no-op, flow continues.
needs-review (unverified citation,
missing citation, persona drift) ->
interrupt() pauses the graph.
/chat returns paused_for_review;
/chat/resume sends Command(resume=...)
re-entering this node to apply
approve / edit / reject.
end note