android-midscene-automation 0.2.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +63 -1
  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 +10 -8
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +13 -1
  507. package/src/App.vue +72 -23
  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 +369 -186
  525. package/src/types.ts +1 -1
@@ -0,0 +1,1167 @@
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
+ """Universal Multi-Model FlashRunner for Artemis.
16
+
17
+ Executes autonomous reactive mobile workflows across Google Gemini,
18
+ OpenAI GPT-4o/o3, Anthropic Claude 3.5/3.7, and OpenRouter endpoints.
19
+
20
+ Conversation shape (shared with the Pro operator, history redesign §3.2):
21
+ the prompt is built every turn from a session :class:`TranscriptLedger` —
22
+ a byte-stable system prefix, the committed earlier turns (append-only; old UI
23
+ lists stripped at depth 1, screenshots resolved to visual summaries at depth
24
+ K, long spans chunk-compressed), and a fresh tail carrying the current
25
+ observation under a ``# CURRENT OBSERVATION [T+mm:ss]`` header. Actions run
26
+ synchronously and their tool messages already carry the outcome; only a turn
27
+ whose action failed ends with an ``--- Action Execution Result (T+mm:ss) ---``
28
+ message (it carries the error). Every timestamp the model sees is a
29
+ session-relative offset — the same clock the video analyzer uses for the
30
+ session recording.
31
+
32
+ Per-turn notices on the tail (the reasoning reminder after a silent turn, the
33
+ no-tool-call nudge, the final-turn warning, the user-guidance wrapper) are
34
+ marked ephemeral: the ledger's scrub edge deletes them once the message leaves
35
+ the recent window, so they never reach the frozen history or a chunk capsule.
36
+ The verbatim body of a user instruction is the exception: it stays in the
37
+ active window until its turn is chunked, whose user lines then carry it on.
38
+ """
39
+
40
+ import asyncio
41
+ import base64
42
+ from dataclasses import dataclass, field
43
+ import json
44
+ from pathlib import Path
45
+ import re
46
+ import uuid
47
+
48
+ from jinja2 import Template
49
+ from langchain_core.messages import (
50
+ BaseMessage,
51
+ HumanMessage,
52
+ SystemMessage,
53
+ ToolMessage,
54
+ )
55
+
56
+ from artemis.agents.flash.summarizer import VisualStepSummarizer, build_focus_context
57
+ from artemis.agents.operator.prompts import REASONING_REMINDER, UserGuidance, render_user_guidance
58
+ from artemis.agents.validator.tool_declarations import (
59
+ ASK_EXPLORER_TOOL,
60
+ CLICK_SEQUENCE_TOOL,
61
+ LIST_NOTES_TOOL,
62
+ READ_NOTE_TOOL,
63
+ REPORT_TASK_STATUS_TOOL,
64
+ VALIDATOR_TOOLS_DECLARATION,
65
+ capture_screenshot_and_parse_ui,
66
+ prune_intermediate_screenshots,
67
+ )
68
+ from artemis.config import (
69
+ MemoryRuntimeConfig,
70
+ MemoryTranscriptConfig,
71
+ StepSummarizerConfig,
72
+ load_agent_config,
73
+ )
74
+ from artemis.context import ArtemisContext
75
+ from artemis.controllers.unified_controller import UnifiedMobileController
76
+ from artemis.data_engine.trace import trace
77
+ from artemis.graph.perception import _check_injected_instruction_file
78
+ from artemis.graph.state import State
79
+ from artemis.llm.structured import ParseFailure, parse_structured
80
+ from artemis.mcp.action_executor import McpActionExecutor
81
+ from artemis.mcp.observation import observe
82
+ from artemis.memory.transcript import PRO_UI_LIST_MARKER, TranscriptLedger, mark_ephemeral
83
+ from artemis.services.llm import (
84
+ acomplete,
85
+ get_llm,
86
+ invoke_llm_with_timeout_message,
87
+ )
88
+ from artemis.tools.history import history_tool_declarations
89
+ from artemis.tools.tool_wrapper import tool_result_messages
90
+ from artemis.utils.coordinates import (
91
+ COORDINATE_SPACE_KEY,
92
+ COORDINATE_SPACE_NORMALIZED,
93
+ parse_swipe_parameters,
94
+ )
95
+ from third_party.mobile_use.utils.logger import get_logger
96
+
97
+ logger = get_logger(__name__)
98
+
99
+ _NO_TOOL_CALL_NOTICE = (
100
+ "You did not call any tools last turn. Please make progress by calling an"
101
+ " action tool or 'report_task_status'."
102
+ )
103
+
104
+ _FINAL_TURN_WARNING = "[WARNING] This is your final turn; only 'report_task_status' is available."
105
+
106
+
107
+ @dataclass
108
+ class _TurnRecord:
109
+ """What one reactive turn produced, for committing it into the ledger.
110
+
111
+ ``step_keys`` are the DataEngine step ids recorded this turn (one per
112
+ executed action; the tool_call_id stands in without a DataEngine) — the
113
+ first keys the observation screenshot to its visual-transition summary,
114
+ all of them feed the chunk ledger. ``actions`` collects the outcome of
115
+ every device action so the turn's execution result can be rendered.
116
+ """
117
+
118
+ step_keys: list[str] = field(default_factory=list)
119
+ actions: list[tuple[str, str, str]] = field(default_factory=list)
120
+
121
+ def result(self) -> dict | None:
122
+ """The turn's execution result in the validator-report shape.
123
+
124
+ ``None`` unless an action failed: Flash executes synchronously and each
125
+ action's tool message already carries its outcome, so a result message
126
+ would only repeat it. A failure is worth its own message because it
127
+ carries the error the turn ended on.
128
+ """
129
+ for name, status, text in self.actions:
130
+ if status != "success":
131
+ # Executor messages name the action themselves ("Error
132
+ # executing click: ..."); prefix only the ones that don't.
133
+ return {"status": "failed", "error": text if name in text else f"{name}: {text}"}
134
+ return None
135
+
136
+
137
+ class FlashRunner:
138
+ """Reactive, ultra-fast agent execution loop supporting all VLM providers."""
139
+
140
+ def __init__(self, ctx: ArtemisContext, goal: str, max_turns: int | None = None):
141
+ self.ctx = ctx
142
+ self.goal = goal
143
+ try:
144
+ cfg = load_agent_config()
145
+ self.max_turns = max_turns if max_turns is not None else cfg.flash.max_turns
146
+ self.step_summarizer_cfg = cfg.flash.step_summarizer
147
+ self.memory_runtime_cfg = cfg.memory.runtime
148
+ self.transcript_cfg = cfg.memory.transcript
149
+ self.chunking_cfg = cfg.memory.chunking
150
+ except Exception:
151
+ self.max_turns = max_turns if max_turns is not None else 0
152
+ self.step_summarizer_cfg = StepSummarizerConfig()
153
+ self.memory_runtime_cfg = MemoryRuntimeConfig()
154
+ self.transcript_cfg = MemoryTranscriptConfig()
155
+ self.chunking_cfg = None
156
+
157
+ self.controller = UnifiedMobileController(ctx)
158
+ # ``agent_name="flash"`` makes ask_explorer follow ``explorer.flash_mode``
159
+ # (the Flash profile knob) instead of the Pro profile's tier.
160
+ self.executor = McpActionExecutor(ctx, self.controller, agent_name="flash")
161
+ self.summarizer = (
162
+ VisualStepSummarizer(
163
+ ctx,
164
+ model_name=self.step_summarizer_cfg.model,
165
+ retry_limit=self.memory_runtime_cfg.retry_limit,
166
+ max_concurrency=self.memory_runtime_cfg.max_concurrency,
167
+ flush_timeout_s=self.memory_runtime_cfg.flush_timeout_s,
168
+ )
169
+ if self.step_summarizer_cfg.enabled
170
+ else None
171
+ )
172
+ # Publish the service on the composition root (ctx.step_memory) so
173
+ # any co-resident consumer shares this run's summary runtime.
174
+ if self.summarizer is not None and getattr(ctx, "step_memory", None) is None:
175
+ try:
176
+ ctx.step_memory = self.summarizer
177
+ except (AttributeError, TypeError, ValueError):
178
+ pass
179
+
180
+ @property
181
+ def turn_limit(self) -> int | None:
182
+ """The reactive turn cap, or ``None`` when the loop is unbounded."""
183
+ try:
184
+ limit = int(self.max_turns) if self.max_turns is not None else 0
185
+ except (TypeError, ValueError):
186
+ return None
187
+ return limit if limit > 0 else None
188
+
189
+ def _video_tools_enabled(self) -> bool:
190
+ """Same gate the Pro graph applies before binding ``video_analyzer``."""
191
+ setup = getattr(self.ctx, "execution_setup", None)
192
+ return getattr(setup, "video_recording_tools_enabled", False) is True
193
+
194
+ def _get_tools(self) -> list:
195
+ # Flash has no tool that writes notes and its prompt never teaches
196
+ # them, so the Validator set's note readers are left out here.
197
+ tools = [
198
+ t
199
+ for t in VALIDATOR_TOOLS_DECLARATION
200
+ if t is not READ_NOTE_TOOL and t is not LIST_NOTES_TOOL
201
+ ]
202
+ tools.insert(1, CLICK_SEQUENCE_TOOL)
203
+ tools.append(ASK_EXPLORER_TOOL)
204
+ # Helper tools shared with the Pro operator: the history tools are
205
+ # declared from the very same args schemas the LangChain tools bind
206
+ # (same availability gates: a DataEngine session, recall config).
207
+ tools.extend(history_tool_declarations(self.ctx))
208
+ if self._video_tools_enabled():
209
+ from artemis.tools.video_tool import VIDEO_ANALYZER_TOOL
210
+
211
+ tools.append(VIDEO_ANALYZER_TOOL)
212
+ tools.append(REPORT_TASK_STATUS_TOOL)
213
+ # With an actuator installed, drop declarations for device actions the
214
+ # backend does not implement (and append its extension tools); without
215
+ # one, the full historical declaration set is kept.
216
+ actuator = getattr(self.ctx, "actuator", None)
217
+ if actuator is not None:
218
+ from artemis.mcp.action_manifest import filter_declarations
219
+
220
+ tools = filter_declarations(tools, actuator, "flash")
221
+ return tools
222
+
223
+ def _prune_intermediate_screenshots(self, messages: list[BaseMessage]) -> None:
224
+ """Prunes binary screenshot blocks from intermediate observation messages."""
225
+ prune_intermediate_screenshots(messages)
226
+
227
+ # ------------------------------------------------------------------
228
+ # run() setup helpers
229
+ # ------------------------------------------------------------------
230
+
231
+ def _build_ledger(self) -> TranscriptLedger:
232
+ """Builds the session transcript ledger with the shared Pro history policy.
233
+
234
+ The ``T+mm:ss`` clock is anchored to the DataEngine session start so
235
+ the observation headers, the chunk ledger lines and the video
236
+ analyzer's action timeline share one origin. L2/L3 chunk compression
237
+ needs DataEngine step records; without an engine the ledger runs
238
+ scrub-edge-only.
239
+ """
240
+ engine = getattr(self.ctx, "data_engine", None)
241
+ session_start = getattr(engine, "session_start_time", None) if engine else None
242
+ cfg = self.transcript_cfg
243
+ ledger = TranscriptLedger(
244
+ step_memory=self.summarizer,
245
+ prune_history_xml=self.step_summarizer_cfg.prune_history_xml,
246
+ image_scrub_depth=getattr(cfg, "image_scrub_depth", 3),
247
+ pending_grace_steps=getattr(cfg, "pending_grace_steps", 3),
248
+ xml_scrub_depth=getattr(cfg, "xml_scrub_depth", 1),
249
+ image_scrub_depth_relaxed=getattr(cfg, "image_scrub_depth_relaxed", None),
250
+ context_budget_tokens=getattr(cfg, "context_budget_tokens", None),
251
+ start_ratio=getattr(cfg, "start_ratio", None),
252
+ session_start=session_start if isinstance(session_start, (int, float)) else None,
253
+ )
254
+ if engine is not None:
255
+ try:
256
+ from artemis.memory import HistoryChunkManager
257
+
258
+ ledger.attach_chunker(
259
+ HistoryChunkManager(
260
+ engine=engine,
261
+ ctx=self.ctx,
262
+ chunking_config=self.chunking_cfg,
263
+ transcript_config=cfg,
264
+ goal=self.goal,
265
+ )
266
+ )
267
+ except Exception as e:
268
+ logger.error(f"History chunk manager unavailable for FlashRunner: {e}")
269
+ try:
270
+ self.ctx.transcript_ledger = ledger
271
+ except (AttributeError, TypeError, ValueError):
272
+ pass
273
+ return ledger
274
+
275
+ #: The model of the current run; decides how screenshot tool results travel.
276
+ _llm = None
277
+
278
+ def _init_llm(self):
279
+ """Initializes the Universal LLM via the Service Layer."""
280
+ return get_llm(self.ctx, name="operator")
281
+
282
+ def _render_system_prompt(self, tools_declaration: list) -> str:
283
+ """Renders the system prompt from the flash_runner.md template.
284
+
285
+ Tool-teaching segments are gated on the available tool set so an absent
286
+ tool leaves no trace in the prompt; until an actuator is wired in, the
287
+ full manifest set reproduces the historical prompt.
288
+ """
289
+ prompt_path = Path(__file__).parent / "flash_runner.md"
290
+ prompt_template = prompt_path.read_text(encoding="utf-8")
291
+ available_tools = frozenset(t.name for t in tools_declaration)
292
+ return Template(prompt_template).render(goal=self.goal, available_tools=available_tools)
293
+
294
+ # ------------------------------------------------------------------
295
+ # Per-turn helpers (observe / think)
296
+ # ------------------------------------------------------------------
297
+
298
+ async def _read_injected_instruction(
299
+ self, offset_label: str | None = None
300
+ ) -> tuple[str | None, UserGuidance | None]:
301
+ """Consumes this turn's injected instruction: ``(instruction, guidance)``.
302
+
303
+ ``instruction`` is the user's text verbatim — what the step record,
304
+ the chunk ledger and the visual-transition lens receive (the same
305
+ shape Pro stamps). ``guidance`` is the shared user-guidance pair
306
+ (:func:`render_user_guidance`, the Pro rendering without a plan to
307
+ edit): the ephemeral ``--- User Guidance ---`` wrapper and the
308
+ persistent verbatim body, both riding on the observation tail.
309
+ ``offset_label`` stamps the body with the session offset. Both are
310
+ None when nothing was injected.
311
+ """
312
+ if not (self.ctx.data_engine and self.ctx.data_engine.base_dir):
313
+ return None, None
314
+ try:
315
+ injected_payload = await asyncio.to_thread(
316
+ _check_injected_instruction_file,
317
+ str(self.ctx.data_engine.base_dir),
318
+ )
319
+ except (OSError, ValueError, AttributeError) as e:
320
+ logger.warning(f"Failed to check injected instruction in FlashRunner: {e}")
321
+ return None, None
322
+ if not (injected_payload and injected_payload.get("instruction")):
323
+ return None, None
324
+ instruction = str(injected_payload["instruction"])
325
+ guidance = render_user_guidance(
326
+ instruction,
327
+ release_loop=bool(injected_payload.get("release_loop")),
328
+ has_plan=False,
329
+ offset_label=offset_label,
330
+ )
331
+ return instruction, guidance
332
+
333
+ def _build_tail(
334
+ self,
335
+ ledger: TranscriptLedger,
336
+ turns: int,
337
+ img_bytes,
338
+ xml_list,
339
+ *,
340
+ injected: UserGuidance | None = None,
341
+ notices: list[str] | None = None,
342
+ is_final: bool = False,
343
+ previous_turn_silent: bool = False,
344
+ ) -> HumanMessage:
345
+ """Builds this turn's observation tail (Pro observation shape).
346
+
347
+ Header, screenshot and UI list carry the same markers as the Pro
348
+ operator's tail so the ledger's scrub edge treats both alike. The
349
+ objective is stated once, in the system prompt; the tail never repeats
350
+ it. Every per-turn notice (nudges, the user-guidance wrapper, the
351
+ final-turn warning, the reasoning reminder after a silent turn) is
352
+ marked ephemeral so it leaves the transcript with the recent window.
353
+ The verbatim body of an injected instruction is the one exception: it
354
+ is a regular block, so a standing instruction stays in the active
355
+ window until the turn is chunked (the chunk's user lines carry it on).
356
+ """
357
+ # ``turns`` is kept for call-site symmetry: the tail has the same shape
358
+ # on every turn.
359
+ blocks: list[dict] = [
360
+ {"type": "text", "text": f"# CURRENT OBSERVATION [{ledger.elapsed_label()}]"}
361
+ ]
362
+ if img_bytes:
363
+ img_b64 = base64.b64encode(img_bytes).decode("utf-8")
364
+ blocks.append({"type": "text", "text": "--- Current Screenshot ---"})
365
+ blocks.append(
366
+ {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}}
367
+ )
368
+ if xml_list:
369
+ blocks.append({"type": "text", "text": f"{PRO_UI_LIST_MARKER}\n{xml_list}"})
370
+ ephemeral: list[int] = []
371
+ # (text, ephemeral) in tail order.
372
+ per_turn: list[tuple[str, bool]] = [(text, True) for text in (notices or [])]
373
+ if injected:
374
+ # The wrapper is per-turn; the verbatim body right after it is a
375
+ # regular block that stays until the turn is chunked.
376
+ per_turn.append((injected.wrapper, True))
377
+ per_turn.append((injected.body, False))
378
+ if is_final:
379
+ per_turn.append((_FINAL_TURN_WARNING, True))
380
+ if previous_turn_silent:
381
+ per_turn.append((REASONING_REMINDER, True))
382
+ for text, is_ephemeral in per_turn:
383
+ if is_ephemeral:
384
+ ephemeral.append(len(blocks))
385
+ blocks.append({"type": "text", "text": text})
386
+ tail = HumanMessage(content=blocks)
387
+ if ephemeral:
388
+ mark_ephemeral(tail, ephemeral)
389
+ return tail
390
+
391
+ def _extract_response_text(self, response) -> str:
392
+ """Extracts the natural-language thought text from the model response."""
393
+ raw_text = response.content if isinstance(response.content, str) else ""
394
+ if isinstance(response.content, list):
395
+ raw_text = "".join(
396
+ b.get("text", "") for b in response.content if isinstance(b, dict) and "text" in b
397
+ )
398
+ return raw_text
399
+
400
+ def _extract_response_thinking(self, response) -> str:
401
+ """Extracts the model's native thinking blocks (thought summaries), if any.
402
+
403
+ Providers running with ``include_thoughts`` return them as ``thinking``
404
+ content blocks next to the ``text`` blocks; they are recorded on the step
405
+ as ``operator_native_thinking`` and never fed back as reply text.
406
+ """
407
+ if not isinstance(response.content, list):
408
+ return ""
409
+ parts = [
410
+ str(b.get("thinking") or "")
411
+ for b in response.content
412
+ if isinstance(b, dict) and b.get("type") == "thinking"
413
+ ]
414
+ return "\n".join(p for p in parts if p.strip())
415
+
416
+ def _token_usage_from_response(self, response) -> dict | None:
417
+ """Extracts token usage metadata from the model response, if present."""
418
+ step_token_usage = None
419
+ if hasattr(response, "usage_metadata") and response.usage_metadata:
420
+ u = response.usage_metadata
421
+ pr = u.get("input_tokens") or u.get("prompt_tokens") or 0
422
+ co = u.get("output_tokens") or u.get("completion_tokens") or 0
423
+ to = u.get("total_tokens") or (pr + co)
424
+ if to > 0:
425
+ step_token_usage = {
426
+ "prompt_tokens": int(pr),
427
+ "completion_tokens": int(co),
428
+ "total_tokens": int(to),
429
+ }
430
+ elif hasattr(response, "response_metadata") and isinstance(
431
+ response.response_metadata, dict
432
+ ):
433
+ u = response.response_metadata.get("usage_metadata") or response.response_metadata.get(
434
+ "token_usage"
435
+ )
436
+ if isinstance(u, dict):
437
+ pr = (
438
+ u.get("input_tokens")
439
+ or u.get("prompt_tokens")
440
+ or u.get("prompt_token_count")
441
+ or 0
442
+ )
443
+ co = (
444
+ u.get("output_tokens")
445
+ or u.get("completion_tokens")
446
+ or u.get("candidates_token_count")
447
+ or 0
448
+ )
449
+ to = u.get("total_tokens") or u.get("total_token_count") or (pr + co)
450
+ if to > 0:
451
+ step_token_usage = {
452
+ "prompt_tokens": int(pr),
453
+ "completion_tokens": int(co),
454
+ "total_tokens": int(to),
455
+ }
456
+ return step_token_usage
457
+
458
+ def _estimate_token_usage(self, messages: list[BaseMessage], raw_text: str) -> dict:
459
+ """Approximates token usage from message sizes when metadata is absent."""
460
+ calc_prompt_chars = 0
461
+ calc_images = 0
462
+ for msg in messages:
463
+ c = getattr(msg, "content", "")
464
+ if isinstance(c, str):
465
+ calc_prompt_chars += len(c)
466
+ elif isinstance(c, list):
467
+ for block in c:
468
+ if isinstance(block, dict):
469
+ if block.get("type") == "image_url" or "image_url" in block:
470
+ calc_images += 1
471
+ else:
472
+ calc_prompt_chars += len(str(block.get("text", "")))
473
+ else:
474
+ calc_prompt_chars += len(str(block))
475
+ prompt_tokens = (calc_prompt_chars // 4) + (calc_images * 258)
476
+ completion_tokens = len(raw_text) // 4
477
+ return {
478
+ "prompt_tokens": max(1, prompt_tokens),
479
+ "completion_tokens": max(1, completion_tokens),
480
+ "total_tokens": max(1, prompt_tokens + completion_tokens),
481
+ }
482
+
483
+ def _resolve_token_usage(
484
+ self, response, messages: list[BaseMessage], raw_text: str
485
+ ) -> tuple[dict, bool]:
486
+ """Token usage for the call and whether the provider measured it.
487
+
488
+ Falls back to an estimate when the response carries no usage
489
+ metadata; the flag lets the caller keep an estimate out of the
490
+ ledger's chars-per-token calibration (an estimate is itself derived
491
+ from character counts, so calibrating on it would be circular).
492
+ """
493
+ step_token_usage = self._token_usage_from_response(response)
494
+ if step_token_usage and step_token_usage.get("total_tokens", 0) > 0:
495
+ return step_token_usage, True
496
+ return self._estimate_token_usage(messages, raw_text), False
497
+
498
+ def _record_llm_trace(self, step_token_usage: dict, raw_text: str) -> None:
499
+ """Records the llm_call trace for this turn in the DataEngine."""
500
+ if self.ctx.data_engine:
501
+ current_step_id = getattr(self.ctx.data_engine, "current_step_id", None)
502
+ self.ctx.data_engine.record_trace(
503
+ type="llm_call",
504
+ name="FlashRunner",
505
+ payload={"token_usage": step_token_usage, "response": raw_text},
506
+ step_id=current_step_id,
507
+ status="success",
508
+ )
509
+
510
+ def _resolve_tool_calls(self, response, raw_text: str) -> list:
511
+ """Returns native tool calls, falling back to text parsing if absent."""
512
+ tool_calls = response.tool_calls or []
513
+
514
+ # Fallback text parsing if tool_calls not parsed natively
515
+ if not tool_calls and "```json" in raw_text:
516
+ parsed = parse_structured(raw_text)
517
+ if isinstance(parsed, ParseFailure):
518
+ logger.warning(
519
+ "FlashRunner response contained a JSON block that could"
520
+ f" not be parsed: {parsed.error}"
521
+ )
522
+ elif isinstance(parsed, dict) and "name" in parsed:
523
+ tool_calls = [
524
+ {
525
+ "name": parsed["name"],
526
+ "args": parsed.get("args", {}),
527
+ "id": str(uuid.uuid4()),
528
+ }
529
+ ]
530
+ return tool_calls
531
+
532
+ # ------------------------------------------------------------------
533
+ # Per-tool-call helpers (act / record / report)
534
+ # ------------------------------------------------------------------
535
+
536
+ async def _finalize_task_report(
537
+ self,
538
+ name: str,
539
+ args: dict,
540
+ tc_id: str,
541
+ raw_text: str,
542
+ step_token_usage: dict,
543
+ pre_screenshot_bytes,
544
+ xml_list,
545
+ messages: list[BaseMessage],
546
+ native_text: str | None = None,
547
+ ) -> dict:
548
+ """Records the final step, acknowledges the tool call, and flushes."""
549
+ final_report = args
550
+ if self.ctx.data_engine:
551
+ try:
552
+ if self.ctx.data_engine.current_step_id is None:
553
+ self.ctx.data_engine.allocate_step_id()
554
+ self.ctx.data_engine.record_step(
555
+ pre_screenshot_bytes=pre_screenshot_bytes,
556
+ ui_tree=xml_list,
557
+ action_taken={"action": "report_task_status", "args": args},
558
+ operator_raw_thinking=raw_text,
559
+ operator_native_thinking=native_text or None,
560
+ last_execution_result={"result": "Task completed with final report."},
561
+ extra_metadata={"token_usage": step_token_usage},
562
+ )
563
+ except Exception as step_err:
564
+ logger.warning(f"Error recording final step in FlashRunner: {step_err}")
565
+ messages.append(
566
+ ToolMessage(
567
+ tool_call_id=tc_id,
568
+ name=name,
569
+ content=json.dumps({"status": "acknowledged"}),
570
+ status="success",
571
+ )
572
+ )
573
+ if self.summarizer:
574
+ await self.summarizer.flush()
575
+ return final_report
576
+
577
+ async def _capture_post_screenshot(
578
+ self, exec_result, name: str, action_names, state: State | None = None
579
+ ):
580
+ """Returns the post-action screenshot, capturing a fallback if needed.
581
+
582
+ When a device action produced no observation (a failed action, an
583
+ unknown tool, ...) the screen is observed through the very same
584
+ pipeline the executor's success path uses (:func:`observe`), so the
585
+ next observation tail carries the indexed minimal UI list and
586
+ ``state`` gets the same refreshed screenshot path and indexed
587
+ elements. Nothing is captured for non-action tools.
588
+ """
589
+ post_img_bytes = exec_result.screenshot_bytes
590
+ if post_img_bytes or name not in action_names:
591
+ return post_img_bytes
592
+ try:
593
+ obs, screenshot_bytes = await observe(self.ctx, self.controller, settle_ms=0)
594
+ except Exception as shot_err:
595
+ logger.warning(f"Failed to capture fallback screenshot in FlashRunner: {shot_err}")
596
+ return post_img_bytes
597
+ if not obs.ok:
598
+ logger.warning(f"Failed to capture fallback screenshot in FlashRunner: {obs.message}")
599
+ return post_img_bytes
600
+ post_img_bytes = screenshot_bytes
601
+ if not exec_result.ui_elements_text:
602
+ exec_result.ui_elements_text = obs.elements_text
603
+ if not exec_result.screenshot_path and obs.screenshot_path:
604
+ exec_result.screenshot_path = obs.screenshot_path
605
+ if state is not None:
606
+ if obs.hierarchy_ok:
607
+ state.indexed_points = [el["center"] for el in obs.elements]
608
+ state.indexed_elements = obs.elements
609
+ if obs.screenshot_path:
610
+ state.latest_screenshot = obs.screenshot_path
611
+ return post_img_bytes
612
+
613
+ def _extract_normalized_coordinates(self, name: str, args: dict, resolved=None):
614
+ """Extracts and enriches coordinate metadata for the recorded action.
615
+
616
+ ``resolved`` is the normalized point the executor sent a point action
617
+ to; it is the coordinate of record when the model addressed an element
618
+ by index (the arguments then carry no point at all).
619
+ """
620
+ norm_coords = None
621
+ norm_start = None
622
+ norm_end = None
623
+ if name in ("click", "tap", "long_press", "input_text") and (
624
+ isinstance(resolved, (list, tuple)) and len(resolved) == 2
625
+ ):
626
+ return [int(resolved[0]), int(resolved[1])], None, None
627
+ if name == "swipe":
628
+ kind, target_val, _ = parse_swipe_parameters(args)
629
+ if kind == "coords" and isinstance(target_val, list) and len(target_val) == 4:
630
+ norm_coords = target_val
631
+ norm_start = target_val[:2]
632
+ norm_end = target_val[2:]
633
+ elif kind == "direction" and isinstance(target_val, str):
634
+ g_lower = target_val.lower()
635
+ if "up" in g_lower:
636
+ norm_coords = [600, 700, 600, 300]
637
+ elif "down" in g_lower:
638
+ norm_coords = [600, 300, 600, 700]
639
+ elif "left" in g_lower:
640
+ norm_coords = [750, 500, 250, 500]
641
+ elif "right" in g_lower:
642
+ norm_coords = [250, 500, 750, 500]
643
+ if norm_coords:
644
+ norm_start = norm_coords[:2]
645
+ norm_end = norm_coords[2:]
646
+ elif name in ("click", "tap", "long_press", "input_text"):
647
+ target = args.get("target") or args.get("coordinates")
648
+ if isinstance(target, (list, tuple)) and len(target) == 2:
649
+ norm_coords = list(target)
650
+ elif isinstance(target, str):
651
+ nums = re.findall(r"-?\d+(?:\.\d+)?", target)
652
+ if len(nums) == 2:
653
+ norm_coords = [int(float(nums[0])), int(float(nums[1]))]
654
+ return norm_coords, norm_start, norm_end
655
+
656
+ def _build_action_record(self, name: str, args: dict, exec_result) -> dict:
657
+ """The action as the step record and the visual lens see it.
658
+
659
+ Flash records the model's own 0–1000 coordinates verbatim (an index
660
+ target records the point the executor resolved it to); the explicit
661
+ space marker keeps every later normalization pass (agent-friendly
662
+ steps, MCP inspector) a no-op on this record. The tool arguments ride
663
+ along under ``args``, and the target semantics the executor validated
664
+ (observed element fields for an index, the model's own
665
+ ``target_description`` for a point) land top level in the shape Pro
666
+ records.
667
+ """
668
+ metadata = getattr(exec_result, "metadata", None) or {}
669
+ resolved = metadata.get("target_coordinates")
670
+ norm_coords, norm_start, norm_end = self._extract_normalized_coordinates(
671
+ name, args, resolved=resolved
672
+ )
673
+ given = args.get("target") or args.get("coordinates") or args.get("sequence")
674
+ if isinstance(given, (int, float)) and not isinstance(given, bool):
675
+ given = None # An element index is not a coordinate.
676
+ action_dict = {
677
+ "action": name,
678
+ "coordinates": given or norm_coords,
679
+ COORDINATE_SPACE_KEY: COORDINATE_SPACE_NORMALIZED,
680
+ "args": args,
681
+ }
682
+ if norm_coords:
683
+ action_dict["normalized_coordinates"] = norm_coords
684
+ if norm_start and norm_end:
685
+ action_dict["normalized_start_coordinates"] = norm_start
686
+ action_dict["normalized_end_coordinates"] = norm_end
687
+ action_dict.update(metadata.get("target_semantics") or {})
688
+ return action_dict
689
+
690
+ def _record_action_step(
691
+ self,
692
+ name: str,
693
+ args: dict,
694
+ exec_result,
695
+ raw_text: str,
696
+ step_token_usage: dict,
697
+ pre_screenshot_bytes,
698
+ xml_list,
699
+ post_img_bytes,
700
+ injected: str | None = None,
701
+ native_text: str | None = None,
702
+ action_dict: dict | None = None,
703
+ ):
704
+ """Records telemetry / step in DataEngine; returns the step id or None."""
705
+ recorded_step_id = None
706
+ try:
707
+ if self.ctx.data_engine.current_step_id is None:
708
+ self.ctx.data_engine.allocate_step_id()
709
+
710
+ if action_dict is None:
711
+ action_dict = self._build_action_record(name, args, exec_result)
712
+
713
+ succeeded = exec_result.status == "success"
714
+ last_execution_result = {
715
+ "status": "dispatched" if succeeded else "failed",
716
+ "result": exec_result.text_summary,
717
+ }
718
+ if not succeeded:
719
+ last_execution_result["error"] = exec_result.text_summary
720
+
721
+ extra_metadata: dict = {"token_usage": step_token_usage}
722
+ if injected:
723
+ # Stamped verbatim on the step it reached: the chunk ledger
724
+ # keeps it as a never-evicted line at every compression level.
725
+ extra_metadata["injected_instruction"] = injected
726
+
727
+ recorded_step_id = self.ctx.data_engine.record_step(
728
+ pre_screenshot_bytes=pre_screenshot_bytes,
729
+ post_screenshot_bytes=post_img_bytes,
730
+ ui_tree=(exec_result.ui_elements_text or xml_list),
731
+ action_taken=action_dict,
732
+ operator_raw_thinking=raw_text,
733
+ operator_native_thinking=native_text or None,
734
+ last_execution_result=last_execution_result,
735
+ extra_metadata=extra_metadata,
736
+ )
737
+ self._notify_history_chunker(recorded_step_id)
738
+ except Exception as step_err:
739
+ logger.warning(f"Error recording step in FlashRunner: {step_err}")
740
+ return recorded_step_id
741
+
742
+ def _notify_history_chunker(self, step_id) -> None:
743
+ """Stamps the recorded step for the chunk manager (single segment, no plan)."""
744
+ ledger = getattr(self.ctx, "transcript_ledger", None)
745
+ chunker = getattr(ledger, "chunker", None) if ledger is not None else None
746
+ if chunker is None or step_id is None:
747
+ return
748
+ try:
749
+ chunker.on_step_stamped(str(step_id), None)
750
+ except Exception as e:
751
+ logger.warning(f"History chunker step stamp failed: {e}")
752
+
753
+ async def _execute_and_record_action(
754
+ self,
755
+ name: str,
756
+ args: dict,
757
+ tc_id: str,
758
+ state: State,
759
+ messages: list[BaseMessage],
760
+ raw_text: str,
761
+ step_token_usage: dict,
762
+ pre_screenshot_bytes,
763
+ xml_list,
764
+ action_sequence: int,
765
+ turn: _TurnRecord,
766
+ injected: str | None = None,
767
+ native_text: str | None = None,
768
+ index_elements: list[dict] | None = None,
769
+ ):
770
+ """Executes one tool call, records it, and updates loop state.
771
+
772
+ The tool message carries the outcome text only; the post-action
773
+ screenshot and UI list become the next turn's observation tail.
774
+ ``index_elements`` is the turn's element snapshot index targets
775
+ resolve against (see :meth:`_process_tool_calls`); ``None`` resolves
776
+ against the state's current list. Returns the (possibly updated)
777
+ pre_screenshot_bytes, xml_list, and action_sequence for the next
778
+ iteration.
779
+ """
780
+ # Dynamic dispatch set: manifest device actions plus any backend
781
+ # extension tools, so extension steps are recorded like actions.
782
+ action_names = self.executor.action_tool_names
783
+ try:
784
+ exec_result = await self.executor.execute(
785
+ name, args, tc_id, state, index_elements=index_elements
786
+ )
787
+
788
+ post_img_bytes = await self._capture_post_screenshot(
789
+ exec_result, name, action_names, state
790
+ )
791
+
792
+ # One record shape for the DataEngine step and the visual lens.
793
+ action_dict = (
794
+ self._build_action_record(name, args, exec_result) if name in action_names else None
795
+ )
796
+
797
+ # Record telemetry / step in DataEngine
798
+ recorded_step_id = None
799
+ if self.ctx.data_engine and name in action_names:
800
+ recorded_step_id = self._record_action_step(
801
+ name,
802
+ args,
803
+ exec_result,
804
+ raw_text,
805
+ step_token_usage,
806
+ pre_screenshot_bytes,
807
+ xml_list,
808
+ post_img_bytes,
809
+ injected=injected,
810
+ native_text=native_text,
811
+ action_dict=action_dict,
812
+ )
813
+
814
+ if name in action_names:
815
+ turn.step_keys.append(str(recorded_step_id) if recorded_step_id else str(tc_id))
816
+ turn.actions.append((name, exec_result.status, exec_result.text_summary))
817
+
818
+ # ⚡ Non-blocking dispatch of objective visual transition summarizer
819
+ if self.summarizer and name in action_names:
820
+ action_sequence += 1
821
+ self.summarizer.dispatch(
822
+ step_number=action_sequence,
823
+ action_name=name,
824
+ # The recorded action minus its verb: the same shape the Pro
825
+ # summarizer hands the lens, so the phrase renders alike.
826
+ action_args={k: v for k, v in action_dict.items() if k != "action"},
827
+ pre_img_bytes=pre_screenshot_bytes,
828
+ post_img_bytes=post_img_bytes,
829
+ exec_outcome=exec_result.text_summary,
830
+ action_key=str(tc_id),
831
+ data_engine_step_id=recorded_step_id,
832
+ # The model's own reasoning for this turn steers the lens's
833
+ # attention; the target it named rides on ``args``.
834
+ focus=build_focus_context(
835
+ intent=raw_text,
836
+ goal=self.goal,
837
+ injected_instruction=injected,
838
+ ),
839
+ )
840
+
841
+ if post_img_bytes:
842
+ pre_screenshot_bytes = post_img_bytes
843
+ if exec_result.ui_elements_text:
844
+ xml_list = exec_result.ui_elements_text
845
+
846
+ # Helper tools may return multimodal blocks (a step screenshot);
847
+ # device actions always report text — their post screenshot is the
848
+ # next observation tail, never a tool-message image. The image
849
+ # carrier follows the model's provider (tool_result_messages).
850
+ raw_blocks = exec_result.raw_result if name not in action_names else None
851
+ content = (
852
+ raw_blocks
853
+ if isinstance(raw_blocks, list) and raw_blocks
854
+ else exec_result.text_summary or f"Action '{name}' dispatched."
855
+ )
856
+ messages.extend(
857
+ tool_result_messages(
858
+ tc_id, content, name=name, status=exec_result.status, llm=self._llm
859
+ )
860
+ )
861
+
862
+ except Exception as e:
863
+ logger.error(f"Error executing tool {name}: {e}")
864
+ if name in action_names:
865
+ turn.step_keys.append(str(tc_id))
866
+ turn.actions.append((name, "error", f"Error executing tool {name}: {e}"))
867
+ messages.append(
868
+ ToolMessage(
869
+ tool_call_id=tc_id,
870
+ name=name,
871
+ content=f"Error executing tool {name}: {e}",
872
+ status="error",
873
+ )
874
+ )
875
+ return pre_screenshot_bytes, xml_list, action_sequence
876
+
877
+ async def _invoke_model(self, llm, current_tools: list, messages: list[BaseMessage]):
878
+ """Binds the active tools and invokes the model through the LLM gateway."""
879
+ # Bind active tools
880
+ bound_llm = llm.bind_tools(current_tools)
881
+
882
+ # Invoke Model. Streaming, live-token UI deltas, classified
883
+ # retries, and pause/resume are all owned by the LLM gateway
884
+ # (acomplete); a typed LLMCallError propagates if it gives up.
885
+ return await invoke_llm_with_timeout_message(
886
+ acomplete(bound_llm, messages), timeout_seconds=10, hard_timeout=180
887
+ )
888
+
889
+ async def _process_tool_calls(
890
+ self,
891
+ tool_calls: list,
892
+ state: State,
893
+ messages: list[BaseMessage],
894
+ raw_text: str,
895
+ step_token_usage: dict,
896
+ pre_screenshot_bytes,
897
+ xml_list,
898
+ action_sequence: int,
899
+ turn: _TurnRecord,
900
+ injected: str | None = None,
901
+ native_text: str | None = None,
902
+ ):
903
+ """Dispatches the turn's tool calls.
904
+
905
+ Every index target of the turn resolves against the element list the
906
+ model decided on: the list of the observation this turn was built
907
+ from, snapshotted here before the first action. Each action still
908
+ refreshes ``state.indexed_elements`` (that refreshed list is what the
909
+ next observation shows), so without the snapshot ``click(5)`` after
910
+ ``click(3)`` would be resolved against the screen after the first
911
+ click.
912
+
913
+ The snapshot is the very list object the observation left on the
914
+ state (not a copy), and observations replace the state's list rather
915
+ than mutating it, so ``ask_explorer`` candidates registered before
916
+ any action land in the snapshot by construction. When a helper tool
917
+ appends candidates after an action has already replaced the state's
918
+ list, the explorer numbered them against that replaced list, so the
919
+ snapshot is re-taken from it: indices the explorer just handed out
920
+ resolve correctly, at the cost of the pre-action numbering (mixing
921
+ the two in one turn is the model's error, not a supported case).
922
+
923
+ Returns (final_report, pre_screenshot_bytes, xml_list, action_sequence);
924
+ final_report is non-None only when 'report_task_status' was called.
925
+ """
926
+ action_names = self.executor.action_tool_names
927
+ index_elements = self._turn_index_snapshot(state)
928
+ for tc in tool_calls:
929
+ name = tc["name"].split(":")[-1] if ":" in tc["name"] else tc["name"]
930
+ args = tc.get("args") or {}
931
+ tc_id = tc.get("id") or str(uuid.uuid4())
932
+ logger.info(f"Executing Flash tool: {name}({args})")
933
+
934
+ if name == "report_task_status":
935
+ final_report = await self._finalize_task_report(
936
+ name,
937
+ args,
938
+ tc_id,
939
+ raw_text,
940
+ step_token_usage,
941
+ pre_screenshot_bytes,
942
+ xml_list,
943
+ messages,
944
+ native_text=native_text,
945
+ )
946
+ return final_report, pre_screenshot_bytes, xml_list, action_sequence
947
+
948
+ elements_before = getattr(state, "indexed_elements", None)
949
+ before_len = len(elements_before) if isinstance(elements_before, list) else 0
950
+ pre_screenshot_bytes, xml_list, action_sequence = await self._execute_and_record_action(
951
+ name,
952
+ args,
953
+ tc_id,
954
+ state,
955
+ messages,
956
+ raw_text,
957
+ step_token_usage,
958
+ pre_screenshot_bytes,
959
+ xml_list,
960
+ action_sequence,
961
+ turn,
962
+ injected=injected,
963
+ native_text=native_text,
964
+ index_elements=index_elements,
965
+ )
966
+ if name not in action_names:
967
+ index_elements = self._extend_index_snapshot(state, index_elements, before_len)
968
+ return None, pre_screenshot_bytes, xml_list, action_sequence
969
+
970
+ @staticmethod
971
+ def _turn_index_snapshot(state: State) -> list[dict]:
972
+ """The element list this turn's index targets resolve against.
973
+
974
+ The state's list object itself, so in-place appends (explorer
975
+ candidates) before any action join the snapshot; an empty list is
976
+ installed on the state when there is none, for the same reason.
977
+ """
978
+ elements = getattr(state, "indexed_elements", None)
979
+ if not isinstance(elements, list):
980
+ elements = []
981
+ try:
982
+ state.indexed_elements = elements
983
+ except (AttributeError, TypeError, ValueError):
984
+ pass
985
+ return elements
986
+
987
+ @staticmethod
988
+ def _extend_index_snapshot(state: State, snapshot: list[dict], before_len: int) -> list[dict]:
989
+ """Re-takes the snapshot after a helper tool appended candidates to a
990
+ list that is no longer the snapshot (an action replaced it first).
991
+
992
+ Appends into the snapshot itself need nothing: the explorer numbered
993
+ them against the snapshot. Only growth of a different list object
994
+ moves the snapshot, since that is the list the new numbers refer to.
995
+ """
996
+ current = getattr(state, "indexed_elements", None)
997
+ if not isinstance(current, list) or current is snapshot:
998
+ return snapshot
999
+ if len(current) > before_len:
1000
+ return current
1001
+ return snapshot
1002
+
1003
+ async def _prepare_conversation(self, state: State, tools_declaration: list):
1004
+ """Installs the static prefix and captures the initial device state."""
1005
+ ledger = self._build_ledger()
1006
+ ledger.set_static_prefix(
1007
+ [SystemMessage(content=self._render_system_prompt(tools_declaration))]
1008
+ )
1009
+
1010
+ # Capture Initial State (Screenshot + UI Tree)
1011
+ shot_path, img_bytes, xml_list = await capture_screenshot_and_parse_ui(
1012
+ self.ctx, state, self.controller, skip_settling=False
1013
+ )
1014
+ state.latest_screenshot = shot_path
1015
+ return ledger, img_bytes, xml_list
1016
+
1017
+ @staticmethod
1018
+ def _commit_turn(ledger: TranscriptLedger, turn: _TurnRecord | None) -> None:
1019
+ """Commits the previous turn once its step ids and outcomes are known."""
1020
+ if turn is None:
1021
+ return
1022
+ ledger.commit_staged(
1023
+ step_key=turn.step_keys[0] if turn.step_keys else None,
1024
+ validator_result=turn.result(),
1025
+ extra_step_keys=turn.step_keys[1:],
1026
+ )
1027
+
1028
+ @trace(type="agent", name="FlashRunner")
1029
+ async def run(self, state: State) -> dict:
1030
+ limit = self.turn_limit
1031
+ logger.info(
1032
+ f"Starting Artemis Flash reactive loop for goal: {self.goal}"
1033
+ f" (turn limit: {limit if limit else 'unlimited'})"
1034
+ )
1035
+
1036
+ # 1. Initialize Universal LLM via Service Layer
1037
+ llm = self._init_llm()
1038
+ self._llm = llm
1039
+
1040
+ tools_declaration = self._get_tools()
1041
+ report_only_tools = [t for t in tools_declaration if t.name == "report_task_status"]
1042
+
1043
+ ledger, img_bytes, xml_list = await self._prepare_conversation(state, tools_declaration)
1044
+
1045
+ turns = 0
1046
+ action_sequence = 0
1047
+ final_report = None
1048
+ current_pre_screenshot_bytes = img_bytes
1049
+ current_xml_list = xml_list
1050
+ previous_turn: _TurnRecord | None = None
1051
+ pending_notices: list[str] = []
1052
+
1053
+ while limit is None or turns < limit:
1054
+ turns += 1
1055
+ logger.info(f"--- Artemis Flash Turn {turns}{f'/{limit}' if limit else ''} ---")
1056
+
1057
+ if self.ctx.data_engine:
1058
+ self.ctx.data_engine.allocate_step_id()
1059
+
1060
+ # Commit the previous turn: its step ids and outcomes exist now.
1061
+ self._commit_turn(ledger, previous_turn)
1062
+ previous_turn = None
1063
+
1064
+ # Check for real-time injected instructions: the verbatim text
1065
+ # is stamped on the step, the guidance pair goes to the tail.
1066
+ injected, injected_guidance = await self._read_injected_instruction(
1067
+ ledger.elapsed_label()
1068
+ )
1069
+
1070
+ # Tool restriction on the final turn (bounded loops only)
1071
+ is_final = limit is not None and turns == limit
1072
+ tail = self._build_tail(
1073
+ ledger,
1074
+ turns,
1075
+ current_pre_screenshot_bytes,
1076
+ current_xml_list,
1077
+ injected=injected_guidance,
1078
+ notices=pending_notices,
1079
+ is_final=is_final,
1080
+ # The ledger judged the turn it just committed: a bare tool
1081
+ # call (or thinking only) earns a one-line reminder, no bounce.
1082
+ previous_turn_silent=bool(getattr(ledger, "last_turn_silent", False)),
1083
+ )
1084
+ pending_notices = []
1085
+
1086
+ # S + F + A + tail: chunk compression and the scrub edge advance here.
1087
+ messages = ledger.render([tail])
1088
+ turn_base = len(messages) - 1
1089
+ current_tools = report_only_tools if is_final else tools_declaration
1090
+
1091
+ response = await self._invoke_model(llm, current_tools, messages)
1092
+
1093
+ if response is None:
1094
+ break
1095
+
1096
+ messages.append(response)
1097
+
1098
+ # Extract thought and tool calls
1099
+ raw_text = self._extract_response_text(response)
1100
+ native_text = self._extract_response_thinking(response)
1101
+
1102
+ # Extract token usage metadata from response
1103
+ step_token_usage, measured = self._resolve_token_usage(response, messages, raw_text)
1104
+ # The ledger's compaction thresholds read the runner's own prompt
1105
+ # size, never the session-wide meter (which any agent overwrites).
1106
+ # The prompt as sent (``messages`` minus the just-appended reply)
1107
+ # rides along so the ledger can calibrate its chars-per-token
1108
+ # ratio — only from a provider measurement, never from the
1109
+ # estimate (same as the Pro operator).
1110
+ ledger.record_prompt_tokens(
1111
+ step_token_usage.get("prompt_tokens"),
1112
+ messages=messages[:-1] if measured else None,
1113
+ )
1114
+
1115
+ self._record_llm_trace(step_token_usage, raw_text)
1116
+
1117
+ tool_calls = self._resolve_tool_calls(response, raw_text)
1118
+
1119
+ if not tool_calls:
1120
+ logger.info(
1121
+ f"FlashRunner received response without tool calls at turn {turns}:"
1122
+ f" {raw_text[:100]}..."
1123
+ )
1124
+ if is_final:
1125
+ final_report = {"status": "failed", "explanation": raw_text}
1126
+ break
1127
+ pending_notices.append(_NO_TOOL_CALL_NOTICE)
1128
+ ledger.stage_turn(messages[turn_base:])
1129
+ previous_turn = _TurnRecord()
1130
+ continue
1131
+
1132
+ # Process tool calls
1133
+ turn = _TurnRecord()
1134
+ (
1135
+ final_report_from_calls,
1136
+ current_pre_screenshot_bytes,
1137
+ current_xml_list,
1138
+ action_sequence,
1139
+ ) = await self._process_tool_calls(
1140
+ tool_calls,
1141
+ state,
1142
+ messages,
1143
+ raw_text,
1144
+ step_token_usage,
1145
+ current_pre_screenshot_bytes,
1146
+ current_xml_list,
1147
+ action_sequence,
1148
+ turn,
1149
+ injected=injected,
1150
+ native_text=native_text,
1151
+ )
1152
+ ledger.stage_turn(messages[turn_base:])
1153
+ previous_turn = turn
1154
+ if final_report_from_calls is not None:
1155
+ return final_report_from_calls
1156
+
1157
+ if self.summarizer:
1158
+ await self.summarizer.flush()
1159
+
1160
+ if final_report is None:
1161
+ explanation = (
1162
+ "Max turns reached without final status report."
1163
+ if limit is not None and turns >= limit
1164
+ else "The model returned no response; the reactive loop stopped."
1165
+ )
1166
+ final_report = {"status": "failed", "explanation": explanation}
1167
+ return final_report