ChatKit Adapter 与会话 API 契约

本契约确定接入方可使用的接口与行为。只发布 @volcengine/chatkit;会话业务由接入方管理,Adapter 负责消息执行协议,Core 维护单个会话的消息与执行状态,UI 负责展示。本文的能力名称与代码保持一致,不保留旧方法别名。

接入与开发导航:接入内置 adapter、实现自定义 adapter。

阅读前提:先认识这些编号

这些字段出现时都是字符串;是否必填以各接口参数表为准。内置协议处理自身定义的编号映射;独立订阅入口还必须满足下文的统一 runId 约束,自定义协议遵守同样规则。

字段代表什么由谁提供接入时怎么用
sessionId一个会话应用/后端提供打开或切换会话;每次 adapter 调用都带它
runId会话内的一次执行前端发起时由 SDK 生成;后端主动执行时由 adapter 从事件识别关联本次输出、停止和续接;后端 ID 不同时由 adapter 映射
messageId(消息的 id)一条消息adapter 根据协议生成或映射定位要更新的消息;同一执行可产生多条消息
partId(片段的 id)消息内的一段内容,如文本或工具调用adapter 根据协议生成或映射与 messageId 一起定位具体内容片段
eventId流中的一条事件/恢复位置后端或事件通道提供,adapter 读取用于事件去重与断流续接;支持 resume 时须提供可靠位置
targetRunId重新生成/编辑重跑所替代的旧执行SDK 根据重跑目标确定rerun 中它指向旧执行,runId 标识新执行
cursor历史消息的分页位置后端返回,adapter 提取下一次 history 原样传回;省略表示第一页

例如:会话 s1 中发起执行 r1,产生消息 m1;消息里的文本片段是 p1。流事件 e1、e2 可以持续追加到同一个 m1 / p1,因此一条事件不等于一条消息。再次发送会产生新的 runId,会话仍是 s1。

编号使用规则:同一消息/片段在实时输出与历史中必须使用一致的 id,才能正确合并;新消息/片段使用新 id。eventId 用于流续接,cursor 用于历史分页,二者不能混用。

按任务选择 API

需求使用哪些 API接入方需要提供什么
使用内置后端协议createXxxAdapter → useChatSessions → Chatkitrun 请求函数与外部 activeSessionId;其余能力按需提供
扩展请求与显示内容transformInput、transformOutput、runtime.updateContent请求字段转换、同步内容转换,或异步业务内容更新
接入新协议createAdapter + AdapterProtocol输入编码、事件解码;订阅与历史的转换按需提供
验证自定义协议runAdapterConformance一致性测试 harness

工厂统一返回 ChatRuntimeAdapter<对应的 ChatInput>,可直接交给会话 hook。配置类型、回调参数与返回类型是这些 API 的数据说明,无需逐个作为独立接口学习。直接实现 ChatRuntimeAdapter 或使用 ChatCoreRuntime 的进阶接入仍受支持。

1. 先确定职责

负责方提供什么
接入方应用URL、鉴权、HTTP/WS 请求;会话列表与分页;创建、改名、删除、fork、归档;路由与当前 sessionId
Adapter请求编码、事件解码、标识关联、内容转换;内置协议提供这些实现
Core每个 sessionId 一个 runtime;消息、轮次、队列、终态、历史合并与恢复
UI 会话层一个 adapter 分给多个 runtime;打开、复用、后台接收、释放与事件汇总
UI 组件默认消息展示、受控 SessionList、renderers 与 slots
后端真实执行结果、会话持久化、历史快照、取消结果与恢复游标

SDK 不维护业务会话目录,不实现 sessions.list/create/rename/remove,也不为 fork、归档、置顶增加 adapter 方法。接入方不应直接修改 Core store。

2. 导出入口与最小接入

