# Edge-Node Runtime Wire Contract

## 계약 메타

- id: `iop.edge-node-runtime-wire`
- boundary: `inner`
- status: active
- 원본 경로:
  - `proto/iop/runtime.proto`
  - `apps/edge/internal/transport/server.go`
  - `apps/node/internal/transport/client.go`
  - `apps/node/internal/transport/session.go`
  - `apps/node/internal/transport/parser.go`
  - `apps/node/internal/bootstrap/runtime_supervisor.go`
  - `apps/edge/internal/transport/connection_handlers.go`
  - `apps/edge/internal/service/model_queue_release.go`
  - `apps/edge/internal/service/status_provider.go`
  - `apps/edge/internal/node/mapper.go`
  - `apps/node/internal/adapters/config_set.go`
- human docs:
  - `apps/edge/README.md`
  - `apps/node/README.md`

## 읽는 조건

- Edge-Node TCP/protobuf transport, initial/reconnect supervision, register/dispatch-ready handshake, connection generation fencing, run stream, provider raw tunnel, cancel, node command, node config refresh를 바꿀 때
- `NodeReadyRequest`, `NodeReadyResponse`, `RunRequest`, `RunEvent`, `ProviderTunnelRequest`, `ProviderTunnelFrame`, `CancelRequest`, `NodeCommandRequest`, `NodeCommandResponse`, `NodeConfigPayload`, `NodeConfigRefresh*` 필드를 바꿀 때
- node adapter 설정 payload나 runtime config가 Edge에서 Node로 전달되는 방식을 바꿀 때

## 범위

이 계약은 Edge와 Node 사이의 내부 TCP proto-socket 경계다.
Edge는 Node 연결을 수락하고, Node는 연결 직후 등록 요청을 보낸다.
실행 요청과 이벤트 스트림, 취소, 조회성 명령, 설정 refresh는 같은 내부 wire 계열에서 처리한다.

## 주요 흐름

- register와 readiness: Node가 `RegisterRequest`를 보내고 Edge가 `RegisterResponse`로 수락 여부와 `NodeConfigPayload`를 돌려준다. accepted registration은 Node ID의 현재 ownership을 pending으로 claim할 뿐 dispatch 가능 상태가 아니다. Node는 config 적용, adapter start, session handler 설치 뒤 `NodeReadyRequest(node_id)`를 보내고, Edge가 current owner를 dispatch-ready로 전환한 뒤 `NodeReadyResponse`로 ack한다. 이 ready ack 전에는 run, provider tunnel, command, config-refresh push와 connected availability/event가 열리지 않는다.
- connectivity supervision: Node daemon은 Fx startup 전에 원격 연결 성공을 요구하지 않고 단일 supervisor goroutine이 initial dial과 established-session reconnect를 같은 policy로 직렬 처리한다. retryable 원격 실패는 재시도하고 local config/credential fatal error, 유한 retry exhaustion, local shutdown만 process terminal로 구분한다.
- disconnect/reconnect: current dispatch-ready owner의 close/heartbeat timeout만 해당 connection generation을 fence한다. Edge는 같은 authoritative lifecycle에서 provider lease를 정확히 한 번 반환하고 resource를 offline/excluded로 만든 뒤 queue를 live candidate 기준으로 재평가한다. accepted Node의 ready transition은 새 generation resource를 활성화하고 기존 waiter를 즉시 pump한다. stale/rejected connection callback은 live state나 lifecycle event를 바꾸지 않는다.
- execution: Edge가 `RunRequest`를 보내고 Node가 `RunEvent` stream으로 실행 상태를 보낸다.
- provider raw tunnel: Edge가 기존 Edge-Node socket으로 `ProviderTunnelRequest`를 보내고 Node가 provider HTTP/SSE 요청을 연 뒤 `ProviderTunnelFrame` stream으로 provider status/header/body/end/error/usage 후보를 sequence와 함께 돌려준다. 이 경로는 OpenAI-compatible provider passthrough용이며 `RunEvent` 실행 stream과 분리된다.
- provider-pool mixed dispatch: Edge service는 model group provider candidate를 선택한 뒤, 같은 selected provider/queue lease로 OpenAI-compatible provider에는 `ProviderTunnelRequest`, Ollama/CLI/native provider에는 normalized `RunRequest`를 보낸다. Edge-Node wire는 client-provided response path selector를 받지 않고, provider type만으로 후보를 제외하지 않는다.
- cancel: Edge가 `CancelRequest`를 보내며 `CANCEL_RUN`과 `TERMINATE_SESSION`을 구분한다.
- command: Edge가 `NodeCommandRequest`를 보내고 Node가 `NodeCommandResponse`로 usage/capabilities/session/transport/provider 상태를 응답한다.
- refresh: Edge가 `NodeConfigRefreshRequest`로 새 config payload를 보내고 Node가 `NodeConfigRefreshResponse`로 적용/재시작 필요/실패를 응답한다.

