plato-sdk-v2 2.1.11__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.
- plato/_sims_generator/__init__.py +19 -4
- plato/_sims_generator/instruction.py +203 -0
- plato/_sims_generator/templates/instruction/helpers.py.jinja +161 -0
- plato/_sims_generator/templates/instruction/init.py.jinja +43 -0
- plato/agents/__init__.py +15 -6
- plato/agents/logging.py +401 -0
- plato/agents/runner.py +98 -302
- plato/agents/trajectory.py +4 -4
- plato/chronos/models/__init__.py +1 -1
- plato/sims/cli.py +299 -123
- plato/sims/registry.py +77 -4
- plato/v1/cli/agent.py +10 -0
- plato/v1/cli/main.py +2 -0
- plato/v1/cli/pm.py +84 -44
- plato/v1/cli/sandbox.py +47 -9
- plato/v1/cli/sim.py +11 -0
- plato/v1/cli/verify.py +1269 -0
- plato/v1/cli/world.py +3 -0
- plato/v1/flow_executor.py +21 -17
- plato/v1/models/env.py +11 -11
- plato/v1/sdk.py +2 -2
- plato/v1/sync_env.py +11 -11
- plato/v1/sync_flow_executor.py +21 -17
- plato/v1/sync_sdk.py +4 -2
- plato/v2/async_/session.py +4 -4
- plato/v2/sync/session.py +4 -4
- plato/worlds/__init__.py +21 -2
- plato/worlds/base.py +222 -2
- plato/worlds/config.py +97 -7
- plato/worlds/runner.py +339 -1
- {plato_sdk_v2-2.1.11.dist-info → plato_sdk_v2-2.2.4.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.1.11.dist-info → plato_sdk_v2-2.2.4.dist-info}/RECORD +34 -29
- plato/agents/callback.py +0 -246
- {plato_sdk_v2-2.1.11.dist-info → plato_sdk_v2-2.2.4.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.1.11.dist-info → plato_sdk_v2-2.2.4.dist-info}/entry_points.txt +0 -0
plato/worlds/base.py
CHANGED
|
@@ -11,8 +11,14 @@ from pydantic import BaseModel, Field
|
|
|
11
11
|
from plato.worlds.config import RunConfig
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
+
from plato.v2.async_.environment import Environment
|
|
14
15
|
from plato.v2.async_.session import Session
|
|
15
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
|
|
21
|
+
|
|
16
22
|
logger = logging.getLogger(__name__)
|
|
17
23
|
|
|
18
24
|
# Global registry of worlds
|
|
@@ -100,6 +106,7 @@ class BaseWorld(ABC, Generic[ConfigT]):
|
|
|
100
106
|
self.logger = logging.getLogger(f"plato.worlds.{self.name}")
|
|
101
107
|
self._step_count: int = 0
|
|
102
108
|
self.plato_session = None
|
|
109
|
+
self._current_step_id: str | None = None
|
|
103
110
|
|
|
104
111
|
@classmethod
|
|
105
112
|
def get_config_class(cls) -> type[RunConfig]:
|
|
@@ -138,6 +145,7 @@ class BaseWorld(ABC, Generic[ConfigT]):
|
|
|
138
145
|
"required": schema.get("required", []),
|
|
139
146
|
"agents": schema.get("agents", []),
|
|
140
147
|
"secrets": schema.get("secrets", []),
|
|
148
|
+
"envs": schema.get("envs", []),
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
@abstractmethod
|
|
@@ -166,6 +174,9 @@ class BaseWorld(ABC, Generic[ConfigT]):
|
|
|
166
174
|
This is called automatically during run() to restore the session
|
|
167
175
|
and start sending heartbeats while the world runs.
|
|
168
176
|
"""
|
|
177
|
+
if not self.config.plato_session:
|
|
178
|
+
return
|
|
179
|
+
|
|
169
180
|
try:
|
|
170
181
|
from plato.v2.async_.session import Session
|
|
171
182
|
|
|
@@ -184,6 +195,163 @@ class BaseWorld(ABC, Generic[ConfigT]):
|
|
|
184
195
|
except Exception as e:
|
|
185
196
|
self.logger.warning(f"Error stopping Plato heartbeat: {e}")
|
|
186
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
|
+
|
|
187
355
|
async def run(self, config: ConfigT) -> None:
|
|
188
356
|
"""Run the world: reset -> step until done -> close.
|
|
189
357
|
|
|
@@ -195,16 +363,56 @@ class BaseWorld(ABC, Generic[ConfigT]):
|
|
|
195
363
|
|
|
196
364
|
self.logger.info(f"Starting world '{self.name}'")
|
|
197
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
|
+
|
|
198
373
|
# Connect to Plato session if configured (for heartbeats)
|
|
199
374
|
await self._connect_plato_session()
|
|
200
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
|
+
|
|
201
384
|
try:
|
|
202
|
-
|
|
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)})
|
|
203
390
|
self.logger.info(f"World reset complete: {obs}")
|
|
204
391
|
|
|
205
392
|
while True:
|
|
206
393
|
self._step_count += 1
|
|
207
|
-
|
|
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
|
+
|
|
208
416
|
self.logger.info(f"Step {self._step_count}: done={result.done}")
|
|
209
417
|
|
|
210
418
|
if result.done:
|
|
@@ -213,4 +421,16 @@ class BaseWorld(ABC, Generic[ConfigT]):
|
|
|
213
421
|
finally:
|
|
214
422
|
await self.close()
|
|
215
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
|
+
|
|
216
436
|
self.logger.info(f"World '{self.name}' completed after {self._step_count} steps")
|
plato/worlds/config.py
CHANGED
|
@@ -7,8 +7,16 @@ 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
|
+
)
|
|
10
15
|
from plato.v2.async_.session import SerializedSession
|
|
11
16
|
|
|
17
|
+
# Union type for environment configurations
|
|
18
|
+
EnvConfig = EnvFromArtifact | EnvFromSimulator | EnvFromResource
|
|
19
|
+
|
|
12
20
|
|
|
13
21
|
class AgentConfig(BaseModel):
|
|
14
22
|
"""Configuration for an agent.
|
|
@@ -46,10 +54,28 @@ class Secret:
|
|
|
46
54
|
self.required = required
|
|
47
55
|
|
|
48
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
|
+
|
|
74
|
+
|
|
49
75
|
class RunConfig(BaseModel):
|
|
50
76
|
"""Base configuration for running a world.
|
|
51
77
|
|
|
52
|
-
Subclass this with your world-specific fields, agents, and
|
|
78
|
+
Subclass this with your world-specific fields, agents, secrets, and envs:
|
|
53
79
|
|
|
54
80
|
class CodeWorldConfig(RunConfig):
|
|
55
81
|
# World-specific fields
|
|
@@ -62,6 +88,12 @@ class RunConfig(BaseModel):
|
|
|
62
88
|
# Secrets (typed)
|
|
63
89
|
git_token: Annotated[str | None, Secret(description="GitHub token")] = None
|
|
64
90
|
|
|
91
|
+
# Environments (typed)
|
|
92
|
+
gitea: Annotated[EnvConfig, Env(description="Git server")] = EnvFromArtifact(
|
|
93
|
+
artifact_id="abc123",
|
|
94
|
+
alias="gitea",
|
|
95
|
+
)
|
|
96
|
+
|
|
65
97
|
Attributes:
|
|
66
98
|
session_id: Unique Chronos session identifier
|
|
67
99
|
callback_url: Callback URL for status updates
|
|
@@ -74,21 +106,21 @@ class RunConfig(BaseModel):
|
|
|
74
106
|
|
|
75
107
|
# Serialized Plato session for connecting to VM and sending heartbeats
|
|
76
108
|
# This is the output of Session.dump() - used to restore session with Session.load()
|
|
77
|
-
plato_session: SerializedSession
|
|
109
|
+
plato_session: SerializedSession | None = None
|
|
78
110
|
|
|
79
111
|
model_config = {"extra": "allow"}
|
|
80
112
|
|
|
81
113
|
@classmethod
|
|
82
|
-
def get_field_annotations(cls) -> dict[str, Agent | Secret | None]:
|
|
83
|
-
"""Get Agent/Secret annotations for each field."""
|
|
84
|
-
result: dict[str, Agent | Secret | None] = {}
|
|
114
|
+
def get_field_annotations(cls) -> dict[str, Agent | Secret | Env | None]:
|
|
115
|
+
"""Get Agent/Secret/Env annotations for each field."""
|
|
116
|
+
result: dict[str, Agent | Secret | Env | None] = {}
|
|
85
117
|
|
|
86
118
|
for field_name, field_info in cls.model_fields.items():
|
|
87
119
|
marker = None
|
|
88
120
|
|
|
89
121
|
# Pydantic stores Annotated metadata in field_info.metadata
|
|
90
122
|
for meta in field_info.metadata:
|
|
91
|
-
if isinstance(meta, (Agent, Secret)):
|
|
123
|
+
if isinstance(meta, (Agent, Secret, Env)):
|
|
92
124
|
marker = meta
|
|
93
125
|
break
|
|
94
126
|
|
|
@@ -98,7 +130,7 @@ class RunConfig(BaseModel):
|
|
|
98
130
|
|
|
99
131
|
@classmethod
|
|
100
132
|
def get_json_schema(cls) -> dict:
|
|
101
|
-
"""Get JSON schema with agents and
|
|
133
|
+
"""Get JSON schema with agents, secrets, and envs separated."""
|
|
102
134
|
# Get full Pydantic schema
|
|
103
135
|
full_schema = cls.model_json_schema()
|
|
104
136
|
full_schema.pop("title", None)
|
|
@@ -110,6 +142,7 @@ class RunConfig(BaseModel):
|
|
|
110
142
|
world_properties = {}
|
|
111
143
|
agents = []
|
|
112
144
|
secrets = []
|
|
145
|
+
envs = []
|
|
113
146
|
|
|
114
147
|
# Skip runtime fields
|
|
115
148
|
runtime_fields = {"session_id", "callback_url", "all_secrets", "plato_session"}
|
|
@@ -136,6 +169,26 @@ class RunConfig(BaseModel):
|
|
|
136
169
|
"required": marker.required,
|
|
137
170
|
}
|
|
138
171
|
)
|
|
172
|
+
elif isinstance(marker, Env):
|
|
173
|
+
# Get default value for this env field
|
|
174
|
+
field_info = cls.model_fields.get(field_name)
|
|
175
|
+
default_value = None
|
|
176
|
+
if field_info and field_info.default is not None:
|
|
177
|
+
# Serialize the default EnvConfig to dict
|
|
178
|
+
default_env = field_info.default
|
|
179
|
+
if hasattr(default_env, "model_dump"):
|
|
180
|
+
default_value = default_env.model_dump()
|
|
181
|
+
elif isinstance(default_env, dict):
|
|
182
|
+
default_value = default_env
|
|
183
|
+
|
|
184
|
+
envs.append(
|
|
185
|
+
{
|
|
186
|
+
"name": field_name,
|
|
187
|
+
"description": marker.description,
|
|
188
|
+
"required": marker.required,
|
|
189
|
+
"default": default_value,
|
|
190
|
+
}
|
|
191
|
+
)
|
|
139
192
|
else:
|
|
140
193
|
world_properties[field_name] = prop_schema
|
|
141
194
|
|
|
@@ -149,8 +202,26 @@ class RunConfig(BaseModel):
|
|
|
149
202
|
"required": required,
|
|
150
203
|
"agents": agents,
|
|
151
204
|
"secrets": secrets,
|
|
205
|
+
"envs": envs,
|
|
152
206
|
}
|
|
153
207
|
|
|
208
|
+
def get_envs(self) -> list[EnvConfig]:
|
|
209
|
+
"""Get all environment configurations from this config.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List of EnvConfig objects (EnvFromArtifact, EnvFromSimulator, or EnvFromResource)
|
|
213
|
+
"""
|
|
214
|
+
annotations = self.get_field_annotations()
|
|
215
|
+
envs: list[EnvConfig] = []
|
|
216
|
+
|
|
217
|
+
for field_name, marker in annotations.items():
|
|
218
|
+
if isinstance(marker, Env):
|
|
219
|
+
value = getattr(self, field_name, None)
|
|
220
|
+
if value is not None:
|
|
221
|
+
envs.append(value)
|
|
222
|
+
|
|
223
|
+
return envs
|
|
224
|
+
|
|
154
225
|
@classmethod
|
|
155
226
|
def from_file(cls, path: str | Path) -> RunConfig:
|
|
156
227
|
"""Load config from a JSON file."""
|
|
@@ -177,6 +248,9 @@ class RunConfig(BaseModel):
|
|
|
177
248
|
# Handle secrets dict -> individual secret fields
|
|
178
249
|
secrets_dict = data.pop("secrets", {})
|
|
179
250
|
|
|
251
|
+
# Handle envs dict -> individual env fields
|
|
252
|
+
envs_dict = data.pop("envs", {})
|
|
253
|
+
|
|
180
254
|
# Merge world_config into top-level
|
|
181
255
|
parsed.update(world_config)
|
|
182
256
|
parsed.update(data)
|
|
@@ -191,8 +265,24 @@ class RunConfig(BaseModel):
|
|
|
191
265
|
parsed[field_name] = AgentConfig(image=str(agent_data))
|
|
192
266
|
elif isinstance(marker, Secret) and field_name in secrets_dict:
|
|
193
267
|
parsed[field_name] = secrets_dict[field_name]
|
|
268
|
+
elif isinstance(marker, Env) and field_name in envs_dict:
|
|
269
|
+
env_data = envs_dict[field_name]
|
|
270
|
+
if isinstance(env_data, dict):
|
|
271
|
+
parsed[field_name] = _parse_env_config(env_data)
|
|
272
|
+
else:
|
|
273
|
+
parsed[field_name] = env_data
|
|
194
274
|
|
|
195
275
|
# Store all secrets for agent use
|
|
196
276
|
parsed["all_secrets"] = secrets_dict
|
|
197
277
|
|
|
198
278
|
return cls(**parsed)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _parse_env_config(data: dict) -> EnvConfig:
|
|
282
|
+
"""Parse an env config dict into the appropriate type."""
|
|
283
|
+
if "artifact_id" in data:
|
|
284
|
+
return EnvFromArtifact(**data)
|
|
285
|
+
elif "sim_config" in data:
|
|
286
|
+
return EnvFromResource(**data)
|
|
287
|
+
else:
|
|
288
|
+
return EnvFromSimulator(**data)
|