minitap-mobile-use 2.3.0__py3-none-any.whl → 2.4.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 (54) 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_minimal_example.py +46 -0
  21. minitap/mobile_use/sdk/services/platform.py +244 -0
  22. minitap/mobile_use/sdk/types/__init__.py +14 -14
  23. minitap/mobile_use/sdk/types/exceptions.py +27 -0
  24. minitap/mobile_use/sdk/types/platform.py +125 -0
  25. minitap/mobile_use/sdk/types/task.py +60 -17
  26. minitap/mobile_use/servers/device_hardware_bridge.py +1 -1
  27. minitap/mobile_use/servers/stop_servers.py +11 -12
  28. minitap/mobile_use/services/llm.py +89 -5
  29. minitap/mobile_use/tools/index.py +0 -6
  30. minitap/mobile_use/tools/mobile/back.py +3 -3
  31. minitap/mobile_use/tools/mobile/clear_text.py +24 -43
  32. minitap/mobile_use/tools/mobile/erase_one_char.py +5 -4
  33. minitap/mobile_use/tools/mobile/glimpse_screen.py +11 -7
  34. minitap/mobile_use/tools/mobile/input_text.py +21 -51
  35. minitap/mobile_use/tools/mobile/launch_app.py +54 -22
  36. minitap/mobile_use/tools/mobile/long_press_on.py +15 -8
  37. minitap/mobile_use/tools/mobile/open_link.py +15 -8
  38. minitap/mobile_use/tools/mobile/press_key.py +15 -8
  39. minitap/mobile_use/tools/mobile/stop_app.py +14 -8
  40. minitap/mobile_use/tools/mobile/swipe.py +11 -5
  41. minitap/mobile_use/tools/mobile/tap.py +103 -21
  42. minitap/mobile_use/tools/mobile/wait_for_animation_to_end.py +3 -3
  43. minitap/mobile_use/tools/test_utils.py +104 -78
  44. minitap/mobile_use/tools/types.py +35 -0
  45. minitap/mobile_use/tools/utils.py +51 -48
  46. minitap/mobile_use/utils/recorder.py +1 -1
  47. minitap/mobile_use/utils/ui_hierarchy.py +9 -2
  48. {minitap_mobile_use-2.3.0.dist-info → minitap_mobile_use-2.4.0.dist-info}/METADATA +3 -1
  49. {minitap_mobile_use-2.3.0.dist-info → minitap_mobile_use-2.4.0.dist-info}/RECORD +51 -50
  50. minitap/mobile_use/tools/mobile/copy_text_from.py +0 -75
  51. minitap/mobile_use/tools/mobile/find_packages.py +0 -69
  52. minitap/mobile_use/tools/mobile/paste_text.py +0 -88
  53. {minitap_mobile_use-2.3.0.dist-info → minitap_mobile_use-2.4.0.dist-info}/WHEEL +0 -0
  54. {minitap_mobile_use-2.3.0.dist-info → minitap_mobile_use-2.4.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,244 @@
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
+ PlatformTaskInfo,
27
+ PlatformTaskRequest,
28
+ TaskRequest,
29
+ )
30
+ from minitap.mobile_use.utils.logger import get_logger
31
+
32
+ logger = get_logger(__name__)
33
+
34
+ DEFAULT_PROFILE = "default"
35
+
36
+
37
+ class PlatformService:
38
+ def __init__(self, api_key: str | None = None):
39
+ self._base_url = settings.MINITAP_API_BASE_URL
40
+
41
+ if api_key:
42
+ self._api_key = api_key
43
+ elif settings.MINITAP_API_KEY:
44
+ self._api_key = settings.MINITAP_API_KEY.get_secret_value()
45
+ else:
46
+ raise PlatformServiceError(
47
+ message="Please provide an API key or set MINITAP_API_KEY environment variable.",
48
+ )
49
+
50
+ self._timeout = httpx.Timeout(timeout=120)
51
+ self._client = httpx.AsyncClient(
52
+ base_url=f"{self._base_url}/api",
53
+ timeout=self._timeout,
54
+ headers={
55
+ "Authorization": f"Bearer {self._api_key}",
56
+ "Content-Type": "application/json",
57
+ },
58
+ )
59
+
60
+ async def create_task_run(self, request: PlatformTaskRequest) -> PlatformTaskInfo:
61
+ try:
62
+ logger.info(f"Getting task: {request.task}")
63
+ response = await self._client.get(url=f"v1/tasks/{request.task}")
64
+ response.raise_for_status()
65
+ task_data = response.json()
66
+ task = TaskResponse(**task_data)
67
+ profile, agent_profile = await self._get_profile(
68
+ profile_name=request.profile or DEFAULT_PROFILE,
69
+ )
70
+ task_request = TaskRequest(
71
+ # Remote configuration
72
+ max_steps=task.options.max_steps,
73
+ goal=task.input_prompt,
74
+ output_description=task.output_description,
75
+ enable_remote_tracing=task.options.enable_tracing,
76
+ profile=profile.name,
77
+ # Local configuration
78
+ record_trace=request.record_trace,
79
+ trace_path=request.trace_path,
80
+ llm_output_path=request.llm_output_path,
81
+ thoughts_output_path=request.thoughts_output_path,
82
+ )
83
+ task_run = await self._create_task_run(task=task, profile=profile)
84
+ return PlatformTaskInfo(
85
+ task_request=task_request,
86
+ llm_profile=agent_profile,
87
+ task_run=task_run,
88
+ )
89
+ except httpx.HTTPStatusError as e:
90
+ raise PlatformServiceError(message=f"Failed to get task: {e}")
91
+
92
+ async def update_task_run_status(
93
+ self,
94
+ task_run_id: str,
95
+ status: TaskRunStatus,
96
+ message: str | None = None,
97
+ output: Any | None = None,
98
+ ) -> None:
99
+ try:
100
+ logger.info(f"Updating task run status for task run: {task_run_id}")
101
+
102
+ sanitized_output: str | None = None
103
+ if isinstance(output, dict):
104
+ sanitized_output = json.dumps(output)
105
+ elif isinstance(output, list):
106
+ sanitized_output = json.dumps(output)
107
+ elif isinstance(output, BaseModel):
108
+ sanitized_output = output.model_dump_json()
109
+ elif isinstance(output, str):
110
+ sanitized_output = output
111
+ else:
112
+ sanitized_output = str(output)
113
+
114
+ update = UpdateTaskRunStatusRequest(
115
+ status=status,
116
+ message=message,
117
+ output=sanitized_output,
118
+ )
119
+ response = await self._client.patch(
120
+ url=f"v1/task-runs/{task_run_id}/status",
121
+ json=update.model_dump(),
122
+ )
123
+ response.raise_for_status()
124
+ except httpx.HTTPStatusError as e:
125
+ raise PlatformServiceError(message=f"Failed to update task run status: {e}")
126
+
127
+ async def upsert_task_run_plan(
128
+ self,
129
+ task_run_id: str,
130
+ started_at: datetime,
131
+ plan: list[Subgoal],
132
+ ended_at: datetime | None = None,
133
+ plan_id: str | None = None,
134
+ ) -> TaskRunPlanResponse:
135
+ try:
136
+ logger.info(f"Upserting task run plan for task run: {task_run_id}")
137
+ ended, subgoals = self._to_api_subgoals(plan)
138
+ if not ended_at and ended:
139
+ ended_at = datetime.now(UTC)
140
+ update = UpsertTaskRunPlanRequest(
141
+ started_at=started_at,
142
+ subgoals=subgoals,
143
+ ended_at=ended_at,
144
+ )
145
+ if plan_id:
146
+ response = await self._client.put(
147
+ url=f"v1/task-runs/{task_run_id}/plans/{plan_id}",
148
+ json=update.model_dump(),
149
+ )
150
+ else:
151
+ response = await self._client.post(
152
+ url=f"v1/task-runs/{task_run_id}/plans",
153
+ json=update.model_dump(),
154
+ )
155
+ response.raise_for_status()
156
+ return TaskRunPlanResponse(**response.json())
157
+
158
+ except ValidationError as e:
159
+ raise PlatformServiceError(message=f"API response validation error: {e}")
160
+ except httpx.HTTPStatusError as e:
161
+ raise PlatformServiceError(message=f"Failed to upsert task run plan: {e}")
162
+
163
+ async def add_agent_thought(self, task_run_id: str, agent: str, thought: str) -> None:
164
+ try:
165
+ logger.info(f"Adding agent thought for task run: {task_run_id}")
166
+ update = UpsertTaskRunAgentThoughtRequest(
167
+ agent=agent,
168
+ content=thought,
169
+ timestamp=datetime.now(UTC),
170
+ )
171
+ response = await self._client.post(
172
+ url=f"v1/task-runs/{task_run_id}/agent-thoughts",
173
+ json=update.model_dump(),
174
+ )
175
+ response.raise_for_status()
176
+ except httpx.HTTPStatusError as e:
177
+ raise PlatformServiceError(message=f"Failed to add agent thought: {e}")
178
+
179
+ def _to_api_subgoals(self, subgoals: list[Subgoal]) -> tuple[bool, list[MobileUseSubgoal]]:
180
+ """
181
+ Returns a tuple of (plan_ended, subgoal_models)
182
+ """
183
+ subgoal_models: list[MobileUseSubgoal] = []
184
+ plan_ended = True
185
+ for subgoal in subgoals:
186
+ if subgoal.status != SubgoalStatus.SUCCESS:
187
+ plan_ended = False
188
+ subgoal_models.append(self._to_api_subgoal(subgoal))
189
+ return plan_ended, subgoal_models
190
+
191
+ def _to_api_subgoal(self, subgoal: Subgoal) -> MobileUseSubgoal:
192
+ state: SubgoalState = "pending"
193
+ match subgoal.status:
194
+ case SubgoalStatus.SUCCESS:
195
+ state = "completed"
196
+ case SubgoalStatus.FAILURE:
197
+ state = "failed"
198
+ case SubgoalStatus.PENDING:
199
+ state = "started"
200
+ case SubgoalStatus.NOT_STARTED:
201
+ state = "pending"
202
+ return MobileUseSubgoal(
203
+ name=subgoal.description,
204
+ state=state,
205
+ started_at=subgoal.started_at,
206
+ ended_at=subgoal.ended_at,
207
+ )
208
+
209
+ async def _create_task_run(
210
+ self,
211
+ task: TaskResponse,
212
+ profile: LLMProfileResponse,
213
+ ) -> TaskRunResponse:
214
+ try:
215
+ logger.info(f"Creating task run for task: {task.name}")
216
+ task_run = CreateTaskRunRequest(
217
+ task_id=task.id,
218
+ llm_profile_id=profile.id,
219
+ )
220
+ response = await self._client.post(url="v1/task-runs", json=task_run.model_dump())
221
+ response.raise_for_status()
222
+ task_run_data = response.json()
223
+ return TaskRunResponse(**task_run_data)
224
+ except ValidationError as e:
225
+ raise PlatformServiceError(message=f"API response validation error: {e}")
226
+ except httpx.HTTPStatusError as e:
227
+ raise PlatformServiceError(message=f"Failed to create task run: {e}")
228
+
229
+ async def _get_profile(self, profile_name: str) -> tuple[LLMProfileResponse, AgentProfile]:
230
+ try:
231
+ logger.info(f"Getting agent profile: {profile_name}")
232
+ response = await self._client.get(url=f"v1/llm-profiles/{profile_name}")
233
+ response.raise_for_status()
234
+ profile_data = response.json()
235
+ profile = LLMProfileResponse(**profile_data)
236
+ agent_profile = AgentProfile(
237
+ name=profile.name,
238
+ llm_config=LLMConfig(**profile.llms),
239
+ )
240
+ return profile, agent_profile
241
+ except ValidationError as e:
242
+ raise PlatformServiceError(message=f"API response validation error: {e}")
243
+ except httpx.HTTPStatusError as e:
244
+ raise PlatformServiceError(message=f"Failed to get agent profile: {e}")
@@ -1,29 +1,29 @@
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
+ PlatformTaskRequest,
23
+ Task,
24
+ TaskRequest,
25
+ TaskRequestCommon,
26
+ TaskResult,
27
27
  )
