UNPKG

@langchain/langgraph

Version:
1 lines 66.8 kB
{"version":3,"file":"algo.cjs","names":["getNullChannelVersion","TAG_HIDDEN","isDeltaChannel","createCheckpoint","emptyChannels","readChannels","_isSend","InvalidUpdateError","NO_WRITES","PUSH","RESUME","INTERRUPT","RETURN","ERROR","ERROR_SOURCE_NODE","RESERVED","getOnlyChannels","TASKS","PULL","isCall","getRunnableForFunc","CONFIG_KEY_TASK_ID","CONFIG_KEY_SEND","CONFIG_KEY_READ","CONFIG_KEY_CHECKPOINTER","CONFIG_KEY_CHECKPOINT_MAP","CONFIG_KEY_SCRATCHPAD","CONFIG_KEY_RESUME_MAP","XXH3","CONFIG_KEY_PREVIOUS_STATE","PREVIOUS","CACHE_NS_WRITES","_isSendInterface","Send","CONFIG_KEY_NODE_ERROR","NodeError","readChannel","EmptyChannelError"],"sources":["../../src/pregel/algo.ts"],"sourcesContent":["/* eslint-disable no-param-reassign */\nimport {\n mergeConfigs,\n patchConfig,\n RunnableConfig,\n} from \"@langchain/core/runnables\";\nimport { CallbackManagerForChainRun } from \"@langchain/core/callbacks/manager\";\nimport {\n All,\n BaseCheckpointSaver,\n Checkpoint,\n ReadonlyCheckpoint,\n copyCheckpoint,\n type PendingWrite,\n type PendingWriteValue,\n uuid5,\n maxChannelVersion,\n BaseStore,\n CheckpointPendingWrite,\n SendProtocol,\n} from \"@langchain/langgraph-checkpoint\";\nimport {\n BaseChannel,\n createCheckpoint,\n emptyChannels,\n isDeltaChannel,\n getOnlyChannels,\n} from \"../channels/base.js\";\nimport { PregelNode } from \"./read.js\";\nimport { readChannel, readChannels } from \"./io.js\";\nimport {\n _isSend,\n _isSendInterface,\n CONFIG_KEY_CHECKPOINT_MAP,\n CHECKPOINT_NAMESPACE_SEPARATOR,\n CONFIG_KEY_CHECKPOINTER,\n CONFIG_KEY_READ,\n CONFIG_KEY_TASK_ID,\n CONFIG_KEY_SEND,\n INTERRUPT,\n RESERVED,\n Send,\n TAG_HIDDEN,\n TASKS,\n CHECKPOINT_NAMESPACE_END,\n PUSH,\n PULL,\n RESUME,\n NULL_TASK_ID,\n CONFIG_KEY_SCRATCHPAD,\n RETURN,\n ERROR,\n ERROR_SOURCE_NODE,\n NO_WRITES,\n CONFIG_KEY_PREVIOUS_STATE,\n PREVIOUS,\n CACHE_NS_WRITES,\n CONFIG_KEY_RESUME_MAP,\n CONFIG_KEY_NODE_ERROR,\n START,\n} from \"../constants.js\";\nimport {\n Call,\n isCall,\n PregelExecutableTask,\n PregelScratchpad,\n PregelTaskDescription,\n SimpleTaskPath,\n TaskPath,\n VariadicTaskPath,\n} from \"./types.js\";\nimport { EmptyChannelError, InvalidUpdateError, NodeError } from \"../errors.js\";\nimport { getNullChannelVersion } from \"./utils/index.js\";\nimport { ExecutionInfo, LangGraphRunnableConfig } from \"./runnable_types.js\";\nimport { getRunnableForFunc } from \"./call.js\";\nimport { IterableReadableWritableStream } from \"./stream.js\";\nimport { XXH3 } from \"../hash.js\";\nimport { Topic } from \"../channels/topic.js\";\n\n/**\n * Construct a type with a set of properties K of type T\n */\nexport type StrRecord<K extends string, T> = {\n [P in K]: T;\n};\n\nexport type WritesProtocol<C = string> = {\n name: string;\n writes: PendingWrite<C>[];\n triggers: string[];\n path?: TaskPath;\n};\n\nexport const increment = (current?: number) => {\n return current !== undefined ? current + 1 : 1;\n};\n\nfunction triggersNextStep(\n updatedChannels: Set<string>,\n triggerToNodes: Record<string, string[]> | undefined\n) {\n if (triggerToNodes == null) return false;\n\n for (const chan of updatedChannels) {\n if (triggerToNodes[chan]) return true;\n }\n\n return false;\n}\n\n// Avoids unnecessary double iteration\nfunction maxChannelMapVersion(\n channelVersions: Record<string, number | string>\n): number | string | undefined {\n let maxVersion: number | string | undefined;\n for (const chan in channelVersions) {\n if (!Object.prototype.hasOwnProperty.call(channelVersions, chan)) continue;\n if (maxVersion == null) {\n maxVersion = channelVersions[chan];\n } else {\n maxVersion = maxChannelVersion(maxVersion, channelVersions[chan]);\n }\n }\n return maxVersion;\n}\n\nexport function shouldInterrupt<N extends PropertyKey, C extends PropertyKey>(\n checkpoint: Checkpoint,\n interruptNodes: All | N[],\n tasks: PregelExecutableTask<N, C>[]\n): boolean {\n const nullVersion = getNullChannelVersion(checkpoint.channel_versions);\n const seen = checkpoint.versions_seen[INTERRUPT] ?? {};\n\n let anyChannelUpdated = false;\n\n if (\n (checkpoint.channel_versions[START] ?? nullVersion) >\n (seen[START] ?? nullVersion)\n ) {\n anyChannelUpdated = true;\n } else {\n for (const chan in checkpoint.channel_versions) {\n if (\n !Object.prototype.hasOwnProperty.call(checkpoint.channel_versions, chan)\n )\n continue;\n\n if (checkpoint.channel_versions[chan] > (seen[chan] ?? nullVersion)) {\n anyChannelUpdated = true;\n break;\n }\n }\n }\n\n const anyTriggeredNodeInInterruptNodes = tasks.some((task) =>\n interruptNodes === \"*\"\n ? !task.config?.tags?.includes(TAG_HIDDEN)\n : interruptNodes.includes(task.name)\n );\n\n return anyChannelUpdated && anyTriggeredNodeInInterruptNodes;\n}\n\nexport function _localRead<Cc extends Record<string, BaseChannel>>(\n checkpoint: ReadonlyCheckpoint,\n channels: Cc,\n task: WritesProtocol<keyof Cc>,\n select: Array<keyof Cc> | keyof Cc,\n fresh: boolean = false\n): Record<string, unknown> | unknown {\n let updated = new Set<keyof Cc>();\n\n if (!Array.isArray(select)) {\n for (const [c] of task.writes) {\n if (c === select) {\n updated = new Set([c]);\n break;\n }\n }\n updated = updated || new Set();\n } else {\n updated = new Set(\n select.filter((c) => task.writes.some(([key, _]) => key === c))\n );\n }\n\n let values: Record<string, unknown>;\n\n if (fresh && updated.size > 0) {\n const localChannels = Object.fromEntries(\n Object.entries(channels).filter(([k, _]) => updated.has(k as keyof Cc))\n ) as Partial<Cc>;\n\n // DeltaChannels are omitted from `channel_values` by default (their state\n // is reconstructed from ancestor writes via the saver). A local read has\n // no saver, so snapshot every delta channel inline to round-trip its full\n // live value through `emptyChannels`.\n const channelsToSnapshot = new Set<string>();\n for (const k in localChannels) {\n if (!Object.prototype.hasOwnProperty.call(localChannels, k)) continue;\n const ch = (localChannels as Record<string, BaseChannel>)[k];\n if (isDeltaChannel(ch) && ch.isAvailable()) channelsToSnapshot.add(k);\n }\n\n const newCheckpoint = createCheckpoint(\n checkpoint,\n localChannels as Cc,\n -1,\n {\n channelsToSnapshot,\n }\n );\n const newChannels = emptyChannels(localChannels as Cc, newCheckpoint);\n\n _applyWrites(\n copyCheckpoint(newCheckpoint),\n newChannels,\n [task],\n undefined,\n undefined\n );\n values = readChannels({ ...channels, ...newChannels }, select);\n } else {\n values = readChannels(channels, select);\n }\n\n return values;\n}\n\nexport function _localWrite(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n commit: (writes: [string, any][]) => any,\n processes: Record<string, PregelNode>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n writes: [string, any][]\n) {\n for (const [chan, value] of writes) {\n if ([PUSH, TASKS].includes(chan) && value != null) {\n if (!_isSend(value)) {\n throw new InvalidUpdateError(\n `Invalid packet type, expected SendProtocol, got ${JSON.stringify(\n value\n )}`\n );\n }\n if (!(value.node in processes)) {\n throw new InvalidUpdateError(\n `Invalid node name \"${value.node}\" in Send packet`\n );\n }\n }\n }\n commit(writes);\n}\n\nconst IGNORE = new Set<string | number | symbol>([\n NO_WRITES,\n PUSH,\n RESUME,\n INTERRUPT,\n RETURN,\n ERROR,\n ERROR_SOURCE_NODE,\n]);\n\nconst RESERVED_SET: ReadonlySet<string> = new Set(RESERVED);\n\nexport function _applyWrites<Cc extends Record<string, BaseChannel>>(\n checkpoint: Checkpoint,\n channels: Cc,\n tasks: WritesProtocol<keyof Cc>[],\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n getNextVersion: ((version: any) => any) | undefined,\n triggerToNodes: Record<string, string[]> | undefined\n): Set<string> {\n // Pre-compute paths once before sorting to avoid repeated .slice() allocations\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const pathCache = new Map<WritesProtocol<keyof Cc>, any[]>();\n for (const task of tasks) {\n pathCache.set(task, task.path?.slice(0, 3) || []);\n }\n\n // Sort tasks by first 3 path elements for deterministic order\n // Later path parts (like task IDs) are ignored for sorting\n tasks.sort((a, b) => {\n const aPath = pathCache.get(a)!;\n const bPath = pathCache.get(b)!;\n\n // Compare each path element\n for (let i = 0; i < Math.min(aPath.length, bPath.length); i += 1) {\n if (aPath[i] < bPath[i]) return -1;\n if (aPath[i] > bPath[i]) return 1;\n }\n\n // If one path is shorter, it comes first\n return aPath.length - bPath.length;\n });\n\n // Filter out non instances of BaseChannel\n const onlyChannels = getOnlyChannels(channels);\n\n // Single pass: update seen versions, check for triggers, collect channels to consume\n let bumpStep = false;\n const channelsToConsume = new Set<string>();\n for (const task of tasks) {\n if (task.triggers.length > 0) bumpStep = true;\n checkpoint.versions_seen[task.name] ??= {};\n for (const chan of task.triggers) {\n if (chan in checkpoint.channel_versions) {\n checkpoint.versions_seen[task.name][chan] =\n checkpoint.channel_versions[chan];\n }\n if (!RESERVED_SET.has(chan)) {\n channelsToConsume.add(chan);\n }\n }\n }\n\n // Find the highest version of all channels\n let maxVersion = maxChannelMapVersion(checkpoint.channel_versions);\n\n let usedNewVersion = false;\n for (const chan of channelsToConsume) {\n if (chan in onlyChannels && onlyChannels[chan].consume()) {\n if (getNextVersion !== undefined) {\n checkpoint.channel_versions[chan] = getNextVersion(maxVersion);\n usedNewVersion = true;\n }\n }\n }\n\n // Group writes by channel\n const pendingWritesByChannel = {} as Record<keyof Cc, PendingWriteValue[]>;\n // Originating task id per grouped write, used to reorder DeltaChannel writes\n // below.\n const pendingWriteTaskIdsByChannel = {} as Record<keyof Cc, string[]>;\n for (const task of tasks) {\n const taskId = (task as { id?: string }).id ?? \"\";\n for (const [chan, val] of task.writes) {\n if (IGNORE.has(chan)) {\n // do nothing\n } else if (chan in onlyChannels) {\n pendingWritesByChannel[chan] ??= [];\n pendingWritesByChannel[chan].push(val);\n pendingWriteTaskIdsByChannel[chan] ??= [];\n pendingWriteTaskIdsByChannel[chan].push(taskId);\n }\n }\n }\n\n // Reorder concurrent DeltaChannel writes to match the checkpointer replay\n // order: task id ascending (byte/string order, matching both MemorySaver and\n // the Postgres `COLLATE \"C\"` sort). A stable sort keeps each task's writes in\n // their original `idx` order, so reconstruction (see\n // `BaseCheckpointSaver.getDeltaChannelHistory`) matches the live value.\n for (const [chan, vals] of Object.entries(pendingWritesByChannel)) {\n if (vals.length < 2) continue;\n if (onlyChannels[chan]?.lc_graph_name !== \"DeltaChannel\") continue;\n const taskIds = pendingWriteTaskIdsByChannel[chan as keyof Cc];\n const paired = vals.map((val, i) => ({ val, taskId: taskIds[i] }));\n paired.sort((a, b) =>\n a.taskId < b.taskId ? -1 : a.taskId > b.taskId ? 1 : 0\n );\n pendingWritesByChannel[chan as keyof Cc] = paired.map((p) => p.val);\n }\n\n // Find the highest version of all channels\n if (maxVersion != null && getNextVersion != null) {\n maxVersion = usedNewVersion ? getNextVersion(maxVersion) : maxVersion;\n }\n\n const updatedChannels: Set<string> = new Set();\n // Apply writes to channels\n for (const [chan, vals] of Object.entries(pendingWritesByChannel)) {\n if (chan in onlyChannels) {\n const channel = onlyChannels[chan];\n let updated;\n try {\n updated = channel.update(vals);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } catch (e: any) {\n if (e.name === InvalidUpdateError.unminifiable_name) {\n const wrappedError = new InvalidUpdateError(\n `Invalid update for channel \"${chan}\" with values ${JSON.stringify(\n vals\n )}: ${e.message}`\n );\n wrappedError.lc_error_code = e.lc_error_code;\n throw wrappedError;\n } else {\n throw e;\n }\n }\n if (updated && getNextVersion !== undefined) {\n checkpoint.channel_versions[chan] = getNextVersion(maxVersion);\n\n // unavailable channels can't trigger tasks, so don't add them\n if (channel.isAvailable()) updatedChannels.add(chan);\n }\n }\n }\n\n // Channels that weren't updated in this step are notified of a new step\n if (bumpStep) {\n for (const chan in onlyChannels) {\n if (!Object.prototype.hasOwnProperty.call(onlyChannels, chan)) continue;\n\n const channel = onlyChannels[chan];\n if (channel.isAvailable() && !updatedChannels.has(chan)) {\n const updated = channel.update([]);\n\n if (updated && getNextVersion !== undefined) {\n checkpoint.channel_versions[chan] = getNextVersion(maxVersion);\n\n // unavailable channels can't trigger tasks, so don't add them\n if (channel.isAvailable()) updatedChannels.add(chan);\n }\n }\n }\n }\n\n // If this is (tentatively) the last superstep, notify all channels of finish\n if (bumpStep && !triggersNextStep(updatedChannels, triggerToNodes)) {\n for (const chan in onlyChannels) {\n if (!Object.prototype.hasOwnProperty.call(onlyChannels, chan)) continue;\n\n const channel = onlyChannels[chan];\n if (channel.finish() && getNextVersion !== undefined) {\n checkpoint.channel_versions[chan] = getNextVersion(maxVersion);\n\n // unavailable channels can't trigger tasks, so don't add them\n if (channel.isAvailable()) updatedChannels.add(chan);\n }\n }\n }\n\n return updatedChannels;\n}\n\nfunction* candidateNodes(\n checkpoint: ReadonlyCheckpoint,\n processes: StrRecord<string, PregelNode>,\n extra: NextTaskExtraFields\n) {\n // This section is an optimization that allows which\n // nodes will be active during the next step.\n // When there's information about:\n // 1. Which channels were updated in the previous step\n // 2. Which nodes are triggered by which channels\n // Then we can determine which nodes should be triggered\n // in the next step without having to cycle through all nodes.\n if (extra.updatedChannels != null && extra.triggerToNodes != null) {\n const triggeredNodes = new Set<string>();\n\n // Get all nodes that have triggers associated with an updated channel\n for (const channel of extra.updatedChannels) {\n const nodeIds = extra.triggerToNodes[channel];\n for (const id of nodeIds ?? []) triggeredNodes.add(id);\n }\n\n // Sort the nodes to ensure deterministic order\n yield* [...triggeredNodes].sort();\n return;\n }\n\n // If there are no values in checkpoint, no need to run\n // through all the PULL candidates\n const isEmptyChannelVersions = (() => {\n for (const chan in checkpoint.channel_versions) {\n if (checkpoint.channel_versions[chan] !== null) return false;\n }\n return true;\n })();\n\n if (isEmptyChannelVersions) return;\n for (const name in processes) {\n if (!Object.prototype.hasOwnProperty.call(processes, name)) continue;\n yield name;\n }\n}\n\n/**\n * Pre-indexed pending writes for O(1) lookups, avoiding repeated\n * linear scans in _prepareSingleTask and _scratchpad.\n */\nexport type PendingWritesIndex = {\n nullResume: unknown | undefined;\n resumeByTaskId: Map<string, unknown[]>;\n successfulWriteTaskIds: Set<string>;\n};\n\n/**\n * Build an index over pendingWrites for O(1) lookups.\n *\n * @internal Exported for benchmarks and regression tests only.\n */\nexport function _indexPendingWrites(\n pendingWrites: [string, string, unknown][] | undefined\n): PendingWritesIndex {\n let nullResume: unknown | undefined;\n const resumeByTaskId = new Map<string, unknown[]>();\n const successfulWriteTaskIds = new Set<string>();\n if (pendingWrites) {\n for (const [tid, chan, val] of pendingWrites) {\n if (tid === NULL_TASK_ID && chan === RESUME && nullResume === undefined) {\n nullResume = val;\n }\n if (chan === RESUME && tid !== NULL_TASK_ID) {\n let arr = resumeByTaskId.get(tid);\n if (!arr) {\n arr = [];\n resumeByTaskId.set(tid, arr);\n }\n arr.push(val);\n }\n if (chan !== ERROR) {\n successfulWriteTaskIds.add(tid);\n }\n }\n }\n return { nullResume, resumeByTaskId, successfulWriteTaskIds };\n}\n\nexport type NextTaskExtraFields = {\n step: number;\n isResuming?: boolean;\n checkpointer?: BaseCheckpointSaver;\n manager?: CallbackManagerForChainRun;\n store?: BaseStore;\n stream?: IterableReadableWritableStream;\n triggerToNodes?: Record<string, string[]>;\n updatedChannels?: Set<string>;\n pendingWritesIndex?: PendingWritesIndex;\n};\n\nexport type NextTaskExtraFieldsWithStore = NextTaskExtraFields & {\n store?: BaseStore;\n};\n\nexport type NextTaskExtraFieldsWithoutStore = NextTaskExtraFields & {\n store?: never;\n};\n\nexport function _prepareNextTasks<\n Nn extends StrRecord<string, PregelNode>,\n Cc extends StrRecord<string, BaseChannel>,\n>(\n checkpoint: ReadonlyCheckpoint,\n pendingWrites: [string, string, unknown][] | undefined,\n processes: Nn,\n channels: Cc,\n config: RunnableConfig,\n forExecution: false,\n extra: NextTaskExtraFieldsWithoutStore\n): Record<string, PregelTaskDescription>;\n\nexport function _prepareNextTasks<\n Nn extends StrRecord<string, PregelNode>,\n Cc extends StrRecord<string, BaseChannel>,\n>(\n checkpoint: ReadonlyCheckpoint,\n pendingWrites: [string, string, unknown][] | undefined,\n processes: Nn,\n channels: Cc,\n config: RunnableConfig,\n forExecution: true,\n extra: NextTaskExtraFieldsWithStore\n): Record<string, PregelExecutableTask<keyof Nn, keyof Cc>>;\n\n/**\n * Prepare the set of tasks that will make up the next Pregel step.\n * This is the union of all PUSH tasks (Sends) and PULL tasks (nodes triggered\n * by edges).\n */\nexport function _prepareNextTasks<\n Nn extends StrRecord<string, PregelNode>,\n Cc extends StrRecord<string, BaseChannel>,\n>(\n checkpoint: ReadonlyCheckpoint,\n pendingWrites: [string, string, unknown][] | undefined,\n processes: Nn,\n channels: Cc,\n config: RunnableConfig,\n forExecution: boolean,\n extra: NextTaskExtraFieldsWithStore | NextTaskExtraFieldsWithoutStore\n):\n | Record<string, PregelTaskDescription>\n | Record<string, PregelExecutableTask<keyof Nn, keyof Cc>> {\n const tasks:\n | Record<string, PregelExecutableTask<keyof Nn, keyof Cc>>\n | Record<string, PregelTaskDescription> = {};\n\n // Pre-index pendingWrites once for O(1) lookups in _prepareSingleTask/_scratchpad\n const indexedExtra: typeof extra = extra.pendingWritesIndex\n ? extra\n : { ...extra, pendingWritesIndex: _indexPendingWrites(pendingWrites) };\n\n // Consume pending tasks\n const tasksChannel = channels[TASKS] as Topic<SendProtocol> | undefined;\n\n if (tasksChannel?.isAvailable()) {\n const len = tasksChannel.get().length;\n for (let i = 0; i < len; i += 1) {\n const task = _prepareSingleTask(\n [PUSH, i],\n checkpoint,\n pendingWrites,\n processes,\n channels,\n config,\n forExecution,\n indexedExtra\n );\n if (task !== undefined) {\n tasks[task.id] = task;\n }\n }\n }\n\n // Check if any processes should be run in next step\n // If so, prepare the values to be passed to them\n for (const name of candidateNodes(checkpoint, processes, indexedExtra)) {\n const task = _prepareSingleTask(\n [PULL, name],\n checkpoint,\n pendingWrites,\n processes,\n channels,\n config,\n forExecution,\n indexedExtra\n );\n if (task !== undefined) {\n tasks[task.id] = task;\n }\n }\n return tasks;\n}\n\nexport function _prepareSingleTask<\n Nn extends StrRecord<string, PregelNode>,\n Cc extends StrRecord<string, BaseChannel>,\n>(\n taskPath: SimpleTaskPath,\n checkpoint: ReadonlyCheckpoint,\n pendingWrites: CheckpointPendingWrite[] | undefined,\n processes: Nn,\n channels: Cc,\n config: RunnableConfig,\n forExecution: false,\n extra: NextTaskExtraFields\n): PregelTaskDescription | undefined;\n\nexport function _prepareSingleTask<\n Nn extends StrRecord<string, PregelNode>,\n Cc extends StrRecord<string, BaseChannel>,\n>(\n taskPath: TaskPath,\n checkpoint: ReadonlyCheckpoint,\n pendingWrites: CheckpointPendingWrite[] | undefined,\n processes: Nn,\n channels: Cc,\n config: RunnableConfig,\n forExecution: true,\n extra: NextTaskExtraFields\n): PregelExecutableTask<keyof Nn, keyof Cc> | undefined;\n\nexport function _prepareSingleTask<\n Nn extends StrRecord<string, PregelNode>,\n Cc extends StrRecord<string, BaseChannel>,\n>(\n taskPath: TaskPath,\n checkpoint: ReadonlyCheckpoint,\n pendingWrites: CheckpointPendingWrite[] | undefined,\n processes: Nn,\n channels: Cc,\n config: RunnableConfig,\n forExecution: boolean,\n extra: NextTaskExtraFieldsWithStore\n): PregelTaskDescription | PregelExecutableTask<keyof Nn, keyof Cc> | undefined;\n\n/**\n * Prepares a single task for the next Pregel step, given a task path, which\n * uniquely identifies a PUSH or PULL task within the graph.\n */\nexport function _prepareSingleTask<\n Nn extends StrRecord<string, PregelNode>,\n Cc extends StrRecord<string, BaseChannel>,\n>(\n taskPath: TaskPath,\n checkpoint: ReadonlyCheckpoint,\n pendingWrites: CheckpointPendingWrite[] | undefined,\n processes: Nn,\n channels: Cc,\n config: LangGraphRunnableConfig,\n forExecution: boolean,\n extra: NextTaskExtraFields\n):\n | PregelTaskDescription\n | PregelExecutableTask<keyof Nn, keyof Cc>\n | undefined {\n const { step, checkpointer, manager } = extra;\n const configurable = config.configurable ?? {};\n const parentNamespace = configurable.checkpoint_ns ?? \"\";\n\n if (taskPath[0] === PUSH && isCall(taskPath[taskPath.length - 1])) {\n const call = taskPath[taskPath.length - 1] as Call;\n const proc = getRunnableForFunc(call.name, call.func);\n const triggers = [PUSH];\n const checkpointNamespace =\n parentNamespace === \"\"\n ? call.name\n : `${parentNamespace}${CHECKPOINT_NAMESPACE_SEPARATOR}${call.name}`;\n const id = uuid5(\n JSON.stringify([\n checkpointNamespace,\n step.toString(),\n call.name,\n PUSH,\n taskPath[1],\n taskPath[2],\n ]),\n checkpoint.id\n );\n const taskCheckpointNamespace = `${checkpointNamespace}${CHECKPOINT_NAMESPACE_END}${id}`;\n\n // we append `true` to the task path to indicate that a call is being made\n // so we should not return interrupts from this task (responsibility lies with the parent)\n const outputTaskPath = [...taskPath.slice(0, 3), true] as VariadicTaskPath;\n const metadata = {\n langgraph_step: step,\n langgraph_node: call.name,\n langgraph_triggers: triggers,\n langgraph_path: outputTaskPath,\n langgraph_checkpoint_ns: taskCheckpointNamespace,\n checkpoint_ns: taskCheckpointNamespace,\n };\n if (forExecution) {\n const writes: [keyof Cc, unknown][] = [];\n const executionInfo: ExecutionInfo = {\n checkpointId: checkpoint.id,\n checkpointNs: taskCheckpointNamespace,\n taskId: id,\n threadId: configurable.thread_id as string | undefined,\n runId: config.runId != null ? String(config.runId) : undefined,\n nodeAttempt: 1,\n };\n const task = {\n name: call.name,\n input: call.input,\n proc,\n writes,\n config: {\n ...patchConfig(\n mergeConfigs(config, {\n metadata,\n store: extra.store ?? config.store,\n }),\n {\n runName: call.name,\n callbacks: manager?.getChild(`graph:step:${step}`),\n configurable: {\n [CONFIG_KEY_TASK_ID]: id,\n [CONFIG_KEY_SEND]: (writes_: PendingWrite[]) =>\n _localWrite(\n (items: PendingWrite<keyof Cc>[]) => writes.push(...items),\n processes,\n writes_\n ),\n [CONFIG_KEY_READ]: (\n select_: Array<keyof Cc> | keyof Cc,\n fresh_: boolean = false\n ) =>\n _localRead(\n checkpoint,\n channels,\n {\n name: call.name,\n writes: writes as PendingWrite[],\n triggers,\n path: outputTaskPath,\n },\n select_,\n fresh_\n ),\n [CONFIG_KEY_CHECKPOINTER]:\n checkpointer ?? configurable[CONFIG_KEY_CHECKPOINTER],\n [CONFIG_KEY_CHECKPOINT_MAP]: {\n ...configurable[CONFIG_KEY_CHECKPOINT_MAP],\n [parentNamespace]: checkpoint.id,\n },\n [CONFIG_KEY_SCRATCHPAD]: _scratchpad({\n pendingWrites: pendingWrites ?? [],\n taskId: id,\n currentTaskInput: call.input,\n resumeMap: config.configurable?.[CONFIG_KEY_RESUME_MAP],\n namespaceHash: XXH3(taskCheckpointNamespace),\n pendingWritesIndex: extra.pendingWritesIndex,\n }),\n [CONFIG_KEY_PREVIOUS_STATE]:\n checkpoint.channel_values[PREVIOUS],\n checkpoint_id: undefined,\n checkpoint_ns: taskCheckpointNamespace,\n },\n }\n ),\n executionInfo,\n },\n triggers,\n retry_policy: call.retry,\n cache_key: call.cache\n ? {\n key: XXH3((call.cache.keyFunc ?? JSON.stringify)([call.input])),\n ns: [CACHE_NS_WRITES, call.name ?? \"__dynamic__\"],\n ttl: call.cache.ttl,\n }\n : undefined,\n id,\n path: outputTaskPath,\n writers: [],\n timeout: call.timeout,\n } satisfies PregelExecutableTask<keyof Nn, keyof Cc>;\n return task;\n } else {\n return {\n id,\n name: call.name,\n interrupts: [],\n path: outputTaskPath,\n };\n }\n } else if (taskPath[0] === PUSH) {\n const index =\n typeof taskPath[1] === \"number\"\n ? taskPath[1]\n : parseInt(taskPath[1] as string, 10);\n\n if (!channels[TASKS]?.isAvailable()) {\n return undefined;\n }\n\n const sends = channels[TASKS].get() as SendProtocol[];\n if (index < 0 || index >= sends.length) {\n return undefined;\n }\n\n const packet =\n _isSendInterface(sends[index]) && !_isSend(sends[index])\n ? new Send(\n sends[index].node,\n sends[index].args,\n sends[index].timeout !== undefined\n ? { timeout: sends[index].timeout }\n : undefined\n )\n : sends[index];\n\n if (!_isSendInterface(packet)) {\n console.warn(\n `Ignoring invalid packet ${JSON.stringify(packet)} in pending sends.`\n );\n return undefined;\n }\n if (!(packet.node in processes)) {\n console.warn(\n `Ignoring unknown node name ${packet.node} in pending sends.`\n );\n return undefined;\n }\n const triggers = [PUSH];\n const checkpointNamespace =\n parentNamespace === \"\"\n ? packet.node\n : `${parentNamespace}${CHECKPOINT_NAMESPACE_SEPARATOR}${packet.node}`;\n const taskId = uuid5(\n JSON.stringify([\n checkpointNamespace,\n step.toString(),\n packet.node,\n PUSH,\n index.toString(),\n ]),\n checkpoint.id\n );\n const taskCheckpointNamespace = `${checkpointNamespace}${CHECKPOINT_NAMESPACE_END}${taskId}`;\n let metadata = {\n langgraph_step: step,\n langgraph_node: packet.node,\n langgraph_triggers: triggers,\n langgraph_path: taskPath.slice(0, 3),\n langgraph_checkpoint_ns: taskCheckpointNamespace,\n checkpoint_ns: taskCheckpointNamespace,\n };\n if (forExecution) {\n const proc = processes[packet.node];\n const node = proc.getNode();\n if (node !== undefined) {\n if (proc.metadata !== undefined) {\n metadata = { ...metadata, ...proc.metadata };\n }\n const writes: [keyof Cc, unknown][] = [];\n const executionInfo: ExecutionInfo = {\n checkpointId: checkpoint.id,\n checkpointNs: taskCheckpointNamespace,\n taskId,\n threadId: configurable.thread_id as string | undefined,\n runId: config.runId != null ? String(config.runId) : undefined,\n nodeAttempt: 1,\n };\n return {\n name: packet.node,\n input: packet.args,\n proc: node,\n subgraphs: proc.subgraphs,\n writes,\n config: {\n ...patchConfig(\n mergeConfigs(config, {\n metadata,\n tags: proc.tags,\n store: extra.store ?? config.store,\n }),\n {\n runName: packet.node,\n callbacks: manager?.getChild(`graph:step:${step}`),\n configurable: {\n [CONFIG_KEY_TASK_ID]: taskId,\n [CONFIG_KEY_SEND]: (writes_: PendingWrite[]) =>\n _localWrite(\n (items: PendingWrite<keyof Cc>[]) =>\n writes.push(...items),\n processes,\n writes_\n ),\n [CONFIG_KEY_READ]: (\n select_: Array<keyof Cc> | keyof Cc,\n fresh_: boolean = false\n ) =>\n _localRead(\n checkpoint,\n channels,\n {\n name: packet.node,\n writes: writes as PendingWrite[],\n triggers,\n path: taskPath,\n },\n select_,\n fresh_\n ),\n [CONFIG_KEY_CHECKPOINTER]:\n checkpointer ?? configurable[CONFIG_KEY_CHECKPOINTER],\n [CONFIG_KEY_CHECKPOINT_MAP]: {\n ...configurable[CONFIG_KEY_CHECKPOINT_MAP],\n [parentNamespace]: checkpoint.id,\n },\n [CONFIG_KEY_SCRATCHPAD]: _scratchpad({\n pendingWrites: pendingWrites ?? [],\n taskId,\n currentTaskInput: packet.args,\n resumeMap: config.configurable?.[CONFIG_KEY_RESUME_MAP],\n namespaceHash: XXH3(taskCheckpointNamespace),\n pendingWritesIndex: extra.pendingWritesIndex,\n }),\n [CONFIG_KEY_PREVIOUS_STATE]:\n checkpoint.channel_values[PREVIOUS],\n checkpoint_id: undefined,\n checkpoint_ns: taskCheckpointNamespace,\n },\n }\n ),\n executionInfo,\n },\n triggers,\n retry_policy: proc.retryPolicy,\n cache_key: proc.cachePolicy\n ? {\n key: XXH3(\n (proc.cachePolicy.keyFunc ?? JSON.stringify)([packet.args])\n ),\n ns: [CACHE_NS_WRITES, proc.name ?? \"__dynamic__\", packet.node],\n ttl: proc.cachePolicy.ttl,\n }\n : undefined,\n id: taskId,\n path: taskPath,\n writers: proc.getWriters(),\n // a per-Send timeout overrides the target node's configured timeout\n timeout: packet.timeout ?? proc.timeout,\n } satisfies PregelExecutableTask<keyof Nn, keyof Cc>;\n }\n } else {\n return {\n id: taskId,\n name: packet.node,\n interrupts: [],\n path: taskPath,\n } satisfies PregelTaskDescription;\n }\n } else if (taskPath[0] === PULL) {\n const name = taskPath[1].toString();\n const proc = processes[name];\n if (proc === undefined) {\n return undefined;\n }\n\n // Check if this task already has successful writes in the pending writes\n if (pendingWrites?.length) {\n // Find the task ID for this node/path\n const checkpointNamespace =\n parentNamespace === \"\"\n ? name\n : `${parentNamespace}${CHECKPOINT_NAMESPACE_SEPARATOR}${name}`;\n\n const taskId = uuid5(\n JSON.stringify([\n checkpointNamespace,\n step.toString(),\n name,\n PULL,\n name,\n ]),\n checkpoint.id\n );\n\n const hasSuccessfulWrites = extra.pendingWritesIndex\n ? extra.pendingWritesIndex.successfulWriteTaskIds.has(taskId)\n : pendingWrites.some((w) => w[0] === taskId && w[1] !== ERROR);\n\n if (hasSuccessfulWrites) {\n return undefined;\n }\n }\n\n const nullVersion = getNullChannelVersion(checkpoint.channel_versions);\n if (nullVersion === undefined) {\n return undefined;\n }\n const seen = checkpoint.versions_seen[name] ?? {};\n\n // Find the first trigger that is available and has a new version\n const trigger = proc.triggers.find((chan) => {\n if (!channels[chan].isAvailable()) return false;\n\n return (\n (checkpoint.channel_versions[chan] ?? nullVersion) >\n (seen[chan] ?? nullVersion)\n );\n });\n\n // If any of the channels read by this process were updated\n if (trigger !== undefined) {\n const val = _procInput(proc, channels, forExecution);\n if (val === undefined) {\n return undefined;\n }\n const checkpointNamespace =\n parentNamespace === \"\"\n ? name\n : `${parentNamespace}${CHECKPOINT_NAMESPACE_SEPARATOR}${name}`;\n const taskId = uuid5(\n JSON.stringify([\n checkpointNamespace,\n step.toString(),\n name,\n PULL,\n [trigger],\n ]),\n checkpoint.id\n );\n const taskCheckpointNamespace = `${checkpointNamespace}${CHECKPOINT_NAMESPACE_END}${taskId}`;\n let metadata = {\n langgraph_step: step,\n langgraph_node: name,\n langgraph_triggers: [trigger],\n langgraph_path: taskPath,\n langgraph_checkpoint_ns: taskCheckpointNamespace,\n checkpoint_ns: taskCheckpointNamespace,\n };\n if (forExecution) {\n const node = proc.getNode();\n if (node !== undefined) {\n if (proc.metadata !== undefined) {\n metadata = { ...metadata, ...proc.metadata };\n }\n const writes: [keyof Cc, unknown][] = [];\n const executionInfo: ExecutionInfo = {\n checkpointId: checkpoint.id,\n checkpointNs: taskCheckpointNamespace,\n taskId,\n threadId: configurable.thread_id as string | undefined,\n runId: config.runId != null ? String(config.runId) : undefined,\n nodeAttempt: 1,\n };\n return {\n name,\n input: val,\n proc: node,\n subgraphs: proc.subgraphs,\n writes,\n config: {\n ...patchConfig(\n mergeConfigs(config, {\n metadata,\n tags: proc.tags,\n store: extra.store ?? config.store,\n }),\n {\n runName: name,\n callbacks: manager?.getChild(`graph:step:${step}`),\n configurable: {\n [CONFIG_KEY_TASK_ID]: taskId,\n [CONFIG_KEY_SEND]: (writes_: PendingWrite[]) =>\n _localWrite(\n (items: PendingWrite<keyof Cc>[]) => {\n writes.push(...items);\n },\n processes,\n writes_\n ),\n [CONFIG_KEY_READ]: (\n select_: Array<keyof Cc> | keyof Cc,\n fresh_: boolean = false\n ) =>\n _localRead(\n checkpoint,\n channels,\n {\n name,\n writes: writes as PendingWrite[],\n triggers: [trigger],\n path: taskPath,\n },\n select_,\n fresh_\n ),\n [CONFIG_KEY_CHECKPOINTER]:\n checkpointer ?? configurable[CONFIG_KEY_CHECKPOINTER],\n [CONFIG_KEY_CHECKPOINT_MAP]: {\n ...configurable[CONFIG_KEY_CHECKPOINT_MAP],\n [parentNamespace]: checkpoint.id,\n },\n [CONFIG_KEY_SCRATCHPAD]: _scratchpad({\n pendingWrites: pendingWrites ?? [],\n taskId,\n currentTaskInput: val,\n resumeMap: config.configurable?.[CONFIG_KEY_RESUME_MAP],\n namespaceHash: XXH3(taskCheckpointNamespace),\n pendingWritesIndex: extra.pendingWritesIndex,\n }),\n [CONFIG_KEY_PREVIOUS_STATE]:\n checkpoint.channel_values[PREVIOUS],\n checkpoint_id: undefined,\n checkpoint_ns: taskCheckpointNamespace,\n },\n }\n ),\n executionInfo,\n },\n triggers: [trigger],\n retry_policy: proc.retryPolicy,\n cache_key: proc.cachePolicy\n ? {\n key: XXH3(\n (proc.cachePolicy.keyFunc ?? JSON.stringify)([val])\n ),\n ns: [CACHE_NS_WRITES, proc.name ?? \"__dynamic__\", name],\n ttl: proc.cachePolicy.ttl,\n }\n : undefined,\n id: taskId,\n path: taskPath,\n writers: proc.getWriters(),\n timeout: proc.timeout,\n } satisfies PregelExecutableTask<keyof Nn, keyof Cc>;\n }\n } else {\n return {\n id: taskId,\n name,\n interrupts: [],\n path: taskPath,\n } satisfies PregelTaskDescription;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Prepare an immediate node-level error handler task for a failed task.\n *\n * The handler runs only after the failed node's retry policy is exhausted (the\n * runner schedules it once a non-bubble-up error settles). It is prepared like\n * a PUSH task targeting the auto-generated handler node, receives the failed\n * node's input, and is injected with a {@link NodeError} under\n * {@link CONFIG_KEY_NODE_ERROR} so the handler can inspect the failure\n * provenance (and route via `Command({ goto })`).\n *\n * @internal\n */\nexport function _prepareNodeErrorHandlerTask<\n Nn extends StrRecord<string, PregelNode>,\n Cc extends StrRecord<string, BaseChannel>,\n>(\n failedTask: PregelExecutableTask<keyof Nn, keyof Cc>,\n handlerNodeName: string,\n error: Error,\n checkpoint: ReadonlyCheckpoint,\n pendingWrites: CheckpointPendingWrite[] | undefined,\n processes: Nn,\n channels: Cc,\n config: LangGraphRunnableConfig,\n extra: NextTaskExtraFieldsWithStore\n): PregelExecutableTask<keyof Nn, keyof Cc> | undefined {\n const { step, checkpointer, manager } = extra;\n const proc = processes[handlerNodeName as keyof Nn];\n if (proc === undefined) {\n return undefined;\n }\n const node = proc.getNode();\n if (node === undefined) {\n return undefined;\n }\n\n const configurable = config.configurable ?? {};\n const parentNamespace = configurable.checkpoint_ns ?? \"\";\n const triggers = [PUSH];\n const checkpointNamespace =\n parentNamespace === \"\"\n ? handlerNodeName\n : `${parentNamespace}${CHECKPOINT_NAMESPACE_SEPARATOR}${handlerNodeName}`;\n // Deterministic task id (includes the failed task id) so resuming from a\n // checkpoint reconstructs the same handler task.\n const taskId = uuid5(\n JSON.stringify([\n checkpointNamespace,\n step.toString(),\n handlerNodeName,\n PUSH,\n \"node_error_handler\",\n failedTask.id,\n ]),\n checkpoint.id\n );\n const taskCheckpointNamespace = `${checkpointNamespace}${CHECKPOINT_NAMESPACE_END}${taskId}`;\n // Last path element is a string (not `true`), so interrupts raised by the\n // handler are surfaced normally rather than deferred to a parent call.\n const taskPath = [\n PUSH,\n String(failedTask.name),\n handlerNodeName,\n false,\n ] as VariadicTaskPath;\n\n let metadata = {\n langgraph_step: step,\n langgraph_node: handlerNodeName,\n langgraph_triggers: triggers,\n langgraph_path: taskPath,\n langgraph_checkpoint_ns: taskCheckpointNamespace,\n checkpoint_ns: taskCheckpointNamespace,\n };\n if (proc.metadata !== undefined) {\n metadata = { ...metadata, ...proc.metadata };\n }\n\n const writes: [keyof Cc, unknown][] = [];\n const executionInfo: ExecutionInfo = {\n checkpointId: checkpoint.id,\n checkpointNs: taskCheckpointNamespace,\n taskId,\n threadId: configurable.thread_id as string | undefined,\n runId: config.runId != null ? String(config.runId) : undefined,\n nodeAttempt: 1,\n };\n\n return {\n name: handlerNodeName as keyof Nn,\n input: failedTask.input,\n proc: node,\n subgraphs: proc.subgraphs,\n writes,\n config: {\n ...patchConfig(\n mergeConfigs(config, {\n metadata,\n tags: proc.tags,\n store: extra.store ?? config.store,\n }),\n {\n runName: handlerNodeName,\n callbacks: manager?.getChild(`graph:step:${step}`),\n configurable: {\n [CONFIG_KEY_TASK_ID]: taskId,\n [CONFIG_KEY_SEND]: (writes_: PendingWrite[]) =>\n _localWrite(\n (items: PendingWrite<keyof Cc>[]) => writes.push(...items),\n processes,\n writes_\n ),\n [CONFIG_KEY_READ]: (\n select_: Array<keyof Cc> | keyof Cc,\n fresh_: boolean = false\n ) =>\n _localRead(\n checkpoint,\n channels,\n {\n name: handlerNodeName,\n writes: writes as PendingWrite[],\n triggers,\n path: taskPath,\n },\n select_,\n fresh_\n ),\n [CONFIG_KEY_CHECKPOINTER]:\n checkpointer ?? configurable[CONFIG_KEY_CHECKPOINTER],\n [CONFIG_KEY_CHECKPOINT_MAP]: {\n ...configurable[CONFIG_KEY_CHECKPOINT_MAP],\n [parentNamespace]: checkpoint.id,\n },\n [CONFIG_KEY_SCRATCHPAD]: _scratchpad({\n pendingWrites: pendingWrites ?? [],\n taskId,\n currentTaskInput: failedTask.input,\n resumeMap: config.configurable?.[CONFIG_KEY_RESUME_MAP],\n namespaceHash: XXH3(taskCheckpointNamespace),\n }),\n [CONFIG_KEY_PREVIOUS_STATE]: checkpoint.channel_values[PREVIOUS],\n [CONFIG_KEY_NODE_ERROR]: new NodeError(\n String(failedTask.name),\n error\n ),\n checkpoint_id: undefined,\n checkpoint_ns: taskCheckpointNamespace,\n },\n }\n ),\n executionInfo,\n },\n triggers,\n retry_policy: proc.retryPolicy,\n cache_key: undefined,\n id: taskId,\n path: taskPath,\n writers: proc.getWriters(),\n } satisfies PregelExecutableTask<keyof Nn, keyof Cc>;\n}\n\n/**\n * Function injected under CONFIG_KEY_READ in task config, to read current state.\n * Used by conditional edges to read a copy of the state with reflecting the writes\n * from that node only.\n *\n * @internal\n */\nfunction _procInput(\n proc: PregelNode,\n channels: StrRecord<string, BaseChannel>,\n forExecution: boolean\n) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let val: any;\n\n if (typeof proc.channels === \"object\" && !Array.isArray(proc.channels)) {\n val = {};\n for (const [k, chan] of Object.entries(proc.channels)) {\n if (proc.triggers.includes(chan)) {\n try {\n val[k] = readChannel(channels, chan, false);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } catch (e: any) {\n if (e.name === EmptyChannelError.unminifiable_name) {\n return undefined;\n } else {\n throw e;\n }\n }\n } else if (chan in channels) {\n try {\n val[k] = readChannel(channels, chan, false);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } catch (e: any) {\n if (e.name === EmptyChannelError.unminifiable_name) {\n continue;\n } else {\n throw e;\n }\n }\n }\n }\n } else if (Array.isArray(proc.channels)) {\n let successfulRead = false;\n for (const chan of proc.channels) {\n try {\n val = readChannel(channels, chan, false);\n successfulRead = true;\n break;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } catch (e: any) {\n if (e.name === EmptyChannelError.unminifiable_name) {\n continue;\n } else {\n throw e;\n }\n }\n }\n if (!successfulRead) {\n return undefined;\n }\n } else {\n throw new Error(\n `Invalid channels type, expected list or dict, got ${proc.channels}`\n );\n }\n\n // If the process has a mapper, apply it to the value\n if (forExecution && proc.mapper !== undefined) {\n val = proc.mapper(val);\n }\n\n return val;\n}\n\n/**\n * Remove any values belonging to UntrackedValue channels from a Send packet\n * before checkpointing.\n *\n * Send is often called with state to be passed to the destination node,\n * which may contain UntrackedValues at the top level.\n *\n * @internal\n */\nexport function sanitizeUntrackedValuesInSend(\n packet: Send,\n channels: StrRecord<string, BaseChannel>\n): Send {\n if (typeof packet.args !== \"object\" || packet.args === null) {\n // Not a dict-like arg\n return packet;\n }\n\n // Top-level keys should be channel names\n const sanitizedArg: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(packet.args)) {\n const channel = channels[key];\n // Check if channel is an UntrackedValue by its lc_graph_name\n if (!channel || channel.lc_graph_name !== \"UntrackedValue\") {\n sanitizedArg[key] = value;\n }\n }\n\n return new Send(packet.node, sanitizedArg);\n}\n\nfunction _scratchpad({\n pendingWrites,\n taskId,\n currentTaskInput,\n resumeMap,\n namespaceHash,\n pendingWritesIndex,\n}: {\n pendingWrites: CheckpointPendingWrite[];\n taskId: string;\n currentTaskInput: unknown;\n resumeMap: Record<string, unknown> | undefined;\n namespaceHash: string;\n pendingWritesIndex?: PendingWritesIndex;\n}): PregelScratchpad {\n const nullResume = pendingWritesIndex\n ? pendingWritesIndex.nullResume\n : pendingWrites.find(\n ([writeTaskId, chan]) => writeTaskId === NULL_TASK_ID && chan === RESUME\n )?.[2];\n\n const resume = (() => {\n // flatMap flattens array resume values one level; mirror that with .flat()\n const result: unknown[] = pendingWritesIndex\n ? (pendingWritesIndex.resumeByTaskId.get(taskId) ?? []).flat()\n : pendingWrites\n .filter(\n ([writeTaskId, chan]) => writeTaskId === taskId && chan === RESUME\n )\n .flatMap(([_writeTaskId, _chan, resume]) => resume);\n\n if (resumeMap != null && namespaceHash in resumeMap) {\n const mappedResume = resumeMap[namespaceHash];\n result.push(mappedResume);\n }\n\n return result;\n })();\n\n const scratchpad = {\n callCounter: 0,\n interruptCounter: -1,\n resume,\n nullResume,\n subgraphCounter: 0,\n currentTaskInput,\n consumeNullResume: () => {\n if (scratchpad.nullResume) {\n delete scratchpad.nullResume;\n pendingWrites.splice(\n pendingWrites.findIndex(\n ([writeTaskId, chan]) =>\n writeTaskId === NULL_TASK_ID && chan === RE