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

@@ -66,6 +66,31 @@ expose:
public: true
```
If the agent should ship with a browser UI, scaffold a packed frontend instead
of bolting on an external app. Call ``init_agent_template(..., frontend="react")``
for a Vite/React app that can read the live Agent Card, signed-in session, and
skill schemas from the deployed agent. Use ``frontend="static"`` only for a
simple no-build HTML app. The manifest block is:
```yaml
frontend:
path: frontend
build: npm run build
dist: dist
mount: /app
auth: inherit
```
Packed frontends are served from the same agent at ``/app``. They receive
generated runtime metadata from ``/app/config.json``, can call skills through
the generated endpoints, and can use the browser helper at
``/app/a2a-client.js``. Do not put platform secrets, provider keys, or private
runtime details in frontend source; the frontend should use the agent's public
skill schemas and inherited platform auth. For local development, run the
agent and then run the React dev server with ``A2A_DEV_AGENT_URL`` pointed at
the local agent. See ``https://docs.a2acloud.io/concepts/packed-frontends`` for
the deployment contract.
If the agent needs system binaries the Python-only base image doesn't
ship (ffmpeg, imagemagick, poppler-utils, sqlite3, etc.), declare
them under ``runtime.apt_packages`` and the platform stamps them into
@@ -128,14 +153,18 @@ when you deploy through the control plane).
Your tools:
- init_agent_template(name, description)
- init_agent_template(name, description, frontend="none")
— initialize agents/<name>/ from the
installed a2a-pack `a2a init` template.
Use frontend="react" for a packed app,
frontend="static" for simple bundled HTML,
or frontend="none" for a headless agent.
Use this FIRST for a new project, then
edit the generated files.
- list_agent_files(name) — see what's already at agents/<name>/
- write_agent_file(name, path, content)
— save agent.py / a2a.yaml / requirements.txt
/ frontend/* files
- read_agent_file(name, path) — re-read a file (for iteration)
- write_agent_skill(name, skill_name, description, instructions,
supporting_files_json="{}")
@@ -146,7 +175,8 @@ Your tools:
in agent.py.
- test_agent_in_sandbox(name) — pip install + ``a2a card`` round-trip;
check exit_code == 0 and the card JSON
looks right
looks right. If a frontend is declared,
inspect the printed ``a2a frontend info``.
- cp_deploy_tarball(name, version="0.1.0", public=True, force=False)
— ship it to the platform; returns the
public URL. It refuses stale MinIO
@@ -187,6 +217,12 @@ Discipline:
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``.
- When the user asks for a usable app, dashboard, charting UI, workflow UI,
or demo surface, prefer ``frontend="react"`` and customize
``frontend/src/App.jsx`` plus ``frontend/src/a2a.js`` around the public
``@skill`` schemas. Keep frontend calls routed through
``/app/config.json`` or the generated client/endpoints instead of
hard-coding deployment URLs.
- For non-trivial agents, create one or more DeepAgents skills under
``skills/<skill-name>/`` and wire ``create_deep_agent(..., skills=[...])``.
Do not replace LLM reasoning with fake deterministic tools that return

View File

@@ -23,10 +23,14 @@ class Settings:
def load_settings() -> Settings:
return Settings(
sandbox_url=os.environ.get(
"SANDBOX_URL", "http://sandbox.sandbox.svc.cluster.local:8000"
sandbox_url=(
os.environ.get("A2A_SANDBOX_URL")
or os.environ.get("SANDBOX_URL", "http://sandbox.sandbox.svc.cluster.local:8000")
),
sandbox_timeout_s=int(
os.environ.get("A2A_SANDBOX_TIMEOUT_S")
or os.environ.get("SANDBOX_TIMEOUT_S", "1200")
),
sandbox_timeout_s=int(os.environ.get("SANDBOX_TIMEOUT_S", "1200")),
sandbox_token=os.environ.get("A2A_SANDBOX_TOKEN") or os.environ.get("SANDBOX_TOKEN"),
deploy_wait_timeout_s=int(os.environ.get("DEPLOY_WAIT_TIMEOUT_S", "900")),
litellm_url=os.environ.get(

View File

@@ -154,6 +154,32 @@ runtime:
If it calls external hosts, declare `egress` on the class. If it needs secrets,
use `required_secrets` and `ctx.secret("NAME")`; do not hard-code credentials.
## Packed Frontends
If the user asks for a usable app, dashboard, workflow UI, or customer-facing
demo surface, scaffold a packed frontend instead of a separate web app. Use
`init_agent_template(name, description, frontend="react")` for the React/Vite
starter or `frontend="static"` for a no-build HTML bundle.
The deployment manifest should keep the browser app under the agent source:
```yaml
frontend:
path: frontend
build: npm run build
dist: dist
mount: /app
auth: inherit
```
At runtime the platform serves the app from `/app`, exposes generated config at
`/app/config.json`, exposes a helper client at `/app/a2a-client.js`, and uses
the agent's public `@skill` schemas for callable operations. Frontend code
should load the generated config/client, call the public skills, and rely on
inherited platform auth. Do not put control-plane tokens, provider keys,
secrets, internal stack details, or private execution assumptions in frontend
source.
## A2A Skill vs DeepAgents Skill
Use A2A `@skill` for caller-visible operations and billing boundaries. Use

View File

@@ -16,6 +16,7 @@ Every generated project needs:
- `requirements.txt`
- Optional `skills/<skill-name>/SKILL.md` bundles for nontrivial DeepAgents
behavior
- Optional `frontend/` source when the agent ships a packed app
Run `list_agent_files` and read the files that matter. If the project was
deployed before, use `sync_agent_workspace_from_repo` before editing when the
@@ -66,11 +67,34 @@ runtime:
Use `runtime.apt_packages` only for Debian system binaries. Python packages
belong in `requirements.txt`.
If a packed frontend is declared, verify the manifest and files line up:
```yaml
frontend:
path: frontend
build: npm run build
dist: dist
mount: /app
auth: inherit
```
For React/Vite frontends, expect `frontend/package.json`,
`frontend/vite.config.js`, `frontend/src/App.jsx`, and `frontend/src/a2a.js`.
For static frontends, expect `frontend/dist/index.html`. The browser code must
not contain platform secrets, provider keys, hard-coded deployment URLs, or
private stack details. It should load `/app/config.json`, use
`/app/a2a-client.js` or generated config endpoints, and call only the public
`@skill` schemas.
## Sandbox And Live Card
Always run `test_agent_in_sandbox(name)` before deploy. Treat nonzero exit as
source failure, then read stderr, edit, and rerun.
When a frontend is present, inspect the sandbox output's `a2a frontend info`.
For React apps, make sure `frontend/package.json` defines a build script and
the deployed build process can produce `frontend/dist/index.html`.
After `cp_deploy_tarball`, compare `live_skills[].input_schema` with the
actual public `@skill` signatures. If a live schema is missing an argument or
shows an old shape, refresh or redeploy until the live Agent Card matches the

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