214 lines
12 KiB
Markdown
214 lines
12 KiB
Markdown
# NomadCode Core
|
|
|
|
NomadCode Core는 사용자 요청을 작업 단위로 받고, 작업 상태를 저장하며, 비동기 Agent 작업 흐름을 관리하기 위한 서버입니다.
|
|
|
|
초기 목표는 작업 생성과 조회, PostgreSQL 기반 작업 상태 저장, River 기반 비동기 작업 실행, Plane/Mattermost Adapter stub 구성, IOP OpenAI-compatible Responses 호출 경로 확보입니다. NomadCode Core는 직접 모델 런타임, 모델 라우팅, RAG, 모델 실행용 MCP/tool policy, output validation, fallback 정책을 소유하지 않고 IOP를 실행/최적화 계층으로 사용합니다. Roadmap Operations Control Plane처럼 NomadCode 내부 상태/action을 외부 agent에게 여는 MCP 표면은 Core의 후속 제어 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, and state update adapter
|
|
- Mattermost Adapter stub
|
|
- 선택적 Docker Compose 실행 환경(PostgreSQL, Redis)
|
|
|
|
## 현재 구현하지 않은 범위
|
|
|
|
- 실제 Mattermost 메시지 발송
|
|
- IOP native protocol 연동
|
|
- Agent Integrator 연동 또는 대체 여부 확정
|
|
- Outline / Forgejo / Nextcloud 연동
|
|
- Roadmap Operations Control Plane MCP 서버
|
|
- 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입니다. `MODEL_BASE_URL`은 IOP Edge listener를 가리키고, 해당 listener는 non-streaming `POST /v1/responses`를 제공해야 합니다. NomadCode는 OpenAI-compatible request/response shape를 기본 계약으로 유지하고, task/workspace/session/approval/artifact/notification 같은 IOP/NomadCode 전용 실행 문맥은 별도 `iop` wrapper field가 아니라 요청 `metadata` 확장으로 전달하는 방향을 기준으로 합니다. 현재 코드와 local script에는 개발 호환용 direct model endpoint/Ollama 기본값이 남아 있을 수 있지만, 로드맵과 운영 기준의 기본 실행 경로는 IOP 경유 호출입니다. 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` 입니다.
|
|
|
|
`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://<iop-edge-openai-listener>" \
|
|
MODEL_API_KEY="<iop-token-or-local-key>" \
|
|
MODEL_NAME="<iop-model-or-profile>" \
|
|
MODEL_CONTEXT_SIZE="262144" \
|
|
MODEL_TIMEOUT_SEC="300" \
|
|
./bin/run
|
|
```
|
|
|
|
모델 호출은 OpenAI-compatible Responses API의 non-streaming `POST /v1/responses` 형식을 사용합니다. IOP Edge OpenAI-compatible listener를 `MODEL_BASE_URL`로 쓰려면 해당 listener가 `/v1/responses`를 제공해야 합니다. NomadCode의 task/workspace/session 문맥은 OpenAI-compatible 표면을 깨는 별도 top-level wrapper가 아니라 `metadata` 확장으로 전달합니다. direct Ollama 호환 경로에서는 `MODEL_CONTEXT_SIZE`를 Ollama 전용 option인 `options.num_ctx`로 전달하지만, 이 경로는 IOP Responses listener가 준비되기 전의 local development compatibility로만 취급합니다.
|
|
|
|
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` 입니다.
|
|
|
|
```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
|
|
```
|
|
|
|
테스트 실행:
|
|
|
|
```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`를 함께 지정합니다.
|
|
|
|
```bash
|
|
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`이 기록됩니다.
|
|
- **최대 시도 횟수 (Max Attempts)**:
|
|
- River 비동기 작업 큐는 실패한 작업 시도를 최대 `workflow.DefaultTaskMaxAttempts` (기본값: 3회)까지 자동으로 다시 시도합니다.
|
|
- 각 시도가 시작될 때마다 작업 메타데이터의 `attempt` 카운트가 1씩 증가하여 기록되며, 실패 시 메타데이터의 `retryable` 필드에 남은 재시도 가능 여부(`attempt < DefaultTaskMaxAttempts`)가 기록됩니다.
|
|
- **재-Enqueue 가능 여부 (Re-Enqueue Behavior)**:
|
|
- 최종적으로 실패 상태(`failed`)인 작업은 다시 `queued`로 Enqueue하여 다시 처음부터 실행할 수 있습니다.
|
|
- 반면 완료(`completed`) 또는 취소(`canceled`)된 터미널 상태의 작업은 다시 시작(Restart/Re-enqueue)할 수 없습니다.
|