UNPKG

@langchain/langgraph-sdk

Version:

Client library for interacting with the LangGraph API

204 lines (203 loc) • 7.57 kB
require("../_virtual/_rolldown/runtime.cjs"); const require_dispatch_config = require("./dispatch-config.cjs"); const require_queue_adapter = require("./queue-adapter.cjs"); let _langchain_core_utils_uuid = require("@langchain/core/utils/uuid"); //#region src/stream/queue-adapter-agent-server.ts /** * Backs "enqueue" with real, durable runs. An entry is a genuine * server-side pending run the instant `enqueue()` resolves. * * Selected when `queue: "server"` is set (see {@link SubmitCoordinator}'s * constructor). */ var AgentServerQueueAdapter = class { #runs; #assistantId; #store; #onError; #getThread; /** One shared subscription for this adapter's lifetime, not one per run. */ #unsubscribe; /** ids canceled while their create() was still in flight. */ #pendingCancel = /* @__PURE__ */ new Set(); #threadId; /** Guards against a stale hydrate() landing after a rapid thread-switch. */ #hydrateGeneration = 0; /** Aborts in-flight bookkeeping `list()` calls on detach(); replaced (never reused) afterward. */ #abortController = new AbortController(); /** Handle for #refreshPending's pending retry, if any; cleared on detach(). */ #refreshRetryTimer; /** Lets enqueue() detect a "started" event during its own create(). */ #startedEventCount = 0; constructor(runs, assistantId, store, onError, getThread) { this.#runs = runs; this.#assistantId = assistantId; this.#store = store; this.#onError = onError; this.#getThread = getThread; } async hydrate(threadId) { this.#threadId = threadId; this.#ensureWatching(threadId); const generation = ++this.#hydrateGeneration; const knownAtRequestTime = this.#knownRunIds(); let pending; try { pending = await this.#runs.list(threadId, { status: "pending", limit: 1e3, select: [ "run_id", "kwargs", "created_at", "multitask_strategy" ], signal: this.#abortController.signal }); } catch (error) { if (generation === this.#hydrateGeneration) this.#onError(error); return; } if (generation !== this.#hydrateGeneration) return; const remoteIds = new Set(pending.map((run) => run.run_id)); this.#store.setState((current) => { const knownRunIds = new Set(current.flatMap((e) => e.runId ? [e.runId] : [])); const stillValid = current.filter((e) => this.#isStillQueued(e, remoteIds, knownAtRequestTime)); return [...pending.filter((run) => !knownRunIds.has(run.run_id)).map((run) => this.#toEntry(run)), ...stillValid]; }); } /** * On a self-created thread, this can race the active run's own * `run.start` request that creates the thread row: if this * `runs.create()` reaches the server first, it may 404. The local * adapter has no such race (it never hits the server while queued). */ async enqueue(threadId, values, options) { this.#threadId = threadId; this.#ensureWatching(threadId); const startedEventsBefore = this.#startedEventCount; const generationAtEnqueue = this.#hydrateGeneration; const id = (0, _langchain_core_utils_uuid.v7)(); this.#store.setState((current) => [...current, { id, values, options, createdAt: /* @__PURE__ */ new Date() }]); try { const run = await this.#runs.create(threadId, this.#assistantId, { input: values, config: require_dispatch_config.bindThreadConfig(options?.config, threadId), metadata: options?.metadata, checkpointId: options?.forkFrom, multitaskStrategy: "enqueue" }); if (this.#pendingCancel.delete(id)) { await this.#runs.cancel(threadId, run.run_id); return; } this.#store.setState((current) => current.map((entry) => entry.id === id ? { ...entry, runId: run.run_id } : entry)); if (this.#startedEventCount !== startedEventsBefore && this.#hydrateGeneration === generationAtEnqueue) this.#refreshPending(threadId); } catch (error) { this.#pendingCancel.delete(id); this.#store.setState((current) => current.filter((entry) => entry.id !== id)); this.#onError(error); throw error; } } async cancel(id) { const entry = this.#store.getSnapshot().find((e) => e.id === id); if (!entry) return false; this.#store.setState((current) => current.filter((e) => e.id !== id)); await this.#cancelEntry(entry); return true; } async clear() { const entries = this.#store.getSnapshot(); this.#store.setState(() => require_queue_adapter.EMPTY_QUEUE); const failures = (await Promise.allSettled(entries.map((entry) => this.#cancelEntry(entry)))).filter((r) => r.status === "rejected"); if (failures.length === 1) this.#onError(failures[0].reason); else if (failures.length > 1) this.#onError(new AggregateError(failures.map((f) => f.reason), `${failures.length} of ${entries.length} queued runs failed to cancel`)); } detach() { this.#hydrateGeneration++; this.#unsubscribe?.(); this.#unsubscribe = void 0; this.#threadId = void 0; this.#abortController.abort(); this.#abortController = new AbortController(); clearTimeout(this.#refreshRetryTimer); this.#refreshRetryTimer = void 0; } async #cancelEntry(entry) { if (entry.runId && this.#threadId) await this.#runs.cancel(this.#threadId, entry.runId); else this.#pendingCancel.add(entry.id); } /** * Lazily opens ONE subscription for this adapter's lifetime, not one * per run. `LifecycleEvent` carries no `run_id`, so a `"running"` * event triggers a `runs.list` re-check instead of a direct match. */ #ensureWatching(threadId) { if (this.#unsubscribe) return; this.#unsubscribe = this.#getThread(threadId).onEvent((event) => { if (event.method !== "lifecycle") return; if (event.params.data.event !== "running") return; this.#startedEventCount++; this.#refreshPending(threadId); }); } /** * Re-lists pending runs and drops any queue entry no longer reported * pending: it's running now. Retried once after a network failure * before surfacing an error — otherwise a single blip would leave a * now-running entry stuck showing "queued" until the next promotion * or a manual hydrate. */ async #refreshPending(threadId, attempt = 0) { if (this.#store.getSnapshot().length === 0) return; const generation = this.#hydrateGeneration; const knownAtRequestTime = this.#knownRunIds(); try { const pending = await this.#runs.list(threadId, { status: "pending", limit: 1e3, signal: this.#abortController.signal }); if (generation !== this.#hydrateGeneration) return; const stillPendingIds = new Set(pending.map((run) => run.run_id)); this.#store.setState((current) => current.filter((entry) => this.#isStillQueued(entry, stillPendingIds, knownAtRequestTime))); } catch (error) { if (generation !== this.#hydrateGeneration) return; if (attempt < 1) { this.#refreshRetryTimer = setTimeout(() => { this.#refreshRetryTimer = void 0; this.#refreshPending(threadId, attempt + 1); }, 2e3); return; } this.#onError(error); } } #toEntry(run) { return { id: run.run_id, runId: run.run_id, values: run.kwargs?.input, createdAt: new Date(run.created_at) }; } #knownRunIds() { return new Set(this.#store.getSnapshot().flatMap((e) => e.runId ? [e.runId] : [])); } /** Shared by hydrate() and #refreshPending(): is a list() response grounds to evict this entry? */ #isStillQueued(entry, stillPendingIds, knownAtRequestTime) { return !entry.runId || stillPendingIds.has(entry.runId) || !knownAtRequestTime.has(entry.runId); } }; //#endregion exports.AgentServerQueueAdapter = AgentServerQueueAdapter; //# sourceMappingURL=queue-adapter-agent-server.cjs.map