Files
hanarang-rails/.plans/design/hierarchy.md
이랑이 a88323716d feat(stage-1-2): hierarchical sub-agent team — sister-agent + SubTask tracking
Stage 1 + 2 통합 구현 — 사용자 피드백 반영:
  manager / principal / lead / junior 계층 구조 추가.
  부장이 복잡도를 판단해서 팀을 동적으로 꾸린다.

Design:
- .plans/design/hierarchy.md — 전체 설계 문서 (점수/tier/plan/escalation)

Prisma schema:
- SubTask (계층 트리 + 역할/모델/state/complexity)
- SubTaskEvent (JSONL 스타일 이벤트 로그)

rails (core):
- src/hierarchy/roles.ts — 4역할 기본 config + 모델 매핑
  manager/principal: gpt-5.4 / glm-5.1
  lead: gpt-codex-5.3 / glm-5
  junior: glm-5-turbo / gpt-5
- src/hierarchy/complexity.ts — 규칙 기반 스코어러 (7 factors, 0-100)
- src/hierarchy/planner.ts — tier → DecompositionPlan (trivial/simple/moderate/complex/massive)
  + concurrency budget 강제
- src/hierarchy/store.ts — Prisma CRUD + tree builder
- src/handoff/http-transport.ts — 실제 HTTP transport (MockTransport 대체)
- src/handoff/build.ts — config + env 기반 transport 빌더
  (env RAILS_TRANSPORT_MODE + RAILS_AGENT_{STAGE}_HOST 오버라이드)
- src/server/http.ts — sub-task 엔드포인트 4개 추가
    POST /api/sub-tasks
    PATCH /api/sub-tasks/:id
    POST /api/sub-tasks/:id/events
    GET /api/pipelines/:id/sub-tasks (tree view)

sister-agent (new sub-project):
- sister-agent/ — 각 LXC 에 배포될 Node.js 데몬
- src/types.ts, roles.ts, complexity.ts, planner.ts
- src/spawn.ts — executeInvocation: 복잡도 점수 → plan → spawn 트리
  simulation mode (현재는 work 를 delay 로 시뮬레이트, LLM 연동은 후속)
- src/rails-client.ts — sub-task / event push
- src/server.ts — POST /invoke 엔드포인트 (port 18801)

LXC 리소스 실측 기반 기본 concurrency:
  104/106/107: 8 concurrent sub-agents
  105 (narang, build 중): 6

검증: tsc --noEmit ✓ | vitest 105/105 ✓ | rails build ✓ | sister-agent build ✓

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:11:16 +09:00

345 lines
11 KiB
Markdown

