@langchain/langgraph-sdk 1.9.25 → 1.9.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/base.cjs.map +1 -1
- package/dist/client/base.d.cts.map +1 -1
- package/dist/client/base.d.ts.map +1 -1
- package/dist/client/base.js.map +1 -1
- package/dist/client/crons/index.cjs +18 -17
- package/dist/client/crons/index.cjs.map +1 -1
- package/dist/client/crons/index.d.cts.map +1 -1
- package/dist/client/crons/index.d.ts.map +1 -1
- package/dist/client/crons/index.js +18 -17
- package/dist/client/crons/index.js.map +1 -1
- package/dist/client/index.cjs +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/stream/index.cjs +1 -1
- package/dist/client/stream/index.js +1 -1
- package/dist/client/stream/transport/agent-server.cjs +1 -1
- package/dist/client/stream/transport/agent-server.js +1 -1
- package/dist/client/stream/transport/http.cjs +4 -4
- package/dist/client/stream/transport/http.cjs.map +1 -1
- package/dist/client/stream/transport/http.d.cts.map +1 -1
- package/dist/client/stream/transport/http.d.ts.map +1 -1
- package/dist/client/stream/transport/http.js +4 -4
- package/dist/client/stream/transport/http.js.map +1 -1
- package/dist/client/stream/transport/index.cjs +1 -1
- package/dist/client/stream/transport/index.js +1 -1
- package/dist/client/stream/transport/types.d.cts +18 -8
- package/dist/client/stream/transport/types.d.cts.map +1 -1
- package/dist/client/stream/transport/types.d.ts +18 -8
- package/dist/client/stream/transport/types.d.ts.map +1 -1
- package/dist/client/stream/transport/websocket.cjs +2 -9
- package/dist/client/stream/transport/websocket.cjs.map +1 -1
- package/dist/client/stream/transport/websocket.d.cts.map +1 -1
- package/dist/client/stream/transport/websocket.d.ts.map +1 -1
- package/dist/client/stream/transport/websocket.js +3 -9
- package/dist/client/stream/transport/websocket.js.map +1 -1
- package/dist/client/stream/types.d.cts +9 -6
- package/dist/client/stream/types.d.cts.map +1 -1
- package/dist/client/stream/types.d.ts +9 -6
- package/dist/client/stream/types.d.ts.map +1 -1
- package/dist/client/threads/index.cjs +1 -1
- package/dist/client/threads/index.cjs.map +1 -1
- package/dist/client/threads/index.d.cts.map +1 -1
- package/dist/client/threads/index.d.ts.map +1 -1
- package/dist/client/threads/index.js +1 -1
- package/dist/client/threads/index.js.map +1 -1
- package/dist/client.cjs +1 -1
- package/dist/client.js +1 -1
- package/dist/index.cjs +3 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/react/stream.custom.cjs +1 -0
- package/dist/react/stream.custom.cjs.map +1 -1
- package/dist/react/stream.custom.d.cts.map +1 -1
- package/dist/react/stream.custom.d.ts.map +1 -1
- package/dist/react/stream.custom.js +1 -0
- package/dist/react/stream.custom.js.map +1 -1
- package/dist/react/stream.lgp.cjs +1 -0
- package/dist/react/stream.lgp.cjs.map +1 -1
- package/dist/react/stream.lgp.js +1 -0
- package/dist/react/stream.lgp.js.map +1 -1
- package/dist/stream/constants.cjs +6 -0
- package/dist/stream/constants.cjs.map +1 -1
- package/dist/stream/constants.d.cts +6 -1
- package/dist/stream/constants.d.cts.map +1 -1
- package/dist/stream/constants.d.ts +6 -1
- package/dist/stream/constants.d.ts.map +1 -1
- package/dist/stream/constants.js +6 -1
- package/dist/stream/constants.js.map +1 -1
- package/dist/stream/controller.cjs +7 -3
- package/dist/stream/controller.cjs.map +1 -1
- package/dist/stream/controller.js +7 -3
- package/dist/stream/controller.js.map +1 -1
- package/dist/stream/discovery/namespace-from-history.cjs +2 -2
- package/dist/stream/discovery/namespace-from-history.cjs.map +1 -1
- package/dist/stream/discovery/namespace-from-history.js +2 -2
- package/dist/stream/discovery/namespace-from-history.js.map +1 -1
- package/dist/stream/index.cjs +2 -1
- package/dist/stream/index.d.cts +2 -2
- package/dist/stream/index.d.ts +2 -2
- package/dist/stream/index.js +2 -2
- package/dist/stream/projections/values.cjs +2 -1
- package/dist/stream/projections/values.cjs.map +1 -1
- package/dist/stream/projections/values.d.cts.map +1 -1
- package/dist/stream/projections/values.d.ts.map +1 -1
- package/dist/stream/projections/values.js +2 -1
- package/dist/stream/projections/values.js.map +1 -1
- package/dist/stream/types.d.cts +40 -2
- package/dist/stream/types.d.cts.map +1 -1
- package/dist/stream/types.d.ts +40 -2
- package/dist/stream/types.d.ts.map +1 -1
- package/dist/types.d.cts +5 -1
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/manager.cjs +1 -0
- package/dist/ui/manager.cjs.map +1 -1
- package/dist/ui/manager.d.cts.map +1 -1
- package/dist/ui/manager.d.ts.map +1 -1
- package/dist/ui/manager.js +1 -0
- package/dist/ui/manager.js.map +1 -1
- package/dist/ui/messages.cjs +2 -1
- package/dist/ui/messages.cjs.map +1 -1
- package/dist/ui/messages.d.cts.map +1 -1
- package/dist/ui/messages.d.ts.map +1 -1
- package/dist/ui/messages.js +2 -1
- package/dist/ui/messages.js.map +1 -1
- package/dist/ui/orchestrator-custom.cjs +1 -0
- package/dist/ui/orchestrator-custom.cjs.map +1 -1
- package/dist/ui/orchestrator-custom.d.cts.map +1 -1
- package/dist/ui/orchestrator-custom.d.ts.map +1 -1
- package/dist/ui/orchestrator-custom.js +1 -0
- package/dist/ui/orchestrator-custom.js.map +1 -1
- package/dist/utils/reconnect.cjs +18 -0
- package/dist/utils/reconnect.cjs.map +1 -0
- package/dist/utils/reconnect.js +18 -0
- package/dist/utils/reconnect.js.map +1 -0
- package/dist/utils/stream.cjs +9 -1
- package/dist/utils/stream.cjs.map +1 -1
- package/dist/utils/stream.d.cts.map +1 -1
- package/dist/utils/stream.d.ts.map +1 -1
- package/dist/utils/stream.js +9 -2
- package/dist/utils/stream.js.map +1 -1
- package/package.json +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orchestrator-custom.js","names":["#options","#historyValues","#threadId","#streamUnsub","#flushPendingHeadlessToolInterrupts","#notify","#getMessages","#listeners","#version","#disposed","#handledHeadlessToolInterruptIds","#branch","#setMessages"],"sources":["../../src/ui/orchestrator-custom.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nimport type { ThreadState, Interrupt } from \"../schema.js\";\nimport type { StreamMode } from \"../types.stream.js\";\nimport type { Message } from \"../types.messages.js\";\nimport type { BagTemplate } from \"../types.template.js\";\nimport { StreamManager, type EventStreamEvent } from \"./manager.js\";\nimport {\n MessageTupleManager,\n toMessageClass,\n ensureMessageInstances,\n} from \"./messages.js\";\nimport {\n extractInterrupts,\n userFacingInterruptsFromValuesArray,\n} from \"./interrupts.js\";\nimport { getToolCallsWithResults } from \"../utils/tools.js\";\nimport { flushPendingHeadlessToolInterrupts } from \"../headless-tools.js\";\nimport type {\n AnyStreamCustomOptions,\n CustomSubmitOptions,\n MessageMetadata,\n GetUpdateType,\n GetCustomEventType,\n GetInterruptType,\n GetConfigurableType,\n SubagentStreamInterface,\n} from \"./types.js\";\n\n/**\n * Create a custom transport thread state.\n * @param values - The values to use.\n * @param threadId - The ID of the thread to use.\n * @returns The custom transport thread state.\n */\nfunction createCustomTransportThreadState<\n StateType extends Record<string, unknown>,\n>(values: StateType, threadId: string): ThreadState<StateType> {\n return {\n values,\n next: [],\n tasks: [],\n metadata: undefined,\n created_at: null,\n checkpoint: {\n thread_id: threadId,\n checkpoint_id: null,\n checkpoint_ns: \"\",\n checkpoint_map: null,\n },\n parent_checkpoint: null,\n };\n}\n\n/**\n * Framework-agnostic orchestrator for custom transport streams.\n *\n * Encapsulates all business logic shared across React, Vue, Svelte, and Angular\n * for custom transport (non-LGP) streaming.\n */\nexport class CustomStreamOrchestrator<\n StateType extends Record<string, unknown> = Record<string, unknown>,\n Bag extends BagTemplate = BagTemplate,\n> {\n readonly stream: StreamManager<StateType, Bag>;\n\n readonly messageManager: MessageTupleManager;\n\n readonly #options: AnyStreamCustomOptions<StateType, Bag>;\n\n readonly #historyValues: StateType;\n\n #threadId: string | null;\n\n #branch: string = \"\";\n\n #listeners = new Set<() => void>();\n\n #version = 0;\n\n #streamUnsub: (() => void) | null = null;\n\n #disposed = false;\n\n #handledHeadlessToolInterruptIds = new Set<string>();\n\n /**\n * Create a new {@link CustomStreamOrchestrator} instance.\n *\n * @param options - Configuration options for the custom transport stream,\n * including thread ID, transport, callbacks, and subagent settings.\n */\n constructor(options: AnyStreamCustomOptions<StateType, Bag>) {\n this.#options = options;\n\n this.#threadId = options.threadId ?? null;\n\n this.messageManager = new MessageTupleManager();\n this.stream = new StreamManager<StateType, Bag>(this.messageManager, {\n throttle: options.throttle ?? false,\n subagentToolNames: options.subagentToolNames,\n filterSubagentMessages: options.filterSubagentMessages,\n toMessage: options.toMessage ?? toMessageClass,\n });\n\n this.#historyValues = options.initialValues ?? ({} as StateType);\n\n this.#streamUnsub = this.stream.subscribe(() => {\n this.#flushPendingHeadlessToolInterrupts();\n this.#notify();\n });\n\n const historyMessages = this.#getMessages(this.#historyValues);\n if (\n options.filterSubagentMessages &&\n !this.stream.isLoading &&\n historyMessages.length > 0\n ) {\n this.stream.reconstructSubagents(historyMessages, {\n skipIfPopulated: true,\n });\n }\n }\n\n /**\n * Register a listener that is called whenever the orchestrator state changes.\n *\n * @param listener - Callback invoked on each state change.\n * @returns An unsubscribe function that removes the listener.\n */\n subscribe = (listener: () => void): (() => void) => {\n this.#listeners.add(listener);\n return () => {\n this.#listeners.delete(listener);\n };\n };\n\n /**\n * Return the current version number, incremented on each state change.\n * Useful as a cache key for external sync (e.g. `useSyncExternalStore`).\n *\n * @returns The current version counter.\n */\n getSnapshot = (): number => this.#version;\n\n /**\n * Custom transports do not negotiate stream modes with the server the way\n * LangGraph Platform streams do, but framework adapters call this method on\n * both orchestrator variants. Keep the API surface aligned as a no-op.\n */\n trackStreamMode(..._modes: StreamMode[]): void {}\n\n #notify(): void {\n if (this.#disposed) return;\n this.#version += 1;\n for (const listener of this.#listeners) {\n listener();\n }\n }\n\n /**\n * Synchronize the external thread ID with the orchestrator.\n * If the ID has changed, the current stream is cleared and listeners\n * are notified.\n *\n * @param newId - The new thread ID, or `null` to clear.\n */\n syncThreadId(newId: string | null): void {\n if (newId !== this.#threadId) {\n this.#threadId = newId;\n this.#handledHeadlessToolInterruptIds.clear();\n this.stream.clear();\n this.#notify();\n }\n }\n\n #getMessages = (value: StateType): Message[] => {\n const messagesKey = this.#options.messagesKey ?? \"messages\";\n return Array.isArray(value[messagesKey])\n ? (value[messagesKey] as Message[])\n : [];\n };\n\n #setMessages = (current: StateType, messages: Message[]): StateType => {\n const messagesKey = this.#options.messagesKey ?? \"messages\";\n return { ...current, [messagesKey]: messages };\n };\n\n /**\n * The current stream state values, falling back to an empty object\n * when no stream values are available.\n */\n get values(): StateType {\n return this.stream.values ?? ({} as StateType);\n }\n\n /**\n * The raw stream state values, or `null` if no stream has been started\n * or values have not yet been received.\n */\n get streamValues(): StateType | null {\n return this.stream.values;\n }\n\n /** The most recent stream error, or `undefined` if no error occurred. */\n get error(): unknown {\n return this.stream.error;\n }\n\n /** Whether a stream is currently in progress. */\n get isLoading(): boolean {\n return this.stream.isLoading;\n }\n\n /** The current branch identifier. */\n get branch(): string {\n return this.#branch;\n }\n\n /**\n * Update the current branch and notify listeners.\n *\n * @param value - The new branch identifier.\n */\n setBranch(value: string): void {\n this.#branch = value;\n this.#notify();\n }\n\n /**\n * All messages from the current stream values, converted to\n * {@link BaseMessage} instances. Returns an empty array when no\n * stream values are available.\n */\n get messages(): BaseMessage[] {\n if (!this.stream.values) return [];\n return ensureMessageInstances(\n this.#getMessages(this.stream.values)\n ) as BaseMessage[];\n }\n\n /**\n * All tool calls paired with their results extracted from the\n * current stream messages.\n */\n get toolCalls() {\n if (!this.stream.values) return [];\n return getToolCallsWithResults(this.#getMessages(this.stream.values));\n }\n\n /**\n * Get tool calls (with results) that belong to a specific AI message.\n *\n * @param message - The AI message whose tool calls to retrieve.\n * @returns Tool calls associated with the given message.\n */\n getToolCalls(message: Message) {\n if (!this.stream.values) return [];\n const allToolCalls = getToolCallsWithResults(\n this.#getMessages(this.stream.values)\n );\n return allToolCalls.filter((tc) => tc.aiMessage.id === message.id);\n }\n\n /**\n * All active interrupts from the current stream values.\n * Returns a single breakpoint interrupt when the interrupt array is\n * present but empty, or an empty array when no interrupts exist.\n */\n get interrupts(): Interrupt<GetInterruptType<Bag>>[] {\n if (\n this.stream.values != null &&\n \"__interrupt__\" in this.stream.values &&\n Array.isArray(this.stream.values.__interrupt__)\n ) {\n return userFacingInterruptsFromValuesArray<GetInterruptType<Bag>>(\n this.stream.values.__interrupt__ as Interrupt<GetInterruptType<Bag>>[]\n );\n }\n return [];\n }\n\n /**\n * The first active interrupt extracted from the current stream values,\n * or `undefined` if there are no interrupts.\n */\n get interrupt(): Interrupt<GetInterruptType<Bag>> | undefined {\n return extractInterrupts<GetInterruptType<Bag>>(this.stream.values);\n }\n\n /**\n * Retrieve stream-level metadata for a given message.\n *\n * @param message - The message to look up metadata for.\n * @param index - Optional positional index used as fallback message ID.\n * @returns The metadata associated with the message, or `undefined`\n * if no stream metadata is available.\n */\n getMessagesMetadata(\n message: Message,\n index?: number\n ): MessageMetadata<StateType> | undefined {\n const streamMetadata = this.messageManager.get(message.id)?.metadata;\n if (streamMetadata != null) {\n return {\n messageId: message.id ?? String(index),\n firstSeenState: undefined,\n branch: undefined,\n branchOptions: undefined,\n streamMetadata,\n } as MessageMetadata<StateType>;\n }\n return undefined;\n }\n\n /** A map of all tracked subagent streams, keyed by tool call ID. */\n get subagents(): Map<string, SubagentStreamInterface> {\n return this.stream.getSubagents();\n }\n\n /** The subset of subagent streams that are currently active (loading). */\n get activeSubagents(): SubagentStreamInterface[] {\n return this.stream.getActiveSubagents();\n }\n\n /**\n * Look up a single subagent stream by its tool call ID.\n *\n * @param toolCallId - The tool call ID that initiated the subagent.\n * @returns The subagent stream, or `undefined` if not found.\n */\n getSubagent(toolCallId: string) {\n return this.stream.getSubagent(toolCallId);\n }\n\n /**\n * Retrieve all subagent streams matching a given tool name / type.\n *\n * @param type - The subagent type (tool name) to filter by.\n * @returns An array of matching subagent streams.\n */\n getSubagentsByType(type: string) {\n return this.stream.getSubagentsByType(type);\n }\n\n /**\n * Retrieve all subagent streams associated with a specific AI message.\n *\n * @param messageId - The ID of the parent AI message.\n * @returns An array of subagent streams linked to the message.\n */\n getSubagentsByMessage(messageId: string) {\n return this.stream.getSubagentsByMessage(messageId);\n }\n\n /**\n * Reconstruct subagent streams from history values when subagent\n * filtering is enabled and the stream is not currently loading.\n * This is a no-op if subagents are already populated.\n */\n reconstructSubagentsIfNeeded(): void {\n const hvMessages = this.#getMessages(this.#historyValues);\n if (\n this.#options.filterSubagentMessages &&\n !this.stream.isLoading &&\n hvMessages.length > 0\n ) {\n this.stream.reconstructSubagents(hvMessages, { skipIfPopulated: true });\n }\n }\n\n /**\n * Abort the current stream and invoke the `onStop` callback\n * if one was provided in the options.\n */\n stop(): void {\n void this.stream.stop(this.#historyValues, {\n onStop: this.#options.onStop,\n });\n }\n\n /**\n * Switch to a different thread. If the thread ID actually changed,\n * the current stream is cleared and listeners are notified.\n *\n * @param newThreadId - The thread ID to switch to, or `null` to clear.\n */\n switchThread(newThreadId: string | null): void {\n if (newThreadId !== this.#threadId) {\n this.#threadId = newThreadId;\n this.#handledHeadlessToolInterruptIds.clear();\n this.stream.clear();\n this.#notify();\n }\n }\n\n /**\n * Start a new stream run against the custom transport.\n *\n * This is the low-level submit entry point that handles thread ID\n * resolution, optimistic value merging, and transport invocation.\n * Prefer {@link submit} unless you need to bypass higher-level wrappers.\n *\n * @param values - The input values to send, or `null`/`undefined` for\n * a resume-style invocation.\n * @param submitOptions - Optional per-call overrides such as\n * `optimisticValues`, `config`, `command`, and error callbacks.\n */\n async submitDirect(\n values: GetUpdateType<Bag, StateType> | null | undefined,\n submitOptions?: CustomSubmitOptions<StateType, GetConfigurableType<Bag>>\n ): Promise<void> {\n type UpdateType = GetUpdateType<Bag, StateType>;\n type CustomType = GetCustomEventType<Bag>;\n\n const currentThreadId = this.#options.threadId ?? null;\n if (currentThreadId !== this.#threadId) {\n this.#threadId = currentThreadId;\n this.stream.clear();\n }\n\n let usableThreadId = this.#threadId ?? submitOptions?.threadId;\n\n this.stream.setStreamValues(() => {\n if (submitOptions?.optimisticValues != null) {\n return {\n ...this.#historyValues,\n ...(typeof submitOptions.optimisticValues === \"function\"\n ? submitOptions.optimisticValues(this.#historyValues)\n : submitOptions.optimisticValues),\n };\n }\n\n return { ...this.#historyValues };\n });\n\n await this.stream.start(\n async (signal: AbortSignal) => {\n if (!usableThreadId) {\n usableThreadId = crypto.randomUUID();\n this.#threadId = usableThreadId;\n this.#options.onThreadId?.(usableThreadId);\n }\n\n if (!usableThreadId) {\n throw new Error(\"Failed to obtain valid thread ID.\");\n }\n\n return this.#options.transport.stream({\n input: values,\n context: submitOptions?.context,\n command: submitOptions?.command,\n streamSubgraphs: submitOptions?.streamSubgraphs,\n signal,\n config: {\n ...submitOptions?.config,\n configurable: {\n thread_id: usableThreadId,\n ...submitOptions?.config?.configurable,\n } as unknown as GetConfigurableType<Bag>,\n },\n }) as Promise<\n AsyncGenerator<EventStreamEvent<StateType, UpdateType, CustomType>>\n >;\n },\n {\n getMessages: this.#getMessages,\n setMessages: this.#setMessages,\n initialValues: {} as StateType,\n callbacks: this.#options,\n onSuccess: () => {\n if (!usableThreadId) return undefined;\n\n const finalValues = this.stream.values ?? this.#historyValues;\n this.#options.onFinish?.(\n createCustomTransportThreadState(finalValues, usableThreadId),\n undefined\n );\n\n return undefined;\n },\n onError: (error) => {\n this.#options.onError?.(error, undefined);\n submitOptions?.onError?.(error, undefined);\n },\n }\n );\n }\n\n /**\n * Submit input values and start a new stream run.\n *\n * Delegates to {@link submitDirect}. Override or wrap this method\n * in framework adapters to add queuing or other middleware.\n *\n * @param values - The input values to send, or `null`/`undefined` for\n * a resume-style invocation.\n * @param submitOptions - Optional per-call overrides.\n */\n async submit(\n values: GetUpdateType<Bag, StateType> | null | undefined,\n submitOptions?: CustomSubmitOptions<StateType, GetConfigurableType<Bag>>\n ): Promise<void> {\n await this.submitDirect(values, submitOptions);\n }\n\n /**\n * Tear down the orchestrator. Marks the instance as disposed,\n * unsubscribes from the stream, and aborts any in-progress stream.\n * After calling this method, no further notifications will be emitted.\n */\n dispose(): void {\n this.#disposed = true;\n this.#streamUnsub?.();\n this.#streamUnsub = null;\n void this.stream.stop(this.#historyValues, {\n onStop: this.#options.onStop,\n });\n }\n\n #flushPendingHeadlessToolInterrupts(): void {\n flushPendingHeadlessToolInterrupts(\n this.stream.values as Record<string, unknown> | null,\n this.#options.tools,\n this.#handledHeadlessToolInterruptIds,\n {\n onTool: this.#options.onTool,\n defer: (run) => {\n void Promise.resolve().then(run);\n },\n resumeSubmit: (command) =>\n this.submit(null, {\n command,\n }),\n }\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"orchestrator-custom.js","names":["#options","#historyValues","#threadId","#streamUnsub","#flushPendingHeadlessToolInterrupts","#notify","#getMessages","#listeners","#version","#disposed","#handledHeadlessToolInterruptIds","#branch","#setMessages"],"sources":["../../src/ui/orchestrator-custom.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nimport { DEFAULT_MESSAGES_KEY } from \"../stream/constants.js\";\nimport type { ThreadState, Interrupt } from \"../schema.js\";\nimport type { StreamMode } from \"../types.stream.js\";\nimport type { Message } from \"../types.messages.js\";\nimport type { BagTemplate } from \"../types.template.js\";\nimport { StreamManager, type EventStreamEvent } from \"./manager.js\";\nimport {\n MessageTupleManager,\n toMessageClass,\n ensureMessageInstances,\n} from \"./messages.js\";\nimport {\n extractInterrupts,\n userFacingInterruptsFromValuesArray,\n} from \"./interrupts.js\";\nimport { getToolCallsWithResults } from \"../utils/tools.js\";\nimport { flushPendingHeadlessToolInterrupts } from \"../headless-tools.js\";\nimport type {\n AnyStreamCustomOptions,\n CustomSubmitOptions,\n MessageMetadata,\n GetUpdateType,\n GetCustomEventType,\n GetInterruptType,\n GetConfigurableType,\n SubagentStreamInterface,\n} from \"./types.js\";\n\n/**\n * Create a custom transport thread state.\n * @param values - The values to use.\n * @param threadId - The ID of the thread to use.\n * @returns The custom transport thread state.\n */\nfunction createCustomTransportThreadState<\n StateType extends Record<string, unknown>,\n>(values: StateType, threadId: string): ThreadState<StateType> {\n return {\n values,\n next: [],\n tasks: [],\n metadata: undefined,\n created_at: null,\n checkpoint: {\n thread_id: threadId,\n checkpoint_id: null,\n checkpoint_ns: \"\",\n checkpoint_map: null,\n },\n parent_checkpoint: null,\n };\n}\n\n/**\n * Framework-agnostic orchestrator for custom transport streams.\n *\n * Encapsulates all business logic shared across React, Vue, Svelte, and Angular\n * for custom transport (non-LGP) streaming.\n */\nexport class CustomStreamOrchestrator<\n StateType extends Record<string, unknown> = Record<string, unknown>,\n Bag extends BagTemplate = BagTemplate,\n> {\n readonly stream: StreamManager<StateType, Bag>;\n\n readonly messageManager: MessageTupleManager;\n\n readonly #options: AnyStreamCustomOptions<StateType, Bag>;\n\n readonly #historyValues: StateType;\n\n #threadId: string | null;\n\n #branch: string = \"\";\n\n #listeners = new Set<() => void>();\n\n #version = 0;\n\n #streamUnsub: (() => void) | null = null;\n\n #disposed = false;\n\n #handledHeadlessToolInterruptIds = new Set<string>();\n\n /**\n * Create a new {@link CustomStreamOrchestrator} instance.\n *\n * @param options - Configuration options for the custom transport stream,\n * including thread ID, transport, callbacks, and subagent settings.\n */\n constructor(options: AnyStreamCustomOptions<StateType, Bag>) {\n this.#options = options;\n\n this.#threadId = options.threadId ?? null;\n\n this.messageManager = new MessageTupleManager();\n this.stream = new StreamManager<StateType, Bag>(this.messageManager, {\n throttle: options.throttle ?? false,\n subagentToolNames: options.subagentToolNames,\n filterSubagentMessages: options.filterSubagentMessages,\n toMessage: options.toMessage ?? toMessageClass,\n });\n\n this.#historyValues = options.initialValues ?? ({} as StateType);\n\n this.#streamUnsub = this.stream.subscribe(() => {\n this.#flushPendingHeadlessToolInterrupts();\n this.#notify();\n });\n\n const historyMessages = this.#getMessages(this.#historyValues);\n if (\n options.filterSubagentMessages &&\n !this.stream.isLoading &&\n historyMessages.length > 0\n ) {\n this.stream.reconstructSubagents(historyMessages, {\n skipIfPopulated: true,\n });\n }\n }\n\n /**\n * Register a listener that is called whenever the orchestrator state changes.\n *\n * @param listener - Callback invoked on each state change.\n * @returns An unsubscribe function that removes the listener.\n */\n subscribe = (listener: () => void): (() => void) => {\n this.#listeners.add(listener);\n return () => {\n this.#listeners.delete(listener);\n };\n };\n\n /**\n * Return the current version number, incremented on each state change.\n * Useful as a cache key for external sync (e.g. `useSyncExternalStore`).\n *\n * @returns The current version counter.\n */\n getSnapshot = (): number => this.#version;\n\n /**\n * Custom transports do not negotiate stream modes with the server the way\n * LangGraph Platform streams do, but framework adapters call this method on\n * both orchestrator variants. Keep the API surface aligned as a no-op.\n */\n trackStreamMode(..._modes: StreamMode[]): void {}\n\n #notify(): void {\n if (this.#disposed) return;\n this.#version += 1;\n for (const listener of this.#listeners) {\n listener();\n }\n }\n\n /**\n * Synchronize the external thread ID with the orchestrator.\n * If the ID has changed, the current stream is cleared and listeners\n * are notified.\n *\n * @param newId - The new thread ID, or `null` to clear.\n */\n syncThreadId(newId: string | null): void {\n if (newId !== this.#threadId) {\n this.#threadId = newId;\n this.#handledHeadlessToolInterruptIds.clear();\n this.stream.clear();\n this.#notify();\n }\n }\n\n #getMessages = (value: StateType): Message[] => {\n const messagesKey = this.#options.messagesKey ?? DEFAULT_MESSAGES_KEY;\n return Array.isArray(value[messagesKey])\n ? (value[messagesKey] as Message[])\n : [];\n };\n\n #setMessages = (current: StateType, messages: Message[]): StateType => {\n const messagesKey = this.#options.messagesKey ?? DEFAULT_MESSAGES_KEY;\n return { ...current, [messagesKey]: messages };\n };\n\n /**\n * The current stream state values, falling back to an empty object\n * when no stream values are available.\n */\n get values(): StateType {\n return this.stream.values ?? ({} as StateType);\n }\n\n /**\n * The raw stream state values, or `null` if no stream has been started\n * or values have not yet been received.\n */\n get streamValues(): StateType | null {\n return this.stream.values;\n }\n\n /** The most recent stream error, or `undefined` if no error occurred. */\n get error(): unknown {\n return this.stream.error;\n }\n\n /** Whether a stream is currently in progress. */\n get isLoading(): boolean {\n return this.stream.isLoading;\n }\n\n /** The current branch identifier. */\n get branch(): string {\n return this.#branch;\n }\n\n /**\n * Update the current branch and notify listeners.\n *\n * @param value - The new branch identifier.\n */\n setBranch(value: string): void {\n this.#branch = value;\n this.#notify();\n }\n\n /**\n * All messages from the current stream values, converted to\n * {@link BaseMessage} instances. Returns an empty array when no\n * stream values are available.\n */\n get messages(): BaseMessage[] {\n if (!this.stream.values) return [];\n return ensureMessageInstances(\n this.#getMessages(this.stream.values)\n ) as BaseMessage[];\n }\n\n /**\n * All tool calls paired with their results extracted from the\n * current stream messages.\n */\n get toolCalls() {\n if (!this.stream.values) return [];\n return getToolCallsWithResults(this.#getMessages(this.stream.values));\n }\n\n /**\n * Get tool calls (with results) that belong to a specific AI message.\n *\n * @param message - The AI message whose tool calls to retrieve.\n * @returns Tool calls associated with the given message.\n */\n getToolCalls(message: Message) {\n if (!this.stream.values) return [];\n const allToolCalls = getToolCallsWithResults(\n this.#getMessages(this.stream.values)\n );\n return allToolCalls.filter((tc) => tc.aiMessage.id === message.id);\n }\n\n /**\n * All active interrupts from the current stream values.\n * Returns a single breakpoint interrupt when the interrupt array is\n * present but empty, or an empty array when no interrupts exist.\n */\n get interrupts(): Interrupt<GetInterruptType<Bag>>[] {\n if (\n this.stream.values != null &&\n \"__interrupt__\" in this.stream.values &&\n Array.isArray(this.stream.values.__interrupt__)\n ) {\n return userFacingInterruptsFromValuesArray<GetInterruptType<Bag>>(\n this.stream.values.__interrupt__ as Interrupt<GetInterruptType<Bag>>[]\n );\n }\n return [];\n }\n\n /**\n * The first active interrupt extracted from the current stream values,\n * or `undefined` if there are no interrupts.\n */\n get interrupt(): Interrupt<GetInterruptType<Bag>> | undefined {\n return extractInterrupts<GetInterruptType<Bag>>(this.stream.values);\n }\n\n /**\n * Retrieve stream-level metadata for a given message.\n *\n * @param message - The message to look up metadata for.\n * @param index - Optional positional index used as fallback message ID.\n * @returns The metadata associated with the message, or `undefined`\n * if no stream metadata is available.\n */\n getMessagesMetadata(\n message: Message,\n index?: number\n ): MessageMetadata<StateType> | undefined {\n const streamMetadata = this.messageManager.get(message.id)?.metadata;\n if (streamMetadata != null) {\n return {\n messageId: message.id ?? String(index),\n firstSeenState: undefined,\n branch: undefined,\n branchOptions: undefined,\n streamMetadata,\n } as MessageMetadata<StateType>;\n }\n return undefined;\n }\n\n /** A map of all tracked subagent streams, keyed by tool call ID. */\n get subagents(): Map<string, SubagentStreamInterface> {\n return this.stream.getSubagents();\n }\n\n /** The subset of subagent streams that are currently active (loading). */\n get activeSubagents(): SubagentStreamInterface[] {\n return this.stream.getActiveSubagents();\n }\n\n /**\n * Look up a single subagent stream by its tool call ID.\n *\n * @param toolCallId - The tool call ID that initiated the subagent.\n * @returns The subagent stream, or `undefined` if not found.\n */\n getSubagent(toolCallId: string) {\n return this.stream.getSubagent(toolCallId);\n }\n\n /**\n * Retrieve all subagent streams matching a given tool name / type.\n *\n * @param type - The subagent type (tool name) to filter by.\n * @returns An array of matching subagent streams.\n */\n getSubagentsByType(type: string) {\n return this.stream.getSubagentsByType(type);\n }\n\n /**\n * Retrieve all subagent streams associated with a specific AI message.\n *\n * @param messageId - The ID of the parent AI message.\n * @returns An array of subagent streams linked to the message.\n */\n getSubagentsByMessage(messageId: string) {\n return this.stream.getSubagentsByMessage(messageId);\n }\n\n /**\n * Reconstruct subagent streams from history values when subagent\n * filtering is enabled and the stream is not currently loading.\n * This is a no-op if subagents are already populated.\n */\n reconstructSubagentsIfNeeded(): void {\n const hvMessages = this.#getMessages(this.#historyValues);\n if (\n this.#options.filterSubagentMessages &&\n !this.stream.isLoading &&\n hvMessages.length > 0\n ) {\n this.stream.reconstructSubagents(hvMessages, { skipIfPopulated: true });\n }\n }\n\n /**\n * Abort the current stream and invoke the `onStop` callback\n * if one was provided in the options.\n */\n stop(): void {\n void this.stream.stop(this.#historyValues, {\n onStop: this.#options.onStop,\n });\n }\n\n /**\n * Switch to a different thread. If the thread ID actually changed,\n * the current stream is cleared and listeners are notified.\n *\n * @param newThreadId - The thread ID to switch to, or `null` to clear.\n */\n switchThread(newThreadId: string | null): void {\n if (newThreadId !== this.#threadId) {\n this.#threadId = newThreadId;\n this.#handledHeadlessToolInterruptIds.clear();\n this.stream.clear();\n this.#notify();\n }\n }\n\n /**\n * Start a new stream run against the custom transport.\n *\n * This is the low-level submit entry point that handles thread ID\n * resolution, optimistic value merging, and transport invocation.\n * Prefer {@link submit} unless you need to bypass higher-level wrappers.\n *\n * @param values - The input values to send, or `null`/`undefined` for\n * a resume-style invocation.\n * @param submitOptions - Optional per-call overrides such as\n * `optimisticValues`, `config`, `command`, and error callbacks.\n */\n async submitDirect(\n values: GetUpdateType<Bag, StateType> | null | undefined,\n submitOptions?: CustomSubmitOptions<StateType, GetConfigurableType<Bag>>\n ): Promise<void> {\n type UpdateType = GetUpdateType<Bag, StateType>;\n type CustomType = GetCustomEventType<Bag>;\n\n const currentThreadId = this.#options.threadId ?? null;\n if (currentThreadId !== this.#threadId) {\n this.#threadId = currentThreadId;\n this.stream.clear();\n }\n\n let usableThreadId = this.#threadId ?? submitOptions?.threadId;\n\n this.stream.setStreamValues(() => {\n if (submitOptions?.optimisticValues != null) {\n return {\n ...this.#historyValues,\n ...(typeof submitOptions.optimisticValues === \"function\"\n ? submitOptions.optimisticValues(this.#historyValues)\n : submitOptions.optimisticValues),\n };\n }\n\n return { ...this.#historyValues };\n });\n\n await this.stream.start(\n async (signal: AbortSignal) => {\n if (!usableThreadId) {\n usableThreadId = crypto.randomUUID();\n this.#threadId = usableThreadId;\n this.#options.onThreadId?.(usableThreadId);\n }\n\n if (!usableThreadId) {\n throw new Error(\"Failed to obtain valid thread ID.\");\n }\n\n return this.#options.transport.stream({\n input: values,\n context: submitOptions?.context,\n command: submitOptions?.command,\n streamSubgraphs: submitOptions?.streamSubgraphs,\n signal,\n config: {\n ...submitOptions?.config,\n configurable: {\n thread_id: usableThreadId,\n ...submitOptions?.config?.configurable,\n } as unknown as GetConfigurableType<Bag>,\n },\n }) as Promise<\n AsyncGenerator<EventStreamEvent<StateType, UpdateType, CustomType>>\n >;\n },\n {\n getMessages: this.#getMessages,\n setMessages: this.#setMessages,\n initialValues: {} as StateType,\n callbacks: this.#options,\n onSuccess: () => {\n if (!usableThreadId) return undefined;\n\n const finalValues = this.stream.values ?? this.#historyValues;\n this.#options.onFinish?.(\n createCustomTransportThreadState(finalValues, usableThreadId),\n undefined\n );\n\n return undefined;\n },\n onError: (error) => {\n this.#options.onError?.(error, undefined);\n submitOptions?.onError?.(error, undefined);\n },\n }\n );\n }\n\n /**\n * Submit input values and start a new stream run.\n *\n * Delegates to {@link submitDirect}. Override or wrap this method\n * in framework adapters to add queuing or other middleware.\n *\n * @param values - The input values to send, or `null`/`undefined` for\n * a resume-style invocation.\n * @param submitOptions - Optional per-call overrides.\n */\n async submit(\n values: GetUpdateType<Bag, StateType> | null | undefined,\n submitOptions?: CustomSubmitOptions<StateType, GetConfigurableType<Bag>>\n ): Promise<void> {\n await this.submitDirect(values, submitOptions);\n }\n\n /**\n * Tear down the orchestrator. Marks the instance as disposed,\n * unsubscribes from the stream, and aborts any in-progress stream.\n * After calling this method, no further notifications will be emitted.\n */\n dispose(): void {\n this.#disposed = true;\n this.#streamUnsub?.();\n this.#streamUnsub = null;\n void this.stream.stop(this.#historyValues, {\n onStop: this.#options.onStop,\n });\n }\n\n #flushPendingHeadlessToolInterrupts(): void {\n flushPendingHeadlessToolInterrupts(\n this.stream.values as Record<string, unknown> | null,\n this.#options.tools,\n this.#handledHeadlessToolInterruptIds,\n {\n onTool: this.#options.onTool,\n defer: (run) => {\n void Promise.resolve().then(run);\n },\n resumeSubmit: (command) =>\n this.submit(null, {\n command,\n }),\n }\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/utils/reconnect.ts
|
|
2
|
+
/** Base delay (ms) for exponential reconnect backoff (`base * 2^(attempt-1)`). */
|
|
3
|
+
const DEFAULT_RECONNECT_BASE_DELAY_MS = 1e3;
|
|
4
|
+
/** Cap (ms) for exponential reconnect backoff before jitter. */
|
|
5
|
+
const DEFAULT_RECONNECT_MAX_DELAY_MS = 5e3;
|
|
6
|
+
/** Max random jitter (ms) added on top of the capped base delay. */
|
|
7
|
+
const DEFAULT_RECONNECT_JITTER_MS = 1e3;
|
|
8
|
+
/**
|
|
9
|
+
* Exponential backoff with jitter for stream reconnect.
|
|
10
|
+
* `min(base * 2^(attempt-1), max) + random(0, jitter)`.
|
|
11
|
+
*/
|
|
12
|
+
function reconnectDelayMs(attempt) {
|
|
13
|
+
return Math.min(DEFAULT_RECONNECT_BASE_DELAY_MS * 2 ** (attempt - 1), DEFAULT_RECONNECT_MAX_DELAY_MS) + Math.random() * DEFAULT_RECONNECT_JITTER_MS;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
exports.reconnectDelayMs = reconnectDelayMs;
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=reconnect.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconnect.cjs","names":[],"sources":["../../src/utils/reconnect.ts"],"sourcesContent":["/**\n * Shared reconnect defaults and backoff for SSE (`streamWithRetry`) and\n * protocol transports (WebSocket / SSE adapters). Keep these in one place so\n * legacy and v2 clients do not diverge.\n */\n\n/** Default max reconnect / retry attempts after an unexpected disconnect. */\nexport const DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;\n\n/** Base delay (ms) for exponential reconnect backoff (`base * 2^(attempt-1)`). */\nexport const DEFAULT_RECONNECT_BASE_DELAY_MS = 1000;\n\n/** Cap (ms) for exponential reconnect backoff before jitter. */\nexport const DEFAULT_RECONNECT_MAX_DELAY_MS = 5000;\n\n/** Max random jitter (ms) added on top of the capped base delay. */\nexport const DEFAULT_RECONNECT_JITTER_MS = 1000;\n\n/**\n * Exponential backoff with jitter for stream reconnect.\n * `min(base * 2^(attempt-1), max) + random(0, jitter)`.\n */\nexport function reconnectDelayMs(attempt: number): number {\n const baseDelay = Math.min(\n DEFAULT_RECONNECT_BASE_DELAY_MS * 2 ** (attempt - 1),\n DEFAULT_RECONNECT_MAX_DELAY_MS\n );\n const jitter = Math.random() * DEFAULT_RECONNECT_JITTER_MS;\n return baseDelay + jitter;\n}\n"],"mappings":";;AAUA,MAAa,kCAAkC;;AAG/C,MAAa,iCAAiC;;AAG9C,MAAa,8BAA8B;;;;;AAM3C,SAAgB,iBAAiB,SAAyB;AAMxD,QALkB,KAAK,IACrB,kCAAkC,MAAM,UAAU,IAClD,+BACD,GACc,KAAK,QAAQ,GAAG"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/utils/reconnect.ts
|
|
2
|
+
/** Base delay (ms) for exponential reconnect backoff (`base * 2^(attempt-1)`). */
|
|
3
|
+
const DEFAULT_RECONNECT_BASE_DELAY_MS = 1e3;
|
|
4
|
+
/** Cap (ms) for exponential reconnect backoff before jitter. */
|
|
5
|
+
const DEFAULT_RECONNECT_MAX_DELAY_MS = 5e3;
|
|
6
|
+
/** Max random jitter (ms) added on top of the capped base delay. */
|
|
7
|
+
const DEFAULT_RECONNECT_JITTER_MS = 1e3;
|
|
8
|
+
/**
|
|
9
|
+
* Exponential backoff with jitter for stream reconnect.
|
|
10
|
+
* `min(base * 2^(attempt-1), max) + random(0, jitter)`.
|
|
11
|
+
*/
|
|
12
|
+
function reconnectDelayMs(attempt) {
|
|
13
|
+
return Math.min(DEFAULT_RECONNECT_BASE_DELAY_MS * 2 ** (attempt - 1), DEFAULT_RECONNECT_MAX_DELAY_MS) + Math.random() * DEFAULT_RECONNECT_JITTER_MS;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { reconnectDelayMs };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=reconnect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconnect.js","names":[],"sources":["../../src/utils/reconnect.ts"],"sourcesContent":["/**\n * Shared reconnect defaults and backoff for SSE (`streamWithRetry`) and\n * protocol transports (WebSocket / SSE adapters). Keep these in one place so\n * legacy and v2 clients do not diverge.\n */\n\n/** Default max reconnect / retry attempts after an unexpected disconnect. */\nexport const DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;\n\n/** Base delay (ms) for exponential reconnect backoff (`base * 2^(attempt-1)`). */\nexport const DEFAULT_RECONNECT_BASE_DELAY_MS = 1000;\n\n/** Cap (ms) for exponential reconnect backoff before jitter. */\nexport const DEFAULT_RECONNECT_MAX_DELAY_MS = 5000;\n\n/** Max random jitter (ms) added on top of the capped base delay. */\nexport const DEFAULT_RECONNECT_JITTER_MS = 1000;\n\n/**\n * Exponential backoff with jitter for stream reconnect.\n * `min(base * 2^(attempt-1), max) + random(0, jitter)`.\n */\nexport function reconnectDelayMs(attempt: number): number {\n const baseDelay = Math.min(\n DEFAULT_RECONNECT_BASE_DELAY_MS * 2 ** (attempt - 1),\n DEFAULT_RECONNECT_MAX_DELAY_MS\n );\n const jitter = Math.random() * DEFAULT_RECONNECT_JITTER_MS;\n return baseDelay + jitter;\n}\n"],"mappings":";;AAUA,MAAa,kCAAkC;;AAG/C,MAAa,iCAAiC;;AAG9C,MAAa,8BAA8B;;;;;AAM3C,SAAgB,iBAAiB,SAAyB;AAMxD,QALkB,KAAK,IACrB,kCAAkC,MAAM,UAAU,IAClD,+BACD,GACc,KAAK,QAAQ,GAAG"}
|
package/dist/utils/stream.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_error = require("./error.cjs");
|
|
2
|
+
const require_reconnect = require("./reconnect.cjs");
|
|
2
3
|
//#region src/utils/stream.ts
|
|
3
4
|
/**
|
|
4
5
|
* Error thrown when maximum reconnection attempts are exceeded.
|
|
@@ -29,6 +30,12 @@ var StreamIdleTimeoutError = class extends Error {
|
|
|
29
30
|
/** `":"` — first byte of an SSE comment / keep-alive line. */
|
|
30
31
|
const SSE_COMMENT_BYTE = 58;
|
|
31
32
|
/**
|
|
33
|
+
* Default idle-reconnect policy for SSE streams. Heartbeat-adaptive
|
|
34
|
+
* `"auto"` stays dormant unless the server emits keep-alive heartbeats.
|
|
35
|
+
* Pass `0` to disable.
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_IDLE_RECONNECT = "auto";
|
|
38
|
+
/**
|
|
32
39
|
* A pass-through {@link TransformStream} that errors the stream when it goes
|
|
33
40
|
* idle, so the surrounding reconnect logic can recover a half-open socket.
|
|
34
41
|
*
|
|
@@ -165,7 +172,7 @@ async function* streamWithRetry(makeRequest, options = {}) {
|
|
|
165
172
|
lastEventId,
|
|
166
173
|
cause: lastError
|
|
167
174
|
});
|
|
168
|
-
const delay =
|
|
175
|
+
const delay = require_reconnect.reconnectDelayMs(attempt);
|
|
169
176
|
await new Promise((resolve) => {
|
|
170
177
|
setTimeout(resolve, delay);
|
|
171
178
|
});
|
|
@@ -260,6 +267,7 @@ var IterableReadableStream = class IterableReadableStream extends ReadableStream
|
|
|
260
267
|
}
|
|
261
268
|
};
|
|
262
269
|
//#endregion
|
|
270
|
+
exports.DEFAULT_IDLE_RECONNECT = DEFAULT_IDLE_RECONNECT;
|
|
263
271
|
exports.IterableReadableStream = IterableReadableStream;
|
|
264
272
|
exports.idleReconnectStream = idleReconnectStream;
|
|
265
273
|
exports.streamWithRetry = streamWithRetry;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.cjs","names":["isNetworkError"],"sources":["../../src/utils/stream.ts"],"sourcesContent":["import { isNetworkError } from \"./error.js\";\n\n// in this case don't quite match.\ntype IterableReadableStreamInterface<T> = ReadableStream<T> & AsyncIterable<T>;\n\n/**\n * Options for streaming with automatic retry logic.\n */\nexport interface StreamWithRetryOptions {\n /**\n * Maximum number of reconnection attempts. Default is 5.\n */\n maxRetries?: number;\n\n /**\n * AbortSignal to cancel the stream.\n */\n signal?: AbortSignal;\n\n /**\n * Callback invoked when a reconnection attempt is made.\n */\n onReconnect?: (options: {\n attempt: number;\n lastEventId?: string;\n cause: unknown;\n }) => void;\n}\n\n/**\n * Parameters for making a stream request\n */\nexport interface StreamRequestParams {\n /**\n * Last event ID to resume from, if available\n */\n lastEventId?: string;\n\n /**\n * Optional reconnection path from the Location header\n */\n reconnectPath?: string;\n}\n\n/**\n * Error thrown when maximum reconnection attempts are exceeded.\n */\nexport class MaxReconnectAttemptsError extends Error {\n constructor(maxAttempts: number, cause: unknown) {\n super(`Exceeded maximum SSE reconnection attempts (${maxAttempts})`);\n this.name = \"MaxReconnectAttemptsError\";\n this.cause = cause;\n }\n}\n\n/**\n * Error injected into the stream by {@link idleReconnectStream} when no lines\n * arrive within the active idle window. Surfacing this during the read is what\n * lets the reconnect loops in `streamWithRetry` and the protocol SSE transport\n * recover from a half-open socket — one that was silently dropped (e.g. a hard\n * pod kill on a platform revision rollover) without a TCP FIN/RST, so neither\n * a `done` nor a thrown network error ever arrives.\n */\nexport class StreamIdleTimeoutError extends Error {\n readonly idleTimeoutMs: number;\n\n constructor(idleTimeoutMs: number) {\n super(\n `No SSE bytes received for ${idleTimeoutMs}ms; assuming the connection is half-open and reconnecting.`\n );\n this.name = \"StreamIdleTimeoutError\";\n this.idleTimeoutMs = idleTimeoutMs;\n }\n}\n\n/** `\":\"` — first byte of an SSE comment / keep-alive line. */\nconst SSE_COMMENT_BYTE = 0x3a;\n\n/**\n * How {@link idleReconnectStream} decides when the connection is idle.\n *\n * - A `number` is a fixed idle window in milliseconds: the watchdog arms\n * immediately (even before the first byte) and trips after that long with no\n * activity. Use when you know your server's behaviour and want guaranteed\n * coverage from t=0; it does not depend on heartbeats.\n * - `\"auto\"` is heartbeat-adaptive: the watchdog stays dormant until it has\n * observed the server's SSE keep-alive comments (e.g. LangGraph Platform's\n * `: heartbeat` every ~5s), then arms with a window derived from the\n * observed cadence. On a server that never sends heartbeats it never arms,\n * so it can't false-fire during a legitimately quiet period.\n */\nexport type IdleReconnectMode = number | \"auto\";\n\nexport interface IdleReconnectStreamOptions {\n /** Fixed timeout (ms) or `\"auto\"` heartbeat-adaptive. */\n mode: IdleReconnectMode;\n /** `\"auto\"`: multiplier applied to the observed heartbeat interval. Default 3. */\n timeoutFactor?: number;\n /** `\"auto\"`: lower clamp for the derived timeout (ms). Default 6000. */\n minTimeoutMs?: number;\n /** `\"auto\"`: upper clamp for the derived timeout (ms). Default 30000. */\n maxTimeoutMs?: number;\n /** Fired immediately before the stream is errored, for logging/metrics. */\n onIdle?: (info: { timeoutMs: number; source: \"fixed\" | \"heartbeat\" }) => void;\n}\n\n/**\n * A pass-through {@link TransformStream} that errors the stream when it goes\n * idle, so the surrounding reconnect logic can recover a half-open socket.\n *\n * MUST sit on the *line* stream — i.e. after\n * {@link import(\"./sse.js\").BytesLineDecoder} but before\n * {@link import(\"./sse.js\").SSEDecoder} (which discards `:` comment lines).\n * Operating at the line level lets the watchdog both (a) reset on any line\n * (data *or* heartbeat = liveness) and (b) recognise heartbeat comment lines\n * to drive `\"auto\"` mode.\n *\n * In `\"auto\"` mode the watchdog is intentionally dormant until it has seen at\n * least two heartbeats (so it can measure the cadence). This means a socket\n * that dies inside the first heartbeat interval won't be caught until a\n * heartbeat would have been due — an acceptable trade for never false-firing\n * on heartbeat-less servers. Pass a fixed `number` if you need coverage from\n * the very first byte.\n */\nexport function idleReconnectStream(\n options: IdleReconnectStreamOptions\n): TransformStream<Uint8Array, Uint8Array> {\n const factor = options.timeoutFactor ?? 3;\n const minTimeoutMs = options.minTimeoutMs ?? 6_000;\n const maxTimeoutMs = options.maxTimeoutMs ?? 30_000;\n const fixedTimeoutMs = typeof options.mode === \"number\" ? options.mode : null;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let controllerRef: TransformStreamDefaultController<Uint8Array> | undefined;\n\n // `\"auto\"` cadence inference.\n let lastHeartbeatAt: number | undefined;\n // The active idle window: the fixed value, or (auto) the heartbeat-derived\n // value once known. `null` means \"not armed yet\".\n let derivedTimeoutMs: number | null = fixedTimeoutMs;\n\n const clear = () => {\n if (timer != null) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n\n const arm = () => {\n clear();\n const timeoutMs = derivedTimeoutMs;\n if (timeoutMs == null || timeoutMs <= 0) return;\n timer = setTimeout(() => {\n options.onIdle?.({\n timeoutMs,\n source: fixedTimeoutMs != null ? \"fixed\" : \"heartbeat\",\n });\n try {\n controllerRef?.error(new StreamIdleTimeoutError(timeoutMs));\n } catch {\n // Stream already closed/errored/cancelled — nothing to abort.\n }\n }, timeoutMs);\n // Don't let the watchdog by itself keep a Node process alive.\n (timer as unknown as { unref?: () => void }).unref?.();\n };\n\n const noteHeartbeat = () => {\n if (fixedTimeoutMs != null) return; // cadence irrelevant in fixed mode\n const now = Date.now();\n if (lastHeartbeatAt != null) {\n const interval = now - lastHeartbeatAt;\n if (interval > 0) {\n const candidate = Math.min(\n Math.max(interval * factor, minTimeoutMs),\n maxTimeoutMs\n );\n // Keep the most conservative (largest) window observed so far.\n derivedTimeoutMs =\n derivedTimeoutMs == null\n ? candidate\n : Math.max(derivedTimeoutMs, candidate);\n }\n }\n lastHeartbeatAt = now;\n };\n\n return new TransformStream<Uint8Array, Uint8Array>({\n start(controller) {\n controllerRef = controller;\n // Fixed mode arms eagerly (also catches pre-first-byte silence). Auto\n // mode waits until a cadence is established in `transform`.\n arm();\n },\n transform(line, controller) {\n // A line beginning with \":\" is an SSE comment / keep-alive heartbeat.\n if (line.length > 0 && line[0] === SSE_COMMENT_BYTE) {\n noteHeartbeat();\n }\n // Any line is liveness — (re)arm the idle timer.\n arm();\n controller.enqueue(line);\n },\n flush() {\n clear();\n },\n });\n}\n\n/**\n * Stream with automatic retry logic for SSE connections.\n * Implements reconnection behavior similar to the Python SDK.\n *\n * @param makeRequest Function to make requests. When `params` is undefined/empty, it's the initial request.\n * When `params.reconnectPath` is provided, it's a reconnection request.\n * @param options Configuration options\n * @returns AsyncGenerator yielding stream events\n */\nexport async function* streamWithRetry<T extends { id?: string }>(\n makeRequest: (params?: StreamRequestParams) => Promise<{\n response: Response;\n stream: ReadableStream<T>;\n }>,\n options: StreamWithRetryOptions = {}\n): AsyncGenerator<T> {\n const maxRetries = options.maxRetries ?? 5;\n let attempt = 0;\n let lastEventId: string | undefined;\n let reconnectPath: string | undefined;\n\n while (true) {\n let shouldRetry = false;\n let lastError: unknown;\n let reader: ReadableStreamDefaultReader<T> | undefined;\n\n try {\n // Check if aborted before making request\n if (options.signal?.aborted) return;\n\n // Make request - initial if no reconnect path, reconnect otherwise\n const { response, stream } = await makeRequest(\n reconnectPath ? { lastEventId, reconnectPath } : undefined\n );\n\n // Check for Location header (server-provided reconnection path)\n const locationHeader = response.headers.get(\"location\");\n if (locationHeader) {\n reconnectPath = locationHeader;\n }\n\n // Verify content type\n const contentType = response.headers.get(\"content-type\")?.split(\";\")[0];\n if (contentType && !contentType.includes(\"text/event-stream\")) {\n throw new Error(\n `Expected response header Content-Type to contain 'text/event-stream', got '${contentType}'`\n );\n }\n\n reader = stream.getReader();\n\n try {\n while (true) {\n // Check abort signal before each read\n if (options.signal?.aborted) {\n await reader.cancel();\n return;\n }\n\n const { done, value } = await reader.read();\n\n if (done) {\n // Stream completed successfully\n break;\n }\n\n // Track last event ID for reconnection\n if (value.id) {\n lastEventId = value.id;\n }\n\n yield value;\n }\n\n // Stream completed successfully, exit retry loop\n break;\n } catch (error) {\n // Error during streaming - attempt reconnect if we have a location header\n if (reconnectPath && !options.signal?.aborted) {\n shouldRetry = true;\n } else {\n throw error;\n }\n } finally {\n if (reader) {\n try {\n reader.releaseLock();\n } catch {\n // Ignore errors when releasing lock\n }\n }\n }\n } catch (error) {\n lastError = error;\n\n // Only retry if we have reconnection capability and it's a network error\n if (isNetworkError(error) && reconnectPath && !options.signal?.aborted) {\n shouldRetry = true;\n } else {\n throw error;\n }\n }\n\n if (shouldRetry) {\n attempt += 1;\n if (attempt > maxRetries) {\n throw new MaxReconnectAttemptsError(maxRetries, lastError);\n }\n\n // Notify about reconnection attempt\n options.onReconnect?.({ attempt, lastEventId, cause: lastError });\n\n // Exponential backoff with jitter: min(1000 * 2^attempt, 5000) + random jitter\n const baseDelay = Math.min(1000 * 2 ** (attempt - 1), 5000);\n const jitter = Math.random() * 1000;\n const delay = baseDelay + jitter;\n\n await new Promise((resolve) => {\n setTimeout(resolve, delay);\n });\n\n continue;\n }\n\n // Successfully completed\n break;\n }\n}\n\n/*\n * Support async iterator syntax for ReadableStreams in all environments.\n * Source: https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490\n */\nexport class IterableReadableStream<T>\n extends ReadableStream<T>\n implements IterableReadableStreamInterface<T>\n{\n public reader: ReadableStreamDefaultReader<T>;\n\n ensureReader() {\n if (!this.reader) {\n this.reader = this.getReader();\n }\n }\n\n async next(): Promise<IteratorResult<T>> {\n this.ensureReader();\n try {\n const result = await this.reader.read();\n if (result.done) {\n this.reader.releaseLock(); // release lock when stream becomes closed\n return {\n done: true,\n value: undefined,\n };\n } else {\n return {\n done: false,\n value: result.value,\n };\n }\n } catch (e) {\n this.reader.releaseLock(); // release lock when stream becomes errored\n throw e;\n }\n }\n\n async return(): Promise<IteratorResult<T>> {\n this.ensureReader();\n // If wrapped in a Node stream, cancel is already called.\n if (this.locked) {\n const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet\n this.reader.releaseLock(); // release lock first\n await cancelPromise; // now await it\n }\n return { done: true, value: undefined };\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async throw(e: any): Promise<IteratorResult<T>> {\n this.ensureReader();\n if (this.locked) {\n const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet\n this.reader.releaseLock(); // release lock first\n await cancelPromise; // now await it\n }\n throw e;\n }\n\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore Not present in Node 18 types, required in latest Node 22\n async [Symbol.asyncDispose]() {\n await this.return();\n }\n\n [Symbol.asyncIterator]() {\n return this;\n }\n\n static fromReadableStream<T>(stream: ReadableStream<T>) {\n // From https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams#reading_the_stream\n const reader = stream.getReader();\n return new IterableReadableStream<T>({\n start(controller) {\n return pump();\n function pump(): Promise<T | undefined> {\n return reader.read().then(({ done, value }) => {\n // When no more data needs to be consumed, close the stream\n if (done) {\n controller.close();\n return;\n }\n // Enqueue the next data chunk into our target stream\n controller.enqueue(value);\n return pump();\n });\n }\n },\n cancel() {\n reader.releaseLock();\n },\n });\n }\n\n static fromAsyncGenerator<T>(generator: AsyncGenerator<T>) {\n return new IterableReadableStream<T>({\n async pull(controller) {\n const { value, done } = await generator.next();\n // When no more data needs to be consumed, close the stream\n if (done) {\n controller.close();\n }\n // Fix: `else if (value)` will hang the streaming when nullish value (e.g. empty string) is pulled\n controller.enqueue(value);\n },\n async cancel(reason) {\n await generator.return(reason);\n },\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"stream.cjs","names":["isNetworkError","reconnectDelayMs"],"sources":["../../src/utils/stream.ts"],"sourcesContent":["import { isNetworkError } from \"./error.js\";\nimport {\n DEFAULT_MAX_RECONNECT_ATTEMPTS,\n reconnectDelayMs,\n} from \"./reconnect.js\";\n\n// in this case don't quite match.\ntype IterableReadableStreamInterface<T> = ReadableStream<T> & AsyncIterable<T>;\n\n/**\n * Options for streaming with automatic retry logic.\n */\nexport interface StreamWithRetryOptions {\n /**\n * Maximum number of reconnection attempts. Default is\n * {@link DEFAULT_MAX_RECONNECT_ATTEMPTS}.\n */\n maxRetries?: number;\n\n /**\n * AbortSignal to cancel the stream.\n */\n signal?: AbortSignal;\n\n /**\n * Callback invoked when a reconnection attempt is made.\n */\n onReconnect?: (options: {\n attempt: number;\n lastEventId?: string;\n cause: unknown;\n }) => void;\n}\n\n/**\n * Parameters for making a stream request\n */\nexport interface StreamRequestParams {\n /**\n * Last event ID to resume from, if available\n */\n lastEventId?: string;\n\n /**\n * Optional reconnection path from the Location header\n */\n reconnectPath?: string;\n}\n\n/**\n * Error thrown when maximum reconnection attempts are exceeded.\n */\nexport class MaxReconnectAttemptsError extends Error {\n constructor(maxAttempts: number, cause: unknown) {\n super(`Exceeded maximum SSE reconnection attempts (${maxAttempts})`);\n this.name = \"MaxReconnectAttemptsError\";\n this.cause = cause;\n }\n}\n\n/**\n * Error injected into the stream by {@link idleReconnectStream} when no lines\n * arrive within the active idle window. Surfacing this during the read is what\n * lets the reconnect loops in `streamWithRetry` and the protocol SSE transport\n * recover from a half-open socket — one that was silently dropped (e.g. a hard\n * pod kill on a platform revision rollover) without a TCP FIN/RST, so neither\n * a `done` nor a thrown network error ever arrives.\n */\nexport class StreamIdleTimeoutError extends Error {\n readonly idleTimeoutMs: number;\n\n constructor(idleTimeoutMs: number) {\n super(\n `No SSE bytes received for ${idleTimeoutMs}ms; assuming the connection is half-open and reconnecting.`\n );\n this.name = \"StreamIdleTimeoutError\";\n this.idleTimeoutMs = idleTimeoutMs;\n }\n}\n\n/** `\":\"` — first byte of an SSE comment / keep-alive line. */\nconst SSE_COMMENT_BYTE = 0x3a;\n\n/**\n * How {@link idleReconnectStream} decides when the connection is idle.\n *\n * - A `number` is a fixed idle window in milliseconds: the watchdog arms\n * immediately (even before the first byte) and trips after that long with no\n * activity. Use when you know your server's behaviour and want guaranteed\n * coverage from t=0; it does not depend on heartbeats.\n * - `\"auto\"` is heartbeat-adaptive: the watchdog stays dormant until it has\n * observed the server's SSE keep-alive comments (e.g. LangGraph Platform's\n * `: heartbeat` every ~5s), then arms with a window derived from the\n * observed cadence. On a server that never sends heartbeats it never arms,\n * so it can't false-fire during a legitimately quiet period.\n */\nexport type IdleReconnectMode = number | \"auto\";\n\n/**\n * Default idle-reconnect policy for SSE streams. Heartbeat-adaptive\n * `\"auto\"` stays dormant unless the server emits keep-alive heartbeats.\n * Pass `0` to disable.\n */\nexport const DEFAULT_IDLE_RECONNECT: IdleReconnectMode = \"auto\";\n\nexport interface IdleReconnectStreamOptions {\n /** Fixed timeout (ms) or `\"auto\"` heartbeat-adaptive. */\n mode: IdleReconnectMode;\n /** `\"auto\"`: multiplier applied to the observed heartbeat interval. Default 3. */\n timeoutFactor?: number;\n /** `\"auto\"`: lower clamp for the derived timeout (ms). Default 6000. */\n minTimeoutMs?: number;\n /** `\"auto\"`: upper clamp for the derived timeout (ms). Default 30000. */\n maxTimeoutMs?: number;\n /** Fired immediately before the stream is errored, for logging/metrics. */\n onIdle?: (info: { timeoutMs: number; source: \"fixed\" | \"heartbeat\" }) => void;\n}\n\n/**\n * A pass-through {@link TransformStream} that errors the stream when it goes\n * idle, so the surrounding reconnect logic can recover a half-open socket.\n *\n * MUST sit on the *line* stream — i.e. after\n * {@link import(\"./sse.js\").BytesLineDecoder} but before\n * {@link import(\"./sse.js\").SSEDecoder} (which discards `:` comment lines).\n * Operating at the line level lets the watchdog both (a) reset on any line\n * (data *or* heartbeat = liveness) and (b) recognise heartbeat comment lines\n * to drive `\"auto\"` mode.\n *\n * In `\"auto\"` mode the watchdog is intentionally dormant until it has seen at\n * least two heartbeats (so it can measure the cadence). This means a socket\n * that dies inside the first heartbeat interval won't be caught until a\n * heartbeat would have been due — an acceptable trade for never false-firing\n * on heartbeat-less servers. Pass a fixed `number` if you need coverage from\n * the very first byte.\n */\nexport function idleReconnectStream(\n options: IdleReconnectStreamOptions\n): TransformStream<Uint8Array, Uint8Array> {\n const factor = options.timeoutFactor ?? 3;\n const minTimeoutMs = options.minTimeoutMs ?? 6_000;\n const maxTimeoutMs = options.maxTimeoutMs ?? 30_000;\n const fixedTimeoutMs = typeof options.mode === \"number\" ? options.mode : null;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let controllerRef: TransformStreamDefaultController<Uint8Array> | undefined;\n\n // `\"auto\"` cadence inference.\n let lastHeartbeatAt: number | undefined;\n // The active idle window: the fixed value, or (auto) the heartbeat-derived\n // value once known. `null` means \"not armed yet\".\n let derivedTimeoutMs: number | null = fixedTimeoutMs;\n\n const clear = () => {\n if (timer != null) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n\n const arm = () => {\n clear();\n const timeoutMs = derivedTimeoutMs;\n if (timeoutMs == null || timeoutMs <= 0) return;\n timer = setTimeout(() => {\n options.onIdle?.({\n timeoutMs,\n source: fixedTimeoutMs != null ? \"fixed\" : \"heartbeat\",\n });\n try {\n controllerRef?.error(new StreamIdleTimeoutError(timeoutMs));\n } catch {\n // Stream already closed/errored/cancelled — nothing to abort.\n }\n }, timeoutMs);\n // Don't let the watchdog by itself keep a Node process alive.\n (timer as unknown as { unref?: () => void }).unref?.();\n };\n\n const noteHeartbeat = () => {\n if (fixedTimeoutMs != null) return; // cadence irrelevant in fixed mode\n const now = Date.now();\n if (lastHeartbeatAt != null) {\n const interval = now - lastHeartbeatAt;\n if (interval > 0) {\n const candidate = Math.min(\n Math.max(interval * factor, minTimeoutMs),\n maxTimeoutMs\n );\n // Keep the most conservative (largest) window observed so far.\n derivedTimeoutMs =\n derivedTimeoutMs == null\n ? candidate\n : Math.max(derivedTimeoutMs, candidate);\n }\n }\n lastHeartbeatAt = now;\n };\n\n return new TransformStream<Uint8Array, Uint8Array>({\n start(controller) {\n controllerRef = controller;\n // Fixed mode arms eagerly (also catches pre-first-byte silence). Auto\n // mode waits until a cadence is established in `transform`.\n arm();\n },\n transform(line, controller) {\n // A line beginning with \":\" is an SSE comment / keep-alive heartbeat.\n if (line.length > 0 && line[0] === SSE_COMMENT_BYTE) {\n noteHeartbeat();\n }\n // Any line is liveness — (re)arm the idle timer.\n arm();\n controller.enqueue(line);\n },\n flush() {\n clear();\n },\n });\n}\n\n/**\n * Stream with automatic retry logic for SSE connections.\n * Implements reconnection behavior similar to the Python SDK.\n *\n * @param makeRequest Function to make requests. When `params` is undefined/empty, it's the initial request.\n * When `params.reconnectPath` is provided, it's a reconnection request.\n * @param options Configuration options\n * @returns AsyncGenerator yielding stream events\n */\nexport async function* streamWithRetry<T extends { id?: string }>(\n makeRequest: (params?: StreamRequestParams) => Promise<{\n response: Response;\n stream: ReadableStream<T>;\n }>,\n options: StreamWithRetryOptions = {}\n): AsyncGenerator<T> {\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;\n let attempt = 0;\n let lastEventId: string | undefined;\n let reconnectPath: string | undefined;\n\n while (true) {\n let shouldRetry = false;\n let lastError: unknown;\n let reader: ReadableStreamDefaultReader<T> | undefined;\n\n try {\n // Check if aborted before making request\n if (options.signal?.aborted) return;\n\n // Make request - initial if no reconnect path, reconnect otherwise\n const { response, stream } = await makeRequest(\n reconnectPath ? { lastEventId, reconnectPath } : undefined\n );\n\n // Check for Location header (server-provided reconnection path)\n const locationHeader = response.headers.get(\"location\");\n if (locationHeader) {\n reconnectPath = locationHeader;\n }\n\n // Verify content type\n const contentType = response.headers.get(\"content-type\")?.split(\";\")[0];\n if (contentType && !contentType.includes(\"text/event-stream\")) {\n throw new Error(\n `Expected response header Content-Type to contain 'text/event-stream', got '${contentType}'`\n );\n }\n\n reader = stream.getReader();\n\n try {\n while (true) {\n // Check abort signal before each read\n if (options.signal?.aborted) {\n await reader.cancel();\n return;\n }\n\n const { done, value } = await reader.read();\n\n if (done) {\n // Stream completed successfully\n break;\n }\n\n // Track last event ID for reconnection\n if (value.id) {\n lastEventId = value.id;\n }\n\n yield value;\n }\n\n // Stream completed successfully, exit retry loop\n break;\n } catch (error) {\n // Error during streaming - attempt reconnect if we have a location header\n if (reconnectPath && !options.signal?.aborted) {\n shouldRetry = true;\n } else {\n throw error;\n }\n } finally {\n if (reader) {\n try {\n reader.releaseLock();\n } catch {\n // Ignore errors when releasing lock\n }\n }\n }\n } catch (error) {\n lastError = error;\n\n // Only retry if we have reconnection capability and it's a network error\n if (isNetworkError(error) && reconnectPath && !options.signal?.aborted) {\n shouldRetry = true;\n } else {\n throw error;\n }\n }\n\n if (shouldRetry) {\n attempt += 1;\n if (attempt > maxRetries) {\n throw new MaxReconnectAttemptsError(maxRetries, lastError);\n }\n\n // Notify about reconnection attempt\n options.onReconnect?.({ attempt, lastEventId, cause: lastError });\n\n // Exponential backoff with jitter (shared with protocol transports).\n const delay = reconnectDelayMs(attempt);\n\n await new Promise((resolve) => {\n setTimeout(resolve, delay);\n });\n\n continue;\n }\n\n // Successfully completed\n break;\n }\n}\n\n/*\n * Support async iterator syntax for ReadableStreams in all environments.\n * Source: https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490\n */\nexport class IterableReadableStream<T>\n extends ReadableStream<T>\n implements IterableReadableStreamInterface<T>\n{\n public reader: ReadableStreamDefaultReader<T>;\n\n ensureReader() {\n if (!this.reader) {\n this.reader = this.getReader();\n }\n }\n\n async next(): Promise<IteratorResult<T>> {\n this.ensureReader();\n try {\n const result = await this.reader.read();\n if (result.done) {\n this.reader.releaseLock(); // release lock when stream becomes closed\n return {\n done: true,\n value: undefined,\n };\n } else {\n return {\n done: false,\n value: result.value,\n };\n }\n } catch (e) {\n this.reader.releaseLock(); // release lock when stream becomes errored\n throw e;\n }\n }\n\n async return(): Promise<IteratorResult<T>> {\n this.ensureReader();\n // If wrapped in a Node stream, cancel is already called.\n if (this.locked) {\n const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet\n this.reader.releaseLock(); // release lock first\n await cancelPromise; // now await it\n }\n return { done: true, value: undefined };\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async throw(e: any): Promise<IteratorResult<T>> {\n this.ensureReader();\n if (this.locked) {\n const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet\n this.reader.releaseLock(); // release lock first\n await cancelPromise; // now await it\n }\n throw e;\n }\n\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore Not present in Node 18 types, required in latest Node 22\n async [Symbol.asyncDispose]() {\n await this.return();\n }\n\n [Symbol.asyncIterator]() {\n return this;\n }\n\n static fromReadableStream<T>(stream: ReadableStream<T>) {\n // From https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams#reading_the_stream\n const reader = stream.getReader();\n return new IterableReadableStream<T>({\n start(controller) {\n return pump();\n function pump(): Promise<T | undefined> {\n return reader.read().then(({ done, value }) => {\n // When no more data needs to be consumed, close the stream\n if (done) {\n controller.close();\n return;\n }\n // Enqueue the next data chunk into our target stream\n controller.enqueue(value);\n return pump();\n });\n }\n },\n cancel() {\n reader.releaseLock();\n },\n });\n }\n\n static fromAsyncGenerator<T>(generator: AsyncGenerator<T>) {\n return new IterableReadableStream<T>({\n async pull(controller) {\n const { value, done } = await generator.next();\n // When no more data needs to be consumed, close the stream\n if (done) {\n controller.close();\n }\n // Fix: `else if (value)` will hang the streaming when nullish value (e.g. empty string) is pulled\n controller.enqueue(value);\n },\n async cancel(reason) {\n await generator.return(reason);\n },\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.d.cts","names":[],"sources":["../../src/utils/stream.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"stream.d.cts","names":[],"sources":["../../src/utils/stream.ts"],"mappings":";KAOK,+BAAA,MAAqC,cAAA,CAAe,CAAA,IAAK,aAAA,CAAc,CAAA;;;;;;;;;;;;;;;;;KAyFhE,iBAAA;AAAA,cAgQC,sBAAA,YACH,cAAA,CAAe,CAAA,aACZ,+BAAA,CAAgC,CAAA;EAEpC,MAAA,EAAQ,2BAAA,CAA4B,CAAA;EAE3C,YAAA,CAAA;EAMM,IAAA,CAAA,GAAQ,OAAA,CAAQ,cAAA,CAAe,CAAA;EAsB/B,MAAA,CAAA,GAAU,OAAA,CAAQ,cAAA,CAAe,CAAA;EAYjC,KAAA,CAAM,CAAA,QAAS,OAAA,CAAQ,cAAA,CAAe,CAAA;EAAA,CAYrC,MAAA,CAAO,YAAA,KAAa,OAAA;EAAA,CAI1B,MAAA,CAAO,aAAA;EAAA,OAID,kBAAA,GAAA,CAAsB,MAAA,EAAQ,cAAA,CAAe,CAAA,IAAE,sBAAA,CAAA,CAAA;EAAA,OAyB/C,kBAAA,GAAA,CAAsB,SAAA,EAAW,cAAA,CAAe,CAAA,IAAE,sBAAA,CAAA,CAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.d.ts","names":[],"sources":["../../src/utils/stream.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"stream.d.ts","names":[],"sources":["../../src/utils/stream.ts"],"mappings":";KAOK,+BAAA,MAAqC,cAAA,CAAe,CAAA,IAAK,aAAA,CAAc,CAAA;;;;;;;;;;;;;;;;;KAyFhE,iBAAA;AAAA,cAgQC,sBAAA,YACH,cAAA,CAAe,CAAA,aACZ,+BAAA,CAAgC,CAAA;EAEpC,MAAA,EAAQ,2BAAA,CAA4B,CAAA;EAE3C,YAAA,CAAA;EAMM,IAAA,CAAA,GAAQ,OAAA,CAAQ,cAAA,CAAe,CAAA;EAsB/B,MAAA,CAAA,GAAU,OAAA,CAAQ,cAAA,CAAe,CAAA;EAYjC,KAAA,CAAM,CAAA,QAAS,OAAA,CAAQ,cAAA,CAAe,CAAA;EAAA,CAYrC,MAAA,CAAO,YAAA,KAAa,OAAA;EAAA,CAI1B,MAAA,CAAO,aAAA;EAAA,OAID,kBAAA,GAAA,CAAsB,MAAA,EAAQ,cAAA,CAAe,CAAA,IAAE,sBAAA,CAAA,CAAA;EAAA,OAyB/C,kBAAA,GAAA,CAAsB,SAAA,EAAW,cAAA,CAAe,CAAA,IAAE,sBAAA,CAAA,CAAA;AAAA"}
|
package/dist/utils/stream.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isNetworkError } from "./error.js";
|
|
2
|
+
import { reconnectDelayMs } from "./reconnect.js";
|
|
2
3
|
//#region src/utils/stream.ts
|
|
3
4
|
/**
|
|
4
5
|
* Error thrown when maximum reconnection attempts are exceeded.
|
|
@@ -29,6 +30,12 @@ var StreamIdleTimeoutError = class extends Error {
|
|
|
29
30
|
/** `":"` — first byte of an SSE comment / keep-alive line. */
|
|
30
31
|
const SSE_COMMENT_BYTE = 58;
|
|
31
32
|
/**
|
|
33
|
+
* Default idle-reconnect policy for SSE streams. Heartbeat-adaptive
|
|
34
|
+
* `"auto"` stays dormant unless the server emits keep-alive heartbeats.
|
|
35
|
+
* Pass `0` to disable.
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_IDLE_RECONNECT = "auto";
|
|
38
|
+
/**
|
|
32
39
|
* A pass-through {@link TransformStream} that errors the stream when it goes
|
|
33
40
|
* idle, so the surrounding reconnect logic can recover a half-open socket.
|
|
34
41
|
*
|
|
@@ -165,7 +172,7 @@ async function* streamWithRetry(makeRequest, options = {}) {
|
|
|
165
172
|
lastEventId,
|
|
166
173
|
cause: lastError
|
|
167
174
|
});
|
|
168
|
-
const delay =
|
|
175
|
+
const delay = reconnectDelayMs(attempt);
|
|
169
176
|
await new Promise((resolve) => {
|
|
170
177
|
setTimeout(resolve, delay);
|
|
171
178
|
});
|
|
@@ -260,6 +267,6 @@ var IterableReadableStream = class IterableReadableStream extends ReadableStream
|
|
|
260
267
|
}
|
|
261
268
|
};
|
|
262
269
|
//#endregion
|
|
263
|
-
export { IterableReadableStream, idleReconnectStream, streamWithRetry };
|
|
270
|
+
export { DEFAULT_IDLE_RECONNECT, IterableReadableStream, idleReconnectStream, streamWithRetry };
|
|
264
271
|
|
|
265
272
|
//# sourceMappingURL=stream.js.map
|
package/dist/utils/stream.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.js","names":[],"sources":["../../src/utils/stream.ts"],"sourcesContent":["import { isNetworkError } from \"./error.js\";\n\n// in this case don't quite match.\ntype IterableReadableStreamInterface<T> = ReadableStream<T> & AsyncIterable<T>;\n\n/**\n * Options for streaming with automatic retry logic.\n */\nexport interface StreamWithRetryOptions {\n /**\n * Maximum number of reconnection attempts. Default is 5.\n */\n maxRetries?: number;\n\n /**\n * AbortSignal to cancel the stream.\n */\n signal?: AbortSignal;\n\n /**\n * Callback invoked when a reconnection attempt is made.\n */\n onReconnect?: (options: {\n attempt: number;\n lastEventId?: string;\n cause: unknown;\n }) => void;\n}\n\n/**\n * Parameters for making a stream request\n */\nexport interface StreamRequestParams {\n /**\n * Last event ID to resume from, if available\n */\n lastEventId?: string;\n\n /**\n * Optional reconnection path from the Location header\n */\n reconnectPath?: string;\n}\n\n/**\n * Error thrown when maximum reconnection attempts are exceeded.\n */\nexport class MaxReconnectAttemptsError extends Error {\n constructor(maxAttempts: number, cause: unknown) {\n super(`Exceeded maximum SSE reconnection attempts (${maxAttempts})`);\n this.name = \"MaxReconnectAttemptsError\";\n this.cause = cause;\n }\n}\n\n/**\n * Error injected into the stream by {@link idleReconnectStream} when no lines\n * arrive within the active idle window. Surfacing this during the read is what\n * lets the reconnect loops in `streamWithRetry` and the protocol SSE transport\n * recover from a half-open socket — one that was silently dropped (e.g. a hard\n * pod kill on a platform revision rollover) without a TCP FIN/RST, so neither\n * a `done` nor a thrown network error ever arrives.\n */\nexport class StreamIdleTimeoutError extends Error {\n readonly idleTimeoutMs: number;\n\n constructor(idleTimeoutMs: number) {\n super(\n `No SSE bytes received for ${idleTimeoutMs}ms; assuming the connection is half-open and reconnecting.`\n );\n this.name = \"StreamIdleTimeoutError\";\n this.idleTimeoutMs = idleTimeoutMs;\n }\n}\n\n/** `\":\"` — first byte of an SSE comment / keep-alive line. */\nconst SSE_COMMENT_BYTE = 0x3a;\n\n/**\n * How {@link idleReconnectStream} decides when the connection is idle.\n *\n * - A `number` is a fixed idle window in milliseconds: the watchdog arms\n * immediately (even before the first byte) and trips after that long with no\n * activity. Use when you know your server's behaviour and want guaranteed\n * coverage from t=0; it does not depend on heartbeats.\n * - `\"auto\"` is heartbeat-adaptive: the watchdog stays dormant until it has\n * observed the server's SSE keep-alive comments (e.g. LangGraph Platform's\n * `: heartbeat` every ~5s), then arms with a window derived from the\n * observed cadence. On a server that never sends heartbeats it never arms,\n * so it can't false-fire during a legitimately quiet period.\n */\nexport type IdleReconnectMode = number | \"auto\";\n\nexport interface IdleReconnectStreamOptions {\n /** Fixed timeout (ms) or `\"auto\"` heartbeat-adaptive. */\n mode: IdleReconnectMode;\n /** `\"auto\"`: multiplier applied to the observed heartbeat interval. Default 3. */\n timeoutFactor?: number;\n /** `\"auto\"`: lower clamp for the derived timeout (ms). Default 6000. */\n minTimeoutMs?: number;\n /** `\"auto\"`: upper clamp for the derived timeout (ms). Default 30000. */\n maxTimeoutMs?: number;\n /** Fired immediately before the stream is errored, for logging/metrics. */\n onIdle?: (info: { timeoutMs: number; source: \"fixed\" | \"heartbeat\" }) => void;\n}\n\n/**\n * A pass-through {@link TransformStream} that errors the stream when it goes\n * idle, so the surrounding reconnect logic can recover a half-open socket.\n *\n * MUST sit on the *line* stream — i.e. after\n * {@link import(\"./sse.js\").BytesLineDecoder} but before\n * {@link import(\"./sse.js\").SSEDecoder} (which discards `:` comment lines).\n * Operating at the line level lets the watchdog both (a) reset on any line\n * (data *or* heartbeat = liveness) and (b) recognise heartbeat comment lines\n * to drive `\"auto\"` mode.\n *\n * In `\"auto\"` mode the watchdog is intentionally dormant until it has seen at\n * least two heartbeats (so it can measure the cadence). This means a socket\n * that dies inside the first heartbeat interval won't be caught until a\n * heartbeat would have been due — an acceptable trade for never false-firing\n * on heartbeat-less servers. Pass a fixed `number` if you need coverage from\n * the very first byte.\n */\nexport function idleReconnectStream(\n options: IdleReconnectStreamOptions\n): TransformStream<Uint8Array, Uint8Array> {\n const factor = options.timeoutFactor ?? 3;\n const minTimeoutMs = options.minTimeoutMs ?? 6_000;\n const maxTimeoutMs = options.maxTimeoutMs ?? 30_000;\n const fixedTimeoutMs = typeof options.mode === \"number\" ? options.mode : null;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let controllerRef: TransformStreamDefaultController<Uint8Array> | undefined;\n\n // `\"auto\"` cadence inference.\n let lastHeartbeatAt: number | undefined;\n // The active idle window: the fixed value, or (auto) the heartbeat-derived\n // value once known. `null` means \"not armed yet\".\n let derivedTimeoutMs: number | null = fixedTimeoutMs;\n\n const clear = () => {\n if (timer != null) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n\n const arm = () => {\n clear();\n const timeoutMs = derivedTimeoutMs;\n if (timeoutMs == null || timeoutMs <= 0) return;\n timer = setTimeout(() => {\n options.onIdle?.({\n timeoutMs,\n source: fixedTimeoutMs != null ? \"fixed\" : \"heartbeat\",\n });\n try {\n controllerRef?.error(new StreamIdleTimeoutError(timeoutMs));\n } catch {\n // Stream already closed/errored/cancelled — nothing to abort.\n }\n }, timeoutMs);\n // Don't let the watchdog by itself keep a Node process alive.\n (timer as unknown as { unref?: () => void }).unref?.();\n };\n\n const noteHeartbeat = () => {\n if (fixedTimeoutMs != null) return; // cadence irrelevant in fixed mode\n const now = Date.now();\n if (lastHeartbeatAt != null) {\n const interval = now - lastHeartbeatAt;\n if (interval > 0) {\n const candidate = Math.min(\n Math.max(interval * factor, minTimeoutMs),\n maxTimeoutMs\n );\n // Keep the most conservative (largest) window observed so far.\n derivedTimeoutMs =\n derivedTimeoutMs == null\n ? candidate\n : Math.max(derivedTimeoutMs, candidate);\n }\n }\n lastHeartbeatAt = now;\n };\n\n return new TransformStream<Uint8Array, Uint8Array>({\n start(controller) {\n controllerRef = controller;\n // Fixed mode arms eagerly (also catches pre-first-byte silence). Auto\n // mode waits until a cadence is established in `transform`.\n arm();\n },\n transform(line, controller) {\n // A line beginning with \":\" is an SSE comment / keep-alive heartbeat.\n if (line.length > 0 && line[0] === SSE_COMMENT_BYTE) {\n noteHeartbeat();\n }\n // Any line is liveness — (re)arm the idle timer.\n arm();\n controller.enqueue(line);\n },\n flush() {\n clear();\n },\n });\n}\n\n/**\n * Stream with automatic retry logic for SSE connections.\n * Implements reconnection behavior similar to the Python SDK.\n *\n * @param makeRequest Function to make requests. When `params` is undefined/empty, it's the initial request.\n * When `params.reconnectPath` is provided, it's a reconnection request.\n * @param options Configuration options\n * @returns AsyncGenerator yielding stream events\n */\nexport async function* streamWithRetry<T extends { id?: string }>(\n makeRequest: (params?: StreamRequestParams) => Promise<{\n response: Response;\n stream: ReadableStream<T>;\n }>,\n options: StreamWithRetryOptions = {}\n): AsyncGenerator<T> {\n const maxRetries = options.maxRetries ?? 5;\n let attempt = 0;\n let lastEventId: string | undefined;\n let reconnectPath: string | undefined;\n\n while (true) {\n let shouldRetry = false;\n let lastError: unknown;\n let reader: ReadableStreamDefaultReader<T> | undefined;\n\n try {\n // Check if aborted before making request\n if (options.signal?.aborted) return;\n\n // Make request - initial if no reconnect path, reconnect otherwise\n const { response, stream } = await makeRequest(\n reconnectPath ? { lastEventId, reconnectPath } : undefined\n );\n\n // Check for Location header (server-provided reconnection path)\n const locationHeader = response.headers.get(\"location\");\n if (locationHeader) {\n reconnectPath = locationHeader;\n }\n\n // Verify content type\n const contentType = response.headers.get(\"content-type\")?.split(\";\")[0];\n if (contentType && !contentType.includes(\"text/event-stream\")) {\n throw new Error(\n `Expected response header Content-Type to contain 'text/event-stream', got '${contentType}'`\n );\n }\n\n reader = stream.getReader();\n\n try {\n while (true) {\n // Check abort signal before each read\n if (options.signal?.aborted) {\n await reader.cancel();\n return;\n }\n\n const { done, value } = await reader.read();\n\n if (done) {\n // Stream completed successfully\n break;\n }\n\n // Track last event ID for reconnection\n if (value.id) {\n lastEventId = value.id;\n }\n\n yield value;\n }\n\n // Stream completed successfully, exit retry loop\n break;\n } catch (error) {\n // Error during streaming - attempt reconnect if we have a location header\n if (reconnectPath && !options.signal?.aborted) {\n shouldRetry = true;\n } else {\n throw error;\n }\n } finally {\n if (reader) {\n try {\n reader.releaseLock();\n } catch {\n // Ignore errors when releasing lock\n }\n }\n }\n } catch (error) {\n lastError = error;\n\n // Only retry if we have reconnection capability and it's a network error\n if (isNetworkError(error) && reconnectPath && !options.signal?.aborted) {\n shouldRetry = true;\n } else {\n throw error;\n }\n }\n\n if (shouldRetry) {\n attempt += 1;\n if (attempt > maxRetries) {\n throw new MaxReconnectAttemptsError(maxRetries, lastError);\n }\n\n // Notify about reconnection attempt\n options.onReconnect?.({ attempt, lastEventId, cause: lastError });\n\n // Exponential backoff with jitter: min(1000 * 2^attempt, 5000) + random jitter\n const baseDelay = Math.min(1000 * 2 ** (attempt - 1), 5000);\n const jitter = Math.random() * 1000;\n const delay = baseDelay + jitter;\n\n await new Promise((resolve) => {\n setTimeout(resolve, delay);\n });\n\n continue;\n }\n\n // Successfully completed\n break;\n }\n}\n\n/*\n * Support async iterator syntax for ReadableStreams in all environments.\n * Source: https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490\n */\nexport class IterableReadableStream<T>\n extends ReadableStream<T>\n implements IterableReadableStreamInterface<T>\n{\n public reader: ReadableStreamDefaultReader<T>;\n\n ensureReader() {\n if (!this.reader) {\n this.reader = this.getReader();\n }\n }\n\n async next(): Promise<IteratorResult<T>> {\n this.ensureReader();\n try {\n const result = await this.reader.read();\n if (result.done) {\n this.reader.releaseLock(); // release lock when stream becomes closed\n return {\n done: true,\n value: undefined,\n };\n } else {\n return {\n done: false,\n value: result.value,\n };\n }\n } catch (e) {\n this.reader.releaseLock(); // release lock when stream becomes errored\n throw e;\n }\n }\n\n async return(): Promise<IteratorResult<T>> {\n this.ensureReader();\n // If wrapped in a Node stream, cancel is already called.\n if (this.locked) {\n const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet\n this.reader.releaseLock(); // release lock first\n await cancelPromise; // now await it\n }\n return { done: true, value: undefined };\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async throw(e: any): Promise<IteratorResult<T>> {\n this.ensureReader();\n if (this.locked) {\n const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet\n this.reader.releaseLock(); // release lock first\n await cancelPromise; // now await it\n }\n throw e;\n }\n\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore Not present in Node 18 types, required in latest Node 22\n async [Symbol.asyncDispose]() {\n await this.return();\n }\n\n [Symbol.asyncIterator]() {\n return this;\n }\n\n static fromReadableStream<T>(stream: ReadableStream<T>) {\n // From https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams#reading_the_stream\n const reader = stream.getReader();\n return new IterableReadableStream<T>({\n start(controller) {\n return pump();\n function pump(): Promise<T | undefined> {\n return reader.read().then(({ done, value }) => {\n // When no more data needs to be consumed, close the stream\n if (done) {\n controller.close();\n return;\n }\n // Enqueue the next data chunk into our target stream\n controller.enqueue(value);\n return pump();\n });\n }\n },\n cancel() {\n reader.releaseLock();\n },\n });\n }\n\n static fromAsyncGenerator<T>(generator: AsyncGenerator<T>) {\n return new IterableReadableStream<T>({\n async pull(controller) {\n const { value, done } = await generator.next();\n // When no more data needs to be consumed, close the stream\n if (done) {\n controller.close();\n }\n // Fix: `else if (value)` will hang the streaming when nullish value (e.g. empty string) is pulled\n controller.enqueue(value);\n },\n async cancel(reason) {\n await generator.return(reason);\n },\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"stream.js","names":[],"sources":["../../src/utils/stream.ts"],"sourcesContent":["import { isNetworkError } from \"./error.js\";\nimport {\n DEFAULT_MAX_RECONNECT_ATTEMPTS,\n reconnectDelayMs,\n} from \"./reconnect.js\";\n\n// in this case don't quite match.\ntype IterableReadableStreamInterface<T> = ReadableStream<T> & AsyncIterable<T>;\n\n/**\n * Options for streaming with automatic retry logic.\n */\nexport interface StreamWithRetryOptions {\n /**\n * Maximum number of reconnection attempts. Default is\n * {@link DEFAULT_MAX_RECONNECT_ATTEMPTS}.\n */\n maxRetries?: number;\n\n /**\n * AbortSignal to cancel the stream.\n */\n signal?: AbortSignal;\n\n /**\n * Callback invoked when a reconnection attempt is made.\n */\n onReconnect?: (options: {\n attempt: number;\n lastEventId?: string;\n cause: unknown;\n }) => void;\n}\n\n/**\n * Parameters for making a stream request\n */\nexport interface StreamRequestParams {\n /**\n * Last event ID to resume from, if available\n */\n lastEventId?: string;\n\n /**\n * Optional reconnection path from the Location header\n */\n reconnectPath?: string;\n}\n\n/**\n * Error thrown when maximum reconnection attempts are exceeded.\n */\nexport class MaxReconnectAttemptsError extends Error {\n constructor(maxAttempts: number, cause: unknown) {\n super(`Exceeded maximum SSE reconnection attempts (${maxAttempts})`);\n this.name = \"MaxReconnectAttemptsError\";\n this.cause = cause;\n }\n}\n\n/**\n * Error injected into the stream by {@link idleReconnectStream} when no lines\n * arrive within the active idle window. Surfacing this during the read is what\n * lets the reconnect loops in `streamWithRetry` and the protocol SSE transport\n * recover from a half-open socket — one that was silently dropped (e.g. a hard\n * pod kill on a platform revision rollover) without a TCP FIN/RST, so neither\n * a `done` nor a thrown network error ever arrives.\n */\nexport class StreamIdleTimeoutError extends Error {\n readonly idleTimeoutMs: number;\n\n constructor(idleTimeoutMs: number) {\n super(\n `No SSE bytes received for ${idleTimeoutMs}ms; assuming the connection is half-open and reconnecting.`\n );\n this.name = \"StreamIdleTimeoutError\";\n this.idleTimeoutMs = idleTimeoutMs;\n }\n}\n\n/** `\":\"` — first byte of an SSE comment / keep-alive line. */\nconst SSE_COMMENT_BYTE = 0x3a;\n\n/**\n * How {@link idleReconnectStream} decides when the connection is idle.\n *\n * - A `number` is a fixed idle window in milliseconds: the watchdog arms\n * immediately (even before the first byte) and trips after that long with no\n * activity. Use when you know your server's behaviour and want guaranteed\n * coverage from t=0; it does not depend on heartbeats.\n * - `\"auto\"` is heartbeat-adaptive: the watchdog stays dormant until it has\n * observed the server's SSE keep-alive comments (e.g. LangGraph Platform's\n * `: heartbeat` every ~5s), then arms with a window derived from the\n * observed cadence. On a server that never sends heartbeats it never arms,\n * so it can't false-fire during a legitimately quiet period.\n */\nexport type IdleReconnectMode = number | \"auto\";\n\n/**\n * Default idle-reconnect policy for SSE streams. Heartbeat-adaptive\n * `\"auto\"` stays dormant unless the server emits keep-alive heartbeats.\n * Pass `0` to disable.\n */\nexport const DEFAULT_IDLE_RECONNECT: IdleReconnectMode = \"auto\";\n\nexport interface IdleReconnectStreamOptions {\n /** Fixed timeout (ms) or `\"auto\"` heartbeat-adaptive. */\n mode: IdleReconnectMode;\n /** `\"auto\"`: multiplier applied to the observed heartbeat interval. Default 3. */\n timeoutFactor?: number;\n /** `\"auto\"`: lower clamp for the derived timeout (ms). Default 6000. */\n minTimeoutMs?: number;\n /** `\"auto\"`: upper clamp for the derived timeout (ms). Default 30000. */\n maxTimeoutMs?: number;\n /** Fired immediately before the stream is errored, for logging/metrics. */\n onIdle?: (info: { timeoutMs: number; source: \"fixed\" | \"heartbeat\" }) => void;\n}\n\n/**\n * A pass-through {@link TransformStream} that errors the stream when it goes\n * idle, so the surrounding reconnect logic can recover a half-open socket.\n *\n * MUST sit on the *line* stream — i.e. after\n * {@link import(\"./sse.js\").BytesLineDecoder} but before\n * {@link import(\"./sse.js\").SSEDecoder} (which discards `:` comment lines).\n * Operating at the line level lets the watchdog both (a) reset on any line\n * (data *or* heartbeat = liveness) and (b) recognise heartbeat comment lines\n * to drive `\"auto\"` mode.\n *\n * In `\"auto\"` mode the watchdog is intentionally dormant until it has seen at\n * least two heartbeats (so it can measure the cadence). This means a socket\n * that dies inside the first heartbeat interval won't be caught until a\n * heartbeat would have been due — an acceptable trade for never false-firing\n * on heartbeat-less servers. Pass a fixed `number` if you need coverage from\n * the very first byte.\n */\nexport function idleReconnectStream(\n options: IdleReconnectStreamOptions\n): TransformStream<Uint8Array, Uint8Array> {\n const factor = options.timeoutFactor ?? 3;\n const minTimeoutMs = options.minTimeoutMs ?? 6_000;\n const maxTimeoutMs = options.maxTimeoutMs ?? 30_000;\n const fixedTimeoutMs = typeof options.mode === \"number\" ? options.mode : null;\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let controllerRef: TransformStreamDefaultController<Uint8Array> | undefined;\n\n // `\"auto\"` cadence inference.\n let lastHeartbeatAt: number | undefined;\n // The active idle window: the fixed value, or (auto) the heartbeat-derived\n // value once known. `null` means \"not armed yet\".\n let derivedTimeoutMs: number | null = fixedTimeoutMs;\n\n const clear = () => {\n if (timer != null) {\n clearTimeout(timer);\n timer = undefined;\n }\n };\n\n const arm = () => {\n clear();\n const timeoutMs = derivedTimeoutMs;\n if (timeoutMs == null || timeoutMs <= 0) return;\n timer = setTimeout(() => {\n options.onIdle?.({\n timeoutMs,\n source: fixedTimeoutMs != null ? \"fixed\" : \"heartbeat\",\n });\n try {\n controllerRef?.error(new StreamIdleTimeoutError(timeoutMs));\n } catch {\n // Stream already closed/errored/cancelled — nothing to abort.\n }\n }, timeoutMs);\n // Don't let the watchdog by itself keep a Node process alive.\n (timer as unknown as { unref?: () => void }).unref?.();\n };\n\n const noteHeartbeat = () => {\n if (fixedTimeoutMs != null) return; // cadence irrelevant in fixed mode\n const now = Date.now();\n if (lastHeartbeatAt != null) {\n const interval = now - lastHeartbeatAt;\n if (interval > 0) {\n const candidate = Math.min(\n Math.max(interval * factor, minTimeoutMs),\n maxTimeoutMs\n );\n // Keep the most conservative (largest) window observed so far.\n derivedTimeoutMs =\n derivedTimeoutMs == null\n ? candidate\n : Math.max(derivedTimeoutMs, candidate);\n }\n }\n lastHeartbeatAt = now;\n };\n\n return new TransformStream<Uint8Array, Uint8Array>({\n start(controller) {\n controllerRef = controller;\n // Fixed mode arms eagerly (also catches pre-first-byte silence). Auto\n // mode waits until a cadence is established in `transform`.\n arm();\n },\n transform(line, controller) {\n // A line beginning with \":\" is an SSE comment / keep-alive heartbeat.\n if (line.length > 0 && line[0] === SSE_COMMENT_BYTE) {\n noteHeartbeat();\n }\n // Any line is liveness — (re)arm the idle timer.\n arm();\n controller.enqueue(line);\n },\n flush() {\n clear();\n },\n });\n}\n\n/**\n * Stream with automatic retry logic for SSE connections.\n * Implements reconnection behavior similar to the Python SDK.\n *\n * @param makeRequest Function to make requests. When `params` is undefined/empty, it's the initial request.\n * When `params.reconnectPath` is provided, it's a reconnection request.\n * @param options Configuration options\n * @returns AsyncGenerator yielding stream events\n */\nexport async function* streamWithRetry<T extends { id?: string }>(\n makeRequest: (params?: StreamRequestParams) => Promise<{\n response: Response;\n stream: ReadableStream<T>;\n }>,\n options: StreamWithRetryOptions = {}\n): AsyncGenerator<T> {\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;\n let attempt = 0;\n let lastEventId: string | undefined;\n let reconnectPath: string | undefined;\n\n while (true) {\n let shouldRetry = false;\n let lastError: unknown;\n let reader: ReadableStreamDefaultReader<T> | undefined;\n\n try {\n // Check if aborted before making request\n if (options.signal?.aborted) return;\n\n // Make request - initial if no reconnect path, reconnect otherwise\n const { response, stream } = await makeRequest(\n reconnectPath ? { lastEventId, reconnectPath } : undefined\n );\n\n // Check for Location header (server-provided reconnection path)\n const locationHeader = response.headers.get(\"location\");\n if (locationHeader) {\n reconnectPath = locationHeader;\n }\n\n // Verify content type\n const contentType = response.headers.get(\"content-type\")?.split(\";\")[0];\n if (contentType && !contentType.includes(\"text/event-stream\")) {\n throw new Error(\n `Expected response header Content-Type to contain 'text/event-stream', got '${contentType}'`\n );\n }\n\n reader = stream.getReader();\n\n try {\n while (true) {\n // Check abort signal before each read\n if (options.signal?.aborted) {\n await reader.cancel();\n return;\n }\n\n const { done, value } = await reader.read();\n\n if (done) {\n // Stream completed successfully\n break;\n }\n\n // Track last event ID for reconnection\n if (value.id) {\n lastEventId = value.id;\n }\n\n yield value;\n }\n\n // Stream completed successfully, exit retry loop\n break;\n } catch (error) {\n // Error during streaming - attempt reconnect if we have a location header\n if (reconnectPath && !options.signal?.aborted) {\n shouldRetry = true;\n } else {\n throw error;\n }\n } finally {\n if (reader) {\n try {\n reader.releaseLock();\n } catch {\n // Ignore errors when releasing lock\n }\n }\n }\n } catch (error) {\n lastError = error;\n\n // Only retry if we have reconnection capability and it's a network error\n if (isNetworkError(error) && reconnectPath && !options.signal?.aborted) {\n shouldRetry = true;\n } else {\n throw error;\n }\n }\n\n if (shouldRetry) {\n attempt += 1;\n if (attempt > maxRetries) {\n throw new MaxReconnectAttemptsError(maxRetries, lastError);\n }\n\n // Notify about reconnection attempt\n options.onReconnect?.({ attempt, lastEventId, cause: lastError });\n\n // Exponential backoff with jitter (shared with protocol transports).\n const delay = reconnectDelayMs(attempt);\n\n await new Promise((resolve) => {\n setTimeout(resolve, delay);\n });\n\n continue;\n }\n\n // Successfully completed\n break;\n }\n}\n\n/*\n * Support async iterator syntax for ReadableStreams in all environments.\n * Source: https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490\n */\nexport class IterableReadableStream<T>\n extends ReadableStream<T>\n implements IterableReadableStreamInterface<T>\n{\n public reader: ReadableStreamDefaultReader<T>;\n\n ensureReader() {\n if (!this.reader) {\n this.reader = this.getReader();\n }\n }\n\n async next(): Promise<IteratorResult<T>> {\n this.ensureReader();\n try {\n const result = await this.reader.read();\n if (result.done) {\n this.reader.releaseLock(); // release lock when stream becomes closed\n return {\n done: true,\n value: undefined,\n };\n } else {\n return {\n done: false,\n value: result.value,\n };\n }\n } catch (e) {\n this.reader.releaseLock(); // release lock when stream becomes errored\n throw e;\n }\n }\n\n async return(): Promise<IteratorResult<T>> {\n this.ensureReader();\n // If wrapped in a Node stream, cancel is already called.\n if (this.locked) {\n const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet\n this.reader.releaseLock(); // release lock first\n await cancelPromise; // now await it\n }\n return { done: true, value: undefined };\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async throw(e: any): Promise<IteratorResult<T>> {\n this.ensureReader();\n if (this.locked) {\n const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet\n this.reader.releaseLock(); // release lock first\n await cancelPromise; // now await it\n }\n throw e;\n }\n\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore Not present in Node 18 types, required in latest Node 22\n async [Symbol.asyncDispose]() {\n await this.return();\n }\n\n [Symbol.asyncIterator]() {\n return this;\n }\n\n static fromReadableStream<T>(stream: ReadableStream<T>) {\n // From https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams#reading_the_stream\n const reader = stream.getReader();\n return new IterableReadableStream<T>({\n start(controller) {\n return pump();\n function pump(): Promise<T | undefined> {\n return reader.read().then(({ done, value }) => {\n // When no more data needs to be consumed, close the stream\n if (done) {\n controller.close();\n return;\n }\n // Enqueue the next data chunk into our target stream\n controller.enqueue(value);\n return pump();\n });\n }\n },\n cancel() {\n reader.releaseLock();\n },\n });\n }\n\n static fromAsyncGenerator<T>(generator: AsyncGenerator<T>) {\n return new IterableReadableStream<T>({\n async pull(controller) {\n const { value, done } = await generator.next();\n // When no more data needs to be consumed, close the stream\n if (done) {\n controller.close();\n }\n // Fix: `else if (value)` will hang the streaming when nullish value (e.g. empty string) is pulled\n controller.enqueue(value);\n },\n async cancel(reason) {\n await generator.return(reason);\n },\n });\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@langchain/langgraph-sdk",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.26",
|
|
4
4
|
"description": "Client library for interacting with the LangGraph API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -20,17 +20,17 @@
|
|
|
20
20
|
"@langchain/scripts": "^0.1.4",
|
|
21
21
|
"@tsconfig/recommended": "^1.0.2",
|
|
22
22
|
"@types/node": "^18.15.11",
|
|
23
|
-
"@types/react": "^19.2.
|
|
23
|
+
"@types/react": "^19.2.17",
|
|
24
24
|
"@types/react-dom": "^19.2.3",
|
|
25
25
|
"@types/ws": "^8.18.1",
|
|
26
26
|
"deepagents": "^1.10.5",
|
|
27
|
-
"langchain": "^1.5.
|
|
27
|
+
"langchain": "^1.5.2",
|
|
28
28
|
"react": "^19.2.7",
|
|
29
29
|
"react-dom": "^19.2.7",
|
|
30
|
-
"svelte": "^5.56.
|
|
30
|
+
"svelte": "^5.56.4",
|
|
31
31
|
"typescript": "^4.9.5 || ^5.4.5",
|
|
32
|
-
"vitest": "^4.1.
|
|
33
|
-
"vue": "^3.5.
|
|
32
|
+
"vitest": "^4.1.9",
|
|
33
|
+
"vue": "^3.5.39",
|
|
34
34
|
"ws": "^8.21.0",
|
|
35
35
|
"zod": "^4.3.5",
|
|
36
36
|
"@langchain/build": "1.0.0"
|