feat: initialize agents from a2a template
All checks were successful
build / build (push) Successful in 2s

This commit is contained in:
robert
2026-05-16 17:35:05 -03:00
parent 137847bab1
commit 1bb6044b15
3 changed files with 102 additions and 3 deletions

View File

@@ -84,6 +84,11 @@ when you deploy through the control plane).
Your tools: Your tools:
- init_agent_template(name, description)
— initialize agents/<name>/ 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/<name>/ - list_agent_files(name) — see what's already at agents/<name>/
- write_agent_file(name, path, content) - write_agent_file(name, path, content)
— save agent.py / a2a.yaml / requirements.txt — save agent.py / a2a.yaml / requirements.txt
@@ -116,8 +121,10 @@ Discipline:
- Pick a kebab-case slug for ``name`` (e.g. ``research-agent``, - Pick a kebab-case slug for ``name`` (e.g. ``research-agent``,
``csv-sanitizer``). Class name is PascalCase from the slug. ``csv-sanitizer``). Class name is PascalCase from the slug.
- Write ALL THREE files (agent.py, a2a.yaml, requirements.txt) before - For a new project, call init_agent_template first. Then read/edit the
testing — partial scaffolds break ``a2a card``. 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, - Always run test_agent_in_sandbox before deploying. If exit_code != 0,
read stderr, edit the offending file, retest. Do NOT deploy a broken read stderr, edit the offending file, retest. Do NOT deploy a broken
scaffold. scaffold.

View File

@@ -14,6 +14,7 @@ import re
import tarfile import tarfile
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from importlib import resources
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import boto3 import boto3
@@ -111,6 +112,36 @@ def build_tools(ctx: ToolContext) -> list[Any]:
s3 = _s3(ctx) s3 = _s3(ctx)
_ensure_bucket(s3, bucket) _ensure_bucket(s3, bucket)
@tool
def init_agent_template(
name: str,
description: str = "A new A2A agent",
) -> str:
"""Initialize ``agents/<name>/`` 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 @tool
def list_agent_files(name: str) -> str: def list_agent_files(name: str) -> str:
"""List every file under ``agents/<name>/`` in the user's workspace. """List every file under ``agents/<name>/`` in the user's workspace.
@@ -434,7 +465,7 @@ def build_tools(ctx: ToolContext) -> list[Any]:
}) })
return [ 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, test_agent_in_sandbox, cp_deploy_tarball, cp_refresh_agent,
list_a2a_pack, read_a2a_pack, list_a2a_pack, read_a2a_pack,
] ]
@@ -479,3 +510,37 @@ def _read_entrypoint(bundle: bytes) -> str | None:
def _class_name(slug: str) -> str: def _class_name(slug: str) -> str:
return "".join(p.capitalize() for p in re.split(r"[-_]+", slug) if p) or "MyAgent" 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

View File

@@ -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")