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,438 @@
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
+ """System Readiness & Diagnostic Orchestration Engine."""
16
+
17
+ import asyncio
18
+ import os
19
+ import subprocess
20
+ import time
21
+ from typing import Any
22
+
23
+ from artemis.core.diagnostics.adb_server_connection import adb_server_connection
24
+ from artemis.core.diagnostics.probes.adb_probe import AdbDeviceProbe
25
+ from artemis.core.diagnostics.probes.base import BaseProbe
26
+ from artemis.core.diagnostics.probes.credentials_probe import (
27
+ LLMCredentialsProbe,
28
+ VisionOCRProbe,
29
+ )
30
+ from artemis.core.diagnostics.probes.runtime_probe import (
31
+ PythonRuntimeProbe,
32
+ SystemConfigProbe,
33
+ )
34
+ from artemis.core.diagnostics.probes.toolchain_probe import ToolchainProbe
35
+ from artemis.core.diagnostics.schema import (
36
+ DeviceInfo,
37
+ ProbeAction,
38
+ ProbeCategory,
39
+ ProbeResult,
40
+ ProbeStatus,
41
+ SystemReadinessReport,
42
+ )
43
+ from artemis.toolchain import toolchain
44
+ from artemis.platform import platform
45
+ from third_party.mobile_use.utils.logger import get_logger
46
+
47
+ logger = get_logger(__name__)
48
+
49
+
50
+ class ReadinessEngine:
51
+ """Central orchestration engine executing modular readiness probes."""
52
+
53
+ _REPORT_CACHE_TTL_SECONDS = 2.0
54
+
55
+ #: Deadline for one probe inside a report. The ADB probe shells out to
56
+ #: ``adb`` without a deadline of its own, and a wedged ADB server would
57
+ #: otherwise hang every surface that runs the report (``artemis doctor``,
58
+ #: the console wizard, ``mobile_diagnose``). A probe that overruns is
59
+ #: reported as a FAIL with the recovery steps instead.
60
+ PROBE_TIMEOUT_SECONDS = 30.0
61
+
62
+ def __init__(self):
63
+ self._probes: dict[str, BaseProbe] = {}
64
+ self._python_probe = PythonRuntimeProbe()
65
+ self._config_probe = SystemConfigProbe()
66
+ self._toolchain_probe = ToolchainProbe()
67
+ self._credentials_probe = LLMCredentialsProbe()
68
+ self._ocr_probe = VisionOCRProbe()
69
+ self._adb_probe = AdbDeviceProbe()
70
+ self._report_cache: SystemReadinessReport | None = None
71
+ self._report_cache_time = 0.0
72
+ self._report_cache_generation = -1
73
+ self._cache_generation = 0
74
+ self._report_lock = asyncio.Lock()
75
+
76
+ # Register default core probes in logical lifecycle order
77
+ self.register_probe(self._python_probe)
78
+ self.register_probe(self._config_probe)
79
+ self.register_probe(self._toolchain_probe)
80
+ self.register_probe(self._credentials_probe)
81
+ self.register_probe(self._ocr_probe)
82
+ self.register_probe(self._adb_probe)
83
+
84
+ def register_probe(self, probe: BaseProbe) -> None:
85
+ """Register a new diagnostic probe."""
86
+ self._probes[probe.probe_id] = probe
87
+
88
+ def unregister_probe(self, probe_id: str) -> None:
89
+ """Remove a diagnostic probe."""
90
+ self._probes.pop(probe_id, None)
91
+
92
+ def set_probe_target_serial(self, serial: str | None) -> None:
93
+ """Set the diagnostics probes' preferred device serial.
94
+
95
+ This is a probe/report preference only (which device the readiness
96
+ report highlights and verifies first). Task routing never reads it
97
+ back: execution targets come from explicit request serials or the
98
+ device pool. Pass None to clear the preference, e.g. after switching
99
+ ADB server endpoints.
100
+ """
101
+ self.invalidate_cache()
102
+ if hasattr(self._adb_probe, "set_target_serial"):
103
+ self._adb_probe.set_target_serial(serial)
104
+
105
+ async def run_probe(self, probe_id: str) -> ProbeResult | None:
106
+ """Execute a single specific probe by ID."""
107
+ probe = self._probes.get(probe_id)
108
+ if not probe:
109
+ return None
110
+ return await probe.probe()
111
+
112
+ async def run_device_submission_probe(self, target_serial: str | None = None) -> ProbeResult:
113
+ """Run the bounded device gate used by task submission."""
114
+ return await self._adb_probe.probe_submission_readiness(target_serial=target_serial)
115
+
116
+ def invalidate_cache(self) -> None:
117
+ """Invalidate the UI readiness snapshot after an explicit configuration change."""
118
+ self._cache_generation += 1
119
+ self._report_cache = None
120
+ self._report_cache_time = 0.0
121
+ self._report_cache_generation = -1
122
+
123
+ def _cached_report(self, max_age_seconds: float) -> SystemReadinessReport | None:
124
+ if self._report_cache is None:
125
+ return None
126
+ if self._report_cache_generation != self._cache_generation:
127
+ return None
128
+ if time.monotonic() - self._report_cache_time > max_age_seconds:
129
+ return None
130
+ return self._report_cache.model_copy(deep=True)
131
+
132
+ async def run_all(
133
+ self,
134
+ categories: list[ProbeCategory] | None = None,
135
+ *,
136
+ force_refresh: bool = False,
137
+ ) -> SystemReadinessReport:
138
+ """Return a coalesced readiness snapshot.
139
+
140
+ The dashboard polls frequently, so allowing every HTTP request to launch a
141
+ complete toolchain and ADB scan creates a request storm. Full reports are
142
+ cached briefly and all concurrent refreshes share one execution. The task
143
+ submission gate remains independent and always performs its own bounded
144
+ device check.
145
+ """
146
+ cacheable = categories is None
147
+ request_started = time.monotonic()
148
+ if cacheable and not force_refresh:
149
+ cached = self._cached_report(self._REPORT_CACHE_TTL_SECONDS)
150
+ if cached is not None:
151
+ return cached
152
+
153
+ async with self._report_lock:
154
+ # A refresh that completed while this caller waited satisfies even a
155
+ # forced request that began before it, coalescing concurrent clicks.
156
+ if cacheable and self._report_cache_time >= request_started:
157
+ cached = self._cached_report(float("inf"))
158
+ if cached is not None:
159
+ return cached
160
+ if cacheable and not force_refresh:
161
+ cached = self._cached_report(self._REPORT_CACHE_TTL_SECONDS)
162
+ if cached is not None:
163
+ return cached
164
+
165
+ if force_refresh:
166
+ toolchain.clear_cache()
167
+ self._adb_probe.invalidate_enrichment_cache()
168
+
169
+ build_generation = self._cache_generation
170
+ report = await self._build_report(categories)
171
+ # If a device/configuration change happened during the scan, return
172
+ # this result only to its original caller and never publish it as the
173
+ # shared snapshot for later requests.
174
+ if cacheable and build_generation == self._cache_generation:
175
+ self._report_cache = report.model_copy(deep=True)
176
+ self._report_cache_time = time.monotonic()
177
+ self._report_cache_generation = build_generation
178
+ return report
179
+
180
+ async def _build_report(
181
+ self, categories: list[ProbeCategory] | None = None
182
+ ) -> SystemReadinessReport:
183
+ """Execute the underlying probes and compile an uncached report."""
184
+ target_probes = [
185
+ probe
186
+ for probe in self._probes.values()
187
+ if categories is None or probe.category in categories
188
+ ]
189
+
190
+ # Concurrently execute probes. A probe that raises must not take the
191
+ # whole report down with it: it becomes a structured FAIL so the
192
+ # remaining probes still reach the user and the verdict stays honest.
193
+ outcomes = await asyncio.gather(
194
+ *[
195
+ asyncio.wait_for(probe.probe(), timeout=self.PROBE_TIMEOUT_SECONDS)
196
+ for probe in target_probes
197
+ ],
198
+ return_exceptions=True,
199
+ )
200
+ results: list[ProbeResult] = [
201
+ outcome
202
+ if isinstance(outcome, ProbeResult)
203
+ else self._crashed_probe_result(probe, outcome)
204
+ for probe, outcome in zip(target_probes, outcomes, strict=True)
205
+ ]
206
+
207
+ blockers = [r for r in results if r.is_blocker]
208
+ passed_blockers = [r for r in blockers if r.status == ProbeStatus.PASS]
209
+ overall_ready = len(blockers) > 0 and len(blockers) == len(passed_blockers)
210
+
211
+ # Extract active device info from ADB probe metadata if available
212
+ active_device: DeviceInfo | None = None
213
+ adb_result = next((r for r in results if r.id == "android_adb"), None)
214
+ if adb_result and adb_result.metadata.get("active_device"):
215
+ try:
216
+ active_device = DeviceInfo(**adb_result.metadata["active_device"])
217
+ except (TypeError, ValueError) as exc:
218
+ # Includes pydantic ValidationError (a ValueError subclass).
219
+ logger.warning(
220
+ f"Malformed active_device metadata from ADB probe; readiness"
221
+ f" report will omit the active device: {exc}"
222
+ )
223
+
224
+ return SystemReadinessReport(
225
+ overall_ready=overall_ready,
226
+ blocker_count=len(blockers),
227
+ passed_blocker_count=len(passed_blockers),
228
+ probes=results,
229
+ active_device=active_device,
230
+ os_type=platform.os_type.value,
231
+ timestamp=time.time(),
232
+ )
233
+
234
+ @staticmethod
235
+ def _crashed_probe_result(probe: BaseProbe, exc: object) -> ProbeResult:
236
+ """Turn an exception escaping ``probe.probe()`` into a FAIL result.
237
+
238
+ Cancellation and other non-``Exception`` errors are re-raised: they are
239
+ not a diagnosis of the host, they are the event loop shutting us down.
240
+ """
241
+ if isinstance(exc, BaseException) and not isinstance(exc, Exception):
242
+ raise exc
243
+ if isinstance(exc, TimeoutError):
244
+ logger.warning(
245
+ f"[ReadinessEngine] Probe '{probe.probe_id}' did not finish within"
246
+ f" {ReadinessEngine.PROBE_TIMEOUT_SECONDS:.0f}s; reporting it as FAIL."
247
+ )
248
+ return ProbeResult(
249
+ id=probe.probe_id,
250
+ category=probe.category,
251
+ title=probe.probe_id.replace("_", " ").title(),
252
+ status=ProbeStatus.FAIL,
253
+ is_blocker=probe.is_blocker,
254
+ summary="Probe timed out",
255
+ description=(
256
+ f"The '{probe.probe_id}' check did not finish within"
257
+ f" {ReadinessEngine.PROBE_TIMEOUT_SECONDS:.0f}s. A hung ADB server is the"
258
+ " usual cause: restart it and run the diagnosis again."
259
+ ),
260
+ actions=[
261
+ ProbeAction(
262
+ action_type="command",
263
+ label="Restart ADB",
264
+ payload="adb kill-server && adb start-server",
265
+ ),
266
+ ],
267
+ metadata={
268
+ "exception_type": "TimeoutError",
269
+ "timeout_seconds": ReadinessEngine.PROBE_TIMEOUT_SECONDS,
270
+ },
271
+ )
272
+ logger.warning(
273
+ f"[ReadinessEngine] Probe '{probe.probe_id}' crashed; reporting it as FAIL: "
274
+ f"{type(exc).__name__}: {exc}"
275
+ )
276
+ return ProbeResult(
277
+ id=probe.probe_id,
278
+ category=probe.category,
279
+ title=probe.probe_id.replace("_", " ").title(),
280
+ status=ProbeStatus.FAIL,
281
+ is_blocker=probe.is_blocker,
282
+ summary="Probe crashed",
283
+ description=(
284
+ f"The '{probe.probe_id}' check raised {type(exc).__name__}: {exc}. "
285
+ "This is a diagnostics bug or a host permission problem, not a device fault."
286
+ ),
287
+ metadata={"exception_type": type(exc).__name__, "exception": str(exc)},
288
+ )
289
+
290
+ async def heal_adb_keys(self, force: bool = False) -> dict[str, Any]:
291
+ """Auto-heal corrupted or 0-byte ADB authentication keys."""
292
+ from artemis.core.diagnostics.adb_keys import heal_adb_keys
293
+
294
+ logger.info("[ReadinessEngine] Auto-healing ADB authentication keys...")
295
+ return await asyncio.to_thread(heal_adb_keys, None, force)
296
+
297
+ async def restart_adb_server(self) -> dict[str, Any]:
298
+ """Execute adb kill-server && adb start-server to recover connectivity, auto-healing corrupted keys if needed."""
299
+ from artemis.core.diagnostics.adb_keys import heal_adb_keys, inspect_adb_keys
300
+
301
+ endpoint = adb_server_connection.current_endpoint()
302
+ if not endpoint.is_local_default:
303
+ return {
304
+ "success": True,
305
+ "skipped": True,
306
+ "message": (
307
+ "Remote ADB is active. Artemis refreshed device discovery without stopping "
308
+ "the active ADB server endpoint."
309
+ ),
310
+ "endpoint": endpoint.to_dict(),
311
+ }
312
+
313
+ adb_path = toolchain.resolve("adb") or "adb"
314
+
315
+ def _restart_sync():
316
+ # If keys are corrupted, heal them first
317
+ key_status = inspect_adb_keys()
318
+ if key_status.is_corrupted:
319
+ logger.warning(
320
+ f"[ReadinessEngine] Corrupted ADB keys detected ({key_status.error_reason}). Auto-healing..."
321
+ )
322
+ return heal_adb_keys(adb_path=adb_path)
323
+
324
+ try:
325
+ clean_env = os.environ.copy()
326
+ clean_env.pop("ADB_SERVER_SOCKET", None)
327
+ subprocess.run(
328
+ [adb_path, "kill-server"],
329
+ stdout=subprocess.DEVNULL,
330
+ stderr=subprocess.DEVNULL,
331
+ timeout=5,
332
+ env=clean_env,
333
+ )
334
+ res = subprocess.run(
335
+ [adb_path, "start-server"],
336
+ stdout=subprocess.DEVNULL,
337
+ stderr=subprocess.DEVNULL,
338
+ timeout=5,
339
+ env=clean_env,
340
+ )
341
+ success = res.returncode == 0
342
+ return {
343
+ "success": success,
344
+ "message": "ADB server restarted successfully"
345
+ if success
346
+ else "Failed to restart ADB",
347
+ "output": "",
348
+ }
349
+ except Exception as exc:
350
+ return {
351
+ "success": False,
352
+ "message": f"Error executing ADB restart: {exc}",
353
+ }
354
+
355
+ logger.info("[ReadinessEngine] Restarting ADB server...")
356
+ return await asyncio.to_thread(_restart_sync)
357
+
358
+ async def launch_emulator(self, avd_name: str) -> dict[str, Any]:
359
+ """Launch an Android emulator by AVD name in the background and track its lifecycle."""
360
+ if not adb_server_connection.current_endpoint().is_local_default:
361
+ return {
362
+ "avd_name": avd_name,
363
+ "status": "failed",
364
+ "pid": None,
365
+ "serial": None,
366
+ "error": "Switch to local ADB before launching a local emulator.",
367
+ "stage_message": "Local ADB is not active",
368
+ "progress_percent": 0,
369
+ "started_at": None,
370
+ "elapsed_seconds": 0,
371
+ "logs": [],
372
+ "can_retry": True,
373
+ }
374
+ from artemis.core.diagnostics.emulator_manager import emulator_manager
375
+
376
+ state = await emulator_manager.launch(avd_name)
377
+ return state.model_dump()
378
+
379
+ def get_emulator_status(self) -> dict[str, Any]:
380
+ """Query current status of background emulator launch."""
381
+ from artemis.core.diagnostics.emulator_manager import emulator_manager
382
+
383
+ return emulator_manager.get_status().model_dump()
384
+
385
+ async def stop_emulator(self) -> dict[str, Any]:
386
+ """Stop current running emulator."""
387
+ from artemis.core.diagnostics.emulator_manager import emulator_manager
388
+
389
+ return await emulator_manager.stop()
390
+
391
+ def dismiss_emulator(self) -> dict[str, Any]:
392
+ """Dismiss current emulator launch state."""
393
+ from artemis.core.diagnostics.emulator_manager import emulator_manager
394
+
395
+ return emulator_manager.dismiss()
396
+
397
+ async def connect_wireless_adb(self, host: str, port: int = 5555) -> dict[str, Any]:
398
+ """Connect to an Android device over Wi-Fi via adb connect."""
399
+ if not adb_server_connection.current_endpoint().is_local_default:
400
+ return {
401
+ "success": False,
402
+ "message": "Switch to local ADB before connecting a Wireless ADB device.",
403
+ }
404
+ clean_host = host.strip()
405
+ if not clean_host:
406
+ return {"success": False, "message": "Host IP address cannot be empty"}
407
+ target = f"{clean_host}:{port}"
408
+ adb_path = toolchain.resolve("adb") or "adb"
409
+
410
+ def _connect_sync():
411
+ try:
412
+ res = subprocess.run(
413
+ [adb_path, "connect", target],
414
+ capture_output=True,
415
+ text=True,
416
+ timeout=8,
417
+ )
418
+ output = (res.stdout + "\n" + res.stderr).strip()
419
+ success = "connected to" in output.lower() and "failed" not in output.lower()
420
+ return {
421
+ "success": success,
422
+ "target": target,
423
+ "output": output,
424
+ "message": f"Connected to {target}" if success else output,
425
+ }
426
+ except Exception as exc:
427
+ return {
428
+ "success": False,
429
+ "target": target,
430
+ "message": f"Connection error: {exc}",
431
+ }
432
+
433
+ logger.info(f"[ReadinessEngine] Connecting to wireless ADB target '{target}'...")
434
+ return await asyncio.to_thread(_connect_sync)
435
+
436
+
437
+ # Global singleton instance
438
+ readiness_engine = ReadinessEngine()
@@ -0,0 +1,216 @@
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
+ """Real-device parity check between the Accessibility Helper and UIAutomator2.
16
+
17
+ The unit tests prove the two backends produce the same element *shapes*; only
18
+ a real screen can prove they describe the same *content*. ``compare_backends``
19
+ dumps the current screen with both, then checks what the agent depends on:
20
+
21
+ * every labelled UIAutomator element (text or content-desc) has a helper
22
+ element with the same label at the same place (bounds IoU >= 0.5), and vice
23
+ versa, so neither backend invents or loses what is on screen;
24
+ * the helper emits no negative and no off-screen bounds (its bounds are
25
+ clipped to the display, like UIAutomator's);
26
+ * dump latency of both, for the record.
27
+
28
+ ``artemis helper parity`` is the CLI front end.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from statistics import median
34
+ import time
35
+ from typing import Any
36
+
37
+ from third_party.mobile_use.clients.ui_automator_client import parse_hierarchy_xml_to_elements
38
+
39
+ MIN_RECALL = 0.9
40
+ MIN_PRECISION = 0.8
41
+
42
+
43
+ def _label(element: dict[str, Any]) -> str:
44
+ text = str(element.get("text") or "").strip()
45
+ if text:
46
+ return text
47
+ return str(element.get("content-desc") or "").strip()
48
+
49
+
50
+ def _iou(a: dict[str, int], b: dict[str, int]) -> float:
51
+ left = max(a["left"], b["left"])
52
+ top = max(a["top"], b["top"])
53
+ right = min(a["right"], b["right"])
54
+ bottom = min(a["bottom"], b["bottom"])
55
+ if right <= left or bottom <= top:
56
+ return 0.0
57
+ inter = (right - left) * (bottom - top)
58
+ area_a = max(1, (a["right"] - a["left"]) * (a["bottom"] - a["top"]))
59
+ area_b = max(1, (b["right"] - b["left"]) * (b["bottom"] - b["top"]))
60
+ return inter / (area_a + area_b - inter)
61
+
62
+
63
+ def analyze_xml(xml: str, width: int, height: int) -> dict[str, Any]:
64
+ """Counts and labelled elements of one dump, plus the bounds problems the helper must not have."""
65
+ elements = parse_hierarchy_xml_to_elements(xml)
66
+ labelled: list[tuple[str, dict[str, int]]] = []
67
+ negative = offscreen = 0
68
+ package = None
69
+ for element in elements:
70
+ bounds = element.get("parsed_bounds")
71
+ if package is None and element.get("package"):
72
+ package = element["package"]
73
+ if not isinstance(bounds, dict):
74
+ continue
75
+ if min(bounds.values()) < 0:
76
+ negative += 1
77
+ if (
78
+ bounds["left"] >= width
79
+ or bounds["top"] >= height
80
+ or bounds["right"] > width
81
+ or bounds["bottom"] > height
82
+ ):
83
+ offscreen += 1
84
+ label = _label(element)
85
+ if label:
86
+ labelled.append((label, bounds))
87
+ return {
88
+ "nodes": len(elements),
89
+ "labelled": len(labelled),
90
+ "labels": labelled,
91
+ "negative_bounds": negative,
92
+ "offscreen_bounds": offscreen,
93
+ "package": package,
94
+ }
95
+
96
+
97
+ def match_labels(
98
+ source: list[tuple[str, dict[str, int]]], target: list[tuple[str, dict[str, int]]]
99
+ ) -> tuple[int, list[str]]:
100
+ """How many labelled ``source`` elements have a same-label, same-place element in ``target``."""
101
+ used = [False] * len(target)
102
+ found = 0
103
+ missing: list[str] = []
104
+ for label, bounds in source:
105
+ hit = False
106
+ for i, (other_label, other_bounds) in enumerate(target):
107
+ if used[i] or other_label != label:
108
+ continue
109
+ if _iou(bounds, other_bounds) >= 0.5:
110
+ used[i] = True
111
+ hit = True
112
+ break
113
+ if hit:
114
+ found += 1
115
+ else:
116
+ missing.append(f"{label!r} at {bounds}")
117
+ return found, missing
118
+
119
+
120
+ def compare_dumps(helper_xml: str, uiautomator_xml: str, width: int, height: int) -> dict[str, Any]:
121
+ """Pure comparison of two dumps of the same screen."""
122
+ helper = analyze_xml(helper_xml, width, height)
123
+ u2 = analyze_xml(uiautomator_xml, width, height)
124
+ u2_found, missing_in_helper = match_labels(u2["labels"], helper["labels"])
125
+ helper_found, extra_in_helper = match_labels(helper["labels"], u2["labels"])
126
+ recall = u2_found / u2["labelled"] if u2["labelled"] else 1.0
127
+ precision = helper_found / helper["labelled"] if helper["labelled"] else 1.0
128
+ problems: list[str] = []
129
+ if helper["negative_bounds"]:
130
+ problems.append(
131
+ f"helper emitted {helper['negative_bounds']} element(s) with negative bounds"
132
+ )
133
+ if helper["offscreen_bounds"]:
134
+ problems.append(
135
+ f"helper emitted {helper['offscreen_bounds']} element(s) outside the screen"
136
+ )
137
+ if recall < MIN_RECALL:
138
+ problems.append(
139
+ f"only {recall:.0%} of UIAutomator's labelled elements appear in the helper dump "
140
+ f"(need {MIN_RECALL:.0%})"
141
+ )
142
+ if precision < MIN_PRECISION:
143
+ problems.append(
144
+ f"only {precision:.0%} of the helper's labelled elements appear in the UIAutomator dump "
145
+ f"(need {MIN_PRECISION:.0%}); the helper is showing things that are not on screen"
146
+ )
147
+ for side in (helper, u2):
148
+ side.pop("labels", None)
149
+ return {
150
+ "ok": not problems,
151
+ "problems": problems,
152
+ "helper": helper,
153
+ "uiautomator": u2,
154
+ "match": {
155
+ "uiautomator_labelled": u2["labelled"],
156
+ "uiautomator_in_helper": u2_found,
157
+ "recall": recall,
158
+ "helper_labelled": helper["labelled"],
159
+ "helper_in_uiautomator": helper_found,
160
+ "precision": precision,
161
+ "missing_in_helper": missing_in_helper,
162
+ "extra_in_helper": extra_in_helper,
163
+ },
164
+ }
165
+
166
+
167
+ def compare_backends(serial: str, rounds: int = 3) -> dict[str, Any]:
168
+ """Dump the current screen of ``serial`` with both backends and compare (device required)."""
169
+ from artemis.clients.accessibility_client import AccessibilityClient
170
+ from third_party.mobile_use.clients.ui_automator_client import UIAutomatorClient
171
+
172
+ helper = AccessibilityClient(serial)
173
+ u2 = UIAutomatorClient(serial)
174
+
175
+ def timed(dump) -> tuple[str, float]:
176
+ times: list[float] = []
177
+ xml = ""
178
+ for _ in range(rounds):
179
+ started = time.perf_counter()
180
+ xml = dump()
181
+ times.append((time.perf_counter() - started) * 1000.0)
182
+ return xml, median(times)
183
+
184
+ # The two cannot run at once: UIAutomator2's UiAutomation connection unbinds
185
+ # the helper. Dump with the helper first, then with UIAutomator2, then stop
186
+ # its server so the helper is back for whoever comes next.
187
+ helper_version = None
188
+ try:
189
+ helper.connect()
190
+ helper_version = helper.session.version_name if helper.session else None
191
+ info = helper.get_hierarchy_json()
192
+ width, height = int(info.get("width") or 0), int(info.get("height") or 0)
193
+ if width <= 0 or height <= 0:
194
+ screen = helper.get_screen_data()
195
+ width, height = screen.width, screen.height
196
+ helper_xml, helper_ms = timed(helper.get_hierarchy)
197
+ finally:
198
+ helper.disconnect()
199
+
200
+ try:
201
+ u2.connect()
202
+ u2_xml, u2_ms = timed(u2.get_hierarchy)
203
+ finally:
204
+ u2.disconnect(stop_server=True)
205
+
206
+ report = compare_dumps(helper_xml, u2_xml, width, height)
207
+ report["serial"] = serial
208
+ report["screen"] = {"width": width, "height": height}
209
+ report["helper"]["median_ms"] = helper_ms
210
+ report["helper"]["version"] = helper_version
211
+ report["uiautomator"]["median_ms"] = u2_ms
212
+ report["xml"] = {"helper": helper_xml, "uiautomator": u2_xml}
213
+ return report
214
+
215
+
216
+ __all__ = ["analyze_xml", "compare_backends", "compare_dumps", "match_labels"]