plato-sdk-v2 2.1.14__py3-none-any.whl → 2.1.16__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/worlds/base.py CHANGED
@@ -4,11 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  from abc import ABC, abstractmethod
7
- from datetime import datetime, timezone
8
7
  from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, get_args, get_origin
9
- from uuid import uuid4
10
8
 
11
- import httpx
12
9
  from pydantic import BaseModel, Field
13
10
 
14
11
  from plato.worlds.config import RunConfig
@@ -16,6 +13,11 @@ from plato.worlds.config import RunConfig
16
13
  if TYPE_CHECKING:
17
14
  from plato.v2.async_.session import Session
18
15
 
16
+ from plato.agents.logging import init_logging as _init_chronos_logging
17
+ from plato.agents.logging import log_event as _log_event
18
+ from plato.agents.logging import reset_logging as _reset_chronos_logging
19
+ from plato.agents.logging import span as _span
20
+
19
21
  logger = logging.getLogger(__name__)
20
22
 
21
23
  # Global registry of worlds
@@ -104,7 +106,6 @@ class BaseWorld(ABC, Generic[ConfigT]):
104
106
  self._step_count: int = 0
105
107
  self.plato_session = None
106
108
  self._current_step_id: str | None = None
107
- self._session_span_id: str | None = None
108
109
 
109
110
  @classmethod
110
111
  def get_config_class(cls) -> type[RunConfig]:
@@ -171,6 +172,9 @@ class BaseWorld(ABC, Generic[ConfigT]):
171
172
  This is called automatically during run() to restore the session
172
173
  and start sending heartbeats while the world runs.
173
174
  """
175
+ if not self.config.plato_session:
176
+ return
177
+
174
178
  try:
175
179
  from plato.v2.async_.session import Session
176
180
 
@@ -189,111 +193,6 @@ class BaseWorld(ABC, Generic[ConfigT]):
189
193
  except Exception as e:
190
194
  self.logger.warning(f"Error stopping Plato heartbeat: {e}")
191
195
 
192
- async def _log_event(
193
- self,
194
- span_type: str,
195
- content: str = "",
196
- source: str = "world",
197
- log_type: str | None = None,
198
- event_id: str | None = None,
199
- parent_id: str | None = None,
200
- step_number: int | None = None,
201
- extra: dict[str, Any] | None = None,
202
- started_at: datetime | None = None,
203
- ended_at: datetime | None = None,
204
- ) -> str | None:
205
- """Log an event to the callback API.
206
-
207
- Args:
208
- span_type: Type of event (reset, step, trajectory, agent_action, etc.)
209
- content: Event description
210
- source: Event source (world, agent, system)
211
- log_type: Log type for rendering (atif, step, etc.)
212
- event_id: Unique ID for this event (generated if not provided)
213
- parent_id: Parent event ID for nesting
214
- step_number: Step number for ordering
215
- extra: Additional structured data
216
- started_at: When the event started
217
- ended_at: When the event ended
218
-
219
- Returns:
220
- The event ID if successfully logged, None otherwise
221
- """
222
- if not self.config.callback_url or not self.config.session_id:
223
- return None
224
-
225
- event_id = event_id or str(uuid4())
226
-
227
- try:
228
- async with httpx.AsyncClient(timeout=10.0) as client:
229
- response = await client.post(
230
- f"{self.config.callback_url}/event",
231
- json={
232
- "session_id": self.config.session_id,
233
- "event_id": event_id,
234
- "parent_id": parent_id,
235
- "source": source,
236
- "span_type": span_type,
237
- "log_type": log_type,
238
- "step_number": step_number,
239
- "content": content,
240
- "extra": extra,
241
- "started_at": started_at.isoformat() if started_at else None,
242
- "ended_at": ended_at.isoformat() if ended_at else None,
243
- },
244
- )
245
- if response.status_code == 200:
246
- return event_id
247
- else:
248
- self.logger.warning(f"Failed to log event: {response.status_code} {response.text}")
249
- except Exception as e:
250
- self.logger.warning(f"Failed to log event: {e}")
251
-
252
- return None
253
-
254
- async def log_agent_action(
255
- self,
256
- agent_name: str,
257
- action: str,
258
- content: str,
259
- extra: dict[str, Any] | None = None,
260
- ) -> str | None:
261
- """Log an agent action during a step.
262
-
263
- Args:
264
- agent_name: Name of the agent
265
- action: Action type (e.g., tool_call, response)
266
- content: Action content/description
267
- extra: Additional data (tool input, output, etc.)
268
-
269
- Returns:
270
- The event ID if successfully logged
271
- """
272
- return await self._log_event(
273
- span_type="agent_action",
274
- content=f"[{agent_name}] {action}: {content}",
275
- source="agent",
276
- parent_id=self._current_step_id,
277
- step_number=self._step_count,
278
- extra={"agent": agent_name, "action": action, **(extra or {})},
279
- )
280
-
281
- async def log_trajectory(self, trajectory: dict[str, Any]) -> str | None:
282
- """Log an ATIF trajectory.
283
-
284
- Args:
285
- trajectory: ATIF trajectory dict
286
-
287
- Returns:
288
- The event ID if successfully logged
289
- """
290
- return await self._log_event(
291
- span_type="trajectory",
292
- log_type="atif",
293
- source="agent",
294
- extra=trajectory,
295
- )
296
-
297
196
  async def run(self, config: ConfigT) -> None:
298
197
  """Run the world: reset -> step until done -> close.
