267 lines
22 KiB
Markdown
267 lines
22 KiB
Markdown
# NomadCode Core
|
|
|
|
NomadCode Core는 사용자 요청을 작업 단위로 받고, 작업 상태를 저장하며, 비동기 Agent 작업 흐름을 관리하기 위한 서버입니다.
|
|
|
|
초기 목표는 작업 생성과 조회, PostgreSQL 기반 작업 상태 저장, River 기반 비동기 작업 실행, Plane/Jira adapter, Mattermost REST post adapter 구성, IOP OpenAI-compatible Responses 호출 경로 확보입니다. NomadCode Core는 직접 모델 런타임, 모델 라우팅, RAG, 모델 실행용 MCP/tool policy, output validation, fallback 정책을 소유하지 않고 IOP를 실행/최적화 계층으로 사용합니다. Roadmap Operations Control Plane은 NomadCode 내부 roadmap/action side effect의 Core action 경계를 먼저 정리하고, 외부 agent 제어 표면은 후속 제어 adapter 스케치에서 별도로 판단합니다.
|
|
|
|
## 현재 구현 범위
|
|
|
|
- Go HTTP Server
|
|
- `GET /healthz`, `GET /readyz`
|
|
- task 생성 / 조회 / 목록 / enqueue API
|
|
- PostgreSQL 연결
|
|
- goose migration
|
|
- sqlc 기반 DB query 생성 구조
|
|
- River task job
|
|
- IOP Edge/OpenAI-compatible Responses 모델 호출 경로
|
|
- 향후 A2A-compatible agent delegation을 위한 client 경로
|
|
- Plane work item lookup, comment, state update, and work item 생성 adapter
|
|
- Jira issue lookup, comment, and status transition adapter
|
|
- Mattermost REST post adapter for task completion notifications
|
|
- 선택적 Docker Compose 실행 환경(PostgreSQL, Redis)
|
|
|
|
## 현재 구현하지 않은 범위
|
|
|
|
- IOP native protocol 연동
|
|
- Agent Integrator 연동 또는 대체 여부 확정
|
|
- Outline / Forgejo / Nextcloud 연동
|
|
- Roadmap Operations Control Plane의 외부 agent 제어 adapter
|
|
- IOP 내부 MCP/tool policy
|
|
- Web Agent UI
|
|
- Flutter 앱
|
|
- 복잡한 권한 정책
|
|
- 복잡한 workflow DSL
|
|
|
|
## 실행 방법
|
|
|
|
로컬 실행은 현재 개발 호스트의 Go와 `code-server` compose에 붙은 PostgreSQL/Redis가 있다는 전제로 진행합니다. local 기본 `DATABASE_URL`은 `postgres://nomadcode:nomadcode@code-server-postgres:5432/nomadcode-core-local?sslmode=disable` 이고, local 기본 `REDIS_URL`은 `redis://code-server-redis:6379/3`, `REDIS_KEY_PREFIX`는 `nomadcode-core:local` 입니다. dev 배포는 Docker Compose로 실행하며, 같은 `code-server-postgres` Postgres와 `code-server-redis` Redis를 사용합니다. dev DB명은 `nomad-core-dev` 이고, dev 기본 `REDIS_URL`은 `redis://code-server-redis:6379/4`, `REDIS_KEY_PREFIX`는 `nomadcode-core:dev` 입니다. `AUTH_PASSWORD`를 설정하면 `/readyz`와 `/api/*`에 HTTP Basic Auth가 적용됩니다.
|
|
|
|
모델 호출의 기본 방향은 IOP Edge의 OpenAI-compatible Responses input surface입니다. 기본 dev endpoint는 `MODEL_BASE_URL=http://toki-labs.com:18083/v1`, 기본 model route는 `MODEL_NAME=codex`입니다. NomadCode는 OpenAI-compatible request/response shape를 기본 계약으로 유지하고, task/workspace/session/approval/artifact/notification 같은 IOP/NomadCode 전용 실행 문맥은 별도 `iop` wrapper field가 아니라 요청 `metadata` 확장으로 전달하는 방향을 기준으로 합니다. `MODEL_API_KEY`가 비어 있으면 Authorization header를 보내지 않습니다. A2A agent 호출 endpoint는 `A2A_EDGE_URL`로 설정하고, 기존 `A2A_AGENT_URL`도 fallback alias로 받습니다. bearer token은 `A2A_TOKEN`, timeout은 `A2A_TIMEOUT_SEC`로 설정합니다. A2A는 향후 외부 agent delegation 표면이며 현재 기본 실행 경로는 아닙니다. Plane 연동은 `PLANE_BASE_URL`, `PLANE_TOKEN`으로 설정하며, toki-labs dev Plane 기본 URL은 `https://plane.toki-labs.com` 입니다.
|
|
|
|
### 포트와 환경 compatibility
|
|
|
|
Core process와 container 내부 포트는 `8080`을 compatibility baseline으로 유지합니다. `Makefile`, `bin/run`, `internal/config`의 local default는 `HTTP_ADDR=:8080`이고, `Dockerfile`도 `EXPOSE 8080`을 유지합니다. shared workspace나 remote runner에서 host publish가 필요할 때 compose 기본값은 container 내부 포트를 그대로 두고 host 쪽을 `18010:8080`으로 노출합니다. 기존 `localhost:8080` curl 예시는 local 단독 실행 compatibility 확인용으로 남기며, compose host publish를 예전처럼 써야 하면 `NOMADCODE_CORE_HOST_PORT=8080`을 지정합니다.
|
|
|
|
PostgreSQL과 Redis는 기본적으로 `code-server-postgres:5432`, `code-server-redis:6379` service DNS로 접근합니다. compose 내부 통신에는 host publish를 추가하지 않습니다. 운영상 host에서 직접 접근해야 하는 경우에만 PostgreSQL은 `15410:5432`, Redis는 `16310:6379` 대역 후보를 사용합니다.
|
|
|
|
외부 endpoint와 secret은 포트 표준과 분리합니다. IOP Edge OpenAI-compatible listener는 `MODEL_BASE_URL`, `MODEL_API_KEY`, `MODEL_NAME`, `MODEL_CONTEXT_SIZE`, `MODEL_TIMEOUT_SEC`로 설정하고, A2A는 `A2A_EDGE_URL`/`A2A_AGENT_URL`, `A2A_TOKEN`, `A2A_TIMEOUT_SEC`로 분리합니다. Core proto-socket endpoint는 `PROTO_SOCKET_PATH`, `PROTO_SOCKET_HEARTBEAT_INTERVAL_SEC`, `PROTO_SOCKET_HEARTBEAT_WAIT_SEC`로 조정하고, Flutter client connector는 `PROTO_SOCKET_HOST`, `PROTO_SOCKET_PORT`, `PROTO_SOCKET_SECURE`, `PROTO_SOCKET_ENABLED`, `PROTO_SOCKET_PATH`, `PROTO_SOCKET_HEARTBEAT_INTERVAL_SECONDS`, `PROTO_SOCKET_HEARTBEAT_WAIT_SECONDS`를 사용합니다. Plane, Jira, Mattermost 값은 각각 `PLANE_*`, `JIRA_*`, `MATTERMOST_*` env로 주입하며 token/API key 원문은 tracked 문서에 기록하지 않습니다.
|
|
|
|
`code-server` PostgreSQL 컨테이너에 DB를 생성하는 예시:
|
|
|
|
```bash
|
|
docker exec code-server-postgres psql -U nomadcode -d postgres -c 'CREATE DATABASE "nomadcode-core-local" OWNER nomadcode;'
|
|
docker exec code-server-postgres psql -U nomadcode -d postgres -c 'CREATE DATABASE "nomad-core-dev" OWNER nomadcode;'
|
|
```
|
|
|
|
Migration 실행:
|
|
|
|
```bash
|
|
./bin/migrate-up
|
|
```
|
|
|
|
서버 실행:
|
|
|
|
```bash
|
|
./bin/run
|
|
```
|
|
|
|
간단한 암호를 걸고 실행:
|
|
|
|
```bash
|
|
AUTH_PASSWORD="change-me" ./bin/run
|
|
curl -u nomadcode:change-me localhost:8080/api/tasks
|
|
```
|
|
|
|
다른 DB 주소를 사용할 경우:
|
|
|
|
```bash
|
|
DATABASE_URL="postgres://user:password@localhost:5432/dbname?sslmode=disable" ./bin/migrate-up
|
|
DATABASE_URL="postgres://user:password@localhost:5432/dbname?sslmode=disable" ./bin/run
|
|
```
|
|
|
|
IOP Edge OpenAI-compatible endpoint를 사용할 경우:
|
|
|
|
```bash
|
|
MODEL_BASE_URL="http://toki-labs.com:18083/v1" \
|
|
MODEL_API_KEY="" \
|
|
MODEL_NAME="codex" \
|
|
MODEL_CONTEXT_SIZE="0" \
|
|
MODEL_TIMEOUT_SEC="900" \
|
|
./bin/run
|
|
```
|
|
|
|
모델 호출은 OpenAI-compatible Responses API의 `POST /v1/responses` 형식을 사용합니다. 기본적으로 non-streaming 형식을 사용하지만, `MODEL_RESPONSES_STREAM` 설정이 `true`로 설정된 경우 `stream: true`로 스트리밍 호출(SSE)을 시도합니다. 스트리밍 호출이 실행되는 동안 클라이언트는 수신된 chunk를 파싱하면서 실시간으로 progress 콜백(`OnProgress`)을 호출하여 `authoring_progress_mode=streaming`, `authoring_progress_reason=receiving stream chunks` 및 최신 타임스탬프(`authoring_run_updated_at`)를 태스크 메타데이터에 머지합니다. 만약 엔드포인트(IOP)가 스트리밍을 지원하지 않아 명시적인 stream unsupported 에러(에러 메시지나 유형 등에 `stream`, `unsupported` 등의 신호 포함)를 응답하는 경우, silent failure 없이 자동으로 1회 non-streaming 호출로 fallback하며 `authoring_progress_mode=stream_unsupported`, `authoring_progress_reason=<원래 에러 메시지>` 메타데이터를 저장하고 재시도합니다. 일반적인 유효성 검증이나 모델 이름 오류 같은 일반 에러는 fallback 없이 즉시 에러로 반환됩니다. `MODEL_RESPONSES_STREAM`이 `false`인 경우 처음부터 non-streaming으로 동작하며 `authoring_progress_mode=non_streaming` 메타데이터가 기록됩니다. `MODEL_BASE_URL`이 `/v1`까지만 가리키면 Core가 `/responses`를 붙여 호출합니다. NomadCode의 task/workspace/session 문맥은 OpenAI-compatible 표면을 깨는 별도 top-level wrapper가 아니라 `metadata` 확장으로 전달합니다. workspace-bound authoring에서는 slot checkout path가 `metadata.workspace` flat string으로 전달됩니다. direct Ollama 호환 경로에서는 `MODEL_CONTEXT_SIZE`를 Ollama 전용 option인 `options.num_ctx`로 전달할 수 있지만, 기본 dev IOP Edge 호출에서는 `0`으로 둡니다.
|
|
|
|
A2A agent 호출 인터페이스는 JSON-RPC 2.0 `message/send`, `tasks/get`, `tasks/cancel`을 우선 지원합니다. `A2A_EDGE_URL`을 설정하면 worker는 A2A `message/send`를 blocking 호출하고, 완료된 task/message 응답만 local task completion으로 반영합니다. 기존 `A2A_AGENT_URL`도 fallback alias로 받습니다. 현재 NomadCode의 기본 실행 경로는 OpenAI-compatible Responses 호출이며, A2A는 후속 agent delegation 작업에서 기본화 여부를 다시 결정합니다.
|
|
|
|
Plane 연동은 Plane API key를 `PLANE_TOKEN`으로 받고 `X-Api-Key` header로 호출합니다. 현재 toki-labs dev Plane은 `https://plane.toki-labs.com`에 있고, 예시 workspace/project 값은 `workspace_slug=general`, `project_id=a6beb42f-7a8a-410c-b50f-ea3ca94828f3` 입니다.
|
|
|
|
Provider-neutral endpoint는 `/api/integrations/{provider}/tasks`로, `provider` path 파라미터에 Plane/Jira 등 구성된 외부 도구명을 넣고, 몸체에 `tenant`, `project`, `id` 등 neutral 필드를 보냅니다.
|
|
|
|
Jira 연동은 Jira Cloud Basic auth(`Email:APIToken`) 형식을 사용합니다. `JIRA_BASE_URL`, `JIRA_EMAIL`, `JIRA_API_TOKEN` 환경 변수를 설정하면 `main.go`에서 Jira client가 초기화됩니다. Jira는 `GET /rest/api/3/issue/{key}`, `POST /rest/api/3/issue/{key}/comment`, `POST /rest/api/3/issue/{key}/transitions` 엔드포인트를 사용합니다.
|
|
|
|
Mattermost task completion notification은 `MATTERMOST_BASE_URL`, `MATTERMOST_TOKEN`, `MATTERMOST_CHANNEL_ID` 환경 변수를 사용해 `POST /api/v4/posts`로 발송합니다. 설정이 누락되거나 Mattermost가 non-2xx를 반환하면 core는 notification projection 오류를 기록하되 canonical task completion 상태를 롤백하지 않습니다.
|
|
|
|
Gito HTTP webhook consumer는 `POST /api/integrations/gito/webhook`에서 signed `branch.updated` wakeup을 받고, local develop checkout을 fetch/scan한 뒤 Plane-origin roadmap creation sync job으로 넘깁니다. `GITO_WEBHOOK_SECRET`, `GITO_REPO_ID`, `GITO_DEVELOP_REPO_PATH`, `ROADMAP_CREATION_TODO_STATE_ID` 또는 `PLANE_TODO_STATE_ID`가 모두 있어야 켜지며, `GITO_BRANCH` 기본값은 `develop`, `GITO_REMOTE_NAME` 기본값은 `origin`입니다. `ROADMAP_CREATION_TODO_STATE_ID`를 설정하면 `PLANE_TODO_STATE_ID`보다 우선합니다. Gito 쪽 bootstrap은 watched branch 등록과 webhook subscription 등록을 사용하고, subscription의 `target_url`은 이 Core callback endpoint를 가리키며 signing secret 원문은 tracked 문서에 기록하지 않습니다. remote local target은 `http://127.0.0.1:18010/api/integrations/gito/webhook`, remote dev target은 `http://127.0.0.1:18011/api/integrations/gito/webhook`입니다. 계약 원문은 `agent-contract/outer/gito-branch-webhook-consumer-v1.md`와 Gito 제공 계약 `../gito/agent-contract/provided/gito-forgejo-branch-events-v1.md`를 함께 봅니다. `GITO_PROTO_SOCKET_URL`은 이전 wire consumer 호환 경로가 필요할 때만 별도로 설정합니다.
|
|
|
|
```bash
|
|
GITO_WEBHOOK_SECRET="<gito-webhook-signing-secret>" \
|
|
GITO_REPO_ID="<gito-repo-id>" \
|
|
GITO_DEVELOP_REPO_PATH="/path/to/develop-checkout" \
|
|
GITO_BRANCH="develop" \
|
|
GITO_REMOTE_NAME="origin" \
|
|
ROADMAP_CREATION_TODO_STATE_ID="<plane-todo-state-id>" \
|
|
./bin/run
|
|
```
|
|
|
|
```bash
|
|
PLANE_BASE_URL="https://plane.toki-labs.com" \
|
|
PLANE_TOKEN="<plane-api-key>" \
|
|
./bin/run
|
|
```
|
|
|
|
Plane work item에서 pending task를 수동 생성하는 예시:
|
|
|
|
```bash
|
|
curl -X POST localhost:8080/api/integrations/plane/tasks \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{
|
|
"workspace_slug": "general",
|
|
"project_id": "a6beb42f-7a8a-410c-b50f-ea3ca94828f3",
|
|
"work_item_id": "8f3861ff-49e4-4944-8ab3-7a7a4e0b95d1",
|
|
"state_id": "ea2e5b48-8bf1-4723-b749-de7723be41e9",
|
|
"external_url": "https://plane.toki-labs.com/general/projects/a6beb42f-7a8a-410c-b50f-ea3ca94828f3/issues/NOMAD-11",
|
|
"comment": "NomadCode core thin e2e smoke"
|
|
}'
|
|
```
|
|
|
|
현재 endpoint는 Plane work item 조회와 `state_id`를 포함한 external ref 저장만 검증합니다. 실제 Plane 상의 state update는 smoke 작업 단계에서 별도로 검증합니다. 자동 enqueue와 완료/실패 결과 발행은 pipeline 설계 이후 별도 마일스톤에서 다룹니다.
|
|
|
|
### Plane Smoke 테스트
|
|
|
|
실제 Plane API와의 통신(조회, 코멘트 추가, 상태 업데이트)을 검증하기 위한 툴을 제공합니다.
|
|
|
|
#### 1. 조회 Smoke 테스트 (Read-Only)
|
|
|
|
```bash
|
|
PLANE_BASE_URL="https://plane.toki-labs.com" \
|
|
PLANE_TOKEN="<plane-api-key>" \
|
|
PLANE_WORKSPACE_SLUG="general" \
|
|
PLANE_PROJECT_ID="a6beb42f-7a8a-410c-b50f-ea3ca94828f3" \
|
|
PLANE_WORK_ITEM_ID="<test-work-item-id>" \
|
|
./bin/plane-smoke
|
|
```
|
|
|
|
#### 2. 쓰기 Smoke 테스트 (Write-Apply)
|
|
|
|
실제 코멘트 작성 및 상태 변경을 유발하므로 테스트용 work item에서만 수행해야 합니다.
|
|
|
|
```bash
|
|
PLANE_BASE_URL="https://plane.toki-labs.com" \
|
|
PLANE_TOKEN="<plane-api-key>" \
|
|
PLANE_WORKSPACE_SLUG="general" \
|
|
PLANE_PROJECT_ID="a6beb42f-7a8a-410c-b50f-ea3ca94828f3" \
|
|
PLANE_WORK_ITEM_ID="<test-work-item-id>" \
|
|
PLANE_SMOKE_COMMENT="NomadCode core Plane smoke" \
|
|
PLANE_SMOKE_STATE_ID="ea2e5b48-8bf1-4723-b749-de7723be41e9" \
|
|
PLANE_SMOKE_APPLY=1 \
|
|
./bin/plane-smoke
|
|
```
|
|
|
|
> Plane work item 생성(`CreateIssue` / provider-neutral `CreateWorkItem`) adapter는 `POST .../work-items/`로 구현되어 있고 deterministic unit test로 검증합니다. live create는 실제 work item을 생성하는 destructive 동작이므로 기본 write smoke 흐름(comment/state)에는 포함하지 않으며, 필요 시 후속 수동 검증으로 진행합니다.
|
|
|
|
테스트 실행:
|
|
|
|
```bash
|
|
./bin/test
|
|
```
|
|
|
|
sqlc 실행:
|
|
|
|
```bash
|
|
./bin/sqlc
|
|
```
|
|
|
|
DB 접근 코드는 `migrations/`의 스키마와 `queries/`의 SQL을 기준으로 `internal/db/`에 생성합니다. `internal/db/db.go`, `internal/db/models.go`, `internal/db/tasks.sql.go`는 생성 파일이므로 직접 수정하지 않습니다.
|
|
|
|
Docker Compose 실행은 dev 배포용입니다. compose는 외부 Docker 네트워크 `net_nginx`에 붙고, 같은 네트워크의 `code-server-postgres`에 `nomad-core-dev` DB로, `code-server-redis`의 Redis DB 4번과 `nomadcode-core:dev` key prefix로 접속합니다. dev Redis 주소나 prefix를 바꿀 때는 local 실행용 `REDIS_URL`, `REDIS_KEY_PREFIX`와 분리된 `DEV_REDIS_URL`, `DEV_REDIS_KEY_PREFIX`를 사용합니다. 외부 노출 테스트에서는 `AUTH_PASSWORD`를 함께 지정합니다.
|
|
|
|
Compose도 Gito HTTP webhook consumer env를 빈 기본값으로 전달하므로, signing secret, repo id, local checkout volume, Todo state id가 준비된 경우에만 consumer가 활성화됩니다. `GITO_DEVELOP_REPO_PATH`는 container에서 접근 가능한 경로여야 합니다. remote local profile은 `NOMADCODE_CORE_HOST_PORT=18010`, remote dev profile은 `NOMADCODE_CORE_HOST_PORT=18011`을 사용하고 Core container 내부 listen은 `8080`으로 유지합니다.
|
|
|
|
```bash
|
|
AUTH_PASSWORD="change-me" ./bin/docker-up
|
|
```
|
|
|
|
Compose의 기본 host publish는 remote/local test workspace 기준 `18010:8080`입니다. dev profile은 같은 remote host에서 local profile과 분리하기 위해 아래처럼 `18011:8080`을 명시합니다.
|
|
|
|
```bash
|
|
NOMADCODE_CORE_HOST_PORT=18011 AUTH_PASSWORD="change-me" ./bin/docker-up
|
|
```
|
|
|
|
기존 host `8080`이 필요한 호환성 확인에서는 아래처럼 명시합니다.
|
|
|
|
```bash
|
|
NOMADCODE_CORE_HOST_PORT=8080 AUTH_PASSWORD="change-me" ./bin/docker-up
|
|
```
|
|
|
|
Makefile은 같은 명령을 감싸는 얇은 alias입니다.
|
|
|
|
```bash
|
|
make migrate-up
|
|
make run
|
|
make test
|
|
make sqlc
|
|
```
|
|
|
|
API 테스트:
|
|
|
|
```bash
|
|
curl localhost:8080/healthz
|
|
curl localhost:8080/readyz
|
|
```
|
|
|
|
```bash
|
|
curl -X POST localhost:8080/api/tasks \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"title":"README 수정 작업","source":"manual","payload":{"message":"README 초안을 정리해줘"}}'
|
|
```
|
|
|
|
```bash
|
|
curl localhost:8080/api/tasks
|
|
curl localhost:8080/api/tasks/{id}
|
|
curl -X POST localhost:8080/api/tasks/{id}/enqueue
|
|
```
|
|
|
|
## 작업 라이프사이클 책임 경계 (Workflow Lifecycle Ownership)
|
|
|
|
NomadCode Core의 작업 라이프사이클 관리는 다음과 같은 책임 경계를 따릅니다.
|
|
|
|
- **Workflow Service**: 외부 HTTP/Proto-Socket 인터페이스와 상호작용하여 작업 생성(Create), 조회(Get), 목록 조회(List), Enqueue API의 시맨틱(Semantics) 및 정책을 제어합니다.
|
|
- **Lifecycle**: 작업 상태 전이(Status Transitions) 및 메타데이터 병합 규칙(Metadata Merge Rules)의 Canonical State 처리를 독점적으로 소유합니다. 상태 변경은 반드시 Lifecycle 헬퍼를 경유해야 합니다.
|
|
- **Scheduler Worker**: 작업의 실행 단계만 담당합니다. 비동기 큐에서 작업을 꺼내어 `StartTask`를 호출하고, 실제 Agent/Model 실행을 수행한 후, 성공 시 `CompleteTask`, 실패 시 `FailTask`를 호출하여 실행 단계를 마무리합니다.
|
|
- **Provider Projection**: 외부 협업 도구(Plane, Mattermost 등)로 상태나 코멘트를 반영(Projection)하는 동작의 실패가 Core의 정규 작업 상태(Canonical Task State)를 롤백해서는 안 됩니다. 외부 투영 실패는 재시도 가능한 독립적인 부수 효과로 처리됩니다.
|
|
|
|
## 최소 재시도 및 타임아웃 정책 (Minimum Retry and Timeout Policy)
|
|
|
|
NomadCode Core의 비동기 작업 재시도 및 타임아웃 처리는 다음과 같은 규칙에 따라 작동합니다.
|
|
|
|
- **단일 시도 타임아웃 (Timeout wraps a single worker execution attempt)**:
|
|
- 개별 작업 실행 시도는 `WORKFLOW_TASK_TIMEOUT_SEC` (기본값: 300초) 설정 범위 내에서 래핑되어 실행됩니다.
|
|
- 이 제한을 초과하면 단일 실행이 실패한 것으로 간주하여 에러를 반환하고, 작업 상태를 `failed`로 변경합니다. 이때 실패 분류 메타데이터로 `failure_type=timeout`, `status_reason=timeout`이 기록됩니다.
|
|
- **예외 — Plane-origin authoring 작업**: checkout slot이 있는 Plane-origin authoring 태스크는 `WORKFLOW_TASK_TIMEOUT_SEC` 제한을 받지 않습니다. IOP Responses 호출은 10분 이상 실행될 수 있으며, 실행 범위는 `MODEL_TIMEOUT_SEC` (기본값: 900초)로만 제어됩니다. 진행 상태는 `authoring_run_state`와 `authoring_run_updated_at` 메타데이터로 관찰합니다.
|
|
- **Plane-origin authoring 3단계 실행 흐름**: Plane-origin authoring 태스크는 세 단계로 실행됩니다. (1) **첫 번째 authoring 응답**: IOP/model 호출로 마일스톤 초안을 생성하고 `response_id`를 기록합니다. 이 단계의 성공만으로 task가 완료되거나 projection으로 넘어가지 않습니다. (2) **push-only 요청**: 첫 번째 응답 성공 직후 workspace agent에 `변경된 내용에 대해 develop 브런치에 푸시해` 요청을 별도 model 호출로 보내고 `push_response_id`와 `push_request_state=succeeded`를 기록합니다. push-only 실패 시 `authoring_failure_category=push_failed`로 기록되고 slot은 `dirty` 상태로 전환됩니다. (3) **develop 브랜치 match 대기**: push-only 성공 후 `wait_type=develop_match`를 설정하고 Gito webhook 기반 develop match 확인을 기다립니다. 이 gate를 통과해야만 task가 `succeeded`로 완료됩니다.
|
|
- **최대 시도 횟수 (Max Attempts)**:
|
|
- River 비동기 작업 큐는 실패한 작업 시도를 최대 `workflow.DefaultTaskMaxAttempts` (기본값: 3회)까지 자동으로 다시 시도합니다.
|
|
- 각 시도가 시작될 때마다 작업 메타데이터의 `attempt` 카운트가 1씩 증가하여 기록되며, 실패 시 메타데이터의 `retryable` 필드에 남은 재시도 가능 여부(`attempt < DefaultTaskMaxAttempts`)가 기록됩니다.
|
|
- **재-Enqueue 가능 여부 (Re-Enqueue Behavior)**:
|
|
- 최종적으로 실패 상태(`failed`)인 작업은 다시 `queued`로 Enqueue하여 다시 처음부터 실행할 수 있습니다.
|
|
- 반면 완료(`completed`) 또는 취소(`canceled`)된 터미널 상태의 작업은 다시 시작(Restart/Re-enqueue)할 수 없습니다.
|
|
- **Plane-origin authoring stale 판단 (Authoring Stale Detection)**:
|
|
- authoring 작업의 stale 여부는 `authoring_run_updated_at` 메타데이터 타임스탬프와 `AUTHORING_STALE_AFTER_SEC` (기본값: 1200초) 기준으로 판단합니다.
|
|
- 실행 중인 authoring 작업은 `authoring_run_state=in_progress`와 `authoring_run_updated_at`이 함께 기록됩니다. 이 타임스탬프가 `AUTHORING_STALE_AFTER_SEC`를 초과하면 stale로 관찰됩니다.
|
|
- queue 대기 중인 authoring 작업은 `task.UpdatedAt`을 기준 타임스탬프로 사용하여 동일한 `AUTHORING_STALE_AFTER_SEC` 임계값에 비교할 수 있습니다. queue 대기 중에는 `WORKFLOW_TASK_TIMEOUT_SEC`가 적용되지 않으므로 조기 실패가 발생하지 않습니다.
|
|
- stale로 판단된 작업에 대한 조치(알림, 재-Enqueue 등)는 후속 모니터링 컴포넌트의 책임이며, 이 설정은 판단 기준만 제공합니다.
|
|
- `MODEL_RESPONSES_STREAM` 설정으로 스트리밍이 활성화되면 실시간 진행 상황을 `authoring_progress_mode`, `authoring_progress_reason`, `authoring_run_updated_at` 메타데이터로 갱신하여 stale monitor가 진행 상황 중단을 파악할 수 있도록 돕습니다. 명시적인 스트리밍 미지원 에러가 반환되는 환경에서는 fallback을 통해 `authoring_progress_mode=stream_unsupported`와 fallback 사유를 남깁니다. 처음부터 비스트리밍일 경우 `authoring_progress_mode=non_streaming`으로 기록됩니다.
|
|
- **retry 메타데이터 (Retry Metadata)**:
|
|
- 모든 실패 작업(generic 및 authoring)은 `FailTaskWithMetadata` 경로를 통해 `retryable` 메타데이터를 기록합니다. `retryable=true`이면 `attempt < DefaultTaskMaxAttempts`이므로 River가 다음 시도를 스케줄링합니다.
|
|
- authoring 태스크 실패 시 `authoring_run_state=failed`, `authoring_failure_type`, `authoring_failure_category`, `authoring_run_updated_at`, `retryable`이 함께 기록됩니다.
|