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/messages | chat |
GET /api/v1/workspaces/:id/sessions | sessions:read |
GET /api/v1/workspaces/:id/sessions/:sessionId/messages | sessions:read |
GET /api/v1/workspaces/:id/sessions/:sessionId/events | sessions:read |
POST /api/v1/workspaces/:id/files (업로드 티켓) | files:write |
POST /api/v1/workspaces/:id/files/download | tools:read |
POST /api/workspaces/:id/approvals/:approvalId/decide | chat |
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_result | response_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는 불투명 값으로만 다룬다.