use-computer 0.0.27__tar.gz → 0.0.28__tar.gz

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 (96) hide show
  1. {use_computer-0.0.27 → use_computer-0.0.28}/PKG-INFO +32 -1
  2. {use_computer-0.0.27 → use_computer-0.0.28}/README.md +8 -0
  3. {use_computer-0.0.27 → use_computer-0.0.28}/pyproject.toml +27 -1
  4. use_computer-0.0.28/use_computer/agents/__init__.py +58 -0
  5. use_computer-0.0.28/use_computer/agents/base/__init__.py +31 -0
  6. use_computer-0.0.28/use_computer/agents/base/actions.py +234 -0
  7. use_computer-0.0.28/use_computer/agents/base/agent.py +263 -0
  8. use_computer-0.0.28/use_computer/agents/base/artifacts.py +90 -0
  9. use_computer-0.0.28/use_computer/agents/base/desktop.py +126 -0
  10. use_computer-0.0.28/use_computer/agents/base/final_state.py +138 -0
  11. use_computer-0.0.28/use_computer/agents/base/prompts.py +44 -0
  12. use_computer-0.0.28/use_computer/agents/base/recordings.py +48 -0
  13. use_computer-0.0.28/use_computer/agents/base/sandbox.py +11 -0
  14. use_computer-0.0.28/use_computer/agents/base/screenshots.py +52 -0
  15. use_computer-0.0.28/use_computer/agents/base/trajectory.py +34 -0
  16. use_computer-0.0.28/use_computer/agents/core.py +65 -0
  17. use_computer-0.0.28/use_computer/agents/debug/README.md +10 -0
  18. use_computer-0.0.28/use_computer/agents/debug/__init__.py +5 -0
  19. use_computer-0.0.28/use_computer/agents/debug/actions.py +147 -0
  20. use_computer-0.0.28/use_computer/agents/debug/agent.py +263 -0
  21. use_computer-0.0.28/use_computer/agents/debug/models.py +88 -0
  22. use_computer-0.0.28/use_computer/agents/ios/__init__.py +1 -0
  23. use_computer-0.0.28/use_computer/agents/ios/agent.py +341 -0
  24. use_computer-0.0.28/use_computer/agents/ios/tools.py +367 -0
  25. use_computer-0.0.28/use_computer/agents/prompts/anthropic.txt +7 -0
  26. use_computer-0.0.28/use_computer/agents/prompts/gemini.txt +6 -0
  27. use_computer-0.0.28/use_computer/agents/prompts/ios.txt +49 -0
  28. use_computer-0.0.28/use_computer/agents/prompts/openai.txt +7 -0
  29. use_computer-0.0.28/use_computer/agents/prompts/pyautogui.txt +50 -0
  30. use_computer-0.0.28/use_computer/agents/providers/__init__.py +1 -0
  31. use_computer-0.0.28/use_computer/agents/providers/anthropic.py +290 -0
  32. use_computer-0.0.28/use_computer/agents/providers/gemini.py +402 -0
  33. use_computer-0.0.28/use_computer/agents/providers/generic.py +627 -0
  34. use_computer-0.0.28/use_computer/agents/providers/openai.py +319 -0
  35. use_computer-0.0.28/use_computer/agents/types.py +192 -0
  36. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/client.py +1 -1
  37. use_computer-0.0.28/use_computer/harbor/__init__.py +1 -0
  38. use_computer-0.0.28/use_computer/harbor/agents.py +138 -0
  39. use_computer-0.0.28/use_computer/harbor/environment/__init__.py +17 -0
  40. use_computer-0.0.28/use_computer/harbor/environment/gateway.py +25 -0
  41. use_computer-0.0.28/use_computer/harbor/environment/platforms/__init__.py +205 -0
  42. use_computer-0.0.28/use_computer/harbor/environment/platforms/ios.py +105 -0
  43. use_computer-0.0.28/use_computer/harbor/environment/platforms/macos.py +293 -0
  44. use_computer-0.0.28/use_computer/harbor/environment/platforms/ubuntu.py +149 -0
  45. use_computer-0.0.28/use_computer/harbor/environment/platforms/windows.py +207 -0
  46. use_computer-0.0.28/use_computer/harbor/environment/setup_files.py +55 -0
  47. use_computer-0.0.28/use_computer/harbor/environment/use_computer.py +236 -0
  48. use_computer-0.0.28/use_computer/harbor/environment/utils/__init__.py +15 -0
  49. use_computer-0.0.28/use_computer/harbor/environment/utils/verifier.py +36 -0
  50. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/parsers.py +2 -3
  51. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/test_ios.sh +2 -2
  52. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/test_ubuntu.sh +0 -1
  53. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/test_windows.sh +0 -1
  54. use_computer-0.0.28/uv.lock +5529 -0
  55. use_computer-0.0.27/uv.lock +0 -941
  56. {use_computer-0.0.27 → use_computer-0.0.28}/.env.example +0 -0
  57. {use_computer-0.0.27 → use_computer-0.0.28}/.gitignore +0 -0
  58. {use_computer-0.0.27 → use_computer-0.0.28}/.pre-commit-config.yaml +0 -0
  59. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/__init__.py +0 -0
  60. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/accessibility.py +0 -0
  61. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/ax_transpile.py +0 -0
  62. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/display.py +0 -0
  63. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/errors.py +0 -0
  64. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/ios/__init__.py +0 -0
  65. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/ios/apps.py +0 -0
  66. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/ios/environment.py +0 -0
  67. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/ios/input.py +0 -0
  68. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/macos/__init__.py +0 -0
  69. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/macos/keyboard.py +0 -0
  70. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/macos/mouse.py +0 -0
  71. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/models.py +0 -0
  72. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/py.typed +0 -0
  73. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/recording.py +0 -0
  74. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/resources/__init__.py +0 -0
  75. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/resources/accessibility.py +0 -0
  76. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/resources/display.py +0 -0
  77. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/resources/recording.py +0 -0
  78. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/resources/screenshot.py +0 -0
  79. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/retry.py +0 -0
  80. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/sandbox.py +0 -0
  81. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/screenshot.py +0 -0
  82. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/simulators.py +0 -0
  83. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/__init__.py +0 -0
  84. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/pre_command.ps1 +0 -0
  85. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/pre_command.sh +0 -0
  86. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/task.toml +0 -0
  87. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/test_ios_nograder.sh +0 -0
  88. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/test_macos.sh +0 -0
  89. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/test_macos_check.sh +0 -0
  90. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/test_macos_nograder.sh +0 -0
  91. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/test_ubuntu_check.sh +0 -0
  92. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/test_ubuntu_nograder.sh +0 -0
  93. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/test_windows_check.sh +0 -0
  94. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/tasks/templates/test_windows_nograder.sh +0 -0
  95. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/windows/__init__.py +0 -0
  96. {use_computer-0.0.27 → use_computer-0.0.28}/use_computer/windows/shell.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: use-computer
3
- Version: 0.0.27
3
+ Version: 0.0.28
4
4
  Summary: Python SDK for use.computer sandboxes across macOS, iOS simulators, Windows, and Ubuntu
5
5
  Project-URL: Homepage, https://use.computer
6
6
  Project-URL: Documentation, https://api.use.computer/docs
@@ -18,6 +18,29 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.10
20
20
  Requires-Dist: httpx>=0.27
21
+ Provides-Extra: agents
22
+ Requires-Dist: pillow>=10; extra == 'agents'
23
+ Provides-Extra: agents-all
24
+ Requires-Dist: anthropic>=0.86; extra == 'agents-all'
25
+ Requires-Dist: google-genai>=1; extra == 'agents-all'
26
+ Requires-Dist: litellm>=1; extra == 'agents-all'
27
+ Requires-Dist: openai>=1; extra == 'agents-all'
28
+ Requires-Dist: pillow>=10; extra == 'agents-all'
29
+ Provides-Extra: agents-anthropic
30
+ Requires-Dist: anthropic>=0.86; extra == 'agents-anthropic'
31
+ Requires-Dist: pillow>=10; extra == 'agents-anthropic'
32
+ Provides-Extra: agents-gemini
33
+ Requires-Dist: google-genai>=1; extra == 'agents-gemini'
34
+ Requires-Dist: pillow>=10; extra == 'agents-gemini'
35
+ Provides-Extra: agents-litellm
36
+ Requires-Dist: litellm>=1; extra == 'agents-litellm'
37
+ Requires-Dist: pillow>=10; extra == 'agents-litellm'
38
+ Provides-Extra: agents-openai
39
+ Requires-Dist: openai>=1; extra == 'agents-openai'
40
+ Requires-Dist: pillow>=10; extra == 'agents-openai'
41
+ Provides-Extra: harbor
42
+ Requires-Dist: pyyaml>=6; extra == 'harbor'
43
+ Requires-Dist: tomli>=2; extra == 'harbor'
21
44
  Description-Content-Type: text/markdown
