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/base.py CHANGED
@@ -1,28 +1,39 @@
1
- """Base world class for Plato worlds - OpenAI Gym-like interface."""
1
+ """Base world class for Plato worlds."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  from abc import ABC, abstractmethod
7
- from typing import TYPE_CHECKING, Any, ClassVar
7
+ from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, get_args, get_origin
8
8
 
9
9
  from pydantic import BaseModel, Field
10
10
 
11
+ from plato.worlds.config import RunConfig
12
+
11
13
  if TYPE_CHECKING:
12
- from plato.worlds.config import RunConfig, WorldConfig
14
+ from plato.v2.async_.environment import Environment
15
+ from plato.v2.async_.session import Session
16
+
17
+ from plato.agents.logging import init_logging as _init_chronos_logging
18
+ from plato.agents.logging import log_event as _log_event
19
+ from plato.agents.logging import reset_logging as _reset_chronos_logging
20
+ from plato.agents.logging import span as _span
13
21
 
14
22
  logger = logging.getLogger(__name__)
15
23
 
16
24
  # Global registry of worlds
17
25
  _WORLD_REGISTRY: dict[str, type[BaseWorld]] = {}
18
26
 
27
+ # Type variable for config
28
+ ConfigT = TypeVar("ConfigT", bound=RunConfig)
29
+
19
30
 
20
31
  def register_world(name: str | None = None):
21
32
  """Decorator to register a world class.
22
33
 
23
34
  Usage:
24
35
  @register_world("code")
25
- class CodeWorld(BaseWorld):
36
+ class CodeWorld(BaseWorld[CodeWorldConfig]):
26
37
  ...
27
38
  """
28
39
 
@@ -45,24 +56,8 @@ def get_world(name: str) -> type[BaseWorld] | None:
45
56
  return _WORLD_REGISTRY.get(name)
46
57
 
47
58
 
48
- class AgentSlot(BaseModel):
49
- """Definition of an agent slot in a world."""
50
-
51
- name: str
52
- description: str = ""
53
- required: bool = True
54
-
55
-
56
- class SecretSlot(BaseModel):
57
- """Definition of a secret slot in a world."""
58
-
59
- name: str
60
- description: str = ""
61
- required: bool = False
62
-
63
-
64
59
  class Observation(BaseModel):
65
- """Observation returned from reset/step - what the agent sees."""
60
+ """Observation returned from reset/step."""
66
61
 
67
62
  data: dict[str, Any] = Field(default_factory=dict)
68
63
 
@@ -70,48 +65,60 @@ class Observation(BaseModel):
70
65
 
71
66
 
72
67
  class StepResult(BaseModel):
73
- """Result of a step - OpenAI Gym style."""
68
+ """Result of a step."""
74
69
 
75
70
  observation: Observation
76
71
  done: bool = False
77
72
  info: dict[str, Any] = Field(default_factory=dict)
78
73
 
79
74
 
80
- class BaseWorld(ABC):
81
- """Base class for Plato worlds - OpenAI Gym-like interface.
75
+ class BaseWorld(ABC, Generic[ConfigT]):
76
+ """Base class for Plato worlds.
82
77
 
83
- Subclasses implement:
84
- - reset(): Setup the world, return initial observation
85
- - step(): Execute one step, return StepResult
78
+ Subclass with a config type parameter for fully typed config access:
86
79
 
87
- The base run() method handles the loop: reset -> step until done.
80
+ class CodeWorldConfig(RunConfig):
81
+ repository_url: str
82
+ prompt: str
83
+ coder: Annotated[AgentConfig, Agent(description="Coding agent")]
84
+ git_token: Annotated[str | None, Secret(description="GitHub token")] = None
88
85
 
89
- Example:
90
86
  @register_world("code")
91
- class CodeWorld(BaseWorld):
87
+ class CodeWorld(BaseWorld[CodeWorldConfig]):
92
88
  name = "code"
93
- agents = ["coder"]
94
-
95
- async def reset(self, config: RunConfig) -> Observation:
96
- # Clone repo, setup workspace
97
- return Observation(data={"workspace": "/workspace"})
89
+ description = "Run coding agents"
98
90
 
99
- async def step(self) -> StepResult:
100
- # Run agent, check if done
101
- return StepResult(observation=obs, done=True)
91
+ async def reset(self) -> Observation:
92
+ url = self.config.repository_url # typed as str
93
+ agent = self.config.coder # typed as AgentConfig
94
+ token = self.config.git_token # typed as str | None
102
95
  """
103
96
 
104
97
  # Class attributes
105
98
  name: ClassVar[str] = "base"
106
99
  description: ClassVar[str] = ""
