# orchestrator/run_context.py from __future__ import annotations import secrets from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path from typing import Any from agents.coders.state import AgentState from common.gitea_mcp_client import GiteaMCPClient from common.llm_client import LLMClient from common.time import now_iso, now_iso_plus from common.youtrack_mcp_client import YouTrackMCPClient @dataclass(frozen=True) class AgentConfig: agent_dir: Path # ── project-level ────────────────────────────────────────── youtrack_project: str git_name: str git_email: str gitea_repo: str = "" gitea_target_branch: str = "main" # ── limits (snapshotted into state at run start) ─────────── max_plan_attempts: int = 3 max_act_attempts: int = 5 max_ci_self_resolve_attempts: int = 2 max_clarification_rounds: int = 3 max_rework_rounds: int = 2 wall_clock_minutes: int = 60 token_budget: int = 500_000 # ── human-await timers ───────────────────────────────────── re_ping_after_days: int = 3 abandon_after_days: int = 5 # ── reviewer policy ──────────────────────────────────────── reviewer_fallback: str = "codeowners" # codeowners | blame | on_call # ── language hints (for Plan) ────────────────────────────── language_markers: dict[str, list[str]] = None # filled from project def project_mem_dir(self): p = self.agent_dir / "memory" / f"{self.youtrack_project}" if not p.exists(): p.mkdir(parents=True, exist_ok=True) return p @dataclass class RunContext: """ Everything a handler needs to do its job: the ports it talks to, the persisted state for this issue, and the identifiers it must stamp into comments and state. """ # ── identifiers ──────────────────────────────────────────── agent_id: str issue_id: str run_id: str # one logical attempt; stable across calls attempt: int # retry counter within the run # ── ports (outbound systems) ─────────────────────────────── llm: LLMClient youtrack: YouTrackMCPClient gitea: GiteaMCPClient # ── persisted pipeline state for this issue ──────────────── state: AgentState # ── static config for this run ───────────────────────────── config: AgentConfig # ── convenience (thin, no hidden behavior) ───────────────── def now(self) -> str: return now_iso() def now_plus(self, **delta) -> str: return now_iso_plus(**delta) def new_run_id() -> str: """ Generate a run id: one logical attempt at a task. Format: run-YYYYMMDD-HHMMSS-<6 hex chars> Example: run-20260921-114500-a3f1c2 - Timestamp prefix makes ids sortable and human-readable in logs. - Random suffix prevents collisions when two runs start in the same second (e.g., parallel issues, fast retries, tests). - The "run-" prefix makes it obvious what kind of id this is when it appears next to a comment marker or in a state file. """ ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") suffix = secrets.token_hex(3) # 3 bytes -> 6 hex chars return f"run-{ts}-{suffix}"