What LangGraph records, and what it does not

Read at81bf17b
Repositorygithub.com/langchain-ai/langgraph
LicenceMIT
Assessed asstores, acts
Reachesno level yet
Read byTroy Brandon Clifford
LevelMeetsWhat the level asks
TR-1 Recorded4/5The record exists and is append-only.
TR-2 Explained0/4Every belief resolves to its evidence, and disagreements survive.
TR-3 Gated2/7Actions carry a verdict, and approvals carry a name.
TR-4 Verifiable0/3The record can be shown not to have changed.

A count is requirements fully met out of those that apply. This system is assessed as stores, acts, and requirements outside that are not counted against it.

LangGraph is assessed as storing and acting but not deriving. It persists state the graph computed and executes nodes with effects, but the framework itself makes no claims about the world beyond what its user's nodes put into a channel, so scoring it on the told-versus-inferred requirement would be scoring the user's code rather than the framework. Its checkpointer is the most append-only structure in this census: every checkpoint is a new row keyed by its own id, carrying parent_checkpoint_id, so the history of a thread is a chain nothing overwrites. The gap is the same one every other system here has, in the same place. interrupt() genuinely pauses before a consequential step, and the value that resumes it is an arbitrary payload with nobody's name on it.

Every verdict, and what it rests on

Twenty requirements, each stated as a capability rather than a format, so a system that holds the information in its own shape counts as having it. An absent verdict cites where the assessor looked and did not find it, which is the difference between a measurement and an accusation.

TR-1 Recorded

The record exists and is append-only.

R1.1 · present. When the system stores a fact, does it durably record when that happened?

R1.2 · present. When a stored fact changes, is the previous version still readable?

R1.3 · present. Does the ordinary write path ever destroy what was previously recorded?

This is what the requirement asks for, arrived at because time travel and resumption need it rather than because a specification asked. The design goal differs and the property is the same.

R1.4 · present. Are entries distinguishable by kind, or is everything one undifferentiated blob of text?

R1.5 · absent. When data must be destroyed for a legal reason, is the destruction itself recorded?

delete_thread is the obvious call to reach for on a subject-erasure request, and it is the one place the otherwise append-only design gives way completely. A row recording the thread id, the time and the requester, with no state in it, would close this without retaining anything that was erased.

TR-2 Explained

Every belief resolves to its evidence, and disagreements survive.

R2.1 · partial. Can the source a stored fact came from be recovered from the store, by following a link rather than by guessing?

Provenance within the run is genuinely good: which node wrote which channel at which step is recoverable. Provenance beyond the run is out of scope for the framework, so a value that came from a customer email is just a string in a channel.

R2.3 · partial. When two stored facts about the same proposition disagree, do both survive?

Both survive as history because nothing is deleted, which is more than most systems here manage. Neither survives as a live claim: reading the current state gives the winner, and finding the loser means walking back through checkpoints.

R2.4 · absent. Is the disagreement itself queryable, or must a reader diff rows to notice it?

  • searched grep -rniE 'class Conflict|contradict|disagree' libs/langgraph/langgraph/ libs/checkpoint/ --include=*.py
    no conflict type, record or query surface. The framework has channels and reducers, not propositions, so there is nothing that could be in conflict

R2.5 · partial. When a conflict is resolved, does the record say who resolved it and by what method?

A channel reducer resolving two writes is a resolution of sorts, decided by developer code, and that is a reasonable place for the decision to live. What is missing is attribution on the manual path: 'update' says a person intervened without saying which person.

  • source libs/checkpoint/langgraph/checkpoint/base/__init__.py:42-48
    a checkpoint created by a manual state update is labelled source 'update', so the fact that a value was overridden by hand is on the record
  • searched grep -rniE 'approver|approved_by|principal|user_id' libs/langgraph/langgraph/types.py
    no match; the checkpoint records that an update happened and never who made it

R2.2 is not assessed here, because this system is not in that business and marking it down for that would be dishonest.

TR-3 Gated

