fix(langchain): preserve inherited callback handlers - #1029
Conversation
The patched CallbackManager._configureSync assumed inheritableHandlers was always an array, silently dropping handlers when LangChain passed a CallbackManager instance instead (e.g. via runManager.getChild()). It also injected a new TraceloopCallbackHandler unconditionally, duplicating it on child runnable invocations. Normalize inheritableHandlers by extracting the handler list whether it is an array or a CallbackManager instance, and filter out any existing traceloop_callback_handler before adding the current one.
📝 WalkthroughWalkthroughThe patch for LangChain's ChangesCallback handler patch fix
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant RunnableWithMessageHistory
participant CallbackManager
participant PatchedConfigureSync
participant OriginalConfigureSync
RunnableWithMessageHistory->>CallbackManager: getChild()
CallbackManager->>PatchedConfigureSync: _configureSync(inheritableHandlers, localHandlers)
PatchedConfigureSync->>PatchedConfigureSync: extract existing handlers (array or object)
PatchedConfigureSync->>PatchedConfigureSync: filter out duplicate traceloop_callback_handler
PatchedConfigureSync->>PatchedConfigureSync: append TraceloopCallbackHandler
PatchedConfigureSync->>OriginalConfigureSync: call(updatedHandlers, localHandlers)
OriginalConfigureSync-->>RunnableWithMessageHistory: configured CallbackManager with preserved handlers
Related issues: Suggested labels: bug, instrumentation-langchain Suggested reviewers: nirga Poem: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/instrumentation-langchain/test/callback-manager-handlers.test.ts (1)
43-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShared static patch state defeats per-test isolation.
FakeCallbackManageris a single module-level class. OncebeforeEachin the first test patches_configureSyncand sets_traceloopPatched, the secondbeforeEach()call tomanuallyInstrumentbecomes a no-op (patchCallbackManagerskips if_traceloopPatchedis already set), so the second test actually exercises the closure/selfcaptured by the first test's instrumentation instance, not the fresh one created in its ownbeforeEach. The assertions still pass here because the tested logic is stateless w.r.t.self, but this masks the fact that per-test setup doesn't actually re-patch, and could hide bugs in future tests that depend on instance-specific state (e.g., tracer/config differences).Consider resetting
_traceloopPatched(or defining a freshFakeCallbackManagerclass) per test to ensure genuine isolation.♻️ Proposed fix for test isolation
beforeEach(() => { const provider = new NodeTracerProvider(); instrumentation = new LangChainInstrumentation(); instrumentation.setTracerProvider(provider); + delete (FakeCallbackManager as unknown as Record<string, unknown>) + ._traceloopPatched; instrumentation.manuallyInstrument({ callbackManagerModule: { CallbackManager: FakeCallbackManager }, }); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/instrumentation-langchain/test/callback-manager-handlers.test.ts` around lines 43 - 100, The test setup for FakeCallbackManager is sharing patched static state across cases, so the second beforeEach does not truly re-run manuallyInstrument and may reuse the first LangChainInstrumentation instance’s patched _configureSync closure. Update the test isolation around patchCallbackManager/_traceloopPatched by resetting that static flag or creating a fresh FakeCallbackManager per test, so each beforeEach gets a clean patch and the assertions exercise the current instrumentation instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/instrumentation-langchain/test/callback-manager-handlers.test.ts`:
- Around line 43-100: The test setup for FakeCallbackManager is sharing patched
static state across cases, so the second beforeEach does not truly re-run
manuallyInstrument and may reuse the first LangChainInstrumentation instance’s
patched _configureSync closure. Update the test isolation around
patchCallbackManager/_traceloopPatched by resetting that static flag or creating
a fresh FakeCallbackManager per test, so each beforeEach gets a clean patch and
the assertions exercise the current instrumentation instance.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a7e98061-5714-4ff1-8df1-51017e6f3d61
📒 Files selected for processing (2)
packages/instrumentation-langchain/src/instrumentation.tspackages/instrumentation-langchain/test/callback-manager-handlers.test.ts
The patched
_configureSyncinpatchCallbackManagerassumedinheritableHandlerswas always an array, so it discarded anyCallbackManagerinstance passed in and dropped inherited handlers such asRootListenersTracer, breakingRunnableWithMessageHistorychat history persistence. It also added a newTraceloopCallbackHandlerunconditionally, duplicating it on child runnable invocations and doubling emitted spans. This normalizesinheritableHandlersby extracting its handler list whether it is an array or aCallbackManagerinstance, and filters out any existingtraceloop_callback_handlerbefore adding the current one. Added a focused test covering both cases — an array input with an existing Traceloop handler, and aCallbackManagerinstance input with a sentinel handler — both of which fail on the pre-fix code and pass after the fix. Verified with the package's test suite, lint, and build.Fixes #1016
Summary by CodeRabbit
Bug Fixes
Tests