@langchain/langgraph 1.4.21 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/channels/base.cjs +113 -8
  2. package/dist/channels/base.cjs.map +1 -1
  3. package/dist/channels/base.d.cts +26 -4
  4. package/dist/channels/base.d.cts.map +1 -1
  5. package/dist/channels/base.d.ts +26 -4
  6. package/dist/channels/base.d.ts.map +1 -1
  7. package/dist/channels/base.js +110 -10
  8. package/dist/channels/base.js.map +1 -1
  9. package/dist/channels/index.cjs +2 -0
  10. package/dist/channels/index.d.cts +2 -2
  11. package/dist/channels/index.d.ts +2 -2
  12. package/dist/channels/index.js +2 -2
  13. package/dist/constants.cjs +14 -0
  14. package/dist/constants.cjs.map +1 -1
  15. package/dist/constants.d.cts.map +1 -1
  16. package/dist/constants.d.ts.map +1 -1
  17. package/dist/constants.js +13 -1
  18. package/dist/constants.js.map +1 -1
  19. package/dist/graph/index.cjs +1 -1
  20. package/dist/graph/index.js +1 -1
  21. package/dist/graph/messages_reducer.cjs +7 -0
  22. package/dist/graph/messages_reducer.cjs.map +1 -1
  23. package/dist/graph/messages_reducer.js +7 -1
  24. package/dist/graph/messages_reducer.js.map +1 -1
  25. package/dist/index.cjs +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/pregel/algo.cjs +46 -45
  28. package/dist/pregel/algo.cjs.map +1 -1
  29. package/dist/pregel/algo.d.cts.map +1 -1
  30. package/dist/pregel/algo.d.ts.map +1 -1
  31. package/dist/pregel/algo.js +47 -47
  32. package/dist/pregel/algo.js.map +1 -1
  33. package/dist/pregel/checkpoint.cjs +70 -0
  34. package/dist/pregel/checkpoint.cjs.map +1 -0
  35. package/dist/pregel/checkpoint.js +67 -0
  36. package/dist/pregel/checkpoint.js.map +1 -0
  37. package/dist/pregel/index.cjs +87 -34
  38. package/dist/pregel/index.cjs.map +1 -1
  39. package/dist/pregel/index.d.cts.map +1 -1
  40. package/dist/pregel/index.d.ts.map +1 -1
  41. package/dist/pregel/index.js +91 -38
  42. package/dist/pregel/index.js.map +1 -1
  43. package/dist/pregel/loop.cjs +325 -109
  44. package/dist/pregel/loop.cjs.map +1 -1
  45. package/dist/pregel/loop.js +330 -115
  46. package/dist/pregel/loop.js.map +1 -1
  47. package/dist/state/prebuilt/messages.cjs +1 -1
  48. package/dist/state/prebuilt/messages.js +1 -1
  49. package/dist/web.cjs +1 -1
  50. package/dist/web.js +1 -1
  51. package/package.json +5 -5
@@ -139,8 +139,15 @@ function messagesDeltaReducer(state, writes) {
139
139
  }
140
140
  return result.filter((m) => m !== null);
141
141
  }
142
+ const MESSAGES_DELTA_REDUCER = Symbol.for("langgraph.messages_delta_reducer");
143
+ Object.defineProperty(messagesDeltaReducer, MESSAGES_DELTA_REDUCER, { value: true });
144
+ /** @internal */
145
+ function isMessagesDeltaReducer(reducer) {
146
+ return typeof reducer === "function" && reducer[MESSAGES_DELTA_REDUCER] === true;
147
+ }
142
148
  //#endregion
143
149
  exports.REMOVE_ALL_MESSAGES = REMOVE_ALL_MESSAGES;
150
+ exports.isMessagesDeltaReducer = isMessagesDeltaReducer;
144
151
  exports.messagesDeltaReducer = messagesDeltaReducer;
145
152
  exports.messagesStateReducer = messagesStateReducer;
146
153
 
