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

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

View File

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

View File

@@ -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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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