## 필드 의미

- `RunRequest.adapter`, `RunRequest.target`: 내부 실행 식별자다. 외부 OpenAI-compatible `model`은 Edge 입력 표면에서 이 둘로 변환되어야 한다.
- `RunRequest.workspace`: CLI agent route 같은 workspace-bound 실행의 작업 디렉터리다.
- `RunRequest.input`: adapter가 해석할 실행 입력이다. CLI 실행에서는 prompt 계열 입력으로 변환된다.
- `RunRequest.metadata`: caller-defined 실행 metadata다. workspace 자체는 별도 `workspace` 필드로 전달한다.
- `RunEvent.type`: `start`, `delta`, `complete`, `error`, `cancelled` 같은 실행 이벤트 종류다.
- `ProviderTunnelRequest`: 기존 Edge-Node socket 위에서 provider HTTP request를 열기 위한 요청이다. `adapter`, `target`, `method`, `path`, `headers`, `body`, `stream`, `timeout_sec`, `metadata`, `session_id`를 싣되 normalized adapter execution인 `RunRequest`와 분리된다. 외부 caller의 response selector를 전달하지 않으며, 경로는 Edge가 `model`로 선택한 provider capability에 의해 결정된다.
- `ProviderTunnelFrame`: Node가 provider response를 Edge로 돌려주는 ordered frame이다. `kind`, `sequence`, `status_code`, `headers`, `body`, `end`, `error`, `usage`, `metadata`를 싣는다. `body`는 passthrough source of truth이며 `RunEvent.delta`나 Edge `events.Bus` fanout payload로 보내지 않는다. `usage`와 `metadata`는 Edge의 metric/log/known-key 관측 후보이고 provider passthrough body에 합쳐지지 않는다.
- tunnel cancellation: HTTP caller disconnect, response wait timeout, 또는 Edge write failure가 발생하면 Edge는 같은 run id에 대한 `CancelRequest(CANCEL_RUN)`을 보내 upstream provider request 중단을 요청한다. Node adapter는 provider request context cancellation을 관측하고 ordered error/end semantics를 유지해야 한다.
- `RunEvent.metadata["openai_tool_calls"]`: OpenAI-compatible provider adapter가 native `tool_calls`를 반환했을 때 완료 이벤트에 싣는 JSON 배열이다. Edge OpenAI-compatible 표면은 이 값을 `message.tool_calls` 또는 stream `delta.tool_calls`로 복원한다. provider assistant content 텍스트를 이 값으로 파싱/합성하지 않는다.
- `RunEvent.metadata["openai_text_tool_fallback"]`: OpenAI-compatible provider adapter가 backend native tool API 거부 후 `tools`/`tool_choice`를 제거하고 text tool-call instruction으로 재시도했을 때 `"true"`를 싣는다. 이 instruction은 backend가 system role 위치를 거부하지 않도록 leading system message에 병합한다. Edge는 이 표시가 있는 실행에서만 assistant content의 text tool-call을 OpenAI-compatible `tool_calls`로 복원할 수 있다.
- `NodeCommandRequest.type`: 실행이 아닌 조회/제어성 명령이다. adapter execution 요청과 섞지 않는다.
- `NodeConfigPayload.adapters`: Edge가 Node에 내려주는 adapter instance 설정이다.
- `NodeReadyRequest.node_id`: `RegisterResponse`가 돌려준 Node identity다. Edge registry의 internal connection generation은 이 wire/config field로 노출하지 않으며, Edge는 `(node_id, current client)` ownership 비교로 stale ready를 거부한다.
- `NodeReadyResponse.ready`: current pending owner의 첫 ready transition과 이미 ready인 같은 owner의 duplicate ready에서 true다. 첫 transition만 provider resource activation, stranded provider-pool waiter pump, `node.connected` event를 만든다. stale/superseded/rejected connection은 false와 reason을 받고 session을 닫아 reconnect해야 한다.
- `AdapterConfig.name`: node 내부 stable adapter instance identity다. 비어 있으면 legacy single-instance type 이름과 동등하다.
- `NodeRuntimeConfig.concurrency`: legacy compatibility runtime metadata다. 실행 admission은 이 값을 node-wide global gate로 사용하지 않고 provider/resource capacity를 기준으로 한다. Node store 위치나 CLI 실행 작업 디렉터리는 이 runtime payload에 싣지 않는다.
- `reconnect.interval_sec`, `reconnect.max_attempts`: initial connect와 established-session reconnect에 공통 적용된다. 명시적 `max_attempts=0`은 local shutdown까지 unlimited, 생략은 기본값 `10`, 양수는 정확한 유한 attempt limit, 음수는 validation error다. unlimited mode의 `interval_sec`는 양수여야 하며 생략은 기본값 `10`을 사용한다. 유한 exhaustion과 non-retryable 오류는 exit code 1, local shutdown은 정상 종료다.
- `ProviderSnapshot`: legacy wire name을 유지하지만 Node 아래 resource/provider 상태 snapshot으로 해석한다. `category`가 `api`, `cli`, `local_inference` resource kind를 나타내며, provider-pool dispatch 대상은 Edge config `models[].providers`가 참조한 resource뿐이다. `in_flight`와 `long_in_flight`는 `node_id + provider_id` lease state의 현재 점유다. `queued`는 Edge queue에서 해당 provider를 live candidate로 포함하는 고유 pending request 수이고 `long_queued`는 그중 long request 수이므로 여러 provider snapshot에 같은 request가 candidate pressure로 나타날 수 있다.
- configured Node가 disconnected/pending이면 Node snapshot은 `connected=false`를 유지하고 provider catalog entry도 남는다. enabled provider의 effective snapshot은 `status=unavailable`, `health=offline`, capacity/in-flight/queued/long-context 관련 수치가 모두 0이다. reconnect ready 뒤에는 같은 resource identity의 새 generation으로 configured capacity와 admission eligibility가 복구된다.
- Node adapter instance는 normalized `RunRequest`와 `ProviderTunnelRequest`가 공유하는 local capacity gate를 사용한다. 이 gate는 Edge provider lease를 복제하는 분산 admission이 아니라 Edge queue를 우회한 실행으로부터 같은 backend를 보호하는 defense-in-depth다.

