refactor(bridge): 실행 경계 선행 안정화를 반영한다
원격 터미널 브리지 POC 전에 Client HTTP lifecycle과 Edge run result surface 계약을 고정해야 하므로 관련 구현, 테스트, 로드맵 상태를 함께 정리한다.
This commit is contained in:
parent
b5c6ad8107
commit
672b4cdb5d
24 changed files with 1252 additions and 94 deletions
|
|
@ -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으로 승격했다.
|
||||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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` 통과.
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
<!-- task=m-bridge-boundary-hardening/01_edge_run_surface plan=0 tag=API -->
|
||||
|
||||
# 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로 이동한다.
|
||||
|
|
@ -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
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
<!-- task=m-bridge-boundary-hardening/01_edge_run_surface plan=0 tag=API -->
|
||||
|
||||
# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
<!-- task=m-bridge-boundary-hardening/02_terminal_core_boundary plan=0 tag=REFACTOR -->
|
||||
|
||||
# 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.
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
<!-- task=m-bridge-boundary-hardening/02_terminal_core_boundary plan=0 tag=REFACTOR -->
|
||||
|
||||
# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
<!-- task=m-bridge-boundary-hardening/03_adapter_config_typed plan=0 tag=API -->
|
||||
|
||||
# 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.
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
<!-- task=m-bridge-boundary-hardening/03_adapter_config_typed plan=0 tag=API -->
|
||||
|
||||
# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -31,16 +31,20 @@ class _ClientHomePageState extends State<ClientHomePage> {
|
|||
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<ClientHomePage> {
|
|||
_notificationSubscription?.cancel();
|
||||
_client?.close();
|
||||
_controller.dispose();
|
||||
_ownedStatusRepository?.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -85,10 +90,7 @@ class _ClientHomePageState extends State<ClientHomePage> {
|
|||
}
|
||||
|
||||
Future<void> _connectAndFetch() async {
|
||||
await Future.wait([
|
||||
_connectWire(),
|
||||
_controller.fetchEdges(),
|
||||
]);
|
||||
await Future.wait([_connectWire(), _controller.fetchEdges()]);
|
||||
}
|
||||
|
||||
Future<void> _connectWire() async {
|
||||
|
|
@ -166,12 +168,19 @@ class _ClientHomePageState extends State<ClientHomePage> {
|
|||
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<ClientHomePage> {
|
|||
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),
|
||||
|
|
|
|||
|
|
@ -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<List<EdgeRegistryView>> fetchEdges() async {
|
||||
|
|
@ -32,14 +41,20 @@ class HttpControlPlaneStatusRepository implements ControlPlaneStatusRepository {
|
|||
}
|
||||
final body = json.decode(response.body) as Map<String, dynamic>;
|
||||
final list = body['edges'] as List? ?? [];
|
||||
return list.map((e) => EdgeRegistryView.fromJson(e as Map<String, dynamic>)).toList();
|
||||
return list
|
||||
.map((e) => EdgeRegistryView.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<EdgeStatusResponseView> 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<String, dynamic>;
|
||||
return EdgeStatusResponseView.fromJson(body);
|
||||
|
|
@ -47,13 +62,19 @@ class HttpControlPlaneStatusRepository implements ControlPlaneStatusRepository {
|
|||
|
||||
@override
|
||||
Future<List<EdgeNodeEventView>> 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<String, dynamic>;
|
||||
final list = body['events'] as List? ?? [];
|
||||
return list.map((e) => EdgeNodeEventView.fromJson(e as Map<String, dynamic>)).toList();
|
||||
return list
|
||||
.map((e) => EdgeNodeEventView.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -68,9 +89,13 @@ class HttpControlPlaneStatusRepository implements ControlPlaneStatusRepository {
|
|||
|
||||
@override
|
||||
Future<EdgeOperationsResponseView> 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<String, dynamic>;
|
||||
return EdgeOperationsResponseView.fromJson(body);
|
||||
|
|
@ -83,9 +108,7 @@ class HttpControlPlaneStatusRepository implements ControlPlaneStatusRepository {
|
|||
String? targetSelector,
|
||||
Map<String, String>? parameters,
|
||||
}) async {
|
||||
final Map<String, dynamic> requestBody = {
|
||||
'operation': operation,
|
||||
};
|
||||
final Map<String, dynamic> 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<String, dynamic>;
|
||||
return EdgeCommandResponseView.fromJson(body);
|
||||
|
|
|
|||
|
|
@ -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<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
requestCount += 1;
|
||||
final response = http.Response(
|
||||
json.encode({'edges': <Map<String, dynamic>>[]}),
|
||||
200,
|
||||
);
|
||||
return http.StreamedResponse(
|
||||
Stream<List<int>>.fromIterable([response.bodyBytes]),
|
||||
response.statusCode,
|
||||
headers: response.headers,
|
||||
request: request,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
closeCount += 1;
|
||||
super.close();
|
||||
}
|
||||
}
|
||||
|
||||
class ControllableStatusRepository implements ControlPlaneStatusRepository {
|
||||
final fleetCompleter = Completer<FleetStatusResponseView>();
|
||||
final statusCompleter = Completer<EdgeStatusResponseView>();
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue