mirror of
https://github.com/langchain-ai/langgraphjs.git
synced 2026-07-25 13:15:45 -04:00
fix(langgraph): respect meta defaults in LastValue (#1825)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@langchain/langgraph": patch
|
||||
---
|
||||
|
||||
respect meta defaults in `LastValue`
|
||||
@@ -15,8 +15,15 @@ export class LastValue<Value> extends BaseChannel<Value, Value, Value> {
|
||||
// value is an array so we don't misinterpret an update to undefined as no write
|
||||
value: [Value] | [] = [];
|
||||
|
||||
constructor(protected initialValueFactory?: () => Value) {
|
||||
super();
|
||||
if (initialValueFactory) {
|
||||
this.value = [initialValueFactory()];
|
||||
}
|
||||
}
|
||||
|
||||
fromCheckpoint(checkpoint?: Value) {
|
||||
const empty = new LastValue<Value>();
|
||||
const empty = new LastValue<Value>(this.initialValueFactory);
|
||||
if (typeof checkpoint !== "undefined") {
|
||||
empty.value = [checkpoint];
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ export class SchemaMetaRegistry {
|
||||
InferInteropZodOutput<typeof channelSchema>
|
||||
>(meta.reducer.fn, meta.default);
|
||||
} else {
|
||||
channels[key] = new LastValue();
|
||||
channels[key] = new LastValue(meta?.default);
|
||||
}
|
||||
}
|
||||
return channels as InteropZodToStateDefinition<T>;
|
||||
|
||||
@@ -47,6 +47,93 @@ describe("LastValue", () => {
|
||||
const channel2 = restoredChannel.fromCheckpoint(checkpoint);
|
||||
expect(channel2.get()).toBe(value);
|
||||
});
|
||||
|
||||
describe("with initialValueFactory", () => {
|
||||
it("should initialize with default value from factory", () => {
|
||||
const channel = new LastValue<string>(() => "default-value");
|
||||
expect(channel.get()).toBe("default-value");
|
||||
expect(channel.isAvailable()).toBe(true);
|
||||
});
|
||||
|
||||
it("should allow updates to override initial value", () => {
|
||||
const channel = new LastValue<string>(() => "default-value");
|
||||
expect(channel.get()).toBe("default-value");
|
||||
|
||||
channel.update(["updated-value"]);
|
||||
expect(channel.get()).toBe("updated-value");
|
||||
});
|
||||
|
||||
it("should preserve initialValueFactory in checkpoint restoration", () => {
|
||||
const initialValueFactory = () => "default-value";
|
||||
const channel = new LastValue<string>(initialValueFactory);
|
||||
expect(channel.get()).toBe("default-value");
|
||||
|
||||
channel.update(["updated-value"]);
|
||||
const checkpoint = channel.checkpoint();
|
||||
|
||||
const restoredChannel = new LastValue<string>(initialValueFactory);
|
||||
const channel2 = restoredChannel.fromCheckpoint(checkpoint);
|
||||
// Checkpoint value takes precedence
|
||||
expect(channel2.get()).toBe("updated-value");
|
||||
|
||||
// Create a new channel from checkpoint without providing factory
|
||||
// This tests that the factory is preserved in the restored channel
|
||||
const channel3 = new LastValue<string>(initialValueFactory);
|
||||
const channel4 = channel3.fromCheckpoint(checkpoint);
|
||||
expect(channel4.get()).toBe("updated-value");
|
||||
});
|
||||
|
||||
it("should use initial value when checkpoint is undefined", () => {
|
||||
const initialValueFactory = () => "default-value";
|
||||
const channel = new LastValue<string>(initialValueFactory);
|
||||
const restoredChannel = channel.fromCheckpoint(undefined);
|
||||
// When checkpoint is undefined, should use initial value
|
||||
expect(restoredChannel.get()).toBe("default-value");
|
||||
});
|
||||
|
||||
it("should work with function that returns different values", () => {
|
||||
let callCount = 0;
|
||||
const initialValueFactory = () => {
|
||||
callCount += 1;
|
||||
return `value-${callCount}`;
|
||||
};
|
||||
|
||||
const channel1 = new LastValue<string>(initialValueFactory);
|
||||
expect(channel1.get()).toBe("value-1");
|
||||
|
||||
const channel2 = new LastValue<string>(initialValueFactory);
|
||||
expect(channel2.get()).toBe("value-2");
|
||||
});
|
||||
|
||||
it("should handle initialValueFactory returning falsy values", () => {
|
||||
const channelZero = new LastValue<number>(() => 0);
|
||||
expect(channelZero.get()).toBe(0);
|
||||
|
||||
const channelEmpty = new LastValue<string>(() => "");
|
||||
expect(channelEmpty.get()).toBe("");
|
||||
|
||||
const channelFalse = new LastValue<boolean>(() => false);
|
||||
expect(channelFalse.get()).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle complex objects from initialValueFactory", () => {
|
||||
const initialValueFactory = () => ({ foo: "bar", count: 42 });
|
||||
const channel = new LastValue<{ foo: string; count: number }>(
|
||||
initialValueFactory
|
||||
);
|
||||
const value = channel.get();
|
||||
expect(value.foo).toBe("bar");
|
||||
expect(value.count).toBe(42);
|
||||
});
|
||||
|
||||
it("should work without initialValueFactory", () => {
|
||||
const channel = new LastValue<number>();
|
||||
expect(() => channel.get()).toThrow(EmptyChannelError);
|
||||
|
||||
channel.update([42]);
|
||||
expect(channel.get()).toBe(42);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Topic", () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
import * as z4 from "zod/v4";
|
||||
import { StateGraph } from "../graph/state.js";
|
||||
import { END, START } from "../constants.js";
|
||||
import { _AnyIdAIMessage, _AnyIdHumanMessage } from "./utils.js";
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
getStateTypeSchema,
|
||||
} from "../graph/zod/schema.js";
|
||||
import { MessagesZodState } from "../graph/messages_annotation.js";
|
||||
import { registry } from "../graph/zod/zod-registry.js";
|
||||
|
||||
describe("StateGraph with Zod schemas", () => {
|
||||
it("should accept Zod schema as input in addNode", async () => {
|
||||
@@ -144,4 +146,115 @@ describe("StateGraph with Zod schemas", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("registry default values", () => {
|
||||
it("should apply registry default when field is missing from input", async () => {
|
||||
const stateSchema = z4.object({
|
||||
foo: z4.string().default("zod-default"),
|
||||
bar: z4.string().register(registry, {
|
||||
default: () => "registry-default",
|
||||
}),
|
||||
baz: z4.string(),
|
||||
});
|
||||
|
||||
const graph = new StateGraph(stateSchema)
|
||||
.addNode("process", (state) => {
|
||||
// Verify defaults are applied when node receives state
|
||||
expect(state.foo).toBe("zod-default");
|
||||
expect(state.bar).toBe("registry-default");
|
||||
expect(state.baz).toBe("provided");
|
||||
return {};
|
||||
})
|
||||
.addEdge(START, "process")
|
||||
.addEdge("process", END)
|
||||
.compile();
|
||||
|
||||
// Only provide baz, foo and bar should get defaults
|
||||
const result = await graph.invoke({ baz: "provided" });
|
||||
// Verify defaults are in final output
|
||||
expect(result.foo).toBe("zod-default");
|
||||
expect(result.bar).toBe("registry-default");
|
||||
expect(result.baz).toBe("provided");
|
||||
});
|
||||
|
||||
it("should prioritize provided input over registry default", async () => {
|
||||
const stateSchema = z4.object({
|
||||
bar: z4.string().register(registry, {
|
||||
default: () => "registry-default",
|
||||
}),
|
||||
});
|
||||
|
||||
const graph = new StateGraph(stateSchema)
|
||||
.addNode("process", (state) => {
|
||||
// Verify provided value takes precedence over default
|
||||
expect(state.bar).toBe("provided-value");
|
||||
return {};
|
||||
})
|
||||
.addEdge(START, "process")
|
||||
.addEdge("process", END)
|
||||
.compile();
|
||||
|
||||
const result = await graph.invoke({ bar: "provided-value" });
|
||||
expect(result.bar).toBe("provided-value");
|
||||
});
|
||||
|
||||
it("should work with registry default alongside reducer", async () => {
|
||||
const stateSchema = z4.object({
|
||||
items: z4.array(z4.string()).register(registry, {
|
||||
default: () => ["initial"],
|
||||
reducer: {
|
||||
fn: (a, b) => a.concat(Array.isArray(b) ? b : [b]),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const graph = new StateGraph(stateSchema)
|
||||
.addNode("add", (state) => {
|
||||
// Verify default is applied before reducer processes update
|
||||
expect(state.items).toEqual(["initial"]);
|
||||
return { items: ["new"] };
|
||||
})
|
||||
.addEdge(START, "add")
|
||||
.addEdge("add", END)
|
||||
.compile();
|
||||
|
||||
const result = await graph.invoke({});
|
||||
// Verify reducer combined default with update
|
||||
expect(result.items).toEqual(["initial", "new"]);
|
||||
});
|
||||
|
||||
it("should work when all combinations of defaults are present", async () => {
|
||||
const stateSchema = z4.object({
|
||||
withZod: z4.string().default("zod-default"),
|
||||
withRegistry: z4.string().register(registry, {
|
||||
default: () => "registry-default",
|
||||
}),
|
||||
withBoth: z4
|
||||
.string()
|
||||
.default("zod-default")
|
||||
.register(registry, {
|
||||
default: () => "registry-default",
|
||||
}),
|
||||
withNeither: z4.string(),
|
||||
});
|
||||
|
||||
const graph = new StateGraph(stateSchema)
|
||||
.addNode("process", (state) => {
|
||||
// Verify defaults are applied correctly
|
||||
expect(state.withZod).toBe("zod-default");
|
||||
expect(state.withRegistry).toBe("registry-default");
|
||||
// Zod default takes precedence during parsing, so registry default isn't used
|
||||
expect(state.withBoth).toBe("zod-default");
|
||||
return {};
|
||||
})
|
||||
.addEdge(START, "process")
|
||||
.addEdge("process", END)
|
||||
.compile();
|
||||
|
||||
const result = await graph.invoke({});
|
||||
expect(result.withZod).toBe("zod-default");
|
||||
expect(result.withRegistry).toBe("registry-default");
|
||||
expect(result.withBoth).toBe("zod-default"); // Zod default takes precedence during parsing
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user