fleet-python 0.2.88__tar.gz → 0.2.89__tar.gz

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.
Files changed (114) hide show
  1. {fleet_python-0.2.88/fleet_python.egg-info → fleet_python-0.2.89}/PKG-INFO +1 -1
  2. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/__init__.py +1 -1
  3. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/__init__.py +1 -1
  4. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/base.py +1 -1
  5. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/orchestrator.py +307 -115
  6. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/base.py +1 -1
  7. {fleet_python-0.2.88 → fleet_python-0.2.89/fleet_python.egg-info}/PKG-INFO +1 -1
  8. {fleet_python-0.2.88 → fleet_python-0.2.89}/pyproject.toml +1 -1
  9. {fleet_python-0.2.88 → fleet_python-0.2.89}/LICENSE +0 -0
  10. {fleet_python-0.2.88 → fleet_python-0.2.89}/README.md +0 -0
  11. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/diff_example.py +0 -0
  12. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/dsl_example.py +0 -0
  13. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example.py +0 -0
  14. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/exampleResume.py +0 -0
  15. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_account.py +0 -0
  16. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_action_log.py +0 -0
  17. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_client.py +0 -0
  18. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_mcp_anthropic.py +0 -0
  19. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_mcp_openai.py +0 -0
  20. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_sync.py +0 -0
  21. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_task.py +0 -0
  22. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_tasks.py +0 -0
  23. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_verifier.py +0 -0
  24. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/export_tasks.py +0 -0
  25. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/fetch_tasks.py +0 -0
  26. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/gemini_example.py +0 -0
  27. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/import_tasks.py +0 -0
  28. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/iterate_verifiers.py +0 -0
  29. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/json_tasks_example.py +0 -0
  30. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/nova_act_example.py +0 -0
  31. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/openai_example.py +0 -0
  32. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/openai_simple_example.py +0 -0
  33. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/query_builder_example.py +0 -0
  34. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/quickstart.py +0 -0
  35. {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/test_cdp_logging.py +0 -0
  36. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/client.py +0 -0
  37. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/env/__init__.py +0 -0
  38. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/env/client.py +0 -0
  39. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/exceptions.py +0 -0
  40. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/global_client.py +0 -0
  41. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/instance/__init__.py +0 -0
  42. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/instance/base.py +0 -0
  43. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/instance/client.py +0 -0
  44. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/models.py +0 -0
  45. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/resources/__init__.py +0 -0
  46. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/resources/base.py +0 -0
  47. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/resources/browser.py +0 -0
  48. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/resources/mcp.py +0 -0
  49. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/resources/sqlite.py +0 -0
  50. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/tasks.py +0 -0
  51. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/verifiers/__init__.py +0 -0
  52. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/verifiers/bundler.py +0 -0
  53. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/verifiers/verifier.py +0 -0
  54. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/__init__.py +0 -0
  55. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/Dockerfile +0 -0
  56. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/__init__.py +0 -0
  57. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/agent.py +0 -0
  58. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/mcp_server.py +0 -0
  59. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/playwright_utils.py +0 -0
  60. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/requirements.txt +0 -0
  61. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/start.sh +0 -0
  62. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/types.py +0 -0
  63. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/utils.py +0 -0
  64. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/cli.py +0 -0
  65. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/client.py +0 -0
  66. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/config.py +0 -0
  67. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/env/__init__.py +0 -0
  68. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/env/client.py +0 -0
  69. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/eval/__init__.py +0 -0
  70. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/eval/uploader.py +0 -0
  71. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/exceptions.py +0 -0
  72. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/global_client.py +0 -0
  73. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/instance/__init__.py +0 -0
  74. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/instance/base.py +0 -0
  75. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/instance/client.py +0 -0
  76. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/instance/models.py +0 -0
  77. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/models.py +0 -0
  78. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/proxy/__init__.py +0 -0
  79. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/proxy/proxy.py +0 -0
  80. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/proxy/whitelist.py +0 -0
  81. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/resources/__init__.py +0 -0
  82. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/resources/base.py +0 -0
  83. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/resources/browser.py +0 -0
  84. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/resources/mcp.py +0 -0
  85. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/resources/sqlite.py +0 -0
  86. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/tasks.py +0 -0
  87. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/types.py +0 -0
  88. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/utils/__init__.py +0 -0
  89. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/utils/http_logging.py +0 -0
  90. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/utils/logging.py +0 -0
  91. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/utils/playwright.py +0 -0
  92. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/__init__.py +0 -0
  93. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/bundler.py +0 -0
  94. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/code.py +0 -0
  95. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/db.py +0 -0
  96. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/decorator.py +0 -0
  97. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/parse.py +0 -0
  98. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/sql_differ.py +0 -0
  99. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/verifier.py +0 -0
  100. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet_python.egg-info/SOURCES.txt +0 -0
  101. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet_python.egg-info/dependency_links.txt +0 -0
  102. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet_python.egg-info/entry_points.txt +0 -0
  103. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet_python.egg-info/requires.txt +0 -0
  104. {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet_python.egg-info/top_level.txt +0 -0
  105. {fleet_python-0.2.88 → fleet_python-0.2.89}/scripts/fix_sync_imports.py +0 -0
  106. {fleet_python-0.2.88 → fleet_python-0.2.89}/scripts/unasync.py +0 -0
  107. {fleet_python-0.2.88 → fleet_python-0.2.89}/setup.cfg +0 -0
  108. {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/__init__.py +0 -0
  109. {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/test_app_method.py +0 -0
  110. {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/test_expect_only.py +0 -0
  111. {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/test_instance_dispatch.py +0 -0
  112. {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/test_sqlite_resource_dual_mode.py +0 -0
  113. {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/test_sqlite_shared_memory_behavior.py +0 -0
  114. {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/test_verifier_from_string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.88
3
+ Version: 0.2.89
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -73,7 +73,7 @@ from . import env
73
73
  from . import global_client as _global_client
74
74
  from ._async import global_client as _async_global_client
75
75
 
76
- __version__ = "0.2.88"
76
+ __version__ = "0.2.89"
77
77
 
78
78
  __all__ = [
79
79
  # Core classes
@@ -44,7 +44,7 @@ from ..types import VerifierFunction
44
44
  from .. import env
45
45
  from . import global_client as _async_global_client
46
46
 
47
- __version__ = "0.2.88"
47
+ __version__ = "0.2.89"
48
48
 
49
49
  __all__ = [
50
50
  # Core classes
@@ -26,7 +26,7 @@ from .exceptions import (
26
26
  try:
27
27
  from .. import __version__
28
28
  except ImportError:
29
- __version__ = "0.2.88"
29
+ __version__ = "0.2.89"
30
30
 
31
31
  logger = logging.getLogger(__name__)
32
32
 
@@ -18,14 +18,16 @@ Usage:
18
18
  """
19
19
 
20
20
  import asyncio
21
+ import atexit
21
22
  import json
22
23
  import logging
23
24
  import os
25
+ import signal
24
26
  import sys
25
27
  import time
26
28
  from datetime import datetime
27
29
  from pathlib import Path
28
- from typing import Dict, List, Optional, Tuple
30
+ from typing import Dict, List, Optional, Set, Tuple
29
31
 
30
32
  import fleet
31
33
  from .utils import get_agent_path
@@ -33,10 +35,130 @@ from .types import AgentConfig, AgentResult, TaskResult
33
35
 
34
36
  logger = logging.getLogger(__name__)
35
37
 
38
+ # Global tracking of running containers for cleanup on exit
39
+ _running_containers: Set[str] = set()
40
+ _cleanup_registered = False
41
+
42
+
43
+ def _cleanup_all_containers():
44
+ """Kill all tracked running containers. Called on exit/signal."""
45
+ import subprocess
46
+
47
+ if not _running_containers:
48
+ return
49
+
50
+ containers = list(_running_containers)
51
+ logger.debug(f"Cleaning up {len(containers)} container(s)...")
52
+
53
+ for container_id in containers:
54
+ try:
55
+ # Use docker kill for immediate termination
56
+ subprocess.run(
57
+ ["docker", "kill", container_id],
58
+ stdout=subprocess.DEVNULL,
59
+ stderr=subprocess.DEVNULL,
60
+ timeout=5,
61
+ )
62
+ _running_containers.discard(container_id)
63
+ except Exception as e:
64
+ logger.debug(f"Failed to kill container {container_id[:12]}: {e}")
65
+
66
+
67
+ def _register_cleanup():
68
+ """Register cleanup handlers (only once)."""
69
+ global _cleanup_registered
70
+ if _cleanup_registered:
71
+ return
72
+ _cleanup_registered = True
73
+
74
+ # Register atexit handler
75
+ atexit.register(_cleanup_all_containers)
76
+
77
+ # Register signal handlers for graceful shutdown
78
+ def signal_handler(signum, frame):
79
+ _cleanup_all_containers()
80
+ # Re-raise to allow normal signal handling
81
+ signal.default_int_handler(signum, frame)
82
+
83
+ try:
84
+ signal.signal(signal.SIGINT, signal_handler)
85
+ signal.signal(signal.SIGTERM, signal_handler)
86
+ except (ValueError, OSError):
87
+ # Can't set signal handlers in some contexts (e.g., non-main thread)
88
+ pass
89
+
90
+
91
+ def _cleanup_orphaned_containers(image_prefix: str = "fleet-cua-"):
92
+ """Kill any orphaned containers from previous runs.
93
+
94
+ This handles cases where a previous run was force-killed (SIGKILL)
95
+ and containers were left running, which could cause port conflicts.
96
+ """
97
+ import subprocess
98
+
99
+ try:
100
+ # List running containers with our image prefix
101
+ result = subprocess.run(
102
+ [
103
+ "docker",
104
+ "ps",
105
+ "--filter",
106
+ f"ancestor={image_prefix}",
107
+ "-q",
108
+ "--format",
109
+ "{{.ID}} {{.Image}}",
110
+ ],
111
+ capture_output=True,
112
+ text=True,
113
+ timeout=10,
114
+ )
115
+
116
+ if result.returncode != 0:
117
+ return
118
+
119
+ # Parse container IDs - we need to filter by image name pattern
120
+ # Use docker ps with format to get both ID and image
121
+ result = subprocess.run(
122
+ ["docker", "ps", "--format", "{{.ID}}\t{{.Image}}"],
123
+ capture_output=True,
124
+ text=True,
125
+ timeout=10,
126
+ )
127
+
128
+ if result.returncode != 0 or not result.stdout.strip():
129
+ return
130
+
131
+ orphaned = []
132
+ for line in result.stdout.strip().split("\n"):
133
+ if not line:
134
+ continue
135
+ parts = line.split("\t")
136
+ if len(parts) >= 2:
137
+ container_id, image = parts[0], parts[1]
138
+ if image.startswith(image_prefix):
139
+ orphaned.append(container_id)
140
+
141
+ if orphaned:
142
+ logger.info(
143
+ f"Cleaning up {len(orphaned)} orphaned container(s) from previous run..."
144
+ )
145
+ for container_id in orphaned:
146
+ try:
147
+ subprocess.run(
148
+ ["docker", "kill", container_id],
149
+ stdout=subprocess.DEVNULL,
150
+ stderr=subprocess.DEVNULL,
151
+ timeout=5,
152
+ )
153
+ except Exception:
154
+ pass
155
+ except Exception as e:
156
+ logger.debug(f"Failed to check for orphaned containers: {e}")
157
+
36
158
 
37
159
  class AgentOrchestrator:
38
160
  """Orchestrates running agents on Fleet tasks."""
39
-
161
+
40
162
  def __init__(self, config: AgentConfig):
41
163
  self.config = config
42
164
  self._port_counter = config.port_range_start
@@ -45,7 +167,9 @@ class AgentOrchestrator:
45
167
  self._docker_image: Optional[str] = None
46
168
  # Track available ports (recycled when tasks complete)
47
169
  self._available_ports: List[Tuple[int, int]] = []
48
-
170
+ # Register global cleanup handlers
171
+ _register_cleanup()
172
+
49
173
  async def _get_next_ports(self) -> Tuple[int, int]:
50
174
  """Get next available MCP port and VNC port."""
51
175
  async with self._port_lock:
@@ -58,51 +182,63 @@ class AgentOrchestrator:
58
182
  self._port_counter += 1
59
183
  self._vnc_port_counter += 1
60
184
  return port, vnc_port
61
-
185
+
62
186
  async def _release_ports(self, port: int, vnc_port: int):
63
187
  """Return ports to the pool for reuse."""
64
188
  async with self._port_lock:
65
189
  self._available_ports.append((port, vnc_port))
66
-
190
+
67
191
  async def run(self) -> List[TaskResult]:
68
192
  """Run agents on all tasks."""
69
193
  from fleet._async import load_tasks
70
194
  from rich.console import Console
71
195
  from rich.live import Live
72
196
  from rich.spinner import Spinner
73
-
197
+
74
198
  console = Console()
75
-
199
+
76
200
  # Create job via Fleet API
77
- job_name = f"eval-{self.config.agent}-{datetime.now().strftime('%Y%m%d_%H%M%S')}"
201
+ job_name = (
202
+ f"eval-{self.config.agent}-{datetime.now().strftime('%Y%m%d_%H%M%S')}"
203
+ )
78
204
  self._job_id = await fleet.job_async(name=job_name)
79
205
  console.print(f"Job: https://fleetai.com/dashboard/jobs/{self._job_id}")
80
-
206
+
81
207
  # Create log directory: ~/.fleet/logs/{job_id}/
82
208
  self._log_dir = Path.home() / ".fleet" / "logs" / self._job_id
83
209
  self._log_dir.mkdir(parents=True, exist_ok=True)
84
-
210
+
85
211
  # Load tasks with spinner
86
- with Live(Spinner("dots", text=f"Loading tasks from {self.config.project_key}..."), console=console, transient=True):
212
+ with Live(
213
+ Spinner("dots", text=f"Loading tasks from {self.config.project_key}..."),
214
+ console=console,
215
+ transient=True,
216
+ ):
87
217
  if self.config.task_keys:
88
218
  tasks = await load_tasks(keys=self.config.task_keys)
89
219
  elif self.config.project_key:
90
220
  tasks = await load_tasks(project_key=self.config.project_key)
91
221
  else:
92
222
  raise ValueError("Either project_key or task_keys required")
93
-
223
+
94
224
  console.print(f"Loaded {len(tasks)} tasks")
95
-
225
+
96
226
  # Build Docker image
97
227
  agent_path = get_agent_path(self.config.agent)
98
228
  await self._build_docker_image(agent_path)
99
-
229
+
100
230
  # Run tasks with concurrency limit and progress
101
- from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
102
-
231
+ from rich.progress import (
232
+ Progress,
233
+ SpinnerColumn,
234
+ TextColumn,
235
+ BarColumn,
236
+ TaskProgressColumn,
237
+ )
238
+
103
239
  semaphore = asyncio.Semaphore(self.config.max_concurrent)
104
240
  results = [None] * len(tasks)
105
-
241
+
106
242
  with Progress(
107
243
  SpinnerColumn(),
108
244
  TextColumn("[progress.description]{task.description}"),
@@ -111,18 +247,18 @@ class AgentOrchestrator:
111
247
  console=console,
112
248
  ) as progress:
113
249
  task_progress = progress.add_task("Running tasks", total=len(tasks))
114
-
250
+
115
251
  async def run_with_semaphore(idx, task):
116
252
  async with semaphore:
117
253
  result = await self._run_task(task)
118
254
  progress.update(task_progress, advance=1)
119
255
  return idx, result
120
-
256
+
121
257
  completed = await asyncio.gather(
122
258
  *[run_with_semaphore(i, t) for i, t in enumerate(tasks)],
123
259
  return_exceptions=True,
124
260
  )
125
-
261
+
126
262
  # Convert to ordered list
127
263
  for item in completed:
128
264
  if isinstance(item, Exception):
@@ -130,74 +266,85 @@ class AgentOrchestrator:
130
266
  continue
131
267
  idx, result = item
132
268
  results[idx] = result
133
-
269
+
134
270
  # Fill any gaps with error results
135
271
  final = []
136
272
  for i, r in enumerate(results):
137
273
  if r is None:
138
- final.append(TaskResult(
139
- task_key=tasks[i].key,
140
- task_prompt=tasks[i].prompt,
141
- error="Task failed unexpectedly",
142
- ))
274
+ final.append(
275
+ TaskResult(
276
+ task_key=tasks[i].key,
277
+ task_prompt=tasks[i].prompt,
278
+ error="Task failed unexpectedly",
279
+ )
280
+ )
143
281
  else:
144
282
  final.append(r)
145
-
283
+
146
284
  # Show logs location
147
- if hasattr(self, '_log_dir') and self._log_dir.exists():
285
+ if hasattr(self, "_log_dir") and self._log_dir.exists():
148
286
  session_logs = list(self._log_dir.glob("*.jsonl"))
149
287
  console.print(f"Logs: {self._log_dir}/ ({len(session_logs)} sessions)")
150
-
288
+
151
289
  return final
152
-
290
+
153
291
  async def _build_docker_image(self, agent_path: Path):
154
292
  """Build Docker image for CUA server."""
155
293
  from rich.console import Console
156
294
  from rich.live import Live
157
295
  from rich.spinner import Spinner
158
-
296
+
159
297
  console = Console()
160
298
  dockerfile = agent_path / "Dockerfile"
161
299
  if not dockerfile.exists():
162
300
  raise FileNotFoundError(f"Dockerfile not found in {agent_path}")
163
-
301
+
164
302
  image_name = f"fleet-cua-{agent_path.name}"
165
-
303
+
304
+ # Clean up any orphaned containers from previous runs (prevents port conflicts)
305
+ _cleanup_orphaned_containers(image_name)
306
+
166
307
  # Build context is the agent directory (all files are self-contained)
167
- with Live(Spinner("dots", text=f"Building Docker image {image_name}..."), console=console, transient=True):
308
+ with Live(
309
+ Spinner("dots", text=f"Building Docker image {image_name}..."),
310
+ console=console,
311
+ transient=True,
312
+ ):
168
313
  proc = await asyncio.create_subprocess_exec(
169
- "docker", "build",
170
- "-t", image_name,
314
+ "docker",
315
+ "build",
316
+ "-t",
317
+ image_name,
171
318
  str(agent_path), # Build context is agent directory
172
319
  stdout=asyncio.subprocess.PIPE,
173
320
  stderr=asyncio.subprocess.PIPE,
174
321
  )
175
322
  stdout, stderr = await proc.communicate()
176
-
323
+
177
324
  if proc.returncode != 0:
178
325
  console.print(f"[red]✗[/red] Docker build failed")
179
326
  console.print(stderr.decode())
180
327
  raise RuntimeError(f"Docker build failed: {stderr.decode()}")
181
-
328
+
182
329
  self._docker_image = image_name
183
330
  console.print(f"Docker image ready: {image_name}")
184
-
331
+
185
332
  async def _run_task(self, task) -> TaskResult:
186
333
  """Run agent on a single task."""
187
334
  from fleet.env import make_async
188
-
335
+
189
336
  start = time.time()
190
337
  task_key = task.key
191
338
  task_prompt = task.prompt
192
339
  short_key = task_key[:20]
193
-
340
+
194
341
  logger.debug(f"[{short_key}] Starting")
195
-
342
+
196
343
  env = None
197
344
  container_id = None
198
345
  port = None
199
346
  vnc_port = None
200
-
347
+
201
348
  try:
202
349
  # 1. Create Fleet environment
203
350
  logger.debug(f"[{short_key}] Creating env...")
@@ -209,9 +356,9 @@ class AgentOrchestrator:
209
356
  )
210
357
  env_url = env.urls.root
211
358
  logger.debug(f"[{short_key}] Env: {env_url}")
212
-
359
+
213
360
  await asyncio.sleep(3) # Wait for env to be ready
214
-
361
+
215
362
  # 2. Start Docker container with CUA server
216
363
  port, vnc_port = await self._get_next_ports()
217
364
  logger.debug(f"[{short_key}] Starting container on port {port}...")
@@ -223,17 +370,17 @@ class AgentOrchestrator:
223
370
  task_key=task_key,
224
371
  )
225
372
  logger.debug(f"[{short_key}] Container: {container_id[:12]}")
226
-
373
+
227
374
  # Always show instance URL
228
375
  print(f"[{short_key}] Instance: {env_url}")
229
376
  if self.config.headful:
230
377
  print(f"[{short_key}] Browser: http://localhost:{vnc_port}/vnc.html")
231
-
378
+
232
379
  # Wait for server to be ready
233
380
  logger.debug(f"[{short_key}] Waiting for CUA server...")
234
381
  await self._wait_for_server(port)
235
382
  logger.debug(f"[{short_key}] CUA server ready")
236
-
383
+
237
384
  # 3. Run agent
238
385
  logger.debug(f"[{short_key}] Running agent...")
239
386
  agent_result = await self._run_agent(
@@ -242,13 +389,15 @@ class AgentOrchestrator:
242
389
  task_key=task_key,
243
390
  instance_id=env.instance_id,
244
391
  )
245
- logger.debug(f"[{short_key}] Agent done: completed={agent_result.completed}")
246
-
392
+ logger.debug(
393
+ f"[{short_key}] Agent done: completed={agent_result.completed}"
394
+ )
395
+
247
396
  # 4. Run verification
248
397
  verification_success = None
249
398
  verification_score = None
250
399
  verifier_execution_id = None
251
-
400
+
252
401
  if agent_result.completed and task.verifier:
253
402
  logger.info(f"[{task_key}] Running verification...")
254
403
  try:
@@ -259,25 +408,31 @@ class AgentOrchestrator:
259
408
  verification_success = v.success
260
409
  verifier_execution_id = v.execution_id
261
410
  # Score is in v.result (the verifier function's return value)
262
- verification_score = v.result if isinstance(v.result, (int, float)) else None
411
+ verification_score = (
412
+ v.result if isinstance(v.result, (int, float)) else None
413
+ )
263
414
  logger.info(f"[{task_key}] Verification: {verification_success}")
264
415
  except Exception as e:
265
416
  logger.error(f"[{task_key}] Verification error: {e}")
266
-
417
+
267
418
  # 5. Complete/fail session (session was created by agent, we just complete it)
268
- session_id = getattr(agent_result, 'session_id', None)
419
+ session_id = getattr(agent_result, "session_id", None)
269
420
  if session_id:
270
421
  try:
271
422
  # Create session object to complete it
272
423
  session = fleet.session_async(session_id=session_id)
273
424
  if verification_success:
274
- await session.complete(verifier_execution_id=verifier_execution_id)
425
+ await session.complete(
426
+ verifier_execution_id=verifier_execution_id
427
+ )
275
428
  else:
276
429
  await session.fail(verifier_execution_id=verifier_execution_id)
277
- logger.info(f"[{task_key}] Session: https://fleetai.com/dashboard/sessions/{session_id}")
430
+ logger.info(
431
+ f"[{task_key}] Session: https://fleetai.com/dashboard/sessions/{session_id}"
432
+ )
278
433
  except Exception as e:
279
434
  logger.error(f"[{task_key}] Session complete error: {e}")
280
-
435
+
281
436
  return TaskResult(
282
437
  task_key=task_key,
283
438
  task_prompt=task_prompt,
@@ -286,7 +441,7 @@ class AgentOrchestrator:
286
441
  verification_score=verification_score,
287
442
  execution_time_ms=int((time.time() - start) * 1000),
288
443
  )
289
-
444
+
290
445
  except Exception as e:
291
446
  logger.exception(f"[{short_key}] Failed: {e}")
292
447
  return TaskResult(
@@ -295,7 +450,7 @@ class AgentOrchestrator:
295
450
  error=str(e),
296
451
  execution_time_ms=int((time.time() - start) * 1000),
297
452
  )
298
-
453
+
299
454
  finally:
300
455
  # Cleanup
301
456
  if container_id:
@@ -307,7 +462,7 @@ class AgentOrchestrator:
307
462
  await env.close()
308
463
  except:
309
464
  pass
310
-
465
+
311
466
  async def _start_container(
312
467
  self,
313
468
  port: int,
@@ -318,62 +473,95 @@ class AgentOrchestrator:
318
473
  ) -> str:
319
474
  """Start Docker container with CUA server."""
320
475
  headless = "false" if self.config.headful else "true"
321
-
476
+
322
477
  cmd = [
323
- "docker", "run", "-d", "--rm",
324
- "-p", f"{port}:8765",
325
- "-e", f"FLEET_ENV_URL={env_url}",
326
- "-e", f"FLEET_TASK_PROMPT={task_prompt}",
327
- "-e", f"FLEET_TASK_KEY={task_key}",
328
- "-e", f"SCREEN_WIDTH={self.config.screen_width}",
329
- "-e", f"SCREEN_HEIGHT={self.config.screen_height}",
330
- "-e", f"HEADLESS={headless}",
478
+ "docker",
479
+ "run",
480
+ "-d",
481
+ "--rm",
482
+ "-p",
483
+ f"{port}:8765",
484
+ "-e",
485
+ f"FLEET_ENV_URL={env_url}",
486
+ "-e",
487
+ f"FLEET_TASK_PROMPT={task_prompt}",
488
+ "-e",
489
+ f"FLEET_TASK_KEY={task_key}",
490
+ "-e",
491
+ f"SCREEN_WIDTH={self.config.screen_width}",
492
+ "-e",
493
+ f"SCREEN_HEIGHT={self.config.screen_height}",
494
+ "-e",
495
+ f"HEADLESS={headless}",
331
496
  ]
332
-
497
+
333
498
  # Add noVNC port mapping if headful
334
499
  if self.config.headful:
335
500
  cmd.extend(["-p", f"{vnc_port}:6080"])
336
-
501
+
337
502
  cmd.append(self._docker_image)
338
-
503
+
339
504
  proc = await asyncio.create_subprocess_exec(
340
505
  *cmd,
341
506
  stdout=asyncio.subprocess.PIPE,
342
507
  stderr=asyncio.subprocess.PIPE,
343
508
  )
344
509
  stdout, stderr = await proc.communicate()
345
-
510
+
346
511
  if proc.returncode != 0:
347
- raise RuntimeError(f"Container start failed: {stderr.decode()}")
348
-
349
- return stdout.decode().strip()
350
-
512
+ stderr_str = stderr.decode()
513
+ # Check for port conflict
514
+ if (
515
+ "port is already allocated" in stderr_str
516
+ or "address already in use" in stderr_str.lower()
517
+ ):
518
+ raise RuntimeError(
519
+ f"Port conflict on {port} or {vnc_port}. Try again or check for orphaned containers with: docker ps"
520
+ )
521
+ raise RuntimeError(f"Container start failed: {stderr_str}")
522
+
523
+ container_id = stdout.decode().strip()
524
+
525
+ # Track container globally for cleanup on exit
526
+ _running_containers.add(container_id)
527
+
528
+ return container_id
529
+
351
530
  async def _stop_container(self, container_id: str):
352
531
  """Stop Docker container and capture logs."""
353
532
  # Get logs before stopping
354
533
  log_proc = await asyncio.create_subprocess_exec(
355
- "docker", "logs", "--tail", "50", container_id,
534
+ "docker",
535
+ "logs",
536
+ "--tail",
537
+ "50",
538
+ container_id,
356
539
  stdout=asyncio.subprocess.PIPE,
357
540
  stderr=asyncio.subprocess.STDOUT,
358
541
  )
359
542
  logs, _ = await log_proc.communicate()
360
543
  if logs:
361
544
  logger.debug(f"Container {container_id[:12]} logs:\n{logs.decode()}")
362
-
545
+
363
546
  proc = await asyncio.create_subprocess_exec(
364
- "docker", "stop", container_id,
547
+ "docker",
548
+ "stop",
549
+ container_id,
365
550
  stdout=asyncio.subprocess.DEVNULL,
366
551
  stderr=asyncio.subprocess.DEVNULL,
367
552
  )
368
553
  await proc.wait()
369
-
554
+
555
+ # Remove from global tracking
556
+ _running_containers.discard(container_id)
557
+
370
558
  async def _wait_for_server(self, port: int, timeout: int = 60):
371
559
  """Wait for CUA server to be ready."""
372
560
  import aiohttp
373
-
561
+
374
562
  url = f"http://localhost:{port}/health"
375
563
  start = time.time()
376
-
564
+
377
565
  while time.time() - start < timeout:
378
566
  try:
379
567
  async with aiohttp.ClientSession() as session:
@@ -383,9 +571,9 @@ class AgentOrchestrator:
383
571
  except:
384
572
  pass
385
573
  await asyncio.sleep(1)
386
-
574
+
387
575
  raise TimeoutError(f"CUA server not ready after {timeout}s")
388
-
576
+
389
577
  async def _run_agent(
390
578
  self,
391
579
  port: int,
@@ -396,35 +584,40 @@ class AgentOrchestrator:
396
584
  """Run agent process."""
397
585
  agent_path = get_agent_path(self.config.agent)
398
586
  agent_script = agent_path / "agent.py"
399
-
587
+
400
588
  # Set up environment
401
589
  env = os.environ.copy()
402
-
590
+
403
591
  # Session log file: ~/.fleet/logs/{job_id}/{task_key}.jsonl
404
592
  session_log_file = self._log_dir / f"{task_key}.jsonl"
405
-
406
- env.update({
407
- "FLEET_MCP_URL": f"http://localhost:{port}",
408
- "FLEET_SESSION_LOG": str(session_log_file), # Unified session log (MCP + HTTP)
409
- "FLEET_JOB_ID": self._job_id,
410
- "FLEET_TASK_PROMPT": task_prompt,
411
- "FLEET_TASK_KEY": task_key,
412
- "FLEET_INSTANCE_ID": instance_id or "",
413
- "FLEET_MODEL": self.config.model,
414
- "FLEET_MAX_STEPS": str(self.config.max_steps),
415
- "FLEET_SCREEN_WIDTH": str(self.config.screen_width),
416
- "FLEET_SCREEN_HEIGHT": str(self.config.screen_height),
417
- "FLEET_VERBOSE": "true" if self.config.verbose else "false",
418
- })
593
+
594
+ env.update(
595
+ {
596
+ "FLEET_MCP_URL": f"http://localhost:{port}",
597
+ "FLEET_SESSION_LOG": str(
598
+ session_log_file
599
+ ), # Unified session log (MCP + HTTP)
600
+ "FLEET_JOB_ID": self._job_id,
601
+ "FLEET_TASK_PROMPT": task_prompt,
602
+ "FLEET_TASK_KEY": task_key,
603
+ "FLEET_INSTANCE_ID": instance_id or "",
604
+ "FLEET_MODEL": self.config.model,
605
+ "FLEET_MAX_STEPS": str(self.config.max_steps),
606
+ "FLEET_SCREEN_WIDTH": str(self.config.screen_width),
607
+ "FLEET_SCREEN_HEIGHT": str(self.config.screen_height),
608
+ "FLEET_VERBOSE": "true" if self.config.verbose else "false",
609
+ }
610
+ )
419
611
  env.update(self.config.api_keys)
420
-
612
+
421
613
  proc = await asyncio.create_subprocess_exec(
422
- sys.executable, str(agent_script),
614
+ sys.executable,
615
+ str(agent_script),
423
616
  stdout=asyncio.subprocess.PIPE,
424
617
  stderr=asyncio.subprocess.PIPE,
425
618
  env=env,
426
619
  )
427
-
620
+
428
621
  try:
429
622
  stdout, stderr = await asyncio.wait_for(
430
623
  proc.communicate(),
@@ -438,11 +631,11 @@ class AgentOrchestrator:
438
631
  completed=False,
439
632
  error="Agent timeout",
440
633
  )
441
-
634
+
442
635
  # Parse result from stdout/stderr
443
636
  stdout_str = stdout.decode()
444
637
  stderr_str = stderr.decode()
445
-
638
+
446
639
  # Show full output in verbose mode
447
640
  if self.config.verbose:
448
641
  logger.info(f"Agent stdout:\n{stdout_str}")
@@ -452,13 +645,13 @@ class AgentOrchestrator:
452
645
  logger.debug(f"Agent stdout: {stdout_str[:500]}")
453
646
  if stderr_str:
454
647
  logger.debug(f"Agent stderr: {stderr_str[:500]}")
455
-
648
+
456
649
  # Always show stderr if agent crashed (non-zero exit or has stderr)
457
650
  if proc.returncode != 0 or stderr_str:
458
651
  short_key = task_key[:20]
459
652
  if stderr_str:
460
653
  print(f"[{short_key}] Agent stderr: {stderr_str[:500]}")
461
-
654
+
462
655
  result_json = None
463
656
  for line in stdout_str.split("\n"):
464
657
  line = line.strip()
@@ -467,7 +660,7 @@ class AgentOrchestrator:
467
660
  result_json = json.loads(line)
468
661
  except:
469
662
  continue
470
-
663
+
471
664
  if result_json:
472
665
  return AgentResult(
473
666
  task_key=result_json.get("task_key", task_key),
@@ -479,12 +672,12 @@ class AgentOrchestrator:
479
672
  transcript=result_json.get("transcript", []),
480
673
  session_id=result_json.get("session_id"),
481
674
  )
482
-
675
+
483
676
  # Include stderr in error message
484
677
  error_msg = f"Agent failed. stdout: {stdout_str[:300]}"
485
678
  if stderr_str:
486
679
  error_msg += f" | stderr: {stderr_str[:300]}"
487
-
680
+
488
681
  return AgentResult(
489
682
  task_key=task_key,
490
683
  completed=False,
@@ -505,7 +698,7 @@ async def run_agent(
505
698
  verbose: bool = False,
506
699
  ) -> List[TaskResult]:
507
700
  """Run agent on Fleet tasks.
508
-
701
+
509
702
  Args:
510
703
  project_key: Fleet project to run on
511
704
  task_keys: Specific tasks (alternative to project_key)
@@ -517,7 +710,7 @@ async def run_agent(
517
710
  api_keys: API keys (e.g., {"GEMINI_API_KEY": "xxx"})
518
711
  headful: Show browser via noVNC
519
712
  verbose: Enable verbose agent logging
520
-
713
+
521
714
  Returns:
522
715
  List of TaskResult
523
716
  """
@@ -533,7 +726,6 @@ async def run_agent(
533
726
  timeout_seconds=timeout_seconds,
534
727
  api_keys=api_keys or {},
535
728
  )
536
-
729
+
537
730
  orchestrator = AgentOrchestrator(config)
538
731
  return await orchestrator.run()
539
-
@@ -27,7 +27,7 @@ from .exceptions import (
27
27
  try:
28
28
  from . import __version__
29
29
  except ImportError:
30
- __version__ = "0.2.88"
30
+ __version__ = "0.2.89"
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.88
3
+ Version: 0.2.89
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "fleet-python"
7
7
 
8
- version = "0.2.88"
8
+ version = "0.2.89"
9
9
  description = "Python SDK for Fleet environments"
10
10
  authors = [
11
11
  {name = "Fleet AI", email = "nic@fleet.so"},
File without changes
File without changes
File without changes