Actions carry a verdict, and approvals carry a name.

R3.1 · present. Does a consequential action produce a durable entry whether or not it ran?

The strongest answer to this requirement in the census. An action that was prepared and never ran is in durable storage rather than in memory, because resumption depends on it being there.

R3.2 · present. Does an action's risk class come from somewhere the proposing model cannot write to?

Binary rather than graded, like the other frameworks here, and outside model control, which is the substance of the requirement.

R3.3 · partial. Are refusals recorded as faithfully as permissions?

A refusal is durably recorded, because everything is, but only as a state change. Nothing distinguishes 'the human said no' from any other value arriving in a channel.

R3.4 · absent. Does a refusal record why it was refused?

  • source libs/langgraph/langgraph/types.py:823
    resume is typed as dict[str, Any] | Any | None, so a reason can be passed but has no defined place and no defined shape
  • searched grep -rniE 'reason|rejection' libs/langgraph/langgraph/types.py
    no field named reason or rejection exists on Command or on the interrupt types; any reason an application supplies is an opaque value the framework does not model

R3.5 · absent. Does an approval identify a person or a named role holder?

  • source libs/langgraph/langgraph/types.py:799-823 Command
    the resume payload identifies which interrupt it answers and carries a value; there is no field for who answered
  • searched grep -rniE 'approver|approved_by|principal|user_id' libs/langgraph/langgraph/types.py
    no match anywhere in the types module that defines the human-in-the-loop surface

R3.6 · absent. Does the approver's identity come from the authentication layer rather than from something the model can write?

  • searched grep -rniE 'approver|approved_by|principal|user_id' libs/langgraph/langgraph/types.py
    no identity is captured at the resume boundary, so none can come from an authentication layer
  • source libs/langgraph/langgraph/types.py:823
    resume is an arbitrary value supplied by the caller, which is exactly the request-body-shaped input the specification warns against treating as identity

R3.7 · absent. Is the acting agent prevented from approving its own action?

  • source libs/langgraph/langgraph/types.py:799-823
    any code holding the thread can send a Command(resume=...), including the same process that raised the interrupt, so a graph can resume its own pause without a person
  • searched grep -rniE 'approver|approved_by|principal|user_id' libs/langgraph/langgraph/types.py
    with no principals modelled, a proposer and an approver cannot be told apart

TR-4 Verifiable

The record can be shown not to have changed.

R4.1 · absent. Does the system publish a scheme under which the record's past state can be verified?

parent_checkpoint_id makes the history a chain in structure, which is most of the shape of a hash chain and none of its force: the links are ids rather than digests, so they order the record without binding it.

  • searched grep -rniE 'hash_chain|merkle|tamper|checksum|signature' libs/checkpoint/ libs/langgraph/langgraph/ --include=*.py
    matches are Python introspection only (inspect.signature, function signature docs); no integrity scheme is defined or published

R4.2 · absent. Can an independent party run that verification without the vendor's cooperation, and without the operator's?

  • searched grep -rniE 'def verify|verification' libs/checkpoint/ --include=*.py
    no verification surface exists, so there is nothing for an independent party to run

R4.3 · absent. Would alteration of a past entry be detectable after the fact?

This is the closest any assessed system comes to being one change away from TR-4 alone: a digest of each checkpoint that included its parent's digest would make the existing chain tamper-evident without altering the write path.

How this was read

Read of libs/checkpoint/langgraph/checkpoint/base/__init__.py, libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py and libs/langgraph/langgraph/types.py at 81bf17b, cloned from the public repository. Line numbers are that commit's.

Found an error? Challenge a finding

Then it is wrong in the ordinary way readings are wrong, and every verdict cites a file and a line at a pinned commit precisely so that being wrong is cheap to demonstrate. The remedy is a pull request against the subject file, and it does not involve persuading anybody. Nobody applied for this and it is not a certification.

The standing register carries every system side by side, and the rubric is the twenty requirements in full, free to apply to anything, including to this assessment.