Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
85c47c6
Add WebSocket support for environment interactions and enhance HTTP s…
rycerzes Dec 4, 2025
e0a063d
impl concurrency management and session handling
rycerzes Dec 4, 2025
95563b0
add async to http server
burtenshaw Dec 6, 2025
1584902
Merge remote-tracking branch 'origin/async-http' into impl/concurrency
rycerzes Dec 7, 2025
3601357
concurrency config
rycerzes Dec 7, 2025
600acb4
chore: add websockets to pyproject.toml
rycerzes Dec 8, 2025
a98851a
add concurrency safe pram
burtenshaw Dec 10, 2025
8197d6f
use factory in template app
burtenshaw Dec 10, 2025
f72b6da
us WS in client
burtenshaw Dec 10, 2025
26b1148
expose ws classes
burtenshaw Dec 10, 2025
1ddd8d8
add websocket examples to template readme
burtenshaw Dec 10, 2025
7138716
add note to toml for github install
burtenshaw Dec 10, 2025
438f966
refactor: enforce env factory usage and drop instance mode
rycerzes Dec 10, 2025
7319be0
refactor(ws): replace WSMessage with typed BaseMessage + discriminate…
rycerzes Dec 10, 2025
561f902
refactor: remove redundant ConcurrencySafetyLevel
rycerzes Dec 10, 2025
c90cca0
update web interface
burtenshaw Dec 10, 2025
f57b36f
make web interface compatible with websockets
burtenshaw Dec 10, 2025
bd2a163
format
burtenshaw Dec 10, 2025
3e116f8
relative imports in template
burtenshaw Dec 10, 2025
25b7cfa
use pydantic in template
burtenshaw Dec 10, 2025
8f23dc4
rename to session_timeout
rycerzes Dec 11, 2025
0d56b83
ConcurrencyConfig, ServerCapacityStatus, and SessionInfo inherit from…
rycerzes Dec 11, 2025
31ae2df
Merge remote-tracking branch 'origin/ws-in-cli' into impl/concurrency
rycerzes Dec 11, 2025
9cd2aac
message classes to inherit from BaseMessage for shared config
rycerzes Dec 11, 2025
77a8c83
refactor: rename CONCURRENCY_SAFE to SUPPORTS_CONCURRENT_SESSIONS
rycerzes Dec 11, 2025
86a222d
refactor: core types for better inference and fix async detection
rycerzes Dec 12, 2025
e95f8b1
fix: concurrency handling and improve exception messages
rycerzes Dec 12, 2025
05e6da0
chore: clean up exception handling and remove unused concurrency conf…
rycerzes Dec 13, 2025
d52850f
refactor: simplify environment factory type annotations and add utili…
rycerzes Dec 14, 2025
737386b
update cli template with naming
burtenshaw Dec 15, 2025
4bdfb6b
update prociders docstring
burtenshaw Dec 15, 2025
227ca93
remove http from env server
burtenshaw Dec 15, 2025
402d144
rename in core to envclient
burtenshaw Dec 15, 2025
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
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ dependencies = [
"huggingface_hub>=0.20.0",
"openai>=2.7.2",
"tomli>=2.3.0",
"tomli-w>=1.2.0"
"tomli-w>=1.2.0",
"websockets>=15.0.1",
]

[project.optional-dependencies]
Expand Down
60 changes: 58 additions & 2 deletions src/openenv/cli/templates/openenv_env/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ The deployed space includes:
- **Web Interface** at `/web` - Interactive UI for exploring the environment
- **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
- **Health Check** at `/health` - Container health monitoring
- **WebSocket** at `/ws` - Persistent session endpoint for low-latency interactions

## Environment Details

Expand Down Expand Up @@ -154,6 +155,61 @@ result = __ENV_NAME__env.step(__ENV_CLASS_NAME__Action(message="Hello!"))

Note: When connecting to an existing server, `__ENV_NAME__env.close()` will NOT stop the server.

### Using the Context Manager

The client supports context manager usage for automatic connection management:

