fleet-python 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fleet-python might be problematic. Click here for more details.

fleet/client.py CHANGED
@@ -20,38 +20,61 @@ import httpx
20
20
  import logging
21
21
  from typing import Optional, List
22
22
 
23
- from .base import InstanceBase, AsyncWrapper, SyncWrapper
23
+ from .base import EnvironmentBase, AsyncWrapper, SyncWrapper
24
24
  from .models import InstanceRequest, InstanceRecord, Environment as EnvironmentModel
25
25
 
26
- from .env import Environment, AsyncEnvironment
26
+ from .manager import InstanceClient, AsyncInstanceClient, ResetRequest, ResetResponse
27
+ from .resources.base import Resource
28
+ from .resources.sqlite import AsyncSQLiteResource
29
+ from .resources.browser import AsyncBrowserResource
27
30
 
28
31
  logger = logging.getLogger(__name__)
29
32
 
30
33
 
31
- class Instance(InstanceBase):
34
+ class Environment(EnvironmentBase):
32
35
  def __init__(self, httpx_client: Optional[httpx.Client] = None, **kwargs):
33
36
  super().__init__(**kwargs)
34
37
  self._httpx_client = httpx_client or httpx.Client()
35
- self._env: Optional[Environment] = None
38
+ self._instance: Optional[InstanceClient] = None
36
39
 
37
40
  @property
38
- def env(self) -> Environment:
39
- if self._env is None:
40
- self._env = Environment(self.manager_url, self._httpx_client)
41
- return self._env
41
+ def instance(self) -> InstanceClient:
42
+ if self._instance is None:
43
+ self._instance = InstanceClient(self.manager_url, self._httpx_client)
44
+ return self._instance
42
45
 
43
46
 
44
- class AsyncInstance(InstanceBase):
47
+ class AsyncEnvironment(EnvironmentBase):
45
48
  def __init__(self, httpx_client: Optional[httpx.AsyncClient] = None, **kwargs):
46
49
  super().__init__(**kwargs)
47
50
  self._httpx_client = httpx_client or httpx.AsyncClient()
48
- self._env: Optional[AsyncEnvironment] = None
51
+ self._instance: Optional[AsyncInstanceClient] = None
49
52
 
50
53
  @property
51
- def env(self) -> AsyncEnvironment:
52
- if self._env is None:
53
- self._env = AsyncEnvironment(self.manager_url, self._httpx_client)
54
- return self._env
54
+ def instance(self) -> AsyncInstanceClient:
55
+ if self._instance is None:
56
+ self._instance = AsyncInstanceClient(self.manager_url, self._httpx_client)
57
+ return self._instance
58
+
59
+ async def reset(
60
+ self, seed: Optional[int] = None, timestamp: Optional[int] = None
61
+ ) -> ResetResponse:
62
+ return await self.instance.reset(ResetRequest(seed=seed, timestamp=timestamp))
63
+
64
+ def db(self, name: str = "current") -> AsyncSQLiteResource:
65
+ return self.instance.db(name)
66
+
67
+ def browser(self, name: str = "cdp") -> AsyncBrowserResource:
68
+ return self.instance.browser(name)
69
+
70
+ def state(self, uri: str) -> Resource:
71
+ return self.instance.state(uri)
72
+
73
+ async def resources(self) -> List[Resource]:
74
+ return await self.instance.resources()
75
+
76
+ async def close(self) -> InstanceRecord:
77
+ return await AsyncFleet().delete(self.instance_id)
55
78
 
56
79
 
57
80
  class Fleet:
@@ -76,23 +99,23 @@ class Fleet:
76
99
  response = self.client.request("GET", f"/v1/env/{env_key}")
77
100
  return EnvironmentModel(**response.json())
78
101
 
79
- def make(self, request: InstanceRequest) -> Instance:
102
+ def make(self, request: InstanceRequest) -> Environment:
80
103
  response = self.client.request(
81
104
  "POST", "/v1/env/instances", json=request.model_dump()
82
105
  )
83
- return Instance(**response.json())
106
+ return Environment(**response.json())
84
107
 
85
- def instances(self, status: Optional[str] = None) -> List[Instance]:
108
+ def instances(self, status: Optional[str] = None) -> List[Environment]:
86
109
  params = {}
87
110
  if status:
88
111
  params["status"] = status
89
112
 
90
113
  response = self.client.request("GET", "/v1/env/instances", params=params)
91
- return [Instance(**instance_data) for instance_data in response.json()]
114
+ return [Environment(**instance_data) for instance_data in response.json()]
92
115
 
93
- def instance(self, instance_id: str) -> Instance:
116
+ def instance(self, instance_id: str) -> Environment:
94
117
  response = self.client.request("GET", f"/v1/env/instances/{instance_id}")
95
- return Instance(**response.json())
118
+ return Environment(**response.json())
96
119
 
97
120
  def delete(self, instance_id: str) -> InstanceRecord:
98
121
  response = self.client.request("DELETE", f"/v1/env/instances/{instance_id}")
@@ -121,29 +144,38 @@ class AsyncFleet:
121
144
  response = await self.client.request("GET", f"/v1/env/{env_key}")
122
145
  return EnvironmentModel(**response.json())
123
146
 
124
- async def make(self, request: InstanceRequest) -> AsyncInstance:
147
+ async def make(self, env_key: str) -> AsyncEnvironment:
148
+ if ":" in env_key:
149
+ env_key_part, version = env_key.split(":", 1)
150
+ if not version.startswith("v"):
151
+ version = f"v{version}"
152
+ else:
153
+ env_key_part = env_key
154
+ version = None
155
+
156
+ request = InstanceRequest(env_key=env_key_part, version=version)
125
157
  response = await self.client.request(
126
158
  "POST", "/v1/env/instances", json=request.model_dump()
127
159
  )
128
- instance = AsyncInstance(**response.json())
129
- await instance.env.load()
160
+ instance = AsyncEnvironment(**response.json())
161
+ await instance.instance.load()
130
162
  return instance
131
163
 
132
- async def instances(self, status: Optional[str] = None) -> List[AsyncInstance]:
164
+ async def instances(self, status: Optional[str] = None) -> List[AsyncEnvironment]:
133
165
  params = {}
134
166
  if status:
135
167
  params["status"] = status
136
168
 
137
169
  response = await self.client.request("GET", "/v1/env/instances", params=params)
138
170
  instances = [
139
- AsyncInstance(**instance_data) for instance_data in response.json()
171
+ AsyncEnvironment(**instance_data) for instance_data in response.json()
140
172
  ]
141
- await asyncio.gather(*[instance.env.load() for instance in instances])
173
+ await asyncio.gather(*[instance.instance.load() for instance in instances])
142
174
  return instances
143
175
 
144
- async def instance(self, instance_id: str) -> AsyncInstance:
176
+ async def instance(self, instance_id: str) -> AsyncEnvironment:
145
177
  response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
146
- instance = AsyncInstance(**response.json())
178
+ instance = AsyncEnvironment(**response.json())
147
179
  await instance.env.load()
148
180
  return instance
149
181
 
fleet/env/__init__.py CHANGED
@@ -1,8 +1,3 @@
1
- """Fleet SDK Environment Module."""
1
+ from .client import make, list_envs, get
2
2
 
3
- from .client import Environment, AsyncEnvironment
4
-
5
- __all__ = [
6
- "Environment",
7
- "AsyncEnvironment",
8
- ]
3
+ __all__ = ["make", "list_envs", "get"]
fleet/env/client.py CHANGED
@@ -1,241 +1,15 @@
1
- """Fleet SDK Base Environment Classes."""
1
+ from ..client import AsyncFleet, AsyncEnvironment
2
+ from ..models import Environment as EnvironmentModel
3
+ from typing import List
2
4
 
3
- from typing import Any, Dict, List, Optional, Tuple
4
- import asyncio
5
- import httpx
6
- import time
7
- import logging
8
- from urllib.parse import urlparse
9
5
 
10
- from ..resources.sqlite import AsyncSQLiteResource
11
- from ..resources.browser import AsyncBrowserResource
12
- from ..resources.base import Resource
6
+ async def make(env_key: str) -> AsyncEnvironment:
7
+ return await AsyncFleet().make(env_key)
13
8
 
14
- from ..exceptions import FleetEnvironmentError, FleetAPIError
15
9
 
16
- from .base import SyncWrapper, AsyncWrapper
17
- from .models import (
18
- ResetResponse,
19
- Resource as ResourceModel,
20
- ResourceType,
21
- HealthResponse,
22
- )
10
+ async def list_envs() -> List[EnvironmentModel]:
11
+ return await AsyncFleet().list_envs()
23
12
 
24
13
 
25
- logger = logging.getLogger(__name__)
26
-
27
-
28
- RESOURCE_TYPES = {
29
- ResourceType.sqlite: AsyncSQLiteResource,
30
- ResourceType.cdp: AsyncBrowserResource,
31
- }
32
-
33
-
34
- class Environment:
35
- def __init__(
36
- self,
37
- url: str,
38
- httpx_client: Optional[httpx.Client] = None,
39
- ):
40
- self.base_url = url
41
- self.client = SyncWrapper(
42
- url=self.base_url, httpx_client=httpx_client or httpx.Client()
43
- )
44
- raise NotImplementedError("SyncManager is not implemented")
45
-
46
- def reset(self) -> ResetResponse:
47
- response = self.client.request("POST", "/reset")
48
- return ResetResponse(**response.json())
49
-
50
-
51
- class AsyncEnvironment:
52
- def __init__(
53
- self,
54
- url: str,
55
- httpx_client: Optional[httpx.AsyncClient] = None,
56
- ):
57
- self.base_url = url
58
- self.client = AsyncWrapper(
59
- url=self.base_url, httpx_client=httpx_client or httpx.AsyncClient()
60
- )
61
- self._resources: Optional[List[ResourceModel]] = None
62
- self._resources_state: Dict[str, Dict[str, Resource]] = {
63
- resource_type.value: {} for resource_type in ResourceType
64
- }
65
-
66
- async def load(self) -> None:
67
- await self._load_resources()
68
-
69
- async def reset(self) -> ResetResponse:
70
- response = await self.client.request("POST", "/reset")
71
- return ResetResponse(**response.json())
72
-
73
- def state(self, uri: str) -> Resource:
74
- url = urlparse(uri)
75
- return self._resources_state[url.scheme][url.netloc]
76
-
77
- def sqlite(self, name: str) -> AsyncSQLiteResource:
78
- return AsyncSQLiteResource(
79
- self._resources_state[ResourceType.sqlite.value][name], self.client
80
- )
81
-
82
- def browser(self, name: str) -> AsyncBrowserResource:
83
- return AsyncBrowserResource(
84
- self._resources_state[ResourceType.cdp.value][name], self.client
85
- )
86
-
87
- async def resources(self) -> List[ResourceModel]:
88
- await self._load_resources()
89
- return self._resources
90
-
91
- async def _load_resources(self) -> None:
92
- if self._resources is None:
93
- response = await self.client.request("GET", "/resources")
94
- if response.status_code != 200:
95
- self._resources = []
96
- return
97
- data = response.json()
98
- self._resources = [
99
- ResourceModel(**resource) for resource in data["resources"]
100
- ]
101
- for resource in self._resources:
102
- if resource.type not in self._resources_state:
103
- self._resources_state[resource.type.value] = {}
104
- self._resources_state[resource.type.value][resource.name] = (
105
- RESOURCE_TYPES[resource.type](resource, self.client)
106
- )
107
-
108
- async def step(self, action: Dict[str, Any]) -> Tuple[Dict[str, Any], float, bool]:
109
- """Execute one step in the environment."""
110
- if not self._instance_id:
111
- raise FleetEnvironmentError(
112
- "Environment not initialized. Call reset() first."
113
- )
114
-
115
- try:
116
- # Increment step count
117
- self._increment_step()
118
-
119
- # Execute action through instance manager API
120
- # This is a placeholder - actual implementation depends on the manager API spec
121
- state, reward, done = await self._execute_action(action)
122
-
123
- return state, reward, done
124
-
125
- except Exception as e:
126
- raise FleetEnvironmentError(f"Failed to execute step: {e}")
127
-
128
- async def close(self) -> None:
129
- """Close the environment and clean up resources."""
130
- try:
131
- # Delete instance if it exists
132
- if self._instance_id:
133
- try:
134
- await self._client.delete_instance(self._instance_id)
135
- logger.info(f"Deleted instance: {self._instance_id}")
136
- except FleetAPIError as e:
137
- logger.warning(f"Failed to delete instance: {e}")
138
- finally:
139
- self._instance_id = None
140
- self._instance_response = None
141
-
142
- # Close manager client
143
- if self._manager_client:
144
- await self._manager_client.close()
145
- self._manager_client = None
146
-
147
- # Close API client
148
- await self._client.close()
149
-
150
- except Exception as e:
151
- logger.error(f"Error closing environment: {e}")
152
-
153
- async def manager_health_check(self) -> Optional[HealthResponse]:
154
- response = await self.client.request("GET", "/health")
155
- return HealthResponse(**response.json())
156
-
157
- async def _wait_for_instance_ready(self, timeout: float = 300.0) -> None:
158
- """Wait for instance to be ready.
159
-
160
- Args:
161
- timeout: Maximum time to wait in seconds
162
- """
163
- start_time = time.time()
164
-
165
- while time.time() - start_time < timeout:
166
- try:
167
- instance = await self._client.get_instance(self._instance_id)
168
- self._instance_response = instance
169
-
170
- if instance.status == "running":
171
- logger.info(f"Instance {self._instance_id} is ready")
172
- return
173
-
174
- elif instance.status == "error":
175
- raise FleetEnvironmentError(
176
- f"Instance {self._instance_id} failed to start"
177
- )
178
-
179
- # Wait before checking again
180
- await asyncio.sleep(5)
181
-
182
- except FleetAPIError as e:
183
- if time.time() - start_time >= timeout:
184
- raise FleetEnvironmentError(
185
- f"Timeout waiting for instance to be ready: {e}"
186
- )
187
- await asyncio.sleep(5)
188
-
189
- raise FleetEnvironmentError(
190
- f"Timeout waiting for instance {self._instance_id} to be ready"
191
- )
192
-
193
- async def _execute_action(
194
- self, action: Dict[str, Any]
195
- ) -> Tuple[Dict[str, Any], float, bool]:
196
- """Execute an action through the instance manager API.
197
-
198
- This is a placeholder implementation that should be extended based on
199
- the actual manager API specification.
200
-
201
- Args:
202
- action: The action to execute as a dictionary
203
-
204
- Returns:
205
- Tuple of (state, reward, done)
206
- """
207
- # Ensure manager client is available
208
- await self._ensure_manager_client()
209
-
210
- # TODO: In the future, this would use the manager API to execute actions
211
- # For example: await self._manager_client.log_action(action)
212
- # For now, return placeholder values
213
-
214
- # Create a placeholder state
215
- state = self._create_state_from_action(action)
216
-
217
- # Create a placeholder reward
218
- reward = 0.0
219
-
220
- # Determine if episode is done (placeholder logic)
221
- done = self._step_count >= 100 # Example: done after 100 steps
222
-
223
- return state, reward, done
224
-
225
- def _create_state_from_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
226
- """Create state based on executed action."""
227
- return {
228
- "instance_id": self._instance_id,
229
- "step": self._step_count,
230
- "last_action": action,
231
- "timestamp": time.time(),
232
- "status": "running",
233
- }
234
-
235
- async def __aenter__(self):
236
- """Async context manager entry."""
237
- return self
238
-
239
- async def __aexit__(self, exc_type, exc_val, exc_tb):
240
- """Async context manager exit."""
241
- await self.close()
14
+ async def get(instance_id: str) -> AsyncEnvironment:
15
+ return await AsyncFleet().instance(instance_id)
@@ -0,0 +1,22 @@
1
+ """Fleet SDK Environment Module."""
2
+
3
+ from .client import InstanceClient, AsyncInstanceClient
4
+ from .models import (
5
+ ResetRequest,
6
+ ResetResponse,
7
+ CDPDescribeResponse,
8
+ ChromeStartRequest,
9
+ ChromeStartResponse,
10
+ ChromeStatusResponse,
11
+ )
12
+
13
+ __all__ = [
14
+ "InstanceClient",
15
+ "AsyncInstanceClient",
16
+ "ResetRequest",
17
+ "ResetResponse",
18
+ "CDPDescribeResponse",
19
+ "ChromeStartRequest",
20
+ "ChromeStartResponse",
21
+ "ChromeStatusResponse",
22
+ ]
@@ -0,0 +1,258 @@
1
+ """Fleet SDK Base Environment Classes."""
2
+
3
+ from typing import Any, Dict, List, Optional, Tuple
4
+ import asyncio
5
+ import httpx
6
+ import time
7
+ import logging
8
+ from urllib.parse import urlparse
9
+
10
+ from ..resources.sqlite import AsyncSQLiteResource
11
+ from ..resources.browser import AsyncBrowserResource
12
+ from ..resources.base import Resource
13
+
14
+ from ..exceptions import FleetEnvironmentError, FleetAPIError
15
+
16
+ from .base import SyncWrapper, AsyncWrapper
17
+ from .models import (
18
+ ResetRequest,
19
+ ResetResponse,
20
+ Resource as ResourceModel,
21
+ ResourceType,
22
+ HealthResponse,
23
+ )
24
+
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ RESOURCE_TYPES = {
30
+ ResourceType.db: AsyncSQLiteResource,
31
+ ResourceType.cdp: AsyncBrowserResource,
32
+ }
33
+
34
+
35
+ class InstanceClient:
36
+ def __init__(
37
+ self,
38
+ url: str,
39
+ httpx_client: Optional[httpx.Client] = None,
40
+ ):
41
+ self.base_url = url
42
+ self.client = SyncWrapper(
43
+ url=self.base_url, httpx_client=httpx_client or httpx.Client()
44
+ )
45
+ raise NotImplementedError("SyncManager is not implemented")
46
+
47
+ def reset(self) -> ResetResponse:
48
+ response = self.client.request("POST", "/reset")
49
+ return ResetResponse(**response.json())
50
+
51
+
52
+ class AsyncInstanceClient:
53
+ def __init__(
54
+ self,
55
+ url: str,
56
+ httpx_client: Optional[httpx.AsyncClient] = None,
57
+ ):
58
+ self.base_url = url
59
+ self.client = AsyncWrapper(
60
+ url=self.base_url, httpx_client=httpx_client or httpx.AsyncClient()
61
+ )
62
+ self._resources: Optional[List[ResourceModel]] = None
63
+ self._resources_state: Dict[str, Dict[str, Resource]] = {
64
+ resource_type.value: {} for resource_type in ResourceType
65
+ }
66
+
67
+ async def load(self) -> None:
68
+ await self._load_resources()
69
+
70
+ async def reset(
71
+ self, reset_request: Optional[ResetRequest] = None
72
+ ) -> ResetResponse:
73
+ response = await self.client.request(
74
+ "POST", "/reset", json=reset_request.model_dump() if reset_request else None
75
+ )
76
+ return ResetResponse(**response.json())
77
+
78
+ def state(self, uri: str) -> Resource:
79
+ url = urlparse(uri)
80
+ return self._resources_state[url.scheme][url.netloc]
81
+
82
+ def db(self, name: str) -> AsyncSQLiteResource:
83
+ """
84
+ Returns an AsyncSQLiteResource object for the given SQLite database name.
85
+
86
+ Args:
87
+ name: The name of the SQLite database to return
88
+
89
+ Returns:
90
+ An AsyncSQLiteResource object for the given SQLite database name
91
+ """
92
+ return AsyncSQLiteResource(
93
+ self._resources_state[ResourceType.db.value][name], self.client
94
+ )
95
+
96
+ def browser(self, name: str) -> AsyncBrowserResource:
97
+ return AsyncBrowserResource(
98
+ self._resources_state[ResourceType.cdp.value][name], self.client
99
+ )
100
+
101
+ async def resources(self) -> List[Resource]:
102
+ await self._load_resources()
103
+ return [
104
+ resource
105
+ for resources_by_name in self._resources_state.values()
106
+ for resource in resources_by_name.values()
107
+ ]
108
+
109
+ async def _load_resources(self) -> None:
110
+ if self._resources is None:
111
+ response = await self.client.request("GET", "/resources")
112
+ if response.status_code != 200:
113
+ self._resources = []
114
+ return
115
+ self._resources = [
116
+ ResourceModel(**resource) for resource in response.json()
117
+ ]
118
+ for resource in self._resources:
119
+ if resource.type not in self._resources_state:
120
+ self._resources_state[resource.type.value] = {}
121
+ self._resources_state[resource.type.value][resource.name] = (
122
+ RESOURCE_TYPES[resource.type](resource, self.client)
123
+ )
124
+
125
+ async def step(self, action: Dict[str, Any]) -> Tuple[Dict[str, Any], float, bool]:
126
+ """Execute one step in the environment."""
127
+ if not self._instance_id:
128
+ raise FleetEnvironmentError(
129
+ "Environment not initialized. Call reset() first."
130
+ )
131
+
132
+ try:
133
+ # Increment step count
134
+ self._increment_step()
135
+
136
+ # Execute action through instance manager API
137
+ # This is a placeholder - actual implementation depends on the manager API spec
138
+ state, reward, done = await self._execute_action(action)
139
+
140
+ return state, reward, done
141
+
142
+ except Exception as e:
143
+ raise FleetEnvironmentError(f"Failed to execute step: {e}")
144
+
145
+ async def close(self) -> None:
146
+ """Close the environment and clean up resources."""
147
+ try:
148
+ # Delete instance if it exists
149
+ if self._instance_id:
150
+ try:
151
+ await self._client.delete_instance(self._instance_id)
152
+ logger.info(f"Deleted instance: {self._instance_id}")
153
+ except FleetAPIError as e:
154
+ logger.warning(f"Failed to delete instance: {e}")
155
+ finally:
156
+ self._instance_id = None
157
+ self._instance_response = None
158
+
159
+ # Close manager client
160
+ if self._manager_client:
161
+ await self._manager_client.close()
162
+ self._manager_client = None
163
+
164
+ # Close API client
165
+ await self._client.close()
166
+
167
+ except Exception as e:
168
+ logger.error(f"Error closing environment: {e}")
169
+
170
+ async def manager_health_check(self) -> Optional[HealthResponse]:
171
+ response = await self.client.request("GET", "/health")
172
+ return HealthResponse(**response.json())
173
+
174
+ async def _wait_for_instance_ready(self, timeout: float = 300.0) -> None:
175
+ """Wait for instance to be ready.
176
+
177
+ Args:
178
+ timeout: Maximum time to wait in seconds
179
+ """
180
+ start_time = time.time()
181
+
182
+ while time.time() - start_time < timeout:
183
+ try:
184
+ instance = await self._client.get_instance(self._instance_id)
185
+ self._instance_response = instance
186
+
187
+ if instance.status == "running":
188
+ logger.info(f"Instance {self._instance_id} is ready")
189
+ return
190
+
191
+ elif instance.status == "error":
192
+ raise FleetEnvironmentError(
193
+ f"Instance {self._instance_id} failed to start"
194
+ )
195
+
196
+ # Wait before checking again
197
+ await asyncio.sleep(5)
198
+
199
+ except FleetAPIError as e:
200
+ if time.time() - start_time >= timeout:
201
+ raise FleetEnvironmentError(
202
+ f"Timeout waiting for instance to be ready: {e}"
203
+ )
204
+ await asyncio.sleep(5)
205
+
206
+ raise FleetEnvironmentError(
207
+ f"Timeout waiting for instance {self._instance_id} to be ready"
208
+ )
209
+
210
+ async def _execute_action(
211
+ self, action: Dict[str, Any]
212
+ ) -> Tuple[Dict[str, Any], float, bool]:
213
+ """Execute an action through the instance manager API.
214
+
215
+ This is a placeholder implementation that should be extended based on
216
+ the actual manager API specification.
217
+
218
+ Args:
219
+ action: The action to execute as a dictionary
220
+
221
+ Returns:
222
+ Tuple of (state, reward, done)
223
+ """
224
+ # Ensure manager client is available
225
+ await self._ensure_manager_client()
226
+
227
+ # TODO: In the future, this would use the manager API to execute actions
228
+ # For example: await self._manager_client.log_action(action)
229
+ # For now, return placeholder values
230
+
231
+ # Create a placeholder state
232
+ state = self._create_state_from_action(action)
233
+
234
+ # Create a placeholder reward
235
+ reward = 0.0
236
+
237
+ # Determine if episode is done (placeholder logic)
238
+ done = self._step_count >= 100 # Example: done after 100 steps
239
+
240
+ return state, reward, done
241
+
242
+ def _create_state_from_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
243
+ """Create state based on executed action."""
244
+ return {
245
+ "instance_id": self._instance_id,
246
+ "step": self._step_count,
247
+ "last_action": action,
248
+ "timestamp": time.time(),
249
+ "status": "running",
250
+ }
251
+
252
+ async def __aenter__(self):
253
+ """Async context manager entry."""
254
+ return self
255
+
256
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
257
+ """Async context manager exit."""
258
+ await self.close()