Install the adapter
@langchain/core as a
peer dependency. In Python the handler ships inside sanning-anchor and the
extra pulls in LangChain.
Attach the handler to a run
Pass the handler the way you pass any other LangChain callback.agent here
is whatever you already invoke.
In TypeScript the entry point is
envelopeAnchorCallbacks.
anchorCallbacks, which autocomplete also offers, is an earlier handler on a
different path and records no files.What a step commits
Every callback commits the whole step: inputs, outputs, the serialised model, tags, metadata, run name and tool call id. Nothing is curated, and nothing is edited before it is hashed. Only the hash reaches Sanning. Prompts, outputs and tool input and output are hashed in your process, and the bytes go to the log store you configured or stay inrecordBytes for you to drain.
The event vocabulary is exported as EVENT_TYPES in both languages:
- Chain steps:
langchain.chain_start,langchain.chain_end,langchain.chain_error. - Model calls:
langchain.chat_model_start,langchain.llm_start,langchain.llm_end,langchain.llm_error. - Tool calls:
langchain.tool_start,langchain.tool_end,langchain.tool_error. - Retrieval:
langchain.retriever_start,langchain.retriever_end. - The agent’s own decisions:
langchain.agent_action,langchain.agent_finish. - Files:
langchain.artifact,langchain.artifact_unreadable.
run.run_id, run.parent_run_id, run.root_run_id, run.seq,
run.prev_event_id, run.tool_call_id, model.id, model.id_returned,
model.provider, and the OpenTelemetry trace_id and span_id when a real
span exists. A field that is unknown is absent rather than empty.
The run tree makes a missing step visible
LangChain gives every step arunId and a parentRunId. The handler commits
that linkage, plus its own per-run ordinal and a pointer at the previous
event, inside each record’s signed bytes:
seq
disagrees. Edit one and its hash breaks. The result is tamper-evident and
reconstructable offline, from the records alone, with no call to Sanning.
Chaining is on by default, and the default is the point: an unchained set
cannot tell “nothing else happened” from “something was removed”.
Close the handler and read the gaps
A step that cannot be anchored still burns its slot in the chain, so the survivors cannot close ranks over it. An auditor sees the hole.close() returns the gaps; pass { raiseOnGaps: true } to make an incomplete
trail an error. Python’s with block asserts completeness on a clean exit and
raises IncompleteTrailError, and a block that is already unwinding gets its
gaps logged so the agent’s own exception survives.
A gap is reported as undelivered, never as a bad record. A timeout says
nothing about whether the control plane accepted the envelope, so retain each
gap’s record bytes: after a failure they are the only copy.
Provenance never takes the agent down. A payload that cannot be serialised is
reported through a warning and skipped, and the run continues.
Files a tool produces
A tool that writes a file gets its own record, with no setup and no resolver to write. Anchoring the tool call proves the agent asked for a file; it does not prove which file came back, and the file record is what answers that.What to read next
Record a file a tool produces
Name, size and hash, and the check an auditor runs against them.
Keep what you anchored
The store the hand-over pack reads, and what breaks without it.