22
45
 
23
46
  # use-computer Python SDK
@@ -29,6 +52,14 @@ pip install use-computer
29
52
  export USE_COMPUTER_API_KEY=mk_live_...
30
53
  ```
31
54
 
55
+ Optional agent and Harbor integrations are installed explicitly:
56
+
57
+ ```bash
58
+ pip install "use-computer[agents-openai]" # or agents-anthropic / agents-gemini / agents-litellm / agents-all
59
+ pip install "use-computer[harbor]" # SDK Harbor adapter deps only; install Harbor separately until it is published
60
+ pip install "use-computer[harbor,agents-all]" # Harbor adapter plus all provider deps
61
+ ```
62
+
32
63
  ```python
33
64
  from use_computer import Computer, SandboxType, SimulatorFamily
34
65
 
@@ -7,6 +7,14 @@ pip install use-computer
7
7
  export USE_COMPUTER_API_KEY=mk_live_...
8
8
  ```
9
9
 
10
+ Optional agent and Harbor integrations are installed explicitly:
11
+
12
+ ```bash
13
+ pip install "use-computer[agents-openai]" # or agents-anthropic / agents-gemini / agents-litellm / agents-all
14
+ pip install "use-computer[harbor]" # SDK Harbor adapter deps only; install Harbor separately until it is published
15
+ pip install "use-computer[harbor,agents-all]" # Harbor adapter plus all provider deps
16
+ ```
17
+
10
18
  ```python
11
19
  from use_computer import Computer, SandboxType, SimulatorFamily
12
20
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "use-computer"
7
- version = "0.0.27"
7
+ version = "0.0.28"
8
8
  description = "Python SDK for use.computer sandboxes across macOS, iOS simulators, Windows, and Ubuntu"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -23,11 +23,37 @@ classifiers = [
23
23
  "Typing :: Typed",
24
24
  ]
25
25
 
26
+ [project.optional-dependencies]
27
+ agents = ["Pillow>=10"]
28
+ agents-openai = ["Pillow>=10", "openai>=1"]
29
+ agents-anthropic = ["Pillow>=10", "anthropic>=0.86"]
30
+ agents-litellm = ["Pillow>=10", "litellm>=1"]
31
+ agents-gemini = ["Pillow>=10", "google-genai>=1"]
32
+ agents-all = [
33
+ "Pillow>=10",
34
+ "openai>=1",
35
+ "anthropic>=0.86",
36
+ "litellm>=1",
37
+ "google-genai>=1",
38
+ ]
39
+ harbor = [
40
+ "PyYAML>=6",
41
+ "tomli>=2",
42
+ ]
43
+
26
44
  [dependency-groups]
27
45
  dev = [
46
+ "anthropic>=0.86",
47
+ "google-genai>=1",
48
+ "harbor @ git+https://github.com/josancamon19/harbor.git@mirror ; python_version >= '3.12'",
49
+ "litellm>=1",
50
+ "openai>=1",
28
51
  "pre-commit>=4",
29
52
  "pytest>=9",
30
53
  "ruff>=0.15",
54
+ "Pillow>=10",
55
+ "PyYAML>=6",
56
+ "tomli>=2",
31
57
  "twine>=6",
32
58
  "ty>=0.0.34",
33
59
  ]
