Local runtime and custom backends

Give any backend cloud threads, persistence, titles and run reports through useLocalRuntime, or compose the cloud thread list yourself.

useLocalRuntime runs your own chat model adapter, whatever protocol your backend speaks. Given a cloud, it gains the full cloud integration: the thread list, message persistence in the aui/v0 format, automatic titles, run reports, engagement events, feedback and attachments.

Setup

app/chat/page.tsx
"use client";

import { useMemo } from "react";
import {
  AssistantCloud,
  AssistantRuntimeProvider,
  useLocalRuntime,
  type ChatModelAdapter,
} from "@assistant-ui/react";
import { ThreadList } from "@/components/assistant-ui/elements/thread-list.aui";
import { Thread } from "@/components/assistant-ui/elements/thread.aui";

const adapter: ChatModelAdapter = {
  async *run({ messages, abortSignal }) {
    const response = await fetch("/api/chat", {
      method: "POST",
      body: JSON.stringify({ messages }),
      signal: abortSignal,
    });
    const reader = response.body!.getReader();
    const decoder = new TextDecoder();
    let text = "";
    for (;;) {
      const { value, done } = await reader.read();
      if (done) break;
      text += decoder.decode(value, { stream: true });
      yield { content: [{ type: "text", text }] };
    }
  },
};

async function getToken() {
  // Return a user token from your auth provider.
  return "...";
}

export default function ChatPage() {
  const cloud = useMemo(
    () =>
      new AssistantCloud({
        baseUrl: process.env.NEXT_PUBLIC_ASSISTANT_BASE_URL!,
        authToken: getToken,
      }),
    [],
  );
  const runtime = useLocalRuntime(adapter, { cloud });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <div className="grid h-dvh grid-cols-[240px_1fr]">
        <ThreadList />
        <Thread />
      </div>
    </AssistantRuntimeProvider>
  );
}

On React, useLocalRuntime(adapter) with NEXT_PUBLIC_ASSISTANT_BASE_URL set and no cloud uses an anonymous client, the same as the AI SDK runtime. On React Native and Ink construct the client from assistant-cloud and pass it.

useDataStreamRuntime from @assistant-ui/react-data-stream, the runtime for a route that returns an assistant-stream response, accepts every local runtime option including cloud, and gets the same integration.

What the runtime does with the cloud

  • Persistence. Each message is stored as the assistant-ui ThreadMessage, format aui/v0, with its parts, status and metadata, and reloaded on the next visit. A run paused for tool approval is stored when it pauses and updated when it resumes, and is not reported until it finishes.
  • Reports. A run's status and outcome come straight from the message status the runtime keeps: a stop reports aborted, a model length limit length, a failure the error and its code. The runtime measures duration, time to first token and step timing itself. Model, provider, usage and trace id are read from the message metadata, which your adapter fills:
yield {
  content: [{ type: "text", text }],
  metadata: {
    custom: { modelId: "gpt-5.6-luna", provider: "openai", traceId },
    unstable_state: null,
  },
};

custom.modelId, custom.provider and custom.traceId are the keys the report reads on this format; per step usage rides on metadata.steps[].usage. See Run reports.

  • Everything else is the same as the AI SDK runtime: titles after the first response, engagement events from the store, feedback and attachments through the default adapters.

Composing the thread list yourself

The AI SDK, LangGraph and local runtimes all build on one hook, which you can use directly when you write a runtime of your own:

import { useCloudThreadListRuntime } from "@assistant-ui/react";

const runtime = useCloudThreadListRuntime({
  cloud,
  runtimeHook: useMyThreadRuntime,
  create: async () => ({ externalId: await backend.createThread() }),
  delete: async (threadId) => backend.deleteThread(threadId),
});
OptionMeaning
cloudThe client the list is backed by.
runtimeHookA hook returning the per thread runtime; it is called once per mounted thread.
createCalled when a new thread is created; return your backend's id and it is stored as the cloud thread's external_id.
deleteCalled with the thread before the cloud thread is deleted.

This is also the way to a runtime that has no cloud option of its own. useAgUiRuntime from @assistant-ui/react-ag-ui and useA2ARuntime from @assistant-ui/react-a2a read the history and attachments adapters from the context this hook provides, and useExternalStoreRuntime underneath them merges the feedback adapter from the same context, so passing either as the runtimeHook gives an AG-UI or A2A app the cloud thread list and titles, persistence of every message, feedback, attachments, engagement events, and run reports read from the stored assistant messages:

import { useCloudThreadListRuntime } from "@assistant-ui/react";
import { useAgUiRuntime } from "@assistant-ui/react-ag-ui";

const runtime = useCloudThreadListRuntime({
  cloud,
  runtimeHook: () => useAgUiRuntime({ agent }),
});

The lower level useCloudThreadListAdapter({ cloud, create, delete, sdk }) returns the adapter alone, and createCloudThreadListAdapter builds it outside React. Both register the calling package's identity on the client through sdk, so the project's Settings › Telemetry lists your integration next to the SDK version.

Custom history adapters

A runtime that wants to store messages in another shape implements ThreadHistoryAdapter.withFormat(adapter) with a MessageFormatAdapter, the same seam the AI SDK runtime uses for ai-sdk/v6. createFormattedPersistence in assistant-cloud handles the id mapping and parent chaining, and the adapter that withFormat returns exposes reportTelemetry(items, { durationMs, stepTimestamps, message }), which sends the run report; the message is the thread message the items came from, whose status and timing complete the report. See the package reference.