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:
@@ -25,9 +25,13 @@ The single `build(name, prompt)` skill:
|
|||||||
- `write_agent_skill(...)` — creates valid DeepAgents
|
- `write_agent_skill(...)` — creates valid DeepAgents
|
||||||
`skills/<skill-name>/SKILL.md` bundles for generated agents, so rich
|
`skills/<skill-name>/SKILL.md` bundles for generated agents, so rich
|
||||||
behavior lives in progressive-disclosure skills instead of fake tools
|
behavior lives in progressive-disclosure skills instead of fake tools
|
||||||
|
- `init_agent_template(name, description, frontend="react"|"static"|"none")`
|
||||||
|
— starts from the current `a2a-pack` scaffold and can include packed
|
||||||
|
frontend templates under `frontend/` for app-like agents
|
||||||
- `test_agent_in_sandbox(name)` — tarballs the workspace project,
|
- `test_agent_in_sandbox(name)` — tarballs the workspace project,
|
||||||
spins a microVM with the public `a2a-pack` wheel installed, runs
|
spins a microVM with the public `a2a-pack` wheel installed, runs
|
||||||
`a2a card` to verify the scaffold is valid
|
`a2a card` to verify the scaffold is valid, and prints frontend metadata
|
||||||
|
when a packed app is declared
|
||||||
- `cp_deploy_tarball(name, version, public)` — POSTs the tarball
|
- `cp_deploy_tarball(name, version, public)` — POSTs the tarball
|
||||||
to `/v1/agents/from-tarball` on the user's behalf using their
|
to `/v1/agents/from-tarball` on the user's behalf using their
|
||||||
forwarded CP JWT
|
forwarded CP JWT
|
||||||
|
|||||||
@@ -66,6 +66,31 @@ expose:
|
|||||||
public: true
|
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
|
If the agent needs system binaries the Python-only base image doesn't
|
||||||
ship (ffmpeg, imagemagick, poppler-utils, sqlite3, etc.), declare
|
ship (ffmpeg, imagemagick, poppler-utils, sqlite3, etc.), declare
|
||||||
them under ``runtime.apt_packages`` and the platform stamps them into
|
them under ``runtime.apt_packages`` and the platform stamps them into
|
||||||
@@ -128,14 +153,18 @@ when you deploy through the control plane).
|
|||||||
|
|
||||||
Your tools:
|
Your tools:
|
||||||
|
|
||||||
- init_agent_template(name, description)
|
- init_agent_template(name, description, frontend="none")
|
||||||
— initialize agents/<name>/ from the
|
— initialize agents/<name>/ from the
|
||||||
installed a2a-pack `a2a init` template.
|
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
|
Use this FIRST for a new project, then
|
||||||
edit the generated files.
|
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
|
||||||
|
/ frontend/* files
|
||||||
- read_agent_file(name, path) — re-read a file (for iteration)
|
- read_agent_file(name, path) — re-read a file (for iteration)
|
||||||
- write_agent_skill(name, skill_name, description, instructions,
|
- write_agent_skill(name, skill_name, description, instructions,
|
||||||
supporting_files_json="{}")
|
supporting_files_json="{}")
|
||||||
@@ -146,7 +175,8 @@ Your tools:
|
|||||||
in agent.py.
|
in agent.py.
|
||||||
- test_agent_in_sandbox(name) — pip install + ``a2a card`` round-trip;
|
- test_agent_in_sandbox(name) — pip install + ``a2a card`` round-trip;
|
||||||
check exit_code == 0 and the card JSON
|
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)
|
- cp_deploy_tarball(name, version="0.1.0", public=True, force=False)
|
||||||
— ship it to the platform; returns the
|
— ship it to the platform; returns the
|
||||||
public URL. It refuses stale MinIO
|
public URL. It refuses stale MinIO
|
||||||
@@ -187,6 +217,12 @@ Discipline:
|
|||||||
generated files instead of inventing boilerplate from memory.
|
generated files instead of inventing boilerplate from memory.
|
||||||
- Ensure all three core files (agent.py, a2a.yaml, requirements.txt)
|
- Ensure all three core files (agent.py, a2a.yaml, requirements.txt)
|
||||||
exist before testing — partial scaffolds break ``a2a card``.
|
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
|
- For non-trivial agents, create one or more DeepAgents skills under
|
||||||
``skills/<skill-name>/`` and wire ``create_deep_agent(..., skills=[...])``.
|
``skills/<skill-name>/`` and wire ``create_deep_agent(..., skills=[...])``.
|
||||||
Do not replace LLM reasoning with fake deterministic tools that return
|
Do not replace LLM reasoning with fake deterministic tools that return
|
||||||
|
|||||||
@@ -23,10 +23,14 @@ class Settings:
|
|||||||
|
|
||||||
def load_settings() -> Settings:
|
def load_settings() -> Settings:
|
||||||
return Settings(
|
return Settings(
|
||||||
sandbox_url=os.environ.get(
|
sandbox_url=(
|
||||||
"SANDBOX_URL", "http://sandbox.sandbox.svc.cluster.local:8000"
|
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"),
|
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")),
|
deploy_wait_timeout_s=int(os.environ.get("DEPLOY_WAIT_TIMEOUT_S", "900")),
|
||||||
litellm_url=os.environ.get(
|
litellm_url=os.environ.get(
|
||||||
|
|||||||
@@ -154,6 +154,32 @@ runtime:
|
|||||||
If it calls external hosts, declare `egress` on the class. If it needs secrets,
|
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.
|
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
|
## A2A Skill vs DeepAgents Skill
|
||||||
|
|
||||||
Use A2A `@skill` for caller-visible operations and billing boundaries. Use
|
Use A2A `@skill` for caller-visible operations and billing boundaries. Use
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Every generated project needs:
|
|||||||
- `requirements.txt`
|
- `requirements.txt`
|
||||||
- Optional `skills/<skill-name>/SKILL.md` bundles for nontrivial DeepAgents
|
- Optional `skills/<skill-name>/SKILL.md` bundles for nontrivial DeepAgents
|
||||||
behavior
|
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
|
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
|
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
|
Use `runtime.apt_packages` only for Debian system binaries. Python packages
|
||||||
belong in `requirements.txt`.
|
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
|
## Sandbox And Live Card
|
||||||
|
|
||||||
Always run `test_agent_in_sandbox(name)` before deploy. Treat nonzero exit as
|
Always run `test_agent_in_sandbox(name)` before deploy. Treat nonzero exit as
|
||||||
source failure, then read stderr, edit, and rerun.
|
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
|
After `cp_deploy_tarball`, compare `live_skills[].input_schema` with the
|
||||||
actual public `@skill` signatures. If a live schema is missing an argument or
|
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
|
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
|
from .config import Settings
|
||||||
|
|
||||||
|
|
||||||
|
A2A_PACK_MIN_VERSION = "0.1.19"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ToolContext:
|
class ToolContext:
|
||||||
bucket: str
|
bucket: str
|
||||||
@@ -132,17 +135,25 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
|||||||
def init_agent_template(
|
def init_agent_template(
|
||||||
name: str,
|
name: str,
|
||||||
description: str = "A new A2A agent",
|
description: str = "A new A2A agent",
|
||||||
|
frontend: str = "none",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Initialize ``agents/<name>/`` from the installed a2a-pack template.
|
"""Initialize ``agents/<name>/`` from the installed a2a-pack template.
|
||||||
|
|
||||||
Use this FIRST for a new project. It writes the same baseline files
|
Use this FIRST for a new project. It writes the same baseline files
|
||||||
as ``a2a init`` for the SDK version installed in this runtime:
|
as ``a2a init`` for the SDK version installed in this runtime:
|
||||||
``agent.py``, ``a2a.yaml``, and ``requirements.txt``. After this,
|
``agent.py``, ``a2a.yaml``, and ``requirements.txt``. Set
|
||||||
edit those files with ``read_agent_file`` + ``write_agent_file`` to
|
``frontend`` to ``"react"`` for a Vite packed app, ``"static"`` for a
|
||||||
implement the user's requested behavior.
|
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:
|
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)
|
prefix = _agent_prefix(name)
|
||||||
except (OSError, RuntimeError, ValueError) as exc:
|
except (OSError, RuntimeError, ValueError) as exc:
|
||||||
return json.dumps({"error": str(exc)})
|
return json.dumps({"error": str(exc)})
|
||||||
@@ -156,7 +167,17 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
|||||||
ContentType="text/plain; charset=utf-8",
|
ContentType="text/plain; charset=utf-8",
|
||||||
)
|
)
|
||||||
written.append({"path": key, "size": len(content)})
|
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
|
@tool
|
||||||
def list_agent_files(name: str) -> str:
|
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.
|
"""Write a file under ``agents/<name>/<path>`` in the user's workspace.
|
||||||
|
|
||||||
Use this for ``agent.py``, ``a2a.yaml``, ``requirements.txt``, and
|
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:
|
try:
|
||||||
prefix = _agent_prefix(name)
|
prefix = _agent_prefix(name)
|
||||||
@@ -277,7 +298,8 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
|||||||
async def test_agent_in_sandbox(name: str) -> str:
|
async def test_agent_in_sandbox(name: str) -> str:
|
||||||
"""Spin a microVM, ``pip install a2a-pack`` + the agent's own
|
"""Spin a microVM, ``pip install a2a-pack`` + the agent's own
|
||||||
requirements.txt, then run ``a2a card`` to verify the scaffold
|
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:
|
try:
|
||||||
prefix = _agent_prefix(name)
|
prefix = _agent_prefix(name)
|
||||||
@@ -294,7 +316,7 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
|||||||
b64 = base64.b64encode(bundle_bytes).decode("ascii")
|
b64 = base64.b64encode(bundle_bytes).decode("ascii")
|
||||||
script = (
|
script = (
|
||||||
"set -e\n"
|
"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"
|
"mkdir -p /tmp/agent\n"
|
||||||
f"echo '{b64}' | base64 -d | tar -xzf - -C /tmp/agent\n"
|
f"echo '{b64}' | base64 -d | tar -xzf - -C /tmp/agent\n"
|
||||||
"cd /tmp/agent\n"
|
"cd /tmp/agent\n"
|
||||||
@@ -304,6 +326,10 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
|||||||
"fi\n"
|
"fi\n"
|
||||||
"echo '--- card ---'\n"
|
"echo '--- card ---'\n"
|
||||||
"a2a card --project .\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 = (
|
headers = (
|
||||||
{"authorization": f"Bearer {settings.sandbox_token}"}
|
{"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"
|
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(
|
def _render_a2a_init_template(
|
||||||
name: str,
|
name: str,
|
||||||
*,
|
*,
|
||||||
description: str = "A new A2A agent",
|
description: str = "A new A2A agent",
|
||||||
|
frontend: str = "none",
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
_validate_name(name)
|
_validate_name(name)
|
||||||
|
frontend_kind = _normalize_frontend_kind(frontend)
|
||||||
class_name = _class_name(name)
|
class_name = _class_name(name)
|
||||||
files = {
|
files = {
|
||||||
"agent.py": _render_sdk_template(
|
"agent.py": _render_sdk_template(
|
||||||
@@ -929,13 +1007,16 @@ def _render_a2a_init_template(
|
|||||||
"a2a.yaml.tmpl",
|
"a2a.yaml.tmpl",
|
||||||
name=name,
|
name=name,
|
||||||
class_name=class_name,
|
class_name=class_name,
|
||||||
|
frontend_block=_frontend_block(frontend_kind),
|
||||||
),
|
),
|
||||||
"requirements.txt": _render_sdk_template("requirements.txt.tmpl"),
|
"requirements.txt": _render_sdk_template("requirements.txt.tmpl"),
|
||||||
}
|
}
|
||||||
|
files.update(_render_frontend_files(frontend_kind, name=name))
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
|
||||||
def _render_sdk_template(template: str, /, **vars: str) -> str:
|
def _render_sdk_template(template: str, /, **vars: str) -> str:
|
||||||
|
parts = template.split("/")
|
||||||
local_templates = (
|
local_templates = (
|
||||||
Path(__file__).resolve().parents[2]
|
Path(__file__).resolve().parents[2]
|
||||||
/ "a2a"
|
/ "a2a"
|
||||||
@@ -944,13 +1025,12 @@ def _render_sdk_template(template: str, /, **vars: str) -> str:
|
|||||||
/ "templates"
|
/ "templates"
|
||||||
)
|
)
|
||||||
if local_templates.exists():
|
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:
|
else:
|
||||||
text = (
|
resource = resources.files("a2a_pack.cli.templates")
|
||||||
resources.files("a2a_pack.cli.templates")
|
for part in parts:
|
||||||
.joinpath(template)
|
resource = resource.joinpath(part)
|
||||||
.read_text(encoding="utf-8")
|
text = resource.read_text(encoding="utf-8")
|
||||||
)
|
|
||||||
for key, value in vars.items():
|
for key, value in vars.items():
|
||||||
text = text.replace("{{ " + key + " }}", value)
|
text = text.replace("{{ " + key + " }}", value)
|
||||||
return text
|
return text
|
||||||
|
|||||||
@@ -32,8 +32,12 @@ spec:
|
|||||||
value: registry.a2acloud.io/agents/agent-builder:9b70bec52ed3a734ce4c53ed1ec5016e8591de32
|
value: registry.a2acloud.io/agents/agent-builder:9b70bec52ed3a734ce4c53ed1ec5016e8591de32
|
||||||
- name: SANDBOX_URL
|
- name: SANDBOX_URL
|
||||||
value: http://sandbox.sandbox.svc.cluster.local:8000
|
value: http://sandbox.sandbox.svc.cluster.local:8000
|
||||||
|
- name: A2A_SANDBOX_URL
|
||||||
|
value: http://sandbox.sandbox.svc.cluster.local:8000
|
||||||
- name: SANDBOX_TIMEOUT_S
|
- name: SANDBOX_TIMEOUT_S
|
||||||
value: "1200"
|
value: "1200"
|
||||||
|
- name: A2A_SANDBOX_TIMEOUT_S
|
||||||
|
value: "1200"
|
||||||
- name: DEPLOY_WAIT_TIMEOUT_S
|
- name: DEPLOY_WAIT_TIMEOUT_S
|
||||||
value: "900"
|
value: "900"
|
||||||
- name: A2A_PLATFORM_SECRET
|
- name: A2A_PLATFORM_SECRET
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
a2a-pack>=0.1.18
|
a2a-pack>=0.1.19
|
||||||
httpx>=0.27
|
httpx>=0.27
|
||||||
boto3>=1.34
|
boto3>=1.34
|
||||||
deepagents>=0.5.0
|
deepagents>=0.5.0
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from agent_builder.builder import (
|
|||||||
_builder_skill_files,
|
_builder_skill_files,
|
||||||
_with_builder_skills,
|
_with_builder_skills,
|
||||||
)
|
)
|
||||||
|
from agent_builder.tools import A2A_PACK_MIN_VERSION
|
||||||
from deepagents.backends import StateBackend
|
from deepagents.backends import StateBackend
|
||||||
|
|
||||||
|
|
||||||
@@ -28,9 +29,18 @@ class BuilderPromptTests(unittest.TestCase):
|
|||||||
"agent_builder/config.py"
|
"agent_builder/config.py"
|
||||||
).read_text()
|
).read_text()
|
||||||
|
|
||||||
self.assertIn('sandbox_timeout_s=int(os.environ.get("SANDBOX_TIMEOUT_S", "1200"))', config_source)
|
self.assertIn('os.environ.get("A2A_SANDBOX_TIMEOUT_S")', config_source)
|
||||||
|
self.assertIn('os.environ.get("SANDBOX_TIMEOUT_S", "1200")', config_source)
|
||||||
self.assertIn('deploy_wait_timeout_s=int(os.environ.get("DEPLOY_WAIT_TIMEOUT_S", "900"))', config_source)
|
self.assertIn('deploy_wait_timeout_s=int(os.environ.get("DEPLOY_WAIT_TIMEOUT_S", "900"))', config_source)
|
||||||
|
|
||||||
|
def test_builder_requires_frontend_capable_a2a_pack(self) -> None:
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
requirements = root.joinpath("requirements.txt").read_text()
|
||||||
|
tools_source = root.joinpath("agent_builder/tools.py").read_text()
|
||||||
|
|
||||||
|
self.assertIn(f"a2a-pack>={A2A_PACK_MIN_VERSION}", requirements)
|
||||||
|
self.assertIn("a2a-pack>={A2A_PACK_MIN_VERSION}", tools_source)
|
||||||
|
|
||||||
def test_prompt_requires_resource_mirror_for_heavy_agents(self) -> None:
|
def test_prompt_requires_resource_mirror_for_heavy_agents(self) -> None:
|
||||||
self.assertIn("declare resources in BOTH places", SYSTEM_PROMPT)
|
self.assertIn("declare resources in BOTH places", SYSTEM_PROMPT)
|
||||||
self.assertIn("resources = Resources", SYSTEM_PROMPT)
|
self.assertIn("resources = Resources", SYSTEM_PROMPT)
|
||||||
@@ -50,6 +60,15 @@ class BuilderPromptTests(unittest.TestCase):
|
|||||||
self.assertIn("outputs/rootfs-captures/...", SYSTEM_PROMPT)
|
self.assertIn("outputs/rootfs-captures/...", SYSTEM_PROMPT)
|
||||||
self.assertIn("not workspace-mounted or rootfs-captured", SYSTEM_PROMPT)
|
self.assertIn("not workspace-mounted or rootfs-captured", SYSTEM_PROMPT)
|
||||||
|
|
||||||
|
def test_prompt_knows_packed_frontend_contract(self) -> None:
|
||||||
|
self.assertIn('init_agent_template(..., frontend="react")', SYSTEM_PROMPT)
|
||||||
|
self.assertIn("frontend:", SYSTEM_PROMPT)
|
||||||
|
self.assertIn("mount: /app", SYSTEM_PROMPT)
|
||||||
|
self.assertIn("/app/config.json", SYSTEM_PROMPT)
|
||||||
|
self.assertIn("/app/a2a-client.js", SYSTEM_PROMPT)
|
||||||
|
self.assertIn("https://docs.a2acloud.io/concepts/packed-frontends", SYSTEM_PROMPT)
|
||||||
|
self.assertIn("frontend/src/App.jsx", SYSTEM_PROMPT)
|
||||||
|
|
||||||
def test_prompt_and_backend_load_packaged_builder_skills(self) -> None:
|
def test_prompt_and_backend_load_packaged_builder_skills(self) -> None:
|
||||||
expected = {
|
expected = {
|
||||||
"deepagent-agent-design",
|
"deepagent-agent-design",
|
||||||
|
|||||||
@@ -41,12 +41,58 @@ class TemplateInitTests(unittest.TestCase):
|
|||||||
self.assertIn("create_deep_agent", files["agent.py"])
|
self.assertIn("create_deep_agent", files["agent.py"])
|
||||||
self.assertIn("name: research-agent", files["a2a.yaml"])
|
self.assertIn("name: research-agent", files["a2a.yaml"])
|
||||||
self.assertIn("entrypoint: agent:ResearchAgent", files["a2a.yaml"])
|
self.assertIn("entrypoint: agent:ResearchAgent", files["a2a.yaml"])
|
||||||
|
self.assertNotIn("{{ frontend_block }}", files["a2a.yaml"])
|
||||||
|
self.assertNotIn("frontend:", files["a2a.yaml"])
|
||||||
self.assertIn("deepagents>=0.5.0", files["requirements.txt"])
|
self.assertIn("deepagents>=0.5.0", files["requirements.txt"])
|
||||||
|
|
||||||
|
def test_render_a2a_init_template_can_include_react_frontend(self) -> None:
|
||||||
|
files = _render_a2a_init_template(
|
||||||
|
"chart-agent",
|
||||||
|
description="Chart helper",
|
||||||
|
frontend="react",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
{
|
||||||
|
"agent.py",
|
||||||
|
"a2a.yaml",
|
||||||
|
"requirements.txt",
|
||||||
|
"frontend/package.json",
|
||||||
|
"frontend/index.html",
|
||||||
|
"frontend/vite.config.js",
|
||||||
|
"frontend/src/main.jsx",
|
||||||
|
"frontend/src/App.jsx",
|
||||||
|
"frontend/src/a2a.js",
|
||||||
|
"frontend/src/style.css",
|
||||||
|
}.issubset(files)
|
||||||
|
)
|
||||||
|
self.assertIn("frontend:", files["a2a.yaml"])
|
||||||
|
self.assertIn("build: npm run build", files["a2a.yaml"])
|
||||||
|
self.assertIn("mount: /app", files["a2a.yaml"])
|
||||||
|
self.assertIn("auth: inherit", files["a2a.yaml"])
|
||||||
|
self.assertIn("Skill runner", files["frontend/src/App.jsx"])
|
||||||
|
self.assertIn("callSkill", files["frontend/src/App.jsx"])
|
||||||
|
self.assertIn("CONFIG_URL", files["frontend/src/a2a.js"])
|
||||||
|
self.assertIn('"/invoke": agent', files["frontend/vite.config.js"])
|
||||||
|
|
||||||
|
def test_render_a2a_init_template_can_include_static_frontend(self) -> None:
|
||||||
|
files = _render_a2a_init_template("status-agent", frontend="static")
|
||||||
|
|
||||||
|
self.assertIn("frontend/dist/index.html", files)
|
||||||
|
self.assertIn("frontend:", files["a2a.yaml"])
|
||||||
|
self.assertIn("dist: dist", files["a2a.yaml"])
|
||||||
|
self.assertIn("mount: /app", files["a2a.yaml"])
|
||||||
|
self.assertNotIn("build: npm run build", files["a2a.yaml"])
|
||||||
|
self.assertIn("./a2a-client.js", files["frontend/dist/index.html"])
|
||||||
|
|
||||||
def test_render_a2a_init_template_rejects_invalid_names(self) -> None:
|
def test_render_a2a_init_template_rejects_invalid_names(self) -> None:
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
_render_a2a_init_template("Bad Name")
|
_render_a2a_init_template("Bad Name")
|
||||||
|
|
||||||
|
def test_render_a2a_init_template_rejects_invalid_frontend(self) -> None:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
_render_a2a_init_template("demo-agent", frontend="vue")
|
||||||
|
|
||||||
def test_agent_projects_can_diverge_from_template(self) -> None:
|
def test_agent_projects_can_diverge_from_template(self) -> None:
|
||||||
files = _render_a2a_init_template("regex-explainer")
|
files = _render_a2a_init_template("regex-explainer")
|
||||||
files["agent.py"] = "from a2a_pack import A2AAgent\n"
|
files["agent.py"] = "from a2a_pack import A2AAgent\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user