장기 실행 에이전트 서비스를 위한 컨트롤 플레인 (Control Plane)
요약
장기 실행 에이전트 앙상블 서비스를 외부 시스템과 상호작용하게 만드는 컨트롤 플레인(Control Plane) 설계 방식을 다룹니다. 데이터 플레인과 분리된 REST API를 통해 CI 파이프라인이나 커스텀 UI가 에이전트 실행을 제어하고 조회하는 방법을 설명합니다.
핵심 포인트
- 데이터 플레인과 컨트롤 플레인의 명확한 설계적 구분
- REST API를 통한 에이전트 실행 제출 및 상태 조회
- WebSocket 없이도 외부 시스템과의 상호작용 가능
- ToolCatalog와 ModelCatalog를 통한 API 활성화
이 시리즈의 이전 포스트에서는 에이전트 앙상블 (agent ensembles)을 장기 실행 서비스(long-running services)로 실행하는 방법을 다루었습니다. 이는 한 번 실행되고 종료되는 대신 WebSocket, HTTP, 큐(queues) 또는 토픽(topics)을 통해 작업을 수락하는 상시 가동 프로세스(always-on processes)를 의미합니다. 일단 앙상블이 서비스가 되면 새로운 범주의 문제가 나타납니다. 외부 시스템이 어떻게 이와 상호작용할 것인가 하는 점입니다.
기존의 WebSocket 대시보드는 실행 이벤트(execution events)를 스트리밍하고 검토 결정(review decisions)을 처리합니다. 이는 관측성(observability)과 인간의 검토(human review)를 충족합니다. 하지만 실행 제출(run submission)은 다루지 못합니다. CI 파이프라인, 오케스트레이터(orchestrator), 또는 커스텀 UI가 WebSocket 연결과 커스텀 클라이언트 코드 없이 실행을 시작하거나, 런타임 파라미터(runtime parameters)를 전달하거나, 현재 실행 중인 내용을 조회하거나, 잘못된 작업을 취소할 수 있는 방법이 없습니다.
Ensemble Control API가 그 간극을 메웁니다.
컨트롤 플레인 (Control Plane) vs. 데이터 플레인 (Data Plane)
API 자체에 대해 알아보기 전에, 명시적으로 언급할 가치가 있는 설계상의 구분이 있습니다.
v3 네트워크 모듈은 앙상블 간 통신(ensemble-to-ensemble communication)을 처리합니다. 즉, 원격 피어(remote peers)에게 작업을 위임하는 태스크, 기능 레지스트리(capability registries), 네임스페이스(namespaces) 간의 연합(federation) 등이 이에 해당합니다. 이것이 **데이터 플레인 (data plane)**입니다. 즉, 앙상블 피어들을 위해 설계된 앙상블 내부 트래픽입니다.
Control API는 **컨트롤 플레인 (control plane)**입니다. CI 파이프라인, 오케스트레이터, 그리고 커스텀 UI가 앙상블 서비스와 통신하는 통로입니다. 대상(audience)이 다르고 의미론(semantics)이 다릅니다. 외부 시스템은 WebSocket 클라이언트가 필요하지 않아야 하며, 앙상블 네트워킹 프로토콜을 이해할 필요도 없어야 하고, 앙상블 피어로 취급되어서도 안 됩니다. REST 우선(REST-first) 설계는 이러한 구분을 반영합니다.
1단계: 핵심 REST 엔드포인트 (Core REST Endpoints)
WebSocket 대시보드와 동일한 Javalin 서버에서 작동하는 네 개의 엔드포인트입니다. 새로운 포트나 새로운 프로세스가 필요하지 않습니다.
POST /api/runs 입력 변수와 함께 실행 제출
GET /api/runs 최근 실행 목록 (상태, 태그로 필터링 가능)
GET /api/runs/{runId} 전체 실행 상세 정보 조회 (상태, 태스크 출력, 메트릭)
...
설정 (Setup)
WebDashboard.builder()에 카탈로그를 추가함으로써 API가 활성화됩니다.
ToolCatalog tools = ToolCatalog.builder()
.tool("web_search", webSearchTool)
.tool("calculator", calculatorTool)
...
대시보드에서 앙상블 (Ensemble)이 연결됩니다:
Ensemble.builder()
.chatLanguageModel(claudeSonnetModel)
.webDashboard(dashboard)
...
ToolCatalog와 ModelCatalog는 두 가지 목적을 수행합니다. 첫째, API를 전송 방식에 무관하게 (transport-agnostic) 만듭니다 (JSON은 도구와 모델을 클래스가 아닌 이름으로 참조합니다). 둘째, 허용 목록 (allowlist) 역할을 합니다. 즉, 등록된 도구와 모델만 사용할 수 있습니다. 이후 단계에서의 동적 작업 생성 (Dynamic task creation) 시 임의의 코드를 인스턴스화할 수 없습니다.
실행 제출 (Submitting a run)
POST /api/runs는 변수 치환 (variable substitution)이 포함된 사전 구성된 앙상블 작업을 제출합니다:
{
"inputs": {
"topic": "AI safety",
...
응답 (202 Accepted):
{
"runId": "run-7f3a2b",
"status": "ACCEPTED",
...
실행은 비동기적으로 수행되며, 응답은 즉시 반환됩니다. 완료 여부는 GET /api/runs/{runId}를 통해 폴링 (Poll) 합니다. 태그 (Tags)는 필터링 및 감사를 위한 임의의 메타데이터입니다. 본문(body)을 비워 제출하면 치환 없이 템플릿 앙상블이 제출됩니다. 만약 maxConcurrentRuns에 도달하면, retryAfterMs 힌트와 함께 429 응답이 반환됩니다.
기능 조회 (Querying capabilities)
GET /api/capabilities는 등록된 항목들을 노출합니다:
{
"tools": [
{ "name": "web_search", "description": "Search the web using Google" },
...
GET /api/runs/{runId}는 작업 출력 및 지표 (metrics)를 포함한 전체 실행 상세 정보를 반환합니다. GET /api/runs는 ?status=RUNNING, ?status=COMPLETED, 또는 ?tag=triggeredBy:ci-pipeline으로 필터링 가능한 최근 실행 목록을 나열합니다.
2단계: 3단계 실행 제출 모델 (The Three-Level Run Submission Model)
컨트롤 API (Control API)에서 가장 흥미로운 설계 결정은 단계별 실행 제출 모델입니다. 세 가지 단계가 있으며, 각 단계는 이전 단계보다 더 동적입니다.
1단계 (위에서 다룸): 사전 구성된 앙상블에 템플릿 변수를 치환합니다. 가장 단순하고 제약이 많은 옵션으로, Java 코드가 실행 내용을 정의합니다.
2단계: 런타임 (runtime)에 개별 작업의 특정 필드를 재정의 (override) 합니다.
3단계: Java 코드를 전혀 변경하지 않고, POST 본문(body) 내에서 완전히 새로운 작업 목록을 정의합니다.
이러한 단계적 접근 방식은 단순한 사례는 단순하게 유지하면서도, 카탈로그 모델 (catalog model)의 안전성 속성을 포기하지 않고 더 동적인 사례들을 가능하게 합니다.
작업 명명 (Task naming)
Level 2와 Level 3를 효과적으로 사용하려면 작업에 논리적인 이름을 부여할 수 있습니다:
Task.builder()
.name("researcher")
.description("Research {topic} focusing on recent developments in {year}")
...
GET /api/capabilities는 설명(description)과 함께 작업 이름들을 반환합니다. Level 2 재정의 (override) 키는 먼저 정확한 이름으로 일치 여부를 확인하며, 일치하는 것이 없을 경우 설명 접두사(description prefix, 처음 50자, 대소문자 구분 없음)를 통해 차선책으로 일치 여부를 확인합니다.
Level 2: 작업별 재정의 (Per-task overrides)
taskOverrides를 사용하면 호출자가 재컴파일 없이 특정 작업의 설명, 모델, 도구 또는 컨텍스트 (context)를 변경할 수 있습니다:
{
"inputs": { "topic": "AI safety" },
"taskOverrides": {
...
재정의 키("researcher")는 템플릿 앙상블 (template ensemble)의 작업 이름들과 대조됩니다. 일치하는 작업이 없으면 요청은 400 에러와 함께 거부됩니다. 원래의 작업 객체는 절대 변형되지 않으며, Task.toBuilder()가 수정된 복사본을 생성합니다.
모든 도구 참조 (tool references)는 ToolCatalog를 기준으로 해결되며, 모든 모델 참조 (model references)는 ModelCatalog를 기준으로 해결됩니다. 호출자는 사전에 등록되지 않은 도구나 모델을 주입할 수 없습니다.
Level 3: 동적 작업 생성 (Dynamic task creation)
요청 본문에 tasks가 제공되면, 템플릿 앙상블의 작업 목록이 완전히 교체됩니다. 템플릿의 모델, 카탈로그 및 설정은 유지되며 오직 작업 목록만 변경됩니다:
{
"tasks": [
{
...
context 필드는 작업 간의 의존성 (dependencies)을 선언합니다. $researcher는 `
WebSocket 실행 제출 (WebSocket run submission)
REST가 유일한 제출 채널은 아닙니다. WebSocket 클라이언트는 run_request 메시지를 사용하여 실행 (run)을 제출할 수 있으며, 이는 이미 대시보드 연결을 가지고 있는 브라우저 기반 UI에 유용합니다:
{
"type": "run_request",
"requestId": "req-1",
...
서버는 run_ack로 즉시 응답합니다. 완료 시에는 요청을 보낸 세션에만 run_result를 전송하며, 기존의 ensemble_completed 브로드캐스트 (broadcast)는 변경 없이 모든 연결된 클라이언트로 계속 전송됩니다.
3단계: 실행 제어 (Run Control)
진행 중인 실행 (in-flight runs)에 적용되는 두 가지 작업입니다.
취소 (Cancellation)
POST /api/runs/{runId}/cancel은 실행 중이거나 수락된 실행을 취소합니다. 이는 협력적 취소 (cooperative cancellation) 방식입니다. 즉, 현재 진행 중인 작업은 정상적으로 완료되며, 취소는 다음 작업이 시작되기 전에 적용됩니다.
{ "runId": "run-abc", "status": "CANCELLING" }
동일한 작업이 WebSocket을 통해서도 가능합니다: { "type": "run_control", "runId": "run-abc", "action": "cancel" }.
이 협력적 모델은 의도된 설계입니다. 실행 중인 작업은 LLM 호출 (LLM-call) 중간 단계에 있습니다. 이를 즉시 중단하면 앙상블 (ensemble)이 정의되지 않은 상태 (undefined state)에 빠질 수 있습니다. 현재 작업을 완료하고 경계 지점에서 깔끔하게 중단하는 것은 이미 진행된 진척도를 잃지 않으면서 결정론적 동작 (deterministic behavior)을 보장합니다.
실행 중 모델 전환 (Mid-run model switching)
POST /api/runs/{runId}/model은 이후의 작업들이 사용할 LLM을 전환합니다:
{ "model": "haiku" }
전환은 다음 LLM 호출 시점에 적용되며, 현재 진행 중인 호출은 이전 모델로 완료됩니다. 모델 별칭 (model alias)은 ModelCatalog에 등록되어 있어야 합니다. 이는 장기 실행 중인 앙상블이 진행되는 도중에 이후 작업들이 더 저렴하거나 빠른 모델을 사용하도록 하고 싶을 때 유용합니다.
4단계: 이벤트 스트리밍 (Event Streaming)
기존의 WebSocket 대시보드는 모든 실행 이벤트 (execution events)를 모든 연결된 세션으로 브로드캐스트합니다. 4단계에서는 필터링 기능과 HTTP 네이티브 대안을 추가합니다.
구독 필터링 (Subscription filtering)
또는 특정 실행으로 필터링할 수 있습니다:
SSE 스트리밍 (SSE streaming)
HTTP 전용 클라이언트(curl 스크립트, 서버리스 함수(serverless functions), 서버 측 통합(server-side integrations))의 경우, WebSocket 연결은 번거로울 수 있습니다. SSE 엔드포인트는 일반 HTTP 연결을 통해 동일한 이벤트 스트림(event stream)을 제공합니다:
GET /api/runs/{runId}/events
Accept: text/event-stream
완료된 실행(runs)의 경우, 저장된 이벤트가 즉시 재생(replay)되고 연결이 종료됩니다. 진행 중인 실행의 경우, 실행이 완료될 때까지 이벤트가 스트리밍됩니다. from 파라미터를 사용하면 저장된 출력의 특정 위치부터 재개하여 재연결(reconnection)을 지원할 수 있습니다.
5단계: 컨트롤 루프 (Control Loop) 완성
5단계에서는 이전에는 WebSocket 대시보드를 통해서만 가능했거나 실행 중인 Java 프로세스와 직접 상호작용해야 했던 세 가지 작업을 통해 API를 완성합니다.
REST 검토 결정 (REST review decisions)
Human-in-the-loop 시스템은 앙상블(ensemble)이 진행되기 전에 검토자가 작업 출력을 승인, 수정 또는 거부하는 검토 게이트(review gates)를 생성합니다. 5단계는 이를 REST로 노출하여, 서버 측 시스템(Slack 봇, CI 파이프라인)이 검토 결정을 자동화하거나 라우팅할 수 있도록 합니다:
POST /api/reviews/{reviewId}
{ "decision": "CONTINUE" }
수정(edits)의 경우:
{ "decision": "EDIT", "revisedOutput": "Updated output..." }
대기 중인 검토 확인:
GET /api/reviews
GET /api/reviews?runId=run-abc
컨텍스트 주입 (Context injection)
실행 중인 앙상블의 DirectiveStore에 지시 사항(directive)을 주입합니다. 이 지시 사항은 앙상블 내 어떤 에이전트(agent)의 다음 LLM 반복(iteration) 시점에 반영됩니다:
POST /api/runs/{runId}/inject
{ "content": "Focus on EU AI Act compliance", "target": "researcher" }
이는 대시보드의 라이브 실행 보기(live run view)를 통해 허용되는 기능의 REST 버전입니다. 실행 중간에 실행을 조종(steer)해야 하는 서버 측 자동화에 유용합니다.
직접적인 도구 호출 (Direct tool invocation)
전체 앙상블을 실행하지 않고 ToolCatalog에 등록된 도구를 실행합니다:
POST /api/tools/calculator/invoke
{ "input": "What is 42 * 17?" }
응답:
{ "tool": "calculator", "status": "SUCCESS", "output": "714", "durationMs": 2 }
이는 통합 테스트 (integration testing), 도구 설정 (tool configuration) 검증, 그리고 앙상블 실행 (ensemble run)의 오버헤드 없이 단일 도구 호출 (single tool call)이 필요한 파이프라인 단계 (pipeline steps)에 유용합니다.
설계상의 긴장 (The Design Tension)
이러한 기능에서 흥미로운 질문은 컨트롤 플레인 (control plane)과 데이터 플레인 (data plane) 사이의 경계가 어디에 위치하느냐 하는 것입니다.
v3 네트워크 모듈은 이미 기능 쿼리 (CapabilityQueryMessage), 작업 위임 (NetworkTask/NetworkTool), 그리고 지시 사항 (DirectiveMessage) 기능을 갖추고 있습니다. 컨트롤 API (Control API) 또한 유사한 작업들을 노출하지만, HTTP를 통해 다른 대상에게 제공되며, 보안 및 액세스 의미론 (access semantics) 또한 다릅니다.
핵심적인 차이점은 대상 (audience)입니다. WebSocket 클라이언트가 필요하지 않고 앙상블 네트워킹 프로토콜 (ensemble networking protocol)을 이해할 필요가 없는 외부 시스템은 앙상블 피어 (ensemble peers)가 아니라 운영자 (operators)입니다. REST 우선 설계 (REST-first design), 카탈로그 기반의 허용 목록 (catalog-enforced allowlists), 그리고 단계별 Level 1/2/3 제출 모델 (graduated Level 1/2/3 submission model)은 이러한 차이점을 전반적으로 반영하고 있습니다.
앙상블 컨트롤 API (Ensemble Control API)는 control API 가이드에 문서화되어 있습니다. 기반이 되는 설계 문서는 design/28입니다. 소스 코드는 GitHub에서 확인할 수 있습니다.
저는 3단계 제출 모델이 어느 부분에서 적절하게 느껴지는지, 혹은 어느 부분에서 부족한지에 대해 관심이 있습니다. Level 2 (기존 작업 재정의)와 Level 3 (새로운 작업 정의) 사이의 경계는 설계상의 긴장이 가장 크게 발생하는 지점입니다. 이 분리가 유용한지, 아니면 대부분의 실제 사용 사례가 결국 어느 한쪽으로 수렴되는지 궁금합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기