```python
from __ENV_NAME__ import __ENV_CLASS_NAME__Action, __ENV_CLASS_NAME__Env

# Connect with context manager (auto-connects and closes)
with __ENV_CLASS_NAME__Env(base_url="http://localhost:8000") as env:
result = env.reset()
print(f"Reset: {result.observation.echoed_message}")
# Multiple steps with low latency
for msg in ["Hello", "World", "!"]:
result = env.step(__ENV_CLASS_NAME__Action(message=msg))
print(f"Echoed: {result.observation.echoed_message}")
```

The client uses WebSocket connections for:
- **Lower latency**: No HTTP connection overhead per request
- **Persistent session**: Server maintains your environment state
- **Efficient for episodes**: Better for many sequential steps

### Concurrent WebSocket Sessions

The server supports multiple concurrent WebSocket connections. To enable this,
modify `server/app.py` to use factory mode:

```python
# In server/app.py - use factory mode for concurrent sessions
app = create_app(
__ENV_CLASS_NAME__Environment, # Pass class, not instance
__ENV_CLASS_NAME__Action,
__ENV_CLASS_NAME__Observation,
max_concurrent_envs=4, # Allow 4 concurrent sessions
)
```

Then multiple clients can connect simultaneously:

```python
from __ENV_NAME__ import __ENV_CLASS_NAME__Action, __ENV_CLASS_NAME__Env
from concurrent.futures import ThreadPoolExecutor

def run_episode(client_id: int):
with __ENV_CLASS_NAME__Env(base_url="http://localhost:8000") as env:
result = env.reset()
for i in range(10):
result = env.step(__ENV_CLASS_NAME__Action(message=f"Client {client_id}, step {i}"))
return client_id, result.observation.message_length

# Run 4 episodes concurrently
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(run_episode, range(4)))
```

## Development & Testing

