G0~G8 성과·동맹 측정 OS 작업 일괄 고정

8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본
폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을
git 이력으로 고정하는 것이 목적이다.

- contracts/routes/services: measurement, outcome_trajectory, rupture_repair,
  deliberate_practice, calibration_transfer, supervision_research,
  multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트
- infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행)
- apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가
- docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG
- scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트

engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
This commit is contained in:
Yun Chan 2026-08-08 01:30:53 +09:00
parent 93dd8f82d7
commit 16e791e044
390 changed files with 243188 additions and 499 deletions

View file

@ -19,7 +19,6 @@ from typing import Any, AsyncIterator, Iterable, Literal, cast
import httpx
from app.contracts.engine_gateway import (
ENGINE_GATEWAY_DEFAULT_MODEL_SENTINEL,
ENGINE_PROVIDER_DEFAULTS,
ENGINE_REASONING_EFFORTS,
EngineCapabilitiesResponse,
@ -29,6 +28,7 @@ from app.contracts.engine_gateway import (
ReasoningEffort,
normalize_engine_gateway_model,
)
from app.services.llm_pricing import estimate_reference_cost
CODEX_DEFAULT_MODEL, CODEX_DEFAULT_EFFORT = ENGINE_PROVIDER_DEFAULTS["codex_cli"]
AGY_DEFAULT_MODEL, AGY_DEFAULT_EFFORT = ENGINE_PROVIDER_DEFAULTS["agy_cli"]
@ -580,6 +580,7 @@ async def _generate_codex(
text = ""
tokens_in = 0
tokens_out = 0
cached_input_tokens = 0
for line in stdout.splitlines():
try:
event = json.loads(line)
@ -593,16 +594,25 @@ async def _generate_codex(
usage = event.get("usage") or {}
tokens_in = int(usage.get("input_tokens") or 0)
tokens_out = int(usage.get("output_tokens") or 0)
cached_input_tokens = int(usage.get("cached_input_tokens") or 0)
elif event.get("type") in {"turn.failed", "error"}:
raise ProviderError(str(event.get("message") or event))
if not text.strip():
raise ProviderError("Codex CLI가 최종 응답을 반환하지 않았습니다.")
estimate = estimate_reference_cost(
provider="codex_cli",
model=model,
tokens_in=tokens_in,
tokens_out=tokens_out,
cached_input_tokens=cached_input_tokens,
)
return ProviderGenerateResult(
text=text,
model=model,
provider="codex_cli",
tokens_in=tokens_in,
tokens_out=tokens_out,
cost_usd=estimate.cost_usd if estimate is not None else 0.0,
structured=_structured_or_none(text, req),
)
@ -610,32 +620,12 @@ async def _generate_codex(
async def _generate_agy(
req: GenerateRequest, system_prompt: str, user_payload: str
) -> ProviderGenerateResult:
binary = _binary("AGY_BIN", "agy")
if binary is None:
raise ProviderError("Agy CLI를 찾을 수 없습니다.")
model, effort = await _resolve_selection(req, "agy_cli")
prompt = _cli_prompt(system_prompt, user_payload)
if os.name == "nt" and len(prompt) > 24_000:
raise ProviderError(
"Agy CLI 프롬프트가 Windows 명령줄 안전 한도(24,000자)를 초과했습니다."
)
args = [binary, "--model", model, "--sandbox"]
if effort:
args += ["--effort", effort]
args += ["--print-timeout", f"{int(CLI_TIMEOUT_SECONDS)}s"]
# Agy의 --print는 바로 뒤 토큰을 프롬프트로 해석하며 stdin 입력은
# 지원하지 않는다. 옵션을 모두 앞에 두고 프롬프트를 마지막에 둔다.
args += ["--print", prompt]
stdout, _ = await _run_process(args, cwd=str(_cli_runtime_cwd()))
text = stdout.strip()
if not text:
raise ProviderError("Agy CLI가 최종 응답을 반환하지 않았습니다.")
return ProviderGenerateResult(
text=text,
model=model,
provider="agy_cli",
structured=_structured_or_none(text, req),
)
# text 출력은 토큰 사용량을 주지 않는다. stream-json의 terminal result를
# 동일하게 소비해 generate와 stream 모두 같은 token/cost 계약을 유지한다.
async for event in _stream_agy(req, system_prompt, user_payload):
if event.type == "done" and event.result is not None:
return event.result
raise ProviderError("Agy CLI가 최종 응답을 반환하지 않았습니다.")
async def _stream_agy(
@ -680,6 +670,7 @@ async def _stream_agy(
final_text = ""
tokens_in = 0
tokens_out = 0
cached_input_tokens = 0
result_status = ""
try:
async with asyncio.timeout(CLI_TIMEOUT_SECONDS):
@ -705,6 +696,11 @@ async def _stream_agy(
usage = result.get("usage") or {}
tokens_in = int(usage.get("input_tokens") or 0)
tokens_out = int(usage.get("output_tokens") or 0)
cached_input_tokens = int(
usage.get("cache_read_tokens")
or usage.get("cached_input_tokens")
or 0
)
returncode = await proc.wait()
except TimeoutError as exc:
raise ProviderError(
@ -731,6 +727,13 @@ async def _stream_agy(
elif not emitted:
emitted = resolved_text
yield ProviderStreamEvent(type="delta", text=resolved_text)
estimate = estimate_reference_cost(
provider="agy_cli",
model=model,
tokens_in=tokens_in,
tokens_out=tokens_out,
cached_input_tokens=cached_input_tokens,
)
yield ProviderStreamEvent(
type="done",
result=ProviderGenerateResult(
@ -739,6 +742,7 @@ async def _stream_agy(
provider="agy_cli",
tokens_in=tokens_in,
tokens_out=tokens_out,
cost_usd=estimate.cost_usd if estimate is not None else 0.0,
structured=_structured_or_none(resolved_text, req),
),
)
@ -789,12 +793,22 @@ async def _generate_claude_api(
raise ProviderError("Anthropic Messages API가 텍스트 응답을 반환하지 않았습니다.")
usage = body.get("usage") or {}
inference_geo = body.get("inference_geo")
tokens_in = int(usage.get("input_tokens") or 0)
tokens_out = int(usage.get("output_tokens") or 0)
estimate = estimate_reference_cost(
provider="claude_api",
model=str(body.get("model") or model),
tokens_in=tokens_in,
tokens_out=tokens_out,
cached_input_tokens=int(usage.get("cache_read_input_tokens") or 0),
)
return ProviderGenerateResult(
text=text,
model=str(body.get("model") or model),
provider="claude_api",
tokens_in=int(usage.get("input_tokens") or 0),
tokens_out=int(usage.get("output_tokens") or 0),
tokens_in=tokens_in,
tokens_out=tokens_out,
cost_usd=estimate.cost_usd if estimate is not None else 0.0,
inference_geo=str(inference_geo) if inference_geo else None,
structured=_structured_or_none(text, req),
)