android-midscene-automation 0.2.3 → 1.1.0

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 +11 -1
  2. package/README.md +63 -1
  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 +10 -8
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +13 -1
  507. package/src/App.vue +72 -23
  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 +369 -186
  525. package/src/types.ts +1 -1
@@ -0,0 +1,1249 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """MCP Tool: mobile_diagnose.
16
+
17
+ Runs the same readiness probes as the web console's device wizard and the
18
+ ``artemis doctor`` CLI, adds the MCP-host probe, and renders the outcome as an
19
+ action list an AI coding assistant can execute without reading Artemis source.
20
+ On request it also boots an installed emulator in the background, verifies the
21
+ configured API keys against the providers, and drives the attached device end
22
+ to end (screenshot + UI hierarchy) to prove a task could actually start.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import subprocess
28
+
29
+ import asyncio
30
+ import os
31
+ from pathlib import Path
32
+ import re
33
+ from typing import Any
34
+
35
+ from mcp_server.base import mcp
36
+ from mcp_server.utils import env_utils
37
+ from artemis.core.diagnostics import readiness_engine
38
+ from artemis.core.diagnostics.device_smoke import smoke_test_device
39
+ from artemis.core.diagnostics.readiness import (
40
+ adb_keys_corrupted,
41
+ base_verdict,
42
+ collect_readiness,
43
+ sort_by_fix_order,
44
+ )
45
+ from artemis.core.diagnostics.schema import ProbeResult, ProbeStatus, SystemReadinessReport
46
+ from artemis.runtime import DeviceExecutionLock, trace_store
47
+ from artemis.runtime.helper_manager import helper_manager
48
+ from artemis.utils.credentials_validator import validate_api_key
49
+ from third_party.mobile_use.utils.logger import get_logger
50
+
51
+ logger = get_logger(__name__)
52
+
53
+ #: Upper bound for one diagnosis phase. The ADB probe shells out without its
54
+ #: own deadline, so a wedged ADB server must surface as a verdict, not a hung
55
+ #: tool. The optional extras (credential verification, device smoke test) run
56
+ #: concurrently under a second budget of the same size.
57
+ DIAGNOSIS_TIMEOUT_SECONDS = 40.0
58
+
59
+ #: Per-provider budget for a live API key verification request.
60
+ CREDENTIAL_CHECK_TIMEOUT_SECONDS = 12.0
61
+
62
+ #: Providers the credentials probe lists by their *endpoint URL* rather than a
63
+ #: key (``OPENAI_BASE_URL`` / ``OLLAMA_BASE_URL`` / ``VLLM_BASE_URL``). Their
64
+ #: "raw_key" is that URL: verification must call the endpoint, not treat the
65
+ #: URL as an API key.
66
+ _ENDPOINT_PROVIDERS = frozenset({"custom", "ollama", "vllm"})
67
+
68
+ #: Metadata keys that carry credential material. The console UI needs them to
69
+ #: prefill its settings form; an MCP caller is an LLM context and must never
70
+ #: see them.
71
+ _SECRET_METADATA_KEYS = frozenset(
72
+ {"raw_key", "key", "api_keys", "current_key", "current_gemini_key"}
73
+ )
74
+
75
+ _ERROR_MARKERS = ("traceback", "error", "exception", "failed", "critical")
76
+
77
+ #: rich/typer write colour escapes into the tee'd stderr log; the model must not see them.
78
+ _ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[A-Za-z]")
79
+
80
+ #: Emulator lifecycle stages during which a second launch must not be issued.
81
+ _LAUNCH_IN_PROGRESS = frozenset({"starting", "waiting_for_adb", "booting"})
82
+
83
+ #: ``emulator -avd X`` / ``<sdk>/emulator/emulator.exe -avd X``: a foreground
84
+ #: process that never returns; it must not be handed to the assistant as a
85
+ #: shell command.
86
+ _EMULATOR_LAUNCH_RE = re.compile(r"emulator(?:\.exe)?\s+-avd\s+(\S+)", re.IGNORECASE)
87
+
88
+
89
+ # --------------------------------------------------------------------------- #
90
+ # Rendering helpers
91
+ # --------------------------------------------------------------------------- #
92
+
93
+
94
+ def _scrub(value: Any) -> Any:
95
+ """Drop secrets and bulky inventories from probe metadata before it reaches the model."""
96
+ if isinstance(value, dict):
97
+ cleaned: dict[str, Any] = {}
98
+ for key, item in value.items():
99
+ if key in _SECRET_METADATA_KEYS:
100
+ continue
101
+ if key == "installed_packages" and isinstance(item, list):
102
+ cleaned["installed_package_count"] = len(item)
103
+ continue
104
+ cleaned[key] = _scrub(item)
105
+ return cleaned
106
+ if isinstance(value, list):
107
+ return [_scrub(item) for item in value]
108
+ return value
109
+
110
+
111
+ def _find(results: list[ProbeResult], probe_id: str) -> ProbeResult | None:
112
+ return next((r for r in results if r.id == probe_id), None)
113
+
114
+
115
+ def _render_check(result: ProbeResult) -> dict[str, Any]:
116
+ """Passing checks are one line each; only problems carry detail, fixes and facts."""
117
+ check: dict[str, Any] = {
118
+ "id": result.id,
119
+ "title": result.title,
120
+ "status": result.status.value,
121
+ "required": result.is_blocker,
122
+ "summary": result.summary,
123
+ }
124
+ if result.status is ProbeStatus.PASS:
125
+ return check
126
+ check["category"] = result.category.value
127
+ check["detail"] = result.description
128
+ check["fix"] = [
129
+ {"type": action.action_type, "label": action.label, "payload": action.payload}
130
+ for action in result.actions
131
+ ]
132
+ check["facts"] = _scrub(result.metadata)
133
+ return check
134
+
135
+
136
+ def _compact_host(metadata: dict[str, Any]) -> dict[str, Any]:
137
+ daemon = metadata.get("daemon") or {}
138
+ host: dict[str, Any] = {
139
+ key: metadata.get(key)
140
+ for key in (
141
+ "server_python",
142
+ "runner_python",
143
+ "interpreter_matches_venv",
144
+ "env_file",
145
+ "env_file_exists",
146
+ "traces_dir",
147
+ )
148
+ }
149
+ if "mcp_client" in metadata:
150
+ host["mcp_client"] = _scrub(metadata["mcp_client"])
151
+ host["daemon"] = {
152
+ key: daemon.get(key)
153
+ for key in ("port", "reachable", "port_held_by_other_process", "log_path")
154
+ }
155
+ return host
156
+
157
+
158
+ def _compact_device(report: SystemReadinessReport) -> dict[str, Any] | None:
159
+ device = report.active_device
160
+ if device is None:
161
+ return None
162
+ return {
163
+ "serial": device.serial,
164
+ "state": device.state,
165
+ "model": device.model,
166
+ "android_version": device.android_version,
167
+ "is_locked": device.is_locked,
168
+ "is_emulator": device.is_emulator,
169
+ }
170
+
171
+
172
+ def _compact_emulator(raw: dict[str, Any]) -> dict[str, Any]:
173
+ status = raw.get("status")
174
+ return {
175
+ "avd_name": raw.get("avd_name"),
176
+ "status": getattr(status, "value", status),
177
+ "stage_message": raw.get("stage_message"),
178
+ "error": raw.get("error"),
179
+ "serial": raw.get("serial"),
180
+ "elapsed_seconds": raw.get("elapsed_seconds"),
181
+ "progress_percent": raw.get("progress_percent"),
182
+ }
183
+
184
+
185
+ def _launch_in_progress(emulator: dict[str, Any] | None) -> bool:
186
+ return emulator is not None and emulator.get("status") in _LAUNCH_IN_PROGRESS
187
+
188
+
189
+ def _launch_guidance(avd: str) -> str:
190
+ return f' Guidance: Call mobile_diagnose(launch_avd="{avd}") to start it in the background.'
191
+
192
+
193
+ def _render_action_lines(action: Any, *, installed_avds: list[str]) -> list[str]:
194
+ """Turn one ProbeAction into next_steps lines the assistant can act on verbatim.
195
+
196
+ Commands chained with ``&&`` become consecutive ``Run:`` lines (PowerShell
197
+ 5.1 cannot parse ``&&``); ``emulator -avd`` never becomes a ``Run:`` line
198
+ because it would block the assistant's shell forever.
199
+ """
200
+ payload = action.payload.strip()
201
+ if action.action_type == "link":
202
+ return [f" Docs: {payload}"]
203
+ if action.action_type != "command":
204
+ return [f" Guidance: {payload}"]
205
+
206
+ lines: list[str] = []
207
+ for part in payload.split("&&"):
208
+ command = part.strip()
209
+ if not command or command == "artemis init":
210
+ continue # interactive; covered by the credential / config guidance
211
+ match = _EMULATOR_LAUNCH_RE.search(command)
212
+ if match is None:
213
+ lines.append(f" Run: {command}")
214
+ continue
215
+ avd = match.group(1)
216
+ if avd in installed_avds:
217
+ lines.append(_launch_guidance(avd))
218
+ else:
219
+ lines.append(
220
+ f" Guidance: No AVD named '{avd}' is installed. Create one in Android Studio's "
221
+ "Device Manager (or ask the user to connect a phone), then call "
222
+ 'mobile_diagnose(launch_avd="<name>").'
223
+ )
224
+ return lines
225
+
226
+
227
+ def _credential_steps(env_file: str | None) -> list[str]:
228
+ location = env_file or "the project's .env file"
229
+ return [
230
+ " Ask the user to add a provider key (for example GEMINI_API_KEY=...) to "
231
+ f"{location}, or to the MCP server's env block in the IDE's MCP config. "
232
+ "Never ask them to paste the key into the chat.",
233
+ " `artemis init` is interactive and cannot run from a tool call; edit the env file instead.",
234
+ " Keys are read when the server starts: restart the MCP server (reload MCP servers in "
235
+ "the IDE) after adding one.",
236
+ ]
237
+
238
+
239
+ # --------------------------------------------------------------------------- #
240
+ # Device lock / task state
241
+ # --------------------------------------------------------------------------- #
242
+
243
+
244
+ def _normalize_serial(value: Any) -> str:
245
+ return re.sub(r"[^a-zA-Z0-9_-]", "_", str(value or "").strip())
246
+
247
+
248
+ def _task_state() -> dict[str, list[dict[str, Any]]]:
249
+ """Compact view of who holds or waits for a device, from the cross-process lock files."""
250
+ active: list[dict[str, Any]] = []
251
+ queued: list[dict[str, Any]] = []
252
+ try:
253
+ seen: set[Any] = set()
254
+ for owner in DeviceExecutionLock.get_active_owners().values():
255
+ token = getattr(owner, "token", id(owner))
256
+ if token in seen:
257
+ continue
258
+ seen.add(token)
259
+ active.append(
260
+ {
261
+ "device": getattr(owner, "device_id", None),
262
+ "session_id": getattr(owner, "session_id", None),
263
+ "pid": getattr(owner, "pid", None),
264
+ "description": getattr(owner, "description", None),
265
+ "ingress": getattr(owner, "ingress", None),
266
+ "started_at": getattr(owner, "acquired_at", None),
267
+ }
268
+ )
269
+ except Exception as exc:
270
+ logger.debug(f"Could not read active device locks: {exc}")
271
+ try:
272
+ for ticket in DeviceExecutionLock.get_queued_tasks():
273
+ queued.append(
274
+ {
275
+ "device": ticket.get("device_id"),
276
+ "session_id": ticket.get("session_id"),
277
+ "pid": ticket.get("pid"),
278
+ "description": ticket.get("goal"),
279
+ "ingress": ticket.get("ingress"),
280
+ "created_at": ticket.get("created_at"),
281
+ }
282
+ )
283
+ except Exception as exc:
284
+ logger.debug(f"Could not read queued device tasks: {exc}")
285
+ return {"active": active, "queued": queued}
286
+
287
+
288
+ def _busy_step(tasks: dict[str, list[dict[str, Any]]], serial: str | None) -> str | None:
289
+ if not serial:
290
+ return None
291
+ wanted = _normalize_serial(serial)
292
+ for entry in tasks.get("active", []):
293
+ if _normalize_serial(entry.get("device")) != wanted:
294
+ continue
295
+ task_id = entry.get("session_id") or "unknown"
296
+ description = entry.get("description") or "no description"
297
+ return (
298
+ f"Device '{serial}' is busy with task {task_id} (pid {entry.get('pid')}): "
299
+ f"{description}. Wait for it to finish, or stop it with "
300
+ f'mobile_manage_task(action="stop", trace_id="{task_id}"). A new task on this '
301
+ "device queues behind it."
302
+ )
303
+ return None
304
+
305
+
306
+ # --------------------------------------------------------------------------- #
307
+ # Emulator launch
308
+ # --------------------------------------------------------------------------- #
309
+
310
+
311
+ def _emulator_status() -> dict[str, Any] | None:
312
+ """Current background launch state, or None when nothing was ever launched."""
313
+ try:
314
+ state = _compact_emulator(readiness_engine.get_emulator_status())
315
+ except Exception:
316
+ return None
317
+ if state.get("status") in (None, "idle"):
318
+ return None
319
+ return state
320
+
321
+
322
+ async def _handle_launch_avd(
323
+ name: str, adb_result: ProbeResult | None
324
+ ) -> tuple[dict[str, Any] | None, list[str]]:
325
+ """Validate the AVD name and start it once; returns (emulator state, next_steps lines)."""
326
+ installed = list((adb_result.metadata.get("installed_avds") if adb_result else None) or [])
327
+ current = _emulator_status()
328
+
329
+ if name not in installed:
330
+ listing = ", ".join(installed) or "none"
331
+ advice = (
332
+ "Pass one of them as launch_avd."
333
+ if installed
334
+ else "Create one in Android Studio's Device Manager or ask the user to connect a phone."
335
+ )
336
+ return current, [
337
+ f"[REQUIRED] AVD '{name}' is not installed, so nothing was launched. Installed AVDs: "
338
+ f"{listing}. {advice}"
339
+ ]
340
+
341
+ if _launch_in_progress(current):
342
+ assert current is not None
343
+ return current, [
344
+ f"Emulator '{current.get('avd_name')}' is already {current.get('status')} "
345
+ f"({current.get('stage_message')}; {current.get('elapsed_seconds') or 0}s elapsed); "
346
+ "a second launch was not started. Call mobile_diagnose again in about 60 seconds "
347
+ "without launch_avd."
348
+ ]
349
+
350
+ try:
351
+ state = _compact_emulator(await readiness_engine.launch_emulator(name))
352
+ except Exception as exc:
353
+ return current, [
354
+ f"[REQUIRED] Launching AVD '{name}' raised {exc.__class__.__name__}: {exc}"
355
+ ]
356
+ if state.get("status") == "failed":
357
+ return state, [
358
+ f"[REQUIRED] Emulator '{name}' failed to launch: "
359
+ f"{state.get('error') or state.get('stage_message')}."
360
+ ]
361
+ return state, [
362
+ f"Emulator '{name}' is starting in the background ({state.get('stage_message')}). Boot "
363
+ "takes 1-3 minutes: call mobile_diagnose again in about 60 seconds, and do not pass "
364
+ "launch_avd again while the emulator status is starting, waiting_for_adb or booting."
365
+ ]
366
+
367
+
368
+ # --------------------------------------------------------------------------- #
369
+ # Credential verification
370
+ # --------------------------------------------------------------------------- #
371
+
372
+
373
+ async def _verify_credentials(cred_result: ProbeResult | None) -> list[dict[str, Any]]:
374
+ """Check every configured key against its provider; the keys never leave this function."""
375
+ if cred_result is None:
376
+ return []
377
+ metadata = cred_result.metadata
378
+ api_keys = metadata.get("api_keys") or {}
379
+ targets: list[tuple[str, str, str]] = []
380
+ for entry in metadata.get("providers") or []:
381
+ provider = str(entry.get("provider") or "").strip()
382
+ raw_key = entry.get("raw_key") or api_keys.get(provider) or ""
383
+ if not provider or not raw_key:
384
+ continue
385
+ targets.append((provider, str(entry.get("label") or provider), str(raw_key)))
386
+ if api_keys.get("ocr"):
387
+ targets.append(("ocr", "Vision OCR", str(api_keys["ocr"])))
388
+ if not targets:
389
+ return []
390
+
391
+ outcomes = await asyncio.gather(
392
+ *(
393
+ validate_api_key(
394
+ provider,
395
+ "EMPTY",
396
+ base_url=key,
397
+ timeout=CREDENTIAL_CHECK_TIMEOUT_SECONDS,
398
+ )
399
+ if provider in _ENDPOINT_PROVIDERS
400
+ else validate_api_key(provider, key, timeout=CREDENTIAL_CHECK_TIMEOUT_SECONDS)
401
+ for provider, _label, key in targets
402
+ ),
403
+ return_exceptions=True,
404
+ )
405
+ verified: list[dict[str, Any]] = []
406
+ for (provider, label, key), outcome in zip(targets, outcomes):
407
+ if isinstance(outcome, BaseException):
408
+ valid, message = False, f"verification raised {outcome.__class__.__name__}: {outcome}"
409
+ else:
410
+ valid, message = bool(outcome[0]), str(outcome[1])
411
+ verified.append(
412
+ {
413
+ "provider": provider,
414
+ "label": label,
415
+ "valid": valid,
416
+ "message": message.replace(key, "***"),
417
+ }
418
+ )
419
+ return verified
420
+
421
+
422
+ def _primary_credential(credentials: list[dict[str, Any]] | None) -> dict[str, Any] | None:
423
+ """The provider the task runner will use: first configured LLM provider (OCR is auxiliary)."""
424
+ for entry in credentials or []:
425
+ if entry.get("provider") != "ocr":
426
+ return entry
427
+ return None
428
+
429
+
430
+ def _credential_verification_steps(
431
+ credentials: list[dict[str, Any]] | None, env_file: str | None
432
+ ) -> tuple[list[str], bool]:
433
+ """Lines for failed verifications; second item says whether a restart is needed."""
434
+ if not credentials:
435
+ return [], False
436
+ steps: list[str] = []
437
+ primary = _primary_credential(credentials)
438
+ needs_restart = False
439
+ for entry in credentials:
440
+ if entry.get("valid"):
441
+ continue
442
+ tag = "REQUIRED" if entry is primary else "OPTIONAL"
443
+ steps.append(
444
+ f"[{tag}] {entry.get('label')} API key ({entry.get('provider')}) failed live "
445
+ f"verification: {entry.get('message')}"
446
+ )
447
+ if entry is primary:
448
+ steps.extend(_credential_steps(env_file))
449
+ needs_restart = True
450
+ return steps, needs_restart
451
+
452
+
453
+ # --------------------------------------------------------------------------- #
454
+ # Device smoke test
455
+ # --------------------------------------------------------------------------- #
456
+
457
+
458
+ def _probe_unavailable(serial: str | None, error: str) -> dict[str, Any]:
459
+ return {
460
+ "ok": False,
461
+ "serial": serial,
462
+ "elapsed_seconds": 0.0,
463
+ "screenshot_bytes": None,
464
+ "element_count": None,
465
+ "error": error,
466
+ "fix": [],
467
+ }
468
+
469
+
470
+ async def _device_smoke_test(device_serial: str | None) -> dict[str, Any]:
471
+ """Drive the device end to end (screenshot + UI hierarchy) through the shared smoke test."""
472
+ return await smoke_test_device(device_serial)
473
+
474
+
475
+ async def _run_device_probe(
476
+ adb_result: ProbeResult | None, requested_device: str | None
477
+ ) -> dict[str, Any]:
478
+ devices = (adb_result.metadata.get("devices") if adb_result else None) or []
479
+ ready = [str(d.get("serial")) for d in devices if d.get("state") == "device"]
480
+ if requested_device and requested_device not in ready:
481
+ return _probe_unavailable(
482
+ requested_device,
483
+ f"requested device '{requested_device}' is not attached and authorized; nothing to probe",
484
+ )
485
+ if not ready:
486
+ return _probe_unavailable(None, "no authorized device is attached; nothing to probe")
487
+ serial = requested_device or (ready[0] if len(ready) == 1 else None)
488
+ try:
489
+ return await _device_smoke_test(serial)
490
+ except Exception as exc:
491
+ return _probe_unavailable(
492
+ serial, f"device smoke test raised {exc.__class__.__name__}: {exc}"
493
+ )
494
+
495
+
496
+ def _device_probe_steps(device_probe: dict[str, Any] | None) -> list[str]:
497
+ if device_probe is None or device_probe.get("ok"):
498
+ return []
499
+ serial = device_probe.get("serial") or "the attached device"
500
+ steps = [f"[REQUIRED] Device probe failed on {serial}: {device_probe.get('error')}"]
501
+ steps.extend(f" Guidance: {fix}" for fix in device_probe.get("fix") or [])
502
+ return steps
503
+
504
+
505
+ # --------------------------------------------------------------------------- #
506
+ # Accessibility helper (UI hierarchy backend)
507
+ # --------------------------------------------------------------------------- #
508
+
509
+
510
+ def _hierarchy_backend() -> str:
511
+ try:
512
+ from artemis.clients.screen_client_factory import resolve_backend
513
+
514
+ return resolve_backend().value
515
+ except (ImportError, ValueError):
516
+ return "auto"
517
+
518
+
519
+ def _helper_target(adb_result: ProbeResult | None, requested_device: str | None) -> str | None:
520
+ """The one ready device whose helper state is worth reporting, or None."""
521
+ devices = (adb_result.metadata.get("devices") if adb_result else None) or []
522
+ ready = [str(d.get("serial")) for d in devices if d.get("state") == "device"]
523
+ if requested_device:
524
+ return requested_device if requested_device in ready else None
525
+ return ready[0] if len(ready) == 1 else None
526
+
527
+
528
+ def _helper_status(serial: str) -> dict[str, Any]:
529
+ try:
530
+ status = helper_manager.status(serial)
531
+ except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as exc:
532
+ return {"serial": serial, "error": f"{exc.__class__.__name__}: {exc}"}
533
+ status["serial"] = serial
534
+ status["backend"] = _hierarchy_backend()
535
+ return status
536
+
537
+
538
+ def _helper_needs_provision(status: dict[str, Any] | None) -> bool:
539
+ if not status or status.get("error"):
540
+ return False
541
+ return not status.get("installed") or bool(status.get("outdated")) or not status.get("enabled")
542
+
543
+
544
+ def _helper_fix(
545
+ adb_result: ProbeResult | None, requested_device: str | None
546
+ ) -> dict[str, Any] | None:
547
+ """Install / upgrade / enable the helper on the idle target device (attempt_fix only)."""
548
+ serial = _helper_target(adb_result, requested_device)
549
+ if serial is None or _hierarchy_backend() == "uiautomator":
550
+ return None
551
+ status = _helper_status(serial)
552
+ if not _helper_needs_provision(status):
553
+ return None
554
+ if not status.get("bundled_apk_present"):
555
+ return {
556
+ "fix": "install_accessibility_helper",
557
+ "success": False,
558
+ "skipped": True,
559
+ "message": "Skipped: the bundled ArtemisAccessibilityHelper.apk is missing from the checkout.",
560
+ }
561
+ wanted = _normalize_serial(serial)
562
+ if any(_normalize_serial(entry.get("device")) == wanted for entry in _task_state()["active"]):
563
+ return {
564
+ "fix": "install_accessibility_helper",
565
+ "success": False,
566
+ "skipped": True,
567
+ "message": f"Skipped: a task is running on {serial}; install it after the task finishes.",
568
+ }
569
+ try:
570
+ result = helper_manager.provision(serial)
571
+ except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as exc:
572
+ return {
573
+ "fix": "install_accessibility_helper",
574
+ "success": False,
575
+ "skipped": False,
576
+ "message": f"provisioning raised {exc.__class__.__name__}: {exc}",
577
+ }
578
+ message = (
579
+ f"{result.action} (installed version {result.installed_version}, "
580
+ f"bundled {result.bundled_version}, service enabled={result.enabled})"
581
+ )
582
+ if result.error:
583
+ message += f": {result.error}"
584
+ return {
585
+ "fix": "install_accessibility_helper",
586
+ "success": result.ok,
587
+ "skipped": False,
588
+ "message": message,
589
+ }
590
+
591
+
592
+ def _helper_steps(status: dict[str, Any] | None, *, attempt_fix: bool) -> list[str]:
593
+ if not status:
594
+ return []
595
+ serial = status.get("serial") or "the device"
596
+ backend = status.get("backend") or "auto"
597
+ if backend == "uiautomator":
598
+ return []
599
+ if status.get("error"):
600
+ return [
601
+ f"[OPTIONAL] Accessibility helper state on {serial} could not be read: {status['error']}"
602
+ ]
603
+ healthy = status.get("installed") and not status.get("outdated") and status.get("enabled")
604
+ if healthy and status.get("reachable"):
605
+ return []
606
+ if not status.get("installed"):
607
+ problem = "is not installed"
608
+ elif status.get("outdated"):
609
+ problem = (
610
+ f"is outdated (device has version {status.get('installed_version')}, "
611
+ f"bundled is {status.get('bundled_version')})"
612
+ )
613
+ elif not status.get("enabled"):
614
+ problem = "is installed but its accessibility service is disabled"
615
+ elif status.get("newer_than_bundled") and status.get("reachable"):
616
+ return [] # a newer dev build is fine; nothing to do
617
+ else:
618
+ problem = "is installed and enabled but not answering on its loopback port"
619
+ tag = "REQUIRED" if backend == "helper" else "OPTIONAL"
620
+ consequence = (
621
+ "tasks cannot read the UI hierarchy until it is fixed"
622
+ if backend == "helper"
623
+ else "tasks fall back to UIAutomator2 (slower dumps, conflicts with Appium/Mobly)"
624
+ )
625
+ steps = [f"[{tag}] Accessibility helper on {serial} {problem}; {consequence}."]
626
+ if not status.get("bundled_apk_present"):
627
+ steps.append(
628
+ " Guidance: the bundled APK is missing; build it with "
629
+ "packages/artemis-accessibility-helper/build_apk.sh."
630
+ )
631
+ return steps
632
+ if problem.startswith("is installed and enabled"):
633
+ steps.append(
634
+ " Guidance: unlock the phone; if the helper still does not answer, reinstall it with "
635
+ "the command below (add --force)."
636
+ )
637
+ steps.append(f" Run: uv run artemis helper install --serial {serial} --force")
638
+ return steps
639
+ if not attempt_fix:
640
+ steps.append(
641
+ " Guidance: call mobile_diagnose(attempt_fix=true) to install and enable it "
642
+ "without touching the phone, or run the command below."
643
+ )
644
+ steps.append(f" Run: uv run artemis helper install --serial {serial}")
645
+ if problem.startswith("is installed but"):
646
+ # Enabling from adb was already attempted once by whoever installed it;
647
+ # on ROMs that reject it only a person can flip the switch.
648
+ from artemis.runtime.helper_manager import MANUAL_ENABLE_PATH
649
+
650
+ steps.append(
651
+ " Guidance: if the command reports that this device rejected enabling the "
652
+ f"service, ask the user to turn it on by hand: {MANUAL_ENABLE_PATH} "
653
+ "(the command opens that settings screen on the phone)."
654
+ )
655
+ return steps
656
+
657
+
658
+ # --------------------------------------------------------------------------- #
659
+ # next_steps
660
+ # --------------------------------------------------------------------------- #
661
+
662
+
663
+ def _device_steps(
664
+ adb_result: ProbeResult | None,
665
+ *,
666
+ attempt_fix: bool,
667
+ requested_device: str | None,
668
+ emulator: dict[str, Any] | None,
669
+ launch_requested: bool,
670
+ tasks: dict[str, list[dict[str, Any]]],
671
+ ) -> list[str]:
672
+ steps: list[str] = []
673
+ if adb_result is None or not adb_result.metadata.get("installed"):
674
+ return steps
675
+ devices = adb_result.metadata.get("devices") or []
676
+ ready = [d for d in devices if d.get("state") == "device"]
677
+ keys = adb_result.metadata.get("adb_keys") or {}
678
+ installed_avds = [str(a) for a in adb_result.metadata.get("installed_avds") or []]
679
+ in_progress = _launch_in_progress(emulator)
680
+
681
+ if requested_device and requested_device not in {d.get("serial") for d in devices}:
682
+ steps.append(
683
+ f"[REQUIRED] Requested device '{requested_device}' is not attached. Attached: "
684
+ + (", ".join(f"{d.get('serial')} ({d.get('state')})" for d in devices) or "none")
685
+ + ". Ask the user to connect it or pick another serial."
686
+ )
687
+ elif requested_device and not any(d.get("serial") == requested_device for d in ready):
688
+ steps.append(
689
+ f"[REQUIRED] Requested device '{requested_device}' is attached but not authorized "
690
+ "(state is not 'device'). Ask the user to accept the USB debugging prompt on it."
691
+ )
692
+
693
+ target = requested_device or (str(ready[0].get("serial")) if len(ready) == 1 else None)
694
+ busy = _busy_step(tasks, target)
695
+ if busy:
696
+ steps.append(busy)
697
+
698
+ if not ready and not in_progress and not launch_requested and installed_avds:
699
+ steps.append(
700
+ f'No device is ready. Call mobile_diagnose(launch_avd="{installed_avds[0]}") to boot '
701
+ f"an installed emulator in the background (installed: {', '.join(installed_avds)}), "
702
+ "or ask the user to connect a phone."
703
+ )
704
+ if len(ready) > 1 and not requested_device:
705
+ steps.append(
706
+ "Several devices are ready: ask the user which serial to use and pass it as "
707
+ "device_serial. Ready: " + ", ".join(str(d.get("serial")) for d in ready) + "."
708
+ )
709
+ if (
710
+ not attempt_fix
711
+ and not in_progress
712
+ and not launch_requested
713
+ and (keys.get("is_corrupted") or not ready)
714
+ ):
715
+ steps.append(
716
+ "Call mobile_diagnose(attempt_fix=true) to let Artemis heal corrupted ADB keys and "
717
+ "restart the ADB server before asking the user to do anything manually."
718
+ )
719
+ return steps
720
+
721
+
722
+ def _next_steps(
723
+ results: list[ProbeResult],
724
+ *,
725
+ attempt_fix: bool,
726
+ fixes_applied: list[dict[str, Any]],
727
+ requested_device: str | None,
728
+ env_file: str | None,
729
+ emulator: dict[str, Any] | None,
730
+ launch_steps: list[str],
731
+ credentials: list[dict[str, Any]] | None,
732
+ tasks: dict[str, list[dict[str, Any]]],
733
+ device_probe: dict[str, Any] | None,
734
+ accessibility_helper: dict[str, Any] | None = None,
735
+ ) -> list[str]:
736
+ steps: list[str] = []
737
+ needs_restart = False
738
+ adb_result = _find(results, "android_adb")
739
+ installed_avds = [
740
+ str(a) for a in (adb_result.metadata.get("installed_avds") if adb_result else None) or []
741
+ ]
742
+ in_progress = _launch_in_progress(emulator)
743
+ credential_lines, credential_restart = _credential_verification_steps(credentials, env_file)
744
+
745
+ for result in sort_by_fix_order(results):
746
+ if result.status is not ProbeStatus.PASS:
747
+ tag = "REQUIRED" if result.is_blocker else "OPTIONAL"
748
+ steps.append(f"[{tag}] {result.title}: {result.description}")
749
+
750
+ if result.id == "gemini_api_key" and result.status is ProbeStatus.FAIL:
751
+ steps.extend(_credential_steps(env_file))
752
+ needs_restart = True
753
+ elif result.id == "android_adb" and in_progress and emulator is not None:
754
+ devices = result.metadata.get("devices") or []
755
+ if not any(d.get("state") == "device" for d in devices):
756
+ steps.append(
757
+ f" Guidance: Emulator '{emulator.get('avd_name')}' is "
758
+ f"{emulator.get('status')} ({emulator.get('stage_message')}; "
759
+ f"{emulator.get('elapsed_seconds') or 0}s elapsed). Wait about 60 "
760
+ "seconds and call mobile_diagnose again instead of asking the user to "
761
+ "connect a device."
762
+ )
763
+ else:
764
+ for action in result.actions:
765
+ steps.extend(_render_action_lines(action, installed_avds=installed_avds))
766
+ else:
767
+ for action in result.actions:
768
+ steps.extend(_render_action_lines(action, installed_avds=installed_avds))
769
+ if result.id in ("system_config", "integration_host", "python_runtime"):
770
+ needs_restart = True
771
+
772
+ if result.id == "gemini_api_key":
773
+ steps.extend(credential_lines)
774
+ needs_restart = needs_restart or credential_restart
775
+
776
+ steps.extend(
777
+ _device_steps(
778
+ adb_result,
779
+ attempt_fix=attempt_fix,
780
+ requested_device=requested_device,
781
+ emulator=emulator,
782
+ launch_requested=bool(launch_steps),
783
+ tasks=tasks,
784
+ )
785
+ )
786
+ steps.extend(launch_steps)
787
+ steps.extend(_device_probe_steps(device_probe))
788
+ steps.extend(_helper_steps(accessibility_helper, attempt_fix=attempt_fix))
789
+
790
+ for fix in fixes_applied:
791
+ outcome = "applied" if fix.get("success") else "did not help"
792
+ steps.append(f"Auto-fix {fix['fix']} {outcome}: {fix.get('message', '')}".rstrip())
793
+
794
+ if not steps:
795
+ steps.append(
796
+ "Environment is ready. Use mobile_run_task to delegate a task; pass device_serial "
797
+ "when more than one device is attached."
798
+ )
799
+ return steps
800
+
801
+ if needs_restart:
802
+ steps.append(
803
+ "After changing the env file, config, or MCP server config: restart the MCP server "
804
+ "(reload MCP servers in the IDE), then call mobile_diagnose again."
805
+ )
806
+ else:
807
+ steps.append("After each fix, call mobile_diagnose again until verdict is 'ready'.")
808
+ return steps
809
+
810
+
811
+ # --------------------------------------------------------------------------- #
812
+ # Logs
813
+ # --------------------------------------------------------------------------- #
814
+
815
+
816
+ def _tail_errors(path: str, *, max_lines: int = 12, max_chars: int = 300) -> list[str]:
817
+ """Return the last error-looking lines of a log without loading the whole file."""
818
+ try:
819
+ size = os.path.getsize(path)
820
+ with open(path, "rb") as handle:
821
+ handle.seek(max(0, size - 64 * 1024))
822
+ text = handle.read().decode("utf-8", errors="replace")
823
+ except OSError:
824
+ return []
825
+ matches = [
826
+ _ANSI_RE.sub("", line).strip()[:max_chars]
827
+ for line in text.splitlines()
828
+ if any(marker in line.lower() for marker in _ERROR_MARKERS)
829
+ ]
830
+ return matches[-max_lines:]
831
+
832
+
833
+ def _last_failed_task(limit: int = 30) -> dict[str, Any] | None:
834
+ """Locate the most recent failed trace and surface its error tail inline."""
835
+ traces_dir = Path(trace_store.TRACES_DIR)
836
+ try:
837
+ candidates = [p for p in traces_dir.iterdir() if p.is_dir()]
838
+ except OSError:
839
+ return None
840
+
841
+ def _mtime(path: Path) -> float:
842
+ try:
843
+ return path.stat().st_mtime
844
+ except OSError:
845
+ return 0.0
846
+
847
+ candidates.sort(key=_mtime, reverse=True)
848
+ for trace_dir in candidates[:limit]:
849
+ status = trace_store.read_status(trace_dir.name)
850
+ if not status or status.get("status") != "failed":
851
+ continue
852
+ stderr_log = trace_store.get_trace_stderr_log_path(trace_dir.name)
853
+ return {
854
+ "trace_id": trace_dir.name,
855
+ "error": (status.get("error") or "")[:600],
856
+ "device_serial": status.get("device_serial"),
857
+ "stderr_log": stderr_log,
858
+ "stdout_log": trace_store.get_trace_stdout_log_path(trace_dir.name),
859
+ "recent_errors": _tail_errors(stderr_log),
860
+ }
861
+ return None
862
+
863
+
864
+ def _collect_logs(host_metadata: dict[str, Any]) -> dict[str, Any]:
865
+ root = env_utils.get_project_root()
866
+ stderr_log = os.path.join(root, "scratch", "mcp_stderr.log")
867
+ return {
868
+ "mcp_stderr_log": stderr_log,
869
+ "mcp_launch_log": os.path.join(root, "scratch", "mcp_launch_debug.log"),
870
+ "daemon_log": (host_metadata.get("daemon") or {}).get("log_path"),
871
+ "recent_mcp_errors": _tail_errors(stderr_log),
872
+ "last_failed_task": _last_failed_task(),
873
+ }
874
+
875
+
876
+ # --------------------------------------------------------------------------- #
877
+ # Self-heal
878
+ # --------------------------------------------------------------------------- #
879
+
880
+
881
+ def _public_fix_result(name: str, result: dict[str, Any]) -> dict[str, Any]:
882
+ return {
883
+ "fix": name,
884
+ "success": bool(result.get("success")),
885
+ "message": str(result.get("message") or ""),
886
+ "skipped": bool(result.get("skipped")),
887
+ }
888
+
889
+
890
+ def _cleanup_stale_locks() -> dict[str, Any] | None:
891
+ """Drop lock files / queue tickets of dead processes; reported only when something changed."""
892
+ try:
893
+ removed = int(DeviceExecutionLock.cleanup_stale_locks())
894
+ except Exception as exc:
895
+ return {
896
+ "fix": "cleanup_stale_locks",
897
+ "success": False,
898
+ "skipped": False,
899
+ "message": f"cleanup raised {exc.__class__.__name__}: {exc}",
900
+ }
901
+ if removed <= 0:
902
+ return None
903
+ return {
904
+ "fix": "cleanup_stale_locks",
905
+ "success": True,
906
+ "skipped": False,
907
+ "message": f"removed {removed} stale device lock(s) / queue ticket(s)",
908
+ }
909
+
910
+
911
+ async def _apply_safe_fixes(
912
+ report: SystemReadinessReport, requested_device: str | None = None
913
+ ) -> list[dict[str, Any]]:
914
+ """Run the self-heals the console exposes as buttons, only when they cannot disrupt a task."""
915
+ applied: list[dict[str, Any]] = []
916
+ cleanup = _cleanup_stale_locks()
917
+ if cleanup is not None:
918
+ applied.append(cleanup)
919
+
920
+ adb = next((p for p in report.probes if p.id == "android_adb"), None)
921
+ if adb is None or not adb.metadata.get("installed"):
922
+ return applied
923
+
924
+ if adb_keys_corrupted(report.probes):
925
+ result = await readiness_engine.heal_adb_keys()
926
+ applied.append(_public_fix_result("heal_adb_keys", result))
927
+ return applied # healing already restarted the ADB server
928
+
929
+ devices = adb.metadata.get("devices") or []
930
+ if any(d.get("state") == "device" for d in devices):
931
+ # A ready device exists; nothing to restart. Provision the accessibility
932
+ # helper on it when it is idle and missing, outdated or disabled.
933
+ helper_fix = await asyncio.to_thread(_helper_fix, adb, requested_device)
934
+ if helper_fix is not None:
935
+ applied.append(helper_fix)
936
+ return applied
937
+ if DeviceExecutionLock.get_active_owners():
938
+ applied.append(
939
+ {
940
+ "fix": "restart_adb_server",
941
+ "success": False,
942
+ "skipped": True,
943
+ "message": "Skipped: an Artemis task currently holds a device lock.",
944
+ }
945
+ )
946
+ return applied
947
+ result = await readiness_engine.restart_adb_server()
948
+ applied.append(_public_fix_result("restart_adb_server", result))
949
+ return applied
950
+
951
+
952
+ def _probes_changed(fixes_applied: list[dict[str, Any]]) -> bool:
953
+ """Only ADB-side fixes can change probe results; lock cleanup does not warrant a re-run."""
954
+ return any(
955
+ fix.get("success")
956
+ and not fix.get("skipped")
957
+ and fix.get("fix") not in ("cleanup_stale_locks", "install_accessibility_helper")
958
+ for fix in fixes_applied
959
+ )
960
+
961
+
962
+ # --------------------------------------------------------------------------- #
963
+ # Collection / verdict
964
+ # --------------------------------------------------------------------------- #
965
+
966
+
967
+ async def _none() -> None:
968
+ return None
969
+
970
+
971
+ async def _run_extras(
972
+ results: list[ProbeResult],
973
+ *,
974
+ requested_device: str | None,
975
+ launch_avd: str | None,
976
+ verify_credentials: bool,
977
+ probe_device: bool,
978
+ ) -> dict[str, Any]:
979
+ """Optional, slower work: emulator launch, live key checks, device smoke test, lock state."""
980
+ adb_result = _find(results, "android_adb")
981
+ launch_steps: list[str] = []
982
+ if launch_avd:
983
+ emulator, launch_steps = await _handle_launch_avd(launch_avd, adb_result)
984
+ else:
985
+ emulator = _emulator_status()
986
+
987
+ helper_serial = _helper_target(adb_result, requested_device)
988
+ credentials, device_probe, accessibility_helper = await asyncio.gather(
989
+ _verify_credentials(_find(results, "gemini_api_key")) if verify_credentials else _none(),
990
+ _run_device_probe(adb_result, requested_device) if probe_device else _none(),
991
+ asyncio.to_thread(_helper_status, helper_serial) if helper_serial else _none(),
992
+ )
993
+ return {
994
+ "emulator": emulator,
995
+ "launch_steps": launch_steps,
996
+ "credentials": credentials,
997
+ "device_probe": device_probe,
998
+ "accessibility_helper": accessibility_helper,
999
+ "tasks": _task_state(),
1000
+ }
1001
+
1002
+
1003
+ def _requested_device_ready(results: list[ProbeResult], requested_device: str | None) -> bool:
1004
+ """A caller-named device counts as a blocker: attached and authorized, or not ready."""
1005
+ if not requested_device:
1006
+ return True
1007
+ adb_result = _find(results, "android_adb")
1008
+ if adb_result is None:
1009
+ return False
1010
+ devices = adb_result.metadata.get("devices") or []
1011
+ return any(d.get("serial") == requested_device and d.get("state") == "device" for d in devices)
1012
+
1013
+
1014
+ def _verdict(
1015
+ results: list[ProbeResult],
1016
+ *,
1017
+ requested_device: str | None,
1018
+ credentials: list[dict[str, Any]] | None,
1019
+ device_probe: dict[str, Any] | None,
1020
+ accessibility_helper: dict[str, Any] | None = None,
1021
+ ) -> str:
1022
+ verdict = base_verdict(results)
1023
+ if verdict == "blocked" or not _requested_device_ready(results, requested_device):
1024
+ return "blocked"
1025
+ primary = _primary_credential(credentials)
1026
+ if primary is not None and not primary.get("valid"):
1027
+ return "blocked"
1028
+ if device_probe is not None and not device_probe.get("ok"):
1029
+ return "blocked"
1030
+ helper_backend = (accessibility_helper or {}).get("backend")
1031
+ if helper_backend == "helper" and not accessibility_helper.get("reachable"):
1032
+ return "blocked"
1033
+ if any(not entry.get("valid") for entry in credentials or []):
1034
+ return "degraded"
1035
+ if helper_backend == "auto" and _helper_needs_provision(accessibility_helper):
1036
+ return "degraded" if verdict == "ready" else verdict
1037
+ return verdict
1038
+
1039
+
1040
+ def _summary(
1041
+ results: list[ProbeResult],
1042
+ verdict: str,
1043
+ *,
1044
+ credentials: list[dict[str, Any]] | None,
1045
+ device_probe: dict[str, Any] | None,
1046
+ ) -> str:
1047
+ blockers = [r for r in results if r.is_blocker]
1048
+ passed = [r for r in blockers if r.status is ProbeStatus.PASS]
1049
+ line = f"{len(passed)}/{len(blockers)} required checks pass ({verdict})."
1050
+ failing = [
1051
+ f"{r.title}: {r.summary}"
1052
+ for r in sort_by_fix_order(results)
1053
+ if r.status is not ProbeStatus.PASS
1054
+ ]
1055
+ failing.extend(
1056
+ f"{entry.get('label')} key verification: {entry.get('message')}"
1057
+ for entry in credentials or []
1058
+ if not entry.get("valid")
1059
+ )
1060
+ if device_probe is not None and not device_probe.get("ok"):
1061
+ failing.append(f"Device probe: {device_probe.get('error')}")
1062
+ if failing:
1063
+ line += " Attention: " + "; ".join(failing) + "."
1064
+ return line
1065
+
1066
+
1067
+ def _timeout_response(fixes_applied: list[dict[str, Any]]) -> dict[str, Any]:
1068
+ return {
1069
+ "verdict": "blocked",
1070
+ "summary": (
1071
+ f"Diagnosis did not finish within {DIAGNOSIS_TIMEOUT_SECONDS:.0f}s; the ADB "
1072
+ "server, the device, or the toolchain scan is hung."
1073
+ ),
1074
+ "next_steps": [
1075
+ "Run: adb kill-server",
1076
+ "Run: adb start-server",
1077
+ "Then call mobile_diagnose again. If it hangs again, ask the user to unplug and "
1078
+ "replug the device or restart the emulator.",
1079
+ ],
1080
+ "checks": [],
1081
+ "host": {},
1082
+ "device": None,
1083
+ "emulator": _emulator_status(),
1084
+ "tasks": _task_state(),
1085
+ "credentials": None,
1086
+ "device_probe": None,
1087
+ "fixes_applied": fixes_applied,
1088
+ "logs": _collect_logs({}),
1089
+ }
1090
+
1091
+
1092
+ @mcp.tool()
1093
+ async def mobile_diagnose(
1094
+ attempt_fix: bool = False,
1095
+ device_serial: str | None = None,
1096
+ launch_avd: str | None = None,
1097
+ verify_credentials: bool = False,
1098
+ probe_device: bool = False,
1099
+ ) -> dict[str, Any]:
1100
+ """Diagnoses why ARTEMIS cannot run tasks from this IDE and returns the fixes.
1101
+
1102
+ Call this FIRST whenever another ARTEMIS tool errors, a task fails to
1103
+ start, no device is found, or the user says ARTEMIS "does not work".
1104
+ It checks, in fix order: Python runtime, config file, the MCP host
1105
+ (interpreter vs project venv, .env location, traces directory, daemon
1106
+ port), LLM credentials, ADB + devices (authorization, lock screen, RSA
1107
+ keys, emulators), and the optional video toolchain. A plain call takes a
1108
+ few seconds; the optional extras cost more (see Args).
1109
+
1110
+ Returns a dict:
1111
+ - `verdict`: "ready" | "degraded" (optional pieces missing) | "blocked".
1112
+ - `summary`: one line with the pass count and what needs attention.
1113
+ - `next_steps`: ordered instructions. `[REQUIRED]`/`[OPTIONAL]` lines
1114
+ name a problem; the indented lines under them are the fix. `Run:`
1115
+ lines are single, local, non-destructive shell commands you may
1116
+ execute yourself, one per line (ask before installing software).
1117
+ `Guidance:` lines need the user, explain a setting, or tell you
1118
+ which mobile_diagnose call to make next. Follow them top to bottom,
1119
+ then call this tool again until `verdict` is "ready".
1120
+ - `checks`: per-check status. Passing checks are one line (id, title,
1121
+ status, required, summary); failing ones add detail, fix, facts.
1122
+ Secrets are never included.
1123
+ - `host`: how the MCP server was launched: server_python,
1124
+ runner_python, interpreter_matches_venv, env_file, env_file_exists,
1125
+ traces_dir, mcp_client (when known), daemon {port, reachable,
1126
+ port_held_by_other_process, log_path}.
1127
+ - `device`: the device a task would use ({serial, state, model,
1128
+ android_version, is_locked, is_emulator, accessibility_helper}) or
1129
+ null. `accessibility_helper` describes the Artemis UI-hierarchy
1130
+ helper APK on that device ({installed, installed_version,
1131
+ bundled_version, outdated, enabled, forward_port, reachable,
1132
+ backend}); with backend "auto" a missing helper only degrades
1133
+ (UIAutomator2 fallback), with backend "helper" it blocks.
1134
+ - `emulator`: background emulator launch state ({avd_name, status,
1135
+ stage_message, error, serial, elapsed_seconds, progress_percent}) or
1136
+ null when nothing was launched. `status` is starting /
1137
+ waiting_for_adb / booting while it boots, then ready or failed.
1138
+ - `tasks`: {"active": [...], "queued": [...]} ARTEMIS tasks holding or
1139
+ waiting for a device (device, session_id, pid, description, ingress,
1140
+ started_at/created_at). Stop a stuck one with
1141
+ mobile_manage_task(action="stop", trace_id=<session_id>).
1142
+ - `credentials`: null unless verify_credentials; else
1143
+ [{provider, label, valid, message}] per configured key.
1144
+ - `device_probe`: null unless probe_device; else {ok, serial,
1145
+ elapsed_seconds, screenshot_bytes, element_count, error, fix}.
1146
+ - `fixes_applied`: what `attempt_fix` did ({fix, success, skipped, message}).
1147
+ - `logs`: paths to the MCP server logs and the daemon log, recent
1148
+ error lines, and the most recent failed task with its error, log
1149
+ paths and `recent_errors` (tail of its stderr log).
1150
+
1151
+ Args:
1152
+ attempt_fix: When true, applies the safe self-heals the ARTEMIS
1153
+ console offers: remove device locks left by dead processes,
1154
+ regenerate corrupted ADB RSA keys, restart the ADB server (only
1155
+ when no device is ready and no task holds a device), and install,
1156
+ upgrade or enable the Artemis accessibility helper APK on the idle
1157
+ target device when it is missing, outdated or disabled. Then
1158
+ re-runs the checks. Nothing else is changed.
1159
+ device_serial: Optional serial the user wants to use; the report
1160
+ then states explicitly whether that device is attached, authorized
1161
+ and idle.
1162
+ launch_avd: Name of an installed Android Virtual Device to boot in
1163
+ the background (pick it from the `next_steps` guidance or the
1164
+ android_adb facts `installed_avds`). Returns immediately; boot
1165
+ takes 1-3 minutes, so call mobile_diagnose again (without
1166
+ launch_avd) after about 60 seconds and watch `emulator.status`.
1167
+ Never pass it while the status is starting/waiting_for_adb/booting.
1168
+ Use it when no device is attached and an AVD exists instead of
1169
+ asking the user to start an emulator.
1170
+ verify_credentials: When true, checks every configured API key
1171
+ against its provider over the network (about 12 seconds worst
1172
+ case, keys stay on the server). Use it when tasks fail with
1173
+ authentication, quota or model errors although the key check
1174
+ passes. An invalid primary key makes the verdict "blocked".
1175
+ probe_device: When true, drives the attached device end to end
1176
+ (screenshot + UI hierarchy, about 20 seconds) to prove a task can
1177
+ really start. Use it when the checks pass but tasks still fail on
1178
+ the device, or the screen stays black. A failed probe makes the
1179
+ verdict "blocked" and lists the fix.
1180
+ """
1181
+ fixes_applied: list[dict[str, Any]] = []
1182
+ requested_device = device_serial.strip() if device_serial and device_serial.strip() else None
1183
+ avd_name = launch_avd.strip() if launch_avd and launch_avd.strip() else None
1184
+ try:
1185
+ report, host = await asyncio.wait_for(
1186
+ collect_readiness(), timeout=DIAGNOSIS_TIMEOUT_SECONDS
1187
+ )
1188
+ if attempt_fix:
1189
+ fixes_applied = await asyncio.wait_for(
1190
+ _apply_safe_fixes(report, requested_device), timeout=DIAGNOSIS_TIMEOUT_SECONDS
1191
+ )
1192
+ if _probes_changed(fixes_applied):
1193
+ report, host = await asyncio.wait_for(
1194
+ collect_readiness(), timeout=DIAGNOSIS_TIMEOUT_SECONDS
1195
+ )
1196
+ results = [*report.probes, host]
1197
+ extras = await asyncio.wait_for(
1198
+ _run_extras(
1199
+ results,
1200
+ requested_device=requested_device,
1201
+ launch_avd=avd_name,
1202
+ verify_credentials=verify_credentials,
1203
+ probe_device=probe_device,
1204
+ ),
1205
+ timeout=DIAGNOSIS_TIMEOUT_SECONDS,
1206
+ )
1207
+ except TimeoutError:
1208
+ return _timeout_response(fixes_applied)
1209
+
1210
+ credentials: list[dict[str, Any]] | None = extras["credentials"]
1211
+ device_probe: dict[str, Any] | None = extras["device_probe"]
1212
+ accessibility_helper: dict[str, Any] | None = extras.get("accessibility_helper")
1213
+ verdict = _verdict(
1214
+ results,
1215
+ requested_device=requested_device,
1216
+ credentials=credentials,
1217
+ device_probe=device_probe,
1218
+ accessibility_helper=accessibility_helper,
1219
+ )
1220
+ device = _compact_device(report)
1221
+ if device is not None and accessibility_helper is not None:
1222
+ device["accessibility_helper"] = accessibility_helper
1223
+ env_file = host.metadata.get("env_file")
1224
+ return {
1225
+ "verdict": verdict,
1226
+ "summary": _summary(results, verdict, credentials=credentials, device_probe=device_probe),
1227
+ "next_steps": _next_steps(
1228
+ results,
1229
+ attempt_fix=attempt_fix,
1230
+ fixes_applied=fixes_applied,
1231
+ requested_device=requested_device,
1232
+ env_file=env_file,
1233
+ emulator=extras["emulator"],
1234
+ launch_steps=extras["launch_steps"],
1235
+ credentials=credentials,
1236
+ tasks=extras["tasks"],
1237
+ device_probe=device_probe,
1238
+ accessibility_helper=accessibility_helper,
1239
+ ),
1240
+ "checks": [_render_check(r) for r in sort_by_fix_order(results)],
1241
+ "host": _compact_host(host.metadata),
1242
+ "device": device,
1243
+ "emulator": extras["emulator"],
1244
+ "tasks": extras["tasks"],
1245
+ "credentials": credentials,
1246
+ "device_probe": device_probe,
1247
+ "fixes_applied": fixes_applied,
1248
+ "logs": _collect_logs(host.metadata),
1249
+ }