Skip to content

Commit 848ff2a

Browse files
authored
fix(langchain/agents): use channels when defining StateGraph schema (#9358)
1 parent a2bce18 commit 848ff2a

File tree

3 files changed

+140
-9
lines changed

3 files changed

+140
-9
lines changed

libs/langchain/src/agents/ReactAgent.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { InteropZodObject } from "@langchain/core/utils/types";
44

55
import {
6-
AnnotationRoot,
76
StateGraph,
87
END,
98
START,
@@ -185,7 +184,7 @@ export class ReactAgent<
185184
);
186185

187186
const workflow = new StateGraph(
188-
schema as unknown as AnnotationRoot<any>,
187+
schema as unknown as AnyAnnotationRoot,
189188
this.options.contextSchema
190189
);
191190

libs/langchain/src/agents/annotation.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
MessagesZodMeta,
88
type BinaryOperatorAggregate,
99
} from "@langchain/langgraph";
10-
import { withLangGraph } from "@langchain/langgraph/zod";
10+
import { withLangGraph, schemaMetaRegistry } from "@langchain/langgraph/zod";
1111

1212
import type { AgentMiddleware, AnyAnnotationRoot } from "./middleware/types.js";
1313
import { InteropZodObject } from "@langchain/core/utils/types";
@@ -27,7 +27,7 @@ export function createAgentAnnotationConditional<
2727
* Create Zod schema object to preserve jsonSchemaExtra
2828
* metadata for LangGraph Studio using v3-compatible withLangGraph
2929
*/
30-
const zodSchema: Record<string, any> = {
30+
const schemaShape: Record<string, any> = {
3131
messages: withLangGraph(z.custom<BaseMessage[]>(), MessagesZodMeta),
3232
jumpTo: z
3333
.union([
@@ -49,8 +49,8 @@ export function createAgentAnnotationConditional<
4949
continue;
5050
}
5151

52-
if (!(key in zodSchema)) {
53-
zodSchema[key] = schema;
52+
if (!(key in schemaShape)) {
53+
schemaShape[key] = schema;
5454
}
5555
}
5656
};
@@ -68,10 +68,12 @@ export function createAgentAnnotationConditional<
6868

6969
// Only include structuredResponse when responseFormat is defined
7070
if (hasStructuredResponse) {
71-
zodSchema.structuredResponse = z.any().optional();
71+
schemaShape.structuredResponse = z.any().optional();
7272
}
7373

74-
return z.object(zodSchema);
74+
const zodSchema = z.object(schemaShape);
75+
const stateDefinition = schemaMetaRegistry.getChannelsForSchema(zodSchema);
76+
return stateDefinition;
7577
}
7678

7779
export const PreHookAnnotation: AnnotationRoot<{

libs/langchain/src/agents/tests/reactAgent.test.ts

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect, beforeEach } from "vitest";
22
import { z } from "zod/v3";
3+
import { z as z4 } from "zod/v4";
34

45
import {
56
BaseMessage,
@@ -17,7 +18,12 @@ import {
1718
type BaseCheckpointSaver,
1819
} from "@langchain/langgraph";
1920

20-
import { providerStrategy, createAgent, createMiddleware } from "../index.js";
21+
import {
22+
providerStrategy,
23+
createAgent,
24+
createMiddleware,
25+
toolStrategy,
26+
} from "../index.js";
2127

2228
import {
2329
FakeToolCallingChatModel,
@@ -878,4 +884,128 @@ describe("createAgent", () => {
878884
const taskInput = JSON.parse(toolMessage.content as string);
879885
expect(taskInput.customField).toBe("test-value");
880886
});
887+
888+
// https://github.com/langchain-ai/langchainjs/issues/9299
889+
it("supports zod 3/4 schemas in createAgent and middleware", async () => {
890+
// Create middleware with Zod v3 schemas
891+
const middleware1 = createMiddleware({
892+
name: "middleware1",
893+
stateSchema: z.object({
894+
middleware1Value: z.string().default("v3-default"),
895+
}),
896+
contextSchema: z.object({
897+
middleware1Context: z.number(),
898+
}),
899+
beforeModel: (_state, { context }) => {
900+
expect(context.middleware1Context).toBe(42);
901+
return {
902+
middleware1Value: "v3-modified",
903+
};
904+
},
905+
});
906+
907+
// Create middleware with Zod v4 schemas
908+
const middleware2 = createMiddleware({
909+
name: "middleware2",
910+
stateSchema: z4.object({
911+
middleware2Value: z4.string().default("v4-default"),
912+
}),
913+
contextSchema: z4.object({
914+
middleware2Context: z4.boolean(),
915+
}),
916+
beforeModel: (_state, { context }) => {
917+
expect(context.middleware2Context).toBe(true);
918+
return {
919+
middleware2Value: "v4-modified",
920+
};
921+
},
922+
});
923+
924+
// Create a tool with Zod v3 schema
925+
const testTool = tool(async (input) => `Result: ${input.query}`, {
926+
name: "test_tool",
927+
description: "A test tool",
928+
schema: z.object({
929+
query: z.string(),
930+
}),
931+
});
932+
933+
const responseFormat = toolStrategy(
934+
z.object({
935+
result: z.string(),
936+
score: z.number(),
937+
})
938+
);
939+
940+
const expectedStructuredResponse = { result: "success", score: 95 };
941+
const model = new FakeToolCallingChatModel({
942+
responses: [
943+
new AIMessage({
944+
content: "",
945+
tool_calls: [
946+
{
947+
name: "test_tool",
948+
id: "tool_1",
949+
args: { query: "test" },
950+
},
951+
],
952+
}),
953+
new AIMessage({
954+
content: "",
955+
tool_calls: [
956+
{
957+
name: responseFormat[0].tool.function.name,
958+
id: "extract",
959+
args: expectedStructuredResponse,
960+
},
961+
],
962+
}),
963+
],
964+
structuredResponse: expectedStructuredResponse,
965+
});
966+
967+
const agent = createAgent({
968+
model,
969+
tools: [testTool],
970+
// Use Zod v4 for agent stateSchema
971+
stateSchema: z4.object({
972+
agentCounter: z4.number().default(0),
973+
agentName: z4.string().optional(),
974+
}),
975+
responseFormat,
976+
middleware: [middleware1, middleware2],
977+
});
978+
979+
const result = await agent.invoke(
980+
{
981+
messages: [new HumanMessage("Test mixed schemas")],
982+
agentCounter: 1,
983+
agentName: "test-agent",
984+
},
985+
{
986+
context: {
987+
middleware1Context: 42,
988+
middleware2Context: true,
989+
},
990+
}
991+
);
992+
993+
// Verify agent state schema (Zod v3)
994+
expect(result.agentCounter).toBe(1);
995+
expect(result.agentName).toBe("test-agent");
996+
997+
// Verify middleware1 state (Zod v3)
998+
expect(result.middleware1Value).toBe("v3-modified");
999+
1000+
// Verify middleware2 state (Zod v4)
1001+
expect(result.middleware2Value).toBe("v4-modified");
1002+
1003+
// Verify structured response (Zod v4)
1004+
expect(result.structuredResponse).toEqual(expectedStructuredResponse);
1005+
expect(result.structuredResponse.result).toBe("success");
1006+
expect(result.structuredResponse.score).toBe(95);
1007+
1008+
// Verify messages were processed correctly
1009+
expect(result.messages.length).toBeGreaterThan(0);
1010+
});
8811011
});

0 commit comments

Comments
 (0)