2026-09-22 23:21:59 +03:00
|
|
|
# orchestrator/run_context.py
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import secrets
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from datetime import datetime, timezone
|
2026-09-23 22:42:21 +03:00
|
|
|
from pathlib import Path
|
2026-09-22 23:21:59 +03:00
|
|
|
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:
|
2026-09-23 22:42:21 +03:00
|
|
|
agent_dir: Path
|
|
|
|
|
|
2026-09-22 23:21:59 +03:00
|
|
|
# ── project-level ──────────────────────────────────────────
|
|
|
|
|
youtrack_project: str
|
2026-09-23 22:42:21 +03:00
|
|
|
git_name: str
|
|
|
|
|
git_email: str
|
|
|
|
|
gitea_repo: str = ""
|
2026-09-22 23:21:59 +03:00
|
|
|
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
|
|
|
|
|
|
2026-09-23 22:42:21 +03:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-09-22 23:21:59 +03:00
|
|
|
|
|
|
|
|
@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}"
|