Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/overwrite-json-discriminator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@langchain/langgraph": patch
---

fix(langgraph): recognize JSON-erased `Overwrite` values across runtimes

`Overwrite` already survives JSON serialization in JS because `Overwrite.toJSON()`
emits the canonical `{ "__overwrite__": value }` sentinel. `_getOverwriteValue`
now additionally recognizes the discriminator form `{ "type": "__overwrite__",
value }` produced when a typed `Overwrite` from another runtime (e.g. a Python
dataclass routed through the LangGraph API server) is serialized and its type is
erased. This keeps `Overwrite` (and `DeltaChannel`) semantics intact across
cross-runtime JSON boundaries. These delta-channel APIs remain Beta.
15 changes: 13 additions & 2 deletions libs/langgraph-core/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,11 @@ export class Overwrite<ValueType = any> implements OverwriteValue<ValueType> {
*
* - If the value is an Overwrite instance (preferred API), return its `.value`.
* - If the value is a wire-format object ({ [OVERWRITE]: value }), extract it.
* - If the value is the discriminator form ({ type: OVERWRITE, value }) that
* results from JSON-serializing a typed `Overwrite` in another runtime (e.g.
* a Python dataclass routed through the LangGraph API server, where the typed
* instance is erased), extract it. Keeps Overwrite semantics intact across
* cross-runtime JSON boundaries.
* - Otherwise, returns undefined.
*
* @template ValueType - The expected type of the Overwrite value.
Expand All @@ -370,8 +375,14 @@ export class Overwrite<ValueType = any> implements OverwriteValue<ValueType> {
export function _getOverwriteValue<ValueType>(
value: unknown
): [true, ValueType] | [false, undefined] {
if (typeof value === "object" && value !== null && OVERWRITE in value) {
return [true, (value as Record<string, ValueType>)[OVERWRITE]];
if (typeof value === "object" && value !== null) {
if (OVERWRITE in value) {
return [true, (value as Record<string, ValueType>)[OVERWRITE]];
}
const rec = value as Record<string, unknown>;
if (rec.type === OVERWRITE && "value" in rec) {
return [true, rec.value as ValueType];
}
}
return [false, undefined];
}
Expand Down
39 changes: 39 additions & 0 deletions libs/langgraph-core/src/tests/constants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,48 @@ import {
Command,
Send,
CommandParams,
Overwrite,
OVERWRITE,
_deserializeCommandSendObjectGraph,
_getOverwriteValue,
} from "../constants.js";

// Cross-language parity with langgraph#8127: an Overwrite must survive a JSON
// boundary so the channel reducer still recognizes it after the typed instance
// is erased (e.g. routed through the LangGraph API server).
describe("_getOverwriteValue", () => {
it("recognizes an Overwrite instance", () => {
expect(_getOverwriteValue(new Overwrite(["b"]))).toEqual([true, ["b"]]);
});

it("recognizes the canonical sentinel wire form", () => {
expect(_getOverwriteValue({ [OVERWRITE]: ["b"] })).toEqual([true, ["b"]]);
});

it("recognizes an Overwrite after a JSON round-trip", () => {
const erased = JSON.parse(JSON.stringify(new Overwrite(["b"])));
expect(_getOverwriteValue(erased)).toEqual([true, ["b"]]);
});

it("recognizes the discriminator form from another runtime", () => {
// Python's Overwrite dataclass JSON-serializes to { value, type }.
expect(_getOverwriteValue({ type: OVERWRITE, value: ["b"] })).toEqual([
true,
["b"],
]);
});

it("does not misclassify look-alike dicts", () => {
expect(_getOverwriteValue({ value: ["b"] })).toEqual([false, undefined]);
expect(_getOverwriteValue({ type: "human", value: "hi" })).toEqual([
false,
undefined,
]);
expect(_getOverwriteValue(["b"])).toEqual([false, undefined]);
expect(_getOverwriteValue(null)).toEqual([false, undefined]);
});
});

describe("_deserializeCommandSendObjectGraph", () => {
it("handles primitive values", () => {
expect(_deserializeCommandSendObjectGraph(null)).toBeNull();
Expand Down
Loading