<!-- task=m-stream-evidence-gate-core/01_event_contract_types plan=0 tag=API -->


# Stream Gate Event Contract Types 구현 계획

## 이 파일을 읽는 구현 에이전트에게

구현 모델은 Plan 모델 `GPT-5` 및 이후 Review 모델과 달라야 한다. 이 계획의 source/test 변경만 수행하고 review stub의 구현 에이전트 소유 섹션을 모두 채운 뒤 `review-ready`를 보고하고 종료한다. 직접 code-review를 수행하거나 다음 에이전트를 시작하지 않는다.

## 역할 모델

- Plan: `GPT-5`
- Implementation/Review identity 기록 위치: [CODE_REVIEW-local-G05.md](CODE_REVIEW-local-G05.md)의 `역할 모델`

## 배경

현재 normalized `RunEvent`와 provider tunnel frame은 앱별 타입과 switch로 처리되어 공통 stream gate가 소비할 transport-agnostic event 계약이 없다. 승인된 SDD의 S09/S21은 codec별 raw 입력을 동일한 typed lifecycle로 수렴하고, terminal failure를 raw-free bounded cause와 단일 `TerminalResult`로 전달하도록 요구한다. 이 child는 후속 단위 테스트와 codec/host fixture가 의존할 production 공개 계약만 만든다.

## 사용자 리뷰 요청 흐름

선택된 Milestone의 `구현 잠금 > 결정 필요`가 구현을 차단할 때만 active review stub의 `사용자 리뷰 요청` 섹션에 `implementation-user-review-request-section.md` 형식으로 기록한다. 구현 중 사용자에게 직접 질문하지 않으며, code-review가 요청의 유효성을 확인하고 실제 `USER_REVIEW.md` 작성을 소유한다.

## 분석 결과

### 읽은 파일

- `go.mod`: module 및 Go 1.24 기준
- `packages/go/audit/audit.go`: 전체, 공통 typed contract와 defensive-copy 관례
- `packages/go/audit/audit_test.go:1`: 1-180, validation/copy 테스트 관례
- `packages/go/events/events.go`: 전체, 공통 event helper 경계
- `packages/go/policy/engine.go`: 전체, 작은 공통 interface 관례
- `proto/iop/runtime.proto:36`: 36-98, `RunEvent`와 `ProviderTunnelFrame` source event shape
- `apps/node/internal/runtime/types.go:47`: 47-80 및 222-274, agent/runtime event와 tunnel frame 종류
- `apps/node/internal/adapters/cli/emitter_profile_json.go:1`: 1-130, agent-family raw event의 runtime event 변환
- `apps/node/internal/adapters/openai_compat/openai_compat_test_support_test.go:1`: 1-90, ordered tunnel fake sink 관례
- `apps/edge/internal/openai/normalized_sse.go:18`: 18-180, eager role emission과 runtime stream lifecycle
- `apps/edge/internal/openai/chat_stream_session.go:29`: 29-151 및 247-314, single terminal 및 event switch
- `apps/edge/internal/openai/provider_tunnel.go:125`: 125-285, response-start/body/error/end commit lifecycle
- `apps/edge/internal/openai/run_result.go:19`: 19-99, normalized non-stream event switch
- `apps/edge/internal/openai/common_types.go:35`: 35-42, 현재 OpenAI error envelope
- `apps/edge/internal/openai/server.go:201`: 201-209, endpoint error 단일 직렬화
- `apps/edge/internal/openai/sse_writer.go:48`: 48-65, SSE terminal error와 `[DONE]`
- `apps/edge/internal/openai/provider_tunnel_test.go:397`: 397-427 및 595-625, pre/post-start provider error fixture
- `apps/edge/internal/openai/chat_stream_session_test.go:60`: 60-145, terminal event matrix

### SDD 기준

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md`
- 상태/잠금: `[승인됨]`, `해제`
- 대상 Acceptance Scenario:
  - `S09` → Milestone Task `event-contract`: OpenAI/agent-family codec이 같은 normalized event/base disposition과 transport-agnostic host interface를 사용한다.
  - `S21` → Milestone Task `event-contract`: 최대 4단계 sanitized failure cause가 단일 terminal result로 전달되고 raw stack/provider body가 노출되지 않는다.
- 대상 Evidence Map:
  - `S09`: fake codec response-start/event table과 `packages/go/streamgate`의 앱 internal import 금지 증거
  - `S21`: bounded cause chain, terminal result, endpoint host single-error fixture
- 이 child는 S09/S21의 production contract를 만들고, 직접 validation/copy 테스트는 dependent child `03+01_event_contract_unit_tests`, fake consumer/host 및 import-boundary closure는 `02+01+03_codec_contract_fixtures`에 둔다.

### 테스트 환경 규칙

- `test_env`: `local`
- 읽은 규칙: `agent-test/local/rules.md`, `agent-test/local/platform-common-smoke.md`
- 선택 명령: `make proto`, 새 package fresh unit test, 기존 Edge/Node transport 회귀, `git diff --check`
- Go test cache: 새 공개 계약의 실제 실행을 요구하므로 `-count=1`
- 외부 endpoint, credential, docker, port: 사용하지 않음
- 불완전한 환경값: 없음. `make proto` toolchain 부재는 성공이 아니라 구현 evidence의 차단으로 기록한다.

### 테스트 커버리지 공백

- normalized event/base disposition, immutable `EvidenceBatch`, filter decision/outcome/recovery intent, bounded `FailureCauseChain`/`TerminalResult`의 공통 production 타입이 없음
- `ReleaseSink`: 실제 commit 동작은 후속 `commit-boundary` 범위이며 이 child에서는 compile 가능한 interface까지만 정의
- validation, defensive-copy, cause cap, terminal mutual exclusion 검증은 dependent child `03+01_event_contract_unit_tests`가 담당

### 심볼 참조

- rename/remove: none
- 신규 예정 심볼은 현재 Go source에 참조가 없다. `rg` 결과 `streamgate`, `NormalizedEvent`, `BaseEventDisposition`, `EvidenceBatch`, `FailureCauseChain`, `TerminalResult`, `ReleaseSink` 기존 정의 없음.

### 분할 판단

- 선택 candidate: Milestone Task `event-contract`
- immediate sibling set:
  - `01_event_contract_types`: production contract 3개 파일
  - `03+01_event_contract_unit_tests`: 직접 단위 테스트 3개 파일
  - `02+01+03_codec_contract_fixtures`: fake codec/host 및 import-boundary 테스트 2개 파일
- `01` write set: `event.go`, `filter_contract.go`, `terminal.go`
- `03` write set: 대응 `_test.go` 3개
- `02` write set: `consumer_contract_test.go`, `package_boundary_test.go`
- shared mutable state: 없음. dependent child들은 production 파일을 읽기만 하며 write set이 겹치지 않는다.
- collision: 없음. `03`은 `01`의 신규 공개 타입을 직접 검증하고, closure `02`는 production 계약과 직접 단위 테스트가 모두 완료된 뒤 실행한다.
- predecessor 상태: `01`은 predecessor가 없는 첫 child다.
- 세분화 결정: production 계약과 직접 단위 테스트는 각각 단독 구현·리뷰 가능한 직렬 경계이므로 분리한다. contract 타입 자체는 validation 불변조건을 공유하므로 파일별로 더 분리하지 않는다.

### 범위 결정 근거

- `apps/edge/**`, `apps/node/**`, `proto/**`, `packages/go/config/**`는 수정하지 않는다. 실제 codec adapter, stream buffering, eager commit 제거, config registry는 후속 Milestone Task 책임이다.
- `evidence-tail`, `commit-boundary`, `filter-registry`, coordinator/rebuilder/observation 동작은 구현하지 않는다.
- `agent-contract/outer/openai-compatible-api.md`는 현재 오류 surface 원본으로만 읽고, 새 외부 field나 활성 계약을 추가하지 않는다.
- raw parser, semantic detector, provider admission/auth/model rewrite를 `packages/go/streamgate`로 끌어올리지 않는다.

### 최종 라우팅

- evaluation_mode: `first-pass`
- build:
  - closures: scope/context/verification/evidence/ownership/decision 모두 `true`
  - basis: 승인된 SDD S09/S21, 새 package의 production write set, deterministic compile 명령, 고정된 dependent sibling, 사용자 결정 없음
  - grade scores: scope_coupling=1, state_concurrency=1, blast_irreversibility=1, evidence_diagnosis=0, verification_complexity=1
  - route: `local`, `G04`, `PLAN-local-G04.md`
- review:
  - closures: scope/context/verification/evidence/ownership/decision 모두 `true`
  - basis: SDD/outer contract/actual source와 새 package compile evidence를 로컬에서 완결 대조 가능
  - grade scores: scope_coupling=1, state_concurrency=1, blast_irreversibility=1, evidence_diagnosis=1, verification_complexity=1
  - route: `local`, `G05`, `CODE_REVIEW-local-G05.md`

## 구현 체크리스트

- [ ] `API-1` normalized event/base disposition, immutable evidence/filter decision, terminal/failure/release 공개 계약을 transport-agnostic 타입과 validation/defensive-copy helper로 구현한다.
- [ ] 계획에 명시한 local compile 검증 명령을 fresh cache 조건으로 실행하고 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 채우고 review-ready를 보고한 뒤 현재 실행을 종료한다. code-review를 실행하거나 다음 에이전트를 시작하지 않는다.

### [API-1] Transport-agnostic event와 terminal 계약

- 문제:
  - `proto/iop/runtime.proto:37-58`은 wire별 `RunEvent`/tunnel kind만 정의하고 공통 lifecycle disposition이 없다.
  - `apps/node/internal/runtime/types.go:47-69,238-263`은 Node internal 타입이라 `packages/go`가 import할 수 없다.
  - `apps/edge/internal/openai/chat_stream_session.go:249-272`와 `provider_tunnel.go:189-268`이 각자 event/terminal switch를 소유한다.
- 해결 방법:
  - `packages/go/streamgate`에 앱 internal import가 없는 타입 계약을 만든다.
  - public struct의 map/slice/string snapshot은 constructor에서 복사하고 accessor도 복사본을 반환한다. mutable map/slice를 직접 노출하지 않는다.
  - `response_start → hold`, text/reasoning/tool release 후보, success terminal 후보, provider error terminal 후보의 base table을 validation 가능한 함수로 고정한다.
  - `EvidenceBatch`는 normalized event, channel별 pending/look-behind, staged response-start, terminal flag, commit state를 private field로 보존한다.
  - decision/outcome은 `not_applicable_for_epoch`와 `deferred_by_requirement`를 별도 enum으로 유지하고 `RecoveryIntent`에는 strategy/typed directive/sanitized reason/priority만 허용한다.
  - `FailureCause`에는 stage/code/consumer/filter/rule id만 두고 chain은 append 시 최대 4개로 bounded한다. `TerminalResult`는 success/error와 안전한 external descriptor를 한 번에 전달한다.
  - `ReleaseSink`는 `context.Context`와 typed response-start/release/terminal payload를 받아 `CommitState`를 반환하는 host interface로 정의하되 실제 상태 머신은 후속 Task로 남긴다.
- Before (`packages/go/streamgate`):

```text
directory does not exist
```

- After (public shape 기준):

```go
package streamgate

import "context"

type EventKind string
type BaseEventDisposition string
type CommitState string

type ReleaseSink interface {
	CommitResponseStart(context.Context, ResponseStart) (CommitState, error)
	Release(context.Context, ReleaseEvent) (CommitState, error)
	CommitTerminal(context.Context, TerminalResult) (CommitState, error)
}
```

- 수정 파일 및 체크리스트:
  - [ ] `packages/go/streamgate/event.go`: event/channel/response-start/tool/terminal/provider-error types, constructors, base disposition
  - [ ] `packages/go/streamgate/filter_contract.go`: immutable `EvidenceBatch`, decision/outcome, sanitized evidence, `RecoveryIntent`
  - [ ] `packages/go/streamgate/terminal.go`: commit state, release payload, failure cause chain, external descriptor, terminal result, `ReleaseSink`
- 테스트 작성: 이 child에서는 작성하지 않는다. 신규 공개 API의 정상/경계 테스트는 `03+01_event_contract_unit_tests`가 담당한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/go/streamgate/event.go` | API-1 |
| `packages/go/streamgate/filter_contract.go` | API-1 |
| `packages/go/streamgate/terminal.go` | API-1 |

## 최종 검증

1. `gofmt -d packages/go/streamgate/event.go packages/go/streamgate/filter_contract.go packages/go/streamgate/terminal.go`
   - 기대 결과: stdout 없음
2. `go test -count=1 ./packages/go/streamgate`
   - 기대 결과: 새 production 계약 package compile PASS
3. `git diff --check -- packages/go/streamgate`
   - 기대 결과: stdout 없음

검증 결과를 review stub에 기록하고 review-ready를 보고한 뒤 현재 실행을 종료한다. code-review를 실행하거나 다음 에이전트를 시작하지 않는다.
