android-midscene-automation 0.2.4 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +159 -29
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +4 -2
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +12 -0
  507. package/src/App.vue +10 -5
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +340 -121
  525. package/src/types.ts +1 -1
@@ -0,0 +1,1174 @@
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
+ import ast
16
+ import json
17
+ import logging
18
+ from pathlib import Path
19
+ import re
20
+ from typing import Any
21
+
22
+ from artemis.utils.plan_grammar import parse_plan
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ #: Suffix on a rendered target label whose text is the acting model's own
27
+ #: ``target_description`` (a coordinate target) rather than the element text
28
+ #: observed in the UI tree (an index target). Every later reader of the history
29
+ #: (Checker, Planner, Summarizer, incident block) sees which kind it is.
30
+ SELF_DESCRIBED_MARKER = "(self-described)"
31
+
32
+
33
+ def _target_label(action_obj: dict, *, fallback: str) -> str:
34
+ """The quoted target label, provenance-marked, or ``fallback`` when unnamed."""
35
+ observed = action_obj.get("target_text")
36
+ if observed:
37
+ return f"'{observed}'"
38
+ described = action_obj.get("target_description")
39
+ if described:
40
+ return f"'{described}' {SELF_DESCRIBED_MARKER}"
41
+ return fallback
42
+
43
+
44
+ def format_action_clean(action_obj) -> str:
45
+ if not action_obj:
46
+ return "No Action"
47
+ if isinstance(action_obj, list):
48
+ if not action_obj:
49
+ return "No Action"
50
+ action_obj = action_obj[0]
51
+
52
+ if not isinstance(action_obj, dict):
53
+ return str(action_obj)
54
+
55
+ act_type = action_obj.get("action") or action_obj.get("name")
56
+ if not act_type:
57
+ return json.dumps(action_obj, ensure_ascii=False)
58
+
59
+ # Flash records the tool call verbatim: the action name at the top level
60
+ # and every argument (app_name, key, text, direction, ...) under ``args``.
61
+ # Read those as fallbacks so both record shapes render the same phrase.
62
+ raw_args = action_obj.get("args")
63
+ if isinstance(raw_args, dict):
64
+ merged = {k: v for k, v in raw_args.items() if k != "action"}
65
+ merged.update({k: v for k, v in action_obj.items() if v is not None and k != "args"})
66
+ merged.setdefault("intent", raw_args.get("action"))
67
+ action_obj = merged
68
+
69
+ coords = action_obj.get("coordinates") or action_obj.get("target")
70
+ app_name = (
71
+ action_obj.get("app_name")
72
+ or action_obj.get("package_name")
73
+ or action_obj.get("package")
74
+ or action_obj.get("package_or_bundle_id")
75
+ or action_obj.get("bundle_id")
76
+ )
77
+ keycode = action_obj.get("keycode") or action_obj.get("key")
78
+
79
+ if act_type in ("tap", "click", "long_press", "long_press_on"):
80
+ # Legacy records carried the label under ``text``; the typed text of an
81
+ # input action never reaches this branch.
82
+ label = _target_label(action_obj, fallback="element")
83
+ if label == "element" and action_obj.get("text"):
84
+ label = f"'{action_obj['text']}'"
85
+ if act_type in ("long_press", "long_press_on"):
86
+ duration = action_obj.get("duration") or action_obj.get("duration_ms")
87
+ duration_str = f" for {duration}ms" if duration else ""
88
+ return f"Long pressed {label} at {coords}{duration_str}"
89
+ else:
90
+ times = action_obj.get("times") or action_obj.get("click_times") or 1
91
+ try:
92
+ times = int(times)
93
+ except (ValueError, TypeError):
94
+ times = 1
95
+
96
+ if times == 2:
97
+ return f"Double tapped {label} at {coords}"
98
+ elif times > 2:
99
+ delay = action_obj.get("delay_ms") or action_obj.get("delay")
100
+ delay_str = f" with {delay}ms delay" if delay else ""
101
+ return f"Tapped {label} at {coords} {times} times{delay_str}"
102
+ else:
103
+ return f"Tapped {label} at {coords}"
104
+ elif act_type in ("input_text", "focus_and_input_text"):
105
+ # The field label is the resolved target's text (or the model's own
106
+ # description of the field) only — the typed text is not the field's name.
107
+ label = _target_label(action_obj, fallback="field")
108
+ text_val = action_obj.get("text")
109
+ clear_exist = action_obj.get("clear_exist") or action_obj.get("clear_before_input")
110
+ clear_str = " (without clearing)" if clear_exist is False else ""
111
+ return f"Inputted '{text_val}' into {label} at {coords}{clear_str}"
112
+ elif act_type == "swipe":
113
+ direction = action_obj.get("action_direction") or action_obj.get("direction")
114
+ if not direction and isinstance(action_obj.get("gesture"), str):
115
+ # Flash swipe-by-direction records the direction as ``gesture``
116
+ # (or the FA ``action`` word, already mapped to ``intent``).
117
+ direction = action_obj.get("gesture")
118
+ if not direction and isinstance(action_obj.get("intent"), str):
119
+ direction = action_obj.get("intent")
120
+ duration = action_obj.get("duration") or action_obj.get("duration_ms")
121
+ duration_str = f" over {duration}ms" if duration else ""
122
+
123
+ start_coords = action_obj.get("start_coordinates")
124
+ end_coords = action_obj.get("end_coordinates")
125
+ if (not start_coords or not end_coords) and "coordinates" in action_obj:
126
+ coords_list = action_obj.get("coordinates")
127
+ if isinstance(coords_list, list) and len(coords_list) == 4:
128
+ start_coords = coords_list[:2]
129
+ end_coords = coords_list[2:]
130
+ if not start_coords or not end_coords:
131
+ start_coords = action_obj.get("normalized_start_coordinates")
132
+ end_coords = action_obj.get("normalized_end_coordinates")
133
+ if direction:
134
+ # A directional swipe is what the agent issued; the recorded path
135
+ # (when any) is the detail, not the headline.
136
+ path = f" (from {start_coords} to {end_coords})" if start_coords and end_coords else ""
137
+ return f"Swiped {direction}{path}{duration_str}"
138
+ dragged = _target_label(action_obj, fallback="")
139
+ dragged_str = f" {dragged}" if dragged else ""
140
+ return f"Swiped{dragged_str} from {start_coords} to {end_coords}{duration_str}"
141
+ elif act_type == "press_key":
142
+ return f"Pressed key '{keycode}'"
143
+ elif act_type == "launch_app":
144
+ return f"Launched app '{app_name}'"
145
+ elif act_type == "stop_app":
146
+ return f"Stopped app '{app_name}'"
147
+ elif act_type == "manage_app":
148
+ intent = action_obj.get("intent")
149
+ if intent == "launch":
150
+ return f"Launched app '{app_name}'"
151
+ if intent == "stop":
152
+ return f"Stopped app '{app_name}'"
153
+ return f"Managed app '{app_name}' (action: {intent})"
154
+ elif act_type == "wait_for_delay":
155
+ delay_ms = (
156
+ action_obj.get("delay_ms") or action_obj.get("time_in_ms") or action_obj.get("duration")
157
+ )
158
+ if not delay_ms:
159
+ delay_sec = action_obj.get("delay_seconds") or action_obj.get("delay")
160
+ if delay_sec:
161
+ try:
162
+ delay_ms = int(float(delay_sec) * 1000)
163
+ except ValueError:
164
+ delay_ms = f"{delay_sec}s"
165
+ delay_val = (
166
+ f"{delay_ms}ms"
167
+ if isinstance(delay_ms, int) or (isinstance(delay_ms, str) and delay_ms.isdigit())
168
+ else str(delay_ms)
169
+ )
170
+ return f"Waited for {delay_val}"
171
+ elif act_type in ("click_sequence", "tap_sequence"):
172
+ seq = (
173
+ action_obj.get("target")
174
+ or action_obj.get("sequence")
175
+ or action_obj.get("coordinates")
176
+ or []
177
+ )
178
+ descriptions = action_obj.get("target_descriptions")
179
+ if isinstance(descriptions, (list, tuple)) and len(descriptions) == len(seq) and seq:
180
+ labelled = ", ".join(
181
+ f"'{d}' {SELF_DESCRIBED_MARKER} at {p}" for d, p in zip(descriptions, seq)
182
+ )
183
+ return f"Tapped sequence of targets: {labelled}"
184
+ return f"Tapped sequence of targets: {seq}"
185
+ elif act_type == "wait_for_text":
186
+ state = str(action_obj.get("wait_state") or "appear")
187
+ timeout = action_obj.get("timeout_ms")
188
+ timeout_str = f" (timeout {timeout}ms)" if timeout else ""
189
+ return f"Waited for text '{action_obj.get('text', '')}' to {state}{timeout_str}"
190
+ elif act_type == "open_link":
191
+ return f"Opened link '{action_obj.get('url', '')}'"
192
+ elif act_type == "erase_one_char":
193
+ return "Erased one character"
194
+ elif act_type == "focus_and_clear_text":
195
+ return f"Cleared text at {coords}"
196
+ else:
197
+ # Unknown action: show only the agent-facing arguments, never the
198
+ # bookkeeping keys the record carries for other consumers.
199
+ internal = {
200
+ "action",
201
+ "name",
202
+ "intent",
203
+ "coordinate_space",
204
+ "normalized_coordinates",
205
+ "normalized_start_coordinates",
206
+ "normalized_end_coordinates",
207
+ }
208
+ shown = {k: v for k, v in action_obj.items() if k not in internal and v is not None}
209
+ args_str = f" with args: {json.dumps(shown, ensure_ascii=False)}" if shown else ""
210
+ return f"Action: {act_type}{args_str}"
211
+
212
+
213
+ def format_actions_clean(actions) -> str:
214
+ """Renders one action or a fast-action burst (a list of 2+ actions)."""
215
+ if isinstance(actions, list):
216
+ if not actions:
217
+ return "No Action"
218
+ if len(actions) > 1:
219
+ steps = " -> ".join(format_action_clean(a) for a in actions)
220
+ return f"Fast-action burst ({len(actions)} actions, unvetted): {steps}"
221
+ return format_action_clean(actions[0])
222
+ return format_action_clean(actions)
223
+
224
+
225
+ #: Past-tense outcome prefix (``format_action_clean``) -> intent-form prefix.
226
+ #: Longest prefixes first so "Double tapped" / "Tapped sequence" win over "Tapped".
227
+ _INTENT_PREFIXES: tuple[tuple[str, str], ...] = (
228
+ ("Tapped sequence of targets: ", "tap sequence of targets: "),
229
+ ("Double tapped ", "double tap "),
230
+ ("Long pressed ", "long press "),
231
+ ("Tapped ", "tap "),
232
+ ("Inputted ", "type "),
233
+ ("Swiped ", "swipe "),
234
+ ("Pressed key ", "press key "),
235
+ ("Launched app ", "launch app "),
236
+ ("Stopped app ", "stop app "),
237
+ ("Managed app ", "manage app "),
238
+ ("Waited for text ", "wait for text "),
239
+ ("Waited for ", "wait "),
240
+ ("Opened link ", "open link "),
241
+ ("Erased one character", "erase one character"),
242
+ ("Cleared text ", "clear text "),
243
+ )
244
+
245
+
246
+ def action_intent_phrase(description: str) -> str:
247
+ """The intent form of a rendered action phrase: ``Tapped 'Wi-Fi row' at
248
+ [500, 520]`` -> ``tap 'Wi-Fi row' at [500, 520]``.
249
+
250
+ ``format_action_clean`` renders actions as past-tense outcomes, which
251
+ reads as success; a failure or incident context must describe what the
252
+ agent *tried* to do instead. Phrases without a known prefix (bursts,
253
+ unknown action types) are returned unchanged.
254
+ """
255
+ text = str(description or "")
256
+ for outcome, intent in _INTENT_PREFIXES:
257
+ if text.startswith(outcome):
258
+ return intent + text[len(outcome) :]
259
+ return text
260
+
261
+
262
+ def format_action_intent(action_obj) -> str:
263
+ """Intent-form rendering of one action (``launch app 'X'``, ``swipe up``)."""
264
+ return action_intent_phrase(format_action_clean(action_obj))
265
+
266
+
267
+ def _is_terminal_attempt_failure(attempts) -> bool:
268
+ if not attempts:
269
+ return False
270
+ last = str(attempts[-1])
271
+ return last != "Dispatched" and not last.startswith("Skipped")
272
+
273
+
274
+ def failed_execution_error(result_obj) -> str | None:
275
+ """The error text of the action that failed in an execution report, if any."""
276
+ if not isinstance(result_obj, dict):
277
+ return None
278
+ exec_list = result_obj.get("execution") or result_obj.get("executed_actions") or []
279
+ if isinstance(exec_list, list):
280
+ for entry in exec_list:
281
+ if isinstance(entry, dict) and _is_terminal_attempt_failure(entry.get("attempts")):
282
+ return " | ".join(str(a) for a in entry["attempts"])
283
+ first = exec_list[0] if exec_list and isinstance(exec_list[0], dict) else {}
284
+ if first.get("error"):
285
+ return str(first["error"])
286
+ error = result_obj.get("error") or result_obj.get("error_msg")
287
+ return str(error) if error else None
288
+
289
+
290
+ def format_incident_clean(incident: dict) -> str:
291
+ """One-line rendering of an execution incident for history and result lines.
292
+
293
+ The action is quoted in intent form (``launch app 'X'``): it did not
294
+ happen, so the past-tense outcome phrase would misreport it as done.
295
+ """
296
+ kind = incident.get("kind") or "exec_error"
297
+ category = incident.get("category") or "general"
298
+ consecutive = incident.get("consecutive_failures") or 1
299
+ description = action_intent_phrase(
300
+ incident.get("action_description") or format_action_clean(incident.get("action"))
301
+ )
302
+ reason = str(incident.get("reason") or "").strip()
303
+ burst_size = int(incident.get("burst_size") or 1)
304
+ index = int(incident.get("action_index") or 0)
305
+ prefix = (
306
+ "Intercepted by Pre-Execution Safety Net" if kind == "safety_net" else "Execution failed"
307
+ )
308
+ where = f"burst action {index + 1}/{burst_size}" if burst_size > 1 else "action"
309
+ tail = ""
310
+ if burst_size > 1 and index < burst_size - 1:
311
+ tail = "; the remaining burst actions were not executed"
312
+ return (
313
+ f"Error: {prefix} ({category}, consecutive failure #{consecutive}) on"
314
+ f" {where} `{description}`: {reason}{tail}"
315
+ )
316
+
317
+
318
+ def format_result_clean(result_obj) -> str | None:
319
+ if not result_obj:
320
+ return None
321
+ if not isinstance(result_obj, dict):
322
+ return str(result_obj)
323
+
324
+ incident = result_obj.get("incident")
325
+ if isinstance(incident, dict) and incident.get("reason"):
326
+ return format_incident_clean(incident)
327
+
328
+ status = result_obj.get("status")
329
+ error = failed_execution_error(result_obj)
330
+ if error:
331
+ return _error_line(error)
332
+ if status in ("failed", "error"):
333
+ return "Error"
334
+ return None
335
+
336
+
337
+ def _error_line(error: str) -> str:
338
+ """One ``Error: ...`` line; executor messages already start with "Error"."""
339
+ text = str(error).strip()
340
+ return text if text.lower().startswith("error") else f"Error: {text}"
341
+
342
+
343
+ def safe_parse_validation_result(result: Any) -> list:
344
+ if isinstance(result, (list, tuple)):
345
+ return list(result)
346
+ if not isinstance(result, str):
347
+ return []
348
+
349
+ # Clean up enum references in string format
350
+
351
+ cleaned = re.sub(
352
+ r"<ValidationErrorCategory\.[A-Z_]+:\s*['\"]([^'\"]*)['\"]>",
353
+ r"'\1'",
354
+ result.strip(),
355
+ )
356
+
357
+ # Try parsing with ast.literal_eval
358
+
359
+ try:
360
+ parsed = ast.literal_eval(cleaned)
361
+ if isinstance(parsed, (list, tuple)):
362
+ return list(parsed)
363
+ except (ValueError, SyntaxError, TypeError, RecursionError):
364
+ pass
365
+
366
+ # Fallback to json.loads if literal_eval failed
367
+ try:
368
+ parsed = json.loads(cleaned)
369
+ if isinstance(parsed, (list, tuple)):
370
+ return list(parsed)
371
+ except ValueError:
372
+ pass
373
+
374
+ return []
375
+
376
+
377
+ #: Block labels of a detailed / replayed step. Role-neutral on purpose: the
378
+ #: same renderer replays a Flash turn (no Operator) and a Pro Operator turn,
379
+ #: and every Pro agent (Checker, Diagnoser, Outputter) reads them.
380
+ REASONING_BLOCK_LABEL = "[Reasoning & tool calls]"
381
+ ACTION_BLOCK_LABEL = "[Action]"
382
+ BURST_BLOCK_LABEL = "[Fast-Action Burst]"
383
+
384
+ #: Continuation lines of a " - " bullet stay inside the bullet.
385
+ _BULLET_CONTINUATION = "\n "
386
+
387
+
388
+ def _indent_continuation(text: str) -> str:
389
+ return text.replace("\n", _BULLET_CONTINUATION)
390
+
391
+
392
+ #: Limit tool-result text in the live context, which is rebuilt every turn.
393
+ LIVE_RESULT_CHARS = 120
394
+
395
+ #: Replay allows longer tool results, but still caps large OCR and hierarchy dumps.
396
+ REPLAY_RESULT_CHARS = 6000
397
+
398
+
399
+ def _clamp_text(text: str, limit: int) -> str:
400
+ if limit and len(text) > limit:
401
+ return text[:limit] + "..."
402
+ return text
403
+
404
+
405
+ def format_tool_call_clean(
406
+ name, args, result, *, result_chars: int = LIVE_RESULT_CHARS
407
+ ) -> str | None:
408
+ ACTION_TOOLS = {
409
+ "click",
410
+ "click_sequence",
411
+ "tap_sequence",
412
+ "swipe",
413
+ "input_text",
414
+ "focus_and_input_text",
415
+ "press_key",
416
+ "long_press",
417
+ "launch_app",
418
+ "manage_app",
419
+ "stop_app",
420
+ "wait_for_delay",
421
+ }
422
+
423
+ # Custom rendering for safety net validations
424
+ if name in ("safety_net_validation", "safety_net_pixel_validation"):
425
+ res_list = safe_parse_validation_result(result)
426
+ if len(res_list) >= 3:
427
+ passed, category, detail = res_list[0], res_list[1], res_list[2]
428
+ if passed:
429
+ return "Passed"
430
+ else:
431
+ return f"Failed - {str(detail).strip()}"
432
+ return "Passed" if result else "Failed"
433
+
434
+ if name in ("read_note", "save_note", "update_note") and args.get("key") == "task_plan":
435
+ return None
436
+
437
+ # Custom rendering for all Action Tools
438
+ if name in ACTION_TOOLS:
439
+ action_name = name
440
+
441
+ # Convert arguments to action_obj format with thorough parameter mapping
442
+ action_obj = {
443
+ "action": action_name,
444
+ "intent": args.get("intent") or args.get("action"),
445
+ "target": (args.get("target") or args.get("coordinates") or args.get("sequence")),
446
+ "target_text": (
447
+ args.get("text") or args.get("target_text") or args.get("target_text_or_desc")
448
+ ),
449
+ "target_description": args.get("target_description"),
450
+ "target_descriptions": args.get("target_descriptions"),
451
+ "text": args.get("text"),
452
+ "clear_exist": (args.get("clear_exist") or args.get("clear_before_input")),
453
+ "clear_before_input": args.get("clear_before_input"),
454
+ "keycode": args.get("keycode") or args.get("key"),
455
+ "key": args.get("key"),
456
+ "app_name": (
457
+ args.get("app_name")
458
+ or args.get("package_name")
459
+ or args.get("package")
460
+ or args.get("package_or_bundle_id")
461
+ or args.get("bundle_id")
462
+ ),
463
+ "delay": (args.get("delay") or args.get("delay_ms") or args.get("time_in_ms")),
464
+ "delay_ms": args.get("delay_ms"),
465
+ "time_in_ms": args.get("time_in_ms"),
466
+ "duration": (args.get("duration") or args.get("duration_ms") or args.get("time_in_ms")),
467
+ "duration_ms": args.get("duration_ms"),
468
+ "times": args.get("times") or args.get("click_times"),
469
+ "click_times": args.get("click_times"),
470
+ "direction": args.get("direction") or args.get("action_direction"),
471
+ "action_direction": args.get("action_direction"),
472
+ }
473
+
474
+ if action_name in ("input_text", "focus_and_input_text"):
475
+ # The typed text is not the field's label.
476
+ action_obj["target_text"] = args.get("target_text") or args.get("target_text_or_desc")
477
+
478
+ # Handle special FA swipe "action" parameter
479
+ if action_name == "swipe" and "action" in args:
480
+ action_val = args["action"]
481
+ if isinstance(action_val, list) and len(action_val) == 4:
482
+ action_obj["coordinates"] = action_val
483
+ elif isinstance(action_val, str):
484
+ action_obj["action_direction"] = action_val
485
+
486
+ # Call format_action_clean to get factual natural language action
487
+ action_clean = format_action_clean(action_obj)
488
+
489
+ # Determine if there was an error in the result
490
+ error_detail = None
491
+ if isinstance(result, dict):
492
+ error_detail = result.get("error") or result.get("message")
493
+ elif isinstance(result, (list, tuple)) and result:
494
+ first_val = result[0]
495
+ if isinstance(first_val, str) and any(
496
+ x in first_val.lower() for x in ("error", "failed", "timeout")
497
+ ):
498
+ error_detail = first_val
499
+ elif isinstance(result, str):
500
+ res_lower = result.lower()
501
+ if "error" in res_lower or "failed" in res_lower or "timeout" in res_lower:
502
+ error_detail = result
503
+
504
+ if error_detail:
505
+ error_clean = str(error_detail).strip()
506
+
507
+ # If the error is formatted as a Python tuple string, e.g. "('Error...', None, None, None)"
508
+ if error_clean.startswith("(") and error_clean.endswith(")"):
509
+ parts = error_clean[1:-1].split(",")
510
+ if parts:
511
+ first_part = parts[0].strip()
512
+ if (first_part.startswith("'") and first_part.endswith("'")) or (
513
+ first_part.startswith('"') and first_part.endswith('"')
514
+ ):
515
+ first_part = first_part[1:-1]
516
+ error_clean = first_part
517
+
518
+ for prefix in (
519
+ "Error executing click: ",
520
+ "Error during click: ",
521
+ "Swipe failed: ",
522
+ "Error during swipe: ",
523
+ "Error executing key press: ",
524
+ "Error executing app launch: ",
525
+ "Error launching app: ",
526
+ ):
527
+ if error_clean.startswith(prefix):
528
+ error_clean = error_clean[len(prefix) :]
529
+ return f"{action_clean} -> (Execution failed: {error_clean})"
530
+ else:
531
+ return action_clean
532
+
533
+ clean_args = {k: v for k, v in args.items() if k not in ("state", "tool_call_id")}
534
+ args_str = json.dumps(clean_args, ensure_ascii=False)
535
+
536
+ # Clean the result
537
+ res_str = ""
538
+ if isinstance(result, str) and result.startswith("["):
539
+ try:
540
+ parsed = json.loads(result)
541
+ if isinstance(parsed, list):
542
+ result = parsed
543
+ except (json.JSONDecodeError, TypeError):
544
+ pass
545
+
546
+ if isinstance(result, list):
547
+ text_blocks = []
548
+ for block in result:
549
+ if isinstance(block, dict):
550
+ if block.get("type") == "text":
551
+ text_blocks.append(block.get("text", "").strip())
552
+ elif block.get("type") == "image_url":
553
+ pass
554
+ if text_blocks:
555
+ res_str = _clamp_text("\n".join(text_blocks), result_chars)
556
+ else:
557
+ res_str = _clamp_text(str(result), result_chars)
558
+ elif isinstance(result, dict):
559
+ error = result.get("error")
560
+ if error:
561
+ res_str = f"Error: {error}"
562
+ else:
563
+ res_str = _clamp_text(json.dumps(result, ensure_ascii=False), result_chars)
564
+ else:
565
+ res_str = _clamp_text(str(result), result_chars)
566
+
567
+ return f"`{name}({args_str})` -> {res_str}"
568
+
569
+
570
+ def _detect_interception(interleaved: list, result: Any) -> tuple[bool, str | None]:
571
+ """Whether the pre-execution safety net intercepted the step's action."""
572
+ for event in interleaved or []:
573
+ if event.get("type") == "tool_call" and event.get("name") in (
574
+ "safety_net_validation",
575
+ "safety_net_pixel_validation",
576
+ ):
577
+ res_list = safe_parse_validation_result(event.get("result"))
578
+ if len(res_list) > 0 and not res_list[0]:
579
+ detail = res_list[2] if len(res_list) >= 3 else None
580
+ return True, detail
581
+ break
582
+
583
+ # Fallback on the result structure (legacy / non-interleaved steps).
584
+ if isinstance(result, dict):
585
+ incident = result.get("incident")
586
+ if isinstance(incident, dict) and incident.get("kind") == "safety_net":
587
+ return True, incident.get("reason")
588
+ exec_list = result.get("execution") or []
589
+ if exec_list and isinstance(exec_list[0], dict):
590
+ attempts = exec_list[0].get("attempts") or []
591
+ if attempts and any(
592
+ "pre-execution validation" in str(att).lower()
593
+ or "validation failed" in str(att).lower()
594
+ for att in attempts
595
+ ):
596
+ return True, attempts[0]
597
+ return False, None
598
+
599
+
600
+ def format_step_action_result(step: dict) -> str:
601
+ action = step.get("action_taken")
602
+ action_clean = format_actions_clean(action) if action else "No Action"
603
+ result_obj = step.get("last_execution_result")
604
+
605
+ intercepted, safety_net_detail = _detect_interception(
606
+ step.get("interleaved_events") or [], result_obj
607
+ )
608
+
609
+ output_parts = [action_clean]
610
+
611
+ if intercepted:
612
+ detail_msg = f": {str(safety_net_detail).strip()}" if safety_net_detail else ""
613
+ output_parts.append(f"(Intercepted by Pre-Execution Safety Net{detail_msg})")
614
+ elif isinstance(result_obj, dict) and result_obj.get("status") == "failed":
615
+ error_msg = failed_execution_error(result_obj)
616
+ if error_msg:
617
+ output_parts.append(f"(Execution failed: {str(error_msg).strip()})")
618
+
619
+ return " -> ".join(output_parts)
620
+
621
+
622
+ def _burst_member_status(result: Any, index: int) -> str:
623
+ """Per-action outcome suffix for one member of a fast-action burst."""
624
+ if not isinstance(result, dict):
625
+ return ""
626
+ exec_list = result.get("execution") or []
627
+ if index >= len(exec_list) or not isinstance(exec_list[index], dict):
628
+ return " (not dispatched)"
629
+ attempts = exec_list[index].get("attempts") or []
630
+ if not attempts:
631
+ return " (dispatched)"
632
+ last = str(attempts[-1])
633
+ if last == "Dispatched":
634
+ return " (dispatched)"
635
+ if last.startswith("Skipped"):
636
+ return " (skipped)"
637
+ return f" (FAILED: {last.strip()})"
638
+
639
+
640
+ def replay_time_label(step: dict, session_start: Any) -> str:
641
+ """The step-header clock of a replay: the session-relative ``T+mm:ss``
642
+ offset every agent prompt and ``search_history`` use, or the legacy
643
+ ``Start: 12.3s`` text when no session start / step timestamp is known."""
644
+ ts = step.get("timestamp")
645
+ if (
646
+ isinstance(session_start, (int, float))
647
+ and not isinstance(session_start, bool)
648
+ and isinstance(ts, (int, float))
649
+ and not isinstance(ts, bool)
650
+ ):
651
+ from artemis.memory.chunking import step_offset_label
652
+
653
+ return step_offset_label(step, float(session_start))
654
+ return f"Start: {step.get('relative_time') or 'N/A'}"
655
+
656
+
657
+ def render_step_replay(
658
+ step: dict,
659
+ *,
660
+ session_start: Any = None,
661
+ result_chars: int = REPLAY_RESULT_CHARS,
662
+ include_summary: bool = True,
663
+ ) -> str:
664
+ """Render a step's reasoning, tool calls, action, and execution result.
665
+
666
+ ``step`` is an agent-friendly record (``DataEngine.get_agent_friendly_step``
667
+ / ``get_agent_friendly_steps``): coordinates already normalized, traces
668
+ already expanded into ``interleaved_events``. Used by ``replay_steps`` and
669
+ the MCP trace inspector. Screenshots are fetched separately.
670
+
671
+ ``session_start`` (the reader's ``session_start_time``) makes the header
672
+ carry the ``T+mm:ss`` session clock; see :func:`replay_time_label`.
673
+ """
674
+ return _render_step_detailed(
675
+ step=step,
676
+ relative_time=str(step.get("relative_time") or "N/A"),
677
+ summary=step.get("summary"),
678
+ action=step.get("action_taken"),
679
+ result=step.get("last_execution_result"),
680
+ is_most_recent=False,
681
+ result_chars=result_chars,
682
+ include_summary=include_summary,
683
+ time_label=replay_time_label(step, session_start),
684
+ )
685
+
686
+
687
+ def _screen_description_line(step: dict, summary: Any) -> str | None:
688
+ """The replay's ``[Screen]`` line: the step's stored screen description
689
+ (the visual-transition summary), or its status when no text exists yet.
690
+
691
+ ``extra_metadata.summary_status`` is ``pending`` while the background
692
+ summarizer has not finished, ``failed`` when it gave up; without a status
693
+ the line is omitted altogether.
694
+ """
695
+ if summary and str(summary).strip():
696
+ return f"[Screen]: {str(summary).strip()}"
697
+ status = str((step.get("extra_metadata") or {}).get("summary_status") or "").lower()
698
+ if status == "pending":
699
+ return "[Screen]: (screen description pending)"
700
+ if status in ("failed", "unavailable"):
701
+ return "[Screen]: (screen description unavailable)"
702
+ return None
703
+
704
+
705
+ def _render_step_detailed(
706
+ step: dict,
707
+ relative_time: str,
708
+ summary: str,
709
+ action: Any,
710
+ result: Any,
711
+ is_most_recent: bool,
712
+ *,
713
+ result_chars: int = LIVE_RESULT_CHARS,
714
+ include_summary: bool = False,
715
+ time_label: str | None = None,
716
+ ) -> str:
717
+ # 1. Step Header (``time_label`` overrides the live window's "Start: 12.3s")
718
+ status_str = "Most Recent Step, " if is_most_recent else ""
719
+ clock = time_label or f"Start: {relative_time}"
720
+
721
+ # In the live window the summary is omitted from the detailed view header
722
+ # as it is already fully detailed below; replay callers opt in because the
723
+ # summary is the Operator's own claim about the step, which an auditor
724
+ # compares against the evidence that follows.
725
+ step_line = f"- **Step {step['step_number']} ({status_str}{clock})**"
726
+ if include_summary:
727
+ screen_line = _screen_description_line(step, summary)
728
+ if screen_line:
729
+ step_line += f"\n * {screen_line}"
730
+
731
+ interleaved = step.get("interleaved_events") or []
732
+
733
+ # 2. Determine if Safety Net validation failed (intercepted)
734
+ intercepted, safety_net_detail = _detect_interception(interleaved, result)
735
+
736
+ # 3. Collect the Operator's own events (system guardians are rendered separately)
737
+ operator_events = []
738
+
739
+ INTERNAL_SYSTEM_TOOLS = {
740
+ "safety_net_validation",
741
+ "safety_net_pixel_validation",
742
+ "hopper",
743
+ }
744
+
745
+ if interleaved:
746
+ for event in interleaved:
747
+ e_type = event.get("type")
748
+ name = event.get("name") or ""
749
+ if e_type in ("thought", "native_thought"):
750
+ operator_events.append(event)
751
+ elif (
752
+ e_type == "tool_call"
753
+ and name not in INTERNAL_SYSTEM_TOOLS
754
+ and not name.startswith("_exec_")
755
+ ):
756
+ operator_events.append(event)
757
+ else:
758
+ # Fallback for legacy/non-interleaved steps
759
+ raw_thinking = step.get("operator_raw_thinking")
760
+ native_thinking = step.get("operator_native_thinking")
761
+ if native_thinking:
762
+ operator_events.append({"type": "native_thought", "content": native_thinking})
763
+ if raw_thinking:
764
+ operator_events.append({"type": "thought", "content": raw_thinking})
765
+
766
+ legacy_tool_calls = step.get("tool_calls") or []
767
+ for tc in legacy_tool_calls:
768
+ name = tc.get("name") or ""
769
+ if name in INTERNAL_SYSTEM_TOOLS or name.startswith("_exec_"):
770
+ continue
771
+ payload = tc.get("payload") or {}
772
+ tc_args = payload.get("args") or tc.get("args") or {}
773
+ tc_result = (
774
+ payload.get("result") or payload.get("error") or tc.get("result") or "No result"
775
+ )
776
+ operator_events.append(
777
+ {
778
+ "type": "tool_call",
779
+ "name": name,
780
+ "args": tc_args,
781
+ "result": tc_result,
782
+ }
783
+ )
784
+
785
+ # Helper to check if a thought content is already in operator_events
786
+ def thought_exists(content):
787
+ content_clean = content.strip().lower()
788
+ for ev in operator_events:
789
+ if (
790
+ ev.get("type") in ("thought", "native_thought")
791
+ and ev.get("content", "").strip().lower() == content_clean
792
+ ):
793
+ return True
794
+ return False
795
+
796
+ # Extract thought from final physical action if present
797
+ action_thought = None
798
+ if action:
799
+ action_parsed = action[0] if isinstance(action, list) and action else action
800
+ if isinstance(action_parsed, dict):
801
+ action_thought = action_parsed.get("thought")
802
+ if action_thought and action_thought.strip() and not thought_exists(action_thought):
803
+ operator_events.insert(0, {"type": "thought", "content": action_thought.strip()})
804
+
805
+ # Extract native thinking from LLM thinking block first if not already present
806
+ native_thinking = step.get("operator_native_thinking")
807
+ if native_thinking and native_thinking.strip() and not thought_exists(native_thinking):
808
+ operator_events.append({"type": "native_thought", "content": native_thinking.strip()})
809
+
810
+ # Extract raw thinking from LLM text content second if not already present
811
+ raw_thinking = step.get("operator_raw_thinking")
812
+ if raw_thinking and raw_thinking.strip() and not thought_exists(raw_thinking):
813
+ operator_events.append({"type": "thought", "content": raw_thinking.strip()})
814
+
815
+ # 4. Render the acting model's reasoning and tool calls (role-neutral: the
816
+ # same block replays a Flash turn and a Pro Operator turn)
817
+ if operator_events:
818
+ step_line += f"\n * {REASONING_BLOCK_LABEL}:"
819
+ ACTION_TOOLS = {
820
+ "click",
821
+ "swipe",
822
+ "input_text",
823
+ "press_key",
824
+ "long_press",
825
+ "launch_app",
826
+ "wait_for_delay",
827
+ }
828
+ for event in operator_events:
829
+ e_type = event["type"]
830
+ content = event.get("content") or ""
831
+ name = event.get("name") or ""
832
+ if not content.strip() and e_type != "tool_call":
833
+ continue
834
+ if e_type in ("native_thought", "thought"):
835
+ cleaned = re.sub(
836
+ r"<short_term_memory>.*?</short_term_memory>",
837
+ "",
838
+ content,
839
+ flags=re.DOTALL,
840
+ )
841
+ cleaned = re.sub(r"</?thought>", "", cleaned, flags=re.DOTALL).strip()
842
+ if cleaned:
843
+ step_line += f"\n - {_indent_continuation(cleaned)}"
844
+ elif e_type == "tool_call":
845
+ if name in ACTION_TOOLS:
846
+ continue
847
+ formatted = format_tool_call_clean(
848
+ name,
849
+ event.get("args") or {},
850
+ event.get("result"),
851
+ result_chars=result_chars,
852
+ )
853
+ if formatted:
854
+ step_line += f"\n - [Tool Call]: {formatted}"
855
+
856
+ # 5. Render the action the step executed (single vetted action or a burst).
857
+ # A failed action's error is printed once, on the [Result] line below.
858
+ if action:
859
+ actions = action if isinstance(action, list) else [action]
860
+ if len(actions) > 1:
861
+ step_line += (
862
+ f"\n * {BURST_BLOCK_LABEL}: {len(actions)} actions fired back to"
863
+ " back without the safety net"
864
+ )
865
+ for i, member in enumerate(actions):
866
+ step_line += (
867
+ f"\n {i + 1}. {format_action_clean(member)}{_burst_member_status(result, i)}"
868
+ )
869
+ else:
870
+ action_clean = format_action_clean(actions[0])
871
+ if intercepted:
872
+ step_line += (
873
+ f"\n * {ACTION_BLOCK_LABEL}: {action_clean}"
874
+ " (Intercepted by Pre-Execution Safety Net)"
875
+ )
876
+ else:
877
+ step_line += f"\n * {ACTION_BLOCK_LABEL}: {action_clean}"
878
+
879
+ # 6. Render Pre-Execution Safety Net
880
+ if intercepted:
881
+ step_line += "\n * [Pre-Execution Safety Net]:"
882
+ detail_str = (
883
+ f"(Intercepted by Pre-Execution Safety Net: {str(safety_net_detail).strip()})"
884
+ if safety_net_detail
885
+ else "(Intercepted by Pre-Execution Safety Net)"
886
+ )
887
+ step_line += f"\n - [Safety Net Check]: {detail_str}"
888
+
889
+ # 7. Validator Execution Result (an open incident renders as the result line)
890
+ if result:
891
+ result_clean = format_result_clean(result)
892
+ if result_clean:
893
+ step_line += f"\n * [Result]: {result_clean}"
894
+
895
+ return step_line
896
+
897
+
898
+ def build_plan_and_history(
899
+ task_plan: str,
900
+ steps: list,
901
+ current_subgoal_hash: str,
902
+ keep_subgoal_hashes: set | None = None,
903
+ min_summaries: int = 5,
904
+ last_n_detailed: int = 1,
905
+ all_detailed: bool = False,
906
+ strict_milestone_pruning: bool = False,
907
+ recent_window_size: int = 3,
908
+ chunks: list | None = None,
909
+ ) -> str:
910
+ """Builds a clean plan list and separate flat chronological execution history with adaptive compression.
911
+
912
+ ``chunks`` (optional, M4): pre-rendered history-chunk blocks — each a dict
913
+ with ``start_step_number``/``end_step_number``/``text``. When provided,
914
+ the chunk blocks are emitted right after the history header and the
915
+ per-step lines inside any chunked range are dropped (the chunk block
916
+ replaces them), except steps that must render detailed (the recent
917
+ window / the most-recent step stay untouched). When ``chunks`` is
918
+ ``None`` or empty the output is byte-identical to the pre-M4 rendering.
919
+ """
920
+ # 1. Plan Checklist Section
921
+ output_parts = []
922
+
923
+ # Check if task_plan has actual subgoals (single-source checkbox grammar).
924
+ has_subgoals = bool(task_plan) and bool(parse_plan(task_plan).items)
925
+
926
+ if has_subgoals:
927
+ output_parts.append("--- Task Plan ---")
928
+ output_parts.append(task_plan)
929
+ output_parts.append("")
930
+ else:
931
+ # Print the plan fallback if not empty
932
+ if task_plan and task_plan != "No task plan yet.":
933
+ output_parts.extend(
934
+ [
935
+ "--- Task Plan ---",
936
+ task_plan,
937
+ "",
938
+ ]
939
+ )
940
+
941
+ output_parts.append("--- Execution History ---")
942
+
943
+ # 2. Chronological Steps History Section
944
+ if not steps:
945
+ output_parts.append("No steps executed yet.")
946
+ else:
947
+ full_info_step_id = steps[-1]["step_id"]
948
+ visible_step_ids = set()
949
+ visible_step_ids.add(full_info_step_id)
950
+
951
+ # Calculate which steps are visible using milestone filtering or sliding window compression
952
+ if keep_subgoal_hashes is not None:
953
+ # Milestone-specific filtering (checker / diagnoser)
954
+ for step in steps:
955
+ meta = step.get("extra_metadata") or {}
956
+ step_subgoal_hash = meta.get("subgoal_hash")
957
+ if step_subgoal_hash in keep_subgoal_hashes:
958
+ visible_step_ids.add(step["step_id"])
959
+ else:
960
+ # Default sliding window compression or strict milestone pruning
961
+ if strict_milestone_pruning:
962
+ recent_step_ids = set()
963
+ actual_recent_n = min(len(steps), recent_window_size)
964
+ for step in steps[-actual_recent_n:]:
965
+ recent_step_ids.add(step["step_id"])
966
+
967
+ for i in range(len(steps) - 1, -1, -1):
968
+ step = steps[i]
969
+ step_id = step["step_id"]
970
+ if step_id == full_info_step_id:
971
+ continue
972
+
973
+ meta = step.get("extra_metadata") or {}
974
+ step_subgoal_hash = meta.get("subgoal_hash")
975
+
976
+ # Strict pruning: only keep if active subgoal OR in recent continuity window
977
+ if step_subgoal_hash == current_subgoal_hash or step_id in recent_step_ids:
978
+ visible_step_ids.add(step_id)
979
+ else:
980
+ summary_count = 0
981
+ for i in range(len(steps) - 1, -1, -1):
982
+ step = steps[i]
983
+ step_id = step["step_id"]
984
+ if step_id == full_info_step_id:
985
+ continue
986
+
987
+ meta = step.get("extra_metadata") or {}
988
+ step_subgoal_hash = meta.get("subgoal_hash")
989
+
990
+ # Keep steps under the active milestone, or slide back until min_summaries is met
991
+ if step_subgoal_hash == current_subgoal_hash or summary_count < min_summaries:
992
+ visible_step_ids.add(step_id)
993
+ summary_count += 1
994
+
995
+ detailed_step_ids = set()
996
+ # Pre-calculate steps that MUST be rendered in detailed mode (last N detailed)
997
+ if last_n_detailed > 0:
998
+ actual_n = min(len(steps), last_n_detailed)
999
+ for step in steps[-actual_n:]:
1000
+ detailed_step_ids.add(step["step_id"])
1001
+
1002
+ # Ensure the last step is always in detailed_step_ids
1003
+ detailed_step_ids.add(full_info_step_id)
1004
+
1005
+ # Chunk blocks replace the per-step lines of already-chunked
1006
+ # ranges. Chunks only ever cover frozen (old) turns, so they are
1007
+ # emitted first, in chronological order; steps that must render
1008
+ # detailed (recent window / most-recent step) are never suppressed.
1009
+ chunked_ranges: list[tuple[int, int]] = []
1010
+ if chunks:
1011
+ for chunk in sorted(chunks, key=lambda c: c.get("start_step_number") or 0):
1012
+ text = chunk.get("text")
1013
+ if not text:
1014
+ continue
1015
+ try:
1016
+ start_n = int(chunk.get("start_step_number"))
1017
+ end_n = int(chunk.get("end_step_number"))
1018
+ except (TypeError, ValueError):
1019
+ continue
1020
+ chunked_ranges.append((start_n, end_n))
1021
+ output_parts.append(text)
1022
+
1023
+ for step in steps:
1024
+ step_id = step["step_id"]
1025
+
1026
+ # Skip steps that are compressed (not marked as visible)
1027
+ if step_id not in visible_step_ids:
1028
+ continue
1029
+
1030
+ if chunked_ranges and step_id not in detailed_step_ids and step_id != full_info_step_id:
1031
+ step_number = step.get("step_number")
1032
+ if isinstance(step_number, int) and any(
1033
+ start_n <= step_number <= end_n for start_n, end_n in chunked_ranges
1034
+ ):
1035
+ continue # represented by its chunk block above
1036
+
1037
+ relative_time = step.get("relative_time") or "N/A"
1038
+ summary = step.get("summary")
1039
+ action = step.get("action_taken")
1040
+ thinking = step.get("operator_raw_thinking")
1041
+ result = step.get("last_execution_result")
1042
+
1043
+ # Determine if this step should be detailed (e.g. current/recent step or missing summary)
1044
+ render_as_detailed = (
1045
+ all_detailed
1046
+ or (step_id in detailed_step_ids)
1047
+ or (not summary or summary == "Action executed.")
1048
+ )
1049
+
1050
+ is_most_recent = step_id == full_info_step_id
1051
+ if render_as_detailed:
1052
+ step_line = _render_step_detailed(
1053
+ step,
1054
+ relative_time,
1055
+ summary,
1056
+ action,
1057
+ result,
1058
+ is_most_recent,
1059
+ )
1060
+ else:
1061
+ step_line = f"- *Step {step['step_number']} (Start: {relative_time}): {summary}*"
1062
+ if action:
1063
+ action_clean = format_step_action_result(step)
1064
+ step_line += f"\n *Action*: {action_clean}"
1065
+
1066
+ output_parts.append(step_line)
1067
+
1068
+ return "\n".join(output_parts)
1069
+
1070
+
1071
+ def get_active_subgoal_hashes(task_plan: str) -> tuple[str, str | None]:
1072
+ """Parses task_plan content to find the active (top-level, nested) subgoal hashes.
1073
+
1074
+ Returns ``(parent_hash, sub_hash)``: the parent is the active top-level
1075
+ milestone (all active nested items consolidate under it); the sub hash is
1076
+ the bottom-most active nested item — the live sub-goal ledger leaf — or
1077
+ ``None`` when the milestone has no in-progress sub-goal. Falls back to the
1078
+ first pending subgoal if no active subgoal is found and all are pending.
1079
+ """
1080
+ if not task_plan:
1081
+ return "default", None
1082
+
1083
+ try:
1084
+ snapshot = parse_plan(task_plan)
1085
+
1086
+ # Bottom-most active item wins (most specific deeply nested subgoal)
1087
+ active_item = snapshot.last_active()
1088
+ if active_item is not None:
1089
+ if not active_item.is_top_level:
1090
+ parent = snapshot.parent_of(active_item)
1091
+ if parent is not None:
1092
+ return parent.key, active_item.key
1093
+ return active_item.key, None
1094
+
1095
+ # No active subgoal found. Check if all are completed.
1096
+ if all(item.is_done for item in snapshot.top_level):
1097
+ return "default", None
1098
+
1099
+ # SAFE FALLBACK: if every item is untouched, the first pending one is active.
1100
+ if all(item.is_pending for item in snapshot.top_level):
1101
+ for item in snapshot.items:
1102
+ if item.is_pending:
1103
+ return item.key, None
1104
+
1105
+ except Exception as exc: # pylint: disable=broad-exception-caught
1106
+ # Unparseable plan text falls back to the default bucket.
1107
+ logger.debug("Active subgoal lookup fell back to default: %s", exc, exc_info=True)
1108
+
1109
+ return "default", None
1110
+
1111
+
1112
+ def get_completed_subgoal_hashes(task_plan: str) -> set[str]:
1113
+ """Extracts MD5 hashes of all completed [x] top-level subgoals from task_plan."""
1114
+ if not task_plan:
1115
+ return set()
1116
+
1117
+ return {item.key for item in parse_plan(task_plan).top_level if item.is_done}
1118
+
1119
+
1120
+ def get_all_subgoal_aliases(target_hash: str, base_dir: str | Path | None = None) -> set[str]:
1121
+ """Transitively resolves all older subgoal hashes that map to target_hash in subgoal_hash_chain.json."""
1122
+ aliases = {target_hash}
1123
+ if not base_dir or not isinstance(base_dir, (str, Path)):
1124
+ return aliases
1125
+
1126
+ chain_path = Path(base_dir) / "notes" / "subgoal_hash_chain.json"
1127
+ if not chain_path.exists():
1128
+ return aliases
1129
+
1130
+ try:
1131
+ chain = json.loads(chain_path.read_text(encoding="utf-8"))
1132
+ except Exception:
1133
+ return aliases
1134
+
1135
+ # Transitively resolve old_hash -> new_hash chains
1136
+ for old_hash in chain:
1137
+ curr = old_hash
1138
+ visited = set()
1139
+ while curr in chain and curr not in visited:
1140
+ visited.add(curr)
1141
+ curr = chain[curr]
1142
+ if curr == target_hash:
1143
+ aliases.add(old_hash)
1144
+
1145
+ return aliases
1146
+
1147
+
1148
+ def get_recent_subgoal_hashes(
1149
+ steps: list, current_subgoal_hash: str, base_dir: str | Path | None = None
1150
+ ) -> set[str]:
1151
+ """Helper to get a set of subgoal hashes containing the current active subgoal hash,
1152
+
1153
+ its historical failed/renamed aliases (resolved via
1154
+ subgoal_hash_chain.json),
1155
+ and the most recent completed subgoal hash as transition context.
1156
+ """
1157
+ # 1. Resolve all alias hashes for the active subgoal
1158
+ aliases = get_all_subgoal_aliases(current_subgoal_hash, base_dir)
1159
+ keep_hashes = set(aliases)
1160
+
1161
+ # 2. Find the most recent completed subgoal in history
1162
+ # (meaning a step whose hash is NOT an alias of the active subgoal)
1163
+ recent_completed_hash = None
1164
+ for step in reversed(steps):
1165
+ meta = step.get("extra_metadata") or {}
1166
+ s_hash = meta.get("subgoal_hash")
1167
+ if s_hash and s_hash not in aliases:
1168
+ recent_completed_hash = s_hash
1169
+ break
1170
+
1171
+ if recent_completed_hash:
1172
+ keep_hashes.add(recent_completed_hash)
1173
+
1174
+ return keep_hashes