Skip to content

LangChain

LangChain routes every model call, tool call, retrieval and chain step through its callback manager, so a BaseCallbackHandler sees the whole run without any change to how your chain calls its models. That is the seam this integration uses — no proxy, no wrapped model class, no base-URL swap. The same handler also covers LangGraph, which executes through the same callback manager.

Install and wire

bash
pip install "cognipeer-observability[langchain]"
bash
npm install @cognipeer/observability
python
import cognipeer_observability as cognipeer
from cognipeer_observability.langchain import CognipeerCallbackHandler

cognipeer.init(agent={"name": "support-bot"})

chain.invoke(
    {"question": "where is my order?"},
    config={"callbacks": [CognipeerCallbackHandler(thread_id="conv-42")]},
)
ts
import { init } from '@cognipeer/observability';
import { CognipeerCallbackHandler } from '@cognipeer/observability/langchain';

init({ agent: { name: 'support-bot' } });

await chain.invoke(
  { question: 'where is my order?' },
  { callbacks: [new CognipeerCallbackHandler({ threadId: 'conv-42' })] },
);

One handler instance produces one session per root run. Reusing the same handler across several invoke() calls gives you one session per call, grouped by thread id — which is what the Threads view expects.

Options

Both SDKs take the same options; the Python names are snake_case.

OptionDefaultMeaning
client / clientprocess-wideSend through a specific client instead of the one init() created
session / sessionWrite into an existing TraceSession. The handler never ends a session it did not create
session_id / sessionIdgeneratedFixed session id. Pass the same value again to append to an existing run
thread_id / threadIdfrom run metadataConversation id — see below
agent / agentfrom init(){"name": …, "version": …, "model": …} for the session
capture_chains / captureChainstrueRecord chain and graph-node runs as spans. Plumbing runnables are always skipped
metadata / metadata{}Extra metadata attached to every event

Two module-level helpers:

FunctionPurpose
cognipeer_config(thread_id, …) / cognipeerConfig(threadId, …)Build a RunnableConfig with the handler attached and the thread id plumbed correctly
install_langchain_tracing(…) / installLangChainTracing(…)Attach a handler to every LangChain run in the process

Global installation

python
from cognipeer_observability.langchain import install_langchain_tracing
install_langchain_tracing(agent={"name": "support-bot"})
ts
await installLangChainTracing({ agent: { name: 'support-bot' } });

This uses LangChain's global configure hook, so no per-invoke config is needed at all.

Not for concurrent servers

Every run in the process shares one handler, and a handler carries per-run state. That is right for a script, a notebook or a batch job, and wrong for a web server handling concurrent unrelated conversations — their spans will interleave into the same session. There, pass a per-invocation handler via cognipeer_config() / cognipeerConfig() instead.

The hook is backed by contextvars (Python) and AsyncLocalStorage (JS), so it must be installed before the work you want traced is spawned. On @langchain/core below 0.3.0 the @langchain/core/context entry point does not exist; the call warns and returns the handler unattached.

Thread grouping

Pass thread_id / threadId to the handler, or use the config helper:

python
from cognipeer_observability.langchain import cognipeer_config

agent.invoke(state, config=cognipeer_config("conv-42"))
ts
import { cognipeerConfig } from '@cognipeer/observability/langchain';

await agent.invoke(state, cognipeerConfig('conv-42'));

The helper exists because of a real version cliff. Up to langchain-core 1.2.x and @langchain/core 1.1.38, ensure_config() copied every primitive configurable key — including LangGraph's thread_id — into config.metadata, where any callback handler could read it. From langchain-core 1.3.0 and @langchain/core 1.1.40 only model and checkpoint_ns are promoted; the rest is routed to a channel applied exclusively to LangSmith's own tracer. A third-party handler reading metadata["thread_id"] gets nothing on current versions.