### Direct Environment Testing
Expand Down Expand Up @@ -189,11 +245,11 @@ __ENV_NAME__/
├── openenv.yaml # OpenEnv manifest
├── pyproject.toml # Project metadata and dependencies
├── uv.lock # Locked dependencies (generated)
├── client.py # __ENV_CLASS_NAME__Env client implementation
├── client.py # __ENV_CLASS_NAME__Env client
├── models.py # Action and Observation models
└── server/
├── __init__.py # Server module exports
├── __ENV_NAME___environment.py # Core environment logic
├── app.py # FastAPI application
├── app.py # FastAPI application (HTTP + WebSocket endpoints)
└── Dockerfile # Container image definition
```
9 changes: 6 additions & 3 deletions src/openenv/cli/templates/openenv_env/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""__ENV_TITLE_NAME__ Environment - A simple test environment for HTTP server."""
"""__ENV_TITLE_NAME__ Environment."""

from .client import __ENV_CLASS_NAME__Env
from .models import __ENV_CLASS_NAME__Action, __ENV_CLASS_NAME__Observation

__all__ = ["__ENV_CLASS_NAME__Action", "__ENV_CLASS_NAME__Observation", "__ENV_CLASS_NAME__Env"]

__all__ = [
"__ENV_CLASS_NAME__Action",
"__ENV_CLASS_NAME__Observation",
"__ENV_CLASS_NAME__Env",
]
47 changes: 22 additions & 25 deletions src/openenv/cli/templates/openenv_env/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,47 @@
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""
__ENV_TITLE_NAME__ Environment HTTP Client.
"""__ENV_TITLE_NAME__ Environment Client."""

This module provides the client for connecting to a __ENV_TITLE_NAME__ Environment server
over HTTP.
"""

from typing import Any, Dict
from typing import Dict

from openenv.core.client_types import StepResult
from openenv.core.env_server.types import State
from openenv.core.http_env_client import HTTPEnvClient
from openenv.core import EnvClient

from .models import __ENV_CLASS_NAME__Action, __ENV_CLASS_NAME__Observation


class __ENV_CLASS_NAME__Env(HTTPEnvClient[__ENV_CLASS_NAME__Action, __ENV_CLASS_NAME__Observation]):
class __ENV_CLASS_NAME__Env(EnvClient[__ENV_CLASS_NAME__Action, __ENV_CLASS_NAME__Observation]):
"""
HTTP client for the __ENV_TITLE_NAME__ Environment.
Client for the __ENV_TITLE_NAME__ Environment.

This client connects to a __ENV_CLASS_NAME__Environment HTTP server and provides
methods to interact with it: reset(), step(), and state access.
This client maintains a persistent WebSocket connection to the environment server,
enabling efficient multi-step interactions with lower latency.
Each client instance has its own dedicated environment session on the server.

Example:
>>> # Connect to a running server
>>> client = __ENV_CLASS_NAME__Env(base_url="http://localhost:8000")
>>> result = client.reset()
>>> print(result.observation.echoed_message)
>>>
>>> # Send a message
>>> result = client.step(__ENV_CLASS_NAME__Action(message="Hello!"))
>>> print(result.observation.echoed_message)
>>> print(result.reward)
>>> with __ENV_CLASS_NAME__Env(base_url="http://localhost:8000") as client:
... result = client.reset()
... print(result.observation.echoed_message)
...
... result = client.step(__ENV_CLASS_NAME__Action(message="Hello!"))
... print(result.observation.echoed_message)

Example with Docker:
>>> # Automatically start container and connect
>>> client = __ENV_CLASS_NAME__Env.from_docker_image("__ENV_NAME__-env:latest")
>>> result = client.reset()
>>> result = client.step(__ENV_CLASS_NAME__Action(message="Test"))
>>> try:
... result = client.reset()
... result = client.step(__ENV_CLASS_NAME__Action(message="Test"))
... finally:
... client.close()
"""

def _step_payload(self, action: __ENV_CLASS_NAME__Action) -> Dict:
"""
Convert __ENV_CLASS_NAME__Action to JSON payload for step request.
Convert __ENV_CLASS_NAME__Action to JSON payload for step message.

Args:
action: __ENV_CLASS_NAME__Action instance
Expand All @@ -64,7 +61,7 @@ def _parse_result(self, payload: Dict) -> StepResult[__ENV_CLASS_NAME__Observati
Parse server response into StepResult[__ENV_CLASS_NAME__Observation].

Args:
payload: JSON response from server
payload: JSON response data from server

Returns:
StepResult with __ENV_CLASS_NAME__Observation
Expand All @@ -89,7 +86,7 @@ def _parse_state(self, payload: Dict) -> State:
Parse server response into State object.

Args:
payload: JSON response from /state endpoint
payload: JSON response from state request

Returns:
State object with episode_id and step_count
Expand Down
10 changes: 4 additions & 6 deletions src/openenv/cli/templates/openenv_env/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,20 @@
The __ENV_NAME__ environment is a simple test environment that echoes back messages.
"""

from dataclasses import dataclass
from pydantic import Field

from openenv.core.env_server.types import Action, Observation


@dataclass(kw_only=True)
class __ENV_CLASS_NAME__Action(Action):
"""Action for the __ENV_TITLE_NAME__ environment - just a message to echo."""

message: str
message: str = Field(..., description="Message to echo back")


@dataclass(kw_only=True)
class __ENV_CLASS_NAME__Observation(Observation):
"""Observation from the __ENV_TITLE_NAME__ environment - the echoed message."""

echoed_message: str
message_length: int = 0
echoed_message: str = Field(default="", description="The echoed message")
message_length: int = Field(default=0, description="Length of the echoed message")

2 changes: 2 additions & 0 deletions src/openenv/cli/templates/openenv_env/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ description = "__ENV_TITLE_NAME__ environment for OpenEnv"
requires-python = ">=3.10"
dependencies = [
# Core OpenEnv runtime (provides FastAPI server + HTTP client types)
# install from github
# "openenv[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
"openenv[core]>=0.2.0",
# Environment-specific dependencies
# Add all dependencies needed for your environment here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ class __ENV_CLASS_NAME__Environment(Environment):
>>> print(obs.message_length) # 5
"""

# Enable concurrent WebSocket sessions.
# Set to True if your environment isolates state between instances.
# When True, multiple WebSocket clients can connect simultaneously, each
# getting their own environment instance (when using factory mode in app.py).
SUPPORTS_CONCURRENT_SESSIONS: bool = True

def __init__(self):
"""Initialize the __ENV_NAME__ environment."""
self._state = State(episode_id=str(uuid4()), step_count=0)
Expand Down
17 changes: 12 additions & 5 deletions src/openenv/cli/templates/openenv_env/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
FastAPI application for the __ENV_TITLE_NAME__ Environment.

This module creates an HTTP server that exposes the __ENV_CLASS_NAME__Environment
over HTTP endpoints, making it compatible with HTTPEnvClient.
over HTTP and WebSocket endpoints, compatible with EnvClient.

Endpoints:
- POST /reset: Reset the environment
- POST /step: Execute an action
- GET /state: Get current environment state
- GET /schema: Get action/observation schemas
- WS /ws: WebSocket endpoint for persistent sessions

Usage:
# Development (with auto-reload):
Expand All @@ -28,18 +35,18 @@
"openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
) from e

from __ENV_NAME__.models import __ENV_CLASS_NAME__Action, __ENV_CLASS_NAME__Observation
# Import from local models.py (PYTHONPATH includes /app/env in Docker)
from models import __ENV_CLASS_NAME__Action, __ENV_CLASS_NAME__Observation
from .__ENV_NAME___environment import __ENV_CLASS_NAME__Environment

# Create the environment instance
env = __ENV_CLASS_NAME__Environment()

# Create the app with web interface and README integration
app = create_app(
env,
__ENV_CLASS_NAME__Environment,
__ENV_CLASS_NAME__Action,
__ENV_CLASS_NAME__Observation,
env_name="__ENV_NAME__",
max_concurrent_envs=1, # increase this number to allow more concurrent WebSocket sessions
)


Expand Down
10 changes: 5 additions & 5 deletions src/openenv/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ Core components for OpenEnv - a framework for building HTTP-based agentic enviro

## Features

- **HTTPEnvClient**: Generic HTTP client for interacting with remote environments
- **HTTPEnvServer**: FastAPI-based server wrapper for exposing environments over HTTP
- **EnvClient**: Generic client for interacting with remote environments
- **HTTPEnvServer**: FastAPI-based server wrapper for exposing environments over HTTP/WebSocket
- **Container Providers**: Pluggable architecture for running containers (Docker, Kubernetes, etc.)
- **Type System**: Strongly-typed Action/Observation/State interfaces
- **Web Interface**: Optional web UI for interacting with environments
Expand All @@ -44,7 +44,7 @@ pip install "openenv[core]"
### Creating an Environment Client

```python
from openenv.core import HTTPEnvClient, StepResult
from openenv.core import EnvClient, StepResult
from dataclasses import dataclass

