4343from pathlib import Path
4444from typing import Any
4545
46- from agent_challenge .evaluation .own_runner .driver import AgentDriver
46+ from agent_challenge .evaluation .own_runner .driver import (
47+ AGENT_LOAD_FAILED_REASON_CODE ,
48+ AgentDriver ,
49+ )
4750from agent_challenge .evaluation .own_runner .reason_codes import (
4851 REASON_CODES ,
4952 is_known_reason_code ,
105108#: finalizes with its sibling trials intact.
106109TRIAL_CRASH_REASON_CODE = "harbor_trial_failed"
107110
111+ #: How many construction failures confirm a broken submission package.
112+ #:
113+ #: Agent construction (unpack + install + import + instantiate) is
114+ #: task-independent: a ZIP missing its manifest, or importing a module it never
115+ #: shipped, breaks identically on every task. Re-running all 30 tasks to
116+ #: relearn that one fact burns hours of wall clock and real LLM budget, so the
117+ #: job stops once this many trials have failed that way. The threshold is >1 so
118+ #: a single transient fault (a flaky package index during ``pip install``)
119+ #: cannot abort an otherwise healthy job.
120+ CONSTRUCTION_FAILURE_ABORT_THRESHOLD = 2
121+
108122# Fail fast at import if the taxonomy ever drops a code we emit.
109123assert TRIAL_TIMEOUT_REASON_CODE in REASON_CODES
110124assert TRIAL_CRASH_REASON_CODE in REASON_CODES
@@ -443,21 +457,36 @@ async def run(self, tasks: Sequence[TaskSpec]) -> JobResult:
443457 state_lock = asyncio .Lock ()
444458 in_flight = 0
445459 peak = 0
460+ construction_failures = 0
461+ short_circuit = False
446462
447463 async def execute (trial_id : TrialId ) -> TrialOutcome :
448- nonlocal in_flight , peak
464+ nonlocal in_flight , peak , construction_failures , short_circuit
449465 # Resume: a persisted result means this trial is already done -- load
450466 # it WITHOUT acquiring the semaphore (it never re-runs, never counts
451467 # toward in-flight, never double-counts).
452468 persisted = self ._load_trial (trial_id )
453469 if persisted is not None :
454470 return persisted
455471
472+ task = task_lookup [trial_id .task_name ]
473+
474+ # Fail-fast: once enough trials have proven the submission's agent
475+ # cannot be constructed, the remaining trials would burn budget to
476+ # reproduce the same packaging error. Resolve them immediately as
477+ # explicit, self-describing failures instead of running them.
478+ async with state_lock :
479+ aborted = short_circuit
480+ if aborted :
481+ outcome = self ._short_circuited_outcome (trial_id , task )
482+ self ._persist_trial (trial_id , outcome )
483+ await self ._notify_trial_listener (trial_id , outcome )
484+ return outcome
485+
456486 async with semaphore :
457487 async with state_lock :
458488 in_flight += 1
459489 peak = max (peak , in_flight )
460- task = task_lookup [trial_id .task_name ]
461490 try :
462491 # Backstop: bound the whole trial (prepare + drive + verify +
463492 # teardown) so one stalled sub-step can never wedge
@@ -481,6 +510,13 @@ async def execute(trial_id: TrialId) -> TrialOutcome:
481510 finally :
482511 async with state_lock :
483512 in_flight -= 1
513+ # Count construction failures so a broken package trips the
514+ # fail-fast gate above for every trial not yet started.
515+ if outcome .reason_code == AGENT_LOAD_FAILED_REASON_CODE :
516+ async with state_lock :
517+ construction_failures += 1
518+ if construction_failures >= CONSTRUCTION_FAILURE_ABORT_THRESHOLD :
519+ short_circuit = True
484520 # Persist immediately so a later crash cannot lose a finished
485521 # trial (and a resume skips it).
486522 self ._persist_trial (trial_id , outcome )
@@ -541,6 +577,34 @@ def _crashed_outcome(
541577 error_text = f"trial crashed: { type (exc ).__name__ } : { exc } " ,
542578 )
543579
580+ def _short_circuited_outcome (self , trial_id : TrialId , task : TaskSpec ) -> TrialOutcome :
581+ """Failed outcome for a trial never run because the package is broken.
582+
583+ Mirrors :meth:`_timed_out_outcome` (``status="failed"``, ``errored=True``,
584+ ``rewards=None``) so the job still aggregates every planned trial and the
585+ totals stay honest -- the task was planned, scored 0, and says exactly
586+ why it never executed.
587+ """
588+
589+ return TrialOutcome (
590+ task_name = trial_id .task_name ,
591+ trial_name = trial_id .trial_name ,
592+ status = "failed" ,
593+ rewards = None ,
594+ reason_code = AGENT_LOAD_FAILED_REASON_CODE ,
595+ errored = True ,
596+ agent_name = self ._config .agent_name ,
597+ model_name = self ._config .model_name ,
598+ source = task .source ,
599+ error_text = (
600+ "short-circuit: agent construction failed on "
601+ f"{ CONSTRUCTION_FAILURE_ABORT_THRESHOLD } earlier trials, so this "
602+ "trial was not run. Agent construction is task-independent -- fix "
603+ "the submission package (installable project + importable modules) "
604+ "and resubmit."
605+ ),
606+ )
607+
544608 # -- lock / persistence ------------------------------------------------
545609
546610 def _check_or_write_lock (self ) -> None :
0 commit comments