plato-sdk-v2 2.0.64__py3-none-any.whl → 2.3.4__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.
Files changed (46) hide show
  1. plato/__init__.py +0 -9
  2. plato/_sims_generator/__init__.py +19 -4
  3. plato/_sims_generator/instruction.py +203 -0
  4. plato/_sims_generator/templates/instruction/helpers.py.jinja +161 -0
  5. plato/_sims_generator/templates/instruction/init.py.jinja +43 -0
  6. plato/agents/__init__.py +99 -430
  7. plato/agents/base.py +145 -0
  8. plato/agents/build.py +61 -0
  9. plato/agents/config.py +160 -0
  10. plato/agents/logging.py +515 -0
  11. plato/agents/runner.py +191 -0
  12. plato/agents/trajectory.py +266 -0
  13. plato/chronos/models/__init__.py +1 -1
  14. plato/sims/cli.py +299 -123
  15. plato/sims/registry.py +77 -4
  16. plato/v1/cli/agent.py +88 -84
  17. plato/v1/cli/pm.py +84 -44
  18. plato/v1/cli/sandbox.py +241 -61
  19. plato/v1/cli/ssh.py +16 -4
  20. plato/v1/cli/verify.py +685 -0
  21. plato/v1/cli/world.py +3 -0
  22. plato/v1/flow_executor.py +21 -17
  23. plato/v1/models/env.py +11 -11
  24. plato/v1/sdk.py +2 -2
  25. plato/v1/sync_env.py +11 -11
  26. plato/v1/sync_flow_executor.py +21 -17
  27. plato/v1/sync_sdk.py +4 -2
  28. plato/v2/__init__.py +2 -0
  29. plato/v2/async_/environment.py +31 -0
  30. plato/v2/async_/session.py +72 -4
  31. plato/v2/sync/environment.py +31 -0
  32. plato/v2/sync/session.py +72 -4
  33. plato/worlds/README.md +71 -56
  34. plato/worlds/__init__.py +56 -18
  35. plato/worlds/base.py +578 -93
  36. plato/worlds/config.py +276 -74
  37. plato/worlds/runner.py +475 -80
  38. {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/METADATA +3 -3
  39. {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/RECORD +41 -36
  40. {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/entry_points.txt +1 -0
  41. plato/agents/callback.py +0 -246
  42. plato/world/__init__.py +0 -44
  43. plato/world/base.py +0 -267
  44. plato/world/config.py +0 -139
  45. plato/world/types.py +0 -47
  46. {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/WHEEL +0 -0
plato/worlds/runner.py CHANGED
@@ -2,15 +2,23 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import argparse
6
5
  import asyncio
6
+ import json
7
7
  import logging
8
- import sys
8
+ import os
9
+ import platform
9
10
  from pathlib import Path
10
- from typing import TYPE_CHECKING
11
+ from typing import Annotated
11
12
 
12
- if TYPE_CHECKING:
13
- from plato.worlds.config import RunConfig
13
+ import typer
14
+
15
+ from plato.worlds.config import EnvConfig, RunConfig
16
+
17
+ app = typer.Typer(
18
+ name="plato-world-runner",
19
+ help="Run Plato worlds",
20
+ no_args_is_help=True,
21
+ )
14
22
 
15
23
  logger = logging.getLogger(__name__)
16
24
 
@@ -45,12 +53,11 @@ async def run_world(world_name: str, config: RunConfig) -> None:
45
53
 
46
54
  Args:
47
55
  world_name: Name of the world to run
48
- config: Run configuration
56
+ config: Run configuration (should be the world's typed config class)
49
57
 
50
58
  Raises:
51
59
  ValueError: If world not found
52
60
  """
53
- # Discover installed world packages
54
61
  discover_worlds()
55
62
 
56
63
  from plato.worlds.base import get_registered_worlds, get_world
@@ -64,105 +71,493 @@ async def run_world(world_name: str, config: RunConfig) -> None:
64
71
  await world.run(config)
65
72
 
66
73
 
67
- def main() -> None:
68
- """CLI entry point for the world runner.
69
-
70
- Usage:
71
- plato-world-runner --world code --config /path/to/config.json
72
- plato-world-runner --list
73
- """
74
- parser = argparse.ArgumentParser(
75
- prog="plato-world-runner",
76
- description="Run Plato worlds",
77
- )
78
- parser.add_argument(
79
- "--world",
80
- "-w",
81
- help="World name to run",
82
- )
83
- parser.add_argument(
84
- "--config",
85
- "-c",
86
- help="Path to config JSON file",
87
- )
88
- parser.add_argument(
89
- "--list",
90
- "-l",
91
- action="store_true",
92
- help="List available worlds",
93
- )
94
- parser.add_argument(
95
- "--verbose",
96
- "-v",
97
- action="store_true",
98
- help="Enable verbose logging",
99
- )
100
-
101
- args = parser.parse_args()
102
-
74
+ @app.command()
75
+ def run(
76
+ world: Annotated[str, typer.Option("--world", "-w", help="World name to run")],
77
+ config: Annotated[Path, typer.Option("--config", "-c", help="Path to config JSON file")],
78
+ verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
79
+ ) -> None:
80
+ """Run a world with the given configuration."""
103
81
  # Setup logging
104
- log_level = logging.DEBUG if args.verbose else logging.INFO
82
+ log_level = logging.DEBUG if verbose else logging.INFO
105
83
  logging.basicConfig(
106
84
  level=log_level,
107
85
  format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
108
86
  )
109
87
 
110
- # List worlds
111
- if args.list:
112
- # Discover installed world packages
113
- discover_worlds()
88
+ if not config.exists():
89
+ typer.echo(f"Error: Config file not found: {config}", err=True)
90
+ raise typer.Exit(1)
114
91
 
115
- from plato.worlds.base import get_registered_worlds
92
+ # Discover worlds first to get config class
93
+ discover_worlds()
116
94
 
117
- worlds = get_registered_worlds()
118
- if not worlds:
119
- print("No worlds found.")
120
- else:
121
- print("Available worlds:")
122
- for name, cls in worlds.items():
123
- desc = getattr(cls, "description", "") or ""
124
- version = cls.get_version()
125
- print(f" {name} (v{version}): {desc}")
126
- return
95
+ from plato.worlds.base import get_registered_worlds, get_world
127
96
 
128
- # Run world
129
- if not args.world:
130
- parser.error("--world is required (or use --list to see available worlds)")
97
+ world_cls = get_world(world)
98
+ if world_cls is None:
99
+ available = list(get_registered_worlds().keys())
100
+ typer.echo(f"Error: World '{world}' not found. Available: {available}", err=True)
101
+ raise typer.Exit(1)
131
102
 
132
- if not args.config:
133
- parser.error("--config is required")
103
+ # Load config using the world's typed config class
104
+ config_class = world_cls.get_config_class()
105
+ run_config = config_class.from_file(config)
134
106
 
135
- config_path = Path(args.config)
136
- if not config_path.exists():
137
- print(f"Error: Config file not found: {config_path}", file=sys.stderr)
138
- sys.exit(1)
107
+ try:
108
+ world_instance = world_cls()
109
+ asyncio.run(world_instance.run(run_config))
110
+ except Exception as e:
111
+ logger.exception(f"World execution failed: {e}")
112
+ raise typer.Exit(1)
139
113
 
140
- # Import here to avoid circular imports
141
- from plato.worlds.config import RunConfig
142
114
 
143
- # Discover worlds first to get config class
115
+ @app.command("list")
116
+ def list_worlds(
117
+ verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
118
+ ) -> None:
119
+ """List available worlds."""
120
+ if verbose:
121
+ logging.basicConfig(level=logging.DEBUG)
122
+
144
123
  discover_worlds()
145
124
 
125
+ from plato.worlds.base import get_registered_worlds
126
+
127
+ worlds = get_registered_worlds()
128
+ if not worlds:
129
+ typer.echo("No worlds found.")
130
+ return
131
+
132
+ typer.echo("Available worlds:")
133
+ for name, cls in worlds.items():
134
+ desc = getattr(cls, "description", "") or ""
135
+ version = cls.get_version()
136
+ typer.echo(f" {name} (v{version}): {desc}")
137
+
138
+
139
+ async def _build_agent_image(
140
+ agent_name: str,
141
+ agents_dir: Path,
142
+ plato_client_root: Path | None = None,
143
+ ) -> bool:
144
+ """Build a local agent Docker image.
145
+
146
+ Args:
147
+ agent_name: Name of the agent (e.g., "openhands")
148
+ agents_dir: Directory containing agent subdirectories
149
+ plato_client_root: Root of plato-client repo (for dev builds), or None for prod builds
150
+
151
+ Returns:
152
+ True if build succeeded, False otherwise
153
+ """
154
+ import subprocess
155
+
156
+ # Resolve paths to absolute
157
+ agents_dir = agents_dir.expanduser().resolve()
158
+ agent_path = agents_dir / agent_name
159
+ dockerfile_path = agent_path / "Dockerfile"
160
+
161
+ if not dockerfile_path.exists():
162
+ logger.warning(f"No Dockerfile found for agent '{agent_name}' at {dockerfile_path}")
163
+ return False
164
+
165
+ image_tag = f"{agent_name}:latest"
166
+
167
+ # Determine build context and target
168
+ if plato_client_root:
169
+ plato_client_root = plato_client_root.expanduser().resolve()
170
+
171
+ if plato_client_root and plato_client_root.exists():
172
+ # Dev build from plato-client root (includes local python-sdk)
173
+ build_context = str(plato_client_root)
174
+ dockerfile_abs = str(dockerfile_path)
175
+ target = "dev"
176
+ logger.info(f"Building {image_tag} (dev mode from {build_context})...")
177
+ else:
178
+ # Prod build from agent directory
179
+ build_context = str(agent_path)
180
+ dockerfile_abs = str(dockerfile_path)
181
+ target = "prod"
182
+ logger.info(f"Building {image_tag} (prod mode from {build_context})...")
183
+
184
+ cmd = [
185
+ "docker",
186
+ "build",
187
+ "--target",
188
+ target,
189
+ "-t",
190
+ image_tag,
191
+ "-f",
192
+ dockerfile_abs,
193
+ ]
194
+
195
+ # Use native platform for local dev on ARM Macs (avoids slow emulation)
196
+ if platform.machine() == "arm64":
197
+ cmd.extend(["--build-arg", "PLATFORM=linux/arm64"])
198
+
199
+ cmd.append(build_context)
200
+
201
+ logger.debug(f"Build command: {' '.join(cmd)}")
202
+
203
+ result = subprocess.run(cmd, capture_output=True, text=True)
204
+
205
+ if result.returncode != 0:
206
+ logger.error(f"Failed to build {image_tag}:\n{result.stderr}")
207
+ return False
208
+
209
+ logger.info(f"Successfully built {image_tag}")
210
+ return True
211
+
212
+
213
+ def _extract_agent_images_from_config(config_data: dict) -> list[str]:
214
+ """Extract agent image names from config data.
215
+
216
+ Args:
217
+ config_data: Raw config dictionary
218
+
219
+ Returns:
220
+ List of image names (without tags) that are local (not from a registry)
221
+ """
222
+ images = []
223
+
224
+ # Check agents section
225
+ agents = config_data.get("agents", {})
226
+ for agent_config in agents.values():
227
+ if isinstance(agent_config, dict):
228
+ image = agent_config.get("image", "")
229
+ # Only include local images (no registry prefix like ghcr.io/)
230
+ if image and "/" not in image.split(":")[0]:
231
+ # Extract name without tag
232
+ name = image.split(":")[0]
233
+ if name not in images:
234
+ images.append(name)
235
+
236
+ # Also check direct coder/verifier fields
237
+ for field in ["coder", "verifier"]:
238
+ agent_config = config_data.get(field, {})
239
+ if isinstance(agent_config, dict):
240
+ image = agent_config.get("image", "")
241
+ if image and "/" not in image.split(":")[0]:
242
+ name = image.split(":")[0]
243
+ if name not in images:
244
+ images.append(name)
245
+
246
+ return images
247
+
248
+
249
+ async def _create_chronos_session(
250
+ chronos_url: str,
251
+ api_key: str,
252
+ world_name: str,
253
+ world_config: dict,
254
+ plato_session_id: str | None = None,
255
+ ) -> tuple[str, str]:
256
+ """Create a session in Chronos.
257
+
258
+ Args:
259
+ chronos_url: Chronos base URL (e.g., https://chronos.plato.so)
260
+ api_key: Plato API key for authentication
261
+ world_name: Name of the world being run
262
+ world_config: World configuration dict
263
+ plato_session_id: Optional Plato session ID if already created
264
+
265
+ Returns:
266
+ Tuple of (session_id, callback_url)
267
+ """
268
+ import httpx
269
+
270
+ url = f"{chronos_url.rstrip('/')}/api/sessions"
271
+
272
+ async with httpx.AsyncClient(timeout=30.0) as client:
273
+ response = await client.post(
274
+ url,
275
+ json={
276
+ "world_name": world_name,
277
+ "world_config": world_config,
278
+ "plato_session_id": plato_session_id,
279
+ },
280
+ headers={"x-api-key": api_key},
281
+ )
282
+ response.raise_for_status()
283
+ data = response.json()
284
+
285
+ return data["public_id"], data["callback_url"]
286
+
287
+
288
+ async def _close_chronos_session(
289
+ chronos_url: str,
290
+ api_key: str,
291
+ session_id: str,
292
+ ) -> None:
293
+ """Close a Chronos session.
294
+
295
+ Args:
296
+ chronos_url: Chronos base URL
297
+ api_key: Plato API key for authentication
298
+ session_id: Chronos session public ID to close
299
+ """
300
+ import httpx
301
+
302
+ url = f"{chronos_url.rstrip('/')}/api/sessions/{session_id}/close"
303
+
304
+ try:
305
+ async with httpx.AsyncClient(timeout=30.0) as client:
306
+ response = await client.post(
307
+ url,
308
+ headers={"x-api-key": api_key},
309
+ )
310
+ response.raise_for_status()
311
+ logger.info(f"Closed Chronos session: {session_id}")
312
+ except Exception as e:
313
+ logger.warning(f"Failed to close Chronos session: {e}")
314
+
315
+
316
+ async def _run_dev(
317
+ world_name: str,
318
+ config_path: Path,
319
+ env_timeout: int = 7200,
320
+ agents_dir: Path | None = None,
321
+ ) -> None:
322
+ """Run a world locally with automatic environment creation.
323
+
324
+ This mimics what Chronos does but runs locally for debugging:
325
+ 1. Load and parse the config
326
+ 2. Build local agent images if --agents-dir is provided
327
+ 3. Create Plato session with all environments
328
+ 4. Create Chronos session for logging/callbacks
329
+ 5. Run the world with the session attached
330
+
331
+ Requires environment variables:
332
+ CHRONOS_URL: Chronos base URL (e.g., https://chronos.plato.so)
333
+ PLATO_API_KEY: API key for Plato and Chronos authentication
334
+
335
+ Args:
336
+ world_name: Name of the world to run
337
+ config_path: Path to the config JSON file
338
+ env_timeout: Timeout for environment creation (seconds)
339
+ agents_dir: Optional directory containing agent source code
340
+ """
341
+ from plato.v2 import AsyncPlato
146
342
  from plato.worlds.base import get_world
147
343
 
148
- world_cls = get_world(args.world)
344
+ # Get required env vars
345
+ chronos_url = os.environ.get("CHRONOS_URL")
346
+ api_key = os.environ.get("PLATO_API_KEY")
347
+
348
+ if not chronos_url:
349
+ raise ValueError("CHRONOS_URL environment variable is required")
350
+ if not api_key:
351
+ raise ValueError("PLATO_API_KEY environment variable is required")
352
+
353
+ discover_worlds()
354
+
355
+ world_cls = get_world(world_name)
149
356
  if world_cls is None:
150
357
  from plato.worlds.base import get_registered_worlds
151
358
 
152
359
  available = list(get_registered_worlds().keys())
153
- print(f"Error: World '{args.world}' not found. Available: {available}", file=sys.stderr)
154
- sys.exit(1)
360
+ raise ValueError(f"World '{world_name}' not found. Available: {available}")
361
+
362
+ # Load config
363
+ config_class = world_cls.get_config_class()
364
+ with open(config_path) as f:
365
+ config_data = json.load(f)
366
+
367
+ # Parse the config to get typed access
368
+ run_config = config_class._from_dict(config_data.copy())
369
+
370
+ # Build local agent images if agents_dir is provided
371
+ if agents_dir:
372
+ # Resolve agents_dir to absolute path
373
+ agents_dir = agents_dir.expanduser().resolve()
374
+ agent_images = _extract_agent_images_from_config(config_data)
375
+ if agent_images:
376
+ logger.info(f"Building local agent images: {agent_images}")
377
+ # Determine if we're in a plato-client repo for dev builds
378
+ # (agents_dir is something like /path/to/plato-client/agents)
379
+ plato_client_root = agents_dir.parent if agents_dir.name == "agents" else None
380
+ for agent_name in agent_images:
381
+ success = await _build_agent_image(agent_name, agents_dir, plato_client_root)
382
+ if not success:
383
+ raise RuntimeError(f"Failed to build agent image: {agent_name}")
384
+ else:
385
+ logger.info("No local agent images found in config")
386
+
387
+ # Get environment configs from the parsed config
388
+ env_configs: list[EnvConfig] = run_config.get_envs()
155
389
 
156
- # Load config with the world's typed config class if available
157
- config = RunConfig.from_file(config_path, config_class=world_cls.config_class)
390
+ # Create Plato client
391
+ plato = AsyncPlato()
392
+ session = None
393
+ plato_session_id: str | None = None
158
394
 
159
395
  try:
160
- # Run the world directly (we already have the class)
161
- world = world_cls()
162
- asyncio.run(world.run(config))
396
+ if env_configs:
397
+ logger.info(f"Creating {len(env_configs)} environments...")
398
+ session = await plato.sessions.create(envs=env_configs, timeout=env_timeout)
399
+ plato_session_id = session.session_id
400
+ logger.info(f"Created Plato session: {plato_session_id}")
401
+ logger.info(f"Environments: {[e.alias for e in session.envs]}")
402
+
403
+ # Serialize and add to config
404
+ serialized = session.dump()
405
+ run_config.plato_session = serialized
406
+ else:
407
+ logger.info("No environments defined for this world")
408
+
409
+ # Create Chronos session (after Plato session so we can link them)
410
+ logger.info(f"Creating Chronos session at {chronos_url}...")
411
+ chronos_session_id, callback_url = await _create_chronos_session(
412
+ chronos_url=chronos_url,
413
+ api_key=api_key,
414
+ world_name=world_name,
415
+ world_config=config_data,
416
+ plato_session_id=plato_session_id,
417
+ )
418
+ logger.info(f"Created Chronos session: {chronos_session_id}")
419
+ logger.info(f"View at: {chronos_url}/sessions/{chronos_session_id}")
420
+
421
+ # Initialize logging
422
+ from plato.agents import init_logging
423
+
424
+ init_logging(
425
+ callback_url=callback_url,
426
+ session_id=chronos_session_id,
427
+ )
428
+
429
+ # Update run_config with session info for agents
430
+ run_config.session_id = chronos_session_id
431
+ run_config.callback_url = callback_url
432
+
433
+ # Run the world
434
+ logger.info(f"Starting world '{world_name}'...")
435
+ world_instance = world_cls()
436
+ await world_instance.run(run_config)
437
+
438
+ finally:
439
+ # Cleanup
440
+ if session:
441
+ logger.info("Closing Plato session...")
442
+ await session.close()
443
+ await plato.close()
444
+
445
+ # Close Chronos session
446
+ if chronos_session_id:
447
+ await _close_chronos_session(chronos_url, api_key, chronos_session_id)
448
+
449
+ # Reset logging
450
+ from plato.agents import reset_logging
451
+
452
+ reset_logging()
453
+
454
+
455
+ def _setup_colored_logging(verbose: bool = False) -> None:
456
+ """Setup colored logging with filtered noisy loggers."""
457
+ log_level = logging.DEBUG if verbose else logging.INFO
458
+
459
+ # Define colors for different log levels
460
+ colors = {
461
+ "DEBUG": "\033[36m", # Cyan
462
+ "INFO": "\033[32m", # Green
463
+ "WARNING": "\033[33m", # Yellow
464
+ "ERROR": "\033[31m", # Red
465
+ "CRITICAL": "\033[35m", # Magenta
466
+ }
467
+ reset = "\033[0m"
468
+
469
+ class ColoredFormatter(logging.Formatter):
470
+ def format(self, record: logging.LogRecord) -> str:
471
+ color = colors.get(record.levelname, "")
472
+ record.levelname = f"{color}{record.levelname}{reset}"
473
+ record.name = f"\033[34m{record.name}{reset}" # Blue for logger name
474
+ return super().format(record)
475
+
476
+ # Create handler with colored formatter
477
+ handler = logging.StreamHandler()
478
+ handler.setFormatter(
479
+ ColoredFormatter(
480
+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
481
+ datefmt="%H:%M:%S",
482
+ )
483
+ )
484
+
485
+ # Configure root logger
486
+ root_logger = logging.getLogger()
487
+ root_logger.setLevel(log_level)
488
+ root_logger.handlers = [handler]
489
+
490
+ # Silence noisy HTTP loggers
491
+ for noisy_logger in ["httpcore", "httpx", "urllib3", "hpack"]:
492
+ logging.getLogger(noisy_logger).setLevel(logging.WARNING)
493
+
494
+
495
+ @app.command("dev")
496
+ def dev(
497
+ world: Annotated[str, typer.Option("--world", "-w", help="World name to run")],
498
+ config: Annotated[Path, typer.Option("--config", "-c", help="Path to config JSON file")],
499
+ env_timeout: Annotated[
500
+ int, typer.Option("--env-timeout", help="Timeout for environment creation (seconds)")
501
+ ] = 7200,
502
+ agents_dir: Annotated[
503
+ Path | None,
504
+ typer.Option("--agents-dir", "-a", help="Directory containing agent source code (builds local images)"),
505
+ ] = None,
506
+ verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
507
+ ) -> None:
508
+ """Run a world locally for development/debugging.
509
+
510
+ This creates Plato environments automatically (like Chronos does),
511
+ creates a Chronos session for logging, and runs the world.
512
+
513
+ Example config.json:
514
+ {
515
+ "instruction": "Create a git repo and upload files to S3",
516
+ "coder": {
517
+ "image": "openhands:latest",
518
+ "config": {"model_name": "gemini/gemini-3-flash-preview"}
519
+ },
520
+ "secrets": {
521
+ "gemini_api_key": "..."
522
+ }
523
+ }
524
+
525
+ Required environment variables:
526
+ CHRONOS_URL: Chronos base URL (e.g., https://chronos.plato.so)
527
+ PLATO_API_KEY: API key for Plato and Chronos authentication
528
+
529
+ Examples:
530
+ # Basic usage
531
+ CHRONOS_URL=https://chronos.plato.so plato-world-runner dev -w code -c config.json
532
+
533
+ # With local agent builds (from plato-client repo)
534
+ plato-world-runner dev -w code -c config.json --agents-dir ~/plato-client/agents
535
+ """
536
+ # Setup colored logging with filtered noisy loggers
537
+ _setup_colored_logging(verbose)
538
+
539
+ if not config.exists():
540
+ typer.echo(f"Error: Config file not found: {config}", err=True)
541
+ raise typer.Exit(1)
542
+
543
+ if not os.environ.get("CHRONOS_URL"):
544
+ typer.echo("Error: CHRONOS_URL environment variable required", err=True)
545
+ raise typer.Exit(1)
546
+
547
+ if not os.environ.get("PLATO_API_KEY"):
548
+ typer.echo("Error: PLATO_API_KEY environment variable required", err=True)
549
+ raise typer.Exit(1)
550
+
551
+ try:
552
+ asyncio.run(_run_dev(world, config, env_timeout, agents_dir))
163
553
  except Exception as e:
164
554
  logger.exception(f"World execution failed: {e}")
165
- sys.exit(1)
555
+ raise typer.Exit(1)
556
+
557
+
558
+ def main() -> None:
559
+ """CLI entry point."""
560
+ app()
166
561
 
167
562
 
168
563
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plato-sdk-v2
3
- Version: 2.0.64
3
+ Version: 2.3.4
4
4
  Summary: Python SDK for the Plato API
5
5
  Author-email: Plato <support@plato.so>
6
6
  License-Expression: MIT
@@ -30,8 +30,6 @@ Requires-Dist: rich>=13.0.0
30
30
  Requires-Dist: tenacity>=9.1.2
31
31
  Requires-Dist: tomli>=2.0.0
32
32
  Requires-Dist: typer>=0.9.0
33
- Provides-Extra: agents
34
- Requires-Dist: harbor>=0.1.35; (python_version >= '3.12') and extra == 'agents'
35
33
  Provides-Extra: db-cleanup
36
34
  Requires-Dist: aiomysql>=0.2; extra == 'db-cleanup'
37
35
  Requires-Dist: aiosqlite>=0.20; extra == 'db-cleanup'
@@ -46,6 +44,8 @@ Description-Content-Type: text/markdown
46
44
 
47
45
  # Plato Python SDK
48
46
 
47
+
48
+
49
49
  Python SDK for the Plato platform. Uses [Harbor](https://harborframework.com) for agent execution.
50
50
 
51
51
  ## Installation