Skip to Content
Sarek AI AccountabilityLangChain / LangGraph

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().

langchain and @langchain/core are optional peer dependencies.

npm install @sarek/ai-accountability langchain @langchain/core

Quick 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.

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:

  1. The LangSmith-compatible metadata fields ls_provider and ls_model_name, when LangChain attaches them.
  2. Otherwise the serialized class id, for example ['langchain', 'chat_models', 'openai', 'ChatOpenAI'] becomes openai.

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.

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.

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.

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.

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.

SDK reference

Last updated on