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 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 in eventBytes 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.
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:
  • The run linkage: run.run_id, run.parent_run_id, run.root_run_id, run.seq and run.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.version and otel.scope.schema_url (https://opentelemetry.io/schemas/1.41.0, the version of the OpenTelemetry names below), on every record. otel.trace_id and otel.span_id appear when a real span exists.
  • Model and tool facts, under metadata.gen_ai with 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.name and gen_ai.tool.call.id. Where an OpenAI-compatible response carries one, openai.response.system_fingerprint records the provider’s serving configuration.
  • Your own claims, when you give them: gen_ai.agent.version from the agentVersion option, on every record, and gen_ai.prompt.name from the run’s metadata key gen_ai.prompt.name, for that call.
These fields are what the integration reported, not proof of what served the request. 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 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. 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.
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.

Close the anchorer yourself

A handler answers for its own records, and close() 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 with failClosed: 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 a langchain.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.
An auditor holding the policy runs 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.

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.