minitap-mobile-use 2.3.0__py3-none-any.whl → 2.5.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.

Potentially problematic release.


This version of minitap-mobile-use might be problematic. Click here for more details.

Files changed (56) hide show
  1. minitap/mobile_use/agents/contextor/contextor.py +2 -2
  2. minitap/mobile_use/agents/cortex/cortex.md +49 -8
  3. minitap/mobile_use/agents/cortex/cortex.py +8 -4
  4. minitap/mobile_use/agents/executor/executor.md +14 -11
  5. minitap/mobile_use/agents/executor/executor.py +6 -5
  6. minitap/mobile_use/agents/hopper/hopper.py +6 -3
  7. minitap/mobile_use/agents/orchestrator/orchestrator.py +26 -11
  8. minitap/mobile_use/agents/outputter/outputter.py +6 -3
  9. minitap/mobile_use/agents/planner/planner.md +20 -22
  10. minitap/mobile_use/agents/planner/planner.py +10 -7
  11. minitap/mobile_use/agents/planner/types.py +4 -2
  12. minitap/mobile_use/agents/planner/utils.py +14 -0
  13. minitap/mobile_use/agents/summarizer/summarizer.py +2 -2
  14. minitap/mobile_use/config.py +6 -1
  15. minitap/mobile_use/context.py +13 -3
  16. minitap/mobile_use/controllers/mobile_command_controller.py +1 -14
  17. minitap/mobile_use/graph/state.py +7 -3
  18. minitap/mobile_use/sdk/agent.py +188 -23
  19. minitap/mobile_use/sdk/examples/README.md +19 -1
  20. minitap/mobile_use/sdk/examples/platform_manual_task_example.py +65 -0
  21. minitap/mobile_use/sdk/examples/platform_minimal_example.py +46 -0
  22. minitap/mobile_use/sdk/services/platform.py +307 -0
  23. minitap/mobile_use/sdk/types/__init__.py +16 -14
  24. minitap/mobile_use/sdk/types/exceptions.py +27 -0
  25. minitap/mobile_use/sdk/types/platform.py +127 -0
  26. minitap/mobile_use/sdk/types/task.py +78 -17
  27. minitap/mobile_use/servers/device_hardware_bridge.py +1 -1
  28. minitap/mobile_use/servers/stop_servers.py +11 -12
  29. minitap/mobile_use/services/llm.py +89 -5
  30. minitap/mobile_use/tools/index.py +0 -6
  31. minitap/mobile_use/tools/mobile/back.py +3 -3
  32. minitap/mobile_use/tools/mobile/clear_text.py +24 -43
  33. minitap/mobile_use/tools/mobile/erase_one_char.py +5 -4
  34. minitap/mobile_use/tools/mobile/glimpse_screen.py +11 -7
  35. minitap/mobile_use/tools/mobile/input_text.py +21 -51
  36. minitap/mobile_use/tools/mobile/launch_app.py +54 -22
  37. minitap/mobile_use/tools/mobile/long_press_on.py +15 -8
  38. minitap/mobile_use/tools/mobile/open_link.py +15 -8
  39. minitap/mobile_use/tools/mobile/press_key.py +15 -8
  40. minitap/mobile_use/tools/mobile/stop_app.py +14 -8
  41. minitap/mobile_use/tools/mobile/swipe.py +11 -5
  42. minitap/mobile_use/tools/mobile/tap.py +103 -21
  43. minitap/mobile_use/tools/mobile/wait_for_animation_to_end.py +3 -3
  44. minitap/mobile_use/tools/test_utils.py +104 -78
  45. minitap/mobile_use/tools/types.py +35 -0
  46. minitap/mobile_use/tools/utils.py +51 -48
  47. minitap/mobile_use/utils/recorder.py +1 -1
  48. minitap/mobile_use/utils/ui_hierarchy.py +9 -2
  49. {minitap_mobile_use-2.3.0.dist-info → minitap_mobile_use-2.5.0.dist-info}/METADATA +3 -1
  50. minitap_mobile_use-2.5.0.dist-info/RECORD +100 -0
  51. minitap/mobile_use/tools/mobile/copy_text_from.py +0 -75
  52. minitap/mobile_use/tools/mobile/find_packages.py +0 -69
  53. minitap/mobile_use/tools/mobile/paste_text.py +0 -88
  54. minitap_mobile_use-2.3.0.dist-info/RECORD +0 -98
  55. {minitap_mobile_use-2.3.0.dist-info → minitap_mobile_use-2.5.0.dist-info}/WHEEL +0 -0
  56. {minitap_mobile_use-2.3.0.dist-info → minitap_mobile_use-2.5.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,307 @@
1
+ import json
2
+ from datetime import UTC, datetime
3
+ from typing import Any
4
+
5
+ import httpx
6
+ from pydantic import BaseModel, ValidationError
7
+
8
+ from minitap.mobile_use.agents.planner.types import Subgoal, SubgoalStatus
9
+ from minitap.mobile_use.config import LLMConfig, settings
10
+ from minitap.mobile_use.sdk.types.exceptions import PlatformServiceError
11
+ from minitap.mobile_use.sdk.types.platform import (
12
+ CreateTaskRunRequest,
13
+ LLMProfileResponse,
14
+ MobileUseSubgoal,
15
+ SubgoalState,
16
+ TaskResponse,
17
+ TaskRunPlanResponse,
18
+ TaskRunResponse,
19
+ TaskRunStatus,
20
+ UpdateTaskRunStatusRequest,
21
+ UpsertTaskRunAgentThoughtRequest,
22
+ UpsertTaskRunPlanRequest,
23
+ )
24
+ from minitap.mobile_use.sdk.types.task import (
25
+ AgentProfile,
26
+ ManualTaskConfig,
27
+ PlatformTaskInfo,
28
+ PlatformTaskRequest,
29
+ TaskRequest,
30
+ )
31
+ from minitap.mobile_use.utils.logger import get_logger
32
+
33
+ logger = get_logger(__name__)
34
+
35
+ DEFAULT_PROFILE = "default"
36
+
37
+
38
+ class PlatformService:
39
+ def __init__(self, api_key: str | None = None):
40
+ self._base_url = settings.MINITAP_API_BASE_URL
41
+
42
+ if api_key:
43
+ self._api_key = api_key
44
+ elif settings.MINITAP_API_KEY:
45
+ self._api_key = settings.MINITAP_API_KEY.get_secret_value()
46
+ else:
47
+ raise PlatformServiceError(
48
+ message="Please provide an API key or set MINITAP_API_KEY environment variable.",
49
+ )
50
+
51
+ self._timeout = httpx.Timeout(timeout=120)
52
+ self._client = httpx.AsyncClient(
53
+ base_url=f"{self._base_url}/api",
54
+ timeout=self._timeout,
55
+ headers={
56
+ "Authorization": f"Bearer {self._api_key}",
57
+ "Content-Type": "application/json",
58
+ },
59
+ )
60
+
61
+ async def create_task_run(self, request: PlatformTaskRequest) -> PlatformTaskInfo:
62
+ try:
63
+ # Check if task is a string (fetch from platform) or ManualTaskConfig (create manually)
64
+ if isinstance(request.task, str):
65
+ # Fetch task from platform
66
+ logger.info(f"Getting task: {request.task}")
67
+ response = await self._client.get(url=f"v1/tasks/{request.task}")
68
+ response.raise_for_status()
69
+ task_data = response.json()
70
+ task = TaskResponse(**task_data)
71
+
72
+ profile, agent_profile = await self._get_profile(
73
+ profile_name=request.profile or DEFAULT_PROFILE,
74
+ )
75
+
76
+ task_request = TaskRequest(
77
+ # Remote configuration
78
+ max_steps=task.options.max_steps,
79
+ goal=task.input_prompt,
80
+ output_description=task.output_description,
81
+ enable_remote_tracing=task.options.enable_tracing,
82
+ profile=profile.name,
83
+ # Local configuration
84
+ record_trace=request.record_trace,
85
+ trace_path=request.trace_path,
86
+ llm_output_path=request.llm_output_path,
87
+ thoughts_output_path=request.thoughts_output_path,
88
+ )
89
+
90
+ task_run = await self._create_task_run(task=task, profile=profile)
91
+ else:
92
+ # Create task manually from ManualTaskConfig
93
+ logger.info(f"Creating manual task with goal: {request.task.goal}")
94
+
95
+ profile, agent_profile = await self._get_profile(
96
+ profile_name=request.profile or DEFAULT_PROFILE,
97
+ )
98
+
99
+ task_request = TaskRequest(
100
+ # Manual configuration
101
+ max_steps=400,
102
+ goal=request.task.goal,
103
+ output_description=request.task.output_description,
104
+ enable_remote_tracing=True,
105
+ profile=DEFAULT_PROFILE,
106
+ # Local configuration
107
+ record_trace=request.record_trace,
108
+ trace_path=request.trace_path,
109
+ llm_output_path=request.llm_output_path,
110
+ thoughts_output_path=request.thoughts_output_path,
111
+ )
112
+
113
+ task_run = await self._create_manual_task_run(
114
+ manual_config=request.task,
115
+ profile=profile,
116
+ )
117
+
118
+ return PlatformTaskInfo(
119
+ task_request=task_request,
120
+ llm_profile=agent_profile,
121
+ task_run=task_run,
122
+ )
123
+ except httpx.HTTPStatusError as e:
124
+ raise PlatformServiceError(message=f"Failed to get task: {e}")
125
+
126
+ async def update_task_run_status(
127
+ self,
128
+ task_run_id: str,
129
+ status: TaskRunStatus,
130
+ message: str | None = None,
131
+ output: Any | None = None,
132
+ ) -> None:
133
+ try:
134
+ logger.info(f"Updating task run status for task run: {task_run_id}")
135
+
136
+ sanitized_output: str | None = None
137
+ if isinstance(output, dict):
138
+ sanitized_output = json.dumps(output)
139
+ elif isinstance(output, list):
140
+ sanitized_output = json.dumps(output)
141
+ elif isinstance(output, BaseModel):
142
+ sanitized_output = output.model_dump_json()
143
+ elif isinstance(output, str):
144
+ sanitized_output = output
145
+ else:
146
+ sanitized_output = str(output)
147
+
148
+ update = UpdateTaskRunStatusRequest(
149
+ status=status,
150
+ message=message,
151
+ output=sanitized_output,
152
+ )
153
+ response = await self._client.patch(
154
+ url=f"v1/task-runs/{task_run_id}/status",
155
+ json=update.model_dump(),
156
+ )
157
+ response.raise_for_status()
158
+ except httpx.HTTPStatusError as e:
159
+ raise PlatformServiceError(message=f"Failed to update task run status: {e}")
160
+
161
+ async def upsert_task_run_plan(
162
+ self,
163
+ task_run_id: str,
164
+ started_at: datetime,
165
+ plan: list[Subgoal],
166
+ ended_at: datetime | None = None,
167
+ plan_id: str | None = None,
168
+ ) -> TaskRunPlanResponse:
169
+ try:
170
+ logger.info(f"Upserting task run plan for task run: {task_run_id}")
171
+ ended, subgoals = self._to_api_subgoals(plan)
172
+ if not ended_at and ended:
173
+ ended_at = datetime.now(UTC)
174
+ update = UpsertTaskRunPlanRequest(
175
+ started_at=started_at,
176
+ subgoals=subgoals,
177
+ ended_at=ended_at,
178
+ )
179
+ if plan_id:
180
+ response = await self._client.put(
181
+ url=f"v1/task-runs/{task_run_id}/plans/{plan_id}",
182
+ json=update.model_dump(),
183
+ )
184
+ else:
185
+ response = await self._client.post(
186
+ url=f"v1/task-runs/{task_run_id}/plans",
187
+ json=update.model_dump(),
188
+ )
189
+ response.raise_for_status()
190
+ return TaskRunPlanResponse(**response.json())
191
+
192
+ except ValidationError as e:
193
+ raise PlatformServiceError(message=f"API response validation error: {e}")
194
+ except httpx.HTTPStatusError as e:
195
+ raise PlatformServiceError(message=f"Failed to upsert task run plan: {e}")
196
+
197
+ async def add_agent_thought(self, task_run_id: str, agent: str, thought: str) -> None:
198
+ try:
199
+ logger.info(f"Adding agent thought for task run: {task_run_id}")
200
+ update = UpsertTaskRunAgentThoughtRequest(
201
+ agent=agent,
202
+ content=thought,
203
+ timestamp=datetime.now(UTC),
204
+ )
205
+ response = await self._client.post(
206
+ url=f"v1/task-runs/{task_run_id}/agent-thoughts",
207
+ json=update.model_dump(),
208
+ )
209
+ response.raise_for_status()
210
+ except httpx.HTTPStatusError as e:
211
+ raise PlatformServiceError(message=f"Failed to add agent thought: {e}")
212
+
213
+ def _to_api_subgoals(self, subgoals: list[Subgoal]) -> tuple[bool, list[MobileUseSubgoal]]:
214
+ """
215
+ Returns a tuple of (plan_ended, subgoal_models)
216
+ """
217
+ subgoal_models: list[MobileUseSubgoal] = []
218
+ plan_ended = True
219
+ for subgoal in subgoals:
220
+ if subgoal.status != SubgoalStatus.SUCCESS:
221
+ plan_ended = False
222
+ subgoal_models.append(self._to_api_subgoal(subgoal))
223
+ return plan_ended, subgoal_models
224
+
225
+ def _to_api_subgoal(self, subgoal: Subgoal) -> MobileUseSubgoal:
226
+ state: SubgoalState = "pending"
227
+ match subgoal.status:
228
+ case SubgoalStatus.SUCCESS:
229
+ state = "completed"
230
+ case SubgoalStatus.FAILURE:
231
+ state = "failed"
232
+ case SubgoalStatus.PENDING:
233
+ state = "started"
234
+ case SubgoalStatus.NOT_STARTED:
235
+ state = "pending"
236
+ return MobileUseSubgoal(
237
+ name=subgoal.description,
238
+ state=state,
239
+ started_at=subgoal.started_at,
240
+ ended_at=subgoal.ended_at,
241
+ )
242
+
243
+ async def _create_task_run(
244
+ self,
245
+ task: TaskResponse,
246
+ profile: LLMProfileResponse,
247
+ ) -> TaskRunResponse:
248
+ try:
249
+ logger.info(f"Creating task run for task: {task.name}")
250
+ task_run = CreateTaskRunRequest(
251
+ task_id=task.id,
252
+ llm_profile_id=profile.id,
253
+ )
254
+ response = await self._client.post(url="v1/task-runs", json=task_run.model_dump())
255
+ response.raise_for_status()
256
+ task_run_data = response.json()
257
+ return TaskRunResponse(**task_run_data)
258
+ except ValidationError as e:
259
+ raise PlatformServiceError(message=f"API response validation error: {e}")
260
+ except httpx.HTTPStatusError as e:
261
+ raise PlatformServiceError(message=f"Failed to create task run: {e}")
262
+
263
+ async def _create_manual_task_run(
264
+ self,
265
+ manual_config: ManualTaskConfig,
266
+ profile: LLMProfileResponse,
267
+ ) -> TaskRunResponse:
268
+ """
269
+ Create an orphan task run from a manual task configuration.
270
+ This creates a task run without a pre-existing task using the /orphan endpoint.
271
+ """
272
+ try:
273
+ logger.info(f"Creating orphan task run with goal: {manual_config.goal}")
274
+
275
+ # Create an orphan task run directly
276
+ orphan_payload = {
277
+ "inputPrompt": manual_config.goal,
278
+ "outputDescription": manual_config.output_description,
279
+ "llmProfileId": profile.id,
280
+ }
281
+
282
+ response = await self._client.post(url="v1/task-runs/orphan", json=orphan_payload)
283
+ response.raise_for_status()
284
+ task_run_data = response.json()
285
+ return TaskRunResponse(**task_run_data)
286
+
287
+ except ValidationError as e:
288
+ raise PlatformServiceError(message=f"API response validation error: {e}")
289
+ except httpx.HTTPStatusError as e:
290
+ raise PlatformServiceError(message=f"Failed to create orphan task run: {e}")
291
+
292
+ async def _get_profile(self, profile_name: str) -> tuple[LLMProfileResponse, AgentProfile]:
293
+ try:
294
+ logger.info(f"Getting agent profile: {profile_name}")
295
+ response = await self._client.get(url=f"v1/llm-profiles/{profile_name}")
296
+ response.raise_for_status()
297
+ profile_data = response.json()
298
+ profile = LLMProfileResponse(**profile_data)
299
+ agent_profile = AgentProfile(
300
+ name=profile.name,
301
+ llm_config=LLMConfig(**profile.llms),
302
+ )
303
+ return profile, agent_profile
304
+ except ValidationError as e:
305
+ raise PlatformServiceError(message=f"API response validation error: {e}")
306
+ except httpx.HTTPStatusError as e:
307
+ raise PlatformServiceError(message=f"Failed to get agent profile: {e}")
@@ -1,29 +1,30 @@
1
1
  """Type definitions for the mobile-use SDK."""
2
2
 
3
3
  from minitap.mobile_use.sdk.types.agent import (
4
- ApiBaseUrl,
5
4
  AgentConfig,
5
+ ApiBaseUrl,
6
6
  DevicePlatform,
7
7
  ServerConfig,
8
8
  )
9
- from minitap.mobile_use.sdk.types.task import (
10
- AgentProfile,
11
- TaskRequest,
12
- TaskStatus,
13
- TaskResult,
14
- TaskRequestCommon,
15
- Task,
16
- )
17
9
  from minitap.mobile_use.sdk.types.exceptions import (
18
- AgentProfileNotFoundError,
19
- AgentTaskRequestError,
20
- DeviceNotFoundError,
21
- ServerStartupError,
22
10
  AgentError,
23
11
  AgentNotInitializedError,
12
+ AgentProfileNotFoundError,
13
+ AgentTaskRequestError,
24
14
  DeviceError,
15
+ DeviceNotFoundError,
25
16
  MobileUseError,
26
17
  ServerError,
18
+ ServerStartupError,
19
+ )
20
+ from minitap.mobile_use.sdk.types.task import (
21
+ AgentProfile,
22
+ ManualTaskConfig,
23
+ PlatformTaskRequest,
24
+ Task,
25
+ TaskRequest,
26
+ TaskRequestCommon,
27
+ TaskResult,
27
28
  )
28
29
 
29
30
  __all__ = [
@@ -33,7 +34,8 @@ __all__ = [
33
34
  "AgentProfile",
34
35
  "ServerConfig",
35
36
  "TaskRequest",
36
- "TaskStatus",
37
+ "ManualTaskConfig",
38
+ "PlatformTaskRequest",
37
39
  "TaskResult",
38
40
  "TaskRequestCommon",
39
41
  "Task",
@@ -102,3 +102,30 @@ class ExecutableNotFoundError(MobileUseError):
102
102
  if executable_name in install_instructions:
103
103
  message += f"\nTo install it, please visit: {install_instructions[executable_name]}"
104
104
  super().__init__(message)
105
+
106
+
107
+ class AgentInvalidApiKeyError(AgentTaskRequestError):
108
+ """Exception raise when the API key could not have been found"""
109
+
110
+ def __init__(self):
111
+ super().__init__(
112
+ "Minitap API key is incorrect. Visit https://platform.minitap.ai/api-keys "
113
+ "to get your API key."
114
+ )
115
+
116
+
117
+ class PlatformServiceUninitializedError(MobileUseError):
118
+ """Exception raised when a platform service call fails."""
119
+
120
+ def __init__(self):
121
+ super().__init__(
122
+ "Platform service is not initialized. "
123
+ "To use Minitap platform service, visit https://platform.minitap.ai.",
124
+ )
125
+
126
+
127
+ class PlatformServiceError(MobileUseError):
128
+ """Exception raised when a platform service call fails."""
129
+
130
+ def __init__(self, message="A platform service-related error occurred"):
131
+ super().__init__(message)
@@ -0,0 +1,127 @@
1
+ from datetime import datetime
2
+ from typing import Annotated, Any, Literal
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field, PlainSerializer
5
+ from pydantic.v1.utils import to_lower_camel
6
+
7
+ TaskRunStatus = Literal["pending", "running", "completed", "failed", "cancelled"]
8
+
9
+ IsoDatetime = Annotated[
10
+ datetime,
11
+ PlainSerializer(
12
+ func=lambda v: v.isoformat() if v else None,
13
+ return_type=str,
14
+ when_used="unless-none",
15
+ ),
16
+ ]
17
+
18
+
19
+ class BaseApiModel(BaseModel):
20
+ model_config = ConfigDict(
21
+ alias_generator=to_lower_camel,
22
+ populate_by_name=True,
23
+ str_strip_whitespace=True,
24
+ )
25
+
26
+
27
+ class LLMProfileResponse(BaseApiModel):
28
+ """Response model for LLM profile."""
29
+
30
+ id: str = Field(..., description="Profile ID")
31
+ name: str = Field(..., description="Profile name")
32
+ description: str | None = Field(None, description="Profile description")
33
+ llms: dict[str, Any] = Field(..., description="LLM configuration")
34
+ created_at: str = Field(..., description="Creation timestamp (ISO format)")
35
+ updated_at: str = Field(..., description="Last update timestamp (ISO format)")
36
+
37
+
38
+ class TaskOptionsResponse(BaseApiModel):
39
+ """Response model for task options."""
40
+
41
+ id: str = Field(..., description="Options ID")
42
+ enable_tracing: bool = Field(..., description="Whether tracing is enabled")
43
+ max_steps: int = Field(..., description="Maximum number of steps")
44
+ created_at: str = Field(..., description="Creation timestamp (ISO format)")
45
+ updated_at: str = Field(..., description="Last update timestamp (ISO format)")
46
+
47
+
48
+ class TaskResponse(BaseApiModel):
49
+ """Response model for task."""
50
+
51
+ id: str = Field(..., description="Task ID")
52
+ name: str = Field(..., description="Task name")
53
+ description: str | None = Field(None, description="Task description")
54
+ input_prompt: str = Field(..., description="Input prompt")
55
+ output_description: str | None = Field(None, description="Output description")
56
+ options: TaskOptionsResponse = Field(..., description="Task options")
57
+ created_at: str = Field(..., description="Creation timestamp (ISO format)")
58
+ updated_at: str = Field(..., description="Last update timestamp (ISO format)")
59
+
60
+
61
+ class CreateTaskRunRequest(BaseApiModel):
62
+ """Request model for creating a task run."""
63
+
64
+ task_id: str = Field(..., description="ID of the task to run")
65
+ llm_profile_id: str = Field(..., description="LLM profile ID to use")
66
+
67
+
68
+ class UpdateTaskRunStatusRequest(BaseApiModel):
69
+ """Request model for updating task run status."""
70
+
71
+ status: TaskRunStatus = Field(..., description="New status of the task run")
72
+ message: str | None = Field(None, description="Message associated with the status")
73
+ output: str | None = Field(None, description="Output of the task run")
74
+
75
+
76
+ class TaskRunResponse(BaseApiModel):
77
+ """Response model for a single task run."""
78
+
79
+ id: str = Field(..., description="Unique identifier for the task run")
80
+ task: TaskResponse | None = Field(
81
+ ..., description="ID of the task this run is for or None if manually created"
82
+ )
83
+ llm_profile: LLMProfileResponse = Field(..., description="LLM profile ID used for this run")
84
+ status: TaskRunStatus = Field(..., description="Current status of the task run")
85
+ input_prompt: str = Field(..., description="Input prompt for this task run")
86
+ output_description: str | None = Field(None, description="Description of expected output")
87
+ created_at: datetime = Field(..., description="When the task run was created")
88
+ started_at: datetime | None = Field(None, description="When the task run started")
89
+ finished_at: datetime | None = Field(None, description="When the task run finished")
90
+
91
+
92
+ SubgoalState = Literal["pending", "started", "completed", "failed"]
93
+
94
+
95
+ class MobileUseSubgoal(BaseModel):
96
+ """Upsert MobileUseSubgoal API model."""
97
+
98
+ name: str = Field(..., description="Name of the subgoal")
99
+ state: SubgoalState = Field(default="pending", description="Current state of the subgoal")
100
+ started_at: IsoDatetime | None = Field(default=None, description="When the subgoal started")
101
+ ended_at: IsoDatetime | None = Field(default=None, description="When the subgoal ended")
102
+
103
+
104
+ class UpsertTaskRunPlanRequest(BaseApiModel):
105
+ """Upsert MobileUseSubgoal API model."""
106
+
107
+ started_at: IsoDatetime = Field(..., description="When the plan started")
108
+ subgoals: list[MobileUseSubgoal] = Field(..., description="Subgoals of the plan")
109
+ ended_at: IsoDatetime | None = Field(
110
+ default=None,
111
+ description="When the plan ended (replanned or completed)",
112
+ )
113
+
114
+
115
+ class TaskRunPlanResponse(UpsertTaskRunPlanRequest):
116
+ """Response model for a task run plan."""
117
+
118
+ id: str = Field(..., description="Unique identifier for the task run plan")
119
+ task_run_id: str = Field(..., description="ID of the task run this plan is for")
120
+
121
+
122
+ class UpsertTaskRunAgentThoughtRequest(BaseApiModel):
123
+ """Upsert MobileUseAgentThought request model."""
124
+
125
+ agent: str = Field(..., description="Agent that produced the thought")
126
+ content: str = Field(..., description="Content of the thought")
127
+ timestamp: IsoDatetime = Field(..., description="Timestamp of the thought (UTC)")
@@ -2,8 +2,8 @@
2
2
  Task-related type definitions for the Mobile-use SDK.
3
3
  """
4
4
 
5
+ from collections.abc import Callable, Coroutine
5
6
  from datetime import datetime
6
- from enum import Enum
7
7
  from pathlib import Path
8
8
  from typing import Any, TypeVar, overload
9
9
 
@@ -12,6 +12,7 @@ from pydantic import BaseModel, Field
12
12
  from minitap.mobile_use.config import LLMConfig, get_default_llm_config
13
13
  from minitap.mobile_use.constants import RECURSION_LIMIT
14
14
  from minitap.mobile_use.context import DeviceContext
15
+ from minitap.mobile_use.sdk.types.platform import TaskRunResponse, TaskRunStatus
15
16
  from minitap.mobile_use.sdk.utils import load_llm_config_override
16
17
 
17
18
 
@@ -54,21 +55,11 @@ class AgentProfile(BaseModel):
54
55
  return f"Profile {self.name}:\n{self.llm_config}"
55
56
 
56
57
 
57
- class TaskStatus(str, Enum):
58
- """Task execution status enumeration."""
59
-
60
- PENDING = "PENDING"
61
- RUNNING = "RUNNING"
62
- COMPLETED = "COMPLETED"
63
- FAILED = "FAILED"
64
- CANCELLED = "CANCELLED"
65
-
66
-
67
58
  T = TypeVar("T", bound=BaseModel)
68
59
  TOutput = TypeVar("TOutput", bound=BaseModel | None)
69
60
 
70
61
 
71
- class TaskRequestCommon(BaseModel):
62
+ class TaskRequestBase(BaseModel):
72
63
  """
73
64
  Defines common parameters of a mobile automation task request.
74
65
  """
@@ -80,6 +71,14 @@ class TaskRequestCommon(BaseModel):
80
71
  thoughts_output_path: Path | None = None
81
72
 
82
73
 
74
+ class TaskRequestCommon(TaskRequestBase):
75
+ """
76
+ Defines common parameters for any task request.
77
+ """
78
+
79
+ max_steps: int = RECURSION_LIMIT
80
+
81
+
83
82
  class TaskRequest[TOutput](TaskRequestCommon):
84
83
  """
85
84
  Defines the format of a mobile automation task request.
@@ -103,6 +102,37 @@ class TaskRequest[TOutput](TaskRequestCommon):
103
102
  task_name: str | None = None
104
103
  output_description: str | None = None
105
104
  output_format: type[TOutput] | None = None
105
+ enable_remote_tracing: bool = False
106
+
107
+
108
+ class ManualTaskConfig(BaseModel):
109
+ """
110
+ Configuration for manually creating a task without fetching from the platform.
111
+
112
+ Attributes:
113
+ goal: Natural language description of the goal to achieve
114
+ output_description: Optional natural language description of expected output format
115
+ """
116
+
117
+ goal: str
118
+ output_description: str | None = None
119
+
120
+
121
+ class PlatformTaskRequest[TOutput](TaskRequestBase):
122
+ """
123
+ Minitap-specific task request for SDK usage via the gateway platform.
124
+
125
+ Attributes:
126
+ task: Either a task name to fetch from the platform, or a
127
+ ManualTaskConfig to create manually
128
+ profile: Optional profile name specified by the user on the platform
129
+ api_key: Optional API key to authenticate with the platform
130
+ (overrides MINITAP_API_KEY env variable)
131
+ """
132
+
133
+ task: str | ManualTaskConfig
134
+ profile: str | None = None
135
+ api_key: str | None = None
106
136
 
107
137
 
108
138
  class TaskResult(BaseModel):
@@ -156,22 +186,30 @@ class Task(BaseModel):
156
186
 
157
187
  id: str
158
188
  device: DeviceContext
159
- status: TaskStatus
189
+ status: TaskRunStatus
190
+ status_message: str | None = None
191
+ on_status_changed: Callable[[TaskRunStatus, str | None, Any | None], Coroutine] | None = None
160
192
  request: TaskRequest
161
193
  created_at: datetime
162
194
  ended_at: datetime | None = None
163
195
  result: TaskResult | None = None
164
196
 
165
- def finalize(
197
+ async def finalize(
166
198
  self,
167
199
  content: Any | None = None,
168
200
  state: dict | None = None,
169
201
  error: str | None = None,
170
202
  cancelled: bool = False,
171
203
  ):
172
- self.status = TaskStatus.COMPLETED if error is None else TaskStatus.FAILED
173
- if self.status == TaskStatus.FAILED and cancelled:
174
- self.status = TaskStatus.CANCELLED
204
+ new_status: TaskRunStatus = "completed" if error is None else "failed"
205
+ if new_status == "failed" and cancelled:
206
+ new_status = "cancelled"
207
+ message = "Task completed successfully"
208
+ if new_status == "failed":
209
+ message = "Task failed" + (f": {error}" if error else "")
210
+ elif new_status == "cancelled":
211
+ message = "Task cancelled" + (f": {error}" if error else "")
212
+ await self.set_status(status=new_status, message=message, output=content or error)
175
213
  self.ended_at = datetime.now()
176
214
 
177
215
  duration = self.ended_at - self.created_at
@@ -189,4 +227,27 @@ class Task(BaseModel):
189
227
  )
190
228
 
191
229
  def get_name(self) -> str:
230
+ if isinstance(self.request, PlatformTaskRequest):
231
+ if isinstance(self.request.task, str):
232
+ return self.request.task
233
+ else:
234
+ # ManualTaskConfig - use first 50 chars of goal
235
+ return f"Manual: {self.request.task.goal[:50]}"
192
236
  return self.request.task_name or self.id
237
+
238
+ async def set_status(
239
+ self,
240
+ status: TaskRunStatus,
241
+ message: str | None = None,
242
+ output: Any | None = None,
243
+ ):
244
+ self.status = status
245
+ self.status_message = message
246
+ if self.on_status_changed:
247
+ await self.on_status_changed(status, message, output)
248
+
249
+
250
+ class PlatformTaskInfo(BaseModel):
251
+ task_request: TaskRequest = Field(..., description="Task request")
252
+ llm_profile: AgentProfile = Field(..., description="LLM profile")
253
+ task_run: TaskRunResponse = Field(..., description="Task run instance on the platform")
@@ -38,7 +38,7 @@ class DeviceHardwareBridge:
38
38
  try:
39
39
  creation_flags = 0
40
40
  if hasattr(subprocess, "CREATE_NO_WINDOW"):
41
- creation_flags = subprocess.CREATE_NO_WINDOW
41
+ creation_flags = subprocess.CREATE_NO_WINDOW # pyright: ignore[reportAttributeAccessIssue]
42
42
 
43
43
  maestro_platform = "android" if self.platform == DevicePlatform.ANDROID else "ios"
44
44
  cmd = ["maestro", "--device", self.device_id, "--platform", maestro_platform]