Download core/runner.py from smlflg/AgentArena: direct link, hf CLI and curl.
- Browser
- Download file 15.4 kB
-
https://huggingface.co/smlflg/AgentArena/resolve/main/core/runner.py
- Command line
-
hf download hf://smlflg/AgentArena/core/runner.py
-
curl -L -o runner.py https://huggingface.co/smlflg/AgentArena/resolve/main/core/runner.py
15.4 kB
| import asyncio | |
| import json | |
| import logging | |
| import uuid | |
| import time | |
| import os | |
| from datetime import datetime | |
| from sqlalchemy.orm import Session | |
| from database import SessionLocal | |
| from models import Task, Agent, Run, RunStatus | |
| from core.sandbox import Sandbox | |
| from adapters import get_adapter | |
| from adapters.base import RunContext | |
| from core.metrics import ( | |
| compute_cost, | |
| parse_pytest_output, | |
| compute_trajectory_efficiency, | |
| analyze_trajectory, | |
| compute_diff_minimality, | |
| compute_composite_score, | |
| ) | |
| from core.judge import Judge, JudgeResult | |
| from core.alignment import ALIGNMENT_RUBRIC_VERSION | |
| logger = logging.getLogger(__name__) | |
| class Runner: | |
| def __init__(self): | |
| self.judge = Judge() | |
| self._active_runs: dict[int, dict] = {} # run_id -> progress info | |
| async def run_single(self, db: Session, task: Task, agent: Agent) -> Run: | |
| """Execute a single benchmark run.""" | |
| agent_config = json.loads(agent.config_json or "{}") | |
| replicate_index = self._next_replicate_index(db, task.id, agent.id) | |
| # 1. Create Run record | |
| run = Run( | |
| task_id=task.id, | |
| agent_id=agent.id, | |
| status=RunStatus.running, | |
| task_family=self._task_family(task), | |
| seed=str(agent_config.get("seed", "")) or None, | |
| replicate_index=replicate_index, | |
| experiment_id=agent_config.get("experiment_id"), | |
| arm_id=agent_config.get("arm_id") or agent.name, | |
| alignment_rubric_version=ALIGNMENT_RUBRIC_VERSION, | |
| created_at=datetime.utcnow(), | |
| ) | |
| db.add(run) | |
| db.commit() | |
| db.refresh(run) | |
| self._active_runs[run.id] = {"status": "starting", "started_at": time.time()} | |
| sandbox = None | |
| judge_sandbox = None | |
| start_time = time.time() | |
| try: | |
| # 2. Create agent sandbox | |
| self._active_runs[run.id]["status"] = "creating_sandbox" | |
| sandbox = await asyncio.to_thread(Sandbox.create, task) | |
| # 3. Get adapter and build RunContext | |
| self._active_runs[run.id]["status"] = "running_agent" | |
| adapter = get_adapter(agent.adapter_type, agent_config) | |
| ctx = RunContext( | |
| work_dir="/workspace", | |
| task_prompt=task.description or task.title, | |
| success_cmd=task.success_cmd, | |
| time_limit_s=task.time_limit_s, | |
| token_limit=task.token_limit, | |
| repo_url=task.repo_url or "", | |
| repo_ref=task.repo_ref or "main", | |
| ) | |
| # 4. Run adapter — returns RunResult dataclass | |
| adapter_result = await adapter.run(ctx, sandbox) | |
| # 5. Get diff — prefer adapter-provided diff (e.g. ClaudeCode runs on host), | |
| # fall back to sandbox.get_diff() for container-based adapters. | |
| self._active_runs[run.id]["status"] = "collecting_diff" | |
| if adapter_result.diff_text: | |
| diff_text = adapter_result.diff_text | |
| else: | |
| diff_text = await asyncio.to_thread(sandbox.get_diff) | |
| # 5b. Destroy agent container — judge must not share it | |
| await asyncio.to_thread(sandbox.destroy) | |
| sandbox = None | |
| # 6. Create isolated judge container (fresh clone + patch applied) | |
| self._active_runs[run.id]["status"] = "creating_judge_sandbox" | |
| judge_sandbox = await asyncio.to_thread( | |
| Sandbox.create_judge_sandbox, task, diff_text | |
| ) | |
| # Abort if patch failed to apply — running judge on unpatched repo gives false negatives | |
| if diff_text and diff_text.strip() and not judge_sandbox.patch_applied: | |
| patch_output = judge_sandbox.patch_output or "unknown" | |
| run.invalid_patch_flag = 1 | |
| raise RuntimeError(f"Patch failed to apply in judge container: {patch_output}") | |
| # 7. Judge in the isolated container | |
| self._active_runs[run.id]["status"] = "judging" | |
| judge_result = await asyncio.to_thread( | |
| self.judge.judge_run, judge_sandbox, task.success_cmd | |
| ) | |
| # 7. Compute metrics | |
| # Use adapter-measured duration (only CLI time), not wall-clock | |
| # which includes sandbox creation + judge overhead. | |
| agent_duration_s = adapter_result.duration_s | |
| end_to_end_duration_s = time.time() - start_time | |
| tokens_input = adapter_result.tokens_input | |
| tokens_output = adapter_result.tokens_output | |
| cost_usd = adapter_result.cost_usd | |
| trajectory = adapter_result.trajectory | |
| # Trajectory and diff analysis | |
| traj_metrics = analyze_trajectory(trajectory) | |
| diff_metrics = compute_diff_minimality(diff_text) | |
| functional_success = 1 if judge_result.functional_score >= 1.0 else 0 | |
| quality_score = ( | |
| judge_result.regression_score * 0.5 | |
| + judge_result.lint_score * 0.25 | |
| + judge_result.complexity_score * 0.25 | |
| ) | |
| final_status = RunStatus.success if functional_success else RunStatus.failure | |
| tests_total = judge_result.tests_total if judge_result.tests_total > 0 else adapter_result.tests_total | |
| tests_passed = judge_result.tests_passed if judge_result.tests_total > 0 else adapter_result.tests_passed | |
| test_pass_rate = (tests_passed / tests_total) if tests_total > 0 else None | |
| # Build full metrics JSON | |
| judge_metrics = { | |
| "functional_score": judge_result.functional_score, | |
| "functional_success": functional_success, | |
| "regression_score": judge_result.regression_score, | |
| "regression_tests_total": judge_result.regression_tests_total, | |
| "regression_tests_passed": judge_result.regression_tests_passed, | |
| "lint_score": judge_result.lint_score, | |
| "lint_delta": judge_result.lint_delta, | |
| "complexity_score": judge_result.complexity_score, | |
| "complexity_delta": judge_result.complexity_delta, | |
| "quality_score": quality_score, | |
| } | |
| # Normalize cost to 0-1 efficiency score (lower cost = higher score) | |
| efficiency = 1.0 / (1.0 + cost_usd / 0.50) if cost_usd > 0 else 1.0 | |
| composite_score = compute_composite_score({ | |
| "functionality": judge_result.functional_score, | |
| "directness_score": traj_metrics.get("directness_score", 0), | |
| "efficiency": efficiency, | |
| "self_correction_rate": traj_metrics.get("self_correction_rate", 0), | |
| "churn": diff_metrics.get("churn", 0), | |
| "loop_count": traj_metrics.get("loop_count", 0), | |
| "total_steps": traj_metrics.get("total_steps", 1), | |
| }) | |
| full_metrics = { | |
| "trajectory": traj_metrics, | |
| "diff": diff_metrics, | |
| "judge": judge_metrics, | |
| "composite_score": composite_score, | |
| "eval": { | |
| "functional_success": functional_success, | |
| "test_pass_rate": test_pass_rate, | |
| "tests_total": tests_total, | |
| "tests_passed": tests_passed, | |
| "agent_duration_s": agent_duration_s, | |
| "end_to_end_duration_s": round(end_to_end_duration_s, 2), | |
| "timeout_flag": 1 if adapter_result.status == RunStatus.timeout else 0, | |
| "tool_failure_flag": 1 if adapter_result.errors_count > 0 else 0, | |
| "invalid_patch_flag": 0, | |
| "task_family": run.task_family, | |
| "seed": run.seed, | |
| "replicate_index": replicate_index, | |
| "experiment_id": run.experiment_id, | |
| "arm_id": run.arm_id, | |
| "alignment_rubric_version": ALIGNMENT_RUBRIC_VERSION, | |
| }, | |
| } | |
| # 8. Update Run record | |
| run.status = final_status | |
| run.duration_s = agent_duration_s | |
| run.agent_duration_s = agent_duration_s | |
| run.end_to_end_duration_s = round(end_to_end_duration_s, 2) | |
| run.tokens_input = tokens_input | |
| run.tokens_output = tokens_output | |
| run.cost_usd = cost_usd | |
| run.tests_total = tests_total | |
| run.tests_passed = tests_passed | |
| run.functional_success = functional_success | |
| run.test_pass_rate = test_pass_rate | |
| run.quality_score = round(quality_score, 4) | |
| run.errors_count = adapter_result.errors_count | |
| run.tool_calls = adapter_result.tool_calls | |
| run.diff_text = diff_text | |
| run.trajectory = json.dumps(trajectory) | |
| run.judge_score = judge_result.overall_score | |
| run.judge_notes = judge_result.overall_notes | |
| run.timeout_flag = 1 if adapter_result.status == RunStatus.timeout else 0 | |
| run.tool_failure_flag = 1 if adapter_result.errors_count > 0 else 0 | |
| run.invalid_patch_flag = 0 | |
| run.metrics_json = json.dumps(full_metrics) | |
| db.commit() | |
| db.refresh(run) | |
| # 8b. Fire-and-forget LLM judge (non-blocking) | |
| if os.environ.get("MINIMAX_API_KEY"): | |
| asyncio.create_task( | |
| _run_llm_judges(db, run.id, task, agent, full_metrics, diff_text, adapter_result.raw_output) | |
| ) | |
| except asyncio.TimeoutError: | |
| run.status = RunStatus.timeout | |
| run.duration_s = time.time() - start_time | |
| run.agent_duration_s = run.duration_s | |
| run.end_to_end_duration_s = run.duration_s | |
| run.judge_notes = f"Run timed out after {task.time_limit_s}s" | |
| run.timeout_flag = 1 | |
| db.commit() | |
| except Exception as e: | |
| run.status = RunStatus.error | |
| run.duration_s = time.time() - start_time | |
| run.agent_duration_s = run.duration_s | |
| run.end_to_end_duration_s = run.duration_s | |
| run.judge_notes = f"Error: {str(e)}" | |
| run.tool_failure_flag = 1 | |
| db.commit() | |
| finally: | |
| # 9. Destroy sandboxes (agent may already be gone, judge always cleaned up) | |
| if sandbox is not None: | |
| await asyncio.to_thread(sandbox.destroy) | |
| if judge_sandbox is not None: | |
| await asyncio.to_thread(judge_sandbox.destroy) | |
| self._active_runs.pop(run.id, None) | |
| db.refresh(run) | |
| return run | |
| async def _run_single_for_k( | |
| self, db: Session, task: Task, agent: Agent, group_id: str | |
| ) -> Run: | |
| """Run a single instance for pass@k, tagging with group_id.""" | |
| run = await self.run_single(db, task, agent) | |
| run.pass_at_k_group = group_id | |
| db.commit() | |
| db.refresh(run) | |
| return run | |
| async def run_pass_at_k( | |
| self, db: Session, task: Task, agent: Agent, k: int = 3 | |
| ) -> list[Run]: | |
| """Run k parallel benchmark runs for pass@k calculation.""" | |
| group_id = str(uuid.uuid4())[:8] | |
| task_id, agent_id = task.id, agent.id | |
| async def _single(i): | |
| run_db = SessionLocal() | |
| try: | |
| t = run_db.query(Task).filter(Task.id == task_id).first() | |
| a = run_db.query(Agent).filter(Agent.id == agent_id).first() | |
| run = await self.run_single(run_db, t, a) | |
| run.pass_at_k_group = group_id | |
| run_db.commit() | |
| run_db.refresh(run) | |
| return run | |
| finally: | |
| run_db.close() | |
| results = await asyncio.gather(*[_single(i) for i in range(k)], return_exceptions=True) | |
| # Log exceptions but don't crash the entire batch | |
| clean = [] | |
| for i, r in enumerate(results): | |
| if isinstance(r, Exception): | |
| logger.error("pass@k run %d/%d failed: %s", i + 1, k, r) | |
| else: | |
| clean.append(r) | |
| return clean | |
| def get_progress(self, run_id: int) -> dict | None: | |
| return self._active_runs.get(run_id) | |
| def _task_family(self, task: Task) -> str: | |
| slug = task.slug or "" | |
| if "_" in slug: | |
| return slug.split("_", 1)[0] | |
| if "-" in slug: | |
| return slug.split("-", 1)[0] | |
| return slug or "unknown" | |
| def _next_replicate_index(self, db: Session, task_id: int, agent_id: int) -> int: | |
| return ( | |
| db.query(Run) | |
| .filter(Run.task_id == task_id, Run.agent_id == agent_id) | |
| .count() | |
| + 1 | |
| ) | |
| async def _run_llm_judges( | |
| db: Session, run_id: int, task: Task, agent: Agent, | |
| full_metrics: dict, diff_text: str, raw_output: str, | |
| ) -> None: | |
| """Fire-and-forget LLM judges — updates metrics_json async.""" | |
| try: | |
| from core.llm_judge import llm_judge | |
| from core.alignment import llm_alignment_judge | |
| judge_metrics = full_metrics.get("judge", {}) | |
| run_data = { | |
| "task_title": task.title, | |
| "task_description": task.description or "", | |
| "agent_name": agent.name, | |
| "adapter_type": agent.adapter_type, | |
| "status": "success" if judge_metrics.get("functional_score", 0) >= 1.0 else "failure", | |
| "functional_score": judge_metrics.get("functional_score"), | |
| "functional_success": judge_metrics.get("functional_success"), | |
| "regression_score": judge_metrics.get("regression_score"), | |
| "lint_score": judge_metrics.get("lint_score"), | |
| "complexity_score": judge_metrics.get("complexity_score"), | |
| "composite_score": full_metrics.get("composite_score"), | |
| "trajectory": full_metrics.get("trajectory", {}), | |
| "diff_text": diff_text or "", | |
| "judge_notes": "", | |
| "tests_total": full_metrics.get("eval", {}).get("tests_total", 0), | |
| "tests_passed": full_metrics.get("eval", {}).get("tests_passed", 0), | |
| "raw_output": raw_output or "", | |
| } | |
| quality_result = await llm_judge(run_data) | |
| alignment_result = await llm_alignment_judge(run_data) | |
| if quality_result: | |
| full_metrics["llm_judge"] = quality_result | |
| if alignment_result: | |
| full_metrics["alignment"] = alignment_result | |
| if quality_result or alignment_result: | |
| from database import SessionLocal | |
| judge_db = SessionLocal() | |
| try: | |
| from models import Run | |
| r = judge_db.query(Run).filter(Run.id == run_id).first() | |
| if r: | |
| import json | |
| if alignment_result: | |
| r.alignment_score_llm = alignment_result.get("alignment_score") | |
| r.alignment_rubric_version = alignment_result.get("rubric_version") | |
| r.metrics_json = json.dumps(full_metrics) | |
| judge_db.commit() | |
| finally: | |
| judge_db.close() | |
| except Exception as e: | |
| import logging | |
| logging.getLogger(__name__).warning("LLM judge background task failed: %s", e) | |