plato-sdk-v2 2.3.10__py3-none-any.whl → 2.4.0__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/agents/config.py +14 -6
- plato/agents/otel.py +16 -3
- plato/agents/runner.py +93 -24
- plato/v1/cli/chronos.py +531 -0
- plato/v1/cli/pm.py +3 -3
- plato/v1/cli/sandbox.py +53 -4
- plato/v1/cli/templates/world-runner.Dockerfile +27 -0
- plato/worlds/base.py +66 -1
- plato/worlds/runner.py +1 -458
- {plato_sdk_v2-2.3.10.dist-info → plato_sdk_v2-2.4.0.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.3.10.dist-info → plato_sdk_v2-2.4.0.dist-info}/RECORD +13 -12
- {plato_sdk_v2-2.3.10.dist-info → plato_sdk_v2-2.4.0.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.3.10.dist-info → plato_sdk_v2-2.4.0.dist-info}/entry_points.txt +0 -0
plato/agents/config.py
CHANGED
|
@@ -18,7 +18,6 @@ Example:
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import json
|
|
21
|
-
from pathlib import Path
|
|
22
21
|
from typing import Any
|
|
23
22
|
|
|
24
23
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
@@ -147,9 +146,18 @@ class AgentConfig(BaseSettings):
|
|
|
147
146
|
return result
|
|
148
147
|
|
|
149
148
|
@classmethod
|
|
150
|
-
def
|
|
151
|
-
"""Load config from
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
def from_env(cls) -> AgentConfig:
|
|
150
|
+
"""Load config from AGENT_CONFIG_B64 environment variable.
|
|
151
|
+
|
|
152
|
+
The runner passes config as base64-encoded JSON in the
|
|
153
|
+
AGENT_CONFIG_B64 environment variable.
|
|
154
|
+
"""
|
|
155
|
+
import base64
|
|
156
|
+
import os
|
|
157
|
+
|
|
158
|
+
config_b64 = os.environ.get("AGENT_CONFIG_B64")
|
|
159
|
+
if not config_b64:
|
|
160
|
+
raise ValueError("AGENT_CONFIG_B64 environment variable not set")
|
|
161
|
+
config_json = base64.b64decode(config_b64).decode()
|
|
162
|
+
data = json.loads(config_json)
|
|
155
163
|
return cls(**data)
|
plato/agents/otel.py
CHANGED
|
@@ -164,8 +164,12 @@ def init_tracing(
|
|
|
164
164
|
_module_logger.error(f"Failed to initialize tracing: {e}")
|
|
165
165
|
|
|
166
166
|
|
|
167
|
-
def shutdown_tracing() -> None:
|
|
168
|
-
"""Shutdown the tracer provider and flush spans.
|
|
167
|
+
def shutdown_tracing(timeout_millis: int = 30000) -> None:
|
|
168
|
+
"""Shutdown the tracer provider and flush spans.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
timeout_millis: Timeout in milliseconds to wait for flush (default 30s)
|
|
172
|
+
"""
|
|
169
173
|
global _tracer_provider, _initialized, _log_handler
|
|
170
174
|
|
|
171
175
|
# Remove log handler
|
|
@@ -179,9 +183,18 @@ def shutdown_tracing() -> None:
|
|
|
179
183
|
|
|
180
184
|
if _tracer_provider:
|
|
181
185
|
try:
|
|
186
|
+
# Force flush all pending spans before shutdown
|
|
187
|
+
print(f"[OTel] Flushing spans (timeout={timeout_millis}ms)...")
|
|
188
|
+
flush_success = _tracer_provider.force_flush(timeout_millis=timeout_millis)
|
|
189
|
+
if flush_success:
|
|
190
|
+
print("[OTel] Span flush completed successfully")
|
|
191
|
+
else:
|
|
192
|
+
print("[OTel] Span flush timed out or failed")
|
|
193
|
+
|
|
182
194
|
_tracer_provider.shutdown()
|
|
183
|
-
|
|
195
|
+
print("[OTel] Tracing shutdown complete")
|
|
184
196
|
except Exception as e:
|
|
197
|
+
print(f"[OTel] Error shutting down tracer: {e}")
|
|
185
198
|
_module_logger.warning(f"Error shutting down tracer: {e}")
|
|
186
199
|
|
|
187
200
|
_tracer_provider = None
|
plato/agents/runner.py
CHANGED
|
@@ -10,16 +10,15 @@ Agents emit their own OTel spans for trajectory events. This runner:
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
|
+
import base64
|
|
13
14
|
import json
|
|
14
15
|
import logging
|
|
15
16
|
import os
|
|
16
17
|
import platform
|
|
17
|
-
import
|
|
18
|
+
import uuid
|
|
18
19
|
|
|
19
20
|
from opentelemetry import trace
|
|
20
21
|
|
|
21
|
-
from plato.agents.artifacts import upload_artifacts
|
|
22
|
-
|
|
23
22
|
logger = logging.getLogger(__name__)
|
|
24
23
|
|
|
25
24
|
|
|
@@ -28,10 +27,10 @@ async def run_agent(
|
|
|
28
27
|
config: dict,
|
|
29
28
|
secrets: dict[str, str],
|
|
30
29
|
instruction: str,
|
|
31
|
-
workspace: str,
|
|
30
|
+
workspace: str | None = None,
|
|
32
31
|
logs_dir: str | None = None,
|
|
33
32
|
pull: bool = True,
|
|
34
|
-
) ->
|
|
33
|
+
) -> str:
|
|
35
34
|
"""Run an agent in a Docker container.
|
|
36
35
|
|
|
37
36
|
Args:
|
|
@@ -39,15 +38,19 @@ async def run_agent(
|
|
|
39
38
|
config: Agent configuration dict
|
|
40
39
|
secrets: Secret values (API keys, etc.)
|
|
41
40
|
instruction: Task instruction for the agent
|
|
42
|
-
workspace:
|
|
43
|
-
logs_dir:
|
|
41
|
+
workspace: Docker volume name for workspace (created if None)
|
|
42
|
+
logs_dir: Ignored (kept for backwards compatibility)
|
|
44
43
|
pull: Whether to pull the image first
|
|
45
44
|
|
|
45
|
+
Returns:
|
|
46
|
+
The container name that was created (for cleanup purposes)
|
|
47
|
+
|
|
46
48
|
Note: Agents handle their own OTel tracing. This runner only passes
|
|
47
49
|
the trace context (TRACEPARENT) so agent spans link to the parent step.
|
|
48
|
-
"""
|
|
49
|
-
logs_dir = logs_dir or tempfile.mkdtemp(prefix="agent_logs_")
|
|
50
50
|
|
|
51
|
+
Note: This uses Docker volumes (not bind mounts) for DIND compatibility.
|
|
52
|
+
The workspace parameter should be a Docker volume name.
|
|
53
|
+
"""
|
|
51
54
|
# Get session info from environment variables
|
|
52
55
|
session_id = os.environ.get("SESSION_ID")
|
|
53
56
|
otel_url = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
@@ -64,15 +67,41 @@ async def run_agent(
|
|
|
64
67
|
)
|
|
65
68
|
await pull_proc.wait()
|
|
66
69
|
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
# Encode config as base64 to pass via environment variable
|
|
71
|
+
# This avoids file mount issues in Docker-in-Docker scenarios
|
|
72
|
+
config_json = json.dumps(config)
|
|
73
|
+
config_b64 = base64.b64encode(config_json.encode()).decode()
|
|
74
|
+
|
|
75
|
+
# Generate a unique container name for inspection
|
|
76
|
+
container_name = f"agent-{uuid.uuid4().hex[:8]}"
|
|
77
|
+
|
|
78
|
+
# Use WORKSPACE_VOLUME env var if set (for DIND compatibility)
|
|
79
|
+
# Otherwise create a new volume
|
|
80
|
+
workspace_volume = os.environ.get("WORKSPACE_VOLUME") or workspace or f"workspace-{uuid.uuid4().hex[:8]}"
|
|
81
|
+
if not os.environ.get("WORKSPACE_VOLUME") and not workspace:
|
|
82
|
+
await asyncio.create_subprocess_exec(
|
|
83
|
+
"docker",
|
|
84
|
+
"volume",
|
|
85
|
+
"create",
|
|
86
|
+
workspace_volume,
|
|
87
|
+
stdout=asyncio.subprocess.DEVNULL,
|
|
88
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Create logs volume
|
|
92
|
+
logs_volume = f"logs-{uuid.uuid4().hex[:8]}"
|
|
93
|
+
await asyncio.create_subprocess_exec(
|
|
94
|
+
"docker",
|
|
95
|
+
"volume",
|
|
96
|
+
"create",
|
|
97
|
+
logs_volume,
|
|
98
|
+
stdout=asyncio.subprocess.DEVNULL,
|
|
99
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
100
|
+
)
|
|
72
101
|
|
|
73
102
|
try:
|
|
74
103
|
# Build docker command
|
|
75
|
-
docker_cmd = ["docker", "run", "--rm", "--privileged"]
|
|
104
|
+
docker_cmd = ["docker", "run", "--rm", "--privileged", "--name", container_name]
|
|
76
105
|
|
|
77
106
|
# Determine if we need host networking
|
|
78
107
|
use_host_network = False
|
|
@@ -97,18 +126,19 @@ async def run_agent(
|
|
|
97
126
|
if use_host_network:
|
|
98
127
|
docker_cmd.extend(["--network=host", "--add-host=localhost:127.0.0.1"])
|
|
99
128
|
|
|
129
|
+
# Use Docker volumes instead of bind mounts for DIND compatibility
|
|
100
130
|
docker_cmd.extend(
|
|
101
131
|
[
|
|
102
132
|
"-v",
|
|
103
|
-
f"{
|
|
104
|
-
"-v",
|
|
105
|
-
f"{logs_dir}:/logs",
|
|
133
|
+
f"{workspace_volume}:/workspace",
|
|
106
134
|
"-v",
|
|
107
|
-
f"{
|
|
135
|
+
f"{logs_volume}:/logs",
|
|
108
136
|
"-v",
|
|
109
137
|
"/var/run/docker.sock:/var/run/docker.sock",
|
|
110
138
|
"-w",
|
|
111
139
|
"/workspace",
|
|
140
|
+
"-e",
|
|
141
|
+
f"AGENT_CONFIG_B64={config_b64}",
|
|
112
142
|
]
|
|
113
143
|
)
|
|
114
144
|
|
|
@@ -151,6 +181,8 @@ async def run_agent(
|
|
|
151
181
|
# Pass instruction via CLI arg
|
|
152
182
|
docker_cmd.extend(["--instruction", instruction])
|
|
153
183
|
|
|
184
|
+
logger.info(f"Starting container: {container_name}")
|
|
185
|
+
|
|
154
186
|
# Run container - agents emit their own OTel spans
|
|
155
187
|
# Use large limit to handle agents that output long lines (e.g., JSON with file contents)
|
|
156
188
|
process = await asyncio.create_subprocess_exec(
|
|
@@ -160,7 +192,33 @@ async def run_agent(
|
|
|
160
192
|
limit=100 * 1024 * 1024, # 100MB buffer limit
|
|
161
193
|
)
|
|
162
194
|
|
|
163
|
-
#
|
|
195
|
+
# Get and print container IP in background
|
|
196
|
+
async def print_container_ip():
|
|
197
|
+
await asyncio.sleep(3) # Wait for container to start
|
|
198
|
+
try:
|
|
199
|
+
inspect_proc = await asyncio.create_subprocess_exec(
|
|
200
|
+
"docker",
|
|
201
|
+
"inspect",
|
|
202
|
+
"-f",
|
|
203
|
+
"{{.NetworkSettings.IPAddress}}",
|
|
204
|
+
container_name,
|
|
205
|
+
stdout=asyncio.subprocess.PIPE,
|
|
206
|
+
stderr=asyncio.subprocess.PIPE,
|
|
207
|
+
)
|
|
208
|
+
stdout, _ = await inspect_proc.communicate()
|
|
209
|
+
container_ip = stdout.decode().strip()
|
|
210
|
+
if container_ip:
|
|
211
|
+
logger.info("=" * 50)
|
|
212
|
+
logger.info(f"Container: {container_name}")
|
|
213
|
+
logger.info(f"Container IP: {container_ip}")
|
|
214
|
+
logger.info(f"noVNC: http://{container_ip}:6080")
|
|
215
|
+
logger.info("=" * 50)
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
asyncio.create_task(print_container_ip())
|
|
220
|
+
|
|
221
|
+
# Stream and capture output for error reporting using chunked reads to handle large lines
|
|
164
222
|
output_lines: list[str] = []
|
|
165
223
|
assert process.stdout is not None
|
|
166
224
|
buffer = ""
|
|
@@ -176,10 +234,13 @@ async def run_agent(
|
|
|
176
234
|
while "\n" in buffer:
|
|
177
235
|
line, buffer = buffer.split("\n", 1)
|
|
178
236
|
output_lines.append(line)
|
|
237
|
+
# Print agent output in real-time
|
|
238
|
+
print(f"[agent] {line}")
|
|
179
239
|
|
|
180
240
|
# Handle any remaining content in buffer
|
|
181
241
|
if buffer.strip():
|
|
182
242
|
output_lines.append(buffer)
|
|
243
|
+
print(f"[agent] {buffer}")
|
|
183
244
|
|
|
184
245
|
await process.wait()
|
|
185
246
|
|
|
@@ -189,8 +250,16 @@ async def run_agent(
|
|
|
189
250
|
raise RuntimeError(f"Agent failed with exit code {exit_code}\n\nAgent output:\n{error_context}")
|
|
190
251
|
|
|
191
252
|
finally:
|
|
192
|
-
|
|
253
|
+
# Clean up volumes
|
|
254
|
+
await asyncio.create_subprocess_exec(
|
|
255
|
+
"docker",
|
|
256
|
+
"volume",
|
|
257
|
+
"rm",
|
|
258
|
+
"-f",
|
|
259
|
+
logs_volume,
|
|
260
|
+
stdout=asyncio.subprocess.DEVNULL,
|
|
261
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
262
|
+
)
|
|
263
|
+
# Note: workspace_volume is not cleaned up as it may be shared
|
|
193
264
|
|
|
194
|
-
|
|
195
|
-
if upload_url:
|
|
196
|
-
await upload_artifacts(upload_url, logs_dir)
|
|
265
|
+
return container_name
|