use-computer 0.0.38__tar.gz → 0.0.40__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 (97) hide show
  1. {use_computer-0.0.38 → use_computer-0.0.40}/PKG-INFO +3 -1
  2. {use_computer-0.0.38 → use_computer-0.0.40}/pyproject.toml +3 -1
  3. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/base/recordings.py +25 -11
  4. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/providers/generic.py +20 -30
  5. use_computer-0.0.40/use_computer/agents/providers/tinker_backend.py +163 -0
  6. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/client.py +18 -8
  7. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/harbor/environment/platforms/__init__.py +2 -1
  8. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/harbor/environment/platforms/ubuntu.py +24 -0
  9. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/sandbox.py +14 -4
  10. {use_computer-0.0.38 → use_computer-0.0.40}/uv.lock +879 -29
  11. {use_computer-0.0.38 → use_computer-0.0.40}/.env.example +0 -0
  12. {use_computer-0.0.38 → use_computer-0.0.40}/.gitignore +0 -0
  13. {use_computer-0.0.38 → use_computer-0.0.40}/.pre-commit-config.yaml +0 -0
  14. {use_computer-0.0.38 → use_computer-0.0.40}/README.md +0 -0
  15. {use_computer-0.0.38 → use_computer-0.0.40}/scripts/smoke_wheel.py +0 -0
  16. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/__init__.py +0 -0
  17. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/accessibility.py +0 -0
  18. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/__init__.py +0 -0
  19. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/base/__init__.py +0 -0
  20. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/base/actions.py +0 -0
  21. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/base/agent.py +0 -0
  22. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/base/artifacts.py +0 -0
  23. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/base/desktop.py +0 -0
  24. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/base/final_state.py +0 -0
  25. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/base/prompts.py +0 -0
  26. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/base/sandbox.py +0 -0
  27. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/base/screenshots.py +0 -0
  28. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/base/trajectory.py +0 -0
  29. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/core.py +0 -0
  30. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/debug/README.md +0 -0
  31. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/debug/__init__.py +0 -0
  32. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/debug/actions.py +0 -0
  33. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/debug/agent.py +0 -0
  34. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/debug/models.py +0 -0
  35. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/ios/__init__.py +0 -0
  36. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/ios/agent.py +0 -0
  37. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/ios/tools.py +0 -0
  38. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/prompts/anthropic.txt +0 -0
  39. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/prompts/gemini.txt +0 -0
  40. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/prompts/ios.txt +0 -0
  41. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/prompts/openai.txt +0 -0
  42. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/prompts/pyautogui.txt +0 -0
  43. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/providers/__init__.py +0 -0
  44. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/providers/anthropic.py +0 -0
  45. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/providers/gemini.py +0 -0
  46. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/providers/openai.py +0 -0
  47. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/agents/types.py +0 -0
  48. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/ax_transpile.py +0 -0
  49. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/display.py +0 -0
  50. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/errors.py +0 -0
  51. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/harbor/__init__.py +0 -0
  52. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/harbor/agents.py +0 -0
  53. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/harbor/environment/__init__.py +0 -0
  54. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/harbor/environment/gateway.py +0 -0
  55. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/harbor/environment/platforms/ios.py +0 -0
  56. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/harbor/environment/platforms/macos.py +0 -0
  57. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/harbor/environment/platforms/windows.py +0 -0
  58. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/harbor/environment/setup_files.py +0 -0
  59. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/harbor/environment/use_computer.py +0 -0
  60. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/harbor/environment/utils/__init__.py +0 -0
  61. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/harbor/environment/utils/verifier.py +0 -0
  62. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/ios/__init__.py +0 -0
  63. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/ios/apps.py +0 -0
  64. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/ios/environment.py +0 -0
  65. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/ios/input.py +0 -0
  66. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/macos/__init__.py +0 -0
  67. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/macos/keyboard.py +0 -0
  68. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/macos/mouse.py +0 -0
  69. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/models.py +0 -0
  70. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/parsers.py +0 -0
  71. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/py.typed +0 -0
  72. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/recording.py +0 -0
  73. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/resources/__init__.py +0 -0
  74. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/resources/accessibility.py +0 -0
  75. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/resources/display.py +0 -0
  76. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/resources/recording.py +0 -0
  77. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/resources/screenshot.py +0 -0
  78. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/retry.py +0 -0
  79. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/screenshot.py +0 -0
  80. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/simulators.py +0 -0
  81. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/__init__.py +0 -0
  82. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/pre_command.ps1 +0 -0
  83. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/pre_command.sh +0 -0
  84. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/task.toml +0 -0
  85. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/test_ios.sh +0 -0
  86. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/test_ios_nograder.sh +0 -0
  87. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/test_macos.sh +0 -0
  88. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/test_macos_check.sh +0 -0
  89. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/test_macos_nograder.sh +0 -0
  90. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/test_ubuntu.sh +0 -0
  91. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/test_ubuntu_check.sh +0 -0
  92. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/test_ubuntu_nograder.sh +0 -0
  93. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/test_windows.sh +0 -0
  94. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/test_windows_check.sh +0 -0
  95. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/tasks/templates/test_windows_nograder.sh +0 -0
  96. {use_computer-0.0.38 → use_computer-0.0.40}/use_computer/windows/__init__.py +0 -0
  97. {use_computer-0.0.38 → use_computer-0.0.40}/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.38
