This release is a pre-release and may not be stable for production use.
tenbri-langgraph
Tenbri for LangGraph and LangChain: every tool call your agent makes is a signed, chained decision.
pip install --pre tenbri-langgraph
import tenbri
tenbri.init() # TENBRI_STREAM=./audit/support TENBRI_APPROVER=person:alice
tenbri.instrument_langchain()
From then on every tool call of every LangChain agent and LangGraph graph in the process is
recorded, in every thread, with no callbacks to pass. The principal is the agent's name
(create_agent(name=...)), else TENBRI_AGENT.
tenbri.init() creates the stream and its keys on first use and flushes at exit; see the
tenbri package for its settings and @tenbri.decision for your own
functions.
Which action, which target, which policy
A tool call is the action of the tool's name. Declare the action in tenbri.toml (the action
schemas tenbrid and tenbri schema check validate against) and the record takes its target and
rule from there, filled from the tool's arguments:
[actions.issue_refund]
target = "stripe:{charge}" # each {argument} is filled from the call; validated as *
policy = "policy:refund@14" # the rule that allowed it: ref@version
init() reads ./tenbri.toml (or TENBRI_SCHEMA). A tool matches an action by its name or its
last __ part, so the MCP tool mcp__billing__issue_refund is issue_refund. A plain dict in
code says the same, and wins:
tenbri.instrument_langchain(
actions={"issue_refund": {"target": "stripe:{charge}", "policy": "policy:refund@14"}}
)
Human in the loop
With HumanInTheLoopMiddleware (and a checkpointer), resume the graph the LangGraph way; the
answers in the Command are recorded as it starts:
config = {"configurable": {"thread_id": "t1"}}
agent.invoke({"messages": [("user", "refund ch_123")]}, config) # paused for review
agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config)
approve and edit write an approval entry by TENBRI_APPROVER (or init(approver=...), or
instrument_langchain(approver=...): a principal, or a function of the tool and its arguments),
and the tool's intent names it. reject writes the call's intent and a refused outcome.
Advanced: TenbriCallbackHandler
The one-liner is built on TenbriCallbackHandler, a handler on a stream you opened, passed in
callbacks, with resume() recording the answers for a person you name. What it records,
below, is what the one-liner records.
from langchain.agents import create_agent
from tenbri import Tenbri
from tenbri_langgraph import TenbriCallbackHandler
log = Tenbri.open("./audit/support", create=True)
tenbri = TenbriCallbackHandler(log, target=lambda tool, args: args.get("charge"))
agent = create_agent(model, tools=[issue_refund], name="support")
agent.invoke({"messages": [("user", "refund ch_123")]}, {"callbacks": [tenbri]})
print(log.verify(log.fingerprint).ok)
- Before each tool call its intent is durable in the stream, or the tool does not run (the
handler has
raise_error=True). After it comes the outcome:ok,error, orrefusedwhen the tool raisestenbri.Refused. LangGraph'sToolNodere-raises a tool's exception unlesshandle_tool_errorsis set, so a refusal, or an intent that could not be written, ends the run with that exception. A process that dies mid-call leaves the intent open, and the stream's next writer reconciles it asunknown. - Who and what:
principalis the agent (create_agent(name=...), orprincipal=),actionis the tool,targetis whattarget(tool, args)returns. - Inputs: the call's arguments are pinned as a keyed digest
(
langchain:tool_call/<id>@args);inputs(tool, args)pins more, such as a policy version. Arguments and results are never stored. - Selection:
tools=["issue_refund"]or a predicate records only those tools. - Traces: the active OpenTelemetry span goes into
trace(installtenbri-langgraph[otel]); the LangChain run id, the tool call id and the LangGraph thread id go intoext.langchain, so a LangSmith run and a decision find each other. - Fail closed by default: when the stream can't be written, the tool call fails.
fail_open=Trueruns the tool anyway and logs a warning.
Human in the loop
With LangChain's HumanInTheLoopMiddleware (and a checkpointer), record the person's answer
as you resume the graph:
config = {"callbacks": [tenbri], "configurable": {"thread_id": "t1"}}
agent.invoke({"messages": [("user", "refund ch_123")]}, config) # paused for review
agent.invoke(tenbri.resume(agent, config, {"decisions": [{"type": "approve"}]}, by="person:alice"), config)
approve and edit write an approval entry, and the tool's intent names it
(approval: {by, entry}), so the stream answers "who approved this, and on what basis".
reject writes the call's intent and a refused outcome. For an interrupt() of your own, call
tenbri.approve(tool, args, by=..., tool_call_id=...) once the person has answered.
A runnable example is in examples/refund_agent.py.
Metadata
Release files for tenbri-langgraph 0.1.0b2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| tenbri_langgraph-0.1.0b2.tar.gz | 13.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tenbri_langgraph-0.1.0b2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 22.8 kB
Release files / tenbri_langgraph-0.1.0b2.tar.gz
| Download URL | tenbri_langgraph-0.1.0b2.tar.gz |
|---|---|
| Size | 13.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2b994f8a9561f5a53663e556a7e58dc6e782dd636d2ad0d6beba637d1565d68c
|
|
BLAKE2b-256 checksum How to use checksums |
640871c1cf9358cb3b36e10ddcd527290619a23c6d369f086fa330044e307af1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logRelease files / tenbri_langgraph-0.1.0b2-py3-none-any.whl
| Download URL | tenbri_langgraph-0.1.0b2-py3-none-any.whl |
|---|---|
| Size | 9.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a659e7ebdf1999b0f90fb09b3040f19c798532a42b9fbe104a0c7dc815121689
|
|
BLAKE2b-256 checksum How to use checksums |
609d992901ae9c2ecb0e76fba58c9983035f51684f16b5f85eb551412dd19b2c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logSecurity for tenbri-langgraph 0.1.0b2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for tenbri_langgraph-0.1.0b2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.