toolbench 0.2.1__tar.gz → 0.3.0__tar.gz
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.
- toolbench-0.3.0/CHANGELOG.md +42 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/PKG-INFO +1 -1
- {toolbench-0.2.1 → toolbench-0.3.0}/pyproject.toml +1 -1
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_codex_runtime.py +7 -1
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_metrics.py +12 -0
- toolbench-0.3.0/tests/test_runner_toolbase_protection.py +29 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/__init__.py +1 -1
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/cli.py +46 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/metrics.py +9 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/runner.py +42 -2
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/runtime.py +134 -2
- {toolbench-0.2.1 → toolbench-0.3.0}/.gitignore +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/LICENSE +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/README.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/calculator/requirements.txt +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/calculator/toolkit.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/calculator/tools/__init__.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/calculator/tools/basic.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/calculator/tools/scientific.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/benchmark.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/ground_truth/answer.json +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/harnesses/orchestral/anthropic.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/harnesses/orchestral/groq.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/harnesses/orchestral/litellm.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/harnesses/orchestral/openai.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/loadouts/additive_only.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/loadouts/all_metrics.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/loadouts/calc_toolbase.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/loadouts/core_only.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/loadouts/full_local.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/loadouts/full_mixed.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/loadouts/full_toolbase.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/loadouts/guided.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/loadouts/primitives_only.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/skills/distance_recipe.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/tools/chebyshev.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/tools/dunderkit.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/tools/euclid.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/tools/extras.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/tools/taxicab.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/variants/derived/prompts/system.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/variants/derived/prompts/user.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/variants/derived/variant.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/variants/direct/prompts/system.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/variants/direct/prompts/user.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/variants/direct/sandbox/template/points.json +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/variants/direct/variant.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/variants/polar/prompts/system.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/variants/polar/prompts/user.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/variants/polar/variant.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/harness_templates/README.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/harness_templates/claude-code/README.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/harness_templates/claude-code/default.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/harness_templates/codex/README.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/harness_templates/codex/default.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/harness_templates/orchestral/README.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/harness_templates/orchestral/anthropic.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/harness_templates/orchestral/google.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/harness_templates/orchestral/groq.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/harness_templates/orchestral/litellm.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/harness_templates/orchestral/openai.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/mkdocs.yml +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/__init__.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/helpers.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_artifact_cleanup.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_artifact_policy.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_benchmark_extends.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_checks.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_cli_runtime_stderr_drain.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_content_checks.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_cost_extraction.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_crash_classifier.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_geometry.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_integrity.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_json_repair.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_judge_score.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_judge_select.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_mcp_source.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_numeric_checks.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_paired_deltas.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_resume.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_rubric_loader.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_runner_llm_judge.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_runner_rate_limit.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_runner_request_params.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_runner_retry.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_runner_transient.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_runtime_registry.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_runtime_version_label.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_schema.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_skills.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_summary_text_cost.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_tool_error_visibility.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_tool_resolver.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_toolbase_sandbox_scoping.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_toolbase_select_provenance.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_trial_loop.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/tests/test_variant.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/__init__.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/artifact_policy.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/benchmark.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/budget.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/checks.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/content_checks.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/crash_classifier.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/failure_modes.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/harness.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/integrity.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/json_repair.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/judge.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/judge_select.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/litellm_pricing.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/llm_factory.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/loadout.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/numeric_checks.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/skills.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/store.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/task.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/tool_policy.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/tool_resolver.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/trajectory.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/core/variant.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/reporting/__init__.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/reporting/_output.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/reporting/_shared.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/reporting/audit_log.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/reporting/k_sweep.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/reporting/parallel_coords.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/reporting/per_stage_k.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/reporting/plot_overview.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/reporting/summary_text.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/toolbench/reporting/transcript.py +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/authoring/custom-tools.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/authoring/define-a-benchmark.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/authoring/overview.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/authoring/rubrics-and-checks.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/explanation.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/guides/harnesses.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/guides/install-and-quickstart.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/guides/loadouts.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/guides/reading-results.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/guides/running-a-benchmark.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/guides/toolbase.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/guides/variants.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/index.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/reference/commands.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/reference/files-and-layout.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/reference/metrics.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/reference/schemas.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/reference/vocabulary.md +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/stylesheets/extra.css +0 -0
- {toolbench-0.2.1 → toolbench-0.3.0}/userdocs/troubleshooting.md +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `toolbench` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
|
6
|
+
project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [0.3.0] — 2026-07-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **MCP preflight verification.** Before any trial runs (and in `--dry-run`), a
|
|
15
|
+
run now verifies that every MCP-serving harness × tools-loadout can actually
|
|
16
|
+
reach its tools: it starts the loadout's `toolbase serve --profile …` exactly
|
|
17
|
+
as the CLI runtime would and completes an MCP `initialize` + `tools/list`
|
|
18
|
+
handshake. If the profile serves zero tools, the handshake times out, or the
|
|
19
|
+
resolved tools are missing, the run **aborts with exit 2 before executing a
|
|
20
|
+
single trial** and prints which cell failed. Previously a `tools` loadout that
|
|
21
|
+
could not reach its tools ran the entire arm silently tool-less and still
|
|
22
|
+
graded as a valid tools result. Verification is gated to the runtimes that
|
|
23
|
+
serve toolbase over MCP (`claude_code`, `codex`) via
|
|
24
|
+
`runtime_serves_toolbase_mcp`; in-process runtimes (orchestral) resolve tools
|
|
25
|
+
directly and are unaffected. New helpers: `verify_toolbase_mcp`,
|
|
26
|
+
`runtime_serves_toolbase_mcp`.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **`_toolbase_command()` now fails loudly.** When the `toolbase` executable
|
|
31
|
+
cannot be located (neither on `PATH` nor beside the running Python), it raises
|
|
32
|
+
with an actionable message instead of returning a bare `"toolbase"`. The bare
|
|
33
|
+
fallback silently produced a broken `.mcp.json` whose stdio server failed to
|
|
34
|
+
launch (`command not found` on the child's `PATH`), leaving the agent with
|
|
35
|
+
"No such tool available" while the trial still graded as if tools were
|
|
36
|
+
present — a failure mode that surfaced when an env's `toolbase` install was
|
|
37
|
+
in flux. Combined with the preflight above, an unresolvable toolbase is now
|
|
38
|
+
caught before the benchmark begins.
|
|
39
|
+
|
|
40
|
+
## [0.2.1] — 2026-07-24
|
|
41
|
+
|
|
42
|
+
- Campaign runtime & metrics hardening.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: toolbench
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A platform and CLI for building benchmarks for agentic tools and harnesses.
|
|
5
5
|
Project-URL: Homepage, https://github.com/tonymenzo/toolbench
|
|
6
6
|
Project-URL: Repository, https://github.com/tonymenzo/toolbench
|
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
from toolbench.core.harness import Harness
|
|
6
6
|
from toolbench.core.llm_factory import SubscriptionLLM
|
|
7
|
-
from toolbench.core.runtime import CodexAgent, _codex_factory
|
|
7
|
+
from toolbench.core.runtime import CodexAgent, _codex_factory, _toolbase_command
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def _agent(tmp_path: Path, **kw) -> CodexAgent:
|
|
@@ -29,6 +29,12 @@ def test_toolbase_mcp_is_auto_approved_for_noninteractive_exec(tmp_path):
|
|
|
29
29
|
assert any("default_tools_approval_mode=\"approve\"" in x for x in args)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def test_toolbase_mcp_command_resolves_to_an_executable():
|
|
33
|
+
command = Path(_toolbase_command())
|
|
34
|
+
assert command.is_absolute()
|
|
35
|
+
assert command.is_file()
|
|
36
|
+
|
|
37
|
+
|
|
32
38
|
def test_resume_uses_only_resume_supported_flags(tmp_path):
|
|
33
39
|
a = _agent(tmp_path)
|
|
34
40
|
a.thread_id = "thread-123"
|
|
@@ -242,6 +242,18 @@ class TestCostUsd(unittest.TestCase):
|
|
|
242
242
|
self.assertAlmostEqual(estimate["usd"], 56.0)
|
|
243
243
|
self.assertTrue(estimate["long_context_pricing_applied"])
|
|
244
244
|
|
|
245
|
+
def test_gpt56_sol_subscription_api_equivalent(self):
|
|
246
|
+
estimate = subscription_api_equivalent_cost(
|
|
247
|
+
"gpt-5.6-sol",
|
|
248
|
+
input_tokens=1_000_000,
|
|
249
|
+
cache_read_tokens=1_000_000,
|
|
250
|
+
output_tokens=1_000_000,
|
|
251
|
+
initial_input_tokens=272_001,
|
|
252
|
+
)
|
|
253
|
+
self.assertAlmostEqual(estimate["usd"], 56.0)
|
|
254
|
+
self.assertTrue(estimate["long_context_pricing_applied"])
|
|
255
|
+
self.assertIn("gpt-5.6-sol", estimate["source"])
|
|
256
|
+
|
|
245
257
|
def test_unknown_subscription_model_is_not_guessed(self):
|
|
246
258
|
self.assertIsNone(subscription_api_equivalent_cost("future-model"))
|
|
247
259
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from toolbench.core.runner import _toolbase_protected_paths
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_toolbase_and_editable_sources_are_protected(tmp_path, monkeypatch):
|
|
7
|
+
toolbase_home = tmp_path / ".toolbase"
|
|
8
|
+
slot = toolbase_home / "cache" / "physics-kit" / "editable"
|
|
9
|
+
slot.mkdir(parents=True)
|
|
10
|
+
source = tmp_path / "physics-kit-source"
|
|
11
|
+
source.mkdir()
|
|
12
|
+
runtime_data = tmp_path / "physics-kit-runtime"
|
|
13
|
+
runtime_data.mkdir()
|
|
14
|
+
(slot / ".install_meta.yaml").write_text(
|
|
15
|
+
"editable: true\n"
|
|
16
|
+
f"source_path: {source}\n"
|
|
17
|
+
)
|
|
18
|
+
monkeypatch.setenv("TOOLBASE_HOME", str(toolbase_home))
|
|
19
|
+
config_dir = toolbase_home / "config"
|
|
20
|
+
config_dir.mkdir()
|
|
21
|
+
(config_dir / "physics-kit.yaml").write_text(
|
|
22
|
+
f"base_directory: {runtime_data}\n"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
paths = _toolbase_protected_paths()
|
|
26
|
+
|
|
27
|
+
assert str(toolbase_home.resolve()) in paths
|
|
28
|
+
assert str(source.resolve()) in paths
|
|
29
|
+
assert str(runtime_data.resolve()) in paths
|
|
@@ -598,6 +598,52 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
598
598
|
release_sources(tmp)
|
|
599
599
|
manifest["resolution"] = resolution_reports
|
|
600
600
|
write_json(run_dir / "manifest.json", manifest)
|
|
601
|
+
|
|
602
|
+
# MCP preflight: for every MCP-serving harness x loadout that serves a
|
|
603
|
+
# toolbase profile, actually start `toolbase serve` and complete a
|
|
604
|
+
# tools/list handshake BEFORE any trial. A profile that resolves but
|
|
605
|
+
# serves no tools (mis-wired toolbase command, env churn) otherwise runs
|
|
606
|
+
# the entire "tools" arm silently tool-less and still grades it as valid.
|
|
607
|
+
# Hard-fail the run here instead; runs verbatim in dry-run too.
|
|
608
|
+
from toolbench.core.runtime import (
|
|
609
|
+
runtime_serves_toolbase_mcp, verify_toolbase_mcp,
|
|
610
|
+
_loadout_toolbase_profile)
|
|
611
|
+
mcp_failures: list[str] = []
|
|
612
|
+
for h in harnesses:
|
|
613
|
+
if not runtime_serves_toolbase_mcp(h.runtime_name):
|
|
614
|
+
continue
|
|
615
|
+
for lo in loadouts:
|
|
616
|
+
profile, proj = _loadout_toolbase_profile(lo)
|
|
617
|
+
if not profile:
|
|
618
|
+
continue
|
|
619
|
+
expected = [t for r in resolution_reports
|
|
620
|
+
if r.get("harness") == h.id and r.get("loadout") == lo.name
|
|
621
|
+
for s in r.get("sources", [])
|
|
622
|
+
if s.get("backend") == "toolbase"
|
|
623
|
+
for t in s.get("tools", [])]
|
|
624
|
+
try:
|
|
625
|
+
served = verify_toolbase_mcp(profile, cwd=(proj or bench_dir))
|
|
626
|
+
missing = [t for t in expected if t not in served]
|
|
627
|
+
if missing:
|
|
628
|
+
mcp_failures.append(
|
|
629
|
+
f"{h.id}/{lo.name} (profile {profile}): server served "
|
|
630
|
+
f"{len(served)} tools, missing {missing}")
|
|
631
|
+
else:
|
|
632
|
+
print(f" MCP preflight OK: {h.id}/{lo.name} — "
|
|
633
|
+
f"{len(served)} tools served ({profile})")
|
|
634
|
+
except Exception as e:
|
|
635
|
+
mcp_failures.append(
|
|
636
|
+
f"{h.id}/{lo.name} (profile {profile}): "
|
|
637
|
+
f"{type(e).__name__}: {e}")
|
|
638
|
+
if mcp_failures:
|
|
639
|
+
print("\n MCP PREFLIGHT FAILED — aborting before any trial ran:")
|
|
640
|
+
for f in mcp_failures:
|
|
641
|
+
print(f" ✗ {f}")
|
|
642
|
+
print(" A `tools` loadout could not reach its tools. Check that "
|
|
643
|
+
"`toolbase` is installed in this env and the profile serves "
|
|
644
|
+
"tools, then re-run.")
|
|
645
|
+
return 2
|
|
646
|
+
|
|
601
647
|
if args.dry_run:
|
|
602
648
|
print(" DRY-RUN: agent.run() will be skipped.")
|
|
603
649
|
|
|
@@ -73,6 +73,15 @@ SUBSCRIPTION_API_EQUIVALENT_PRICING: dict[tuple[str, str], dict[str, float | int
|
|
|
73
73
|
"long_output_multiplier": 1.5,
|
|
74
74
|
"source": "https://developers.openai.com/api/docs/models/gpt-5.5",
|
|
75
75
|
},
|
|
76
|
+
("openai", "gpt-5.6-sol"): {
|
|
77
|
+
"input": 5.00,
|
|
78
|
+
"cached_input": 0.50,
|
|
79
|
+
"output": 30.00,
|
|
80
|
+
"long_context_threshold": 272_000,
|
|
81
|
+
"long_input_multiplier": 2.0,
|
|
82
|
+
"long_output_multiplier": 1.5,
|
|
83
|
+
"source": "https://developers.openai.com/api/docs/models/gpt-5.6-sol",
|
|
84
|
+
},
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
|
|
@@ -19,6 +19,7 @@ import traceback
|
|
|
19
19
|
from dataclasses import dataclass
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
|
|
22
|
+
import yaml
|
|
22
23
|
from orchestral.tools.hooks import TruncateOutputHook
|
|
23
24
|
|
|
24
25
|
from .artifact_policy import DEFAULT_POLICY, ArtifactPolicy
|
|
@@ -67,6 +68,42 @@ _PROVIDER_CONTROL_KEYS = ("name", "cache_bust")
|
|
|
67
68
|
CODE_ARG_KEYS = ("code", "script", "source", "program")
|
|
68
69
|
|
|
69
70
|
|
|
71
|
+
def _toolbase_protected_paths() -> list[str]:
|
|
72
|
+
"""Paths a benchmark agent must not inspect outside the MCP interface.
|
|
73
|
+
|
|
74
|
+
Toolbase may serve editable toolkit checkouts. A CLI agent with ordinary
|
|
75
|
+
filesystem read access could otherwise locate and import those exact tool
|
|
76
|
+
implementations in a ``core_only`` arm, contaminating the control. MCP
|
|
77
|
+
server children are not confined by Codex/Claude's shell filesystem
|
|
78
|
+
profile, so denying these paths to the agent does not prevent the declared
|
|
79
|
+
Toolbase loadout from executing them.
|
|
80
|
+
"""
|
|
81
|
+
root = Path(os.environ.get("TOOLBASE_HOME", Path.home() / ".toolbase")).resolve()
|
|
82
|
+
protected = [str(root)]
|
|
83
|
+
for meta_path in root.glob("cache/*/*/.install_meta.yaml"):
|
|
84
|
+
try:
|
|
85
|
+
meta = yaml.safe_load(meta_path.read_text()) or {}
|
|
86
|
+
source = meta.get("source_path") if meta.get("editable") else None
|
|
87
|
+
if source:
|
|
88
|
+
protected.append(str(Path(source).expanduser().resolve()))
|
|
89
|
+
except Exception:
|
|
90
|
+
continue
|
|
91
|
+
# Toolkit runtime configs can point at persistent working/data directories.
|
|
92
|
+
# Those may contain outputs from earlier tool-assisted runs and are equally
|
|
93
|
+
# inappropriate inputs to a fresh or core-only benchmark trial.
|
|
94
|
+
for config_path in root.glob("config/*.yaml"):
|
|
95
|
+
try:
|
|
96
|
+
config = yaml.safe_load(config_path.read_text()) or {}
|
|
97
|
+
base_directory = config.get("base_directory")
|
|
98
|
+
if base_directory:
|
|
99
|
+
protected.append(
|
|
100
|
+
str(Path(base_directory).expanduser().resolve())
|
|
101
|
+
)
|
|
102
|
+
except Exception:
|
|
103
|
+
continue
|
|
104
|
+
return list(dict.fromkeys(protected))
|
|
105
|
+
|
|
106
|
+
|
|
70
107
|
def _warn(prefix: str, exc: BaseException) -> None:
|
|
71
108
|
"""Emit a one-line stderr breadcrumb for a non-fatal failure.
|
|
72
109
|
|
|
@@ -452,8 +489,11 @@ class TrialRunner:
|
|
|
452
489
|
# The benchmark tree holds the ground-truth answer key
|
|
453
490
|
# (soln/); the Bash sandbox (if the harness enables it)
|
|
454
491
|
# deny-reads these so a trial cannot reach it.
|
|
455
|
-
protected_paths=(
|
|
456
|
-
|
|
492
|
+
protected_paths=(
|
|
493
|
+
((benchmark_dir if isinstance(benchmark_dir, list)
|
|
494
|
+
else [benchmark_dir]) if benchmark_dir else [])
|
|
495
|
+
+ _toolbase_protected_paths()
|
|
496
|
+
),
|
|
457
497
|
)
|
|
458
498
|
# One resume loop over the SAME agent / sandbox / context.
|
|
459
499
|
# After each agent.run we either:
|
|
@@ -185,6 +185,35 @@ register_runtime("orchestral", _orchestral_factory, dist="orchestral-ai")
|
|
|
185
185
|
|
|
186
186
|
# The MCP server name `tb connect claude-code` writes into .mcp.json.
|
|
187
187
|
_TOOLBASE_MCP_SERVER = "toolbase"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _toolbase_command() -> str:
|
|
191
|
+
"""Resolve the Toolbase executable for child MCP processes.
|
|
192
|
+
|
|
193
|
+
Invoking Toolbench with an absolute virtual-environment Python does not
|
|
194
|
+
activate that environment or prepend its ``bin`` directory to ``PATH``.
|
|
195
|
+
Prefer PATH when available, then the executable beside the running Python.
|
|
196
|
+
|
|
197
|
+
Raises ``RuntimeError`` when Toolbase cannot be located, instead of
|
|
198
|
+
returning a bare ``"toolbase"``. A bare command is what silently broke a
|
|
199
|
+
campaign: a child MCP process launched as ``toolbase serve ...`` failed to
|
|
200
|
+
start (``command not found`` on the child's PATH), the agent merely saw
|
|
201
|
+
"No such tool available", and the trial still graded as if it had tools.
|
|
202
|
+
Fail loudly here — the run preflight (`verify_toolbase_mcp`) turns this into
|
|
203
|
+
an abort before any trial runs.
|
|
204
|
+
"""
|
|
205
|
+
found = shutil.which("toolbase")
|
|
206
|
+
if found:
|
|
207
|
+
return found
|
|
208
|
+
sibling = Path(sys.executable).resolve().with_name("toolbase")
|
|
209
|
+
if sibling.is_file():
|
|
210
|
+
return str(sibling)
|
|
211
|
+
raise RuntimeError(
|
|
212
|
+
"toolbase executable not found: neither on PATH nor beside the running "
|
|
213
|
+
f"Python ({Path(sys.executable).resolve().with_name('toolbase')}). "
|
|
214
|
+
"Install toolbase into this environment (`pip install toolbase`); a CLI "
|
|
215
|
+
"runtime cannot serve the loadout's MCP tools without it."
|
|
216
|
+
)
|
|
188
217
|
# No hardcoded toolkit: the MCP profile a CLI runtime serves is derived from the
|
|
189
218
|
# benchmark's loadout (its `toolbase: {profile: ...}` source) via
|
|
190
219
|
# `_loadout_toolbase_profile`. None => serve no MCP server (the `core` baseline
|
|
@@ -235,6 +264,109 @@ def _loadout_toolbase_profile(loadout) -> tuple[str | None, str | None]:
|
|
|
235
264
|
return None, None
|
|
236
265
|
|
|
237
266
|
|
|
267
|
+
# Runtimes that serve the loadout's toolbase profile to their agent over a
|
|
268
|
+
# stdio MCP server (`toolbase serve --profile ...`) rather than resolving tools
|
|
269
|
+
# in-process. For these the MCP connection is verified in the run preflight: a
|
|
270
|
+
# profile that resolves but serves zero tools (a mis-wired toolbase command,
|
|
271
|
+
# env churn) otherwise runs the whole "tools" arm silently tool-less. A new CLI
|
|
272
|
+
# runtime that serves toolbase over MCP registers its name here.
|
|
273
|
+
_MCP_SERVING_RUNTIMES = {"claude_code", "codex"}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def runtime_serves_toolbase_mcp(runtime_name: str) -> bool:
|
|
277
|
+
"""True when a harness's runtime drives its agent's tools through a
|
|
278
|
+
`toolbase serve` MCP child (claude-code, codex), so the run preflight must
|
|
279
|
+
verify the server actually serves its tools. In-process runtimes
|
|
280
|
+
(orchestral) resolve tools directly and need no MCP check."""
|
|
281
|
+
return (runtime_name or "").lower() in _MCP_SERVING_RUNTIMES
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def verify_toolbase_mcp(profile: str, *, cwd: str, call_timeout_s: int = 60,
|
|
285
|
+
timeout: float = 45.0) -> list[str]:
|
|
286
|
+
"""Start `toolbase serve --profile <profile>` exactly as a CLI runtime does
|
|
287
|
+
and complete an MCP initialize + tools/list handshake, returning the served
|
|
288
|
+
tool names.
|
|
289
|
+
|
|
290
|
+
Raises ``RuntimeError`` if the server cannot launch, the handshake times
|
|
291
|
+
out, or it serves zero tools. This is the preflight guard that a `tools`
|
|
292
|
+
loadout actually reaches its tools *before* any trial runs (a served-but-
|
|
293
|
+
empty toolset previously graded as a valid tools arm)."""
|
|
294
|
+
import queue
|
|
295
|
+
import threading
|
|
296
|
+
import time as _time
|
|
297
|
+
|
|
298
|
+
cmd = [_toolbase_command(), "serve", "--profile", profile,
|
|
299
|
+
"--call-timeout", str(call_timeout_s)]
|
|
300
|
+
try:
|
|
301
|
+
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
302
|
+
stderr=subprocess.PIPE, text=True, cwd=cwd, bufsize=1)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
raise RuntimeError(f"could not launch `{' '.join(cmd)}`: {e}") from e
|
|
305
|
+
|
|
306
|
+
err_chunks: list[str] = []
|
|
307
|
+
threading.Thread(target=_drain_stream, args=(proc.stderr, err_chunks),
|
|
308
|
+
daemon=True).start()
|
|
309
|
+
out_q: "queue.Queue" = queue.Queue()
|
|
310
|
+
|
|
311
|
+
def _read_out():
|
|
312
|
+
try:
|
|
313
|
+
for line in proc.stdout: # type: ignore[union-attr]
|
|
314
|
+
out_q.put(line)
|
|
315
|
+
except Exception:
|
|
316
|
+
pass
|
|
317
|
+
out_q.put(None)
|
|
318
|
+
|
|
319
|
+
threading.Thread(target=_read_out, daemon=True).start()
|
|
320
|
+
|
|
321
|
+
def _send(obj):
|
|
322
|
+
proc.stdin.write(json.dumps(obj) + "\n") # type: ignore[union-attr]
|
|
323
|
+
proc.stdin.flush() # type: ignore[union-attr]
|
|
324
|
+
|
|
325
|
+
def _fail(msg):
|
|
326
|
+
tail = "".join(err_chunks)[-400:].strip()
|
|
327
|
+
raise RuntimeError(f"{msg} (server exit={proc.poll()}"
|
|
328
|
+
+ (f"; stderr: …{tail}" if tail else "") + ")")
|
|
329
|
+
try:
|
|
330
|
+
_send({"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
|
331
|
+
"params": {"protocolVersion": "2024-11-05", "capabilities": {},
|
|
332
|
+
"clientInfo": {"name": "toolbench-preflight",
|
|
333
|
+
"version": "0"}}})
|
|
334
|
+
_send({"jsonrpc": "2.0", "method": "notifications/initialized"})
|
|
335
|
+
_send({"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
|
|
336
|
+
deadline = _time.monotonic() + timeout
|
|
337
|
+
while _time.monotonic() < deadline:
|
|
338
|
+
try:
|
|
339
|
+
line = out_q.get(timeout=1.0)
|
|
340
|
+
except queue.Empty:
|
|
341
|
+
if proc.poll() is not None:
|
|
342
|
+
_fail(f"MCP server for profile {profile!r} exited before "
|
|
343
|
+
"returning tools/list")
|
|
344
|
+
continue
|
|
345
|
+
if line is None:
|
|
346
|
+
_fail(f"MCP server for profile {profile!r} closed its output "
|
|
347
|
+
"before returning tools/list")
|
|
348
|
+
try:
|
|
349
|
+
msg = json.loads(line)
|
|
350
|
+
except Exception:
|
|
351
|
+
continue
|
|
352
|
+
if msg.get("id") == 2:
|
|
353
|
+
tools = [t.get("name")
|
|
354
|
+
for t in (msg.get("result") or {}).get("tools", [])]
|
|
355
|
+
if not tools:
|
|
356
|
+
_fail(f"MCP server served 0 tools for profile {profile!r}")
|
|
357
|
+
return tools
|
|
358
|
+
_fail(f"MCP handshake for profile {profile!r} timed out after "
|
|
359
|
+
f"{timeout:.0f}s")
|
|
360
|
+
finally:
|
|
361
|
+
try:
|
|
362
|
+
proc.terminate(); proc.wait(timeout=5)
|
|
363
|
+
except Exception:
|
|
364
|
+
try:
|
|
365
|
+
proc.kill()
|
|
366
|
+
except Exception:
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
|
|
238
370
|
class _ClaudeCodeResponse:
|
|
239
371
|
"""Minimal response object matching the runner's defensive reads:
|
|
240
372
|
`.text`, `.tool_calls`, and a `.context.messages` iterable."""
|
|
@@ -323,7 +455,7 @@ class ClaudeCodeAgent:
|
|
|
323
455
|
"mcpServers": {
|
|
324
456
|
_TOOLBASE_MCP_SERVER: {
|
|
325
457
|
"type": "stdio",
|
|
326
|
-
"command":
|
|
458
|
+
"command": _toolbase_command(),
|
|
327
459
|
"args": args,
|
|
328
460
|
}
|
|
329
461
|
}
|
|
@@ -728,7 +860,7 @@ class CodexAgent:
|
|
|
728
860
|
"--call-timeout", str(self.call_timeout_s)]
|
|
729
861
|
return [
|
|
730
862
|
"-c", f"mcp_servers.{_TOOLBASE_MCP_SERVER}.command="
|
|
731
|
-
+ json.dumps(
|
|
863
|
+
+ json.dumps(_toolbase_command()),
|
|
732
864
|
"-c", f"mcp_servers.{_TOOLBASE_MCP_SERVER}.args="
|
|
733
865
|
+ json.dumps(serve_args),
|
|
734
866
|
# `codex exec` is noninteractive. Without a server-level default,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{toolbench-0.2.1 → toolbench-0.3.0}/examples/geometry/variants/direct/sandbox/template/points.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|