Adapters
An adapter plugs into an agent framework’s own hooks and writes what the agent does into a Tenbri stream. Each tool call becomes a decision: an intent that is durable before the tool runs, then an outcome once it has run. Approvals given through the framework’s own flow are recorded too.
Three lines to instrument your agent
Section titled “Three lines to instrument your agent”Install the adapter, open the stream, instrument the framework. Here with the Claude Agent SDK; the other frameworks differ only in the package and the function name.
pip install --pre tenbri-claude-agent-sdkimport tenbri
tenbri.init() # the stream, from TENBRI_STREAMtenbri.instrument_claude_agent_sdk() # every session from here on is recorded
# ... your agent, unchangednpm install @tenbri/claude-agent-sdk@next tenbri@next @anthropic-ai/claude-agent-sdkimport { init } from 'tenbri'import { instrumentClaudeAgentSdk } from '@tenbri/claude-agent-sdk'
init() // the stream, from TENBRI_STREAMconst options = instrumentClaudeAgentSdk({ /* your options, unchanged */ })export TENBRI_STREAM=./audit TENBRI_AGENT=agent:support TENBRI_APPROVER=person:aliceinit() creates the stream and its signing keys on first use and flushes at exit. It reads its
settings from the environment; an argument to init() wins over its variable:
| Variable | What it is |
|---|---|
TENBRI_STREAM |
Where entries go: a directory, file://..., s3://bucket/prefix or r2://bucket/prefix. Required. |
TENBRI_KEYS |
The keys directory, apart from the stream. Default ~/.config/tenbri/keys. |
TENBRI_AGENT |
Who acts, when the framework does not say (the OpenAI Agents SDK and LangGraph use the agent’s name). |
TENBRI_APPROVER |
Who answers the framework’s approval prompts: person:alice. Without one, approvals write no entry. |
TENBRI_SCHEMA |
The action schemas. Default ./tenbri.toml when there is one. |
Pick your framework
Section titled “Pick your framework”| Framework | Python package | Python | npm package | TypeScript |
|---|---|---|---|---|
| Claude Agent SDK | tenbri-claude-agent-sdk |
tenbri.instrument_claude_agent_sdk() |
@tenbri/claude-agent-sdk |
instrumentClaudeAgentSdk(options) |
| OpenAI Agents SDK | tenbri-openai-agents |
tenbri.instrument_openai_agents() |
@tenbri/openai-agents |
instrumentOpenAIAgents() |
| LangGraph and LangChain | tenbri-langgraph |
tenbri.instrument_langchain() |
@tenbri/langgraph |
instrumentLangChain() |
Each adapter is released with the tenbri binding, at the same version (0.1.0-beta.2). While
Tenbri is in pre-release, install with pip install --pre and the @next npm tag. Each one is
tested in CI with no model and no API key: the Claude adapter drives the real SDK against a
stand-in for the Claude Code CLI, and the OpenAI and LangGraph adapters run their framework with a
scripted model. Each page shows how to do the same in your own tests.
Map tool calls to actions
Section titled “Map tool calls to actions”A tool call is recorded as the action of the tool’s name. Say more about it in tenbri.toml, the
action schemas tenbri schema check and tenbrid already validate entries against:
[actions.issue_refund]target = "stripe:{charge}" # {charge} is filled from the tool call's argumentspolicy = "policy:refund@14" # the rule that allowed it: ref@version- A tool matches an action by its name, or by its last
__part: the MCP toolmcp__billing__issue_refundisissue_refund. - Each
{argument}intargetis filled from the call’s arguments ({order.id}reads a nested field), so a call with{"charge": "ch_123"}is recorded with the targetstripe:ch_123. When validating, a placeholder matches like*. policybecomes the intent’srule:{"ref": "policy:refund", "version": "14", "result": "allow"}. When validating, an intent of that action must carry that rule.- A plain dict (object) in code says the same and wins over the file, per tool name:
tenbri.instrument_openai_agents(actions={"issue_refund": {"target": "stripe:{charge}"}}). It can also rename the action ("action": "close_ticket") and take functions of the arguments.
tenbri.toml rejects action types it does not declare by default, so tenbri schema check flags
the entries of tools you left out. Declare them, or set unknown_actions = "warn" at the top of
the file.
Why an adapter
Section titled “Why an adapter”You could call intent and outcome around every tool yourself, and for a framework without an
adapter @tenbri.decision does it for you (here is how).
An adapter does it at the one place every tool call passes through, so a new tool is recorded the
day it is added, approvals are tied to the calls they allowed, and nothing in your tools changes.
What gets recorded
Section titled “What gets recorded”Every adapter follows the same mapping:
| Agent event | Tenbri entry |
|---|---|
| A tool call is about to run | An intent decision. principal is the agent, action.type is the action, action.target and rule come from the mapping. |
| The tool returned | The outcome with status ok |
| The tool raised or failed | The outcome with status error |
| A person or a rule denied the call | The outcome with status refused |
| The run was interrupted mid-tool, or the process died | unknown: from the adapter on interrupt, or from the next writer that reconciles the open intent |
| A person approved the call | An approval entry by the approver; the decision names it in its approval member (approvals) |
| A handoff to another agent (OpenAI Agents SDK) | An intent and an ok outcome, action handoff, target agent:<next agent> |
How each framework’s events map:
| Tenbri entry | Claude Agent SDK | OpenAI Agents SDK | LangGraph and LangChain |
|---|---|---|---|
intent |
PreToolUse hook |
on_tool_start (Python), a tenbri tool input guardrail (TypeScript) |
on_tool_start / handleToolStart callback |
outcome ok |
PostToolUse |
the tool returned | on_tool_end / handleToolEnd |
outcome error |
PostToolUseFailure |
the tool raised or timed out | the tool raised, or returned an error ToolMessage |
outcome refused |
can_use_tool denied, or PermissionDenied |
a guardrail rejected the call, or RunState.reject(item) |
the tool raised tenbri.Refused, or a person rejected the interrupt in Command(resume=...) |
outcome unknown |
PostToolUseFailure with an interrupt |
the run was cancelled (Python) | a LangGraph interrupt inside the tool |
approval |
can_use_tool allowed, with an approver |
RunState.approve(item) |
a person approved or edited the interrupt in Command(resume=...) |
The approver is TENBRI_APPROVER, or the approver option: a principal, or a function of the
framework’s call returning one. See Streams and entries for
the entry kinds.
Guarantees
Section titled “Guarantees”- Fail closed. If the intent cannot be written, the tool does not run.
init(fail_open=True)(failOpen), or the adapter’s ownfail_openoption, lets the tool run unrecorded, with a warning. - No prompts stored. Adapters never write prompts, tool arguments or tool results. The mapping
decides what goes into
action.target. - Keyed digests. The Claude and LangGraph adapters pin each call’s input by its keyed digest
(an entry in
inputs), keyed by the stream’s local key store, per subject: no secret in your code. Whoever holds the content and the key can prove what the agent used; nobody can guess it from the log, and erasing a subject’s key leaves its digests unconfirmable. - Trace ids. Entries carry the active trace id when there is one, so a decision and its trace find each other.
