callbacks — Observer pattern for tracing, logging, streaming, metrics

Observer pattern for tracing, logging, streaming, metrics

Module langchain_classic/callbacks/ — Observer pattern for tracing, logging, streaming, metrics. Part of the Cross-cutting group.

🎯 Why This Module Exists

This module is documented from the callbacks_deep.md Phase 1B research artifact. See sections below for design decisions, algorithms, error philosophy, performance, and evolution clues.

45
Source files
0
Sub-directories
Cross-cutting
Category
Python
Language

Observer Tracing LangSmith

Architecture

flowchart LR
  subgraph Core["langchain_classic/callbacks/"]
    BASE["Base abstraction"]
    IMPLS["Concrete implementations"]
    BASE --> IMPLS
  end
  subgraph Up["Upstream callers"]
    U1["chains/"]
    U2["agents/"]
  end
  subgraph Down["Downstream"]
    D1["langchain_core"]
    D2["External providers"]
  end
  Up --> Core
  Core --> Down

Data Flow

sequenceDiagram
  participant Caller as Caller
  participant Callback as callbacks
  participant Core as langchain_core
  participant Ext as External
  Caller->>Callback: invoke(input)
  Note over Callback: Validate & route
  Callback->>Core: delegate primitive
  Core->>Ext: provider call
  Ext-->>Core: response
  Core-->>Callback: typed output
  Callback-->>Caller: result

Existence rationale

callbacks/ implements the observer pattern that lets every chain, agent, retriever, LLM call, and tool emit lifecycle events (on_*_start, on_*_end, on_*_error, on_*_new_token) to N subscribers without coupling. Without it, streaming UIs, distributed tracing (LangSmith), token cost accounting, and verbose=True debug printing would each need bespoke instrumentation in every component. By centralising on BaseCallbackHandler, langchain_classic lets a single CallbackManager fan an event out to a stdout printer, a file logger, a LangSmith tracer, and a streaming WebSocket — concurrently.

Design decisions

DecisionChoice madePlausible alternativesRationale
Event surfaceOne method per (event × component): on_chain_start, on_llm_new_token, …Generic on_event(name, payload)Static type + IDE autocomplete trump flexibility; payload shapes differ
Manager-per-callCallbackManager.configure(...) builds a fresh manager from local + global handlersSingleton managerPer-run isolation + nested run trees with parent IDs
Inheritance for tracersRun trees: each child manager carries parent_run_idFlat listLets tracers reconstruct chain-of-thought call graph
Sync + async parityBaseCallbackHandler (sync) + AsyncCallbackHandlerAsync onlyStreaming UIs in sync code need callbacks too
ignore_* flagsEach handler has ignore_chain, ignore_llm, …Subclass-time pruningLets users wire one handler for one event class without subclassing
Verbose handler is globalset_verbose(True) adds StdOutCallbackHandler to globalsPass per-callMirrors logging.basicConfig idiom; matches user mental model
Tracers as handlersLangChainTracer is a BaseCallbackHandlerCustom hook systemSingle dispatch path; tracers are just another subscriber

Algorithm deep-dives

1. Manager configuration & dispatch

Trace.

  1. Caller calls manager = CallbackManager.configure(inheritable_callbacks, local_callbacks, verbose, inheritable_tags, …).
  2. configure merges global handlers (langchain_classic.globals), inherited handlers from a parent run, and call-local handlers, dedup-by-id.
  3. If verbose and no ConsoleCallbackHandler → adds one.
  4. If env LANGCHAIN_TRACING_V2=true and no LangChainTracer → adds one.
  5. Returned manager carries run_id, parent_run_id, tags, metadata.

On on_chain_start(...):

  1. Generates a new run_id (UUID4).
  2. For each handler with not handler.ignore_chain: try handler.on_chain_start(serialized, inputs, run_id, parent_run_id, tags, metadata).
  3. Exceptions in handlers are caught + logged, never propagated — handler bugs must not crash the run.
  4. Returns a CallbackManagerForChainRun whose parent_run_id = run_id so children form a tree.

Why per-call. Streaming UIs need a different handler set than batch processing; tracers need fresh run IDs per top-level call. A single global manager would either leak handlers across runs or require explicit teardown.

