plato-sdk-v2 2.0.50__py3-none-any.whl → 2.2.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 (158) hide show
  1. plato/__init__.py +7 -6
  2. plato/_generated/__init__.py +1 -1
  3. plato/_generated/api/v1/env/evaluate_session.py +3 -3
  4. plato/_generated/api/v1/env/log_state_mutation.py +4 -4
  5. plato/_generated/api/v1/sandbox/checkpoint_vm.py +3 -3
  6. plato/_generated/api/v1/sandbox/save_vm_snapshot.py +3 -3
  7. plato/_generated/api/v1/sandbox/setup_sandbox.py +8 -8
  8. plato/_generated/api/v1/session/__init__.py +2 -0
  9. plato/_generated/api/v1/session/get_sessions_for_archival.py +100 -0
  10. plato/_generated/api/v1/testcases/__init__.py +6 -2
  11. plato/_generated/api/v1/testcases/get_mutation_groups_for_testcase.py +98 -0
  12. plato/_generated/api/v1/testcases/{get_next_output_testcase_for_scoring.py → get_next_testcase_for_scoring.py} +23 -10
  13. plato/_generated/api/v1/testcases/get_testcase_metadata_for_scoring.py +74 -0
  14. plato/_generated/api/v2/__init__.py +2 -1
  15. plato/_generated/api/v2/jobs/__init__.py +4 -0
  16. plato/_generated/api/v2/jobs/checkpoint.py +3 -3
  17. plato/_generated/api/v2/jobs/disk_snapshot.py +3 -3
  18. plato/_generated/api/v2/jobs/log_for_job.py +4 -39
  19. plato/_generated/api/v2/jobs/make.py +4 -4
  20. plato/_generated/api/v2/jobs/setup_sandbox.py +97 -0
  21. plato/_generated/api/v2/jobs/snapshot.py +3 -3
  22. plato/_generated/api/v2/jobs/snapshot_store.py +91 -0
  23. plato/_generated/api/v2/sessions/__init__.py +4 -0
  24. plato/_generated/api/v2/sessions/checkpoint.py +3 -3
  25. plato/_generated/api/v2/sessions/disk_snapshot.py +3 -3
  26. plato/_generated/api/v2/sessions/evaluate.py +3 -3
  27. plato/_generated/api/v2/sessions/log_job_mutation.py +4 -39
  28. plato/_generated/api/v2/sessions/make.py +4 -4
  29. plato/_generated/api/v2/sessions/setup_sandbox.py +98 -0
  30. plato/_generated/api/v2/sessions/snapshot.py +3 -3
  31. plato/_generated/api/v2/sessions/snapshot_store.py +94 -0
  32. plato/_generated/api/v2/user/__init__.py +7 -0
  33. plato/_generated/api/v2/user/get_current_user.py +76 -0
  34. plato/_generated/models/__init__.py +174 -23
  35. plato/_sims_generator/__init__.py +19 -4
  36. plato/_sims_generator/instruction.py +203 -0
  37. plato/_sims_generator/templates/instruction/helpers.py.jinja +161 -0
  38. plato/_sims_generator/templates/instruction/init.py.jinja +43 -0
  39. plato/agents/__init__.py +107 -517
  40. plato/agents/base.py +145 -0
  41. plato/agents/build.py +61 -0
  42. plato/agents/config.py +160 -0
  43. plato/agents/logging.py +401 -0
  44. plato/agents/runner.py +161 -0
  45. plato/agents/trajectory.py +266 -0
  46. plato/chronos/__init__.py +37 -0
  47. plato/chronos/api/__init__.py +3 -0
  48. plato/chronos/api/agents/__init__.py +13 -0
  49. plato/chronos/api/agents/create_agent.py +63 -0
  50. plato/chronos/api/agents/delete_agent.py +61 -0
  51. plato/chronos/api/agents/get_agent.py +62 -0
  52. plato/chronos/api/agents/get_agent_schema.py +72 -0
  53. plato/chronos/api/agents/get_agent_versions.py +62 -0
  54. plato/chronos/api/agents/list_agents.py +57 -0
  55. plato/chronos/api/agents/lookup_agent.py +74 -0
  56. plato/chronos/api/auth/__init__.py +9 -0
  57. plato/chronos/api/auth/debug_auth_api_auth_debug_get.py +43 -0
  58. plato/chronos/api/auth/get_auth_status_api_auth_status_get.py +61 -0
  59. plato/chronos/api/auth/get_current_user_route_api_auth_me_get.py +60 -0
  60. plato/chronos/api/callback/__init__.py +11 -0
  61. plato/chronos/api/callback/push_agent_logs.py +61 -0
  62. plato/chronos/api/callback/update_agent_status.py +57 -0
  63. plato/chronos/api/callback/upload_artifacts.py +59 -0
  64. plato/chronos/api/callback/upload_logs_zip.py +57 -0
  65. plato/chronos/api/callback/upload_trajectory.py +57 -0
  66. plato/chronos/api/default/__init__.py +7 -0
  67. plato/chronos/api/default/health.py +43 -0
  68. plato/chronos/api/jobs/__init__.py +7 -0
  69. plato/chronos/api/jobs/launch_job.py +63 -0
  70. plato/chronos/api/registry/__init__.py +19 -0
  71. plato/chronos/api/registry/get_agent_schema_api_registry_agents__agent_name__schema_get.py +62 -0
  72. plato/chronos/api/registry/get_agent_versions_api_registry_agents__agent_name__versions_get.py +52 -0
  73. plato/chronos/api/registry/get_world_schema_api_registry_worlds__package_name__schema_get.py +68 -0
  74. plato/chronos/api/registry/get_world_versions_api_registry_worlds__package_name__versions_get.py +52 -0
  75. plato/chronos/api/registry/list_registry_agents_api_registry_agents_get.py +44 -0
  76. plato/chronos/api/registry/list_registry_worlds_api_registry_worlds_get.py +44 -0
  77. plato/chronos/api/runtimes/__init__.py +11 -0
  78. plato/chronos/api/runtimes/create_runtime.py +63 -0
  79. plato/chronos/api/runtimes/delete_runtime.py +61 -0
  80. plato/chronos/api/runtimes/get_runtime.py +62 -0
  81. plato/chronos/api/runtimes/list_runtimes.py +57 -0
  82. plato/chronos/api/runtimes/test_runtime.py +67 -0
  83. plato/chronos/api/secrets/__init__.py +11 -0
  84. plato/chronos/api/secrets/create_secret.py +63 -0
  85. plato/chronos/api/secrets/delete_secret.py +61 -0
  86. plato/chronos/api/secrets/get_secret.py +62 -0
  87. plato/chronos/api/secrets/list_secrets.py +57 -0
  88. plato/chronos/api/secrets/update_secret.py +68 -0
  89. plato/chronos/api/sessions/__init__.py +10 -0
  90. plato/chronos/api/sessions/get_session.py +62 -0
  91. plato/chronos/api/sessions/get_session_logs.py +72 -0
  92. plato/chronos/api/sessions/get_session_logs_download.py +62 -0
  93. plato/chronos/api/sessions/list_sessions.py +57 -0
  94. plato/chronos/api/status/__init__.py +8 -0
  95. plato/chronos/api/status/get_status_api_status_get.py +44 -0
  96. plato/chronos/api/status/get_version_info_api_version_get.py +44 -0
  97. plato/chronos/api/templates/__init__.py +11 -0
  98. plato/chronos/api/templates/create_template.py +63 -0
  99. plato/chronos/api/templates/delete_template.py +61 -0
  100. plato/chronos/api/templates/get_template.py +62 -0
  101. plato/chronos/api/templates/list_templates.py +57 -0
  102. plato/chronos/api/templates/update_template.py +68 -0
  103. plato/chronos/api/trajectories/__init__.py +8 -0
  104. plato/chronos/api/trajectories/get_trajectory.py +62 -0
  105. plato/chronos/api/trajectories/list_trajectories.py +62 -0
  106. plato/chronos/api/worlds/__init__.py +10 -0
  107. plato/chronos/api/worlds/create_world.py +63 -0
  108. plato/chronos/api/worlds/delete_world.py +61 -0
  109. plato/chronos/api/worlds/get_world.py +62 -0
  110. plato/chronos/api/worlds/list_worlds.py +57 -0
  111. plato/chronos/client.py +171 -0
  112. plato/chronos/errors.py +141 -0
  113. plato/chronos/models/__init__.py +647 -0
  114. plato/chronos/py.typed +0 -0
  115. plato/sims/cli.py +299 -123
  116. plato/sims/registry.py +77 -4
  117. plato/v1/cli/agent.py +88 -84
  118. plato/v1/cli/main.py +2 -0
  119. plato/v1/cli/pm.py +441 -119
  120. plato/v1/cli/sandbox.py +747 -191
  121. plato/v1/cli/sim.py +11 -0
  122. plato/v1/cli/verify.py +1269 -0
  123. plato/v1/cli/world.py +3 -0
  124. plato/v1/flow_executor.py +21 -17
  125. plato/v1/models/env.py +11 -11
  126. plato/v1/sdk.py +2 -2
  127. plato/v1/sync_env.py +11 -11
  128. plato/v1/sync_flow_executor.py +21 -17
  129. plato/v1/sync_sdk.py +4 -2
  130. plato/v2/__init__.py +2 -0
  131. plato/v2/async_/environment.py +20 -1
  132. plato/v2/async_/session.py +54 -3
  133. plato/v2/sync/environment.py +2 -1
  134. plato/v2/sync/session.py +52 -2
  135. plato/worlds/README.md +218 -0
  136. plato/worlds/__init__.py +54 -18
  137. plato/worlds/base.py +304 -93
  138. plato/worlds/config.py +239 -73
  139. plato/worlds/runner.py +391 -80
  140. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/METADATA +1 -3
  141. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/RECORD +143 -68
  142. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/entry_points.txt +1 -0
  143. plato/_generated/api/v2/interfaces/__init__.py +0 -27
  144. plato/_generated/api/v2/interfaces/v2_interface_browser_create.py +0 -68
  145. plato/_generated/api/v2/interfaces/v2_interface_cdp_url.py +0 -65
  146. plato/_generated/api/v2/interfaces/v2_interface_click.py +0 -64
  147. plato/_generated/api/v2/interfaces/v2_interface_close.py +0 -59
  148. plato/_generated/api/v2/interfaces/v2_interface_computer_create.py +0 -68
  149. plato/_generated/api/v2/interfaces/v2_interface_cursor.py +0 -64
  150. plato/_generated/api/v2/interfaces/v2_interface_key.py +0 -68
  151. plato/_generated/api/v2/interfaces/v2_interface_screenshot.py +0 -65
  152. plato/_generated/api/v2/interfaces/v2_interface_scroll.py +0 -70
  153. plato/_generated/api/v2/interfaces/v2_interface_type.py +0 -64
  154. plato/world/__init__.py +0 -44
  155. plato/world/base.py +0 -267
  156. plato/world/config.py +0 -139
  157. plato/world/types.py +0 -47
  158. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.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,409 @@ 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 _run_dev(
250
+ world_name: str,
251
+ config_path: Path,
252
+ env_timeout: int = 600,
253
+ chronos_url: str | None = None,
254
+ api_key: str | None = None,
255
+ agents_dir: Path | None = None,
256
+ ) -> None:
257
+ """Run a world locally with automatic environment creation.
258
+
259
+ This mimics what Chronos does but runs locally for debugging:
260
+ 1. Load and parse the config
261
+ 2. Build local agent images if --agents-dir is provided
262
+ 3. Create Plato session with all environments
263
+ 4. Optionally initialize Chronos logging for callbacks
264
+ 5. Run the world with the session attached
265
+
266
+ Args:
267
+ world_name: Name of the world to run
268
+ config_path: Path to the config JSON file
269
+ env_timeout: Timeout for environment creation (seconds)
270
+ chronos_url: Optional Chronos base URL for sending log events
271
+ api_key: Optional Plato API key (used for Chronos session creation)
272
+ agents_dir: Optional directory containing agent source code
273
+ """
274
+ from uuid import uuid4
275
+
276
+ from plato.v2 import AsyncPlato
146
277
  from plato.worlds.base import get_world