28
28
 
29
29
  __all__ = [
@@ -33,7 +33,7 @@ __all__ = [
33
33
  "AgentProfile",
34
34
  "ServerConfig",
35
35
  "TaskRequest",
36
- "TaskStatus",
36
+ "PlatformTaskRequest",
37
37
  "TaskResult",
38
38
  "TaskRequestCommon",
39
39
  "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,125 @@
1
+ from typing import Annotated, Any, Literal
2
+ from pydantic import BaseModel, ConfigDict, Field, PlainSerializer
3
+ from pydantic.v1.utils import to_lower_camel
4
+ from datetime import datetime
5
+
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 = Field(..., description="ID of the task this run is for")
81
+ llm_profile: LLMProfileResponse = Field(..., description="LLM profile ID used for this run")
82
+ status: TaskRunStatus = Field(..., description="Current status of the task run")
83
+ input_prompt: str = Field(..., description="Input prompt for this task run")
84
+ output_description: str | None = Field(None, description="Description of expected output")
85
+ created_at: datetime = Field(..., description="When the task run was created")
86
+ started_at: datetime | None = Field(None, description="When the task run started")
87
+ finished_at: datetime | None = Field(None, description="When the task run finished")
88
+
89
+
90
+ SubgoalState = Literal["pending", "started", "completed", "failed"]
91
+
92
+
93
+ class MobileUseSubgoal(BaseModel):
94
+ """Upsert MobileUseSubgoal API model."""
95
+
96
+ name: str = Field(..., description="Name of the subgoal")
97
+ state: SubgoalState = Field(default="pending", description="Current state of the subgoal")
98
+ started_at: IsoDatetime | None = Field(default=None, description="When the subgoal started")
99
+ ended_at: IsoDatetime | None = Field(default=None, description="When the subgoal ended")
100
+
101
+
102
+ class UpsertTaskRunPlanRequest(BaseApiModel):
103
+ """Upsert MobileUseSubgoal API model."""
104
+
105
+ started_at: IsoDatetime = Field(..., description="When the plan started")
106
+ subgoals: list[MobileUseSubgoal] = Field(..., description="Subgoals of the plan")
107
+ ended_at: IsoDatetime | None = Field(
108
+ default=None,
109
+ description="When the plan ended (replanned or completed)",
110
+ )
111
+
112
+
113
+ class TaskRunPlanResponse(UpsertTaskRunPlanRequest):
114
+ """Response model for a task run plan."""
115
+
116
+ id: str = Field(..., description="Unique identifier for the task run plan")
117
+ task_run_id: str = Field(..., description="ID of the task run this plan is for")
118
+
119
+
120
+ class UpsertTaskRunAgentThoughtRequest(BaseApiModel):
121
+ """Upsert MobileUseAgentThought request model."""
122
+
123
+ agent: str = Field(..., description="Agent that produced the thought")
124
+ content: str = Field(..., description="Content of the thought")
125
+ 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,23 @@ 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 PlatformTaskRequest[TOutput](TaskRequestBase):
109
+ """
110
+ Minitap-specific task request for SDK usage via the gateway platform.
111
+
112
+ Attributes:
113
+ task: Required task name specified by the user on the platform
114
+ profile: Optional profile name specified by the user on the platform
115
+ api_key: Optional API key to authenticate with the platform
116
+ (overrides MINITAP_API_KEY env variable)
117
+ """
118
+
119
+ task: str
120
+ profile: str | None = None
121
+ api_key: str | None = None
106
122
 
107
123
 
108
124
  class TaskResult(BaseModel):
@@ -156,22 +172,30 @@ class Task(BaseModel):
156
172
 
157
173
  id: str
158
174
  device: DeviceContext
159
- status: TaskStatus
175
+ status: TaskRunStatus
176
+ status_message: str | None = None
177
+ on_status_changed: Callable[[TaskRunStatus, str | None, Any | None], Coroutine] | None = None
160
178
  request: TaskRequest
161
179
  created_at: datetime
162
180
  ended_at: datetime | None = None
163
181
  result: TaskResult | None = None
164
182
 
165
- def finalize(
183
+ async def finalize(
166
184
  self,
167
185
  content: Any | None = None,
168
186
  state: dict | None = None,
169
187
  error: str | None = None,
170
188
  cancelled: bool = False,
171
189
  ):
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
190
+ new_status: TaskRunStatus = "completed" if error is None else "failed"
191
+ if new_status == "failed" and cancelled:
192
+ new_status = "cancelled"
193
+ message = "Task completed successfully"
194
+ if new_status == "failed":
195
+ message = "Task failed" + (f": {error}" if error else "")
196
+ elif new_status == "cancelled":
197
+ message = "Task cancelled" + (f": {error}" if error else "")
198
+ await self.set_status(status=new_status, message=message, output=content or error)
175
199
  self.ended_at = datetime.now()
176
200
 
177
201
  duration = self.ended_at - self.created_at
@@ -189,4 +213,23 @@ class Task(BaseModel):
189
213
  )
190
214
 
191
215
  def get_name(self) -> str:
216
+ if isinstance(self.request, PlatformTaskRequest):
217
+ return self.request.task
192
218
  return self.request.task_name or self.id
219
+
220
+ async def set_status(
221
+ self,
222
+ status: TaskRunStatus,
223
+ message: str | None = None,
224
+ output: Any | None = None,
225
+ ):
226
+ self.status = status
227
+ self.status_message = message
228
+ if self.on_status_changed:
229
+ await self.on_status_changed(status, message, output)
230
+
231
+
232
+ class PlatformTaskInfo(BaseModel):
233
+ task_request: TaskRequest = Field(..., description="Task request")
234
+ llm_profile: AgentProfile = Field(..., description="LLM profile")
235
+ 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]
@@ -70,18 +70,17 @@ def stop_process_gracefully(process: psutil.Process, timeout: int = 5) -> bool:
70
70
  return False
71
71
 
72
72
 
73
- def check_service_health(port: int, service_name: str) -> bool:
73
+ def check_service_running(port: int, service_name: str) -> bool:
74
74
  try:
75
75
  if port == server_settings.DEVICE_SCREEN_API_PORT:
76
- response = requests.get(f"http://localhost:{port}/health", timeout=2)
76
+ requests.get(f"http://localhost:{port}/health", timeout=2)
77
77
  elif port == DEVICE_HARDWARE_BRIDGE_PORT:
78
- response = requests.get(f"http://localhost:{port}/api/banner-message", timeout=2)
78
+ requests.get(f"http://localhost:{port}/api/banner-message", timeout=2)
79
79
  else:
80
80
  return False
81
81
 
82
- if response.status_code == 200:
83
- logger.debug(f"{service_name} is still responding on port {port}")
84
- return True
82
+ logger.debug(f"{service_name} is still responding on port {port}")
83
+ return True
85
84
  except requests.exceptions.RequestException:
86
85
  pass
87
86
 
@@ -92,7 +91,7 @@ def stop_device_screen_api() -> bool:
92
91
  logger.info("Stopping Device Screen API...")
93
92
  api_port = server_settings.DEVICE_SCREEN_API_PORT
94
93
 
95
- if not check_service_health(api_port, "Device Screen API"):
94
+ if not check_service_running(api_port, "Device Screen API"):
96
95
  logger.success("Device Screen API is not running")
97
96
  return True
98
97
 
@@ -109,7 +108,7 @@ def stop_device_screen_api() -> bool:
109
108
  logger.warning("No Device Screen API processes found, but service is still responding")
110
109
  # Still try to verify if service actually stops
111
110
  time.sleep(1)
112
- if not check_service_health(api_port, "Device Screen API"):
111
+ if not check_service_running(api_port, "Device Screen API"):
113
112
  logger.success("Device Screen API stopped successfully (was orphaned)")
114
113
  return True
115
114
  return False
@@ -120,7 +119,7 @@ def stop_device_screen_api() -> bool:
120
119
 
121
120
  # Verify service is stopped
122
121
  time.sleep(1)
123
- if check_service_health(api_port, "Device Screen API"):
122
+ if check_service_running(api_port, "Device Screen API"):
124
123
  logger.error("Device Screen API is still running after stop attempt")
125
124
  return False
126
125
 
@@ -131,7 +130,7 @@ def stop_device_screen_api() -> bool:
131
130
  def stop_device_hardware_bridge() -> bool:
132
131
  logger.info("Stopping Device Hardware Bridge...")
133
132
 
134
- if not check_service_health(DEVICE_HARDWARE_BRIDGE_PORT, "Maestro Studio"):
133
+ if not check_service_running(DEVICE_HARDWARE_BRIDGE_PORT, "Maestro Studio"):
135
134
  logger.success("Device Hardware Bridge is not running")
136
135
  return True
137
136
 
@@ -145,7 +144,7 @@ def stop_device_hardware_bridge() -> bool:
145
144
  logger.warning("No Device Hardware Bridge processes found, but service is still responding")
146
145
  # Still try to verify if service actually stops
147
146
  time.sleep(1)
148
- if not check_service_health(DEVICE_HARDWARE_BRIDGE_PORT, "Maestro Studio"):
147
+ if not check_service_running(DEVICE_HARDWARE_BRIDGE_PORT, "Maestro Studio"):
149
148
  logger.success("Device Hardware Bridge stopped successfully (was orphaned)")
150
149
  return True
151
150
  return False
@@ -154,7 +153,7 @@ def stop_device_hardware_bridge() -> bool:
154
153
  stop_process_gracefully(proc)
155
154
 
156
155
  time.sleep(1)
157
- if check_service_health(DEVICE_HARDWARE_BRIDGE_PORT, "Maestro Studio"):
156
+ if check_service_running(DEVICE_HARDWARE_BRIDGE_PORT, "Maestro Studio"):
158
157
  logger.error("Device Hardware Bridge is still running after stop attempt")
159
158
  return False
160
159