# Design — Hierarchical Sub-Agent Team (Manager / Principal / Lead / Junior)
> **목적**: 각 stage 의 agent(부장) 가 팀을 꾸려 작업을 분산시키도록 한다.
> 평면 구조 (stage 당 1명) → 조직 구조 (부장 + 수석 + 선임 + 신입).
## 왜 필요한가
현재 rails 는 stage 당 agent 1명이 전부 처리하는 구조. 이건:
- ❌ 병렬성 낭비 — 큰 태스크도 순차 처리
- ❌ 모델 비용 비효율 — 모든 작업을 고가 모델로
- ❌ 결과 품질 저하 — 한 모델이 전략+전술+실행 전부 담당
- ❌ 실제 팀 구조와 미스매치
## 조직 구조
```
manager (부장) — 전략, 최종 승인
└── principal (수석) — 태스크 분해, 기술 리뷰
└── lead (선임) — 실행 리드, 작은 팀 조율
└── junior (신입) — 개별 태스크 실행
```
**엄격한 한 계단씩 아님** — 복잡도에 따라 manager 가 직접 lead 또는 junior 를 바로 spawn 할 수도 있다. 결정은 manager 의 "판단 코드".
## 역할 정의 (`roles.yaml`)
```yaml
hierarchy:
manager:
korean: 부장
responsibilities: [strategy, team-composition, final-approval, escalation-relay]
models:
primary: gpt-5.4
fallback: glm-5.1
can_spawn: [principal, lead, junior] # 복잡도에 따라 직접 spawn 가능
max_spawn_per_call: 4 # 한 번에 최대 4개 팀원
principal:
korean: 수석
responsibilities: [task-decomposition, technical-review, risk-assessment]
models:
primary: gpt-5.4
fallback: glm-5.1
can_spawn: [lead, junior]
max_spawn_per_call: 3
lead:
korean: 선임
responsibilities: [execution-lead, sub-team-coordination, mid-validation]
models:
primary: gpt-codex-5.3
fallback: glm-5
can_spawn: [junior]
max_spawn_per_call: 4
junior:
korean: 신입
responsibilities: [single-task-execution, unit-output]
models:
primary: glm-5-turbo
fallback: gpt-5
can_spawn: []
max_spawn_per_call: 0
# LXC 별 동시 실행 상한 — narang 은 빌드 중이라 보수적
concurrency_limits:
default: 8
overrides:
narang: 6
```
## 복잡도 판단 (Complexity Scoring)
Manager 가 태스크를 받으면 먼저 복잡도 점수(0-100) 를 계산한다. 이 점수로
팀 구성 규모가 결정된다.
### 점수 요소 (Deterministic)
| 요소 | 조건 | 점수 |
|---|---|---|
| **Scope scale** (from description + keywords) | 단일 파일 / "한 줄" | +0~5 |
| | 컴포넌트 1개 / small feature | +5~15 |
| | 여러 파일 / 멀티 모듈 | +15~30 |
| | Sprint 단위 | +30~50 |
| | 전체 프로젝트 / architecture | +50~80 |
| | From scratch / scaffold | +70~100 |
| **Multi-domain** (+5 each, cap +20) | frontend / backend / db / infra / ci / security / test 언급 | max +20 |
| **Risk keywords** (+10 each, cap +30) | migration / breaking / security / auth / data-loss | max +30 |
| **Parallelism hints** (+5 each, cap +15) | "multiple" / "동시에" / "parallel" / "bulk" | max +15 |
| **Uncertainty** | "probably" / "maybe" / "아직 모르겠" | +10 |
| **Estimated LOC** | >500 추정 | +10 |
| **Cross-agent dependency** | 다른 stage 와 명시 연관 | +10 |
### Tier → Decomposition Plan
| Score | Tier | 전략 |
|---|---|---|
| 0-15 | **trivial** | Manager 직접 처리 (spawn X) |
| 16-30 | **simple** | 1 junior |
| 31-50 | **moderate** | 1 lead + 1-2 junior |
| 51-75 | **complex** | 1 principal + 2 lead + 4 junior |
| 76-100 | **massive** | 2 principal + 각자 팀 (병렬 fanout) |
### LLM 보강 (optional)
규칙 기반 점수 + 기본 plan 을 cheap LLM 에게 주고
"이 plan 이 맞는지 / 조정 필요한지" 판단받음. 규칙 + LLM 합의가 최종 plan.
## 상향 에스컬레이션 (Upward Escalation)
하위 역할이 실패하면 **즉시 상위** 로 에스컬레이션 (재시도 아님).
```
junior 실패 (confidence < 0.5 or 명시적 escalate)
→ lead 가 해당 태스크 재수행
→ 또 실패
→ principal
→ 또 실패
→ manager
→ 또 실패
→ rails orchestrator → 사용자
```
같은 역할로 재시도는 resilience retry 가 담당 (Sprint 005).
위 상향 에스컬레이션은 **서로 다른 역할** 로 넘기는 흐름.
## 데이터 모델 (Prisma)
### SubTask
```prisma
model SubTask {
id String @id @db.VarChar(26) // ULID
pipelineId String @db.VarChar(26)
parentId String? @db.VarChar(26) // null = manager 직속
role String @db.VarChar(30) // manager|principal|lead|junior
agentName String @db.VarChar(50) // harang|narang|darang|erang
title String @db.VarChar(500)
description String @db.Text
state String @db.VarChar(30) // queued|running|done|failed|escalated
complexityScore Int?
complexityTier String? @db.VarChar(20)
model String @db.VarChar(50) // 사용 모델
resultJson String? @db.LongText
errorReason String? @db.Text
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
parent SubTask? @relation("SubTaskHierarchy", fields: [parentId], references: [id])
children SubTask[] @relation("SubTaskHierarchy")
events SubTaskEvent[]
@@index([pipelineId, parentId])
@@index([state])
@@index([agentName, state])
}
```
### SubTaskEvent
```prisma
model SubTaskEvent {
id Int @id @default(autoincrement())
subTaskId String @db.VarChar(26)
eventType String @db.VarChar(50) // spawned|started|progress|output|completed|failed|escalated
payloadJson String @db.LongText
timestamp DateTime @default(now())
subTask SubTask @relation(fields: [subTaskId], references: [id], onDelete: Cascade)
@@index([subTaskId, timestamp])
@@index([eventType])
}
```
## Sister-Agent 데몬 (LXC 에 배포)
각 sister LXC (104/105/106/107) 에 Node.js 데몬이 돈다. 포트 **18801** (openclaw-gateway 와 분리).
### 책임
1. rails 로부터 `POST /invoke` 수신
2. 복잡도 점수 계산
3. Decomposition plan 생성
4. sub-agent spawn (OpenClaw 를 경유하거나 LLM 직접 호출)
5. sub-task 이벤트를 rails 에 실시간 push
6. 결과 집계 후 rails 에 HandoffMessage 반환
### 디렉토리 (new sub-project under rails repo)
```
hanarang-rails/
└── sister-agent/
├── package.json
├── tsconfig.json
├── roles.yaml
└── src/
├── server.ts # HTTP /invoke 엔드포인트
├── complexity/
│ ├── scorer.ts # 규칙 기반 점수 계산
│ └── planner.ts # decomposition 전략
├── hierarchy/
│ ├── roles.ts # YAML 로더
│ ├── spawn.ts # openclaw agent spawn wrapper
│ └── escalate.ts # 상향 에스컬레이션
├── reporting/
│ └── rails-client.ts # rails API 로 이벤트 push
└── index.ts
```
### 통신 프로토콜
#### 1. rails → sister-agent: `POST /invoke`
```json
{
"pipelineId": "01HW...",
"contractId": "01HW...",
"stage": "implement",
"task": {
"title": "Sprint 001 — todo app MVP",
"description": "Next.js + Nest.js 로 기본 TODO CRUD",
"workdir": "/home/narang/projects/todo-app"
},
"timeoutMs": 600000,
"railsApiUrl": "http://10.10.10.169:18800"
}
```
#### 2. sister-agent → rails: `POST /api/sub-tasks`
```json
{
"id": "01HW...",
"pipelineId": "01HW...",
"parentId": null,
"role": "manager",
"agentName": "narang",
"title": "root task",
"description": "...",
"complexityScore": 42,
"complexityTier": "moderate",
"model": "gpt-5.4"
}
```
#### 3. sister-agent → rails: `POST /api/sub-tasks/:id/events`
```json
{
"eventType": "spawned",
"payload": { "childId": "01HW..." }
}
```
#### 4. sister-agent → rails: `POST /invoke` 응답 (HandoffMessage)
```json
{
"stage": "implement",
"verdict": "IMPL_DONE",
"payload": {
"branch": "feature/sprint-001",
"commits": ["abc1234"],
"workdir": "/home/narang/projects/todo-app",
"selfTestReport": { "typecheck": "pass" }
},
"errorReason": ""
}
```
## LLM 호출 전략 (현실적)
초기 구현은 **openclaw CLI wrapping** 으로 간다:
```bash
openclaw agent \
--prompt "$(cat prompt.txt)" \
--model gpt-5.4 \
--output json
```
sister-agent 가 sub-process 로 `openclaw agent` 를 호출하고 stdout 을
structured JSON 으로 파싱한다. 이게 안 되면 OpenAI/Z.ai SDK 직접 호출로
fallback.
## 관제 대시보드 연동
대시보드는 `sub_tasks` 테이블을 트리 구조로 렌더링한다:
```
Pipeline 01KNV4...
├── 🦊 하랑 (manager) — planning [45s] [gpt-5.4]
│ ├── principal: 요구사항 분해 [done, 12s] [gpt-5.4]
│ └── lead: Sprint 분해 [done, 18s] [gpt-codex-5.3]
│ ├── junior: SPRINT-001 문서 [done, 4s] [glm-5-turbo]
│ ├── junior: SPRINT-002 문서 [done, 5s] [glm-5-turbo]
│ └── junior: SPRINT-003 문서 [done, 4s] [glm-5-turbo]
├── ⚙️ 나랑 (manager) — implementing [current] [gpt-5.4]
│ ├── principal: 아키텍처 검토 [done, 8s] [glm-5.1]
│ └── lead: 코딩 리드 [running] [gpt-codex-5.3]
│ ├── junior: frontend scaffold [running] [glm-5-turbo]
│ └── junior: backend scaffold [queued] [glm-5-turbo]
```
각 노드 click → 상세 모달 (prompt / output / timing / 모델 / 비용).
## 관찰 가능성 (Observability)
모든 서브태스크 전이가 `SubTaskEvent` 로 기록되고 `POST /api/stream`
(Socket.IO) 을 통해 대시보드에 실시간 푸시된다. SIEM 관점에서:
- `spawned` — 부모 노드 등록
- `started` — 실제 LLM 호출 시작
- `progress` — 중간 출력 (streaming 지원 시)
- `output` — 부분 결과
- `completed` — 성공 종료
- `failed` — 실패 종료 (재시도 대상)
- `escalated` — 상위로 에스컬레이션
## 보안
- sister-agent 가 받는 task 는 rails 에서 HMAC 서명 포함 (nonce 재사용 방지)
- sister-agent ↔ rails 통신은 내부 네트워크 (vmbr1) 한정
- 모델 API 키는 각 sister LXC 의 openclaw 설정에 이미 있음 — sister-agent 는
openclaw CLI 만 wrapping 하면 키 노출 없음
- rails DB 의 `resultJson` 에 비밀이 들어가지 않도록 sister-agent 가 masking
## 참고
- `state-machine.md` — pipeline FSM (stage 단위)
- `handoff.md` — rails ↔ sister (stage 단위 HandoffMessage)
- `retry-policy.md` — 같은 역할 재시도 정책
- `transports.md` — SisterTransport 추상화 (HttpTransport 가 여기 들어감)
## Open questions (Stage 2 에서 결정)
- [ ] Sub-task streaming output 은 SSE 로 할지 Socket.IO 로 할지
- [ ] 모델 비용 트래킹을 SubTask 에 추가할지
- [ ] Token 수 트래킹
- [ ] Escalation 시 부모 sub-task 의 retry count 합산 로직