@@ -1 +1 @@
1
- {"version":3,"file":"messages_reducer.cjs","names":["coerceMessageLikeToMessage","RemoveMessage","BaseMessage"],"sources":["../../src/graph/messages_reducer.ts"],"sourcesContent":["import {\n BaseMessage,\n BaseMessageLike,\n coerceMessageLikeToMessage,\n RemoveMessage,\n} from \"@langchain/core/messages\";\nimport { v4 } from \"@langchain/core/utils/uuid\";\n\n/**\n * Special value that signifies the intent to remove all previous messages in the state reducer.\n * Used as the unique identifier for a `RemoveMessage` instance which, when encountered,\n * causes all prior messages to be discarded, leaving only those following this marker.\n */\nexport const REMOVE_ALL_MESSAGES = \"__remove_all__\";\n\n/**\n * Type that represents an acceptable input for the messages state reducer.\n *\n * - Can be a single `BaseMessage` or `BaseMessageLike`.\n * - Can be an array of `BaseMessage` or `BaseMessageLike`.\n */\nexport type Messages =\n | Array<BaseMessage | BaseMessageLike>\n | BaseMessage\n | BaseMessageLike;\n\n/**\n * Reducer function for combining two sets of messages in LangGraph's state system.\n *\n * This reducer handles several tasks:\n * 1. Normalizes both `left` and `right` message inputs to arrays.\n * 2. Coerces any message-like objects into real `BaseMessage` instances.\n * 3. Ensures all messages have unique, stable IDs by generating missing ones.\n * 4. If a `RemoveMessage` instance is encountered in `right` with the ID `REMOVE_ALL_MESSAGES`,\n * all previous messages are discarded and only the subsequent messages in `right` are returned.\n * 5. Otherwise, merges `left` and `right` messages together following these rules:\n * - If a message in `right` shares an ID with a message in `left`:\n * - If it is a `RemoveMessage`, that message (by ID) is marked for removal.\n * - If it is a normal message, it replaces the message with the same ID from `left`.\n * - If a message in `right` **does not exist** in `left`:\n * - If it is a `RemoveMessage`, this is considered an error (cannot remove non-existent ID).\n * - Otherwise, the message is appended.\n * - Messages flagged for removal are omitted from the final output.\n *\n * @param left - The existing array (or single message) of messages from current state.\n * @param right - The new array (or single message) of messages to be applied.\n * @returns A new array of `BaseMessage` objects representing the updated state.\n *\n * @throws Error if a `RemoveMessage` is used to delete a message with an ID that does not exist in the merged list.\n *\n * @example\n * ```ts\n * const msg1 = new AIMessage(\"hello\");\n * const msg2 = new HumanMessage(\"hi\");\n * const removal = new RemoveMessage({ id: msg1.id });\n * const newState = messagesStateReducer([msg1], [msg2, removal]);\n * // newState will only contain msg2 (msg1 is removed)\n * ```\n */\nexport function messagesStateReducer(\n left: Messages,\n right: Messages\n): BaseMessage[] {\n // Ensure both left and right are arrays\n const leftArray = Array.isArray(left) ? left : [left];\n const rightArray = Array.isArray(right) ? right : [right];\n\n // Convert all input to BaseMessage instances\n const leftMessages = (leftArray as BaseMessageLike[]).map(\n coerceMessageLikeToMessage\n );\n const rightMessages = (rightArray as BaseMessageLike[]).map(\n coerceMessageLikeToMessage\n );\n\n // Assign missing IDs to any message in the left array\n for (const m of leftMessages) {\n if (m.id === null || m.id === undefined) {\n m.id = v4();\n m.lc_kwargs.id = m.id;\n }\n }\n\n // Assign missing IDs and check for \"remove all\" marker in right array\n let removeAllIdx: number | undefined;\n for (let i = 0; i < rightMessages.length; i += 1) {\n const m = rightMessages[i];\n if (m.id === null || m.id === undefined) {\n m.id = v4();\n m.lc_kwargs.id = m.id;\n }\n\n // If RemoveMessage with special REMOVE_ALL_MESSAGES id is found\n if (RemoveMessage.isInstance(m) && m.id === REMOVE_ALL_MESSAGES) {\n removeAllIdx = i;\n }\n }\n\n // If remove-all is present, all previous messages are wiped; return only subsequent ones\n if (removeAllIdx != null) return rightMessages.slice(removeAllIdx + 1);\n\n // Begin normal merging logic\n const merged = [...leftMessages];\n const mergedById = new Map(merged.map((m, i) => [m.id, i]));\n const idsToRemove = new Set();\n\n for (const m of rightMessages) {\n const existingIdx = mergedById.get(m.id);\n if (existingIdx !== undefined) {\n // Case: updating or removing an existing message by id\n if (RemoveMessage.isInstance(m)) {\n idsToRemove.add(m.id);\n } else {\n idsToRemove.delete(m.id);\n merged[existingIdx] = m;\n }\n } else {\n // Case: inserting a completely new message\n if (RemoveMessage.isInstance(m)) {\n throw new Error(\n `Attempting to delete a message with an ID that doesn't exist ('${m.id}')`\n );\n }\n mergedById.set(m.id, merged.length);\n merged.push(m);\n }\n }\n\n // Remove any messages whose IDs are marked for removal\n return merged.filter((m) => !idsToRemove.has(m.id));\n}\n\n/**\n * **Experimental.** Batch reducer for use with `DeltaChannel`.\n *\n * Processes all writes in one pass — dedup by ID and `RemoveMessage`\n * tombstoning — without calling {@link messagesStateReducer}.\n *\n * This reducer is batching-invariant, as required by `DeltaChannel`:\n * `reducer(reducer(state, xs), ys) === reducer(state, xs.concat(ys))`.\n *\n * A `RemoveMessage` carrying the {@link REMOVE_ALL_MESSAGES} sentinel id\n * clears all messages accumulated so far (prior state plus earlier writes in\n * the same batch) and keeps only the messages that follow it, mirroring\n * {@link messagesStateReducer}. Clearing happens in the same single linear\n * pass, so the batching-invariant still holds.\n *\n * Raw object / string inputs are coerced to typed `BaseMessage` objects so\n * that HTTP-driven graphs work without a separate coercion step. This is not\n * full {@link messagesStateReducer} parity — unknown-id `RemoveMessage`\n * errors and missing-id UUID assignment are not handled here.\n *\n * @param state - The current accumulated list of messages.\n * @param writes - Batch of writes, each a single message-like or an array.\n * @returns The new accumulated list of messages.\n *\n * @example\n * ```typescript\n * import { DeltaChannel, messagesDeltaReducer } from \"@langchain/langgraph\";\n *\n * const channel = new DeltaChannel(messagesDeltaReducer);\n * ```\n */\nexport function messagesDeltaReducer(\n state: BaseMessage[],\n writes: Messages[]\n): BaseMessage[] {\n // Each write is either an array of message-likes or a single message-like.\n // Only arrays flatten; everything else is one message.\n const flat: BaseMessageLike[] = [];\n for (const w of writes) {\n if (Array.isArray(w)) {\n flat.push(...(w as BaseMessageLike[]));\n } else {\n flat.push(w as BaseMessageLike);\n }\n }\n\n // Steady state: the reducer's own output is already typed, so skip coercion\n // on state when the first element is a BaseMessage. Only raw input (initial\n // objects, deserialized blobs) hits the slow path.\n const stateMsgs: BaseMessage[] =\n state.length > 0 && BaseMessage.isInstance(state[0])\n ? state\n : (state as BaseMessageLike[]).map(coerceMessageLikeToMessage);\n const msgs: BaseMessage[] = flat.map(coerceMessageLikeToMessage);\n\n const index = new Map<string, number>();\n for (let i = 0; i < stateMsgs.length; i += 1) {\n const mid = stateMsgs[i].id;\n if (mid != null) index.set(mid, i);\n }\n\n const result: (BaseMessage | null)[] = [...stateMsgs];\n for (const msg of msgs) {\n const mid = msg.id;\n if (RemoveMessage.isInstance(msg) && mid === REMOVE_ALL_MESSAGES) {\n // Discard everything accumulated so far (prior state and earlier writes\n // in this batch); only messages following the sentinel are kept. Doing\n // this inline keeps the reducer batching-invariant.\n result.length = 0;\n index.clear();\n } else if (mid == null) {\n result.push(msg);\n } else if (RemoveMessage.isInstance(msg)) {\n if (index.has(mid)) {\n result[index.get(mid)!] = null;\n index.delete(mid);\n }\n } else if (index.has(mid)) {\n result[index.get(mid)!] = msg;\n } else {\n index.set(mid, result.length);\n result.push(msg);\n }\n }\n return result.filter((m): m is BaseMessage => m !== null);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"messages_reducer.cjs","names":["coerceMessageLikeToMessage","RemoveMessage","BaseMessage"],"sources":["../../src/graph/messages_reducer.ts"],"sourcesContent":["import {\n BaseMessage,\n BaseMessageLike,\n coerceMessageLikeToMessage,\n RemoveMessage,\n} from \"@langchain/core/messages\";\nimport { v4 } from \"@langchain/core/utils/uuid\";\n\n/**\n * Special value that signifies the intent to remove all previous messages in the state reducer.\n * Used as the unique identifier for a `RemoveMessage` instance which, when encountered,\n * causes all prior messages to be discarded, leaving only those following this marker.\n */\nexport const REMOVE_ALL_MESSAGES = \"__remove_all__\";\n\n/**\n * Type that represents an acceptable input for the messages state reducer.\n *\n * - Can be a single `BaseMessage` or `BaseMessageLike`.\n * - Can be an array of `BaseMessage` or `BaseMessageLike`.\n */\nexport type Messages =\n | Array<BaseMessage | BaseMessageLike>\n | BaseMessage\n | BaseMessageLike;\n\n/**\n * Reducer function for combining two sets of messages in LangGraph's state system.\n *\n * This reducer handles several tasks:\n * 1. Normalizes both `left` and `right` message inputs to arrays.\n * 2. Coerces any message-like objects into real `BaseMessage` instances.\n * 3. Ensures all messages have unique, stable IDs by generating missing ones.\n * 4. If a `RemoveMessage` instance is encountered in `right` with the ID `REMOVE_ALL_MESSAGES`,\n * all previous messages are discarded and only the subsequent messages in `right` are returned.\n * 5. Otherwise, merges `left` and `right` messages together following these rules:\n * - If a message in `right` shares an ID with a message in `left`:\n * - If it is a `RemoveMessage`, that message (by ID) is marked for removal.\n * - If it is a normal message, it replaces the message with the same ID from `left`.\n * - If a message in `right` **does not exist** in `left`:\n * - If it is a `RemoveMessage`, this is considered an error (cannot remove non-existent ID).\n * - Otherwise, the message is appended.\n * - Messages flagged for removal are omitted from the final output.\n *\n * @param left - The existing array (or single message) of messages from current state.\n * @param right - The new array (or single message) of messages to be applied.\n * @returns A new array of `BaseMessage` objects representing the updated state.\n *\n * @throws Error if a `RemoveMessage` is used to delete a message with an ID that does not exist in the merged list.\n *\n * @example\n * ```ts\n * const msg1 = new AIMessage(\"hello\");\n * const msg2 = new HumanMessage(\"hi\");\n * const removal = new RemoveMessage({ id: msg1.id });\n * const newState = messagesStateReducer([msg1], [msg2, removal]);\n * // newState will only contain msg2 (msg1 is removed)\n * ```\n */\nexport function messagesStateReducer(\n left: Messages,\n right: Messages\n): BaseMessage[] {\n // Ensure both left and right are arrays\n const leftArray = Array.isArray(left) ? left : [left];\n const rightArray = Array.isArray(right) ? right : [right];\n\n // Convert all input to BaseMessage instances\n const leftMessages = (leftArray as BaseMessageLike[]).map(\n coerceMessageLikeToMessage\n );\n const rightMessages = (rightArray as BaseMessageLike[]).map(\n coerceMessageLikeToMessage\n );\n\n // Assign missing IDs to any message in the left array\n for (const m of leftMessages) {\n if (m.id === null || m.id === undefined) {\n m.id = v4();\n m.lc_kwargs.id = m.id;\n }\n }\n\n // Assign missing IDs and check for \"remove all\" marker in right array\n let removeAllIdx: number | undefined;\n for (let i = 0; i < rightMessages.length; i += 1) {\n const m = rightMessages[i];\n if (m.id === null || m.id === undefined) {\n m.id = v4();\n m.lc_kwargs.id = m.id;\n }\n\n // If RemoveMessage with special REMOVE_ALL_MESSAGES id is found\n if (RemoveMessage.isInstance(m) && m.id === REMOVE_ALL_MESSAGES) {\n removeAllIdx = i;\n }\n }\n\n // If remove-all is present, all previous messages are wiped; return only subsequent ones\n if (removeAllIdx != null) return rightMessages.slice(removeAllIdx + 1);\n\n // Begin normal merging logic\n const merged = [...leftMessages];\n const mergedById = new Map(merged.map((m, i) => [m.id, i]));\n const idsToRemove = new Set();\n\n for (const m of rightMessages) {\n const existingIdx = mergedById.get(m.id);\n if (existingIdx !== undefined) {\n // Case: updating or removing an existing message by id\n if (RemoveMessage.isInstance(m)) {\n idsToRemove.add(m.id);\n } else {\n idsToRemove.delete(m.id);\n merged[existingIdx] = m;\n }\n } else {\n // Case: inserting a completely new message\n if (RemoveMessage.isInstance(m)) {\n throw new Error(\n `Attempting to delete a message with an ID that doesn't exist ('${m.id}')`\n );\n }\n mergedById.set(m.id, merged.length);\n merged.push(m);\n }\n }\n\n // Remove any messages whose IDs are marked for removal\n return merged.filter((m) => !idsToRemove.has(m.id));\n}\n\n/**\n * **Experimental.** Batch reducer for use with `DeltaChannel`.\n *\n * Processes all writes in one pass — dedup by ID and `RemoveMessage`\n * tombstoning — without calling {@link messagesStateReducer}.\n *\n * This reducer is batching-invariant, as required by `DeltaChannel`:\n * `reducer(reducer(state, xs), ys) === reducer(state, xs.concat(ys))`.\n *\n * A `RemoveMessage` carrying the {@link REMOVE_ALL_MESSAGES} sentinel id\n * clears all messages accumulated so far (prior state plus earlier writes in\n * the same batch) and keeps only the messages that follow it, mirroring\n * {@link messagesStateReducer}. Clearing happens in the same single linear\n * pass, so the batching-invariant still holds.\n *\n * Raw object / string inputs are coerced to typed `BaseMessage` objects so\n * that HTTP-driven graphs work without a separate coercion step. This is not\n * full {@link messagesStateReducer} parity — unknown-id `RemoveMessage`\n * errors and missing-id UUID assignment are not handled here.\n *\n * @param state - The current accumulated list of messages.\n * @param writes - Batch of writes, each a single message-like or an array.\n * @returns The new accumulated list of messages.\n *\n * @example\n * ```typescript\n * import { DeltaChannel, messagesDeltaReducer } from \"@langchain/langgraph\";\n *\n * const channel = new DeltaChannel(messagesDeltaReducer);\n * ```\n */\nexport function messagesDeltaReducer(\n state: BaseMessage[],\n writes: Messages[]\n): BaseMessage[] {\n // Each write is either an array of message-likes or a single message-like.\n // Only arrays flatten; everything else is one message.\n const flat: BaseMessageLike[] = [];\n for (const w of writes) {\n if (Array.isArray(w)) {\n flat.push(...(w as BaseMessageLike[]));\n } else {\n flat.push(w as BaseMessageLike);\n }\n }\n\n // Steady state: the reducer's own output is already typed, so skip coercion\n // on state when the first element is a BaseMessage. Only raw input (initial\n // objects, deserialized blobs) hits the slow path.\n const stateMsgs: BaseMessage[] =\n state.length > 0 && BaseMessage.isInstance(state[0])\n ? state\n : (state as BaseMessageLike[]).map(coerceMessageLikeToMessage);\n const msgs: BaseMessage[] = flat.map(coerceMessageLikeToMessage);\n\n const index = new Map<string, number>();\n for (let i = 0; i < stateMsgs.length; i += 1) {\n const mid = stateMsgs[i].id;\n if (mid != null) index.set(mid, i);\n }\n\n const result: (BaseMessage | null)[] = [...stateMsgs];\n for (const msg of msgs) {\n const mid = msg.id;\n if (RemoveMessage.isInstance(msg) && mid === REMOVE_ALL_MESSAGES) {\n // Discard everything accumulated so far (prior state and earlier writes\n // in this batch); only messages following the sentinel are kept. Doing\n // this inline keeps the reducer batching-invariant.\n result.length = 0;\n index.clear();\n } else if (mid == null) {\n result.push(msg);\n } else if (RemoveMessage.isInstance(msg)) {\n if (index.has(mid)) {\n result[index.get(mid)!] = null;\n index.delete(mid);\n }\n } else if (index.has(mid)) {\n result[index.get(mid)!] = msg;\n } else {\n index.set(mid, result.length);\n result.push(msg);\n }\n }\n return result.filter((m): m is BaseMessage => m !== null);\n}\n\n// A brand rather than reference equality, so the reducer is still recognized\n// from another copy of this package (its ESM and CJS builds both loaded, or a\n// copy nested under another package).\nconst MESSAGES_DELTA_REDUCER = Symbol.for(\"langgraph.messages_delta_reducer\");\nObject.defineProperty(messagesDeltaReducer, MESSAGES_DELTA_REDUCER, {\n value: true,\n});\n\n/** @internal */\nexport function isMessagesDeltaReducer(reducer: unknown): boolean {\n return (\n typeof reducer === \"function\" &&\n (reducer as unknown as Record<symbol, unknown>)[MESSAGES_DELTA_REDUCER] ===\n true\n );\n}\n"],"mappings":"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"}
@@ -139,7 +139,13 @@ function messagesDeltaReducer(state, writes) {
139
139
  }
