Skip to main content
This page anchors a whole agent run instead of a call per step. You add one callback handler, and every chain, model, tool and retriever step becomes its own signed record, along with every file a tool produces. The handler needs an anchorer, which Anchor from your agent builds and explains.

Install the adapter

In TypeScript the adapter is its own package and takes @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 in recordBytes 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.
Each record also carries a closed list of promoted fields in named metadata, so a reader can select one run, or one model, without opening the body: 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 a runId 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:
Delete an event and the next one’s pointer dangles. Reorder them and 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.
The two languages differ in one default, and it is deliberate. TypeScript’s 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.

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.