Add conversational meta-agent compose tool
All checks were successful
build / build (push) Successful in 15s

This commit is contained in:
robert
2026-06-01 22:47:40 -03:00
parent c2ad8eca4c
commit d665563953
5 changed files with 327 additions and 7 deletions

View File

@@ -1,14 +1,17 @@
from __future__ import annotations
import asyncio
import io
import json
import tarfile
import unittest
from unittest.mock import patch
from agent_builder.config import Settings
from agent_builder.tools import (
_BUILDER_STATE_FILE,
_BUILDER_INTERNAL_PREFIX,
_parse_manifest_json,
_deployment_drift_error,
_files_from_tarball,
_parse_supporting_skill_files,
@@ -235,6 +238,124 @@ class TemplateInitTests(unittest.TestCase):
self.assertIn("agents/website-scraper/a2a.yaml", workspace.objects)
self.assertIn("agents/website-scraper/requirements.txt", workspace.objects)
def test_parse_manifest_json_requires_object_with_composition(self) -> None:
with self.assertRaises(ValueError):
_parse_manifest_json("[]")
with self.assertRaises(ValueError):
_parse_manifest_json('{"goal":"ship"}')
parsed = _parse_manifest_json('{"composition":[{"name":"writer"}]}')
self.assertEqual(parsed["composition"][0]["name"], "writer")
def test_cp_compose_meta_agent_requires_manifest_object(self) -> None:
tools = build_tools(
ToolContext(
bucket="bucket",
settings=_settings(),
workspace=_FakeWorkspace(),
cp_jwt="jwt-user",
)
)
compose = _tool_by_name(tools, "cp_compose_meta_agent")
result = json.loads(
asyncio.run(
compose.ainvoke(
{
"name": "report-meta",
"manifest_json": "[]",
}
)
)
)
self.assertIn("manifest_json must be a JSON object", result["error"])
def test_cp_compose_meta_agent_posts_manifest_and_records_state(self) -> None:
workspace = _FakeWorkspace()
tools = build_tools(
ToolContext(
bucket="bucket",
settings=_settings(deploy_wait_timeout_s=0),
workspace=workspace,
cp_jwt="jwt-user",
)
)
compose = _tool_by_name(tools, "cp_compose_meta_agent")
manifest = {
"composition": {
"sub_agents": [{"name": "writer", "skills": ["draft"]}],
"max_nodes": 3,
},
"goal": {
"objective": "Ship a report.",
"success_criteria": ["draft complete"],
},
"memory": {"tiers": ["files", "kv"], "namespace": "report"},
}
_FakeAsyncClient.posts = []
with patch("agent_builder.tools.httpx.AsyncClient", _FakeAsyncClient):
result = json.loads(
asyncio.run(
compose.ainvoke(
{
"name": "report-meta",
"manifest_json": json.dumps(manifest),
"description": "Report coordinator",
"version": "1.2.3",
"public": True,
}
)
)
)
self.assertFalse(result["ok"])
self.assertEqual(result["name"], "report-meta")
self.assertEqual(result["head_sha"], "source-sha")
self.assertEqual(len(_FakeAsyncClient.posts), 1)
post = _FakeAsyncClient.posts[0]
self.assertEqual(post["url"], "http://cp.test/v1/agents/compose")
self.assertEqual(post["headers"]["authorization"], "bearer jwt-user")
self.assertEqual(post["json"]["name"], "report-meta")
self.assertEqual(post["json"]["manifest"], manifest)
self.assertTrue(post["json"]["public"])
manifest_key = "agents/report-meta/meta_agent_manifest.json"
self.assertIn(manifest_key, workspace.objects)
saved_manifest = json.loads(workspace.objects[manifest_key].decode("utf-8"))
self.assertEqual(saved_manifest["goal"]["objective"], "Ship a report.")
state = json.loads(
workspace.objects[
"agents/report-meta/" + _BUILDER_STATE_FILE
].decode("utf-8")
)
self.assertEqual(state["repo_head_sha"], "source-sha")
self.assertEqual(state["source"], "agent-builder-compose")
def test_cp_compose_meta_agent_requires_cp_jwt(self) -> None:
tools = build_tools(
ToolContext(
bucket="bucket",
settings=_settings(),
workspace=_FakeWorkspace(),
)
)
compose = _tool_by_name(tools, "cp_compose_meta_agent")
result = json.loads(
asyncio.run(
compose.ainvoke(
{
"name": "report-meta",
"manifest_json": '{"composition":[{"name":"writer"}]}',
}
)
)
)
self.assertIn("no CP JWT", result["error"])
def _tarball(files: dict[str, str]) -> bytes:
buf = io.BytesIO()
@@ -315,12 +436,12 @@ def _tool_by_name(tools: list[object], name: str) -> object:
raise AssertionError(f"tool not found: {name}")
def _settings() -> Settings:
def _settings(*, deploy_wait_timeout_s: int = 1) -> Settings:
return Settings(
sandbox_url="http://sandbox.test",
sandbox_timeout_s=1,
sandbox_token=None,
deploy_wait_timeout_s=1,
deploy_wait_timeout_s=deploy_wait_timeout_s,
litellm_url="http://litellm.test",
litellm_key="",
litellm_model="gpt-test",
@@ -330,3 +451,47 @@ def _settings() -> Settings:
minio_secret_key="secret",
image="python:3.11-slim",
)
class _FakeResponse:
def __init__(self, status_code: int, body: dict[str, object]) -> None:
self.status_code = status_code
self._body = body
self.text = json.dumps(body)
def json(self) -> dict[str, object]:
return self._body
class _FakeAsyncClient:
posts: list[dict[str, object]] = []
def __init__(self, *args: object, **kwargs: object) -> None:
pass
async def __aenter__(self) -> "_FakeAsyncClient":
return self
async def __aexit__(self, *args: object) -> None:
return None
async def post(
self,
url: str,
*,
headers: dict[str, str],
json: dict[str, object],
) -> _FakeResponse:
self.posts.append({"url": url, "headers": headers, "json": json})
return _FakeResponse(
201,
{
"name": json["name"],
"version": json["version"],
"status": "building",
"expected_url": "https://report-meta.a2acloud.io",
"deployment_id": "dpl_123",
"head_sha": "source-sha",
"preview": {"skills": ["pursue"]},
},
)