140
140
  return result.filter((m) => m !== null);
141
141
  }
142
+ const MESSAGES_DELTA_REDUCER = Symbol.for("langgraph.messages_delta_reducer");
143
+ Object.defineProperty(messagesDeltaReducer, MESSAGES_DELTA_REDUCER, { value: true });
144
+ /** @internal */
145
+ function isMessagesDeltaReducer(reducer) {
146
+ return typeof reducer === "function" && reducer[MESSAGES_DELTA_REDUCER] === true;
147
+ }
142
148
  //#endregion
143
- export { REMOVE_ALL_MESSAGES, messagesDeltaReducer, messagesStateReducer };
149
+ export { REMOVE_ALL_MESSAGES, isMessagesDeltaReducer, messagesDeltaReducer, messagesStateReducer };
144
150
 
145
151
  //# sourceMappingURL=messages_reducer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"messages_reducer.js","names":[],"sources":["../../src/graph/messages_reducer.ts"],"sourcesContent":["import {\n BaseMessage,\n BaseMessageLike,\n coerceMessageLikeToMessage,\n RemoveMessage,\n} from \"@langchain/core/messages\";\nimport { v4 } from \"@langchain/core/utils/uuid\";\n\n/**\n * Special value that signifies the intent to remove all previous messages in the state reducer.\n * Used as the unique identifier for a `RemoveMessage` instance which, when encountered,\n * causes all prior messages to be discarded, leaving only those following this marker.\n */\nexport const REMOVE_ALL_MESSAGES = \"__remove_all__\";\n\n/**\n * Type that represents an acceptable input for the messages state reducer.\n *\n * - Can be a single `BaseMessage` or `BaseMessageLike`.\n * - Can be an array of `BaseMessage` or `BaseMessageLike`.\n */\nexport type Messages =\n | Array<BaseMessage | BaseMessageLike>\n | BaseMessage\n | BaseMessageLike;\n\n/**\n * Reducer function for combining two sets of messages in LangGraph's state system.\n *\n * This reducer handles several tasks:\n * 1. Normalizes both `left` and `right` message inputs to arrays.\n * 2. Coerces any message-like objects into real `BaseMessage` instances.\n * 3. Ensures all messages have unique, stable IDs by generating missing ones.\n * 4. If a `RemoveMessage` instance is encountered in `right` with the ID `REMOVE_ALL_MESSAGES`,\n * all previous messages are discarded and only the subsequent messages in `right` are returned.\n * 5. Otherwise, merges `left` and `right` messages together following these rules:\n * - If a message in `right` shares an ID with a message in `left`:\n * - If it is a `RemoveMessage`, that message (by ID) is marked for removal.\n * - If it is a normal message, it replaces the message with the same ID from `left`.\n * - If a message in `right` **does not exist** in `left`:\n * - If it is a `RemoveMessage`, this is considered an error (cannot remove non-existent ID).\n * - Otherwise, the message is appended.\n * - Messages flagged for removal are omitted from the final output.\n *\n * @param left - The existing array (or single message) of messages from current state.\n * @param right - The new array (or single message) of messages to be applied.\n * @returns A new array of `BaseMessage` objects representing the updated state.\n *\n * @throws Error if a `RemoveMessage` is used to delete a message with an ID that does not exist in the merged list.\n *\n * @example\n * ```ts\n * const msg1 = new AIMessage(\"hello\");\n * const msg2 = new HumanMessage(\"hi\");\n * const removal = new RemoveMessage({ id: msg1.id });\n * const newState = messagesStateReducer([msg1], [msg2, removal]);\n * // newState will only contain msg2 (msg1 is removed)\n * ```\n */\nexport function messagesStateReducer(\n left: Messages,\n right: Messages\n): BaseMessage[] {\n // Ensure both left and right are arrays\n const leftArray = Array.isArray(left) ? left : [left];\n const rightArray = Array.isArray(right) ? right : [right];\n\n // Convert all input to BaseMessage instances\n const leftMessages = (leftArray as BaseMessageLike[]).map(\n coerceMessageLikeToMessage\n );\n const rightMessages = (rightArray as BaseMessageLike[]).map(\n coerceMessageLikeToMessage\n );\n\n // Assign missing IDs to any message in the left array\n for (const m of leftMessages) {\n if (m.id === null || m.id === undefined) {\n m.id = v4();\n m.lc_kwargs.id = m.id;\n }\n }\n\n // Assign missing IDs and check for \"remove all\" marker in right array\n let removeAllIdx: number | undefined;\n for (let i = 0; i < rightMessages.length; i += 1) {\n const m = rightMessages[i];\n if (m.id === null || m.id === undefined) {\n m.id = v4();\n m.lc_kwargs.id = m.id;\n }\n\n // If RemoveMessage with special REMOVE_ALL_MESSAGES id is found\n if (RemoveMessage.isInstance(m) && m.id === REMOVE_ALL_MESSAGES) {\n removeAllIdx = i;\n }\n }\n\n // If remove-all is present, all previous messages are wiped; return only subsequent ones\n if (removeAllIdx != null) return rightMessages.slice(removeAllIdx + 1);\n\n // Begin normal merging logic\n const merged = [...leftMessages];\n const mergedById = new Map(merged.map((m, i) => [m.id, i]));\n const idsToRemove = new Set();\n\n for (const m of rightMessages) {\n const existingIdx = mergedById.get(m.id);\n if (existingIdx !== undefined) {\n // Case: updating or removing an existing message by id\n if (RemoveMessage.isInstance(m)) {\n idsToRemove.add(m.id);\n } else {\n idsToRemove.delete(m.id);\n merged[existingIdx] = m;\n }\n } else {\n // Case: inserting a completely new message\n if (RemoveMessage.isInstance(m)) {\n throw new Error(\n `Attempting to delete a message with an ID that doesn't exist ('${m.id}')`\n );\n }\n mergedById.set(m.id, merged.length);\n merged.push(m);\n }\n }\n\n // Remove any messages whose IDs are marked for removal\n return merged.filter((m) => !idsToRemove.has(m.id));\n}\n\n/**\n * **Experimental.** Batch reducer for use with `DeltaChannel`.\n *\n * Processes all writes in one pass — dedup by ID and `RemoveMessage`\n * tombstoning — without calling {@link messagesStateReducer}.\n *\n * This reducer is batching-invariant, as required by `DeltaChannel`:\n * `reducer(reducer(state, xs), ys) === reducer(state, xs.concat(ys))`.\n *\n * A `RemoveMessage` carrying the {@link REMOVE_ALL_MESSAGES} sentinel id\n * clears all messages accumulated so far (prior state plus earlier writes in\n * the same batch) and keeps only the messages that follow it, mirroring\n * {@link messagesStateReducer}. Clearing happens in the same single linear\n * pass, so the batching-invariant still holds.\n *\n * Raw object / string inputs are coerced to typed `BaseMessage` objects so\n * that HTTP-driven graphs work without a separate coercion step. This is not\n * full {@link messagesStateReducer} parity — unknown-id `RemoveMessage`\n * errors and missing-id UUID assignment are not handled here.\n *\n * @param state - The current accumulated list of messages.\n * @param writes - Batch of writes, each a single message-like or an array.\n * @returns The new accumulated list of messages.\n *\n * @example\n * ```typescript\n * import { DeltaChannel, messagesDeltaReducer } from \"@langchain/langgraph\";\n *\n * const channel = new DeltaChannel(messagesDeltaReducer);\n * ```\n */\nexport function messagesDeltaReducer(\n state: BaseMessage[],\n writes: Messages[]\n): BaseMessage[] {\n // Each write is either an array of message-likes or a single message-like.\n // Only arrays flatten; everything else is one message.\n const flat: BaseMessageLike[] = [];\n for (const w of writes) {\n if (Array.isArray(w)) {\n flat.push(...(w as BaseMessageLike[]));\n } else {\n flat.push(w as BaseMessageLike);\n }\n }\n\n // Steady state: the reducer's own output is already typed, so skip coercion\n // on state when the first element is a BaseMessage. Only raw input (initial\n // objects, deserialized blobs) hits the slow path.\n const stateMsgs: BaseMessage[] =\n state.length > 0 && BaseMessage.isInstance(state[0])\n ? state\n : (state as BaseMessageLike[]).map(coerceMessageLikeToMessage);\n const msgs: BaseMessage[] = flat.map(coerceMessageLikeToMessage);\n\n const index = new Map<string, number>();\n for (let i = 0; i < stateMsgs.length; i += 1) {\n const mid = stateMsgs[i].id;\n if (mid != null) index.set(mid, i);\n }\n\n const result: (BaseMessage | null)[] = [...stateMsgs];\n for (const msg of msgs) {\n const mid = msg.id;\n if (RemoveMessage.isInstance(msg) && mid === REMOVE_ALL_MESSAGES) {\n // Discard everything accumulated so far (prior state and earlier writes\n // in this batch); only messages following the sentinel are kept. Doing\n // this inline keeps the reducer batching-invariant.\n result.length = 0;\n index.clear();\n } else if (mid == null) {\n result.push(msg);\n } else if (RemoveMessage.isInstance(msg)) {\n if (index.has(mid)) {\n result[index.get(mid)!] = null;\n index.delete(mid);\n }\n } else if (index.has(mid)) {\n result[index.get(mid)!] = msg;\n } else {\n index.set(mid, result.length);\n result.push(msg);\n }\n }\n return result.filter((m): m is BaseMessage => m !== null);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"messages_reducer.js","names":[],"sources":["../../src/graph/messages_reducer.ts"],"sourcesContent":["import {\n BaseMessage,\n BaseMessageLike,\n coerceMessageLikeToMessage,\n RemoveMessage,\n} from \"@langchain/core/messages\";\nimport { v4 } from \"@langchain/core/utils/uuid\";\n\n/**\n * Special value that signifies the intent to remove all previous messages in the state reducer.\n * Used as the unique identifier for a `RemoveMessage` instance which, when encountered,\n * causes all prior messages to be discarded, leaving only those following this marker.\n */\nexport const REMOVE_ALL_MESSAGES = \"__remove_all__\";\n\n/**\n * Type that represents an acceptable input for the messages state reducer.\n *\n * - Can be a single `BaseMessage` or `BaseMessageLike`.\n * - Can be an array of `BaseMessage` or `BaseMessageLike`.\n */\nexport type Messages =\n | Array<BaseMessage | BaseMessageLike>\n | BaseMessage\n | BaseMessageLike;\n\n/**\n * Reducer function for combining two sets of messages in LangGraph's state system.\n *\n * This reducer handles several tasks:\n * 1. Normalizes both `left` and `right` message inputs to arrays.\n * 2. Coerces any message-like objects into real `BaseMessage` instances.\n * 3. Ensures all messages have unique, stable IDs by generating missing ones.\n * 4. If a `RemoveMessage` instance is encountered in `right` with the ID `REMOVE_ALL_MESSAGES`,\n * all previous messages are discarded and only the subsequent messages in `right` are returned.\n * 5. Otherwise, merges `left` and `right` messages together following these rules:\n * - If a message in `right` shares an ID with a message in `left`:\n * - If it is a `RemoveMessage`, that message (by ID) is marked for removal.\n * - If it is a normal message, it replaces the message with the same ID from `left`.\n * - If a message in `right` **does not exist** in `left`:\n * - If it is a `RemoveMessage`, this is considered an error (cannot remove non-existent ID).\n * - Otherwise, the message is appended.\n * - Messages flagged for removal are omitted from the final output.\n *\n * @param left - The existing array (or single message) of messages from current state.\n * @param right - The new array (or single message) of messages to be applied.\n * @returns A new array of `BaseMessage` objects representing the updated state.\n *\n * @throws Error if a `RemoveMessage` is used to delete a message with an ID that does not exist in the merged list.\n *\n * @example\n * ```ts\n * const msg1 = new AIMessage(\"hello\");\n * const msg2 = new HumanMessage(\"hi\");\n * const removal = new RemoveMessage({ id: msg1.id });\n * const newState = messagesStateReducer([msg1], [msg2, removal]);\n * // newState will only contain msg2 (msg1 is removed)\n * ```\n */\nexport function messagesStateReducer(\n left: Messages,\n right: Messages\n): BaseMessage[] {\n // Ensure both left and right are arrays\n const leftArray = Array.isArray(left) ? left : [left];\n const rightArray = Array.isArray(right) ? right : [right];\n\n // Convert all input to BaseMessage instances\n const leftMessages = (leftArray as BaseMessageLike[]).map(\n coerceMessageLikeToMessage\n );\n const rightMessages = (rightArray as BaseMessageLike[]).map(\n coerceMessageLikeToMessage\n );\n\n // Assign missing IDs to any message in the left array\n for (const m of leftMessages) {\n if (m.id === null || m.id === undefined) {\n m.id = v4();\n m.lc_kwargs.id = m.id;\n }\n }\n\n // Assign missing IDs and check for \"remove all\" marker in right array\n let removeAllIdx: number | undefined;\n for (let i = 0; i < rightMessages.length; i += 1) {\n const m = rightMessages[i];\n if (m.id === null || m.id === undefined) {\n m.id = v4();\n m.lc_kwargs.id = m.id;\n }\n\n // If RemoveMessage with special REMOVE_ALL_MESSAGES id is found\n if (RemoveMessage.isInstance(m) && m.id === REMOVE_ALL_MESSAGES) {\n removeAllIdx = i;\n }\n }\n\n // If remove-all is present, all previous messages are wiped; return only subsequent ones\n if (removeAllIdx != null) return rightMessages.slice(removeAllIdx + 1);\n\n // Begin normal merging logic\n const merged = [...leftMessages];\n const mergedById = new Map(merged.map((m, i) => [m.id, i]));\n const idsToRemove = new Set();\n\n for (const m of rightMessages) {\n const existingIdx = mergedById.get(m.id);\n if (existingIdx !== undefined) {\n // Case: updating or removing an existing message by id\n if (RemoveMessage.isInstance(m)) {\n idsToRemove.add(m.id);\n } else {\n idsToRemove.delete(m.id);\n merged[existingIdx] = m;\n }\n } else {\n // Case: inserting a completely new message\n if (RemoveMessage.isInstance(m)) {\n throw new Error(\n `Attempting to delete a message with an ID that doesn't exist ('${m.id}')`\n );\n }\n mergedById.set(m.id, merged.length);\n merged.push(m);\n }\n }\n\n // Remove any messages whose IDs are marked for removal\n return merged.filter((m) => !idsToRemove.has(m.id));\n}\n\n/**\n * **Experimental.** Batch reducer for use with `DeltaChannel`.\n *\n * Processes all writes in one pass — dedup by ID and `RemoveMessage`\n * tombstoning — without calling {@link messagesStateReducer}.\n *\n * This reducer is batching-invariant, as required by `DeltaChannel`:\n * `reducer(reducer(state, xs), ys) === reducer(state, xs.concat(ys))`.\n *\n * A `RemoveMessage` carrying the {@link REMOVE_ALL_MESSAGES} sentinel id\n * clears all messages accumulated so far (prior state plus earlier writes in\n * the same batch) and keeps only the messages that follow it, mirroring\n * {@link messagesStateReducer}. Clearing happens in the same single linear\n * pass, so the batching-invariant still holds.\n *\n * Raw object / string inputs are coerced to typed `BaseMessage` objects so\n * that HTTP-driven graphs work without a separate coercion step. This is not\n * full {@link messagesStateReducer} parity — unknown-id `RemoveMessage`\n * errors and missing-id UUID assignment are not handled here.\n *\n * @param state - The current accumulated list of messages.\n * @param writes - Batch of writes, each a single message-like or an array.\n * @returns The new accumulated list of messages.\n *\n * @example\n * ```typescript\n * import { DeltaChannel, messagesDeltaReducer } from \"@langchain/langgraph\";\n *\n * const channel = new DeltaChannel(messagesDeltaReducer);\n * ```\n */\nexport function messagesDeltaReducer(\n state: BaseMessage[],\n writes: Messages[]\n): BaseMessage[] {\n // Each write is either an array of message-likes or a single message-like.\n // Only arrays flatten; everything else is one message.\n const flat: BaseMessageLike[] = [];\n for (const w of writes) {\n if (Array.isArray(w)) {\n flat.push(...(w as BaseMessageLike[]));\n } else {\n flat.push(w as BaseMessageLike);\n }\n }\n\n // Steady state: the reducer's own output is already typed, so skip coercion\n // on state when the first element is a BaseMessage. Only raw input (initial\n // objects, deserialized blobs) hits the slow path.\n const stateMsgs: BaseMessage[] =\n state.length > 0 && BaseMessage.isInstance(state[0])\n ? state\n : (state as BaseMessageLike[]).map(coerceMessageLikeToMessage);\n const msgs: BaseMessage[] = flat.map(coerceMessageLikeToMessage);\n\n const index = new Map<string, number>();\n for (let i = 0; i < stateMsgs.length; i += 1) {\n const mid = stateMsgs[i].id;\n if (mid != null) index.set(mid, i);\n }\n\n const result: (BaseMessage | null)[] = [...stateMsgs];\n for (const msg of msgs) {\n const mid = msg.id;\n if (RemoveMessage.isInstance(msg) && mid === REMOVE_ALL_MESSAGES) {\n // Discard everything accumulated so far (prior state and earlier writes\n // in this batch); only messages following the sentinel are kept. Doing\n // this inline keeps the reducer batching-invariant.\n result.length = 0;\n index.clear();\n } else if (mid == null) {\n result.push(msg);\n } else if (RemoveMessage.isInstance(msg)) {\n if (index.has(mid)) {\n result[index.get(mid)!] = null;\n index.delete(mid);\n }\n } else if (index.has(mid)) {\n result[index.get(mid)!] = msg;\n } else {\n index.set(mid, result.length);\n result.push(msg);\n }\n }\n return result.filter((m): m is BaseMessage => m !== null);\n}\n\n// A brand rather than reference equality, so the reducer is still recognized\n// from another copy of this package (its ESM and CJS builds both loaded, or a\n// copy nested under another package).\nconst MESSAGES_DELTA_REDUCER = Symbol.for(\"langgraph.messages_delta_reducer\");\nObject.defineProperty(messagesDeltaReducer, MESSAGES_DELTA_REDUCER, {\n value: true,\n});\n\n/** @internal */\nexport function isMessagesDeltaReducer(reducer: unknown): boolean {\n return (\n typeof reducer === \"function\" &&\n (reducer as unknown as Record<symbol, unknown>)[MESSAGES_DELTA_REDUCER] ===\n true\n );\n}\n"],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -8,6 +8,7 @@ const require_annotation = require("./graph/annotation.cjs");
