Guidance for coding agents and contributors working in this repository.
agent-env is a Python SDK for building environments that AI agents act in, and for running and
evaluating agents against them. It deploys environments (MCP servers, databases, websites, gateways)
into sandboxes, keeps immutable versioned artifacts, runs tasks as DAGs of steps (deploy an env,
deploy an agent, prompt it, verify the outcome) and records every run. Persistence, sandboxes, the
model endpoint and every extension are selected in one config file; a bare install runs fully local.
The in-repo packages/agentenv-protocol is the open data-plane protocol and server SDK for
environments; agent-env depends on it.
Python 3.11 or newer. The repo convention is a .venv at the root, which the Makefile and CI use.
uv sync --extra dev && . .venv/bin/activateWithout uv:
python3.11 -m venv .venv && . .venv/bin/activate
pip install -e ./packages/agentenv-protocol -e '.[dev]'Both install the in-repo protocol package together with the dev extra: agent-env depends on it and
the workspace copy is the one to develop against (uv sync does this through the uv workspace in
pyproject.toml). make install is the pip route in one step. No cloud credentials are needed; the
default stores are local.
| Command | What it does |
|---|---|
make unit-test |
tst/unit and packages/agentenv-protocol/tests, in parallel, no network |
make int-test-fast |
tst/integration minus int_test_slow, in parallel; needs Docker and a local OCI registry (docker run -d -p 5000:5000 public.ecr.aws/docker/library/registry:2) |
make int-test-slow |
the int_test_slow tests, serially (they build the same Docker tags from module fixtures and race under xdist) |
make installer-test |
tst/installer: plugin add / remove through the real pip, uv and pipx, offline against wheels built from the checkout, and the container user journey (python:3.12-slim, --network none) with the PEP 668 refusal; needs uv, pipx and Docker |
make clean-install-test |
the clean-install CI gate: both distributions built as the release builds them, installed into a fresh venv from public PyPI with an allowlisted environment, and agent-env run hello run twice by name and checked through the local store; needs Python 3.11, uv and git |
pytest tst/unit/env/store_test.py -v |
one file; add --log-cli-level=DEBUG for debug logs |
uv build |
wheel agentenv_framework-<version>-py3-none-any.whl (src/agent_env only) and sdist agentenv_framework-<version>.tar.gz (src, tst, the protocol package) |
agent-env, python -m agent_env.cli |
the CLI; agent-env config show prints which config file is in effect and where each section came from |
- One file:
.agentenv/config.toml, found by walking up from the working directory, or the fileAGENT_ENV_CONFIGnames (it must exist). Exactly one file is read, whole; a section that is absent falls back to the code default, never to another file..agentenv/config.example.tomlis the committed template;.agentenv/config.tomlis git-ignored. - Precedence per section: built-in default < config file <
AGENT_ENV_*environment variable (AGENT_ENV_DOCUMENT_STORE,AGENT_ENV_OBJECT_STORE,AGENT_ENV_IMAGE_STORE,AGENT_ENV_SECRET_STORE,AGENT_ENV_RUNNER) <configure(...)in code. - Defaults are local and carry no external coordinates: SQLite document store, filesystem object
store, an OCI registry at
localhost:5000, env-var secret store,LocalRunner,localDocker sandboxes for envs and agents. MongoDB, S3, Cloud Storage, ECR, AWS Secrets Manager, Google Cloud Secret Manager, Modal and E2B exist as implementations and are selected by config. - The model endpoint is unset until
[model] base_url/api_key(orLITELLM_BASE_URL/LITELLM_API_KEY) is configured. [agents] default_a2a_agent_id: the agent adeploy_agentstep without an id deploys; built-ina2a-default, overridable byconfigure(default_a2a_agent_id=...).- Secrets never live in the file: use
secret:KEYorenv:NAMEreferences, resolved through[stores.secret]. - agent-env has no stage concept: "dev" versus "prod" is which file
AGENT_ENV_CONFIGnames. A platform plugin may pick that file; core must never read a stage variable, take a stage argument or carry a stage attribute (tst/unit/config/test_no_stage_concept.pyenforces this). - Re-pointing
AGENT_ENV_CONFIGafter the first store or registry is built is unsupported: set it before anything resolves, or callreset_config(), which drops the singleton and every registry.
Source lives in src/agent_env/. Every kind of object is identified on the wire by a type string
and deserialized through a registry.
| Package | Contents |
|---|---|
env/ |
Env base class: a subclass declares type, implements from_dict and async deploy(...) -> DeployedEnv, and may override reset and load_file_artifact_universe. Built-ins in env/envs/: MCPServerEnv (mcp_server), MultiEnv (multi, MCP servers plus websites), ServiceDBEnv (service_db, PostgreSQL), WebsiteEnv (website), GatewayEnv (gateway_server). env/gateway/gateway.py is the env-side gateway server, container-only code. |
artifact/ |
Immutable versioned artifacts; every put() creates a new version. The document goes to the document store, binary payloads to the object store. Built-ins: CliArtifact (cli), FileArtifact (file), FileArtifactUniverse (file_artifact_universe), DockerImageArtifact (docker_image), EnvironmentArtifact (environment; legacy service), EnvironmentUniverseArtifact (environment_universe; legacy service_universe), SkillArtifact (skill), VMImageArtifact (vm_image). |
task/, task_step/ |
A Task holds its TaskSteps inline; Task.run() executes them as a DAG. depends_on (None means all prior steps) gates a step, independent steps run concurrently, fail_task_on_error makes a failure fatal or tolerated, retry_config rolls a failed span back through the step journal and re-dispatches it. Built-in steps live in task_step/task_steps/ (deploy_env, deploy_agent, prompt_agent, the verifiers under verifiers/, and more). |
store/ |
Four store ABCs with local and cloud implementations: DocumentStore (SQLite, MongoDB), ObjectStore (filesystem, S3, Cloud Storage), ImageStore (local OCI registry, ECR), SecretStore (env vars or file, AWS Secrets Manager, Google Cloud Secret Manager). VersionedEntityStore implements the shared versioned get/put logic, QueryBuilder is the immutable chained query API, store/base.py holds the error types. A new backend must pass the conformance kits in tst/store/. |
config/ |
The Config singleton (get_config, configure, reset_config) in config/runtime.py, file discovery in config/loader.py, and load_impl, which resolves module:Class pointers. agent_env.store re-exports the config names for compatibility. |
providers/ |
providers/sandbox_providers/ holds the sandbox providers local, modal, modal_vm, e2b, sail_vm (the sail extra); [sandbox] default and agent_default accept a comma-separated fallback chain. providers/env_providers/ holds the environment providers: EnvironmentProvider (an env's containers and state store) and EnvironmentGatewayProvider, which renders a docker-compose for the gateway and its MCP servers inside the sandbox; providers/env_state/ holds env-state providers (local_postgres built in). |
a2a_agent/ |
The A2AAgent entity (a2a_agent), its stores and the validator steps. The protocol package provides the agent-side framework. |
runner/ |
The [runner] seam: Runner.submit() returns (run_id, instance_id); LocalRunner is built in. |
explorer/ |
Optional local web UI: agent-env up, needs the explorer extra, binds loopback :8234. |
cli/ |
Click CLI with the groups a2a-agent, artifact, config, env, eval, plugin, run, task, up. |
examples/ |
The bundles agent-env ships (hello), registered under agent_env.bundles in pyproject.toml. Tests run them, but they are examples for users, not fixtures. |
Everything out-of-tree is declared in config.toml as a module:Class pointer and constructed with
from_config(**config):
[stores.document|object|image|secret],[runner],[sandbox.providers.<name>],[state.providers.<name>],[explorer.plugins]: one implementation each.[envs],[artifacts],[task_steps]withimpls = ["module:Class", ...]: extra classes for the registries. The identity is the class'stype(aClassVaron envs and steps, the Pydantictypedefault on artifacts); a class that keeps the base default, collides with a registered type, or leaves a method its base requires unimplemented fails at registry build withConfigError.- For sandbox and state providers the table name must equal the
typethe provider produces, because that string is persisted and used to reconnect. - Environment providers have no table: an installed package registers one as an
agent_env.env_providersentry point named itstype, which records carry asenv_provider_type. [plugins.<distribution name>]is reserved for an installed plugin's own settings, read withagent_env.plugins.settings. Core reads nothing inside it and only reports it (config show,config explain); every other top-level table is core's, so do not add one for a plugin.- Each section row in
config/describe.pydeclares the keys its reader takes (FileSection.keys), andconfig showwarns about any other. A reader that takes a new key declares it there in the same change. - CLI: an installed package adds top-level groups through the
agent_env.cli_pluginsentry-point group and root options throughagent_env.cli_root_options. Core names win, a plugin that fails to import is skipped with a warning, two different root options on one flag are both left off and reported as a conflict, and grafting subcommands onto built-in groups is unsupported. Core does not add a root option a known plugin uses.pluginis a core group (agent-env plugin list/show/check/add/remove), so a plugin command of that name is skipped.
Do not add a seam that nothing in this repository consumes or defaults: a new config section needs an
in-tree implementation or default, and new behaviour is selected in config.toml, not by a new
environment variable.
Task.run() is the single execution entry point; the in-process LocalRunner and external
orchestrators call it the same way. What callers rely on:
register_task_instance()creates the task-instance record at start; passing an existinginstance_idmakes it an idempotent upsert. After each steprecord_step_complete()writes only the fields that step changed (a path-level diff) and derivescurrent_step/status; failures go throughrecord_task_failure(), retries throughundo_steps().- The runner writes its run identifier to
context.metadata["workflow_id"]before callingrun(); core stores it opaquely with the instance. - Per-run overrides travel in
context.metadata["user_overrides"](litellm_api_key,agent_sandbox,env_state_type,step_params), never inos.environ; steps fall back to the configured[model]key. - Every persisted or transmitted context snapshot passes through
TaskStepContext.to_safe_dict()or the context-ops diff, which strip_REDACTED_KEYS(task_step/context.py);regraft_redacted_keysrestores live values when a context is rebuilt from a stored document. - A run is resumable: pass a previously persisted
context, astart_stepand the existinginstance_id; completed steps are seeded as SUCCESS.
- Tiers are by path.
tst/unit/runs offline:tst/unit/conftest.pyblocks IP sockets with pytest-socket and AWS goes to moto, so anything that needs the network is either missing a mock or belongs intst/integration/. Opt out per test with@pytest.mark.enable_socket, or override the_disable_networkfixture in a closer conftest when a loopback server is the point. tst/integration/needs Docker and the local registry on:5000and runs on the local default backends. Minutes-long tests (real image builds, sandbox VMs, gateways, agents) carry@pytest.mark.int_test_slow(or module-levelpytestmark) and run serially.- Skips are declared capability gaps, decided at collection time: the gates in
tst/util/capabilities.pyproduceskipifmarks with the reasonagentenv-capability-missing: <name>(model_endpoint_configured,remote_sandbox,default_a2a_agent,mcp_server_sources,gnu_stat). CI rejects any other skip reason; a broken configuration must fail loudly, not skip. - The suite is backend-agnostic: a test about one particular backend lives with that backend, not
here. A custom store must pass
tst/store/{conformance,object_conformance,image_conformance,secret_conformance}.py, the same kits the built-ins pass. - Shared helpers live in
tst/util/(config.pyinstalls a config document,a2a_test_agent.pybuilds the echo A2A agent,capabilities.py,image_cache.py); fixtures and mock services intst/data/. Nothing test-only ships undersrc/. tst/conftest.pyresolves the config afresh per module and resets it per test; with no config file the suite runs on the local defaults.
The required checks on pull requests and main are unit, integration-local,
integration-local-slow, installer, plugin-api and clean-install. .github/workflows/local-backends.yml runs
the first four, all installed from public PyPI with no secrets and no external services (a registry:2 service container for the integration jobs). The
unit job also fails if uv.lock resolves anything from a registry other than PyPI or if
agentenv-framework-protocol is not the editable workspace member. The integration jobs run
.github/scripts/check_skip_policy.py over the JUnit report: only agentenv-capability-missing
skips for the allowed capabilities pass, and the fast job allows no skips under
tst/integration/store/. The installer job runs tst/installer when a pull request or push
touches the plugin CLI, agent_env/plugins, the tier itself or the lockfile, and allows no skips; a
changes job decides. .github/workflows/plugin-api.yml runs the plugin-api job on every pull
request, title edits included, and on main: .github/scripts/check_plugin_api.py compares the
plugin surface (its BASES and USED, which a unit test holds to the README "Plugin compatibility"
list) between HEAD^1 and HEAD with griffe, pinned in the dev extra, and fails on a break the
title does not mark with !. .github/workflows/clean-install.yml runs the clean-install job on
every pull request and on main: .github/scripts/clean_install.py builds both distributions as the
release does, fails if the wheel leaves out a file tracked under src/agent_env/examples or a
agent_env.bundles entry point, installs the two wheels into a fresh venv from public PyPI with an
allowlisted environment (no AWS, no config, no plugin), and runs agent-env run hello
twice by name; .github/scripts/check_clean_install.py, run by that venv, checks the local store and
that the runs left no sandbox work folder.
Actions are pinned to commit SHAs.
- PR titles (the squash commit) follow Conventional Commits:
type(scope)!?: imperative summary, with the types in usefeat,fix,refactor,test,docs,ci,chore,perf,build,security;!marks a breaking change, andplugin-apirequires it for a plugin-surface break. - Versions are cut by the maintainers' release automation. Never edit
versioninpyproject.tomlor push a tag in a PR. - Names: the distribution is
agentenv-framework, the import packageagent_env, the console scriptagent-env. Only the first is whatuv add,pip installandimportlib.metadata.version()take. - Comments: default to none. The code says what; a docstring of one to three dense lines says why when that is not obvious. No comment that restates the code, no trailing assertion notes in tests.
- Imports at module top, never inside functions.
- No ticket ids, program codenames or review context in code, docstrings or docs; that belongs in the commit message and the PR.
- No defensive code for a case no caller produces; say so in the PR instead.
- Keep core vendor-neutral: no vendor hostnames, account ids or deployment facts in
src/,tst/or this file. A deployment plugs in through the extension points above.
README.md is the user-facing front door (configuration, custom envs, steps, artifacts and
providers, the CLI). packages/agentenv-protocol/README.md documents the
protocol and the A2A agent framework.