Skip to content
AgentEnv Framework

Useful A2A extensions

The urn:agentenv extensions the framework relies on to provide full functionality

Extensions are what an agent supports beyond plain A2A messages, such as taking environments or handing back what it did. Each one is listed on the agent card, and the framework uses it only when the card lists it, so one task can drive agents that support different things.

The extensions

The SDK defines eight. It implements some of them for you; for the rest, you implement what your agent does:

ExtensionWhat the framework does with itImplemented by
agent-config/v1prompt_agent sends the settings a step sets, such as model and system_prompt, before its prompt; deploy_agent sets name, description, role and system_promptThe SDK, from your config class
mcp-config/v1deploy_agent posts the MCP URL of each env it is givenThe SDK
trajectory/v1prompt_agent reads the trajectory of each task once the agent repliesThe SDK, from the trajectory run() returns
skill-config/v1deploy_agent, add_skills and a2a-agent add-skill install skillsYou install, the SDK lists; see Skills
peer-agents/v1peer_agents sends each agent the other agents it can messageYou
snapshot/v1snapshot_agent_state saves the agent's state as an artifact; deploy_agent loads it into a new instanceYou
triggers/v1register_agent_triggers registers rules; prompt_agent asks the agent which fire on each turn when the agent plays the userThe SDK
install/v1install_agent runs the install commands it declares inside a container a task already runs, instead of deploying the agent's imageYou declare the commands

Declaring one

Declare an extension the SDK implements in extensions on @a2a_agent, as ClaudiusRLMAgent declares MCP_CONFIG_V1 and TRAJECTORY_V1; its config class turns on agent-config by itself. For one you implement, bind a method to each of its operations with @extension, and the card lists it. Bind all of an extension's operations or none of them.

agent-config

agent-config lets you configure an agent, such as its model, effort or system prompt. Every agent takes these settings the same way, and how it applies them is up to the agent. In a task, a prompt_agent step that sets model to claude-sonnet-5-5 moves the agent to Sonnet from its next request.

mcp-config

mcp-config lets you give an agent environments: any MCP server, by URL. In a task, deploy_agent adds each env it names, so claudius sees the email env as the MCP server env4276. Each task hands the agent its servers, and how it uses their tools is up to the agent.

trajectory

trajectory lets you see what an agent did in each task, in the agent's own format. claudius returns its tool calls as claudius/v1 at the end of run(). In a task, prompt_agent fetches the trajectory after each reply and stores it with the run, where a grader such as rubrics_verifier reads it.

skill-config

skill-config lets you install skills on an agent, from a task or the command line. deploy_agent installs them right after the deploy, add_skills later in a task, and agent-env a2a-agent add-skill on any running instance. Skills covers the handlers you write.

peer-agents

peer-agents lets the agents in a task message each other. A peer_agents step tells an agent who its peers are, and you implement what the agent does with them. ClaudiusRLMAgent keeps its peers and gains a method that messages one:

claudius/agent.py (additions)
from urllib.parse import urljoin
from uuid import uuid4

import httpx

from agentenv_protocol.a2a_agent import PEER_AGENTS_V1, PeerAgentsSetRequest, extension


class ClaudiusRLMAgent(AgentEnvAgent):
    def __init__(self) -> None:
        self.peers = {}

    @extension(PEER_AGENTS_V1.set)
    async def set_peers(self, request: PeerAgentsSetRequest):
        self.peers = {peer.name: peer for peer in request.peers}
        return {"status": "updated"}

    @extension(PEER_AGENTS_V1.list)
    async def list_peers(self):
        return {"peers": [peer.model_dump() for peer in self.peers.values()]}

    async def ask_peer(self, name: str, text: str) -> str:
        """Send a peer one A2A message and return its reply."""
        peer = self.peers[name]
        message = {"kind": "message", "messageId": uuid4().hex, "role": "user", "parts": [{"kind": "text", "text": text}]}
        async with httpx.AsyncClient(timeout=600) as client:
            response = await client.post(
                urljoin(peer.url, peer.card.get("url", "/a2a")),
                json={"jsonrpc": "2.0", "id": 1, "method": "message/send", "params": {"message": message}},
            )
        task = response.json()["result"]
        return "".join(part["text"] for part in task["status"]["message"]["parts"] if part["kind"] == "text")

Offer ask_peer to Claude as a tool, and the agent can hand part of a task to a peer. Peering is one way, so for finance to message assistant as well, make it a source too. Peers must reach each other's A2A URLs, so run them on a sandbox such as modal; on local, one agent's container cannot reach another's.

snapshot

snapshot lets you save an agent's conversation and pick it up later in a new instance. Only your agent knows what it holds, so you implement save and load. These additions give ClaudiusRLMAgent conversations to save:

claudius/agent.py (additions)
import io
import json
import tarfile
import tempfile
from pathlib import Path
from uuid import uuid4

from agentenv_protocol.a2a_agent import (
    SNAPSHOT_V1,
    ObjectSnapshotLoadRequest,
    ObjectSnapshotSaveRequest,
    ObjectSnapshotSaveResponse,
    SnapshotUploadedObjects,
    download,
    extension,
    upload,
)