8
8
  const require_config = require("./pregel/utils/config.cjs");
9
9
  const require_callbacks = require("./callbacks.cjs");
10
10
  const require_runtime = require("./pregel/runtime.cjs");
11
+ const require_messages_reducer = require("./graph/messages_reducer.cjs");
11
12
  const require_convert = require("./stream/convert.cjs");
12
13
  const require_stream_channel = require("./stream/stream-channel.cjs");
13
14
  const require_lifecycle = require("./stream/transformers/lifecycle.cjs");
@@ -26,7 +27,6 @@ const require_reduced = require("./state/values/reduced.cjs");
26
27
  const require_untracked = require("./state/values/untracked.cjs");
27
28
  const require_delta$1 = require("./state/values/delta.cjs");
28
29
  const require_schema = require("./state/schema.cjs");
29
- const require_messages_reducer = require("./graph/messages_reducer.cjs");
30
30
  const require_messages$1 = require("./state/prebuilt/messages.cjs");
31
31
  const require_state = require("./graph/state.cjs");
32
32
  const require_message = require("./graph/message.cjs");
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { Annotation } from "./graph/annotation.js";
7
7
  import { getConfig, getCurrentTaskInput, getStore, getWriter } from "./pregel/utils/config.js";
8
8
  import { GraphCallbackHandler } from "./callbacks.js";
