17ad42dc44
* Fix Windows installer progress presentation * Update installer release contracts * Stabilize Windows process-tree fixture handshake * Avoid joining asyncio executor workers in cancellation test * Stabilize Windows orphan recovery cleanup deadline * Stabilize approval deadline regression on Windows * Stabilize long-task history evidence
2142 lines
82 KiB
Python
2142 lines
82 KiB
Python
#!/usr/bin/env python3
|
|
"""Execute one bounded long-task release-gate case against a local Gateway.
|
|
|
|
This is the concrete driver for :mod:`scripts.live_long_task_release_gate`.
|
|
It deliberately accepts no provider key flags: the coordinator supplies only
|
|
the selected provider environment variable (and, for a fallback row, the one
|
|
fallback variable) to this process. A case runs in an isolated temporary
|
|
state/workspace, raw Gateway logs are removed, and the only durable output is
|
|
the coordinator's bounded numeric result schema.
|
|
|
|
The browser scenarios delegate page interaction to the checked-in Playwright
|
|
helper in ``opensquilla-webui/scripts/live-long-task-browser.mjs``. The Python
|
|
process remains the lifecycle owner so a browser can request a graceful or
|
|
forced Gateway restart without receiving a process id or credentials.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import contextlib
|
|
import json
|
|
import math
|
|
import os
|
|
import re
|
|
import socket
|
|
import sqlite3
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
import uuid
|
|
from collections.abc import Iterable, Mapping
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any, Final, Literal, cast
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
SRC_DIR = REPO_ROOT / "src"
|
|
WEBUI_ROOT = REPO_ROOT / "opensquilla-webui"
|
|
if str(REPO_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
if str(SRC_DIR) not in sys.path:
|
|
sys.path.insert(0, str(SRC_DIR))
|
|
|
|
from opensquilla.gateway_client import GatewayRPCClient # noqa: E402
|
|
from opensquilla.provider.registry import get_provider_spec # noqa: E402
|
|
from scripts.live_harness_security import ( # noqa: E402
|
|
classify_failure,
|
|
is_temporary_report_path,
|
|
minimal_child_environment,
|
|
registry_endpoint,
|
|
scan_and_remove_temporary_tree,
|
|
write_safe_report,
|
|
)
|
|
from scripts.live_long_task_release_gate import ( # noqa: E402
|
|
_MINIMUM_PHYSICAL_REQUESTS,
|
|
ALL_SCENARIOS,
|
|
CREDENTIAL_ENV_BY_PROVIDER,
|
|
EXIT_BUDGET,
|
|
EXIT_CONFIGURATION,
|
|
EXIT_FAILED,
|
|
EXIT_INTERRUPTED,
|
|
EXIT_PASSED,
|
|
MODEL_MATRIX,
|
|
PERFORMANCE_REPORT_ENV,
|
|
)
|
|
from scripts.long_task_fault_proxy import ( # noqa: E402
|
|
DeterministicFaultProxy,
|
|
FaultScenario,
|
|
)
|
|
|
|
_CASE_FIELDS: Final = frozenset(
|
|
{
|
|
"schema_version",
|
|
"case_id",
|
|
"provider",
|
|
"model",
|
|
"scenario",
|
|
"repeat_index",
|
|
"fallback_provider",
|
|
"remaining_budget",
|
|
}
|
|
)
|
|
_BUDGET_FIELDS: Final = frozenset(
|
|
{"wall_ms", "billed_cost_usd", "physical_requests", "billed_tokens"}
|
|
)
|
|
_SAFE_ID = re.compile(r"^[a-z0-9][a-z0-9-]{0,239}$")
|
|
_SAFE_MODEL = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,199}$")
|
|
_BROWSER_HELPER: Final = WEBUI_ROOT / "scripts" / "live-long-task-browser.mjs"
|
|
_TERMINAL_EVENTS: Final = frozenset({"session.event.done", "session.event.error"})
|
|
_MAX_CASE_FILE_BYTES: Final = 64 * 1024
|
|
_MAX_BROWSER_RESULT_BYTES: Final = 64 * 1024
|
|
_HISTORY_SETTLE_TIMEOUT_SECONDS: Final = 5.0
|
|
_HISTORY_SETTLE_EVENT_WAIT_SECONDS: Final = 0.05
|
|
_PERFORMANCE_FIXTURE: Final = {
|
|
"historyMessages": 200,
|
|
"reasoningDeltas": 20_000,
|
|
"toolFragments": 10_000,
|
|
"textDeltas": 4_000,
|
|
}
|
|
|
|
|
|
class DriverConfigurationError(RuntimeError):
|
|
"""The case cannot start without changing its configuration."""
|
|
|
|
|
|
class DriverAssertionError(RuntimeError):
|
|
"""The live case ran, but its required observable outcome was absent."""
|
|
|
|
|
|
class DriverBudgetError(RuntimeError):
|
|
"""The remaining coordinator budget cannot safely cover this case."""
|
|
|
|
|
|
def _free_port() -> int:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
sock.bind(("127.0.0.1", 0))
|
|
return int(sock.getsockname()[1])
|
|
|
|
|
|
def _read_turn_call_records(log_dir: Path) -> list[dict[str, Any]]:
|
|
records: list[dict[str, Any]] = []
|
|
for path in sorted(log_dir.glob("turn-calls-*.jsonl")):
|
|
try:
|
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
except (OSError, UnicodeError):
|
|
continue
|
|
for line in lines:
|
|
try:
|
|
payload = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
if isinstance(payload, dict):
|
|
records.append(payload)
|
|
return records
|
|
|
|
|
|
def _usage_from_llm_responses(records: Iterable[Mapping[str, Any]]) -> dict[str, Any]:
|
|
totals: dict[str, int | float] = {
|
|
"totalInputTokens": 0,
|
|
"totalOutputTokens": 0,
|
|
"reasoningTokens": 0,
|
|
"cachedTokens": 0,
|
|
"totalCostUsd": 0.0,
|
|
}
|
|
for record in records:
|
|
payload = record.get("payload")
|
|
usage = payload.get("usage") if isinstance(payload, Mapping) else None
|
|
if not isinstance(usage, Mapping):
|
|
continue
|
|
totals["totalInputTokens"] += int(usage.get("input_tokens") or 0)
|
|
totals["totalOutputTokens"] += int(usage.get("output_tokens") or 0)
|
|
totals["reasoningTokens"] += int(usage.get("reasoning_tokens") or 0)
|
|
totals["cachedTokens"] += int(usage.get("cached_tokens") or 0)
|
|
totals["totalCostUsd"] += float(usage.get("billed_cost") or 0.0)
|
|
totals["totalTokens"] = (
|
|
int(totals["totalInputTokens"])
|
|
+ int(totals["totalOutputTokens"])
|
|
+ int(totals["reasoningTokens"])
|
|
)
|
|
return totals
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CaseBudget:
|
|
wall_ms: int
|
|
billed_cost_usd: float
|
|
physical_requests: int
|
|
billed_tokens: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LiveCase:
|
|
case_id: str
|
|
provider: str
|
|
model: str
|
|
scenario: str
|
|
repeat_index: int
|
|
fallback_provider: str | None
|
|
remaining_budget: CaseBudget
|
|
|
|
|
|
@dataclass
|
|
class TurnObservation:
|
|
session_key: str
|
|
marker: str
|
|
started_monotonic: float
|
|
terminal_event: str = ""
|
|
terminal_reason: str = ""
|
|
text_bytes: int = 0
|
|
text_chunks: int = 0
|
|
marker_seen_in_stream: bool = False
|
|
thinking_chunks: int = 0
|
|
tool_ids: set[str] = field(default_factory=set)
|
|
activity_events: int = 0
|
|
activity_phases: list[tuple[str, float, int, bool]] = field(default_factory=list)
|
|
reasoning_pulse_times: list[float] = field(default_factory=list)
|
|
first_reasoning_ms: float | None = None
|
|
first_token_ms: float | None = None
|
|
activity_latency_ms: float | None = None
|
|
compactions: int = 0
|
|
_marker_tail: str = ""
|
|
|
|
@property
|
|
def completed(self) -> bool:
|
|
return self.terminal_event == "session.event.done"
|
|
|
|
def consume(self, frame: Mapping[str, Any]) -> None:
|
|
event_name = str(frame.get("event") or "")
|
|
payload = frame.get("payload")
|
|
if not isinstance(payload, Mapping):
|
|
payload = {}
|
|
event_session = str(payload.get("session_key", payload.get("key", "")) or "")
|
|
if event_session and event_session != self.session_key:
|
|
return
|
|
now = time.monotonic()
|
|
elapsed_ms = max(0.0, (now - self.started_monotonic) * 1000)
|
|
|
|
if event_name == "session.event.text_delta":
|
|
text = str(payload.get("text") or "")
|
|
if text:
|
|
encoded = text.encode("utf-8")
|
|
self.text_bytes += len(encoded)
|
|
self.text_chunks += 1
|
|
if self.first_token_ms is None:
|
|
self.first_token_ms = elapsed_ms
|
|
# Retain only enough synthetic text to prove the marker; raw
|
|
# provider output never enters the public result.
|
|
self._marker_tail = (self._marker_tail + text)[-1024:]
|
|
self.marker_seen_in_stream = self.marker in self._marker_tail
|
|
elif event_name == "session.event.thinking":
|
|
text = str(payload.get("text") or "")
|
|
if text:
|
|
self.thinking_chunks += 1
|
|
if self.first_reasoning_ms is None:
|
|
self.first_reasoning_ms = elapsed_ms
|
|
elif event_name in {"session.event.tool_use", "session.event.tool_use_delta"}:
|
|
raw_tool_id = payload.get(
|
|
"tool_use_id",
|
|
payload.get("toolUseId", payload.get("id")),
|
|
)
|
|
if isinstance(raw_tool_id, str) and raw_tool_id:
|
|
self.tool_ids.add(raw_tool_id)
|
|
elif event_name == "session.event.provider_activity":
|
|
phase = str(payload.get("phase") or "")
|
|
heartbeat = payload.get("heartbeat") is True
|
|
emitted_at = _nonnegative_int(payload.get("emitted_at"), default=0)
|
|
started_at = _nonnegative_int(payload.get("started_at"), default=0)
|
|
self.activity_events += 1
|
|
self.activity_phases.append((phase, now, emitted_at, heartbeat))
|
|
if phase == "reasoning":
|
|
self.reasoning_pulse_times.append(now)
|
|
if self.first_reasoning_ms is None:
|
|
self.first_reasoning_ms = elapsed_ms
|
|
if emitted_at and started_at:
|
|
candidate = max(0.0, float(emitted_at - started_at))
|
|
if self.activity_latency_ms is None:
|
|
self.activity_latency_ms = candidate
|
|
else:
|
|
self.activity_latency_ms = min(self.activity_latency_ms, candidate)
|
|
elif event_name == "session.event.compaction":
|
|
status = str(payload.get("status") or "").lower()
|
|
if (
|
|
payload.get("applied") is True
|
|
or payload.get("compacted") is True
|
|
or status
|
|
in {
|
|
"completed",
|
|
"applied",
|
|
}
|
|
):
|
|
self.compactions += 1
|
|
|
|
if event_name in _TERMINAL_EVENTS:
|
|
self.terminal_event = event_name
|
|
self.terminal_reason = str(payload.get("reason") or payload.get("status") or "")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BrowserEvidence:
|
|
status: Literal["passed", "failed"]
|
|
counts: dict[str, int]
|
|
metrics: dict[str, float]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DurableAccountingEvidence:
|
|
event_count: int = 0
|
|
missing_cost_entries: int = 0
|
|
input_tokens: int = 0
|
|
output_tokens: int = 0
|
|
reasoning_tokens: int = 0
|
|
cached_tokens: int = 0
|
|
billed_cost_usd: float = 0.0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PerformanceGateEvidence:
|
|
counts: dict[str, int]
|
|
metrics: dict[str, float]
|
|
|
|
|
|
def _nonnegative_int(value: Any, *, default: int | None = None) -> int:
|
|
if value is None and default is not None:
|
|
return default
|
|
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
raise DriverConfigurationError("case contains an invalid non-negative integer")
|
|
return int(value)
|
|
|
|
|
|
def _nonnegative_float(value: Any) -> float:
|
|
if isinstance(value, bool) or not isinstance(value, int | float):
|
|
raise DriverConfigurationError("case contains an invalid non-negative number")
|
|
rendered = float(value)
|
|
if not math.isfinite(rendered) or rendered < 0:
|
|
raise DriverConfigurationError("case contains an invalid non-negative number")
|
|
return rendered
|
|
|
|
|
|
def _safe_case_path(path: Path) -> Path:
|
|
try:
|
|
mode = path.lstat().st_mode
|
|
except FileNotFoundError as exc:
|
|
raise DriverConfigurationError("case file is missing") from exc
|
|
if stat.S_ISLNK(mode) or not stat.S_ISREG(mode):
|
|
raise DriverConfigurationError("case file must be a regular file")
|
|
resolved = path.resolve(strict=True)
|
|
if not is_temporary_report_path(resolved):
|
|
raise DriverConfigurationError("case file must be inside system temporary storage")
|
|
if not resolved.parent.name.startswith("opensquilla-long-task-case-"):
|
|
raise DriverConfigurationError("case file is not owned by the release coordinator")
|
|
if resolved.stat().st_size > _MAX_CASE_FILE_BYTES:
|
|
raise DriverConfigurationError("case file is too large")
|
|
return resolved
|
|
|
|
|
|
def _safe_output_path(path: Path, *, case_path: Path) -> Path:
|
|
resolved = path.resolve(strict=False)
|
|
if not is_temporary_report_path(resolved) or resolved.parent != case_path.parent:
|
|
raise DriverConfigurationError("result must share the coordinator temporary directory")
|
|
if resolved.exists():
|
|
mode = resolved.lstat().st_mode
|
|
if stat.S_ISLNK(mode) or not stat.S_ISREG(mode):
|
|
raise DriverConfigurationError("result path must not be a link or special file")
|
|
return resolved
|
|
|
|
|
|
def load_case(path: Path) -> LiveCase:
|
|
case_path = _safe_case_path(path)
|
|
try:
|
|
payload = json.loads(case_path.read_text(encoding="utf-8"))
|
|
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
raise DriverConfigurationError("case file is not valid JSON") from exc
|
|
if not isinstance(payload, Mapping) or set(payload) != _CASE_FIELDS:
|
|
raise DriverConfigurationError("case file has an unsupported schema")
|
|
if payload.get("schema_version") != 1:
|
|
raise DriverConfigurationError("case schema version is unsupported")
|
|
|
|
case_id = str(payload.get("case_id") or "")
|
|
provider = str(payload.get("provider") or "").lower()
|
|
model = str(payload.get("model") or "")
|
|
scenario = str(payload.get("scenario") or "")
|
|
fallback_raw = payload.get("fallback_provider")
|
|
fallback_provider = str(fallback_raw).lower() if fallback_raw is not None else None
|
|
if not _SAFE_ID.fullmatch(case_id):
|
|
raise DriverConfigurationError("case id is invalid")
|
|
if provider not in MODEL_MATRIX or model not in MODEL_MATRIX[provider]:
|
|
raise DriverConfigurationError("provider/model is outside the release matrix")
|
|
if scenario not in ALL_SCENARIOS:
|
|
raise DriverConfigurationError("scenario is outside the release matrix")
|
|
if fallback_provider is not None and fallback_provider not in MODEL_MATRIX:
|
|
raise DriverConfigurationError("fallback provider is outside the release matrix")
|
|
if scenario == "fallback" and not fallback_provider:
|
|
raise DriverConfigurationError("fallback scenario requires a fallback provider")
|
|
if scenario != "fallback" and fallback_provider is not None:
|
|
raise DriverConfigurationError("only fallback scenarios may name a fallback provider")
|
|
if not _SAFE_MODEL.fullmatch(model):
|
|
raise DriverConfigurationError("model id is invalid")
|
|
|
|
budget_raw = payload.get("remaining_budget")
|
|
if not isinstance(budget_raw, Mapping) or set(budget_raw) != _BUDGET_FIELDS:
|
|
raise DriverConfigurationError("remaining budget is invalid")
|
|
budget = CaseBudget(
|
|
wall_ms=_nonnegative_int(budget_raw.get("wall_ms")),
|
|
billed_cost_usd=_nonnegative_float(budget_raw.get("billed_cost_usd")),
|
|
physical_requests=_nonnegative_int(budget_raw.get("physical_requests")),
|
|
billed_tokens=_nonnegative_int(budget_raw.get("billed_tokens")),
|
|
)
|
|
repeat_index = _nonnegative_int(payload.get("repeat_index"))
|
|
if repeat_index < 1:
|
|
raise DriverConfigurationError("repeat index must be positive")
|
|
if budget.physical_requests < _MINIMUM_PHYSICAL_REQUESTS[scenario]:
|
|
raise DriverBudgetError("remaining physical-request budget cannot cover this case")
|
|
if budget.wall_ms < 1 or budget.billed_tokens < 1 or budget.billed_cost_usd <= 0:
|
|
raise DriverBudgetError("remaining release budget is exhausted")
|
|
|
|
required_envs = {CREDENTIAL_ENV_BY_PROVIDER[provider]}
|
|
if fallback_provider:
|
|
required_envs.add(CREDENTIAL_ENV_BY_PROVIDER[fallback_provider])
|
|
if any(not os.environ.get(name, "").strip() for name in required_envs):
|
|
raise DriverConfigurationError("required provider credential environment is missing")
|
|
return LiveCase(
|
|
case_id=case_id,
|
|
provider=provider,
|
|
model=model,
|
|
scenario=scenario,
|
|
repeat_index=repeat_index,
|
|
fallback_provider=fallback_provider,
|
|
remaining_budget=budget,
|
|
)
|
|
|
|
|
|
def _toml_string(value: str | Path) -> str:
|
|
return json.dumps(str(value), ensure_ascii=False)
|
|
|
|
|
|
def _provider_model(provider: str, *, pro: bool = False) -> str:
|
|
models = MODEL_MATRIX[provider]
|
|
return models[1 if pro else 0]
|
|
|
|
|
|
def _max_tokens_for_scenario(scenario: str) -> int:
|
|
if scenario == "long_answer":
|
|
return 32_768
|
|
if scenario in {"long_reasoning", "browser_hidden_11_minutes"}:
|
|
return 16_384
|
|
if scenario.startswith("browser_") or scenario.startswith("queue_"):
|
|
return 8_192
|
|
return 4_096
|
|
|
|
|
|
def render_gateway_config(
|
|
case: LiveCase,
|
|
*,
|
|
workspace_dir: Path,
|
|
primary_base_url: str | None = None,
|
|
routed_base_url: str | None = None,
|
|
force_router: bool | None = None,
|
|
) -> str:
|
|
"""Render a credential-name-only config for one isolated Gateway."""
|
|
|
|
is_fallback = case.scenario == "fallback"
|
|
primary_provider = cast(str, case.fallback_provider) if is_fallback else case.provider
|
|
primary_model = _provider_model(primary_provider, pro=True) if is_fallback else case.model
|
|
primary_endpoint = primary_base_url or registry_endpoint(primary_provider)
|
|
router_enabled = (
|
|
force_router if force_router is not None else case.scenario in {"router", "fallback"}
|
|
)
|
|
retries = 0 if is_fallback else (1 if case.scenario.startswith("fault_") else 1)
|
|
tool_allow = (
|
|
'also_allow = ["read_file"]'
|
|
if case.scenario in {"tool_compaction", "browser_stop_each_phase"}
|
|
else "also_allow = []"
|
|
)
|
|
lines = [
|
|
'host = "127.0.0.1"',
|
|
"debug = false",
|
|
"log_file_enabled = false",
|
|
f"workspace_dir = {_toml_string(workspace_dir)}",
|
|
"llm_request_timeout_seconds = 900",
|
|
"agent_runtime_timeout_seconds = 1800",
|
|
"agent_max_iterations = 32",
|
|
f"agent_max_provider_retries = {retries}",
|
|
"",
|
|
"[auth]",
|
|
'mode = "none"',
|
|
"",
|
|
"[control_ui]",
|
|
"enabled = true",
|
|
'frontend = "vue"',
|
|
"",
|
|
"[rate_limit]",
|
|
"enabled = false",
|
|
"",
|
|
"[privacy]",
|
|
"disable_network_observability = true",
|
|
"",
|
|
"[tools]",
|
|
'profile = "minimal"',
|
|
tool_allow,
|
|
"",
|
|
"[task_runtime]",
|
|
"turn_hard_deadline_s = 1800",
|
|
"",
|
|
"[memory]",
|
|
'source = "state"',
|
|
"",
|
|
"[naming]",
|
|
"enabled = false",
|
|
"",
|
|
"[llm]",
|
|
f"provider = {_toml_string(primary_provider)}",
|
|
f"model = {_toml_string(primary_model)}",
|
|
f"api_key_env = {_toml_string(get_provider_spec(primary_provider).env_key)}",
|
|
f"base_url = {_toml_string(primary_endpoint)}",
|
|
f"max_tokens = {_max_tokens_for_scenario(case.scenario)}",
|
|
"",
|
|
"[squilla_router]",
|
|
f"enabled = {'true' if router_enabled else 'false'}",
|
|
f"cross_provider_tiers = {'true' if is_fallback else 'false'}",
|
|
'tier_provider_mismatch = "veto"',
|
|
'rollout_phase = "full"',
|
|
'strategy = "v4_phase3"',
|
|
'default_tier = "c0"',
|
|
"require_router_runtime = false",
|
|
]
|
|
if router_enabled:
|
|
for tier in ("c0", "c1", "c2", "c3"):
|
|
lines.extend(
|
|
[
|
|
"",
|
|
f"[squilla_router.tiers.{tier}]",
|
|
f"provider = {_toml_string(case.provider)}",
|
|
f"model = {_toml_string(case.model)}",
|
|
"supports_image = false",
|
|
"image_only = false",
|
|
]
|
|
)
|
|
if is_fallback:
|
|
lines.extend(
|
|
[
|
|
"",
|
|
f"[llm_profiles.{case.provider}]",
|
|
f"model = {_toml_string(case.model)}",
|
|
f"api_key_env = {_toml_string(get_provider_spec(case.provider).env_key)}",
|
|
f"base_url = {_toml_string(routed_base_url or registry_endpoint(case.provider))}",
|
|
]
|
|
)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
class GatewayProcess:
|
|
"""Own one isolated Gateway process and its non-persisted raw artifacts."""
|
|
|
|
def __init__(self, case: LiveCase, *, secret_values: tuple[str, ...]) -> None:
|
|
self.case = case
|
|
self.secret_values = secret_values
|
|
self.root = Path(tempfile.mkdtemp(prefix="opensquilla-live-case-"))
|
|
os.chmod(self.root, 0o700)
|
|
self.config_path = self.root / "gateway.toml"
|
|
self.state_dir = self.root / "state"
|
|
self.user_state_dir = self.root / "user-state"
|
|
self.workspace_dir = self.root / "workspace"
|
|
self.turn_log_dir = self.root / "turn-calls"
|
|
for directory in (
|
|
self.state_dir,
|
|
self.user_state_dir,
|
|
self.workspace_dir,
|
|
self.turn_log_dir,
|
|
):
|
|
directory.mkdir(mode=0o700)
|
|
self.port = _free_port()
|
|
self.proc: subprocess.Popen[bytes] | None = None
|
|
self._stdout: Any = None
|
|
self._stderr: Any = None
|
|
|
|
@property
|
|
def http_url(self) -> str:
|
|
return f"http://127.0.0.1:{self.port}"
|
|
|
|
@property
|
|
def ws_url(self) -> str:
|
|
return f"ws://127.0.0.1:{self.port}/ws"
|
|
|
|
def write_config(
|
|
self,
|
|
*,
|
|
primary_base_url: str | None = None,
|
|
routed_base_url: str | None = None,
|
|
force_router: bool | None = None,
|
|
) -> None:
|
|
self.config_path.write_text(
|
|
render_gateway_config(
|
|
self.case,
|
|
workspace_dir=self.workspace_dir,
|
|
primary_base_url=primary_base_url,
|
|
routed_base_url=routed_base_url,
|
|
force_router=force_router,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
os.chmod(self.config_path, 0o600)
|
|
|
|
def _child_env(self) -> dict[str, str]:
|
|
env = dict(os.environ)
|
|
env.update(
|
|
{
|
|
"PYTHONPATH": os.pathsep.join((str(REPO_ROOT), str(SRC_DIR))),
|
|
"OPENSQUILLA_GATEWAY_CONFIG_PATH": str(self.config_path),
|
|
"OPENSQUILLA_STATE_DIR": str(self.state_dir),
|
|
"OPENSQUILLA_USER_STATE_DIR": str(self.user_state_dir),
|
|
"OPENSQUILLA_TEST_PROFILE_LOCK_ROOT": "1",
|
|
"OPENSQUILLA_MEMORY_DREAM_DISABLED": "1",
|
|
"OPENSQUILLA_TURN_CALL_LOG": "1",
|
|
"OPENSQUILLA_TURN_CALL_LOG_DIR": str(self.turn_log_dir),
|
|
"OPENSQUILLA_LIVE_DISABLE_DOTENV": "1",
|
|
}
|
|
)
|
|
return env
|
|
|
|
def start(self) -> None:
|
|
if self.proc is not None and self.proc.poll() is None:
|
|
raise RuntimeError("Gateway is already running")
|
|
self._stdout = (self.root / "gateway.stdout.log").open("ab")
|
|
self._stderr = (self.root / "gateway.stderr.log").open("ab")
|
|
self.proc = subprocess.Popen(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"opensquilla.cli.main",
|
|
"gateway",
|
|
"run",
|
|
"--port",
|
|
str(self.port),
|
|
"--bind",
|
|
"127.0.0.1",
|
|
],
|
|
cwd=self.workspace_dir,
|
|
env=self._child_env(),
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=self._stdout,
|
|
stderr=self._stderr,
|
|
)
|
|
deadline = time.monotonic() + 45
|
|
while time.monotonic() < deadline:
|
|
if self.proc.poll() is not None:
|
|
raise DriverConfigurationError("Gateway exited during startup")
|
|
try:
|
|
with urllib.request.urlopen(f"{self.http_url}/health", timeout=1) as response:
|
|
if response.status == 200:
|
|
return
|
|
except (urllib.error.URLError, TimeoutError, OSError):
|
|
# The Gateway may still be binding; retry until the bounded deadline.
|
|
pass
|
|
time.sleep(0.25)
|
|
raise DriverConfigurationError("Gateway did not become healthy")
|
|
|
|
def stop(self, *, force: bool = False) -> None:
|
|
proc = self.proc
|
|
if proc is not None and proc.poll() is None:
|
|
if force:
|
|
if os.name == "nt":
|
|
# ``Popen.kill`` terminates only the direct process on
|
|
# Windows. A child which inherited a Gateway log or
|
|
# SQLite handle can otherwise outlive it and make the
|
|
# privacy cleanup permanently fail. The PID is the exact
|
|
# process created above; suppress command output so no
|
|
# machine-local path or process detail reaches reports.
|
|
try:
|
|
subprocess.run( # noqa: S603 - fixed OS command and owned PID.
|
|
["taskkill", "/PID", str(proc.pid), "/T", "/F"],
|
|
check=False,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
timeout=10,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
)
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
# Fall through to the direct-process kill below. The
|
|
# bounded artifact deletion remains the final proof
|
|
# that no inherited handle survived.
|
|
pass
|
|
if proc.poll() is None:
|
|
proc.kill()
|
|
else:
|
|
proc.kill()
|
|
else:
|
|
proc.terminate()
|
|
try:
|
|
proc.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
proc.wait(timeout=5)
|
|
self.proc = None
|
|
for stream_name in ("_stdout", "_stderr"):
|
|
stream = getattr(self, stream_name)
|
|
if stream is not None:
|
|
stream.close()
|
|
setattr(self, stream_name, None)
|
|
|
|
def restart(self, *, force: bool) -> None:
|
|
self.stop(force=force)
|
|
self.start()
|
|
|
|
def raw_records(self) -> list[dict[str, Any]]:
|
|
return _read_turn_call_records(self.turn_log_dir)
|
|
|
|
def cleanup(self) -> None:
|
|
self.stop(force=True)
|
|
# Windows can retain a just-closed SQLite or log handle for a short
|
|
# interval after the child has exited. Re-run the complete secret scan
|
|
# before each bounded delete retry; persistent deletion failures still
|
|
# fail the release row. The scanner owns its separate transient-I/O
|
|
# retry; credential and non-I/O scan failures remain fail-closed.
|
|
cleanup_attempts = 30 if os.name == "nt" else 10
|
|
for attempt in range(cleanup_attempts):
|
|
try:
|
|
scan_and_remove_temporary_tree(self.root, self.secret_values)
|
|
return
|
|
except OSError:
|
|
if attempt + 1 == cleanup_attempts:
|
|
raise
|
|
time.sleep(min(0.05 * (2**attempt), 0.5))
|
|
|
|
|
|
def _artifact_cleanup_stage(error: Exception) -> str:
|
|
"""Project cleanup failures to one stable, non-sensitive diagnostic code."""
|
|
|
|
if isinstance(error, OSError):
|
|
return "artifact_delete_failed"
|
|
if isinstance(error, RuntimeError):
|
|
message = str(error)
|
|
if message == "unable to scan temporary live artifacts before deletion":
|
|
return "artifact_scan_failed"
|
|
if message == "credential detected in temporary live artifacts":
|
|
return "artifact_secret_detected"
|
|
return "artifact_cleanup_failed"
|
|
|
|
|
|
def _synthetic_marker(case: LiveCase, suffix: str = "") -> str:
|
|
digest = uuid.uuid5(uuid.NAMESPACE_URL, f"{case.case_id}:{suffix}").hex[:16].upper()
|
|
return f"OSQ_{digest}"
|
|
|
|
|
|
def _write_tool_fixture(gateway: GatewayProcess) -> list[Path]:
|
|
fixture_dir = gateway.workspace_dir / "synthetic-tool-fixture"
|
|
fixture_dir.mkdir(mode=0o700)
|
|
paths: list[Path] = []
|
|
for index in range(20):
|
|
path = fixture_dir / f"item-{index:02d}.txt"
|
|
path.write_text(f"synthetic item {index:02d}\n", encoding="utf-8")
|
|
paths.append(path)
|
|
return paths
|
|
|
|
|
|
def prompt_for_case(case: LiveCase, gateway: GatewayProcess) -> tuple[str, str]:
|
|
"""Return a synthetic prompt and marker; neither is written to the report."""
|
|
|
|
marker = _synthetic_marker(case)
|
|
scenario = case.scenario
|
|
if scenario == "direct":
|
|
return (
|
|
"Do not call tools. Reply with one short sentence containing exactly "
|
|
f"this synthetic marker: {marker}",
|
|
marker,
|
|
)
|
|
if scenario == "router":
|
|
return (
|
|
"Do not call tools. Briefly explain why a deterministic checksum is useful, "
|
|
f"then end with this exact synthetic marker: {marker}",
|
|
marker,
|
|
)
|
|
if scenario == "long_reasoning":
|
|
return (
|
|
"Do not call tools. Work through a rigorous independent derivation of the "
|
|
"finite identity sum_{k=0}^n (-1)^k C(n,k)^3, checking parity and boundary "
|
|
"cases. Use deep internal reasoning, but return a concise proof and end with "
|
|
f"this exact synthetic marker: {marker}",
|
|
marker,
|
|
)
|
|
if scenario == "tool_compaction":
|
|
paths = _write_tool_fixture(gateway)
|
|
rendered = ", ".join(str(path) for path in paths)
|
|
return (
|
|
"Use the read_file tool exactly once for every one of these twenty synthetic "
|
|
f"files, with no other tools: {rendered}. After all twenty results arrive, "
|
|
"summarize their item numbers in one sentence and end with the exact marker "
|
|
f"{marker}.",
|
|
marker,
|
|
)
|
|
if scenario == "long_answer":
|
|
return (
|
|
"Do not call tools. Generate a synthetic Markdown performance fixture between "
|
|
"18,000 and 24,000 UTF-8 bytes. Use numbered sections, fenced code, tables, "
|
|
"inline math, and citations that point only to example.test. Begin and end with "
|
|
f"the exact marker {marker}. Do not stop before the lower byte bound.",
|
|
marker,
|
|
)
|
|
if scenario.startswith("fault_") or scenario == "fallback":
|
|
return (
|
|
f"Do not call tools. Reply with the exact synthetic marker and no other text: {marker}",
|
|
marker,
|
|
)
|
|
if scenario.startswith("queue_"):
|
|
return (
|
|
"Do not call tools. Produce 250 numbered synthetic lines slowly enough for a "
|
|
"queued follow-up to be staged, and end with this exact marker: "
|
|
f"{marker}",
|
|
marker,
|
|
)
|
|
if scenario == "browser_hidden_11_minutes":
|
|
return (
|
|
"Do not call tools. Perform a careful long derivation, then return 300 numbered "
|
|
f"synthetic verification statements and end with {marker}.",
|
|
marker,
|
|
)
|
|
return (
|
|
"Do not call tools. Produce 220 numbered synthetic lines and end with this exact "
|
|
f"marker: {marker}",
|
|
marker,
|
|
)
|
|
|
|
|
|
def _fault_sequence(scenario: str) -> tuple[FaultScenario, ...]:
|
|
mapping = {
|
|
"fault_429_retry_after": (FaultScenario.RATE_LIMITED, FaultScenario.OK),
|
|
"fault_503": (FaultScenario.OVERLOADED, FaultScenario.OK),
|
|
"fault_reset_before_first_token": (
|
|
FaultScenario.RESET_BEFORE_FIRST_TOKEN,
|
|
FaultScenario.OK,
|
|
),
|
|
"fault_partial_then_reset": (FaultScenario.PARTIAL_THEN_RESET, FaultScenario.OK),
|
|
"fault_reasoning_only": (FaultScenario.REASONING_ONLY, FaultScenario.OK),
|
|
"fault_late_terminal": (FaultScenario.LATE_TERMINAL,),
|
|
"fallback": (FaultScenario.OVERLOADED,),
|
|
}
|
|
try:
|
|
return mapping[scenario]
|
|
except KeyError as exc:
|
|
raise ValueError(f"no fault sequence for {scenario!r}") from exc
|
|
|
|
|
|
async def _history_evidence(
|
|
client: GatewayRPCClient,
|
|
*,
|
|
session_key: str,
|
|
assistant_marker: str = "",
|
|
user_marker: str = "",
|
|
) -> tuple[int, int, int, int]:
|
|
history = await client.call(
|
|
"chat.history",
|
|
{"sessionKey": session_key, "limit": 1000},
|
|
)
|
|
messages = history.get("messages", []) if isinstance(history, Mapping) else []
|
|
if not isinstance(messages, list):
|
|
return 0, 0, 0, 0
|
|
assistant_bytes = 0
|
|
assistant_occurrences = 0
|
|
user_occurrences = 0
|
|
user_attachment_occurrences = 0
|
|
for raw_message in messages:
|
|
if not isinstance(raw_message, Mapping):
|
|
continue
|
|
role = str(raw_message.get("role") or "")
|
|
text = str(raw_message.get("text", raw_message.get("content", "")) or "")
|
|
if role == "assistant":
|
|
assistant_bytes = len(text.encode("utf-8"))
|
|
if assistant_marker and assistant_marker in text:
|
|
assistant_occurrences += 1
|
|
elif role == "user" and user_marker and user_marker in text:
|
|
user_occurrences += 1
|
|
attachments = raw_message.get("attachments")
|
|
if isinstance(attachments, list):
|
|
user_attachment_occurrences += len(attachments)
|
|
return (
|
|
assistant_bytes,
|
|
assistant_occurrences,
|
|
user_occurrences,
|
|
user_attachment_occurrences,
|
|
)
|
|
|
|
|
|
async def _wait_for_assistant_history_evidence(
|
|
client: GatewayRPCClient,
|
|
observation: TurnObservation,
|
|
*,
|
|
session_key: str,
|
|
assistant_marker: str,
|
|
deadline: float,
|
|
) -> tuple[int, int, int, int]:
|
|
# The terminal stream event can win a narrow race with the assistant
|
|
# transcript commit. Keep the durable marker assertion, but let history
|
|
# converge within its own bounded phase. Waiting on the event queue avoids
|
|
# a blind sleep and keeps the client responsive to late stream frames.
|
|
settle_deadline = min(
|
|
deadline,
|
|
time.monotonic() + _HISTORY_SETTLE_TIMEOUT_SECONDS,
|
|
)
|
|
while True:
|
|
evidence = await _history_evidence(
|
|
client,
|
|
session_key=session_key,
|
|
assistant_marker=assistant_marker,
|
|
)
|
|
if evidence[1] > 0:
|
|
return evidence
|
|
remaining = settle_deadline - time.monotonic()
|
|
if remaining <= 0:
|
|
return evidence
|
|
try:
|
|
frame = await client.recv_event(
|
|
timeout=min(remaining, _HISTORY_SETTLE_EVENT_WAIT_SECONDS)
|
|
)
|
|
except TimeoutError:
|
|
continue
|
|
observation.consume(frame)
|
|
|
|
|
|
async def _cancelled_webui_stop_count(
|
|
client: GatewayRPCClient,
|
|
*,
|
|
session_key: str,
|
|
) -> int:
|
|
history = await client.call(
|
|
"chat.history",
|
|
{"sessionKey": session_key, "limit": 1000},
|
|
)
|
|
outcomes = history.get("turn_outcomes", []) if isinstance(history, Mapping) else []
|
|
if not isinstance(outcomes, list):
|
|
return 0
|
|
count = 0
|
|
for raw_outcome in outcomes:
|
|
if not isinstance(raw_outcome, Mapping):
|
|
continue
|
|
outcome = raw_outcome.get("outcome")
|
|
if not isinstance(outcome, Mapping):
|
|
continue
|
|
status = str(raw_outcome.get("status") or "").lower()
|
|
source = str(outcome.get("cancellation_source") or "").lower()
|
|
if status == "cancelled" and source == "webui_stop":
|
|
count += 1
|
|
return count
|
|
|
|
|
|
async def _send_and_observe(
|
|
gateway: GatewayProcess,
|
|
*,
|
|
prompt: str,
|
|
marker: str,
|
|
session_key: str,
|
|
timeout_seconds: float,
|
|
) -> tuple[TurnObservation, int, int]:
|
|
client = GatewayRPCClient(scopes=["operator.admin"], request_timeout_s=60.0)
|
|
await client.connect(gateway.ws_url)
|
|
try:
|
|
await client.call(
|
|
"sessions.messages.subscribe",
|
|
{"key": session_key, "fast_ack": True},
|
|
)
|
|
started = time.monotonic()
|
|
observation = TurnObservation(
|
|
session_key=session_key,
|
|
marker=marker,
|
|
started_monotonic=started,
|
|
)
|
|
request_id = f"live-{uuid.uuid4()}"
|
|
message_id = f"msg-{uuid.uuid4()}"
|
|
await client.call(
|
|
"sessions.send",
|
|
{
|
|
"key": session_key,
|
|
"message": prompt,
|
|
"intent": "new_chat",
|
|
"queueMode": "followup",
|
|
"clientRequestId": request_id,
|
|
"clientMessageId": message_id,
|
|
"_source": {
|
|
"surface_id": "long-task-release-gate",
|
|
"client_request_id": request_id,
|
|
"client_message_id": message_id,
|
|
},
|
|
},
|
|
)
|
|
deadline = time.monotonic() + timeout_seconds
|
|
while not observation.terminal_event:
|
|
remaining = deadline - time.monotonic()
|
|
if remaining <= 0:
|
|
raise TimeoutError("live turn exceeded its case timeout")
|
|
frame = await client.recv_event(timeout=min(remaining, 30.0))
|
|
observation.consume(frame)
|
|
if observation.completed and observation.marker_seen_in_stream:
|
|
assistant_bytes, assistant_markers, _, _ = (
|
|
await _wait_for_assistant_history_evidence(
|
|
client,
|
|
observation,
|
|
session_key=session_key,
|
|
assistant_marker=marker,
|
|
deadline=deadline,
|
|
)
|
|
)
|
|
else:
|
|
assistant_bytes, assistant_markers, _, _ = await _history_evidence(
|
|
client,
|
|
session_key=session_key,
|
|
assistant_marker=marker,
|
|
)
|
|
return observation, assistant_bytes, assistant_markers
|
|
finally:
|
|
await client.close()
|
|
|
|
|
|
async def _manual_compaction(gateway: GatewayProcess, session_key: str) -> bool:
|
|
client = GatewayRPCClient(scopes=["operator.admin"], request_timeout_s=180.0)
|
|
await client.connect(gateway.ws_url)
|
|
try:
|
|
payload = await client.call(
|
|
"sessions.compact",
|
|
{
|
|
"key": session_key,
|
|
"wait": True,
|
|
"instructions": (
|
|
"Compact the synthetic tool results while preserving their item numbers."
|
|
),
|
|
},
|
|
)
|
|
finally:
|
|
await client.close()
|
|
return bool(
|
|
isinstance(payload, Mapping)
|
|
and (
|
|
payload.get("compacted") is True
|
|
or payload.get("applied") is True
|
|
or str(payload.get("status") or "").lower() in {"completed", "applied"}
|
|
)
|
|
)
|
|
|
|
|
|
async def _router_decision_count(gateway: GatewayProcess, session_key: str) -> int:
|
|
client = GatewayRPCClient(scopes=["operator.admin"], request_timeout_s=30.0)
|
|
await client.connect(gateway.ws_url)
|
|
try:
|
|
payload = await client.call(
|
|
"router.decisions.list",
|
|
{"sessionKey": session_key, "limit": 5},
|
|
)
|
|
except Exception:
|
|
return 0
|
|
finally:
|
|
await client.close()
|
|
decisions = payload.get("decisions", []) if isinstance(payload, Mapping) else []
|
|
return len(decisions) if isinstance(decisions, list) else 0
|
|
|
|
|
|
async def _durable_accounting_evidence(
|
|
gateway: GatewayProcess,
|
|
*,
|
|
expected_provider_legs: int,
|
|
) -> DurableAccountingEvidence:
|
|
"""Return bounded evidence that every physical call reached the usage ledger.
|
|
|
|
The Gateway persists one usage event per provider execution leg, including
|
|
failed and interrupted legs. Poll briefly because finalization is queued;
|
|
after a forced Gateway restart, boot first recovers a started event as
|
|
``unknown``. The query and returned evidence contain no request or response
|
|
content.
|
|
"""
|
|
|
|
client = GatewayRPCClient(scopes=["operator.admin"], request_timeout_s=30.0)
|
|
await client.connect(gateway.ws_url)
|
|
deadline = time.monotonic() + 5.0
|
|
try:
|
|
while True:
|
|
payload = await client.call(
|
|
"usage.query",
|
|
{
|
|
"schemaVersion": 1,
|
|
"timezone": "UTC",
|
|
"range": {"preset": "all"},
|
|
"include": {"days": False, "models": False, "sessions": False},
|
|
},
|
|
)
|
|
totals = payload.get("attributedTotals") if isinstance(payload, Mapping) else None
|
|
if not isinstance(totals, Mapping):
|
|
totals = {}
|
|
event_count = _nonnegative_int(
|
|
payload.get("eventCount") if isinstance(payload, Mapping) else None,
|
|
default=0,
|
|
)
|
|
missing_cost_entries = _nonnegative_int(
|
|
totals.get("missingCostEntries"),
|
|
default=0,
|
|
)
|
|
latest = DurableAccountingEvidence(
|
|
event_count=event_count,
|
|
missing_cost_entries=missing_cost_entries,
|
|
input_tokens=_nonnegative_int(totals.get("inputTokens"), default=0),
|
|
output_tokens=_nonnegative_int(totals.get("outputTokens"), default=0),
|
|
reasoning_tokens=_nonnegative_int(totals.get("reasoningTokens"), default=0),
|
|
cached_tokens=_nonnegative_int(totals.get("cacheReadTokens"), default=0),
|
|
billed_cost_usd=_nonnegative_float(totals.get("billedCostUsd", 0.0)),
|
|
)
|
|
if event_count >= expected_provider_legs or time.monotonic() >= deadline:
|
|
return latest
|
|
await asyncio.sleep(0.1)
|
|
finally:
|
|
await client.close()
|
|
|
|
|
|
def _accounting_from_records(
|
|
records: list[dict[str, Any]],
|
|
) -> tuple[dict[str, int], dict[str, float]]:
|
|
terminal_records = [
|
|
record for record in records if record.get("kind") in {"llm_response", "llm_error"}
|
|
]
|
|
raw = _usage_from_llm_responses(terminal_records)
|
|
input_tokens = _nonnegative_int(raw.get("totalInputTokens"), default=0)
|
|
output_tokens = _nonnegative_int(raw.get("totalOutputTokens"), default=0)
|
|
reasoning_tokens = _nonnegative_int(raw.get("reasoningTokens"), default=0)
|
|
cached_tokens = _nonnegative_int(raw.get("cachedTokens"), default=0)
|
|
total_tokens = _nonnegative_int(raw.get("totalTokens"), default=0)
|
|
billed_cost = max(0.0, float(raw.get("totalCostUsd") or 0.0))
|
|
return (
|
|
{
|
|
"input_tokens": input_tokens,
|
|
"output_tokens": output_tokens,
|
|
"reasoning_tokens": reasoning_tokens,
|
|
"cached_tokens": cached_tokens,
|
|
"total_tokens": total_tokens,
|
|
},
|
|
{"billed_cost_usd": billed_cost},
|
|
)
|
|
|
|
|
|
def _failure_class_from_records(records: Iterable[Mapping[str, Any]]) -> str:
|
|
fragments: list[str] = []
|
|
for record in records:
|
|
if record.get("kind") != "llm_error":
|
|
continue
|
|
payload = record.get("payload")
|
|
if not isinstance(payload, Mapping):
|
|
continue
|
|
nested_error = payload.get("error")
|
|
sources = [payload]
|
|
if isinstance(nested_error, Mapping):
|
|
sources.append(nested_error)
|
|
for source in sources:
|
|
for key in ("code", "error_code", "exception_type", "message"):
|
|
value = source.get(key)
|
|
if isinstance(value, str):
|
|
fragments.append(value[:200])
|
|
return classify_failure(" ".join(fragments)) if fragments else "assertion"
|
|
|
|
|
|
def _max_reasoning_gap_ms(observation: TurnObservation) -> float:
|
|
times = observation.reasoning_pulse_times
|
|
if len(times) < 2:
|
|
return 0.0
|
|
return max((right - left) * 1000 for left, right in zip(times, times[1:]))
|
|
|
|
|
|
def _gateway_session_database(gateway: GatewayProcess) -> Path | None:
|
|
candidates = [gateway.state_dir / "sessions.db"]
|
|
root = getattr(gateway, "root", None)
|
|
if isinstance(root, Path):
|
|
candidates.extend(path for path in root.rglob("sessions.db") if path.is_file())
|
|
return next((path for path in candidates if path.is_file()), None)
|
|
|
|
|
|
def _durable_accounting_from_database(gateway: GatewayProcess) -> tuple[int, int]:
|
|
database = _gateway_session_database(gateway)
|
|
if database is None:
|
|
return 0, 0
|
|
try:
|
|
# sqlite3.Connection's context manager controls only the transaction;
|
|
# it does not close the database. Keep the read handle bounded so a
|
|
# Windows release runner can remove the isolated state tree afterward.
|
|
with contextlib.closing(
|
|
sqlite3.connect(f"{database.resolve().as_uri()}?mode=ro", uri=True)
|
|
) as connection:
|
|
row = connection.execute(
|
|
"""
|
|
SELECT
|
|
COUNT(*),
|
|
COALESCE(SUM(
|
|
CASE
|
|
WHEN status IN ('started', 'unknown')
|
|
AND missing_cost_entries = 0 THEN 1
|
|
ELSE missing_cost_entries
|
|
END
|
|
), 0)
|
|
FROM usage_events
|
|
"""
|
|
).fetchone()
|
|
except sqlite3.Error:
|
|
return 0, 0
|
|
return (int(row[0] or 0), int(row[1] or 0)) if row else (0, 0)
|
|
|
|
|
|
def _fallback_preceded_backup_usage_start(
|
|
gateway: GatewayProcess,
|
|
observation: TurnObservation,
|
|
) -> bool:
|
|
fallback_emitted_at = min(
|
|
(
|
|
emitted_at
|
|
for phase, _when, emitted_at, _heartbeat in observation.activity_phases
|
|
if phase == "fallback" and emitted_at > 0
|
|
),
|
|
default=0,
|
|
)
|
|
if fallback_emitted_at <= 0:
|
|
return False
|
|
database = _gateway_session_database(gateway)
|
|
if database is None:
|
|
return False
|
|
try:
|
|
with contextlib.closing(
|
|
sqlite3.connect(f"{database.resolve().as_uri()}?mode=ro", uri=True)
|
|
) as connection:
|
|
row = connection.execute(
|
|
"""
|
|
SELECT MAX(started_at_ms)
|
|
FROM usage_events
|
|
"""
|
|
).fetchone()
|
|
except sqlite3.Error:
|
|
return False
|
|
backup_started_at = int(row[0] or 0) if row else 0
|
|
return backup_started_at > 0 and fallback_emitted_at <= backup_started_at
|
|
|
|
|
|
def _base_evidence(
|
|
observation: TurnObservation,
|
|
*,
|
|
physical_requests: int,
|
|
records: list[dict[str, Any]],
|
|
) -> tuple[dict[str, int], dict[str, float]]:
|
|
phases = [phase for phase, _when, _epoch, _heartbeat in observation.activity_phases]
|
|
retry_phases = sum(phase in {"retry_wait", "retrying"} for phase in phases)
|
|
fallback_phases = sum(phase == "fallback" for phase in phases)
|
|
counts = {
|
|
"provider_legs": physical_requests,
|
|
"tool_legs": len(observation.tool_ids),
|
|
"retry_legs": max(retry_phases, max(0, physical_requests - 1 - fallback_phases)),
|
|
"fallback_legs": fallback_phases,
|
|
"activity_events": observation.activity_events,
|
|
"tokens_rendered": observation.text_bytes,
|
|
"output_bytes": observation.text_bytes,
|
|
"incremental_chunks": observation.text_chunks,
|
|
"reasoning_pulses": len(observation.reasoning_pulse_times),
|
|
}
|
|
metrics: dict[str, float] = {
|
|
"max_reasoning_pulse_gap_ms": _max_reasoning_gap_ms(observation),
|
|
}
|
|
if observation.first_reasoning_ms is not None:
|
|
metrics["first_reasoning_ms"] = observation.first_reasoning_ms
|
|
if observation.first_token_ms is not None:
|
|
metrics["first_token_ms"] = observation.first_token_ms
|
|
if observation.activity_latency_ms is not None:
|
|
metrics["activity_latency_ms"] = observation.activity_latency_ms
|
|
elif observation.reasoning_pulse_times:
|
|
metrics["activity_latency_ms"] = 0.0
|
|
del records
|
|
return counts, metrics
|
|
|
|
|
|
def _add_durable_accounting_evidence(
|
|
gateway: GatewayProcess,
|
|
*,
|
|
counts: dict[str, int],
|
|
expected_provider_legs: int,
|
|
) -> DurableAccountingEvidence:
|
|
evidence = asyncio.run(
|
|
_durable_accounting_evidence(
|
|
gateway,
|
|
expected_provider_legs=expected_provider_legs,
|
|
)
|
|
)
|
|
counts["accounted_provider_legs"] = evidence.event_count
|
|
counts["usage_missing_cost_entries"] = evidence.missing_cost_entries
|
|
return evidence
|
|
|
|
|
|
def _accounting_from_durable(
|
|
evidence: DurableAccountingEvidence,
|
|
) -> tuple[dict[str, int], dict[str, float]]:
|
|
return (
|
|
{
|
|
"input_tokens": evidence.input_tokens,
|
|
"output_tokens": evidence.output_tokens,
|
|
"reasoning_tokens": evidence.reasoning_tokens,
|
|
"cached_tokens": evidence.cached_tokens,
|
|
"total_tokens": (
|
|
evidence.input_tokens + evidence.output_tokens + evidence.reasoning_tokens
|
|
),
|
|
},
|
|
{"billed_cost_usd": evidence.billed_cost_usd},
|
|
)
|
|
|
|
|
|
def _merge_accounting_lower_bounds(
|
|
first_usage: Mapping[str, int],
|
|
first_cost: Mapping[str, float],
|
|
second_usage: Mapping[str, int],
|
|
second_cost: Mapping[str, float],
|
|
) -> tuple[dict[str, int], dict[str, float]]:
|
|
usage = {
|
|
key: max(int(first_usage.get(key, 0)), int(second_usage.get(key, 0)))
|
|
for key in ("input_tokens", "output_tokens", "reasoning_tokens", "cached_tokens")
|
|
}
|
|
usage["total_tokens"] = max(
|
|
sum(usage[key] for key in ("input_tokens", "output_tokens", "reasoning_tokens")),
|
|
int(first_usage.get("total_tokens", 0)),
|
|
int(second_usage.get("total_tokens", 0)),
|
|
)
|
|
return usage, {
|
|
"billed_cost_usd": max(
|
|
float(first_cost.get("billed_cost_usd", 0.0)),
|
|
float(second_cost.get("billed_cost_usd", 0.0)),
|
|
)
|
|
}
|
|
|
|
|
|
def _browser_number(value: Any, *, integer: bool) -> int | float:
|
|
if isinstance(value, bool) or not isinstance(value, int | float):
|
|
raise DriverAssertionError("browser helper returned an invalid numeric metric")
|
|
number = float(value)
|
|
if not math.isfinite(number) or number < 0:
|
|
raise DriverAssertionError("browser helper returned an invalid numeric metric")
|
|
if integer:
|
|
if not number.is_integer():
|
|
raise DriverAssertionError("browser helper returned a fractional count")
|
|
return int(number)
|
|
return number
|
|
|
|
|
|
def _load_browser_evidence(path: Path, *, return_code: int) -> BrowserEvidence:
|
|
try:
|
|
mode = path.lstat().st_mode
|
|
if stat.S_ISLNK(mode) or not stat.S_ISREG(mode):
|
|
raise DriverAssertionError("browser helper result is not a regular file")
|
|
if path.stat().st_size > _MAX_BROWSER_RESULT_BYTES:
|
|
raise DriverAssertionError("browser helper result is too large")
|
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
except (FileNotFoundError, OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
raise DriverAssertionError("browser helper did not produce a valid result") from exc
|
|
if not isinstance(payload, Mapping) or set(payload) != {"status", "counts", "metrics"}:
|
|
raise DriverAssertionError("browser helper result has an unsupported schema")
|
|
status = str(payload.get("status") or "")
|
|
if status not in {"passed", "failed"}:
|
|
raise DriverAssertionError("browser helper result status is invalid")
|
|
if return_code != 0 and status == "passed":
|
|
raise DriverAssertionError("failed browser process reported a passing result")
|
|
raw_counts = payload.get("counts")
|
|
raw_metrics = payload.get("metrics")
|
|
if not isinstance(raw_counts, Mapping) or not isinstance(raw_metrics, Mapping):
|
|
raise DriverAssertionError("browser helper evidence must be numeric mappings")
|
|
counts = {
|
|
str(key): cast(int, _browser_number(value, integer=True))
|
|
for key, value in raw_counts.items()
|
|
if isinstance(key, str)
|
|
}
|
|
metrics = {
|
|
str(key): float(_browser_number(value, integer=False))
|
|
for key, value in raw_metrics.items()
|
|
if isinstance(key, str)
|
|
}
|
|
return BrowserEvidence(status=cast(Any, status), counts=counts, metrics=metrics)
|
|
|
|
|
|
def _load_performance_report(name: str) -> Mapping[str, Any]:
|
|
env_name = PERFORMANCE_REPORT_ENV[name]
|
|
raw_path = os.environ.get(env_name, "").strip()
|
|
if not raw_path:
|
|
raise DriverAssertionError("required deterministic performance report is missing")
|
|
path = Path(raw_path)
|
|
if not path.is_absolute() or not is_temporary_report_path(path):
|
|
raise DriverAssertionError("deterministic performance report is outside system temp")
|
|
try:
|
|
mode = path.lstat().st_mode
|
|
if stat.S_ISLNK(mode) or not stat.S_ISREG(mode):
|
|
raise DriverAssertionError("deterministic performance report is not a regular file")
|
|
if path.stat().st_size > _MAX_BROWSER_RESULT_BYTES:
|
|
raise DriverAssertionError("deterministic performance report is too large")
|
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
except (FileNotFoundError, OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
raise DriverAssertionError("deterministic performance report is unreadable") from exc
|
|
if not isinstance(payload, Mapping):
|
|
raise DriverAssertionError("deterministic performance report must be an object")
|
|
return payload
|
|
|
|
|
|
def _report_number(
|
|
report: Mapping[str, Any],
|
|
key: str,
|
|
*,
|
|
positive: bool = False,
|
|
integer: bool = False,
|
|
) -> float:
|
|
value = report.get(key)
|
|
if isinstance(value, bool) or not isinstance(value, int | float):
|
|
raise DriverAssertionError("deterministic performance evidence is not numeric")
|
|
number = float(value)
|
|
if not math.isfinite(number) or number < 0 or (positive and number <= 0):
|
|
raise DriverAssertionError("deterministic performance evidence is outside its domain")
|
|
if integer and not number.is_integer():
|
|
raise DriverAssertionError("deterministic performance count is fractional")
|
|
return number
|
|
|
|
|
|
def _validate_performance_fixture(
|
|
report: Mapping[str, Any],
|
|
*,
|
|
require_text_bytes: bool,
|
|
) -> None:
|
|
fixture = report.get("fixture")
|
|
if not isinstance(fixture, Mapping):
|
|
raise DriverAssertionError("deterministic performance fixture is missing")
|
|
expected = dict(_PERFORMANCE_FIXTURE)
|
|
if require_text_bytes:
|
|
expected["textBytes"] = 128 * 1_024
|
|
if set(fixture) != set(expected):
|
|
raise DriverAssertionError("deterministic performance fixture schema differs")
|
|
for key, expected_value in expected.items():
|
|
value = fixture.get(key)
|
|
if isinstance(value, bool) or value != expected_value:
|
|
raise DriverAssertionError("deterministic performance fixture differs")
|
|
|
|
|
|
def _reduction_percent(baseline: float, candidate: float) -> float:
|
|
return max(0.0, (1.0 - candidate / baseline) * 100.0)
|
|
|
|
|
|
def _load_performance_gate_evidence() -> PerformanceGateEvidence:
|
|
"""Load fixed-Chromium evidence produced by the checked-in offline specs.
|
|
|
|
The real-provider browser case proves integration and output size. Absolute
|
|
and relative performance thresholds come from one deterministic fixture so
|
|
provider latency/content cannot make the release gate nondeterministic.
|
|
"""
|
|
|
|
baseline = _load_performance_report("baseline")
|
|
candidate = _load_performance_report("candidate")
|
|
resilience = _load_performance_report("resilience")
|
|
if baseline.get("schemaVersion") != 1 or baseline.get("mode") != "baseline":
|
|
raise DriverAssertionError("baseline characterization schema is invalid")
|
|
if candidate.get("schemaVersion") != 1 or candidate.get("mode") != "candidate":
|
|
raise DriverAssertionError("candidate characterization schema is invalid")
|
|
if resilience.get("schemaVersion") != 1:
|
|
raise DriverAssertionError("resilience performance schema is invalid")
|
|
_validate_performance_fixture(baseline, require_text_bytes=True)
|
|
_validate_performance_fixture(candidate, require_text_bytes=True)
|
|
if baseline.get("fixture") != candidate.get("fixture"):
|
|
raise DriverAssertionError("baseline and candidate fixtures differ")
|
|
_validate_performance_fixture(resilience, require_text_bytes=False)
|
|
|
|
baseline_heap = _report_number(baseline, "peakHeapDeltaBytes", positive=True)
|
|
candidate_heap = _report_number(candidate, "peakHeapDeltaBytes")
|
|
baseline_recalc = _report_number(baseline, "recalcStyleCount", positive=True)
|
|
candidate_recalc = _report_number(candidate, "recalcStyleCount")
|
|
parse_reduction = _report_number(resilience, "liveParseReduction")
|
|
if parse_reduction > 1:
|
|
raise DriverAssertionError("live parse reduction is outside its domain")
|
|
|
|
counts = {
|
|
"dom_nodes": int(_report_number(resilience, "domNodes", integer=True)),
|
|
"mounted_rows": int(_report_number(resilience, "ordinaryRows", integer=True)),
|
|
}
|
|
metrics = {
|
|
"input_next_paint_p95_ms": _report_number(resilience, "inputP95"),
|
|
"input_next_paint_max_ms": _report_number(resilience, "inputMax"),
|
|
"max_main_thread_task_ms": _report_number(resilience, "longestTask"),
|
|
"peak_heap_delta_bytes": _report_number(resilience, "peakHeapDeltaBytes"),
|
|
"post_gc_heap_delta_bytes": _report_number(resilience, "postGcHeapDeltaBytes"),
|
|
"post_gc_growth_bytes": _report_number(
|
|
resilience,
|
|
"maxRetentionGrowthPerTurnBytes",
|
|
),
|
|
"anchor_drift_px": _report_number(resilience, "upscrollAnchorDrift"),
|
|
"bottom_gap_px": _report_number(resilience, "bottomGapWhileFollowing"),
|
|
"markdown_parse_reduction_pct": parse_reduction * 100.0,
|
|
"recalc_style_reduction_pct": _reduction_percent(
|
|
baseline_recalc,
|
|
candidate_recalc,
|
|
),
|
|
"peak_heap_reduction_pct": _reduction_percent(
|
|
baseline_heap,
|
|
candidate_heap,
|
|
),
|
|
}
|
|
return PerformanceGateEvidence(counts=counts, metrics=metrics)
|
|
|
|
|
|
def _browser_child_environment() -> dict[str, str]:
|
|
env = minimal_child_environment(os.environ)
|
|
# Browser discovery is path based. No provider credential, config value,
|
|
# or arbitrary OPENSQUILLA_* variable crosses this boundary.
|
|
env["CI"] = "1"
|
|
return env
|
|
|
|
|
|
def _run_browser_helper(
|
|
case: LiveCase,
|
|
gateway: GatewayProcess,
|
|
*,
|
|
prompt: str,
|
|
marker: str,
|
|
restart_callback: Any | None = None,
|
|
) -> tuple[BrowserEvidence, str, str, set[str]]:
|
|
if not _BROWSER_HELPER.is_file():
|
|
raise DriverConfigurationError("checked-in browser helper is missing")
|
|
input_path = gateway.root / "browser-input.json"
|
|
output_path = gateway.root / "browser-result.json"
|
|
command_path = gateway.root / "browser-command.json"
|
|
ready_path = gateway.root / "browser-ready.json"
|
|
queue_marker = _synthetic_marker(case, "queued")
|
|
recovery_marker = _synthetic_marker(case, "recovered")
|
|
timeout_ms = min(case.remaining_budget.wall_ms - 15_000, 29 * 60 * 1000)
|
|
if timeout_ms <= 0:
|
|
raise DriverBudgetError("browser case has no usable wall-clock budget")
|
|
payload = {
|
|
"schemaVersion": 1,
|
|
"scenario": case.scenario,
|
|
"gatewayUrl": gateway.http_url,
|
|
"sessionKey": f"agent:main:webchat:{uuid.uuid4().hex[:12]}",
|
|
"alternateSessionKey": f"agent:main:webchat:{uuid.uuid4().hex[:12]}",
|
|
"prompt": prompt,
|
|
"marker": marker,
|
|
"queuePrompt": (
|
|
"Describe the attached synthetic text in one sentence and include exactly "
|
|
f"this marker: {queue_marker}"
|
|
),
|
|
"queueMarker": queue_marker,
|
|
"recoveryPrompt": (
|
|
"Do not call tools. Reply in at least eight streamed chunks and end with "
|
|
f"this exact marker: {recovery_marker}"
|
|
),
|
|
"recoveryMarker": recovery_marker,
|
|
"timeoutMs": timeout_ms,
|
|
"commandPath": str(command_path),
|
|
"readyPath": str(ready_path),
|
|
"stopPrompts": {
|
|
"reasoning": (
|
|
"Do not call tools. Use deep reasoning to derive a difficult finite-sum proof, "
|
|
f"then end with {marker}."
|
|
),
|
|
"tool": (
|
|
"Use read_file repeatedly on synthetic-tool-fixture/item-00.txt through "
|
|
f"item-19.txt, then end with {marker}."
|
|
),
|
|
"output": (
|
|
f"Do not call tools. Produce 300 numbered synthetic lines and end with {marker}."
|
|
),
|
|
"retry": f"Do not call tools. Reply with {marker}.",
|
|
},
|
|
}
|
|
input_path.write_text(
|
|
json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
os.chmod(input_path, 0o600)
|
|
completed: subprocess.Popen[bytes] | None = None
|
|
try:
|
|
completed = subprocess.Popen(
|
|
[
|
|
"node",
|
|
str(_BROWSER_HELPER),
|
|
"--input",
|
|
str(input_path),
|
|
"--output",
|
|
str(output_path),
|
|
],
|
|
cwd=WEBUI_ROOT,
|
|
env=_browser_child_environment(),
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
# Leave the helper enough time to serialize its bounded failure result
|
|
# after the last Playwright assertion reaches its own timeout.
|
|
deadline = (
|
|
time.monotonic()
|
|
+ min(
|
|
case.remaining_budget.wall_ms - 5_000,
|
|
timeout_ms + 10_000,
|
|
)
|
|
/ 1000
|
|
)
|
|
handled_commands = 0
|
|
while completed.poll() is None:
|
|
if time.monotonic() >= deadline:
|
|
completed.kill()
|
|
completed.wait(timeout=5)
|
|
raise DriverBudgetError("browser case exhausted its wall-clock allowance")
|
|
if command_path.exists():
|
|
try:
|
|
command = json.loads(command_path.read_text(encoding="utf-8"))
|
|
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
raise DriverAssertionError("browser restart command is invalid") from exc
|
|
command_path.unlink(missing_ok=True)
|
|
ready_path.unlink(missing_ok=True)
|
|
action = str(command.get("action") or "") if isinstance(command, Mapping) else ""
|
|
if action not in {"restart_graceful", "restart_forced", "reconfigure_retry"}:
|
|
raise DriverAssertionError("browser requested an unsupported lifecycle action")
|
|
handled_commands += 1
|
|
if handled_commands > 2:
|
|
raise DriverAssertionError(
|
|
"browser requested too many Gateway lifecycle actions"
|
|
)
|
|
if restart_callback is not None:
|
|
restart_callback(action)
|
|
else:
|
|
gateway.restart(force=action != "restart_graceful")
|
|
ready_path.write_text('{"ready":true}\n', encoding="utf-8")
|
|
os.chmod(ready_path, 0o600)
|
|
time.sleep(0.05)
|
|
evidence = _load_browser_evidence(output_path, return_code=completed.returncode or 0)
|
|
primary_session = str(payload["sessionKey"])
|
|
return (
|
|
evidence,
|
|
queue_marker,
|
|
primary_session,
|
|
{primary_session, str(payload["alternateSessionKey"])},
|
|
)
|
|
finally:
|
|
if completed is not None and completed.poll() is None:
|
|
completed.kill()
|
|
with contextlib.suppress(subprocess.TimeoutExpired):
|
|
completed.wait(timeout=5)
|
|
|
|
|
|
def _payload(
|
|
*,
|
|
status: str,
|
|
stage: str,
|
|
latency_ms: int,
|
|
physical_requests: int,
|
|
usage: Mapping[str, int],
|
|
cost: Mapping[str, float],
|
|
counts: Mapping[str, int],
|
|
metrics: Mapping[str, float],
|
|
failure_class: str | None = None,
|
|
) -> dict[str, Any]:
|
|
result: dict[str, Any] = {
|
|
"status": status,
|
|
"stage": stage,
|
|
"physical_requests": physical_requests,
|
|
"latency_ms": max(0, int(latency_ms)),
|
|
"usage": {str(key): int(value) for key, value in usage.items()},
|
|
"cost": {str(key): float(value) for key, value in cost.items()},
|
|
"counts": {str(key): int(value) for key, value in counts.items()},
|
|
"metrics": {str(key): float(value) for key, value in metrics.items()},
|
|
}
|
|
if failure_class is not None:
|
|
result["failure_class"] = failure_class
|
|
return result
|
|
|
|
|
|
def _budget_was_exceeded(case: LiveCase, result: Mapping[str, Any]) -> bool:
|
|
raw_usage = result.get("usage")
|
|
raw_cost = result.get("cost")
|
|
usage: Mapping[str, Any] = raw_usage if isinstance(raw_usage, Mapping) else {}
|
|
cost: Mapping[str, Any] = raw_cost if isinstance(raw_cost, Mapping) else {}
|
|
return bool(
|
|
int(result.get("physical_requests") or 0) > case.remaining_budget.physical_requests
|
|
or int(usage.get("total_tokens") or 0) > case.remaining_budget.billed_tokens
|
|
or float(cost.get("billed_cost_usd") or 0.0) > case.remaining_budget.billed_cost_usd
|
|
)
|
|
|
|
|
|
def _run_rpc_case(
|
|
case: LiveCase,
|
|
gateway: GatewayProcess,
|
|
*,
|
|
proxy: DeterministicFaultProxy | None,
|
|
) -> dict[str, Any]:
|
|
prompt, marker = prompt_for_case(case, gateway)
|
|
expected_marker = "synthetic complete" if case.scenario.startswith("fault_") else marker
|
|
session_key = f"agent:main:webchat:{uuid.uuid4().hex[:12]}"
|
|
timeout_seconds = max(
|
|
30.0,
|
|
min(28 * 60.0, case.remaining_budget.wall_ms / 1000 - 15.0),
|
|
)
|
|
started = time.monotonic()
|
|
observation, assistant_bytes, assistant_markers = asyncio.run(
|
|
_send_and_observe(
|
|
gateway,
|
|
prompt=prompt,
|
|
marker=expected_marker,
|
|
session_key=session_key,
|
|
timeout_seconds=timeout_seconds,
|
|
)
|
|
)
|
|
if case.scenario == "tool_compaction":
|
|
if asyncio.run(_manual_compaction(gateway, session_key)):
|
|
observation.compactions += 1
|
|
# The case owns an isolated Gateway and disables provider-backed naming.
|
|
# Count every call record so an unexpected auxiliary request cannot hide
|
|
# outside the selected session and silently evade the hard budget.
|
|
records = gateway.raw_records()
|
|
physical_requests = sum(record.get("kind") == "llm_request" for record in records)
|
|
if proxy is not None:
|
|
physical_requests = max(physical_requests, len(proxy.records))
|
|
raw_usage, raw_cost = _accounting_from_records(records)
|
|
counts, metrics = _base_evidence(
|
|
observation,
|
|
physical_requests=physical_requests,
|
|
records=records,
|
|
)
|
|
counts["output_bytes"] = assistant_bytes
|
|
counts["compactions"] = observation.compactions
|
|
if case.scenario == "router":
|
|
counts["router_decisions"] = asyncio.run(_router_decision_count(gateway, session_key))
|
|
if case.scenario == "fault_429_retry_after" and proxy is not None:
|
|
records_snapshot = proxy.records
|
|
if len(records_snapshot) >= 2:
|
|
retry_wait_ms = max(
|
|
0.0,
|
|
(
|
|
records_snapshot[1].received_monotonic_ns
|
|
- records_snapshot[0].received_monotonic_ns
|
|
)
|
|
/ 1_000_000,
|
|
)
|
|
metrics["retry_wait_ms"] = retry_wait_ms
|
|
counts["retry_after_honored"] = int(retry_wait_ms >= 8_000)
|
|
if case.scenario == "fallback":
|
|
assert case.fallback_provider is not None
|
|
counts["fallback_before_request"] = int(
|
|
_fallback_preceded_backup_usage_start(
|
|
gateway,
|
|
observation,
|
|
)
|
|
)
|
|
|
|
durable_accounting = _add_durable_accounting_evidence(
|
|
gateway,
|
|
counts=counts,
|
|
expected_provider_legs=max(
|
|
physical_requests,
|
|
_MINIMUM_PHYSICAL_REQUESTS[case.scenario],
|
|
),
|
|
)
|
|
physical_requests = max(physical_requests, durable_accounting.event_count)
|
|
counts["provider_legs"] = physical_requests
|
|
accounting_complete = durable_accounting.event_count == physical_requests
|
|
durable_usage, durable_cost = _accounting_from_durable(durable_accounting)
|
|
usage, cost = _merge_accounting_lower_bounds(
|
|
raw_usage,
|
|
raw_cost,
|
|
durable_usage,
|
|
durable_cost,
|
|
)
|
|
|
|
partial_terminal_is_expected = case.scenario == "fault_partial_then_reset"
|
|
passed = observation.completed or (
|
|
partial_terminal_is_expected
|
|
and bool(observation.terminal_event)
|
|
and observation.text_chunks > 0
|
|
)
|
|
if not partial_terminal_is_expected and assistant_markers < 1:
|
|
passed = False
|
|
if case.scenario == "tool_compaction" and observation.compactions < 1:
|
|
passed = False
|
|
if not accounting_complete:
|
|
passed = False
|
|
elapsed_ms = int((time.monotonic() - started) * 1000)
|
|
return _payload(
|
|
status="passed" if passed else "failed",
|
|
stage="terminal" if passed else "provider",
|
|
latency_ms=elapsed_ms,
|
|
physical_requests=physical_requests,
|
|
usage=usage,
|
|
cost=cost,
|
|
counts=counts,
|
|
metrics=metrics,
|
|
failure_class=None if passed else _failure_class_from_records(records),
|
|
)
|
|
|
|
|
|
def _run_browser_case(
|
|
case: LiveCase,
|
|
gateway: GatewayProcess,
|
|
*,
|
|
retry_proxy: DeterministicFaultProxy | None = None,
|
|
performance_evidence: PerformanceGateEvidence | None = None,
|
|
) -> dict[str, Any]:
|
|
prompt, marker = prompt_for_case(case, gateway)
|
|
started = time.monotonic()
|
|
|
|
def restart_callback(action: str) -> None:
|
|
if action == "reconfigure_retry":
|
|
if retry_proxy is None:
|
|
raise DriverAssertionError("retry phase requested without its fault proxy")
|
|
gateway.stop(force=True)
|
|
gateway.write_config(
|
|
primary_base_url=retry_proxy.base_url,
|
|
force_router=False,
|
|
)
|
|
gateway.start()
|
|
return
|
|
gateway.restart(force=action == "restart_forced")
|
|
|
|
evidence, queue_marker, primary_session, _session_keys = _run_browser_helper(
|
|
case,
|
|
gateway,
|
|
prompt=prompt,
|
|
marker=marker,
|
|
restart_callback=restart_callback,
|
|
)
|
|
records = gateway.raw_records()
|
|
physical_requests = sum(record.get("kind") == "llm_request" for record in records)
|
|
if retry_proxy is not None:
|
|
physical_requests = max(physical_requests, len(retry_proxy.records))
|
|
raw_usage, raw_cost = _accounting_from_records(records)
|
|
counts = dict(evidence.counts)
|
|
metrics = dict(evidence.metrics)
|
|
if case.scenario == "long_answer":
|
|
if performance_evidence is None:
|
|
raise DriverAssertionError("long answer is missing deterministic performance evidence")
|
|
counts.update(performance_evidence.counts)
|
|
metrics.update(performance_evidence.metrics)
|
|
counts["provider_legs"] = physical_requests
|
|
durable_accounting = _add_durable_accounting_evidence(
|
|
gateway,
|
|
counts=counts,
|
|
expected_provider_legs=max(
|
|
physical_requests,
|
|
_MINIMUM_PHYSICAL_REQUESTS[case.scenario],
|
|
),
|
|
)
|
|
physical_requests = max(physical_requests, durable_accounting.event_count)
|
|
counts["provider_legs"] = physical_requests
|
|
accounting_complete = durable_accounting.event_count == physical_requests
|
|
durable_usage, durable_cost = _accounting_from_durable(durable_accounting)
|
|
usage, cost = _merge_accounting_lower_bounds(
|
|
raw_usage,
|
|
raw_cost,
|
|
durable_usage,
|
|
durable_cost,
|
|
)
|
|
|
|
if case.scenario.startswith("queue_"):
|
|
client = GatewayRPCClient(scopes=["operator.admin"], request_timeout_s=30.0)
|
|
|
|
async def queue_history_count() -> tuple[int, int, int]:
|
|
await client.connect(gateway.ws_url)
|
|
try:
|
|
(
|
|
_assistant_bytes,
|
|
_assistant_count,
|
|
user_count,
|
|
attachment_count,
|
|
) = await _history_evidence(
|
|
client,
|
|
session_key=primary_session,
|
|
user_marker=queue_marker,
|
|
)
|
|
pending = await client.call(
|
|
"sessions.pending_inputs.list",
|
|
{"key": primary_session},
|
|
)
|
|
pending_items = pending.get("items", []) if isinstance(pending, Mapping) else []
|
|
pending_count = len(pending_items) if isinstance(pending_items, list) else -1
|
|
return user_count, attachment_count, pending_count
|
|
finally:
|
|
await client.close()
|
|
|
|
transcript_occurrences, attachment_occurrences, pending_count = asyncio.run(
|
|
queue_history_count()
|
|
)
|
|
counts["transcript_occurrences"] = transcript_occurrences
|
|
counts["dispatched_inputs"] = transcript_occurrences
|
|
counts["attachment_inputs"] = attachment_occurrences
|
|
counts["pending_inputs_remaining"] = max(0, pending_count)
|
|
counts["queue_exact_once"] = int(
|
|
transcript_occurrences == 1 and attachment_occurrences == 1 and pending_count == 0
|
|
)
|
|
elif case.scenario == "browser_stop_each_phase":
|
|
client = GatewayRPCClient(scopes=["operator.admin"], request_timeout_s=30.0)
|
|
|
|
async def stop_history_count() -> int:
|
|
await client.connect(gateway.ws_url)
|
|
try:
|
|
return await _cancelled_webui_stop_count(
|
|
client,
|
|
session_key=primary_session,
|
|
)
|
|
finally:
|
|
await client.close()
|
|
|
|
# Do not trust the optimistic UI click count as cancellation proof.
|
|
# The durable task ledger must project four explicit webui_stop outcomes.
|
|
counts["cancelled_turns"] = asyncio.run(stop_history_count())
|
|
|
|
passed = evidence.status == "passed" and accounting_complete
|
|
elapsed_ms = int((time.monotonic() - started) * 1000)
|
|
return _payload(
|
|
status="passed" if passed else "failed",
|
|
stage="browser" if passed else "browser_assertion",
|
|
latency_ms=elapsed_ms,
|
|
physical_requests=physical_requests,
|
|
usage=usage,
|
|
cost=cost,
|
|
counts=counts,
|
|
metrics=metrics,
|
|
failure_class=None if passed else "assertion",
|
|
)
|
|
|
|
|
|
def _accounted_runtime_failure(
|
|
gateway: GatewayProcess,
|
|
*,
|
|
proxy: DeterministicFaultProxy | None,
|
|
retry_proxy: DeterministicFaultProxy | None,
|
|
stage: str,
|
|
failure_class: str,
|
|
started_monotonic: float,
|
|
) -> dict[str, Any]:
|
|
records = gateway.raw_records()
|
|
physical_requests = sum(record.get("kind") == "llm_request" for record in records)
|
|
if proxy is not None:
|
|
physical_requests = max(physical_requests, len(proxy.records))
|
|
if retry_proxy is not None:
|
|
physical_requests = max(physical_requests, len(retry_proxy.records))
|
|
accounted_legs, missing_cost_entries = _durable_accounting_from_database(gateway)
|
|
physical_requests = max(physical_requests, accounted_legs)
|
|
usage, cost = _accounting_from_records(records)
|
|
counts = {
|
|
"provider_legs": physical_requests,
|
|
"accounted_provider_legs": accounted_legs,
|
|
"usage_missing_cost_entries": missing_cost_entries,
|
|
}
|
|
return _payload(
|
|
status="inconclusive" if failure_class == "budget" else "failed",
|
|
stage=stage,
|
|
latency_ms=int((time.monotonic() - started_monotonic) * 1000),
|
|
physical_requests=physical_requests,
|
|
usage=usage,
|
|
cost=cost,
|
|
counts=counts,
|
|
metrics={},
|
|
failure_class=failure_class,
|
|
)
|
|
|
|
|
|
def execute_case(case: LiveCase) -> tuple[dict[str, Any], int]:
|
|
required_envs = {CREDENTIAL_ENV_BY_PROVIDER[case.provider]}
|
|
if case.fallback_provider:
|
|
required_envs.add(CREDENTIAL_ENV_BY_PROVIDER[case.fallback_provider])
|
|
secret_values = tuple(
|
|
value for name in sorted(required_envs) if (value := os.environ.get(name, "").strip())
|
|
)
|
|
gateway = GatewayProcess(case, secret_values=secret_values)
|
|
proxy: DeterministicFaultProxy | None = None
|
|
retry_proxy: DeterministicFaultProxy | None = None
|
|
result: dict[str, Any] | None = None
|
|
gateway_started = False
|
|
performance_evidence: PerformanceGateEvidence | None = None
|
|
started_monotonic = time.monotonic()
|
|
try:
|
|
if case.scenario.startswith("fault_") or case.scenario == "fallback":
|
|
proxy = DeterministicFaultProxy(
|
|
_fault_sequence(case.scenario),
|
|
late_terminal_delay_seconds=0.25,
|
|
).start()
|
|
if case.scenario == "browser_stop_each_phase":
|
|
_write_tool_fixture(gateway)
|
|
retry_proxy = DeterministicFaultProxy(
|
|
(FaultScenario.RATE_LIMITED, FaultScenario.OK),
|
|
).start()
|
|
if case.scenario == "fallback":
|
|
assert proxy is not None
|
|
gateway.write_config(routed_base_url=proxy.base_url)
|
|
elif case.scenario.startswith("fault_"):
|
|
assert proxy is not None
|
|
gateway.write_config(primary_base_url=proxy.base_url)
|
|
else:
|
|
gateway.write_config()
|
|
gateway.start()
|
|
gateway_started = True
|
|
if case.scenario == "long_answer":
|
|
# Reports are read only after the isolated Gateway exists so a
|
|
# malformed or absent report becomes a normal assertion row with
|
|
# zero provider requests, not a coordinator configuration crash.
|
|
performance_evidence = _load_performance_gate_evidence()
|
|
|
|
if (
|
|
case.scenario in {"long_answer", "long_reasoning"}
|
|
or case.scenario.startswith("browser_")
|
|
or case.scenario.startswith("queue_")
|
|
):
|
|
result = _run_browser_case(
|
|
case,
|
|
gateway,
|
|
retry_proxy=retry_proxy,
|
|
performance_evidence=performance_evidence,
|
|
)
|
|
else:
|
|
result = _run_rpc_case(case, gateway, proxy=proxy)
|
|
if _budget_was_exceeded(case, result):
|
|
result["status"] = "inconclusive"
|
|
result["stage"] = "budget"
|
|
result["failure_class"] = "budget"
|
|
exit_code = EXIT_BUDGET
|
|
else:
|
|
exit_code = EXIT_PASSED if result["status"] == "passed" else EXIT_FAILED
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except DriverBudgetError:
|
|
if not gateway_started:
|
|
raise
|
|
result = _accounted_runtime_failure(
|
|
gateway,
|
|
proxy=proxy,
|
|
retry_proxy=retry_proxy,
|
|
stage="budget",
|
|
failure_class="budget",
|
|
started_monotonic=started_monotonic,
|
|
)
|
|
exit_code = EXIT_BUDGET
|
|
except DriverConfigurationError:
|
|
if not gateway_started:
|
|
raise
|
|
result = _accounted_runtime_failure(
|
|
gateway,
|
|
proxy=proxy,
|
|
retry_proxy=retry_proxy,
|
|
stage="runtime_configuration",
|
|
failure_class="configuration",
|
|
started_monotonic=started_monotonic,
|
|
)
|
|
exit_code = EXIT_CONFIGURATION
|
|
except DriverAssertionError:
|
|
result = _accounted_runtime_failure(
|
|
gateway,
|
|
proxy=proxy,
|
|
retry_proxy=retry_proxy,
|
|
stage="browser_assertion",
|
|
failure_class="assertion",
|
|
started_monotonic=started_monotonic,
|
|
)
|
|
exit_code = EXIT_FAILED
|
|
except TimeoutError:
|
|
result = _accounted_runtime_failure(
|
|
gateway,
|
|
proxy=proxy,
|
|
retry_proxy=retry_proxy,
|
|
stage="case_timeout",
|
|
failure_class="inconclusive",
|
|
started_monotonic=started_monotonic,
|
|
)
|
|
exit_code = EXIT_FAILED
|
|
except Exception:
|
|
result = _accounted_runtime_failure(
|
|
gateway,
|
|
proxy=proxy,
|
|
retry_proxy=retry_proxy,
|
|
stage="driver",
|
|
failure_class="implementation",
|
|
started_monotonic=started_monotonic,
|
|
)
|
|
exit_code = EXIT_FAILED
|
|
finally:
|
|
if proxy is not None:
|
|
proxy.close()
|
|
if retry_proxy is not None:
|
|
retry_proxy.close()
|
|
try:
|
|
gateway.cleanup()
|
|
except Exception as exc:
|
|
if result is None:
|
|
raise
|
|
result.update(
|
|
{
|
|
"status": "failed",
|
|
"stage": _artifact_cleanup_stage(exc),
|
|
"failure_class": "implementation",
|
|
}
|
|
)
|
|
exit_code = EXIT_FAILED
|
|
assert result is not None
|
|
return result, exit_code
|
|
|
|
|
|
def _failure_payload(*, stage: str, failure_class: str) -> dict[str, Any]:
|
|
return _payload(
|
|
status="failed" if failure_class != "budget" else "inconclusive",
|
|
stage=stage,
|
|
latency_ms=0,
|
|
physical_requests=0,
|
|
usage={},
|
|
cost={},
|
|
counts={},
|
|
metrics={},
|
|
failure_class=failure_class,
|
|
)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--case-file", required=True, type=Path)
|
|
parser.add_argument("--output", required=True, type=Path)
|
|
args = parser.parse_args(argv)
|
|
|
|
case_path: Path | None = None
|
|
output_path: Path | None = None
|
|
secret_values: tuple[str, ...] = ()
|
|
result: dict[str, Any]
|
|
try:
|
|
case_path = _safe_case_path(args.case_file)
|
|
output_path = _safe_output_path(args.output, case_path=case_path)
|
|
case = load_case(case_path)
|
|
required_envs = {CREDENTIAL_ENV_BY_PROVIDER[case.provider]}
|
|
if case.fallback_provider:
|
|
required_envs.add(CREDENTIAL_ENV_BY_PROVIDER[case.fallback_provider])
|
|
secret_values = tuple(
|
|
value for name in sorted(required_envs) if (value := os.environ.get(name, "").strip())
|
|
)
|
|
result, exit_code = execute_case(case)
|
|
except KeyboardInterrupt:
|
|
return EXIT_INTERRUPTED
|
|
except DriverBudgetError:
|
|
result = _failure_payload(stage="budget", failure_class="budget")
|
|
exit_code = EXIT_BUDGET
|
|
except DriverConfigurationError:
|
|
result = _failure_payload(stage="driver_preflight", failure_class="configuration")
|
|
exit_code = EXIT_CONFIGURATION
|
|
except DriverAssertionError:
|
|
result = _failure_payload(stage="browser_assertion", failure_class="assertion")
|
|
exit_code = EXIT_FAILED
|
|
except TimeoutError:
|
|
result = _failure_payload(stage="case_timeout", failure_class="inconclusive")
|
|
exit_code = EXIT_FAILED
|
|
except Exception:
|
|
# Never serialize an exception: provider errors can contain headers or
|
|
# response bodies. The bounded taxonomy is the complete public detail.
|
|
result = _failure_payload(stage="driver", failure_class="implementation")
|
|
exit_code = EXIT_FAILED
|
|
|
|
if output_path is None:
|
|
if case_path is None:
|
|
with contextlib.suppress(Exception):
|
|
case_path = _safe_case_path(args.case_file)
|
|
if case_path is None:
|
|
return EXIT_CONFIGURATION
|
|
try:
|
|
output_path = _safe_output_path(args.output, case_path=case_path)
|
|
except DriverConfigurationError:
|
|
return EXIT_CONFIGURATION
|
|
write_safe_report(output_path, result, secret_values)
|
|
return exit_code
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|