v0.7.9 RELEASED – LangChain callback handler + LangGraph proxy adapter. Available on PyPI and npm.

Who this is for: Python developers building LLM applications with LangChain/LangGraph, compliance leads managing orchestrated AI systems, and auditors evaluating multi-step AI pipelines.

Orchestration is not compliance. LangChain and LangGraph are orchestration frameworks, not compliance tools. They route prompts to models and manage agent state. When your LCEL chain or LangGraph agent completes a multi-step workflow, what evidence proves each inference step was governed? LangSmith traces execution for debugging. SWT3 witnesses decisions for accountability. These are fundamentally different records serving fundamentally different audiences.

Contents

1. Two Integration Patterns 2. LangChain Callback Handler 3. RAG Pipeline Witnessing 4. LangGraph Agent Witnessing 5. Streaming Graph Execution 6. Combining Both Patterns 7. Model-Agnostic Evidence 8. Regulatory Evidence Mapping 9. Quick Start 10. References

1. Two Integration Patterns

LangChain and LangGraph solve different problems. LangChain composes LLM calls into chains and pipelines using LCEL (LangChain Expression Language). LangGraph builds stateful, multi-step agents with explicit graph topologies. SWT3 provides a dedicated adapter for each.

Framework Integration What It Witnesses When to Use
LangChain SWT3CallbackHandler Every LLM call and retriever result in any chain or pipeline LCEL chains, sequential pipelines, RAG with retrievers, any LangChain-managed LLM call
LangGraph wrap_langgraph / wrapLangGraph Each graph invocation: input state hash, output state hash, model, latency Stateful agents, multi-step workflows, compiled graphs with branching or cycles

The callback handler is event-driven. It hooks into LangChain's callback system and fires on every LLM call and retriever result, regardless of which provider is underneath. The LangGraph proxy intercepts graph-level invoke(), stream(), ainvoke(), and astream() calls and witnesses the complete state transition.

These are not mutually exclusive. You can use both in the same application. The callback handler witnesses individual LLM calls inside the graph. The proxy witnesses the overall graph execution. Section 6 covers this two-layer pattern.

2. LangChain Callback Handler

The SWT3CallbackHandler plugs into LangChain's native callback system. Pass it once when constructing your LLM, and every call through that LLM -- whether it originates from a chain, an agent, or a direct invocation -- is witnessed automatically.

from langchain_openai import ChatOpenAI
from swt3_ai import Witness
from swt3_ai.adapters.langchain import SWT3CallbackHandler

witness = Witness(
    endpoint="https://your-instance.example.com/api/v1/witness",
    api_key="axm_your_key",
    tenant_id="YOUR_TENANT",
)
handler = SWT3CallbackHandler(witness)

llm = ChatOpenAI(model="gpt-4o", callbacks=[handler])
response = llm.invoke("Analyze this regulatory filing.")
# Every LLM call through this chain is witnessed automatically

witness.flush()

The callback handler captures three events:

It works with ANY LangChain-supported LLM. OpenAI, Anthropic, Bedrock, Ollama, local models -- the handler does not care which provider is underneath. It extracts the actual model name from LangChain's internal serialization, so the witness anchor always records the real model, not a generic wrapper name.

TypeScript note. LangChain's callback system is Python-native. The SWT3CallbackHandler is Python-only. For TypeScript LLM applications, wrap the client directly with witness.wrap() (for OpenAI, Anthropic, etc.) or use the LangGraph adapter (wrapLangGraph) for graph-based agents. Section 4 covers the TypeScript LangGraph pattern.

AI-INF.1 -- Inference Witnessing

Framework-agnostic evidence

The callback handler extracts the actual model name from LangChain's serialization metadata, not from the class name. If you construct ChatOpenAI(model="gpt-4o"), the anchor records gpt-4o as the model and openai as the provider -- regardless of how many LCEL steps wrap the call.

Assessor tip

Query anchors by provider to see which underlying models the organization is actually using through LangChain. Compare against their approved model inventory. If anchors show a model not on the approved list, the orchestration layer may be routing to unapproved providers -- a governance finding even if the orchestration code was reviewed.

System Prompt Witnessing

The callback handler captures system prompts automatically. When on_chat_model_start fires, the handler extracts system messages from the messages array and hashes them independently as system_prompt_hash. This works for any LangChain chat model -- OpenAI, Anthropic, Bedrock, or local -- without configuration.

llm = ChatOpenAI(model="gpt-4o", callbacks=[handler])

# System prompt passed via LangChain's standard pattern
from langchain_core.messages import SystemMessage, HumanMessage

response = llm.invoke([
    SystemMessage(content="You are a claims adjuster. Never approve claims over $50,000 without escalation."),
    HumanMessage(content="Review claim #7721."),
])
# system_prompt_hash captures the adjuster instructions for this call
AI-INF.2 -- System Prompt Governance

Drift detection through orchestration layers

Assessor tip

LangChain applications frequently construct system prompts dynamically -- injecting user context, retrieval results, or chain-of-thought templates. The system_prompt_hash captures the FINAL assembled prompt that reaches the model, not the template. If the hash changes between calls with the same template, the dynamic injection changed. Query for hash stability over the assessment period to verify prompt governance.

3. RAG Pipeline Witnessing

When a LangChain retriever fetches documents, the callback handler automatically witnesses the retrieval as RAG context. No additional code is needed beyond what you already wrote in Section 2.

from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from swt3_ai import Witness
from swt3_ai.adapters.langchain import SWT3CallbackHandler

witness = Witness(
    endpoint="https://your-instance.example.com/api/v1/witness",
    api_key="axm_your_key",
    tenant_id="YOUR_TENANT",
)
handler = SWT3CallbackHandler(witness)

embeddings = OpenAIEmbeddings()
retriever = FAISS.from_texts(texts, embeddings).as_retriever()

llm = ChatOpenAI(model="gpt-4o", callbacks=[handler])

# LCEL chain: retriever pipes into LLM
chain = retriever | llm
result = chain.invoke("What is our data retention policy?")
# Both the retrieval and the LLM call are witnessed

witness.flush()

When on_retriever_end fires, the handler captures each retrieved document's content hash, source identifier, and similarity score as RagChunk objects. It then calls witness.witness_rag_context() internally, minting an AI-RAG.1 anchor without any additional code from you.

This means every RAG pipeline built with LangChain retrievers automatically produces provenance evidence: which documents were retrieved, from which source, with what relevance, for which query -- all fingerprinted and anchored.

AI-RAG.1 -- Retrieval Provenance

Automatic RAG evidence

Each retriever invocation produces an anchor recording: the query hash, the number of documents retrieved, the content hash of each document, and the source identifiers. The response itself is not stored -- only its fingerprint.

Assessor tip

For RAG-based applications, verify that every inference anchor (AI-INF.1) has a corresponding retrieval anchor (AI-RAG.1) with a timestamp immediately preceding it. If inference anchors exist without retrieval anchors, the application may be generating responses without consulting its knowledge base -- a retrieval bypass that undermines the stated RAG architecture. Compare source_id values against the organization's approved document corpus.

4. LangGraph Agent Witnessing

LangGraph agents are stateful graphs: they take an input state, pass it through nodes (which may call LLMs, tools, or other logic), and produce an output state. The wrap_langgraph proxy intercepts graph-level invocations and witnesses the complete state transition.

Python

from langgraph.graph import StateGraph
from swt3_ai import Witness
from swt3_ai.adapters.langgraph import wrap_langgraph

witness = Witness(
    endpoint="https://your-instance.example.com/api/v1/witness",
    api_key="axm_your_key",
    tenant_id="YOUR_TENANT",
    agent_id="claims-processor",
)

graph = StateGraph(...)  # your graph definition
app = graph.compile()
witnessed_app = wrap_langgraph(app, witness)

result = witnessed_app.invoke({"messages": [("user", "Process this claim")]})
# The entire graph execution is witnessed as a single anchor

witness.flush()

TypeScript

import { StateGraph } from '@langchain/langgraph';
import { Witness, wrapLangGraph } from '@tenova/swt3-ai';

const witness = new Witness({
  endpoint: 'https://your-instance.example.com/api/v1/witness',
  apiKey: 'axm_your_key',
  tenantId: 'YOUR_TENANT',
  agentId: 'claims-processor',
});

const graph = new StateGraph({...}); // your graph definition
const app = graph.compile();
const witnessedApp = wrapLangGraph(app, witness);

const result = await witnessedApp.invoke({
  messages: [['user', 'Process this claim']],
});

await witness.flush();

The proxy intercepts four methods: invoke(), stream(), ainvoke(), and astream(). For invoke() and ainvoke(), the input state dictionary is hashed as prompt_hash and the output state is hashed as response_hash. For streaming variants, the proxy accumulates the last chunk and uses it as the response hash after the stream completes.

AI-INF.1 -- Graph-Level Witnessing

Complete state transition evidence

The anchor captures the entire graph execution as a single witnessed event: the input state fingerprint, the output state fingerprint, the model used by the graph, and the total wall-clock latency across all nodes.