9
9
  import { RunControl } from "./pregel/runtime.js";
10
+ import { REMOVE_ALL_MESSAGES, messagesDeltaReducer, messagesStateReducer } from "./graph/messages_reducer.js";
10
11
  import { STREAM_EVENTS_V3_MODES, convertToProtocolEvent, isCheckpointEnvelope } from "./stream/convert.js";
11
12
  import { StreamChannel } from "./stream/stream-channel.js";
12
13
  import { createLifecycleTransformer, filterLifecycleEntries } from "./stream/transformers/lifecycle.js";
@@ -26,7 +27,6 @@ import { ReducedValue } from "./state/values/reduced.js";
26
27
  import { UntrackedValue } from "./state/values/untracked.js";
27
28
  import { DeltaValue } from "./state/values/delta.js";
28
29
  import { StateSchema } from "./state/schema.js";
29
- import { REMOVE_ALL_MESSAGES, messagesDeltaReducer, messagesStateReducer } from "./graph/messages_reducer.js";
30
30
  import { MessagesDeltaValue, MessagesValue } from "./state/prebuilt/messages.js";
31
31
  import { CompiledStateGraph, StateGraph } from "./graph/state.js";
32
32
  import { MessageGraph, pushMessage } from "./graph/message.js";
@@ -12,6 +12,28 @@ let _langchain_core_runnables = require("@langchain/core/runnables");
12
12
  const increment = (current) => {
13
13
  return current !== void 0 ? current + 1 : 1;
14
14
  };
