diff --git a/agent_builder/tools.py b/agent_builder/tools.py index 03a4ea6..64d732e 100644 --- a/agent_builder/tools.py +++ b/agent_builder/tools.py @@ -213,6 +213,16 @@ def build_tools(ctx: ToolContext) -> list[Any]: return json.dumps({ "error": "no files in agents/" + name + "/ — write some first", }) + try: + _assert_current_agent_project(bundle_bytes) + except RuntimeError as exc: + return json.dumps({ + "error": str(exc), + "hint": ( + "Call init_agent_template first, then edit the generated " + "DeepAgents files instead of replacing agent.py from memory." + ), + }) b64 = base64.b64encode(bundle_bytes).decode("ascii") script = ( "set -e\n" @@ -279,6 +289,16 @@ def build_tools(ctx: ToolContext) -> list[Any]: bundle_bytes = _tarball_workspace_dir(s3, bucket, prefix) if not bundle_bytes: return json.dumps({"error": f"no files at agents/{name}/"}) + try: + _assert_current_agent_project(bundle_bytes) + except RuntimeError as exc: + return json.dumps({ + "error": str(exc), + "hint": ( + "Run init_agent_template again or restore the generated " + "DeepAgents scaffold before deploying." + ), + }) # Read the entrypoint from a2a.yaml so we can pass it verbatim. entrypoint = _read_entrypoint(bundle_bytes) or f"agent:{_class_name(name)}" @@ -508,6 +528,32 @@ def _read_entrypoint(bundle: bytes) -> str | None: return None +def _read_bundle_text(bundle: bytes, path: str) -> str | None: + try: + with tarfile.open(fileobj=io.BytesIO(bundle), mode="r:gz") as tf: + for member in tf.getmembers(): + if member.name == path and member.isfile(): + extracted = tf.extractfile(member) + if extracted is None: + return None + return extracted.read().decode("utf-8", "replace") + except Exception: # noqa: BLE001 + return None + return None + + +def _assert_current_agent_project(bundle: bytes) -> None: + files = { + path: text + for path in ("agent.py", "requirements.txt") + if (text := _read_bundle_text(bundle, path)) is not None + } + _assert_current_deepagents_scaffold( + files, + source="agent project", + ) + + def _class_name(slug: str) -> str: return "".join(p.capitalize() for p in re.split(r"[-_]+", slug) if p) or "MyAgent" @@ -538,6 +584,17 @@ def _render_a2a_init_template( def _assert_current_a2a_init_template(files: dict[str, str]) -> None: + _assert_current_deepagents_scaffold( + files, + source="installed a2a-pack init template", + ) + + +def _assert_current_deepagents_scaffold( + files: dict[str, str], + *, + source: str, +) -> None: required_markers = { "agent.py": ( "LLMProvisioning.CALLER_PROVIDED", @@ -567,7 +624,7 @@ def _assert_current_a2a_init_template(files: dict[str, str]) -> None: except Exception: # noqa: BLE001 version = "unknown" raise RuntimeError( - "installed a2a-pack init template is stale; expected the 0.1.3+ " + f"{source} is stale; expected the 0.1.3+ " "DeepAgents caller-LLM scaffold. " f"a2a_pack={version}; missing markers: {', '.join(missing)}" ) diff --git a/tests/test_template_init.py b/tests/test_template_init.py index 7c901de..417cc9c 100644 --- a/tests/test_template_init.py +++ b/tests/test_template_init.py @@ -1,8 +1,11 @@ from __future__ import annotations +import io +import tarfile import unittest from agent_builder.tools import ( + _assert_current_agent_project, _assert_current_a2a_init_template, _render_a2a_init_template, ) @@ -35,3 +38,23 @@ class TemplateInitTests(unittest.TestCase): "agent.py": "from a2a_pack import A2AAgent", "requirements.txt": "httpx>=0.27", }) + + def test_stale_agent_project_is_rejected(self) -> None: + bundle = _tarball({ + "agent.py": "from a2a_pack import A2AAgent\n", + "requirements.txt": "httpx>=0.27\n", + }) + + with self.assertRaisesRegex(RuntimeError, "agent project is stale"): + _assert_current_agent_project(bundle) + + +def _tarball(files: dict[str, str]) -> bytes: + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w:gz") as tf: + for path, text in files.items(): + body = text.encode("utf-8") + info = tarfile.TarInfo(path) + info.size = len(body) + tf.addfile(info, io.BytesIO(body)) + return buf.getvalue()