"""Build the inner deepagents graph that writes + tests + deploys a new agent.""" from __future__ import annotations from dataclasses import dataclass from typing import Any from deepagents import create_deep_agent from langchain_openai import ChatOpenAI from .config import Settings, load_settings from .tools import ToolContext, build_tools @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//`` 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. The generated ``a2a.yaml`` looks like: ```yaml name: version: 0.1.0 entrypoint: agent: 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 agents, subprocesses must 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, persist or wrap that artifact 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 GeneratedDeck``; do not assume raw ``manim`` is enough for a Manim Slides deck. 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// 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// - write_agent_file(name, path, content) — save agent.py / a2a.yaml / requirements.txt - read_agent_file(name, path) — re-read a file (for iteration) - 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. - 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``. - 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, stop and report that the builder workspace is stale. 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 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 None prefix = ctx.project_prefix.strip("/") if not prefix: return None 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 None 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 WorkspaceBackend(workspace)