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/config.py CHANGED
@@ -3,121 +3,323 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from pathlib import Path
6
- from typing import Any, Generic, TypeVar
6
+ from typing import Any
7
7
 
8
8
  from pydantic import BaseModel, Field
9
9
 
10
+ from plato._generated.models import (
11
+ EnvFromArtifact,
12
+ EnvFromResource,
13
+ EnvFromSimulator,
14
+ )
15
+ from plato.v2.async_.session import SerializedSession
16
+
17
+ # Union type for environment configurations
18
+ EnvConfig = EnvFromArtifact | EnvFromSimulator | EnvFromResource
19
+
10
20
 
11
21
  class AgentConfig(BaseModel):
12
- """Configuration for a single agent slot.
22
+ """Configuration for an agent.
13
23
 
14
24
  Attributes:
15
25
  image: Docker image URI for the agent
16
- config: Agent-specific configuration (passed to the agent)
26
+ config: Agent-specific configuration passed to the agent
17
27
  """
18
28
 
19
29
  image: str
20
30
  config: dict[str, Any] = Field(default_factory=dict)
21
31
 
22
32
 
23
- class WorldConfig(BaseModel):
24
- """World-specific configuration base class.
33
+ class Agent:
34
+ """Annotation marker for agent fields.
35
+
36
+ Usage:
37
+ coder: Annotated[AgentConfig, Agent(description="Coding agent")]
38
+ """
39
+
40
+ def __init__(self, description: str = "", required: bool = True):
41
+ self.description = description
42
+ self.required = required
43
+
44
+
45
+ class Secret:
46
+ """Annotation marker for secret fields.
47
+
48
+ Usage:
49
+ api_key: Annotated[str | None, Secret(description="API key")] = None
50
+ """
51
+
52
+ def __init__(self, description: str = "", required: bool = False):
53
+ self.description = description
54
+ self.required = required
55
+
56
+
57
+ class Env:
58
+ """Annotation marker for environment fields.
59
+
60
+ Environments are VMs that run alongside the world's runtime.
61
+ They can be specified by artifact ID, simulator name, or resource config.
62
+
63
+ Usage:
64
+ gitea: Annotated[EnvConfig, Env(description="Git server")] = EnvFromArtifact(
65
+ artifact_id="abc123",
66
+ alias="gitea",
67
+ )
68
+ """
69
+
70
+ def __init__(self, description: str = "", required: bool = True):
71
+ self.description = description
72
+ self.required = required
73
+
25
74
 
26
- Subclass this to create typed configurations for your world:
75
+ class StateConfig(BaseModel):
76
+ """Configuration for world state persistence.
27
77
 
28
- class CodeWorldConfig(WorldConfig):
78
+ The state directory is a git-tracked directory that persists across checkpoints.
79
+ At each checkpoint, the state directory is git bundled and uploaded as an artifact.
80
+ On restore, bootstrap.sh downloads and unbundles the state before the world starts.
81
+
82
+ Attributes:
83
+ enabled: Whether to enable state persistence (default: True).
84
+ path: Path to the state directory (default: /state).
85
+ """
86
+
87
+ enabled: bool = True
88
+ path: str = "/state"
89
+
90
+
91
+ class CheckpointConfig(BaseModel):
92
+ """Configuration for automatic checkpointing during world execution.
93
+
94
+ Attributes:
95
+ enabled: Whether to enable automatic checkpoints after steps.
96
+ interval: Create checkpoint every N steps (default: 1 = every step).
97
+ exclude_envs: Environment aliases to exclude from checkpoints (default: ["runtime"]).
98
+ """
99
+
100
+ enabled: bool = True
101
+ interval: int = 1
102
+ exclude_envs: list[str] = Field(default_factory=lambda: ["runtime"])
103
+
104
+
105
+ class RunConfig(BaseModel):
106
+ """Base configuration for running a world.
107
+
108
+ Subclass this with your world-specific fields, agents, secrets, and envs:
109
+
110
+ class CodeWorldConfig(RunConfig):
111
+ # World-specific fields
29
112
  repository_url: str
30
- checkout: str = "main"
31
113
  prompt: str
32
114
 
33
- The schema will be auto-generated from the Pydantic model.
115
+ # Agents (typed)
116
+ coder: Annotated[AgentConfig, Agent(description="Coding agent")]
117
+
118
+ # Secrets (typed)
119
+ git_token: Annotated[str | None, Secret(description="GitHub token")] = None
120
+
121
+ # Environments (typed)
122
+ gitea: Annotated[EnvConfig, Env(description="Git server")] = EnvFromArtifact(
123
+ artifact_id="abc123",
124
+ alias="gitea",
125
+ )
126
+
127
+ Attributes:
128
+ session_id: Unique Chronos session identifier
129
+ callback_url: Callback URL for status updates
130
+ plato_session: Serialized Plato session for connecting to existing VM session
131
+ checkpoint: Configuration for automatic checkpoints after steps
34
132
  """
35
133
 
