MINARA
参考协议

Chat 协议

在 Minara 网关上构建第三方聊天客户端的完整契约,覆盖回合循环、流式事件、历史重建、交互问答、附件与模型选择

本页是 Minara chat 协议的接入白皮书。它描述一个客户端用自己的 UI 在网关上跑完整对话所需的一切:社区自建的 Web 终端、移动 App、消息渠道 桥接,或一段自动化脚本。内置 Web UI 使用的正是这套契约,没有任何私有 端点。

一个合规客户端只需要接触四个面:

接口面作用
POST /v1/chat/stream发起一次 agent 回合
GET /v1/stream(WebSocket)chat 频道上投递该回合的事件
GET /v1/sessions*列出、读取、搜索已持久化的历史
POST /v1/interactions/:id/answer回答 agent 的反问

两个 npm 包封装了这套契约,TypeScript 客户端不必手写任何管道代码:

  • @minara/types 承载 wire 类型(ChatStreamEventChatAttachment、问题载荷)。
  • @minara/gateway-client 承载带类型的 HTTP 客户端、多路复用 WebSocket 客户端,以及下文描述的 回合模型 SDK(reduceAgentEventsessionRowsToTurns)。

两个包都是纯 ESM,零 UI 依赖。非 TypeScript 客户端直接实现同一套 JSON 契约即可;OpenAPI 规格 覆盖了全部端点。

鉴权

网关设置了 GATEWAY_AUTH_TOKEN 时,HTTP 请求携带 Authorization: Bearer <token>,WebSocket 升级请求携带 ?token=<token>(浏览器 WebSocket 无法设置请求头)。未设置该环境 变量时,网关接受匿名的本地请求。

回合循环

一次对话交换分三步:

  1. POST /v1/chat/stream 提交用户消息。网关立即返回 { session_id, kind, is_new },回合在后台运行。
  2. 以返回的 session_id 为 key,订阅多路复用 WebSocket 上的 chat 频道。回合事件按序到达,带每频道递增的序列号;断线重连从最后见到的 seq 续传。帧格式、订阅握手、回放与控制帧在 流协议页面 中规定。
  3. 把每个事件折叠到进行中的 assistant 回合上,直到 doneerror 到达。

不必担心 POST 返回与 WebSocket 订阅之间的时间差:网关为每个会话缓冲 在途回合的全部事件,fromSeq: 0subscribe 会从头回放缓冲区。 POST 返回后再订阅永远是安全的。

请求字段

message 是唯一必填字段。完整请求体:

字段用途
message用户文本(或语音转写文本)
session_id继续既有会话;省略则新建会话
session_kind新会话归属的界面桶(默认 chat,还有 institutionmarketsworkflowstrategy-studiodata-studiomessaging
attachments已上传文件的引用,见附件与语音
voice_input_key原始麦克风录音的 FileStore key
model本回合指定运行的模型 id,见模型选择
reasoning_effort本回合的思考档位(minimal / low / medium / high
retrytrue 替换会话最近一个回合,避免堆叠重复
surfaceweb(默认)或 clicli 会从系统提示里去掉自定义 URI wire 协议
prompt_modefull(默认)或 minimal,无头调用方可选精简提示骨架

完整参数语义见端点参考

流式事件

每个事件都是 { type, data }。权威的联合类型是 packages/types/src/chat-stream.ts 中的 ChatStreamEvent,SDK 以 AgentEvent 名字再导出。按关注点分组:

分组事件客户端义务
握手start检查 protocol_version(见版本化);读取 supported_blocks
会话身份sessionsession_title绑定会话 id,更新标题
回合文本text_deltareasoning_deltaresponse追加文本;response 是兜底,仅在没有任何 text_delta 时才有意义
工具调用tool_call_starttool_call_result渲染调用卡片;调用可与文本交错
子工具进度sub_tool_callsub_tool_text_deltasub_tool_thinking_deltatool_progress可选;长耗时工具在此流出内部进度
富 UIui_block渲染或忽略;见 UI Block 协议
交互pending_question_addedpending_question_resolved交互问答
中途引导user_interjectioncontext_compacted插入注入的用户气泡;展示压缩提示
进度phaseskill_activatedtodo_snapshotgoal可选的状态展示
终结doneerror恰好到达一个;把回合翻到最终状态

只追加 text_delta、在 done / error 上收尾的客户端就已经合规。 其余分组都是体验增强,缺了不影响正确性。未知事件类型必须跳过,绝不能 当作错误,因为联合类型会随时间增长。

done 携带两个值得处理的可选字段:用户通过 POST /v1/chat/interrupt 停止回合时的 interrupted: true,以及来不及注入的引导消息列表 pending_interjections: string[](把它们当普通消息重发即可)。

SDK reducer

@minara/gateway-client 导出的正是内置 Web UI 运行的那个 reducer。 它把事件逐个折叠到 AssistantTurn 上(文本、工具卡片、UI block 的 有序 segment,加上工具调用状态),副作用通过可选回调路由:

import {
  createMultiplexClient,
  createAgentStreamState,
  reduceAgentEvent,
  type AssistantTurn,
} from "@minara/gateway-client";

let turn: AssistantTurn = {
  id: "t1", toolCalls: [], segments: [], skills: null,
  todos: [], status: "streaming",
};
const state = createAgentStreamState();

const ws = createMultiplexClient({ baseUrl, apiKey });
const sub = ws.subscribe("chat", sessionId, {
  onEvent: (ev) =>
    reduceAgentEvent("t1", ev, {
      sessionKind: "chat",
      patchAssistant: (fn) => { turn = fn(turn); render(turn); },
      onPendingQuestionAdded: (q) => showQuestionPanel(q),
      onDone: () => sub.unsubscribe(),
    }, state),
});

使用这个 reducer 可以保证你的回合状态与 Web UI 逐字节一致,包括回放 时抑制重复 tool start、segment 交错排序等边界情形。

历史

任何刷新或断连缺口之后,已持久化会话是唯一事实源:

  • GET /v1/sessions?kind=&origin=&limit=&offset= 列出会话,按最近 更新排序。
  • GET /v1/sessions/:id 以消息行返回完整转录。assistant 行携带 metadata blob,内含流当时产出的有序 segment 与工具调用记录, 重建后渲染出的内容与用户当时看到的完全一致。响应还携带 is_streaming(重连到在途 WebSocket 缓冲)与 pending_questions (重新渲染未答问题)。
  • GET /v1/sessions/search?q= 跨会话对消息内容做全文搜索,按匹配度 返回摘录片段。

SDK 的 sessionRowsToTurns(detail.messages) 把消息行转换成与实时 reducer 相同的 Turn[] 形状,一条渲染路径同时服务实时流与历史:

import { sessionRowsToTurns, type SessionDetail } from "@minara/gateway-client";

const detail: SessionDetail = await fetch(`${baseUrl}/v1/sessions/${id}`)
  .then((r) => r.json());
const turns = sessionRowsToTurns(detail.messages);
if (detail.is_streaming) {
  // 重连:以 fromSeq 0 订阅 chat 频道,继续折叠事件
}

交互:agent 的反问

agent 在回合中需要输入时(澄清问题、在选项间做决定、资金动作前的 确认),它发出 pending_question_added,并继续处理其他能做的事。 载荷携带问题 id、请求类型(selectconfirminputsecret)、带类型化选项的问题列表,以及 asked_by 来源。

客户端渲染提示后通过 HTTP 作答,answers[] 每个条目对应一个问题, 以 0 起始的 questionIndex 定位:

POST /v1/interactions/:id/answer
{ "answers": [ { "questionIndex": 0, "selected_labels": ["Confirm"] } ] }

SDK 以 answerInteraction 封装了该端点:

import { createGatewayClient } from "@minara/gateway-client";

const gateway = createGatewayClient({ baseUrl, apiKey });
const outcome = await gateway.answerInteraction(q.id, [
  { questionIndex: 0, selected_labels: ["Confirm"] },
]);
if (outcome === "not_pending") closeWidget(); // 已被他处作答或已超时

"not_pending"(wire 上是 HTTP 404)是多个客户端同时打开同一问题时 的预期结果,不是错误。

selectconfirm 的答案把所选项的 label 文本放进 selected_labels;自由文本与 inputfree_textsecret 请求 永远不通过此端点作答:客户端把值写入载荷中 writeEndpoint 指向的 凭据路由,只回答结果。完整语义见 端点参考

有效作答后,网关向所有订阅者发出 pending_question_resolved,多个 同时打开的客户端无需轮询即可收敛。刷新后,未答问题通过 GET /v1/sessions/:idpending_questions 重新拿到。

资金动作的确认走同一机制,kind: "confirm"。无法渲染确认 UI 的 客户端不得以程序方式作答。未作答的确认会超时,动作被取消。这是安全 的默认行为。

附件与语音

文件输入分两步。先上传字节:

POST /v1/files            (multipart/form-data, field name "file")
→ { "key": "chat/files/2026/07/chart.png", "url": "/v1/files/…",
    "mediaType": "image/png", "size": 48213, "uploaded_at": "2026-07-17T…" }

再在回合请求里引用返回的 key:

{
  "message": "这张图说明了什么?",
  "attachments": [
    { "key": "chat/files/2026/07/chart.png", "filename": "chart.png",
      "media_type": "image/png", "size": 48213, "kind": "image" }
  ]
}

kindimagepdfspreadsheettextoffice 之一。 限制:每回合最多 8 个附件,图片单个 5 MiB,其他文件单个 20 MiB。 GET /v1/files/:key 取回字节,历史渲染图片附件也是走这条路。

语音输入复用同一存储:POST /v1/voice/transcribe?persist 转写录音, 同时返回转写文本与 FileStore key。转写文本作为 message 发送,key 作为 voice_input_key,历史即可回放原始音频。

模型选择

GET /v1/llm/available-models 返回当前 provider 连接所服务的模型。 回合请求可通过 model 钉住其中任意一个,并用 reasoning_effort 调整思考预算。覆盖只作用于该回合:它从不改动部署级默认 (PUT /v1/llm/default-model),同一网关的并发客户端各自保持独立 选择。非法取值在回合开始前就以 400 失败。

版本化与兼容性

协议以加法方式演进。新事件类型与新的可选字段会随时间出现;合规客户端 跳过未知事件类型、忽略未知字段。在同一协议版本内,既有事件和字段的 含义永不改变。

start 事件显式宣告版本:

{ "type": "start", "data": { "session_id": "chat_...", "protocol_version": 1 } }

protocol_version 只在既有事件或字段发生破坏性变更时递增;加法式变更 永不递增。没有该字段的 start 事件视为版本 1。只有当宣告的版本大于 客户端构建时依据的版本才拒绝该流。小于等于的版本总是可以安全消费。

npm 包遵循同一纪律:wire 层的破坏性变更会提升 @minara/types@minara/gateway-client 的主版本号。TypeScript 客户端钉住主版本、 自由升级次版本即可;SDK reducer 吸收加法式变更,客户端代码无需改动。

合规清单

最小合规客户端:

  1. 发送 POST /v1/chat/stream,绑定返回的 session_id
  2. /v1/stream 上订阅 chat:<session_id>,渲染 text_delta, 在 done / error 上收尾。跳过未知事件类型。
  3. 载入时从 GET /v1/sessions/:id 重建历史。

完整功能客户端在此之上叠加工具调用卡片、ui_block 渲染、交互作答 流程、附件、语音、单回合模型覆盖与会话搜索。每项能力相互独立,可按 任意顺序采用。

本页目录