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 루트 권장.
스킬을 이름으로 호출 (skill / input)
command(자유 텍스트) 대신 발행된 스킬을 이름으로 부를 수 있다. 둘은 택일이며 정확히 하나를 지정한다(둘 다 / 둘 다 아님 = 400).
{ "skill": "ir-extract", "input": { "companyId": "abc", "documentUrl": "https://…" } }
- 하이픈·언더스코어·대소문자는 서로 흡수된다(
IR_Extract=ir-extract). - 발행(active)된 스킬만 호출된다. 없는 이름이면
400+code: "UNKNOWN_SKILL"과 유사 후보를 돌려준다. 프롬프트로 조용히 강등되지 않으므로, 오타가 "실행된 것처럼" 보이는 일이 없다. input의 값은 대화 기록에 저장되지 않는다. 에이전트 프롬프트로만 전달되고, 저장·표시되는 것은/스킬이름 (입력 키 목록)형태의 라벨뿐이다. 토큰·자격증명을 입력에 담아도 채팅 본문에 남지 않는다.command에/스킬이름을 적는 관례와 다르다 — 그 경우 서버는 자유 텍스트로 취급하고 스킬 로드는 모델의 추론에 달린다. 확실히 실행하려면skill필드를 쓴다.
입력은 계약으로 검증된다. 스킬이 타입드 계약(interface.inputs)을 선언했으면 서버가 이름·타입·enum·필수 여부를 대조하고, 어긋나면 400 + code: "INVALID_SKILL_INPUT" + violations[]로 어느 파라미터가 왜 틀렸는지 알려준다. 모르는 키도 거부한다 — 오타난 파라미터를 조용히 버리면 값을 보냈다고 믿게 되기 때문이다. 계약을 선언하지 않은 스킬은 무엇이든 통과한다.
에이전트에게 전달될 때 입력은 <inputs> 데이터 블록으로 감싸진다(첨부의 <attached_files>와 같은 규약). 지시문과 호출자 데이터의 경계를 세워, 값 안의 문장이 지시로 읽힐 여지를 줄인다.
스트리밍·블로킹·콜백 세 경로 모두 동일하게 지원한다.
자격증명을 이름으로 선언 (secret_input)
이번 실행에 쓸 자격증명을 이름 배열로 선언한다. 값을 넣는 필드는 의도적으로 없다.
{ "skill": "notion-sync", "input": { "dbId": "abc" }, "secret_input": ["NOTION_TOKEN"] }
- 값은 워크스페이스 vault(암호화)에만 있고, 러너의 주입 프록시가 허용 호스트로 나가는 요청에서만 실제 값으로 치환한다. 샌드박스에서
echo $NOTION_TOKEN을 하면 placeholder가 나온다 — 즉 에이전트가 값을 읽거나 다른 곳으로 보낼 수 없다. - 등록되지 않은 이름이면
400+code: "UNKNOWN_SECRET"과 사용 가능한 키 이름 목록을 돌려준다(값은 절대 응답에 담기지 않는다). 조용히 무시하면 호출자는 주입됐다고 믿고 에이전트는 값 없이 즉흥 대응한다. - 선언하면 에이전트가 "그 키가 있고, 값은 읽을 수 없고, 어느 호스트로만 나간다"를 알게 된다. 이미 있는 키를 사용자에게 다시 요구하는 낭비가 사라진다.
- 새 자격증명은 워크스페이스 설정에서 먼저 등록한다(대화 중이라면 에이전트가
request_secret으로 마스킹 카드를 띄운다).
응답은 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는 불투명 값으로만 다룬다.