## 금지 사항

- Control Plane이나 Client가 Node에 직접 연결하거나 직접 스케줄링하는 계약을 만들지 않는다.
- 외부 API의 `model` 용어를 이 내부 경계의 대표 실행 식별자로 되살리지 않는다.
- `proto/gen/iop/*.pb.go` 생성물을 직접 수정하지 않는다.
- transport handler에서 console/HTTP/A2A 표면 응답을 직접 만들지 않는다. 표면별 변환은 Edge service/input 계층에 둔다.
- Node address, token, transport internals를 Control Plane status 계약으로 노출하지 않는다.
- accepted registration만으로 Node를 dispatch candidate, connected snapshot/event 또는 config refresh recipient로 취급하지 않는다.
- provider lease 반환, generation fencing, queue settlement 같은 correctness 전이를 drop 가능한 node event fanout의 성공에 의존시키지 않는다.
- OS service/Task Scheduler restart를 retryable initial connect 또는 장기 outage 복구의 correctness owner로 사용하지 않는다.

## 변경 시 확인할 코드/테스트

- `proto/iop/runtime.proto`
- `apps/edge/internal/transport/*_test.go`
- `apps/node/internal/transport/*_test.go`
- `apps/edge/internal/bootstrap/reconnect_readiness_integration_test.go`
- `apps/edge/internal/openai/provider_dispatch_test.go`
- `apps/edge/internal/openai/provider_selection_test.go`
- `apps/edge/internal/openai/provider_tunnel_test.go`
- `apps/edge/internal/openai/cancellation_routes_test.go`
- `apps/node/internal/adapters/openai_compat/*_test.go`
- `apps/node/internal/adapters/vllm/*_test.go`
- `apps/edge/internal/node/mapper_test.go`
- `apps/node/internal/adapters/config_set_test.go`
- `apps/node/internal/adapters/adapters_blackbox_test.go`
- proto 변경 시 `make proto`, Client가 소비하면 `make proto-dart`
