<!-- task=04+03_a2a_agent_surface plan=0 tag=A2A -->

# IOP Edge Input Surface A2A Plan

## 이 파일을 읽는 구현 에이전트에게

**`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것은 필수 최종 단계다. 이 파일을 채우기 전에는 구현이 완료된 것이 아니다.**
구현 체크리스트를 기준으로 작업하고, 이 plan과 review stub의 모든 구현 체크리스트를 완료하며, 중간/최종 검증을 실행한 뒤 실제 구현 내용과 검증 출력을 `CODE_REVIEW-cloud-G08.md`에 기록한다.
리뷰 파일의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 archive 지시는 실행하지 말고, `코드리뷰 전용 체크리스트`는 수정하거나 체크하지 않는다.

## 배경

NomadCode Core는 A2A JSON-RPC client를 통해 IOP/외부 agent를 호출할 준비가 필요하다. IOP에는 이미 Edge의 OpenAI-compatible 모델 serving 입력 표면이 있으므로, A2A도 별도 축이 아니라 같은 Edge input surface 계열로 관리해야 한다. 이 작업은 OpenAI-compatible 입력과 A2A 입력을 Edge에서 함께 관리하고, 둘 다 내부 `adapter + target` 실행으로 수렴시키는 shared input pipeline을 마련한다.

## 분석 결과

### 읽은 파일

- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-task/03+node_setup_cli/PLAN-cloud-G07.md`
- `README.md`
- `apps/edge/README.md`
- `docs/architecture.md`
- `proto/iop/runtime.proto`
- `apps/edge/internal/service/service.go`
- `apps/edge/internal/service/service_test.go`
- `apps/edge/internal/openai/server.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/edge/internal/events/bus.go`
- `apps/edge/internal/events/bus_test.go`
- `apps/edge/internal/bootstrap/runtime.go`
- `apps/edge/internal/bootstrap/runtime_test.go`
- `apps/edge/internal/bootstrap/module.go`
- `apps/edge/internal/transport/server.go`
- `apps/edge/internal/transport/server_test.go`
- `packages/config/config.go`
- `packages/config/config_test.go`
- `go.mod`

### 테스트 커버리지 공백

- Edge input surface lifecycle: OpenAI server wiring은 있으나 OpenAI와 A2A를 함께 관리하는 input manager 테스트가 없다. 새 `apps/edge/internal/input/manager_test.go`가 필요하다.
- A2A JSON-RPC envelope parsing/response writing: 기존 테스트 없음. 새 `apps/edge/internal/input/a2a/server_test.go`에 정상/에러/bad method 테스트가 필요하다.
- A2A `message/send` -> `edgeservice.SubmitRunRequest` 변환: OpenAI surface 테스트는 있으나 A2A DTO와 metadata/configuration은 미검증이다. 새 테스트가 필요하다.
- `RunEvent` -> A2A `TaskStatus`/artifact/history 변환: `events.Bus` replay 테스트만 있고 A2A 상태 registry 테스트가 없다. 새 테스트가 필요하다.
- runtime/config wiring: OpenAI config/wiring 테스트는 있으나 A2A config/wiring 테스트가 없다. `packages/config/config_test.go`, `apps/edge/internal/bootstrap/runtime_test.go` 확장이 필요하다.

### 심볼 참조

- renamed/removed symbols: none.
- 새 public/internal symbols 예정: `config.EdgeA2AConf`, `input.Manager`, `input/a2a.Server`, `input/a2a.TaskStore` 또는 동등 타입. 기존 call site 변경은 `bootstrap.Runtime`, `NewRuntime`, `Runtime.Start`, `Runtime.Stop`에 한정한다.

### 범위 결정 근거

- `proto/iop/runtime.proto`와 `proto/gen/**`는 변경하지 않는다. A2A는 Edge HTTP input surface이고, edge-node native protocol은 기존 `RunRequest`/`RunEvent`로 충분히 매핑 가능하다.
- `apps/node/**`는 변경하지 않는다. Node는 이미 `RunEvent`를 발생시키며 A2A는 Edge 수신면에서 변환한다.
- Control Plane 구현은 하지 않는다. 현재 task는 Core가 호출할 수 있는 IOP Edge agent endpoint를 먼저 제공하는 것이다.
- 기존 `apps/edge/internal/openai` 구현을 대규모 이동하지 않는다. 1차 구현은 `apps/edge/internal/input` manager가 기존 OpenAI server와 새 A2A server를 함께 소유하게 하고, 실제 package relocation은 필요성이 확인되면 별도 계획으로 다룬다.
- `message/stream`, push notification, `tasks/list`, authenticated extended agent card는 1차 범위에서 제외한다. Core 쪽 현재 client는 `message/send`, `tasks/get`, `tasks/cancel`만 요구한다.

