Skip to main content

A Testimony Record from a LangGraph human-in-the-loop flow

One file, LangGraph as its only dependency, and nothing from OMEM.

pip install testimony-langgraph
rec.approve(graph, config,
            approver={"id": "sam@example.com", "kind": "human"},
            identity_source="auth-session")
rec.write("record.jsonl")
testimony-validate record.jsonl
# Conformance: TR-4

The reference validator ships with the package, so a record can be checked by whoever is holding it without cloning anything. It is one standard-library file with no network access, and it is the same file this repository uses, copied in at build time rather than forked.

What problem this solves

LangGraph pauses a graph with interrupt() and resumes it with Command(resume=value). The resume payload says which interrupt it answers and carries a value. It has no field for who answered, and no principal is modelled on that boundary, so any code holding the thread can resume it, including the process that raised the pause.

That is not a defect in LangGraph. It is a graph and checkpointing library and it never claimed to be an authorisation layer. But the consequence is that an approval flow built on interrupt() produces, by default, a record in which a run where an engineer read the arguments and decided is indistinguishable from one where a script resumed everything automatically.

This is measured rather than asserted. A September 2026 assessment of eight agent memory and agent framework implementations (10.5281/zenodo.22290922) examined the six that take or gate actions. Against the question "does an approval identify a person or a named role holder", four were assessed absent and one could not be established either way. LangGraph 1.2.11 was one of the four, with the evidence pinned to libs/langgraph/langgraph/types.py.

What this adapter does not do

It does not invent an approver. The identity does not exist anywhere in LangGraph for it to find. What it does instead is refuse to write an approval unless you supply an identity from your own authentication layer, and make the omission visible rather than silent: resume the graph without going through approve() or refuse() and the record still says exactly what happened, it simply does not reach TR-3.

Three things it refuses outright, each of them a way to produce a plausible file that lies:

refusal why
an action absent from your risk table a default risk class is a guess wearing a policy's clothes
identity_source the model could have written request-body, model, prompt, plan, agent, or empty
approver id equal to proposer id an agent's own credential signing off its own action satisfies every other requirement and is worth nothing

It also refuses to write a record while an action is still awaiting a verdict, because that file would describe a gate that never closed.

Usage

Your node names the action in the interrupt payload. That name is what the risk table is keyed on, and the table belongs to you rather than to the model:

def gate(state):
    ok = interrupt({"action": "issue_refund",
                    "args": {"ticket": state["ticket"], "amount": state["amount"]}})
    return {"refunded": bool(ok)}
from testimony_langgraph import Recorder

rec = Recorder(
    agent={"id": "support-agent", "kind": "agent"},
    risk={"issue_refund": "high", "send_receipt": "low"},
    risk_source="registry",
)

ev = rec.cite("api", "billing://orders/8812", digest="sha256:...")
rec.believe("customer:acme", "eligible_for_refund", evidence=[ev])

rec.invoke(graph, {"ticket": 41}, config)      # runs until the interrupt

rec.approve(graph, config,
            approver={"id": "troy@example.com", "kind": "human"},
            identity_source="auth-session")    # from YOUR auth, not the payload

rec.write("record.jsonl")

Refusing is recorded with the same standing as permitting, because a system that only records what it did is a receipt:

rec.refuse(graph, config, reason="amount exceeds desk limit",
           approver={"id": "troy@example.com", "kind": "human"},
           identity_source="auth-session")

rec.warnings() says out loud what a record will not demonstrate, before you find out from a validator.

What the levels mean here

  • TR-1 the record is well formed and append-only
  • TR-2 beliefs cite their evidence, or say there is none
  • TR-3 actions are gated, and approvals name a person from authentication
  • TR-4 an integrity digest covers the record

The example reaches TR-4. Delete the approve() call and resume the graph directly, the way an application does today, and it stops below TR-3. That difference is the entire contribution of this file.

On the integrity entry

The digest covers every entry written before it, so any later alteration of the file is detectable by anyone holding the value. It does not prove the file was not rewritten wholesale by whoever produced it. That needs an external anchor, and this scheme does not claim to be one. See the security considerations in the specification.

Specification

The point of the adapter is that you can read it in one sitting, copy it, and change it. If it is wrong about what LangGraph carries across the resume boundary, the fix is a pull request and the assessment above should be corrected too.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

testimony_langgraph-0.1.0.tar.gz (17.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

testimony_langgraph-0.1.0-py3-none-any.whl (16.9 kB view details)

Uploaded Python 3

File details

Details for the file testimony_langgraph-0.1.0.tar.gz.

File metadata

  • Download URL: testimony_langgraph-0.1.0.tar.gz
  • Upload date:
  • Size: 17.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for testimony_langgraph-0.1.0.tar.gz
Algorithm Hash digest
SHA256 db9c83060ceb046633cd24c74befe53dbe55b15e697eb511270b7d59851e1f4a
MD5 b8bc6798369d4841091a6b51862e165b
BLAKE2b-256 2fc32a974f1fc0e1821f81e766da11665069de086a253c491e1d333f1d9365be

See more details on using hashes here.

Provenance

The following attestation bundles were made for testimony_langgraph-0.1.0.tar.gz:

Publisher: release-adapter.yml on troybrandonc-bit/machine-testimony

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file testimony_langgraph-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for testimony_langgraph-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f947c64debc2ba757944a6a29cd0ed8ab6b31981554256a5f2f0ed3894a65585
MD5 e8c78eba3c7880efcac24a2b3c3418d1
BLAKE2b-256 1e6fe8db576f632cab2d35ddf30e2c0fa9e3232957f0bdb4394506c0952f0d84

See more details on using hashes here.

Provenance

The following attestation bundles were made for testimony_langgraph-0.1.0-py3-none-any.whl:

Publisher: release-adapter.yml on troybrandonc-bit/machine-testimony

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page