15
+ /**
16
+ * Serialize a task path to the string stored with its writes, matching
17
+ * Python's `task_path_str` byte for byte:
18
+ *
19
+ * - arrays (nested paths included) become `"~"` + their serialized elements
20
+ * joined by `", "` — so the empty path is `"~"`;
21
+ * - booleans format as ints (Python's `isinstance(x, int)` is true for
22
+ * `bool` there): `true` → `"0000000001"`, `false` → `"0000000000"`;
23
+ * - ints are zero-padded to 10 digits, sign-aware: `-1` → `"-000000001"`;
24
+ * - anything else (node names) is used as-is.
25
+ *
26
+ * This is the `taskPath` passed to `BaseCheckpointSaver.putWrites` and the
27
+ * first component of `writesSortKey`, so the stored string and its sort
28
+ * order are identical across the JS and Python runtimes.
29
+ */
30
+ function taskPathStr(path) {
31
+ if (Array.isArray(path)) return `~${path.map(taskPathStr).join(", ")}`;
32
+ if (typeof path === "boolean") return path ? "0000000001" : "0000000000";
33
+ if (typeof path === "number") return path < 0 ? `-${String(-path).padStart(9, "0")}` : String(path).padStart(10, "0");
34
+ if (path === void 0 || path === null) return "~";
35
+ return String(path);
36
+ }
15
37
  function triggersNextStep(updatedChannels, triggerToNodes) {
16
38
  if (triggerToNodes == null) return false;
17
39
  for (const chan of updatedChannels) if (triggerToNodes[chan]) return true;
@@ -87,17 +109,9 @@ const IGNORE = new Set([
87
109
  ]);
88
110
  const RESERVED_SET = new Set(require_constants.RESERVED);
89
111
  function _applyWrites(checkpoint, channels, tasks, getNextVersion, triggerToNodes) {
90
- const pathCache = /* @__PURE__ */ new Map();
91
- for (const task of tasks) pathCache.set(task, task.path?.slice(0, 3) || []);
92
- tasks.sort((a, b) => {
93
- const aPath = pathCache.get(a);
94
- const bPath = pathCache.get(b);
95
- for (let i = 0; i < Math.min(aPath.length, bPath.length); i += 1) {
96
- if (aPath[i] < bPath[i]) return -1;
97
- if (aPath[i] > bPath[i]) return 1;
98
- }
99
- return aPath.length - bPath.length;
100
- });
112
+ const sortKeys = /* @__PURE__ */ new Map();
113
+ for (const task of tasks) sortKeys.set(task, (0, _langchain_langgraph_checkpoint.writesSortKey)(taskPathStr(task.path)));
114
+ tasks.sort((a, b) => (0, _langchain_langgraph_checkpoint.compareWritesSortKeys)(sortKeys.get(a), sortKeys.get(b)));
101
115
  const onlyChannels = require_base.getOnlyChannels(channels);
102
116
  let bumpStep = false;
103
117
  const channelsToConsume = /* @__PURE__ */ new Set();
@@ -110,36 +124,20 @@ function _applyWrites(checkpoint, channels, tasks, getNextVersion, triggerToNode
110
124
  }
111
125
  }
112
126
  let maxVersion = maxChannelMapVersion(checkpoint.channel_versions);
113
- let usedNewVersion = false;
127
+ let consumedVersion;
114
128
  for (const chan of channelsToConsume) if (chan in onlyChannels && onlyChannels[chan].consume()) {
115
129
  if (getNextVersion !== void 0) {
116
- checkpoint.channel_versions[chan] = getNextVersion(maxVersion);
117
- usedNewVersion = true;
130
+ consumedVersion ??= getNextVersion(maxVersion);
131
+ checkpoint.channel_versions[chan] = consumedVersion;
118
132
  }
119
133
  }
120
134
  const pendingWritesByChannel = {};
121
- const pendingWriteTaskIdsByChannel = {};
122
- for (const task of tasks) {
123
- const taskId = task.id ?? "";
124
- for (const [chan, val] of task.writes) if (IGNORE.has(chan)) {} else if (chan in onlyChannels) {
125
- pendingWritesByChannel[chan] ??= [];
126
- pendingWritesByChannel[chan].push(val);
127
- pendingWriteTaskIdsByChannel[chan] ??= [];
128
- pendingWriteTaskIdsByChannel[chan].push(taskId);
129
- }
130
- }
131
- for (const [chan, vals] of Object.entries(pendingWritesByChannel)) {
132
- if (vals.length < 2) continue;
133
- if (onlyChannels[chan]?.lc_graph_name !== "DeltaChannel") continue;
134
- const taskIds = pendingWriteTaskIdsByChannel[chan];
135
- const paired = vals.map((val, i) => ({
136
- val,
137
- taskId: taskIds[i]
138
- }));
139
- paired.sort((a, b) => a.taskId < b.taskId ? -1 : a.taskId > b.taskId ? 1 : 0);
140
- pendingWritesByChannel[chan] = paired.map((p) => p.val);
135
+ for (const task of tasks) for (const [chan, val] of task.writes) if (IGNORE.has(chan)) {} else if (chan in onlyChannels) {
136
+ pendingWritesByChannel[chan] ??= [];
137
+ pendingWritesByChannel[chan].push(val);
141
138
  }
142
- if (maxVersion != null && getNextVersion != null) maxVersion = usedNewVersion ? getNextVersion(maxVersion) : maxVersion;
139
+ if (maxVersion != null && consumedVersion !== void 0) maxVersion = consumedVersion;
140
+ const nextVersion = getNextVersion?.(maxVersion);
143
141
  const updatedChannels = /* @__PURE__ */ new Set();
144
142
  for (const [chan, vals] of Object.entries(pendingWritesByChannel)) if (chan in onlyChannels) {
145
143
  const channel = onlyChannels[chan];
@@ -154,7 +152,7 @@ function _applyWrites(checkpoint, channels, tasks, getNextVersion, triggerToNode
154
152
  } else throw e;
155
153
  }
156
154
  if (updated && getNextVersion !== void 0) {
157
- checkpoint.channel_versions[chan] = getNextVersion(maxVersion);
155
+ checkpoint.channel_versions[chan] = nextVersion;
158
156
  if (channel.isAvailable()) updatedChannels.add(chan);
159
157
  }
160
158
  }
@@ -163,7 +161,7 @@ function _applyWrites(checkpoint, channels, tasks, getNextVersion, triggerToNode
163
161
  const channel = onlyChannels[chan];
164
162
  if (channel.isAvailable() && !updatedChannels.has(chan)) {
165
163
  if (channel.update([]) && getNextVersion !== void 0) {
166
- checkpoint.channel_versions[chan] = getNextVersion(maxVersion);
164
+ checkpoint.channel_versions[chan] = nextVersion;
167
165
  if (channel.isAvailable()) updatedChannels.add(chan);
168
166
  }
169
167
  }
@@ -172,7 +170,7 @@ function _applyWrites(checkpoint, channels, tasks, getNextVersion, triggerToNode
172
170
  if (!Object.prototype.hasOwnProperty.call(onlyChannels, chan)) continue;
173
171
  const channel = onlyChannels[chan];
174
172
  if (channel.finish() && getNextVersion !== void 0) {
175
- checkpoint.channel_versions[chan] = getNextVersion(maxVersion);
173
+ checkpoint.channel_versions[chan] = nextVersion;
176
174
  if (channel.isAvailable()) updatedChannels.add(chan);
177
175
  }
178
176
  }
@@ -373,11 +371,12 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
373
371
  index.toString()
374
372
  ]), checkpoint.id);