299
198
 
@@ -305,84 +204,55 @@ class BaseWorld(ABC, Generic[ConfigT]):
305
204
 
306
205
  self.logger.info(f"Starting world '{self.name}'")
307
206
 
207
+ # Initialize the logging singleton for agents to use
208
+ if config.callback_url and config.session_id:
209
+ _init_chronos_logging(
210
+ callback_url=config.callback_url,
211
+ session_id=config.session_id,
212
+ )
213
+
308
214
  # Connect to Plato session if configured (for heartbeats)
309
215
  await self._connect_plato_session()
310
216
 
311
217
  # Log session start
312
- session_start = datetime.now(timezone.utc)
313
- self._session_span_id = await self._log_event(
218
+ await _log_event(
314
219
  span_type="session_start",
315
220
  content=f"World '{self.name}' started",
316
221
  source="world",
317
- started_at=session_start,
318
222
  extra={"world_name": self.name, "world_version": self.get_version()},
319
223
  )
320
224
 
321
225
  try:
322
- # Log and execute reset
323
- reset_start = datetime.now(timezone.utc)
324
- reset_id = str(uuid4())
325
- await self._log_event(
326
- span_type="reset",
327
- content="World reset started",
328
- source="world",
329
- event_id=reset_id,
330
- parent_id=self._session_span_id,
331
- started_at=reset_start,
332
- )
333
-
334
- obs = await self.reset()
335
- reset_end = datetime.now(timezone.utc)
336
-
337
- await self._log_event(
338
- span_type="reset",
339
- content="World reset complete",
340
- source="world",
341
- event_id=reset_id,
342
- parent_id=self._session_span_id,
343
- started_at=reset_start,
344
- ended_at=reset_end,
345
- extra={"observation": obs.model_dump() if hasattr(obs, "model_dump") else str(obs)},
346
- )
226
+ # Execute reset with automatic span tracking
227
+ async with _span("reset", span_type="reset", source="world") as reset_span:
228
+ reset_span.log(f"Resetting world '{self.name}'")
229
+ obs = await self.reset()
230
+ reset_span.set_extra({"observation": obs.model_dump() if hasattr(obs, "model_dump") else str(obs)})
347
231
  self.logger.info(f"World reset complete: {obs}")
348
232
 
349
233
  while True:
350
234
  self._step_count += 1
351
- step_start = datetime.now(timezone.utc)
352
- self._current_step_id = str(uuid4())
353
235
 
354
- # Log step start
355
- await self._log_event(
236
+ # Execute step with automatic span tracking
237
+ # The span automatically sets itself as the current parent,
238
+ # so agent trajectories will nest under this step
239
+ async with _span(
240
+ f"step_{self._step_count}",
356
241
  span_type="step",
357
- content=f"Step {self._step_count} started",
358
242
  source="world",
359
- event_id=self._current_step_id,
360
- parent_id=self._session_span_id,
361
- step_number=self._step_count,
362
- started_at=step_start,
363
- )
364
-
365
- result = await self.step()
366
- step_end = datetime.now(timezone.utc)
367
-
368
- # Log step completion
369
- await self._log_event(
370
- span_type="step",
371
- content=f"Step {self._step_count} {'completed' if result.done else 'continues'}",
372
- source="world",
373
- event_id=self._current_step_id,
374
- parent_id=self._session_span_id,
375
- step_number=self._step_count,
376
- started_at=step_start,
377
- ended_at=step_end,
378
- extra={
379
- "done": result.done,
380
- "observation": result.observation.model_dump()
381
- if hasattr(result.observation, "model_dump")
382
- else str(result.observation),
383
- "info": result.info,
384
- },
385
- )
243
+ ) as step_span:
244
+ self._current_step_id = step_span.event_id
245
+ step_span.log(f"Step {self._step_count} started")
246
+ result = await self.step()
247
+ step_span.set_extra(
248
+ {
249
+ "done": result.done,
250
+ "observation": result.observation.model_dump()
251
+ if hasattr(result.observation, "model_dump")
252
+ else str(result.observation),
253
+ "info": result.info,
254
+ }
255
+ )
386
256
 
387
257
  self.logger.info(f"Step {self._step_count}: done={result.done}")
388
258
 
@@ -394,15 +264,14 @@ class BaseWorld(ABC, Generic[ConfigT]):
394
264
  await self._disconnect_plato_session()
395
265
 
396
266
  # Log session end
397
- session_end = datetime.now(timezone.utc)
398
- await self._log_event(
267
+ await _log_event(
399
268
  span_type="session_end",
400
269
  content=f"World '{self.name}' completed after {self._step_count} steps",
401
270
  source="world",
402
- parent_id=self._session_span_id,
403
- started_at=session_start,
404
- ended_at=session_end,
405
271
  extra={"total_steps": self._step_count},
406
272
  )
407
273
 
274
+ # Reset the logging singleton
275
+ _reset_chronos_logging()
276
+
408
277
  self.logger.info(f"World '{self.name}' completed after {self._step_count} steps")
plato/worlds/config.py CHANGED
@@ -74,7 +74,7 @@ class RunConfig(BaseModel):
74
74
 
75
75
  # Serialized Plato session for connecting to VM and sending heartbeats
76
76
  # This is the output of Session.dump() - used to restore session with Session.load()
77
- plato_session: SerializedSession
77
+ plato_session: SerializedSession | None = None
78
78
 
79
79
  model_config = {"extra": "allow"}
80
80
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plato-sdk-v2
3
- Version: 2.1.14
3
+ Version: 2.1.16
4
4
  Summary: Python SDK for the Plato API
5
5
  Author-email: Plato <support@plato.so>
6
6
  License-Expression: MIT
@@ -294,12 +294,12 @@ plato/_sims_generator/templates/python/errors.py.jinja,sha256=8L_FbHczBNLXJrbSlN
294
294
  plato/_sims_generator/templates/python/package_init.py.jinja,sha256=sOcJxUT0LuOWu5jOMGGKYxfCEjcYQv1hGF3n0iOA4hQ,986
295
295
  plato/_sims_generator/templates/python/tag_init.py.jinja,sha256=WB_9cv0JKIVg5TOXeSolET3tAfVg7sExjboh5jbCXz4,170
296
296
  plato/_sims_generator/templates/python/version_init.py.jinja,sha256=sGvFcYVfzXFyQDAe0PSOrg9yys93KE0XInFQNb1TvCY,179
297
- plato/agents/__init__.py,sha256=byhtkrMNBDT8lUtryZK_Ov6u-gtnRvyex2OUGKVAy30,2656
297
+ plato/agents/__init__.py,sha256=qslIFTVSe1yFeTRCKr8Z-mInWarj2HDbNZV4u6AiXek,2755
298
298
  plato/agents/base.py,sha256=vUbPQuNSo6Ka2lIB_ZOXgi4EoAjtAD7GIj9LnNotam0,4577
299
299
  plato/agents/build.py,sha256=CNMbVQFs2_pYit1dA29Davve28Yi4c7TNK9wBB7odrE,1621
300
- plato/agents/callback.py,sha256=3BdQfAx_nVDRimx9FiuhpaDFiLnvMNB0thCKeszz3D8,11188
301
300
  plato/agents/config.py,sha256=VZVMdCmEQnoR0VkrGdScG8p6zSKVFe7BZPd2h8lKNjI,5460
302
- plato/agents/runner.py,sha256=jgDV28qK1dT1z5IvO61ALpxdULFRvZAIiCWn62_ANfs,11669
301
+ plato/agents/logging.py,sha256=z9rDlGPbrpcTS8PephbK2rDqT7thC1KyLkua4ypUkv4,12210
302
+ plato/agents/runner.py,sha256=rOWYTSAhdola_FdrbviP955NBusNtBUy-q_c5fDA9to,5123
303
303
  plato/agents/trajectory.py,sha256=ayXEMCfYvIuXU2JkQWfPOVO9JywNSx8Kf6Ztrgsdh-I,10441
304
304
  plato/chronos/__init__.py,sha256=RHMvSrQS_-vkKOyTRuAkp2gKDP1HEuBLDnw8jcZs1Jg,739
305
305
  plato/chronos/client.py,sha256=YcOGtHWERyOD9z8LKt8bRMVL0cEwL2hiAP4qQgdZlUI,5495
@@ -459,11 +459,11 @@ plato/v2/utils/models.py,sha256=PwehSSnIRG-tM3tWL1PzZEH77ZHhIAZ9R0UPs6YknbM,1441
459
459
  plato/v2/utils/proxy_tunnel.py,sha256=8ZTd0jCGSfIHMvSv1fgEyacuISWnGPHLPbDglWroTzY,10463
460
460
  plato/worlds/README.md,sha256=TgG4aidude0ouJSCfY81Ev45hsUxPkO85HUIiWNqkcc,5463
461
461
  plato/worlds/__init__.py,sha256=0ogDVw5GEk4OT7FSD0Z7SrafTn_TBdo8DOU_v6ncbd4,1478
462
- plato/worlds/base.py,sha256=TT7tNNEW-JVV78IFhS3gY1_2afaFvBarK81FGFlRauM,14210
462
+ plato/worlds/base.py,sha256=vLAHkYpYg4kl7LgftQ0uC6xXhASVqREogn6msNWA6Hk,9650
463
463
  plato/worlds/build_hook.py,sha256=KSoW0kqa5b7NyZ7MYOw2qsZ_2FkWuz0M3Ru7AKOP7Qw,3486
464
- plato/worlds/config.py,sha256=RDBGhZRFJNshFVO-1H8xDcqxzUqahlSJtoo-eQpavgs,6242
464
+ plato/worlds/config.py,sha256=-K1dnlRL5I-DNYEoc1I4cKIsTw53X_1QrRY2SWJvRiQ,6256
465
465
  plato/worlds/runner.py,sha256=r9B2BxBae8_dM7y5cJf9xhThp_I1Qvf_tlPq2rs8qC8,4013
466
- plato_sdk_v2-2.1.14.dist-info/METADATA,sha256=aizMxFdqXeYqJQdIzUTnXRVwyPYXax641bLFAIwkVt0,8509
467
- plato_sdk_v2-2.1.14.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
468
- plato_sdk_v2-2.1.14.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
469
- plato_sdk_v2-2.1.14.dist-info/RECORD,,
466
+ plato_sdk_v2-2.1.16.dist-info/METADATA,sha256=5S_mx4Iij8UNv8od9y6b3n8T00t6gFzM01-1wS-rLdg,8509
467
+ plato_sdk_v2-2.1.16.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
468
+ plato_sdk_v2-2.1.16.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
469
+ plato_sdk_v2-2.1.16.dist-info/RECORD,,
plato/agents/callback.py DELETED
@@ -1,332 +0,0 @@
1
- """Chronos callback utilities for agents.
2
-
3
- Simple API:
4
- - log_event() - Log any event (trajectory, logs, status changes)
5
- - upload_logs() - Upload logs to S3
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import base64
11
- import io
12
- import json
13
- import logging
14
- import zipfile
15
- from pathlib import Path
16
- from typing import Any
17
-
18
- import httpx
19
-
20
- logger = logging.getLogger(__name__)
21
-
22
-
23
- class ChronosCallback:
24
- """Utility class for communicating with Chronos server.
25
-
26
- Example:
27
- callback = ChronosCallback(
28
- callback_url="http://chronos.example.com/api/callback",
29
- session_id="abc123",
30
- )
31
-
32
- # Log a trajectory event
33
- await callback.log_event(
34
- span_type="trajectory",
35
- log_type="atif",
36
- extra=trajectory_dict,
37
- )
38
-
39
- # Upload logs to S3
40
- await callback.upload_logs(logs_dir="/path/to/logs")
41
- """
42
-
43
- def __init__(self, callback_url: str, session_id: str):
44
- """Initialize the callback client.
45
-
46
- Args:
47
- callback_url: Full callback base URL (e.g., http://server/api/callback)
48
- session_id: The Chronos session ID for this run
49
- """
50
- self.callback_url = callback_url.rstrip("/")
51
- self.session_id = session_id
52
- self._enabled = bool(callback_url and session_id)
53
-
54
- @property
55
- def enabled(self) -> bool:
56
- """Check if callbacks are enabled (both server and session_id set)."""
57
- return self._enabled
58
-
59
- async def log_event(
60
- self,
61
- span_type: str,
62
- content: str = "",
63
- source: str = "agent",
64
- log_type: str | None = None,
65
- step_number: int | None = None,
66
- extra: dict[str, Any] | None = None,
67
- event_id: str | None = None,
68
- parent_id: str | None = None,
69
- started_at: str | None = None,
70
- ended_at: str | None = None,
71
- ) -> bool:
72
- """Log an event to Chronos.
73
-
74
- Use this for all event types:
75
- - Trajectory: span_type='trajectory', log_type='atif', extra={...atif data...}
76
- - Steps: span_type='step', extra={observation, done, info}
77
- - Status: span_type='status_change', extra={status: '...'}
78
-
79
- Args:
80
- span_type: Event type (trajectory, step, reset, agent_action, etc.)
81
- content: Event description
82
- source: Event source (agent, world, system)
83
- log_type: Log type for rendering (atif, step, etc.)
84
- step_number: Step number for ordering
85
- extra: Structured data (trajectory, step info, etc.)
86
- event_id: Unique event ID (generated if not provided)
87
- parent_id: Parent event ID for nesting
88
- started_at: ISO timestamp when event started
89
- ended_at: ISO timestamp when event ended
90
-
91
- Returns:
92
- True if event was logged successfully, False otherwise.
93
- """
94
- if not self._enabled:
95
- return False
96
-
97
- try:
98
- async with httpx.AsyncClient(timeout=30.0) as client:
99
- response = await client.post(
100
- f"{self.callback_url}/event",
101
- json={
102
- "session_id": self.session_id,
103
- "span_type": span_type,
104
- "content": content,
105
- "source": source,
106
- "log_type": log_type,
107
- "step_number": step_number,
108
- "extra": extra,
109
- "event_id": event_id,
110
- "parent_id": parent_id,
111
- "started_at": started_at,
112
- "ended_at": ended_at,
113
- },
114
- )
115
- if response.status_code == 200:
116
- return True
117
- else:
118
- logger.warning(f"Failed to log event: {response.status_code} {response.text}")
119
- return False
120
- except Exception as e:
121
- logger.warning(f"Failed to log event to Chronos: {e}")
122
- return False
123
-
124
- async def update_status(
125
- self,
126
- status: str,
127
- message: str | None = None,
128
- extra: dict[str, Any] | None = None,
129
- api_key: str | None = None,
130
- ) -> bool:
131
- """Update the session status in Chronos.
132
-
133
- Args:
134
- status: New status (running, completed, failed)
135
- message: Optional status message
136
- extra: Optional additional data
137
- api_key: API key for closing Plato session on terminal status
138
-
139
- Returns:
140
- True if status was updated successfully, False otherwise.
141
- """
142
- if not self._enabled:
143
- return False
144
-
145
- try:
146
- async with httpx.AsyncClient(timeout=10.0) as client:
147
- response = await client.post(
148
- f"{self.callback_url}/status",
149
- json={
150
- "session_id": self.session_id,
151
- "status": status,
152
- "message": message,
153
- "extra": extra,
154
- "api_key": api_key,
155
- },
156
- )
157
- return response.status_code == 200
158
- except Exception as e:
159
- logger.warning(f"Failed to update status in Chronos: {e}")
160
- return False
161
-
162
- def find_trajectory(self, logs_dir: str) -> dict[str, Any] | None:
163
- """Find and load ATIF trajectory from logs directory.
164
-
165
- Args:
166
- logs_dir: Path to the logs directory
167
-
168
- Returns:
169
- Parsed ATIF trajectory dict if found, None otherwise.
170
- """
171
- trajectory_path = Path(logs_dir) / "agent" / "trajectory.json"
172
-
173
- if not trajectory_path.exists():
174
- logger.info(f"No trajectory file at {trajectory_path}")
175
- return None
176
-
177
- try:
178
- with open(trajectory_path) as f:
179
- data = json.load(f)
180
- if isinstance(data, dict) and "schema_version" in data:
181
- logger.info(f"Found ATIF trajectory: {trajectory_path}")
182
- return data
183
- logger.info(f"Trajectory file exists but no schema_version: {trajectory_path}")
184
- return None
185
- except Exception as e:
186
- logger.warning(f"Failed to load trajectory from {trajectory_path}: {e}")
187
- return None
188
-
189
- def zip_logs_dir(self, logs_dir: str) -> bytes:
190
- """Zip the entire logs directory.
191
-
192
- Args:
193
- logs_dir: Path to the logs directory
194
-
195
- Returns:
196
- Zip file contents as bytes.
197
- """
198
- logs_path = Path(logs_dir)
199
- buffer = io.BytesIO()
200
-
201
- with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
202
- for file_path in logs_path.rglob("*"):
203
- if file_path.is_file():
204
- arcname = file_path.relative_to(logs_path)
205
- zf.write(file_path, arcname)
206
-
207
- buffer.seek(0)
208
- return buffer.read()
209
-
210
- async def upload_logs(self, logs_dir: str) -> str | None:
211
- """Upload logs directory to S3 via Chronos.
212
-
213
- Args:
214
- logs_dir: Path to the logs directory
215
-
216
- Returns:
217
- S3 URL if successful, None otherwise.
218
- """
219
- if not self._enabled:
220
- return None
221
-
222
- try:
223
- logs_zip = self.zip_logs_dir(logs_dir)
224
- logs_base64 = base64.b64encode(logs_zip).decode("utf-8")
225
- logger.info(f"Zipped logs: {len(logs_zip)} bytes")
226
- except Exception as e:
227
- logger.warning(f"Failed to zip logs: {e}")
228
- return None
229
-
230
- try:
231
- async with httpx.AsyncClient(timeout=60.0) as client:
232
- response = await client.post(
233
- f"{self.callback_url}/logs-upload",
234
- json={
235
- "session_id": self.session_id,
236
- "logs_base64": logs_base64,
237
- },
238
- )
239
- if response.status_code == 200:
240
- result = response.json()
241
- logger.info(f"Uploaded logs to Chronos: {result}")
242
- return result.get("logs_url")
243
- else:
244
- logger.warning(f"Failed to upload logs: {response.status_code} {response.text}")
245
- return None
246
- except Exception as e:
247
- logger.warning(f"Failed to upload logs to Chronos: {e}")
248
- return None
249
-
250
- async def upload_artifacts(
251
- self,
252
- logs_dir: str,
253
- trajectory: dict[str, Any] | None = None,
254
- agent_image: str | None = None,
255
- ) -> bool:
256
- """Upload trajectory and logs to Chronos.
257
-
258
- If trajectory is not provided, attempts to find it in the logs directory.
259
-
260
- Args:
261
- logs_dir: Path to the logs directory
262
- trajectory: Optional pre-loaded trajectory dict.
263
- agent_image: Optional agent Docker image URI to include in trajectory.
264
-
265
- Returns:
266
- True if successful, False otherwise.
267
- """
268
- if not self._enabled:
269
- return False
270
-
271
- success = False
272
-
273
- # Find and upload trajectory
274
- if trajectory is None:
275
- trajectory = self.find_trajectory(logs_dir)
276
-
277
- if trajectory:
278
- # Merge agent image into trajectory
279
- if agent_image:
280
- agent = trajectory.get("agent", {})
281
- extra = agent.get("extra") or {}
282
- extra["image"] = agent_image
283
- agent["extra"] = extra
284
- trajectory["agent"] = agent
285
-
286
- logger.info("Uploading trajectory to Chronos")
287
- if await self.log_event(
288
- span_type="trajectory",
289
- log_type="atif",
290
- extra=trajectory,
291
- source="agent",
292
- ):
293
- logger.info("Trajectory uploaded successfully")
294
- success = True
295
- else:
296
- logger.warning("Failed to upload trajectory")
297
-
298
- # Upload logs
299
- logs_url = await self.upload_logs(logs_dir)
300
- if logs_url:
301
- logger.info(f"Logs uploaded: {logs_url}")
302
- success = True
303
-
304
- return success
305
-
306
- # Legacy methods for backwards compatibility
307
-
308
- async def push_logs(self, logs: list[dict[str, Any]]) -> bool:
309
- """Legacy: Push log entries (just acknowledges now)."""
310
- if not self._enabled:
311
- return False
312
- try:
313
- async with httpx.AsyncClient(timeout=10.0) as client:
314
- response = await client.post(
315
- f"{self.callback_url}/logs",
316
- json={"session_id": self.session_id, "logs": logs},
317
- )
318
- return response.status_code == 200
319
- except Exception:
320
- return False
321
-
322
- async def push_log(
323
- self,
324
- message: str,
325
- level: str = "info",
326
- extra: dict[str, Any] | None = None,
327
- ) -> bool:
328
- """Legacy: Push a single log entry."""
329
- log_entry: dict[str, Any] = {"level": level, "message": message}
330
- if extra:
331
- log_entry["extra"] = extra
332
- return await self.push_logs([log_entry])