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,1055 @@
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 asyncio
16
+ import base64
17
+ import functools
18
+ import json
19
+ from collections import Counter
20
+ from typing import Annotated, Literal
21
+
22
+ from langchain_core.messages import ToolMessage
23
+ from langchain_core.tools import StructuredTool
24
+ from langchain_core.tools.base import InjectedToolCallId
25
+ from langgraph.constants import END, START
26
+ from langgraph.graph import StateGraph
27
+ from langgraph.graph.state import CompiledStateGraph
28
+ from langgraph.prebuilt import InjectedState
29
+ from pydantic import BaseModel, Field
30
+
31
+ from artemis.agents.checker.checker import run_final_check, verdicts_allow_release
32
+ from artemis.agents.operator.operator import OperatorNode
33
+ from artemis.agents.planner.planner import (
34
+ PlannerNode,
35
+ run_async_planner_validation,
36
+ )
37
+ from artemis.agents.summarizer.summarizer import SummarizerNode
38
+ from artemis.agents.validator.validator import ValidatorNode
39
+ from artemis.context import ArtemisContext
40
+ from artemis.graph.checkpoints import (
41
+ append_ledger_record,
42
+ attempt_trace_id,
43
+ compute_run_outcome,
44
+ final_check_enabled,
45
+ FINAL_CHECKPOINT_ID,
46
+ FINAL_SUBGOAL_TEXT,
47
+ harvest_finished_checkpoints,
48
+ has_ledger_records,
49
+ is_checker_note_key,
50
+ publish_checker_event,
51
+ queue_checkpoints,
52
+ read_ledger,
53
+ reinstate_check_items,
54
+ retire_check_items,
55
+ revert_subgoal_status,
56
+ settle_all_checkpoints,
57
+ spawn_pending_checkpoints,
58
+ sync_finding_lines,
59
+ verdict_payloads,
60
+ write_run_outcome,
61
+ )
62
+ from artemis.graph.perception import perception_node
63
+ from artemis.graph.state import State
64
+ from artemis.graph.visibility import strict_state
65
+ from artemis.tools.command_tool import (
66
+ HANG_GUIDANCE,
67
+ manage_task_wrapper,
68
+ run_adb_command_wrapper,
69
+ )
70
+ from artemis.tools.diagnostic_tool import ask_diagnoser_wrapper
71
+ from artemis.tools.explorer_tool import ask_explorer_wrapper
72
+ from artemis.tools.index import get_tools_from_wrappers
73
+ from artemis.tools.scratchpad import (
74
+ append_note_wrapper,
75
+ list_notes_wrapper,
76
+ read_note_wrapper,
77
+ save_note_wrapper,
78
+ update_note_wrapper,
79
+ )
80
+ from artemis.tools.tool_wrapper import invoke_tool_with_injection
81
+ from artemis.tools.video_tool import get_video_analyzer_tool
82
+ from artemis.utils.notes import (
83
+ SAVE_NOTE_ARG_CONTENT_DESC,
84
+ SAVE_NOTE_ARG_KEY_DESC,
85
+ UPDATE_NOTE_ARG_KEY_DESC,
86
+ UPDATE_NOTE_ARG_REPLACEMENT_DESC,
87
+ UPDATE_NOTE_ARG_TARGET_DESC,
88
+ get_note_file_path,
89
+ )
90
+ from artemis.utils.plan_grammar import (
91
+ milestones_changed,
92
+ missing_check_items,
93
+ new_top_level_completions,
94
+ parse_plan,
95
+ render_check_line,
96
+ restore_missing_check_items,
97
+ subgoal_hash,
98
+ unintended_milestone_edits,
99
+ )
100
+ from artemis.utils.task_tree import get_active_subgoal_hashes
101
+ from artemis.utils.cython_compat import CyFunctionDetector
102
+ from third_party.mobile_use.graph import (
103
+ CONVERGENCE_NODE,
104
+ add_convergence_edges,
105
+ add_convergence_node,
106
+ )
107
+ from third_party.mobile_use.utils.logger import get_logger
108
+
109
+ logger = get_logger(__name__)
110
+
111
+
112
+ def _notify_history_chunker(ctx: ArtemisContext, method: str, *args) -> None:
113
+ """Best-effort event feed to the transcript's L2/L3 chunk manager (M3).
114
+
115
+ The chunker only exists when the transcript flag is on; every failure is
116
+ swallowed — history compression must never gate the execution path.
117
+ """
118
+ try:
119
+ chunker = getattr(getattr(ctx, "transcript_ledger", None), "chunker", None)
120
+ if chunker is not None:
121
+ getattr(chunker, method)(*args)
122
+ except Exception as e:
123
+ logger.debug(f"History chunker notification '{method}' skipped: {e}")
124
+
125
+
126
+ async def execution_check_node(state: State, ctx: ArtemisContext):
127
+ state = strict_state(state, "execution_check")
128
+ logger.info("Starting execution_check_node")
129
+
130
+ if not ctx.data_engine:
131
+ return {"checker_success": True}
132
+
133
+ checker_success = True
134
+
135
+ def _record_turn(extra_metadata: dict) -> str | None:
136
+ """Records this Operator turn in the DataEngine — unconditionally: every
137
+ turn (including planner-rejected ones) leaves a step record."""
138
+ raw_data = state.operator_raw_data
139
+ if not raw_data:
140
+ return None
141
+ try:
142
+ screenshot_b64 = raw_data.get("screenshot_b64")
143
+ xml_hierarchy = raw_data.get("xml_hierarchy")
144
+ ocr_results = raw_data.get("ocr_results")
145
+
146
+ screenshot_bytes = base64.b64decode(screenshot_b64)
147
+
148
+ action_taken = None
149
+ if state.structured_decisions:
150
+ try:
151
+ action_taken = json.loads(state.structured_decisions)
152
+ except Exception as e:
153
+ logger.warning(f"Failed to parse structured decisions: {e}")
154
+
155
+ subgoal_hash_val, sub_subgoal_hash = _get_active_subgoal_hashes(ctx)
156
+ # The user's injected instruction is stamped verbatim on the step
157
+ # it reached: the chunk ledger (band ③) preserves it as a
158
+ # never-evicted line at every compression level (§3.3).
159
+ injected = getattr(state, "injected_instruction", None)
160
+ step_id = ctx.data_engine.record_step(
161
+ pre_screenshot_bytes=screenshot_bytes,
162
+ ui_tree=xml_hierarchy,
163
+ ocr_result=ocr_results,
164
+ action_taken=action_taken,
165
+ operator_raw_thinking=getattr(state, "operator_raw_thinking", None),
166
+ operator_native_thinking=getattr(state, "operator_native_thinking", None),
167
+ extra_metadata={
168
+ "subgoal_hash": subgoal_hash_val,
169
+ "sub_subgoal_hash": sub_subgoal_hash,
170
+ "width": raw_data.get("width"),
171
+ "height": raw_data.get("height"),
172
+ **({"injected_instruction": injected} if injected else {}),
173
+ **extra_metadata,
174
+ },
175
+ )
176
+ logger.info(f"Recorded step in DataEngine: {step_id}")
177
+ # Milestone-switch fact source: the stamped subgoal hash of every
178
+ # executed step, compared consecutively inside the chunk manager.
179
+ _notify_history_chunker(ctx, "on_step_stamped", str(step_id), subgoal_hash_val)
180
+ return str(step_id)
181
+ except Exception as e:
182
+ logger.error(f"Failed to record step in DataEngine: {e}")
183
+ return None
184
+
185
+ # Harvest finished checkpoint attempts (non-blocking: done() tasks only).
186
+ check_findings = harvest_finished_checkpoints(ctx, state)
187
+ turn_metadata: dict = {}
188
+
189
+ if hasattr(ctx, "planner_task") and ctx.planner_task:
190
+ logger.info("Waiting for planner validation task to complete...")
191
+ awaited_planner_task = ctx.planner_task
192
+ try:
193
+ planner_result = await awaited_planner_task
194
+ ctx.planner_task = None
195
+
196
+ flagged = bool(planner_result and planner_result.get("status") == "failed")
197
+ if flagged:
198
+ # Advisory outcome: the plan change stays applied and the
199
+ # turn's actions run. The Operator only receives the concern
200
+ # and its reason through operator_feedback — the one feedback
201
+ # channel its next prompt renders.
202
+ logger.warning("Planner validation flagged the task plan changes.")
203
+ feedback = planner_result.get(
204
+ "feedback", "The reviewer had a concern about this plan change."
205
+ )
206
+ check_findings.append(
207
+ "[planner] Advisory review of your recent top-level plan"
208
+ f" change: {feedback} The change was NOT rolled back —"
209
+ " weigh this concern against your own observations and"
210
+ " correct the plan only if you agree."
211
+ )
212
+ turn_metadata["planner_flagged"] = True
213
+ else:
214
+ logger.info("Planner validation approved the task plan changes.")
215
+
216
+ # Advance the ratchet baseline either way: the reviewed content is
217
+ # the new reference, so an already-flagged change is not re-flagged
218
+ # on every subsequent write.
219
+ if ctx.pending_validated_plan is not None:
220
+ ctx.last_validated_plan = ctx.pending_validated_plan
221
+ ctx.pending_validated_plan = None
222
+ except asyncio.CancelledError:
223
+ if not awaited_planner_task.cancelled():
224
+ raise # this node itself is being cancelled, not the awaited task
225
+ logger.info("Planner validation task was superseded by a newer plan write.")
226
+ ctx.planner_task = None
227
+ except Exception as e:
228
+ logger.error(f"Planner validation task failed: {e}")
229
+
230
+ # Every Operator turn is recorded — verification never gates history.
231
+ current_step_id = _record_turn(turn_metadata)
232
+
233
+ # Spawn queued checkpoints only now: the just-recorded step is the evidence
234
+ # anchor (its pre screenshot plus the previous turn's post state capture
235
+ # the completion moment). Superseded-but-finished attempts are booked here.
236
+ check_findings.extend(await spawn_pending_checkpoints(ctx, state, current_step_id))
237
+
238
+ update = {
239
+ "checker_success": checker_success,
240
+ "current_step_id": current_step_id,
241
+ "operator_feedback": check_findings or None,
242
+ }
243
+ if checker_success:
244
+ update["subagent_calls"] = []
245
+ return update
246
+
247
+
248
+ async def exit_settlement_node(state: State, ctx: ArtemisContext):
249
+ """Two-phase exit: settlement (unconditional) then final review (gated).
250
+
251
+ Phase 1 — settlement barrier: the only intentional blocking wait in the
252
+ whole flow. Every started check attempt gets booked into the ledger before
253
+ exit, no matter which switches are on.
254
+
255
+ Phase 2 — final review (``disable_final_check`` off): audits the USER'S
256
+ ORIGINAL GOAL plus all declared check items. Unmet verify criteria route
257
+ back into the loop (bounded by ``final_check_max_attempts``); assert
258
+ failures never do.
259
+ """
260
+ state = strict_state(state, "exit_settlement")
261
+ logger.info("Starting exit_settlement_node")
262
+ update: dict = {"exit_settlement_route": "end"}
263
+ if not ctx.data_engine:
264
+ return update
265
+
266
+ # Phase 1: settle every outstanding attempt (bounded by settlement_timeout).
267
+ await settle_all_checkpoints(ctx, state)
268
+ # Queued-but-never-spawned checkpoints are dropped here: their items simply
269
+ # remain unchecked and are reported as such.
270
+ ctx.pending_checkpoints.clear()
271
+
272
+ base_dir = ctx.data_engine.base_dir
273
+ plan_text = ""
274
+ plan_path = get_note_file_path(base_dir, "task_plan")
275
+ if plan_path.exists():
276
+ try:
277
+ plan_text = plan_path.read_text(encoding="utf-8")
278
+ except Exception as e:
279
+ logger.error(f"Failed to read task plan at settlement: {e}")
280
+ snapshot = parse_plan(plan_text)
281
+
282
+ setup = ctx.execution_setup
283
+ user_stopped = bool(getattr(state, "user_stop_requested", False))
284
+ halted = bool(getattr(ctx, "assert_halt", False))
285
+ max_attempts = int(getattr(setup, "final_check_max_attempts", 3) or 3) if setup else 3
286
+
287
+ verify_blocked = False
288
+ blocked_findings: list[str] = []
289
+ if final_check_enabled(ctx):
290
+ attempt_no = ctx.final_check_attempts + 1
291
+ ctx.final_check_attempts = attempt_no
292
+ final_attempt_id = f"final#{attempt_no}"
293
+ checkpoint_timeout = float(getattr(setup, "checkpoint_timeout", 180.0) or 180.0)
294
+ ledger = read_ledger(base_dir)
295
+
296
+ def _final_event(status: str, verdicts: list[dict], **extra) -> None:
297
+ publish_checker_event(
298
+ ctx,
299
+ {
300
+ "event": "attempt_finished",
301
+ "phase": "final",
302
+ "attempt_id": final_attempt_id,
303
+ "checkpoint_id": FINAL_CHECKPOINT_ID,
304
+ "subgoal_text": FINAL_SUBGOAL_TEXT,
305
+ "trace_id": attempt_trace_id(ctx, final_attempt_id),
306
+ "status": status,
307
+ "verdicts": verdicts,
308
+ **extra,
309
+ },
310
+ )
311
+
312
+ try:
313
+ report = await asyncio.wait_for(
314
+ run_final_check(
315
+ ctx,
316
+ goal=getattr(state, "initial_goal", ""),
317
+ plan_text=plan_text,
318
+ ledger=ledger,
319
+ check_items=list(snapshot.all_check_items),
320
+ attempt_id=final_attempt_id,
321
+ ),
322
+ timeout=checkpoint_timeout,
323
+ )
324
+ except Exception as e:
325
+ # Fail-open: release, but the verdict value stays inconclusive.
326
+ logger.warning(f"Final check errored ({e}); releasing fail-open.")
327
+ error_verdicts: list[dict] = []
328
+ for ci in snapshot.all_check_items:
329
+ append_ledger_record(
330
+ base_dir,
331
+ {
332
+ "attempt_id": final_attempt_id,
333
+ "checkpoint_id": FINAL_CHECKPOINT_ID,
334
+ "subgoal_text": FINAL_SUBGOAL_TEXT,
335
+ "trace_id": attempt_trace_id(ctx, final_attempt_id),
336
+ "item_text": ci.text,
337
+ "kind": ci.kind,
338
+ "when": ci.when,
339
+ "status": "inconclusive",
340
+ "evidence": f"final check error: {e}",
341
+ "anchor_step_id": None,
342
+ },
343
+ )
344
+ error_verdicts.append(
345
+ {
346
+ "item_text": ci.text,
347
+ "kind": ci.kind,
348
+ "status": "inconclusive",
349
+ "evidence": f"final check error: {e}",
350
+ "suggestion": "",
351
+ }
352
+ )
353
+ _final_event("error", error_verdicts, error=str(e), route="end")
354
+ report = None
355
+
356
+ if report is not None:
357
+ for v in report.verdicts:
358
+ when = next(
359
+ (
360
+ ci.when
361
+ for ci in snapshot.all_check_items
362
+ if ci.kind == v.kind and ci.text == v.item_text
363
+ ),
364
+ "at_end",
365
+ )
366
+ append_ledger_record(
367
+ base_dir,
368
+ {
369
+ "attempt_id": final_attempt_id,
370
+ "checkpoint_id": FINAL_CHECKPOINT_ID,
371
+ "subgoal_text": FINAL_SUBGOAL_TEXT,
372
+ "trace_id": attempt_trace_id(ctx, final_attempt_id),
373
+ "item_text": v.item_text,
374
+ "kind": v.kind,
375
+ "when": when,
376
+ "status": v.status,
377
+ "evidence": v.evidence,
378
+ "suggestion": v.suggestion,
379
+ "anchor_step_id": None,
380
+ },
381
+ )
382
+
383
+ passed = verdicts_allow_release(report) and not report.unmet_subgoals
384
+ if not passed:
385
+ if user_stopped or halted or attempt_no >= max_attempts:
386
+ verify_blocked = True
387
+ for v in report.verdicts:
388
+ if v.kind == "verify" and v.status == "failed":
389
+ blocked_findings.append(
390
+ f"[verify failed] '{v.item_text}': {v.evidence}"
391
+ )
392
+ for text in report.unmet_subgoals:
393
+ blocked_findings.append(f"[unmet subgoal] '{text}'")
394
+ logger.warning(
395
+ "Final check found unmet verify criteria but the retry"
396
+ " budget is exhausted (or a stop/halt is latched);"
397
+ " ending with a blocked outcome."
398
+ )
399
+ _final_event(
400
+ "done",
401
+ verdict_payloads(report.verdicts),
402
+ unmet_subgoals=list(report.unmet_subgoals),
403
+ findings=list(blocked_findings),
404
+ route="blocked",
405
+ )
406
+ else:
407
+ findings: list[str] = []
408
+ for text in report.unmet_subgoals:
409
+ if revert_subgoal_status(ctx, subgoal_hash(text)):
410
+ findings.append(
411
+ f"[final check] Subgoal '{text}' was set back to"
412
+ " in-progress: its acceptance criteria are not met."
413
+ )
414
+ for v in report.verdicts:
415
+ if v.kind == "verify" and v.status == "failed":
416
+ suggestion = f" Suggestion: {v.suggestion}" if v.suggestion else ""
417
+ findings.append(
418
+ f"[verify failed] '{v.item_text}': {v.evidence}{suggestion}"
419
+ )
420
+ update["operator_feedback"] = findings or None
421
+ update["exit_settlement_route"] = "continue"
422
+ _final_event(
423
+ "done",
424
+ verdict_payloads(report.verdicts),
425
+ unmet_subgoals=list(report.unmet_subgoals),
426
+ findings=list(findings),
427
+ route="continue",
428
+ )
429
+ return update
430
+ else:
431
+ _final_event(
432
+ "done",
433
+ verdict_payloads(report.verdicts),
434
+ unmet_subgoals=[],
435
+ findings=[],
436
+ route="end",
437
+ )
438
+
439
+ # END path: assemble the machine-readable run outcome.
440
+ records = read_ledger(base_dir)
441
+ retired = getattr(ctx, "guidance_retired_checks", None)
442
+ outcome = compute_run_outcome(
443
+ snapshot,
444
+ records,
445
+ verify_blocked=verify_blocked,
446
+ retired=retired if isinstance(retired, set) else None,
447
+ )
448
+ if snapshot.all_check_items or records or verify_blocked:
449
+ update["run_outcome"] = outcome.model_dump()
450
+ # BLOCKED/partial wrap-ups carry the last findings in the metadata file.
451
+ extra: dict = {"last_findings": blocked_findings} if blocked_findings else {}
452
+ # Which UI-hierarchy source served the run, so a report reader can tell a
453
+ # helper run from a UIAutomator2 fallback without reading logs.
454
+ from artemis.clients.screen_client_factory import hierarchy_backend_summary
455
+
456
+ environment = hierarchy_backend_summary(getattr(ctx, "ui_adb_client", None))
457
+ if environment:
458
+ extra["environment"] = environment
459
+ write_run_outcome(base_dir, outcome, extra or None)
460
+ publish_checker_event(
461
+ ctx,
462
+ {
463
+ "event": "run_outcome",
464
+ "phase": "outcome",
465
+ **outcome.model_dump(),
466
+ "last_findings": list(blocked_findings),
467
+ },
468
+ )
469
+ logger.info(
470
+ f"Run outcome: task_status={outcome.task_status},"
471
+ f" tests(passed={outcome.tests.passed}, failed={outcome.tests.failed},"
472
+ f" inconclusive={outcome.tests.inconclusive},"
473
+ f" unchecked={outcome.tests.unchecked},"
474
+ f" retired={outcome.tests.retired})"
475
+ )
476
+ return update
477
+
478
+
479
+ def exit_settlement_gate(state: State) -> Literal["continue", "end"]:
480
+ route = getattr(state, "exit_settlement_route", None)
481
+ return "continue" if route == "continue" else "end"
482
+
483
+
484
+ def execution_check_edge(
485
+ state: State,
486
+ ) -> Literal["execute_decisions", "review_subgoals"]:
487
+ if state.checker_success:
488
+ if state.structured_decisions:
489
+ return "execute_decisions"
490
+ return "review_subgoals"
491
+ return "review_subgoals"
492
+
493
+
494
+ def _get_active_subgoal_hashes(ctx: ArtemisContext) -> tuple[str, str | None]:
495
+ """Parses task_plan.md to find the active subgoal and sub-subgoal hashes."""
496
+
497
+ if not ctx.data_engine:
498
+ return "default", None
499
+
500
+ task_plan_path = get_note_file_path(ctx.data_engine.base_dir, "task_plan")
501
+
502
+ if not task_plan_path.exists():
503
+ return "default", None
504
+
505
+ try:
506
+ content = task_plan_path.read_text(encoding="utf-8")
507
+
508
+ return get_active_subgoal_hashes(content)
509
+ except Exception as e:
510
+ logger.error(f"Failed to parse active subgoal: {e}")
511
+
512
+ return "default", None
513
+
514
+
515
+ def check_plan_mutation_rejections(
516
+ content_before: str, content_after: str, state: State | None = None
517
+ ) -> str | None:
518
+ """Checks if a task plan modification violates the machine-channel safety rules."""
519
+ before = parse_plan(content_before)
520
+ after = parse_plan(content_after)
521
+
522
+ # 1. Incomplete nested subgoals check
523
+ if after.all_top_level_done and any(
524
+ (item.is_pending or item.is_active) and not item.is_top_level for item in after.items
525
+ ):
526
+ return (
527
+ "Your changes to the task plan were not applied. Your changes would cause the"
528
+ " task to end completely, but there seem to be goals in the task plan"
529
+ " that have not been marked as completed. If you believe these goals"
530
+ " are already completed, please update their statuses."
531
+ )
532
+
533
+ # 2. Continuous monitoring protection: a [Loop:continuous] milestone can only
534
+ # transition to [x] (or disappear) after the user injects an explicit stop
535
+ # signal (user_stop_requested); intent is never inferred from wording.
536
+ user_stopped = bool(state is not None and getattr(state, "user_stop_requested", False))
537
+
538
+ if before.continuous_top_level and not user_stopped:
539
+ if not after.continuous_top_level:
540
+ return (
541
+ "Your changes to the task plan were rejected. You cannot delete an active"
542
+ " [Loop:continuous] continuous monitoring milestone or remove its tag."
543
+ " Please keep the milestone active (in progress [/]) and record each"
544
+ " check cycle as an indented subtask."
545
+ )
546
+ if any(item.is_done for item in after.continuous_top_level):
547
+ return (
548
+ "Your changes to the task plan were rejected. [Loop:continuous] continuous"
549
+ " monitoring milestones must remain in progress ([/]) and cannot be"
550
+ " unilaterally marked as completed [x] until the user injects an explicit"
551
+ " stop signal. Please keep it active ([/]) and record each polling check"
552
+ " as an indented subtask."
553
+ )
554
+
555
+ return None
556
+
557
+
558
+ def check_unintended_rewrite(content_before: str, content_after: str) -> str | None:
559
+ """Detects the hand-slip signature of a full-file rewrite: top-level milestones
560
+ whose status did not change but whose text drifted. Declared edits via
561
+ update_note (explicit target/replacement) are exempt.
562
+ """
563
+ edits = unintended_milestone_edits(parse_plan(content_before), parse_plan(content_after))
564
+ if not edits:
565
+ return None
566
+
567
+ listing = "\n".join(f'- "{b}"\n -> "{a}"' for b, a in edits)
568
+ return (
569
+ "Your changes to the task plan were not applied. Your rewrite reworded"
570
+ " top-level milestones whose status you did not change — this usually means"
571
+ " the plan was regenerated from memory and historical milestones drifted"
572
+ f" unintentionally:\n{listing}\nIf a rewording was intentional, apply it"
573
+ " explicitly with the update_note tool (target/replacement). Otherwise,"
574
+ " re-apply only the changes you actually intended."
575
+ )
576
+
577
+
578
+ def _reject_checker_note_write(key: str, tool_call_id: str | None) -> ToolMessage | None:
579
+ """Guard for the system-authored checker note channel.
580
+
581
+ Note keys with the reserved ``checker-`` / ``checker:`` prefix are written
582
+ exclusively by the checkpoint verifier (repair logs); model-side writes are
583
+ rejected with guidance instead of silently colliding with the log.
584
+ """
585
+ if not is_checker_note_key(key):
586
+ return None
587
+ return ToolMessage(
588
+ content=(
589
+ f"The note key '{key}' was not written: keys with the 'checker-'"
590
+ " prefix are reserved for system-authored verification repair"
591
+ " logs. Those notes are read-only for you — read them with"
592
+ " read_note. Record your own observations under a different key."
593
+ ),
594
+ tool_call_id=tool_call_id or "",
595
+ status="success",
596
+ )
597
+
598
+
599
+ class NoteArgs(BaseModel):
600
+ model_config = {"ignored_types": (CyFunctionDetector,)}
601
+ key: str = Field(..., description=SAVE_NOTE_ARG_KEY_DESC)
602
+ content: str = Field(..., description=SAVE_NOTE_ARG_CONTENT_DESC)
603
+
604
+
605
+ async def _process_plan_write(
606
+ ctx: ArtemisContext,
607
+ state: State | None,
608
+ task_plan_path,
609
+ content_before: str,
610
+ content_after: str,
611
+ result,
612
+ declared_intent: bool,
613
+ ):
614
+ """Shared post-write pipeline for every task_plan mutation.
615
+
616
+ Constitution order: hard rejections with rollback (machine-channel rules
617
+ only), then ratchet-baseline planner validation, then checker trigger on
618
+ new top-level completions. ``declared_intent`` marks surgical update_note
619
+ edits, which are exempt from the full-rewrite hand-slip check.
620
+ """
621
+ rejection_message = check_plan_mutation_rejections(content_before, content_after, state)
622
+ if rejection_message is None and not declared_intent:
623
+ rejection_message = check_unintended_rewrite(content_before, content_after)
624
+ if rejection_message:
625
+ logger.info(f"Rejected task plan update: {rejection_message}")
626
+ if task_plan_path.exists():
627
+ task_plan_path.write_text(content_before, encoding="utf-8")
628
+
629
+ # Guidance, not a tool failure: status stays "success" so the Operator
630
+ # loop treats it as feedback content (matching the legacy envelope
631
+ # behavior where the SystemMessage carried no error status).
632
+ return ToolMessage(content=rejection_message, tool_call_id="", status="success")
633
+
634
+ # Deterministic check-line guard (Operator-source writes only; the Planner
635
+ # node writes through unwrapped tools and retains revision authority):
636
+ # deleted/rewritten check lines are merged back in a pure text operation.
637
+ # This is content-driven and independent of any switch — a resumed plan
638
+ # with check lines stays protected even when checking is disabled.
639
+ # The one exception is user guidance: it outranks the declared standards,
640
+ # so every check line that existed when an instruction arrived (recorded
641
+ # by perception in ``ctx.guidance_unprotected_checks``) may be dropped or
642
+ # reworded at any later write — the Operator usually re-plans over several
643
+ # surgical edits, turns after the guidance landed. Lines added after the
644
+ # instruction are protected again until the next one.
645
+ #
646
+ # ``missing_check_items`` diffs on the full signature (kind, when, text,
647
+ # parent hash), so rewording a milestone re-signs every check line under
648
+ # it. Such a line has merely moved: it is still declared, so it is neither
649
+ # merged back (that would duplicate it as a task-level ``@end`` orphan)
650
+ # nor retired (the ledger, the harvest gate and the run outcome all key a
651
+ # check item by ``(kind, text)``). Only a line whose ``(kind, text)`` is
652
+ # gone from the new plan counts as dropped.
653
+ before_snapshot = parse_plan(content_before)
654
+ after_snapshot = parse_plan(content_after)
655
+ missing = missing_check_items(before_snapshot, after_snapshot)
656
+ unprotected = getattr(ctx, "guidance_unprotected_checks", None)
657
+ if not isinstance(unprotected, set):
658
+ unprotected = set()
659
+ still_declared = Counter((ci.kind, ci.text) for ci in after_snapshot.check_items)
660
+ dropped: list = []
661
+ for ci in missing:
662
+ if still_declared.get((ci.kind, ci.text), 0) > 0:
663
+ still_declared[(ci.kind, ci.text)] -= 1
664
+ continue # moved under a reworded milestone / re-timed: still declared
665
+ dropped.append(ci)
666
+ waived = [ci for ci in dropped if (ci.kind, ci.text) in unprotected]
667
+ protected = [ci for ci in dropped if (ci.kind, ci.text) not in unprotected]
668
+ if waived:
669
+ logger.info(
670
+ "Plan write dropped declared check lines because of user guidance"
671
+ f" (the change stands): {[render_check_line(ci).strip() for ci in waived]}"
672
+ )
673
+ # Records the retirement (harvest gate + run outcome) and withdraws any
674
+ # standing finding headline those lines had already produced.
675
+ retire_check_items(ctx, [(ci.kind, ci.text) for ci in waived])
676
+ # A retired line the Operator declares again is live again: the harvest
677
+ # gate and the run outcome must agree with the plan on disk.
678
+ reinstate_check_items(ctx, [(ci.kind, ci.text) for ci in after_snapshot.check_items])
679
+ if protected:
680
+ merged = restore_missing_check_items(content_before, content_after, items=protected)
681
+ if merged is not None:
682
+ logger.info(
683
+ "Plan write removed or rewrote declared check lines; merging them"
684
+ " back (check standards are protected deterministically):"
685
+ f" {[render_check_line(ci).strip() for ci in protected]}"
686
+ )
687
+ content_after = merged
688
+ try:
689
+ task_plan_path.write_text(content_after, encoding="utf-8")
690
+ except Exception as e:
691
+ logger.error(f"Failed to write merged check lines: {e}")
692
+
693
+ # Deterministic finding-line projection: unresolved verify findings are
694
+ # re-rendered from the checkpoint repair state on every plan write, so a
695
+ # model deletion grows back and a resolved finding is retired. Finding
696
+ # lines live outside the checkbox/check machine channel, so this rewrite
697
+ # never influences the parses below.
698
+ sync_finding_lines(ctx)
699
+
700
+ before = before_snapshot
701
+ after = parse_plan(content_after)
702
+
703
+ # Ratchet baseline: judge the current plan against the last *validated*
704
+ # plan rather than the previous write, so many small edits cannot drift
705
+ # the milestones below any per-edit trigger.
706
+ if ctx.last_validated_plan is None:
707
+ ctx.last_validated_plan = content_before
708
+
709
+ disable_planner_validation = (
710
+ ctx.execution_setup and ctx.execution_setup.disable_planner_validation
711
+ )
712
+ if not disable_planner_validation:
713
+ baseline = parse_plan(ctx.last_validated_plan)
714
+ if milestones_changed(baseline, after):
715
+ logger.info(
716
+ "Top-level task plan diverged from the validated baseline."
717
+ " Triggering async Planner Validation."
718
+ )
719
+
720
+ if ctx.planner_task and not ctx.planner_task.done():
721
+ # A newer write supersedes the in-flight validation.
722
+ ctx.planner_task.cancel()
723
+
724
+ ctx.pending_validated_plan = content_after
725
+ initial_goal = (
726
+ state.initial_goal if state and hasattr(state, "initial_goal") else "Unknown Goal"
727
+ )
728
+
729
+ ctx.planner_task = asyncio.create_task(
730
+ run_async_planner_validation(
731
+ ctx,
732
+ initial_goal,
733
+ ctx.last_validated_plan,
734
+ content_after,
735
+ getattr(state, "operator_raw_thinking", None),
736
+ getattr(state, "operator_native_thinking", None),
737
+ )
738
+ )
739
+
740
+ if isinstance(result, ToolMessage) and isinstance(result.content, str):
741
+ result.content = (
742
+ f"{result.content}\n\nWe have applied your changes"
743
+ " to the task plan. A lightweight background review"
744
+ " is checking the milestone change against the"
745
+ " original goal; if it has a concern you will get an"
746
+ " advisory hint with the reason (the change stays"
747
+ " applied either way)."
748
+ )
749
+
750
+ # Queue (never spawn) a checkpoint for EVERY newly completed top-level
751
+ # subgoal carrying on_complete check items. Spawning happens in
752
+ # execution_check_node after this turn's step is recorded, so the evidence
753
+ # anchor points at the correct step.
754
+ new_completions = new_top_level_completions(before, after)
755
+ if new_completions:
756
+ queue_checkpoints(ctx, state, after, new_completions, content_after)
757
+ # History chunking (M3): a top-level completion only queues an
758
+ # UNCONFIRMED boundary — the next stamped step confirms it via an
759
+ # actual subgoal-hash change (pseudo-switch protection, §3.3).
760
+ _notify_history_chunker(ctx, "queue_boundary_hint")
761
+
762
+ return result
763
+
764
+
765
+ async def _intercept_plan_write(
766
+ ctx: ArtemisContext,
767
+ original_tool,
768
+ args: dict,
769
+ tool_call_id,
770
+ state,
771
+ *,
772
+ declared_intent: bool,
773
+ content_after_fallback: str = "",
774
+ ):
775
+ """Run a note write and process any resulting task-plan changes."""
776
+ rejection = _reject_checker_note_write(args["key"], tool_call_id)
777
+ if rejection is not None:
778
+ return rejection
779
+ if args["key"] != "task_plan" or not ctx.data_engine:
780
+ return await invoke_tool_with_injection(
781
+ original_tool, args, tool_call_id, state, record_trace=False
782
+ )
783
+
784
+ task_plan_path = get_note_file_path(ctx.data_engine.base_dir, "task_plan")
785
+
786
+ content_before = ""
787
+ if task_plan_path.exists():
788
+ content_before = task_plan_path.read_text(encoding="utf-8")
789
+
790
+ result = await invoke_tool_with_injection(
791
+ original_tool, args, tool_call_id, state, record_trace=False
792
+ )
793
+
794
+ content_after = content_after_fallback
795
+ if task_plan_path.exists():
796
+ content_after = task_plan_path.read_text(encoding="utf-8")
797
+
798
+ return await _process_plan_write(
799
+ ctx,
800
+ state,
801
+ task_plan_path,
802
+ content_before,
803
+ content_after,
804
+ result,
805
+ declared_intent=declared_intent,
806
+ )
807
+
808
+
809
+ def wrap_note_tool(ctx: ArtemisContext, original_tool):
810
+
811
+ async def wrapped_note_tool(
812
+ key: str,
813
+ content: str,
814
+ tool_call_id: Annotated[str, InjectedToolCallId] = None,
815
+ state: Annotated[State, InjectedState] = None,
816
+ ):
817
+ """Wrapped tool to intercept task plan updates."""
818
+ return await _intercept_plan_write(
819
+ ctx,
820
+ original_tool,
821
+ {"key": key, "content": content},
822
+ tool_call_id,
823
+ state,
824
+ declared_intent=False,
825
+ content_after_fallback=content,
826
+ )
827
+
828
+ return StructuredTool.from_function(
829
+ func=lambda *a, **kw: None,
830
+ coroutine=wrapped_note_tool,
831
+ name=original_tool.name,
832
+ description=original_tool.description,
833
+ args_schema=NoteArgs,
834
+ )
835
+
836
+
837
+ class UpdateNoteArgs(BaseModel):
838
+ model_config = {"ignored_types": (CyFunctionDetector,)}
839
+ key: str = Field(..., description=UPDATE_NOTE_ARG_KEY_DESC)
840
+ target: str = Field(..., description=UPDATE_NOTE_ARG_TARGET_DESC)
841
+ replacement: str = Field(..., description=UPDATE_NOTE_ARG_REPLACEMENT_DESC)
842
+
843
+
844
+ def wrap_update_note_tool(ctx: ArtemisContext, original_tool):
845
+
846
+ async def wrapped_update_note(
847
+ key: str,
848
+ target: str,
849
+ replacement: str,
850
+ tool_call_id: Annotated[str, InjectedToolCallId] = None,
851
+ state: Annotated[State, InjectedState] = None,
852
+ ):
853
+ """Wrapped tool to intercept task plan updates via update_note."""
854
+ return await _intercept_plan_write(
855
+ ctx,
856
+ original_tool,
857
+ {"key": key, "target": target, "replacement": replacement},
858
+ tool_call_id,
859
+ state,
860
+ declared_intent=True,
861
+ )
862
+
863
+ return StructuredTool.from_function(
864
+ func=lambda *a, **kw: None,
865
+ coroutine=wrapped_update_note,
866
+ name=original_tool.name,
867
+ description=original_tool.description,
868
+ args_schema=UpdateNoteArgs,
869
+ )
870
+
871
+
872
+ def convergence_gate(
873
+ state: State, ctx: ArtemisContext
874
+ ) -> Literal["continue", "exit_settlement", "end"]:
875
+ logger.info("Starting convergence_gate")
876
+
877
+ if not ctx.data_engine:
878
+ return "end"
879
+
880
+ def _read_plan() -> str:
881
+ file_path = get_note_file_path(ctx.data_engine.base_dir, "task_plan")
882
+ if not file_path.exists():
883
+ return ""
884
+ try:
885
+ return file_path.read_text(encoding="utf-8")
886
+ except Exception:
887
+ return ""
888
+
889
+ def _terminal_route() -> Literal["exit_settlement", "end"]:
890
+ """Settlement and final review are decoupled from each other but share
891
+ the entry: any check item, in-flight/queued attempt, ledger record, or
892
+ an enabled final review forces the settlement node."""
893
+ plan_snapshot = parse_plan(_read_plan())
894
+ needs_settlement = (
895
+ bool(plan_snapshot.all_check_items)
896
+ or bool(getattr(ctx, "checkpoint_tasks", None))
897
+ or bool(getattr(ctx, "pending_checkpoints", None))
898
+ or has_ledger_records(ctx.data_engine.base_dir)
899
+ or final_check_enabled(ctx)
900
+ )
901
+ return "exit_settlement" if needs_settlement else "end"
902
+
903
+ # A halt latched by an assert failure (policy 'halt') terminates the run
904
+ # regardless of plan progress.
905
+ if getattr(ctx, "assert_halt", False):
906
+ logger.warning("Assert-halt latched; routing to exit settlement.")
907
+ return _terminal_route()
908
+
909
+ # Check planner-validation result. ``None`` means "not yet evaluated"
910
+ # (the first pass after the Planner node) — only an explicit False is a
911
+ # failure; otherwise the log would misreport the initial plan as rejected.
912
+ if getattr(state, "checker_success", None) is False:
913
+ logger.info("Plan validation failed, returning to operator for retry.")
914
+ return "continue"
915
+
916
+ file_path = get_note_file_path(ctx.data_engine.base_dir, "task_plan")
917
+ if not file_path.exists():
918
+ logger.warning(f"Task plan file not found at {file_path}")
919
+ return "continue"
920
+ try:
921
+ task_plan_content = file_path.read_text(encoding="utf-8")
922
+ except Exception as e:
923
+ logger.error(f"Failed to read task plan: {e}")
924
+ return "continue"
925
+
926
+ snapshot = parse_plan(task_plan_content)
927
+
928
+ if not snapshot.has_top_level:
929
+ # An empty plan does not bypass an enabled final review: the review
930
+ # object is the user's original goal, not the plan.
931
+ logger.info("No subgoals found in file, ending")
932
+ return _terminal_route()
933
+
934
+ if snapshot.all_top_level_done:
935
+ user_stopped = bool(getattr(state, "user_stop_requested", False)) if state else False
936
+ if snapshot.continuous_top_level and not user_stopped:
937
+ logger.warning(
938
+ "All subgoals marked [x] but task plan has a [Loop:continuous]"
939
+ " milestone without a user stop signal. Continuing."
940
+ )
941
+ return "continue"
942
+ logger.info("All subgoals are completed, ending the goal")
943
+ return _terminal_route()
944
+
945
+ return "continue"
946
+
947
+
948
+ async def get_graph(ctx: ArtemisContext) -> CompiledStateGraph:
949
+ graph_builder = StateGraph(State)
950
+
951
+ # Get native tools
952
+ native_wrappers = [
953
+ save_note_wrapper,
954
+ read_note_wrapper,
955
+ list_notes_wrapper,
956
+ update_note_wrapper,
957
+ append_note_wrapper,
958
+ ]
959
+
960
+ native_tools = get_tools_from_wrappers(ctx, native_wrappers)
961
+
962
+ ## Define nodes
963
+ graph_builder.add_node("planner", PlannerNode(ctx, tools=native_tools))
964
+
965
+ # Wrap native tools for Operator
966
+ operator_native_tools = []
967
+ for t in native_tools:
968
+ if t.name == "save_note":
969
+ operator_native_tools.append(wrap_note_tool(ctx, t))
970
+ elif t.name == "update_note":
971
+ operator_native_tools.append(wrap_update_note_tool(ctx, t))
972
+ elif t.name == "append_note":
973
+ operator_native_tools.append(wrap_note_tool(ctx, t))
974
+ else:
975
+ operator_native_tools.append(t)
976
+
977
+ # Get specialized tools for Operator (history tools: shared definitions)
978
+ from artemis.tools.history import HISTORY_TOOL_WRAPPERS
979
+
980
+ operator_specialized_wrappers = [
981
+ ask_diagnoser_wrapper,
982
+ run_adb_command_wrapper,
983
+ manage_task_wrapper,
984
+ ask_explorer_wrapper,
985
+ *HISTORY_TOOL_WRAPPERS,
986
+ ]
987
+ # ask_committee does not follow the Operator's pre-decision / turn-ending contract.
988
+
989
+ operator_specialized_tools = get_tools_from_wrappers(ctx, operator_specialized_wrappers)
990
+
991
+ # Customize descriptions for run_adb_command and manage_task to match exploratory role
992
+ for t in operator_specialized_tools:
993
+ if t.name == "run_adb_command":
994
+ t.description = (
995
+ "[SHELL] Executes a shell command directly on the Android"
996
+ " mobile device via ADB shell. Use it when the device action"
997
+ " tools cannot achieve the goal, or to start background"
998
+ " instrumentation (performance, power, traces); never to bypass"
999
+ " goals that the action tools can achieve.\nCan run synchronously"
1000
+ " or transition to a background task if execution takes longer"
1001
+ " than WaitMsBeforeAsync.\nSupports persistent environments"
1002
+ " (environment variables) on the phone across invocations.\n" + HANG_GUIDANCE
1003
+ )
1004
+ elif t.name == "manage_task":
1005
+ t.description = (
1006
+ "[SHELL] Manage background ADB shell tasks launched via"
1007
+ " run_adb_command to monitor or terminate ongoing background"
1008
+ " commands."
1009
+ )
1010
+
1011
+ operator_tools = operator_native_tools + operator_specialized_tools
1012
+
1013
+ if ctx.execution_setup and ctx.execution_setup.video_recording_tools_enabled:
1014
+ operator_tools.append(get_video_analyzer_tool(ctx, role="operator"))
1015
+
1016
+ graph_builder.add_node("operator", OperatorNode(ctx, tools=operator_tools))
1017
+ graph_builder.add_node("validator", ValidatorNode(ctx))
1018
+
1019
+ graph_builder.add_node("summarizer", SummarizerNode(ctx))
1020
+ graph_builder.add_node("execution_check", functools.partial(execution_check_node, ctx=ctx))
1021
+ graph_builder.add_node("perception", functools.partial(perception_node, ctx=ctx))
1022
+ add_convergence_node(graph_builder)
1023
+ graph_builder.add_node("exit_settlement", functools.partial(exit_settlement_node, ctx=ctx))
1024
+
1025
+ ## Linking nodes
1026
+ graph_builder.add_edge(START, "planner")
1027
+ graph_builder.add_edge("planner", CONVERGENCE_NODE)
1028
+ graph_builder.add_edge("operator", "execution_check")
1029
+ graph_builder.add_conditional_edges(
1030
+ "execution_check",
1031
+ execution_check_edge,
1032
+ {
1033
+ "review_subgoals": CONVERGENCE_NODE,
1034
+ "execute_decisions": "validator",
1035
+ },
1036
+ )
1037
+ graph_builder.add_edge("validator", "summarizer")
1038
+ graph_builder.add_edge("summarizer", CONVERGENCE_NODE)
1039
+
1040
+ add_convergence_edges(
1041
+ graph_builder,
1042
+ functools.partial(convergence_gate, ctx=ctx),
1043
+ {"continue": "perception", "exit_settlement": "exit_settlement", "end": END},
1044
+ )
1045
+ graph_builder.add_conditional_edges(
1046
+ source="exit_settlement",
1047
+ path=exit_settlement_gate,
1048
+ path_map={
1049
+ "continue": "perception",
1050
+ "end": END,
1051
+ },
1052
+ )
1053
+ graph_builder.add_edge("perception", "operator")
1054
+
1055
+ return graph_builder.compile()