### 빌드 등급

- build lane/grade: `cloud-G08`. Edge input lifecycle, JSON-RPC/A2A protocol, HTTP auth, async state mapping, runtime wiring이 엮이는 cross-domain 작업이고 신규 테스트가 필요하다.
- review lane/grade: `cloud-G08`. OpenAI input 회귀, protocol/schema 호환성, concurrency/task state registry, config defaults, verification 신뢰성을 함께 봐야 한다.

## 구현 체크리스트

- [ ] [A2A-1] Edge input config와 input manager runtime wiring을 추가한다.
- [ ] [A2A-2] A2A JSON-RPC input server와 DTO를 추가한다.
- [ ] [A2A-3] `RunEvent`를 A2A `Task` 상태/출력으로 매핑하는 task registry를 추가한다.
- [ ] [A2A-4] OpenAI-compatible과 A2A의 사용처를 문서와 로드맵에 반영하고 최종 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

이 task directory는 `04+03_a2a_agent_surface`이므로 sibling `03+node_setup_cli`가 `complete.log`를 만든 뒤 구현을 시작한다. 구현 순서는 A2A-1 -> A2A-2 -> A2A-3 -> A2A-4로 진행한다.

### [A2A-1] Edge input config와 input manager runtime wiring

#### 문제

`packages/config/config.go:13-21`의 `EdgeConfig`에는 OpenAI input surface만 있고 A2A input surface 설정이 없다.

```go
13 type EdgeConfig struct {
14     Edge    EdgeInfo         `mapstructure:"edge"    yaml:"edge"`
15     Server  EdgeServerConf   `mapstructure:"server"  yaml:"server"`
16     OpenAI  EdgeOpenAIConf   `mapstructure:"openai"  yaml:"openai"`
17     TLS     TLSConf          `mapstructure:"tls"     yaml:"tls"`
18     Logging LoggingConf      `mapstructure:"logging" yaml:"logging"`
19     Metrics MetricsConf      `mapstructure:"metrics" yaml:"metrics"`
20     Console EdgeConsoleConf  `mapstructure:"console" yaml:"console"`
21     Nodes   []NodeDefinition `mapstructure:"nodes"   yaml:"nodes"`
```

`apps/edge/internal/bootstrap/runtime.go:31-48`도 OpenAI server를 직접 생성한다. A2A를 여기에 병렬 필드로 추가하면 Edge inbound/input 표면이 runtime에 흩어진다.

#### 해결 방법

`EdgeA2AConf`를 추가하고 `EdgeConfig`에 `A2A` 필드를 둔다. OpenAI 설정과 같은 패턴으로 `enabled`, `listen`, `path`, `node`, `adapter`, `target`, `session_id`, `timeout_sec`, `bearer_token`을 둔다. 기본값은 disabled, listen `0.0.0.0:8081`, path `/a2a`, adapter `cli`, session `a2a`, timeout `120`으로 둔다.

그 다음 `apps/edge/internal/input` manager를 추가해 기존 OpenAI-compatible server와 새 A2A server의 lifecycle을 한 곳에서 관리한다. OpenAI server 구현은 `apps/edge/internal/openai`에 그대로 두고, manager가 `edgeopenai.NewServer(cfg.OpenAI, svc, ...)`와 `edgea2a.NewServer(cfg.A2A, svc, ...)`를 생성한다.

```go
type EdgeConfig struct {
    Edge    EdgeInfo       `mapstructure:"edge" yaml:"edge"`
    Server  EdgeServerConf `mapstructure:"server" yaml:"server"`
    OpenAI  EdgeOpenAIConf `mapstructure:"openai" yaml:"openai"`
    A2A     EdgeA2AConf    `mapstructure:"a2a" yaml:"a2a"`
    // ...
}

type EdgeA2AConf struct {
    Enabled     bool   `mapstructure:"enabled" yaml:"enabled"`
    Listen      string `mapstructure:"listen" yaml:"listen"`
    Path        string `mapstructure:"path" yaml:"path"`
    NodeRef     string `mapstructure:"node" yaml:"node"`
    Adapter     string `mapstructure:"adapter" yaml:"adapter"`
    Target      string `mapstructure:"target" yaml:"target"`
    SessionID   string `mapstructure:"session_id" yaml:"session_id"`
    TimeoutSec  int    `mapstructure:"timeout_sec" yaml:"timeout_sec"`
    BearerToken string `mapstructure:"bearer_token" yaml:"bearer_token"`
}

type Manager struct {
    OpenAI *edgeopenai.Server
    A2A    *edgea2a.Server
}
```

#### 수정 파일 및 체크리스트

- [ ] `packages/config/config.go`: `EdgeA2AConf`, defaults 추가.
- [ ] `packages/config/config_test.go`: defaults/override 테스트 추가.
- [ ] `apps/edge/internal/input/manager.go`: OpenAI-compatible server와 A2A server를 함께 생성/Start/Stop하는 manager 추가.
- [ ] `apps/edge/internal/input/manager_test.go`: disabled/enabled lifecycle 동작 테스트 추가.
- [ ] `apps/edge/internal/bootstrap/runtime.go`: `OpenAI` 직접 필드 대신 input manager wiring 추가.
- [ ] `apps/edge/internal/bootstrap/runtime_test.go`: `NewRuntime` input manager wiring 확인 추가.

#### 테스트 작성

- 작성: `packages/config/config_test.go`에 `TestLoadEdge_A2ADefaults`, `TestLoadEdge_A2AOverride`.
- 작성: `apps/edge/internal/input/manager_test.go`에 `TestManagerOwnsOpenAIAndA2AInputs`.
- 작성: `apps/edge/internal/bootstrap/runtime_test.go`에 `TestNewRuntimeWiresInputManager`.

#### 중간 검증

```bash
go test -count=1 ./packages/config ./apps/edge/internal/input ./apps/edge/internal/bootstrap
```

기대 결과: A2A config default/override, Edge input manager, runtime wiring 테스트가 PASS.

### [A2A-2] A2A JSON-RPC input server와 DTO

#### 문제

`apps/edge/internal/openai/server.go:19-52`는 OpenAI-compatible HTTP input만 제공한다. A2A spec의 JSON-RPC `message/send`, `tasks/get`, `tasks/cancel`을 받을 Edge input endpoint가 없다.

```go
19 type runService interface {
20     SubmitRun(context.Context, edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error)
21 }
...
49 mux := http.NewServeMux()
50 mux.HandleFunc("/healthz", s.handleHealthz)
51 mux.HandleFunc("/v1/models", s.handleModels)
52 mux.HandleFunc("/v1/chat/completions", s.handleChatCompletions)
```

#### 해결 방법

새 패키지 `apps/edge/internal/input/a2a`를 만들고 OpenAI server와 같은 lifecycle 패턴을 따른다. endpoint는 `cfg.Path` 기본 `/a2a`에서 JSON-RPC 2.0 POST만 받고, agent card는 `/.well-known/agent.json`에 최소 메타데이터를 제공한다. auth는 `bearer_token`이 비어 있으면 disabled, 값이 있으면 `Authorization: Bearer <token>`을 요구한다.

```go
package a2a

type Server struct {
    cfg     config.EdgeA2AConf
    service runService
    tasks   *TaskStore
    logger  *zap.Logger
    server  *http.Server
}

func (s *Server) handleRPC(w http.ResponseWriter, r *http.Request) {
    // decode JSON-RPC 2.0
    // dispatch message/send, tasks/get, tasks/cancel
    // always return JSON-RPC response envelope
}
```

DTO는 A2A field name을 그대로 사용한다: `Message`, `Part`, `Task`, `TaskStatus`, `Artifact`, `MessageSendParams`, `TaskQueryParams`, `TaskIDParams`, `JSONRPCRequest`, `JSONRPCResponse`, `JSONRPCError`.

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/input/a2a/server.go`: lifecycle, routing, auth, JSON-RPC dispatch.
- [ ] `apps/edge/internal/input/a2a/types.go`: A2A DTO 정의.
- [ ] `apps/edge/internal/input/a2a/server_test.go`: request/response envelope, method dispatch, auth 테스트.

#### 테스트 작성

- 작성: `TestHandleMessageSendDispatchesRun`, `TestHandleGetTaskReturnsStoredTask`, `TestHandleCancelTask`, `TestRejectsBadAuth`, `TestRejectsUnknownMethod`.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/input/a2a
```

