android-midscene-automation 0.2.3 → 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 +11 -1
  2. package/README.md +63 -1
  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 +10 -8
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +13 -1
  507. package/src/App.vue +72 -23
  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 +369 -186
  525. package/src/types.ts +1 -1
@@ -0,0 +1,437 @@
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
+ """Native Gemini execution path for a single video chunk sub-agent.
16
+
17
+ Extracted from ``video_analyzer.py`` as a pure structural split. Patchable
18
+ collaborators (``get_controller``, ``compress_video_for_api``, semaphores,
19
+ LLM event recorders, ...) are looked up late through the facade module so
20
+ ``mock.patch("artemis.agents.video_analyzer.video_analyzer.<name>")`` targets
21
+ keep working. The sub-agent conversation itself lives in
22
+ ``chunk_conversation``.
23
+ """
24
+
25
+ import asyncio
26
+ from pathlib import Path
27
+
28
+ from artemis.agents.video_analyzer import video_analyzer as _va
29
+ from artemis.agents.video_analyzer.chunk_agentic import run_agentic_chunk_conversation
30
+ from artemis.agents.video_analyzer.chunk_conversation import (
31
+ _ChunkMedia,
32
+ _run_native_chunk_conversation,
33
+ )
34
+ from artemis.agents.video_analyzer.reliability import (
35
+ AgenticVideoDegraded,
36
+ classify_video_failure,
37
+ is_agentic_rejection,
38
+ )
39
+ from artemis.llm.reliability import retry_policy_for
40
+ from third_party.mobile_use.utils.logger import get_logger
41
+
42
+ logger = get_logger(__name__)
43
+
44
+
45
+ async def exec_single_chunk(
46
+ analyzer, start_time: float, end_time: float | None, specific_query: str
47
+ ) -> str:
48
+ """Analyzes one closed chunk interval, with claim/lease and retry handling."""
49
+ max_retries = analyzer.native_max_retries
50
+ current_start = round(start_time, 1) if isinstance(start_time, (int, float)) else start_time
51
+ current_end = round(end_time, 1) if isinstance(end_time, (int, float)) else end_time
52
+
53
+ if not isinstance(current_start, (int, float)) or not isinstance(current_end, (int, float)):
54
+ raise ValueError("Child video analysis requires a closed numeric interval")
55
+
56
+ device_id = getattr(getattr(analyzer.ctx, "device", None), "device_id", None)
57
+ active_session = _va.get_active_session(device_id) if device_id else None
58
+ claim = analyzer.blackboard.claim_segment(
59
+ current_start,
60
+ current_end,
61
+ specific_query,
62
+ model_name=analyzer.model_name,
63
+ source_generation=(active_session.generation if active_session is not None else None),
64
+ )
65
+ if claim.state == "cached":
66
+ cached = (
67
+ f"[from {current_start:.1f}s to {current_end:.1f}s] Summary: "
68
+ f"{claim.summary or 'No summary provided.'}"
69
+ )
70
+ if claim.analysis:
71
+ cached += f" Analysis: {claim.analysis}"
72
+ return cached
73
+ if claim.state == "in_progress":
74
+ return (
75
+ f"[from {current_start:.1f}s to {current_end:.1f}s] "
76
+ "Analysis is already in progress in another video agent; do not duplicate it."
77
+ )
78
+ lease_owner = claim.lease_owner
79
+ if lease_owner is None:
80
+ raise RuntimeError("Video blackboard returned a claim without a lease owner")
81
+
82
+ media = _ChunkMedia()
83
+
84
+ attempt = 0
85
+ while attempt <= max_retries:
86
+ # The mode this attempt was launched in: a parallel chunk may degrade
87
+ # the run while this one is still in flight.
88
+ attempt_mode = analyzer.video_processing
89
+ try:
90
+ logger.info(
91
+ f"Attempt {attempt + 1}/{max_retries + 1} for sub-agent:"
92
+ f" start={current_start}, end={current_end}"
93
+ )
94
+ # A sibling may switch the run to static during queuing or media
95
+ # preparation. Check the size before preparation and before dispatch.
96
+ _ensure_fits_static_ceiling(analyzer, current_start, current_end)
97
+ result, slowdown_factor = await _prepare_chunk_media(
98
+ analyzer, media, current_start, current_end
99
+ )
100
+ media.prompt_with_context = _build_chunk_prompt(
101
+ analyzer,
102
+ result,
103
+ media.actual_start,
104
+ slowdown_factor,
105
+ start_time,
106
+ end_time,
107
+ specific_query,
108
+ )
109
+ _ensure_fits_static_ceiling(analyzer, current_start, current_end)
110
+
111
+ if not analyzer.use_native_gemini:
112
+ chunk_result = await analyzer._exec_single_chunk_universal(
113
+ compressed_path=media.compressed_path,
114
+ raw_path=media.path,
115
+ start_time=current_start,
116
+ end_time=current_end,
117
+ actual_start=media.actual_start,
118
+ prompt_with_context=media.prompt_with_context,
119
+ specific_query=specific_query,
120
+ )
121
+ summary, analysis = analyzer._parse_chunk_result(chunk_result)
122
+ analyzer.blackboard.complete_segment(
123
+ current_start,
124
+ current_end,
125
+ specific_query,
126
+ lease_owner,
127
+ summary,
128
+ analysis,
129
+ )
130
+ return chunk_result
131
+
132
+ if analyzer.video_processing == "agentic":
133
+ return await run_agentic_chunk_conversation(
134
+ analyzer,
135
+ media,
136
+ current_start,
137
+ current_end,
138
+ start_time,
139
+ end_time,
140
+ specific_query,
141
+ lease_owner,
142
+ )
143
+
144
+ return await _run_native_chunk_conversation(
145
+ analyzer,
146
+ media,
147
+ current_start,
148
+ current_end,
149
+ start_time,
150
+ end_time,
151
+ specific_query,
152
+ lease_owner,
153
+ )
154
+
155
+ except Exception as e:
156
+ # Retry decisions are owned by the shared classification layer.
157
+ # Every attempt restarts from scratch (segment re-extracted,
158
+ # file re-uploaded, stream re-consumed); partially accumulated
159
+ # stream output from the failed attempt is discarded, never
160
+ # surfaced as a complete answer.
161
+ failure = classify_video_failure(e)
162
+ if attempt_mode == "agentic" and is_agentic_rejection(failure, e):
163
+ # Switch to static and let the coordinator resize the interval.
164
+ _degrade_agentic_to_static(analyzer, e, failure)
165
+ return await _finish_failed_chunk(
166
+ analyzer,
167
+ media,
168
+ current_start,
169
+ current_end,
170
+ specific_query,
171
+ lease_owner,
172
+ AgenticVideoDegraded(
173
+ f"Agentic video processing rejected ({e}); re-plan"
174
+ f" {current_start:.1f}s-{current_end:.1f}s at the static chunk size"
175
+ ),
176
+ retryable=False,
177
+ )
178
+ attempts_so_far = attempt + 1
179
+ if not failure.retryable:
180
+ logger.warning(
181
+ f"Sub-agent non-retryable failure [{failure.category.value}]"
182
+ f" on attempt {attempts_so_far}: {e}"
183
+ )
184
+ return await _finish_failed_chunk(
185
+ analyzer,
186
+ media,
187
+ current_start,
188
+ current_end,
189
+ specific_query,
190
+ lease_owner,
191
+ e,
192
+ retryable=False,
193
+ )
194
+ policy = retry_policy_for(failure.category)
195
+ allowed_attempts = min(max_retries + 1, policy.max_attempts)
196
+ if attempts_so_far >= allowed_attempts:
197
+ logger.warning(
198
+ f"Sub-agent exhausted {attempts_so_far} attempt(s)"
199
+ f" [{failure.category.value}]: {e}"
200
+ )
201
+ return await _finish_failed_chunk(
202
+ analyzer,
203
+ media,
204
+ current_start,
205
+ current_end,
206
+ specific_query,
207
+ lease_owner,
208
+ e,
209
+ )
210
+ delay = policy.delay_for(attempts_so_far)
211
+ logger.warning(
212
+ f"Sub-agent retryable failure [{failure.category.value}]"
213
+ f" ({attempts_so_far}/{allowed_attempts}): {e}; retrying"
214
+ f" from scratch in {delay:.2f}s (partial stream output"
215
+ " from the failed attempt is discarded)"
216
+ )
217
+ _va._record_llm_retry(
218
+ str(e),
219
+ delay,
220
+ attempt=attempts_so_far,
221
+ max_retries=allowed_attempts,
222
+ source="video_sub_agent",
223
+ )
224
+ await asyncio.sleep(delay)
225
+ attempt += 1
226
+
227
+ raise RuntimeError(
228
+ f"Video sub-agent exhausted retries for {current_start:.1f}s-{current_end:.1f}s"
229
+ )
230
+
231
+
232
+ def _degrade_agentic_to_static(analyzer, error: Exception, failure) -> None:
233
+ """Switches the rest of this run to static video understanding (idempotent)."""
234
+ if analyzer.video_processing != "agentic":
235
+ return
236
+ logger.warning(
237
+ f"Agentic video processing rejected ({error}); the rest of this run"
238
+ " analyzes video in static mode"
239
+ )
240
+ _va._record_llm_event(
241
+ "llm_fallback",
242
+ {
243
+ "reason": "agentic_video_rejected",
244
+ "category": failure.category.value,
245
+ "error": str(error)[:500],
246
+ },
247
+ )
248
+ analyzer.video_processing = "static"
249
+ analyzer.chunk_size_seconds = analyzer.static_chunk_size_seconds
250
+
251
+
252
+ def _ensure_fits_static_ceiling(analyzer, current_start: float, current_end: float) -> None:
253
+ """Refuses to analyze a clip sized for the agentic ceiling once the run is static.
254
+
255
+ Raises :class:`AgenticVideoDegraded`, which the attempt loop treats as a
256
+ non-retryable failure: the lease is released via ``fail_segment`` and the
257
+ coordinator re-plans the interval into static-sized pieces.
258
+ """
259
+ if analyzer.video_processing == "agentic":
260
+ return
261
+ if current_end - current_start <= analyzer.chunk_size_seconds + 1e-3:
262
+ return
263
+ raise AgenticVideoDegraded(
264
+ f"Run degraded to static video processing while {current_start:.1f}s-{current_end:.1f}s"
265
+ f" was pending; re-plan it at the static chunk size"
266
+ f" ({analyzer.chunk_size_seconds:.0f}s)"
267
+ )
268
+
269
+
270
+ async def _finish_failed_chunk(
271
+ analyzer,
272
+ media: _ChunkMedia,
273
+ current_start: float,
274
+ current_end: float,
275
+ specific_query: str,
276
+ lease_owner: str,
277
+ error: Exception,
278
+ *,
279
+ retryable: bool | None = None,
280
+ ):
281
+ """Records terminal failure, attempting the universal fallback first when eligible."""
282
+ failure = classify_video_failure(error)
283
+ if (
284
+ analyzer.use_native_gemini
285
+ and failure.should_fallback
286
+ and media.path is not None
287
+ and media.compressed_path is not None
288
+ and media.actual_start is not None
289
+ and media.prompt_with_context is not None
290
+ ):
291
+ try:
292
+ logger.warning("Native video chunk exhausted retries; using universal fallback")
293
+ _va._record_llm_event(
294
+ "llm_fallback",
295
+ {
296
+ "reason": "native_video_chunk_failed",
297
+ "category": failure.category.value,
298
+ "error": str(error)[:500],
299
+ },
300
+ )
301
+ fallback_result = await analyzer._exec_single_chunk_universal(
302
+ compressed_path=media.compressed_path,
303
+ raw_path=media.path,
304
+ start_time=current_start,
305
+ end_time=current_end,
306
+ actual_start=media.actual_start,
307
+ prompt_with_context=media.prompt_with_context,
308
+ specific_query=specific_query,
309
+ force_fallback=True,
310
+ )
311
+ summary, analysis = analyzer._parse_chunk_result(fallback_result)
312
+ analyzer.blackboard.complete_segment(
313
+ current_start,
314
+ current_end,
315
+ specific_query,
316
+ lease_owner,
317
+ summary,
318
+ analysis,
319
+ )
320
+ return fallback_result
321
+ except Exception as fallback_error:
322
+ error = fallback_error
323
+ failure = classify_video_failure(error)
324
+ retryable = None
325
+ analyzer.blackboard.fail_segment(
326
+ current_start,
327
+ current_end,
328
+ specific_query,
329
+ lease_owner,
330
+ str(error),
331
+ retryable=failure.retryable if retryable is None else retryable,
332
+ error_category=failure.category.value,
333
+ )
334
+ raise error
335
+
336
+
337
+ async def _prepare_chunk_media(
338
+ analyzer, media: _ChunkMedia, current_start: float, current_end: float
339
+ ):
340
+ """Extracts and compresses the chunk segment, tracking artifacts for cleanup."""
341
+ controller = _va.get_controller(analyzer.ctx)
342
+ async with _va.TRANSCODE_SEMAPHORE:
343
+ result = await controller.extract_segment_metadata(current_start, current_end)
344
+ if not result.success or not result.video_path:
345
+ raise Exception(f"Failed to get video segment: {result.message}")
346
+
347
+ path = Path(result.video_path)
348
+ analyzer.local_files_to_cleanup.add(path)
349
+ analyzer.local_dirs_to_cleanup.add(path.parent)
350
+ media.path = path
351
+
352
+ actual_start = getattr(result, "actual_start_relative_time", current_start)
353
+ if not isinstance(actual_start, (int, float)):
354
+ actual_start = current_start
355
+ media.actual_start = actual_start
356
+
357
+ slowdown_factor = 1.0
358
+ if current_end is not None and (current_end - current_start) <= _va.SLOWDOWN_THRESHOLD_SECONDS:
359
+ slowdown_factor = _va.SLOWDOWN_FACTOR
360
+
361
+ async with _va.TRANSCODE_SEMAPHORE:
362
+ compressed_path = await _va.compress_video_for_api(
363
+ path,
364
+ force_compress=True,
365
+ start_offset_seconds=actual_start,
366
+ slowdown_factor=slowdown_factor,
367
+ )
368
+ if compressed_path != path:
369
+ analyzer.local_files_to_cleanup.add(compressed_path)
370
+ analyzer.local_dirs_to_cleanup.add(compressed_path.parent)
371
+ media.compressed_path = compressed_path
372
+
373
+ actual_start = getattr(result, "actual_start_relative_time", current_start)
374
+ if not isinstance(actual_start, (int, float)):
375
+ actual_start = current_start
376
+ media.actual_start = actual_start
377
+ return result, slowdown_factor
378
+
379
+
380
+ def _build_chunk_prompt(
381
+ analyzer,
382
+ result,
383
+ actual_start: float,
384
+ slowdown_factor: float,
385
+ start_time: float,
386
+ end_time: float | None,
387
+ specific_query: str,
388
+ ) -> str:
389
+ """Assembles the sub-agent prompt with timing context, ledger, and warnings."""
390
+ warnings = analyzer.get_overlapping_warnings(start_time, end_time)
391
+ warning_block = ""
392
+ if warnings:
393
+ lines = ["WARNING: The following queries were already tried in this timeframe:\n"]
394
+ for w in warnings:
395
+ lines.append(f"Searched for: {w['target']} -> {w['summary']}")
396
+ warning_block = "\n".join(lines) + "\n\n"
397
+
398
+ duration_secs = getattr(result, "duration_seconds", None)
399
+ actual_end = actual_start + duration_secs if isinstance(duration_secs, (int, float)) else None
400
+ end_str = f" to {actual_end:.1f}s" if actual_end is not None else ""
401
+ warning_val = getattr(result, "warning", None)
402
+ truncation_note = (
403
+ f" (NOTE: {warning_val})" if isinstance(warning_val, str) and warning_val else ""
404
+ )
405
+
406
+ slowdown_note = ""
407
+ if slowdown_factor != 1.0:
408
+ slowdown_note = "WARNING: This video is slowed down to capture fast micro-actions.\n\n"
409
+
410
+ prompt_with_context = (
411
+ "IMPORTANT CONTEXT: This video segment corresponds to"
412
+ " the test's relative time from"
413
+ f" {actual_start:.1f}s{end_str}{truncation_note}. A"
414
+ " timestamp showing the exact test relative time"
415
+ " (formatted as '<seconds> s') is burned in the"
416
+ " top-right corner of the video. Please watch this"
417
+ " burned-in timestamp and use its value to report all"
418
+ " timestamps (e.g., if the burned timestamp shows '90"
419
+ f" s', report 90.0s).\n\n{slowdown_note}"
420
+ )
421
+ if analyzer.enable_ledger:
422
+ prompt_with_context += (
423
+ "BLACKBOARD"
424
+ f" LEDGER:\n{analyzer.blackboard_ledger}\nDirective:"
425
+ " Use the Blackboard to understand what happened"
426
+ " outside your assigned timeframe. Do not"
427
+ " duplicate findings already on the board.\n\n"
428
+ )
429
+ if warning_block:
430
+ prompt_with_context += (
431
+ f"{warning_block}Directive: Avoid repeating"
432
+ " searches that have already failed. Try to"
433
+ " approach the problem from a different angle or"
434
+ " use a different search strategy.\n\n"
435
+ )
436
+ prompt_with_context += f"{specific_query}"
437
+ return prompt_with_context
@@ -0,0 +1,137 @@
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 logging
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ConflictResolutionService:
21
+ @staticmethod
22
+ def calculate_1d_iou(a, b):
23
+ start = max(a[0], b[0])
24
+ end = min(a[1], b[1])
25
+ intersection = max(0.0, end - start)
26
+ union = (a[1] - a[0]) + (b[1] - b[0]) - intersection
27
+ return intersection / (union + 1e-6)
28
+
29
+ @staticmethod
30
+ def calculate_1d_iomin(a, b):
31
+ start = max(a[0], b[0])
32
+ end = min(a[1], b[1])
33
+ intersection = max(0.0, end - start)
34
+ min_len = min(a[1] - a[0], b[1] - b[0])
35
+ if min_len <= 0:
36
+ return 0.0
37
+ return intersection / min_len
38
+
39
+ @staticmethod
40
+ def sanitize_interval(interval):
41
+ start = interval.get("start", 0.0)
42
+ end = interval.get("end", "unknown")
43
+
44
+ if isinstance(start, (int, float)):
45
+ start = max(0.0, float(start))
46
+
47
+ if end != "unknown" and isinstance(end, (int, float)):
48
+ end = max(0.0, float(end))
49
+ if start > end:
50
+ start, end = end, start
51
+
52
+ interval["start"] = start
53
+ interval["end"] = end
54
+
55
+ if "confidence_score" not in interval or interval["confidence_score"] is None:
56
+ interval["confidence_score"] = 0.0
57
+
58
+ if end != "unknown":
59
+ interval["duration"] = float(end) - float(start)
60
+ else:
61
+ interval["duration"] = 0.0
62
+
63
+ return interval
64
+
65
+ @staticmethod
66
+ def group_by_semantic_class(proposals):
67
+ groups = {}
68
+ for p in proposals:
69
+ target = p.get("target", "unknown")
70
+ if target not in groups:
71
+ groups[target] = []
72
+ groups[target].append(p)
73
+ return groups
74
+
75
+ @staticmethod
76
+ def apply_1d_nms(group_proposals):
77
+ if len(group_proposals) <= 1:
78
+ return group_proposals
79
+
80
+ unknown_end_proposals = [p for p in group_proposals if p.get("end") == "unknown"]
81
+ valid_proposals = [p for p in group_proposals if p.get("end") != "unknown"]
82
+
83
+ valid_proposals.sort(
84
+ key=lambda x: (
85
+ x.get("confidence_score", 0.0),
86
+ -x.get("duration", 0.0),
87
+ ),
88
+ reverse=True,
89
+ )
90
+
91
+ n = len(valid_proposals)
92
+ suppressed_mask = [False] * n
93
+
94
+ for i in range(n):
95
+ if suppressed_mask[i]:
96
+ continue
97
+ a = (valid_proposals[i]["start"], valid_proposals[i]["end"])
98
+ for j in range(i + 1, n):
99
+ if suppressed_mask[j]:
100
+ continue
101
+ b = (valid_proposals[j]["start"], valid_proposals[j]["end"])
102
+ iou = ConflictResolutionService.calculate_1d_iou(a, b)
103
+ iomin = ConflictResolutionService.calculate_1d_iomin(a, b)
104
+
105
+ if iou > 0.5 or iomin > 0.8:
106
+ suppressed_mask[j] = True
107
+
108
+ kept_proposals = [valid_proposals[i] for i in range(n) if not suppressed_mask[i]]
109
+ return kept_proposals + unknown_end_proposals
110
+
111
+ @classmethod
112
+ def clean(cls, blackboard_entries):
113
+ try:
114
+ sanitized = [cls.sanitize_interval(dict(e)) for e in blackboard_entries]
115
+ groups = cls.group_by_semantic_class(sanitized)
116
+
117
+ cleaned_entries = []
118
+ for target, proposals in groups.items():
119
+ if len(proposals) <= 1:
120
+ cleaned_entries.extend(proposals)
121
+ else:
122
+ cleaned_entries.extend(cls.apply_1d_nms(proposals))
123
+
124
+ suppressed_count = len(blackboard_entries) - len(cleaned_entries)
125
+ if suppressed_count > 0:
126
+ logger.info(
127
+ "NMS filtering telemetry: Successfully suppressed"
128
+ f" {suppressed_count} redundant proposals."
129
+ )
130
+
131
+ # Sort chronologically by start time before returning so downstream systems read it in order
132
+ cleaned_entries.sort(key=lambda x: float(x.get("start", 0.0)))
133
+
134
+ return cleaned_entries
135
+ except Exception as e:
136
+ logger.error(f"NMS filter crashed: {e}. Falling back to raw ledger state.")
137
+ return blackboard_entries