UNPKG

@langchain/langgraph

Version:
194 lines (193 loc) 7.45 kB
import { getDeltaMaxSuperstepsSinceSnapshot } from "../constants.js"; import { EmptyChannelError } from "../errors.js"; import { DeltaSnapshot, uuid6 } from "@langchain/langgraph-checkpoint"; //#region src/channels/base.ts /** Matches Postgres `uuid` / Python `uuid.UUID` (128-bit, 8-4-4-4-12 hex). */ const STRUCTURED_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; /** * Structural check for a {@link DeltaChannel} without importing it (avoids an * import cycle: `delta.ts` imports `base.ts`). */ function isDeltaChannel(channel) { return channel != null && channel.lc_graph_name === "DeltaChannel"; } function isBaseChannel(obj) { return obj != null && obj.lg_is_channel === true; } /** @internal */ var BaseChannel = class { ValueType; UpdateType; /** @ignore */ lg_is_channel = true; /** * Mark the current value of the channel as consumed. By default, no-op. * A channel can use this method to modify its state, preventing the value * from being consumed again. * * Returns True if the channel was updated, False otherwise. */ consume() { return false; } /** * Notify the channel that the Pregel run is finishing. By default, no-op. * A channel can use this method to modify its state, preventing finish. * * Returns True if the channel was updated, False otherwise. */ finish() { return false; } /** * Return True if the channel is available (not empty), False otherwise. * Subclasses should override this method to provide a more efficient * implementation than calling get() and catching EmptyChannelError. */ isAvailable() { try { this.get(); return true; } catch (error) { if (error.name === EmptyChannelError.unminifiable_name) return false; throw error; } } /** * Compare this channel with another channel for equality. * Used to determine if two channels with the same key are semantically equivalent. * Subclasses should override this method to provide a meaningful comparison. * * @param {BaseChannel} other - The other channel to compare with. * @returns {boolean} True if the channels are equal, false otherwise. */ equals(other) { return this === other; } }; const IS_ONLY_BASE_CHANNEL = Symbol.for("LG_IS_ONLY_BASE_CHANNEL"); function getOnlyChannels(channels) { if (channels[IS_ONLY_BASE_CHANNEL] === true) return channels; const newChannels = {}; for (const k in channels) { if (!Object.prototype.hasOwnProperty.call(channels, k)) continue; const value = channels[k]; if (isBaseChannel(value)) newChannels[k] = value; } Object.assign(newChannels, { [IS_ONLY_BASE_CHANNEL]: true }); return newChannels; } function emptyChannels(channels, checkpoint) { const filteredChannels = getOnlyChannels(channels); const newChannels = {}; for (const k in filteredChannels) { if (!Object.prototype.hasOwnProperty.call(filteredChannels, k)) continue; const channelValue = checkpoint.channel_values[k]; newChannels[k] = filteredChannels[k].fromCheckpoint(channelValue); } Object.assign(newChannels, { [IS_ONLY_BASE_CHANNEL]: true }); return newChannels; } /** * Synthetic task id for exit-mode DeltaChannel writes. * * Embeds the superstep in the first UUID group so `ORDER BY task_id, idx` * preserves chronological order while remaining a valid RFC UUID (required by * Postgres `checkpoint_writes.task_id uuid` columns). */ function exitDeltaTaskId(step, taskId) { if (!STRUCTURED_UUID.test(taskId)) throw new TypeError(`Invalid task id for exit delta: ${taskId}`); const parts = taskId.toLowerCase().split("-"); return `${String(step).padStart(8, "0")}-${parts[1]}-${parts[2]}-${parts[3]}-${parts[4]}`; } /** * Return the set of {@link DeltaChannel} names that should snapshot now. * * A channel snapshots when EITHER its accumulated update count reaches * `snapshotFrequency` OR the total supersteps since its last snapshot reaches * `DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT`. Pure predicate — no mutation. */ function deltaChannelsToSnapshot(channels, countersSinceDeltaSnapshot) { const result = /* @__PURE__ */ new Set(); const maxSupersteps = getDeltaMaxSuperstepsSinceSnapshot(); for (const name in channels) { if (!Object.prototype.hasOwnProperty.call(channels, name)) continue; const ch = channels[name]; if (!isDeltaChannel(ch) || !ch.isAvailable()) continue; const [updates, supersteps] = countersSinceDeltaSnapshot[name] ?? [0, 0]; if (updates >= ch.snapshotFrequency || supersteps >= maxSupersteps) result.add(name); } return result; } function createCheckpoint(checkpoint, channels, step, options) { const channelsToSnapshot = options?.channelsToSnapshot ?? /* @__PURE__ */ new Set(); const { updatedChannels, getNextVersion } = options ?? {}; let values; let channelVersions = checkpoint.channel_versions; if (channels === void 0) values = checkpoint.channel_values; else { values = {}; channelVersions = { ...checkpoint.channel_versions }; for (const k in channels) { if (!Object.prototype.hasOwnProperty.call(channels, k)) continue; const channel = channels[k]; if (channelsToSnapshot.has(k)) { if (getNextVersion !== void 0 && (updatedChannels === void 0 || !updatedChannels.has(k))) channelVersions[k] = getNextVersion(channelVersions[k]); values[k] = new DeltaSnapshot(channel.get()); continue; } if (isDeltaChannel(channel)) continue; try { values[k] = channel.checkpoint(); } catch (error) { if (error.name === EmptyChannelError.unminifiable_name) {} else throw error; } } } return { v: 4, id: options?.id ?? uuid6(step), ts: (/* @__PURE__ */ new Date()).toISOString(), channel_values: values, channel_versions: channelVersions, versions_seen: checkpoint.versions_seen }; } /** * Hydrate channels from a checkpoint, reconstructing any {@link DeltaChannel} * whose value is absent from `channel_values` by replaying ancestor writes. * * For most channels (and for delta channels with a {@link DeltaSnapshot} or a * migrated plain value in `channel_values`), {@link emptyChannels} is * sufficient and no saver access is required. When a delta channel is absent * from `channel_values`, an ancestor walk via `saver.getDeltaChannelHistory` * finds the nearest seed and accumulates the writes between it and the * target. All delta channels needing replay are batched into a single saver * call. */ async function channelsFromCheckpoint(specs, checkpoint, options) { const channels = emptyChannels(specs, checkpoint); const { saver, config } = options ?? {}; const filteredSpecs = getOnlyChannels(specs); const deltaKeys = []; for (const k in filteredSpecs) { if (!Object.prototype.hasOwnProperty.call(filteredSpecs, k)) continue; if (isDeltaChannel(filteredSpecs[k]) && !Object.prototype.hasOwnProperty.call(checkpoint.channel_values, k)) deltaKeys.push(k); } if (deltaKeys.length === 0 || saver === void 0 || config === void 0) return channels; const histories = await saver.getDeltaChannelHistory({ config, channels: deltaKeys }); for (const k of deltaKeys) { const history = histories[k]; if (history === void 0) continue; const replayCh = filteredSpecs[k].fromCheckpoint(history.seed); replayCh.replayWrites(history.writes); channels[k] = replayCh; } return channels; } //#endregion export { BaseChannel, channelsFromCheckpoint, createCheckpoint, deltaChannelsToSnapshot, emptyChannels, exitDeltaTaskId, getOnlyChannels, isBaseChannel, isDeltaChannel }; //# sourceMappingURL=base.js.map