UNPKG

@langchain/langgraph

Version:
1 lines 13.7 kB
{"version":3,"file":"timeout.cjs","names":["BaseCallbackHandler","#scope","#touch","CONFIG_KEY_SEND","CONFIG_KEY_CALL","patchConfigurable","combineAbortSignals","NodeTimeoutError"],"sources":["../../src/pregel/timeout.ts"],"sourcesContent":["import { BaseCallbackHandler } from \"@langchain/core/callbacks/base\";\nimport { PendingWrite } from \"@langchain/langgraph-checkpoint\";\nimport { CONFIG_KEY_CALL, CONFIG_KEY_SEND } from \"../constants.js\";\nimport { NodeTimeoutError } from \"../errors.js\";\nimport { LangGraphRunnableConfig } from \"./runnable_types.js\";\nimport { PregelExecutableTask } from \"./types.js\";\nimport {\n combineAbortSignals,\n patchConfigurable,\n TimeoutPolicy,\n} from \"./utils/index.js\";\n\n/**\n * Tracks the live progress state of a single timed node attempt.\n *\n * The scope guards observable-progress channels (writes, child-task\n * scheduling, the custom stream writer, callbacks) so that:\n *\n * 1. `idleTimeout` is refreshed whenever the node makes progress, and\n * 2. once the attempt is `close()`d after a timeout fires, late writes/calls\n * from the still-running background task are dropped (Python parity:\n * buffered writes from the failed attempt must not leak into the\n * checkpoint).\n *\n * @internal\n */\nclass TimedAttemptScope {\n active = true;\n\n lastProgress = Date.now();\n\n private refreshOn: \"auto\" | \"heartbeat\";\n\n constructor(refreshOn: \"auto\" | \"heartbeat\") {\n this.refreshOn = refreshOn;\n }\n\n /** Record progress now. Always honored (used by `runtime.heartbeat()`). */\n touch(): void {\n this.lastProgress = Date.now();\n }\n\n /**\n * Record progress for an automatic signal (write/call/stream/callback).\n * No-op when `refreshOn === \"heartbeat\"`, where only explicit heartbeats\n * count as progress.\n */\n autoTouch(): void {\n if (this.refreshOn === \"auto\") {\n this.lastProgress = Date.now();\n }\n }\n\n close(): void {\n this.active = false;\n }\n}\n\n/**\n * Callback handler that refreshes a {@link TimedAttemptScope} on any LangChain\n * callback event emitted under the node's run. Because it is attached via\n * `config.callbacks`, it only observes events from runs descended from this\n * node's attempt, not from sibling nodes.\n *\n * @internal\n */\nclass IdleProgressCallbackHandler extends BaseCallbackHandler {\n name = \"IdleProgressCallbackHandler\";\n\n awaitHandlers = false;\n\n #scope: TimedAttemptScope;\n\n constructor(scope: TimedAttemptScope) {\n super();\n this.#scope = scope;\n }\n\n #touch = () => {\n this.#scope.autoTouch();\n };\n\n handleLLMStart = this.#touch;\n\n handleChatModelStart = this.#touch;\n\n handleLLMNewToken = this.#touch;\n\n handleLLMEnd = this.#touch;\n\n handleLLMError = this.#touch;\n\n handleChainStart = this.#touch;\n\n handleChainEnd = this.#touch;\n\n handleChainError = this.#touch;\n\n handleToolStart = this.#touch;\n\n handleToolEnd = this.#touch;\n\n handleToolError = this.#touch;\n\n handleText = this.#touch;\n\n handleRetrieverStart = this.#touch;\n\n handleRetrieverEnd = this.#touch;\n\n handleRetrieverError = this.#touch;\n\n handleCustomEvent = this.#touch;\n}\n\n/**\n * Wrap the node attempt config so observable-progress signals refresh the idle\n * clock and are dropped once the scope is closed. Also injects\n * {@link LangGraphRunnableConfig.heartbeat}.\n */\nfunction wrapConfig(\n config: LangGraphRunnableConfig,\n scope: TimedAttemptScope,\n policy: TimeoutPolicy,\n taskName: string\n): LangGraphRunnableConfig {\n const configurable = config.configurable ?? {};\n const patch: Record<string, unknown> = {};\n\n const send = configurable[CONFIG_KEY_SEND];\n if (typeof send === \"function\") {\n patch[CONFIG_KEY_SEND] = (writes: PendingWrite[]) => {\n if (!scope.active) return undefined;\n if (writes && writes.length) scope.autoTouch();\n return send(writes);\n };\n }\n\n const callFn = configurable[CONFIG_KEY_CALL];\n if (typeof callFn === \"function\") {\n patch[CONFIG_KEY_CALL] = (...args: unknown[]) => {\n if (!scope.active) {\n throw new Error(\n `Node \"${taskName}\" attempt was cancelled after its timeout fired`\n );\n }\n scope.autoTouch();\n return (callFn as (...a: unknown[]) => unknown)(...args);\n };\n }\n\n const out: LangGraphRunnableConfig =\n Object.keys(patch).length > 0 ? patchConfigurable(config, patch) : config;\n const wrapped: LangGraphRunnableConfig = { ...out };\n\n // `heartbeat` always resets the idle clock, even under `refreshOn:\n // \"heartbeat\"`. It is a no-op when no idle timeout is configured.\n wrapped.heartbeat = () => {\n if (policy.idleTimeout !== undefined) scope.touch();\n };\n\n if (typeof wrapped.writer === \"function\") {\n const writer = wrapped.writer;\n wrapped.writer = ((chunk: unknown) => {\n if (!scope.active) return undefined;\n scope.autoTouch();\n return (writer as (c: unknown) => unknown)(chunk);\n }) as typeof wrapped.writer;\n }\n\n if (\n (policy.refreshOn ?? \"auto\") === \"auto\" &&\n policy.idleTimeout !== undefined\n ) {\n const handler = new IdleProgressCallbackHandler(scope);\n const cb = wrapped.callbacks;\n if (cb === undefined) {\n wrapped.callbacks = [handler];\n } else if (Array.isArray(cb)) {\n wrapped.callbacks = [...cb, handler];\n } else {\n const copied = cb.copy();\n copied.addHandler(handler, true);\n wrapped.callbacks = copied;\n }\n }\n\n return wrapped;\n}\n\ntype AttemptOutcome<T> =\n | { type: \"ok\"; value: T }\n | { type: \"err\"; error: unknown }\n | { type: \"timeout\"; kind: \"run\" | \"idle\" };\n\n/**\n * Run a single node attempt under a {@link TimeoutPolicy}.\n *\n * Races the node invocation against per-attempt run/idle watchdogs. On\n * successful completion (or node error), returns/rethrows normally. When a\n * watchdog fires first, the scope is closed, the task's buffered writes are\n * dropped, the attempt's {@link AbortSignal} is aborted, and a\n * {@link NodeTimeoutError} is thrown.\n *\n * @internal\n */\nexport async function runAttemptWithTimeout<T>(\n task: PregelExecutableTask<string, string>,\n config: LangGraphRunnableConfig,\n policy: TimeoutPolicy,\n invoke: (scopedConfig: LangGraphRunnableConfig) => Promise<T>\n): Promise<T> {\n const refreshOn = policy.refreshOn ?? \"auto\";\n const scope = new TimedAttemptScope(refreshOn);\n\n const timeoutController = new AbortController();\n const { signal: composedSignal, dispose } = combineAbortSignals(\n config.signal,\n timeoutController.signal\n );\n\n const scopedConfig = wrapConfig(\n { ...config, signal: composedSignal },\n scope,\n policy,\n String(task.name)\n );\n\n const start = Date.now();\n const bg = invoke(scopedConfig);\n\n // Normalize the node result into an outcome. Catching the rejection here\n // (rather than via a separate `bg.catch`) keeps `bg` handled even when a\n // watchdog wins the race below, so its late settlement can never surface as\n // an unhandled rejection.\n const nodeOutcome: Promise<AttemptOutcome<T>> = bg.then(\n (value) => ({ type: \"ok\", value }),\n (error) => ({ type: \"err\", error })\n );\n\n let runTimer: ReturnType<typeof setTimeout> | undefined;\n let idleTimer: ReturnType<typeof setTimeout> | undefined;\n const clearTimers = () => {\n if (runTimer !== undefined) clearTimeout(runTimer);\n if (idleTimer !== undefined) clearTimeout(idleTimer);\n };\n\n // The watchdog never rejects: it either resolves with a timeout outcome or\n // stays pending forever (and is dropped) when the node wins the race. The\n // idle timer re-arms itself off `scope.lastProgress`, so it can't be a single\n // fixed `setTimeout`.\n const watchdog = new Promise<AttemptOutcome<T>>((resolve) => {\n if (policy.runTimeout !== undefined) {\n runTimer = setTimeout(\n () => resolve({ type: \"timeout\", kind: \"run\" }),\n policy.runTimeout\n );\n }\n\n if (policy.idleTimeout !== undefined) {\n const idleMs = policy.idleTimeout;\n const checkIdle = () => {\n const remaining = scope.lastProgress + idleMs - Date.now();\n if (remaining <= 0) {\n resolve({ type: \"timeout\", kind: \"idle\" });\n } else {\n idleTimer = setTimeout(checkIdle, remaining);\n }\n };\n idleTimer = setTimeout(checkIdle, idleMs);\n }\n });\n\n let outcome: AttemptOutcome<T>;\n try {\n outcome = await Promise.race([nodeOutcome, watchdog]);\n } finally {\n clearTimers();\n }\n\n // Watchdog timers are macrotasks and cannot fire while a synchronous\n // (CPU-bound) node blocks the event loop. Such a node — or one with a long\n // synchronous prefix — can therefore settle the race as \"ok\"/\"err\" before an\n // already-expired timer ever runs, bypassing the documented hard wall-clock\n // cap. Re-check the budget against wall-clock time here so the caps hold for\n // synchronous nodes too. (For genuinely async work the watchdog already\n // wins the race, so this only catches what timers structurally cannot.)\n if (outcome.type !== \"timeout\") {\n const now = Date.now();\n if (policy.runTimeout !== undefined && now - start >= policy.runTimeout) {\n outcome = { type: \"timeout\", kind: \"run\" };\n } else if (\n policy.idleTimeout !== undefined &&\n now - scope.lastProgress >= policy.idleTimeout\n ) {\n outcome = { type: \"timeout\", kind: \"idle\" };\n }\n }\n\n if (outcome.type === \"ok\") {\n dispose?.();\n return outcome.value;\n }\n if (outcome.type === \"err\") {\n dispose?.();\n throw outcome.error;\n }\n\n // A watchdog fired: close the scope (drop late writes/calls), discard the\n // attempt's buffered writes, abort the node, and surface a NodeTimeoutError.\n const elapsed = Date.now() - start;\n scope.close();\n task.writes.splice(0, task.writes.length);\n // Abort BEFORE disposing the combined signal so the abort actually\n // propagates to the node's signal (dispose removes the relay listeners).\n timeoutController.abort();\n dispose?.();\n\n throw new NodeTimeoutError({\n node: String(task.name),\n elapsed,\n kind: outcome.kind,\n runTimeout: policy.runTimeout,\n idleTimeout: policy.idleTimeout,\n });\n}\n"],"mappings":"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"}