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,488 @@
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
+ """Per-turn action execution loop for the Validator.
16
+
17
+ Two execution tiers, selected by how many turn-ending actions the Operator
18
+ emitted in the turn:
19
+
20
+ * **Vetted single action** (one action): the pre-execution Safety Net runs
21
+ first (XML-first with pixel fallback), then the action executes locally with
22
+ a short retry loop.
23
+ * **Fast-action burst** (two or more actions): the actions fire back to back
24
+ with no safety net, no retries and no screenshots in between. The burst is
25
+ the Operator's tool against turn latency (transient menus, toasts, control
26
+ bars); the Operator owns the risk. The first failure aborts the rest.
27
+
28
+ Either way, a failure does not spawn a repair agent. The loop opens an
29
+ *execution incident* (:mod:`artemis.agents.validator.incidents`) that rides in
30
+ graph state and in the step's execution report, and the Operator resolves it
31
+ on its next turn with the live screen in front of it.
32
+
33
+ Extracted from ``validator.py``; the public entry point remains
34
+ ``ValidatorNode.__call__``, which delegates here. All patchable seams
35
+ (_exec_action, _validate_action_precondition*, _get_initial_screenshot, ...)
36
+ are dispatched through the node so existing test patches keep working.
37
+ """
38
+
39
+ import asyncio
40
+ import base64
41
+ from dataclasses import dataclass
42
+ import time
43
+ import uuid
44
+ from uuid import UUID
45
+
46
+ from artemis.agents.validator.categories import ValidationErrorCategory
47
+ from artemis.agents.validator.incidents import (
48
+ KIND_EXEC_ERROR,
49
+ KIND_SAFETY_NET,
50
+ open_incident,
51
+ )
52
+ from artemis.data_engine.trace import CURRENT_TRACE_ID
53
+ from artemis.graph.state import State
54
+ from artemis.utils.coordinates import normalize_action_dict
55
+ from artemis.utils.task_tree import format_action_clean
56
+ from third_party.mobile_use.utils.logger import get_logger
57
+
58
+ logger = get_logger(__name__)
59
+
60
+ #: ``attempts`` marker for actions a burst never reached.
61
+ BURST_SKIPPED_MARKER = "Skipped (burst aborted)"
62
+
63
+
64
+ @dataclass
65
+ class _ActionOutcome:
66
+ """Result of processing a single action item (pre-check + execution)."""
67
+
68
+ action_item: dict
69
+ success: bool = False
70
+ error_msg: str = ""
71
+ error_category: ValidationErrorCategory = ValidationErrorCategory.GENERAL
72
+ intercepted: bool = False
73
+ post_screenshot_b64: str | None = None
74
+
75
+
76
+ async def _run_precondition_gate(
77
+ node, session, action_item: dict, state: State, pre_screenshot_b64: str, original_coords: list
78
+ ) -> tuple[bool, ValidationErrorCategory, str]:
79
+ """Runs the pre-execution Safety Net: XML-first with pixel fallback."""
80
+ is_explorer_candidate = action_item.get("target_class") == "ExplorerCandidate"
81
+ is_ocr_element = "[OCR]" in str(action_item.get("target_class") or "")
82
+ has_index_metadata = bool(
83
+ (
84
+ action_item.get("target_text")
85
+ or action_item.get("target_bounds")
86
+ or action_item.get("target_resource_id")
87
+ )
88
+ and not is_explorer_candidate
89
+ and not is_ocr_element
90
+ )
91
+
92
+ async def _pixel_check() -> tuple[bool, ValidationErrorCategory, str]:
93
+ return await node._validate_action_precondition_pixel(
94
+ session, action_item, pre_screenshot_b64, original_coords, state=state
95
+ )
96
+
97
+ if not has_index_metadata:
98
+ # Pure coordinate/non-index interactions: pixel-based validation is the only gate.
99
+ return await _pixel_check()
100
+
101
+ # Index-based validation is fastest and safest when metadata is present
102
+ (
103
+ validation_passed,
104
+ validation_category,
105
+ validation_error,
106
+ ) = await node._validate_action_precondition(session, action_item, state=state)
107
+
108
+ if validation_passed:
109
+ return validation_passed, validation_category, validation_error
110
+
111
+ if validation_category == ValidationErrorCategory.XML_BYPASSED:
112
+ logger.info(
113
+ f"Pre-execution XML-based validation bypassed ({validation_error})."
114
+ " Falling back to Pixel-based validation..."
115
+ )
116
+ return await _pixel_check()
117
+
118
+ logger.info(
119
+ f"Pre-execution XML-based validation failed: {validation_error}."
120
+ " Attempting Pixel-based validation fallback..."
121
+ )
122
+ pixel_passed, pixel_category, _pixel_error = await _pixel_check()
123
+ if pixel_passed and pixel_category == ValidationErrorCategory.NONE:
124
+ logger.success("Pixel validation confirmed the target after XML validation failed.")
125
+ return True, ValidationErrorCategory.NONE, ""
126
+
127
+ return validation_passed, validation_category, validation_error
128
+
129
+
130
+ async def _attempt_local_execution(
131
+ node, session, action_item: dict, action_name, attempts_log: list, *, max_local_retries: int
132
+ ) -> tuple[bool, str]:
133
+ """Executes the action locally with a short retry loop."""
134
+ success = False
135
+ error_msg = ""
136
+ for attempt in range(max_local_retries):
137
+ try:
138
+ exec_success, exec_error = await node._exec_action(session, action_item)
139
+ except Exception as e:
140
+ exec_success = False
141
+ exec_error = str(e)
142
+
143
+ # No effect polling: "dispatched" only says the device accepted the
144
+ # command; whether it had the intended effect is the Operator's call.
145
+ if exec_success:
146
+ success = True
147
+ error_msg = ""
148
+ attempts_log.append("Dispatched")
149
+ break
150
+ else:
151
+ success = False
152
+ error_msg = exec_error
153
+ attempts_log.append(error_msg)
154
+
155
+ if attempt < max_local_retries - 1:
156
+ await asyncio.sleep(0.5)
157
+
158
+ return success, error_msg
159
+
160
+
161
+ async def _capture_live_screenshot(session, context_desc: str) -> str | None:
162
+ try:
163
+ return await session.screenshot_b64()
164
+ except Exception as e:
165
+ logger.error(f"Failed to capture {context_desc}: {e!r}")
166
+ return None
167
+
168
+
169
+ async def _process_action(
170
+ node,
171
+ state: State,
172
+ session,
173
+ action_item: dict,
174
+ action_name,
175
+ pre_screenshot_b64: str,
176
+ *,
177
+ burst: bool,
178
+ ) -> _ActionOutcome:
179
+ """Runs precondition checks (vetted tier only) and execution for one action item."""
180
+ action_item = dict(action_item) # Make a copy to avoid mutating original state actions
181
+ outcome = _ActionOutcome(action_item=action_item)
182
+ attempts_log = []
183
+
184
+ validation_passed = True
185
+ if not burst:
186
+ # 1. Pre-execution validation (Safety Net)
187
+ original_coords = list(action_item.get("coordinates") or [])
188
+ (
189
+ validation_passed,
190
+ validation_category,
191
+ validation_error,
192
+ ) = await _run_precondition_gate(
193
+ node, session, action_item, state, pre_screenshot_b64, original_coords
194
+ )
195
+ if not validation_passed:
196
+ outcome.success = False
197
+ outcome.intercepted = True
198
+ outcome.error_msg = f"Pre-execution validation failed: {validation_error}"
199
+ outcome.error_category = validation_category
200
+ attempts_log.append(outcome.error_msg)
201
+
202
+ if validation_passed:
203
+ # 2. Local execution: a vetted action gets a short retry loop, a burst
204
+ # member fires exactly once (retries would break the burst's timing).
205
+ if burst:
206
+ max_local_retries = 1
207
+ else:
208
+ max_local_retries = 1 if action_name == "launch_app" else 2
209
+ outcome.success, outcome.error_msg = await _attempt_local_execution(
210
+ node,
211
+ session,
212
+ action_item,
213
+ action_name,
214
+ attempts_log,
215
+ max_local_retries=max_local_retries,
216
+ )
217
+
218
+ if not outcome.success:
219
+ logger.info("Action failed, capturing failure screenshot...")
220
+ outcome.post_screenshot_b64 = await _capture_live_screenshot(session, "failure screenshot")
221
+
222
+ # Enrich the executed action record
223
+ if len(attempts_log) > 1 or not outcome.success:
224
+ action_item["attempts"] = list(attempts_log)
225
+
226
+ return outcome
227
+
228
+
229
+ def _absorb_post_screenshot(ctx, state: State, post_screenshot_b64: str) -> str | None:
230
+ """Persists the post-action screenshot and points state at it.
231
+
232
+ Returns the stored image name (None when no data engine is attached).
233
+ """
234
+ decoded_bytes = base64.b64decode(post_screenshot_b64)
235
+ logger.info(f"Successfully decoded post_screenshot_b64 ({len(decoded_bytes)} bytes)")
236
+ if not ctx.data_engine:
237
+ return None
238
+
239
+ post_image_name = ctx.data_engine.get_or_create_image(decoded_bytes)
240
+ screenshot_path = str(ctx.data_engine.get_image_path(post_image_name))
241
+ state.latest_screenshot = screenshot_path
242
+ logger.info(f"Validator updated state.latest_screenshot to: {screenshot_path}")
243
+ return post_image_name
244
+
245
+
246
+ def _record_action_start(ctx, step_id, action_name, action_item, parent_id, action_trace_id):
247
+ if ctx.data_engine and step_id:
248
+ ctx.data_engine.record_trace(
249
+ type="action",
250
+ name=action_name,
251
+ payload={"action": action_item, "status": "running"},
252
+ status="running",
253
+ parent_trace_id=parent_id,
254
+ step_id=step_id,
255
+ trace_id=action_trace_id,
256
+ )
257
+
258
+
259
+ def _record_action_end(
260
+ ctx,
261
+ step_id,
262
+ action_name,
263
+ outcome: _ActionOutcome,
264
+ post_image_name,
265
+ parent_id,
266
+ action_trace_id,
267
+ start_time: float,
268
+ *,
269
+ burst: bool,
270
+ ):
271
+ duration = time.time() - start_time
272
+ if ctx.data_engine and step_id:
273
+ relative_time = ctx.data_engine.get_relative_time(time.time())
274
+ ctx.data_engine.record_trace(
275
+ type="action",
276
+ name=action_name,
277
+ payload={
278
+ "action": outcome.action_item,
279
+ "success": outcome.success,
280
+ "error_msg": outcome.error_msg,
281
+ "post_screenshot": post_image_name,
282
+ "timestamp": time.time(),
283
+ "relative_time": relative_time,
284
+ "burst": burst,
285
+ },
286
+ status="success" if outcome.success else "failed",
287
+ duration=duration,
288
+ parent_trace_id=parent_id,
289
+ step_id=step_id,
290
+ trace_id=action_trace_id,
291
+ )
292
+
293
+
294
+ def _normalize_point(ctx, point) -> list[int] | None:
295
+ """Pixel [x, y] -> normalized 0-1000 [x, y] using the device dims."""
296
+ try:
297
+ x, y = int(point[0]), int(point[1])
298
+ except (TypeError, ValueError, IndexError, KeyError):
299
+ return None
300
+ width = getattr(getattr(ctx, "device", None), "device_width", None) or 1080
301
+ height = getattr(getattr(ctx, "device", None), "device_height", None) or 2400
302
+ return [
303
+ max(0, min(1000, round(x * 1000 / width))),
304
+ max(0, min(1000, round(y * 1000 / height))),
305
+ ]
306
+
307
+
308
+ def _incident_evidence(ctx, outcome: _ActionOutcome) -> dict:
309
+ """Structured facts the safety net attached to the action item, normalized."""
310
+ raw = outcome.action_item.pop("safety_net_evidence", None) or {}
311
+ evidence: dict = {}
312
+ if raw.get("new_center"):
313
+ normalized = _normalize_point(ctx, raw["new_center"])
314
+ if normalized:
315
+ evidence["new_location"] = normalized
316
+ if raw.get("new_bounds") is not None:
317
+ evidence["new_bounds"] = raw["new_bounds"]
318
+ if raw.get("occupant"):
319
+ evidence["occupant"] = raw["occupant"]
320
+ if raw.get("occupant_bounds") is not None:
321
+ evidence["occupant_bounds"] = raw["occupant_bounds"]
322
+ return evidence
323
+
324
+
325
+ def _current_step_number(ctx, state: State) -> int | None:
326
+ step_id = getattr(state, "current_step_id", None)
327
+ if ctx.data_engine and step_id:
328
+ try:
329
+ return ctx.data_engine.get_step_number(UUID(step_id))
330
+ except Exception:
331
+ return None
332
+ return None
333
+
334
+
335
+ def _model_facing_action(ctx, action_item: dict) -> dict:
336
+ """The action item in the Operator's own 0–1000 coordinate space.
337
+
338
+ The live item carries the controller's pixels; every phrase rendered for
339
+ a model is normalized first, and the space marker makes that idempotent.
340
+ """
341
+ width = getattr(getattr(ctx, "device", None), "device_width", None) or 1080
342
+ height = getattr(getattr(ctx, "device", None), "device_height", None) or 2400
343
+ return normalize_action_dict(action_item, width, height)
344
+
345
+
346
+ def _build_incident(ctx, state: State, outcome: _ActionOutcome, *, index: int, total: int) -> dict:
347
+ step_number = _current_step_number(ctx, state)
348
+ return open_incident(
349
+ previous=getattr(state, "open_incident", None),
350
+ kind=KIND_SAFETY_NET if outcome.intercepted else KIND_EXEC_ERROR,
351
+ category=outcome.error_category,
352
+ reason=outcome.error_msg,
353
+ action_item=outcome.action_item,
354
+ action_description=format_action_clean(_model_facing_action(ctx, outcome.action_item)),
355
+ action_index=index,
356
+ burst_size=total,
357
+ step_number=step_number,
358
+ evidence=_incident_evidence(ctx, outcome),
359
+ )
360
+
361
+
362
+ async def run_validation_loop(node, state: State) -> dict:
363
+ """Executes all pending Operator decisions and returns the execution report.
364
+
365
+ ``node`` is the ValidatorNode facade; every overridable seam is invoked
366
+ through it.
367
+ """
368
+ ctx = node.ctx
369
+
370
+ actions, error_msg = node._parse_decisions(state.structured_decisions)
371
+ if error_msg:
372
+ return {}
373
+
374
+ execution: list[dict] = []
375
+ actions_to_execute = list(actions)
376
+ total_actions = len(actions_to_execute)
377
+ burst = total_actions > 1
378
+ if burst:
379
+ logger.info(
380
+ f"Fast-action burst: {total_actions} actions will fire back to back"
381
+ " without the safety net."
382
+ )
383
+
384
+ session = await node._get_mcp_session()
385
+
386
+ try:
387
+ last_screenshot_b64, last_screenshot_name = await node._get_initial_screenshot(
388
+ session, state
389
+ )
390
+ # Preserve the turn-initial screenshot (what the operator saw when making decisions)
391
+ decision_screenshot_name = last_screenshot_name
392
+ except Exception:
393
+ return {}
394
+
395
+ step_id = UUID(state.current_step_id) if state.current_step_id else None
396
+
397
+ incident: dict | None = None
398
+ success = False
399
+ index = 0
400
+
401
+ while actions_to_execute:
402
+ action_item = actions_to_execute.pop(0)
403
+ action_name = action_item.get("action")
404
+
405
+ logger.info(f"Executing action: {action_name}")
406
+
407
+ action_trace_id = uuid.uuid4()
408
+ parent_id = CURRENT_TRACE_ID.get()
409
+ token = CURRENT_TRACE_ID.set(action_trace_id)
410
+ start_time = time.time()
411
+
412
+ _record_action_start(ctx, step_id, action_name, action_item, parent_id, action_trace_id)
413
+
414
+ try:
415
+ outcome = await _process_action(
416
+ node, state, session, action_item, action_name, last_screenshot_b64, burst=burst
417
+ )
418
+ success = outcome.success
419
+ execution.append(outcome.action_item)
420
+
421
+ post_image_name = None
422
+ if outcome.post_screenshot_b64:
423
+ post_image_name = _absorb_post_screenshot(ctx, state, outcome.post_screenshot_b64)
424
+ last_screenshot_b64 = outcome.post_screenshot_b64
425
+ if post_image_name:
426
+ last_screenshot_name = post_image_name
427
+
428
+ _record_action_end(
429
+ ctx,
430
+ step_id,
431
+ action_name,
432
+ outcome,
433
+ post_image_name,
434
+ parent_id,
435
+ action_trace_id,
436
+ start_time,
437
+ burst=burst,
438
+ )
439
+ finally:
440
+ CURRENT_TRACE_ID.reset(token)
441
+
442
+ if not success:
443
+ incident = _build_incident(ctx, state, outcome, index=index, total=total_actions)
444
+ logger.warning(
445
+ f"Action failed: {outcome.action_item}. Opened execution incident"
446
+ f" ({incident['kind']}/{incident['category']},"
447
+ f" consecutive={incident['consecutive_failures']}) for the Operator."
448
+ )
449
+ break
450
+ index += 1
451
+
452
+ # Append any skipped actions to the execution log
453
+ for skipped in actions_to_execute:
454
+ skipped_copy = dict(skipped)
455
+ skipped_copy["attempts"] = [BURST_SKIPPED_MARKER if burst else "Skipped"]
456
+ execution.append(skipped_copy)
457
+
458
+ report = {
459
+ "execution": execution,
460
+ "status": "dispatched" if success else "failed",
461
+ "burst": burst,
462
+ "incident": incident,
463
+ }
464
+
465
+ # Determine if a distinct post-action screenshot was captured
466
+ distinct_post_image_name = (
467
+ last_screenshot_name
468
+ if (last_screenshot_name and last_screenshot_name != decision_screenshot_name)
469
+ else None
470
+ )
471
+
472
+ if ctx.data_engine and step_id:
473
+ ctx.data_engine.update_step_execution_result(
474
+ step_id, report, post_image_name=distinct_post_image_name
475
+ )
476
+
477
+ # A successful turn closes whatever incident was open (handing the Operator
478
+ # a one-turn CLOSED notice); a failed one keeps and escalates it.
479
+ closed = None
480
+ previous = getattr(state, "open_incident", None)
481
+ if success and isinstance(previous, dict) and previous.get("kind"):
482
+ closed = dict(previous)
483
+ closed["closed_at_step"] = _current_step_number(ctx, state)
484
+ return {
485
+ "last_execution_result": report,
486
+ "open_incident": incident,
487
+ "last_closed_incident": closed,
488
+ }
@@ -0,0 +1,120 @@
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
+ """Execution failures passed from the Validator to the Operator.
16
+
17
+ Incidents are stored in ``state.open_incident`` and the step's
18
+ ``last_execution_result``. Consecutive failures increment the count; a
19
+ terminal action dispatched without error closes the incident. Prompt rendering lives in
20
+ ``artemis.agents.operator.prompts.ExecutionIncidentPromptComponent``.
21
+ """
22
+
23
+ from dataclasses import asdict, dataclass, field
24
+ import time
25
+ from typing import Any
26
+
27
+ from artemis.agents.validator.categories import ValidationErrorCategory
28
+
29
+ #: The safety net refused to execute the action (target missing/shifted/covered).
30
+ KIND_SAFETY_NET = "safety_net"
31
+ #: The action was dispatched but the device/executor reported a failure.
32
+ KIND_EXEC_ERROR = "exec_error"
33
+
34
+ #: Keys of an action item that are bulky or internal and never useful to the
35
+ #: Operator when the incident is rendered or persisted.
36
+ _ACTION_ITEM_DROP_KEYS = frozenset(
37
+ {
38
+ "attempts",
39
+ "safety_net_evidence",
40
+ "thought",
41
+ }
42
+ )
43
+
44
+
45
+ @dataclass
46
+ class ExecutionIncident:
47
+ """One blocked turn-ending action, as reported to the Operator."""
48
+
49
+ kind: str
50
+ category: str
51
+ reason: str
52
+ action: dict[str, Any]
53
+ action_description: str
54
+ action_index: int = 0
55
+ burst_size: int = 1
56
+ step_number: int | None = None
57
+ consecutive_failures: int = 1
58
+ evidence: dict[str, Any] = field(default_factory=dict)
59
+ opened_at: float = field(default_factory=time.time)
60
+
61
+ @property
62
+ def is_burst(self) -> bool:
63
+ return self.burst_size > 1
64
+
65
+ def to_dict(self) -> dict[str, Any]:
66
+ return asdict(self)
67
+
68
+ @classmethod
69
+ def from_dict(cls, data: dict[str, Any] | None) -> "ExecutionIncident | None":
70
+ if not isinstance(data, dict) or not data.get("kind"):
71
+ return None
72
+ known = {f for f in cls.__dataclass_fields__}
73
+ # Older step ledgers may lack these fields.
74
+ kwargs: dict[str, Any] = {
75
+ "category": str(ValidationErrorCategory.GENERAL),
76
+ "reason": "",
77
+ "action": {},
78
+ "action_description": "",
79
+ }
80
+ kwargs.update({k: v for k, v in data.items() if k in known})
81
+ return cls(**kwargs)
82
+
83
+
84
+ def sanitize_action_item(action_item: dict[str, Any]) -> dict[str, Any]:
85
+ """A copy of the action item without bulky/internal keys."""
86
+ return {k: v for k, v in (action_item or {}).items() if k not in _ACTION_ITEM_DROP_KEYS}
87
+
88
+
89
+ def open_incident(
90
+ *,
91
+ previous: dict[str, Any] | None,
92
+ kind: str,
93
+ category: ValidationErrorCategory | str,
94
+ reason: str,
95
+ action_item: dict[str, Any],
96
+ action_description: str,
97
+ action_index: int = 0,
98
+ burst_size: int = 1,
99
+ step_number: int | None = None,
100
+ evidence: dict[str, Any] | None = None,
101
+ ) -> dict[str, Any]:
102
+ """Builds the incident dict for a failed action.
103
+
104
+ Continue the failure count from ``previous`` if an incident is still open.
105
+ """
106
+ prior = ExecutionIncident.from_dict(previous)
107
+ consecutive = (prior.consecutive_failures + 1) if prior else 1
108
+ incident = ExecutionIncident(
109
+ kind=kind,
110
+ category=str(category.value if isinstance(category, ValidationErrorCategory) else category),
111
+ reason=(reason or "").strip(),
112
+ action=sanitize_action_item(action_item),
113
+ action_description=action_description,
114
+ action_index=action_index,
115
+ burst_size=burst_size,
116
+ step_number=step_number,
117
+ consecutive_failures=consecutive,
118
+ evidence=dict(evidence or {}),
119
+ )
120
+ return incident.to_dict()
@@ -0,0 +1,15 @@
1
+ Role
2
+ UI State Verification Expert. The Operator decided to act at the red dot while looking at Image 1 (Reference). Image 2 (Current State) is the live screen now. Decide whether the action can still land on its intended target.
3
+
4
+ Read the `[Target]` block first to determine which rule applies.
5
+
6
+ Rules
7
+ 1. `Kind: specific UI control` — the target has a label, resource id, class or bounds. Judge whether that exact control remains visible at the red dot in Image 2. Ignore video, map, animation or background changes beneath it. A control that has hidden, faded, collapsed, moved or become covered is not present, even if tapping the same spot could restore it.
8
+ 2. `Kind: described target` — the Operator aimed at a coordinate and stated what it is (`Operator's description`). That statement is the Operator's belief, not an observation: confirm against Image 1 what actually sits under the red dot. If the description names a control (button, icon, link, handle, field, knob) and Image 1 shows one there, apply Rule 1 to that control. If the description names a surface (video body, backdrop, blank area, map) or Image 1 shows no distinct control, apply the surface rule.
9
+ 3. `Kind: coordinates only` — no target metadata at all. Identify what appears under the red dot in both images. If Image 1 shows a distinct button, icon, link or handle, apply Rule 1. Apply the surface rule only when Image 1 shows blank space, a backdrop, or the body of a video, animation, map or live stream.
10
+ 4. Surface rule. Typical intents: tap the video body to wake its hidden controls, tap the backdrop to dismiss a dialog. Surfaces are expected to change between frames: ignore natural frame changes and the appearance or fading of overlay controls. Output `{"is_present": true, "confidence": 1.0}` unless an unexpected blocking popup now covers the red dot.
11
+ 5. Use `[Planned Action & Original Thinking]` to resolve whether the target was a control or its underlying surface. Do not treat a missing control as present because another tap could restore it.
12
+
13
+ Output
14
+ Return EXACTLY this raw JSON and nothing else. Keep reasoning strictly under 35 words.
15
+ {"reasoning": "brief explanation", "is_present": boolean, "confidence": float}