MINARA
참조프로토콜

채팅 프로토콜

Minara 게이트웨이 위에서 서드파티 채팅 클라이언트를 구축하기 위한 완전한 계약 사항을 다룹니다. 턴 루프, 스트리밍 이벤트, 히스토리 재구성, 인터랙션, 첨부 파일, 모델 선택까지 모두 포함합니다.

이 페이지는 Minara 채팅 프로토콜의 통합 백서입니다. 커뮤니티 웹 터미널, 모바일 앱, 메시징 채널 브리지, 또는 자동화 스크립트 등 자체 UI로 게이트웨이와 완전한 대화를 실행하는 클라이언트가 알아야 할 모든 내용을 설명합니다. 기본 제공 웹 UI도 비공개 엔드포인트 없이 정확히 이 계약을 따릅니다.

규격을 준수하는 클라이언트는 네 가지 인터페이스를 사용합니다.

인터페이스역할
POST /v1/chat/stream에이전트 턴을 시작합니다
GET /v1/stream (WebSocket)chat 채널로 턴의 이벤트를 전달합니다
GET /v1/sessions*저장된 히스토리를 목록 조회, 읽기, 검색합니다
POST /v1/interactions/:id/answer에이전트가 역으로 묻는 질문에 답변합니다

TypeScript 클라이언트가 플러밍 코드를 직접 작성하지 않아도 되도록, 두 가지 npm 패키지가 이 계약을 감쌉니다.

  • @minara/types는 와이어 타입(ChatStreamEvent, ChatAttachment, 질문 페이로드)을 제공합니다.
  • @minara/gateway-client는 타입이 지정된 HTTP 클라이언트, 멀티플렉스 WebSocket 클라이언트, 그리고 아래에서 설명하는 턴 모델 SDK(reduceAgentEvent, sessionRowsToTurns)를 제공합니다.

두 패키지 모두 UI 의존성이 없는 순수 ESM입니다. 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를 키로 하여 멀티플렉스 WebSocket의 chat 채널을 구독합니다. 턴의 이벤트는 채널별 시퀀스 번호와 함께 순서대로 도착합니다. 재연결 시 마지막으로 수신한 seq부터 재개됩니다. 프레임 형식, 구독 핸드셰이크, 재생, 제어 프레임의 상세 내용은 스트림 프로토콜 페이지에서 확인하십시오.
  3. done 또는 error가 도착할 때까지 각 이벤트를 진행 중인 어시스턴트 턴에 적용합니다.

이론상 응답은 WebSocket 구독보다 먼저 도착할 수 있습니다. 그러나 게이트웨이는 세션별로 진행 중인 턴의 모든 이벤트를 버퍼에 저장하며, fromSeq: 0으로 subscribe하면 버퍼의 처음부터 재생합니다. 따라서 POST 응답 수신 후 구독해도 항상 안전합니다.

요청 필드

message가 유일한 필수 필드입니다. 전체 요청 본문은 다음과 같습니다.

필드설명
message사용자의 텍스트 또는 음성 전사 내용
session_id기존 대화를 이어가려면 지정합니다. 새 대화를 시작하려면 생략합니다.
session_kind새 세션의 표면 버킷입니다 (chat 기본값, institution, markets, workflow, strategy-studio, data-studio, messaging)
attachments업로드된 파일 참조입니다. 첨부 파일 항목을 참조하십시오.
voice_input_key원본 마이크 녹음의 FileStore 키
model특정 모델 ID로 이 턴을 실행합니다. 모델 선택 항목을 참조하십시오.
reasoning_effort이 턴의 thinking 등급입니다 (minimal / low / medium / high)
retrytrue로 설정하면 중복 턴을 추가하는 대신 세션의 최신 턴을 교체합니다.
surfaceweb(기본값) 또는 cli입니다. cli는 시스템 프롬프트에서 커스텀 URI 와이어 프로토콜을 제외합니다.
prompt_modefull(기본값) 또는 minimal입니다. 간결한 프롬프트 골격을 원하는 헤드리스 호출자를 위한 옵션입니다.

파라미터의 전체 의미는 엔드포인트 참조에서 확인할 수 있습니다.

스트림 이벤트

모든 이벤트는 { type, data } 형태입니다. 권위 있는 유니언 타입은 packages/types/src/chat-stream.tsChatStreamEvent이며, SDK에서 AgentEvent로 재내보내기됩니다. 용도별로 분류하면 다음과 같습니다.

