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,859 @@
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
+ """Checker: independent, zero-side-effect verdict agent.
16
+
17
+ Two entry points share one tool loop:
18
+
19
+ - :func:`run_checkpoint_check` — audits a completed subgoal's ``on_complete``
20
+ check items against the evidence anchored at completion time. Deliberately
21
+ has NO live-screen access: the device has moved on, and the tool table (not
22
+ prompt persuasion) enforces the evidence discipline.
23
+ - :func:`run_final_check` — audits the user's original goal plus all declared
24
+ check items at task exit, with the final screen state available.
25
+
26
+ Both entries only ever read: step history, notes, and an enumerated table of
27
+ read-only device probes. No device actions, no note writes, no sub-agents.
28
+ The release decision is computed by the caller from the verdicts
29
+ (:func:`verdicts_allow_release`) — releasing never rewrites a verdict.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import asyncio
35
+ import json
36
+ import re
37
+ import time
38
+ from dataclasses import dataclass, field
39
+ from pathlib import Path
40
+ from typing import Any, Literal
41
+
42
+ from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
43
+ from langchain_core.tools import BaseTool, tool
44
+
45
+ from pydantic import BaseModel, Field, ValidationError
46
+
47
+ from artemis.core.tool_failure import ToolFailure, is_tool_failure
48
+ from artemis.constants import CHECKER_MAX_ITERATIONS
49
+ from artemis.context import ArtemisContext
50
+ from artemis.data_engine.context_vars import CURRENT_TRACE_ID
51
+ from artemis.data_engine.trace import trace, trace_langchain_tool
52
+ from artemis.services.llm import acomplete, get_llm, invoke_llm_with_timeout_message
53
+ from artemis.tools.history import get_history_tools
54
+ from artemis.tools.scratchpad import get_read_note_tool_pure
55
+ from artemis.tools.tool_wrapper import (
56
+ invoke_tool_with_injection,
57
+ split_multimodal_result,
58
+ tool_result_messages,
59
+ )
60
+ from artemis.utils.task_tree import format_actions_clean
61
+ from artemis.utils.ocr_api import is_ocr_configured, perform_ocr
62
+ from artemis.utils.ocr_xml_fusion import (
63
+ _crop_image_remove_status_bar,
64
+ _detect_status_bar_height,
65
+ _map_coordinates_back,
66
+ fuse_ocr_with_xml,
67
+ )
68
+ from artemis.utils.visualization import format_minimal_list_with_elements
69
+ from third_party.mobile_use.utils.logger import get_logger
70
+
71
+ logger = get_logger(__name__)
72
+
73
+ _PROBE_OUTPUT_LIMIT = 8000
74
+
75
+
76
+ # --- Structured output ----------------------------------------------------------------
77
+
78
+
79
+ class CheckVerdict(BaseModel):
80
+ """One check item's verdict. The verdict value is never altered by the
81
+ release decision (fail-open affects release only)."""
82
+
83
+ item_text: str = Field(description="The check line's text, quoted verbatim.")
84
+ kind: Literal["verify", "assert"]
85
+ status: Literal["passed", "failed", "inconclusive"]
86
+ evidence: str = Field(
87
+ description=(
88
+ "Concrete observation backing the verdict: a probe output, note"
89
+ " excerpt, or screenshot element. Vague evidence invalidates a"
90
+ " failed verdict."
91
+ )
92
+ )
93
+ suggestion: str = Field(
94
+ default="",
95
+ description="For failed verify items only: actionable repair hint.",
96
+ )
97
+
98
+
99
+ class CheckReport(BaseModel):
100
+ verdicts: list[CheckVerdict] = Field(default_factory=list)
101
+ unmet_subgoals: list[str] = Field(
102
+ default_factory=list,
103
+ description=(
104
+ "Final check only: plan subgoals (quoted verbatim) marked complete"
105
+ " whose verify criteria are demonstrably unmet."
106
+ ),
107
+ )
108
+
109
+
110
+ def verdicts_allow_release(report: CheckReport) -> bool:
111
+ """Node-side release decision: every verify verdict passed or inconclusive.
112
+
113
+ Computed outside the model so that releasing (fail-open) can never rewrite
114
+ a verdict value. Assert failures never block release.
115
+ """
116
+ return all(
117
+ v.status in ("passed", "inconclusive") for v in report.verdicts if v.kind == "verify"
118
+ )
119
+
120
+
121
+ # --- Read-only device probes (enumerated; no free-form command channel) --------------
122
+
123
+
124
+ _KEY_RE = re.compile(r"^[A-Za-z0-9._-]+$")
125
+ _URI_RE = re.compile(r"^content://[A-Za-z0-9./_-]+$")
126
+ _SETTING_NAMESPACES = frozenset({"system", "secure", "global"})
127
+
128
+
129
+ @dataclass(frozen=True)
130
+ class ProbeParam:
131
+ name: str
132
+ pattern: re.Pattern
133
+ allowed: frozenset[str] | None = None
134
+
135
+
136
+ @dataclass(frozen=True)
137
+ class ProbeSpec:
138
+ """A fixed argv template. ``<name>`` placeholders are substituted with
139
+ regex-validated parameters; argv is built as a list and never passes
140
+ through shell string concatenation."""
141
+
142
+ argv: tuple[str, ...]
143
+ params: tuple[ProbeParam, ...] = field(default=())
144
+
145
+
146
+ PROBES: dict[str, ProbeSpec] = {
147
+ "alarms": ProbeSpec(argv=("dumpsys", "alarm")),
148
+ "battery": ProbeSpec(argv=("dumpsys", "battery")),
149
+ "foreground": ProbeSpec(argv=("dumpsys", "activity", "activities")),
150
+ "notifications": ProbeSpec(argv=("dumpsys", "notification")),
151
+ "setting": ProbeSpec(
152
+ argv=("settings", "get", "<namespace>", "<key>"),
153
+ params=(
154
+ ProbeParam("namespace", _KEY_RE, _SETTING_NAMESPACES),
155
+ ProbeParam("key", _KEY_RE),
156
+ ),
157
+ ),
158
+ "content": ProbeSpec(
159
+ argv=("content", "query", "--uri", "<uri>"),
160
+ params=(ProbeParam("uri", _URI_RE),),
161
+ ),
162
+ "packages": ProbeSpec(argv=("pm", "list", "packages")),
163
+ "prop": ProbeSpec(argv=("getprop", "<key>"), params=(ProbeParam("key", _KEY_RE),)),
164
+ }
165
+
166
+
167
+ def build_probe_argv(kind: str, params: dict[str, Any] | None = None) -> list[str]:
168
+ """Builds the argv list for an enumerated probe.
169
+
170
+ Raises ``ValueError`` for unknown probe kinds, missing/extra parameters, or
171
+ parameter values that fail their regex (whitespace and shell metacharacters
172
+ can never pass). ``dumpsys battery set``-style mutating subcommands are
173
+ structurally inexpressible: the no-parameter probes accept no arguments at
174
+ all.
175
+ """
176
+ spec = PROBES.get(kind)
177
+ if spec is None:
178
+ raise ValueError(f"Unknown probe kind '{kind}'. Allowed: {', '.join(sorted(PROBES))}.")
179
+ params = dict(params or {})
180
+ expected = {p.name for p in spec.params}
181
+ extra = set(params) - expected
182
+ if extra:
183
+ raise ValueError(f"Probe '{kind}' accepts no parameter(s): {sorted(extra)}")
184
+ missing = expected - set(params)
185
+ if missing:
186
+ raise ValueError(f"Probe '{kind}' requires parameter(s): {sorted(missing)}")
187
+
188
+ values: dict[str, str] = {}
189
+ for p in spec.params:
190
+ raw = params[p.name]
191
+ if not isinstance(raw, str) or not p.pattern.match(raw):
192
+ raise ValueError(f"Probe '{kind}' parameter '{p.name}' has an invalid value.")
193
+ if p.allowed is not None and raw not in p.allowed:
194
+ raise ValueError(
195
+ f"Probe '{kind}' parameter '{p.name}' must be one of {sorted(p.allowed)}."
196
+ )
197
+ values[p.name] = raw
198
+
199
+ argv: list[str] = []
200
+ for token in spec.argv:
201
+ if token.startswith("<") and token.endswith(">"):
202
+ argv.append(values[token[1:-1]])
203
+ else:
204
+ argv.append(token)
205
+ return argv
206
+
207
+
208
+ async def _execute_probe(ctx: ArtemisContext, argv: list[str]) -> str:
209
+ """Executes a validated probe argv over the ADB device channel.
210
+
211
+ A failed probe answers with a :class:`ToolFailure` so the tool loop marks
212
+ the result as an error structurally rather than by its wording.
213
+ """
214
+ try:
215
+ adb_client = ctx.get_adb_client()
216
+ device = adb_client.device(serial=ctx.device.device_id)
217
+ output = await asyncio.to_thread(device.shell, argv)
218
+ text = str(output)
219
+ if len(text) > _PROBE_OUTPUT_LIMIT:
220
+ text = text[:_PROBE_OUTPUT_LIMIT] + "\n... [output truncated]"
221
+ return text
222
+ except Exception as e:
223
+ return ToolFailure(f"Error executing probe: {e}")
224
+
225
+
226
+ def get_probe_tool(ctx: ArtemisContext) -> BaseTool:
227
+ @tool
228
+ async def probe_device(kind: str, params: dict | None = None) -> str:
229
+ """Runs one enumerated read-only device state probe.
230
+
231
+ kind: one of 'alarms', 'battery', 'foreground', 'notifications',
232
+ 'setting' (params: namespace in system|secure|global, key),
233
+ 'content' (params: uri, a content:// URI), 'packages',
234
+ 'prop' (params: key). Anything else is rejected.
235
+ """
236
+ try:
237
+ argv = build_probe_argv(kind, params)
238
+ except ValueError as e:
239
+ return ToolFailure(f"Error: {e}")
240
+ return await _execute_probe(ctx, argv)
241
+
242
+ return probe_device
243
+
244
+
245
+ # --- Prompt assembly (segments x entry x content x config) ---------------------------
246
+
247
+
248
+ def _load_prompts() -> dict[str, str]:
249
+ prompts_path = Path(__file__).parent / "checker.json"
250
+ try:
251
+ return json.loads(prompts_path.read_text(encoding="utf-8"))
252
+ except Exception as e:
253
+ logger.error(f"Failed to load checker prompts: {e}")
254
+ return {}
255
+
256
+
257
+ def probes_enabled(ctx: ArtemisContext) -> bool:
258
+ setup = getattr(ctx, "execution_setup", None)
259
+ return not (setup and getattr(setup, "disable_device_probes", False))
260
+
261
+
262
+ def assemble_checker_prompt_segments(
263
+ entry: Literal["checkpoint", "final"],
264
+ check_items: list,
265
+ probe_tool_registered: bool,
266
+ prompts: dict[str, str] | None = None,
267
+ ) -> str:
268
+ """Assembles the system prompt segments for one run.
269
+
270
+ Segment table (assembly discipline: every rule present corresponds to an
271
+ active mechanism, nothing more):
272
+ - base rules: always
273
+ - verify semantics: only when a verify item is under audit
274
+ - assert semantics: only when an assert item is under audit
275
+ - anchor guide: checkpoint entry only
276
+ - final guide: final entry only
277
+ - probe guide: only when the probe tool is actually registered
278
+ """
279
+ prompts = prompts if prompts is not None else _load_prompts()
280
+ segments = [prompts.get("base_rules", "")]
281
+ kinds = {getattr(ci, "kind", None) for ci in check_items}
282
+ if "verify" in kinds:
283
+ segments.append(prompts.get("verify_semantics", ""))
284
+ if "assert" in kinds:
285
+ segments.append(prompts.get("assert_semantics", ""))
286
+ if entry == "checkpoint":
287
+ segments.append(prompts.get("anchor_guide", ""))
288
+ else:
289
+ segments.append(prompts.get("final_guide", ""))
290
+ if probe_tool_registered:
291
+ segments.append(prompts.get("probe_guide", ""))
292
+ return "\n\n".join(s for s in segments if s)
293
+
294
+
295
+ def build_checker_tools(
296
+ ctx: ArtemisContext, entry: Literal["checkpoint", "final"]
297
+ ) -> list[BaseTool]:
298
+ """The tool table is the single source of Checker authority — all read-only.
299
+
300
+ The checkpoint entry deliberately gets NO live-screen capability: its
301
+ evidence is anchored history plus persistent-state probes; the current
302
+ screen has already moved past the audited moment. The final entry receives
303
+ the final screen through its prompt (not a tool), so the table is identical
304
+ for both entries; the difference lives in what the prompt provides.
305
+ Device actions, note writes, and sub-agents are registered for neither.
306
+
307
+ The history tools are the shared ``search_history`` / ``replay_steps`` /
308
+ ``get_step_screenshot`` definitions (read-only, deterministic).
309
+ """
310
+ tools: list[BaseTool] = [
311
+ *get_history_tools(ctx),
312
+ get_read_note_tool_pure(ctx),
313
+ ]
314
+ if probes_enabled(ctx):
315
+ tools.append(get_probe_tool(ctx))
316
+ return tools
317
+
318
+
319
+ # --- History formatting --------------------------------------------------------------
320
+
321
+
322
+ def _load_steps(ctx: ArtemisContext) -> list[dict] | str:
323
+ """The agent-friendly step records, or the reason they are unavailable."""
324
+ if not ctx.data_engine:
325
+ return "No execution history available."
326
+ try:
327
+ return list(ctx.data_engine.get_agent_friendly_steps() or [])
328
+ except Exception as e:
329
+ return f"Failed to load execution history: {e}"
330
+
331
+
332
+ def _step_offset_label(step: dict, session_start: float | None) -> str:
333
+ """Session-relative ``T+mm:ss`` label of a step: the clock every agent shares.
334
+
335
+ The recorded timestamp against the session start is authoritative; a
336
+ record that only carries the engine's ``"97.7s"`` label is converted.
337
+ """
338
+ from artemis.memory.transcript import format_session_offset
339
+
340
+ ts = step.get("timestamp")
341
+ if isinstance(session_start, (int, float)) and isinstance(ts, (int, float)):
342
+ return format_session_offset(float(ts) - float(session_start))
343
+ rel = str(step.get("relative_time") or "").strip()
344
+ if rel.startswith("T+"):
345
+ return rel
346
+ if rel.endswith("s"):
347
+ try:
348
+ return format_session_offset(float(rel[:-1]))
349
+ except ValueError:
350
+ pass
351
+ return "T+??:??"
352
+
353
+
354
+ def _format_history(ctx: ArtemisContext, limit: int = 60, steps: list[dict] | None = None) -> str:
355
+ if steps is None:
356
+ steps = _load_steps(ctx)
357
+ if isinstance(steps, str):
358
+ return steps
359
+ if not steps:
360
+ return "No execution history available."
361
+ session_start = getattr(ctx.data_engine, "session_start_time", None)
362
+ lines = []
363
+ for s in steps[-limit:]:
364
+ num = s.get("step_number")
365
+ summary = (s.get("summary") or "").strip()
366
+ action = s.get("action_taken")
367
+ if not summary and not action:
368
+ # Nothing to audit on this step: an empty row is only noise.
369
+ continue
370
+ action_str = ""
371
+ if action and not summary:
372
+ # The shared renderer keeps the target's provenance visible: a
373
+ # self-described coordinate target reads "'play button' (self-described)",
374
+ # an observed index target reads "'Login'" with no marker.
375
+ try:
376
+ action_str = format_actions_clean(action)[:160]
377
+ except Exception:
378
+ action_str = str(action)[:160]
379
+ label = _step_offset_label(s, session_start)
380
+ lines.append(f"- Step {num} ({label}): {summary or action_str}".rstrip())
381
+ return "\n".join(lines) or "No execution history available."
382
+
383
+
384
+ def _format_user_guidance(ctx: ArtemisContext, steps: list[dict] | None = None) -> str:
385
+ """Every mid-run user instruction, read from the step it was stamped on."""
386
+ if steps is None:
387
+ steps = _load_steps(ctx)
388
+ if isinstance(steps, str) or not steps:
389
+ return "(none)"
390
+ session_start = getattr(ctx.data_engine, "session_start_time", None)
391
+ lines = []
392
+ for s in steps:
393
+ instruction = (s.get("extra_metadata") or {}).get("injected_instruction")
394
+ if isinstance(instruction, str) and instruction.strip():
395
+ label = _step_offset_label(s, session_start)
396
+ lines.append(f'- Step {s.get("step_number")} ({label}): "{instruction.strip()}"')
397
+ return "\n".join(lines) or "(none)"
398
+
399
+
400
+ def _format_check_items(check_items: list) -> str:
401
+ lines = []
402
+ for ci in check_items:
403
+ lines.append(f"- [{ci.kind}] ({ci.when}) {ci.text}")
404
+ return "\n".join(lines) if lines else "(none)"
405
+
406
+
407
+ # --- Shared tool loop ----------------------------------------------------------------
408
+
409
+
410
+ async def _structured_report(llm, messages) -> CheckReport:
411
+ structured_llm = llm.with_structured_output(CheckReport)
412
+ # A conversation must end with a user turn: Gemini rejects requests whose
413
+ # last message is the model's own answer ("Requests ending with a model
414
+ # turn are not supported"), which is exactly the state after the loop's
415
+ # final tool-free reply. Nudge with an explicit verdict request instead.
416
+ if messages and isinstance(messages[-1], AIMessage):
417
+ messages = [
418
+ *messages,
419
+ HumanMessage(content="Now provide your structured verdict report."),
420
+ ]
421
+ result = await invoke_llm_with_timeout_message(structured_llm.ainvoke(messages))
422
+ if isinstance(result, CheckReport):
423
+ return result
424
+ if isinstance(result, dict):
425
+ try:
426
+ return CheckReport.model_validate(result)
427
+ except ValidationError as exc:
428
+ logger.debug("Structured verdict dict failed CheckReport validation: %s", exc)
429
+ return CheckReport(verdicts=[])
430
+
431
+
432
+ def _normalize_report(report: CheckReport, check_items: list) -> CheckReport:
433
+ """Node-side hygiene: vague failed verdicts downgrade to inconclusive; every
434
+ expected item gets a verdict (missing ones are inconclusive)."""
435
+ verdicts: list[CheckVerdict] = []
436
+ for v in report.verdicts:
437
+ if v.status == "failed" and not v.evidence.strip():
438
+ verdicts.append(
439
+ CheckVerdict(
440
+ item_text=v.item_text,
441
+ kind=v.kind,
442
+ status="inconclusive",
443
+ evidence="failed verdict lacked concrete evidence; downgraded",
444
+ suggestion=v.suggestion,
445
+ )
446
+ )
447
+ else:
448
+ verdicts.append(v)
449
+
450
+ covered = {(v.kind, v.item_text) for v in verdicts}
451
+ for ci in check_items:
452
+ if (ci.kind, ci.text) not in covered:
453
+ verdicts.append(
454
+ CheckVerdict(
455
+ item_text=ci.text,
456
+ kind=ci.kind,
457
+ status="inconclusive",
458
+ evidence="no verdict produced for this item",
459
+ )
460
+ )
461
+ return CheckReport(verdicts=verdicts, unmet_subgoals=list(report.unmet_subgoals))
462
+
463
+
464
+ async def _run_check_loop(
465
+ ctx: ArtemisContext,
466
+ messages: list,
467
+ tools: list[BaseTool],
468
+ check_items: list,
469
+ ) -> CheckReport:
470
+ llm = get_llm(ctx=ctx, name="checker")
471
+ traced_tools = [trace_langchain_tool(t, ctx) for t in tools]
472
+
473
+ max_iterations = (
474
+ getattr(ctx.execution_setup, "checker_max_iterations", CHECKER_MAX_ITERATIONS)
475
+ if ctx.execution_setup
476
+ else CHECKER_MAX_ITERATIONS
477
+ )
478
+
479
+ report: CheckReport | None = None
480
+ for i in range(max_iterations):
481
+ if i == max_iterations - 1:
482
+ messages.append(
483
+ HumanMessage(
484
+ content=(
485
+ "This is your final iteration; provide your structured verdict report now."
486
+ )
487
+ )
488
+ )
489
+ report = await _structured_report(llm, messages)
490
+ break
491
+
492
+ active_llm = llm.bind_tools(tools=traced_tools)
493
+ response = await invoke_llm_with_timeout_message(acomplete(active_llm, messages))
494
+
495
+ if not response.tool_calls:
496
+ report = await _structured_report(llm, messages + [response])
497
+ break
498
+
499
+ messages.append(response)
500
+ for tc in response.tool_calls:
501
+ tool_name = tc["name"].split(":")[-1] if ":" in tc["name"] else tc["name"]
502
+ args = dict(tc["args"])
503
+ tool_to_run = next((t for t in traced_tools if t.name == tool_name), None)
504
+ if tool_to_run is None:
505
+ messages.append(
506
+ ToolMessage(
507
+ tool_call_id=tc["id"],
508
+ content=f"Error: Tool {tool_name} not supported",
509
+ status="error",
510
+ )
511
+ )
512
+ continue
513
+ try:
514
+ result_obj = await invoke_tool_with_injection(
515
+ tool=tool_to_run, args=args, tool_call_id=tc["id"]
516
+ )
517
+ text, _ = split_multimodal_result(result_obj)
518
+ status = "error" if is_tool_failure(result_obj) else "success"
519
+ except Exception as e:
520
+ result_obj = f"Error running tool {tool_name}: {e}"
521
+ status = "error"
522
+ # Screenshots (get_step_screenshot) enter the conversation in the
523
+ # carrier the model's provider accepts; text results stay a plain
524
+ # ToolMessage.
525
+ messages.extend(
526
+ tool_result_messages(tc["id"], result_obj, name=tool_name, status=status, llm=llm)
527
+ )
528
+
529
+ if report is None:
530
+ report = CheckReport(verdicts=[])
531
+ return _normalize_report(report, check_items)
532
+
533
+
534
+ # --- Streamed-reasoning transcript -----------------------------------------------------
535
+
536
+
537
+ class CheckerStreamCapture:
538
+ """Collects the Checker's own streamed LLM output for replay.
539
+
540
+ The LLM gateway forwards every provider delta to the DataEngine bus as an
541
+ ``llm_stream`` event carrying the emitting agent's trace id
542
+ (``parent_trace_id``). Inside the Checker's traced scope that id is the
543
+ Checker's own, so filtering the bus by it yields exactly the text the live
544
+ UI rendered for this attempt — one segment per (LLM turn, stream kind),
545
+ stamped with the time its first chunk arrived. A mid-stream reset
546
+ (``llm_stream_reset`` / ``discard``) drops that turn's partial text, the
547
+ same way the live UI does.
548
+ """
549
+
550
+ def __init__(self, ctx: ArtemisContext):
551
+ self._engine = getattr(ctx, "data_engine", None)
552
+ try:
553
+ trace_id = CURRENT_TRACE_ID.get()
554
+ except LookupError:
555
+ trace_id = None
556
+ self.trace_id: str | None = str(trace_id) if trace_id else None
557
+ self._segments: dict[tuple[str, str], dict] = {}
558
+ self._subscribed = False
559
+
560
+ def __enter__(self) -> CheckerStreamCapture:
561
+ subscribe = getattr(self._engine, "subscribe", None)
562
+ if self.trace_id and callable(subscribe):
563
+ try:
564
+ subscribe(self._on_event)
565
+ self._subscribed = True
566
+ except Exception as e:
567
+ logger.debug(f"Checker stream capture not attached: {e}")
568
+ return self
569
+
570
+ def __exit__(self, *exc) -> None:
571
+ if not self._subscribed:
572
+ return
573
+ self._subscribed = False
574
+ try:
575
+ self._engine.unsubscribe(self._on_event)
576
+ except Exception as e:
577
+ logger.debug(f"Checker stream capture not detached: {e}")
578
+
579
+ def _on_event(self, event_type: str, data: Any) -> None:
580
+ if not isinstance(data, dict):
581
+ return
582
+ if event_type == "llm_stream":
583
+ if str(data.get("parent_trace_id") or "") != self.trace_id:
584
+ return
585
+ exec_id = str(data.get("execution_id") or "")
586
+ chunk = data.get("chunk")
587
+ if not exec_id or not isinstance(chunk, str) or not chunk:
588
+ return
589
+ role = "thought" if data.get("stream_type") == "thinking" else "answer"
590
+ segment = self._segments.get((exec_id, role))
591
+ if segment is None:
592
+ self._segments[(exec_id, role)] = {
593
+ "execution_id": exec_id,
594
+ "role": role,
595
+ "when": time.time(),
596
+ "text": chunk,
597
+ }
598
+ else:
599
+ segment["text"] += chunk
600
+ elif event_type == "llm_stream_reset" and data.get("action") == "discard":
601
+ exec_id = str(data.get("stream_exec_id") or data.get("stream_execution_id") or "")
602
+ if exec_id:
603
+ for key in [k for k in self._segments if k[0] == exec_id]:
604
+ del self._segments[key]
605
+
606
+ @property
607
+ def segments(self) -> list[dict]:
608
+ """Segments in the order their first chunk arrived."""
609
+ return [dict(s) for s in sorted(self._segments.values(), key=lambda s: s["when"])]
610
+
611
+
612
+ def _persist_stream(
613
+ ctx: ArtemisContext,
614
+ capture: CheckerStreamCapture,
615
+ *,
616
+ attempt_id: str | None,
617
+ phase: str,
618
+ checkpoint_id: str,
619
+ ) -> None:
620
+ """Writes the attempt's transcript next to the ledger (best-effort).
621
+
622
+ Runs when the attempt ends for any reason — verdict, exception, timeout or
623
+ supersession — so a cancelled attempt still leaves what it said. A
624
+ transcript is observability, not execution state: writing it never
625
+ influences booking or release (the ledger stays the source of truth).
626
+ """
627
+ segments = capture.segments
628
+ if not attempt_id:
629
+ return
630
+ thoughts = sum(1 for s in segments if s["role"] == "thought")
631
+ chars = sum(len(s["text"]) for s in segments)
632
+ # Log empty turns too, so missing transcripts can be checked in the session log.
633
+ logger.info(
634
+ f"Checker transcript for {attempt_id}: {len(segments)} segment(s)"
635
+ f" ({thoughts} thought, {len(segments) - thoughts} answer), {chars} chars"
636
+ )
637
+ if not segments:
638
+ return
639
+ try:
640
+ from artemis.graph.checkpoints import append_attempt_stream_record
641
+
642
+ append_attempt_stream_record(
643
+ ctx.data_engine.base_dir,
644
+ {
645
+ "attempt_id": attempt_id,
646
+ "checkpoint_id": checkpoint_id,
647
+ "phase": phase,
648
+ "trace_id": capture.trace_id,
649
+ "segments": segments,
650
+ },
651
+ )
652
+ except Exception as e:
653
+ logger.debug(f"Checker stream transcript for {attempt_id} not persisted: {e}")
654
+
655
+
656
+ # --- Entry points --------------------------------------------------------------------
657
+
658
+
659
+ def _announce(ctx: ArtemisContext, **kwargs) -> None:
660
+ """Lazy bridge to the checkpoint bus (avoids an import cycle at module load)."""
661
+ try:
662
+ from artemis.graph.checkpoints import announce_checker_attempt
663
+
664
+ announce_checker_attempt(ctx, **kwargs)
665
+ except Exception as e:
666
+ logger.debug(f"Checker attempt announcement skipped: {e}")
667
+
668
+
669
+ @trace(type="agent", name="checker")
670
+ async def run_checkpoint_check(
671
+ ctx: ArtemisContext,
672
+ check_items,
673
+ anchor,
674
+ goal: str,
675
+ subgoal_text: str,
676
+ attempt_id: str | None = None,
677
+ ) -> CheckReport:
678
+ """Audits one completed subgoal's on_complete check items.
679
+
680
+ Evidence discipline is enforced by the tool table: no live-screen access —
681
+ the anchored history (anchor step's pre screenshot, previous step's post
682
+ state) and persistent-state probes are the only valid sources.
683
+ """
684
+ items = list(check_items)
685
+ checkpoint_id = attempt_id.split("#", 1)[0] if attempt_id else ""
686
+ logger.info(f"Starting checkpoint check for '{subgoal_text}' ({len(items)} item(s))")
687
+ _announce(
688
+ ctx,
689
+ attempt_id=attempt_id,
690
+ phase="checkpoint",
691
+ checkpoint_id=checkpoint_id,
692
+ subgoal_text=subgoal_text,
693
+ anchor_step_id=getattr(anchor, "anchor_step_id", None),
694
+ check_items=items,
695
+ )
696
+ prompts = _load_prompts()
697
+ tools = build_checker_tools(ctx, "checkpoint")
698
+ probe_registered = any(t.name == "probe_device" for t in tools)
699
+ system_content = "\n\n".join(
700
+ s
701
+ for s in (
702
+ prompts.get("system", "You are the Checker."),
703
+ assemble_checker_prompt_segments("checkpoint", items, probe_registered, prompts),
704
+ )
705
+ if s
706
+ )
707
+
708
+ anchor_step_no = None
709
+ if ctx.data_engine and anchor.anchor_step_id:
710
+ try:
711
+ anchor_step_no = ctx.data_engine.get_step_number(anchor.anchor_step_id)
712
+ except Exception:
713
+ anchor_step_no = None
714
+
715
+ human_text = (
716
+ f"# User's Original Goal\n{goal}\n\n"
717
+ f"# Audited Subgoal (just marked complete)\n{subgoal_text}\n\n"
718
+ f"# Check Items\n{_format_check_items(items)}\n\n"
719
+ f"# Evidence Anchor\nThe subgoal was marked complete at step"
720
+ f" {anchor_step_no if anchor_step_no is not None else 'unknown'}."
721
+ " The anchor step's pre-action screenshot and the preceding step's"
722
+ " post-action state capture the completion moment.\n\n"
723
+ f"# Task Plan at Completion Time\n{anchor.plan_text}\n\n"
724
+ f"# Execution History (summaries)\n{_format_history(ctx)}"
725
+ )
726
+
727
+ messages = [
728
+ SystemMessage(content=system_content),
729
+ HumanMessage(content=human_text),
730
+ ]
731
+ with CheckerStreamCapture(ctx) as capture:
732
+ try:
733
+ return await _run_check_loop(ctx, messages, tools, items)
734
+ finally:
735
+ _persist_stream(
736
+ ctx,
737
+ capture,
738
+ attempt_id=attempt_id,
739
+ phase="checkpoint",
740
+ checkpoint_id=checkpoint_id,
741
+ )
742
+
743
+
744
+ async def _capture_final_screen(ctx: ArtemisContext) -> tuple[str | None, str]:
745
+ """Best-effort capture of the final live screen (final entry only)."""
746
+ try:
747
+ from artemis.controllers.unified_controller import UnifiedMobileController
748
+
749
+ controller = UnifiedMobileController(ctx)
750
+ device_data = await controller.get_screen_data()
751
+ screenshot_b64 = device_data.base64
752
+ xml_hierarchy = device_data.elements
753
+ width = device_data.width
754
+ height = device_data.height
755
+
756
+ ocr_results = []
757
+ if is_ocr_configured():
758
+ try:
759
+ status_bar_height = _detect_status_bar_height(xml_hierarchy, height)
760
+ if status_bar_height > 0:
761
+ cropped_b64, _, _ = _crop_image_remove_status_bar(
762
+ screenshot_b64, status_bar_height
763
+ )
764
+ raw_ocr = await perform_ocr(cropped_b64)
765
+ ocr_results = _map_coordinates_back(raw_ocr, status_bar_height)
766
+ else:
767
+ ocr_results = await perform_ocr(screenshot_b64)
768
+ except Exception as e:
769
+ logger.warning(f"Final check OCR failed, proceeding without OCR: {e}")
770
+
771
+ fused_xml = fuse_ocr_with_xml(xml_hierarchy, ocr_results)
772
+ minimal_list, _, _ = format_minimal_list_with_elements(fused_xml, width, height)
773
+ return screenshot_b64, minimal_list
774
+ except Exception as e:
775
+ logger.warning(f"Failed to capture final screen state: {e}")
776
+ return None, "Final screen state unavailable."
777
+
778
+
779
+ def _format_ledger(ledger: list[dict]) -> str:
780
+ if not ledger:
781
+ return "(no checkpoint verdicts were recorded)"
782
+ lines = []
783
+ for r in ledger:
784
+ lines.append(
785
+ f"- [{r.get('kind')}] '{r.get('item_text')}' -> {r.get('status')}"
786
+ f" (attempt {r.get('attempt_id')}): {r.get('evidence', '')}"
787
+ )
788
+ return "\n".join(lines)
789
+
790
+
791
+ @trace(type="agent", name="checker")
792
+ async def run_final_check(
793
+ ctx: ArtemisContext,
794
+ goal: str,
795
+ plan_text: str,
796
+ ledger: list[dict],
797
+ check_items,
798
+ attempt_id: str | None = None,
799
+ ) -> CheckReport:
800
+ """Final review at task exit: audits the user's original goal and all
801
+ declared check items, with the final screen state available."""
802
+ items = list(check_items)
803
+ logger.info(f"Starting final check ({len(items)} declared item(s))")
804
+ _announce(
805
+ ctx,
806
+ attempt_id=attempt_id,
807
+ phase="final",
808
+ checkpoint_id="final",
809
+ subgoal_text="Final review against the user's original goal",
810
+ anchor_step_id=None,
811
+ check_items=items,
812
+ )
813
+ prompts = _load_prompts()
814
+ tools = build_checker_tools(ctx, "final")
815
+ probe_registered = any(t.name == "probe_device" for t in tools)
816
+ system_content = "\n\n".join(
817
+ s
818
+ for s in (
819
+ prompts.get("system", "You are the Checker."),
820
+ assemble_checker_prompt_segments("final", items, probe_registered, prompts),
821
+ )
822
+ if s
823
+ )
824
+
825
+ screenshot_b64, minimal_list = await _capture_final_screen(ctx)
826
+ steps = _load_steps(ctx)
827
+
828
+ human_text = (
829
+ f"# User's Original Goal\n{goal}\n\n"
830
+ f"# User guidance received during the run\n{_format_user_guidance(ctx, steps)}\n\n"
831
+ f"# Final Task Plan\n{plan_text or '(no plan)'}\n\n"
832
+ f"# Declared Check Items\n{_format_check_items(items)}\n\n"
833
+ f"# Verdict Ledger (checkpoint results, authoritative for on_complete"
834
+ f" items)\n{_format_ledger(ledger)}\n\n"
835
+ f"# Execution History (summaries)\n{_format_history(ctx, steps=steps)}\n\n"
836
+ f"# Final Screen Elements\n{minimal_list}"
837
+ )
838
+
839
+ content: list = [{"type": "text", "text": human_text}]
840
+ if screenshot_b64:
841
+ content.append({"type": "text", "text": "--- Final Screenshot ---"})
842
+ content.append(
843
+ {
844
+ "type": "image_url",
845
+ "image_url": {"url": f"data:image/jpeg;base64,{screenshot_b64}"},
846
+ }
847
+ )
848
+
849
+ messages = [
850
+ SystemMessage(content=system_content),
851
+ HumanMessage(content=content),
852
+ ]
853
+ with CheckerStreamCapture(ctx) as capture:
854
+ try:
855
+ return await _run_check_loop(ctx, messages, tools, items)
856
+ finally:
857
+ _persist_stream(
858
+ ctx, capture, attempt_id=attempt_id, phase="final", checkpoint_id="final"
859
+ )