How to Convert OpenEnv Async EnvClient to Synchronous with the `.sync()` Method
Call .sync() on any OpenEnv EnvClient instance to receive a SyncEnvClient wrapper that runs async WebSocket operations in a background thread, exposing a blocking synchronous API.
OpenEnv's EnvClient is deliberately asynchronous to maintain persistent WebSocket connections without blocking the caller. For developers working in synchronous codebases, standard Python scripts, or REPL environments, the huggingface/OpenEnv repository provides a built-in conversion mechanism that wraps the async client while preserving full functionality.
Why OpenEnv Uses Async by Default
The EnvClient class in src/openenv/core/env_client.py employs async/await patterns to handle long-lived WebSocket connections and Docker provisioning efficiently. This architecture prevents I/O blocking during network operations but requires async and await keywords throughout consuming code. For libraries and scripts that cannot accommodate an async event loop, the .sync() factory method offers a seamless bridge.
The .sync() Method Architecture
How EnvClient.sync() Creates the Wrapper
Defined at line 85 in src/openenv/core/env_client.py, the .sync() method imports and returns a SyncEnvClient instance. This method passes the original async client (self) to the wrapper, which manages thread-safe communication between your synchronous code and the underlying async implementation.
The SyncEnvClient Implementation
The SyncEnvClient class in src/openenv/core/sync_client.py creates a dedicated asyncio event loop running in a separate daemon thread. Key implementation details include:
- Coroutine Scheduling: Uses
asyncio.run_coroutine_threadsafeto dispatch async method calls from the main thread to the background loop. - Dynamic Method Wrapping: Implements
__getattr__to automatically wrap any coroutine-based method on the underlying async client, caching the synchronous wrapper for subsequent invocations. - Blocking Execution: Every method call (
reset,step,state, etc.) blocks until the coroutine completes, returning the result directly to the caller.
Resource Management and Cleanup
SyncEnvClient implements the context manager protocol with __enter__ and __exit__ methods. The close() method ensures proper teardown by:
- Calling the async client's
close()coroutine. - Stopping the background event loop cleanly.
- Preventing zombie daemon threads from persisting after client usage.
Practical Code Examples
Basic Synchronous Usage with Context Managers
from echo_env import CallToolAction, EchoEnv
# Convert async client to synchronous
sync_client = EchoEnv(base_url="https://openenv-echo-env.hf.space").sync()
with sync_client:
# Blocking reset call
result = sync_client.reset()
print(result.observation.echoed_message)
# Blocking step call
result = sync_client.step(
CallToolAction(
tool_name="echo_message",
arguments={"message": "Hello, World!"},
)
)
print(result.observation.result)
Manual Connection Lifecycle
from openenv.core import GenericEnvClient
# Create sync wrapper from GenericEnvClient
client = GenericEnvClient(base_url="ws://localhost:8000").sync()
# Explicitly manage WebSocket lifecycle
client.connect() # Establishes connection in background loop
obs = client.reset() # Blocking execution
obs = client.step({"code": "print('hello')"})
client.disconnect() # Closes WebSocket
client.close() # Terminates background event loop
Hybrid Async and Sync Access
async_client = EchoEnv(base_url="...")
sync_client = async_client.sync()
# Use synchronous wrapper for main workflow
result = sync_client.reset()
# Access underlying async methods when needed
await async_client.some_async_helper()
Supported Client Types
The .sync() method is available across the OpenEnv client hierarchy:
- GenericEnvClient (
src/openenv/core/generic_client.py): The base implementation for generic environment connections. - Environment-specific clients: Concrete implementations like
EchoEnvthat inherit fromEnvClientand expose the same synchronous conversion capability via inheritance.
Summary
- OpenEnv's
EnvClientuses async architecture for non-blocking WebSocket and Docker operations. - The
.sync()method insrc/openenv/core/env_client.pyreturns aSyncEnvClientwrapper that runs a dedicated event loop in a background daemon thread. - Method calls use
asyncio.run_coroutine_threadsafeto bridge sync and async execution transparently. - Context manager support via
__enter__and__exit__ensures proper resource cleanup through theclose()method. - All async clients—including
GenericEnvClientand environment-specific implementations—support the synchronous conversion pattern.
Frequently Asked Questions
Does using .sync() impact performance compared to native async?
The synchronous wrapper introduces minimal overhead through thread scheduling and blocking synchronization primitives. While native async remains optimal for high-concurrency scenarios, the .sync() wrapper provides comparable performance for standard sequential workflows and blocking operations.
Can I use the async client and sync wrapper simultaneously?
Yes. Calling .sync() creates a wrapper that references the original async client. You can interleave calls between the async_client (using await) and sync_client (blocking), though you should consider thread-safety when accessing shared state between the two interfaces.
What happens if I don't call close() on the sync client?
The SyncEnvClient daemon thread will continue running until the Python process terminates. Using the context manager (with statement) or explicitly calling close() ensures the background event loop stops cleanly and releases the WebSocket connection immediately.
Is .sync() available on all OpenEnv clients?
Any class inheriting from EnvClient—including GenericEnvClient and environment-specific implementations like EchoEnv—inherits the .sync() method defined in src/openenv/core/env_client.py and can generate a synchronous wrapper.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →