Files
wehub-resource-sync 2c632336aa
CI / Viewer CI (push) Successful in 13m37s
CI / Core CI (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:38 +08:00

382 lines
12 KiB
Python

"""
Shared run identifiers, context objects, and small runtime helpers.
"""
from __future__ import annotations
import hashlib
import json
import platform
import subprocess
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from agent.cost import parse_max_cost_usd
from agent.providers.factory import (
ProviderConfig,
default_model_id,
infer_provider_from_model_id,
)
from agent.run_config import SingleRunSettings
from storage.record_authors import resolve_current_record_author
from storage.repo import StorageRepo
from storage.revisions import INITIAL_REVISION_ID, validate_revision_id
from storage.trajectories import ensure_shared_system_prompt_file
MAX_SINGLE_RUN_SLUG_LEN = 120
@dataclass(slots=True, frozen=True)
class SingleRunContext:
repo_root: Path
created_at: str
run_id: str
record_id: str
revision_id: str
run_dir: Path
staging_dir: Path
staging_prompt_path: Path
script_path: Path
checkpoint_urdf_path: Path
trace_dir: Path
cost_path: Path
record_dir: Path
record_revision_dir: Path
record_prompt_path: Path
record_model_path: Path
record_provenance_path: Path
record_cost_path: Path
record_trace_dir: Path
record_inputs_dir: Path
record_urdf_path: Path
@dataclass(slots=True, frozen=True)
class RunExecutionOutcome:
exit_code: int
run_id: str
record_id: str
status: str
message: str | None = None
record_dir: Path | None = None
staging_dir: Path | None = None
turn_count: int | None = None
tool_call_count: int | None = None
compile_attempt_count: int | None = None
provider: str | None = None
model_id: str | None = None
sdk_package: str | None = None
def _read_logged_cost_totals(cost_path: Path) -> tuple[dict[str, int] | None, float | None]:
try:
payload = json.loads(cost_path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
return None, None
if not isinstance(payload, dict):
return None, None
total = payload.get("all_in_total")
if not isinstance(total, dict):
total = payload.get("total")
if not isinstance(total, dict):
return None, None
raw_tokens = total.get("tokens")
tokens: dict[str, int] | None = None
if isinstance(raw_tokens, dict):
parsed_tokens: dict[str, int] = {}
for key in (
"prompt_tokens",
"cached_tokens",
"uncached_prompt_tokens",
"candidates_tokens",
"total_tokens",
):
value = raw_tokens.get(key)
if isinstance(value, int):
parsed_tokens[key] = value
if parsed_tokens:
tokens = parsed_tokens
costs_usd = total.get("costs_usd")
amount = costs_usd.get("total") if isinstance(costs_usd, dict) else None
total_cost = float(amount) if isinstance(amount, (int, float)) else None
return tokens, total_cost
def _slugify(text: str) -> str:
cleaned: list[str] = []
last_dash = False
for ch in text.lower():
if ch.isalnum():
cleaned.append(ch)
last_dash = False
elif not last_dash:
cleaned.append("-")
last_dash = True
slug = "".join(cleaned).strip("-")
return slug or "object"
def _build_single_run_slug(prompt: str) -> str:
base = _slugify(prompt) or "object"
if len(base) <= MAX_SINGLE_RUN_SLUG_LEN:
return base
digest = hashlib.sha1(prompt.encode("utf-8")).hexdigest()[:10]
reserved = len("_") + len(digest)
max_base_len = max(1, MAX_SINGLE_RUN_SLUG_LEN - reserved)
truncated_base = base[:max_base_len].rstrip("-") or "object"
return f"{truncated_base}_{digest}"
def _utc_now(now: datetime | None = None) -> str:
current = now or datetime.now(timezone.utc)
if current.tzinfo is None:
current = current.replace(tzinfo=timezone.utc)
return (
current.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
)
def _timestamp_token(now: datetime | None = None) -> str:
current = now or datetime.now(timezone.utc)
if current.tzinfo is None:
current = current.replace(tzinfo=timezone.utc)
return current.astimezone(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
def _sha256_text(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def _sha256_file(path: Path) -> str | None:
if not path.exists():
return None
return hashlib.sha256(path.read_bytes()).hexdigest()
def _ensure_shared_system_prompt(
storage_repo: StorageRepo,
system_prompt_path: Path,
) -> str:
shared_path = ensure_shared_system_prompt_file(storage_repo, system_prompt_path)
return shared_path.stem
def _relative_to_repo(path: Path, repo_root: Path) -> str:
try:
return path.resolve().relative_to(repo_root.resolve()).as_posix()
except ValueError:
return path.resolve().as_posix()
def _prompt_preview(prompt: str, *, max_len: int = 160) -> str:
collapsed = " ".join(prompt.split())
if len(collapsed) <= max_len:
return collapsed
return collapsed[: max_len - 3].rstrip() + "..."
def _display_title(prompt: str, *, label: str | None = None) -> str:
if label and label.strip():
return label.strip()
first_line = next((line.strip() for line in prompt.splitlines() if line.strip()), "")
return first_line[:120] if first_line else "Untitled run"
def _detect_git_commit(repo_root: Path) -> str | None:
try:
result = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=repo_root,
check=True,
capture_output=True,
text=True,
)
except Exception:
return None
commit = result.stdout.strip()
return commit or None
def _resolve_runtime_record_author(repo_root: Path) -> str | None:
try:
return resolve_current_record_author(repo_root)
except Exception:
return None
def _detect_uv_lock_sha256(repo_root: Path) -> str | None:
return _sha256_file(repo_root / "uv.lock")
def _platform_id() -> str:
return f"{platform.system().lower()}-{platform.machine().lower()}"
def _infer_provider_from_model_id(model_id: str | None) -> str | None:
return infer_provider_from_model_id(model_id)
def _default_model_id(
*,
provider: str,
model_id: str | None,
thinking_level: str,
openai_transport: str,
openai_reasoning_summary: str | None,
) -> str:
return default_model_id(
ProviderConfig(
provider=provider,
model_id=model_id,
thinking_level=thinking_level,
openai_transport=openai_transport,
openai_reasoning_summary=openai_reasoning_summary,
)
)
def _single_run_settings_summary(
*,
provider: str,
model_id: str,
thinking_level: str,
max_turns: int,
system_prompt_path: str,
sdk_package: str,
openai_transport: str,
openai_reasoning_summary: str | None,
max_cost_usd: float | None,
) -> dict[str, Any]:
return SingleRunSettings(
provider=provider,
model_id=model_id,
thinking_level=thinking_level,
max_turns=max_turns,
system_prompt_path=system_prompt_path,
sdk_package=sdk_package,
openai_transport=openai_transport,
openai_reasoning_summary=openai_reasoning_summary,
max_cost_usd=max_cost_usd,
).to_summary()
def _first_string(value: Any, default: str) -> str:
text = str(value or "").strip()
return text or default
def _optional_string(value: Any) -> str | None:
text = str(value or "").strip()
return text or None
def _optional_bool(value: Any) -> bool | None:
if isinstance(value, bool):
return value
if value is None:
return None
text = str(value).strip().lower()
if not text:
return None
if text in {"1", "true", "t", "yes", "y", "on"}:
return True
if text in {"0", "false", "f", "no", "n", "off"}:
return False
return None
def _optional_max_cost_usd(value: Any, *, label: str) -> float | None:
try:
return parse_max_cost_usd(value, label=label)
except ValueError as exc:
raise ValueError(str(exc)) from exc
def _thinking_level_from_run_parameters(
storage_repo: StorageRepo,
*,
run_id: str | None,
record_id: str,
) -> str | None:
normalized_run_id = _optional_string(run_id)
if not normalized_run_id:
return None
run_metadata = storage_repo.read_json(storage_repo.layout.run_metadata_path(normalized_run_id))
if isinstance(run_metadata, dict):
settings_summary = run_metadata.get("settings_summary")
if isinstance(settings_summary, dict):
thinking_level = _optional_string(settings_summary.get("thinking_level"))
if thinking_level:
return thinking_level
thinking_levels = settings_summary.get("thinking_levels")
if isinstance(thinking_levels, list) and len(thinking_levels) == 1:
return _optional_string(thinking_levels[0])
allocations = storage_repo.read_json(
storage_repo.layout.run_allocations_path(normalized_run_id)
)
rows = allocations.get("rows") if isinstance(allocations, dict) else None
if not isinstance(rows, list):
return None
for row in rows:
if not isinstance(row, dict):
continue
if _optional_string(row.get("record_id")) != record_id:
continue
return _optional_string(row.get("thinking_level"))
return None
def _build_single_run_context(
*,
repo_root: Path,
prompt: str,
storage_repo: StorageRepo,
record_id: str | None = None,
run_id: str | None = None,
revision_id: str | None = None,
now: datetime | None = None,
) -> SingleRunContext:
created_at = _utc_now(now)
token = _timestamp_token(now)
prompt_slug = _build_single_run_slug(prompt)[:48]
digest = hashlib.sha1(prompt.encode("utf-8")).hexdigest()[:8]
resolved_run_id = run_id or f"run_{token}_{digest}"
resolved_record_id = record_id or f"rec_{prompt_slug}_{token}_{digest}"
resolved_revision_id = validate_revision_id(revision_id or INITIAL_REVISION_ID)
run_dir = storage_repo.layout.run_dir(resolved_run_id)
staging_dir = storage_repo.layout.run_staging_dir(resolved_run_id) / resolved_record_id
staging_dir.mkdir(parents=True, exist_ok=True)
record_dir = storage_repo.layout.record_dir(resolved_record_id)
revision_dir = storage_repo.layout.record_revision_dir(resolved_record_id, resolved_revision_id)
return SingleRunContext(
repo_root=repo_root.resolve(),
created_at=created_at,
run_id=resolved_run_id,
record_id=resolved_record_id,
revision_id=resolved_revision_id,
run_dir=run_dir,
staging_dir=staging_dir,
staging_prompt_path=staging_dir / "prompt.txt",
script_path=staging_dir / "model.py",
checkpoint_urdf_path=staging_dir / "model.urdf",
trace_dir=staging_dir / "traces",
cost_path=staging_dir / "cost.json",
record_dir=record_dir,
record_revision_dir=revision_dir,
record_prompt_path=revision_dir / "prompt.txt",
record_model_path=revision_dir / "model.py",
record_provenance_path=revision_dir / "provenance.json",
record_cost_path=revision_dir / "cost.json",
record_trace_dir=revision_dir / "traces",
record_inputs_dir=revision_dir / "inputs",
record_urdf_path=storage_repo.layout.record_materialization_urdf_path(resolved_record_id),
)