Skip to main content
This page records the files your agent’s tools write, so that months later someone can hold a file and establish it is the one the agent produced. Anchoring the tool call proves the agent asked for a file. It does not prove which file came back, and that is the question an auditor asks second. It needs no setup. Attach the LangChain handler as Trace a LangChain agent describes, and file records follow.

What a file record holds

One record per file, of type langchain.artifact, linked to the tool step that produced it. Its promoted metadata is the part a reader can use without opening the body:
path and content_type join name and byte_length when the tool supplied them. The hash lives on the record itself, in event.content_hash, not in this metadata.
From 0.16.0, the record commits to the file’s own bytes (proof#92). event.content_hash is the file’s SHA-256, and the bytes go to your log store the same way any other content does: never sent to Sanning, which receives the signed envelope only. A record written before this release commits to a descriptor instead, and does not match the file when it is handed in beside a pack; see Verify at the command line.

Return the file from your tool

A tool that saves a file returns where it saved it. That return value is the whole integration.
A run that calls that tool anchors the tool start, the tool end, and one record per file, and the file records sit inside the tool’s own run.

Check a file against its record

Whoever holds the file runs one command and compares the result to event.content_hash in the record. No Sanning code takes part in that step:
Replace claims-week-37.csv with the file you are checking. On macOS the command is shasum -a 256. A match establishes that this file is the one the record commits to. What that means for a review is the reader’s conclusion, not ours.

Shapes the SDK recognizes

Three shapes in a tool’s return value are recognized:
  1. LangChain’s own artifact field on a tool message, the first-class channel and what an agent framework hits.
  2. Raw bytes returned from the tool.
  3. A path, as the whole return value, which is what a tool that saves a file returns.
An object that names a file, such as { "name": ..., "path": ... }, counts as a declaration however it arrived. A file is recorded only when the tool result says it came back. Never scraped out of prose, never discovered by watching a folder, never taken from a configured list. A bare string is treated as a path only when a file exists there, so an ordinary tool returning ordinary text records nothing and says nothing.

Show which policy a step applied

A tool that reads a policy file and returns it as its artifact gets a record that commits to the file’s bytes, on that tool’s step. For the example, see Trace a LangChain agent.

The read root

Reading is scoped to one folder, and the folder limits reading rather than storing anything. It defaults to the process’s working directory, because a tool result is text a model influenced, and following a path out of one is an untrusted read.
Containment is checked after the path resolves, so .. and a symlink out of the root both fail closed. A file above max_bytes is recorded as unreadable rather than truncated.

A file the SDK cannot read

A file the tool declared and the SDK could not hash becomes its own record, langchain.artifact_unreadable, carrying the reason. Silence is how three files went unrecorded on a run that reported success.
The reasons are not_found, outside_root, not_a_regular_file, too_large, read_failed and path_reading_disabled. A trail that says “I could not hash this” is evidence. A trail that says nothing is not.

Trace a LangChain agent

The handler that produces these records, and the run tree around them.

Keep what you anchored

Where the record bytes live, and why a pack cannot be built without them.