runtime について
assistant-ui Runtime は、アシスタントのチャット UI を構築する開発者向けの状態・操作レイヤーです。メッセージストアをローカルで管理する方式と外部ストアを使う方式があり、共通のクライアント API からスレッド、分岐、実行ライフサイクルを扱えます。
主な機能
- 01ローカル実行時と外部ストアの選択肢
- 02メッセージ、スレッド、実行ライフサイクルの状態
- 03分岐、編集、再生成
- 04リアクティブな状態とイベント Hook
- 05フレームワークおよびプロトコルアダプター
活用シーン
- 01チャット UI とモデルアダプターの接続
- 02既存のメッセージストアを使ったチャットコンポーネントの利用
- 03スレッド状態の監視と実行制御
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ assistant-ui/ skills --skill runtimeインストール後、Claude Code で/runtimeを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/adapters.mdexternal-store.mdlocal-runtime.mdruntime-concepts.mdstate-hooks.mdtypes.mdvoice.md詳細
assistant-ui ランタイム
最新の API については、常に assistant-ui.com/llms.txt を参照してください。
ランタイムはチャット UI の背後にある状態とアクションのレイヤーです。メッセージ、スレッド、ブランチ、実行ライフサイクルを管理し、すべてのプリミティブとフックが統一された AssistantClient を通じてそこから読み取ります。構築方法は 2 つあります。LocalRuntime(useLocalRuntime)はメッセージストアを自動で管理します。1 つの ChatModelAdapter.run 関数を実装するだけで、ブランチ、編集、再生成が無料で利用できます。ExternalStoreRuntime(useExternalStoreRuntime)はその逆で、メッセージを自分で管理し、提供するコールバックに応じて UI 機能が有効になります。useChatRuntime(@assistant-ui/ai-sdk)などのフレームワークアダプターや、useAssistantTransportRuntime などのプロトコルランタイムは、この 2 つのいずれかの上に構築されています。AssistantRuntimeProvider の下にマウントされると、すべてのランタイムは useAui、useAuiState、useAuiEvent を通じて同じ方法で読み取られ、操作されます。
リファレンス
- ./references/local-runtime.md -- useLocalRuntime の詳細: ストリーミング、ツール呼び出し、human-in-the-loop、承認ゲート、resumeRun
- ./references/external-store.md -- useExternalStoreRuntime の詳細: ハンドラーマトリクス、ブランチ、キューイング、ツール結果
- ./references/state-hooks.md -- useAui、useAuiState、useAuiEvent、状態の形状、削除されたレガシーフック
- ./references/types.md -- ThreadMessage、MessageStatus、MessagePart、添付ファイル、機能の型
- ./references/adapters.md -- 添付ファイル、音声、ディクテーション、フィードバック、サジェスチョン、履歴のアダプター
- ./references/voice.md -- RealtimeVoiceAdapter によるリアルタイム双方向音声
- ./references/runtime-concepts.md -- unstable_ ポリシー、useAssistantTransportRuntime、MessageNotSentError、reloadMainThread、メッセージのタイミング
ランタイム階層
AssistantRuntime
├── ThreadListRuntime (threads)
│ └── ThreadListItemRuntime[] (threadListItem)
└── ThreadRuntime (thread)
├── ComposerRuntime (composer) new-message input
├── SuggestionRuntime[] (suggestion) follow-up prompts, via thread.suggestions
└── MessageRuntime[] (message)
├── ComposerRuntime (composer) edit-message input
├── MessagePartRuntime[] (part)
├── ChainOfThoughtRuntime (chainOfThought)
│ └── MessagePartRuntime[] (part)
└── AttachmentRuntime[] (attachment)
modelContext と tools はスレッドスコープとは独立して解決されます。詳細は runtime-concepts.md と tools スキルを参照してください。ツリー内のすべてのノードは、aui.<scope>(命令型)または useAuiState セレクター内の s.<scope>(リアクティブ)から到達可能で、呼び出し元コンポーネントがレンダリングされる場所に自動的にスコープされます。
useAui、useAuiState、useAuiEvent
useAui() は現在の AssistantClient を返します。状態を購読しないため、その同一性は構造的な変更(たとえばスレッドの切り替え)のときのみ変化します。イベントハンドラーや命令型コードで使用してください。
import { useAui } from "@assistant-ui/react";
const aui = useAui();
aui.thread.append({ role: "user", content: [{ type: "text", text: "Hello!" }] });
aui.thread.cancelRun();
useAuiState(selector) は AssistantState の一部を購読し、選択された値が変化したとき(Object.is で比較)にのみ再レンダリングします。セレクターはストアの更新ごとに実行されるため、プリミティブまたは安定した参照を返す必要があり、新しいオブジェクトや配列リテラルを返してはならず、また状態全体を返してもいけません(それは例外をスローします)。
import { useAuiState } from "@assistant-ui/react";
const isRunning = useAuiState((s) => s.thread.isRunning); // primitive: correct
const messages = useAuiState((s) => s.thread.messages); // stable array reference: correct
// Wrong: a new object literal every call re-renders on every store update
const bad = useAuiState((s) => ({ isRunning: s.thread.isRunning, text: s.composer.text }));
useAuiState は値ごとに 1 回呼び出してください(または複数の呼び出しを組み合わせてください)。値をまとめるためにスコープを新しいオブジェクトに展開しないでください。
useAuiEvent(nameOrSelector, callback) はコンポーネントのライフタイムの間購読します。コールバックはエフェクトイベントのシム内で実行されるため、メモ化された参照なしで最新のクロージャが発火します。
import { useAuiEvent } from "@assistant-ui/react";
useAuiEvent("thread.modelContextUpdate", ({ threadId }) => {
console.log("Model context updated", threadId);
});
スコープアクセサーはプロパティです
0.15 以降、aui.<scope> は呼び出しではなくプロパティです。呼び出し(aui.thread())はまだ動作しますが非推奨です。スコープ上のメソッドは括弧を保持します。
aui.thread.getState(); // property accessor
aui.threads.switchToNewThread();
aui.thread.composer().send(); // composer() is a method of the thread scope
aui.thread.message({ index: 0 }); // selector object, not a bare index
利用できないスコープを選択しても例外はスローされなくなりました。aui.message は常に真値です。使用前に source で利用可能性を確認してください。スコープがマウントされていない場合、source は null になります。
if (aui.message.source != null) {
aui.message.reload();
}
source、query、name は予約されたアクセサリープロパティであり、スコープのメソッドに解決されることはありません。
AuiConfig と AuiProvider
AuiProvider は、サブツリーに対して AssistantClient をマウントします。その config プロパティは、@assistant-ui/react からインポートした AuiConfig({...}) で構築する必要があります(生のオブジェクトリテラルは型エラーになります)。トップレベルでは、config だけでそのサブツリーのクライアントが作成されます。親プロバイダーの下にネストする場合、extends は必須です。extends={aui} は親クライアントを拡張し、extends={null} は新しいルートを分離します(開発時に強制されます)。AssistantRuntimeProvider は内部的に AuiProvider をインストールし、さらに config を受け付けて、ランタイム自身のスコープに加えて追加のスコープ(ツールキットなど)をアタッチします。ランタイムの周りに AuiProvider を手動で配線する代わりに、こちらを使用してください。
import { AssistantRuntimeProvider, AuiConfig, AuiProvider, Tools, useAui } from "@assistant-ui/react";
// Runtime root: config attaches extra scopes next to the runtime's own
const config = AuiConfig({ tools: Tools({ toolkit }) });
<AssistantRuntimeProvider runtime={runtime} config={config}>{children}</AssistantRuntimeProvider>;
// Nested scope: extend the parent client
function MessageScope({ children }: { children: React.ReactNode }) {
const aui = useAui();
const nested = AuiConfig({ tools: Tools({ toolkit: extraToolkit }) });
return <AuiProvider extends={aui} config={nested}>{children}</AuiProvider>;
}
// Isolated root: detach from any parent client
const isolated = AuiConfig({});
<AuiProvider extends={null} config={isolated}>{children}</AuiProvider>;
config は単なるデータです。モジュールスコープに巻き上げたり、レンダリングごとにインラインで構築したり、メモ化したりできます。プロバイダーが config の同一性に依存することはありません。AuiProvider の ref は、マウント後に生成されたクライアントを受け取ります。
スレッドとメッセージの操作
const aui = useAui();
const thread = aui.thread;
thread.append({ role: "user", content: [{ type: "text", text: "Hello" }] });
thread.startRun({ parentId: null });
thread.cancelRun();
const state = thread.getState(); // { messages, isRunning, capabilities, composer, ... }
const message = thread.message({ index: 0 }); // or { id: messageId }
message.reload();
message.switchToBranch({ position: "next" });
message.submitFeedback({ type: "positive" });
const editComposer = message.composer();
editComposer.beginEdit();
editComposer.setText("Updated");
editComposer.send();
イベント
ほとんどのイベントは非推奨であり、代わりに useAuiState から同じ遷移を導出することが推奨されます。これは、初回レンダリング時とリプレイ時に、イベントハンドラーとは異なり正しく動作します。
| イベント | ステータス |
|---|---|
threads.selectionChanged | 現行: メインスレッドの切り替えごとに { threadId, previousThreadId } を伴って 1 回発火します。マウント時に最初に選択されたスレッドに対しては発火しません |
thread.modelContextUpdate | 現行: モデルコンテキストは state ではなくプロバイダーに存在するため、state から導出できる等価なものがありません |
composer.attachmentAddError | 現行: reason は "no-adapter" | "not-accepted" | "adapter-error" です |
composer.send、composer.attachmentAdd | 非推奨: composer の text / attachments を監視してください |
thread.runStart、thread.runEnd | 非推奨: s.threadRunning の切り替わりを監視してください |
thread.initialize | 非推奨: s.thread.messages が空でなくなるのを監視してください |
threadListItem.switchedTo、threadListItem.switchedAway | 非推奨: threads.selectionChanged を使用し、必要に応じてアイテムごとのスコープ内で id によるフィルタリングを行ってください |
非推奨のペアは引き続き発火し、次のメジャーまで動作し続けます。threads.selectionChanged は、古いペアが発火しなかった状況でも発火します。たとえば switchToNewThread() や、マウント後に解決されるディープリンクされた初期スレッドなどです。
オプションのスコープ
s.optional.<scope> は、スコープがマウントされていない場合に例外をスローする代わりに undefined に解決されます。これは、スコープの内側と外側の両方でレンダリングされるコンポーネントからスコープを読み取る安全な方法です。
const partType = useAuiState((s) => s.optional.part?.type);
命令型の同等物は aui.<scope>.source != null です。
ケイパビリティ
const capabilities = useAuiState((s) => s.thread.capabilities);
RuntimeCapabilities(@assistant-ui/core から): switchToBranch、switchBranchDuringRun、edit、reload、delete、cancel、refetchThread、unstable_copy、speech、dictation、voice、attachments、feedback、queue。ランタイムはこれらのほぼすべてを、明示的なオプションではなく、指定したもの(コールバック、アダプター)から導出します。unstable_copy は、ExternalStoreRuntime が unstable_capabilities を介して強制的にオフにできる唯一のフラグです。refetchThread は、aui.threads.reloadMainThread() が開いているスレッドをその場で更新するのか、それともランタイムの再マウントにフォールバックするのかを報告します。詳細は runtime-concepts.md を参照してください。
よくある落とし穴
「undefined のプロパティを読み取れません」
- フックが
AssistantRuntimeProvider(または設定がスコープを提供するAuiProvider)の内部で呼び出されていることを確認してください。 - まだマウントされていない可能性があるスコープについては、
s.optional.<scope>を読み取るか、aui.<scope>.source != nullでガードしてください。
useAuiState による無限再レンダリング
- セレクターが新しいオブジェクトまたは配列リテラルを返していました。これにはスコープをオブジェクトにスプレッドする場合も含まれます。プリミティブは個別の呼び出しで選択するか、メモ化された参照を返してください。
レガシーフックのインポートが解決できない
useAssistantRuntime、useThreadRuntime、useThread、useMessage、useComposer、useMessagePart、useAttachment、useThreadListItemなどは 0.15 で削除されました。完全なマッピングについては state-hooks.md を、または update スキルを参照してください。
状態が更新されない
- レンダー内で
getState()を読み取るのではなく、useAuiStateでセレクターを使用してください。getState()は一度きりのスナップショットであり、サブスクリプションではありません。
複数のスレッドまたは会話サイドバー
- このスキルは単一スレッドの状態とランタイムフックを扱います。スレッドの作成、切り替え、アーカイブ、およびスレッド一覧のレンダリングについては、代わりに thread-list スキルを使用してください。
関連スキル
- thread-list -- マルチスレッド UI、スレッドの CRUD、
threads.selectionChangedのコンシューマー - tools -- ツールキットの作成、ツール呼び出し UI、承認ゲートとヒューマンツールの詳細
- elements -- この状態を自動的に読み取る、スタイル済みの
Threadおよびコンポーザーコンポーネント - cloud --
AssistantCloud、マネージド永続化、およびuseChatRuntime({ cloud }) - streaming -- プロトコルランタイムの基盤となる DataStream および AssistantTransport のワイヤプロトコル