diff --git a/libs/hackbot-runtime/hackbot_runtime/config.py b/libs/hackbot-runtime/hackbot_runtime/config.py index 3b43aa62c65..83dd121d14e 100644 --- a/libs/hackbot-runtime/hackbot_runtime/config.py +++ b/libs/hackbot-runtime/hackbot_runtime/config.py @@ -7,6 +7,8 @@ """ import tomllib +from functools import cached_property +from importlib import metadata from pathlib import Path from pydantic import BaseModel @@ -42,6 +44,18 @@ class HackbotConfig(BaseModel): source: SourceConfig | None = None firefox: FirefoxConfig | None = None + # Set by the runtime from the entry point, not read from the toml. + agent_name: str | None = None + + @cached_property + def agent_version(self) -> str | None: + """The version in the agent's pyproject.toml, or None when it isn't installed.""" + if self.agent_name is None: + return None + try: + return metadata.version(f"hackbot-agent-{self.agent_name}") + except metadata.PackageNotFoundError: + return None def load_config(path: Path) -> HackbotConfig: diff --git a/libs/hackbot-runtime/hackbot_runtime/runtime.py b/libs/hackbot-runtime/hackbot_runtime/runtime.py index ede5dbc65f1..4cecbb629cd 100644 --- a/libs/hackbot-runtime/hackbot_runtime/runtime.py +++ b/libs/hackbot-runtime/hackbot_runtime/runtime.py @@ -118,9 +118,12 @@ def _discover_config_path(entrypoint: Callable) -> Path | None: def _resolve_config(entrypoint: Callable, config: ConfigArg) -> HackbotConfig: if isinstance(config, HackbotConfig): - return config - path = config if isinstance(config, Path) else _discover_config_path(entrypoint) - return load_config(path) if path else HackbotConfig() + parsed = config + else: + path = config if isinstance(config, Path) else _discover_config_path(entrypoint) + parsed = load_config(path) if path else HackbotConfig() + parsed.agent_name = resolve_agent_name(entrypoint) + return parsed def _load_hackbot(entrypoint: Callable, config: ConfigArg) -> HackbotContext | None: @@ -169,6 +172,7 @@ def _finish(ctx: HackbotContext, outcome: object) -> int: log.error(msg) payload = _error_payload(ctx, msg) exit_code = 1 + payload["agent_version"] = ctx.config.agent_version try: _publish_log(ctx) diff --git a/libs/hackbot-runtime/tests/test_runtime.py b/libs/hackbot-runtime/tests/test_runtime.py index 02d7a81ef68..ca1581e2dc0 100644 --- a/libs/hackbot-runtime/tests/test_runtime.py +++ b/libs/hackbot-runtime/tests/test_runtime.py @@ -8,6 +8,7 @@ AgentError, HackbotAgentResult, HackbotContext, + config, run_async, runtime, ) @@ -228,6 +229,21 @@ async def main(ctx): assert summary["findings"] == {"num_turns": 1, "total_cost_usd": None} +def test_run_async_records_agent_version(tmp_path, monkeypatch): + _run_env(tmp_path, monkeypatch) + versions = {"hackbot-agent-tests": "1.2.3"} + monkeypatch.setattr(config.metadata, "version", versions.__getitem__) + + async def main(ctx): + return HackbotAgentResult(num_turns=1) + + with pytest.raises(SystemExit): + run_async(main) + + summary = json.loads((tmp_path / "t" / "summary.json").read_text()) + assert summary["agent_version"] == "1.2.3" + + def test_run_async_exits_nonzero_when_agent_raises(tmp_path, monkeypatch): _run_env(tmp_path, monkeypatch) diff --git a/services/hackbot-api/app/schemas.py b/services/hackbot-api/app/schemas.py index 4eda982a7e3..db129d0e336 100644 --- a/services/hackbot-api/app/schemas.py +++ b/services/hackbot-api/app/schemas.py @@ -29,6 +29,7 @@ class ArtifactRef(BaseModel): class RunSummary(BaseModel): status: str error: str | None = None + agent_version: str | None = None findings: dict[str, Any] = {} actions: list[dict[str, Any]] = []