入口接入方使用的核心符号
@volcengine/chatkitChatkit、ChatkitProvider、SessionList、useChatRuntime、useChatSessions、useComposer、useCurrentSession、useCurrentRun、useChatkitActions
@volcengine/chatkit/coreChatCoreRuntime、ChatRuntimeAdapter、createAdapter、AdapterProtocol、SessionInfo、StreamEvent、StreamResult、ContentChange
@volcengine/chatkit/adapters/openaicreateOpenAiAdapter,Chat Completions 文本与工具流、可选浏览器工具循环
@volcengine/chatkit/adapters/adkcreateAdkAdapter,ADK 事件与历史转换
@volcengine/chatkit/adapters/a2acreateA2aAdapter,A2A 消息、任务与事件转换
@volcengine/chatkit/adapters/hermescreateHermesAdapter,Hermes 会话消息与执行事件转换
@volcengine/chatkit/adapters/hermes-gatewaycreateHermesGatewayAdapter、createHermesGatewayConnection,Hermes Dashboard(tui_gateway)事件、审批与追问;自带 WebSocket 连接
@volcengine/chatkit/adapters/dshcreateDshAdapter、createDshConnection,DeepSeek Harness 会话事件、审批与追问;自带 remote.mux 连接
@volcengine/chatkit/adapters/codexcreateCodexAdapter、createCodexConnection,Codex app-server 条目流、审批与追问;自带 WebSocket JSON-RPC 连接
@volcengine/chatkit/adapters/opencodecreateOpenCodeAdapter、createOpenCodeConnection,OpenCode server 消息片段流、审批与追问;自带 REST + 全局 SSE 连接
@volcengine/chatkit/adapters/mockcreateMockAdapter,内存消息后端与脚本回放;业务目录仍由外部提供
@volcengine/chatkit/testingrunAdapterConformance;配置类型 AdapterConformanceHarness、ConformanceScenario、ConformanceTestApi
@volcengine/chatkit/presentation工具展示模型(headless):createToolRegistry、getToolPresentation、summarizeToolCall
@volcengine/chatkit/message-codec消息编解码:convertToChatkitMessages、createMergedChatkitFlowTransform 与富文本消息编解码
@volcengine/chatkit/devtools开发调试:createTurnMetrics、ChatkitTelemetryInspector,不进根入口体积

内置表示已实现协议转换,不表示已提供网络服务、鉴权或会话业务接口。

import { useMemo } from 'react';
import { Chatkit, useChatRuntime } from '@volcengine/chatkit';
import { createOpenAiAdapter } from '@volcengine/chatkit/adapters/openai';
import '@volcengine/chatkit/styles.css';

export function Conversation({ sessionId }: { sessionId: string }) {
  const adapter = useMemo(
    () =>
      createOpenAiAdapter({
        run: ({ sessionId, runId, body }) =>
          fetch('/api/chat/run', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ sessionId, runId, input: body }),
          }),
      }),
    []
  );
  const runtime = useChatRuntime({ sessionId, adapter });
  return runtime ? <Chatkit runtime={runtime} /> : null;
}

这里 /api/chat/run 必须返回所选协议的事件流。后端要求先创建会话时,应用先取得真实 sessionId,再打开 Conversation。直接链接到已有 sessionId 不依赖会话列表。

3. 创建配置与 runtime 契约

接入方把能力函数传给工厂,工厂生成 ChatRuntimeAdapter。相同业务动作同名,但两层参数不同:Core 提供统一 input,Adapter 编码成 body 后调用接入函数;接入函数返回原始响应,Adapter 解码并向 Core 提交事实。

创建配置生成的 adapter 方法区别
runrun接入函数接收协议 body;adapter 方法接收统一 input 与既有 messages
resumeresume接入函数接收后端 eventId;adapter 方法接收 Core 保存的 from 游标
steersteer接入函数接收编码后的 body;adapter 方法接收追加 input
abortabort发起远端取消;等待事件或历史确认终态
regenerate / editregenerate / edit新 runId 承接回答,targetRunId 标识被重做的轮次
historyhistory接入函数返回 Response;adapter 方法返回 HistoryResult;cursor 可选
subscribeconnectsubscribe 建立原始事件流;connect 确保会话接通并处理复用、路由与缓冲
无start(ingress)绑定 Core 事实入口,返回解绑函数;普通接入方不实现

不需要自行实现 start 或持有 ingress;只有直接实现底层 ChatRuntimeAdapter 才会涉及它们。底层 run/regenerate/edit 返回 Promise<boolean>,resume 返回 Promise<void | boolean>,steer/abort 返回 Promise<boolean>。这些返回值不替代后端终态事实。

