# SDD: Stream Evidence Gate Core ## 위치 - Milestone: [Milestone 문서](../../../phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md) - Phase: [PHASE.md](../../../phase/knowledge-tool-optimization-extension/PHASE.md) ## 상태 [승인됨] ## SDD 잠금 - 상태: 해제 - 사용자 리뷰: 없음 - 잠금 항목: 없음 ## 문제 / 비목표 - 문제: 여러 stream consumer가 raw protocol parser, bounded tail, response-start staging, release/terminal commit과 recovery를 자체 구현하면 repeat loop, tool-call syntax, missing tool-call, schema, workflow terminal replacement마다 byte 경계·eager HTTP commit·중복 `[DONE]`·post-commit 복구 안전성이 달라진다. 또한 filter fan-out 동안 provider event를 계속 무제한 적재하거나 각 consumer가 자체 로그만 남기면 메모리 경계와 같은 request timeline의 decision을 보장할 수 없다. Core는 normalized event 이후의 hold/release, single-flight evaluation, strategy별 recovery와 sanitized observation mechanics를 한 번만 제공해야 한다. - 비목표: - raw OpenAI SSE, CLI JSON, agent-family protocol을 직접 parse한다. - 반복/문법/tool 필요성 같은 semantic detector 또는 LLM judge를 결정한다. - filter가 개별 retry loop/counter, raw request mutation, provider selection 또는 dispatch를 소유한다. - provider-pool의 후보 선택 알고리즘이나 filter별 corrective prompt의 의미를 Core가 결정한다. - 기본 rolling 경로에서 전체 응답을 buffer하거나 terminal gate를 hard bound 없이 운영하거나 시간 경과만으로 미검증 event를 release한다. - Edge provider-pool 또는 Node runtime의 구체 타입을 공통 Core package가 import한다. - cross-request 오류를 사건으로 저장·중복 집계하거나, 연결된 소스 저장소를 LLM으로 분석해 수정 제안·프로젝트 작업 문서·사용자 승인·변경 요청·병합·배포를 조율한다. ## Source of Truth | 영역 | 기준 | 메모 | |------|------|------| | Roadmap | [Milestone 문서](../../../phase/knowledge-tool-optimization-extension/milestones/stream-evidence-gate-core.md) | 범위, consumer, 완료 evidence 기준 | | Code | `packages/go/streamgate`, `packages/go/config`, `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/node/internal/runtime`, `apps/node/internal/adapters/openai_compat`, `apps/node/internal/adapters/cli` | transport-agnostic Core와 host codec/rebuilder/dispatcher/release sink의 책임 경계 | | Contract | [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md) | OpenAI caller surface는 기존 계약을 유지하고 Core는 내부 normalized event만 소비한다 | | Config Contract | [edge-config-runtime-refresh.md](../../../../agent-contract/inner/edge-config-runtime-refresh.md) | limit 정책의 실제 config field/default/validation/refresh 분류를 구현할 때 함께 갱신한다. 현재 active 계약에 미구현 field를 선반영하지 않는다. | | User Decision | 현재 사용자 요청 | 기본 500-rune evidence hold, no time-based release, 공통 Core 분리, model/provider별 filter on/off, 동일 immutable evidence batch의 활성 filter 병렬 평가와 all-complete barrier, filter intent를 하나의 공통 RecoveryPlan으로 재조립·재admission하는 Coordinator를 같은 Milestone에 두는 방향이 확정됐다. consumer별 외부 보조 준비는 filter barrier 안이 아니라 plan 선택/current attempt 종료 뒤 optional `RecoveryPlanPreparer`로 직렬화한다. terminal failure는 bounded sanitized 원인 사슬을 내부에 보존하고 endpoint host가 외부 OpenAI-compatible 오류로 한 번만 평탄화한다. cross-request 오류 사건의 중복 집계와 LLM 기반 소스 분석·수정 제안·승인·변경 요청·병합·배포는 Core 밖의 별도 범용 플랫폼으로 프로젝트화한다. | ## State Machine | 상태 | 진입 조건 | 다음 상태 | 근거 | |------|-----------|-----------|------| | `request_bound` | host가 configured byte limit 안의 lossless ingress snapshot, registry/config generation, required filter capability와 recovery budget snapshot을 고정했다 | `attempt_dispatching`, `terminal_error` | 같은 request의 recovery cycle이 같은 policy, strategy budget과 request 전체 hard cap을 사용하고 oversize는 dispatch 전 종료 | | `attempt_dispatching` | 최초 request 또는 rebuilt request가 준비됐다 | `filter_resolving`, `terminal_error` | host `AttemptDispatcher`가 admission을 한 번 수행해 actual model/provider/execution path/event source/controller를 반환 | | `filter_resolving` | attempt의 실제 model/provider/capability가 확정됐다 | `normalized`, `terminal_error` | 고정된 registry snapshot으로 active filter set과 hold/failure policy를 attempt-local resolve. replace-attempt는 pending/look-behind를 초기화하고 continuation은 committed look-behind/release cursor를 보존 | | `normalized` | codec이 raw provider/agent event를 typed normalized event로 변환했다 | `response_staging`, `evidence_holding`, `terminal_evaluating`, `terminal_error` | response-start/text/reasoning/tool/terminal/error event | | `response_staging` | provider response-start 또는 protocol opening event가 도착했지만 safe output은 없다 | `evidence_holding`, `terminal_evaluating`, `terminal_error` | status/header/opening role을 `ReleaseSink`에 쓰지 않고 attempt-local staging | | `evidence_holding` | hold mode가 요구한 rolling rune, terminal 또는 fragment completion evidence가 아직 부족하다 | `filter_evaluating`, `terminal_evaluating`, `terminal_error` | channel별 rolling/terminal/fragment mode와 hard bound. 시간은 release 조건이 아님 | | `filter_evaluating` | subscribed event/hold trigger가 준비되거나 terminal batch가 확정됐다 | `arbitrating`, `terminal_error` | event-applicable/trigger-ready filter는 병렬 실행하고, subscribed event가 없으면 `not_applicable_for_epoch`, release-blocking trigger 미충족이면 `deferred_by_requirement`로 표시한다. complete outcome set을 모으는 동안 다음 ingress는 bounded backpressure | | `arbitrating` | base event disposition과 모든 active filter outcome이 준비됐다 | `releasing`, `evidence_holding`, `recovery_planning`, `terminal_error` | deterministic single action. blocking deferred는 hold, eligible recovery는 candidate를 대체하고 unmatched provider error는 base terminal 유지 | | `recovery_planning` | Arbiter가 하나의 `RecoveryIntent`를 선택했다 | `attempt_aborting`, `terminal_error` | strategy budget과 request 전체 recovery hard cap, cancel, commit/terminal/tool side effect, strategy eligibility, same-plan re-entry guard를 확인하고 preparer/continuation에 필요한 bounded request-local snapshot과 cursor를 immutable하게 고정한다 | | `attempt_aborting` | recovery eligibility가 통과했다 | `plan_preparing`, `rebuilding`, `terminal_error` | 현재 attempt를 idempotent cancel/close하고 실패/timeout이면 보조 plan 준비나 다음 attempt를 시작하지 않음 | | `plan_preparing` | 이전 attempt ownership이 종료됐고 선택된 plan이 consumer별 보조 directive 준비를 요구한다 | `rebuilding`, `terminal_error` | optional `RecoveryPlanPreparer`를 plan/idempotency key당 최대 1회, bounded deadline으로 호출한다. filter barrier 밖에서 실행하고 실패/timeout이면 fallback 없이 terminal | | `rebuilding` | 이전 attempt ownership이 종료됐고 보조 plan 준비가 완료됐거나 필요 없다 | `attempt_dispatching`, `terminal_error` | lossless ingress/continuation snapshot과 endpoint/family `RequestRebuilder`; cycle당 rebuilt request 하나 | | `releasing` | Arbiter가 safe prefix 또는 protocol-safe replacement를 허용했다 | `stream_open`, `evidence_holding`, `terminal_evaluating` | 첫 release라면 staged response-start/opening event를 먼저 한 번 commit하고 safe event를 순서대로 전송 | | `stream_open` | downstream transport/opening event 또는 body가 commit됐다 | `evidence_holding`, `terminal_evaluating`, `recovery_planning`, `done`, `terminal_error` | 이미 전송한 event rollback/exact/schema replay 금지. safe-prefix continuation 또는 pre-terminal replacement만 capability와 side-effect guard 아래 허용 | | `terminal_evaluating` | terminal event가 evidence 충족 전 또는 후 도착했다 | `filter_evaluating`, `terminal_error` | terminal flag가 포함된 final `EvidenceBatch`, all-filter evaluation | | `terminal_error` | provider idle/error, hard buffer overflow, blocking filter failure, recovery ineligible 또는 post-terminal error가 발생했다 | `done` | uncommitted이면 staged response를 버리고 host error status/body, stream-open이면 protocol terminal error만 한 번 전송 | | `done` | terminal sequence 또는 terminal error가 downstream에 확정됐다 | 없음 | terminal event 1회 | ## Interface Contract - 계약 원문: [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md) - 입력: - `NormalizedEvent`: codec이 생성한 `response_start`, `text_delta`, `reasoning_delta`, `tool_call_fragment`, `terminal`, `provider_error` 중 하나다. `response_start`는 status와 host가 allowlist로 정규화한 header metadata를 포함한다. hop-by-hop header와 body 변환 시 무효가 되는 `Content-Length`는 staging/release 대상에서 제외한다. Core는 raw wire bytes나 caller 제품명을 받지 않는다. - `BaseEventDisposition`: Core가 event lifecycle에서 만드는 `hold`, `release_candidate`, `terminal_success_candidate`, `terminal_error_candidate`다. response-start는 hold, user-facing safe delta는 release candidate, normal terminal은 success terminal candidate, provider error는 error terminal candidate다. filter가 매칭되지 않아도 이 기본 의미는 사라지지 않는다. - `StreamTraceContext`: runtime이 이미 가진 내부 `request_id`, `run_id`, provider id, model id correlation이다. Core는 새 public field를 만들지 않고 이 값을 observation에 전파한다. - `FilterContext`: immutable request-local config generation, attempt/epoch, environment, endpoint/family, model group/actual model/provider/execution path, stream, commit/terminal/tool-side-effect state와 trace correlation이다. - `FilterRegistrySnapshot`: request 시작 시 고정한 stable filter id별 구현과 config generation이다. admission 전 `RequiredFilterCapabilities`로 필수 후보를 제한하고 actual target이 정해진 각 attempt에서 environment/model/provider/capability별 active set, priority, hold mode와 `blocking|observe_only` enforcement를 resolve한다. config reload는 다음 request부터 반영하고 현재 request의 retry에는 섞지 않는다. - `EvidenceBatch`: 같은 evaluation epoch의 normalized event snapshot, channel별 pending, bounded committed look-behind, staged response-start metadata, terminal 여부와 commit state를 read-only accessor로 제공한다. 모든 trigger-ready filter가 같은 logical snapshot을 읽고 mutable map/slice 참조를 공유하지 않는다. replace-attempt는 buffer를 초기화하고 continuation은 committed look-behind/release cursor만 보존하며 observation/log로 원문을 전달하지 않는다. - `stream_hold.evidence_runes`: 기본 500. environment/model/provider policy가 제공하는 Unicode rune evidence window다. - `FilterHoldRequirement`: 대상 channel, `none|rolling_window|terminal_gate|fragment_gate`, subscribed normalized event kinds, `evidence_runes`, terminal/fragment trigger와 `max_buffer_runes` hard bound다. `none` event-only filter는 channel release를 막지 않는다. Core는 channel별 strongest blocking requirement를 합성하고 filter가 별도 raw buffer나 unbounded hold를 만들지 못하게 한다. - `Filter`: Go interface로 stable `ID`, attempt-level `Applies`, `HoldRequirement`/event subscription, synchronous `Evaluate(context.Context, FilterContext, EvidenceBatch) (FilterDecision, error)`를 제공한다. 공통 context는 caller cancel과 bounded deadline을 전달하며 구현은 취소를 준수해야 한다. Gate Coordinator가 fan-out/수집을 관리하고 구현은 concurrency, request mutation, submit을 소유하지 않는다. - `FilterDecision`: `pass`, `observe`, `violation`, `fatal`, `replacement` 중 하나다. `consumer_id`, stable `filter_id`, `rule_id`, sanitized evidence와 선택적 `RecoveryIntent`를 포함한다. evidence 수집 종료점은 static hold requirement가 결정하므로 filter가 평가 뒤 무기한 `need_more_evidence`로 hold를 연장하지 않는다. - `FilterOutcome`: active filter마다 `evaluated(FilterDecision)`, `evaluation_error`, `not_applicable_for_epoch`, `deferred_by_requirement` 중 하나다. not-applicable은 subscribed event가 없어 release를 막지 않는 outcome이고, deferred는 static blocking trigger 미충족이라 pass로 간주하지 않는다. terminal/hard bound에서 계속 deferred면 invalid/fail policy로 수렴한다. - `FilterFailurePolicy`: Registry가 filter 외부에서 정한다. blocking filter error/deadline은 `fatal`로, observe-only filter error/deadline은 `observe_error` outcome으로 정규화되며 둘 다 all-complete 결과와 observation에 포함된다. caller cancel/host shutdown은 barrier를 취소하고 no-release로 종료한다. - `FailureCause`: terminal failure의 내부 원인 한 단계다. `stage`, 안정 `code`, consumer/filter/rule id만 가지며 raw stack trace, provider body/endpoint, user prompt, output/reasoning 원문, tool args/result, 인증 정보는 가질 수 없다. `FailureCauseChain`은 request-local 최대 4단계이고 consumer와 Coordinator가 append-only로 조립한다. - `RecoveryIntent`: `exact_replay`, `continuation_repair`, `schema_repair` strategy id와 typed repair directive, sanitized reason, filter priority를 가진다. raw body/auth, attempt counter, provider 선택 또는 실행 함수는 포함하지 않는다. - `RecoveryBudget`: immutable request-local `max_recovery_attempts_total`, strategy별 limit과 사용량을 가진다. 전체 cap 기본값이자 MVP 절대 상한은 최초 provider 실행을 제외한 3회이며 request 시작 시 policy snapshot으로 고정한다. policy는 `0..3`에서 낮출 수만 있고 3 초과는 config load/refresh validation error다. exact/continuation/schema recovery dispatch를 모두 합산하고 각 strategy cap보다 우선한다. 어느 cap이든 소진되면 새 plan을 만들지 않고 terminal로 수렴하며 filter/strategy를 바꿔 우회할 수 없다. - `IngressSnapshot`: endpoint/family rebuilder가 unknown caller field를 손실 없이 보존할 수 있는 request-local snapshot이다. `max_ingress_snapshot_bytes` 기본값이자 MVP 절대 상한은 16 MiB이며 policy는 이보다 낮출 수만 있고 request 시작 시 고정한다. host는 `io.ReadAll` 전에 `http.MaxBytesReader` 또는 동등한 limit+1 판정을 raw body에 적용한다. OpenAI JSON endpoint는 raw body를 canonical lossless representation으로 사용하고, 다른 codec의 lossless tree는 byte-preserving round-trip fixture가 있을 때만 허용한다. `retained_bytes`는 runtime heap 추정치가 아니라 request가 소유한 backing byte/string buffer의 보수적 logical byte 합이며 shared backing은 한 번만 센다. SnapshotBuilder는 bounded typed semantic view를 더한 직후, RequestRebuilder는 임시 output 할당 전후의 current peak를 다시 검사하고 canonical full copy를 둘 이상 유지하지 않는다. 따라서 raw body가 pre-read gate와 같아도 typed view를 더한 total retained limit을 넘으면 수용하지 않는다. ingress/rebuild 초과는 snapshot/filter/log/cross-request cache에 원문을 남기거나 provider/recovery budget을 소비하지 않고 terminal로 끝내며 raw auth header도 보존하지 않는다. request 종료/cancel/overflow에서는 retained object를 release한다. - `AttemptDispatcher`: host가 구현하며 rebuilt request와 required capability를 기존 admission에 전달해 actual model/provider/execution path, normalized event source와 `AttemptController`를 가진 `AttemptBinding` 하나를 반환한다. concrete provider selection/auth/model rewrite 알고리즘은 Core 밖에 있다. - `AttemptController`: 현재 attempt의 provider transport cancel/close와 ownership release를 idempotent하게 수행한다. Core가 plan 선택 시 고정한 request-local preparation/continuation snapshot과 cursor는 지우지 않는다. recovery는 성공적인 abort 또는 명시적 already-closed 확인 전에 preparer나 다음 dispatch를 호출하지 않는다. - `RecoveryPlanPreparer`: 선택된 plan이 consumer별 bounded directive 준비를 요구할 때 host/consumer가 구현하는 optional interface다. Core가 all-complete arbitration과 eligibility 판정, current attempt ownership 종료 뒤 plan/idempotency key당 최대 1회 bounded deadline으로 호출한다. 입력은 plan 선택 시 고정한 request-local bounded preparation snapshot이며 구체 범위는 consumer 계약이 정하고 observation/log에는 복제하지 않는다. preparer는 provider admission/dispatch, retry/fallback, budget mutation을 소유하지 않고 성공 시 typed directive만 보강한다. 실패는 bounded cause를 가진 terminal로 수렴하며 outbound recovery dispatch가 없으므로 request recovery budget을 소비하지 않는다. snapshot은 성공·실패·cancel 뒤 release한다. - `RequestRebuilder`: host/endpoint/family가 bounded lossless ingress snapshot, selected plan과 continuation snapshot을 새 request로 조립한다. exact replay는 admission rewrite 전 caller raw body를 보존하고 continuation/schema만 typed patch를 적용한다. rebuild 전 예상 peak와 할당 후 실제 peak `retained_bytes`를 모두 확인하고 한도를 넘으면 기존 snapshot과 임시 output을 즉시 release한 뒤 dispatch하지 않는다. - `ReleaseSink`: host transport가 staged response-start, safe event, replacement, terminal/error를 순서대로 commit하고 현재 commit state를 반환한다. 첫 commit 뒤 status/header를 다시 쓰거나 opening role/event를 중복 전송하지 않는다. - 출력: - `ReleaseEvent`: filter 통과 safe prefix 또는 허용된 replacement다. - `CommitState`: `transport_uncommitted`, `stream_open`, `terminal_committed`다. HTTP status/header flush, SSE/agent opening event 또는 첫 body 중 가장 이른 user-visible write가 `stream_open`이다. exact/schema replay는 uncommitted에 한정하고 continuation/replacement는 별도 capability matrix를 따른다. - `TerminalResult`: terminal event 또는 terminal error를 한 번만 전송한다. terminal error는 선택적 bounded `FailureCauseChain`과 외부 오류의 `type`/`code`/안전한 message template/`param` descriptor를 가진다. Core는 이를 재해석하지 않고 `ReleaseSink`에 전달하며 endpoint codec이 [외부 오류 계약](../../../../agent-contract/outer/openai-compatible-api.md#오류-응답)으로 직렬화한다. - `ArbitrationResult`: base disposition과 모든 filter outcome을 합성한 `release`, `hold`, `terminal`, `recover`, `replacement` 단일 action이다. hold는 blocking deferred가 hard bound 전일 때만, recover는 eligible intent가 base candidate를 대체할 때만 선택한다. unmatched error는 terminal을 유지하고 같은 epoch에서 action/dispatch를 둘 이상 만들지 않는다. - `RecoveryPlan`: 선택된 strategy, `replace_attempt|continue_stream` resume mode, contributing filter/rule ids, plan/idempotency key, strategy attempt와 request 전체 recovery attempt, 각 limit, typed directive, optional bounded preparation snapshot reference와 preparer id/status, commit/side-effect eligibility와 continuation cursor를 포함한다. RecoveryPlan Coordinator만 생성하며 preparer 성공 전에는 rebuild/dispatch 가능한 final plan으로 표시하지 않는다. snapshot 원문은 observation에 직렬화하지 않는다. - `RebuiltRequest`: `RequestRebuilder`가 immutable ingress snapshot과 plan directive로 만든 endpoint별 새 attempt다. 기존 admission 경로가 provider를 다시 선택하고 auth를 재계산한다. - `FilterObservation`: filter evaluation 또는 lifecycle/coordinator decision마다 emit하는 내부 timeline event다. 기존 request/run/provider/model correlation과 config generation, attempt/epoch, consumer/filter/rule id, hold mode, event kind, effective/pending rune, decision/failure policy, commit state, terminal reason, plan id/strategy/resume mode/shared attempt, preparer id/status/deadline outcome, 최대 4개의 sanitized cause code와 sanitized evidence를 담는다. 기존 sink가 저장·보존을 소유하며 preparer input/output, raw output/prompt/tool args/result/auth와 delta 원문은 담지 않는다. - 금지: - `stream_hold.evidence_runes`가 부족한 event를 시간 경과만으로 release하지 않는다. - provider idle/timeout 시 pending tail을 flush하지 않는다. - Core가 raw protocol parser, caller-specific policy, cross-request cache, LLM judge를 소유하지 않는다. - 이미 전송한 event를 rollback하거나 exact/schema replay하지 않는다. continuation/replacement도 기존 bytes를 suppress하지 않고 terminal/tool side effect 뒤에는 실행하지 않는다. - 활성 filter outcome이 모두 수집되기 전에 release/retry/replace하지 않는다. filter error/deadline을 silent pass로 간주하지 않는다. - 하나의 evaluation epoch를 겹쳐 실행하거나 evaluation 중 provider event를 unbounded queue에 적재하지 않는다. - provider response status/header 또는 opening role/event를 safe release 전에 eager commit하지 않는다. - terminal gate hard bound 초과 시 partial output을 release하지 않는다. - 현재 attempt cancel/close ownership이 끝나기 전에 다음 recovery attempt를 dispatch하지 않는다. - filter가 request를 직접 mutate하거나 retry loop/counter, provider admission, request submit을 실행하지 않는다. - 선택된 recovery plan의 후처리인 `RecoveryPlanPreparer`/translator를 filter evaluation 또는 all-complete barrier 안에서 실행하지 않는다. consumer가 별도 Filter로 선언한 bounded LLM judge까지 금지하는 규칙은 아니지만, 해당 Filter는 자신의 invocation cap/deadline/failure policy를 지켜야 한다. preparer 실패를 재호출/fallback으로 우회하거나 recovery dispatch 없이 budget을 소비하지 않는다. - 같은 evaluation epoch/recovery cycle에서 둘 이상의 `RecoveryPlan` 또는 outbound retry를 실행하지 않는다. 단, request-local strategy budget과 request 전체 hard cap이 모두 남아 있고 새 attempt가 다시 violation을 반환하면 다음 cycle을 시작할 수 있다. - `max_recovery_attempts_total`을 3보다 높이거나 strategy별 limit 합으로 request 전체 cap을 우회하지 않는다. - request body를 unbounded `io.ReadAll`한 뒤 길이만 검사하지 않는다. `max_ingress_snapshot_bytes`를 넘는 요청이나 rebuilt request를 snapshot으로 보존하거나 provider에 dispatch하지 않는다. - OpenAI JSON raw body 외 표현을 byte-preserving 증거 없이 exact replay canonical source로 사용하거나 canonical lossless representation의 full copy를 둘 이상 유지하지 않는다. - `FilterObservation`에 raw output, raw request/prompt, tool args/result, 인증 정보 또는 caller 제품명을 넣지 않는다. - raw stack trace나 provider endpoint/body를 `FailureCauseChain`, `TerminalResult`, public error message, `FilterObservation`에 넣지 않는다. ## Acceptance Scenarios | ID | Milestone Task | Given | When | Then | |----|----------------|-------|------|------| | S01 | `evidence-tail` | rolling text delta가 pending에 누적되고 이전 safe prefix가 release됐다 | 기본 500 rune 또는 policy override 200 rune 증거가 충족돼 다음 batch를 만든다 | filter는 같은 크기의 bounded committed look-behind와 pending을 함께 보아 경계 반복을 판정하고 전체 응답을 buffer하지 않는다. replace-attempt는 초기화하고 continuation은 look-behind/cursor를 보존한다 | | S02 | `evidence-tail` | 한국어 UTF-8 multi-byte 문자열이 chunk 경계에서 나뉜다 | evidence rune을 계산한다 | byte가 아닌 Unicode rune으로 정확히 계산하고 깨진 문자 없이 release한다 | | S03 | `commit-boundary` | evidence 미충족 pending tail과 staged response-start가 있고 terminal event가 온다 | Core가 최종 all-filter evaluation을 한다 | safe response-start/output 또는 terminal error가 한 번만 결정되고 eager status/role은 없다 | | S04 | `commit-boundary` | evidence 미충족 pending tail 상태에서 provider idle timeout이 난다 | terminal error를 처리한다 | pending tail은 release하지 않고 terminal error만 전송한다 | | S05 | `commit-boundary` | Core가 safe prefix를 전송해 stream이 열렸다 | 이후 provider error 또는 filter violation이 난다 | already released event는 rollback/exact/schema replay하지 않고 continuation-safe가 아니면 terminal policy로 끝낸다 | | S06 | `policy-hook` | output validation, syntax gate, missing-tool gate, integrity filter, workflow terminal hook이 같은 Core를 소비한다 | 각 filter가 자기 detector decision과 선택적 `RecoveryIntent`를 반환한다 | semantic policy/intent는 filter별로 분리되고 request mutation/retry loop/rebuild/dispatch는 공통 Coordinator만 수행한다 | | S07 | `policy-hook` | rolling, schema terminal, tool fragment와 provider-error event-only filter가 같은 request에서 활성이다 | Core가 channel requirement와 event applicability outcome을 합성한다 | rolling은 조기 violation 가능, schema는 terminal 전 blocking deferred, tool은 fragment gate, 평상시 provider-error는 not-applicable로 content를 막지 않는다. duplicate buffer/commit은 없다 | | S08 | `filter-observation` | repeat guard와 syntax gate가 stable filter id 및 sanitized fingerprint/count/offset evidence를 제공한다 | Core가 filter와 coordinator lifecycle event를 emit한다 | 같은 request/run/provider/model timeline에서 config generation, attempt/epoch, staging, 각 decision/failure policy, plan/strategy/resume mode를 추적할 수 있고 raw output/prompt/tool args는 없다 | | S09 | `event-contract` | OpenAI와 agent-family codec이 response-start를 포함한 서로 다른 raw event를 같은 normalized table로 변환한다 | transport-agnostic Core와 host ReleaseSink가 event를 처리한다 | Core가 raw parser나 Edge/Node internal type을 import하지 않고 typed event/host interface만으로 같은 lifecycle을 통과한다 | | S10 | `adoption-doc` | output validation, syntax, missing-tool, integrity, workflow consumer 문서가 Core를 참조한다 | consumer boundary와 observation/recovery mapping을 검토한다 | 모든 consumer가 Core link, own semantic policy/intent, stable filter id와 sanitized observation을 명시한다. recovery consumer는 strategy/request-total cap과 common RecoveryPlan을, ingress snapshot consumer는 bounded snapshot을 사용하고 raw buffer·snapshot/rebuild·retry loop를 중복 구현하지 않는다 | | S11 | `filter-registry` | request 중 config reload가 일어나고 recovery admission이 provider를 바꾸며 required filter capability가 있다 | registry가 request snapshot과 각 attempt actual target으로 active set을 resolve한다 | 같은 config generation을 유지하면서 provider별 set은 다시 계산되고 unsupported candidate는 dispatch 전 제외되며 disabled/duplicate filter는 평가되지 않는다 | | S12 | `parallel-evaluation` | ready filter, terminal-trigger filter, error-event-only filter가 있고 평가 중 provider event가 더 도착한다 | Gate Coordinator가 evaluated/deferred/not-applicable outcome과 bounded backpressure를 만든다 | complete outcome set 전 action하지 않고 blocking deferred는 pass가 아니며 event-only not-applicable은 release를 막지 않는다. race/overlap/unbounded queue가 없고 failure/cancel policy가 적용된다 | | S13 | `decision-arbiter` | release/error/terminal base disposition과 pass/not-applicable/deferred/violation/fatal/replacement outcome이 섞여 있다 | Arbiter가 결과를 합성한다 | unmatched provider error는 terminal, matched eligible error는 recovery, blocking deferred는 hold가 되고 priority/stable id로 single action만 선택한다 | | S14 | `recovery-coordinator` | 두 filter가 같은 epoch에 retryable violation을 반환하고 strategy budget과 기본/절대 상한 3회의 request 전체 recovery cap이 남아 있다 | Coordinator가 intent를 dedupe하고 commit/side-effect별 eligibility와 두 budget을 판정한다 | cycle마다 plan/idempotency key/dispatch 하나만 만들고 exact/schema는 uncommitted에, continuation은 safe stream-open에만 허용한다. 0/1/3회 policy는 적용되고 4 이상 config는 거부되며 strategy 또는 전체 cap 소진, cancel/side-effect/re-entry 위반은 다른 strategy 우회 없이 terminal로 끝낸다 | | S15 | `request-rebuilder` | configured byte limit 안에서 unknown caller field가 있는 Chat/Responses raw snapshot과 exact/continuation/schema directive가 있다 | endpoint rebuilder와 host dispatcher가 새 attempt를 만든다 | OpenAI JSON raw body를 canonical source로 하나만 보존하고 bounded typed view/rebuild allocation을 계상한다. exact는 admission rewrite 전 caller body와 unknown field를 보존하고 typed patch만 적용한다. concrete model/auth는 admission에서 다시 계산되고 normalized↔tunnel path 전환도 새 `AttemptBinding`으로 이어진다 | | S16 | `vertical-slice` | model/provider on/off 가능한 `NoopFilter`와 test-only `InjectedViolationFilter`가 등록돼 있다 | staged response-start부터 filters, Arbiter, abort, Rebuilder, Dispatcher, ReleaseSink까지 한 사이클 돈다 | disabled/enabled pass, eager header/role 없음, injected violation one retry, simultaneous violation single retry, provider/path switch, failure policy, backpressure, exhausted terminal이 end-to-end로 검증된다 | | S17 | `commit-boundary` | provider가 response-start를 보낸 뒤 user body release 전에 filter-matched 500 body/error를 보낸다 | provider-error filter가 exact replay intent를 반환한다 | original status/header/role은 commit되지 않고 retry 성공 attempt의 response-start와 output만 한 번 노출된다 | | S18 | `recovery-coordinator` | 정상 text prefix가 이미 release된 뒤 repeat filter가 continuation intent를 반환한다 | Coordinator가 tool/terminal side effect가 없는 stream-open eligibility를 확인한다 | released prefix와 committed look-behind/cursor를 보존하고 새 attempt의 opening/prefix 중복 없이 같은 stream에 continuation만 이어 쓰며 exact/schema intent는 거부된다 | | S19 | `evidence-tail` | terminal-gate filter가 활성이고 provider output이 configured hard bound를 넘는다 | Core가 buffer overflow를 감지한다 | partial output이나 staged response-start를 release하지 않고 attempt를 취소한 뒤 terminal error로 끝낸다 | | S20 | `recovery-coordinator` | recovery가 선택됐지만 현재 attempt abort가 timeout 또는 실패한다 | Coordinator가 다음 dispatch를 준비한다 | next attempt를 시작하지 않고 terminal로 끝내 두 live provider attempt와 중복 side effect를 방지한다 | | S21 | `event-contract` | consumer가 `repetition_loop_detected` 뒤 auxiliary recovery step 실패를 뜻하는 `recovery_notice_translation_failed` 원인을 terminal로 올린다 | Core가 terminal result와 host ReleaseSink를 처리한다 | 원인 사슬은 최대 4개의 sanitized code로 보존되고, host는 endpoint별 외부 오류 하나만 전송한다. raw stack/provider body는 observation과 public output에 없다 | | S22 | `request-rebuilder` | ingress raw body가 pre-read limit을 넘거나 raw body가 gate와 같아도 typed view/rebuild 임시 output을 포함한 logical current peak retained size가 기본값/절대 상한 16 MiB 또는 작은 policy override를 넘는다 | host가 limit+1 overflow를 snapshot 생성 전에 판정하고 SnapshotBuilder/Rebuilder가 owned backing은 합산하되 shared backing은 한 번만 세어 typed view 추가 직후와 rebuild 할당 전후에 retained bytes를 다시 검사한다 | pre-read limit-1/limit은 body gate만 통과하고 limit+1은 즉시 거부된다. 이후 initial retained overflow는 endpoint host의 request-too-large 오류, rebuild overflow는 commit state에 맞는 recovery terminal error 하나로 끝난다. canonical raw body와 임시 object를 release하고 request-local cache·filter·로그에 남기지 않으며 provider dispatch/recovery budget 소비가 없고 16 MiB 초과 config는 거부된다 | | S23 | `recovery-coordinator` | all-complete Arbiter가 보조 directive 준비가 필요한 continuation plan 하나를 선택했다 | Coordinator가 bounded preparation/continuation snapshot과 cursor를 고정하고 current provider transport ownership을 종료한 뒤 registered `RecoveryPlanPreparer`를 실행한다 | abort가 snapshot을 지우지 않고 preparer는 filter barrier 밖에서 plan/idempotency key당 최대 1회와 bounded deadline을 지킨다. 성공 뒤에만 rebuild/dispatch하고 timeout/invalid/error는 재호출/fallback 없이 single terminal로 끝내며 outbound dispatch가 없으므로 recovery budget을 소비하지 않는다. snapshot은 모든 종료 경로에서 release된다 | ## Evidence Map | Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 | |----------|-------------------|------------------|---------------------------| | S01 | 200/500-rune policy, pending+look-behind bound, cross-boundary repeat, replace reset/continuation preserve tests | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `evidence-tail`, rolling threshold/bounded look-behind/recovery-mode assertion | | S02 | Korean multi-byte split chunk rune fixture | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `evidence-tail`, Unicode boundary assertion | | S03 | terminal-before-evidence with staged response-start fixture | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `commit-boundary`, no eager commit/single terminal assertion | | S04 | provider idle timeout before evidence fixture | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `commit-boundary`, pending-tail no-release assertion | | S05 | uncommitted/stream-open/terminal-committed error and strategy matrix | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `commit-boundary`, no rollback/exact-schema post-open prohibition assertion | | S06 | OpenAI and agent-family fake codec consumer decision/intent matrix, direct-submit prohibition check | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `policy-hook`, consumer semantic/intent ownership and common recovery ownership assertion | | S07 | rolling/terminal/fragment mixed-mode composition and channel-bound tests | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `policy-hook`, strongest bounded requirement/single buffer assertion | | S08 | multi-consumer observation timeline with config/attempt/epoch/staging/failure/resume mode and raw absence | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `filter-observation`, one raw-free request timeline assertion | | S09 | OpenAI/agent-family fake codec response-start/event table and host-interface package dependency test | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `event-contract`, transport-agnostic typed contract/no internal import assertion | | S10 | five dependent Milestone Core-link/filter-id/sanitized-observation/limits/common-RecoveryPlan mapping review | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `adoption-doc`, consumer documentation/no duplicate raw buffer/snapshot/rebuild/retry loop assertion | | S11 | config-generation isolation, policy precedence, required capability admission, provider-switch and duplicate-id tests | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `filter-registry`, snapshot/attempt re-resolution/preflight assertion | | S12 | evaluated/deferred/not-applicable mixed outcomes, barrier, race, backpressure, failure/cancel fixtures | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `parallel-evaluation`, complete outcome set/deferred-vs-event-only/single-flight assertion | | S13 | base disposition + all outcome arbitration matrix, matched/unmatched error and deterministic repeats | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `decision-arbiter`, default terminal preservation/single-action assertion | | S14 | simultaneous intent, commit-strategy matrix, 0/1/3회 및 4 이상 config validation, bounded cycles, strategy/request-total cap 교차 소진, cancel/side-effect/re-entry fixtures | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `recovery-coordinator`, one plan/dispatch per cycle, absolute total-cap 우선과 no strategy bypass assertion | | S15 | Chat/Responses bounded raw canonical exact/continuation/schema rebuild, byte-preserving alternative codec, unknown field, auth safety and path-switch tests | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `request-rebuilder`, caller-body equality/retained-byte accounting/host admission/AttemptBinding assertion | | S16 | staged-start `NoopFilter`/`InjectedViolationFilter` vertical integration with path switch and backpressure | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `vertical-slice`, request-to-recovery-to-release one-cycle assertion | | S17 | response-start + matched 500 before body, successful replay response exposure fixture | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `commit-boundary`, original start hidden/final start once assertion | | S18 | stream-open repeat continuation, preserved cursor/look-behind and duplicate opening/prefix suppression fixture | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `recovery-coordinator`, safe post-open continuation/exact-schema rejection assertion | | S19 | terminal-gate hard buffer overflow and attempt cancel fixture | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `evidence-tail`, fail-closed/no partial release assertion | | S20 | attempt abort timeout/failure before next dispatch fixture | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `recovery-coordinator`, no overlapping live attempt assertion | | S21 | bounded failure-cause chain, one terminal result, Chat/Responses host error codec fake fixture | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `event-contract`, raw-free cause chain/endpoint-specific single-error assertion | | S22 | host pre-read raw-body limit-1/limit/limit+1, exact-limit body+typed-view overflow, configured smaller limit, rebuild pre/post-allocation peak overflow, initial-vs-rebuild terminal and release/raw-absence fixtures | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `request-rebuilder`, body-gate-vs-total-retained 구분/no unbounded read/no dispatch/no budget/no raw retention and 16 MiB absolute-cap assertion | | S23 | all-complete snapshot freeze, transport-only abort-before-prepare, plan/idempotency-key single call, success/timeout/invalid/error, no-fallback/no-budget/snapshot-release fixtures | `agent-task/m-stream-evidence-gate-core/...` | `Roadmap Completion`에 `recovery-coordinator`, preparer의 filter-barrier 외부 실행/one-shot deadline/success-only rebuild/request-local snapshot lifetime assertion | ## Cross-repo Dependencies - 없음 ## Drift Check - [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다. - [x] Evidence Map이 code-review/complete.log에서 검증 가능하다. - [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다. - [x] 사용자 리뷰가 필요한 항목은 없음으로 확인했다. ## 사용자 리뷰 이력 - 2026-07-23: 사용자 요청으로 evidence-based pending tail을 공통 stream mechanics로 분리했다. 기본 500-rune hold와 time-based release 금지는 확정됐다. - 2026-07-23: 사용자 요청으로 여러 consumer filter의 decision을 같은 request timeline에서 추적할 수 있는 raw-free `FilterObservation` 공통 계약을 추가했다. - 2026-07-23: Coordinator를 별도 Milestone으로 분리하지 않고 이 Core Milestone 안의 독립 컴포넌트로 두기로 했다. 활성 filter는 동일 immutable evidence batch를 병렬 평가하고 all-complete barrier 뒤 결과를 합성하며, filter는 model/provider별 on/off 가능한 공통 interface 구현체로 detector와 `RecoveryIntent`만 제공한다. 공통 Coordinator가 하나의 plan, strategy/request-total budget, endpoint별 request rebuild와 single re-admission을 소유한다. - 2026-07-23: 재검토에서 HTTP/SSE response-start staging, rolling/terminal/fragment hold 분리, single-flight backpressure, request-local config snapshot, strategy별 post-commit eligibility와 host adapter interface를 보강했다. 특히 exact/schema replay는 uncommitted에 한정하고, 반복 continuation은 이미 보낸 safe prefix를 보존한 stream-open 복구로 명시했다. - 2026-07-23: 안정성 재검토에서 strategy별 budget을 합산하는 최초 실행 제외 기본값/절대 상한 3회의 request 전체 recovery hard cap과 기본값/절대 상한 16 MiB lossless ingress snapshot retained-byte hard limit을 추가했다. OpenAI JSON raw body를 canonical source로 하나만 보존하고 host pre-read limit을 적용하도록 확정했다. - 2026-07-23: consumer별 번역/보조 호출이 병렬 filter barrier 안에서 반복 실행되지 않도록 optional `RecoveryPlanPreparer`를 all-complete/Arbiter와 current attempt ownership 종료 뒤의 one-shot 단계로 추가했다. preparer 실패는 fallback·recovery dispatch·budget 소비 없이 terminal로 수렴한다. - 2026-07-23: cross-request 오류 수집·안전한 지문 기반 중복 집계·연결 소스 분석·수정 제안·프로젝트 작업 문서·사용자 승인·변경 요청·병합·배포·재발 확인은 별도 범용 플랫폼으로 프로젝트화하기로 확정했다. Core는 동기 요청 경로에서 이 루프를 실행하지 않고 raw-free terminal/observation event를 전달하는 경계만 소유한다. ## 작업 컨텍스트 - 표준선: `packages/go/streamgate`는 codec과 semantic filter 사이의 transport-agnostic boundary다. Gate Coordinator가 state/single-flight fan-out/all-complete barrier를, Arbiter가 single action을, RecoveryPlan Coordinator가 strategy eligibility, strategy별 budget, request 전체 hard cap과 abort 뒤 optional one-shot `RecoveryPlanPreparer` 호출 순서를 담당한다. raw parser, concrete admission/auth/model rewrite, preparer 의미와 response writer는 host/consumer interface 뒤에 둔다. - 구현 접점: Edge `writeProviderTunnelResponse`의 provider response-start header flush와 `streamChatCompletion`의 opening role write는 현재 eager commit 지점이므로 `ReleaseSink` 뒤로 이동한다. `streamBufferedChatCompletion`/`completeChatCompletion`의 tool-validation retry loop는 새 Coordinator의 첫 migration 대상이며 기존 loop와 공존시키지 않는다. - 표준선: default rolling hold는 text/reasoning release barrier다. terminal 전체가 필요한 schema/judge는 explicit bounded `terminal_gate`, incomplete tool fragment는 `fragment_gate`를 선언한다. rolling look-behind는 사용자 전송을 지연시키지 않고 cross-boundary 판정에만 쓰며 terminal gate를 제외한 기본 경로는 content 전체를 보관하지 않는다. - 표준선: response status/header와 opening role/event도 user-visible commit이다. 첫 safe release 전에는 staging하고, stream-open 이후 exact/schema replay는 금지한다. continuation recovery는 기존 safe prefix와 look-behind/release cursor를 보존하고 terminal/tool side effect 전 같은 downstream stream에 이어 쓰는 별도 resume mode다. - 표준선: request 시작 시 registry/config generation을 고정한다. recovery가 provider/path를 바꾸면 같은 snapshot으로 attempt-local active set을 다시 resolve하고, required filter capability는 admission 후보 조건으로 전달한다. - 표준선: Core는 관측 envelope와 request/run/provider/model correlation을 emit하지만 filter 의미나 observation 저장·보존을 소유하지 않는다. filter는 자기 stable filter/rule id와 sanitized evidence만 채우며, 기존 observability sink가 모든 filter/coordinator decision을 하나의 `FilterObservation` timeline으로 남긴다. - 표준선: 별도 오류 수정 플랫폼은 `오류 발생 → 수집·민감정보 제거 → 안정 지문 기반 사건 생성/중복 count → 실행 버전·커밋·저장소 연결 → 비동기 LLM 분석과 재현 근거 → 중립 수정 제안 → 프로젝트별 작업 문서 → 사용자 승인 → 격리 수정·테스트·독립 검토 → 변경 요청 → 병합·배포 → 재발 관찰` 생명주기를 소유한다. 같은 오류는 새 작업을 만들지 않고 canonical 사건의 count와 first/last seen·영향 버전만 갱신한다. - 표준선: filter interface는 context-aware synchronous pure evaluation 형태로 유지하고 Gate Coordinator가 goroutine fan-out/수집, single-flight backpressure와 cancel/deadline fail policy를 관리한다. 이로써 filter 구현이 concurrency, request mutation, attempt counter, provider admission을 각각 재구현하지 않는다. - 표준선: 최초 vertical slice는 production semantic detector 대신 model/provider별 on/off 가능한 `NoopFilter`와 test-only `InjectedViolationFilter`로 공통 파이프라인 한 사이클을 먼저 검증한다. - 표준선: host는 ingress body를 읽기 전에 기본값/절대 상한 16 MiB의 `max_ingress_snapshot_bytes`를 적용한다. OpenAI JSON은 raw body를 canonical source로 하나만 보존하고 Core/Rebuilder가 typed view와 rebuild 임시 output까지 retained byte를 계상한다. limit 초과는 provider dispatch와 recovery budget 소비 전에 fail-closed한다. - 표준선: limit policy를 실제 config에 구현할 때 [Edge Config And Runtime Refresh Contract](../../../../agent-contract/inner/edge-config-runtime-refresh.md)에 field/default/range와 refresh 분류를 함께 반영한다. 현재 active contract에는 미구현 config field를 선반영하지 않는다. - 후속 SDD: [OpenAI-compatible 출력 검증 필터 SDD](../openai-compatible-output-validation-filters/SDD.md)