Docs / Integrations / LangChain

LangChain

Mesedi ships as a LangChain callback handler, in both Python and TypeScript. Wrap your entry function with mesedi.wrap and attach the handler to LangChain's existing callbacks slot. Every LLM invocation and tool call inside the wrap flows into Mesedi as an llm_call or tool_call event.

The examples below are TypeScript. Skip to Python if that is your stack.

Install

npm install mesedi@^0.7 @langchain/core

Mesedi's adapter treats @langchain/core as an optional peer dependency. If you never import mesedi/integrations/langchain, the library is not loaded and there is no install cost for non-LangChain customers. Peer range is >=0.3.0 <0.5.0.

Wire it up

import { configure, wrap } from "mesedi";
import { MesediLangChainCallbackHandler } from "mesedi/integrations/langchain";
import { ChatOpenAI } from "@langchain/openai";

// Configure Mesedi once (usually at app startup).
configure({ apiKey: process.env.MESEDI_API_KEY });

export const runAgent = wrap(
  { agentName: "support-triage" },
  async (question: string) => {
    const llm = new ChatOpenAI({ model: "gpt-4o" });
    const result = await llm.invoke(
      [{ role: "user", content: question }],
      { callbacks: [new MesediLangChainCallbackHandler()] },
    );
    return String(result.content);
  },
);

wrap() owns the execution boundary (started / completed / crash signature). The callback handler owns the intra-execution events. Cross-cutting Mesedi features (per-project detector thresholds, custom security patterns, hard-halt budgets) all keep working through LangChain runs without any additional wiring.

Python

The Python SDK ships the same adapter against langchain-core. Note the class name differs: it is MesediCallbackHandler in Python and MesediLangChainCallbackHandler in TypeScript.

pip install "mesedi[langchain]"
import os
import mesedi
from mesedi.integrations.langchain import MesediCallbackHandler

mesedi.configure(api_key=os.environ["MESEDI_API_KEY"])

@mesedi.wrap
def run_agent(question: str) -> str:
    chain = build_chain()
    result = chain.invoke(
        {"input": question},
        config={"callbacks": [MesediCallbackHandler()]},
    )
    return result["output"]

The adapter imports BaseCallbackHandler from langchain_core.callbacks and falls back to the legacy langchain.callbacks.base location, so both modern and older installs work. Peer range is langchain-core>=0.1. Importing mesedi never requires LangChain to be installed.

Not yet supported in the Python adapter: async callbacks (sync only), streaming attribution (on_llm_new_token is ignored, you see the assembled response), per-chain depth tracking (on_chain_start fires but is discarded because @mesedi.wrap owns the boundary), and multi-modal message blocks (the text part is extracted, the rest dropped).

Where to attach the handler

The callbacks: option exists on every LangChain runnable's invoke(), stream(), and batch() methods, and on the RunnableConfig passed to agent executors. Attach one MesediLangChainCallbackHandler per top-level call; LangChain propagates it to every nested runnable automatically.

Callback-to-event mapping

LangChain fires typed callbacks for every LLM invocation and tool call. The handler translates each pair (start + end, or start + error) into the corresponding Mesedi event on the end/error callback.

LangChain callbackMesedi eventNotes
handleLLMStart + handleLLMEndllm_call (status ok)Plain (non-chat) LLM. Model, prompt, response, tokens.
handleChatModelStart + handleLLMEndllm_call (status ok)Chat model. Extracts last user + system message from the conversation.
handleLLMErrorllm_call (status failed)Response text and token counts are omitted on the failure path.
handleToolStart + handleToolEndtool_call (status ok)Tool name, input, output. Payload shape matches @mesedi.tool.
handleToolErrortool_call (status failed)Carries exception_type + exception_message.

Detector coverage

All 20 Mesedi failure-class detectors fire when driven through this adapter. Ten are native: the callback-to-event mapping above feeds them directly, as long as you attach MesediLangChainCallbackHandler at the callbacks: slot on the runnable. Ten are hybrid: you call an SDK emit helper inside your wrap() and the detector picks up the shape it needs. The LangChain integration harness exercises all 20 end-to-end against a real backend.

DetectorModeFires via
loopsNativehandleLLMStart + handleToolStart callbacks (identical_call, similar_call, step_count)
tool_failuresNativehandleToolError callback
driftNativehandleLLMStart user_message divergence
cost_velocityNativehandleLLMEnd token counts + provider pricing
prompt_injectionNativehandleLLMStart user_message pattern match
data_leakageNativehandleLLMStart user_message DLP match
context_overflowNativehandleLLMStart prompt-size vs model window
token_wasteNativehandleLLMStart shared prefix across 3+ user_messages
tool_schema_driftNativehandleToolEnd structured return_value fingerprint (attach callbacks on tool.invoke())
sandbox_escapeNativehandleToolStart arguments match the dangerous-call regex
crashesHybridthrow inside wrap(): same pattern every SDK language uses
semantic_loopHybridCall checkpoint(name, metadata) at each phase: LangChain has no chain-boundary equivalent
validator_failuresHybridCall validatorResult() after your schema check
infrastructure_throttledHybridCall emitInfrastructureEvent() when you observe a rate-limit / retry
grounding_failureHybridCall emitEvalScore() with your RAG / faithfulness score
cascading_failureHybridCall emitAgentHandoff() to a child execution
coordination_deadlockHybridCall emitAgentHandoff(): Tarjan's SCC catches the cycle
provider_incidentHybridEmit a failed llm_call event with error_class="timeout" / "rate_limited" when your provider client throws
hitl_timeoutHybridrequestHumanIntervention() + completeHumanIntervention() with timeout kind
hitl_rejection_spikeHybridRepeated validatorResult() with human_verdict=rejected over a window

Hybrid rows use exactly the same SDK helpers you'd call from any Mesedi integration (or from plain wrap()-only code). The callback handler and the helpers coexist in one execution: no double-counting, no setup order dependency.

Halt-safe boundaries

Every emitted llm_call and tool_call is a halt-safe boundary. If you pass a budget to wrap(), the handler checks it at each callback, increments the step counter, and on LLM calls adds any observed input/output tokens toward the per-execution cap. Behavior matches @mesedi.tool and the Anthropic patch.

Current limitations

  • Streaming (handleLLMNewToken) is not currently consumed. Mesedi receives one llm_call event with the assembled final response on handleLLMEnd. Per-token attribution needs an event-payload schema change and lands in a later release.
  • Per-chain depth tracking is not exposed. handleChainStart and handleChainEnd fire for every nested runnable but the handler ignores them; wrap() already owns the execution boundary. A chain-as-execution mode (no wrap() required) is a later iteration.
  • Multi-modal content blocks in chat messages are consumed for their text parts only. Image / audio blocks are ignored on the input side; the wire format doesn't yet carry them.
  • Outside a wrap, all callbacks silently no-op. Attach the handler inside a wrap()-decorated function for events to reach Mesedi.

Related