@langchain/langgraph-sdk
Version:
Client library for interacting with the LangGraph API
271 lines (270 loc) • 7.89 kB
JavaScript
import { MultiCursorBuffer } from "../multi-cursor-buffer.js";
import { ToolCallAssembler, shouldIgnoreScopedTaskToolEvent, toClientAssembledToolCall } from "./tools.js";
import { StreamingMessageAssembler } from "../messages.js";
import { MediaAssembler } from "../media.js";
import { SubgraphDiscoveryHandle, isTerminalLifecycleAt } from "./subgraphs.js";
//#region src/client/stream/handles/subagents.ts
/**
* Discovered subagent within a streaming session. Mirrors the
* in-process `SubagentRunStream` from DeepAgent.
*
* Each subagent is discovered when a `tool-started` event with
* `tool_name === "task"` is observed. The `taskInput` and `output`
* promises resolve from the task tool's lifecycle events.
*
* Use lazy getters (`sub.messages`, `sub.toolCalls`, etc.) for
* namespace-scoped projections.
*/
var SubagentHandle = class {
name;
callId;
taskInput;
output;
namespace;
#session;
#messagesIterable;
#toolCallsIterable;
#subgraphsIterable;
#mediaDispatcherStarted = false;
#audioBuffer;
#imagesBuffer;
#videoBuffer;
#filesBuffer;
constructor(name, callId, namespace, taskInput, output, session) {
this.name = name;
this.callId = callId;
this.namespace = namespace;
this.taskInput = taskInput;
this.output = output;
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 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 toolsEvent = event;
if (shouldIgnoreScopedTaskToolEvent(this.namespace, toolsEvent)) return;
const tc = assembler.consume(toolsEvent);
if (tc) buffer.push(toClientAssembledToolCall(tc));
}, () => 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;
}
#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();
});
}
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"],
namespaces: [this.namespace]
}), this.#session, this.namespace);
for await (const sub of discovery) buffer.push(sub);
buffer.close();
})();
return buffer;
}
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 SubagentHandle} instances as task
* tool calls are discovered from the `tools` channel.
*
* Mirrors the in-process `createSubagentTransformer` from DeepAgent:
* watches for `tool_name === "task"` with `tool-started`, extracts
* `subagent_type` and `description` from the input, and resolves
* `output` on `tool-finished`.
*/
var SubagentDiscoveryHandle = class {
#source;
#session;
#queue = [];
#waiters = [];
#pending = /* @__PURE__ */ new Map();
#sourcePump;
#closed = false;
constructor(source, session) {
this.#source = source;
this.#session = session;
}
#processEvent(event) {
if (event.method !== "tools") return void 0;
const tools = event;
const data = tools.params.data;
const toolCallId = data.tool_call_id;
if (data.tool_name === "task" && data.event === "tool-started") {
const rawInput = data.input;
const input = typeof rawInput === "string" ? JSON.parse(rawInput) : rawInput ?? {};
const name = input.subagent_type ?? "unknown";
const description = input.description ?? "";
let resolveTaskInput;
let resolveOutput;
let rejectOutput;
const taskInput = new Promise((r) => {
resolveTaskInput = r;
});
const output = new Promise((res, rej) => {
resolveOutput = res;
rejectOutput = rej;
});
resolveTaskInput(description);
this.#pending.set(toolCallId, {
resolveOutput,
rejectOutput
});
return new SubagentHandle(name, toolCallId, [...tools.params.namespace], taskInput, output, this.#session);
}
if (toolCallId) {
const pending = this.#pending.get(toolCallId);
if (pending) {
if (data.event === "tool-finished") {
pending.resolveOutput(data.output);
this.#pending.delete(toolCallId);
} else if (data.event === "tool-error") {
const message = data.message ?? "unknown error";
pending.rejectOutput(new Error(message));
this.#pending.delete(toolCallId);
}
}
}
}
#start() {
if (this.#sourcePump) return;
this.#sourcePump = (async () => {
for await (const event of this.#source) {
const handle = this.#processEvent(event);
if (!handle) continue;
const waiter = this.#waiters.shift();
if (waiter) waiter({
done: false,
value: handle
});
else this.#queue.push(handle);
}
this.#closed = true;
for (const pending of this.#pending.values()) pending.resolveOutput(void 0);
this.#pending.clear();
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 { SubagentDiscoveryHandle, SubagentHandle };
//# sourceMappingURL=subagents.js.map