daiops A2A 연결 가이드
외부 에이전트/오케스트레이터에서 daiops 직원(워크스페이스)을 표준 A2A(Agent2Agent) 에이전트로 호출하는 방법. (AGENT-API-3 Phase B)
REST 면(트리거·SSE 챗)은 GET /api/v1/openapi로 문서화되며, A2A 면은 JSON-RPC 2.0이라 이 문서로 별도 안내한다(MCP 서버와 동일한 이유).
A2A는 MCP 서버(Phase A)와 나란한 두 번째 프로토콜 파사드다. 실행은 기존 v1 능력(SSE 챗·트리거)에 매핑된다.
1. 디스커버리 (Agent Card)
daiops는 멀티테넌트라 A2A Agent Card가 직원(워크스페이스)별이다. 표준 루트 /.well-known/agent.json(단일 origin)로는 특정 직원을 표현할 수 없어 워크스페이스 스코프 경로로 노출한다.
GET /api/v1/workspaces/{workspaceId}/agent-card # 무인증 공개
- 카드의
url필드가 A2A JSON-RPC 서비스 엔드포인트(/a2a)를 가리킨다. skills는 직원-facing 능력(chat_with_employee·run_task·query_wiki)이며 MCP 카탈로그와 단일 소스로 일치한다.capabilities.streaming=true(message/stream),pushNotifications=false(A2A push 미구현 — 결과 push가 필요하면 REST 트리거의callback_url을 사용).
2. 서비스 엔드포인트
POST /api/v1/workspaces/{workspaceId}/a2a # JSON-RPC 2.0
- Base URL:
https://daiops.com - Content-Type:
application/json
지원 메서드(A2A 0.2.x + 초안 별칭):
| 메서드 | 별칭 | 동작 |
|---|---|---|
message/send | tasks/send | 블로킹. 최종 Task 반환. |
message/stream | tasks/sendSubscribe | SSE. status-update/artifact-update 스트림. |
tasks/get | — | 세션 상태 조회 → Task. |
tasks/cancel | — | 진행 중 세션 즉시 중지 → Task{ state:"canceled" }. 진행 중이 아니면 -32002. (§8 참조) |
3. 인증
Bearer API 키(dai_live_…)를 사용하며 a2a 스코프가 필요하다. Settings → API 키에서 발급.
Authorization: Bearer dai_live_xxxxxxxx
- rate limit: 키 단위 분당 60회 초과 시
429(+Retry-After). - quota: 실행 경로(message/send·message/stream)에 사용량 한도 게이트 적용. tasks/get은 무비용.
- Agent Card(GET /agent-card)는 무인증 공개(디스커버리).
라이프사이클(직원 상태) · 에러 코드
직원은 유휴 시 자동 퇴근(중지)하고 장기 미사용 시 휴직(archived, 컴퓨트 삭제)한다. message/send·message/stream은 상태에 따라:
- 퇴근(stopped): 자동 출근(auto-wake) 후 실행 — 첫 이벤트가 수 초~수십 초 지연될 수 있다.
- 휴직(archived): 자동 복귀하지 않음.
-32013(HTTP 409, "on leave — 복귀 필요"). 복귀(revive)는 소유자가 수행. - 자동 출근 꺼짐(external_auto_wake=false):
-32014(HTTP 409, "수동 출근 필요"). - 깨우는 중/일시 불가:
-32015(HTTP 503). 잠시 후 재시도. - 활발히 호출되는 직원은 자동 퇴근되지 않는다(호출이 활동 타이머를 갱신).
에러 코드 규약
표준/A2A 예약 코드는 스펙 의미로만 쓰고, daiops 도메인 에러는 예약 구간(-32001)에 둔다. -32006)을 침범하지 않도록 서버 정의 구간(-32010error.data.reason으로 프로그램 분기를 보조한다.
| 코드 | 의미 |
|---|---|
-32700 | Parse error (malformed JSON) |
-32600 | Invalid Request (배치 미지원·스키마 불일치) |
-32601 | Method not found (미지원 RPC 메서드) |
-32602 | Invalid params (필수 파라미터 누락) |
-32603 | Internal error (서버 내부 결함) |
-32001 | Task not found (tasks/get·tasks/cancel·이어쓰기 대상 세션 부재) |
-32002 | Task not cancelable (tasks/cancel 대상이 진행 중이 아님) |
-32010 | 인증 실패 (HTTP 401/403) |
-32011 | Rate limit 초과 (HTTP 429, Retry-After 헤더) |
-32012 | 워크스페이스(직원) 없음 |
-32013 | 휴직(on leave) — 복귀 필요 |
-32014 | 자동 출근 꺼짐 — 수동 출근 필요 |
-32015 | 일시 불가(깨우는 중 등) — 재시도 대상 |
-32016 | 사용량 한도 초과 |
-32017 | 세션 시작 실패. 재시도 가능 케이스는 error.data.code로 구분: WORKSPACE_BUSY(HTTP 503, 동시 실행 상한 초과)·RUN_ACTIVE(HTTP 409, 같은 세션 실행 진행 중). 이때 error.data.retryAfterSeconds와 HTTP Retry-After 헤더가 재시도 대기를 안내한다. |
멱등:
message/send는 at-least-once다. 네트워크 재시도 시 같은messageId라도 중복 실행될 수 있으므로, 호출자는 필요 시 자체 중복 제거를 둔다.
4. Task 모델
- Task id = 내부 세션 id(Task/세션 1:1).
message.taskId로 기존 Task를 이어쓰고,caller_id(또는message.contextId)로 호출자별 세션을 자동 재사용한다. - contextId:
message/send·message/stream은 호출자가 보낸message.contextId를 응답 Task·이벤트에 그대로 에코한다(미지정 시 taskId). 단tasks/get은 원 contextId를 보관하지 않아 taskId로 반환한다. - 입출력 파일(FilePart): 텍스트 파트와 함께
file(uri 또는 base64bytes)를 주고받는다. Agent CarddefaultInputModes/defaultOutputModes에application/octet-stream으로 선언. 파일 크기:uri(원격 URL)는 daiops가 서버측에서 당겨오므로 50MB까지 가능하고,bytes(base64 인라인)는 요청 바디에 실려 배포 요청 바디 한도(Vercel ~4.5MB)에 종속된다 — 대용량은 도달 가능한 URL이면uri를 쓴다(bytes는 호스팅 수단이 없거나 daiops가 URL에 도달할 수 없는 경우의 경로). 초과·미지원 등으로 처리되지 못한 첨부는 조용히 버려지지 않고 응답 텍스트 말미에 드롭 사유로 통보된다. - Task state 매핑:
thinking→working,plan_pending/secret_pending→input-required,completed→completed,error/failed→failed.
세션(Task) 목록 조회 — A2A엔 없음(의도적), 크로스-프로토콜로 확보
A2A는 표준 규격상 세션(Task) 목록 메서드가 없다(tasks/get은 id를 이미 알아야 하는 단건 조회). daiops도 표준 준수를 위해 A2A에 비표준 list 메서드를 추가하지 않는다. 이어쓸 기존 세션의 id는 다음 크로스-프로토콜 경로로 얻는다(모두 같은 chat_sessions.id = A2A taskId):
- REST:
GET /api/v1/workspaces/{id}/sessions?channel=a2a(sessions:read스코프) — 반환items[].id를message.taskId로 넘긴다. - MCP:
list_sessions도구 (mcp:read스코프). - 자기 세션만 안정 재사용하면 되는 소비자는 목록 없이
caller_id(또는message.contextId)로 자동 재사용해도 된다.
웹 뷰어: A2A 세션은
channel='a2a'로 기록되어 daiops 웹 통합 스레드 리스트/사이드바(A2A 진입점)에 read-only thread로 노출된다(REST=api, MCP=mcp와 구분).
5. message/send (블로킹)
curl -sX POST "$BASE/api/v1/workspaces/$WS/a2a" -H "$AUTH" -H 'Content-Type: application/json' -d '{
"jsonrpc":"2.0","id":1,"method":"message/send",
"params":{"message":{"kind":"message","role":"user","parts":[{"kind":"text","text":"이번 주 요약"}]}}
}'
응답 result는 Task(kind:task). 완료면 status.state="completed" + artifacts에 응답 텍스트/구조화 결과.
응답시간 바운딩 (선택 — 짧은 콜백 창용)
카카오톡·슬랙 콜백처럼 회신 창이 짧은(≈1분) 연동은 params에 실행 예산을 실어 예측가능한 지연을 얻는다. message/send·message/stream·MCP chat_with_employee 공통.
| 파라미터 | 범위 | 동작 |
|---|---|---|
max_turns | 1~50 | 에이전트 도구 루프 최대 turn. 낮게 주면 반복을 조기 종료. 미지정 시 50 |
max_response_seconds | 5~770 | 벽시계 예산(초). 초과 시 러너 실행을 멈추고 종료 — 생성된 부분은 세션에 영속돼 tasks/get으로 회수. 미지정 시 모델 기본 |
# 45초 예산·최대 6 turn으로 제한한 message/send
curl -sX POST "$BASE/api/v1/workspaces/$WS/a2a" -H "$AUTH" -H 'Content-Type: application/json' -d '{
"jsonrpc":"2.0","id":1,"method":"message/send",
"params":{
"message":{"kind":"message","role":"user","parts":[{"kind":"text","text":"이번 주 요약"}]},
"max_response_seconds":45, "max_turns":6
}
}'
범위 밖 값은 클램프된다(0·음수·비수치는 미지정으로 간주 = 기존 동작). 마감 초과 시 부분 텍스트를 강제로 반환하지 않고 "아직 처리 중" 신호를 준다 — 짧은 콜백 창에서는 이 신호를 받고 별도 채널(알림톡 등)로 지연 답변을 보내는 구성을 권장한다.
6. message/stream (SSE)
같은 body에 method:"message/stream". 응답은 text/event-stream이며 각 data:는 JSON-RPC 응답 봉투로 감싼 A2A 이벤트다:
status-update(state=working) — 진행/도구 사용artifact-update(append) — 응답 텍스트 델타(artifactId="response")artifact-update(lastChunk) — 구조화 결과(artifactId="structured")status-update(final=true) — completed / failed / input-required
각 이벤트의 id:는 agent-runner seq다. 끊기면 표준 Last-Event-ID로 GET /api/v1/workspaces/{id}/sessions/{sessionId}/events에 재접속해 이어받는다.
7. input-required (결재/시크릿) 회신
에이전트가 위험 도구(결재) 또는 시크릿을 요구하면 status-update{ state:"input-required", final:true }가 오고, status.message.metadata에 회신 URL이 실린다:
- 결재:
{ kind:"approval", approval_id, decide_url }→decide_url에{ "decision":"allow"|"deny" }POST(Bearerchat스코프). - 시크릿:
{ kind:"secret", key_name, approval_id, secret_url }→secret_url에{ "action":"provide", "value":"…" }또는{ "action":"skip" }POST(Bearersecret스코프).
회신 후 실행은 자동 재개되며, 이어지는 이벤트는 sessions/{sessionId}/events(SSE resume)로 이어받는다. 상호작용 결재 흐름은 message/stream을 사용한다 — 블로킹 message/send는 결재 해소까지 대기한다.
8. 보급 상황 · 우선순위 (B2 재확인)
A2A는 2025년 표준화가 진행 중인 신생 프로토콜이다(Linux Foundation 이관). 현재 daiops 소비자(주로 Lattice·내부 서비스)는 REST 트리거와 MCP 서버로 충분히 커버된다. 본 Phase B는 표준 상호운용 구현(Agent Card + message/send·stream + tasks/get + tasks/cancel + input-required 배선 + FilePart 입출력)이다.
구현됨(Phase B 후속):
tasks/cancel— 진행 중 세션을 즉시 중지(in-process abort + runner 취소 + DB aborted).{ "id": "<taskId>" }params, 결과Task{ state:"canceled" }. 진행 중이 아니면-32002.- FilePart 입출력 — 인바운드:
message.parts에{ kind:"file", file:{ name, mimeType, uri } }(원격 URL, 50MB) 또는{ …, bytes:"<base64>" }(인라인, 요청 바디 한도 ~4.5MB — 대용량은uri권장). 텍스트 지시와 함께 보내면 파일을 샌드박스에 배치해 에이전트가 읽는다(처리 실패분은 응답에 드롭 사유로 통보). 아웃바운드: 에이전트가 만든 산출물은artifactId:"files"아티팩트의 FilePart(file.uri= 7일 signed URL)로 반환된다(message/send는 최종 Task, message/stream은 completed 직전 artifact-update).실제 수요가 확인되면 확장할 항목:
tasks/pushNotificationConfig(webhook 등록) — Agent Cardcapabilities.pushNotifications:false로 미지원 명시.- 루트
/.well-known디렉토리형 디스커버리(다중 직원 카탈로그).수요 미확인 시 현 구현을 유지한다(과투자 방지).