Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e3db4c7
feat(protocol): record a computer's display, from the driver's @recor…
earakely-scale Oct 27, 2026
d333a7d
feat(protocol): probe a driver's recordings
earakely-scale Oct 27, 2026
58f15da
feat(task): record a computer env with start_computer_recording and e…
earakely-scale Oct 27, 2026
b05248f
feat(computer): the reference driver records its display through x11grab
earakely-scale Oct 27, 2026
c9bdb6a
docs(computer): document computer-recording/v1, @record and the recor…
earakely-scale Oct 27, 2026
8eab232
fix(protocol): encode a stream's frames as deltas, not each one as a …
earakely-scale Oct 27, 2026
b0fcf8d
fix(protocol): end stalled recordings on time, keep segments sealing …
earakely-scale Oct 27, 2026
937dd08
fix(task): keep a retried recording's namespace, refuse a reused name…
earakely-scale Oct 27, 2026
57792e9
fix(computer): fail the probe on a segment read that returns nothing
earakely-scale Oct 27, 2026
1945d26
feat(computer): let a recording run for up to 24 hours by default
earakely-scale Oct 27, 2026
47f0dbe
feat(computer): close a recording's segments every 15 s, up to 10,000…
earakely-scale Oct 27, 2026
0c333ca
refactor(computer): a typed recording manifest, frames_in_thread, and…
earakely-scale Oct 27, 2026
16a275a
test(computer): recording chaos in the integration tier
earakely-scale Oct 27, 2026
8e90e98
fix(computer): never drop a source's error, and time recording tests …
earakely-scale Oct 27, 2026
4db534b
feat(computer): record every computer by default, with PyAV in the co…
earakely-scale Oct 27, 2026
d09c51a
refactor(task): one before_teardown for what must reach the stores be…
earakely-scale Oct 27, 2026
155bf32
test(computer): allow a full segment to read exactly max_segment_s
earakely-scale Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ and deserialized through a registry.
|---|---|
| `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`), `ComputerDriverEnv` (`computer_driver`, a computer driver's files and what its class declares for each platform it supports, read at put in a process of its own, `agentenv_protocol.computer._declare`, where the driver's own dependencies needn't be installed; its deploy is refused until agent-env creates computers). `env/gateway/gateway.py` is the env-side gateway server, container-only code. While its clock is armed it serves agents `get_time` (`env/gateway/get_time.py`), which answers in UTC and, given an IANA `time_zone`, also in that zone; executors' working `hours` (`env/gateway/triggers.py`) take a fixed `utc_offset` or an IANA `time_zone`. |
| `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`), `EnvironmentUniverseArtifact` (`environment_universe`), `SkillArtifact` (`skill`); `[artifacts] type_aliases` maps another stored spelling of a type onto one of them. A `DockerImageArtifact` holds a tar.gz of its image, or, written by `put_ref` (`--image-ref` on `env mcp-server put` and `a2a-agent put`), only a registry reference with its tag pinned to a digest, which sandboxes pull, or a published image's tag with its `image_digest` and its build context, which sandboxes pull by the digest and build from the context when they can't (the infra images, and a bundle agent built from a shipped agent's sources, `Plan.published`), or, written by `put_context`, only its build context, which a VM sandbox builds before running it (`VmSandbox.load_docker_images`) and Modal builds before creating its container (`ModalSandboxProvider.prepare_image`, outside a fallback chain's deadline); anything else that runs images by name (a container on the local provider, a lone server on any provider but Modal) refuses such an image. A build context (`utils/docker/context.py`) is what `docker build` would send, `.dockerignore` applied, written so equal files give equal bytes and an equal `source_digest`. |
| `task/`, `task_step/` | A `Task` holds its `TaskStep`s 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). `task/episode/` runs a task with some of its agents played from outside, as a training loop does (`EpisodeSpec`). |
| `task/`, `task_step/` | A `Task` holds its `TaskStep`s 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/`, `start_computer_recording` and `end_computer_recording`, whose open recordings `teardown_run` and `teardown_sandboxes` end first, and more). `task/episode/` runs a task with some of its agents played from outside, as a training loop does (`EpisodeSpec`). |
| `store/` | Four store ABCs with local and cloud implementations: `DocumentStore` (SQLite, MongoDB, DynamoDB, Firestore with MongoDB compatibility), `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. `store/routing.py` is the `@local` namespace: while the CLI or a `build_app` server runs, an `@local/…` id and what an `@local` task's run records go to local stores beside the configured ones, and that run's writes to the configured stores are refused. 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), `smol_vm` (the `smol` extra), `tensorlake` (the `tensorlake` extra), `tenki` (the `tenki` extra), `vercel` (the `vercel` extra), `boxd` (the `boxd` extra), `daytona` (the `daytona` extra); `[sandbox] default` and `agent_default` accept a comma-separated fallback chain, whose deploys skip a provider that can't run them. A sandbox provider declares what it runs (`ON_THIS_MACHINE`, and for VMs on this machine the address their requests to it arrive at, `REACHES_HOST_AT`; `CONTAINER_ACCEPTS`, `PRIVATE_NETWORK`; `SUPPORTED_SANDBOX_MODES`, derived from whether it implements `create_sandbox` and `create_vm`; and `SANDBOX_ACCEPTS`, the images an agent's sandbox runs, which defaults to those a VM loads when it implements `create_vm` and to `CONTAINER_ACCEPTS` otherwise, so only a provider that implements `create_vm` yet gives an agent a container, like the local one, declares it), each sandbox carries the `platform` of the images it runs (linux/amd64 unless its provider sets another when it creates it; None, any, unchecked, for a local sandbox), and code outside `providers/sandbox_providers/` reads those, never which provider it has. `providers/env_providers/` holds the environment providers: `EnvironmentProvider` (an env's containers and state store), `EnvironmentGatewayProvider` (`gateway`, an env's default), which renders a docker-compose for the gateway and its MCP servers inside the sandbox, and `EnvironmentServerProvider` (`server`), one MCP server in its own container with no gateway in front and no state store; `providers/env_state/` holds env-state providers (`local_postgres` built in). |
Expand All @@ -120,7 +120,7 @@ and deserialized through a registry.
| `explorer/` | Optional local web UI: `agent-env up`, binds loopback `:8234`. |
| `cli/` | Click CLI with the groups `a2a-agent`, `artifact`, `bundle`, `config`, `env`, `eval`, `plugin`, `run`, `task`, `up`. `task run`, `task run-batch` and `eval run` hand their runs to the configured `[runner]` and follow them. |
| `agents/` | The `agents` bundle: the agents agent-env ships, one folder each under `agents/agents/`, with a smoke task each under `agents/tasks/`. Its Claude Code, `@local/agentenv-framework/agents/claude-code`, is the default agent. An agent's Python packages are its folder's `requirements.txt`, installed in its image, never in agent-env's own environment. A run that pulls one of its agents reads the whole bundle, so `tst/unit/bundle/shipped_bundles_test.py` parses and checks every shipped bundle. |
| `drivers/` | The `drivers` bundle: the computer drivers agent-env ships as `computer_driver` envs, the reference driver (X11 on Linux) aliased `reference`. It has no tasks: a bundle with neither tasks nor evals is refused unless its `aliases.toml` names something for other bundles to use. `tst/integration/computer/` serves it on a scaffold desktop image and runs the conformance probe and an agent against it. |
| `drivers/` | The `drivers` bundle: the computer drivers agent-env ships as `computer_driver` envs, the reference driver (X11 on Linux, recording through `x11grab`) aliased `reference`. It has no tasks: a bundle with neither tasks nor evals is refused unless its `aliases.toml` names something for other bundles to use. `tst/integration/computer/` serves it on a scaffold desktop image and runs the conformance probe and an agent against it. |
| `examples/` | The example bundles agent-env ships (`hello`, and `echo-mcp`, which needs Docker). Tests run them, but they are examples for users, not fixtures. Every shipped bundle is registered under `agent_env.bundles` in `pyproject.toml`, `<name> = "<package>"` shipping `src/<package as a path>/<name>/`. |