3
+ Version: 0.0.40
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
@@ -24,6 +24,8 @@ Requires-Dist: google-genai>=1; extra == 'agents'
24
24
  Requires-Dist: litellm>=1; extra == 'agents'
25
25
  Requires-Dist: openai>=1; extra == 'agents'
26
26
  Requires-Dist: pillow>=10; extra == 'agents'
27
+ Requires-Dist: tinker-cookbook>=0.1.0; (python_full_version >= '3.11') and extra == 'agents'
28
+ Requires-Dist: tinker>=0.14.0; (python_full_version >= '3.11') and extra == 'agents'
27
29
  Provides-Extra: harbor
28
30
  Requires-Dist: harbor>=0.4.0; (python_version >= '3.12') and extra == 'harbor'
29
31
  Requires-Dist: pyyaml>=6; extra == 'harbor'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "use-computer"
7
- version = "0.0.38"
7
+ version = "0.0.40"
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"
@@ -30,6 +30,8 @@ agents = [
30
30
  "anthropic>=0.86",
31
31
  "litellm>=1",
32
32
  "google-genai>=1",
33
+ "tinker>=0.14.0 ; python_full_version >= '3.11'",
34
+ "tinker-cookbook>=0.1.0 ; python_full_version >= '3.11'",
33
35
  ]
