Runtime components, options, and adapters for using assistant-ui with externally owned chat state.
API Reference
ExternalStoreAdapter
ExternalStoreAdapterisDisabled?: booleanWhether the entire thread is disabled. When `true`, the composer's input is also disabled (the user cannot type, attach files, or submit). For a narrower gate that keeps the input usable but blocks only sending, use `isSendDisabled`.
isSendDisabled?: booleanWhether sending new messages is currently disabled. When `true`, the thread composer's input remains usable but `send()` becomes a no-op and the thread composer's `canSend` is `false`. Use this to gate sending on external React state (e.g. while tool config is loading) without disabling the input itself the way `isDisabled` does. Edit composers (saving message edits) intentionally ignore this flag.
isRunning?: booleanWhether the thread is running. When provided, this value flows directly to `thread.isRunning`, letting the application keep the thread in a running state even after the last assistant message has completed (for example while non-message stream chunks like suggestions or metadata updates are still arriving). When omitted, `thread.isRunning` falls back to the last-message-status heuristic.
isLoading?: booleanmessages?: readonly T[]messageRepository?: ExportedMessageRepositoryheadId?: string | nullmessages: Array<{ message: ThreadMessage; parentId: string | null; runConfig?: RunConfig; }>
suggestions?: readonly ThreadSuggestion[]state?: ReadonlyJSONValueextras?: unknownsetMessages?: ((messages: readonly T[]) => void)unstable_onBranchChangedeprecatedunstable?: ((event: ExternalStoreBranchChange) => void)Fires when the user explicitly switches branches via the runtime's `switchToBranch` action (e.g. a BranchPicker click). It does not fire on adapter resync, `append`, edit/regenerate, content-only updates, or while the thread is running. Consecutive switches that resolve to the same canonical head are de-duped. `headId` is the canonical (persisted) head of the now-visible branch — optimistic/transient ids are never surfaced. `visibleMessageIds` lists the visible path in order. This complements `setMessages` rather than replacing it: switching still requires `setMessages`, and this callback does not on its own enable branch switching.
Deprecated: This API is still under active development and might change without notice.
onImport?: ((messages: readonly ThreadMessage[]) => void)onExportExternalState?: (() => any)onLoadExternalState?: ((state: any) => void)onNew: (message: AppendMessage) => Promise<void>queue?: ExternalThreadQueueAdapterOpt in to message queuing. Typically produced by `createMessageQueue`.
items: readonly QueueItemState[]enqueue: (message: AppendMessage, options: { steer: boolean }) => voidsteer: (queueItemId: string) => voidremove: (queueItemId: string) => voidclear: (reason: "edit" | "reload" | "cancel-run") => void
onEdit?: ((message: AppendMessage) => Promise<void>)onDelete?: ((messageId: string) => Promise<void> | void)onReload?: ((parentId: string | null, config: StartRunConfig) => Promise<void>)onResume?: ((config: ResumeRunConfig) => Promise<void>)onCancel?: (() => Promise<void>)onAddToolResult?: ((options: AddToolResultOptions) => Promise<void> | void)onResumeToolCall?: ((options: { toolCallId: string; payload: unknown }) => void)onRespondToToolApproval?: ((options: RespondToToolApprovalOptions) => Promise<void> | void)convertMessage?: ExternalStoreMessageConverter<T>adapters?: ExternalStoreAdapter["adapters"]attachments?: AttachmentAdapteraccept: stringadd: (state: { file: File; }) => Promise<PendingAttachment> | AsyncGenerator<PendingAttachment, void>remove: (attachment: Attachment) => Promise<void>send: (attachment: PendingAttachment) => Promise<CompleteAttachment>
speech?: SpeechSynthesisAdapterspeak: (text: string) => SpeechSynthesisAdapter.Utterance
dictation?: DictationAdapterlisten: () => DictationAdapter.SessiondisableInputDuringDictation?: boolean
voice?: RealtimeVoiceAdapterconnect: (options: { abortSignal?: AbortSignal; }) => RealtimeVoiceAdapter.Session
feedback?: FeedbackAdaptersubmit: (feedback: FeedbackAdapterFeedback) => void
threadListdeprecated?: ExternalStoreThreadListAdapterDeprecated: This API is still under active development and might change without notice.
threadIddeprecated?: stringDeprecated: This API is still under active development and might change without notice.
isLoading?: booleanthreads?: readonly ExternalStoreThreadData<"regular">[]archivedThreads?: readonly ExternalStoreThreadData<"archived">[]onSwitchToNewThreaddeprecated?: (() => Promise<void> | void)Deprecated: This API is still under active development and might change without notice.
onSwitchToThreaddeprecated?: ((threadId: string) => Promise<void> | void)Deprecated: This API is still under active development and might change without notice.
onRename?: ( threadId: string, newTitle: string, ) => (Promise<void> | void)onUpdateCustom?: (( threadId: string, custom: Record<string, unknown> | undefined, ) => Promise<void> | void)onArchive?: ((threadId: string) => Promise<void> | void)onUnarchive?: ((threadId: string) => Promise<void> | void)onDelete?: ((threadId: string) => Promise<void> | void)
unstable_capabilitiesunstable?: ExternalStoreAdapter["unstable_capabilities"]copy?: boolean
unstable_enableToolInvocationsunstable?: booleanOpt in to the built-in client-side tool-invocations pipeline (`streamCall` / `execute` / tool-status tracking) for this thread. Defaults to `false` — the runtime does *not* drive client-side tool callbacks on its own. Set to `true` to have the runtime construct a `ToolInvocationTracker` and feed every snapshot through it, so tool callbacks fire automatically for tool-call parts in `messages`. Opt-in by default because most external-store runtimes either run tools entirely server-side, or already wire their own client-side dispatch path. Enabling the embedded tracker on top of an existing dispatch path would cause tool callbacks to run twice. When enabled, client-side tool results (from `execute()` returning, or from `streamCall` resolving) flow back through `adapter.onAddToolResult` like any other tool result, with `modelContent` populated when present.
setToolStatuses?: ((statuses: Record<string, ToolExecutionStatus>) => void)Receives the current per-tool-call execution status map whenever it changes. Only invoked when `unstable_enableToolInvocations` is `true` — the runtime maintains the map via the embedded tracker. Wire this into local React state and feed it into the converter's `metadata.toolStatuses` so the UI can render `executing` spinners and human-input prompts.
ExternalThread
ExternalThread props0: ExternalThreadPropsmessages: readonly ExternalThreadMessage[]isRunning?: booleanisSendDisabled?: booleanWhether sending new messages is currently disabled. When `true`, the thread composer's input remains usable but `send()` is a no-op and `composer.canSend` is `false`. Edit composers (saving message edits) intentionally ignore this flag.
onNew?: (message: AppendMessage) => voidCallback for new messages (non-queue runtimes).
onEdit?: (message: AppendMessage) => voidonReload?: (parentId: string | null) => voidonStartRun?: () => voidonCancel?: () => voidqueue?: ExternalThreadQueueAdapterQueue adapter for runtimes that support message queuing and steering.
items: readonly QueueItemState[]enqueue: (message: AppendMessage, options: { steer: boolean }) => voidsteer: (queueItemId: string) => voidremove: (queueItemId: string) => voidclear: (reason: "edit" | "reload" | "cancel-run") => void
branches?: ExternalThreadBranchAdapterBranch adapter for runtimes that track sibling variants of messages.
getBranches: (messageId: string) => readonly string[]Returns the sibling branch ids for a message in display order, including the message's own id. Return an empty array for messages without alternative branches.
switchToBranch: (branchId: string) => voidMakes the given branch the visible one. The runtime is expected to swap the `messages` array to the selected branch. May be invoked programmatically while a run is in progress; pending queue items are not cleared, and reconciling them with the new branch is the runtime's responsibility.
onRespondToToolApproval?: (options: RespondToToolApprovalOptions) => voidCallback for tool approval decisions. Absent: responding to an approval throws a capability error.
length: 1toString: () => stringtoLocaleString: { (): string; (locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string; }pop: () => ExternalThreadPropspush: (...items: ExternalThreadProps[]) => numberconcat: { (...items: ConcatArray<ExternalThreadProps>[]): ExternalThreadProps[]; (...items: (ExternalThreadProps | ConcatArray<ExternalThreadProps>)[]): ExternalThreadProps[]; }join: (separator?: string) => stringreverse: () => ExternalThreadProps[]shift: () => ExternalThreadPropsslice: (start?: number, end?: number) => ExternalThreadProps[]sort: (compareFn?: ((a: ExternalThreadProps, b: ExternalThreadProps) => number) | undefined) => [ExternalThreadProps]splice: { (start: number, deleteCount?: number): ExternalThreadProps[]; (start: number, deleteCount: number, ...items: ExternalThreadProps[]): ExternalThreadProps[]; }unshift: (...items: ExternalThreadProps[]) => numberindexOf: (searchElement: ExternalThreadProps, fromIndex?: number) => numberlastIndexOf: (searchElement: ExternalThreadProps, fromIndex?: number) => numberevery: { <S>(predicate: (value: ExternalThreadProps, index: number, array: ExternalThreadProps[]) => value is S, thisArg?: any): this is S[]; (predicate: (value: ExternalThreadProps, index: number, array: ExternalThreadProps[]) => unknown, thisArg?: any): boolean; }some: (predicate: (value: ExternalThreadProps, index: number, array: ExternalThreadProps[]) => unknown, thisArg?: any) => booleanforEach: (callbackfn: (value: ExternalThreadProps, index: number, array: ExternalThreadProps[]) => void, thisArg?: any) => voidmap: <U>(callbackfn: (value: ExternalThreadProps, index: number, array: ExternalThreadProps[]) => U, thisArg?: any) => U[]filter: { <S>(predicate: (value: ExternalThreadProps, index: number, array: ExternalThreadProps[]) => value is S, thisArg?: any): S[]; (predicate: (value: ExternalThreadProps, index: number, array: ExternalThreadProps[]) => unknown, thisArg?: any): ExternalThreadProps[]; }reduce: { (callbackfn: (previousValue: ExternalThreadProps, currentValue: ExternalThreadProps, currentIndex: number, array: ExternalThreadProps[]) => ExternalThreadProps): ExternalThreadProps; (callbackfn: (previousValue: ExternalThreadProps, currentValue: ExternalThreadProps, currentIndex: number, array: ExternalThreadProps[]) => ExternalThreadProps, initialValue: ExternalThreadProps): ExternalThreadProps; <U>(callbackfn: (previousValue: U, currentValue: ExternalThreadProps, currentIndex: number, array: ExternalThreadProps[]) => U, initialValue: U): U; }reduceRight: { (callbackfn: (previousValue: ExternalThreadProps, currentValue: ExternalThreadProps, currentIndex: number, array: ExternalThreadProps[]) => ExternalThreadProps): ExternalThreadProps; (callbackfn: (previousValue: ExternalThreadProps, currentValue: ExternalThreadProps, currentIndex: number, array: ExternalThreadProps[]) => ExternalThreadProps, initialValue: ExternalThreadProps): ExternalThreadProps; <U>(callbackfn: (previousValue: U, currentValue: ExternalThreadProps, currentIndex: number, array: ExternalThreadProps[]) => U, initialValue: U): U; }find: { <S>(predicate: (value: ExternalThreadProps, index: number, obj: ExternalThreadProps[]) => value is S, thisArg?: any): S | undefined; (predicate: (value: ExternalThreadProps, index: number, obj: ExternalThreadProps[]) => unknown, thisArg?: any): ExternalThreadProps | undefined; }findIndex: (predicate: (value: ExternalThreadProps, index: number, obj: ExternalThreadProps[]) => unknown, thisArg?: any) => numberfill: (value: ExternalThreadProps, start?: number, end?: number) => [ExternalThreadProps]copyWithin: (target: number, start: number, end?: number) => [ExternalThreadProps]entries: () => ArrayIterator<[number, ExternalThreadProps]>keys: () => ArrayIterator<number>values: () => ArrayIterator<ExternalThreadProps>includes: (searchElement: ExternalThreadProps, fromIndex?: number) => booleanflatMap: <U, This>(callback: (this: This, value: ExternalThreadProps, index: number, array: ExternalThreadProps[]) => U | readonly U[], thisArg?: This | undefined) => U[]flat: <A, D>(this: A, depth?: D | undefined) => FlatArray<A, D>[]at: (index: number) => ExternalThreadPropsfindLast: { <S>(predicate: (value: ExternalThreadProps, index: number, array: ExternalThreadProps[]) => value is S, thisArg?: any): S | undefined; (predicate: (value: ExternalThreadProps, index: number, array: ExternalThreadProps[]) => unknown, thisArg?: any): ExternalThreadProps | undefined; }findLastIndex: (predicate: (value: ExternalThreadProps, index: number, array: ExternalThreadProps[]) => unknown, thisArg?: any) => numbertoReversed: () => ExternalThreadProps[]toSorted: (compareFn?: ((a: ExternalThreadProps, b: ExternalThreadProps) => number) | undefined) => ExternalThreadProps[]toSpliced: { (start: number, deleteCount: number, ...items: ExternalThreadProps[]): ExternalThreadProps[]; (start: number, deleteCount?: number): ExternalThreadProps[]; }with: (index: number, value: ExternalThreadProps) => ExternalThreadProps[]
ExternalThreadProps
ExternalThreadPropsmessages: readonly ExternalThreadMessage[]isRunning?: booleanisSendDisabled?: booleanWhether sending new messages is currently disabled. When `true`, the thread composer's input remains usable but `send()` is a no-op and `composer.canSend` is `false`. Edit composers (saving message edits) intentionally ignore this flag.
onNew?: (message: AppendMessage) => voidCallback for new messages (non-queue runtimes).
onEdit?: (message: AppendMessage) => voidonReload?: (parentId: string | null) => voidonStartRun?: () => voidonCancel?: () => voidqueue?: ExternalThreadQueueAdapterQueue adapter for runtimes that support message queuing and steering.
items: readonly QueueItemState[]enqueue: (message: AppendMessage, options: { steer: boolean }) => voidsteer: (queueItemId: string) => voidremove: (queueItemId: string) => voidclear: (reason: "edit" | "reload" | "cancel-run") => void
branches?: ExternalThreadBranchAdapterBranch adapter for runtimes that track sibling variants of messages.
getBranches: (messageId: string) => readonly string[]Returns the sibling branch ids for a message in display order, including the message's own id. Return an empty array for messages without alternative branches.
switchToBranch: (branchId: string) => voidMakes the given branch the visible one. The runtime is expected to swap the `messages` array to the selected branch. May be invoked programmatically while a run is in progress; pending queue items are not cleared, and reconciling them with the new branch is the runtime's responsibility.
onRespondToToolApproval?: (options: RespondToToolApprovalOptions) => voidCallback for tool approval decisions. Absent: responding to an approval throws a capability error.
ExternalThreadQueueAdapter
The queue surface a runtime exposes so the composer can stay usable during a run and render the pending messages.
ExternalThreadQueueAdapteritems: readonly QueueItemState[]enqueue: (message: AppendMessage, options: { steer: boolean }) => voidsteer: (queueItemId: string) => voidremove: (queueItemId: string) => voidclear: (reason: "edit" | "reload" | "cancel-run") => void
pickExternalStoreSharedOptions
const pickExternalStoreSharedOptions: (options: ExternalStoreSharedOptions) => ExternalStoreSharedOptions;useExternalStoreRuntime
useExternalStoreRuntimestore: ExternalStoreAdapter<T>isDisabled?: booleanWhether the entire thread is disabled. When `true`, the composer's input is also disabled (the user cannot type, attach files, or submit). For a narrower gate that keeps the input usable but blocks only sending, use `isSendDisabled`.
isSendDisabled?: booleanWhether sending new messages is currently disabled. When `true`, the thread composer's input remains usable but `send()` becomes a no-op and the thread composer's `canSend` is `false`. Use this to gate sending on external React state (e.g. while tool config is loading) without disabling the input itself the way `isDisabled` does. Edit composers (saving message edits) intentionally ignore this flag.
isRunning?: booleanWhether the thread is running. When provided, this value flows directly to `thread.isRunning`, letting the application keep the thread in a running state even after the last assistant message has completed (for example while non-message stream chunks like suggestions or metadata updates are still arriving). When omitted, `thread.isRunning` falls back to the last-message-status heuristic.
isLoading?: booleanmessages?: readonly T[]messageRepository?: ExportedMessageRepositoryheadId?: string | nullmessages: Array<{ message: ThreadMessage; parentId: string | null; runConfig?: RunConfig; }>
suggestions?: readonly ThreadSuggestion[]state?: ReadonlyJSONValueextras?: unknownsetMessages?: ((messages: readonly T[]) => void)unstable_onBranchChangedeprecatedunstable?: ((event: ExternalStoreBranchChange) => void)Fires when the user explicitly switches branches via the runtime's `switchToBranch` action (e.g. a BranchPicker click). It does not fire on adapter resync, `append`, edit/regenerate, content-only updates, or while the thread is running. Consecutive switches that resolve to the same canonical head are de-duped. `headId` is the canonical (persisted) head of the now-visible branch — optimistic/transient ids are never surfaced. `visibleMessageIds` lists the visible path in order. This complements `setMessages` rather than replacing it: switching still requires `setMessages`, and this callback does not on its own enable branch switching.
Deprecated: This API is still under active development and might change without notice.
onImport?: ((messages: readonly ThreadMessage[]) => void)onExportExternalState?: (() => any)onLoadExternalState?: ((state: any) => void)onNew: (message: AppendMessage) => Promise<void>queue?: ExternalThreadQueueAdapterOpt in to message queuing. Typically produced by `createMessageQueue`.
items: readonly QueueItemState[]enqueue: (message: AppendMessage, options: { steer: boolean }) => voidsteer: (queueItemId: string) => voidremove: (queueItemId: string) => voidclear: (reason: "edit" | "reload" | "cancel-run") => void
onEdit?: ((message: AppendMessage) => Promise<void>)onDelete?: ((messageId: string) => Promise<void> | void)onReload?: ((parentId: string | null, config: StartRunConfig) => Promise<void>)onResume?: ((config: ResumeRunConfig) => Promise<void>)onCancel?: (() => Promise<void>)onAddToolResult?: ((options: AddToolResultOptions) => Promise<void> | void)onResumeToolCall?: ((options: { toolCallId: string; payload: unknown }) => void)onRespondToToolApproval?: ((options: RespondToToolApprovalOptions) => Promise<void> | void)convertMessage?: ExternalStoreMessageConverter<T>adapters?: ExternalStoreAdapter["adapters"]attachments?: AttachmentAdapteraccept: stringadd: (state: { file: File; }) => Promise<PendingAttachment> | AsyncGenerator<PendingAttachment, void>remove: (attachment: Attachment) => Promise<void>send: (attachment: PendingAttachment) => Promise<CompleteAttachment>
speech?: SpeechSynthesisAdapterspeak: (text: string) => SpeechSynthesisAdapter.Utterance
dictation?: DictationAdapterlisten: () => DictationAdapter.SessiondisableInputDuringDictation?: boolean
voice?: RealtimeVoiceAdapterconnect: (options: { abortSignal?: AbortSignal; }) => RealtimeVoiceAdapter.Session
feedback?: FeedbackAdaptersubmit: (feedback: FeedbackAdapterFeedback) => void
threadListdeprecated?: ExternalStoreThreadListAdapterDeprecated: This API is still under active development and might change without notice.
threadIddeprecated?: stringDeprecated: This API is still under active development and might change without notice.
isLoading?: booleanthreads?: readonly ExternalStoreThreadData<"regular">[]archivedThreads?: readonly ExternalStoreThreadData<"archived">[]onSwitchToNewThreaddeprecated?: (() => Promise<void> | void)Deprecated: This API is still under active development and might change without notice.
onSwitchToThreaddeprecated?: ((threadId: string) => Promise<void> | void)Deprecated: This API is still under active development and might change without notice.
onRename?: ( threadId: string, newTitle: string, ) => (Promise<void> | void)onUpdateCustom?: (( threadId: string, custom: Record<string, unknown> | undefined, ) => Promise<void> | void)onArchive?: ((threadId: string) => Promise<void> | void)onUnarchive?: ((threadId: string) => Promise<void> | void)onDelete?: ((threadId: string) => Promise<void> | void)
unstable_capabilitiesunstable?: ExternalStoreAdapter["unstable_capabilities"]copy?: boolean
unstable_enableToolInvocationsunstable?: booleanOpt in to the built-in client-side tool-invocations pipeline (`streamCall` / `execute` / tool-status tracking) for this thread. Defaults to `false` — the runtime does *not* drive client-side tool callbacks on its own. Set to `true` to have the runtime construct a `ToolInvocationTracker` and feed every snapshot through it, so tool callbacks fire automatically for tool-call parts in `messages`. Opt-in by default because most external-store runtimes either run tools entirely server-side, or already wire their own client-side dispatch path. Enabling the embedded tracker on top of an existing dispatch path would cause tool callbacks to run twice. When enabled, client-side tool results (from `execute()` returning, or from `streamCall` resolving) flow back through `adapter.onAddToolResult` like any other tool result, with `modelContent` populated when present.
setToolStatuses?: ((statuses: Record<string, ToolExecutionStatus>) => void)Receives the current per-tool-call execution status map whenever it changes. Only invoked when `unstable_enableToolInvocations` is `true` — the runtime maintains the map via the embedded tracker. Wire this into local React state and feed it into the converter's `metadata.toolStatuses` so the UI can render `executing` spinners and human-input prompts.
useExternalStoreSharedOptions
useExternalStoreSharedOptionsoptions: ExternalStoreSharedOptionssuggestions?: readonly ThreadSuggestion[]isDisabled?: booleanWhether the entire thread is disabled. When `true`, the composer's input is also disabled (the user cannot type, attach files, or submit). For a narrower gate that keeps the input usable but blocks only sending, use `isSendDisabled`.
isSendDisabled?: booleanWhether sending new messages is currently disabled. When `true`, the thread composer's input remains usable but `send()` becomes a no-op and the thread composer's `canSend` is `false`. Use this to gate sending on external React state (e.g. while tool config is loading) without disabling the input itself the way `isDisabled` does. Edit composers (saving message edits) intentionally ignore this flag.
unstable_capabilitiesunstable?: ExternalStoreSharedOptions["unstable_capabilities"]copy?: boolean