2. Streaming token dispatch

AsyncIteratorCallbackHandler is the bridge between LLM on_llm_new_token and an async generator the application consumes:

  1. Maintains a asyncio.Queue.
  2. on_llm_new_token(token) → queue.put_nowait(token).
  3. on_llm_end → queue.put_nowait(SENTINEL).
  4. The user's async loop iterates await queue.get() until SENTINEL.

This decouples LLM emission cadence from the consumer's read cadence — the LLM streams as fast as the network allows, the UI catches up on its own clock.

Error philosophy

Handler exceptions are quarantined. BaseCallbackManager._handle_event swallows handler exceptions and logs them — by design, a broken tracer must not break a working chain. Tools and chains, by contrast, propagate up — the application decides on retry.

Performance characteristics

Step-by-Step Walkthrough

Follow a typical operation through the callbacks/ module:

  1. Caller invokes the public surface — usually .invoke(input) on a Runnable, or a legacy __call__(inputs_dict) on a Chain subclass.
  2. Pydantic validation — every entry point is a BaseModel; constructor args, input dicts, and tool args_schema are validated before any expensive work runs. Fail-fast on type errors.
  3. CallbackManager.configure — a fresh manager is built per call, fans on_*_start events to all subscribers (LangSmith tracer, stdout handler, custom logging), and stamps a UUID4 run_id for tree reconstruction.
  4. callbacks-specific logic — the module's core method runs: prompt rendering, retrieval, tool execution, memory load, document split, etc. See the deep-dive sections above.
  5. Provider call — typically a single HTTP request to OpenAI/Anthropic/etc., or a local SDK call. Retries are handled by tenacity with exponential backoff and a configurable max-attempts knob.
  6. Streaming fan-out — if the provider streams tokens, each chunk fires on_llm_new_token through the callback manager so downstream observers (UI, log, tracer) see partial output.
  7. OutputParser pass — if a parser is attached, the raw provider output is parsed into a structured Python object; OutputParserException may trigger a retry chain (RetryWithErrorOutputParser).
  8. Memory write-back — for chains/agents that have a BaseMemory attached, save_context(inputs, outputs) persists the turn before returning to the caller.
  9. on_*_end fan-out — the callback manager closes the run span; tracers serialise the Run tree and ship it to LangSmith asynchronously.
  10. Result returned — caller receives a typed object (AIMessage, dict, BaseModel, list[Document], ...) matching the module's declared output schema.

What Could Go Wrong

Failure Mode Trigger Detection Recovery
Provider 5xx / rate-limit OpenAI/Anthropic/etc. transient error HTTP status from SDK tenacity retries with exponential backoff up to max_retries; then surface to caller
Output parser failure LLM returned malformed JSON / wrong schema OutputParserException in parser RetryWithErrorOutputParser re-prompts with the parser error included; or surface
Pydantic validation Caller passes wrong-shaped dict ValidationError at __init__ / invoke Surface immediately — no fallback is correct here
Callback handler exception User-supplied tracer raises Try/except around each fan-out Logged and swallowed by default; raise_error=True to opt in to propagation
Provider auth missing OPENAI_API_KEY unset SDK AuthenticationError Surface with a helpful message; no retry (would just fail again)

Performance Profile

Hot Paths

Resource Usage

Directory Map

Source tree for callbacks
callbacks/
├── __init__.py
├── aim_callback.py
├── argilla_callback.py
├── arize_callback.py
├── arthur_callback.py
├── base.py
├── clearml_callback.py
├── comet_ml_callback.py
├── confident_callback.py
├── context_callback.py
├── file.py
├── flyte_callback.py
├── human.py
├── infino_callback.py
├── labelstudio_callback.py
├── llmonitor_callback.py
├── manager.py
├── mlflow_callback.py
├── openai_info.py
├── promptlayer_callback.py
├── sagemaker_callback.py
├── stdout.py
├── streaming_aiter.py
├── streaming_aiter_final_only.py
├── streaming_stdout.py
├── streaming_stdout_final_only.py
├── streamlit/
├── tracers/
├── trubrics_callback.py
├── utils.py
├── wandb_callback.py
└── whylabs_callback.py