Teach agent builder packed frontends
All checks were successful
build / build (push) Successful in 25s
All checks were successful
build / build (push) Successful in 25s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user