Skip to content
Tenbri pangolinTenbri

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.

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.

Terminal window
pip install --pre tenbri-claude-agent-sdk
agent.py
import tenbri
tenbri.init() # the stream, from TENBRI_STREAM
tenbri.instrument_claude_agent_sdk() # every session from here on is recorded
# ... your agent, unchanged
Terminal window
export TENBRI_STREAM=./audit TENBRI_AGENT=agent:support TENBRI_APPROVER=person:alice

init() 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.
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.

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:

tenbri.toml
[actions.issue_refund]
target = "stripe:{charge}" # {charge} is filled from the tool call's arguments
policy = "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 tool mcp__billing__issue_refund is issue_refund.
  • Each {argument} in target is filled from the call’s arguments ({order.id} reads a nested field), so a call with {"charge": "ch_123"} is recorded with the target stripe:ch_123. When validating, a placeholder matches like *.
  • policy becomes the intent’s rule: {"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.

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.

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.

  • Fail closed. If the intent cannot be written, the tool does not run. init(fail_open=True) (failOpen), or the adapter’s own fail_open option, 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.