147
278
 
148
- world_cls = get_world(args.world)
279
+ discover_worlds()
280
+
281
+ world_cls = get_world(world_name)
149
282
  if world_cls is None:
150
283
  from plato.worlds.base import get_registered_worlds
151
284
 
152
285
  available = list(get_registered_worlds().keys())
153
- print(f"Error: World '{args.world}' not found. Available: {available}", file=sys.stderr)
154
- sys.exit(1)
286
+ raise ValueError(f"World '{world_name}' not found. Available: {available}")
287
+
288
+ # Load config
289
+ config_class = world_cls.get_config_class()
290
+ with open(config_path) as f:
291
+ config_data = json.load(f)
292
+
293
+ # Parse the config to get typed access
294
+ run_config = config_class._from_dict(config_data.copy())
295
+
296
+ # Build local agent images if agents_dir is provided
297
+ if agents_dir:
298
+ # Resolve agents_dir to absolute path
299
+ agents_dir = agents_dir.expanduser().resolve()
300
+ agent_images = _extract_agent_images_from_config(config_data)
301
+ if agent_images:
302
+ logger.info(f"Building local agent images: {agent_images}")
303
+ # Determine if we're in a plato-client repo for dev builds
304
+ # (agents_dir is something like /path/to/plato-client/agents)
305
+ plato_client_root = agents_dir.parent if agents_dir.name == "agents" else None
306
+ for agent_name in agent_images:
307
+ success = await _build_agent_image(agent_name, agents_dir, plato_client_root)
308
+ if not success:
309
+ raise RuntimeError(f"Failed to build agent image: {agent_name}")
310
+ else:
311
+ logger.info("No local agent images found in config")
312
+
313
+ # Get environment configs from the parsed config
314
+ env_configs: list[EnvConfig] = run_config.get_envs()
155
315
 
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)
316
+ # Create Plato client
317
+ plato = AsyncPlato()
318
+ session = None
319
+
320
+ # Initialize Chronos logging if URL provided
321
+ chronos_session_id: str | None = None
322
+ if chronos_url:
323
+ from plato.agents import init_logging
324
+
325
+ chronos_session_id = f"dev-{uuid4().hex[:8]}"
326
+ callback_url = f"{chronos_url.rstrip('/')}/api/v1/callback"
327
+ init_logging(
328
+ callback_url=callback_url,
329
+ session_id=chronos_session_id,
330
+ )
331
+ logger.info(f"Chronos logging enabled: {callback_url} (session: {chronos_session_id})")
332
+
333
+ # Update run_config with session info for agents
334
+ run_config.session_id = chronos_session_id
335
+ run_config.callback_url = callback_url
158
336
 
159
337
  try:
160
- # Run the world directly (we already have the class)
161
- world = world_cls()
162
- asyncio.run(world.run(config))
338
+ if env_configs:
339
+ logger.info(f"Creating {len(env_configs)} environments...")
340
+ session = await plato.sessions.create(envs=env_configs, timeout=env_timeout)
341
+ logger.info(f"Created Plato session: {session.session_id}")
342
+ logger.info(f"Environments: {[e.alias for e in session.envs]}")
343
+
344
+ # Serialize and add to config
345
+ serialized = session.dump()
346
+ run_config.plato_session = serialized
347
+ else:
348
+ logger.info("No environments defined for this world")
349
+
350
+ # Run the world
351
+ logger.info(f"Starting world '{world_name}'...")
352
+ world_instance = world_cls()
353
+ await world_instance.run(run_config)
354
+
355
+ finally:
356
+ # Cleanup
357
+ if session:
358
+ logger.info("Closing Plato session...")
359
+ await session.close()
360
+ await plato.close()
361
+
362
+ # Reset logging
363
+ if chronos_url:
364
+ from plato.agents import reset_logging
365
+
366
+ reset_logging()
367
+
368
+
369
+ def _setup_colored_logging(verbose: bool = False) -> None:
370
+ """Setup colored logging with filtered noisy loggers."""
371
+ log_level = logging.DEBUG if verbose else logging.INFO
372
+
373
+ # Define colors for different log levels
374
+ colors = {
375
+ "DEBUG": "\033[36m", # Cyan
376
+ "INFO": "\033[32m", # Green
377
+ "WARNING": "\033[33m", # Yellow
378
+ "ERROR": "\033[31m", # Red
379
+ "CRITICAL": "\033[35m", # Magenta
380
+ }
381
+ reset = "\033[0m"
382
+
383
+ class ColoredFormatter(logging.Formatter):
384
+ def format(self, record: logging.LogRecord) -> str:
385
+ color = colors.get(record.levelname, "")
386
+ record.levelname = f"{color}{record.levelname}{reset}"
387
+ record.name = f"\033[34m{record.name}{reset}" # Blue for logger name
388
+ return super().format(record)
389
+
390
+ # Create handler with colored formatter
391
+ handler = logging.StreamHandler()
392
+ handler.setFormatter(
393
+ ColoredFormatter(
394
+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
395
+ datefmt="%H:%M:%S",
396
+ )
397
+ )
398
+
399
+ # Configure root logger
400
+ root_logger = logging.getLogger()
401
+ root_logger.setLevel(log_level)
402
+ root_logger.handlers = [handler]
403
+
404
+ # Silence noisy HTTP loggers
405
+ for noisy_logger in ["httpcore", "httpx", "urllib3", "hpack"]:
406
+ logging.getLogger(noisy_logger).setLevel(logging.WARNING)
407
+
408
+
409
+ @app.command("dev")
410
+ def dev(
411
+ world: Annotated[str, typer.Option("--world", "-w", help="World name to run")],
412
+ config: Annotated[Path, typer.Option("--config", "-c", help="Path to config JSON file")],
413
+ env_timeout: Annotated[int, typer.Option("--env-timeout", help="Timeout for environment creation (seconds)")] = 600,
414
+ chronos_url: Annotated[
415
+ str | None, typer.Option("--chronos-url", help="Chronos base URL for log events (e.g., http://localhost:8000)")
416
+ ] = None,
417
+ api_key: Annotated[str | None, typer.Option("--api-key", help="Plato API key for Chronos authentication")] = None,
418
+ agents_dir: Annotated[
419
+ Path | None,
420
+ typer.Option("--agents-dir", "-a", help="Directory containing agent source code (builds local images)"),
421
+ ] = None,
422
+ verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose logging")] = False,
423
+ ) -> None:
424
+ """Run a world locally for development/debugging.
425
+
426
+ This creates Plato environments automatically (like Chronos does)
427
+ and runs the world with the session attached.
428
+
429
+ Optionally sends log events to a Chronos server for real-time monitoring.
430
+
431
+ Example config.json:
432
+ {
433
+ "instruction": "Create a git repo and upload files to S3",
434
+ "coder": {
435
+ "image": "openhands:latest",
436
+ "config": {"model_name": "gemini/gemini-3-flash-preview"}
437
+ },
438
+ "secrets": {
439
+ "gemini_api_key": "..."
440
+ }
441
+ }
442
+
443
+ Environment variables:
444
+ PLATO_API_KEY: API key for Plato (required)
445
+
446
+ Examples:
447
+ # Basic usage
448
+ plato-world-runner dev -w code -c config.json
449
+
450
+ # With local agent builds (from plato-client repo)
451
+ plato-world-runner dev -w code -c config.json --agents-dir ~/plato-client/agents
452
+
453
+ # With Chronos logging
454
+ plato-world-runner dev -w code -c config.json --chronos-url http://localhost:8000
455
+ """
456
+ # Setup colored logging with filtered noisy loggers
457
+ _setup_colored_logging(verbose)
458
+
459
+ if not config.exists():
460
+ typer.echo(f"Error: Config file not found: {config}", err=True)
461
+ raise typer.Exit(1)
462
+
463
+ if not os.environ.get("PLATO_API_KEY"):
464
+ typer.echo("Error: PLATO_API_KEY environment variable required", err=True)
465
+ raise typer.Exit(1)
466
+
467
+ try:
468
+ asyncio.run(_run_dev(world, config, env_timeout, chronos_url, api_key, agents_dir))
163
469
  except Exception as e:
164
470
  logger.exception(f"World execution failed: {e}")
165
- sys.exit(1)
471
+ raise typer.Exit(1)
472
+
473
+
474
+ def main() -> None:
475
+ """CLI entry point."""
476
+ app()
166
477
 
167
478
 
168
479
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plato-sdk-v2
3
- Version: 2.0.50
3
+ Version: 2.2.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'