에이전트리아(Agentria)에서 완성한 어빌리티(Ability, 워크플로)를 API로 배포했다면, 외부 서비스에서 이 API 엔드포인트(Endpoint)를 호출해 어빌리티를 실행할 수 있습니다. Ability API는 비동기(Async) 실행, SSE(Server-Sent Events) 스트리밍, 상태 조회, 요청 취소를 지원하는 REST API입니다.
이 가이드에서는 각 엔드포인트를 호출하는 방법과, 실행 결과를 안전하게 받아오는 방법을 안내합니다.
external_request_id는 클라이언트가 요청을 보내기 전에 직접 발급하는 식별자로, 두 가지 역할을 합니다. 첫째, 동일한 external_request_id로 재요청이 오면 서버가 같은 요청으로 판단해 409 Conflict로 거부하므로, 네트워크 재시도 등으로 인한 중복 실행(멱등성, Idempotency 보장)을 방지할 수 있습니다. 둘째, 클라이언트가 요청 시점에 이미 이 식별자를 확보해두는 방식이므로, 서버가 응답으로 request_id를 돌려주기 전부터 이 값으로 해당 요청을 추적·조회할 수 있습니다.
JSON: APIResponseSchema 그대로 ({"request_id":"...","status":"COMPLETED","results":{...},...})
error
어빌리티 실패 또는 처리 오류 (최종, 1회)
JSON: APIResponseSchema(status="FAILURE") 또는 plain 문자열
event_type이 request_id면 스트림 시작을 알리는 메타 이벤트, node면 중간 이벤트, response 또는 error면 최종 이벤트입니다. 또는 body JSON의 status 값이 COMPLETED·FAILURE이면 최종 이벤트로 판단할 수 있습니다. 스트림이 종료되면 연결이 닫힙니다.
constformData = newFormData();formData.append('params_json',JSON.stringify({inputData:'안녕하세요'}));formData.append('debug','false');constresponse = awaitfetch(`${host}/api/ability/my-ability/sse`,{method:'POST',headers:{'X-API-KEY':'your-api-key'},body:formData});// 스트림 유실 시 GET .../{requestId}/result 재조회용. 첫 request_id 이벤트로도 갱신됩니다.letrequestId = response.headers.get('X-Ability-Request-Id');// 한 번의 read() 가 여러 이벤트를 합쳐서 줄 수 있으므로 line-buffered 파싱이 필요합니다.constreader = response.body.getReader();constdecoder = newTextDecoder();letbuf = '';letcurrentEvent = null;constevents = [];while(true){const{done,value} = awaitreader.read();if(done)break;buf += decoder.decode(value,{stream:true});letnl;while((nl = buf.indexOf('\n')) !== -1){constline = buf.slice(0,nl);buf = buf.slice(nl + 1);if(line === ''){currentEvent = null;// 이벤트 경계continue;}if(line.endsWith(':')){currentEvent = line.slice(0, -1);// "node" | "response" | "error"continue;}// body 라인 — JSON.parse 시도 후 실패하면 raw string으로 처리letbody;try{body = JSON.parse(line);}catch{body = line;}if(currentEvent === 'request_id'){requestId = body;// plain UUID 문자열}elseif(currentEvent === 'node'){console.log('[node]',body.ability_node_id,body.ability_node_name,body.results);}elseif(currentEvent === 'response' || currentEvent === 'error'){events.push(body);console.log(`[${currentEvent}] status=${body.status}`,body.results);}}}// 최종 결과 = events[events.length - 1]constfinal = events[events.length - 1];if(final?.status === 'FAILURE'){console.error('실패:',final.failure_reason);}// 최종 이벤트 없이 스트림이 끊긴 경우 — 재전송(재실행) 대신 재조회if(!final && requestId){constres = awaitfetch(`${host}/api/ability/my-ability/${requestId}/result`,{headers:{'X-API-KEY':'your-api-key'}});constresult = awaitres.json();// status 가 COMPLETED/FAILURE 가 될 때까지 폴링}
constformData = newFormData();formData.append('params_json',JSON.stringify({inputData:'안녕하세요'}));formData.append('debug','false');constresponse = awaitfetch(`${host}/api/ability/my-ability/sse`,{method:'POST',headers:{'X-API-KEY':'your-api-key'},body:formData});// 스트림 유실 시 GET .../{requestId}/result 재조회용. 첫 request_id 이벤트로도 갱신됩니다.letrequestId = response.headers.get('X-Ability-Request-Id');// 한 번의 read() 가 여러 이벤트를 합쳐서 줄 수 있으므로 line-buffered 파싱이 필요합니다.constreader = response.body.getReader();constdecoder = newTextDecoder();letbuf = '';letcurrentEvent = null;constevents = [];while(true){const{done,value} = awaitreader.read();if(done)break;buf += decoder.decode(value,{stream:true});letnl;while((nl = buf.indexOf('\n')) !== -1){constline = buf.slice(0,nl);buf = buf.slice(nl + 1);if(line === ''){currentEvent = null;// 이벤트 경계continue;}if(line.endsWith(':')){currentEvent = line.slice(0, -1);// "node" | "response" | "error"continue;}// body 라인 — JSON.parse 시도 후 실패하면 raw string으로 처리letbody;try{body = JSON.parse(line);}catch{body = line;}if(currentEvent === 'request_id'){requestId = body;// plain UUID 문자열}elseif(currentEvent === 'node'){console.log('[node]',body.ability_node_id,body.ability_node_name,body.results);}elseif(currentEvent === 'response' || currentEvent === 'error'){events.push(body);console.log(`[${currentEvent}] status=${body.status}`,body.results);}}}// 최종 결과 = events[events.length - 1]constfinal = events[events.length - 1];if(final?.status === 'FAILURE'){console.error('실패:',final.failure_reason);}// 최종 이벤트 없이 스트림이 끊긴 경우 — 재전송(재실행) 대신 재조회if(!final && requestId){constres = awaitfetch(`${host}/api/ability/my-ability/${requestId}/result`,{headers:{'X-API-KEY':'your-api-key'}});constresult = awaitres.json();// status 가 COMPLETED/FAILURE 가 될 때까지 폴링}
constformData = newFormData();formData.append('params_json',JSON.stringify({inputData:'안녕하세요'}));formData.append('debug','false');constresponse = awaitfetch(`${host}/api/ability/my-ability/sse`,{method:'POST',headers:{'X-API-KEY':'your-api-key'},body:formData});// 스트림 유실 시 GET .../{requestId}/result 재조회용. 첫 request_id 이벤트로도 갱신됩니다.letrequestId = response.headers.get('X-Ability-Request-Id');// 한 번의 read() 가 여러 이벤트를 합쳐서 줄 수 있으므로 line-buffered 파싱이 필요합니다.constreader = response.body.getReader();constdecoder = newTextDecoder();letbuf = '';letcurrentEvent = null;constevents = [];while(true){const{done,value} = awaitreader.read();if(done)break;buf += decoder.decode(value,{stream:true});letnl;while((nl = buf.indexOf('\n')) !== -1){constline = buf.slice(0,nl);buf = buf.slice(nl + 1);if(line === ''){currentEvent = null;// 이벤트 경계continue;}if(line.endsWith(':')){currentEvent = line.slice(0, -1);// "node" | "response" | "error"continue;}// body 라인 — JSON.parse 시도 후 실패하면 raw string으로 처리letbody;try{body = JSON.parse(line);}catch{body = line;}if(currentEvent === 'request_id'){requestId = body;// plain UUID 문자열}elseif(currentEvent === 'node'){console.log('[node]',body.ability_node_id,body.ability_node_name,body.results);}elseif(currentEvent === 'response' || currentEvent === 'error'){events.push(body);console.log(`[${currentEvent}] status=${body.status}`,body.results);}}}// 최종 결과 = events[events.length - 1]constfinal = events[events.length - 1];if(final?.status === 'FAILURE'){console.error('실패:',final.failure_reason);}// 최종 이벤트 없이 스트림이 끊긴 경우 — 재전송(재실행) 대신 재조회if(!final && requestId){constres = awaitfetch(`${host}/api/ability/my-ability/${requestId}/result`,{headers:{'X-API-KEY':'your-api-key'}});constresult = awaitres.json();// status 가 COMPLETED/FAILURE 가 될 때까지 폴링}
참고 (Python 클라이언트): 위와 동일한 라인 버퍼링이 필요합니다. httpx.AsyncClient.stream()의 aiter_lines()는 한 줄씩 값을 넘겨주므로, node: / response: / error: 접두 라인은 JSON 파싱 오류로 건너뛰고 JSON 라인만 파싱하면 됩니다. 단, request_id 이벤트의 body(plain UUID)는 JSON이 아니므로, 재조회용 ID가 필요하면 직전 라인이 request_id:인지 추적하거나 응답 헤더 X-Ability-Request-Id를 사용합니다.
{"request_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","external_request_id":null,"chat_room_id":null,"status":"COMPLETED","failure_reason":null,"results":{"output":"어빌리티 실행 결과"},"artifact_metadata_json":null,"request_params_json":"{\"inputData\":\"안녕하세요\"}","result_metadata_json":null,"requested_time":"2026-01-15T10:30:00","updated_time":"2026-01-15T10:31:00"}
{"request_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","external_request_id":null,"chat_room_id":null,"status":"COMPLETED","failure_reason":null,"results":{"output":"어빌리티 실행 결과"},"artifact_metadata_json":null,"request_params_json":"{\"inputData\":\"안녕하세요\"}","result_metadata_json":null,"requested_time":"2026-01-15T10:30:00","updated_time":"2026-01-15T10:31:00"}
{"request_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","external_request_id":null,"chat_room_id":null,"status":"COMPLETED","failure_reason":null,"results":{"output":"어빌리티 실행 결과"},"artifact_metadata_json":null,"request_params_json":"{\"inputData\":\"안녕하세요\"}","result_metadata_json":null,"requested_time":"2026-01-15T10:30:00","updated_time":"2026-01-15T10:31:00"}
external_request_id는 요청 시 외부 ID를 부여한 경우에만 값이 채워지며, 그 외에는 null입니다.
// external_request_id를 부여한 요청의 콜백 — 키 포함{"request_id":"a1b2c3d4-...","external_request_id":"order-12345","status":"COMPLETED","results":{...},"failure_reason":null}// external_request_id를 부여하지 않은 요청의 콜백 — 키 자체가 생략됨{"request_id":"a1b2c3d4-...","status":"COMPLETED","results":{...},"failure_reason":null}
// external_request_id를 부여한 요청의 콜백 — 키 포함{"request_id":"a1b2c3d4-...","external_request_id":"order-12345","status":"COMPLETED","results":{...},"failure_reason":null}// external_request_id를 부여하지 않은 요청의 콜백 — 키 자체가 생략됨{"request_id":"a1b2c3d4-...","status":"COMPLETED","results":{...},"failure_reason":null}
// external_request_id를 부여한 요청의 콜백 — 키 포함{"request_id":"a1b2c3d4-...","external_request_id":"order-12345","status":"COMPLETED","results":{...},"failure_reason":null}// external_request_id를 부여하지 않은 요청의 콜백 — 키 자체가 생략됨{"request_id":"a1b2c3d4-...","status":"COMPLETED","results":{...},"failure_reason":null}
외부 ID가 없는 요청은 external_request_id 키 자체를 콜백 페이로드에 포함하지 않습니다. 기존 방식 그대로 유지되므로 이전 연동 코드와도 호환됩니다.
세 가지 방법이 있습니다. GET /{api_endpoint}/{request_id}/status(또는 /result)로 상태를 폴링하거나, 실행 요청 시 callback_url을 지정해 완료 시점에 결과를 콜백으로 받거나, /sse 엔드포인트로 실행 중 각 노드의 결과를 실시간 스트리밍으로 받을 수 있습니다.
서버는 스트림 연결과 무관하게 어빌리티 실행을 계속 완료하고 결과를 기록합니다. 연결이 끊겨 최종 이벤트를 받지 못했다면, 같은 요청을 재전송하지 않고 request_id(또는 external_request_id)로 상태·결과 조회 API를 폴링해 결과를 가져와야 합니다. 같은 요청을 재전송하면 어빌리티가 다시 실행되므로 주의합니다.