能力函数完整约定

普通能力回调不接收 signal。SDK 在释放运行时时取消 Response 的 reader,或调用异步迭代器的 return();自定义事件流必须在 return() 中移除监听并结束 pending next(),不要关闭其他会话共享的连接。回调返回前的请求超时由应用自己的网络客户端管理。资源释放不请求后端停止。

能力函数参数返回值
run(必填)sessionId, runId, body, input?无 subscribe:StreamResult;有 subscribe:受理 Response
subscribesessionIdStreamResult;resolve 时监听和缓冲必须已经就绪
resumesessionId, runId, eventId;协议需要时含 body无 subscribe:StreamResult;有 subscribe:受理 Response
steersessionId, runId, body受理 Response
abortsessionId, runId;协议需要时含 body受理 Response
regenerate / editsessionId, runId, targetRunId, body与 run 相同
historysessionId, cursor?协议历史的 Response
type StreamEvent<Event> = { data: Event; eventId?: string };
type StreamResult<Event> = Response | AsyncIterable<StreamEvent<Event>>;

未提供的能力不挂到 adapter,UI 按有效能力显示执行操作。historyPaging 表示允许历史翻页,与独立方法无关;唯一 history 方法负责第一页与后续页,是否还有内容由 hasMore/cursor 决定。

HTTP 非 2xx 或回调抛错表示调用未成功;非流式 2xx 响应只证明 HTTP 成功,工厂不猜测应用自定义业务码。应用需在能力回调中校验业务响应并抛错。协议事件里的业务失败由对应解码器处理为 failed。受理成功不等于执行完成。流关闭不能推断 completed。OpenAI 工具循环可以在同一 runId 下多次调用 run。

核心数据模型

下表是接入方需要关心的 Core/UI 数据形状;必选为不传即形状不合法,可选列用 ? 表示可省略 / 为 undefined(不是 null)——只有字段显式标 | null 才可赋 null(如 activeRun、adapter 透传的 unknown 数据)。逐字段联合类型以 @volcengine/chatkit/core 导出的类型声明为准。

| 结构 | 必选 | 可选 | 语义与约束 | | --------------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | RuntimeSnapshot(Core 冻结发布) | sessionId、runs、messages、baseline、scheduling | 见各子对象 | runs: Record<RunId, RunView>;messages: RuntimeMessage[];baseline={ready, reconciliationRequired, cursor?, hasMore, loading};scheduling={activeRunId?, queuedRunIds[]} | | RunView(runs 的值,实时 run) | id: RunId、origin: RunOrigin、status、phase、workItems、createdAt | connection?、startedAt?、executionStartedAt?、phaseChangedAt?、finishedAt?、abort?、error?、input?、position?、interruptReason?(时间戳列非穷举,以源码为准) | connection 是对象 {status, error?},非裸枚举;abort 见下;status/phase 取值见状态机 | | HistoryRunSnapshot(HistoryResult.runSnapshots 元素) | 同 RunView 但 status 限定终态 | 同 RunView | Omit<RunView,'status'> & { status: completed/failed/interrupted };不是 RuntimeSnapshot 内嵌的历史快照 | | RuntimeMessage | id、role、parts | runId?、createdAt?、metadata? | role ∈ system/user/assistant/tool;输出归属当前 runId;先建消息才能追加 delta | | RuntimePart | id、type + 随 variant 必填 | 随 variant 扩展 | text/reasoning 必有 text;tool-call 必有 toolCallId、toolName;media 必有 mediaType;data 必有 kind、data(这些是该 variant 的必填,不在可选列) | | ProtocolUpdate | type | 随类型 | message(替换整条)/ delta(只追加文本)/ phase / completed / failed / interrupted;终态必须有协议证据 | | HistoryResult | messages、hasMore | cursor?、runSnapshots?、activeRun?、session? | messages 重建内容;runSnapshots 恢复明确终态;activeRun 单主要恢复目标(可 | null);cursor 翻页 | | RunAbortView(即 runSnapshot 的 abort) | requestedAt: number、outputSealed: boolean | acceptedAt?、remote?、remoteStatus? | requestedAt 非负有限;acceptedAt 有值须 >= requestedAt;outputSealed 表中止后输出封口,不要求 interrupted 或 acceptedAt;终态可先于停止 ACK | | RunError | code、message | retryable?、cause? | Core 在 emitFailed 准入时用 toRunError 归一,接入方不拿到 [object Object] | | ChatkitActionInput(公开 actions.send) | content | attachments?、metadata? | content 为富文本片段数组;attachments 可省,纯文本最小 { content: [{type:'text',text}] } | | ChatkitControlledSendInput(受控完整表单) | content、attachments | metadata? | attachments 必传;与公开 action 入参分型,不混称 | | ContentUpdateResult | ok | 随联合 | 成功 { ok:true, changed };失败 { ok:false, code, message }(无 changed),code ∈ runtime-disposed/wrong-session/target-not-found/ambiguous-target/output-sealed/invalid-change/stale-version/duplicate-change |

