@langchain/langgraph-sdk
Version:
Client library for interacting with the LangGraph API
1,552 lines • 56.3 kB
JavaScript
const require_subscription = require("./subscription.cjs");
const require_multi_cursor_buffer = require("./multi-cursor-buffer.cjs");
require("../../stream/constants.cjs");
const require_messages = require("../../ui/messages.cjs");
const require_tools = require("./handles/tools.cjs");
const require_messages$1 = require("./messages.cjs");
const require_media = require("./media.cjs");
const require_subgraphs = require("./handles/subgraphs.cjs");
const require_subagents = require("./handles/subagents.cjs");
require("./handles/index.cjs");
const require_error = require("./error.cjs");
const require_headless_tools = require("../../headless-tools.cjs");
//#region src/client/stream/index.ts
const MESSAGE_LIKE_TYPES = new Set([
"human",
"user",
"ai",
"assistant",
"tool",
"system",
"function",
"remove"
]);
/**
* When the state payload has a `messages` array containing plain
* serialized messages (objects with a recognized `type` field), coerce
* them into `@langchain/core/messages` class instances so remote runs
* expose the same shape as in-process runs.
*
* Returns the input unchanged when the payload is not an object, does
* not include a `messages` key, or contains entries that are already
* class instances / not message-like.
*/
function coerceStateMessages(value) {
if (value == null || typeof value !== "object" || Array.isArray(value)) return value;
const state = value;
const messages = state.messages;
if (!Array.isArray(messages) || messages.length === 0) return value;
if (!messages.some((msg) => {
if (msg == null || typeof msg !== "object") return false;
if (typeof msg.getType === "function") return false;
const type = msg.type;
return typeof type === "string" && MESSAGE_LIKE_TYPES.has(type);
})) return value;
return {
...state,
messages: require_messages.ensureMessageInstances(messages)
};
}
function namespaceKey(ns) {
return ns.join("\0");
}
function maxSeq(current, next) {
if (next == null) return current;
if (current == null) return next;
return Math.max(current, next);
}
const ROOT_TERMINAL_LIFECYCLE_EVENTS = new Set([
"completed",
"failed",
"interrupted"
]);
/**
* Detect a root-namespace terminal lifecycle event. Used by
* `#startProjection`'s `endOnRootTerminal` guard to settle per-run
* dispatchers regardless of whether the shared-stream pause logic
* applies to their underlying subscription.
*/
function isRootTerminalLifecycle(event) {
if (event.method !== "lifecycle") return false;
if (event.params.namespace.length !== 0) return false;
const data = event.params.data;
return data?.event != null && ROOT_TERMINAL_LIFECYCLE_EVENTS.has(data.event);
}
function namespaceListsEqual(a, b) {
if (a === b) return true;
if (a === void 0 || b === void 0) return false;
if (a.length !== b.length) return false;
const aKeys = /* @__PURE__ */ new Set();
for (const ns of a) aKeys.add(namespaceKey(ns));
for (const ns of b) if (!aKeys.has(namespaceKey(ns))) return false;
return true;
}
/**
* Structural equality on filters. Two filters are equal iff they
* request the same channel set, the same namespace prefix set
* (with `undefined` meaning wildcard), and the same depth
* (with `undefined` meaning unbounded).
*/
function filterEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.channels.length !== b.channels.length) return false;
const aChannels = new Set(a.channels);
for (const ch of b.channels) if (!aChannels.has(ch)) return false;
if (!namespaceListsEqual(a.namespaces, b.namespaces)) return false;
if ((a.depth ?? null) !== (b.depth ?? null)) return false;
return true;
}
function isPrefix(prefix, candidate) {
if (prefix.length > candidate.length) return false;
for (let i = 0; i < prefix.length; i += 1) if (prefix[i] !== candidate[i]) return false;
return true;
}
/**
* Whether the `coverer` filter delivers every event a subscription
* opened with `target` could want.
*
* Rules:
* - Channels: target.channels must be a subset of coverer.channels.
* - Namespaces:
* - coverer wildcard (`undefined`) → coverer covers all prefixes.
* - coverer explicit + target wildcard → not covered.
* - both explicit → every target prefix must have some coverer
* prefix that is its ancestor (coverer's prefix delivers events
* for all descendants, modulo depth).
* - Depth:
* - coverer unbounded (`undefined`) → depth is covered.
* - otherwise, for each target prefix `tp` covered by coverer
* prefix `cp`, the maximum event depth target wants
* (`tp.length + (target.depth ?? ∞) - cp.length`) must be
* `<= coverer.depth`. For a wildcard target with bounded depth,
* target's max absolute depth is `target.depth` (prefix is `[]`).
*/
function filterCovers(coverer, target) {
const covererChannels = new Set(coverer.channels);
for (const ch of target.channels) if (!covererChannels.has(ch)) return false;
const covererDepth = coverer.depth;
const targetDepth = target.depth;
if (coverer.namespaces == null) {
if (covererDepth == null) return true;
if (targetDepth == null) return false;
return targetDepth <= covererDepth;
}
if (target.namespaces == null) return false;
for (const tp of target.namespaces) if (!coverer.namespaces.some((cp) => {
if (!isPrefix(cp, tp)) return false;
if (covererDepth == null) return true;
if (targetDepth == null) return false;
return tp.length - cp.length + targetDepth <= covererDepth;
})) return false;
return true;
}
function normalizeSubscribeParams(paramsOrChannels, options = {}) {
if (typeof paramsOrChannels === "object" && !Array.isArray(paramsOrChannels) && "channels" in paramsOrChannels) return paramsOrChannels;
const channels = Array.isArray(paramsOrChannels) ? [...paramsOrChannels] : [paramsOrChannels];
return {
...options,
channels
};
}
/**
* Fold the ergonomic top-level `forkFrom` checkpoint id into
* `config.configurable.checkpoint_id` and strip `forkFrom` from the
* outgoing params.
*
* `forkFrom` is purely an SDK-side convenience: callers say
* `submit(input, { forkFrom })` instead of hand-building a nested
* RunnableConfig. The agent server only ever accepts the fork target via
* `config.configurable.checkpoint_id` (the same field the legacy run
* endpoints use), so we translate here — before the `run.start` message
* hits the wire — keeping a single, legacy-compliant way to provide it.
*
* `forkFrom` takes precedence over any `checkpoint_id` the caller already
* placed in `config.configurable`, matching the prior server-side merge.
*/
function foldForkFromIntoConfig(params) {
const { forkFrom, ...rest } = params;
if (typeof forkFrom !== "string" || forkFrom.length === 0) return rest;
const config = rest.config != null && typeof rest.config === "object" ? rest.config : {};
const configurable = config.configurable != null && typeof config.configurable === "object" ? config.configurable : {};
return {
...rest,
config: {
...config,
configurable: {
...configurable,
checkpoint_id: forkFrom
}
}
};
}
/**
* Async iterable handle for raw event subscriptions.
*
* An optional `transform` maps each incoming event before it is queued
* or delivered to a waiting consumer. This is used by named custom
* channel subscriptions (e.g. `"custom:a2a"`) to unwrap the payload
* so callers receive the raw emitted data instead of the protocol
* event envelope.
*/
var SubscriptionHandle = class {
subscriptionId;
params;
queue = [];
waiters = [];
closed = false;
paused = false;
resumeResolve;
onUnsubscribe;
transform;
constructor(subscriptionId, params, onUnsubscribe, transform) {
this.subscriptionId = subscriptionId;
this.params = params;
this.onUnsubscribe = onUnsubscribe;
this.transform = transform ?? ((event) => event);
}
push(event) {
if (this.closed) return;
const value = this.transform(event);
const waiter = this.waiters.shift();
if (waiter) {
waiter({
done: false,
value
});
return;
}
this.queue.push(value);
}
/**
* Pause the subscription: resolve all waiting iterators with `done: true`
* so `for await` loops exit, but keep the subscription alive. New events
* arriving while paused are still buffered. Call `resume()` to allow
* iterators to consume again.
*/
pause() {
if (this.closed) return;
this.paused = true;
while (this.waiters.length > 0) this.waiters.shift()?.({
done: true,
value: void 0
});
}
/**
* Resume a paused subscription so new `for await` loops can consume
* buffered and future events.
*/
resume() {
this.paused = false;
this.resumeResolve?.();
this.resumeResolve = void 0;
}
/**
* Returns a promise that resolves when `resume()` is called. Resolves
* immediately if not currently paused.
*/
waitForResume() {
if (!this.paused) return Promise.resolve();
return new Promise((resolve) => {
this.resumeResolve = resolve;
});
}
get isPaused() {
return this.paused;
}
close() {
this.closed = true;
this.paused = false;
while (this.waiters.length > 0) this.waiters.shift()?.({
done: true,
value: void 0
});
this.resumeResolve?.();
this.resumeResolve = void 0;
}
async unsubscribe() {
if (this.closed) return;
this.close();
await this.onUnsubscribe(this.subscriptionId);
}
[Symbol.asyncIterator]() {
return {
next: async () => {
if (this.queue.length > 0) return {
done: false,
value: this.queue.shift()
};
if (this.closed || this.paused) return {
done: true,
value: void 0
};
return await new Promise((resolve) => {
this.waiters.push(resolve);
});
},
return: async () => {
this.close();
return {
done: true,
value: void 0
};
}
};
}
};
/**
* High-level wrapper around a protocol connection to a specific thread.
*
* In the thread-centric protocol, threads are durable (backed by
* checkpoints) and connections are ephemeral. A `ThreadStream` is the
* client-side handle for interacting with a thread: starting runs,
* subscribing to events, consuming assembled projections (`messages`,
* `values`, `toolCalls`, etc.), and responding to interrupts.
*
* Construct via `client.threads.stream(threadId?, { assistantId? })`.
*
* @typeParam TExtensions - Optional map of `{ name: payload }` pairs
* describing the transformer projections the bound assistant exposes
* on `custom:<name>` channels. Narrows `thread.extensions.<name>` to
* `ThreadExtension<payload>`. Defaults to `Record<string, unknown>`.
*/
var ThreadStream = class {
threadId;
ordering = {};
run;
agent;
input;
state;
/**
* Whether the run was interrupted (a lifecycle "interrupted" event
* was received). Mirrors the in-process `run.interrupted`.
*/
interrupted = false;
/**
* Interrupt payloads collected during the run, if any.
* Mirrors the in-process `run.interrupts`.
*/
interrupts = [];
assistantId;
#nextCommandId;
#transportAdapter;
#pending = /* @__PURE__ */ new Map();
#subscriptions = /* @__PURE__ */ new Map();
#seenEventIds = /* @__PURE__ */ new Set();
/**
* Headless tool interrupts can be auto-resumed by the React hook before
* the shared SSE content pump has processed the root `interrupted`
* lifecycle event. `respondInput()` clears `interrupts`, so keep a
* short-lived marker here until that stale terminal passes through the
* content pump and we can avoid pausing it.
*/
#headlessInterruptsAwaitingTerminal = /* @__PURE__ */ new Set();
#closed = false;
#opened = false;
#openPromise;
#sharedStream = null;
#sharedStreamFilter = null;
#rotationState = "idle";
/** Pending `subscribe()` promises waiting for a covering rotation. */
#pendingSubResolves = [];
#terminalPauseTimer;
#terminalPauseSeq;
#lifecycleSubId = null;
#lifecycleStartPromise;
#runStartReady = null;
#lifecycleWatcherHandle = null;
#lifecycleWatcherStartPromise;
#onEventListeners = /* @__PURE__ */ new Set();
#onErrorListeners = /* @__PURE__ */ new Set();
#messagesIterable;
#valuesProjection;
#toolCallsIterable;
#subgraphsIterable;
#subagentsIterable;
#outputPromise;
#extensionsProxy;
#extensionsCache = /* @__PURE__ */ new Map();
/**
* Shared state for the single `"custom"` channel subscription that
* backs every `thread.extensions.<name>` handle.
*
* One subscription is opened eagerly from {@link run.start} (mirroring
* the {@link values} eager-start pattern) so that per-name handles
* created before, during, or after the run can all resolve correctly.
*
* - `events` retains every custom event for backfill into
* late-constructed handles.
* - `eventListeners` fan new events out to live per-name handlers.
* - `endListeners` fire when the dispatcher's run terminates, so each
* handle can resolve its `PromiseLike` side with its last-seen
* payload.
*/
#extensionsDispatcherStarted = false;
#extensionsEnded = false;
#extensionsEvents = [];
#extensionsEventListeners = [];
#extensionsEndListeners = [];
/**
* Shared state for the single `messages`-channel subscription that
* backs every media handle iterable (`thread.audio`, `thread.images`,
* `thread.video`, `thread.files`). One subscription serves all four
* iterables; per-type buffers track the handles already emitted so
* late attachers replay through {@link MultiCursorBuffer}.
*/
#mediaDispatcherStarted = false;
#mediaAssembler;
/** Object URLs minted by media handles, tracked for {@link close} cleanup. */
#mediaHandles = /* @__PURE__ */ new Set();
#audioBuffer = new require_multi_cursor_buffer.MultiCursorBuffer();
#imagesBuffer = new require_multi_cursor_buffer.MultiCursorBuffer();
#videoBuffer = new require_multi_cursor_buffer.MultiCursorBuffer();
#filesBuffer = new require_multi_cursor_buffer.MultiCursorBuffer();
#fetchOption;
constructor(transportAdapter, options) {
if (!options?.assistantId) throw new Error("ThreadStream requires an assistantId option.");
this.#transportAdapter = transportAdapter;
this.threadId = transportAdapter.threadId;
this.assistantId = options.assistantId;
this.#nextCommandId = options.startingCommandId ?? 1;
this.#fetchOption = options.fetch;
this.run = { start: async (params) => {
this.#prepareForNextRun();
return await this.#withRunStartGate(() => {
this.#ensureLifecycleTracking();
this.values;
return this.#send("run.start", {
...foldForkFromIntoConfig(params),
assistant_id: this.assistantId
});
});
} };
this.agent = { getTree: async (params = {}) => await this.#send("agent.getTree", params) };
this.input = {
respond: async (params) => {
this.#prepareForNextRun();
this.#ensureLifecycleTracking();
this.values;
await this.#send("input.respond", params);
},
inject: async (params) => {
await this.#send("input.inject", params);
}
};
this.state = {
get: async (params) => await this.#send("state.get", params),
listCheckpoints: async (params) => await this.#send("state.listCheckpoints", params),
fork: async (params) => await this.#send("state.fork", params)
};
if (this.#transportAdapter.openEventStream == null) {
this.#transportAdapter.setOnReconnected?.(() => this.#resubscribeWebSocketSubscriptions());
this.#consumeEvents();
}
}
/**
* Ensure the underlying transport is connected.
*
* For HTTP/SSE this is a no-op. For WebSocket this performs the
* handshake. Called lazily on first command; safe to call multiple times.
*/
async #ensureOpen() {
if (this.#opened) return;
if (this.#openPromise == null) this.#openPromise = this.#transportAdapter.open().then(() => {
this.#opened = true;
});
await this.#openPromise;
}
/**
* Channels bundled into every lazy getter's SSE filter so that
* interrupt tracking works without a separate lifecycle subscription.
*/
#lifecycleChannels() {
return ["lifecycle", "input"];
}
/**
* Lazily start a dedicated lifecycle+input subscription so that
* `thread.interrupted` / `thread.interrupts` work even when the
* caller never accesses a lazy getter (e.g. they only call
* `run.start` and `subscribe({ channels: ["custom:..."] })`).
*
* Idempotent and fire-and-forget — invoked from `run.start` and
* `input.respond`.
*/
#ensureLifecycleTracking() {
if (this.#lifecycleStartPromise != null) return;
this.#lifecycleStartPromise = (async () => {
this.#lifecycleSubId = (await this.#subscribeRaw({ channels: this.#lifecycleChannels() })).subscriptionId;
})().catch(() => void 0);
}
/**
* Run `operation` (a `run.start` send) while holding the run-start
* gate. Sets `#runStartReady` before invoking `operation` so any
* subscription kicked off synchronously inside it (e.g. the lifecycle
* watcher and the values projection) sees the gate when it eventually
* reaches `#startLifecycleWatcherSse` / `#reconcileStream` /
* `#subscribeViaCommand` and awaits it. The gate resolves the moment
* `operation` settles, so server-side subscribes land immediately
* after the thread is committed.
*/
async #withRunStartGate(operation) {
let resolveGate;
let rejectGate;
const gate = new Promise((resolve, reject) => {
resolveGate = resolve;
rejectGate = reject;
});
this.#runStartReady = gate;
gate.catch(() => void 0);
try {
const result = await operation();
resolveGate();
return result;
} catch (err) {
rejectGate(err);
throw err;
} finally {
if (this.#runStartReady === gate) this.#runStartReady = null;
}
}
/**
* Reset interrupt state and resume all paused user subscriptions.
* Called before `run.start()` and `input.respond()` so that
* iterators on the same handle pick up the next run's events.
*
* @param respondedInterruptId - When responding to one of several
* pending interrupts, only that entry is removed. Clearing the
* full list here would drop other headless-tool interrupts that
* are still awaiting client execution.
*/
#prepareForNextRun(respondedInterruptId) {
this.interrupted = false;
if (respondedInterruptId != null) {
const respondedIds = new Set(Array.isArray(respondedInterruptId) ? respondedInterruptId : [respondedInterruptId]);
for (let index = this.interrupts.length - 1; index >= 0; index -= 1) if (respondedIds.has(this.interrupts[index].interruptId)) this.interrupts.splice(index, 1);
} else this.interrupts.length = 0;
if (this.#terminalPauseTimer != null) {
clearTimeout(this.#terminalPauseTimer);
this.#terminalPauseTimer = void 0;
}
this.#terminalPauseSeq = void 0;
for (const [id, subscription] of this.#subscriptions) if (id !== this.#lifecycleSubId) subscription.resume();
}
/**
* Streaming messages. Each `for await` loop gets an independent cursor
* over the shared buffer; late consumers see all previously emitted
* messages. Mirrors the in-process `run.messages`.
*/
get messages() {
if (this.#messagesIterable) return this.#messagesIterable;
const buffer = new require_multi_cursor_buffer.MultiCursorBuffer();
this.#messagesIterable = buffer;
const assembler = new require_messages$1.StreamingMessageAssembler();
this.#startProjection(["messages", ...this.#lifecycleChannels()], (event) => {
if (isRootTerminalLifecycle(event)) {
assembler.abandonActive();
return;
}
if (event.method !== "messages") return;
const msg = assembler.consume(event);
if (msg) buffer.push(require_messages$1.toStreamingMessageHandle(msg));
}, () => {
assembler.abandonActive();
buffer.close();
});
return buffer;
}
/**
* State values. Iterable for intermediate snapshots; also
* `PromiseLike` — `await thread.values` resolves with the final
* state. Mirrors the in-process `run.values`.
*/
get values() {
if (this.#valuesProjection) return this.#valuesProjection;
const buffer = new require_multi_cursor_buffer.MultiCursorBuffer();
let lastValue;
let resolveOutput;
const outputPromise = new Promise((resolve) => {
resolveOutput = resolve;
});
this.#outputPromise = outputPromise;
const projection = Object.assign(buffer, { then: (onfulfilled, onrejected) => outputPromise.then(onfulfilled, onrejected) });
this.#valuesProjection = projection;
this.#startProjection(["values", ...this.#lifecycleChannels()], (event) => {
if (event.method !== "values") return;
const data = coerceStateMessages(event.params.data);
lastValue = data;
buffer.push(data);
}, () => {
resolveOutput(lastValue);
buffer.close();
});
return projection;
}
/**
* Tool calls with a promise-based {@link output} for script consumers.
* Mirrors the in-process `run.toolCalls`.
*/
get toolCalls() {
if (this.#toolCallsIterable) return this.#toolCallsIterable;
const buffer = new require_multi_cursor_buffer.MultiCursorBuffer();
this.#toolCallsIterable = buffer;
const assembler = new require_tools.ToolCallAssembler();
this.#startProjection(["tools", ...this.#lifecycleChannels()], (event) => {
if (event.method !== "tools") return;
const tc = assembler.consume(event);
if (tc) buffer.push(require_tools.toClientAssembledToolCall(tc));
}, () => buffer.close());
return buffer;
}
/**
* Discovered subgraphs. Mirrors the in-process `run.subgraphs`.
*/
get subgraphs() {
if (this.#subgraphsIterable) return this.#subgraphsIterable;
const buffer = new require_multi_cursor_buffer.MultiCursorBuffer();
this.#subgraphsIterable = buffer;
(async () => {
const discovery = new require_subgraphs.SubgraphDiscoveryHandle(await this.#subscribeRaw({ channels: ["tools", ...this.#lifecycleChannels()] }), this, []);
for await (const sub of discovery) buffer.push(sub);
buffer.close();
})();
return buffer;
}
/**
* Discovered subagents.
*/
get subagents() {
if (this.#subagentsIterable) return this.#subagentsIterable;
const buffer = new require_multi_cursor_buffer.MultiCursorBuffer();
this.#subagentsIterable = buffer;
(async () => {
const discovery = new require_subagents.SubagentDiscoveryHandle(await this.#subscribeRaw({ channels: ["tools", ...this.#lifecycleChannels()] }), this);
for await (const sub of discovery) buffer.push(sub);
buffer.close();
})();
return buffer;
}
/**
* Audio media handles, one per message containing at least one
* `AudioBlock`. Each `for await` opens an independent cursor over
* the shared buffer; late consumers replay every previously emitted
* audio handle.
*
* Yields one item per message on the first matching
* `content-block-start` — messages with no audio blocks are skipped.
*/
get audio() {
this.#ensureMediaDispatcher();
return this.#audioBuffer;
}
/**
* Image media handles, one per message containing at least one
* `ImageBlock`. See {@link audio} for shared semantics.
*/
get images() {
this.#ensureMediaDispatcher();
return this.#imagesBuffer;
}
/**
* Video media handles, one per message containing at least one
* `VideoBlock`. See {@link audio} for shared semantics.
*/
get video() {
this.#ensureMediaDispatcher();
return this.#videoBuffer;
}
/**
* File media handles, one per message containing at least one
* `FileBlock`. See {@link audio} for shared semantics.
*/
get files() {
this.#ensureMediaDispatcher();
return this.#filesBuffer;
}
/**
* Promise that resolves with the final state value when the run
* completes. Shares the `values` getter's SSE connection.
* Mirrors the in-process `run.output`.
*/
get output() {
this.values;
return this.#outputPromise;
}
/**
* Proxy over compile-time {@link StreamTransformer} projections
* exposed by the bound assistant on `custom:<name>` channels.
*
* Each access (e.g. `thread.extensions.toolActivity`) lazily opens a
* dedicated `custom:<name>` subscription, returns a cached
* {@link ThreadExtension} handle that is both `AsyncIterable<T>`
* (streaming items as they arrive) and `PromiseLike<T>` (resolves
* with the final value when the run terminates), and reuses the same
* handle on subsequent access.
*
* Mirrors the in-process `run.extensions.<name>` shape.
*/
get extensions() {
if (this.#extensionsProxy) return this.#extensionsProxy;
const cache = this.#extensionsCache;
const createExtension = (name) => this.#createExtension(name);
this.#extensionsProxy = new Proxy(Object.create(null), {
get: (_target, prop) => {
if (typeof prop !== "string") return void 0;
const cached = cache.get(prop);
if (cached) return cached;
const extension = createExtension(prop);
cache.set(prop, extension);
return extension;
},
has: (_target, prop) => typeof prop === "string"
});
return this.#extensionsProxy;
}
/**
* Lazily open one shared subscription on the `custom` channel that
* buffers every custom event for this run and fans it out to any
* per-name extension handles.
*
* Deliberately **lazy**: the dispatcher only starts on first access
* to `thread.extensions.<name>`. Runs that never touch extensions
* pay no subscription cost. Runs that touch extensions after events
* have already fired rely on the server's per-session event buffer,
* which replays matching events to new subscriptions.
*
* Each handle retains a PromiseLike that resolves with the
* transformer's last-observed payload, independent of when the
* caller grabs the handle (before, during, or after the run), as
* long as the server still has the events buffered.
*
* Idempotent. Invoked only from {@link #createExtension}.
*/
#ensureExtensionsDispatcher() {
if (this.#extensionsDispatcherStarted) return;
this.#extensionsDispatcherStarted = true;
this.#startProjection(["custom", ...this.#lifecycleChannels()], (event) => {
if (event.method !== "custom") return;
this.#extensionsEvents.push(event);
for (const listener of this.#extensionsEventListeners) listener(event);
}, () => {
this.#extensionsEnded = true;
const listeners = this.#extensionsEndListeners.splice(0);
for (const listener of listeners) listener();
}, { endOnRootTerminal: true });
}
/**
* Open the single shared `messages`-channel subscription that backs
* every media iterable (audio/images/video/files). Idempotent.
*
* The {@link MediaAssembler} fans out to four per-type
* {@link MultiCursorBuffer}s; each buffer feeds its corresponding
* lazy getter. One handle is yielded per `(messageId, blockType)` on
* the first matching `content-block-start`, so messages without any
* media blocks of a given type never appear on that iterable.
*/
#ensureMediaDispatcher() {
if (this.#mediaDispatcherStarted) return;
this.#mediaDispatcherStarted = true;
const assembler = new require_media.MediaAssembler({
fetch: this.#fetchOption,
onAudio: (m) => {
this.#mediaHandles.add(m);
this.#audioBuffer.push(m);
},
onImage: (m) => {
this.#mediaHandles.add(m);
this.#imagesBuffer.push(m);
},
onVideo: (m) => {
this.#mediaHandles.add(m);
this.#videoBuffer.push(m);
},
onFile: (m) => {
this.#mediaHandles.add(m);
this.#filesBuffer.push(m);
}
});
this.#mediaAssembler = assembler;
this.#startProjection(["messages", ...this.#lifecycleChannels()], (event) => {
if (event.method !== "messages") return;
assembler.consume(event);
}, () => {
assembler.close();
this.#audioBuffer.close();
this.#imagesBuffer.close();
this.#videoBuffer.close();
this.#filesBuffer.close();
});
}
/**
* Build a single {@link ThreadExtension} handle for a named
* `custom:<name>` projection.
*
* The handle reads from the shared extensions dispatcher: past events
* matching {@link name} are backfilled on construction, future events
* arrive via a registered listener, and the handle's `PromiseLike`
* side resolves with its last-seen payload once the run terminates
* (which may already have happened, in which case it resolves on the
* next microtask).
*/
#createExtension(name) {
this.#ensureExtensionsDispatcher();
const buffer = new require_multi_cursor_buffer.MultiCursorBuffer();
let lastValue;
let resolveFinal;
const finalPromise = new Promise((resolve) => {
resolveFinal = resolve;
});
const handleEvent = (event) => {
const data = event.params.data;
if (data?.name !== name) return;
lastValue = data.payload;
buffer.push(data.payload);
};
for (const event of this.#extensionsEvents) handleEvent(event);
this.#extensionsEventListeners.push(handleEvent);
const settle = () => {
resolveFinal(lastValue);
buffer.close();
};
if (this.#extensionsEnded) settle();
else this.#extensionsEndListeners.push(settle);
return Object.assign(buffer, { then: (onfulfilled, onrejected) => finalPromise.then(onfulfilled, onrejected) });
}
/**
* Generic projection starter: opens a raw subscription with the given
* channels, feeds events through the consumer, and calls onDone when
* the stream ends.
*
* When `endOnRootTerminal` is set, the projection unsubscribes its
* own handle one macrotask after observing a root-namespace terminal
* lifecycle event. This is needed by projections that may be opened
* AFTER a run already terminated: the shared-stream pause logic
* skips subscriptions whose `registeredAfterSeq` is past the
* terminal so raw `subscribe()` callers can keep draining replayed
* descendants — but a per-run dispatcher (e.g. the extensions
* pipeline) needs the projection to settle so its `PromiseLike`
* surface resolves. The macrotask deferral mirrors the deferred
* pause in `#handleIncoming`, giving trailing same-tick custom
* events (transformer `finalize()` flushes) a chance to drain.
*/
async #startProjection(channels, onEvent, onDone, options = {}) {
let endTimer;
let rawHandle;
try {
rawHandle = await this.#subscribeRaw({ channels });
const handle = rawHandle;
for await (const event of handle) {
onEvent(event);
if (options.endOnRootTerminal && endTimer == null && isRootTerminalLifecycle(event)) endTimer = setTimeout(() => {
endTimer = void 0;
handle.unsubscribe().catch(() => void 0);
}, 0);
}
} catch {} finally {
if (endTimer != null) clearTimeout(endTimer);
onDone();
}
}
/**
* Start a run without the v1 eager lazy-getter shims.
*
* `run.start` (the v1 entry point) eagerly opens a wildcard `values`
* projection so `thread.output` / `thread.values` resolve regardless
* of access order, and calls `#ensureLifecycleTracking` which opens
* another wildcard `["lifecycle", "input"]` subscription. Both
* subscriptions widen `#computeUnionFilter` to wildcard, defeating
* the progressive-expansion rotation strategy.
*
* `submitRun` skips those shims — callers that manage their own
* content subscriptions (such as `StreamController`) get the narrow
* union filter they asked for. Lifecycle / interrupt tracking is
* instead served by the dedicated `#startLifecycleWatcher`, which
* opens a wildcard `["lifecycle", "input"]` stream alongside the
* narrow content pump on both SSE and WebSocket transports.
*/
async submitRun(params) {
this.#prepareForNextRun();
return await this.#withRunStartGate(() => {
this.#startLifecycleWatcher();
return this.#send("run.start", {
...foldForkFromIntoConfig(params),
assistant_id: this.assistantId
});
});
}
/**
* Respond to an interrupt without the v1 eager lazy-getter shims.
* See {@link submitRun} for why this exists alongside
* {@link input.respond}.
*/
async respondInput(params) {
const respondedIds = "responses" in params ? params.responses.map((entry) => entry.interrupt_id) : params.interrupt_id;
this.#prepareForNextRun(respondedIds);
this.#startLifecycleWatcher();
await this.#send("input.respond", params);
}
/**
* Register a listener for every globally-unique event on the thread.
*
* Fires exactly once per `event_id` across both the content pump
* (user `subscribe()` calls) and the lifecycle watcher. Events
* without an `event_id` always fire through (dedup is best-effort).
*
* Returns an unsubscribe function. Primary consumer is
* `StreamController`, which uses the listener to feed discovery
* runners and pick up deeply-nested interrupts that the narrow
* content pump wouldn't deliver.
*/
onEvent(listener) {
this.#onEventListeners.add(listener);
return () => {
this.#onEventListeners.delete(listener);
};
}
/**
* Register a listener for stream-level failures: an unsolicited server
* error frame, or the transport giving up on reconnecting. Returns an
* unsubscribe function.
*/
onError(listener) {
this.#onErrorListeners.add(listener);
return () => {
this.#onErrorListeners.delete(listener);
};
}
/**
* Lazily open the wildcard discovery watcher stream.
*
* Idempotent. Used by both transports, but through different
* mechanisms:
*
* - **SSE**: opens a dedicated event stream via
* {@link TransportAdapter.openEventStream}. The stream runs
* outside `#computeUnionFilter`, so the shared SSE stream's
* content pump can stay narrow (e.g. `depth: 1`) while we still
* capture every lifecycle/input event at any depth.
* - **WebSocket**: opens a wildcard watcher subscription
* subscription via the normal command path. The WS server
* delivers matching events on the shared command connection and
* `#handleIncoming` dispatches them through `#fireOnEvent` and
* the thread-level effects — same downstream semantics as the
* SSE watcher, just reusing the transport that's already open.
*
* Why this matters: consumers of {@link onEvent} (notably
* `StreamController`'s subgraph/subagent discovery runners and
* nested interrupt capture) depend on observing namespaced
* lifecycle events at any depth. Without this watcher, WS clients
* would only ever receive events matching the content pump's
* narrow filter (depth 1 from the root), breaking inference rules
* that require deeper descendants (e.g. the "has-descendants"
* signal used to promote a subgraph host).
*/
#startLifecycleWatcher() {
if (this.#lifecycleWatcherStartPromise != null) return;
if (this.#transportAdapter.openEventStream != null) {
this.#lifecycleWatcherStartPromise = this.#startLifecycleWatcherSse();
return;
}
this.#lifecycleWatcherStartPromise = this.#startLifecycleWatcherWebSocket();
}
/**
* Public, idempotent entry point to start the wildcard lifecycle
* watcher.
*
* The watcher is normally started lazily by `submitRun` /
* `respondInput` because for fresh (self-created) threads the SSE
* stream would 404 if opened before the server has the thread row.
* Callers that already know the thread exists server-side
* (`StreamController.hydrate` of an existing thread) can use this
* to start the watcher up front. The watcher subscribes to wildcard
* lifecycle events across every namespace, so it sees arbitrarily-
* nested subagent lifecycle messages that the narrow root content
* pump (running at `depth: 1`) wouldn't reach — that's what makes
* subagent discovery work for historical thread loads.
*
* Idempotent — repeat calls reuse the in-flight start promise.
*/
startLifecycleWatcher() {
this.#startLifecycleWatcher();
}
async #startLifecycleWatcherSse() {
if (this.#runStartReady != null) try {
await this.#runStartReady;
} catch {
return;
}
const filter = { channels: ["lifecycle", "input"] };
let handle;
try {
handle = this.#transportAdapter.openEventStream(filter);
} catch {
return;
}
try {
await handle.ready;
} catch {
try {
handle.close();
} catch {}
return;
}
if (this.#closed) {
try {
handle.close();
} catch {}
return;
}
this.#lifecycleWatcherHandle = handle;
try {
for await (const message of handle.events) {
if (this.#closed) break;
this.#handleLifecycleWatcherMessage(message);
}
} catch {}
}
async #startLifecycleWatcherWebSocket() {
let handle;
try {
handle = await this.#subscribeRaw({ channels: ["lifecycle", "input"] });
} catch {
return;
}
if (this.#closed) {
try {
handle.close();
} catch {}
return;
}
try {
for await (const _event of handle) if (this.#closed) break;
} catch {}
}
/**
* Process an event from the dedicated lifecycle watcher stream.
*
* Unlike `#handleIncoming`, this does NOT fan out to user
* subscriptions — user subs with namespace wildcards already widen
* `#computeUnionFilter` and therefore receive the event on the
* content pump. Delivering via both streams would only add per-sub
* dedup churn without expanding what the user can observe.
*
* We still run global-dedup thread-level side effects (interrupt
* capture, `onEvent` fan-out) so deeply-nested interrupts outside
* the content pump's narrow scope are recorded.
*/
#handleLifecycleWatcherMessage(message) {
if (message.type !== "event") return;
if (typeof message.seq === "number") this.ordering.lastSeenSeq = maxSeq(this.ordering.lastSeenSeq, message.seq);
if (message.event_id) this.ordering.lastEventId = message.event_id;
const eventId = message.event_id ?? void 0;
const globallyProcessed = eventId != null && this.#seenEventIds.has(eventId);
if (eventId != null) this.#seenEventIds.add(eventId);
if (globallyProcessed) return;
this.#applyThreadLevelEffects(message);
this.#fireOnEvent(message);
}
#applyThreadLevelEffects(event) {
if (event.method === "lifecycle") {
if (event.params.data.event === "interrupted") this.interrupted = true;
}
if (event.method === "input.requested") {
const data = event.params.data;
const interruptId = data.interrupt_id ?? `interrupt_${this.interrupts.length}`;
this.interrupts.push({
interruptId,
payload: data.payload,
namespace: [...event.params.namespace]
});
if (require_headless_tools.isHeadlessToolInterrupt(data.payload)) this.#headlessInterruptsAwaitingTerminal.add(interruptId);
}
}
#fireOnEvent(event) {
if (this.#onEventListeners.size === 0) return;
for (const listener of this.#onEventListeners) try {
listener(event);
} catch {}
}
#fireOnError(error) {
for (const listener of this.#onErrorListeners) try {
listener(error);
} catch {}
}
async close() {
if (this.#closed) return;
this.#closed = true;
if (this.#terminalPauseTimer != null) {
clearTimeout(this.#terminalPauseTimer);
this.#terminalPauseTimer = void 0;
}
this.#terminalPauseSeq = void 0;
for (const pending of this.#pendingSubResolves) pending.reject(/* @__PURE__ */ new Error("ThreadStream closed"));
this.#pendingSubResolves.length = 0;
if (this.#sharedStream != null) {
try {
this.#sharedStream.close();
} catch {}
this.#sharedStream = null;
this.#sharedStreamFilter = null;
}
if (this.#lifecycleWatcherHandle != null) {
try {
this.#lifecycleWatcherHandle.close();
} catch {}
this.#lifecycleWatcherHandle = null;
}
const lifecycleWatcherStartPromise = this.#lifecycleWatcherStartPromise;
this.#lifecycleWatcherStartPromise = void 0;
this.#onEventListeners.clear();
this.#onErrorListeners.clear();
for (const subscription of this.#subscriptions.values()) subscription.close();
this.#subscriptions.clear();
try {
await lifecycleWatcherStartPromise;
} catch {}
for (const handle of this.#mediaHandles) try {
handle.revoke();
} catch {}
this.#mediaHandles.clear();
this.#mediaAssembler?.close();
this.#audioBuffer.close();
this.#imagesBuffer.close();
this.#videoBuffer.close();
this.#filesBuffer.close();
await this.#transportAdapter.close();
}
async subscribe(paramsOrChannels, options = {}) {
const isParamsObject = typeof paramsOrChannels === "object" && !Array.isArray(paramsOrChannels) && "channels" in paramsOrChannels;
const params = normalizeSubscribeParams(paramsOrChannels, options);
return await this.#subscribeRaw(params, { unwrapNamedCustom: !isParamsObject });
}
async #subscribeRaw(params, options = {}) {
await this.#ensureOpen();
const { unwrapNamedCustom = true } = options;
const hasOnlyNamedCustom = params.channels.length > 0 && params.channels.every((ch) => ch.startsWith("custom:"));
const transform = unwrapNamedCustom && hasOnlyNamedCustom ? (event) => event.params.data?.payload ?? event : void 0;
if (this.#transportAdapter.openEventStream != null) return this.#subscribeViaSharedStream(params, transform);
return this.#subscribeViaCommand(params, transform);
}
/**
* Subscribe via the single shared SSE connection.
*
* The subscription is registered immediately in `#subscriptions` so
* fan-out can reach it the moment events begin flowing. The returned
* promise resolves after a stream rotation completes whose union
* filter covers this subscription's channels — mirroring the per-sub
* `await streamHandle.ready` semantics callers depended on.
*
* Every subscribe schedules a stream rotation, even when the current
* stream's filter already covers `params`. Rotating opens a fresh
* server-side session that replays the run's full history from
* `seq=0`; without it a late-joining sub would only see events that
* arrive after it registered, because the shared pump's dedup drops
* events the existing sub already consumed. Per-sub dedup
* (`seenEventIds`) protects existing subs from receiving the
* replay as duplicates. Rapid subscribes in the same microtask are
* coalesced by `#scheduleReconcile` into a single rotation.
*/
async #subscribeViaSharedStream(params, transform) {
const subscriptionId = `sse-${this.#nextCommandId++}`;
const handle = new SubscriptionHandle(subscriptionId, params, async (id) => {
this.#subscriptions.delete(id);
this.#scheduleReconcile();
}, transform);
const subscription = Object.assign(handle, {
filter: params,
registeredAfterSeq: this.ordering.lastSeenSeq,
seenEventIds: /* @__PURE__ */ new Set()
});
this.#subscriptions.set(subscriptionId, subscription);
const covered = new Promise((resolve, reject) => {
this.#pendingSubResolves.push({
filter: params,
resolve,
reject
});
});
this.#scheduleReconcile();
try {
await covered;
} catch (err) {
this.#subscriptions.delete(subscriptionId);
throw err;
}
return handle;
}
/**
* Progressive-expansion union of every currently-registered
* subscription's filter. The server receives the narrowest filter
* that still covers every active sub so deeply-namespaced or
* selectively-opened projections don't pull down the entire thread's
* event firehose.
*
* Unioning rules (matching the server's matching semantics in
* `matchesSinkFilter`):
* - Channels: set union.
* - Namespaces: if any subscription requests a wildcard
* (`namespaces === undefined`) the union is wildcard; otherwise
* the union is the deduplicated list of every explicit prefix.
* - Depth: if any subscription is unbounded (`depth === undefined`)
* the union is unbounded; otherwise the union is the maximum
* depth across all subscriptions (matching the per-sub "max
* reach below the prefix" semantics).
*
* Returns `null` when there are no subscriptions.
*/
#computeUnionFilter() {
if (this.#subscriptions.size === 0) return null;
const channels = /* @__PURE__ */ new Set();
let wildcardNamespaces = false;
const namespaceMap = /* @__PURE__ */ new Map();
let unboundedDepth = false;
let maxDepth = 0;
for (const sub of this.#subscriptions.values()) {
for (const ch of sub.filter.channels) channels.add(ch);
if (sub.filter.namespaces == null) wildcardNamespaces = true;
else if (!wildcardNamespaces) for (const ns of sub.filter.namespaces) namespaceMap.set(namespaceKey(ns), ns);
if (sub.filter.depth == null) unboundedDepth = true;
else if (!unboundedDepth && sub.filter.depth > maxDepth) maxDepth = sub.filter.depth;
}
const result = { channels: [...channels] };
if (!wildcardNamespaces) result.namespaces = [...namespaceMap.values()];
if (!unboundedDepth) result.depth = maxDepth;
return result;
}
/**
* Schedule a stream reconciliation for the next microtask.
*
* Coalesces multiple subscribe/unsubscribe calls in the same tick
* into a single rotation, and serializes across ticks (no two
* rotations ever run concurrently).
*/
#scheduleReconcile() {
if (this.#closed) return;
if (this.#rotationState !== "idle") return;
this.#rotationState = "scheduled";
queueMicrotask(() => {
if (this.#closed) {
this.#rotationState = "idle";
return;
}
this.#rotationState = "idle";
this.#reconcileStream().catch(() => {
this.#rotationState = "idle";
});
});
}
/**
* Reconcile the shared SSE stream to match the desired union filter.
*
* Rotation strategy: open the new stream first, await its `ready`,
* then close the old one. Overlap is absorbed by `#seenEventIds`
* dedup in `#handleIncoming`.
*
* Error handling:
* - Failure before `ready` resolves: reject all pending `subscribe`
* promises whose filter isn't covered by the existing stream,
* and keep the existing stream running for other subscriptions.
* - Failure mid-pump on the active stream: close the thread via
* {@link #failThreadWithError} so higher layers can rebind.
*/
async #reconcileStream() {
if (this.#closed) return;
if (this.#rotationState === "rotating") return;
const desired = this.#computeUnionFilter();
if (desired == null) return;
if (this.#runStartReady != null) {
try {
await this.#runStartReady;
} catch (err) {
const normalized = err instanceof Error ? err : /* @__PURE__ */ new Error("run.start failed");
this.#rejectUncoveredPending(normalized);
return;
}
if (this.#closed) return;
if (this.#rotationState === "rotating") return;
}
if (this.#sharedStreamFilter != null && filterEqual(desired, this.#sharedStreamFilter) && this.#pendingSubResolves.length === 0) {
this.#resolvePending();
return;
}
this.#rotationState = "rotating";
let newHandle;
try {
newHandle = this.#transportAdapter.openEventStream(desired);
} catch (err) {
this.#rotationState = "idle";
this.#rejectUncoveredPending(err);
return;
}
try {
await newHandle.ready;
} catch (err) {
this.#rotationState = "idle";
try {
newHandle.close();
} catch {}
this.#rejectUncoveredPending(err);
return;
}
if (this.#closed) {
try {
newHandle.close();
} catch {}
this.#rotationState = "idle";
return;
}
this.#pumpStream(newHandle);
const oldHandle = this.#sharedStream;
this.#sharedStream = newHandle;
this.#sharedStreamFilter = desired;
if (oldHandle != null) try {
oldHandle.close();
} catch {}
this.#rotationState = "idle";
this.#resolvePending();
const next = this.#computeUnionFilter();
if (next != null && !filterEqual(next, this.#sharedStreamFilter)) this.#scheduleReconcile();
}
/**
* Pump events from a shared-stream handle into `#handleIncoming`.
* One pump task runs per open stream; during rotation overlap two
* pumps may be active briefly, with `#seenEventIds` deduping.
*/
async #pumpStream(handle) {
try {
for await (const message of handle.events) {
if (this.#closed) break;
this.#handleIncoming(message);
}
} catch (err) {
if (handle === this.#sharedStream && !this.#closed) this.#failThreadWithError(err);
}
}
/**
* Resolve any pending `subscribe()` promises whose filter is now
* covered by the active shared stream. Called after every successful
* rotation (and after no-op reconciliations).
*/
#resolvePending() {
if (this.#sharedStreamFilter == null) return;
const current = this.#sharedStreamFilter;
if (this.#pendingSubResolves.length === 0) return;
const stillPending = [];
for (const pending of this.#pendingSubResolves) if (filterCovers(current, pending.filter)) pending.resolve();
else stillPending.push(pending);
this.#pendingSubResolves.length = 0;
this.#pendingSubResolves.push(...stillPending);
}
/**
* Reject pending `subscribe()` promises whose filter isn't covered
* by the existing stream (they're the ones that triggered the
* failed rotation). Covered pending subs are resolved normally —
* they didn't need the new stream.
*/
#rejectUncoveredPending(err) {
if (this.#pendingSubResolves.length === 0) return;
const current = this.#sharedStreamFilter;
const stillPending = [];
for (const pending of this.#pendingSubResolves) if (current != null && filterCovers(current, pending.filter)) pending.resolve();
else stillPending.push(pending);
this.#pendingSubResolves.length = 0;
for (const pending of stillPending) pending.reject(err);
}
/**
* Terminate the thread due to an unrecoverable shared-stream error.
* Rejects pending commands, closes subscriptions, and marks the
* thread closed so no further rotations occur.
*/
#failThreadWithError(err) {
const normalized = err instanceof Error ? err : new Error(String(err));
for (const pending of this.#pending.values()) pending.reject(normalized);
this.#pending.clear();
for (const pending of this.#pendingSubResolves) pending.reject(normalized);
this.#pendingSubResolves.length = 0;
for (const subscription of this.#subscriptions.values()) subscription.close();
this.#fireOnError(normalized);
}
/**
* Command-based subscription (WebSocket fallback). The server replays
* matching buffered events on subscribe via the same WebSocket stream.
*/
async #subscribeViaCommand(params, transform) {
const placeholderId = `pending:${this.#nextCommandId}:${Math.random().toString(36).slice(2, 10)}`;
let resolvedId = placeholderId;
const handle = new SubscriptionHandle(placeholderId, params, async () => {
this.#subscriptions.delete(resolvedId);
if (!this.#closed && resolvedId !== placeholderId) await this.#send("subscription.unsubscribe", { subscription_id: resolvedId }).catch((err) => {
if (err instanceof require_error.ProtocolError && err.code === "no_such_subscription") return;
throw err;
});
}, transform);
const subscription = Object.assign(handle, {
filter: params,
registeredAfterSeq: this.ordering.lastSeenSeq,
seenEventIds: /* @__PURE__ */ new Set()
});
this.#subscriptions.set(placeholderId, subscription);
if (this.#runStartReady != null) try {
await this.#runStartReady;
} catch (err) {
this.#subscriptions.delete(placeholderId);
throw err;
}
let result;
try {
result = await this.#send("subscription.subscribe", params);
} catch (err) {
this.#subscriptions.delete(placeholderId);
throw err;
}
this.#subscriptions.delete(placeholderId);
resolvedId = result.subscription_id;
handle.subscriptionId = resolvedId;
this.#subscriptions.set(resolvedId, subscription);
return handle;
}
/**
* Re-issue `subscription.subscribe` for every active WS subscription
* after the transport reconnects. The server replays buffered events on
* the new socket; client-side `event_id` dedup suppresses duplicates.
*/
async #resubscribeWebSocketSubscriptions() {
if (this.#transportAdapter.openEventStream != null || this.#closed) return;
const entries = [...this.#subscriptions.entries()];
await Promise.all(entries.map(async ([id, subscription]) => {
if (id.startsWith("pending:")) return;
try {
const nextId = (await this.#send("subscription.subscribe", subscription.filter)).subscription_id;
if (nextId === id) return;
this.#subscriptions.delete(id);
subscription.subscriptionId = nextId;
this.#subscriptions.set(nextId, subscription);
if (this.#lifecycleSubId === id) this.#lifecycleSubId = nextId;
} catch {}
}));
}
async #consumeEvents() {
try {
for await (const message of this.#transportAdapter.events()) this.#handleIncoming(message);
for (const subscription of this.#subscriptions.values()) subscription.close();
} catch (error) {
this.#failThreadWithError(error);
}
}
/**
* Pause non-lifecycle subscriptions after a root terminal lifecycle.
*
* The pause is deferred one macrotask so same-run trailing events
* emitted immediately after terminal (for example final `values`)
* can still drain. `terminalSeq` lets replay attachers skip terminals
* that happened before they registered, so late subscribers can keep
* consuming the replayed history they joined for.
*/
#scheduleTerminalPause(terminalSeq) {
if (this.#terminalPauseTimer != null) clearTimeout(this.#terminalPauseTimer);
this.#terminalPauseSeq = terminalSeq ?? null;
this.#terminalPauseTimer = setTimeout(() => {
this.#terminalPauseTimer = void 0;
if (this.#closed) return;
for (const [id, subscription] of this.#subscriptions) {
if (id === this.#lifecycleSubId) continue;
if (terminalSeq != null && subscription.registeredAfterSeq != null && subscription.registeredAfterSeq >= terminalSeq) continue;
subscription.pause();
}
}, 0);
}
#handleIncoming(message) {
if (message.type === "event") {
if (typeof message.seq === "number") this.ordering.lastSeenSeq = maxSeq(this.ordering.lastSeenSeq, message.seq);
if (message.event_id) this.ordering.lastEventId = message.event_id;
const eventId = message.event_id ?? void 0;
const globallyProcessed = eventId != null && this.#seenEventIds.has(eventId);
if (eventId != null) this.#seenEventIds.add(eventId);
const TERMINAL_LIFECYCLE_EVENTS = new Set([
"interrupted",
"completed",
"failed"
]);
if (!globallyProcessed) {
this.#applyThreadLevelEffects(message);
this.#fireOnEvent(message);
}
let fannedToAny = false;
for (const subscription of this.#subscriptions.values()) {
if (!require_subscription.matchesSubscription(message, subscription.filter)) continue;
if (eventId != null) {
if (subscription.seenEventIds.has(eventId)) continue;
subscription.seenEventIds.add(eventId);
}
subscription.push(message);
fannedToAny = true;
}
if (fannedToAny && this.#terminalPauseSeq !== void 0) {
const eventSeq = typeof message.seq === "number" ? message.seq : void 0;
const terminalSeq = this.#terminalPauseSeq;
if (terminalSeq === null || eventSeq == null || eventSeq > terminalSeq) {
if (this.#terminalPauseTimer != null) {
clearTimeout(this.#terminalPauseTimer);
this.#terminalPauseTimer = void 0;
}
for (const [id, subscription] of this.#subscriptions) if (id !== this.#lifecycleSubId) subscription.resume();
this.#scheduleTerminalPause(terminalSeq === null ? void 0 : terminalSeq);
}
}
if (fannedToAny && message.method === "lifecycle" && message.params.namespace.length === 0 && TERMINAL_LIFECYCLE_EVENTS.has(message.params.data.event)) {
if (message.params.data.event === "interrupted" && this.#headlessInterruptsAwaitingTerminal.size > 0) {
this.#headlessInterruptsAwaitingTerminal.clear();
return;
}
this.#scheduleTerminalPause(typeof message.seq === "number" ? message.seq : void 0);
}
return;
}
const messageId = typeof message.id === "number" ? message.id : void 0;
const pending = messageId === void 0 ? void 0 : this.#pending.get(messageId);
if (!pending) {
if (message.type === "error" && message.id == null) this.#fireOnError(new require_error.ProtocolError(message));
return;
}
if (messageId !== void 0) this.#pending.delete(messageId);
if (message.type === "error") {
pending.reject(new require_error.ProtocolError(message));
return;
}
if (typeof message.meta?.applied_through_seq === "number") this.ordering.lastAppliedThroughSeq = message.meta.applied_through_seq;
pending.resolve(message);
}
async #send(method, params) {
await this.#ensureOpen();
const id = this.#nextCommandId++;
const command = {
id,
method,
params
};
const responsePromise = new Promise((resolve, reject) => {
this.#pending.set(id, {
resolve,
reject
});
});
let immediate;
try {
immediate = await this.#transportAdapter.send(command);
} catch (error) {
this.#pending.delete(id);
throw error;
}
if (immediate) {
this.#pending.delete(id);
if (immediate.type === "error") throw new require_error.ProtocolError(immediate);
if (typeof immediate.meta?.applied_through_seq === "number") this.ordering.lastAppliedThroughSeq = immediate.meta.applied_through_seq;
return immediate.result;
}
return (await responsePromise).result;
}
};
//#endregion
exports.SubscriptionHandle = SubscriptionHandle;
exports.ThreadStream = ThreadStream;
//# sourceMappingURL=index.cjs.map