기대 결과: JSON-RPC envelope, auth, method dispatch 테스트가 PASS.

### [A2A-3] RunEvent를 A2A Task 상태/출력으로 매핑하는 task registry

#### 문제

`apps/edge/internal/service/service.go:133-190`은 `SubmitRun` 후 `RunHandle` stream을 반환하고, `apps/edge/internal/openai/server.go:269-312`는 이 stream을 OpenAI completion text로만 수집한다. A2A input에서 `tasks/get`이 조회할 수 있는 `Task` snapshot 저장소와 상태 매핑이 없다.

```go
269 func collectRunText(ctx context.Context, handle *edgeservice.RunHandle) (string, *openAIUsage, error) {
270     var b strings.Builder
...
288         switch event.GetType() {
289         case "delta":
290             b.WriteString(event.GetDelta())
291         case "complete":
299             return b.String(), usage, nil
300         case "error", "cancelled":
308             return "", nil, fmt.Errorf("%s", msg)
```

#### 해결 방법

`apps/edge/internal/input/a2a/task_store.go`를 추가해 in-memory task registry를 둔다. `message/send`는 OpenAI input과 동일하게 `edgeservice.SubmitRunRequest`를 만들고 `SubmitRunRequest{Background:false}`로 dispatch한다. `configuration.blocking=true`이면 `RunHandle`을 끝까지 수집해 `completed/failed/canceled` task를 반환한다. `blocking=false`이면 background goroutine이 stream을 수집해 registry를 갱신하고, 응답은 `working` task를 반환한다.

RunEvent 매핑:

- `start` -> `working`
- `delta` -> artifact text append
- `complete` -> `completed`, final artifact 생성
- `error` -> `failed`
- `cancelled` -> `canceled`
- node disconnect -> `failed`

`tasks/cancel`은 저장된 task의 run metadata로 `edgeservice.TerminateSession`을 호출하지 말고, 1차에서는 `CancelRequest` 경로가 service에 없으므로 cancel unsupported를 명시적으로 `-32002`로 반환하거나 service에 `CancelRun`을 추가하는 두 선택지가 있다. 구현 에이전트는 최소 범위로 `CancelRun`을 service에 추가할 수 있는지 먼저 판단하고, call path가 작으면 `Service.CancelRun` -> `iop.CancelRequest{Action:CANCEL_RUN}`을 추가한다. 범위가 커지면 `TaskNotCancelableError`로 제한하고 review에 기록한다.

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/internal/input/a2a/task_store.go`: task snapshot registry, event reducer, output artifact builder.
- [ ] `apps/edge/internal/input/a2a/server.go`: blocking/non-blocking `message/send`, `tasks/get`, `tasks/cancel` 연결.
- [ ] 선택 시 `apps/edge/internal/service/service.go`: `CancelRun` surface-neutral method 추가.
- [ ] 선택 시 `apps/edge/internal/service/service_test.go`: `CancelRun` request construction 테스트 추가.

#### 테스트 작성

- 작성: `TestTaskStoreMapsRunEventsToCompletedTask`, `TestTaskStoreMapsErrorToFailedTask`, `TestMessageSendBlockingReturnsCompletedTask`.
- 작성 또는 명시적 skip: cancel path. `CancelRun`을 구현하면 `TestCancelTaskSendsCancelRun`; 구현하지 않으면 `TestCancelTaskReturnsNotCancelable`.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/input/a2a ./apps/edge/internal/service
```

기대 결과: A2A task state mapping과 cancel 정책 테스트가 PASS.

### [A2A-4] 문서와 로드맵

#### 문제

`apps/edge/README.md:158-162`에는 OpenAI-compatible serving만 설명되어 있다. 루트 `README.md` 로드맵과 `docs/architecture.md`도 OpenAI-compatible과 IOP native protocol 중심으로 설명하고 있어, Edge 외부 입력으로 OpenAI-compatible과 A2A 두 방식을 병행 지원한다는 구분이 부족하다. `go.mod`에는 `net/http`, `encoding/json` 등 표준 라이브러리로 충분한 구성이 이미 가능하므로 신규 dependency가 필요하지 않다.

