Full Agent Observability in Your Own Database with LangChain, LangSmith SDK, and DeepAgents
How to trace every LLM call, tool invocation, and sub-agent in LangChain DeepAgents — and store it all in your own MongoDB instead of LangSmith cloud.
Every LangChain agent call produces a tree of runs: the root agent, sub-agents, tool invocations, LLM calls. LangSmith traces this automatically. But what if you need the trace data in your own database?
We built a production system that traces every LLM call in LangChain DeepAgents and stores it in MongoDB — using only official SDK types, no custom BaseTracer subclasses, no field redeclaration. Here’s how.
The Problem
LangSmith cloud gives you tracing out of the box. Set LANGSMITH_TRACING=true, and every model.invoke(), agent.stream(), and tool call is captured.
But we needed:
- Data stays in our MongoDB — prompts, responses, business logic never leave our infrastructure
- Business context on runs — tenant ID, agent type, entity ID linked to each trace
- No custom tracer code — reuse the SDK’s run tree management, metadata inheritance, token extraction
- Works with DeepAgents — sub-agents, tool calls, the full tree
The naive approach is subclassing BaseTracer and reimplementing persistRun / _endTrace. This misses dotted_order propagation, metadata inheritance, token extraction, and time-to-first-token tracking. Don’t do this.
The Solution: LangSmithTracingClientInterface
The langsmith SDK defines a 2-method interface for persistence:
interface LangSmithTracingClientInterface {
createRun(run: RunCreate): Promise<void>;
updateRun(runId: string, run: RunUpdate): Promise<void>;
}
LangChainTracer from @langchain/core handles everything — run tree management, dotted_order, trace_id propagation, metadata/tag inheritance, token extraction from LLM outputs — and delegates storage to this interface.
By implementing it for MongoDB, you get the full SDK for free.
graph TD A["Your agent code"] -->|"callbacks: [tracer]"| B["LangChainTracer"] B -->|"run tree management"| C["RunTree"] C -->|"flat DTOs"| D["MongoDBLangSmithClient"] D -->|"createRun(RunCreate)"| E["MongoDB"] D -->|"updateRun(RunUpdate)"| E B -->|"metadata inheritance"| F["All child runs get traceId, tags"] B -->|"token extraction"| G["usage_metadata on LLM runs"]
Step 1: The MongoDB Client
Implement LangSmithTracingClientInterface. The createRun method receives a flat RunCreate DTO — no circular references, no child_runs tree, no sanitizeForBson. Just insert it.
import type { LangSmithTracingClientInterface } from 'langsmith';
import type { RunCreate, RunUpdate } from 'langsmith/schemas';
class MongoDBLangSmithClient implements LangSmithTracingClientInterface {
constructor(
private readonly storage: ObservabilityStorage,
private readonly businessContext?: { tenantId: ObjectId; entity: ObjectId },
) {}
async createRun(run: RunCreate): Promise<void> {
const { child_runs, ...doc } = run;
await this.storage.insertLangChainRun({
...doc,
businessContext: this.businessContext,
});
}
async updateRun(runId: string, run: RunUpdate): Promise<void> {
await this.storage.updateLangChainRun(runId, run);
}
}
The businessContext is optional metadata you attach to every run — tenant, agent type, whatever your app needs. It’s not part of LangChain’s model; it’s yours.
Step 2: The Factory Function
Create a factory that returns a standard LangChainTracer backed by your MongoDB client:
import { LangChainTracer } from '@langchain/core/tracers/tracer_langchain';
function createMongoDBTracer(
traceId: string,
storage: ObservabilityStorage,
businessContext?: { tenantId: ObjectId; entity: ObjectId },
metadata?: Record<string, unknown>,
): LangChainTracer {
const client = new MongoDBLangSmithClient(storage, businessContext);
return new LangChainTracer({
client,
projectName: 'default',
metadata: { traceId, ...metadata },
});
}
Use it anywhere you invoke a LangChain runnable:
const traceId = uuid7(); // time-ordered UUID from langsmith SDK
const tracer = createMongoDBTracer(traceId, storage, {
tenantId, entityId, agentType: 'sales-assistant',
});
await agent.stream(
{ messages: [{ role: 'user', content: message }] },
{ callbacks: [tracer] },
);
That’s it. Every run in the tree — root agent, sub-agents, LLM calls, tool invocations — is traced with correct dotted_order, trace_id, metadata inheritance, and token counts.
Step 3: The Storage Schema
Extend RunCreate from langsmith/schemas directly. Don’t redeclare its fields:
import type { RunCreate } from 'langsmith/schemas';
interface LangChainRunDoc extends RunCreate {
_id: ObjectId;
businessContext?: {
tenantId: ObjectId;
entityId: ObjectId;
agentType?: string;
};
createdAt: Date;
}
RunCreate already defines id, name, start_time, run_type, inputs, outputs, extra, tags, events, trace_id, dotted_order, parent_run_id. You add only your business fields.
Step 4: What You Get Automatically
Run Tree Management
LangChainTracer uses RunTree internally. When a run starts, it calls createRun with a flat DTO. When it ends, it calls updateRun with outputs, error, end_time, and extra metadata. You don’t manage the tree — the SDK does.
Metadata Inheritance
Pass metadata to the tracer, and it propagates to all child runs:
const tracer = createMongoDBTracer(traceId, storage, ctx, {
thread_id: conversationId, // for thread-level queries
agentType: 'pricing',
});
Every child run (sub-agents, tools, LLM calls) gets thread_id in its extra.metadata. Query by thread in MongoDB:
db.langchain_runs.find({ 'extra.metadata.thread_id': conversationId })
Token Extraction
LangChainTracer.onLLMEnd extracts usage_metadata from chat model outputs and stores it in run.extra.metadata.usage_metadata. No custom extraction needed.
Time-to-First-Token
For streaming LLM runs, LangChainTracer tracks first_token_time via the RunTree. Your updateRun call receives this in the RunUpdate.
Step 5: Querying
The root run (where parent_run_id is absent) IS the trace session. Query it directly:
// Get all runs for a trace
async findRunsByTraceId(traceId: string) {
return db.langchain_runs.find({ traceId }).sort({ dotted_order: 1 });
}
// Get root run (the session)
async findRootRun(traceId: string) {
return db.langchain_runs.findOne({
traceId,
parent_run_id: { $exists: false },
});
}
// Get runs by message ID (from extra.metadata)
async findRunsByMessageId(messageId: string) {
return db.langchain_runs.find({
'extra.metadata.message_id': messageId,
});
}
No joins, no separate session collection. One collection, one query.
Step 6: What’s in the Data
Each document in langchain_runs looks like this:
{
"_id": "...",
"id": "run-uuid-7",
"name": "ChatOpenAI",
"run_type": "llm",
"trace_id": "root-run-uuid",
"parent_run_id": "parent-uuid",
"dotted_order": "20260722T150322...1234.20260722T150322...5678",
"start_time": 1753216202000,
"end_time": 1753216203500,
"inputs": { "messages": [...] },
"outputs": { "generations": [...] },
"extra": {
"metadata": {
"ls_provider": "openai",
"ls_model_name": "gpt-4o",
"thread_id": "conversation-123",
"usage_metadata": {
"input_tokens": 1250,
"output_tokens": 340,
"total_tokens": 1590
}
}
},
"tags": ["sales-assistant", "pricing"],
"businessContext": {
"tenantId": "...",
"entityId": "...",
"agentType": "pricing"
},
"createdAt": "2026-07-22T15:03:22.000Z"
}
LLM config (provider, model) comes from extra.metadata.ls_provider and extra.metadata.ls_model_name — no snapshotting needed.
DeepAgents Integration
DeepAgents builds on LangGraph, which uses LangChain runnables. The same callbacks: [tracer] pattern traces the full deep agent tree:
graph TD A["DeepAgent"] -->|"callbacks"| B["LangChainTracer"] B --> C["Root run: agent"] C --> D["Sub-agent: pricing"] C --> E["Sub-agent: scheduling"] D --> F["LLM call: ChatOpenAI"] D --> G["Tool: calculatePrice"] E --> H["LLM call: ChatOpenAI"] E --> I["Tool: checkAvailability"] F -->|"parent_run_id = D"| J["langchain_runs"] G -->|"parent_run_id = D"| J H -->|"parent_run_id = E"| J I -->|"parent_run_id = E"| J C -->|"parent_run_id absent"| J J -->|"dotted_order sorts tree"| K["Waterfall rendering"]
Each run has parent_run_id linking to its parent, and dotted_order for hierarchical sorting. The full tree is reconstructable from a single traceId query.
MongoDB Indexes
db.langchain_runs.createIndex({ runId: 1 }, { unique: true });
db.langchain_runs.createIndex({ traceId: 1, dotted_order: 1 });
db.langchain_runs.createIndex({ parent_run_id: 1 });
db.langchain_runs.createIndex({ 'extra.metadata.message_id': 1 }, { sparse: true });
The traceId + dotted_order index covers the most common query: get all runs for a trace in execution order.
What We Deleted
After switching to this approach, we removed:
| What | Why |
|---|---|
Custom BaseTracer subclasses |
LangChainTracer handles everything |
sanitizeForBson function |
Flat DTOs have no circular refs |
Manual dotted_order / trace_id propagation |
SDK handles it |
Separate trace_sessions collection |
Root run IS the session |
| LLM config snapshot fields | Derived from extra.metadata at query time |
| 13 session CRUD methods | Replaced by root-run queries |
Common Mistakes
Don’t subclass BaseTracer directly. It’s abstract and misses run tree management. Use LangChainTracer with a custom client.
Don’t store Run objects (with child_runs). They have circular references. LangChainTracer already flattens to RunCreate / RunUpdate DTOs.
Don’t redeclare BaseRun fields in custom types. Extend RunCreate from langsmith/schemas directly.
Don’t snapshot LLM config on every run. The provider and model are in extra.metadata.ls_provider and extra.metadata.ls_model_name. Derive at query time.
Don’t forget thread_id on all runs. Pass it in RunnableConfig.metadata. LangChainTracer propagates it to children. Without it, thread-level queries fail.
Related Reading
- Executable Skills for Agents Are Better Than Docs — why agents need executable code, not documentation
- OpenWiki vs QuotyAI — two DeepAgents projects, two architectures
- Determinism as Infrastructure — why deterministic execution matters for production AI
- Codex for Sales — running AI-generated business logic in production
Frequently Asked Questions
Why not just use LangSmith cloud for tracing? Data sovereignty. Your LLM prompts, responses, and business logic leave your infrastructure. For regulated industries or competitive sensitivity, keeping trace data in your own database is non-negotiable. You also avoid per-trace pricing at scale.
What is the LangSmithTracingClientInterface?
A 2-method interface from the langsmith SDK: createRun and updateRun. LangChainTracer delegates all persistence to this interface. By implementing it for MongoDB, you get the full SDK’s run tree management, metadata inheritance, and token extraction — while storing data locally.
Why extend RunCreate instead of creating custom types?
RunCreate from langsmith/schemas is the official LangChain persistence DTO. It’s what LangChainTracer sends to the client. By extending it directly, you eliminate field redeclaration, avoid drift from SDK updates, and get type-safe persistence for free.
Does this work with DeepAgents sub-agents?
Yes. DeepAgents builds on LangGraph, which uses LangChain runnables. The same LangChainTracer with callbacks traces the full tree: root agent, sub-agents, tool calls, LLM calls. Each sub-agent’s runs are linked via parent_run_id and dotted_order.
How do you handle thread-level grouping without LangSmith?
Pass thread_id in the RunnableConfig metadata. LangChainTracer propagates it to all child runs. Query by thread_id in your MongoDB collection. This is identical to how LangSmith threads work — you just own the storage.
Tags: langchain langsmith deep-agents observability mongodb llm-tracing ai-agents typescript langsmith-tracing-client runcreate langchain-tracer
Found this useful? Share it.
Related articles
OpenWiki vs QuotyAI: Two LangChain DeepAgents Projects, Two Architectures
OpenWiki uses LangChain DeepAgents to generate agent documentation for codebases. QuotyAI uses the same createDeepAgent API to generate executable TypeScript business logic. A technical comparison of agent architecture, subagent patterns, and runtime execution.
Read articleSpec-Driven vs Code-First vs Chat-to-Code: Three Philosophies for Teaching AI About Your Business
Three approaches to building AI agent knowledge — spec-driven development, code-first documentation, and chat-to-code generation. A comparison of Spec Kit, OpenWiki, Mintlify, and QuotyAI with real pitfalls, products, and convergence signals.
Read articleCodex for Sales: Running AI-Generated Business Logic in Production
AI-generated pricing and scheduling code needs git-like lifecycle management: staging, validation, approval, rollback. Real patterns from a production multi-agent system.
Read article