Planned adapters
Three adapters ship today: Claude Agent SDK, OpenAI Agents SDK and LangGraph and LangChain. The Tenbri README plans these next for v0.1.0:
| Adapter | Planned as |
|---|---|
| Pydantic AI | Experimental |
| Vercel AI SDK | Experimental |
| Mastra | Experimental |
| Google ADK | Experimental |
| AWS Strands | Experimental |
| Microsoft Agent Framework | Experimental |
| CrewAI | Experimental |
| LlamaIndex | Experimental |
| A2A | Experimental |
Experimental means documented and tested, but free to change in a 0.x minor release. The three shipped adapters are Stable: covered by the format promise and the 0.x API policy.
Use Tenbri from any framework today
Section titled “Use Tenbri from any framework today”An adapter only automates what you can do yourself. Find your tool functions, the ones the agent
calls, and record each call as a decision: @tenbri.decision in Python, decision({...}, fn) in
TypeScript. Each call then writes:
- Before the function runs, an intent:
principalis the agent,action.typethe action,action.targetwhat it acts on. If the intent cannot be written, the function does not run. - After it runs, the outcome:
ok,refusedwhen it raisestenbri.Refused(throwsRefused),errorfor any other exception (which propagates),unknownwhen cancelled (anAbortErrorin TypeScript).
Here is a refund tool, issue_refund, recorded that way. tenbri.init() opens the stream, as for
the adapters (from TENBRI_STREAM and TENBRI_AGENT when you pass nothing), and
tenbri.agent() (agent()) says who acts for a block of code:
import tenbri
tenbri.init("./audit", agent="agent:support") # or TENBRI_STREAM and TENBRI_AGENT
POLICY = "Refunds up to 500 EUR are automatic."
@tenbri.decision( "issue_refund", target="stripe:{charge}", # a template of the arguments policy="policy:refund@14", # the rule that allowed it inputs=[("policy:refund", "14", POLICY)], # what it relied on, by keyed digest)def issue_refund(charge: str, amount: int) -> str: if amount > 500: raise tenbri.Refused("over the refund ceiling") # recorded as refused return f"refunded {amount} EUR of {charge}" # your tool
print(issue_refund("ch_123", 30))with tenbri.agent("agent:billing"): # who acts, for this block try: issue_refund("ch_456", 900) except tenbri.Refused as e: print("refused:", e)
log = tenbri.default()for m in log.query(action="issue_refund"): e = m.entry print(e["seq"], e["phase"], e.get("status", ""), e["principal"]["id"], e["action"]["target"])print("verified:", log.verify(log.fingerprint).ok)python wrap.pyrefunded 30 EUR of ch_123refused: over the refund ceiling3 intent support stripe:ch_1234 outcome ok support stripe:ch_1235 intent billing stripe:ch_4566 outcome refused billing stripe:ch_456verified: TrueThe decorator works the same on async def functions. target and subject are templates of the
function’s arguments ("stripe:{charge}", "{order.id}") or functions of them; inputs can be a
function of the arguments too. Who acts is principal=, else the innermost
with tenbri.agent(...), else init(agent=...) or TENBRI_AGENT: with none, the call raises
InvalidRequestError: no principal.
import { Refused, agent, decision, init } from 'tenbri'
const log = await init({ stream: './audit', agent: 'agent:support' }) // or TENBRI_STREAM and TENBRI_AGENT
const POLICY = 'Refunds up to 500 EUR are automatic.'
const issueRefund = decision( { action: 'issue_refund', target: 'stripe:{charge}', // a template of the first argument policy: 'policy:refund@14', // the rule that allowed it inputs: [{ ref: 'policy:refund', version: '14', content: POLICY }], // what it relied on, by keyed digest }, async ({ charge, amount }: { charge: string; amount: number }) => { if (amount > 500) throw new Refused('over the refund ceiling') // recorded as refused return `refunded ${amount} EUR of ${charge}` // your tool },)
console.log(await issueRefund({ charge: 'ch_123', amount: 30 }))// who acts, for what the callback runsawait agent('agent:billing', () => issueRefund({ charge: 'ch_456', amount: 900 })).catch((e) => console.log('refused:', e.message))
for (const { entry: e } of await log.query({ action: 'issue_refund' })) { console.log(e.seq, e.phase, e.status ?? '', e.principal?.id, e.action?.target)}console.log('verified:', (await log.verify(log.fingerprint!)).ok)node wrap.tsrefunded 30 EUR of ch_123refused: over the refund ceiling3 intent support stripe:ch_1234 outcome ok support stripe:ch_1235 intent billing stripe:ch_4566 outcome refused billing stripe:ch_456verified: truedecision() returns an async function. target and subject are templates of the arguments
('stripe:{charge}' reads the first argument’s charge, '{1}' is the second argument) or
functions of them. Who acts is principal, else the innermost agent(...) scope, else
init({ agent }) or TENBRI_AGENT.
The first intent, as stored (signatures and hashes shortened). The policy’s text is pinned by its keyed digest, keyed by the stream’s local key store, so no secret goes in your code:
{"action":{"target":"stripe:ch_123","type":"issue_refund"},"durability":"local","id":"01M5D1B9FZC4VQKEK17YG8N8SB","inputs":[{"digest":"hmac-sha256:ec9ac64d…","ref":"policy:refund","version":"14"}],"kind":"decision","phase":"intent","prev":"sha256:ab5571a7…","principal":{"id":"support","kind":"agent"},"rule":{"ref":"policy:refund","result":"allow","version":"14"},"seq":3,"sig":"ed25519:d4e3c10e…","signer":"sess:2026-10-20:acfd88f7edf2","source":"sdk","stream":"tnb1:9ad7a1625fbfd405ed5ae0addf924507","ts":"2026-10-20T16:12:10.627Z","v":1}If a person approved the call, write an approval entry first and name it from the decision, as
the adapters do. For full control (several streams, your own drafts), the stream itself has
intent, outcome and append: see the bindings’ READMEs.
From a shell or another language
Section titled “From a shell or another language”Without a binding, write the same intent and outcome with the CLI, or through tenbrid over REST:
For shell-driven agents and scripts. tenbri append reads a draft entry on stdin and prints it as
stored; the script uses jq to read the intent’s id. A draft for the CLI carries every required
field, source included.
#!/bin/sh# The tool call, wrapped by hand: intent before, outcome after.set -eexport TENBRI_KEYS=./keys[ -d ./audit ] || tenbri init ./audit
intent=$(tenbri append ./audit <<JSON{"kind":"decision","phase":"intent","source":"sdk","principal":{"kind":"agent","id":"support"}, "action":{"type":"issue_refund","target":"stripe:ch_123"}}JSON)id=$(printf '%s' "$intent" | jq -r .id)
# Run the tool only once its intent is durable.if ./issue-refund ch_123; then status=ok; else status=error; fi
tenbri append ./audit <<JSON{"kind":"decision","phase":"outcome","source":"sdk","intent":"$id","status":"$status", "principal":{"kind":"agent","id":"support"},"action":{"type":"issue_refund","target":"stripe:ch_123"}}JSON./refund.shcreated stream tnb1:6ea0f69406b84005a960755dd3cfd8e8 at ./auditstream key fingerprint (pin it): sha256:6ea0f69406b84005a960755dd3cfd8e8e2c8352d0a4de17430173e60bdcea2b4keys: ./keys/tnb1-6ea0f69406b84005a960755dd3cfd8e8verify with: tenbri verify ./audit --key sha256:6ea0f69406b84005a960755dd3cfd8e8e2c8352d0a4de17430173e60bdcea2b4refunded ch_123{"action":{"target":"stripe:ch_123","type":"issue_refund"},"id":"01M5D1BJNTJZWCDWZ0E8WS5QHV","intent":"01M5D1BJBSKW9AH2H7S8S8D9FK","kind":"decision","phase":"outcome","prev":"sha256:4d38ec8d…","principal":{"id":"support","kind":"agent"},"seq":4,"sig":"ed25519:298c779b…","signer":"sess:2026-10-20:8439598040cf","source":"sdk","status":"ok","stream":"tnb1:6ea0f69406b84005a960755dd3cfd8e8","ts":"2026-10-20T16:12:20.026Z","v":1}./issue-refund stands for your tool. A draft the writer refuses prints
tenbri: entry rejected: <code>: <detail> and exits 2, so set -e stops before the tool runs: a
draft without source, for example, is rejected with invalid_field.
From any language, through tenbrid. Start it with a stream and a token as in
tenbrid over REST; the token’s principal goes on every entry, and
the server checks action schemas. curl -f exits non-zero when the intent is refused, so set -e
stops before the tool runs.
#!/bin/shset -eAPI=http://127.0.0.1:7070/v1/streams/support# The intent: its Idempotency-Key is the entry id, a ULID your client makes, so a retry is safe.intent=$(curl -sf -X POST "$API/intents" \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: 01K7ZJ4Q3V8M2R6T9W1X5Y7Z0B" \ -H "Content-Type: application/json" \ -d '{"action":{"type":"issue_refund","target":"stripe:ch_123"}}')id=$(printf '%s' "$intent" | jq -r .entry.id)
# Run the tool only once its intent is durable.if ./issue-refund ch_123; then status=ok; else status=error; fi
curl -sf -X POST "$API/outcomes" \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: 01K7ZJ4R8N2B6C4D9E1F3G5H7K" \ -H "Content-Type: application/json" \ -d "{\"intent\":\"$id\",\"status\":\"$status\"}"./refund-rest.shrefunded ch_123{"entry":{"action":{"target":"stripe:ch_123","type":"issue_refund"},"id":"01K7ZJ4R8N2B6C4D9E1F3G5H7K","intent":"01K7ZJ4Q3V8M2R6T9W1X5Y7Z0B","kind":"decision","phase":"outcome","prev":"sha256:39b7833c…","principal":{"id":"support","kind":"agent"},"seq":4,"sig":"ed25519:51d619c6…","signer":"sess:2026-10-20:dbd0e69efa58","source":"sdk","status":"ok","stream":"tnb1:1786a17fc8a9690c394e4c8c00eabfaa","ts":"2026-10-20T14:52:21.574Z","v":1},"replayed":false}Generate a fresh ULID per entry in your client: the fixed keys here are for the example. Sending
the same key again returns the original entry with "replayed":true.
Then read the stream back with tenbri tail, tenbri verify --key, or the bindings’ query and
verify, as on any adapter page. See Streams and entries
for the fields and the entry format for the exact draft shape.