## Extension points
Expand Down
36 changes: 36 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ The runtime dependencies of `agentenv-framework` and its `aws`, `gcp`, `sail`, `
- Source: <https://github.com/authlib/authlib>
- [License text 20](#license-text-20)

### av 19.0.2

- License: BSD-3-Clause
- Copyright: Copyright retained by original committers. All rights reserved.
- Source: <https://github.com/PyAV-Org/PyAV>
- Bundled: its binary wheels include FFmpeg's libraries and the codecs they are built with, among them libx264 and libx265, each under its own license
- [License text 116](#license-text-116)

### backports.tarfile 1.2.0

- License: MIT
Expand Down Expand Up @@ -15245,3 +15253,31 @@ The above copyright notice and this permission notice shall be included in all c

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

### License text 116

```text
Copyright retained by original committers. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the project nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
51 changes: 47 additions & 4 deletions packages/agentenv-protocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ async with ToolSession(mcp_url=mcp_url, role="alice@example.com") as session:

A computer driver is what runs inside a computer an agent uses through screenshots and input. Write
one as a `ComputerDriverEnvironment` subclass from `agentenv_protocol.computer`, which needs the
`computer` extra (`pip install 'agentenv-framework-protocol[computer]'`: mcp, uvicorn and Pillow).
`computer` extra (`pip install 'agentenv-framework-protocol[computer]'`: mcp, uvicorn, Pillow and PyAV).
The driver marks the methods that capture the screen and send input; the base class serves the
computer contract from them, so the tools, results and errors can't drift from it. A complete
driver for an X11 desktop is agent-env's reference driver, in its
Expand Down Expand Up @@ -367,6 +367,7 @@ pixels), at the display's size times its scale. Input operations may return an `
| `@key_down`, `@key_up` | both or none | `computer-keyboard/v1` and its two tools |
| `@run_in_session` | no; a Linux default | `computer-exec/v1` |
| `@write_files` | no; a Linux default | `data/add` |
| `@record` | no | the stream `computer-recording/v1` records from; without it the base class polls `@capture` |

