iop/agent-task/opencode_sse_stream/plan_cloud_G07_1.log
toki 554e7ef269 feat: opencode SSE 스트림 어댑터 및 관련 개선사항 적용
- opencode SSE 스트림 파서 어댑터 추가 (opencode_sse.go)
- 블랙박스/내부 테스트 파일 추가
- node/edge 애플리케이션의 CLI 어댑터 개선
- 설정 파일 및 README 업데이트
- agent-task opencode_sse_stream 추가
2026-05-12 07:31:09 +09:00

160 lines
6.6 KiB
Text

<!-- task=opencode_sse_stream plan=1 tag=REVIEW_OPENCODE_SSE -->
# Review Fix Plan - OpenCode SSE API Contract
## 이 파일을 읽는 구현 에이전트에게
**반드시 마지막에 `CODE_REVIEW-cloud-G07.md`의 모든 섹션을 실제 구현 내용과 검증 출력으로 채워야 한다. 이 파일 작성 전에는 작업이 완료된 것이 아니다.** 아래 체크리스트를 끝까지 수행하고, 중간/최종 검증 명령의 실제 stdout/stderr를 review 파일에 붙인다. review 파일의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 archive 지시(`*.log` rename, `complete.log` 작성)는 구현 에이전트가 실행하지 않는다.
## 배경
1차 구현은 테스트를 통과했지만 OpenCode server OpenAPI 계약과 맞지 않는 부분이 확인됐다. `/session/{sessionID}/prompt_async`의 `model` payload는 객체여야 하고, `session.status.status`는 문자열이 아니라 `SessionStatus` 객체다. 이 두 문제는 실서버에서 prompt 전송 실패나 stream completion 누락으로 이어질 수 있다.
## 분석 결과
### 읽은 파일
- `agent-task/opencode_sse_stream/plan_cloud_G07_0.log`
- `agent-task/opencode_sse_stream/code_review_cloud_G07_0.log`
- `apps/node/internal/adapters/cli/opencode_sse.go`
- `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go`
- `apps/node/internal/adapters/cli/opencode_sse_internal_test.go`
- `configs/edge.yaml`
- local OpenCode `1.14.46` `/doc` OpenAPI response
### 테스트 커버리지 공백
- `--model provider/model`이 `/prompt_async` body에서 `{providerID, modelID}` 객체로 전송되는지 검증하지 않는다.
- `session.status` 이벤트가 `{"status":{"type":"idle"}}` 형태로 올 때 complete 되는지 검증하지 않는다.
### 심볼 참조
- renamed/removed symbols: none.
- 새 helper 후보: `opencodeModelPayload`, `opencodeIdleStatus`.
### 범위 결정 근거
- `runtime.Adapter`, protobuf, Edge transport는 변경하지 않는다. 문제는 opencode SSE adapter 내부 HTTP payload/event parsing 계약에 한정된다.
- `configs/edge.yaml`의 opencode profile 이름과 args는 유지한다.
- 실제 opencode binary/model 통합 테스트는 제외한다. `httptest.Server`와 local OpenAPI 계약에 맞춘 unit/blackbox tests로 검증한다.
### 빌드 등급
- build=`cloud-G07`, review=`cloud-G07`: HTTP API 계약과 streaming completion 조건을 수정하므로 기존 등급을 유지한다.
### [REVIEW_OPENCODE_SSE-1] prompt_async model payload를 OpenCode schema에 맞춘다
#### 문제
```go
// apps/node/internal/adapters/cli/opencode_sse.go:562
if opts.Model != "" {
payload["model"] = opts.Model
}
```
OpenCode `/session/{sessionID}/prompt_async` schema는 `model`을 `{providerID, modelID}` 객체로 요구한다. 현재 구현은 문자열을 보내므로 `--model ollama-dgx/qwen3.6:35b-a3b-bf16` 사용 시 실서버가 Bad Request를 반환할 수 있다.
#### 해결 방법
`--model provider/model`을 첫 `/` 기준으로 분리한다. provider 또는 model이 비어 있거나 `/`가 없으면 명확한 error를 반환하고 `RuntimeEvent error`를 emit한다.
```go
// after
if opts.Model != "" {
model, err := opencodeModelPayload(opts.Model)
if err != nil {
return err
}
payload["model"] = model
}
```
#### 수정 파일 및 체크리스트
- [ ] `apps/node/internal/adapters/cli/opencode_sse.go`: `opencodeModelPayload` helper 추가, `opencodePromptAsync`에서 객체 payload 사용.
- [ ] `apps/node/internal/adapters/cli/opencode_sse_internal_test.go`: 정상 split, `/` 없는 model, provider 빈 값, model 빈 값 테스트.
- [ ] `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go`: fake server의 `lastPromptBody`를 JSON decode해서 `model.providerID`, `model.modelID`, `parts[0].text`를 검증.
#### 테스트 작성
- 작성한다.
- `TestOpencodeModelPayload` 또는 동등한 이름으로 model parsing boundary를 검증한다.
- `TestCLIExecuteOpencodeSSE_StreamsTextDeltas`에 prompt body assertion을 추가한다.
#### 중간 검증
```bash
go test -count=1 ./apps/node/internal/adapters/cli
```
예상 결과: package pass. Go test cache는 허용하지 않는다.
### [REVIEW_OPENCODE_SSE-2] session.status 객체 idle을 complete로 처리한다
#### 문제
```go
// apps/node/internal/adapters/cli/opencode_sse.go:401
case "session.status":
if status, ok := stringFromProp(e.ev.Properties, "status"); ok && status == "idle" {
```
OpenCode `EventSessionStatus.properties.status`는 `SessionStatus` 객체다. `{"status":{"type":"idle"}}` 이벤트가 오면 현재 구현은 complete를 emit하지 않는다.
#### 해결 방법
문자열 `"idle"`과 객체 `{"type":"idle"}`를 모두 처리하는 helper를 추가한다.
```go
// after
case "session.status":
if opencodeStatusIdle(e.ev.Properties) {
return emitOpencodeComplete(ctx, spec.RunID, inputTokens, outputTokens, sink)
}
```
complete event 중복 코드는 작게 helper로 묶어도 된다. 단, 이 refactor는 `opencode_sse.go` 안에 한정한다.
#### 수정 파일 및 체크리스트
- [ ] `apps/node/internal/adapters/cli/opencode_sse.go`: `opencodeStatusIdle` helper 추가, `session.status` branch 수정.
- [ ] `apps/node/internal/adapters/cli/opencode_sse_internal_test.go`: string idle, object idle, busy/retry/non-map false 테스트.
- [ ] `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go`: `session.idle` 없이 `session.status` object idle만 보내도 run이 complete 되는 regression test 추가.
#### 테스트 작성
- 작성한다.
- `TestCLIExecuteOpencodeSSE_SessionStatusObjectIdleCompletes`를 추가한다.
#### 중간 검증
```bash
go test -count=1 ./apps/node/internal/adapters/cli
```
예상 결과: package pass. Go test cache는 허용하지 않는다.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/node/internal/adapters/cli/opencode_sse.go` | REVIEW_OPENCODE_SSE-1, REVIEW_OPENCODE_SSE-2 |
| `apps/node/internal/adapters/cli/opencode_sse_internal_test.go` | REVIEW_OPENCODE_SSE-1, REVIEW_OPENCODE_SSE-2 |
| `apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go` | REVIEW_OPENCODE_SSE-1, REVIEW_OPENCODE_SSE-2 |
## 최종 검증
```bash
go test -count=1 ./apps/node/internal/adapters/cli ./apps/node/internal/adapters ./apps/edge/internal/node ./apps/edge/internal/transport ./packages/config
```
예상 결과: package pass. Go test cache는 허용하지 않는다.
```bash
go test -count=1 ./...
```
예상 결과: repository-wide pass. Go test cache는 허용하지 않는다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 전체 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.