cognipeer_config() writes the thread id to both configurable (where LangGraph's checkpointer needs it) and metadata (where the handler can still see it), so it is correct on every version.

Without a thread id each run is its own orphan session. Everything else still works — you just lose conversation grouping.

What is captured

DataCapturedNotes
PromptsFull message list from on_chat_model_start, one section per message with its role
CompletionsIncluding assistant tool_calls, rendered as tool_call sections
Token usageusage_metadata first, legacy llm_output.token_usage as fallback
Cached input tokensFrom usage_metadata.input_token_details.cache_read. Its input_tokens already includes cached tokens, which is exactly what Console's pricing expects
Tool callsArguments and results. Python gets structured inputs; JS receives a JSON string and parses it
Tool definitionsRead from invocation_params.tools / .functions, normalised to {name, description, parameters}. Verified live against ChatOpenAI().bind_tools([...])
Model namemetadata.ls_model_nameinvocation_params.modelllm_output.model_name
HierarchyBuilt from run_id / parent_run_id; skipped plumbing runs are remapped to the nearest recorded ancestor
ErrorsException attached to the failing step and the session marked failed
RetrievalQuery plus the returned documents as a retrieval event

Tool definitions are the interesting one

LangChain strips tools and functions out of the tracing metadata it builds — but the unfiltered invocation_params still reaches handlers. That makes a callback handler the only place a third-party integration can see the tool menu the model was actually offered, which is frequently the largest single line item in an agent's prompt bill.

Version support

The handler reads every payload defensively and works from LangChain 0.1 through 1.x. What changes with version:

VersionBehaviour
Python langchain-core
< 0.2.2No standardised token usage. Only provider-specific llm_output, so usage is often absent and cached tokens are unobtainable
0.2.2AIMessage.usage_metadata lands — normalised usage across providers
0.2.5metadata["ls_model_name"] / ls_provider become available
0.3.xusage_metadata gains input_token_details.cache_read
1.2.0tool_call_id passed to on_tool_start, recorded as toolExecutionId
1.3.0configurable no longer copied into metadata — use cognipeer_config()
JS @langchain/core
< 0.2.9No usage_metadata; only llmOutput.tokenUsage
0.2.0ls_model_name / ls_provider
0.3.0@langchain/core/context appears, enabling installLangChainTracing
~1.1.15handleChainStart's .d.ts argument order diverges from runtime (handled)
~1.1.25toolCallId appended to handleToolStart
1.1.40configurable no longer copied into metadata — use cognipeerConfig()

Gotchas

Streaming reports no tokens unless you ask for them. Usage is read at on_llm_end. On a streaming call the provider only reports it when explicitly enabled — ChatOpenAI(stream_usage=True) / new ChatOpenAI({ streamUsage: true }) — and some providers never do. The integration deliberately leaves the token fields absent rather than writing zero, so an unpriced call shows up as unknown instead of silently under-reporting spend.

Python: the handler sets run_inline = True. Under an async run LangChain otherwise dispatches sync handlers onto a ten-worker thread pool with a copy of the context and no ordering guarantee, which reorders spans and breaks the start/end pairing this handler depends on. Running inline costs a negligible amount of time on the calling thread and makes the trace correct.

JS: callbacks are fire-and-forget by default. awaitHandlers defaults to false, so LangChain queues handler bodies without awaiting them. The handler opts in with _awaitHandler: true. On a platform that freezes the process the moment a response is returned — Lambda, Vercel, Cloudflare Workers — also do this before returning:

ts
import { awaitAllCallbacks } from '@langchain/core/callbacks/promises';
import { flush } from '@cognipeer/observability';

await awaitAllCallbacks();
await flush();

Handler exceptions are swallowed by LangChain. Both runtimes log and continue unless raise_error is set, which means a broken exporter looks like "tracing just doesn't work". The handler contains its own failures and reports them through the client's logger — run with COGNIPEER_DEBUG=1 to see them.

serialized is not an identity. It is frequently {}, None, or a not_implemented stub, so labels come from run_name and metadata first. Do not expect a node's class name to appear reliably.

Multi-prompt batches fork run ids. A .batch() or multi-prompt .generate() produces N sibling runs sharing one parent, of which only the first uses the caller-supplied run id. Each becomes its own event.

Legacy agent callbacks never fire. on_agent_action / on_agent_finish (Python) and handleAgentAction / handleAgentEnd (JS) are AgentExecutor-only and are never emitted by modern tool-calling agents or LangGraph. Tool correlation is built from run ids, not from those.

Differences between the two SDKs

PythonTypeScript
Tool-call idFirst-class toolExecutionId on the eventRecorded as metadata.toolCallId
Global installinstall_langchain_tracing() — synchronousinstallLangChainTracing()async (@langchain/core/context is dynamically imported)
Active session accessorhandler.session propertyhandler.session getter

Verify it worked

Run one invocation, then open Tracing → Sessions. You should see a session named after agent.name, containing:

  • one ai_call event per model call, labelled with the model id, carrying the prompt and completion as message sections and a Tool Definitions badge list when tools are bound;
  • one tool_call event per tool, with Arguments and Result sections;
  • token counts on each model call, and totals on the session header.

If nothing appears:

bash
COGNIPEER_DEBUG=1 python your_agent.py

Debug mode logs every HTTP POST the exporter makes and its status code. See Troubleshooting for what each failure mode looks like.

Community edition is AGPL-3.0. Commercial licensing and support are available separately.