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/skills/a2apack-agent-authoring/SKILL.md
robert 9a90add738
All checks were successful
build / build (push) Successful in 4s
Sanitize generated pricing fields
2026-06-02 14:19:13 -03:00

7.8 KiB

name, description
name description
a2apack-agent-authoring Author production a2a-pack agents with clear public @skill schemas, runtime declarations, platform LLM grants, optional caller-provided LLMs, workspace grants, pricing, resources, and secure platform capabilities. Use when writing or reviewing agent.py for an A2A agent.

A2A Pack Agent Authoring

An A2A agent should expose a small, stable public API and put rich reasoning inside implementation helpers, DeepAgents skills, or subagents. The public @skill methods are the contract shown on the Agent Card.

Public Contract

Use A2AAgent class attributes for runtime and marketplace behavior:

  • name, description, and version identify the agent.
  • config_model and auth_model define agent configuration and caller auth.
  • llm_provisioning tells the platform where LLM credentials come from.
  • pricing tells callers what they pay and whether they supply LLM creds.
  • resources, egress, tools_used, and workspace_access describe runtime requirements.
  • wants_cp_jwt=True is only for trusted platform agents. It forwards the caller's control-plane token.

For hosted generated/user-owned agents that call an LLM, default to LLMProvisioning.PLATFORM with Pricing(..., caller_pays_llm=False, ...) so main-agent handoffs mint a scoped A2A LiteLLM grant for the callee. Use LLMProvisioning.CALLER_PROVIDED only when the user explicitly wants BYOK or caller-paid inference. Reserve LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED for trusted platform/meta agents that must work with either the caller's selected creds or a platform fallback grant. In every mode, read ctx.llm; never read A2A_LITELLM_KEY, OPENAI_API_KEY, provider keys, or platform secrets directly. If ctx.llm.api_key is empty, return a clear setup/config result before constructing ChatOpenAI.

Pricing only accepts price_per_call_usd, caller_pays_llm, and notes. Do not put runtime.pricing.compute, runtime.pricing.total_usd, compute=, or total_usd= in generated agent.py; the platform derives those values from declared Resources and billing policy.

Keep each @skill method async, put RunContext[...] immediately after self, and annotate every public argument. Do not use *args or **kwargs; they are rejected and would not publish a useful schema.

Code Example

from __future__ import annotations

from typing import Any

from pydantic import BaseModel

from a2a_pack import (
    A2AAgent,
    EgressPolicy,
    LLMProvisioning,
    NoAuth,
    Pricing,
    Resources,
    RunContext,
    WorkspaceAccess,
    WorkspaceMode,
    skill,
)


class ResearchConfig(BaseModel):
    default_depth: int = 3


class ResearchAgent(A2AAgent[ResearchConfig, NoAuth]):
    name = "research-agent"
    description = "Researches a topic and writes a concise cited report."
    version = "0.1.0"

    config_model = ResearchConfig
    auth_model = NoAuth

    llm_provisioning = LLMProvisioning.PLATFORM
    pricing = Pricing(
        price_per_call_usd=0.05,
        caller_pays_llm=False,
        notes="Uses a scoped platform LLM grant through ctx.llm.",
    )
    resources = Resources(cpu="1", memory="1Gi", max_runtime_seconds=900)
    egress = EgressPolicy(allow_hosts=("api.openai.com",))
    tools_used = ("deepagents", "langchain")
    workspace_access = WorkspaceAccess.dynamic(
        max_files=128,
        allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY),
        require_reason=False,
    )

    @skill(
        description="Research a topic, save report artifacts, and return a summary",
        timeout_seconds=900,
        cost_class="llm-heavy",
    )
    async def research(
        self,
        ctx: RunContext[NoAuth],
        topic: str,
        depth: int | None = None,
        save_path: str = "outputs/research-report.md",
    ) -> dict[str, Any]:
        creds = ctx.llm
        await ctx.emit_progress(f"researching with {creds.model}")
        if not creds.api_key:
            return {
                "summary": "LLM credentials were not available for this run.",
                "artifact": None,
                "path": save_path,
            }
        graph = self._build_graph(ctx)
        state = await graph.ainvoke(
            {
                "messages": [
                    {
                        "role": "user",
                        "content": (
                            f"Research {topic!r} at depth {depth or self.config.default_depth}. "
                            f"Write the final markdown report to /workspace/{save_path}."
                        ),
                    }
                ]
            },
            config={"recursion_limit": 500},
        )
        report_text = _last_message_text(state)
        ref = await ctx.write_artifact(
            "research-summary.md",
            report_text.encode("utf-8"),
            "text/markdown",
        )
        await ctx.emit_artifact(ref)
        return {"summary": report_text, "artifact": ref.uri, "path": save_path}

Schema Rules

Public arguments become the live A2A input schema. Prefer plain JSON-shaped types: str, int, float, bool, list[str], dict[str, Any], or Pydantic models. Provide defaults for optional knobs. Avoid exposing internal implementation details such as model names, temporary paths, prompt fragments, or unbounded command strings.

Runtime Rules

If a skill calls ctx.workspace_backend() or ctx.workspace, declare workspace_access. If it writes user-visible files, also emit artifacts with ctx.write_artifact and ctx.emit_artifact.

If a skill runs code, renderers, converters, or data tools that create downloadable files, run them through await ctx.workspace_shell(...) or await ctx.workspace_python(...). The sandbox persists /workspace writes directly and mirrors changed files elsewhere in the guest rootfs under outputs/rootfs-captures/.... In-process subprocesses write inside the agent pod, not the caller's mounted or rootfs-captured workspace.

When invoking an inner DeepAgents graph, pass config={"recursion_limit": 500} to graph.ainvoke(...) or graph.astream_events(...). Complex skill/subagent workflows can exceed LangGraph defaults during normal operation.

If an agent needs system binaries, mirror the need in a2a.yaml:

runtime:
  apt_packages: [ffmpeg, poppler-utils]
  resources:
    cpu: "2"
    memory: 2Gi
    max_runtime_seconds: 900

If it calls external hosts, declare egress on the class. If it needs secrets, use required_secrets and ctx.secret("NAME"); do not hard-code credentials.

Packed Frontends

If the user asks for a usable app, dashboard, workflow UI, or customer-facing demo surface, scaffold a packed frontend instead of a separate web app. Use init_agent_template(name, description, frontend="react") for the React/Vite starter or frontend="static" for a no-build HTML bundle.

The deployment manifest should keep the browser app under the agent source:

frontend:
  path: frontend
  build: npm run build
  dist: dist
  mount: /app
  auth: inherit

At runtime the platform serves the app from /app, exposes generated config at /app/config.json, exposes a helper client at /app/a2a-client.js, and uses the agent's public @skill schemas for callable operations. Frontend code should load the generated config/client, call the public skills, and rely on inherited platform auth. Do not put control-plane tokens, provider keys, secrets, internal stack details, or private execution assumptions in frontend source.

A2A Skill vs DeepAgents Skill

Use A2A @skill for caller-visible operations and billing boundaries. Use DeepAgents skills for internal procedural knowledge the LLM should apply. A generated project can have one public @skill that invokes a DeepAgent loaded with several internal skills/<name>/SKILL.md bundles.