Repository navigation
Framework Guides
Every framework below exposes a hook that fires before and after a tool
call. The pattern is always the same: intent leaf before, result leaf
after, on one journal per agent identity. witness_log.step does exactly
this for a plain function; the adapters in witness_log.integrations do it
through each framework's own hook, and record a handoff where the framework
has one.
| framework | install | adapter | handoff |
|---|---|---|---|
| OpenAI Agents SDK | pip install "witness-log[openai-agents]" |
witness_log.integrations.openai_agents.WitnessLogAudit / WitnessLogHooks
|
on_handoff |
| LangGraph | pip install "witness-log[langgraph]" |
witness_log.integrations.langgraph.WitnessLogGraphHandler |
Command(goto=...) |
| LangChain | pip install "witness-log[langchain]" |
witness_log.integrations.langchain.WitnessLogCallbackHandler |
– |
| Google ADK | pip install "witness-log[google-adk]" |
witness_log.integrations.google_adk.WitnessLogPlugin |
transfer_to_agent |
| pydantic-ai | pip install "witness-log[pydantic-ai]" |
witness_log.integrations.pydantic_ai.WitnessLogToolset |
– |
| AutoGen AgentChat | pip install "witness-log[autogen]" |
witness_log.integrations.autogen.WitnessLogAutoGen |
HandoffMessage |
| LlamaIndex | pip install "witness-log[llamaindex]" |
witness_log.integrations.llamaindex.WitnessLogLlamaIndex / WitnessLogSpanHandler
|
handoff tool |
| CrewAI | pip install "witness-log[crewai]" |
witness_log.integrations.crewai.witness_crewai |
– |
| MCP hosts | pip install "witness-log[mcp]" |
witness-log-mcp (MCP integration) |
– |
| any stdio MCP server, as run | pip install witness-log |
witness-log-mcp-proxy (MCP proxy) |
– |
| Claude Code hooks | pip install witness-log |
witness-log-hook (Hooks) |
– |
pip install "witness-log[all-frameworks]" installs every extra except
crewai (its mcp<2 pin conflicts with the mcp extra; give it its own
environment).
What is common to all of them:
-
Pairing by the framework's call id. Two parallel calls of one tool
are two intents and two results, each result's
intent_indexpointing at its own intent. Nothing is keyed by tool name. -
Payload shapes match
step: the intent commits to{"tool", "args", "t", "call_id", "agent", ...}, the result to{"tool", "intent_index", "outcome", "value", ...}withoutcome"ok"or"error". A tool that raises is recorded as"error"whether the framework re-raises it, swallows it into a message for the model, or asks for a retry. -
Never raises into the framework unless you pass
raise_on_error=True; append failures are kept in.errors.record_output=Falsekeeps return values out of the result leaf (errors are still recorded). -
One log or one journal per agent. Every adapter takes either a
JournaledLog(the whole run in one journal) or anAgentJournals(one journal per agent name under a directory). With journals a handoff writes aHandoffAttestationSentleaf in the source agent's journal andReceivedin the target's, over the same state bytes, sowitness-log bind(orwitness_log.profiles.ap2.bind_dossiersin Python) proves the two journals agree on who handed what to whom. With one log the handoff is the handoff tool's own intent / result pair, annotatedhandoff_to. -
Payloads are kept for disclosure. The adapter's
Recorderkeeps the exact bytes it appended (.recorder.payloads, orjournals.recorder(name).payloads), so a leaf can later be disclosed as leaf + payload + salt (AgentJournals.disclose(i),witness_log.profiles.ap2.dispute_export) or erased (AgentJournals.forget(i)). -
Tested against the real framework, nightly. Each adapter has a test
that imports the installed framework and runs its own tool-calling loop
with a scripted model; the
frameworks-liveworkflow installs the latest release of each framework from PyPI and fails if the test is skipped or breaks.
# one journal per agent, sealed identities from your KMS; every adapter takes `journals` in place of a log
from agents import Runner
from witness_log.integrations import AgentJournals
from witness_log.integrations.openai_agents import WitnessLogHooks
journals = AgentJournals("audit/", seed_for=kms.seed_for, sealed=True)
await Runner.run(triage_agent, "bill me", hooks=WitnessLogHooks(journals))
journals.export_dossiers() # audit/<agent>.dossier.json, one per agent
journals.disclose(0, agent="triage") # {"leaf", "payload", "salt", "verified": True}
journals.forget(0, agent="triage") # right to be forgotten; the dossier still verifiesjournals.witness("trusted.json") runs one witnessing round per journal
against your trusted directory and rewrites each dossier with the
cosignatures it collected.
To get witnessed while the agent runs, add a background
Witnesser next to any single log:
w = witness_log.Witnesser(log, "trusted.json", "agent.jsonl"), then
w.request() whenever you like and w.flush(timeout) at the end.
import witness_log
from agents import Runner, add_trace_processor
from witness_log.integrations.openai_agents import WitnessLogAudit
audit = WitnessLogAudit("audit/", seed_for=kms.seed_for, sealed=True) # one journal per agent.name
add_trace_processor(audit.processor()) # completes tools that raise through
result = await Runner.run(triage_agent, "bill me", hooks=audit.hooks)
audit.export_dossiers() # audit/triage.dossier.json, audit/billing.dossier.jsonFor one journal shared by the run, WitnessLogHooks(witness_log.open("agent.jsonl"))
as hooks=. No context= is required.
What the adapter handles in this SDK specifically (tested against
openai-agents 0.23 with its agents.testing.ScriptedModel):
- Calls are keyed by
ToolContext.tool_call_idand the arguments come fromToolContext.tool_arguments(the model's raw JSON, parsed). The 0.3.5 wiki snippet keyed by tool name and raisedKeyErrorwhen one tool was called twice in parallel. - A tool that raises is replaced, before
on_tool_endsees it, by the string "An error occurred while running the tool. Please try again."; the real exception goes only to the trace span as "Error running tool (non-fatal)". The adapter reads the span error (or recognises the default text when tracing is off) and recordsoutcome: "error". - A tool declared with
failure_error_function=Noneraises through the run and never reacheson_tool_end;audit.processor()is aTracingProcessorthat writes that result leaf when the function span ends. Nothing is exported anywhere by it. -
on_handoffwritesSentin the source agent's journal,Receivedin the target's, and an intent / result pair namedhandoffin the source.
Alternatively decorate the tool itself, arguments included:
from agents import function_tool
@function_tool
@witness_log.step(log)
def get_balance(account: str) -> dict:
return {"account": account, "balance": 42}import witness_log
from witness_log.integrations.langgraph import WitnessLogGraphHandler
handler = WitnessLogGraphHandler(witness_log.open("graph.jsonl"))
out = graph.invoke({"messages": [("user", "hi")]}, config={"callbacks": [handler]})LangGraph runs tools through LangChain's callbacks, so this is the
LangChain handler keyed by run_id with two additions: the node a tool
runs in (langgraph_node in the callback metadata) is recorded as the
agent and, with AgentJournals, selects the journal; and a tool or node
that returns Command(goto="other_node") (the create_handoff_tool /
supervisor pattern) is a handoff from the current node to goto. Tool
errors are recorded as "error" whether ToolNode(handle_tool_errors=True)
turns them into a message or they propagate.
import witness_log
from witness_log.integrations.langchain import WitnessLogCallbackHandler
handler = WitnessLogCallbackHandler(witness_log.open("agent.jsonl"))
agent.invoke({"messages": [("user", "balances?")]}, config={"callbacks": [handler]}) # create_agent / AgentExecutoron_tool_start / on_tool_end / on_tool_error keyed by run_id. Use
the LangGraph handler above when the agent is a graph with several nodes.
import witness_log
from google.adk.apps import App
from google.adk.runners import InMemoryRunner
from witness_log.integrations.google_adk import WitnessLogPlugin
plugin = WitnessLogPlugin(witness_log.open("agent.jsonl"))
runner = InMemoryRunner(app=App(name="app", root_agent=root_agent, plugins=[plugin]))A plugin applies to every agent under the runner (per-agent
before_tool_callback / after_tool_callback do not). Calls are keyed by
ToolContext.function_call_id, the agent is ToolContext.agent_name, a
tool that raises reaches on_tool_error_callback and is recorded as
"error" (the plugin returns None there, so ADK's own error handling is
unchanged). When a tool sets tool_context.actions.transfer_to_agent
(the built-in transfer_to_agent, which ADK adds for sub_agents), the
result leaf carries handoff_to and, with AgentJournals, the two
attestations are written. Tested against google-adk 2.11.
import witness_log
from pydantic_ai import Agent
from pydantic_ai.toolsets import FunctionToolset
from witness_log.integrations.pydantic_ai import WitnessLogToolset
tools = FunctionToolset([get_balance, send_invoice])
agent = Agent(model, name="billing", toolsets=[WitnessLogToolset(tools, witness_log.open("agent.jsonl"))]) # model: "openai:gpt-4o", ...
agent.run_sync("bill account a")WitnessLogToolset(wrapped, target) is a WrapperToolset, so it takes
the wrapped toolset first as pydantic-ai's own wrappers do. Calls are
keyed by RunContext.tool_call_id and the recorded arguments are the
validated ones. ModelRetry is recorded as "error" with retry: true;
any other exception is recorded and re-raised unchanged. Tested against
pydantic-ai-slim 2.55.
import witness_log
from witness_log.integrations.autogen import WitnessLogAutoGen
audit = WitnessLogAutoGen(witness_log.open("team.jsonl"))
result = await audit.run(team, task="bill me") # in place of await team.run(task=...)AgentChat has no per-tool hook; what agents do is the event stream, so the
adapter reads it. ToolCallRequestEvent is one intent per FunctionCall
(keyed by its id), ToolCallExecutionEvent one result per
FunctionExecutionResult (outcome from is_error), a HandoffMessage a
handoff from source to target. To keep your own loop:
async for event in audit.record(team.run_stream(task="bill me")):
... # events pass through unchanged (Console works)Tested against autogen-agentchat 0.7 with autogen-ext's
ReplayChatCompletionClient.
import witness_log
from witness_log.integrations.llamaindex import WitnessLogLlamaIndex
audit = WitnessLogLlamaIndex(witness_log.open("agent.jsonl"))
result = await audit.run(workflow, user_msg="bill me") # in place of await workflow.run(user_msg=...)AgentWorkflow / FunctionAgent / ReActAgent emit a ToolCall and a
ToolCallResult (tool_output.is_error) per call with the model's
tool_id; the current agent comes from the surrounding AgentInput /
AgentOutput events. The built-in handoff tool (can_handoff_to) is
recorded as a handoff to to_agent. For tools called outside an agent
workflow, hook the instrumentation instead:
from llama_index.core.instrumentation import get_dispatcher
from witness_log.integrations.llamaindex import WitnessLogSpanHandler
get_dispatcher().add_span_handler(WitnessLogSpanHandler(witness_log.open("tools.jsonl")))That records the FunctionTool.call / acall spans: intent when the
span opens, result when it exits or is dropped with an exception. Use one
mechanism per log, not both. Tested against llama-index-core 0.14.
import witness_log
from witness_log.integrations.crewai import witness_crewai
log = witness_log.open("agent.jsonl")
with witness_crewai(log): # CrewAI's global before/after tool-call hooks
crew.kickoff()Or decorate individual tools (works with any CrewAI version):
from crewai.tools import tool
@tool("search")
@witness_log.step(log, name="search")
def search(query: str) -> str:
"""Search the web."""
return do_search(query)crewai pins mcp<2, so install the crewai and mcp extras in separate
environments.
# wl_audit.py
import witness_log
from witness_log.integrations import Recorder
log = witness_log.open("agent.jsonl") # one identity per agent
rec = Recorder(log) # rec.intent(tool, args) -> index; rec.result(tool, index, value, outcome)
intent, result = rec.intent, rec.resultfor block in response.content:
if block.type == "tool_use":
i = wl_audit.intent(block.name, block.input, call_id=block.id)
out = TOOLS[block.name](**block.input)
wl_audit.result(block.name, i, out, call_id=block.id)witness-log-hook install claude-code --journal ~/.witness-log/claude-code.jsonlprints the .claude/settings.json entries (it writes nothing):
{"hooks": {
"PreToolUse": [{"matcher": "", "hooks": [{"type": "command", "command": "witness-log-hook --autostart --journal ~/.witness-log/claude-code.jsonl"}]}],
"PostToolUse": [{"matcher": "", "hooks": [{"type": "command", "command": "witness-log-hook --autostart --journal ~/.witness-log/claude-code.jsonl"}]}]}}Each hook invocation sends the event to witness-log-hookd, a daemon
that holds the journal open behind a Unix socket (started on first use by
--autostart), so parallel tool calls do not fight over the journal's
single-writer lock and no session key is minted per call. PreToolUse
becomes an intent leaf, PostToolUse a result leaf. Details, the exact
payload shape, Cursor and Codex: Hooks.
Whatever framework: log.sync() (or journals.sync()), then either a
background witness_log.Witnesser(log, "trusted.json", "agent.jsonl") with
w.flush(timeout) (Quickstart: Python §5),
journals.witness("trusted.json") for a set of journals, or
witness-log witness submit agent.jsonl --directory trusted.json --out dossier.json,
then witness-log verify dossier.json --identity <key> --directory trusted.json --min-witnesses N
with N no larger than the number of witnesses that can cosign (two local
witnesses: --min-witnesses 2). Two agents' dossiers that handed work to
each other: witness-log bind a.json b.json.
Start
- Home
- Why witness-log
- Concepts
-
Comparison
- Tessera · Microsoft AGT · Signet · immudb · Agent Receipts · SCITT
Use it
- Quickstart: Python
- Quickstart: TypeScript
- Quickstart: Rust
- Quickstart: CLI
- CLI reference
- MCP integration
- Framework guides
- Quickstart: Go
- Browser verifier
- Other languages
Tutorials
Understand it
- Architecture
- Design decisions
- How a leaf becomes a proof
- Witness protocol walkthrough
- Redaction explained
- Keys and identity
Trust it
- Verification and verdicts
- Getting witnessed
- Operator onboarding
- Witness status
- Running a witness
- Compliance
- Threat model
- Spec and interop
- SCITT mapping
Run it
Project