android-midscene-automation 0.2.4 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +159 -29
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +4 -2
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +12 -0
  507. package/src/App.vue +10 -5
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +340 -121
  525. package/src/types.ts +1 -1
@@ -0,0 +1,623 @@
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 base64
16
+ from datetime import datetime
17
+ import hashlib
18
+ import json
19
+ import logging
20
+ import os
21
+ from pathlib import Path
22
+ import subprocess
23
+ import threading
24
+ import time
25
+ from typing import Any
26
+ import urllib.parse
27
+ from fastapi import HTTPException
28
+
29
+ from artemis.config import IMAGES_DIR, TRACES_PATH, WORKSPACE_ROOT
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class MediaService:
35
+ """Service handling media indexing, local file security, payload unwrapping and notes/plans."""
36
+
37
+ # Conversion results keyed by source path -> (mtime, size, resolved path).
38
+ # Failed conversions are cached too (resolved path == source path) so a broken
39
+ # file doesn't re-run ffmpeg (up to 45s) on every /api/sessions poll.
40
+ _playable_cache: dict[str, tuple[float, int, str]] = {}
41
+ _playable_cache_lock = threading.Lock()
42
+
43
+ @classmethod
44
+ def ensure_browser_playable_video(cls, p: Path) -> Path:
45
+ """Ensure the video file is in a browser-supported container (MP4).
46
+
47
+ If given an MKV, check if a converted MP4 already exists or convert it.
48
+ """
49
+ try:
50
+ if not p.exists():
51
+ return p
52
+ suffix = p.suffix.lower()
53
+ if suffix == ".mp4":
54
+ return p
55
+ if suffix in (".mkv", ".webm"):
56
+ if cls.is_live_recording(p):
57
+ # scrcpy is still appending to this file: converting now
58
+ # would freeze a few-seconds partial as recording.mp4.
59
+ return p
60
+ src_stat = p.stat()
61
+ cache_key = str(p)
62
+ cached = cls._playable_cache.get(cache_key)
63
+ if cached and cached[0] == src_stat.st_mtime and cached[1] == src_stat.st_size:
64
+ return Path(cached[2])
65
+ mp4_cand = p.with_suffix(".mp4")
66
+ if (
67
+ mp4_cand.exists()
68
+ and mp4_cand.stat().st_size > 0
69
+ and mp4_cand.stat().st_mtime >= src_stat.st_mtime
70
+ ):
71
+ # An MP4 older than its source is a stale partial left by a
72
+ # mid-run scan; fall through and convert again.
73
+ cls._playable_cache[cache_key] = (
74
+ src_stat.st_mtime,
75
+ src_stat.st_size,
76
+ str(mp4_cand),
77
+ )
78
+ return mp4_cand
79
+ with cls._playable_cache_lock:
80
+ # Another thread may have converted this file while we waited.
81
+ cached = cls._playable_cache.get(cache_key)
82
+ if cached and cached[0] == src_stat.st_mtime and cached[1] == src_stat.st_size:
83
+ return Path(cached[2])
84
+ try:
85
+ result = cls._convert_to_mp4(p, mp4_cand)
86
+ except Exception:
87
+ result = p
88
+ cls._playable_cache[cache_key] = (
89
+ src_stat.st_mtime,
90
+ src_stat.st_size,
91
+ str(result),
92
+ )
93
+ return result
94
+ except OSError:
95
+ # Filesystem probe failed (missing/locked file): serve the
96
+ # original path and let the media endpoint report the error.
97
+ pass
98
+ return p
99
+
100
+ @staticmethod
101
+ def _convert_to_mp4(p: Path, mp4_cand: Path) -> Path:
102
+ from artemis.utils.video import get_ffmpeg_path
103
+
104
+ ffmpeg = get_ffmpeg_path()
105
+ # 1. Attempt fast copy remux
106
+ subprocess.run(
107
+ [
108
+ ffmpeg,
109
+ "-y",
110
+ "-fflags",
111
+ "+genpts",
112
+ "-i",
113
+ str(p),
114
+ "-c",
115
+ "copy",
116
+ "-movflags",
117
+ "+faststart",
118
+ str(mp4_cand),
119
+ ],
120
+ capture_output=True,
121
+ timeout=15,
122
+ )
123
+ if mp4_cand.exists() and mp4_cand.stat().st_size > 0:
124
+ return mp4_cand
125
+ # 2. If copy remux failed, fallback to ultrafast re-encode
126
+ subprocess.run(
127
+ [
128
+ ffmpeg,
129
+ "-y",
130
+ "-fflags",
131
+ "+genpts+discardcorrupt",
132
+ "-i",
133
+ str(p),
134
+ "-c:v",
135
+ "libx264",
136
+ "-preset",
137
+ "ultrafast",
138
+ "-pix_fmt",
139
+ "yuv420p",
140
+ "-movflags",
141
+ "+faststart",
142
+ str(mp4_cand),
143
+ ],
144
+ capture_output=True,
145
+ timeout=30,
146
+ )
147
+ if mp4_cand.exists() and mp4_cand.stat().st_size > 0:
148
+ return mp4_cand
149
+ return p
150
+
151
+ LIVE_RECORDING_GRACE_SECONDS = 10.0
152
+ TRACE_STATUS_SUFFIXES = ("_PASS_", "_FAIL_", "_TESTFAIL_")
153
+
154
+ @classmethod
155
+ def is_live_recording(cls, p: Path) -> bool:
156
+ """Whether ``p`` is a raw .mkv that scrcpy is still appending to.
157
+
158
+ scrcpy touches the file continuously while recording; a raw file that
159
+ has not changed for a few seconds belongs to a finished (or killed)
160
+ recording and is safe to convert.
161
+ """
162
+ if p.suffix.lower() != ".mkv":
163
+ return False
164
+ try:
165
+ return (time.time() - p.stat().st_mtime) < cls.LIVE_RECORDING_GRACE_SECONDS
166
+ except OSError:
167
+ return False
168
+
169
+ @staticmethod
170
+ def is_browser_playable(p: Path) -> bool:
171
+ return p.suffix.lower() in (".mp4", ".webm")
172
+
173
+ @classmethod
174
+ def strip_trace_status_suffix(cls, folder_name: str) -> str:
175
+ for marker in cls.TRACE_STATUS_SUFFIXES:
176
+ folder_name = folder_name.split(marker)[0]
177
+ return folder_name
178
+
179
+ @classmethod
180
+ def _finalize_trace_folder_name(cls, folder: Path, session_start_time: float | None) -> Path:
181
+ """Give an unfinished temp trace folder the terminal name the worker uses."""
182
+ try:
183
+ if folder.resolve().parent != Path(TRACES_PATH).resolve():
184
+ return folder
185
+ except OSError:
186
+ return folder
187
+ if any(marker in folder.name for marker in cls.TRACE_STATUS_SUFFIXES):
188
+ return folder
189
+ try:
190
+ stamp = datetime.fromtimestamp(float(session_start_time or time.time())).strftime(
191
+ "%Y-%m-%dT%H-%M-%S"
192
+ )
193
+ except (TypeError, ValueError, OSError, OverflowError):
194
+ stamp = datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
195
+ target = folder.with_name(f"{folder.name}_FAIL_{stamp}")
196
+ if target.exists():
197
+ return folder
198
+ try:
199
+ folder.rename(target)
200
+ except OSError as exc:
201
+ logger.warning("Could not rename orphaned trace folder %s: %s", folder, exc)
202
+ return folder
203
+ return target
204
+
205
+ @classmethod
206
+ def recover_orphaned_recording(
207
+ cls,
208
+ local_video_path: str | Path | None,
209
+ session_start_time: float | None = None,
210
+ ) -> Path | None:
211
+ """Finalize the recording of a worker that died before its own stop path ran.
212
+
213
+ A hard-killed worker leaves the raw scrcpy ``recording.mkv`` behind
214
+ (playable, but never remuxed) plus, possibly, a few-seconds partial
215
+ ``recording.mp4`` that a session-list scan converted mid-run. The raw
216
+ file is remuxed over that partial and removed on success, and the temp
217
+ trace folder is renamed the way the worker would have done it.
218
+
219
+ Returns the final browser-playable path, or None when nothing usable
220
+ exists (the caller then records a terminal recording failure).
221
+ """
222
+ if not local_video_path:
223
+ return None
224
+ source = Path(str(local_video_path))
225
+ folder = source.parent
226
+ if not folder.is_dir():
227
+ return None
228
+ raw = source if source.suffix.lower() == ".mkv" else folder / "recording.mkv"
229
+ mp4 = folder / "recording.mp4"
230
+
231
+ def _non_empty(path: Path) -> bool:
232
+ try:
233
+ return path.is_file() and path.stat().st_size > 0
234
+ except OSError:
235
+ return False
236
+
237
+ if _non_empty(raw):
238
+ if cls.is_live_recording(raw):
239
+ return None
240
+ with cls._playable_cache_lock:
241
+ converted = cls._convert_to_mp4(raw, mp4)
242
+ cls._playable_cache.pop(str(raw), None)
243
+ if converted != mp4 or not _non_empty(mp4):
244
+ return None
245
+ try:
246
+ raw.unlink()
247
+ except OSError:
248
+ pass
249
+ final = mp4
250
+ elif _non_empty(mp4):
251
+ final = mp4
252
+ elif _non_empty(source) and cls.is_browser_playable(source):
253
+ final = source
254
+ else:
255
+ return None
256
+
257
+ folder = cls._finalize_trace_folder_name(folder, session_start_time)
258
+ return folder / final.name
259
+
260
+ @staticmethod
261
+ def path_to_video_url(p: Path) -> str:
262
+ resolved = p.resolve()
263
+ try:
264
+ rel = resolved.relative_to(WORKSPACE_ROOT)
265
+ return f"/videos/{urllib.parse.quote(rel.as_posix(), safe='/')}"
266
+ except ValueError:
267
+ return f"/videos/{urllib.parse.quote(resolved.as_posix().lstrip('/'), safe='/:')}"
268
+
269
+ @classmethod
270
+ def build_video_index(cls) -> dict[str, str]:
271
+ idx = {}
272
+ dirs = [
273
+ TRACES_PATH,
274
+ WORKSPACE_ROOT / "artemis-traces",
275
+ WORKSPACE_ROOT / ".benchmarks" / "diagnoser" / "outputs" / "artifacts",
276
+ ]
277
+ for d in dirs:
278
+ if not d.exists():
279
+ continue
280
+ try:
281
+ entries = list(d.iterdir())
282
+ except OSError as exc:
283
+ logger.warning("Video index scan failed under %s: %s", d, exc)
284
+ continue
285
+ for item in entries:
286
+ # One unreadable entry must not abort the rest of the directory.
287
+ try:
288
+ if item.is_dir():
289
+ for ext in [".mp4", ".mkv", ".webm"]:
290
+ vfile = item / f"recording{ext}"
291
+ if vfile.exists():
292
+ vfile = cls.ensure_browser_playable_video(vfile)
293
+ if not cls.is_browser_playable(vfile):
294
+ # Live (still recording) or unconvertible raw file.
295
+ break
296
+ url = cls.path_to_video_url(vfile)
297
+ idx[item.name] = url
298
+ prefix = cls.strip_trace_status_suffix(item.name)
299
+ idx[prefix] = url
300
+ break
301
+ elif item.suffix in [".mp4", ".mkv", ".webm"]:
302
+ item = cls.ensure_browser_playable_video(item)
303
+ if not cls.is_browser_playable(item):
304
+ continue
305
+ url = cls.path_to_video_url(item)
306
+ idx[item.stem] = url
307
+ idx[item.name] = url
308
+ except OSError as exc:
309
+ logger.warning("Skipping unindexable video entry %s: %s", item, exc)
310
+ return idx
311
+
312
+ @classmethod
313
+ def resolve_video_url(
314
+ cls,
315
+ row_dict: dict[str, Any],
316
+ video_rec_map: dict[str, str],
317
+ video_idx: dict[str, str],
318
+ ) -> str | None:
319
+ raw_sid = row_dict.get("session_id")
320
+ s_id = (
321
+ str(raw_sid).strip()
322
+ if raw_sid is not None and str(raw_sid).strip().lower() != "none"
323
+ else ""
324
+ )
325
+ v_url = None
326
+ v_fp = row_dict.get("video_filepath")
327
+ if v_fp and os.path.exists(v_fp):
328
+ p = cls.ensure_browser_playable_video(Path(v_fp))
329
+ if cls.is_browser_playable(p):
330
+ v_url = cls.path_to_video_url(p)
331
+
332
+ orig_rec = video_rec_map.get(s_id)
333
+ if not v_url and orig_rec:
334
+ p = Path(orig_rec)
335
+ if p.exists():
336
+ p = cls.ensure_browser_playable_video(p)
337
+ if cls.is_browser_playable(p):
338
+ v_url = cls.path_to_video_url(p)
339
+ if not v_url:
340
+ folder_name = p.parent.name
341
+ v_url = video_idx.get(folder_name) or video_idx.get(folder_name.split("_")[0])
342
+
343
+ if not v_url:
344
+ v_url = video_idx.get(s_id)
345
+
346
+ if not v_url and s_id:
347
+ sess_dir = TRACES_PATH / s_id
348
+ if sess_dir.is_dir():
349
+ for ext in [".mp4", ".webm", ".mkv"]:
350
+ vfile = sess_dir / f"recording{ext}"
351
+ if vfile.exists():
352
+ vfile = cls.ensure_browser_playable_video(vfile)
353
+ if cls.is_browser_playable(vfile):
354
+ v_url = cls.path_to_video_url(vfile)
355
+ break
356
+ if not v_url:
357
+ try:
358
+ for item in sess_dir.iterdir():
359
+ if item.is_file() and item.suffix.lower() in [".mp4", ".webm", ".mkv"]:
360
+ item = cls.ensure_browser_playable_video(item)
361
+ if not cls.is_browser_playable(item):
362
+ continue
363
+ v_url = cls.path_to_video_url(item)
364
+ break
365
+ except OSError:
366
+ # Unreadable session dir: no recording to resolve.
367
+ pass
368
+
369
+ if not v_url and row_dict.get("initial_goal"):
370
+ goal_str = row_dict["initial_goal"]
371
+ task_hint = (
372
+ goal_str.split("Task: ")[1].split()[0].strip()
373
+ if "Task: " in goal_str
374
+ else goal_str.split("\n")[0].strip()
375
+ )
376
+ v_url = video_idx.get(task_hint)
377
+
378
+ return v_url
379
+
380
+ @classmethod
381
+ def resolve_video_segments(cls, video_url: str | None) -> list[dict[str, Any]]:
382
+ """Resolve a recording sidecar manifest into browser-safe segment URLs."""
383
+ if not video_url or not video_url.startswith("/videos/"):
384
+ return []
385
+ relative = urllib.parse.unquote(video_url.removeprefix("/videos/"))
386
+ video_path = (WORKSPACE_ROOT / relative).resolve()
387
+ manifest_path = video_path.parent / "recording.json"
388
+ if not manifest_path.is_file():
389
+ return []
390
+ try:
391
+ payload = json.loads(manifest_path.read_text(encoding="utf-8"))
392
+ segments = []
393
+ for item in payload.get("segments", []):
394
+ segment_path = (manifest_path.parent / str(item["file"])).resolve()
395
+ if (
396
+ segment_path.parent != manifest_path.parent.resolve()
397
+ or not segment_path.is_file()
398
+ ):
399
+ continue
400
+ segment = {
401
+ "url": cls.path_to_video_url(segment_path),
402
+ "start": float(item.get("start", 0)),
403
+ "duration": float(item.get("duration", 0)),
404
+ "width": int(item.get("width", 0)),
405
+ "height": int(item.get("height", 0)),
406
+ }
407
+ # Manifest v2 carries the real session-relative timeline so the
408
+ # UI can seek by step time across scrcpy restart gaps.
409
+ if item.get("offset_ms") is not None and item.get("duration_ms") is not None:
410
+ segment["offset_ms"] = int(item["offset_ms"])
411
+ segment["duration_ms"] = int(item["duration_ms"])
412
+ segments.append(segment)
413
+ return segments
414
+ except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError):
415
+ return []
416
+
417
+ @staticmethod
418
+ def unwrap_payload(obj: Any) -> Any:
419
+ """Unwraps trace payloads, storing base64 images as disk files and
420
+ truncating large dumps.
421
+ """
422
+ if isinstance(obj, str):
423
+ try:
424
+ if (obj.startswith("{") and obj.endswith("}")) or (
425
+ obj.startswith("[") and obj.endswith("]")
426
+ ):
427
+ return MediaService.unwrap_payload(json.loads(obj))
428
+ except ValueError:
429
+ # Looks like JSON but is not: treat it as a plain string.
430
+ pass
431
+
432
+ # Check if the string is a Base64 image
433
+ is_base64_img = False
434
+ base64_data = None
435
+ if obj.startswith("data:image/") and ";base64," in obj:
436
+ try:
437
+ _, base64_data = obj.split(";base64,", 1)
438
+ is_base64_img = True
439
+ except ValueError:
440
+ # Malformed data URI: fall through to plain-string handling.
441
+ pass
442
+ elif obj.startswith("iVBORw0KGgo") or obj.startswith("/9j/"):
443
+ is_base64_img = True
444
+ base64_data = obj
445
+
446
+ if is_base64_img and base64_data:
447
+ try:
448
+ image_bytes = base64.b64decode(base64_data)
449
+ image_hash = hashlib.sha256(image_bytes).hexdigest()
450
+
451
+ IMAGES_DIR.mkdir(parents=True, exist_ok=True)
452
+ image_path = IMAGES_DIR / f"{image_hash}.jpg"
453
+ if not image_path.exists():
454
+ image_path.write_bytes(image_bytes)
455
+
456
+ return f"image://{image_hash}"
457
+ except (ValueError, OSError) as exc:
458
+ # Invalid base64 or image cache write failure: fall back
459
+ # to the truncated raw string below.
460
+ logger.debug("Could not persist inline image: %s", exc)
461
+
462
+ if len(obj) > 2000:
463
+ preview_start = obj[:100].replace("\n", " ")
464
+ preview_end = obj[-100:].replace("\n", " ")
465
+ return (
466
+ f"<Massive String: {preview_start}...[truncated"
467
+ f" {len(obj) - 200} characters for UI performance]...{preview_end}"
468
+ f" (length={len(obj)})>"
469
+ )
470
+ return obj
471
+ elif isinstance(obj, dict):
472
+ return {k: MediaService.unwrap_payload(v) for k, v in obj.items()}
473
+ elif isinstance(obj, list):
474
+ if len(obj) > 20 and any(
475
+ isinstance(x, dict) and ("bounds" in x or "resource-id" in x) for x in obj
476
+ ):
477
+ return [f"<XML UI List with {len(obj)} elements truncated for UI performance>"]
478
+ return [MediaService.unwrap_payload(x) for x in obj]
479
+ return obj
480
+
481
+ _LOCAL_FILE_MEDIA_TYPES = {
482
+ ".jpg": "image/jpeg",
483
+ ".jpeg": "image/jpeg",
484
+ ".png": "image/png",
485
+ ".gif": "image/gif",
486
+ ".webp": "image/webp",
487
+ ".mp4": "video/mp4",
488
+ ".webm": "video/webm",
489
+ ".mkv": "video/x-matroska",
490
+ }
491
+
492
+ @classmethod
493
+ def get_safe_local_file(cls, path_str: str) -> tuple[Path, str]:
494
+ """Serve trace media only: resolved path must sit inside the workspace
495
+ or traces tree AND carry a media extension, so this endpoint can never
496
+ hand out .env, databases, or source files."""
497
+ decoded_path = urllib.parse.unquote(path_str)
498
+ if decoded_path.startswith("file://"):
499
+ decoded_path = decoded_path[len("file://") :]
500
+ try:
501
+ p = Path(decoded_path).resolve(strict=True)
502
+ except (OSError, ValueError):
503
+ raise HTTPException(status_code=404, detail="File not found")
504
+
505
+ if not p.is_file():
506
+ raise HTTPException(status_code=404, detail="File not found")
507
+
508
+ allowed_roots = []
509
+ for root in (WORKSPACE_ROOT, TRACES_PATH):
510
+ try:
511
+ allowed_roots.append(Path(root).resolve())
512
+ except OSError:
513
+ continue
514
+ if not any(p.is_relative_to(root) for root in allowed_roots):
515
+ raise HTTPException(
516
+ status_code=403,
517
+ detail="Access denied. Path is outside workspace root.",
518
+ )
519
+
520
+ media_type = cls._LOCAL_FILE_MEDIA_TYPES.get(p.suffix.lower())
521
+ if media_type is None:
522
+ raise HTTPException(
523
+ status_code=403,
524
+ detail="Access denied. Only media files are served.",
525
+ )
526
+
527
+ return p, media_type
528
+
529
+ @staticmethod
530
+ def get_task_plan_content(session_id: str) -> str:
531
+ plan_path = TRACES_PATH / session_id / "notes" / "task_plan.md"
532
+ if not plan_path.exists():
533
+ plan_path = TRACES_PATH / "notes" / "task_plan.md"
534
+
535
+ if plan_path.exists():
536
+ try:
537
+ return plan_path.read_text(encoding="utf-8")
538
+ except Exception as e:
539
+ return f"Error reading task plan: {e}"
540
+ return "No task plan created yet."
541
+
542
+ @staticmethod
543
+ def _read_jsonl_records(path: Path, label: str, session_id: str) -> list[dict[str, Any]]:
544
+ records: list[dict[str, Any]] = []
545
+ if not path.is_file():
546
+ return records
547
+ try:
548
+ for line in path.read_text(encoding="utf-8").splitlines():
549
+ line = line.strip()
550
+ if not line:
551
+ continue
552
+ try:
553
+ rec = json.loads(line)
554
+ except ValueError:
555
+ continue
556
+ if isinstance(rec, dict):
557
+ records.append(rec)
558
+ except (OSError, ValueError) as e:
559
+ logger.warning(f"Failed to read {label} for {session_id}: {e}")
560
+ return records
561
+
562
+ @staticmethod
563
+ def get_session_checks(session_id: str) -> dict[str, Any]:
564
+ """Checker material of one session for UI backfill: the append-only
565
+ verdict ledger (``check_ledger.jsonl``), the per-attempt transcripts of
566
+ the Checker's streamed reasoning (``check_streams.jsonl``: one record
567
+ per attempt with timestamped ``segments``) and the machine-readable run
568
+ outcome (``run_outcome.json``), all written by the Checker /
569
+ ``artemis.graph.checkpoints``. Missing files mean "no checks ran"
570
+ (sessions recorded before transcripts existed simply have no streams).
571
+ """
572
+ session_dir = TRACES_PATH / session_id
573
+ records = MediaService._read_jsonl_records(
574
+ session_dir / "check_ledger.jsonl", "check ledger", session_id
575
+ )
576
+ streams = MediaService._read_jsonl_records(
577
+ session_dir / "check_streams.jsonl", "check streams", session_id
578
+ )
579
+
580
+ run_outcome: dict[str, Any] | None = None
581
+ outcome_path = session_dir / "run_outcome.json"
582
+ if outcome_path.is_file():
583
+ try:
584
+ loaded = json.loads(outcome_path.read_text(encoding="utf-8"))
585
+ if isinstance(loaded, dict):
586
+ run_outcome = loaded
587
+ except (OSError, ValueError) as e:
588
+ logger.warning(f"Failed to read run outcome for {session_id}: {e}")
589
+
590
+ return {"records": records, "streams": streams, "run_outcome": run_outcome}
591
+
592
+ @staticmethod
593
+ def get_session_notes_content(session_id: str) -> dict[str, str]:
594
+ notes_dir = TRACES_PATH / session_id / "notes"
595
+ notes_content = {}
596
+
597
+ if notes_dir.exists() and notes_dir.is_dir():
598
+ for file in notes_dir.iterdir():
599
+ if file.is_file() and file.suffix in [".md", ".txt", ".json", ".yaml"]:
600
+ try:
601
+ notes_content[file.name] = file.read_text(encoding="utf-8")
602
+ except Exception as e:
603
+ notes_content[file.name] = f"Error reading file: {e}"
604
+
605
+ if not notes_content:
606
+ global_notes_dir = TRACES_PATH / "notes"
607
+ if global_notes_dir.exists() and global_notes_dir.is_dir():
608
+ for file in global_notes_dir.iterdir():
609
+ if file.is_file() and file.suffix in [
610
+ ".md",
611
+ ".txt",
612
+ ".json",
613
+ ".yaml",
614
+ ]:
615
+ try:
616
+ notes_content[file.name] = file.read_text(encoding="utf-8")
617
+ except Exception as e:
618
+ notes_content[file.name] = f"Error reading file: {e}"
619
+
620
+ return notes_content
621
+
622
+
623
+ media_service = MediaService()