diff --git a/agent_builder/builder.py b/agent_builder/builder.py index f0ef686..2eacce5 100644 --- a/agent_builder/builder.py +++ b/agent_builder/builder.py @@ -84,6 +84,11 @@ when you deploy through the control plane). Your tools: + - init_agent_template(name, description) + — initialize agents// from the + installed a2a-pack `a2a init` template. + Use this FIRST for a new project, then + edit the generated files. - list_agent_files(name) — see what's already at agents// - write_agent_file(name, path, content) — save agent.py / a2a.yaml / requirements.txt @@ -116,8 +121,10 @@ Discipline: - Pick a kebab-case slug for ``name`` (e.g. ``research-agent``, ``csv-sanitizer``). Class name is PascalCase from the slug. - - Write ALL THREE files (agent.py, a2a.yaml, requirements.txt) before - testing — partial scaffolds break ``a2a card``. + - For a new project, call init_agent_template first. Then read/edit the + generated files instead of inventing boilerplate from memory. + - Ensure all three core files (agent.py, a2a.yaml, requirements.txt) + exist before testing — partial scaffolds break ``a2a card``. - Always run test_agent_in_sandbox before deploying. If exit_code != 0, read stderr, edit the offending file, retest. Do NOT deploy a broken scaffold. diff --git a/agent_builder/tools.py b/agent_builder/tools.py index 3ca7e06..327ea84 100644 --- a/agent_builder/tools.py +++ b/agent_builder/tools.py @@ -14,6 +14,7 @@ import re import tarfile import time from dataclasses import dataclass +from importlib import resources from typing import TYPE_CHECKING, Any import boto3 @@ -111,6 +112,36 @@ def build_tools(ctx: ToolContext) -> list[Any]: s3 = _s3(ctx) _ensure_bucket(s3, bucket) + @tool + def init_agent_template( + name: str, + description: str = "A new A2A agent", + ) -> str: + """Initialize ``agents//`` from the installed a2a-pack template. + + Use this FIRST for a new project. It writes the same baseline files + as ``a2a init`` for the SDK version installed in this runtime: + ``agent.py``, ``a2a.yaml``, and ``requirements.txt``. After this, + edit those files with ``read_agent_file`` + ``write_agent_file`` to + implement the user's requested behavior. + """ + try: + files = _render_a2a_init_template(name, description=description) + prefix = _agent_prefix(name) + except (OSError, ValueError) as exc: + return json.dumps({"error": str(exc)}) + written: list[dict[str, Any]] = [] + for path, content in files.items(): + key = prefix + path + s3.put_object( + Bucket=bucket, + Key=key, + Body=content.encode("utf-8"), + ContentType="text/plain; charset=utf-8", + ) + written.append({"path": key, "size": len(content)}) + return json.dumps({"ok": True, "agent": name, "files": written}) + @tool def list_agent_files(name: str) -> str: """List every file under ``agents//`` in the user's workspace. @@ -434,7 +465,7 @@ def build_tools(ctx: ToolContext) -> list[Any]: }) return [ - list_agent_files, write_agent_file, read_agent_file, + init_agent_template, list_agent_files, write_agent_file, read_agent_file, test_agent_in_sandbox, cp_deploy_tarball, cp_refresh_agent, list_a2a_pack, read_a2a_pack, ] @@ -479,3 +510,37 @@ def _read_entrypoint(bundle: bytes) -> str | None: def _class_name(slug: str) -> str: return "".join(p.capitalize() for p in re.split(r"[-_]+", slug) if p) or "MyAgent" + + +def _render_a2a_init_template( + name: str, + *, + description: str = "A new A2A agent", +) -> dict[str, str]: + _validate_name(name) + class_name = _class_name(name) + return { + "agent.py": _render_sdk_template( + "agent.py.tmpl", + name=name, + class_name=class_name, + description=description, + ), + "a2a.yaml": _render_sdk_template( + "a2a.yaml.tmpl", + name=name, + class_name=class_name, + ), + "requirements.txt": _render_sdk_template("requirements.txt.tmpl"), + } + + +def _render_sdk_template(template: str, /, **vars: str) -> str: + text = ( + resources.files("a2a_pack.cli.templates") + .joinpath(template) + .read_text(encoding="utf-8") + ) + for key, value in vars.items(): + text = text.replace("{{ " + key + " }}", value) + return text diff --git a/tests/test_template_init.py b/tests/test_template_init.py new file mode 100644 index 0000000..e29a92b --- /dev/null +++ b/tests/test_template_init.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import unittest + +from agent_builder.tools import _render_a2a_init_template + + +class TemplateInitTests(unittest.TestCase): + def test_render_a2a_init_template_matches_sdk_starter(self) -> None: + files = _render_a2a_init_template( + "research-agent", + description="Research helper", + ) + + self.assertEqual(set(files), {"agent.py", "a2a.yaml", "requirements.txt"}) + self.assertIn('name = "research-agent"', files["agent.py"]) + self.assertIn('description = "Research helper"', files["agent.py"]) + self.assertIn("LLMProvisioning.CALLER_PROVIDED", files["agent.py"]) + self.assertIn("ctx.llm", files["agent.py"]) + self.assertIn("create_deep_agent", files["agent.py"]) + self.assertIn("name: research-agent", files["a2a.yaml"]) + self.assertIn("entrypoint: agent:ResearchAgent", files["a2a.yaml"]) + self.assertIn("deepagents>=0.5.0", files["requirements.txt"]) + + def test_render_a2a_init_template_rejects_invalid_names(self) -> None: + with self.assertRaises(ValueError): + _render_a2a_init_template("Bad Name")