@langchain/langgraph-sdk
Version:
Client library for interacting with the LangGraph API
372 lines (371 loc) • 11.3 kB
JavaScript
import { MultiCursorBuffer } from "../multi-cursor-buffer.js";
import { ToolCallAssembler, toClientAssembledToolCall } from "./tools.js";
import { StreamingMessageAssembler } from "../messages.js";
import { MediaAssembler } from "../media.js";
//#region src/client/stream/handles/subgraphs.ts
const TERMINAL_LIFECYCLE_EVENTS = new Set([
"completed",
"failed",
"interrupted"
]);
/** Whether `event` is a terminal lifecycle event for exactly `namespace`. */
function isTerminalLifecycleAt(event, namespace) {
if (event.method !== "lifecycle") return false;
const eventNamespace = event.params.namespace;
if (eventNamespace.length !== namespace.length) return false;
if (!namespace.every((segment, i) => eventNamespace[i] === segment)) return false;
const data = event.params.data;
return data?.event != null && TERMINAL_LIFECYCLE_EVENTS.has(data.event);
}
/**
* Discovered subgraph within a streaming session.
*
* Mirrors the in-process `SubgraphRunStream` pattern: each subgraph
* has `name`, `index`, `namespace`, and lazy getters for projections
* scoped to this subgraph's namespace.
*
* ```ts
* for await (const sub of session.subgraphs) {
* for await (const msg of sub.messages) { ... }
* const state = await sub.output;
* }
* ```
*/
var SubgraphHandle = class {
name;
index;
namespace;
/**
* Non-empty when upstream attached a `cause` to this subgraph's
* `lifecycle.started` event. Population is product-specific and
* performed by stream transformers on the runtime side (e.g.
* deepagents' `SubagentTransformer` emits
* `{ type: "toolCall", tool_call_id }`). Generic clients should
* treat `cause.type` as an open enum — the protocol allows future
* variants (`send`, `edge`, ...) to be forwarded verbatim without
* a SDK bump.
*/
cause;
graphName;
/**
* Raw `tool-started` event that triggered this subgraph, when
* `cause.type === "toolCall"` and the matching event has been
* observed on the `tools` channel.
*/
toolStartedEvent;
#session;
#messagesIterable;
#valuesProjection;
#toolCallsIterable;
#subgraphsIterable;
#subagentsIterable;
#outputPromise;
#mediaDispatcherStarted = false;
#audioBuffer;
#imagesBuffer;
#videoBuffer;
#filesBuffer;
constructor(name, index, namespace, session, options) {
this.name = name;
this.index = index;
this.namespace = namespace;
this.cause = options?.cause;
this.graphName = options?.graphName;
this.toolStartedEvent = options?.toolStartedEvent;
this.#session = session;
}
get messages() {
if (this.#messagesIterable) return this.#messagesIterable;
const buffer = new MultiCursorBuffer();
this.#messagesIterable = buffer;
const assembler = new StreamingMessageAssembler();
this.#startProjection(["messages", "lifecycle"], (event) => {
if (isTerminalLifecycleAt(event, this.namespace)) {
assembler.abandonActive(this.namespace);
return;
}
if (event.method !== "messages") return;
const msg = assembler.consume(event);
if (msg) buffer.push(msg);
}, () => {
assembler.abandonActive();
buffer.close();
});
return buffer;
}
get values() {
if (this.#valuesProjection) return this.#valuesProjection;
const buffer = new 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"], (event) => {
if (event.method !== "values") return;
const data = event.params.data;
lastValue = data;
buffer.push(data);
}, () => {
resolveOutput(lastValue);
buffer.close();
});
return projection;
}
get toolCalls() {
if (this.#toolCallsIterable) return this.#toolCallsIterable;
const buffer = new MultiCursorBuffer();
this.#toolCallsIterable = buffer;
const assembler = new ToolCallAssembler();
this.#startProjection(["tools"], (event) => {
if (event.method !== "tools") return;
const tc = assembler.consume(event);
if (tc) buffer.push(toClientAssembledToolCall(tc));
}, () => buffer.close());
return buffer;
}
get subgraphs() {
if (this.#subgraphsIterable) return this.#subgraphsIterable;
const buffer = new MultiCursorBuffer();
this.#subgraphsIterable = buffer;
(async () => {
const discovery = new SubgraphDiscoveryHandle(await this.#session.subscribe({
channels: ["lifecycle", "tools"],
namespaces: [this.namespace]
}), this.#session, this.namespace);
for await (const sub of discovery) buffer.push(sub);
buffer.close();
})();
return buffer;
}
get subagents() {
if (this.#subagentsIterable) return this.#subagentsIterable;
const buffer = new MultiCursorBuffer();
this.#subagentsIterable = buffer;
(async () => {
const rawHandle = await this.#session.subscribe({
channels: ["tools", "lifecycle"],
namespaces: [this.namespace]
});
const { SubagentDiscoveryHandle: Discovery } = await import("./subagents.js");
const discovery = new Discovery(rawHandle, this.#session);
for await (const sub of discovery) buffer.push(sub);
buffer.close();
})();
return buffer;
}
get audio() {
this.#ensureMediaDispatcher();
return this.#audioBuffer;
}
get images() {
this.#ensureMediaDispatcher();
return this.#imagesBuffer;
}
get video() {
this.#ensureMediaDispatcher();
return this.#videoBuffer;
}
get files() {
this.#ensureMediaDispatcher();
return this.#filesBuffer;
}
get output() {
this.values;
return this.#outputPromise;
}
#ensureMediaDispatcher() {
if (this.#mediaDispatcherStarted) return;
this.#mediaDispatcherStarted = true;
const audio = new MultiCursorBuffer();
const images = new MultiCursorBuffer();
const video = new MultiCursorBuffer();
const files = new MultiCursorBuffer();
this.#audioBuffer = audio;
this.#imagesBuffer = images;
this.#videoBuffer = video;
this.#filesBuffer = files;
const assembler = new MediaAssembler({
onAudio: (m) => audio.push(m),
onImage: (m) => images.push(m),
onVideo: (m) => video.push(m),
onFile: (m) => files.push(m)
});
this.#startProjection(["messages"], (event) => {
if (event.method !== "messages") return;
assembler.consume(event);
}, () => {
assembler.close();
audio.close();
images.close();
video.close();
files.close();
});
}
subscribe(paramsOrChannels, options = {}) {
if (typeof paramsOrChannels === "object" && !Array.isArray(paramsOrChannels) && "channels" in paramsOrChannels) return this.#session.subscribe({
...paramsOrChannels,
namespaces: paramsOrChannels.namespaces ?? [this.namespace]
});
return this.#session.subscribe(paramsOrChannels, {
...options,
namespaces: options.namespaces ?? [this.namespace]
});
}
async #startProjection(channels, onEvent, onDone) {
try {
const rawHandle = await this.#session.subscribe({
channels,
namespaces: [this.namespace]
});
for await (const event of rawHandle) onEvent(event);
} finally {
onDone();
}
}
};
/**
* Async iterable that yields {@link SubgraphHandle} instances as new
* subgraph namespaces are discovered from `lifecycle` events.
*
* Mirrors the in-process `run.subgraphs` pattern. A new subgraph is
* discovered when a `lifecycle` event with `event: "started"` is
* received at a namespace depth of exactly `parentDepth + 1`.
*/
var SubgraphDiscoveryHandle = class {
#source;
#session;
#parentNamespace;
#discovered = /* @__PURE__ */ new Set();
#pendingToolStarts = /* @__PURE__ */ new Map();
#pendingToolCallHandles = /* @__PURE__ */ new Map();
#queue = [];
#waiters = [];
#sourcePump;
#closed = false;
constructor(source, session, parentNamespace = []) {
this.#source = source;
this.#session = session;
this.#parentNamespace = parentNamespace;
}
#emit(handle) {
const waiter = this.#waiters.shift();
if (waiter) waiter({
done: false,
value: handle
});
else this.#queue.push(handle);
}
#processToolEvent(event) {
if (event.method !== "tools") return false;
const tools = event;
const data = tools.params.data;
if (data.event !== "tool-started") return true;
const toolCallId = data.tool_call_id;
if (!toolCallId) return true;
const pendingHandle = this.#pendingToolCallHandles.get(toolCallId);
if (pendingHandle) {
pendingHandle.toolStartedEvent = tools;
this.#pendingToolCallHandles.delete(toolCallId);
return true;
}
this.#pendingToolStarts.set(toolCallId, tools);
return true;
}
#processEvent(event) {
if (this.#processToolEvent(event)) return void 0;
if (event.method !== "lifecycle") return void 0;
const lifecycle = event;
if (lifecycle.params.data.event !== "started") return void 0;
const ns = event.params.namespace;
if (ns.length !== this.#parentNamespace.length + 1) return void 0;
if (!this.#parentNamespace.every((seg, i) => ns[i] === seg)) return void 0;
const nsKey = ns.join("/");
if (this.#discovered.has(nsKey)) return void 0;
this.#discovered.add(nsKey);
const lastSegment = ns[ns.length - 1] ?? "";
const colonIdx = lastSegment.lastIndexOf(":");
let name;
let index;
if (colonIdx >= 0) {
name = lastSegment.slice(0, colonIdx);
const suffix = lastSegment.slice(colonIdx + 1);
index = /^\d+$/.test(suffix) ? Number(suffix) : 0;
} else {
name = lastSegment;
index = 0;
}
const data = lifecycle.params.data;
const cause = data.cause && typeof data.cause === "object" ? data.cause : void 0;
let toolStartedEvent;
if (cause?.type === "toolCall") {
const toolCallId = cause.tool_call_id;
if (toolCallId) {
toolStartedEvent = this.#pendingToolStarts.get(toolCallId);
this.#pendingToolStarts.delete(toolCallId);
}
}
const handle = new SubgraphHandle(name, index, [...ns], this.#session, {
cause,
graphName: data.graph_name,
toolStartedEvent
});
if (cause?.type === "toolCall" && toolStartedEvent == null) {
const toolCallId = cause.tool_call_id;
if (toolCallId) this.#pendingToolCallHandles.set(toolCallId, handle);
}
return handle;
}
#start() {
if (this.#sourcePump) return;
this.#sourcePump = (async () => {
for await (const event of this.#source) {
const handle = this.#processEvent(event);
if (!handle) continue;
this.#emit(handle);
}
this.#pendingToolStarts.clear();
this.#pendingToolCallHandles.clear();
this.#closed = true;
while (this.#waiters.length > 0) this.#waiters.shift()?.({
done: true,
value: void 0
});
})();
}
async close() {
this.#closed = true;
await this.#source.unsubscribe();
}
[Symbol.asyncIterator]() {
this.#start();
return {
next: async () => {
if (this.#queue.length > 0) return {
done: false,
value: this.#queue.shift()
};
if (this.#closed) return {
done: true,
value: void 0
};
return await new Promise((resolve) => {
this.#waiters.push(resolve);
});
},
return: async () => {
await this.close();
return {
done: true,
value: void 0
};
}
};
}
};
//#endregion
export { SubgraphDiscoveryHandle, SubgraphHandle, isTerminalLifecycleAt };
//# sourceMappingURL=subgraphs.js.map