LangChain and LangGraph
LangChain has no client instance to wrap, so the integration is a callback
instead of monitor(). You attach one callback to a chain, agent or graph, and
every LLM, tool, retriever and chain step it runs is hashed locally with
SHA3-256, signed with the agent’s P-256 key, and anchored as a per-event
proof. The result is an Article 12 record of what your pipeline did, in the
same .ldgp format and the same verification flow as the direct-provider path.
The same callback covers LangGraph: it uses LangChain’s callback system, so the
integration is identical.
Installation
The Sarek SDK does not pull in LangChain. Install the LangChain packages
yourself; the callback imports them lazily, only when you call langchain().
TypeScript
langchain and @langchain/core are optional peer dependencies.
npm install @sarek/ai-accountability langchain @langchain/coreQuick start
langchain() returns a callback that you pass into your chain’s callbacks.
It is async because LangChain is imported on demand. Create one callback per
request so each request gets its own trace.
TypeScript
import { ChatOpenAI } from '@langchain/openai'
import { AiLogger } from '@sarek/ai-accountability'
const sarek = await AiLogger.init({ name: 'loan-desk', role: 'credit-assessor' })
const model = new ChatOpenAI({ model: 'gpt-4o', temperature: 0.2 })
const callback = await sarek.langchain()
const result = await model.invoke('Approve loan #4821?', {
callbacks: [callback]
})
// One signed LogEntry (eventType: 'llm') and one .ldgp proof are written.
// result.content is returned unchanged.
await sarek.close()langgraph() is the same method under a clearer name. Use it for LangGraph
apps; the callback behaves identically.
The callback is a standard LangChain BaseCallbackHandler. The Python callback
is synchronous and works with both invoke() and ainvoke(); the TypeScript
callback works with both .invoke() and .stream().
Provider detection
There is no client object on the callback path, so detection does not work the
way it does for monitor(), and there is no provider hint to pass. The callback
derives the provider and model from each event itself:
- The LangSmith-compatible metadata fields
ls_providerandls_model_name, when LangChain attaches them. - Otherwise the serialized class id, for example
['langchain', 'chat_models', 'openai', 'ChatOpenAI']becomesopenai.
If neither is present, provider and model are recorded as empty strings.
Everything else in the entry, the hashes, signature and proof, is unaffected.
What gets logged
The callback writes to the same place as the direct path: one line per event in
ai-accountability/logs/<agent>.jsonl and one proof in
ai-accountability/proofs/<agent>/log-<logId>.ldgp. Each event produces its own
log entry and its own proof.
Unlike the direct path, which only logs LLM calls, the callback logs five event
types: llm, tool, retriever, chain and error. For the full field
tables, see the SDK reference for
TypeScript or
Python.
One request, many proofs
A single user request through a chain or agent produces several events, and
therefore several entries and several .ldgp proofs. They are tied together by
traceId, which is anchored to the root run, the one event whose parentRunId
is null. That id is identical to LangSmith’s trace_id, so a Sarek proof and
a LangSmith trace line up.
Request comes in
chain runId 'abc-123' parentRunId null <- root, traceId = 'abc-123'
llm runId 'def-456' parentRunId 'abc-123'
llm runId 'ghi-789' parentRunId 'abc-123'
tool runId 'jkl-012' parentRunId 'abc-123' (send_email)
Four entries, four proofs, all carrying traceId 'abc-123'.An auditor can pull every proof for a request by its traceId, verify each one
on its own, and rebuild the decision tree from the parentRunId links.
To override the anchor, pass your own traceId. When the callback never sees a
root run, it falls back to a generated UUID.
TypeScript
const callback = await sarek.langchain({
role: 'credit-assessor',
traceId: 'request-2026-06-16-4821'
})LangGraph
LangGraph runs on the same callback system, so there is nothing extra to wire
up. Attach the callback at the top of the run and LangGraph propagates it to
every node. Tool calls, where an agent takes a real action such as sending mail
or writing to a database, are logged as tool events.
TypeScript
import { AiLogger } from '@sarek/ai-accountability'
const sarek = await AiLogger.init({ name: 'devbot', role: 'support-agent' })
export async function runWorkflow(query: string) {
const callback = await sarek.langchain() // new trace per request
return workflowApp.invoke(
{ query },
{ callbacks: [callback] } // propagated to every node
)
}In a graph run the callback also records the LangGraph metadata: thread_id
(the conversation anchor, more durable than a per-request id for multi-turn
work), langgraph_node (which node produced the call), langgraph_step (the
super-step, which separates otherwise identical iterations of a loop) and
checkpoint_ns. On a plain LangChain run these are null.
Streaming
Streaming is supported and needs nothing special. When you call .stream() or
.astream(), LangChain delivers tokens to your code one by one, but it also
aggregates the stream internally and hands the callback the complete result at
the end. The callback hashes that full output once, so a streamed call and a
non-streamed call on the same input produce the same outputHash. There is no
per-token handler, and the proof format is the same either way.
Error handling
A failed call is itself a loggable event. When an LLM, tool or chain errors, the
callback writes an entry with outcome: 'error', the exception class in
errorClass, and a SHA3-256 hash of the message in errorHash. The raw message
is never stored.
The callback never throws into your pipeline; every handler is isolated. If a
handler hits an internal error, it is routed to your onError callback. If you
do not provide one, that error, and the entry it would have produced, is dropped
silently. In production, set onError so internal failures are visible.
TypeScript
const callback = await sarek.langchain({
role: 'credit-assessor',
onError: (err, context) => {
appLogger.error('sarek callback error', {
handler: context.handler,
runId: context.runId,
err
})
}
})Retriever errors are not logged as error events; LLM, tool and chain errors are.
As with the rest of the SDK, anchoring runs in the background and never blocks
the AI response. Call close() before the process exits so in-flight proofs
finish.
LangSmith
Sarek and LangSmith are complementary. LangSmith keeps traces in LangChain’s
infrastructure; Sarek adds an independent, third-party-verifiable proof. Pass
both callbacks together. Because Sarek’s traceId equals LangSmith’s
trace_id, the two records cross-reference cleanly.
TypeScript
const sarekCallback = await sarek.langchain()
await model.invoke(input, {
callbacks: [langsmithTracer, sarekCallback]
})Verification
A proof from a LangChain pipeline is byte-for-byte the same kind of .ldgp
proof as one from a directly monitored client, and it verifies through the same
flow. Verify any single event by its proof, with or without the original data.
See Verification.