@dataclass
Expand All @@ -55,7 +55,7 @@ class MyAction:
class MyObservation:
response: str

class MyEnvClient(HTTPEnvClient[MyAction, MyObservation]):
class MyEnvClient(EnvClient[MyAction, MyObservation]):
def _step_payload(self, action: MyAction) -> dict:
return {"text": action.text}

Expand Down Expand Up @@ -141,7 +141,7 @@ provider.stop_container()

## API Reference

### HTTPEnvClient
### EnvClient

Base class for environment clients with these abstract methods:

Expand Down
11 changes: 4 additions & 7 deletions src/openenv/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,10 @@
"""Core components for agentic environments."""

# Re-export main components from submodules for convenience
from .env_server import *
from .client_types import StepResult
from .http_env_client import HTTPEnvClient
from .env_server import * # noqa: F403
from . import env_server
from .env_client import EnvClient

# Note: MCP module doesn't export anything yet

__all__ = [
"HTTPEnvClient",
"StepResult",
]
__all__ = ["EnvClient"] + env_server.__all__ # type: ignore
5 changes: 3 additions & 2 deletions src/openenv/core/client_types.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Type definitions for EnvTorch
from dataclasses import dataclass
from typing import Any, Generic, Optional, TypeVar
from typing import Generic, Optional, TypeVar

# Generic type for observations
ObsT = TypeVar("ObsT") # TypeVar for typehinting in IDEs
ObsT = TypeVar("ObsT")
StateT = TypeVar("StateT")


@dataclass
Expand Down
2 changes: 1 addition & 1 deletion src/openenv/core/containers/runtime/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Container provider abstractions for running environment servers.

This module provides a pluggable architecture for different container providers
(local Docker, Kubernetes, cloud providers, etc.) to be used with HTTPEnvClient.
(local Docker, Kubernetes, cloud providers, etc.) to be used with EnvClient.
"""

from __future__ import annotations
Expand Down
Loading