plato-sdk-v2 2.3.3__py3-none-any.whl → 2.4.1__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/__init__.py +24 -16
- plato/agents/artifacts.py +108 -0
- plato/agents/config.py +16 -13
- plato/agents/otel.py +261 -0
- plato/agents/runner.py +223 -149
- plato/chronos/models/__init__.py +9 -1
- plato/v1/cli/chronos.py +788 -0
- plato/v1/cli/main.py +2 -0
- plato/v1/cli/pm.py +3 -3
- plato/v1/cli/sandbox.py +58 -6
- plato/v1/cli/ssh.py +21 -14
- plato/v1/cli/templates/world-runner.Dockerfile +27 -0
- plato/v1/cli/utils.py +32 -12
- plato/worlds/README.md +2 -1
- plato/worlds/base.py +222 -101
- plato/worlds/config.py +5 -3
- plato/worlds/runner.py +1 -391
- {plato_sdk_v2-2.3.3.dist-info → plato_sdk_v2-2.4.1.dist-info}/METADATA +4 -3
- {plato_sdk_v2-2.3.3.dist-info → plato_sdk_v2-2.4.1.dist-info}/RECORD +21 -24
- plato/agents/logging.py +0 -515
- plato/chronos/api/callback/__init__.py +0 -11
- plato/chronos/api/callback/push_agent_logs.py +0 -61
- plato/chronos/api/callback/update_agent_status.py +0 -57
- plato/chronos/api/callback/upload_artifacts.py +0 -59
- plato/chronos/api/callback/upload_logs_zip.py +0 -57
- plato/chronos/api/callback/upload_trajectory.py +0 -57
- {plato_sdk_v2-2.3.3.dist-info → plato_sdk_v2-2.4.1.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.3.3.dist-info → plato_sdk_v2-2.4.1.dist-info}/entry_points.txt +0 -0
plato/agents/logging.py
DELETED
|
@@ -1,515 +0,0 @@
|
|
|
1
|
-
"""Chronos logging utilities.
|
|
2
|
-
|
|
3
|
-
Simple singleton-based logging for Chronos events and spans.
|
|
4
|
-
|
|
5
|
-
Usage:
|
|
6
|
-
from plato.agents.logging import init_logging, span, log_event, upload_logs
|
|
7
|
-
|
|
8
|
-
# Initialize once (typically in world setup)
|
|
9
|
-
init_logging(
|
|
10
|
-
callback_url="http://chronos/api/callback",
|
|
11
|
-
session_id="session-123",
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
# Use spans anywhere - parent tracking is automatic
|
|
15
|
-
async with span("clone_repo") as s:
|
|
16
|
-
s.log("Cloning...")
|
|
17
|
-
|
|
18
|
-
async with span("checkout"): # Automatically nested under clone_repo
|
|
19
|
-
s.log("Checking out...")
|
|
20
|
-
|
|
21
|
-
# Log events - parent is automatically the current span
|
|
22
|
-
await log_event(span_type="my_event", content="Something happened")
|
|
23
|
-
|
|
24
|
-
# Upload artifacts (zip of directory)
|
|
25
|
-
await upload_artifacts(dir_path="/path/to/logs")
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
from __future__ import annotations
|
|
29
|
-
|
|
30
|
-
import base64
|
|
31
|
-
import io
|
|
32
|
-
import logging
|
|
33
|
-
import zipfile
|
|
34
|
-
from datetime import datetime, timezone
|
|
35
|
-
from pathlib import Path
|
|
36
|
-
from typing import Any
|
|
37
|
-
from uuid import uuid4
|
|
38
|
-
|
|
39
|
-
import httpx
|
|
40
|
-
|
|
41
|
-
logger = logging.getLogger(__name__)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class _ChronosLogger:
|
|
45
|
-
"""Internal singleton logger for Chronos events."""
|
|
46
|
-
|
|
47
|
-
_instance: _ChronosLogger | None = None
|
|
48
|
-
|
|
49
|
-
def __init__(
|
|
50
|
-
self,
|
|
51
|
-
callback_url: str,
|
|
52
|
-
session_id: str,
|
|
53
|
-
parent_event_id: str | None = None,
|
|
54
|
-
):
|
|
55
|
-
self.callback_url = callback_url.rstrip("/")
|
|
56
|
-
self.session_id = session_id
|
|
57
|
-
self.parent_event_id = parent_event_id
|
|
58
|
-
self._current_span_id: str | None = None
|
|
59
|
-
self._enabled = bool(callback_url and session_id)
|
|
60
|
-
|
|
61
|
-
@property
|
|
62
|
-
def enabled(self) -> bool:
|
|
63
|
-
return self._enabled
|
|
64
|
-
|
|
65
|
-
@property
|
|
66
|
-
def current_parent_id(self) -> str | None:
|
|
67
|
-
"""Get the current parent ID (current span or root parent)."""
|
|
68
|
-
return self._current_span_id or self.parent_event_id
|
|
69
|
-
|
|
70
|
-
async def log_event(
|
|
71
|
-
self,
|
|
72
|
-
span_type: str,
|
|
73
|
-
content: str = "",
|
|
74
|
-
source: str = "agent",
|
|
75
|
-
log_type: str | None = None,
|
|
76
|
-
step_number: int | None = None,
|
|
77
|
-
extra: dict[str, Any] | None = None,
|
|
78
|
-
event_id: str | None = None,
|
|
79
|
-
parent_id: str | None = None,
|
|
80
|
-
started_at: str | None = None,
|
|
81
|
-
ended_at: str | None = None,
|
|
82
|
-
) -> bool:
|
|
83
|
-
"""Log an event to Chronos."""
|
|
84
|
-
if not self._enabled:
|
|
85
|
-
return False
|
|
86
|
-
|
|
87
|
-
try:
|
|
88
|
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
89
|
-
response = await client.post(
|
|
90
|
-
f"{self.callback_url}/event",
|
|
91
|
-
json={
|
|
92
|
-
"session_id": self.session_id,
|
|
93
|
-
"span_type": span_type,
|
|
94
|
-
"content": content,
|
|
95
|
-
"source": source,
|
|
96
|
-
"log_type": log_type,
|
|
97
|
-
"step_number": step_number,
|
|
98
|
-
"extra": extra,
|
|
99
|
-
"event_id": event_id,
|
|
100
|
-
"parent_id": parent_id,
|
|
101
|
-
"started_at": started_at,
|
|
102
|
-
"ended_at": ended_at,
|
|
103
|
-
},
|
|
104
|
-
)
|
|
105
|
-
if response.status_code == 200:
|
|
106
|
-
return True
|
|
107
|
-
else:
|
|
108
|
-
logger.warning(f"Failed to log event: {response.status_code} {response.text}")
|
|
109
|
-
return False
|
|
110
|
-
except Exception as e:
|
|
111
|
-
logger.warning(f"Failed to log event to Chronos: {e}")
|
|
112
|
-
return False
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
class SpanContext:
|
|
116
|
-
"""Context manager for logging spans with automatic timing and nesting."""
|
|
117
|
-
|
|
118
|
-
def __init__(
|
|
119
|
-
self,
|
|
120
|
-
name: str,
|
|
121
|
-
span_type: str = "span",
|
|
122
|
-
source: str = "agent",
|
|
123
|
-
extra: dict[str, Any] | None = None,
|
|
124
|
-
):
|
|
125
|
-
self._name = name
|
|
126
|
-
self._span_type = span_type
|
|
127
|
-
self._source = source
|
|
128
|
-
self._extra = extra or {}
|
|
129
|
-
self._event_id: str | None = None
|
|
130
|
-
self._started_at: datetime | None = None
|
|
131
|
-
self._child_logs: list[str] = []
|
|
132
|
-
self._previous_span_id: str | None = None
|
|
133
|
-
|
|
134
|
-
async def __aenter__(self) -> SpanContext:
|
|
135
|
-
"""Log span start and set as current span."""
|
|
136
|
-
self._started_at = datetime.now(timezone.utc)
|
|
137
|
-
self._event_id = str(uuid4())
|
|
138
|
-
|
|
139
|
-
chronos = _ChronosLogger._instance
|
|
140
|
-
if chronos and chronos.enabled:
|
|
141
|
-
# Save previous span and set self as current
|
|
142
|
-
self._previous_span_id = chronos._current_span_id
|
|
143
|
-
parent_id = chronos.current_parent_id
|
|
144
|
-
|
|
145
|
-
await chronos.log_event(
|
|
146
|
-
span_type=self._span_type,
|
|
147
|
-
content=f"{self._name} started",
|
|
148
|
-
source=self._source,
|
|
149
|
-
event_id=self._event_id,
|
|
150
|
-
parent_id=parent_id,
|
|
151
|
-
started_at=self._started_at.isoformat(),
|
|
152
|
-
extra=self._extra,
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
# Set self as the current span for child events
|
|
156
|
-
chronos._current_span_id = self._event_id
|
|
157
|
-
|
|
158
|
-
return self
|
|
159
|
-
|
|
160
|
-
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
161
|
-
"""Log span end and restore previous span."""
|
|
162
|
-
ended_at = datetime.now(timezone.utc)
|
|
163
|
-
status = "failed" if exc_type else "completed"
|
|
164
|
-
|
|
165
|
-
chronos = _ChronosLogger._instance
|
|
166
|
-
if chronos and chronos.enabled:
|
|
167
|
-
# Restore previous span
|
|
168
|
-
chronos._current_span_id = self._previous_span_id
|
|
169
|
-
parent_id = self._previous_span_id or chronos.parent_event_id
|
|
170
|
-
|
|
171
|
-
# Update extra with any child logs
|
|
172
|
-
extra = {**self._extra}
|
|
173
|
-
if self._child_logs:
|
|
174
|
-
extra["logs"] = self._child_logs
|
|
175
|
-
if exc_type:
|
|
176
|
-
extra["error"] = str(exc_val)
|
|
177
|
-
|
|
178
|
-
await chronos.log_event(
|
|
179
|
-
span_type=self._span_type,
|
|
180
|
-
content=f"{self._name} {status}",
|
|
181
|
-
source=self._source,
|
|
182
|
-
event_id=self._event_id,
|
|
183
|
-
parent_id=parent_id,
|
|
184
|
-
started_at=self._started_at.isoformat() if self._started_at else None,
|
|
185
|
-
ended_at=ended_at.isoformat(),
|
|
186
|
-
extra=extra,
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
def log(self, message: str) -> None:
|
|
190
|
-
"""Add a log message to the span."""
|
|
191
|
-
self._child_logs.append(message)
|
|
192
|
-
logger.info(f"[{self._name}] {message}")
|
|
193
|
-
|
|
194
|
-
def set_extra(self, data: dict[str, Any]) -> None:
|
|
195
|
-
"""Update the span's extra data."""
|
|
196
|
-
self._extra.update(data)
|
|
197
|
-
|
|
198
|
-
@property
|
|
199
|
-
def event_id(self) -> str | None:
|
|
200
|
-
"""Get the span's event ID (available after entering context)."""
|
|
201
|
-
return self._event_id
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
class _NoOpSpanContext:
|
|
205
|
-
"""No-op span context when logging is not initialized."""
|
|
206
|
-
|
|
207
|
-
def __init__(self, name: str, **kwargs):
|
|
208
|
-
self._name = name
|
|
209
|
-
self._event_id = str(uuid4())
|
|
210
|
-
|
|
211
|
-
async def __aenter__(self) -> _NoOpSpanContext:
|
|
212
|
-
return self
|
|
213
|
-
|
|
214
|
-
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
215
|
-
pass
|
|
216
|
-
|
|
217
|
-
def log(self, message: str) -> None:
|
|
218
|
-
logger.debug(f"[{self._name}] {message}")
|
|
219
|
-
|
|
220
|
-
def set_extra(self, data: dict[str, Any]) -> None:
|
|
221
|
-
pass
|
|
222
|
-
|
|
223
|
-
@property
|
|
224
|
-
def event_id(self) -> str | None:
|
|
225
|
-
return self._event_id
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
# =============================================================================
|
|
229
|
-
# Public API
|
|
230
|
-
# =============================================================================
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def init_logging(
|
|
234
|
-
callback_url: str,
|
|
235
|
-
session_id: str,
|
|
236
|
-
parent_event_id: str | None = None,
|
|
237
|
-
) -> None:
|
|
238
|
-
"""Initialize Chronos logging.
|
|
239
|
-
|
|
240
|
-
Call this once at startup (e.g., in world.run()).
|
|
241
|
-
|
|
242
|
-
Args:
|
|
243
|
-
callback_url: Chronos callback URL (e.g., http://chronos/api/callback)
|
|
244
|
-
session_id: Chronos session ID
|
|
245
|
-
parent_event_id: Optional root parent event ID (e.g., step ID)
|
|
246
|
-
"""
|
|
247
|
-
_ChronosLogger._instance = _ChronosLogger(
|
|
248
|
-
callback_url=callback_url,
|
|
249
|
-
session_id=session_id,
|
|
250
|
-
parent_event_id=parent_event_id,
|
|
251
|
-
)
|
|
252
|
-
logger.info(f"Chronos logging initialized: session={session_id}")
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def set_parent_event_id(parent_event_id: str | None) -> None:
|
|
256
|
-
"""Set the root parent event ID.
|
|
257
|
-
|
|
258
|
-
Useful for updating the parent when entering a new step.
|
|
259
|
-
"""
|
|
260
|
-
if _ChronosLogger._instance:
|
|
261
|
-
_ChronosLogger._instance.parent_event_id = parent_event_id
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def span(
|
|
265
|
-
name: str,
|
|
266
|
-
span_type: str = "span",
|
|
267
|
-
source: str = "agent",
|
|
268
|
-
extra: dict[str, Any] | None = None,
|
|
269
|
-
) -> SpanContext | _NoOpSpanContext:
|
|
270
|
-
"""Create a span context manager.
|
|
271
|
-
|
|
272
|
-
Usage:
|
|
273
|
-
async with span("my_operation") as s:
|
|
274
|
-
s.log("Starting...")
|
|
275
|
-
# nested spans work automatically
|
|
276
|
-
async with span("sub_operation"):
|
|
277
|
-
...
|
|
278
|
-
|
|
279
|
-
Args:
|
|
280
|
-
name: Span name/description
|
|
281
|
-
span_type: Event type for the span
|
|
282
|
-
source: Event source (agent, world, system)
|
|
283
|
-
extra: Additional data
|
|
284
|
-
|
|
285
|
-
Returns:
|
|
286
|
-
SpanContext (or NoOpSpanContext if logging not initialized)
|
|
287
|
-
"""
|
|
288
|
-
if _ChronosLogger._instance and _ChronosLogger._instance.enabled:
|
|
289
|
-
return SpanContext(
|
|
290
|
-
name=name,
|
|
291
|
-
span_type=span_type,
|
|
292
|
-
source=source,
|
|
293
|
-
extra=extra,
|
|
294
|
-
)
|
|
295
|
-
else:
|
|
296
|
-
return _NoOpSpanContext(name=name)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
async def log_event(
|
|
300
|
-
span_type: str,
|
|
301
|
-
content: str = "",
|
|
302
|
-
source: str = "agent",
|
|
303
|
-
log_type: str | None = None,
|
|
304
|
-
extra: dict[str, Any] | None = None,
|
|
305
|
-
parent_id: str | None = None,
|
|
306
|
-
) -> bool:
|
|
307
|
-
"""Log a single event to Chronos.
|
|
308
|
-
|
|
309
|
-
For spans with timing, use span() instead.
|
|
310
|
-
"""
|
|
311
|
-
chronos = _ChronosLogger._instance
|
|
312
|
-
if not chronos or not chronos.enabled:
|
|
313
|
-
return False
|
|
314
|
-
|
|
315
|
-
return await chronos.log_event(
|
|
316
|
-
span_type=span_type,
|
|
317
|
-
content=content,
|
|
318
|
-
source=source,
|
|
319
|
-
log_type=log_type,
|
|
320
|
-
extra=extra,
|
|
321
|
-
parent_id=parent_id or chronos.current_parent_id,
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def is_logging_enabled() -> bool:
|
|
326
|
-
"""Check if Chronos logging is enabled."""
|
|
327
|
-
return _ChronosLogger._instance is not None and _ChronosLogger._instance.enabled
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def reset_logging() -> None:
|
|
331
|
-
"""Reset the logger (mainly for testing)."""
|
|
332
|
-
_ChronosLogger._instance = None
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
# =============================================================================
|
|
336
|
-
# Logs Upload
|
|
337
|
-
# =============================================================================
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def zip_directory(dir_path: str) -> bytes:
|
|
341
|
-
"""Zip an entire directory.
|
|
342
|
-
|
|
343
|
-
Args:
|
|
344
|
-
dir_path: Path to the directory
|
|
345
|
-
|
|
346
|
-
Returns:
|
|
347
|
-
Zip file contents as bytes.
|
|
348
|
-
"""
|
|
349
|
-
path = Path(dir_path)
|
|
350
|
-
buffer = io.BytesIO()
|
|
351
|
-
|
|
352
|
-
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
353
|
-
for file_path in path.rglob("*"):
|
|
354
|
-
if file_path.is_file():
|
|
355
|
-
arcname = file_path.relative_to(path)
|
|
356
|
-
zf.write(file_path, arcname)
|
|
357
|
-
|
|
358
|
-
buffer.seek(0)
|
|
359
|
-
return buffer.read()
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
async def upload_artifacts(dir_path: str) -> str | None:
|
|
363
|
-
"""Upload a directory as a zip to Chronos.
|
|
364
|
-
|
|
365
|
-
Args:
|
|
366
|
-
dir_path: Path to the directory to upload
|
|
367
|
-
|
|
368
|
-
Returns:
|
|
369
|
-
URL if successful, None otherwise.
|
|
370
|
-
"""
|
|
371
|
-
chronos = _ChronosLogger._instance
|
|
372
|
-
if not chronos or not chronos.enabled:
|
|
373
|
-
return None
|
|
374
|
-
|
|
375
|
-
try:
|
|
376
|
-
zip_data = zip_directory(dir_path)
|
|
377
|
-
zip_base64 = base64.b64encode(zip_data).decode("utf-8")
|
|
378
|
-
logger.info(f"Zipped directory: {len(zip_data)} bytes")
|
|
379
|
-
except Exception as e:
|
|
380
|
-
logger.warning(f"Failed to zip directory: {e}")
|
|
381
|
-
return None
|
|
382
|
-
|
|
383
|
-
try:
|
|
384
|
-
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
385
|
-
response = await client.post(
|
|
386
|
-
f"{chronos.callback_url}/logs-upload",
|
|
387
|
-
json={
|
|
388
|
-
"session_id": chronos.session_id,
|
|
389
|
-
"logs_base64": zip_base64,
|
|
390
|
-
},
|
|
391
|
-
)
|
|
392
|
-
if response.status_code == 200:
|
|
393
|
-
result = response.json()
|
|
394
|
-
logger.info(f"Uploaded artifacts: {result}")
|
|
395
|
-
return result.get("logs_url")
|
|
396
|
-
else:
|
|
397
|
-
logger.warning(f"Failed to upload artifacts: {response.status_code} {response.text}")
|
|
398
|
-
return None
|
|
399
|
-
except Exception as e:
|
|
400
|
-
logger.warning(f"Failed to upload artifacts: {e}")
|
|
401
|
-
return None
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
# =============================================================================
|
|
405
|
-
# Artifact Upload (Generic)
|
|
406
|
-
# =============================================================================
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
async def upload_artifact(
|
|
410
|
-
data: bytes,
|
|
411
|
-
artifact_type: str,
|
|
412
|
-
filename: str | None = None,
|
|
413
|
-
extra: dict[str, Any] | None = None,
|
|
414
|
-
) -> dict[str, Any] | None:
|
|
415
|
-
"""Upload an artifact to Chronos.
|
|
416
|
-
|
|
417
|
-
Artifacts are stored in S3 and linked to the session in the database.
|
|
418
|
-
|
|
419
|
-
Args:
|
|
420
|
-
data: Raw bytes of the artifact
|
|
421
|
-
artifact_type: Type of artifact (e.g., "state", "logs", "trajectory")
|
|
422
|
-
filename: Optional filename for the artifact
|
|
423
|
-
extra: Optional extra data to store with the artifact
|
|
424
|
-
|
|
425
|
-
Returns:
|
|
426
|
-
Dict with artifact_id and s3_url if successful, None otherwise.
|
|
427
|
-
"""
|
|
428
|
-
chronos = _ChronosLogger._instance
|
|
429
|
-
if not chronos or not chronos.enabled:
|
|
430
|
-
return None
|
|
431
|
-
|
|
432
|
-
try:
|
|
433
|
-
data_base64 = base64.b64encode(data).decode("utf-8")
|
|
434
|
-
logger.info(f"Uploading artifact: type={artifact_type}, size={len(data)} bytes")
|
|
435
|
-
except Exception as e:
|
|
436
|
-
logger.warning(f"Failed to encode artifact: {e}")
|
|
437
|
-
return None
|
|
438
|
-
|
|
439
|
-
try:
|
|
440
|
-
async with httpx.AsyncClient(timeout=120.0) as client:
|
|
441
|
-
response = await client.post(
|
|
442
|
-
f"{chronos.callback_url}/artifact",
|
|
443
|
-
json={
|
|
444
|
-
"session_id": chronos.session_id,
|
|
445
|
-
"artifact_type": artifact_type,
|
|
446
|
-
"data_base64": data_base64,
|
|
447
|
-
"filename": filename,
|
|
448
|
-
"extra": extra or {},
|
|
449
|
-
},
|
|
450
|
-
)
|
|
451
|
-
if response.status_code == 200:
|
|
452
|
-
result = response.json()
|
|
453
|
-
logger.info(f"Uploaded artifact: {result}")
|
|
454
|
-
return result
|
|
455
|
-
else:
|
|
456
|
-
logger.warning(f"Failed to upload artifact: {response.status_code} {response.text}")
|
|
457
|
-
return None
|
|
458
|
-
except Exception as e:
|
|
459
|
-
logger.warning(f"Failed to upload artifact: {e}")
|
|
460
|
-
return None
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
# =============================================================================
|
|
464
|
-
# Checkpoint Upload
|
|
465
|
-
# =============================================================================
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
async def upload_checkpoint(
|
|
469
|
-
step_number: int,
|
|
470
|
-
env_snapshots: dict[str, str],
|
|
471
|
-
state_artifact_id: str | None = None,
|
|
472
|
-
extra: dict[str, Any] | None = None,
|
|
473
|
-
) -> dict[str, Any] | None:
|
|
474
|
-
"""Upload checkpoint data to Chronos.
|
|
475
|
-
|
|
476
|
-
A checkpoint includes:
|
|
477
|
-
- Environment snapshots (artifact IDs per env alias)
|
|
478
|
-
- State artifact (git bundle of /state directory)
|
|
479
|
-
- Extra data (step number, timestamp, etc.)
|
|
480
|
-
|
|
481
|
-
Args:
|
|
482
|
-
step_number: The step number when this checkpoint was created
|
|
483
|
-
env_snapshots: Dict mapping env alias to artifact_id
|
|
484
|
-
state_artifact_id: Artifact ID of the state bundle (from upload_artifact)
|
|
485
|
-
extra: Optional additional data
|
|
486
|
-
|
|
487
|
-
Returns:
|
|
488
|
-
Dict with checkpoint_id if successful, None otherwise.
|
|
489
|
-
"""
|
|
490
|
-
chronos = _ChronosLogger._instance
|
|
491
|
-
if not chronos or not chronos.enabled:
|
|
492
|
-
return None
|
|
493
|
-
|
|
494
|
-
try:
|
|
495
|
-
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
496
|
-
response = await client.post(
|
|
497
|
-
f"{chronos.callback_url}/checkpoint",
|
|
498
|
-
json={
|
|
499
|
-
"session_id": chronos.session_id,
|
|
500
|
-
"step_number": step_number,
|
|
501
|
-
"env_snapshots": env_snapshots,
|
|
502
|
-
"state_artifact_id": state_artifact_id,
|
|
503
|
-
"extra": extra or {},
|
|
504
|
-
},
|
|
505
|
-
)
|
|
506
|
-
if response.status_code == 200:
|
|
507
|
-
result = response.json()
|
|
508
|
-
logger.info(f"Uploaded checkpoint: step={step_number}, checkpoint_id={result.get('checkpoint_id')}")
|
|
509
|
-
return result
|
|
510
|
-
else:
|
|
511
|
-
logger.warning(f"Failed to upload checkpoint: {response.status_code} {response.text}")
|
|
512
|
-
return None
|
|
513
|
-
except Exception as e:
|
|
514
|
-
logger.warning(f"Failed to upload checkpoint: {e}")
|
|
515
|
-
return None
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"""API endpoints."""
|
|
2
|
-
|
|
3
|
-
from . import push_agent_logs, update_agent_status, upload_artifacts, upload_logs_zip, upload_trajectory
|
|
4
|
-
|
|
5
|
-
__all__ = [
|
|
6
|
-
"push_agent_logs",
|
|
7
|
-
"update_agent_status",
|
|
8
|
-
"upload_trajectory",
|
|
9
|
-
"upload_logs_zip",
|
|
10
|
-
"upload_artifacts",
|
|
11
|
-
]
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
"""Push Agent Logs"""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import httpx
|
|
8
|
-
|
|
9
|
-
from plato.chronos.errors import raise_for_status
|
|
10
|
-
from plato.chronos.models import AgentLogsRequest, AgentLogsResponse
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _build_request_args(
|
|
14
|
-
body: AgentLogsRequest,
|
|
15
|
-
) -> dict[str, Any]:
|
|
16
|
-
"""Build request arguments."""
|
|
17
|
-
url = "/api/callback/logs"
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
"method": "POST",
|
|
21
|
-
"url": url,
|
|
22
|
-
"json": body.model_dump(mode="json", exclude_none=True),
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def sync(
|
|
27
|
-
client: httpx.Client,
|
|
28
|
-
body: AgentLogsRequest,
|
|
29
|
-
) -> AgentLogsResponse:
|
|
30
|
-
"""Receive logs from an agent running in a VM.
|
|
31
|
-
|
|
32
|
-
This endpoint acknowledges log receipt for real-time streaming.
|
|
33
|
-
Logs are NOT stored in the database - they're uploaded to S3 as a zip
|
|
34
|
-
at the end of the session via the /artifacts endpoint."""
|
|
35
|
-
|
|
36
|
-
request_args = _build_request_args(
|
|
37
|
-
body=body,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
response = client.request(**request_args)
|
|
41
|
-
raise_for_status(response)
|
|
42
|
-
return AgentLogsResponse.model_validate(response.json())
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
async def asyncio(
|
|
46
|
-
client: httpx.AsyncClient,
|
|
47
|
-
body: AgentLogsRequest,
|
|
48
|
-
) -> AgentLogsResponse:
|
|
49
|
-
"""Receive logs from an agent running in a VM.
|
|
50
|
-
|
|
51
|
-
This endpoint acknowledges log receipt for real-time streaming.
|
|
52
|
-
Logs are NOT stored in the database - they're uploaded to S3 as a zip
|
|
53
|
-
at the end of the session via the /artifacts endpoint."""
|
|
54
|
-
|
|
55
|
-
request_args = _build_request_args(
|
|
56
|
-
body=body,
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
response = await client.request(**request_args)
|
|
60
|
-
raise_for_status(response)
|
|
61
|
-
return AgentLogsResponse.model_validate(response.json())
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
"""Update Agent Status"""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import httpx
|
|
8
|
-
|
|
9
|
-
from plato.chronos.errors import raise_for_status
|
|
10
|
-
from plato.chronos.models import AgentLogsResponse, AgentStatusRequest
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _build_request_args(
|
|
14
|
-
body: AgentStatusRequest,
|
|
15
|
-
) -> dict[str, Any]:
|
|
16
|
-
"""Build request arguments."""
|
|
17
|
-
url = "/api/callback/status"
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
"method": "POST",
|
|
21
|
-
"url": url,
|
|
22
|
-
"json": body.model_dump(mode="json", exclude_none=True),
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def sync(
|
|
27
|
-
client: httpx.Client,
|
|
28
|
-
body: AgentStatusRequest,
|
|
29
|
-
) -> AgentLogsResponse:
|
|
30
|
-
"""Update the status of a running session.
|
|
31
|
-
|
|
32
|
-
Called by agents to report completion or failure."""
|
|
33
|
-
|
|
34
|
-
request_args = _build_request_args(
|
|
35
|
-
body=body,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
response = client.request(**request_args)
|
|
39
|
-
raise_for_status(response)
|
|
40
|
-
return AgentLogsResponse.model_validate(response.json())
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
async def asyncio(
|
|
44
|
-
client: httpx.AsyncClient,
|
|
45
|
-
body: AgentStatusRequest,
|
|
46
|
-
) -> AgentLogsResponse:
|
|
47
|
-
"""Update the status of a running session.
|
|
48
|
-
|
|
49
|
-
Called by agents to report completion or failure."""
|
|
50
|
-
|
|
51
|
-
request_args = _build_request_args(
|
|
52
|
-
body=body,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
response = await client.request(**request_args)
|
|
56
|
-
raise_for_status(response)
|
|
57
|
-
return AgentLogsResponse.model_validate(response.json())
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
"""Upload Artifacts Endpoint"""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import httpx
|
|
8
|
-
|
|
9
|
-
from plato.chronos.errors import raise_for_status
|
|
10
|
-
from plato.chronos.models import ArtifactsUploadRequest, ArtifactsUploadResponse
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _build_request_args(
|
|
14
|
-
body: ArtifactsUploadRequest,
|
|
15
|
-
) -> dict[str, Any]:
|
|
16
|
-
"""Build request arguments."""
|
|
17
|
-
url = "/api/callback/artifacts"
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
"method": "POST",
|
|
21
|
-
"url": url,
|
|
22
|
-
"json": body.model_dump(mode="json", exclude_none=True),
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def sync(
|
|
27
|
-
client: httpx.Client,
|
|
28
|
-
body: ArtifactsUploadRequest,
|
|
29
|
-
) -> ArtifactsUploadResponse:
|
|
30
|
-
"""Upload trajectory and/or logs for a session.
|
|
31
|
-
|
|
32
|
-
Convenience endpoint to upload both artifacts in one request.
|
|
33
|
-
Trajectory is stored in DB, logs are uploaded to S3."""
|
|
34
|
-
|
|
35
|
-
request_args = _build_request_args(
|
|
36
|
-
body=body,
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
response = client.request(**request_args)
|
|
40
|
-
raise_for_status(response)
|
|
41
|
-
return ArtifactsUploadResponse.model_validate(response.json())
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
async def asyncio(
|
|
45
|
-
client: httpx.AsyncClient,
|
|
46
|
-
body: ArtifactsUploadRequest,
|
|
47
|
-
) -> ArtifactsUploadResponse:
|
|
48
|
-
"""Upload trajectory and/or logs for a session.
|
|
49
|
-
|
|
50
|
-
Convenience endpoint to upload both artifacts in one request.
|
|
51
|
-
Trajectory is stored in DB, logs are uploaded to S3."""
|
|
52
|
-
|
|
53
|
-
request_args = _build_request_args(
|
|
54
|
-
body=body,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
response = await client.request(**request_args)
|
|
58
|
-
raise_for_status(response)
|
|
59
|
-
return ArtifactsUploadResponse.model_validate(response.json())
|