Teach agent builder packed frontends
All checks were successful
build / build (push) Successful in 25s

This commit is contained in:
robert
2026-05-23 15:01:14 -04:00
parent f88fdf80ee
commit 0b9e594bec
10 changed files with 265 additions and 22 deletions

View File

@@ -28,6 +28,9 @@ if TYPE_CHECKING:
from .config import Settings
A2A_PACK_MIN_VERSION = "0.1.19"
@dataclass(frozen=True)
class ToolContext:
bucket: str
@@ -132,17 +135,25 @@ def build_tools(ctx: ToolContext) -> list[Any]:
def init_agent_template(
name: str,
description: str = "A new A2A agent",
frontend: str = "none",
) -> 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.
``agent.py``, ``a2a.yaml``, and ``requirements.txt``. Set
``frontend`` to ``"react"`` for a Vite packed app, ``"static"`` for a
minimal bundled HTML app, or ``"none"`` for a headless agent. 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)
frontend_kind = _normalize_frontend_kind(frontend)
files = _render_a2a_init_template(
name,
description=description,
frontend=frontend_kind,
)
prefix = _agent_prefix(name)
except (OSError, RuntimeError, ValueError) as exc:
return json.dumps({"error": str(exc)})
@@ -156,7 +167,17 @@ def build_tools(ctx: ToolContext) -> list[Any]:
ContentType="text/plain; charset=utf-8",
)
written.append({"path": key, "size": len(content)})
return json.dumps({"ok": True, "agent": name, "files": written})
return json.dumps({
"ok": True,
"agent": name,
"frontend": frontend_kind,
"files": written,
"docs": (
"https://docs.a2acloud.io/concepts/packed-frontends"
if frontend_kind != "none"
else "https://docs.a2acloud.io/quickstart"
),
})
@tool
def list_agent_files(name: str) -> str:
@@ -186,7 +207,7 @@ def build_tools(ctx: ToolContext) -> list[Any]:
"""Write a file under ``agents/<name>/<path>`` in the user's workspace.
Use this for ``agent.py``, ``a2a.yaml``, ``requirements.txt``, and
any auxiliary modules the agent needs.
any auxiliary modules or packed frontend files the agent needs.
"""
try:
prefix = _agent_prefix(name)
@@ -277,7 +298,8 @@ def build_tools(ctx: ToolContext) -> list[Any]:
async def test_agent_in_sandbox(name: str) -> str:
"""Spin a microVM, ``pip install a2a-pack`` + the agent's own
requirements.txt, then run ``a2a card`` to verify the scaffold
produces a valid Card. Returns stdout/stderr.
produces a valid Card. When a packed frontend is declared, it also
prints ``a2a frontend info`` metadata. Returns stdout/stderr.
"""
try:
prefix = _agent_prefix(name)
@@ -294,7 +316,7 @@ def build_tools(ctx: ToolContext) -> list[Any]:
b64 = base64.b64encode(bundle_bytes).decode("ascii")
script = (
"set -e\n"
"pip install --quiet a2a-pack >/dev/null\n"
f"pip install --quiet 'a2a-pack>={A2A_PACK_MIN_VERSION}' >/dev/null\n"
"mkdir -p /tmp/agent\n"
f"echo '{b64}' | base64 -d | tar -xzf - -C /tmp/agent\n"
"cd /tmp/agent\n"
@@ -304,6 +326,10 @@ def build_tools(ctx: ToolContext) -> list[Any]:
"fi\n"
"echo '--- card ---'\n"
"a2a card --project .\n"
"if grep -q '^frontend:' a2a.yaml 2>/dev/null; then\n"
" echo '--- frontend ---'\n"
" a2a frontend info --project . || true\n"
"fi\n"
)
headers = (
{"authorization": f"Bearer {settings.sandbox_token}"}
@@ -911,12 +937,64 @@ def _class_name(slug: str) -> str:
return "".join(p.capitalize() for p in re.split(r"[-_]+", slug) if p) or "MyAgent"
_FRONTEND_TEMPLATE_FILES: dict[str, dict[str, str]] = {
"static": {
"frontend/dist/index.html": "frontend/static-index.html.tmpl",
},
"react": {
"frontend/package.json": "frontend/react-package.json.tmpl",
"frontend/index.html": "frontend/react-index.html.tmpl",
"frontend/vite.config.js": "frontend/react-vite.config.js.tmpl",
"frontend/src/main.jsx": "frontend/react-main.jsx.tmpl",
"frontend/src/App.jsx": "frontend/react-app.jsx.tmpl",
"frontend/src/a2a.js": "frontend/react-a2a.js.tmpl",
"frontend/src/style.css": "frontend/react-style.css.tmpl",
},
}
def _normalize_frontend_kind(frontend: str | None) -> str:
kind = (frontend or "none").strip().lower()
if kind in {"", "no", "off", "false"}:
kind = "none"
if kind not in {"none", "static", "react"}:
raise ValueError("frontend must be one of: none, static, react")
return kind
def _frontend_block(frontend: str) -> str:
kind = _normalize_frontend_kind(frontend)
if kind == "none":
return ""
build = "\n build: npm run build" if kind == "react" else ""
return (
"frontend:\n"
" path: frontend"
f"{build}\n"
" dist: dist\n"
" mount: /app\n"
" auth: inherit"
)
def _render_frontend_files(frontend: str, *, name: str) -> dict[str, str]:
kind = _normalize_frontend_kind(frontend)
if kind == "none":
return {}
return {
path: _render_sdk_template(template, name=name)
for path, template in _FRONTEND_TEMPLATE_FILES[kind].items()
}
def _render_a2a_init_template(
name: str,
*,
description: str = "A new A2A agent",
frontend: str = "none",
) -> dict[str, str]:
_validate_name(name)
frontend_kind = _normalize_frontend_kind(frontend)
class_name = _class_name(name)
files = {
"agent.py": _render_sdk_template(
@@ -929,13 +1007,16 @@ def _render_a2a_init_template(
"a2a.yaml.tmpl",
name=name,
class_name=class_name,
frontend_block=_frontend_block(frontend_kind),
),
"requirements.txt": _render_sdk_template("requirements.txt.tmpl"),
}
files.update(_render_frontend_files(frontend_kind, name=name))
return files
def _render_sdk_template(template: str, /, **vars: str) -> str:
parts = template.split("/")
local_templates = (
Path(__file__).resolve().parents[2]
/ "a2a"
@@ -944,13 +1025,12 @@ def _render_sdk_template(template: str, /, **vars: str) -> str:
/ "templates"
)
if local_templates.exists():
text = local_templates.joinpath(template).read_text(encoding="utf-8")
text = local_templates.joinpath(*parts).read_text(encoding="utf-8")
else:
text = (
resources.files("a2a_pack.cli.templates")
.joinpath(template)
.read_text(encoding="utf-8")
)
resource = resources.files("a2a_pack.cli.templates")
for part in parts:
resource = resource.joinpath(part)
text = resource.read_text(encoding="utf-8")
for key, value in vars.items():
text = text.replace("{{ " + key + " }}", value)
return text