@@ -0,0 +1,58 @@
1
+ """Optional computer-use agents.
2
+
3
+ Install extras for provider-backed agents:
4
+ use-computer[agents-openai], use-computer[agents-anthropic],
5
+ use-computer[agents-litellm], use-computer[agents-gemini], or
6
+ use-computer[agents-all].
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from importlib import import_module
12
+ from typing import Any
13
+
14
+ from use_computer.agents.core import AgentRunOptions, AgentRunResult, AgentUsage
15
+
16
+ _LAZY_EXPORTS = {
17
+ "AnthropicComputerAgent": (
18
+ "use_computer.agents.providers.anthropic",
19
+ "AnthropicComputerAgent",
20
+ ),
21
+ "AnthropicCUAAgent": ("use_computer.agents.providers.anthropic", "AnthropicCUAAgent"),
22
+ "DebugComputerAgent": ("use_computer.agents.debug.agent", "DebugComputerAgent"),
23
+ "DebugCUAAgent": ("use_computer.agents.debug.agent", "DebugCUAAgent"),
24
+ "GeminiComputerAgent": ("use_computer.agents.providers.gemini", "GeminiComputerAgent"),
25
+ "GeminiCUAAgent": ("use_computer.agents.providers.gemini", "GeminiCUAAgent"),
26
+ "GenericComputerAgent": ("use_computer.agents.providers.generic", "GenericComputerAgent"),
27
+ "GenericCUAAgent": ("use_computer.agents.providers.generic", "GenericCUAAgent"),
28
+ "IOSAgent": ("use_computer.agents.ios.agent", "IOSAgent"),
29
+ "IOSComputerAgent": ("use_computer.agents.ios.agent", "IOSComputerAgent"),
30
+ "OpenAIComputerAgent": ("use_computer.agents.providers.openai", "OpenAIComputerAgent"),
31
+ "OpenAICUAAgent": ("use_computer.agents.providers.openai", "OpenAICUAAgent"),
32
+ }
33
+
34
+ __all__ = [
35
+ "AgentRunOptions",
36
+ "AgentRunResult",
37
+ "AgentUsage",
38
+ *_LAZY_EXPORTS,
39
+ ]
40
+
41
+
42
+ def __getattr__(name: str) -> Any:
43
+ target = _LAZY_EXPORTS.get(name)
44
+ if target is None:
45
+ raise AttributeError(name)
46
+ module_name, attr = target
47
+ try:
48
+ module = import_module(module_name)
49
+ except ModuleNotFoundError as exc:
50
+ raise ModuleNotFoundError(
51
+ f"{name} requires an optional agent extra. Install one of "
52
+ "`use-computer[agents]`, `use-computer[agents-openai]`, "
53
+ "`use-computer[agents-anthropic]`, `use-computer[agents-litellm]`, "
54
+ "`use-computer[agents-gemini]`, or `use-computer[agents-all]`."
55
+ ) from exc
56
+ value = getattr(module, attr)
57
+ globals()[name] = value
58
+ return value
@@ -0,0 +1,31 @@
1
+ """Public base-agent surface for CUA agents."""
2
+
3
+ from use_computer.agents.base.actions import _annotate_click, execute_action
4
+ from use_computer.agents.base.agent import BaseCUAAgent
5
+ from use_computer.agents.base.desktop import (
6
+ DesktopProfile,
7
+ OpenAIComputerEnvironment,
8
+ desktop_profile,
9
+ )
10
+ from use_computer.agents.base.prompts import build_system_prompt, load_prompt
11
+ from use_computer.agents.base.sandbox import ComputerSandbox
12
+ from use_computer.agents.base.screenshots import (
13
+ scale_screenshot_for_model,
14
+ screenshot_cap_for_model,
15
+ )
16
+ from use_computer.agents.base.trajectory import write_trajectory
17
+
18
+ __all__ = [
19
+ "BaseCUAAgent",
20
+ "ComputerSandbox",
21
+ "DesktopProfile",
22
+ "OpenAIComputerEnvironment",
23
+ "_annotate_click",
24
+ "build_system_prompt",
25
+ "desktop_profile",
26
+ "execute_action",
27
+ "load_prompt",
28
+ "scale_screenshot_for_model",
29
+ "screenshot_cap_for_model",
30
+ "write_trajectory",
31
+ ]
@@ -0,0 +1,234 @@
1
+ """Desktop action execution for CUA agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import io
7
+ import logging
8
+ import time
9
+ from collections.abc import Mapping
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import httpx
14
+ from PIL import Image, ImageDraw
15
+
16
+ from use_computer.agents.base.desktop import normalize_key
17
+ from use_computer.agents.base.sandbox import ComputerSandbox
18
+ from use_computer.agents.types import DesktopAction, DesktopCommand, MouseButton
19
+
20
+ _log = logging.getLogger("use_computer.agent")
21
+
22
+
23
+ def _extract_coords(
24
+ action: dict[str, Any],
25
+ ) -> tuple[tuple[int, int] | None, tuple[int, int] | None]:
26
+ """Pull (point, secondary_point) from any action shape we use."""
27
+ point: tuple[int, int] | None = None
28
+ secondary: tuple[int, int] | None = None
29
+ if "coordinate" in action and action["coordinate"]:
30
+ c = action["coordinate"]
31
+ point = (int(c[0]), int(c[1]))
32
+ if "start_coordinate" in action and action["start_coordinate"]:
33
+ s = action["start_coordinate"]
34
+ secondary = (int(s[0]), int(s[1]))
35
+ if point is None and "x" in action and "y" in action:
36
+ try:
37
+ point = (int(float(action["x"])), int(float(action["y"])))
38
+ except (TypeError, ValueError):
39
+ pass
40
+ if "to_x" in action and "to_y" in action:
41
+ try:
42
+ point = (int(float(action["to_x"])), int(float(action["to_y"])))
43
+ except (TypeError, ValueError):
44
+ pass
45
+ if "from_x" in action and "from_y" in action:
46
+ try:
47
+ secondary = (int(float(action["from_x"])), int(float(action["from_y"])))
48
+ except (TypeError, ValueError):
49
+ pass
50
+ return point, secondary
51
+
52
+
53
+ def _annotate_click(png_bytes: bytes, action: dict[str, Any]) -> bytes:
54
+ """Draw a red marker at the click/move/scroll/drag coordinate."""
55
+ point, start = _extract_coords(action)
56
+ if point is None and start is None:
57
+ return png_bytes
58
+ try:
59
+ img = Image.open(io.BytesIO(png_bytes)).convert("RGBA")
60
+ overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
61
+ draw = ImageDraw.Draw(overlay)
62
+ red = (255, 30, 30, 240)
63
+ radius = 5
64
+ if point and start:
65
+ draw.line([start, point], fill=red, width=2)
66
+ for px, py in (start, point):
67
+ draw.ellipse([px - radius, py - radius, px + radius, py + radius], fill=red)
68
+ else:
69
+ cx, cy = point or start # type: ignore
70
+ draw.ellipse([cx - radius, cy - radius, cx + radius, cy + radius], fill=red)
71
+ out = Image.alpha_composite(img, overlay).convert("RGB")
72
+ buf = io.BytesIO()
73
+ out.save(buf, format="PNG")
74
+ return buf.getvalue()
75
+ except Exception as e:
76
+ _log.warning(f"annotate_click failed: {e}")
77
+ return png_bytes
78
+
79
+
80
+ async def execute_action(
81
+ sandbox: ComputerSandbox,
82
+ action: dict[str, Any],
83
+ images_dir: Path,
84
+ step_idx: int,
85
+ key_aliases: Mapping[str, str] | None = None,
86
+ ) -> tuple[str, bytes | None, str | None]:
87
+ """Execute a computer-use action; returns (result_text, ss_bytes, ss_path)."""
88
+ command = DesktopCommand.from_mapping(action)
89
+ img_name = f"step_{step_idx:03d}.png"
90
+ t0 = time.monotonic()
91
+
92
+ if command.action is DesktopAction.SCREENSHOT:
93
+ ss = await _take_screenshot_with_recovery(sandbox, command.label)
94
+ if ss is None:
95
+ return "Screenshot failed after retries", None, None
96
+ (images_dir / img_name).write_bytes(ss)
97
+ _log.info(f"action screenshot: {time.monotonic() - t0:.2f}s, {len(ss)} bytes")
98
+ return "Screenshot taken", ss, f"images/{img_name}"
99
+
100
+ try:
101
+ if command.action in {DesktopAction.LEFT_CLICK, DesktopAction.CLICK}:
102
+ await sandbox.mouse.click(
103
+ command.point.x,
104
+ command.point.y,
105
+ button=MouseButton.LEFT.value,
106
+ )
107
+ elif command.action is DesktopAction.RIGHT_CLICK:
108
+ await sandbox.mouse.click(
109
+ command.point.x,
110
+ command.point.y,
111
+ button=MouseButton.RIGHT.value,
112
+ )
113
+ elif command.action is DesktopAction.DOUBLE_CLICK:
114
+ await sandbox.mouse.click(
115
+ command.point.x,
116
+ command.point.y,
117
+ button=MouseButton.LEFT.value,
118
+ double=True,
119
+ )
120
+ elif command.action is DesktopAction.TRIPLE_CLICK:
121
+ await sandbox.mouse.click(
122
+ command.point.x,
123
+ command.point.y,
124
+ button=MouseButton.LEFT.value,
125
+ double=True,
126
+ )
127
+ await asyncio.sleep(0.05)
128
+ await sandbox.mouse.click(
129
+ command.point.x,
130
+ command.point.y,
131
+ button=MouseButton.LEFT.value,
132
+ )
133
+ elif command.action is DesktopAction.MIDDLE_CLICK:
134
+ await sandbox.mouse.click(
135
+ command.point.x,
136
+ command.point.y,
137
+ button=MouseButton.MIDDLE.value,
138
+ )
139
+ elif command.action is DesktopAction.TYPE:
140
+ await sandbox.keyboard.type(command.text)
141
+ elif command.action is DesktopAction.KEY:
142
+ key = normalize_key(command.key or command.text, key_aliases)
143
+ if "+" in key:
144
+ await sandbox.keyboard.hotkey(key)
145
+ else:
146
+ await sandbox.keyboard.press(key)
147
+ elif command.action is DesktopAction.SCROLL:
148
+ await sandbox.mouse.scroll(
149
+ command.point.x,
150
+ command.point.y,
151
+ command.direction.value,
152
+ command.amount,
153
+ )
154
+ elif command.action is DesktopAction.MOVE:
155
+ await sandbox.mouse.move(command.point.x, command.point.y)
156
+ elif command.action is DesktopAction.DRAG:
157
+ await sandbox.mouse.drag(
158
+ command.start_point.x,
159
+ command.start_point.y,
160
+ command.point.x,
161
+ command.point.y,
162
+ )
163
+ elif command.action is DesktopAction.WAIT:
164
+ await asyncio.sleep(command.duration)
165
+ else:
166
+ return f"Unknown action: {command.label}", None, None
167
+ except httpx.HTTPStatusError as e:
168
+ detail = e.response.text.strip().replace("\n", " ")[:240]
169
+ msg = f"Action '{command.label}' failed: HTTP {e.response.status_code} {detail}"
170
+ _log.warning(msg)
171
+ ss = await _take_screenshot_with_recovery(sandbox, command.label)
172
+ if ss is None:
173
+ return msg, None, None
174
+ (images_dir / img_name).write_bytes(ss)
175
+ return msg, ss, f"images/{img_name}"
176
+ except httpx.HTTPError as e:
177
+ msg = f"Action '{command.label}' failed: {type(e).__name__}: {e}"
178
+ _log.warning(msg)
179
+ ss = await _take_screenshot_with_recovery(sandbox, command.label)
180
+ if ss is None:
181
+ return msg, None, None
182
+ (images_dir / img_name).write_bytes(ss)
183
+ return msg, ss, f"images/{img_name}"
184
+
185
+ action_elapsed = time.monotonic() - t0
186
+ await asyncio.sleep(2)
187
+ ss = await _take_screenshot_with_recovery(sandbox, command.label)
188
+ if ss is None:
189
+ return (
190
+ f"Action '{command.label}' executed; screenshot failed after retries",
191
+ None,
192
+ None,
193
+ )
194
+ annotated = _annotate_click(ss, action)
195
+ (images_dir / img_name).write_bytes(annotated)
196
+ total_elapsed = time.monotonic() - t0
197
+ screenshot_elapsed = total_elapsed - action_elapsed
198
+ _log.info(
199
+ f"action {command.label}: {action_elapsed:.2f}s + "
200
+ f"screenshot {screenshot_elapsed:.2f}s = {total_elapsed:.2f}s total"
201
+ )
202
+ return f"Action '{command.label}' executed", ss, f"images/{img_name}"
203
+
204
+
205
+ async def _take_screenshot_with_recovery(
206
+ sandbox: ComputerSandbox, action_type: str
207
+ ) -> bytes | None:
208
+ attempts = 4
209
+ for attempt in range(1, attempts + 1):
210
+ try:
211
+ return await sandbox.screenshot.take_full_screen()
212
+ except httpx.HTTPStatusError as e:
213
+ if attempt == attempts:
214
+ _log.warning(
215
+ f"screenshot after {action_type} failed after retries: "
216
+ f"HTTP {e.response.status_code} {e.response.text[:160]}"
217
+ )
218
+ return None
219
+ _log.warning(
220
+ f"screenshot after {action_type} failed: HTTP {e.response.status_code}; "
221
+ f"retrying in 10s ({attempt}/{attempts - 1})"
222
+ )
223
+ await asyncio.sleep(10)
224
+ except httpx.HTTPError as e:
225
+ if attempt == attempts:
226
+ _log.warning(
227
+ f"screenshot after {action_type} failed after retries: {type(e).__name__}: {e}"
228
+ )
229
+ return None
230
+ _log.warning(
231
+ f"screenshot after {action_type} failed: {type(e).__name__}; "
232
+ f"retrying in 10s ({attempt}/{attempts - 1})"
233
+ )
234
+ await asyncio.sleep(10)