UNPKG

@langchain/langgraph-sdk

Version:

Client library for interacting with the LangGraph API

1 lines • 42.1 kB
{"version":3,"file":"submit-coordinator.cjs","names":["#options","#rootStore","#queueAdapter","#getDisposed","#getCurrentThreadId","#setCurrentThreadId","#rememberSelfCreatedThreadId","#isSelfCreatedThreadId","#forgetSelfCreatedThreadId","#hydrate","#ensureThread","#startDeferredRootPump","#abandonDeferredRootPump","#waitForRootPumpReady","#awaitNextTerminal","#awaitResumedRunTerminal","#onSubmitStart","#onRunStart","#onRunCreated","#onRunStartReturned","#onRunCompleted","#onRunEnd","#beginOptimistic","#settleOptimistic","AgentServerQueueAdapter","LocalQueueAdapter","#runAbort","bindThreadConfig","#scheduleQueueDrain"],"sources":["../../src/stream/submit-coordinator.ts"],"sourcesContent":["/**\n * Owns the run-submission lifecycle for a single\n * {@link StreamController}.\n *\n * # What this module is\n *\n * The {@link SubmitCoordinator} is the piece of the controller that\n * dispatches runs (`submit()`), enforces multitask strategies, queues\n * deferred submissions, races dispatch against terminal lifecycle\n * events, and surfaces errors back through the per-submit `onError`\n * callback and the root snapshot.\n *\n * Conceptually a submit looks like:\n *\n * 1. Optionally rebind to a different thread (`options.threadId`).\n * 2. Mint a thread id if one isn't bound yet.\n * 3. Wait for the controller's root pump to be ready (so the\n * transport is subscribed before the run is dispatched —\n * otherwise we could miss replayed events).\n * 4. Apply the {@link StreamSubmitOptions.multitaskStrategy} to\n * decide whether to abort, enqueue, reject, or proceed.\n * 5. Race the dispatch promise (`thread.submitRun()`) against the next root\n * terminal lifecycle event.\n * 6. Settle the resulting state (loading flag, error slot) and\n * drain the next queued submission, if any.\n *\n * # Why it lives in its own class\n *\n * The submit lifecycle is the most state-heavy part of the\n * controller — six promises, an abort controller, a queue, a\n * terminal-vs-command race, and bidirectional callback wiring with\n * the controller. Splitting it out keeps `controller.ts` focused on\n * subscription / projection wiring while letting the submit logic\n * evolve independently.\n *\n * # Why we race \"command\" against \"terminal\"\n *\n * For fast runs, the server's terminal lifecycle event can arrive\n * *before* the dispatch HTTP response has resolved. Racing the two\n * lets us detect terminal early and not block waiting for a now-stale\n * dispatch response. The dispatch response is still consumed (via\n * `.then(notifyCreated).catch(reportError)`) so `onCreated` still\n * fires and dispatch errors still surface through `onError`.\n *\n * # Queue semantics (`multitaskStrategy: \"enqueue\"`)\n *\n * When a run is already in flight, an `\"enqueue\"` submit is delegated to\n * a {@link QueueAdapter} instead of being handled inline.\n * `SubmitCoordinator` only owns *which* adapter gets built and feeding\n * it a dispatch callback (`onIdle`) once the active run settles.\n *\n * @see StreamController - The owner; injects every collaborator dep.\n */\nimport { v7 as uuidv7 } from \"@langchain/core/utils/uuid\";\nimport type { ThreadStream } from \"../client/stream/index.js\";\nimport { bindThreadConfig } from \"./dispatch-config.js\";\nimport {\n EMPTY_QUEUE,\n type QueueAdapter,\n type ServerQueueCapability,\n type SubmissionQueueEntry,\n type SubmissionQueueSnapshot,\n} from \"./queue-adapter.js\";\nimport { AgentServerQueueAdapter } from \"./queue-adapter-agent-server.js\";\nimport { LocalQueueAdapter } from \"./queue-adapter-local.js\";\nimport { StreamStore } from \"./store.js\";\nimport type { OptimisticHandle } from \"./optimistic-input.js\";\nimport type {\n RootSnapshot,\n RunExecutionReason,\n StreamControllerOptions,\n StreamSubmitOptions,\n} from \"./types.js\";\n\nexport { EMPTY_QUEUE };\nexport type { SubmissionQueueEntry, SubmissionQueueSnapshot };\n\n/**\n * Result of awaiting the next root terminal lifecycle event. Mirrors\n * the three terminal lifecycle states the protocol surfaces, plus a\n * synthetic `\"aborted\"` for client-side cancellation.\n */\ntype TerminalResult = {\n event: \"completed\" | \"failed\" | \"interrupted\" | \"aborted\";\n error?: string;\n};\n\nfunction terminalReason(event: TerminalResult[\"event\"]): RunExecutionReason {\n if (event === \"completed\") return \"success\";\n if (event === \"failed\") return \"error\";\n if (event === \"interrupted\") return \"interrupt\";\n return \"stopped\";\n}\n\n/** Validates `client.runs`'s shape; throws rather than falling back to local. */\nfunction assertServerQueueCapability(\n value: unknown\n): asserts value is ServerQueueCapability {\n const ok =\n typeof value === \"object\" &&\n value != null &&\n typeof (value as ServerQueueCapability).create === \"function\" &&\n typeof (value as ServerQueueCapability).list === \"function\" &&\n typeof (value as ServerQueueCapability).cancel === \"function\";\n if (!ok) {\n throw new Error(\n 'queue: \"server\" requires client.runs to implement create/list/cancel'\n );\n }\n}\n\n/**\n * Coordinates one controller's run-submission lifecycle.\n *\n * The constructor takes a bag of callbacks rather than a reference to\n * the parent {@link StreamController} on purpose:\n *\n * - It keeps the dependency surface explicit and testable — every\n * piece of controller state the submit lifecycle touches is one\n * of these closures.\n * - It avoids a cyclic dependency between controller and coordinator.\n * - Tests can construct one with stub callbacks and assert behavior\n * without mocking the entire controller.\n *\n * @typeParam StateType - Root state shape.\n * @typeParam InterruptType - Root interrupt payload shape.\n * @typeParam ConfigurableType - `config.configurable` shape accepted\n * by submit (usually `Record<string, unknown>`).\n */\nexport class SubmitCoordinator<\n StateType extends object = Record<string, unknown>,\n InterruptType = unknown,\n ConfigurableType extends object = Record<string, unknown>,\n> {\n /** Controller-level options forwarded into `submitRun` / callbacks. */\n readonly #options: StreamControllerOptions<StateType>;\n /** Root snapshot store; written for `isLoading`, `error`, `interrupts`. */\n readonly #rootStore: StreamStore<RootSnapshot<StateType, InterruptType>>;\n /** Pending submissions awaiting the active run to terminate. */\n /** Backs `\"enqueue\"`: local-only defer, or real durable runs when `queue: \"server\"`. */\n readonly #queueAdapter: QueueAdapter<StateType>;\n /** Probes the controller's `disposed` flag from deferred work. */\n readonly #getDisposed: () => boolean;\n /** Reads the controller's currently-bound thread id. */\n readonly #getCurrentThreadId: () => string | null;\n /** Updates the controller's thread id (used when minting a new id). */\n readonly #setCurrentThreadId: (threadId: string | null) => void;\n /** Records a thread id we created client-side so hydrate can skip a 404 round-trip. */\n readonly #rememberSelfCreatedThreadId: (threadId: string) => void;\n /** True when the id is pending server-side create (minted or hydrate 404). */\n readonly #isSelfCreatedThreadId: (threadId: string) => boolean;\n /** Drops a thread id from the self-created set once it's committed server-side. */\n readonly #forgetSelfCreatedThreadId: (threadId: string) => void;\n /** Triggers a hydrate on the controller (used by `options.threadId` rebinds). */\n readonly #hydrate: (threadId?: string | null) => Promise<void>;\n /** Lazily creates / returns the active {@link ThreadStream}. */\n readonly #ensureThread: (\n threadId: string,\n deferRootPump?: boolean\n ) => ThreadStream;\n /** Starts the previously-deferred root pump after a self-created thread commits. */\n readonly #startDeferredRootPump: () => void;\n /** Abandons a deferred root pump after a self-created dispatch fails. */\n readonly #abandonDeferredRootPump: () => void;\n /** Resolves once the controller's root subscription pump is up. */\n readonly #waitForRootPumpReady: () => Promise<void> | undefined;\n /** Resolves on the next root terminal lifecycle (or on abort). */\n readonly #awaitNextTerminal: (signal: AbortSignal) => Promise<TerminalResult>;\n /**\n * Resolves on the resumed run's terminal, skipping stale `interrupted`\n * events from the run being resumed (see {@link dispatchResume}).\n */\n readonly #awaitResumedRunTerminal: (\n signal: AbortSignal\n ) => Promise<TerminalResult>;\n /** Called once at the start of every {@link submit} invocation. */\n readonly #onSubmitStart: () => void;\n /** Marks that a local run dispatch is now active. */\n readonly #onRunStart: () => void;\n /** Records a server-accepted local run id and fires `onCreated`. */\n readonly #onRunCreated: (runId: string) => void;\n /** `run.start` answered for the current submit, with or without a run id. */\n readonly #onRunStartReturned: (runId: string | undefined) => void;\n /** Fires `onCompleted` for the local run lifecycle. */\n readonly #onRunCompleted: (\n reason: RunExecutionReason,\n runId?: string\n ) => void;\n /** Marks the local run dispatch lifecycle as settled. */\n readonly #onRunEnd: () => void;\n /**\n * Apply a submit input optimistically before dispatch. Returns the\n * id-injected payload to dispatch plus a handle for terminal\n * reconciliation, or `undefined` when optimistic UI is disabled / no\n * echo applies (dispatch the raw input).\n */\n readonly #beginOptimistic: (\n input: unknown\n ) => { dispatchInput: unknown; handle: OptimisticHandle } | undefined;\n /** Reconcile optimistic state when a run terminates. */\n readonly #settleOptimistic: (\n handle: OptimisticHandle,\n event: TerminalResult[\"event\"]\n ) => void;\n\n /**\n * Active submission's abort controller. `undefined` between submits.\n *\n * Used both for `multitaskStrategy: \"rollback\"` (abort the previous\n * controller's signal) and `stop()` (abort the current one without\n * starting a new one).\n */\n #runAbort: AbortController | undefined;\n\n constructor(params: {\n options: StreamControllerOptions<StateType>;\n rootStore: StreamStore<RootSnapshot<StateType, InterruptType>>;\n queueStore: StreamStore<SubmissionQueueSnapshot<StateType>>;\n getDisposed: () => boolean;\n getCurrentThreadId: () => string | null;\n setCurrentThreadId: (threadId: string | null) => void;\n rememberSelfCreatedThreadId: (threadId: string) => void;\n isSelfCreatedThreadId: (threadId: string) => boolean;\n forgetSelfCreatedThreadId: (threadId: string) => void;\n hydrate: (threadId?: string | null) => Promise<void>;\n ensureThread: (threadId: string, deferRootPump?: boolean) => ThreadStream;\n startDeferredRootPump: () => void;\n abandonDeferredRootPump: () => void;\n waitForRootPumpReady: () => Promise<void> | undefined;\n awaitNextTerminal: (signal: AbortSignal) => Promise<TerminalResult>;\n awaitResumedRunTerminal: (signal: AbortSignal) => Promise<TerminalResult>;\n onSubmitStart?: () => void;\n onRunStart?: () => void;\n onRunCreated?: (runId: string) => void;\n onRunStartReturned?: (runId: string | undefined) => void;\n onRunCompleted?: (reason: RunExecutionReason, runId?: string) => void;\n onRunEnd?: () => void;\n beginOptimistic?: (\n input: unknown\n ) => { dispatchInput: unknown; handle: OptimisticHandle } | undefined;\n settleOptimistic?: (\n handle: OptimisticHandle,\n event: TerminalResult[\"event\"]\n ) => void;\n }) {\n this.#options = params.options;\n this.#rootStore = params.rootStore;\n const onQueueError = (error: unknown) => {\n this.#rootStore.setState((state) => ({ ...state, error }));\n };\n // Custom transports don't get server-backed queueing at all: there's\n // nowhere on `AgentServerAdapter` to declare the capability. Chosen\n // once, here, for this coordinator's lifetime and never re-evaluated.\n const serverQueueEnabled =\n typeof params.options.transport !== \"object\" &&\n params.options.queue === \"server\";\n if (serverQueueEnabled) {\n assertServerQueueCapability(params.options.client.runs);\n }\n this.#queueAdapter = serverQueueEnabled\n ? new AgentServerQueueAdapter(\n params.options.client.runs,\n params.options.assistantId,\n params.queueStore,\n onQueueError,\n (threadId) => params.ensureThread(threadId, true) // deferRootPump: true, watching shouldn't force-start a root pump\n )\n : new LocalQueueAdapter(params.queueStore, onQueueError);\n this.#getDisposed = params.getDisposed;\n this.#getCurrentThreadId = params.getCurrentThreadId;\n this.#setCurrentThreadId = params.setCurrentThreadId;\n this.#rememberSelfCreatedThreadId = params.rememberSelfCreatedThreadId;\n this.#isSelfCreatedThreadId = params.isSelfCreatedThreadId;\n this.#forgetSelfCreatedThreadId = params.forgetSelfCreatedThreadId;\n this.#hydrate = params.hydrate;\n this.#ensureThread = params.ensureThread;\n this.#startDeferredRootPump = params.startDeferredRootPump;\n this.#abandonDeferredRootPump = params.abandonDeferredRootPump;\n this.#waitForRootPumpReady = params.waitForRootPumpReady;\n this.#awaitNextTerminal = params.awaitNextTerminal;\n this.#awaitResumedRunTerminal = params.awaitResumedRunTerminal;\n this.#onSubmitStart = params.onSubmitStart ?? (() => undefined);\n this.#onRunStart = params.onRunStart ?? (() => undefined);\n this.#onRunCreated = params.onRunCreated ?? (() => undefined);\n this.#onRunStartReturned = params.onRunStartReturned ?? (() => undefined);\n this.#onRunCompleted = params.onRunCompleted ?? (() => undefined);\n this.#onRunEnd = params.onRunEnd ?? (() => undefined);\n this.#beginOptimistic = params.beginOptimistic ?? (() => undefined);\n this.#settleOptimistic = params.settleOptimistic ?? (() => undefined);\n }\n\n /**\n * Submit input to the active thread.\n *\n * Honours {@link StreamSubmitOptions.multitaskStrategy}:\n *\n * - `\"rollback\"` (default): aborts any in-flight run and\n * dispatches immediately.\n * - `\"reject\"`: throws synchronously when a run is\n * already in flight.\n * - `\"enqueue\"`: defers via {@link QueueAdapter.enqueue};\n * resolves once accepted (durably, if server-backed), not on completion.\n * - `\"interrupt\"`: falls through to the default path\n *\n * Errors are routed through both the per-submit `onError` callback\n * and `rootStore.error`. Aborts (controller dispose / rollback) are\n * silently dropped.\n *\n * To resume a pending interrupt, use {@link StreamController.respond}\n * instead of `submit()`.\n *\n * @param input - Input payload for the run.\n * @param options - Per-submit options (config, metadata, callbacks,\n * strategy, etc).\n */\n async submit(\n input: unknown,\n options?: StreamSubmitOptions<StateType, ConfigurableType>\n ): Promise<void> {\n if (this.#getDisposed()) return;\n\n // Per-submit thread override: rebind first so the rest of the\n // submit operates against the new thread.\n const overrideThreadId = options?.threadId;\n if (\n overrideThreadId !== undefined &&\n overrideThreadId !== this.#getCurrentThreadId()\n ) {\n await this.#hydrate(overrideThreadId);\n }\n\n // Self-created thread id path: mint client-side so the controller\n // (and Suspense boundaries) get a stable id even before the run\n // is dispatched.\n const wasSelfCreated = this.#getCurrentThreadId() == null;\n if (wasSelfCreated) {\n const threadId = uuidv7();\n this.#setCurrentThreadId(threadId);\n this.#rememberSelfCreatedThreadId(threadId);\n this.#options.onThreadId?.(threadId);\n this.#rootStore.setState((s) => ({\n ...s,\n threadId,\n }));\n }\n\n const currentThreadId = this.#getCurrentThreadId();\n if (currentThreadId == null) return;\n // For threads that don't exist server-side yet (just minted here,\n // or an externally-minted id that hydrate marked missing via 404)\n // we defer the persistent root SSE pump until after `submitRun` /\n // `respondInput` commits the thread. Opening the pump's\n // `subscription.subscribe` against a not-yet-existent thread row\n // either 404s or — on langgraph_api's in-mem runtime — joins a\n // dead subscription that delivers 0 bytes until idle reconnect.\n // The deferred path starts the pump after dispatch returns (see\n // `#startDeferredRootPump` calls below).\n const pendingServerCreate =\n wasSelfCreated || this.#isSelfCreatedThreadId(currentThreadId);\n const thread = this.#ensureThread(currentThreadId, pendingServerCreate);\n const activeThreadId = currentThreadId;\n\n const strategy = options?.multitaskStrategy ?? \"rollback\";\n // `wasSelfCreated` short-circuit: when this submit just minted a\n // brand-new thread id (the user clicked \"New Thread\"), the\n // strategy check shouldn't see a run on the *previous* thread as\n // a reason to enqueue. The previous run is on a thread the user\n // navigated away from; abandoning its client-side abort tracking\n // is correct (the server-side run continues independently).\n // Without this, `enqueue` would trap the new submission and\n // `submitRun` never fires for the new thread — leaving a freshly-\n // minted thread id committed to the URL but never to the server.\n // True for a run this client dispatched (#runAbort) or one it only\n // observed via the persistent lifecycle listener (started elsewhere —\n // another tab, a webhook, Slack).\n const hasActiveRun =\n !wasSelfCreated &&\n ((this.#runAbort != null && !this.#runAbort.signal.aborted) ||\n this.#rootStore.getSnapshot().isLoading);\n if (hasActiveRun && strategy === \"reject\") {\n throw new Error(\n \"submit() rejected: a run is already in flight and multitaskStrategy is 'reject'.\"\n );\n }\n if (hasActiveRun && strategy === \"enqueue\") {\n try {\n await this.#queueAdapter.enqueue(\n currentThreadId,\n (input ?? undefined) as Partial<StateType> | null | undefined,\n // QueueAdapter is generic only over StateType, not ConfigurableType.\n options as StreamSubmitOptions<StateType> | undefined\n );\n } catch (error) {\n // Already routed to rootStore.error by the adapter; must not reject.\n try {\n options?.onError?.(error);\n } catch {\n /* caller-supplied callback errors must not crash the submit */\n }\n }\n return;\n }\n\n // Only once this submit will actually dispatch a command. Enqueue /\n // reject return above so they cannot reset the in-flight run's\n // interrupt replay barrier or drop events buffered for it.\n this.#onSubmitStart();\n\n // Rollback: abort the previous run before starting a new one.\n this.#runAbort?.abort();\n const abort = new AbortController();\n this.#runAbort = abort;\n\n // Claim the in-flight slot before awaiting the root pump so\n // concurrent `enqueue` submits in the same tick observe\n // `hasActiveRun` and land in {@link queueStore}.\n this.#rootStore.setState((s) => ({\n ...s,\n interrupts: [],\n interrupt: undefined,\n error: undefined,\n isLoading: true,\n }));\n\n // Declared before the try so the catch/finally can settle the\n // submit lifecycle (loading flag, abort slot, optimistic state)\n // even if optimistic preparation or the pump wait throws.\n let optimisticHandle: OptimisticHandle | undefined;\n let dispatchInput: unknown = input;\n let createdRunId: string | undefined;\n let pendingCompletionReason: RunExecutionReason | undefined;\n let completionNotified = false;\n let settleEvent: TerminalResult[\"event\"] | undefined;\n const notifyCompletion = (reason: RunExecutionReason): void => {\n if (completionNotified) return;\n if (createdRunId == null) {\n pendingCompletionReason = reason;\n return;\n }\n completionNotified = true;\n this.#onRunCompleted(reason, createdRunId);\n };\n const reportError = (error: unknown): void => {\n if (abort.signal.aborted) return;\n this.#rootStore.setState((s) => ({ ...s, error }));\n try {\n options?.onError?.(error);\n } catch {\n /* caller-supplied callback errors must not crash the submit */\n }\n };\n\n try {\n // Apply the input optimistically *before* the first await so the\n // user's message (and any merged state) paints without waiting for\n // the server round-trip. Kept as the first statement in the try so\n // the synchronous paint still precedes the first `await`, while a\n // synchronous coercion failure (e.g. a malformed message entry)\n // settles the submit lifecycle through the catch/finally below —\n // exactly like a dispatch failure — instead of wedging `isLoading`\n // / `#runAbort` and stranding later enqueue/reject submits behind a\n // phantom in-flight run. Runs only on the dispatched path — an\n // `\"enqueue\"`d submission returns above and echoes when it drains,\n // keeping one optimistic batch bound to exactly one run lifecycle.\n // `dispatchInput` carries the minted ids the server must echo for\n // reconciliation, so the run is dispatched with it (not raw input).\n const prepared = this.#beginOptimistic(input);\n if (prepared != null) {\n optimisticHandle = prepared.handle;\n dispatchInput = prepared.dispatchInput;\n }\n\n // Wait for the root subscription to be live; otherwise the\n // dispatch could resolve before we're listening for events and\n // we'd miss the terminal that ends the run.\n await this.#waitForRootPumpReady();\n\n const boundConfig = bindThreadConfig(options?.config, currentThreadId);\n // Subscribe to the next terminal *before* dispatching so a fast\n // run's terminal can't race us.\n const terminalPromise = this.#awaitNextTerminal(abort.signal);\n this.#onRunStart();\n\n let terminalSettled = false;\n let terminal: TerminalResult | undefined;\n\n const commandPromise = thread.submitRun({\n input: dispatchInput ?? null,\n config: boundConfig,\n metadata: (options?.metadata ?? undefined) as Record<string, unknown>,\n forkFrom: options?.forkFrom,\n multitaskStrategy:\n options?.multitaskStrategy === \"enqueue\"\n ? \"enqueue\"\n : options?.multitaskStrategy,\n });\n // Start the deferred root pump *after* the dispatch HTTP\n // response lands — that's when the thread row exists server-\n // side. Doing it synchronously here would race the response\n // and the pump's `subscription.subscribe` would 404. Same\n // reason we drop the self-created flag only after dispatch:\n // future hydrates need the thread to exist before they fetch\n // state.\n //\n // Fire-and-forget: we don't want to gate Promise.race on this,\n // and `commandPromise.catch` is already handled below. A\n // dispatch failure means there's no thread to pump anyway.\n void commandPromise.then(\n () => {\n this.#startDeferredRootPump();\n this.#forgetSelfCreatedThreadId(activeThreadId);\n },\n () => {\n // Dispatch failed. Without abandoning, `#rootPumpDeferred`\n // stays armed and `selfCreatedThreadIds` still holds this\n // id — a retry submit would see `pendingServerCreate=true`\n // again but `#ensureThread` would early-return because\n // `#thread != null`, and the root pump would never start.\n // Tear down so the next submit re-runs `#ensureThread`\n // from scratch. Keep the self-created mark so the retry\n // still defers the pump (the server row was never created).\n if (pendingServerCreate) {\n this.#abandonDeferredRootPump();\n }\n }\n );\n const notifyCreated = (result: { run_id?: unknown }) => {\n const runId =\n typeof result.run_id === \"string\" ? result.run_id : undefined;\n if (!abort.signal.aborted) this.#onRunStartReturned(runId);\n if (runId == null) return;\n createdRunId = runId;\n this.#onRunCreated(createdRunId);\n if (pendingCompletionReason != null) {\n notifyCompletion(pendingCompletionReason);\n }\n };\n const first = await Promise.race([\n terminalPromise.then((value) => ({\n type: \"terminal\" as const,\n value,\n })),\n commandPromise.then(\n (result) => ({ type: \"command\" as const, result }),\n (error) => ({ type: \"error\" as const, error })\n ),\n ]);\n if (first.type === \"error\") throw first.error;\n if (first.type === \"command\") {\n notifyCreated(first.result);\n } else {\n // Terminal landed first (very fast runs). Wait for the\n // dispatch response in the background so onCreated fires\n // and dispatch errors still surface.\n terminal = first.value;\n terminalSettled = true;\n void commandPromise.then(notifyCreated).catch((error) => {\n if (!terminalSettled) reportError(error);\n });\n }\n\n terminal ??= await terminalPromise;\n terminalSettled = true;\n settleEvent = terminal.event;\n if (terminal.event === \"failed\" && !abort.signal.aborted) {\n const runError = new Error(\n terminal.error ?? \"Run failed with no error message\"\n );\n this.#rootStore.setState((s) => ({ ...s, error: runError }));\n try {\n options?.onError?.(runError);\n } catch {\n /* caller-supplied callback errors must not crash the submit */\n }\n }\n notifyCompletion(terminalReason(terminal.event));\n } catch (error) {\n if (!abort.signal.aborted) settleEvent = \"failed\";\n reportError(error);\n } finally {\n // Always settle loading and clear our slot of the abort\n // controller. Schedule queue drain on the next macrotask so any\n // late state updates from this run finish flushing first.\n this.#rootStore.setState((s) => ({ ...s, isLoading: false }));\n if (this.#runAbort === abort) this.#runAbort = undefined;\n // Reconcile optimistic state: flip pending messages to sent/failed\n // and roll back un-echoed non-message keys. `aborted` covers a\n // rollback-resubmit or `stop()` cancelling this run.\n if (optimisticHandle != null) {\n this.#settleOptimistic(\n optimisticHandle,\n abort.signal.aborted ? \"aborted\" : (settleEvent ?? \"failed\")\n );\n }\n this.#onRunEnd();\n this.#scheduleQueueDrain();\n }\n }\n\n /**\n * Schedules a queue-drain attempt on the next macrotask, once the\n * just-finished run's state has flushed. A no-op unless the queue\n * adapter is idle-driven (see {@link QueueAdapter.onIdle}) and no\n * other run is active.\n */\n #scheduleQueueDrain(): void {\n setTimeout(() => {\n if (this.#getDisposed()) return;\n if (this.#runAbort != null && !this.#runAbort.signal.aborted) return;\n if (this.#rootStore.getSnapshot().isLoading) return;\n\n this.#queueAdapter.onIdle?.((values, drainOptions) =>\n this.submit(\n values,\n drainOptions as StreamSubmitOptions<StateType, ConfigurableType>\n ).catch(() => {\n /* submit() already routes errors through the per-submit onError\n * hook and the root store; swallow here so a failing drain does\n * not surface as an unhandled rejection. */\n })\n );\n }, 0);\n }\n\n /**\n * Surface a *resumed* run's failure the same way {@link submit} surfaces\n * a fresh run's failure — by writing it to the reactive\n * {@link RootSnapshot.error} slot.\n *\n * `respond()` / `respondAll()` dispatch their `input.respond` command on\n * the controller directly (they target a specific interrupt, so they\n * cannot go through {@link submit}, which only does `run.start`). The\n * resumed run therefore never passed through the submit lifecycle that\n * populates `rootStore.error` — only the persistent lifecycle listener\n * observed it, and that listener drives `isLoading` alone. Without this,\n * a resumed run that fails (e.g. a missing model key surfaced after the\n * user approves an interrupt) would flip `isLoading` back to `false`\n * with `error` left untouched, so `stream.error`-driven UIs (error\n * banners, API-key retry prompts) would silently miss it.\n *\n * The `dispatch` thunk is awaited, so a dispatch failure rejects the\n * caller's `respond()` *and* lands in `rootStore.error`. The resumed\n * run's terminal is watched in the **background** so the returned promise\n * still settles on dispatch — preserving the resume command's\n * resolve-on-dispatch contract (and avoiding a hang when no terminal is\n * ever emitted, e.g. in unit tests).\n *\n * Reuses the shared {@link #runAbort} slot, so `stop()`, `dispose()`, and\n * a rollback `submit()` all cancel the terminal watch (no spurious error\n * on user-initiated cancel) and treat the resumed run as the active run.\n *\n * The terminal watch uses {@link #awaitResumedRunTerminal}, which skips\n * stale `interrupted` terminals from the run being resumed (they can reach\n * the pump after `input.requested` but before `respondInput` calls\n * `#prepareForNextRun`) and only accepts a later `interrupted` once a\n * root `running` lifecycle for the resumed run has been observed.\n *\n * @param dispatch - Sends the `input.respond` command (and marks the\n * targeted interrupt resolved). Invoked after the terminal watch is\n * armed.\n * @param optimisticHandle - Optional handle from an optimistic `update`\n * applied before dispatch (HITL \"push card into state + resume\"). Settled\n * on the resumed run's terminal — pending messages flip to `sent` and\n * un-echoed non-message keys roll back on failure — exactly like the\n * `submit()` optimistic lifecycle. A dispatch failure settles it `failed`.\n */\n async dispatchResume(\n dispatch: () => Promise<void>,\n optimisticHandle?: OptimisticHandle\n ): Promise<void> {\n if (this.#getDisposed()) return;\n\n // Same allowlist clear as submit(): a resumed run can pause on a\n // *new* interrupt id. Without this, the hydrate-window allowlist\n // still contains only the interrupt being answered and\n // `#recordRootInterrupt` drops the follow-on `input.requested`.\n this.#onSubmitStart();\n\n // Rollback any run still tracked as active (mirrors submit()), then\n // claim the in-flight slot so stop()/dispose()/a concurrent submit\n // cancels the terminal watch armed below.\n this.#runAbort?.abort();\n const abort = new AbortController();\n this.#runAbort = abort;\n\n // Optimistically clear a stale error from a previous run, matching\n // submit()'s reset, so the resume starts from a clean error slot.\n this.#rootStore.setState((s) =>\n s.error === undefined ? s : { ...s, error: undefined }\n );\n\n const reportError = (error: unknown): void => {\n if (abort.signal.aborted) return;\n this.#rootStore.setState((s) => ({ ...s, error }));\n };\n\n // Settle the optimistic `update` exactly once, whether the resumed run\n // terminates (success/failure/interrupt) or the dispatch itself fails.\n let optimisticSettled = false;\n const settleOptimisticOnce = (event: TerminalResult[\"event\"]): void => {\n if (optimisticSettled || optimisticHandle == null) return;\n optimisticSettled = true;\n this.#settleOptimistic(optimisticHandle, event);\n };\n\n // Subscribe to the resumed run's terminal *before* dispatching so a fast\n // `failed` can't race us. Unlike `#awaitNextTerminal`, the resume watcher\n // ignores stale `interrupted` events until root `running` is seen.\n // Watched in the background — we never gate the returned promise on the\n // resumed run's terminal.\n const terminalPromise = this.#awaitResumedRunTerminal(abort.signal);\n void terminalPromise.then((terminal) => {\n if (this.#runAbort === abort) this.#runAbort = undefined;\n if (terminal.event === \"failed\" && !abort.signal.aborted) {\n reportError(\n new Error(terminal.error ?? \"Run failed with no error message\")\n );\n }\n settleOptimisticOnce(abort.signal.aborted ? \"aborted\" : terminal.event);\n // Drain any submission enqueued while the resumed run was active.\n this.#scheduleQueueDrain();\n });\n\n try {\n await dispatch();\n } catch (error) {\n // The `input.respond` send itself failed, before any run started.\n reportError(error);\n settleOptimisticOnce(\"failed\");\n if (this.#runAbort === abort) this.#runAbort = undefined;\n throw error;\n }\n }\n\n /**\n * Abort the current run (if any) and force `isLoading=false`.\n *\n * Client-side only — server-side cancel is handled by\n * {@link StreamController.stop} before this is invoked.\n */\n async stop(): Promise<void> {\n this.abortActiveRun();\n this.#rootStore.setState((s) => ({ ...s, isLoading: false }));\n }\n\n /**\n * Abort the current run without forcing the loading flag down.\n *\n * Used by {@link StreamController.dispose}: disposal already tears\n * down the root store, so flipping `isLoading` here is unnecessary\n * and would race the dispose path.\n */\n abortActiveRun(): void {\n this.#runAbort?.abort();\n this.#runAbort = undefined;\n }\n\n /**\n * Populate the queue from whatever the configured adapter considers\n * the source of truth. No-op for `LocalQueueAdapter`; for the\n * server-backed adapter this is what makes pending runs created\n * before this page load (or by another tab/session) show up.\n * Fire-and-forget from the caller's side: the adapter manages its\n * own staleness guard against a rapid, repeated call for a newer\n * thread.\n */\n async hydrateQueue(threadId: string): Promise<void> {\n await this.#queueAdapter.hydrate(threadId);\n }\n\n /**\n * Public entry point for {@link #scheduleQueueDrain}. Called by the\n * controller when `isLoading` settles to `false` for a run this\n * coordinator never dispatched itself.\n */\n scheduleQueueDrainOnObservedIdle(): void {\n this.#scheduleQueueDrain();\n }\n\n /**\n * Cancel a queued submission by id.\n *\n * @param id - Client-side queue entry id to remove.\n * @returns `true` when the entry was found and dropped, `false` otherwise.\n */\n async cancelQueued(id: string): Promise<boolean> {\n return this.#queueAdapter.cancel(id);\n }\n\n /**\n * Drop every queued submission. Cancels the underlying server-side\n * runs too when server-backed; see `AgentServerQueueAdapter.clear`.\n */\n async clearQueue(): Promise<void> {\n await this.#queueAdapter.clear();\n }\n\n /**\n * Release whatever the queue adapter is watching. Called by\n * {@link StreamController} on thread rebind and on dispose, alongside\n * its own reset of the (still directly-owned, reactive) queue store.\n */\n detachQueue(): void {\n this.#queueAdapter.detach();\n }\n}\n"],"mappings":"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"}