#### 해결 방법

문서에는 Edge input surface를 다음 두 방식으로 정리한다.

- OpenAI-compatible HTTP API: 모델 클라이언트, Cline류 도구, 단순 chat completion/inference 호환에 사용한다. 외부 `model`은 Edge에서 `adapter + target`으로 변환한다.
- A2A JSON-RPC HTTP API: NomadCode Core나 외부 agent가 작업을 위임하고 `Task` 상태, artifact, cancel/polling을 공유해야 할 때 사용한다. 역시 Edge에서 `adapter + target`으로 변환한다.

`apps/edge/README.md`에 A2A serving 섹션을 추가하고, 루트 `README.md`의 Roadmap과 `docs/architecture.md`의 외부 API 표면에도 두 입력 방식과 사용처를 반영한다.

#### 수정 파일 및 체크리스트

- [ ] `apps/edge/README.md`: Edge input surface로 OpenAI-compatible과 A2A 설명/설정 예시/지원 method 추가.
- [ ] `README.md`: Roadmap에 두 Edge input 방식과 사용처 추가.
- [ ] `docs/architecture.md`: 외부 API 표면에 OpenAI-compatible input과 A2A input의 역할 구분 추가.
- [ ] `go.mod`: 신규 dependency가 없음을 유지. 추가가 꼭 필요하면 이유를 review에 기록한다.

#### 테스트 작성

- 문서 변경 자체는 별도 테스트 없음. A2A behavior는 A2A-1~A2A-3 테스트로 검증한다.

#### 중간 검증

```bash
rg --sort path "Edge input|OpenAI-compatible|A2A|message/send|tasks/get|tasks/cancel" README.md docs/architecture.md apps/edge/README.md packages/config apps/edge/internal
```

기대 결과: Roadmap, architecture, Edge README, config, input manager, A2A package에 관련 항목이 결정적 순서로 출력된다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/config/config.go` | A2A-1 |
| `packages/config/config_test.go` | A2A-1 |
| `apps/edge/internal/input/manager.go` | A2A-1 |
| `apps/edge/internal/input/manager_test.go` | A2A-1 |
| `apps/edge/internal/bootstrap/runtime.go` | A2A-1 |
| `apps/edge/internal/bootstrap/runtime_test.go` | A2A-1 |
| `apps/edge/internal/input/a2a/types.go` | A2A-2 |
| `apps/edge/internal/input/a2a/server.go` | A2A-2, A2A-3 |
| `apps/edge/internal/input/a2a/task_store.go` | A2A-3 |
| `apps/edge/internal/input/a2a/server_test.go` | A2A-2, A2A-3 |
| `apps/edge/internal/input/a2a/task_store_test.go` | A2A-3 |
| `apps/edge/internal/service/service.go` | A2A-3 optional cancel |
| `apps/edge/internal/service/service_test.go` | A2A-3 optional cancel |
| `apps/edge/README.md` | A2A-4 |
| `README.md` | A2A-4 |
| `docs/architecture.md` | A2A-4 |
| `go.mod` | A2A-4 dependency check only |

## 최종 검증

Go test cache output은 허용하지 않는다. 모든 테스트는 fresh 실행한다.

```bash
gofmt -w packages/config/config.go packages/config/config_test.go apps/edge/internal/bootstrap/runtime.go apps/edge/internal/bootstrap/runtime_test.go apps/edge/internal/input apps/edge/internal/input/a2a
go test -count=1 ./packages/config ./apps/edge/internal/input ./apps/edge/internal/input/a2a ./apps/edge/internal/bootstrap ./apps/edge/internal/service
go test -count=1 ./...
rg --sort path "Edge input|OpenAI-compatible|A2A|message/send|tasks/get|tasks/cancel" README.md docs/architecture.md apps/edge/README.md packages/config apps/edge/internal
git status --short
```

기대 결과: formatting 후 모든 tests PASS, deterministic `rg`에 Edge input manager, OpenAI-compatible, A2A config/server/docs 경로가 출력, `git status --short`에는 이 plan이 지정한 구현 파일과 review stub 작성만 남는다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
