콘텐츠로 이동

실시간: WebSocket, SSE, 라이브 캔버스

전체 실시간 인터페이스: WS 채팅 프로토콜, 터미널, 라이브 캔버스(A2UI), 노드, SSE 이벤트/로그 스트림.

Revka 게이트웨이는 모든 인터랙티브·서버 푸시 상호작용을 두 가지 실시간 전송 방식으로 처리합니다. WebSocket은 양방향 세션에, Server-Sent Events(SSE)는 단방향 브로드캐스트 스트림에 사용됩니다. 이 페이지는 /ws/chat 에이전트 프로토콜과 오류 코드, PTY 터미널, 라이브 캔버스(A2UI) 시스템, 동적 노드 레지스트리, 대시보드 라이브 뷰에 데이터를 공급하는 두 SSE 스트림에 대한 상세 참조 문서입니다.

커스텀 클라이언트, 외부 통합, 또는 게이트웨이와 실시간으로 통신하는 엣지 디바이스를 구축하는 경우 이 문서를 참고하세요. 전송 방식 개요 및 REST와의 관계는 게이트웨이 API 개요를 참조하세요. 베어러 토큰 발급 방법은 페어링 및 인증을 참조하세요.

모든 /ws/* 엔드포인트는 페어링이 활성화된 경우 베어러 토큰을 요구합니다. 브라우저는 new WebSocket()에서 Authorization 헤더를 설정할 수 없으므로, 게이트웨이는 다음 세 가지 소스에서 우선순위 순서로 토큰을 추출합니다.

  1. Authorization: Bearer <token> 헤더 — 비브라우저 클라이언트용
  2. Sec-WebSocket-Protocol: bearer.<token> 서브프로토콜 — 브라우저 호환
  3. ?token=<token> 쿼리 파라미터 — 브라우저 호환 폴백
// Browser: pass the token as a subprotocol alongside the channel subprotocol.
const ws = new WebSocket(
"wss://host:port/ws/chat?session_id=" + sessionId,
["revka.v1", "bearer." + token],
);

Revka 에이전트와 클라이언트 간의 양방향 세션인 주요 인터랙티브 채널입니다. 세션 재개, 진행 중 스티어링, 중단, 파일 첨부, 페이지 컨텍스트 힌트, 스트리밍 출력을 지원합니다.

ws://host:port/ws/chat?session_id=<uuid>&name=My+Session&token=<bearer>

서브프로토콜: revka.v1

파라미터필수 여부설명
session_id아니오재개할 기존 세션의 UUID. 생략하면 새 세션을 생성합니다.
name아니오세션 백엔드에 저장되는 사람이 읽을 수 있는 레이블.
token아니오베어러 토큰(우선순위 참조 — 헤더 및 서브프로토콜이 우선합니다).

업그레이드 후 서버는 첫 번째 메시지로 session_start 프레임을 전송합니다. 메시지를 전송하지 않으면 5초 핸드셰이크 타임아웃 후 연결은 수신 전용 브로드캐스트 릴레이가 됩니다 — agent_event 프레임만 관찰하려는 워크플로우 뷰어에 유용합니다.

프레임필드목적
{"type":"connect", ...}session_id?, device_name?, capabilities?첫 번째 프레임의 선택적 핸드셰이크; 서버가 connected로 응답합니다.
{"type":"message","content":"..."}content (필수), page_context?, attachments?에이전트에 채팅 메시지를 전송합니다.
{"type":"steer","content":"..."}content (필수)진행 중 스티어링 메모를 삽입합니다.
{"type":"stop"}현재 진행 중인 턴을 취소합니다.
{
"type": "message",
"content": "Summarize the open PRs",
"page_context": "workflows",
"attachments": ["a1b2c3d4-...", "e5f6a7b8-..."]
}
프레임필드의미
{"type":"session_start", ...}session_id, resumed, message_count, name업그레이드 후 첫 번째 프레임.
{"type":"connected","message":"..."}messageconnect 핸드셰이크 프레임의 응답.
{"type":"chunk","content":"..."}contentLLM의 스트리밍 텍스트 델타.
{"type":"thinking","content":"..."}content확장 사고 델타(thinking이 활성화된 경우).
{"type":"tool_call","name":"...","args":{}}name, args도구 호출 알림.
{"type":"tool_result","name":"...","output":"..."}name, output도구 실행 결과.
{"type":"operator_status","phase":"...","detail":"..."}phase, detail오퍼레이터 라이프사이클 상태(예: queued, steering).
{"type":"agent_event","event":{}}event오퍼레이터로부터 릴레이된 브로드캐스트 이벤트.
{"type":"chunk_reset"}done 프레임 도착 전에 누적된 초안을 지웁니다.
{"type":"done","full_response":"..."}full_response이번 턴의 완전한 응답.
{"type":"stopped","message":"..."}messagestop 프레임으로 턴이 취소되었습니다.
{"type":"error","message":"...","code":"..."}message, code오류가 발생했습니다(아래 코드 참조).

error 프레임의 code 필드는 다음 중 하나입니다.

코드원인
INVALID_JSON프레임이 유효한 JSON이 아닙니다.
EMPTY_CONTENTmessage 프레임에 content가 없습니다.
SESSION_BUSY세션별 큐가 가득 찼습니다 — 이미 진행 중인 턴이 있습니다(직렬화 참조).
AGENT_INIT_FAILED이번 턴에서 에이전트를 초기화할 수 없었습니다.
AUTH_ERROR인증에 실패했습니다.
PROVIDER_ERROR업스트림 LLM 프로바이더가 오류를 반환했습니다.
AGENT_ERROR에이전트가 턴 도중 오류를 발생시켰습니다.
NO_ACTIVE_TURN진행 중인 턴 없이 steer가 전송되었습니다.
UNKNOWN_MESSAGE_TYPE프레임의 type을 인식할 수 없습니다.

턴 진행 중에 {"type":"steer","content":"..."} 를 전송하면 가이던스를 삽입할 수 있습니다. 해당 메모는 다음 오퍼레이터 경계에서 적용되며 즉시 반영되지 않습니다 — 도구 호출 도중 스티어를 전송하면 해당 호출이 완료된 후에 적용됩니다. 이는 의도된 동작입니다. 턴이 거의 끝날 무렵에 전송된 스티어는 done 이전에 적용되지 않을 수 있습니다.

{"type":"stop"} 을 전송하면 진행 중인 턴을 취소하며, 서버는 stopped 프레임으로 응답합니다. 진행 중인 턴이 없을 때 stop을 전송하면 stopped 프레임이 반환됩니다(메시지: ‘No active Operator turn to stop.’). 진행 중인 턴 없이 steer를 전송하면 코드 NO_ACTIVE_TURNerror가 반환됩니다.

먼저 POST /api/sessions/{session_id}/attachments(멀티파트, 파일당 25 MiB)를 통해 파일을 업로드하면 file_id가 반환됩니다. 그런 다음 message 프레임의 attachments 배열에서 해당 ID를 참조하세요. 게이트웨이는 각 ID를 에이전트가 인식하는 콘텐츠 마커로 변환합니다. 이미지는 비전 지원 프로바이더를 위해 [IMAGE:/path] 마커가 되고, 텍스트 파일은 최대 200,000자까지 인라인으로 처리되며 초과분에는 잘림 마커가 표시됩니다.

message 프레임의 선택적 page_context 필드는 대시보드 페이지에 특화된 LLM 지침으로 전환합니다. 허용되는 값은 agent_pool, agent_teams, skills, workflows, canvas입니다. 예를 들어 canvas 컨텍스트는 에이전트가 텍스트로 설명하는 대신 render_canvas 도구를 호출하도록 지시합니다. (Architect 에디터는 page_context 값 대신 content 내에 <editor-state><architect-instructions> 같은 XML 블록을 사용합니다.)

각 세션은 동시에 하나의 진행 중인 턴만 허용합니다. 동일 세션에 대한 동시 요청은 FIFO 순서로 큐에 들어가며 현재 턴이 완료되면 실행됩니다. 큐가 가득 찬 경우 서버는 코드 SESSION_BUSYerror를 반환합니다. 직렬화는 SQLite 히스토리 손상을 방지하고 세션 상태의 일관성을 유지합니다.

큐 잠금 타임아웃은 기본값 300초입니다. 웹 검색, 서브에이전트 위임 등의 긴 도구 체인은 일상적으로 30초를 초과하므로, 느린 채팅 턴을 위해서는 요청별 REST 타임아웃이 아닌 이 값을 높이세요.

변수기본값설명
REVKA_GATEWAY_SESSION_LOCK_TIMEOUT_SECS300큐에 대기 중인 요청이 세션 잠금을 기다리는 최대 시간(초).

타임아웃 시 Previous message is still being processed — please wait, or retry once it completes 메시지가 반환됩니다. 세션은 또한 dashboard_<uuid> 메모리 접두사를 사용해 세션별로 Kumiho 메모리를 격리합니다.

portable_pty 기반의 WebSocket PTY 터미널입니다. 사용자의 쉘이나 지정된 AI 코딩 CLI를 PTY 내에서 실행하고 stdio를 양방향으로 브리지합니다. 이는 대시보드의 Code 탭을 구동하며 전체 쉘 접근 권한을 제공합니다 — 권한이 있는 엔드포인트로 취급하세요.

ws://host:port/ws/terminal?token=<bearer>&tool=claude&cwd=/my/project&cols=120&rows=40&mcp_session=<id>&mcp_token=<tok>
파라미터기본값설명
token베어러 토큰.
session_id선택적 세션 레이블.
tool(사용자 $SHELL)실행할 CLI: claude, codex, opencode, gemini, agy, cursor. 생략하면 사용자 쉘을 사용합니다.
cwd작업 디렉터리(틸드 확장 및 유효성 검사 적용).
mcp_session자동 주입을 위한 MCP 데몬 세션 ID.
mcp_token자동 주입을 위한 MCP 베어러 토큰.
cols80PTY 초기 열 수.
rows24PTY 초기 행 수.
  • 클라이언트 → 서버: 텍스트 또는 바이너리 프레임은 PTY stdin으로 파이프되는 원시 키스트로크/바이트 데이터입니다. 크기 조정을 위해서는 {"type":"resize","cols":N,"rows":N}을 전송하세요.
  • 서버 → 클라이언트: 텍스트 프레임은 UTF-8로 디코딩된 PTY 출력입니다. 실행 실패 시 서버는 빨간색 ANSI 오류 프레임을 전송합니다.
const term = new Terminal(); // xterm.js
const ws = new WebSocket(url, ["bearer." + token]);
ws.binaryType = "arraybuffer";
term.onData((d) => ws.send(d)); // keystrokes → PTY
ws.onmessage = (e) => term.write(typeof e.data === "string"
? e.data : new Uint8Array(e.data)); // PTY output → screen
term.onResize(({ cols, rows }) =>
ws.send(JSON.stringify({ type: "resize", cols, rows })));

mcp_sessionmcp_token이 제공되면 게이트웨이는 CLI별 MCP 설정을 작성해 실행된 도구가 인프로세스 MCP 서버에 접근할 수 있게 합니다. 각 CLI는 고유한 메커니즘을 사용합니다.

도구작성되는 설정메커니즘
claude<tmpdir>/.mcp.json--mcp-config <path> CLI 플래그
codex<tmpdir>/.codex/config.tomlHOME 리디렉션
opencode<tmpdir>/.config/opencode/config.jsonHOME + XDG_CONFIG_HOME 리디렉션
gemini<tmpdir>/.gemini/settings.jsonHOME 리디렉션
agy<tmpdir>/.gemini/config/mcp_config.jsonHOME 리디렉션
cursor<tmpdir>/.cursor/mcp.jsonHOME 리디렉션

환경 변수 REVKA_MCP_URL, REVKA_MCP_SESSION, REVKA_MCP_TOKEN은 항상 설정됩니다. 도구 분기의 경우 CLI 설정 파일이 격리된 임시 디렉터리를 사용하도록 HOME이 리디렉션되며, WebSocket이 닫힐 때 임시 디렉터리가 삭제됩니다. 쉘은 TERM=xterm-256colorCOLORTERM=truecolor를 설정하고 사용자 환경의 안전한 하위 집합(PATH, LANG, LC_ALL 등)을 전달합니다.

라이브 캔버스는 Revka의 A2UI(에이전트-to-UI) 인터페이스입니다. 에이전트가 풍부한 HTML/SVG/마크다운 패널을 렌더링하여 연결된 모든 브라우저 탭에 실시간으로 스트리밍합니다. 에이전트(또는 모든 호출자)가 지정된 이름의 캔버스에 프레임을 푸시하면 모든 WebSocket 구독자가 즉시 수신합니다. 캔버스 상태는 인메모리이며 데몬 재시작 시 유지되지 않습니다.

default라는 기본 캔버스가 있으며, 추가적으로 임의의 수의 명명된 캔버스를 사용할 수 있습니다.

WebSocket 라이브 캔버스 (/ws/canvas/:id)

섹션 제목: “WebSocket 라이브 캔버스 (/ws/canvas/:id)”

단일 명명된 캔버스를 구독하고 새 프레임이 푸시될 때마다 수신합니다.

ws://host:port/ws/canvas/<canvas_id>?token=<bearer>

연결 시 서버는 현재 스냅샷(있는 경우)을 즉시 전송한 후 connected 프레임을 전송합니다.

프레임필드의미
{"type":"frame","canvas_id":"...","frame":{}}canvas_id, frame캔버스에 푸시된 새 콘텐츠.
{"type":"connected","canvas_id":"..."}canvas_id업그레이드 후 초기 응답.
{"type":"lagged","canvas_id":"...","missed_frames":N}canvas_id, missed_frames클라이언트가 브로드캐스트 링 버퍼에서 뒤처졌습니다.
{"type":"error","error":"Maximum canvas count reached"}error레지스트리가 최대 용량에 도달했습니다.

frame 객체는 frame_id, content_type, content, timestamp를 포함합니다. 대시보드에서 text/html 프레임은 샌드박스 처리된 iframe에서 렌더링됩니다.

REST 엔드포인트는 캔버스 상태를 관리하고 WebSocket은 업데이트를 스트리밍합니다. 모든 캔버스 엔드포인트는 이름 또는 UUID인 :id를 허용합니다.

메서드경로인증목적
GET/api/canvasBearer활성 캔버스 ID 목록 → {"canvases":["default", ...]}
GET/api/canvas/:idBearer현재 스냅샷 조회 → {"canvas_id":"...","frame":{}}
GET/api/canvas/:id/historyBearer프레임 히스토리 조회 → {"canvas_id":"...","frames":[...]}
POST/api/canvas/:id루프백: 없음; 외부: bearer프레임 푸시.
DELETE/api/canvas/:id루프백: 없음; 외부: bearer캔버스 초기화.
POST /api/canvas/default
Content-Type: application/json
{ "content_type": "html", "content": "<h1>Build status</h1><p>Green</p>" }

content_type 필드는 캔버스 콘텐츠 유형 허용 목록인 html, svg, markdown, text에 대해 유효성을 검사합니다. 임의의 eval 콘텐츠는 설계상 제외됩니다. 설정된 프레임별 최대값을 초과하는 콘텐츠는 HTTP 413을 반환합니다.

항목동작
허용되는 content_typehtml, svg, markdown, text(eval 제외)
크기 초과 콘텐츠413 거부
레지스트리가 최대 용량error 프레임 Maximum canvas count reached; 먼저 미사용 캔버스를 DELETE하세요

에이전트는 REST를 직접 호출하는 대신 두 가지 오퍼레이터 도구를 통해 라이브 캔버스를 구동합니다.

  • render_canvas — 지정된 이름의 캔버스에 새 프레임을 푸시합니다(에이전트의 POST /api/canvas/:id 경로).
  • clear_canvas — 캔버스를 초기화합니다(에이전트의 DELETE /api/canvas/:id 경로).

채팅 메시지에 page_context: "canvas"가 포함되면 에이전트는 텍스트로 출력을 설명하는 대신 render_canvas를 호출해 출력을 그리도록 지시받습니다. 임베드된 <img><a> 태그는 HMAC 서명된 워크스페이스 에셋 URL을 통해 워크스페이스 파일을 참조할 수 있으며, 상대 경로이므로 터널을 통해서도 작동합니다.

WebSocket 동적 노드 레지스트리 (/ws/nodes)

섹션 제목: “WebSocket 동적 노드 레지스트리 (/ws/nodes)”

외부 프로세스(휴대폰, 센서, IoT 디바이스, 원격 Revka 인스턴스 등)가 WebSocket을 통해 연결하고 기능을 광고합니다. 각 기능은 동적으로 사용 가능한 에이전트 도구가 되며, 게이트웨이는 호출을 올바른 노드로 디스패치합니다. 레지스트리는 순수 인메모리이며 재시작 시 초기화됩니다.

ws://host:port/ws/nodes?token=<bearer>

서브프로토콜: revka.nodes.v1

프레임필드목적
{"type":"register","node_id":"...","capabilities":[...]}node_id(1~128자), capabilities노드와 기능을 등록합니다.
{"type":"result","call_id":"...","success":true,"output":"...","error":null}call_id, success, output, error?호출 결과를 반환합니다.
프레임필드목적
{"type":"invoke","call_id":"...","capability":"...","args":{}}call_id, capability, args노드에 대한 호출 요청.
{"type":"error","message":"..."}message오류(예: 레지스트리가 최대 용량).
{
"type": "register",
"node_id": "phone-1",
"capabilities": [
{ "name": "camera.snap", "description": "Take a photo",
"parameters": { "type": "object", "properties": {} } },
{ "name": "gps.locate", "description": "Read GPS fix",
"parameters": { "type": "object", "properties": {} } }
]
}

각 기능의 parameters는 JSON Schema 객체이며 기본값은 {"type":"object","properties":{}}입니다. 기능은 노드 ID가 접두사로 붙은 도구로 에이전트에 노출됩니다. 동일한 node_id로 재등록하면 업데이트가 적용되며, WebSocket이 닫히면 노드가 등록 해제되고 해당 기능이 사라집니다.

인증: 설정의 노드별 auth_token이 우선 적용되며, 노드 토큰이 설정되지 않은 경우 표준 페어링 가드가 적용됩니다. 레지스트리 용량은 설정 가능한 max_nodes로 제한됩니다.

노드 디스커버리는 [nodes] enabled = true로 활성화됩니다. REST 인터페이스를 통해 연결된 노드를 나열하고 특정 노드의 기능을 호출할 수 있습니다.

메서드경로인증목적
GET/api/nodesBearer연결된 노드와 기능 목록 조회.
POST/api/nodes/{node_id}/invokeBearer기능 호출 — 본문: {"capability":"...","args":{}}.

invoke 엔드포인트는 호출당 30초 하드 타임아웃을 적용합니다. 중앙 게이트웨이에서 멀티 노드 에이전트 네트워크를 오케스트레이션하는 데 사용하세요. 오케스트레이션 패턴은 특수 스위트: 노드를, 엣지 디바이스 설정은 하드웨어 빠른 시작을 참조하세요.

게이트웨이의 내부 브로드캐스트 채널을 대시보드 클라이언트에 노출하는 Server-Sent Events 스트림입니다. LLM 요청, 도구 호출, 에이전트 라이프사이클, 관찰 가능성 오류를 실시간으로 제공합니다. 이는 브로드캐스트/관찰 가능성 데이터이며 세션별 대화가 아닙니다. 대화에는 /ws/chat을 사용하세요.

GET /api/events
Authorization: Bearer <token>
Accept: text/event-stream

페어링이 활성화된 경우 인증이 필요합니다. 브로드캐스트 채널의 용량은 4096개 이벤트이며, Axum의 기본 킵얼라이브 주석 프레임이 연결을 유지합니다.

type필드발생 시점
llm_requestprovider, model, timestampLLM API 호출이 시작될 때.
tool_call_starttool, timestamp도구 호출이 시작될 때.
tool_calltool, duration_ms, success, timestamp도구 호출이 완료될 때.
agent_startprovider, model, timestamp에이전트 턴이 시작될 때.
agent_endprovider, model, duration_ms, tokens_used, cost_usd, timestamp에이전트 턴이 종료될 때.
errorcomponent, message, timestamp관찰 가능성 오류가 발생할 때.
channel_eventpayload오퍼레이터 채널 이벤트(/ws/chat 클라이언트에게 agent_event로도 릴레이됨).

channel_event 페이로드에는 승인 요청(human_approval_request) 및 기타 오퍼레이터 알림이 포함되며, 대시보드는 이를 페이지 전체 승인 토스트로 표시합니다.

const es = new EventSource("/api/events"); // browser adds no Authorization header
es.onmessage = (e) => {
const evt = JSON.parse(e.data);
if (evt.type === "agent_end") console.log("cost:", evt.cost_usd);
};

데몬 로그 SSE 스트림 (/api/daemon/logs)

섹션 제목: “데몬 로그 SSE 스트림 (/api/daemon/logs)”

데몬 stderr 로그를 테일링하고 새 줄을 SSE 이벤트로 스트리밍합니다. 연결 시 마지막 약 64 KB의 초기 버스트를 전송한 후 새로 추가된 바이트를 500ms 간격으로 폴링합니다. 초기 버스트를 넘어선 이전 로그는 재생되지 않습니다.

GET /api/daemon/logs
Authorization: Bearer <token>
Accept: text/event-stream

로그 파일은 ~/.revka/logs/daemon.stderr.log에 위치합니다(설정 파일의 상위 디렉터리 기준으로 경로를 해석합니다). 테일러는 로테이션과 잘림을 처리합니다. 파일 크기가 줄어들면 처음부터 다시 읽습니다.

이벤트 페이로드의미
{"type":"log","line":"...","timestamp":"..."}새 로그 줄.
{"type":"log_unavailable","line":"daemon log not readable at ...","timestamp":"..."}게이트웨이가 로그 파일을 읽을 수 없습니다.

대시보드 Logs 페이지는 이 스트림을 일시 중지/재개 및 유형별 필터링 기능이 있는 최근 500개 항목 롤링 버퍼로 표시합니다. 로그, 감사, doctor, 페어링 및 스킨 페이지를 참조하세요.

SSE vs. WebSocket — 전송 방식 선택

섹션 제목: “SSE vs. WebSocket — 전송 방식 선택”
  1. 페어링 한 번 수행하여 베어러 토큰 획득(페어링 및 인증 참조).

  2. SSE를 통해 /api/events를 구독하여 게이트웨이 전체의 라이브 관찰 가능성, 비용, 승인 이벤트를 수신합니다.

  3. revka.v1bearer.<token> 서브프로토콜로 /ws/chat을 엽니다. message 프레임을 전송하고 chunk 프레임을 초안으로 스트리밍한 후 chunk_reset 시 폐기하고 done.full_response를 렌더링합니다.

  4. 필요에 따라 steer / stop 프레임으로 진행 중인 턴을 스티어링 또는 중단합니다.

  5. 에이전트가 A2UI 출력을 렌더링하는 경우 /ws/canvas/default로 캔버스를 확인하거나, 코딩 세션을 위해 /ws/terminal을 엽니다.