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,98 @@
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
+ """Single source of truth for PID liveness probing across the Artemis codebase.
16
+
17
+ Semantics (decided by the maintainers):
18
+
19
+ - psutil only. No bare ``os.kill(pid, 0)`` fallback: on Windows, CPython
20
+ implements ``os.kill(pid, 0)`` for non-CTRL signals via
21
+ ``OpenProcess``/``TerminateProcess`` handle probing, which is both unreliable
22
+ for dead pids and semantically surprising.
23
+ - ``psutil.NoSuchProcess`` -> dead. Zombie processes -> dead.
24
+ - ``psutil.AccessDenied`` / ``PermissionError`` -> **alive**. When liveness
25
+ cannot be determined the probe defaults to "alive": it is always safer to
26
+ skip reaping a resource than to reap one that is still owned.
27
+ - ``created_at`` (a ``psutil``-style process create time) enables PID-reuse
28
+ protection: when provided and the probed process's create time differs by
29
+ one second or more, the original process is dead and its PID was recycled.
30
+ - Any other unexpected error -> alive (default-alive), with a warning log.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import logging
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ def pid_is_alive(pid: int, created_at: float | None = None) -> bool:
41
+ """Return whether the process identified by ``pid`` is still alive.
42
+
43
+ Args:
44
+ pid: The process id to probe. Non-positive or unparsable pids are dead.
45
+ created_at: Optional expected process create time (as reported by
46
+ ``psutil.Process.create_time()``). When given and > 0, a create-time
47
+ mismatch of >= 1 second means the PID was recycled -> dead.
48
+
49
+ Returns:
50
+ True when the process is alive or liveness cannot be determined
51
+ (default-alive); False when the process is definitively gone, a
52
+ zombie, or its PID has been recycled by another process.
53
+ """
54
+ try:
55
+ pid = int(pid)
56
+ except (TypeError, ValueError):
57
+ return False
58
+ if pid <= 0:
59
+ return False
60
+
61
+ try:
62
+ import psutil
63
+ except Exception as exc: # pragma: no cover - psutil is a hard dependency
64
+ logger.warning(f"psutil unavailable while probing pid {pid}: {exc}; assuming alive")
65
+ return True
66
+
67
+ try:
68
+ process = psutil.Process(pid)
69
+ if not process.is_running():
70
+ return False
71
+
72
+ try:
73
+ if process.status() == psutil.STATUS_ZOMBIE:
74
+ return False
75
+ except psutil.NoSuchProcess:
76
+ return False
77
+ except (psutil.AccessDenied, PermissionError):
78
+ # Cannot inspect the status; keep the default-alive verdict.
79
+ pass
80
+
81
+ if created_at is not None and created_at > 0:
82
+ try:
83
+ if abs(process.create_time() - float(created_at)) >= 1.0:
84
+ # Same PID, different birth time: the PID was recycled.
85
+ return False
86
+ except psutil.NoSuchProcess:
87
+ return False
88
+ except (psutil.AccessDenied, PermissionError):
89
+ pass
90
+
91
+ return True
92
+ except psutil.NoSuchProcess:
93
+ return False
94
+ except (psutil.AccessDenied, PermissionError):
95
+ return True
96
+ except Exception as exc:
97
+ logger.warning(f"Could not determine liveness of pid {pid}: {exc}; assuming alive")
98
+ return True
@@ -0,0 +1,435 @@
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
+ """Artemis Server Lifecycle and Process Management.
16
+
17
+ Provides cross-platform facilities to discover, inspect, terminate,
18
+ and restart running Artemis UI / backend server instances regardless
19
+ of which terminal or process spawned them.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import os
26
+ from pathlib import Path
27
+ import shutil
28
+ import socket
29
+ import subprocess
30
+ import sys
31
+ import time
32
+ from typing import Any, Callable
33
+ import urllib.error
34
+ import urllib.request
35
+
36
+ from artemis.config.paths import ROOT_DIR, get_server_info_file
37
+ from artemis.runtime.device_lock import DeviceExecutionLock
38
+ from artemis.runtime.process_probe import pid_is_alive
39
+ from artemis.runtime.supervisor import ProcessSupervisor
40
+ from third_party.mobile_use.utils.logger import get_logger
41
+
42
+ logger = get_logger(__name__)
43
+
44
+
45
+ def is_port_in_use(port: int, host: str = "127.0.0.1", timeout: float = 0.5) -> bool:
46
+ """Check whether a TCP port is currently listening / occupied."""
47
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
48
+ s.settimeout(timeout)
49
+ try:
50
+ return s.connect_ex((host, port)) == 0
51
+ except Exception:
52
+ return False
53
+
54
+
55
+ def write_server_info(
56
+ port: int,
57
+ host: str = "127.0.0.1",
58
+ pid: int | None = None,
59
+ lifecycle_token: str | None = None,
60
+ ) -> Path:
61
+ """Persist current Artemis server runtime metadata to well-known path."""
62
+ info_file = get_server_info_file()
63
+ try:
64
+ info_file.parent.mkdir(parents=True, exist_ok=True)
65
+ data = {
66
+ "pid": pid or os.getpid(),
67
+ "port": port,
68
+ "host": host,
69
+ "started_at": time.time(),
70
+ "cwd": str(ROOT_DIR),
71
+ "sys_executable": sys.executable,
72
+ "cmdline": sys.argv,
73
+ }
74
+ if lifecycle_token:
75
+ data["lifecycle_token"] = lifecycle_token
76
+ info_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
77
+ logger.debug(f"Saved Artemis server info (PID: {data['pid']}, port: {port}) to {info_file}")
78
+ except Exception as e:
79
+ logger.warning(f"Could not persist server metadata to {info_file}: {e}")
80
+ return info_file
81
+
82
+
83
+ def read_server_info() -> dict[str, Any] | None:
84
+ """Read and validate the persisted server runtime metadata if present."""
85
+ info_file = get_server_info_file()
86
+ if not info_file.exists():
87
+ return None
88
+ try:
89
+ content = info_file.read_text(encoding="utf-8").strip()
90
+ if not content:
91
+ return None
92
+ data = json.loads(content)
93
+ if isinstance(data, dict) and "pid" in data:
94
+ return data
95
+ except Exception as e:
96
+ logger.debug(f"Failed to read server info from {info_file}: {e}")
97
+ return None
98
+
99
+
100
+ def clear_server_info(
101
+ *,
102
+ port: int | None = None,
103
+ lifecycle_token: str | None = None,
104
+ ) -> None:
105
+ """Remove persisted metadata, optionally only for the matching server.
106
+
107
+ Scoped removal keeps ``artemis stop --port <unused>`` and a late shutdown
108
+ finalizer from deleting metadata written by a different server instance.
109
+ """
110
+ info_file = get_server_info_file()
111
+ try:
112
+ if info_file.exists():
113
+ if port is not None or lifecycle_token is not None:
114
+ info = read_server_info()
115
+ if not info:
116
+ return
117
+ if port is not None and info.get("port") != port:
118
+ return
119
+ if lifecycle_token is not None and info.get("lifecycle_token") != lifecycle_token:
120
+ return
121
+ info_file.unlink()
122
+ logger.debug(f"Cleared server info file at {info_file}")
123
+ except Exception as e:
124
+ logger.debug(f"Error clearing server info file {info_file}: {e}")
125
+
126
+
127
+ def request_graceful_shutdown(port: int, timeout: float = 1.5) -> bool:
128
+ """Ask a locally managed Artemis server to run its shutdown lifecycle.
129
+
130
+ The per-process token in the server metadata prevents this control request
131
+ from being accepted remotely or accidentally sent to an unrelated service
132
+ using the same port. A missing/stale token simply makes the caller fall back
133
+ to process termination.
134
+ """
135
+ info = read_server_info()
136
+ if not info or info.get("port") != port:
137
+ return False
138
+ token = info.get("lifecycle_token")
139
+ if not isinstance(token, str) or not token:
140
+ return False
141
+
142
+ request = urllib.request.Request(
143
+ f"http://127.0.0.1:{port}/api/system/shutdown",
144
+ method="POST",
145
+ headers={
146
+ "User-Agent": "Artemis-Lifecycle-Client/1.0",
147
+ "X-Artemis-Lifecycle-Token": token,
148
+ },
149
+ )
150
+ try:
151
+ with urllib.request.urlopen(request, timeout=timeout) as response:
152
+ return response.status in (200, 202)
153
+ except (OSError, urllib.error.URLError, urllib.error.HTTPError):
154
+ return False
155
+ except Exception:
156
+ return False
157
+
158
+
159
+ # Inverted dependency: the session database lives in the application layer
160
+ # (apps.admin_console), which this base runtime package must not import.
161
+ # The application layer registers its orphan-session reconciler here
162
+ # (apps/admin_console/database/repositories/session_repository.py does so on
163
+ # import); entry points that stop servers without loading the admin console
164
+ # perform that import as part of their assembly. Without a registration,
165
+ # reconciliation is skipped -- the admin console re-runs it on next startup.
166
+ _session_reconciler: Callable[[], int] | None = None
167
+
168
+
169
+ def register_session_reconciler(reconciler: Callable[[], int]) -> None:
170
+ """Register the app-layer callback that marks orphaned sessions as failed."""
171
+ global _session_reconciler
172
+ _session_reconciler = reconciler
173
+
174
+
175
+ def _reconcile_orphaned_sessions() -> int:
176
+ """Immediately mark sessions whose worker PIDs died during forced stop."""
177
+ if _session_reconciler is None:
178
+ return 0
179
+ try:
180
+ return _session_reconciler()
181
+ except Exception as exc:
182
+ logger.debug(f"Could not reconcile orphaned sessions after server stop: {exc}")
183
+ return 0
184
+
185
+
186
+ def _any_pid_alive(pids: list[int]) -> bool:
187
+ """Return whether any target process is still running (zombies excluded)."""
188
+ return any(pid_is_alive(pid) for pid in pids)
189
+
190
+
191
+ def find_server_pids(port: int = 8000) -> list[int]:
192
+ """Find all process IDs associated with the Artemis server listening on `port`.
193
+
194
+ Uses a multi-tiered discovery strategy:
195
+ 1. Check persisted server metadata file.
196
+ 2. Check port listeners via lsof (macOS/Linux), fuser (Linux), or netstat (Windows).
197
+ 3. Fallback to psutil net_connections and process table scan.
198
+ """
199
+ discovered: set[int] = set()
200
+
201
+ # 1. Check metadata file
202
+ info = read_server_info()
203
+ if info and info.get("port") == port:
204
+ saved_pid = info.get("pid")
205
+ if saved_pid and isinstance(saved_pid, int):
206
+ if pid_is_alive(saved_pid):
207
+ discovered.add(saved_pid)
208
+
209
+ # 2. Check port listeners with platform-native tools
210
+ # 2a. lsof (macOS & Linux)
211
+ if shutil.which("lsof"):
212
+ try:
213
+ res = subprocess.run(
214
+ ["lsof", "-ti", f":{port}", "-sTCP:LISTEN"],
215
+ stdin=subprocess.DEVNULL,
216
+ capture_output=True,
217
+ text=True,
218
+ timeout=2.0,
219
+ )
220
+ if res.returncode == 0 and res.stdout.strip():
221
+ for token in res.stdout.strip().splitlines():
222
+ token = token.strip()
223
+ if token.isdigit():
224
+ discovered.add(int(token))
225
+ except (OSError, ValueError, subprocess.SubprocessError):
226
+ pass
227
+
228
+ # 2b. fuser (Linux fallback)
229
+ if not discovered and shutil.which("fuser"):
230
+ try:
231
+ res = subprocess.run(
232
+ ["fuser", f"{port}/tcp"],
233
+ stdin=subprocess.DEVNULL,
234
+ capture_output=True,
235
+ text=True,
236
+ timeout=2.0,
237
+ )
238
+ for token in res.stdout.strip().split():
239
+ token = token.strip()
240
+ if token.isdigit():
241
+ discovered.add(int(token))
242
+ except (OSError, ValueError, subprocess.SubprocessError):
243
+ pass
244
+
245
+ # 2c. netstat (Windows fallback)
246
+ if not discovered and sys.platform == "win32":
247
+ try:
248
+ res = subprocess.run(
249
+ ["netstat", "-ano"],
250
+ stdin=subprocess.DEVNULL,
251
+ capture_output=True,
252
+ text=True,
253
+ timeout=3.0,
254
+ )
255
+ if res.returncode == 0:
256
+ for line in res.stdout.splitlines():
257
+ if f":{port}" in line and "LISTENING" in line.upper():
258
+ parts = line.strip().split()
259
+ if parts and parts[-1].isdigit():
260
+ discovered.add(int(parts[-1]))
261
+ except (OSError, ValueError, subprocess.SubprocessError):
262
+ pass
263
+
264
+ # 2d. psutil net_connections fallback
265
+ if not discovered:
266
+ try:
267
+ import psutil
268
+
269
+ for conn in psutil.net_connections(kind="inet"):
270
+ if conn.laddr and conn.laddr.port == port:
271
+ if conn.pid:
272
+ discovered.add(conn.pid)
273
+ except Exception as exc: # pylint: disable=broad-exception-caught
274
+ # psutil is optional and net_connections() fails in platform-specific ways.
275
+ logger.debug(f"psutil net_connections port scan skipped: {exc}", exc_info=True)
276
+
277
+ # 3. Process table scan if still nothing found on port but port is in use
278
+ if not discovered and is_port_in_use(port):
279
+ try:
280
+ import psutil
281
+
282
+ for proc in psutil.process_iter(["pid", "cmdline"]):
283
+ try:
284
+ cmdline = " ".join(proc.info.get("cmdline") or [])
285
+ if "apps.admin_console.server" in cmdline or (
286
+ "artemis" in cmdline and "ui" in cmdline
287
+ ):
288
+ if proc.info.get("pid"):
289
+ discovered.add(proc.info["pid"])
290
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
291
+ continue
292
+ except Exception as exc: # pylint: disable=broad-exception-caught
293
+ # psutil is optional and process_iter() fails in platform-specific ways.
294
+ logger.debug(f"psutil process table scan skipped: {exc}", exc_info=True)
295
+
296
+ return sorted(discovered)
297
+
298
+
299
+ def get_server_status(port: int = 8000) -> dict[str, Any]:
300
+ """Retrieve detailed status of the Artemis server on given port."""
301
+ in_use = is_port_in_use(port)
302
+ pids = find_server_pids(port)
303
+ info = read_server_info()
304
+
305
+ uptime_seconds: float | None = None
306
+ if info and "started_at" in info and isinstance(info["started_at"], (int, float)):
307
+ uptime_seconds = max(0.0, time.time() - info["started_at"])
308
+
309
+ is_running = in_use or bool(pids)
310
+
311
+ return {
312
+ "running": is_running,
313
+ "port": port,
314
+ "pids": pids,
315
+ "active_pid": pids[0] if pids else (info.get("pid") if info else None),
316
+ "uptime_seconds": uptime_seconds,
317
+ "url": f"http://localhost:{port}" if is_running else None,
318
+ "admin_url": f"http://localhost:{port}/admin" if is_running else None,
319
+ "metadata": info,
320
+ }
321
+
322
+
323
+ def stop_server(
324
+ port: int = 8000,
325
+ timeout: float = 12.0,
326
+ force: bool = False,
327
+ ) -> tuple[bool, str, list[int]]:
328
+ """Gracefully terminate any running Artemis server on `port`.
329
+
330
+ Args:
331
+ port: TCP port to inspect and terminate.
332
+ timeout: Maximum seconds to wait for the server shutdown lifecycle.
333
+ force: If True, skip the shutdown API and terminate the process tree.
334
+
335
+ Returns:
336
+ tuple (success, message, affected_pids)
337
+ """
338
+ pids = find_server_pids(port)
339
+
340
+ if not pids and not is_port_in_use(port):
341
+ clear_server_info(port=port)
342
+ DeviceExecutionLock.cleanup_stale_locks()
343
+ return True, f"No active server detected on port {port}.", []
344
+
345
+ # If no PID was discovered but port is still in use, try one more aggressive discovery
346
+ if not pids and is_port_in_use(port):
347
+ time.sleep(0.2)
348
+ pids = find_server_pids(port)
349
+
350
+ stopped_pids: list[int] = []
351
+ current_pid = os.getpid()
352
+
353
+ # A normal stop is an application lifecycle request, not an OS signal.
354
+ # This matters on Windows where taskkill /F and os.kill(..., SIGTERM) both
355
+ # bypass FastAPI's shutdown hook. Give Uvicorn enough time to cancel task
356
+ # workers, persist terminal session state, and release IPC/device resources.
357
+ graceful_requested = False
358
+ if not force:
359
+ graceful_requested = request_graceful_shutdown(
360
+ port,
361
+ timeout=min(2.0, max(0.25, timeout)),
362
+ )
363
+ if graceful_requested:
364
+ deadline = time.time() + max(0.25, timeout)
365
+ while time.time() < deadline:
366
+ if not is_port_in_use(port) and not _any_pid_alive(pids):
367
+ break
368
+ time.sleep(0.1)
369
+
370
+ if not is_port_in_use(port) and not _any_pid_alive(pids):
371
+ cleaned_locks = DeviceExecutionLock.cleanup_stale_locks()
372
+ clear_server_info(port=port)
373
+ pid_str = f" (PID: {', '.join(map(str, pids))})" if pids else ""
374
+ msg = f"Artemis server stopped gracefully{pid_str}. Port {port} is released."
375
+ if cleaned_locks:
376
+ msg += f" Cleaned up {cleaned_locks} device lock(s)."
377
+ return True, msg, pids
378
+
379
+ logger.warning(
380
+ f"Artemis server on port {port} did not exit within {timeout}s; "
381
+ "falling back to process-tree termination."
382
+ )
383
+
384
+ for pid in pids:
385
+ if pid == current_pid:
386
+ # Don't terminate self during stop_server call
387
+ continue
388
+ try:
389
+ ProcessSupervisor.terminate_tree(
390
+ pid,
391
+ timeout_seconds=0.5 if force or graceful_requested else timeout,
392
+ )
393
+ stopped_pids.append(pid)
394
+ except Exception as e:
395
+ logger.warning(f"Failed to terminate process {pid}: {e}")
396
+
397
+ # Wait for the port to actually become free
398
+ deadline = time.time() + max(1.5, timeout)
399
+ while time.time() < deadline:
400
+ if not is_port_in_use(port):
401
+ break
402
+ time.sleep(0.15)
403
+
404
+ # Force kill if port is still occupied
405
+ if is_port_in_use(port) and not force:
406
+ remaining_pids = find_server_pids(port)
407
+ for pid in remaining_pids:
408
+ if pid != current_pid:
409
+ try:
410
+ ProcessSupervisor.terminate_tree(pid, timeout_seconds=0.5)
411
+ except Exception as exc: # pylint: disable=broad-exception-caught
412
+ # Force-kill is best effort; the port check below reports the outcome.
413
+ logger.debug(f"Force kill of pid {pid} failed: {exc}", exc_info=True)
414
+ time.sleep(0.3)
415
+
416
+ # Cleanup stale device execution locks left by terminated server
417
+ cleaned_locks = DeviceExecutionLock.cleanup_stale_locks()
418
+ reconciled_sessions = _reconcile_orphaned_sessions()
419
+ clear_server_info(port=port)
420
+
421
+ port_free = not is_port_in_use(port)
422
+ if port_free:
423
+ pid_str = f" (PID: {', '.join(map(str, stopped_pids))})" if stopped_pids else ""
424
+ msg = f"Artemis server stopped{pid_str}. Port {port} is released."
425
+ if cleaned_locks:
426
+ msg += f" Cleaned up {cleaned_locks} device lock(s)."
427
+ if reconciled_sessions:
428
+ msg += f" Reconciled {reconciled_sessions} interrupted session(s)."
429
+ return True, msg, stopped_pids
430
+ else:
431
+ return (
432
+ False,
433
+ f"Port {port} remains occupied after stop attempt. Some background process may require manual termination.",
434
+ stopped_pids,
435
+ )
@@ -0,0 +1,121 @@
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-platform Process Supervisor and Lifecycle Manager."""
16
+
17
+ import asyncio
18
+ from collections.abc import Sequence
19
+ import os
20
+ from pathlib import Path
21
+
22
+ from artemis.platform import platform
23
+ from third_party.mobile_use.utils.logger import get_logger
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ class ProcessSupervisor:
29
+ """Manages process spawning, tree cleanup, signals, and UTF-8 stream handling."""
30
+
31
+ @staticmethod
32
+ def prepare_environment(extra_env: dict[str, str] | None = None) -> dict[str, str]:
33
+ """Prepare child process environment with guaranteed UTF-8 mode."""
34
+ env = os.environ.copy()
35
+ env["PYTHONUNBUFFERED"] = "1"
36
+ env["PYTHONUTF8"] = "1"
37
+ if extra_env:
38
+ env.update(extra_env)
39
+ return env
40
+
41
+ @staticmethod
42
+ async def spawn_async(
43
+ cmd: Sequence[str],
44
+ cwd: Path | str | None = None,
45
+ env: dict[str, str] | None = None,
46
+ capture_output: bool = True,
47
+ ) -> asyncio.subprocess.Process:
48
+ """Spawn an asynchronous subprocess with clean environment and cross-platform flags."""
49
+ prepared_env = ProcessSupervisor.prepare_environment(env)
50
+ cwd_str = str(cwd) if cwd else None
51
+
52
+ stdout_dest = asyncio.subprocess.PIPE if capture_output else None
53
+ stderr_dest = asyncio.subprocess.PIPE if capture_output else None
54
+
55
+ return await asyncio.create_subprocess_exec(
56
+ *cmd,
57
+ stdout=stdout_dest,
58
+ stderr=stderr_dest,
59
+ cwd=cwd_str,
60
+ env=prepared_env,
61
+ )
62
+
63
+ @staticmethod
64
+ def terminate_tree(pid: int, timeout_seconds: float = 3.0) -> bool:
65
+ """Recursively terminate a process tree."""
66
+ return platform.process.terminate_process_tree(pid, timeout_seconds=timeout_seconds)
67
+
68
+ @staticmethod
69
+ def terminate_tree_verified(
70
+ pid: int,
71
+ process_created_at: float,
72
+ timeout_seconds: float = 3.0,
73
+ ) -> bool:
74
+ """Terminate a process tree only if PID and creation time still match.
75
+
76
+ Cross-entry task control discovers the worker through a shared lease.
77
+ Verifying creation time prevents a stale lease from terminating an
78
+ unrelated process after the operating system reuses its PID.
79
+ """
80
+ try:
81
+ import psutil
82
+ except Exception:
83
+ return False
84
+ try:
85
+ process = psutil.Process(pid)
86
+ if process_created_at > 0 and abs(process.create_time() - process_created_at) >= 1.0:
87
+ return False
88
+ except psutil.NoSuchProcess:
89
+ return True
90
+ except Exception:
91
+ return False
92
+ return ProcessSupervisor.terminate_tree(pid, timeout_seconds=timeout_seconds)
93
+
94
+ @staticmethod
95
+ async def stop_process(
96
+ proc: asyncio.subprocess.Process | None,
97
+ timeout_seconds: float = 3.0,
98
+ ) -> None:
99
+ """Safely stop an asyncio Process and kill its children if necessary."""
100
+ if proc is None:
101
+ return
102
+
103
+ if proc.returncode is not None:
104
+ return
105
+
106
+ try:
107
+ if proc.pid:
108
+ ProcessSupervisor.terminate_tree(proc.pid, timeout_seconds=timeout_seconds)
109
+ else:
110
+ proc.terminate()
111
+ await asyncio.wait_for(proc.wait(), timeout=timeout_seconds)
112
+ except Exception as e:
113
+ logger.debug(f"Failed graceful stop for process {proc}: {e}")
114
+ try:
115
+ proc.kill()
116
+ except (ProcessLookupError, OSError) as kill_exc:
117
+ # ProcessLookupError: already exited, nothing to do.
118
+ logger.debug(f"Failed to kill process {proc}: {kill_exc}")
119
+
120
+
121
+ process_supervisor: ProcessSupervisor = ProcessSupervisor()