그룹이벤트클라이언트 의무
핸드셰이크startprotocol_version을 확인합니다(버전 관리 참조). supported_blocks를 읽습니다.
세션 식별session, session_title세션 ID 바인딩, 제목 업데이트
턴 텍스트text_delta, reasoning_delta, response텍스트 추가. responsetext_delta가 하나도 도착하지 않은 경우의 폴백입니다.
도구 호출tool_call_start, tool_call_result호출 카드 렌더링. 호출은 텍스트와 교차될 수 있습니다.
서브 도구 진행sub_tool_call, sub_tool_text_delta, sub_tool_thinking_delta, tool_progress선택 사항. 장시간 실행 도구는 내부 상태를 여기서 스트리밍합니다.
리치 UIui_block렌더링하거나 무시합니다. UI Block 프로토콜을 참조하십시오.
인터랙션pending_question_added, pending_question_resolved인터랙션 항목을 참조하십시오.
스티어링user_interjection, context_compacted삽입된 사용자 말풍선을 표시하고 컨텍스트 압축 힌트를 보여줍니다.
진행 상태phase, skill_activated, todo_snapshot, goal선택적 상태 표시 요소입니다.
종료done, error정확히 하나가 도착합니다. 턴을 최종 상태로 전환합니다.

text_delta만 추가하고 done / error에서 중단하는 클라이언트도 규격을 준수합니다. 나머지 그룹은 경험을 향상시키지만 필수 요소는 아닙니다. 유니언은 시간이 지나면서 확장되므로, 알 수 없는 이벤트 타입은 오류로 처리하지 말고 반드시 무시해야 합니다.

done에는 두 가지 선택적 필드가 포함되며 처리하는 것을 권장합니다. interrupted: true는 사용자가 POST /v1/chat/interrupt로 턴을 중단했을 때 포함되며, pending_interjections: string[]은 너무 늦게 도착하여 삽입되지 못한 스티어링 메시지 목록입니다. 해당 메시지는 일반 메시지로 다시 전송하십시오.

SDK 리듀서

@minara/gateway-client는 기본 제공 웹 UI가 실제로 사용하는 리듀서를 그대로 내보냅니다. 이 리듀서는 이벤트 하나씩을 AssistantTurn(텍스트, 도구 카드, UI 블록의 순서 있는 세그먼트와 도구 호출 상태)에 적용하고, 선택적 콜백을 통해 사이드 이펙트를 처리합니다.

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),
});

리듀서를 사용하면 재생 시 중복 억제된 도구 시작 처리, 교차된 세그먼트 순서 등의 엣지 케이스를 포함하여 턴 상태가 웹 UI와 바이트 단위로 일치함을 보장합니다.

히스토리

저장된 세션은 리로드 또는 재연결 공백 이후 신뢰할 수 있는 데이터 소스입니다.

  • GET /v1/sessions?kind=&origin=&limit=&offset=은 최근 업데이트 순으로 세션 목록을 반환합니다.
  • GET /v1/sessions/:id는 메시지 행으로 구성된 전체 전사 내용을 반환합니다. 어시스턴트 행에는 스트림이 생성한 순서 있는 세그먼트와 도구 호출 기록을 담은 metadata 블롭이 포함되어 있어, 사용자가 실시간으로 본 내용과 동일하게 재구성할 수 있습니다. 응답에는 is_streaming(라이브 WebSocket 버퍼에 재연결 필요 여부)과 pending_questions(열려 있는 질문 재렌더링)도 포함됩니다.
  • GET /v1/sessions/search?q=는 세션 전체의 메시지 내용에 대해 전문 검색을 실행하고 가장 일치하는 스니펫을 반환합니다.

SDK의 sessionRowsToTurns(detail.messages)는 행을 라이브 리듀서가 구성하는 것과 동일한 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 채널을 구독하고 리듀싱을 계속합니다
}

인터랙션: 에이전트가 역으로 묻는 경우

에이전트가 턴 도중 입력이 필요할 때(명확화 질문, 옵션 간 결정, 또는 자금 이동 전 확인) pending_question_added를 내보내고 나머지 작업을 계속 진행합니다. 페이로드에는 질문 ID, 요청 종류(select, confirm, input, secret), 타입이 지정된 옵션이 포함된 질문 목록, 그리고 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"(와이어 상 HTTP 404)은 여러 클라이언트가 같은 질문을 열어 둔 경우에 예상되는 결과이며, 오류가 아닙니다.

selectconfirm 답변은 선택한 옵션의 label 텍스트를 selected_labels에 넣고, 자유 형식 및 input 답변은 free_text를 사용합니다. secret 요청은 이 엔드포인트를 통해 답변하지 않습니다. 클라이언트가 값을 페이로드의 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…" }

그런 다음 반환된 키를 턴 요청에서 참조합니다.

{
  "message": "what does this chart show?",
  "attachments": [
    { "key": "chat/files/2026/07/chart.png", "filename": "chart.png",
      "media_type": "image/png", "size": 48213, "kind": "image" }
  ]
}

kindimage, pdf, spreadsheet, text, office 중 하나입니다. 제한 사항: 턴당 첨부 파일 8개, 이미지당 5 MiB, 기타 파일은 20 MiB입니다. GET /v1/files/:key로 파일 바이트를 다시 받을 수 있으며, 히스토리에서 이미지 첨부 파일을 렌더링할 때도 이 방법을 사용합니다.

음성 입력도 동일한 저장소를 사용합니다. POST /v1/voice/transcribe?persist는 녹음을 전사하고 전사 내용과 FileStore 키를 모두 반환합니다. 전사 내용을 message로, 키를 voice_input_key로 전송하면 히스토리에서 원본 오디오를 재생할 수 있습니다.

모델 선택

GET /v1/llm/available-models는 활성 공급자 연결에서 제공하는 모델 목록을 반환합니다. 턴 요청에서 model로 모델을 고정하고, reasoning_effort로 thinking 예산을 조정할 수 있습니다. 이 설정은 해당 턴에만 적용됩니다. 배포 기본값(PUT /v1/llm/default-model)은 변경되지 않으므로 동일한 게이트웨이의 여러 클라이언트가 독립적인 선택을 유지합니다. 유효하지 않은 값은 턴이 시작되기 전 400 오류로 실패합니다.

버전 관리와 호환성

프로토콜은 추가 방식으로 진화합니다. 새로운 이벤트 타입과 새로운 선택적 필드가 계속 추가됩니다. 규격을 준수하는 클라이언트는 알 수 없는 이벤트 타입을 건너뛰고 알 수 없는 필드를 무시합니다. 같은 프로토콜 버전 안에서 기존 이벤트와 필드의 의미는 변하지 않습니다.

start 이벤트가 버전을 명시적으로 선언합니다.

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

protocol_version은 기존 이벤트나 필드에 파괴적 변경이 있을 때만 증가하며, 추가적 변경으로는 증가하지 않습니다. 이 필드가 없는 start 이벤트는 버전 1로 간주합니다. 선언된 버전이 클라이언트 빌드 시점의 대응 버전보다 클 때만 스트림을 거부하십시오. 같거나 낮은 버전은 항상 안전하게 소비할 수 있습니다.

npm 패키지도 같은 규율을 따릅니다. 와이어 계층의 파괴적 변경은 @minara/types@minara/gateway-client의 메이저 버전을 올립니다. TypeScript 클라이언트는 메이저 버전을 고정하고 마이너 버전을 자유롭게 업그레이드하면 됩니다. 추가적 변경은 SDK 리듀서가 흡수하므로 클라이언트 코드를 변경할 필요가 없습니다.

규격 준수 체크리스트

최소 규격 준수 클라이언트의 조건은 다음과 같습니다.

  1. POST /v1/chat/stream을 전송하고 반환된 session_id를 바인딩합니다.
  2. /v1/stream에서 chat:<session_id>를 구독하고, text_delta를 렌더링하며, done / error에서 종료합니다. 알 수 없는 이벤트 타입은 무시합니다.
  3. 로드 시 GET /v1/sessions/:id에서 히스토리를 재구성합니다.

완전한 기능을 갖춘 클라이언트는 도구 호출 카드, ui_block 렌더링, 인터랙션 답변 플로, 첨부 파일, 음성, 턴별 모델 오버라이드, 세션 검색을 추가합니다. 각 기능은 독립적이므로 원하는 순서로 도입할 수 있습니다.

목차