plato-sdk-v2 2.3.10__py3-none-any.whl → 2.4.0__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.
- plato/agents/config.py +14 -6
- plato/agents/otel.py +16 -3
- plato/agents/runner.py +93 -24
- plato/v1/cli/chronos.py +531 -0
- plato/v1/cli/pm.py +3 -3
- plato/v1/cli/sandbox.py +53 -4
- plato/v1/cli/templates/world-runner.Dockerfile +27 -0
- plato/worlds/base.py +66 -1
- plato/worlds/runner.py +1 -458
- {plato_sdk_v2-2.3.10.dist-info → plato_sdk_v2-2.4.0.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.3.10.dist-info → plato_sdk_v2-2.4.0.dist-info}/RECORD +13 -12
- {plato_sdk_v2-2.3.10.dist-info → plato_sdk_v2-2.4.0.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.3.10.dist-info → plato_sdk_v2-2.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# World runner image for plato chronos dev
|
|
2
|
+
# Includes git, docker CLI, and Python dependencies
|
|
3
|
+
|
|
4
|
+
FROM python:3.12-slim
|
|
5
|
+
|
|
6
|
+
# Install git and docker CLI
|
|
7
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
8
|
+
git \
|
|
9
|
+
curl \
|
|
10
|
+
ca-certificates \
|
|
11
|
+
&& curl -fsSL https://get.docker.com -o get-docker.sh \
|
|
12
|
+
&& sh get-docker.sh \
|
|
13
|
+
&& rm get-docker.sh \
|
|
14
|
+
&& apt-get clean \
|
|
15
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
16
|
+
|
|
17
|
+
# Install uv for fast package installation
|
|
18
|
+
RUN pip install --no-cache-dir uv
|
|
19
|
+
|
|
20
|
+
WORKDIR /world
|
|
21
|
+
|
|
22
|
+
# Entry point expects:
|
|
23
|
+
# - /world mounted with world source
|
|
24
|
+
# - /python-sdk mounted with plato SDK source (optional, for dev)
|
|
25
|
+
# - /config.json mounted with config
|
|
26
|
+
# - WORLD_NAME env var set
|
|
27
|
+
CMD ["bash", "-c", "if [ -d /python-sdk ]; then uv pip install --system /python-sdk; fi && uv pip install --system . 2>/dev/null || pip install -q . && plato-world-runner run --world $WORLD_NAME --config /config.json"]
|
plato/worlds/base.py
CHANGED
|
@@ -25,6 +25,7 @@ from plato.agents.otel import (
|
|
|
25
25
|
init_tracing,
|
|
26
26
|
shutdown_tracing,
|
|
27
27
|
)
|
|
28
|
+
from plato.agents.runner import run_agent as _run_agent_raw
|
|
28
29
|
|
|
29
30
|
logger = logging.getLogger(__name__)
|
|
30
31
|
|
|
@@ -126,6 +127,7 @@ class BaseWorld(ABC, Generic[ConfigT]):
|
|
|
126
127
|
self.plato_session = None
|
|
127
128
|
self._current_step_id: str | None = None
|
|
128
129
|
self._session_id: str | None = None
|
|
130
|
+
self._agent_containers: list[str] = [] # Track spawned agent containers for cleanup
|
|
129
131
|
|
|
130
132
|
@classmethod
|
|
131
133
|
def get_config_class(cls) -> type[RunConfig]:
|
|
@@ -185,7 +187,70 @@ class BaseWorld(ABC, Generic[ConfigT]):
|
|
|
185
187
|
|
|
186
188
|
async def close(self) -> None:
|
|
187
189
|
"""Cleanup resources. Called after run completes."""
|
|
188
|
-
|
|
190
|
+
await self._cleanup_agent_containers()
|
|
191
|
+
|
|
192
|
+
async def _cleanup_agent_containers(self) -> None:
|
|
193
|
+
"""Stop any agent containers spawned by this world."""
|
|
194
|
+
import asyncio
|
|
195
|
+
|
|
196
|
+
if not self._agent_containers:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
self.logger.info(f"Stopping {len(self._agent_containers)} agent container(s)...")
|
|
200
|
+
for container_name in self._agent_containers:
|
|
201
|
+
try:
|
|
202
|
+
proc = await asyncio.create_subprocess_exec(
|
|
203
|
+
"docker",
|
|
204
|
+
"stop",
|
|
205
|
+
container_name,
|
|
206
|
+
stdout=asyncio.subprocess.DEVNULL,
|
|
207
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
208
|
+
)
|
|
209
|
+
await proc.wait()
|
|
210
|
+
self.logger.debug(f"Stopped container: {container_name}")
|
|
211
|
+
except Exception as e:
|
|
212
|
+
self.logger.warning(f"Failed to stop container {container_name}: {e}")
|
|
213
|
+
self._agent_containers.clear()
|
|
214
|
+
self.logger.info("Agent containers stopped")
|
|
215
|
+
|
|
216
|
+
async def run_agent(
|
|
217
|
+
self,
|
|
218
|
+
image: str,
|
|
219
|
+
config: dict,
|
|
220
|
+
secrets: dict[str, str],
|
|
221
|
+
instruction: str,
|
|
222
|
+
workspace: str | None = None,
|
|
223
|
+
logs_dir: str | None = None,
|
|
224
|
+
pull: bool = True,
|
|
225
|
+
) -> str:
|
|
226
|
+
"""Run an agent in a Docker container, tracking the container for cleanup.
|
|
227
|
+
|
|
228
|
+
This is a wrapper around plato.agents.runner.run_agent that automatically
|
|
229
|
+
tracks spawned containers so they can be cleaned up when the world closes.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
image: Docker image URI
|
|
233
|
+
config: Agent configuration dict
|
|
234
|
+
secrets: Secret values (API keys, etc.)
|
|
235
|
+
instruction: Task instruction for the agent
|
|
236
|
+
workspace: Docker volume name for workspace
|
|
237
|
+
logs_dir: Ignored (kept for backwards compatibility)
|
|
238
|
+
pull: Whether to pull the image first
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
The container name that was created
|
|
242
|
+
"""
|
|
243
|
+
container_name = await _run_agent_raw(
|
|
244
|
+
image=image,
|
|
245
|
+
config=config,
|
|
246
|
+
secrets=secrets,
|
|
247
|
+
instruction=instruction,
|
|
248
|
+
workspace=workspace,
|
|
249
|
+
logs_dir=logs_dir,
|
|
250
|
+
pull=pull,
|
|
251
|
+
)
|
|
252
|
+
self._agent_containers.append(container_name)
|
|
253
|
+
return container_name
|
|
189
254
|
|
|
190
255
|
async def _connect_plato_session(self) -> None:
|
|
191
256
|
"""Connect to Plato session from config.
|
plato/worlds/runner.py
CHANGED
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
import json
|
|
7
6
|
import logging
|
|
8
|
-
import os
|
|
9
7
|
from pathlib import Path
|
|
10
8
|
from typing import Annotated
|
|
11
9
|
|
|
12
10
|
import typer
|
|
13
11
|
|
|
14
|
-
from plato.worlds.config import
|
|
12
|
+
from plato.worlds.config import RunConfig
|
|
15
13
|
|
|
16
14
|
app = typer.Typer(
|
|
17
15
|
name="plato-world-runner",
|
|
@@ -135,461 +133,6 @@ def list_worlds(
|
|
|
135
133
|
typer.echo(f" {name} (v{version}): {desc}")
|
|
136
134
|
|
|
137
135
|
|
|
138
|
-
def _get_docker_platform() -> str:
|
|
139
|
-
"""Get the appropriate Docker platform for the current system.
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
Docker platform string (e.g., "linux/arm64" or "linux/amd64")
|
|
143
|
-
"""
|
|
144
|
-
import platform as plat
|
|
145
|
-
|
|
146
|
-
system = plat.system()
|
|
147
|
-
machine = plat.machine().lower()
|
|
148
|
-
|
|
149
|
-
# On macOS with Apple Silicon (arm64/aarch64), use linux/arm64
|
|
150
|
-
if system == "Darwin" and machine in ("arm64", "aarch64"):
|
|
151
|
-
return "linux/arm64"
|
|
152
|
-
# On Linux ARM
|
|
153
|
-
elif system == "Linux" and machine in ("arm64", "aarch64"):
|
|
154
|
-
return "linux/arm64"
|
|
155
|
-
# Default to amd64 for x86_64 or other architectures
|
|
156
|
-
else:
|
|
157
|
-
return "linux/amd64"
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
async def _build_agent_image(
|
|
161
|
-
agent_name: str,
|
|
162
|
-
agents_dir: Path,
|
|
163
|
-
plato_client_root: Path | None = None,
|
|
164
|
-
) -> bool:
|
|
165
|
-
"""Build a local agent Docker image.
|
|
166
|
-
|
|
167
|
-
Args:
|
|
168
|
-
agent_name: Name of the agent (e.g., "openhands")
|
|
169
|
-
agents_dir: Directory containing agent subdirectories
|
|
170
|
-
plato_client_root: Root of plato-client repo (for dev builds), or None for prod builds
|
|
171
|
-
|
|
172
|
-
Returns:
|
|
173
|
-
True if build succeeded, False otherwise
|
|
174
|
-
"""
|
|
175
|
-
import subprocess
|
|
176
|
-
|
|
177
|
-
# Resolve paths to absolute
|
|
178
|
-
agents_dir = agents_dir.expanduser().resolve()
|
|
179
|
-
agent_path = agents_dir / agent_name
|
|
180
|
-
dockerfile_path = agent_path / "Dockerfile"
|
|
181
|
-
|
|
182
|
-
if not dockerfile_path.exists():
|
|
183
|
-
logger.warning(f"No Dockerfile found for agent '{agent_name}' at {dockerfile_path}")
|
|
184
|
-
return False
|
|
185
|
-
|
|
186
|
-
image_tag = f"{agent_name}:latest"
|
|
187
|
-
|
|
188
|
-
# Determine build context and target
|
|
189
|
-
if plato_client_root:
|
|
190
|
-
plato_client_root = plato_client_root.expanduser().resolve()
|
|
191
|
-
|
|
192
|
-
if plato_client_root and plato_client_root.exists():
|
|
193
|
-
# Dev build from plato-client root (includes local python-sdk)
|
|
194
|
-
build_context = str(plato_client_root)
|
|
195
|
-
dockerfile_abs = str(dockerfile_path)
|
|
196
|
-
target = "dev"
|
|
197
|
-
logger.info(f"Building {image_tag} (dev mode from {build_context})...")
|
|
198
|
-
else:
|
|
199
|
-
# Prod build from agent directory
|
|
200
|
-
build_context = str(agent_path)
|
|
201
|
-
dockerfile_abs = str(dockerfile_path)
|
|
202
|
-
target = "prod"
|
|
203
|
-
logger.info(f"Building {image_tag} (prod mode from {build_context})...")
|
|
204
|
-
|
|
205
|
-
# Detect platform for ARM Mac support
|
|
206
|
-
docker_platform = _get_docker_platform()
|
|
207
|
-
logger.info(f"Building for platform: {docker_platform}")
|
|
208
|
-
|
|
209
|
-
cmd = [
|
|
210
|
-
"docker",
|
|
211
|
-
"build",
|
|
212
|
-
"--platform",
|
|
213
|
-
docker_platform,
|
|
214
|
-
"--build-arg",
|
|
215
|
-
f"PLATFORM={docker_platform}",
|
|
216
|
-
"--target",
|
|
217
|
-
target,
|
|
218
|
-
"-t",
|
|
219
|
-
image_tag,
|
|
220
|
-
"-f",
|
|
221
|
-
dockerfile_abs,
|
|
222
|
-
]
|
|
223
|
-
|
|
224
|
-
cmd.append(build_context)
|
|
225
|
-
|
|
226
|
-
logger.debug(f"Build command: {' '.join(cmd)}")
|
|
227
|
-
|
|
228
|
-
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
229
|
-
|
|
230
|
-
if result.returncode != 0:
|
|
231
|
-
logger.error(f"Failed to build {image_tag}:\n{result.stderr}")
|
|
232
|
-
return False
|
|
233
|
-
|
|
234
|
-
logger.info(f"Successfully built {image_tag}")
|
|
235
|
-
return True
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def _extract_agent_images_from_config(config_data: dict) -> list[str]:
|
|
239
|
-
"""Extract agent image names from config data.
|
|
240
|
-
|
|
241
|
-
Args:
|
|
242
|
-
config_data: Raw config dictionary
|
|
243
|
-
|
|
244
|
-
Returns:
|
|
245
|
-
List of image names (without tags) that are local (not from a registry)
|
|
246
|
-
"""
|
|
247
|
-
images = []
|
|
248
|
-
|
|
249
|
-
# Check agents section
|
|
250
|
-
agents = config_data.get("agents", {})
|
|
251
|
-
for agent_config in agents.values():
|
|
252
|
-
if isinstance(agent_config, dict):
|
|
253
|
-
image = agent_config.get("image", "")
|
|
254
|
-
# Only include local images (no registry prefix like ghcr.io/)
|
|
255
|
-
if image and "/" not in image.split(":")[0]:
|
|
256
|
-
# Extract name without tag
|
|
257
|
-
name = image.split(":")[0]
|
|
258
|
-
if name not in images:
|
|
259
|
-
images.append(name)
|
|
260
|
-
|
|
261
|
-
# Also check direct coder/verifier fields
|
|
262
|
-
for field in ["coder", "verifier"]:
|
|
263
|
-
agent_config = config_data.get(field, {})
|
|
264
|
-
if isinstance(agent_config, dict):
|
|
265
|
-
image = agent_config.get("image", "")
|
|
266
|
-
if image and "/" not in image.split(":")[0]:
|
|
267
|
-
name = image.split(":")[0]
|
|
268
|
-
if name not in images:
|
|
269
|
-
images.append(name)
|
|
270
|
-
|
|
271
|
-
return images
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
class ChronosSessionInfo:
|
|
275
|
-
"""Info returned from Chronos session creation."""
|
|
276
|
-
|
|
277
|
-
def __init__(self, public_id: str, otel_url: str, upload_url: str):
|
|
278
|
-
self.public_id = public_id
|
|
279
|
-
self.otel_url = otel_url
|
|
280
|
-
self.upload_url = upload_url
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
async def _create_chronos_session(
|
|
284
|
-
chronos_url: str,
|
|
285
|
-
api_key: str,
|
|
286
|
-
world_name: str,
|
|
287
|
-
world_config: dict,
|
|
288
|
-
plato_session_id: str | None = None,
|
|
289
|
-
) -> ChronosSessionInfo:
|
|
290
|
-
"""Create a session in Chronos.
|
|
291
|
-
|
|
292
|
-
Args:
|
|
293
|
-
chronos_url: Chronos base URL (e.g., https://chronos.plato.so)
|
|
294
|
-
api_key: Plato API key for authentication
|
|
295
|
-
world_name: Name of the world being run
|
|
296
|
-
world_config: World configuration dict
|
|
297
|
-
plato_session_id: Optional Plato session ID if already created
|
|
298
|
-
|
|
299
|
-
Returns:
|
|
300
|
-
ChronosSessionInfo with session_id, otel_url, and upload_url
|
|
301
|
-
"""
|
|
302
|
-
import httpx
|
|
303
|
-
|
|
304
|
-
url = f"{chronos_url.rstrip('/')}/api/sessions"
|
|
305
|
-
|
|
306
|
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
307
|
-
response = await client.post(
|
|
308
|
-
url,
|
|
309
|
-
json={
|
|
310
|
-
"world_name": world_name,
|
|
311
|
-
"world_config": world_config,
|
|
312
|
-
"plato_session_id": plato_session_id,
|
|
313
|
-
},
|
|
314
|
-
headers={"x-api-key": api_key},
|
|
315
|
-
)
|
|
316
|
-
response.raise_for_status()
|
|
317
|
-
data = response.json()
|
|
318
|
-
|
|
319
|
-
print(f"[Runner] Chronos session response: {data}")
|
|
320
|
-
return ChronosSessionInfo(
|
|
321
|
-
public_id=data["public_id"],
|
|
322
|
-
otel_url=data["otel_url"],
|
|
323
|
-
upload_url=data["upload_url"],
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
async def _close_chronos_session(
|
|
328
|
-
chronos_url: str,
|
|
329
|
-
api_key: str,
|
|
330
|
-
session_id: str,
|
|
331
|
-
) -> None:
|
|
332
|
-
"""Close a Chronos session.
|
|
333
|
-
|
|
334
|
-
Args:
|
|
335
|
-
chronos_url: Chronos base URL
|
|
336
|
-
api_key: Plato API key for authentication
|
|
337
|
-
session_id: Chronos session public ID to close
|
|
338
|
-
"""
|
|
339
|
-
import httpx
|
|
340
|
-
|
|
341
|
-
url = f"{chronos_url.rstrip('/')}/api/sessions/{session_id}/close"
|
|
342
|
-
|
|
343
|
-
try:
|
|
344
|
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
345
|
-
response = await client.post(
|
|
346
|
-
url,
|
|
347
|
-
headers={"x-api-key": api_key},
|
|
348
|
-
)
|
|
349
|
-
response.raise_for_status()
|
|
350
|
-
logger.info(f"Closed Chronos session: {session_id}")
|
|
351
|
-
except Exception as e:
|
|
352
|
-
logger.warning(f"Failed to close Chronos session: {e}")
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
async def _run_dev(
|
|
356
|
-
world_name: str,
|
|
357
|
-
config_path: Path,
|
|
358
|
-
env_timeout: int = 7200,
|
|
359
|
-
agents_dir: Path | None = None,
|
|
360
|
-
) -> None:
|
|
361
|
-
"""Run a world locally with automatic environment creation.
|
|
362
|
-
|
|
363
|
-
This mimics what Chronos does but runs locally for debugging:
|
|
364
|
-
1. Load and parse the config
|
|
365
|
-
2. Build local agent images if --agents-dir is provided
|
|
366
|
-
3. Create Plato session with all environments
|
|
367
|
-
4. Create Chronos session for OTel traces
|
|
368
|
-
5. Run the world with the session attached
|
|
369
|
-
|
|
370
|
-
Requires environment variables:
|
|
371
|
-
CHRONOS_URL: Chronos base URL (e.g., https://chronos.plato.so)
|
|
372
|
-
PLATO_API_KEY: API key for Plato and Chronos authentication
|
|
373
|
-
|
|
374
|
-
Args:
|
|
375
|
-
world_name: Name of the world to run
|
|
376
|
-
config_path: Path to the config JSON file
|
|
377
|
-
env_timeout: Timeout for environment creation (seconds)
|
|
378
|
-
agents_dir: Optional directory containing agent source code
|
|
379
|
-
"""
|
|
380
|
-
from plato.v2 import AsyncPlato
|
|
381
|
-
from plato.worlds.base import get_world
|
|
382
|
-
|
|
383
|
-
# Get required env vars
|
|
384
|
-
chronos_url = os.environ.get("CHRONOS_URL")
|
|
385
|
-
api_key = os.environ.get("PLATO_API_KEY")
|
|
386
|
-
|
|
387
|
-
if not chronos_url:
|
|
388
|
-
raise ValueError("CHRONOS_URL environment variable is required")
|
|
389
|
-
if not api_key:
|
|
390
|
-
raise ValueError("PLATO_API_KEY environment variable is required")
|
|
391
|
-
|
|
392
|
-
discover_worlds()
|
|
393
|
-
|
|
394
|
-
world_cls = get_world(world_name)
|
|
395
|
-
if world_cls is None:
|
|
396
|
-
from plato.worlds.base import get_registered_worlds
|
|
397
|
-
|
|
398
|
-
available = list(get_registered_worlds().keys())
|
|
399
|
-
raise ValueError(f"World '{world_name}' not found. Available: {available}")
|
|
400
|
-
|
|
401
|
-
# Load config
|
|
402
|
-
config_class = world_cls.get_config_class()
|
|
403
|
-
with open(config_path) as f:
|
|
404
|
-
config_data = json.load(f)
|
|
405
|
-
|
|
406
|
-
# Parse the config to get typed access
|
|
407
|
-
run_config = config_class._from_dict(config_data.copy())
|
|
408
|
-
|
|
409
|
-
# Build local agent images if agents_dir is provided
|
|
410
|
-
if agents_dir:
|
|
411
|
-
# Resolve agents_dir to absolute path
|
|
412
|
-
agents_dir = agents_dir.expanduser().resolve()
|
|
413
|
-
agent_images = _extract_agent_images_from_config(config_data)
|
|
414
|
-
if agent_images:
|
|
415
|
-
logger.info(f"Building local agent images: {agent_images}")
|
|
416
|
-
# Determine if we're in a plato-client repo for dev builds
|
|
417
|
-
# (agents_dir is something like /path/to/plato-client/agents)
|
|
418
|
-
plato_client_root = agents_dir.parent if agents_dir.name == "agents" else None
|
|
419
|
-
for agent_name in agent_images:
|
|
420
|
-
success = await _build_agent_image(agent_name, agents_dir, plato_client_root)
|
|
421
|
-
if not success:
|
|
422
|
-
raise RuntimeError(f"Failed to build agent image: {agent_name}")
|
|
423
|
-
else:
|
|
424
|
-
logger.info("No local agent images found in config")
|
|
425
|
-
|
|
426
|
-
# Get environment configs from the parsed config
|
|
427
|
-
env_configs: list[EnvConfig] = run_config.get_envs()
|
|
428
|
-
|
|
429
|
-
# Create Plato client
|
|
430
|
-
plato = AsyncPlato()
|
|
431
|
-
session = None
|
|
432
|
-
plato_session_id: str | None = None
|
|
433
|
-
chronos_session_id: str | None = None
|
|
434
|
-
|
|
435
|
-
try:
|
|
436
|
-
if env_configs:
|
|
437
|
-
logger.info(f"Creating {len(env_configs)} environments...")
|
|
438
|
-
session = await plato.sessions.create(envs=env_configs, timeout=env_timeout)
|
|
439
|
-
plato_session_id = session.session_id
|
|
440
|
-
logger.info(f"Created Plato session: {plato_session_id}")
|
|
441
|
-
logger.info(f"Environments: {[e.alias for e in session.envs]}")
|
|
442
|
-
|
|
443
|
-
# Serialize and add to config
|
|
444
|
-
serialized = session.dump()
|
|
445
|
-
run_config.plato_session = serialized
|
|
446
|
-
else:
|
|
447
|
-
logger.info("No environments defined for this world")
|
|
448
|
-
|
|
449
|
-
# Create Chronos session (after Plato session so we can link them)
|
|
450
|
-
logger.info(f"Creating Chronos session at {chronos_url}...")
|
|
451
|
-
chronos_session = await _create_chronos_session(
|
|
452
|
-
chronos_url=chronos_url,
|
|
453
|
-
api_key=api_key,
|
|
454
|
-
world_name=world_name,
|
|
455
|
-
world_config=config_data,
|
|
456
|
-
plato_session_id=plato_session_id,
|
|
457
|
-
)
|
|
458
|
-
chronos_session_id = chronos_session.public_id
|
|
459
|
-
logger.info(f"Created Chronos session: {chronos_session_id}")
|
|
460
|
-
logger.info(f"View at: {chronos_url}/sessions/{chronos_session_id}")
|
|
461
|
-
|
|
462
|
-
# Update run_config with session info from Chronos
|
|
463
|
-
run_config.session_id = chronos_session_id
|
|
464
|
-
# Use base chronos URL for OTEL endpoint (more reliable than session-provided URL)
|
|
465
|
-
run_config.otel_url = f"{chronos_url.rstrip('/')}/api/otel"
|
|
466
|
-
run_config.upload_url = chronos_session.upload_url
|
|
467
|
-
|
|
468
|
-
# Run the world
|
|
469
|
-
logger.info(f"Starting world '{world_name}'...")
|
|
470
|
-
world_instance = world_cls()
|
|
471
|
-
await world_instance.run(run_config)
|
|
472
|
-
|
|
473
|
-
finally:
|
|
474
|
-
# Cleanup
|
|
475
|
-
if session:
|
|
476
|
-
logger.info("Closing Plato session...")
|
|
477
|
-
await session.close()
|
|
478
|
-
await plato.close()
|
|
479
|
-
|
|
480
|
-
# Close Chronos session
|
|
481
|
-
if chronos_session_id:
|
|
482
|
-
await _close_chronos_session(chronos_url, api_key, chronos_session_id)
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
def _setup_colored_logging(verbose: bool = False) -> None:
|
|
486
|
-
"""Setup colored logging with filtered noisy loggers."""
|
|
487
|
-
log_level = logging.DEBUG if verbose else logging.INFO
|
|
488
|
-
|
|
489
|
-
# Define colors for different log levels
|
|
490
|
-
colors = {
|
|
491
|
-
"DEBUG": "\033[36m", # Cyan
|
|
492
|
-
"INFO": "\033[32m", # Green
|
|
493
|
-
"WARNING": "\033[33m", # Yellow
|
|
494
|
-
"ERROR": "\033[31m", # Red
|
|
495
|
-
"CRITICAL": "\033[35m", # Magenta
|
|
496
|
-
}
|
|
497
|
-
reset = "\033[0m"
|
|
498
|
-
|
|
499
|
-
class ColoredFormatter(logging.Formatter):
|
|
500
|
-
def format(self, record: logging.LogRecord) -> str:
|
|
501
|
-
color = colors.get(record.levelname, "")
|
|
502
|
-
record.levelname = f"{color}{record.levelname}{reset}"
|
|
503
|
-
record.name = f"\033[34m{record.name}{reset}" # Blue for logger name
|
|
504
|
-
return super().format(record)
|
|
505
|
-
|
|
506
|
-
# Create handler with colored formatter
|
|
507
|
-
handler = logging.StreamHandler()
|
|
508
|
-
handler.setFormatter(
|
|
509
|
-
ColoredFormatter(
|
|
510
|
-
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
511
|
-
datefmt="%H:%M:%S",
|
|
512
|
-
)
|
|
513
|
-
)
|
|
514
|
-
|
|
515
|
-
# Configure root logger
|
|
516
|
-
root_logger = logging.getLogger()
|
|
517
|
-
root_logger.setLevel(log_level)
|
|
518
|
-
root_logger.handlers = [handler]
|
|
519
|
-
|
|
520
|
-
# Silence noisy HTTP loggers
|
|
521
|
-
for noisy_logger in ["httpcore", "httpx", "urllib3", "hpack"]:
|
|
522
|
-
logging.getLogger(noisy_logger).setLevel(logging.WARNING)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
@app.command("dev")
|
|
526
|
-
def dev(
|
|
527
|
-
world: Annotated[str, typer.Option("--world", "-w", help="World name to run")],
|
|
528
|
-
config: Annotated[Path, typer.Option("--config", "-c", help="Path to config JSON file")],
|
|
529
|
-
env_timeout: Annotated[
|
|
530
|
-
int,
|
|
531
|
-
typer.Option("--env-timeout", help="Timeout for environment creation (seconds)"),
|
|
532
|
-
] = 7200,
|
|
533
|
-
agents_dir: Annotated[
|
|
534
|
-
Path | None,
|
|
535
|
-
typer.Option(
|
|
536
|
-
"--agents-dir",
|
|
537
|
-
"-a",
|
|
538
|
-
help="Directory containing agent source code (builds local images)",
|
|
539
|
-
),
|
|
540
|
-
] = None,
|
|
541
|
-
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
|
|
542
|
-
) -> None:
|
|
543
|
-
"""Run a world locally for development/debugging.
|
|
544
|
-
|
|
545
|
-
This creates Plato environments automatically (like Chronos does),
|
|
546
|
-
creates a Chronos session for logging, and runs the world.
|
|
547
|
-
|
|
548
|
-
Example config.json:
|
|
549
|
-
{
|
|
550
|
-
"instruction": "Create a git repo and upload files to S3",
|
|
551
|
-
"coder": {
|
|
552
|
-
"image": "openhands:latest",
|
|
553
|
-
"config": {"model_name": "gemini/gemini-3-flash-preview"}
|
|
554
|
-
},
|
|
555
|
-
"secrets": {
|
|
556
|
-
"gemini_api_key": "..."
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
Required environment variables:
|
|
561
|
-
CHRONOS_URL: Chronos base URL (e.g., https://chronos.plato.so)
|
|
562
|
-
PLATO_API_KEY: API key for Plato and Chronos authentication
|
|
563
|
-
|
|
564
|
-
Examples:
|
|
565
|
-
# Basic usage
|
|
566
|
-
CHRONOS_URL=https://chronos.plato.so plato-world-runner dev -w code -c config.json
|
|
567
|
-
|
|
568
|
-
# With local agent builds (from plato-client repo)
|
|
569
|
-
plato-world-runner dev -w code -c config.json --agents-dir ~/plato-client/agents
|
|
570
|
-
"""
|
|
571
|
-
# Setup colored logging with filtered noisy loggers
|
|
572
|
-
_setup_colored_logging(verbose)
|
|
573
|
-
|
|
574
|
-
if not config.exists():
|
|
575
|
-
typer.echo(f"Error: Config file not found: {config}", err=True)
|
|
576
|
-
raise typer.Exit(1)
|
|
577
|
-
|
|
578
|
-
if not os.environ.get("CHRONOS_URL"):
|
|
579
|
-
typer.echo("Error: CHRONOS_URL environment variable required", err=True)
|
|
580
|
-
raise typer.Exit(1)
|
|
581
|
-
|
|
582
|
-
if not os.environ.get("PLATO_API_KEY"):
|
|
583
|
-
typer.echo("Error: PLATO_API_KEY environment variable required", err=True)
|
|
584
|
-
raise typer.Exit(1)
|
|
585
|
-
|
|
586
|
-
try:
|
|
587
|
-
asyncio.run(_run_dev(world, config, env_timeout, agents_dir))
|
|
588
|
-
except Exception as e:
|
|
589
|
-
logger.exception(f"World execution failed: {e}")
|
|
590
|
-
raise typer.Exit(1)
|
|
591
|
-
|
|
592
|
-
|
|
593
136
|
def main() -> None:
|
|
594
137
|
"""CLI entry point."""
|
|
595
138
|
app()
|
|
@@ -301,9 +301,9 @@ plato/agents/__init__.py,sha256=Cxc-HUMwRGQ4D1hHnFo9vt2AV5upPRYP4e3y8X6Hzr0,3052
|
|
|
301
301
|
plato/agents/artifacts.py,sha256=ljeI0wzsp7Q6uKqMb-k7kTb680Vizs54ohtM-d7zvOg,2929
|
|
302
302
|
plato/agents/base.py,sha256=vUbPQuNSo6Ka2lIB_ZOXgi4EoAjtAD7GIj9LnNotam0,4577
|
|
303
303
|
plato/agents/build.py,sha256=CNMbVQFs2_pYit1dA29Davve28Yi4c7TNK9wBB7odrE,1621
|
|
304
|
-
plato/agents/config.py,sha256=
|
|
305
|
-
plato/agents/otel.py,sha256=
|
|
306
|
-
plato/agents/runner.py,sha256=
|
|
304
|
+
plato/agents/config.py,sha256=GWXEAbruNVI2q3XIWpQ9vGLK2wGhsFPYA-oekmAlrg8,5392
|
|
305
|
+
plato/agents/otel.py,sha256=A2LkkjBtjSe0eztr9UvYvSUOwfmShCgPg3OUN8nOyIo,9159
|
|
306
|
+
plato/agents/runner.py,sha256=piow_uO1eymreRMU-TX8tcIOn6aurafpjA5ptSR9JPM,9211
|
|
307
307
|
plato/agents/trajectory.py,sha256=WdiBmua0KvCrNaM3qgPI7-7B4xmSkfbP4oZ_9_8qHzU,10529
|
|
308
308
|
plato/chronos/__init__.py,sha256=RHMvSrQS_-vkKOyTRuAkp2gKDP1HEuBLDnw8jcZs1Jg,739
|
|
309
309
|
plato/chronos/client.py,sha256=YcOGtHWERyOD9z8LKt8bRMVL0cEwL2hiAP4qQgdZlUI,5495
|
|
@@ -387,14 +387,15 @@ plato/v1/sync_flow_executor.py,sha256=kgvNYOtA9FHeNfP7qb8ZPUIlTsfIss_Z98W8uX5vec
|
|
|
387
387
|
plato/v1/sync_sdk.py,sha256=2sedg1QJiSxr1I3kCyfaLAnlAgHlbblc3QQP_47O30k,25697
|
|
388
388
|
plato/v1/cli/__init__.py,sha256=om4b7PxgsoI7rEwuQelmQkqPdhMVn53_5qEN8kvksYw,105
|
|
389
389
|
plato/v1/cli/agent.py,sha256=G6TV3blG_BqMDBWS-CG7GwzqoqcJTMsIKQ88jvLXb4k,43745
|
|
390
|
-
plato/v1/cli/chronos.py,sha256=
|
|
390
|
+
plato/v1/cli/chronos.py,sha256=RDFzzfP800dzeGVSKKilKGQkG5DairYjSI4RgWVNV_I,25483
|
|
391
391
|
plato/v1/cli/main.py,sha256=iKUz6Mu-4-dgr29qOUmDqBaumOCzNQKZsHAalVtaH0Q,6932
|
|
392
|
-
plato/v1/cli/pm.py,sha256=
|
|
393
|
-
plato/v1/cli/sandbox.py,sha256=
|
|
392
|
+
plato/v1/cli/pm.py,sha256=TIvXBIWFDjr4s1girMMCuvHWQJkjpmsS-igAamddIWE,49746
|
|
393
|
+
plato/v1/cli/sandbox.py,sha256=jhTney-Pr8bGmWIXOjVIMtZJ7v7uIoRnuh3wfG7weRg,98718
|
|
394
394
|
plato/v1/cli/ssh.py,sha256=enrf7Y01ZeRIyHDEX0Yt7up5zEe7MCvE9u8SP4Oqiz4,6926
|
|
395
395
|
plato/v1/cli/utils.py,sha256=ba7Crv4OjDmgCv4SeB8UeZDin-iOdQw_3N6fd-g5XVk,4572
|
|
396
396
|
plato/v1/cli/verify.py,sha256=7QmQwfOOkr8a51f8xfVIr2zif7wGl2E8HOZTbOaIoV0,20671
|
|
397
397
|
plato/v1/cli/world.py,sha256=yBUadOJs1QYm6Jmx_ACDzogybRq5x4B-BnTvGO_ulQk,9757
|
|
398
|
+
plato/v1/cli/templates/world-runner.Dockerfile,sha256=p59nPCAOUgphSiOpWA3eteRXUWTmZV6n57zn3dWUoYM,932
|
|
398
399
|
plato/v1/examples/doordash_tasks.py,sha256=8Sz9qx-vTmiOAiCAbrDRvZGsA1qQQBr1KHbxXdjr7OI,23233
|
|
399
400
|
plato/v1/examples/loadtest.py,sha256=ZsQYNN_fZjE7CbrbVJb4KDc0OLaH7b66iPrEHDhuw0U,5609
|
|
400
401
|
plato/v1/examples/test_env.py,sha256=8kUISbZyMi0Xh9HK7Il1okKQyz0Iq-vAKWgzC8kqUfU,4513
|
|
@@ -459,11 +460,11 @@ plato/v2/utils/models.py,sha256=PwehSSnIRG-tM3tWL1PzZEH77ZHhIAZ9R0UPs6YknbM,1441
|
|
|
459
460
|
plato/v2/utils/proxy_tunnel.py,sha256=8ZTd0jCGSfIHMvSv1fgEyacuISWnGPHLPbDglWroTzY,10463
|
|
460
461
|
plato/worlds/README.md,sha256=XFOkEA3cNNcrWkk-Cxnsl-zn-y0kvUENKQRSqFKpdqw,5479
|
|
461
462
|
plato/worlds/__init__.py,sha256=ALoou3l5lXvs_YZc5eH6HdMHpvhnpzKWqz__aSC1jFc,2152
|
|
462
|
-
plato/worlds/base.py,sha256=
|
|
463
|
+
plato/worlds/base.py,sha256=1O3iKilXlr56mUPVovHY_BjM3S8T57FrotF4895qv5Y,30675
|
|
463
464
|
plato/worlds/build_hook.py,sha256=KSoW0kqa5b7NyZ7MYOw2qsZ_2FkWuz0M3Ru7AKOP7Qw,3486
|
|
464
465
|
plato/worlds/config.py,sha256=a5frj3mt06rSlT25kE-L8Q2b2MTWkR-8cUoBKpC8tG4,11036
|
|
465
|
-
plato/worlds/runner.py,sha256=
|
|
466
|
-
plato_sdk_v2-2.
|
|
467
|
-
plato_sdk_v2-2.
|
|
468
|
-
plato_sdk_v2-2.
|
|
469
|
-
plato_sdk_v2-2.
|
|
466
|
+
plato/worlds/runner.py,sha256=r9B2BxBae8_dM7y5cJf9xhThp_I1Qvf_tlPq2rs8qC8,4013
|
|
467
|
+
plato_sdk_v2-2.4.0.dist-info/METADATA,sha256=IxvrIY5w2FyH_FYPZLPLlFFS34b3fa25HTAfDc988vo,8652
|
|
468
|
+
plato_sdk_v2-2.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
469
|
+
plato_sdk_v2-2.4.0.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
|
|
470
|
+
plato_sdk_v2-2.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|