diff --git a/agent-contract/outer/openai-compatible-api.md b/agent-contract/outer/openai-compatible-api.md index b583cc7..15259df 100644 --- a/agent-contract/outer/openai-compatible-api.md +++ b/agent-contract/outer/openai-compatible-api.md @@ -165,7 +165,9 @@ Workspace-bound route는 workspace가 없거나 상대 경로이면 OpenAI-compa - `parallel_tool_calls` - `stream_options` -`tools`가 있는 Chat Completions 요청에서 현재 Edge는 OpenAI tool call 생성을 보장하지 않는다. +`tools`가 있는 Chat Completions 요청에서 Edge는 backend-native tool call stream을 일반 구조화 이벤트로 중계하지 않는다. +단, backend가 assistant content에 Cline-style 텍스트 tool call 블록을 출력하고 그 function name이 요청의 `tools[].function.name`에 포함되어 있으면 Edge는 해당 블록을 OpenAI-compatible `message.tool_calls` 또는 stream `delta.tool_calls`로 변환하고 `finish_reason: "tool_calls"`를 반환한다. +지원하는 텍스트 블록의 최소 형태는 `>>JSON-or-text`이다. 호환성을 위해 `tool_choice`가 생략되었거나 `auto`/강제 tool 선택 형태로 들어오면 내부 실행 입력에서는 `tool_choice: "none"`으로 낮춰 백엔드의 auto tool-calling 요구 조건에 의해 요청 전체가 실패하지 않게 한다. `parallel_tool_calls`와 `stream_options`는 클라이언트 호환성을 위해 수신하지만 현재 Edge 실행 의미에는 반영하지 않는다. diff --git a/agent-roadmap/phase/operational-observability-provider-management/PHASE.md b/agent-roadmap/phase/operational-observability-provider-management/PHASE.md index 8557139..f7a46f1 100644 --- a/agent-roadmap/phase/operational-observability-provider-management/PHASE.md +++ b/agent-roadmap/phase/operational-observability-provider-management/PHASE.md @@ -20,6 +20,10 @@ provider 확장 Phase에서 검증한 Ollama, vLLM, SGLang, Lemonade 같은 추 - 경로: `agent-roadmap/archive/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md` - 요약: `models[]`를 외부 model key이자 운영 catalog model로 두고, Node 하위 provider와 served model list를 기준으로 Edge가 provider 선택, load-ratio routing, target rewrite, read-only catalog 상태를 소유하는 MVP 계약을 완료했다. +- [계획] Node Resource Model Unification + - 경로: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` + - 요약: Node를 Edge 연결 identity로 두고 CLI, OpenAI-compatible provider, 기타 resource를 같은 Node 아래 나열하며 provider/resource capacity만 concurrency를 소유하도록 runtime 계약과 dev-runtime 구성을 정렬한다. + - [스케치] 사용량, 토큰, 로그 운영 추적 MVP - 경로: `agent-roadmap/phase/operational-observability-provider-management/milestones/usage-token-log-ops-mvp.md` - 요약: 사용자 관리, token 관리, API/CLI/local inference 사용량 집계, 요청별 device/provider/time/token ledger, 로그 관리의 1차 운영 경계를 스케치한다. diff --git a/agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md b/agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md new file mode 100644 index 0000000..8046769 --- /dev/null +++ b/agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md @@ -0,0 +1,80 @@ +# Milestone: Node Resource Model Unification + +## 위치 + +- Roadmap: `agent-roadmap/ROADMAP.md` +- Phase: `agent-roadmap/phase/operational-observability-provider-management/PHASE.md` + +## 목표 + +Node를 provider/CLI를 함께 담는 Edge 연결 identity로 정리한다. +Node 자체는 global gate가 되지 않고, CLI, OpenAI-compatible provider, 기타 resource는 Node 아래 catalog로 나열되며 concurrency와 queue는 provider/resource capacity에서만 관리한다. +Mac dev-runtime에서는 Codex CLI와 MLX vLLM provider를 하나의 `mac-codex-node` 아래 resource로 합치고, Edge routing/status와 smoke 기준을 이 모델에 맞춘다. + +## 상태 + +[계획] + +## 구현 잠금 + +- 상태: 해제 +- SDD: 필요 +- SDD 문서: `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md` +- SDD 사유: Node identity, config schema, runtime wire 의미, Edge dispatch/status, Node admission, dev field smoke가 함께 바뀌는 runtime 책임 경계 변경이다. +- 잠금 해제 조건: 아래 체크리스트 + - [x] SDD 잠금이 해제되어 있다. + - [x] SDD 사용자 리뷰가 없거나 승인/해결되었다. + - [x] Acceptance Scenario가 Milestone 기능 Task와 연결되어 있다. + - [x] Evidence Map이 완료 시 `Roadmap Completion`과 최종 검증 evidence로 검증 가능하게 연결되어 있다. +- 결정 필요: 없음 + +## 범위 + +- `NodeRuntimeConfig.concurrency`와 `runtime.concurrency`를 legacy/compat metadata로 낮추고 node-wide admission source에서 제외한다. +- `nodes[].providers[]`를 Node 아래 resource/provider catalog로 해석하고, provider가 참조하는 adapter instance validation을 강화한다. +- Node runtime admission에서 global gate를 제거하고 provider adapter capacity gate만 유지한다. +- CLI adapter는 IOP 레벨 concurrency 제한을 갖지 않는 unlimited resource로 표현한다. +- Edge dispatch는 provider pool 요청만 queue/concurrency path를 사용하고, CLI/legacy direct route는 provider queue를 타지 않는다. +- Edge status는 Node 아래 정의된 resource/provider catalog를 우선해 중복 snapshot을 피한다. +- dev-runtime 구성과 문서, smoke 기준을 `mac-codex-node` 하나에 Codex CLI와 `mac-mlx-vllm` provider resource가 함께 있는 형태로 정렬한다. + +## 기능 + +### Epic: [node-resource-model] Node Identity and Resource Catalog + +Node를 Edge 연결 owner로 두고, 실행 제어는 Node 하위 resource/provider가 소유하도록 계약, runtime, dev 검증을 하나로 묶는다. + +- [ ] [contract-schema] Node runtime contract와 config validation이 Node identity, resource catalog, provider adapter reference 의미를 고정한다. 현재 작업: `agent-task/m-node-resource-model-unification/01_contract_schema/PLAN-cloud-G07.md`. 검증: contract/config/proto 관련 unit test와 `Roadmap Completion` evidence가 남아 있다. +- [ ] [node-admission] Node runtime admission에서 node-wide global gate가 제거되고 CLI는 unlimited, provider adapter는 capacity gate를 유지한다. 현재 작업: `agent-task/m-node-resource-model-unification/02+01_node_admission/PLAN-local-G06.md`. 검증: Node concurrency regression test와 CLI capability test가 통과한다. +- [ ] [edge-routing-status] Edge dispatch와 status가 provider pool/resource catalog 기준으로 동작하고 CLI/direct route는 provider queue에 묶이지 않는다. 현재 작업: `agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status/PLAN-cloud-G07.md`. 검증: Edge queue, OpenAI-compatible route, status provider test가 통과한다. +- [ ] [dev-runtime-profile] dev-runtime에서 Mac Codex CLI와 MLX vLLM provider가 하나의 Node identity 아래 resource로 배포되고 3 connected nodes, 3 provider candidates, total capacity 10 smoke가 확인된다. 현재 작업: `agent-task/m-node-resource-model-unification/04+01,02,03_dev_runtime_profile/PLAN-cloud-G07.md`. 검증: clean sync/rebuild, config check, refresh dry-run 또는 apply, `/v1/responses`와 `/v1/chat/completions` capacity smoke evidence가 남아 있다. + +## 완료 리뷰 + +- 상태: 없음 +- 요청일: 없음 +- 완료 근거: 새 계획 Milestone이며 기능 Task가 아직 충족되지 않았다. +- 검토 항목: 모든 기능 Task의 `Roadmap Completion`, SDD Evidence Map, 최종 dev-runtime smoke evidence +- 리뷰 코멘트: 없음 + +## 범위 제외 + +- Node를 여러 resource의 global gate 또는 global scheduler로 확장하는 설계 +- Control Plane을 Edge-local runtime registry의 canonical store로 만드는 변경 +- 새 외부 HTTP/gRPC endpoint 추가 +- provider/device/model qualification report, lifecycle policy, 품질 기반 routing +- billing, chargeback, 조직 IAM, 장기 audit retention + +## 작업 컨텍스트 + +- 관련 경로: `agent-contract/inner/edge-node-runtime-wire.md`, `agent-contract/inner/edge-config-runtime-refresh.md`, `proto/iop/runtime.proto`, `packages/go/config`, `apps/edge`, `apps/node`, `configs`, `agent-test/dev`, `agent-test/local`, `docs/edge-local-dev-guide.md` +- 표준선(선택): Node는 Edge와 연결되는 identity와 resource registry 책임을 가진다. Node 자체는 global admission gate가 아니며, resource/provider는 정의된 대로만 실행에 사용된다. +- 표준선(선택): provider concurrency와 queue는 provider pool/resource capacity에서만 관리하고, CLI 호출 수는 IOP 레벨에서 제한하지 않는다. +- 선행 작업: Model Alias Provider Pool과 Provider Catalog +- 후속 작업: 요청 실행 로그와 Usage Ledger 기반, Provider-Device-Model Qualification 리포트 +- 현재 작업 연결: + - `agent-task/m-node-resource-model-unification/01_contract_schema/PLAN-cloud-G07.md` + - `agent-task/m-node-resource-model-unification/02+01_node_admission/PLAN-local-G06.md` + - `agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status/PLAN-cloud-G07.md` + - `agent-task/m-node-resource-model-unification/04+01,02,03_dev_runtime_profile/PLAN-cloud-G07.md` +- 확인 필요: 없음 diff --git a/agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md b/agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md new file mode 100644 index 0000000..b295e87 --- /dev/null +++ b/agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md @@ -0,0 +1,117 @@ +# SDD: Node Resource Model Unification + +## 위치 + +- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` +- Phase: `agent-roadmap/phase/operational-observability-provider-management/PHASE.md` + +## 상태 + +[승인됨] + +## SDD 잠금 + +- 상태: 해제 +- 사용자 리뷰: 없음 +- 잠금 항목: + - 없음 + +## 문제 / 비목표 + +- 문제: 같은 Mac host에 Codex CLI와 MLX vLLM provider가 함께 있어도 현재 runtime과 dev profile은 이를 별도 Node처럼 취급할 수 있고, `runtime.concurrency`/global gate 의미가 Node를 shared capacity gate로 오해하게 만든다. 이 SDD는 Node를 Edge 연결 identity와 resource registry로 고정하고, provider/resource만 concurrency를 소유하도록 계약과 검증 기준을 고정한다. +- 비목표: + - Node를 global scheduler, global resource gate, 또는 Control Plane canonical registry로 만들지 않는다. + - provider/device/model qualification report나 lifecycle policy는 이번 Milestone에서 구현하지 않는다. + - 새 public endpoint나 새 provider engine을 추가하지 않는다. + - billing, org IAM, 장기 audit retention, 품질 평가 기반 routing은 다루지 않는다. + +## Source of Truth + +| 영역 | 기준 | 메모 | +|------|------|------| +| Roadmap | `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` | 목표, 기능 Task, 구현 잠금, 완료 판단 기준 | +| Contract | `agent-contract/inner/edge-node-runtime-wire.md`, `agent-contract/inner/edge-config-runtime-refresh.md` | Node runtime config, Edge-Node wire, config refresh 의미 기준 | +| Code | `proto/iop/runtime.proto`, `packages/go/config`, `apps/edge`, `apps/node`, `configs` | config validation, runtime admission, Edge dispatch/status 구현 기준 | +| External Provider | dev-runtime vLLM/MLX, Lemonade, vLLM provider endpoints | provider id, adapter instance, capacity smoke 기준 | +| User Decision | 없음 | 사용자가 Node/resource/provider 책임 경계와 provider-only concurrency 방향을 확정했다 | + +## State Machine + +| 상태 | 진입 조건 | 다음 상태 | 근거 | +|------|-----------|-----------|------| +| `node-configured` | `nodes[].id`와 adapter/resource catalog가 config에 존재한다 | `resource-catalog-validated` 또는 `config-error` | Edge config load/refresh | +| `resource-catalog-validated` | provider/resource가 같은 Node 안의 enabled adapter instance와 capacity/category를 참조한다 | `node-connected` | config validation result | +| `node-connected` | Node가 Edge에 연결되고 runtime config를 받는다 | `run-routed` 또는 `status-reported` | Edge-Node transport | +| `run-routed` | Edge가 CLI/direct request 또는 provider pool request를 받는다 | `provider-admitted` 또는 `direct-dispatched` | RunRequest routing fields | +| `provider-admitted` | request가 provider pool이고 provider capacity slot을 얻었다 | `provider-dispatched` 또는 `queued` | provider pool queue/admission | +| `direct-dispatched` | request가 CLI 또는 legacy direct route다 | `run-complete` 또는 `run-error` | Node adapter execution | +| `provider-dispatched` | Edge가 선택된 provider adapter target으로 dispatch했다 | `run-complete` 또는 `run-error` | provider adapter execution | +| `status-reported` | Edge가 Node/resource/provider snapshot을 집계한다 | 없음 | status provider snapshot | + +## Interface Contract + +- 계약 원문: `agent-contract/inner/edge-node-runtime-wire.md`, `agent-contract/inner/edge-config-runtime-refresh.md` +- 입력: + - `nodes[].id`: Edge와 연결되는 Node identity다. 같은 host의 CLI와 provider resource가 이 id 아래에 함께 있을 수 있다. + - `nodes[].alias`: 운영자가 인식하는 Node 별칭이다. routing source of truth는 id와 resource/provider id다. + - `nodes[].adapters`: Node 안에서 실행 가능한 adapter instance 목록이다. + - `nodes[].providers[]`: Node 아래 resource/provider catalog다. provider pool candidate와 status snapshot의 resource source로 사용한다. + - `nodes[].providers[].adapter`: 같은 Node 안의 enabled adapter instance를 참조해야 한다. + - `nodes[].providers[].capacity`: provider/resource admission capacity다. provider pool concurrency와 queue는 이 값을 기준으로 한다. + - `NodeRuntimeConfig.concurrency` / `runtime.concurrency`: legacy/compat metadata다. Node-wide admission source로 사용하지 않는다. + - `RunRequest.ProviderPool`: provider pool queue/admission을 사용할지 결정하는 flag다. `ModelGroupKey`만으로 queue에 진입하지 않는다. +- 출력: + - provider/resource status snapshot: Node 아래 정의된 resource/provider catalog를 우선해 provider id, node id, model/capacity/in-flight/queued 상태를 표현한다. + - direct execution result: CLI/direct route는 provider queue accounting을 증가시키지 않는다. + - provider execution result: provider pool route는 provider capacity와 queue accounting을 반영한다. +- 금지: + - Node id를 여러 resource의 global gate나 global concurrency bucket으로 쓰지 않는다. + - CLI adapter를 IOP level concurrency로 제한하지 않는다. + - `ModelGroupKey`가 있다는 이유만으로 CLI/direct route를 provider queue에 넣지 않는다. + - `mac-mlx-vllm` provider를 dev-runtime에서 별도 IOP Node identity로 분리하지 않는다. + - Control Plane을 Edge-local runtime registry의 canonical source로 만들지 않는다. + +## Acceptance Scenarios + +| ID | Milestone Task | Given | When | Then | +|----|----------------|-------|------|------| +| S01 | `contract-schema` | runtime contract와 config schema가 `NodeRuntimeConfig.concurrency`와 `nodes[].providers[]`를 포함한다 | 계약과 comments를 갱신한다 | node-wide concurrency가 legacy/compat metadata로 문서화되고 resource/provider capacity가 admission source로 문서화된다 | +| S02 | `contract-schema` | provider resource가 같은 Node의 disabled/missing adapter를 참조한다 | config validation을 실행한다 | validation error가 발생하고 enabled adapter reference만 통과한다 | +| S03 | `node-admission` | Node runtime concurrency가 1이고 서로 다른 CLI/provider resource가 있다 | 동시에 실행 요청을 보낸다 | Node global gate rejection 없이 adapter/resource capacity만 적용된다 | +| S04 | `node-admission` | CLI adapter capabilities를 조회한다 | status/capability snapshot을 만든다 | CLI `MaxConcurrency`는 unlimited 의미인 `0`으로 표현되고 provider adapters는 capacity gate를 유지한다 | +| S05 | `edge-routing-status` | CLI/direct request와 provider pool request가 모두 `ModelGroupKey`를 가진다 | Edge dispatch를 실행한다 | provider pool request만 queue를 사용하고 CLI/direct request는 direct dispatch된다 | +| S06 | `edge-routing-status` | Node가 resource catalog와 legacy adapter snapshot을 모두 가질 수 있다 | Edge status snapshot을 조회한다 | 정의된 resource/provider catalog가 우선되고 중복 adapter-level provider snapshot은 생성되지 않는다 | +| S07 | `dev-runtime-profile` | dev-runtime Mac host가 Codex CLI와 MLX vLLM provider를 실행한다 | clean deploy와 OpenAI-compatible capacity smoke를 실행한다 | connected Node는 `mac-codex-node`, `gx10-vllm-node`, `onexplayer-lemonade-node`이고 provider capacity total 10, queued >= 1, final in_flight/queued 0/0이 관측된다 | + +## Evidence Map + +| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 | +|----------|-------------------|------------------|---------------------------| +| S01 | contract/proto/comment diff plus targeted tests | `agent-task/m-node-resource-model-unification/01_contract_schema` | `contract-schema` Roadmap Completion과 contract/config/proto verification output | +| S02 | config validation unit test | `agent-task/m-node-resource-model-unification/01_contract_schema` | `contract-schema` Roadmap Completion과 missing/disabled adapter validation output | +| S03 | Node concurrency regression test | `agent-task/m-node-resource-model-unification/02+01_node_admission` | `node-admission` Roadmap Completion과 global gate no-op evidence | +| S04 | CLI capability and provider capacity tests | `agent-task/m-node-resource-model-unification/02+01_node_admission` | `node-admission` Roadmap Completion과 CLI unlimited/provider gate output | +| S05 | Edge dispatch/provider queue tests | `agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status` | `edge-routing-status` Roadmap Completion과 provider-only queue evidence | +| S06 | Edge status provider tests | `agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status` | `edge-routing-status` Roadmap Completion과 resource catalog snapshot evidence | +| S07 | dev-runtime clean deploy and capacity smoke | `agent-task/m-node-resource-model-unification/04+01,02,03_dev_runtime_profile` | `dev-runtime-profile` Roadmap Completion과 `/v1/responses`, `/v1/chat/completions`, capacity accounting evidence | + +## Cross-repo Dependencies + +- 없음 + +## Drift Check + +- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다. +- [x] Evidence Map이 code-review/complete.log에서 검증 가능하다. +- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다. +- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에만 남겼다. + +## 사용자 리뷰 이력 + +- 없음 + +## 작업 컨텍스트 + +- 표준선: Node는 Edge 연결 identity와 resource registry 책임을 가진다. CLI와 provider는 같은 Node 아래 resource로 공존할 수 있으며, concurrency는 provider/resource capacity가 소유한다. +- 표준선: provider pool queue는 `ProviderPool`이 true인 요청에서만 사용한다. CLI/direct route는 `ModelGroupKey`가 있어도 provider queue accounting을 사용하지 않는다. +- 후속 SDD: 요청 실행 로그와 Usage Ledger 기반이 provider/resource/node identity를 ledger schema로 확장할 때 이 SDD의 identity 기준을 참조한다. diff --git a/agent-task/m-node-resource-model-unification/01_contract_schema/CODE_REVIEW-cloud-G07.md b/agent-task/m-node-resource-model-unification/01_contract_schema/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..d3b8864 --- /dev/null +++ b/agent-task/m-node-resource-model-unification/01_contract_schema/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,115 @@ + + +# 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 selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked 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-27 +task=m-node-resource-model-unification/01_contract_schema, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` +- Task ids: + - `contract-schema`: Node runtime contract and resource catalog validation +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Runtime Concurrency Contract | [ ] | +| [API-2] Resource Catalog Validation | [ ] | +| [API-3] Resource Snapshot Compatibility | [ ] | + +## 구현 체크리스트 + +- [ ] `NodeRuntimeConfig.concurrency`를 node-wide admission source가 아닌 legacy/compat runtime payload로 계약 문서와 comments에서 정리한다. +- [ ] `nodes[].providers[]`를 현재 wire의 resource catalog로 해석한다는 compat 기준을 문서화하고, `category=cli|api|local_inference`가 CLI/provider/resource 구분자임을 명시한다. +- [ ] `nodes[].providers[].adapter`가 같은 node 안의 enabled adapter instance로 해석되어야 한다는 config validation을 추가한다. +- [ ] provider adapter reference, duplicate/resource category, legacy config compatibility regression test를 추가한다. +- [ ] proto 변경이 발생하면 `make proto`를 실행하고 generated diff가 원본 proto와 일치하는지 확인한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_0.log`로 아카이브한다. +- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_0.log`로 아카이브한다. +- [ ] `.gitignore`의 Agent-Ops 관리 block이 task artifact를 unignore하는지 확인한다. +- [ ] PASS이면 `complete.log`를 작성하고 active task 디렉터리를 archive로 이동한다. +- [ ] WARN/FAIL이면 다음 active plan/review 또는 `USER_REVIEW.md`를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 연결 대상: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `runtime.concurrency`가 더 이상 node-wide admission source로 문서화되지 않았는지 확인한다. +- provider/resource adapter validation이 같은 node의 enabled adapter 기준인지 확인한다. +- proto 변경이 있으면 generated 파일이 `make proto` 결과인지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +### API-1 중간 검증 +```text +$ go test -count=1 ./apps/edge/internal/configrefresh +(output) +``` + +### API-2 중간 검증 +```text +$ go test -count=1 ./packages/go/config ./apps/edge/internal/edgevalidate +(output) +``` + +### API-3 중간 검증 +```text +$ go test -count=1 ./proto/gen/... +(output) +``` + +### 최종 검증 +```text +$ go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/configrefresh ./apps/edge/internal/node +(output) +``` + +> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section?** diff --git a/agent-task/m-node-resource-model-unification/01_contract_schema/PLAN-cloud-G07.md b/agent-task/m-node-resource-model-unification/01_contract_schema/PLAN-cloud-G07.md new file mode 100644 index 0000000..0bae576 --- /dev/null +++ b/agent-task/m-node-resource-model-unification/01_contract_schema/PLAN-cloud-G07.md @@ -0,0 +1,225 @@ + + +# Node Resource Model Contract Plan + +## 이 파일을 읽는 구현 에이전트에게 + +이 계획을 구현한 뒤에는 반드시 같은 디렉터리의 `CODE_REVIEW-cloud-G07.md`에서 구현 에이전트 소유 섹션을 실제 구현 내용, 설계 결정, 검증 출력으로 채운다. 구현 중 선택된 Milestone의 `구현 잠금 > 결정 필요`가 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 외부 환경, secret, 서비스 준비, 증거 공백은 사용자 리뷰 요청이 아니라 검증/후속 계획으로 기록한다. + +## 배경 + +현재 config와 wire는 `nodes[].adapters`와 `nodes[].providers`를 이미 갖고 있지만, 계약 문서는 `NodeRuntimeConfig.concurrency`를 node-wide 실행 제한으로 정의한다. 사용자가 정리한 방향은 node가 global gate가 아니라 하나의 identity/connection owner이고, provider/CLI 같은 resource가 node 아래에 명시되는 구조다. 첫 단계는 구현 전 계약과 validation 기준을 맞춰 이후 Node/Edge 변경이 같은 의미를 공유하게 하는 것이다. + +## 사용자 리뷰 요청 흐름 + +구현 중 사용자 리뷰 요청은 선택된 Milestone lock 결정이 직접 막을 때만 `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 선택된 Milestone의 구현 잠금은 해제되어 있으므로 기본값은 `없음`이다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` +- Task ids: + - `contract-schema`: Node runtime contract and resource catalog validation +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/rules/project/rules.md` +- `agent-ops/rules/common/rules-roadmap.md` +- `agent-ops/skills/common/router.md` +- `agent-ops/skills/common/plan/SKILL.md` +- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` +- `agent-roadmap/current.md` +- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md` +- `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` +- `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md` +- `agent-roadmap/phase/update-plane-self-update-foundation/PHASE.md` +- `agent-roadmap/phase/update-plane-self-update-foundation/milestones/update-plane-stable-protocol.md` +- `agent-test/local/rules.md` +- `agent-test/local/node-smoke.md` +- `agent-test/local/edge-smoke.md` +- `agent-test/local/platform-common-smoke.md` +- `agent-test/dev/inventory.yaml` +- `agent-test/dev/edge-smoke.md` +- `agent-test/dev/node-smoke.md` +- `agent-test/dev/platform-common-smoke.md` +- `agent-contract/index.md` +- `agent-contract/inner/edge-node-runtime-wire.md` +- `agent-contract/inner/control-plane-edge-wire.md` +- `agent-contract/inner/edge-config-runtime-refresh.md` +- `agent-contract/outer/openai-compatible-api.md` +- `proto/iop/runtime.proto` +- `proto/iop/control.proto` +- `packages/go/config/config.go` +- `apps/edge/internal/edgevalidate/validate.go` +- `apps/edge/internal/configrefresh/classify.go` +- `apps/edge/internal/node/mapper.go` +- `apps/node/internal/adapters/config_set.go` +- `packages/go/config/config_test.go` +- `apps/edge/internal/configrefresh/classify_test.go` +- `apps/edge/internal/node/mapper_test.go` +- `apps/node/internal/adapters/config_set_test.go` + +### SDD 기준 + +선택된 Milestone은 `SDD: 필요`이며 `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`가 `[승인됨]`, SDD 잠금 `해제` 상태다. 이 subtask는 Acceptance Scenario `S01`, `S02`와 Milestone Task `contract-schema`를 대상으로 한다. + +Evidence Map은 contract/proto/comment diff, config validation unit test, missing/disabled adapter validation output을 요구한다. 따라서 구현 체크리스트는 `NodeRuntimeConfig.concurrency` legacy/no-op 계약 정리, `nodes[].providers[]` resource catalog 문서화, provider adapter reference validation, 관련 regression test를 포함한다. + +### 테스트 환경 규칙 + +test_env는 기본값 `local`이다. `agent-test/local/rules.md`와 matching profile `node-smoke`, `edge-smoke`, `platform-common-smoke`를 읽었다. 이 subtask는 `packages/go/**`, `proto/**`, `agent-contract/**`를 건드리므로 `go test -count=1 ./packages/go/... ./proto/gen/...`와, proto를 바꾸는 경우 `make proto`를 적용한다. dev-runtime 문서와 원격 smoke는 이 subtask의 필수 검증이 아니며 04번 subtask가 맡는다. + +### 테스트 커버리지 공백 + +- `NodeRuntimeConfig.concurrency` 의미를 legacy/no-op으로 바꾸는 계약 문구는 문서성 변경이라 unit coverage가 없다. +- provider adapter reference validation은 config/edgevalidate regression test가 필요하다. +- `ProviderSnapshot`을 resource snapshot으로 취급하는 compat 의미는 proto field 추가 없이 문서와 status tests로 검증한다. + +### 심볼 참조 + +삭제/rename 예정 symbol은 없다. 의미 변경 대상은 `NodeRuntimeConfig.concurrency`, `ProviderSnapshot`, `nodes[].providers[].adapter`, `runtime.concurrency` 문자열이다. + +### 분할 판단 + +split policy를 먼저 평가했다. 공유 task group은 `m-node-resource-model-unification`이다. + +- `01_contract_schema`: 계약/config validation 기준. +- `02+01_node_admission`: 01 완료 후 Node admission 의미 변경. +- `03+01_edge_dispatch_status`: 01 완료 후 Edge dispatch/status 의미 변경. +- `04+01,02,03_dev_runtime_profile`: 앞선 세 구현 완료 후 dev-runtime 문서/config/smoke 정리. + +이 subtask는 선행 의존성이 없다. + +### 범위 결정 근거 + +새 외부 API endpoint나 gRPC 도입은 제외한다. `proto`는 가능하면 field 추가 없이 legacy `ProviderSnapshot`을 resource snapshot compat surface로 문서화하고, 실제 새 field가 꼭 필요하다고 구현 중 판단될 때만 `make proto`와 생성물 diff를 포함한다. + +### 빌드 등급 + +`cloud-G07`: config/proto/contract 의미가 여러 런타임 경계에 영향을 주며, wrong-local implementation이 dev/runtime status를 깨뜨릴 수 있다. + +## 구현 체크리스트 + +- [ ] `NodeRuntimeConfig.concurrency`를 node-wide admission source가 아닌 legacy/compat runtime payload로 계약 문서와 comments에서 정리한다. +- [ ] `nodes[].providers[]`를 현재 wire의 resource catalog로 해석한다는 compat 기준을 문서화하고, `category=cli|api|local_inference`가 CLI/provider/resource 구분자임을 명시한다. +- [ ] `nodes[].providers[].adapter`가 같은 node 안의 enabled adapter instance로 해석되어야 한다는 config validation을 추가한다. +- [ ] provider adapter reference, duplicate/resource category, legacy config compatibility regression test를 추가한다. +- [ ] proto 변경이 발생하면 `make proto`를 실행하고 generated diff가 원본 proto와 일치하는지 확인한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] Runtime Concurrency Contract + +**문제:** `agent-contract/inner/edge-node-runtime-wire.md`는 `NodeRuntimeConfig.concurrency`를 node-wide 실행 제한으로 정의하고, `apps/edge/internal/configrefresh/classify.go:220`도 live-applyable node-wide concurrency로 주석화한다. 사용자의 방향과 충돌한다. + +**해결 방법:** contract, proto comment, Go comments를 `legacy compatibility payload; provider/resource capacity owns admission` 의미로 바꾼다. Wire field를 즉시 제거하지 않는다. + +Before: + +```proto +// proto/iop/runtime.proto:251 +message NodeRuntimeConfig { + int32 concurrency = 1; + reserved 2; +} +``` + +After: + +```proto +// NodeRuntimeConfig carries legacy node runtime metadata. Execution admission +// must not use this as a node-wide global gate; provider/resource capacity owns concurrency. +message NodeRuntimeConfig { + int32 concurrency = 1; // legacy compatibility, 0/unset means no node-wide limit + reserved 2; +} +``` + +**수정 파일 및 체크리스트:** + +- [ ] `proto/iop/runtime.proto` +- [ ] `agent-contract/inner/edge-node-runtime-wire.md` +- [ ] `agent-contract/inner/edge-config-runtime-refresh.md` +- [ ] `apps/edge/internal/configrefresh/classify.go` +- [ ] `apps/edge/internal/configrefresh/classify_test.go` + +**테스트 작성:** `TestClassifyRuntimeConcurrencyLegacyAppliedOrIgnored` 형태로 기존 `runtime.concurrency` only diff의 기대 의미를 갱신한다. 즉시 제거가 아니면 refresh result는 compat상 `applied`여도 admission 변경을 뜻하지 않는다고 확인한다. + +**중간 검증:** + +```bash +go test -count=1 ./apps/edge/internal/configrefresh +``` + +### [API-2] Resource Catalog Validation + +**문제:** `packages/go/config/config.go:112`의 `NodeProviderConf`는 `Adapter`를 문자열로 받지만, `LoadEdge`의 provider loop(`packages/go/config/config.go:512`)와 `edgevalidate.ValidateEdgeConfig`는 adapter가 같은 node의 enabled adapter instance인지 확인하지 않는다. Edge dispatch의 `providerDispatchable`도 `apps/edge/internal/service/run_dispatch.go:476`에서 non-empty만 본다. + +**해결 방법:** `edgevalidate` 또는 config helper에 provider adapter reference resolver를 추가한다. Exact instance key를 우선하고, type-name route는 enabled instance가 정확히 1개일 때만 허용한다. `category=cli` resource는 CLI enabled를 요구한다. + +**수정 파일 및 체크리스트:** + +- [ ] `apps/edge/internal/edgevalidate/validate.go` +- [ ] `packages/go/config/config.go` +- [ ] `packages/go/config/config_test.go` +- [ ] `apps/edge/internal/edgevalidate/*_test.go` 또는 기존 test package + +**테스트 작성:** bad adapter, disabled adapter, ambiguous type route, valid CLI resource, valid OpenAI-compatible provider instance를 table test로 추가한다. + +**중간 검증:** + +```bash +go test -count=1 ./packages/go/config ./apps/edge/internal/edgevalidate +``` + +### [API-3] Resource Snapshot Compatibility + +**문제:** wire는 `proto/iop/runtime.proto:118`의 `ProviderSnapshot`과 `proto/iop/control.proto:83`의 `provider_snapshots`만 갖는다. 이름은 provider지만 사용자는 CLI/provider 등 resource 목록을 node 아래에 보고 싶어 한다. + +**해결 방법:** field rename 없이 `ProviderSnapshot`을 legacy wire name으로 유지하고, 계약 문서에서 `category`가 resource kind를 나타내며 provider-pool 대상은 `models[].providers`에 참조된 catalog resource뿐이라고 명시한다. 실제 status 정리는 03번 subtask에서 수행한다. + +**수정 파일 및 체크리스트:** + +- [ ] `proto/iop/runtime.proto` +- [ ] `proto/iop/control.proto` +- [ ] `agent-contract/inner/control-plane-edge-wire.md` +- [ ] `agent-contract/inner/edge-config-runtime-refresh.md` + +**테스트 작성:** proto field 추가가 없으면 새 generated test는 생략한다. 문서/comment-only면 `go test` regression으로 충분하다. + +**중간 검증:** + +```bash +go test -count=1 ./proto/gen/... +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `proto/iop/runtime.proto` | API-1, API-3 | +| `proto/iop/control.proto` | API-3 | +| `agent-contract/inner/edge-node-runtime-wire.md` | API-1 | +| `agent-contract/inner/control-plane-edge-wire.md` | API-3 | +| `agent-contract/inner/edge-config-runtime-refresh.md` | API-1, API-3 | +| `packages/go/config/config.go` | API-2 | +| `packages/go/config/config_test.go` | API-2 | +| `apps/edge/internal/edgevalidate/validate.go` | API-2 | +| `apps/edge/internal/configrefresh/classify.go` | API-1 | +| `apps/edge/internal/configrefresh/classify_test.go` | API-1 | + +## 최종 검증 + +```bash +go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/configrefresh ./apps/edge/internal/node +``` + +Proto 원본을 변경했다면 추가로 실행한다. + +```bash +make proto +git diff -- proto/gen/iop +``` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-node-resource-model-unification/02+01_node_admission/CODE_REVIEW-local-G06.md b/agent-task/m-node-resource-model-unification/02+01_node_admission/CODE_REVIEW-local-G06.md new file mode 100644 index 0000000..b9a870e --- /dev/null +++ b/agent-task/m-node-resource-model-unification/02+01_node_admission/CODE_REVIEW-local-G06.md @@ -0,0 +1,103 @@ + + +# Code Review Reference - REFACTOR + +> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.** +> Complete the checklist, fill implementation-owned sections, then stop with active files in place and report ready for review. Do not ask the user directly during implementation. + +## 개요 + +date=2026-06-27 +task=m-node-resource-model-unification/02+01_node_admission, plan=0, tag=REFACTOR + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` +- Task ids: + - `node-admission`: Node admission global gate removal and resource capacity preservation +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REFACTOR-1] Remove Node Global Admission | [ ] | +| [REFACTOR-2] Make Runtime Concurrency No-op For Admission | [ ] | +| [REFACTOR-3] CLI Capability Unlimited | [ ] | + +## 구현 체크리스트 + +- [ ] `globalGate`를 admission path에서 제거하거나 no-op으로 축소해 node-wide 실행 제한이 생기지 않게 한다. +- [ ] adapter gate는 유지하고, provider/openai_compat/vllm/ollama capacity가 `Capabilities().MaxConcurrency`로 제한되는지 보존한다. +- [ ] `NodeRuntimeConfig.concurrency` refresh는 config metadata로만 보존하고 admission capacity를 바꾸지 않게 한다. +- [ ] CLI adapter `Capabilities()`의 `MaxConcurrency`를 `0`으로 바꿔 unlimited 의미를 노출한다. +- [ ] Node concurrency tests를 새 의미에 맞게 갱신하고, provider adapter capacity 제한 regression을 유지한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] active files를 `.log`로 아카이브한다. +- [ ] PASS이면 `complete.log`를 작성하고 archive로 이동한다. +- [ ] WARN/FAIL이면 다음 active plan/review 또는 `USER_REVIEW.md`를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 연결 대상: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- Node admission이 adapter gate만 사용하는지 확인한다. +- runtime concurrency refresh가 admission capacity를 변경하지 않는지 확인한다. +- CLI capabilities가 `MaxConcurrency=0`이고 provider adapters는 capacity gate를 유지하는지 확인한다. + +## 검증 결과 + +### REFACTOR-1 중간 검증 +```text +$ go test -count=1 ./apps/node/internal/node +(output) +``` + +### REFACTOR-2 중간 검증 +```text +$ go test -count=1 ./apps/node/internal/node ./apps/node/internal/adapters +(output) +``` + +### REFACTOR-3 중간 검증 +```text +$ go test -count=1 ./apps/node/internal/adapters/cli ./apps/node/internal/node +(output) +``` + +### 최종 검증 +```text +$ go test -count=1 ./apps/node/... +(output) +``` + +> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section?** diff --git a/agent-task/m-node-resource-model-unification/02+01_node_admission/PLAN-local-G06.md b/agent-task/m-node-resource-model-unification/02+01_node_admission/PLAN-local-G06.md new file mode 100644 index 0000000..657a907 --- /dev/null +++ b/agent-task/m-node-resource-model-unification/02+01_node_admission/PLAN-local-G06.md @@ -0,0 +1,217 @@ + + +# Node Admission Resource Plan + +## 이 파일을 읽는 구현 에이전트에게 + +이 계획은 `01_contract_schema` 완료 후 구현한다. 구현 뒤에는 반드시 `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 채운다. 구현 중 직접 사용자에게 질문하지 말고, 선택된 Milestone lock 결정만 review stub의 `사용자 리뷰 요청`에 기록한다. + +## 배경 + +Node는 현재 하나의 identity/connection owner가 아니라 `globalGate`를 가진 shared capacity gate처럼 동작한다. 사용자가 정리한 방향에서는 node-wide concurrency가 없어야 하며, CLI는 IOP 레벨에서 제한하지 않고 provider/resource capacity만 admission을 가져야 한다. 이 subtask는 Node 내부 admission을 resource/adapter 단위로 낮춘다. + +## 사용자 리뷰 요청 흐름 + +선택된 Milestone의 구현 잠금은 해제되어 있으므로 사용자 리뷰 요청 기본값은 `없음`이다. 환경/secret/provider 상태 문제는 검증 결과나 후속 계획으로 기록한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` +- Task ids: + - `node-admission`: Node admission global gate removal and resource capacity preservation +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `apps/node/internal/node/node.go` +- `apps/node/internal/node/run_manager.go` +- `apps/node/internal/adapters/cli/cli.go` +- `apps/node/internal/adapters/config_set.go` +- `apps/node/internal/adapters/openai_compat/openai_compat.go` +- `proto/iop/runtime.proto` +- `apps/node/internal/node/node_test.go` +- `apps/node/internal/node/node_concurrency_integration_test.go` +- `apps/node/internal/adapters/cli/cli_internal_test.go` +- `apps/node/internal/adapters/config_set_test.go` +- `apps/node/internal/adapters/openai_compat/openai_compat_test.go` +- `agent-test/local/rules.md` +- `agent-test/local/node-smoke.md` +- `agent-test/local/platform-common-smoke.md` +- `agent-contract/inner/edge-node-runtime-wire.md` +- `agent-contract/inner/edge-config-runtime-refresh.md` +- `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` +- `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md` + +### SDD 기준 + +선택된 Milestone은 `SDD: 필요`이며 `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`가 `[승인됨]`, SDD 잠금 `해제` 상태다. 이 subtask는 Acceptance Scenario `S03`, `S04`와 Milestone Task `node-admission`을 대상으로 한다. + +Evidence Map은 Node concurrency regression, global gate no-op evidence, CLI unlimited/provider gate output을 요구한다. 따라서 구현 체크리스트는 `globalGate` 제거 또는 no-op 축소, adapter gate 보존, runtime concurrency refresh no-op, CLI `MaxConcurrency=0`, 관련 Node/CLI regression test를 포함한다. + +### 테스트 환경 규칙 + +test_env는 `local`이다. Node 변경이므로 `agent-test/local/node-smoke.md` 기준을 적용한다. 필수 unit은 `go test -count=1 ./apps/node/...`이며, 실행 요청/admission을 바꾸므로 가능하면 repo 내부 edge-node smoke도 04번 통합 task에서 확인한다. + +### 테스트 커버리지 공백 + +- 기존 `apps/node/internal/node/node_test.go`는 global gate rejection을 기대하는 테스트를 포함한다. 이 테스트들은 새 의미로 갱신해야 한다. +- CLI `MaxConcurrency=4` 기대 테스트는 명시적으로 없지만 capabilities 결과를 쓰는 status/capability 테스트가 영향받을 수 있다. +- provider adapter gate는 기존 concurrency tests가 일부 커버하지만, runtime concurrency가 no-op임을 별도 regression으로 확인해야 한다. + +### 심볼 참조 + +제거 또는 의미 축소 대상: + +- `globalGate`: `apps/node/internal/node/node.go:37`, `:68`, `:212`, `:345`, `:357`, `:402`, `:405` +- `admissionManager.global`: `apps/node/internal/node/run_manager.go:195` +- `MaxConcurrency: 4` for CLI: `apps/node/internal/adapters/cli/cli.go:213` +- runtime concurrency tests: `apps/node/internal/node/node_test.go` around NCDRAIN runtime concurrency tests and concurrency integration tests. + +### 분할 판단 + +이 subtask는 `02+01_node_admission`이므로 predecessor `01_contract_schema`의 `complete.log`가 필요하다. 현재 predecessor는 active plan 상태이며 `complete.log`가 없다. 구현자는 runtime이 dependency를 만족시킨 뒤 시작해야 한다. + +### 범위 결정 근거 + +Edge queue/status 정책은 03번 subtask 범위다. 이 subtask는 Node-local admission과 CLI capability만 바꾼다. provider adapter capacity는 adapter `Capabilities().MaxConcurrency`로 계속 Node safety gate를 유지한다. + +### 빌드 등급 + +`local-G06`: Node-local refactor이며 test coverage가 가까이에 있지만 concurrency behavior 변경이라 ordering regression 위험이 있다. + +## 의존 관계 및 구현 순서 + +`02+01_node_admission`은 sibling `01_contract_schema` 완료 후 구현한다. directory dependency 외 추가 의존성은 없다. + +## 구현 체크리스트 + +- [ ] `globalGate`를 admission path에서 제거하거나 no-op으로 축소해 node-wide 실행 제한이 생기지 않게 한다. +- [ ] adapter gate는 유지하고, provider/openai_compat/vllm/ollama capacity가 `Capabilities().MaxConcurrency`로 제한되는지 보존한다. +- [ ] `NodeRuntimeConfig.concurrency` refresh는 config metadata로만 보존하고 admission capacity를 바꾸지 않게 한다. +- [ ] CLI adapter `Capabilities()`의 `MaxConcurrency`를 `0`으로 바꿔 unlimited 의미를 노출한다. +- [ ] Node concurrency tests를 새 의미에 맞게 갱신하고, provider adapter capacity 제한 regression을 유지한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [REFACTOR-1] Remove Node Global Admission + +**문제:** `apps/node/internal/node/node.go:208`의 `admissionFor`가 global gate와 adapter gate를 같이 잡고, `apps/node/internal/node/run_manager.go:207`의 `acquire`가 adapter 성공 뒤 global 실패 시 실행을 거부한다. 이 구조는 하나의 node에 CLI와 MLX provider가 동시에 있을 때 서로를 막을 수 있다. + +**해결 방법:** admission manager를 adapter gate only로 단순화한다. `Node.New`의 `globalConcurrency` 인자는 compatibility를 위해 남기더라도 admission에 쓰지 않는다. 관련 comments를 node identity/resource model에 맞게 고친다. + +Before: + +```go +// apps/node/internal/node/node.go:210 +return &admissionManager{ + global: n.globalGate, + adapter: n.adapterGateFor(adapterKey, caps), +} +``` + +After: + +```go +return &admissionManager{ + adapter: n.adapterGateFor(adapterKey, caps), +} +``` + +**수정 파일 및 체크리스트:** + +- [ ] `apps/node/internal/node/node.go` +- [ ] `apps/node/internal/node/run_manager.go` +- [ ] `apps/node/internal/node/node_test.go` +- [ ] `apps/node/internal/node/node_concurrency_integration_test.go` + +**테스트 작성:** runtime concurrency가 1이어도 서로 다른 unlimited adapters가 global rejection을 받지 않는 test를 추가하고, adapter `MaxConcurrency=1`은 두 번째 run을 reject하는 기존 test를 유지/갱신한다. + +**중간 검증:** + +```bash +go test -count=1 ./apps/node/internal/node +``` + +### [REFACTOR-2] Make Runtime Concurrency No-op For Admission + +**문제:** `apps/node/internal/node/node.go:337`부터 refresh가 `runtime.concurrency` change를 계산하고 `n.globalGate.updateCapacity`를 호출한다. 새 방향에서는 provider/resource capacity가 concurrency source다. + +**해결 방법:** `ConfigSet.Runtime`에는 값을 보존하되 live admission mutation을 제거한다. no-adapter-change refresh와 adapter swap refresh 모두 runtime concurrency 때문에 gate가 바뀌지 않아야 한다. + +**수정 파일 및 체크리스트:** + +- [ ] `apps/node/internal/node/node.go` +- [ ] `apps/node/internal/adapters/config_set.go` +- [ ] `apps/node/internal/node/node_test.go` +- [ ] `apps/node/internal/adapters/config_set_test.go` + +**테스트 작성:** 기존 NCDRAIN runtime concurrency tests를 "config refresh stores runtime metadata but does not throttle admission"으로 갱신한다. + +**중간 검증:** + +```bash +go test -count=1 ./apps/node/internal/node ./apps/node/internal/adapters +``` + +### [REFACTOR-3] CLI Capability Unlimited + +**문제:** CLI adapter는 `apps/node/internal/adapters/cli/cli.go:213`에서 `MaxConcurrency: 4`를 반환한다. 현재 admission path에서는 CLI 호출도 4개로 제한된다. + +**해결 방법:** CLI `MaxConcurrency`를 `0`으로 반환한다. `fifoGate`의 기존 의미(`capacity <= 0` unlimited)를 그대로 사용한다. + +Before: + +```go +// apps/node/internal/adapters/cli/cli.go:210 +return runtime.Capabilities{ + AdapterName: Name, + Targets: profiles, + MaxConcurrency: 4, +}, nil +``` + +After: + +```go +return runtime.Capabilities{ + AdapterName: Name, + Targets: profiles, + MaxConcurrency: 0, +}, nil +``` + +**수정 파일 및 체크리스트:** + +- [ ] `apps/node/internal/adapters/cli/cli.go` +- [ ] `apps/node/internal/adapters/cli/cli_internal_test.go` +- [ ] `apps/node/internal/node/node_test.go` + +**테스트 작성:** CLI capabilities regression을 추가하거나 Node capability command test 기대값을 `0`으로 갱신한다. + +**중간 검증:** + +```bash +go test -count=1 ./apps/node/internal/adapters/cli ./apps/node/internal/node +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `apps/node/internal/node/node.go` | REFACTOR-1, REFACTOR-2 | +| `apps/node/internal/node/run_manager.go` | REFACTOR-1 | +| `apps/node/internal/adapters/cli/cli.go` | REFACTOR-3 | +| `apps/node/internal/adapters/config_set.go` | REFACTOR-2 | +| `apps/node/internal/node/node_test.go` | REFACTOR-1, REFACTOR-2, REFACTOR-3 | +| `apps/node/internal/node/node_concurrency_integration_test.go` | REFACTOR-1 | +| `apps/node/internal/adapters/cli/cli_internal_test.go` | REFACTOR-3 | +| `apps/node/internal/adapters/config_set_test.go` | REFACTOR-2 | + +## 최종 검증 + +```bash +go test -count=1 ./apps/node/... +``` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status/CODE_REVIEW-cloud-G07.md b/agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..bccdd92 --- /dev/null +++ b/agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,103 @@ + + +# Code Review Reference - REFACTOR + +> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.** +> Complete the checklist, fill implementation-owned sections, then stop with active files in place and report ready for review. Do not ask the user directly during implementation. + +## 개요 + +date=2026-06-27 +task=m-node-resource-model-unification/03+01_edge_dispatch_status, plan=0, tag=REFACTOR + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` +- Task ids: + - `edge-routing-status`: Edge provider-only queue routing and resource-first status +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REFACTOR-1] Provider-only Queue Admission | [ ] | +| [REFACTOR-2] Provider Adapter Dispatch Validation | [ ] | +| [REFACTOR-3] Resource-first Status Snapshot | [ ] | + +## 구현 체크리스트 + +- [ ] service queue 진입 조건을 `ProviderPool && ModelGroupKey != "" && queue != nil`로 제한한다. +- [ ] CLI/openai legacy route는 `ModelGroupKey`가 있어도 direct dispatch로 흐르고 provider pool queue/inflight를 사용하지 않게 한다. +- [ ] provider-pool candidate resolution에서 provider adapter가 같은 node의 enabled adapter instance인지 확인한다. +- [ ] status snapshot은 정의된 resource/provider catalog를 우선하고, legacy adapter snapshot은 catalog가 없을 때만 compat로 낸다. +- [ ] provider별 in_flight/queued accounting이 같은 node의 여러 provider에서 독립적으로 유지되는지 test를 보강한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] active files를 `.log`로 아카이브한다. +- [ ] PASS이면 `complete.log`를 작성하고 archive로 이동한다. +- [ ] WARN/FAIL이면 다음 active plan/review 또는 `USER_REVIEW.md`를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 연결 대상: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- provider-pool만 queue를 타는지 확인한다. +- bad provider adapter가 config/load 또는 candidate resolution에서 막히는지 확인한다. +- status provider snapshot이 catalog resource 중심으로 중복 없이 나오는지 확인한다. + +## 검증 결과 + +### REFACTOR-1 중간 검증 +```text +$ go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai +(output) +``` + +### REFACTOR-2 중간 검증 +```text +$ go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/edgevalidate +(output) +``` + +### REFACTOR-3 중간 검증 +```text +$ go test -count=1 ./apps/edge/internal/service +(output) +``` + +### 최종 검증 +```text +$ go test -count=1 ./apps/edge/... +(output) +``` + +> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section?** diff --git a/agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status/PLAN-cloud-G07.md b/agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status/PLAN-cloud-G07.md new file mode 100644 index 0000000..65bb2a3 --- /dev/null +++ b/agent-task/m-node-resource-model-unification/03+01_edge_dispatch_status/PLAN-cloud-G07.md @@ -0,0 +1,207 @@ + + +# Edge Dispatch And Status Resource Plan + +## 이 파일을 읽는 구현 에이전트에게 + +이 계획은 `01_contract_schema` 완료 후 구현한다. 구현 뒤에는 반드시 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 채운다. Milestone lock 결정 외 이유로 사용자에게 직접 질문하지 않는다. + +## 배경 + +Edge는 OpenAI-compatible 요청에 `ModelGroupKey`를 항상 담고, service는 `ModelGroupKey != ""`이면 queue를 탄다. 사용자의 방향에서는 provider pool만 concurrency/queue를 관리해야 하며 CLI route는 queue/global gate에 묶이지 않아야 한다. 또한 Control Plane status는 node 아래 실제 정의된 resource/provider를 중복 없이 보여야 한다. + +## 사용자 리뷰 요청 흐름 + +선택된 Milestone의 구현 잠금은 해제되어 있으므로 기본값은 `없음`이다. 외부 provider 접근 실패나 dev smoke 증거 공백은 04번 검증 또는 후속 계획으로 기록한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` +- Task ids: + - `edge-routing-status`: Edge provider-only queue routing and resource-first status +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `apps/edge/internal/service/run_dispatch.go` +- `apps/edge/internal/service/model_queue.go` +- `apps/edge/internal/service/status_provider.go` +- `apps/edge/internal/openai/chat_handler.go` +- `apps/edge/internal/openai/responses_handler.go` +- `apps/edge/internal/openai/routes.go` +- `apps/edge/internal/edgevalidate/validate.go` +- `apps/edge/internal/node/mapper.go` +- `apps/edge/internal/node/store.go` +- `apps/edge/internal/bootstrap/runtime.go` +- `apps/edge/internal/transport/server.go` +- `apps/edge/internal/service/model_queue_test.go` +- `apps/edge/internal/service/service_test.go` +- `apps/edge/internal/service/service_internal_test.go` +- `apps/edge/internal/service/status_provider_test.go` +- `apps/edge/internal/openai/server_test.go` +- `apps/edge/internal/node/mapper_test.go` +- `apps/edge/internal/node/store_test.go` +- `apps/edge/internal/bootstrap/runtime_test.go` +- `apps/edge/internal/transport/server_test.go` +- `agent-test/local/edge-smoke.md` +- `agent-test/local/platform-common-smoke.md` +- `agent-contract/outer/openai-compatible-api.md` +- `agent-contract/inner/control-plane-edge-wire.md` +- `agent-contract/inner/edge-config-runtime-refresh.md` +- `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` +- `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md` + +### SDD 기준 + +선택된 Milestone은 `SDD: 필요`이며 `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`가 `[승인됨]`, SDD 잠금 `해제` 상태다. 이 subtask는 Acceptance Scenario `S05`, `S06`과 Milestone Task `edge-routing-status`를 대상으로 한다. + +Evidence Map은 provider-only queue test, CLI/direct route direct dispatch evidence, resource catalog status snapshot evidence를 요구한다. 따라서 구현 체크리스트는 `ProviderPool && ModelGroupKey != "" && queue != nil` queue 조건, provider adapter validation, resource-first status snapshot, provider별 accounting regression을 포함한다. + +### 테스트 환경 규칙 + +test_env는 `local`이다. Edge service/OpenAI/status 변경이므로 `agent-test/local/edge-smoke.md`와 `platform-common-smoke.md`를 적용한다. 필수 unit은 `go test -count=1 ./apps/edge/...`이며, OpenAI-compatible boundary는 `/v1/models`, `/v1/responses`, `/v1/chat/completions` unit/e2e coverage를 확인한다. + +### 테스트 커버리지 공백 + +- CLI route가 `ModelGroupKey`를 가져도 provider queue를 타지 않는 regression이 필요하다. +- provider adapter가 missing/disabled/ambiguous일 때 config validation 또는 candidate resolution에서 제외되는 test가 필요하다. +- `getSnapshotForNode`가 adapter-level snapshot과 provider catalog snapshot을 중복으로 내는 현상은 status provider test 갱신이 필요하다. + +### 심볼 참조 + +의미 변경 대상: + +- `SubmitRun`: `apps/edge/internal/service/run_dispatch.go:107` +- `resolveAdapterForNode`: `apps/edge/internal/service/run_dispatch.go:297` +- `providerDispatchable`: `apps/edge/internal/service/run_dispatch.go:476` +- `getSnapshotForNode`: `apps/edge/internal/service/model_queue.go:529` +- `ModelGroupKey` writes: `apps/edge/internal/openai/chat_handler.go:75`, `apps/edge/internal/openai/responses_handler.go:89` + +### 분할 판단 + +이 subtask는 `03+01_edge_dispatch_status`이므로 predecessor `01_contract_schema` 완료가 필요하다. 현재 predecessor는 active plan 상태이며 `complete.log`가 없다. `02+01_node_admission`과 병렬 가능하나 04번은 둘 다 완료 후 진행한다. + +### 범위 결정 근거 + +Node-local gate 제거는 02번 범위다. dev-runtime remote deploy와 actual `build/dev-runtime/edge.yaml` 반영은 04번 범위다. 이 subtask는 Edge runtime code와 tests만 변경한다. + +### 빌드 등급 + +`cloud-G07`: OpenAI outer API, service dispatch, provider queue, Control Plane status가 같이 걸린 cross-domain 변경이다. + +## 의존 관계 및 구현 순서 + +`03+01_edge_dispatch_status`는 sibling `01_contract_schema` 완료 후 구현한다. `02`와 직접 dependency는 없지만 04가 두 결과를 통합 검증한다. + +## 구현 체크리스트 + +- [ ] service queue 진입 조건을 `ProviderPool && ModelGroupKey != "" && queue != nil`로 제한한다. +- [ ] CLI/openai legacy route는 `ModelGroupKey`가 있어도 direct dispatch로 흐르고 provider pool queue/inflight를 사용하지 않게 한다. +- [ ] provider-pool candidate resolution에서 provider adapter가 같은 node의 enabled adapter instance인지 확인한다. +- [ ] status snapshot은 정의된 resource/provider catalog를 우선하고, legacy adapter snapshot은 catalog가 없을 때만 compat로 낸다. +- [ ] provider별 in_flight/queued accounting이 같은 node의 여러 provider에서 독립적으로 유지되는지 test를 보강한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [REFACTOR-1] Provider-only Queue Admission + +**문제:** `apps/edge/internal/service/run_dispatch.go:107`은 `ModelGroupKey != ""`이면 모든 route를 queue로 보낸다. OpenAI handlers는 legacy CLI route에도 `ModelGroupKey`를 채운다(`chat_handler.go:75`, `responses_handler.go:89`). + +**해결 방법:** queue path는 `req.ProviderPool` true일 때만 사용한다. OpenAI handler는 그대로 `ProviderPool`을 전달하므로 handler 변경은 최소화한다. + +Before: + +```go +// apps/edge/internal/service/run_dispatch.go:107 +if req.ModelGroupKey != "" && s.queue != nil { + return s.submitRunQueued(ctx, req) +} +``` + +After: + +```go +if req.ProviderPool && req.ModelGroupKey != "" && s.queue != nil { + return s.submitRunQueued(ctx, req) +} +return s.submitRunDirect(req) +``` + +**수정 파일 및 체크리스트:** + +- [ ] `apps/edge/internal/service/run_dispatch.go` +- [ ] `apps/edge/internal/openai/server_test.go` +- [ ] `apps/edge/internal/service/service_test.go` + +**테스트 작성:** legacy CLI model route request with non-empty `ModelGroupKey`가 queue groups/inflight를 만들지 않고 direct dispatch되는 test를 추가한다. provider-pool route는 기존처럼 queue를 탄다는 test를 유지한다. + +**중간 검증:** + +```bash +go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/openai +``` + +### [REFACTOR-2] Provider Adapter Dispatch Validation + +**문제:** `providerDispatchable`은 `apps/edge/internal/service/run_dispatch.go:476`에서 non-empty adapter만 확인한다. `nodes[].providers[].adapter`가 실제 node adapter와 어긋나도 provider-pool candidate가 될 수 있다. + +**해결 방법:** 01번 validation helper를 재사용하거나 service-local resolver를 추가해 provider adapter가 enabled exact instance 또는 unambiguous type route인지 확인한다. Config validation에서 막되, service candidate resolution도 defensive check를 둔다. + +**수정 파일 및 체크리스트:** + +- [ ] `apps/edge/internal/service/run_dispatch.go` +- [ ] `apps/edge/internal/service/model_queue_test.go` +- [ ] `apps/edge/internal/edgevalidate/validate.go` +- [ ] `apps/edge/internal/edgevalidate/*_test.go` + +**테스트 작성:** missing adapter, disabled adapter, ambiguous `openai_compat` type route, valid named adapter 각각 provider-pool candidate 결과를 검증한다. + +**중간 검증:** + +```bash +go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/edgevalidate +``` + +### [REFACTOR-3] Resource-first Status Snapshot + +**문제:** `getSnapshotForNode`는 `apps/edge/internal/service/model_queue.go:540`부터 CLI/Ollama/vLLM/OpenAICompat adapter snapshots를 만들고, 다시 `:626`부터 provider catalog snapshots를 추가한다. 같은 OpenAICompat provider가 adapter snapshot과 provider snapshot으로 중복 노출된다. + +**해결 방법:** `rec.Providers`가 있으면 그 catalog를 source of truth로 삼아 snapshots를 만들고 providerID stats만 붙인다. `rec.Providers`가 비어 있는 old config에서만 adapter-level compat snapshots를 낸다. CLI resource를 status에 보이게 하려면 config에 category `cli` provider/resource entry가 있어야 한다. + +**수정 파일 및 체크리스트:** + +- [ ] `apps/edge/internal/service/model_queue.go` +- [ ] `apps/edge/internal/service/status_provider.go` +- [ ] `apps/edge/internal/service/status_provider_test.go` +- [ ] `apps/edge/internal/service/model_queue_test.go` +- [ ] `proto/iop/control.proto` comments only if needed + +**테스트 작성:** node with CLI + OpenAICompat + providers catalog가 catalog snapshots만 반환하고, provider pool stats가 provider id별로 붙는지 확인한다. providers empty legacy node는 old adapter snapshot fallback을 확인한다. + +**중간 검증:** + +```bash +go test -count=1 ./apps/edge/internal/service +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `apps/edge/internal/service/run_dispatch.go` | REFACTOR-1, REFACTOR-2 | +| `apps/edge/internal/service/model_queue.go` | REFACTOR-2, REFACTOR-3 | +| `apps/edge/internal/service/status_provider.go` | REFACTOR-3 | +| `apps/edge/internal/openai/server_test.go` | REFACTOR-1 | +| `apps/edge/internal/service/model_queue_test.go` | REFACTOR-2, REFACTOR-3 | +| `apps/edge/internal/service/service_test.go` | REFACTOR-1 | +| `apps/edge/internal/service/status_provider_test.go` | REFACTOR-3 | +| `apps/edge/internal/edgevalidate/validate.go` | REFACTOR-2 | + +## 최종 검증 + +```bash +go test -count=1 ./apps/edge/... +``` + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-node-resource-model-unification/04+01,02,03_dev_runtime_profile/CODE_REVIEW-cloud-G07.md b/agent-task/m-node-resource-model-unification/04+01,02,03_dev_runtime_profile/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..dc9a055 --- /dev/null +++ b/agent-task/m-node-resource-model-unification/04+01,02,03_dev_runtime_profile/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,116 @@ + + +# Code Review Reference - TEST + +> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.** +> Complete the checklist, fill implementation-owned sections, then stop with active files in place and report ready for review. Do not ask the user directly during implementation. + +## 개요 + +date=2026-06-27 +task=m-node-resource-model-unification/04+01,02,03_dev_runtime_profile, plan=0, tag=TEST + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` +- Task ids: + - `dev-runtime-profile`: Dev runtime single Mac node profile and capacity smoke +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [TEST-1] Dev Profile Merge For Mac Resources | [ ] | +| [TEST-2] Dev Docs And Deploy Skill Alignment | [ ] | +| [TEST-3] Remote Dev Runtime Smoke | [ ] | +| [TEST-4] Capacity Smoke With Provider-only Concurrency | [ ] | + +## 구현 체크리스트 + +- [ ] dev-runtime config/profile에서 `mac-codex-node` 하나에 CLI adapter와 `mac-mlx-vllm` OpenAI-compatible provider resource를 함께 둔다. +- [ ] `mac-mlx-vllm-node` 별도 node/process/log 기준을 제거하거나 legacy note로 낮추고, provider id `mac-mlx-vllm`은 유지한다. +- [ ] `agent-test/dev/*`, `agent-test/local/*`, `docs/edge-local-dev-guide.md`, dev-runtime deploy skill의 node/provider 기준을 3 connected nodes + 3 provider candidates로 일치시킨다. +- [ ] dev-runtime clean sync, rebuild, config check, refresh dry-run/apply 또는 restart-required handling을 수행한다. +- [ ] `/v1/responses`와 `/v1/chat/completions` capacity smoke에서 total capacity 10, queued >= 1을 관측하고 완료 후 0/0 회복을 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. + +- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [ ] active files를 `.log`로 아카이브한다. +- [ ] PASS이면 `complete.log`를 작성하고 archive로 이동한다. +- [ ] WARN/FAIL이면 다음 active plan/review 또는 `USER_REVIEW.md`를 작성한다. + +## 계획 대비 변경 사항 + +_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ + +## 주요 설계 결정 + +_구현 에이전트가 주요 설계 결정 사항을 기록한다._ + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 연결 대상: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- dev profile이 3 connected nodes + 3 provider candidates로 일관되는지 확인한다. +- `mac-mlx-vllm` provider id가 유지되고 `mac-codex-node` 아래 resource로 이동했는지 확인한다. +- remote smoke가 HTTP success뿐 아니라 provider snapshot capacity evidence를 남겼는지 확인한다. + +## 검증 결과 + +### TEST-1 중간 검증 +```text +$ rg --sort path -n "4-node|4개 Node|mac-mlx-vllm-node|connected non-candidate" agent-test/dev agent-test/local docs/edge-local-dev-guide.md agent-ops/skills/project/dev-runtime-deploy/SKILL.md +(output) +``` + +### TEST-2 중간 검증 +```text +$ rg --sort path -n "4-node|4개 Node|4개 node|mac-mlx-vllm-node|node-mac-mlx-vllm" agent-test/dev agent-test/local docs/edge-local-dev-guide.md agent-ops/skills/project/dev-runtime-deploy/SKILL.md +(output) +``` + +### TEST-3 중간 검증 +```text +$ ssh toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && git status --short --branch && git log --oneline -1 && go test ./...' +(output) +``` + +### TEST-4 중간 검증 +```text +$ ssh toki@toki-labs.com 'curl -fsS http://127.0.0.1:18001/edges/edge-toki-labs-dev/status' +(output) +``` + +### 최종 검증 +```text +$ go test -count=1 ./... +(output) + +$ ssh toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && git status --short --branch && git log --oneline -1 && test -x build/dev-runtime/bin/edge && build/dev-runtime/bin/edge --config build/dev-runtime/edge.yaml config check && build/dev-runtime/bin/edge --config build/dev-runtime/edge.yaml config refresh --mode dry-run --addr 127.0.0.1:19093' +(output) + +$ ssh toki@toki-labs.com 'curl -fsS http://127.0.0.1:18001/edges/edge-toki-labs-dev/status && curl -fsS http://127.0.0.1:18083/v1/models' +(output) +``` + +> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section?** diff --git a/agent-task/m-node-resource-model-unification/04+01,02,03_dev_runtime_profile/PLAN-cloud-G07.md b/agent-task/m-node-resource-model-unification/04+01,02,03_dev_runtime_profile/PLAN-cloud-G07.md new file mode 100644 index 0000000..bcc9136 --- /dev/null +++ b/agent-task/m-node-resource-model-unification/04+01,02,03_dev_runtime_profile/PLAN-cloud-G07.md @@ -0,0 +1,248 @@ + + +# Dev Runtime Single Mac Node Profile Plan + +## 이 파일을 읽는 구현 에이전트에게 + +이 계획은 `01_contract_schema`, `02+01_node_admission`, `03+01_edge_dispatch_status` 완료 후 구현한다. 구현 뒤에는 반드시 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 채운다. 원격 runner, secret, provider 접근 문제가 있으면 사용자에게 묻지 말고 검증 결과와 남은 위험으로 기록한다. + +## 배경 + +사용자는 mac에 연결된 Codex CLI와 MLX vLLM provider가 node 두 개가 아니라 하나의 node id/alias 아래 resource로 나열되기를 원한다. 현재 dev 문서와 inventory는 `mac-codex-node`와 `mac-mlx-vllm-node`를 분리하고 4-node 기준을 섞어 둔다. 이 subtask는 구현 결과를 dev-runtime 기준에 반영하고, 3 connected nodes + 3 provider candidates로 배포 검증한다. + +## 사용자 리뷰 요청 흐름 + +선택된 Milestone의 구현 잠금은 해제되어 있으므로 기본값은 `없음`이다. 원격 host, token, provider 상태, port 충돌, smoke 불안정은 사용자 리뷰 요청이 아니라 검증 blocker/후속 evidence로 기록한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` +- Task ids: + - `dev-runtime-profile`: Dev runtime single Mac node profile and capacity smoke +- Completion mode: check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `agent-test/dev/inventory.yaml` +- `agent-test/dev/rules.md` +- `agent-test/dev/edge-smoke.md` +- `agent-test/dev/node-smoke.md` +- `agent-test/dev/platform-common-smoke.md` +- `agent-test/local/edge-smoke.md` +- `agent-test/local/node-smoke.md` +- `agent-test/local/platform-common-smoke.md` +- `docs/edge-local-dev-guide.md` +- `agent-ops/skills/project/dev-runtime-deploy/SKILL.md` +- `scripts/dev/edge.sh` +- `scripts/dev/node.sh` +- `scripts/dev/web.sh` +- `configs/edge.yaml` +- `apps/edge/internal/edgecmd/node_register.go` +- `apps/edge/internal/edgecmd/edgecmd_test.go` +- `apps/edge/internal/bootstrap/runtime.go` +- `apps/edge/internal/bootstrap/runtime_test.go` +- `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md` +- `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md` + +### SDD 기준 + +선택된 Milestone은 `SDD: 필요`이며 `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`가 `[승인됨]`, SDD 잠금 `해제` 상태다. 이 subtask는 Acceptance Scenario `S07`과 Milestone Task `dev-runtime-profile`을 대상으로 한다. + +Evidence Map은 dev-runtime clean deploy, connected nodes/provider candidates 확인, `/v1/responses`, `/v1/chat/completions`, capacity accounting smoke evidence를 요구한다. 따라서 구현 체크리스트는 Mac resource merge, dev docs alignment, config check/refresh handling, total capacity 10과 queued >= 1 smoke 기록을 포함한다. + +### 테스트 환경 규칙 + +test_env는 plan 기본값 `local`이지만 이 subtask는 dev-runtime 원격 배포 검증을 포함한다. `agent-test/dev/inventory.yaml`, `agent-test/dev/edge-smoke.md`, `agent-test/dev/node-smoke.md`를 추가로 읽고 dev preflight를 계획에 포함한다. 필수 local unit은 `go test -count=1 ./...`이고, 원격 검증은 clean sync/rebuild 후 `config check`, `config refresh --mode dry-run`, `/v1/models`, `/v1/responses`, `/v1/chat/completions` capacity smoke다. + +### 테스트 환경 프리플라이트 + +- runner: `ssh toki@toki-labs.com` +- repo root: `/Users/toki/agent-work/iop-dev` +- Edge config: `build/dev-runtime/edge.yaml` +- Edge id: `edge-toki-labs-dev` +- binaries: `build/dev-runtime/bin/edge`, `build/dev-runtime/bin/iop-node`, `build/dev-runtime/bin/iop-node-linux-arm64`, `build/dev-runtime/bin/iop-node-windows-amd64.exe` +- ports: Control Plane `18001`, OpenAI `18083`, Edge-Node TCP `18084`, refresh admin `19093` +- source sync: clean `origin/main` sync and `go test ./...` before rebuild. +- expected nodes after change: `mac-codex-node`, `gx10-vllm-node`, `onexplayer-lemonade-node`. +- expected provider candidates: `gx10-vllm=4`, `onexplayer-lemonade=3`, `mac-mlx-vllm=3`. + +### 테스트 커버리지 공백 + +- `build/dev-runtime/edge.yaml` is runtime artifact, not tracked; unit tests cannot prove remote config was actually merged. +- Capacity smoke is timing-sensitive; plan must record max observed `in_flight` and `queued`, not just HTTP success. +- Dev docs currently disagree: deploy skill mentions 3 connected nodes while `agent-test/dev/*` and docs still say 4 nodes. + +### 심볼 참조 + +삭제/rename symbol 없음. config/resource id changes: + +- remove separate `mac-mlx-vllm-node` runtime node process from dev profile. +- keep provider id `mac-mlx-vllm` under `mac-codex-node` so `models[].providers` can remain stable. + +### 분할 판단 + +이 subtask는 `04+01,02,03_dev_runtime_profile`이다. predecessors `01`, `02`, `03` 모두 `complete.log`가 필요하다. 현재 모두 active plan 상태이며 `complete.log`가 없다. + +### 범위 결정 근거 + +This task does not rework Edge/Node source behavior beyond prior subtasks. It updates dev profile/docs and performs deployment validation. Production/corp environment is excluded. + +### 빌드 등급 + +`cloud-G07`: remote runner, multi-host runtime, Windows/macOS/Linux node bootstrap, and external provider capacity smoke are central. + +## 의존 관계 및 구현 순서 + +`04+01,02,03_dev_runtime_profile` must wait for sibling `01_contract_schema`, `02+01_node_admission`, and `03+01_edge_dispatch_status` completion. No additional hidden dependency is added. + +## 구현 체크리스트 + +- [ ] dev-runtime config/profile에서 `mac-codex-node` 하나에 CLI adapter와 `mac-mlx-vllm` OpenAI-compatible provider resource를 함께 둔다. +- [ ] `mac-mlx-vllm-node` 별도 node/process/log 기준을 제거하거나 legacy note로 낮추고, provider id `mac-mlx-vllm`은 유지한다. +- [ ] `agent-test/dev/*`, `agent-test/local/*`, `docs/edge-local-dev-guide.md`, dev-runtime deploy skill의 node/provider 기준을 3 connected nodes + 3 provider candidates로 일치시킨다. +- [ ] dev-runtime clean sync, rebuild, config check, refresh dry-run/apply 또는 restart-required handling을 수행한다. +- [ ] `/v1/responses`와 `/v1/chat/completions` capacity smoke에서 total capacity 10, queued >= 1을 관측하고 완료 후 0/0 회복을 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [TEST-1] Dev Profile Merge For Mac Resources + +**문제:** `agent-test/dev/inventory.yaml:49`는 `mac-codex-node`를 CLI node로, `:69`는 `mac-mlx-vllm-node`를 별도 provider node로 둔다. 사용자의 원하는 구조는 같은 mac node identity 아래 CLI와 MLX provider resource가 함께 나열되는 것이다. + +**해결 방법:** dev-runtime `edge.yaml` candidate에서 `mac-codex-node` adapters에 CLI와 `openai_compat_instances[name=mac-mlx-vllm]`를 같이 두고, `providers[]`에 `id=mac-mlx-vllm`, `adapter=mac-mlx-vllm`, `capacity=3`을 둔다. `models[].providers.mac-mlx-vllm`는 유지한다. + +Example target shape: + +```yaml +nodes: + - id: mac-codex-node + alias: mac-codex + adapters: + cli: + enabled: true + openai_compat_instances: + - name: mac-mlx-vllm + enabled: true + provider: vllm-mlx + endpoint: http://127.0.0.1:8002/v1 + capacity: 3 + providers: + - id: mac-mlx-vllm + type: vllm-mlx + category: local_inference + adapter: mac-mlx-vllm + capacity: 3 +``` + +**수정 파일 및 체크리스트:** + +- [ ] remote `build/dev-runtime/edge.yaml` candidate +- [ ] `agent-test/dev/inventory.yaml` +- [ ] `agent-test/dev/edge-smoke.md` +- [ ] `agent-test/dev/node-smoke.md` +- [ ] `agent-test/local/edge-smoke.md` +- [ ] `agent-test/local/node-smoke.md` + +**테스트 작성:** tracked test rule/docs 변경은 unit test 대신 deterministic grep 검증을 둔다. Remote `config check`가 schema test 역할을 한다. + +**중간 검증:** + +```bash +rg --sort path -n "4-node|4개 Node|mac-mlx-vllm-node|connected non-candidate" agent-test/dev agent-test/local docs/edge-local-dev-guide.md agent-ops/skills/project/dev-runtime-deploy/SKILL.md +``` + +### [TEST-2] Dev Docs And Deploy Skill Alignment + +**문제:** `docs/edge-local-dev-guide.md:133`부터 provider candidates와 `:136` connected non-candidate가 4-node 기준으로 남아 있고, `agent-test/dev/edge-smoke.md:46`도 4-node 연결을 말한다. deploy skill은 이미 3개 node connected 기준을 일부 사용한다. + +**해결 방법:** 모든 dev-runtime 문서의 표현을 "3 connected nodes, 3 provider candidates, mac node has CLI + MLX provider resource"로 통일한다. Mac MLX process 운영 파일은 vllm-mlx provider process 로그로 유지하되 별도 iop-node 로그 기준은 제거한다. + +**수정 파일 및 체크리스트:** + +- [ ] `docs/edge-local-dev-guide.md` +- [ ] `agent-ops/skills/project/dev-runtime-deploy/SKILL.md` +- [ ] `agent-test/dev/rules.md` +- [ ] `agent-test/dev/edge-smoke.md` +- [ ] `agent-test/dev/node-smoke.md` +- [ ] `agent-test/local/edge-smoke.md` +- [ ] `agent-test/local/node-smoke.md` + +**테스트 작성:** docs-only consistency grep을 사용한다. + +**중간 검증:** + +```bash +rg --sort path -n "4-node|4개 Node|4개 node|mac-mlx-vllm-node|node-mac-mlx-vllm" agent-test/dev agent-test/local docs/edge-local-dev-guide.md agent-ops/skills/project/dev-runtime-deploy/SKILL.md +``` + +### [TEST-3] Remote Dev Runtime Smoke + +**문제:** source 변경만으로는 사용자가 원하는 runtime 구조가 실제 dev environment에서 동작하는지 확인할 수 없다. 별도 mac MLX node process가 남으면 SQLite lock이나 duplicate node confusion이 재발할 수 있다. + +**해결 방법:** remote runner를 clean sync/rebuild하고 Edge/mac/gx10/onexplayer runtime을 새 config로 재시작한다. Control Plane status에서 connected node가 3개인지, `mac-codex-node` snapshot에 CLI resource와 `mac-mlx-vllm` provider resource가 같이 보이는지 확인한다. + +**수정 파일 및 체크리스트:** + +- [ ] remote `/Users/toki/agent-work/iop-dev/build/dev-runtime/edge.yaml` +- [ ] remote process scripts/commands used for Edge and Node restart +- [ ] `CODE_REVIEW-cloud-G07.md` verification output + +**테스트 작성:** no unit test. 실제 remote smoke가 test다. + +**중간 검증:** + +```bash +ssh toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && git status --short --branch && git log --oneline -1 && go test ./...' +``` + +### [TEST-4] Capacity Smoke With Provider-only Concurrency + +**문제:** 새 구조에서 mac CLI와 mac MLX provider가 같은 node id를 공유하므로, provider capacity accounting이 node-wide gate 없이 provider id별로 독립 유지되어야 한다. + +**해결 방법:** `/v1/responses`와 `/v1/chat/completions` 각각 capacity+1 동시 요청을 보내고, Control Plane `provider_snapshots`에서 `gx10-vllm + onexplayer-lemonade + mac-mlx-vllm` total `in_flight=10`, `queued>=1`, 완료 후 `0/0`을 기록한다. CLI 호출은 별도 provider queue를 차지하지 않는지 status로 확인한다. + +**수정 파일 및 체크리스트:** + +- [ ] remote smoke command/output 기록 +- [ ] `CODE_REVIEW-cloud-G07.md` capacity evidence + +**테스트 작성:** remote smoke evidence. 요청별 node assignment 공개 API가 없으므로 aggregate snapshot으로 판정한다. + +**중간 검증:** + +```bash +ssh toki@toki-labs.com 'curl -fsS http://127.0.0.1:18001/edges/edge-toki-labs-dev/status' +``` + +## 수정 파일 요약 + +| 파일 | 항목 | +|---|---| +| `agent-test/dev/inventory.yaml` | TEST-1 | +| `agent-test/dev/rules.md` | TEST-2 | +| `agent-test/dev/edge-smoke.md` | TEST-1, TEST-2 | +| `agent-test/dev/node-smoke.md` | TEST-1, TEST-2 | +| `agent-test/local/edge-smoke.md` | TEST-1, TEST-2 | +| `agent-test/local/node-smoke.md` | TEST-1, TEST-2 | +| `docs/edge-local-dev-guide.md` | TEST-2 | +| `agent-ops/skills/project/dev-runtime-deploy/SKILL.md` | TEST-2 | +| remote `build/dev-runtime/edge.yaml` | TEST-1, TEST-3, TEST-4 | + +## 최종 검증 + +```bash +go test -count=1 ./... +``` + +```bash +ssh toki@toki-labs.com 'cd /Users/toki/agent-work/iop-dev && git status --short --branch && git log --oneline -1 && test -x build/dev-runtime/bin/edge && build/dev-runtime/bin/edge --config build/dev-runtime/edge.yaml config check && build/dev-runtime/bin/edge --config build/dev-runtime/edge.yaml config refresh --mode dry-run --addr 127.0.0.1:19093' +``` + +```bash +ssh toki@toki-labs.com 'curl -fsS http://127.0.0.1:18001/edges/edge-toki-labs-dev/status && curl -fsS http://127.0.0.1:18083/v1/models' +``` + +Capacity smoke command may use a temporary script under `/tmp` on the runner. Record the exact script path and command in the review file; do not add it to the repo. Expected result: responses and chat completions each observe total provider `in_flight=10`, `queued>=1`, and final provider snapshots recover to `in_flight=0`, `queued=0`. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/edge/internal/openai/chat_handler.go b/apps/edge/internal/openai/chat_handler.go index 059fe66..6a4733e 100644 --- a/apps/edge/internal/openai/chat_handler.go +++ b/apps/edge/internal/openai/chat_handler.go @@ -149,6 +149,13 @@ func (s *Server) completeChatCompletion(w http.ResponseWriter, r *http.Request, return } text, reasoning, normalized := normalizeCompletionOutput(outputPolicy, text, reasoning) + message := chatMessage{Role: "assistant", Content: text, ReasoningContent: reasoning} + if cleaned, toolCalls := synthesizeToolCallsFromText(text, req.Tools, handle.Dispatch().RunID); len(toolCalls) > 0 { + message.Content = cleaned + message.ReasoningContent = "" + message.ToolCalls = toolCalls + finishReason = "tool_calls" + } s.logger.Info("openai chat completion output", zap.String("run_id", handle.Dispatch().RunID), zap.Bool("strict_output", outputPolicy.Strict), @@ -167,7 +174,7 @@ func (s *Server) completeChatCompletion(w http.ResponseWriter, r *http.Request, Model: responseModel(req.Model, handle.Dispatch().Target), Choices: []chatCompletionChoice{{ Index: 0, - Message: chatMessage{Role: "assistant", Content: text, ReasoningContent: reasoning}, + Message: message, FinishReason: finishReason, }}, Usage: usage, diff --git a/apps/edge/internal/openai/server_test.go b/apps/edge/internal/openai/server_test.go index eaa2704..63b7661 100644 --- a/apps/edge/internal/openai/server_test.go +++ b/apps/edge/internal/openai/server_test.go @@ -263,6 +263,95 @@ func TestChatCompletionsDowngradesToolChoiceAutoToNone(t *testing.T) { } } +func TestChatCompletionsSynthesizesToolCallsFromClineTextBlock(t *testing.T) { + fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)} + fake.events <- &iop.RunEvent{Type: "delta", Delta: "먼저 현재 git 상태를 확인하겠습니다.\n\n\n\n\n[\"cd /config/workspace/iop && git status\"]\n\n\n"} + fake.events <- &iop.RunEvent{Type: "complete"} + + srv := NewServer(config.EdgeOpenAIConf{Adapter: "openai_compat"}, fake, nil) + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{ + "model":"qwen3.6:35b", + "messages":[{"role":"user","content":"status"}], + "tools":[{"type":"function","function":{"name":"run_commands","parameters":{"type":"object"}}}], + "tool_choice":"auto" + }`)) + w := httptest.NewRecorder() + + srv.handleChatCompletions(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status: got %d body=%s", w.Code, w.Body.String()) + } + var resp chatCompletionResponse + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode response: %v", err) + } + choice := resp.Choices[0] + if choice.FinishReason != "tool_calls" { + t.Fatalf("finish_reason: got %q, want tool_calls", choice.FinishReason) + } + if strings.Contains(choice.Message.Content, "") { + t.Fatalf("content still contains raw tool_call block: %q", choice.Message.Content) + } + if choice.Message.Content != "먼저 현재 git 상태를 확인하겠습니다." { + t.Fatalf("content: got %q", choice.Message.Content) + } + if len(choice.Message.ToolCalls) != 1 { + t.Fatalf("tool_calls len: got %d", len(choice.Message.ToolCalls)) + } + call, ok := choice.Message.ToolCalls[0].(map[string]any) + if !ok { + t.Fatalf("tool call shape: %+v", choice.Message.ToolCalls[0]) + } + if call["id"] != "call_iop_run-test_1" { + t.Fatalf("tool call id: got %+v", call["id"]) + } + fn, ok := call["function"].(map[string]any) + if !ok { + t.Fatalf("function shape: %+v", call["function"]) + } + if fn["name"] != "run_commands" { + t.Fatalf("function name: got %+v", fn["name"]) + } + var args map[string][]string + if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil { + t.Fatalf("arguments JSON: %v", err) + } + if len(args["commands"]) != 1 || args["commands"][0] != "cd /config/workspace/iop && git status" { + t.Fatalf("commands args: %+v", args["commands"]) + } +} + +func TestChatCompletionsLeavesUnknownTextToolCallAsContent(t *testing.T) { + fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)} + fake.events <- &iop.RunEvent{Type: "delta", Delta: "true"} + fake.events <- &iop.RunEvent{Type: "complete"} + + srv := NewServer(config.EdgeOpenAIConf{Adapter: "openai_compat"}, fake, nil) + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{ + "model":"qwen3.6:35b", + "messages":[{"role":"user","content":"status"}], + "tools":[{"type":"function","function":{"name":"run_commands"}}] + }`)) + w := httptest.NewRecorder() + + srv.handleChatCompletions(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status: got %d body=%s", w.Code, w.Body.String()) + } + var resp chatCompletionResponse + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode response: %v", err) + } + if resp.Choices[0].FinishReason == "tool_calls" || len(resp.Choices[0].Message.ToolCalls) != 0 { + t.Fatalf("unexpected tool call conversion: %+v", resp.Choices[0]) + } + if !strings.Contains(resp.Choices[0].Message.Content, "") { + t.Fatalf("raw unknown tool call should remain content: %q", resp.Choices[0].Message.Content) + } +} + func TestChatCompletionsMapsMaxCompletionTokens(t *testing.T) { fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)} fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"} @@ -317,6 +406,41 @@ func TestChatCompletionsStreamsSSE(t *testing.T) { } } +func TestChatCompletionsStreamSynthesizesToolCallsFromClineTextBlock(t *testing.T) { + fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)} + fake.events <- &iop.RunEvent{Type: "delta", Delta: "먼저 현재 git 상태를 확인하겠습니다.\n\n"} + fake.events <- &iop.RunEvent{Type: "delta", Delta: "\n\n\n[\"cd /config/workspace/iop && git status\"]\n\n\n"} + fake.events <- &iop.RunEvent{Type: "complete"} + + srv := NewServer(config.EdgeOpenAIConf{Adapter: "openai_compat"}, fake, nil) + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{ + "model":"stream-model", + "stream":true, + "messages":[{"role":"user","content":"status"}], + "tools":[{"type":"function","function":{"name":"run_commands"}}] + }`)) + w := httptest.NewRecorder() + + srv.handleChatCompletions(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status: got %d body=%s", w.Code, w.Body.String()) + } + body := w.Body.String() + if !strings.Contains(body, `"tool_calls"`) || !strings.Contains(body, `"finish_reason":"tool_calls"`) { + t.Fatalf("stream did not synthesize tool_calls:\n%s", body) + } + if strings.Contains(body, "") || strings.Contains(body, `\u003ctool_call`) { + t.Fatalf("stream leaked raw tool_call block:\n%s", body) + } + if !strings.Contains(body, `"index":0`) { + t.Fatalf("stream tool_call delta should include index:\n%s", body) + } + if !strings.Contains(body, `"content":"먼저 현재 git 상태를 확인하겠습니다."`) { + t.Fatalf("stream should preserve text before tool call:\n%s", body) + } +} + func TestChatCompletionsStreamingReportsClosedRunStream(t *testing.T) { fake := &fakeRunService{events: make(chan *iop.RunEvent)} close(fake.events) diff --git a/apps/edge/internal/openai/stream.go b/apps/edge/internal/openai/stream.go index d640227..57f3cef 100644 --- a/apps/edge/internal/openai/stream.go +++ b/apps/edge/internal/openai/stream.go @@ -36,8 +36,8 @@ func (s *Server) streamChatCompletion(w http.ResponseWriter, r *http.Request, re }}, }) - if outputPolicy.Strict && outputPolicy.StreamBuffer { - s.streamBufferedChatCompletion(w, r, handle, id, created, model, flusher, outputPolicy) + if len(req.Tools) > 0 || (outputPolicy.Strict && outputPolicy.StreamBuffer) { + s.streamBufferedChatCompletion(w, r, handle, id, created, model, flusher, outputPolicy, req.Tools) return } @@ -152,7 +152,7 @@ func (s *Server) streamChatCompletion(w http.ResponseWriter, r *http.Request, re } } -func (s *Server) streamBufferedChatCompletion(w http.ResponseWriter, r *http.Request, handle edgeservice.RunResult, id string, created int64, model string, flusher http.Flusher, outputPolicy strictOutputPolicy) { +func (s *Server) streamBufferedChatCompletion(w http.ResponseWriter, r *http.Request, handle edgeservice.RunResult, id string, created int64, model string, flusher http.Flusher, outputPolicy strictOutputPolicy, tools []any) { text, reasoning, finishReason, _, err := collectRunResult(r.Context(), handle.Stream(), handle.WaitTimeout()) if err != nil { writeSSEError(w, flusher, err.Error()) @@ -170,7 +170,31 @@ func (s *Server) streamBufferedChatCompletion(w http.ResponseWriter, r *http.Req zap.String("content_preview", previewString(text, 1000)), zap.String("reasoning_preview", previewString(reasoning, 1000)), ) - if text != "" { + if cleaned, toolCalls := synthesizeToolCallsFromText(text, tools, handle.Dispatch().RunID); len(toolCalls) > 0 { + if cleaned != "" { + writeSSE(w, flusher, chatCompletionChunk{ + ID: id, + Object: "chat.completion.chunk", + Created: created, + Model: model, + Choices: []chatCompletionChunkChoice{{ + Index: 0, + Delta: chatDelta{Content: cleaned}, + }}, + }) + } + writeSSE(w, flusher, chatCompletionChunk{ + ID: id, + Object: "chat.completion.chunk", + Created: created, + Model: model, + Choices: []chatCompletionChunkChoice{{ + Index: 0, + Delta: chatDelta{ToolCalls: toolCallsForStreamDelta(toolCalls)}, + }}, + }) + finishReason = "tool_calls" + } else if text != "" { writeSSE(w, flusher, chatCompletionChunk{ ID: id, Object: "chat.completion.chunk", diff --git a/apps/edge/internal/openai/types.go b/apps/edge/internal/openai/types.go index a3b9c93..8bbc067 100644 --- a/apps/edge/internal/openai/types.go +++ b/apps/edge/internal/openai/types.go @@ -2,6 +2,8 @@ package openai import ( "encoding/json" + "fmt" + "regexp" "strings" ) @@ -31,6 +33,7 @@ type chatMessage struct { ReasoningContent string `json:"reasoning_content,omitempty"` Images []any `json:"images,omitempty"` ToolCalls []any `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` ToolName string `json:"tool_name,omitempty"` } @@ -41,6 +44,7 @@ func (m *chatMessage) UnmarshalJSON(b []byte) error { ReasoningContent string `json:"reasoning_content,omitempty"` Images []any `json:"images,omitempty"` ToolCalls []any `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` ToolName string `json:"tool_name,omitempty"` } if err := json.Unmarshal(b, &raw); err != nil { @@ -51,6 +55,7 @@ func (m *chatMessage) UnmarshalJSON(b []byte) error { m.ReasoningContent = raw.ReasoningContent m.Images = raw.Images m.ToolCalls = raw.ToolCalls + m.ToolCallID = raw.ToolCallID m.ToolName = raw.ToolName return nil } @@ -91,8 +96,10 @@ func (req chatCompletionRequest) runInput(prompt string, messages []chatMessage, } func normalizeToolChoice(any) string { - // IOP does not yet emit OpenAI tool_calls, so prevent downstream auto - // tool selection from turning an otherwise valid chat request into a 400. + // IOP can synthesize OpenAI tool_calls from recognized text tool-call + // blocks, but it still cannot carry backend-native tool calls through the + // runtime event stream. Keep downstream auto tool selection disabled so a + // provider does not return structured calls that would be dropped. return "none" } @@ -132,6 +139,9 @@ func chatMessagesInput(messages []chatMessage) []any { if len(msg.ToolCalls) > 0 { m["tool_calls"] = msg.ToolCalls } + if msg.ToolCallID != "" { + m["tool_call_id"] = msg.ToolCallID + } if msg.ToolName != "" { m["tool_name"] = msg.ToolName } @@ -142,6 +152,8 @@ func chatMessagesInput(messages []chatMessage) []any { func contentToString(v any) string { switch t := v.(type) { + case nil: + return "" case string: return t case []any: @@ -160,6 +172,153 @@ func contentToString(v any) string { } } +var ( + textToolCallBlockRE = regexp.MustCompile(`(?is)<\s*tool_call\s*>(.*?)<\s*/\s*tool_call\s*>`) + textToolFunctionRE = regexp.MustCompile(`(?is)<\s*function\s*=\s*([A-Za-z_][A-Za-z0-9_.:-]*)\s*>(.*?)<\s*/\s*function(?:\s*=\s*[A-Za-z_][A-Za-z0-9_.:-]*)?\s*>`) + textToolParameterRE = regexp.MustCompile(`(?is)<\s*parameter\s*=\s*([A-Za-z_][A-Za-z0-9_.:-]*)\s*>(.*?)<\s*/\s*parameter(?:\s*=\s*[A-Za-z_][A-Za-z0-9_.:-]*)?\s*>`) + textToolCallOpeningHint = "= 'a' && r <= 'z': + b.WriteRune(r) + case r >= 'A' && r <= 'Z': + b.WriteRune(r) + case r >= '0' && r <= '9': + b.WriteRune(r) + case r == '_' || r == '-': + b.WriteRune(r) + default: + b.WriteByte('_') + } + } + return strings.Trim(b.String(), "_-") +} + +func toolCallsForStreamDelta(toolCalls []any) []any { + out := make([]any, 0, len(toolCalls)) + for i, item := range toolCalls { + call, ok := item.(map[string]any) + if !ok { + out = append(out, item) + continue + } + delta := make(map[string]any, len(call)+1) + for k, v := range call { + delta[k] = v + } + delta["index"] = i + out = append(out, delta) + } + return out +} + +func requestedToolNames(tools []any) map[string]struct{} { + names := make(map[string]struct{}, len(tools)) + for _, tool := range tools { + m, ok := tool.(map[string]any) + if !ok { + continue + } + if name, ok := m["name"].(string); ok && strings.TrimSpace(name) != "" { + names[strings.TrimSpace(name)] = struct{}{} + } + fn, ok := m["function"].(map[string]any) + if !ok { + continue + } + if name, ok := fn["name"].(string); ok && strings.TrimSpace(name) != "" { + names[strings.TrimSpace(name)] = struct{}{} + } + } + return names +} + +func parseTextToolCallBlock(block string, allowed map[string]struct{}) (string, string, bool) { + match := textToolFunctionRE.FindStringSubmatchIndex(block) + if match == nil { + return "", "", false + } + name := strings.TrimSpace(block[match[2]:match[3]]) + if _, ok := allowed[name]; !ok { + return "", "", false + } + body := block[match[4]:match[5]] + args := map[string]any{} + for _, param := range textToolParameterRE.FindAllStringSubmatchIndex(body, -1) { + key := strings.TrimSpace(body[param[2]:param[3]]) + if key == "" { + continue + } + args[key] = parseTextToolParameterValue(body[param[4]:param[5]]) + } + encoded, err := json.Marshal(args) + if err != nil { + return "", "", false + } + return name, string(encoded), true +} + +func parseTextToolParameterValue(raw string) any { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return "" + } + var decoded any + if err := json.Unmarshal([]byte(trimmed), &decoded); err == nil { + return decoded + } + return trimmed +} + func previewString(s string, max int) string { if max <= 0 || len(s) <= max { return s @@ -208,6 +367,7 @@ type chatDelta struct { Role string `json:"role,omitempty"` Content string `json:"content,omitempty"` ReasoningContent string `json:"reasoning_content,omitempty"` + ToolCalls []any `json:"tool_calls,omitempty"` } type openAIUsage struct { diff --git a/apps/node/internal/adapters/openai_compat/openai_compat.go b/apps/node/internal/adapters/openai_compat/openai_compat.go index 5478602..95aac76 100644 --- a/apps/node/internal/adapters/openai_compat/openai_compat.go +++ b/apps/node/internal/adapters/openai_compat/openai_compat.go @@ -355,12 +355,20 @@ func messagesFromInput(input map[string]any) []chatMessage { } role, _ := m["role"].(string) content, _ := m["content"].(string) + toolCallID, _ := m["tool_call_id"].(string) + toolCalls := anySlice(m["tool_calls"]) role = strings.TrimSpace(role) content = strings.TrimSpace(content) - if role == "" || content == "" { + toolCallID = strings.TrimSpace(toolCallID) + if role == "" || (content == "" && len(toolCalls) == 0) { continue } - out = append(out, chatMessage{Role: role, Content: content}) + out = append(out, chatMessage{ + Role: role, + Content: content, + ToolCalls: toolCalls, + ToolCallID: toolCallID, + }) } if len(out) > 0 { return out @@ -391,6 +399,13 @@ func stringInput(input map[string]any, key string) string { return "" } +func anySlice(v any) []any { + if items, ok := v.([]any); ok { + return items + } + return nil +} + // joinOpenAIPath appends an OpenAI-compatible path to the endpoint. When the // endpoint already targets the /v1 root it strips the leading /v1 from the path // so a configured ".../v1" endpoint does not produce ".../v1/v1/...". @@ -413,8 +428,10 @@ func readLimited(r io.Reader, limit int64) string { } type chatMessage struct { - Role string `json:"role"` - Content string `json:"content"` + Role string `json:"role"` + Content string `json:"content"` + ToolCalls []any `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` } type chatChunk struct { diff --git a/apps/node/internal/adapters/openai_compat/openai_compat_test.go b/apps/node/internal/adapters/openai_compat/openai_compat_test.go index cb12faf..b21548a 100644 --- a/apps/node/internal/adapters/openai_compat/openai_compat_test.go +++ b/apps/node/internal/adapters/openai_compat/openai_compat_test.go @@ -321,6 +321,61 @@ func TestOpenAICompatExecutePassesToolsAndToolChoice(t *testing.T) { } } +func TestOpenAICompatExecutePreservesToolCallMessages(t *testing.T) { + var body map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("decode: %v", err) + } + w.Header().Set("Content-Type", "text/event-stream") + _, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"ok"}}]}`) + _, _ = fmt.Fprintf(w, "data: [DONE]\n\n") + })) + defer server.Close() + + adapter := New(config.OpenAICompatConf{Endpoint: server.URL}, zap.NewNop()) + sink := &fakeSink{} + if err := adapter.Execute(context.Background(), runtime.ExecutionSpec{ + RunID: "run-tool-turn", + Target: "qwen3.6:35b", + Input: map[string]any{ + "messages": []any{ + map[string]any{ + "role": "assistant", + "content": "", + "tool_calls": []any{map[string]any{ + "id": "call_iop_1", + "type": "function", + "function": map[string]any{ + "name": "run_commands", + "arguments": `{"commands":["git status"]}`, + }, + }}, + }, + map[string]any{ + "role": "tool", + "content": "clean", + "tool_call_id": "call_iop_1", + }, + }, + }, + }, sink); err != nil { + t.Fatalf("Execute failed: %v", err) + } + messages, ok := body["messages"].([]any) + if !ok || len(messages) != 2 { + t.Fatalf("messages not preserved: %+v", body["messages"]) + } + assistantMsg := messages[0].(map[string]any) + if calls, ok := assistantMsg["tool_calls"].([]any); !ok || len(calls) != 1 { + t.Fatalf("assistant tool_calls not preserved: %+v", assistantMsg) + } + toolMsg := messages[1].(map[string]any) + if toolMsg["tool_call_id"] != "call_iop_1" { + t.Fatalf("tool_call_id not preserved: %+v", toolMsg) + } +} + func TestOpenAICompatExecuteRejectsEmptyEndpointOrModel(t *testing.T) { t.Run("empty_endpoint", func(t *testing.T) { adapter := New(config.OpenAICompatConf{}, zap.NewNop())