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 渲染、交互作答 流程、附件、語音、單回合模型覆蓋與會話搜索。每項能力相互獨立,可按 任意順序採用。

本頁目錄