Page actions

React streaming preview

Send a message, watch partial text arrive, and wait for the response to finish.

Recorded in the real example with fixture responses paced for readability. No live model requests or narration. Actual-speed footage with reading pauses. Capture details

Read the walkthrough
  1. Enter a message and choose Send.
  2. The conversation displays partial response text while the request is active.
  3. The completed response remains in the conversation.
Download the text walkthrough

Let the traveler read an answer while it is being written. Streaming turns one backend response into a succession of message updates, so the UI can show useful text before the whole run finishes.

The native React APIs on this page are experimental, private source-build packages. Run the repository example to use them; they are not an npm installation recipe.

Start with the learning workbench to follow one turn, inspect partial text and completion, and connect each observation to source. This page keeps the full streaming example in Run, Code, and API.

Observe a session, own its lifetime

The application creates and confirms a thread, constructs a session for it, and owns submission and disposal. useAgent observes a stable object with getSnapshot and subscribe; rendering or subscribing does not start a request. The example projects the session into message rows before publishing its application snapshot. This small observer shows the same separation without the rest of the demo UI.

Sequence diagramOne user turn, from request to response
  1. Person → ApplicationAsk for a three-day trip plan.This starts one user turn.
  2. Application → RuntimeSubmit a message on a confirmed thread.The adapter starts an agent run.
  3. Runtime → Model / toolsPlan, look up information and continue.One turn can contain several model and tool calls.
  4. Runtime → Threadplane → UIStream message and state updates.The response is built incrementally; a chunk is not a new turn.
  5. Runtime → StorageSave state when the backend supports it.LangGraph checkpoints belong to the thread.
  6. Threadplane → ApplicationShow the final answer and settled status.The next user message starts the next turn.
A turn describes the interaction. A run executes the agent. Requests carry commands and events.
Read diagram description

Read top to bottom. A person asks for a trip plan. The application submits their message to a confirmed thread. An agent run can invoke a model and tools more than once. Threadplane observes streamed message and state events and the UI updates as they arrive. Persistence depends on the backend: LangGraph can save checkpoints during execution; the AG-UI protocol alone does not provide storage. When the run settles, the UI shows the answer. The next user message starts another turn; a tool result or streamed token does not.

import type { AgentSession } from '@threadplane/core';
import { useAgent } from '@threadplane/react';
 
export function ResponseStatus({ session }: { session: AgentSession }) {
  const snapshot = useAgent(session);
  return <p role="status">{snapshot.status === 'running' ? 'Working…' : snapshot.status === 'error' ? 'Unable to complete' : 'Ready'}</p>;
}

The React conversation walkthrough connects this capability to requests, tools, UI state, and saved history.

Try the example

Choose Run, send a message, and watch the response arrive incrementally. Later messages reuse the confirmed conversation. Stop cancels active work. After cancellation or a request failure, choose New conversation before sending again; the example never silently retries an uncertain conversation creation.

The shared runtime uses the existing Threadplane example backend. A developer LangGraph connection uses the runtime settings in the workspace. Keep credentials in those settings, rather than in messages or application code.

Ownership and rendering

A stable application owner controls the session and message-content lifetime. The session is created only after conversation creation is confirmed. React observes the application's snapshots through useAgent; the application controls conversation creation, cancellation, replacement, and disposal. Leaving the example releases the session and prevents late results from updating its UI.

app.tsx
// eslint-disable-next-line @nx/enforce-module-boundaries -- The isolated consumer copies this internal example layout.
import { ExampleFooter, ExampleLayout } from '../../../../shared/react-example';
import { useEffect, useRef, useState } from 'react';
import { useAgent } from '@threadplane/react';
import { ChatInput, MessageList } from '@threadplane/react/chat';
import { createStreamingInspection } from './inspection';
import type { InspectionFrame } from './inspection';
import {
  createConnectedApplication,
  type streamingConnection,
} from './connection';
 