134
+ session_id: str = ""
135
+ callback_url: str = ""
136
+ all_secrets: dict[str, str] = Field(default_factory=dict) # All secrets (world + agent)
137
+
138
+ # Serialized Plato session for connecting to VM and sending heartbeats
139
+ # This is the output of Session.dump() - used to restore session with Session.load()
140
+ plato_session: SerializedSession | None = None
141
+
142
+ # Checkpoint configuration for automatic snapshots after steps
143
+ checkpoint: CheckpointConfig = Field(default_factory=CheckpointConfig)
144
+
145
+ # State persistence configuration
146
+ state: StateConfig = Field(default_factory=StateConfig)
147
+
36
148
  model_config = {"extra": "allow"}
37
149
 
38
- def get(self, key: str, default: Any = None) -> Any:
39
- """Get a config value by key (for backwards compatibility)."""
40
- return getattr(self, key, default)
150
+ @classmethod
151
+ def get_field_annotations(cls) -> dict[str, Agent | Secret | Env | None]:
152
+ """Get Agent/Secret/Env annotations for each field."""
153
+ result: dict[str, Agent | Secret | Env | None] = {}
154
+
155
+ for field_name, field_info in cls.model_fields.items():
156
+ marker = None
157
+
158
+ # Pydantic stores Annotated metadata in field_info.metadata
159
+ for meta in field_info.metadata:
160
+ if isinstance(meta, (Agent, Secret, Env)):
161
+ marker = meta
162
+ break
163
+
164
+ result[field_name] = marker
165
+
166
+ return result
41
167
 
42
168
  @classmethod
43
169
  def get_json_schema(cls) -> dict:
44
- """Get JSON schema for this config class."""
45
- schema = cls.model_json_schema()
46
- # Remove Pydantic-specific keys
47
- schema.pop("title", None)
48
- return schema
170
+ """Get JSON schema with agents, secrets, and envs separated."""
171
+ # Get full Pydantic schema
172
+ full_schema = cls.model_json_schema()
173
+ full_schema.pop("title", None)
49
174
 
175
+ # Separate fields by annotation type
176
+ annotations = cls.get_field_annotations()
177
+ properties = full_schema.get("properties", {})
50
178
 
51
- # Type variable for typed config classes
52
- WorldConfigT = TypeVar("WorldConfigT", bound=WorldConfig)
179
+ world_properties = {}
180
+ agents = []
181
+ secrets = []
182
+ envs = []
53
183
 
184
+ # Skip runtime fields
185
+ runtime_fields = {"session_id", "callback_url", "all_secrets", "plato_session", "checkpoint", "state"}
54
186
 
55
- class RunConfig(BaseModel, Generic[WorldConfigT]):
56
- """Full configuration for running a world.
187
+ for field_name, prop_schema in properties.items():
188
+ if field_name in runtime_fields:
189
+ continue
57
190
 
58
- This is passed to the world runner and contains everything
59
- needed to execute a world. Use with a type parameter for typed config access:
191
+ marker = annotations.get(field_name)
60
192
 
61
- config: RunConfig[CodeWorldConfig] = ...
62
- config.world_config.repository_url # type-safe access
193
+ if isinstance(marker, Agent):
194
+ agents.append(
195
+ {
196
+ "name": field_name,
197
+ "description": marker.description,
198
+ "required": marker.required,
199
+ }
200
+ )
201
+ elif isinstance(marker, Secret):
202
+ secrets.append(
203
+ {
204
+ "name": field_name,
205
+ "description": marker.description,
206
+ "required": marker.required,
207
+ }
208
+ )
209
+ elif isinstance(marker, Env):
210
+ # Get default value for this env field
211
+ field_info = cls.model_fields.get(field_name)
212
+ default_value = None
213
+ if field_info and field_info.default is not None:
214
+ # Serialize the default EnvConfig to dict
215
+ default_env = field_info.default
216
+ if hasattr(default_env, "model_dump"):
217
+ default_value = default_env.model_dump()
218
+ elif isinstance(default_env, dict):
219
+ default_value = default_env
63
220
 
64
- Attributes:
65
- world_config: World-specific configuration (typed via generic)
66
- agents: Mapping of slot name to agent configuration
67
- secrets: Secret values (API keys, tokens, etc.)
68
- session_id: Unique session identifier
69
- callback_url: Full callback URL for Chronos (e.g., http://server/api/callback)
70
- """
221
+ envs.append(
222
+ {
223
+ "name": field_name,
224
+ "description": marker.description,
225
+ "required": marker.required,
226
+ "default": default_value,
227
+ }
228
+ )
229
+ else:
230
+ world_properties[field_name] = prop_schema
71
231
 
72
- world_config: WorldConfigT = Field(default_factory=WorldConfig) # type: ignore[assignment]
73
- agents: dict[str, AgentConfig] = Field(default_factory=dict)
74
- secrets: dict[str, str] = Field(default_factory=dict)
75
- session_id: str = ""
76
- callback_url: str = "" # Full callback URL for Chronos
232
+ # Compute required fields (excluding runtime and annotated fields)
233
+ required = [
234
+ r for r in full_schema.get("required", []) if r not in runtime_fields and annotations.get(r) is None
235
+ ]
77
236
 
