agentbox-rl 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. agentbox/__init__.py +52 -0
  2. agentbox/agent/__init__.py +6 -0
  3. agentbox/agent/agent.py +59 -0
  4. agentbox/agent/loop.py +201 -0
  5. agentbox/agent/prompts.py +44 -0
  6. agentbox/benchmark/__init__.py +29 -0
  7. agentbox/benchmark/compare.py +51 -0
  8. agentbox/benchmark/hash.py +59 -0
  9. agentbox/benchmark/loader.py +152 -0
  10. agentbox/benchmark/report.py +175 -0
  11. agentbox/benchmark/runner.py +131 -0
  12. agentbox/benchmark/schema.py +198 -0
  13. agentbox/benchmark/setup_check.py +57 -0
  14. agentbox/cli/__init__.py +1 -0
  15. agentbox/cli/bench.py +571 -0
  16. agentbox/cli/generate.py +314 -0
  17. agentbox/cli/main.py +276 -0
  18. agentbox/cli/traj.py +82 -0
  19. agentbox/config.py +84 -0
  20. agentbox/config_load.py +107 -0
  21. agentbox/errors.py +79 -0
  22. agentbox/jobs/__init__.py +15 -0
  23. agentbox/jobs/artifacts.py +65 -0
  24. agentbox/jobs/cache.py +39 -0
  25. agentbox/jobs/types.py +67 -0
  26. agentbox/metrics/__init__.py +5 -0
  27. agentbox/metrics/aggregate.py +63 -0
  28. agentbox/model/__init__.py +17 -0
  29. agentbox/model/base.py +32 -0
  30. agentbox/model/errors.py +5 -0
  31. agentbox/model/mock.py +43 -0
  32. agentbox/model/openai_compat.py +153 -0
  33. agentbox/model/probe.py +186 -0
  34. agentbox/py.typed +0 -0
  35. agentbox/runner/__init__.py +6 -0
  36. agentbox/runner/parallel.py +194 -0
  37. agentbox/runner/rollout.py +251 -0
  38. agentbox/sandbox/__init__.py +15 -0
  39. agentbox/sandbox/docker_backend.py +158 -0
  40. agentbox/sandbox/images.py +29 -0
  41. agentbox/sandbox/manager.py +265 -0
  42. agentbox/sandbox/paths.py +65 -0
  43. agentbox/sandbox/prune.py +42 -0
  44. agentbox/sandbox/snapshot.py +43 -0
  45. agentbox/sandbox/types.py +67 -0
  46. agentbox/tasks/__init__.py +18 -0
  47. agentbox/tasks/bulk.py +67 -0
  48. agentbox/tasks/filter.py +100 -0
  49. agentbox/tasks/generate/__init__.py +42 -0
  50. agentbox/tasks/generate/batch.py +308 -0
  51. agentbox/tasks/generate/constraints.py +41 -0
  52. agentbox/tasks/generate/dedup.py +109 -0
  53. agentbox/tasks/generate/generator.py +665 -0
  54. agentbox/tasks/generate/llm_validate.py +325 -0
  55. agentbox/tasks/generate/lm.py +79 -0
  56. agentbox/tasks/generate/prompts.py +7 -0
  57. agentbox/tasks/generate/review.py +111 -0
  58. agentbox/tasks/generate/signatures.py +178 -0
  59. agentbox/tasks/generate/static_qc.py +236 -0
  60. agentbox/tasks/generate/strip_impl.py +71 -0
  61. agentbox/tasks/generate/validate.py +212 -0
  62. agentbox/tasks/loader.py +15 -0
  63. agentbox/tasks/rewards.py +47 -0
  64. agentbox/tasks/schema.py +79 -0
  65. agentbox/tasks/seeder.py +78 -0
  66. agentbox/tasks/verifier.py +65 -0
  67. agentbox/tools/__init__.py +16 -0
  68. agentbox/tools/base.py +54 -0
  69. agentbox/tools/builtins.py +31 -0
  70. agentbox/tools/decorator.py +89 -0
  71. agentbox/tools/executor.py +87 -0
  72. agentbox/tools/file_tools.py +162 -0
  73. agentbox/tools/registry.py +99 -0
  74. agentbox/tools/shell.py +63 -0
  75. agentbox/tools/tests_tool.py +35 -0
  76. agentbox/trajectory/__init__.py +12 -0
  77. agentbox/trajectory/formats/__init__.py +6 -0
  78. agentbox/trajectory/formats/art.py +68 -0
  79. agentbox/trajectory/formats/jsonl.py +17 -0
  80. agentbox/trajectory/recorder.py +76 -0
  81. agentbox/trajectory/render.py +334 -0
  82. agentbox/trajectory/schema.py +124 -0
  83. agentbox/types.py +42 -0
  84. agentbox/version.py +3 -0
  85. agentbox_rl-0.2.0.dist-info/METADATA +427 -0
  86. agentbox_rl-0.2.0.dist-info/RECORD +89 -0
  87. agentbox_rl-0.2.0.dist-info/WHEEL +4 -0
  88. agentbox_rl-0.2.0.dist-info/entry_points.txt +2 -0
  89. agentbox_rl-0.2.0.dist-info/licenses/LICENSE +176 -0
