fleet-python 0.2.21__py3-none-any.whl → 0.2.23__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.

Potentially problematic release.


This version of fleet-python might be problematic. Click here for more details.

fleet/client.py CHANGED
@@ -19,29 +19,18 @@ import cloudpickle
19
19
  import httpx
20
20
  import logging
21
21
  import os
22
- from typing import List, Optional
23
- import json
24
- import time
25
- from datetime import datetime
26
- from typing import Dict, List, Any, Tuple
22
+ from typing import List, Optional, Dict
27
23
 
28
24
  from .base import EnvironmentBase, SyncWrapper
29
25
  from .models import (
30
26
  InstanceRequest,
31
- InstanceRecord,
27
+ InstanceResponse,
32
28
  Environment as EnvironmentModel,
33
29
  VerifiersCheckResponse,
34
- VerificationResponse,
35
30
  VerifiersExecuteResponse,
36
- ToolLogEntry,
37
- ActionLogEntry,
38
- EnvironmentSnapshot,
39
- SnapshotValidation,
40
- ToolLogResponse,
41
- ToolSessionStartRequest,
42
- ToolSessionStartResponse,
43
- ToolLogQueryRequest,
31
+ TaskListResponse,
44
32
  )
33
+ from .tasks import Task
45
34
 
46
35
  from .instance import (
47
36
  InstanceClient,
@@ -52,110 +41,28 @@ from .instance import (
52
41
  from .config import DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, REGION_BASE_URL
53
42
  from .instance.base import default_httpx_client
54
43
  from .instance.client import ValidatorType
55
- from .instance.models import (
56
- Resource as ResourceModel,
57
- CDPDescribeResponse, # Add this
58
- )
59
44
  from .resources.base import Resource
60
45
  from .resources.sqlite import SQLiteResource
61
46
  from .resources.browser import BrowserResource
62
- from .resources.mcp import MCPResource
63
47
 
64
48
  logger = logging.getLogger(__name__)
65
49
 
66
50
 
67
- class LoggingBrowserResource(BrowserResource):
68
- """Browser resource wrapper that automatically logs all tool usage."""
69
-
70
- def __init__(
71
- self,
72
- resource: ResourceModel,
73
- client: "SyncWrapper",
74
- session_id: Optional[str] = None,
75
- ):
76
- super().__init__(resource, client)
77
- self._session_id = session_id
78
-
79
- def _log_tool_action(
80
- self,
81
- action: str,
82
- parameters: Dict[str, Any],
83
- result: Any = None,
84
- error: str = None,
85
- ):
86
- """Log a tool action to the tool_log table."""
87
- if not self._session_id:
88
- return
89
-
90
- start_time = time.time()
91
- try:
92
- self.client.request(
93
- "POST",
94
- "/log-tool",
95
- json={
96
- "tool_name": "browser",
97
- "action": action,
98
- "parameters": parameters,
99
- "result": result if result else {},
100
- "success": error is None,
101
- "error": error,
102
- "duration_ms": int((time.time() - start_time) * 1000),
103
- "session_id": self._session_id,
104
- "user_agent": "fleet-sdk",
105
- },
106
- )
107
- except Exception as e:
108
- logger.warning(f"Failed to log tool action: {e}")
109
-
110
- def start(self, width: int = 1920, height: int = 1080) -> CDPDescribeResponse:
111
- """Start browser and log the action."""
112
- parameters = {
113
- "width": width,
114
- "height": height,
115
- "resolution": f"{width}x{height}",
116
- }
117
- try:
118
- result = super().start(width, height)
119
- self._log_tool_action("start", parameters, {"success": True})
120
- return result
121
- except Exception as e:
122
- self._log_tool_action("start", parameters, error=str(e))
123
- raise
124
-
125
- def screenshot(self) -> Dict[str, Any]:
126
- """Take screenshot and log the action."""
127
- parameters = {}
128
- try:
129
- # This assumes there's a screenshot method - you may need to implement it
130
- result = {
131
- "url": self.devtools_url(),
132
- "timestamp": datetime.utcnow().isoformat(),
133
- }
134
- self._log_tool_action("screenshot", parameters, result)
135
- return result
136
- except Exception as e:
137
- self._log_tool_action("screenshot", parameters, error=str(e))
138
- raise
139
-
140
-
141
- class Environment(EnvironmentBase):
51
+ class SyncEnv(EnvironmentBase):
142
52
  def __init__(self, client: Optional[SyncWrapper], **kwargs):
143
53
  super().__init__(**kwargs)
144
54
  self._client = client
145
- self._instance: Optional[InstanceClient] = None
146
55
  self._apps: Dict[str, InstanceClient] = {}
147
- self._session_id: Optional[str] = None # ADD THIS
56
+ self._instance: Optional[InstanceClient] = None
148
57
 
149
58
  @property
150
59
  def instance(self) -> InstanceClient:
151
60
  if self._instance is None:
152
61
  self._instance = InstanceClient(
153
- self.manager_url,
154
- self.env_key,
155
- self._client.httpx_client if self._client else None,
62
+ self.manager_url, self._client.httpx_client if self._client else None
156
63
  )
157
64
  return self._instance
158
-
65
+
159
66
  def app(self, name: str) -> InstanceClient:
160
67
  if name not in self._apps:
161
68
  # Extract base URL by removing the current app path (e.g., /sentry/api/v1/env)
@@ -169,16 +76,10 @@ class Environment(EnvironmentBase):
169
76
 
170
77
  self._apps[name] = InstanceClient(
171
78
  f"{base_url}/{name}/api/v1/env",
172
- self.env_key,
173
79
  self._client.httpx_client if self._client else None,
174
80
  )
175
81
  return self._apps[name]
176
82
 
177
- @property
178
- def session_id(self) -> Optional[str]: # ADD THIS PROPERTY
179
- """Get the current tool logging session ID."""
180
- return self._session_id
181
-
182
83
  @property
183
84
  def _load_client(self) -> SyncWrapper:
184
85
  if self._client is None:
@@ -193,17 +94,8 @@ class Environment(EnvironmentBase):
193
94
  def db(self, name: str = "current") -> SQLiteResource:
194
95
  return self.instance.db(name)
195
96
 
196
- @property
197
- def mcp(self) -> MCPResource:
198
- return self.instance.mcp()
199
-
200
97
  def browser(self, name: str = "cdp") -> BrowserResource:
201
- """Get browser resource with automatic logging."""
202
- base_browser = self.instance.browser(name)
203
- # Wrap it with logging capabilities
204
- return LoggingBrowserResource(
205
- base_browser.resource, base_browser.client, self._session_id
206
- )
98
+ return self.instance.browser(name)
207
99
 
208
100
  def state(self, uri: str) -> Resource:
209
101
  return self.instance.state(uri)
@@ -211,16 +103,7 @@ class Environment(EnvironmentBase):
211
103
  def resources(self) -> List[Resource]:
212
104
  return self.instance.resources()
213
105
 
214
- def close(self) -> InstanceRecord:
215
- if hasattr(self, "_session_id") and self._session_id:
216
- try:
217
- self.instance.client.request(
218
- "POST", f"/end-tool-session/{self._session_id}"
219
- )
220
- logger.info(f"Ended tool logging session: {self._session_id}")
221
- except Exception as e:
222
- logger.warning(f"Failed to end tool logging session: {e}")
223
-
106
+ def close(self) -> InstanceResponse:
224
107
  return _delete_instance(self._load_client, self.instance_id)
225
108
 
226
109
  def verify(self, validator: ValidatorType) -> ExecuteFunctionResponse:
@@ -235,26 +118,26 @@ class Environment(EnvironmentBase):
235
118
  return _check_bundle_exists(self._load_client, bundle_hash)
236
119
 
237
120
  def execute_verifier_remote(
238
- self,
239
- bundle_data: bytes,
121
+ self,
122
+ bundle_data: bytes,
240
123
  bundle_sha: str,
241
124
  key: str,
242
125
  function_name: str,
243
- args: tuple,
244
- kwargs: dict,
126
+ args: tuple,
127
+ kwargs: dict,
245
128
  timeout: Optional[int] = 30,
246
129
  needs_upload: bool = True,
247
130
  ) -> VerifiersExecuteResponse:
248
131
  return _execute_verifier_remote(
249
- self._load_client,
250
- bundle_data,
132
+ self._load_client,
133
+ bundle_data,
251
134
  bundle_sha,
252
135
  key,
253
136
  function_name,
254
- args,
255
- kwargs,
137
+ args,
138
+ kwargs,
256
139
  timeout,
257
- needs_upload,
140
+ needs_upload
258
141
  )
259
142
 
260
143
  def __getstate__(self):
@@ -266,120 +149,6 @@ class Environment(EnvironmentBase):
266
149
  def __setstate__(self, state):
267
150
  self.__dict__.update(state)
268
151
 
269
- def get_snapshot(
270
- self, browser_resource: Optional[BrowserResource] = None
271
- ) -> EnvironmentSnapshot:
272
- """
273
- Get a snapshot of the current environment state including action logs and tool logs.
274
-
275
- Args:
276
- browser_resource: Optional browser resource to capture current state from
277
-
278
- Returns:
279
- EnvironmentSnapshot containing all logs and state information
280
- """
281
- # Get current timestamp
282
- timestamp = datetime.utcnow().isoformat() + "Z"
283
-
284
- # Get session ID from current tool logs or generate new one
285
- session_id = self._get_current_session_id() or f"snapshot-{int(time.time())}"
286
-
287
- # Query tool logs
288
- tool_logs_response = self.instance.client.request(
289
- "POST",
290
- "/query-tool-logs",
291
- json={
292
- "session_id": session_id,
293
- "limit": None, # Get all logs for this session
294
- },
295
- )
296
- tool_logs_data = tool_logs_response.json()
297
- tool_logs = [
298
- ToolLogEntry(**entry) for entry in tool_logs_data.get("entries", [])
299
- ]
300
-
301
- # Query action logs
302
- action_logs_query = """
303
- SELECT id, timestamp, action_type, payload, sql, args, path
304
- FROM action_log
305
- ORDER BY timestamp DESC
306
- LIMIT 10000
307
- """
308
- action_logs_response = self.instance.client.request(
309
- "POST",
310
- "/resources/sqlite/action_log/query",
311
- json={"query": action_logs_query, "read_only": True},
312
- )
313
- action_logs_data = action_logs_response.json()
314
-
315
- action_logs = []
316
- if action_logs_data.get("success") and action_logs_data.get("rows"):
317
- columns = action_logs_data["columns"]
318
- for row in action_logs_data["rows"]:
319
- entry_dict = dict(zip(columns, row))
320
- action_logs.append(ActionLogEntry(**entry_dict))
321
-
322
- # Get current page URL and viewport if browser is available
323
- page_url = ""
324
- viewport_size = (1920, 1080) # Default
325
-
326
- if browser_resource:
327
- try:
328
- # Get current page URL via CDP
329
- cdp_info = browser_resource.describe()
330
- # You might need to implement a method to get current URL via CDP
331
- # For now, we'll look for the last navigation in tool logs
332
- for log in reversed(tool_logs):
333
- if log.tool_name == "browser" and log.action == "screenshot":
334
- if log.result and "url" in log.result:
335
- page_url = log.result["url"]
336
- break
337
-
338
- # Get viewport size from last browser start or from logs
339
- for log in reversed(tool_logs):
340
- if log.tool_name == "browser" and log.action == "start":
341
- if log.parameters and "resolution" in log.parameters:
342
- res = log.parameters["resolution"].split("x")
343
- viewport_size = (int(res[0]), int(res[1]))
344
- break
345
- except Exception as e:
346
- logger.warning(f"Could not get browser state: {e}")
347
-
348
- # Create snapshot
349
- return EnvironmentSnapshot(
350
- env_key=self.env_key,
351
- instance_id=self.instance_id,
352
- timestamp=timestamp,
353
- session_id=session_id,
354
- tool_logs=tool_logs,
355
- action_logs=action_logs,
356
- page_url=page_url,
357
- viewport_size=viewport_size,
358
- metadata={
359
- "snapshot_version": "1.0",
360
- "tool_log_count": len(tool_logs),
361
- "action_log_count": len(action_logs),
362
- },
363
- )
364
-
365
- def _get_current_session_id(self) -> Optional[str]:
366
- """Get the current session ID from the environment."""
367
- # First try to use the stored session ID
368
- if hasattr(self, "_session_id") and self._session_id:
369
- return self._session_id
370
-
371
- # Otherwise, try to get the most recent session ID from tool logs
372
- try:
373
- response = self.instance.client.request(
374
- "POST", "/query-tool-logs", json={"limit": 1}
375
- )
376
- data = response.json()
377
- if data.get("entries") and len(data["entries"]) > 0:
378
- return data["entries"][0].get("session_id")
379
- except Exception:
380
- pass
381
- return None
382
-
383
152
 
384
153
  class Fleet:
385
154
  def __init__(
@@ -412,11 +181,8 @@ class Fleet:
412
181
  return EnvironmentModel(**response.json())
413
182
 
414
183
  def make(
415
- self,
416
- env_key: str,
417
- region: Optional[str] = None,
418
- session_id: Optional[str] = None,
419
- ) -> Environment:
184
+ self, env_key: str, region: Optional[str] = None
185
+ ) -> SyncEnv:
420
186
  if ":" in env_key:
421
187
  env_key_part, version = env_key.split(":", 1)
422
188
  if not version.startswith("v") and len(version) != 0 and version[0].isdigit():
@@ -425,7 +191,7 @@ class Fleet:
425
191
  env_key_part = env_key
426
192
  version = None
427
193
 
428
- request = InstanceRequest(env_key=env_key_part, version=version, region=region)
194
+ request = InstanceRequest(env_key=env_key_part, version=version, region=region, created_from="sdk")
429
195
  region_base_url = REGION_BASE_URL.get(region)
430
196
  response = self.client.request(
431
197
  "POST",
@@ -433,38 +199,13 @@ class Fleet:
433
199
  json=request.model_dump(),
434
200
  base_url=region_base_url,
435
201
  )
436
- instance = Environment(client=self.client, **response.json())
202
+ instance = SyncEnv(client=self.client, **response.json())
437
203
  instance.instance.load()
438
-
439
- # Start tool logging session automatically
440
- if session_id is None:
441
- session_id = f"env-{instance.instance_id}-{int(time.time())}"
442
-
443
- try:
444
- instance.instance.client.request(
445
- "POST",
446
- "/start-tool-session",
447
- json={
448
- "session_id": session_id,
449
- "metadata": {
450
- "env_key": env_key,
451
- "instance_id": instance.instance_id,
452
- "region": region or "default",
453
- "started_at": datetime.utcnow().isoformat() + "Z",
454
- },
455
- },
456
- )
457
- instance._session_id = session_id
458
- logger.info(f"Started tool logging session: {session_id}")
459
- except Exception as e:
460
- logger.warning(f"Failed to start tool logging session: {e}")
461
- instance._session_id = None
462
-
463
204
  return instance
464
205
 
465
206
  def instances(
466
207
  self, status: Optional[str] = None, region: Optional[str] = None
467
- ) -> List[Environment]:
208
+ ) -> List[SyncEnv]:
468
209
  params = {}
469
210
  if status:
470
211
  params["status"] = status
@@ -473,13 +214,13 @@ class Fleet:
473
214
 
474
215
  response = self.client.request("GET", "/v1/env/instances", params=params)
475
216
  return [
476
- Environment(client=self.client, **instance_data)
217
+ SyncEnv(client=self.client, **instance_data)
477
218
  for instance_data in response.json()
478
219
  ]
479
220
 
480
- def instance(self, instance_id: str) -> Environment:
221
+ def instance(self, instance_id: str) -> SyncEnv:
481
222
  response = self.client.request("GET", f"/v1/env/instances/{instance_id}")
482
- instance = Environment(client=self.client, **response.json())
223
+ instance = SyncEnv(client=self.client, **response.json())
483
224
  instance.instance.load()
484
225
  return instance
485
226
 
@@ -489,111 +230,49 @@ class Fleet:
489
230
  def execute_verifier_remote(
490
231
  self, bundle_data: bytes, args: tuple, kwargs: dict, timeout: Optional[int] = 30
491
232
  ) -> VerifiersExecuteResponse:
492
- return _execute_verifier_remote(self.client, bundle_data, args, kwargs, timeout)
233
+ return _execute_verifier_remote(
234
+ self.client, bundle_data, args, kwargs, timeout
235
+ )
493
236
 
494
- def delete(self, instance_id: str) -> InstanceRecord:
237
+ def delete(self, instance_id: str) -> InstanceResponse:
495
238
  return _delete_instance(self.client, instance_id)
496
239
 
497
- def resume(
498
- self,
499
- snapshot: EnvironmentSnapshot,
500
- validate: bool = True,
501
- playback_speed: float = 1.0,
502
- ) -> Tuple[Environment, SnapshotValidation]:
503
- """
504
- Resume an environment from a snapshot by recreating the state.
505
-
240
+ def load_tasks(self, env_key: Optional[str] = None) -> List[Task]:
241
+ """Load tasks for the authenticated team, optionally filtered by environment.
242
+
506
243
  Args:
507
- snapshot: EnvironmentSnapshot to resume from
508
- validate: Whether to validate the resumed state matches the snapshot
509
- playback_speed: Speed multiplier for replaying actions (1.0 = normal speed)
510
-
244
+ env_key: Optional environment key to filter tasks by
245
+
511
246
  Returns:
512
- Tuple of (new Environment instance, validation results)
247
+ List[Task] containing Task objects
513
248
  """
514
- # Create new environment instance
515
- new_env = self.make(snapshot.env_key)
516
-
517
- # Start a new tool session for tracking
518
- replay_session_id = f"replay-{snapshot.session_id}-{int(time.time())}"
519
- new_env.instance.client.request(
520
- "POST",
521
- "/start-tool-session",
522
- json={
523
- "session_id": replay_session_id,
524
- "metadata": {
525
- "type": "snapshot_replay",
526
- "original_session_id": snapshot.session_id,
527
- "snapshot_timestamp": snapshot.timestamp,
528
- },
529
- },
530
- )
531
-
532
- # Update tool logs with environment name before replay
533
- for log in snapshot.tool_logs:
534
- log_dict = log.dict()
535
- log_dict["session_id"] = replay_session_id
536
- log_dict["metadata"] = {"env_name": snapshot.env_key, "replay": True}
537
-
538
- # Start browser with same viewport
539
- browser = new_env.browser()
540
- browser.start(width=snapshot.viewport_size[0], height=snapshot.viewport_size[1])
541
-
542
- # Replay tool logs in order
543
- validation_errors = []
544
- last_timestamp = None
545
-
546
- for i, tool_log in enumerate(snapshot.tool_logs):
547
- try:
548
- # Calculate wait time between actions
549
- if last_timestamp and playback_speed > 0:
550
- current_ts = datetime.fromisoformat(tool_log.timestamp.rstrip("Z"))
551
- last_ts = datetime.fromisoformat(last_timestamp.rstrip("Z"))
552
- wait_time = (current_ts - last_ts).total_seconds() / playback_speed
553
- if wait_time > 0:
554
- time.sleep(min(wait_time, 5)) # Cap at 5 seconds
555
-
556
- # Replay the tool action
557
- _replay_tool_action(
558
- None,
559
- tool_log,
560
- new_env.instance._client,
561
- replay_session_id,
562
- )
563
-
564
- last_timestamp = tool_log.timestamp
565
-
566
- except Exception as e:
567
- error_msg = f"Failed to replay action {i}: {tool_log.tool_name}.{tool_log.action} - {e}"
568
- logger.error(error_msg)
569
- validation_errors.append(error_msg)
570
-
571
- # End replay session
572
- new_env.instance.client.request(
573
- "POST", f"/end-tool-session/{replay_session_id}"
574
- )
575
-
576
- # Validate if requested
577
- validation = SnapshotValidation(
578
- success=True,
579
- page_match=True,
580
- action_log_match=True,
581
- discrepancies=validation_errors,
582
- message="Replay completed",
583
- )
584
-
585
- if validate:
586
- validation = _validate_resumed_state(
587
- new_env, snapshot, None, validation_errors
249
+ params = {}
250
+ if env_key is not None:
251
+ params["env_key"] = env_key
252
+
253
+ response = self.client.request("GET", "/v1/tasks", params=params)
254
+ task_list_response = TaskListResponse(**response.json())
255
+
256
+ # Transform TaskResponse objects to Task objects
257
+ tasks = []
258
+ for task_response in task_list_response.tasks:
259
+ task = Task(
260
+ key=task_response.key,
261
+ prompt=task_response.prompt,
262
+ env_id=task_response.environment_id, # Map environment_id -> env_id
263
+ created_at=task_response.created_at,
264
+ verifier=None, # Keep blank for now as requested
265
+ metadata={} # Default empty metadata
588
266
  )
589
-
590
- return new_env, validation
267
+ tasks.append(task)
268
+
269
+ return tasks
591
270
 
592
271
 
593
272
  # Shared
594
- def _delete_instance(client: SyncWrapper, instance_id: str) -> InstanceRecord:
273
+ def _delete_instance(client: SyncWrapper, instance_id: str) -> InstanceResponse:
595
274
  response = client.request("DELETE", f"/v1/env/instances/{instance_id}")
596
- return InstanceRecord(**response.json())
275
+ return InstanceResponse(**response.json())
597
276
 
598
277
 
599
278
  def _check_bundle_exists(
@@ -629,182 +308,25 @@ def _execute_verifier_remote(
629
308
  "timeout": timeout,
630
309
  "region": "us-west-1", # TODO: make configurable
631
310
  }
632
-
311
+
633
312
  # Add bundle data only if upload is needed
634
313
  if needs_upload:
635
314
  bundle_b64 = base64.b64encode(bundle_data).decode("utf-8")
636
315
  request_data["bundle"] = bundle_b64
637
-
316
+
638
317
  # Debug logging
639
- logger.debug(
640
- f"Sending verifier execute request: key={key}, sha256={bundle_sha[:8]}..., function_name={function_name}"
641
- )
318
+ logger.debug(f"Sending verifier execute request: key={key}, sha256={bundle_sha[:8]}..., function_name={function_name}")
642
319
  logger.debug(f"Request has bundle: {needs_upload}")
643
320
  logger.debug(f"Using client with base_url: {client.base_url}")
644
321
  logger.debug(f"Request data keys: {list(request_data.keys())}")
645
- logger.debug(
646
- f"Bundle size: {len(request_data.get('bundle', ''))} chars"
647
- if "bundle" in request_data
648
- else "No bundle"
649
- )
322
+ logger.debug(f"Bundle size: {len(request_data.get('bundle', ''))} chars" if 'bundle' in request_data else "No bundle")
650
323
 
651
324
  # Note: This should be called on the instance URL, not the orchestrator
652
325
  # The instance has manager URLs for verifier execution
653
326
  response = client.request("POST", "/v1/verifiers/execute", json=request_data)
654
-
327
+
655
328
  # Debug the response
656
329
  response_json = response.json()
657
330
  logger.debug(f"Verifier execute response: {response_json}")
658
331
 
659
332
  return VerifiersExecuteResponse(**response_json)
660
-
661
-
662
- def _replay_tool_action(
663
- playwright_wrapper,
664
- tool_log: ToolLogEntry,
665
- client: "SyncWrapper",
666
- session_id: str,
667
- ) -> None:
668
- """Replay a single tool action."""
669
- start_time = time.time()
670
-
671
- try:
672
- if tool_log.tool_name == "browser":
673
- # Map browser actions to playwright wrapper methods
674
- action_map = {
675
- "screenshot": lambda: playwright_wrapper.screenshot(),
676
- "left_click": lambda: playwright_wrapper.click(
677
- tool_log.parameters.get("x"), tool_log.parameters.get("y")
678
- ),
679
- "type": lambda: playwright_wrapper.type(
680
- tool_log.parameters.get("text", "")
681
- ),
682
- "key": lambda: playwright_wrapper.key(
683
- tool_log.parameters.get("text", "")
684
- ),
685
- "scroll": lambda: playwright_wrapper.scroll(
686
- direction=tool_log.parameters.get("scroll_direction", "down"),
687
- amount=tool_log.parameters.get("scroll_amount", 3),
688
- ),
689
- "mouse_move": lambda: playwright_wrapper.mouse_move(
690
- tool_log.parameters.get("x"), tool_log.parameters.get("y")
691
- ),
692
- "wait": lambda: time.sleep(tool_log.parameters.get("duration", 1)),
693
- # Add more action mappings as needed
694
- }
695
-
696
- if tool_log.action in action_map:
697
- result = action_map[tool_log.action]()
698
- else:
699
- logger.warning(f"Unknown browser action: {tool_log.action}")
700
- result = None
701
-
702
- elif tool_log.tool_name == "complete_task":
703
- # Don't replay task completion
704
- logger.info("Skipping complete_task during replay")
705
- return
706
-
707
- elif tool_log.tool_name == "report_issue":
708
- # Log but don't replay issue reports
709
- logger.info(f"Previous issue reported: {tool_log.parameters}")
710
- return
711
-
712
- elif tool_log.tool_name == "give_up":
713
- # Don't replay give up
714
- logger.warning(f"Original session gave up: {tool_log.parameters}")
715
- return
716
-
717
- # Log the replayed action
718
- duration_ms = int((time.time() - start_time) * 1000)
719
-
720
- client.request(
721
- "POST",
722
- "/log-tool",
723
- json={
724
- "tool_name": tool_log.tool_name,
725
- "action": tool_log.action,
726
- "parameters": tool_log.parameters,
727
- "result": result if result else tool_log.result,
728
- "success": True,
729
- "duration_ms": duration_ms,
730
- "session_id": session_id,
731
- "user_agent": "snapshot_replay",
732
- },
733
- )
734
-
735
- except Exception as e:
736
- # Log failure
737
- duration_ms = int((time.time() - start_time) * 1000)
738
-
739
- client.request(
740
- "POST",
741
- "/log-tool",
742
- json={
743
- "tool_name": tool_log.tool_name,
744
- "action": tool_log.action,
745
- "parameters": tool_log.parameters,
746
- "success": False,
747
- "error": str(e),
748
- "duration_ms": duration_ms,
749
- "session_id": session_id,
750
- "user_agent": "snapshot_replay",
751
- },
752
- )
753
- raise
754
-
755
-
756
- def _validate_resumed_state(
757
- new_env: Environment,
758
- snapshot: EnvironmentSnapshot,
759
- playwright_wrapper,
760
- existing_errors: List[str],
761
- ) -> SnapshotValidation:
762
- """Validate that the resumed state matches the snapshot."""
763
- discrepancies = existing_errors.copy()
764
-
765
- # Check current page URL
766
- page_match = True
767
- try:
768
- current_screenshot = playwright_wrapper.screenshot()
769
- current_url = current_screenshot.get("url", "")
770
-
771
- if current_url != snapshot.page_url:
772
- page_match = False
773
- discrepancies.append(
774
- f"Page URL mismatch: expected '{snapshot.page_url}', got '{current_url}'"
775
- )
776
- except Exception as e:
777
- page_match = False
778
- discrepancies.append(f"Could not verify page URL: {e}")
779
-
780
- # Compare action logs
781
- action_log_match = True
782
- try:
783
- # Get new action logs
784
- new_snapshot = new_env.get_snapshot()
785
-
786
- # Compare counts
787
- if len(new_snapshot.action_logs) != len(snapshot.action_logs):
788
- action_log_match = False
789
- discrepancies.append(
790
- f"Action log count mismatch: expected {len(snapshot.action_logs)}, "
791
- f"got {len(new_snapshot.action_logs)}"
792
- )
793
-
794
- # Could do more detailed comparison here if needed
795
-
796
- except Exception as e:
797
- action_log_match = False
798
- discrepancies.append(f"Could not verify action logs: {e}")
799
-
800
- success = page_match and action_log_match and len(discrepancies) == 0
801
-
802
- return SnapshotValidation(
803
- success=success,
804
- page_match=page_match,
805
- action_log_match=action_log_match,
806
- discrepancies=discrepancies,
807
- message="Validation completed"
808
- if success
809
- else "Validation failed with discrepancies",
810
- )