export function StreamingDemo({
  connection,
  onReady,
  onInspection,
}: {
  readonly connection: ReturnType<typeof streamingConnection>;
  readonly onReady: () => void;
  readonly onInspection?: (frame: InspectionFrame) => void;
}) {
  const publisher = useRef(onInspection);
  publisher.current = onInspection;
  const [inspection] = useState(() =>
    createStreamingInspection((frame) => publisher.current?.(frame))
  );
  const [application, setApplication] = useState(() =>
    createConnectedApplication(connection)
  );
  const [resetting, setResetting] = useState(false);
  const [generation, setGeneration] = useState(0);
  const live = useRef(true);
  const snapshot = useAgent(application);
  useEffect(() => inspection.attach(application), [application, inspection]);
  useEffect(() => {
    onReady();
  }, [onReady]);
  useEffect(
    () => () => {
      void application.dispose();
    },
    [application]
  );
  useEffect(
    () => () => {
      live.current = false;
      inspection.dispose();
    },
    [inspection]
  );
 
  async function reset() {
    if (snapshot.busy || resetting) return;
    setResetting(true);
    inspection.reset();
    await application.dispose();
    if (!live.current) return;
    setApplication(createConnectedApplication(connection));
    setGeneration((value) => value + 1);
    setResetting(false);
  }
 
  const status = snapshot.busy
    ? snapshot.creation === 'pending'
      ? 'Creating conversation…'
      : 'Receiving response…'
    : snapshot.outcome === 'success'
    ? 'Response complete.'
    : snapshot.outcome === 'aborted'
    ? 'Stopped. Start a new conversation to continue.'
    : !snapshot.canSubmit
    ? 'Start a new conversation to continue.'
    : 'Ready.';
 
  return (
    <ExampleLayout className="streaming-demo">
      <header>
        <div>
          <h1>LangGraph streaming</h1>
        </div>
        <button
          type="button"
          disabled={snapshot.busy || resetting}
          onClick={() => void reset()}
        >
          New conversation
        </button>
      </header>
      <p>Watch an answer arrive as the agent writes it.</p>
      <MessageList rows={snapshot.rows} label="Conversation" />
      <ExampleFooter>
        {snapshot.error && <p role="alert">{snapshot.error}</p>}
        <ChatInput
          key={generation}
          label="Message"
          placeholder="Ask about LangGraph…"
          disabled={resetting || (!snapshot.canSubmit && !snapshot.busy)}
          busy={snapshot.busy}
          onStop={snapshot.busy ? () => void application.stop() : undefined}
          onSubmit={(text) => {
            if (!application.getSnapshot().canSubmit) return false;
            void inspection.submit(application, text);
          }}
        />
        <p role="status">{status}</p>
      </ExampleFooter>
    </ExampleLayout>
  );
}

The connection uses the framework-neutral LangGraph session and the installed SDK. It shares the Python graph with Angular, while its UI and runtime bundle are separate.

connection.ts
import { Client } from '@langchain/langgraph-sdk';
import { createSession } from '@threadplane/langgraph';
// eslint-disable-next-line @nx/enforce-module-boundaries -- The isolated pilot build copies this neutral internal bridge; it is not a published package.
import type { RuntimeBridgeConfiguration } from '../../../../../libs/cockpit-runtime-bridge/src/index';
import { createStreamingApplication } from './application';
 
export function streamingConnection(
  configuration: RuntimeBridgeConfiguration
): {
  apiUrl: string;
  headers: Record<string, string>;
} {
  if (configuration.status === 'error')
    throw new Error('Runtime configuration failed.');
  if (
    configuration.status === 'configured' &&
    configuration.target.kind === 'ag-ui'
  ) {
    throw new Error('Runtime configuration failed.');
  }
  const target =
    configuration.status === 'configured' &&
    configuration.target.kind === 'langsmith'
      ? configuration.target
      : null;
  return {
    apiUrl: target?.apiUrl ?? new URL('/api', window.location.origin).href,
    headers: target ? { 'x-api-key': target.apiKey } : {},
  };
}
 
export function createConnectedApplication(
  connection: ReturnType<typeof streamingConnection>
) {
  const client = new Client({
    apiUrl: connection.apiUrl,
    apiKey: null,
    defaultHeaders: connection.headers,
    callerOptions: { maxRetries: 0 },
  });
  return createStreamingApplication({
    async createThread(signal) {
      const threadId = crypto.randomUUID();
      const created = await client.threads.create({
        threadId,
        ifExists: 'raise',
        signal,
      });
      if (created.thread_id !== threadId)
        throw new Error('Conversation creation was not confirmed.');
      return threadId;
    },
    sessionFactory: (threadId) =>
      createSession({
        assistantId: 'streaming',
        threadId,
        apiUrl: connection.apiUrl,
        clientOptions: { maxRetries: 0, defaultHeaders: connection.headers },
      }),
  });
}

Build from source

From a Threadplane repository checkout with dependencies installed:

npx nx build cockpit-langgraph-streaming-react
npx nx e2e cockpit-langgraph-streaming-react

The build installs local candidate packages in an isolated consumer and checks that the browser bundle uses those packages. The browser tests exercise real SDK requests against a local fixture. Run and Code on this page show the same React implementation.

Looking for something specific?