UI 通过 store.subscribe(listener) 重读冻结的 RuntimeSnapshot。RunView.connection 是对象 {status: connected/disconnected/recovering, error?},其状态独立于 run 的 status/phase。

4. subscribe 如何工作

没有 subscribe 时,run 发请求并返回这一轮的回答流。有 subscribe 时,发送与接收分离:SDK 先接通会话,再发 run,所有回答从订阅流接收;run/resume/regenerate/edit 只返回受理响应。

const adapter = createOpenAiAdapter({
  subscribe: async ({ sessionId }) => {
    const response = await fetch(`/api/chat/events?${new URLSearchParams({ sessionId })}`, {});
    if (!response.ok) throw new Error('订阅失败');
    // 后端约定:成功响应头返回时,服务端订阅已经就绪。
    return response;
  },
  run: ({ sessionId, runId, body }) =>
    fetch('/api/chat/send', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ sessionId, runId, input: body }),
    }),
});

SDK 持续读取 Response.body 并解析 SSE,或者通过 for-await 消费接入方返回的异步序列。原始事件先识别 runId,再送到协议解析器,转换成事实进入 Core,最后由 UI 订阅快照更新。

WebSocket 连接、消息监听与缓冲队列由接入方实现。subscribe 不能在服务端确认之前 resolve;释放会话只移除该会话的监听,不能关闭其他会话共用的连接。发送命令也必须等待后端受理,socket.send 返回不等于受理。

OpenAI 订阅事件的当前约定:每个事件含 run_id;本页未发起的轮次先有 type: run.started;choices 中的 finish_reason 表示本次响应结束。普通 Chat Completions chunk 的 id 不是 ChatKit runId。其他协议使用各自的身份规则;自定义协议通过 identifyEvent 提供。

订阅模式拒绝 run/resume/regenerate/edit 返回 Content-Type 为 text/event-stream 的第二条输出流;其受理响应正文释放,不解释应用业务字段。单轮结束不关闭会话订阅。停止分两步:调用 abort 请求后端取消,随后保持接收输出、显示“正在停止”。只有后端终态事件或 history 返回该轮明确终态,才结束这一轮并释放执行位。失败或未确认时不能宣称已停止;缺少 abort 时不显示停止按钮。未派发的排队消息可直接取消。

5. 会话目录和路由由外部控制

SessionInfo 是会话摘要:id 必填,title、createdAt、updatedAt、status、metadata 可选。时间统一为毫秒。业务字段可放 metadata;SDK 不解释置顶、归档、权限等业务含义。status 是 idle / running / completed / error / unknown:running 表示存在活跃 Run;completed / error 表示最近一轮已结束但用户尚未查看;idle 表示没有活跃 Run 且没有未读结果。

SessionList 接收 readonly ChatkitSessionView[],它与 SessionInfo 的摘要字段相容。组件不会修改数组或发后端请求。

