Send Agent DSL from agent-builder deploys
All checks were successful
build / build (push) Successful in 3s

This commit is contained in:
robert
2026-06-04 17:10:18 -03:00
parent be37a2f248
commit 6ab34c7200
2 changed files with 116 additions and 4 deletions

View File

@@ -14,6 +14,7 @@ import io
import json import json
import re import re
import tarfile import tarfile
import tempfile
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from importlib import resources from importlib import resources
@@ -530,6 +531,10 @@ def build_tools(ctx: ToolContext) -> list[Any]:
return json.dumps({"error": f"no files at agents/{name}/"}) return json.dumps({"error": f"no files at agents/{name}/"})
# Read the entrypoint from a2a.yaml so we can pass it verbatim. # Read the entrypoint from a2a.yaml so we can pass it verbatim.
entrypoint = _read_entrypoint(bundle_bytes) or f"agent:{_class_name(name)}" entrypoint = _read_entrypoint(bundle_bytes) or f"agent:{_class_name(name)}"
try:
agent_dsl_json = _compile_agent_dsl_json(bundle_bytes)
except Exception as exc: # noqa: BLE001
return json.dumps({"error": f"agent_dsl compile failed: {exc}"})
# NOTE: the CP from-tarball endpoint names the file field # NOTE: the CP from-tarball endpoint names the file field
# ``source`` (see apps/control-plane/control_plane/routes/agents.py). # ``source`` (see apps/control-plane/control_plane/routes/agents.py).
@@ -538,6 +543,7 @@ def build_tools(ctx: ToolContext) -> list[Any]:
"name": name, "version": version, "name": name, "version": version,
"entrypoint": entrypoint, "public": "true" if public else "false", "entrypoint": entrypoint, "public": "true" if public else "false",
"description": "Built by agent-builder.", "description": "Built by agent-builder.",
"agent_dsl": agent_dsl_json,
} }
if isinstance(base_head, str) and base_head: if isinstance(base_head, str) and base_head:
data["base_head_sha"] = base_head data["base_head_sha"] = base_head
@@ -1146,6 +1152,20 @@ def _files_from_tarball(bundle: bytes) -> dict[str, bytes]:
return out return out
def _compile_agent_dsl_json(bundle: bytes) -> str:
from a2a_pack.cli.main import _compile_project_dsl
files = _files_from_tarball(bundle)
with tempfile.TemporaryDirectory(prefix="agent-builder-dsl-") as tmp:
root = Path(tmp)
for rel, body in files.items():
path = root / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(body)
_cfg, dsl = _compile_project_dsl(root)
return dsl.model_dump_json()
def _replace_workspace_files( def _replace_workspace_files(
store_or_s3: Any, store_or_s3: Any,
bucket_or_prefix: str, bucket_or_prefix: str,

View File

@@ -272,6 +272,89 @@ class TemplateInitTests(unittest.TestCase):
self.assertIn("manifest_json must be a JSON object", result["error"]) self.assertIn("manifest_json must be a JSON object", result["error"])
def test_cp_deploy_tarball_posts_agent_dsl(self) -> None:
workspace = _FakeWorkspace()
prefix = "agents/demo-agent/"
files = {
"agent.py": "\n".join(
[
"from a2a_pack import A2AAgent, NoAuth, RunContext, skill",
"",
"class DemoAgent(A2AAgent[None, NoAuth]):",
' name = "demo-agent"',
' description = "Demo agent"',
' version = "0.1.0"',
" auth_model = NoAuth",
"",
' @skill(description="Echo text")',
" async def echo(self, ctx: RunContext[NoAuth], text: str) -> dict:",
" return {'ok': True, 'text': text}",
"",
]
),
"a2a.yaml": "\n".join(
[
"name: demo-agent",
"version: 0.1.0",
"entrypoint: agent:DemoAgent",
"expose:",
" public: false",
"",
]
),
"requirements.txt": "",
}
for rel, content in files.items():
workspace.write_bytes(prefix + rel, content.encode("utf-8"))
tools = build_tools(
ToolContext(
bucket="bucket",
settings=_settings(deploy_wait_timeout_s=0),
workspace=workspace,
cp_jwt="jwt-user",
)
)
deploy = _tool_by_name(tools, "cp_deploy_tarball")
async def _not_live(*args: object, **kwargs: object):
return False, {}
async def _no_latest(*args: object, **kwargs: object):
return None
_FakeAsyncClient.posts = []
with (
patch("agent_builder.tools.httpx.AsyncClient", _FakeAsyncClient),
patch("agent_builder.tools._wait_for_live_card", _not_live),
patch("agent_builder.tools._latest_cp_deployment", _no_latest),
):
result = json.loads(
asyncio.run(
deploy.ainvoke(
{
"name": "demo-agent",
"version": "0.1.0",
"public": False,
}
)
)
)
self.assertFalse(result["ok"])
self.assertEqual(len(_FakeAsyncClient.posts), 1)
post = _FakeAsyncClient.posts[0]
self.assertEqual(post["url"], "http://cp.test/v1/agents/from-tarball")
self.assertEqual(post["headers"]["authorization"], "bearer jwt-user")
data = post["data"]
self.assertEqual(data["name"], "demo-agent")
self.assertEqual(data["entrypoint"], "agent:DemoAgent")
self.assertEqual(data["public"], "false")
dsl = json.loads(data["agent_dsl"])
self.assertEqual(dsl["name"], "demo-agent")
self.assertEqual(dsl["version"], "0.1.0")
self.assertEqual(dsl["entrypoint"]["module"], "agent")
self.assertEqual(dsl["entrypoint"]["class_name"], "DemoAgent")
def test_cp_compose_meta_agent_posts_manifest_and_records_state(self) -> None: def test_cp_compose_meta_agent_posts_manifest_and_records_state(self) -> None:
workspace = _FakeWorkspace() workspace = _FakeWorkspace()
tools = build_tools( tools = build_tools(
@@ -497,14 +580,23 @@ class _FakeAsyncClient:
url: str, url: str,
*, *,
headers: dict[str, str], headers: dict[str, str],
json: dict[str, object], json: dict[str, object] | None = None,
data: dict[str, object] | None = None,
files: dict[str, object] | None = None,
) -> _FakeResponse: ) -> _FakeResponse:
self.posts.append({"url": url, "headers": headers, "json": json}) self.posts.append({
"url": url,
"headers": headers,
"json": json,
"data": data,
"files": files,
})
payload = json or data or {}
return _FakeResponse( return _FakeResponse(
201, 201,
{ {
"name": json["name"], "name": payload["name"],
"version": json["version"], "version": payload["version"],
"status": "building", "status": "building",
"expected_url": "https://report-meta.a2acloud.io", "expected_url": "https://report-meta.a2acloud.io",
"deployment_id": "dpl_123", "deployment_id": "dpl_123",