@langchain/langgraph
Version:
LangGraph
1,107 lines • 69.6 kB
JavaScript
"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