“Save state” is not enough. First decide what state means.
Four categories
Conversation
What users/models said.
Model: I found pricing information.
Useful, but not automatically authoritative.
Execution state
What the runtime believes about progress.
current requirement = pricing
status = researching
pages fetched = 7
Artifacts
Useful objects produced by the run.
fetched page
evidence record
verification result
draft report
External reality
What actually exists outside the runtime.
Did the report get published?
Did the refund happen?
Did the ticket get created?
These can disagree.
Model: “I published it.”
Application: published = true
External system: no report exists
The external system is authoritative about that external effect.
State is a model of reality
A field such as:
state["published"] = True
is still only an application claim.
Good state design makes incorrect claims hard to represent.
Structured state
class Evidence(TypedDict):
evidence_id: str
requirement: str
url: str
content_hash: str
quote: str
retrieved_at: str
class RequirementState(TypedDict):
status: str
finding: str | None
evidence_ids: list[str]
class VendorReviewState(TypedDict):
run_id: str
vendor_name: str
vendor_url: str
requirements: dict[str, RequirementState]
discovered_urls: list[str]
visited_urls: list[str]
evidence: list[Evidence]
contradictions: list[dict]
errors: list[dict]
status: str
terminal_reason: str | None
pages_fetched: int
cost_usd: float
schema_version: int
code_version: str
Why structure matters
Weak:
“Pricing seems complete.”
Checkable:
{
"status": "verified",
"finding": "No public list price is available.",
"evidence_ids": ["ev_91"]
}
Introduce invariants
An invariant is a property that must remain true even when expected failures occur.
Example:
IF requirement.status == VERIFIED
THEN at least one evidence ID must resolve to stored evidence.
Code:
def validate_state(state):
for name, req in state["requirements"].items():
if req["status"] == "verified" and not req["evidence_ids"]:
raise StateInvariantError(
f"{name} verified without evidence"
)
This is stronger than asking the model to “remember to cite sources.”
State transitions
Do not allow arbitrary jumps.
PENDING
↓
RESEARCHING
↓
CANDIDATE_FOUND
↓
VERIFYING
├────────→ VERIFIED
├────────→ UNKNOWN
└────────→ NEEDS_MORE_RESEARCH
A finding is not the same as a verified finding.
First persistence
Before LangGraph, persist the state after meaningful transitions.
Conceptually:
transition(state)
save_state(state)
continue_work()
Kill the process.
Reload the saved state.
Now the application can recover the facts of the run.
But questions remain:
Which function should execute next?
What if a process dies halfway through a function?
Which operations can run again?
Who discovers unfinished work?
Those are durable-execution questions.
Schema evolution
A run may outlive a deployment.
Persist:
schema_version
code_version
When state changes, explicitly classify old runs as:
compatible
requires migration
cannot safely resume
FAILURE LAB 02: False Completion
Inject:
state["requirements"]["pricing"] = {
"status": "verified",
"finding": "Pricing available",
"evidence_ids": [],
}
If the UI/report accepts this, the system has a state-model bug.
Fix the invariant in:
- the transition into
verified; - an independent report/evaluation check.
Prove it
A test must fail when verified state has no resolvable evidence.
Check your understanding
Answer before moving on. If one is fuzzy, the relevant section is a scroll away.
- What is the difference between conversation and execution state?
- Why is external reality not identical to application state?
- What is an invariant?
- Why model progress as transitions?
- Why does state schema version matter?
Exit criteria
Observable conditions, not “I understand it”. Check them off; progress is saved in your browser.
- VendorReviewState persists to disk after every step and a fresh process can load it
- Reducers are idempotent: the double-apply test passes
- The operational questions are answerable from state alone, with the process dead
- no_false_completion exists as code and passes on both fixture vendors