Unable to use "stateSchema" within createMiddleware without throwing TypeError: keyValidator._parse is not a function

Hello.

I am currently writing a post processing Middleware function to automatically summarize and remove unnecessary messages after summarization. I know I can use the summarizationMiddleware, which works great.. but it doesn’t auto-remove summarized messages from the state after summarization, and also I need to store the summarization message that was created in the state, so that it can be re-used for future summarization messages. Otherwise the summarization re-runs after every subsequent createAgent LLM call, which is obviously not ideal. Both of these additional features would be much appreciated in the future, as this would save writing customMiddleware. Unless there is some other way to achieve this?

So I have written a customMiddleware to do this, but it is not working as expected. The problem is that, even though my graph stateSchema includes a custom state field for messagesSummary (see code below).. LangSmith is showing that the middleWare sucessfully outputs the result of messagesSummary as expected, but this change to the state never passes through to the final output of the createAgent call See: LangSmith for full trace.

Here is the stateSchema I am using for the graph.

export const DynamicAnnotation = Annotation.Root({
…MessagesAnnotation.spec, // Spread in the messages state

// Message Summary (used when summarising the conversation for shorter LLM)
messagesSummary: Annotation<string>({
    reducer: (x, y) => {
        // Handle concurrent updates by taking the last non-empty value
        if (Array.isArray(y)) {
            // Multiple updates in same step - take the last non-empty one
            const lastUpdate = y.filter(val => val != null && val.trim() !== '').pop();
            return lastUpdate ?? x;
        }
        // Single update - replace with new value if provided, otherwise keep existing
        return y ?? x;
    }
}),

// Main dataset used for the graph (not appended, replaced);
data: Annotation<Record<string, any>>(),

// Temp data store for various purposes (e.g.: Evaluation iterators) - not appended, replaced;
_temp: Annotation<Record<string, any>>({
    default: () => ({}),
    reducer: (x, y) => ({ ...x, ...y }),
}),

// Next node to route too
next: Annotation<string>()

})

So to attempt to fix this, I updated the middleware.. to define stateSchema.. aka

