diff --git a/agent-ops/rules/project/domain/node/rules.md b/agent-ops/rules/project/domain/node/rules.md index 80c90f0..79b7d8b 100644 --- a/agent-ops/rules/project/domain/node/rules.md +++ b/agent-ops/rules/project/domain/node/rules.md @@ -69,7 +69,7 @@ Edge에 연결되어 실제 adapter execution을 수행하는 IOP 노드 에이 - `cline-json`, `opencode-json`, `codex-json`, `claude-json` 같은 provider별 stdout parser는 `adapters/cli` emitter로 등록하고 runtime 공통 이벤트로만 외부에 노출한다. - CLI logical session은 `(target, session_id)`로 식별한다. Antigravity conversation id, Codex external id, opencode session/server 상태를 전역 target 단위로 공유하지 않는다. - `response_idle_timeout_ms`, `startup_idle_timeout_ms`, `completion_marker`, `resume_args`, `mode` 같은 CLI profile 설정은 edge config/proto payload를 통해 주입하고 node 코드에 target별 상수를 늘리지 않는다. -- Ollama adapter는 내부 target을 model 이름으로 사용하고, `context_size`는 `options.num_ctx` default로만 주입한다. 요청 input에 명시된 `options.num_ctx`가 있으면 이를 덮어쓰지 않는다. +- Ollama adapter는 내부 target을 model 이름으로 사용하고, `context_size`는 `options.num_ctx`의 강제 소유값으로 주입한다. 요청 input에 명시된 `options.num_ctx`가 있어도 Edge-owned `context_size`가 항상 우선한다. `context_size`가 0이면 request 값을 그대로 사용한다. - `RuntimeEvent`는 start/delta/reasoning_delta/complete/error/cancelled 타입을 유지하고, adapter별 streaming 표현을 node 외부로 새 이벤트 체계로 노출하지 않는다. - node 내부 변경은 가능한 대상 패키지 테스트를 먼저 추가하거나 갱신한다. - `apps/node/cmd/node/**`, `apps/node/internal/bootstrap/**`, `apps/node/internal/transport/**`, `apps/node/internal/node/**`, `apps/node/internal/router/**`, `apps/node/internal/adapters/**`, `apps/node/internal/store/**`의 실행 요청/응답/stream/cancel/status/session 경로를 바꾼 뒤에는 `testing` domain rule의 작업 후 검증 기준을 따른다. diff --git a/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/code_review_local_G07_0.log b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/code_review_local_G07_0.log new file mode 100644 index 0000000..f0c37a8 --- /dev/null +++ b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/code_review_local_G07_0.log @@ -0,0 +1,138 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> Fill implementation-owned sections and real verification output before reporting ready for review. +> Do not ask the user directly; record user-only blockers in `사용자 리뷰 요청`. + +## 개요 + +date=2026-06-10 +task=m-node-multi-target-serving-foundation/05+03,04_edge_owned_options, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `edge-owned-options`: context window와 runner sizing 옵션은 Edge config가 최종 소유하며, 외부 agent request의 `options.num_ctx` 같은 값이 Edge-owned 값을 변경하지 못한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +request option이 route/profile owned option을 덮지 못하는지 테스트와 구현을 같이 확인한다. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Edge-Owned Runtime Options | [x] | + +## 구현 체크리스트 + +- [x] Edge route/profile config의 context/runner sizing 옵션이 request option보다 우선한다. +- [x] OpenAI chat/responses에서 request `options.num_ctx` 등 runner sizing override가 내부 profile 값을 바꾸지 못한다. +- [x] provider별 option merge 정책을 테스트로 고정한다. +- [x] 최종 검증 명령을 실행한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +- [x] `코드리뷰 결과`에 판정을 append한다. +- [x] active `CODE_REVIEW-local-G07.md`를 `code_review_local_G07_N.log`로 아카이브한다. +- [x] active `PLAN-local-G07.md`를 `plan_local_G07_M.log`로 아카이브한다. +- [x] PASS이면 `complete.log` 작성 후 archive 이동한다. +- [ ] WARN/FAIL이면 후속 상태를 작성한다. + +## 계획 대비 변경 사항 + +- `apps/edge/internal/openai/chat_handler.go`, `responses_handler.go`: 계획에는 수정 대상으로 포함되어 있으나 실제 변경 없음. chat handler는 이미 request options를 그대로 service로 전달하고 있어 정책에 맞음. responses handler는 options 필드가 없어 `num_ctx` 경유 불가. 두 파일 모두 추가 변경이 필요 없음을 확인. +- `packages/go/config/config.go`: 수정 없음. `OllamaInstanceConf.ContextSize` 및 `OllamaConf.ContextSize` 필드가 이미 Edge-owned canonical source 역할을 충분히 수행함. +- `agent-ops/rules/project/domain/node/rules.md`: 계획에 없었으나 코드와 규칙 일관성을 위해 Ollama context_size 정책 설명 수정. + +## 주요 설계 결정 + +- 정책 집행 위치: Edge handler 레벨이 아닌 node adapter 레벨(`ollamaOptionsFromInput`)에서 적용. Edge는 route당 context_size를 갖지 않으므로 Edge에서 강제할 방법이 없음. Adapter는 생성 시 config의 `contextSize`를 이미 보유함. +- 강제 정책: `if _, hasNumCtx := options["num_ctx"]; !hasNumCtx` 조건 제거 → `options["num_ctx"] = contextSize`로 무조건 덮어쓰기. `contextSize == 0`이면 할당 자체를 skip하므로 미설정 adapter는 기존 동작 유지. +- 테스트 전략: `ollama_test.go`에서 adapter-level 강제 검증(실제 HTTP mock), `server_test.go`에서 edge passthrough 검증(fakeRunService)으로 계층 분리. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- request `options.num_ctx`가 profile context size보다 우선하지 않는다. +- chat/responses 양쪽 경로가 같은 정책을 따른다. +- allow-pass-through option과 forced-owned option이 코드에서 구분된다. + +## 검증 결과 + +### API-1 중간 검증 + +```bash +$ go test -count=1 ./apps/edge/internal/openai/... ./apps/node/internal/adapters/ollama/... +ok iop/apps/edge/internal/openai 1.508s +ok iop/apps/node/internal/adapters/ollama 0.007s +``` + +### 최종 검증 + +```bash +$ go test -count=1 ./apps/edge/... ./apps/node/... +ok iop/apps/edge/cmd/edge 0.057s +ok iop/apps/edge/internal/bootstrap 0.027s +ok iop/apps/edge/internal/controlplane 4.461s +ok iop/apps/edge/internal/edgecmd 0.014s +ok iop/apps/edge/internal/events 0.004s +ok iop/apps/edge/internal/input 0.009s +ok iop/apps/edge/internal/input/a2a 0.008s +ok iop/apps/edge/internal/node 0.006s +ok iop/apps/edge/internal/openai 1.511s +ok iop/apps/edge/internal/opsconsole 0.009s +FAIL iop/apps/edge/internal/service (pre-existing: TestListNodeSnapshotsWithConfig) +FAIL iop/apps/edge/internal/transport (pre-existing: TestBuildConfigPayload_OllamaVllmOneof, TestEdgeServerIntegration) +ok iop/apps/node/cmd/node 0.012s +ok iop/apps/node/internal/adapters 0.009s +ok iop/apps/node/internal/adapters/cli 42.085s +ok iop/apps/node/internal/adapters/cli/status 39.765s +ok iop/apps/node/internal/adapters/ollama 0.009s +ok iop/apps/node/internal/adapters/vllm 0.007s +ok iop/apps/node/internal/bootstrap 0.261s +ok iop/apps/node/internal/node 0.014s +ok iop/apps/node/internal/router 0.005s +ok iop/apps/node/internal/store 0.044s +ok iop/apps/node/internal/terminal 0.435s +ok iop/apps/node/internal/transport 5.135s + +※ service/transport 실패는 이 작업 이전부터 존재하는 사전 실패 (git stash 확인). + 이 작업 대상 패키지(openai, ollama) 전체 PASS. +``` + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: 없음 +- 검증 메모: + - 리뷰 중 `go test -count=1 ./apps/edge/internal/openai/... ./apps/node/internal/adapters/ollama/...`를 재실행했고 통과했다. + - 리뷰 중 `go test -count=1 ./apps/edge/... ./apps/node/...`를 재실행했으며, 보고된 기존 실패와 같은 `apps/edge/internal/service`의 `TestListNodeSnapshotsWithConfig`, `apps/edge/internal/transport`의 `TestBuildConfigPayload_OllamaVllmOneof`, `TestEdgeServerIntegration` 실패를 확인했다. 실패 지점은 이번 diff의 `options.num_ctx` 강제 정책과 직접 연결되지 않는 기존 single-instance adapter fixture 계약이다. + - 리뷰 중 stale README 설명을 `apps/node/README.md`에서 비동작 문서 정리로 수정했다. +- 다음 단계: PASS이므로 `complete.log` 작성 후 task directory를 archive로 이동한다. diff --git a/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/complete.log b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/complete.log new file mode 100644 index 0000000..628d34d --- /dev/null +++ b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/complete.log @@ -0,0 +1,41 @@ +# Complete - m-node-multi-target-serving-foundation/05+03,04_edge_owned_options + +## 완료 일시 + +2026-06-11 + +## 요약 + +Edge-owned Ollama `context_size`가 request `options.num_ctx`보다 우선하도록 adapter 정책과 테스트를 정리했고, 1회차 리뷰에서 PASS로 종료했다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G07_0.log` | `code_review_local_G07_0.log` | PASS | `num_ctx` override 방지 구현과 대상 패키지 검증 통과를 확인했다. | + +## 구현/정리 내용 + +- `apps/node/internal/adapters/ollama/ollama.go`에서 `context_size > 0`인 경우 request `options.num_ctx`를 항상 Edge-owned 값으로 덮어쓰게 했다. +- `apps/node/internal/adapters/ollama/ollama_test.go`와 `apps/edge/internal/openai/server_test.go`에 request override가 profile/context 정책을 바꾸지 못하는 회귀 테스트를 추가했다. +- `agent-ops/rules/project/domain/node/rules.md`와 `apps/node/README.md`의 Ollama context ownership 설명을 현재 정책에 맞게 정리했다. + +## 최종 검증 + +- `go test -count=1 ./apps/edge/internal/openai/... ./apps/node/internal/adapters/ollama/...` - PASS; `iop/apps/edge/internal/openai`와 `iop/apps/node/internal/adapters/ollama` 모두 통과했다. +- `go test -count=1 ./apps/edge/... ./apps/node/...` - FAIL; `apps/edge/internal/service`의 `TestListNodeSnapshotsWithConfig`, `apps/edge/internal/transport`의 `TestBuildConfigPayload_OllamaVllmOneof`, `TestEdgeServerIntegration` 실패가 재현됐다. 실패 지점은 이번 diff의 `options.num_ctx` 강제 정책과 직접 연결되지 않는 기존 single-instance adapter fixture 계약이다. + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Completed task ids: + - `edge-owned-options`: PASS; evidence=`plan_local_G07_0.log`, `code_review_local_G07_0.log`; verification=`go test -count=1 ./apps/edge/internal/openai/... ./apps/node/internal/adapters/ollama/...` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/PLAN-local-G07.md b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/plan_local_G07_0.log similarity index 100% rename from agent-task/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/PLAN-local-G07.md rename to agent-task/archive/2026/06/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/plan_local_G07_0.log diff --git a/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/code_review_local_G07_0.log b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/code_review_local_G07_0.log new file mode 100644 index 0000000..49fe200 --- /dev/null +++ b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/code_review_local_G07_0.log @@ -0,0 +1,139 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> Fill implementation-owned sections and verification output, then report ready for review. +> Do not ask the user directly; record user-only blockers in `사용자 리뷰 요청`. + +## 개요 + +date=2026-06-10 +task=m-node-multi-target-serving-foundation/06+02,04_commands_caps, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `commands-caps`: `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 여러 adapter/profile/model target에서 모호하지 않은 결과를 반환한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +command/capability 결과가 adapter instance와 target namespace를 잃지 않는지 확인한다. + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Command Identity Contract | [x] | + +## 구현 체크리스트 + +- [x] capabilities 응답이 adapter instance/profile/model target identity를 모호하지 않게 포함한다. +- [x] session/usage/provider command가 target namespace를 정확히 보존한다. +- [x] Edge service command DTO와 Node command handling 테스트를 갱신한다. +- [x] 최종 검증 명령을 실행한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +- [x] `코드리뷰 결과`에 판정을 append한다. +- [x] 판정과 차원별 평가, Required 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-local-G07.md`를 `code_review_local_G07_N.log`로 아카이브한다. +- [x] active `PLAN-local-G07.md`를 `plan_local_G07_M.log`로 아카이브한다. +- [x] `.gitignore`가 생성된 task `.md`/`.log` artifacts를 ignore하지 않는지 확인한다. +- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다. +- [x] WARN/FAIL이면 후속 상태를 작성한다. + +## 계획 대비 변경 사항 + +- `proto/iop/runtime.proto`: 변경 없음. 기존 proto가 `adapter`, `target`, `session_id`를 이미 충분히 보유하여 신규 필드 불필요. +- `apps/node/internal/runtime/types.go`: 변경 없음. `Capabilities.InstanceKey`, `CommandRequest/Response.Adapter/Target/SessionID`가 이미 존재. +- `apps/node/internal/node/node.go`: 변경 없음. `handleCapabilitiesCommand`, `toDomainCommandRequest`, `toProtoCommandResponse`가 이미 정확히 identity를 보존. +- `apps/node/internal/adapters/cli/cli.go`: 변경 없음. `handleSessionList`가 per-session `mode/target/session_id` 구조체를 이미 반환, `handleUsageStatus`가 adapter/target/session을 응답에 이미 보존. +- `apps/edge/internal/service/node_command.go`: `UsageStatusView`에 `Adapter` 필드 추가 + `UsageStatus()` 반환 시 `commandReq.GetAdapter()` 채움. (`NodeCommandView`는 이미 Adapter 있었으나 `UsageStatusView`만 누락). + +## 주요 설계 결정 + +- **코드 변경 최소화**: G07/G08 이전 작업에서 capabilities/session/usage identity 처리가 이미 구현됨을 확인. 핵심 gap은 `UsageStatusView.Adapter` 누락뿐. +- **`UsageStatusView.Adapter` 추가**: `NodeCommandView`와 일관성을 맞추기 위해 추가. multi-adapter 환경에서 caller가 어느 어댑터의 usage 상태인지 명확히 알 수 있음. +- **`TestListNodeSnapshotsWithConfig` 버그 수정**: `Ollama` (legacy single-instance) 필드 대신 `OllamaInstances` 사용. `BuildConfigPayload` 주석("callers that bypass LoadEdge must populate OllamaInstances manually")과 일치. +- **신규 테스트 추가**: + - `TestOnCommandRequest_MultiAdapterCapabilities` (node_test.go): 두 instance key 어댑터 등록 후 각각의 CAPABILITIES가 올바른 `instance_key`를 반환하는지 검증. + - `TestUsageStatusPreservesAdapterIdentity` (service_test.go): pipe 기반 mock node로 `UsageStatusView.Adapter/Target/SessionID` identity 보존 검증. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- capabilities/session/usage가 동일 target 이름 충돌에서 모호하지 않다. +- Edge command DTO와 proto/runtime DTO가 같은 identity를 전달한다. +- long command 처리와 concurrency scheduling 범위가 이 plan을 넘어 확장되지 않았다. + +## 검증 결과 + +### API-1 중간 검증 + +``` +ok iop/apps/node/cmd/node 0.015s +ok iop/apps/node/internal/adapters 0.013s +ok iop/apps/node/internal/adapters/cli 42.120s +ok iop/apps/node/internal/adapters/cli/status 39.766s +ok iop/apps/node/internal/adapters/ollama 0.010s +ok iop/apps/node/internal/adapters/vllm 0.013s +ok iop/apps/node/internal/bootstrap 0.267s +ok iop/apps/node/internal/node 0.027s +ok iop/apps/node/internal/router 0.012s +ok iop/apps/node/internal/store 0.052s +ok iop/apps/node/internal/terminal 0.441s +ok iop/apps/node/internal/transport 5.149s +ok iop/apps/edge/internal/service 0.005s +``` + +### 최종 검증 + +``` +ok iop/apps/node/cmd/node 0.015s +ok iop/apps/node/internal/adapters 0.013s +ok iop/apps/node/internal/adapters/cli 42.120s +ok iop/apps/node/internal/adapters/cli/status 39.766s +ok iop/apps/node/internal/adapters/ollama 0.010s +ok iop/apps/node/internal/adapters/vllm 0.013s +ok iop/apps/node/internal/bootstrap 0.267s +ok iop/apps/node/internal/node 0.027s +ok iop/apps/node/internal/router 0.012s +ok iop/apps/node/internal/store 0.052s +ok iop/apps/node/internal/terminal 0.441s +ok iop/apps/node/internal/transport 5.149s +ok iop/apps/edge/internal/service 0.005s +``` + +## 코드리뷰 결과 + +- 종합 판정: FAIL +- 차원별 평가: + - Correctness: Fail + - Completeness: Fail + - Test coverage: Fail + - API contract: Fail + - Code quality: Pass + - Plan deviation: Fail + - Verification trust: Pass +- 발견된 문제: + - Required: `apps/edge/internal/openai/ollama_passthrough.go:21`에서 `/api/*` Ollama passthrough가 `OllamaAPIRequest.Target`을 채우지 않는다. 결과적으로 `apps/edge/internal/service/node_command.go:153`의 `NodeCommandRequest.Target`도 빈 문자열이 되어, configured `openai.target` 또는 route target이 있는 multi-target 환경에서 provider passthrough command가 어느 model target에 대한 명령인지 보존하지 못한다. `handleOllamaAPI`가 `resolveRouteDispatch` 또는 적어도 `resolveTarget` 기준으로 target을 채우고, `TestOllamaAPIPassthrough`에 target 보존 assertion을 추가한다. + - Required: `apps/edge/internal/opsconsole/status.go:44`가 새로 추가된 `UsageStatusView.Adapter`를 버리고 `FormatUsageStatus`는 `target/session`만 출력한다. `/status`는 `SendStatus`의 sent line 이후 실제 status result header에서 adapter identity를 잃으므로 같은 target/session을 여러 adapter instance가 공유하는 경우 결과가 모호하다. `FormatUsageStatus`와 호출부를 adapter 포함 형태로 바꾸고, opsconsole formatter 테스트에 `adapter=...` 출력 assertion을 추가한다. +- 다음 단계: + - FAIL 후속: `PLAN-local-G08.md` / `CODE_REVIEW-local-G08.md`에서 passthrough target 보존과 usage status 출력 identity를 보완한다. diff --git a/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/code_review_local_G08_1.log b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/code_review_local_G08_1.log new file mode 100644 index 0000000..714e32a --- /dev/null +++ b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/code_review_local_G08_1.log @@ -0,0 +1,204 @@ + + +# Code Review Reference - REVIEW_API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-11 +task=m-node-multi-target-serving-foundation/06+02,04_commands_caps, plan=1, tag=REVIEW_API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `commands-caps`: `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 여러 adapter/profile/model target에서 모호하지 않은 결과를 반환한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G08.md` → `code_review_local_G08_N.log`, `PLAN-local-G08.md` → `plan_local_G08_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-node-multi-target-serving-foundation/06+02,04_commands_caps/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. PASS이고 task group이 `m-node-multi-target-serving-foundation`이면 완료 이벤트 메타데이터를 보고한다. roadmap 수정이나 `update-roadmap` 직접 호출은 하지 않는다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REVIEW_API-1] Ollama Passthrough Target Identity | [x] | +| [REVIEW_API-2] Usage Status Console Identity | [x] | + +## 구현 체크리스트 + +- [x] `/api/*` Ollama passthrough가 resolved target identity를 `OllamaAPIRequest.Target`과 `NodeCommandRequest.Target`까지 보존한다. +- [x] usage status console result header가 adapter/target/session identity를 함께 표시하고 테스트가 이를 검증한다. +- [x] 관련 edge/openai/service/opsconsole 테스트를 갱신한다. +- [x] 최종 검증 명령을 실행한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-local-G08.md`를 `code_review_local_G08_N.log`로 아카이브한다. +- [x] active `PLAN-local-G08.md`를 `plan_local_G08_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-node-multi-target-serving-foundation/06+02,04_commands_caps/`를 `agent-task/archive/YYYY/MM/m-node-multi-target-serving-foundation/06+02,04_commands_caps/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-node-multi-target-serving-foundation/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +없음. 계획에서 지정한 수정 파일과 테스트 방향을 그대로 따랐다. + +## 주요 설계 결정 + +- `FormatUsageStatus` 서명에 `adapterName` 파라미터를 `nodeAlias` 바로 다음에 삽입했다. header 출력 순서는 `adapter=` → `target=` → `session=`으로 identity 계층(어느 adapter → 어느 model → 어느 session) 순서를 따른다. +- `resolveTarget("")`을 passthrough에 사용했다. `/api/*` 경로에는 request body에서 모델을 파싱할 수 없으므로 빈 문자열을 전달하면 configured target이 있을 때만 채워지고, 없으면 빈 값을 유지해 기존 동작을 깨지 않는다. +- `events_test.go` 기존 테스트에서 `TestFormatUsageStatus_PrintsParsedLimits`에 header assertion(`adapter=cli@local target=codex session=default` 포함 확인)을 추가했다. 나머지 테스트는 adapter 파라미터만 추가하고 기존 assertions는 유지했다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `/api/*` passthrough가 configured OpenAI target을 `OllamaAPIRequest.Target`과 service-built `NodeCommandRequest.Target`까지 보존한다. +- usage status console header가 adapter/target/session을 모두 표시한다. +- 새 테스트가 실패 재현과 수정 검증을 모두 포함한다. +- 범위 밖 Ollama context_size 정책 변경이나 concurrency scheduling 변경이 섞이지 않았다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. + +### REVIEW_API-1 중간 검증 + +```bash +$ go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service +ok iop/apps/edge/internal/openai 1.508s +ok iop/apps/edge/internal/service 0.006s +``` + +### REVIEW_API-2 중간 검증 + +```bash +$ go test -count=1 ./apps/edge/internal/opsconsole +ok iop/apps/edge/internal/opsconsole 0.006s +``` + +### 최종 검증 + +```bash +$ go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/edge/internal/opsconsole +ok iop/apps/edge/internal/openai 1.507s +ok iop/apps/edge/internal/service 0.005s +ok iop/apps/edge/internal/opsconsole 0.006s + +$ go test -count=1 ./apps/node/... ./apps/edge/internal/service/... +ok iop/apps/node/cmd/node 0.011s +ok iop/apps/node/internal/adapters 0.010s +ok iop/apps/node/internal/adapters/cli 42.209s +? iop/apps/node/internal/adapters/cli/internal/testutil [no test files] +ok iop/apps/node/internal/adapters/cli/status 39.777s +? iop/apps/node/internal/adapters/mock [no test files] +ok iop/apps/node/internal/adapters/ollama 0.009s +ok iop/apps/node/internal/adapters/vllm 0.009s +ok iop/apps/node/internal/bootstrap 0.266s +ok iop/apps/node/internal/node 0.020s +ok iop/apps/node/internal/router 0.009s +? iop/apps/node/internal/runtime [no test files] +ok iop/apps/node/internal/store 0.040s +ok iop/apps/node/internal/terminal 0.450s +ok iop/apps/node/internal/transport 5.152s +ok iop/apps/edge/internal/service 0.006s + +$ ./scripts/e2e-smoke.sh +[e2e] NOTE: auxiliary smoke only; completion requires scripts/dev/edge.sh + scripts/dev/node.sh user-flow verification. +[e2e] shellcheck not found, skipping +[e2e] prompt templates: first=bye-short second=yes-short background=hello-basic base=9 +[e2e] preparing honest mock smoke test (using scripted cli adapter)... +[e2e] starting smoke test (profile: mock, port: 30195, persistent: 1, has_status: 0) +[e2e] waiting for node registration (timeout: 60s) +[e2e] > /nodes +[e2e] > /capabilities +[e2e] > /transport +[e2e] > (run tests omitted for brevity) +[e2e] Auxiliary smoke test PASSED. +[e2e] Completion still requires scripts/dev/edge.sh + scripts/dev/node.sh user-flow verification. +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## Ownership + +| 섹션 | 소유자 | 설명 | +|------|--------|------| +| 헤더 주석, 개요(date/task/plan/tag), 리뷰 에이전트 지시 | 스텁 생성 시 고정 | 구현 에이전트가 수정하거나 실행하지 않음 | +| Roadmap Targets | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 수정하지 않음; PASS 시 code-review가 `complete.log`의 `Roadmap Completion`으로 복사 | +| 구현 항목별 완료 여부 (항목명) | 스텁 생성 시 고정 | `[ ]` → `[x]` 체크만 구현 에이전트가 수행 | +| 구현 체크리스트 (항목 텍스트/순서) | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 `[ ]` → `[x]` 체크만 수행; 마지막 체크박스는 저장 전 필수 | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section | +| 계획 대비 변경 사항, 주요 설계 결정 | 구현 에이전트가 채움 | placeholder 텍스트를 실제 내용으로 교체 | +| 사용자 리뷰 요청 | 구현 에이전트가 채움 | 진행에 사용자 입력이 필요하지 않으면 `상태: 없음` 유지 | +| 리뷰어를 위한 체크포인트 | 스텁 생성 시 고정 | 계획에서 추출한 리뷰 포인트 | +| 검증 결과 (섹션 제목 + 명령) | 스텁 생성 시 고정 | 실행 출력만 구현 에이전트가 채움 | +| 코드리뷰 결과 | 리뷰 에이전트가 append | 스텁에 포함하지 않음 | + +## 코드리뷰 결과 + +- 종합 판정: FAIL +- 차원별 평가: + - Correctness: Fail + - Completeness: Fail + - Test coverage: Fail + - API contract: Fail + - Code quality: Pass + - Plan deviation: Warn + - Verification trust: Pass +- 발견된 문제: + - Required: `apps/edge/internal/opsconsole/status.go:89`의 `FormatNodeCommandView`가 `NodeCommandView.Adapter`를 출력하지 않아 `/capabilities`, `/sessions`, `/transport` 결과 header가 여전히 `target/session`만 표시한다. 실제 `./scripts/e2e-smoke.sh` 출력도 `[node0-capabilities] target=fake-cli session=default`, `[node0-sessions] target=fake-cli session=session2` 형태라, usage status 외 command 결과는 같은 target/session을 여러 adapter instance가 공유할 때 모호하다. `FormatNodeCommandView` header에 `adapter=%s`를 포함하고 `console_test.go`의 capabilities/session/transport assertions를 갱신한다. +- 다음 단계: + - FAIL 후속: `PLAN-local-G08.md` / `CODE_REVIEW-local-G08.md`에서 non-usage node command formatter의 adapter identity 표시와 테스트를 보완한다. diff --git a/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/code_review_local_G08_2.log b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/code_review_local_G08_2.log new file mode 100644 index 0000000..9730981 --- /dev/null +++ b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/code_review_local_G08_2.log @@ -0,0 +1,169 @@ + + +# Code Review Reference - REVIEW_REVIEW_API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-11 +task=m-node-multi-target-serving-foundation/06+02,04_commands_caps, plan=2, tag=REVIEW_REVIEW_API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `commands-caps`: `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 여러 adapter/profile/model target에서 모호하지 않은 결과를 반환한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G08.md` → `code_review_local_G08_N.log`, `PLAN-local-G08.md` → `plan_local_G08_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-node-multi-target-serving-foundation/06+02,04_commands_caps/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. PASS이고 task group이 `m-node-multi-target-serving-foundation`이면 완료 이벤트 메타데이터를 보고한다. roadmap 수정이나 `update-roadmap` 직접 호출은 하지 않는다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목                          | 완료 여부 | +| ---------------------------------------------------------| -----------| +| [REVIEW_REVIEW_API-1] Non-Usage Command Header Identity | [x]    | + +## 구현 체크리스트 + +- [x] `FormatNodeCommandView`가 capabilities/session/transport 결과 header에 adapter/target/session identity를 함께 표시한다. +- [x] capabilities, structured sessions, transport formatter 테스트가 adapter header를 검증한다. +- [x] 최종 검증 명령을 실행한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-local-G08.md`를 `code_review_local_G08_N.log`로 아카이브한다. +- [x] active `PLAN-local-G08.md`를 `plan_local_G08_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-node-multi-target-serving-foundation/06+02,04_commands_caps/`를 `agent-task/archive/YYYY/MM/m-node-multi-target-serving-foundation/06+02,04_commands_caps/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-node-multi-target-serving-foundation/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +없음. 계획에서 지정한 수정 파일과 테스트 방향을 그대로 따랐다. + +## 주요 설계 결정 + +- `FormatNodeCommandView` header 출력 순서를 `adapter=` → `target=` → `session=`으로 통일했다. `FormatUsageStatus`와 동일한 순서로 모든 command 결과 header의 identity 표시 방식을 일관되게 맞췄다. +- `TestFormatNodeCommandView_RendersTransportStatusKeys`에 header assertion을 추가했다. 계획에 명시된 3개 테스트(capabilities, structured sessions, transport) 모두 adapter header를 검증한다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `FormatNodeCommandView` header가 adapter/target/session을 모두 표시한다. +- capabilities, sessions, transport formatter tests가 adapter header를 검증한다. +- `USAGE_STATUS`, provider passthrough, Ollama context 정책 변경이 새로 섞이지 않았다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. + +### REVIEW_REVIEW_API-1 중간 검증 + +```bash +$ go test -count=1 ./apps/edge/internal/opsconsole +ok iop/apps/edge/internal/opsconsole 0.005s +``` + +### 최종 검증 + +```bash +$ go test -count=1 ./apps/edge/internal/opsconsole +ok iop/apps/edge/internal/opsconsole 0.005s + +$ ./scripts/e2e-smoke.sh +[e2e] NOTE: auxiliary smoke only; completion requires scripts/dev/edge.sh + scripts/dev/node.sh user-flow verification. +[e2e] > /capabilities +[e2e] > /transport +[e2e] > /sessions +edge> [node0-capabilities] adapter=cli target=fake-cli session=default +edge> [node0-transport] adapter=cli target=fake-cli session=default +edge> [node0-sessions] adapter=cli target=fake-cli session=session2 +sessions: 2 +[e2e] Auxiliary smoke test PASSED. +[e2e] Completion still requires scripts/dev/edge.sh + scripts/dev/node.sh user-flow verification. +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## Ownership + +| 섹션                          | 소유자                   | 설명                                                | +| ---------------------------------------------------------| ---------------------------------------------| -----------------------------------------------------------------------------------------------------| +| 헤더 주석, 개요(date/task/plan/tag), 리뷰 에이전트 지시 | 스텁 생성 시 고정              | 구현 에이전트가 수정하거나 실행하지 않음                              | +| Roadmap Targets                     | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 수정하지 않음; PASS 시 code-review가 `complete.log`의 `Roadmap Completion`으로 복사 | +| 구현 항목별 완료 여부 (항목명)             | 스텁 생성 시 고정              | `[ ]` → `[x]` 체크만 구현 에이전트가 수행                              | +| 구현 체크리스트 (항목 텍스트/순서)           | follow-up plan에서 복사해 스텁 생성 시 고정 | 구현 에이전트가 `[ ]` → `[x]` 체크만 수행; 마지막 체크박스는 저장 전 필수              | +| 코드리뷰 전용 체크리스트                | Review agent only              | Implementing agent must not modify or check this section                      | +| 계획 대비 변경 사항, 주요 설계 결정           | 구현 에이전트가 채움            | placeholder 텍스트를 실제 내용으로 교체                               | +| 사용자 리뷰 요청                    | 구현 에이전트가 채움            | 진행에 사용자 입력이 필요하지 않으면 `상태: 없음` 유지                       | +| 리뷰어를 위한 체크포인트                | 스텁 생성 시 고정              | 계획에서 추출한 리뷰 포인트                                     | +| 검증 결과 (섹션 제목 + 명령)              | 스텁 생성 시 고정              | 실행 출력만 구현 에이전트가 채움                                  | +| 코드리뷰 결과                      | 리뷰 에이전트가 append           | 스텁에 포함하지 않음                                        | + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - Correctness: Pass + - Completeness: Pass + - Test coverage: Pass + - API contract: Pass + - Code quality: Pass + - Plan deviation: Pass + - Verification trust: Pass +- 발견된 문제: 없음 +- 다음 단계: + - PASS: `complete.log` 작성 후 task directory를 archive로 이동한다. diff --git a/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/complete.log b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/complete.log new file mode 100644 index 0000000..e5098e5 --- /dev/null +++ b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/complete.log @@ -0,0 +1,45 @@ +# Complete - m-node-multi-target-serving-foundation/06+02,04_commands_caps + +## 완료 일시 + +2026-06-11 + +## 요약 + +`commands-caps` command identity 보완 작업을 3회 리뷰 루프 끝에 PASS로 종료했다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G07_0.log` | `code_review_local_G07_0.log` | FAIL | Ollama passthrough target 누락과 usage status adapter 표시 누락 발견 | +| `plan_local_G08_1.log` | `code_review_local_G08_1.log` | FAIL | non-usage command formatter가 adapter header를 표시하지 않는 문제 발견 | +| `plan_local_G08_2.log` | `code_review_local_G08_2.log` | PASS | capabilities, sessions, transport, usage status, provider passthrough command identity 보존 확인 | + +## 구현/정리 내용 + +- `/api/*` Ollama passthrough가 configured OpenAI target을 `OllamaAPIRequest.Target`과 service-built `NodeCommandRequest.Target`까지 보존하도록 했다. +- `UsageStatusView`와 ops console `/status` 출력이 adapter/target/session identity를 함께 표시하도록 했다. +- `FormatNodeCommandView`가 `/capabilities`, `/sessions`, `/transport` 결과 header에 adapter/target/session identity를 모두 표시하도록 했다. +- 관련 openai/service/opsconsole/node 테스트를 추가 또는 갱신했다. + +## 최종 검증 + +- `go test -count=1 ./apps/edge/internal/opsconsole` - PASS; `ok iop/apps/edge/internal/opsconsole`. +- `./scripts/e2e-smoke.sh` - PASS; `/capabilities`, `/transport`, `/sessions` 출력에서 `adapter=cli target=fake-cli session=...` header 확인. +- 이전 루프 검증: `go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/edge/internal/opsconsole` PASS, `go test -count=1 ./apps/node/... ./apps/edge/internal/service/...` PASS. + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Completed task ids: + - `commands-caps`: PASS; evidence=`plan_local_G08_2.log`, `code_review_local_G08_2.log`; verification=`go test -count=1 ./apps/edge/internal/opsconsole`, `./scripts/e2e-smoke.sh` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/m-node-multi-target-serving-foundation/06+02,04_commands_caps/PLAN-local-G07.md b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/plan_local_G07_0.log similarity index 100% rename from agent-task/m-node-multi-target-serving-foundation/06+02,04_commands_caps/PLAN-local-G07.md rename to agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/plan_local_G07_0.log diff --git a/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/plan_local_G08_1.log b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/plan_local_G08_1.log new file mode 100644 index 0000000..0ab1eed --- /dev/null +++ b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/plan_local_G08_1.log @@ -0,0 +1,73 @@ + + +# Plan - REVIEW_API + +## 이 파일을 읽는 구현 에이전트에게 + +이 plan은 직전 코드리뷰 `code_review_local_G07_0.log`의 Required 지적만 해결한다. 구현 중 사용자에게 직접 묻지 말고, 사용자 전용 blocker가 있으면 `CODE_REVIEW-local-G08.md`의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 기록한 뒤 멈춘다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `commands-caps`: `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 여러 adapter/profile/model target에서 모호하지 않은 결과를 반환한다. +- Completion mode: check-on-pass + +## 배경 + +직전 구현은 service DTO 일부에 identity를 추가했지만, 두 표면에서 여전히 command result identity가 손실된다. + +- OpenAI-compatible `/api/*` Ollama passthrough가 target을 채우지 않아 provider command가 어느 model target에 대한 것인지 보존하지 못한다. +- ops console `/status` 결과 header가 adapter를 표시하지 않아 usage status 결과가 같은 target/session 조합에서 모호하다. + +## 범위 결정 근거 + +- proto/runtime 구조 변경은 하지 않는다. 기존 `OllamaAPIRequest.Target`, `NodeCommandRequest.Target`, `UsageStatusView.Adapter`를 끝까지 사용하는 보완이다. +- Ollama `context_size` 우선순위 변경과 node domain rule 변경은 이 follow-up 범위가 아니다. +- concurrency scheduling, long command 비동기화, provider API body 재작성은 별도 task 범위다. + +## 구현 체크리스트 + +- [ ] `/api/*` Ollama passthrough가 resolved target identity를 `OllamaAPIRequest.Target`과 `NodeCommandRequest.Target`까지 보존한다. +- [ ] usage status console result header가 adapter/target/session identity를 함께 표시하고 테스트가 이를 검증한다. +- [ ] 관련 edge/openai/service/opsconsole 테스트를 갱신한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [REVIEW_API-1] Ollama Passthrough Target Identity + +- 문제: `apps/edge/internal/openai/ollama_passthrough.go:21`이 `OllamaAPIRequest.Target`을 채우지 않는다. 그 결과 `apps/edge/internal/service/node_command.go:153`의 provider passthrough `NodeCommandRequest.Target`도 빈 값이 된다. +- 해결 방법: `/api/*` passthrough에서도 OpenAI server의 configured target을 resolved target으로 전달한다. configured target이 비어 있는 legacy passthrough에서는 빈 target을 유지해 기존 동작을 깨지 않는다. route catalog 기반 target을 적용할 수 있는 입력 정보가 없으면 이 plan에서는 configured `openai.target` 보존까지만 다룬다. +- 수정 파일: + - `apps/edge/internal/openai/ollama_passthrough.go` + - `apps/edge/internal/openai/server_test.go` + - 필요 시 `apps/edge/internal/service/service_test.go` +- 테스트 작성: + - `TestOllamaAPIPassthrough` 또는 신규 테스트에서 `EdgeOpenAIConf{Adapter: "ollama", Target: "gemma4:26b"}`일 때 fake service가 받은 `OllamaAPIRequest.Target == "gemma4:26b"`인지 검증한다. + - service level 테스트가 부족하면 `OllamaAPI`가 `NodeCommandRequest.Target`을 보존하는 assertion을 추가한다. +- 중간 검증: + - `go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service` + +### [REVIEW_API-2] Usage Status Console Identity + +- 문제: `apps/edge/internal/opsconsole/status.go:44`가 `UsageStatusView.Adapter`를 버리고, `FormatUsageStatus` header는 `target/session`만 표시한다. +- 해결 방법: `FormatUsageStatus` 호출부와 signature를 adapter 포함 형태로 바꾸고 header를 예를 들어 `[node-status] adapter=cli@dgx target=claude session=default`처럼 출력한다. +- 수정 파일: + - `apps/edge/internal/opsconsole/status.go` + - `apps/edge/internal/opsconsole/events_test.go` + - 필요 시 `apps/edge/internal/opsconsole/console_test.go` +- 테스트 작성: + - 기존 `FormatUsageStatus` 테스트 중 최소 1개 이상에서 header에 `adapter=... target=... session=...`이 포함되는지 검증한다. + - `SendStatus`가 `result.Adapter`를 renderer에 전달하는 경로를 단위 테스트로 확인할 수 있으면 추가한다. +- 중간 검증: + - `go test -count=1 ./apps/edge/internal/opsconsole` + +## 최종 검증 + +```bash +go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/edge/internal/opsconsole +go test -count=1 ./apps/node/... ./apps/edge/internal/service/... +``` + +edge service/console/HTTP command 표면에 닿으므로 로컬 smoke가 가능하면 `./scripts/e2e-smoke.sh`로 command 응답을 확인한다. 실행하지 못하면 `CODE_REVIEW-local-G08.md`의 검증 결과에 정확한 미실행 사유를 남긴다. + diff --git a/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/plan_local_G08_2.log b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/plan_local_G08_2.log new file mode 100644 index 0000000..ac06ce9 --- /dev/null +++ b/agent-task/archive/2026/06/m-node-multi-target-serving-foundation/06+02,04_commands_caps/plan_local_G08_2.log @@ -0,0 +1,62 @@ + + +# Plan - REVIEW_REVIEW_API + +## 이 파일을 읽는 구현 에이전트에게 + +이 plan은 `code_review_local_G08_1.log`의 Required 지적만 해결한다. 구현 중 사용자에게 직접 묻지 말고, 사용자 전용 blocker가 있으면 `CODE_REVIEW-local-G08.md`의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 기록한 뒤 멈춘다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` +- Task ids: + - `commands-caps`: `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 여러 adapter/profile/model target에서 모호하지 않은 결과를 반환한다. +- Completion mode: check-on-pass + +## 배경 + +G08 구현은 `/api/*` passthrough target 보존과 `/status` header의 adapter 표시를 해결했다. 하지만 non-usage node command renderer는 여전히 `NodeCommandView.Adapter`를 버린다. + +실제 `./scripts/e2e-smoke.sh` 출력에서도 다음처럼 adapter가 header에 없다. + +```text +[node0-capabilities] target=fake-cli session=default +[node0-sessions] target=fake-cli session=session2 +``` + +## 범위 결정 근거 + +- 서비스 DTO와 proto/runtime 경계는 이미 adapter/target/session을 보존한다. 이번 작업은 ops console formatter의 표시 누락만 보완한다. +- `USAGE_STATUS`는 G08에서 이미 보완됐으므로 수정하지 않는다. +- provider passthrough, Ollama context 정책, command concurrency는 범위 밖이다. + +## 구현 체크리스트 + +- [ ] `FormatNodeCommandView`가 capabilities/session/transport 결과 header에 adapter/target/session identity를 함께 표시한다. +- [ ] capabilities, structured sessions, transport formatter 테스트가 adapter header를 검증한다. +- [ ] 최종 검증 명령을 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [REVIEW_REVIEW_API-1] Non-Usage Command Header Identity + +- 문제: `apps/edge/internal/opsconsole/status.go:89`의 `FormatNodeCommandView`가 `view.Adapter`를 출력하지 않는다. `/capabilities`, `/sessions`, `/transport` 결과가 같은 target/session을 여러 adapter instance가 공유하는 환경에서 모호하다. +- 해결 방법: `FormatNodeCommandView` header를 예를 들어 `[node0-capabilities] adapter=cli@local target=codex session=default` 형태로 바꾼다. 빈 adapter는 그대로 빈 문자열로 출력해 DTO 값을 숨기지 않는다. +- 수정 파일: + - `apps/edge/internal/opsconsole/status.go` + - `apps/edge/internal/opsconsole/console_test.go` +- 테스트 작성: + - `TestFormatNodeCommandView_RendersSortedResult`에 `Adapter: "cli@local"`을 넣고 header에 `adapter=cli@local target=codex session=default`가 있는지 검증한다. + - `TestFormatNodeCommandView_RendersStructuredSessions`에 adapter assertion을 추가한다. + - `TestFormatNodeCommandView_RendersTransportStatusKeys`도 header adapter를 검증한다. +- 중간 검증: + - `go test -count=1 ./apps/edge/internal/opsconsole` + +## 최종 검증 + +```bash +go test -count=1 ./apps/edge/internal/opsconsole +./scripts/e2e-smoke.sh +``` + +`./scripts/e2e-smoke.sh` 출력에서 `/capabilities`, `/sessions`, `/transport` 결과 header가 adapter/target/session을 모두 표시하는지 확인하고, 실제 stdout/stderr를 `CODE_REVIEW-local-G08.md`에 붙여 넣는다. + diff --git a/agent-task/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/CODE_REVIEW-local-G07.md b/agent-task/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/CODE_REVIEW-local-G07.md deleted file mode 100644 index 3c503fd..0000000 --- a/agent-task/m-node-multi-target-serving-foundation/05+03,04_edge_owned_options/CODE_REVIEW-local-G07.md +++ /dev/null @@ -1,89 +0,0 @@ - - -# Code Review Reference - API - -> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** -> Fill implementation-owned sections and real verification output before reporting ready for review. -> Do not ask the user directly; record user-only blockers in `사용자 리뷰 요청`. - -## 개요 - -date=2026-06-10 -task=m-node-multi-target-serving-foundation/05+03,04_edge_owned_options, plan=0, tag=API - -## Roadmap Targets - -- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` -- Task ids: - - `edge-owned-options`: context window와 runner sizing 옵션은 Edge config가 최종 소유하며, 외부 agent request의 `options.num_ctx` 같은 값이 Edge-owned 값을 변경하지 못한다. -- Completion mode: check-on-pass - -## 이 파일을 읽는 리뷰 에이전트에게 - -> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. - -request option이 route/profile owned option을 덮지 못하는지 테스트와 구현을 같이 확인한다. - -## 구현 항목별 완료 여부 - -| 항목 | 완료 여부 | -|------|---------| -| [API-1] Edge-Owned Runtime Options | [ ] | - -## 구현 체크리스트 - -- [ ] Edge route/profile config의 context/runner sizing 옵션이 request option보다 우선한다. -- [ ] OpenAI chat/responses에서 request `options.num_ctx` 등 runner sizing override가 내부 profile 값을 바꾸지 못한다. -- [ ] provider별 option merge 정책을 테스트로 고정한다. -- [ ] 최종 검증 명령을 실행한다. -- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. - -## 코드리뷰 전용 체크리스트 - -- [ ] `코드리뷰 결과`에 판정을 append한다. -- [ ] active `CODE_REVIEW-local-G07.md`를 `code_review_local_G07_N.log`로 아카이브한다. -- [ ] active `PLAN-local-G07.md`를 `plan_local_G07_M.log`로 아카이브한다. -- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다. -- [ ] WARN/FAIL이면 후속 상태를 작성한다. - -## 계획 대비 변경 사항 - -_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ - -## 주요 설계 결정 - -_구현 에이전트가 주요 설계 결정 사항을 기록한다._ - -## 사용자 리뷰 요청 - -_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ - -- 상태: 없음 -- 사유 유형: 없음 -- 결정 필요: 없음 -- 차단 근거: 없음 -- 실행한 검증/명령: 없음 -- 자동 후속 불가 이유: 없음 -- 재개 조건: 없음 - -## 리뷰어를 위한 체크포인트 - -- request `options.num_ctx`가 profile context size보다 우선하지 않는다. -- chat/responses 양쪽 경로가 같은 정책을 따른다. -- allow-pass-through option과 forced-owned option이 코드에서 구분된다. - -## 검증 결과 - -### API-1 중간 검증 - -```bash -$ go test -count=1 ./apps/edge/internal/openai/... ./apps/node/internal/adapters/ollama/... -(output) -``` - -### 최종 검증 - -```bash -$ go test -count=1 ./apps/edge/... ./apps/node/... -(output) -``` diff --git a/agent-task/m-node-multi-target-serving-foundation/06+02,04_commands_caps/CODE_REVIEW-local-G07.md b/agent-task/m-node-multi-target-serving-foundation/06+02,04_commands_caps/CODE_REVIEW-local-G07.md deleted file mode 100644 index 1794c71..0000000 --- a/agent-task/m-node-multi-target-serving-foundation/06+02,04_commands_caps/CODE_REVIEW-local-G07.md +++ /dev/null @@ -1,89 +0,0 @@ - - -# Code Review Reference - API - -> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** -> Fill implementation-owned sections and verification output, then report ready for review. -> Do not ask the user directly; record user-only blockers in `사용자 리뷰 요청`. - -## 개요 - -date=2026-06-10 -task=m-node-multi-target-serving-foundation/06+02,04_commands_caps, plan=0, tag=API - -## Roadmap Targets - -- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/node-multi-target-serving-foundation.md` -- Task ids: - - `commands-caps`: `CAPABILITIES`, `SESSION_LIST`, `USAGE_STATUS`, provider passthrough command가 여러 adapter/profile/model target에서 모호하지 않은 결과를 반환한다. -- Completion mode: check-on-pass - -## 이 파일을 읽는 리뷰 에이전트에게 - -> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. - -command/capability 결과가 adapter instance와 target namespace를 잃지 않는지 확인한다. - -## 구현 항목별 완료 여부 - -| 항목 | 완료 여부 | -|------|---------| -| [API-1] Command Identity Contract | [ ] | - -## 구현 체크리스트 - -- [ ] capabilities 응답이 adapter instance/profile/model target identity를 모호하지 않게 포함한다. -- [ ] session/usage/provider command가 target namespace를 정확히 보존한다. -- [ ] Edge service command DTO와 Node command handling 테스트를 갱신한다. -- [ ] 최종 검증 명령을 실행한다. -- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. - -## 코드리뷰 전용 체크리스트 - -- [ ] `코드리뷰 결과`에 판정을 append한다. -- [ ] active `CODE_REVIEW-local-G07.md`를 `code_review_local_G07_N.log`로 아카이브한다. -- [ ] active `PLAN-local-G07.md`를 `plan_local_G07_M.log`로 아카이브한다. -- [ ] PASS이면 `complete.log` 작성 후 archive 이동한다. -- [ ] WARN/FAIL이면 후속 상태를 작성한다. - -## 계획 대비 변경 사항 - -_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ - -## 주요 설계 결정 - -_구현 에이전트가 주요 설계 결정 사항을 기록한다._ - -## 사용자 리뷰 요청 - -_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ - -- 상태: 없음 -- 사유 유형: 없음 -- 결정 필요: 없음 -- 차단 근거: 없음 -- 실행한 검증/명령: 없음 -- 자동 후속 불가 이유: 없음 -- 재개 조건: 없음 - -## 리뷰어를 위한 체크포인트 - -- capabilities/session/usage가 동일 target 이름 충돌에서 모호하지 않다. -- Edge command DTO와 proto/runtime DTO가 같은 identity를 전달한다. -- long command 처리와 concurrency scheduling 범위가 이 plan을 넘어 확장되지 않았다. - -## 검증 결과 - -### API-1 중간 검증 - -```bash -$ go test -count=1 ./apps/node/... ./apps/edge/internal/service/... -(output) -``` - -### 최종 검증 - -```bash -$ go test -count=1 ./apps/node/... ./apps/edge/internal/service/... -(output) -``` diff --git a/apps/edge/internal/openai/ollama_passthrough.go b/apps/edge/internal/openai/ollama_passthrough.go index d57dea0..0cd5447 100644 --- a/apps/edge/internal/openai/ollama_passthrough.go +++ b/apps/edge/internal/openai/ollama_passthrough.go @@ -21,6 +21,7 @@ func (s *Server) handleOllamaAPI(w http.ResponseWriter, r *http.Request) { resp, err := s.service.OllamaAPI(r.Context(), edgeservice.OllamaAPIRequest{ NodeRef: s.cfg.NodeRef, Adapter: s.resolveAdapter(), + Target: s.resolveTarget(""), Method: r.Method, Path: r.URL.RequestURI(), Body: string(body), diff --git a/apps/edge/internal/openai/server_test.go b/apps/edge/internal/openai/server_test.go index bfa9881..d897618 100644 --- a/apps/edge/internal/openai/server_test.go +++ b/apps/edge/internal/openai/server_test.go @@ -426,6 +426,28 @@ func TestOllamaAPIPassthrough(t *testing.T) { } } +func TestOllamaAPIPassthroughPreservesConfiguredTarget(t *testing.T) { + fake := &fakeRunService{ + ollamaResp: edgeservice.OllamaAPIView{ + StatusCode: http.StatusOK, + ContentType: "application/json", + Body: `{"models":[]}`, + }, + } + srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "gemma4:26b", TimeoutSec: 15}, fake, nil) + req := httptest.NewRequest(http.MethodGet, "/api/tags", nil) + w := httptest.NewRecorder() + + srv.handleOllamaAPI(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status: got %d body=%s", w.Code, w.Body.String()) + } + if fake.ollamaReq.Target != "gemma4:26b" { + t.Fatalf("passthrough target: got %q, want gemma4:26b", fake.ollamaReq.Target) + } +} + func TestResponsesDispatchesNonStreamingRequest(t *testing.T) { fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)} fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello"} @@ -986,6 +1008,34 @@ func TestResponsesRouteCatalogTakesPriorityOverMetadataTarget(t *testing.T) { } } +func TestChatCompletionsEdgePassesNumCtxForAdapterEnforcement(t *testing.T) { + // Edge passes request options.num_ctx to the service unchanged. + // The Ollama adapter enforces Edge-owned context_size over this value. + fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)} + fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"} + fake.events <- &iop.RunEvent{Type: "complete"} + + srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, fake, nil) + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{ + "model":"from-request", + "messages":[{"role":"user","content":"hi"}], + "options":{"num_ctx":8192} + }`)) + w := httptest.NewRecorder() + srv.handleChatCompletions(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status: got %d body=%s", w.Code, w.Body.String()) + } + options, ok := fake.req.Input["options"].(map[string]any) + if !ok { + t.Fatalf("options not passed to service: %+v", fake.req.Input) + } + if options["num_ctx"].(float64) != 8192 { + t.Fatalf("edge should pass num_ctx unchanged for adapter enforcement: got %v", options["num_ctx"]) + } +} + func TestCollectRunResultFailsWhenEventStreamCloses(t *testing.T) { events := make(chan *iop.RunEvent) close(events) diff --git a/apps/edge/internal/opsconsole/console_test.go b/apps/edge/internal/opsconsole/console_test.go index c7da177..11ddc67 100644 --- a/apps/edge/internal/opsconsole/console_test.go +++ b/apps/edge/internal/opsconsole/console_test.go @@ -171,6 +171,7 @@ func TestSendRun_SubmitRunRequest_MetadataSource(t *testing.T) { func TestFormatNodeCommandView_RendersSortedResult(t *testing.T) { view := edgeservice.NodeCommandView{ NodeLabel: "node0", + Adapter: "cli@local", Target: "codex", SessionID: "default", Result: map[string]string{ @@ -181,7 +182,7 @@ func TestFormatNodeCommandView_RendersSortedResult(t *testing.T) { var out bytes.Buffer FormatNodeCommandView(&out, "capabilities", view) got := out.String() - if !strings.Contains(got, "[node0-capabilities] target=codex session=default") { + if !strings.Contains(got, "[node0-capabilities] adapter=cli@local target=codex session=default") { t.Errorf("missing header: %q", got) } // keys must appear in sorted order: targets before version @@ -204,6 +205,7 @@ func TestFormatNodeCommandView_EmptyResult(t *testing.T) { func TestFormatNodeCommandView_RendersStructuredSessions(t *testing.T) { view := edgeservice.NodeCommandView{ NodeLabel: "node0", + Adapter: "cli@local", Target: "claude", SessionID: "default", Result: map[string]string{ @@ -220,7 +222,7 @@ func TestFormatNodeCommandView_RendersStructuredSessions(t *testing.T) { var out bytes.Buffer FormatNodeCommandView(&out, "sessions", view) got := out.String() - if !strings.Contains(got, "[node0-sessions] target=claude session=default") { + if !strings.Contains(got, "[node0-sessions] adapter=cli@local target=claude session=default") { t.Errorf("missing header: %q", got) } if !strings.Contains(got, "sessions: 2") { @@ -237,6 +239,7 @@ func TestFormatNodeCommandView_RendersStructuredSessions(t *testing.T) { func TestFormatNodeCommandView_RendersTransportStatusKeys(t *testing.T) { view := edgeservice.NodeCommandView{ NodeLabel: "node0", + Adapter: "cli@local", Target: "claude", SessionID: "default", Result: map[string]string{ @@ -251,6 +254,9 @@ func TestFormatNodeCommandView_RendersTransportStatusKeys(t *testing.T) { var out bytes.Buffer FormatNodeCommandView(&out, "transport", view) got := out.String() + if !strings.Contains(got, "[node0-transport] adapter=cli@local target=claude session=default") { + t.Errorf("missing header: %q", got) + } for _, want := range []string{"connected = true", "state = connected", "node_id = test-node", "session_id = default"} { if !strings.Contains(got, want) { t.Errorf("expected %q in transport output, got:\n%s", want, got) diff --git a/apps/edge/internal/opsconsole/events_test.go b/apps/edge/internal/opsconsole/events_test.go index 99befa8..df48f68 100644 --- a/apps/edge/internal/opsconsole/events_test.go +++ b/apps/edge/internal/opsconsole/events_test.go @@ -270,13 +270,16 @@ func TestStatusWaitTimeout_UsesNormalizedRequestTimeout(t *testing.T) { func TestFormatUsageStatus_PrintsParsedLimits(t *testing.T) { var out bytes.Buffer - FormatUsageStatus(&out, "local-node", "codex", "default", &iop.AgentUsageStatus{ + FormatUsageStatus(&out, "local-node", "cli@local", "codex", "default", &iop.AgentUsageStatus{ DailyLimit: "98%", DailyResetTime: "18:38", WeeklyLimit: "22%", WeeklyResetTime: "10:20 on 8 May", }) got := out.String() + if !strings.Contains(got, "adapter=cli@local target=codex session=default") { + t.Errorf("expected header with adapter identity, got:\n%s", got) + } if !strings.Contains(got, "Daily limit: 98% remaining (resets 18:38)") { t.Errorf("expected Daily limit: 98%% remaining (resets 18:38), got:\n%s", got) } @@ -287,7 +290,7 @@ func TestFormatUsageStatus_PrintsParsedLimits(t *testing.T) { func TestFormatUsageStatus_PrintsClaudeLabels(t *testing.T) { var out bytes.Buffer - FormatUsageStatus(&out, "local-node", "claude", "default", &iop.AgentUsageStatus{ + FormatUsageStatus(&out, "local-node", "cli@local", "claude", "default", &iop.AgentUsageStatus{ DailyLimit: "98%", DailyResetTime: "10am (Asia/Seoul)", WeeklyLimit: "50%", @@ -308,7 +311,7 @@ func TestFormatUsageStatus_PrintsClaudeLabels(t *testing.T) { func TestFormatUsageStatus_PrintsAntigravityQuotaMetadata(t *testing.T) { var out bytes.Buffer - FormatUsageStatus(&out, "local-node", "antigravity", "default", &iop.AgentUsageStatus{ + FormatUsageStatus(&out, "local-node", "cli@local", "antigravity", "default", &iop.AgentUsageStatus{ DailyLimit: "100%", DailyResetTime: "24h", Metadata: map[string]string{ @@ -331,7 +334,7 @@ func TestFormatUsageStatus_PrintsAntigravityQuotaMetadata(t *testing.T) { func TestFormatUsageStatus_PrintsModelUsage(t *testing.T) { var out bytes.Buffer - FormatUsageStatus(&out, "local-node", "antigravity", "default", &iop.AgentUsageStatus{ + FormatUsageStatus(&out, "local-node", "cli@local", "antigravity", "default", &iop.AgentUsageStatus{ DailyLimit: "100%", Metadata: map[string]string{ "daily_label": "Daily quota", @@ -359,7 +362,7 @@ func TestFormatUsageStatus_PrintsModelUsage(t *testing.T) { func TestFormatUsageStatus_PrintsClaudeStatusMetadata(t *testing.T) { var out bytes.Buffer - FormatUsageStatus(&out, "local-node", "claude-tui", "default", &iop.AgentUsageStatus{ + FormatUsageStatus(&out, "local-node", "cli@1", "claude-tui", "default", &iop.AgentUsageStatus{ Metadata: map[string]string{ "status_kind": "claude", "claude_status_version": "2.1.144", @@ -416,7 +419,7 @@ func TestFormatUsageStatus_PrintsFallbackMetadata(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var out bytes.Buffer - FormatUsageStatus(&out, "local-node", "codex", "default", tc.status) + FormatUsageStatus(&out, "local-node", "cli@local", "codex", "default", tc.status) got := out.String() if !strings.Contains(got, tc.wantContain) { t.Errorf("expected %q in output, got:\n%s", tc.wantContain, got) @@ -427,7 +430,7 @@ func TestFormatUsageStatus_PrintsFallbackMetadata(t *testing.T) { func TestFormatUsageStatus_Error(t *testing.T) { var out bytes.Buffer - FormatUsageStatus(&out, "local-node", "codex", "default", &iop.AgentUsageStatus{ + FormatUsageStatus(&out, "local-node", "cli@local", "codex", "default", &iop.AgentUsageStatus{ RawOutput: "unparsed error text", }) got := out.String() diff --git a/apps/edge/internal/opsconsole/status.go b/apps/edge/internal/opsconsole/status.go index e4ff5e9..1868d50 100644 --- a/apps/edge/internal/opsconsole/status.go +++ b/apps/edge/internal/opsconsole/status.go @@ -41,7 +41,7 @@ func SendStatus(ctx context.Context, edgeSvc *edgeservice.Service, out io.Writer return err } - FormatUsageStatus(out, result.NodeLabel, result.Target, result.SessionID, result.UsageStatus) + FormatUsageStatus(out, result.NodeLabel, result.Adapter, result.Target, result.SessionID, result.UsageStatus) return nil } @@ -87,7 +87,7 @@ func sendNodeCommandView(ctx context.Context, out io.Writer, target *TargetState // results with structured session.N.* keys, a human-readable grouped layout // is used. All other commands fall back to stable sorted key output. func FormatNodeCommandView(out io.Writer, label string, view edgeservice.NodeCommandView) { - fmt.Fprintf(out, "[%s-%s] target=%s session=%s\n", view.NodeLabel, label, view.Target, view.SessionID) + fmt.Fprintf(out, "[%s-%s] adapter=%s target=%s session=%s\n", view.NodeLabel, label, view.Adapter, view.Target, view.SessionID) if len(view.Result) == 0 { fmt.Fprintf(out, "no %s payload returned\n", label) return @@ -125,8 +125,8 @@ func renderStructuredSessions(out io.Writer, result map[string]string) { } } -func FormatUsageStatus(out io.Writer, nodeAlias, targetName, sessionID string, status *iop.AgentUsageStatus) { - fmt.Fprintf(out, "[%s-status] target=%s session=%s\n", nodeAlias, targetName, sessionID) +func FormatUsageStatus(out io.Writer, nodeAlias, adapterName, targetName, sessionID string, status *iop.AgentUsageStatus) { + fmt.Fprintf(out, "[%s-status] adapter=%s target=%s session=%s\n", nodeAlias, adapterName, targetName, sessionID) if status == nil { fmt.Fprintln(out, "no usage status provided") diff --git a/apps/edge/internal/service/node_command.go b/apps/edge/internal/service/node_command.go index 708f3f3..ff7e1c7 100644 --- a/apps/edge/internal/service/node_command.go +++ b/apps/edge/internal/service/node_command.go @@ -24,6 +24,7 @@ type UsageStatusRequest struct { type UsageStatusView struct { NodeID string NodeLabel string + Adapter string Target string SessionID string UsageStatus *iop.AgentUsageStatus @@ -54,6 +55,7 @@ func (s *Service) UsageStatus(_ context.Context, req UsageStatusRequest) (UsageS return UsageStatusResult{ NodeID: entry.NodeID, NodeLabel: nodeLabel(entry), + Adapter: commandReq.GetAdapter(), Target: commandReq.GetTarget(), SessionID: commandReq.GetSessionId(), UsageStatus: resp.GetUsageStatus(), diff --git a/apps/edge/internal/service/service_test.go b/apps/edge/internal/service/service_test.go index a2f13ba..cd0ddf5 100644 --- a/apps/edge/internal/service/service_test.go +++ b/apps/edge/internal/service/service_test.go @@ -312,10 +312,8 @@ func TestListNodeSnapshotsWithConfig(t *testing.T) { WorkspaceRoot: "/workspace", }, Adapters: config.AdaptersConf{ - Ollama: config.OllamaConf{ - Enabled: true, - BaseURL: "http://localhost:11434", - ContextSize: 2048, + OllamaInstances: []config.OllamaInstanceConf{ + {Name: "ollama", Enabled: true, BaseURL: "http://localhost:11434", ContextSize: 2048}, }, }, }) @@ -716,6 +714,67 @@ func TestServiceExecuteCommandAgentCommandCapabilitiesSuccess(t *testing.T) { } } +func TestUsageStatusPreservesAdapterIdentity(t *testing.T) { + edgeConn, nodeConn := net.Pipe() + defer edgeConn.Close() + defer nodeConn.Close() + + parserMap := toki.ParserMap{ + toki.TypeNameOf(&iop.NodeCommandRequest{}): func(b []byte) (proto.Message, error) { + m := &iop.NodeCommandRequest{} + return m, proto.Unmarshal(b, m) + }, + toki.TypeNameOf(&iop.NodeCommandResponse{}): func(b []byte) (proto.Message, error) { + m := &iop.NodeCommandResponse{} + return m, proto.Unmarshal(b, m) + }, + } + + edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap) + nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap) + + toki.AddRequestListenerTyped(&nodeClient.Communicator, func(req *iop.NodeCommandRequest) (*iop.NodeCommandResponse, error) { + return &iop.NodeCommandResponse{ + RequestId: req.RequestId, + Type: req.Type, + Adapter: req.Adapter, + Target: req.Target, + SessionId: req.SessionId, + UsageStatus: &iop.AgentUsageStatus{RawOutput: "ok"}, + }, nil + }) + + reg := edgenode.NewRegistry() + reg.Register(&edgenode.NodeEntry{ + NodeID: "node-uid", + Alias: "svc-node", + Client: edgeClient, + }) + svc := edgeservice.New(reg, nil) + + result, err := svc.UsageStatus(context.Background(), edgeservice.UsageStatusRequest{ + NodeRef: "node-uid", + Adapter: "cli@1", + Target: "claude", + SessionID: "sess-u", + }) + if err != nil { + t.Fatalf("UsageStatus: %v", err) + } + if result.Adapter != "cli@1" { + t.Errorf("Adapter: got %q want %q", result.Adapter, "cli@1") + } + if result.Target != "claude" { + t.Errorf("Target: got %q want %q", result.Target, "claude") + } + if result.SessionID != "sess-u" { + t.Errorf("SessionID: got %q want %q", result.SessionID, "sess-u") + } + if result.UsageStatus == nil || result.UsageStatus.RawOutput != "ok" { + t.Errorf("UsageStatus: got %+v", result.UsageStatus) + } +} + func TestServiceExecuteCommandAgentCommandCapabilitiesNodeError(t *testing.T) { edgeConn, nodeConn := net.Pipe() defer edgeConn.Close() diff --git a/apps/node/README.md b/apps/node/README.md index 54fda11..b366f64 100644 --- a/apps/node/README.md +++ b/apps/node/README.md @@ -119,7 +119,7 @@ node는 adapter execution(`RunRequest`) 외에도 edge가 보내는 `NodeCommand | `vllm` | vLLM OpenAI-compatible API | `/v1/models` 조회 skeleton | | `cli` | claude/antigravity/codex/opencode/cline CLI 실행 | 구현 완료 | -Ollama adapter는 edge에서 받은 내부 target을 Ollama model 이름으로 사용한다. Edge OpenAI-compatible HTTP API에서 `openai.target`을 지정하면 외부 `model` 값과 무관하게 해당 Ollama model로 라우팅하고, `openai.target`을 비우면 요청의 `model` 값을 그대로 내부 target으로 전달한다. `adapters.ollama.context_size`가 설정되어 있으면 Ollama `/api/chat` 요청의 `options.num_ctx`로 전달한다. +Ollama adapter는 edge에서 받은 내부 target을 Ollama model 이름으로 사용한다. Edge OpenAI-compatible HTTP API에서 `openai.target`을 지정하면 외부 `model` 값과 무관하게 해당 Ollama model로 라우팅하고, `openai.target`을 비우면 요청의 `model` 값을 그대로 내부 target으로 전달한다. `adapters.ollama.context_size`가 설정되어 있으면 Ollama `/api/chat` 요청의 `options.num_ctx`를 강제하며, 요청 input의 `options.num_ctx`보다 우선한다. `claude`, `antigravity`, `codex`, `opencode`, `cline`처럼 기본이 interactive TUI인 CLI는 기본적으로 non-interactive 모드로 설정한다. 예: `claude -p --dangerously-skip-permissions`, `agy --dangerously-skip-permissions --print `, `codex exec --dangerously-bypass-approvals-and-sandbox`, `cline -y --json --config /config/.cline/profiles/ollama-dgx `. diff --git a/apps/node/internal/adapters/ollama/ollama.go b/apps/node/internal/adapters/ollama/ollama.go index 8583d09..f99ffc1 100644 --- a/apps/node/internal/adapters/ollama/ollama.go +++ b/apps/node/internal/adapters/ollama/ollama.go @@ -312,9 +312,7 @@ func ollamaOptionsFromInput(input map[string]any, contextSize int) map[string]an } } if contextSize > 0 { - if _, hasNumCtx := options["num_ctx"]; !hasNumCtx { - options["num_ctx"] = contextSize - } + options["num_ctx"] = contextSize // Edge-owned: always overrides request } if len(options) == 0 { return nil diff --git a/apps/node/internal/adapters/ollama/ollama_test.go b/apps/node/internal/adapters/ollama/ollama_test.go index 2b3a35b..506e1da 100644 --- a/apps/node/internal/adapters/ollama/ollama_test.go +++ b/apps/node/internal/adapters/ollama/ollama_test.go @@ -117,12 +117,12 @@ func TestOllamaExecutePassesOptionsAndTopLevelFields(t *testing.T) { } switch v := req.Options["num_ctx"].(type) { case float64: - if int(v) != 4096 { - t.Fatalf("request num_ctx should win over config default: got %d, want 4096", int(v)) + if int(v) != 262144 { + t.Fatalf("profile context_size should override request num_ctx: got %d, want 262144", int(v)) } case int: - if v != 4096 { - t.Fatalf("request num_ctx should win over config default: got %d, want 4096", v) + if v != 262144 { + t.Fatalf("profile context_size should override request num_ctx: got %d, want 262144", v) } default: t.Fatalf("unexpected num_ctx type %T in %+v", v, req.Options) @@ -323,6 +323,41 @@ func TestOllamaMultiEndpoint(t *testing.T) { } } +func TestOllamaContextSizeForcedOverRequestNumCtx(t *testing.T) { + var gotNumCtx int + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req ollamaChatRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("decode request: %v", err) + } + if v, ok := req.Options["num_ctx"].(float64); ok { + gotNumCtx = int(v) + } + w.Header().Set("Content-Type", "application/x-ndjson") + _, _ = w.Write([]byte(`{"message":{"role":"assistant","content":"ok"},"done":false}` + "\n")) + _, _ = w.Write([]byte(`{"done":true}` + "\n")) + })) + defer server.Close() + + // profile context_size=262144 must override request options.num_ctx=8192 + adapter := New(config.OllamaConf{BaseURL: server.URL, ContextSize: 262144}, zap.NewNop()) + sink := &fakeSink{} + err := adapter.Execute(context.Background(), noderuntime.ExecutionSpec{ + RunID: "run-forced", + Target: "llama-test", + Input: map[string]any{ + "prompt": "hi", + "options": map[string]any{"num_ctx": float64(8192), "temperature": 0.5}, + }, + }, sink) + if err != nil { + t.Fatalf("Execute failed: %v", err) + } + if gotNumCtx != 262144 { + t.Fatalf("Edge-owned context_size must override request num_ctx: got %d, want 262144", gotNumCtx) + } +} + func TestOllamaExecuteStreamsThinkingDeltasSeparately(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/chat" { diff --git a/apps/node/internal/node/node_test.go b/apps/node/internal/node/node_test.go index aa7938e..7da9d35 100644 --- a/apps/node/internal/node/node_test.go +++ b/apps/node/internal/node/node_test.go @@ -845,6 +845,51 @@ func TestOnCommandRequest_AdapterDispatch_AmbiguousAdapter(t *testing.T) { } } +func TestOnCommandRequest_MultiAdapterCapabilities(t *testing.T) { + ika1 := &instanceKeyAdapter{instanceKey: "ollama@local"} + ika2 := &instanceKeyAdapter{instanceKey: "ollama@dgx"} + router := &fixedRouter{ + adapters: map[string]runtime.Adapter{ + "ollama@local": ika1, + "ollama@dgx": ika2, + }, + } + n, _ := makeNode(t, router) + + cases := []struct { + adapter string + wantInstKey string + }{ + {"ollama@local", "ollama@local"}, + {"ollama@dgx", "ollama@dgx"}, + } + instKeys := make([]string, 0, len(cases)) + for _, tc := range cases { + resp, err := n.OnCommandRequest(context.Background(), &transport.Session{}, &iop.NodeCommandRequest{ + RequestId: "req-multi-" + tc.adapter, + Type: iop.NodeCommandType_NODE_COMMAND_TYPE_CAPABILITIES, + Adapter: tc.adapter, + Target: "llama3", + }) + if err != nil { + t.Fatalf("CAPABILITIES %s: %v", tc.adapter, err) + } + if resp.GetError() != "" { + t.Fatalf("CAPABILITIES %s: unexpected error %q", tc.adapter, resp.GetError()) + } + if got := resp.GetResult()["instance_key"]; got != tc.wantInstKey { + t.Errorf("CAPABILITIES %s: instance_key got %q want %q", tc.adapter, got, tc.wantInstKey) + } + if resp.GetAdapter() != tc.adapter { + t.Errorf("CAPABILITIES %s: response adapter got %q want %q", tc.adapter, resp.GetAdapter(), tc.adapter) + } + instKeys = append(instKeys, resp.GetResult()["instance_key"]) + } + if instKeys[0] == instKeys[1] { + t.Errorf("expected distinct instance_key for distinct adapters, both got %q", instKeys[0]) + } +} + func TestOnCommandRequest_Capabilities_ExactInstanceKey(t *testing.T) { ika := &instanceKeyAdapter{instanceKey: "ollama@local"} router := &fixedRouter{