Assessor tip

The anchor captures the complete state transition, not just the final output. If two graph invocations produce the same output but followed different node paths (different intermediate reasoning), their prompt_hash or response_hash values will differ because the full state -- including intermediate state mutations -- is included in the hash. This distinguishes LangGraph witnessing from simple input/output logging.

5. Streaming Graph Execution

LangGraph's stream() and astream() yield chunks as the graph executes. The SWT3 proxy passes every chunk through untouched -- your application receives the stream exactly as it would without witnessing. The witness anchor is minted after the stream completes.

Synchronous Streaming

for chunk in witnessed_app.stream({"messages": [("user", "Search and summarize")]}):
    print(chunk)  # Every chunk passes through immediately
# Witness anchor minted after stream completes

Async Streaming

async for chunk in witnessed_app.astream({"messages": [("user", "Search and summarize")]}):
    print(chunk)  # Every chunk passes through immediately
# Witness anchor minted after stream completes

The last-chunk pattern. During streaming, the proxy records a timestamp at stream start and accumulates a reference to the last chunk yielded. After the stream's iterator is exhausted, the proxy hashes the last chunk as the response hash, calculates total latency, and mints the anchor. Every chunk is yielded to the developer with zero added latency -- the witnessing overhead is deferred entirely to after the stream ends.

Stream interruption. If the stream is interrupted (the consumer breaks out of the loop early), the proxy witnesses whatever was received up to that point. The anchor's response_hash reflects the last chunk that was actually yielded, not the complete graph output. This is intentional -- the evidence records what was delivered, not what could have been.

6. Combining Both Patterns

Complex architectures often use LangChain LLMs inside LangGraph agents. A LangGraph node might call a LangChain ChatModel, which might use a retriever. With both adapters active, you get a two-layer evidence chain: per-LLM-call evidence from the callback handler and per-graph-execution evidence from the proxy.

from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, START, END
from swt3_ai import Witness
from swt3_ai.adapters.langchain import SWT3CallbackHandler
from swt3_ai.adapters.langgraph import wrap_langgraph

witness = Witness(
    endpoint="https://your-instance.example.com/api/v1/witness",
    api_key="axm_your_key",
    tenant_id="YOUR_TENANT",
    agent_id="research-agent",
)

# Layer 1: Callback handler witnesses every LLM call
handler = SWT3CallbackHandler(witness)
llm = ChatOpenAI(model="gpt-4o", callbacks=[handler])

# Build a LangGraph agent that uses the witnessed LLM
def research_node(state: MessagesState):
    return {"messages": [llm.invoke(state["messages"])]}

def summarize_node(state: MessagesState):
    return {"messages": [llm.invoke(state["messages"])]}

graph = StateGraph(MessagesState)
graph.add_node("research", research_node)
graph.add_node("summarize", summarize_node)
graph.add_edge(START, "research")
graph.add_edge("research", "summarize")
graph.add_edge("summarize", END)

# Layer 2: Proxy witnesses the overall graph execution
witnessed_app = wrap_langgraph(graph.compile(), witness)

result = witnessed_app.invoke({"messages": [("user", "Research and summarize AI regulation")]})
# 3 anchors minted: research LLM call + summarize LLM call + graph execution

witness.flush()

Three anchors from one agent run. With both patterns active, you get per-LLM-call evidence (AI-INF.1 per model invocation) AND per-agent-execution evidence (AI-INF.1 per graph run). Auditors can trace from the agent-level decision back to the individual inference calls that produced it. The timestamps on all three anchors fall within the same execution window, creating a verifiable causal chain.

Two-layer evidence chain

Assessor verification procedure

For a given agent execution, the ledger should contain: one graph-level anchor with agent_id set, plus one or more LLM-call anchors with timestamps falling within the graph anchor's time window.

Assessor tip

Query for graph-level anchors by agent_id, then query for LLM-call anchors within the same time window (graph start to graph end). The number of LLM-call anchors should match the expected number of LLM-calling nodes in the graph. If the graph has 3 LLM nodes but only 2 call anchors appear, a node may have been skipped or conditionally bypassed -- cross-reference with the graph topology to determine whether this is expected behavior or a finding.

7. Model-Agnostic Evidence

LangChain abstracts the LLM provider behind a uniform interface. The SWT3 callback handler extracts the real model identity from LangChain's internal serialization, regardless of which wrapper class you use. This means the witness anchor always records the actual model and provider, not the LangChain class name.

LangChain Provider Class Underlying Model Provider Tag in Evidence
ChatOpenAI gpt-4o openai
ChatAnthropic claude-sonnet-4 anthropic
ChatBedrock us.anthropic.claude-sonnet-4-20250514 bedrock
ChatGoogleGenerativeAI gemini-2.0-flash google
OllamaLLM llama3.2 langchain
ChatMistralAI mistral-large-latest mistral

This matters for multi-model pipelines. A single LCEL chain might route different queries to different models based on complexity, cost, or latency. The callback handler witnesses each call with the actual model used, so your evidence trail reflects reality -- not the abstraction layer.

Provider detection is best-effort for custom wrappers. If you have built a custom LangChain LLM class, the handler falls back to langchain as the provider tag. The model name is still extracted from the serialized kwargs. Override model_name in your custom class to ensure accurate evidence.

8. Regulatory Evidence Mapping

Every anchor minted through LangChain or LangGraph maps to specific regulatory requirements. The evidence is produced at inference time, not reconstructed after the fact.

Obligation Regulation SWT3 Evidence Procedure
Automatic logging of AI operations EU AI Act Art. 12 Per-inference anchor with model ID, prompt hash, response hash AI-INF.1
Transparency of AI system behavior EU AI Act Art. 13 Model identity and provider extracted from LangChain serialization AI-INF.1
Document data sources used by AI EU AI Act Art. 10 RAG retrieval provenance with document hashes and source IDs AI-RAG.1
Track AI system interfaces and dependencies NIST AI RMF MAP 1.5 Two-layer chain: graph execution + individual LLM calls AI-INF.1
Mechanisms to track AI behavior NIST AI RMF MN 2.2 Decision-level witnessing across all graph nodes AI-INF.1
Ongoing model monitoring SR 11-7 / SR 26-2 Model hash drift detection across time AI-INF.1
Audit events for AI decisions SOC 2 CC7.2 Cryptographic fingerprint per orchestrated decision AI-INF.1
Agent identity and accountability NIST AI RMF GV 1.1 agent_id tag on every graph-level anchor AI-ID.1
Audit workpaper guidance

How to use SWT3 anchors for orchestrated AI assessments

Assessor tip

Request a ledger export filtered by provider IN ("openai", "anthropic", "bedrock", "langchain", "google", "mistral") for the assessment period. For orchestrated applications: (1) confirm that graph-level anchors with agent_id exist for every agent in the system inventory, (2) confirm that per-LLM-call anchors exist within each graph execution window, (3) verify RAG anchors (AI-RAG.1) precede inference anchors when the system is documented as RAG-based. If the organization claims a RAG architecture but no AI-RAG.1 anchors exist, the retrieval step is unwitnessed -- a documentation gap or an implementation finding.

9. Quick Start

Install

# Python (LangChain + LangGraph)
pip install swt3-ai langchain-openai langgraph

# TypeScript (LangGraph only)
npm install @tenova/swt3-ai @langchain/langgraph

LangChain -- Minimal Example (Python)

from langchain_openai import ChatOpenAI
from swt3_ai import Witness
from swt3_ai.adapters.langchain import SWT3CallbackHandler

witness = Witness(
    endpoint="https://your-instance.example.com/api/v1/witness",
    api_key="axm_your_key",
    tenant_id="YOUR_TENANT",
)

llm = ChatOpenAI(model="gpt-4o", callbacks=[SWT3CallbackHandler(witness)])
response = llm.invoke("Summarize this document.")
witness.flush()

LangGraph -- Minimal Example (Python)

from langgraph.graph import StateGraph
from swt3_ai import Witness
from swt3_ai.adapters.langgraph import wrap_langgraph

witness = Witness(
    endpoint="https://your-instance.example.com/api/v1/witness",
    api_key="axm_your_key",
    tenant_id="YOUR_TENANT",
)

app = StateGraph(...).compile()  # your compiled graph
witnessed_app = wrap_langgraph(app, witness)
result = witnessed_app.invoke({"messages": [("user", "Process this request")]})
witness.flush()

LangGraph -- Minimal Example (TypeScript)

import { StateGraph } from '@langchain/langgraph';
import { Witness, wrapLangGraph } from '@tenova/swt3-ai';

const witness = new Witness({
  endpoint: 'https://your-instance.example.com/api/v1/witness',
  apiKey: 'axm_your_key',
  tenantId: 'YOUR_TENANT',
});

const app = new StateGraph({...}).compile(); // your compiled graph
const witnessedApp = wrapLangGraph(app, witness);
const result = await witnessedApp.invoke({ messages: [['user', 'Process this request']] });
await witness.flush();

10. References

Other Witnessing Guides