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.
- {fleet_python-0.2.88/fleet_python.egg-info → fleet_python-0.2.89}/PKG-INFO +1 -1
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/__init__.py +1 -1
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/__init__.py +1 -1
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/base.py +1 -1
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/orchestrator.py +307 -115
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/base.py +1 -1
- {fleet_python-0.2.88 → fleet_python-0.2.89/fleet_python.egg-info}/PKG-INFO +1 -1
- {fleet_python-0.2.88 → fleet_python-0.2.89}/pyproject.toml +1 -1
- {fleet_python-0.2.88 → fleet_python-0.2.89}/LICENSE +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/README.md +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/diff_example.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/dsl_example.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/exampleResume.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_account.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_action_log.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_client.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_mcp_anthropic.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_mcp_openai.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_sync.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_task.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_tasks.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/example_verifier.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/export_tasks.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/fetch_tasks.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/gemini_example.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/import_tasks.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/iterate_verifiers.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/json_tasks_example.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/nova_act_example.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/openai_example.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/openai_simple_example.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/query_builder_example.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/quickstart.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/examples/test_cdp_logging.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/client.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/env/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/env/client.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/exceptions.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/global_client.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/instance/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/instance/base.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/instance/client.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/models.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/resources/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/resources/base.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/resources/browser.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/resources/mcp.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/resources/sqlite.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/tasks.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/verifiers/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/verifiers/bundler.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/_async/verifiers/verifier.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/Dockerfile +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/agent.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/mcp_server.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/playwright_utils.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/requirements.txt +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/gemini_cua/start.sh +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/types.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/agent/utils.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/cli.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/client.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/config.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/env/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/env/client.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/eval/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/eval/uploader.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/exceptions.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/global_client.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/instance/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/instance/base.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/instance/client.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/instance/models.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/models.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/proxy/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/proxy/proxy.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/proxy/whitelist.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/resources/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/resources/base.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/resources/browser.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/resources/mcp.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/resources/sqlite.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/tasks.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/types.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/utils/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/utils/http_logging.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/utils/logging.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/utils/playwright.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/bundler.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/code.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/db.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/decorator.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/parse.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/sql_differ.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet/verifiers/verifier.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet_python.egg-info/SOURCES.txt +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet_python.egg-info/entry_points.txt +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet_python.egg-info/requires.txt +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/fleet_python.egg-info/top_level.txt +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/scripts/fix_sync_imports.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/scripts/unasync.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/setup.cfg +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/__init__.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/test_app_method.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/test_expect_only.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/test_instance_dispatch.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/test_sqlite_resource_dual_mode.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/test_sqlite_shared_memory_behavior.py +0 -0
- {fleet_python-0.2.88 → fleet_python-0.2.89}/tests/test_verifier_from_string.py +0 -0
|
@@ -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 =
|
|
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(
|
|
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
|
|
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(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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,
|
|
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(
|
|
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",
|
|
170
|
-
"
|
|
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(
|
|
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 =
|
|
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,
|
|
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(
|
|
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(
|
|
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",
|
|
324
|
-
"
|
|
325
|
-
"-
|
|
326
|
-
"
|
|
327
|
-
"-
|
|
328
|
-
|
|
329
|
-
"-e",
|
|
330
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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",
|
|
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",
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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,
|
|
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
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|