UNPKG

@langchain/langgraph-sdk

Version:

Client library for interacting with the LangGraph API

895 lines (894 loc) • 30.9 kB
import { MessageTupleManager, ensureHistoryMessageInstances, ensureMessageInstances, toMessageClass } from "./messages.js"; import { flushPendingHeadlessToolInterrupts } from "../headless-tools.js"; import { extractInterrupts, userFacingInterruptsFromThreadTasks, userFacingInterruptsFromValuesArray } from "./interrupts.js"; import { StreamError } from "./errors.js"; import { getToolCallsWithResults } from "../utils/tools.js"; import { StreamManager } from "./manager.js"; import { filterStream, onFinishRequiresThreadState, unique } from "./utils.js"; import { getBranchContext, getMessagesMetadataMap } from "./branching.js"; import { PendingRunsTracker } from "./queue.js"; //#region src/ui/orchestrator.ts /** * Fetch the history of a thread. * @param client - The client to use. * @param threadId - The ID of the thread to fetch the history of. * @param options - The options to use. * @returns The history of the thread. */ function fetchHistory(client, threadId, options) { if (options?.limit === false) return client.threads.getState(threadId).then((state) => { if (state.checkpoint == null) return []; return [state]; }); const limit = typeof options?.limit === "number" ? options.limit : 10; return client.threads.getHistory(threadId, { limit }); } /** * Resolve the run metadata storage. * @param reconnectOnMount - The reconnect on mount option. * @returns The run metadata storage. */ function resolveRunMetadataStorage(reconnectOnMount) { if (typeof globalThis.window === "undefined") return null; if (reconnectOnMount === true) return globalThis.window.sessionStorage; if (typeof reconnectOnMount === "function") return reconnectOnMount(); return null; } /** * Resolve the callback stream modes. * @param options - The options to use. * @returns The callback stream modes. */ function resolveCallbackStreamModes(options) { const modes = []; if (options.onUpdateEvent) modes.push("updates"); if (options.onCustomEvent) modes.push("custom"); if (options.onCheckpointEvent) modes.push("checkpoints"); if (options.onTaskEvent) modes.push("tasks"); if ("onDebugEvent" in options && options.onDebugEvent) modes.push("debug"); if ("onLangChainEvent" in options && options.onLangChainEvent) modes.push("events"); return modes; } /** * Framework-agnostic orchestrator for LangGraph Platform streams. * * Encapsulates all business logic shared across React, Vue, Svelte, and Angular: * thread management, history fetching, stream lifecycle, queue management, * branching, subagent management, and auto-reconnect. * * Framework adapters subscribe to state changes via {@link subscribe} and * map the orchestrator's getters to framework-specific reactive primitives. */ var StreamOrchestrator = class { stream; messageManager; pendingRuns; #options; #accessors; historyLimit; #runMetadataStorage; #callbackStreamModes; #trackedStreamModes = []; #threadId; #threadIdPromise = null; #threadIdStreaming = null; #history; #branch = ""; #submitting = false; #listeners = /* @__PURE__ */ new Set(); #version = 0; #streamUnsub = null; #queueUnsub = null; #disposed = false; #handledHeadlessToolInterruptIds = /* @__PURE__ */ new Set(); /** * Create a new StreamOrchestrator. * * @param options - Configuration options for the stream, including callbacks, * throttle settings, reconnect behaviour, and subagent filters. * @param accessors - Framework-specific accessors that resolve reactive * primitives (client, assistant ID, messages key) at call time. */ constructor(options, accessors) { this.#options = options; this.#accessors = accessors; this.#runMetadataStorage = resolveRunMetadataStorage(options.reconnectOnMount); this.#callbackStreamModes = resolveCallbackStreamModes(options); this.historyLimit = typeof options.fetchStateHistory === "object" && options.fetchStateHistory != null ? options.fetchStateHistory.limit ?? false : options.fetchStateHistory ?? false; this.messageManager = new MessageTupleManager(); this.stream = new StreamManager(this.messageManager, { throttle: options.throttle ?? false, subagentToolNames: options.subagentToolNames, filterSubagentMessages: options.filterSubagentMessages, toMessage: options.toMessage ?? toMessageClass }); this.pendingRuns = new PendingRunsTracker(); this.#threadId = void 0; this.#history = { data: void 0, error: void 0, isLoading: false, mutate: this.#mutate }; this.#streamUnsub = this.stream.subscribe(() => { this.#flushPendingHeadlessToolInterrupts(this.stream.values); this.#notify(); }); this.#queueUnsub = this.pendingRuns.subscribe(() => { this.#notify(); }); } /** * Register a listener that is called whenever the orchestrator's internal * state changes (stream updates, queue changes, history mutations, etc.). * * @param listener - Callback invoked on every state change. * @returns An unsubscribe function that removes the listener. */ subscribe(listener) { this.#listeners.add(listener); return () => { this.#listeners.delete(listener); }; } /** * Return the current version number, incremented on every state change. * Useful as a React `useSyncExternalStore` snapshot. * * @returns The current monotonically increasing version counter. */ getSnapshot() { return this.#version; } /** * Increment the version counter and invoke all registered listeners. * No-op if the orchestrator has been disposed. */ #notify() { if (this.#disposed) return; this.#version += 1; for (const listener of this.#listeners) listener(); } /** * The current thread ID, or `undefined` if no thread is active. */ get threadId() { return this.#threadId; } /** * Update thread ID from an external source (e.g. reactive prop change). * Clears the current stream and triggers a history fetch. * @param newId - The new thread ID to set. * @returns The new thread ID. */ setThreadId(newId) { if (newId === this.#threadId) return; this.#threadId = newId; this.#handledHeadlessToolInterruptIds.clear(); this.stream.clear(); this.#fetchHistoryForThread(newId); this.#notify(); } /** * Update the thread ID from within a submit flow. Sets both the * streaming and canonical thread IDs, fires the `onThreadId` callback, * and notifies listeners. * * @param newId - The newly created or resolved thread ID. */ #setThreadIdFromSubmit(newId) { this.#threadIdStreaming = newId; this.#threadId = newId; this.#handledHeadlessToolInterruptIds.clear(); this.#options.onThreadId?.(newId); this.#notify(); } #fetchHistoryForThread(threadId) { if (this.#threadIdStreaming != null && this.#threadIdStreaming === threadId) return; if (threadId != null) { this.#history = { ...this.#history, isLoading: true, mutate: this.#mutate }; this.#notify(); this.#mutate(threadId); } else { this.#history = { data: void 0, error: void 0, isLoading: false, mutate: this.#mutate }; this.#notify(); } } /** * The current thread history fetch state, including data, loading status, * error, and a {@link UseStreamThread.mutate | mutate} function to * manually re-fetch. */ get historyData() { return this.#history; } async #mutate(mutateId) { const tid = mutateId ?? this.#threadId; if (!tid) return void 0; try { const data = await fetchHistory(this.#accessors.getClient(), tid, { limit: this.historyLimit }); this.#history = { data, error: void 0, isLoading: false, mutate: this.#mutate }; this.#flushPendingHeadlessToolInterrupts(data?.at(0)?.values); this.#notify(); return data; } catch (err) { this.#history = { ...this.#history, error: err, isLoading: false }; this.#notify(); this.#options.onError?.(err, void 0); return; } } /** * Trigger initial history fetch for the current thread ID. * Should be called once after construction when the initial threadId is known. */ initThreadId(threadId) { this.#threadId = threadId; this.#handledHeadlessToolInterruptIds.clear(); this.#fetchHistoryForThread(threadId); } /** * The currently active branch identifier. An empty string represents * the main (default) branch. */ get branch() { return this.#branch; } /** * Set the active branch and notify listeners if the value changed. * * @param value - The branch identifier to switch to. */ setBranch(value) { if (value === this.#branch) return; this.#branch = value; this.#notify(); } /** * Derived branch context computed from the current branch and thread * history. Contains the thread head, branch tree, and checkpoint-to-branch * mapping for the active branch. */ get branchContext() { return getBranchContext(this.#branch, this.#history.data ?? void 0); } #getMessages(value) { const messagesKey = this.#accessors.getMessagesKey(); return Array.isArray(value[messagesKey]) ? value[messagesKey] : []; } #setMessages(current, messages) { const messagesKey = this.#accessors.getMessagesKey(); return { ...current, [messagesKey]: messages }; } /** * The state values from the thread head of the current branch history, * falling back to {@link AnyStreamOptions.initialValues | initialValues} * or an empty object. */ get historyValues() { return this.branchContext.threadHead?.values ?? this.#options.initialValues ?? {}; } /** * The error from the last task in the thread head, if any. * Attempts to parse structured {@link StreamError} instances from JSON. */ get historyError() { const error = this.branchContext.threadHead?.tasks?.at(-1)?.error; if (error == null) return void 0; try { const parsed = JSON.parse(error); if (StreamError.isStructuredError(parsed)) return new StreamError(parsed); return parsed; } catch {} return error; } /** * The latest state values received from the active stream, or `null` if * no stream is running or no values have been received yet. */ get streamValues() { return this.stream.values; } /** * The error from the active stream, if one occurred during streaming. */ get streamError() { return this.stream.error; } /** * The merged state values, preferring live stream values over history. * This is the primary way to read the current thread state. */ get values() { return this.stream.values ?? this.historyValues; } /** * The first available error from the stream, history, or thread fetch. * Returns `undefined` when no error is present. */ get error() { return this.stream.error ?? this.historyError ?? this.#history.error; } /** * Whether the stream is currently active and receiving events. */ get isLoading() { return this.stream.isLoading; } /** * The messages array extracted from the current {@link values} using the * configured messages key. */ get messages() { return this.#getMessages(this.values); } /** * The current messages converted to LangChain {@link BaseMessage} instances. * Automatically tracks the `"messages-tuple"` stream mode. */ get messageInstances() { this.trackStreamMode("messages-tuple"); return ensureMessageInstances(this.messages); } /** * All tool calls with their corresponding results extracted from * the current messages. Automatically tracks the `"messages-tuple"` * stream mode. */ get toolCalls() { this.trackStreamMode("messages-tuple"); return getToolCallsWithResults(this.#getMessages(this.values)); } /** * Get tool calls with results for a specific AI message. * Automatically tracks the `"messages-tuple"` stream mode. * * @param message - The AI message to extract tool calls from. * @returns Tool calls whose AI message ID matches the given message. */ getToolCalls(message) { this.trackStreamMode("messages-tuple"); return getToolCallsWithResults(this.#getMessages(this.values)).filter((tc) => tc.aiMessage.id === message.id); } /** * All active interrupts for the current thread state. * Returns an empty array when the stream is loading or no interrupts * are present. Falls back to a `{ when: "breakpoint" }` sentinel when * there are pending next nodes but no explicit interrupt data. */ get interrupts() { const v = this.values; if (v != null && "__interrupt__" in v && Array.isArray(v.__interrupt__)) return userFacingInterruptsFromValuesArray(v.__interrupt__); if (this.isLoading) return []; const taskInterrupts = userFacingInterruptsFromThreadTasks((this.branchContext.threadHead?.tasks ?? []).flatMap((t) => t.interrupts ?? [])); if (taskInterrupts != null) return taskInterrupts; if (!(this.branchContext.threadHead?.next ?? []).length || this.error != null) return []; return [{ when: "breakpoint" }]; } /** * The single most relevant interrupt for the current thread state, * or `undefined` if no interrupt is active. Convenience accessor that * delegates to {@link extractInterrupts}. */ get interrupt() { return extractInterrupts(this.values, { isLoading: this.isLoading, threadState: this.branchContext.threadHead, error: this.error }); } /** * Flattened history messages as LangChain {@link BaseMessage} instances, * ordered chronologically across all branch checkpoints. * * @throws If `fetchStateHistory` was not enabled in the options. */ get flatHistory() { if (this.historyLimit === false) throw new Error("`fetchStateHistory` must be set to `true` to use `history`"); return ensureHistoryMessageInstances(this.branchContext.flatHistory, this.#accessors.getMessagesKey()); } /** * Whether the initial thread history is still being loaded and no data * is available yet. Returns `false` once the first fetch completes. */ get isThreadLoading() { return this.#history.isLoading && this.#history.data == null; } /** * The full branch tree structure for the current thread history. * * @experimental This API may change in future releases. * @throws If `fetchStateHistory` was not enabled in the options. */ get experimental_branchTree() { if (this.historyLimit === false) throw new Error("`fetchStateHistory` must be set to `true` to use `experimental_branchTree`"); return this.branchContext.branchTree; } /** * A map of metadata entries for all messages, derived from history * and branch context. Used internally by {@link getMessagesMetadata}. */ get messageMetadata() { return getMessagesMetadataMap({ initialValues: this.#options.initialValues, history: this.#history.data, getMessages: (value) => this.#getMessages(value), branchContext: this.branchContext }); } /** * Look up metadata for a specific message, merging stream-time metadata * with history-derived metadata. * * @param message - The message to look up metadata for. * @param index - Optional positional index used as a fallback identifier. * @returns The merged metadata, or `undefined` if none is available. */ getMessagesMetadata(message, index) { const streamMetadata = this.messageManager.get(message.id)?.metadata; const historyMetadata = this.messageMetadata?.find((m) => m.messageId === (message.id ?? index)); if (streamMetadata != null || historyMetadata != null) return { ...historyMetadata, streamMetadata }; } /** * The list of pending run entries currently waiting in the queue. */ get queueEntries() { return this.pendingRuns.entries; } /** * The number of pending runs in the queue. */ get queueSize() { return this.pendingRuns.size; } /** * Cancel and remove a specific pending run from the queue. * If the run exists and a thread is active, the run is also cancelled * on the server. * * @param id - The run ID to cancel. * @returns `true` if the run was found and removed, `false` otherwise. */ async cancelQueueItem(id) { const tid = this.#threadId; const removed = this.pendingRuns.remove(id); if (removed && tid) await this.#accessors.getClient().runs.cancel(tid, id); return removed; } /** * Remove all pending runs from the queue and cancel them on the server. */ async clearQueue() { const tid = this.#threadId; const removed = this.pendingRuns.removeAll(); if (tid && removed.length > 0) await Promise.all(removed.map((e) => this.#accessors.getClient().runs.cancel(tid, e.id))); } /** * A map of all known subagent stream interfaces, keyed by tool call ID. */ get subagents() { return this.stream.getSubagents(); } /** * The subset of subagents that are currently active (streaming). */ get activeSubagents() { return this.stream.getActiveSubagents(); } /** * Retrieve a specific subagent stream interface by its tool call ID. * * @param toolCallId - The tool call ID that spawned the subagent. * @returns The subagent interface, or `undefined` if not found. */ getSubagent(toolCallId) { return this.stream.getSubagent(toolCallId); } /** * Retrieve all subagent stream interfaces that match a given agent type. * * @param type - The agent type name to filter by. * @returns An array of matching subagent interfaces. */ getSubagentsByType(type) { return this.stream.getSubagentsByType(type); } /** * Retrieve all subagent stream interfaces associated with a specific * AI message. * * @param messageId - The ID of the parent AI message. * @returns An array of subagent interfaces spawned by that message. */ getSubagentsByMessage(messageId) { return this.stream.getSubagentsByMessage(messageId); } /** * Reconstruct subagents from history messages if applicable. * Call this when history finishes loading and the stream isn't active. * Returns an AbortController for cancelling the subagent history fetch, * or null if no reconstruction was needed. */ reconstructSubagentsIfNeeded() { const hvMessages = this.#getMessages(this.historyValues); if (!(this.#options.filterSubagentMessages && !this.isLoading && !this.#history.isLoading && hvMessages.length > 0)) return null; this.stream.reconstructSubagents(hvMessages, { skipIfPopulated: true }); const tid = this.#threadId; if (this.historyLimit !== false && tid) { const controller = new AbortController(); this.stream.fetchSubagentHistory(this.#accessors.getClient().threads, tid, { messagesKey: this.#accessors.getMessagesKey(), historyLimit: typeof this.historyLimit === "number" ? this.historyLimit : void 0, signal: controller.signal }); return controller; } return null; } /** * Register additional stream modes that should be included in future * stream requests. Modes are deduplicated automatically. * * @param modes - One or more stream modes to track. */ trackStreamMode(...modes) { for (const mode of modes) if (!this.#trackedStreamModes.includes(mode)) this.#trackedStreamModes.push(mode); } /** * Stop the currently active stream. If reconnect metadata storage is * configured, also cancels the run on the server and cleans up stored * run metadata. */ async stop() { const wasLoading = this.stream.isLoading; const threadId = this.#threadId; const shouldRefetch = this.historyLimit === true || typeof this.historyLimit === "number" || onFinishRequiresThreadState(this.#options.onFinish); await this.stream.stop(this.historyValues, { onStop: (args) => { if (this.#runMetadataStorage && this.#threadId) { const runId = this.#runMetadataStorage.getItem(`lg:stream:${this.#threadId}`); if (runId) this.#accessors.getClient().runs.cancel(this.#threadId, runId); this.#runMetadataStorage.removeItem(`lg:stream:${this.#threadId}`); } this.#options.onStop?.(args); } }); if (wasLoading && threadId) if (shouldRefetch) { const newHistory = await this.#mutate(threadId); this.stream.setStreamValues(newHistory?.at(0)?.values ?? null); } else this.stream.setStreamValues(this.historyValues); } /** * Join an existing run's event stream by run ID. Used for reconnecting * to in-progress runs or consuming queued runs. * * @param runId - The ID of the run to join. * @param lastEventId - The last event ID received, for resuming mid-stream. * Defaults to `"-1"` (start from the beginning). * @param joinOptions - Additional options for stream mode and event filtering. */ async joinStream(runId, lastEventId, joinOptions) { lastEventId ??= "-1"; const tid = this.#threadId; if (!tid) return; this.#threadIdStreaming = tid; const callbackMeta = { thread_id: tid, run_id: runId }; const shouldRefetchJoin = this.historyLimit === true || typeof this.historyLimit === "number" || onFinishRequiresThreadState(this.#options.onFinish); const client = this.#accessors.getClient(); await this.stream.start(async (signal) => { const rawStream = client.runs.joinStream(tid, runId, { signal, lastEventId, streamMode: joinOptions?.streamMode }); return joinOptions?.filter != null ? filterStream(rawStream, joinOptions.filter) : rawStream; }, { getMessages: (value) => this.#getMessages(value), setMessages: (current, messages) => this.#setMessages(current, messages), initialValues: this.historyValues, callbacks: this.#options, onSuccess: async () => { this.#runMetadataStorage?.removeItem(`lg:stream:${tid}`); if (!shouldRefetchJoin) { if (this.#options.onFinish != null && !onFinishRequiresThreadState(this.#options.onFinish)) this.#options.onFinish(void 0, callbackMeta); return; } const lastHead = (await this.#mutate(tid))?.at(0); if (lastHead) this.#options.onFinish?.(lastHead, callbackMeta); }, onError: (error) => { this.#options.onError?.(error, callbackMeta); }, onFinish: () => { this.#threadIdStreaming = null; } }); } /** * Submit input values directly to the LangGraph Platform, creating a new * thread if necessary. Starts a streaming run and processes events until * completion. Unlike {@link submit}, this does not handle queueing — if * a stream is already active, a concurrent run will be started. * * @param values - The state values to send as run input. * @param submitOptions - Optional configuration for the run (config, * checkpoint, multitask strategy, optimistic values, etc.). */ submitDirect(values, submitOptions) { const currentBranchContext = this.branchContext; const checkpointId = submitOptions?.checkpoint?.checkpoint_id; this.#branch = checkpointId != null ? currentBranchContext.branchByCheckpoint[checkpointId]?.branch ?? "" : ""; const includeImplicitBranch = this.historyLimit === true || typeof this.historyLimit === "number"; const shouldRefetch = includeImplicitBranch || onFinishRequiresThreadState(this.#options.onFinish); let checkpoint = submitOptions?.checkpoint ?? (includeImplicitBranch ? currentBranchContext.threadHead?.checkpoint : void 0) ?? void 0; if (submitOptions?.checkpoint === null) checkpoint = void 0; if (checkpoint != null) delete checkpoint.thread_id; let callbackMeta; let rejoinKey; let usableThreadId; const client = this.#accessors.getClient(); const assistantId = this.#accessors.getAssistantId(); return this.stream.start(async (signal) => { usableThreadId = this.#threadId; if (usableThreadId) this.#threadIdStreaming = usableThreadId; if (!usableThreadId) { const threadPromise = client.threads.create({ threadId: submitOptions?.threadId, metadata: submitOptions?.metadata }); this.#threadIdPromise = threadPromise.then((t) => t.thread_id); usableThreadId = (await threadPromise).thread_id; this.#setThreadIdFromSubmit(usableThreadId); } const streamMode = unique([ "values", "updates", ...submitOptions?.streamMode ?? [], ...this.#trackedStreamModes, ...this.#callbackStreamModes ]); this.stream.setStreamValues(() => { const prev = { ...this.historyValues, ...this.stream.values }; if (submitOptions?.optimisticValues != null) return { ...prev, ...typeof submitOptions.optimisticValues === "function" ? submitOptions.optimisticValues(prev) : submitOptions.optimisticValues }; return { ...prev }; }); const streamResumable = submitOptions?.streamResumable ?? !!this.#runMetadataStorage; return client.runs.stream(usableThreadId, assistantId, { input: values, config: submitOptions?.config, context: submitOptions?.context, command: submitOptions?.command, interruptBefore: submitOptions?.interruptBefore, interruptAfter: submitOptions?.interruptAfter, metadata: submitOptions?.metadata, multitaskStrategy: submitOptions?.multitaskStrategy, onCompletion: submitOptions?.onCompletion, onDisconnect: submitOptions?.onDisconnect ?? (streamResumable ? "continue" : "cancel"), signal, checkpoint, streamMode, streamSubgraphs: submitOptions?.streamSubgraphs, streamResumable, durability: submitOptions?.durability, onRunCreated: (params) => { callbackMeta = { run_id: params.run_id, thread_id: params.thread_id ?? usableThreadId }; if (this.#runMetadataStorage) { rejoinKey = `lg:stream:${usableThreadId}`; this.#runMetadataStorage.setItem(rejoinKey, callbackMeta.run_id); } this.#options.onCreated?.(callbackMeta); } }); }, { getMessages: (value) => this.#getMessages(value), setMessages: (current, messages) => this.#setMessages(current, messages), initialValues: this.historyValues, callbacks: this.#options, onSuccess: async () => { if (rejoinKey) this.#runMetadataStorage?.removeItem(rejoinKey); if (shouldRefetch && usableThreadId) { const lastHead = (await this.#mutate(usableThreadId))?.at(0); if (lastHead) { this.#options.onFinish?.(lastHead, callbackMeta); return null; } } else if (this.#options.onFinish != null && !onFinishRequiresThreadState(this.#options.onFinish)) this.#options.onFinish(void 0, callbackMeta); }, onError: (error) => { this.#options.onError?.(error, callbackMeta); submitOptions?.onError?.(error, callbackMeta); }, onFinish: () => { this.#threadIdStreaming = null; } }); } #drainQueue() { if (!this.isLoading && !this.#submitting && this.pendingRuns.size > 0) { const next = this.pendingRuns.shift(); if (next) { this.#submitting = true; this.joinStream(next.id).finally(() => { this.#submitting = false; this.#drainQueue(); }); } } } /** * Trigger queue draining. Framework adapters should call this * when isLoading or queue size changes. */ drainQueue() { this.#drainQueue(); } /** * Submit input values with automatic queue management. If a stream is * already active, the run is enqueued (unless the multitask strategy * is `"interrupt"` or `"rollback"`, in which case the current run is * replaced). Queued runs are drained sequentially via {@link drainQueue}. * * @param values - The state values to send as run input. * @param submitOptions - Optional configuration for the run. * @returns The result of {@link submitDirect} if the run was started * immediately, or `void` if the run was enqueued. */ async submit(values, submitOptions) { if (this.stream.isLoading || this.#submitting) { if (submitOptions?.multitaskStrategy === "interrupt" || submitOptions?.multitaskStrategy === "rollback") { this.#submitting = true; try { await this.submitDirect(values, submitOptions); } finally { this.#submitting = false; } return; } let usableThreadId = this.#threadId; if (!usableThreadId && this.#threadIdPromise) usableThreadId = await this.#threadIdPromise; if (usableThreadId) { const client = this.#accessors.getClient(); const assistantId = this.#accessors.getAssistantId(); try { const run = await client.runs.create(usableThreadId, assistantId, { input: values, config: submitOptions?.config, context: submitOptions?.context, command: submitOptions?.command, interruptBefore: submitOptions?.interruptBefore, interruptAfter: submitOptions?.interruptAfter, metadata: submitOptions?.metadata, multitaskStrategy: "enqueue", streamResumable: true, streamSubgraphs: submitOptions?.streamSubgraphs, durability: submitOptions?.durability }); this.pendingRuns.add({ id: run.run_id, values, options: submitOptions, createdAt: new Date(run.created_at) }); } catch (error) { this.#options.onError?.(error, void 0); submitOptions?.onError?.(error, void 0); } return; } } this.#submitting = true; const result = this.submitDirect(values, submitOptions); Promise.resolve(result).finally(() => { this.#submitting = false; this.#drainQueue(); }); return result; } /** * Switch to a different thread (or clear the current thread). * Clears the active stream, cancels all queued runs on the previous * thread, fetches history for the new thread, and notifies the * {@link AnyStreamOptions.onThreadId | onThreadId} callback. * * @param newThreadId - The thread ID to switch to, or `null` to clear. */ switchThread(newThreadId) { if (newThreadId !== (this.#threadId ?? null)) { const prevThreadId = this.#threadId; this.#threadId = newThreadId ?? void 0; this.#handledHeadlessToolInterruptIds.clear(); this.stream.clear(); const removed = this.pendingRuns.removeAll(); if (prevThreadId && removed.length > 0) { const client = this.#accessors.getClient(); Promise.all(removed.map((e) => client.runs.cancel(prevThreadId, e.id))); } this.#fetchHistoryForThread(this.#threadId); if (newThreadId != null) this.#options.onThreadId?.(newThreadId); this.#notify(); } } /** * Attempt to reconnect to a previously running stream. * Returns true if a reconnection was initiated. */ tryReconnect() { if (this.#runMetadataStorage && this.#threadId) { const runId = this.#runMetadataStorage.getItem(`lg:stream:${this.#threadId}`); if (runId) { this.joinStream(runId); return true; } } return false; } /** * Whether reconnect-on-mount behaviour is enabled (i.e. run metadata * storage is available). */ get shouldReconnect() { return !!this.#runMetadataStorage; } /** * Tear down the orchestrator: stop the active stream, remove all * internal subscriptions, and mark the instance as disposed. * After calling this method, the orchestrator should not be reused. */ dispose() { this.#disposed = true; this.#streamUnsub?.(); this.#queueUnsub?.(); this.#streamUnsub = null; this.#queueUnsub = null; this.stop().catch(() => void 0); } #flushPendingHeadlessToolInterrupts(values) { flushPendingHeadlessToolInterrupts(values, this.#options.tools, this.#handledHeadlessToolInterruptIds, { onTool: this.#options.onTool, defer: (run) => { Promise.resolve().then(run); }, resumeSubmit: (command) => void this.submit(null, { multitaskStrategy: "interrupt", command }) }); } }; //#endregion export { StreamOrchestrator }; //# sourceMappingURL=orchestrator.js.map