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,878 @@
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 loop of ``Explorer.run``.
16
+
17
+ Split out of ``artemis.agents.explorer.explorer``: the native-SDK reasoning
18
+ loop and its named phases (model invocation, submit_answer interception and
19
+ validation, tool dispatching, failure mapping, resource cleanup), packaged as
20
+ a mixin consumed by ``Explorer``. Every google-genai call goes through the
21
+ SDK's ``client.aio`` surface so uploads, cache management and generation
22
+ never block the event loop. This module also owns
23
+ :func:`_generate_content_with_reliability`, the shared reliability wrapper
24
+ for native google-genai model calls.
25
+ """
26
+
27
+ import asyncio
28
+ from collections.abc import Awaitable, Callable
29
+ import json
30
+ import os
31
+ import time
32
+ from typing import TYPE_CHECKING, Any
33
+
34
+ from google.genai import types
35
+
36
+ from artemis.agents.explorer.geometry import is_valid_norm_point
37
+ from artemis.agents.explorer.tiers import SUBMIT_TOOL
38
+ from artemis.constants import SAFETY_SETTINGS_BLOCK_NONE
39
+ from artemis.data_engine.trace import TraceSpan
40
+ from artemis.llm.reliability import (
41
+ LLMExhaustedError,
42
+ LLMPermanentError,
43
+ classify_failure,
44
+ retry_policy_for,
45
+ )
46
+ from artemis.services.llm import _record_llm_event, _record_llm_retry
47
+ from third_party.mobile_use.utils.logger import get_logger
48
+
49
+ logger = get_logger(__name__)
50
+
51
+ #: Loads a local image as a ``Part`` (inline bytes or a File API reference).
52
+ ImagePartGetter = Callable[[str], Awaitable[types.Part]]
53
+
54
+
55
+ async def _generate_content_with_reliability(operation, *, label: str = "Explorer model call"):
56
+ """Run one native google-genai model call under the shared reliability layer.
57
+
58
+ ``operation`` must return a fresh awaitable on each call. Non-retryable
59
+ failures raise ``LLMPermanentError``; exhausted retries raise
60
+ ``LLMExhaustedError`` according to the shared category policy.
61
+ """
62
+ attempt = 0
63
+ while True:
64
+ try:
65
+ return await operation()
66
+ except Exception as call_err:
67
+ attempt += 1
68
+ failure = classify_failure(call_err)
69
+ if not failure.retryable:
70
+ logger.error(f"{label} permanently failed [{failure.category.value}]: {call_err}")
71
+ _record_llm_event(
72
+ "llm_gave_up",
73
+ {
74
+ "source": "explorer",
75
+ "error": str(call_err)[:1000],
76
+ "category": failure.category.value,
77
+ "retryable": False,
78
+ },
79
+ status="failed",
80
+ )
81
+ raise LLMPermanentError(
82
+ f"{label} failed [{failure.category.value}]: {call_err}",
83
+ failure=failure,
84
+ cause=call_err,
85
+ ) from call_err
86
+ policy = retry_policy_for(failure.category)
87
+ if attempt >= policy.max_attempts:
88
+ logger.error(
89
+ f"{label} exhausted {attempt} attempt(s) [{failure.category.value}]: {call_err}"
90
+ )
91
+ _record_llm_event(
92
+ "llm_gave_up",
93
+ {
94
+ "source": "explorer",
95
+ "error": str(call_err)[:1000],
96
+ "category": failure.category.value,
97
+ "retryable": True,
98
+ "attempts": attempt,
99
+ },
100
+ status="failed",
101
+ )
102
+ raise LLMExhaustedError(
103
+ f"{label} exhausted {attempt} attempt(s)"
104
+ f" [{failure.category.value}]: {call_err}",
105
+ failure=failure,
106
+ cause=call_err,
107
+ ) from call_err
108
+ delay = policy.delay_for(attempt)
109
+ logger.warning(
110
+ f"{label} failed [{failure.category.value}] on attempt"
111
+ f" {attempt}/{policy.max_attempts}: {call_err}."
112
+ f" Retrying in {delay:.2f}s..."
113
+ )
114
+ _record_llm_retry(
115
+ str(call_err),
116
+ delay,
117
+ attempt=attempt,
118
+ max_retries=policy.max_attempts,
119
+ source="explorer",
120
+ )
121
+ await asyncio.sleep(delay)
122
+
123
+
124
+ def _bare_tool_name(name: str) -> str:
125
+ """Strips a namespace prefix (``ns:tool``) some models add to tool names."""
126
+ return name.split(":")[-1] if ":" in name else name
127
+
128
+
129
+ class NativeRunnerMixin:
130
+ """Native Gemini SDK reasoning loop of :class:`Explorer`."""
131
+
132
+ # This mixin is composed into ``Explorer``. Declaring the attributes and
133
+ # sibling-mixin methods it consumes makes that host contract visible to
134
+ # static analysis without adding runtime shims or changing MRO behavior.
135
+ if TYPE_CHECKING:
136
+ from artemis.context import ArtemisContext
137
+
138
+ ctx: ArtemisContext
139
+ denylisted_tools: set[str]
140
+ turn_latencies: list[float]
141
+ turn_cached_tokens: list[int]
142
+ trace_history: list[dict[str, Any]]
143
+
144
+ def get_exposed_tools(
145
+ self, only_submit: bool = False
146
+ ) -> list[types.FunctionDeclaration]: ...
147
+
148
+ def _prune_historical_images(self, contents: list, keep_last: int = 1) -> None: ...
149
+
150
+ def _enrich_candidates(self, candidates: list[Any]) -> list[Any]: ...
151
+
152
+ async def exec_ask_perception_tool(
153
+ self,
154
+ search_query: str,
155
+ nx: int,
156
+ ny: int,
157
+ detect_queries: list[str],
158
+ ) -> dict[str, Any]: ...
159
+
160
+ async def exec_detect_objects(
161
+ self, queries: list[str], target_image_id: str = "img_0"
162
+ ) -> dict[str, Any]: ...
163
+
164
+ async def exec_ask_image_processor(
165
+ self, instruction: str, target_image_id: str = "img_0"
166
+ ) -> dict[str, Any]: ...
167
+
168
+ async def exec_get_ocr_list(self) -> dict[str, Any]: ...
169
+
170
+ async def exec_inspect_region(
171
+ self,
172
+ x_min: int,
173
+ y_min: int,
174
+ x_max: int,
175
+ y_max: int,
176
+ zoom_factor: float = 2.0,
177
+ ) -> dict[str, Any]: ...
178
+
179
+ def _make_image_part_getter(
180
+ self, client, use_file_api: bool, uploaded_files: list
181
+ ) -> ImagePartGetter:
182
+ """Builds the image-part loader used for screenshots and tool images."""
183
+
184
+ async def get_image_part(file_path: str) -> types.Part:
185
+ if use_file_api:
186
+ file_ref = await client.aio.files.upload(file=file_path)
187
+ uploaded_files.append(file_ref)
188
+ return types.Part(
189
+ file_data=types.FileData(
190
+ file_uri=file_ref.uri,
191
+ mime_type=file_ref.mime_type or "image/jpeg",
192
+ )
193
+ )
194
+ with open(file_path, "rb") as f:
195
+ img_bytes = f.read()
196
+ return types.Part.from_bytes(data=img_bytes, mime_type="image/jpeg")
197
+
198
+ return get_image_part
199
+
200
+ def _build_initial_contents(
201
+ self, query: str, context_feedback: str, minimal_list: str, initial_part: types.Part
202
+ ) -> list[types.Content]:
203
+ """Builds the initial user content with the operator request and screenshot."""
204
+ return [
205
+ types.Content(
206
+ role="user",
207
+ parts=[
208
+ types.Part.from_text(
209
+ text=f"""Operator Request:
210
+ - Query: {query}
211
+ - Context Feedback: {context_feedback}
212
+
213
+ Initial marked UI elements list (corresponding to numbers ①, ②... in the image):
214
+ {minimal_list}
215
+ """
216
+ ),
217
+ initial_part,
218
+ ],
219
+ )
220
+ ]
221
+
222
+ async def _create_cache_resource(
223
+ self, client, model_name: str, prompt_template: str, contents: list
224
+ ):
225
+ """Creates the explicit cache resource; returns None on failure."""
226
+ try:
227
+ logger.info("Creating cache resource for Explorer...")
228
+ cached_content = await client.aio.caches.create(
229
+ model=model_name,
230
+ config=types.CreateCachedContentConfig(
231
+ contents=[contents[0]],
232
+ system_instruction=prompt_template,
233
+ tools=[types.Tool(function_declarations=self.get_exposed_tools())],
234
+ tool_config=types.ToolConfig(
235
+ function_calling_config=types.FunctionCallingConfig(
236
+ mode=types.FunctionCallingConfigMode.ANY
237
+ ),
238
+ include_server_side_tool_invocations=True,
239
+ ),
240
+ ttl="300s",
241
+ ),
242
+ )
243
+ logger.info(f"Cache resource created successfully: {cached_content.name}")
244
+ return cached_content
245
+ except Exception as cache_err:
246
+ logger.error(f"Failed to create cache resource: {cache_err}")
247
+ return None
248
+
249
+ async def _run_native(
250
+ self,
251
+ *,
252
+ client,
253
+ query: str,
254
+ context_feedback: str,
255
+ minimal_list: str,
256
+ img_to_read: str,
257
+ enable_caching: bool,
258
+ prompt_template: str,
259
+ model_name: str,
260
+ temperature,
261
+ thinking_level,
262
+ max_turns: int,
263
+ ) -> str:
264
+ """Runs the native Gemini path: cache setup, reasoning loop, cleanup."""
265
+ use_file_api = os.getenv("ARTEMIS_USE_FILE_API", "false").lower() == "true"
266
+ uploaded_files: list = []
267
+ get_image_part = self._make_image_part_getter(client, use_file_api, uploaded_files)
268
+
269
+ cached_content = None
270
+ try:
271
+ initial_part = await get_image_part(img_to_read)
272
+ contents = self._build_initial_contents(
273
+ query, context_feedback, minimal_list, initial_part
274
+ )
275
+
276
+ if enable_caching:
277
+ cached_content = await self._create_cache_resource(
278
+ client, model_name, prompt_template, contents
279
+ )
280
+
281
+ agent_outcome = await self._native_loop(
282
+ client=client,
283
+ contents=contents,
284
+ get_image_part=get_image_part,
285
+ cached_content=cached_content,
286
+ prompt_template=prompt_template,
287
+ model_name=model_name,
288
+ temperature=temperature,
289
+ thinking_level=thinking_level,
290
+ max_turns=max_turns,
291
+ )
292
+
293
+ except Exception as e:
294
+ agent_outcome = self._format_native_failure(e, model_name)
295
+ finally:
296
+ await self._cleanup_native_resources(client, cached_content, uploaded_files)
297
+
298
+ return agent_outcome
299
+
300
+ async def _native_loop(
301
+ self,
302
+ *,
303
+ client,
304
+ contents: list,
305
+ get_image_part: ImagePartGetter,
306
+ cached_content,
307
+ prompt_template: str,
308
+ model_name: str,
309
+ temperature,
310
+ thinking_level,
311
+ max_turns: int,
312
+ ) -> str:
313
+ """Main native reasoning loop: one model turn per iteration."""
314
+ turn = 0
315
+ agent_outcome = ""
316
+ self.turn_latencies = []
317
+ self.turn_cached_tokens = []
318
+ self.trace_history = []
319
+
320
+ while turn < max_turns:
321
+ turn += 1
322
+ logger.info(f"Turn {turn}/{max_turns}: Invoking Native Gemini SDK for Explorer...")
323
+
324
+ self._prune_historical_images(contents, keep_last=1)
325
+
326
+ is_final_turn = turn == max_turns
327
+ if is_final_turn:
328
+ warning_msg = (
329
+ "\n[WARNING] This is your final iteration. You MUST"
330
+ " call 'submit_answer' to submit your final result. No"
331
+ " other tools are available."
332
+ )
333
+ contents.append(
334
+ types.Content(
335
+ role="user",
336
+ parts=[types.Part.from_text(text=warning_msg)],
337
+ )
338
+ )
339
+
340
+ response, thinking_parts, text_parts = await self._invoke_native_model(
341
+ client=client,
342
+ contents=contents,
343
+ cached_content=cached_content,
344
+ is_final_turn=is_final_turn,
345
+ prompt_template=prompt_template,
346
+ model_name=model_name,
347
+ temperature=temperature,
348
+ thinking_level=thinking_level,
349
+ )
350
+
351
+ turn_record = {
352
+ "iteration": turn,
353
+ "thoughts": ("\n".join(thinking_parts) if thinking_parts else ""),
354
+ "tool_calls": [],
355
+ }
356
+
357
+ function_calls = response.function_calls
358
+ if not function_calls:
359
+ self._handle_missing_tool_calls(
360
+ contents, response, turn_record, text_parts, max_turns, turn
361
+ )
362
+ continue
363
+
364
+ # Record the model's function call request in history
365
+ contents.append(self._model_content(response, function_calls))
366
+
367
+ tool_response_parts: list[types.Part] = []
368
+
369
+ # Intercept and validate submit_answer to manage the task lifecycle
370
+ submit_outcome = self._process_submit_answer(
371
+ function_calls, turn_record, tool_response_parts
372
+ )
373
+ if submit_outcome is not None:
374
+ agent_outcome = submit_outcome
375
+ break
376
+
377
+ await self._dispatch_tool_calls(
378
+ function_calls, get_image_part, turn_record, tool_response_parts
379
+ )
380
+
381
+ if tool_response_parts:
382
+ # Native Gemini SDK allows the 'user' role to return mixed parts
383
+ contents.append(types.Content(role="user", parts=tool_response_parts))
384
+
385
+ self.trace_history.append(turn_record)
386
+
387
+ if turn >= max_turns and not agent_outcome:
388
+ agent_outcome = (
389
+ "Error: Explorer reached maximum iterations without a conclusive answer."
390
+ )
391
+
392
+ return agent_outcome
393
+
394
+ @staticmethod
395
+ def _model_content(response, function_calls: list) -> types.Content:
396
+ """The model turn to record: the SDK's content when present, else a rebuild."""
397
+ candidates = getattr(response, "candidates", None)
398
+ if candidates:
399
+ content = getattr(candidates[0], "content", None)
400
+ if content:
401
+ return content
402
+ return types.Content(
403
+ role="model",
404
+ parts=[types.Part(function_call=fc) for fc in function_calls],
405
+ )
406
+
407
+ def _build_generate_config(
408
+ self,
409
+ *,
410
+ cached_content,
411
+ is_final_turn: bool,
412
+ prompt_template: str,
413
+ temperature,
414
+ thinking_level,
415
+ ) -> types.GenerateContentConfig:
416
+ """Builds the per-turn generation config (cached vs full tool config)."""
417
+ thinking_config = (
418
+ types.ThinkingConfig(thinking_level=thinking_level) if thinking_level else None
419
+ )
420
+ if cached_content and not is_final_turn:
421
+ return types.GenerateContentConfig(
422
+ cached_content=cached_content.name,
423
+ temperature=temperature,
424
+ safety_settings=SAFETY_SETTINGS_BLOCK_NONE,
425
+ thinking_config=thinking_config,
426
+ )
427
+ return types.GenerateContentConfig(
428
+ system_instruction=prompt_template,
429
+ temperature=temperature,
430
+ safety_settings=SAFETY_SETTINGS_BLOCK_NONE,
431
+ tools=[
432
+ types.Tool(function_declarations=self.get_exposed_tools(only_submit=is_final_turn))
433
+ ],
434
+ tool_config=types.ToolConfig(
435
+ function_calling_config=types.FunctionCallingConfig(
436
+ mode=types.FunctionCallingConfigMode.ANY
437
+ ),
438
+ include_server_side_tool_invocations=True,
439
+ ),
440
+ thinking_config=thinking_config,
441
+ )
442
+
443
+ @staticmethod
444
+ def _extract_response_texts(response) -> tuple[list[str], list[str]]:
445
+ """Extracts model thoughts and plain text parts from a response."""
446
+ thinking_parts = []
447
+ text_parts = []
448
+ candidates = getattr(response, "candidates", None)
449
+ if not candidates and isinstance(response, dict):
450
+ candidates = response.get("candidates")
451
+
452
+ if candidates and len(candidates) > 0:
453
+ candidate = candidates[0]
454
+ content = getattr(candidate, "content", None)
455
+ if not content and isinstance(candidate, dict):
456
+ content = candidate.get("content")
457
+
458
+ parts = getattr(content, "parts", None)
459
+ if not parts and isinstance(content, dict):
460
+ parts = content.get("parts")
461
+
462
+ if parts:
463
+ for part in parts:
464
+ is_thought = False
465
+ part_text = None
466
+ if isinstance(part, dict):
467
+ is_thought = part.get("thought", False)
468
+ part_text = part.get("text", None)
469
+ else:
470
+ is_thought = getattr(part, "thought", False)
471
+ part_text = getattr(part, "text", None)
472
+
473
+ if is_thought and part_text:
474
+ thinking_parts.append(part_text)
475
+ elif part_text:
476
+ text_parts.append(part_text)
477
+ return thinking_parts, text_parts
478
+
479
+ async def _invoke_native_model(
480
+ self,
481
+ *,
482
+ client,
483
+ contents: list,
484
+ cached_content,
485
+ is_final_turn: bool,
486
+ prompt_template: str,
487
+ model_name: str,
488
+ temperature,
489
+ thinking_level,
490
+ ):
491
+ """Invokes the model for one turn under a trace span; returns response and texts."""
492
+ ctx = self.ctx
493
+ start_turn = time.perf_counter()
494
+ with TraceSpan(name="gemini_explorer_call", ctx=ctx) as span:
495
+ generate_config = self._build_generate_config(
496
+ cached_content=cached_content,
497
+ is_final_turn=is_final_turn,
498
+ prompt_template=prompt_template,
499
+ temperature=temperature,
500
+ thinking_level=thinking_level,
501
+ )
502
+
503
+ response = await _generate_content_with_reliability(
504
+ lambda: asyncio.wait_for(
505
+ client.aio.models.generate_content(
506
+ model=model_name,
507
+ contents=contents,
508
+ config=generate_config,
509
+ ),
510
+ timeout=180,
511
+ ),
512
+ )
513
+ end_turn = time.perf_counter()
514
+ turn_dur = end_turn - start_turn
515
+ self.turn_latencies.append(turn_dur)
516
+
517
+ cached_tokens = 0
518
+ if getattr(response, "usage_metadata", None) and getattr(
519
+ response.usage_metadata,
520
+ "cached_content_token_count",
521
+ None,
522
+ ):
523
+ cached_tokens = response.usage_metadata.cached_content_token_count
524
+ self.turn_cached_tokens.append(cached_tokens)
525
+ span.result = (
526
+ f"Function calls: {len(response.function_calls)}"
527
+ if response.function_calls
528
+ else "Final answer"
529
+ )
530
+ # Extract and log model thoughts and text if present
531
+ thinking_parts, text_parts = self._extract_response_texts(response)
532
+ if thinking_parts:
533
+ span.payload["explorer_thought"] = "\n".join(thinking_parts)
534
+ if text_parts:
535
+ span.payload["explorer_text"] = "\n".join(text_parts)
536
+
537
+ return response, thinking_parts, text_parts
538
+
539
+ def _handle_missing_tool_calls(
540
+ self,
541
+ contents: list,
542
+ response,
543
+ turn_record: dict,
544
+ text_parts: list[str],
545
+ max_turns: int,
546
+ turn: int,
547
+ ) -> None:
548
+ """Handles a turn where the model produced plain text instead of tool calls.
549
+
550
+ Responses without candidates (safety blocks, empty replies) are
551
+ recorded as a synthetic model text part so the conversation history
552
+ stays well-formed for the retry.
553
+ """
554
+ logger.warning(f"Explorer turn {turn}: Model hallucinated plain text. Forcing retry.")
555
+ model_text = "\n".join(text_parts) if text_parts else ""
556
+ turn_record["tool_calls"].append(
557
+ {
558
+ "name": "hallucinated_plain_text",
559
+ "args": {"text": model_text},
560
+ "response": {
561
+ "error": (
562
+ "Forced retry because model failed to call submit_answer or any other tool"
563
+ )
564
+ },
565
+ }
566
+ )
567
+ self.trace_history.append(turn_record)
568
+
569
+ candidates = getattr(response, "candidates", None)
570
+ model_content = getattr(candidates[0], "content", None) if candidates else None
571
+ if not model_content:
572
+ model_content = types.Content(
573
+ role="model",
574
+ parts=[types.Part.from_text(text=model_text or "(no response)")],
575
+ )
576
+ contents.append(model_content)
577
+ contents.append(
578
+ types.Content(
579
+ role="user",
580
+ parts=[
581
+ types.Part.from_text(
582
+ text=(
583
+ "You have not called 'submit_answer'"
584
+ " to submit your final answer yet. If"
585
+ " you are not certain about the final"
586
+ " answer, you can continue exploring"
587
+ f" {max_turns - turn} more"
588
+ " time(s)."
589
+ )
590
+ )
591
+ ],
592
+ )
593
+ )
594
+
595
+ @staticmethod
596
+ def _validate_submit_args(function_calls: list, args: dict, candidates: list) -> list[str]:
597
+ """Validates a submit_answer call; returns the list of validation errors."""
598
+ errors = []
599
+
600
+ if len(function_calls) > 1:
601
+ errors.append(
602
+ "The tool 'submit_answer' MUST be called"
603
+ " individually in your final turn without any other"
604
+ " parallel tool calls. Please remove other tool"
605
+ " calls and re-submit."
606
+ )
607
+
608
+ fallback_message = args.get("fallback_message", "")
609
+ if not candidates and not fallback_message:
610
+ errors.append(
611
+ "The candidates list is empty. You must provide at"
612
+ " least one candidate matching the query, or"
613
+ " explain observations via fallback_message."
614
+ )
615
+
616
+ for i, cand in enumerate(candidates):
617
+ label = cand.get("label")
618
+ coords = cand.get("coords")
619
+ if not label:
620
+ errors.append(f"Candidate at index {i} is missing a 'label'.")
621
+ if not coords or not isinstance(coords, list) or len(coords) != 2:
622
+ errors.append(
623
+ f"Candidate '{label or i}' must have 'coords'"
624
+ " as an array of exactly 2 integers `[nx,"
625
+ " ny]`."
626
+ )
627
+ elif not is_valid_norm_point(coords):
628
+ try:
629
+ nx, ny = int(coords[0]), int(coords[1])
630
+ except (ValueError, TypeError):
631
+ errors.append(f"Candidate '{label or i}' coordinates are invalid integers.")
632
+ else:
633
+ errors.append(
634
+ f"Candidate '{label or i}' coordinates"
635
+ f" `[{nx}, {ny}]` must strictly be in"
636
+ " the `[0-1000]` normalized scale"
637
+ " range inclusive."
638
+ )
639
+
640
+ return errors
641
+
642
+ def _process_submit_answer(
643
+ self, function_calls: list, turn_record: dict, tool_response_parts: list
644
+ ) -> str | None:
645
+ """Intercepts submit_answer; returns the final outcome JSON when valid.
646
+
647
+ On validation failure the error is fed back as a tool response and
648
+ None is returned so the ReAct loop can continue and self-correct.
649
+ """
650
+ submit_call = next(
651
+ (fc for fc in function_calls if _bare_tool_name(fc.name) == SUBMIT_TOOL),
652
+ None,
653
+ )
654
+ if not submit_call:
655
+ return None
656
+
657
+ args = submit_call.args or {}
658
+ candidates = args.get("candidates", [])
659
+ errors = self._validate_submit_args(function_calls, args, candidates)
660
+
661
+ if errors:
662
+ logger.warning(f"Explorer submit_answer validation failed: {errors}")
663
+ turn_record["tool_calls"].append(
664
+ {
665
+ "name": SUBMIT_TOOL,
666
+ "args": args,
667
+ "response": {"error": ("Validation Failed:\n" + "\n".join(errors))},
668
+ }
669
+ )
670
+ tool_response_parts.append(
671
+ types.Part.from_function_response(
672
+ name=SUBMIT_TOOL,
673
+ response={
674
+ "error": (
675
+ "Validation Failed:\n"
676
+ + "\n".join(errors)
677
+ + "\nPlease correct these formatting"
678
+ " errors and re-submit using"
679
+ " submit_answer."
680
+ )
681
+ },
682
+ )
683
+ )
684
+ # Allow the ReAct loop to continue so the model can self-correct
685
+ return None
686
+
687
+ logger.info("Explorer submit_answer validation passed successfully!")
688
+ # Candidates submitted by a registered label inherit that element's
689
+ # bounds; the rest of the model's arguments are passed through as is.
690
+ outcome_args = dict(args)
691
+ outcome_args["candidates"] = self._enrich_candidates(candidates)
692
+ agent_outcome = json.dumps(outcome_args, ensure_ascii=False)
693
+ turn_record["tool_calls"].append(
694
+ {"name": SUBMIT_TOOL, "args": args, "response": {"result": "success"}}
695
+ )
696
+ self.trace_history.append(turn_record)
697
+ return agent_outcome
698
+
699
+ @staticmethod
700
+ async def _append_tool_response_with_images(
701
+ name: str,
702
+ res: dict,
703
+ get_image_part: ImagePartGetter,
704
+ tool_response_parts: list,
705
+ *,
706
+ result_key: str = "result",
707
+ ) -> None:
708
+ """Appends a tool result part plus every annotated image it produced.
709
+
710
+ Image loading failures are logged and skipped: the textual result is
711
+ still delivered, so one unreadable annotation cannot sink the turn.
712
+ """
713
+ tool_response_parts.append(
714
+ types.Part.from_function_response(name=name, response={result_key: res.get("text")})
715
+ )
716
+ image_paths = [p for p in [res.get("image_path"), *(res.get("image_paths") or [])] if p]
717
+ for img_p in image_paths:
718
+ try:
719
+ tool_response_parts.append(await get_image_part(img_p))
720
+ except Exception as e:
721
+ logger.warning(f"Failed to load image response part for {img_p}: {e}")
722
+
723
+ async def _execute_native_tool(
724
+ self,
725
+ name: str,
726
+ args: dict,
727
+ get_image_part: ImagePartGetter,
728
+ tool_call_trace: dict,
729
+ tool_response_parts: list,
730
+ ) -> None:
731
+ """Executes one non-submit tool call and appends its response parts."""
732
+ if name == "ask_perception_tool":
733
+ search_query = args.get("search_query")
734
+ nx = args.get("nx")
735
+ ny = args.get("ny")
736
+ detect_queries = args.get("detect_queries")
737
+ res = await self.exec_ask_perception_tool(
738
+ search_query=search_query if isinstance(search_query, str) else "",
739
+ nx=nx if isinstance(nx, int) else -1,
740
+ ny=ny if isinstance(ny, int) else -1,
741
+ detect_queries=detect_queries if isinstance(detect_queries, list) else [],
742
+ )
743
+ tool_call_trace["response"] = res
744
+ await self._append_tool_response_with_images(
745
+ name, res, get_image_part, tool_response_parts, result_key="text"
746
+ )
747
+ elif name == "detect_objects":
748
+ res = await self.exec_detect_objects(
749
+ queries=args.get("queries") or [],
750
+ target_image_id=args.get("target_image_id", "img_0"),
751
+ )
752
+ tool_call_trace["response"] = res
753
+ await self._append_tool_response_with_images(
754
+ name, res, get_image_part, tool_response_parts
755
+ )
756
+ elif name == "ask_image_processor":
757
+ res = await self.exec_ask_image_processor(
758
+ instruction=args.get("instruction") or "",
759
+ target_image_id=args.get("target_image_id", "img_0"),
760
+ )
761
+ tool_call_trace["response"] = res
762
+ await self._append_tool_response_with_images(
763
+ name, res, get_image_part, tool_response_parts
764
+ )
765
+ elif name == "get_ocr_list":
766
+ res = await self.exec_get_ocr_list()
767
+ tool_call_trace["response"] = res
768
+ await self._append_tool_response_with_images(
769
+ name, res, get_image_part, tool_response_parts
770
+ )
771
+ elif name == "inspect_region":
772
+ res = await self.exec_inspect_region(
773
+ x_min=args["x_min"],
774
+ y_min=args["y_min"],
775
+ x_max=args["x_max"],
776
+ y_max=args["y_max"],
777
+ zoom_factor=args.get("zoom_factor", 2.0),
778
+ )
779
+ tool_call_trace["response"] = res
780
+ await self._append_tool_response_with_images(
781
+ name, res, get_image_part, tool_response_parts
782
+ )
783
+ else:
784
+ tool_call_trace["response"] = {"error": f"Tool {name} not found"}
785
+ tool_response_parts.append(
786
+ types.Part.from_function_response(
787
+ name=name,
788
+ response={"error": f"Tool {name} not found"},
789
+ )
790
+ )
791
+
792
+ async def _dispatch_tool_calls(
793
+ self,
794
+ function_calls: list,
795
+ get_image_part: ImagePartGetter,
796
+ turn_record: dict,
797
+ tool_response_parts: list,
798
+ ) -> None:
799
+ """Dispatches all non-submit tool calls sequentially with tracing.
800
+
801
+ Denylisted names are refused here as well, even though they are
802
+ absent from the declarations, because models occasionally call tools
803
+ they were never offered.
804
+ """
805
+ for fc in function_calls:
806
+ name = _bare_tool_name(fc.name)
807
+ if name == SUBMIT_TOOL:
808
+ continue
809
+ args = fc.args or {}
810
+ tool_call_trace = {"name": name, "args": args}
811
+ if name in self.denylisted_tools:
812
+ logger.warning(
813
+ f"Explorer attempted to call denylisted tool '{name}'. Blocking execution."
814
+ )
815
+ error = f"Tool '{name}' is denylisted and unavailable."
816
+ tool_call_trace["response"] = {"error": error}
817
+ turn_record["tool_calls"].append(tool_call_trace)
818
+ tool_response_parts.append(
819
+ types.Part.from_function_response(name=fc.name, response={"error": error})
820
+ )
821
+ continue
822
+
823
+ logger.info(f"Explorer executing tool '{name}' sequentially...")
824
+
825
+ try:
826
+ await self._execute_native_tool(
827
+ name, args, get_image_part, tool_call_trace, tool_response_parts
828
+ )
829
+ except Exception as e:
830
+ logger.error(f"Explorer tool {name} execution failed: {e}")
831
+ tool_call_trace["response"] = {"error": str(e)}
832
+ tool_response_parts.append(
833
+ types.Part.from_function_response(name=name, response={"error": str(e)})
834
+ )
835
+ finally:
836
+ turn_record["tool_calls"].append(tool_call_trace)
837
+
838
+ def _format_native_failure(self, e: Exception, model_name: str) -> str:
839
+ """Maps a loop failure to the user-facing fallback outcome JSON."""
840
+ logger.error(f"Explorer execution loop failed: {e}")
841
+
842
+ err_msg = str(e)
843
+ if "preempted" in err_msg.lower():
844
+ clean_msg = (
845
+ "Model request was preempted by the server due to high"
846
+ " demand. Please try again later."
847
+ )
848
+ elif "overloaded" in err_msg.lower() or "503" in err_msg:
849
+ clean_msg = "Model service is currently overloaded. Please try again later."
850
+ elif "quota" in err_msg.lower() or "429" in err_msg:
851
+ clean_msg = "Model API quota exceeded or rate limited."
852
+ elif "thinkingconfig" in err_msg.lower():
853
+ clean_msg = (
854
+ "Model configuration error: ThinkingConfig is not"
855
+ f" supported for model {model_name}."
856
+ )
857
+ else:
858
+ clean_msg = f"Explorer agent execution failed: {err_msg}"
859
+
860
+ return json.dumps(
861
+ {"candidates": [], "fallback_message": clean_msg},
862
+ ensure_ascii=False,
863
+ )
864
+
865
+ async def _cleanup_native_resources(self, client, cached_content, uploaded_files: list) -> None:
866
+ """Deletes the cache resource and File API uploads created for this run."""
867
+ if cached_content:
868
+ try:
869
+ logger.info(f"Deleting cache resource: {cached_content.name}")
870
+ await client.aio.caches.delete(name=cached_content.name)
871
+ except Exception as cleanup_cache_err:
872
+ logger.warning(f"Failed to delete cache resource: {cleanup_cache_err}")
873
+ for file_ref in uploaded_files:
874
+ try:
875
+ await client.aio.files.delete(name=file_ref.name)
876
+ except Exception as cleanup_err:
877
+ logger.warning(f"Failed to delete uploaded file {file_ref.name}: {cleanup_err}")
878
+ logger.info(f"Explorer turn latencies: {self.turn_latencies}")