android-midscene-automation 0.2.4 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +159 -29
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +4 -2
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +12 -0
  507. package/src/App.vue +10 -5
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +340 -121
  525. package/src/types.ts +1 -1
@@ -0,0 +1,254 @@
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 cooperative cancellation for task workers.
16
+
17
+ Workers use ``CREATE_NO_WINDOW`` on Windows, so console signals cannot reach
18
+ them reliably. The daemon writes a cancel marker in the shared temp directory;
19
+ the worker polls it and cancels its asyncio task, allowing recording, trace
20
+ and device-lease cleanup to finish. The daemon kills unresponsive workers
21
+ after a deadline.
22
+
23
+ Markers use the session id, falling back to the PID and process creation time
24
+ to avoid cancelling a different process after PID reuse.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from collections.abc import Callable
30
+ import json
31
+ import os
32
+ from pathlib import Path
33
+ import time
34
+ from typing import Any
35
+ import uuid
36
+
37
+ from artemis.config.paths import get_temp_dir
38
+
39
+ CANCEL_DIR_NAME = "cancel-requests"
40
+ MARKER_TTL_SECONDS = 3600.0
41
+ DEFAULT_POLL_SECONDS = 0.5
42
+
43
+
44
+ def cancel_request_dir() -> Path:
45
+ """Directory holding cancel markers (created on demand)."""
46
+ directory = get_temp_dir(CANCEL_DIR_NAME)
47
+ directory.mkdir(parents=True, exist_ok=True)
48
+ return directory
49
+
50
+
51
+ def _session_marker(session_id: str) -> Path:
52
+ safe = "".join(ch if ch.isalnum() or ch in "-_." else "_" for ch in str(session_id).strip())
53
+ return cancel_request_dir() / f"session-{safe}.cancel"
54
+
55
+
56
+ def _pid_marker(pid: int) -> Path:
57
+ return cancel_request_dir() / f"pid-{int(pid)}.cancel"
58
+
59
+
60
+ def _write_marker(path: Path, payload: dict[str, Any]) -> None:
61
+ tmp = path.with_name(f"{path.name}.{uuid.uuid4().hex}.tmp")
62
+ tmp.write_text(json.dumps(payload), encoding="utf-8")
63
+ os.replace(tmp, path)
64
+
65
+
66
+ def _read_marker(path: Path) -> dict[str, Any] | None:
67
+ try:
68
+ payload = json.loads(path.read_text(encoding="utf-8"))
69
+ except (OSError, ValueError):
70
+ return None
71
+ return payload if isinstance(payload, dict) else None
72
+
73
+
74
+ def _is_expired(payload: dict[str, Any] | None, path: Path, now: float) -> bool:
75
+ requested_at = None
76
+ if payload is not None:
77
+ try:
78
+ requested_at = float(payload.get("requested_at"))
79
+ except (TypeError, ValueError):
80
+ requested_at = None
81
+ if requested_at is None:
82
+ try:
83
+ requested_at = path.stat().st_mtime
84
+ except OSError:
85
+ return True
86
+ return (now - requested_at) > MARKER_TTL_SECONDS
87
+
88
+
89
+ def request_cancel(
90
+ *,
91
+ session_id: str | None = None,
92
+ pid: int | None = None,
93
+ process_created_at: float = 0.0,
94
+ reason: str = "",
95
+ ) -> list[Path]:
96
+ """Ask the worker identified by ``session_id`` and/or ``pid`` to cancel.
97
+
98
+ Returns the marker paths that were written. Writing is best-effort: an
99
+ unwritable temp directory yields an empty list and the caller falls back
100
+ to a hard kill.
101
+ """
102
+ written: list[Path] = []
103
+ now = time.time()
104
+ payload = {
105
+ "requested_at": now,
106
+ "reason": reason,
107
+ "session_id": str(session_id) if session_id else None,
108
+ "pid": int(pid) if pid else None,
109
+ "process_created_at": float(process_created_at or 0.0),
110
+ }
111
+ targets: list[Path] = []
112
+ if session_id:
113
+ targets.append(_session_marker(str(session_id)))
114
+ if pid:
115
+ targets.append(_pid_marker(int(pid)))
116
+ for target in targets:
117
+ try:
118
+ _write_marker(target, payload)
119
+ written.append(target)
120
+ except OSError:
121
+ continue
122
+ purge_expired(now)
123
+ return written
124
+
125
+
126
+ def is_cancel_requested(
127
+ *,
128
+ session_id: str | None = None,
129
+ pid: int | None = None,
130
+ process_created_at: float | None = None,
131
+ ) -> bool:
132
+ """Return True when a live cancel marker targets this worker.
133
+
134
+ A PID marker is honoured only when its recorded process creation time is
135
+ absent or matches ``process_created_at`` (within one second), which
136
+ protects a fresh worker from a marker left behind for a dead one.
137
+ """
138
+ now = time.time()
139
+ if session_id:
140
+ path = _session_marker(str(session_id))
141
+ if path.exists():
142
+ payload = _read_marker(path)
143
+ if not _is_expired(payload, path, now):
144
+ return True
145
+ if pid:
146
+ path = _pid_marker(int(pid))
147
+ if path.exists():
148
+ payload = _read_marker(path)
149
+ if payload is None:
150
+ # Partially written or unreadable marker: retry on the next poll.
151
+ return False
152
+ if _is_expired(payload, path, now):
153
+ return False
154
+ try:
155
+ recorded = float(payload.get("process_created_at") or 0.0)
156
+ except (TypeError, ValueError):
157
+ recorded = 0.0
158
+ if (
159
+ recorded > 0
160
+ and process_created_at is not None
161
+ and process_created_at > 0
162
+ and abs(recorded - process_created_at) >= 1.0
163
+ ):
164
+ return False
165
+ return True
166
+ return False
167
+
168
+
169
+ def clear_cancel_request(*, session_id: str | None = None, pid: int | None = None) -> None:
170
+ """Remove the markers for a worker (best-effort)."""
171
+ targets: list[Path] = []
172
+ if session_id:
173
+ targets.append(_session_marker(str(session_id)))
174
+ if pid:
175
+ targets.append(_pid_marker(int(pid)))
176
+ for target in targets:
177
+ try:
178
+ target.unlink(missing_ok=True)
179
+ except OSError:
180
+ continue
181
+
182
+
183
+ def purge_expired(now: float | None = None) -> int:
184
+ """Delete markers older than the TTL. Returns the number removed."""
185
+ now = time.time() if now is None else now
186
+ removed = 0
187
+ try:
188
+ entries = list(cancel_request_dir().glob("*.cancel"))
189
+ except OSError:
190
+ return 0
191
+ for path in entries:
192
+ payload = _read_marker(path)
193
+ if _is_expired(payload, path, now):
194
+ try:
195
+ path.unlink(missing_ok=True)
196
+ removed += 1
197
+ except OSError:
198
+ continue
199
+ return removed
200
+
201
+
202
+ def current_process_created_at() -> float:
203
+ """Creation time of the current process, or 0.0 when unavailable."""
204
+ try:
205
+ import psutil
206
+
207
+ return float(psutil.Process(os.getpid()).create_time())
208
+ except Exception:
209
+ return 0.0
210
+
211
+
212
+ async def watch_for_cancel_request(
213
+ on_cancel: Callable[[], Any],
214
+ *,
215
+ session_id: str | None = None,
216
+ pid: int | None = None,
217
+ process_created_at: float | None = None,
218
+ poll_seconds: float | None = None,
219
+ ) -> bool:
220
+ """Poll for a cancel marker and invoke ``on_cancel`` once it appears.
221
+
222
+ Returns True when a cancellation was delivered. The coroutine runs until
223
+ it delivers one or is itself cancelled, so callers should run it as a
224
+ background task alongside the work it guards.
225
+ """
226
+ import asyncio
227
+
228
+ if poll_seconds is None:
229
+ try:
230
+ poll_seconds = float(os.getenv("ARTEMIS_CANCEL_POLL_SECONDS", DEFAULT_POLL_SECONDS))
231
+ except ValueError:
232
+ poll_seconds = DEFAULT_POLL_SECONDS
233
+ poll_seconds = max(0.05, poll_seconds)
234
+ if pid is None:
235
+ pid = os.getpid()
236
+ if process_created_at is None:
237
+ process_created_at = current_process_created_at()
238
+
239
+ while True:
240
+ try:
241
+ requested = is_cancel_requested(
242
+ session_id=session_id,
243
+ pid=pid,
244
+ process_created_at=process_created_at,
245
+ )
246
+ except Exception:
247
+ requested = False
248
+ if requested:
249
+ clear_cancel_request(session_id=session_id, pid=pid)
250
+ result = on_cancel()
251
+ if asyncio.iscoroutine(result):
252
+ await result
253
+ return True
254
+ await asyncio.sleep(poll_seconds)
@@ -0,0 +1,422 @@
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
+ """Client utilities for communicating with and auto-spawning the Artemis Daemon."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from http.client import HTTPException
21
+ import os
22
+ from pathlib import Path
23
+ import subprocess
24
+ import sys
25
+ import time
26
+ from typing import Any
27
+ import urllib.error
28
+ import urllib.request
29
+
30
+ from artemis.config.paths import ROOT_DIR
31
+ from third_party.mobile_use.utils.logger import get_logger
32
+
33
+ logger = get_logger(__name__)
34
+
35
+ DEFAULT_DAEMON_HOST = os.environ.get("ARTEMIS_DAEMON_HOST", "127.0.0.1")
36
+ DEFAULT_DAEMON_PORT = int(os.environ.get("ARTEMIS_DAEMON_PORT", "8000"))
37
+
38
+
39
+ def is_standalone_forced() -> bool:
40
+ """Return True if standalone/embedded execution is explicitly configured."""
41
+ return os.environ.get("ARTEMIS_STANDALONE", "").lower() in ("1", "true", "yes")
42
+
43
+
44
+ def is_daemon_running(
45
+ host: str = DEFAULT_DAEMON_HOST,
46
+ port: int = DEFAULT_DAEMON_PORT,
47
+ timeout: float = 0.3,
48
+ ) -> bool:
49
+ """Fast probe to check if the Artemis Daemon HTTP server is responding."""
50
+ url = f"http://{host}:{port}/api/system/readiness"
51
+ try:
52
+ req = urllib.request.Request(url, headers={"User-Agent": "Artemis-Daemon-Probe"})
53
+ with urllib.request.urlopen(req, timeout=timeout) as response:
54
+ return response.status in (200, 400, 404, 503)
55
+ except Exception:
56
+ # Fallback probe to root endpoint
57
+ try:
58
+ root_url = f"http://{host}:{port}/"
59
+ req = urllib.request.Request(root_url, headers={"User-Agent": "Artemis-Daemon-Probe"})
60
+ with urllib.request.urlopen(req, timeout=timeout) as response:
61
+ return response.status in (200, 301, 302, 404)
62
+ except Exception:
63
+ return False
64
+
65
+
66
+ def is_artemis_daemon(
67
+ host: str = DEFAULT_DAEMON_HOST,
68
+ port: int = DEFAULT_DAEMON_PORT,
69
+ timeout: float = 2.0,
70
+ ) -> bool:
71
+ """Identity probe: is the process on ``host:port`` the Artemis daemon?
72
+
73
+ :func:`is_daemon_running` is a liveness probe that accepts any HTTP
74
+ listener (a dev server on port 8000 answers ``200`` on ``/``). Diagnostics
75
+ need to tell the daemon apart from a port squatter, so this asks the
76
+ daemon's lightweight ``/api/system/server-status`` route and requires the
77
+ JSON shape only Artemis serves.
78
+ """
79
+ url = f"http://{host}:{port}/api/system/server-status"
80
+ try:
81
+ req = urllib.request.Request(url, headers={"User-Agent": "Artemis-Daemon-Probe"})
82
+ with urllib.request.urlopen(req, timeout=timeout) as response:
83
+ if response.status != 200:
84
+ return False
85
+ payload = json.loads(response.read().decode("utf-8", errors="replace") or "null")
86
+ except (OSError, ValueError, HTTPException):
87
+ return False
88
+ return isinstance(payload, dict) and "current_pid" in payload and "port" in payload
89
+
90
+
91
+ def daemon_log_path(port: int = DEFAULT_DAEMON_PORT) -> Path:
92
+ """Return the log file path used by a background-spawned Daemon on the given port."""
93
+ from artemis.config.paths import get_app_dir
94
+
95
+ return get_app_dir() / "logs" / f"daemon-{port}.log"
96
+
97
+
98
+ def spawn_daemon(
99
+ host: str = DEFAULT_DAEMON_HOST,
100
+ port: int = DEFAULT_DAEMON_PORT,
101
+ ) -> subprocess.Popen | None:
102
+ """Silently spawn the Artemis Daemon server in the background as a detached process.
103
+
104
+ The server is always launched via ``sys.executable -m`` (never through the
105
+ ``artemis``/``artemis-admin`` console-script shims) so the long-lived process
106
+ chain never holds a lock on ``Scripts/artemis.exe`` — on Windows, a resident
107
+ shim blocks ``uv sync``/reinstalls with "os error 32" until it exits.
108
+ """
109
+ env = os.environ.copy()
110
+ env["PYTHONUNBUFFERED"] = "1"
111
+ env["PYTHONUTF8"] = "1"
112
+
113
+ cmd = [
114
+ sys.executable,
115
+ "-m",
116
+ "apps.admin_console.server",
117
+ "--host",
118
+ host,
119
+ "--port",
120
+ str(port),
121
+ ]
122
+
123
+ # Persist daemon output for post-mortem debugging (daemon mode is the
124
+ # default for `artemis restart`, so silent DEVNULL would hide crashes).
125
+ log_handle: Any = subprocess.DEVNULL
126
+ try:
127
+ log_file = daemon_log_path(port)
128
+ log_file.parent.mkdir(parents=True, exist_ok=True)
129
+ log_handle = open(log_file, "ab")
130
+ except Exception:
131
+ log_handle = subprocess.DEVNULL
132
+
133
+ kwargs: dict[str, Any] = {
134
+ "cwd": str(ROOT_DIR),
135
+ "env": env,
136
+ "stdout": log_handle,
137
+ "stderr": subprocess.STDOUT if log_handle is not subprocess.DEVNULL else subprocess.DEVNULL,
138
+ "stdin": subprocess.DEVNULL,
139
+ }
140
+
141
+ if sys.platform == "win32":
142
+ kwargs["creationflags"] = (
143
+ subprocess.CREATE_NEW_PROCESS_GROUP
144
+ | getattr(subprocess, "DETACHED_PROCESS", 0x00000008)
145
+ | getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
146
+ )
147
+ else:
148
+ kwargs["start_new_session"] = True
149
+
150
+ try:
151
+ proc = subprocess.Popen(cmd, **kwargs)
152
+ logger.info(
153
+ f"Spawned Artemis Daemon in background (PID {proc.pid}) at http://{host}:{port}"
154
+ )
155
+ return proc
156
+ except Exception as exc:
157
+ logger.warning(f"Could not auto-spawn Artemis Daemon: {exc}")
158
+ return None
159
+ finally:
160
+ # The child inherited the handle; the parent's copy is no longer needed.
161
+ if log_handle is not subprocess.DEVNULL:
162
+ try:
163
+ log_handle.close()
164
+ except OSError:
165
+ pass
166
+
167
+
168
+ def ensure_daemon_running(
169
+ host: str = DEFAULT_DAEMON_HOST,
170
+ port: int = DEFAULT_DAEMON_PORT,
171
+ timeout: float = 2.0,
172
+ wait_ready: bool = False,
173
+ ) -> tuple[bool, str | None]:
174
+ """Ensure the Artemis Daemon is running, auto-spawning it if necessary.
175
+
176
+ Args:
177
+ host: Daemon host to probe.
178
+ port: Daemon port to probe.
179
+ timeout: Maximum seconds to wait if wait_ready is True.
180
+ wait_ready: If True, blocks until Daemon is healthy or timeout reached.
181
+ If False, spawns asynchronously and returns immediately.
182
+
183
+ Returns:
184
+ (True, base_url) if Daemon is accessible.
185
+ (False, None) if standalone mode is forced or Daemon could not be reached.
186
+ """
187
+ if is_standalone_forced():
188
+ return False, None
189
+
190
+ if is_daemon_running(host, port):
191
+ return True, f"http://{host}:{port}"
192
+
193
+ # Auto-spawn Daemon in background
194
+ spawn_daemon(host, port)
195
+
196
+ if not wait_ready:
197
+ return True, f"http://{host}:{port}"
198
+
199
+ # Poll until ready or timeout
200
+ started = time.monotonic()
201
+ while time.monotonic() - started < timeout:
202
+ time.sleep(0.1)
203
+ if is_daemon_running(host, port):
204
+ return True, f"http://{host}:{port}"
205
+
206
+ logger.warning(f"Daemon did not respond within {timeout}s. Falling back to standalone mode.")
207
+ return False, None
208
+
209
+
210
+ def submit_task_to_daemon(
211
+ goal: str,
212
+ *,
213
+ profile: str = "flash",
214
+ device_serial: str | None = None,
215
+ expected_output: str | None = None,
216
+ enable_outputter: bool | None = None,
217
+ locked_app_package: str | None = None,
218
+ app_path: str | None = None,
219
+ session_id: str | None = None,
220
+ ingress: str = "client",
221
+ conversation_id: str | None = None,
222
+ verification_level: str | None = None,
223
+ explorer_mode: str | None = None,
224
+ base_url: str | None = None,
225
+ timeout: float = 15.0,
226
+ ) -> dict[str, Any] | None:
227
+ """Submit a task to the running Daemon via REST API.
228
+
229
+ ``verification_level`` ('off' | 'final' | 'checkpoints' | 'strict') and
230
+ ``explorer_mode`` ('flash' | 'pro' | 'ultra') are the Pro-profile tuning
231
+ knobs of ``/api/run``; they are forwarded verbatim and ignored by Flash.
232
+
233
+ Returns the response JSON dict if successfully enqueued, or None on error.
234
+ """
235
+ url = f"{base_url or f'http://{DEFAULT_DAEMON_HOST}:{DEFAULT_DAEMON_PORT}'}/api/run"
236
+ payload = {
237
+ "goal": goal,
238
+ "profile": profile,
239
+ "device_serial": device_serial,
240
+ "expected_output": expected_output,
241
+ "enable_outputter": enable_outputter,
242
+ "verification_level": verification_level,
243
+ "explorer_mode": explorer_mode,
244
+ "locked_app_package": locked_app_package,
245
+ "app_path": app_path,
246
+ "session_id": session_id,
247
+ "ingress": ingress,
248
+ "conversation_id": conversation_id,
249
+ }
250
+
251
+ try:
252
+ data = json.dumps(payload).encode("utf-8")
253
+ req = urllib.request.Request(
254
+ url,
255
+ data=data,
256
+ headers={"Content-Type": "application/json", "User-Agent": "Artemis-Client"},
257
+ method="POST",
258
+ )
259
+ with urllib.request.urlopen(req, timeout=timeout) as response:
260
+ if response.status in (200, 201):
261
+ return json.loads(response.read().decode("utf-8"))
262
+ except Exception as exc:
263
+ logger.debug(f"Failed to submit task to Daemon at {url}: {exc}")
264
+ return None
265
+ return None
266
+
267
+
268
+ def stop_task_on_daemon(
269
+ session_id: str,
270
+ *,
271
+ base_url: str | None = None,
272
+ timeout: float = 5.0,
273
+ ) -> bool:
274
+ """Request the running Artemis Daemon to stop a task by session ID."""
275
+ url = f"{base_url or f'http://{DEFAULT_DAEMON_HOST}:{DEFAULT_DAEMON_PORT}'}/api/stop"
276
+ payload = {"session_id": session_id}
277
+ try:
278
+ data = json.dumps(payload).encode("utf-8")
279
+ req = urllib.request.Request(
280
+ url,
281
+ data=data,
282
+ headers={"Content-Type": "application/json", "User-Agent": "Artemis-Client"},
283
+ method="POST",
284
+ )
285
+ with urllib.request.urlopen(req, timeout=timeout) as response:
286
+ if response.status == 200:
287
+ resp_data = json.loads(response.read().decode("utf-8"))
288
+ return resp_data.get("status") == "stopped"
289
+ except Exception as exc:
290
+ logger.debug(f"Failed to request stop from Daemon at {url}: {exc}")
291
+ return False
292
+ return False
293
+
294
+
295
+ def get_daemon_session(
296
+ session_id: str,
297
+ *,
298
+ base_url: str | None = None,
299
+ timeout: float = 5.0,
300
+ ) -> dict[str, Any] | None:
301
+ """Fetch details of a single session from the Artemis Daemon."""
302
+ url = f"{base_url or f'http://{DEFAULT_DAEMON_HOST}:{DEFAULT_DAEMON_PORT}'}/api/sessions/{session_id}"
303
+ try:
304
+ req = urllib.request.Request(
305
+ url,
306
+ headers={"User-Agent": "Artemis-Client"},
307
+ method="GET",
308
+ )
309
+ with urllib.request.urlopen(req, timeout=timeout) as response:
310
+ if response.status == 200:
311
+ return json.loads(response.read().decode("utf-8"))
312
+ except Exception as exc:
313
+ logger.debug(f"Failed to get session {session_id} from Daemon at {url}: {exc}")
314
+ return None
315
+ return None
316
+
317
+
318
+ def get_daemon_status(
319
+ *,
320
+ base_url: str | None = None,
321
+ timeout: float = 5.0,
322
+ ) -> dict[str, Any] | None:
323
+ """Fetch global scheduler and device status from the Artemis Daemon."""
324
+ url = f"{base_url or f'http://{DEFAULT_DAEMON_HOST}:{DEFAULT_DAEMON_PORT}'}/api/status"
325
+ try:
326
+ req = urllib.request.Request(
327
+ url,
328
+ headers={"User-Agent": "Artemis-Client"},
329
+ method="GET",
330
+ )
331
+ with urllib.request.urlopen(req, timeout=timeout) as response:
332
+ if response.status == 200:
333
+ return json.loads(response.read().decode("utf-8"))
334
+ except Exception as exc:
335
+ logger.debug(f"Failed to get status from Daemon at {url}: {exc}")
336
+ return None
337
+ return None
338
+
339
+
340
+ def submit_batch_to_daemon(
341
+ goals: list[str],
342
+ *,
343
+ profile: str = "flash",
344
+ device_serial: str | None = None,
345
+ ingress: str = "cli",
346
+ verification_level: str | None = None,
347
+ explorer_mode: str | None = None,
348
+ base_url: str | None = None,
349
+ timeout: float = 15.0,
350
+ ) -> dict[str, Any] | None:
351
+ """Submit a batch of goals to the running Daemon.
352
+
353
+ ``verification_level`` / ``explorer_mode`` apply to every goal of the batch
354
+ (see :func:`submit_task_to_daemon`).
355
+ """
356
+ url = f"{base_url or f'http://{DEFAULT_DAEMON_HOST}:{DEFAULT_DAEMON_PORT}'}/api/run"
357
+ payload = {
358
+ "goals": goals,
359
+ "profile": profile,
360
+ "device_serial": device_serial,
361
+ "ingress": ingress,
362
+ "verification_level": verification_level,
363
+ "explorer_mode": explorer_mode,
364
+ }
365
+ try:
366
+ data = json.dumps(payload).encode("utf-8")
367
+ req = urllib.request.Request(
368
+ url,
369
+ data=data,
370
+ headers={"Content-Type": "application/json", "User-Agent": "Artemis-Client"},
371
+ method="POST",
372
+ )
373
+ with urllib.request.urlopen(req, timeout=timeout) as response:
374
+ if response.status in (200, 201):
375
+ return json.loads(response.read().decode("utf-8"))
376
+ except Exception as exc:
377
+ logger.debug(f"Failed to submit batch to Daemon at {url}: {exc}")
378
+ return None
379
+ return None
380
+
381
+
382
+ def wait_for_daemon_task(
383
+ session_id: str,
384
+ *,
385
+ base_url: str | None = None,
386
+ timeout: float = 600.0,
387
+ poll_interval: float = 1.0,
388
+ on_status_update: Any = None,
389
+ ) -> dict[str, Any]:
390
+ """Poll the Daemon until the task reaches a terminal status (completed, failed, cancelled)."""
391
+ started = time.monotonic()
392
+ last_status = None
393
+
394
+ while time.monotonic() - started < timeout:
395
+ sess = get_daemon_session(session_id, base_url=base_url)
396
+ if sess:
397
+ status = sess.get("status")
398
+ if status != last_status:
399
+ last_status = status
400
+ if callable(on_status_update):
401
+ on_status_update(sess)
402
+
403
+ if status in ("completed", "success", "failed", "cancelled"):
404
+ return sess
405
+ else:
406
+ # Check if task is queued in daemon status
407
+ st = get_daemon_status(base_url=base_url)
408
+ if st:
409
+ queue = st.get("queue") or []
410
+ is_queued = any(
411
+ isinstance(item, dict) and str(item.get("session_id")) == str(session_id)
412
+ for item in queue
413
+ )
414
+ current_st = "queued" if is_queued else "launching"
415
+ if current_st != last_status:
416
+ last_status = current_st
417
+ if callable(on_status_update):
418
+ on_status_update({"status": current_st, "session_id": session_id})
419
+
420
+ time.sleep(poll_interval)
421
+
422
+ return {"session_id": session_id, "status": "timeout", "error": f"Timed out after {timeout}s"}