SessionList prop类型与行为
sessions必填,外部会话摘要数组
activeSessionId可选,当前高亮 ID,由外部控制
onSelect必填,(id) => void 或 Promise<void>;一般更新路由
loading / hasMore可选,列表加载与分页状态,缺省 false
onCreate / onLoadMore可选,() => void 或 Promise<void>;未提供则隐藏入口
onRename可选,(id, title) => void/boolean 或相应 Promise
onRemove可选,(id) => void/boolean 或相应 Promise;false 表示拒绝
itemActions / itemBadge自定义业务操作与角标,例如 fork、收藏、归档
header / footer列表头尾内容
onError操作或渲染异常的报告回调
locale / className / style语言与布局
const [items, setItems] = useState<SessionInfo[]>([]);
const [activeSessionId, setActiveSessionId] = useState<string>();
const chat = useChatSessions({ adapter, activeSessionId });
const opened = useSyncExternalStore(chat.store.subscribe, chat.store.getSnapshot, chat.store.getSnapshot);
const visibleItems = items.map(item => ({
  ...item,
  status: opened.openedSessions.find(open => open.id === item.id)?.status ?? item.status,
}));

<SessionList sessions={visibleItems} activeSessionId={activeSessionId} onSelect={setActiveSessionId} />;
<Chatkit sessions={chat} />;

实际应用可以直接从 URL 读取 activeSessionId,onSelect 执行导航。浏览器前进后退、深链接和 fork 后跳转都走同一条路。SDK 不自动选择列表首项,不创建虚拟草稿 ID;没有 activeSessionId 时 runtime 为空。

列表筛选掉一个会话不意味着它已被删除,也不会释放其 runtime。SDK 不回写标题或目录数组;应用根据业务接口或运行事件刷新元数据。已打开会话的实时状态从 chat.store 的 openedSessions 合并进列表。后台会话正常结束或失败后分别进入 completed / error;选中后变为 idle,当前会话结束时直接保持 idle。SessionList 不展示 idle,completed 使用蓝色实心圆点,error 使用红色问号,unknown 使用黄色感叹号;状态图标悬停时显示文案。该已读状态只随已打开 runtime 保存在内存;需要刷新或跨设备保留时由应用持久化。

useChatSessions 的资源生命周期

参数:adapter 必填且引用稳定;activeSessionId 可选并持续受控;onError、eventAttributes、stallThresholdMs 与单会话 hook 相同。

返回项契约
runtime当前选中会话的 ChatkitRuntime,初始化前、未选择或已释放时为空
actions.open(id)Promise<ChatkitRuntime>;打开或复用并等待历史初始化,不改变外部选择;已释放则拒绝。历史/连接错误经 onError 报告,不保证已获取完整历史
actions.release(id)boolean;释放指定本地 runtime 和接收资源;不存在返回 false,不删除后端会话,不请求远端取消
store.getSnapshot(){ activeSessionId, openedSessions: readonly { id, status }[] };不包含目录
store.subscribe(listener)订阅资源与运行状态变化,返回退订函数
events汇总全部已打开会话的事件,各自保留 sessionId
capabilities当前 adapter 的执行能力

切换保留旧 runtime,后台继续执行;直到显式 release、adapter 替换或 hook 卸载才释放。已释放的当前 ID 不会因普通重渲染自动重新打开,可显式 open 或切换 ID。应用负责选择下一个会话以及控制缓存数量;SDK 不内置 LRU 或空闲超时。

新建:应用调用后端 create,更新目录并导航到新 ID。首发前先完成创建;如果先发再导航,用 await chat.actions.open(id) 获取新 runtime,再调用其 actions.send,避免误发到旧会话。

删除:应用调用后端 delete,成功后更新目录、chat.actions.release(id),再决定下一条路由。若需要远端停止,应由后端删除语义或显式取消操作保证。

fork:应用调用后端 fork({ sessionId, messageId }),后端复制上下文并返回新 ID;应用更新目录并导航。SDK 通过 history 读取新会话。复制前端消息不等于后端 fork。单个 session 内的消息树分支切换不在本契约范围内。

6. 扩展入口如何选择

需求入口
更换网关、鉴权、连接方式run/subscribe/history 等能力函数
默认请求增加业务字段transformInput
流内附带工具标题、业务卡片transformOutput
独立异步接口补充已有内容runtime.updateContent
改视觉和交互renderers / slots
改变消息、工具和终态的协议语义新协议 Adapter
fork、归档、分享等会话操作外部业务接口和受控目录

