---
spec_doc_type: spec
spec_id: runtime/edge-node-execution
status: 부분
source_evidence:
  - type: contract
    path: agent-contract/inner/edge-node-runtime-wire.md
    notes: Edge-Node register, run stream, cancel, node command, config refresh wire 계약
  - type: code
    path: proto/iop/runtime.proto
    notes: RunRequest, RunEvent, CancelRequest, NodeCommandRequest, RegisterRequest, NodeConfigPayload 원문
  - type: code
    path: apps/edge/internal/transport/server.go
    notes: Edge TCP proto-socket server, node register handshake, event relay
  - type: code
    path: apps/edge/internal/service/run_submit.go
    notes: surface-neutral SubmitRun과 direct/queued dispatch
  - type: code
    path: apps/edge/internal/service/provider_tunnel.go
    notes: provider tunnel dispatch와 request-bound frame relay
  - type: code
    path: apps/edge/internal/service/model_queue_release.go
    notes: connection generation fencing, lease 반환, disconnect/reconnect queue 재평가
  - type: code
    path: apps/edge/internal/service/status_provider.go
    notes: configured offline Node/provider snapshot과 dispatch-ready connectivity join
  - type: code
    path: apps/node/internal/bootstrap/runtime_supervisor.go
    notes: initial connect와 established-session reconnect를 공유하는 connectivity supervisor
  - type: code
    path: apps/node/internal/node/run_handler.go
    notes: Node RunRequest 처리와 adapter 실행
  - type: code
    path: apps/node/internal/node/tunnel_handler.go
    notes: Node provider tunnel request 처리와 frame relay
  - type: code
    path: apps/node/internal/adapters/openai_compat/execute.go
    notes: OpenAI-compatible provider 실행 stream과 RuntimeEvent usage 변환
  - type: code
    path: apps/node/internal/adapters/openai_compat/provider_tunnel.go
    notes: OpenAI-compatible provider raw HTTP/SSE tunnel 처리
  - type: code
    path: apps/node/internal/adapters/vllm/vllm.go
    notes: vLLM usage payload의 reasoning/cached token 변환
  - type: test
    path: apps/edge/internal/transport/server_test.go
    notes: Edge transport server 단위 검증
  - type: test
    path: apps/node/internal/node/run_cancel_test.go
    notes: Node run 실행과 cancel 처리 검증
  - type: test
    path: apps/node/internal/node/provider_tunnel_test.go
    notes: Node provider tunnel lifecycle 검증
  - type: test
    path: apps/node/internal/adapters/openai_compat/execute_test.go
    notes: OpenAI-compatible provider stream과 usage breakdown 검증
  - type: test
    path: apps/node/internal/adapters/openai_compat/provider_tunnel_test.go
    notes: OpenAI-compatible provider raw tunnel 검증
  - type: test
    path: apps/node/internal/adapters/vllm/vllm_test.go
    notes: vLLM usage breakdown 검증
  - type: test
    path: apps/edge/internal/bootstrap/reconnect_readiness_integration_test.go
    notes: 실제 iop-node reconnect 뒤 queued waiter의 ready-gated dispatch와 terminal/counter 수렴 검증
  - type: test
    path: apps/edge/internal/service/queue_reservation_test.go
    notes: provider lease exactly-once 반환과 connection generation race 검증
  - type: test
    path: apps/node/internal/bootstrap/module_test.go
    notes: delayed initial connect, unlimited/finite retry, fatal, shutdown과 disabled metrics listener 검증
  - type: test
    path: scripts/dev/edge-node-reconnect-diagnostic.sh
    notes: 별도 Edge·Node 프로세스의 메시지 relay 순서, terminal ordering, reconnect user-flow 검증
---

# 스펙: Edge-Node 실행 경로

## 목적

Edge와 Node 사이에 현재 구현된 실행 기능을 기능 단위로 정리한다. 코드 배치 규칙이나 도메인별 작업 지침은 domain rule을 따른다.

## 기능 목록

