android-midscene-automation 0.2.4 → 1.1.1

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 (525) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +159 -29
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +4 -2
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +12 -0
  507. package/src/App.vue +10 -5
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +340 -121
  525. package/src/types.ts +1 -1
@@ -0,0 +1,1060 @@
1
+ # Copyright 2025-2026 Minitap, Inc.
2
+ # Modifications Copyright 2026 Google LLC
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Originally from mobile-use (https://github.com/minitap-ai/mobile-use).
17
+ # See third_party/mobile_use/METADATA for the upstream source and local modifications.
18
+
19
+ """Base SDK agent: device/client setup, task creation and the task run loop.
20
+
21
+ Artemis extends this class in ``artemis.sdk.agent.Agent`` with tracing setup, device
22
+ environment preparation, cancellation watching and UI hierarchy backend reporting.
23
+ """
24
+
25
+ import asyncio
26
+ import os
27
+
28
+ try:
29
+ from datetime import UTC, datetime
30
+ except ImportError:
31
+ from datetime import datetime
32
+
33
+ UTC = UTC
34
+ import shutil
35
+ import threading
36
+ import uuid
37
+ from pathlib import Path
38
+ from platform import system
39
+ from shutil import which
40
+ from types import NoneType
41
+ from typing import Any, TypeVar, overload
42
+
43
+ from adbutils import AdbClient
44
+ from PIL import Image
45
+ from pydantic import BaseModel
46
+
47
+ from artemis.agents.flash.runner import FlashRunner
48
+ from artemis.agents.outputter.outputter import outputter
49
+ from artemis.clients.screen_client_factory import (
50
+ ScreenClient,
51
+ create_screen_client,
52
+ )
53
+ from artemis.config import (
54
+ OutputConfig,
55
+ record_events,
56
+ )
57
+ from artemis.constants import RECURSION_LIMIT
58
+ from artemis.context import (
59
+ ArtemisContext,
60
+ DeviceContext,
61
+ DevicePlatform,
62
+ ExecutionSetup,
63
+ )
64
+ from artemis.controllers.controller_factory import get_controller
65
+ from artemis.data_engine.trace import DataEngineCallbackHandler
66
+ from artemis.graph.graph import get_graph
67
+ from artemis.graph.state import State
68
+ from artemis.runtime import DeviceExecutionLock
69
+ from artemis.sdk.run_outcome import attach_test_summary, resolve_trace_suffix
70
+ from artemis.sdk.types.agent import AgentConfig
71
+ from artemis.utils.startup_progress import publish_startup_progress
72
+ from third_party.mobile_use.controllers.platform_specific_commands_controller import (
73
+ get_first_device,
74
+ )
75
+ from third_party.mobile_use.sdk.builders.task_request_builder import TaskRequestBuilder
76
+ from third_party.mobile_use.sdk.types.exceptions import (
77
+ AgentError,
78
+ AgentNotInitializedError,
79
+ AgentProfileNotFoundError,
80
+ AgentTaskRequestError,
81
+ DeviceNotFoundError,
82
+ ExecutableNotFoundError,
83
+ )
84
+ from third_party.mobile_use.sdk.types.task import (
85
+ AgentProfile,
86
+ Task,
87
+ TaskRequest,
88
+ )
89
+ from third_party.mobile_use.utils.app_launch_utils import _handle_initial_app_launch
90
+ from third_party.mobile_use.utils.logger import get_logger
91
+ from third_party.mobile_use.utils.media import (
92
+ create_gif_from_trace_folder,
93
+ create_steps_json_from_trace_folder,
94
+ remove_images_from_trace_folder,
95
+ remove_steps_json_from_trace_folder,
96
+ )
97
+
98
+ logger = get_logger(__name__)
99
+
100
+ TOutput = TypeVar("TOutput", bound=BaseModel | None)
101
+
102
+
103
+ class AgentBase:
104
+ _config: AgentConfig
105
+ _tasks: list[Task] = []
106
+ _tmp_traces_dir: Path
107
+ _initialized: bool = False
108
+ _device_context: DeviceContext
109
+ _adb_client: AdbClient | None
110
+ _ui_adb_client: ScreenClient | Any | None
111
+ _current_task: asyncio.Task | None = None
112
+ _task_lock: asyncio.Lock
113
+ _cloud_mobile_id: str | None = None
114
+
115
+ async def init(
116
+ self,
117
+ api_key: str | None = None,
118
+ device_id: str | None = None,
119
+ device_serial: str | None = None,
120
+ retry_count: int = 5,
121
+ retry_wait_seconds: int = 5,
122
+ ):
123
+ target_dev = device_serial or device_id
124
+ if target_dev:
125
+ self._config = self._config.model_copy(
126
+ update={"device_id": target_dev, "device_platform": DevicePlatform.ANDROID}
127
+ )
128
+
129
+ return await self._init_internal(
130
+ api_key=api_key,
131
+ retry_count=retry_count,
132
+ retry_wait_seconds=retry_wait_seconds,
133
+ )
134
+
135
+ async def _init_internal(
136
+ self,
137
+ api_key: str | None = None,
138
+ retry_count: int = 5,
139
+ retry_wait_seconds: int = 5,
140
+ ):
141
+
142
+ if os.environ.get("ARTEMIS_CLOUD_MODE") != "1" and not which("adb"):
143
+ raise ExecutableNotFoundError("adb")
144
+
145
+ if self._initialized:
146
+ logger.warning("Agent is already initialized. Skipping...")
147
+ return True
148
+
149
+ # Get first available device ID
150
+ if os.environ.get("ARTEMIS_CLOUD_MODE") == "1":
151
+ device_id = os.environ.get("ADB_DEVICE_SERIAL", "cloud_device")
152
+ platform = DevicePlatform.ANDROID
153
+ elif not self._config.device_id or not self._config.device_platform:
154
+ device_id, platform, _ = get_first_device(logger=logger)
155
+ else:
156
+ device_id, platform = (
157
+ self._config.device_id,
158
+ self._config.device_platform,
159
+ )
160
+
161
+ if not device_id or not platform:
162
+ error_msg = "No device found. Exiting."
163
+ logger.error(error_msg)
164
+ raise DeviceNotFoundError(error_msg)
165
+
166
+ # Initialize clients
167
+ publish_startup_progress(
168
+ "device_check", "Checking the Android device", session_id=self._session_id
169
+ )
170
+ if os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
171
+ self._init_clients(
172
+ device_id=device_id,
173
+ platform=platform,
174
+ )
175
+ else:
176
+ # Cloud mode is pinned to UIAutomator2 through the gateway; the
177
+ # hierarchy backend switch only applies to locally attached devices.
178
+ from cloud_service.virtualization import RemoteAdbClient, RemoteUIAutomatorClient
179
+
180
+ self._adb_client = RemoteAdbClient()
181
+ self._ui_adb_client = RemoteUIAutomatorClient(
182
+ device_id=device_id,
183
+ adb_client=self._adb_client,
184
+ )
185
+
186
+ self._device_context = await self._get_device_context(
187
+ device_id=device_id, platform=platform
188
+ )
189
+ logger.info(self._device_context.to_str())
190
+ publish_startup_progress(
191
+ "device_ready", "Android device connected", session_id=self._session_id
192
+ )
193
+
194
+ # Asynchronously pre-warm LLM connection pools in the background
195
+ asyncio.create_task(self._prewarm_llm_connections(api_key))
196
+
197
+ logger.info("✅ Artemis agent initialized.")
198
+ self._initialized = True
199
+
200
+ return True
201
+
202
+ async def install_apk(self, apk_path: str | Path) -> None:
203
+ """Install an APK on the connected device.
204
+
205
+ For cloud mobiles, the APK must be x86_64 compatible.
206
+
207
+ Args:
208
+ apk_path: Path to the local APK file to install
209
+
210
+ Raises:
211
+ AgentNotInitializedError: If the agent is not initialized
212
+ AgentError: If attempting to install on non-Android device or ADB
213
+ operations fail
214
+ FileNotFoundError: If the APK file doesn't exist
215
+ CloudMobileServiceUninitializedError: If cloud service is
216
+ unavailable
217
+ """
218
+ await self._install_apk_internal(apk_path)
219
+
220
+ async def _install_apk_internal(self, apk_path: str | Path) -> None:
221
+ if isinstance(apk_path, str):
222
+ apk_path = Path(apk_path)
223
+
224
+ if not apk_path.exists():
225
+ raise FileNotFoundError(f"APK file not found: {apk_path}")
226
+
227
+ if not self._initialized:
228
+ raise AgentNotInitializedError()
229
+
230
+ device_id = self._device_context.device_id
231
+ logger.info(f"Installing APK on Android device '{device_id}'")
232
+ if not self._adb_client:
233
+ raise AgentError("ADB client not initialized")
234
+
235
+ device = self._adb_client.device(serial=device_id)
236
+ await asyncio.to_thread(device.install, apk_path)
237
+ logger.info(f"APK installed successfully on Android device '{device_id}'")
238
+
239
+ async def install_app(self, app_path: str | Path) -> str | None:
240
+ """Install an app on the connected device.
241
+
242
+ For Android: Installs an APK file using ADB.
243
+
244
+ Args:
245
+ app_path: Path to the app to install (Android APK file).
246
+
247
+ Returns:
248
+ None.
249
+
250
+ Raises:
251
+ AgentNotInitializedError: If the agent is not initialized
252
+ AgentError: If installation fails or platform is unsupported
253
+ FileNotFoundError: If the app file/folder doesn't exist
254
+ """
255
+ return await self._install_app_internal(app_path)
256
+
257
+ async def _install_app_internal(self, app_path: str | Path) -> str | None:
258
+ if isinstance(app_path, str):
259
+ app_path = Path(app_path)
260
+
261
+ if not app_path.exists():
262
+ raise FileNotFoundError(f"App not found: {app_path}")
263
+
264
+ if not self._initialized:
265
+ raise AgentNotInitializedError()
266
+
267
+ await self._install_apk_internal(app_path)
268
+ return None
269
+
270
+ def new_task(self, goal: str):
271
+ """Create a new task request builder.
272
+
273
+ Args:
274
+ goal: Natural language description of what to accomplish
275
+
276
+ Returns:
277
+ TaskRequestBuilder that can be configured with:
278
+ - .with_output_format() for structured output
279
+ - .with_output_description() for output description
280
+ - .with_locked_app_package() to restrict execution to a specific app
281
+ - .using_profile() to specify an LLM profile
282
+ - .with_max_steps() to set maximum execution steps
283
+ - .with_trace_recording() to enable trace recording
284
+ - .with_name() to set a custom task name
285
+ """
286
+ return TaskRequestBuilder[None].from_common(
287
+ goal=goal,
288
+ common=self._config.task_request_defaults,
289
+ )
290
+
291
+ @overload
292
+ async def run_task(
293
+ self,
294
+ *,
295
+ goal: str,
296
+ output: type[TOutput],
297
+ profile: str | AgentProfile | None = None,
298
+ name: str | None = None,
299
+ locked_app_package: str | None = None,
300
+ app_path: str | Path | None = None,
301
+ ) -> TOutput | None: ...
302
+
303
+ @overload
304
+ async def run_task(
305
+ self,
306
+ *,
307
+ goal: str,
308
+ output: str,
309
+ profile: str | AgentProfile | None = None,
310
+ name: str | None = None,
311
+ locked_app_package: str | None = None,
312
+ app_path: str | Path | None = None,
313
+ ) -> str | dict | None: ...
314
+
315
+ @overload
316
+ async def run_task(
317
+ self,
318
+ *,
319
+ goal: str,
320
+ output=None,
321
+ profile: str | AgentProfile | None = None,
322
+ name: str | None = None,
323
+ locked_app_package: str | None = None,
324
+ app_path: str | Path | None = None,
325
+ ) -> str | None: ...
326
+
327
+ @overload
328
+ async def run_task(
329
+ self,
330
+ *,
331
+ request: TaskRequest[None],
332
+ locked_app_package: str | None = None,
333
+ app_path: str | Path | None = None,
334
+ ) -> str | dict | None: ...
335
+
336
+ @overload
337
+ async def run_task(
338
+ self,
339
+ *,
340
+ request: TaskRequest[TOutput],
341
+ locked_app_package: str | None = None,
342
+ app_path: str | Path | None = None,
343
+ ) -> TOutput | None: ...
344
+
345
+ async def run_task(
346
+ self,
347
+ *,
348
+ goal: str | None = None,
349
+ output: type[TOutput] | str | None = None,
350
+ profile: str | AgentProfile | None = None,
351
+ locked_app_package: str | None = None,
352
+ name: str | None = None,
353
+ app_path: str | Path | None = None,
354
+ request: TaskRequest[TOutput] | None = None,
355
+ ) -> str | dict | TOutput | None:
356
+
357
+ # Normal local execution path
358
+ if request is not None:
359
+ if locked_app_package is not None:
360
+ if request.locked_app_package:
361
+ logger.warning(
362
+ "Locked app package specified both in the request and"
363
+ " as a parameter. Using the parameter value."
364
+ )
365
+ request.locked_app_package = locked_app_package
366
+ # Handle app_path parameter override
367
+ if app_path is not None:
368
+ if request.app_path:
369
+ logger.warning(
370
+ "App path specified both in the request and as a"
371
+ " parameter. Using the parameter value."
372
+ )
373
+ request.app_path = Path(app_path) if isinstance(app_path, str) else app_path
374
+ return await self._run_task(request=request)
375
+ if goal is None:
376
+ raise AgentTaskRequestError("Goal is required")
377
+ task_request = self.new_task(goal=goal)
378
+ if output is not None:
379
+ if isinstance(output, str):
380
+ task_request.with_output_description(description=output)
381
+ elif output is not NoneType:
382
+ task_request.with_output_format(output_format=output)
383
+ if profile is not None:
384
+ task_request.using_profile(profile=profile)
385
+ if name is not None:
386
+ task_request.with_name(name=name)
387
+ if locked_app_package is not None:
388
+ task_request.with_locked_app_package(package_name=locked_app_package)
389
+ if app_path is not None:
390
+ task_request.with_app_path(app_path=app_path)
391
+ return await self._run_task(task_request.build())
392
+
393
+ async def _run_task(
394
+ self,
395
+ request: TaskRequest[TOutput],
396
+ ) -> str | dict | TOutput | None:
397
+ if not self._initialized:
398
+ raise AgentNotInitializedError()
399
+
400
+ if request.profile:
401
+ agent_profile = self._config.agent_profiles.get(request.profile)
402
+ if agent_profile is None:
403
+ if request.profile.lower() in (
404
+ "flash",
405
+ "pro",
406
+ "ultra",
407
+ "default",
408
+ ):
409
+ agent_profile = self._config.default_profile
410
+ else:
411
+ raise AgentProfileNotFoundError(request.profile)
412
+ else:
413
+ agent_profile = self._config.default_profile
414
+
415
+ logger.info(str(agent_profile))
416
+
417
+ on_status_changed = None
418
+ task_id = str(
419
+ self._session_id
420
+ or os.getenv("ARTEMIS_SESSION_ID")
421
+ or os.getenv("ARTEMIS_CLOUD_SESSION_ID")
422
+ or uuid.uuid4()
423
+ )
424
+
425
+ task = Task(
426
+ id=task_id,
427
+ device=self._device_context,
428
+ status="pending",
429
+ request=request,
430
+ created_at=datetime.now(),
431
+ on_status_changed=on_status_changed,
432
+ )
433
+ self._tasks.append(task)
434
+ task_name = task.get_name()
435
+
436
+ context = ArtemisContext(
437
+ device=self._device_context,
438
+ adb_client=self._adb_client,
439
+ ui_adb_client=self._ui_adb_client,
440
+ llm_config=agent_profile.llm_config,
441
+ agent_config=self._config,
442
+ )
443
+
444
+ output_config = None
445
+ if request.output_description or request.output_format:
446
+ output_config = OutputConfig(
447
+ output_description=request.output_description,
448
+ structured_output=request.output_format, # type: ignore
449
+ )
450
+ logger.info(str(output_config))
451
+
452
+ logger.info(f"[{task_name}] Starting graph with goal: `{request.goal}`")
453
+ state = self._get_graph_state(task=task)
454
+ graph_input = state.model_dump()
455
+ datetime.now(UTC)
456
+
457
+ async def _execute_task_logic():
458
+ last_state: State | None = None
459
+ last_state_snapshot: dict | None = None
460
+ output = None
461
+ effective_mode = getattr(self._config, "concurrency_mode", "per_device")
462
+ effective_max = getattr(self._config, "max_concurrency", None)
463
+ sess_id = (
464
+ self._session_id
465
+ or os.getenv("ARTEMIS_SESSION_ID")
466
+ or os.getenv("ARTEMIS_CLOUD_SESSION_ID")
467
+ or getattr(task, "id", None)
468
+ or getattr(getattr(task, "request", None), "task_name", None)
469
+ )
470
+ active_owner = DeviceExecutionLock.get_active_owner(self._device_context.device_id)
471
+ already_held = (
472
+ active_owner is not None
473
+ and active_owner.pid == os.getpid()
474
+ and (
475
+ (sess_id and str(active_owner.session_id) == str(sess_id))
476
+ or active_owner.token == self._device_context.device_id
477
+ )
478
+ )
479
+ device_lock = (
480
+ None
481
+ if already_held
482
+ else DeviceExecutionLock(
483
+ self._device_context.device_id,
484
+ description=f"{request.goal[:120]}",
485
+ concurrency_mode=effective_mode,
486
+ max_concurrency=effective_max,
487
+ session_id=str(sess_id) if sess_id else None,
488
+ ingress=os.getenv("ARTEMIS_TASK_INGRESS") or "agent",
489
+ )
490
+ )
491
+ try:
492
+ if device_lock is not None and os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
493
+ queue_cancel_event = threading.Event()
494
+ acquire_task = asyncio.create_task(
495
+ asyncio.to_thread(
496
+ device_lock.acquire,
497
+ cancel_event=queue_cancel_event,
498
+ )
499
+ )
500
+ try:
501
+ await asyncio.shield(acquire_task)
502
+ except asyncio.CancelledError:
503
+ queue_cancel_event.set()
504
+ try:
505
+ await asyncio.shield(acquire_task)
506
+ except Exception as exc: # pylint: disable=broad-exception-caught
507
+ # Draining the lock acquisition after cancellation is best effort.
508
+ logger.debug(
509
+ f"Device lock acquisition drain after cancel failed: {exc}",
510
+ exc_info=True,
511
+ )
512
+ raise
513
+ # All device mutation and UI initialization happens only after
514
+ # this task reaches the head of the one global FIFO queue.
515
+ # Session creation must be inside the same boundary as well:
516
+ # DataEngine uses a shared database, so a queued task must not
517
+ # publish a new active session while the current task is still
518
+ # finishing.
519
+ self._prepare_tracing(task=task, context=context)
520
+ self._prepare_output_files(task=task)
521
+ if os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
522
+ if self._ui_adb_client is not None:
523
+ await self._connect_screen_client(context, str(sess_id))
524
+ await self._ensure_device_unlocked()
525
+ publish_startup_progress(
526
+ "environment", "Preparing the device environment", session_id=str(sess_id)
527
+ )
528
+ await self._prepare_app_installation(task=task)
529
+ await self._prepare_device_environment(context=context)
530
+ await self._prepare_app_lock(task=task, context=context)
531
+ async with context:
532
+ recording_started = False
533
+ if self._config.video_recording_tools_enabled:
534
+ try:
535
+ controller = get_controller(context)
536
+
537
+ output_dir = None
538
+ if context.execution_setup and context.execution_setup.traces_path:
539
+ output_dir = (
540
+ self._tmp_traces_dir / context.execution_setup.trace_name
541
+ ).resolve()
542
+
543
+ logger.info(f"[{task_name}] Starting automated screen recording...")
544
+ start_res = await controller.start_video_recording(
545
+ output_dir=output_dir
546
+ )
547
+ if start_res and start_res.success:
548
+ recording_started = True
549
+ else:
550
+ logger.warning(
551
+ f"[{task_name}] Failed to start screen"
552
+ f" recording: {start_res.message if start_res else 'unknown'}"
553
+ )
554
+ except Exception as e:
555
+ logger.error(f"[{task_name}] Failed to start screen recording: {e}")
556
+
557
+ publish_startup_progress(
558
+ "environment_ready", "Device environment is ready", session_id=str(sess_id)
559
+ )
560
+ try:
561
+ if request.profile and request.profile.lower() == "flash":
562
+ logger.info(f"[{task_name}] Invoking FlashRunner reactive loop...")
563
+ await task.set_status(
564
+ status="running",
565
+ message="Invoking FlashRunner...",
566
+ )
567
+
568
+ # An explicit max_steps caps the reactive loop; the
569
+ # default leaves the cap to agent.flash.max_turns
570
+ # (0 = unlimited).
571
+ max_turns = (
572
+ request.max_steps if request.max_steps != RECURSION_LIMIT else None
573
+ )
574
+ runner = FlashRunner(context, goal=request.goal, max_turns=max_turns)
575
+ flash_result = await runner.run(state)
576
+
577
+ output = flash_result
578
+ last_state_snapshot = state.model_dump()
579
+
580
+ status = flash_result.get("status")
581
+ if status == "completed":
582
+ logger.info(f"✅ Automation '{task_name}' is success ✅")
583
+ await task.finalize(content=output, state=last_state_snapshot)
584
+ if context.data_engine:
585
+ context.data_engine.end_session("completed")
586
+ else:
587
+ err = (
588
+ f"[{task_name}] FlashRunner failed:"
589
+ f" {flash_result.get('explanation')}"
590
+ )
591
+ logger.warning(err)
592
+ await task.finalize(
593
+ content=output,
594
+ state=last_state_snapshot,
595
+ error=err,
596
+ )
597
+ if context.data_engine:
598
+ context.data_engine.end_session("failed")
599
+ return output
600
+ else:
601
+ logger.info(f"[{task_name}] Invoking graph with input: {graph_input}")
602
+ await task.set_status(status="running", message="Invoking graph...")
603
+ async for chunk in (await get_graph(context)).astream(
604
+ input=graph_input,
605
+ config={
606
+ "recursion_limit": task.request.max_steps,
607
+ "callbacks": (
608
+ (
609
+ list(self._config.graph_config_callbacks)
610
+ if self._config.graph_config_callbacks is not None
611
+ else []
612
+ )
613
+ + [DataEngineCallbackHandler(context)]
614
+ if context.data_engine
615
+ else self._config.graph_config_callbacks
616
+ ),
617
+ },
618
+ stream_mode=[
619
+ "messages",
620
+ "custom",
621
+ "updates",
622
+ "values",
623
+ ],
624
+ ):
625
+ stream_mode, payload = chunk
626
+ if stream_mode == "values":
627
+ last_state_snapshot = payload # type: ignore
628
+ last_state = State(**last_state_snapshot) # type: ignore
629
+
630
+ if not last_state:
631
+ err = f"[{task_name}] No result received from graph"
632
+ logger.warning(err)
633
+ await task.finalize(
634
+ content=output,
635
+ state=last_state_snapshot,
636
+ error=err,
637
+ )
638
+ if context.data_engine:
639
+ context.data_engine.end_session("failed")
640
+ return None
641
+
642
+ output = await self._extract_output(
643
+ task_name=task_name,
644
+ ctx=context,
645
+ request=request,
646
+ output_config=output_config,
647
+ state=last_state,
648
+ )
649
+ # Run outcome (goal axis x test axis) drives the
650
+ # wrap-up instead of an unconditional success path.
651
+ run_outcome = getattr(last_state, "run_outcome", None)
652
+ context.run_outcome = run_outcome
653
+ output = attach_test_summary(output, run_outcome)
654
+ task_status_axis = (
655
+ run_outcome.get("task_status") if run_outcome else None
656
+ )
657
+ tests_failed = (
658
+ int((run_outcome.get("tests") or {}).get("failed", 0))
659
+ if run_outcome
660
+ else 0
661
+ )
662
+ if task_status_axis == "blocked":
663
+ err = (
664
+ f"[{task_name}] Task ended blocked: verify"
665
+ " criteria unmet after exhausting the final"
666
+ " check budget."
667
+ )
668
+ logger.warning(err)
669
+ await task.finalize(
670
+ content=output,
671
+ state=last_state_snapshot,
672
+ error=err,
673
+ )
674
+ if context.data_engine:
675
+ context.data_engine.end_session("failed")
676
+ return output
677
+
678
+ if tests_failed > 0:
679
+ logger.warning(
680
+ f"[{task_name}] Task completed, but"
681
+ f" {tests_failed} assertion(s) failed —"
682
+ " see the test summary."
683
+ )
684
+ else:
685
+ logger.info(f"✅ Automation '{task_name}' is success ✅")
686
+ await task.finalize(content=output, state=last_state_snapshot)
687
+ if context.data_engine:
688
+ context.data_engine.end_session("completed")
689
+
690
+ return output
691
+ finally:
692
+ if recording_started:
693
+
694
+ async def _safe_stop_recording():
695
+ try:
696
+ logger.info(
697
+ f"[{task_name}] Stopping automated screen recording..."
698
+ )
699
+ controller = get_controller(context)
700
+ return await controller.stop_video_recording()
701
+ except Exception as e:
702
+ logger.error(
703
+ f"[{task_name}] Error stopping screen recording: {e}"
704
+ )
705
+ return None
706
+
707
+ stop_task = asyncio.create_task(_safe_stop_recording())
708
+ try:
709
+ stop_result = await asyncio.shield(stop_task)
710
+ except (asyncio.CancelledError, BaseException):
711
+ try:
712
+ stop_result = await asyncio.wait_for(stop_task, timeout=15.0)
713
+ except Exception:
714
+ stop_result = None
715
+
716
+ if stop_result and stop_result.success and stop_result.video_path:
717
+ logger.info(
718
+ f"[{task_name}] Screen recording saved"
719
+ f" to: {stop_result.video_path}"
720
+ )
721
+ elif stop_result:
722
+ logger.warning(
723
+ f"[{task_name}] Failed to save screen"
724
+ f" recording: {stop_result.message}"
725
+ )
726
+ except asyncio.CancelledError:
727
+ err = f"[{task_name}] Task cancelled"
728
+ logger.warning(err)
729
+ await task.finalize(
730
+ content=output,
731
+ state=last_state_snapshot,
732
+ error=err,
733
+ cancelled=True,
734
+ )
735
+ if context.data_engine:
736
+ context.data_engine.end_session("cancelled")
737
+
738
+ raise
739
+ except Exception as e:
740
+ err = f"[{task_name}] Error running automation: {e}"
741
+ logger.error(err)
742
+ await task.finalize(
743
+ content=output,
744
+ state=last_state_snapshot,
745
+ error=err,
746
+ )
747
+ if context.data_engine:
748
+ context.data_engine.end_session("failed")
749
+
750
+ raise
751
+ finally:
752
+ try:
753
+ # Background ADB processes (logcat, screenrecord, ...) started
754
+ # by the Operator must not outlive the automation task.
755
+ from artemis.tools.command_tool import shutdown_adb_background_tasks
756
+
757
+ await asyncio.wait_for(shutdown_adb_background_tasks(context), timeout=15.0)
758
+ except Exception as e:
759
+ logger.warning(f"[{task_name}] Failed to stop background ADB tasks: {e}")
760
+ try:
761
+ await self._finalize_tracing_safely(task=task, context=context)
762
+ finally:
763
+ if os.environ.get("ARTEMIS_CLOUD_MODE") != "1":
764
+ try:
765
+ if self._ui_adb_client is not None:
766
+ await asyncio.to_thread(self._ui_adb_client.disconnect)
767
+ finally:
768
+ if device_lock is not None:
769
+ await asyncio.to_thread(device_lock.release)
770
+
771
+ async with self._task_lock:
772
+ if self._current_task and not self._current_task.done():
773
+ logger.warning(
774
+ "Another automation task is already running. "
775
+ "Stopping it before starting the new one."
776
+ )
777
+ self.stop_current_task()
778
+ try:
779
+ await self._current_task
780
+ except asyncio.CancelledError:
781
+ pass
782
+
783
+ cancel_watcher: asyncio.Task | None = None
784
+ try:
785
+ self._current_task = asyncio.create_task(_execute_task_logic())
786
+ cancel_watcher = asyncio.create_task(self._watch_external_cancel(task_name))
787
+ return await self._current_task
788
+ finally:
789
+ self._current_task = None
790
+ if cancel_watcher is not None and not cancel_watcher.done():
791
+ cancel_watcher.cancel()
792
+ try:
793
+ await cancel_watcher
794
+ except asyncio.CancelledError:
795
+ pass
796
+
797
+ def stop_current_task(self):
798
+ """Requests cancellation of the currently running automation task."""
799
+ if self._current_task and not self._current_task.done():
800
+ logger.info("Requesting to stop the current automation task...")
801
+ was_cancelled = self._current_task.cancel()
802
+ if was_cancelled:
803
+ logger.success("Cancellation request for the current task was sent.")
804
+ else:
805
+ logger.warning(
806
+ "Could not send cancellation request for the current task "
807
+ "(it may already be completing)."
808
+ )
809
+ else:
810
+ logger.info("No active automation task to stop.")
811
+
812
+ async def get_screenshot(self) -> Image.Image:
813
+ """Capture a screenshot from the mobile device.
814
+
815
+ Returns:
816
+ Screenshot as PIL Image
817
+
818
+ Raises:
819
+ AgentNotInitializedError: If the agent is not initialized
820
+ Exception: If screenshot capture fails
821
+ """
822
+ if not self._initialized:
823
+ raise AgentNotInitializedError()
824
+
825
+ # Use ADB to capture screenshot
826
+ logger.info("Capturing screenshot from local Android device")
827
+ if not self._adb_client:
828
+ raise Exception("ADB client not initialized")
829
+
830
+ device = self._adb_client.device(serial=self._device_context.device_id)
831
+ screenshot = await asyncio.to_thread(device.screenshot)
832
+ logger.info("Screenshot captured from local Android device")
833
+ return screenshot
834
+
835
+ async def _prepare_app_installation(self, task: Task) -> str | None:
836
+ """Install app if app_path is specified in the task request.
837
+
838
+ Returns:
839
+ None.
840
+ """
841
+ if not task.request.app_path:
842
+ return None
843
+
844
+ task_name = task.get_name()
845
+ logger.info(f"[{task_name}] Installing app from: {task.request.app_path}")
846
+
847
+ await self.install_app(task.request.app_path)
848
+
849
+ return None
850
+
851
+ async def _prepare_app_lock(self, task: Task, context: ArtemisContext):
852
+ """Prepare app lock by launching the locked app if specified."""
853
+ if not task.request.locked_app_package:
854
+ return
855
+
856
+ task_name = task.get_name()
857
+ logger.info(f"[{task_name}] Preparing app lock for: {task.request.locked_app_package}")
858
+
859
+ app_lock_status = await _handle_initial_app_launch(
860
+ ctx=context, locked_app_package=task.request.locked_app_package
861
+ )
862
+
863
+ if context.execution_setup is None:
864
+ context.execution_setup = ExecutionSetup(app_lock_status=app_lock_status)
865
+ else:
866
+ context.execution_setup.app_lock_status = app_lock_status
867
+
868
+ if app_lock_status.locked_app_initial_launch_success is False:
869
+ error = app_lock_status.locked_app_initial_launch_error
870
+ logger.warning(f"[{task_name}] Failed to launch locked app: {error}")
871
+
872
+ def _prepare_trace_paths(self, task: Task) -> str:
873
+ """Create the temp (and, when recording, output) trace directories; return the task name."""
874
+ task_name = task.get_name()
875
+ temp_trace_path = Path(self._tmp_traces_dir / task_name).resolve()
876
+ temp_trace_path.mkdir(parents=True, exist_ok=True)
877
+
878
+ if task.request.record_trace:
879
+ traces_output_path = Path(task.request.trace_path).resolve()
880
+ logger.info(f"[{task_name}] 📂 Traces output path: {traces_output_path}")
881
+ logger.info(f"[{task_name}] 📄📂 Traces temp path: {temp_trace_path}")
882
+ traces_output_path.mkdir(parents=True, exist_ok=True)
883
+ return task_name
884
+
885
+ async def _finalize_tracing(self, task: Task, context: ArtemisContext):
886
+ if context.data_engine:
887
+ session_status = "completed"
888
+ if task.status == "failed":
889
+ session_status = "failed"
890
+ elif task.status == "cancelled":
891
+ session_status = "cancelled"
892
+ context.data_engine.end_session(status=session_status)
893
+
894
+ exec_setup_ctx = context.execution_setup
895
+ if not exec_setup_ctx:
896
+ return
897
+
898
+ if exec_setup_ctx.traces_path is None or exec_setup_ctx.trace_name is None:
899
+ return
900
+
901
+ task_name = task.get_name()
902
+ temp_trace_path = (self._tmp_traces_dir / exec_setup_ctx.trace_name).resolve()
903
+
904
+ if not task.request.record_trace:
905
+ try:
906
+ if temp_trace_path.exists():
907
+ shutil.rmtree(temp_trace_path)
908
+ logger.info(f"[{task_name}] Cleaned up temporary trace folder.")
909
+ except Exception as e:
910
+ logger.error(f"[{task_name}] Failed to clean up temp trace folder: {e}")
911
+ return
912
+
913
+ status = resolve_trace_suffix(task.status, getattr(context, "run_outcome", None))
914
+ ts = task.created_at.strftime("%Y-%m-%dT%H-%M-%S")
915
+ new_name = f"{exec_setup_ctx.trace_name}{status}_{ts}"
916
+
917
+ traces_output_path = Path(task.request.trace_path).resolve()
918
+
919
+ logger.info(f"[{task_name}] Compiling trace FROM FOLDER: " + str(temp_trace_path))
920
+ create_gif_from_trace_folder(temp_trace_path)
921
+ create_steps_json_from_trace_folder(temp_trace_path)
922
+
923
+ logger.info(f"[{task_name}] Video created, removing dust...")
924
+ remove_images_from_trace_folder(temp_trace_path)
925
+ remove_steps_json_from_trace_folder(temp_trace_path)
926
+ logger.info(f"[{task_name}] 📽️ Trace compiled, moving to output path 📽️")
927
+
928
+ output_folder_path = temp_trace_path.rename(traces_output_path / new_name).resolve()
929
+ logger.info(f"[{task_name}] 📂✅ Traces located in: {output_folder_path}")
930
+
931
+ new_video_path = output_folder_path / "recording.mp4"
932
+ if new_video_path.exists() and context.data_engine:
933
+ context.data_engine.update_video_path(new_video_path)
934
+
935
+ def _prepare_output_files(self, task: Task):
936
+ if task.request.llm_output_path:
937
+ _validate_and_prepare_file(file_path=task.request.llm_output_path)
938
+
939
+ async def _extract_output(
940
+ self,
941
+ task_name: str,
942
+ ctx: ArtemisContext,
943
+ request: TaskRequest[TOutput],
944
+ output_config: OutputConfig | None,
945
+ state: State,
946
+ ) -> str | dict | TOutput | None:
947
+ exec_setup = getattr(ctx, "execution_setup", None)
948
+ outputter_cfg = getattr(exec_setup, "outputter", None) or (
949
+ getattr(ctx.agent_config, "outputter", None) if ctx and ctx.agent_config else None
950
+ )
951
+ outputter_enabled = getattr(outputter_cfg, "enabled", True) if outputter_cfg else True
952
+ force_synthesis = (
953
+ getattr(outputter_cfg, "force_synthesis", False) if outputter_cfg else False
954
+ )
955
+ if exec_setup and getattr(exec_setup, "disable_outputter", False):
956
+ outputter_enabled = False
957
+
958
+ should_run = outputter_enabled and (
959
+ (
960
+ output_config
961
+ and output_config.needs_structured_format(default_enabled=outputter_enabled)
962
+ )
963
+ or force_synthesis
964
+ or (output_config and output_config.enable_outputter is True)
965
+ )
966
+ if output_config and output_config.enable_outputter is False:
967
+ should_run = False
968
+
969
+ if should_run:
970
+ logger.info(f"[{task_name}] Generating structured output via Outputter...")
971
+ try:
972
+ structured_output = await outputter(
973
+ ctx=ctx,
974
+ output_config=output_config or OutputConfig(),
975
+ graph_output=state,
976
+ )
977
+ logger.info(f"[{task_name}] Structured output: {structured_output}")
978
+ record_events(
979
+ output_path=request.llm_output_path,
980
+ events=structured_output,
981
+ )
982
+ if request.output_format is not None and request.output_format is not NoneType:
983
+ return request.output_format.model_validate(structured_output)
984
+ return structured_output
985
+ except Exception as e:
986
+ logger.error(f"[{task_name}] Failed to generate structured output: {e}")
987
+ return None
988
+ return None
989
+
990
+ def _init_clients(
991
+ self,
992
+ device_id: str,
993
+ platform: DevicePlatform,
994
+ ):
995
+ self._adb_client = AdbClient(
996
+ host=self._config.servers.adb_host,
997
+ port=self._config.servers.adb_port,
998
+ )
999
+ self._ui_adb_client = create_screen_client(device_id)
1000
+
1001
+ async def _get_device_context(
1002
+ self,
1003
+ device_id: str,
1004
+ platform: DevicePlatform,
1005
+ ) -> DeviceContext:
1006
+
1007
+ host_platform = system()
1008
+ if os.environ.get("ARTEMIS_CLOUD_MODE") == "1":
1009
+ width, height = (1080, 2400)
1010
+ if self._adb_client:
1011
+ try:
1012
+ width, height = self._adb_client.device(device_id).window_size()
1013
+ logger.info(
1014
+ f"Retrieved remote device screen dimensions dynamically: {width}x{height}"
1015
+ )
1016
+ except Exception as e:
1017
+ logger.debug(
1018
+ f"Failed to query remote window size: {e}. Defaulting to 1080x2400."
1019
+ )
1020
+ return DeviceContext(
1021
+ host_platform="LINUX",
1022
+ mobile_platform=platform,
1023
+ device_id=device_id,
1024
+ device_width=width,
1025
+ device_height=height,
1026
+ )
1027
+
1028
+ # Query dimensions without starting UIAutomator or acquiring an awake
1029
+ # strategy. Those belong inside the global execution queue lease.
1030
+ if self._adb_client:
1031
+ try:
1032
+ device_width, device_height = self._adb_client.device(device_id).window_size()
1033
+ logger.info(f"Retrieved Android screen dimensions: {device_width}x{device_height}")
1034
+ except Exception as e:
1035
+ logger.warning(f"Failed to get Android window size: {e}, using defaults")
1036
+ device_width, device_height = 1080, 2340
1037
+ else:
1038
+ logger.warning("ADB client not available, using default dimensions")
1039
+ device_width, device_height = 1080, 2340
1040
+
1041
+ from artemis.platform import platform as pal_platform
1042
+
1043
+ return DeviceContext(
1044
+ host_platform=pal_platform.os_type.name,
1045
+ mobile_platform=platform,
1046
+ device_id=device_id,
1047
+ device_width=device_width,
1048
+ device_height=device_height,
1049
+ )
1050
+
1051
+
1052
+ def _validate_and_prepare_file(file_path: Path):
1053
+ path_obj = Path(file_path)
1054
+ if path_obj.exists() and path_obj.is_dir():
1055
+ raise AgentTaskRequestError(f"Error: Path '{file_path}' is a directory, not a file.")
1056
+ try:
1057
+ path_obj.parent.mkdir(parents=True, exist_ok=True)
1058
+ path_obj.touch(exist_ok=True)
1059
+ except OSError as e:
1060
+ raise AgentTaskRequestError(f"Error creating file '{file_path}': {e}")