The class is checked when it is defined: a missing required operation, half a group, two methods
for one operation that some platform could both match, or features that don't match the
Expand Down Expand Up @@ -398,8 +399,9 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
- **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
capture, plus the driver's own `ready_checks()`; tools refuse `device_unavailable` until they
pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
descriptions), `computer-keyboard/v1` and `computer-exec/v1` (`run {argv, cwd?, env?, timeout_s?,
request_id}`: output capped per stream, a repeated `request_id` answered with the first result).
descriptions), `computer-keyboard/v1`, `computer-exec/v1` (`run {argv, cwd?, env?, timeout_s?,
request_id}`: output capped per stream, a repeated `request_id` answered with the first result) and,
with `recording`, `computer-recording/v1` (below).
- **The data plane.** `data/add` takes file parts that name their path, relative to the session
user's home, under the `data-objects/v1` marker (`{"path": ...}`), as inline bytes, an https URL,
or a `transfers.ReadObject` read grant under the marker's `read`, and writes them atomically.
Expand All @@ -425,6 +427,47 @@ names the caller whose latest frame and others' input a call uses.
`MyDriver.declaration(platform)` returns the card the class serves on a `Platform`, built without
running the driver, its operations or its dependencies.

### Recording the display

Every driver serves `computer-recording/v1`: video of the display, as MP4 (H.264) segments, for the
framework to start and end around the work it wants on record. `recording = Recording(...)` sets its
limits, and `recording = None` turns it off.

```python
from agentenv_protocol.computer import RecordedFrame, Recording, record


class MyDriver(ComputerDriverEnvironment):
recording = Recording(max_fps=30) # max_concurrent=2, max_duration_s=86400, max_segment_s=15

@record
async def stream(self, hints): # hints.scope, fps, max_edge, cursor, max_segment_s, directory
async for image, captured_at in my_screen_stream(hints.fps):
yield RecordedFrame(image, captured_at) # a Pillow image or a PyAV VideoFrame
```

