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
anchorCallbacks, which returns an
AnchorCallbackHandler.What a step commits
Every callback commits the whole step: inputs, outputs, the serialized 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 ineventBytes 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,langchain.retriever_error. - The agent’s own decisions:
langchain.agent_action,langchain.agent_finish. - Files:
langchain.artifact,langchain.artifact_unreadable. - The run’s end:
langchain.run_closed, the closing record.
- The run linkage:
run.run_id,run.parent_run_id,run.root_run_id,run.seqandrun.prev_event_id, plus the step name (run.name) and the framework client class (run.client). - The recorder:
otel.scope.name(@sanning/anchor-langchain),otel.scope.versionandotel.scope.schema_url(https://opentelemetry.io/schemas/1.41.0, the version of the OpenTelemetry names below), on every record.otel.trace_idandotel.span_idappear when a real span exists. - Model and tool facts, under
metadata.gen_aiwith the names the OpenTelemetry GenAI semantic conventions give them, pinned to v1.41.0:gen_ai.provider.name,gen_ai.request.model, the call’s settings (gen_ai.request.temperature,.max_tokens,.seed,.top_p),gen_ai.response.model,gen_ai.response.id,gen_ai.response.finish_reasons, token use (gen_ai.usage.input_tokens,.output_tokens,.reasoning.output_tokens),gen_ai.tool.nameandgen_ai.tool.call.id. Where an OpenAI-compatible response carries one,openai.response.system_fingerprintrecords the provider’s serving configuration. - Your own claims, when you give them:
gen_ai.agent.versionfrom theagentVersionoption, on every record, andgen_ai.prompt.namefrom the run’s metadata keygen_ai.prompt.name, for that call.
gen_ai.provider.name is the service the call went to, such as
openai, anthropic, aws.bedrock or openrouter, and is absent when
unknown. In Python it comes from the endpoint the client resolved. In
TypeScript it comes only from an endpoint set in the client’s own
configuration: an endpoint set by an environment variable, or the client’s
default, records no provider. Step and tool names are signed by default; set recordNames: false
to omit run.name and gen_ai.tool.name when a name could reveal more than
you want a reader of the record to see.
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. 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.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.
Close the anchorer yourself
A handler answers for its own records, andclose() never closes the
anchorer. gaps, isComplete and close() count only the records that
handler handed over (chain_gaps does the same in Python), so one anchorer can
serve a handler per request and one request’s lost record never marks another
request’s trail incomplete. To see every hole on the anchorer, including
records you anchored beside the handler, read anchorer.gaps. Call
await anchorer.close() (anchorer.close() in Python) when the process is
done with the anchorer, after every handler that uses it. That is where a
buffered record still pending is reported.
Stop the agent when a record is lost
Provenance does not take the agent down by default. With an anchorer built withfailClosed: true (fail_closed=True in Python), the handler instead
waits for each step’s records and stops the agent when any record is lost,
before the next step runs. A lost record is any record of the handler’s that is
not in the trail, including the record of a file a tool returned: one the plane
refused, one the handler could not build, a chain-store failure, or a step
whose callback threw before its record. A tool whose tool_start record is
lost does not run.
A stopped handler stays stopped. Every later step throws the same error, and a
new run on it throws an error that says to construct a new handler. Construct
one handler per run; Hand over and verify a case shows the pattern.
Show which policy a step applied
To show which policy file a step read, have the tool that reads it return the file as its artifact. The handler then writes alangchain.artifact record on
that tool’s step, and the record commits to the file’s own bytes. The trail
shows the policy the step actually read, not one named when you configured the
handler.
sha256sum and compares the result with
the record’s event.content_hash. The file’s bytes go to your log store and
never to Sanning. The path must sit inside the artifact read root; see
File records.
Provenance never takes the agent down. A payload that cannot be serialized 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.