34
36
  harbor = [
35
37
  "harbor>=0.4.0; python_version >= '3.12'",
@@ -11,6 +11,11 @@ from use_computer.sandbox import AsyncSandbox
11
11
 
12
12
  class RecordingMixin:
13
13
  async def start_recording(self: Any, sandbox: AsyncSandbox) -> None:
14
+ if getattr(getattr(sandbox, "type", None), "value", "") == "ios":
15
+ try:
16
+ await sandbox.screenshot.take_full_screen()
17
+ except Exception as e:
18
+ self.logger.warning(f"iOS recording warm-up screenshot failed: {e}")
14
19
  try:
15
20
  result = await sandbox.recording.start(name="trial")
16
21
  self._recording_id = result.id
@@ -22,6 +27,7 @@ class RecordingMixin:
22
27
  if not self._recording_id:
23
28
  return
24
29
  await asyncio.sleep(3.0)
30
+ rec_path = self.logs_dir / "recording.mp4"
25
31
  try:
26
32
  info = await sandbox.recording.stop(self._recording_id)
27
33
  self.logger.info(
@@ -33,16 +39,24 @@ class RecordingMixin:
33
39
 
34
40
  await asyncio.sleep(2)
35
41
 
36
- rec_path = self.logs_dir / "recording.mp4"
37
- self.logger.info(f"Downloading recording → {rec_path}")
38
- t_dl = time.monotonic()
39
- await sandbox.recording.download(self._recording_id, str(rec_path), timeout=180.0)
40
- dl_elapsed = time.monotonic() - t_dl
41
- local_size = rec_path.stat().st_size
42
- self.logger.info(
43
- f"Recording saved: {rec_path} "
44
- f"local_size={local_size}B ({local_size / (1024 * 1024):.2f} MB) "
45
- f"download_elapsed={dl_elapsed:.1f}s"
46
- )
42
+ await self._download_recording(sandbox, rec_path)
47
43
  except Exception as e:
48
44
  self.logger.warning(f"Recording save failed: {type(e).__name__}: {e}")
45
+ try:
46
+ await self._download_recording(sandbox, rec_path)
47
+ except Exception as fallback:
48
+ self.logger.warning(
49
+ f"Recording fallback download failed: {type(fallback).__name__}: {fallback}"
50
+ )
51
+
52
+ async def _download_recording(self: Any, sandbox: AsyncSandbox, rec_path) -> None:
53
+ self.logger.info(f"Downloading recording → {rec_path}")
54
+ t_dl = time.monotonic()
55
+ await sandbox.recording.download(self._recording_id, str(rec_path), timeout=180.0)
56
+ dl_elapsed = time.monotonic() - t_dl
57
+ local_size = rec_path.stat().st_size
58
+ self.logger.info(
59
+ f"Recording saved: {rec_path} "
60
+ f"local_size={local_size}B ({local_size / (1024 * 1024):.2f} MB) "
61
+ f"download_elapsed={dl_elapsed:.1f}s"
62
+ )
@@ -83,6 +83,11 @@ class GenericCUAAgent(BaseCUAAgent):
83
83
  coord_mode: str | None = None,
84
84
  reasoning_effort: str | None = None,
85
85
  reasoning_history: str | None = None,
86
+ renderer_name: str | None = None,
87
+ model_path: str | None = None,
88
+ temperature: float = 1.0,
89
+ top_p: float = 0.95,
90
+ top_k: int = -1,
86
91
  **kwargs: Any,
87
92
  ):
88
93
  super().__init__(logs_dir=logs_dir, model_name=model_name, **kwargs)
@@ -106,7 +111,12 @@ class GenericCUAAgent(BaseCUAAgent):
106
111
  ReasoningHistory(reasoning_history) if reasoning_history is not None else None
107
112
  ),
108
113
  ).as_dict()
109
- self._tinker_llm = None
114
+ self._tinker_backend = None
115
+ self._renderer_name = renderer_name
116
+ self._model_path = model_path
117
+ self._temperature = temperature
118
+ self._top_p = top_p
119
+ self._top_k = top_k
110
120
  # Coordinate convention emitted by the model:
111
121
  # "api_pixel" — Kimi default: 0..1 floats OR pixel ints in API space (1280x800)
112
122
  # "normalized_1000" — Qwen-VL default: 0..1000 ints as per-mille of screen
@@ -168,39 +178,19 @@ class GenericCUAAgent(BaseCUAAgent):
168
178
  async def _tinker_completion(
169
179
  self, model: str, messages: list[dict], max_tokens: int
170
180
  ) -> tuple[str, int, int]:
171
- if self._tinker_llm is None:
172
- from harbor.llms.tinker import TinkerLLM
181
+ if self._tinker_backend is None:
182
+ from use_computer.agents.providers.tinker_backend import TinkerBackend
173
183
 