| 기능 | 설명 |
|------|------|
| Node token 등록과 dispatch-ready | Node가 `RegisterRequest.token`으로 ownership/config를 받고, config 적용·adapter start·handler 설치 뒤 `NodeReadyRequest`/ack로 dispatch-ready가 된다. |
| Node config payload 전달 | Edge가 token에 매칭되는 node record를 찾아 `NodeConfigPayload`를 `RegisterResponse`에 담아 내려준다. |
| 등록 실패 처리 | unknown token, duplicate connection, config payload build failure를 register response와 node lifecycle event로 표현한다. |
| 실행 요청 전달 | Edge service가 `SubmitRun` 요청을 `RunRequest`로 만들어 선택된 Node에 보낸다. 명시 node가 없고 연결 node가 1개면 single-node fallback을 사용한다. |
| adapter 실행 | Node가 `RunRequest.adapter`로 adapter instance를 찾고 adapter `Execute`를 호출한다. admission은 adapter `Capabilities().MaxConcurrency` 기준이다. |
| 실행 이벤트 스트림 | Node adapter가 낸 start, delta, reasoning_delta, complete, error, cancelled 이벤트를 `RunEvent`로 Edge에 relay한다. |
| provider raw tunnel | Edge가 `ProviderTunnelRequest`를 보내면 Node가 provider HTTP/SSE response를 열고 ordered `ProviderTunnelFrame`으로 status/header/body/end/error/usage 후보를 relay한다. |
| mixed provider dispatch wire | provider-pool model group은 Edge service에서 provider를 먼저 선택한 뒤 OpenAI-compatible provider에는 `ProviderTunnelRequest`, Ollama/CLI/native provider에는 normalized `RunRequest`를 보낸다. |
| provider resource lease | 여러 model key가 같은 provider를 참조해도 Edge가 `node_id + provider_id` lease에서 일반·long capacity를 합산하고 terminal/send 실패/disconnect가 lease를 정확히 한 번 반환한다. |
| Node connectivity supervision | 단일 supervisor가 retryable initial connect 실패와 established-session disconnect를 같은 reconnect policy로 처리하고 local shutdown, fatal 오류, 유한 exhaustion만 terminal로 구분한다. |
| disconnect/reconnect fencing | current dispatch-ready owner의 generation만 provider를 offline/excluded로 만들고 queue를 재평가하며, reconnect ready는 새 generation candidate와 기존 waiter를 즉시 복구한다. |
| adapter-local capacity guard | Node의 normalized 실행과 provider tunnel 실행은 같은 stable adapter instance capacity gate를 공유해 Edge admission 우회 실행도 backend 한도를 넘지 않는다. |
| usage breakdown relay | `RunEvent.usage`와 `ProviderTunnelFrame.usage`는 provider가 보고한 input/output/reasoning/cached input token count를 Edge 관측 계층으로 전달한다. |
| terminal event 합성 | adapter가 terminal event 없이 종료하면 Node가 terminal event를 합성한다. |
| run cancel | Edge가 `CancelRequest`를 보내면 Node run manager가 active run context를 cancel한다. |
| logical session 종료 | adapter가 `SessionTerminator`를 구현한 경우 `TERMINATE_SESSION`으로 session을 종료한다. 모든 adapter 공통 기능은 아니다. |
| Node command | capabilities, transport status, usage status, session list, ollama API 계열 조회/제어성 command를 실행 요청과 분리해 처리한다. |
| Node local run store | Node가 run id, adapter, target, session id, background, status, timestamps, error를 SQLite에 기록한다. |
| Edge event fanout | Edge event bus가 run event와 node lifecycle event를 in-process subscriber에게 fanout한다. |

## 범위

- 포함: Edge-Node TCP/protobuf transport, register handshake, run/cancel/command, provider raw tunnel, Node adapter execution, Edge event bus fanout, Node local run store.
- 제외: OpenAI-compatible/A2A HTTP request shape, Control Plane 운영 wire, provider-pool config refresh 상세, durable global history/audit.

## 주요 흐름

### Node 등록

```mermaid
sequenceDiagram
  participant Node
  participant EdgeTransport as Edge transport
  participant NodeStore as Edge NodeStore

  Node->>EdgeTransport: TCP connect
  Node->>EdgeTransport: RegisterRequest(token)
  EdgeTransport->>NodeStore: token으로 NodeRecord 조회
  alt token valid
    EdgeTransport->>EdgeTransport: NodeConfigPayload 생성, pending ownership claim
    EdgeTransport-->>Node: RegisterResponse(accepted=true, config)
    Node->>Node: config 적용, adapter start, session handler 설치
    Node->>EdgeTransport: NodeReadyRequest(node_id)
    EdgeTransport->>EdgeTransport: current owner를 dispatch-ready로 전환
    EdgeTransport->>EdgeTransport: provider availability 활성화, queued waiter pump, connected event
    EdgeTransport-->>Node: NodeReadyResponse(ready=true)
  else token invalid or duplicate
    EdgeTransport-->>Node: RegisterResponse(accepted=false, reason)
  end
```

