diff --git a/agent-roadmap/phase/inference-provider-extension/PHASE.md b/agent-roadmap/phase/inference-provider-extension/PHASE.md index d3f38cc3..10263748 100644 --- a/agent-roadmap/phase/inference-provider-extension/PHASE.md +++ b/agent-roadmap/phase/inference-provider-extension/PHASE.md @@ -7,13 +7,17 @@ ## 목표 Ollama 경로가 안정화된 뒤, 그 결과를 기준선으로 삼아 SGLang 같은 추가 추론 서버 provider를 붙인다. -이 Phase는 여러 provider의 adapter/config/target/model 매핑을 성급히 일반화하지 않고, Ollama에서 검증된 serving 기준을 기준선으로 삼아 추가 provider를 하나씩 검증한다. +이 Phase는 하나의 Node transport 위에서 여러 CLI profile, terminal gateway, 추론 엔진, model target을 함께 쓰는 구조를 기준선으로 삼고, provider별 adapter/config/target/model 매핑을 단계적으로 검증한다. ## Milestone 흐름 완료된 Milestone은 archive 경로를 가리키고, 검토중, 진행중, 계획 또는 보류 Milestone은 이 Phase 하위 `milestones/` 경로를 가리킨다. 완료, 검토중, 진행중, 계획 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다. +- [계획] Node 단일 통로 멀티 타겟 서빙 기반 + - 경로: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` + - 요약: 하나의 Node 연결이 여러 CLI profile, terminal gateway, Ollama/vLLM/SGLang 같은 추론 엔진 연결, 여러 model target을 동시에 제공할 수 있도록 config/proto/routing/runtime 기준선을 정리한다. + - [계획] SGLang provider 서빙 경로 추가 - 경로: `agent-roadmap/phase/inference-provider-extension/milestones/sglang-provider-serving-validation.md` - 요약: Ollama 경로가 안정화된 뒤 추가 추론 서버 provider로 SGLang을 붙이고, 같은 Edge OpenAI-compatible 입력 표면에서 모델 조회와 non-streaming/streaming chat을 검증한다. @@ -22,5 +26,5 @@ Ollama 경로가 안정화된 뒤, 그 결과를 기준선으로 삼아 SGLang - 이 Phase는 Ollama 경로 안정화가 선행된 뒤 시작한다. - 진행중인 Ollama 실테스트와 후속 안정화 작업을 이 Phase로 흡수하지 않는다. -- 여러 추론 서버 provider 표준화는 Ollama 경로가 안정화된 결과를 기준선으로 삼아 진행한다. +- 여러 추론 서버 provider 표준화는 Ollama 경로가 안정화된 결과와 Node 단일 통로 멀티 타겟 기준선을 함께 기준으로 삼아 진행한다. - cloud fallback, 자동 부하 라우팅, 품질 평가 feedback과 후속 최적화 계층은 이 Phase에서 다루지 않는다. diff --git a/agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md b/agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md new file mode 100644 index 00000000..e6366884 --- /dev/null +++ b/agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md @@ -0,0 +1,89 @@ +# Milestone: Node 단일 통로 멀티 타겟 서빙 기반 + +## 위치 + +- Roadmap: `agent-roadmap/ROADMAP.md` +- Phase: `agent-roadmap/phase/inference-provider-extension/PHASE.md` + +## 목표 + +하나의 Node proto-socket 연결 위에서 여러 CLI profile, terminal gateway, 추론 엔진 연결, model target을 함께 사용할 수 있는 기준선을 만든다. +Edge는 Node별 adapter/profile/engine/model 라우팅과 Edge-owned runtime option을 소유하고, Node는 그 단일 통로에서 여러 실행 target을 안정적으로 처리한다. + +## 상태 + +[계획] + +## 승격 조건 + +- 없음 + +## 구현 잠금 + +- 상태: 해제 +- 결정 필요: 없음 + +## 범위 + +- Node 하나가 단일 TCP/proto-socket 연결로 여러 adapter와 target을 동시에 제공하는 구조를 공식 기준선으로 정리한다. +- CLI profile map, terminal gateway, Ollama/vLLM/SGLang 같은 추론 엔진 연결을 같은 `adapter + target` 실행 모델에서 표현한다. +- Edge config와 proto payload가 adapter type별 단일 endpoint에 갇히지 않고 여러 engine/profile instance와 model route를 표현할 수 있게 한다. +- OpenAI-compatible `/v1/models`, `/v1/chat/completions`, `/v1/responses`가 Edge-owned model catalog와 route table을 통해 내부 `adapter + target`으로 변환되게 한다. +- 모델별 context window, runner sizing, keep-alive 같은 load/unload 영향 옵션은 agent request가 아니라 Edge 설정이 소유한다. +- Node runtime concurrency와 adapter capability가 멀티 타겟 동시 실행에서 실제로 적용되는지 검증한다. + +## 기능 + +### Epic: [multi-target-node] Single Node Multi Target Serving + +- [ ] [config-topology] Edge `nodes[]` 설정과 `NodeConfigPayload`가 하나의 Node 안에 여러 CLI profile, terminal gateway, 추론 engine/profile instance, model target route를 표현한다. +- [ ] [adapter-registry] Node adapter registry가 동일 adapter type의 여러 engine/profile instance를 덮어쓰지 않고 라우팅 가능한 target namespace로 관리한다. +- [ ] [route-catalog] Edge OpenAI-compatible model catalog가 외부 `model`을 Edge-owned route table로 해석하고, 고정 `openai.target` 단일 override 없이 내부 `adapter + target`으로 변환한다. 검증: `/v1/models`가 여러 route를 노출하고 각 model 요청이 기대 adapter/target으로 dispatch되는 단위 테스트를 통과한다. +- [ ] [engine-profiles] Ollama는 여러 `base_url`/model target과 target별 `context_size`를 지원하고, vLLM/SGLang 같은 OpenAI-compatible inference engine 연결을 같은 Node 통로에서 확장할 수 있는 provider/profile 기준이 정리되어 있다. +- [ ] [edge-owned-options] context window와 runner sizing 옵션은 Edge config가 최종 소유하며, 외부 agent request의 `options.num_ctx` 같은 값이 Edge-owned 값을 변경하지 못한다. 검증: 요청 option override가 route/profile 설정으로 덮이는 테스트를 통과한다. +- [ ] [commands-caps] `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 여러 adapter/profile/model target에서 모호하지 않은 결과를 반환한다. +- [ ] [runtime-concurrency] `runtime.concurrency`와 adapter capability가 Node 멀티 호출에서 실제 제한 또는 스케줄링 정책으로 적용된다. 검증: 동시 run 테스트에서 제한 초과 요청이 queue/reject/timeout 중 정의된 정책대로 동작한다. +- [ ] [field-smoke] split-host smoke에서 하나의 연결된 Node가 최소 두 개의 inference model target과 하나의 CLI target을 같은 Edge-Node 연결로 호출할 수 있음을 확인한다. + +## 작업 분할 + +- 상태: 첫 Epic의 각 기능 Task는 config/proto, Edge routing, Node registry, provider profile, runtime concurrency, field smoke에 걸치는 구조 변경이라 작은 즉시 코드 변경으로 처리하지 않는다. +- Active task group: `agent-task/m-node-multi-target-serving-foundation/` +- 분할: + - `01_config_topology`: `[config-topology]` + - `02+01_adapter_registry`: `[adapter-registry]` + - `03+01_route_catalog`: `[route-catalog]` + - `04+01,02_engine_profiles`: `[engine-profiles]` + - `05+03,04_edge_owned_options`: `[edge-owned-options]` + - `06+02,04_commands_caps`: `[commands-caps]` + - `07+02_runtime_concurrency`: `[runtime-concurrency]` + - `08+03,04,05,06,07_field_smoke`: `[field-smoke]` +- 적용 원칙: 작은 보정이 각 subtask 구현 중 발견되면 해당 plan 범위 안에서 바로 처리하고, 구조 변경 또는 검증 실패는 plan/code-review loop로 이어간다. + +## 완료 리뷰 + +- 상태: 없음 +- 요청일: 없음 +- 완료 근거: 모든 기능 Task와 Task 안에 명시된 검증이 아직 충족되지 않았다. +- 리뷰 필요: + - [ ] 사용자가 완료 결과를 확인했다 + - [ ] archive 이동을 승인했다 +- 리뷰 코멘트: 없음 + +## 범위 제외 + +- Control Plane이 Node에 직접 연결하거나 Node를 직접 스케줄링하는 구조 +- OpenAI-compatible/A2A payload에 terminal 제어 기능을 섞는 구조 +- cloud fallback, 품질 평가 feedback, 자동 비용/품질 최적화 +- RAG, context 압축, web search, MCP/tool policy 같은 후속 최적화 계층 +- 모든 provider의 완전한 production hardening을 한 번에 끝내는 작업 + +## 작업 컨텍스트 + +- 관련 경로: `packages/go/config`, `proto/iop`, `apps/edge/internal/node`, `apps/edge/internal/service`, `apps/edge/internal/openai`, `apps/node/internal/adapters`, `apps/node/internal/router`, `apps/node/internal/node`, `configs`, `docs` +- 표준선(선택): Node transport는 node id당 하나의 TCP/proto-socket 연결을 유지하고, 그 연결 위에서 `adapter + target + session_id` 기준으로 여러 실행을 multiplex한다. +- 표준선(선택): Edge는 Node registry, adapter/profile configuration, model catalog, routing, runtime option의 원본 소유자다. +- 표준선(선택): CLI adapter의 profile map과 `(target, session_id)` session key는 멀티 타겟 구조의 기존 기준선으로 유지한다. +- 선행 작업: Ollama 서빙 안정화 기반, proto-socket 멀티 호출 안정화 +- 후속 작업: SGLang provider 서빙 경로 추가, 지식과 도구 최적화 확장 +- 확인 필요: 구현 중 발견되는 provider별 세부 옵션은 Edge-owned route/profile 설정에 남기고, agent request가 runner sizing을 직접 바꾸는 경로는 허용하지 않는다. diff --git a/agent-roadmap/phase/inference-provider-extension/milestones/sglang-provider-serving-validation.md b/agent-roadmap/phase/inference-provider-extension/milestones/sglang-provider-serving-validation.md index 9c656cc3..ef2087b9 100644 --- a/agent-roadmap/phase/inference-provider-extension/milestones/sglang-provider-serving-validation.md +++ b/agent-roadmap/phase/inference-provider-extension/milestones/sglang-provider-serving-validation.md @@ -66,6 +66,6 @@ Ollama 실테스트에서 안정화한 모델 조회, non-streaming/streaming ch - 관련 경로: `apps/edge`, `apps/node`, `packages/go/config`, `proto/iop`, `configs`, `bin/edge.sh`, `bin/node.sh`, `agent-test/local/edge-smoke.md`, `agent-test/local/node-smoke.md`, `agent-test/local/platform-common-smoke.md` - 표준선(선택): 내부 실행 계약은 `adapter + target`을 유지하고, OpenAI-compatible API는 외부 호환 입력 표면으로 둔다. - 표준선(선택): Ollama provider에서 안정화한 field smoke 기준을 SGLang provider에도 그대로 적용한다. -- 선행 작업: Ollama 실테스트와 후속 안정화 +- 선행 작업: Ollama 실테스트와 후속 안정화, Node 단일 통로 멀티 타겟 서빙 기반 - 후속 작업: SGLang provider 실테스트에서 확인된 serving 경로 안정화 보완 - 확인 필요: SGLang adapter 경계, 실테스트 endpoint/model, 인증/헤더 필요 여부, streaming 기대 동작 diff --git a/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/code_review_cloud_G08_0.log b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/code_review_cloud_G08_0.log new file mode 100644 index 00000000..155a493a --- /dev/null +++ b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/code_review_cloud_G08_0.log @@ -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. +> Finalization is review-agent-only. + +## 개요 + +date=2026-06-10 +task=m-node-multi-target-serving-foundation/01_config_topology, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `config-topology`: Edge `nodes[]` 설정과 `NodeConfigPayload`가 하나의 Node 안에 여러 CLI profile, terminal gateway, 추론 engine/profile instance, model target route를 표현한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +구현을 실제 소스와 대조하고 검증 출력이 코드와 일치하는지 확인한다. PASS이면 active 파일을 log로 아카이브하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. WARN/FAIL이면 user-review gate를 확인하고 후속 plan/review 또는 `USER_REVIEW.md`를 작성한다. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Multi Target Topology Contract | [x] | + +## 구현 체크리스트 + +- [x] Edge config와 proto payload가 multi adapter/profile/model route topology를 표현하고 legacy 단일 설정을 호환한다. +- [x] config/proto/mapper/edgecmd 테스트가 멀티 topology와 legacy compat를 검증한다. +- [x] `make proto` 필요 여부를 확인하고 생성물 및 import 깨짐을 정리한다. +- [x] 최종 검증 명령을 실행한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] active `CODE_REVIEW-cloud-G08.md`를 `code_review_cloud_G08_N.log`로 아카이브한다. +- [x] active `PLAN-local-G08.md`를 `plan_local_G08_M.log`로 아카이브한다. +- [ ] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리를 archive로 이동한다. +- [ ] PASS이고 task group이 `m-node-multi-target-serving-foundation`이면 런타임 완료 이벤트 메타데이터를 보고한다. +- [x] WARN/FAIL이면 후속 active plan/review 또는 `USER_REVIEW.md`를 작성한다. + +## 계획 대비 변경 사항 + +- **`edgecmd/node_register.go`**: plan에서 "register/config 생성 경로 보강"을 언급했으나, `node_register.go`의 `--adapter` 플래그 흐름은 단일 ollama/cli 진입 경로로 남겨뒀다. Multi-instance 등록은 edge.yaml 직접 편집으로 처리하는 것이 현 UX 방향과 맞고, CLI 플래그로 다수의 named instance를 표현하는 인터페이스 설계는 후속 task 범위로 판단해 이번 변경에서 제외했다. `validateEdgeConfig`에서 `ollama_instances`/`vllm_instances` 유효성 검증은 추가했다. +- **`normalizeAdapters`의 legacy 이름 고정**: legacy `Ollama.Enabled` 단일 필드는 `OllamaInstances`로 promote 시 name을 `"ollama"`로 고정한다. 이는 기존 단일 인스턴스 시나리오에서 instance identity를 예측 가능하게 만들기 위함이다. +- **기존 `TestBuildConfigPayload_OllamaEnabled` 수정**: mapper가 `OllamaInstances`만 읽도록 변경되었으므로, LoadEdge를 통하지 않고 NodeRecord를 직접 구성하는 기존 테스트도 `OllamaInstances`를 세팅하도록 수정했다. 이것은 "mapper는 post-normalisation 상태를 받는다"는 계약을 테스트에 명시하는 것이다. + +## 주요 설계 결정 + +- **AdapterConfig.name 필드 (proto field 8)**: `AdapterConfig`에 `name` 문자열 필드를 추가해 동일 type의 여러 instance를 구분하는 stable identity로 사용한다. 빈 값은 legacy single-instance로 간주한다. +- **AdapterConfig.target 필드 (proto field 9)**: 선택적 default target/route 힌트. 이번 task에서는 proto contract 확보 목적으로 추가만 하고 routing 로직은 후속 task에서 사용한다. +- **`OllamaInstances`/`VllmInstances` slice를 `AdaptersConf`에 추가**: 동일 type 여러 engine instance를 표현한다. legacy `Ollama`/`Vllm` 단일 필드는 `LoadEdge` 내 `normalizeAdapters`에서 슬라이스 앞에 promote되어 하위 코드가 슬라이스만 읽으면 된다. +- **중복 name 검증**: `normalizeAdapters`(config load)와 `BuildConfigPayload`(mapper) 양쪽에서 중복 name을 에러로 처리해 키 충돌을 load 시점에 잡는다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- legacy 단일 `ollama`/`vllm`/`cli` 설정이 깨지지 않았는지 확인한다. +- proto 변경 시 생성물과 call site가 함께 반영됐는지 확인한다. +- 동일 adapter type 여러 instance의 key 충돌 정책이 테스트로 고정됐는지 확인한다. + +## 검증 결과 + +### API-1 중간 검증 + +```bash +$ go test -count=1 ./packages/go/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/... +ok iop/packages/go/audit 0.002s +ok iop/packages/go/config 0.013s +ok iop/packages/go/hostsetup 0.004s +ok iop/packages/go/observability 0.015s +ok iop/apps/edge/internal/node 0.005s +ok iop/apps/edge/internal/edgecmd 0.007s +``` + +### 최종 검증 + +```bash +$ go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/... +ok iop/packages/go/audit 0.002s +ok iop/packages/go/config 0.014s +ok iop/packages/go/hostsetup 0.006s +ok iop/packages/go/observability 0.038s +? iop/proto/gen/iop [no test files] +ok iop/apps/edge/internal/node 0.004s +ok iop/apps/edge/internal/edgecmd 0.011s +``` + +```bash +$ make proto +protoc \ + --go_out=. \ + --go_opt=module=iop \ + --proto_path=. \ + proto/iop/runtime.proto \ + proto/iop/node.proto \ + proto/iop/control.proto \ + proto/iop/job.proto +``` + +proto 변경 반영됨: `AdapterConfig`에 `name`(field 8), `target`(field 9) 추가. `proto/gen/iop/runtime.pb.go` 갱신 확인. + +## 코드리뷰 결과 + +- 종합 판정: `WARN` +- 차원별 평가: + - correctness: Warn + - completeness: Pass + - test coverage: Warn + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: + - Suggested: `apps/edge/internal/edgecmd/config.go:143`의 `hasOllama`/`hasVllm` 판정이 `len(instance) > 0`만 확인해, 모든 instance가 `enabled: false`여도 node가 adapter를 가진 것으로 검증 통과할 수 있다. 활성 instance 기준으로 판정하고 이 회귀를 edgecmd 테스트로 고정한다. + - Suggested: `apps/edge/internal/edgecmd/config.go:151` 주변 검증이 `ollama_instances[].base_url`만 확인하고 `vllm_instances[].endpoint`는 확인하지 않는다. `enabled: true`인 vLLM instance의 빈 endpoint를 거부하고 테스트를 추가한다. +- 다음 단계: WARN/FAIL 후속 plan/review를 생성한다. diff --git a/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/code_review_cloud_G08_1.log b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/code_review_cloud_G08_1.log new file mode 100644 index 00000000..7e73a7a0 --- /dev/null +++ b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/code_review_cloud_G08_1.log @@ -0,0 +1,167 @@ + + +# Code Review Reference - REVIEW_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`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves. +> 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-10 +task=m-node-multi-target-serving-foundation/01_config_topology, plan=1, tag=REVIEW_API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `config-topology`: Edge `nodes[]` 설정과 `NodeConfigPayload`가 하나의 Node 안에 여러 CLI profile, terminal gateway, 추론 engine/profile instance, model target route를 표현한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G08.md` -> `code_review_cloud_G08_N.log`, `PLAN-local-G08.md` -> `plan_local_G08_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-node-multi-target-serving-foundation/01_config_topology/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. PASS이고 task group이 `m-node-multi-target-serving-foundation`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REVIEW_API-1] Edge Config Validation Follow-Up | [x] | + +## 구현 체크리스트 + +- [x] `validateEdgeConfig`가 legacy adapter와 multi-instance adapter 모두 실제 enabled 상태 기준으로 "at least one adapter"를 판단한다. +- [x] `validateEdgeConfig`가 enabled vLLM legacy/instance endpoint 빈 값을 거부하고 edgecmd 테스트가 이를 검증한다. +- [x] 최종 검증 명령을 실행한다. +- [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_cloud_G08_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G08_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-node-multi-target-serving-foundation/01_config_topology/`를 `agent-task/archive/YYYY/MM/m-node-multi-target-serving-foundation/01_config_topology/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-node-multi-target-serving-foundation`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-node-multi-target-serving-foundation/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G08.md`와 `CODE_REVIEW-cloud-G08.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로 이동한다. + +## 계획 대비 변경 사항 + +없음. plan이 지정한 두 항목(enabled 기준 at-least-one, vLLM endpoint 빈 값 거부)을 그대로 구현했다. + +## 주요 설계 결정 + +- **`anyOllamaEnabled` / `anyVllmEnabled` 헬퍼 분리**: `validateEdgeConfig` 내부에서 legacy 단일 필드와 instance slice 양쪽을 순회해 실제 enabled 인스턴스가 하나라도 있는지 판단한다. 이전 `len(OllamaInstances) > 0` 방식은 `Enabled: false` 인스턴스만 있는 경우에도 true를 반환하는 문제가 있었다. +- **vLLM 검증 일관성**: legacy `Vllm.Enabled && Endpoint == ""` 와 `VllmInstances[]` enabled + empty endpoint 를 동일 패턴으로 거부한다. Ollama의 base_url 검증과 대칭을 맞췄다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `config check`가 실제 enabled adapter가 없는 node를 거부하는지 확인한다. +- enabled legacy `vllm`과 `vllm_instances[]`의 빈 endpoint가 거부되는지 확인한다. +- 기존 legacy `ollama`/`cli` 경로가 불필요하게 깨지지 않았는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### REVIEW_API-1 중간 검증 +```bash +$ go test -count=1 ./apps/edge/internal/edgecmd/... +ok iop/apps/edge/internal/edgecmd 0.007s +``` + +### 최종 검증 +```bash +$ go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/... +ok iop/packages/go/audit 0.002s +? iop/packages/go/auth [no test files] +ok iop/packages/go/config 0.013s +? iop/packages/go/events [no test files] +ok iop/packages/go/hostsetup 0.004s +? iop/packages/go/jobs [no test files] +? iop/packages/go/metadata [no test files] +ok iop/packages/go/observability 0.012s +? iop/packages/go/policy [no test files] +? iop/packages/go/version [no test files] +? iop/proto/gen/iop [no test files] +ok iop/apps/edge/internal/node 0.005s +ok iop/apps/edge/internal/edgecmd 0.006s +``` + +--- + +> **[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. + +Sections and their ownership: + +| 섹션 | 소유자 | 설명 | +|------|--------|------| +| 헤더 주석, 개요(date/task/plan/tag), 리뷰 에이전트 지시 | 스텁 생성 시 고정 | 구현 에이전트가 수정하거나 실행하지 않음 | +| Roadmap Targets | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 수정하지 않음; PASS 시 code-review가 `complete.log`의 `Roadmap Completion`으로 복사 | +| 구현 항목별 완료 여부 (항목명) | 스텁 생성 시 고정 | `[ ]` -> `[x]` 체크만 구현 에이전트가 수행 | +| 구현 체크리스트 (항목 텍스트/순서) | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 `[ ]` -> `[x]` 체크만 수행; 마지막 체크박스는 저장 전 필수 | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section | +| 계획 대비 변경 사항, 주요 설계 결정 | 구현 에이전트가 채움 | placeholder 텍스트를 실제 내용으로 교체 | +| 사용자 리뷰 요청 | 구현 에이전트가 채움 | 진행에 사용자 입력이 필요하지 않으면 `상태: 없음` 유지; 구현 중 직접 질문은 금지; 필요하면 결정 항목, 근거, 명령 출력, 자동 후속 불가 이유, 재개 조건을 기록 | +| 리뷰어를 위한 체크포인트 | 스텁 생성 시 고정 | 계획에서 추출한 리뷰 포인트 | +| 검증 결과 (섹션 제목 + 명령) | 스텁 생성 시 고정 | 실행 출력만 구현 에이전트가 채움; 명령 변경은 `계획 대비 변경 사항`에 기록 | +| 코드리뷰 결과 | 리뷰 에이전트가 append | 스텁에 포함하지 않음 | + +## 코드리뷰 결과 + +- 종합 판정: `PASS` +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: 없음 +- 다음 단계: PASS 종결 처리로 `complete.log`를 작성하고 task directory를 archive로 이동한다. diff --git a/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/complete.log b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/complete.log new file mode 100644 index 00000000..6a46a0b4 --- /dev/null +++ b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/complete.log @@ -0,0 +1,41 @@ +# Complete - m-node-multi-target-serving-foundation/01_config_topology + +## 완료 일시 + +2026-06-10 + +## 요약 + +Edge config multi-target topology foundation review loop completed after 2 reviews; final verdict PASS. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G08_0.log` | `code_review_cloud_G08_0.log` | WARN | Multi topology contract was judgeable, but `config check` needed enabled-instance and vLLM endpoint validation follow-up. | +| `plan_local_G08_1.log` | `code_review_cloud_G08_1.log` | PASS | Follow-up validation gaps were fixed and covered by edgecmd tests. | + +## 구현/정리 내용 + +- Edge config validation now counts enabled legacy adapters and enabled multi-instance adapters when enforcing "at least one adapter". +- Enabled legacy vLLM and `vllm_instances[]` entries now reject empty endpoints. +- Edgecmd tests cover disabled instance-only rejection, vLLM empty endpoint rejection, and enabled Ollama instance acceptance. + +## 최종 검증 + +- `go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/...` - PASS; reviewer rerun succeeded across package/config/proto-gen/edge node/edgecmd targets. + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Completed task ids: + - `config-topology`: PASS; evidence=`plan_local_G08_0.log`, `code_review_cloud_G08_0.log`, `plan_local_G08_1.log`, `code_review_cloud_G08_1.log`; verification=`go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/...` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/plan_local_G08_0.log b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/plan_local_G08_0.log new file mode 100644 index 00000000..a1f7ebc2 --- /dev/null +++ b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/plan_local_G08_0.log @@ -0,0 +1,124 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +`CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션 작성까지가 구현의 일부다. 코드 변경 후 검증을 실행하고 실제 변경 내용, 검증 출력, 계획 대비 변경 사항을 채운 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청`에 근거를 남기고 중단한다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`, `USER_REVIEW.md`, archive log, `complete.log`를 만들지 않는다. + +## 배경 + +현재 Edge `nodes[]`와 `NodeConfigPayload`는 adapter type별 단일 endpoint를 표현하는 구조다. 하나의 Node 연결에서 여러 CLI profile, terminal gateway, 여러 inference engine/profile, model route를 함께 쓰려면 config와 proto payload가 먼저 멀티 타겟 topology를 표현해야 한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 blocker는 active `CODE_REVIEW-cloud-G08.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 직접 사용자 프롬프트를 만들지 않으며, code-review가 요청 타당성과 실제 `USER_REVIEW.md` 작성 여부를 판단한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `config-topology`: Edge `nodes[]` 설정과 `NodeConfigPayload`가 하나의 Node 안에 여러 CLI profile, terminal gateway, 추론 engine/profile instance, model target route를 표현한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-test/local/rules.md` +- `agent-test/local/node-smoke.md` +- `agent-test/local/edge-smoke.md` +- `agent-test/local/platform-common-smoke.md` +- `packages/go/config/config.go` +- `packages/go/config/config_test.go` +- `proto/iop/runtime.proto` +- `configs/edge.yaml` +- `apps/edge/internal/node/mapper.go` +- `apps/edge/internal/node/mapper_test.go` +- `apps/edge/internal/edgecmd/config.go` +- `apps/edge/internal/edgecmd/node_register.go` +- `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` + +### 테스트 환경 규칙 + +- test_env: `local` +- local rules/profile을 읽었다. 적용 명령은 `go test ./packages/go/... ./proto/gen/...`, `go test ./apps/edge/internal/node/...`, `go test ./apps/edge/internal/edgecmd/...`이며 proto schema 변경 시 `make proto` 후 생성물 차이를 확인한다. +- `platform-common-smoke`, `edge-smoke`, `node-smoke` 기준상 사용자 실행 경로가 바뀌면 최소 edge/node config round-trip smoke 증거를 남겨야 한다. + +### 테스트 커버리지 공백 + +- `EdgeConfig.Nodes[].Adapters`가 단일 `OllamaConf`/`VllmConf`만 갖는 구조는 `config_test.go`의 단일 context size 테스트로만 덮인다. 멀티 engine/profile decode 테스트가 없다. +- `NodeConfigPayload`는 typed oneof가 단일 adapter instance를 전송하는 테스트만 있다. 동일 type 여러 instance와 route/profile id 전달 테스트가 없다. +- `edgecmd node register`는 단일 ollama/cli 플래그 중심이라 여러 profile/engine 입력 테스트가 없다. + +### 심볼 참조 + +- 변경 전 rename/remove 예정 심볼: 없음. 새 config/proto 필드를 추가하고 legacy 필드는 compat path로 유지한다. +- 관련 참조: `AdaptersConf`, `OllamaConf`, `VllmConf`, `CLIConf`, `BuildConfigPayload`, `NodeConfigPayload`, `AdapterConfig`. + +### 분할 판단 + +- split 정책을 적용했다. 공유 API/proto/config foundation이 먼저 필요하므로 task group은 `m-node-multi-target-serving-foundation`이고 이 작업은 `01_config_topology` 독립 선행 작업이다. +- sibling 작업: `02+01_adapter_registry`, `03+01_route_catalog`, `04+01,02_engine_profiles`, `05+03,04_edge_owned_options`, `06+02,04_commands_caps`, `07+02_runtime_concurrency`, `08+03,04,05,06,07_field_smoke`. +- predecessor: 없음. + +### 범위 결정 근거 + +- Edge OpenAI route 해석, Node adapter registry, provider 실행 구현은 후속 sibling task로 분리한다. +- SGLang 완성 구현, cloud fallback, RAG/tool policy는 milestone 범위 제외에 남긴다. + +### 빌드 등급 + +- build: `local-G08`, review: `cloud-G08`. 프로토콜/schema와 config/call-site가 함께 움직이지만 범위가 foundation 파일로 제한된다. + +## 구현 체크리스트 + +- [ ] Edge config와 proto payload가 multi adapter/profile/model route topology를 표현하고 legacy 단일 설정을 호환한다. +- [ ] config/proto/mapper/edgecmd 테스트가 멀티 topology와 legacy compat를 검증한다. +- [ ] `make proto` 필요 여부를 확인하고 생성물 및 import 깨짐을 정리한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] Multi Target Topology Contract + +- 문제: `AdaptersConf`는 `Ollama`, `Vllm`, `CLI` 단일 필드만 제공해 동일 adapter type 여러 engine/profile instance를 표현하지 못한다 (`packages/go/config/config.go:168`). `NodeConfigPayload`와 `AdapterConfig`도 instance identity나 route metadata 없이 `type` 중심이다 (`proto/iop/runtime.proto:147`, `proto/iop/runtime.proto:153`). `BuildConfigPayload`는 ollama/vllm을 각각 한 번만 append한다 (`apps/edge/internal/node/mapper.go:24`, `apps/edge/internal/node/mapper.go:36`). +- 해결 방법: `NodeConf`/`AdaptersConf`에 typed profile collection을 추가하고 기존 단일 필드는 legacy alias로 유지한다. `AdapterConfig`에는 stable `name` 또는 `id`, optional target route/profile metadata를 추가한다. Mapper는 legacy 단일 필드와 신규 collection을 모두 payload로 변환하되 중복 key는 명확한 에러로 처리한다. +- 수정 파일 및 체크리스트: + - [ ] `packages/go/config/config.go`: multi profile structs와 defaults/legacy normalization 추가. + - [ ] `proto/iop/runtime.proto`: adapter instance identity와 route/profile payload 필드 추가. + - [ ] `apps/edge/internal/node/mapper.go`: 신규 topology를 `NodeConfigPayload`로 변환. + - [ ] `apps/edge/internal/edgecmd/node_register.go`: register/config 생성 경로가 신규 topology를 만들 수 있게 보강. + - [ ] `configs/edge.yaml`: 사람용 예시를 multi target 기준으로 갱신. +- 테스트 작성: 추가한다. `packages/go/config/config_test.go`에 멀티 Ollama/vLLM/CLI profile decode와 legacy 단일 decode 테스트, `apps/edge/internal/node/mapper_test.go`에 동일 adapter type 여러 instance payload 테스트, `apps/edge/internal/edgecmd` 테스트에 register 출력 topology 테스트를 추가한다. +- 중간 검증: + - `go test -count=1 ./packages/go/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/...` + - proto 변경 시 `make proto && go test -count=1 ./proto/gen/...` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `packages/go/config/config.go` | API-1 | +| `packages/go/config/config_test.go` | API-1 | +| `proto/iop/runtime.proto` | API-1 | +| `proto/gen/**` | API-1 | +| `apps/edge/internal/node/mapper.go` | API-1 | +| `apps/edge/internal/node/mapper_test.go` | API-1 | +| `apps/edge/internal/edgecmd/config.go` | API-1 | +| `apps/edge/internal/edgecmd/node_register.go` | API-1 | +| `configs/edge.yaml` | API-1 | + +## 최종 검증 + +```bash +go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/... +``` + +proto 파일을 수정했으면 아래도 실행한다. + +```bash +make proto +git diff -- proto/gen +``` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/plan_local_G08_1.log b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/plan_local_G08_1.log new file mode 100644 index 00000000..bd66c802 --- /dev/null +++ b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/01_config_topology/plan_local_G08_1.log @@ -0,0 +1,78 @@ + + +# Plan - REVIEW_API + +## 이 파일을 읽는 구현 에이전트에게 + +`CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션 작성까지가 구현의 일부다. 코드 변경 후 검증을 실행하고 실제 변경 내용, 검증 출력, 계획 대비 변경 사항을 채운 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청`에 근거를 남기고 중단한다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`, `USER_REVIEW.md`, archive log, `complete.log`를 만들지 않는다. + +## 배경 + +1차 구현은 Edge config와 `NodeConfigPayload`가 multi adapter/profile topology를 표현하도록 확장했다. 코드리뷰 결과, 핵심 contract 자체는 judgeable하지만 `iop-edge config check`의 adapter 활성 판정과 vLLM endpoint 검증에 빈틈이 있어 후속 보강이 필요하다. + +## 사용자 리뷰 요청 흐름 + +구현 중 blocker는 active `CODE_REVIEW-cloud-G08.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 직접 사용자 프롬프트를 만들지 않으며, code-review가 요청 타당성과 실제 `USER_REVIEW.md` 작성 여부를 판단한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `config-topology`: Edge `nodes[]` 설정과 `NodeConfigPayload`가 하나의 Node 안에 여러 CLI profile, terminal gateway, 추론 engine/profile instance, model target route를 표현한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 이전 루프 + +- Archived plan: `agent-task/m-node-multi-target-serving-foundation/01_config_topology/plan_local_G08_0.log` +- Archived review: `agent-task/m-node-multi-target-serving-foundation/01_config_topology/code_review_cloud_G08_0.log` +- Verdict: `WARN` + +### 리뷰 발견 사항 + +- `apps/edge/internal/edgecmd/config.go:143`의 `hasOllama`/`hasVllm` 판정이 instance slice 길이만 확인한다. 모든 instance가 `enabled: false`여도 adapter가 있다고 판단할 수 있다. +- `apps/edge/internal/edgecmd/config.go:151` 주변 검증은 `ollama_instances[].base_url`만 확인한다. `enabled: true`인 legacy `vllm.endpoint` 및 `vllm_instances[].endpoint`가 빈 값인 경우를 거부하지 않는다. + +### 범위 결정 근거 + +- 기존 multi topology contract, proto field, mapper 구조는 유지한다. +- 후속 범위는 `config check` 검증 보강과 관련 edgecmd 테스트 추가에 한정한다. +- Node runtime routing, OpenAI route catalog, adapter registry 구현은 sibling task 범위이므로 다루지 않는다. + +### 빌드 등급 + +- build: `local-G08`, review: `cloud-G08`. 수정 범위는 deterministic config validation과 테스트에 한정되지만, milestone-linked API foundation의 후속 검토라 기존 review route를 유지한다. + +## 구현 체크리스트 + +- [ ] `validateEdgeConfig`가 legacy adapter와 multi-instance adapter 모두 실제 enabled 상태 기준으로 "at least one adapter"를 판단한다. +- [ ] `validateEdgeConfig`가 enabled vLLM legacy/instance endpoint 빈 값을 거부하고 edgecmd 테스트가 이를 검증한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [REVIEW_API-1] Edge Config Validation Follow-Up + +- 문제: 비활성 instance만 존재하는 node가 `config check`에서 통과할 수 있고, enabled vLLM endpoint 빈 값이 검증되지 않는다. +- 해결 방법: `validateEdgeConfig`에서 enabled instance 존재 여부를 helper 또는 명확한 loop로 계산한다. legacy `vllm.enabled`와 `vllm_instances[].enabled`가 true이면 endpoint가 비어 있지 않아야 한다. 기존 Ollama 검증은 유지하되 multi-instance 판정도 enabled 기준으로 정리한다. +- 수정 파일 및 체크리스트: + - [ ] `apps/edge/internal/edgecmd/config.go`: enabled adapter 판정과 vLLM endpoint 검증 보강. + - [ ] `apps/edge/internal/edgecmd/edgecmd_test.go`: 비활성 instance-only node 거부, enabled vLLM endpoint 빈 값 거부 테스트 추가. +- 테스트 작성: 추가한다. `edgecmd` 테스트에서 `config check` command를 실행해 실제 사용자-facing validation 결과를 고정한다. +- 중간 검증: + - `go test -count=1 ./apps/edge/internal/edgecmd/...` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/edge/internal/edgecmd/config.go` | REVIEW_API-1 | +| `apps/edge/internal/edgecmd/edgecmd_test.go` | REVIEW_API-1 | + +## 최종 검증 + +```bash +go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/node/... ./apps/edge/internal/edgecmd/... +``` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-node-multi-target-serving-foundation/02+01_adapter_registry/CODE_REVIEW-cloud-G07.md b/agent-task/m-node-multi-target-serving-foundation/02+01_adapter_registry/CODE_REVIEW-cloud-G07.md new file mode 100644 index 00000000..e0b094e7 --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/02+01_adapter_registry/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,96 @@ + + +# 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. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation. +> Finalization is review-agent-only. + +## 개요 + +date=2026-06-10 +task=m-node-multi-target-serving-foundation/02+01_adapter_registry, plan=0, tag=REFACTOR + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `adapter-registry`: Node adapter registry가 동일 adapter type의 여러 engine/profile instance를 덮어쓰지 않고 라우팅 가능한 target namespace로 관리한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +구현을 plan과 대조하고, registry/router namespace가 legacy 단일 요청과 multi instance 요청을 모두 안전하게 처리하는지 검토한다. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REFACTOR-1] Registry Namespace | [ ] | + +## 구현 체크리스트 + +- [ ] Registry가 adapter type과 instance key를 보존하고 동일 adapter type 여러 instance를 덮어쓰지 않는다. +- [ ] Factory가 `NodeConfigPayload`의 multi adapter instance를 registry namespace로 등록한다. +- [ ] Router가 request의 adapter/target을 새 namespace로 해석하고 legacy 단일 adapter 요청을 호환한다. +- [ ] registry/factory/router/node 단위 테스트를 추가 또는 갱신한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] active `CODE_REVIEW-cloud-G07.md`를 `code_review_cloud_G07_N.log`로 아카이브한다. +- [ ] active `PLAN-local-G08.md`를 `plan_local_G08_M.log`로 아카이브한다. +- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다. +- [ ] WARN/FAIL이면 후속 active plan/review 또는 `USER_REVIEW.md`를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- 동일 adapter type 여러 instance가 등록되어도 overwrite가 없는지 확인한다. +- ambiguous legacy adapter 요청이 조용히 임의 target으로 가지 않는지 확인한다. +- lifecycle `Start`/`Stop` 순서와 `All()` 진단 출력이 새 key 기준으로 안정적인지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +### REFACTOR-1 중간 검증 + +```bash +$ go test -count=1 ./apps/node/internal/adapters/... ./apps/node/internal/router/... +(output) +``` + +### 최종 검증 + +```bash +$ go test -count=1 ./apps/node/... +(output) +``` diff --git a/agent-task/m-node-multi-target-serving-foundation/02+01_adapter_registry/PLAN-local-G08.md b/agent-task/m-node-multi-target-serving-foundation/02+01_adapter_registry/PLAN-local-G08.md new file mode 100644 index 00000000..6a004a1c --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/02+01_adapter_registry/PLAN-local-G08.md @@ -0,0 +1,113 @@ + + +# Plan - REFACTOR + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료의 마지막 단계는 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션 작성이다. 검증을 실행하고 실제 출력과 설계 결정을 기록한 뒤 active 파일을 유지하고 리뷰 준비를 보고한다. 사용자 전용 결정이나 외부 환경 prerequisite 없이는 진행할 수 없을 때만 `사용자 리뷰 요청`을 채우고 멈춘다. + +## 배경 + +Node registry는 adapter name을 map key로 쓰기 때문에 같은 adapter type의 여러 engine/profile instance를 등록하면 덮어쓴다. 멀티 타겟 Node에서는 adapter type과 route target을 분리해 라우팅 가능한 namespace가 필요하다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 에이전트는 직접 질문하지 않으며, code-review가 user-review stop 여부를 판단한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `adapter-registry`: Node adapter registry가 동일 adapter type의 여러 engine/profile instance를 덮어쓰지 않고 라우팅 가능한 target namespace로 관리한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-test/local/rules.md` +- `agent-test/local/node-smoke.md` +- `apps/node/internal/adapters/registry.go` +- `apps/node/internal/adapters/factory.go` +- `apps/node/internal/adapters/adapters_blackbox_test.go` +- `apps/node/internal/router/router.go` +- `apps/node/internal/router/router_test.go` +- `apps/node/internal/runtime/types.go` +- `apps/node/internal/node/node.go` +- `apps/node/internal/node/node_test.go` +- `proto/iop/runtime.proto` + +### 테스트 환경 규칙 + +- test_env: `local` +- `node-smoke` 기준 명령은 `go test ./apps/node/...`와 필요 시 edge-node command/run round-trip smoke다. 이 plan은 registry/router 단위 변경이므로 `go test -count=1 ./apps/node/...`를 최종 계약으로 둔다. + +### 테스트 커버리지 공백 + +- registry는 `map[string]runtime.Adapter`로 name 중복을 덮어쓰는 현재 동작을 명시적으로 방어하는 테스트가 부족하다. +- factory는 payload 내 동일 type 여러 adapter instance 등록을 허용하지 않는 구조다. +- router는 `adapter` 단일 string으로 adapter instance와 adapter type을 구분하지 않는다. + +### 심볼 참조 + +- rename/remove 예정 심볼: 없음. 단, `Registry.Get`, `Registry.Register`, `Router.ResolveAdapter` call site는 signature 변경 가능성이 있으므로 전체 grep 후 갱신한다. + +### 분할 판단 + +- predecessor `01`: `01_config_topology`가 active plan으로 존재하며 아직 `complete.log`가 없다. 구현은 `agent-task/m-node-multi-target-serving-foundation/01_config_topology/complete.log`가 생긴 뒤 시작한다. +- 이 작업은 registry/router 내부 변경만 다루고 provider 실행/route catalog는 sibling task로 분리한다. + +### 범위 결정 근거 + +- OpenAI route table과 engine profile별 endpoint 실행은 후속 task에서 처리한다. +- concurrency scheduling은 `07+02_runtime_concurrency`에 둔다. + +### 빌드 등급 + +- build: `local-G08`, review: `cloud-G07`. 내부 refactor지만 Node routing contract라 review는 더 넓은 맥락을 본다. + +## 의존 관계 및 구현 순서 + +- `02+01_adapter_registry`는 `01_config_topology` 완료 후 구현한다. 디렉터리 이름의 `+01` 외 추가 dependency를 만들지 않는다. + +## 구현 체크리스트 + +- [ ] Registry가 adapter type과 instance key를 보존하고 동일 adapter type 여러 instance를 덮어쓰지 않는다. +- [ ] Factory가 `NodeConfigPayload`의 multi adapter instance를 registry namespace로 등록한다. +- [ ] Router가 request의 adapter/target을 새 namespace로 해석하고 legacy 단일 adapter 요청을 호환한다. +- [ ] registry/factory/router/node 단위 테스트를 추가 또는 갱신한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [REFACTOR-1] Registry Namespace + +- 문제: `Registry.Register`는 `a.Name()`만 key로 쓰고 기존 key를 덮어쓴다 (`apps/node/internal/adapters/registry.go:26`). `BuildFromPayload`도 `"ollama"` 하나만 등록한다 (`apps/node/internal/adapters/factory.go:29`). 이 상태에서는 `ollama@local`, `ollama@dgx`, `vllm@fast` 같은 여러 instance가 공존할 수 없다. +- 해결 방법: adapter registry entry에 stable instance id/type/targets를 둔다. `Get`/`All`/lifecycle 순서는 새 key 기준으로 유지하고, legacy `"ollama"` 단일 요청은 단일 후보일 때만 호환한다. ambiguous legacy 요청은 명확한 error를 반환한다. +- 수정 파일 및 체크리스트: + - [ ] `apps/node/internal/adapters/registry.go`: key model과 duplicate policy 구현. + - [ ] `apps/node/internal/adapters/factory.go`: payload instance id를 registry key로 등록. + - [ ] `apps/node/internal/router/router.go`: request resolution 업데이트. + - [ ] `apps/node/internal/runtime/types.go`: 필요한 routing metadata 최소 추가. +- 테스트 작성: 추가한다. `adapters_blackbox_test.go` 또는 신규 registry test에 duplicate type multi instance 등록, lifecycle ordering, ambiguous legacy lookup 테스트를 둔다. `router_test.go`에 adapter instance/target 해석 테스트를 둔다. +- 중간 검증: + - `go test -count=1 ./apps/node/internal/adapters/... ./apps/node/internal/router/...` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/node/internal/adapters/registry.go` | REFACTOR-1 | +| `apps/node/internal/adapters/factory.go` | REFACTOR-1 | +| `apps/node/internal/adapters/adapters_blackbox_test.go` | REFACTOR-1 | +| `apps/node/internal/router/router.go` | REFACTOR-1 | +| `apps/node/internal/router/router_test.go` | REFACTOR-1 | +| `apps/node/internal/runtime/types.go` | REFACTOR-1 | +| `apps/node/internal/node/node_test.go` | REFACTOR-1 | + +## 최종 검증 + +```bash +go test -count=1 ./apps/node/... +``` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-node-multi-target-serving-foundation/03+01_route_catalog/CODE_REVIEW-cloud-G08.md b/agent-task/m-node-multi-target-serving-foundation/03+01_route_catalog/CODE_REVIEW-cloud-G08.md new file mode 100644 index 00000000..f3520333 --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/03+01_route_catalog/CODE_REVIEW-cloud-G08.md @@ -0,0 +1,91 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> Complete every implementation-owned section, paste real verification output, then stop with active files in place. +> If blocked by a user-only decision or user-owned external prerequisite, fill `사용자 리뷰 요청`; do not ask the user directly. +> Finalization is review-agent-only. + +## 개요 + +date=2026-06-10 +task=m-node-multi-target-serving-foundation/03+01_route_catalog, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `route-catalog`: Edge OpenAI-compatible model catalog가 외부 `model`을 Edge-owned route table로 해석하고, 고정 `openai.target` 단일 override 없이 내부 `adapter + target`으로 변환한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +route catalog가 legacy fallback과 multi model dispatch를 모두 보존하는지 실제 테스트와 대조한다. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] OpenAI Route Catalog | [ ] | + +## 구현 체크리스트 + +- [ ] Edge config에 external model catalog와 route table을 추가하고 legacy `openai.target` 동작을 호환한다. +- [ ] `/v1/models`가 route table의 여러 model id를 노출한다. +- [ ] `/v1/chat/completions`와 `/v1/responses`가 외부 model을 내부 adapter/target/node/profile로 변환한다. +- [ ] route miss, ambiguous route, legacy fallback 테스트를 추가한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +- [ ] `코드리뷰 결과`에 판정을 append한다. +- [ ] active `CODE_REVIEW-cloud-G08.md`를 `code_review_cloud_G08_N.log`로 아카이브한다. +- [ ] active `PLAN-local-G08.md`를 `plan_local_G08_M.log`로 아카이브한다. +- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다. +- [ ] WARN/FAIL이면 후속 상태를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `/v1/models`가 route catalog source of truth를 사용한다. +- chat/responses가 같은 resolver를 공유하거나 동일한 우선순위로 동작한다. +- legacy `openai.target` 사용자가 갑자기 깨지지 않는다. + +## 검증 결과 + +### API-1 중간 검증 + +```bash +$ go test -count=1 ./packages/go/... ./apps/edge/internal/openai/... ./apps/edge/internal/service/... +(output) +``` + +### 최종 검증 + +```bash +$ go test -count=1 ./packages/go/... ./apps/edge/... +(output) +``` diff --git a/agent-task/m-node-multi-target-serving-foundation/03+01_route_catalog/PLAN-local-G08.md b/agent-task/m-node-multi-target-serving-foundation/03+01_route_catalog/PLAN-local-G08.md new file mode 100644 index 00000000..2a5d9e43 --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/03+01_route_catalog/PLAN-local-G08.md @@ -0,0 +1,120 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료 전 `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채운다. 사용자 전용 blocker는 review stub의 `사용자 리뷰 요청`에 기록하고, 직접 질문이나 `USER_REVIEW.md` 작성은 하지 않는다. + +## 배경 + +현재 OpenAI-compatible surface는 `openai.target` 단일 override 또는 요청 model 값을 그대로 내부 target으로 사용한다. Edge가 model catalog와 route table을 소유해야 외부 `model`을 내부 `adapter + target + profile`로 안정적으로 변환할 수 있다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub에 기록한다. code-review가 blocker 타당성과 후속 상태를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `route-catalog`: Edge OpenAI-compatible model catalog가 외부 `model`을 Edge-owned route table로 해석하고, 고정 `openai.target` 단일 override 없이 내부 `adapter + target`으로 변환한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-test/local/rules.md` +- `agent-test/local/edge-smoke.md` +- `packages/go/config/config.go` +- `packages/go/config/config_test.go` +- `apps/edge/internal/openai/routes.go` +- `apps/edge/internal/openai/types.go` +- `apps/edge/internal/openai/chat_handler.go` +- `apps/edge/internal/openai/responses_handler.go` +- `apps/edge/internal/openai/server_test.go` +- `apps/edge/internal/service/run_dispatch.go` +- `apps/edge/internal/service/service_test.go` +- `configs/edge.yaml` + +### 테스트 환경 규칙 + +- test_env: `local` +- edge smoke 기준: config/API 변경은 `go test ./apps/edge/...`, OpenAI surface 변경은 `/healthz`, `/v1/models`, `/v1/chat/completions` 또는 `/v1/responses` smoke 증거가 필요하다. 이 plan은 단위 테스트를 계약으로 두고 live smoke는 field-smoke task로 넘긴다. + +### 테스트 커버리지 공백 + +- `/v1/models`는 `cfg.Models` 또는 단일 `cfg.Target`만 노출한다 (`apps/edge/internal/openai/routes.go:28`). +- chat/responses는 `s.cfg.Target`이 있으면 요청 model과 metadata override를 무시한다 (`apps/edge/internal/openai/chat_handler.go:132`). +- 여러 외부 model이 서로 다른 adapter/target으로 dispatch되는 테스트가 없다. + +### 심볼 참조 + +- rename/remove 예정 심볼: 없음. `EdgeOpenAIConf.Target`, `Models`, `Adapter`, `NodeRef`는 legacy compat로 유지한다. +- route 해석 관련 참조: `handleModels`, `resolveAdapter`, `resolveTargetWithOverride`, `SubmitRunRequest`. + +### 분할 판단 + +- predecessor `01`: `01_config_topology` active plan이 있으며 아직 완료 전이다. 구현은 `01_config_topology/complete.log` 이후 시작한다. +- route catalog는 Edge API surface 변경이므로 Node registry/provider 작업과 분리했다. + +### 범위 결정 근거 + +- 실제 provider별 endpoint/context 실행은 `04+01,02_engine_profiles`와 `05+03,04_edge_owned_options`에서 처리한다. +- live split-host 검증은 `08+03,04,05,06,07_field_smoke`에서 수행한다. + +### 빌드 등급 + +- build: `local-G08`, review: `cloud-G08`. OpenAI-compatible API contract와 Edge routing이 함께 바뀌어 높은 검토가 필요하다. + +## 의존 관계 및 구현 순서 + +- `03+01_route_catalog`는 `01_config_topology` 완료 후 구현한다. 디렉터리 이름에 없는 `02` 의존성은 추가하지 않는다. + +## 구현 체크리스트 + +- [ ] Edge config에 external model catalog와 route table을 추가하고 legacy `openai.target` 동작을 호환한다. +- [ ] `/v1/models`가 route table의 여러 model id를 노출한다. +- [ ] `/v1/chat/completions`와 `/v1/responses`가 외부 model을 내부 adapter/target/node/profile로 변환한다. +- [ ] route miss, ambiguous route, legacy fallback 테스트를 추가한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] OpenAI Route Catalog + +- 문제: `handleModels`는 route table 없이 문자열 목록만 반환한다 (`apps/edge/internal/openai/routes.go:23`). `resolveTargetWithOverride`는 `cfg.Target`을 전역 override로 우선해 모델별 라우팅을 막는다 (`apps/edge/internal/openai/chat_handler.go:132`). `/v1/responses`도 같은 해석을 사용한다 (`apps/edge/internal/openai/responses_handler.go:51`). +- 해결 방법: `EdgeOpenAIConf`에 `routes` 또는 `model_routes`를 추가해 external model id, node ref, adapter instance, target, session/timeout/profile option reference를 표현한다. handler는 request model을 route catalog로 해석하고, 없을 때만 legacy target/model fallback을 사용한다. +- 수정 파일 및 체크리스트: + - [ ] `packages/go/config/config.go`: route catalog config type 추가. + - [ ] `apps/edge/internal/openai/types.go`: request/response model handling 보존. + - [ ] `apps/edge/internal/openai/routes.go`: `/v1/models` catalog 기반 출력. + - [ ] `apps/edge/internal/openai/chat_handler.go`: route resolver 적용. + - [ ] `apps/edge/internal/openai/responses_handler.go`: route resolver 적용. + - [ ] `apps/edge/internal/service/run_dispatch.go`: 필요한 route metadata 전달. +- 테스트 작성: 추가한다. `server_test.go`에 `/v1/models` multi route, chat dispatch model A/B, responses metadata override와 route 우선순위, missing model 400/404 테스트를 둔다. `config_test.go`에 route catalog decode를 추가한다. +- 중간 검증: + - `go test -count=1 ./packages/go/... ./apps/edge/internal/openai/... ./apps/edge/internal/service/...` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `packages/go/config/config.go` | API-1 | +| `packages/go/config/config_test.go` | API-1 | +| `apps/edge/internal/openai/routes.go` | API-1 | +| `apps/edge/internal/openai/types.go` | API-1 | +| `apps/edge/internal/openai/chat_handler.go` | API-1 | +| `apps/edge/internal/openai/responses_handler.go` | API-1 | +| `apps/edge/internal/openai/server_test.go` | API-1 | +| `apps/edge/internal/service/run_dispatch.go` | API-1 | +| `apps/edge/internal/service/service_test.go` | API-1 | +| `configs/edge.yaml` | API-1 | + +## 최종 검증 + +```bash +go test -count=1 ./packages/go/... ./apps/edge/... +``` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-node-multi-target-serving-foundation/04+01,02_engine_profiles/CODE_REVIEW-cloud-G08.md b/agent-task/m-node-multi-target-serving-foundation/04+01,02_engine_profiles/CODE_REVIEW-cloud-G08.md new file mode 100644 index 00000000..8ac35d12 --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/04+01,02_engine_profiles/CODE_REVIEW-cloud-G08.md @@ -0,0 +1,91 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> Fill implementation-owned sections and verification output, then stop with active files in place. +> Do not ask the user directly; record user-only blockers in `사용자 리뷰 요청`. +> Finalization is review-agent-only. + +## 개요 + +date=2026-06-10 +task=m-node-multi-target-serving-foundation/04+01,02_engine_profiles, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `engine-profiles`: Ollama는 여러 `base_url`/model target과 target별 `context_size`를 지원하고, vLLM/SGLang 같은 OpenAI-compatible inference engine 연결을 같은 Node 통로에서 확장할 수 있는 provider/profile 기준이 정리되어 있다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +provider profile 선택이 target별 endpoint/context를 정확히 고정하고, vLLM/SGLang 확장 기준이 과도하게 넓어지지 않았는지 확인한다. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Provider Profile Execution | [ ] | + +## 구현 체크리스트 + +- [ ] Ollama multi profile이 endpoint/model/context를 target별로 선택한다. +- [ ] vLLM 또는 공통 OpenAI-compatible provider/profile baseline이 registry namespace 위에서 확장 가능하게 정리된다. +- [ ] provider capabilities가 profile/target 식별자를 모호하지 않게 반환한다. +- [ ] Ollama/vLLM/factory 테스트가 multi endpoint/profile 선택을 검증한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +- [ ] `코드리뷰 결과`에 판정을 append한다. +- [ ] active `CODE_REVIEW-cloud-G08.md`를 `code_review_cloud_G08_N.log`로 아카이브한다. +- [ ] active `PLAN-cloud-G07.md`를 `plan_cloud_G07_M.log`로 아카이브한다. +- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다. +- [ ] WARN/FAIL이면 후속 상태를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- Ollama request가 target별 fake server로 정확히 나뉘는지 확인한다. +- target별 `context_size`가 request option으로 반영되는지 확인한다. +- vLLM/SGLang 기준선이 disabled dead-end가 아니라 후속 provider 추가 경로를 만든다. + +## 검증 결과 + +### API-1 중간 검증 + +```bash +$ go test -count=1 ./apps/node/internal/adapters/ollama/... ./apps/node/internal/adapters/vllm/... ./apps/node/internal/adapters/... +(output) +``` + +### 최종 검증 + +```bash +$ go test -count=1 ./apps/node/... +(output) +``` diff --git a/agent-task/m-node-multi-target-serving-foundation/04+01,02_engine_profiles/PLAN-cloud-G07.md b/agent-task/m-node-multi-target-serving-foundation/04+01,02_engine_profiles/PLAN-cloud-G07.md new file mode 100644 index 00000000..bb4cd179 --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/04+01,02_engine_profiles/PLAN-cloud-G07.md @@ -0,0 +1,113 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +구현 마지막 단계는 `CODE_REVIEW-cloud-G08.md`에 실제 변경 내용과 검증 출력 기록이다. 사용자 전용 결정 없이 막히면 직접 질문하지 말고 `사용자 리뷰 요청`에 증거를 남긴다. + +## 배경 + +Ollama adapter는 하나의 `baseURL`과 `contextSize`만 가진다. 같은 Node 연결에서 여러 Ollama endpoint/model, vLLM/SGLang류 OpenAI-compatible engine profile을 함께 쓰려면 provider/profile 기준을 Node 쪽 실행 단위에 반영해야 한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 blocker는 active review stub에 기록한다. code-review가 user-review stop 여부를 판단한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `engine-profiles`: Ollama는 여러 `base_url`/model target과 target별 `context_size`를 지원하고, vLLM/SGLang 같은 OpenAI-compatible inference engine 연결을 같은 Node 통로에서 확장할 수 있는 provider/profile 기준이 정리되어 있다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-test/local/rules.md` +- `agent-test/local/node-smoke.md` +- `packages/go/config/config.go` +- `proto/iop/runtime.proto` +- `apps/node/internal/adapters/ollama/ollama.go` +- `apps/node/internal/adapters/ollama/ollama_test.go` +- `apps/node/internal/adapters/vllm/vllm.go` +- `apps/node/internal/adapters/vllm/vllm_test.go` +- `apps/node/internal/adapters/factory.go` +- `apps/node/internal/runtime/types.go` + +### 테스트 환경 규칙 + +- test_env: `local` +- Node profile 기준 검증은 `go test ./apps/node/...`다. OpenAI-compatible provider 실행은 unit/fake HTTP server로 deterministic하게 검증한다. + +### 테스트 커버리지 공백 + +- Ollama는 adapter instance 하나가 `baseURL` 하나만 가진다 (`apps/node/internal/adapters/ollama/ollama.go:23`). +- `Capabilities`가 endpoint별 target/source를 구분하지 않는다 (`apps/node/internal/adapters/ollama/ollama.go:45`). +- vLLM은 `Execute`가 비활성 에러를 반환한다 (`apps/node/internal/adapters/vllm/vllm.go:47`). +- multiple profile selection과 target별 `context_size` 테스트가 없다. + +### 심볼 참조 + +- rename/remove 예정 심볼: 없음. `ollama.New`, `vllm.New`, `runtime.Capabilities`, `ExecutionSpec.Target/Input` 확장 가능성이 있다. + +### 분할 판단 + +- predecessor `01`: config/proto topology 완료 필요. 현재 active plan, complete 없음. +- predecessor `02`: registry namespace 완료 필요. 현재 active plan, complete 없음. +- provider 실행은 route catalog와 독립적이지만 registry namespace가 있어야 같은 type 여러 instance를 안전하게 검증할 수 있다. + +### 범위 결정 근거 + +- SGLang 전용 production hardening은 후속 SGLang milestone로 넘긴다. +- Edge route catalog와 OpenAI request option ownership은 별도 sibling task에서 다룬다. + +### 빌드 등급 + +- build: `cloud-G07`, review: `cloud-G08`. provider 실행, fake HTTP server, config/proto 결과가 함께 얽혀 있어 cloud lane으로 둔다. + +## 의존 관계 및 구현 순서 + +- `04+01,02_engine_profiles`는 `01_config_topology`와 `02+01_adapter_registry` 완료 후 구현한다. 디렉터리 이름의 `+01,02` 외 추가 dependency를 만들지 않는다. + +## 구현 체크리스트 + +- [ ] Ollama multi profile이 endpoint/model/context를 target별로 선택한다. +- [ ] vLLM 또는 공통 OpenAI-compatible provider/profile baseline이 registry namespace 위에서 확장 가능하게 정리된다. +- [ ] provider capabilities가 profile/target 식별자를 모호하지 않게 반환한다. +- [ ] Ollama/vLLM/factory 테스트가 multi endpoint/profile 선택을 검증한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] Provider Profile Execution + +- 문제: `Ollama`는 `baseURL`과 `contextSize`를 adapter instance 필드 하나로 보관한다 (`apps/node/internal/adapters/ollama/ollama.go:23`). `Execute`는 `spec.Target`을 model로만 해석한다 (`apps/node/internal/adapters/ollama/ollama.go:54`). vLLM은 target 조회만 일부 있고 실행은 disabled다 (`apps/node/internal/adapters/vllm/vllm.go:47`). +- 해결 방법: registry instance 또는 provider profile이 endpoint/context/model mapping을 가진다. `ExecutionSpec`가 route/profile metadata를 받거나 target namespace를 parsing 없이 안정적으로 해석한다. vLLM/SGLang류는 공통 OpenAI-compatible provider interface를 두되 완성 범위는 baseline과 fake-server 테스트로 제한한다. +- 수정 파일 및 체크리스트: + - [ ] `apps/node/internal/adapters/ollama/ollama.go`: profile-aware endpoint/model/context selection. + - [ ] `apps/node/internal/adapters/vllm/vllm.go`: OpenAI-compatible provider baseline 또는 명확한 extension point. + - [ ] `apps/node/internal/adapters/factory.go`: profile config를 provider instance로 생성. + - [ ] `apps/node/internal/runtime/types.go`: 필요한 profile route metadata 최소 추가. +- 테스트 작성: 추가한다. `ollama_test.go`에 두 fake server가 서로 다른 model/context를 받는 테스트를 두고, `vllm_test.go`에는 `/v1/models` 및 실행 baseline contract를 고정한다. +- 중간 검증: + - `go test -count=1 ./apps/node/internal/adapters/ollama/... ./apps/node/internal/adapters/vllm/... ./apps/node/internal/adapters/...` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/node/internal/adapters/ollama/ollama.go` | API-1 | +| `apps/node/internal/adapters/ollama/ollama_test.go` | API-1 | +| `apps/node/internal/adapters/vllm/vllm.go` | API-1 | +| `apps/node/internal/adapters/vllm/vllm_test.go` | API-1 | +| `apps/node/internal/adapters/factory.go` | API-1 | +| `apps/node/internal/runtime/types.go` | API-1 | + +## 최종 검증 + +```bash +go test -count=1 ./apps/node/... +``` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/CODE_REVIEW-local-G07.md b/agent-task/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/CODE_REVIEW-local-G07.md new file mode 100644 index 00000000..3c503fd3 --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/CODE_REVIEW-local-G07.md @@ -0,0 +1,89 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> Fill implementation-owned sections and real verification output before reporting ready for review. +> Do not ask the user directly; record user-only blockers in `사용자 리뷰 요청`. + +## 개요 + +date=2026-06-10 +task=m-node-multi-target-serving-foundation/05+03,04_edge_owned_options, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `edge-owned-options`: context window와 runner sizing 옵션은 Edge config가 최종 소유하며, 외부 agent request의 `options.num_ctx` 같은 값이 Edge-owned 값을 변경하지 못한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +request option이 route/profile owned option을 덮지 못하는지 테스트와 구현을 같이 확인한다. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Edge-Owned Runtime Options | [ ] | + +## 구현 체크리스트 + +- [ ] Edge route/profile config의 context/runner sizing 옵션이 request option보다 우선한다. +- [ ] OpenAI chat/responses에서 request `options.num_ctx` 등 runner sizing override가 내부 profile 값을 바꾸지 못한다. +- [ ] provider별 option merge 정책을 테스트로 고정한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +- [ ] `코드리뷰 결과`에 판정을 append한다. +- [ ] active `CODE_REVIEW-local-G07.md`를 `code_review_local_G07_N.log`로 아카이브한다. +- [ ] active `PLAN-local-G07.md`를 `plan_local_G07_M.log`로 아카이브한다. +- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다. +- [ ] WARN/FAIL이면 후속 상태를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- request `options.num_ctx`가 profile context size보다 우선하지 않는다. +- chat/responses 양쪽 경로가 같은 정책을 따른다. +- allow-pass-through option과 forced-owned option이 코드에서 구분된다. + +## 검증 결과 + +### API-1 중간 검증 + +```bash +$ go test -count=1 ./apps/edge/internal/openai/... ./apps/node/internal/adapters/ollama/... +(output) +``` + +### 최종 검증 + +```bash +$ go test -count=1 ./apps/edge/... ./apps/node/... +(output) +``` diff --git a/agent-task/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/PLAN-local-G07.md b/agent-task/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/PLAN-local-G07.md new file mode 100644 index 00000000..1e7208a4 --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/PLAN-local-G07.md @@ -0,0 +1,110 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료 전 `CODE_REVIEW-local-G07.md`를 실제 구현 내용과 검증 출력으로 채운다. 사용자 전용 blocker는 review stub에 기록하고 직접 질문하지 않는다. + +## 배경 + +사용자는 context window/runner sizing이 agent request마다 달라져 모델 로딩/언로딩이 흔들리는 것을 원하지 않는다. 현재 Ollama 단일 adapter에는 `num_ctx` override 방어가 들어갔지만, route/profile 기반 멀티 타겟 구조에서도 Edge-owned option이 최종 소유권을 가져야 한다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub의 `사용자 리뷰 요청`에 기록한다. code-review가 실제 user-review stop 여부를 판단한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `edge-owned-options`: context window와 runner sizing 옵션은 Edge config가 최종 소유하며, 외부 agent request의 `options.num_ctx` 같은 값이 Edge-owned 값을 변경하지 못한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-test/local/rules.md` +- `agent-test/local/edge-smoke.md` +- `agent-test/local/node-smoke.md` +- `apps/edge/internal/openai/chat_handler.go` +- `apps/edge/internal/openai/responses_handler.go` +- `apps/edge/internal/openai/server_test.go` +- `apps/node/internal/adapters/ollama/ollama.go` +- `apps/node/internal/adapters/ollama/ollama_test.go` +- `packages/go/config/config.go` + +### 테스트 환경 규칙 + +- test_env: `local` +- unit contract는 `go test ./apps/edge/... ./apps/node/...`다. live agent request override smoke는 field-smoke task에서 처리한다. + +### 테스트 커버리지 공백 + +- 현재 `ollamaOptionsFromInput`은 단일 `contextSize`를 `num_ctx`로 강제한다 (`apps/node/internal/adapters/ollama/ollama.go:298`). +- route/profile별 context ownership과 OpenAI handler에서 request option이 어떻게 전달되는지에 대한 end-to-end 단위 테스트가 부족하다. +- `keep_alive`나 runner sizing 성격의 추가 옵션 소유권 기준이 명문화되어 있지 않다. + +### 심볼 참조 + +- rename/remove 예정 심볼: 없음. `ollamaOptionsFromInput`, OpenAI request option pass-through, route/profile config 필드 참조를 grep한다. + +### 분할 판단 + +- predecessor `03`: route catalog 완료 필요. 현재 active plan, complete 없음. +- predecessor `04`: engine profile 완료 필요. 현재 active plan, complete 없음. +- 이 작업은 옵션 소유권 정책만 다루며 provider profile 구조 자체는 수정하지 않는다. + +### 범위 결정 근거 + +- 새로운 provider 추가나 route catalog schema 변경은 선행 task 범위다. +- 실제 원격 모델 로딩/언로딩 관찰은 field-smoke로 넘긴다. + +### 빌드 등급 + +- build: `local-G07`, review: `local-G07`. 정책은 중요하지만 단위 테스트로 검출 가능하고 범위가 명확하다. + +## 의존 관계 및 구현 순서 + +- `05+03,04_edge_owned_options`는 `03+01_route_catalog`와 `04+01,02_engine_profiles` 완료 후 구현한다. + +## 구현 체크리스트 + +- [ ] Edge route/profile config의 context/runner sizing 옵션이 request option보다 우선한다. +- [ ] OpenAI chat/responses에서 request `options.num_ctx` 등 runner sizing override가 내부 profile 값을 바꾸지 못한다. +- [ ] provider별 option merge 정책을 테스트로 고정한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] Edge-Owned Runtime Options + +- 문제: 단일 Ollama adapter에는 `num_ctx` 방어가 있지만 (`apps/node/internal/adapters/ollama/ollama.go:305`), route/profile 기반에서는 외부 request option과 Edge-owned profile option의 merge 우선순위가 다시 불명확해질 수 있다. +- 해결 방법: Edge config route/profile의 runner sizing option을 canonical source로 정의하고, handler 또는 provider adapter에서 request options를 allow/pass-through와 forced-owned option으로 분리한다. `num_ctx`처럼 runner sizing에 영향이 큰 옵션은 Edge-owned 값이 있을 때 항상 덮어쓴다. +- 수정 파일 및 체크리스트: + - [ ] `packages/go/config/config.go`: route/profile option ownership 필드 확인 또는 보강. + - [ ] `apps/edge/internal/openai/chat_handler.go`: request option 전달 시 route/profile metadata 유지. + - [ ] `apps/edge/internal/openai/responses_handler.go`: responses path도 같은 정책 적용. + - [ ] `apps/node/internal/adapters/ollama/ollama.go`: profile별 forced option 적용. +- 테스트 작성: 추가한다. `server_test.go`와 `ollama_test.go`에 request `options.num_ctx=8192`가 route/profile `context_size=262144`로 덮이는 테스트를 둔다. provider별 runner sizing option 목록은 최소 `num_ctx`로 시작하고 확장 가능하게 한다. +- 중간 검증: + - `go test -count=1 ./apps/edge/internal/openai/... ./apps/node/internal/adapters/ollama/...` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `packages/go/config/config.go` | API-1 | +| `apps/edge/internal/openai/chat_handler.go` | API-1 | +| `apps/edge/internal/openai/responses_handler.go` | API-1 | +| `apps/edge/internal/openai/server_test.go` | API-1 | +| `apps/node/internal/adapters/ollama/ollama.go` | API-1 | +| `apps/node/internal/adapters/ollama/ollama_test.go` | API-1 | + +## 최종 검증 + +```bash +go test -count=1 ./apps/edge/... ./apps/node/... +``` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-node-multi-target-serving-foundation/06+02,04_commands_caps/CODE_REVIEW-local-G07.md b/agent-task/m-node-multi-target-serving-foundation/06+02,04_commands_caps/CODE_REVIEW-local-G07.md new file mode 100644 index 00000000..1794c714 --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/06+02,04_commands_caps/CODE_REVIEW-local-G07.md @@ -0,0 +1,89 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> Fill implementation-owned sections and verification output, then report ready for review. +> Do not ask the user directly; record user-only blockers in `사용자 리뷰 요청`. + +## 개요 + +date=2026-06-10 +task=m-node-multi-target-serving-foundation/06+02,04_commands_caps, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `commands-caps`: `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 여러 adapter/profile/model target에서 모호하지 않은 결과를 반환한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +command/capability 결과가 adapter instance와 target namespace를 잃지 않는지 확인한다. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Command Identity Contract | [ ] | + +## 구현 체크리스트 + +- [ ] capabilities 응답이 adapter instance/profile/model target identity를 모호하지 않게 포함한다. +- [ ] session/usage/provider command가 target namespace를 정확히 보존한다. +- [ ] Edge service command DTO와 Node command handling 테스트를 갱신한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +- [ ] `코드리뷰 결과`에 판정을 append한다. +- [ ] active `CODE_REVIEW-local-G07.md`를 `code_review_local_G07_N.log`로 아카이브한다. +- [ ] active `PLAN-local-G07.md`를 `plan_local_G07_M.log`로 아카이브한다. +- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다. +- [ ] WARN/FAIL이면 후속 상태를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- capabilities/session/usage가 동일 target 이름 충돌에서 모호하지 않다. +- Edge command DTO와 proto/runtime DTO가 같은 identity를 전달한다. +- long command 처리와 concurrency scheduling 범위가 이 plan을 넘어 확장되지 않았다. + +## 검증 결과 + +### API-1 중간 검증 + +```bash +$ go test -count=1 ./apps/node/... ./apps/edge/internal/service/... +(output) +``` + +### 최종 검증 + +```bash +$ go test -count=1 ./apps/node/... ./apps/edge/internal/service/... +(output) +``` diff --git a/agent-task/m-node-multi-target-serving-foundation/06+02,04_commands_caps/PLAN-local-G07.md b/agent-task/m-node-multi-target-serving-foundation/06+02,04_commands_caps/PLAN-local-G07.md new file mode 100644 index 00000000..cdea7582 --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/06+02,04_commands_caps/PLAN-local-G07.md @@ -0,0 +1,116 @@ + + +# Plan - API + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료 전 `CODE_REVIEW-local-G07.md`를 실제 변경 사항과 검증 출력으로 채운다. 사용자 전용 blocker는 review stub에 기록한다. + +## 배경 + +멀티 adapter/profile/model target 환경에서는 `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 어떤 target에 대한 응답인지 명확해야 한다. 현재 command path는 adapter/target 문자열만 전달해 multi profile 결과가 모호해질 수 있다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub에 기록한다. code-review가 후속 상태를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `commands-caps`: `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 여러 adapter/profile/model target에서 모호하지 않은 결과를 반환한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-test/local/rules.md` +- `agent-test/local/node-smoke.md` +- `agent-test/local/edge-smoke.md` +- `apps/edge/internal/service/node_command.go` +- `apps/edge/internal/service/status_provider.go` +- `apps/edge/internal/service/service_test.go` +- `apps/node/internal/node/node.go` +- `apps/node/internal/node/node_test.go` +- `apps/node/internal/adapters/cli/cli.go` +- `apps/node/internal/runtime/types.go` +- `apps/node/internal/transport/session.go` +- `proto/iop/runtime.proto` + +### 테스트 환경 규칙 + +- test_env: `local` +- command path는 node/edge 양쪽 단위 테스트와 최소 proto-socket command round-trip smoke가 필요하다. 이 plan은 `go test -count=1 ./apps/node/... ./apps/edge/internal/service/...`를 계약으로 둔다. + +### 테스트 커버리지 공백 + +- CLI capabilities는 profile target list만 반환한다 (`apps/node/internal/adapters/cli/cli.go:182`). +- session list는 label 문자열 중심이라 adapter instance/profile identity가 확장될 때 모호해질 수 있다 (`apps/node/internal/adapters/cli/cli.go:356`). +- `NodeCommandRequest` listener는 typed request를 동기 처리하므로 long command가 command path를 막을 수 있다 (`apps/node/internal/transport/session.go:69`). + +### 심볼 참조 + +- rename/remove 예정 심볼: 없음. `NodeCommandRequest`, `runtime.CommandRequest`, `Capabilities`, CLI `HandleCommand` 참조를 확인한다. + +### 분할 판단 + +- predecessor `02`: adapter registry namespace 완료 필요. 현재 active plan, complete 없음. +- predecessor `04`: provider profile baseline 완료 필요. 현재 active plan, complete 없음. +- runtime concurrency scheduling은 `07`로 분리하고, 이 plan은 command result identity와 passthrough ambiguity만 다룬다. + +### 범위 결정 근거 + +- OpenAI request routing과 provider execution은 선행 task에 있다. +- terminal gateway protocol 자체 변경은 필요할 때 별도 task로 분리한다. + +### 빌드 등급 + +- build: `local-G07`, review: `local-G07`. command contract는 명확하고 단위 테스트로 방어 가능하다. + +## 의존 관계 및 구현 순서 + +- `06+02,04_commands_caps`는 `02+01_adapter_registry`와 `04+01,02_engine_profiles` 완료 후 구현한다. + +## 구현 체크리스트 + +- [ ] capabilities 응답이 adapter instance/profile/model target identity를 모호하지 않게 포함한다. +- [ ] session/usage/provider command가 target namespace를 정확히 보존한다. +- [ ] Edge service command DTO와 Node command handling 테스트를 갱신한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] Command Identity Contract + +- 문제: command request/response는 adapter/target/session 문자열을 전달하지만 multi profile에서 이 값이 adapter instance인지 model target인지 모호할 수 있다. CLI session list는 label 문자열만 기본 출력으로 제공한다 (`apps/node/internal/adapters/cli/cli.go:366`). +- 해결 방법: command request/response와 capabilities result에 route/profile namespace를 명확히 포함한다. Edge service DTO는 old fields를 유지하되 new identity fields를 채운다. CLI/session/provider command는 target namespace를 잃지 않고 반환한다. +- 수정 파일 및 체크리스트: + - [ ] `proto/iop/runtime.proto`: command/capability identity 필드 보강 여부 확인. + - [ ] `apps/node/internal/runtime/types.go`: command response/capability DTO 갱신. + - [ ] `apps/node/internal/node/node.go`: command dispatch/result mapping 갱신. + - [ ] `apps/node/internal/adapters/cli/cli.go`: session/usage result identity 보존. + - [ ] `apps/edge/internal/service/node_command.go`: Edge command DTO 매핑 갱신. +- 테스트 작성: 추가한다. `node_test.go`에 multi adapter/profile command routing, `service_test.go`에 command request/response DTO, CLI 테스트에 session list identity 검증을 둔다. +- 중간 검증: + - `go test -count=1 ./apps/node/... ./apps/edge/internal/service/...` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `proto/iop/runtime.proto` | API-1 | +| `apps/node/internal/runtime/types.go` | API-1 | +| `apps/node/internal/node/node.go` | API-1 | +| `apps/node/internal/node/node_test.go` | API-1 | +| `apps/node/internal/adapters/cli/cli.go` | API-1 | +| `apps/edge/internal/service/node_command.go` | API-1 | +| `apps/edge/internal/service/status_provider.go` | API-1 | +| `apps/edge/internal/service/service_test.go` | API-1 | + +## 최종 검증 + +```bash +go test -count=1 ./apps/node/... ./apps/edge/internal/service/... +``` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-node-multi-target-serving-foundation/07+02_runtime_concurrency/CODE_REVIEW-cloud-G08.md b/agent-task/m-node-multi-target-serving-foundation/07+02_runtime_concurrency/CODE_REVIEW-cloud-G08.md new file mode 100644 index 00000000..04b0451e --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/07+02_runtime_concurrency/CODE_REVIEW-cloud-G08.md @@ -0,0 +1,90 @@ + + +# Code Review Reference - REFACTOR + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> Fill implementation-owned sections and actual verification output, then stop with active files in place. +> Do not ask the user directly; record user-only blockers in `사용자 리뷰 요청`. + +## 개요 + +date=2026-06-10 +task=m-node-multi-target-serving-foundation/07+02_runtime_concurrency, plan=0, tag=REFACTOR + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `runtime-concurrency`: `runtime.concurrency`와 adapter capability가 Node 멀티 호출에서 실제 제한 또는 스케줄링 정책으로 적용된다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +concurrency policy가 deterministic하고 foreground/background 양쪽에서 같은지 검토한다. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REFACTOR-1] Runtime Concurrency Gate | [ ] | + +## 구현 체크리스트 + +- [ ] Node runtime global concurrency와 adapter/profile concurrency 중 적용 우선순위를 정의한다. +- [ ] Foreground/background run 모두 동일한 scheduling/limit 정책을 통과한다. +- [ ] 제한 초과 요청의 queue/reject/timeout 중 하나를 선택하고 error/event/store 상태를 테스트로 고정한다. +- [ ] race/order test를 추가하고 최종 검증을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +- [ ] `코드리뷰 결과`에 판정을 append한다. +- [ ] active `CODE_REVIEW-cloud-G08.md`를 `code_review_cloud_G08_N.log`로 아카이브한다. +- [ ] active `PLAN-cloud-G07.md`를 `plan_cloud_G07_M.log`로 아카이브한다. +- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다. +- [ ] WARN/FAIL이면 후속 상태를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- permit acquire/release가 adapter error, timeout, cancellation, background run에서 누수되지 않는다. +- 선택한 제한 초과 정책이 테스트와 문서에 일치한다. +- proto-socket transport 변경으로 문제를 우회하지 않았다. + +## 검증 결과 + +### REFACTOR-1 중간 검증 + +```bash +$ go test -race -count=1 ./apps/node/internal/node/... +(output) +``` + +### 최종 검증 + +```bash +$ go test -race -count=1 ./apps/node/internal/node/... +$ go test -count=1 ./apps/node/... +(output) +``` diff --git a/agent-task/m-node-multi-target-serving-foundation/07+02_runtime_concurrency/PLAN-cloud-G07.md b/agent-task/m-node-multi-target-serving-foundation/07+02_runtime_concurrency/PLAN-cloud-G07.md new file mode 100644 index 00000000..93a3af88 --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/07+02_runtime_concurrency/PLAN-cloud-G07.md @@ -0,0 +1,109 @@ + + +# Plan - REFACTOR + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료 전 `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채운다. 사용자 전용 blocker는 review stub에 기록하고 직접 질문하지 않는다. + +## 배경 + +proto-socket은 nonce/pending request로 멀티 호출을 실을 수 있지만, Node runtime이 실제 동시 실행 제한과 queue/reject/timeout 정책을 적용해야 안정적이다. 현재 `runtime.concurrency`와 adapter capability의 `MaxConcurrency`는 실행 path에서 실질적인 gate로 보이지 않는다. + +## 사용자 리뷰 요청 흐름 + +구현 중 차단은 active review stub에 기록한다. code-review가 후속 상태를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `runtime-concurrency`: `runtime.concurrency`와 adapter capability가 Node 멀티 호출에서 실제 제한 또는 스케줄링 정책으로 적용된다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-test/local/rules.md` +- `agent-test/local/node-smoke.md` +- `apps/node/internal/node/node.go` +- `apps/node/internal/node/run_manager.go` +- `apps/node/internal/node/node_test.go` +- `apps/node/internal/runtime/types.go` +- `apps/node/internal/transport/session.go` +- `apps/node/internal/adapters/registry.go` +- `proto/iop/runtime.proto` + +### 테스트 환경 규칙 + +- test_env: `local` +- concurrency logic은 `go test -race` 또는 deterministic ordering test가 권장된다. 최종 계약은 `go test -race -count=1 ./apps/node/internal/node/...`와 `go test -count=1 ./apps/node/...`다. + +### 테스트 커버리지 공백 + +- `Node.OnRunRequest`는 adapter 실행 전 concurrency permit을 획득하지 않는다 (`apps/node/internal/node/node.go:122`). +- `run_manager`는 active run tracking/cancel 중심이고 scheduling/limit 정책이 없다. +- background run은 goroutine으로 바로 실행되어 global 또는 adapter별 limit을 우회할 수 있다 (`apps/node/internal/node/node.go:132`). + +### 심볼 참조 + +- rename/remove 예정 심볼: 없음. `NodeRuntimeConfig.Concurrency`, `runtime.Capabilities.MaxConcurrency`, `RunRequest.Background`, `runManager` 참조를 확인한다. + +### 분할 판단 + +- predecessor `02`: adapter registry namespace 완료 필요. 현재 active plan, complete 없음. +- route catalog/provider profile과 독립적으로 Node runtime gate를 구현할 수 있어 `03/04/05/06`에는 의존하지 않는다. + +### 범위 결정 근거 + +- proto-socket 자체의 멀티 요청 구현을 다시 작성하지 않는다. transport는 요청을 실어 나르는 계층으로 두고 Node runtime scheduling만 담당한다. +- policy는 하나만 선택한다. queue/reject/timeout 중 여러 정책을 동시에 노출하지 않는다. + +### 빌드 등급 + +- build: `cloud-G07`, review: `cloud-G08`. concurrency는 hard-to-review failure mode가 있어 cloud lane으로 둔다. + +## 의존 관계 및 구현 순서 + +- `07+02_runtime_concurrency`는 `02+01_adapter_registry` 완료 후 구현한다. + +## 구현 체크리스트 + +- [ ] Node runtime global concurrency와 adapter/profile concurrency 중 적용 우선순위를 정의한다. +- [ ] Foreground/background run 모두 동일한 scheduling/limit 정책을 통과한다. +- [ ] 제한 초과 요청의 queue/reject/timeout 중 하나를 선택하고 error/event/store 상태를 테스트로 고정한다. +- [ ] race/order test를 추가하고 최종 검증을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [REFACTOR-1] Runtime Concurrency Gate + +- 문제: `Node.OnRunRequest`는 store insert 후 바로 adapter `Execute`를 호출한다 (`apps/node/internal/node/node.go:127`). runtime concurrency config와 adapter capability `MaxConcurrency`가 실행 제한으로 연결되어 있지 않다. +- 해결 방법: Node에 scheduler/permit manager를 두고 global runtime limit과 adapter/profile limit을 계산한다. policy는 우선 `reject` 또는 bounded wait timeout 중 하나로 고정하고, background/foreground 모두 같은 permit path를 탄다. cancellation과 store completion status가 permit 획득 전/후에서 일관되게 남도록 한다. +- 수정 파일 및 체크리스트: + - [ ] `apps/node/internal/node/node.go`: run execution을 scheduler permit으로 감싼다. + - [ ] `apps/node/internal/node/run_manager.go`: active run tracking과 scheduling 책임을 분리하거나 확장한다. + - [ ] `apps/node/internal/runtime/types.go`: capability/concurrency metadata 확인. + - [ ] `proto/iop/runtime.proto`: 필요한 config 필드가 이미 충분한지 확인. +- 테스트 작성: 추가한다. `node_test.go`에 blocking adapter 두 개를 이용해 limit 1에서 두 번째 요청이 정의된 정책대로 reject/timeout/queue 되는지, background run도 gate를 통과하는지 검증한다. race test가 통과해야 한다. +- 중간 검증: + - `go test -race -count=1 ./apps/node/internal/node/...` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/node/internal/node/node.go` | REFACTOR-1 | +| `apps/node/internal/node/run_manager.go` | REFACTOR-1 | +| `apps/node/internal/node/node_test.go` | REFACTOR-1 | +| `apps/node/internal/runtime/types.go` | REFACTOR-1 | +| `proto/iop/runtime.proto` | REFACTOR-1 | + +## 최종 검증 + +```bash +go test -race -count=1 ./apps/node/internal/node/... +go test -count=1 ./apps/node/... +``` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke/CODE_REVIEW-cloud-G07.md b/agent-task/m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke/CODE_REVIEW-cloud-G07.md new file mode 100644 index 00000000..87dd444a --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,112 @@ + + +# Code Review Reference - TEST + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> Record the exact remote/local environment, commands, stdout/stderr, and any artifact paths. +> If blocked by user-owned server, node, token, secret, or model endpoint preparation, fill `사용자 리뷰 요청` and stop. +> Do not ask the user directly. + +## 개요 + +date=2026-06-10 +task=m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke, plan=0, tag=TEST + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `field-smoke`: split-host smoke에서 하나의 연결된 Node가 최소 두 개의 inference model target과 하나의 CLI target을 같은 Edge-Node 연결로 호출할 수 있음을 확인한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +smoke 증거가 같은 Edge-Node 연결과 같은 Node id를 가리키는지 확인한다. 환경 blocker가 repo-owned인지 user-owned인지 구분한다. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [TEST-1] Split Host Multi Target Smoke | [ ] | + +## 구현 체크리스트 + +- [ ] split-host smoke 사전 조건과 실제 실행 config를 기록한다. +- [ ] Edge `/v1/models`가 두 inference model target을 노출하는 증거를 수집한다. +- [ ] 같은 연결된 Node를 통해 inference model target 두 개를 각각 호출하는 증거를 수집한다. +- [ ] 같은 연결된 Node를 통해 CLI target 하나를 호출하는 증거를 수집한다. +- [ ] 실패 시 repo-owned fix와 user-owned prerequisite을 구분해 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +- [ ] `코드리뷰 결과`에 판정을 append한다. +- [ ] active `CODE_REVIEW-cloud-G07.md`를 `code_review_cloud_G07_N.log`로 아카이브한다. +- [ ] active `PLAN-cloud-G07.md`를 `plan_cloud_G07_M.log`로 아카이브한다. +- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다. +- [ ] WARN/FAIL이면 후속 상태 또는 `USER_REVIEW.md`를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `/v1/models`에 두 inference route가 실제로 보인다. +- 두 inference 호출과 CLI 호출이 같은 connected Node id/alias를 사용한다. +- secret/token 값이 문서에 노출되지 않았다. +- 실패가 있으면 repo-owned follow-up과 user-owned blocker가 분리되어 있다. + +## 검증 결과 + +### TEST-1 중간 검증 + +```bash +$ go test -count=1 ./... +(output) +``` + +```bash +$ curl -fsS http:///healthz +(output) +``` + +```bash +$ curl -fsS http:///v1/models +(output) +``` + +### 최종 검증 + +```bash +$ curl -fsS http:///v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"","messages":[{"role":"user","content":"ping"}]}' +(output) +``` + +```bash +$ curl -fsS http:///v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"","messages":[{"role":"user","content":"ping"}]}' +(output) +``` + +```bash +$ +(output) +``` diff --git a/agent-task/m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke/PLAN-cloud-G07.md b/agent-task/m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke/PLAN-cloud-G07.md new file mode 100644 index 00000000..4a77e625 --- /dev/null +++ b/agent-task/m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke/PLAN-cloud-G07.md @@ -0,0 +1,118 @@ + + +# Plan - TEST + +## 이 파일을 읽는 구현 에이전트에게 + +이 작업은 field smoke 증거 수집용이다. 코드 변경이 필요하면 최소 범위로 제한하고, `CODE_REVIEW-cloud-G07.md`에 실행 환경, 명령, 실제 출력, 실패 시 원인을 기록한다. 사용자 소유 원격 서버/노드/secret 준비가 필요하면 직접 묻지 말고 `사용자 리뷰 요청`에 재개 조건을 남긴다. + +## 배경 + +첫 Epic의 최종 확인은 하나의 Edge-Node proto-socket 연결에서 최소 두 inference model target과 하나의 CLI target을 호출하는 것이다. 단위 테스트가 통과해도 split-host 연결, bootstrap, edge API, CLI target을 함께 확인해야 실제 목표가 닫힌다. + +## 사용자 리뷰 요청 흐름 + +원격 서버, 노드, secret, 외부 모델 endpoint처럼 사용자 소유 prerequisite이 없으면 진행할 수 없는 경우 active review stub의 `사용자 리뷰 요청`에 기록한다. code-review가 user-review stop 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `field-smoke`: split-host smoke에서 하나의 연결된 Node가 최소 두 개의 inference model target과 하나의 CLI target을 같은 Edge-Node 연결로 호출할 수 있음을 확인한다. +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-test/local/rules.md` +- `agent-test/local/edge-smoke.md` +- `agent-test/local/node-smoke.md` +- `docs` +- `configs/edge.yaml` +- `apps/edge/internal/openai/routes.go` +- `apps/edge/internal/service/service.go` +- `apps/node/internal/transport/client.go` +- `apps/node/internal/transport/session.go` + +### 테스트 환경 규칙 + +- test_env: `local` +- field smoke는 local unit test가 아니라 split-host 실제 실행 증거가 핵심이다. 그래도 사전 회귀는 `go test -count=1 ./...` 또는 변경 범위별 테스트를 실행한다. +- 원격 endpoint, node token, 모델 endpoint secret은 사용자 소유 환경일 수 있으므로 없으면 `사용자 리뷰 요청` 사유가 된다. + +### 테스트 커버리지 공백 + +- 단위 테스트는 하나의 연결에서 두 inference model target과 CLI target을 모두 호출하는 실제 split-host 경로를 증명하지 않는다. +- `/v1/models`, `/v1/chat/completions`, `/v1/responses`, CLI command path를 같은 Node connection으로 묶은 증거가 없다. + +### 심볼 참조 + +- rename/remove 예정 심볼: 없음. smoke script나 docs를 추가할 경우 기존 command entrypoint와 config path를 확인한다. + +### 분할 판단 + +- predecessor `03`: route catalog 완료 필요. 현재 active plan, complete 없음. +- predecessor `04`: engine profiles 완료 필요. 현재 active plan, complete 없음. +- predecessor `05`: edge-owned options 완료 필요. 현재 active plan, complete 없음. +- predecessor `06`: commands/caps 완료 필요. 현재 active plan, complete 없음. +- predecessor `07`: runtime concurrency 완료 필요. 현재 active plan, complete 없음. +- field smoke는 모든 기능 구현 후 실행해야 하므로 최종 dependent task로 둔다. + +### 범위 결정 근거 + +- smoke 실패로 발견한 작은 config/docs 보정은 이 task에서 처리할 수 있다. +- 구조적 코드 결함은 새 follow-up plan으로 분리한다. 원격 secret이나 서버 준비는 repo가 해결하지 않는다. + +### 빌드 등급 + +- build: `cloud-G07`, review: `cloud-G07`. 원격/터미널 실행 증거와 장시간 command 진단이 중심이다. + +## 의존 관계 및 구현 순서 + +- `08+03,04,05,06,07_field_smoke`는 `03`, `04`, `05`, `06`, `07` predecessor subtask의 `complete.log`가 모두 생긴 뒤 실행한다. + +## 구현 체크리스트 + +- [ ] split-host smoke 사전 조건과 실제 실행 config를 기록한다. +- [ ] Edge `/v1/models`가 두 inference model target을 노출하는 증거를 수집한다. +- [ ] 같은 연결된 Node를 통해 inference model target 두 개를 각각 호출하는 증거를 수집한다. +- [ ] 같은 연결된 Node를 통해 CLI target 하나를 호출하는 증거를 수집한다. +- [ ] 실패 시 repo-owned fix와 user-owned prerequisite을 구분해 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [TEST-1] Split Host Multi Target Smoke + +- 문제: milestone 목표는 단위 테스트만으로 완료됐다고 볼 수 없다. 한 Node connection에서 multiple inference + CLI target을 함께 쓰는 실제 경로가 검증되어야 한다. +- 해결 방법: 빌드 산출물, Edge config, Node bootstrap config, node token, model route ids, CLI target id를 기록하고, `/healthz`, `/v1/models`, inference call 2개, CLI command/run 1개를 같은 Node id로 확인한다. 출력에는 request model, internal target/node id, status, 최소 응답 본문을 남긴다. +- 수정 파일 및 체크리스트: + - [ ] 필요 시 `docs/` 또는 smoke 문서에 최신 명령을 보정한다. + - [ ] 필요 시 `configs/edge.yaml` 예시를 실제 multi target smoke와 맞춘다. + - [ ] 코드 변경은 smoke를 막는 작은 config/docs 보정으로 제한한다. +- 테스트 작성: 신규 단위 테스트는 이 task의 주목적이 아니다. 필요한 경우 smoke script/문서 검증 명령만 추가한다. +- 중간 검증: + - `go test -count=1 ./...` + - `curl -fsS http:///healthz` + - `curl -fsS http:///v1/models` + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `docs/**` | TEST-1 | +| `configs/edge.yaml` | TEST-1 | +| `agent-task/m-node-multi-target-serving-foundation/08+03,04,05,06,07_field_smoke/CODE_REVIEW-cloud-G07.md` | TEST-1 | + +## 최종 검증 + +```bash +go test -count=1 ./... +curl -fsS http:///healthz +curl -fsS http:///v1/models +curl -fsS http:///v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"","messages":[{"role":"user","content":"ping"}]}' +curl -fsS http:///v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"","messages":[{"role":"user","content":"ping"}]}' +``` + +CLI target 호출은 프로젝트의 최신 Edge command endpoint 또는 `iop-edge` command로 실행하고, 실제 명령과 출력을 `CODE_REVIEW-cloud-G07.md`에 기록한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/edge/internal/edgecmd/config.go b/apps/edge/internal/edgecmd/config.go index ff3133fc..9263be6c 100644 --- a/apps/edge/internal/edgecmd/config.go +++ b/apps/edge/internal/edgecmd/config.go @@ -125,6 +125,30 @@ func configCheckCmd() *cobra.Command { } } +func anyOllamaEnabled(a config.AdaptersConf) bool { + if a.Ollama.Enabled { + return true + } + for _, inst := range a.OllamaInstances { + if inst.Enabled { + return true + } + } + return false +} + +func anyVllmEnabled(a config.AdaptersConf) bool { + if a.Vllm.Enabled { + return true + } + for _, inst := range a.VllmInstances { + if inst.Enabled { + return true + } + } + return false +} + func validateEdgeConfig(cfg *config.EdgeConfig) error { _, err := node.LoadFromConfig(cfg.Nodes) if err != nil { @@ -140,12 +164,27 @@ func validateEdgeConfig(cfg *config.EdgeConfig) error { name = fmt.Sprintf("index %d", i) } - if !n.Adapters.Ollama.Enabled && !n.Adapters.CLI.Enabled && !n.Adapters.Vllm.Enabled { + hasOllama := anyOllamaEnabled(n.Adapters) + hasVllm := anyVllmEnabled(n.Adapters) + if !hasOllama && !n.Adapters.CLI.Enabled && !hasVllm { return fmt.Errorf("node %q: at least one adapter must be enabled", name) } if n.Adapters.Ollama.Enabled && n.Adapters.Ollama.BaseURL == "" { return fmt.Errorf("node %q: ollama adapter base_url must not be empty", name) } + for j, inst := range n.Adapters.OllamaInstances { + if inst.Enabled && inst.BaseURL == "" { + return fmt.Errorf("node %q: ollama_instances[%d] %q: base_url must not be empty", name, j, inst.Name) + } + } + if n.Adapters.Vllm.Enabled && n.Adapters.Vllm.Endpoint == "" { + return fmt.Errorf("node %q: vllm adapter endpoint must not be empty", name) + } + for j, inst := range n.Adapters.VllmInstances { + if inst.Enabled && inst.Endpoint == "" { + return fmt.Errorf("node %q: vllm_instances[%d] %q: endpoint must not be empty", name, j, inst.Name) + } + } if n.Runtime.Concurrency < 0 { return fmt.Errorf("node %q: runtime concurrency must be non-negative", name) } diff --git a/apps/edge/internal/edgecmd/edgecmd_test.go b/apps/edge/internal/edgecmd/edgecmd_test.go index 1c0cdb02..4f55d2c5 100644 --- a/apps/edge/internal/edgecmd/edgecmd_test.go +++ b/apps/edge/internal/edgecmd/edgecmd_test.go @@ -216,3 +216,90 @@ bootstrap: t.Errorf("artifact_base_url = %q", doc.Bootstrap.ArtifactBaseURL) } } + +func TestValidateEdgeConfig_VllmLegacyEmptyEndpointRejected(t *testing.T) { + cfg := &config.EdgeConfig{ + Nodes: []config.NodeDefinition{ + { + ID: "node-1", + Alias: "test", + Token: "tok", + Adapters: config.AdaptersConf{ + Vllm: config.VllmConf{Enabled: true, Endpoint: ""}, + }, + }, + }, + } + err := validateEdgeConfig(cfg) + if err == nil { + t.Fatal("expected error for enabled vllm with empty endpoint") + } +} + +func TestValidateEdgeConfig_VllmInstanceEmptyEndpointRejected(t *testing.T) { + cfg := &config.EdgeConfig{ + Nodes: []config.NodeDefinition{ + { + ID: "node-1", + Alias: "test", + Token: "tok", + Adapters: config.AdaptersConf{ + VllmInstances: []config.VllmInstanceConf{ + {Name: "a100", Enabled: true, Endpoint: ""}, + }, + }, + }, + }, + } + err := validateEdgeConfig(cfg) + if err == nil { + t.Fatal("expected error for enabled vllm instance with empty endpoint") + } +} + +func TestValidateEdgeConfig_AllAdaptersDisabledRejected(t *testing.T) { + cfg := &config.EdgeConfig{ + Nodes: []config.NodeDefinition{ + { + ID: "node-1", + Alias: "test", + Token: "tok", + Adapters: config.AdaptersConf{ + Ollama: config.OllamaConf{Enabled: false}, + Vllm: config.VllmConf{Enabled: false}, + CLI: config.CLIConf{Enabled: false}, + OllamaInstances: []config.OllamaInstanceConf{ + {Name: "off", Enabled: false, BaseURL: "http://127.0.0.1:11434"}, + }, + VllmInstances: []config.VllmInstanceConf{ + {Name: "off", Enabled: false, Endpoint: "http://10.0.0.5:8000"}, + }, + }, + }, + }, + } + err := validateEdgeConfig(cfg) + if err == nil { + t.Fatal("expected error when all adapters are disabled") + } +} + +func TestValidateEdgeConfig_OllamaInstanceEnabledAccepted(t *testing.T) { + cfg := &config.EdgeConfig{ + Nodes: []config.NodeDefinition{ + { + ID: "node-1", + Alias: "test", + Token: "tok", + Adapters: config.AdaptersConf{ + OllamaInstances: []config.OllamaInstanceConf{ + {Name: "local", Enabled: true, BaseURL: "http://127.0.0.1:11434"}, + }, + }, + }, + }, + } + if err := validateEdgeConfig(cfg); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/apps/edge/internal/node/mapper.go b/apps/edge/internal/node/mapper.go index fd427c58..04fabd83 100644 --- a/apps/edge/internal/node/mapper.go +++ b/apps/edge/internal/node/mapper.go @@ -1,12 +1,18 @@ package node import ( + "fmt" + "iop/packages/go/config" iop "iop/proto/gen/iop" ) // BuildConfigPayload converts a NodeRecord's adapter config to the proto payload // sent to node during registration. +// +// After config normalisation, OllamaInstances and VllmInstances already contain +// the promoted legacy single-instance entries, so only the slice fields are +// iterated here. Duplicate instance names produce an error. func BuildConfigPayload(rec *NodeRecord) (*iop.NodeConfigPayload, error) { payload := &iop.NodeConfigPayload{ Runtime: &iop.NodeRuntimeConfig{ @@ -21,27 +27,49 @@ func BuildConfigPayload(rec *NodeRecord) (*iop.NodeConfigPayload, error) { Config: &iop.AdapterConfig_Mock{Mock: &iop.MockAdapterConfig{}}, }) - if rec.Adapters.Ollama.Enabled { + seen := make(map[string]struct{}) + + for _, inst := range rec.Adapters.OllamaInstances { + if !inst.Enabled { + continue + } + key := "ollama:" + inst.Name + if _, dup := seen[key]; dup { + return nil, fmt.Errorf("duplicate ollama instance name %q", inst.Name) + } + seen[key] = struct{}{} payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{ Type: "ollama", Enabled: true, + Name: inst.Name, Config: &iop.AdapterConfig_Ollama{ Ollama: &iop.OllamaAdapterConfig{ - BaseUrl: rec.Adapters.Ollama.BaseURL, - ContextSize: int32(rec.Adapters.Ollama.ContextSize), + BaseUrl: inst.BaseURL, + ContextSize: int32(inst.ContextSize), }, }, }) } - if rec.Adapters.Vllm.Enabled { + + for _, inst := range rec.Adapters.VllmInstances { + if !inst.Enabled { + continue + } + key := "vllm:" + inst.Name + if _, dup := seen[key]; dup { + return nil, fmt.Errorf("duplicate vllm instance name %q", inst.Name) + } + seen[key] = struct{}{} payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{ Type: "vllm", Enabled: true, + Name: inst.Name, Config: &iop.AdapterConfig_Vllm{ - Vllm: &iop.VllmAdapterConfig{Endpoint: rec.Adapters.Vllm.Endpoint}, + Vllm: &iop.VllmAdapterConfig{Endpoint: inst.Endpoint}, }, }) } + if rec.Adapters.CLI.Enabled { profiles := make(map[string]*iop.CLIProfileConfig, len(rec.Adapters.CLI.Profiles)) for name, p := range rec.Adapters.CLI.Profiles { diff --git a/apps/edge/internal/node/mapper_test.go b/apps/edge/internal/node/mapper_test.go index 26615621..c4c87a7b 100644 --- a/apps/edge/internal/node/mapper_test.go +++ b/apps/edge/internal/node/mapper_test.go @@ -9,15 +9,16 @@ import ( ) func TestBuildConfigPayload_OllamaEnabled(t *testing.T) { + // Simulates the post-normalisation state produced by LoadEdge for a legacy + // single-instance ollama config: OllamaInstances is populated by LoadEdge, + // so callers that bypass LoadEdge must populate it manually. rec := &edgenode.NodeRecord{ ID: "node-1", Alias: "test", Token: "token", Adapters: config.AdaptersConf{ - Ollama: config.OllamaConf{ - Enabled: true, - BaseURL: "http://localhost:11434", - ContextSize: 262144, + OllamaInstances: []config.OllamaInstanceConf{ + {Name: "ollama", Enabled: true, BaseURL: "http://localhost:11434", ContextSize: 262144}, }, }, Runtime: config.RuntimeConf{ @@ -243,6 +244,139 @@ func TestBuildConfigPayload_MockAlwaysPresent(t *testing.T) { } } +func TestBuildConfigPayload_MultiOllamaInstances(t *testing.T) { + rec := &edgenode.NodeRecord{ + ID: "node-1", + Alias: "test", + Token: "token", + Adapters: config.AdaptersConf{ + OllamaInstances: []config.OllamaInstanceConf{ + {Name: "local", Enabled: true, BaseURL: "http://127.0.0.1:11434", ContextSize: 131072}, + {Name: "dgx", Enabled: true, BaseURL: "http://192.168.0.91:11434", ContextSize: 262144}, + }, + }, + Runtime: config.RuntimeConf{WorkspaceRoot: "/tmp/ws"}, + } + + payload, err := edgenode.BuildConfigPayload(rec) + if err != nil { + t.Fatalf("BuildConfigPayload failed: %v", err) + } + + var ollamaAdapters []*iop.AdapterConfig + for _, a := range payload.Adapters { + if a.Type == "ollama" { + ollamaAdapters = append(ollamaAdapters, a) + } + } + if len(ollamaAdapters) != 2 { + t.Fatalf("expected 2 ollama adapters, got %d", len(ollamaAdapters)) + } + names := map[string]bool{} + for _, a := range ollamaAdapters { + names[a.Name] = true + } + if !names["local"] || !names["dgx"] { + t.Errorf("expected instance names local and dgx, got %v", names) + } +} + +func TestBuildConfigPayload_MultiVllmInstances(t *testing.T) { + rec := &edgenode.NodeRecord{ + ID: "node-1", + Alias: "test", + Token: "token", + Adapters: config.AdaptersConf{ + VllmInstances: []config.VllmInstanceConf{ + {Name: "a100", Enabled: true, Endpoint: "http://10.0.0.5:8000"}, + {Name: "h100", Enabled: true, Endpoint: "http://10.0.0.6:8000"}, + }, + }, + Runtime: config.RuntimeConf{WorkspaceRoot: "/tmp/ws"}, + } + + payload, err := edgenode.BuildConfigPayload(rec) + if err != nil { + t.Fatalf("BuildConfigPayload failed: %v", err) + } + + var vllmAdapters []*iop.AdapterConfig + for _, a := range payload.Adapters { + if a.Type == "vllm" { + vllmAdapters = append(vllmAdapters, a) + } + } + if len(vllmAdapters) != 2 { + t.Fatalf("expected 2 vllm adapters, got %d", len(vllmAdapters)) + } + names := map[string]bool{} + for _, a := range vllmAdapters { + names[a.Name] = true + if a.GetVllm() == nil { + t.Errorf("instance %q: expected vllm typed config", a.Name) + } + } + if !names["a100"] || !names["h100"] { + t.Errorf("expected instance names a100 and h100, got %v", names) + } +} + +func TestBuildConfigPayload_LegacyOllamaViaInstances(t *testing.T) { + rec := &edgenode.NodeRecord{ + ID: "node-1", + Alias: "test", + Token: "token", + Adapters: config.AdaptersConf{ + // Simulate what LoadEdge produces after legacy normalisation. + OllamaInstances: []config.OllamaInstanceConf{ + {Name: "ollama", Enabled: true, BaseURL: "http://localhost:11434", ContextSize: 4096}, + }, + }, + Runtime: config.RuntimeConf{WorkspaceRoot: "/tmp/ws"}, + } + + payload, err := edgenode.BuildConfigPayload(rec) + if err != nil { + t.Fatalf("BuildConfigPayload failed: %v", err) + } + + var found *iop.AdapterConfig + for _, a := range payload.Adapters { + if a.Type == "ollama" { + found = a + break + } + } + if found == nil { + t.Fatal("expected ollama adapter in payload") + } + if found.Name != "ollama" { + t.Errorf("expected name %q, got %q", "ollama", found.Name) + } + if found.GetOllama().GetBaseUrl() != "http://localhost:11434" { + t.Errorf("unexpected base_url: %q", found.GetOllama().GetBaseUrl()) + } +} + +func TestBuildConfigPayload_DuplicateOllamaNameError(t *testing.T) { + rec := &edgenode.NodeRecord{ + ID: "node-1", + Alias: "test", + Token: "token", + Adapters: config.AdaptersConf{ + OllamaInstances: []config.OllamaInstanceConf{ + {Name: "dup", Enabled: true, BaseURL: "http://127.0.0.1:11434"}, + {Name: "dup", Enabled: true, BaseURL: "http://127.0.0.2:11434"}, + }, + }, + } + + _, err := edgenode.BuildConfigPayload(rec) + if err == nil { + t.Fatal("expected error for duplicate ollama instance name") + } +} + func TestBuildConfigPayload_MockUsesTypedConfig(t *testing.T) { rec := &edgenode.NodeRecord{ ID: "node-1", diff --git a/apps/node/internal/adapters/ollama/ollama.go b/apps/node/internal/adapters/ollama/ollama.go index f59a5bc4..ddfb35f3 100644 --- a/apps/node/internal/adapters/ollama/ollama.go +++ b/apps/node/internal/adapters/ollama/ollama.go @@ -303,9 +303,8 @@ func ollamaOptionsFromInput(input map[string]any, contextSize int) map[string]an } } if contextSize > 0 { - if _, ok := options["num_ctx"]; !ok { - options["num_ctx"] = contextSize - } + // num_ctx affects Ollama runner sizing, so keep it owned by Edge config. + options["num_ctx"] = contextSize } if len(options) == 0 { return nil diff --git a/apps/node/internal/adapters/ollama/ollama_test.go b/apps/node/internal/adapters/ollama/ollama_test.go index 1cdbe6fe..ee828300 100644 --- a/apps/node/internal/adapters/ollama/ollama_test.go +++ b/apps/node/internal/adapters/ollama/ollama_test.go @@ -115,8 +115,17 @@ func TestOllamaExecutePassesOptionsAndTopLevelFields(t *testing.T) { if req.Options["temperature"].(float64) != 0.2 || req.Options["top_p"].(float64) != 0.9 || req.Options["num_predict"].(float64) != 32 { t.Fatalf("unexpected options: %+v", req.Options) } - if req.Options["num_ctx"].(float64) != 4096 { - t.Fatalf("request num_ctx should override config context size: %+v", req.Options) + switch v := req.Options["num_ctx"].(type) { + case float64: + if int(v) != 262144 { + t.Fatalf("config context size should override request num_ctx: %+v", req.Options) + } + case int: + if v != 262144 { + t.Fatalf("config context size should override request num_ctx: %+v", req.Options) + } + default: + t.Fatalf("unexpected num_ctx type %T in %+v", v, req.Options) } if req.KeepAlive != "10m" || req.Think != false || req.Format != "json" { t.Fatalf("top-level fields not passed: keep_alive=%v think=%v format=%v", req.KeepAlive, req.Think, req.Format) diff --git a/apps/node/internal/transport/client.go b/apps/node/internal/transport/client.go index 0852fb35..975038b1 100644 --- a/apps/node/internal/transport/client.go +++ b/apps/node/internal/transport/client.go @@ -20,8 +20,11 @@ const ( // proto-socket go/base_client.go sendHeartBeat), but a larger wait window // gives the peer's next heartbeat an extra chance to overwrite any stray // timer on slow or jittery links before it fires. - heartbeatWaitSec = 45 - registerTimeout = 10 * time.Second + heartbeatWaitSec = 45 + registerTimeout = 10 * time.Second + registerInitialWait = 100 * time.Millisecond + registerRetryWait = 250 * time.Millisecond + registerAttempts = 3 ) // RegisterResult is returned by DialEdge after successful registration. @@ -49,11 +52,7 @@ func DialEdge(ctx context.Context, addr, token string, logger *zap.Logger) (*Reg return nil, fmt.Errorf("transport: dial edge %s: %w", addr, err) } - resp, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse]( - &client.Communicator, - &iop.RegisterRequest{Token: token}, - registerTimeout, - ) + resp, err := registerWithEdge(ctx, client, token, logger) if err != nil { _ = client.Close() return nil, fmt.Errorf("transport: register: %w", err) @@ -75,3 +74,43 @@ func DialEdge(ctx context.Context, addr, token string, logger *zap.Logger) (*Reg Config: resp.GetConfig(), }, nil } + +func registerWithEdge(ctx context.Context, client *toki.TcpClient, token string, logger *zap.Logger) (*iop.RegisterResponse, error) { + timer := time.NewTimer(registerInitialWait) + select { + case <-ctx.Done(): + timer.Stop() + return nil, ctx.Err() + case <-timer.C: + } + + var lastErr error + for attempt := 1; attempt <= registerAttempts; attempt++ { + resp, err := toki.SendRequestTyped[*iop.RegisterRequest, *iop.RegisterResponse]( + &client.Communicator, + &iop.RegisterRequest{Token: token}, + registerTimeout, + ) + if err == nil { + return resp, nil + } + lastErr = err + if attempt == registerAttempts || !client.IsAlive() { + break + } + if logger != nil { + logger.Warn("register request failed, retrying", + zap.Int("attempt", attempt), + zap.Error(err), + ) + } + timer := time.NewTimer(registerRetryWait) + select { + case <-ctx.Done(): + timer.Stop() + return nil, ctx.Err() + case <-timer.C: + } + } + return nil, lastErr +} diff --git a/configs/edge.yaml b/configs/edge.yaml index 694f720e..35047608 100644 --- a/configs/edge.yaml +++ b/configs/edge.yaml @@ -69,6 +69,8 @@ nodes: # id is the stable node identity; omitting it falls back to an auto UUID (dev only). # agent_kind selects the registration kind; omitting it defaults to "generic-node". # Allowed values: "generic-node" (default). + # + # Single-adapter example (legacy style — still supported): - id: "node-example-01" alias: "example-node" token: "" @@ -101,3 +103,32 @@ nodes: runtime: concurrency: 1 workspace_root: "" + + # Multi-adapter example: two Ollama instances + CLI profiles on one node. + # Each ollama_instances entry requires a unique "name" within the node. + # - id: "node-multi-01" + # alias: "multi-engine-node" + # token: "" + # adapters: + # ollama_instances: + # - name: "ollama-local" + # enabled: true + # base_url: "http://127.0.0.1:11434" + # context_size: 131072 + # - name: "ollama-dgx" + # enabled: true + # base_url: "http://192.168.0.91:11434" + # context_size: 262144 + # vllm_instances: + # - name: "vllm-a100" + # enabled: true + # endpoint: "http://10.0.0.5:8000" + # cli: + # enabled: true + # profiles: + # claude: + # command: "claude" + # mode: "antigravity-print" + # runtime: + # concurrency: 4 + # workspace_root: "/workspace" diff --git a/packages/go/config/config.go b/packages/go/config/config.go index 04c5c43a..68245cb5 100644 --- a/packages/go/config/config.go +++ b/packages/go/config/config.go @@ -166,9 +166,33 @@ type MetricsConf struct { } type AdaptersConf struct { + // Legacy single-instance fields. When non-empty they are normalised into + // OllamaInstances / VllmInstances during config load so that all downstream + // code only needs to inspect the slice fields. Ollama OllamaConf `mapstructure:"ollama" yaml:"ollama"` Vllm VllmConf `mapstructure:"vllm" yaml:"vllm"` CLI CLIConf `mapstructure:"cli" yaml:"cli"` + + // Multi-instance collections. Each entry carries a unique Name that acts as + // the stable adapter instance identity within the node. Names must be unique + // within each type collection; duplicate names are rejected at load time. + OllamaInstances []OllamaInstanceConf `mapstructure:"ollama_instances" yaml:"ollama_instances,omitempty"` + VllmInstances []VllmInstanceConf `mapstructure:"vllm_instances" yaml:"vllm_instances,omitempty"` +} + +// OllamaInstanceConf is one named Ollama engine instance within a node. +type OllamaInstanceConf struct { + Name string `mapstructure:"name" yaml:"name"` + Enabled bool `mapstructure:"enabled" yaml:"enabled"` + BaseURL string `mapstructure:"base_url" yaml:"base_url"` + ContextSize int `mapstructure:"context_size" yaml:"context_size"` +} + +// VllmInstanceConf is one named vLLM engine instance within a node. +type VllmInstanceConf struct { + Name string `mapstructure:"name" yaml:"name"` + Enabled bool `mapstructure:"enabled" yaml:"enabled"` + Endpoint string `mapstructure:"endpoint" yaml:"endpoint"` } type OllamaConf struct { @@ -248,10 +272,63 @@ func LoadEdge(cfgFile string) (*EdgeConfig, error) { return nil, fmt.Errorf("nodes[%d] alias=%q: %w", i, cfg.Nodes[i].Alias, err) } cfg.Nodes[i].AgentKind = kind + + if err := normalizeAdapters(&cfg.Nodes[i].Adapters); err != nil { + name := cfg.Nodes[i].ID + if name == "" { + name = cfg.Nodes[i].Alias + } + return nil, fmt.Errorf("nodes[%d] %q adapters: %w", i, name, err) + } } return &cfg, nil } +// normalizeAdapters promotes legacy single-instance Ollama/Vllm fields into the +// typed instance slices and validates that all instance names are unique. +func normalizeAdapters(a *AdaptersConf) error { + if a.Ollama.Enabled { + legacy := OllamaInstanceConf{ + Name: "ollama", + Enabled: a.Ollama.Enabled, + BaseURL: a.Ollama.BaseURL, + ContextSize: a.Ollama.ContextSize, + } + a.OllamaInstances = append([]OllamaInstanceConf{legacy}, a.OllamaInstances...) + } + if a.Vllm.Enabled { + legacy := VllmInstanceConf{ + Name: "vllm", + Enabled: a.Vllm.Enabled, + Endpoint: a.Vllm.Endpoint, + } + a.VllmInstances = append([]VllmInstanceConf{legacy}, a.VllmInstances...) + } + + if err := checkUniqueNames("ollama_instances", func(i int) string { return a.OllamaInstances[i].Name }, len(a.OllamaInstances)); err != nil { + return err + } + if err := checkUniqueNames("vllm_instances", func(i int) string { return a.VllmInstances[i].Name }, len(a.VllmInstances)); err != nil { + return err + } + return nil +} + +func checkUniqueNames(field string, name func(int) string, n int) error { + seen := make(map[string]struct{}, n) + for i := 0; i < n; i++ { + k := name(i) + if k == "" { + return fmt.Errorf("%s[%d]: name must not be empty", field, i) + } + if _, dup := seen[k]; dup { + return fmt.Errorf("%s: duplicate name %q", field, k) + } + seen[k] = struct{}{} + } + return nil +} + func setDefaults(v *viper.Viper) { v.SetDefault("transport.edge_addr", "localhost:9090") v.SetDefault("logging.level", "info") diff --git a/packages/go/config/config_test.go b/packages/go/config/config_test.go index f8f20fc3..eefa7719 100644 --- a/packages/go/config/config_test.go +++ b/packages/go/config/config_test.go @@ -921,6 +921,145 @@ func TestLoadEdge_ControlPlaneDefaults(t *testing.T) { } } +func TestLoadEdge_MultiOllamaInstances(t *testing.T) { + dir := t.TempDir() + f := filepath.Join(dir, "edge.yaml") + yaml := ` +server: + listen: "0.0.0.0:9090" +nodes: + - alias: "multi" + adapters: + ollama_instances: + - name: "local" + enabled: true + base_url: "http://127.0.0.1:11434" + context_size: 131072 + - name: "dgx" + enabled: true + base_url: "http://192.168.0.91:11434" + context_size: 262144 +` + if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil { + t.Fatalf("write yaml: %v", err) + } + cfg, err := config.LoadEdge(f) + if err != nil { + t.Fatalf("load: %v", err) + } + if len(cfg.Nodes) == 0 { + t.Fatal("expected 1 node") + } + insts := cfg.Nodes[0].Adapters.OllamaInstances + if len(insts) != 2 { + t.Fatalf("expected 2 ollama instances, got %d", len(insts)) + } + if insts[0].Name != "local" || insts[0].BaseURL != "http://127.0.0.1:11434" || insts[0].ContextSize != 131072 { + t.Errorf("unexpected instance[0]: %+v", insts[0]) + } + if insts[1].Name != "dgx" || insts[1].BaseURL != "http://192.168.0.91:11434" || insts[1].ContextSize != 262144 { + t.Errorf("unexpected instance[1]: %+v", insts[1]) + } +} + +func TestLoadEdge_MultiVllmInstances(t *testing.T) { + dir := t.TempDir() + f := filepath.Join(dir, "edge.yaml") + yaml := ` +server: + listen: "0.0.0.0:9090" +nodes: + - alias: "multi-vllm" + adapters: + vllm_instances: + - name: "a100" + enabled: true + endpoint: "http://10.0.0.5:8000" + - name: "h100" + enabled: true + endpoint: "http://10.0.0.6:8000" +` + if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil { + t.Fatalf("write yaml: %v", err) + } + cfg, err := config.LoadEdge(f) + if err != nil { + t.Fatalf("load: %v", err) + } + if len(cfg.Nodes) == 0 { + t.Fatal("expected 1 node") + } + insts := cfg.Nodes[0].Adapters.VllmInstances + if len(insts) != 2 { + t.Fatalf("expected 2 vllm instances, got %d", len(insts)) + } + if insts[0].Name != "a100" || insts[0].Endpoint != "http://10.0.0.5:8000" { + t.Errorf("unexpected instance[0]: %+v", insts[0]) + } + if insts[1].Name != "h100" || insts[1].Endpoint != "http://10.0.0.6:8000" { + t.Errorf("unexpected instance[1]: %+v", insts[1]) + } +} + +func TestLoadEdge_LegacyOllamaPromotedToInstances(t *testing.T) { + dir := t.TempDir() + f := filepath.Join(dir, "edge.yaml") + yaml := ` +server: + listen: "0.0.0.0:9090" +nodes: + - alias: "local" + adapters: + ollama: + enabled: true + base_url: "http://192.168.0.91:11434" + context_size: 262144 +` + if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil { + t.Fatalf("write yaml: %v", err) + } + cfg, err := config.LoadEdge(f) + if err != nil { + t.Fatalf("load: %v", err) + } + insts := cfg.Nodes[0].Adapters.OllamaInstances + if len(insts) != 1 { + t.Fatalf("expected 1 ollama instance from legacy field, got %d", len(insts)) + } + if insts[0].Name != "ollama" { + t.Errorf("expected instance name %q, got %q", "ollama", insts[0].Name) + } + if insts[0].BaseURL != "http://192.168.0.91:11434" { + t.Errorf("expected base_url %q, got %q", "http://192.168.0.91:11434", insts[0].BaseURL) + } +} + +func TestLoadEdge_DuplicateOllamaInstanceNameRejected(t *testing.T) { + dir := t.TempDir() + f := filepath.Join(dir, "edge.yaml") + yaml := ` +server: + listen: "0.0.0.0:9090" +nodes: + - alias: "dup" + adapters: + ollama_instances: + - name: "same" + enabled: true + base_url: "http://127.0.0.1:11434" + - name: "same" + enabled: true + base_url: "http://127.0.0.2:11434" +` + if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil { + t.Fatalf("write yaml: %v", err) + } + _, err := config.LoadEdge(f) + if err == nil { + t.Fatal("expected error for duplicate ollama instance name") + } +} + func TestLoadEdge_ControlPlaneOverride(t *testing.T) { dir := t.TempDir() f := filepath.Join(dir, "edge.yaml") diff --git a/proto/gen/iop/runtime.pb.go b/proto/gen/iop/runtime.pb.go index f0a731d8..5f0228f3 100644 --- a/proto/gen/iop/runtime.pb.go +++ b/proto/gen/iop/runtime.pb.go @@ -1220,6 +1220,9 @@ func (x *NodeConfigPayload) GetRuntime() *NodeRuntimeConfig { } // AdapterConfig describes one adapter to enable on the node. +// name is the stable instance identity within a node; for single-instance +// adapters it may be empty (equivalent to the type name). When a node carries +// multiple instances of the same adapter type each must have a unique name. type AdapterConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // "mock" | "ollama" | "vllm" | "cli" @@ -1232,6 +1235,8 @@ type AdapterConfig struct { // *AdapterConfig_Vllm // *AdapterConfig_Mock Config isAdapterConfig_Config `protobuf_oneof:"config"` + Name string `protobuf:"bytes,8,opt,name=name,proto3" json:"name,omitempty"` // stable instance identity; empty = legacy single-instance + Target string `protobuf:"bytes,9,opt,name=target,proto3" json:"target,omitempty"` // optional default target/route hint for this instance unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1330,6 +1335,20 @@ func (x *AdapterConfig) GetMock() *MockAdapterConfig { return nil } +func (x *AdapterConfig) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AdapterConfig) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + type isAdapterConfig_Config interface { isAdapterConfig_Config() } @@ -1888,7 +1907,7 @@ const file_proto_iop_runtime_proto_rawDesc = "" + "\x06config\x18\x05 \x01(\v2\x16.iop.NodeConfigPayloadR\x06config\"u\n" + "\x11NodeConfigPayload\x12.\n" + "\badapters\x18\x01 \x03(\v2\x12.iop.AdapterConfigR\badapters\x120\n" + - "\aruntime\x18\x02 \x01(\v2\x16.iop.NodeRuntimeConfigR\aruntime\"\xb7\x02\n" + + "\aruntime\x18\x02 \x01(\v2\x16.iop.NodeRuntimeConfigR\aruntime\"\xe3\x02\n" + "\rAdapterConfig\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x18\n" + "\aenabled\x18\x02 \x01(\bR\aenabled\x123\n" + @@ -1896,7 +1915,9 @@ const file_proto_iop_runtime_proto_rawDesc = "" + "\x03cli\x18\x04 \x01(\v2\x15.iop.CLIAdapterConfigH\x00R\x03cli\x122\n" + "\x06ollama\x18\x05 \x01(\v2\x18.iop.OllamaAdapterConfigH\x00R\x06ollama\x12,\n" + "\x04vllm\x18\x06 \x01(\v2\x16.iop.VllmAdapterConfigH\x00R\x04vllm\x12,\n" + - "\x04mock\x18\a \x01(\v2\x16.iop.MockAdapterConfigH\x00R\x04mockB\b\n" + + "\x04mock\x18\a \x01(\v2\x16.iop.MockAdapterConfigH\x00R\x04mock\x12\x12\n" + + "\x04name\x18\b \x01(\tR\x04name\x12\x16\n" + + "\x06target\x18\t \x01(\tR\x06targetB\b\n" + "\x06config\"\x13\n" + "\x11MockAdapterConfig\"\xa7\x01\n" + "\x10CLIAdapterConfig\x12?\n" + diff --git a/proto/iop/runtime.proto b/proto/iop/runtime.proto index 972ec428..42313560 100644 --- a/proto/iop/runtime.proto +++ b/proto/iop/runtime.proto @@ -151,6 +151,9 @@ message NodeConfigPayload { } // AdapterConfig describes one adapter to enable on the node. +// name is the stable instance identity within a node; for single-instance +// adapters it may be empty (equivalent to the type name). When a node carries +// multiple instances of the same adapter type each must have a unique name. message AdapterConfig { string type = 1; // "mock" | "ollama" | "vllm" | "cli" bool enabled = 2; @@ -161,6 +164,8 @@ message AdapterConfig { VllmAdapterConfig vllm = 6; MockAdapterConfig mock = 7; } + string name = 8; // stable instance identity; empty = legacy single-instance + string target = 9; // optional default target/route hint for this instance } message MockAdapterConfig {}