Skip to content
AgentEnv Framework

Creating your own environment

Write an environment as an AgentEnvEnvironment, with the tools an agent calls and the data plane the framework uses

You write an environment as a subclass of AgentEnvEnvironment, from the agentenv-framework-protocol package. Decorators on its methods tell the framework what each method is for. This server, the one these pages follow, holds an inbox that an agent can search and a sent folder that records what the agent sends:

email/server.py
import json
from pathlib import Path

from agentenv_protocol import (
    AgentEnvEnvironment,
    DataPart,
    FilePart,
    add_data,
    environment_card,
    get_data,
    reset_data,
    tool,
)


@environment_card(name="email")
class EmailEnv(AgentEnvEnvironment):
    def __init__(self) -> None:
        self.inbox: list[dict] = []
        self.sent: list[dict] = []

    @reset_data
    async def _reset(self) -> None:
        self.inbox.clear()
        self.sent.clear()

    @add_data
    async def _add(self, parts: list) -> None:
        for part in parts:
            if isinstance(part, DataPart):
                self.inbox.extend(part.data.get("inbox", []))
                self.sent.extend(part.data.get("sent", []))
            elif isinstance(part, FilePart):
                seed = json.loads(Path(part.file.uri.removeprefix("file://")).read_text())
                self.inbox.extend(seed.get("inbox", []))
                self.sent.extend(seed.get("sent", []))

    @get_data
    async def _state(self) -> list:
        return [DataPart(data={"inbox": self.inbox, "sent": self.sent})]

    @tool(name="{environment_name}_search")
    async def search(self, query: str) -> dict:
        """Find inbox emails whose subject or body contains the query."""
        q = query.lower()
        return {"emails": [e for e in self.inbox if q in (e["subject"] + " " + e["body"]).lower()]}

    @tool()
    async def send(self, to: str, subject: str, body: str) -> dict:
        """Send an email."""
        self.sent.append({"to": to, "subject": subject, "body": body})
        return {"sent": len(self.sent)}


if __name__ == "__main__":
    EmailEnv().serve()

Tools

The two @tool methods are what the agent can do. Each one becomes an MCP tool: its docstring becomes the tool's description, and its type hints become the tool's input schema. send therefore takes three required strings, to, subject and body.

A tool's name is its method's name, unless you pass one to @tool. search passes {environment_name}_search, and when the server starts, the placeholder becomes the environment's name, so the tool is email_search. send passes none, so its tool is send. The prefix is optional. It exists because of the gateway, a proxy the framework runs, by default, in front of every env instance: when Composing environments puts a contacts server behind the same gateway, prefixes keep the two servers' tools apart.

The data plane

The @reset_data, @add_data and @get_data methods form the data plane: a second endpoint, /agentenv, that the framework uses to control the environment's state. It speaks JSON-RPC, where each request is a JSON object that names a method. Each decorator answers one method: data/reset empties the inbox and the sent folder, data/add loads emails into them, inline or from a file, and data/get reads both back.

The data plane is not an MCP tool, so the agent is never offered it.

EmailEnv is one small environment, and a task often needs more than one. Composing environments writes a second one and combines the two, and Deploying your environment runs EmailEnv in a sandbox and calls its data plane.

Last updated on

Ask a question · Report an issue

On this page