UNPKG

@langchain/langgraph-sdk

Version:

Client library for interacting with the LangGraph API

2,229 lines • 93.1 kB
require("../_virtual/_rolldown/runtime.cjs");
const require_constants = require("./constants.cjs");
const require_message_coercion = require("./message-coercion.cjs");
const require_tools = require("../client/stream/handles/tools.cjs");
const require_headless_tools = require("../headless-tools.cjs");
const require_store = require("./store.cjs");
const require_channel_registry = require("./channel-registry.cjs");
const require_hitl_interrupt_payload = require("../ui/hitl-interrupt-payload.cjs");
const require_interrupts = require("../ui/interrupts.cjs");
const require_namespace = require("./namespace.cjs");
const require_subagents = require("./discovery/subagents.cjs");
const require_subgraphs = require("./discovery/subgraphs.cjs");
require("./discovery/index.cjs");
const require_namespace_from_history = require("./discovery/namespace-from-history.cjs");
const require_message_metadata_tracker = require("./message-metadata-tracker.cjs");
const require_lifecycle_loading_tracker = require("./lifecycle-loading-tracker.cjs");
const require_root_message_projection = require("./root-message-projection.cjs");
const require_optimistic_input = require("./optimistic-input.cjs");
const require_queue_adapter = require("./queue-adapter.cjs");
const require_submit_coordinator = require("./submit-coordinator.cjs");
const require_tool_calls = require("./tool-calls.cjs");
let _langchain_core_utils_uuid = require("@langchain/core/utils/uuid");
require("@langchain/core/messages");
//#region src/stream/controller.ts
/**
* Framework-agnostic controller for the experimental v2 stream.
*
* Responsibilities:
*  - Owns at most one {@link ThreadStream} at a time (swapped on
*    `hydrate(newThreadId)` or `dispose`).
*  - Exposes three always-on observable surfaces via {@link StreamStore}:
*      - `rootStore`      : root values/messages/toolCalls/interrupts/…
*      - `subagentStore`  : discovery map of subagents (no content)
*      - `subgraphStore`  : discovery map of subgraphs  (no content)
*  - Owns a {@link ChannelRegistry} that framework selector hooks
*    (`useMessages`, `useToolCalls`, `useExtension`, `useChannel`)
*    use to lazily open per-namespace subscriptions.
*  - Imperative run surface: `submit`, `stop`, `respond`, `joinStream`.
*
* A single multi-channel subscription (`values`, `lifecycle`, `input`,
* `messages`, `tools`) powers every always-on projection and both
* discovery runners. Selector hooks add their own (deduped)
* subscriptions on top — so even a UI with many subagents only opens
* one extra subscription per `(channels, namespace)` actually
* rendered on screen.
*/
function isAbortLikeError(error) {
	if (error == null || typeof error !== "object") return false;
	const maybeError = error;
	return maybeError.name === "AbortError" || typeof maybeError.message === "string" && maybeError.message.includes("aborted");
}
function lifecycleReason(event) {
	if (event === "completed") return "success";
	if (event === "failed") return "error";
	if (event === "interrupted") return "interrupt";
	return null;
}
/**
* Decide whether a hydrated thread is *active* (a run is executing or
* paused awaiting resume) from the `getState()` snapshot alone — no
* extra request.
*
* Why this gate exists: a finished thread does not need either of the
* always-on SSE pumps. Subagent/subgraph cards are already seeded from
* the `getState()` messages and a single bounded `getHistory()` page, so
* opening the depth-1 content pump + the wildcard lifecycle watcher only
* to replay a completed run and then idle forever is pure waste. We open
* the pumps eagerly only when the thread is active; otherwise they come
* up on the first local `submit()` (the existing deferred-pump path) or
* a thread swap that lands on an active thread.
*
* The gate is deliberately conservative: we only conclude *idle* when
* the state proves it. A thread is treated as active unless `next` is a
* present, empty array AND no task carries a pending interrupt:
*  - `next` missing / not an array: unknown shape (a server or custom
*    client may omit it). Treat as active so an already-running
*    server-side run is still observed on reconnect — never silently
*    disable streaming on an unfamiliar `getState` shape.
*  - `next.length > 0`: the checkpoint still has nodes to execute, i.e.
*    a run is mid-flight or paused at an interrupt.
*  - `next` is `[]` but a `tasks[].interrupts` is non-empty: the thread
*    is interrupted and a resume (which starts a run) must be observable.
*  - `next` is `[]` and no pending interrupts: a completed run → idle.
*/
function isThreadStateActive(state) {
	if (state == null) return true;
	if (!Array.isArray(state.next)) return true;
	if (state.next.length > 0) return true;
	if (Array.isArray(state.tasks)) for (const task of state.tasks) {
		const interrupts = task?.interrupts;
		if (Array.isArray(interrupts) && interrupts.length > 0) return true;
	}
	return false;
}
const ROOT_NAMESPACE = [];
/**
* Channel set covered by the always-on root subscription. Exported so
* projections (and transports) can reason about what the root pump
* already delivers before opening additional server subscriptions.
*/
const ROOT_PUMP_CHANNELS = [
	"values",
	"checkpoints",
	"lifecycle",
	"input",
	"messages",
	"tools"
];
const PARKED_INTERRUPT_SETTLE_MS = 5e3;
const PARKED_INTERRUPT_SETTLE_POLL_MS = 500;
/**
* Coordinates one thread's protocol-v2 stream and exposes stable
* observable projections for framework bindings.
*
* The controller owns the root subscription, lazily binds scoped
* projections through {@link ChannelRegistry}, and normalizes protocol
* events into class-message, tool-call, discovery, interrupt, and queue
* stores.
*
* @typeParam StateType - Shape of the graph state exposed on `values`.
* @typeParam InterruptType - Shape of protocol interrupt payloads.
* @typeParam ConfigurableType - Shape of `config.configurable` accepted by submit.
*/
var StreamController = class {
	rootStore;
	subagentStore;
	subgraphStore;
	subgraphByNodeStore;
	messageMetadataStore;
	queueStore;
	registry;
	#options;
	#messagesKey;
	#subagents = new require_subagents.SubagentDiscovery();
	#subgraphs = new require_subgraphs.SubgraphDiscovery();
	#messageMetadata = new require_message_metadata_tracker.MessageMetadataTracker();
	#thread;
	#currentThreadId;
	#rootSubscription;
	#rootPump;
	#rootPumpReady;
	/**
	* `true` while a self-created thread has its root pump deferred until
	* the first `submitRun` / `respondInput` commits the thread row
	* server-side. See `#ensureThread` and `#startDeferredRootPump`.
	*/
	#rootPumpDeferred = false;
	#threadEventUnsubscribe;
	#threadErrorUnsubscribe;
	#disposed = false;
	#pendingDisposeTimer = null;
	/**
	* Interrupt ids this controller has locally marked resolved via
	* {@link respond} / {@link respondAll}. Used only to suppress
	* *historical* SSE replay of those `input.requested` events (there
	* is no `input.responded` protocol event). A live event after the
	* resume barrier clears an id from this set so an interrupt raised
	* again by a later run can reappear on `rootStore.interrupts`.
	*/
	#resolvedInterrupts = /* @__PURE__ */ new Set();
	/**
	* Set of interrupt IDs the server reports as currently *active* on
	* the thread (from `state.tasks[].interrupts`). Populated during
	* {@link hydrate} from `client.threads.getState()` and used as a
	* strict allowlist when processing replayed `input.requested`
	* events from the persistent SSE subscription. Without this guard,
	* SSE replay re-adds historically-requested interrupts that have
	* since been resolved (no `input.responded` event exists in the
	* protocol, so the SDK has no other way to tell replay from live
	* for an idle thread). After a command is accepted, events newer
	* than {@link #interruptReplayThroughSeq} bypass the allowlist.
	*/
	#hydratedActiveInterruptIds = null;
	/**
	* Creation time of the checkpoint the latest {@link hydrate} seeded
	* from. A new stream subscription replays the thread's retained
	* history, and events produced before this moment are already part of
	* the seed: applying them again rewinds `messages`, `values` and
	* `toolCalls` to older turns and then rebuilds them.
	*/
	#hydratedCheckpointTime;
	/**
	* Highest event sequence already applied when the latest run command
	* was accepted. Interrupts outside the hydrate-time allowlist remain
	* filtered through this sequence, while newer events are live output
	* from the newly-started run and must be accepted.
	*/
	#interruptReplayThroughSeq;
	/** Unknown inputs waiting to be classified as live or replayed history. */
	#pendingHydrationInterruptEvents = [];
	#settleParkedInterruptsPromise;
	/**
	* Unknown interrupt events received between local command dispatch and its
	* response. The response supplies the sequence barrier needed to classify
	* each event as historical replay or live output from the new run.
	*/
	#pendingInterruptEvents = [];
	#interruptCommandPending = false;
	/**
	* Monotonic counter bumped at the start of each `submit()` and used
	* by {@link hydrate} to skip its post-fetch allowlist write when a
	* submit started while the state fetch was in flight. Without this
	* guard, a submit-then-hydrate race could re-install a stale
	* allowlist that filters out genuinely-new live interrupts emitted
	* by the just-started run.
	*/
	#submitGeneration = 0;
	/**
	* Monotonic counter bumped at the start of each `#teardownThread`.
	* The root pump captures the generation when it starts and drops
	* any event whose generation no longer matches — including queued
	* events that `SubscriptionHandle.close()` still drains after the
	* snapshot has already been cleared by `hydrate()`.
	*/
	#rootPumpGeneration = 0;
	/**
	* Thread ids this controller minted client-side on first `submit()`.
	* `hydrate()` skips `threads.getState()` for these — we know there
	* is nothing checkpointed yet. Cleared once dispatch commits the
	* thread server-side.
	*/
	#selfCreatedThreadIds = /* @__PURE__ */ new Set();
	/**
	* Caller-supplied thread ids that 404'd on hydrate. Unlike
	* {@link #selfCreatedThreadIds}, these are revalidated on every
	* hydrate: another client may create the same id between visits.
	* While marked missing, `submit()` still defers the root SSE pump
	* until `run.start` / `commands` commits the row — joining
	* `/stream/events` beforehand leaves a dead subscription on
	* langgraph_api's in-mem runtime.
	*/
	#missingThreadIds = /* @__PURE__ */ new Set();
	/**
	* In-flight per-subagent namespace resolutions, keyed by tool-call
	* id. De-dupes concurrent {@link resolveSubagentNamespace} calls so
	* re-renders / multiple consumers of the same subagent don't issue
	* parallel `getHistory` walks.
	*/
	#namespaceResolves = /* @__PURE__ */ new Map();
	/**
	* In-flight hydrate-time discovery seed ({@link #seedDiscoveryFromHistory}):
	* a single bounded `getHistory` page that bulk-promotes every
	* still-default subagent namespace and seeds subgraph hosts. Per-card
	* {@link resolveSubagentNamespace} calls await this shared promise
	* instead of each firing their own `getHistory` walk, so opening N
	* cards right after reconnect costs one history read, not N. Re-armed
	* per hydrate cycle and cleared once it settles.
	*/
	#discoverySeedPromise;
	#scopedHistorySeeds = /* @__PURE__ */ new Map();
	#rootEventListeners = /* @__PURE__ */ new Set();
	#rootBus;
	#activeRunId;
	/**
	* Run id `run.start` returned for the in-flight local `submit()`. Kept
	* apart from {@link #activeRunId}, which a rolled-back submit's late
	* response can still overwrite.
	*/
	#submittedRunId;
	/** `true` from a local run's dispatch until its `run.start` answers. */
	#submittedRunIdPending = false;
	/** One per armed terminal wait: re-runs the lifecycle events it held until `run.start` answered. */
	#heldLifecycleReleases = /* @__PURE__ */ new Set();
	#localRunDepth = 0;
	/**
	* `true` once a root `values` event has been applied for the current
	* optimistic run. Reset to `false` in {@link #beginOptimistic} and
	* read in {@link #settleOptimistic}: when a run terminates without
	* the server ever echoing a `values` snapshot, optimistically-merged
	* non-message keys are rolled back to their pre-submit values.
	*/
	#sawValuesForRun = false;
	/**
	* Single-shot hydration promise. Exposed via `hydrationPromise`
	* so Suspense wrappers can throw it until the first hydrate
	* completes (resolve) or fails (reject). Reset whenever a new
	* hydrate cycle begins so `<Suspense>` boundaries re-suspend on
	* thread switch.
	*/
	#hydrationPromise;
	#resolveHydration;
	#rejectHydration;
	/**
	* Tool assembler lives for the lifetime of a thread; reset on
	* rebind so a fresh thread starts with a clean slate.
	*/
	#rootToolAssembler = new require_tools.ToolCallAssembler();
	#rootMessages;
	#lifecycleLoading;
	#submitter;
	#threadListeners = /* @__PURE__ */ new Set();
	/**
	* Create a controller around a LangGraph client and optional initial thread.
	*
	* @param options - Runtime configuration, client, thread id, and initial state.
	*/
	constructor(options) {
		this.#options = options;
		this.#messagesKey = options.messagesKey ?? "messages";
		this.#currentThreadId = options.threadId ?? null;
		this.#rootBus = {
			channels: ROOT_PUMP_CHANNELS,
			subscribe: (listener) => {
				this.#rootEventListeners.add(listener);
				return () => {
					this.#rootEventListeners.delete(listener);
				};
			},
			trySeedFromHistory: (params) => this.#trySeedProjectionFromHistory(params)
		};
		this.registry = new require_channel_registry.ChannelRegistry(this.#rootBus);
		this.subagentStore = this.#subagents.store;
		this.subgraphStore = this.#subgraphs.store;
		this.subgraphByNodeStore = this.#subgraphs.byNodeStore;
		this.rootStore = new require_store.StreamStore(this.#createInitialSnapshot());
		this.#rootMessages = new require_root_message_projection.RootMessageProjection({
			messagesKey: this.#messagesKey,
			store: this.rootStore
		});
		this.#lifecycleLoading = new require_lifecycle_loading_tracker.LifecycleLoadingTracker({
			store: this.rootStore,
			isDisposed: () => this.#disposed,
			onSettled: () => this.#submitter.scheduleQueueDrainOnObservedIdle(),
			ignore: (event) => this.#submittedRunMatch(event) != null
		});
		this.messageMetadataStore = this.#messageMetadata.store;
		this.queueStore = new require_store.StreamStore(require_queue_adapter.EMPTY_QUEUE);
		this.#submitter = new require_submit_coordinator.SubmitCoordinator({
			options: this.#options,
			rootStore: this.rootStore,
			queueStore: this.queueStore,
			getDisposed: () => this.#disposed,
			getCurrentThreadId: () => this.#currentThreadId,
			setCurrentThreadId: (threadId) => {
				this.#currentThreadId = threadId;
			},
			rememberSelfCreatedThreadId: (threadId) => {
				this.#selfCreatedThreadIds.add(threadId);
			},
			isSelfCreatedThreadId: (threadId) => this.#selfCreatedThreadIds.has(threadId) || this.#missingThreadIds.has(threadId),
			hydrate: (threadId) => this.hydrate(threadId),
			ensureThread: (threadId, deferRootPump) => this.#ensureThread(threadId, deferRootPump),
			startDeferredRootPump: () => this.#startDeferredRootPump(),
			abandonDeferredRootPump: () => this.#abandonDeferredRootPump(),
			forgetSelfCreatedThreadId: (threadId) => {
				this.#selfCreatedThreadIds.delete(threadId);
				this.#missingThreadIds.delete(threadId);
			},
			waitForRootPumpReady: () => this.#rootPumpReady,
			awaitNextTerminal: (signal) => this.#awaitNextTerminal(signal),
			awaitResumedRunTerminal: (signal) => this.#awaitResumedRunTerminal(signal),
			onSubmitStart: () => {
				this.#submitGeneration += 1;
				this.#interruptCommandPending = this.#hydratedActiveInterruptIds != null;
				this.#pendingInterruptEvents = this.#pendingHydrationInterruptEvents;
				this.#pendingHydrationInterruptEvents = [];
			},
			onRunStart: () => this.#markLocalRunStart(),
			onRunStartReturned: (runId) => {
				this.#submittedRunId = runId;
				this.#submittedRunIdPending = false;
				for (const release of [...this.#heldLifecycleReleases]) release();
			},
			onRunCreated: (runId) => {
				this.#advanceInterruptReplayBarrier();
				this.#notifyCreated(runId);
			},
			onRunCompleted: (reason, runId) => this.#notifyCompleted(reason, runId),
			onRunEnd: () => {
				this.#interruptCommandPending = false;
				this.#markLocalRunEnd();
			},
			beginOptimistic: (input) => this.#beginOptimistic(input),
			settleOptimistic: (handle, event) => this.#settleOptimistic(handle, event)
		});
		this.#hydrationPromise = this.#createHydrationPromise();
		/**
		* Attach a default no-op catch so orphaned hydrationPromise
		* rejections (e.g. controllers spawned during Suspense retries
		* whose promise never gets subscribed to because the suspense
		* cache already holds an earlier one) don't surface as unhandled
		* rejections. Callers that attach their own handlers via .then()
		* still receive the rejection on their derived promise.
		*/
		this.#hydrationPromise.catch(() => void 0);
		/**
		* Kick off the initial hydrate eagerly when a caller-supplied
		* thread id is present. Suspense consumers throw
		* `hydrationPromise` on the very first render, which unmounts the
		* subtree before any `useEffect` can run — so if we waited for an
		* effect to drive the hydrate we'd deadlock. Firing here makes
		* the promise settle independently of the component lifecycle.
		*/
		if (this.#currentThreadId != null) this.hydrate(this.#currentThreadId);
		else this.#resolveHydration();
	}
	/**
	* Promise that settles the first time {@link hydrate} finishes on
	* the current thread. Resolves on a clean hydration, rejects when
	* the thread-state fetch errors. A fresh promise is installed on
	* every thread swap so `<Suspense>` wrappers re-suspend on
	* `switchThread`.
	*/
	get hydrationPromise() {
		return this.#hydrationPromise;
	}
	/**
	* Create the deferred promise backing the current hydration cycle.
	*/
	#createHydrationPromise() {
		return new Promise((resolve, reject) => {
			this.#resolveHydration = resolve;
			this.#rejectHydration = reject;
		});
	}
	/**
	* Replace the current hydration promise before a thread swap.
	*/
	#resetHydrationPromise() {
		/**
		* Swallow rejection on the orphaned promise so Node doesn't
		* flag it as unhandled; Suspense callers that still hold a
		* reference see the rejection they subscribed to.
		*/
		this.#hydrationPromise.catch(() => void 0);
		this.#hydrationPromise = this.#createHydrationPromise();
	}
	/**
	* Load thread state for hydration, preferring the active custom
	* adapter's `getState()` when present.
	*/
	async #fetchHydrationState() {
		const threadId = this.#currentThreadId;
		if (threadId == null) return null;
		const transport = this.#options.transport;
		if (transport != null && typeof transport === "object" && typeof transport.getState === "function") return await transport.getState();
		return this.#options.client.threads.getState(threadId);
	}
	/**
	* Fetch the checkpointed thread state and seed the root snapshot.
	* Re-calling with a different `threadId` swaps the underlying
	* {@link ThreadStream}, rewires the registry to the new thread, and
	* resets assemblers.
	*
	* @param threadId - Optional replacement thread id; `null` clears the active thread.
	*/
	async hydrate(threadId) {
		if (this.#disposed) return;
		const target = threadId === void 0 ? this.#currentThreadId : threadId;
		const changed = target !== this.#currentThreadId;
		this.#currentThreadId = target ?? null;
		this.#discoverySeedPromise = void 0;
		this.#scopedHistorySeeds.clear();
		this.rootStore.setState((s) => ({
			...s,
			threadId: this.#currentThreadId
		}));
		if (changed) {
			/**
			* Swap to a new thread: re-arm the hydration promise so any
			* Suspense boundary remounted against the new id suspends again.
			*/
			this.#resetHydrationPromise();
			/**
			* Kick teardown so the synchronous abort (unbind, drop onEvent,
			* close the root subscription, reset assemblers) runs before we
			* touch the snapshot. Then reset immediately so the UI does not
			* wait on pump/close. An interrupted thread parks the root pump
			* on `waitForResume()`; awaiting teardown *before* this reset
			* left `threadId` null while `messages` still showed the previous
			* conversation (`useStream` fire-and-forgets `hydrate()`).
			*/
			const teardown = this.#teardownThread();
			this.rootStore.setState(() => ({
				...this.#createInitialSnapshot(),
				threadId: this.#currentThreadId
			}));
			/**
			* Drop queued submissions — they were targeted at the previous
			* thread so dispatching them against the new thread would be
			* surprising. Mirrors the legacy `StreamOrchestrator` behaviour.
			*/
			this.queueStore.setState(() => require_queue_adapter.EMPTY_QUEUE);
			await teardown;
		}
		if (this.#currentThreadId == null) {
			this.rootStore.setState((s) => ({
				...s,
				isThreadLoading: false
			}));
			this.#resolveHydration();
			return;
		}
		/**
		* Self-generated thread ids have nothing to fetch server-side yet
		* — the thread is created lazily by the first `run.start`. Calling
		* `threads.getState()` here would return a 404 and surface a
		* spurious error to the UI.
		*/
		if (this.#selfCreatedThreadIds.has(this.#currentThreadId)) {
			this.rootStore.setState((s) => ({
				...s,
				isThreadLoading: false
			}));
			this.#resolveHydration();
			return;
		}
		this.rootStore.setState((s) => ({
			...s,
			isThreadLoading: true
		}));
		const hydratedThreadId = this.#currentThreadId;
		let hydrationError;
		let threadExists = false;
		let threadMissing = false;
		let threadActive = true;
		let hasActiveInterrupts = false;
		try {
			const state = await this.#fetchHydrationState();
			if (this.#disposed || this.#currentThreadId !== hydratedThreadId) return;
			threadExists = state != null;
			threadActive = isThreadStateActive(state);
			const checkpointTime = Date.parse(state?.created_at ?? "");
			this.#hydratedCheckpointTime = Number.isNaN(checkpointTime) ? void 0 : checkpointTime;
			this.#missingThreadIds.delete(hydratedThreadId);
			if (state?.values != null) {
				/**
				* `threads.getState()` returns the legacy `ThreadState` shape
				* where `parent_checkpoint` is an object (`{ thread_id,
				* checkpoint_id, checkpoint_ns }`). Synthesize the v2
				* `Checkpoint` envelope (matching the `checkpoints` channel
				* payload) so hydrated messages also get their
				* `parentCheckpointId` populated for fork / edit flows.
				*/
				const checkpointId = state.checkpoint?.checkpoint_id;
				const parentCheckpointId = state.parent_checkpoint?.checkpoint_id ?? void 0;
				/**
				* Carry the checkpoint `step` from `getState()` metadata so the
				* root message projection treats this seed as the authoritative
				* latest superstep. The content pump's reconnect replay emits
				* older checkpoints (lower step); marking the seed's step lets
				* the projection reject those as stale instead of letting them
				* remove the seeded message tail (the final assistant turn).
				*/
				const seedStep = state.metadata?.step;
				const syntheticCheckpoint = typeof checkpointId === "string" ? {
					id: checkpointId,
					...parentCheckpointId != null ? { parent_id: parentCheckpointId } : {},
					...typeof seedStep === "number" ? { step: seedStep } : {}
				} : void 0;
				this.#applyValues(state.values, syntheticCheckpoint);
				/**
				* Seed subagent discovery from checkpoint messages so deep-agent
				* cards render on refresh without waiting for SSE replay. Zero
				* extra HTTP (reuses the `getState` payload); mirrors the
				* interrupt-seeding below. `#subagents` was cleared in
				* `#teardownThread`, and `seedFromCheckpointMessages` is
				* idempotent, so this is safe on re-hydrate.
				*/
				const seedMessages = state.values[this.#messagesKey];
				if (Array.isArray(seedMessages)) {
					this.#subagents.seedFromCheckpointMessages(seedMessages);
					const finishedToolCalls = require_tool_calls.seedToolCallsFromMessages(ROOT_NAMESPACE, require_message_coercion.ensureMessageInstances(seedMessages)).filter((toolCall) => toolCall.status === "finished");
					if (finishedToolCalls.length > 0) this.rootStore.setState((s) => ({
						...s,
						toolCalls: finishedToolCalls.reduce((toolCalls, toolCall) => require_tool_calls.upsertToolCall(toolCalls, toolCall), s.toolCalls)
					}));
					/**
					* An idle (finished) thread defers its root SSE pump; the first
					* `submit()` brings it up and the transport replays the finished
					* run from `seq=0`. Seal the seeded message ids so that replay's
					* `messages` channel deltas can't downgrade the already-complete
					* tail to empty partials (a visible "messages replay"). Only safe
					* for idle threads, where every seeded message is final — an
					* active thread's tail may still be streaming and must keep
					* receiving deltas. New ids from the next run are never sealed,
					* and the seal lifts once a newer checkpoint advances the
					* timeline.
					*/
					if (!threadActive) {
						const sealedIds = seedMessages.map((message) => message?.id).filter((id) => typeof id === "string");
						if (sealedIds.length > 0) this.#rootMessages.sealMessageIds(sealedIds);
					}
				}
			}
			/**
			* Converge to server truth: drop any optimistic messages the
			* server state does not contain (`pending` / `failed` that were
			* never persisted — e.g. a failed run's user message). Echoed
			* ids were flipped to `"sent"` by `#applyValues` above and so are
			* excluded from `unpersistedOptimisticIds()`.
			*/
			const unpersisted = this.#messageMetadata.unpersistedOptimisticIds();
			if (unpersisted.size > 0) {
				this.#rootMessages.dropMessages(unpersisted);
				this.#messageMetadata.forget(unpersisted);
			}
			/**
			* Sync the visible interrupt list to the server's authoritative
			* `state.tasks[].interrupts`. Without this, SSE replay of past
			* `input.requested` events would re-add resolved interrupts to
			* the UI on every page navigation back to the thread.
			*
			* Only runs when `state.tasks` is an array — runtimes that don't
			* surface tasks in `threads.getState()` are left untouched (an
			* unconditional overwrite would wipe any in-flight interrupt
			* state the wildcard watcher may already have recorded).
			*/
			if (Array.isArray(state?.tasks)) {
				const generationAtFetch = this.#submitGeneration;
				const { activeInterrupts, activeIds } = collectActiveInterruptsFromTasks(state.tasks);
				hasActiveInterrupts = activeInterrupts.length > 0;
				for (const id of activeIds) this.#resolvedInterrupts.delete(id);
				this.rootStore.setState((s) => ({
					...s,
					interrupts: activeInterrupts,
					interrupt: activeInterrupts[0]
				}));
				if (this.#submitGeneration === generationAtFetch) {
					this.#hydratedActiveInterruptIds = activeIds;
					this.#interruptReplayThroughSeq = void 0;
				}
			}
		} catch (error) {
			if (error?.status !== 404) {
				hydrationError = error;
				this.rootStore.setState((s) => ({
					...s,
					error
				}));
			} else threadMissing = true;
		} finally {
			this.rootStore.setState((s) => ({
				...s,
				isThreadLoading: false
			}));
			if (hydrationError != null) this.#rejectHydration(hydrationError);
			else this.#resolveHydration();
		}
		/**
		* Missing threads must not open `/stream/events`. The in-mem
		* runtime accepts the join against a not-yet-created id but the
		* subscription never attaches to the live channel — the first run
		* then delivers 0 bytes until idle reconnect. Park the pump until
		* `submit()`'s `run.start` creates the row (see
		* {@link #startDeferredRootPump}). Tracked in `#missingThreadIds`
		* (not `#selfCreatedThreadIds`) so a later hydrate revalidates —
		* another client may create the same id between visits.
		*/
		if (threadMissing) {
			this.#missingThreadIds.add(hydratedThreadId);
			return;
		}
		/**
		* Open the shared subscription on mount so in-flight server-side
		* runs are observed even when no local `submit()` is active — BUT
		* only when the thread is actually active (see
		* {@link isThreadStateActive}). A finished thread's cards are seeded
		* from `getState()` + the bounded `getHistory()` below, so opening
		* the depth-1 content pump just to replay a completed run and idle
		* forever is pure waste. When idle we take the deferred path: the
		* pump (and watcher) come up on the first local `submit()` via
		* {@link #startDeferredRootPump}, exactly like a self-created thread.
		* The transport replays from `seq=0` on the deferred subscribe, so
		* nothing is missed.
		*/
		const thread = this.#ensureThread(hydratedThreadId, !threadActive);
		/**
		* Picks up runs pending from before this page load, or another tab.
		* Fire-and-forget. Must run after `#ensureThread` above, not before:
		* `AgentServerQueueAdapter`'s own `getThread` callback also calls
		* `#ensureThread(threadId, true)`, which would otherwise create
		* `#thread` with the pump deferred before the line above decides it
		* should be eager.
		*/
		this.#submitter.hydrateQueue(hydratedThreadId);
		/**
		* Start the wildcard lifecycle watcher up-front for existing,
		* active threads. The root content pump runs at `depth: 1`, which
		* covers root-namespace and one-deep events but not arbitrarily-
		* nested subagent / subgraph lifecycle — the dedicated watcher
		* handles those.
		*
		* Skipped when:
		*  - the thread is idle/finished — there are no live events to
		*    watch; discovery is seeded from history below, and the watcher
		*    starts with the deferred pump on the first `submit()`.
		*  - the thread is self-created (new) — the watcher would 404
		*    against a not-yet-existent thread; `submitRun` / `respondInput`
		*    call `startLifecycleWatcher` on first submission instead.
		*/
		if (threadExists && threadActive) {
			thread.startLifecycleWatcher();
			/**
			* Seed `isLoading` from the checkpoint itself, not just a live
			* lifecycle event: a subscriber that joins after a foreign run's
			* own `running` event already fired would otherwise never observe
			* it. Excludes a thread merely parked at an interrupt — that's
			* waiting on a human, not "loading", matching
			* `LifecycleLoadingTracker`'s own `interrupted → isLoading = false`.
			*/
			if (threadActive && !hasActiveInterrupts) this.rootStore.setState((s) => s.isLoading ? s : {
				...s,
				isLoading: true
			});
		}
		if (threadExists) {
			/**
			* Seed subgraph discovery and promote subagent execution
			* namespaces from a single bounded `getHistory` page. Subgraph
			* structure is not present in the root checkpoint messages
			* (unlike subagents), so it can only be reconstructed from
			* history. Fire-and-forget — not awaited into the hydration
			* promise, so Suspense / first paint stay unblocked; cards fill
			* in progressively when it resolves.
			*
			* Held in `#discoverySeedPromise` so lazy per-card
			* {@link resolveSubagentNamespace} calls coalesce onto this single
			* read instead of each firing their own `getHistory` walk.
			*/
			const seed = this.#seedDiscoveryFromHistory(this.#currentThreadId).finally(() => {
				if (this.#discoverySeedPromise === seed) this.#discoverySeedPromise = void 0;
			});
			this.#discoverySeedPromise = seed;
		}
	}
	/**
	* One bounded, non-blocking `getHistory` read at hydrate that seeds
	* subgraph hosts and bulk-promotes still-default subagent execution
	* namespaces. O(1) in requests regardless of subagent/subgraph count.
	*/
	async #seedDiscoveryFromHistory(threadId) {
		try {
			const history = await require_namespace_from_history.getHistoryPage(this.#options.client, threadId, { limit: 20 });
			if (this.#disposed || this.#currentThreadId !== threadId) return;
			this.#primeScopedHistorySeedsFromHistory(threadId, history);
			const hosts = require_namespace_from_history.collectSubgraphHostNamespaces(history);
			this.#subgraphs.seedFromHistory(hosts);
			const defaultOnlyIds = [...this.#subagents.snapshot.values()].filter(namespaceIsDefaultOnly).map((entry) => entry.id);
			if (defaultOnlyIds.length > 0) {
				const map = require_namespace_from_history.mapSubagentNamespaces(history, defaultOnlyIds, this.#messagesKey);
				for (const [id, segment] of map) this.#subagents.applyExecutionNamespace(id, segment);
			}
		} catch {}
	}
	/**
	* Lazily resolve a single subagent's execution namespace from
	* checkpoint history. Intended call site: the first scoped
	* `useMessages` / `useToolCalls` mount for a subagent whose namespace
	* is still the default `tools:<toolCallId>`. A fallback for the
	* hydrate-time bulk seed ({@link #seedDiscoveryFromHistory}) — most
	* subagents are already promoted by the time a panel opens.
	*
	* Skips ids already promoted past default-only (SSE replay or a prior
	* resolve). Concurrent calls for the same id share one `getHistory`
	* walk via {@link #namespaceResolves}.
	*
	* @param toolCallId - Parent `task` tool-call id (the subagent's discovery key).
	*/
	async resolveSubagentNamespace(toolCallId) {
		if (this.#disposed) return;
		const threadId = this.#currentThreadId;
		if (threadId == null) return;
		if (!namespaceIsDefaultOnly(this.#subagents.snapshot.get(toolCallId))) return;
		const inflight = this.#namespaceResolves.get(toolCallId);
		if (inflight != null) return inflight;
		const run = (async () => {
			try {
				/**
				* Coalesce onto the hydrate-time discovery seed. That single
				* bounded `getHistory` page bulk-promotes every default-only
				* subagent, so when many cards mount at once (the common
				* reconnect case) they all await this one read instead of each
				* firing their own walk. Re-check after it settles: usually the
				* bulk seed already promoted us and no further fetch is needed.
				*/
				const seed = this.#discoverySeedPromise;
				if (seed != null) {
					await seed;
					if (this.#disposed || this.#currentThreadId !== threadId) return;
					if (!namespaceIsDefaultOnly(this.#subagents.snapshot.get(toolCallId))) return;
				}
				const map = await require_namespace_from_history.resolveSubagentNamespaces(this.#options.client, threadId, [toolCallId], { messagesKey: this.#messagesKey });
				if (this.#disposed || this.#currentThreadId !== threadId) return;
				const segment = map.get(toolCallId);
				if (segment != null) this.#subagents.applyExecutionNamespace(toolCallId, segment);
			} catch {} finally {
				this.#namespaceResolves.delete(toolCallId);
			}
		})();
		this.#namespaceResolves.set(toolCallId, run);
		return run;
	}
	/**
	* Try to satisfy a scoped selector projection from checkpoint history
	* instead of opening a scoped `/events` replay.
	*
	* This is only valid while the root pump is deferred, which means hydrate
	* has classified the thread as idle/stale. Active and interrupted threads
	* must keep using SSE so ongoing work and resumes are observed. For an idle
	* thread, though, a late-mounted subagent card only needs the latest scoped
	* checkpoint snapshot; opening `/events` just asks the server to replay work
	* that already finished and can be slow for namespaces discovered from
	* history.
	*
	* Returns `true` when the projection was handled without `/events`. That can
	* mean either the store was seeded from namespace-specific history, or the
	* projection targeted a default subagent namespace that should be skipped
	* because hydrate promoted it to its execution namespace. Returns `false`
	* when the caller should fall back to the normal subscription path.
	*/
	async #trySeedProjectionFromHistory(params) {
		const threadId = this.#currentThreadId;
		if (this.#disposed || threadId == null || params.namespace.length === 0 || !this.#rootPumpDeferred || this.#selfCreatedThreadIds.has(threadId) || this.#missingThreadIds.has(threadId)) return false;
		if (await this.#skipDefaultSubagentProjection(params.namespace, threadId)) return true;
		if (this.#disposed || this.#currentThreadId !== threadId || !this.#rootPumpDeferred) return false;
		const seed = await this.#getScopedHistorySeed(threadId, params.namespace);
		if (seed == null || this.#disposed || this.#currentThreadId !== threadId || !this.#rootPumpDeferred) return false;
		if (await this.#skipDefaultSubagentProjection(params.namespace, threadId)) return true;
		if (params.kind === "messages") {
			params.store.setValue(seed.messages);
			return true;
		}
		params.store.setValue(seed.toolCalls);
		return true;
	}
	/**
	* Suppress subscriptions for placeholder subagent namespaces once hydrate has
	* resolved the real execution namespace.
	*
	* Deep-agent discovery first creates cards at `tools:<toolCallId>`. The
	* actual worker history usually lives under a different checkpoint namespace
	* such as `tools:<uuid>`, and hydrate resolves that mapping from the bounded
	* root history seed. React/Vue/Svelte/Angular selector effects can mount
	* while that seed is still in flight, so this helper waits for it and then
	* returns `true` when the original placeholder namespace is stale. Returning
	* `true` tells the projection runtime not to open an `/events` subscription
	* for the wrong namespace; the framework will re-render with the promoted
	* card namespace and acquire the real projection.
	*/
	async #skipDefaultSubagentProjection(namespace, threadId) {
		const toolCallId = defaultSubagentToolCallId(namespace);
		if (toolCallId == null) return false;
		if (!namespaceIsDefaultOnly(this.#subagents.snapshot.get(toolCallId))) return false;
		const seed = this.#discoverySeedPromise;
		if (seed != null) await seed;
		if (this.#disposed || this.#currentThreadId !== threadId) return true;
		return !namespaceIsDefaultOnly(this.#subagents.snapshot.get(toolCallId));
	}
	/**
	* Load and cache the latest checkpoint snapshot for one scoped namespace.
	*
	* `useMessages(stream, subagent)` and `useToolCalls(stream, subagent)` often
	* mount together. Both need the same namespace-specific history page, so the
	* controller keeps an in-flight promise per `threadId + checkpoint_ns`.
	* The cache may already be primed by the hydrate-time discovery history page;
	* otherwise this method performs a narrow `checkpoint_ns` read and derives
	* both projection snapshots from that one response:
	*
	* - `messages` are coerced with the stream-local message coercion rules, so
	*   serialized `content_blocks` and tool-call metadata hydrate correctly.
	* - `toolCalls` are reconstructed from AI tool calls plus matching
	*   ToolMessages, enough for finished/stale card panels without replaying
	*   the `tools` channel.
	*
	* Returns `null` when history does not contain usable values, or the request
	* fails. Callers treat that as a signal to fall back to `/events` so custom
	* servers or unusual state shapes still work.
	*/
	#getScopedHistorySeed(threadId, namespace) {
		const checkpointNs = require_namespace.namespaceKey(namespace);
		const key = `${threadId}|${checkpointNs}`;
		const existing = this.#scopedHistorySeeds.get(key);
		if (existing != null) return existing;
		const seed = (async () => {
			try {
				const values = (await require_namespace_from_history.getHistoryPage(this.#options.client, threadId, {
					limit: 1,
					checkpoint: { checkpoint_ns: checkpointNs }
				}))[0]?.values;
				if (values == null || typeof values !== "object") return null;
				const messages = extractAndCoerceMessagesWithFallback(values, this.#messagesKey);
				if (messages == null) return null;
				return {
					messages,
					toolCalls: require_tool_calls.seedToolCallsFromMessages(namespace, messages)
				};
			} catch {
				return null;
			}
		})();
		this.#scopedHistorySeeds.set(key, seed);
		return seed;
	}
	/**
	* Reuse the hydrate-time discovery history page as scoped projection data
	* when it already contains checkpoint values for a namespace.
	*
	* The discovery read is required to resolve subagent execution namespaces and
	* subgraph hosts. That same page often includes the latest values for those
	* namespaces, so priming `#scopedHistorySeeds` here lets later
	* `useMessages(stream, subagent)` / `useToolCalls(stream, subagent)` mounts
	* hydrate from memory instead of issuing an immediate second `getHistory`
	* request. If a namespace is not present in the bounded page,
	* `#getScopedHistorySeed` still falls back to a targeted `checkpoint_ns`
	* history read.
	*/
	#primeScopedHistorySeedsFromHistory(threadId, history) {
		for (const state of history) {
			const checkpointNs = state.checkpoint?.checkpoint_ns;
			if (typeof checkpointNs !== "string" || checkpointNs.length === 0) continue;
			const namespace = checkpointNs.split("\0").filter((segment) => segment.length > 0);
			if (namespace.length === 0) continue;
			const key = `${threadId}|${require_namespace.namespaceKey(namespace)}`;
			if (this.#scopedHistorySeeds.has(key)) continue;
			const values = state.values;
			if (values == null || typeof values !== "object") continue;
			const messages = extractAndCoerceMessagesWithFallback(values, this.#messagesKey);
			if (messages == null) continue;
			this.#scopedHistorySeeds.set(key, Promise.resolve({
				messages,
				toolCalls: require_tool_calls.seedToolCallsFromMessages(namespace, messages)
			}));
		}
	}
	/**
	* Submit input to the active thread.
	*
	* To resume a pending interrupt, use {@link respond} instead.
	*
	* @param input - Input payload for a new run.
	* @param options - Per-run config, metadata, multitask behavior, and callbacks.
	*/
	async submit(input, options) {
		await this.#submitter.submit(input, options);
	}
	/**
	* Disconnect the client from the active run and mark the controller
	* idle. By default also cancels the run server-side; pass
	* `{ cancel: false }` or call {@link disconnect} to keep the agent
	* running (join/rejoin).
	*/
	async stop(options) {
		if (options?.cancel ?? true) {
			const threadId = this.#currentThreadId;
			const runId = this.#activeRunId;
			if (runId != null && this.rootStore.getSnapshot().isLoading) {
				const dropped = this.#rootMessages.closeRun({
					sync: true,
					unfinishedOnly: true
				});
				if (dropped.size > 0) this.#messageMetadata.forget(dropped);
			}
			if (threadId != null && runId != null) try {
				await this.#options.client.runs.cancel(threadId, runId);
			} catch {}
		}
		await this.#submitter.stop();
	}
	/**
	* Disconnect the client without cancelling the run server-side.
	* Alias for `stop({ cancel: false })`.
	*/
	async disconnect() {
		return this.stop({ cancel: false });
	}
	#markLocalRunStart() {
		this.#localRunDepth += 1;
		this.#submittedRunId = void 0;
		this.#submittedRunIdPending = true;
	}
	#markLocalRunEnd() {
		this.#localRunDepth = Math.max(0, this.#localRunDepth - 1);
		if (this.#localRunDepth === 0) this.#submittedRunIdPending = false;
	}
	#notifyCreated(runId) {
		this.#activeRunId = runId;
		try {
			this.#options.onCreated?.({ runId });
		} catch {}
	}
	#notifyCompleted(reason, runId = this.#activeRunId) {
		if (runId != null && runId === this.#activeRunId) this.#activeRunId = void 0;
		setTimeout(() => {
			if (this.#disposed) return;
			try {
				this.#options.onCompleted?.(runId == null ? { reason } : {
					runId,
					reason
				});
			} catch {}
		}, 0);
	}
	#runLifecycleListener = (event) => {
		if (this.#localRunDepth > 0) return;
		if (event.method !== "lifecycle") return;
		if (!require_namespace.isRootNamespace(event.params.namespace)) return;
		if (!this.rootStore.getSnapshot().isLoading) return;
		const lifecycle = event.params.data;
		const reason = lifecycleReason(lifecycle?.event);
		if (reason == null) return;
		this.#notifyCompleted(reason);
	};
	/**
	* Cancel a queued submission by id. Returns `true` when the entry
	* was found and removed, `false` otherwise.
	*
	* Removes the client-side entry either way; also issues a server-side
	* cancel when the configured queue adapter is server-backed (see
	* `AgentServerAdapter.serverQueue`). A no-op for the default,
	* client-only adapter.
	*
	* @param id - Client-side queue entry id to remove.
	*/
	async cancelQueued(id) {
		return this.#submitter.cancelQueued(id);
	}
	/**
	* Drop every queued submission. Cancels the underlying server-side
	* runs too when the configured queue adapter is server-backed.
	*/
	async clearQueue() {
		await this.#submitter.clearQueue();
	}
	/**
	* Respond to a single pending protocol interrupt.
	*
	* When `options.interruptId` is omitted, resolution walks
	* {@link ThreadStream.interrupts `thread.interrupts`} from newest to
	* oldest and picks the first entry whose `interruptId` has not already
	* been resolved by a prior `respond()` call. That entry may be at the
	* root (`namespace: []`) or inside a subgraph (non-empty `namespace`).
	* {@link RootSnapshot.interrupts `rootStore.interrupts`} /
	* framework `stream.interrupts` mirrors the same pending interrupts
	* (including nested namespaces) for UI rendering.
	*
	* Omitting `interruptId` is fine when exactly one interrupt is pending.
	* When several can be active (parallel subagents, fan-out, nested
	* graphs), pass an explicit `interruptId` so you resume the interrupt
	* the user acted on. `namespace` is resolved automatically from
	* `thread.interrupts` / `stream.interrupts` when omitted.
	*
	* To resume several interrupts pending at the same checkpoint in one
	* command, use {@link respondAll} — sequential single `respond()` calls
	* would not work, since the first resume starts a run, leaving the
	* others with no interrupted run to respond to.
	*
	* The server validates `namespace` against the pending interrupt. Root
	* interrupts use `namespace: []`. Subgraph interrupts carry the exact
	* tuple on each {@link Interrupt} / {@link InterruptPayload} entry.
	*
	* @param response - Payload sent back to the interrupted namespace.
	* @param options - Optional target (`interruptId` / `namespace`) and
	*   run-level `config` / `metadata` folded into the run that services
	*   the resume (model/user config, trigger source, test flags, …).
	*   Equivalent to the same fields on {@link StreamSubmitOptions}.
	*
	* @example Single pending interrupt (safe to omit a target)
	* ```ts
	* await controller.respond({ approved: true });
	* ```
	*
	* @example Carry run config / metadata onto the resume
	* ```ts
	* await controller.respond(
	*   { approved: true },
	*   { config: { configurable: { model: "gpt-4o" } }, metadata: { source: "ui" } },
	* );
	* ```
	*
	* @example Multiple / nested interrupts — target by id
	* ```tsx
	* for (const intr of stream.interrupts) {
	*   await stream.respond(decide(intr.value), { interruptId: intr.id! });
	* }
	* ```
	*
	* Each {@link InterruptPayload} on `thread.interrupts` and each
	* {@link Interrupt} on `stream.interrupts` mirrors an
	* `input.requested` event with its protocol `namespace`.
	*/
	async respond(response, options) {
		if (this.#disposed || this.#thread == null) throw new Error("No active thread to respond to.");
		const resolved = options?.interruptId != null ? {
			interruptId: options.interruptId,
			namespace: this.#resolveNamespaceForInterruptId(options.interruptId, options.namespace)
		} : this.#resolveInterruptForResume();
		if (resolved == null) throw new Error("No pending interrupt to respond to.");
		const thread = this.#thread;
		const prepared = options?.update != null ? this.#beginOptimistic(options.update) : void 0;
		const dispatchUpdate = this.#resolveDispatchUpdate(options?.update, prepared);
		try {
			await this.#submitter.dispatchResume(async () => {
				await thread.respondInput({
					namespace: resolved.namespace,
					interrupt_id: resolved.interruptId,
					response: require_hitl_interrupt_payload.normalizeHitlResponseForServer(response),
					...dispatchUpdate != null ? { update: dispatchUpdate } : {},
					...options?.goto != null ? { goto: options.goto } : {},
					config: options?.config,
					metadata: options?.metadata
				});
				this.#advanceInterruptReplayBarrier();
				this.#markInterruptResolvedInRootStore(resolved.interruptId);
			}, prepared?.handle);
		} catch (error) {
			this.#discardPendingInterruptEvents();
			if (this.#disposed && isAbortLikeError(error)) return;
			throw error;
		}
	}
	/**
	* Resume several pending interrupts at the same checkpoint in a single
	* command.
	*
	* Required when a run pauses on multiple interrupts simultaneously
	* (e.g. parallel tool-authorization prompts): a single
	* `Command({ resume })` carrying every interrupt's payload resumes them
	* together. Sequential {@link respond} calls would fail because the
	* first resume starts a run, leaving the rest with no interrupted run to
	* respond to.
	*
	* `responsesById` maps each pending `interruptId` to the payload sent
	* back to it, so different interrupts can receive different responses
	* (approve one, deny another). To send the *same* payload to several
	* interrupts, build the map with that value for each id, e.g.
	* `Object.fromEntries(ids.map((id) => [id, response]))`.
	*
	* The server resumes by `interruptId`, so namespaces are resolved
	* internally from `getThread()?.interrupts` and need not be supplied.
	*
	* @param responsesById - Map of pending `interruptId` to its response
	*   payload. Must contain at least one entry.
	* @param options - Optional run-level `config` / `metadata` folded into
	*   the single run that services the batched resume. Equivalent to the
	*   same fields on {@link StreamSubmitOptions}.
	*
	* @example Distinct payloads per interrupt
	* ```tsx
	* await stream.respondAll({
	*   [interruptA.id]: { approved: true },
	*   [interruptB.id]: { approved: false },
	* });
	* ```
	*
	* @example Same payload to every pending interrupt
	* ```tsx
	* await stream.respondAll(
	*   Object.fromEntries(stream.interrupts.map((i) => [i.id!, { approved: true }])),
	* );
	* ```
	*/
	async respondAll(responsesById, options) {
		if (this.#disposed || this.#thread == null) throw new Error("No active thread to respond to.");
		const entries = Object.entries(responsesById);
		if (entries.length === 0) throw new Error("respondAll() requires at least one response.");
		const thread = this.#thread;
		const responses = entries.map(([interruptId, response]) => ({
			interrupt_id: interruptId,
			response: require_hitl_interrupt_payload.normalizeHitlResponseForServer(response),
			namespace: this.#resolveNamespaceForInterruptId(interruptId)
		}));
		const prepared = options?.update != null ? this.#beginOptimistic(options.update) : void 0;
		const dispatchUpdate = this.#resolveDispatchUpdate(options?.update, prepared);
		try {
			await this.#submitter.dispatchResume(async () => {
				await thread.respondInput({
					responses,
					...dispatchUpdate != null ? { update: dispatchUpdate } : {},
					...options?.goto != null ? { goto: options.goto } : {},
					config: options?.config,
					metadata: options?.metadata
				});
				this.#advanceInterruptReplayBarrier();
				for (const { interrupt_id: interruptId } of responses) this.#markInterruptResolvedInRootStore(interruptId);
			}, prepared?.handle);
		} catch (error) {
			this.#discardPendingInterruptEvents();
			if (this.#disposed && isAbortLikeError(error)) return;
			throw error;
		}
	}
	/**
	* Dispose the active thread, subscriptions, registry entries, and listeners.
	*/
	async dispose() {
		if (this.#disposed) return;
		this.#cancelPendingDispose();
		this.#disposed = true;
		this.#submitter.abortActiveRun();
		await this.#teardownThread();
		await this.registry.dispose();
		this.#threadListeners.clear();
	}
	/**
	* StrictMode-safe lifecycle hook for framework bindings.
	*
	* React 18+ `StrictMode` intentionally mounts → unmounts → remounts
	* components in dev to surface effect-cleanup bugs. A naive
	* `useEffect(() => () => controller.dispose())` would permanently
	* tear the controller down on that first synthetic unmount, leaving
	* every subsequent `submit()` a silent no-op.
	*
	* Call {@link activate} from the bind site's effect and return the
	* result as the effect's cleanup. The controller uses deferred
	* disposal: a `release()` only schedules a dispose on the next
	* microtask, which is cancelled if another `activate()` arrives
	* before it fires (the normal StrictMode remount path).
	*/
	activate() {
		this.#cancelPendingDispose();
		return () => {
			if (this.#disposed) return;
			this.#pendingDisposeTimer = setTimeout(() => {
				this.#pendingDisposeTimer = null;
				this.dispose().catch(() => void 0);
			}, 0);
		};
	}
	/**
	* Cancel a deferred dispose scheduled by {@link activate}.
	*/
	#cancelPendingDispose() {
		if (this.#pendingDisposeTimer != null) {
			clearTimeout(this.#pendingDisposeTimer);
			this.#pendingDisposeTimer = null;
		}
	}
	/**
	* Returns the bound {@link ThreadStream}, if one exists. Prefer
	* {@link StreamController.rootStore} and selector projections for
	* UI work; use this for low-level protocol access.
	*/
	getThread() {
		return this.#thread;
	}
	/**
	* Listen for `ThreadStream` lifecycle (swap on thread-id change,
	* detach on dispose). The listener fires immediately with the
	* current thread (may be `undefined`).
	*
	* @param listener - Callback invoked immediately and on every thread swap.
	*/
	subscribeThread(listener) {
		this.#threadListeners.add(listener);
		listener(this.#thread);
		return () => {
			this.#threadListeners.delete(listener);
		};
	}
	/**
	* Build the initial root snapshot from configured initial values.
	*/
	#createInitialSnapshot() {
		const values = this.#options.initialValues ?? {};
		return {
			values,
			messages: extractAndCoerceMessages(values, this.#messagesKey),
			toolCalls: [],
			interrupts: [],
			interrupt: void 0,
			isLoading: false,
			isThreadLoading: this.#currentThreadId != null && !this.#selfCreatedThreadIds.has(this.#currentThreadId),
			error: void 0,
			threadId: this.#currentThreadId
		};
	}
	/**
	* Return the active thread stream, creating and binding one when needed.
	*
	* @param threadId - Thread id used when constructing the stream.
	* @param deferRootPump - When `true`, build the ThreadStream and bind
	*   the registry but skip starting the persistent root SSE pump. Used
	*   for client-self-created thread ids whose server-side thread row
	*   doesn't exist yet — opening the pump's `subscription.subscribe`
	*   against a not-yet-existent thread produces a `404: Thread not
	*   found` protocol error that strands terminal lifecycle events and
	*   leaves the UI showing nothing until the user reloads. The pump is
	*   started later via {@link #startDeferredRootPump} after `submitRun`
	*   / `respondInput` commits the thread server-side.
	*
	*   Note: PR 2381's `#runStartReady` gate covers the analogous race
	*   for the in-flight `run.start` send, but only when that send is
	*   already pending. `#ensureThread` runs *before* `submitRun` is
	*   called (and thus before the gate is armed), so on transports
	*   that subscribe synchronously (WebSocket) the deferred path is
	*   still required.
	*/
	#ensureThread(threadId, deferRootPump = false) {
		if (this.#thread != null) {
			if (!deferRootPump && !this.#rootPumpDeferred && this.#rootPump == null) this.#startRootPump(this.#thread);
			return this.#thread;
		}
		this.#thread = this.#options.client.threads.stream(threadId, {
			assistantId: this.#options.assistantId,
			transport: this.#options.transport,
			fetch: this.#options.fetch,
			webSocketFactory: this.#options.webSocketFactory,
			maxReconnectAttempts: this.#options.maxReconnectAttempts,
			streamIdleReconnect: this.#options.streamIdleReconnect,
			reconnectDelayMs: this.#options.reconnectDelayMs,
			onReconnect: this.#options.onReconnect,
			onConnected: this.#options.onConnected
		});
		this.registry.bind(this.#thread);
		if (deferRootPump) {
			this.#rootPumpReady = Promise.resolve();
			this.#rootPumpDeferred = true;
		} else this.#startRootPump(this.#thread);
		this.#notifyThreadListeners();
		return this.#thread;
	}
	/**
	* Start the previously-deferred root SSE pump after the first
	* `submitRun` / `respondInput` has committed the thread server-side.
	*
	* No-op when the pump was started eagerly in {@link #ensureThread}
	* (i.e. for hydrated existing threads, or for any thread whose pump
	* has already been brought up).
	*/
	#startDeferredRootPump() {
		if (!this.#rootPumpDeferred) return;
		if (this.#thread == null) return;
		this.#rootPumpDeferred = false;
		this.#startRootPump(this.#thread);
	}
	/**
	* Abandon a deferred root pump that never started because its
	* triggering dispatch (`submitRun` / `respondInput`) failed.
	*
	* Without this, the controller would be wedged in a state where:
	*   - `#thread` is wired but no content pump is open
	*   - `#rootPumpDeferred` stays `true`
	*
	* A retry submit must re-run `#ensureThread` from scratch (with
	* `deferRootPump` still true — the id stays in
	* `#selfCreatedThreadIds` until a successful dispatch). Tearing
	* down `#thread` is the simplest recovery; the failed dispatch
	* means there was nothing to subscribe to anyway.
	*/
	#abandonDeferredRootPump() {
		if (!this.#rootPumpDeferred) return;
		this.#rootPumpDeferred = false;
		this.#teardownThread();
	}
	/**
	* Close the current thread stream and reset per-thread assembly state.
	*/
	async #teardownThread() {
		/**
		* Invalidate the in-flight root pump before `close()` unparks it.
		* `SubscriptionHandle.close()` leaves queued events intact and the
		* iterator drains them before observing `closed`, so without this
		* bump a parked interrupted pump would still dispatch stale
		* `values` / `messages` into a snapshot `hydrate()` already cleared.
		*/
		this.#rootPumpGeneration += 1;
		const thread = this.#thread;
		this.#thread = void 0;
		this.registry.bind(void 0);
		this.#threadEventUnsubscribe?.();
		this.#threadEventUnsubscribe = void 0;
		this.#threadErrorUnsubscribe?.();
		this.#threadErrorUnsubscribe = void 0;
		/**
		* Persistent lifecycle driver is scoped to the current thread
		* stream. Remove it so a swap to a new thread starts with a clean
		* listener set (a new one is installed in `#startRootPump`).
		*/
		this.#rootEventListeners.delete(this.#lifecycleLoading.listener);
		this.#rootEventListeners.delete(this.#runLifecycleListener);
		/**
		* Close first so a pump parked on `waitForResume()` (interrupted
		* run) unparks even if the unsubscribe RPC is slow. `close()` is
		* idempotent; `unsubscribe()` is then a no-op once closed.
		*/
		const subscription = this.#rootSubscription;
		this.#rootSubscription = void 0;
		subscription?.close();
		this.#rootPumpReady = void 0;
		this.#rootPumpDeferred = false;
		this.#rootMessages.reset();
		this.#rootToolAssembler = new require_tools.ToolCallAssembler();
		this.#lifecycleLoading.reset();
		this.#subagents.reset();
		this.#subgraphs.reset();
		this.#scopedHistorySeeds.clear();
		this.#activeRunId = void 0;
		this.#localRunDepth = 0;
		this.#messageMetadata.reset();
		this.#hydratedActiveInterruptIds = null;
		this.#hydratedCheckpointTime = void 0;
		this.#interruptReplayThroughSeq = void 0;
		this.#pendingHydrationInterruptEvents = [];
		this.#discardPendingInterruptEvents();
		this.queueStore.setState(() => require_queue_adapter.EMPTY_QUEUE);
		this.#submitter.detachQueue();
		try {
			await subscription?.unsubscribe();
		} catch {}
		try {
			await this.#rootPump;
		} catch {}
		this.#rootPump = void 0;
		if (thread != null) {
			try {
				await thread.close();
			} catch {}
			this.#notifyThreadListeners();
		}
	}
	/**
	* Determine whether the configured transport uses the resumable event-stream path.
	*/
	#usesEventStreamTransport() {
		const transport = this.#options.transport;
		if (transport === "websocket") return false;
		if (transport == null || transport === "sse") return true;
		return typeof transport.openEventStream === "function";
	}
	/**
	* Start the always-on root subscription pump for the provided thread.
	*
	* @param thread - Thread stream to subscribe to and fan out from.
	*/
	#startRootPump(thread) {
		if (this.#rootPump != null) return;
		const pumpGeneration = this.#rootPumpGeneration;
		let resolveReady;
		this.#rootPumpReady = new Promise((resolve) => {
			resolveReady = resolve;
		});
		/**
		* Wildcard discovery + interrupt tracking is delivered via the
		* thread's dedicated lifecycle watcher (see `ThreadStream.onEvent`).
		* This callback fires once per globally-unique event across both
		* the content pump AND the watcher, so we can drive discovery
		* runners and nested HITL capture without widening the content
		* pump's narrow filter.
		*/
		this.#threadEventUnsubscribe = thread.onEvent((event) => this.#onWildcardEvent(event));
		this.#threadErrorUnsubscribe = thread.onError((error) => {
			this.rootStore.setState((s) => ({
				...s,
				error,
				isLoading: false
			}));
			this.#rootSubscription?.close();
		});
		/**
		* Persistent isLoading driver. Drives `isLoading` from
		* root-namespace lifecycle events so that in-flight runs observed
		* via `hydrate()` (not initiated by a local `submit()`) still flip
		* the UI to loading. `running` → true; terminals → false. The
		* optimistic `isLoading = true` inside `submit()` stays because
		* that fires before any subscription event arrives.
		*/
		this.#rootEventListeners.add(this.#lifecycleLoading.listener);
		this.#rootEventListeners.add(this.#runLifecycleListener);
		const pump = (async () => {
			try {
				/**
				* Root content pump: depth 1 is required because the controller
				* classifies tool events at namespace length ≤ 1 as root-level
				* (see `#onWildcardEvent`'s `isRootLevelTool` check). The deep-
				* agent `task` dispatcher fires `tools.tool-started` at
				* `["tools:<id>"]` (length 1), so a depth-0 filter would drop
				* those events server-side before they reached `root.toolCalls`.
				*
				* Deeper content (subagent message tokens, values snapshots) is
				* pulled in on demand by per-namespace selector projections
				* (e.g. `useMessages(sub)`).
				*/
				const subscriptionPromise = thread.subscribe({
					channels: [...ROOT_PUMP_CHANNELS],
					namespaces: [[]],
					depth: 1
				});
				if (this.#usesEventStreamTransport())
 /**
				* SSE streams can legitimately withhold response headers until
				* the first event is available. Waiting for `subscribe()` here
				* would deadlock new-thread submits: the run is not dispatched
				* until the root pump is "ready", but the pump does not become
				* ready until the run emits. `thread.subscribe()` has already
				* registered the local subscription and scheduled the stream
				* rotation by this point; waiting one microtask lets the fetch
				* get kicked off without requiring headers to arrive.
				*/
				queueMicrotask(() => {
					resolveReady?.();
					resolveReady = void 0;
				});
				const subscription = await subscriptionPromise;
				if (pumpGeneration !== this.#rootPumpGeneration) {
					resolveReady?.();
					resolveReady = void 0;
					try {
						subscription.close();
					} catch {}
					return;
				}
				resolveReady?.();
				resolveReady = void 0;
				this.#rootSubscription = subscription;
				/**
				* The SSE transport pauses the underlying subscription when
				* a terminal root lifecycle event arrives (so `for await`
				* loops observing a single run exit cleanly) and re-opens
				* the next run's server stream on `#prepareForNextRun`,
				* resuming the subscription handle. The root pump needs to
				* survive that hand-off: we re-enter the inner `for await`
				* for every resumed iteration until the subscription is
				* permanently closed or the controller is disposed.
				*/
				while (!this.#disposed && pumpGeneration === this.#rootPumpGeneration) {
					for await (const event of subscription) {
						if (this.#disposed || pumpGeneration !== this.#rootPumpGeneration) break;
						/**
						* Resilience: isolate per-event dispatch from the pump loop.
						*
						* `#onRootEvent` runs synchronously and, transitively,
						* invokes every root-bus listener (selector projections that
						* opted into the shared stream) plus every `rootStore`
						* subscriber. Some of those subscribers live in a React
						* render tree — `useStream` drives
						* `useSyncExternalStore`, so a misbehaving component can
						* surface a render-phase error ("Maximum update depth
						* exceeded", "The result of getSnapshot should be cached",
						* etc.) that propagates out here.
						*
						* Without this guard, a single throw bubbles through the
						* `for await` loop and terminates the root pump permanently.
						* That is catastrophic: no more root events get processed —
						* the terminal `lifecycle: completed` never lands, so
						* `#awaitNextTerminal` never resolves, `isLoading` stays
						* `true`, composers stay disabled, and the final assistant
						* turn never commits to `stream.messages`. The UI looks
						* hung even though the server is still emitting events
						* (and `ThreadStream.onEvent` keeps firing).
						*
						* We therefore swallow the error and keep pumping. The
						* the pump's correctness guarantees do not depend on any
						* consumer behaving well.
						*/
						try {
							this.#onRootEvent(event);
						} catch {}
					}
					if (this.#disposed || pumpGeneration !== this.#rootPumpGeneration) break;
					if (!subscription.isPaused) {
						const snapshot = this.rootStore.getSnapshot();
						if (snapshot.isLoading && snapshot.error == null) this.rootStore.setState((s) => ({
							...s,
							error: /* @__PURE__ */ new Error("Thread event stream closed before the active run completed."),
							isLoading: false
						}));
						break;
					}
					await subscription.waitForResume();
				}
			} catch {
				resolveReady?.();
				resolveReady = void 0;
			}
		})();
		this.#rootPump = pump;
		pump.finally(() => {
			if (pumpGeneration !== this.#rootPumpGeneration) return;
			if (this.#rootPump !== pump) return;
			this.#rootPumpGeneration += 1;
			this.#rootSubscription = void 0;
			this.#rootPump = void 0;
			this.#rootPumpReady = void 0;
			this.#threadEventUnsubscribe?.();
			this.#threadEventUnsubscribe = void 0;
			this.#threadErrorUnsubscribe?.();
			this.#threadErrorUnsubscribe = void 0;
			this.#rootEventListeners.delete(this.#lifecycleLoading.listener);
			this.#rootEventListeners.delete(this.#runLifecycleListener);
		});
	}
	/**
	* Handle an event delivered via {@link ThreadStream.onEvent}.
	*
	* `onEvent` fires once per globally-unique event across the content
	* pump and the wildcard lifecycle watcher, so this is the single
	* entry point for wildcard discovery / interrupt tracking. It does
	* NOT fan events out to the root bus (that's driven by the content
	* pump iterator so root-bus short-circuits stay depth-1 scoped) and
	* it does NOT process root content — messages/tools/values at root
	* are handled by `#onRootEvent` off the content pump.
	*
	* @param event - Raw protocol event observed by the thread-wide listener.
	*/
	#onWildcardEvent(event) {
		try {
			this.#subagents.push(event);
		} catch {}
		this.#subgraphs.push(event);
		this.#lifecycleLoading.handle(event);
		/**
		* `input.requested` events (including HITL inside a subagent /
		* subgraph) are not always observable via the narrow content pump.
		* Mirror every namespace into `rootStore.interrupts` here so UI
		* state does not depend on the content pump being the first
		* consumer. `ThreadStream.interrupts` is updated in parallel for
		* resume targeting.
		*/
		this.#recordInterrupt(event);
		this.#settleParkedInterruptsOnTerminal(event);
	}
	/**
	* Process one root-pump event and update all root projections.
	*
	* @param event - Event yielded by the root subscription.
	*/
	#onRootEvent(event) {
		try {
			this.#subagents.push(event);
		} catch {}
		/**
		* Fan root-pump events out to every root-bus listener (selector
		* projections that opted into the shared stream,
		* `#awaitTerminal`, etc.). The root bus mirrors the content
		* pump's narrow scope (depth 1 at root) so projections that
		* short-circuit via the bus stay bounded.
		*/
		if (this.#rootEventListeners.size > 0) for (const listener of this.#rootEventListeners) try {
			listener(event);
		} catch {}
		const floor = this.#hydratedCheckpointTime;
		if (floor != null && event.params.timestamp < floor) return;
		/**
		* `messages` and `tools` events are emitted under a node's
		* namespace — for a typical StateGraph the LLM's token deltas
		* land on `["model:<uuid>"]`, tool executions on
		* `["tools:<uuid>"]`, etc. The orchestrator's own turns (root
		* agent, or an orchestrator-scoped subgraph like `model:*` /
		* `model_request:*`) belong in `root.messages` and
		* `root.toolCalls`.
		*
		* Subagent / tool-internal branches do NOT:
		*   - `task:*` segment — legacy subagent convention.
		*   - `tools:*` segment — every tool execution is wrapped in a
		*     `tools` subgraph. For simple tools its only content is
		*     the eventual tool result (also echoed verbatim by
		*     `values.messages` so we don't lose anything). For the
		*     deep-agent `task` tool its content IS the spawned
		*     subagent's full message + tool stream, which is surfaced
		*     separately via `useMessages(stream, subagent)` /
		*     `useToolCalls(stream, subagent)`.
		*
		* We therefore drop `messages` events from any namespace that
		* contains a `task:*` or `tools:*` segment; the authoritative
		* tool-result text lands in `root.messages` via the root
		* `values.messages` snapshot merge in `#applyValues`.
		*/
		const isInternalNamespace = require_namespace.isInternalWorkNamespace(event.params.namespace);
		const hasLegacySubagentNamespace = require_namespace.isLegacySubagentNamespace(event.params.namespace);
		if (event.method === "messages") {
			if (!isInternalNamespace) this.#rootMessages.handleMessage(event);
			return;
		}
		if (event.method === "tools") {
			if (event.params.namespace.length <= 1 && !hasLegacySubagentNamespace) {
				/**
				* Record the `namespace → tool_call_id` association so that
				* the ensuing `message-start` (role: "tool") at the same
				* namespace can recover the `tool_call_id` (the `messages`
				* channel's start event doesn't carry it directly).
				*/
				const toolData = event.params.data;
				if (toolData.event === "tool-started" && typeof toolData.tool_call_id === "string") this.#rootMessages.recordToolCallNamespace(event.params.namespace, toolData.tool_call_id);
				const tc = this.#rootToolAssembler.consume(event);
				if (tc != null) this.rootStore.setState((s) => ({
					...s,
					toolCalls: require_tool_calls.upsertToolCall(s.toolCalls, tc)
				}));
			}
			return;
		}
		/**
		* The `checkpoints` channel carries the lightweight envelope
		* (`id`, `parent_id`, `step`, `source`) emitted immediately
		* before its companion `values` event on the same superstep.
		* Buffer the envelope per-namespace so the ensuing `values`
		* event at the same namespace can pair with it in `#applyValues`.
		* The buffer is read-and-cleared on consumption so a subsequent
		* `values` event without a new checkpoint doesn't reuse stale
		* metadata.
		*/
		if (event.method === "checkpoints") {
			const data = event.params.data;
			this.#messageMetadata.bufferCheckpoint(event.params.namespace, data);
			return;
		}
		if (!require_namespace.isRootNamespace(event.params.namespace)) return;
		if (event.method === "values") {
			const valuesEvent = event;
			const bufferedCheckpoint = this.#messageMetadata.consumeCheckpoint(event.params.namespace);
			this.#applyValues(valuesEvent.params.data, bufferedCheckpoint);
			return;
		}
		if (event.method === "input.requested") {
			this.#recordInterrupt(event);
			return;
		}
		if (event.method === "lifecycle") {
			/**
			* Root lifecycle transitions are observed elsewhere
			* (#awaitTerminal) to unblock `submit`. Here they drop a closed
			* run's never-checkpointed messages.
			*/
			const lifecycle = event.params.data;
			const dropped = this.#rootMessages.applyLifecycle(lifecycle?.event);
			if (dropped.size > 0) this.#messageMetadata.forget(dropped);
		}
	}
	/**
	* How a lifecycle event relates to the run a local `submit()` started.
	* New subscriptions replay earlier runs' terminals, and accepting one
	* would end the submitted run early: `"other"` marks those, `"pending"`
	* an event that names a run before `run.start` has answered. `undefined`
	* when it can't be told apart (no submit in flight, no run named, or no
	* run id returned).
	*/
	#submittedRunMatch(event) {
		if (event.method !== "lifecycle" || this.#localRunDepth === 0) return;
		const runId = synthesizedEventRunId(event.event_id);
		if (runId == null) return void 0;
		if (this.#submittedRunIdPending) return "pending";
		if (this.#submittedRunId == null) return void 0;
		return runId === this.#submittedRunId ? void 0 : "other";
	}
	/**
	* Merge a `values` payload into root values and root messages.
	*
	* @param raw - Raw `values` channel payload.
	* @param checkpoint - Optional checkpoint envelope paired with the values event.
	*/
	/**
	* Apply a submit input optimistically to the root projection before
	* the server responds. Mints stable ids for id-less messages (so the
	* server echo reconciles by id), appends them to the projection, and
	* shallow-merges non-message input keys into `values`.
	*
	* Returns the dispatch payload (id-injected) for the coordinator to
	* send, plus an {@link OptimisticHandle} for terminal reconciliation.
	* Returns `undefined` when optimistic UI is disabled or there is
	* nothing to echo, in which case the coordinator dispatches the raw
	* input unchanged.
	*
	* @param input - Raw input passed to `submit()`.
	*/
	#beginOptimistic(input) {
		if (this.#options.optimistic === false) return void 0;
		if (input == null || typeof input !== "object" || Array.isArray(input)) return;
		const prepared = require_optimistic_input.prepareOptimisticInput(input, this.#messagesKey, () => (0, _langchain_core_utils_uuid.v7)());
		const extraKeys = Object.keys(prepared.extraValues);
		if (prepared.echoedIds.length === 0 && extraKeys.length === 0) return;
		const currentValues = this.rootStore.getSnapshot().values;
		const restoreKeys = extraKeys.map((key) => ({
			key,
			hadKey: Object.prototype.hasOwnProperty.call(currentValues, key),
			prevValue: currentValues[key]
		}));
		this.#sawValuesForRun = false;
		this.#rootMessages.appendOptimistic(prepared.optimisticMessages, prepared.extraValues, { sync: true });
		if (prepared.echoedIds.length > 0) this.#messageMetadata.markPending(prepared.echoedIds);
		return {
			dispatchInput: prepared.dispatchInput,
			handle: {
				echoedIds: prepared.echoedIds,
				restoreKeys
			}
		};
	}
	/**
	* Pick the `update` payload to dispatch on a resume (`respond` /
	* `respondAll`).
	*
	* When the optimistic path ran ({@link #beginOptimistic} returned a handle),
	* its `dispatchInput` already carries the minted message ids the server must
	* echo back, so dispatch that — the echo reconciles the optimistic messages
	* by id (no duplicate). Otherwise (optimistic UI disabled, or an `update`
	* with no echoable messages — e.g. the tuple-entry form) fall back to
	* serializing `BaseMessage` instances to dicts, exactly as before. Returns
	* `undefined` when there is no `update`, so the server still sees a plain
	* resume.
	*/
	#resolveDispatchUpdate(update, prepared) {
		if (prepared != null) return prepared.dispatchInput;
		if (update == null) return void 0;
		return require_optimistic_input.serializeUpdateMessages(update, this.#messagesKey);
	}
	/**
	* Reconcile optimistic state when a run terminates.
	*
	*   - Messages: any echoed id still `"pending"` (never echoed by the
	*     server) is flipped to `"sent"` on success/interrupt, or
	*     `"failed"` on failure/abort. Ids the server already echoed were
	*     flipped to `"sent"` in {@link #applyValues} and are untouched.
	*   - Non-message keys: rolled back to their pre-submit values when no
	*     server `values` event landed during the run (otherwise the
	*     server snapshot already reconciled them). Skipped on abort,
	*     where a superseding run (or `stop()`) owns subsequent state.
	*
	* @param handle - Handle returned by {@link #beginOptimistic}.
	* @param event  - Terminal lifecycle event for the run.
	*/
	#settleOptimistic(handle, event) {
		const failed = event === "failed" || event === "aborted";
		this.#messageMetadata.resolvePending(handle.echoedIds, failed ? "failed" : "sent");
		if (event !== "aborted" && !this.#sawValuesForRun) this.#rootMessages.restoreValueKeys(handle.restoreKeys);
	}
	#applyValues(raw, checkpoint) {
		if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return;
		const state = raw;
		this.#sawValuesForRun = true;
		/**
		* Surface parent_checkpoint per-message when the values event
		* carries the lightweight checkpoint envelope (populated by
		* `@langchain/langgraph-core`'s `_emitValuesWithCheckpointMeta` and
		* forwarded through `convertToProtocolEvent`). Consumers surface
		* this as `useMessageMetadata(stream, msg.id).parentCheckpointId`
		* for fork / edit flows.
		*/
		const parentCheckpointId = checkpoint?.parent_id;
		if (parentCheckpointId != null && Array.isArray(state[this.#messagesKey])) this.#messageMetadata.recordMessages(state[this.#messagesKey], { parentCheckpointId });
		const maybeMessages = state[this.#messagesKey];
		let nextValues;
		let nextMessages;
		if (Array.isArray(maybeMessages)) {
			const coerced = require_message_coercion.ensureMessageInstances(maybeMessages);
			nextValues = {
				...state,
				[this.#messagesKey]: coerced
			};
			nextMessages = coerced;
		} else {
			nextValues = state;
			nextMessages = [];
		}
		this.#rootMessages.applyValues(nextValues, nextMessages, { step: checkpoint?.step });
		if (nextMessages.length > 0) {
			this.#messageMetadata.resolvePending(nextMessages.map((m) => m.id).filter((id) => typeof id === "string"), "sent");
			this.rootStore.setState((s) => {
				const toolCalls = require_tool_calls.reconcileToolCallsFromMessages(s.toolCalls, nextMessages);
				if (toolCalls === s.toolCalls) return s;
				return {
					...s,
					toolCalls
				};
			});
		}
	}
	/**
	* Mirror protocol interrupts (any namespace) into the root snapshot.
	*
	* This can be called from both the wildcard lifecycle/input watcher and the
	* root content pump. Store-level dedup keeps the user-facing list stable.
	*/
	#recordInterrupt(event) {
		if (event.method !== "input.requested") return;
		const data = event.params.data;
		const interruptId = data?.interrupt_id;
		if (typeof interruptId !== "string") return;
		const eventSeq = typeof event.seq === "number" ? event.seq : void 0;
		if (this.#resolvedInterrupts.has(interruptId)) {
			if (!(this.#hydratedActiveInterruptIds == null || typeof this.#interruptReplayThroughSeq === "number" && eventSeq != null && eventSeq > this.#interruptReplayThroughSeq)) return;
			this.#resolvedInterrupts.delete(interruptId);
		}
		const isUnknownInterrupt = this.#hydratedActiveInterruptIds != null && !this.#hydratedActiveInterruptIds.has(interruptId);
		if (isUnknownInterrupt && this.#interruptCommandPending) {
			this.#pendingInterruptEvents.push(event);
			return;
		}
		const barrier = this.#interruptReplayThroughSeq;
		if (isUnknownInterrupt && barrier == null) {
			this.#pendingHydrationInterruptEvents.push(event);
			return;
		}
		if (isUnknownInterrupt && barrier != null && eventSeq != null && eventSeq <= barrier) return;
		const namespace = Array.isArray(event.params.namespace) ? [...event.params.namespace] : [...ROOT_NAMESPACE];
		const interrupt = require_interrupts.normalizeInterruptForClient({
			id: interruptId,
			value: data.payload,
			namespace
		});
		this.rootStore.setState((s) => {
			if (s.interrupts.some((entry) => entry.id === interruptId)) return s;
			const interrupts = [...s.interrupts, interrupt];
			return {
				...s,
				interrupts,
				interrupt: interrupts[0]
			};
		});
	}
	/**
	* Convert the hydrate-time allowlist into a replay barrier after a command
	* is accepted. Built-in transports update `lastAppliedThroughSeq` from the
	* command response before resolving `submitRun` / `respondInput`.
	*/
	#advanceInterruptReplayBarrier() {
		if (this.#hydratedActiveInterruptIds == null) {
			this.#discardPendingInterruptEvents();
			return;
		}
		const throughSeq = this.#thread?.ordering?.lastAppliedThroughSeq;
		if (typeof throughSeq === "number") this.#interruptReplayThroughSeq = throughSeq;
		else {
			this.#hydratedActiveInterruptIds = null;
			this.#interruptReplayThroughSeq = void 0;
		}
		this.#interruptCommandPending = false;
		const pendingEvents = this.#pendingInterruptEvents;
		this.#pendingInterruptEvents = [];
		for (const event of pendingEvents) this.#recordInterrupt(event);
	}
	#discardPendingInterruptEvents() {
		this.#interruptCommandPending = false;
		this.#pendingInterruptEvents = [];
	}
	/**
	* Resolve the protocol namespace for a targeted resume.
	*
	* Prefer an explicit caller-supplied namespace, then the matching
	* {@link ThreadStream.interrupts} entry, then the UI mirror on
	* `rootStore.interrupts`, finally root (`[]`).
	*/
	#resolveNamespaceForInterruptId(interruptId, explicitNamespace) {
		if (explicitNamespace != null) return [...explicitNamespace];
		const fromThread = this.#thread?.interrupts.find((entry) => entry.interruptId === interruptId)?.namespace;
		if (fromThread != null) return [...fromThread];
		const fromRoot = this.rootStore.getSnapshot().interrupts.find((entry) => entry.id === interruptId)?.namespace;
		if (fromRoot != null) return [...fromRoot];
		return [...ROOT_NAMESPACE];
	}
	/**
	* Mark an interrupt resolved for historical-replay filtering and
	* mirror the removal into the root snapshot the framework hooks read.
	*
	* Tombstones are not permanent: a live `input.requested` after the
	* resume barrier clears the id when the same interrupt is raised again.
	*/
	#markInterruptResolvedInRootStore(interruptId) {
		this.#resolvedInterrupts.add(interruptId);
		this.rootStore.setState((s) => {
			const interrupts = s.interrupts.filter((entry) => entry.id !== interruptId);
			if (interrupts.length === s.interrupts.length && s.interrupt?.id !== interruptId) return s;
			return {
				...s,
				interrupts,
				interrupt: interrupts[0]
			};
		});
	}
	/**
	* Re-sync `rootStore.interrupts` from the server's authoritative
	* `state.tasks[].interrupts` after a resumed run settles.
	*
	* Sequential `respond()` of parallel interrupts optimistically clears
	* each targeted id client-side. The server checkpoint can continue to
	* list the original interrupt batch after accepting a partial response,
	* so locally-resolved ids remain filtered while untouched siblings are
	* restored. This keeps `stream.interrupts` truthful without re-exposing
	* an already-consumed interrupt.
	*
	* Uses the same transport-aware state fetch as {@link hydrate}, and
	* drops the result if the thread swapped or a new submit/respond
	* started while the fetch was in flight.
	*/
	async #reconcilePendingInterruptsFromServer() {
		const threadId = this.#currentThreadId;
		if (threadId == null || this.#disposed) return null;
		const generationAtFetch = this.#submitGeneration;
		try {
			const state = await this.#fetchHydrationState();
			if (this.#disposed || this.#currentThreadId !== threadId || this.#submitGeneration !== generationAtFetch) return null;
			if (!Array.isArray(state?.tasks)) return null;
			const { activeInterrupts, activeIds } = collectActiveInterruptsFromTasks(state.tasks);
			const pendingInterrupts = activeInterrupts.filter((interrupt) => interrupt.id == null || !this.#resolvedInterrupts.has(interrupt.id));
			this.rootStore.setState((s) => ({
				...s,
				interrupts: pendingInterrupts,
				interrupt: pendingInterrupts[0]
			}));
			this.#hydratedActiveInterruptIds = activeIds;
			return state;
		} catch {
			return null;
		}
	}
	#settleParkedInterruptsOnTerminal(event) {
		if (this.#pendingHydrationInterruptEvents.length === 0) return;
		if (event.method !== "lifecycle") return;
		if (!require_namespace.isRootNamespace(event.params.namespace)) return;
		const status = event.params.data?.event;
		if (status !== "interrupted" && status !== "completed" && status !== "failed") return;
		this.#settleParkedInterrupts();
	}
	/**
	* Decide parked interrupts with server state instead of a stream marker.
	*
	* After a passive rejoin the stream cannot tell a replayed, already
	* resolved `input.requested` from one the current run just raised: the
	* SSE replays the thread buffer on connect with no marker for where the
	* replay ends. `state.tasks[].interrupts` is authoritative: parked ids it
	* lists are live, the rest are history.
	*/
	async #settleParkedInterrupts() {
		const makePromise = async () => {
			try {
				const threadId = this.#currentThreadId;
				const generation = this.#submitGeneration;
				const deadline = Date.now() + PARKED_INTERRUPT_SETTLE_MS;
				for (;;) {
					if (this.#disposed || this.#currentThreadId !== threadId || this.#submitGeneration !== generation || this.#pendingHydrationInterruptEvents.length === 0) return;
					const parkedIds = new Set(this.#pendingHydrationInterruptEvents.map((event) => event.params.data.interrupt_id));
					const state = await this.#reconcilePendingInterruptsFromServer();
					if (state != null) {
						if ([...this.#hydratedActiveInterruptIds ?? []].some((id) => parkedIds.has(id)) || !isThreadStateActive(state)) {
							this.#pendingHydrationInterruptEvents = [];
							return;
						}
					}
					if (Date.now() >= deadline) return;
					await new Promise((resolve) => {
						setTimeout(resolve, PARKED_INTERRUPT_SETTLE_POLL_MS);
					});
				}
			} finally {
				this.#settleParkedInterruptsPromise = void 0;
			}
		};
		this.#settleParkedInterruptsPromise ??= makePromise();
		return this.#settleParkedInterruptsPromise;
	}
	/**
	* Resolve on the next root-namespace terminal lifecycle event
	* (`completed` / `failed` / `interrupted`) or on abort.
	*
	* Attaches to the controller's root event bus instead of opening
	* a second server subscription. Callers should register the
	* returned promise **before** dispatching the command that
	* triggers the run (`thread.run.start` / `thread.input.respond`)
	* — the root pump fans events out synchronously on arrival, so a
	* late registration would miss the terminal for fast runs.
	*
	* @param signal - Abort signal for the local submit lifecycle.
	*/
	#awaitNextTerminal(signal) {
		return this.#awaitRootTerminal(signal, { skipInterruptedUntilRunning: false });
	}
	/**
	* Resolve on the resumed run's root terminal lifecycle.
	*
	* Unlike {@link #awaitNextTerminal}, ignores `interrupted` events until a
	* root `running` lifecycle has been observed. Headless-tool flows can emit
	* a stale `interrupted` for the run being resumed after `input.requested`
	* but before `respondInput` calls `#prepareForNextRun`; accepting that
	* terminal would unsubscribe the watcher before the resumed run's `failed`
	* terminal arrives.
	*
	* On `interrupted` / `completed`, reconciles pending interrupts from
	* server state so optimistic clears from {@link respond} cannot leave
	* `stream.interrupts` empty while the checkpoint still has siblings.
	*/
	async #awaitResumedRunTerminal(signal) {
		const terminal = await this.#awaitRootTerminal(signal, { skipInterruptedUntilRunning: true });
		if (!signal.aborted && !this.#disposed && (terminal.event === "interrupted" || terminal.event === "completed")) await this.#reconcilePendingInterruptsFromServer();
		return terminal;
	}
	#awaitRootTerminal(signal, options) {
		return new Promise((resolve) => {
			let settled = false;
			let sawRunning = false;
			const held = [];
			const heldReleases = this.#heldLifecycleReleases;
			function finish(result) {
				if (settled) return;
				settled = true;
				heldReleases.delete(releaseHeld);
				unsubscribeRoot?.();
				unsubscribeThread?.();
				unsubscribeError?.();
				signal.removeEventListener("abort", finishAborted);
				resolve(result);
			}
			const finishAborted = () => finish({ event: "aborted" });
			const onEvent = (event) => {
				if (settled) return;
				if (event.method !== "lifecycle") return;
				if (!require_namespace.isRootNamespace(event.params.namespace)) return;
				const match = this.#submittedRunMatch(event);
				if (match === "other") return;
				if (match === "pending") {
					held.push(event);
					return;
				}
				const lifecycle = event.params.data;
				if (lifecycle?.event === "running") {
					sawRunning = true;
					return;
				}
				if (lifecycle?.event === "completed") setTimeout(() => finish({ event: "completed" }), 0);
				else if (lifecycle?.event === "failed") setTimeout(() => finish({
					event: "failed",
					error: lifecycle.error
				}), 0);
				else if (lifecycle?.event === "interrupted") {
					if (options.skipInterruptedUntilRunning && !sawRunning) return;
					setTimeout(() => finish({ event: "interrupted" }), 0);
				}
			};
			const releaseHeld = () => {
				for (const event of held.splice(0)) onEvent(event);
			};
			heldReleases.add(releaseHeld);
			const unsubscribeRoot = this.#rootBus.subscribe(onEvent);
			const unsubscribeThread = this.#thread?.onEvent(onEvent);
			const unsubscribeError = this.#thread?.onError((error) => finish({
				event: "failed",
				error: error.message
			}));
			if (signal.aborted) finishAborted();
			else signal.addEventListener("abort", finishAborted, { once: true });
		});
	}
	/**
	* Resolve which protocol interrupt a resume command should target.
	* Headless-tool resumes are keyed by tool-call id; without matching
	* on that id, parallel tool handlers would respond to the wrong
	* interrupt (always the newest).
	*/
	#resolveInterruptForResume(resume) {
		const thread = this.#thread;
		if (thread == null) return null;
		return require_headless_tools.resolveInterruptTargetForHeadlessResume(resume, thread.interrupts, this.#resolvedInterrupts);
	}
	/**
	* Notify listeners that the underlying thread stream changed.
	*/
	#notifyThreadListeners() {
		for (const listener of this.#threadListeners) listener(this.#thread);
	}
};
/**
* True when a subagent still sits on its default `tools:<toolCallId>`
* namespace — i.e. no execution namespace has been observed (via SSE
* replay) or resolved (via history) yet. Used to gate lazy namespace
* resolution so already-promoted subagents aren't re-fetched.
*/
function namespaceIsDefaultOnly(entry) {
	if (entry == null) return false;
	return entry.namespace.length === 1 && entry.namespace[0] === `tools:${entry.id}`;
}
function defaultSubagentToolCallId(namespace) {
	if (namespace.length !== 1) return void 0;
	const segment = namespace[0];
	if (!segment.startsWith("tools:")) return void 0;
	const id = segment.slice(6);
	return id.length > 0 ? id : void 0;
}
/**
* Extract and coerce the configured messages key from a values object.
*
* @param values - State values object to read from.
* @param messagesKey - Key that contains the message array.
*/
function extractAndCoerceMessages(values, messagesKey) {
	const raw = values[messagesKey];
	if (!Array.isArray(raw)) return [];
	return require_message_coercion.ensureMessageInstances(raw);
}
function extractAndCoerceMessagesWithFallback(values, messagesKey) {
	let raw = values[messagesKey];
	if (!Array.isArray(raw) && messagesKey !== "messages") raw = values[require_constants.DEFAULT_MESSAGES_KEY];
	if (!Array.isArray(raw)) return null;
	return require_message_coercion.ensureMessageInstances(raw);
}
/**
* Collect pending interrupts from a `threads.getState()` `tasks` array.
*
* Shared by {@link StreamController.hydrate} and the post-resume
* reconcile path so both stay aligned on namespace derivation and
* id dedupe.
*/
function collectActiveInterruptsFromTasks(tasks) {
	const activeInterrupts = [];
	const activeIds = /* @__PURE__ */ new Set();
	for (const task of tasks) {
		if (!Array.isArray(task?.interrupts)) continue;
		const typedTask = task;
		const checkpointNs = typedTask.checkpoint?.checkpoint_ns;
		const namespace = typeof checkpointNs === "string" && checkpointNs.length > 0 ? checkpointNs.split("|").filter((segment) => segment.length > 0) : [...ROOT_NAMESPACE];
		for (const interrupt of typedTask.interrupts) {
			const typed = interrupt;
			const id = typed?.id;
			if (typeof id !== "string" || activeIds.has(id)) continue;
			activeIds.add(id);
			activeInterrupts.push(require_interrupts.normalizeInterruptForClient({
				id,
				value: typed?.value,
				namespace: Array.isArray(typed?.namespace) ? [...typed.namespace] : namespace
			}));
		}
	}
	return {
		activeInterrupts,
		activeIds
	};
}
/**
* Run id from a `synth:<run_id>:...` event id, the form the agent server
* uses for events it synthesizes such as root `lifecycle`. Events carry no
* run id field.
*/
function synthesizedEventRunId(eventId) {
	if (eventId == null || !eventId.startsWith("synth:")) return void 0;
	return eventId.split(":")[1] || void 0;
}
//#endregion
exports.ROOT_PUMP_CHANNELS = ROOT_PUMP_CHANNELS;
exports.StreamController = StreamController;

//# sourceMappingURL=controller.cjs.map