By default, useLocalRuntime manages threads and messages in memory. You can connect to your own backend in two ways depending on your needs.
Option 1: ChatModelAdapter only#
The simplest approach — keep thread management local, but send messages to your backend for inference.
import type { ChatModelAdapter } from "@assistant-ui/react-ink";
export const myChatAdapter: ChatModelAdapter = {
async *run({ messages, abortSignal }) {
const response = await fetch("https://my-api.com/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages }),
signal: abortSignal,
});
const reader = response.body?.getReader();
if (!reader) throw new Error("No response body");
const decoder = new TextDecoder();
let fullText = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
fullText += chunk;
yield { content: [{ type: "text", text: fullText }] };
}
},
};
import { useLocalRuntime, AssistantRuntimeProvider } from "@assistant-ui/react-ink";
import { myChatAdapter } from "./adapters/my-chat-adapter.js";
export function App() {
const runtime = useLocalRuntime(myChatAdapter);
return (
<AssistantRuntimeProvider runtime={runtime}>
{/* your chat UI */}
</AssistantRuntimeProvider>
);
}
This gives you:
- Streaming chat responses from your API
- In-memory thread list (lost on process exit)
- Multi-thread support
Option 2: Full backend thread management#
When you want your backend to own thread state (e.g. for persistence across sessions, team sharing, or server-side history), implement a RemoteThreadListAdapter.
Implement the adapter#
import type { RemoteThreadListAdapter } from "@assistant-ui/react-ink";
import { createAssistantStream } from "assistant-stream";
const API_BASE = "https://my-api.com";
export const myThreadListAdapter: RemoteThreadListAdapter = {
async list() {
const res = await fetch(`${API_BASE}/threads`);
const threads = await res.json();
return {
threads: threads.map((t: any) => ({
remoteId: t.id,
status: t.archived ? "archived" : "regular",
title: t.title,
})),
};
},
async initialize(localId) {
const res = await fetch(`${API_BASE}/threads`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ localId }),
});
const { id } = await res.json();
return { remoteId: id, externalId: undefined };
},
async rename(remoteId, title) {
await fetch(`${API_BASE}/threads/${remoteId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
});
},
async archive(remoteId) {
await fetch(`${API_BASE}/threads/${remoteId}/archive`, {
method: "POST",
});
},
async unarchive(remoteId) {
await fetch(`${API_BASE}/threads/${remoteId}/unarchive`, {
method: "POST",
});
},
async delete(remoteId) {
await fetch(`${API_BASE}/threads/${remoteId}`, { method: "DELETE" });
},
async fetch(remoteId) {
const res = await fetch(`${API_BASE}/threads/${remoteId}`);
const t = await res.json();
return {
remoteId: t.id,
status: t.archived ? "archived" : "regular",
title: t.title,
};
},
async generateTitle(remoteId, unstable_messages) {
return createAssistantStream(async (controller) => {
const res = await fetch(`${API_BASE}/threads/${remoteId}/title`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: unstable_messages }),
});
const { title } = await res.json();
controller.appendText(title);
});
},
};
Compose the runtime#
import {
useLocalRuntime,
useRemoteThreadListRuntime,
AssistantRuntimeProvider,
} from "@assistant-ui/react-ink";
import { myChatAdapter } from "./adapters/my-chat-adapter.js";
import { myThreadListAdapter } from "./adapters/my-thread-list-adapter.js";
function useAppRuntime() {
return useRemoteThreadListRuntime({
runtimeHook: () => useLocalRuntime(myChatAdapter),
adapter: myThreadListAdapter,
});
}
export function App() {
const runtime = useAppRuntime();
return (
<AssistantRuntimeProvider runtime={runtime}>
{/* your chat UI */}
</AssistantRuntimeProvider>
);
}
Adapter methods#
| Method | Description |
|---|---|
list() | Return all threads on mount |
initialize(localId) | Create a thread server-side, return { remoteId } |
rename(remoteId, title) | Persist title changes |
archive(remoteId) | Mark thread as archived |
unarchive(remoteId) | Restore archived thread |
delete(remoteId) | Permanently remove thread |
fetch(remoteId) | Fetch single thread metadata |
generateTitle(remoteId, unstable_messages) | Return an AssistantStream with the generated title |
Which option to choose?#
| Option 1: ChatModelAdapter | Option 2: RemoteThreadListAdapter | |
|---|---|---|
| Thread storage | In-memory (process lifetime) | Your backend |
| Message storage | In-memory | On-device (can add history adapter for server-side) |
| Cross-session persistence | No | Yes |
| Setup complexity | Minimal | Moderate |
| Best for | CLI tools, demos, prototypes | Production apps with persistence |