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,1021 @@
1
+ /**
2
+ * Copyright 2026 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { StepBlock, PhaseBlock, StepEvent, StreamSegment, StreamResetNotice, DEFAULT_STREAM_RESET_MESSAGE, PersistedCheckerStream } from '../core/models/stream.model';
18
+ import { isAndroidAction, isReportStatusAction, getReportStatusExplanation, getReportStatusValue, getActionObject } from './action-formatter.util';
19
+ import { getUniqueGenericTools, isInternalPlumbingTool } from './tool-formatter.util';
20
+
21
+ /**
22
+ * Helper to safely extract milliseconds timestamp
23
+ */
24
+ export function getItemTimestamp(ts: any, fallback: number = 0): number {
25
+ if (ts !== undefined && ts !== null) {
26
+ if (typeof ts === 'number') {
27
+ return ts < 1e11 ? ts * 1000 : ts;
28
+ }
29
+ const parsed = new Date(ts).getTime();
30
+ if (!isNaN(parsed) && parsed > 0) return parsed;
31
+ }
32
+ return fallback;
33
+ }
34
+
35
+ /**
36
+ * Index of the `checker` block that owns a trace id (the Checker's own agent
37
+ * trace). The Checker runs concurrently with Operator steps, so its streamed
38
+ * reasoning and tool traces are routed by `parent_trace_id`, never by the
39
+ * step that happens to be current.
40
+ */
41
+ function findCheckerBlockByTrace(blocks: StepBlock[], parentTraceId: any): number {
42
+ if (!parentTraceId) return -1;
43
+ const key = String(parentTraceId);
44
+ return blocks.findIndex(b => b.type === 'checker' && b.data?.trace_id && String(b.data.trace_id) === key);
45
+ }
46
+
47
+ /** Latest still-running checker block (fallback when only the agent name is known). */
48
+ function findRunningCheckerBlock(blocks: StepBlock[]): number {
49
+ for (let i = blocks.length - 1; i >= 0; i--) {
50
+ if (blocks[i].type === 'checker' && blocks[i].data?.isCompleted === false) return i;
51
+ }
52
+ return -1;
53
+ }
54
+
55
+ /**
56
+ * Rebuild the live `StreamSegment` list of a Checker attempt from its
57
+ * persisted transcript, so a reopened session renders the same interleaved
58
+ * Thought/Work segments as the live stream did.
59
+ */
60
+ export function persistedStreamToSegments(record: PersistedCheckerStream | null | undefined): StreamSegment[] {
61
+ if (!record || !Array.isArray(record.segments)) return [];
62
+ const segments: StreamSegment[] = [];
63
+ for (const seg of record.segments) {
64
+ if (!seg || typeof seg.text !== 'string' || !seg.text) continue;
65
+ const when = typeof seg.when === 'number' && seg.when > 0 ? seg.when : (record.ts || 0);
66
+ segments.push({
67
+ execution_id: String(seg.execution_id || ''),
68
+ stream_type: seg.role === 'thought' ? 'thinking' : 'text',
69
+ text: seg.text,
70
+ timestamp: new Date(when * 1000).toISOString(),
71
+ isCompleted: true
72
+ });
73
+ }
74
+ return segments;
75
+ }
76
+
77
+ function segmentKey(segment: StreamSegment): string {
78
+ return `${segment.execution_id}|${segment.stream_type}`;
79
+ }
80
+
81
+ /**
82
+ * Merge persisted segments into the ones a block already holds. Live chunks
83
+ * (complete by the time the transcript exists) win over the backfill copy of
84
+ * the same turn; turns only the transcript knows are added, then everything
85
+ * is kept in first-chunk order.
86
+ */
87
+ function mergeStreamSegments(existing: any, incoming: any): StreamSegment[] | undefined {
88
+ const base: StreamSegment[] = Array.isArray(existing) ? [...existing] : [];
89
+ const extra: StreamSegment[] = Array.isArray(incoming) ? incoming : [];
90
+ if (extra.length === 0) return base.length > 0 ? base : undefined;
91
+ const seen = new Set(base.map(segmentKey));
92
+ let added = false;
93
+ for (const segment of extra) {
94
+ const key = segmentKey(segment);
95
+ if (seen.has(key)) continue;
96
+ seen.add(key);
97
+ base.push(segment);
98
+ added = true;
99
+ }
100
+ if (added) {
101
+ base.sort((a, b) => getItemTimestamp(a.timestamp, 0) - getItemTimestamp(b.timestamp, 0));
102
+ }
103
+ return base;
104
+ }
105
+
106
+ /**
107
+ * Keep the flat `operator_*_thinking` fields (joined text + first-seen time)
108
+ * in step with the segment list for consumers that only know steps.
109
+ */
110
+ function applyFlatStreamFields(data: any, segments: StreamSegment[]): void {
111
+ const of = (type: string) => segments.filter((s) => s.stream_type === type && s.text.trim());
112
+ const joined = (type: string) => of(type).map((s) => s.text).join('\n\n');
113
+ data.operator_native_thinking = joined('thinking');
114
+ data.operator_raw_thinking = joined('text');
115
+ const firstThinking = of('thinking')[0];
116
+ const firstText = of('text')[0];
117
+ if (firstThinking && !data.operator_native_thinking_timestamp) {
118
+ data.operator_native_thinking_timestamp = firstThinking.timestamp;
119
+ }
120
+ if (firstText && !data.operator_raw_thinking_timestamp) {
121
+ data.operator_raw_thinking_timestamp = firstText.timestamp;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Apply one `checker_event` (attempt_started / attempt_finished / run_outcome)
127
+ * to the block list. Attempts are keyed by attempt id so live events and the
128
+ * ledger backfill merge instead of duplicating.
129
+ */
130
+ function applyCheckerLog(blocks: StepBlock[], log: any): void {
131
+ const d = log.data || {};
132
+ if (!d.event) return;
133
+
134
+ if (d.event === 'run_outcome') {
135
+ const id = 'checker-outcome';
136
+ const idx = blocks.findIndex(b => b.id === id);
137
+ const data = {
138
+ ...(idx > -1 ? blocks[idx].data : {}),
139
+ ...d,
140
+ phase: 'outcome',
141
+ status: 'done',
142
+ isCompleted: true,
143
+ generic_tools: []
144
+ };
145
+ const block: StepBlock = { id, type: 'checker', timestamp: log.timestamp, data };
146
+ if (idx > -1) blocks[idx] = block; else blocks.push(block);
147
+ return;
148
+ }
149
+
150
+ if (!d.attempt_id) return;
151
+ const id = `checker-${d.attempt_id}`;
152
+ const idx = blocks.findIndex(b => b.id === id);
153
+ const existing = idx > -1 ? blocks[idx] : null;
154
+ const ts = typeof d.ts === 'number' ? d.ts : (log.timestamp ? new Date(log.timestamp).getTime() / 1000 : undefined);
155
+
156
+ if (d.event === 'attempt_started') {
157
+ const data = {
158
+ ...(existing?.data || {}),
159
+ ...d,
160
+ // A late "started" must not resurrect a finished attempt.
161
+ status: existing?.data?.isCompleted ? existing.data.status : 'running',
162
+ isCompleted: existing?.data?.isCompleted ?? false,
163
+ started_at: existing?.data?.started_at ?? ts,
164
+ verdicts: existing?.data?.verdicts || [],
165
+ findings: existing?.data?.findings || [],
166
+ generic_tools: existing?.data?.generic_tools || []
167
+ };
168
+ const block: StepBlock = { id, type: 'checker', timestamp: existing?.timestamp || log.timestamp, data };
169
+ if (existing) blocks[idx] = block; else blocks.push(block);
170
+ return;
171
+ }
172
+
173
+ if (d.event === 'attempt_finished') {
174
+ const startedAt = existing?.data?.started_at ?? ts;
175
+ const verdicts = Array.isArray(d.verdicts) ? d.verdicts : (existing?.data?.verdicts || []);
176
+ const items = existing?.data?.items?.length
177
+ ? existing.data.items
178
+ : verdicts.map((v: any) => ({ kind: v.kind, text: v.item_text, when: v.when }));
179
+ // The ledger backfill carries the attempt's persisted transcript as
180
+ // ready-made segments; a live block keeps the ones it streamed.
181
+ const streamSegments = mergeStreamSegments(existing?.data?.stream_segments, d.stream_segments);
182
+ const data: any = {
183
+ ...(existing?.data || {}),
184
+ ...d,
185
+ status: d.status || 'done',
186
+ verdicts,
187
+ items,
188
+ findings: Array.isArray(d.findings) ? d.findings : (existing?.data?.findings || []),
189
+ generic_tools: existing?.data?.generic_tools || [],
190
+ stream_segments: streamSegments,
191
+ started_at: startedAt,
192
+ finished_at: ts,
193
+ duration: startedAt !== undefined && ts !== undefined ? Math.max(0, ts - startedAt) : undefined,
194
+ isCompleted: true
195
+ };
196
+ if (streamSegments) applyFlatStreamFields(data, streamSegments);
197
+ const block: StepBlock = { id, type: 'checker', timestamp: existing?.timestamp || log.timestamp, data };
198
+ if (existing) blocks[idx] = block; else blocks.push(block);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Persisted steps list the Checker's tool traces (and its agent trace) under
204
+ * the Operator step that was current while it ran, because traces are stored
205
+ * by step id. Once the checker blocks exist (ledger backfill carries the
206
+ * attempt's trace id), move those traces to the attempt they belong to and
207
+ * drop the agent trace itself.
208
+ */
209
+ function relocateCheckerTools(blocks: StepBlock[]): void {
210
+ const checkerByTrace = new Map<string, number>();
211
+ blocks.forEach((b, i) => {
212
+ if (b.type === 'checker' && b.data?.trace_id) {
213
+ checkerByTrace.set(String(b.data.trace_id), i);
214
+ }
215
+ });
216
+ if (checkerByTrace.size === 0) return;
217
+
218
+ blocks.forEach((block, i) => {
219
+ if (block.type === 'checker') return;
220
+ const tools: any[] = block.data?.generic_tools || [];
221
+ if (tools.length === 0) return;
222
+ const keep: any[] = [];
223
+ let moved = false;
224
+ for (const tool of tools) {
225
+ const ownTrace = tool?.trace_id ? String(tool.trace_id) : '';
226
+ const parent = tool?.parent_trace_id ? String(tool.parent_trace_id) : '';
227
+ if (ownTrace && checkerByTrace.has(ownTrace) && tool?.type === 'agent') {
228
+ moved = true; // the Checker's own agent trace: not a tool of the step
229
+ continue;
230
+ }
231
+ const target = parent ? checkerByTrace.get(parent) : undefined;
232
+ if (target === undefined) {
233
+ keep.push(tool);
234
+ continue;
235
+ }
236
+ const checker = blocks[target];
237
+ const existing: any[] = checker.data.generic_tools || [];
238
+ if (!existing.some((t) => t.trace_id && t.trace_id === tool.trace_id)) {
239
+ blocks[target] = { ...checker, data: { ...checker.data, generic_tools: [...existing, tool] } };
240
+ }
241
+ moved = true;
242
+ }
243
+ if (moved) {
244
+ blocks[i] = { ...block, data: { ...block.data, generic_tools: keep } };
245
+ }
246
+ });
247
+ }
248
+
249
+ /**
250
+ * A `hierarchy_backend` log trace that records a *switch* (helper -> UIAutomator2
251
+ * or back). The initial "source: ..." line belongs to the startup block; only
252
+ * the mid-run change is worth a row in the timeline.
253
+ */
254
+ export function isBackendSwitchNote(tool: any): boolean {
255
+ return tool?.type === 'log'
256
+ && tool?.name === 'hierarchy_backend'
257
+ && Boolean(tool?.payload?.previous_backend);
258
+ }
259
+
260
+ /** Consolidate raw SSE logs into deduplicated and ordered StepBlocks. */
261
+ export function consolidateLogsToBlocks(rawLogs: any[]): StepBlock[] {
262
+ if (!rawLogs || rawLogs.length === 0) return [];
263
+
264
+ const blocks: StepBlock[] = [];
265
+
266
+ rawLogs.forEach(log => {
267
+ if (!log) return;
268
+
269
+ if (log.type === 'checker_event') {
270
+ applyCheckerLog(blocks, log);
271
+ return;
272
+ }
273
+
274
+ if (log.type === 'llm_stream') {
275
+ const execId = log.data?.execution_id || log.id;
276
+ const stepId = log.data?.step_id;
277
+ const streamType = log.data?.stream_type || 'text';
278
+ const text = log.data?.text || '';
279
+ const isCompleted = log.data?.isCompleted ?? false;
280
+ const isReset = log.data?.isReset ?? false;
281
+ const resetMessage = log.data?.resetMessage;
282
+
283
+ // The Checker's own reasoning streams under its agent trace: route it to
284
+ // the attempt block, never to the Operator step that is current.
285
+ const checkerIndex = findCheckerBlockByTrace(blocks, log.data?.parent_trace_id);
286
+ if (checkerIndex > -1) {
287
+ const checkerBlock = blocks[checkerIndex];
288
+ const checkerData: any = { ...checkerBlock.data };
289
+ // The Checker is a multi-turn tool loop: every LLM turn streams under
290
+ // its own execution id. Keep one segment per turn (with its first-seen
291
+ // time) so the timeline can interleave the text with the tool calls it
292
+ // triggered, instead of collapsing all turns into one early block.
293
+ const segments: StreamSegment[] = Array.isArray(checkerData.stream_segments)
294
+ ? [...checkerData.stream_segments]
295
+ : [];
296
+ const segmentIndex = segments.findIndex(
297
+ (s) => s.execution_id === execId && s.stream_type === streamType
298
+ );
299
+ const segment: StreamSegment = {
300
+ execution_id: execId,
301
+ stream_type: streamType === 'thinking' ? 'thinking' : 'text',
302
+ text,
303
+ timestamp: segmentIndex > -1 ? segments[segmentIndex].timestamp : log.timestamp,
304
+ isCompleted,
305
+ isReset,
306
+ resetMessage
307
+ };
308
+ if (segmentIndex > -1) segments[segmentIndex] = segment; else segments.push(segment);
309
+ checkerData.stream_segments = segments;
310
+ // Flat fields stay as the joined text for consumers that only know steps.
311
+ if (streamType === 'thinking') {
312
+ checkerData.operator_native_thinking_timestamp =
313
+ checkerData.operator_native_thinking_timestamp || log.timestamp;
314
+ } else {
315
+ checkerData.operator_raw_thinking_timestamp =
316
+ checkerData.operator_raw_thinking_timestamp || log.timestamp;
317
+ }
318
+ applyFlatStreamFields(checkerData, segments);
319
+ if (execId && !checkerData.execution_id) {
320
+ checkerData.execution_id = execId;
321
+ }
322
+ blocks[checkerIndex] = { ...checkerBlock, data: checkerData };
323
+ return;
324
+ }
325
+
326
+ // Find existing block: match by stepId if available, or by execId, or active incomplete block
327
+ let existingIndex = -1;
328
+ if (stepId) {
329
+ existingIndex = blocks.findIndex(b => b.id === `step-${stepId}` || b.data?.step_id === stepId);
330
+ }
331
+ if (existingIndex === -1 && execId) {
332
+ existingIndex = blocks.findIndex(b => b.data?.execution_id === execId || b.id === `stream-${execId}`);
333
+ }
334
+ // If no stepId and not matched by execId, check if there is an unattached stream block that was reset.
335
+ // The retry stream supersedes that reset block rather than spawning an orphaned duplicate card.
336
+ if (existingIndex === -1 && !stepId) {
337
+ existingIndex = blocks.findIndex(b => b.type === 'llm_stream' && b.data?.isReset);
338
+ }
339
+
340
+ if (existingIndex > -1) {
341
+ const existing = blocks[existingIndex];
342
+ const updatedData = { ...existing.data };
343
+ const existingResets: StreamResetNotice[] =
344
+ Array.isArray(updatedData.stream_resets) ? [...updatedData.stream_resets] : [];
345
+
346
+ if (isReset) {
347
+ updatedData.isReset = true;
348
+ updatedData.resetMessage = resetMessage;
349
+ const msg = resetMessage || DEFAULT_STREAM_RESET_MESSAGE;
350
+ const resetId = execId || `reset-${log.timestamp || updatedData.step_id || 'stream'}`;
351
+ const existingResetIdx = existingResets.findIndex(r => r.id === resetId);
352
+ if (existingResetIdx > -1) {
353
+ existingResets[existingResetIdx] = { id: resetId, message: msg, isWaiting: true, streamType };
354
+ } else {
355
+ existingResets.push({ id: resetId, message: msg, isWaiting: true, streamType });
356
+ }
357
+ updatedData.stream_resets = existingResets;
358
+ } else if (execId && updatedData.execution_id !== execId) {
359
+ // New execution replacing a reset stream: mark prior resets as finished waiting
360
+ updatedData.stream_resets = existingResets.map(r => ({ ...r, isWaiting: false }));
361
+ updatedData.isReset = false;
362
+ updatedData.resetMessage = undefined;
363
+ updatedData.execution_id = execId;
364
+ } else if (!isReset && text && text.length > 0 && existingResets.some(r => r.isWaiting)) {
365
+ updatedData.stream_resets = existingResets.map(r => ({ ...r, isWaiting: false }));
366
+ updatedData.isReset = false;
367
+ updatedData.resetMessage = undefined;
368
+ }
369
+
370
+ if (streamType === 'thinking') {
371
+ updatedData.operator_native_thinking = text;
372
+ updatedData.operator_native_thinking_timestamp =
373
+ updatedData.operator_native_thinking_timestamp || log.timestamp;
374
+ } else {
375
+ updatedData.operator_raw_thinking = text;
376
+ updatedData.operator_raw_thinking_timestamp =
377
+ updatedData.operator_raw_thinking_timestamp || log.timestamp;
378
+ }
379
+ if (stepId && !updatedData.step_id) {
380
+ updatedData.step_id = stepId;
381
+ }
382
+ if (execId && !updatedData.execution_id) {
383
+ updatedData.execution_id = execId;
384
+ }
385
+ updatedData.isCompleted = isCompleted;
386
+ if (isCompleted && updatedData.stream_resets) {
387
+ updatedData.stream_resets = updatedData.stream_resets.map((r: any) => ({ ...r, isWaiting: false }));
388
+ }
389
+
390
+ blocks[existingIndex] = {
391
+ ...existing,
392
+ data: updatedData
393
+ };
394
+ } else {
395
+ const blockId = stepId ? `step-${stepId}` : `stream-${execId}`;
396
+ const blockData: any = {
397
+ execution_id: execId,
398
+ step_id: stepId,
399
+ isCompleted: isCompleted,
400
+ isReset: isReset,
401
+ resetMessage: resetMessage,
402
+ stream_resets: isReset ? [{
403
+ id: execId || `reset-${log.timestamp || stepId || 'stream'}`,
404
+ message: resetMessage || DEFAULT_STREAM_RESET_MESSAGE,
405
+ isWaiting: true,
406
+ streamType
407
+ }] : [],
408
+ generic_tools: []
409
+ };
410
+ if (streamType === 'thinking') {
411
+ blockData.operator_native_thinking = text;
412
+ blockData.operator_native_thinking_timestamp = log.timestamp;
413
+ } else {
414
+ blockData.operator_raw_thinking = text;
415
+ blockData.operator_raw_thinking_timestamp = log.timestamp;
416
+ }
417
+
418
+ blocks.push({
419
+ id: blockId,
420
+ type: stepId ? 'step' : 'llm_stream',
421
+ timestamp: log.timestamp,
422
+ data: blockData
423
+ });
424
+ }
425
+ } else if (log.type === 'step_recorded' || log.type === 'step_updated') {
426
+ const stepId = log.data.step_id || log.data.step_number || 'unknown';
427
+ // Find if there is an existing block for this step or an unattached stream block from this turn
428
+ let existingIndex = blocks.findIndex(b => b.id === `step-${stepId}` || b.data?.step_id === stepId);
429
+
430
+ // No adoption of untagged stream blocks here: the Operator's streams
431
+ // always carry the step id (Perception / the Flash turn pre-allocate it
432
+ // before the LLM call), so a stream without one belongs to another agent
433
+ // (Planner, Checker...). A step whose Operator emitted only tool calls
434
+ // and no text used to steal the latest such block -- typically the
435
+ // Planner's -- and render its action inside it at the top of the timeline.
436
+
437
+ if (existingIndex > -1) {
438
+ const existingBlock = blocks[existingIndex];
439
+ const existingTools = existingBlock.data.generic_tools || [];
440
+ const newTools = log.data.generic_tools || [];
441
+ const mergedTools = [...existingTools];
442
+ newTools.forEach((nt: any) => {
443
+ const matchIdx = mergedTools.findIndex(et => et.trace_id && nt.trace_id && et.trace_id === nt.trace_id);
444
+ if (matchIdx > -1) {
445
+ mergedTools[matchIdx] = { ...mergedTools[matchIdx], ...nt };
446
+ } else {
447
+ mergedTools.push(nt);
448
+ }
449
+ });
450
+
451
+ const mergedData = {
452
+ ...existingBlock.data,
453
+ ...log.data,
454
+ operator_native_thinking: log.data.operator_native_thinking || existingBlock.data.operator_native_thinking || undefined,
455
+ operator_raw_thinking: log.data.operator_raw_thinking || existingBlock.data.operator_raw_thinking || undefined,
456
+ operator_native_thinking_timestamp: existingBlock.data.operator_native_thinking_timestamp
457
+ || log.data.operator_native_thinking_timestamp
458
+ || (log.data.operator_native_thinking ? log.timestamp : undefined),
459
+ operator_raw_thinking_timestamp: existingBlock.data.operator_raw_thinking_timestamp
460
+ || log.data.operator_raw_thinking_timestamp
461
+ || (log.data.operator_raw_thinking ? log.timestamp : undefined),
462
+ action_taken: log.data.action_taken || existingBlock.data.action_taken || undefined,
463
+ last_execution_result: log.data.last_execution_result || existingBlock.data.last_execution_result || undefined,
464
+ generic_tools: mergedTools,
465
+ isCompleted: true
466
+ };
467
+ if (mergedData.stream_resets) {
468
+ mergedData.stream_resets = mergedData.stream_resets.map((r: any) => ({ ...r, isWaiting: false }));
469
+ }
470
+
471
+ blocks[existingIndex] = {
472
+ ...existingBlock,
473
+ id: `step-${stepId}`,
474
+ type: 'step',
475
+ timestamp: existingBlock.timestamp || log.timestamp,
476
+ data: mergedData
477
+ };
478
+ } else {
479
+ blocks.push({
480
+ id: `step-${stepId}`,
481
+ type: 'step',
482
+ timestamp: log.timestamp,
483
+ data: {
484
+ ...log.data,
485
+ isCompleted: true,
486
+ operator_native_thinking_timestamp: log.data.operator_native_thinking_timestamp
487
+ || (log.data.operator_native_thinking ? log.timestamp : undefined),
488
+ operator_raw_thinking_timestamp: log.data.operator_raw_thinking_timestamp
489
+ || (log.data.operator_raw_thinking ? log.timestamp : undefined),
490
+ generic_tools: log.data.generic_tools || []
491
+ }
492
+ });
493
+ }
494
+ } else if (log.type === 'trace_recorded') {
495
+ const isAction = log.data.type === 'action';
496
+ const isTool = log.data.type === 'tool' || isAction;
497
+ const isVisibleLLMEvent = log.data.type === 'llm_call'
498
+ && (log.data.status === 'failed' || log.data.status === 'retrying');
499
+ if (!isTool && !isVisibleLLMEvent && !isBackendSwitchNote(log.data)) return;
500
+
501
+ let stepId = log.data.step_id;
502
+ let existingIndex = -1;
503
+
504
+ // 0. Tools invoked by the Checker belong to its attempt block (routed by
505
+ // the parent trace, falling back to the running attempt by agent name).
506
+ let checkerIndex = findCheckerBlockByTrace(blocks, log.data.parent_trace_id);
507
+ if (checkerIndex === -1 && String(log.data.agent_name || '').toLowerCase() === 'checker') {
508
+ checkerIndex = findRunningCheckerBlock(blocks);
509
+ }
510
+ if (checkerIndex > -1) {
511
+ existingIndex = checkerIndex;
512
+ }
513
+
514
+ // 1. If this trace_id already exists in some block, update that block directly
515
+ if (existingIndex === -1 && log.data.trace_id) {
516
+ existingIndex = blocks.findIndex(b =>
517
+ b.data?.generic_tools?.some((t: any) => t.trace_id === log.data.trace_id)
518
+ );
519
+ }
520
+
521
+ // 2. If not found by trace_id and stepId is present, find by step_id
522
+ if (existingIndex === -1 && stepId) {
523
+ existingIndex = blocks.findIndex(b => b.id === `step-${stepId}` || b.data?.step_id === stepId);
524
+ }
525
+
526
+ // 3. If still not found and no stepId (e.g. pre-planning / initial planning phase)
527
+ if (existingIndex === -1 && !stepId) {
528
+ existingIndex = blocks.findIndex(b => b.id === 'step-pre-planning' || b.data?.step_id === 'pre-planning');
529
+ }
530
+
531
+ // 4. Untagged traces only: attach to the latest block that precedes
532
+ // them. A trace that names a step whose block does not exist yet
533
+ // (tool call before any streamed text, or an action recorded before
534
+ // the step in Flash) creates that step's block below instead of being
535
+ // glued to an earlier step, where the later `step_recorded` (which
536
+ // carries the same trace) would leave a duplicate card.
537
+ if (existingIndex === -1 && !stepId) {
538
+ const logTime = log.timestamp ? new Date(log.timestamp).getTime() : 0;
539
+ let maxStepTime = -1;
540
+ for (let i = 0; i < blocks.length; i++) {
541
+ const b = blocks[i];
542
+ const bTime = b.timestamp ? new Date(b.timestamp).getTime() : 0;
543
+ if (bTime <= logTime && bTime >= maxStepTime) {
544
+ maxStepTime = bTime;
545
+ existingIndex = i;
546
+ }
547
+ }
548
+ }
549
+
550
+ if (existingIndex > -1) {
551
+ const stepData = blocks[existingIndex].data;
552
+ const genericTools = stepData.generic_tools ? [...stepData.generic_tools] : [];
553
+ const existingToolIdx = log.data.trace_id
554
+ ? genericTools.findIndex((t: any) => t.trace_id === log.data.trace_id)
555
+ : -1;
556
+
557
+ if (existingToolIdx > -1) {
558
+ genericTools[existingToolIdx] = {
559
+ ...genericTools[existingToolIdx],
560
+ ...log.data
561
+ };
562
+ } else {
563
+ genericTools.push(log.data);
564
+ }
565
+
566
+ blocks[existingIndex] = {
567
+ ...blocks[existingIndex],
568
+ data: {
569
+ ...stepData,
570
+ generic_tools: genericTools
571
+ }
572
+ };
573
+ } else {
574
+ const fallbackStepId = stepId || 'pre-planning';
575
+ blocks.push({
576
+ id: `step-${fallbackStepId}`,
577
+ type: 'step',
578
+ timestamp: log.timestamp,
579
+ data: {
580
+ step_id: fallbackStepId,
581
+ isCompleted: true,
582
+ generic_tools: [log.data]
583
+ }
584
+ });
585
+ }
586
+ }
587
+ });
588
+
589
+ relocateCheckerTools(blocks);
590
+
591
+ blocks.sort((a, b) => {
592
+ const timeA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
593
+ const timeB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
594
+ if (Math.abs(timeA - timeB) > 50) {
595
+ return timeA - timeB;
596
+ }
597
+ // Order by step_number if available (0 is planning phase)
598
+ const getStepOrder = (block: StepBlock): number => {
599
+ if (block.data?.step_number !== undefined && block.data.step_number !== null) {
600
+ return Number(block.data.step_number);
601
+ }
602
+ if (block.data?.step_id === 'pre-planning' || block.data?.step_type === 'planning') {
603
+ return 0;
604
+ }
605
+ return 99999;
606
+ };
607
+
608
+ const stepNumA = getStepOrder(a);
609
+ const stepNumB = getStepOrder(b);
610
+ if (stepNumA !== stepNumB && stepNumA !== 99999 && stepNumB !== 99999) {
611
+ return stepNumA - stepNumB;
612
+ }
613
+ return timeA - timeB;
614
+ });
615
+
616
+ return blocks;
617
+ }
618
+
619
+ /**
620
+ * Cache for the token-estimate fallback. Tool payloads can carry multi-megabyte
621
+ * base64 screenshots; serializing them on every recompute would block the main
622
+ * thread, so each payload object is measured exactly once.
623
+ */
624
+ const payloadSizeCache = new WeakMap<object, { isImage: boolean; length: number }>();
625
+
626
+ function estimatePayloadSize(payload: any): { isImage: boolean; length: number } {
627
+ if (typeof payload === 'string') {
628
+ return {
629
+ isImage: payload.includes('data:image/') || payload.includes('base64,'),
630
+ length: payload.length
631
+ };
632
+ }
633
+ if (payload === null || typeof payload !== 'object') {
634
+ return { isImage: false, length: String(payload).length };
635
+ }
636
+ const cached = payloadSizeCache.get(payload);
637
+ if (cached) return cached;
638
+ let payloadStr = '';
639
+ try {
640
+ payloadStr = JSON.stringify(payload) || '';
641
+ } catch {
642
+ payloadStr = String(payload);
643
+ }
644
+ const result = {
645
+ isImage: payloadStr.includes('data:image/') || payloadStr.includes('base64,'),
646
+ length: payloadStr.length
647
+ };
648
+ payloadSizeCache.set(payload, result);
649
+ return result;
650
+ }
651
+
652
+ /**
653
+ * Safely extract and aggregate token usage for a step block
654
+ */
655
+ export function extractBlockTokens(block: StepBlock): { total: number; prompt: number; completion: number } {
656
+ if (!block || !block.data) return { total: 0, prompt: 0, completion: 0 };
657
+ const data = block.data;
658
+
659
+ // 1. Direct total_tokens or token_usage object
660
+ if (data.total_tokens !== undefined && data.total_tokens !== null && Number(data.total_tokens) > 0) {
661
+ const pr = data.token_usage?.prompt_tokens || data.token_usage?.input_tokens || 0;
662
+ const co = data.token_usage?.completion_tokens || data.token_usage?.output_tokens || 0;
663
+ return { total: Number(data.total_tokens), prompt: Number(pr), completion: Number(co) };
664
+ }
665
+ if (data.token_usage && typeof data.token_usage === 'object') {
666
+ const pr = data.token_usage.prompt_tokens || data.token_usage.input_tokens || 0;
667
+ const co = data.token_usage.completion_tokens || data.token_usage.output_tokens || 0;
668
+ const to = data.token_usage.total_tokens || (pr + co);
669
+ if (to > 0) {
670
+ return { total: Number(to), prompt: Number(pr), completion: Number(co) };
671
+ }
672
+ }
673
+ if (data.extra_metadata?.token_usage && typeof data.extra_metadata.token_usage === 'object') {
674
+ const u = data.extra_metadata.token_usage;
675
+ const pr = u.prompt_tokens || u.prompt_token_count || u.input_tokens || 0;
676
+ const co = u.completion_tokens || u.candidates_token_count || u.output_tokens || 0;
677
+ const to = u.total_tokens || u.total_token_count || (pr + co);
678
+ if (to > 0) {
679
+ return { total: Number(to), prompt: Number(pr), completion: Number(co) };
680
+ }
681
+ }
682
+
683
+ // 2. Generic tools traces (e.g. llm_call traces or tool payloads)
684
+ let sumPrompt = 0;
685
+ let sumCompletion = 0;
686
+ let sumTotal = 0;
687
+ if (data.generic_tools && Array.isArray(data.generic_tools)) {
688
+ data.generic_tools.forEach((t: any) => {
689
+ if (t && t.payload) {
690
+ const u = t.payload.token_usage || t.payload.usage_metadata;
691
+ if (u) {
692
+ const pr = u.prompt_tokens || u.prompt_token_count || u.input_tokens || 0;
693
+ const co = u.completion_tokens || u.candidates_token_count || u.output_tokens || 0;
694
+ const to = u.total_tokens || u.total_token_count || (pr + co);
695
+ sumPrompt += Number(pr);
696
+ sumCompletion += Number(co);
697
+ sumTotal += Number(to);
698
+ }
699
+ }
700
+ });
701
+ }
702
+ if (sumTotal > 0) {
703
+ return { total: sumTotal, prompt: sumPrompt, completion: sumCompletion };
704
+ }
705
+
706
+ // 3. Fallback estimate from thoughts/messages/actions text if no tokens stored
707
+ let textLen = 0;
708
+ let numImages = 0;
709
+ if (data.operator_native_thinking) textLen += data.operator_native_thinking.length;
710
+ if (data.operator_raw_thinking) textLen += data.operator_raw_thinking.length;
711
+ if (data.generic_tools && Array.isArray(data.generic_tools)) {
712
+ data.generic_tools.forEach((t: any) => {
713
+ if (t.payload) {
714
+ const estimate = estimatePayloadSize(t.payload);
715
+ if (estimate.isImage) {
716
+ numImages += 1;
717
+ } else {
718
+ textLen += estimate.length;
719
+ }
720
+ }
721
+ });
722
+ }
723
+ if (textLen > 20 || numImages > 0) {
724
+ const estimated = Math.round(textLen / 4) + (numImages * 258);
725
+ return { total: estimated, prompt: 0, completion: estimated };
726
+ }
727
+
728
+ return { total: 0, prompt: 0, completion: 0 };
729
+ }
730
+
731
+ /**
732
+ * Format token count with clean units (e.g. "842 tokens", "76.4k tokens", "1.2M tokens")
733
+ */
734
+ export function formatTokenCount(tokens?: number): string {
735
+ if (!tokens || tokens <= 0) return '0 tokens';
736
+ if (tokens < 1000) {
737
+ return `${tokens} tokens`;
738
+ }
739
+ if (tokens < 1_000_000) {
740
+ const inK = (tokens / 1000).toFixed(1).replace(/\.0$/, '');
741
+ return `${inK}k tokens`;
742
+ }
743
+ const inM = (tokens / 1_000_000).toFixed(2).replace(/\.?0+$/, '');
744
+ return `${inM}M tokens`;
745
+ }
746
+
747
+ /**
748
+ * Group step blocks into chronological execution phases (segmented per work unit)
749
+ */
750
+ export function groupBlocksToPhases(blocks: StepBlock[], sessionStartTime: number): PhaseBlock[] {
751
+ if (!blocks || blocks.length === 0) return [];
752
+ const phases: PhaseBlock[] = [];
753
+
754
+ // Base start timestamp in ms
755
+ let previousEndTime = sessionStartTime ? sessionStartTime * 1000 : 0;
756
+
757
+ blocks.forEach((block, index) => {
758
+ const blockTime = block.timestamp ? new Date(block.timestamp).getTime() : Date.now();
759
+
760
+ if (previousEndTime === 0 && blockTime > 0) {
761
+ previousEndTime = blockTime;
762
+ }
763
+
764
+ let durationSeconds = 0;
765
+ if (block.data?.duration && typeof block.data.duration === 'number' && block.data.duration > 0) {
766
+ durationSeconds = Math.max(1, Math.round(block.data.duration));
767
+ } else if (blockTime >= previousEndTime && previousEndTime > 0) {
768
+ durationSeconds = Math.max(1, Math.round((blockTime - previousEndTime) / 1000));
769
+ } else {
770
+ durationSeconds = 1;
771
+ }
772
+
773
+ const tokensInfo = extractBlockTokens(block);
774
+
775
+ phases.push({
776
+ id: `phase-${block.id || index}`,
777
+ durationSeconds,
778
+ tokens: tokensInfo.total > 0 ? tokensInfo.total : undefined,
779
+ promptTokens: tokensInfo.prompt > 0 ? tokensInfo.prompt : undefined,
780
+ completionTokens: tokensInfo.completion > 0 ? tokensInfo.completion : undefined,
781
+ blocks: [block]
782
+ });
783
+
784
+ previousEndTime = blockTime;
785
+ });
786
+
787
+ return phases;
788
+ }
789
+
790
+ /**
791
+ * Returns all actions and tools inside a step card, sorted in true chronological sequence by timestamp
792
+ */
793
+ export function getSortedStepEvents(
794
+ stepData: any,
795
+ cache?: WeakMap<any, { signature: string; events: StepEvent[] }>
796
+ ): StepEvent[] {
797
+ if (!stepData || typeof stepData !== 'object') return [];
798
+ const toolsLen = stepData.generic_tools ? stepData.generic_tools.length : 0;
799
+ const toolsSignature = stepData.generic_tools && Array.isArray(stepData.generic_tools)
800
+ ? stepData.generic_tools.map((t: any) => `${t?.trace_id || t?.name || ''}:${t?.status || ''}`).join(',')
801
+ : '';
802
+ const actionTs = stepData.action_taken ? (stepData.action_taken.timestamp || stepData.action_taken.start_time || stepData.action_taken.created_at) : null;
803
+ const nativeText = typeof stepData.operator_native_thinking === 'string'
804
+ ? stepData.operator_native_thinking.trim()
805
+ : '';
806
+ const rawText = typeof stepData.operator_raw_thinking === 'string'
807
+ ? stepData.operator_raw_thinking.trim()
808
+ : '';
809
+ const segments: StreamSegment[] | null = Array.isArray(stepData.stream_segments) && stepData.stream_segments.length > 0
810
+ ? stepData.stream_segments
811
+ : null;
812
+ const signature = [
813
+ toolsLen,
814
+ toolsSignature,
815
+ actionTs ?? '',
816
+ stepData.operator_native_thinking_timestamp ?? '',
817
+ stepData.operator_raw_thinking_timestamp ?? '',
818
+ nativeText.length,
819
+ rawText.length,
820
+ stepData.isReset ? '1' : '0',
821
+ stepData.resetMessage ?? '',
822
+ stepData.stream_resets ? stepData.stream_resets.map((r: any) => `${r.id}:${r.isWaiting ? 1 : 0}`).join(',') : '',
823
+ segments ? segments.map((s) => `${s.execution_id}:${s.stream_type}:${s.text.length}`).join(',') : ''
824
+ ].join('|');
825
+
826
+ if (cache) {
827
+ const cached = cache.get(stepData);
828
+ if (cached?.signature === signature) {
829
+ return cached.events;
830
+ }
831
+ }
832
+
833
+ const events: Array<{ type: StepEvent['type']; data: any; timestamp: number; sequence: number }> = [];
834
+ const fallbackTime = getItemTimestamp(stepData.timestamp, 0);
835
+ let sequence = 0;
836
+
837
+ // Text and tools share one timeline. Live streams carry their first-seen
838
+ // timestamps; persisted steps use the step timestamp as a stable fallback.
839
+ if (segments) {
840
+ // Multi-turn agent (Checker): one event per streamed turn, each at the
841
+ // time its first chunk arrived, so turns sort between the tool calls.
842
+ segments.forEach((segment) => {
843
+ if ((!segment.text || !segment.text.trim()) && !segment.isReset) return;
844
+ events.push({
845
+ type: segment.stream_type === 'thinking' ? 'thinking' : 'text',
846
+ data: {
847
+ text: segment.text,
848
+ execution_id: segment.execution_id,
849
+ segment: true,
850
+ isReset: segment.isReset,
851
+ resetMessage: segment.resetMessage
852
+ },
853
+ timestamp: getItemTimestamp(segment.timestamp, fallbackTime),
854
+ sequence: sequence++
855
+ });
856
+ });
857
+ } else {
858
+ const hasThinkingResets = Array.isArray(stepData.stream_resets) && stepData.stream_resets.some((r: any) => r.streamType === 'thinking');
859
+ if (nativeText || hasThinkingResets) {
860
+ events.push({
861
+ type: 'thinking',
862
+ data: { text: stepData.operator_native_thinking || '' },
863
+ timestamp: getItemTimestamp(stepData.operator_native_thinking_timestamp, fallbackTime),
864
+ sequence: sequence++
865
+ });
866
+ }
867
+
868
+ if (rawText || (stepData.isReset && stepData.resetMessage) || (Array.isArray(stepData.stream_resets) && stepData.stream_resets.length > 0)) {
869
+ events.push({
870
+ type: 'text',
871
+ data: {
872
+ text: stepData.operator_raw_thinking || '',
873
+ isReset: stepData.isReset,
874
+ resetMessage: stepData.resetMessage
875
+ },
876
+ timestamp: getItemTimestamp(stepData.operator_raw_thinking_timestamp, fallbackTime),
877
+ sequence: sequence++
878
+ });
879
+ }
880
+ }
881
+
882
+ // 2. Single Source of Truth & Fallback Resolution:
883
+ const uniqueTools = getUniqueGenericTools(stepData.generic_tools);
884
+ const hasActionInTools = uniqueTools.some((tool: any) =>
885
+ tool && (tool.type === 'action' || isAndroidAction(tool) || isReportStatusAction(tool))
886
+ );
887
+
888
+ // Fallback: only add action_taken if generic_tools did NOT already contain an action trace.
889
+ // Pushed before uniqueTools so that when timestamps are missing/equal, the primary action
890
+ // preserves deterministic sequence ordering ahead of generic fallback tools.
891
+ if (!hasActionInTools && stepData.action_taken && (isAndroidAction(stepData.action_taken) || isReportStatusAction(stepData.action_taken))) {
892
+ const actObj = getActionObject(stepData.action_taken);
893
+ const actTime = getItemTimestamp(
894
+ actObj?.timestamp ?? actObj?.start_time ?? actObj?.created_at,
895
+ fallbackTime
896
+ );
897
+ events.push({
898
+ type: 'action',
899
+ data: stepData.action_taken,
900
+ timestamp: actTime,
901
+ sequence: sequence++
902
+ });
903
+ }
904
+
905
+ // Add all real tool calls and actions from the event stream.
906
+ uniqueTools.forEach((tool: any) => {
907
+ if (isInternalPlumbingTool(tool) && !isReportStatusAction(tool)) {
908
+ return;
909
+ }
910
+
911
+ const isAction = tool.type === 'action' || isAndroidAction(tool) || isReportStatusAction(tool);
912
+ const toolTime = getItemTimestamp(
913
+ tool.timestamp ?? tool.start_time ?? tool.created_at,
914
+ fallbackTime
915
+ );
916
+ events.push({
917
+ type: isAction ? 'action' : 'tool',
918
+ data: tool,
919
+ timestamp: toolTime,
920
+ sequence: sequence++
921
+ });
922
+ });
923
+
924
+ // Sort chronologically. The explicit sequence makes equal/missing timestamp
925
+ // behavior deterministic across browsers and preserves the legacy fallback.
926
+ events.sort((a, b) => (a.timestamp - b.timestamp) || (a.sequence - b.sequence));
927
+
928
+ const result: StepEvent[] = events.map(e => ({
929
+ type: e.type,
930
+ data: e.data,
931
+ timestamp: e.timestamp
932
+ }));
933
+ if (cache) {
934
+ cache.set(stepData, { signature, events: result });
935
+ }
936
+ return result;
937
+ }
938
+
939
+ /**
940
+ * Compile human-readable session summary from logs
941
+ */
942
+ export function compileSessionSummary(logs: any[]): string {
943
+ if (!logs || logs.length === 0) return 'No logs available.';
944
+
945
+ // Check for goal completed / checker response / cancellation / report_task_status
946
+ for (let i = logs.length - 1; i >= 0; i--) {
947
+ const log = logs[i];
948
+ if (log.type === 'session_ended') {
949
+ if (log.data?.status === 'cancelled' || log.data?.was_stopped_manually) {
950
+ return 'Task stopped manually.';
951
+ }
952
+ }
953
+ if (log.type === 'llm_stream' && log.data?.text) {
954
+ const text = log.data.text;
955
+ if (text.includes('```json') && text.includes('"success"')) {
956
+ return 'Execution completed and verified by checker.';
957
+ }
958
+ }
959
+ if (log.type === 'step_recorded' || log.type === 'step_updated') {
960
+ const act = log.data?.action_taken;
961
+ if (act && isReportStatusAction(act)) {
962
+ const exp = getReportStatusExplanation(act);
963
+ if (exp) return exp;
964
+ }
965
+ if (log.data?.status === 'completed') {
966
+ return log.data.message || 'Task completed successfully.';
967
+ }
968
+ }
969
+ }
970
+
971
+ return 'Execution session in progress or ended.';
972
+ }
973
+
974
+ /**
975
+ * Calculate total session duration in seconds
976
+ */
977
+ export function computeSessionDuration(sessionStartTime: number, logs: any[]): number {
978
+ if (!sessionStartTime || !logs || logs.length === 0) return 0;
979
+ const startMs = sessionStartTime * 1000;
980
+ const lastLog = logs[logs.length - 1];
981
+ const endMs = lastLog?.timestamp ? new Date(lastLog.timestamp).getTime() : Date.now();
982
+ return Math.max(0, Math.round((endMs - startMs) / 1000));
983
+ }
984
+
985
+ /**
986
+ * Check if the planning loader should be visible (while running, waiting for next step/action and not actively typing a stream)
987
+ */
988
+ export function checkPlanningLoader(
989
+ logs: any[],
990
+ isRunning: boolean,
991
+ isCurrentRunningSession: boolean = true
992
+ ): boolean {
993
+ if (!isRunning || !isCurrentRunningSession) return false;
994
+ if (!logs || logs.length === 0) return true;
995
+
996
+ // If the latest step or event indicates task completion, do not show loader
997
+ for (let i = logs.length - 1; i >= Math.max(0, logs.length - 3); i--) {
998
+ const log = logs[i];
999
+ if (log.type === 'session_ended') return false;
1000
+ if (log.type === 'step_recorded' || log.type === 'step_updated') {
1001
+ const act = log.data?.action_taken;
1002
+ if (act && (act.action === 'report_task_status' || act === 'report_task_status' || isReportStatusAction(act))) {
1003
+ return false;
1004
+ }
1005
+ if (log.data?.status === 'completed' || log.data?.status === 'failed') {
1006
+ return false;
1007
+ }
1008
+ }
1009
+ }
1010
+
1011
+ // If there is an active incomplete stream currently streaming chunks, hide the planning loader
1012
+ const hasActiveStream = logs.some(
1013
+ l => l && l.type === 'llm_stream' && l.data && l.data.isCompleted === false
1014
+ );
1015
+
1016
+ if (hasActiveStream) {
1017
+ return false;
1018
+ }
1019
+
1020
+ return true;
1021
+ }