- **The stream.** `@record` yields `RecordedFrame`s from a stream the OS pushes, which the base class
scales, encodes and splits into segments, each frame timed by its `captured_at`; or
`RecordedSegment`s, files a native recorder finished under `hints.directory`, which it only hashes,
uploads and lists. The base class closes the stream at `end`. A source that blocks, as most OS
capture APIs do, goes through `frames_in_thread(frames)`: the generator `frames()` runs in a thread
of its own, the latest 8 frames wait for the encoder, and it is closed when the recording stops. A
driver without `@record` records by the base class polling `@capture`, with a pointer drawn at
`@pointer_position`: the slow path, at most 15 fps.
- **The methods.** `start {request_id, scope?, fps?, max_edge?, cursor?, upload?}` returns
`{recording_id, started_at, format}`; `end {recording_id, upload?, reason?}` closes the last segment
and returns the manifest. A repeated `request_id`, or a repeated `end`, answers the same. A start
past `max_concurrent` is refused `limit_exceeded`; `max_duration_s`, free disk under 512 MB or a stream
failing for 60 s end a recording by itself.
- **Segments.** A segment closes at `max_segment_s`, when the frame size changes or after a gap in the
stream. Each uploads as it closes under `start`'s `write_namespace` grant, at
`<recording_id>/<index>.mp4`, and is then deleted; `end`'s own grant uploads any that missed. With no
grant they stay under `~/.agentenv-recordings/`.
- **The manifest** (`RecordingManifest`). Its `segments` (`ManifestSegment`) carry each one's path,
`started_at` and `ended_at` (its first and last frames' capture times), size, frame count, byte size,
SHA-256 and whether it uploaded, and `ended_by` says why the recording ended: `end`, `reset`,
`teardown`, `limit` or `error`. A field it doesn't know is left out.

### Checking a driver: the conformance probe

The probe checks a running driver against the contract through its card, tools and extensions. It
Expand All @@ -447,7 +490,7 @@ The CLI exits 0 when no check failed, 1 when one did and 2 when it can't reach t
| Level | Checks | Needs on the computer |
|---|---|---|
| `basic` | P0 ready and a screenshot; P1 the card and tool schemas; P2 native frames, regions and JPEG; P5 refusals and their codes; X the declared extensions (`computer-display`, `computer-spaces`, `computer-keyboard`, `computer-exec`, `data/get`, per-role tool access, the pointer) | nothing |
| `full` | `basic`, then P3 colours land where the mapping says; P4 clicks show their effect; P5 refusals change nothing; P6 typed and pressed keys arrive; P7 scrolls and drags arrive; P9 commands and `data/add` run as one unprivileged user; X pointer and keyboard input, `data/add` | Linux, `python3` with Tk, a 1280x800 screen or larger, `computer-exec` and `data/add` |
| `full` | `basic`, then P3 colours land where the mapping says; P4 clicks show their effect; P5 refusals change nothing; P6 typed and pressed keys arrive; P7 scrolls and drags arrive; P9 commands and `data/add` run as one unprivileged user; X pointer and keyboard input, `data/add`, and a recording's segments matching their files with no gap, a repeated `request_id` joining it and `max_concurrent` held | Linux, `python3` with Tk, a 1280x800 screen or larger, `computer-exec` and `data/add` |

At `full` the probe writes the Fixture Desk, a Tk window (`conformance/fixture.py`), into
`~/.agentenv-probe` with `data/add` and starts it with `computer-exec`. It aims at the boxes the
Expand Down
5 changes: 4 additions & 1 deletion packages/agentenv-protocol/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ agent = [
"regex>=2023.12.25",
"uvicorn>=0.30",
]
# A computer driver: ComputerDriverEnvironment serves its tools over mcp's FastMCP and crops and encodes frames.
# A computer driver: ComputerDriverEnvironment serves its tools over mcp's FastMCP, crops and encodes frames, and
# records the display as H.264 with PyAV (computer-recording/v1).
computer = [
"av>=17",
"mcp>=1.25,<2",
"Pillow>=10.4",
"uvicorn>=0.30",
Expand All @@ -50,6 +52,7 @@ dev = [
"mcp>=1.25,<2",
"fastmcp>=3.4,<4", # the standalone-fastmcp mount path
"Pillow>=10.4", # the computer extra's frame crops and encodings
"av>=17", # the computer extra's encoder, and the tests' decoder
]
[project.urls]
Homepage = "https://www.agentenvframework.com"
Expand Down
Loading
Loading