This guide walks application authors through the jump from the pre-v1 useStream hook (previously shipped as @langchain/langgraph-sdk/react and later re-exported as @langchain/react) to the useStream hook built for new event-based streaming, which ships with @langchain/react v1.
Short version: the useStream import name does not change, but the return shape, option bag, and protocol semantics do. Most chat apps migrate in well under an hour by following the checklists below. Apps that lean heavily on history / branch / fetchStateHistory or on a custom UseStreamTransport have more work to do.
The legacy useStream was built against the legacy streaming protocol and accreted a large surface of opt-in callbacks (onUpdateEvent, onCustomEvent, onMetadataEvent, …) plus derived state (history, branch, getMessagesMetadata, joinStream) that had to be recomputed on every render.
The v1 package hook uses new event-based streaming. In practice that means:
values / messages / toolCalls / interrupts are always available at the root with zero extra wire cost.typeof agent flows through to values, toolCalls[].args, and subagent-state maps.@langchain/react to ^1.0.0 and @langchain/langgraph-sdk to the matching new event-based streaming runtime.import { useStream } from "@langchain/react" now resolves to the hook built for new event-based streaming. useStreamExperimental is not exported from this package.onError, onFinish, onUpdateEvent, onCustomEvent, onMetadataEvent, onLangChainEvent, onDebugEvent, onCheckpointEvent, onTaskEvent, onToolEvent, onStop, fetchStateHistory, reconnectOnMount, throttle, thread, filterSubagentMessages, subagentToolNames.transport: new FetchStreamTransport(...) with transport: new HttpAgentServerAdapter(...) (see §9).branch, setBranch, history, experimental_branchTree, getMessagesMetadata, toolProgress, joinStream, switchThread, queue, activeSubagents, getSubagent, getSubagentsByType, getSubagentsByMessage.getMessagesMetadata(msg)?.firstSeenState?.parent_checkpoint with useMessageMetadata(stream, msg.id)?.parentCheckpointId (§6).stream.queue with useSubmissionQueue(stream) (§6).stream.switchThread(id) with passing a new threadId prop and letting the hook reload on change (§4).useMessages(stream, subagent) etc.) (§7).useSuspenseStream now returns the v1 shape (matching useStream minus isLoading / isThreadLoading / hydrationPromise, plus isStreaming). Remove any suspenseCache, createSuspenseCache, or fetchStateHistory props (§11).submit(..., { onDisconnect, streamResumable }) with stream.stop() (cancel) or stream.disconnect() (join/rejoin) (§5.3).tsc. The option bag and return type are now discriminated and strongly typed.assistantId, client, apiUrl, apiKey, callerOptions, defaultHeaders, threadId, onThreadId, initialValues, messagesKey, onCreated, tools, onTool all keep working.
| Option | Notes |
|---|---|
transport |
"sse" / "websocket" selects the built-in wire transport; an AgentServerAdapter instance flips the hook into the custom branch. |
fetch |
Agent Server branch only. Forwarded to the built-in SSE transport. |
webSocketFactory |
Agent Server branch only. Forwarded to the built-in WebSocket transport. |
onCompleted |
Fires with { runId?, reason } when active streaming ends; runId may be absent for re-attached in-flight runs. |
optimistic |
Controls automatic optimistic echo of submit() input. Defaults to true; set false for server-authoritative-only. |
| Legacy option | v1 replacement |
|---|---|
onError (hook-level) |
Read stream.error directly, or pass a per-submit onError via submit(input, { onError }). |
onFinish |
Use onCompleted, or derive render state from isLoading / useValues(stream). |
onUpdateEvent, onCustomEvent, onMetadataEvent, onLangChainEvent, onDebugEvent, onCheckpointEvent, onTaskEvent, onToolEvent |
Drop. Read events with useChannel / useExtension; use useChannelEffect for per-event side effects. |
onStop |
Drop. Use stream.stop() to cancel or stream.disconnect() to leave the agent running server-side (§5.3). |
fetchStateHistory |
Drop. Fork/edit flows use useMessageMetadata + submit({}, { forkFrom }) (§5). |
reconnectOnMount |
Drop. Re-attach is automatic. |
throttle |
Drop. The hook batches state updates natively. |
thread |
Drop. External thread managers should drive threadId / initialValues. |
filterSubagentMessages |
Drop. Subagent messages live on per-subagent selector hooks (§7). |
subagentToolNames |
Drop. Subagent classification is driven by new event-based streaming lifecycle events. |
See §5.4 for optimistic update behavior and §6 for selector-hook replacements, including useChannelEffect for analytics-style callbacks.
// Before
useStream({ onFinish: (state) => analytics.track("turn_finished", state) });
// After
useStream({
assistantId,
onCompleted: ({ runId, reason }) => analytics.track("turn_finished", { runId, reason }),
});
values, messages, toolCalls, interrupts / interrupt, isLoading, error, threadId, client, assistantId, submit, stop, respond, disconnect. Note submit argument types are wider (§5); stop(options?) cancels server-side by default, disconnect() is join/rejoin client-only (§5.3).
| Field | What changed |
|---|---|
subagents |
Now a ReadonlyMap<string, SubagentDiscoverySnapshot>. Snapshot carries id / name / namespace / status only — read content via selector hooks (§7). |
isThreadLoading |
Reflects the initial thread-load lifecycle rather than fetchStateHistory. |
| Legacy field | v1 replacement |
|---|---|
branch, setBranch, experimental_branchTree |
useMessageMetadata(stream, msg.id) + submit(input, { forkFrom }). |
history, fetchStateHistory |
Fetch explicitly with client.threads.getHistory(threadId) if you need it; most apps do not. |
getMessagesMetadata(msg, i) |
useMessageMetadata(stream, msg.id) returns { parentCheckpointId } (§6). |
toolProgress |
Each AssembledToolCall carries its own status — read via useToolCalls(stream). |
joinStream(runId, ...) |
Remounting the hook with the right threadId rejoins automatically. |
switchThread(newThreadId) |
Drive threadId as a prop. The hook reloads on change. |
queue |
useSubmissionQueue(stream) companion hook (§6). |
activeSubagents, getSubagent, getSubagentsByType, getSubagentsByMessage |
Iterate stream.subagents (a Map) and filter inline. |
subgraphs (ReadonlyMap<string, SubgraphDiscoverySnapshot>) and subgraphsByNode (ReadonlyMap<string, SubgraphDiscoverySnapshot[]>).
// Before
const { messages, isLoading, error, submit, branch, setBranch, getMessagesMetadata } = useStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
onError: (err) => console.error(err),
fetchStateHistory: true,
});
// After
const stream = useStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
});
const { messages, isLoading, error, submit } = stream;
useEffect(() => {
if (error) console.error(error);
}, [error]);
const { parentCheckpointId } = useMessageMetadata(stream, messages.at(-1)?.id) ?? {};
submit() signature changessubmit() now accepts either a wire-format message payload or an array of BaseMessage class instances:
await submit({ messages: [{ role: "user", content: "hi" }] });
await submit({ messages: [new HumanMessage("hi")] });
await submit({ messages: new HumanMessage("hi") });
This is driven by the WidenUpdateMessages<T> helper.
Legacy SubmitOptions field |
v1 StreamSubmitOptions equivalent |
|---|---|
context |
Fold into config.configurable. |
checkpoint: { checkpoint_id } |
forkFrom: "cp_123" (direct checkpoint id string). |
command: { resume } |
Use stream.respond() instead. |
interruptBefore, interruptAfter |
Drop — not supported with new event-based streaming. |
multitaskStrategy |
Unchanged. "rollback" (default), "reject", "enqueue" honoured client-side; "interrupt" falls back to "rollback". |
onCompletion |
Use the hook-level onCompleted option. |
onDisconnect, feedbackKeys, streamMode, runId, optimisticValues, streamSubgraphs, streamResumable, checkpointDuring |
Drop from submit. Disconnect/cancel policy lives on stop() / disconnect() (§5.3); optimistic echo is automatic (§5.4). |
(new submit option) onError |
Per-submit fire-and-forget error callback. |
(new) threadId |
Per-submit thread override. |
// Before
await submit(
{ messages: [new HumanMessage("retry")] },
{
checkpoint: { checkpoint_id: "cp_123" },
multitaskStrategy: "rollback",
optimisticValues: (prev) => ({
messages: [...prev.messages, new HumanMessage("retry")],
}),
},
);
// After
await submit(
{ messages: [new HumanMessage("retry")] },
{ forkFrom: "cp_123", multitaskStrategy: "rollback" },
);
Legacy stop() only aborted the client transport, and per-submit onDisconnect decided whether the agent kept running. v1 makes the split explicit on the stream handle:
| Legacy pattern | v1 replacement |
|---|---|
| Stop button in a normal chat (cancel the agent) | await stream.stop() — default { cancel: true } calls client.runs.cancel, then disconnects. |
| Join/rejoin — leave the agent running | await stream.disconnect() or await stream.stop({ cancel: false }) |
submit(..., { onDisconnect: "cancel" }) |
Call stream.stop() when the user cancels. |
submit(..., { onDisconnect: "continue", streamResumable: true }) |
Call stream.disconnect() when navigating away; reattach by remounting with the same threadId. |
// Before — disconnect policy lived on submit.
await submit(input, { onDisconnect: "continue", streamResumable: true });
// After — explicit stop vs. disconnect.
await submit(input);
await stream.stop(); // chat cancel (server + client)
await stream.disconnect(); // join/rejoin (client only)
optimisticValues)Legacy optimisticValues let you hand-merge state into visible values before a run streamed back — most commonly to show the user's own message instantly. v1 makes this automatic: the input you pass to submit() is reflected in values / messages immediately and then reconciled against authoritative server state as it streams in.
// Legacy: manually echo the user's message via a callback.
await submit(
{ messages: [new HumanMessage("hi")] },
{
optimisticValues: (prev) => ({
messages: [...prev.messages, new HumanMessage("hi")],
}),
},
);
// v1: just submit. The human message appears at once.
await submit({ messages: [new HumanMessage("hi")] });
How it works:
id gets a stable client id that is sent to the server, so the server echo reconciles by id instead of duplicating.values and converge to server truth on the first values event, or roll back if the run fails before any echo.useMessageMetadata(stream, message.id).optimisticStatus ("pending" → "sent", or "failed" if the run errors before the message is echoed).function Bubble({ stream, message }: { stream: AnyStream; message: BaseMessage }) {
const { optimisticStatus } = useMessageMetadata(stream, message.id) ?? {};
return (
<div data-pending={optimisticStatus === "pending"}>
{message.text}
{optimisticStatus === "failed" && <RetryButton />}
</div>
);
}
Opt out per hook with optimistic: false when you want server-authoritative-only behavior, such as deterministic SSR/tests or non-chat state graphs:
const stream = useStream({ assistantId: "agent", optimistic: false });
Legacy useStream returned everything in one object. v1 keeps the always-on data on the root return and pushes the rest into companion selector hooks that ref-count their server subscriptions.
| Hook | Replaces |
|---|---|
useValues(stream) |
stream.values |
useMessages(stream) |
stream.messages |
useToolCalls(stream) |
stream.toolCalls |
useMessageMetadata(stream, msgId) |
stream.getMessagesMetadata(msg, i) |
useSubmissionQueue(stream) |
stream.queue |
useExtension(stream, name) |
Per-event callbacks |
useChannel(stream, channels) |
Raw event callbacks |
useChannelEffect(stream, channels, { onEvent }) |
onLangChainEvent, onCustomEvent |
useAudio / useImages / useVideo / useFiles |
— |
// Before: everything on the root
const { messages, toolCalls, getMessagesMetadata, queue } = useStream({ assistantId });
// After: always-on stays on the root; rest moves to selectors
const stream = useStream({ assistantId });
const messages = useMessages(stream); // or just stream.messages
const metadata = useMessageMetadata(stream, messages.at(-1)?.id);
const { entries, size, cancel, clear } = useSubmissionQueue(stream);
branch flow)Use useMessageMetadata to read the checkpoint associated with a message, then pass that checkpoint id as forkFrom on the next submit:
function EditButton({ stream, message }: { stream: UseStreamReturn; message: BaseMessage }) {
const metadata = useMessageMetadata(stream, message.id);
return (
<button
disabled={!metadata?.parentCheckpointId}
onClick={() => {
const forkFrom = metadata?.parentCheckpointId;
if (!forkFrom) return;
void stream.submit({ messages: [new HumanMessage("...revised prompt...")] }, { forkFrom });
}}
>
Edit from here
</button>
);
}
queue flow)multitaskStrategy: "enqueue" records pending submissions client-side. Use useSubmissionQueue(stream) to render and manage that queue:
function Composer({ stream }: { stream: UseStreamReturn }) {
const { entries, cancel, clear } = useSubmissionQueue(stream);
return (
<>
<button
onClick={() =>
stream.submit({ messages: [new HumanMessage("go")] }, { multitaskStrategy: "enqueue" })
}
>
Queue turn
</button>
<ol>
{entries.map((entry) => (
<li key={entry.id}>
pending... <button onClick={() => cancel(entry.id)}>cancel</button>
</li>
))}
</ol>
{entries.length > 0 && <button onClick={clear}>Clear queue</button>}
</>
);
}
Subagents and subgraphs are now discovered eagerly but streamed lazily. The discovery maps (stream.subagents, stream.subgraphs, stream.subgraphsByNode) are kept in sync with zero extra wire cost; each snapshot exposes identity fields only:
interface SubagentDiscoverySnapshot {
readonly id: string; // tool-call id that spawned it
readonly name: string; // "researcher", "writer", ...
readonly namespace: readonly string[];
readonly parentId: string | null;
readonly depth: number;
readonly status: "pending" | "running" | "complete" | "error";
// No messages / toolCalls / values. Use selector hooks below.
}
Replace every subagent.messages / subagent.toolCalls / subagent.values read with the matching selector, passing the discovery snapshot as target:
// Before
{
[...stream.subagents.values()].map((s) => (
<SubagentCard key={s.id} messages={s.messages} toolCalls={s.toolCalls} />
));
}
// After
{
[...stream.subagents.values()].map((s) => (
<SubagentCard key={s.id} stream={stream} subagent={s} />
));
}
function SubagentCard({ stream, subagent }) {
const messages = useMessages(stream, subagent);
const toolCalls = useToolCalls(stream, subagent);
const values = useValues<ResearcherState>(stream, subagent);
}
The first time any component mounts useMessages(stream, subagent), a messages-channel subscription is opened and scoped to subagent.namespace. When the last consumer unmounts, the subscription is released automatically. Views that do not render a subagent's messages never pay for them.
activeSubagents, getSubagent(id), getSubagentsByType(name), and getSubagentsByMessage(msg) are gone. Derive the equivalents inline:
const active = [...stream.subagents.values()].filter((subagent) => subagent.status === "running");
const researcher = [...stream.subagents.values()].find(
(subagent) => subagent.name === "researcher",
);
const byType = new Map<string, SubagentDiscoverySnapshot[]>();
for (const subagent of stream.subagents.values()) {
const bucket = byType.get(subagent.name) ?? [];
bucket.push(subagent);
byType.set(subagent.name, bucket);
}
tools + onTool)The legacy tools / onTool options are preserved one-for-one. No migration is needed if you were already using this API. The helper exports (flushPendingHeadlessToolInterrupts, findHeadlessTool, handleHeadlessToolInterrupt, …) are still available from @langchain/react.
UseStreamTransport → AgentServerAdapterThe legacy UseStreamTransport interface and FetchStreamTransport class are replaced by AgentServerAdapter, a richer interface that owns the entire transport — both commands and the event stream — and matches the new event-based streaming protocol's request shape. The convenience HttpAgentServerAdapter covers the common case (SSE + WS with injectable fetch / webSocketFactory / defaultHeaders).
HttpAgentServerAdapter// Before
import { FetchStreamTransport, useStream } from "@langchain/react";
const transport = new FetchStreamTransport({ apiUrl: "/api/chat" });
const stream = useStream({ transport });
// After
import { HttpAgentServerAdapter, useStream } from "@langchain/react";
const transport = new HttpAgentServerAdapter({
apiUrl: "/api/chat",
threadId: "thread-123", // required: the adapter is bound to a thread
defaultHeaders: { Authorization: `Bearer ${token}` },
// Optional: fetch override or webSocketFactory for WebSocket transports
fetch: myAuthedFetch,
});
const stream = useStream({ transport });
If you hand-rolled a UseStreamTransport, migrate to AgentServerAdapter:
interface AgentServerAdapter {
readonly threadId: string;
open(): Promise<void>;
send(command: Command): Promise<CommandResponse | ErrorResponse | void>;
events(): AsyncIterable<Message>;
openEventStream?(params: SubscribeParams): EventStreamHandle;
close(): Promise<void>;
// Optional — implement if your server supports them:
getState?(): Promise<{
values: unknown;
checkpoint?: { checkpoint_id?: string } | null;
} | null>;
getHistory?(options?: { limit?: number }): Promise<
Array<{
values: unknown;
checkpoint?: { checkpoint_id?: string } | null;
}>
>;
}
The adapter is used exactly as-is. The Agent Server Client is not constructed when a custom adapter is supplied, so bundles that only use a custom adapter tree-shake the built-in SSE/WebSocket transport stack.
Passing both assistantId + apiUrl and a transport: AgentServerAdapter is now a compile-time error. See Transports for the full interface.
useStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
transport: myAdapter, // `apiUrl` is `never` on the custom-adapter branch
});
Pick one branch per useStream instance.
StreamProvider / useStreamContextThe provider and consumer are unchanged at the call site. Because the underlying useStream changed, the context value changes accordingly — update any destructuring per §4. StreamProviderProps<T> (Agent Server branch) and StreamProviderCustomProps<T> (custom-adapter branch) mirror the two arms of the options union.
useSuspenseStreamuseSuspenseStream is now a slim port aligned with the v1 package API, built on top of useStream and the controller's hydrationPromise. The legacy implementation prefetched threads.getHistory(threadId) into an external SuspenseCache; v1 drops history entirely and uses the controller's hydration lifecycle directly.
| Legacy surface | v1 replacement |
|---|---|
SuspenseCache, createSuspenseCache, invalidateSuspenseCache |
Gone. The hook uses a module-level cache keyed on (apiUrl, assistantId, threadId). |
suspenseCache option |
Gone. No caller-side setup required. |
fetchStateHistory: { limit } prefetch |
Gone. The hook hydrates via threads.getState() and suspends until that settles. |
branch / setBranch / history / getMessagesMetadata on the return |
Gone, same as plain useStream. Use the companion hooks (§6). |
UseSuspenseStreamReturn<T> is UseStreamReturn<T> with:
isLoading, isThreadLoading, and hydrationPromise removed because Suspense handles those phases.isStreaming: boolean added so you can render a typing indicator distinct from the suspended initial-load state.error.All other fields (values, messages, toolCalls, interrupts, subagents, subgraphs, submit, stop, respond, …) are identical to useStream's return.
import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { useSuspenseStream } from "@langchain/react";
function App() {
return (
<ErrorBoundary fallback={<ErrorDisplay />}>
<Suspense fallback={<Spinner />}>
<Chat />
</Suspense>
</ErrorBoundary>
);
}
function Chat() {
const { messages, isStreaming } = useSuspenseStream({
assistantId: "agent",
apiUrl: "http://localhost:2024",
threadId,
});
return <MessageList messages={messages} streaming={isStreaming} />;
}
Thread switching works naturally: changing the threadId prop re-suspends the component while the new thread hydrates.
| Helper | Use |
|---|---|
UseStreamReturn<T> (alias: UseStreamResult<T>) |
The fully-resolved return type of useStream<T>. Prop-drill as { stream: UseStreamReturn<typeof agent> }. |
AnyStream |
Type-erased handle (UseStreamReturn<any, any, any>). |
InferStateType<T> |
Unwraps a compiled graph / agent brand / agent tool array into its state shape. |
InferToolCalls<T> |
Derives a discriminated union of tool-call shapes. |
InferSubagentStates<T> |
{ name: State, … } map derived from a DeepAgent brand. |
WidenUpdateMessages<T> |
Widens messages in a partial state update. |
StreamSubmitOptions<State, Configurable> |
Options shape accepted by submit(). |
AgentServerAdapter, HttpAgentServerAdapter, HttpAgentServerAdapterOptions |
Custom-transport interface + convenience class. |
UseStreamOptions, AgentServerOptions, CustomAdapterOptions |
Discriminated options union; rarely needed at call sites. |
Breaking change: the legacy type aliases re-exported from @langchain/react are no longer available from this package: UseStream, UseSuspenseStream, UseStreamCustom, UseStreamCustomOptions, UseStreamTransport, UseStreamThread, GetToolCallsType, QueueEntry, QueueInterface, SubagentStream, FetchStreamTransport, and others.
| Legacy name | v1 replacement |
|---|---|
UseStream<State, Bag> |
UseStreamReturn<State> (or UseStreamReturn<typeof agent>). |
UseStreamOptions<State, Bag> |
UseStreamOptions<State> (or just let the hook infer). |
UseStreamTransport |
AgentServerAdapter (§9). |
FetchStreamTransport |
HttpAgentServerAdapter (§9). |
GetToolCallsType<State> |
InferToolCalls<typeof agent>. |
UseSuspenseStream<…> |
UseSuspenseStreamReturn<T> (§11). |
QueueEntry, QueueInterface |
SubmissionQueueEntry, UseSubmissionQueueReturn (§6). |
SubagentStream, SubagentStreamInterface |
SubagentDiscoverySnapshot + useMessages(stream, subagent) (§7). |
Apps that cannot migrate all call sites in one pass can keep using the legacy types by importing them directly from @langchain/langgraph-sdk/ui during the transition.
MessageMetadata collisionBreaking change: v1 exports a new MessageMetadata from @langchain/langgraph-sdk/stream with shape { parentCheckpointId }, different from the legacy one ({ messageId, firstSeenState, branch, branchOptions }). Legacy call sites must import the legacy type from @langchain/langgraph-sdk/ui.
Some v1 features are accepted but not yet executed end-to-end on all servers:
| Feature | Status today |
|---|---|
submit(input, { forkFrom }) |
Type-accepted; forwarded on run.start. |
multitaskStrategy: "enqueue" |
Fully honoured client-side; drains queued entries sequentially. |
multitaskStrategy: "reject" |
Fully honoured client-side — submit() throws when a run is already in flight. |
multitaskStrategy: "rollback" (default) |
Fully honoured client-side — in-flight run aborted, new submission dispatched. |
multitaskStrategy: "interrupt" |
Type-accepted. Falls back to "rollback" until server-side semantics land. |
useMessageMetadata().parentCheckpointId |
Populated from the parent_checkpoint field on values events. |
We still need a raw event stream for analytics. What replaces onLangChainEvent / onDebugEvent / onCustomEvent?
For per-event side effects (analytics, logging), the direct replacement is useChannelEffect, which calls back once per event without re-rendering:
useChannelEffect(stream, ["lifecycle", "tools", "custom"], {
replay: false,
onEvent(event) {
sendAnalytics(event);
},
onError(error) {
logger.error(error);
},
});
If you instead want to render an event log, use useChannel(stream, channels) for a bounded buffer of raw events scoped to a namespace, or subscribe to a specific extension with useExtension(stream, name). For app-wide telemetry that should run regardless of which component is mounted, pipe the raw stream through a custom AgentServerAdapter (§9) and tee events to your analytics sink there.
The callback receives v1 protocol events (
lifecycle.*,tools.*,messages.*,custom), not the legacy LangChain event names (on_chain_start,on_tool_end, …). Map the protocol events to your analytics schema, or normalize on the backend if you need the old names.
My backend only emits values events (no messages channel). Will streaming still work?
Yes — stream.messages merges messages-channel deltas and values.messages snapshots. Backends that only emit values render full turns at once instead of token-by-token.
We pinned @langchain/langgraph-sdk in app code. Do we need to bump it?
Yes. @langchain/react v1 depends on the new event-based streaming runtime in @langchain/langgraph-sdk.
How do I migrate a useStream call that was deeply generic (useStream<State, Bag>)?
v1 takes three generics: useStream<T, InterruptType, ConfigurableType> where T is either a plain state shape or an agent brand. The legacy Bag options are gone; if you passed InterruptType via Bag, lift it to the second generic slot.
// Before
useStream<MyState, { InterruptType: MyInterrupt }>({ ... });
// After
useStream<MyState, MyInterrupt>({ ... });
Where did the legacy useStream / FetchStreamTransport surface go?
The pre-v1 hook implementations and old useSuspenseStream implementation have been removed from @langchain/react v1:
import { FetchStreamTransport } from "@langchain/react" no longer resolves — use HttpAgentServerAdapter from the same package (§9).UseStream, UseStreamOptions, UseStreamTransport, QueueEntry, SubagentStream, …) are no longer re-exported; use the v1 names from §12.useSuspenseStream is still exported, but it is the v1 port built on useStream (§11), not the pre-v1 implementation.Apps that still need legacy types mid-migration can import them directly from @langchain/langgraph-sdk/ui. No pre-v1 runtime code remains in the @langchain/react bundle.
Does multitaskStrategy: "enqueue" work today?
Yes, end-to-end on the client. Submissions issued with { multitaskStrategy: "enqueue" } while another run is in flight are recorded in the controller's queue store, exposed via useSubmissionQueue(stream) with cancel(id) / clear() affordances, and drained sequentially once the active run settles. Switching threads clears the queue.