> ## Documentation Index
> Fetch the complete documentation index at: https://langchain-5e9cc07a-preview-opensw-1772748614-696205f.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Frontend

> Build generative UIs with real-time streaming from LangChain agents, LangGraph graphs, and custom APIs

The `useStream` React hook provides seamless integration with LangGraph streaming capabilities. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great generative UI experiences.

Key features:

* <Icon icon="messages" size={16} /> **Messages streaming** — Handle a stream of message chunks to form a complete message
* <Icon icon="refresh" size={16} /> **Automatic state management** — for messages, interrupts, loading states, and errors
* <Icon icon="git-branch" size={16} /> **Conversation branching** — Create alternate conversation paths from any point in the chat history
* <Icon icon="palette" size={16} /> **UI-agnostic design** — Bring your own components and styling

## Installation

Install the LangGraph SDK to use the `useStream` hook in your React application:

```bash theme={null}
npm install @langchain/langgraph-sdk
```

## Basic usage

The `useStream` hook connects to any LangGraph graph, whether that's running on from your own endpoint, or deployed using [LangSmith deployments](/langsmith/deployments).

```tsx theme={null}
import { useStream } from "@langchain/langgraph-sdk/react";

function Chat() {
  const stream = useStream({
    assistantId: "agent",
    // Local development
    apiUrl: "http://localhost:2024",
    // Production deployment (LangSmith hosted)
    // apiUrl: "https://your-deployment.us.langgraph.app"
  });

  const handleSubmit = (message: string) => {
    stream.submit({
      messages: [
        { content: message, type: "human" }
      ],
    });
  };

  return (
    <div>
      {stream.messages.map((message, idx) => (
        <div key={message.id ?? idx}>
          {message.type}: {message.content}
        </div>
      ))}

      {stream.isLoading && <div>Loading...</div>}
      {stream.error && <div>Error: {stream.error.message}</div>}
    </div>
  );
}
```

<Tip>
  Learn how to [deploy your agents to LangSmith](/oss/javascript/langchain/deploy) for production-ready hosting with built-in observability, authentication, and scaling.
</Tip>

