UNPKG

@langchain/langgraph

Version:

LangGraph

1,107 lines 69.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Pregel = exports.Channel = void 0; /* eslint-disable no-param-reassign */ const runnables_1 = require("@langchain/core/runnables"); const langgraph_checkpoint_1 = require("@langchain/langgraph-checkpoint"); const base_js_1 = require("../channels/base.cjs"); const read_js_1 = require("./read.cjs"); const validate_js_1 = require("./validate.cjs"); const io_js_1 = require("./io.cjs"); const debug_js_1 = require("./debug.cjs"); const write_js_1 = require("./write.cjs"); const constants_js_1 = require("../constants.cjs"); const errors_js_1 = require("../errors.cjs"); const algo_js_1 = require("./algo.cjs"); const index_js_1 = require("./utils/index.cjs"); const subgraph_js_1 = require("./utils/subgraph.cjs"); const loop_js_1 = require("./loop.cjs"); const base_js_2 = require("../managed/base.cjs"); const utils_js_1 = require("../utils.cjs"); const config_js_1 = require("./utils/config.cjs"); const messages_js_1 = require("./messages.cjs"); const runner_js_1 = require("./runner.cjs"); const stream_js_1 = require("./stream.cjs"); /** * Utility class for working with channels in the Pregel system. * Provides static methods for subscribing to channels and writing to them. * * Channels are the communication pathways between nodes in a Pregel graph. * They enable message passing and state updates between different parts of the graph. */ class Channel { static subscribeTo(channels, options) { const { key, tags } = { key: undefined, tags: undefined, ...(options ?? {}), }; if (Array.isArray(channels) && key !== undefined) { throw new Error("Can't specify a key when subscribing to multiple channels"); } let channelMappingOrArray; if (typeof channels === "string") { if (key) { channelMappingOrArray = { [key]: channels }; } else { channelMappingOrArray = [channels]; } } else { channelMappingOrArray = Object.fromEntries(channels.map((chan) => [chan, chan])); } const triggers = Array.isArray(channels) ? channels : [channels]; return new read_js_1.PregelNode({ channels: channelMappingOrArray, triggers, tags, }); } /** * Creates a ChannelWrite that specifies how to write values to channels. * This is used to define how nodes send output to channels. * * @example * ```typescript * // Write to multiple channels * const write = Channel.writeTo(["output", "state"]); * * // Write with specific values * const write = Channel.writeTo(["output"], { * state: "completed", * result: calculateResult() * }); * * // Write with a transformation function * const write = Channel.writeTo(["output"], { * result: (x) => processResult(x) * }); * ``` * * @param channels - Array of channel names to write to * @param writes - Optional map of channel names to values or transformations * @returns A ChannelWrite object that can be used to write to the specified channels */ static writeTo(channels, writes) { const channelWriteEntries = []; for (const channel of channels) { channelWriteEntries.push({ channel, value: write_js_1.PASSTHROUGH, skipNone: false, }); } for (const [key, value] of Object.entries(writes ?? {})) { if (runnables_1.Runnable.isRunnable(value) || typeof value === "function") { channelWriteEntries.push({ channel: key, value: write_js_1.PASSTHROUGH, skipNone: true, mapper: (0, runnables_1._coerceToRunnable)(value), }); } else { channelWriteEntries.push({ channel: key, value, skipNone: false, }); } } return new write_js_1.ChannelWrite(channelWriteEntries); } } exports.Channel = Channel; // This is a workaround to allow Pregel to override `invoke` / `stream` and `withConfig` // without having to adhere to the types in the `Runnable` class (thanks to `any`). // Alternatively we could mark those methods with @ts-ignore / @ts-expect-error, // but these do not get carried over when building via `tsc`. class PartialRunnable extends runnables_1.Runnable { constructor() { super(...arguments); Object.defineProperty(this, "lc_namespace", { enumerable: true, configurable: true, writable: true, value: ["langgraph", "pregel"] }); } invoke(_input, _options // eslint-disable-next-line @typescript-eslint/no-explicit-any ) { throw new Error("Not implemented"); } // Overriden by `Pregel` withConfig(_config) { return super.withConfig(_config); } // Overriden by `Pregel` stream(input, options // eslint-disable-next-line @typescript-eslint/no-explicit-any ) { return super.stream(input, options); } } /** * The Pregel class is the core runtime engine of LangGraph, implementing a message-passing graph computation model * inspired by [Google's Pregel system](https://research.google/pubs/pregel-a-system-for-large-scale-graph-processing/). * It provides the foundation for building reliable, controllable agent workflows that can evolve state over time. * * Key features: * - Message passing between nodes in discrete "supersteps" * - Built-in persistence layer through checkpointers * - First-class streaming support for values, updates, and events * - Human-in-the-loop capabilities via interrupts * - Support for parallel node execution within supersteps * * The Pregel class is not intended to be instantiated directly by consumers. Instead, use the following higher-level APIs: * - {@link StateGraph}: The main graph class for building agent workflows * - Compiling a {@link StateGraph} will return a {@link CompiledGraph} instance, which extends `Pregel` * - Functional API: A declarative approach using tasks and entrypoints * - A `Pregel` instance is returned by the {@link entrypoint} function * * @example * ```typescript * // Using StateGraph API * const graph = new StateGraph(annotation) * .addNode("nodeA", myNodeFunction) * .addEdge("nodeA", "nodeB") * .compile(); * * // The compiled graph is a Pregel instance * const result = await graph.invoke(input); * ``` * * @example * ```typescript * // Using Functional API * import { task, entrypoint } from "@langchain/langgraph"; * import { MemorySaver } from "@langchain/langgraph-checkpoint"; * * // Define tasks that can be composed * const addOne = task("add", async (x: number) => x + 1); * * // Create a workflow using the entrypoint function * const workflow = entrypoint({ * name: "workflow", * checkpointer: new MemorySaver() * }, async (numbers: number[]) => { * // Tasks can be run in parallel * const results = await Promise.all(numbers.map(n => addOne(n))); * return results; * }); * * // The workflow is a Pregel instance * const result = await workflow.invoke([1, 2, 3]); // Returns [2, 3, 4] * ``` * * @typeParam Nodes - Mapping of node names to their {@link PregelNode} implementations * @typeParam Channels - Mapping of channel names to their {@link BaseChannel} or {@link ManagedValueSpec} implementations * @typeParam ConfigurableFieldType - Type of configurable fields that can be passed to the graph * @typeParam InputType - Type of input values accepted by the graph * @typeParam OutputType - Type of output values produced by the graph */ class Pregel extends PartialRunnable { /** * Name of the class when serialized * @internal */ static lc_name() { return "LangGraph"; } /** * Constructor for Pregel - meant for internal use only. * * @internal */ constructor(fields) { super(fields); /** @internal LangChain namespace for serialization necessary because Pregel extends Runnable */ Object.defineProperty(this, "lc_namespace", { enumerable: true, configurable: true, writable: true, value: ["langgraph", "pregel"] }); /** @internal Flag indicating this is a Pregel instance - necessary for serialization */ Object.defineProperty(this, "lg_is_pregel", { enumerable: true, configurable: true, writable: true, value: true }); /** The nodes in the graph, mapping node names to their PregelNode instances */ Object.defineProperty(this, "nodes", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** The channels in the graph, mapping channel names to their BaseChannel or ManagedValueSpec instances */ Object.defineProperty(this, "channels", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * The input channels for the graph. These channels receive the initial input when the graph is invoked. * Can be a single channel key or an array of channel keys. */ Object.defineProperty(this, "inputChannels", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * The output channels for the graph. These channels contain the final output when the graph completes. * Can be a single channel key or an array of channel keys. */ Object.defineProperty(this, "outputChannels", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Whether to automatically validate the graph structure when it is compiled. Defaults to true. */ Object.defineProperty(this, "autoValidate", { enumerable: true, configurable: true, writable: true, value: true }); /** * The streaming modes enabled for this graph. Defaults to ["values"]. * Supported modes: * - "values": Streams the full state after each step * - "updates": Streams state updates after each step * - "messages": Streams messages from within nodes * - "custom": Streams custom events from within nodes * - "debug": Streams events related to the execution of the graph - useful for tracing & debugging graph execution */ Object.defineProperty(this, "streamMode", { enumerable: true, configurable: true, writable: true, value: ["values"] }); /** * Optional channels to stream. If not specified, all channels will be streamed. * Can be a single channel key or an array of channel keys. */ Object.defineProperty(this, "streamChannels", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * Optional array of node names or "all" to interrupt after executing these nodes. * Used for implementing human-in-the-loop workflows. */ Object.defineProperty(this, "interruptAfter", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * Optional array of node names or "all" to interrupt before executing these nodes. * Used for implementing human-in-the-loop workflows. */ Object.defineProperty(this, "interruptBefore", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Optional timeout in milliseconds for the execution of each superstep */ Object.defineProperty(this, "stepTimeout", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Whether to enable debug logging. Defaults to false. */ Object.defineProperty(this, "debug", { enumerable: true, configurable: true, writable: true, value: false }); /** * Optional checkpointer for persisting graph state. * When provided, saves a checkpoint of the graph state at every superstep. * When false or undefined, checkpointing is disabled, and the graph will not be able to save or restore state. */ Object.defineProperty(this, "checkpointer", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** Optional retry policy for handling failures in node execution */ Object.defineProperty(this, "retryPolicy", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** The default configuration for graph execution, can be overridden on a per-invocation basis */ Object.defineProperty(this, "config", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * Optional long-term memory store for the graph, allows for persistence & retrieval of data across threads */ Object.defineProperty(this, "store", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "triggerToNodes", { enumerable: true, configurable: true, writable: true, value: {} }); /** * Optional cache for the graph, useful for caching tasks. */ Object.defineProperty(this, "cache", { enumerable: true, configurable: true, writable: true, value: void 0 }); let { streamMode } = fields; if (streamMode != null && !Array.isArray(streamMode)) { streamMode = [streamMode]; } this.nodes = fields.nodes; this.channels = fields.channels; this.autoValidate = fields.autoValidate ?? this.autoValidate; this.streamMode = streamMode ?? this.streamMode; this.inputChannels = fields.inputChannels; this.outputChannels = fields.outputChannels; this.streamChannels = fields.streamChannels ?? this.streamChannels; this.interruptAfter = fields.interruptAfter; this.interruptBefore = fields.interruptBefore; this.stepTimeout = fields.stepTimeout ?? this.stepTimeout; this.debug = fields.debug ?? this.debug; this.checkpointer = fields.checkpointer; this.retryPolicy = fields.retryPolicy; this.config = fields.config; this.store = fields.store; this.cache = fields.cache; this.name = fields.name; if (this.autoValidate) { this.validate(); } } /** * Creates a new instance of the Pregel graph with updated configuration. * This method follows the immutable pattern - instead of modifying the current instance, * it returns a new instance with the merged configuration. * * @example * ```typescript * // Create a new instance with debug enabled * const debugGraph = graph.withConfig({ debug: true }); * * // Create a new instance with a specific thread ID * const threadGraph = graph.withConfig({ * configurable: { thread_id: "123" } * }); * ``` * * @param config - The configuration to merge with the current configuration * @returns A new Pregel instance with the merged configuration */ withConfig(config) { const mergedConfig = (0, runnables_1.mergeConfigs)(this.config, config); // eslint-disable-next-line @typescript-eslint/no-explicit-any return new this.constructor({ ...this, config: mergedConfig }); } /** * Validates the graph structure to ensure it is well-formed. * Checks for: * - No orphaned nodes * - Valid input/output channel configurations * - Valid interrupt configurations * * @returns this - The Pregel instance for method chaining * @throws {GraphValidationError} If the graph structure is invalid */ validate() { (0, validate_js_1.validateGraph)({ nodes: this.nodes, channels: this.channels, outputChannels: this.outputChannels, inputChannels: this.inputChannels, streamChannels: this.streamChannels, interruptAfterNodes: this.interruptAfter, interruptBeforeNodes: this.interruptBefore, }); for (const [name, node] of Object.entries(this.nodes)) { for (const trigger of node.triggers) { this.triggerToNodes[trigger] ??= []; this.triggerToNodes[trigger].push(name); } } return this; } /** * Gets a list of all channels that should be streamed. * If streamChannels is specified, returns those channels. * Otherwise, returns all channels in the graph. * * @returns Array of channel keys to stream */ get streamChannelsList() { if (Array.isArray(this.streamChannels)) { return this.streamChannels; } else if (this.streamChannels) { return [this.streamChannels]; } else { return Object.keys(this.channels); } } /** * Gets the channels to stream in their original format. * If streamChannels is specified, returns it as-is (either single key or array). * Otherwise, returns all channels in the graph as an array. * * @returns Channel keys to stream, either as a single key or array */ get streamChannelsAsIs() { if (this.streamChannels) { return this.streamChannels; } else { return Object.keys(this.channels); } } /** * Gets a drawable representation of the graph structure. * This is an async version of getGraph() and is the preferred method to use. * * @param config - Configuration for generating the graph visualization * @returns A representation of the graph that can be visualized */ async getGraphAsync(config) { return this.getGraph(config); } /** * Gets all subgraphs within this graph. * A subgraph is a Pregel instance that is nested within a node of this graph. * * @deprecated Use getSubgraphsAsync instead. The async method will become the default in the next minor release. * @param namespace - Optional namespace to filter subgraphs * @param recurse - Whether to recursively get subgraphs of subgraphs * @returns Generator yielding tuples of [name, subgraph] */ *getSubgraphs(namespace, recurse // eslint-disable-next-line @typescript-eslint/no-explicit-any ) { for (const [name, node] of Object.entries(this.nodes)) { // filter by prefix if (namespace !== undefined) { if (!namespace.startsWith(name)) { continue; } } const candidates = node.subgraphs?.length ? node.subgraphs : [node.bound]; for (const candidate of candidates) { const graph = (0, subgraph_js_1.findSubgraphPregel)(candidate); if (graph !== undefined) { if (name === namespace) { yield [name, graph]; return; } if (namespace === undefined) { yield [name, graph]; } if (recurse) { let newNamespace = namespace; if (namespace !== undefined) { newNamespace = namespace.slice(name.length + 1); } for (const [subgraphName, subgraph] of graph.getSubgraphs(newNamespace, recurse)) { yield [ `${name}${constants_js_1.CHECKPOINT_NAMESPACE_SEPARATOR}${subgraphName}`, subgraph, ]; } } } } } } /** * Gets all subgraphs within this graph asynchronously. * A subgraph is a Pregel instance that is nested within a node of this graph. * * @param namespace - Optional namespace to filter subgraphs * @param recurse - Whether to recursively get subgraphs of subgraphs * @returns AsyncGenerator yielding tuples of [name, subgraph] */ async *getSubgraphsAsync(namespace, recurse // eslint-disable-next-line @typescript-eslint/no-explicit-any ) { yield* this.getSubgraphs(namespace, recurse); } /** * Prepares a state snapshot from saved checkpoint data. * This is an internal method used by getState and getStateHistory. * * @param config - Configuration for preparing the snapshot * @param saved - Optional saved checkpoint data * @param subgraphCheckpointer - Optional checkpointer for subgraphs * @param applyPendingWrites - Whether to apply pending writes to tasks and then to channels * @returns A snapshot of the graph state * @internal */ async _prepareStateSnapshot({ config, saved, subgraphCheckpointer, applyPendingWrites = false, }) { if (saved === undefined) { return { values: {}, next: [], config, tasks: [], }; } // Create all channels const { managed } = await this.prepareSpecs(config, { skipManaged: true, }); const channels = (0, base_js_1.emptyChannels)(this.channels, saved.checkpoint); // Apply null writes first (from NULL_TASK_ID) if (saved.pendingWrites?.length) { const nullWrites = saved.pendingWrites .filter(([taskId, _]) => taskId === constants_js_1.NULL_TASK_ID) .map(([_, channel, value]) => [String(channel), value]); if (nullWrites.length > 0) { (0, algo_js_1._applyWrites)(saved.checkpoint, channels, [ { name: constants_js_1.INPUT, writes: nullWrites, triggers: [], }, ], undefined, this.triggerToNodes); } } // Prepare next tasks const nextTasks = Object.values((0, algo_js_1._prepareNextTasks)(saved.checkpoint, saved.pendingWrites, this.nodes, channels, managed, saved.config, true, { step: (saved.metadata?.step ?? -1) + 1, store: this.store })); // Find subgraphs const subgraphs = await (0, utils_js_1.gatherIterator)(this.getSubgraphsAsync()); const parentNamespace = saved.config.configurable?.checkpoint_ns ?? ""; const taskStates = {}; // Prepare task states for subgraphs for (const task of nextTasks) { const matchingSubgraph = subgraphs.find(([name]) => name === task.name); if (!matchingSubgraph) { continue; } // assemble checkpoint_ns for this task let taskNs = `${String(task.name)}${constants_js_1.CHECKPOINT_NAMESPACE_END}${task.id}`; if (parentNamespace) { taskNs = `${parentNamespace}${constants_js_1.CHECKPOINT_NAMESPACE_SEPARATOR}${taskNs}`; } if (subgraphCheckpointer === undefined) { // set config as signal that subgraph checkpoints exist const config = { configurable: { thread_id: saved.config.configurable?.thread_id, checkpoint_ns: taskNs, }, }; taskStates[task.id] = config; } else { // get the state of the subgraph const subgraphConfig = { configurable: { [constants_js_1.CONFIG_KEY_CHECKPOINTER]: subgraphCheckpointer, thread_id: saved.config.configurable?.thread_id, checkpoint_ns: taskNs, }, }; const pregel = matchingSubgraph[1]; taskStates[task.id] = await pregel.getState(subgraphConfig, { subgraphs: true, }); } } // Apply pending writes to tasks and then to channels if applyPendingWrites is true if (applyPendingWrites && saved.pendingWrites?.length) { // Map task IDs to task objects for easy lookup const nextTaskById = Object.fromEntries(nextTasks.map((task) => [task.id, task])); // Apply pending writes to the appropriate tasks for (const [taskId, channel, value] of saved.pendingWrites) { // Skip special channels and tasks not in nextTasks if ([constants_js_1.ERROR, constants_js_1.INTERRUPT, langgraph_checkpoint_1.SCHEDULED].includes(channel)) { continue; } if (!(taskId in nextTaskById)) { continue; } // Add the write to the task nextTaskById[taskId].writes.push([String(channel), value]); } // Apply writes from tasks that have writes const tasksWithWrites = nextTasks.filter((task) => task.writes.length > 0); if (tasksWithWrites.length > 0) { (0, algo_js_1._applyWrites)(saved.checkpoint, channels, tasksWithWrites, undefined, this.triggerToNodes); } } // Preserve thread_id from the config in metadata let metadata = saved?.metadata; if (metadata && saved?.config?.configurable?.thread_id) { metadata = { ...metadata, thread_id: saved.config.configurable.thread_id, }; } // Filter next tasks - only include tasks without writes const nextList = nextTasks .filter((task) => task.writes.length === 0) .map((task) => task.name); // assemble the state snapshot return { values: (0, io_js_1.readChannels)(channels, this.streamChannelsAsIs), next: nextList, tasks: (0, debug_js_1.tasksWithWrites)(nextTasks, saved?.pendingWrites ?? [], taskStates), metadata, config: (0, index_js_1.patchCheckpointMap)(saved.config, saved.metadata), createdAt: saved.checkpoint.ts, parentConfig: saved.parentConfig, }; } /** * Gets the current state of the graph. * Requires a checkpointer to be configured. * * @param config - Configuration for retrieving the state * @param options - Additional options * @returns A snapshot of the current graph state * @throws {GraphValueError} If no checkpointer is configured */ async getState(config, options) { const checkpointer = config.configurable?.[constants_js_1.CONFIG_KEY_CHECKPOINTER] ?? this.checkpointer; if (!checkpointer) { throw new errors_js_1.GraphValueError("No checkpointer set"); } const checkpointNamespace = config.configurable?.checkpoint_ns ?? ""; if (checkpointNamespace !== "" && config.configurable?.[constants_js_1.CONFIG_KEY_CHECKPOINTER] === undefined) { // remove task_ids from checkpoint_ns const recastNamespace = (0, config_js_1.recastCheckpointNamespace)(checkpointNamespace); for await (const [name, subgraph] of this.getSubgraphsAsync(recastNamespace, true)) { if (name === recastNamespace) { return await subgraph.getState((0, utils_js_1.patchConfigurable)(config, { [constants_js_1.CONFIG_KEY_CHECKPOINTER]: checkpointer, }), { subgraphs: options?.subgraphs }); } } throw new Error(`Subgraph with namespace "${recastNamespace}" not found.`); } const mergedConfig = (0, runnables_1.mergeConfigs)(this.config, config); const saved = await checkpointer.getTuple(config); const snapshot = await this._prepareStateSnapshot({ config: mergedConfig, saved, subgraphCheckpointer: options?.subgraphs ? checkpointer : undefined, applyPendingWrites: !config.configurable?.checkpoint_id, }); return snapshot; } /** * Gets the history of graph states. * Requires a checkpointer to be configured. * Useful for: * - Debugging execution history * - Implementing time travel * - Analyzing graph behavior * * @param config - Configuration for retrieving the history * @param options - Options for filtering the history * @returns An async iterator of state snapshots * @throws {Error} If no checkpointer is configured */ async *getStateHistory(config, options) { const checkpointer = config.configurable?.[constants_js_1.CONFIG_KEY_CHECKPOINTER] ?? this.checkpointer; if (!checkpointer) { throw new Error("No checkpointer set"); } const checkpointNamespace = config.configurable?.checkpoint_ns ?? ""; if (checkpointNamespace !== "" && config.configurable?.[constants_js_1.CONFIG_KEY_CHECKPOINTER] === undefined) { const recastNamespace = (0, config_js_1.recastCheckpointNamespace)(checkpointNamespace); // find the subgraph with the matching name for await (const [name, pregel] of this.getSubgraphsAsync(recastNamespace, true)) { if (name === recastNamespace) { yield* pregel.getStateHistory((0, utils_js_1.patchConfigurable)(config, { [constants_js_1.CONFIG_KEY_CHECKPOINTER]: checkpointer, }), options); return; } } throw new Error(`Subgraph with namespace "${recastNamespace}" not found.`); } const mergedConfig = (0, runnables_1.mergeConfigs)(this.config, config, { configurable: { checkpoint_ns: checkpointNamespace }, }); for await (const checkpointTuple of checkpointer.list(mergedConfig, options)) { yield this._prepareStateSnapshot({ config: checkpointTuple.config, saved: checkpointTuple, }); } } /** * Apply updates to the graph state in bulk. * Requires a checkpointer to be configured. * * This method is useful for recreating a thread * from a list of updates, especially if a checkpoint * is created as a result of multiple tasks. * * @internal The API might change in the future. * * @param startConfig - Configuration for the update * @param updates - The list of updates to apply to graph state * @returns Updated configuration * @throws {GraphValueError} If no checkpointer is configured * @throws {InvalidUpdateError} If the update cannot be attributed to a node or an update can be only applied in sequence. */ async bulkUpdateState(startConfig, supersteps) { const checkpointer = startConfig.configurable?.[constants_js_1.CONFIG_KEY_CHECKPOINTER] ?? this.checkpointer; if (!checkpointer) { throw new errors_js_1.GraphValueError("No checkpointer set"); } if (supersteps.length === 0) { throw new Error("No supersteps provided"); } if (supersteps.some((s) => s.updates.length === 0)) { throw new Error("No updates provided"); } // delegate to subgraph const checkpointNamespace = startConfig.configurable?.checkpoint_ns ?? ""; if (checkpointNamespace !== "" && startConfig.configurable?.[constants_js_1.CONFIG_KEY_CHECKPOINTER] === undefined) { // remove task_ids from checkpoint_ns const recastNamespace = (0, config_js_1.recastCheckpointNamespace)(checkpointNamespace); // find the subgraph with the matching name // eslint-disable-next-line no-unreachable-loop for await (const [, pregel] of this.getSubgraphsAsync(recastNamespace, true)) { return await pregel.bulkUpdateState((0, utils_js_1.patchConfigurable)(startConfig, { [constants_js_1.CONFIG_KEY_CHECKPOINTER]: checkpointer, }), supersteps); } throw new Error(`Subgraph "${recastNamespace}" not found`); } const updateSuperStep = async (inputConfig, updates) => { // get last checkpoint const config = this.config ? (0, runnables_1.mergeConfigs)(this.config, inputConfig) : inputConfig; const saved = await checkpointer.getTuple(config); const checkpoint = saved !== undefined ? (0, langgraph_checkpoint_1.copyCheckpoint)(saved.checkpoint) : (0, langgraph_checkpoint_1.emptyCheckpoint)(); const checkpointPreviousVersions = { ...saved?.checkpoint.channel_versions, }; const step = saved?.metadata?.step ?? -1; // merge configurable fields with previous checkpoint config let checkpointConfig = (0, utils_js_1.patchConfigurable)(config, { checkpoint_ns: config.configurable?.checkpoint_ns ?? "", }); let checkpointMetadata = config.metadata ?? {}; if (saved?.config.configurable) { checkpointConfig = (0, utils_js_1.patchConfigurable)(config, saved.config.configurable); checkpointMetadata = { ...saved.metadata, ...checkpointMetadata, }; } // Find last node that updated the state, if not provided const { values, asNode } = updates[0]; if (values == null && asNode === undefined) { if (updates.length > 1) { throw new errors_js_1.InvalidUpdateError(`Cannot create empty checkpoint with multiple updates`); } const nextConfig = await checkpointer.put(checkpointConfig, (0, base_js_1.createCheckpoint)(checkpoint, undefined, step), { source: "update", step: step + 1, writes: {}, parents: saved?.metadata?.parents ?? {}, }, {}); return (0, index_js_1.patchCheckpointMap)(nextConfig, saved ? saved.metadata : undefined); } // update channels const channels = (0, base_js_1.emptyChannels)(this.channels, checkpoint); // Pass `skipManaged: true` as managed values are not used/relevant in update state calls. const { managed } = await this.prepareSpecs(config, { skipManaged: true, }); if (values === null && asNode === constants_js_1.END) { if (updates.length > 1) { throw new errors_js_1.InvalidUpdateError(`Cannot apply multiple updates when clearing state`); } if (saved) { // tasks for this checkpoint const nextTasks = (0, algo_js_1._prepareNextTasks)(checkpoint, saved.pendingWrites || [], this.nodes, channels, managed, saved.config, true, { step: (saved.metadata?.step ?? -1) + 1, checkpointer: this.checkpointer || undefined, store: this.store, }); // apply null writes const nullWrites = (saved.pendingWrites || []) .filter((w) => w[0] === constants_js_1.NULL_TASK_ID) .map((w) => w.slice(1)); if (nullWrites.length > 0) { (0, algo_js_1._applyWrites)(saved.checkpoint, channels, [ { name: constants_js_1.INPUT, writes: nullWrites, triggers: [], }, ], undefined, this.triggerToNodes); } // apply writes from tasks that already ran for (const [taskId, k, v] of saved.pendingWrites || []) { if ([constants_js_1.ERROR, constants_js_1.INTERRUPT, langgraph_checkpoint_1.SCHEDULED].includes(k)) { continue; } if (!(taskId in nextTasks)) { continue; } nextTasks[taskId].writes.push([k, v]); } // clear all current tasks (0, algo_js_1._applyWrites)(checkpoint, channels, Object.values(nextTasks), undefined, this.triggerToNodes); } // save checkpoint const nextConfig = await checkpointer.put(checkpointConfig, (0, base_js_1.createCheckpoint)(checkpoint, undefined, step), { ...checkpointMetadata, source: "update", step: step + 1, writes: {}, parents: saved?.metadata?.parents ?? {}, }, {}); return (0, index_js_1.patchCheckpointMap)(nextConfig, saved ? saved.metadata : undefined); } if (values == null && asNode === constants_js_1.COPY) { if (updates.length > 1) { throw new errors_js_1.InvalidUpdateError(`Cannot copy checkpoint with multiple updates`); } const nextConfig = await checkpointer.put(saved?.parentConfig ?? checkpointConfig, (0, base_js_1.createCheckpoint)(checkpoint, undefined, step), { source: "fork", step: step + 1, writes: {}, parents: saved?.metadata?.parents ?? {}, }, {}); return (0, index_js_1.patchCheckpointMap)(nextConfig, saved ? saved.metadata : undefined); } if (asNode === constants_js_1.INPUT) { if (updates.length > 1) { throw new errors_js_1.InvalidUpdateError(`Cannot apply multiple updates when updating as input`); } const inputWrites = await (0, utils_js_1.gatherIterator)((0, io_js_1.mapInput)(this.inputChannels, values)); if (inputWrites.length === 0) { throw new errors_js_1.InvalidUpdateError(`Received no input writes for ${JSON.stringify(this.inputChannels, null, 2)}`); } // apply to checkpoint (0, algo_js_1._applyWrites)(checkpoint, channels, [ { name: constants_js_1.INPUT, writes: inputWrites, triggers: [], }, ], checkpointer.getNextVersion.bind(this.checkpointer), this.triggerToNodes); // apply input write to channels const nextStep = saved?.metadata?.step != null ? saved.metadata.step + 1 : -1; const nextConfig = await checkpointer.put(checkpointConfig, (0, base_js_1.createCheckpoint)(checkpoint, channels, nextStep), { source: "input", step: nextStep, writes: Object.fromEntries(inputWrites), parents: saved?.metadata?.parents ?? {}, }, (0, index_js_1.getNewChannelVersions)(checkpointPreviousVersions, checkpoint.channel_versions)); // Store the writes await checkpointer.putWrites(nextConfig, inputWrites, (0, langgraph_checkpoint_1.uuid5)(constants_js_1.INPUT, checkpoint.id)); return (0, index_js_1.patchCheckpointMap)(nextConfig, saved ? saved.metadata : undefined); } // apply pending writes, if not on specific checkpoint if (config.configurable?.checkpoint_id === undefined && saved?.pendingWrites !== undefined && saved.pendingWrites.length > 0) { // tasks for this checkpoint const nextTasks = (0, algo_js_1._prepareNextTasks)(checkpoint, saved.pendingWrites, this.nodes, channels, managed, saved.config, true, { store: this.store, // eslint-disable-next-line @typescript-eslint/no-explicit-any checkpointer: this.checkpointer, step: (saved.metadata?.step ?? -1) + 1, }); // apply null writes const nullWrites = (saved.pendingWrites ?? []) .filter((w) => w[0] === constants_js_1.NULL_TASK_ID) .map((w) => w.slice(1)); if (nullWrites.length > 0) { (0, algo_js_1._applyWrites)(saved.checkpoint, channels, [{ name: constants_js_1.INPUT, writes: nullWrites, triggers: [] }], undefined, this.triggerToNodes); } // apply writes for (const [tid, k, v] of saved.pendingWrites) { if ([constants_js_1.ERROR, constants_js_1.INTERRUPT, langgraph_checkpoint_1.SCHEDULED].includes(k) || nextTasks[tid] === undefined) { continue; } nextTasks[tid].writes.push([k, v]); } const tasks = Object.values(nextTasks).filter((task) => { return task.writes.length > 0; }); if (tasks.length > 0) { (0, algo_js_1._applyWrites)(checkpoint, channels, tasks, undefined, this.triggerToNodes); } } const nonNullVersion = Object.values(checkpoint.versions_seen) .map((seenVersions) => { return Object.values(seenVersions); }) .flat() .find((v) => !!v); const validUpdates = []; if (updates.length === 1) { // eslint-disable-next-line prefer-const let { values, asNode } = updates[0]; if (asNode === undefined && Object.keys(this.nodes).length === 1) { // if only one node, use it [asNode] = Object.keys(this.nodes); } else if (asNode === undefined && nonNullVersion === undefined) { if (typeof this.inputChannels === "string" && this.nodes[this.inputChannels] !== undefined) { asNode = this.inputChannels; } } else if (asNode === undefined) { const lastSeenByNode = Object.entries(checkpoint.versions_seen) .map(([n, seen]) => { return Object.values(seen).map((v) => { return [v, n]; }); }) .flat() .sort(([aNumber], [bNumber]) => (0, langgraph_checkpoint_1.compareChannelVersions)(aNumber, bNumber)); // if two nodes updated the state at the same time, it's ambiguous if (lastSeenByNode) { if (lastSeenByNode.length === 1) { // eslint-disable-next-line prefer-destructuring asNode = lastSeenByNode[0][1]; } else if (lastSeenByNode[lastSeenByNode.length - 1][0] !== lastSeenByNode[lastSeenByNode.length - 2][0]) { // eslint-disable-next-line prefer-destructuring asNode = lastSeenByNode[lastSeenByNode.length - 1][1]; } } } if (asNode === undefined) { throw new errors_js_1.InvalidUpdateError(`Ambiguous update, specify "asNode"`); } validUpdates.push({ values, asNode }); } else { for (const { asNode, values } of updates) { if (asNode == null) { throw new errors_js_1.InvalidUpdateError(`"asNode" is required when applying multiple updates`); } validUpdates.push({ values, asNode }); } } const tasks = []; for (const { asNode, values } of validUpdates) { if (this.nodes[asNode] === undefined) { throw new errors_js_1.InvalidUpdateError(`Node "${asNode.toString()}" does not exist`); } // run all writers of the chosen node const writers = this.nodes[asNode].getWriters(); if (!writers.length) { throw new errors_js_1.InvalidUpdateError(`No writers found for node "${asNode.toString()}"`); } tasks.push({ name: asNode, input: values, proc: writers.length > 1 ? // eslint-disable-next-line @typescript-eslint/no-explicit-any runnables_1.RunnableSequence.from(writers, { omitSequenceTags: true, }) : writers[0], writes: [], triggers: [constants_js_1.INTERRUPT], id: (0, langgraph_checkpoint_1.uuid5)(constants_js_1.INTERRUPT, checkpoint.id), writers: [], }); } for (const task of tasks) { // execute task await task.proc.invoke(task.input, (0, runnables_1.patchConfig)({ ...config, store: config?.store ?? this.store, }, { runName: config.runName ?? `${this.getName()}UpdateState`, configurable: { [constants_js_1.CONFIG_KEY_SEND]: (items) => task.writes.push(...items), [constants_js_1.CONFIG_KEY_READ]: (select_, fresh_ = false) => (0, algo_js_1._localRead)(step, checkpoint, channels, managed, // TODO: Why does keyof StrRecord allow number and symbol? task, select_, fresh_), }, })); } for (const task of tasks) { // channel writes are saved to current checkpoint const channelWrites = task.writes.filter((w) => w[0] !== constants_js_1.PUSH); // save task writes if (saved !== undefined && channelWrites.length > 0) { await checkpointer.putWrites(checkpointConfig, channelWrites, task.id); } } // apply to checkpoint // TODO: Why does keyof StrRecord allow number and symbol? (0, algo_js_1._applyWrites)(checkpoint, channels, tasks, checkpointer.getNextVersion.bind(this.checkpointer), this.triggerToNodes); const newVersions = (0, index_js_1.getNewChannelVersions)(checkpointPreviousVersions, checkpoint.channel_versions); const nextConfig = await checkpointer.put(checkpointConfig, (0, base_js_1.createCheckpoint)(checkpoint, channels, step + 1), { source: "update", step: step + 1, writes: Object.fromEntries(validUpdates.map((update) => [update.asNode, update.values])), parents: saved?.metadata?.parents ?? {}, }, newVersions); for (const task of tasks) { // push writes are saved to next checkpoint const pushWrites = task.writes.filter((w) => w[0] === constants_js_1.PUSH); if (pushWrites.length > 0) { await checkpointer.putWrites(nextConfig, pushWrites, task.id); } } return (0, index_js_1.patchCheckpointMap)(nextConfig, saved ? saved.metadata : undefined); }; let currentConfig = startConfig; for (const { updates } of supersteps) { currentConfig = await updateSuperStep(currentConfig, updates); } return currentConfig; } /** * Updates the state of the graph with new values. * Requires a checkpointer to be con