375
373
  const taskCheckpointNamespace = `${checkpointNamespace}:${taskId}`;
374
+ const translatedTaskPath = [...taskPath.slice(0, 3), false];
376
375
  let metadata = {
377
376
  langgraph_step: step,
378
377
  langgraph_node: packet.node,
379
378
  langgraph_triggers: triggers,
380
- langgraph_path: taskPath.slice(0, 3),
379
+ langgraph_path: translatedTaskPath,
381
380
  langgraph_checkpoint_ns: taskCheckpointNamespace,
382
381
  checkpoint_ns: taskCheckpointNamespace
383
382
  };
@@ -419,7 +418,7 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
419
418
  name: packet.node,
420
419
  writes,
421
420
  triggers,
422
- path: taskPath
421
+ path: translatedTaskPath
423
422
  }, select_, fresh_),
424
423
  [require_constants.CONFIG_KEY_CHECKPOINTER]: checkpointer ?? configurable["__pregel_checkpointer"],
425
424
  [require_constants.CONFIG_KEY_CHECKPOINT_MAP]: {
@@ -453,7 +452,7 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
453
452
  ttl: proc.cachePolicy.ttl
454
453
  } : void 0,
455
454
  id: taskId,
456
- path: taskPath,
455
+ path: translatedTaskPath,
457
456
  writers: proc.getWriters(),
