openadapt-ml 0.1.0__py3-none-any.whl → 0.2.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.
Files changed (63) hide show
  1. openadapt_ml/benchmarks/__init__.py +8 -0
  2. openadapt_ml/benchmarks/agent.py +90 -11
  3. openadapt_ml/benchmarks/azure.py +35 -6
  4. openadapt_ml/benchmarks/cli.py +4449 -201
  5. openadapt_ml/benchmarks/live_tracker.py +180 -0
  6. openadapt_ml/benchmarks/runner.py +41 -4
  7. openadapt_ml/benchmarks/viewer.py +1219 -0
  8. openadapt_ml/benchmarks/vm_monitor.py +610 -0
  9. openadapt_ml/benchmarks/waa.py +61 -4
  10. openadapt_ml/benchmarks/waa_deploy/Dockerfile +222 -0
  11. openadapt_ml/benchmarks/waa_deploy/__init__.py +10 -0
  12. openadapt_ml/benchmarks/waa_deploy/api_agent.py +539 -0
  13. openadapt_ml/benchmarks/waa_deploy/start_waa_server.bat +53 -0
  14. openadapt_ml/benchmarks/waa_live.py +619 -0
  15. openadapt_ml/cloud/local.py +1555 -1
  16. openadapt_ml/cloud/ssh_tunnel.py +553 -0
  17. openadapt_ml/datasets/next_action.py +87 -68
  18. openadapt_ml/evals/grounding.py +26 -8
  19. openadapt_ml/evals/trajectory_matching.py +84 -36
  20. openadapt_ml/experiments/demo_prompt/__init__.py +19 -0
  21. openadapt_ml/experiments/demo_prompt/format_demo.py +226 -0
  22. openadapt_ml/experiments/demo_prompt/results/experiment_20251231_002125.json +83 -0
  23. openadapt_ml/experiments/demo_prompt/results/experiment_n30_20251231_165958.json +1100 -0
  24. openadapt_ml/experiments/demo_prompt/results/multistep_20251231_025051.json +182 -0
  25. openadapt_ml/experiments/demo_prompt/run_experiment.py +531 -0
  26. openadapt_ml/experiments/waa_demo/__init__.py +10 -0
  27. openadapt_ml/experiments/waa_demo/demos.py +357 -0
  28. openadapt_ml/experiments/waa_demo/runner.py +717 -0
  29. openadapt_ml/experiments/waa_demo/tasks.py +151 -0
  30. openadapt_ml/export/__init__.py +9 -0
  31. openadapt_ml/export/__main__.py +6 -0
  32. openadapt_ml/export/cli.py +89 -0
  33. openadapt_ml/export/parquet.py +265 -0
  34. openadapt_ml/ingest/__init__.py +3 -4
  35. openadapt_ml/ingest/capture.py +89 -81
  36. openadapt_ml/ingest/loader.py +116 -68
  37. openadapt_ml/ingest/synthetic.py +221 -159
  38. openadapt_ml/retrieval/README.md +226 -0
  39. openadapt_ml/retrieval/USAGE.md +391 -0
  40. openadapt_ml/retrieval/__init__.py +91 -0
  41. openadapt_ml/retrieval/demo_retriever.py +817 -0
  42. openadapt_ml/retrieval/embeddings.py +629 -0
  43. openadapt_ml/retrieval/index.py +194 -0
  44. openadapt_ml/retrieval/retriever.py +160 -0
  45. openadapt_ml/runtime/policy.py +10 -10
  46. openadapt_ml/schema/__init__.py +104 -0
  47. openadapt_ml/schema/converters.py +541 -0
  48. openadapt_ml/schema/episode.py +457 -0
  49. openadapt_ml/scripts/compare.py +26 -16
  50. openadapt_ml/scripts/eval_policy.py +4 -5
  51. openadapt_ml/scripts/prepare_synthetic.py +14 -17
  52. openadapt_ml/scripts/train.py +81 -70
  53. openadapt_ml/training/benchmark_viewer.py +3225 -0
  54. openadapt_ml/training/trainer.py +120 -363
  55. openadapt_ml/training/trl_trainer.py +354 -0
  56. {openadapt_ml-0.1.0.dist-info → openadapt_ml-0.2.0.dist-info}/METADATA +102 -60
  57. openadapt_ml-0.2.0.dist-info/RECORD +86 -0
  58. openadapt_ml/schemas/__init__.py +0 -53
  59. openadapt_ml/schemas/sessions.py +0 -122
  60. openadapt_ml/schemas/validation.py +0 -252
  61. openadapt_ml-0.1.0.dist-info/RECORD +0 -55
  62. {openadapt_ml-0.1.0.dist-info → openadapt_ml-0.2.0.dist-info}/WHEEL +0 -0
  63. {openadapt_ml-0.1.0.dist-info → openadapt_ml-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,541 @@
1
+ """
2
+ Converters for benchmark-specific episode formats.
3
+
4
+ Supported formats:
5
+ - WAA (Windows Agent Arena)
6
+ - WebArena (coming soon)
7
+ - OSWorld (coming soon)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+ from pathlib import Path
15
+ from typing import Any, Optional, Union
16
+
17
+ from openadapt_ml.schema.episode import (
18
+ Action,
19
+ ActionType,
20
+ BenchmarkSource,
21
+ Coordinates,
22
+ Episode,
23
+ Observation,
24
+ Step,
25
+ UIElement,
26
+ )
27
+
28
+
29
+ # ============================================================================
30
+ # WAA (Windows Agent Arena) Converter
31
+ # ============================================================================
32
+
33
+ def _parse_waa_action(action_str: str) -> tuple[ActionType, dict[str, Any]]:
34
+ """Parse WAA action string into ActionType and parameters.
35
+
36
+ WAA action format examples:
37
+ - pyautogui.click(100, 200)
38
+ - pyautogui.write('hello')
39
+ - pyautogui.press('enter')
40
+ - pyautogui.hotkey('ctrl', 'c')
41
+ - pyautogui.scroll(3)
42
+ - DONE
43
+ - FAIL
44
+ """
45
+ action_str = action_str.strip()
46
+
47
+ # Meta actions
48
+ if action_str == "DONE":
49
+ return ActionType.DONE, {}
50
+ if action_str == "FAIL":
51
+ return ActionType.FAIL, {}
52
+
53
+ # Parse pyautogui calls
54
+ match = re.match(r"pyautogui\.(\w+)\((.*)\)", action_str)
55
+ if not match:
56
+ # Try without pyautogui prefix
57
+ match = re.match(r"(\w+)\((.*)\)", action_str)
58
+
59
+ if match:
60
+ func_name = match.group(1).lower()
61
+ args_str = match.group(2)
62
+
63
+ # Parse arguments (handle strings with commas inside)
64
+ args = []
65
+ current_arg = ""
66
+ in_string = False
67
+ string_char = None
68
+
69
+ for char in args_str:
70
+ if char in "'\"" and not in_string:
71
+ in_string = True
72
+ string_char = char
73
+ elif char == string_char and in_string:
74
+ in_string = False
75
+ string_char = None
76
+ elif char == "," and not in_string:
77
+ if current_arg.strip():
78
+ args.append(current_arg.strip())
79
+ current_arg = ""
80
+ continue
81
+ current_arg += char
82
+
83
+ if current_arg.strip():
84
+ args.append(current_arg.strip())
85
+
86
+ # Clean up string arguments
87
+ cleaned_args = []
88
+ for arg in args:
89
+ arg = arg.strip()
90
+ if (arg.startswith("'") and arg.endswith("'")) or (
91
+ arg.startswith('"') and arg.endswith('"')
92
+ ):
93
+ cleaned_args.append(arg[1:-1])
94
+ else:
95
+ try:
96
+ cleaned_args.append(int(arg))
97
+ except ValueError:
98
+ try:
99
+ cleaned_args.append(float(arg))
100
+ except ValueError:
101
+ cleaned_args.append(arg)
102
+
103
+ # Map function to action type
104
+ if func_name == "click":
105
+ params = {}
106
+ if len(cleaned_args) >= 2:
107
+ params["coordinates"] = Coordinates(x=int(cleaned_args[0]), y=int(cleaned_args[1]))
108
+ return ActionType.CLICK, params
109
+
110
+ elif func_name == "doubleclick":
111
+ params = {}
112
+ if len(cleaned_args) >= 2:
113
+ params["coordinates"] = Coordinates(x=int(cleaned_args[0]), y=int(cleaned_args[1]))
114
+ return ActionType.DOUBLE_CLICK, params
115
+
116
+ elif func_name == "rightclick":
117
+ params = {}
118
+ if len(cleaned_args) >= 2:
119
+ params["coordinates"] = Coordinates(x=int(cleaned_args[0]), y=int(cleaned_args[1]))
120
+ return ActionType.RIGHT_CLICK, params
121
+
122
+ elif func_name in ("write", "typewrite"):
123
+ return ActionType.TYPE, {"text": cleaned_args[0] if cleaned_args else ""}
124
+
125
+ elif func_name == "press":
126
+ return ActionType.KEY, {"key": cleaned_args[0] if cleaned_args else ""}
127
+
128
+ elif func_name == "hotkey":
129
+ if len(cleaned_args) >= 2:
130
+ return ActionType.HOTKEY, {
131
+ "key": cleaned_args[-1],
132
+ "modifiers": list(cleaned_args[:-1]),
133
+ }
134
+ return ActionType.KEY, {"key": cleaned_args[0] if cleaned_args else ""}
135
+
136
+ elif func_name == "scroll":
137
+ amount = cleaned_args[0] if cleaned_args else 0
138
+ direction = "up" if amount > 0 else "down"
139
+ return ActionType.SCROLL, {
140
+ "scroll_direction": direction,
141
+ "scroll_amount": abs(int(amount)) * 100, # Convert to pixels
142
+ }
143
+
144
+ elif func_name == "moveto":
145
+ params = {}
146
+ if len(cleaned_args) >= 2:
147
+ params["coordinates"] = Coordinates(x=int(cleaned_args[0]), y=int(cleaned_args[1]))
148
+ return ActionType.HOVER, params
149
+
150
+ elif func_name == "drag" or func_name == "dragto":
151
+ params = {}
152
+ if len(cleaned_args) >= 2:
153
+ params["end_coordinates"] = Coordinates(
154
+ x=int(cleaned_args[0]), y=int(cleaned_args[1])
155
+ )
156
+ return ActionType.DRAG, params
157
+
158
+ # Fallback - treat as raw text if nothing matched
159
+ return ActionType.TYPE, {"text": action_str, "raw": {"original": action_str}}
160
+
161
+
162
+ def from_waa_trajectory(
163
+ trajectory: list[dict[str, Any]],
164
+ task_info: dict[str, Any],
165
+ episode_id: Optional[str] = None,
166
+ ) -> Episode:
167
+ """Convert WAA trajectory format to Episode.
168
+
169
+ Args:
170
+ trajectory: List of WAA step dictionaries with keys like:
171
+ - screenshot_path: Path to screenshot
172
+ - action: Action string (pyautogui format)
173
+ - a11y_tree: Accessibility tree (optional)
174
+ - thought: Agent reasoning (optional)
175
+ task_info: Task metadata with keys like:
176
+ - id: Task ID
177
+ - instruction: Task instruction
178
+ - domain: Task domain (file_explorer, etc.)
179
+
180
+ Returns:
181
+ Episode instance
182
+ """
183
+ steps = []
184
+
185
+ for i, step_data in enumerate(trajectory):
186
+ # Parse observation
187
+ observation = Observation(
188
+ screenshot_path=step_data.get("screenshot_path"),
189
+ a11y_tree=step_data.get("a11y_tree"),
190
+ window_title=step_data.get("window_title"),
191
+ raw=step_data.get("observation_raw"),
192
+ )
193
+
194
+ # Parse action
195
+ action_str = step_data.get("action", "")
196
+ action_type, action_params = _parse_waa_action(action_str)
197
+
198
+ action = Action(
199
+ type=action_type,
200
+ raw={"original": action_str},
201
+ **action_params,
202
+ )
203
+
204
+ # Create step
205
+ step = Step(
206
+ step_index=i,
207
+ observation=observation,
208
+ action=action,
209
+ reasoning=step_data.get("thought") or step_data.get("reasoning"),
210
+ reward=step_data.get("reward"),
211
+ done=step_data.get("done"),
212
+ )
213
+ steps.append(step)
214
+
215
+ # Extract task info
216
+ task_id = task_info.get("id") or task_info.get("task_id")
217
+ instruction = task_info.get("instruction") or task_info.get("goal", "")
218
+
219
+ if episode_id is None:
220
+ episode_id = f"waa_{task_id}" if task_id else f"waa_episode_{id(trajectory)}"
221
+
222
+ return Episode(
223
+ episode_id=episode_id,
224
+ task_id=task_id,
225
+ instruction=instruction,
226
+ steps=steps,
227
+ success=task_info.get("success"),
228
+ source=BenchmarkSource.WAA,
229
+ metadata={
230
+ "domain": task_info.get("domain"),
231
+ "difficulty": task_info.get("difficulty"),
232
+ **{k: v for k, v in task_info.items() if k not in ["id", "task_id", "instruction", "goal", "success", "domain", "difficulty"]},
233
+ },
234
+ )
235
+
236
+
237
+ def to_waa_trajectory(episode: Episode) -> tuple[list[dict[str, Any]], dict[str, Any]]:
238
+ """Convert Episode to WAA trajectory format.
239
+
240
+ Args:
241
+ episode: Episode instance
242
+
243
+ Returns:
244
+ Tuple of (trajectory, task_info)
245
+ """
246
+ trajectory = []
247
+
248
+ for step in episode.steps:
249
+ step_data = {
250
+ "screenshot_path": step.observation.screenshot_path,
251
+ "a11y_tree": step.observation.a11y_tree,
252
+ "window_title": step.observation.window_title,
253
+ }
254
+
255
+ # Convert action back to pyautogui format
256
+ action = step.action
257
+ if action.raw and "original" in action.raw:
258
+ step_data["action"] = action.raw["original"]
259
+ else:
260
+ step_data["action"] = _action_to_pyautogui(action)
261
+
262
+ if step.reasoning:
263
+ step_data["thought"] = step.reasoning
264
+
265
+ if step.reward is not None:
266
+ step_data["reward"] = step.reward
267
+
268
+ if step.done is not None:
269
+ step_data["done"] = step.done
270
+
271
+ trajectory.append(step_data)
272
+
273
+ task_info = {
274
+ "id": episode.task_id,
275
+ "instruction": episode.instruction,
276
+ "success": episode.success,
277
+ }
278
+
279
+ if episode.metadata:
280
+ task_info.update(episode.metadata)
281
+
282
+ return trajectory, task_info
283
+
284
+
285
+ def _action_to_pyautogui(action: Action) -> str:
286
+ """Convert Action to pyautogui string format."""
287
+ if action.type == ActionType.DONE:
288
+ return "DONE"
289
+ if action.type == ActionType.FAIL:
290
+ return "FAIL"
291
+
292
+ if action.type == ActionType.CLICK:
293
+ if action.coordinates:
294
+ return f"pyautogui.click({action.coordinates.x}, {action.coordinates.y})"
295
+ return "pyautogui.click()"
296
+
297
+ if action.type == ActionType.DOUBLE_CLICK:
298
+ if action.coordinates:
299
+ return f"pyautogui.doubleClick({action.coordinates.x}, {action.coordinates.y})"
300
+ return "pyautogui.doubleClick()"
301
+
302
+ if action.type == ActionType.RIGHT_CLICK:
303
+ if action.coordinates:
304
+ return f"pyautogui.rightClick({action.coordinates.x}, {action.coordinates.y})"
305
+ return "pyautogui.rightClick()"
306
+
307
+ if action.type == ActionType.TYPE:
308
+ text = action.text or ""
309
+ # Escape single quotes
310
+ text = text.replace("'", "\\'")
311
+ return f"pyautogui.write('{text}')"
312
+
313
+ if action.type == ActionType.KEY:
314
+ return f"pyautogui.press('{action.key}')"
315
+
316
+ if action.type == ActionType.HOTKEY:
317
+ modifiers = action.modifiers or []
318
+ keys = modifiers + [action.key]
319
+ keys_str = ", ".join(f"'{k}'" for k in keys)
320
+ return f"pyautogui.hotkey({keys_str})"
321
+
322
+ if action.type == ActionType.SCROLL:
323
+ amount = action.scroll_amount or 100
324
+ if action.scroll_direction in ("down", "right"):
325
+ amount = -amount
326
+ return f"pyautogui.scroll({amount // 100})"
327
+
328
+ if action.type == ActionType.HOVER:
329
+ if action.coordinates:
330
+ return f"pyautogui.moveTo({action.coordinates.x}, {action.coordinates.y})"
331
+ return "pyautogui.moveTo()"
332
+
333
+ if action.type == ActionType.DRAG:
334
+ if action.end_coordinates:
335
+ return f"pyautogui.dragTo({action.end_coordinates.x}, {action.end_coordinates.y})"
336
+ return "pyautogui.drag()"
337
+
338
+ return f"# Unknown action: {action.type}"
339
+
340
+
341
+ # ============================================================================
342
+ # Internal Format Converter (openadapt_ml.schemas.sessions)
343
+ # ============================================================================
344
+
345
+ def from_internal_episode(
346
+ internal_episode: Any,
347
+ episode_id: Optional[str] = None,
348
+ ) -> Episode:
349
+ """Convert from internal training format (openadapt_ml.schemas.sessions.Episode).
350
+
351
+ This converts from the dataclass-based format used by the training pipeline
352
+ to the Pydantic-based Episode format used for external interoperability.
353
+
354
+ Args:
355
+ internal_episode: An openadapt_ml.schemas.sessions.Episode instance
356
+ episode_id: Override episode ID (defaults to internal_episode.id)
357
+
358
+ Returns:
359
+ Episode instance in the new format
360
+ """
361
+ steps = []
362
+ for i, step in enumerate(internal_episode.steps):
363
+ # Convert observation
364
+ obs = Observation(
365
+ screenshot_path=step.observation.image_path,
366
+ a11y_tree=step.observation.accessibility_tree,
367
+ dom=step.observation.dom_html,
368
+ window_title=step.observation.window_title,
369
+ raw=step.observation.meta,
370
+ )
371
+
372
+ # Convert action - note: internal format uses normalized coords in x/y
373
+ action_type_map = {
374
+ "click": ActionType.CLICK,
375
+ "double_click": ActionType.DOUBLE_CLICK,
376
+ "right_click": ActionType.RIGHT_CLICK,
377
+ "drag": ActionType.DRAG,
378
+ "scroll": ActionType.SCROLL,
379
+ "type": ActionType.TYPE,
380
+ "key": ActionType.KEY,
381
+ "wait": ActionType.WAIT,
382
+ "done": ActionType.DONE,
383
+ "failed": ActionType.FAIL,
384
+ "answer": ActionType.DONE, # Map answer to done
385
+ }
386
+ action_type = action_type_map.get(step.action.type, ActionType.CLICK)
387
+
388
+ action = Action(
389
+ type=action_type,
390
+ # Store normalized coords from internal format
391
+ normalized_coordinates=(step.action.x, step.action.y)
392
+ if step.action.x is not None and step.action.y is not None
393
+ else None,
394
+ text=step.action.text,
395
+ key=step.action.key,
396
+ modifiers=step.action.modifiers,
397
+ scroll_direction=step.action.scroll_direction,
398
+ scroll_amount=int(step.action.scroll_amount) if step.action.scroll_amount else None,
399
+ normalized_end=(step.action.end_x, step.action.end_y)
400
+ if step.action.end_x is not None and step.action.end_y is not None
401
+ else None,
402
+ element=UIElement(
403
+ element_id=step.action.target_node_id,
404
+ role=step.action.target_role,
405
+ name=step.action.target_name,
406
+ ) if step.action.target_node_id else None,
407
+ raw=step.action.raw,
408
+ )
409
+
410
+ steps.append(Step(
411
+ step_index=i,
412
+ observation=obs,
413
+ action=action,
414
+ reasoning=step.thought,
415
+ timestamp=step.t,
416
+ ))
417
+
418
+ return Episode(
419
+ episode_id=episode_id or internal_episode.id,
420
+ instruction=internal_episode.goal,
421
+ steps=steps,
422
+ success=internal_episode.success,
423
+ metadata={
424
+ "workflow_id": internal_episode.workflow_id,
425
+ "summary": internal_episode.summary,
426
+ } if internal_episode.workflow_id or internal_episode.summary else None,
427
+ )
428
+
429
+
430
+ def to_internal_episode(episode: Episode) -> dict:
431
+ """Convert Episode to internal training format (as dict).
432
+
433
+ Returns a dict matching openadapt_ml.schemas.sessions.Episode structure.
434
+ The caller can construct the dataclass from this dict.
435
+
436
+ Args:
437
+ episode: Episode in new format
438
+
439
+ Returns:
440
+ Dict matching internal Episode structure
441
+ """
442
+ steps = []
443
+ for step in episode.steps:
444
+ # Get normalized coordinates
445
+ norm_x, norm_y = None, None
446
+ if step.action.normalized_coordinates:
447
+ norm_x, norm_y = step.action.normalized_coordinates
448
+ elif step.action.coordinates:
449
+ # Can't convert pixel to normalized without screen size
450
+ # Store in raw for reference
451
+ pass
452
+
453
+ step_dict = {
454
+ "t": step.timestamp or float(step.step_index),
455
+ "observation": {
456
+ "image_path": step.observation.screenshot_path,
457
+ "accessibility_tree": step.observation.a11y_tree,
458
+ "dom_html": step.observation.dom,
459
+ "window_title": step.observation.window_title,
460
+ "meta": step.observation.raw,
461
+ },
462
+ "action": {
463
+ "type": step.action.type.value,
464
+ "x": norm_x,
465
+ "y": norm_y,
466
+ "text": step.action.text,
467
+ "key": step.action.key,
468
+ "modifiers": step.action.modifiers,
469
+ "scroll_direction": step.action.scroll_direction,
470
+ "scroll_amount": step.action.scroll_amount,
471
+ "end_x": step.action.normalized_end[0] if step.action.normalized_end else None,
472
+ "end_y": step.action.normalized_end[1] if step.action.normalized_end else None,
473
+ "target_node_id": step.action.element.element_id if step.action.element else None,
474
+ "target_role": step.action.element.role if step.action.element else None,
475
+ "target_name": step.action.element.name if step.action.element else None,
476
+ "raw": step.action.raw,
477
+ },
478
+ "thought": step.reasoning,
479
+ }
480
+ steps.append(step_dict)
481
+
482
+ return {
483
+ "id": episode.episode_id,
484
+ "goal": episode.instruction,
485
+ "steps": steps,
486
+ "success": episode.success,
487
+ "workflow_id": episode.metadata.get("workflow_id") if episode.metadata else None,
488
+ "summary": episode.metadata.get("summary") if episode.metadata else None,
489
+ }
490
+
491
+
492
+ def load_waa_result(result_dir: Union[str, Path]) -> Episode:
493
+ """Load episode from WAA result directory.
494
+
495
+ WAA result directories contain:
496
+ - result.txt: Final score
497
+ - trajectory.json or similar: Step-by-step data
498
+
499
+ Args:
500
+ result_dir: Path to WAA result directory
501
+
502
+ Returns:
503
+ Episode instance
504
+ """
505
+ result_dir = Path(result_dir)
506
+
507
+ # Try to find trajectory file
508
+ trajectory_files = list(result_dir.glob("*trajectory*.json")) + list(
509
+ result_dir.glob("*steps*.json")
510
+ )
511
+
512
+ trajectory = []
513
+ task_info = {}
514
+
515
+ if trajectory_files:
516
+ with open(trajectory_files[0]) as f:
517
+ data = json.load(f)
518
+ if isinstance(data, list):
519
+ trajectory = data
520
+ elif isinstance(data, dict):
521
+ trajectory = data.get("steps", data.get("trajectory", []))
522
+ task_info = {k: v for k, v in data.items() if k not in ["steps", "trajectory"]}
523
+
524
+ # Try to read result
525
+ result_file = result_dir / "result.txt"
526
+ if result_file.exists():
527
+ with open(result_file) as f:
528
+ result_str = f.read().strip()
529
+ try:
530
+ task_info["success"] = float(result_str) > 0
531
+ except ValueError:
532
+ pass
533
+
534
+ # Try to get task info from parent directory name
535
+ task_id = result_dir.name
536
+ if task_id and "task_id" not in task_info:
537
+ task_info["task_id"] = task_id
538
+
539
+ return from_waa_trajectory(
540
+ trajectory, task_info, episode_id=f"waa_{task_id}"
541
+ )