<Accordion title="`useStream` parameters">
  <ParamField body="assistantId" type="string" required>
    The ID of the agent to connect to. When using LangSmith deployments, this must match the agent ID shown in your deployment dashboard. For custom API deployments or local development, this can be any string that your server uses to identify the agent.
  </ParamField>

  <ParamField body="apiUrl" type="string">
    The URL of the Agent Server. Defaults to `http://localhost:2024` for local development.
  </ParamField>

  <ParamField body="apiKey" type="string">
    API key for authentication. Required when connecting to deployed agents on LangSmith.
  </ParamField>

  <ParamField body="threadId" type="string">
    Connect to an existing thread instead of creating a new one. Useful for resuming conversations.
  </ParamField>

  <ParamField body="onThreadId" type="(id: string) => void">
    Callback invoked when a new thread is created. Use this to persist the thread ID for later use.
  </ParamField>

  <ParamField body="reconnectOnMount" type="boolean | (() => Storage)">
    Automatically resume an ongoing run when the component mounts. Set to `true` to use session storage, or provide a custom storage function.
  </ParamField>

  <ParamField body="onCreated" type="(run: Run) => void">
    Callback invoked when a new run is created. Useful for persisting run metadata for resumption.
  </ParamField>

  <ParamField body="onError" type="(error: Error) => void">
    Callback invoked when an error occurs during streaming.
  </ParamField>

  <ParamField body="onFinish" type="(state: StateType, run?: Run) => void">
    Callback invoked when the stream completes successfully with the final state.
  </ParamField>

  <ParamField body="onCustomEvent" type="(data: unknown, context: { mutate }) => void">
    Handle custom events emitted from your agent using the `writer`. See [Custom streaming events](#custom-streaming-events).
  </ParamField>

  <ParamField body="onUpdateEvent" type="(data: unknown, context: { mutate }) => void">
    Handle state update events after each graph step.
  </ParamField>

  <ParamField body="onMetadataEvent" type="(metadata: { run_id, thread_id }) => void">
    Handle metadata events with run and thread information.
  </ParamField>

  <ParamField body="messagesKey" type="string" default="messages">
    The key in the graph state that contains the messages array.
  </ParamField>

  <ParamField body="throttle" type="boolean" default="true">
    Batch state updates for better rendering performance. Disable for immediate updates.
  </ParamField>

  <ParamField body="initialValues" type="StateType | null">
    Initial state values to display while the first stream is loading. Useful for showing cached thread data immediately.
  </ParamField>
</Accordion>

<Accordion title="`useStream` return values">
  <ParamField body="messages" type="Message[]">
    All messages in the current thread, including both human and AI messages.
  </ParamField>

  <ParamField body="values" type="StateType">
    The current graph state values. Type is inferred from the agent or graph type parameter.
  </ParamField>

  <ParamField body="isLoading" type="boolean">
    Whether a stream is currently in progress. Use this to show loading indicators.
  </ParamField>

  <ParamField body="error" type="Error | null">
    Any error that occurred during streaming. `null` when no error.
  </ParamField>

  <ParamField body="interrupt" type="Interrupt | undefined">
    Current interrupt requiring user input, such as human-in-the-loop approval requests.
  </ParamField>

  <ParamField body="toolCalls" type="ToolCallWithResult[]">
    All tool calls across all messages, with their results and state (`pending`, `completed`, or `error`).
  </ParamField>

  <ParamField body="submit" type="(input, options?) => Promise<void>">
    Submit new input to the agent. Pass `null` as input when resuming from an interrupt with a command. Options include `checkpoint` for branching, `optimisticValues` for optimistic updates, and `threadId` for optimistic thread creation.
  </ParamField>

  <ParamField body="stop" type="() => void">
    Stop the current stream immediately.
  </ParamField>

  <ParamField body="joinStream" type="(runId: string) => void">
    Resume an existing stream by run ID. Use with `onCreated` for manual stream resumption.
  </ParamField>

  <ParamField body="setBranch" type="(branch: string) => void">
    Switch to a different branch in the conversation history.
  </ParamField>

  <ParamField body="getToolCalls" type="(message) => ToolCall[]">
    Get all tool calls for a specific AI message.
  </ParamField>

  <ParamField body="getMessagesMetadata" type="(message) => MessageMetadata">
    Get metadata for a message, including streaming info like `langgraph_node` for identifying the source node, and `firstSeenState` for branching.
  </ParamField>

  <ParamField body="experimental_branchTree" type="BranchTree">
    Tree representation of the thread for advanced branching controls in non-message based graphs.
  </ParamField>
</Accordion>

## Thread management

Keep track of conversations with built-in thread management. You can access the current thread ID and get notified when new threads are created:

```tsx theme={null}
import { useState } from "react";
import { useStream } from "@langchain/langgraph-sdk/react";

function Chat() {
  const [threadId, setThreadId] = useState<string | null>(null);

  const stream = useStream({
    apiUrl: "http://localhost:2024",
    assistantId: "agent",
    threadId: threadId,
    onThreadId: setThreadId,
  });

  // threadId is updated when a new thread is created
  // Store it in URL params or localStorage for persistence
}
```

We recommend storing the `threadId` to let users resume conversations after page refreshes.

### Resume after page refresh

The `useStream` hook can automatically resume an ongoing run upon mounting by setting `reconnectOnMount: true`. This is useful for continuing a stream after a page refresh, ensuring no messages and events generated during the downtime are lost.

```tsx theme={null}
const stream = useStream({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",
  reconnectOnMount: true,
});
```

By default the ID of the created run is stored in `window.sessionStorage`, which can be swapped by passing a custom storage function:

```tsx theme={null}
const stream = useStream({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",
  reconnectOnMount: () => window.localStorage,
});
```

For manual control over the resumption process, use the run callbacks to persist metadata and `joinStream` to resume:

```tsx theme={null}
import { useStream } from "@langchain/langgraph-sdk/react";
import { useEffect, useRef } from "react";

function Chat({ threadId }: { threadId: string | null }) {
  const stream = useStream({
    apiUrl: "http://localhost:2024",
    assistantId: "agent",
    threadId,
    onCreated: (run) => {
      // Persist run ID when stream starts
      window.sessionStorage.setItem(`resume:${run.thread_id}`, run.run_id);
    },
    onFinish: (_, run) => {
      // Clean up when stream completes
      window.sessionStorage.removeItem(`resume:${run?.thread_id}`);
    },
  });

  // Resume stream on mount if there's a stored run ID
  const joinedThreadId = useRef<string | null>(null);
  useEffect(() => {
    if (!threadId) return;
    const runId = window.sessionStorage.getItem(`resume:${threadId}`);
    if (runId && joinedThreadId.current !== threadId) {
      stream.joinStream(runId);
      joinedThreadId.current = threadId;
    }
  }, [threadId]);

  const handleSubmit = (text: string) => {
    // Use streamResumable to ensure events aren't lost
    stream.submit(
      { messages: [{ type: "human", content: text }] },
      { streamResumable: true }
    );
  };
}
```

<Card title="Try the session persistence example" icon="rotate" href="https://github.com/langchain-ai/langgraphjs/tree/main/examples/ui-react/src/examples/session-persistence">
  See a complete implementation of stream resumption with `reconnectOnMount` and thread persistence in the `session-persistence` example.
</Card>

## Optimistic updates

You can optimistically update the client state before performing a network request, providing immediate feedback to the user:

```tsx theme={null}
const stream = useStream({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",
});

const handleSubmit = (text: string) => {
  const newMessage = { type: "human" as const, content: text };

  stream.submit(
    { messages: [newMessage] },
    {
      optimisticValues(prev) {
        const prevMessages = prev.messages ?? [];
        return { ...prev, messages: [...prevMessages, newMessage] };
      },
    }
  );
};
```

### Optimistic thread creation

Use the `threadId` option in `submit` to enable optimistic UI patterns where you need to know the thread ID before the thread is created:

```tsx theme={null}
import { useState } from "react";
import { useStream } from "@langchain/langgraph-sdk/react";

function Chat() {
  const [threadId, setThreadId] = useState<string | null>(null);
  const [optimisticThreadId] = useState(() => crypto.randomUUID());

  const stream = useStream({
    apiUrl: "http://localhost:2024",
    assistantId: "agent",
    threadId,
    onThreadId: setThreadId,
  });

  const handleSubmit = (text: string) => {
    // Navigate immediately without waiting for thread creation
    window.history.pushState({}, "", `/threads/${optimisticThreadId}`);

    // Create thread with the predetermined ID
    stream.submit(
      { messages: [{ type: "human", content: text }] },
      { threadId: optimisticThreadId }
    );
  };
}
```

### Cached thread display

Use the `initialValues` option to display cached thread data immediately while the history is being loaded from the server:

```tsx theme={null}
function Chat({ threadId, cachedData }) {
  const stream = useStream({
    apiUrl: "http://localhost:2024",
    assistantId: "agent",
    threadId,
    initialValues: cachedData?.values,
  });

  // Shows cached messages instantly, then updates when server responds
}
```

## Branching

Create alternate conversation paths by editing previous messages or regenerating AI responses. Use `getMessagesMetadata()` to access checkpoint information for branching:

<CodeGroup>
  ```tsx Chat.tsx theme={null}
  import { useStream } from "@langchain/langgraph-sdk/react";
  import { BranchSwitcher } from "./BranchSwitcher";

  function Chat() {
    const stream = useStream({
      apiUrl: "http://localhost:2024",
      assistantId: "agent",
    });

    return (
      <div>
        {stream.messages.map((message) => {
          const meta = stream.getMessagesMetadata(message);
          const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;

          return (
            <div key={message.id}>
              <div>{message.content as string}</div>

              {/* Edit human messages */}
              {message.type === "human" && (
                <button
                  onClick={() => {
                    const newContent = prompt("Edit message:", message.content as string);
                    if (newContent) {
                      stream.submit(
                        { messages: [{ type: "human", content: newContent }] },
                        { checkpoint: parentCheckpoint }
                      );
                    }
                  }}
                >
                  Edit
                </button>
              )}

              {/* Regenerate AI messages */}
              {message.type === "ai" && (
                <button
                  onClick={() => stream.submit(undefined, { checkpoint: parentCheckpoint })}
                >
                  Regenerate
                </button>
              )}

              {/* Switch between branches */}
              <BranchSwitcher
                branch={meta?.branch}
                branchOptions={meta?.branchOptions}
                onSelect={(branch) => stream.setBranch(branch)}
              />
            </div>
          );
        })}
      </div>
    );
  }
  ```

  ```tsx BranchSwitcher.tsx theme={null}
  /**
   * Component for navigating between conversation branches.
   * Shows the current branch position and allows switching between alternatives.
   */
  export function BranchSwitcher({
    branch,
    branchOptions,
    onSelect,
  }: {
    branch: string | undefined;
    branchOptions: string[] | undefined;
    onSelect: (branch: string) => void;
  }) {
    if (!branchOptions || !branch) return null;
    const index = branchOptions.indexOf(branch);

    return (
      <div className="flex items-center gap-2">
        <button
          type="button"
          disabled={index <= 0}
          onClick={() => onSelect(branchOptions[index - 1])}
        >
          ←
        </button>
        <span>{index + 1} / {branchOptions.length}</span>
        <button
          type="button"
          disabled={index >= branchOptions.length - 1}
          onClick={() => onSelect(branchOptions[index + 1])}
        >
          →
        </button>
      </div>
    );
  }
  ```
</CodeGroup>

For advanced use cases, use the `experimental_branchTree` property to get the tree representation of the thread for non-message based graphs.

<Card title="Try the branching example" icon="git-branch" href="https://github.com/langchain-ai/langgraphjs/tree/main/examples/ui-react/src/examples/branching-chat">
  See a complete implementation of conversation branching with edit, regenerate, and branch switching in the `branching-chat` example.
</Card>

## Type-safe streaming

The `useStream` hook supports full type inference when used with agents created via [`createAgent`](https://reference.langchain.com/javascript/langchain/index/createAgent) or graphs created with [`StateGraph`](https://reference.langchain.com/javascript/langchain-langgraph/index/StateGraph). Pass `typeof agent` or `typeof graph` as the type parameter to automatically infer tool call types.

### With `createAgent`

When using [`createAgent`](https://reference.langchain.com/javascript/langchain/index/createAgent), tool call types are automatically inferred from the tools you register to your agent:

<CodeGroup>
  ```typescript agent.ts theme={null}
  import { createAgent, tool } from "langchain";
  import { z } from "zod";

  const getWeather = tool(
    async ({ location }) => `Weather in ${location}: Sunny, 72°F`,
    {
      name: "get_weather",
      description: "Get weather for a location",
      schema: z.object({
        location: z.string().describe("The city to get weather for"),
      }),
    }
  );

  export const agent = createAgent({
    model: "openai:gpt-4.1-mini",
    tools: [getWeather],
  });
  ```

  ```tsx Chat.tsx theme={null}
  import { useStream } from "@langchain/langgraph-sdk/react";
  import type { agent } from "./agent";

  function Chat() {
    // Tool calls are automatically typed from the agent's tools
    const stream = useStream<typeof agent>({
      assistantId: "agent",
      apiUrl: "http://localhost:2024",
    });

    // stream.toolCalls[0].call.name is typed as "get_weather"
    // stream.toolCalls[0].call.args is typed as { location: string }
  }
  ```
</CodeGroup>

### With `StateGraph`

For custom [`StateGraph`](https://reference.langchain.com/javascript/langchain-langgraph/index/StateGraph) applications, the state types are inferred from the graph's annotation:

<CodeGroup>
  ```typescript graph.ts theme={null}
  import { StateGraph, MessagesAnnotation, START, END } from "@langchain/langgraph";
  import { ChatOpenAI } from "@langchain/openai";

  const model = new ChatOpenAI({ model: "gpt-4.1-mini" });

  const workflow = new StateGraph(MessagesAnnotation)
    .addNode("agent", async (state) => {
      const response = await model.invoke(state.messages);
      return { messages: [response] };
    })
    .addEdge(START, "agent")
    .addEdge("agent", END);

  export const graph = workflow.compile();
  ```

  ```tsx Chat.tsx theme={null}
  import { useStream } from "@langchain/langgraph-sdk/react";
  import type { graph } from "./graph";

  function Chat() {
    // State types are automatically inferred from the graph
    const stream = useStream<typeof graph>({
      assistantId: "my-graph",
      apiUrl: "http://localhost:2024",
    });

    // stream.values is typed based on the graph's state annotation
  }
  ```
</CodeGroup>

### With Annotation types

If you're using LangGraph.js, you can reuse your graph's annotation types. Make sure to only import types to avoid importing the entire LangGraph.js runtime:

```tsx theme={null}
import {
  Annotation,
  MessagesAnnotation,
  type StateType,
  type UpdateType,
} from "@langchain/langgraph/web";

const AgentState = Annotation.Root({
  ...MessagesAnnotation.spec,
  context: Annotation<string>(),
});

const stream = useStream<
  StateType<typeof AgentState.spec>,
  { UpdateType: UpdateType<typeof AgentState.spec> }
>({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",
});
```

### Advanced type configuration

You can specify additional type parameters for interrupts, custom events, and configurable options:

```tsx theme={null}
import type { Message } from "@langchain/langgraph-sdk";

type State = { messages: Message[]; context?: string };

const stream = useStream<
  State,
  {
    UpdateType: { messages: Message[] | Message; context?: string };
    InterruptType: string;
    CustomEventType: { type: "progress" | "debug"; payload: unknown };
    ConfigurableType: { model: string };
  }
>({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",
});

// stream.interrupt is typed as string | undefined
// onCustomEvent receives typed events
```

## Rendering tool calls

Use `getToolCalls` to extract and render tool calls from AI messages. Tool calls include the call details, result (if completed), and state.

<CodeGroup>
  ```tsx Chat.tsx theme={null}
  import { useStream } from "@langchain/langgraph-sdk/react";
  import type { agent } from "./agent";
  import { ToolCallCard } from "./ToolCallCard";
  import { MessageBubble } from "./MessageBubble";

  function Chat() {
    const stream = useStream<typeof agent>({
      assistantId: "agent",
      apiUrl: "http://localhost:2024",
    });

    return (
      <div className="flex flex-col gap-4">
        {stream.messages.map((message, idx) => {
          if (message.type === "ai") {
            const toolCalls = stream.getToolCalls(message);

            if (toolCalls.length > 0) {
              return (
                <div key={message.id ?? idx} className="flex flex-col gap-2">
                  {toolCalls.map((toolCall) => (
                    <ToolCallCard key={toolCall.id} toolCall={toolCall} />
                  ))}
                </div>
              );
            }
          }

          return <MessageBubble key={message.id ?? idx} message={message} />;
        })}
      </div>
    );
  }
  ```

  ```tsx ToolCallCard.tsx theme={null}
  import type {
    ToolCallWithResult,
    ToolCallFromTool,
    ToolCallState,
    InferAgentToolCalls,
  } from "@langchain/langgraph-sdk/react";
  import type { ToolMessage } from "@langchain/langgraph-sdk";
  import type { agent } from "./agent";
  import type { getWeather } from "./tools";
  import { parseToolResult } from "./utils";
  import { WeatherCard } from "./WeatherCard";

  /**
   * Define tool call types for this component.
   * Use InferAgentToolCalls for agents or ToolCallFromTool for individual tools.
   */
  type AgentToolCalls = InferAgentToolCalls<typeof agent>;

  /**
   * Component that renders a tool call with its result.
   * Uses typed ToolCallWithResult for discriminated union narrowing.
   */
  export function ToolCallCard({
    toolCall,
  }: {
    toolCall: ToolCallWithResult<AgentToolCalls>;
  }) {
    const { call, result, state } = toolCall;

    // Type narrowing works when call.name is a literal type
    if (call.name === "get_weather") {
      return <WeatherCard call={call} result={result} state={state} />;
    }

    // Fallback for other tools
    return <GenericToolCallCard call={call} result={result} state={state} />;
  }
  ```

  ```tsx GenericToolCallCard.tsx theme={null}
  import type { ToolCallState } from "@langchain/langgraph-sdk/react";
  import type { ToolMessage } from "@langchain/langgraph-sdk";
  import { parseToolResult } from "./utils";

  /**
   * Generic fallback for unknown or unhandled tools.
   * Uses a simple type that works with any tool call.
   */
  export function GenericToolCallCard({
    call,
    result,
    state,
  }: {
    call: { name: string; args: Record<string, unknown> };
    result?: ToolMessage;
    state: ToolCallState;
  }) {
    const isLoading = state === "pending";
    const parsedResult = parseToolResult(result);

    return (
      <div className="bg-neutral-900 rounded-lg p-4 border border-neutral-800">
        <div className="flex items-center gap-3 mb-3">
          <div className="flex-1">
            <div className="text-sm font-medium text-white font-mono">
              {call.name}
            </div>
            <div className="text-xs text-neutral-500">
              {isLoading ? "Processing..." : "Completed"}
            </div>
          </div>
        </div>
        <pre className="text-xs bg-black rounded p-2 mb-2 overflow-x-auto">
          {JSON.stringify(call.args, null, 2)}
        </pre>
        {result && (
          <div className="text-sm rounded-lg p-3 bg-black text-neutral-300">
            {parsedResult.content}
          </div>
        )}
      </div>
    );
  }
  ```

  ```tsx WeatherCard.tsx theme={null}
  import type { ToolCallFromTool, ToolCallState } from "@langchain/langgraph-sdk/react";
  import type { ToolMessage } from "@langchain/langgraph-sdk";
  import type { getWeather } from "./tools";
  import { parseToolResult } from "./utils";

  // Infer tool call type directly from the tool definition
  type GetWeatherToolCall = ToolCallFromTool<typeof getWeather>;

  /**
   * Weather-specific tool card with rich UI.
   * Uses ToolCallFromTool to infer args type from the tool schema.
   */
  export function WeatherCard({
    call,
    result,
    state,
  }: {
    call: GetWeatherToolCall;
    result?: ToolMessage;
    state: ToolCallState;
  }) {
    const isLoading = state === "pending";
    const parsedResult = parseToolResult(result);

    return (
      <div className="relative overflow-hidden rounded-xl">
        {/* Sky gradient background */}
        <div className="absolute inset-0 bg-gradient-to-br from-sky-600 to-indigo-600" />

        <div className="relative p-4">
          <div className="flex items-center gap-2 text-white/80 text-xs mb-3">
            {/* call.args is typed as { location: string } from the tool schema */}
            <span className="font-medium">{call.args.location}</span>
            {isLoading && <span className="ml-auto">Loading...</span>}
          </div>

          {parsedResult.status === "error" ? (
            <div className="bg-red-500/20 rounded-lg p-3 text-red-200 text-sm">
              {parsedResult.content}
            </div>
          ) : (
            <div className="text-white text-lg font-medium">
              {parsedResult.content || "Fetching weather..."}
            </div>
          )}
        </div>
      </div>
    );
  }
  ```

  ```typescript tools.ts theme={null}
  import { tool } from "@langchain/core/tools";
  import { z } from "zod";

  // Define the weather tool with a Zod schema
  export const getWeather = tool(
    async ({ location }) => {
      // Tool implementation
      return JSON.stringify({ status: "success", content: `Weather in ${location}: Sunny, 72°F` });
    },
    {
      name: "get_weather",
      description: "Get the current weather for a location",
      schema: z.object({
        location: z.string().describe("The city and state, e.g. San Francisco, CA"),
      }),
    }
  );
  ```

  ```typescript utils.ts theme={null}
  import type { ToolMessage } from "@langchain/langgraph-sdk";

  /**
   * Helper to parse tool result safely.
   * Tool results may be JSON strings or plain text.
   */
  export function parseToolResult(result?: ToolMessage): {
    status: string;
    content: string;
  } {
    if (!result) return { status: "pending", content: "" };
    try {
      return JSON.parse(result.content as string);
    } catch {
      return { status: "success", content: result.content as string };
    }
  }
  ```
</CodeGroup>

<Card title="Try the tool calling example" icon="hammer" href="https://github.com/langchain-ai/langgraphjs/tree/main/examples/ui-react/src/examples/tool-calling-agent">
  See a complete implementation of tool call rendering with weather, calculator, and note-taking tools in the `tool-calling-agent` example.
</Card>

## Custom streaming events

Stream custom data from your agent using the `writer` in your tools or nodes. Handle these events in the UI with the `onCustomEvent` callback.

<CodeGroup>
  ```typescript agent.ts theme={null}
  import { tool, type ToolRuntime } from "langchain";
  import { z } from "zod";

  // Define your custom event types
  interface ProgressData {
    type: "progress";
    id: string;
    message: string;
    progress: number;
  }

  const analyzeDataTool = tool(
    async ({ dataSource }, config: ToolRuntime) => {
      const steps = ["Connecting...", "Fetching...", "Processing...", "Done!"];

      for (let i = 0; i < steps.length; i++) {
        // Emit progress events during execution
        config.writer?.({
          type: "progress",
          id: `analysis-${Date.now()}`,
          message: steps[i],
          progress: ((i + 1) / steps.length) * 100,
        } satisfies ProgressData);

        await new Promise((resolve) => setTimeout(resolve, 500));
      }

      return JSON.stringify({ result: "Analysis complete" });
    },
    {
      name: "analyze_data",
      description: "Analyze data with progress updates",
      schema: z.object({
        dataSource: z.string().describe("Data source to analyze"),
      }),
    }
  );
  ```

  ```tsx Chat.tsx theme={null}
  import { useState, useCallback } from "react";
  import { useStream } from "@langchain/langgraph-sdk/react";
  import type { agent } from "./agent";

  interface ProgressData {
    type: "progress";
    id: string;
    message: string;
    progress: number;
  }

  function isProgressData(data: unknown): data is ProgressData {
    return (
      typeof data === "object" &&
      data !== null &&
      "type" in data &&
      (data as ProgressData).type === "progress"
    );
  }

  function CustomStreamingUI() {
    const [progressData, setProgressData] = useState<Map<string, ProgressData>>(
      new Map()
    );

    const handleCustomEvent = useCallback((data: unknown) => {
      if (isProgressData(data)) {
        setProgressData((prev) => {
          const updated = new Map(prev);
          updated.set(data.id, data);
          return updated;
        });
      }
    }, []);

    const stream = useStream<typeof agent>({
      assistantId: "custom-streaming",
      apiUrl: "http://localhost:2024",
      onCustomEvent: handleCustomEvent,
    });

    return (
      <div>
        {/* Render progress cards */}
        {Array.from(progressData.values()).map((data) => (
          <div key={data.id} className="bg-neutral-800 rounded-lg p-4 mb-4">
            <div className="flex justify-between mb-2">
              <span className="text-sm text-white">{data.message}</span>
              <span className="text-xs text-neutral-400">{data.progress}%</span>
            </div>
            <div className="w-full bg-neutral-700 rounded-full h-2">
              <div
                className="bg-blue-500 h-2 rounded-full transition-all"
                style={{ width: `${data.progress}%` }}
              />
            </div>
          </div>
        ))}
      </div>
    );
  }
  ```
</CodeGroup>

<Card title="Try the custom streaming example" icon="bolt" href="https://github.com/langchain-ai/langgraphjs/tree/main/examples/ui-react/src/examples/custom-streaming">
  See a complete implementation of custom events with progress bars, status badges, and file operation cards in the `custom-streaming` example.
</Card>

## Event handling

The `useStream` hook provides callback options that give you access to different types of streaming events. You don't need to explicitly configure stream modes—just pass callbacks for the event types you want to handle:

```tsx theme={null}
const stream = useStream({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",

  // Handle state updates after each graph step
  onUpdateEvent: (update, options) => {
    console.log("Graph update:", update);
  },

  // Handle custom events streamed from your graph
  onCustomEvent: (event, options) => {
    console.log("Custom event:", event);
  },

  // Handle metadata events with run/thread info
  onMetadataEvent: (metadata) => {
    console.log("Run ID:", metadata.run_id);
    console.log("Thread ID:", metadata.thread_id);
  },

  onError: (error) => {
    console.error("Stream error:", error);
  },

  onFinish: (state, options) => {
    console.log("Stream finished with final state:", state);
  },
});
```

### Available callbacks

| Callback          | Description                                                  | Stream mode |
| ----------------- | ------------------------------------------------------------ | ----------- |
| `onUpdateEvent`   | Called when a state update is received after each graph step | `updates`   |
| `onCustomEvent`   | Called when a custom event is received from your graph       | `custom`    |
| `onMetadataEvent` | Called with run and thread metadata                          | `metadata`  |
| `onError`         | Called when an error occurs                                  | -           |
| `onFinish`        | Called when the stream completes                             | -           |

## Multi-agent streaming

When working with multi-agent systems or graphs with multiple nodes, use message metadata to identify which node generated each message. This is particularly useful when multiple LLMs run in parallel and you want to display their outputs with distinct visual styling.

<CodeGroup>
  ```tsx Chat.tsx theme={null}
  import { useStream } from "@langchain/langgraph-sdk/react";
  import type { agent } from "./agent";
  import { MessageBubble } from "./MessageBubble";

  // Node configuration for visual display
  const NODE_CONFIG: Record<string, { label: string; color: string }> = {
    researcher_analytical: { label: "Analytical Research", color: "cyan" },
    researcher_creative: { label: "Creative Research", color: "purple" },
    researcher_practical: { label: "Practical Research", color: "emerald" },
  };

  function MultiAgentChat() {
    const stream = useStream<typeof agent>({
      assistantId: "parallel-research",
      apiUrl: "http://localhost:2024",
    });

    return (
      <div className="flex flex-col gap-4">
        {stream.messages.map((message, idx) => {
          if (message.type !== "ai") {
            return <MessageBubble key={message.id ?? idx} message={message} />;
          }

          // Get streaming metadata to identify the source node
          const metadata = stream.getMessagesMetadata?.(message);
          const nodeName =
            (metadata?.streamMetadata?.langgraph_node as string) ||
            (message as { name?: string }).name;

          const config = nodeName ? NODE_CONFIG[nodeName] : null;

          if (!config) {
            return <MessageBubble key={message.id ?? idx} message={message} />;
          }

          return (
            <div
              key={message.id ?? idx}
              className={`bg-${config.color}-950/30 border border-${config.color}-500/30 rounded-xl p-4`}
            >
              <div className={`text-sm font-semibold text-${config.color}-400 mb-2`}>
                {config.label}
              </div>
              <div className="text-neutral-200 whitespace-pre-wrap">
                {typeof message.content === "string" ? message.content : ""}
              </div>
            </div>
          );
        })}
      </div>
    );
  }
  ```

  ```typescript agent.ts theme={null}
  import { ChatOpenAI } from "@langchain/openai";
  import {
    StateGraph,
    START,
    END,
    Send,
    StateSchema,
    MessagesValue,
    GraphNode,
    ConditionalEdgeRouter,
  } from "@langchain/langgraph";
  import { AIMessage } from "@langchain/core/messages";
  import { z } from "zod";

  // Use different model instances for variety
  const analyticalModel = new ChatOpenAI({ model: "gpt-4.1-mini", temperature: 0.3 });
  const creativeModel = new ChatOpenAI({ model: "gpt-4.1-mini", temperature: 0.9 });
  const practicalModel = new ChatOpenAI({ model: "gpt-4.1-mini", temperature: 0.5 });

  // Define the state schema
  const StateAnnotation = new StateSchema({
    messages: MessagesValue,
    topic: z.string().default(""),
    analyticalResearch: z.string().default(""),
    creativeResearch: z.string().default(""),
    practicalResearch: z.string().default(""),
  });

  type State = typeof StateAnnotation.State;

  // Fan-out to parallel researchers
  const fanOutToResearchers: ConditionalEdgeRouter<State> = (state) => {
    return [
      new Send("researcher_analytical", state),
      new Send("researcher_creative", state),
      new Send("researcher_practical", state),
    ];
  };

  const dispatcherNode: GraphNode<State> = async (state) => {
    const lastMessage = state.messages.at(-1);
    const topic = typeof lastMessage?.content === "string" ? lastMessage.content : "";
    return { topic };
  };

  const analyticalResearcherNode: GraphNode<State> = async (state) => {
    const response = await analyticalModel.invoke([
      { role: "system", content: "You are an analytical research expert. Focus on data and evidence." },
      { role: "user", content: `Research: ${state.topic}` },
    ]);
    return {
      analyticalResearch: response.content as string,
      messages: [new AIMessage({ content: response.content as string, name: "researcher_analytical" })],
    };
  };

  // Similar nodes for creative and practical researchers...

  // Build the graph with parallel execution
  const workflow = new StateGraph(StateAnnotation)
    .addNode("dispatcher", dispatcherNode)
    .addNode("researcher_analytical", analyticalResearcherNode)
    .addNode("researcher_creative", creativeResearcherNode)
    .addNode("researcher_practical", practicalResearcherNode)
    .addEdge(START, "dispatcher")
    .addConditionalEdges("dispatcher", fanOutToResearchers)
    .addEdge("researcher_analytical", END)
    .addEdge("researcher_creative", END)
    .addEdge("researcher_practical", END);

  export const agent = workflow.compile();
  ```
</CodeGroup>

<Card title="Try the parallel research example" icon="users" href="https://github.com/langchain-ai/langgraphjs/tree/main/examples/ui-react/src/examples/parallel-research">
  See a complete implementation of multi-agent streaming with three parallel researchers and distinct visual styling in the `parallel-research` example.
</Card>

## Human-in-the-loop

Handle interrupts when the agent requires human approval for tool execution. Learn more in the [How to handle interrupts](/oss/javascript/langgraph/interrupts#pause-using-interrupt) guide.

<CodeGroup>
  ```tsx Chat.tsx theme={null}
  import { useState } from "react";
  import { useStream } from "@langchain/langgraph-sdk/react";
  import type { HITLRequest, HITLResponse } from "langchain";
  import type { agent } from "./agent";
  import { MessageBubble } from "./MessageBubble";

  function HumanInTheLoopChat() {
    const stream = useStream<typeof agent, { InterruptType: HITLRequest }>({
      assistantId: "human-in-the-loop",
      apiUrl: "http://localhost:2024",
    });

    const [isProcessing, setIsProcessing] = useState(false);

    // Type assertion for interrupt value
    const hitlRequest = stream.interrupt?.value as HITLRequest | undefined;

    const handleApprove = async (index: number) => {
      if (!hitlRequest) return;
      setIsProcessing(true);

      try {
        const decisions: HITLResponse["decisions"] =
          hitlRequest.actionRequests.map((_, i) =>
            i === index ? { type: "approve" } : { type: "approve" }
          );

        await stream.submit(null, {
          command: {
            resume: { decisions } as HITLResponse,
          },
        });
      } finally {
        setIsProcessing(false);
      }
    };

    const handleReject = async (index: number, reason: string) => {
      if (!hitlRequest) return;
      setIsProcessing(true);

      try {
        const decisions: HITLResponse["decisions"] =
          hitlRequest.actionRequests.map((_, i) =>
            i === index
              ? { type: "reject", message: reason }
              : { type: "reject", message: "Rejected along with other actions" }
          );

        await stream.submit(null, {
          command: {
            resume: { decisions } as HITLResponse,
          },
        });
      } finally {
        setIsProcessing(false);
      }
    };

    return (
      <div>
        {/* Render messages */}
        {stream.messages.map((message, idx) => (
          <MessageBubble key={message.id ?? idx} message={message} />
        ))}

        {/* Render approval UI when interrupted */}
        {hitlRequest && hitlRequest.actionRequests.length > 0 && (
          <div className="bg-amber-900/20 border border-amber-500/30 rounded-xl p-4 mt-4">
            <h3 className="text-amber-400 font-semibold mb-4">
              Action requires approval
            </h3>

            {hitlRequest.actionRequests.map((action, idx) => (
              <div
                key={idx}
                className="bg-neutral-900 rounded-lg p-4 mb-4 last:mb-0"
              >
                <div className="flex items-center gap-2 mb-2">
                  <span className="text-sm font-mono text-white">
                    {action.name}
                  </span>
                </div>

                <pre className="text-xs bg-black rounded p-2 mb-3 overflow-x-auto">
                  {JSON.stringify(action.args, null, 2)}
                </pre>

                <div className="flex gap-2">
                  <button
                    onClick={() => handleApprove(idx)}
                    disabled={isProcessing}
                    className="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-sm rounded disabled:opacity-50"
                  >
                    Approve
                  </button>
                  <button
                    onClick={() => handleReject(idx, "User rejected")}
                    disabled={isProcessing}
                    className="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white text-sm rounded disabled:opacity-50"
                  >
                    Reject
                  </button>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    );
  }
  ```

  ```typescript agent.ts theme={null}
  import { createAgent, tool, humanInTheLoopMiddleware } from "langchain";
  import { ChatOpenAI } from "@langchain/openai";
  import { MemorySaver } from "@langchain/langgraph";
  import { z } from "zod";

  const model = new ChatOpenAI({ model: "gpt-4.1-mini" });

  // Tool that requires human approval
  const sendEmail = tool(
    async ({ to, subject, body }) => {
      return {
        status: "success",
        content: `Email sent to ${to} with subject "${subject}"`,
      };
    },
    {
      name: "send_email",
      description: "Send an email. Requires human approval.",
      schema: z.object({
        to: z.string().describe("Recipient email address"),
        subject: z.string().describe("Email subject"),
        body: z.string().describe("Email body"),
      }),
    }
  );

  // Tool that requires approval with limited options
  const deleteFile = tool(
    async ({ path }) => {
      return { status: "success", content: `File "${path}" deleted` };
    },
    {
      name: "delete_file",
      description: "Delete a file. Requires human approval.",
      schema: z.object({
        path: z.string().describe("File path to delete"),
      }),
    }
  );

  // Safe tool - no approval needed
  const readFile = tool(
    async ({ path }) => {
      return { status: "success", content: `Contents of ${path}...` };
    },
    {
      name: "read_file",
      description: "Read file contents. No approval needed.",
      schema: z.object({
        path: z.string().describe("File path to read"),
      }),
    }
  );

  // Create agent with HITL middleware
  export const agent = createAgent({
    model,
    tools: [sendEmail, deleteFile, readFile],
    middleware: [
      humanInTheLoopMiddleware({
        interruptOn: {
          // Email requires all decision types
          send_email: {
            allowedDecisions: ["approve", "edit", "reject"],
            description: "📧 Review email before sending",
          },
          // Deletion only allows approve/reject
          delete_file: {
            allowedDecisions: ["approve", "reject"],
            description: "🗑️ Confirm file deletion",
          },
          // Reading is safe - auto-approved
          read_file: false,
        },
      }),
    ],
    // Required for HITL - persists state across interrupts
    checkpointer: new MemorySaver(),
  });
  ```
</CodeGroup>

<Card title="Try the human-in-the-loop example" icon="hand-stop" href="https://github.com/langchain-ai/langgraphjs/tree/main/examples/ui-react/src/examples/human-in-the-loop">
  See a complete implementation of approval workflows with approve, reject, and edit actions in the `human-in-the-loop` example.
</Card>

## Reasoning models

<Warning>
  Extended reasoning/thinking support is currently experimental. The streaming interface for reasoning tokens varies by provider (OpenAI vs. Anthropic) and may change as abstractions are developed.
</Warning>

When using models with extended reasoning capabilities (like OpenAI's reasoning models or Anthropic's extended thinking), the thinking process is embedded in the message content. You'll need to extract and display it separately.

<CodeGroup>
  ```tsx Chat.tsx theme={null}
  import { useStream } from "@langchain/langgraph-sdk/react";
  import type { Message } from "@langchain/langgraph-sdk";
  import type { agent } from "./agent";
  import { getReasoningFromMessage, getTextContent } from "./utils";

  function ReasoningChat() {
    const stream = useStream<typeof agent>({
      assistantId: "reasoning-agent",
      apiUrl: "http://localhost:2024",
    });

    return (
      <div className="flex flex-col gap-4">
        {stream.messages.map((message, idx) => {
          if (message.type === "ai") {
            const reasoning = getReasoningFromMessage(message);
            const textContent = getTextContent(message);

            return (
              <div key={message.id ?? idx}>
                {/* Render reasoning bubble if present */}
                {reasoning && (
                  <div className="mb-4">
                    <div className="text-xs font-medium text-amber-400/80 mb-2">
                      Reasoning
                    </div>
                    <div className="bg-amber-950/50 border border-amber-500/20 rounded-2xl px-4 py-3">
                      <div className="text-sm text-amber-100/90 whitespace-pre-wrap">
                        {reasoning}
                      </div>
                    </div>
                  </div>
                )}

                {/* Render text content */}
                {textContent && (
                  <div className="text-neutral-100 whitespace-pre-wrap">
                    {textContent}
                  </div>
                )}
              </div>
            );
          }

          return <MessageBubble key={message.id ?? idx} message={message} />;
        })}

        {stream.isLoading && (
          <div className="flex items-center gap-2 text-amber-400/70">
            <span className="text-sm">Thinking...</span>
          </div>
        )}
      </div>
    );
  }
  ```

  ```typescript utils.ts theme={null}
  import type { Message, AIMessage } from "@langchain/langgraph-sdk";

  /**
   * Extracts reasoning/thinking content from an AI message.
   * Supports both OpenAI reasoning (additional_kwargs.reasoning.summary)
   * and Anthropic extended thinking (content blocks with type "thinking").
   */
  export function getReasoningFromMessage(message: Message): string | undefined {
    type MessageWithExtras = AIMessage & {
      additional_kwargs?: {
        reasoning?: {
          summary?: Array<{ type: string; text: string }>;
        };
      };
      contentBlocks?: Array<{ type: string; thinking?: string }>;
    };

    const msg = message as MessageWithExtras;

    // Check for OpenAI reasoning in additional_kwargs
    if (msg.additional_kwargs?.reasoning?.summary) {
      const content = msg.additional_kwargs.reasoning.summary
        .filter((item) => item.type === "summary_text")
        .map((item) => item.text)
        .join("");

      if (content.trim()) return content;
    }

    // Check for Anthropic thinking in contentBlocks
    if (msg.contentBlocks?.length) {
      const thinking = msg.contentBlocks
        .filter((b) => b.type === "thinking" && b.thinking)
        .map((b) => b.thinking)
        .join("\n");

      if (thinking) return thinking;
    }

    // Check for thinking in message.content array
    if (Array.isArray(msg.content)) {
      const thinking = msg.content
        .filter((b): b is { type: "thinking"; thinking: string } =>
          typeof b === "object" && b?.type === "thinking" && "thinking" in b
        )
        .map((b) => b.thinking)
        .join("\n");

      if (thinking) return thinking;
    }

    return undefined;
  }

  /**
   * Extracts text content from a message.
   */
  export function getTextContent(message: Message): string {
    if (typeof message.content === "string") return message.content;

    if (Array.isArray(message.content)) {
      return message.content
        .filter((c): c is { type: "text"; text: string } => c.type === "text")
        .map((c) => c.text)
        .join("");
    }

    return "";
  }
  ```
</CodeGroup>

<Card title="Try the reasoning example" icon="brain" href="https://github.com/langchain-ai/langgraphjs/tree/main/examples/ui-react/src/examples/reasoning-agent">
  See a complete implementation of reasoning token display with OpenAI and Anthropic models in the `reasoning-agent` example.
</Card>

## Custom state types

For custom LangGraph applications, embed your tool call types in your state's messages property.

```tsx theme={null}
import { Message } from "@langchain/langgraph-sdk";
import { useStream } from "@langchain/langgraph-sdk/react";

// Define your tool call types as a discriminated union
type MyToolCalls =
  | { name: "search"; args: { query: string }; id?: string }
  | { name: "calculate"; args: { expression: string }; id?: string };

// Embed tool call types in your state's messages
interface MyGraphState {
  messages: Message<MyToolCalls>[];
  context?: string;
}

function CustomGraphChat() {
  const stream = useStream<MyGraphState>({
    assistantId: "my-graph",
    apiUrl: "http://localhost:2024",
  });

  // stream.values is typed as MyGraphState
  // stream.toolCalls[0].call.name is typed as "search" | "calculate"
}
```

You can also specify additional type configuration for interrupts and configurable options:

```tsx theme={null}
interface MyGraphState {
  messages: Message<MyToolCalls>[];
}

function CustomGraphChat() {
  const stream = useStream<
    MyGraphState,
    {
      InterruptType: { question: string };
      ConfigurableType: { userId: string };
    }
  >({
    assistantId: "my-graph",
    apiUrl: "http://localhost:2024",
  });

  // stream.interrupt is typed as { question: string } | undefined
}
```

## Custom transport

For custom API endpoints or non-standard deployments, use the `transport` option with `FetchStreamTransport` to connect to any streaming API.

```tsx theme={null}
import { useMemo } from "react";
import { useStream, FetchStreamTransport } from "@langchain/langgraph-sdk/react";

function CustomAPIChat({ apiKey }: { apiKey: string }) {
  // Create transport with custom request handling
  const transport = useMemo(() => {
    return new FetchStreamTransport({
      apiUrl: "/api/my-agent",
      onRequest: async (url: string, init: RequestInit) => {
        // Inject API key or other custom data into requests
        const customBody = JSON.stringify({
          ...(JSON.parse(init.body as string) || {}),
          apiKey,
        });

        return {
          ...init,
          body: customBody,
          headers: {
            ...init.headers,
            "X-Custom-Header": "value",
          },
        };
      },
    });
  }, [apiKey]);

  const stream = useStream({
    transport,
  });

  // Use stream as normal
  return (
    <div>
      {stream.messages.map((message, idx) => (
        <MessageBubble key={message.id ?? idx} message={message} />
      ))}
    </div>
  );
}
```

### Example: Streaming from Next.js endpoints

You can host your agent in Next.js API routes instead of running a separate Agent Server. The `useStream` hook communicates via Server-Sent Events (SSE), so any endpoint that returns the correct event format works—including your own Next.js route.

<Tip>
  **When to use your own API routes vs LangSmith**

  For basic agent interaction (streaming, tool calls), rolling your own Next.js API routes works well. For persisting conversations, loading thread history, and conversation branching, consider [LangSmith deployments](/langsmith/deployments), which provide these features out of the box.
</Tip>

#### Server: Stream from an API route

In your Next.js API route, stream the agent's output by returning a `Response` with `text/event-stream` encoding:

```typescript theme={null}
// app/api/agent/route.ts
import { NextRequest } from "next/server";
import { createAgent, tool } from "langchain";
import { ChatAnthropic } from "@langchain/anthropic";
import { MemorySaver } from "@langchain/langgraph";
import type { BaseMessage } from "@langchain/core/messages";

const checkpointer = new MemorySaver();

export async function POST(request: NextRequest) {
  const body = await request.json();

  const agent = createAgent({
    model: new ChatAnthropic({ model: "claude-sonnet-4-6" }),
    tools: [/* your tools */],
    checkpointer,
  });

  const stream = await agent.stream(
    { messages: body.messages },
    {
      encoding: "text/event-stream",
      streamMode: ["values", "updates", "messages"],
      configurable: body.config?.configurable,
      recursionLimit: 10,
    }
  );

  return new Response(stream, {
    headers: { "Content-Type": "text/event-stream" },
  });
}
```

#### Client: Connect with FetchStreamTransport

Use `FetchStreamTransport` to point `useStream` at your Next.js API route. Pass `transport` instead of `apiUrl` and `assistantId` when using a custom endpoint:

```tsx theme={null}
import { useMemo } from "react";
import { useStream, FetchStreamTransport } from "@langchain/langgraph-sdk/react";

function ChatInterface({ apiKey }: { apiKey: string }) {
  const transport = useMemo(() => {
    return new FetchStreamTransport({
      apiUrl: "/api/agent",
      onRequest: async (url: string, init: RequestInit) => {
        // Inject API key or other data into the request body
        const customBody = JSON.stringify({
          ...(JSON.parse(init.body as string) || {}),
          apiKey,
        });
        return { ...init, body: customBody };
      },
    });
  }, [apiKey]);

  const stream = useStream({
    transport,
  });

  return (
    <div>
      {stream.messages.map((message, idx) => (
        <div key={message.id ?? idx}>{/* render message */}</div>
      ))}
      {stream.isLoading && <div>Loading...</div>}
    </div>
  );
}
```

For thread history, pass `threadId` to `useStream` and include it in `configurable` when streaming from your API route. The agent's checkpointer will load and persist state per thread.

<Card title="Full Next.js example" icon="brand-github" href="https://github.com/christian-bromann/langchain-nextjs">
  See a complete Next.js app with LangChain agents, streaming chat, and tool calling in the [langchain-nextjs](https://github.com/christian-bromann/langchain-nextjs) repository.
</Card>

## Related

* [Streaming overview](/oss/javascript/langchain/streaming/overview) — Server-side streaming with LangChain agents
* [useStream API Reference](https://reference.langchain.com/javascript/functions/_langchain_langgraph-sdk.react.useStream.html) — Full API documentation
* [Agent Chat UI](/oss/javascript/langchain/ui) — Pre-built chat interface for LangGraph agents
* [Human-in-the-loop](/oss/javascript/langchain/human-in-the-loop) — Configuring interrupts for human review
* [Multi-agent systems](/oss/javascript/langchain/multi-agent) — Building agents with multiple LLMs

***

<div className="source-links">
  <Callout icon="edit">
    [Edit this page on GitHub](https://github.com/langchain-ai/docs/edit/main/src/oss/langchain/streaming/frontend.mdx) or [file an issue](https://github.com/langchain-ai/docs/issues/new/choose).
  </Callout>

  <Callout icon="terminal-2">
    [Connect these docs](/use-these-docs) to Claude, VSCode, and more via MCP for real-time answers.
  </Callout>
</div>
