본문으로 건너뛰기

daiops SSE 스트리밍 챗 API (AGENT-API-1)

외부 서비스가 daiops 직원(워크스페이스)과 실시간 스트리밍으로 대화하는 공개 API. 메시지를 보내면 토큰·도구 호출·결재 요청·완료 이벤트를 표준 Server-Sent Events로 흘려보내고, 연결이 끊기면 표준 Last-Event-ID로 재접속해 이어받는다.

OpenAPI 스펙: GET /api/v1/openapi (무인증). 인증·rate limit·에러 응답은 그 문서를 단일 소스로 참조.

인증

Authorization: Bearer <키> — API 키(dai_live_…, 설정 > 개발자 도구에서 발급) 또는 레거시 workspace webhook_token.

엔드포인트스코프
POST /api/v1/workspaces/:id/messageschat
GET /api/v1/workspaces/:id/sessionssessions:read
GET /api/v1/workspaces/:id/sessions/:sessionId/messagessessions:read
GET /api/v1/workspaces/:id/sessions/:sessionId/eventssessions:read
POST /api/v1/workspaces/:id/files (업로드 티켓)files:write
POST /api/v1/workspaces/:id/files/downloadtools:read
POST /api/workspaces/:id/approvals/:approvalId/decidechat

1. 메시지 전송 + 스트리밍 (기본 경로)

POST /api/v1/workspaces/:id/messages
Authorization: Bearer dai_live_…
Content-Type: application/json

{ "command": "...", "caller_id": "optional", "session_id": "optional-uuid", "response_schema": { "type": "object", "...": "..." } }
  • caller_id: 지정 시 session_key=api:{caller_id}로 세션을 재사용해 이전 대화 맥락을 잇는다. 미지정 시 매 호출 새 세션.
  • session_id: 명시적 이어쓰기(소유권 검증). caller_id보다 우선.
  • response_schema: 구조화 출력(JSON Schema). 지정 시 도구를 자유롭게 쓴 뒤 최종 응답만 이 스키마로 강제·검증하고, 검증된 결과를 structured 이벤트로 노출한다(원시 JSON은 text로 흘리지 않음). object 루트 권장.

응답은 Content-Type: text/event-stream. 첫 프레임은 open(세션 ID 통지), 이후 이벤트가 흐르고 마지막에 done으로 종료한다.

2. 이벤트 스키마 (schemaVersion)

모든 data:는 JSON이며 스키마 버전 v를 포함한다. 현재 v=1. 이벤트 종류 추가는 하위호환(minor), 제거/의미 변경은 breaking.

id:는 agent-runner EventBuffer의 순번(seq, 단조 증가)이며 재접속 커서다.

event:data설명
open{ session_id, from_seq? }스트림 확립. 재접속 대상 세션 ID.
text{ v, text }토큰 단위 응답 텍스트(누적 아님, 델타).
tool_use{ v, tool, summary, tool_use_id? }도구 호출 시작.
tool_result{ v, tool, ok, output, truncated?, tool_use_id? }도구 결과(output 최대 4000자, 초과 시 truncated:true).
input_required{ v, kind, ... }사용자 입력 필요(아래 3). A2A input-required 시맨틱.
usage{ v, input_tokens, output_tokens, cost_usd, model }토큰/비용(소유자 스코프).
structured{ v, result }구조화 출력(response_schema 지정 시에만). 스키마 검증 통과한 최종 payload. text=사람이 읽을 서술, structured=기계용 검증 결과로 분리(원시 JSON은 text로 나오지 않음). 내부 강제 도구(submit_structured_response)의 tool_use/tool_result도 노출되지 않는다.
attachment{ v, attachments:[{ filename, mime, size, download_url }] }에이전트가 만든 산출물 파일. done 직전 1회(파일이 있을 때만). download_url은 7일 signed URL — 그대로 GET하면 내려받는다. 이것이 파일 발견의 정본이다(텍스트 스트림의 내부 마커를 파싱하지 말 것).
error{ v, code, message, recoverable }오류. code=session_gone이면 재개 불가(새로 시작).
done{ v, session_id, resumed? }스트림 종료. 항상 마지막 1회.

인프라 단계(thinking/reconnect/retry/stall/도구 tail 등)는 노출하지 않는다(노이즈 방지).

2.1 블로킹 응답 (stream:false)

stream:false(콜백 없음)면 SSE 대신 실행 결과를 동기 JSON(200)으로 반환한다. 필드:

필드설명
success실행 성공 여부(불리언).
output최종 답변 텍스트(실패 시 null).
session_id이어쓰기용 세션 ID.
structured_resultresponse_schema 지정 시에만. 스키마 준수 JSON.
references답변이 참조한 지식 문서 목록(참조가 있을 때만). 각 항목 { path, source }path=문서 경로(wiki_read=위키 페이지 경로, read/grep=knowledge 상대 경로), source=wiki_read|read|grep. 에이전트가 문서를 실제로 읽은 시점에 포착되며 dedup·등장 순서를 보존한다. 답변 수치를 원문(등재 문서 덤프)과 대조하는 검증에 쓴다.
attachments산출물 파일(있을 때만, 7일 signed download_url).
rejected_attachments처리 실패한 인바운드 첨부(있을 때만).
error오류 사유(timed_out 등) 또는 null.

