From 63e24236ee096b16b9a2a58d88d3c4832298b9dd Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Tue, 27 Oct 2026 21:32:11 +0100 Subject: [PATCH 1/6] Record the agent's commit in each run's summary --- agents/bug-fix/Dockerfile | 3 +++ agents/cloudbuild.yaml | 1 + libs/hackbot-runtime/hackbot_runtime/runtime.py | 2 ++ libs/hackbot-runtime/tests/test_runtime.py | 11 +++++++++++ services/hackbot-api/app/schemas.py | 1 + 5 files changed, 18 insertions(+) diff --git a/agents/bug-fix/Dockerfile b/agents/bug-fix/Dockerfile index b1eb74a5c55..27bc4a0fc61 100644 --- a/agents/bug-fix/Dockerfile +++ b/agents/bug-fix/Dockerfile @@ -122,6 +122,9 @@ ENV PATH="${PATH}:/home/agent/.mozbuild/jdk/current/bin:/home/agent/.mozbuild/an # packages, mostly) stay cached across those. COPY --from=builder /opt/venv /opt/venv +ARG HACKBOT_AGENT_COMMIT +ENV HACKBOT_AGENT_COMMIT=$HACKBOT_AGENT_COMMIT + USER agent ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/agents/cloudbuild.yaml b/agents/cloudbuild.yaml index 094624b5209..5d3f0862ca7 100644 --- a/agents/cloudbuild.yaml +++ b/agents/cloudbuild.yaml @@ -36,6 +36,7 @@ steps: docker build -f "agents/${_AGENT}/Dockerfile" --target "$$target" \ --label "hackbot.config_hash=$$CONFIG_HASH" \ --label "org.opencontainers.image.revision=$COMMIT_SHA" \ + --build-arg "HACKBOT_AGENT_COMMIT=$COMMIT_SHA" \ --label "org.opencontainers.image.created=$$created" \ -t "$$image" . docker push "$$image" diff --git a/libs/hackbot-runtime/hackbot_runtime/runtime.py b/libs/hackbot-runtime/hackbot_runtime/runtime.py index e93afc31b59..24be22e71b4 100644 --- a/libs/hackbot-runtime/hackbot_runtime/runtime.py +++ b/libs/hackbot-runtime/hackbot_runtime/runtime.py @@ -73,6 +73,7 @@ def _ok_payload(ctx: HackbotContext, findings: dict) -> dict: return { "status": "ok", "error": None, + "agent_commit": os.environ.get("HACKBOT_AGENT_COMMIT"), "findings": findings, "actions": ctx.actions.actions, } @@ -84,6 +85,7 @@ def _error_payload( return { "status": "error", "error": error, + "agent_commit": os.environ.get("HACKBOT_AGENT_COMMIT"), "findings": {"traceback": traceback_str} if traceback_str else {}, "actions": ctx.actions.actions, } diff --git a/libs/hackbot-runtime/tests/test_runtime.py b/libs/hackbot-runtime/tests/test_runtime.py index 7ecc8e6fd1d..2844fce968f 100644 --- a/libs/hackbot-runtime/tests/test_runtime.py +++ b/libs/hackbot-runtime/tests/test_runtime.py @@ -64,6 +64,17 @@ def test_summary_written_for_exception(tmp_path): assert "boom" in summary["error"] +def test_summary_records_agent_commit(tmp_path, monkeypatch): + monkeypatch.setenv("HACKBOT_AGENT_COMMIT", "abc123") + ctx = _ctx(tmp_path) + _finish(ctx, HackbotAgentResult(num_turns=1)) + + summary = json.loads( + (tmp_path / "artifacts" / "local-test" / "summary.json").read_text() + ) + assert summary["agent_commit"] == "abc123" + + def test_removed_action_is_absent_from_summary(tmp_path): ctx = _ctx(tmp_path) inaccurate = ctx.actions.record( diff --git a/services/hackbot-api/app/schemas.py b/services/hackbot-api/app/schemas.py index 99349258612..fbb1fbfb23b 100644 --- a/services/hackbot-api/app/schemas.py +++ b/services/hackbot-api/app/schemas.py @@ -23,6 +23,7 @@ class ArtifactRef(BaseModel): class RunSummary(BaseModel): status: str error: str | None = None + agent_commit: str | None = None findings: dict[str, Any] = {} actions: list[dict[str, Any]] = [] From e098a4568cf1ab2201ddcf597a0c6b6b5d3d7d8a Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Thu, 29 Oct 2026 09:25:01 +0100 Subject: [PATCH 2/6] Record the agent's package version in each run's summary --- agents/bug-fix/Dockerfile | 3 --- agents/cloudbuild.yaml | 1 - .../hackbot_runtime/runtime.py | 22 ++++++++++++---- libs/hackbot-runtime/tests/test_runtime.py | 26 +++++++++++-------- services/hackbot-api/app/schemas.py | 2 +- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/agents/bug-fix/Dockerfile b/agents/bug-fix/Dockerfile index 27bc4a0fc61..b1eb74a5c55 100644 --- a/agents/bug-fix/Dockerfile +++ b/agents/bug-fix/Dockerfile @@ -122,9 +122,6 @@ ENV PATH="${PATH}:/home/agent/.mozbuild/jdk/current/bin:/home/agent/.mozbuild/an # packages, mostly) stay cached across those. COPY --from=builder /opt/venv /opt/venv -ARG HACKBOT_AGENT_COMMIT -ENV HACKBOT_AGENT_COMMIT=$HACKBOT_AGENT_COMMIT - USER agent ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/agents/cloudbuild.yaml b/agents/cloudbuild.yaml index 5d3f0862ca7..094624b5209 100644 --- a/agents/cloudbuild.yaml +++ b/agents/cloudbuild.yaml @@ -36,7 +36,6 @@ steps: docker build -f "agents/${_AGENT}/Dockerfile" --target "$$target" \ --label "hackbot.config_hash=$$CONFIG_HASH" \ --label "org.opencontainers.image.revision=$COMMIT_SHA" \ - --build-arg "HACKBOT_AGENT_COMMIT=$COMMIT_SHA" \ --label "org.opencontainers.image.created=$$created" \ -t "$$image" . docker push "$$image" diff --git a/libs/hackbot-runtime/hackbot_runtime/runtime.py b/libs/hackbot-runtime/hackbot_runtime/runtime.py index 24be22e71b4..71be3c9fba6 100644 --- a/libs/hackbot-runtime/hackbot_runtime/runtime.py +++ b/libs/hackbot-runtime/hackbot_runtime/runtime.py @@ -5,6 +5,7 @@ import sys import traceback from collections.abc import Awaitable, Callable +from importlib import metadata from pathlib import Path from typing import NoReturn @@ -59,6 +60,14 @@ def _configure_sentry(entrypoint: Callable) -> None: sentry_sdk.set_tag("agent", resolve_agent_name(entrypoint)) +def _agent_version(entrypoint: Callable) -> str | None: + """The version in the agent's pyproject.toml, or None when it isn't installed.""" + try: + return metadata.version(f"hackbot-agent-{resolve_agent_name(entrypoint)}") + except metadata.PackageNotFoundError: + return None + + def _configure_logging() -> None: if not logging.getLogger().handlers: logging.basicConfig( @@ -73,7 +82,6 @@ def _ok_payload(ctx: HackbotContext, findings: dict) -> dict: return { "status": "ok", "error": None, - "agent_commit": os.environ.get("HACKBOT_AGENT_COMMIT"), "findings": findings, "actions": ctx.actions.actions, } @@ -85,7 +93,6 @@ def _error_payload( return { "status": "error", "error": error, - "agent_commit": os.environ.get("HACKBOT_AGENT_COMMIT"), "findings": {"traceback": traceback_str} if traceback_str else {}, "actions": ctx.actions.actions, } @@ -149,7 +156,9 @@ def _publish_log(ctx: HackbotContext) -> None: ctx.publish_file(_AGENT_LOG_KEY, ctx.log_path, "text/plain; charset=utf-8") -def _finish(ctx: HackbotContext, outcome: object) -> int: +def _finish( + ctx: HackbotContext, outcome: object, agent_version: str | None = None +) -> int: """Write summary.json from the agent's outcome and return the exit code. ``outcome`` is the agent's :class:`HackbotAgentResult` on success, or the @@ -171,6 +180,7 @@ def _finish(ctx: HackbotContext, outcome: object) -> int: log.error(msg) payload = _error_payload(ctx, msg) exit_code = 1 + payload["agent_version"] = agent_version try: _publish_log(ctx) @@ -215,7 +225,8 @@ def run(entrypoint: AgentMain, config: ConfigArg = None) -> NoReturn: log.exception("Agent raised an exception") outcome = exc - raise SystemExit(_finish(ctx, outcome)) + agent_version = _agent_version(entrypoint) + raise SystemExit(_finish(ctx, outcome, agent_version)) def run_async(entrypoint: AsyncAgentMain, config: ConfigArg = None) -> NoReturn: @@ -234,4 +245,5 @@ def run_async(entrypoint: AsyncAgentMain, config: ConfigArg = None) -> NoReturn: log.exception("Agent raised an exception") outcome = exc - raise SystemExit(_finish(ctx, outcome)) + agent_version = _agent_version(entrypoint) + raise SystemExit(_finish(ctx, outcome, agent_version)) diff --git a/libs/hackbot-runtime/tests/test_runtime.py b/libs/hackbot-runtime/tests/test_runtime.py index 2844fce968f..af787726119 100644 --- a/libs/hackbot-runtime/tests/test_runtime.py +++ b/libs/hackbot-runtime/tests/test_runtime.py @@ -64,17 +64,6 @@ def test_summary_written_for_exception(tmp_path): assert "boom" in summary["error"] -def test_summary_records_agent_commit(tmp_path, monkeypatch): - monkeypatch.setenv("HACKBOT_AGENT_COMMIT", "abc123") - ctx = _ctx(tmp_path) - _finish(ctx, HackbotAgentResult(num_turns=1)) - - summary = json.loads( - (tmp_path / "artifacts" / "local-test" / "summary.json").read_text() - ) - assert summary["agent_commit"] == "abc123" - - def test_removed_action_is_absent_from_summary(tmp_path): ctx = _ctx(tmp_path) inaccurate = ctx.actions.record( @@ -238,6 +227,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(runtime.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 fbb1fbfb23b..b261a11446f 100644 --- a/services/hackbot-api/app/schemas.py +++ b/services/hackbot-api/app/schemas.py @@ -23,7 +23,7 @@ class ArtifactRef(BaseModel): class RunSummary(BaseModel): status: str error: str | None = None - agent_commit: str | None = None + agent_version: str | None = None findings: dict[str, Any] = {} actions: list[dict[str, Any]] = [] From 521415109ba5cb16805d55dfade347d25771699f Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Fri, 6 Nov 2026 19:13:26 +0100 Subject: [PATCH 3/6] Store the agent name and version on HackbotContext --- .../hackbot_runtime/context.py | 20 +++++++++++++++++- .../hackbot_runtime/runtime.py | 21 +++++++++---------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/libs/hackbot-runtime/hackbot_runtime/context.py b/libs/hackbot-runtime/hackbot_runtime/context.py index 8334df607d0..ef0fc050204 100644 --- a/libs/hackbot-runtime/hackbot_runtime/context.py +++ b/libs/hackbot-runtime/hackbot_runtime/context.py @@ -86,6 +86,9 @@ class HackbotContext(BaseSettings): # Capability declarations from hackbot.toml (not env); attached after # construction via from_config()/from_config_obj(). _config: HackbotConfig = PrivateAttr(default_factory=HackbotConfig) + # Which agent is running and its package version, set by the runtime. + _agent_name: str | None = PrivateAttr(default=None) + _agent_version: str | None = PrivateAttr(default=None) # The commit the source checkout started from, recorded when source_repo is # prepared. Stays None for agents that never touch source, which is how # publish_changes() knows there are no changes to collect. @@ -107,16 +110,31 @@ def from_config(cls, config_path: Path) -> "HackbotContext": return cls.from_config_obj(load_config(config_path)) @classmethod - def from_config_obj(cls, config: HackbotConfig) -> "HackbotContext": + def from_config_obj( + cls, + config: HackbotConfig, + agent_name: str | None = None, + agent_version: str | None = None, + ) -> "HackbotContext": """Build from an already-parsed config plus env-derived fields.""" obj = cls() obj._config = config + obj._agent_name = agent_name + obj._agent_version = agent_version return obj @property def config(self) -> HackbotConfig: return self._config + @property + def agent_name(self) -> str | None: + return self._agent_name + + @property + def agent_version(self) -> str | None: + return self._agent_version + # --- Platform capabilities (declared in hackbot.toml) ------------- # async def prepare_repo( diff --git a/libs/hackbot-runtime/hackbot_runtime/runtime.py b/libs/hackbot-runtime/hackbot_runtime/runtime.py index 71be3c9fba6..46fc70d791f 100644 --- a/libs/hackbot-runtime/hackbot_runtime/runtime.py +++ b/libs/hackbot-runtime/hackbot_runtime/runtime.py @@ -60,10 +60,10 @@ def _configure_sentry(entrypoint: Callable) -> None: sentry_sdk.set_tag("agent", resolve_agent_name(entrypoint)) -def _agent_version(entrypoint: Callable) -> str | None: +def _agent_version(agent_name: str) -> str | None: """The version in the agent's pyproject.toml, or None when it isn't installed.""" try: - return metadata.version(f"hackbot-agent-{resolve_agent_name(entrypoint)}") + return metadata.version(f"hackbot-agent-{agent_name}") except metadata.PackageNotFoundError: return None @@ -140,8 +140,11 @@ def _load_hackbot(entrypoint: Callable, config: ConfigArg) -> HackbotContext | N the entry point's module), falling back to an empty config when there's none. """ parsed = _resolve_config(entrypoint, config) + agent_name = resolve_agent_name(entrypoint) try: - return HackbotContext.from_config_obj(parsed) + return HackbotContext.from_config_obj( + parsed, agent_name=agent_name, agent_version=_agent_version(agent_name) + ) except ValidationError as exc: log.error( "Failed to load HackbotContext from env; no summary can be written.\n%s", @@ -156,9 +159,7 @@ def _publish_log(ctx: HackbotContext) -> None: ctx.publish_file(_AGENT_LOG_KEY, ctx.log_path, "text/plain; charset=utf-8") -def _finish( - ctx: HackbotContext, outcome: object, agent_version: str | None = None -) -> int: +def _finish(ctx: HackbotContext, outcome: object) -> int: """Write summary.json from the agent's outcome and return the exit code. ``outcome`` is the agent's :class:`HackbotAgentResult` on success, or the @@ -180,7 +181,7 @@ def _finish( log.error(msg) payload = _error_payload(ctx, msg) exit_code = 1 - payload["agent_version"] = agent_version + payload["agent_version"] = ctx.agent_version try: _publish_log(ctx) @@ -225,8 +226,7 @@ def run(entrypoint: AgentMain, config: ConfigArg = None) -> NoReturn: log.exception("Agent raised an exception") outcome = exc - agent_version = _agent_version(entrypoint) - raise SystemExit(_finish(ctx, outcome, agent_version)) + raise SystemExit(_finish(ctx, outcome)) def run_async(entrypoint: AsyncAgentMain, config: ConfigArg = None) -> NoReturn: @@ -245,5 +245,4 @@ def run_async(entrypoint: AsyncAgentMain, config: ConfigArg = None) -> NoReturn: log.exception("Agent raised an exception") outcome = exc - agent_version = _agent_version(entrypoint) - raise SystemExit(_finish(ctx, outcome, agent_version)) + raise SystemExit(_finish(ctx, outcome)) From 48b6e19e2b1c665682b21cdf17a9c874a02e526f Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Tue, 10 Nov 2026 22:22:05 +0100 Subject: [PATCH 4/6] Look up the agent version from the context instead of passing it in --- .../hackbot-runtime/hackbot_runtime/config.py | 2 ++ .../hackbot_runtime/context.py | 27 +++++++------------ .../hackbot_runtime/runtime.py | 15 ++--------- libs/hackbot-runtime/tests/test_runtime.py | 3 ++- 4 files changed, 16 insertions(+), 31 deletions(-) diff --git a/libs/hackbot-runtime/hackbot_runtime/config.py b/libs/hackbot-runtime/hackbot_runtime/config.py index 3b43aa62c65..790953f54c2 100644 --- a/libs/hackbot-runtime/hackbot_runtime/config.py +++ b/libs/hackbot-runtime/hackbot_runtime/config.py @@ -42,6 +42,8 @@ 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 def load_config(path: Path) -> HackbotConfig: diff --git a/libs/hackbot-runtime/hackbot_runtime/context.py b/libs/hackbot-runtime/hackbot_runtime/context.py index bd74242021f..44eb779fab9 100644 --- a/libs/hackbot-runtime/hackbot_runtime/context.py +++ b/libs/hackbot-runtime/hackbot_runtime/context.py @@ -16,6 +16,7 @@ import tempfile import uuid from functools import cached_property +from importlib import metadata from pathlib import Path from typing import TYPE_CHECKING, Annotated @@ -91,9 +92,6 @@ class HackbotContext(BaseSettings): # Capability declarations from hackbot.toml (not env); attached after # construction via from_config()/from_config_obj(). _config: HackbotConfig = PrivateAttr(default_factory=HackbotConfig) - # Which agent is running and its package version, set by the runtime. - _agent_name: str | None = PrivateAttr(default=None) - _agent_version: str | None = PrivateAttr(default=None) # The commit the source checkout started from, recorded when source_repo is # prepared. Stays None for agents that never touch source, which is how # publish_changes() knows there are no changes to collect. @@ -116,30 +114,25 @@ def from_config(cls, config_path: Path) -> HackbotContext: return cls.from_config_obj(load_config(config_path)) @classmethod - def from_config_obj( - cls, - config: HackbotConfig, - agent_name: str | None = None, - agent_version: str | None = None, - ) -> HackbotContext: + def from_config_obj(cls, config: HackbotConfig) -> HackbotContext: """Build from an already-parsed config plus env-derived fields.""" obj = cls() obj._config = config - obj._agent_name = agent_name - obj._agent_version = agent_version return obj @property def config(self) -> HackbotConfig: return self._config - @property - def agent_name(self) -> str | None: - return self._agent_name - - @property + @cached_property def agent_version(self) -> str | None: - return self._agent_version + """The version in the agent's pyproject.toml, or None when it isn't installed.""" + if self._config.agent_name is None: + return None + try: + return metadata.version(f"hackbot-agent-{self._config.agent_name}") + except metadata.PackageNotFoundError: + return None # --- Platform capabilities (declared in hackbot.toml) ------------- # diff --git a/libs/hackbot-runtime/hackbot_runtime/runtime.py b/libs/hackbot-runtime/hackbot_runtime/runtime.py index 50ed2acb655..00d31a1ce99 100644 --- a/libs/hackbot-runtime/hackbot_runtime/runtime.py +++ b/libs/hackbot-runtime/hackbot_runtime/runtime.py @@ -5,7 +5,6 @@ import sys import traceback from collections.abc import Awaitable, Callable -from importlib import metadata from pathlib import Path from typing import NoReturn @@ -60,14 +59,6 @@ def _configure_sentry(entrypoint: Callable) -> None: sentry_sdk.set_tag("agent", resolve_agent_name(entrypoint)) -def _agent_version(agent_name: str) -> str | None: - """The version in the agent's pyproject.toml, or None when it isn't installed.""" - try: - return metadata.version(f"hackbot-agent-{agent_name}") - except metadata.PackageNotFoundError: - return None - - def _configure_logging() -> None: if not logging.getLogger().handlers: logging.basicConfig( @@ -140,11 +131,9 @@ def _load_hackbot(entrypoint: Callable, config: ConfigArg) -> HackbotContext | N the entry point's module), falling back to an empty config when there's none. """ parsed = _resolve_config(entrypoint, config) - agent_name = resolve_agent_name(entrypoint) + parsed.agent_name = resolve_agent_name(entrypoint) try: - return HackbotContext.from_config_obj( - parsed, agent_name=agent_name, agent_version=_agent_version(agent_name) - ) + return HackbotContext.from_config_obj(parsed) except ValidationError as exc: log.error( "Failed to load HackbotContext from env; no summary can be written.\n%s", diff --git a/libs/hackbot-runtime/tests/test_runtime.py b/libs/hackbot-runtime/tests/test_runtime.py index afd4b4bac68..0930e938fe4 100644 --- a/libs/hackbot-runtime/tests/test_runtime.py +++ b/libs/hackbot-runtime/tests/test_runtime.py @@ -8,6 +8,7 @@ AgentError, HackbotAgentResult, HackbotContext, + context, run_async, runtime, ) @@ -231,7 +232,7 @@ async def main(ctx): def test_run_async_records_agent_version(tmp_path, monkeypatch): _run_env(tmp_path, monkeypatch) versions = {"hackbot-agent-tests": "1.2.3"} - monkeypatch.setattr(runtime.metadata, "version", versions.__getitem__) + monkeypatch.setattr(context.metadata, "version", versions.__getitem__) async def main(ctx): return HackbotAgentResult(num_turns=1) From 546139275f52a42938fe039c068aea2e0769fd59 Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Thu, 12 Nov 2026 18:15:52 +0100 Subject: [PATCH 5/6] Move agent_version to HackbotConfig --- libs/hackbot-runtime/hackbot_runtime/config.py | 12 ++++++++++++ libs/hackbot-runtime/hackbot_runtime/context.py | 11 ----------- libs/hackbot-runtime/hackbot_runtime/runtime.py | 2 +- libs/hackbot-runtime/tests/test_runtime.py | 4 ++-- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/libs/hackbot-runtime/hackbot_runtime/config.py b/libs/hackbot-runtime/hackbot_runtime/config.py index 790953f54c2..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 @@ -45,6 +47,16 @@ class HackbotConfig(BaseModel): # 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: """Load and validate ``hackbot.toml`` at ``path``. diff --git a/libs/hackbot-runtime/hackbot_runtime/context.py b/libs/hackbot-runtime/hackbot_runtime/context.py index 44eb779fab9..4450834934a 100644 --- a/libs/hackbot-runtime/hackbot_runtime/context.py +++ b/libs/hackbot-runtime/hackbot_runtime/context.py @@ -16,7 +16,6 @@ import tempfile import uuid from functools import cached_property -from importlib import metadata from pathlib import Path from typing import TYPE_CHECKING, Annotated @@ -124,16 +123,6 @@ def from_config_obj(cls, config: HackbotConfig) -> HackbotContext: def config(self) -> HackbotConfig: return self._config - @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._config.agent_name is None: - return None - try: - return metadata.version(f"hackbot-agent-{self._config.agent_name}") - except metadata.PackageNotFoundError: - return None - # --- Platform capabilities (declared in hackbot.toml) ------------- # async def prepare_repo( diff --git a/libs/hackbot-runtime/hackbot_runtime/runtime.py b/libs/hackbot-runtime/hackbot_runtime/runtime.py index 00d31a1ce99..6f789f98c0e 100644 --- a/libs/hackbot-runtime/hackbot_runtime/runtime.py +++ b/libs/hackbot-runtime/hackbot_runtime/runtime.py @@ -170,7 +170,7 @@ def _finish(ctx: HackbotContext, outcome: object) -> int: log.error(msg) payload = _error_payload(ctx, msg) exit_code = 1 - payload["agent_version"] = ctx.agent_version + 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 0930e938fe4..ca1581e2dc0 100644 --- a/libs/hackbot-runtime/tests/test_runtime.py +++ b/libs/hackbot-runtime/tests/test_runtime.py @@ -8,7 +8,7 @@ AgentError, HackbotAgentResult, HackbotContext, - context, + config, run_async, runtime, ) @@ -232,7 +232,7 @@ async def main(ctx): def test_run_async_records_agent_version(tmp_path, monkeypatch): _run_env(tmp_path, monkeypatch) versions = {"hackbot-agent-tests": "1.2.3"} - monkeypatch.setattr(context.metadata, "version", versions.__getitem__) + monkeypatch.setattr(config.metadata, "version", versions.__getitem__) async def main(ctx): return HackbotAgentResult(num_turns=1) From 21e60bbfcae4f8f831165afb5bc4a0845e05a56c Mon Sep 17 00:00:00 2001 From: ayoubdiourin7 Date: Thu, 12 Nov 2026 18:23:47 +0100 Subject: [PATCH 6/6] Set agent_name inside _resolve_config --- libs/hackbot-runtime/hackbot_runtime/runtime.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libs/hackbot-runtime/hackbot_runtime/runtime.py b/libs/hackbot-runtime/hackbot_runtime/runtime.py index 6f789f98c0e..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: @@ -131,7 +134,6 @@ def _load_hackbot(entrypoint: Callable, config: ConfigArg) -> HackbotContext | N the entry point's module), falling back to an empty config when there's none. """ parsed = _resolve_config(entrypoint, config) - parsed.agent_name = resolve_agent_name(entrypoint) try: return HackbotContext.from_config_obj(parsed) except ValidationError as exc: