feat(langgraph): split streaming protocol from LGP (#1554)

This commit is contained in:
David Duong
2025-08-27 17:24:03 +02:00
committed by GitHub
parent 134564f7a9
commit dcc117f52c
10 changed files with 996 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@langchain/langgraph": patch
---
feat(langgraph): add `toLangGraphEventStream` method to stream events in LGP compatible format
+1
View File
@@ -19,6 +19,7 @@ export const config = {
remote: "remote",
zod: "graph/zod/index",
"zod/schema": "graph/zod/schema",
"ui": "ui/index"
},
tsConfigPath: resolve("./tsconfig.json"),
cjsSource: "./dist-cjs",
+2
View File
@@ -595,6 +595,8 @@ export class CompiledGraph<
> {
declare NodeType: N;
declare "~NodeReturnType": NodeReturnType;
declare RunInput: State;
declare RunOutput: Update;
+68
View File
@@ -0,0 +1,68 @@
import { expectTypeOf, it } from "vitest";
import { Annotation, StateGraph } from "../graph/index.js";
import { MessagesAnnotation } from "../graph/messages_annotation.js";
import { END, START } from "../constants.js";
import { toLangGraphEventStream } from "../ui/stream.js";
import type { SerializedMessage } from "../ui/types.message.js";
it("toLangGraphEventStream", async () => {
const graph = new StateGraph(
Annotation.Root({
messages: MessagesAnnotation.spec.messages,
foo: Annotation<string>,
})
)
.addNode("one", () => ({
messages: { role: "ai", content: "one" as const },
}))
.addNode("two", () => ({
messages: { role: "ai", content: "two" as const },
foo: "foo:two" as const,
}))
.addEdge(START, "one")
.addEdge("one", "two")
.addEdge("two", END)
.compile();
type GraphType = typeof graph;
type ExtraType = { CustomType: string; InterruptType: number };
const stream = await toLangGraphEventStream<GraphType, ExtraType>(
graph.streamEvents({ messages: "input" }, { version: "v2" })
);
for await (const { event, data } of stream) {
if (event === "values") {
expectTypeOf(data).toExtend<{
messages: SerializedMessage.AnyMessage[];
foo: string;
}>();
expectTypeOf(data).toExtend<{ __interrupt__?: number }>();
expectTypeOf(data).not.toExtend<{ __interrupt__?: string }>();
}
if (event === "updates") {
expectTypeOf(data).toExtend<{
one?: { messages: { role: "ai"; content: "one" } };
two?: { messages: { role: "ai"; content: "two" }; foo: "foo:two" };
}>();
expectTypeOf(data).not.toExtend<{
one?: { messages?: { role: "human"; content: "two" } };
}>();
expectTypeOf(data).not.toExtend<{ two?: { foo: "invalid" } }>();
expectTypeOf(data).toExtend<{
[node: string]: {
messages?: SerializedMessage.AnyMessage[] | undefined;
foo?: string | undefined;
};
}>();
}
if (event === "custom") {
expectTypeOf(data).toExtend<string>();
}
}
});
+206
View File
@@ -0,0 +1,206 @@
import { IterableReadableStream } from "@langchain/core/utils/stream";
import type {
AnyPregelLike,
ExtraTypeBag,
InferLangGraphEventStream,
} from "./types.infer.js";
const CR = "\r".charCodeAt(0);
const LF = "\n".charCodeAt(0);
const NULL = "\0".charCodeAt(0);
const COLON = ":".charCodeAt(0);
const SPACE = " ".charCodeAt(0);
const TRAILING_NEWLINE = [CR, LF];
function BytesLineDecoder() {
let buffer: Uint8Array[] = [];
let trailingCr = false;
return new TransformStream<Uint8Array, Uint8Array>({
start() {
buffer = [];
trailingCr = false;
},
transform(chunk, controller) {
// See https://docs.python.org/3/glossary.html#term-universal-newlines
let text = chunk;
// Handle trailing CR from previous chunk
if (trailingCr) {
text = joinArrays([[CR], text]);
trailingCr = false;
}
// Check for trailing CR in current chunk
if (text.length > 0 && text.at(-1) === CR) {
trailingCr = true;
text = text.subarray(0, -1);
}
if (!text.length) return;
const trailingNewline = TRAILING_NEWLINE.includes(text.at(-1)!);
const lastIdx = text.length - 1;
const { lines } = text.reduce<{ lines: Uint8Array[]; from: number }>(
(acc, cur, idx) => {
if (acc.from > idx) return acc;
if (cur === CR || cur === LF) {
acc.lines.push(text.subarray(acc.from, idx));
if (cur === CR && text[idx + 1] === LF) {
acc.from = idx + 2;
} else {
acc.from = idx + 1;
}
}
if (idx === lastIdx && acc.from <= lastIdx) {
acc.lines.push(text.subarray(acc.from));
}
return acc;
},
{ lines: [], from: 0 }
);
if (lines.length === 1 && !trailingNewline) {
buffer.push(lines[0]);
return;
}
if (buffer.length) {
// Include existing buffer in first line
buffer.push(lines[0]);
lines[0] = joinArrays(buffer);
buffer = [];
}
if (!trailingNewline) {
// If the last segment is not newline terminated,
// buffer it for the next chunk
if (lines.length) buffer = [lines.pop()!];
}
// Enqueue complete lines
for (const line of lines) {
controller.enqueue(line);
}
},
flush(controller) {
if (buffer.length) {
controller.enqueue(joinArrays(buffer));
}
},
});
}
function SSEDecoder<
Part extends {
id?: string | undefined;
event: string;
data: unknown;
}
>() {
let event = "";
let data: Uint8Array[] = [];
let lastEventId = "";
let retry: number | null = null;
const decoder = new TextDecoder();
return new TransformStream<Uint8Array, Part>({
transform(chunk, controller) {
// Handle empty line case
if (!chunk.length) {
if (!event && !data.length && !lastEventId && retry == null) return;
const sse = {
id: lastEventId || undefined,
event,
data: data.length ? decodeArraysToJson(decoder, data) : null,
};
// NOTE: as per the SSE spec, do not reset lastEventId
event = "";
data = [];
retry = null;
controller.enqueue(sse as Part);
return;
}
// Ignore comments
if (chunk[0] === COLON) return;
const sepIdx = chunk.indexOf(COLON);
if (sepIdx === -1) return;
const fieldName = decoder.decode(chunk.subarray(0, sepIdx));
let value = chunk.subarray(sepIdx + 1);
if (value[0] === SPACE) value = value.subarray(1);
if (fieldName === "event") {
event = decoder.decode(value);
} else if (fieldName === "data") {
data.push(value);
} else if (fieldName === "id") {
if (value.indexOf(NULL) === -1) lastEventId = decoder.decode(value);
} else if (fieldName === "retry") {
const retryNum = Number.parseInt(decoder.decode(value), 10);
if (!Number.isNaN(retryNum)) retry = retryNum;
}
},
flush(controller) {
if (event) {
controller.enqueue({
id: lastEventId || undefined,
event,
data: data.length ? decodeArraysToJson(decoder, data) : null,
} as Part);
}
},
});
}
function joinArrays(data: ArrayLike<number>[]) {
const totalLength = data.reduce((acc, curr) => acc + curr.length, 0);
const merged = new Uint8Array(totalLength);
let offset = 0;
for (const c of data) {
merged.set(c, offset);
offset += c.length;
}
return merged;
}
function decodeArraysToJson(decoder: TextDecoder, data: ArrayLike<number>[]) {
return JSON.parse(decoder.decode(joinArrays(data)));
}
export async function* stream<
TGraph extends AnyPregelLike,
TExtra extends ExtraTypeBag = ExtraTypeBag
>(options: {
url: string;
headers: HeadersInit;
}): AsyncGenerator<InferLangGraphEventStream<TGraph, TExtra>> {
const res = await fetch(options.url, {
method: "POST",
headers: options.headers,
body: JSON.stringify({ type: "stream" }),
});
if (!res.ok) {
throw new Error(`Failed to stream: ${res.statusText}`);
}
yield* IterableReadableStream.fromReadableStream(
(res.body || new ReadableStream({ start: (ctrl) => ctrl.close() }))
.pipeThrough(BytesLineDecoder())
.pipeThrough(SSEDecoder<InferLangGraphEventStream<TGraph, TExtra>>())
);
}
+7
View File
@@ -0,0 +1,7 @@
export {
toLangGraphEventStream,
toLangGraphEventStreamResponse,
} from "./stream.js";
export type { SerializedMessage } from "./types.message.js";
export type { LangGraphEventStream } from "./types.schema.js";
export type { InferLangGraphEventStream, ExtraTypeBag } from "./types.infer.js";
+231
View File
@@ -0,0 +1,231 @@
import type { RunnableConfig } from "@langchain/core/runnables";
import type { StreamEvent } from "@langchain/core/tracers/log_stream";
import type { StreamMode, StreamOutputMap } from "../pregel/types.js";
import type {
AnyPregelLike,
ExtraTypeBag,
InferLangGraphEventStream,
} from "./types.infer.js";
type StreamCheckpointsOutput<StreamValues> = StreamOutputMap<
"checkpoints",
false,
StreamValues,
unknown,
string,
unknown
>;
const serialiseAsDict = <T>(obj: T): T => {
return JSON.parse(
JSON.stringify(obj, function (key: string | number, value: unknown) {
const rawValue = this[key];
if (
rawValue != null &&
typeof rawValue === "object" &&
"toDict" in rawValue &&
typeof rawValue.toDict === "function"
) {
const { type, data } = rawValue.toDict();
return { ...data, type };
}
return value;
})
);
};
const serializeError = (error: unknown) => {
// eslint-disable-next-line no-instanceof/no-instanceof
if (error instanceof Error) {
return { error: error.name, message: error.message };
}
return { error: "Error", message: JSON.stringify(error) };
};
const isRunnableConfig = (
config: unknown
): config is RunnableConfig & { configurable: Record<string, unknown> } => {
if (typeof config !== "object" || config == null) return false;
return (
"configurable" in config &&
typeof config.configurable === "object" &&
config.configurable != null
);
};
const extractCheckpointFromConfig = (
config: RunnableConfig | null | undefined
) => {
if (!isRunnableConfig(config) || !config.configurable.thread_id) {
return null;
}
return {
thread_id: config.configurable.thread_id,
checkpoint_ns: config.configurable.checkpoint_ns || "",
checkpoint_id: config.configurable.checkpoint_id || null,
checkpoint_map: config.configurable.checkpoint_map || null,
};
};
const serializeConfig = (config: unknown) => {
if (isRunnableConfig(config)) {
const configurable = Object.fromEntries(
Object.entries(config.configurable).filter(
([key]) => !key.startsWith("__")
)
);
const newConfig = { ...config, configurable };
delete newConfig.callbacks;
return newConfig;
}
return config;
};
function serializeCheckpoint(payload: StreamCheckpointsOutput<unknown>) {
const result: Record<string, unknown> = {
...payload,
checkpoint: extractCheckpointFromConfig(payload.config),
parent_checkpoint: extractCheckpointFromConfig(payload.parentConfig),
config: serializeConfig(payload.config),
parent_config: serializeConfig(payload.parentConfig),
tasks: payload.tasks.map((task) => {
if (isRunnableConfig(task.state)) {
const checkpoint = extractCheckpointFromConfig(task.state);
if (checkpoint != null) {
const cloneTask: Record<string, unknown> = { ...task, checkpoint };
delete cloneTask.state;
return cloneTask;
}
}
return task;
}),
};
delete result.parentConfig;
return result;
}
/**
* Converts a `graph.streamEvents()` output into a LangGraph Platform compatible event stream.
* @experimental Does not follow semver.
*
* @param events
*/
export async function* toLangGraphEventStream<
TGraph extends AnyPregelLike,
TExtra extends ExtraTypeBag = ExtraTypeBag
>(
events: AsyncIterable<StreamEvent> | Promise<AsyncIterable<StreamEvent>>
): AsyncGenerator<InferLangGraphEventStream<TGraph, TExtra>> {
let rootRunId: string | undefined;
try {
for await (const event of await events) {
if (event.event === "on_chain_start" && rootRunId == null) {
rootRunId = event.run_id;
}
if (event.tags?.includes("langsmith:hidden")) continue;
if (event.event === "on_chain_stream" && event.run_id === rootRunId) {
if (!Array.isArray(event.data.chunk)) {
continue;
}
type AnyStreamOutput = StreamOutputMap<
StreamMode[],
true,
unknown,
unknown,
string,
unknown
>;
const [ns, mode, chunk] = (
event.data.chunk.length === 3
? event.data.chunk
: [null, ...event.data.chunk]
) as AnyStreamOutput;
// Listen for debug events and capture checkpoint
let data: unknown = chunk;
if (mode === "debug") {
const debugChunk = chunk;
if (debugChunk.type === "checkpoint") {
data = {
...debugChunk,
payload: serializeCheckpoint(debugChunk.payload),
};
}
}
if (mode === "checkpoints") {
data = serializeCheckpoint(chunk);
}
// This needs to be done for LC.js V0 messages, since they
// by default serialize using the verbose Serializable protocol.
data = serialiseAsDict(data);
yield {
event: ns?.length ? `${mode}|${ns.join("|")}` : mode,
data,
} as InferLangGraphEventStream<TGraph, TExtra>;
}
}
} catch (error) {
yield { event: "error", data: serializeError(error) };
}
}
/**
* Converts a `graph.streamEvents()` output into a LangGraph Platform compatible Web Response.
* @experimental Does not follow semver.
*/
export function toLangGraphEventStreamResponse(options: {
status?: number;
statusText?: string;
headers?: Headers | Record<string, string>;
stream: AsyncIterable<StreamEvent> | Promise<AsyncIterable<StreamEvent>>;
}) {
const headers = new Headers({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
if (options.headers) {
if (
"forEach" in options.headers &&
typeof options.headers.forEach === "function"
) {
options.headers.forEach((v, k) => headers.set(k, v));
} else {
Object.entries(options.headers).map(([k, v]) => headers.set(k, v));
}
}
return new Response(
new ReadableStream({
async start(controller) {
try {
for await (const { event, data } of toLangGraphEventStream(
options.stream
)) {
controller.enqueue(`event: ${event}\n`);
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
}
} finally {
controller.close();
}
},
}),
{ headers, status: options.status, statusText: options.statusText }
);
}
+102
View File
@@ -0,0 +1,102 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { BaseMessage } from "@langchain/core/messages";
import type { SerializedMessage } from "./types.message.js";
import type { LangGraphEventStream } from "./types.schema.js";
type Equals<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
? 1
: 2
? true
: false;
type MatchBaseMessage<T> = T extends BaseMessage ? BaseMessage : never;
type MatchBaseMessageArray<T> = T extends Array<infer C>
? Equals<MatchBaseMessage<C>, BaseMessage> extends true
? BaseMessage[]
: never
: never;
type ReplaceMessages<T, TDepth extends Array<0> = []> = TDepth extends [0, 0, 0]
? any
: T extends unknown
? {
[K in keyof T]: 0 extends 1 & T[K]
? T[K]
: Equals<MatchBaseMessageArray<T[K]>, BaseMessage[]> extends true
? SerializedMessage.AnyMessage[]
: Equals<MatchBaseMessage<T[K]>, BaseMessage> extends true
? SerializedMessage.AnyMessage
: ReplaceMessages<T[K], [0, ...TDepth]>;
}
: never;
/* eslint-disable @typescript-eslint/no-explicit-any */
type Defactorify<T> = T extends (...args: any[]) => infer R
? Awaited<R>
: Awaited<T>;
type AnyPregel = {
lg_is_pregel: boolean;
stream: (...args: any[]) => any;
invoke: (...args: any[]) => any;
};
type AnyGraph = {
compiled: boolean;
compile: (...args: any[]) => any;
};
export type AnyPregelLike =
| AnyPregel
| AnyGraph
| ((...args: any[]) => AnyPregel | AnyGraph);
type ReflectCompiledGraph<T> = T extends {
RunInput: infer State;
RunOutput: infer Update;
"~NodeReturnType"?: infer ReturnType;
}
? {
state: ReplaceMessages<State>;
update: ReplaceMessages<Update>;
returnType: ReturnType;
}
: T extends { "~InputType": infer InputType; "~OutputType": infer OutputType }
? { state: ReplaceMessages<OutputType>; update: ReplaceMessages<InputType> }
: never;
type InferGraph<T> = Defactorify<T> extends infer DT
? DT extends {
compile(...args: any[]): infer Compiled;
}
? ReflectCompiledGraph<Compiled>
: ReflectCompiledGraph<DT>
: never;
export type ExtraTypeBag<TCustomType = unknown, TInterruptType = unknown> = {
CustomType?: TCustomType;
InterruptType?: TInterruptType;
};
type GetCustomType<Bag extends ExtraTypeBag> = Bag extends {
CustomType: unknown;
}
? Bag["CustomType"]
: unknown;
type GetInterruptType<Bag extends ExtraTypeBag> = Bag extends {
InterruptType: unknown;
}
? Bag["InterruptType"]
: unknown;
export type InferLangGraphEventStream<
TGraph extends AnyPregelLike,
TExtra extends ExtraTypeBag = ExtraTypeBag
> = LangGraphEventStream<
InferGraph<TGraph>["state"],
InferGraph<TGraph>["update"],
GetCustomType<TExtra>,
GetInterruptType<TExtra>,
InferGraph<TGraph>["returnType"]
>;
+96
View File
@@ -0,0 +1,96 @@
type ImageDetail = "auto" | "low" | "high";
type MessageContentImageUrl = {
type: "image_url";
image_url: string | { url: string; detail?: ImageDetail | undefined };
};
type MessageContentText = { type: "text"; text: string };
type MessageContentComplex = MessageContentText | MessageContentImageUrl;
type MessageContent = string | MessageContentComplex[];
/**
* Model-specific additional kwargs, which is passed back to the underlying LLM.
*/
type MessageAdditionalKwargs = Record<string, unknown>;
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace SerializedMessage {
type BaseMessage = {
additional_kwargs?: MessageAdditionalKwargs | undefined;
content: MessageContent;
id?: string | undefined;
name?: string | undefined;
response_metadata?: Record<string, unknown> | undefined;
};
export type HumanMessage = BaseMessage & {
type: "human";
example?: boolean | undefined;
};
export type AIMessage = BaseMessage & {
type: "ai";
example?: boolean | undefined;
tool_calls?:
| {
name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args: { [x: string]: any };
id?: string | undefined;
type?: "tool_call" | undefined;
}[]
| undefined;
invalid_tool_calls?:
| {
name?: string | undefined;
args?: string | undefined;
id?: string | undefined;
error?: string | undefined;
type?: "invalid_tool_call" | undefined;
}[]
| undefined;
usage_metadata?:
| {
input_tokens: number;
output_tokens: number;
total_tokens: number;
input_token_details?:
| {
audio?: number | undefined;
cache_read?: number | undefined;
cache_creation?: number | undefined;
}
| undefined;
output_token_details?:
| { audio?: number | undefined; reasoning?: number | undefined }
| undefined;
}
| undefined;
};
export type ToolMessage = BaseMessage & {
type: "tool";
status?: "error" | "success" | undefined;
tool_call_id: string;
/**
* Artifact of the Tool execution which is not meant to be sent to the model.
*
* Should only be specified if it is different from the message content, e.g. if only
* a subset of the full tool output is being passed as message content but the full
* output is needed in other parts of the code.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
artifact?: any;
};
export type SystemMessage = BaseMessage & { type: "system" };
export type FunctionMessage = BaseMessage & { type: "function" };
export type RemoveMessage = BaseMessage & { type: "remove" };
export type AnyMessage =
| HumanMessage
| AIMessage
| ToolMessage
| SystemMessage
| FunctionMessage
| RemoveMessage;
}
+278
View File
@@ -0,0 +1,278 @@
import type { SerializedMessage } from "./types.message.js";
type Optional<T> = T | null | undefined;
type MessageTupleMetadata = {
tags: string[];
[key: string]: unknown;
};
type DefaultValues = Record<string, unknown>[] | Record<string, unknown>;
interface ThreadState<ValuesType = DefaultValues> {
/** The state values */
values: ValuesType;
/** The next nodes to execute. If empty, the thread is done until new input is received */
next: string[];
/** Checkpoint of the thread state */
checkpoint: Checkpoint;
/** Metadata for this state */
metadata: CheckpointMetadata;
/** Time of state creation */
created_at: Optional<string>;
/** The parent checkpoint. If missing, this is the root checkpoint */
parent_checkpoint: Optional<Checkpoint>;
/** Tasks to execute in this step. If already attempted, may contain an error */
tasks: Array<ThreadTask>;
}
interface ThreadTask {
id: string;
name: string;
result?: unknown;
error: Optional<string>;
interrupts: Array<Interrupt>;
checkpoint: Optional<Checkpoint>;
state: Optional<ThreadState>;
}
type Config = {
/**
* Tags for this call and any sub-calls (eg. a Chain calling an LLM).
* You can use these to filter calls.
*/
tags?: string[];
/**
* Maximum number of times a call can recurse.
* If not provided, defaults to 25.
*/
recursion_limit?: number;
/**
* Runtime values for attributes previously made configurable on this Runnable.
*/
configurable?: {
/**
* ID of the thread
*/
thread_id?: Optional<string>;
/**
* Timestamp of the state checkpoint
*/
checkpoint_id?: Optional<string>;
[key: string]: unknown;
};
};
type CheckpointMetadata = Optional<{
source?: "input" | "loop" | "update" | (string & {}); // eslint-disable-line @typescript-eslint/ban-types
step?: number;
writes?: Record<string, unknown> | null;
parents?: Record<string, string>;
[key: string]: unknown;
}>;
interface Checkpoint {
thread_id: string;
checkpoint_ns: string;
checkpoint_id: Optional<string>;
checkpoint_map: Optional<Record<string, unknown>>;
}
/**
* An interrupt thrown inside a thread.
*/
interface Interrupt<TValue = unknown> {
/**
* The ID of the interrupt.
*/
id?: string;
/**
* The value of the interrupt.
*/
value?: TValue;
}
type AsSubgraph<TEvent extends { id?: string; event: string; data: unknown }> =
{
id?: TEvent["id"];
event: TEvent["event"] | `${TEvent["event"]}|${string}`;
data: TEvent["data"];
};
/**
* Stream event with values after completion of each step.
*/
type ValuesStreamEvent<StateType, InterruptType> = AsSubgraph<{
id?: string;
event: "values";
data: unknown extends InterruptType
? StateType
: StateType & { __interrupt__?: InterruptType };
}>;
/**
* Stream event with message chunks coming from LLM invocations inside nodes.
*/
type MessagesTupleStreamEvent = AsSubgraph<{
id?: string;
event: "messages";
// TODO: add types for message and config, which do not depend on LangChain
// while making sure it's easy to keep them in sync.
data: [message: SerializedMessage.AnyMessage, config: MessageTupleMetadata];
}>;
/**
* Metadata stream event with information about the run and thread
*/
type MetadataStreamEvent = {
id?: string;
event: "metadata";
data: { run_id: string; thread_id: string };
};
/**
* Stream event with error information.
*/
type SubgraphErrorStreamEvent = AsSubgraph<{
id?: string;
event: "error";
data: { error: string; message: string };
}>;
/**
* Stream event with updates to the state after each step.
* The streamed outputs include the name of the node that
* produced the update as well as the update.
*/
type UpdatesStreamEvent<UpdateType, NodeReturnType> = AsSubgraph<{
id?: string;
event: "updates";
data: NodeReturnType extends Record<string, unknown>
? { [K in keyof NodeReturnType]?: NodeReturnType[K] } & {
[node: string]: UpdateType;
}
: Record<string, UpdateType>;
}>;
/**
* Streaming custom data from inside the nodes.
*/
type CustomStreamEvent<T> = AsSubgraph<{
id?: string;
event: "custom";
data: T;
}>;
type TasksStreamCreateEvent<StateType> = {
id?: string;
event: "tasks";
data: {
id: string;
name: string;
interrupts: Interrupt[];
input: StateType;
triggers: string[];
};
};
type TasksStreamResultEvent<UpdateType> = {
id?: string;
event: "tasks";
data: {
id: string;
name: string;
interrupts: Interrupt[];
result: [string, UpdateType][];
};
};
type TasksStreamErrorEvent = {
id?: string;
event: "tasks";
data: {
id: string;
name: string;
interrupts: Interrupt[];
error: string;
};
};
type TasksStreamEvent<StateType, UpdateType> =
| AsSubgraph<TasksStreamCreateEvent<StateType>>
| AsSubgraph<TasksStreamResultEvent<UpdateType>>
| AsSubgraph<TasksStreamErrorEvent>;
type CheckpointsStreamEvent<StateType> = AsSubgraph<{
id?: string;
event: "checkpoints";
data: {
values: StateType;
next: string[];
config: Config;
metadata: CheckpointMetadata;
tasks: ThreadTask[];
};
}>;
/**
* Stream event with detailed debug information.
*/
type DebugStreamEvent = AsSubgraph<{
id?: string;
event: "debug";
data: unknown;
}>;
/**
* Stream event with events occurring during execution.
*/
type EventsStreamEvent = {
id?: string;
event: "events";
data: {
event:
| `on_${
| "chat_model"
| "llm"
| "chain"
| "tool"
| "retriever"
| "prompt"}_${"start" | "stream" | "end"}`
| (string & {}); // eslint-disable-line @typescript-eslint/ban-types
name: string;
tags: string[];
run_id: string;
metadata: Record<string, unknown>;
parent_ids: string[];
data: unknown;
};
};
export type LangGraphEventStream<
TStateType = unknown,
TUpdateType = TStateType,
TCustomType = unknown,
TInterruptType = unknown,
TNodeReturnType = unknown
> =
| ValuesStreamEvent<TStateType, TInterruptType>
| UpdatesStreamEvent<TUpdateType, TNodeReturnType>
| CustomStreamEvent<TCustomType>
| DebugStreamEvent
| MessagesTupleStreamEvent
| EventsStreamEvent
| TasksStreamEvent<TStateType, TUpdateType>
| CheckpointsStreamEvent<TStateType>
| SubgraphErrorStreamEvent
| MetadataStreamEvent;