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 d0fa3f3f5f
Some checks failed
build / build (push) Failing after 2s
builder: require durable workspace execution
2026-05-19 11:29:01 -03:00

312 lines
14 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")
@dataclass(frozen=True)
class BuilderContext:
bucket: str
cp_jwt: str | None = None
settings: Settings | None = None
project_prefix: str | None = None
# Optional caller-provided LLM creds (when the outer platform Card
# declares llm_provisioning=caller_provided). Falls back to settings.
llm_base_url: str | None = None
llm_api_key: str | None = None
llm_model: str | 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.CALLER_PROVIDED``, reads caller LLM credentials 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.
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 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
```
For render/media/data agents, commands that create user-visible files MUST run
through ``await ctx.workspace_shell(...)`` or ``await ctx.workspace_python(...)``
and write under ``/workspace/outputs/...``. Do not use in-process
``asyncio.create_subprocess_exec`` for durable outputs; the agent container is
not workspace-mounted, 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 under ``/workspace/outputs/...`` 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)
— initialize agents/<name>/ from the
installed a2a-pack `a2a init` template.
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
- 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
- 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.
- 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),
``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.
- Ensure all three core files (agent.py, a2a.yaml, requirements.txt)
exist before testing — partial scaffolds break ``a2a card``.
- 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,
))
model = ChatOpenAI(
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,
temperature=0.0,
)
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())
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,
)
)
return _with_builder_skills(WorkspaceBackend(workspace))
def _with_builder_skills(default_backend: Any) -> 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},
)
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