본문으로 건너뛰기

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 루트 권장.

스킬을 이름으로 호출 (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_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는 불투명 값으로만 다룬다.