LangChain + LangGraph Integration
Pair contextweaver with LangChain and LangGraph so chain-based and graph-based agents get budget-aware memory, bounded tool routing, and a context firewall — without giving up the framework's agent loop.
Importable adapter
As of issue #502, contextweaver.adapters.langchain ships a first-class
BaseTool → SelectableItem adapter, so you no longer hand-roll the conversion:
from contextweaver.adapters.langchain import load_langchain_catalog
# `tools` is your list of LangChain BaseTool instances.
catalog = load_langchain_catalog(tools, namespace="research")
Install the optional extra for live loading (pip install
'contextweaver[langchain]'); the plain-dict path
(langchain_tools_to_catalog) needs no extra and is handy in tests. Feed the
returned Catalog to the Router exactly as in the routing examples below.
Why
LangChain's memory classes (ConversationBufferMemory,
ConversationSummaryMemory) and LangGraph's stateful checkpoints both
accumulate conversation history with no token-budget enforcement.
Long sessions blow through the model's context window; large tool
outputs (multi-KB JSON, RAG retrievals) bloat the prompt; loading every
tool into the system message wastes thousands of tokens.
contextweaver provides three composable replacements:
| Pain | contextweaver answer |
|---|---|
Unbounded ConversationBufferMemory |
ContextManager.build_sync(phase=…) returns a phase-specific, budgeted prompt |
| Loading every tool into the LLM | Router.route(query) returns a bounded shortlist |
| Large tool results bloat the prompt | ContextManager.ingest_tool_result_sync() firewalls raw bytes to the artifact store |
The repo already ships a runnable LangChain memory-replacement demo:
examples/langchain_memory_demo.py
(installable via pip install 'contextweaver[langchain]'). Read that
first — it shows the token-budget delta side by side.
Prerequisites
pip install 'contextweaver[langchain]' langchain langchain-openai langgraph
export OPENAI_API_KEY=sk-...
LangChain Integration
Architecture
User query
│
▼
contextweaver Router ← all tools (Catalog)
│ (top-k shortlist)
▼
LangChain Agent ← receives only the shortlist
│ (tool call)
▼
contextweaver Firewall ← intercepts large results
│ (summary)
▼
contextweaver ContextManager ← phase-specific compilation
│ (pack.prompt)
▼
LLM
Memory replacement
ConversationBufferMemory is the canonical "memory" you want to replace:
# Before — unbounded memory
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory()
# After — budget-aware memory via contextweaver
from contextweaver.context.manager import ContextManager
from contextweaver.config import ContextBudget
from contextweaver.types import ContextItem, ItemKind, Phase
ctx_mgr = ContextManager(
budget=ContextBudget(route=500, call=1200, interpret=1500, answer=3000),
)
To get the conversation history that a LangChain agent expects, build a prompt for the relevant phase right before invoking the agent:
def respond(user_query: str, turn: int) -> str:
# 1. Ingest the user turn into the event log.
ctx_mgr.ingest_sync(ContextItem(
id=f"u{turn}", kind=ItemKind.user_turn, text=user_query,
))
# 2. Compile a phase-specific, budgeted prompt.
pack = ctx_mgr.build_sync(phase=Phase.answer, query=user_query)
# 3. Invoke the LangChain agent with the compiled prompt as "history".
result = agent_executor.invoke({"input": user_query, "history": pack.prompt})
# 4. Ingest the agent's response so the next turn can see it.
ctx_mgr.ingest_sync(ContextItem(
id=f"a{turn}", kind=ItemKind.agent_msg, text=result["output"],
))
return result["output"]
pack.stats.included_count / dropped_count tell you exactly what was
kept and what was dropped by the budget — surface these in logs.
Firewalling tool results via a callback
LangChain emits a BaseCallbackHandler.on_tool_end() event after every
tool finishes. That's the natural hook for the context firewall:
from langchain_core.callbacks import BaseCallbackHandler
class ContextWeaverCallback(BaseCallbackHandler):
"""Route LangChain tool results through contextweaver's firewall."""
def __init__(self, ctx_mgr: ContextManager) -> None:
self._mgr = ctx_mgr
self._call_counter = 0
def on_tool_end(self, output: str, *, name: str, **_: object) -> None:
self._call_counter += 1
tool_call_id = f"tc-{self._call_counter}"
self._mgr.ingest_tool_result_sync(
tool_call_id=tool_call_id,
raw_output=str(output),
tool_name=name,
)
Wire it into your AgentExecutor:
from langchain.agents import AgentExecutor, create_openai_functions_agent
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4")
agent = create_openai_functions_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
callbacks=[ContextWeaverCallback(ctx_mgr)],
)
Routing to a tool shortlist
Use Router.route() to narrow the tool list before building the
agent:
from contextweaver.routing.catalog import Catalog
from contextweaver.routing.router import Router
from contextweaver.routing.tree import TreeBuilder
# (build Catalog of SelectableItems from your tool definitions)
graph = TreeBuilder().build(catalog.all())
router = Router(graph, items=catalog.all(), top_k=3)
shortlist_ids = router.route(user_query).candidate_ids
selected_tools = [t for t in all_tools if t.name in shortlist_ids]
agent = create_openai_functions_agent(llm, selected_tools, prompt)
LangGraph Integration
LangGraph state is the natural place to park a ContextManager because
each node sees the same state object.
Architecture
User query
│
▼
[LangGraph State] ← ctx_mgr lives here
│
▼
node: route ← ctx_mgr.build_sync(phase=Phase.route)
│ (Router.route → shortlist)
▼
node: call_tool ← framework calls the tool
│ (ctx_mgr.ingest_tool_result_sync — firewall)
▼
node: interpret ← ctx_mgr.build_sync(phase=Phase.interpret)
│
▼
node: answer ← ctx_mgr.build_sync(phase=Phase.answer)
│
▼
LLM
Stateful 4-node graph
from typing import TypedDict
from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph
from contextweaver.context.manager import ContextManager
from contextweaver.routing.router import Router
from contextweaver.types import ContextItem, ItemKind, Phase
class AgentState(TypedDict):
user_query: str
routed_tools: list[str]
tool_result: str
answer: str
turn: int
ctx_mgr = ContextManager()
# `router` and `llm` from the earlier sections
def route_node(state: AgentState) -> AgentState:
ctx_mgr.ingest_sync(ContextItem(
id=f"u{state['turn']}", kind=ItemKind.user_turn, text=state["user_query"],
))
routed = router.route(state["user_query"])
return {**state, "routed_tools": list(routed.candidate_ids)}
def call_tool_node(state: AgentState) -> AgentState:
tool_id = state["routed_tools"][0]
raw = execute_tool(tool_id, state["user_query"]) # your runtime
ctx_mgr.ingest_sync(ContextItem(
id=f"tc-{state['turn']}", kind=ItemKind.tool_call,
text=f"{tool_id}(...)", parent_id=f"u{state['turn']}",
))
item, _ = ctx_mgr.ingest_tool_result_sync(
tool_call_id=f"tc-{state['turn']}",
raw_output=str(raw),
tool_name=tool_id,
)
return {**state, "tool_result": item.text}
def answer_node(state: AgentState) -> AgentState:
pack = ctx_mgr.build_sync(phase=Phase.answer, query=state["user_query"])
answer = ChatOpenAI(model="gpt-4").invoke(pack.prompt)
return {**state, "answer": str(answer.content)}
graph = StateGraph(AgentState)
graph.add_node("route", route_node)
graph.add_node("call_tool", call_tool_node)
graph.add_node("answer", answer_node)
graph.add_edge("route", "call_tool")
graph.add_edge("call_tool", "answer")
graph.add_edge("answer", END)
graph.set_entry_point("route")
app = graph.compile()
result = app.invoke({"user_query": "...", "routed_tools": [], "tool_result": "",
"answer": "", "turn": 1})
Putting ctx_mgr in the closure (rather than the state itself) keeps the
state pickle-friendly for LangGraph checkpoints.
Migration guide — LangChain memory → contextweaver
| LangChain pattern | contextweaver equivalent |
|---|---|
ConversationBufferMemory() |
ContextManager(budget=ContextBudget(...)) |
ConversationSummaryMemory(llm=...) |
ContextManager(summarizer=YourSummarizer()) — implement the Summarizer protocol |
memory.load_memory_variables({}) |
ctx_mgr.build_sync(phase=Phase.answer, query=user_query).prompt |
memory.save_context(in, out) |
ctx_mgr.ingest_sync(ContextItem(kind=ItemKind.user_turn, ...)) + ctx_mgr.ingest_sync(ContextItem(kind=ItemKind.agent_msg, ...)) |
See examples/langchain_memory_demo.py
for a runnable side-by-side comparison.
Troubleshooting
AgentExecutorignorespack.prompt. Make sure the prompt template has a"{history}"placeholder (or whichever variable you injected the pack into) — LangChain does not auto-discover it.- Callbacks not firing. Confirm you passed
callbacks=[...]to theAgentExecutor, not just to theLLM.on_tool_endis on the executor. - LangGraph checkpoints fail to pickle. Keep
ContextManageroutside theTypedDictstate — it holds an in-memory event log that isn't serialised by default. Persistmgr.event_log.to_dict()separately alongside the checkpoint if you need cross-session continuity. - Tool not in the shortlist. Inspect
result.scores— the TF-IDF retriever may need richer descriptions or tags. You can also usecontext_hints=[...]to inject conversation context into scoring.
See also
- How contextweaver Fits — boundary, hook points, non-goals
- Cookbook — FastMCP, A2A, BYOT, firewall + drilldown
examples/langchain_memory_demo.py— runnable side-by-side LangChain memory comparison- LangChain docs · LangGraph docs
- Tracking issue: #80