107
- agents: ClassVar[list[AgentSlot | str]] = []
108
- secrets: ClassVar[list[SecretSlot | str]] = []
109
- config_class: ClassVar[type[WorldConfig] | None] = None # Override with typed config
100
+
101
+ # Instance attributes
102
+ config: ConfigT # Typed via generic parameter
103
+ plato_session: Session | None = None # Connected Plato session (if running on managed VM)
110
104
 
111
105
  def __init__(self) -> None:
112
106
  self.logger = logging.getLogger(f"plato.worlds.{self.name}")
113
- self.config: RunConfig | None = None
114
107
  self._step_count: int = 0
108
+ self.plato_session = None
109
+ self._current_step_id: str | None = None
110
+
111
+ @classmethod
112
+ def get_config_class(cls) -> type[RunConfig]:
113
+ """Get the config class from the generic parameter."""
114
+ # Walk up the class hierarchy to find Generic base
115
+ for base in getattr(cls, "__orig_bases__", []):
116
+ origin = get_origin(base)
117
+ if origin is BaseWorld:
118
+ args = get_args(base)
119
+ if args and isinstance(args[0], type) and issubclass(args[0], RunConfig):
120
+ return args[0]
121
+ return RunConfig
115
122
 
116
123
  @classmethod
117
124
  def get_version(cls) -> str:
@@ -125,62 +132,27 @@ class BaseWorld(ABC):
125
132
  continue
126
133
  return "0.0.0"
127
134
 
128
- @classmethod
129
- def get_agent_slots(cls) -> list[AgentSlot]:
130
- """Get the list of agent slots."""
131
- return [AgentSlot(name=a) if isinstance(a, str) else a for a in cls.agents]
132
-
133
- @classmethod
134
- def get_secret_slots(cls) -> list[SecretSlot]:
135
- """Get the list of secret slots."""
136
- return [SecretSlot(name=s) if isinstance(s, str) else s for s in cls.secrets]
137
-
138
- @classmethod
139
- def get_config_schema(cls) -> dict:
140
- """Get JSON schema for world_config.
141
-
142
- If config_class is set, auto-generates schema from the Pydantic model.
143
- Override this method to provide a custom schema.
144
- """
145
- if cls.config_class is not None:
146
- return cls.config_class.get_json_schema()
147
- return {"type": "object", "properties": {}, "required": []}
148
-
149
135
  @classmethod
150
136
  def get_schema(cls) -> dict:
151
- """Get full schema including world_config, agents, and secrets."""
152
- config_schema = cls.get_config_schema()
153
- agent_slots = cls.get_agent_slots()
154
- secret_slots = cls.get_secret_slots()
137
+ """Get full schema including world config, agents, and secrets."""
138
+ config_class = cls.get_config_class()
139
+ schema = config_class.get_json_schema()
155
140
 
156
141
  return {
157
142
  "$schema": "https://json-schema.org/draft/2020-12/schema",
158
143
  "type": "object",
159
- "properties": config_schema.get("properties", {}),
160
- "required": config_schema.get("required", []),
161
- "agents": [
162
- {
163
- "name": slot.name,
164
- "description": slot.description,
165
- "required": slot.required,
166
- }
167
- for slot in agent_slots
168
- ],
169
- "secrets": [
170
- {
171
- "name": slot.name,
172
- "description": slot.description,
173
- "required": slot.required,
174
- }
175
- for slot in secret_slots
176
- ],
144
+ "properties": schema.get("properties", {}),
145
+ "required": schema.get("required", []),
146
+ "agents": schema.get("agents", []),
147
+ "secrets": schema.get("secrets", []),
148
+ "envs": schema.get("envs", []),
177
149
  }
178
150
 
179
151
  @abstractmethod
180
- async def reset(self, config: RunConfig) -> Observation:
152
+ async def reset(self) -> Observation:
181
153
  """Setup the world and return initial observation.
182
154
 
183
- Called once at the start. Setup workspace, clone repos, etc.
155
+ Access configuration via self.config (fully typed).
184
156
  """
185
157
  pass
186
158
 
@@ -196,25 +168,251 @@ class BaseWorld(ABC):
196
168
  """Cleanup resources. Called after run completes."""
197
169
  pass
198
170
 
199
- async def run(self, config: RunConfig) -> None:
171
+ async def _connect_plato_session(self) -> None:
172
+ """Connect to Plato session from config.
173
+
174
+ This is called automatically during run() to restore the session
175
+ and start sending heartbeats while the world runs.
176
+ """
177
+ if not self.config.plato_session:
178
+ return
179
+
180
+ try:
181
+ from plato.v2.async_.session import Session
182
+
183
+ self.logger.info("Restoring Plato session from serialized data")
184
+ self.plato_session = await Session.load(self.config.plato_session, start_heartbeat=True)
185
+ self.logger.info(f"Plato session {self.plato_session.session_id} restored, heartbeat started")
186
+ except Exception as e:
187
+ self.logger.warning(f"Failed to restore Plato session: {e}")
188
+
189
+ async def _disconnect_plato_session(self) -> None:
190
+ """Stop heartbeat for the Plato session (does not close the session)."""
191
+ if self.plato_session:
192
+ try:
193
+ await self.plato_session.stop_heartbeat()
194
+ self.logger.info("Plato session heartbeat stopped")
195
+ except Exception as e:
196
+ self.logger.warning(f"Error stopping Plato heartbeat: {e}")
197
+
198
+ def get_env(self, alias: str) -> Environment | None:
199
+ """Get an environment by alias.
200
+
201
+ Use this to access environments defined in the world config (e.g., gitea, localstack).
202
+
203
+ Args:
204
+ alias: The environment alias (e.g., "gitea", "localstack", "runtime")
205
+
206
+ Returns:
207
+ The Environment object or None if not found or session not connected.
208
+
209
+ Example:
210
+ gitea = self.get_env("gitea")
211
+ if gitea:
212
+ result = await gitea.execute("git status")
213
+ """
214
+ if not self.plato_session:
215
+ self.logger.warning("Cannot get env: Plato session not connected")
216
+ return None
217
+ return self.plato_session.get_env(alias)
218
+
219
+ @property
220
+ def envs(self) -> list[Environment]:
221
+ """Get all environments in the Plato session.
222
+
223
+ Returns:
224
+ List of Environment objects. Empty list if session not connected.
225
+ """
226
+ if not self.plato_session:
227
+ return []
228
+ return self.plato_session.envs
229
+
230
+ def get_sim_env_vars(self) -> dict[str, str]:
231
+ """Get environment variables from all configured sims.
232
+
233
+ Automatically discovers and loads env vars from sims like localstack, gitea, etc.
234
+ based on the environments configured in the world.
235
+
236
+ Returns:
237
+ Dict of environment variable name -> value
238
+
239
+ Example:
240
+ env_vars = self.get_sim_env_vars()
241
+ # Returns: {"AWS_ENDPOINT_URL": "https://...", "GITEA_URL": "https://...", ...}
242
+ """
243
+ env_vars: dict[str, str] = {}
244
+
245
+ # Known sim packages and their env aliases
246
+ sim_packages = [
247
+ ("localstack", "localstack"),
248
+ ("gitea", "gitea"),
249
+ ]
250
+
251
+ for package_name, env_alias in sim_packages:
252
+ env = self.get_env(env_alias)
253
+ if not env:
254
+ continue
255
+
256
+ try:
257
+ # Dynamically import the sim package
258
+ sim_module = __import__(f"plato.sims.{package_name}", fromlist=[package_name])
259
+
260
+ # Get service URLs and env vars
261
+ service_urls = sim_module.get_service_urls(env.job_id)
262
+ sim_vars = sim_module.get_env_vars(service_urls)
263
+ env_vars.update(sim_vars)
264
+ self.logger.info(f"{package_name} env vars: {list(sim_vars.keys())}")
265
+ except ImportError:
266
+ self.logger.debug(f"{package_name} sim package not installed, skipping")
267
+ except Exception as e:
268
+ self.logger.warning(f"Failed to get {package_name} env vars: {e}")
269
+
270
+ return env_vars
271
+
272
+ def get_sim_instructions(self) -> str:
273
+ """Get usage instructions from all configured sims.
274
+
275
+ Returns markdown-formatted instructions for using LocalStack, Gitea, etc.
276
+ based on the environments configured in the world.
277
+
278
+ Returns:
279
+ Markdown string with instructions, or empty string if no sims configured.
280
+
281
+ Example:
282
+ instructions = self.get_sim_instructions()
283
+ # Returns markdown with LocalStack/Gitea setup instructions
284
+ """
285
+ instructions_parts: list[str] = []
286
+
287
+ # Known sim packages and their env aliases
288
+ sim_packages = [
289
+ ("localstack", "localstack"),
290
+ ("gitea", "gitea"),
291
+ ]
292
+
293
+ for package_name, env_alias in sim_packages:
294
+ env = self.get_env(env_alias)
295
+ if not env:
296
+ continue
297
+
298
+ try:
299
+ # Dynamically import the sim package
300
+ sim_module = __import__(f"plato.sims.{package_name}", fromlist=[package_name])
301
+
302
+ # Get instructions using the job_id
303
+ if hasattr(sim_module, "get_instructions_from_job"):
304
+ instructions = sim_module.get_instructions_from_job(env.job_id)
305
+ if instructions:
306
+ instructions_parts.append(instructions)
307
+ self.logger.info(f"Added {package_name} instructions to prompt")
308
+ except ImportError:
309
+ self.logger.debug(f"{package_name} sim package not installed, skipping instructions")
310
+ except Exception as e:
311
+ self.logger.warning(f"Failed to get {package_name} instructions: {e}")
312
+
313
+ if instructions_parts:
314
+ return "\n\n---\n\n".join(instructions_parts)
315
+ return ""
316
+
317
+ def format_instruction_with_sims(self, instruction: str) -> str:
318
+ """Format an instruction with sim context prepended.
319
+
320
+ Automatically adds available service instructions (LocalStack, Gitea, etc.)
321
+ before the main instruction.
322
+
323
+ Args:
324
+ instruction: The base instruction/task
325
+
326
+ Returns:
327
+ Formatted instruction with sim context, or original instruction if no sims.
328
+
329
+ Example:
330
+ formatted = self.format_instruction_with_sims("Fix the bug in main.py")
331
+ # Returns:
332
+ # ## Available Services
333
+ # The following services are available...
334
+ # [LocalStack instructions]
335
+ # ---
336
+ # ## Task
337
+ # Fix the bug in main.py
338
+ """
339
+ sim_instructions = self.get_sim_instructions()
340
+
341
+ if sim_instructions:
342
+ return f"""## Available Services
343
+
344
+ The following services are available for your use:
345
+
346
+ {sim_instructions}
347
+
348
+ ---
349
+
350
+ ## Task
351
+
352
+ {instruction}"""
353
+ return instruction
354
+
355
+ async def run(self, config: ConfigT) -> None:
200
356
  """Run the world: reset -> step until done -> close.
201
357
 
202
- This is the main entry point. Don't override this.
358
+ This is the main entry point. If plato_session_id is provided in config,
359
+ automatically connects to the Plato session to send heartbeats.
203
360
  """
204
361
  self.config = config
205
362
  self._step_count = 0
206
363
 
207
364
  self.logger.info(f"Starting world '{self.name}'")
208
365
 
366
+ # Initialize the logging singleton for agents to use
367
+ if config.callback_url and config.session_id:
368
+ _init_chronos_logging(
369
+ callback_url=config.callback_url,
370
+ session_id=config.session_id,
371
+ )
372
+
373
+ # Connect to Plato session if configured (for heartbeats)
374
+ await self._connect_plato_session()
375
+
376
+ # Log session start
377
+ await _log_event(
378
+ span_type="session_start",
379
+ content=f"World '{self.name}' started",
380
+ source="world",
381
+ extra={"world_name": self.name, "world_version": self.get_version()},
382
+ )
383
+
209
384
  try:
210
- # Reset (setup)
211
- obs = await self.reset(config)
385
+ # Execute reset with automatic span tracking
386
+ async with _span("reset", span_type="reset", source="world") as reset_span:
387
+ reset_span.log(f"Resetting world '{self.name}'")
388
+ obs = await self.reset()
389
+ reset_span.set_extra({"observation": obs.model_dump() if hasattr(obs, "model_dump") else str(obs)})
212
390
  self.logger.info(f"World reset complete: {obs}")
213
391
 
214
- # Step loop
215
392
  while True:
216
393
  self._step_count += 1
217
- result = await self.step()
394
+
395
+ # Execute step with automatic span tracking
396
+ # The span automatically sets itself as the current parent,
397
+ # so agent trajectories will nest under this step
398
+ async with _span(
399
+ f"step_{self._step_count}",
400
+ span_type="step",
401
+ source="world",
402
+ ) as step_span:
403
+ self._current_step_id = step_span.event_id
404
+ step_span.log(f"Step {self._step_count} started")
405
+ result = await self.step()
406
+ step_span.set_extra(
407
+ {
408
+ "done": result.done,
409
+ "observation": result.observation.model_dump()
410
+ if hasattr(result.observation, "model_dump")
411
+ else str(result.observation),
412
+ "info": result.info,
413
+ }
414
+ )
415
+
218
416
  self.logger.info(f"Step {self._step_count}: done={result.done}")
219
417
 
220
418
  if result.done:
@@ -222,4 +420,17 @@ class BaseWorld(ABC):
222
420
 
223
421
  finally:
224
422
  await self.close()
423
+ await self._disconnect_plato_session()
424
+
425
+ # Log session end
426
+ await _log_event(
427
+ span_type="session_end",
428
+ content=f"World '{self.name}' completed after {self._step_count} steps",
429
+ source="world",
430
+ extra={"total_steps": self._step_count},
431
+ )
432
+
433
+ # Reset the logging singleton
434
+ _reset_chronos_logging()
435
+
225
436
  self.logger.info(f"World '{self.name}' completed after {self._step_count} steps")