android-midscene-automation 0.2.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +35 -0
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +4 -2
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +12 -0
  507. package/src/App.vue +10 -5
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +340 -121
  525. package/src/types.ts +1 -1
@@ -0,0 +1,760 @@
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
+ """MCP-backed action executor for the FlashRunner.
16
+
17
+ Device actions use the in-process MCP session. Argument normalization,
18
+ element-index resolution, coordinate descriptions, smart swipes, post-action
19
+ observations, state updates, tracing, and helper tools are handled here on the
20
+ agent side.
21
+
22
+ Flash binds the same agent dialect as the Pro Operator: a ``click``,
23
+ ``long_press`` or ``input_text`` target is either an element index into the
24
+ indexed ``--- Visible UI Elements ---`` list (resolved here to the element's
25
+ center, its observed text/bounds/id recorded) or a normalized ``[x, y]`` pair
26
+ carrying the model's own ``target_description``. The wire only ever sees
27
+ coordinates.
28
+
29
+ Device action status comes from ``ActionResult.ok``; helper tools report failures
30
+ through ``ToolFailure`` or an explicit status.
31
+ """
32
+
33
+ import ast
34
+ import hashlib
35
+ import json
36
+ from pathlib import Path
37
+ from typing import Any
38
+
39
+ from artemis.core.tool_failure import ToolFailure, is_tool_failure
40
+ from artemis.context import ArtemisContext
41
+ from artemis.controllers.unified_controller import UnifiedMobileController
42
+ from artemis.data_engine.trace import TraceSpan
43
+ from artemis.mcp.action_manifest import OPTIONAL_ACTIONS, REQUIRED_ACTIONS
44
+ from artemis.mcp.action_specs import exception_prefix
45
+ from artemis.mcp.action_session import ActionSession, get_action_session
46
+ from artemis.mcp.action_types import ActionCode, ActionResult
47
+ from artemis.mcp.actuators.adb import AdbActuator
48
+ from artemis.agents.validator.tool_declarations import (
49
+ ToolExecutionResult,
50
+ normalize_coordinate_target,
51
+ )
52
+ from artemis.tools.history import HISTORY_TOOL_NAMES, history_tool_by_name
53
+ from artemis.tools.tool_wrapper import split_multimodal_result
54
+ from artemis.utils.coordinates import (
55
+ compute_smart_swipe_coordinates,
56
+ parse_swipe_parameters,
57
+ )
58
+ from artemis.utils.notes import (
59
+ format_list_notes_failure,
60
+ format_list_notes_success,
61
+ format_read_note_failure,
62
+ list_notes_info,
63
+ read_note_content,
64
+ )
65
+ from third_party.mobile_use.utils.logger import get_logger
66
+
67
+ logger = get_logger(__name__)
68
+
69
+ __all__ = ["McpActionExecutor"]
70
+
71
+
72
+ #: Non-device tools executed agent-side (never behind the action server).
73
+ AGENT_TOOL_NAMES: frozenset[str] = (
74
+ frozenset({"read_note", "list_notes", "ask_explorer", "video_analyzer"}) | HISTORY_TOOL_NAMES
75
+ )
76
+
77
+ #: Actions whose ``target`` is a single point: an element index or an [x, y] pair.
78
+ _POINT_TARGET_ACTIONS: frozenset[str] = frozenset({"click", "long_press", "input_text"})
79
+
80
+
81
+ class _ArgError(ValueError):
82
+ """Argument-translation failure whose message is already fully formatted."""
83
+
84
+
85
+ class McpActionExecutor:
86
+ """Routes agent tool calls through the unified action MCP session."""
87
+
88
+ def __init__(
89
+ self,
90
+ ctx: ArtemisContext,
91
+ controller: UnifiedMobileController | None = None,
92
+ actuator: AdbActuator | None = None,
93
+ agent_name: str = "validator",
94
+ ):
95
+ self.ctx = ctx
96
+ self.actuator = actuator or getattr(ctx, "actuator", None) or AdbActuator(ctx, controller)
97
+ self.controller = self.actuator.controller
98
+ # Identifies the calling agent / profile to the Explorer tier resolver
99
+ # (``explorer.flash_mode`` vs ``explorer.pro_mode``); the agent itself
100
+ # never sees or chooses the tier.
101
+ self.agent_name = agent_name
102
+ self._session: ActionSession | None = None
103
+
104
+ @property
105
+ def action_tool_names(self) -> frozenset[str]:
106
+ """Device actions plus backend extension names -- the dynamic dispatch set."""
107
+ return (REQUIRED_ACTIONS | OPTIONAL_ACTIONS) | {e.name for e in self.actuator.extensions()}
108
+
109
+ async def _session_or_start(self) -> ActionSession:
110
+ if self._session is None or not self._session.started:
111
+ self._session = await get_action_session(self.ctx, actuator=self.actuator)
112
+ return self._session
113
+
114
+ # --- Public entry ----------------------------------------------------------------
115
+
116
+ async def execute(
117
+ self,
118
+ name: str,
119
+ args: dict[str, Any],
120
+ tool_call_id: str,
121
+ state: Any,
122
+ *,
123
+ index_elements: list[dict[str, Any]] | None = None,
124
+ ) -> ToolExecutionResult:
125
+ """Executes the named tool and wraps the outcome for the calling agent.
126
+
127
+ ``index_elements`` is the element list an index target (``click(3)``)
128
+ resolves against; ``None`` means ``state.indexed_elements`` at call
129
+ time. A multi-action turn passes the list from the observation the
130
+ model decided on, so its later indices are not resolved against the
131
+ list refreshed by its earlier actions (``state.indexed_elements`` is
132
+ still refreshed after every action: it is what the next observation
133
+ shows).
134
+ """
135
+ raw_name = name.split(":")[-1] if ":" in name else name
136
+
137
+ if raw_name in AGENT_TOOL_NAMES:
138
+ return await self._execute_agent_tool(raw_name, name, args, tool_call_id, state)
139
+
140
+ if raw_name in self.action_tool_names:
141
+ return await self._execute_device_action(
142
+ raw_name, name, args, tool_call_id, state, index_elements=index_elements
143
+ )
144
+
145
+ return ToolExecutionResult(
146
+ tool_call_id=tool_call_id,
147
+ tool_name=name,
148
+ status="error",
149
+ text_summary=f"Error: Tool '{name}' not supported.",
150
+ )
151
+
152
+ # --- Device actions --------------------------------------------------------------
153
+
154
+ async def _execute_device_action(
155
+ self,
156
+ raw_name: str,
157
+ name: str,
158
+ args: dict[str, Any],
159
+ tool_call_id: str,
160
+ state: Any,
161
+ *,
162
+ index_elements: list[dict[str, Any]] | None = None,
163
+ ) -> ToolExecutionResult:
164
+ span = TraceSpan(name=raw_name, trace_type="action", ctx=self.ctx)
165
+ span.payload = {"args": args}
166
+
167
+ message = ""
168
+ res: ActionResult | None = None
169
+ img_bytes: bytes | None = None
170
+ shot_path: str | None = None
171
+ xml_list: str | None = None
172
+ target_semantics: dict[str, Any] = {}
173
+ target_coordinates: list[int] | None = None
174
+ with span:
175
+ try:
176
+ session = await self._session_or_start()
177
+ wire_name, wire_args, finalize, target_semantics = self._translate(
178
+ raw_name, args, state, index_elements=index_elements
179
+ )
180
+ if raw_name in _POINT_TARGET_ACTIONS:
181
+ # The normalized point the action was sent to: the recorded
182
+ # coordinates of an index target (the model named an index,
183
+ # not a point).
184
+ target_coordinates = wire_args.get("target")
185
+ extension = raw_name not in (REQUIRED_ACTIONS | OPTIONAL_ACTIONS)
186
+
187
+ if extension:
188
+ raw_result = await session.call_raw(wire_name, wire_args)
189
+ message = next(
190
+ (b.text for b in raw_result.content if getattr(b, "type", "") == "text"),
191
+ f"Extension tool '{raw_name}' completed.",
192
+ )
193
+ res = ActionResult.success(raw_name, message)
194
+ else:
195
+ res = await session.call(wire_name, wire_args)
196
+ message = finalize(res) if finalize else res.message
197
+
198
+ if res.ok or self._observe_despite_failure(raw_name, res):
199
+ obs = await session.observe(settle_ms=400)
200
+ if obs.ok:
201
+ shot_path = obs.screenshot_path
202
+ xml_list = obs.elements_text
203
+ if obs.hierarchy_ok:
204
+ state.indexed_points = [el["center"] for el in obs.elements]
205
+ state.indexed_elements = obs.elements
206
+ if shot_path:
207
+ try:
208
+ img_bytes = Path(shot_path).read_bytes()
209
+ except Exception as read_err:
210
+ logger.warning(f"Failed to read observed screenshot: {read_err}")
211
+ except _ArgError as e:
212
+ res = ActionResult.failure(raw_name, str(e), code=ActionCode.INVALID_ARGS)
213
+ message = str(e)
214
+ except Exception as e:
215
+ message = f"{exception_prefix(raw_name)}: {e}"
216
+ res = ActionResult.failure(raw_name, message, detail=repr(e))
217
+
218
+ if shot_path:
219
+ state.latest_screenshot = shot_path
220
+
221
+ status = "success" if res.ok else "error"
222
+ post_image_name = None
223
+ if shot_path:
224
+ post_image_name = Path(shot_path).stem
225
+ elif img_bytes:
226
+ post_image_name = hashlib.sha256(img_bytes).hexdigest()
227
+ span.result = {
228
+ "outcome": message,
229
+ "post_image_name": post_image_name,
230
+ "has_xml": bool(xml_list),
231
+ "status": status,
232
+ }
233
+ if status == "error":
234
+ span.status = "failed"
235
+ span.error = message
236
+
237
+ return ToolExecutionResult(
238
+ tool_call_id=tool_call_id,
239
+ tool_name=name,
240
+ status=status,
241
+ text_summary=message,
242
+ screenshot_bytes=img_bytes,
243
+ screenshot_path=shot_path,
244
+ ui_elements_text=xml_list,
245
+ metadata={
246
+ "code": res.code.value,
247
+ "normalized_coordinates": res.normalized_coordinates,
248
+ "target_semantics": target_semantics,
249
+ "target_coordinates": target_coordinates,
250
+ },
251
+ )
252
+
253
+ @staticmethod
254
+ def _observe_despite_failure(raw_name: str, res: ActionResult) -> bool:
255
+ """Capture the screen after launch failures and wait_for_text timeouts.
256
+
257
+ Argument and package lookup errors do not require an observation.
258
+ """
259
+ if raw_name == "manage_app":
260
+ return res.code not in (ActionCode.PACKAGE_NOT_FOUND, ActionCode.INVALID_ARGS)
261
+ if raw_name == "wait_for_text":
262
+ return res.code is ActionCode.TIMEOUT
263
+ return False
264
+
265
+ # --- Argument translation --------------------------------------------------------
266
+
267
+ def _translate(
268
+ self,
269
+ raw_name: str,
270
+ args: dict[str, Any],
271
+ state: Any,
272
+ *,
273
+ index_elements: list[dict[str, Any]] | None = None,
274
+ ) -> tuple[str, dict[str, Any], Any, dict[str, Any]]:
275
+ """Maps agent-facing args to wire args; returns (name, args, finalize, recorded).
276
+
277
+ ``finalize(res)`` adds client-side context to swipe result messages.
278
+ ``recorded`` holds the target semantics in the Pro Operator's record
279
+ format: the observed element fields of an index target, or the validated
280
+ ``target_description`` of a coordinate target. Focused input and
281
+ directional swipes have no target, so they record nothing.
282
+ ``index_elements`` overrides the list index targets resolve against
283
+ (see :meth:`execute`).
284
+ """
285
+ if raw_name == "click":
286
+ target, recorded = self._resolve_target(
287
+ args.get("target") or args.get("coordinates"),
288
+ "click",
289
+ args,
290
+ state,
291
+ index_elements=index_elements,
292
+ )
293
+ return (
294
+ "click",
295
+ {
296
+ "target": target,
297
+ "times": args.get("times", 1),
298
+ "delay_ms": args.get("delay_ms", 100),
299
+ },
300
+ None,
301
+ recorded,
302
+ )
303
+
304
+ if raw_name == "long_press":
305
+ target, recorded = self._resolve_target(
306
+ args.get("target") or args.get("coordinates"),
307
+ "long press",
308
+ args,
309
+ state,
310
+ index_elements=index_elements,
311
+ )
312
+ return (
313
+ "long_press",
314
+ {
315
+ "target": target,
316
+ "duration_ms": args.get("duration_ms", args.get("duration", 1000)),
317
+ },
318
+ None,
319
+ recorded,
320
+ )
321
+
322
+ if raw_name == "input_text":
323
+ raw_target = args.get("target") or args.get("coordinates")
324
+ target = None
325
+ recorded = {}
326
+ if raw_target:
327
+ target, recorded = self._resolve_target(
328
+ raw_target, "input text", args, state, index_elements=index_elements
329
+ )
330
+ return (
331
+ "input_text",
332
+ {
333
+ "text": args.get("text", ""),
334
+ "target": target,
335
+ "clear_exist": args.get("clear_exist", True),
336
+ },
337
+ None,
338
+ recorded,
339
+ )
340
+
341
+ if raw_name == "click_sequence":
342
+ sequence = self._resolve_sequence(args.get("sequence") or [])
343
+ recorded = self._require_descriptions(args, len(sequence))
344
+ return (
345
+ "click_sequence",
346
+ {
347
+ "sequence": sequence,
348
+ "delay_ms": args.get("delay_ms", 50),
349
+ },
350
+ None,
351
+ recorded,
352
+ )
353
+
354
+ if raw_name == "swipe":
355
+ return self._translate_swipe(args, state, index_elements=index_elements)
356
+
357
+ if raw_name == "press_key":
358
+ return "press_key", {"key": args.get("key", "BACK")}, None, {}
359
+
360
+ if raw_name == "manage_app":
361
+ return (
362
+ "manage_app",
363
+ {
364
+ "action": args.get("action", "launch"),
365
+ "app_name": args.get("app_name", ""),
366
+ },
367
+ None,
368
+ {},
369
+ )
370
+
371
+ if raw_name == "wait_for_delay":
372
+ return "wait_for_delay", {"time_in_ms": args.get("time_in_ms", 1000)}, None, {}
373
+
374
+ if raw_name == "wait_for_text":
375
+ return (
376
+ "wait_for_text",
377
+ {
378
+ "text": args.get("text", ""),
379
+ "wait_state": args.get("wait_state") or "appear",
380
+ "timeout_ms": args.get("timeout_ms", 5000),
381
+ },
382
+ None,
383
+ {},
384
+ )
385
+
386
+ # Backend extension: pass the arguments straight through.
387
+ return raw_name, dict(args), None, {}
388
+
389
+ def _screen_size(self) -> tuple[int, int]:
390
+ """The device resolution the indexed element list was built against."""
391
+ device = getattr(self.ctx, "device", None)
392
+ width = getattr(device, "device_width", None) if device else None
393
+ height = getattr(device, "device_height", None) if device else None
394
+ return (
395
+ width if isinstance(width, int) and width > 0 else 1080,
396
+ height if isinstance(height, int) and height > 0 else 2400,
397
+ )
398
+
399
+ def _resolve_target(
400
+ self,
401
+ raw: Any,
402
+ label: str,
403
+ args: dict[str, Any],
404
+ state: Any,
405
+ *,
406
+ index_elements: list[dict[str, Any]] | None = None,
407
+ ) -> tuple[list[int], dict[str, Any]]:
408
+ """Resolves a point target to ``([x, y] normalized, recorded semantics)``.
409
+
410
+ An element index carries observed semantics (text, bounds, id, class
411
+ straight from the indexed list); a coordinate pair carries only the
412
+ model's own ``target_description``. The two never mix (Pro parity).
413
+ """
414
+ target = normalize_coordinate_target(raw)
415
+ if isinstance(target, bool):
416
+ target = None
417
+ if isinstance(target, (int, float)):
418
+ return self._resolve_index(int(target), label, state, index_elements=index_elements)
419
+ if isinstance(target, (list, tuple)) and len(target) == 2:
420
+ recorded = self._require_description(args, label)
421
+ return [int(target[0]), int(target[1])], recorded
422
+ raise _ArgError(
423
+ f"Error during {label}: Invalid target format: {raw!r}. Use an element"
424
+ " index from the Visible UI Elements list (e.g. 3) or normalized [x, y]"
425
+ " coordinates with a target_description."
426
+ )
427
+
428
+ def _resolve_index(
429
+ self,
430
+ index: int,
431
+ label: str,
432
+ state: Any,
433
+ *,
434
+ index_elements: list[dict[str, Any]] | None = None,
435
+ ) -> tuple[list[int], dict[str, Any]]:
436
+ """Maps an element index onto the element's center (normalized 0-1000).
437
+
438
+ The recorded fields are what the Operator records for an index target,
439
+ with bounds normalized like every other Flash coordinate. The index is
440
+ resolved against ``index_elements`` when given, else against the
441
+ state's current list.
442
+ """
443
+ elements = self._index_elements(state, index_elements)
444
+ if not 1 <= index <= len(elements):
445
+ hint = (
446
+ " The list is empty on this screen."
447
+ if not elements
448
+ else f" Active index range is 1 to {len(elements)}."
449
+ )
450
+ raise _ArgError(
451
+ f"Error during {label}: Invalid target index {index}.{hint} Use an index"
452
+ " shown in the Visible UI Elements list, or normalized [x, y]"
453
+ " coordinates with a target_description (ask_explorer can locate an"
454
+ " element that is visible but not listed)."
455
+ )
456
+ element = elements[index - 1]
457
+ width, height = self._screen_size()
458
+
459
+ def _norm_x(v: Any) -> int:
460
+ return int(max(0, min(1000, round(float(v) * 1000 / max(1, width)))))
461
+
462
+ def _norm_y(v: Any) -> int:
463
+ return int(max(0, min(1000, round(float(v) * 1000 / max(1, height)))))
464
+
465
+ center = element.get("center")
466
+ if not (isinstance(center, (list, tuple)) and len(center) == 2):
467
+ raise _ArgError(f"Error during {label}: element [{index}] has no usable center.")
468
+ bounds = element.get("bounds")
469
+ norm_bounds = None
470
+ if isinstance(bounds, (list, tuple)) and len(bounds) == 4:
471
+ norm_bounds = [
472
+ _norm_x(bounds[0]),
473
+ _norm_y(bounds[1]),
474
+ _norm_x(bounds[2]),
475
+ _norm_y(bounds[3]),
476
+ ]
477
+ recorded = {
478
+ "target_text": element.get("text"),
479
+ "target_bounds": norm_bounds,
480
+ "target_resource_id": element.get("resource_id"),
481
+ "target_class": element.get("class"),
482
+ }
483
+ return [_norm_x(center[0]), _norm_y(center[1])], recorded
484
+
485
+ @staticmethod
486
+ def _require_description(args: dict[str, Any], label: str) -> dict[str, Any]:
487
+ """Validate and trim the coordinate target description for the action record."""
488
+ description = args.get("target_description")
489
+ if not (isinstance(description, str) and description.strip()):
490
+ raise _ArgError(
491
+ f"Error during {label}: 'target_description' is required (what the"
492
+ " target is, in a few words)."
493
+ )
494
+ return {"target_description": description.strip()}
495
+
496
+ @staticmethod
497
+ def _require_descriptions(args: dict[str, Any], count: int) -> dict[str, Any]:
498
+ """``click_sequence``: one description per sequence entry, in order.
499
+
500
+ Returns the cleaned statements as the recorded ``target_descriptions`` field.
501
+ """
502
+ descriptions = args.get("target_descriptions")
503
+ if isinstance(descriptions, str):
504
+ descriptions = [descriptions]
505
+ if (
506
+ not isinstance(descriptions, (list, tuple))
507
+ or len(descriptions) != count
508
+ or not all(isinstance(d, str) and d.strip() for d in descriptions)
509
+ ):
510
+ raise _ArgError(
511
+ "Error during click sequence: 'target_descriptions' is required, one"
512
+ f" non-empty entry per sequence entry ({count} expected)."
513
+ )
514
+ return {"target_descriptions": [d.strip() for d in descriptions]}
515
+
516
+ @staticmethod
517
+ def _resolve_sequence(sequence: Any) -> list[list[int]]:
518
+ """Normalize click_sequence entries to coordinate pairs; reject element indices."""
519
+ if isinstance(sequence, str):
520
+ sequence_str = sequence.strip()
521
+ try:
522
+ sequence = json.loads(sequence_str)
523
+ except ValueError:
524
+ try:
525
+ sequence = ast.literal_eval(sequence_str)
526
+ except (ValueError, SyntaxError, TypeError, RecursionError):
527
+ pass
528
+
529
+ if not isinstance(sequence, (list, tuple)):
530
+ raise _ArgError(f"Error during click sequence: Invalid sequence format: {sequence}")
531
+
532
+ resolved: list[list[int]] = []
533
+ for position, raw_target in enumerate(sequence, start=1):
534
+ target = normalize_coordinate_target(raw_target)
535
+ if isinstance(target, (int, float)) and not isinstance(target, bool):
536
+ raise _ArgError(
537
+ f"Error during click sequence: entry {position} ({raw_target!r}) is an"
538
+ " element index; click_sequence takes normalized [x, y] coordinate"
539
+ " pairs only. Use the element's coordinates from the element list,"
540
+ " or ask_explorer to locate it."
541
+ )
542
+ if isinstance(target, (list, tuple)) and len(target) == 2:
543
+ nx, ny = int(target[0]), int(target[1])
544
+ else:
545
+ raise _ArgError(f"Error during click sequence: Invalid target format: {raw_target}")
546
+ resolved.append([nx, ny])
547
+ return resolved
548
+
549
+ @staticmethod
550
+ def _index_elements(
551
+ state: Any, index_elements: list[dict[str, Any]] | None
552
+ ) -> list[dict[str, Any]]:
553
+ """The element list index targets resolve against (``None`` -> state)."""
554
+ if index_elements is not None:
555
+ return index_elements
556
+ return getattr(state, "indexed_elements", None) or []
557
+
558
+ def _translate_swipe(
559
+ self,
560
+ args: dict[str, Any],
561
+ state: Any,
562
+ *,
563
+ index_elements: list[dict[str, Any]] | None = None,
564
+ ) -> tuple[str, dict[str, Any], Any, dict[str, Any]]:
565
+ width, height = self._screen_size()
566
+ default_duration = args.get("duration", 400)
567
+ kind, target, final_duration = parse_swipe_parameters(
568
+ dict(args), default_duration=default_duration
569
+ )
570
+
571
+ if kind == "direction":
572
+ dir_name = target
573
+ x1, y1, x2, y2, smart_dur = compute_smart_swipe_coordinates(
574
+ direction=target,
575
+ target=args.get("target"),
576
+ indexed_elements=self._index_elements(state, index_elements) if state else None,
577
+ ui_hierarchy=getattr(state, "latest_ui_hierarchy", None) if state else None,
578
+ width=width,
579
+ height=height,
580
+ duration=final_duration,
581
+ )
582
+
583
+ def _norm(v: float, size: int) -> int:
584
+ return int(max(0, min(1000, round(v * 1000 / max(1, size)))))
585
+
586
+ def finalize(res: ActionResult) -> str:
587
+ if not res.ok:
588
+ return f"Error swiping {dir_name}: {res.detail}"
589
+ return f"Swiped {dir_name}."
590
+
591
+ return (
592
+ "swipe",
593
+ {
594
+ "start": [_norm(x1, width), _norm(y1, height)],
595
+ "end": [_norm(x2, width), _norm(y2, height)],
596
+ "duration_ms": smart_dur,
597
+ },
598
+ finalize,
599
+ {},
600
+ )
601
+
602
+ if kind == "coords":
603
+ x1, y1, x2, y2 = target
604
+ recorded = self._require_description(args, "swipe")
605
+
606
+ def finalize(res: ActionResult) -> str:
607
+ if not res.ok:
608
+ return f"Error dragging: {res.detail}"
609
+ return f"Swiped from [{x1}, {y1}] to [{x2}, {y2}] (normalized)."
610
+
611
+ return (
612
+ "swipe",
613
+ {
614
+ "start": [int(x1), int(y1)],
615
+ "end": [int(x2), int(y2)],
616
+ "duration_ms": final_duration,
617
+ },
618
+ finalize,
619
+ recorded,
620
+ )
621
+
622
+ raise _ArgError(f"Error during swipe: Invalid direction: {args}")
623
+
624
+ # --- Non-device tools (stay agent-side) ------------------------------------------
625
+
626
+ async def _execute_agent_tool(
627
+ self,
628
+ raw_name: str,
629
+ name: str,
630
+ args: dict[str, Any],
631
+ tool_call_id: str,
632
+ state: Any,
633
+ ) -> ToolExecutionResult:
634
+ span = TraceSpan(name=raw_name, trace_type="tool", ctx=self.ctx)
635
+ span.payload = {"args": args}
636
+ ok: bool | None = None
637
+ blocks: list[dict[str, Any]] | None = None
638
+ with span:
639
+ if raw_name == "read_note":
640
+ text = await self._read_note(
641
+ args.get("key", ""), args.get("start_line"), args.get("end_line")
642
+ )
643
+ elif raw_name == "list_notes":
644
+ text = await self._list_notes()
645
+ elif raw_name == "video_analyzer":
646
+ text, ok = await self._video_analyzer(
647
+ args.get("time_description") or args.get("TimeDescription") or "",
648
+ args.get("purpose") or args.get("Purpose") or "",
649
+ )
650
+ elif raw_name in HISTORY_TOOL_NAMES:
651
+ text, blocks = await self._history_tool(raw_name, args)
652
+ else:
653
+ text, ok = await self._ask_explorer(
654
+ # ``task_description`` is the pre-contract argument name
655
+ # kept for old MCP clients.
656
+ args.get("query") or args.get("task_description") or "",
657
+ args.get("context_feedback"),
658
+ state,
659
+ )
660
+ span.result = {"outcome": text}
661
+ if ok is False:
662
+ span.status = "failed"
663
+ span.error = text
664
+
665
+ # The Explorer and the video analyzer report their status explicitly;
666
+ # note and history texts carry it structurally (a ``ToolFailure``), so a
667
+ # history lookup's "no match" answer is an answer, not an error.
668
+ if ok is None:
669
+ ok = not is_tool_failure(text)
670
+ return ToolExecutionResult(
671
+ tool_call_id=tool_call_id,
672
+ tool_name=name,
673
+ status="success" if ok else "error",
674
+ text_summary=text,
675
+ # Content blocks (a step screenshot) ride along for callers that
676
+ # forward multimodal tool results; the text summary stays complete.
677
+ raw_result=blocks,
678
+ )
679
+
680
+ async def _read_note(self, key: str, start_line: int | None, end_line: int | None) -> str:
681
+ try:
682
+ base_dir = self.ctx.data_engine.base_dir if self.ctx.data_engine else None
683
+ if not base_dir:
684
+ return format_read_note_failure(key, "DataEngine not initialized.")
685
+ return read_note_content(base_dir, key, start_line, end_line)
686
+ except Exception as e:
687
+ return format_read_note_failure(key, str(e))
688
+
689
+ async def _list_notes(self) -> str:
690
+ try:
691
+ base_dir = self.ctx.data_engine.base_dir if self.ctx.data_engine else None
692
+ if not base_dir:
693
+ return format_list_notes_failure("DataEngine not initialized.")
694
+ return format_list_notes_success(list_notes_info(base_dir))
695
+ except Exception as e:
696
+ return format_list_notes_failure(str(e))
697
+
698
+ async def _history_tool(
699
+ self, raw_name: str, args: dict[str, Any]
700
+ ) -> tuple[str, list[dict] | None]:
701
+ """Runs one of the shared history tools (the same instances the Pro
702
+ agents bind), dispatched generically by name.
703
+
704
+ Returns the text answer and, for a screenshot, the multimodal content
705
+ blocks the tool produced.
706
+ """
707
+ tool = history_tool_by_name(raw_name)
708
+ if tool is None:
709
+ return ToolFailure(f"Error: Tool '{raw_name}' not supported."), None
710
+ accepted = {k: v for k, v in args.items() if k in tool.args_schema.model_fields}
711
+ try:
712
+ result = await tool.execute(ctx=self.ctx, **accepted)
713
+ except Exception as e:
714
+ logger.error(f"Error running {raw_name}: {e}")
715
+ return ToolFailure(f"{raw_name} failed: {e}"), None
716
+ text, images = split_multimodal_result(result)
717
+ if images:
718
+ return text or "Screenshot attached.", result
719
+ return text, None
720
+
721
+ async def _video_analyzer(self, time_description: str, purpose: str) -> tuple[str, bool]:
722
+ """Runs the video-analyzing subagent over the session recording.
723
+
724
+ Same subagent the Pro operator's ``video_analyzer`` LangChain tool
725
+ delegates to; imported lazily because the video stack is heavy.
726
+ """
727
+ from artemis.agents.video_analyzer.video_analyzer import VideoAnalyzer
728
+
729
+ try:
730
+ outcome, status = await VideoAnalyzer(self.ctx).run(time_description, purpose)
731
+ except Exception as e:
732
+ logger.error(f"Error running video analyzer: {e}")
733
+ return f"Error running video analyzer: {e}", False
734
+ if status in ("failed", "error"):
735
+ return f"Video analysis failed: {outcome}", False
736
+ return outcome, True
737
+
738
+ async def _ask_explorer(
739
+ self, query: str, context_feedback: str | None, state: Any
740
+ ) -> tuple[str, bool]:
741
+ """Runs the Explorer pipeline for this executor's agent; returns ``(text, ok)``.
742
+
743
+ ``ok`` is False only when the Explorer run itself failed: a clean
744
+ "not found" is a successful answer the agent has to reason about.
745
+ """
746
+ # Imported lazily: the explorer stack is heavy and recursive (it spawns an LLM
747
+ # sub-agent), which is also why it must never live behind the action server.
748
+ from artemis.tools.explorer_tool import locate, register_candidates, render_text
749
+
750
+ try:
751
+ outcome = await locate(
752
+ self.ctx, state, query, context_feedback or "", agent_name=self.agent_name
753
+ )
754
+ registered = register_candidates(self.ctx, state, outcome)
755
+ # Candidates join ``state.indexed_elements``, and this executor's
756
+ # ``click`` resolves an index against that list (Pro parity), so the
757
+ # answer teaches the index syntax next to the coordinates.
758
+ return render_text(query, outcome, registered, index_targets=True), not outcome.error
759
+ except Exception as e:
760
+ return f"Error executing ask_explorer: {e}", False