WORKSPACE = Path("/app/workspace")


def archive(directory: Path) -> bytes:
    buffer = io.BytesIO()
    with tarfile.open(fileobj=buffer, mode="w") as tar:
        tar.add(directory, arcname=".")
    return buffer.getvalue()


class ClaudiusRLMAgent(AgentEnvAgent):
    def __init__(self) -> None:
        self.conversations = {}
        WORKSPACE.mkdir(parents=True, exist_ok=True)

    @extension(SNAPSHOT_V1.save)
    async def save_snapshot(self, request: ObjectSnapshotSaveRequest):
        conversation = json.dumps(self.conversations.get(request.context_id, [])).encode()
        objects = {"trajectory": await upload(request.objects.trajectory, conversation)}
        if request.objects.workspace is not None:
            objects["workspace"] = await upload(request.objects.workspace, archive(WORKSPACE))
        return ObjectSnapshotSaveResponse(
            context_id=request.context_id, objects=SnapshotUploadedObjects(**objects)
        )

    @extension(SNAPSHOT_V1.load)
    async def load_snapshot(self, request: ObjectSnapshotLoadRequest):
        with tempfile.TemporaryDirectory() as tmp:
            await download(request.objects.trajectory, Path(tmp, "trajectory"))
            conversation = json.loads(Path(tmp, "trajectory").read_bytes())
            if request.objects.workspace is not None:
                await download(request.objects.workspace, Path(tmp, "workspace"))
                with tarfile.open(Path(tmp, "workspace")) as tar:
                    tar.extractall(WORKSPACE, filter="data")
        context_id = request.target_context_id or uuid4().hex
        self.conversations[context_id] = conversation
        return {"context_id": context_id}

run() then starts each task from self.conversations[request.context_id] and saves the messages back. In a task, snapshot_agent_state saves the snapshot as an artifact, and a later deploy_agent loads it into a new instance. Snapshots move through signed URLs, so they need an object store such as S3.

triggers

triggers lets you script the user in a multi-turn conversation. When a prompt_agent step runs several turns, an agent such as user-sim plays the user, and rules you give it answer for it, such as confirming when the agent asks. Declare TRIGGERS_V1 on that agent, and the SDK evaluates the rules a register_agent_triggers step gives it:

A register_agent_triggers step
{"id": "rules", "type": "register_agent_triggers", "agent_name": "user-sim",
 "triggers": [
   {"id": "confirm", "when": {"type": "conversational", "where": {"message": {"regex": "Send it\\?"}}},
    "actions": [{"type": "say", "text": "Yes, send it."}]},
   {"id": "sent", "when": {"type": "conversational", "where": {"message": {"regex": "Sent"}}},
    "actions": [{"type": "say", "text": "Thanks."}, {"type": "end"}]}]}

A rule's when is one of four kinds:

  • step, a turn number, where cmp is eq or gte
  • conversational, a regex, equals or exists test on the agent's reply
  • env_trigger, the status of a trigger that a register_env_triggers step registered on an env (see Triggers)
  • all or any of other rules

Each rule fires at most once per conversation unless it sets "once": false.

install

install lets a task run an agent inside a container it already runs, instead of in a sandbox of its own. The agent declares the shell commands that install and start it, and install_agent runs them. claudius does not declare it; these additions do:

claudius/agent.py (additions)
from agentenv_protocol.a2a_agent import INSTALL_V1, enable

INSTALL = enable(
    INSTALL_V1,
    install_commands=[
        "docker exec {container} mkdir -p /opt/claudius",
        "docker cp {agent_ctx_tar} {container}:/opt/claudius/claudius.tar.gz",
        "docker exec -w /opt/claudius {container} sh -c 'tar -xzf claudius.tar.gz && python3 -m venv .venv"
        " && .venv/bin/pip install \"agentenv-framework-protocol[agent]\" \"mcp>=1.25,<2\" anthropic'",
        "docker exec -d -w /opt/claudius -e A2A_PORT={a2a_port} \\\n"
        "  -e LITELLM_BASE_URL={litellm_base_url} -e LITELLM_API_KEY={litellm_api_key} {container} .venv/bin/python agent.py",
    ],
    required_params=["container", "agent_ctx_tar", "a2a_port", "litellm_base_url", "litellm_api_key"],
    a2a_port=8000,
)


@a2a_agent(identity=..., config=ClaudiusRLMConfig, extensions=(MCP_CONFIG_V1, TRAJECTORY_V1, INSTALL))
class ClaudiusRLMAgent(AgentEnvAgent):
    ...

The commands reach the container with docker cp and docker exec, and install_agent fills in placeholders such as {container} and {a2a_port}. It also reads the card from an a2a_server.py in the image, holding AGENT_CARD = A2AAgentApplication(ClaudiusRLMAgent()).card.

Your own extensions

For anything else, @custom_extension adds an extension of your own, under a URN outside urn:agentenv:. The card lists it like the others, and your own steps or clients call it.

Last updated on

Ask a question · Report an issue

On this page