关于 runtime
assistant-ui Runtime 为构建助手聊天界面的开发者提供状态与操作层。它支持由本地运行时管理消息,或接入外部消息存储,并通过统一客户端 API 暴露线程、分支和运行生命周期。
主要功能
- 01本地运行时与外部存储模式
- 02消息、线程和运行生命周期状态
- 03分支、编辑与重新生成
- 04响应式状态和事件 Hook
- 05框架及协议适配器
适用场景
- 01将聊天界面连接到模型适配器
- 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 Runtime
请务必查阅 assistant-ui.com/llms.txt 以获取最新 API。
Runtime 是聊天 UI 背后的状态与操作层:它管理消息、线程、分支和运行生命周期,所有原语和钩子都通过统一的 AssistantClient 读取它。构建 Runtime 有两种方式。LocalRuntime(useLocalRuntime)会为你管理消息存储;你只需实现一个 ChatModelAdapter.run 函数,分支、编辑和重新生成便可直接使用。ExternalStoreRuntime(useExternalStoreRuntime)则相反:消息由你管理,UI 功能会根据你提供的回调启用。useChatRuntime(@assistant-ui/ai-sdk)等框架适配器,以及 useAssistantTransportRuntime 等协议 Runtime,都基于这两种方式之一构建。挂载到 AssistantRuntimeProvider 下后,所有 Runtime 都通过 useAui、useAuiState 和 useAuiEvent 以相同方式读取和驱动。
参考资料
- ./references/local-runtime.md -- 深入了解 useLocalRuntime:流式传输、工具调用、人机协作、审批关卡、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、消息计时
Runtime 层级
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(也可以组合多次调用);不要将作用域展开到新对象中来捆绑多个值。
useAuiEvent(nameOrSelector, callback) 会在组件的整个生命周期内订阅。回调在 effect-event shim 中运行,因此无需记忆化引用,也会触发最新的闭包。
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 就会创建该子树的客户端。嵌套在父级 Provider 下时,必须设置 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>;
配置是纯数据:可以将它提到模块作用域、在每次渲染时内联构建,或使用 memo 记忆化;Provider 不会依赖配置的标识。挂载后,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 }。挂载时不会针对最初选中的线程触发 |
thread.modelContextUpdate | 当前可用:模型上下文位于 Provider 中,而不是状态中,因此没有可从状态推导的等效方式 |
composer.attachmentAddError | 当前可用:reason 为 "no-adapter" | "not-accepted" | "adapter-error" |
composer.send、composer.attachmentAdd | 已弃用:观察 composer 的 text / attachments |
thread.runStart、thread.runEnd | 已弃用:观察 s.thread.isRunning 的状态变化 |
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 是唯一一个可通过 unstable_capabilities 强制关闭的标志,ExternalStoreRuntime 支持此操作。refetchThread 表示 aui.threads.reloadMainThread() 会原地刷新打开的线程,还是会改为重新挂载运行时;详见 runtime-concepts.md。
常见问题
“Cannot read property of 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 技能。
状态未更新
- 使用
useAuiState的选择器,而不是在渲染时读取getState();getState()只会获取一次快照,不会订阅状态变化。
多个会话或对话侧边栏
- 本技能介绍单个会话的状态和运行时钩子。若要创建、切换、归档会话,以及渲染会话列表,请改用 thread-list 技能。
相关技能
- thread-list -- 多会话 UI、会话 CRUD、
threads.selectionChanged的使用者 - tools -- 工具包编写、工具调用 UI、审批关卡和人工工具详解
- elements -- 为你读取此状态的样式化
Thread和撰写器组件 - cloud --
AssistantCloud、托管式持久化和useChatRuntime({ cloud }) - streaming -- 协议运行时底层的 DataStream 和 AssistantTransport 线缆协议