174
- self._tinker_llm = TinkerLLM(
184
+ self._tinker_backend = TinkerBackend(
175
185
  model_name=model,
186
+ model_path=self._model_path,
187
+ renderer_name=self._renderer_name,
188
+ temperature=self._temperature,
189
+ top_p=self._top_p,
190
+ top_k=self._top_k,
176
191
  max_tokens=max_tokens,
177
192
  )
178
-
179
- # TinkerLLM.call() expects (prompt, message_history)
180
- # Split system + history from the final user message
181
- history = messages[:-1] if len(messages) > 1 else []
182
- prompt = ""
183
- last_msg = messages[-1] if messages else {"content": ""}
184
- content = last_msg.get("content")
185
- if isinstance(content, str):
186
- prompt = content
187
- elif isinstance(content, list):
188
- prompt = " ".join(
189
- p.get("text", "")
190
- for p in content
191
- if isinstance(p, dict) and p.get("type") == "text"
192
- )
193
-
194
- response = await self._tinker_llm.call(prompt=prompt, message_history=history)
195
- content = response.content or ""
196
- text = (
197
- content
198
- if isinstance(content, str)
199
- else "".join(p.get("text", "") if isinstance(p, dict) else str(p) for p in content)
200
- )
201
- prompt_tokens = response.usage.prompt_tokens if response.usage else 0
202
- completion_tokens = response.usage.completion_tokens if response.usage else 0
203
- return text, prompt_tokens, completion_tokens
193
+ return await self._tinker_backend.complete(messages, max_tokens)
204
194
 
205
195
  async def _run_agent(
206
196
  self,
@@ -0,0 +1,163 @@
1
+ """Image-aware Tinker sampling backend for the generic CUA agent.
2
+
3
+ harbor's TinkerLLM forwards text only, so a vision agent loses its screenshots
4
+ on the tinker path. This backend builds the generation prompt through a
5
+ tinker-cookbook renderer with the model's image processor attached, so the
6
+ OpenAI-style `image_url` parts the generic agent emits reach the model.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import base64
13
+ import importlib
14
+ import io
15
+ from typing import Any, cast
16
+
17
+ from PIL import Image
18
+
19
+
20
+ class TinkerBackend:
21
+ """Vision-capable tinker sampler that consumes OpenAI chat messages."""
22
+
23
+ def __init__(
24
+ self,
25
+ *,
26
+ model_name: str,
27
+ model_path: str | None = None,
28
+ renderer_name: str | None = None,
29
+ temperature: float = 1.0,
30
+ top_p: float = 0.95,
31
+ top_k: int = -1,
32
+ max_tokens: int = 2048,
33
+ retry_times: int = 5,
34
+ retry_interval: float = 5.0,
35
+ ) -> None:
36
+ self.model_name = model_name
37
+ self.model_path = model_path
38
+ self.renderer_name = renderer_name
39
+ self.temperature = temperature
40
+ self.top_p = top_p
41
+ self.top_k = top_k
42
+ self.max_tokens = max_tokens
43
+ self.retry_times = retry_times
44
+ self.retry_interval = retry_interval
45
+ self._sampling_client = None
46
+ self._renderer = None
47
+
48
+ async def _ensure_ready(self) -> None:
49
+ if self._sampling_client is not None and self._renderer is not None:
50
+ return
51
+ try:
52
+ tinker = importlib.import_module("tinker")
53
+ get_image_processor = importlib.import_module(
54
+ "tinker_cookbook.image_processing_utils"
55
+ ).get_image_processor
56
+ get_model_attributes = importlib.import_module(
57
+ "tinker_cookbook.model_info"
58
+ ).get_model_attributes
59
+ get_renderer = importlib.import_module("tinker_cookbook.renderers").get_renderer
60
+ get_tokenizer = importlib.import_module("tinker_cookbook.tokenizer_utils").get_tokenizer
61
+ except ImportError as exc:
62
+ raise ImportError(
63
+ "TinkerBackend requires `tinker` and `tinker-cookbook`. "
64
+ "Install them with `uv pip install harbor[tinker]`."
65
+ ) from exc
66
+
67
+ service_client = tinker.ServiceClient()
68
+ if self.model_path:
69
+ self._sampling_client = await service_client.create_sampling_client_async(
70
+ model_path=self.model_path
71
+ )
72
+ else:
73
+ self._sampling_client = await service_client.create_sampling_client_async(
74
+ base_model=self.model_name
75
+ )
76
+
77
+ tokenizer = get_tokenizer(self.model_name)
78
+ attrs = get_model_attributes(self.model_name)
79
+ image_processor = get_image_processor(self.model_name) if attrs.is_vl else None
80
+ renderer_name = self.renderer_name or _default_renderer(self.model_name)
81
+ self._renderer = get_renderer(
82
+ renderer_name,
83
+ tokenizer,
84
+ image_processor=image_processor,
85
+ model_name=self.model_name,
86
+ )
87
+
88
+ async def complete(
89
+ self, messages: list[dict[str, Any]], max_tokens: int
90
+ ) -> tuple[str, int, int]:
91
+ """Sample one response; returns (text, prompt_tokens, completion_tokens)."""
92
+ await self._ensure_ready()
93
+ assert self._sampling_client is not None
94
+ assert self._renderer is not None
95
+
96
+ tinker = importlib.import_module("tinker")
97
+ get_text_content = importlib.import_module("tinker_cookbook.renderers").get_text_content
98
+
99
+ model_input = self._renderer.build_generation_prompt(
100
+ cast(Any, _to_tinker_messages(messages))
101
+ )
102
+ stop = self._renderer.get_stop_sequences()
103
+
104
+ last_error: Exception | None = None
105
+ for attempt in range(self.retry_times):
106
+ try:
107
+ response = await self._sampling_client.sample_async(
108
+ prompt=model_input,
109
+ num_samples=1,
110
+ sampling_params=tinker.SamplingParams(
111
+ temperature=self.temperature,
112
+ top_p=self.top_p,
113
+ top_k=self.top_k,
114
+ max_tokens=max_tokens,
115
+ stop=stop,
116
+ ),
117
+ )
118
+ sequence = response.sequences[0]
119
+ parsed_message, _termination = self._renderer.parse_response(sequence.tokens)
120
+ return get_text_content(parsed_message), model_input.length, len(sequence.tokens)
121
+ except Exception as exc:
122
+ last_error = exc
123
+ if attempt == self.retry_times - 1:
124
+ break
125
+ await asyncio.sleep(self.retry_interval)
126
+ assert last_error is not None
127
+ raise last_error
128
+
129
+
130
+ def _to_tinker_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
131
+ """Convert OpenAI chat messages to tinker-cookbook messages (text + PIL images)."""
132
+ converted: list[dict[str, Any]] = []
133
+ for message in messages:
134
+ content = message.get("content")
135
+ if isinstance(content, str):
136
+ converted.append({"role": message["role"], "content": content})
137
+ continue
138
+
139
+ parts: list[dict[str, Any]] = []
140
+ for part in content or []:
141
+ if part.get("type") == "text":
142
+ parts.append({"type": "text", "text": part.get("text", "")})
143
+ elif part.get("type") == "image_url":
144
+ url = part.get("image_url", {}).get("url", "")
145
+ b64 = url.split(",", 1)[1] if "," in url else url
146
+ image = Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB")
147
+ parts.append({"type": "image", "image": image})
148
+ if len(parts) == 1 and parts[0]["type"] == "text":
149
+ converted.append({"role": message["role"], "content": parts[0]["text"]})
150
+ else:
151
+ converted.append({"role": message["role"], "content": parts})
152
+ return converted
153
+
154
+
155
+ def _default_renderer(model_name: str) -> str:
156
+ get_recommended_renderer_names = importlib.import_module(
157
+ "tinker_cookbook.model_info"
158
+ ).get_recommended_renderer_names
159
+
160
+ recommended = get_recommended_renderer_names(model_name)
161
+ if "qwen3_5_disable_thinking" in recommended:
162
+ return "qwen3_5_disable_thinking"
163
+ return recommended[0]
@@ -53,6 +53,12 @@ def _normalize_sandbox_type(type: SandboxTypeInput) -> SandboxType:
53
53
  return SandboxType(type)
54
54
 
55
55
 
56
+ def _expand_core_simulator_id(value: str, kind: str) -> str:
57
+ if not value or value.startswith("com.apple."):
58
+ return value
59
+ return f"com.apple.CoreSimulator.{kind}.{value}"
60
+
61
+
56
62
  @dataclass
57
63
  class RunStatus:
58
64
  """Status of an ad-hoc or model run."""
@@ -183,7 +189,7 @@ class Computer:
183
189
 
184
190
  Args:
185
191
  type: SandboxType.MACOS (default), IOS, WINDOWS, UBUNTU, or the raw API string.
186
- host: Pin sandbox to a specific host machine (e.g. "mm001").
192
+ host: Pin sandbox to a specific host machine; for iOS this must be a Mini IP.
187
193
  family: Simulator family enum/alias, for example SimulatorFamily.TV.
188
194
  device_type: Simulator device type identifier, for exact pinning.
189
195
  runtime: Simulator runtime identifier, for exact pinning.
@@ -195,7 +201,7 @@ class Computer:
195
201
  """
196
202
  sandbox_type = _normalize_sandbox_type(type)
197
203
  body: dict = {"type": sandbox_type.value}
198
- if host:
204
+ if host and sandbox_type != SandboxType.IOS:
199
205
  body["host"] = host
200
206
  if sandbox_type in (SandboxType.WINDOWS, SandboxType.UBUNTU):
201
207
  if version and snapshot:
@@ -207,14 +213,16 @@ class Computer:
207
213
  if resources:
208
214
  body["resources"] = resources
209
215
  if sandbox_type == SandboxType.IOS:
216
+ if host:
217
+ body["mini_ip"] = host
210
218
  if family and (not device_type or not runtime):
211
219
  choice = select_simulator(self.platforms(), family)
212
220
  device_type = device_type or choice.device_type
213
221
  runtime = runtime or choice.runtime
214
222
  if device_type:
215
- body["device_type"] = device_type
223
+ body["device_type"] = _expand_core_simulator_id(device_type, "SimDeviceType")
216
224
  if runtime:
217
- body["runtime"] = runtime
225
+ body["runtime"] = _expand_core_simulator_id(runtime, "SimRuntime")
218
226
 
219
227
  resp = self._http.post("/v1/sandboxes", json=body, timeout=180.0)
220
228
  resp.raise_for_status()
@@ -425,7 +433,7 @@ class AsyncComputer:
425
433
 
426
434
  Args:
427
435
  type: SandboxType.MACOS (default), IOS, WINDOWS, UBUNTU, or the raw API string.
428
- host: Pin sandbox to a specific host machine (e.g. "mm001").
436
+ host: Pin sandbox to a specific host machine; for iOS this must be a Mini IP.
429
437
  family: Simulator family enum/alias, for example SimulatorFamily.TV.
430
438
  device_type: Simulator device type identifier, for exact pinning.
431
439
  runtime: Simulator runtime identifier, for exact pinning.
@@ -437,7 +445,7 @@ class AsyncComputer:
437
445
  """
438
446
  sandbox_type = _normalize_sandbox_type(type)
439
447
  body: dict = {"type": sandbox_type.value}
440
- if host:
448
+ if host and sandbox_type != SandboxType.IOS:
441
449
  body["host"] = host
442
450
  if sandbox_type in (SandboxType.WINDOWS, SandboxType.UBUNTU):
443
451
  if version and snapshot:
@@ -449,14 +457,16 @@ class AsyncComputer:
449
457
  if resources:
450
458
  body["resources"] = resources
451
459
  if sandbox_type == SandboxType.IOS:
460
+ if host:
461
+ body["mini_ip"] = host
452
462
  if family and (not device_type or not runtime):
453
463
  choice = select_simulator(await self.platforms(), family)
454
464
  device_type = device_type or choice.device_type
455
465
  runtime = runtime or choice.runtime
456
466
  if device_type:
457
- body["device_type"] = device_type
467
+ body["device_type"] = _expand_core_simulator_id(device_type, "SimDeviceType")
458
468
  if runtime:
459
- body["runtime"] = runtime
469
+ body["runtime"] = _expand_core_simulator_id(runtime, "SimRuntime")
460
470
 
461
471
  resp = await self._http.post("/v1/sandboxes", json=body, timeout=180.0)
462
472
  resp.raise_for_status()
@@ -104,12 +104,13 @@ class IOSAdapter(PlatformAdapter):
104
104
  env.logger.info(f"ios pin: device_type={device_type!r} runtime={runtime!r}")
105
105
  return await env._client.create(
106
106
  type="ios",
107
+ host=env._host,
107
108
  device_type=device_type,
108
109
  runtime=runtime,
109
110
  )
110
111
 
111
112
  async def setup(self, env: Any) -> None:
112
- await asyncio.sleep(60.0)
113
+ await asyncio.sleep(2.0)
113
114
 
114
115
  async def exec(
115
116
  self,
@@ -109,6 +109,8 @@ async def exec_local(
109
109
 
110
110
 
111
111
  async def run_setup(env: Any) -> None:
112
+ await wake_desktop(env)
113
+
112
114
  setup_dir = env._task_dir / "tests" / "setup"
113
115
 
114
116
  for setup_file in read_setup_files(setup_dir):
@@ -139,6 +141,28 @@ async def run_setup(env: Any) -> None:
139
141
  env.logger.info("task setup complete")
140
142
 
141
143
 
144
+ async def wake_desktop(env: Any) -> None:
145
+ script = """
146
+ echo 'user:lume' | sudo chpasswd || true
147
+ export DISPLAY=:0
148
+ export XAUTHORITY=/home/user/.Xauthority
149
+ gsettings set org.gnome.desktop.screensaver lock-enabled false || true
150
+ gsettings set org.gnome.desktop.session idle-delay 0 || true
151
+ xset s off -dpms || true
152
+ xset dpms force on || true
153
+ xdotool key Escape || true
154
+ sleep 1
155
+ xdotool key ctrl+a || true
156
+ xdotool type lume || true
157
+ xdotool key Return || true
158
+ sleep 3
159
+ xset dpms force on || true
160
+ """
161
+ result = await env.ubuntu_sandbox.shell.run(script, shell="bash", timeout=60)
162
+ if result.return_code != 0:
163
+ env.logger.warning(f"ubuntu desktop wake rc={result.return_code}: {result.stderr or ''}")
164
+
165
+
142
166
  async def upload_file(env: Any, source: Path | str, target: str) -> None:
143
167
  await env.ubuntu_sandbox.upload(str(source), target)
144
168
 
@@ -236,7 +236,11 @@ class MacOSSandbox(Sandbox):
236
236
  return ActResult(data=resp.json())
237
237
 
238
238
  def exec_ssh(self, command: str, timeout: int = 120) -> ExecResult:
239
- resp = self._http.post(f"{self._prefix}/exec", json={"command": command}, timeout=timeout)
239
+ resp = self._http.post(
240
+ f"{self._prefix}/exec",
241
+ json={"command": command, "timeout": timeout},
242
+ timeout=timeout,
243
+ )
240
244
  resp.raise_for_status()
241
245
  return ExecResult.from_dict(resp.json())
242
246
 
@@ -254,7 +258,9 @@ class MacOSSandbox(Sandbox):
254
258
  command if you need brew binaries.
255
259
  """
256
260
  resp = self._http.post(
257
- f"{self._prefix}/exec_ax", json={"command": command}, timeout=timeout
261
+ f"{self._prefix}/exec_ax",
262
+ json={"command": command, "timeout": timeout},
263
+ timeout=timeout,
258
264
  )
259
265
  resp.raise_for_status()
260
266
  return ExecResult.from_dict(resp.json())
@@ -650,7 +656,9 @@ class AsyncMacOSSandbox(AsyncSandbox):
650
656
 
651
657
  async def exec_ssh(self, command: str, timeout: int = 120) -> ExecResult:
652
658
  resp = await self._http.post(
653
- f"{self._prefix}/exec", json={"command": command}, timeout=timeout
659
+ f"{self._prefix}/exec",
660
+ json={"command": command, "timeout": timeout},
661
+ timeout=timeout,
654
662
  )
655
663
  resp.raise_for_status()
656
664
  return ExecResult.from_dict(resp.json())
@@ -658,7 +666,9 @@ class AsyncMacOSSandbox(AsyncSandbox):
658
666
  async def exec_ax(self, command: str, timeout: int = 120) -> ExecResult:
659
667
  """Async version of exec_ax — see MacOSSandbox.exec_ax for details."""
660
668
  resp = await self._http.post(
661
- f"{self._prefix}/exec_ax", json={"command": command}, timeout=timeout
669
+ f"{self._prefix}/exec_ax",
670
+ json={"command": command, "timeout": timeout},
671
+ timeout=timeout,
662
672
  )
663
673
  resp.raise_for_status()
664
674
  return ExecResult.from_dict(resp.json())