@langchain/langgraph
Version:
194 lines (193 loc) • 7.45 kB
JavaScript
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