transformInput(input, { defaultInput, sessionId, runId, capability }) 同步返回完整协议 body,不是局部 patch。默认编码先执行;返回值交给能力函数。capability 为 run/resume/steer/regenerate/edit,但只有协议有对应请求体时才调用;例如 OpenAI 的 resume/abort 不调用它。

transformInput: (_input, { defaultInput }) => ({
  ...defaultInput,
  knowledge_base_id: 'kb-123',
});

transformOutput(event, { sessionId, runId?, eventId?, messageId?, source, defaultChanges }) 同步返回 OutputChange[] 或 undefined。undefined 保留默认内容,数组表示采用选中内容,追加时展开 defaultChanges;[] 过滤本事件的默认内容,但不屏蔽执行信号。转换抛错时保留默认内容并报告错误。

transformOutput(event, ctx) {
  if (!event.order || !ctx.messageId) return undefined;
  return [...ctx.defaultChanges, {
    type: 'set-part', messageId: ctx.messageId,
    part: { id: `order:${event.order.id}`, type: 'data', kind: 'shop.order', data: event.order },
  }];
}

以上业务字段示例需按具体事件类型校验。data 片段通过 renderers.parts['shop.order'] 渲染。defaultChanges 是默认内容凭据,只能原样保留或过滤,不能自行构造或传给 updateContent。

runtime.updateContent(change | changes) 返回 Promise<ContentUpdateResult>,用于聊天流之外的更新,例如 await 摘要接口后修改已经存在的工具标题。它不发请求、不持久化、不创建会话或任意消息。

ContentChange.type允许修改
set-part指定 messageId 上增加或替换自定义 data 片段
update-part按 messageId+partId 或 runId+toolCallId 定位,更新 title/metadata/data
update-message指定 messageId 的 metadata

目标必须已存在。不得修改默认文本、工具参数、工具结果或执行状态。正常完成后允许补充,已确认中止的输出拒绝补写。批量原子提交;失败返回 ok:false 和 code/message,成功返回 ok:true 和 changed。

sourceId/changeId/version 用于来源内去重与版本检查;提供 changeId/version 必须同时提供 sourceId。对象浅合并,嵌套对象和数组整体替换,set-part 整体替换。异步操作必须捕获原 runtime 与目标 ID,切换后不得改写当前新会话。

source 的类型包含 stream/history/recovery;当前续流仍使用 stream,历史读取使用 history,不依赖 recovery 必然出现。

7. 新协议作者实现什么

createAdapter({ protocol, ...能力函数 }) 返回统一 ChatRuntimeAdapter。无需注册到全局,也无需修改 UI。只有后端的消息与执行语义无法由内置协议解释时才编写新协议;业务卡片与额外请求字段优先使用上面的扩展入口。

protocol 方法契约
encodeInput(必填)(input, { sessionId, runId, messages }) => body,同步
decodeEvent(必填)(event, { sessionId, runId, eventId? }) => readonly ProtocolUpdate[],同步
identifyEvent(event) => { runId, sessionId?, startsRun?, endsResponse? } 或 undefined;有 subscribe 时必填
projectInput(input, { sessionId, runId }) => readonly RuntimeMessage[];可选,缺省依赖后端回显用户输入
decodeHistory(raw, { sessionId }) => HistoryResult;有 history 时必填
replayHistory(raw, { sessionId }) => Iterable<{ event, message }>;可选,把历史回放成与实时同形的事件逐条交给 transformOutput;同一条消息最后一步的 message 必须与 decodeHistory 结果相同

ProtocolUpdate 支持 message、delta、phase、completed、failed、interrupted。message 是完整消息快照;delta 为已有文本或思考片段追加文字;终态只能来自协议证据。未知但有效的业务事件可返回 [],之后仍可交给 transformOutput。

协议作者负责稳定的 messageId/partId/toolCallId、按 sessionId+runId 隔离的拼接状态、历史和实时同构,以及明确的开始/结束识别。identifyEvent 的 endsResponse 只结束本轮读取,不替 decodeEvent 生成 completed。

可运行的完整实现与在线演示见文档站的自定义适配器,使用同一公共工厂,并通过类型检查与一致性测试。

编辑重发与回复重试的统一约定

