# Milestone: [output-02] OpenAI-compatible Incomplete Tool Call Syntax Gate

## 위치

- Roadmap: [ROADMAP.md](../../../ROADMAP.md)
- Phase: [PHASE.md](../PHASE.md)

## 목표

OpenAI-compatible provider stream이 terminal 상태로 닫혔지만 완성된 tool call과 raw/reasoning/content tool-call marker scanner 결과가 맞지 않는 케이스를 runtime에서 deterministic하게 판정한다.
이 Milestone은 `tools[]`나 `tool_choice` 존재 여부를 필수 증거로 삼지 않고, provider가 tool-call 문법을 시작했거나 완성한 흔적과 Edge가 조립한 `assembled_tool_call_count`의 불일치만 좁게 다룬다.
예: `assembled_tool_call_count=0`인데 `assembled_reasoning="<tool_call|>"`처럼 marker attempt가 남은 terminal 응답은 incomplete tool-call syntax로 분류한다.

## 상태

[계획]

## 승격 조건

- 없음

## 구현 잠금

- 상태: 잠금
- SDD: 불필요
- SDD 문서: 없음
- SDD 사유: 현재 범위는 provider 원문/조립 결과를 비교하는 runtime classification과 관측 필드이며, public API/proto/config schema를 확정 변경하지 않는다. 감지 후 retry/error stream 정책을 확장할 경우 별도 SDD 필요 여부를 재판정한다.
- 잠금 해제 조건: 아래 체크리스트
  - [ ] matcher가 의존할 marker grammar와 provider별 적용 범위가 정리되어 있다.
  - [ ] gate에 걸린 terminal 응답을 내부 retry로 처리할지 invalid-provider-output으로 처리할지 결정되어 있다.
  - [ ] content marker가 이미 downstream으로 흘렀을 때의 처리와 first-byte 전 buffer 필요 여부가 정리되어 있다.
- 결정 필요: 아래 체크리스트
  - [ ] 초기 marker set을 Gemma 계열 `<tool_call|>` 중심으로 시작할지, provider별 registry로 시작할지 결정한다.
  - [ ] `incomplete_at_eof`, `marker_attempt_count > assembled_tool_call_count`, `complete_marker_count > assembled_tool_call_count` 각각의 action을 결정한다.
  - [ ] matched response를 1회 internal retry로 돌릴지, tool-call syntax error로 즉시 닫을지 결정한다.
  - [ ] reasoning-only marker와 content marker를 같은 severity로 볼지 결정한다.
  - [ ] gate 결과를 Pi/agent client가 볼 수 있는 error/metadata로 노출할지, Edge 로그/observation에만 남길지 결정한다.

## 범위

- OpenAI-compatible Chat Completions provider passthrough/guarded stream의 terminal 응답 검사
- raw provider chunk, assembled content, assembled reasoning에서 tool-call marker attempt/complete/incomplete EOF를 세는 scanner
- `assembled_tool_call_count`와 scanner 결과의 불일치 판정
- `tools[]`, `tool_choice`, "tool이 필요했는가" 의미 판단과 독립적인 malformed/incomplete syntax 분류
- Edge 실행 로그와 smoke evidence에 남길 gate observation 필드

## 기능

### Epic: [syntax-gate] Incomplete Tool Call Syntax Gate

tool call marker가 존재하지만 완성된 tool call로 조립되지 않은 terminal 응답을 runtime에서 deterministic하게 잡는 capability를 묶는다.

- [ ] [marker-scanner] `incomplete_tool_call_syntax`가 [Stream Evidence Gate Core](stream-evidence-gate-core.md)의 `Filter` interface를 구현하고, endpoint codec이 전달한 content/reasoning event에서 `marker_attempt_count`, `complete_marker_count`, `incomplete_at_eof`를 산출한다. filter가 raw request mutation이나 retry submit을 소유하지 않는다.
- [ ] [count-mismatch] terminal 응답에서 `marker_attempt_count > assembled_tool_call_count`, `complete_marker_count > assembled_tool_call_count`, 또는 `incomplete_at_eof=true`이면 incomplete tool-call syntax로 분류한다.
- [ ] [no-tool-choice-dependency] `tools[]` 또는 `tool_choice`가 로그/요청에 없거나 `tool_choice`가 생략된 요청도 marker/count mismatch만으로 판정한다. 명시 `tool_choice=none`인데 marker가 나오면 missing tool-call이 아니라 malformed provider output으로 분류한다.

### Epic: [syntax-gate-integration] Syntax Gate Integration

syntax 판정 결과를 stream recovery, 운영 관측, fixture 검증에 연결하는 산출물을 묶는다.

- [ ] [stream-boundary] reasoning-only marker와 content marker를 구분하고, [Stream Evidence Gate Core](stream-evidence-gate-core.md)의 bounded `fragment_gate`, 동일 immutable batch와 parallel all-complete barrier에서 marker scanner decision을 적용한다. retryable syntax 문제는 typed RecoveryIntent, 복구 불가는 fatal을 반환하며 stream-open marker는 rollback/exact replay하지 않는다. recovery는 strategy limit과 최초 실행 제외 기본값/절대 상한 3회의 request 전체 hard cap을 모두 소비하고 cap 소진을 다른 filter로 우회하지 않는다. Core buffer/Arbiter/Coordinator를 재구현하지 않는다.
- [ ] [ops-signal] `incomplete_tool_call_syntax` stable filter id와 marker scanner count, `assembled_tool_call_count`, gate result를 [Stream Evidence Gate Core](stream-evidence-gate-core.md)의 `FilterObservation` timeline에 sanitized evidence로 남기고, provider/model/run id correlation으로 Edge 로그/observation과 연결한다. marker/raw content나 tool args는 남기지 않는다.
- [ ] [fixture-tests] `<tool_call|>` 단독 종료, 완성된 marker와 parser count 불일치, 정상 tool call, marker 없는 최종 답변 fixture가 gate 기대값을 검증한다.

## 완료 리뷰

- 상태: 없음
- 요청일: 없음
- 완료 근거: 기능 Task가 아직 충족되지 않았고, gate match 후 action 정책 결정이 남아 있다.
- 검토 항목:
  - [ ] fixture test가 marker/count mismatch와 정상 종료를 구분한다.
  - [ ] dev-corp Pi 실패 사례와 같은 `assembled_reasoning="<tool_call|>"`, `assembled_tool_call_count=0` 케이스가 gate에 걸린다.
  - [ ] gate action 정책과 stream boundary가 잠금 해제 조건과 일치한다.
- agent-ui 상태 반영: 해당 없음
- 리뷰 코멘트: 없음

## 범위 제외

- tool 필요 여부를 의미적으로 판정하는 LLM judge
- `I will edit`, `도구를 호출하겠다` 같은 자연어 의도만 보고 missing tool-call로 판정하는 방식
- `tools[]` 또는 `tool_choice` 존재 여부를 필수 증거로 삼는 gate
- tool schema validation 실패 재시도
- 모델 교체, Pi prompt 변경, provider option 변경
- 무제한 retry 또는 recursive repair loop

## 작업 컨텍스트

- 관련 경로: `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/node/internal/adapters/openai_compat`, [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md)
- 표준선(선택): 이 gate는 "tool을 써야 했는가"가 아니라 "tool-call 문법을 시작/완성한 흔적과 조립된 tool call 수가 맞는가"만 판단한다.
- 표준선(선택): `tool_choice`가 없으면 `unknown`으로 두고 이 gate의 필수 조건으로 쓰지 않는다.
- 표준선(선택): marker scanner는 regex만으로 시작할 수 있지만 provider별 marker가 늘어나면 작은 state scanner/registry로 승격한다. scanner는 bounded `fragment_gate` requirement, `FilterDecision`과 선택적 intent만 반환하며 hold/release, 병렬 barrier, strategy budget, 최초 실행 제외 기본값/절대 상한 3회의 request 전체 recovery hard cap, rebuild/dispatch는 Core가 소유한다.
- 표준선(선택): 이 consumer는 stable filter id `incomplete_tool_call_syntax`, marker/count 의미 판정과 typed intent, sanitized count evidence만 소유하고 stable id와 count evidence를 Core `FilterObservation`에 제공한다. recovery가 승인되면 Core `RecoveryPlan`과 strategy/request-total cap, bounded ingress snapshot을 사용하며 raw stream buffer, request snapshot/rebuild, retry loop, 공개 오류 사슬 직렬화를 재구현하지 않는다. scanner/rebuild 실패는 sanitized `FailureCauseChain`으로 전달하고 endpoint host가 외부 오류 하나만 직렬화한다.
- 상위 통합 후보: [OpenAI-compatible Runtime Output Integrity Filter](openai-compatible-runtime-output-integrity-filter.md)
- 선행 작업: [OpenAI-compatible Tool Call Boundary Hardening](../../../archive/phase/knowledge-tool-optimization-extension/milestones/openai-compatible-tool-call-boundary-hardening.md), [Stream Evidence Gate Core](stream-evidence-gate-core.md), [OpenAI-compatible 출력 검증 필터](openai-compatible-output-validation-filters.md)
- 후속 작업: [LLM 판별 기반 Missing Tool Call 재시도 Gate](llm-judged-missing-tool-call-retry-gate.md)
- 확인 필요: `구현 잠금 > 결정 필요` 항목
