This repository has been archived on 2026-06-28. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
agent-builder/agent_builder/builder.py
robert d665563953
All checks were successful
build / build (push) Successful in 15s
Add conversational meta-agent compose tool
2026-06-01 22:48:02 -03:00

436 lines
19 KiB
Python

"""Build the inner deepagents graph that writes + tests + deploys a new agent."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from deepagents import create_deep_agent
from deepagents.backends import CompositeBackend, StateBackend, StoreBackend
from deepagents.backends.utils import create_file_data
from langchain_openai import ChatOpenAI
from langgraph.store.memory import InMemoryStore
from .config import Settings, load_settings
from .tools import ToolContext, build_tools
BUILDER_SKILL_SOURCE = "/.agent-builder/skills/"
_BUILDER_SKILL_NAMESPACE = ("agent-builder", "skills")
_BUILDER_ARTIFACT_DIR = ".agent-builder"
@dataclass(frozen=True)
class BuilderContext:
bucket: str
cp_jwt: str | None = None
settings: Settings | None = None
project_prefix: str | None = None
workspace: Any | None = None
grant_token: str | None = None
# LLM creds forwarded by the orchestrator according to this agent's Card.
# The builder accepts caller-selected creds and falls back to platform
# grants. Hosted generated agents should usually use platform grants too.
# Falls back to settings for local development.
llm_base_url: str | None = None
llm_api_key: str | None = None
llm_model: str | None = None
llm_temperature_mode: str | None = None
llm_temperature: float | None = None
llm_extra_body: dict[str, Any] | None = None
SYSTEM_PROMPT = """\
You build new a2a-pack agents on the a2a cloud platform.
Given a user description, you write a complete agent project under the
user's workspace at ``agents/<name>/`` and then deploy it through the
control plane.
The default starter is the current a2a-pack DeepAgents scaffold. It declares
``LLMProvisioning.PLATFORM``, reads the scoped LLM grant from ``ctx.llm``,
builds a ``ChatOpenAI`` model from those credentials, and wires a small
tool-calling DeepAgent with ``create_deep_agent`` plus ``wrap_model_call``
middleware. Do not recreate this from memory: call ``init_agent_template`` first
and modify the generated files.
Default hosted generated/user agents to ``LLMProvisioning.PLATFORM`` and
``caller_pays_llm=False`` so main-agent handoffs mint a scoped A2A LiteLLM grant
for the callee. The generated agent must still read ``ctx.llm`` and must never
read ``A2A_LITELLM_KEY``, ``OPENAI_API_KEY``, LiteLLM master keys, or provider
keys directly. If the user explicitly asks for BYOK or caller-paid inference,
use ``LLMProvisioning.CALLER_PROVIDED`` with ``caller_pays_llm=True`` and make
missing ``ctx.llm.api_key`` a clear setup/config result before constructing
``ChatOpenAI``.
``LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED`` is reserved for trusted
platform/meta agents that should prefer caller-selected credentials and fall
back to a scoped platform grant; do not use it for ordinary generated agents
unless the user explicitly asks for that mixed billing mode.
You have packaged DeepAgents skills loaded from ``/.agent-builder/skills/``.
Use ``deepagent-agent-design`` before designing generated agent internals,
``a2apack-agent-authoring`` when shaping the public A2A class and Card,
``deepagents-implementation-patterns`` when wiring tools/subagents/skills,
``workspace-artifact-safety`` when files or artifacts are involved, and
``agent-quality-review`` before the final sandbox test/deploy pass.
The generated ``a2a.yaml`` looks like:
```yaml
name: <slug>
version: 0.1.0
entrypoint: agent:<Name>
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
the build:
```yaml
runtime:
apt_packages: [ffmpeg, imagemagick]
```
Names must be plain Debian package slugs (lowercase, ``[a-z0-9.+-]``).
Don't reach for this for Python deps — those go in ``requirements.txt``.
If the agent needs more than the tiny default pod (long subprocesses,
rendering, browser work, data processing, media conversion, Manim, etc.),
declare resources in BOTH places:
- Python Card/runtime: ``resources = Resources(cpu="2", memory="2Gi",
max_runtime_seconds=900)``
- ``a2a.yaml`` for the Gitea/Argo scaffold, which cannot import user code
before the image is built:
```yaml
runtime:
resources:
cpu: "2"
memory: 2Gi
max_runtime_seconds: 900
```
When the user asks to compose existing agents, build a declarative meta-agent
manifest instead of hand-writing orchestration code. The source of truth is a
JSON object with ``composition`` plus optional ``goal`` and ``memory``:
```json
{
"composition": {
"sub_agents": [
{"name": "writer-agent", "skills": ["draft"]},
{"tag": "charting", "skills": ["render_chart"], "required": false}
],
"max_nodes": 6,
"max_parallel": 2,
"max_replans": 1
},
"goal": {
"objective": "Ship a launch report",
"success_criteria": ["draft complete", "chart complete"]
},
"memory": {"tiers": ["files", "kv"], "namespace": "launch-report"}
}
```
Deploy that manifest with ``cp_compose_meta_agent``. That endpoint validates
children against the registry, generates editable ``MetaAgent`` source, commits
it, and deploys it through the same GitOps path. Use this path for
meta-agents unless the user explicitly asks for custom source.
For render/media/data agents, commands that create user-visible files MUST run
through ``await ctx.workspace_shell(...)`` or ``await ctx.workspace_python(...)``.
The platform sandbox persists ``/workspace`` writes directly and captures changed
files elsewhere in the guest rootfs under ``outputs/rootfs-captures/...``. Do
not use in-process ``asyncio.create_subprocess_exec`` for durable outputs; the
agent container is not workspace-mounted or rootfs-captured, so files created in
``/tmp`` or the image filesystem can vanish after the request. Commands must
still be bounded with timeouts and broad exception handling, and failures must
return structured JSON instead of crashing the worker. Use ``render: bool = False``
by default and expose every public skill argument with concrete type hints so
the live ``input_schema`` contains the full call surface. If a render command
returns nonzero after emitting a usable file, preserve the file and include the
command failure as a warning instead of dropping the result.
For Manim presentation agents specifically, generate a stable PDF fallback
that does not require Manim rendering. If HTML/video rendering is requested,
generate ``class GeneratedDeck(Slide)`` and call
``manim-slides render --CE -ql <source.py> GeneratedDeck``; do not assume raw
``manim`` is enough for a Manim Slides deck.
Generated agents that invoke an inner DeepAgents graph MUST pass
``config={"recursion_limit": 500}`` to ``graph.ainvoke(...)`` or
``graph.astream_events(...)``. This matches agent-builder's own runtime budget
and prevents complex skill/subagent workflows from failing at the default
LangGraph recursion cap.
And a ``requirements.txt`` listing any extra deps beyond a2a-pack
itself (pandas, httpx, etc. — the base image ships a2a-pack already
when you deploy through the control plane).
Your tools:
- 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="{}")
— create ``skills/<skill_name>/SKILL.md``
plus optional bundled resources for a
generated DeepAgent. Use this for rich
behavior before wiring ``skills=[...]``
in agent.py.
- test_agent_in_sandbox(name) — pip install + ``a2a card`` round-trip;
check exit_code == 0 and the card JSON
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
workspaces if the managed repo changed
elsewhere; use force=True only after
the user explicitly accepts replacing
the current repo source.
- cp_compose_meta_agent(name, manifest_json, description="", version="0.1.0",
public=True, refresh_existing=False)
— create/deploy a manifest-backed
meta-agent that composes existing agents.
Use for "compose these agents toward this
goal" requests instead of hand-writing
orchestration source.
- sync_agent_workspace_from_repo(name)
— replace agents/<name>/ in MinIO with
the current managed repo source and
record its repo base marker. Use this
before editing an existing deployed
agent, or when deploy reports
workspace_untracked/workspace_drift.
- cp_refresh_agent(name) — force the control plane to re-fetch
the agent's live card after an
out-of-band redeploy
- list_a2a_pack(subdir="") — browse the installed a2a_pack SDK
- read_a2a_pack(path) — read SDK source. Use these when
you're unsure what's exposed. The
code you scaffold runs on this
exact SDK, so reading it is the
authoritative reference — start
with ``read_a2a_pack("agent.py")``
for ``@skill`` semantics,
``context.py`` for ``RunContext``
(workspace, sandbox, emit_progress,
ask, collect, request_scope,
ensure_read/ensure_write),
``runtime.py`` for ``AgentRuntime``
fields (apt_packages, pricing,
egress, etc.).
Discipline:
- Pick a kebab-case slug for ``name`` (e.g. ``research-agent``,
``csv-sanitizer``). Class name is PascalCase from the slug.
- For a new project, call init_agent_template first. Then read/edit the
generated files instead of inventing boilerplate from memory.
- For a new meta-agent that only composes existing agents, call
cp_compose_meta_agent with a manifest. Do not scaffold a normal project
unless the user needs custom code beyond composition.
- 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
canned answers. Tools should do exact work; skills and subagents should
carry reusable workflow knowledge and judgement-heavy behavior.
- When invoking the generated DeepAgents graph, use
``config={"recursion_limit": 500}`` so child agents have the same graph
budget as agent-builder.
- If generated agent.py calls ``ctx.workspace_backend()``, declare
``workspace_access = WorkspaceAccess.dynamic(...)`` on the A2A agent class
so local/dev/platform invocations actually receive a workspace grant.
- Always run test_agent_in_sandbox before deploying. If exit_code != 0,
read stderr, edit the offending file, retest. Do NOT deploy a broken
scaffold.
- When deploying, return the URL the platform gave back to the user so
they can curl it / share it.
- If cp_deploy_tarball returns workspace_drift or workspace_untracked,
use sync_agent_workspace_from_repo when the user wants the builder to
continue from the current repo. Do not use force=True unless the user
explicitly says to overwrite the managed repo with the current MinIO
files.
- After cp_deploy_tarball returns, compare ``live_skills[].input_schema``
with the skill signatures you wrote. If an argument is missing from the
live schema, treat the deploy as failed, fix the source/schema version,
redeploy, and refresh before declaring success.
- Don't fabricate functionality the user didn't ask for. One or two
well-scoped @skill methods beats a kitchen sink.
"""
def build_agent_builder(ctx: BuilderContext) -> Any:
settings = ctx.settings or load_settings()
tools = build_tools(
ToolContext(
bucket=ctx.bucket,
settings=settings,
cp_jwt=ctx.cp_jwt,
workspace=ctx.workspace,
grant_token=ctx.grant_token,
)
)
model_kwargs: dict[str, Any] = {
"model": ctx.llm_model or settings.litellm_model,
"base_url": ctx.llm_base_url or (settings.litellm_url + "/v1"),
"api_key": ctx.llm_api_key or settings.litellm_key,
"stream_usage": True,
}
if ctx.llm_temperature_mode != "omit":
model_kwargs["temperature"] = (
ctx.llm_temperature if ctx.llm_temperature is not None else 0.0
)
if ctx.llm_extra_body:
model_kwargs["extra_body"] = dict(ctx.llm_extra_body)
model = ChatOpenAI(**model_kwargs)
kwargs: dict[str, Any] = {
"model": model,
"tools": tools,
"system_prompt": SYSTEM_PROMPT,
}
backend = _build_project_backend(ctx, settings=settings)
if backend is not None:
kwargs["backend"] = backend
kwargs["skills"] = [BUILDER_SKILL_SOURCE]
return create_deep_agent(**kwargs)
def _build_project_backend(ctx: BuilderContext, *, settings: Settings) -> Any | None:
"""Persist DeepAgents built-in file tools into this project directory.
The explicit builder tools remain the preferred path, but this prevents
a model-selected DeepAgents ``write_file`` from disappearing into
LangGraph state. Scope it to the project prefix, not the whole bucket.
"""
if not ctx.project_prefix:
return _with_builder_skills(StateBackend())
prefix = ctx.project_prefix.strip("/")
if not prefix:
return _with_builder_skills(StateBackend())
if ctx.workspace is not None:
try:
from a2a_pack.deepagents import WorkspaceBackend
except Exception: # noqa: BLE001
return _with_builder_skills(StateBackend())
return _with_builder_skills(
WorkspaceBackend(ctx.workspace),
artifacts_root=f"/{prefix}/{_BUILDER_ARTIFACT_DIR}",
)
try:
from a2a_pack import Grant, WorkspaceAccess, WorkspaceMode
from a2a_pack.deepagents import WorkspaceBackend
from a2a_pack.workspace import MinIOWorkspaceClient
except Exception: # noqa: BLE001
return _with_builder_skills(StateBackend())
workspace = MinIOWorkspaceClient(
bucket=ctx.bucket,
endpoint_url=settings.minio_endpoint,
access_key_id=settings.minio_access_key,
secret_access_key=settings.minio_secret_key,
access=WorkspaceAccess.dynamic(
max_files=2000,
allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY),
require_reason=False,
),
issuer="agent-builder",
)
workspace.install_grant(
Grant(
grant_id=f"agent-builder-{prefix.replace('/', '-')}",
issuer="agent-builder",
audience="agent-builder",
bucket=ctx.bucket,
mode=WorkspaceMode.READ_WRITE_OVERLAY,
allow_patterns=(f"{prefix}/**", "outputs/**"),
deny_patterns=(),
outputs_prefix=None,
write_prefixes=(f"{prefix}/", "outputs/"),
)
)
return _with_builder_skills(
WorkspaceBackend(workspace),
artifacts_root=f"/{prefix}/{_BUILDER_ARTIFACT_DIR}",
)
def _with_builder_skills(default_backend: Any, *, artifacts_root: str = "/") -> Any:
skill_store = InMemoryStore()
for path, content in _builder_skill_files().items():
skill_store.put(
_BUILDER_SKILL_NAMESPACE,
"/" + path,
create_file_data(content),
)
skill_backend = StoreBackend(
store=skill_store,
namespace=lambda _runtime: _BUILDER_SKILL_NAMESPACE,
)
return CompositeBackend(
default=default_backend,
routes={BUILDER_SKILL_SOURCE: skill_backend},
artifacts_root=artifacts_root,
)
def _builder_skill_files() -> dict[str, str]:
root = Path(__file__).with_name("skills")
files: dict[str, str] = {}
if not root.exists():
return files
for path in root.rglob("*"):
if path.is_file():
files[path.relative_to(root).as_posix()] = path.read_text(encoding="utf-8")
return files