Node process는 이 handshake 바깥에서 단일 connectivity supervisor를 실행한다. retryable initial dial/register 실패와 session disconnect는 같은 bounded cadence로 재시도하고, 명시적 `reconnect.max_attempts=0`은 local shutdown까지 unlimited로 동작한다.

### 실행 요청과 이벤트

```mermaid
sequenceDiagram
  participant Caller
  participant EdgeService as Edge service
  participant EdgeTransport as Edge transport
  participant Node
  participant Adapter

  Caller->>EdgeService: SubmitRun(adapter, target, input)
  EdgeService->>EdgeService: Node 선택
  EdgeService->>EdgeTransport: RunRequest
  EdgeTransport->>Node: RunRequest
  Node->>Node: adapter instance resolve
  Node->>Adapter: Execute(spec)
  Adapter-->>Node: RuntimeEvent(delta/start/complete)
  Node-->>EdgeTransport: RunEvent
  EdgeTransport-->>Caller: run stream
```

### Provider raw tunnel

```mermaid
sequenceDiagram
  participant OpenAI as Edge OpenAI surface
  participant EdgeService as Edge service
  participant Node
  participant Provider

  OpenAI->>EdgeService: SubmitProviderTunnel
  EdgeService->>Node: ProviderTunnelRequest
  Node->>Provider: HTTP/SSE request
  Provider-->>Node: status/header/body
  Node-->>EdgeService: ProviderTunnelFrame sequence
  EdgeService-->>OpenAI: request-bound frame stream
```

### 취소와 session 종료

```mermaid
sequenceDiagram
  participant EdgeService as Edge service
  participant Node
  participant Adapter

  alt cancel run
    EdgeService->>Node: CancelRequest(CANCEL_RUN, run_id)
    Node->>Node: active run context cancel
  else terminate session
    EdgeService->>Node: CancelRequest(TERMINATE_SESSION, adapter, target, session_id)
    Node->>Adapter: TerminateSession(target, session_id)
  end
```

## 계약

- `iop.edge-node-runtime-wire`: `agent-contract/inner/edge-node-runtime-wire.md`
- proto 원문: `proto/iop/runtime.proto`

## 설정/데이터/이벤트

- Edge의 node source of truth는 `configs/edge.yaml`과 `packages/go/config`의 `nodes[]` 구조다.
- accepted registration은 duplicate ownership claim과 config 전달만 담당한다. ready ack 전 Node는 direct/provider-pool dispatch, provider tunnel/command, config refresh push, connected snapshot/event에서 제외된다.
- Edge registry의 connection generation은 internal fence이며 wire/config로 노출하지 않는다. current client의 첫 ready만 provider resource activation과 queue pump를 수행하고, duplicate ready는 idempotent ack, stale/rejected ready는 reject로 처리한다.
- current owner disconnect는 event bus와 분리된 authoritative service 경로에서 해당 generation의 provider lease를 exactly-once 반환하고 resource를 offline으로 fence한 뒤 모든 model group waiter를 live candidate로 재평가한다. 후보가 없어진 waiter는 queue timeout을 기다리지 않고 unavailable로 끝난다.
- configured Node/provider는 연결이 끊겨도 snapshot catalog에서 사라지지 않는다. Node는 `connected=false`, enabled provider는 `status=unavailable`, `health=offline`, effective capacity/counter 0으로 보이며 ready reconnect 뒤 새 generation의 configured capacity가 복구된다.
- `reconnect.max_attempts` 생략은 `10`, 명시적 `0`은 unlimited, 양수는 유한 limit이다. unlimited mode는 양수 `interval_sec`가 필요하고 생략값은 `10`이다. fatal config/credential 오류와 유한 exhaustion은 non-zero terminal, local shutdown은 정상 종료다.
- `RunEvent`는 adapter execution stream이고, `EdgeNodeEvent`는 node lifecycle/control event다.
- `ProviderTunnelFrame.body`는 OpenAI-compatible provider passthrough의 source of truth이며 `RunEvent.delta`나 Edge event bus payload로 보내지 않는다.
- `ProviderTunnelFrame.usage`와 `metadata`는 관측 후보이며 pure passthrough body에 합쳐지지 않는다.
- provider-pool mixed dispatch에서 `ProviderTunnelRequest`와 `RunRequest` 중 어느 wire를 사용할지는 selected provider capability에서 파생되며, client request metadata selector로 결정하지 않는다.
- `Usage.reasoning_tokens`와 `Usage.cached_input_tokens`는 provider가 별도 보고한 경우에만 채워지는 optional breakdown이다.
- Node local DB는 기본 `file:iop.db?cache=shared&mode=rwc`로 열린다.
- heartbeat는 Edge와 Node transport 양쪽에서 2초 interval, 5초 wait 기준을 사용한다. 정상적인 프로세스·OS 종료는 transport close로 즉시 감지하고, heartbeat timeout은 종료 신호가 오지 않는 전원 차단·네트워크 단절의 fallback으로 사용한다.

