Hi, I’m using LangChain v1 (TypeScript) with createAgent and MongoDBSaver checkpointer for a RAG application. I’m experiencing behaviour where tool results seem to be reused across conversation turns instead of making fresh tool calls.
Setup:
-
LangChain v1.x (TypeScript)
-
createAgent with MongoDBSaver checkpointer
-
Custom retrieval tool with parameters captured in closure (e.g., groupIds)
-
Multi-turn conversations
Observed Behaviour:
-
Request 1 (new conversation):
- question: “When was Company1 formed?”
- groupIds: [“681…”, “684…”]
- //Works - retrieval tool called, correct results
-
Request 2 (continuing conversation):
- question: “What is Company2 fiscal year?”
- conversationId: “69123…” // same conversation
- groupIds: [“681…”, “684…”] // SAME groupIds
// Returns wrong results - citations show Company1 docs from Request 1
What I Expected:
-
Agent loads conversation history (Q&A about Company1)
-
Agent sees NEW question about Company2
-
Agent calls the retrieval tool with a fresh query
-
Tool performs a new vector search (same groupIds, different semantic query)
-
Returns Company2 documents
What Actually Happens:
-
Agent returns “not found” or Company1 information
-
Citations show the SAME documents from Request 1
-
Tool is either not called or returns cached results
What I’ve Tried:
-
Middleware to filter ToolMessages:
export const filterToolMessagesMiddleware = createMiddleware({ name: 'FilterToolMessages', beforeModel: async (state: { messages: BaseMessage[] }) => { const filtered = state.messages.filter(msg => !(msg instanceof ToolMessage) && !(msg instanceof AIMessage && msg.tool_calls?.length > 0) ) return { messages: filtered } } }) const agent = createAgent({ model: llm, tools: [retrievalTool], checkpointer, middleware: [filterToolMessagesMiddleware] })
- Result: Still reuses old results
-
Manual checkpoint management (current workaround):
// Load checkpoint manually const existingState = await checkpointer.get(threadConfig) const existingMessages = existingState?.channel_values?.messages || [] // Filter out tool messages const conversationHistory = existingMessages.filter(msg => !(msg instanceof ToolMessage) && !(msg instanceof AIMessage && msg.tool_calls?.length > 0) ) // Create agent WITHOUT checkpointer const agent = createAgent({ model: llm, tools: [retrievalTool] // NO checkpointer }) // Invoke with filtered history const result = await agent.invoke({ messages: [...conversationHistory, new HumanMessage(question)] }) // Save manually await checkpointer.put(threadConfig, { channel_values: { messages: result.messages } })
Result: Works - fresh tool calls every time
Questions:
-
Is this expected behaviour when using createAgent with a checkpointer?
-
Should middleware be sufficient to prevent tool result reuse?
-
Is manual checkpoint management the recommended approach for this use case?
-
Am I missing something in my implementation?
Any guidance would be greatly appreciated!
Environment:
-
LangChain: v1.x (TypeScript)
-
Node.js: v24
-
Checkpointer: MongoDBSaver