@@ -0,0 +1,175 @@
1
+ """Build and render benchmark reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import defaultdict
6
+ from collections.abc import Sequence
7
+ from typing import Any
8
+
9
+ from agentbox.benchmark.schema import (
10
+ BenchmarkReport,
11
+ ModelReport,
12
+ TaskResult,
13
+ )
14
+ from agentbox.metrics.aggregate import AggregateMetrics, aggregate_trajectories
15
+ from agentbox.trajectory.schema import Trajectory
16
+ from agentbox.types import FinalStatus
17
+
18
+
19
+ def redact_model_config(cfg: dict[str, Any]) -> dict[str, Any]:
20
+ out = dict(cfg)
21
+ if "api_key" in out:
22
+ out["api_key"] = "***" if out["api_key"] else None
23
+ return out
24
+
25
+
26
+ def build_task_results(
27
+ model_id: str,
28
+ trajs: Sequence[Trajectory],
29
+ *,
30
+ trajectory_paths: dict[str, list[str]] | None = None,
31
+ ) -> list[TaskResult]:
32
+ by_task: dict[str, list[Trajectory]] = defaultdict(list)
33
+ for t in trajs:
34
+ by_task[t.task_id].append(t)
35
+
36
+ results: list[TaskResult] = []
37
+ for task_id, group in sorted(by_task.items()):
38
+ n = len(group)
39
+ successes = sum(1 for x in group if x.final_status == FinalStatus.SUCCESS)
40
+ statuses: dict[str, int] = defaultdict(int)
41
+ seed_errors = 0
42
+ setup_fails = 0
43
+ for x in group:
44
+ statuses[x.final_status.value] += 1
45
+ err = (x.error or "").lower()
46
+ if x.final_status == FinalStatus.ERROR:
47
+ if "setup check" in err:
48
+ setup_fails += 1
49
+ elif "seed" in err:
50
+ seed_errors += 1
51
+ pass_at_k = 1.0 if successes > 0 else 0.0
52
+ results.append(
53
+ TaskResult(
54
+ task_id=task_id,
55
+ model_id=model_id,
56
+ trajectory_paths=(trajectory_paths or {}).get(task_id, []),
57
+ n=n,
58
+ successes=successes,
59
+ pass_at_1=successes / n if n else 0.0,
60
+ pass_at_k=pass_at_k,
61
+ mean_reward=sum(x.reward for x in group) / n if n else 0.0,
62
+ mean_steps=sum(x.metrics.steps for x in group) / n if n else 0.0,
63
+ mean_duration_s=sum(x.metrics.duration_s for x in group) / n if n else 0.0,
64
+ seed_errors=seed_errors,
65
+ setup_check_failures=setup_fails,
66
+ statuses=dict(statuses),
67
+ )
68
+ )
69
+ return results
70
+
71
+
72
+ def build_model_report(
73
+ model_id: str,
74
+ model_config_public: dict[str, Any],
75
+ trajs: Sequence[Trajectory],
76
+ *,
77
+ wall_clock_s: float,
78
+ trajectory_paths: dict[str, list[str]] | None = None,
79
+ ) -> ModelReport:
80
+ by_task = build_task_results(
81
+ model_id, trajs, trajectory_paths=trajectory_paths
82
+ )
83
+ agg = aggregate_trajectories(trajs)
84
+ pass_at_k_mean = (
85
+ sum(t.pass_at_k or 0.0 for t in by_task) / len(by_task) if by_task else None
86
+ )
87
+ return ModelReport(
88
+ model_id=model_id,
89
+ model_config_public=redact_model_config(model_config_public),
90
+ aggregate=agg,
91
+ by_task=by_task,
92
+ wall_clock_s=wall_clock_s,
93
+ pass_at_k_mean=pass_at_k_mean,
94
+ )
95
+
96
+
97
+ def build_leaderboard(models: Sequence[ModelReport]) -> list[dict[str, Any]]:
98
+ rows = []
99
+ for m in models:
100
+ rows.append(
101
+ {
102
+ "model_id": m.model_id,
103
+ "success_rate": m.aggregate.success_rate,
104
+ "mean_reward": m.aggregate.mean_reward,
105
+ "mean_steps": m.aggregate.mean_steps,
106
+ "mean_duration_s": m.aggregate.mean_duration_s,
107
+ "pass_at_k_mean": m.pass_at_k_mean,
108
+ "n": m.aggregate.n,
109
+ "wall_clock_s": m.wall_clock_s,
110
+ }
111
+ )
112
+ rows.sort(key=lambda r: (-r["success_rate"], -r["mean_reward"], r["mean_steps"]))
113
+ return rows
114
+
115
+
116
+ def render_markdown(report: BenchmarkReport) -> str:
117
+ lines: list[str] = [
118
+ f"# Benchmark Report: {report.suite_id}",
119
+ "",
120
+ f"- **Suite version:** {report.suite_version}",
121
+ f"- **Content hash:** `{report.suite_content_hash or 'none'}`",
122
+ f"- **AgentBox:** {report.agentbox_version}",
123
+ f"- **Report id:** {report.report_id}",
124
+ f"- **Started:** {report.started_at.isoformat()}",
125
+ f"- **Finished:** {report.finished_at.isoformat()}",
126
+ "",
127
+ "## Leaderboard",
128
+ "",
129
+ "| Rank | Model | Success rate | Mean reward | Mean steps | Mean duration (s) | Pass@k | n |",
130
+ "| --- | --- | --- | --- | --- | --- | --- | --- |",
131
+ ]
132
+ for i, row in enumerate(report.leaderboard, start=1):
133
+ pak = row.get("pass_at_k_mean")
134
+ pak_s = f"{pak:.3f}" if pak is not None else "—"
135
+ lines.append(
136
+ f"| {i} | {row['model_id']} | {row['success_rate']:.3f} | "
137
+ f"{row['mean_reward']:.3f} | {row['mean_steps']:.2f} | "
138
+ f"{row['mean_duration_s']:.2f} | {pak_s} | {row['n']} |"
139
+ )
140
+
141
+ for m in report.models:
142
+ lines.extend(
143
+ [
144
+ "",
145
+ f"## Model: {m.model_id}",
146
+ "",
147
+ f"- Config: `{m.model_config_public}`",
148
+ f"- Success rate: **{m.aggregate.success_rate:.3f}**",
149
+ f"- Mean reward: {m.aggregate.mean_reward:.3f}",
150
+ f"- Mean steps: {m.aggregate.mean_steps:.2f}",
151
+ f"- Statuses: {m.aggregate.by_status}",
152
+ f"- Wall clock: {m.wall_clock_s:.1f}s",
153
+ "",
154
+ "| Task | n | Successes | Pass@1 | Pass@k | Mean reward | Mean steps |",
155
+ "| --- | --- | --- | --- | --- | --- | --- |",
156
+ ]
157
+ )
158
+ for t in m.by_task:
159
+ pak = f"{t.pass_at_k:.3f}" if t.pass_at_k is not None else "—"
160
+ lines.append(
161
+ f"| {t.task_id} | {t.n} | {t.successes} | {t.pass_at_1:.3f} | "
162
+ f"{pak} | {t.mean_reward:.3f} | {t.mean_steps:.2f} |"
163
+ )
164
+
165
+ lines.extend(
166
+ [
167
+ "",
168
+ "## Notes",
169
+ "",
170
+ f"- Pass@k definition: {report.pass_at_k_definition}",
171
+ "- API keys are redacted from this report.",
172
+ "",
173
+ ]
174
+ )
175
+ return "\n".join(lines)
@@ -0,0 +1,131 @@
1
+ """Execute frozen benchmark suites against model endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import time
7
+ import uuid
8
+ from collections import defaultdict
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from agentbox.benchmark.loader import snapshot_suite
14
+ from agentbox.benchmark.report import build_leaderboard, build_model_report
15
+ from agentbox.benchmark.schema import (
16
+ BenchmarkReport,
17
+ BenchmarkRunConfig,
18
+ ModelUnderTest,
19
+ )
20
+ from agentbox.config import AgentConfig, ModelConfig, RolloutConfig
21
+ from agentbox.runner.parallel import ParallelRunner
22
+ from agentbox.trajectory.schema import Trajectory
23
+ from agentbox.version import __version__
24
+
25
+ logger = logging.getLogger("agentbox.benchmark")
26
+
27
+
28
+ class BenchmarkRunner:
29
+ """Run a BenchmarkSuite against one or more OpenAI-compatible models."""
30
+
31
+ def __init__(self, config: BenchmarkRunConfig) -> None:
32
+ self.config = config
33
+
34
+ async def run(self) -> BenchmarkReport:
35
+ cfg = self.config
36
+ suite = cfg.suite
37
+ suite.verify_integrity(strict=cfg.strict_integrity)
38
+
39
+ out = Path(cfg.output_dir)
40
+ out.mkdir(parents=True, exist_ok=True)
41
+ snapshot_suite(suite, out / "suite_snapshot")
42
+
43
+ started = datetime.now(timezone.utc)
44
+ model_reports = []
45
+ for mut in cfg.models:
46
+ logger.info("benchmark model_id=%s start", mut.model_id)
47
+ mr = await self._run_model(mut)
48
+ model_reports.append(mr)
49
+
50
+ finished = datetime.now(timezone.utc)
51
+ report = BenchmarkReport(
52
+ report_id=str(uuid.uuid4()),
53
+ suite_id=suite.manifest.suite_id,
54
+ suite_version=suite.manifest.version,
55
+ suite_content_hash=suite.manifest.content_hash,
56
+ agentbox_version=__version__,
57
+ started_at=started,
58
+ finished_at=finished,
59
+ run_tags=dict(cfg.tags),
60
+ models=model_reports,
61
+ leaderboard=build_leaderboard(model_reports),
62
+ )
63
+ report.save_json(out / "report.json")
64
+ report.save_markdown(out / "REPORT.md")
65
+ return report
66
+
67
+ async def _run_model(self, mut: ModelUnderTest) -> Any:
68
+ cfg = self.config
69
+ suite = cfg.suite
70
+ mdir = Path(cfg.output_dir) / "models" / mut.model_id
71
+ traj_dir = mdir / "trajectories"
72
+ traj_dir.mkdir(parents=True, exist_ok=True)
73
+
74
+ # Fairness: suite agent tools/max_steps only; model cannot change tools
75
+ agent_cfg = suite.manifest.agent.model_copy(
76
+ deep=True,
77
+ update={"custom_tools": [], "drop_tools_prob": 0.0},
78
+ )
79
+ concurrency = cfg.concurrency or suite.manifest.concurrency
80
+ n_per = cfg.n_per_task or suite.manifest.n_per_task
81
+ seed = suite.manifest.seed
82
+
83
+ rollout_config = RolloutConfig(
84
+ sandbox=suite.manifest.sandbox,
85
+ model=mut.model,
86
+ agent=agent_cfg,
87
+ seed=seed,
88
+ )
89
+
90
+ path_map: dict[str, list[str]] = defaultdict(list)
91
+ trajs_acc: list[Trajectory] = []
92
+
93
+ def on_trajectory(traj: Trajectory) -> None:
94
+ trajs_acc.append(traj)
95
+ if cfg.save_trajectories:
96
+ fname = f"{traj.task_id}_{traj.run_id[:8]}.json"
97
+ path = traj_dir / fname
98
+ traj.save(path)
99
+ path_map[traj.task_id].append(str(path))
100
+
101
+ # Thread setup_checks into each Rollout via ParallelRunner extension
102
+ runner = ParallelRunner(
103
+ concurrency=concurrency,
104
+ config=rollout_config,
105
+ model=mut.model,
106
+ agent=agent_cfg,
107
+ sandbox=suite.manifest.sandbox,
108
+ )
109
+ setup_checks = list(suite.manifest.scoring.setup_checks)
110
+
111
+ t0 = time.monotonic()
112
+ trajs = await runner.run_tasks(
113
+ suite.tasks,
114
+ n_per_task=n_per,
115
+ progress=cfg.progress,
116
+ on_trajectory=on_trajectory,
117
+ setup_checks=setup_checks,
118
+ )
119
+ wall = time.monotonic() - t0
120
+
121
+ public = mut.model.model_dump(mode="json")
122
+ report = build_model_report(
123
+ mut.model_id,
124
+ public,
125
+ trajs,
126
+ wall_clock_s=wall,
127
+ trajectory_paths=dict(path_map),
128
+ )
129
+ report_path = mdir / "model_report.json"
130
+ report_path.write_text(report.model_dump_json(indent=2), encoding="utf-8")
131
+ return report
@@ -0,0 +1,198 @@
1
+ """Benchmark suite, run, and report schemas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any, Literal
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+ from agentbox.config import AgentConfig, ModelConfig, SandboxConfig
12
+ from agentbox.metrics.aggregate import AggregateMetrics
13
+ from agentbox.tasks.schema import Task
14
+ from agentbox.trajectory.schema import Trajectory
15
+ from agentbox.types import ToolMode
16
+
17
+
18
+ class SetupCheckSpec(BaseModel):
19
+ """Post-seed environment health check (not task success)."""
20
+
21
+ name: str
22
+ command: str
23
+ success_exit_code: int = 0
24
+ timeout_s: float = 60.0
25
+ required: bool = True
26
+
27
+
28
+ class ScoringConfig(BaseModel):
29
+ """How a suite scores rollouts."""
30
+
31
+ primary: Literal["verifier"] = "verifier"
32
+ require_seed_ok: bool = True
33
+ setup_checks: list[SetupCheckSpec] = Field(default_factory=list)
34
+ track_steps: bool = True
35
+ track_duration: bool = True
36
+ enable_ruler: bool = False
37
+ ruler_group_size: int = 4
38
+
39
+
40
+ def _default_bench_agent() -> AgentConfig:
41
+ return AgentConfig(
42
+ tools=ToolMode.STRUCTURED,
43
+ max_steps=40,
44
+ drop_tools_prob=0.0,
45
+ custom_tools=[],
46
+ )
47
+
48
+
49
+ class BenchmarkSuiteManifest(BaseModel):
50
+ """suite.json manifest for a frozen evaluation pack."""
51
+
52
+ schema_version: Literal["1"] = "1"
53
+ suite_id: str
54
+ name: str
55
+ description: str = ""
56
+ version: str = "1.0.0"
57
+ tasks_path: str = "tasks"
58
+ sandbox: SandboxConfig = Field(default_factory=SandboxConfig)
59
+ agent: AgentConfig = Field(default_factory=_default_bench_agent)
60
+ n_per_task: int = 1
61
+ concurrency: int = 8
62
+ seed: int | None = 42
63
+ scoring: ScoringConfig = Field(default_factory=ScoringConfig)
64
+ content_hash: str | None = None
65
+ created_at: datetime | None = None
66
+ metadata: dict[str, Any] = Field(default_factory=dict)
67
+
68
+ model_config = ConfigDict(extra="ignore")
69
+
70
+
71
+ class BenchmarkSuite(BaseModel):
72
+ """In-memory suite: manifest + loaded tasks."""
73
+
74
+ root: Path | None = None
75
+ manifest: BenchmarkSuiteManifest
76
+ tasks: list[Task] = Field(default_factory=list)
77
+
78
+ model_config = ConfigDict(arbitrary_types_allowed=True)
79
+
80
+ def task_ids(self) -> list[str]:
81
+ return [t.task_id for t in self.tasks]
82
+
83
+ def freeze(self) -> BenchmarkSuite:
84
+ from agentbox.benchmark.hash import compute_suite_content_hash
85
+
86
+ m = self.manifest.model_copy(deep=True)
87
+ m.created_at = m.created_at or datetime.now(timezone.utc)
88
+ m.content_hash = compute_suite_content_hash(self.tasks, m)
89
+ return BenchmarkSuite(root=self.root, manifest=m, tasks=list(self.tasks))
90
+
91
+ def verify_integrity(self, *, strict: bool = False) -> bool:
92
+ from agentbox.benchmark.hash import compute_suite_content_hash
93
+ from agentbox.errors import SuiteIntegrityError
94
+
95
+ if not self.manifest.content_hash:
96
+ if strict:
97
+ raise SuiteIntegrityError("Suite has no content_hash; run freeze()")
98
+ return True
99
+ current = compute_suite_content_hash(self.tasks, self.manifest)
100
+ ok = current == self.manifest.content_hash
101
+ if not ok and strict:
102
+ raise SuiteIntegrityError(
103
+ f"Suite integrity check failed: expected {self.manifest.content_hash}, got {current}"
104
+ )
105
+ return ok
106
+
107
+
108
+ class ModelUnderTest(BaseModel):
109
+ """One model endpoint under evaluation."""
110
+
111
+ model_id: str
112
+ model: ModelConfig
113
+
114
+
115
+ class BenchmarkRunConfig(BaseModel):
116
+ """Configuration for executing a suite against one or more models."""
117
+
118
+ suite: BenchmarkSuite
119
+ models: list[ModelUnderTest]
120
+ concurrency: int | None = None
121
+ n_per_task: int | None = None
122
+ save_trajectories: bool = True
123
+ output_dir: Path
124
+ progress: bool = True
125
+ fail_fast: bool = False
126
+ strict_integrity: bool = False
127
+ tags: dict[str, str] = Field(default_factory=dict)
128
+
129
+ model_config = ConfigDict(arbitrary_types_allowed=True)
130
+
131
+
132
+ class TaskResult(BaseModel):
133
+ task_id: str
134
+ model_id: str
135
+ trajectory_paths: list[str] = Field(default_factory=list)
136
+ n: int = 0
137
+ successes: int = 0
138
+ pass_at_1: float = 0.0
139
+ pass_at_k: float | None = None
140
+ mean_reward: float = 0.0
141
+ mean_steps: float = 0.0
142
+ mean_duration_s: float = 0.0
143
+ seed_errors: int = 0
144
+ setup_check_failures: int = 0
145
+ statuses: dict[str, int] = Field(default_factory=dict)
146
+
147
+
148
+ class ModelReport(BaseModel):
149
+ model_id: str
150
+ model_config_public: dict[str, Any]
151
+ aggregate: AggregateMetrics
152
+ by_task: list[TaskResult] = Field(default_factory=list)
153
+ wall_clock_s: float = 0.0
154
+ pass_at_k_mean: float | None = None
155
+
156
+
157
+ class BenchmarkReport(BaseModel):
158
+ schema_version: Literal["1"] = "1"
159
+ report_id: str
160
+ suite_id: str
161
+ suite_version: str
162
+ suite_content_hash: str | None = None
163
+ agentbox_version: str
164
+ started_at: datetime
165
+ finished_at: datetime
166
+ run_tags: dict[str, str] = Field(default_factory=dict)
167
+ models: list[ModelReport] = Field(default_factory=list)
168
+ leaderboard: list[dict[str, Any]] = Field(default_factory=list)
169
+ pass_at_k_definition: str = (
170
+ "pass_at_k = fraction of tasks with >=1 success among n_per_task rollouts; "
171
+ "pass_at_1 = mean per-task success rate of first/all rollouts"
172
+ )
173
+
174
+ def save_json(self, path: str | Path) -> None:
175
+ path = Path(path)
176
+ path.parent.mkdir(parents=True, exist_ok=True)
177
+ path.write_text(self.model_dump_json(indent=2), encoding="utf-8")
178
+
179
+ def save_markdown(self, path: str | Path) -> None:
180
+ from agentbox.benchmark.report import render_markdown
181
+
182
+ path = Path(path)
183
+ path.parent.mkdir(parents=True, exist_ok=True)
184
+ path.write_text(render_markdown(self), encoding="utf-8")
185
+
186
+ @classmethod
187
+ def load(cls, path: str | Path) -> BenchmarkReport:
188
+ import json
189
+
190
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
191
+ return cls.model_validate(data)
192
+
193
+
194
+ class ComparisonResult(BaseModel):
195
+ suite_id: str
196
+ hash_match: bool
197
+ warnings: list[str] = Field(default_factory=list)
198
+ deltas: list[dict[str, Any]] = Field(default_factory=list)
@@ -0,0 +1,57 @@
1
+ """Post-seed environment health checks (not task success scoring)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ from agentbox.benchmark.schema import SetupCheckSpec
10
+ from agentbox.sandbox.types import ExecResult
11
+
12
+ if TYPE_CHECKING:
13
+ from agentbox.sandbox.manager import SandboxManager
14
+ from agentbox.sandbox.types import Sandbox
15
+
16
+
17
+ class SetupCheckResult(BaseModel):
18
+ model_config = ConfigDict(arbitrary_types_allowed=True)
19
+
20
+ ok: bool
21
+ results: list[ExecResult] = Field(default_factory=list)
22
+ failed_check: str | None = None
23
+ error: str | None = None
24
+
25
+
26
+ class SetupChecker:
27
+ """Run suite-level commands after seed, before the agent starts."""
28
+
29
+ def __init__(self, manager: SandboxManager) -> None:
30
+ self.manager = manager
31
+
32
+ async def run(
33
+ self,
34
+ sandbox: Sandbox,
35
+ checks: list[SetupCheckSpec],
36
+ ) -> SetupCheckResult:
37
+ if not checks:
38
+ return SetupCheckResult(ok=True)
39
+
40
+ results: list[ExecResult] = []
41
+ for check in checks:
42
+ result = await self.manager.exec(
43
+ sandbox,
44
+ check.command,
45
+ timeout_s=check.timeout_s,
46
+ )
47
+ results.append(result)
48
+ failed = result.timed_out or result.exit_code != check.success_exit_code
49
+ if failed and check.required:
50
+ err = result.stderr or result.stdout or "setup check failed"
51
+ return SetupCheckResult(
52
+ ok=False,
53
+ results=results,
54
+ failed_check=check.name,
55
+ error=f"setup check {check.name!r} failed: {err}",
56
+ )
57
+ return SetupCheckResult(ok=True, results=results)
@@ -0,0 +1 @@
1
+ """Command-line interface."""