LangChain Reference home pageLangChain ReferenceLangChain Reference
  • GitHub
  • Main Docs
Deep Agents
LangChain
LangGraph
Integrations
LangSmith
  • Overview
  • Getting started
  • useStream
  • Selectors
  • Interrupts & headless tools
  • Subagents & subgraphs
  • Fork & edit from a checkpoint
  • Submission queue
  • Multimodal media
  • Transports
  • Suspense
  • StreamProvider & context
  • Type safety
  • Migrating to v1
LangGraph SDK
  • Client
  • Auth
  • React
  • Logging
  • React Ui
  • Server
LangGraph Checkpoint
LangGraph Checkpoint MongoDB
LangGraph Checkpoint Postgres
LangGraph Checkpoint Redis
  • Shallow
  • Store
LangGraph Checkpoint SQLite
LangGraph Checkpoint Validation
  • Cli
LangGraph API
LangGraph CLI
LangGraph CUA
  • Utils
LangGraph Supervisor
LangGraph Swarm
⌘I

LangChain Assistant

Ask a question to get started

Enter to send•Shift+Enter new line

Menu

OverviewGetting starteduseStreamSelectorsInterrupts & headless toolsSubagents & subgraphsFork & edit from a checkpointSubmission queueMultimodal mediaTransportsSuspenseStreamProvider & contextType safetyMigrating to v1
LangGraph SDK
ClientAuthReactLoggingReact UiServer
LangGraph Checkpoint
LangGraph Checkpoint MongoDB
LangGraph Checkpoint Postgres
LangGraph Checkpoint Redis
ShallowStore
LangGraph Checkpoint SQLite
LangGraph Checkpoint Validation
Cli
LangGraph API
LangGraph CLI
LangGraph CUA
Utils
LangGraph Supervisor
LangGraph Swarm
Language
Theme
JavaScript@langchain/reactMigrating to v1

Migrating to v1

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.

1. Why the breaking change?

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:

  • Selector-based subscriptions. Namespaced data (subagent messages, subgraph tool calls, media) is opened only when a component actually mounts a selector hook, and released on unmount.
  • Always-on root projections. values / messages / toolCalls / interrupts are always available at the root with zero extra wire cost.
  • First-class re-attach. Remounting a hook on an in-flight thread attaches to the live subscription instead of replaying from scratch.
  • Discriminated option bag. The Agent Server path and the custom-adapter path are now two arms of a discriminated union.
  • Type inference from agent brands. typeof agent flows through to values, toolCalls[].args, and subagent-state maps.

2. TL;DR migration checklist

  • Upgrade @langchain/react to ^1.0.0 and @langchain/langgraph-sdk to the matching new event-based streaming runtime.
  • Imports stay the same — import { useStream } from "@langchain/react" now resolves to the hook built for new event-based streaming. useStreamExperimental is not exported from this package.
  • Remove these option-bag fields (gone; see §3): onError, onFinish, onUpdateEvent, onCustomEvent, onMetadataEvent, onLangChainEvent, onDebugEvent, onCheckpointEvent, onTaskEvent, onToolEvent, onStop, fetchStateHistory, reconnectOnMount, throttle, thread, filterSubagentMessages, subagentToolNames.
  • Replace transport: new FetchStreamTransport(...) with transport: new HttpAgentServerAdapter(...) (see §9).
  • Remove these return-shape fields (moved or dropped; see §4): branch, setBranch, history, experimental_branchTree, getMessagesMetadata, toolProgress, joinStream, switchThread, queue, activeSubagents, getSubagent, getSubagentsByType, getSubagentsByMessage.
  • Replace getMessagesMetadata(msg)?.firstSeenState?.parent_checkpoint with useMessageMetadata(stream, msg.id)?.parentCheckpointId (§6).
  • Replace stream.queue with useSubmissionQueue(stream) (§6).
  • Replace stream.switchThread(id) with passing a new threadId prop and letting the hook reload on change (§4).
  • Inside subagent-aware UIs, read per-subagent data with selector hooks (useMessages(stream, subagent) etc.) (§7).
  • Suspense apps: useSuspenseStream now returns the v1 shape (matching useStream minus isLoading / isThreadLoading / hydrationPromise, plus isStreaming). Remove any suspenseCache, createSuspenseCache, or fetchStateHistory props (§11).
  • Replace submit(..., { onDisconnect, streamResumable }) with stream.stop() (cancel) or stream.disconnect() (join/rejoin) (§5.3).
  • Re-run tsc. The option bag and return type are now discriminated and strongly typed.

3. Option-bag migration

3.1 Still supported — same meaning

assistantId, client, apiUrl, apiKey, callerOptions, defaultHeaders, threadId, onThreadId, initialValues, messagesKey, onCreated, tools, onTool all keep working.

3.2 New options

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.

3.3 Removed — with replacements

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 }),
});

4. Return-shape migration

4.1 Still there — same meaning

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).

4.2 Still there — different meaning

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.

4.3 Removed — with replacements

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.

4.4 New fields

subgraphs (ReadonlyMap<string, SubgraphDiscoverySnapshot>) and subgraphsByNode (ReadonlyMap<string, SubgraphDiscoverySnapshot[]>).

4.5 Worked example — minimal diff

// 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) ?? {};

5. submit() signature changes

5.1 Input widening

submit() 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.

5.2 Option changes

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" },
);

5.3 Stop / disconnect

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)

5.4 Optimistic updates (replaces 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:

  • Messages in the input are appended right away. Any message without an id gets a stable client id that is sent to the server, so the server echo reconciles by id instead of duplicating.
  • Other input keys are shallow-merged into values and converge to server truth on the first values event, or roll back if the run fails before any echo.
  • Per-message status is exposed via 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 });

6. Companion selector hooks — the new mental model

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);

6.1 Fork from message (the old 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>
  );
}

6.2 Enqueue-and-cancel (the old 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>}
    </>
  );
}

7. Subagents & subgraphs

7.1 Discovery

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.
}

7.2 Per-subagent content

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.

7.3 Removed helpers

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);
}

8. Headless tools (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.

9. Custom transports: UseStreamTransport → AgentServerAdapter

The 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).

9.1 Most apps: drop in 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 });

9.2 Custom implementations

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.

9.3 Discriminated options

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.

10. StreamProvider / useStreamContext

The 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.

11. useSuspenseStream

useSuspenseStream 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.

11.1 What was dropped

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).

11.2 Return shape

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.
  • Non-streaming errors thrown to the nearest Error Boundary instead of surfacing via error.

All other fields (values, messages, toolCalls, interrupts, subagents, subgraphs, submit, stop, respond, …) are identical to useStream's return.

11.3 Minimal usage

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.

12. Type helpers

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.

12.1 Legacy type aliases (removed)

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.

12.2 MessageMetadata collision

Breaking 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.

13. Known gaps & server-side prerequisites

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.

14. FAQ

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).
  • Legacy type aliases (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.