- antigravity 기반 CLI status adapter 구현 (gemini 대체) - milestone 파일명 통일 (接두어 제거) - agent-ops roadmap/current.md 갱신 - edge/node README 및 설정 문서 업데이트 - e2e smoke 스크립트 개선
165 lines
9.1 KiB
Markdown
165 lines
9.1 KiB
Markdown
# node — Node Agent
|
|
|
|
디바이스당 1개 실행되는 IOP 노드 에이전트.
|
|
Edge에 연결되어 `adapter + target` 실행을 수행하고, TCP/protobuf 소켓을 통해 요청, 명령, 이벤트 스트림을 처리한다. mTLS helper는 `packages/auth`에 있지만 현재 Edge-Node transport에는 아직 연결하지 않았다.
|
|
|
|
## 내부 구조 (Hexagonal Architecture)
|
|
|
|
```
|
|
cmd/node/ — CLI 진입점 (cobra)
|
|
internal/
|
|
bootstrap/ — fx 의존성 주입 모듈
|
|
node/ — 핵심 노드 서비스 (transport.Handler 구현)
|
|
runtime/ — 도메인 타입: ExecutionSpec, RuntimeEvent, 인터페이스
|
|
router/ — RunRequest → ExecutionSpec 라우팅
|
|
transport/ — Edge로 연결하는 TCP/protobuf client와 session 처리
|
|
adapters/
|
|
mock/ — 에코 테스트 어댑터
|
|
ollama/ — Ollama /api/chat 스트리밍 어댑터
|
|
vllm/ — vLLM OpenAI-compatible 어댑터 skeleton
|
|
cli/ — CLI 프로세스 어댑터 (claude/antigravity/codex/opencode/cline)
|
|
store/ — SQLite 실행 이력
|
|
```
|
|
|
|
## 실행
|
|
|
|
```bash
|
|
# 빌드
|
|
go build -o bin/node ./apps/node/cmd/node
|
|
|
|
# 버전 확인
|
|
./bin/node version
|
|
|
|
# 설정 파일 로드 검증
|
|
./bin/node config check --config configs/node.yaml
|
|
|
|
# 설정 확인
|
|
./bin/node config print --config configs/node.yaml
|
|
|
|
# 서버 실행
|
|
./bin/node serve --config configs/node.yaml
|
|
```
|
|
|
|
## 호스트 환경 준비 (setup)
|
|
|
|
운영 설치 공식 경로는 `setup` 명령으로 일원화한다.
|
|
|
|
```bash
|
|
sudo iop-node setup --enable --start
|
|
```
|
|
|
|
배포 전에 결과를 검토하려면 `--dry-run`을 쓴다.
|
|
|
|
```bash
|
|
sudo iop-node setup --dry-run --binary /usr/local/bin/iop-node
|
|
```
|
|
|
|
`setup`은 다음을 담당한다.
|
|
|
|
- 실행 user/group(`iop`) 준비
|
|
- `/etc/iop`와 `/var/lib/iop/node` 디렉터리 준비
|
|
- 설정 파일이 없을 때만 기본 템플릿 생성 (`--overwrite-config`로 강제 갱신)
|
|
- systemd unit 생성 또는 갱신 (`/etc/systemd/system/iop-node.service`)
|
|
- `systemctl daemon-reload`
|
|
- 옵션에 따른 `--enable`, `--start`, `--restart`
|
|
|
|
`setup`의 `--config` 기본값은 `/etc/iop/node.yaml`이다. 개발용 명령(`serve`, `config print/check`)의 root persistent `--config` 기본값(`configs/node.yaml`)과는 다르다.
|
|
|
|
현재 구현된 node CLI 표면은 다음과 같다.
|
|
|
|
```text
|
|
iop-node serve
|
|
iop-node setup
|
|
iop-node config print
|
|
iop-node config check
|
|
iop-node version
|
|
```
|
|
|
|
원격 edge에 붙는 수동 테스트는 `configs/node.yaml`의 `transport.edge_addr`와 `transport.token`을 먼저 맞춘 뒤 repo root에서 실행한다.
|
|
|
|
```bash
|
|
./bin/node.sh
|
|
```
|
|
|
|
`bin/node.sh`는 `configs/node.yaml`의 `transport.edge_addr` 포트가 열릴 때까지 최대 30초 기다린 뒤 node를 시작한다. 테스트 중 edge 주소만 임시로 바꾸려면 `IOP_EDGE_ADDR=host:port ./bin/node.sh`를 사용할 수 있다.
|
|
|
|
edge 예시 설정은 cli adapter의 `opencode` profile을 사용한다. `configs/edge.yaml`의 `console.target`을 `claude`, `claude-tui`, `antigravity`, `codex`, `opencode`, `cline-dgx` 같은 profile 이름으로 바꾸면 같은 실행 파이프라인에서 다른 CLI target을 검증할 수 있다.
|
|
|
|
edge에서 실행 요청을 받으면 node는 해당 입력을 선택된 adapter target으로 전달하고, adapter가 emit한 delta/event를 Edge로 스트리밍한다.
|
|
|
|
```text
|
|
[edge-message] hello
|
|
[node-event] start run_id=manual-...
|
|
[node-event] complete run_id=manual-... detail="opencode sse execution complete"
|
|
[node-message] <adapter output>
|
|
```
|
|
|
|
## Edge-Node 메시지 경계
|
|
|
|
node는 같은 TCP/protobuf session 위에서 여러 메시지 계열을 처리한다.
|
|
|
|
- `RunRequest`: adapter execution 시작. CLI adapter에서는 prompt 전달로 해석된다.
|
|
- `CancelRequest`: 실행 취소 또는 logical session 종료.
|
|
- `NodeCommandRequest`: 실행이 아닌 node/adapter 조회성 명령. 현재 `USAGE_STATUS`, `CAPABILITIES`, `SESSION_LIST`, `TRANSPORT_STATUS`를 지원한다.
|
|
- `RunEvent`: adapter execution stream. `start`, `delta`, `complete`, `error`, `cancelled` 같은 실행 이벤트를 edge로 보낸다.
|
|
- `EdgeNodeEvent`: edge-node lifecycle/control event envelope. edge 연결 해제 등 실행 스트림이 아닌 이벤트를 다룬다.
|
|
|
|
즉 CLI에 메시지를 보내는 요청 파이프라인과 이벤트 파이프라인은 논리적으로 분리되어 있고, 물리 transport만 공유한다.
|
|
|
|
## Logical Session (transport 1개 · session 여러 개)
|
|
|
|
edge-node transport 연결은 **호스트당 1개**를 유지한다. 그 연결 위에서 CLI adapter는 `session_id`가 다른 여러 장수 worker process를 독립적으로 관리한다.
|
|
|
|
| 개념 | 설명 |
|
|
|---|---|
|
|
| transport 연결 | edge-node 호스트 쌍당 1개 TCP 연결 |
|
|
| logical session | `(adapter, target, session_id)` 로 식별되는 장수 worker process |
|
|
| run | session 위에서 실행되는 단일 요청 |
|
|
|
|
**cancel vs terminate:**
|
|
|
|
- `CancelAction_CANCEL_RUN` (기본값): 현재 실행 중인 run만 중단. session process는 살아있다.
|
|
- `CancelAction_TERMINATE_SESSION`: session process를 명시적으로 종료. 이후 같은 `session_id`로의 요청은 새 process를 만들거나(`CREATE_IF_MISSING`) 에러를 반환한다(`REQUIRE_EXISTING`).
|
|
|
|
**session_mode:**
|
|
|
|
- `RUN_SESSION_MODE_CREATE_IF_MISSING` (기본값): session이 없으면 새로 생성.
|
|
- `RUN_SESSION_MODE_REQUIRE_EXISTING`: session이 없으면 에러 반환. 새 process 생성 금지.
|
|
|
|
## Node Commands
|
|
|
|
node는 adapter execution(`RunRequest`) 외에도 edge가 보내는 `NodeCommandRequest`를 처리한다.
|
|
현재 구현된 command:
|
|
- `USAGE_STATUS`: 선택된 `adapter/target` (예: `cli/codex`)의 사용량 한도와 초기화 시간을 조회한다.
|
|
- Codex의 경우 TUI를 시작하고 `/status` 명령을 전송한 뒤 출력된 `% left` 정보를 파싱해 `AgentUsageStatus`로 반환한다.
|
|
- 그 외 adapter/target은 지원하지 않는 경우 명시적 에러를 반환한다.
|
|
- `CAPABILITIES`: 요청한 adapter의 capability를 조회한다. 현재 응답은 adapter 이름, target 목록, max_concurrency를 포함한다.
|
|
- `SESSION_LIST`: CLI adapter가 관리하는 logical session 목록을 `mode:target/session_id` 문자열로 조회한다.
|
|
- `TRANSPORT_STATUS`: node 관점의 edge 연결 여부와 요청 echo fields(node_id, adapter, target, session_id)를 조회한다. heartbeat 상세 카운터는 현재 응답에 포함되지 않는다.
|
|
|
|
## 어댑터
|
|
|
|
| 어댑터 | 설명 | 상태 |
|
|
|---|---|---|
|
|
| `mock` | 입력 에코, 스트리밍 테스트용 | 구현 완료 |
|
|
| `ollama` | 로컬 Ollama `/api/chat` 스트리밍 연동 | 기본 구현 완료 |
|
|
| `vllm` | vLLM OpenAI-compatible API | `/v1/models` 조회 skeleton |
|
|
| `cli` | claude/antigravity/codex/opencode/cline CLI 실행 | 구현 완료 |
|
|
|
|
Ollama adapter는 edge에서 받은 내부 target을 Ollama model 이름으로 사용한다. Edge OpenAI-compatible HTTP API에서 `openai.target`을 지정하면 외부 `model` 값과 무관하게 해당 Ollama model로 라우팅하고, `openai.target`을 비우면 요청의 `model` 값을 그대로 내부 target으로 전달한다. `adapters.ollama.context_size`가 설정되어 있으면 Ollama `/api/chat` 요청의 `options.num_ctx`로 전달한다.
|
|
|
|
`claude`, `antigravity`, `codex`, `opencode`, `cline`처럼 기본이 interactive TUI인 CLI는 기본적으로 non-interactive 모드로 설정한다. 예: `claude -p --dangerously-skip-permissions`, `agy --dangerously-skip-permissions --print <prompt>`, `codex exec --dangerously-bypass-approvals-and-sandbox`, `cline -y --json --config /config/.cline/profiles/ollama-dgx <prompt>`.
|
|
|
|
Claude는 용도별로 두 profile을 둔다. `claude`는 `claude -p` 기반 one-shot profile이며 Agent SDK/credit 경로 검증용으로 유지한다. `claude-tui`는 `persistent: true`, `terminal: true`, `mode: "persistent-lazy"`로 일반 Claude Code TUI를 첫 요청 시 lazy start한다. TUI 출력은 structured JSON이 아니므로 idle timeout 기반으로 completion을 판단한다.
|
|
|
|
`opencode`는 profile `mode: "opencode-sse"`로 설정해 `opencode serve`의 HTTP/SSE 인터페이스를 사용한다. `profile.Command`가 가리키는 opencode 바이너리를 `serve --hostname 127.0.0.1 --port 0`으로 띄우고 `/global/event` SSE에서 text delta를 `RuntimeEvent` delta로 relay한다. 외부에서 이미 실행 중인 server에 연결하려면 `--attach <url>`을 args에 추가한다. legacy `output_format: "opencode-json"` stdout JSONL 경로는 `opencode run --format json` 사용 시에만 의미가 있다.
|
|
|
|
Cline은 mutable `--config` 디렉터리를 프로필처럼 나눈다. `/config/.cline/profiles/ollama-dgx`와 `/config/.cline/profiles/ollama-m1`는 `opencode`의 `ollama-dgx`/`ollama-m1` provider 설정을 Cline의 `ollama` provider 설정으로 변환한 값이다.
|
|
|
|
## Transport
|
|
|
|
- TCP 4-byte length-prefix framing
|
|
- protobuf message framing via `common-proto-socket`
|
|
- edge 연결 해제는 `EdgeNodeEvent`로 로컬 표시되며, 실행 스트림 `RunEvent`와 분리한다.
|
|
- mTLS helper는 존재하지만 현재 transport 설정에는 아직 연결되지 않음
|
|
- 하트비트: 30초 간격
|