android-midscene-automation 0.2.4 → 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 +7 -0
  2. package/README.md +35 -0
  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,283 @@
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
+ """Cross-process ownership for Android's shell screen-bright wake lock."""
16
+
17
+ from __future__ import annotations
18
+
19
+ from contextlib import contextmanager
20
+ from datetime import UTC, datetime
21
+ import hashlib
22
+ import json
23
+ import os
24
+ from pathlib import Path
25
+ import re
26
+ import subprocess
27
+ import time
28
+ import uuid
29
+
30
+ from artemis.config.paths import get_temp_dir
31
+ from artemis.runtime.adb_endpoint import adb_command
32
+ from artemis.runtime.process_probe import pid_is_alive
33
+ from third_party.mobile_use.utils.logger import get_logger
34
+
35
+ logger = get_logger(__name__)
36
+
37
+ _WAKE_LOCK_TYPE = "SCREEN_BRIGHT_WAKE_LOCK"
38
+ _WAKE_LOCK_RE = re.compile(
39
+ rf"{_WAKE_LOCK_TYPE}:.*\bheld=(true|false)\b.*\brefCount=(\d+)\b",
40
+ re.IGNORECASE,
41
+ )
42
+
43
+
44
+ class ScreenAwakeLease:
45
+ """Share exactly one Android shell wake-lock reference across clients.
46
+
47
+ ``cmd power set-wakelock`` exposes one ref-counted shell wake lock for a
48
+ display/type pair. Calling it independently from every process leaks
49
+ references whenever a process crashes. PID-aware lease files make the
50
+ Android reference a process-shared resource instead: the first live client
51
+ normalizes stale references and acquires one, later clients only register,
52
+ and the last client drains the shared reference on disconnect.
53
+ """
54
+
55
+ _MUTEX_TIMEOUT_SECONDS = 10.0
56
+ _MUTEX_POLL_SECONDS = 0.05
57
+ _MALFORMED_GRACE_SECONDS = 5.0
58
+ _MAX_REFERENCE_DRAIN = 256
59
+
60
+ def __init__(self, device_id: str):
61
+ self.device_id = device_id
62
+ self.token = uuid.uuid4().hex
63
+ device_hash = hashlib.sha256(device_id.encode("utf-8")).hexdigest()[:16]
64
+ root = get_temp_dir("awake-leases")
65
+ self.lease_dir = root / f"{device_hash}.leases"
66
+ self.mutex_path = root / f"{device_hash}.mutex"
67
+ self.lease_path = self.lease_dir / f"{self.token}.lease"
68
+ self._acquired = False
69
+
70
+ @staticmethod
71
+ def _process_created_at(pid: int | None = None) -> float:
72
+ try:
73
+ import psutil
74
+
75
+ return float(psutil.Process(pid or os.getpid()).create_time())
76
+ except Exception:
77
+ return 0.0
78
+
79
+ @staticmethod
80
+ def _owner_is_alive(payload: dict) -> bool:
81
+ try:
82
+ pid = int(payload["pid"])
83
+ created_at = float(payload.get("process_created_at", 0.0) or 0.0)
84
+ except (KeyError, TypeError, ValueError):
85
+ return False
86
+ return pid_is_alive(pid, created_at if created_at > 0 else None)
87
+
88
+ @staticmethod
89
+ def _read_payload(path: Path) -> dict | None:
90
+ try:
91
+ value = json.loads(path.read_text(encoding="utf-8"))
92
+ return value if isinstance(value, dict) else None
93
+ except (OSError, ValueError, TypeError, json.JSONDecodeError):
94
+ return None
95
+
96
+ @classmethod
97
+ def _path_is_stale(cls, path: Path) -> bool:
98
+ payload = cls._read_payload(path)
99
+ if payload is not None:
100
+ return not cls._owner_is_alive(payload)
101
+ try:
102
+ return time.time() - path.stat().st_mtime >= cls._MALFORMED_GRACE_SECONDS
103
+ except OSError:
104
+ return True
105
+
106
+ def _payload(self) -> dict:
107
+ return {
108
+ "pid": os.getpid(),
109
+ "process_created_at": self._process_created_at(),
110
+ "token": self.token,
111
+ "device_id": self.device_id,
112
+ "created_at": datetime.now(UTC).isoformat(),
113
+ }
114
+
115
+ @contextmanager
116
+ def _mutex(self):
117
+ self.mutex_path.parent.mkdir(parents=True, exist_ok=True)
118
+ started_at = time.monotonic()
119
+ acquired = False
120
+ while not acquired:
121
+ try:
122
+ fd = os.open(self.mutex_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
123
+ except FileExistsError:
124
+ if self._path_is_stale(self.mutex_path):
125
+ try:
126
+ self.mutex_path.unlink(missing_ok=True)
127
+ except OSError:
128
+ pass
129
+ continue
130
+ if time.monotonic() - started_at >= self._MUTEX_TIMEOUT_SECONDS:
131
+ raise TimeoutError("Timed out acquiring the Artemis awake-lease mutex.")
132
+ time.sleep(self._MUTEX_POLL_SECONDS)
133
+ else:
134
+ try:
135
+ os.write(fd, json.dumps(self._payload()).encode("utf-8"))
136
+ finally:
137
+ os.close(fd)
138
+ acquired = True
139
+ try:
140
+ yield
141
+ finally:
142
+ payload = self._read_payload(self.mutex_path)
143
+ if payload is not None and payload.get("token") == self.token:
144
+ try:
145
+ self.mutex_path.unlink(missing_ok=True)
146
+ except OSError:
147
+ pass
148
+
149
+ def _run(self, args: list[str], description: str) -> subprocess.CompletedProcess[str] | None:
150
+ try:
151
+ result = subprocess.run(
152
+ adb_command(["-s", self.device_id, *args]),
153
+ stdin=subprocess.DEVNULL,
154
+ capture_output=True,
155
+ text=True,
156
+ timeout=10,
157
+ check=False,
158
+ )
159
+ if result.returncode != 0:
160
+ detail = (result.stderr or result.stdout).strip()
161
+ logger.warning(
162
+ f"Could not {description} on {self.device_id}: {detail or 'ADB command failed'}"
163
+ )
164
+ return result
165
+ except Exception as exc:
166
+ logger.warning(f"Could not {description} on {self.device_id}: {exc}")
167
+ return None
168
+
169
+ def _wake_lock_state(self) -> tuple[bool, int] | None:
170
+ result = self._run(
171
+ ["shell", "cmd", "power", "set-wakelock", "list"],
172
+ "inspect the shared screen-bright wake lock",
173
+ )
174
+ if result is None or result.returncode != 0:
175
+ return None
176
+ match = _WAKE_LOCK_RE.search(result.stdout)
177
+ if match is None:
178
+ return (False, 0)
179
+ return (match.group(1).lower() == "true", int(match.group(2)))
180
+
181
+ def _set_wake_lock(self, action: str) -> bool:
182
+ result = self._run(
183
+ [
184
+ "shell",
185
+ "cmd",
186
+ "power",
187
+ "set-wakelock",
188
+ action,
189
+ "-d",
190
+ "0",
191
+ _WAKE_LOCK_TYPE,
192
+ ],
193
+ f"{action} the shared screen-bright wake lock",
194
+ )
195
+ return result is not None and result.returncode == 0
196
+
197
+ def _drain_references(self) -> bool:
198
+ """Release legacy or crashed-process references until none remain."""
199
+ for _ in range(self._MAX_REFERENCE_DRAIN):
200
+ state = self._wake_lock_state()
201
+ if state is None:
202
+ return False
203
+ held, count = state
204
+ if not held or count <= 0:
205
+ return True
206
+ if not self._set_wake_lock("release"):
207
+ return False
208
+ logger.warning(
209
+ f"Refused to drain more than {self._MAX_REFERENCE_DRAIN} shared wake-lock "
210
+ f"references on {self.device_id}"
211
+ )
212
+ return False
213
+
214
+ def _live_leases(self) -> list[Path]:
215
+ self.lease_dir.mkdir(parents=True, exist_ok=True)
216
+ live: list[Path] = []
217
+ for path in self.lease_dir.glob("*.lease"):
218
+ if self._path_is_stale(path):
219
+ try:
220
+ path.unlink(missing_ok=True)
221
+ except OSError:
222
+ pass
223
+ else:
224
+ live.append(path)
225
+ return live
226
+
227
+ def acquire(self) -> bool:
228
+ if self._acquired:
229
+ return True
230
+ try:
231
+ with self._mutex():
232
+ live = self._live_leases()
233
+ state = self._wake_lock_state()
234
+ if state is None:
235
+ return False
236
+
237
+ if not live:
238
+ # No registered owner means every existing reference is
239
+ # legacy or belongs to a crashed process.
240
+ if not self._drain_references():
241
+ return False
242
+ state = (False, 0)
243
+
244
+ held, count = state
245
+ if not held or count <= 0:
246
+ if not self._set_wake_lock("acquire"):
247
+ return False
248
+ verified = self._wake_lock_state()
249
+ if verified is None or not verified[0] or verified[1] <= 0:
250
+ return False
251
+
252
+ self.lease_path.write_text(
253
+ json.dumps(self._payload(), ensure_ascii=False), encoding="utf-8"
254
+ )
255
+ self._acquired = True
256
+ return True
257
+ except Exception as exc:
258
+ logger.warning(f"Could not acquire shared awake lease on {self.device_id}: {exc}")
259
+ return False
260
+
261
+ def release(self) -> None:
262
+ if not self._acquired:
263
+ return
264
+ try:
265
+ with self._mutex():
266
+ self.lease_path.unlink(missing_ok=True)
267
+ if not self._live_leases():
268
+ self._drain_references()
269
+ except Exception as exc:
270
+ logger.warning(f"Could not release shared awake lease on {self.device_id}: {exc}")
271
+ finally:
272
+ self._acquired = False
273
+
274
+ def cleanup_unowned_references(self) -> bool:
275
+ """Drain legacy shell wake locks only when no live old host owns them."""
276
+ try:
277
+ with self._mutex():
278
+ if self._live_leases():
279
+ return False
280
+ return self._drain_references()
281
+ except Exception as exc:
282
+ logger.warning(f"Could not clean unowned screen wake locks on {self.device_id}: {exc}")
283
+ return False
@@ -0,0 +1,343 @@
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
+ """USB-scoped screen-awake policy shared by every local Artemis entrypoint."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import re
21
+ import subprocess
22
+ import threading
23
+ from typing import Iterable
24
+
25
+ from adbutils import AdbClient
26
+
27
+ from artemis.config import settings
28
+ from artemis.runtime.adb_endpoint import adb_command
29
+ from artemis.runtime.awake_lease import ScreenAwakeLease
30
+ from third_party.mobile_use.utils.logger import get_logger
31
+
32
+ logger = get_logger(__name__)
33
+
34
+ AWAKE_STRATEGY_USB = "usb_stay_on"
35
+ AWAKE_STRATEGY_HEARTBEAT = "host_heartbeat"
36
+ AWAKE_HEARTBEAT_INTERVAL_SECONDS = 5.0
37
+ DEVICE_DISCOVERY_INTERVAL_SECONDS = 2.0
38
+
39
+
40
+ def _awake_enabled() -> bool:
41
+ enabled = os.environ.get("ARTEMIS_KEEP_DEVICE_AWAKE", "true").strip().lower()
42
+ return enabled not in {"0", "false", "no", "off"}
43
+
44
+
45
+ def _run_awake_adb_command(
46
+ device_id: str, args: list[str], description: str
47
+ ) -> subprocess.CompletedProcess[str] | None:
48
+ """Run one non-fatal ADB command used by the USB awake policy."""
49
+ try:
50
+ result = subprocess.run(
51
+ adb_command(["-s", device_id, *args]),
52
+ stdin=subprocess.DEVNULL,
53
+ capture_output=True,
54
+ text=True,
55
+ timeout=10,
56
+ check=False,
57
+ )
58
+ if result.returncode != 0:
59
+ detail = (result.stderr or result.stdout).strip()
60
+ logger.warning(
61
+ f"Could not {description} on {device_id}: {detail or 'ADB command failed'}"
62
+ )
63
+ return result
64
+ except Exception as exc:
65
+ logger.warning(f"Could not {description} on {device_id}: {exc}")
66
+ return None
67
+
68
+
69
+ def sanitize_device_state(device_id: str) -> None:
70
+ """Sanitize Android device state by cleaning up orphan screenrecord or modal dialogs."""
71
+ if not device_id:
72
+ return
73
+ # 1. Kill any dangling screenrecord processes left from interrupted tasks
74
+ _run_awake_adb_command(
75
+ device_id,
76
+ ["shell", "pkill", "-f", "screenrecord"],
77
+ "kill orphan screenrecord processes",
78
+ )
79
+ # 2. Close system modal dialogs (ANR / crash popups)
80
+ _run_awake_adb_command(
81
+ device_id,
82
+ ["shell", "am", "broadcast", "-a", "android.intent.action.CLOSE_SYSTEM_DIALOGS"],
83
+ "dismiss system crash/ANR dialogs",
84
+ )
85
+
86
+
87
+ def _configure_usb_stay_awake(device_id: str) -> str | None:
88
+ """Enable USB stay-awake, falling back to an effective host heartbeat.
89
+
90
+ The policy is intentionally scoped to USB power rather than an Artemis
91
+ process or task. Android applies the normal screen timeout as soon as USB
92
+ power is removed, even though no ADB command can be sent after unplugging.
93
+ AC, wireless, and dock charging retain the user's normal behavior.
94
+ """
95
+ if not _awake_enabled() or os.environ.get("ARTEMIS_CLOUD_MODE") == "1":
96
+ return None
97
+
98
+ # Migration cleanup: previous Artemis versions used a persistent shell
99
+ # wake lock. Drain it only after every live legacy owner has exited.
100
+ ScreenAwakeLease(device_id).cleanup_unowned_references()
101
+
102
+ commands = (
103
+ (
104
+ "enable USB-scoped stay-awake policy",
105
+ ["shell", "svc", "power", "stayon", "usb"],
106
+ ),
107
+ ("wake display", ["shell", "input", "keyevent", "KEYCODE_WAKEUP"]),
108
+ ("dismiss non-secure keyguard", ["shell", "wm", "dismiss-keyguard"]),
109
+ )
110
+ for description, args in commands:
111
+ _run_awake_adb_command(device_id, args, description)
112
+
113
+ configured = _run_awake_adb_command(
114
+ device_id,
115
+ ["shell", "settings", "get", "global", "stay_on_while_plugged_in"],
116
+ "verify USB-scoped stay-awake policy",
117
+ )
118
+ power_state = _run_awake_adb_command(
119
+ device_id,
120
+ ["shell", "dumpsys", "power"],
121
+ "verify active USB stay-awake state",
122
+ )
123
+ try:
124
+ value = int(configured.stdout.strip()) if configured is not None else -1
125
+ except (TypeError, ValueError):
126
+ value = -1
127
+ stay_on_active = (
128
+ configured is not None
129
+ and configured.returncode == 0
130
+ and value == 2
131
+ and power_state is not None
132
+ and power_state.returncode == 0
133
+ and re.search(r"\bmStayOn=true\b", power_state.stdout) is not None
134
+ )
135
+ if stay_on_active:
136
+ logger.info(f"USB-scoped stay-awake policy verified active on {device_id}")
137
+ return AWAKE_STRATEGY_USB
138
+
139
+ logger.warning(
140
+ f"USB stay-awake is not active on {device_id} (setting={value}); "
141
+ f"using a {AWAKE_HEARTBEAT_INTERVAL_SECONDS:g}-second host heartbeat"
142
+ )
143
+ primed = _run_awake_adb_command(
144
+ device_id,
145
+ ["shell", "input", "keyevent", "KEYCODE_UNKNOWN"],
146
+ "prime the host stay-awake heartbeat",
147
+ )
148
+ if primed is None or primed.returncode != 0:
149
+ return None
150
+ return AWAKE_STRATEGY_HEARTBEAT
151
+
152
+
153
+ def _discover_connected_device_ids() -> list[str]:
154
+ """Return the explicitly targeted device, or the connected devices Artemis manages."""
155
+ if not _awake_enabled() or os.environ.get("ARTEMIS_CLOUD_MODE") == "1":
156
+ return []
157
+
158
+ target = os.environ.get("ARTEMIS_DEVICE_ID") or os.environ.get("ADB_DEVICE_SERIAL")
159
+ host = os.environ.get("ADB_HOST") or settings.ADB_HOST or "127.0.0.1"
160
+ port_text = os.environ.get("ADB_PORT")
161
+ try:
162
+ port = int(port_text) if port_text else int(settings.ADB_PORT or 5037)
163
+ except (TypeError, ValueError):
164
+ port = 5037
165
+
166
+ try:
167
+ devices = AdbClient(host=host, port=port).device_list()
168
+ serials = [device.serial for device in devices if device.serial]
169
+ except Exception as exc:
170
+ logger.debug(f"Could not discover a device for the USB awake policy: {exc}")
171
+ return []
172
+
173
+ if target:
174
+ return [target] if target in serials else []
175
+ # Keep awake only the devices Artemis actually manages: serials claimed in
176
+ # the device pool by an active execution lock or a live queue reservation.
177
+ # Other devices sharing the same ADB server belong to unrelated tools or
178
+ # users, and their power policy must not be touched.
179
+ try:
180
+ from artemis.runtime.device_pool import device_pool
181
+
182
+ claimed = device_pool.get_claimed_serials()
183
+ except Exception as exc:
184
+ logger.debug(f"Could not query claimed devices for the awake policy: {exc}")
185
+ return []
186
+ return [serial for serial in serials if serial in claimed]
187
+
188
+
189
+ class ScreenAwakeService:
190
+ """Apply USB stay-awake or a connection-scoped host heartbeat."""
191
+
192
+ def __init__(self):
193
+ self._lock = threading.RLock()
194
+ self._strategies: dict[str, str] = {}
195
+ self._heartbeat_stops: dict[str, threading.Event] = {}
196
+ self._heartbeat_threads: dict[str, threading.Thread] = {}
197
+ self._monitor_stop: threading.Event | None = None
198
+ self._monitor_thread: threading.Thread | None = None
199
+ self._shutdown_requested = False
200
+
201
+ def start(self, device_ids: Iterable[str] | None = None) -> dict[str, str | None]:
202
+ if self._shutdown_requested:
203
+ return {}
204
+ selected = list(device_ids) if device_ids is not None else _discover_connected_device_ids()
205
+ results = {}
206
+ for device_id in dict.fromkeys(selected):
207
+ if self._shutdown_requested:
208
+ break
209
+ if device_id:
210
+ results[device_id] = self.ensure_device(device_id)
211
+ if (
212
+ _awake_enabled()
213
+ and os.environ.get("ARTEMIS_CLOUD_MODE") != "1"
214
+ and not self._shutdown_requested
215
+ ):
216
+ self._start_device_monitor()
217
+ return results
218
+
219
+ def ensure_device(self, device_id: str) -> str | None:
220
+ """Configure one device once per process; Android owns unplug behavior."""
221
+ if (
222
+ not device_id
223
+ or not _awake_enabled()
224
+ or os.environ.get("ARTEMIS_CLOUD_MODE") == "1"
225
+ or self._shutdown_requested
226
+ ):
227
+ return None
228
+ with self._lock:
229
+ existing = self._strategies.get(device_id)
230
+ if existing is not None:
231
+ return existing
232
+ strategy = _configure_usb_stay_awake(device_id)
233
+ if strategy is not None:
234
+ self._strategies[device_id] = strategy
235
+ if strategy == AWAKE_STRATEGY_HEARTBEAT:
236
+ self._start_heartbeat(device_id)
237
+ return strategy
238
+
239
+ def _start_heartbeat(self, device_id: str) -> None:
240
+ if device_id in self._heartbeat_threads:
241
+ return
242
+ stop_event = threading.Event()
243
+
244
+ def heartbeat() -> None:
245
+ while not stop_event.wait(AWAKE_HEARTBEAT_INTERVAL_SECONDS):
246
+ _run_awake_adb_command(
247
+ device_id,
248
+ ["shell", "input", "keyevent", "KEYCODE_UNKNOWN"],
249
+ "send the host stay-awake heartbeat",
250
+ )
251
+
252
+ thread = threading.Thread(
253
+ target=heartbeat,
254
+ name=f"artemis-awake-heartbeat-{device_id}",
255
+ daemon=True,
256
+ )
257
+ self._heartbeat_stops[device_id] = stop_event
258
+ self._heartbeat_threads[device_id] = thread
259
+ thread.start()
260
+
261
+ def _start_device_monitor(self) -> None:
262
+ with self._lock:
263
+ if self._monitor_thread is not None and self._monitor_thread.is_alive():
264
+ return
265
+ stop_event = threading.Event()
266
+
267
+ def monitor() -> None:
268
+ while not stop_event.wait(DEVICE_DISCOVERY_INTERVAL_SECONDS):
269
+ connected = set(_discover_connected_device_ids())
270
+ if stop_event.is_set():
271
+ break
272
+ self._reconcile_connected_devices(connected)
273
+
274
+ thread = threading.Thread(
275
+ target=monitor,
276
+ name="artemis-awake-device-monitor",
277
+ daemon=True,
278
+ )
279
+ self._monitor_stop = stop_event
280
+ self._monitor_thread = thread
281
+ thread.start()
282
+
283
+ def _reconcile_connected_devices(self, connected: set[str]) -> None:
284
+ """Stop unplugged heartbeats and enroll newly attached devices."""
285
+ with self._lock:
286
+ missing = set(self._strategies) - connected
287
+ for device_id in missing:
288
+ self._stop_device(device_id)
289
+ for device_id in connected:
290
+ self.ensure_device(device_id)
291
+
292
+ def _stop_device(self, device_id: str) -> None:
293
+ with self._lock:
294
+ stop_event = self._heartbeat_stops.pop(device_id, None)
295
+ thread = self._heartbeat_threads.pop(device_id, None)
296
+ self._strategies.pop(device_id, None)
297
+ if stop_event is not None:
298
+ stop_event.set()
299
+ if thread is not None and thread is not threading.current_thread():
300
+ thread.join(timeout=2.0)
301
+
302
+ def shutdown(self) -> None:
303
+ """Stop host work; unplug behavior remains owned by Android's USB policy."""
304
+ self._shutdown_requested = True
305
+ with self._lock:
306
+ monitor_stop = self._monitor_stop
307
+ monitor_thread = self._monitor_thread
308
+ heartbeat_stops = list(self._heartbeat_stops.values())
309
+ heartbeat_threads = list(self._heartbeat_threads.values())
310
+ if monitor_stop is not None:
311
+ monitor_stop.set()
312
+ for stop_event in heartbeat_stops:
313
+ stop_event.set()
314
+ self._monitor_stop = None
315
+ self._monitor_thread = None
316
+ self._heartbeat_stops.clear()
317
+ self._heartbeat_threads.clear()
318
+ self._strategies.clear()
319
+
320
+ if monitor_thread is not None:
321
+ monitor_thread.join(timeout=2.0)
322
+ for thread in heartbeat_threads:
323
+ thread.join(timeout=2.0)
324
+
325
+ @property
326
+ def device_ids(self) -> tuple[str, ...]:
327
+ with self._lock:
328
+ return tuple(self._strategies)
329
+
330
+
331
+ screen_awake_service = ScreenAwakeService()
332
+
333
+
334
+ def start_awake_service(device_ids: Iterable[str] | None = None) -> dict[str, str | None]:
335
+ return screen_awake_service.start(device_ids)
336
+
337
+
338
+ def ensure_device_awake(device_id: str) -> str | None:
339
+ return screen_awake_service.ensure_device(device_id)
340
+
341
+
342
+ def shutdown_awake_service() -> None:
343
+ screen_awake_service.shutdown()