Skip to content

Chat Controller

createChatController is the central orchestrator of rxjs-ai. It wires together a model adapter, a message history, and a reactive status machine into a single, easy-to-use handle. Every interaction — sending a message, retrying, or cancelling — is expressed as an RxJS stream, so you can compose it with any other reactive logic in your application.

import { createChatController } from "rxjs-ai";
const chat = createChatController(myAdapter);

Before creating a controller you need an adapter — an object with a single method that bridges rxjs-ai to your LLM of choice.

interface ChatModelAdapter {
complete(request: ChatModelRequest): Observable<ChatChunk>;
}
interface ChatModelRequest {
messages: ChatMessage[];
signal: AbortSignal;
}

The complete method receives the full conversation history and an AbortSignal for cancellation, and must return an Observable that emits one or more chunks as the model responds.

A chunk is either a plain string or a structured object:

type ChatChunk = string | { content: string; done?: boolean };
FormWhen to use
stringQuick prototyping — every emission is appended to the assistant message.
{ content, done? }Production adapters — set done: true on the final chunk to signal completion explicitly.
import { createChatController } from "rxjs-ai";
const adapter: ChatModelAdapter = {
complete({ messages, signal }) {
// Return an Observable<ChatChunk>
return myLlmStream(messages, signal);
},
};
const chat = createChatController(adapter);
// Subscribe to state changes
chat.messages$.subscribe((msgs) => console.log("Messages:", msgs));
chat.status$.subscribe((s) => console.log("Status:", s));
// Send a user message — the adapter is called automatically
chat.send("Hello, AI!");
ArgumentTypeDescription
modelChatModelAdapterRequired. The model adapter to use for completions. Passed as the first positional argument.
optionsCreateChatControllerOptionsOptional configuration object (see below).
OptionTypeDefaultDescription
now() => numberDate.nowClock function used for message timestamps.
idGenerator() => stringinternal nanoidCustom ID generator for message IDs.
initialMessagesChatMessage[][]Pre-populate the conversation history.
PropertyTypeDescription
messages$Observable<ChatMessage[]>Current message list. Emits after every mutation.
status$Observable<ChatStatus>Lifecycle status of the controller.
error$Observable<unknown | null>The latest error, or null when cleared.
state$Observable<ChatState>Combined snapshot of messages, status, and error.
MethodSignatureDescription
getState() => ChatStateSynchronously read the current state snapshot.
send(content: string, meta?: Record<string, unknown>) => voidAppend a user message and trigger a completion.
retryLast() => voidRemove the last assistant message and re-run the completion.
cancel() => voidAbort the in-flight completion. Status moves to cancelled.
destroy() => voidTear down all internal subscriptions. The controller must not be reused.

The controller follows a deterministic state machine:

idle ──▶ loading ──▶ streaming ──▶ idle
│
├──▶ error
│
└──▶ cancelled
  • idle — No request in flight. Ready to send.
  • loading — A request has been dispatched to the adapter but no chunk has arrived yet.
  • streaming — At least one chunk has been received; the assistant message is being built up.
  • error — The adapter Observable errored. Inspect error$ for details. Call retryLast() to try again.
  • cancelled — The user called cancel() while loading or streaming.

After error or cancelled, the status returns to idle once the user calls send() or retryLast().

type ChatRole = "system" | "user" | "assistant" | "tool";
interface ChatMessage {
id: string;
role: ChatRole;
content: string;
createdAt: number;
meta?: Record<string, unknown>;
}
type ChatStatus = "idle" | "loading" | "streaming" | "error" | "cancelled";
interface ChatState {
messages: ChatMessage[];
status: ChatStatus;
error: unknown | null;
}
type ChatChunk = string | { content: string; done?: boolean };
interface ChatModelRequest {
messages: ChatMessage[];
signal: AbortSignal;
}
interface ChatModelAdapter {
complete(request: ChatModelRequest): Observable<ChatChunk>;
}
interface ChatController {
messages$: Observable<ChatMessage[]>;
status$: Observable<ChatStatus>;
error$: Observable<unknown | null>;
state$: Observable<ChatState>;
getState(): ChatState;
send(content: string, meta?: Record<string, unknown>): void;
retryLast(): void;
cancel(): void;
destroy(): void;
}