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_threadsafe to 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:

  1. Calling the async client's close() coroutine.
  2. Stopping the background event loop cleanly.
  3. 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 EchoEnv that inherit from EnvClient and expose the same synchronous conversion capability via inheritance.

Summary

  • OpenEnv's EnvClient uses async architecture for non-blocking WebSocket and Docker operations.
  • The .sync() method in src/openenv/core/env_client.py returns a SyncEnvClient wrapper that runs a dedicated event loop in a background daemon thread.
  • Method calls use asyncio.run_coroutine_threadsafe to bridge sync and async execution transparently.
  • Context manager support via __enter__ and __exit__ ensures proper resource cleanup through the close() method.
  • All async clients—including GenericEnvClient and 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →