Skip to content

feat(instrument): LangChain LCEL support (RunnableSequence/Parallel/Lambda/Passthrough/Branch)#131

Closed
mmercuri wants to merge 1 commit into
feat/instrument-frameworks-orchestrationfrom
feat/instrument-langchain-lcel-support
Closed

feat(instrument): LangChain LCEL support (RunnableSequence/Parallel/Lambda/Passthrough/Branch)#131
mmercuri wants to merge 1 commit into
feat/instrument-frameworks-orchestrationfrom
feat/instrument-langchain-lcel-support

Conversation

@mmercuri

Copy link
Copy Markdown
Contributor

Summary

Adds first-class LangChain Expression Language (LCEL) tracing to the LangChain adapter, fixing spec 04b §1 weakness #4. LCEL is the dominant authoring pattern as of langchain-core 0.2+ (the prompt | llm | parser pipe pattern), and the adapter previously treated LCEL pipelines as opaque unless a langgraph_node marker was present.

The adapter now:

  • Detects all five LCEL primitives from on_chain_start callbacks: RunnableSequence, RunnableParallel<a,b,...>, RunnableLambda, RunnablePassthrough, RunnableBranch
  • Tracks the composition tree across nested invocations (sequence-in-parallel-in-sequence resolves correctly with depth and parent linkage)
  • Decodes composition position tags (seq:step:N, map:key:K, branch:N, condition:N) so children know where they sit in the parent
  • Emits per-runnable events at L1 (agent.input/agent.output) and L2 (agent.code) with kind, depth, position, parallel-branch keys, passthrough markers, and SHA-256 lambda fingerprints
  • Emits a synthetic chain.composition snapshot as an agent.code event at root completion (success or error) so the dashboard can render the full executed DAG without reconstructing it from the per-step stream
  • Preserves LangGraph behavior: when metadata["langgraph_node"] is present, the existing LangGraph attribution path runs and LCEL tracking is suppressed for that subtree (no double-emission)
  • Propagates RunnableConfig opaquely: tags and metadata flow through the callback chain; user-supplied tags don't confuse position parsing

Files

  • src/layerlens/instrument/adapters/frameworks/langchain/lcel.py (new, 365 lines) — RunnableKind, LCELRunnableTracker, CompositionPosition, LCELNode, helper functions
  • src/layerlens/instrument/adapters/frameworks/langchain/callbacks.py (modified) — on_chain_start/end/error wired through the tracker
  • src/layerlens/instrument/adapters/frameworks/langchain/__init__.py — public LCEL surface
  • tests/instrument/adapters/frameworks/langchain/test_lcel.py (new, 43 tests)
  • samples/instrument/langchain/lcel_main.py (new) — runs offline, no API key
  • docs/adapters/frameworks-langchain-lcel.md (new) — coverage matrix + event reference

Test plan

  • uv run pytest tests/instrument/adapters/frameworks/langchain/test_lcel.py -x43/43 passed
  • uv run pytest tests/instrument/adapters/frameworks/80/80 passed (43 new + 37 baseline; no regression in the autogen/crewai/langfuse adapters)
  • uv run mypy --strict src/layerlens/instrument/adapters/frameworks/langchain/Success: no issues found in 7 source files
  • uv run ruff check src/.../langchain/ tests/.../langchain/All checks passed
  • uv run pyright src/.../langchain/0 errors, 0 warnings, 0 informations
  • python -m samples.instrument.langchain.lcel_main — exit 0, captures 28 events (9 agent.input, 9 agent.code per-step, 1 chain.composition snapshot, 9 agent.output)

Sample output snippet

== LCEL agent.input events (9) ==
- sequence: RunnableSequence
  - parallel: RunnableParallel<context,question> [sequence.step=1]
    - other: fake_retriever [parallel.key=context]
    - passthrough: RunnablePassthrough [parallel.key=question]
  - other: fake_llm [sequence.step=2]
  - other: fake_parser [sequence.step=3]
  - branch: RunnableBranch [sequence.step=4]
    - other: is_short_question [branch.condition=1]
    - lambda: RunnableLambda [branch.body=default]

== chain.composition snapshot (1) ==
  root=sequence ('RunnableSequence')  nodes=9  max_depth=2  status=ok
  kind_counts: {'sequence': 1, 'branch': 1, 'lambda': 1, 'other': 4, 'parallel': 1, 'passthrough': 1}

Spec reference

docs/incubation-docs/adapter-framework/04-per-framework-specs/04b-langchain-adapter-spec.md §1 weakness #4 and §4 (LCEL Support).

…ugh/Branch)

Adds LangChain Expression Language (LCEL) tracing to the LangChain
adapter, fixing spec 04b §1 weakness #4: LCEL is the dominant authoring
pattern in langchain-core 0.2+, but the adapter's chain handlers were
written for the legacy Chain API and treated LCEL pipelines as opaque
unless a langgraph_node marker was present.

What is added
-------------
- src/layerlens/instrument/adapters/frameworks/langchain/lcel.py
  - RunnableKind enum (sequence/parallel/lambda/passthrough/branch/other)
  - detect_runnable_kind() — classify from the on_chain_start `name` kwarg
  - parse_composition_tag() — decode seq:step:N / map:key:K /
    branch:N / condition:N tags into a CompositionPosition
  - parse_parallel_branches() — extract branch keys from
    "RunnableParallel<a,b>" name strings
  - fingerprint_lambda() — deterministic SHA-256 over
    (name, depth, position) for diff-friendly lambda identity
  - LCELRunnableTracker — per-handler state machine that records the
    active runnable hierarchy across nested invocations and produces
    composition payloads at root completion
- src/layerlens/instrument/adapters/frameworks/langchain/callbacks.py
  - on_chain_start/end/error wired through the tracker; LangGraph node
    behavior pre-empts LCEL tracking to avoid double-emission
  - Per-runnable agent.input (L1), agent.output (L1), agent.code (L2)
    events with composition metadata
  - Synthetic chain.composition snapshot emitted as an agent.code event
    with `kind="chain.composition"` at root completion (success or
    error) so debuggers see the full executed graph in one record
  - Tracker reset on disconnect()
- tests/instrument/adapters/frameworks/langchain/test_lcel.py
  - 43 tests covering all five Runnable types, tag parsing, hierarchy
    construction, composition payload, error paths, capture-config
    gating, LangGraph non-regression, RunnableConfig propagation, and
    fingerprint determinism
- samples/instrument/langchain/lcel_main.py
  - Runnable LCEL pipeline (RAG-style: parallel | lambda | lambda |
    branch) demonstrating all five primitives; runs offline with
    deterministic stand-ins, no API key required
- docs/adapters/frameworks-langchain-lcel.md
  - Coverage matrix for all five primitives + RunnableConfig
  - Event reference (agent.input / agent.output / agent.code +
    composition snapshot schema)
  - Capture-config gating, hierarchy/depth semantics, lambda
    fingerprinting, and LangGraph interaction notes

Validation
----------
- uv run pytest tests/instrument/adapters/frameworks/langchain/ -x
  -> 43 passed
- uv run pytest tests/instrument/adapters/frameworks/ (full subtree)
  -> 80 passed (43 new LCEL + 37 baseline; no regression)
- uv run mypy --strict src/layerlens/instrument/adapters/frameworks/langchain/
  -> Success: no issues found in 7 source files
- uv run ruff check src/.../langchain/ tests/.../langchain/
  -> All checks passed
- uv run pyright src/.../langchain/
  -> 0 errors, 0 warnings, 0 informations
- python -m samples.instrument.langchain.lcel_main
  -> exit 0, 28 events captured (9 agent.input, 9 agent.code, 1
     chain.composition snapshot, 9 agent.output)

Spec reference: docs/incubation-docs/adapter-framework/04-per-framework-specs/04b-langchain-adapter-spec.md §1 weakness #4 and §4
@mmercuri mmercuri requested a review from m-peko April 27, 2026 06:03
@m-peko m-peko closed this May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants