181 lines
8.1 KiB
Markdown
181 lines
8.1 KiB
Markdown
<!-- task=07_cli_profile_proto_message plan=0 tag=API -->
|
|
|
|
# CLI 프로파일 전용 proto 메시지 도입
|
|
|
|
## 이 파일을 읽는 구현 에이전트에게
|
|
|
|
아래 체크리스트를 순서대로 완료하고, 각 항목의 중간 검증과 최종 검증을 실제로 실행하세요. 구현이 끝나면 `agent-task/07_cli_profile_proto_message/CODE_REVIEW.md`의 모든 섹션을 실제 구현 내용과 명령 출력으로 채우세요. `CODE_REVIEW.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log`로 이름 변경, `complete.log` 작성)는 구현 에이전트가 수행하면 안 되며, 리뷰 스킬 전용입니다.
|
|
|
|
## 배경
|
|
|
|
edge가 node에 보내는 `NodeConfigPayload`에서 CLI 프로파일은 `AdapterConfig.settings` (`google.protobuf.Struct`)로 직렬화되고, node 쪽에서 손으로 디코드합니다. (`apps/edge/internal/transport/server.go:190-207`의 `buildConfigPayload`와 `apps/node/internal/adapters/factory.go:69-118`의 `cliConfFromStruct`) 프로파일에 새 필드를 추가하면 edge·node 양쪽을 손으로 맞춰야 하며, structpb 인코딩 특성상 정수가 float로 들어가는 등의 변환 코드가 필요합니다. domain rule(`agent-ops/rules/project/rules.md`의 "protobuf 계약 변경은 .proto부터")을 따라 CLI 프로파일 전용 proto 메시지를 정의하고, edge·node 양쪽에서 구조체 단위로 주고받게 합니다.
|
|
|
|
## 의존 관계 및 구현 순서
|
|
|
|
1. `[API-1]` `proto/iop/runtime.proto`에 `CLIAdapterConfig` / `CLIProfileConfig` 메시지를 추가하고 `make proto`로 생성물 갱신.
|
|
2. `[API-2]` edge `buildConfigPayload`가 새 메시지로 직접 채우도록 변경.
|
|
3. `[API-3]` node `BuildFromPayload`가 structpb 디코드 대신 새 메시지를 그대로 사용.
|
|
|
|
### [API-1] proto 메시지 정의 및 생성물 갱신
|
|
|
|
#### 문제
|
|
|
|
`proto/iop/runtime.proto:96-100`의 `AdapterConfig`는 `google.protobuf.Struct settings`로 모든 어댑터 타입의 설정을 우회 직렬화합니다. CLI 프로파일은 필드가 9개로 늘어나 string-keyed map 직렬화의 위험이 큽니다.
|
|
|
|
#### 해결 방법
|
|
|
|
`proto/iop/runtime.proto`의 `AdapterConfig`에 `oneof config`를 도입해 어댑터 타입별 typed 메시지를 가질 수 있도록 합니다. CLI 전용 메시지는 다음과 같이 정의합니다.
|
|
|
|
```proto
|
|
message AdapterConfig {
|
|
string type = 1;
|
|
bool enabled = 2;
|
|
google.protobuf.Struct settings = 3; // 구버전 호환용; 신규 어댑터는 oneof 사용 권장
|
|
oneof config {
|
|
CLIAdapterConfig cli = 4;
|
|
OllamaAdapterConfig ollama = 5;
|
|
VllmAdapterConfig vllm = 6;
|
|
}
|
|
}
|
|
|
|
message CLIAdapterConfig {
|
|
map<string, CLIProfileConfig> profiles = 1;
|
|
}
|
|
|
|
message CLIProfileConfig {
|
|
string command = 1;
|
|
repeated string args = 2;
|
|
repeated string env = 3;
|
|
bool persistent = 4;
|
|
bool terminal = 5;
|
|
int32 response_idle_timeout_ms = 6;
|
|
int32 startup_idle_timeout_ms = 7;
|
|
string output_format = 8;
|
|
}
|
|
|
|
message OllamaAdapterConfig { string base_url = 1; }
|
|
message VllmAdapterConfig { string endpoint = 1; }
|
|
```
|
|
|
|
오래된 `settings` 필드는 일단 유지해 mock/기타 어댑터 호환을 깨지 않습니다. CLI/Ollama/Vllm은 신규 oneof 경로만 사용하도록 합니다.
|
|
|
|
#### 수정 파일 및 체크리스트
|
|
|
|
- [ ] `proto/iop/runtime.proto`에 `CLIAdapterConfig`, `CLIProfileConfig`, `OllamaAdapterConfig`, `VllmAdapterConfig` 메시지를 추가한다.
|
|
- [ ] `AdapterConfig`에 `oneof config { ... }`를 추가한다 (`settings` 필드는 그대로 둔다).
|
|
- [ ] `make proto`를 실행해 `proto/gen/iop/runtime.pb.go`를 갱신한다.
|
|
- [ ] 생성 파일은 직접 수정하지 않는다.
|
|
|
|
#### 테스트 작성
|
|
|
|
테스트는 `[API-2]`/`[API-3]`에서 통합 검증한다. proto 정의 단독으로는 별도 단위 테스트를 작성하지 않는다.
|
|
|
|
#### 중간 검증
|
|
|
|
```bash
|
|
make proto
|
|
git diff proto/iop/runtime.proto
|
|
go build ./proto/gen/...
|
|
```
|
|
|
|
예상 결과: `runtime.proto`에 새 메시지/oneof가 추가되어 있고, 생성물이 빌드된다.
|
|
|
|
### [API-2] edge `buildConfigPayload`를 typed 메시지로 갱신
|
|
|
|
#### 문제
|
|
|
|
`apps/edge/internal/transport/server.go:155-209`의 `buildConfigPayload`가 `structpb.NewStruct(map[string]any{...})`로 CLI/Ollama/Vllm 설정을 우회 직렬화합니다. `stringsToAny` 같은 보조 함수도 필요합니다.
|
|
|
|
#### 해결 방법
|
|
|
|
CLI/Ollama/Vllm은 `oneof config`로 직접 채우고, mock만 기존 `settings` 경로를 유지합니다.
|
|
|
|
```go
|
|
if rec.Adapters.CLI.Enabled {
|
|
profiles := make(map[string]*iop.CLIProfileConfig, len(rec.Adapters.CLI.Profiles))
|
|
for name, p := range rec.Adapters.CLI.Profiles {
|
|
profiles[name] = &iop.CLIProfileConfig{
|
|
Command: p.Command,
|
|
Args: append([]string(nil), p.Args...),
|
|
Env: append([]string(nil), p.Env...),
|
|
Persistent: p.Persistent,
|
|
Terminal: p.Terminal,
|
|
ResponseIdleTimeoutMs: int32(p.ResponseIdleTimeoutMS),
|
|
StartupIdleTimeoutMs: int32(p.StartupIdleTimeoutMS),
|
|
OutputFormat: p.OutputFormat,
|
|
}
|
|
}
|
|
payload.Adapters = append(payload.Adapters, &iop.AdapterConfig{
|
|
Type: "cli", Enabled: true,
|
|
Config: &iop.AdapterConfig_Cli{Cli: &iop.CLIAdapterConfig{Profiles: profiles}},
|
|
})
|
|
}
|
|
```
|
|
|
|
#### 수정 파일 및 체크리스트
|
|
|
|
- [ ] `apps/edge/internal/transport/server.go`의 `buildConfigPayload`에서 cli/ollama/vllm 분기를 oneof 기반으로 다시 쓴다.
|
|
- [ ] `stringsToAny` 헬퍼는 더 이상 사용되지 않으면 제거한다.
|
|
- [ ] mock 어댑터 분기는 그대로 둔다.
|
|
|
|
#### 테스트 작성
|
|
|
|
`apps/edge/internal/transport/server_test.go`에 `TestBuildConfigPayload_CLIOneof` (가칭)를 추가해 CLI 프로파일 한 개 정의에서 oneof 경로가 채워지고, settings 필드는 비어 있는지 단언한다.
|
|
|
|
#### 중간 검증
|
|
|
|
```bash
|
|
go test ./apps/edge/internal/transport/...
|
|
```
|
|
|
|
예상 결과: 신규 테스트 포함 PASS.
|
|
|
|
### [API-3] node `BuildFromPayload`를 typed 메시지 기반으로 갱신
|
|
|
|
#### 문제
|
|
|
|
`apps/node/internal/adapters/factory.go:69-118`의 `cliConfFromStruct`가 structpb를 손으로 디코드하면서 `boolFromAny`, `intFromAny` 같은 헬퍼에 의존합니다. structpb의 number→float64 변환 등 함정이 많습니다.
|
|
|
|
#### 해결 방법
|
|
|
|
`AdapterConfig.GetCli()`가 nil이 아니면 typed 메시지 그대로 `config.CLIConf`로 매핑하고, structpb 경로(`cliConfFromStruct`)는 제거합니다. Ollama/Vllm도 동일하게 변경.
|
|
|
|
#### 수정 파일 및 체크리스트
|
|
|
|
- [ ] `apps/node/internal/adapters/factory.go`의 `cli/ollama/vllm` 분기를 oneof 기반으로 재작성한다.
|
|
- [ ] `cliConfFromStruct`, `ollamaConfFromStruct`, `vllmConfFromStruct`, `boolFromAny`, `intFromAny`를 모두 제거한다.
|
|
- [ ] `apps/node/internal/adapters/factory_internal_test.go`와 `factory_test.go`의 케이스를 typed 메시지 입력 기반으로 갱신한다.
|
|
|
|
#### 테스트 작성
|
|
|
|
`factory_test.go`의 기존 시나리오(여러 어댑터 enabled, CLI 프로파일 매핑)를 oneof 입력으로 다시 작성한다. 신규 케이스: `Persistent`, `ResponseIdleTimeoutMS`, `StartupIdleTimeoutMS`가 정확히 0이 아닌 값으로 전달되는지 단언.
|
|
|
|
#### 중간 검증
|
|
|
|
```bash
|
|
go test ./apps/node/internal/adapters/...
|
|
```
|
|
|
|
예상 결과: PASS.
|
|
|
|
## 수정 파일 요약
|
|
|
|
| 파일 | 항목 |
|
|
|------|------|
|
|
| `proto/iop/runtime.proto` | API-1 |
|
|
| `proto/gen/iop/runtime.pb.go` (생성물) | API-1 |
|
|
| `apps/edge/internal/transport/server.go` | API-2 |
|
|
| `apps/edge/internal/transport/server_test.go` | API-2 |
|
|
| `apps/node/internal/adapters/factory.go` | API-3 |
|
|
| `apps/node/internal/adapters/factory_internal_test.go` | API-3 |
|
|
| `apps/node/internal/adapters/factory_test.go` | API-3 |
|
|
|
|
## 최종 검증
|
|
|
|
```bash
|
|
make proto
|
|
go build ./...
|
|
go test ./...
|
|
```
|
|
|
|
예상 결과: build/test 모두 PASS, 새 oneof 경로로 edge↔node가 동작.
|