전체 스키마는 OpenAPI(GET /api/v1/openapi)가 단일 소스다. references는 현재 블로킹 응답 전용이다(SSE 스트림에는 아직 노출되지 않음).

3. 결재/시크릿 (input_required)

kind=approval — 도구 실행에 결재가 필요할 때. data: { kind:'approval', plan, operations, approval_id, decide_url, reason? }.

decide_url로 결정을 회신하면 스트림이 이어진다(같은 세션 resume):

POST {decide_url}          # = /api/workspaces/:id/approvals/:approvalId/decide
Authorization: Bearer <키>
{ "decision": "allow" | "deny", "allowlist_pattern": "optional" }

회신 후 GET .../sessions/:sessionId/events(마지막 id로 재접속)로 이후 이벤트를 이어받는다. 10분 내 미회신 시 자동 deny된다(in-flight timeout).

kind=secret — 환경변수/시크릿 요청. data: { kind:'secret', key_name, reason, approval_id, secret_url }.

secret_url로 값을 제출하면 스트림이 이어진다(approval과 동형, 같은 세션 resume):

POST {secret_url}          # = /api/v1/workspaces/:id/approvals/:approvalId/secret
Authorization: Bearer <키>   # secret 스코프 필요
{ "action": "provide", "value": "<시크릿 평문>" }   # 또는 { "action": "skip" }

평문 value는 이 요청 바디에서만 흐르고 vault에 암호화 저장된다(응답/로그/DB에 평문 미저장). 회신 후 GET .../sessions/:sessionId/events(마지막 id로 재접속)로 이후 이벤트를 이어받는다. 10분 내 미회신 시 자동 deny된다(in-flight timeout).

4. 재접속 (Last-Event-ID)

연결이 끊기면(네트워크/서버 함수 timeout) 마지막으로 받은 id로 재접속한다. 표준 EventSource는 이를 자동 처리한다.

GET /api/v1/workspaces/:id/sessions/:sessionId/events
Authorization: Bearer <키>
Last-Event-ID: 42            # 또는 ?last_event_id=42 (헤더 스트립 프록시 대비)
  • 42 이후(from_seq) 누락분을 replay한 뒤 live-tail 한다.
  • 진행 중(thinking) 세션만 재개하며, 이미 완료됐거나 시작 전이면 즉시 done(resumed:false).
  • 버퍼가 사라졌으면 error{code:session_gone} — 새 스트림을 시작해야 한다.

재접속 유예 (SLA)

재개는 agent-runner EventBuffer 보존 기간(약 24시간) 내에서만 보장된다. 그 이후 재접속은 session_gone이 될 수 있다. 장기 단절 후에는 POST .../messages로 새로 시작하라.

5. 실행 중지 (cancel)

진행 중인 turn을 중지하려면 POST .../sessions/:sessionId/cancel을 호출한다(chat 스코프). SSE 연결을 끊는 것만으로는 서버의 러너 SDK 루프가 멈추지 않는다 — 스트림 종료는 소비자 측 정리일 뿐이라, 더 이상 필요 없는 장기/폭주 실행은 명시적으로 취소해야 한다.

curl -X POST https://daiops.com/api/v1/workspaces/$WS/sessions/$SESSION/cancel \
  -H "Authorization: Bearer $KEY"
# → { "data": { "canceled": true, "runner_halt_deferred": false } }
  • 멱등 — 이미 끝난 세션이면 { canceled: false, reason: "not_in_progress" }(에러 아님).
  • runner_halt_deferred: true면 취소는 접수됐으나 러너 중지 확인을 아직 못 받은 상태다(러너는 max_response_seconds·max_turns 한도 내 유한 종료). MCP는 cancel_session 도구, A2A는 tasks/cancel로 동일하게 중지한다.

6. 파일 주고받기

파일 인바운드(업로드 티켓·origin_url)·아웃바운드(attachment 이벤트·/files/download)·과거 산출물 재조회는 파일 주고받기 가이드에서 전체를 다룬다. 요약: 산출물은 스트림 완료 직전 attachment 이벤트(위 §2 표)로 7일 다운로드 URL이 오며, 이것이 발견의 정본이다.

7. caller_id 계약

  • caller_id를 매 호출 동일하게 주면 대화 맥락이 이어진다(session_key=api:{caller_id}).
  • caller_id 없이 호출하면 매번 독립 세션이라 재접속으로 맥락을 이을 수 없다.
  • caller_id의 의미는 호출자 소관(예: 최종 사용자 ID) — daiops는 불투명 값으로만 다룬다.