edit(编辑后重发)与 regenerate(回复后重试 / 重新生成)均覆盖目标轮次及之后的历史,保留目标轮次之前的内容。前者用新输入,后者复用原始输入;旧内容不参与新执行的上下文。能力函数必须让后端同步替换当前主线历史,确保刷新、翻页与重新打开后旧内容不再出现。后端无法覆盖历史时,不提供对应能力。请求未受理时保留旧历史,已受理后生成失败时保留新轮次及失败状态。

8. 历史、恢复与错误

HistoryResult 必含 messages 和 hasMore,可含 cursor、session、runSnapshots、activeRun。session 是摘要,不是业务目录;runSnapshots 只恢复明确提供的终态。activeRun 对象包含 runId/eventId,null 表示确认无执行,省略表示未知。会话 status:running 不能代替可靠 runId 和恢复位置。

history 取快照;subscribe 接实时;resume 补收已知轮次的缺失输出。三者不可互相替代。有订阅时先接收并缓冲,再加载历史;从匹配快照的 eventId 恢复,重放顺序和完整性由后端保证。没有游标不能承诺精确续接,不重新发送旧问题。

输入转换抛错使当前命令失败;输出转换抛错保留默认内容。协议/网络错误通过现有 onError 与运行事件报告;本地 IO 异常不伪造远端失败。取消一个会话的资源不影响其他会话。

协议身份与恢复边界

协议独立订阅的执行字段开始/本次响应结束
OpenAIrun_id,不是 chunk.idtype=run.started/choices.finish_reason
ADKinvocationIdtype=run.started/turnComplete、run.completed 或 errorCode
Hermesrun_id;session_id 可校验归属run.started/run.completed、run.failed、run.cancelled、run.interrupted、done
A2Aresult.taskId 或 result.id;contextId 校验会话status=submitted/working/final=true 或 completed/failed/canceled/rejected
自定义protocol.identifyEvent 返回 runIdstartsRun/endsResponse;终态由 decodeEvent 另行表达

这些订阅字段必须使用 SDK 的 runId。请求流模式中协议可维护本地与远端 ID 映射,但独立订阅在进入解码器前先按身份路由,因此后端或应用事件封装必须归一编号。历史必须恢复同一身份;不能依赖页面内临时映射在刷新后仍存在。OpenAI 的 finish_reason=tool_calls 仅结束一次模型响应,工具循环可在同一个 runId 内继续。

subscribe 只有在服务端监听和客户端缓冲均就绪时才能 resolve。打开时先连接并暂存实时事件,再读取历史,最后按基线释放;对 history.activeRun 的恢复由 resume 补收对应游标之后的有序输出。后端负责重放完整性,eventId 只做同轮去重,不做大小排序或全局精确一次投递保证。

会话层对已知断连执行或停止未确认执行最多尝试三次 recover,等待间隔为 500、1500、3000 ms。停止未确认时查询 history;断连且有位置与 resume 时尝试续接,否则查询 history。手动 recover 可继续查询;不会重发 run。无活跃执行的空闲订阅没有自动重连循环,需要持续保活时由应用返回的事件序列管理重连。失败的 connect 也不承诺无限重试。

本地执行位是单个,远端被动执行可以多个交错接收。UI 无参数 abort 优先 scheduling.activeRunId;没有执行位时,仅一个远端活跃目标才会停止;多个候选返回 false。指定目标用 Core abort({ runId })。已受理或请求中的重复停止返回 false;失败标记 unknown 后允许重试原目标。队列可通过 Core abort({ runId }) 或 cancelQueued 管理。

停止请求尚未返回时,abort.remote 暂缺,表示请求进行中;不能初始化为 unknown,否则会放行并发重复请求。返回 false 或抛错后才标记 unknown,保留 interrupting、输出接收和执行位,允许重试或通过 history 恢复。该结果只表示取消未确认,不是执行终态;不能回滚整轮快照,以免覆盖请求期间收到的输出或终态。

HistoryResult.activeRun 只声明一个主要恢复目标。存在多个远端执行时,需要后端确定主要目标;其余执行可继续从订阅接收,但本版没有完整多目标历史恢复和任务控制 UI。缺少 resume 能力、事件位置或协议所需身份时,续接返回 false,不推断执行已终止。activeRun=null 表示后端确认无活动执行,但不能单凭空值推断每个已知执行的终态,仍需 runSnapshots 或事件。