78
- def get_agent(self, slot_name: str) -> AgentConfig | None:
79
- """Get agent config for a slot."""
80
- return self.agents.get(slot_name)
237
+ return {
238
+ "properties": world_properties,
239
+ "required": required,
240
+ "agents": agents,
241
+ "secrets": secrets,
242
+ "envs": envs,
243
+ }
81
244
 
82
- def get_secret(self, key: str, default: str | None = None) -> str | None:
83
- """Get a secret value."""
84
- return self.secrets.get(key, default)
245
+ def get_envs(self) -> list[EnvConfig]:
246
+ """Get all environment configurations from this config.
85
247
 
86
- @classmethod
87
- def from_file(
88
- cls,
89
- path: str | Path,
90
- config_class: type[WorldConfig] | None = None,
91
- ) -> RunConfig:
92
- """Load run config from a JSON file.
93
-
94
- Args:
95
- path: Path to JSON config file
96
- config_class: Optional WorldConfig subclass to parse world_config into.
97
- Defaults to WorldConfig if not provided.
248
+ Returns:
249
+ List of EnvConfig objects (EnvFromArtifact, EnvFromSimulator, or EnvFromResource)
98
250
  """
251
+ annotations = self.get_field_annotations()
252
+ envs: list[EnvConfig] = []
253
+
254
+ for field_name, marker in annotations.items():
255
+ if isinstance(marker, Env):
256
+ value = getattr(self, field_name, None)
257
+ if value is not None:
258
+ envs.append(value)
259
+
260
+ return envs
261
+
262
+ @classmethod
263
+ def from_file(cls, path: str | Path) -> RunConfig:
264
+ """Load config from a JSON file."""
99
265
  import json
100
266
 
101
267
  path = Path(path)
102
268
  with open(path) as f:
103
269
  data = json.load(f)
104
270
 
105
- # Parse agents into AgentConfig objects
106
- agents = {}
107
- for slot_name, agent_data in data.get("agents", {}).items():
108
- if isinstance(agent_data, dict):
109
- agents[slot_name] = AgentConfig(**agent_data)
110
- else:
111
- agents[slot_name] = AgentConfig(image=str(agent_data))
112
-
113
- # Use provided config class or default to WorldConfig
114
- config_cls = config_class or WorldConfig
115
- world_config = config_cls(**data.get("world_config", {}))
116
-
117
- return cls(
118
- world_config=world_config, # type: ignore[arg-type]
119
- agents=agents,
120
- secrets=data.get("secrets", {}),
121
- session_id=data.get("session_id", ""),
122
- callback_url=data.get("callback_url", ""),
123
- )
271
+ return cls._from_dict(data)
272
+
273
+ @classmethod
274
+ def _from_dict(cls, data: dict) -> RunConfig:
275
+ """Parse config from a dictionary, handling nested structures."""
276
+ annotations = cls.get_field_annotations()
277
+ parsed: dict[str, Any] = {}
278
+
279
+ # Handle world_config if present (for backwards compatibility)
280
+ world_config = data.pop("world_config", {})
281
+
282
+ # Handle agents dict -> individual agent fields
283
+ agents_dict = data.pop("agents", {})
284
+
285
+ # Handle secrets dict -> individual secret fields
286
+ secrets_dict = data.pop("secrets", {})
287
+
288
+ # Handle envs dict -> individual env fields
289
+ envs_dict = data.pop("envs", {})
290
+
291
+ # Merge world_config into top-level
292
+ parsed.update(world_config)
293
+ parsed.update(data)
294
+
295
+ # Map agents dict to typed fields
296
+ for field_name, marker in annotations.items():
297
+ if isinstance(marker, Agent) and field_name in agents_dict:
298
+ agent_data = agents_dict[field_name]
299
+ if isinstance(agent_data, dict):
300
+ parsed[field_name] = AgentConfig(**agent_data)
301
+ else:
302
+ parsed[field_name] = AgentConfig(image=str(agent_data))
303
+ elif isinstance(marker, Secret) and field_name in secrets_dict:
304
+ parsed[field_name] = secrets_dict[field_name]
305
+ elif isinstance(marker, Env) and field_name in envs_dict:
306
+ env_data = envs_dict[field_name]
307
+ if isinstance(env_data, dict):
308
+ parsed[field_name] = _parse_env_config(env_data)
309
+ else:
310
+ parsed[field_name] = env_data
311
+
312
+ # Store all secrets for agent use
313
+ parsed["all_secrets"] = secrets_dict
314
+
315
+ return cls(**parsed)
316
+
317
+
318
+ def _parse_env_config(data: dict) -> EnvConfig:
319
+ """Parse an env config dict into the appropriate type."""
320
+ if "artifact_id" in data:
321
+ return EnvFromArtifact(**data)
322
+ elif "sim_config" in data:
323
+ return EnvFromResource(**data)
324
+ else:
325
+ return EnvFromSimulator(**data)