458
457
  timeout: packet.timeout ?? proc.timeout
459
458
  };
@@ -462,7 +461,7 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
462
461
  id: taskId,
463
462
  name: packet.node,
464
463
  interrupts: [],
465
- path: taskPath
464
+ path: translatedTaskPath
466
465
  };
467
466
  } else if (taskPath[0] === "__pregel_pull") {
468
467
  const name = taskPath[1].toString();
@@ -624,10 +623,11 @@ function _prepareNodeErrorHandlerTask(failedTask, handlerNodeName, error, checkp
624
623
  failedTask.id
625
624
  ]), checkpoint.id);
626
625
  const taskCheckpointNamespace = `${checkpointNamespace}:${taskId}`;
626
+ const failedPath = failedTask.path?.slice(0, 3) ?? [];
627
627
  const taskPath = [
628
- require_constants.PUSH,
629
- String(failedTask.name),
630
- handlerNodeName,
628
+ failedPath[0] ?? "__pregel_push",
629
+ ...failedPath.slice(1),
630
+ "node_error_handler",
631
631
  false
632
632
  ];
633
633
  let metadata = {
@@ -793,5 +793,6 @@ exports._prepareSingleTask = _prepareSingleTask;
793
793
  exports.increment = increment;
794
794
  exports.sanitizeUntrackedValuesInSend = sanitizeUntrackedValuesInSend;
795
795
  exports.shouldInterrupt = shouldInterrupt;
796
+ exports.taskPathStr = taskPathStr;
796
797
 
797
798
  //# sourceMappingURL=algo.cjs.map