From 984d26ecafff573a4f59cc917f2ffe1f6f5f77ec Mon Sep 17 00:00:00 2001 From: Pratyush Singhal Date: Wed, 21 Oct 2026 08:42:49 -0700 Subject: [PATCH 1/8] docs(agents): add an AGENTS.md for writing the shipped agents A short guide in the agents bundle for contributors and coding agents writing or changing an A2A agent: the folder layout, where each pattern lives in the claude-code and pi agents, where the protocol and the control plane's side are defined, the rules that most often break an agent, and the commands to check a change. Co-Authored-By: Claude Opus 5.5 --- src/agent_env/agents/AGENTS.md | 89 ++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/agent_env/agents/AGENTS.md diff --git a/src/agent_env/agents/AGENTS.md b/src/agent_env/agents/AGENTS.md new file mode 100644 index 00000000..6f6213e7 --- /dev/null +++ b/src/agent_env/agents/AGENTS.md @@ -0,0 +1,89 @@ +# AGENTS.md: the shipped agents + +Guidance for writing or changing an A2A agent in this bundle. The repository's `AGENTS.md` still applies; this file +only adds what is specific to agents. `README.md` here is the user-facing description of each agent. + +## Layout + +| Path | What it is | +|---|---| +| `agents//` | one agent: a Docker build context whose `Dockerfile` makes it an agent, deployed as `@local/agentenv-framework/agents/` | +| `agents//a2a_server.py` | the module that builds the app and exports `AGENT_CARD`; `install_agent` imports it from the image | +| `agents//requirements.txt` | exactly one `agentenv-framework-protocol[agent]` line, which the release rewrites to a pin (`tst/unit/test_release_protocol_pins.py`) | +| `tasks/-smoke.json` | deploy, one prompt, one `response_contains` check; `agent-env run agents --task -smoke` | +| `tst/unit/agents//` | the agent's offline tests; `agent.py` there imports its modules from this folder by name | + +The bundle is the `agents` entry in `[project.entry-points."agent_env.bundles"]`, so every file here ships in the +wheel. + +## Where to look + +Every agent is built on the SDK, `agentenv_protocol.a2a_agent`. Copy from the agent closer to your runtime. + +| To see how to | Read | +|---|---| +| declare the agent, its config and extensions | `claude-code/a2a_server.py` (`ClaudeCodeConfig`, the `@a2a_agent` block), `pi/pi_agent.py` (`PiConfig`) | +| run a CLI per turn and stream its events as the trajectory | `ClaudeCodeAgent.run`, `claude_bridge.run_claude` (`event_callback=request.trajectory.append`) | +| return a trajectory once the run ends | `pi/pi_agent.py` `_result` (`native_trajectory`) | +| map the `[model]` endpoint and `LITELLM_EXTRA_HEADERS` onto a CLI | `claude_bridge.cli_env`; pi's `_models_json` for an OpenAI-compatible one | +| hand `request.mcp_servers` to the runtime | `claude_bridge.py` (`--mcp-config`), pi `_mcp_json` (`mcp.json`, headers as `${VAR}`) | +| read file parts | `kit.materialize_file_part` | +| install skills, inline and bundle | `kit.install_skill`, pi `add_inline_skill` / `add_bundle_skill` | +| keep session refs past the SDK's 1,024-context LRU | `kit.DurableSessionRefs` | +| answer errors as `{"detail": ...}` | `kit.json_error_bodies`, applied in `build_app` | +| save and restore a conversation and workspace | `ClaudeCodeAgent.save_snapshot` / `load_snapshot`, pi `save_snapshot` / `load_snapshot` | +| capture and replay a changelog | pi `enable_changelog` / `apply_changelog`, `pi/changelog.py` (`ChangelogCapture`, `apply`) | +| serve a whole context's trajectory | `ClaudeCodeAgent.get_context_trajectory` (`TRAJECTORY_V1.get.context`) | +| replace the SDK's peer tools with your own | pi `set_peers` / `list_peers`, `pi/peers.py` | +| install into a running container or a VM host | `_INSTALL_COMMANDS` / `_INSTALL_COMMANDS_HOST` and `install.sh` (Claude Code), `INSTALL_COMMANDS` and `start.sh` (pi) | +| test an agent offline | `tst/unit/agents/claude_code/` (`conftest.py`, `test_card_contract.py`), `tst/unit/agents/pi/` (`fake_pi.py`) | + +Smaller single-feature agents are in `packages/agentenv-protocol/examples/`. + +## The protocol + +- `packages/agentenv-protocol/src/agentenv_protocol/a2a_agent/extensions.py` defines every `urn:agentenv:*` + extension, its endpoint, operations, request and response models, and which side owns each operation (`SDK` or + `RUNTIME`). It is the source of truth for the wire format; read it before guessing a shape. +- `framework.py` there builds the app: card, routes, executor, default handlers. `tasks/v1.py` is `TaskRequest`, + `TaskResult` and `TrajectoryLog`. `agentenv_protocol/transfers.py` is `upload`, `download` and `NamespaceUploader`. +- `packages/agentenv-protocol/README.md`, section "A2A agent framework", is the prose reference, including live + trajectories, object transfer and staging. +- The control plane's side: `src/agent_env/a2a_agent/` (deploy, config negotiation, object transfer) and the steps + in `src/agent_env/task_step/task_steps/` (`deploy_agent.py`, `prompt_agent.py`, `install_agent.py`). +- The validator: `src/agent_env/a2a_agent/validator.py` and `task_step/task_steps/a2a_agent_validator/`, one step per + check. + +A change to an extension's shape is a protocol change: make it in `packages/agentenv-protocol` with its tests, not by +working around the SDK in an agent. v1 routes are frozen; a new version gets its own path. + +## Rules that bite + +- `prompt_agent` always sends `timeout_seconds` (600 unless the step sets one), and `agent-config` merges each post + into the last: a field one prompt sets stays set until another sets it. +- Declare only the config fields the runtime honours, under the names `prompt_agent` sends (`model`, `system_prompt`, + `effort`, `max_turns`, `max_thinking_tokens`, `output_format`, `timeout_seconds`, `task_id`, ...). Secrets are + `WriteOnly`. +- `skill-config` needs both `add.inline` and `add.bundle`. Overriding an SDK-owned operation means overriding its + whole group. +- A snapshot `save` uploads both the trajectory and the workspace object, an empty workspace if there is none; + `load` runs on a fresh agent, so create state directories at startup. +- An agent that overrides `peer-agents` registers its own MCP server through + `default_handlers.call(MCP_CONFIG_V1.add, ...)` on a `127.0.0.1` URL ending in `/mcp`, as pi does. +- Turns of different contexts run concurrently, and the validator relies on it; don't serialize them. +- `install_agent` imports `AGENT_CARD` with `/app` on `sys.path` and without the entrypoint, and logs only each + install template's first line; keep credentials out of templates (pi writes them through a heredoc). +- The image starts the server from `CMD`, not `ENTRYPOINT`, binds `0.0.0.0:$A2A_PORT`, runs as a non-root user that + tolerates root, and installs everything at build time. +- `TaskResult.builder().failed(...)` adds no text part; add one. Use `infra_error` only when a retry could help. + +## Checking a change + +```bash +pytest tst/unit/agents/ -v # offline, with the runtime faked +agent-env run agents --task -smoke # builds the image; needs Docker and [model] +agent-env a2a-agent validate --id # the full check matrix, against the reference env +``` + +`agent-env a2a-agent validate` needs the reference env `multi-caf64d` in the document store. A new agent also gets a +paragraph in `README.md` here. From f0b5eaef911077d8756fa445e1d43822388554b3 Mon Sep 17 00:00:00 2001 From: Pratyush Singhal Date: Wed, 21 Oct 2026 08:46:35 -0700 Subject: [PATCH 2/8] docs(agents): focus AGENTS.md on writing an A2A agent Drop the guidance on changing the framework itself (release pins, where the shipped agents' tests and README entries go, protocol changes) and keep what someone writing an agent needs: the agent's shape, where each pattern lives in the claude-code and pi agents, the protocol references, the rules that most often break an agent, and how to register and validate it. Co-Authored-By: Claude Opus 5.5 --- src/agent_env/agents/AGENTS.md | 97 ++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/src/agent_env/agents/AGENTS.md b/src/agent_env/agents/AGENTS.md index 6f6213e7..d5a70897 100644 --- a/src/agent_env/agents/AGENTS.md +++ b/src/agent_env/agents/AGENTS.md @@ -1,61 +1,61 @@ -# AGENTS.md: the shipped agents +# AGENTS.md: writing an A2A agent -Guidance for writing or changing an A2A agent in this bundle. The repository's `AGENTS.md` still applies; this file -only adds what is specific to agents. `README.md` here is the user-facing description of each agent. +How to write an agent that agent-env can deploy, prompt, inspect and validate. The agents in this folder, +`agents/claude-code/` and `agents/pi/`, are the reference implementations: copy from the one closer to your runtime. +`README.md` here describes what each of them does. -## Layout +## The shape of an agent -| Path | What it is | -|---|---| -| `agents//` | one agent: a Docker build context whose `Dockerfile` makes it an agent, deployed as `@local/agentenv-framework/agents/` | -| `agents//a2a_server.py` | the module that builds the app and exports `AGENT_CARD`; `install_agent` imports it from the image | -| `agents//requirements.txt` | exactly one `agentenv-framework-protocol[agent]` line, which the release rewrites to a pin (`tst/unit/test_release_protocol_pins.py`) | -| `tasks/-smoke.json` | deploy, one prompt, one `response_contains` check; `agent-env run agents --task -smoke` | -| `tst/unit/agents//` | the agent's offline tests; `agent.py` there imports its modules from this folder by name | +An agent is a Docker image running an A2A server, built with the SDK, `agentenv_protocol.a2a_agent` +(`pip install 'agentenv-framework-protocol[agent]'`). You write one class: a typed config, the extensions it supports, +`run()` for one turn, and handlers for what only your runtime can do. The SDK serves the Agent Card, the JSON-RPC +endpoint and every `/ext/*` route. `pi/a2a_server.py` is the whole entry point: + +```python +from pi_agent import PiAgent -The bundle is the `agents` entry in `[project.entry-points."agent_env.bundles"]`, so every file here ships in the -wheel. +agent = PiAgent() +app = agent.create_app() +AGENT_CARD = app.state.agentenv_a2a.card # install/v1 imports this name -## Where to look +if __name__ == "__main__": + agent.serve() # 0.0.0.0:$A2A_PORT +``` -Every agent is built on the SDK, `agentenv_protocol.a2a_agent`. Copy from the agent closer to your runtime. +## Where each pattern lives -| To see how to | Read | +| To | Read | |---|---| | declare the agent, its config and extensions | `claude-code/a2a_server.py` (`ClaudeCodeConfig`, the `@a2a_agent` block), `pi/pi_agent.py` (`PiConfig`) | | run a CLI per turn and stream its events as the trajectory | `ClaudeCodeAgent.run`, `claude_bridge.run_claude` (`event_callback=request.trajectory.append`) | -| return a trajectory once the run ends | `pi/pi_agent.py` `_result` (`native_trajectory`) | -| map the `[model]` endpoint and `LITELLM_EXTRA_HEADERS` onto a CLI | `claude_bridge.cli_env`; pi's `_models_json` for an OpenAI-compatible one | -| hand `request.mcp_servers` to the runtime | `claude_bridge.py` (`--mcp-config`), pi `_mcp_json` (`mcp.json`, headers as `${VAR}`) | +| return a trajectory once the run ends | pi `_result` (`native_trajectory`) | +| map the model endpoint and `LITELLM_EXTRA_HEADERS` onto a CLI | `claude_bridge.cli_env`; pi `_models_json` for an OpenAI-compatible one | +| hand `request.mcp_servers` to the runtime | `claude_bridge.py` (`--mcp-config`), pi `_mcp_json` (`mcp.json`, header values as `${VAR}`) | | read file parts | `kit.materialize_file_part` | | install skills, inline and bundle | `kit.install_skill`, pi `add_inline_skill` / `add_bundle_skill` | | keep session refs past the SDK's 1,024-context LRU | `kit.DurableSessionRefs` | | answer errors as `{"detail": ...}` | `kit.json_error_bodies`, applied in `build_app` | | save and restore a conversation and workspace | `ClaudeCodeAgent.save_snapshot` / `load_snapshot`, pi `save_snapshot` / `load_snapshot` | -| capture and replay a changelog | pi `enable_changelog` / `apply_changelog`, `pi/changelog.py` (`ChangelogCapture`, `apply`) | +| capture and replay a changelog | pi `enable_changelog` / `apply_changelog`, `pi/changelog.py` | | serve a whole context's trajectory | `ClaudeCodeAgent.get_context_trajectory` (`TRAJECTORY_V1.get.context`) | | replace the SDK's peer tools with your own | pi `set_peers` / `list_peers`, `pi/peers.py` | | install into a running container or a VM host | `_INSTALL_COMMANDS` / `_INSTALL_COMMANDS_HOST` and `install.sh` (Claude Code), `INSTALL_COMMANDS` and `start.sh` (pi) | -| test an agent offline | `tst/unit/agents/claude_code/` (`conftest.py`, `test_card_contract.py`), `tst/unit/agents/pi/` (`fake_pi.py`) | +| build the image | `claude-code/Dockerfile`, `pi/Dockerfile` | +| test the agent offline | `tst/unit/agents/claude_code/` (`conftest.py`, `test_card_contract.py`), `tst/unit/agents/pi/` (`fake_pi.py`) | -Smaller single-feature agents are in `packages/agentenv-protocol/examples/`. +`packages/agentenv-protocol/examples/` has smaller agents, one feature each (`basic_agent.py`, `streaming_agent.py`, +`multimodal_agent.py`, `custom_extensions_agent.py`, `advanced_agent.py`). ## The protocol - `packages/agentenv-protocol/src/agentenv_protocol/a2a_agent/extensions.py` defines every `urn:agentenv:*` - extension, its endpoint, operations, request and response models, and which side owns each operation (`SDK` or - `RUNTIME`). It is the source of truth for the wire format; read it before guessing a shape. -- `framework.py` there builds the app: card, routes, executor, default handlers. `tasks/v1.py` is `TaskRequest`, - `TaskResult` and `TrajectoryLog`. `agentenv_protocol/transfers.py` is `upload`, `download` and `NamespaceUploader`. -- `packages/agentenv-protocol/README.md`, section "A2A agent framework", is the prose reference, including live - trajectories, object transfer and staging. -- The control plane's side: `src/agent_env/a2a_agent/` (deploy, config negotiation, object transfer) and the steps - in `src/agent_env/task_step/task_steps/` (`deploy_agent.py`, `prompt_agent.py`, `install_agent.py`). -- The validator: `src/agent_env/a2a_agent/validator.py` and `task_step/task_steps/a2a_agent_validator/`, one step per - check. - -A change to an extension's shape is a protocol change: make it in `packages/agentenv-protocol` with its tests, not by -working around the SDK in an agent. v1 routes are frozen; a new version gets its own path. + extension: its endpoint, operations, request and response models, and whether the SDK or your runtime implements + each operation. It is the source of truth for the wire format. +- `tasks/v1.py` there is `TaskRequest`, `TaskResult` and `TrajectoryLog`; `agentenv_protocol/transfers.py` is + `upload`, `download` and `NamespaceUploader` for object grants. +- `packages/agentenv-protocol/README.md`, section "A2A agent framework", is the prose reference. +- What agent-env sends you: `src/agent_env/task_step/task_steps/deploy_agent.py` (env vars, MCP registrations, + identity) and `prompt_agent.py` (config fields, then `message/send`). ## Rules that bite @@ -68,22 +68,27 @@ working around the SDK in an agent. v1 routes are frozen; a new version gets its whole group. - A snapshot `save` uploads both the trajectory and the workspace object, an empty workspace if there is none; `load` runs on a fresh agent, so create state directories at startup. -- An agent that overrides `peer-agents` registers its own MCP server through - `default_handlers.call(MCP_CONFIG_V1.add, ...)` on a `127.0.0.1` URL ending in `/mcp`, as pi does. +- The SDK serves the peer tools from protocol 0.1.350. An agent that overrides `peer-agents` registers its own MCP + server through `default_handlers.call(MCP_CONFIG_V1.add, ...)` on a `127.0.0.1` URL ending in `/mcp`, as pi does. - Turns of different contexts run concurrently, and the validator relies on it; don't serialize them. -- `install_agent` imports `AGENT_CARD` with `/app` on `sys.path` and without the entrypoint, and logs only each +- `install_agent` imports `AGENT_CARD` with `/app` on `sys.path` and without your entrypoint, and logs only each install template's first line; keep credentials out of templates (pi writes them through a heredoc). -- The image starts the server from `CMD`, not `ENTRYPOINT`, binds `0.0.0.0:$A2A_PORT`, runs as a non-root user that - tolerates root, and installs everything at build time. +- Start the server from `CMD`, not `ENTRYPOINT`, on `0.0.0.0:$A2A_PORT`, as a non-root user that tolerates root, with + everything installed at build time. - `TaskResult.builder().failed(...)` adds no text part; add one. Use `infra_error` only when a retry could help. -## Checking a change +## Registering and validating + +In a bundle, a folder `agents//` with a Dockerfile is an agent, with optional defaults in `agent.toml` +(`[default_env_vars]`, `[metadata] default_model`, `min_disk_size_gb`). Give it a smoke task like +`tasks/claude-code-smoke.json`. ```bash -pytest tst/unit/agents/ -v # offline, with the runtime faked -agent-env run agents --task -smoke # builds the image; needs Docker and [model] -agent-env a2a-agent validate --id # the full check matrix, against the reference env +agent-env run ./my-bundle # build, register under @local, run its tasks +agent-env a2a-agent put --id my-agent --dockerfile path/to/Dockerfile +agent-env a2a-agent validate --id my-agent # every extension the card advertises +agent-env a2a-agent get --id my-agent # the validation results ``` -`agent-env a2a-agent validate` needs the reference env `multi-caf64d` in the document store. A new agent also gets a -paragraph in `README.md` here. +`validate` runs against the reference env `multi-caf64d`, which must exist in the document store. The checks are in +`src/agent_env/a2a_agent/validator.py` and `src/agent_env/task_step/task_steps/a2a_agent_validator/`, one step each. From 1a4a45311012b7acaec50e59fa0ad9fa615935aa Mon Sep 17 00:00:00 2001 From: Pratyush Singhal Date: Wed, 21 Oct 2026 09:28:50 -0700 Subject: [PATCH 3/8] docs(agents): base AGENTS.md on the Claude Code agent and address greptile review findings - Point every pattern at the Claude Code agent, the default and most tested one, and use pi only for what Claude Code doesn't do: native trajectory, the snapshot changelog and custom peer tools. - Prefix the agent paths with agents/ so they resolve from this folder. - Say that validate runs a fixed set of checks, and list what it doesn't cover, so a green run isn't read as proof for every advertised extension. Co-Authored-By: Claude Opus 5.5 --- src/agent_env/agents/AGENTS.md | 65 +++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/src/agent_env/agents/AGENTS.md b/src/agent_env/agents/AGENTS.md index d5a70897..5a6b749a 100644 --- a/src/agent_env/agents/AGENTS.md +++ b/src/agent_env/agents/AGENTS.md @@ -1,47 +1,51 @@ # AGENTS.md: writing an A2A agent -How to write an agent that agent-env can deploy, prompt, inspect and validate. The agents in this folder, -`agents/claude-code/` and `agents/pi/`, are the reference implementations: copy from the one closer to your runtime. -`README.md` here describes what each of them does. +How to write an agent that agent-env can deploy, prompt, inspect and validate. The reference is the Claude Code agent, +`agents/claude-code/`: agent-env's default agent and its most tested one. `agents/pi/` covers the few patterns Claude +Code doesn't use. `README.md` here describes what each of them does. ## The shape of an agent An agent is a Docker image running an A2A server, built with the SDK, `agentenv_protocol.a2a_agent` (`pip install 'agentenv-framework-protocol[agent]'`). You write one class: a typed config, the extensions it supports, `run()` for one turn, and handlers for what only your runtime can do. The SDK serves the Agent Card, the JSON-RPC -endpoint and every `/ext/*` route. `pi/a2a_server.py` is the whole entry point: +endpoint and every `/ext/*` route. The end of `agents/claude-code/a2a_server.py` is the whole entry point: ```python -from pi_agent import PiAgent +def build_app(agent: ClaudeCodeAgent) -> Starlette: + return json_error_bodies(agent.create_app()) -agent = PiAgent() -app = agent.create_app() -AGENT_CARD = app.state.agentenv_a2a.card # install/v1 imports this name -if __name__ == "__main__": - agent.serve() # 0.0.0.0:$A2A_PORT +app = build_app(ClaudeCodeAgent()) +AGENT_CARD = app.state.agentenv_a2a.card # install/v1 imports this name ``` +and `agents/claude-code/entrypoint.sh` serves it with +`exec uvicorn a2a_server:app --host 0.0.0.0 --port "${A2A_PORT:-8000}"`. + ## Where each pattern lives +Paths are under `agents/claude-code/` unless they name `agents/pi/`. + | To | Read | |---|---| -| declare the agent, its config and extensions | `claude-code/a2a_server.py` (`ClaudeCodeConfig`, the `@a2a_agent` block), `pi/pi_agent.py` (`PiConfig`) | +| declare the agent, its config and extensions | `a2a_server.py`: `ClaudeCodeConfig` and the `@a2a_agent` block | | run a CLI per turn and stream its events as the trajectory | `ClaudeCodeAgent.run`, `claude_bridge.run_claude` (`event_callback=request.trajectory.append`) | -| return a trajectory once the run ends | pi `_result` (`native_trajectory`) | -| map the model endpoint and `LITELLM_EXTRA_HEADERS` onto a CLI | `claude_bridge.cli_env`; pi `_models_json` for an OpenAI-compatible one | -| hand `request.mcp_servers` to the runtime | `claude_bridge.py` (`--mcp-config`), pi `_mcp_json` (`mcp.json`, header values as `${VAR}`) | +| map the model endpoint and `LITELLM_EXTRA_HEADERS` onto a CLI | `claude_bridge.cli_env` | +| hand `request.mcp_servers` to the runtime | `claude_bridge.py`, where it builds `--mcp-config` | | read file parts | `kit.materialize_file_part` | -| install skills, inline and bundle | `kit.install_skill`, pi `add_inline_skill` / `add_bundle_skill` | +| install skills, inline and bundle | `kit.install_skill`, bound in `ClaudeCodeAgent.add_inline_skill` / `add_bundle_skill` | | keep session refs past the SDK's 1,024-context LRU | `kit.DurableSessionRefs` | | answer errors as `{"detail": ...}` | `kit.json_error_bodies`, applied in `build_app` | -| save and restore a conversation and workspace | `ClaudeCodeAgent.save_snapshot` / `load_snapshot`, pi `save_snapshot` / `load_snapshot` | -| capture and replay a changelog | pi `enable_changelog` / `apply_changelog`, `pi/changelog.py` | +| save and restore a conversation and workspace | `ClaudeCodeAgent.save_snapshot` / `load_snapshot` | | serve a whole context's trajectory | `ClaudeCodeAgent.get_context_trajectory` (`TRAJECTORY_V1.get.context`) | -| replace the SDK's peer tools with your own | pi `set_peers` / `list_peers`, `pi/peers.py` | -| install into a running container or a VM host | `_INSTALL_COMMANDS` / `_INSTALL_COMMANDS_HOST` and `install.sh` (Claude Code), `INSTALL_COMMANDS` and `start.sh` (pi) | -| build the image | `claude-code/Dockerfile`, `pi/Dockerfile` | -| test the agent offline | `tst/unit/agents/claude_code/` (`conftest.py`, `test_card_contract.py`), `tst/unit/agents/pi/` (`fake_pi.py`) | +| delegate to peer agents | the `enable(PEER_AGENTS_V1, ...)` line: the SDK serves the peer tools | +| install into a running container or onto a VM host | `_INSTALL_COMMANDS`, `_INSTALL_COMMANDS_HOST` and `install.sh` | +| build the image | `Dockerfile` | +| test the agent offline | `tst/unit/agents/claude_code/` (`conftest.py`, `test_card_contract.py`, `test_extensions.py`) | +| return a trajectory once the run ends, not live | `agents/pi/pi_agent.py` `_result` (`native_trajectory`) | +| capture and replay a snapshot changelog | `agents/pi/pi_agent.py` `enable_changelog` / `apply_changelog`, `agents/pi/changelog.py` | +| replace the SDK's peer tools with your own | `agents/pi/pi_agent.py` `set_peers` / `list_peers`, `agents/pi/peers.py` | `packages/agentenv-protocol/examples/` has smaller agents, one feature each (`basic_agent.py`, `streaming_agent.py`, `multimodal_agent.py`, `custom_extensions_agent.py`, `advanced_agent.py`). @@ -68,13 +72,15 @@ if __name__ == "__main__": whole group. - A snapshot `save` uploads both the trajectory and the workspace object, an empty workspace if there is none; `load` runs on a fresh agent, so create state directories at startup. +- Bind the session before the runtime starts, as `ClaudeCodeAgent.run` does, so a live trajectory read or a snapshot + taken mid-turn finds it. - The SDK serves the peer tools from protocol 0.1.350. An agent that overrides `peer-agents` registers its own MCP - server through `default_handlers.call(MCP_CONFIG_V1.add, ...)` on a `127.0.0.1` URL ending in `/mcp`, as pi does. + server through `default_handlers.call(MCP_CONFIG_V1.add, ...)` on a `127.0.0.1` URL ending in `/mcp`. - Turns of different contexts run concurrently, and the validator relies on it; don't serialize them. -- `install_agent` imports `AGENT_CARD` with `/app` on `sys.path` and without your entrypoint, and logs only each - install template's first line; keep credentials out of templates (pi writes them through a heredoc). +- `install_agent` imports `AGENT_CARD` with `/app` on `sys.path` and without your entrypoint. Pass the endpoint as + environment on the command that needs it, as `_ENDPOINT` does, not as arguments. - Start the server from `CMD`, not `ENTRYPOINT`, on `0.0.0.0:$A2A_PORT`, as a non-root user that tolerates root, with - everything installed at build time. + everything installed at build time (`Dockerfile`, `entrypoint.sh`). - `TaskResult.builder().failed(...)` adds no text part; add one. Use `infra_error` only when a retry could help. ## Registering and validating @@ -86,9 +92,12 @@ In a bundle, a folder `agents//` with a Dockerfile is an agent, with optio ```bash agent-env run ./my-bundle # build, register under @local, run its tasks agent-env a2a-agent put --id my-agent --dockerfile path/to/Dockerfile -agent-env a2a-agent validate --id my-agent # every extension the card advertises +agent-env a2a-agent validate --id my-agent # agent-env's built-in checks agent-env a2a-agent get --id my-agent # the validation results ``` -`validate` runs against the reference env `multi-caf64d`, which must exist in the document store. The checks are in -`src/agent_env/a2a_agent/validator.py` and `src/agent_env/task_step/task_steps/a2a_agent_validator/`, one step each. +`validate` runs a fixed set of checks against the reference env `multi-caf64d`, which must exist in the document +store; the checks are in `src/agent_env/a2a_agent/validator.py` and +`src/agent_env/task_step/task_steps/a2a_agent_validator/`, one step each. It doesn't cover everything an agent can +advertise: `triggers/v1`, `attribution-probe/v1`, host-mode `install/v1` and custom extensions need tests of your +own. From 2f46f9d7f0795682b51d5d7a5cdecdcb77e0fa29 Mon Sep 17 00:00:00 2001 From: Pratyush Singhal Date: Wed, 21 Oct 2026 09:41:57 -0700 Subject: [PATCH 4/8] docs(agents): document each extension in AGENTS.md with its reference Add a section per urn:agentenv:* extension (agent-config, mcp-config, trajectory, skill-config, snapshot, peer-agents, triggers, attribution-probe, install, staging) and custom extensions: what agent-env uses it for, what the SDK implements and what the agent does, and where to copy it from, in the Claude Code agent first, then pi, the protocol examples or its tests. The extension rules move into their sections, and kit.py is named explicitly. Co-Authored-By: Claude Opus 5.5 --- src/agent_env/agents/AGENTS.md | 154 +++++++++++++++++++++++++-------- 1 file changed, 116 insertions(+), 38 deletions(-) diff --git a/src/agent_env/agents/AGENTS.md b/src/agent_env/agents/AGENTS.md index 5a6b749a..52138285 100644 --- a/src/agent_env/agents/AGENTS.md +++ b/src/agent_env/agents/AGENTS.md @@ -25,62 +25,140 @@ and `agents/claude-code/entrypoint.sh` serves it with ## Where each pattern lives -Paths are under `agents/claude-code/` unless they name `agents/pi/`. +Paths are under `agents/claude-code/`. `kit.py` there holds the plumbing any CLI agent needs; copy it rather than +rewriting it. | To | Read | |---|---| -| declare the agent, its config and extensions | `a2a_server.py`: `ClaudeCodeConfig` and the `@a2a_agent` block | -| run a CLI per turn and stream its events as the trajectory | `ClaudeCodeAgent.run`, `claude_bridge.run_claude` (`event_callback=request.trajectory.append`) | -| map the model endpoint and `LITELLM_EXTRA_HEADERS` onto a CLI | `claude_bridge.cli_env` | -| hand `request.mcp_servers` to the runtime | `claude_bridge.py`, where it builds `--mcp-config` | -| read file parts | `kit.materialize_file_part` | -| install skills, inline and bundle | `kit.install_skill`, bound in `ClaudeCodeAgent.add_inline_skill` / `add_bundle_skill` | -| keep session refs past the SDK's 1,024-context LRU | `kit.DurableSessionRefs` | -| answer errors as `{"detail": ...}` | `kit.json_error_bodies`, applied in `build_app` | -| save and restore a conversation and workspace | `ClaudeCodeAgent.save_snapshot` / `load_snapshot` | -| serve a whole context's trajectory | `ClaudeCodeAgent.get_context_trajectory` (`TRAJECTORY_V1.get.context`) | -| delegate to peer agents | the `enable(PEER_AGENTS_V1, ...)` line: the SDK serves the peer tools | -| install into a running container or onto a VM host | `_INSTALL_COMMANDS`, `_INSTALL_COMMANDS_HOST` and `install.sh` | -| build the image | `Dockerfile` | +| declare the agent and its extensions | `a2a_server.py`, the `@a2a_agent` block | +| run a CLI per turn | `ClaudeCodeAgent.run`, `claude_bridge.py` `run_claude` | +| map the model endpoint and `LITELLM_EXTRA_HEADERS` onto a CLI | `claude_bridge.py` `cli_env` | +| read file parts | `kit.py` `materialize_file_part` | +| keep session refs past the SDK's 1,024-context LRU | `kit.py` `DurableSessionRefs` | +| answer errors as `{"detail": ...}` | `kit.py` `json_error_bodies`, applied in `build_app` | +| build the image | `Dockerfile`, `entrypoint.sh` | | test the agent offline | `tst/unit/agents/claude_code/` (`conftest.py`, `test_card_contract.py`, `test_extensions.py`) | -| return a trajectory once the run ends, not live | `agents/pi/pi_agent.py` `_result` (`native_trajectory`) | -| capture and replay a snapshot changelog | `agents/pi/pi_agent.py` `enable_changelog` / `apply_changelog`, `agents/pi/changelog.py` | -| replace the SDK's peer tools with your own | `agents/pi/pi_agent.py` `set_peers` / `list_peers`, `agents/pi/peers.py` | -`packages/agentenv-protocol/examples/` has smaller agents, one feature each (`basic_agent.py`, `streaming_agent.py`, -`multimodal_agent.py`, `custom_extensions_agent.py`, `advanced_agent.py`). +`packages/agentenv-protocol/examples/` has smaller agents, one feature each. + +## Extensions + +Each extension is a `urn:agentenv:*` URI on the Agent Card with an `/ext/*` endpoint. An operation is implemented by +the SDK or by your runtime. List an extension in `extensions=(...)`, with `enable(EXT, ...)` for options; binding a +handler with `@extension(EXT.operation)` also turns it on. Overriding an SDK-owned operation means overriding its +whole group, and `self.default_handlers.call(...)` reaches the SDK's own handler from yours. Shapes are in +`packages/agentenv-protocol/src/agentenv_protocol/a2a_agent/extensions.py`. + +### `agent-config/v1`, `/ext/agent-config` + +agent-env configures the agent: identity at deploy, then `model`, `system_prompt`, `timeout_seconds` and the rest +before each prompt. The SDK implements `set` and `get`; you declare an `AgentConfig` subclass with `config=` and read +`request.config` in `run()`. Only the fields you declare are sent to you, every field needs a default, and secrets +are `WriteOnly`. `prompt_agent` always sends `timeout_seconds` (600 unless the step sets one), and each `set` merges +into the last, so a field one prompt sets stays set until another sets it. + +Reference: `ClaudeCodeConfig` and `config=ClaudeCodeConfig`. Overriding `set`/`get`: +`packages/agentenv-protocol/examples/advanced_agent.py`. + +### `mcp-config/v1`, `/ext/mcp-config` + +agent-env registers each env's MCP server as `{url, name, headers}`. The SDK implements `add` and `list`; you hand +`request.mcp_servers` to the runtime every turn, keeping the names and passing the headers through. + +Reference: `enable(MCP_CONFIG_V1, ...)`, and `claude_bridge.py` where it builds `--mcp-config`. + +### `trajectory/v1`, `/ext/trajectory` + +agent-env reads what the agent did, and verifiers grade it. Call `request.trajectory.set_format(...)`, then append +each native event as it happens; with `enable(TRAJECTORY_V1, live=True)` it can be read while the turn runs. The SDK +serves the reads; bind `get.context` and `get.context_objects` to serve a whole conversation from your own record. + +Reference: `enable(TRAJECTORY_V1, live=True, ...)`, `ClaudeCodeAgent.run` passing +`event_callback=request.trajectory.append`, and `get_context_trajectory` / `upload_context_trajectory`. A runtime +that only has a record at the end returns it with `native_trajectory(...)`: `agents/pi/pi_agent.py` `_result`. + +### `skill-config/v1`, `/ext/skill-config` + +agent-env installs skills, inline (`skill_md`) or as a bundle of files behind read grants. You implement both +`add.inline` and `add.bundle`; the SDK implements `list`, rejects duplicate names and passes installed skills in +`request.skills`. + +Reference: `ClaudeCodeAgent.add_inline_skill` / `add_bundle_skill`, both calling `kit.py` `install_skill`. + +### `snapshot/v1`, `/ext/snapshot` + +agent-env saves an agent's conversation and workspace and restores them into another agent. You implement `save`, +uploading both the trajectory and the workspace object through the grants (an empty workspace if there is none), +and `load`, restoring them into `target_context_id`. `load` runs on a fresh agent, so create state directories at +startup. The optional changelog uploads one increment per tool call and replays them in order. + +Reference: `ClaudeCodeAgent.save_snapshot` / `load_snapshot`. Changelog: `agents/pi/pi_agent.py` +`enable_changelog` / `apply_changelog`, and `agents/pi/changelog.py`. + +### `peer-agents/v1`, `/ext/peer-agents` + +agent-env tells the agent about the task's other agents. Enabled with no handlers, the SDK (protocol 0.1.350 or +newer) serves six MCP peer tools and adds them to `request.mcp_servers` as `peer_agents`. To serve your own, +override `set` and `list` and register your MCP server through `default_handlers.call(MCP_CONFIG_V1.add, ...)` on a +`127.0.0.1` URL ending in `/mcp`. + +Reference: `enable(PEER_AGENTS_V1, ...)`. Your own tools: `agents/pi/pi_agent.py` `set_peers` / `list_peers`, and +`agents/pi/peers.py`. + +### `triggers/v1`, `/ext/triggers` + +agent-env registers scripted triggers and asks, each turn, what an agent playing a user or stakeholder says next. +The SDK implements `register`, `decide` and `state`; enable it and do nothing else. + +Reference: `enable(TRIGGERS_V1, ...)`. Overriding `decide` to change the decision: +`packages/agentenv-protocol/examples/advanced_agent.py`. + +### `attribution-probe/v1`, `/ext/attribution-probe` + +The agent reports the attribution it last sent with a model request. You implement `probe`, which also declares the +extension. Neither shipped agent does. + +Reference: `test_attribution_probe_reports_what_the_agent_last_sent` in +`packages/agentenv-protocol/tests/test_a2a_agent.py`. + +### `install/v1` (no endpoint) + +A card declaration that lets a task install the agent into a container it already runs, or onto a VM sandbox's host. +`install_agent` imports `AGENT_CARD` from the image's `a2a_server` module, with `/app` on `sys.path` and without your +entrypoint, then fills each `{param}` of the commands in, quoted. Pass the endpoint as environment on the command +that needs it, not as arguments; register the image with `--dockerfile`, since its build context is what gets +installed. + +Reference: `enable(INSTALL_V1, ...)` with `_INSTALL_COMMANDS`, `_INSTALL_COMMANDS_HOST` and `_ENDPOINT`, and +`install.sh`. + +### `staging/v1`, `/ext/staging` + +An object store on the agent's own server, which agent-env uses when the agent can't reach its object store. It is +on by default and needs nothing from you; `AGENTENV_STAGING_MAX_BYTES=0` turns it off. + +Reference: `packages/agentenv-protocol/src/agentenv_protocol/a2a_agent/staging.py`. + +### Your own extensions + +Use your own URI, not `urn:agentenv:*`. Reference: `packages/agentenv-protocol/examples/custom_extensions_agent.py`. ## The protocol -- `packages/agentenv-protocol/src/agentenv_protocol/a2a_agent/extensions.py` defines every `urn:agentenv:*` - extension: its endpoint, operations, request and response models, and whether the SDK or your runtime implements - each operation. It is the source of truth for the wire format. -- `tasks/v1.py` there is `TaskRequest`, `TaskResult` and `TrajectoryLog`; `agentenv_protocol/transfers.py` is - `upload`, `download` and `NamespaceUploader` for object grants. +- `extensions.py` (above) is the source of truth for the wire format. `tasks/v1.py` next to it is `TaskRequest`, + `TaskResult` and `TrajectoryLog`; `agentenv_protocol/transfers.py` is `upload`, `download` and + `NamespaceUploader` for object grants. - `packages/agentenv-protocol/README.md`, section "A2A agent framework", is the prose reference. - What agent-env sends you: `src/agent_env/task_step/task_steps/deploy_agent.py` (env vars, MCP registrations, identity) and `prompt_agent.py` (config fields, then `message/send`). ## Rules that bite -- `prompt_agent` always sends `timeout_seconds` (600 unless the step sets one), and `agent-config` merges each post - into the last: a field one prompt sets stays set until another sets it. -- Declare only the config fields the runtime honours, under the names `prompt_agent` sends (`model`, `system_prompt`, - `effort`, `max_turns`, `max_thinking_tokens`, `output_format`, `timeout_seconds`, `task_id`, ...). Secrets are - `WriteOnly`. -- `skill-config` needs both `add.inline` and `add.bundle`. Overriding an SDK-owned operation means overriding its - whole group. -- A snapshot `save` uploads both the trajectory and the workspace object, an empty workspace if there is none; - `load` runs on a fresh agent, so create state directories at startup. - Bind the session before the runtime starts, as `ClaudeCodeAgent.run` does, so a live trajectory read or a snapshot taken mid-turn finds it. -- The SDK serves the peer tools from protocol 0.1.350. An agent that overrides `peer-agents` registers its own MCP - server through `default_handlers.call(MCP_CONFIG_V1.add, ...)` on a `127.0.0.1` URL ending in `/mcp`. - Turns of different contexts run concurrently, and the validator relies on it; don't serialize them. -- `install_agent` imports `AGENT_CARD` with `/app` on `sys.path` and without your entrypoint. Pass the endpoint as - environment on the command that needs it, as `_ENDPOINT` does, not as arguments. - Start the server from `CMD`, not `ENTRYPOINT`, on `0.0.0.0:$A2A_PORT`, as a non-root user that tolerates root, with - everything installed at build time (`Dockerfile`, `entrypoint.sh`). + everything installed at build time. - `TaskResult.builder().failed(...)` adds no text part; add one. Use `infra_error` only when a retry could help. ## Registering and validating From 65048d9e000ef92a930a37145d5bf9c07cf62ae1 Mon Sep 17 00:00:00 2001 From: Pratyush Singhal Date: Wed, 21 Oct 2026 09:54:12 -0700 Subject: [PATCH 5/8] docs(agents): document the agent loop and the Agent Card in AGENTS.md Organize AGENTS.md as Agent, Agent loop, Agent Card and Extensions. The agent loop section covers core A2A task handling: the JSON-RPC methods agent-env calls, the task lifecycle the SDK owns, what run() receives and returns, errors, contexts and sessions, deadlines and cancellation, and streaming. The Agent Card section covers identity, what the SDK derives and what updates it live. Each points to the Claude Code agent and its tests; the remaining rules move into those sections. Co-Authored-By: Claude Opus 5.5 --- src/agent_env/agents/AGENTS.md | 92 +++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/src/agent_env/agents/AGENTS.md b/src/agent_env/agents/AGENTS.md index 52138285..db365b1d 100644 --- a/src/agent_env/agents/AGENTS.md +++ b/src/agent_env/agents/AGENTS.md @@ -4,12 +4,13 @@ How to write an agent that agent-env can deploy, prompt, inspect and validate. T `agents/claude-code/`: agent-env's default agent and its most tested one. `agents/pi/` covers the few patterns Claude Code doesn't use. `README.md` here describes what each of them does. -## The shape of an agent +## Agent An agent is a Docker image running an A2A server, built with the SDK, `agentenv_protocol.a2a_agent` -(`pip install 'agentenv-framework-protocol[agent]'`). You write one class: a typed config, the extensions it supports, -`run()` for one turn, and handlers for what only your runtime can do. The SDK serves the Agent Card, the JSON-RPC -endpoint and every `/ext/*` route. The end of `agents/claude-code/a2a_server.py` is the whole entry point: +(`pip install 'agentenv-framework-protocol[agent]'`). You write one `AgentEnvAgent` subclass, decorated with +`@a2a_agent(...)`: its identity, a typed config, the extensions it supports, `run()` for one task, and handlers for +what only your runtime can do. The SDK serves the Agent Card, the A2A JSON-RPC endpoint, `/health` and every `/ext/*` +route. The end of `agents/claude-code/a2a_server.py` is the whole entry point: ```python def build_app(agent: ClaudeCodeAgent) -> Starlette: @@ -20,30 +21,80 @@ app = build_app(ClaudeCodeAgent()) AGENT_CARD = app.state.agentenv_a2a.card # install/v1 imports this name ``` -and `agents/claude-code/entrypoint.sh` serves it with -`exec uvicorn a2a_server:app --host 0.0.0.0 --port "${A2A_PORT:-8000}"`. +`agents/claude-code/entrypoint.sh` serves it with `exec uvicorn a2a_server:app --host 0.0.0.0 --port +"${A2A_PORT:-8000}"`, from the image's `CMD` (not `ENTRYPOINT`), as a non-root user that tolerates root, with +everything installed at build time. -## Where each pattern lives - -Paths are under `agents/claude-code/`. `kit.py` there holds the plumbing any CLI agent needs; copy it rather than -rewriting it. +Paths below are under `agents/claude-code/`. `kit.py` there holds the plumbing any CLI agent needs; copy it rather +than rewriting it. | To | Read | |---|---| -| declare the agent and its extensions | `a2a_server.py`, the `@a2a_agent` block | -| run a CLI per turn | `ClaudeCodeAgent.run`, `claude_bridge.py` `run_claude` | +| declare the agent | `a2a_server.py`, the `@a2a_agent` block on `ClaudeCodeAgent` | | map the model endpoint and `LITELLM_EXTRA_HEADERS` onto a CLI | `claude_bridge.py` `cli_env` | -| read file parts | `kit.py` `materialize_file_part` | -| keep session refs past the SDK's 1,024-context LRU | `kit.py` `DurableSessionRefs` | | answer errors as `{"detail": ...}` | `kit.py` `json_error_bodies`, applied in `build_app` | +| stop running turns when the server stops | `_turns_stop_with_the_server`, passed as `lifespan=` | | build the image | `Dockerfile`, `entrypoint.sh` | -| test the agent offline | `tst/unit/agents/claude_code/` (`conftest.py`, `test_card_contract.py`, `test_extensions.py`) | +| test the agent offline | `tst/unit/agents/claude_code/` (`conftest.py` builds the app with `TestClient`) | `packages/agentenv-protocol/examples/` has smaller agents, one feature each. +## Agent loop: handling tasks + +agent-env speaks A2A JSON-RPC to `/a2a`: `message/send` for each prompt, then `tasks/get` until the task ends, and +`tasks/cancel` to stop one (`src/agent_env/a2a_agent/protocol.py`). The SDK owns the task: it creates it +(`submitted`, then `working`), builds a `TaskRequest`, calls your `run()` and maps what it returns onto the task's +terminal state, `completed`, `failed` or `canceled`. Your `run()` is one turn. + +- **In:** `TaskRequest` carries `task_id`, `context_id`, `parts` (`TextPart`, `FilePart` by `bytes` or `uri`, + `DataPart`), `config` (your typed config, as it stood when the task was created), `mcp_servers`, `skills`, + `metadata`, `session_ref`, `workspace` and `trajectory`. +- **Out:** a `TaskResult` from `TaskResult.text(...)`, `TaskResult.failure(code, message)` or `TaskResult.builder()`. + Its parts become the task's status message, with `usage` as a data part; a failure adds + `{error_type, error_code, error_message}`. A success needs at least one part, and `builder().failed(...)` adds no + text part, so add one. +- **Errors:** `agent_error`, the default, means the agent tried and failed; `infra_error` means the run couldn't + happen, and agent-env may retry it. An exception that escapes `run()` becomes an opaque `infra_error`, its text + never sent. +- **Context and sessions:** turns of one `context_id` run one at a time; different contexts run concurrently, and the + validator relies on it, so don't serialize them. Return your runtime's conversation id as `session_ref` and the next + turn in that context gets it back; bind it before the runtime starts, so a live trajectory read or a mid-turn + snapshot finds it. +- **Deadline and cancel:** bound the turn by `request.config.timeout_seconds`. On `tasks/cancel`, `run()` gets + `CancelledError`; kill the runtime and anything it started before re-raising. +- **Streaming:** make `run()` an async generator that yields `TaskProgress` and ends with one `TaskResult`; the card + then advertises streaming. + +| To | Read | +|---|---| +| handle a turn: parts in, session bound, CLI run, result out | `ClaudeCodeAgent.run` and `_turn` | +| turn file parts into files the runtime reads | `kit.py` `materialize_file_part` | +| run the CLI within the deadline, streaming its events | `claude_bridge.py` `run_claude` | +| kill the CLI and its tools on timeout or cancel | `claude_bridge.py` `_kill_cli`, `kill_running_clis` | +| keep session refs past the SDK's 1,024-context LRU | `kit.py` `DurableSessionRefs` | +| test the loop | `tst/unit/agents/claude_code/test_message_contract.py`, `test_claude_bridge_cancel.py` | +| stream progress | `packages/agentenv-protocol/examples/streaming_agent.py` | + +## Agent Card + +The SDK builds the card from `@a2a_agent(...)` and serves it at `/.well-known/agent-card.json` and +`/.well-known/agent.json`, the path agent-env reads. You never write it by hand. + +- **Identity:** `AgentIdentity(name, description, version, input_modes, output_modes, skills)`. List in + `input_modes` only the media types `run()` handles; the validator sends every modality probe and records your + declared modes beside the results. `skills` here describe the agent for discovery; they are not installed skills. +- **Derived:** the RPC `url` (`/a2a`), `capabilities.streaming` from the shape of `run()`, and + `capabilities.extensions`, one entry per extension with `params.endpoint` and `params.methods`. For agent-config, + `methods.set.request.supported` lists your config fields: agent-env sends only those. `staging/v1` is appended. +- **Live:** setting `name` or `description` through agent-config, or installing a skill, updates the served card. + +Reference: the `identity=AgentIdentity(...)` block on `ClaudeCodeAgent`, and +`tst/unit/agents/claude_code/test_card_contract.py`, which pins the card. +`curl localhost:8000/.well-known/agent-card.json` shows a running agent's card. + ## Extensions -Each extension is a `urn:agentenv:*` URI on the Agent Card with an `/ext/*` endpoint. An operation is implemented by +Each extension is a `urn:agentenv:*` URI on the Agent Card, most with an `/ext/*` endpoint. An operation is implemented by the SDK or by your runtime. List an extension in `extensions=(...)`, with `enable(EXT, ...)` for options; binding a handler with `@extension(EXT.operation)` also turns it on. Overriding an SDK-owned operation means overriding its whole group, and `self.default_handlers.call(...)` reaches the SDK's own handler from yours. Shapes are in @@ -152,15 +203,6 @@ Use your own URI, not `urn:agentenv:*`. Reference: `packages/agentenv-protocol/e - What agent-env sends you: `src/agent_env/task_step/task_steps/deploy_agent.py` (env vars, MCP registrations, identity) and `prompt_agent.py` (config fields, then `message/send`). -## Rules that bite - -- Bind the session before the runtime starts, as `ClaudeCodeAgent.run` does, so a live trajectory read or a snapshot - taken mid-turn finds it. -- Turns of different contexts run concurrently, and the validator relies on it; don't serialize them. -- Start the server from `CMD`, not `ENTRYPOINT`, on `0.0.0.0:$A2A_PORT`, as a non-root user that tolerates root, with - everything installed at build time. -- `TaskResult.builder().failed(...)` adds no text part; add one. Use `infra_error` only when a retry could help. - ## Registering and validating In a bundle, a folder `agents//` with a Dockerfile is an agent, with optional defaults in `agent.toml` From e5fb7c4b081eacede4287687f492681be58aaa69 Mon Sep 17 00:00:00 2001 From: Pratyush Singhal Date: Wed, 21 Oct 2026 09:54:33 -0700 Subject: [PATCH 6/8] docs(agents): rewrap the extensions intro in AGENTS.md Co-Authored-By: Claude Opus 5.5 --- src/agent_env/agents/AGENTS.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/agent_env/agents/AGENTS.md b/src/agent_env/agents/AGENTS.md index db365b1d..f7ba400e 100644 --- a/src/agent_env/agents/AGENTS.md +++ b/src/agent_env/agents/AGENTS.md @@ -94,11 +94,11 @@ Reference: the `identity=AgentIdentity(...)` block on `ClaudeCodeAgent`, and ## Extensions -Each extension is a `urn:agentenv:*` URI on the Agent Card, most with an `/ext/*` endpoint. An operation is implemented by -the SDK or by your runtime. List an extension in `extensions=(...)`, with `enable(EXT, ...)` for options; binding a -handler with `@extension(EXT.operation)` also turns it on. Overriding an SDK-owned operation means overriding its -whole group, and `self.default_handlers.call(...)` reaches the SDK's own handler from yours. Shapes are in -`packages/agentenv-protocol/src/agentenv_protocol/a2a_agent/extensions.py`. +Each extension is a `urn:agentenv:*` URI on the Agent Card, most with an `/ext/*` endpoint. An operation is +implemented by the SDK or by your runtime. List an extension in `extensions=(...)`, with `enable(EXT, ...)` for +options; binding a handler with `@extension(EXT.operation)` also turns it on. Overriding an SDK-owned operation +means overriding its whole group, and `self.default_handlers.call(...)` reaches the SDK's own handler from yours. +Shapes are in `packages/agentenv-protocol/src/agentenv_protocol/a2a_agent/extensions.py`. ### `agent-config/v1`, `/ext/agent-config` From a09bc45a19f86c20be095b2a0f2e2c1d34f3b925 Mon Sep 17 00:00:00 2001 From: Pratyush Singhal Date: Wed, 21 Oct 2026 10:31:52 -0700 Subject: [PATCH 7/8] docs(agents): add the steps to add your own agent to AGENTS.md A numbered walk-through from an empty bundle to a validated agent: copy the Claude Code agent's folder, declare the agent, write the turn, map the model endpoint, add extensions, test offline, add defaults and a smoke task, run the bundle, register and validate, and use the agent from a task or as the default. It absorbs the registering and validating section. Co-Authored-By: Claude Opus 5.5 --- src/agent_env/agents/AGENTS.md | 50 ++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/agent_env/agents/AGENTS.md b/src/agent_env/agents/AGENTS.md index f7ba400e..ef390777 100644 --- a/src/agent_env/agents/AGENTS.md +++ b/src/agent_env/agents/AGENTS.md @@ -194,24 +194,35 @@ Reference: `packages/agentenv-protocol/src/agentenv_protocol/a2a_agent/staging.p Use your own URI, not `urn:agentenv:*`. Reference: `packages/agentenv-protocol/examples/custom_extensions_agent.py`. -## The protocol - -- `extensions.py` (above) is the source of truth for the wire format. `tasks/v1.py` next to it is `TaskRequest`, - `TaskResult` and `TrajectoryLog`; `agentenv_protocol/transfers.py` is `upload`, `download` and - `NamespaceUploader` for object grants. -- `packages/agentenv-protocol/README.md`, section "A2A agent framework", is the prose reference. -- What agent-env sends you: `src/agent_env/task_step/task_steps/deploy_agent.py` (env vars, MCP registrations, - identity) and `prompt_agent.py` (config fields, then `message/send`). - -## Registering and validating - -In a bundle, a folder `agents//` with a Dockerfile is an agent, with optional defaults in `agent.toml` -(`[default_env_vars]`, `[metadata] default_model`, `min_disk_size_gb`). Give it a smoke task like -`tasks/claude-code-smoke.json`. +## Adding your own agent + +1. **Make a bundle with an agent folder.** A bundle is a folder; `agents//` in it, with a `Dockerfile`, is an + agent named ``. Start from a copy of `agents/claude-code/`: `a2a_server.py`, `kit.py`, `Dockerfile`, + `entrypoint.sh`, and a `requirements.txt` with `agentenv-framework-protocol[agent]`. +2. **Declare the agent** (Agent, Agent Card): an `AgentConfig` subclass with only the fields your runtime honours, + and `@a2a_agent(identity=AgentIdentity(...), config=..., extensions=(...))` with the `input_modes` `run()` handles. +3. **Write the turn** (Agent loop): `run()` turns `request.parts` into a prompt, runs your runtime within + `request.config.timeout_seconds` with `request.mcp_servers` connected, records the trajectory, and returns a + `TaskResult` with a `session_ref`. Replace `claude_bridge.py` with your runtime's bridge. +4. **Map the model endpoint**: read `LITELLM_BASE_URL`, `LITELLM_API_KEY` and `LITELLM_EXTRA_HEADERS` and hand them + to your runtime, as `claude_bridge.py` `cli_env` does. +5. **Add extensions** (Extensions): `mcp-config` and `trajectory` first, then every other one your runtime can + honour. Keep `AGENT_CARD = app.state.agentenv_a2a.card` at the end of `a2a_server.py`. +6. **Test it offline** with Starlette's `TestClient` and your runtime faked, as `tst/unit/agents/claude_code/` does: + the card, one turn, one failure, a cancel. +7. **Add defaults and a smoke task.** An optional `agents//agent.toml` holds `[default_env_vars]` and + `[metadata]` (`default_model`, `min_disk_size_gb`). `tasks/-smoke.json` deploys the agent by folder name, + prompts it once and checks the reply, as `tasks/claude-code-smoke.json` does. +8. **Run it**: `agent-env run ./my-bundle` builds the image, registers the agent and runs the bundle's tasks. It needs + Docker and a `[model]` endpoint in `.agentenv/config.toml`. +9. **Register and validate it**: `agent-env a2a-agent put` stores the image and registers the agent under an id any + task can name in `deploy_agent`'s `a2a_agent_id`, then runs the validator; fix what fails and validate again. +10. **Use it**: name it in a task's `deploy_agent` step, or make it the agent every `deploy_agent` without an id gets + with `[agents] default_a2a_agent_id` in `.agentenv/config.toml`. ```bash agent-env run ./my-bundle # build, register under @local, run its tasks -agent-env a2a-agent put --id my-agent --dockerfile path/to/Dockerfile +agent-env a2a-agent put --id my-agent --dockerfile my-bundle/agents/my-agent/Dockerfile agent-env a2a-agent validate --id my-agent # agent-env's built-in checks agent-env a2a-agent get --id my-agent # the validation results ``` @@ -221,3 +232,12 @@ store; the checks are in `src/agent_env/a2a_agent/validator.py` and `src/agent_env/task_step/task_steps/a2a_agent_validator/`, one step each. It doesn't cover everything an agent can advertise: `triggers/v1`, `attribution-probe/v1`, host-mode `install/v1` and custom extensions need tests of your own. + +## The protocol + +- `extensions.py` (above) is the source of truth for the wire format. `tasks/v1.py` next to it is `TaskRequest`, + `TaskResult` and `TrajectoryLog`; `agentenv_protocol/transfers.py` is `upload`, `download` and + `NamespaceUploader` for object grants. +- `packages/agentenv-protocol/README.md`, section "A2A agent framework", is the prose reference. +- What agent-env sends you: `src/agent_env/task_step/task_steps/deploy_agent.py` (env vars, MCP registrations, + identity) and `prompt_agent.py` (config fields, then `message/send`). From b824c84db3f091453464b74eab737a0c937de820 Mon Sep 17 00:00:00 2001 From: Pratyush Singhal Date: Wed, 21 Oct 2026 11:13:42 -0700 Subject: [PATCH 8/8] docs(agents): drop the reference-agent sentence from the AGENTS.md intro Co-Authored-By: Claude Opus 5.5 --- src/agent_env/agents/AGENTS.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/agent_env/agents/AGENTS.md b/src/agent_env/agents/AGENTS.md index ef390777..f1fe8e54 100644 --- a/src/agent_env/agents/AGENTS.md +++ b/src/agent_env/agents/AGENTS.md @@ -1,8 +1,7 @@ # AGENTS.md: writing an A2A agent -How to write an agent that agent-env can deploy, prompt, inspect and validate. The reference is the Claude Code agent, -`agents/claude-code/`: agent-env's default agent and its most tested one. `agents/pi/` covers the few patterns Claude -Code doesn't use. `README.md` here describes what each of them does. +How to write an agent that agent-env can deploy, prompt, inspect and validate. `README.md` here describes the agents +in this folder. ## Agent