|
44 | 44 | # in one job that is a confound, not just noise. |
45 | 45 | AGENT_VERSION_PACKAGES = {"codex": "@openai/codex"} |
46 | 46 |
|
| 47 | +# Default retry policy for the generated Harbor job. Transient agent API errors |
| 48 | +# (e.g. UnknownApiError, "API Error: Failed to parse JSON") are one-off proxy |
| 49 | +# hiccups that a clean re-run of the trial almost always clears, so retrying |
| 50 | +# recovers otherwise-lost trials. We rely on Harbor's default |
| 51 | +# ``RetryConfig.exclude_exceptions`` (which already contains AgentTimeoutError, |
| 52 | +# VerifierTimeoutError, ApiUsageLimitError, ...) so genuine timeouts/limits stay |
| 53 | +# non-retried. A retry re-runs the whole trial from scratch, so it is capped low. |
| 54 | +DEFAULT_RETRY = {"max_retries": 2} |
| 55 | + |
| 56 | +# Default per-run model spend cap for claude-code (``--max-budget-usd``). The |
| 57 | +# agent timeout is now 3 hours (see ``kotlinai.harbor.AGENT_TIMEOUT_SEC``); this |
| 58 | +# bounds spend under that larger time budget. A retry re-runs the whole trial, |
| 59 | +# so this cap is per agent run, not per trial. Passed as a string, matching how |
| 60 | +# claude-code's CLI flag is resolved from kwargs. |
| 61 | +DEFAULT_AGENT_MAX_BUDGET_USD = "30" |
| 62 | + |
47 | 63 | # Two-sided 95% Student's t critical values indexed by degrees of freedom (entry |
48 | 64 | # 0 is unused). Beyond the table the value has converged on the normal quantile. |
49 | 65 | # fmt: off |
@@ -77,6 +93,21 @@ def pin_agent_version(agent: dict[str, Any], resolver: Callable[[str], str]) -> |
77 | 93 | kwargs["version"] = resolver(package) |
78 | 94 |
|
79 | 95 |
|
| 96 | +def apply_agent_cost_limit(agent: dict[str, Any]) -> None: |
| 97 | + """Cap claude-code's per-run model spend unless the caller set a limit. |
| 98 | +
|
| 99 | + Harbor passes ``kwargs.max_budget_usd`` to claude-code as ``--max-budget-usd``. |
| 100 | + Now that the agent timeout is 3 hours (see ``AGENT_TIMEOUT_SEC``), this keeps |
| 101 | + a single agent run's spend bounded. The flag is claude-code specific, so this |
| 102 | + is a no-op for other agents, and an explicit ``agent.kwargs.max_budget_usd`` |
| 103 | + in the config still wins. |
| 104 | + """ |
| 105 | + if agent["name"].strip().lower() != "claude-code": |
| 106 | + return |
| 107 | + kwargs = agent.setdefault("kwargs", {}) |
| 108 | + kwargs.setdefault("max_budget_usd", DEFAULT_AGENT_MAX_BUDGET_USD) |
| 109 | + |
| 110 | + |
80 | 111 | @dataclass(frozen=True) |
81 | 112 | class ComparisonSettings: |
82 | 113 | languages: tuple[str, str] |
@@ -162,6 +193,8 @@ def validate_comparison_config(config: dict[str, Any]) -> ComparisonSettings: |
162 | 193 | conflicting_fields = sorted(set(harbor) & RESERVED_HARBOR_FIELDS) |
163 | 194 | if conflicting_fields: |
164 | 195 | raise ComparisonError(f"harbor contains generated field(s): {conflicting_fields}") |
| 196 | + if "retry" in harbor and not isinstance(harbor["retry"], dict): |
| 197 | + raise ComparisonError("harbor.retry must be an object") |
165 | 198 | return ComparisonSettings( |
166 | 199 | languages=languages, |
167 | 200 | parallelism=parallelism, |
@@ -194,6 +227,9 @@ def build_comparison_job_config( |
194 | 227 | an IndexError partway through. Datasets keep that registry populated. |
195 | 228 | """ |
196 | 229 | config = copy.deepcopy(settings.harbor) |
| 230 | + # A default retry only when the user has not set one, so an explicit |
| 231 | + # ``harbor.retry`` in the config still fully overrides it. |
| 232 | + config.setdefault("retry", copy.deepcopy(DEFAULT_RETRY)) |
197 | 233 | config["job_name"] = job_name |
198 | 234 | config["jobs_dir"] = str(jobs_dir.resolve()) |
199 | 235 | config["n_concurrent_trials"] = settings.parallelism |
@@ -253,6 +289,8 @@ def run_comparison_experiment( |
253 | 289 | # CLI is the confound this run exists to avoid. Pin explicitly in the config |
254 | 290 | # to run without registry access. |
255 | 291 | pin_agent_version(settings.agent, version_resolver) |
| 292 | + # Bound claude-code's per-run spend now that the agent timeout is 3 hours. |
| 293 | + apply_agent_cost_limit(settings.agent) |
256 | 294 | output_dir.mkdir(parents=True) |
257 | 295 | jobs_dir = output_dir / "jobs" |
258 | 296 | repo_root = runner.parent.parent |
|
0 commit comments