createMiddleware({
    name: "SummarizationMiddleware",
    stateSchema: z.object({
        messagesSummary: z.string().optional(),
      }),
.....

This matches the return statement for createMiddleware…

.....
const summaryResponse = await model.invoke(summaryPrompt);
const updatedSummary = "SUMMARY OF PREVIOUS CONVERSATION HISTORY: " +
(typeof summaryResponse.content === ‘string’ ? summaryResponse.content : JSON.stringify(summaryResponse.content));

            // Create RemoveMessage objects for all messages being removed
            const messagesToRemove: RemoveMessage[] = [];
            
            try {
                for (const messageToSummarize of messagesToSummarize) {
                    const msgId = (messageToSummarize as any).id;
                    if (msgId) {
                        // Verify the message exists in the original messages array
                        const messageExists = messages.some((m: any) => m.id === msgId);
                        if (messageExists) {
                            messagesToRemove.push(
                                new RemoveMessage({
                                    id: msgId as string,
                                })
                            );
                        }
                    }
                }
            } catch (error) {
                console.warn("Error creating RemoveMessage objects:", error);
            }

            trimmerRunning[trimmerId] = false;

            // Return updated state with:
            // 1. Preserved messages + RemoveMessage objects (to remove old 
messages)
            // 2. Updated messagesSummary
            return {
                messages: [...messagesToRemove],
                messagesSummary: updatedSummary
            };

But then i get the following error when the middleware tries to run..

Error in AgentGraphNode simple_agent: TypeError: keyValidator._parse is not a function
    at ZodObject._parse (file:///home/runner/workspace/node_modules/zod/v3/types.js:1963:37)
    at ZodObject._parseSync (file:///home/runner/workspace/node_modules/zod/v3/types.js:100:29)
    at ZodObject.safeParse (file:///home/runner/workspace/node_modules/zod/v3/types.js:129:29)
    at ZodObject.parse (file:///home/runner/workspace/node_modules/zod/v3/types.js:111:29)
    at interopParse (file:///home/runner/workspace/node_modules/@langchain/core/dist/utils/types/zod.js:136:43)
    at CompiledStateGraph._validateInput (file:///home/runner/workspace/node_modules/@langchain/langgraph/dist/graph/state.js:438:30)
    at CompiledStateGraph._streamIterator (file:///home/runner/workspace/node_modules/@langchain/langgraph/dist/pregel/index.js:964:33)
    at _streamIterator.next (<anonymous>)
    at file:///home/runner/workspace/node_modules/@langchain/core/dist/utils/stream.js:136:41
    at AsyncLocalStorage.run (node:internal/async_local_storage/async_hooks:91:14)
❌ Failed to test template undefined: TypeError: keyValidator._parse is not a function
    at ZodObject._parse (file:///home/runner/workspace/node_modules/zod/v3/types.js:1963:37)
    at ZodObject._parseSync (file:///home/runner/workspace/node_modules/zod/v3/types.js:100:29)
    at ZodObject.safeParse (file:///home/runner/workspace/node_modules/zod/v3/types.js:129:29)
    at ZodObject.parse (file:///home/runner/workspace/node_modules/zod/v3/types.js:111:29)
    at interopParse (file:///home/runner/workspace/node_modules/@langchain/core/dist/utils/types/zod.js:136:43)
    at CompiledStateGraph._validateInput (file:///home/runner/workspace/node_modules/@langchain/langgraph/dist/graph/state.js:438:30)
    at CompiledStateGraph._streamIterator (file:///home/runner/workspace/node_modules/@langchain/langgraph/dist/pregel/index.js:964:33)
    at _streamIterator.next (<anonymous>)
    at file:///home/runner/workspace/node_modules/@langchain/core/dist/utils/stream.js:136:41
    at AsyncLocalStorage.run (node:internal/async_local_storage/async_hooks:91:14) {
  pregelTaskId: 'eb9eae4e-9173-5cf5-823e-5002e5367b1d'
}

I even attempted to run the default code from LangChain Docs (section “Custom state schema), and I get the same error as above.. Custom middleware - Docs by LangChain

Any help to resolve this would be much appreciated.

Note, here is my packages.json to see current versions I am running:

{
“name”: “nodejs”,
“version”: “1.0.0”,
“type”: “module”,
“main”: “index.js”,
“scripts”: {
“test”: “echo “Error: no test specified” && exit 1”,
“build:ts”: “npx tsc”,
“dev”: “npx tsc && node dist/index.js”,
“start”: “npx tsc && node dist/index.js”
},
“keywords”: ,
“author”: “”,
“license”: “ISC”,
“dependencies”: {
“@google/genai”: “^1.25.0”,
@langchain/community”: “^0.3.57”,
@langchain/core”: “^1.0.5”,
@langchain/google-genai”: “^0.2.18”,
@langchain/langgraph”: “^1.0.2”,
@langchain/openai”: “^0.6.16”,
@langchain/tavily”: “^0.1.5”,
“@tavily/core”: “^0.5.12”,
“@types/json-stable-stringify”: “^1.2.0”,
“@types/pg”: “^8.15.5”,
“dotenv”: “^17.2.3”,
“duck-duck-scrape”: “^2.2.7”,
“install”: “^0.13.0”,
“json-stable-stringify”: “^1.3.0”,
“langchain”: “^1.0.0-alpha.9”,
“npm”: “^11.6.2”,
“nunjucks”: “^3.2.4”,
“pg”: “^8.16.3”,
“tiktoken”: “^1.0.22”,
“zod”: “^4.1.12”
},
“devDependencies”: {
“@types/node”: “^24.8.0”,
“@types/nunjucks”: “^3.2.6”,
“ts-node”: “^10.9.2”,
“typescript”: “^5.9.3”
},
“description”: “”
}

hi @WeOH-co

your package.json deps cannot get installed since this error is throw when installing them:
ERESOLVE unable to resolve dependency tree - you likely instlled them with --legacy-peer-deps flag.

Anyway, your issue is most probaby becasue Zod 4 instead of Zod 3 - try to install/use Zod 3

Or omport the Zod adapter once when using Zod v4 with LangGraph v1: import "@langchain/langgraph/zod";:

import { z } from "zod";
import "@langchain/langgraph/zod";

And keep using z from "zod" (v4). Do not mix zod/v3 and zod in the same schema.

I see that langgraphjs @1.0.2 support both Zod versions:

"peerDependencies": {
    "@langchain/core": "^1.0.1",
    "zod": "^3.25.32 || ^4.1.0",
    "zod-to-json-schema": "^3.x"
  },

Thankyou you were spot on. I have fixed up all my dependencies, and now the problem is fixed.

Just a side question (potentially not for yourself), but do you think its valid to add functionality to the built-in summarizer to remove summarized messages, and automatically track and store the previously summarized message? This would save having to re-write the built-in summarizer to add this functionality.. Keen to get your thoughts.

Hi @WeOH-co

good it helped. Could you mark the post as “solved” for the others so they can see how to solve their issues?

Regarding your question: could you clarify what you mean by “automatically track and store the previously summarized message” and why you want to track and store the messages?

Thanks. I have marked as solved.

So essentially, when summarising the messages stream based on token limits, all subsequent calls to other agents should include be able to retrieve a stripped extract of messages that were preserved (from the state), and the previously created summary that was created the last time the summarizationMiddleware created. This saves on unnecessary LLM summarization calls.

The problem is (as far as I can tell from LangSmith logs), the summarizationMiddleware only handles this requirement when it is run (once off summarization), and then repeats this behaviour (re-calling LLM to summarize) after every subsequent agent call. This is obviously inefficient.
Unless I should be handling this some other way? Am i missing the point?