内置协议配置

工厂额外配置与默认值输出转换历史来源
createOpenAiAdaptersystemPrompt?;tools?;maxSteps 默认 8将助手/工具历史还原为 chunk
createAdkAdapter无额外协议必填配置回放 ADK 历史事件
createA2aAdapterprofile: 'a2a'(默认)或 'adk'由任务历史合成事件
createHermesAdapterinstructions?history.message 事件
createMockAdapterpace 默认 1;getScenarioId?;能力布尔开关默认开启本地模拟,无真实后端请求

网络协议工厂的 onError 为 (kind: string, error: unknown, extra?: Record<string, unknown>) => void;createAdapter 的前两参相同,不提供协议专有 extra。kind 是诊断类别,不能用它替代执行状态判断。Mock 只报告本地转换异常,因此保留 (error: unknown) => void。通知回调抛错被隔离,未配置时记录 console.error;不产生异步全局异常。协议明确的业务失败主要体现在运行状态,不保证每个失败事件都触发 onError。

transformInput 或编码失败使当前调用失败;transformOutput 抛错保留默认内容。createAdapter 会报告能力调用中的编码、解码或网络异常,再保留原错误向上抛;abort 的调用失败返回 false。内置协议在请求与转换层报告已识别异常,其余异步派发异常由会话层 onError 接收。UI actions.send 的 accepted 表示本地入队;派发错误异步报告;steer 表示后端受理;regenerate/edit 表示新执行入队。Core send 返回的 dispatched 是派发结果 Promise,完成时间取决于 adapter,不能作为后端执行完成通知。

实现新适配器的完整示例见写一个新的适配器,三种内容扩展的用法见扩展。

应用侧 runtime 操作

以下是 useChatRuntime 或 useChatSessions().runtime 的受控操作,区别于网络能力回调和底层 adapter 方法。这些受控入口的入参是 ChatkitControlledSendInput:{ content, attachments, metadata? }。content 按序记录文字、结构化引用和带 placement 的附件出现位置;attachments 是完整资源表。纯文本最小 { content: [{ type: 'text', text: '你好' }], attachments: [] }。内置适配器使用同一输入,网络回调的独立 input 提供规范化原始结构,方便历史持久化。存储、预览、模型读取与迁移见 多模态附件。

与之区分:公开 hook useChatkitActions() 暴露的 send / edit 用更宽松的 ChatkitActionInput——可直接传字符串,或传 { content, attachments?, metadata? }(attachments 可省,内部补 []);useChatkitActions 不暴露 steer(插话只在受控 runtime 层)。字段以上表「核心数据模型」为唯一权威。

操作返回类型准确含义
actions.send(input)Promise<{ accepted: boolean }>本地入队;派发异常由会话层 onError 异步报告
actions.abort()Promise<boolean>本次停止请求受理,或排队项本地取消成功;false 也可能表示重复请求或目标不明确
actions.steer?(input)Promise<{ accepted: boolean }>后端接受追加输入;优先当前执行位,否则只允许唯一远端目标;无目标、多目标或正在停止时不派发
actions.regenerate?(messageId?)Promise<{ accepted: boolean }>重做入队;省略 ID 选择最后一条回答
actions.edit?(messageId, input)Promise<{ accepted: boolean }>编辑后重做入队;messageId 为用户消息
actions.recover?(runId)Promise<boolean>成功查询或续接;不证明该执行已完成
actions.loadMoreHistory()Promise<boolean>历史页已应用;无能力、无更多页或过期事务时为 false

runtime.store.getSnapshot() 返回 UI 受控层的 ChatkitControlledSnapshot(形状为只读 { sessionId, messages, sending, history: { loading, hasMore } };与 Core 发布给运行时的 RuntimeSnapshot 是不同层的视图,勿混),使用 store.subscribe(listener) 监听并重读快照,返回退订函数。runtime.events 提供执行事件,不是另一条后端传输通道。runtime.updateContent 使用本文第 6 节的内容变更结构。