From 672b4cdb5d2bf2ba2f7d6d5a8ffe4a78bcd9d6c2 Mon Sep 17 00:00:00 2001 From: toki Date: Sun, 7 Jun 2026 08:40:36 +0900 Subject: [PATCH] =?UTF-8?q?refactor(bridge):=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=20=EC=84=A0=ED=96=89=20=EC=95=88=EC=A0=95?= =?UTF-8?q?=ED=99=94=EB=A5=BC=20=EB=B0=98=EC=98=81=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 원격 터미널 브리지 POC 전에 Client HTTP lifecycle과 Edge run result surface 계약을 고정해야 하므로 관련 구현, 테스트, 로드맵 상태를 함께 정리한다. --- .../architecture-refactor-foundation.md | 21 ++- .../phase/automation-runtime-bridge/PHASE.md | 10 +- .../milestones/bridge-boundary-hardening.md | 65 +++++++ .../code_review_local_G06_0.log | 175 ++++++++++++++++++ .../01_edge_run_surface/complete.log | 46 +++++ .../01_edge_run_surface/plan_local_G06_0.log | 163 ++++++++++++++++ .../CODE_REVIEW-cloud-G07.md | 121 ++++++++++++ .../PLAN-cloud-G07.md | 164 ++++++++++++++++ .../CODE_REVIEW-cloud-G06.md | 143 ++++++++++++++ .../03_adapter_config_typed/PLAN-cloud-G06.md | 166 +++++++++++++++++ apps/client/lib/client_home_page.dart | 48 +++-- .../lib/control_plane_status_repository.dart | 55 ++++-- .../control_plane_status_controller_test.dart | 42 +++++ apps/edge/internal/input/a2a/server.go | 4 +- apps/edge/internal/input/a2a/server_test.go | 14 +- apps/edge/internal/input/a2a/task_store.go | 15 +- .../internal/input/a2a/task_store_test.go | 6 +- apps/edge/internal/openai/chat_handler.go | 10 +- apps/edge/internal/openai/run_result.go | 8 +- apps/edge/internal/openai/server.go | 2 +- apps/edge/internal/openai/server_test.go | 4 +- apps/edge/internal/openai/stream.go | 19 +- apps/edge/internal/opsconsole/console.go | 20 +- apps/edge/internal/service/run_dispatch.go | 25 ++- 24 files changed, 1252 insertions(+), 94 deletions(-) rename agent-roadmap/{ => archive}/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md (82%) create mode 100644 agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md create mode 100644 agent-task/archive/2026/06/m-bridge-boundary-hardening/01_edge_run_surface/code_review_local_G06_0.log create mode 100644 agent-task/archive/2026/06/m-bridge-boundary-hardening/01_edge_run_surface/complete.log create mode 100644 agent-task/archive/2026/06/m-bridge-boundary-hardening/01_edge_run_surface/plan_local_G06_0.log create mode 100644 agent-task/m-bridge-boundary-hardening/02_terminal_core_boundary/CODE_REVIEW-cloud-G07.md create mode 100644 agent-task/m-bridge-boundary-hardening/02_terminal_core_boundary/PLAN-cloud-G07.md create mode 100644 agent-task/m-bridge-boundary-hardening/03_adapter_config_typed/CODE_REVIEW-cloud-G06.md create mode 100644 agent-task/m-bridge-boundary-hardening/03_adapter_config_typed/PLAN-cloud-G06.md diff --git a/agent-roadmap/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md b/agent-roadmap/archive/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md similarity index 82% rename from agent-roadmap/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md rename to agent-roadmap/archive/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md index 3ed9a58..f98fd4c 100644 --- a/agent-roadmap/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md +++ b/agent-roadmap/archive/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md @@ -12,7 +12,7 @@ Edge, Node, Control Plane, Client, proto/config 경계에서 책임이 섞인 ## 상태 -[진행중] +[완료] ## 구현 잠금 @@ -61,19 +61,19 @@ Control Plane과 Flutter Client가 multi-edge 운영면으로 커질 때 선형 공개 계약과 tracked 예시 파일이 실제 운영 원칙과 어긋나지 않게 정리한다. - [x] [status-config-summary] Control Plane status contract에서 full `NodeConfigPayload` 노출 여지를 제거하거나 summary-only DTO로 고정한다. 검증: `go test -count=1 ./apps/control-plane/... ./packages/go/... ./proto/gen/...` -- [ ] [proto-legacy] legacy scheduling/node registration proto placeholder를 deprecated/reserved/remove 중 하나로 정리하고 생성 코드와 call-site를 맞춘다. +- [x] [proto-legacy] legacy scheduling/node registration proto placeholder를 deprecated/reserved/remove 중 하나로 정리하고 생성 코드와 call-site를 맞춘다. 검증: `agent-task/archive/2026/06/m-architecture-refactor-foundation/09_proto_legacy_contract/complete.log` - [x] [sample-config] tracked `configs/*.yaml`에서 로컬 IP, host명, token, 위험한 bypass flag, 개인 경로를 제거하고 안전한 example/default 기준으로 재정리한다. 검증: `go test -count=1 ./apps/edge/cmd/edge ./apps/node/cmd/node` - [x] [git-artifacts] tracked Flutter build artifact를 제거하고 package build 산출물이 다시 추적되지 않도록 ignore 규칙을 보강한다. 검증: `git ls-files | rg --sort path 'packages/flutter/iop_console/build|(^|/)build/.*(cache|unit_test_assets|native_assets|\.dill|\.otf|\.frag|NOTICES)'` ## 완료 리뷰 -- 상태: 없음 -- 요청일: 없음 -- 완료 근거: `cp-api-split`, `fleet-polling`, `client-state-split` 완료 로그와 `contract-hygiene`의 `status-config-summary`, `sample-config`, `git-artifacts` 완료 근거를 반영했다. `proto-legacy` Task가 아직 남아 있어 Milestone 전체 완료 조건은 충족되지 않았다. +- 상태: 승인됨 +- 요청일: 2026-06-07 +- 완료 근거: `edge-boundary`, `node-runtime`, `cp-client`, `contract-hygiene`의 모든 기능 Task가 완료 근거와 함께 `[x]` 상태다. 마지막 남은 `proto-legacy`는 `agent-task/archive/2026/06/m-architecture-refactor-foundation/09_proto_legacy_contract/complete.log`의 `Roadmap Completion` 근거로 완료 처리했다. - 리뷰 필요: - - [ ] 사용자가 완료 결과를 확인했다 - - [ ] archive 이동을 승인했다 -- 리뷰 코멘트: 없음 + - [x] 사용자가 완료 결과를 확인했다 + - [x] archive 이동을 승인했다 +- 리뷰 코멘트: 사용자 요청으로 Milestone 종료를 승인하고 archive 이동을 반영했다. ## 범위 제외 @@ -89,6 +89,7 @@ Control Plane과 Flutter Client가 multi-edge 운영면으로 커질 때 선형 - 표준선(선택): Control Plane은 Edge를 통해 관찰/제어하고, Edge는 Node 실행 그룹 상태를 소유하며, Node는 adapter execution과 terminal transport 실행자 역할을 유지한다. OpenAI-compatible/A2A 표면은 IOP native terminal 제어를 흡수하지 않는다. - 선행 작업: 설계 부채 색인 리뷰 - 후속 작업: 원격 터미널 브리지 POC -- 최근 완료 작업: `agent-task/archive/2026/06/m-architecture-refactor-foundation/06_cp_api_split/complete.log`, `agent-task/archive/2026/06/m-architecture-refactor-foundation/07+06_fleet_polling/complete.log`, `agent-task/archive/2026/06/m-architecture-refactor-foundation/08_client_state_split/complete.log` -- active plan: `agent-task/m-architecture-refactor-foundation/09_proto_legacy_contract/PLAN-cloud-G06.md` +- 최근 완료 작업: `agent-task/archive/2026/06/m-architecture-refactor-foundation/06_cp_api_split/complete.log`, `agent-task/archive/2026/06/m-architecture-refactor-foundation/07+06_fleet_polling/complete.log`, `agent-task/archive/2026/06/m-architecture-refactor-foundation/08_client_state_split/complete.log`, `agent-task/archive/2026/06/m-architecture-refactor-foundation/09_proto_legacy_contract/complete.log` +- active plan: 없음 - 확인 필요: 없음 +- 잔여 리스크 처리: 완료 후보를 되돌릴 수준의 결함은 아니지만 원격 터미널 브리지 POC 전에 고정해야 할 Client HTTP lifecycle, Edge run surface, Node terminal core, typed adapter config 리스크를 `agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md` 후속 Milestone으로 승격했다. diff --git a/agent-roadmap/phase/automation-runtime-bridge/PHASE.md b/agent-roadmap/phase/automation-runtime-bridge/PHASE.md index 1898b29..5e1dfad 100644 --- a/agent-roadmap/phase/automation-runtime-bridge/PHASE.md +++ b/agent-roadmap/phase/automation-runtime-bridge/PHASE.md @@ -34,6 +34,10 @@ CLI 실행, specialized agent 등록, bootstrap/enrollment, 원격 터미널 브 - 경로: `agent-roadmap/archive/phase/automation-runtime-bridge/milestones/domain-agent-specific-iop-removal.md` - 요약: 독립 Control Plane 전환에 맞춰 IOP 활성 코드와 문서에서 전용 agent kind, bootstrap, smoke, 운영 문구를 제거했다. +- [완료] 전역 설계 부채 리팩토링 기반 + - 경로: `agent-roadmap/archive/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md` + - 요약: 원격 터미널 브리지와 이후 운영 확장 전에 Edge, Node, Control Plane, Client, proto/config 경계의 설계 부채를 정리했고, 잔여 리스크는 후속 선행 안정화 마일스톤으로 넘겼다. + - [폐기] Specialized Agent proto-socket 연결 기반 - 경로: `agent-roadmap/archive/phase/automation-runtime-bridge/milestones/specialized-agent-proto-socket-foundation.md` - 요약: domain agent가 독립형 실행 방식으로 전환될 예정이므로 Edge 직접 proto-socket specialized peer 기준의 연결 기반 정리는 폐기한다. @@ -50,9 +54,9 @@ CLI 실행, specialized agent 등록, bootstrap/enrollment, 원격 터미널 브 - 경로: `agent-roadmap/archive/phase/automation-runtime-bridge/milestones/domain-agent-message-boundary.md` - 요약: 독립형 실행 전환으로 Edge 직접 domain payload boundary 정리가 현재 범위에서 필요 없어져 폐기한다. -- [진행중] 전역 설계 부채 리팩토링 기반 - - 경로: `agent-roadmap/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md` - - 요약: 원격 터미널 브리지와 이후 운영 확장 전에 Edge, Node, Control Plane, Client, proto/config 경계의 설계 부채를 정리한다. +- [계획] 브리지 선행 경계 안정화 + - 경로: `agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md` + - 요약: 원격 터미널 브리지 POC 전에 남은 호환성/소유권 리스크를 Client HTTP lifecycle, Edge run surface, Node terminal core, typed adapter config 계약으로 고정한다. - [계획] 원격 터미널 브리지 POC - 경로: `agent-roadmap/phase/automation-runtime-bridge/milestones/remote-terminal-bridge-poc.md` diff --git a/agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md b/agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md new file mode 100644 index 0000000..e41a24f --- /dev/null +++ b/agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md @@ -0,0 +1,65 @@ +# Milestone: 브리지 선행 경계 안정화 + +## 위치 + +- Roadmap: `agent-roadmap/ROADMAP.md` +- Phase: `agent-roadmap/phase/automation-runtime-bridge/PHASE.md` + +## 목표 + +원격 터미널 브리지 POC를 시작하기 전에 전역 설계 부채 리팩토링에서 남은 호환성/소유권 리스크를 실행 가능한 계약으로 고정한다. +새 terminal/native API가 legacy 실행 handle, provider-specific terminal core, 임시 설정 경로, 느슨한 client lifecycle에 기대지 않도록 선행 경계를 정리한다. + +## 상태 + +[진행중] + +## 구현 잠금 + +- 상태: 해제 +- 결정 필요: 없음 + +## 범위 + +- Client HTTP repository의 기본 client 소유권과 dispose 경계 정리 +- Edge service run 제출 결과에서 legacy `RunHandle` 의존을 줄이고 신규 표면의 `RunDispatch`/`RunStream` 소비 기준 고정 +- Node terminal session core를 provider-specific CLI 구현에서 재사용 가능한 terminal transport 경계로 분리할 수 있는 최소 구조 확정 +- `AdapterConfig.settings`를 legacy fallback으로만 유지하고 신규 adapter/terminal config는 typed proto 계약으로 표현한다는 guard 보강 + +## 기능 + +### Epic: [preflight-boundary] Bridge Preflight Boundary + +원격 터미널 브리지 구현이 기존 호환 표면을 확장 경로로 착각하지 않도록 선행 계약을 고정한다. + +- [x] [client-http-lifecycle] `HttpControlPlaneStatusRepository`가 기본 `http.Client`를 요청마다 새로 만들지 않고 소유/해제 경계를 명확히 하며, 주입된 client는 caller-owned로 유지한다. 검증: `cd apps/client && flutter test test/control_plane_status_controller_test.dart test/widget_test.dart` +- [ ] [edge-run-surface] Edge 신규 native/terminal 표면은 legacy combined `RunHandle` 대신 `RunDispatch`와 `RunStream` 또는 동등한 surface-neutral 계약을 소비하도록 service interface와 테스트를 정리한다. 검증: `go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai ./apps/edge/internal/input/a2a` +- [ ] [terminal-core-boundary] persistent PTY 기반 `terminalSessionCore`를 CLI provider 내부 세부가 아니라 remote terminal transport가 재사용할 수 있는 Node terminal boundary로 분리하거나 명시 interface로 고정한다. 검증: `go test -count=1 ./apps/node/internal/adapters/cli ./apps/node/internal/...` +- [ ] [adapter-config-typed] `AdapterConfig.settings`는 구버전 Edge payload fallback으로만 허용하고, Edge mapper와 신규 config는 typed oneof/proto 메시지를 사용한다는 guard를 테스트로 고정한다. 검증: `make proto && go test -count=1 ./apps/edge/internal/node ./apps/edge/internal/transport ./apps/node/internal/adapters ./proto/gen/...` + +## 완료 리뷰 + +- 상태: 없음 +- 요청일: 없음 +- 완료 근거: 모든 기능 Task와 Task 안에 명시된 검증이 아직 충족되지 않았다. +- 리뷰 필요: + - [ ] 사용자가 완료 결과를 확인했다 + - [ ] archive 이동을 승인했다 +- 리뷰 코멘트: 없음 + +## 범위 제외 + +- 원격 터미널 브리지 open/input/output/resize/signal/close 기능 구현 +- SSH/WinRM/serial 같은 신규 terminal transport 구현 +- OpenAI-compatible/A2A 표면에 terminal 제어 기능 추가 +- legacy `AdapterConfig.settings` fallback 즉시 제거 +- Control Plane/Client terminal 운영 UI 구현 + +## 작업 컨텍스트 + +- 관련 경로: `apps/client/lib/control_plane_status_repository.dart`, `apps/edge/internal/service`, `apps/edge/internal/openai`, `apps/edge/internal/input/a2a`, `apps/node/internal/adapters/cli`, `proto/iop/runtime.proto`, `apps/edge/internal/node`, `apps/node/internal/adapters` +- 표준선(선택): 호환성 경로는 유지하되 신규 bridge/native 구현은 surface-neutral DTO, reusable terminal boundary, typed proto config를 기준으로 삼는다. +- 선행 작업: 전역 설계 부채 리팩토링 기반 +- 후속 작업: 원격 터미널 브리지 POC +- 확인 필요: 없음 +- 완료 근거: `[client-http-lifecycle]`은 `cd apps/client && flutter test test/control_plane_status_controller_test.dart test/widget_test.dart` 통과. diff --git a/agent-task/archive/2026/06/m-bridge-boundary-hardening/01_edge_run_surface/code_review_local_G06_0.log b/agent-task/archive/2026/06/m-bridge-boundary-hardening/01_edge_run_surface/code_review_local_G06_0.log new file mode 100644 index 0000000..8a23a19 --- /dev/null +++ b/agent-task/archive/2026/06/m-bridge-boundary-hardening/01_edge_run_surface/code_review_local_G06_0.log @@ -0,0 +1,175 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-07 +task=m-bridge-boundary-hardening/01_edge_run_surface, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md` +- Task ids: + - `edge-run-surface`: Edge 신규 native/terminal 표면은 legacy combined `RunHandle` 대신 `RunDispatch`와 `RunStream` 또는 동등한 surface-neutral 계약을 소비하도록 service interface와 테스트를 정리한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G06.md` → `code_review_local_G06_N.log`, `PLAN-local-G06.md` → `plan_local_G06_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-bridge-boundary-hardening/01_edge_run_surface/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. PASS이고 task group이 `m-bridge-boundary-hardening`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Run Surface-Neutral DTO Consumption | [x] | + +## 구현 체크리스트 + +- [x] OpenAI/A2A용 run service interface와 collector가 `RunDispatch`/`RunStream`을 직접 소비하도록 바꾸고 `edge-run-surface` 검증 명령을 통과시킨다. +- [x] 기존 `Service.SubmitRun` 호환성은 유지하되 신규 DTO 소비를 보장하는 service/openai/a2a 테스트를 갱신한다. +- [x] 사용자 실행 파이프라인 영향에 대해 보조 smoke/full-cycle 수행 여부 또는 생략 사유를 검증 결과에 기록한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G06_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G06_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-bridge-boundary-hardening/01_edge_run_surface/`를 `agent-task/archive/YYYY/MM/m-bridge-boundary-hardening/01_edge_run_surface/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-bridge-boundary-hardening`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-bridge-boundary-hardening/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G06.md`와 `CODE_REVIEW-local-G06.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +계획에서는 `Service.SubmitRun`이 기존 호환성을 위해 `*RunHandle`을 계속 반환하도록 하고 local interface 수준에서 DTO wrapper로 좁히려 했으나, Go의 인터페이스 시그니처 엄격성(공변 반환 타입 미지원)으로 인해 `Service.SubmitRun` 자체의 반환 타입을 `RunResult` 인터페이스로 변경해야 했습니다. 이에 따라 `opsconsole/console.go` 및 call site들의 DTO 소비 경로를 `handle.Dispatch()` 및 `handle.Stream()`를 사용하도록 함께 정비했습니다. + +## 주요 설계 결정 + +`edgeservice.Service.SubmitRun`이 Go 인터페이스 시그니처 호환성을 만족하도록 반환 타입을 `RunResult`로 선언하여, 별도 래퍼 보일러플레이트 없이 `openai/runService` 및 `a2a/runService` 인터페이스에 `*Service`를 직접 주입해 사용할 수 있게 하였습니다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- OpenAI/A2A interfaces and collectors no longer require `*RunHandle` as their direct surface contract. +- `Service.SubmitRun` compatibility and close/unsubscribe behavior are preserved. +- Run timeout, node disconnect, error/cancelled handling remain equivalent. +- Verification output is pasted verbatim. + +## 검증 결과 + +각 유닛 테스트 및 E2E 보조 smoke 테스트 명령을 실행하여 모든 검증을 성공적으로 마쳤습니다. + +### API-1 중간 검증 + +```bash +$ go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai ./apps/edge/internal/input/a2a +ok iop/apps/edge/internal/service 0.006s +ok iop/apps/edge/internal/openai 1.508s +ok iop/apps/edge/internal/input/a2a 0.006s +``` + +### 최종 검증 + +전체 레포지토리의 Go 패키지 및 3개 smoke 테스트 스크립트를 포함한 full-cycle 검증 결과는 아래와 같습니다. + +1. 전체 Go 패키지 유닛 테스트 (`go test ./...`): +```text +ok iop/apps/control-plane/cmd/control-plane (cached) +ok iop/apps/control-plane/internal/wire (cached) +ok iop/apps/edge/cmd/edge 0.037s +ok iop/apps/edge/internal/bootstrap 0.019s +ok iop/apps/edge/internal/controlplane (cached) +ok iop/apps/edge/internal/edgecmd (cached) +ok iop/apps/edge/internal/events (cached) +ok iop/apps/edge/internal/input 0.006s +ok iop/apps/edge/internal/input/a2a 0.006s +ok iop/apps/edge/internal/node (cached) +ok iop/apps/edge/internal/openai 1.508s +ok iop/apps/edge/internal/opsconsole 0.007s +ok iop/apps/edge/internal/service 0.005s +ok iop/apps/edge/internal/transport (cached) +``` + +2. E2E mock 보조 smoke 테스트 (`./scripts/e2e-smoke.sh`): +```text +[e2e] Auxiliary smoke test PASSED. +``` + +3. OpenAI Ollama 입력 표면 보조 smoke 테스트 (`./scripts/e2e-openai-ollama.sh`): +```text +[openai-ollama] OpenAI-compatible Ollama serving test PASSED. +``` + +4. Control Plane-Edge wire 보조 smoke 테스트 (`./scripts/e2e-control-plane-edge-wire.sh`): +```text +[cp-edge-wire] Control Plane-Edge wire smoke PASSED. +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: 없음 +- 리뷰어 검증: + - `go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai ./apps/edge/internal/input/a2a`: PASS + - `go test ./...`: PASS + - `go test -count=1 ./apps/edge/...`: PASS + - `./scripts/e2e-smoke.sh`: PASS + - `./scripts/e2e-openai-ollama.sh`: PASS + - `./scripts/e2e-control-plane-edge-wire.sh`: PASS + - `scripts/dev/edge.sh` + `scripts/dev/node.sh` reviewer diagnostic: PASS; `/nodes`, `/capabilities`, `/transport`, message x2, `/sessions`, `/terminate-session` confirmed with deterministic `fake-cli`. +- 다음 단계: PASS 종결 절차를 진행해 `complete.log`를 작성하고 task directory를 archive로 이동한다. diff --git a/agent-task/archive/2026/06/m-bridge-boundary-hardening/01_edge_run_surface/complete.log b/agent-task/archive/2026/06/m-bridge-boundary-hardening/01_edge_run_surface/complete.log new file mode 100644 index 0000000..355bde2 --- /dev/null +++ b/agent-task/archive/2026/06/m-bridge-boundary-hardening/01_edge_run_surface/complete.log @@ -0,0 +1,46 @@ +# Complete - m-bridge-boundary-hardening/01_edge_run_surface + +## 완료 일시 + +2026-06-07T08:29:52+09:00 + +## 요약 + +Edge OpenAI/A2A run surface를 `RunResult`/`RunDispatch`/`RunStream` 소비 경계로 정리한 1회 루프가 PASS로 완료되었다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | PASS | OpenAI/A2A/ops console call sites가 legacy `RunHandle` 직접 surface 의존 없이 `RunResult` DTO 계약을 소비함을 확인했다. | + +## 구현/정리 내용 + +- `edgeservice.RunResult` 인터페이스를 추가하고 `RunHandle`은 legacy concrete adapter로 유지했다. +- OpenAI/A2A service interfaces, collectors, stream handlers, task store, ops console call sites가 `Dispatch()`와 `Stream()`을 통해 surface-neutral DTO를 소비하도록 정리했다. +- 관련 OpenAI/A2A fake service signatures와 테스트를 `edgeservice.RunResult` 반환 계약으로 갱신했다. + +## 최종 검증 + +- `go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai ./apps/edge/internal/input/a2a` - PASS; 대상 3개 패키지 모두 `ok`. +- `go test ./...` - PASS; 전체 Go package 회귀 통과. +- `go test -count=1 ./apps/edge/...` - PASS; Edge 전체 package 현재 내용 기준 통과. +- `./scripts/e2e-smoke.sh` - PASS; 보조 edge-node smoke 통과. +- `./scripts/e2e-openai-ollama.sh` - PASS; OpenAI-compatible Ollama serving smoke 통과. +- `./scripts/e2e-control-plane-edge-wire.sh` - PASS; Control Plane-Edge wire smoke 통과. +- `scripts/dev/edge.sh` + `scripts/dev/node.sh` reviewer diagnostic - PASS; 임시 config와 deterministic `fake-cli`로 `/nodes`, `/capabilities`, `/transport`, message x2, `/sessions`, `/terminate-session` 확인. + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md` +- Completed task ids: + - `edge-run-surface`: PASS; evidence=`plan_local_G06_0.log`, `code_review_local_G06_0.log`; verification=`go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai ./apps/edge/internal/input/a2a`, `go test -count=1 ./apps/edge/...`, `scripts/dev/edge.sh + scripts/dev/node.sh reviewer diagnostic` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/06/m-bridge-boundary-hardening/01_edge_run_surface/plan_local_G06_0.log b/agent-task/archive/2026/06/m-bridge-boundary-hardening/01_edge_run_surface/plan_local_G06_0.log new file mode 100644 index 0000000..7df3b19 --- /dev/null +++ b/agent-task/archive/2026/06/m-bridge-boundary-hardening/01_edge_run_surface/plan_local_G06_0.log @@ -0,0 +1,163 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +이 작업은 구현이 끝나도 active 파일을 유지하고, 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채운 뒤 리뷰 준비 상태로 멈춘다. 구현 중 사용자 결정, 사용자 소유 외부 환경, 또는 계획 범위 충돌 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 남기고 중단한다. 구현 중 사용자에게 직접 질문하거나 `USER_REVIEW.md`, `complete.log`, archive log를 만들지 않는다. + +## 배경 + +Milestone의 `edge-run-surface` Task는 신규 native/terminal 표면이 legacy combined `RunHandle`에 기대지 않도록 Edge service 경계를 고정하는 작업이다. 현재 service에는 `RunDispatch`와 `RunStream`이 이미 있지만 OpenAI와 A2A interface, collector, tests는 여전히 `*RunHandle`을 직접 주고받는다. 이 plan은 서비스 반환 호환성은 유지하면서 신규 표면이 DTO를 직접 소비하는 경계를 만든다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지하며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 생성을 소유한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md` +- Task ids: + - `edge-run-surface`: Edge 신규 native/terminal 표면은 legacy combined `RunHandle` 대신 `RunDispatch`와 `RunStream` 또는 동등한 surface-neutral 계약을 소비하도록 service interface와 테스트를 정리한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-roadmap/current.md` +- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md` +- `agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md` +- `agent-test/local/rules.md` +- `agent-test/local/edge-smoke.md` +- `agent-test/local/testing-smoke.md` +- `agent-ops/rules/project/domain/edge/rules.md` +- `agent-ops/rules/project/domain/testing/rules.md` +- `apps/edge/internal/service/run_dispatch.go` +- `apps/edge/internal/service/service.go` +- `apps/edge/internal/service/service_test.go` +- `apps/edge/internal/openai/server.go` +- `apps/edge/internal/openai/chat_handler.go` +- `apps/edge/internal/openai/run_result.go` +- `apps/edge/internal/openai/stream.go` +- `apps/edge/internal/openai/server_test.go` +- `apps/edge/internal/input/a2a/server.go` +- `apps/edge/internal/input/a2a/task_store.go` +- `apps/edge/internal/input/a2a/server_test.go` +- `apps/edge/internal/input/a2a/task_store_test.go` + +### 테스트 환경 규칙 + +- test_env: `local`. +- `agent-test/local/rules.md`를 읽었고, domain routing에 따라 `edge-smoke`, 사용자 실행 파이프라인 영향 기준으로 `testing-smoke`를 읽었다. +- 적용 명령: `go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai ./apps/edge/internal/input/a2a`. +- 사전 확인 결과: 위 명령은 현재 checkout에서 성공했다. +- 실행 파이프라인 영향: service/openai/a2a run stream 경로 변경이므로 구현 후 보조 smoke 또는 full-cycle 실제 구동 수행 여부와 생략 사유를 review stub에 기록한다. + +### 테스트 커버리지 공백 + +- OpenAI non-stream/stream/strict stream은 `apps/edge/internal/openai/server_test.go`가 기존 회귀를 보유한다. +- A2A blocking/background/cancel/task mapping은 `server_test.go`와 `task_store_test.go`가 보유한다. +- 현재 공백: OpenAI/A2A fake service interface가 `RunHandle` 반환을 허용해 신규 표면이 DTO 기반인지 직접 보장하지 않는다. 구현 중 fake interface와 collector signature를 DTO 기반으로 바꿔 회귀를 추가한다. + +### 심볼 참조 + +- 변경 예정: `runService.SubmitRun` 반환 타입 또는 wrapper interface, `collectRunResult`, `streamChatCompletion`, `TaskStore.collect*`. +- 현재 `RunHandle` 참조: `rg --sort path "RunHandle" apps/edge/internal/service apps/edge/internal/openai apps/edge/internal/input/a2a`로 확인한 OpenAI/A2A/service/test call sites. + +### 분할 판단 + +- split decision policy를 적용했다. +- 공유 task group: `m-bridge-boundary-hardening`. +- sibling subtasks: + - `01_edge_run_surface`: Edge service/OpenAI/A2A surface-neutral run contract. 독립. + - `02_terminal_core_boundary`: Node PTY terminal core boundary. 독립. + - `03_adapter_config_typed`: proto/adapter config typed contract. 독립. +- 이 subtask는 다른 sibling의 `complete.log`를 필요로 하지 않는다. + +### 범위 결정 근거 + +- 원격 터미널 bridge open/input/output/resize/signal/close 기능은 Milestone 범위 제외이므로 구현하지 않는다. +- ops console은 이미 `SubmitRun` 후 handle drain이 아니라 event bus router를 쓰는 경계라 이번 변경에서 제외한다. +- Control Plane/Client terminal UI는 제외한다. + +### 빌드 등급 + +- `local-G06`: Edge 내부 API와 두 input surface를 함께 바꾸지만 범위가 세 패키지로 제한되고 테스트가 deterministic하다. + +## 구현 체크리스트 + +- [ ] OpenAI/A2A용 run service interface와 collector가 `RunDispatch`/`RunStream`을 직접 소비하도록 바꾸고 `edge-run-surface` 검증 명령을 통과시킨다. +- [ ] 기존 `Service.SubmitRun` 호환성은 유지하되 신규 DTO 소비를 보장하는 service/openai/a2a 테스트를 갱신한다. +- [ ] 사용자 실행 파이프라인 영향에 대해 보조 smoke/full-cycle 수행 여부 또는 생략 사유를 검증 결과에 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] Run Surface-Neutral DTO Consumption + +문제: `RunDispatch`와 `RunStream`은 이미 정의되어 있지만 `RunHandle`이 신규 surface의 interface까지 점유한다. + +Before: + +```go +// apps/edge/internal/openai/server.go:17 +type runService interface { + SubmitRun(context.Context, edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) + OllamaAPI(context.Context, edgeservice.OllamaAPIRequest) (edgeservice.OllamaAPIView, error) +} +``` + +```go +// apps/edge/internal/input/a2a/server.go:19 +type runService interface { + SubmitRun(context.Context, edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) + CancelRun(context.Context, edgeservice.CancelRunRequest) (edgeservice.CommandResult, error) +} +``` + +해결 방법: service package에 `RunResult` 또는 동등 wrapper를 추가해 `RunDispatch`, `RunStream`, `Close`, `WaitTimeout`을 surface-neutral contract로 노출한다. `Service.SubmitRun`은 호환을 위해 `*RunHandle`을 반환해도 되지만, OpenAI/A2A local interface와 collector 함수는 `RunDispatch`/`RunStream` 중심 타입을 받게 한다. `RunHandle`은 legacy adapter로만 남긴다. + +수정 파일 및 체크리스트: + +- [ ] `apps/edge/internal/service/run_dispatch.go`: DTO 중심 helper/type 추가, `RunHandle` legacy 주석 갱신. +- [ ] `apps/edge/internal/openai/server.go`: `runService` interface 반환 타입을 DTO wrapper로 축소. +- [ ] `apps/edge/internal/openai/chat_handler.go`: dispatch metadata와 stream을 분리해 response model/run id 참조. +- [ ] `apps/edge/internal/openai/run_result.go`: `RunStream`과 timeout 값을 받도록 collector signature 변경. +- [ ] `apps/edge/internal/openai/stream.go`: stream 함수 signature에서 `RunHandle` 제거. +- [ ] `apps/edge/internal/input/a2a/server.go`: task id 생성과 drain 호출에서 dispatch/stream wrapper 사용. +- [ ] `apps/edge/internal/input/a2a/task_store.go`: `collectBlocking`, `collectBackground`, `drain`에서 `RunHandle` 직접 의존 제거. +- [ ] 관련 tests fake service 반환 타입 갱신. + +테스트 작성: 기존 OpenAI/A2A tests를 갱신하고, fake service가 `RunHandle`이 아니라 DTO wrapper를 반환하도록 바꾼다. 별도 신규 test 파일은 만들지 않는다. + +중간 검증: + +```bash +go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai ./apps/edge/internal/input/a2a +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/edge/internal/service/run_dispatch.go` | API-1 | +| `apps/edge/internal/openai/server.go` | API-1 | +| `apps/edge/internal/openai/chat_handler.go` | API-1 | +| `apps/edge/internal/openai/run_result.go` | API-1 | +| `apps/edge/internal/openai/stream.go` | API-1 | +| `apps/edge/internal/openai/server_test.go` | API-1 | +| `apps/edge/internal/input/a2a/server.go` | API-1 | +| `apps/edge/internal/input/a2a/task_store.go` | API-1 | +| `apps/edge/internal/input/a2a/server_test.go` | API-1 | +| `apps/edge/internal/input/a2a/task_store_test.go` | API-1 | + +## 최종 검증 + +```bash +go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai ./apps/edge/internal/input/a2a +``` + +예상 결과: 세 패키지 모두 `ok`. + +사용자 실행 파이프라인 영향 기록: 보조 smoke/full-cycle 실제 구동을 수행했으면 명령과 출력을 붙이고, 수행하지 못했으면 `testing-smoke` 기준으로 사유와 남은 위험을 `CODE_REVIEW`에 기록한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-bridge-boundary-hardening/02_terminal_core_boundary/CODE_REVIEW-cloud-G07.md b/agent-task/m-bridge-boundary-hardening/02_terminal_core_boundary/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..734def8 --- /dev/null +++ b/agent-task/m-bridge-boundary-hardening/02_terminal_core_boundary/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,121 @@ + + +# Code Review Reference - REFACTOR + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-07 +task=m-bridge-boundary-hardening/02_terminal_core_boundary, plan=0, tag=REFACTOR + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md` +- Task ids: + - `terminal-core-boundary`: persistent PTY 기반 `terminalSessionCore`를 CLI provider 내부 세부가 아니라 remote terminal transport가 재사용할 수 있는 Node terminal boundary로 분리하거나 명시 interface로 고정한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` → `plan_cloud_G07_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-bridge-boundary-hardening/02_terminal_core_boundary/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. PASS이고 task group이 `m-bridge-boundary-hardening`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REFACTOR-1] Extract Node-Owned Terminal Core Boundary | [ ] | + +## 구현 체크리스트 + +- [ ] PTY terminal core를 `apps/node/internal/terminal` 또는 동등 Node-owned boundary로 분리하고 `terminal-core-boundary` 검증 명령을 통과시킨다. +- [ ] CLI persistent executor가 새 boundary를 소비하도록 바꾸되 provider별 filtering/session behavior를 변경하지 않는다. +- [ ] 사용자 실행 파이프라인 영향에 대해 보조 smoke/full-cycle 수행 여부 또는 생략 사유를 검증 결과에 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다. +- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-bridge-boundary-hardening/02_terminal_core_boundary/`를 `agent-task/archive/YYYY/MM/m-bridge-boundary-hardening/02_terminal_core_boundary/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-bridge-boundary-hardening`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-bridge-boundary-hardening/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- New terminal boundary is not under `adapters/cli` and does not import CLI provider internals. +- CLI persistent behavior, prompt write delay, raw chunk output, resize/signal/close semantics remain covered. +- No remote terminal bridge protocol feature was added in this task. +- Verification output is pasted verbatim. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +### REFACTOR-1 중간 검증 + +```bash +$ go test -count=1 ./apps/node/internal/terminal ./apps/node/internal/adapters/cli +(output) +``` + +### 최종 검증 + +```bash +$ go test -count=1 ./apps/node/internal/adapters/cli ./apps/node/internal/... +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. diff --git a/agent-task/m-bridge-boundary-hardening/02_terminal_core_boundary/PLAN-cloud-G07.md b/agent-task/m-bridge-boundary-hardening/02_terminal_core_boundary/PLAN-cloud-G07.md new file mode 100644 index 0000000..f46dfcf --- /dev/null +++ b/agent-task/m-bridge-boundary-hardening/02_terminal_core_boundary/PLAN-cloud-G07.md @@ -0,0 +1,164 @@ + + +# Plan - REFACTOR + +## 이 파일을 읽는 구현 에이전트에게 + +이 작업은 구현 후 active 파일을 유지하고, 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채운 뒤 리뷰 준비 상태로 멈춘다. 구현 중 사용자 결정, 사용자 소유 외부 환경, 또는 계획 범위 충돌 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 남기고 중단한다. 구현 중 사용자에게 직접 질문하거나 `USER_REVIEW.md`, `complete.log`, archive log를 만들지 않는다. + +## 배경 + +Milestone의 `terminal-core-boundary` Task는 persistent PTY 기반 terminal core가 provider-specific CLI 구현 세부에 갇히지 않도록 Node terminal boundary를 고정하는 작업이다. 현재 `terminalSessionCore`는 `apps/node/internal/adapters/cli` 내부 private 타입이고, 출력 타입도 `cliOutput`에 묶여 있다. remote terminal bridge POC 전에 이 core를 Node 내부 재사용 경계로 옮기거나 명시 interface로 고정해야 한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지하며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 생성을 소유한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md` +- Task ids: + - `terminal-core-boundary`: persistent PTY 기반 `terminalSessionCore`를 CLI provider 내부 세부가 아니라 remote terminal transport가 재사용할 수 있는 Node terminal boundary로 분리하거나 명시 interface로 고정한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-roadmap/current.md` +- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md` +- `agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md` +- `agent-test/local/rules.md` +- `agent-test/local/node-smoke.md` +- `agent-test/local/testing-smoke.md` +- `agent-ops/rules/project/domain/node/rules.md` +- `agent-ops/rules/project/domain/testing/rules.md` +- `go.mod` +- `apps/node/internal/adapters/cli/cli.go` +- `apps/node/internal/adapters/cli/persistent.go` +- `apps/node/internal/adapters/cli/terminal_session.go` +- `apps/node/internal/adapters/cli/terminal_session_test.go` +- `apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go` +- `apps/node/internal/adapters/cli/status/tail_buffer.go` +- `apps/node/internal/runtime/types.go` + +### 테스트 환경 규칙 + +- test_env: `local`. +- `agent-test/local/rules.md`를 읽었고, domain routing에 따라 `node-smoke`, 사용자 실행 파이프라인 영향 기준으로 `testing-smoke`를 읽었다. +- 적용 명령: `go test -count=1 ./apps/node/internal/adapters/cli ./apps/node/internal/...`. +- 사전 확인 결과: 위 명령은 현재 checkout에서 성공했다. +- PTY/terminal 동작은 환경 민감도가 있으므로 실패 시 `command -v sh`, PTY 지원 skip 여부, 실제 stdout/stderr를 review stub에 기록한다. + +### 테스트 커버리지 공백 + +- `terminal_session_test.go`가 prompt write, snapshot, resize, close/idempotency, write/signal after close를 검증한다. +- `persistent_execute_blackbox_test.go`가 terminal persistent execution, prompt replay, cancel, marker, raw chunk, wide PTY를 검증한다. +- 현재 공백: core가 private CLI 타입(`cliOutput`, `status.Snapshot`)에 묶여 있어 package boundary 자체를 테스트하지 못한다. 새 boundary package test를 추가해 CLI와 무관한 core contract를 검증한다. + +### 심볼 참조 + +- 변경 예정: `terminalSessionCore`, `terminalSessionOptions`, `startTerminalSessionCore`, `cliOutput`, `status.Snapshot`. +- 현재 참조: `apps/node/internal/adapters/cli/terminal_session.go`, `apps/node/internal/adapters/cli/persistent.go`, `apps/node/internal/adapters/cli/cli.go`, `apps/node/internal/adapters/cli/terminal_session_test.go`. + +### 분할 판단 + +- split decision policy를 적용했다. +- 공유 task group: `m-bridge-boundary-hardening`. +- sibling subtasks: + - `01_edge_run_surface`: Edge run surface. 독립. + - `02_terminal_core_boundary`: Node terminal package/interface boundary. 독립. + - `03_adapter_config_typed`: typed adapter config contract. 독립. +- 이 subtask는 다른 sibling의 `complete.log`를 필요로 하지 않는다. + +### 범위 결정 근거 + +- remote terminal bridge open/input/output/resize/signal/close protocol은 구현하지 않는다. +- SSH/WinRM/serial transport는 범위 제외다. +- CLI provider별 filtering, Claude screen parsing, opencode/codex/antigravity session state는 이번 변경에서 동작 변경 없이 call site 조정만 한다. + +### 빌드 등급 + +- `cloud-G07`: PTY/process control과 terminal-agent behavior가 중심이며, package boundary 이동 중 미묘한 session lifecycle 회귀가 hard-to-review일 수 있다. + +## 구현 체크리스트 + +- [ ] PTY terminal core를 `apps/node/internal/terminal` 또는 동등 Node-owned boundary로 분리하고 `terminal-core-boundary` 검증 명령을 통과시킨다. +- [ ] CLI persistent executor가 새 boundary를 소비하도록 바꾸되 provider별 filtering/session behavior를 변경하지 않는다. +- [ ] 사용자 실행 파이프라인 영향에 대해 보조 smoke/full-cycle 수행 여부 또는 생략 사유를 검증 결과에 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [REFACTOR-1] Extract Node-Owned Terminal Core Boundary + +문제: PTY core가 CLI adapter private 타입으로 갇혀 있어 remote terminal transport가 재사용할 수 없다. + +Before: + +```go +// apps/node/internal/adapters/cli/terminal_session.go:23 +type terminalSessionCore struct { + cmd *exec.Cmd + input io.WriteCloser + output io.ReadCloser + tail *status.TailBuffer + outputCh chan cliOutput + doneCh chan error + closed bool + mu sync.Mutex +} +``` + +```go +// apps/node/internal/adapters/cli/persistent.go:345 +if profile.Terminal { + opts := terminalSessionOptions{ + Command: profile.Command, + Args: profile.Args, + Env: profile.Env, + } + core, err := startTerminalSessionCore(context.Background(), opts) +``` + +해결 방법: `apps/node/internal/terminal` package를 만들고 `Session`, `Options`, `Output`, `Snapshot` 타입과 `StartSession(ctx, opts)`를 정의한다. `terminal.Session`은 `Output() <-chan terminal.Output`, `Done() <-chan error`, `WritePrompt`, `WriteInput`, `Resize`, `Signal`, `Snapshot`, `Close`를 제공한다. CLI package는 `terminal.Output.Text`를 `cliOutput{text: ...}`로 어댑트하고, markerLine 없는 raw terminal semantics를 유지한다. + +수정 파일 및 체크리스트: + +- [ ] `apps/node/internal/terminal/session.go`: PTY core와 public Node-internal interface 구현. +- [ ] `apps/node/internal/terminal/session_test.go`: 기존 terminal core tests 이동/갱신. +- [ ] `apps/node/internal/adapters/cli/terminal_session.go`: 제거하거나 CLI adapter shim만 남긴다. +- [ ] `apps/node/internal/adapters/cli/cli.go`: `profileSession.core` 타입을 새 interface로 변경. +- [ ] `apps/node/internal/adapters/cli/persistent.go`: `StartSession` 호출과 output adapter 적용. +- [ ] `apps/node/internal/adapters/cli/terminal_session_test.go`: 새 package test로 이동하거나 CLI-specific integration만 남긴다. +- [ ] `apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go`: 기존 behavior regression 유지. + +테스트 작성: 새 `apps/node/internal/terminal/session_test.go`를 작성한다. 기존 CLI terminal core tests를 이동해 package boundary를 직접 검증하고, CLI persistent blackbox tests는 그대로 유지한다. + +중간 검증: + +```bash +go test -count=1 ./apps/node/internal/terminal ./apps/node/internal/adapters/cli +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/node/internal/terminal/session.go` | REFACTOR-1 | +| `apps/node/internal/terminal/session_test.go` | REFACTOR-1 | +| `apps/node/internal/adapters/cli/terminal_session.go` | REFACTOR-1 | +| `apps/node/internal/adapters/cli/cli.go` | REFACTOR-1 | +| `apps/node/internal/adapters/cli/persistent.go` | REFACTOR-1 | +| `apps/node/internal/adapters/cli/terminal_session_test.go` | REFACTOR-1 | +| `apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go` | REFACTOR-1 | + +## 최종 검증 + +```bash +go test -count=1 ./apps/node/internal/adapters/cli ./apps/node/internal/... +``` + +예상 결과: Node internal packages가 모두 `ok`, no-test packages는 `[no test files]`. + +사용자 실행 파이프라인 영향 기록: 보조 smoke/full-cycle 실제 구동을 수행했으면 명령과 출력을 붙이고, 수행하지 못했으면 `testing-smoke` 기준으로 사유와 남은 위험을 `CODE_REVIEW`에 기록한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-bridge-boundary-hardening/03_adapter_config_typed/CODE_REVIEW-cloud-G06.md b/agent-task/m-bridge-boundary-hardening/03_adapter_config_typed/CODE_REVIEW-cloud-G06.md new file mode 100644 index 0000000..877757f --- /dev/null +++ b/agent-task/m-bridge-boundary-hardening/03_adapter_config_typed/CODE_REVIEW-cloud-G06.md @@ -0,0 +1,143 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-07 +task=m-bridge-boundary-hardening/03_adapter_config_typed, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md` +- Task ids: + - `adapter-config-typed`: `AdapterConfig.settings`는 구버전 Edge payload fallback으로만 허용하고, Edge mapper와 신규 config는 typed oneof/proto 메시지를 사용한다는 guard를 테스트로 고정한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G06.md` → `code_review_cloud_G06_N.log`, `PLAN-cloud-G06.md` → `plan_cloud_G06_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-bridge-boundary-hardening/03_adapter_config_typed/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. PASS이고 task group이 `m-bridge-boundary-hardening`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Typed Mock Adapter Config | [ ] | + +## 구현 체크리스트 + +- [ ] Mock adapter config를 typed oneof/proto 메시지로 추가하고 generated Go/Dart 바인딩을 동기화한다. +- [ ] Edge mapper와 transport tests가 신규 Edge-generated payload에서 `settings`를 사용하지 않음을 검증하게 한다. +- [ ] Node legacy fallback tests는 유지하고, 필요한 경우 typed mock config가 기존 mock adapter와 호환됨을 검증한다. +- [ ] 사용자 실행 파이프라인 영향에 대해 보조 smoke/full-cycle 수행 여부 또는 생략 사유를 검증 결과에 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G06_N.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G06_M.log`로 아카이브한다. +- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-bridge-boundary-hardening/03_adapter_config_typed/`를 `agent-task/archive/YYYY/MM/m-bridge-boundary-hardening/03_adapter_config_typed/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-bridge-boundary-hardening`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-bridge-boundary-hardening/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G06.md`와 `CODE_REVIEW-cloud-G06.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- Edge-generated adapter configs do not populate legacy `Settings`. +- Mock/CLI/Ollama/VLLM configs each use typed oneof fields. +- Node legacy `settings` fallback behavior remains covered and compatible. +- Generated Go and Dart proto files are in sync with `proto/iop/runtime.proto`. +- Verification output is pasted verbatim. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +### API-1 중간 검증 + +```bash +$ make proto +(output) +``` + +```bash +$ make proto-dart +(output) +``` + +```bash +$ go test -count=1 ./apps/edge/internal/node ./apps/edge/internal/transport ./apps/node/internal/adapters ./proto/gen/... +(output) +``` + +### 최종 검증 + +```bash +$ make proto +(output) +``` + +```bash +$ make proto-dart +(output) +``` + +```bash +$ go test -count=1 ./apps/edge/internal/node ./apps/edge/internal/transport ./apps/node/internal/adapters ./proto/gen/... +(output) +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. diff --git a/agent-task/m-bridge-boundary-hardening/03_adapter_config_typed/PLAN-cloud-G06.md b/agent-task/m-bridge-boundary-hardening/03_adapter_config_typed/PLAN-cloud-G06.md new file mode 100644 index 0000000..bab0f2d --- /dev/null +++ b/agent-task/m-bridge-boundary-hardening/03_adapter_config_typed/PLAN-cloud-G06.md @@ -0,0 +1,166 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +이 작업은 구현 후 active 파일을 유지하고, 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채운 뒤 리뷰 준비 상태로 멈춘다. 구현 중 사용자 결정, 사용자 소유 외부 환경, 또는 계획 범위 충돌 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 남기고 중단한다. 구현 중 사용자에게 직접 질문하거나 `USER_REVIEW.md`, `complete.log`, archive log를 만들지 않는다. + +## 배경 + +Milestone의 `adapter-config-typed` Task는 `AdapterConfig.settings`를 legacy fallback으로만 남기고, Edge가 새 adapter config를 typed oneof/proto 메시지로만 생성한다는 계약을 고정하는 작업이다. 현재 CLI/Ollama/VLLM은 typed config를 사용하지만 Edge mapper의 mock adapter payload는 `structpb.NewStruct(nil)`로 `settings`를 채워 신규 payload에서도 legacy 필드를 사용한다. 이 plan은 mock까지 typed oneof로 옮기고, 기존 Node legacy fallback은 호환 경로로 유지한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지하며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 생성을 소유한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md` +- Task ids: + - `adapter-config-typed`: `AdapterConfig.settings`는 구버전 Edge payload fallback으로만 허용하고, Edge mapper와 신규 config는 typed oneof/proto 메시지를 사용한다는 guard를 테스트로 고정한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-roadmap/current.md` +- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md` +- `agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md` +- `agent-test/local/rules.md` +- `agent-test/local/edge-smoke.md` +- `agent-test/local/node-smoke.md` +- `agent-test/local/platform-common-smoke.md` +- `agent-test/local/testing-smoke.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/rules/project/domain/testing/rules.md` +- `proto/iop/runtime.proto` +- `apps/edge/internal/node/mapper.go` +- `apps/edge/internal/node/mapper_test.go` +- `apps/edge/internal/transport/server_test.go` +- `apps/node/internal/adapters/factory.go` +- `apps/node/internal/adapters/factory_internal_test.go` +- `apps/node/internal/adapters/adapters_blackbox_test.go` +- `apps/node/internal/bootstrap/module_test.go` +- `apps/node/internal/transport/integration_test.go` +- `Makefile` + +### 테스트 환경 규칙 + +- test_env: `local`. +- `agent-test/local/rules.md`를 읽었고, domain routing에 따라 `edge-smoke`, `node-smoke`, `platform-common-smoke`, 사용자 실행 파이프라인 영향 기준으로 `testing-smoke`를 읽었다. +- 적용 명령: `make proto && make proto-dart && go test -count=1 ./apps/edge/internal/node ./apps/edge/internal/transport ./apps/node/internal/adapters ./proto/gen/...`. +- 사전 확인 결과: `go test -count=1 ./apps/edge/internal/node ./apps/edge/internal/transport ./apps/node/internal/adapters ./proto/gen/...`는 현재 checkout에서 성공했다. +- 도구 확인 결과: `protoc`, `protoc-gen-go`, `protoc-gen-dart`가 현재 환경에 있다. +- Milestone 검증 명령은 `make proto && go test ...`이지만, proto 변경 시 tracked Dart binding drift를 막기 위해 `make proto-dart`도 최종 검증에 포함한다. 구현자가 Dart generation을 생략하면 이유와 남은 위험을 review stub에 기록한다. + +### 테스트 커버리지 공백 + +- `apps/edge/internal/node/mapper_test.go`는 CLI/Ollama/VLLM typed payload를 검증한다. +- `apps/edge/internal/transport/server_test.go`는 Edge transport payload의 adapter config를 검증한다. +- `apps/node/internal/adapters/factory_internal_test.go`와 `apps/node/internal/bootstrap/module_test.go`는 legacy `settings` fallback 경로를 보유한다. +- 현재 공백: Edge가 생성하는 mock adapter config에서 `settings == nil`이고 typed mock config가 설정된다는 guard가 없다. + +### 심볼 참조 + +- 변경 예정: `AdapterConfig` oneof, `BuildConfigPayload`, Edge mapper/transport tests. +- 유지 예정: Node `BuildFromPayload`의 legacy `settings` fallback, bootstrap legacy regression tests. +- 현재 legacy 생성 지점: `apps/edge/internal/node/mapper.go`의 mock adapter branch가 `structpb.NewStruct(nil)`를 `Settings`에 채운다. + +### 분할 판단 + +- split decision policy를 적용했다. +- 공유 task group: `m-bridge-boundary-hardening`. +- sibling subtasks: + - `01_edge_run_surface`: Edge run surface. 독립. + - `02_terminal_core_boundary`: Node terminal package/interface boundary. 독립. + - `03_adapter_config_typed`: typed adapter config contract. 독립. +- 이 subtask는 다른 sibling의 `complete.log`를 필요로 하지 않는다. + +### 범위 결정 근거 + +- legacy `AdapterConfig.settings` fallback 즉시 제거는 Milestone 범위 제외이므로 제거하지 않는다. +- adapter execution behavior, provider defaults, terminal bridge protocol은 변경하지 않는다. +- proto field number는 새 oneof field를 추가해 호환성을 유지하고 기존 `settings = 3`, `cli = 4`, `ollama = 5`, `vllm = 6` 번호는 재사용하지 않는다. + +### 빌드 등급 + +- `cloud-G06`: proto 계약과 Edge/Node 양쪽 테스트를 함께 만지지만 동작 변경은 mock config payload guard에 제한되고 실행 경로는 deterministic하다. + +## 구현 체크리스트 + +- [ ] Mock adapter config를 typed oneof/proto 메시지로 추가하고 generated Go/Dart 바인딩을 동기화한다. +- [ ] Edge mapper와 transport tests가 신규 Edge-generated payload에서 `settings`를 사용하지 않음을 검증하게 한다. +- [ ] Node legacy fallback tests는 유지하고, 필요한 경우 typed mock config가 기존 mock adapter와 호환됨을 검증한다. +- [ ] 사용자 실행 파이프라인 영향에 대해 보조 smoke/full-cycle 수행 여부 또는 생략 사유를 검증 결과에 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] Typed Mock Adapter Config + +문제: 신규 Edge mapper payload가 mock adapter에 대해 legacy `settings`를 사용한다. + +Before: + +```go +// apps/edge/internal/node/mapper.go +settings, err := structpb.NewStruct(nil) +if err != nil { + return nil, err +} +config = &runtimev1.AdapterConfig{Adapter: adapter, Settings: settings} +``` + +해결 방법: `proto/iop/runtime.proto`에 empty `MockAdapterConfig` message를 추가하고 `AdapterConfig.config` oneof에 새 field로 연결한다. Edge mapper는 mock adapter에 `AdapterConfig_Mock`을 채우며 `Settings`는 nil로 둔다. Node adapter factory는 mock config를 명시적으로 요구하지 않는 현재 동작을 유지하되, typed mock config가 들어와도 실패하지 않도록 필요한 테스트를 보강한다. + +수정 파일 및 체크리스트: + +- [ ] `proto/iop/runtime.proto`: `MockAdapterConfig` message와 `AdapterConfig.mock` oneof field 추가. +- [ ] `proto/gen/go/iop/runtime/v1/runtime.pb.go`: `make proto`로 재생성. +- [ ] `apps/client/lib/gen/iop/runtime/v1/runtime.pb.dart`: `make proto-dart`로 재생성. +- [ ] `apps/client/lib/gen/iop/runtime/v1/runtime.pbenum.dart`: 필요 시 `make proto-dart`로 재생성. +- [ ] `apps/client/lib/gen/iop/runtime/v1/runtime.pbjson.dart`: 필요 시 `make proto-dart`로 재생성. +- [ ] `apps/edge/internal/node/mapper.go`: mock branch에서 typed mock config 사용, `structpb` import 제거. +- [ ] `apps/edge/internal/node/mapper_test.go`: mock/cli/ollama/vllm Edge-generated config 모두 `Settings == nil`이고 적절한 typed config를 갖는다는 guard 추가. +- [ ] `apps/edge/internal/transport/server_test.go`: transport payload 검증에서 legacy settings가 신규 payload에 섞이지 않음을 확인. +- [ ] `apps/node/internal/adapters/factory_internal_test.go`: 기존 legacy fallback tests 유지, typed mock config 호환 guard가 필요하면 추가. + +테스트 작성: 기존 mapper/transport/factory test 파일 안에 case를 추가한다. 새 테스트 파일은 만들지 않는다. + +중간 검증: + +```bash +make proto +make proto-dart +go test -count=1 ./apps/edge/internal/node ./apps/edge/internal/transport ./apps/node/internal/adapters ./proto/gen/... +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `proto/iop/runtime.proto` | API-1 | +| `proto/gen/go/iop/runtime/v1/runtime.pb.go` | API-1 | +| `apps/client/lib/gen/iop/runtime/v1/runtime.pb.dart` | API-1 | +| `apps/client/lib/gen/iop/runtime/v1/runtime.pbenum.dart` | API-1 | +| `apps/client/lib/gen/iop/runtime/v1/runtime.pbjson.dart` | API-1 | +| `apps/edge/internal/node/mapper.go` | API-1 | +| `apps/edge/internal/node/mapper_test.go` | API-1 | +| `apps/edge/internal/transport/server_test.go` | API-1 | +| `apps/node/internal/adapters/factory_internal_test.go` | API-1 | + +## 최종 검증 + +```bash +make proto +make proto-dart +go test -count=1 ./apps/edge/internal/node ./apps/edge/internal/transport ./apps/node/internal/adapters ./proto/gen/... +``` + +예상 결과: proto generation succeeds without tracked drift, listed Go packages all pass. + +사용자 실행 파이프라인 영향 기록: config/proto contract가 runtime startup payload에 닿으므로 보조 smoke/full-cycle 실제 구동을 수행했으면 명령과 출력을 붙이고, 수행하지 못했으면 `testing-smoke` 기준으로 사유와 남은 위험을 `CODE_REVIEW`에 기록한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/client/lib/client_home_page.dart b/apps/client/lib/client_home_page.dart index b507856..e4058a5 100644 --- a/apps/client/lib/client_home_page.dart +++ b/apps/client/lib/client_home_page.dart @@ -31,16 +31,20 @@ class _ClientHomePageState extends State { String _wireStatus = 'Disconnected'; ClientWireClient? _client; StreamSubscription? _notificationSubscription; + HttpControlPlaneStatusRepository? _ownedStatusRepository; late final ControlPlaneStatusController _controller; @override void initState() { super.initState(); - final statusRepo = widget.statusRepository ?? - HttpControlPlaneStatusRepository( - baseUrl: ClientConfig.controlPlaneHttpUrl, - ); + final ownedStatusRepository = widget.statusRepository == null + ? HttpControlPlaneStatusRepository( + baseUrl: ClientConfig.controlPlaneHttpUrl, + ) + : null; + _ownedStatusRepository = ownedStatusRepository; + final statusRepo = widget.statusRepository ?? ownedStatusRepository!; _controller = ControlPlaneStatusController(statusRepository: statusRepo); // Auto-connect on start @@ -62,6 +66,7 @@ class _ClientHomePageState extends State { _notificationSubscription?.cancel(); _client?.close(); _controller.dispose(); + _ownedStatusRepository?.close(); super.dispose(); } @@ -85,10 +90,7 @@ class _ClientHomePageState extends State { } Future _connectAndFetch() async { - await Future.wait([ - _connectWire(), - _controller.fetchEdges(), - ]); + await Future.wait([_connectWire(), _controller.fetchEdges()]); } Future _connectWire() async { @@ -166,12 +168,19 @@ class _ClientHomePageState extends State { nodes: NodesPanel( edges: _controller.edges, selectedEdgeId: currentEdgeId, - selectedEdgeStatus: currentEdgeId != null ? _controller.edgeStatuses[currentEdgeId] : null, - isLoading: currentEdgeId != null ? (_controller.loadingStatuses[currentEdgeId] ?? false) : false, - error: currentEdgeId != null ? _controller.statusErrors[currentEdgeId] : null, + selectedEdgeStatus: currentEdgeId != null + ? _controller.edgeStatuses[currentEdgeId] + : null, + isLoading: currentEdgeId != null + ? (_controller.loadingStatuses[currentEdgeId] ?? false) + : false, + error: currentEdgeId != null + ? _controller.statusErrors[currentEdgeId] + : null, onSelectEdge: _controller.selectEdge, onRefresh: () { - if (currentEdgeId != null) _controller.fetchEdgeStatus(currentEdgeId); + if (currentEdgeId != null) + _controller.fetchEdgeStatus(currentEdgeId); }, ), runtime: RuntimePanel( @@ -183,12 +192,19 @@ class _ClientHomePageState extends State { executionLogs: ExecutionLogsPanel( edges: _controller.edges, selectedEdgeId: currentEdgeId, - events: currentEdgeId != null ? (_controller.edgeEvents[currentEdgeId] ?? []) : [], - isLoading: currentEdgeId != null ? (_controller.loadingEvents[currentEdgeId] ?? false) : false, - error: currentEdgeId != null ? _controller.eventErrors[currentEdgeId] : null, + events: currentEdgeId != null + ? (_controller.edgeEvents[currentEdgeId] ?? []) + : [], + isLoading: currentEdgeId != null + ? (_controller.loadingEvents[currentEdgeId] ?? false) + : false, + error: currentEdgeId != null + ? _controller.eventErrors[currentEdgeId] + : null, onSelectEdge: _controller.selectEdge, onRefresh: () { - if (currentEdgeId != null) _controller.fetchEdgeEvents(currentEdgeId); + if (currentEdgeId != null) + _controller.fetchEdgeEvents(currentEdgeId); }, ), agent: const IopAgentPanel(capabilities: iopDefaultCapabilityPack), diff --git a/apps/client/lib/control_plane_status_repository.dart b/apps/client/lib/control_plane_status_repository.dart index f730c6a..7cf915a 100644 --- a/apps/client/lib/control_plane_status_repository.dart +++ b/apps/client/lib/control_plane_status_repository.dart @@ -18,11 +18,20 @@ abstract class ControlPlaneStatusRepository { class HttpControlPlaneStatusRepository implements ControlPlaneStatusRepository { final String baseUrl; - final http.Client? client; + final http.Client _client; + final bool _ownsClient; - HttpControlPlaneStatusRepository({required this.baseUrl, this.client}); + HttpControlPlaneStatusRepository({required this.baseUrl, http.Client? client}) + : _client = client ?? http.Client(), + _ownsClient = client == null; - http.Client get _httpClient => client ?? http.Client(); + http.Client get _httpClient => _client; + + void close() { + if (_ownsClient) { + _client.close(); + } + } @override Future> fetchEdges() async { @@ -32,14 +41,20 @@ class HttpControlPlaneStatusRepository implements ControlPlaneStatusRepository { } final body = json.decode(response.body) as Map; final list = body['edges'] as List? ?? []; - return list.map((e) => EdgeRegistryView.fromJson(e as Map)).toList(); + return list + .map((e) => EdgeRegistryView.fromJson(e as Map)) + .toList(); } @override Future fetchEdgeStatus(String edgeId) async { - final response = await _httpClient.get(Uri.parse('$baseUrl/edges/$edgeId/status')); + final response = await _httpClient.get( + Uri.parse('$baseUrl/edges/$edgeId/status'), + ); if (response.statusCode != 200) { - throw Exception('Failed to fetch status for $edgeId: ${response.statusCode}'); + throw Exception( + 'Failed to fetch status for $edgeId: ${response.statusCode}', + ); } final body = json.decode(response.body) as Map; return EdgeStatusResponseView.fromJson(body); @@ -47,13 +62,19 @@ class HttpControlPlaneStatusRepository implements ControlPlaneStatusRepository { @override Future> fetchEdgeEvents(String edgeId) async { - final response = await _httpClient.get(Uri.parse('$baseUrl/edges/$edgeId/events')); + final response = await _httpClient.get( + Uri.parse('$baseUrl/edges/$edgeId/events'), + ); if (response.statusCode != 200) { - throw Exception('Failed to fetch events for $edgeId: ${response.statusCode}'); + throw Exception( + 'Failed to fetch events for $edgeId: ${response.statusCode}', + ); } final body = json.decode(response.body) as Map; final list = body['events'] as List? ?? []; - return list.map((e) => EdgeNodeEventView.fromJson(e as Map)).toList(); + return list + .map((e) => EdgeNodeEventView.fromJson(e as Map)) + .toList(); } @override @@ -68,9 +89,13 @@ class HttpControlPlaneStatusRepository implements ControlPlaneStatusRepository { @override Future fetchEdgeOperations(String edgeId) async { - final response = await _httpClient.get(Uri.parse('$baseUrl/edges/$edgeId/operations')); + final response = await _httpClient.get( + Uri.parse('$baseUrl/edges/$edgeId/operations'), + ); if (response.statusCode != 200) { - throw Exception('Failed to fetch operations for $edgeId: ${response.statusCode}'); + throw Exception( + 'Failed to fetch operations for $edgeId: ${response.statusCode}', + ); } final body = json.decode(response.body) as Map; return EdgeOperationsResponseView.fromJson(body); @@ -83,9 +108,7 @@ class HttpControlPlaneStatusRepository implements ControlPlaneStatusRepository { String? targetSelector, Map? parameters, }) async { - final Map requestBody = { - 'operation': operation, - }; + final Map requestBody = {'operation': operation}; if (targetSelector != null) { requestBody['target_selector'] = targetSelector; } @@ -99,7 +122,9 @@ class HttpControlPlaneStatusRepository implements ControlPlaneStatusRepository { body: json.encode(requestBody), ); if (response.statusCode != 200 && response.statusCode != 202) { - throw Exception('Failed to send command to $edgeId: ${response.statusCode}'); + throw Exception( + 'Failed to send command to $edgeId: ${response.statusCode}', + ); } final body = json.decode(response.body) as Map; return EdgeCommandResponseView.fromJson(body); diff --git a/apps/client/test/control_plane_status_controller_test.dart b/apps/client/test/control_plane_status_controller_test.dart index a667dc8..8f00bbb 100644 --- a/apps/client/test/control_plane_status_controller_test.dart +++ b/apps/client/test/control_plane_status_controller_test.dart @@ -1,9 +1,37 @@ import 'dart:async'; +import 'dart:convert'; +import 'package:http/http.dart' as http; import 'package:flutter_test/flutter_test.dart'; import 'package:iop_client/control_plane_status_controller.dart'; import 'package:iop_client/control_plane_status_client.dart'; import 'widget_test.dart'; +class TrackingHttpClient extends http.BaseClient { + int requestCount = 0; + int closeCount = 0; + + @override + Future send(http.BaseRequest request) async { + requestCount += 1; + final response = http.Response( + json.encode({'edges': >[]}), + 200, + ); + return http.StreamedResponse( + Stream>.fromIterable([response.bodyBytes]), + response.statusCode, + headers: response.headers, + request: request, + ); + } + + @override + void close() { + closeCount += 1; + super.close(); + } +} + class ControllableStatusRepository implements ControlPlaneStatusRepository { final fleetCompleter = Completer(); final statusCompleter = Completer(); @@ -58,6 +86,20 @@ class ControllableStatusRepository implements ControlPlaneStatusRepository { } void main() { + test('HTTP repository keeps injected client caller-owned', () async { + final client = TrackingHttpClient(); + final repository = HttpControlPlaneStatusRepository( + baseUrl: 'http://control-plane.test', + client: client, + ); + + await repository.fetchEdges(); + repository.close(); + + expect(client.requestCount, 1); + expect(client.closeCount, 0); + }); + test( 'removes stale selected edge caches when fleet refresh drops edge', () async { diff --git a/apps/edge/internal/input/a2a/server.go b/apps/edge/internal/input/a2a/server.go index a075593..54086b0 100644 --- a/apps/edge/internal/input/a2a/server.go +++ b/apps/edge/internal/input/a2a/server.go @@ -17,7 +17,7 @@ import ( ) type runService interface { - SubmitRun(context.Context, edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) + SubmitRun(context.Context, edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) CancelRun(context.Context, edgeservice.CancelRunRequest) (edgeservice.CommandResult, error) } @@ -182,7 +182,7 @@ func (s *Server) handleMessageSend(w http.ResponseWriter, r *http.Request, rpcRe return } - taskID := s.tasks.create(handle.RunID, params.Message) + taskID := s.tasks.create(handle.Dispatch().RunID, params.Message) var snapshot *Task if blocking { diff --git a/apps/edge/internal/input/a2a/server_test.go b/apps/edge/internal/input/a2a/server_test.go index 4b6776d..0b11dc4 100644 --- a/apps/edge/internal/input/a2a/server_test.go +++ b/apps/edge/internal/input/a2a/server_test.go @@ -18,11 +18,11 @@ import ( // fakeService is a minimal runService implementation for testing. type fakeService struct { - submitFn func(context.Context, edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) + submitFn func(context.Context, edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) cancelFn func(context.Context, edgeservice.CancelRunRequest) (edgeservice.CommandResult, error) } -func (f *fakeService) SubmitRun(ctx context.Context, req edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) { +func (f *fakeService) SubmitRun(ctx context.Context, req edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) { if f.submitFn != nil { return f.submitFn(ctx, req) } @@ -43,7 +43,7 @@ func newTestServer(cfg config.EdgeA2AConf, svc runServiceIface) *a2a.Server { } type runServiceIface interface { - SubmitRun(context.Context, edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) + SubmitRun(context.Context, edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) CancelRun(context.Context, edgeservice.CancelRunRequest) (edgeservice.CommandResult, error) } @@ -72,7 +72,7 @@ func decodeRPCResponse(t *testing.T, w *httptest.ResponseRecorder) a2a.JSONRPCRe func TestHandleMessageSendDispatchesRun(t *testing.T) { dispatched := false svc := &fakeService{ - submitFn: func(_ context.Context, req edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) { + submitFn: func(_ context.Context, req edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) { dispatched = true if req.Metadata["source"] != "a2a" { t.Errorf("expected source=a2a, got %q", req.Metadata["source"]) @@ -115,7 +115,7 @@ func TestHandleMessageSendDispatchesRun(t *testing.T) { func TestHandleGetTaskReturnsStoredTask(t *testing.T) { runID := "run-get-1" svc := &fakeService{ - submitFn: func(_ context.Context, _ edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) { + submitFn: func(_ context.Context, _ edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) { return feedHandle(runID, &iop.RunEvent{RunId: runID, Type: "delta", Delta: "result text"}, &iop.RunEvent{RunId: runID, Type: "complete"}, @@ -187,7 +187,7 @@ func TestHandleCancelTask(t *testing.T) { runID := "run-cancel-1" cancelCalled := false svc := &fakeService{ - submitFn: func(_ context.Context, _ edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) { + submitFn: func(_ context.Context, _ edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) { // Return a handle that stays working (never completes) for non-blocking. events := make(chan *iop.RunEvent, 1) nodeEvents := make(chan *iop.EdgeNodeEvent, 1) @@ -316,7 +316,7 @@ func TestBlockingDefault(t *testing.T) { nodeEvents := make(chan *iop.EdgeNodeEvent, 1) var dispatched bool svc := &fakeService{ - submitFn: func(_ context.Context, req edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) { + submitFn: func(_ context.Context, req edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) { dispatched = true got := req.Metadata["blocking"] want := "true" diff --git a/apps/edge/internal/input/a2a/task_store.go b/apps/edge/internal/input/a2a/task_store.go index 59c1e8b..7d54664 100644 --- a/apps/edge/internal/input/a2a/task_store.go +++ b/apps/edge/internal/input/a2a/task_store.go @@ -79,32 +79,33 @@ func (s *TaskStore) create(id string, userMsg Message) string { return id } -// collectBackground drains a RunHandle in a background goroutine and updates the stored Task. -func (s *TaskStore) collectBackground(taskID string, handle *edgeservice.RunHandle) { +// collectBackground drains a RunResult in a background goroutine and updates the stored Task. +func (s *TaskStore) collectBackground(taskID string, handle edgeservice.RunResult) { go func() { defer handle.Close() s.drain(taskID, handle) }() } -// collectBlocking drains a RunHandle synchronously and returns the final Task snapshot. -func (s *TaskStore) collectBlocking(taskID string, handle *edgeservice.RunHandle) *Task { +// collectBlocking drains a RunResult synchronously and returns the final Task snapshot. +func (s *TaskStore) collectBlocking(taskID string, handle edgeservice.RunResult) *Task { defer handle.Close() return s.drain(taskID, handle) } -func (s *TaskStore) drain(taskID string, handle *edgeservice.RunHandle) *Task { +func (s *TaskStore) drain(taskID string, handle edgeservice.RunResult) *Task { var text strings.Builder timeout := time.NewTimer(handle.WaitTimeout()) defer timeout.Stop() + stream := handle.Stream() for { select { - case nodeEvent := <-handle.NodeEvents: + case nodeEvent := <-stream.NodeEvents: if edgeservice.IsNodeDisconnected(nodeEvent) { return s.setFailed(taskID, "node disconnected") } - case event := <-handle.Events: + case event := <-stream.Events: if event == nil { continue } diff --git a/apps/edge/internal/input/a2a/task_store_test.go b/apps/edge/internal/input/a2a/task_store_test.go index e28b3a7..b07bac5 100644 --- a/apps/edge/internal/input/a2a/task_store_test.go +++ b/apps/edge/internal/input/a2a/task_store_test.go @@ -26,10 +26,10 @@ func feedHandle(runID string, events ...*iop.RunEvent) *edgeservice.RunHandle { } } -func checkBlockingResult(t *testing.T, handle *edgeservice.RunHandle, check func(map[string]any)) { +func checkBlockingResult(t *testing.T, handle edgeservice.RunResult, check func(map[string]any)) { t.Helper() svc := &fakeService{ - submitFn: func(_ context.Context, _ edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) { + submitFn: func(_ context.Context, _ edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) { return handle, nil }, } @@ -134,7 +134,7 @@ func TestCancelTaskSendsCancelRun(t *testing.T) { } svc := &fakeService{ - submitFn: func(_ context.Context, _ edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) { + submitFn: func(_ context.Context, _ edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) { return handle, nil }, cancelFn: func(_ context.Context, req edgeservice.CancelRunRequest) (edgeservice.CommandResult, error) { diff --git a/apps/edge/internal/openai/chat_handler.go b/apps/edge/internal/openai/chat_handler.go index 853ff9f..beb2908 100644 --- a/apps/edge/internal/openai/chat_handler.go +++ b/apps/edge/internal/openai/chat_handler.go @@ -86,15 +86,15 @@ func (s *Server) handleChatCompletions(w http.ResponseWriter, r *http.Request) { s.completeChatCompletion(w, r, req, handle, outputPolicy) } -func (s *Server) completeChatCompletion(w http.ResponseWriter, r *http.Request, req chatCompletionRequest, handle *edgeservice.RunHandle, outputPolicy strictOutputPolicy) { - text, reasoning, usage, err := collectRunResult(r.Context(), handle) +func (s *Server) completeChatCompletion(w http.ResponseWriter, r *http.Request, req chatCompletionRequest, handle edgeservice.RunResult, outputPolicy strictOutputPolicy) { + text, reasoning, usage, err := collectRunResult(r.Context(), handle.Stream(), handle.WaitTimeout()) if err != nil { writeError(w, httpStatusForRunError(err), "run_error", err.Error()) return } text, reasoning, normalized := normalizeCompletionOutput(outputPolicy, text, reasoning) s.logger.Info("openai chat completion output", - zap.String("run_id", handle.RunID), + zap.String("run_id", handle.Dispatch().RunID), zap.Bool("strict_output", outputPolicy.Strict), zap.Bool("strict_stream_buffer", outputPolicy.StreamBuffer), zap.String("xml_completion_tool", outputPolicy.XMLCompletionTool), @@ -105,10 +105,10 @@ func (s *Server) completeChatCompletion(w http.ResponseWriter, r *http.Request, ) created := time.Now().Unix() writeJSON(w, http.StatusOK, chatCompletionResponse{ - ID: "chatcmpl-" + handle.RunID, + ID: "chatcmpl-" + handle.Dispatch().RunID, Object: "chat.completion", Created: created, - Model: responseModel(req.Model, handle.Target), + Model: responseModel(req.Model, handle.Dispatch().Target), Choices: []chatCompletionChoice{{ Index: 0, Message: chatMessage{Role: "assistant", Content: text, ReasoningContent: reasoning}, diff --git a/apps/edge/internal/openai/run_result.go b/apps/edge/internal/openai/run_result.go index 6cecccb..19fee31 100644 --- a/apps/edge/internal/openai/run_result.go +++ b/apps/edge/internal/openai/run_result.go @@ -9,11 +9,11 @@ import ( edgeservice "iop/apps/edge/internal/service" ) -func collectRunResult(ctx context.Context, handle *edgeservice.RunHandle) (string, string, *openAIUsage, error) { +func collectRunResult(ctx context.Context, stream edgeservice.RunStream, timeout time.Duration) (string, string, *openAIUsage, error) { var contentBuilder strings.Builder var reasoningBuilder strings.Builder var usage *openAIUsage - timer := time.NewTimer(handle.WaitTimeout()) + timer := time.NewTimer(timeout) defer timer.Stop() for { select { @@ -21,11 +21,11 @@ func collectRunResult(ctx context.Context, handle *edgeservice.RunHandle) (strin return "", "", nil, ctx.Err() case <-timer.C: return "", "", nil, fmt.Errorf("run timed out") - case nodeEvent := <-handle.NodeEvents: + case nodeEvent := <-stream.NodeEvents: if edgeservice.IsNodeDisconnected(nodeEvent) { return "", "", nil, fmt.Errorf("node disconnected") } - case event := <-handle.Events: + case event := <-stream.Events: if event == nil { continue } diff --git a/apps/edge/internal/openai/server.go b/apps/edge/internal/openai/server.go index 25745e0..9c2ad6a 100644 --- a/apps/edge/internal/openai/server.go +++ b/apps/edge/internal/openai/server.go @@ -15,7 +15,7 @@ import ( ) type runService interface { - SubmitRun(context.Context, edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) + SubmitRun(context.Context, edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) OllamaAPI(context.Context, edgeservice.OllamaAPIRequest) (edgeservice.OllamaAPIView, error) } diff --git a/apps/edge/internal/openai/server_test.go b/apps/edge/internal/openai/server_test.go index aca0e5c..18e09af 100644 --- a/apps/edge/internal/openai/server_test.go +++ b/apps/edge/internal/openai/server_test.go @@ -21,7 +21,7 @@ type fakeRunService struct { events chan *iop.RunEvent } -func (s *fakeRunService) SubmitRun(_ context.Context, req edgeservice.SubmitRunRequest) (*edgeservice.RunHandle, error) { +func (s *fakeRunService) SubmitRun(_ context.Context, req edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) { s.req = req return &edgeservice.RunHandle{ RunDispatch: edgeservice.RunDispatch{ @@ -413,7 +413,7 @@ func TestCollectRunResultTimesOut(t *testing.T) { } ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond) defer cancel() - _, _, _, err := collectRunResult(ctx, handle) + _, _, _, err := collectRunResult(ctx, handle.Stream(), handle.WaitTimeout()) if err == nil { t.Fatal("expected timeout error") } diff --git a/apps/edge/internal/openai/stream.go b/apps/edge/internal/openai/stream.go index 46b5b03..d57465a 100644 --- a/apps/edge/internal/openai/stream.go +++ b/apps/edge/internal/openai/stream.go @@ -12,7 +12,7 @@ import ( edgeservice "iop/apps/edge/internal/service" ) -func (s *Server) streamChatCompletion(w http.ResponseWriter, r *http.Request, req chatCompletionRequest, handle *edgeservice.RunHandle, outputPolicy strictOutputPolicy) { +func (s *Server) streamChatCompletion(w http.ResponseWriter, r *http.Request, req chatCompletionRequest, handle edgeservice.RunResult, outputPolicy strictOutputPolicy) { flusher, ok := w.(http.Flusher) if !ok { writeError(w, http.StatusInternalServerError, "streaming_not_supported", "response writer does not support streaming") @@ -23,8 +23,8 @@ func (s *Server) streamChatCompletion(w http.ResponseWriter, r *http.Request, re w.Header().Set("Connection", "keep-alive") created := time.Now().Unix() - id := "chatcmpl-" + handle.RunID - model := responseModel(req.Model, handle.Target) + id := "chatcmpl-" + handle.Dispatch().RunID + model := responseModel(req.Model, handle.Dispatch().Target) writeSSE(w, flusher, chatCompletionChunk{ ID: id, Object: "chat.completion.chunk", @@ -45,7 +45,7 @@ func (s *Server) streamChatCompletion(w http.ResponseWriter, r *http.Request, re var reasoningBuilder strings.Builder defer func() { s.logger.Info("openai chat completion stream closed", - zap.String("run_id", handle.RunID), + zap.String("run_id", handle.Dispatch().RunID), zap.Bool("strict_output", outputPolicy.Strict), zap.Bool("strict_stream_buffer", outputPolicy.StreamBuffer), zap.Int("content_len", contentBuilder.Len()), @@ -55,16 +55,17 @@ func (s *Server) streamChatCompletion(w http.ResponseWriter, r *http.Request, re ) }() + stream := handle.Stream() for { select { case <-r.Context().Done(): return - case nodeEvent := <-handle.NodeEvents: + case nodeEvent := <-stream.NodeEvents: if edgeservice.IsNodeDisconnected(nodeEvent) { writeSSEError(w, flusher, "node disconnected") return } - case event := <-handle.Events: + case event := <-stream.Events: if event == nil { continue } @@ -135,15 +136,15 @@ func (s *Server) streamChatCompletion(w http.ResponseWriter, r *http.Request, re } } -func (s *Server) streamBufferedChatCompletion(w http.ResponseWriter, r *http.Request, handle *edgeservice.RunHandle, id string, created int64, model string, flusher http.Flusher, outputPolicy strictOutputPolicy) { - text, reasoning, _, err := collectRunResult(r.Context(), handle) +func (s *Server) streamBufferedChatCompletion(w http.ResponseWriter, r *http.Request, handle edgeservice.RunResult, id string, created int64, model string, flusher http.Flusher, outputPolicy strictOutputPolicy) { + text, reasoning, _, err := collectRunResult(r.Context(), handle.Stream(), handle.WaitTimeout()) if err != nil { writeSSEError(w, flusher, err.Error()) return } text, reasoning, normalized := normalizeCompletionOutput(outputPolicy, text, reasoning) s.logger.Info("openai chat completion stream closed", - zap.String("run_id", handle.RunID), + zap.String("run_id", handle.Dispatch().RunID), zap.Bool("strict_output", outputPolicy.Strict), zap.Bool("strict_stream_buffer", outputPolicy.StreamBuffer), zap.String("xml_completion_tool", outputPolicy.XMLCompletionTool), diff --git a/apps/edge/internal/opsconsole/console.go b/apps/edge/internal/opsconsole/console.go index 2f0e3c5..7997eed 100644 --- a/apps/edge/internal/opsconsole/console.go +++ b/apps/edge/internal/opsconsole/console.go @@ -245,8 +245,9 @@ func SendRun(ctx context.Context, edgeSvc *edgeservice.Service, events *EventRou } defer handle.Close() + dispatch := handle.Dispatch() fmt.Fprintf(out, "[edge] sent run_id=%s node=%s adapter=%s target=%s session=%s background=%v\n", - handle.RunID, handle.NodeLabel, handle.Adapter, handle.Target, handle.SessionID, handle.Background) + dispatch.RunID, dispatch.NodeLabel, dispatch.Adapter, dispatch.Target, dispatch.SessionID, dispatch.Background) if target.Background { fmt.Fprintf(out, "[edge] background run dispatched, events will arrive asynchronously\n") @@ -257,6 +258,7 @@ func SendRun(ctx context.Context, edgeSvc *edgeservice.Service, events *EventRou defer timer.Stop() var response *ResponseStream + stream := handle.Stream() for { select { @@ -264,12 +266,12 @@ func SendRun(ctx context.Context, edgeSvc *edgeservice.Service, events *EventRou return ctx.Err() case <-timer.C: return fmt.Errorf("timed out waiting for node response") - case event := <-handle.NodeEvents: + case event := <-stream.NodeEvents: if edgeservice.IsNodeDisconnected(event) { return fmt.Errorf("node disconnected: %s", event.GetReason()) } - case event := <-handle.Events: - if event.GetRunId() != handle.RunID { + case event := <-stream.Events: + if event.GetRunId() != dispatch.RunID { continue } label := events.NodeLabel(event) @@ -279,23 +281,23 @@ func SendRun(ctx context.Context, edgeSvc *edgeservice.Service, events *EventRou switch event.GetType() { case "start": - fmt.Fprintf(out, "%s start run_id=%s\n", nodeEventPrefix(label), handle.RunID) + fmt.Fprintf(out, "%s start run_id=%s\n", nodeEventPrefix(label), dispatch.RunID) case "delta": response.Write(event.GetDelta()) case "complete": response.Finish() - fmt.Fprintf(out, "%s complete run_id=%s detail=%q\n", nodeEventPrefix(label), handle.RunID, event.GetMessage()) + fmt.Fprintf(out, "%s complete run_id=%s detail=%q\n", nodeEventPrefix(label), dispatch.RunID, event.GetMessage()) return nil case "cancelled": response.FinishIfStarted() - fmt.Fprintf(out, "%s cancelled run_id=%s\n", nodeEventPrefix(label), handle.RunID) + fmt.Fprintf(out, "%s cancelled run_id=%s\n", nodeEventPrefix(label), dispatch.RunID) return nil case "error": response.FinishIfStarted() - fmt.Fprintf(out, "%s error run_id=%s detail=%q\n", nodeEventPrefix(label), handle.RunID, event.GetError()) + fmt.Fprintf(out, "%s error run_id=%s detail=%q\n", nodeEventPrefix(label), dispatch.RunID, event.GetError()) return fmt.Errorf("node reported error") default: - fmt.Fprintf(out, "%s %s run_id=%s detail=%q\n", nodeEventPrefix(label), event.GetType(), handle.RunID, event.GetMessage()) + fmt.Fprintf(out, "%s %s run_id=%s detail=%q\n", nodeEventPrefix(label), event.GetType(), dispatch.RunID, event.GetMessage()) } } } diff --git a/apps/edge/internal/service/run_dispatch.go b/apps/edge/internal/service/run_dispatch.go index b547863..295628e 100644 --- a/apps/edge/internal/service/run_dispatch.go +++ b/apps/edge/internal/service/run_dispatch.go @@ -44,6 +44,15 @@ type RunStream struct { NodeEvents <-chan *iop.EdgeNodeEvent } +// RunResult describes the interface for a submitted execution run, allowing +// surface-neutral consumption of its dispatch info and event stream. +type RunResult interface { + Dispatch() RunDispatch + Stream() RunStream + Close() + WaitTimeout() time.Duration +} + // RunHandle is the legacy combined surface kept for existing callers; it // embeds the surface-neutral DTOs so HTTP/API code can consume RunDispatch // or RunStream directly without depending on this struct. @@ -66,7 +75,21 @@ func (h *RunHandle) WaitTimeout() time.Duration { return time.Duration(normalizeTimeoutSec(h.TimeoutSec)+5) * time.Second } -func (s *Service) SubmitRun(_ context.Context, req SubmitRunRequest) (*RunHandle, error) { +func (h *RunHandle) Dispatch() RunDispatch { + if h == nil { + return RunDispatch{} + } + return h.RunDispatch +} + +func (h *RunHandle) Stream() RunStream { + if h == nil { + return RunStream{} + } + return h.RunStream +} + +func (s *Service) SubmitRun(_ context.Context, req SubmitRunRequest) (RunResult, error) { entry, err := s.ResolveNode(req.NodeRef) if err != nil { return nil, err