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,1394 @@
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 json
16
+ from typing import Any
17
+ from uuid import uuid4
18
+
19
+ from langchain_core.messages import SystemMessage, ToolMessage
20
+ from langchain_core.tools import BaseTool
21
+
22
+ from artemis.core.tool_failure import is_tool_failure
23
+ from artemis.context import ArtemisContext
24
+ from artemis.data_engine.trace import (
25
+ TraceSpan,
26
+ trace,
27
+ trace_langchain_tool,
28
+ )
29
+ from artemis.graph.state import State
30
+ from artemis.graph.visibility import strict_state
31
+ from artemis.mcp.action_specs import OPERATOR_SHELL_ORDER, operator_shell_tool
32
+ from artemis.llm.google import usage_from_message
33
+ from artemis.services.llm import acomplete, get_llm, invoke_llm_with_timeout_message
34
+ from artemis.tools.command_tool import (
35
+ analyze_task_output_wrapper,
36
+ get_adb_task_registry,
37
+ )
38
+ from artemis.tools.index import get_tool_by_name
39
+ from artemis.tools.tool_wrapper import (
40
+ get_tool_result_content,
41
+ invoke_tool_with_injection,
42
+ tool_result_messages,
43
+ )
44
+ from artemis.utils.coordinates import (
45
+ COORDINATE_SPACE_KEY,
46
+ COORDINATE_SPACE_PIXEL,
47
+ compute_smart_swipe_coordinates,
48
+ parse_swipe_parameters,
49
+ )
50
+ from artemis.utils.notes import get_note_file_path
51
+ from artemis.utils.plan_grammar import parse_plan
52
+ from artemis.memory.context_policy import build_history_for
53
+ from artemis.utils.task_tree import get_active_subgoal_hashes
54
+ from artemis.utils.visualization import format_minimal_list_with_elements
55
+ from third_party.mobile_use.utils.decorators import agent_lifecycle_logging
56
+ from third_party.mobile_use.utils.logger import get_logger
57
+
58
+ logger = get_logger(__name__)
59
+
60
+ DEFERRING_TOOLS = {
61
+ "ask_diagnoser",
62
+ "video_analyzer",
63
+ "read_note",
64
+ "list_notes",
65
+ "search_history",
66
+ "replay_steps",
67
+ "get_step_screenshot",
68
+ "run_adb_command",
69
+ "manage_task",
70
+ "analyze_task_output",
71
+ "ask_explorer",
72
+ }
73
+
74
+ # Bare key names press_key accepts (case-insensitive, optional KEYCODE_ prefix).
75
+ SUPPORTED_PRESS_KEYS = ("ENTER", "BACK", "HOME", "APP_SWITCH")
76
+
77
+ from artemis.agents.operator.prompts import (
78
+ OPERATOR_MAX_TOOL_ITERATIONS,
79
+ PromptBuilder,
80
+ PromptComponent,
81
+ TemplatePromptComponent,
82
+ ObservationPromptComponent,
83
+ PlanRecitationPromptComponent,
84
+ ScreenshotSimilarityPromptComponent,
85
+ HistoricalStateHintPromptComponent,
86
+ FeedbackPromptComponent,
87
+ ExecutionIncidentPromptComponent,
88
+ BackgroundTasksPromptComponent,
89
+ render_plan_ledger_bounce,
90
+ ReasoningReminderPromptComponent,
91
+ unwritten_action_streak,
92
+ ToolLimitWarningPromptComponent,
93
+ InjectedInstructionPromptComponent,
94
+ render_transcript_static_system,
95
+ )
96
+ from artemis.agents.operator.prompts import load_operator_prompts
97
+
98
+
99
+ def _described(value: Any) -> str | None:
100
+ """Trim target_description, returning None for missing or blank values."""
101
+ if isinstance(value, str) and value.strip():
102
+ return value.strip()
103
+ return None
104
+
105
+
106
+ class OperatorNode:
107
+ def __init__(
108
+ self,
109
+ ctx: ArtemisContext,
110
+ tools: list[BaseTool] | None = None,
111
+ prompt_components: list[PromptComponent] | None = None,
112
+ last_n_detailed: int = 1,
113
+ transcript_config=None,
114
+ ):
115
+ self.ctx = ctx
116
+ self.tools = tools or []
117
+ self.prompt_components = prompt_components or []
118
+ self.last_n_detailed = last_n_detailed
119
+ try:
120
+ self.prompts = load_operator_prompts()
121
+ except (OSError, ValueError, json.JSONDecodeError) as e:
122
+ raise RuntimeError(f"Failed to load operator prompts: {e}") from e
123
+
124
+ # M2 transcript flag (agent.memory.transcript.enabled). Off keeps the
125
+ # legacy 2-message prompt path byte-for-byte; an explicit
126
+ # ``transcript_config`` overrides the loaded configuration (tests).
127
+ if transcript_config is not None:
128
+ self._transcript_cfg = transcript_config
129
+ else:
130
+ try:
131
+ from artemis.config import load_agent_config
132
+
133
+ self._transcript_cfg = load_agent_config().memory.transcript
134
+ except Exception:
135
+ from artemis.config.agent import MemoryTranscriptConfig
136
+
137
+ self._transcript_cfg = MemoryTranscriptConfig()
138
+ # Index into the rendered message list where this turn's tail begins;
139
+ # set by the transcript build and consumed after the tool loop.
140
+ self._transcript_turn_base: int | None = None
141
+ self._turn_plan_baseline: str | None = None
142
+ self._turn_ledger_streak: int = 0
143
+ # Whether the previous turn's replies carried no visible reasoning text;
144
+ # the next observation tail then carries a one-line reminder (no extra
145
+ # model call). Legacy-path fallback only: with a transcript ledger the
146
+ # ledger's own ``last_turn_silent`` is authoritative.
147
+ self._previous_turn_silent: bool = False
148
+
149
+ def _previous_turn_was_silent(self) -> bool:
150
+ """Whether the previous turn carried no visible reasoning text.
151
+
152
+ The transcript ledger judges its committed turns itself; the local flag
153
+ only serves the legacy path, which has no ledger.
154
+ """
155
+ from artemis.memory import TranscriptLedger
156
+
157
+ ledger = getattr(self.ctx, "transcript_ledger", None)
158
+ if isinstance(ledger, TranscriptLedger):
159
+ return bool(ledger.last_turn_silent)
160
+ return self._previous_turn_silent
161
+
162
+ def _available_device_actions(self) -> frozenset[str]:
163
+ """Device actions the installed actuator backend provides.
164
+
165
+ Without an explicit ``ctx.actuator`` the full manifest set applies (the
166
+ default AdbActuator implements everything), so behavior is unchanged until a
167
+ partial backend is actually installed.
168
+ """
169
+ from artemis.mcp.action_manifest import (
170
+ OPTIONAL_ACTIONS,
171
+ REQUIRED_ACTIONS,
172
+ available_device_actions,
173
+ )
174
+
175
+ actuator = getattr(self.ctx, "actuator", None)
176
+ if actuator is not None and callable(getattr(actuator, "capabilities", None)):
177
+ try:
178
+ extension_names = frozenset(
179
+ e.name for e in actuator.extensions() if "operator" in e.targets
180
+ )
181
+ return available_device_actions(actuator) | extension_names
182
+ except Exception as e:
183
+ logger.warning(f"Failed to read actuator capabilities: {e}")
184
+ return REQUIRED_ACTIONS | OPTIONAL_ACTIONS
185
+
186
+ def _get_history_and_plan(self) -> tuple[list[dict], str]:
187
+ steps = []
188
+ if self.ctx.data_engine:
189
+ steps = self.ctx.data_engine.get_agent_friendly_steps()
190
+
191
+ task_plan = "No task plan yet."
192
+ if self.ctx.data_engine:
193
+ task_plan_path = get_note_file_path(self.ctx.data_engine.base_dir, "task_plan")
194
+
195
+ if task_plan_path.exists():
196
+ try:
197
+ task_plan = task_plan_path.read_text(encoding="utf-8")
198
+ except Exception as e:
199
+ task_plan = f"Error reading task plan: {e}"
200
+
201
+ return steps, task_plan
202
+
203
+ async def _build_prompt(
204
+ self,
205
+ state: State,
206
+ latest_screenshot_b64: str,
207
+ fused_xml: list[dict],
208
+ minimal_list: str,
209
+ current_step_num: int,
210
+ steps: list[dict],
211
+ task_plan: str,
212
+ active_background_tasks: list[dict] = None,
213
+ newly_finished_tasks: list[dict] = None,
214
+ ) -> list:
215
+ self._transcript_turn_base = None
216
+ if getattr(self._transcript_cfg, "enabled", False):
217
+ try:
218
+ return await self._build_prompt_transcript(
219
+ state,
220
+ latest_screenshot_b64,
221
+ minimal_list,
222
+ current_step_num,
223
+ steps,
224
+ task_plan,
225
+ active_background_tasks=active_background_tasks,
226
+ newly_finished_tasks=newly_finished_tasks,
227
+ )
228
+ except Exception as e:
229
+ logger.error(
230
+ f"Transcript prompt path failed; falling back to the legacy"
231
+ f" 2-message build for this turn: {e}"
232
+ )
233
+ self._transcript_turn_base = None
234
+
235
+ plan_and_history = self._build_plan_and_history(steps, task_plan)
236
+
237
+ builder = PromptBuilder()
238
+
239
+ # One constant component list: the prompt template is never switched by
240
+ # verification results. Check-related components are append-only and
241
+ # render nothing when there is nothing to say.
242
+ components = self.prompt_components
243
+ if not components:
244
+ components = [
245
+ (
246
+ TemplatePromptComponent(),
247
+ {"template_name": "main_template"},
248
+ ),
249
+ (ExecutionIncidentPromptComponent(), {}),
250
+ (ObservationPromptComponent(), {}),
251
+ (ScreenshotSimilarityPromptComponent(), {}),
252
+ (HistoricalStateHintPromptComponent(), {}),
253
+ (InjectedInstructionPromptComponent(), {}),
254
+ (BackgroundTasksPromptComponent(), {}),
255
+ (FeedbackPromptComponent(), {}),
256
+ (ReasoningReminderPromptComponent(), {}),
257
+ (ToolLimitWarningPromptComponent(), {}),
258
+ ]
259
+ else:
260
+ components = [(c, {}) for c in components] + [
261
+ (ReasoningReminderPromptComponent(), {}),
262
+ (ToolLimitWarningPromptComponent(), {}),
263
+ ]
264
+
265
+ # Active and finished tasks are now passed as arguments
266
+ active_tasks = active_background_tasks or []
267
+ newly_finished = newly_finished_tasks or []
268
+
269
+ for component, extra_kwargs in components:
270
+ kwargs_to_pass = {
271
+ "prompts": self.prompts,
272
+ "plan_and_history": plan_and_history,
273
+ "task_plan": task_plan,
274
+ "latest_screenshot_b64": latest_screenshot_b64,
275
+ "minimal_list": minimal_list,
276
+ "current_step_num": current_step_num,
277
+ "active_background_tasks": active_tasks,
278
+ "newly_finished_tasks": newly_finished,
279
+ "previous_turn_silent": self._previous_turn_was_silent(),
280
+ "steps": steps,
281
+ }
282
+ kwargs_to_pass.update(extra_kwargs)
283
+ await component(builder, state, self.ctx, **kwargs_to_pass)
284
+
285
+ return builder.build()
286
+
287
+ # ------------------------------------------------------------------
288
+ # Transcript prompt path (M2, flag agent.memory.transcript.enabled)
289
+ # ------------------------------------------------------------------
290
+
291
+ def _ensure_transcript_ledger(self, state=None):
292
+ """Session ledger on the composition root, created on first use."""
293
+ from artemis.memory import TranscriptLedger, ensure_step_memory
294
+
295
+ ledger = getattr(self.ctx, "transcript_ledger", None)
296
+ if isinstance(ledger, TranscriptLedger):
297
+ return ledger
298
+ cfg = self._transcript_cfg
299
+ # Anchor the ``T+mm:ss`` clock to the DataEngine session start so the
300
+ # tail offsets agree with the chunk ledger lines, the restored-history
301
+ # block and the video analyzer's action timeline.
302
+ session_start = getattr(self.ctx.data_engine, "session_start_time", None)
303
+ ledger = TranscriptLedger(
304
+ step_memory=ensure_step_memory(self.ctx),
305
+ image_scrub_depth=getattr(cfg, "image_scrub_depth", 3),
306
+ pending_grace_steps=getattr(cfg, "pending_grace_steps", 3),
307
+ xml_scrub_depth=getattr(cfg, "xml_scrub_depth", 1),
308
+ image_scrub_depth_relaxed=getattr(cfg, "image_scrub_depth_relaxed", None),
309
+ context_budget_tokens=getattr(cfg, "context_budget_tokens", None),
310
+ start_ratio=getattr(cfg, "start_ratio", None),
311
+ session_start=session_start if isinstance(session_start, (int, float)) else None,
312
+ )
313
+ # L2/L3 chunk compression (M3) rides the same flag; without a
314
+ # DataEngine there are no step records to chunk, so the ledger simply
315
+ # runs scrub-edge-only.
316
+ if self.ctx.data_engine is not None:
317
+ try:
318
+ from artemis.memory import HistoryChunkManager
319
+
320
+ chunking_cfg = None
321
+ try:
322
+ from artemis.config import load_agent_config
323
+
324
+ chunking_cfg = load_agent_config().memory.chunking
325
+ except Exception as exc:
326
+ logger.debug(
327
+ "Chunking config unavailable; using chunker defaults: %s",
328
+ exc,
329
+ exc_info=True,
330
+ )
331
+ ledger.attach_chunker(
332
+ HistoryChunkManager(
333
+ engine=self.ctx.data_engine,
334
+ ctx=self.ctx,
335
+ chunking_config=chunking_cfg,
336
+ transcript_config=cfg,
337
+ goal=getattr(state, "initial_goal", None) if state else None,
338
+ )
339
+ )
340
+ except Exception as e:
341
+ logger.error(f"History chunk manager unavailable: {e}")
342
+ try:
343
+ self.ctx.transcript_ledger = ledger
344
+ except (AttributeError, TypeError, ValueError):
345
+ pass
346
+ return ledger
347
+
348
+ async def _build_prompt_transcript(
349
+ self,
350
+ state: State,
351
+ latest_screenshot_b64: str,
352
+ minimal_list: str,
353
+ current_step_num: int,
354
+ steps: list[dict],
355
+ task_plan: str,
356
+ active_background_tasks: list[dict] = None,
357
+ newly_finished_tasks: list[dict] = None,
358
+ ) -> list:
359
+ """Build ``S + F + A + tail`` from the session transcript ledger.
360
+
361
+ The static system message renders once per session (byte-stable S
362
+ region); the previous turn is committed into the append-only active
363
+ region together with its step key and validator result; the fresh tail
364
+ carries the current observation plus the task-plan recitation.
365
+ """
366
+ from langchain_core.messages import HumanMessage
367
+
368
+ from artemis.memory.transcript import mark_ephemeral
369
+
370
+ ledger = self._ensure_transcript_ledger(state)
371
+
372
+ # 1. S region: rendered exactly once per session.
373
+ if not ledger.has_static_prefix:
374
+ static_text = render_transcript_static_system(self.prompts, self.ctx, state)
375
+ ledger.set_static_prefix([SystemMessage(content=static_text)])
376
+
377
+ # 2. F region cold start: an empty ledger over an existing step record
378
+ # trail means the process restarted — freeze the compiled history once.
379
+ # A staged (not yet committed) turn means this is turn 2+ of a live
380
+ # session, not a cold start — step records already exist then, and
381
+ # seeding would be rejected by the empty-ledger invariant.
382
+ if (
383
+ ledger.turn_count == 0
384
+ and not ledger.has_staged_turn
385
+ and not ledger.has_restored_history
386
+ and steps
387
+ ):
388
+ restored = build_history_for(
389
+ "operator_cold_start",
390
+ task_plan,
391
+ steps,
392
+ self._get_active_subgoal_hash(),
393
+ )
394
+ ledger.set_restored_history(
395
+ "[Restored history] Rebuilt from the step records of this task"
396
+ " after a process restart; step times below are relative to the"
397
+ " original session start.\n"
398
+ f"{restored}"
399
+ )
400
+
401
+ # 3. Commit the previous turn (its step id and validator result only
402
+ # exist now). A turn without a terminal action never ran the validator,
403
+ # and ``structured_decisions`` is cleared on planner rejection — the
404
+ # gate keeps stale reports out of the ledger.
405
+ validator_result = (
406
+ getattr(state, "last_execution_result", None)
407
+ if getattr(state, "structured_decisions", None)
408
+ else None
409
+ )
410
+ ledger.commit_staged(
411
+ step_key=getattr(state, "current_step_id", None),
412
+ validator_result=validator_result,
413
+ )
414
+
415
+ # 4. Current tail: observation + plan recitation + injected components.
416
+ builder = PromptBuilder()
417
+ builder.add_human_content(f"# CURRENT OBSERVATION [{ledger.elapsed_label()}]")
418
+ components = [
419
+ (PlanRecitationPromptComponent(), {}),
420
+ (ExecutionIncidentPromptComponent(), {}),
421
+ (ObservationPromptComponent(), {}),
422
+ (ScreenshotSimilarityPromptComponent(), {}),
423
+ (HistoricalStateHintPromptComponent(), {}),
424
+ (InjectedInstructionPromptComponent(), {}),
425
+ (BackgroundTasksPromptComponent(), {}),
426
+ (FeedbackPromptComponent(), {}),
427
+ (ReasoningReminderPromptComponent(), {}),
428
+ (ToolLimitWarningPromptComponent(), {}),
429
+ ]
430
+ for component, extra_kwargs in components:
431
+ kwargs_to_pass = {
432
+ "prompts": self.prompts,
433
+ "task_plan": task_plan,
434
+ "latest_screenshot_b64": latest_screenshot_b64,
435
+ "minimal_list": minimal_list,
436
+ "current_step_num": current_step_num,
437
+ "active_background_tasks": active_background_tasks or [],
438
+ "newly_finished_tasks": newly_finished_tasks or [],
439
+ "steps": steps,
440
+ "previous_turn_silent": self._previous_turn_was_silent(),
441
+ }
442
+ kwargs_to_pass.update(extra_kwargs)
443
+ await component(builder, state, self.ctx, **kwargs_to_pass)
444
+
445
+ tail_content = []
446
+ for part in builder.human_parts:
447
+ if isinstance(part, str):
448
+ tail_content.append({"type": "text", "text": part})
449
+ else:
450
+ tail_content.append(part)
451
+ tail = HumanMessage(content=tail_content)
452
+ # Per-turn notices (reminders, hints, guidance, incident blocks) are
453
+ # deleted by the scrub edge once the turn leaves the recent window.
454
+ if builder.ephemeral_indices:
455
+ mark_ephemeral(tail, builder.ephemeral_indices)
456
+
457
+ messages = ledger.render([tail])
458
+ self._transcript_turn_base = len(messages) - 1
459
+ return messages
460
+
461
+ def _stage_transcript_turn(self, messages: list) -> None:
462
+ """Hand this turn's tail + tool-loop products to the ledger."""
463
+ base = self._transcript_turn_base
464
+ self._transcript_turn_base = None
465
+ if base is None:
466
+ return
467
+ try:
468
+ ledger = getattr(self.ctx, "transcript_ledger", None)
469
+ if ledger is not None:
470
+ ledger.stage_turn(messages[base:])
471
+ except Exception as e:
472
+ logger.error(f"Failed to stage transcript turn: {e}")
473
+
474
+ async def _invoke_llm_loop(
475
+ self,
476
+ base_llm,
477
+ current_messages: list,
478
+ traced_tools: list,
479
+ new_subagent_calls: list,
480
+ state: State,
481
+ ) -> tuple[list[dict] | None, str | None, str | None, bool]:
482
+ max_iterations = OPERATOR_MAX_TOOL_ITERATIONS
483
+ action_result = None
484
+ raw_thoughts = []
485
+ native_thoughts = []
486
+ tool_limit_exceeded = False
487
+ plan_gate_bounced = False
488
+
489
+ for iteration in range(max_iterations):
490
+ if iteration == max_iterations - 1:
491
+ logger.warning(
492
+ "Operator has reached the maximum number of tool calls. Adding reminder."
493
+ )
494
+ current_messages.append(
495
+ SystemMessage(
496
+ content=(
497
+ "You have reached the maximum number of tool calls"
498
+ " for this turn. You must make a final action, or"
499
+ " save your current research results via notes to"
500
+ " continue later."
501
+ )
502
+ )
503
+ )
504
+
505
+ bound_llm = base_llm.bind_tools(tools=traced_tools)
506
+ response = await invoke_llm_with_timeout_message(acomplete(bound_llm, current_messages))
507
+
508
+ usage = usage_from_message(response)
509
+ # The ledger's compaction thresholds read the operator's own
510
+ # prompt size, never the session-wide meter (which the
511
+ # Planner/Checker/sub-agent calls overwrite). The messages ride
512
+ # along so the ledger can calibrate its chars-per-token ratio.
513
+ transcript_ledger = getattr(self.ctx, "transcript_ledger", None)
514
+ if usage is not None:
515
+ logger.info(
516
+ f"LLM usage: prompt_tokens={usage['prompt_tokens']}"
517
+ f" (cached={usage['cached_tokens']}),"
518
+ f" completion_tokens={usage['completion_tokens']}"
519
+ )
520
+ if hasattr(transcript_ledger, "record_prompt_tokens"):
521
+ transcript_ledger.record_prompt_tokens(
522
+ usage.get("prompt_tokens"), messages=current_messages
523
+ )
524
+ elif hasattr(transcript_ledger, "record_prompt_tokens"):
525
+ # No usage metadata from the provider: record an estimate so
526
+ # the start gate and thresholds still see a context base.
527
+ from artemis.memory.transcript import estimate_prompt_tokens
528
+
529
+ transcript_ledger.record_prompt_tokens(estimate_prompt_tokens(current_messages))
530
+
531
+ if response.content:
532
+ if isinstance(response.content, str):
533
+ raw_thoughts.append(response.content)
534
+ elif isinstance(response.content, list):
535
+ for item in response.content:
536
+ if isinstance(item, dict):
537
+ if item.get("type") == "text":
538
+ raw_thoughts.append(item["text"])
539
+ elif item.get("type") == "thinking":
540
+ native_thoughts.append(item["thinking"])
541
+
542
+ additional_kwargs = getattr(response, "additional_kwargs", None)
543
+ if (
544
+ not response.tool_calls
545
+ and type(additional_kwargs) is dict
546
+ and additional_kwargs.get("function_call")
547
+ ):
548
+ fc = additional_kwargs["function_call"]
549
+ try:
550
+ arguments_str = fc.get("arguments", "{}")
551
+ if isinstance(arguments_str, str):
552
+ arguments = json.loads(arguments_str)
553
+ else:
554
+ arguments = arguments_str
555
+
556
+ tool_call = {
557
+ "name": fc["name"],
558
+ "args": arguments,
559
+ "id": f"call_{uuid4().hex[:8]}",
560
+ "type": "tool_call",
561
+ }
562
+ response.tool_calls = [tool_call]
563
+ logger.info(
564
+ "Fallback: Manually parsed function_call from"
565
+ f" additional_kwargs: {tool_call}"
566
+ )
567
+ except Exception as e:
568
+ logger.error(f"Failed to parse function_call fallback: {e}")
569
+
570
+ if not response.tool_calls:
571
+ logger.warning("LLM stopped without calling any tool. Encouraging action.")
572
+ break
573
+
574
+ current_messages.append(response)
575
+
576
+ # Manifest-driven, so a backend's extension actions are classified as
577
+ # turn-ending device actions without touching this file.
578
+ action_tool_names = sorted(self._available_device_actions())
579
+
580
+ def normalize_name(name: str) -> str:
581
+ return name.split(":")[-1] if ":" in name else name
582
+
583
+ action_calls = [
584
+ tc for tc in response.tool_calls if normalize_name(tc["name"]) in action_tool_names
585
+ ]
586
+ other_calls = [
587
+ tc
588
+ for tc in response.tool_calls
589
+ if normalize_name(tc["name"]) not in action_tool_names
590
+ ]
591
+
592
+ tool_outputs = []
593
+ validation_errors = False
594
+
595
+ other_tool_failed = False
596
+ other_tool_failure_msg = ""
597
+
598
+ has_terminal_calls = bool(action_calls)
599
+
600
+ if other_calls:
601
+ results = []
602
+ for tc in other_calls:
603
+ tool_name = tc["name"]
604
+ logger.info(f"Operator requested tool: {tool_name}")
605
+ if tool_name in ["ask_diagnoser", "video_analyzer"]:
606
+ new_subagent_calls.append(tool_name)
607
+
608
+ if ":" in tool_name:
609
+ tool_to_run = get_tool_by_name(tool_name, traced_tools)
610
+ else:
611
+ tool_to_run = next(
612
+ (t for t in traced_tools if t.name == tool_name),
613
+ None,
614
+ )
615
+
616
+ if tool_to_run:
617
+ args = dict(tc["args"])
618
+ span = TraceSpan(name=tool_name, trace_type="tool", ctx=self.ctx)
619
+ span.payload = {"args": args}
620
+
621
+ with span:
622
+ try:
623
+ state.operator_raw_thinking = (
624
+ "\n".join(raw_thoughts) if raw_thoughts else None
625
+ )
626
+ state.operator_native_thinking = (
627
+ "\n".join(native_thoughts) if native_thoughts else None
628
+ )
629
+
630
+ result_obj = await invoke_tool_with_injection(
631
+ tool=tool_to_run,
632
+ args=args,
633
+ tool_call_id=tc["id"],
634
+ state=state,
635
+ )
636
+
637
+ content = get_tool_result_content(result_obj)
638
+ # Failure is structural (a ToolMessage status or a
639
+ # ToolFailure text), never sniffed from the words.
640
+ is_err = is_tool_failure(result_obj)
641
+ status = "error" if is_err else "success"
642
+
643
+ if is_err:
644
+ other_tool_failed = True
645
+ other_tool_failure_msg = str(content)
646
+ status = "error"
647
+
648
+ span.status = "failed"
649
+ span.error = str(content)
650
+ else:
651
+ span.result = content
652
+
653
+ results.append((content, status))
654
+ except Exception as e:
655
+ other_tool_failed = True
656
+ other_tool_failure_msg = str(e)
657
+
658
+ span.status = "failed"
659
+ span.error = str(e)
660
+
661
+ results.append(
662
+ (
663
+ f"Error running tool {tool_name}: {e}",
664
+ "error",
665
+ )
666
+ )
667
+ else:
668
+ other_tool_failed = True
669
+ other_tool_failure_msg = f"Tool {tool_name} not supported"
670
+ results.append((f"Error: Tool {tool_name} not supported", "error"))
671
+
672
+ for tc, (content, status) in zip(other_calls, results):
673
+ # A step screenshot (content blocks) travels in the carrier
674
+ # this model's provider accepts; text stays a ToolMessage.
675
+ tool_outputs.extend(
676
+ tool_result_messages(
677
+ tc["id"],
678
+ content,
679
+ name=normalize_name(tc["name"]),
680
+ status=status,
681
+ llm=base_llm,
682
+ )
683
+ )
684
+
685
+ if other_tool_failed:
686
+ if action_calls:
687
+ logger.warning(
688
+ "Scratchpad/helper tool call failed. Deferring"
689
+ f" screen actions: {other_tool_failure_msg}"
690
+ )
691
+ # Every rejected call needs its own ToolMessage, but the
692
+ # full explanation travels once; the rest point at it.
693
+ for idx, tc in enumerate(action_calls):
694
+ tool_outputs.append(
695
+ ToolMessage(
696
+ tool_call_id=tc["id"],
697
+ name=normalize_name(tc["name"]),
698
+ content=(
699
+ "Not executed: this screen action was"
700
+ " rejected because an accompanying tool"
701
+ f" call failed. Error: {other_tool_failure_msg}."
702
+ " No screen action ran this turn. Correct"
703
+ " the error, then re-issue the action."
704
+ if idx == 0
705
+ else "Not executed: see the accompanying"
706
+ " tool-call error above."
707
+ ),
708
+ status="error",
709
+ )
710
+ )
711
+ action_calls = []
712
+
713
+ elif has_terminal_calls:
714
+ has_deferring_calls = any(
715
+ normalize_name(tc["name"]) in DEFERRING_TOOLS for tc in other_calls
716
+ )
717
+ if has_deferring_calls:
718
+ logger.info(
719
+ "Operator mixed pre-decision tools"
720
+ " (memory/exploration) and terminal action calls."
721
+ " Deferring terminal actions."
722
+ )
723
+
724
+ for idx, tc in enumerate(action_calls):
725
+ tool_outputs.append(
726
+ ToolMessage(
727
+ tool_call_id=tc["id"],
728
+ name=normalize_name(tc["name"]),
729
+ content=(
730
+ "Not executed: this screen action was"
731
+ " deferred because the same turn also"
732
+ " called a result-dependent pre-decision"
733
+ " tool. No screen action ran this turn."
734
+ " Read the tool results above, then"
735
+ " re-issue the action."
736
+ if idx == 0
737
+ else "Not executed: deferred for the same"
738
+ " reason as the screen action above."
739
+ ),
740
+ status="error",
741
+ )
742
+ )
743
+
744
+ action_calls = []
745
+
746
+ if action_calls:
747
+ logger.info(
748
+ f"Operator requested {len(action_calls)} action(s)."
749
+ " Translating and processing all."
750
+ )
751
+ actions_list = []
752
+ burst_cap = self._max_burst_actions()
753
+ bounce = None
754
+ if (
755
+ not plan_gate_bounced
756
+ and iteration < max_iterations - 1
757
+ and len(action_calls) == 1
758
+ and not getattr(state, "open_incident", None)
759
+ and self._plan_changed_this_turn() is False
760
+ ):
761
+ bounce = self._plan_ledger_bounce_reason()
762
+ if bounce:
763
+ plan_gate_bounced = True
764
+ validation_errors = True
765
+ logger.info(f"Plan ledger gate: bouncing action. {bounce}")
766
+ for tc in action_calls:
767
+ tool_outputs.append(
768
+ ToolMessage(
769
+ tool_call_id=tc["id"],
770
+ name=normalize_name(tc["name"]),
771
+ content=bounce,
772
+ status="error",
773
+ )
774
+ )
775
+ action_calls_to_translate = []
776
+ elif len(action_calls) > burst_cap:
777
+ # A multi-action turn is a fast-action burst that the Validator
778
+ # fires without the safety net; cap its length before it runs.
779
+ validation_errors = True
780
+ burst_error = (
781
+ f"Error: {len(action_calls)} Turn-Ending Actions in one"
782
+ " turn exceed the fast-action burst limit of"
783
+ f" {burst_cap}. Nothing was executed. Re-issue at most"
784
+ f" {burst_cap} actions (a burst is for sub-second"
785
+ " sequences on transient UI, not for batching normal"
786
+ " steps), or a single vetted action."
787
+ )
788
+ # One ToolMessage per rejected call, full text only on the first.
789
+ for idx, tc in enumerate(action_calls):
790
+ tool_outputs.append(
791
+ ToolMessage(
792
+ tool_call_id=tc["id"],
793
+ name=normalize_name(tc["name"]),
794
+ content=(
795
+ burst_error
796
+ if idx == 0
797
+ else "Not executed: see the burst-limit error above."
798
+ ),
799
+ status="error",
800
+ )
801
+ )
802
+ action_calls_to_translate = []
803
+ else:
804
+ if plan_gate_bounced and self._plan_changed_this_turn() is False:
805
+ logger.warning(
806
+ "Plan ledger gate: executing actions after one bounce without"
807
+ " a task_plan change (ledger gap this turn)."
808
+ )
809
+ action_calls_to_translate = action_calls
810
+ for tc in action_calls_to_translate:
811
+ translated_actions, error = self._translate_and_validate_tool(tc, state)
812
+ if error:
813
+ validation_errors = True
814
+ tool_outputs.append(
815
+ ToolMessage(
816
+ tool_call_id=tc["id"],
817
+ name=normalize_name(tc["name"]),
818
+ content=error,
819
+ status="error",
820
+ )
821
+ )
822
+ else:
823
+ actions_list.extend(translated_actions)
824
+ tool_outputs.append(
825
+ ToolMessage(
826
+ tool_call_id=tc["id"],
827
+ name=normalize_name(tc["name"]),
828
+ content="Action Recorded",
829
+ status="success",
830
+ )
831
+ )
832
+
833
+ if not validation_errors:
834
+ action_result = actions_list
835
+
836
+ for tm in tool_outputs:
837
+ current_messages.append(tm)
838
+
839
+ if action_calls and not validation_errors:
840
+ break
841
+
842
+ if validation_errors:
843
+ logger.warning("Some action calls failed validation. Feeding back to LLM.")
844
+ continue
845
+ else:
846
+ tool_limit_exceeded = True
847
+
848
+ raw_thinking = "\n".join(raw_thoughts) if raw_thoughts else None
849
+ native_thinking = "\n".join(native_thoughts) if native_thoughts else None
850
+ return (
851
+ action_result,
852
+ raw_thinking,
853
+ native_thinking,
854
+ tool_limit_exceeded,
855
+ )
856
+
857
+ @agent_lifecycle_logging("Operator", logger)
858
+ @trace(type="agent", name="operator")
859
+ async def __call__(self, state: State):
860
+ state = strict_state(state, "operator")
861
+ # 1. Get perception data from state (populated by Perception node)
862
+ operator_raw_data = getattr(state, "operator_raw_data", {}) or {}
863
+ latest_screenshot_b64 = operator_raw_data.get("screenshot_b64")
864
+ xml_hierarchy = operator_raw_data.get("xml_hierarchy")
865
+ operator_raw_data.get("ocr_results")
866
+ fused_xml = state.latest_ui_hierarchy if hasattr(state, "latest_ui_hierarchy") else []
867
+ state.latest_screenshot if hasattr(state, "latest_screenshot") else None
868
+
869
+ if not latest_screenshot_b64 or xml_hierarchy is None:
870
+ logger.error(
871
+ "Operator requires perception data in state (populated by Perception node)"
872
+ )
873
+ raise ValueError("Operator requires perception data in state")
874
+
875
+ # 3. Get history and plan
876
+ steps, task_plan = self._get_history_and_plan()
877
+ self._turn_plan_baseline = self._plan_ledger_baseline(task_plan)
878
+ self._turn_ledger_streak = unwritten_action_streak(steps)
879
+
880
+ # 4. Format minimal list
881
+
882
+ width = 1080
883
+ height = 2400
884
+ w_raw = operator_raw_data.get("width")
885
+ h_raw = operator_raw_data.get("height")
886
+ if isinstance(w_raw, int) and isinstance(h_raw, int):
887
+ width = w_raw
888
+ height = h_raw
889
+ else:
890
+ try:
891
+ if self.ctx and hasattr(self.ctx, "device") and self.ctx.device:
892
+ w = getattr(self.ctx.device, "device_width", 1080)
893
+ h = getattr(self.ctx.device, "device_height", 2400)
894
+ if isinstance(w, int):
895
+ width = w
896
+ if isinstance(h, int):
897
+ height = h
898
+ except (AttributeError, TypeError):
899
+ pass
900
+ minimal_list, elements, labels = format_minimal_list_with_elements(fused_xml, width, height)
901
+
902
+ # Save transient indexed_points and indexed_elements on the state instance
903
+ state.indexed_points = [el["center"] for el in elements]
904
+ state.indexed_elements = elements
905
+
906
+ current_step_num = len(steps) + 1
907
+
908
+ # 5. Prepare Tools. Device-action shells come from the canonical manifest
909
+ # (artemis/mcp/action_specs.py) and are assembled against the installed
910
+ # actuator backend's capabilities: an action the backend does not implement
911
+ # is simply never declared (and the prompt assembly drops its teaching
912
+ # segments in lockstep).
913
+ available_actions = self._available_device_actions()
914
+ all_tools = [
915
+ operator_shell_tool(name) for name in OPERATOR_SHELL_ORDER if name in available_actions
916
+ ] + self.tools
917
+
918
+ # 5. Mount the task-output analyzer and collect background ADB task state.
919
+ # The analyzer is always available: the "output truncated, use
920
+ # analyze_task_output" hint arrives mid-turn, and tools are bound once per
921
+ # turn, so a conditional mount would be one turn late.
922
+ if not any(t.name == "analyze_task_output" for t in all_tools):
923
+ all_tools.append(analyze_task_output_wrapper.tool_fn_getter(self.ctx))
924
+
925
+ traced_tools = [trace_langchain_tool(t, self.ctx) for t in all_tools]
926
+
927
+ adb_registry = get_adb_task_registry(self.ctx)
928
+ active_background_tasks = adb_registry.active_task_summaries()
929
+ newly_finished_tasks = adb_registry.pop_unnotified_finished()
930
+
931
+ # 6. Prepare LLM
932
+ llm = get_llm(ctx=self.ctx, name="operator")
933
+
934
+ # 7. Build Prompt
935
+ messages = await self._build_prompt(
936
+ state,
937
+ latest_screenshot_b64,
938
+ fused_xml,
939
+ minimal_list,
940
+ current_step_num,
941
+ steps,
942
+ task_plan,
943
+ active_background_tasks=active_background_tasks,
944
+ newly_finished_tasks=newly_finished_tasks,
945
+ )
946
+
947
+ # 8. Invoke LLM Loop
948
+ new_subagent_calls = []
949
+ (
950
+ action_result,
951
+ raw_thinking,
952
+ native_thinking,
953
+ tool_limit_exceeded,
954
+ ) = await self._invoke_llm_loop(
955
+ base_llm=llm,
956
+ current_messages=messages,
957
+ traced_tools=traced_tools,
958
+ new_subagent_calls=new_subagent_calls,
959
+ state=state,
960
+ )
961
+ self._previous_turn_silent = not (raw_thinking and raw_thinking.strip())
962
+
963
+ # 8b. Transcript path: hold this turn's messages (observation tail +
964
+ # tool-loop products) for commit at the next build, when the turn's
965
+ # step id and validator result exist.
966
+ self._stage_transcript_turn(messages)
967
+
968
+ # 9. Update State
969
+ structured_decisions = json.dumps(action_result) if action_result else None
970
+
971
+ return {
972
+ "structured_decisions": structured_decisions,
973
+ "operator_raw_thinking": raw_thinking,
974
+ "operator_native_thinking": native_thinking,
975
+ "indexed_points": state.indexed_points,
976
+ "indexed_elements": state.indexed_elements,
977
+ "current_step_id": state.current_step_id,
978
+ "subagent_calls": ((state.subagent_calls or []) + new_subagent_calls),
979
+ "operator_tool_limit_exceeded": tool_limit_exceeded,
980
+ }
981
+
982
+ def _plan_ledger_gate_enabled(self) -> bool:
983
+ try:
984
+ from artemis.config import load_agent_config
985
+
986
+ return bool(load_agent_config().pro.execution.plan_ledger_gate)
987
+ except (AttributeError, OSError, TypeError, ValueError):
988
+ return True
989
+
990
+ def _read_task_plan_text(self) -> str | None:
991
+ """Current ``task_plan`` note text, or ``None`` when it cannot be read."""
992
+ engine = getattr(self.ctx, "data_engine", None)
993
+ if not engine:
994
+ return None
995
+ try:
996
+ path = get_note_file_path(engine.base_dir, "task_plan")
997
+ if not path.exists():
998
+ return None
999
+ return path.read_text(encoding="utf-8")
1000
+ except (AttributeError, OSError, TypeError) as e:
1001
+ logger.warning(f"Plan ledger gate: failed to read task_plan: {e}")
1002
+ return None
1003
+
1004
+ def _plan_ledger_baseline(self, task_plan: str) -> str | None:
1005
+ """Return the plan text used as the current turn's baseline."""
1006
+ if not self._plan_ledger_gate_enabled() or not getattr(self.ctx, "data_engine", None):
1007
+ return None
1008
+ if not parse_plan(task_plan).has_top_level:
1009
+ return None
1010
+ return task_plan
1011
+
1012
+ def _plan_ledger_stale_turns(self) -> int:
1013
+ try:
1014
+ from artemis.config import load_agent_config
1015
+
1016
+ return int(load_agent_config().pro.execution.plan_ledger_stale_turns)
1017
+ except (AttributeError, OSError, TypeError, ValueError):
1018
+ return 4
1019
+
1020
+ def _plan_ledger_bounce_reason(self) -> str | None:
1021
+ """Bounce message when the (unchanged) ledger is broken or stale, else ``None``."""
1022
+ current = self._read_task_plan_text()
1023
+ if current is None:
1024
+ return None
1025
+ return render_plan_ledger_bounce(
1026
+ current, self._turn_ledger_streak, self._plan_ledger_stale_turns()
1027
+ )
1028
+
1029
+ def _plan_changed_this_turn(self) -> bool | None:
1030
+ """Whether ``task_plan`` differs from the turn's baseline (``None``: gate inactive)."""
1031
+ baseline = self._turn_plan_baseline
1032
+ if baseline is None:
1033
+ return None
1034
+ current = self._read_task_plan_text()
1035
+ if current is None:
1036
+ return None
1037
+ return current != baseline
1038
+
1039
+ def _get_active_subgoal_hash(self) -> str:
1040
+ """Parses task_plan.md to find the active top-level subgoal hash."""
1041
+
1042
+ if not self.ctx.data_engine:
1043
+ return "default"
1044
+
1045
+ task_plan_path = get_note_file_path(self.ctx.data_engine.base_dir, "task_plan")
1046
+
1047
+ if not task_plan_path.exists():
1048
+ return "default"
1049
+
1050
+ try:
1051
+ content = task_plan_path.read_text(encoding="utf-8")
1052
+
1053
+ parent_hash, _ = get_active_subgoal_hashes(content)
1054
+ return parent_hash
1055
+ except Exception as e:
1056
+ logger.error(f"Failed to parse active subgoal: {e}")
1057
+
1058
+ return "default"
1059
+
1060
+ def _build_plan_and_history(self, steps: list[dict[str, Any]], task_plan: str) -> str:
1061
+ """Builds a plan checklist and separate execution history."""
1062
+
1063
+ active_subgoal_hash = self._get_active_subgoal_hash()
1064
+ return build_history_for(
1065
+ "operator",
1066
+ task_plan,
1067
+ steps,
1068
+ active_subgoal_hash,
1069
+ last_n_detailed=self.last_n_detailed,
1070
+ )
1071
+
1072
+ def _max_burst_actions(self) -> int:
1073
+ """Configured ceiling for actions in one fast-action burst (default 4)."""
1074
+ try:
1075
+ from artemis.config import load_agent_config
1076
+
1077
+ return int(load_agent_config().pro.execution.max_burst_actions)
1078
+ except Exception:
1079
+ return 4
1080
+
1081
+ def _translate_and_validate_tool(self, tc: dict, state: State) -> tuple[list[dict], str | None]:
1082
+ actions, err = self._translate_and_validate_tool_inner(tc, state)
1083
+ if err:
1084
+ return [], err
1085
+ return actions, None
1086
+
1087
+ def _translate_and_validate_tool_inner(
1088
+ self, tc: dict, state: State
1089
+ ) -> tuple[list[dict], str | None]:
1090
+ tool_name = tc["name"].split(":")[-1] if ":" in tc["name"] else tc["name"]
1091
+ args = tc["args"]
1092
+
1093
+ # Get device resolution fallbacks
1094
+ width = 1080
1095
+ height = 2400
1096
+ operator_raw_data = getattr(state, "operator_raw_data", {}) or {}
1097
+ w_raw = operator_raw_data.get("width")
1098
+ h_raw = operator_raw_data.get("height")
1099
+ if isinstance(w_raw, int) and isinstance(h_raw, int):
1100
+ width = w_raw
1101
+ height = h_raw
1102
+ else:
1103
+ try:
1104
+ if self.ctx and hasattr(self.ctx, "device") and self.ctx.device:
1105
+ w = getattr(self.ctx.device, "device_width", 1080)
1106
+ h = getattr(self.ctx.device, "device_height", 2400)
1107
+ if isinstance(w, int):
1108
+ width = w
1109
+ if isinstance(h, int):
1110
+ height = h
1111
+ except (AttributeError, TypeError):
1112
+ pass
1113
+
1114
+ def resolve_target_element(target: Any, description: Any) -> tuple[dict | None, str | None]:
1115
+ """Resolves a target to ``{"center": [x, y], **semantics}``.
1116
+
1117
+ An element index carries observed semantics (text, bounds, id, class
1118
+ straight from the indexed list); a coordinate pair carries only the
1119
+ model's own ``target_description``. The two never mix.
1120
+ """
1121
+ if isinstance(target, (int, float)):
1122
+ try:
1123
+ target_int = int(target)
1124
+ except (ValueError, TypeError):
1125
+ return None, f"Error: Invalid target index {target}."
1126
+ indexed_elements = getattr(state, "indexed_elements", None) or []
1127
+ if 1 <= target_int <= len(indexed_elements):
1128
+ el = indexed_elements[target_int - 1]
1129
+ return {
1130
+ "center": el.get("center"),
1131
+ "target_text": el.get("text"),
1132
+ "target_bounds": el.get("bounds"),
1133
+ "target_resource_id": el.get("resource_id"),
1134
+ "target_class": el.get("class"),
1135
+ }, None
1136
+ return (
1137
+ None,
1138
+ (
1139
+ f"Error: Invalid target index {target_int}. Active"
1140
+ f" index range is 1 to {len(indexed_elements)}."
1141
+ ),
1142
+ )
1143
+ elif isinstance(target, list) and len(target) == 2:
1144
+ described = _described(description)
1145
+ if described is None:
1146
+ return (
1147
+ None,
1148
+ (
1149
+ f"Error: Coordinate target {target} requires"
1150
+ " 'target_description' (what the element is, in a few"
1151
+ " words). Provide it, or address the element by index."
1152
+ ),
1153
+ )
1154
+ try:
1155
+ nx, ny = map(float, target)
1156
+ x = int(max(0, min(width - 1, nx * width / 1000)))
1157
+ y = int(max(0, min(height - 1, ny * height / 1000)))
1158
+ return {"center": [x, y], "target_description": described}, None
1159
+ except (ValueError, TypeError):
1160
+ return (
1161
+ None,
1162
+ (f"Error: Invalid coordinates {target}. Must be list of 2 numbers."),
1163
+ )
1164
+ return (
1165
+ None,
1166
+ (
1167
+ f"Error: Invalid target parameter {target}. Must be index"
1168
+ " (int/float) or coordinates [x, y]."
1169
+ ),
1170
+ )
1171
+
1172
+ def target_fields(el: dict) -> dict:
1173
+ """The recorded target semantics: observed (index) or described (coords)."""
1174
+ return {k: v for k, v in el.items() if k != "center"}
1175
+
1176
+ if tool_name == "click":
1177
+ target = args.get("target")
1178
+ times = args.get("times", 1)
1179
+ delay_ms = args.get("delay_ms", 100)
1180
+ el, err = resolve_target_element(target, args.get("target_description"))
1181
+ if err:
1182
+ return [], err
1183
+ norm_c = [
1184
+ int(round(el["center"][0] * 1000.0 / width)),
1185
+ int(round(el["center"][1] * 1000.0 / height)),
1186
+ ]
1187
+ return [
1188
+ {
1189
+ "action": "tap",
1190
+ "coordinates": el["center"],
1191
+ COORDINATE_SPACE_KEY: COORDINATE_SPACE_PIXEL,
1192
+ "normalized_coordinates": norm_c,
1193
+ "times": times,
1194
+ "delay_ms": delay_ms,
1195
+ **target_fields(el),
1196
+ }
1197
+ ], None
1198
+
1199
+ elif tool_name == "long_press":
1200
+ target = args.get("target")
1201
+ duration = args.get("duration", 1000)
1202
+ el, err = resolve_target_element(target, args.get("target_description"))
1203
+ if err:
1204
+ return [], err
1205
+ norm_c = [
1206
+ int(round(el["center"][0] * 1000.0 / width)),
1207
+ int(round(el["center"][1] * 1000.0 / height)),
1208
+ ]
1209
+ return [
1210
+ {
1211
+ "action": "long_press_on",
1212
+ "coordinates": el["center"],
1213
+ COORDINATE_SPACE_KEY: COORDINATE_SPACE_PIXEL,
1214
+ "normalized_coordinates": norm_c,
1215
+ "duration": duration,
1216
+ **target_fields(el),
1217
+ }
1218
+ ], None
1219
+
1220
+ elif tool_name == "input_text":
1221
+ text = args.get("text")
1222
+ target = args.get("target")
1223
+ clear_exist = args.get("clear_exist", True)
1224
+
1225
+ if text is None:
1226
+ return [], "Error: Missing required argument 'text'."
1227
+ if not isinstance(text, str):
1228
+ return (
1229
+ [],
1230
+ (f"Error: 'text' must be a string, got {type(text).__name__}."),
1231
+ )
1232
+
1233
+ el, err = resolve_target_element(target, args.get("target_description"))
1234
+ if err:
1235
+ return [], err
1236
+
1237
+ norm_c = [
1238
+ int(round(el["center"][0] * 1000.0 / width)),
1239
+ int(round(el["center"][1] * 1000.0 / height)),
1240
+ ]
1241
+ actions = []
1242
+ actions.append(
1243
+ {
1244
+ "action": "focus_and_input_text",
1245
+ "coordinates": el["center"],
1246
+ COORDINATE_SPACE_KEY: COORDINATE_SPACE_PIXEL,
1247
+ "normalized_coordinates": norm_c,
1248
+ "text": text,
1249
+ "clear_before_input": clear_exist,
1250
+ **target_fields(el),
1251
+ }
1252
+ )
1253
+ return actions, None
1254
+
1255
+ elif tool_name == "swipe":
1256
+ kind, target, parsed_duration = parse_swipe_parameters(args, default_duration=None)
1257
+ duration = args.get("duration")
1258
+
1259
+ if duration is not None:
1260
+ if not isinstance(duration, (int, float)):
1261
+ try:
1262
+ duration = int(duration)
1263
+ except (ValueError, TypeError):
1264
+ return (
1265
+ [],
1266
+ (f"Error: 'duration' must be a number, got {type(duration).__name__}."),
1267
+ )
1268
+ else:
1269
+ duration = int(duration)
1270
+ else:
1271
+ duration = parsed_duration
1272
+
1273
+ # Only a coordinate gesture needs the model to say what it drags; a
1274
+ # directional scroll targets the page, not an element.
1275
+ described: str | None = None
1276
+ if kind == "direction":
1277
+ x1, y1, x2, y2, smart_dur = compute_smart_swipe_coordinates(
1278
+ direction=target,
1279
+ target=args.get("target"),
1280
+ indexed_elements=getattr(state, "indexed_elements", None),
1281
+ ui_hierarchy=getattr(state, "latest_ui_hierarchy", None),
1282
+ width=width,
1283
+ height=height,
1284
+ duration=duration,
1285
+ )
1286
+ if duration is None:
1287
+ duration = smart_dur
1288
+ elif kind == "coords" and isinstance(target, list) and len(target) == 4:
1289
+ described = _described(args.get("target_description"))
1290
+ if described is None:
1291
+ return (
1292
+ [],
1293
+ (
1294
+ f"Error: Coordinate swipe {target} requires"
1295
+ " 'target_description' (what is being dragged, in a few"
1296
+ " words). Provide it, or use a directional swipe."
1297
+ ),
1298
+ )
1299
+ try:
1300
+ nx1, ny1, nx2, ny2 = map(float, target)
1301
+ x1 = int(max(0, min(width - 1, nx1 * width / 1000)))
1302
+ y1 = int(max(0, min(height - 1, ny1 * height / 1000)))
1303
+ x2 = int(max(0, min(width - 1, nx2 * width / 1000)))
1304
+ y2 = int(max(0, min(height - 1, ny2 * height / 1000)))
1305
+ except (ValueError, TypeError):
1306
+ return (
1307
+ [],
1308
+ f"Error: Invalid custom swipe coordinates {target}.",
1309
+ )
1310
+ else:
1311
+ return (
1312
+ [],
1313
+ (
1314
+ f"Error: Invalid swipe gesture parameters {args}. Must be"
1315
+ " direction string or custom coordinates."
1316
+ ),
1317
+ )
1318
+
1319
+ # Calculate duration automatically if not provided (safe range 350-900ms to eliminate Android inertial fling)
1320
+ if duration is None:
1321
+ dist = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
1322
+ if dist <= 300:
1323
+ duration = 350
1324
+ elif dist <= 600:
1325
+ duration = 550
1326
+ elif dist <= 1000:
1327
+ duration = 800
1328
+ else:
1329
+ duration = 900
1330
+
1331
+ nx1, ny1 = int(round(x1 * 1000.0 / width)), int(round(y1 * 1000.0 / height))
1332
+ nx2, ny2 = int(round(x2 * 1000.0 / width)), int(round(y2 * 1000.0 / height))
1333
+ return [
1334
+ {
1335
+ "action": "swipe",
1336
+ "coordinates": [x1, y1, x2, y2],
1337
+ COORDINATE_SPACE_KEY: COORDINATE_SPACE_PIXEL,
1338
+ "normalized_coordinates": [nx1, ny1, nx2, ny2],
1339
+ "normalized_start_coordinates": [nx1, ny1],
1340
+ "normalized_end_coordinates": [nx2, ny2],
1341
+ "duration": duration,
1342
+ # A directional scroll keeps the direction the agent issued so
1343
+ # history and replays render "Swiped up (...)" rather than a
1344
+ # bare coordinate path.
1345
+ **({"direction": str(target)} if kind == "direction" else {}),
1346
+ **({"target_description": described} if described else {}),
1347
+ }
1348
+ ], None
1349
+
1350
+ elif tool_name == "press_key":
1351
+ key = args.get("key") or args.get("keycode")
1352
+ if not key:
1353
+ return [], "Error: Missing required argument 'key'."
1354
+ if not isinstance(key, str):
1355
+ return (
1356
+ [],
1357
+ f"Error: 'key' must be a string, got {type(key).__name__}.",
1358
+ )
1359
+
1360
+ normalized_key = key.strip().upper()
1361
+ if normalized_key.startswith("KEYCODE_"):
1362
+ normalized_key = normalized_key[len("KEYCODE_") :]
1363
+ if normalized_key not in SUPPORTED_PRESS_KEYS:
1364
+ return (
1365
+ [],
1366
+ f"Error: Unsupported key '{key}'. Supported keys:"
1367
+ f" {', '.join(SUPPORTED_PRESS_KEYS)}.",
1368
+ )
1369
+ return [{"action": "press_key", "keycode": f"KEYCODE_{normalized_key}"}], None
1370
+
1371
+ elif tool_name == "manage_app":
1372
+ action = args.get("action")
1373
+ app_name = args.get("app_name")
1374
+ if action not in ["launch", "stop"]:
1375
+ return (
1376
+ [],
1377
+ f"Error: Unsupported app management action '{action}'.",
1378
+ )
1379
+ if not app_name or not isinstance(app_name, str):
1380
+ return [], "Error: 'app_name' must be a valid non-empty string."
1381
+ action_str = "launch_app" if action == "launch" else "stop_app"
1382
+ return [{"action": action_str, "app_name": app_name}], None
1383
+
1384
+ elif tool_name == "wait_for_delay":
1385
+ time_in_ms = args.get("time_in_ms")
1386
+ if time_in_ms is None:
1387
+ return [], "Error: Missing required argument 'time_in_ms'."
1388
+ try:
1389
+ time_in_ms = int(time_in_ms)
1390
+ except (ValueError, TypeError):
1391
+ return [], f"Error: Invalid time_in_ms value {time_in_ms}."
1392
+ return [{"action": "wait_for_delay", "time_in_ms": time_in_ms}], None
1393
+
1394
+ return [], f"Error: Unsupported tool name '{tool_name}'."