## 검증

- `go test ./apps/edge/internal/transport ./apps/edge/internal/service ./apps/edge/internal/node`
- `go test ./apps/node/internal/transport ./apps/node/internal/node ./apps/node/internal/router ./apps/node/internal/adapters ./apps/node/internal/store`
- `go test ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/vllm`
- `go test ./apps/edge/internal/bootstrap -run '^TestActualNodeReconnectReadyPumpsQueuedWaiterExactlyOnce$'` - 실제 `iop-node` 재연결 뒤 기존 provider-pool waiter의 dispatch 1회, terminal 1회, counter 0 수렴을 확인한다.
- `./scripts/e2e-provider-capacity-smoke.sh` - loopback provider에서 두 model alias가 capacity 1 resource를 공유하고 final normal/long counter가 0으로 회복하는지 확인한다.
- `make test-e2e` - Edge-Node와 OpenAI 보조 smoke를 함께 실행한다. runtime path 변경 시 사용자 흐름 검증을 대체하지 않는다.

## 한계와 주의사항

- mTLS helper는 존재하지만 현재 Edge-Node transport 설정에는 연결되어 있지 않다.
- `TERMINATE_SESSION`은 모든 adapter에 공통으로 보장되는 기능이 아니다.
- Node store는 전역 query/audit API가 아니다. 상위 운영 이력은 별도 설계가 필요하다.
- provider raw tunnel은 기존 socket 위 request-bound stream이다. 별도 Node stream server나 Edge의 provider direct access 경로가 아니다.
- usage breakdown은 provider-reported 값만 전달한다. provider가 보고하지 않은 reasoning token을 Node나 Edge가 추정하지 않는다.

## 변경 기록

- 2026-07-07: 현재 코드, 계약, README 기준으로 bootstrap spec 작성.
- 2026-07-07: 기능 목록 중심으로 축소하고 주요 흐름을 Mermaid sequence diagram으로 정리.
- 2026-07-08: Provider raw tunnel 실행 흐름과 passthrough event/data 경계를 현재 계약 기준으로 반영.
- 2026-07-10: `RunEvent.usage`/`ProviderTunnelFrame.usage`의 input/output/reasoning/cached input breakdown 전달 기준을 반영.
- 2026-07-12: Model Group Mixed Provider Dispatch 종료 검토 기준으로 selected provider capability에서 파생되는 `ProviderTunnelRequest`/`RunRequest` 분기 경계를 반영.
- 2026-07-18: 저장소 구조 분해 뒤 Edge run/tunnel, Node handler, adapter split test의 `source_evidence`를 현재 경로로 동기화.
- 2026-07-22: accepted registration을 pending ownership/config 단계로 제한하고, handler 설치 뒤 `NodeReadyRequest`/ack로 dispatch eligibility와 reconnect waiter pump를 여는 순서를 반영.
- 2026-07-22: provider resource lease, connection generation fencing, initial/장기 reconnect supervision, offline snapshot과 adapter-local capacity guard를 현재 구현·계약·회귀 테스트 기준으로 동기화.
