android-midscene-automation 0.2.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +35 -0
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +4 -2
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +12 -0
  507. package/src/App.vue +10 -5
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +340 -121
  525. package/src/types.ts +1 -1
@@ -0,0 +1,868 @@
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
+ """The canonical device-action manifest: every dialect of every action, side by side.
16
+
17
+ Historically each device action was declared independently at three model-facing
18
+ sites -- the Operator's inline LangChain shells, the Validator/Flash
19
+ ``ToolDeclaration`` constants, and the action MCP server's function signatures -- and
20
+ the declarations drifted apart (``click.target`` took an element index in one place
21
+ and only coordinates in another). This module is now the single place all three are
22
+ *defined*; the historical sites import their surface from here, generated.
23
+
24
+ An action has one agent-facing dialect and one wire dialect:
25
+
26
+ * ``operator`` -- the agent dialect. Both profiles receive the same indexed
27
+ "Visible UI Elements" list, so target parameters accept ``int`` element indices
28
+ as well as normalized coordinates. The Pro Operator binds it as a LangChain shell
29
+ (declaration-only, "Action Recorded": the Operator's translate step lowers the
30
+ call into structured decisions); the FlashRunner binds the very same description
31
+ and parameters projected onto a JSON ``ToolDeclaration`` (:func:`tool_declaration`),
32
+ and ``McpActionExecutor`` resolves an index against the current element list.
33
+ One definition, two bindings: the two profiles can no longer drift apart.
34
+ * ``declaration`` -- a JSON-only agent dialect, used solely for actions that have
35
+ no operator shell (``click_sequence`` is declared to Flash alone).
36
+ * ``wire`` -- what the action MCP server serves, mirroring the actuator protocol
37
+ one-to-one: normalized coordinates, millisecond durations, no addressing sugar.
38
+ The client-side executor (``McpActionExecutor``) lowers the agent dialect onto it.
39
+
40
+ A coordinate target names nothing by itself, so the agent dialect pairs it with
41
+ ``target_description`` (``target_descriptions`` for a ``click_sequence``): the
42
+ model's own statement of what it is aiming at. It is required whenever the target
43
+ is a coordinate, recorded verbatim on the action, and never sent to the wire. An
44
+ element index needs none -- its text, bounds and id are read from the indexed
45
+ list -- and the recorded fields stay separate (``target_text``/``target_bounds``/...
46
+ for observed elements, ``target_description`` for described coordinates) so no
47
+ reader can mistake the model's belief for an observation.
48
+
49
+ ``tests/unit/mcp/test_action_specs.py`` pins the generated schema of every model
50
+ surface and checks that the Flash declaration of each shared action is exactly the
51
+ operator shell's schema, so a divergence between what one profile sees and what the
52
+ other sees -- the historical definition of drift -- fails CI instead of shipping.
53
+
54
+ The classification of actions (required/optional/internal) stays in
55
+ ``action_manifest``; this module holds their schemas and teaching text.
56
+ """
57
+
58
+ from collections.abc import Awaitable, Callable
59
+ from dataclasses import dataclass
60
+ from functools import cache
61
+ import inspect
62
+ import types
63
+ from typing import Annotated, Any, Literal, Union, get_args, get_origin
64
+
65
+ from langchain_core.tools import StructuredTool
66
+ from mcp.types import CallToolResult
67
+ from pydantic import Field, create_model
68
+
69
+ from artemis.core.tool_declaration import ToolDeclaration
70
+ from artemis.mcp.action_types import ActionResult
71
+
72
+ __all__ = [
73
+ "ACTION_SPECS",
74
+ "ActionSpec",
75
+ "DeclarationDialect",
76
+ "EXCEPTION_PREFIXES",
77
+ "OPERATOR_SHELL_ORDER",
78
+ "OperatorDialect",
79
+ "ParamSpec",
80
+ "WireDialect",
81
+ "exception_prefix",
82
+ "make_wire_handler",
83
+ "operator_shell_tool",
84
+ "tool_declaration",
85
+ "wire_dialects",
86
+ ]
87
+
88
+
89
+ # --- Spec structure ------------------------------------------------------------------
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class ParamSpec:
94
+ """One parameter of a Python-typed dialect (operator shell or wire)."""
95
+
96
+ name: str
97
+ annotation: Any
98
+ description: str | None = None
99
+ required: bool = True
100
+ default: Any = None
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class OperatorDialect:
105
+ """The agent dialect: index-capable targets, one wording for both profiles.
106
+
107
+ Bound by the Pro Operator as a declaration-only LangChain shell and by the
108
+ FlashRunner as the JSON projection of the same parameters.
109
+ """
110
+
111
+ description: str
112
+ params: tuple[ParamSpec, ...]
113
+
114
+
115
+ @dataclass(frozen=True)
116
+ class DeclarationDialect:
117
+ """A JSON-only agent dialect, for actions without an operator shell."""
118
+
119
+ description: str
120
+ parameters: dict[str, Any]
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class WireDialect:
125
+ """The action-server tool mirroring one actuator method.
126
+
127
+ ``bind`` receives the actuator and the validated, default-filled argument dict
128
+ and performs the (tiny) call-site conversion the historical hand-written wrapper
129
+ performed -- typically ``int()`` coercion of coordinate pairs.
130
+ """
131
+
132
+ description: str
133
+ params: tuple[ParamSpec, ...]
134
+ bind: Callable[[Any, dict[str, Any]], Awaitable[ActionResult]]
135
+
136
+
137
+ @dataclass(frozen=True)
138
+ class ActionSpec:
139
+ """All dialects of one canonical device action, plus their declared differences.
140
+
141
+ Attributes:
142
+ name: The canonical tool name every dialect shares.
143
+ operator: The agent dialect (Operator shell and, projected, the Flash
144
+ declaration), or ``None`` when neither profile binds this action as a
145
+ shell.
146
+ declaration: A JSON-only agent dialect for actions without an operator
147
+ shell, or ``None`` (an action can be wire-only, reachable through
148
+ executors but never declared to a model directly).
149
+ wire: Action-server dialect; ``None`` only for purely virtual actions.
150
+ differences: Human-readable record of every deliberate divergence between
151
+ the agent dialect and the wire (semantics, not just spelling). Empty
152
+ means the dialects align modulo the documented dialect philosophy above.
153
+ """
154
+
155
+ name: str
156
+ operator: OperatorDialect | None = None
157
+ declaration: DeclarationDialect | None = None
158
+ wire: WireDialect | None = None
159
+ differences: str = ""
160
+
161
+
162
+ # --- Shared type aliases -------------------------------------------------------------
163
+
164
+ #: Agent-dialect target: element index into the indexed UI list, or a normalized
165
+ #: [x, y] pair.
166
+ IndexOrCoords = int | list[int]
167
+
168
+ Direction = Literal["up", "down", "left", "right"]
169
+
170
+ #: Wording for the description that a coordinate target must carry. An element
171
+ #: index already names its element (its text, id and bounds are recorded from the
172
+ #: indexed list); a bare coordinate names nothing, so the model states what it is
173
+ #: aiming at and that statement is recorded as ``target_description``.
174
+ _TARGET_DESCRIPTION = (
175
+ "What the target is, in a few words (e.g. 'play button', 'search input',"
176
+ " 'video body'). REQUIRED when target is a coordinate pair; ignored for an"
177
+ " element index."
178
+ )
179
+
180
+
181
+ # --- Wire bindings -------------------------------------------------------------------
182
+ # Each binding reproduces the exact conversion of the historical hand-written FastMCP
183
+ # wrapper it replaces. Arguments arrive validated and default-filled.
184
+
185
+
186
+ async def _wire_click(actuator: Any, a: dict[str, Any]) -> ActionResult:
187
+ return await actuator.click(
188
+ int(a["target"][0]), int(a["target"][1]), times=a["times"], delay_ms=a["delay_ms"]
189
+ )
190
+
191
+
192
+ async def _wire_click_sequence(actuator: Any, a: dict[str, Any]) -> ActionResult:
193
+ points = [(int(p[0]), int(p[1])) for p in a["sequence"]]
194
+ return await actuator.click_sequence(points, delay_ms=a["delay_ms"])
195
+
196
+
197
+ async def _wire_long_press(actuator: Any, a: dict[str, Any]) -> ActionResult:
198
+ return await actuator.long_press(
199
+ int(a["target"][0]), int(a["target"][1]), duration_ms=a["duration_ms"]
200
+ )
201
+
202
+
203
+ async def _wire_input_text(actuator: Any, a: dict[str, Any]) -> ActionResult:
204
+ target = a["target"]
205
+ norm = (int(target[0]), int(target[1])) if target else None
206
+ return await actuator.input_text(a["text"], norm, clear_exist=a["clear_exist"])
207
+
208
+
209
+ async def _wire_swipe(actuator: Any, a: dict[str, Any]) -> ActionResult:
210
+ start, end = a["start"], a["end"]
211
+ return await actuator.swipe(
212
+ (int(start[0]), int(start[1])), (int(end[0]), int(end[1])), a["duration_ms"]
213
+ )
214
+
215
+
216
+ async def _wire_press_key(actuator: Any, a: dict[str, Any]) -> ActionResult:
217
+ return await actuator.press_key(a["key"])
218
+
219
+
220
+ async def _wire_manage_app(actuator: Any, a: dict[str, Any]) -> ActionResult:
221
+ return await actuator.manage_app(a["action"], a["app_name"])
222
+
223
+
224
+ async def _wire_wait_for_delay(actuator: Any, a: dict[str, Any]) -> ActionResult:
225
+ return await actuator.wait_for_delay(a["time_in_ms"])
226
+
227
+
228
+ async def _wire_wait_for_text(actuator: Any, a: dict[str, Any]) -> ActionResult:
229
+ return await actuator.wait_for_text(a["text"], a["wait_state"], a["timeout_ms"])
230
+
231
+
232
+ async def _wire_open_link(actuator: Any, a: dict[str, Any]) -> ActionResult:
233
+ return await actuator.open_link(a["url"])
234
+
235
+
236
+ async def _wire_erase_one_char(actuator: Any, a: dict[str, Any]) -> ActionResult:
237
+ return await actuator.erase_one_char()
238
+
239
+
240
+ async def _wire_focus_and_clear_text(actuator: Any, a: dict[str, Any]) -> ActionResult:
241
+ return await actuator.focus_and_clear_text(int(a["target"][0]), int(a["target"][1]))
242
+
243
+
244
+ # --- The manifest --------------------------------------------------------------------
245
+
246
+ _SPECS: tuple[ActionSpec, ...] = (
247
+ ActionSpec(
248
+ name="click",
249
+ operator=OperatorDialect(
250
+ description=(
251
+ "[ACTION] Click on the target location on the screen (supports element"
252
+ " index or absolute normalized coordinates)."
253
+ ),
254
+ params=(
255
+ ParamSpec(
256
+ "target",
257
+ IndexOrCoords,
258
+ "Click target. Can be an element index number (int, e.g. 3) OR"
259
+ " normalized coordinates (list of 2 integers, e.g. [500, 600]).",
260
+ ),
261
+ ParamSpec(
262
+ "target_description",
263
+ str | None,
264
+ _TARGET_DESCRIPTION,
265
+ required=False,
266
+ default=None,
267
+ ),
268
+ ParamSpec(
269
+ "times",
270
+ int,
271
+ "Number of consecutive clicks on this target. Use this for"
272
+ " double-clicks or multi-clicks (e.g. 7 to enter developer"
273
+ " mode). Default is 1.",
274
+ required=False,
275
+ default=1,
276
+ ),
277
+ ParamSpec(
278
+ "delay_ms",
279
+ int,
280
+ "Delay in milliseconds between consecutive clicks. Default is 100.",
281
+ required=False,
282
+ default=100,
283
+ ),
284
+ ),
285
+ ),
286
+ wire=WireDialect(
287
+ description="Tap at a 0-1000 normalized [x, y] coordinate.",
288
+ params=(
289
+ ParamSpec("target", list[int]),
290
+ ParamSpec("times", int, required=False, default=1),
291
+ ParamSpec("delay_ms", int, required=False, default=100),
292
+ ),
293
+ bind=_wire_click,
294
+ ),
295
+ differences=(
296
+ "target: the agent dialect accepts an element index or a coordinate pair"
297
+ " (an index is resolved client-side against the indexed element list); the"
298
+ " wire takes coordinates only. target_description: recorded, never sent"
299
+ " to the wire; required only for coordinate targets."
300
+ ),
301
+ ),
302
+ ActionSpec(
303
+ name="click_sequence",
304
+ declaration=DeclarationDialect(
305
+ description=(
306
+ "[ACTION] Executes a sequence of taps one by one in order on the"
307
+ " specified targets (e.g. [[500, 280], [885, 362]]). The screen will be returned ONLY after all clicks"
308
+ " in the sequence have completed."
309
+ ),
310
+ parameters={
311
+ "type": "object",
312
+ "properties": {
313
+ "sequence": {
314
+ "type": "array",
315
+ "items": {
316
+ "type": "array",
317
+ "items": {"type": "integer"},
318
+ "description": (
319
+ "Normalized coordinates [x, y] in 0-1000 scale (e.g., [500, 280])."
320
+ ),
321
+ },
322
+ "description": (
323
+ "List of targets to tap in sequence, e.g. [[500, 280], [885, 362]]."
324
+ ),
325
+ },
326
+ "target_descriptions": {
327
+ "type": "array",
328
+ "items": {"type": "string"},
329
+ "description": (
330
+ "What each target is, in a few words, one entry per sequence"
331
+ " entry in the same order (e.g. ['video body', 'skip button'])."
332
+ " Required: a coordinate names nothing by itself."
333
+ ),
334
+ },
335
+ "delay_ms": {
336
+ "type": "integer",
337
+ "description": (
338
+ "Delay between consecutive taps in milliseconds (default 50ms)."
339
+ ),
340
+ },
341
+ },
342
+ "required": ["sequence", "target_descriptions"],
343
+ },
344
+ ),
345
+ wire=WireDialect(
346
+ description=("Tap a series of 0-1000 normalized [x, y] points in one atomic burst."),
347
+ params=(
348
+ ParamSpec("sequence", list[list[int]]),
349
+ ParamSpec("delay_ms", int, required=False, default=50),
350
+ ),
351
+ bind=_wire_click_sequence,
352
+ ),
353
+ differences=(
354
+ "Declared to Flash only (the Pro Operator chains actions as a fast-action"
355
+ " burst instead); sequence entries are normalized coordinate pairs in both"
356
+ " dialects (an element index is refused, never resolved client-side)."
357
+ " target_descriptions is recorded per entry and never sent to the wire."
358
+ " Unlike click, a burst is not resolved against the element list: its"
359
+ " whole point is to outrun a transient UI, and the list was captured"
360
+ " before the trigger tap."
361
+ ),
362
+ ),
363
+ ActionSpec(
364
+ name="long_press",
365
+ operator=OperatorDialect(
366
+ description=(
367
+ "[ACTION] Long press on the target location on the screen (supports"
368
+ " element index or absolute normalized coordinates)."
369
+ ),
370
+ params=(
371
+ ParamSpec(
372
+ "target",
373
+ IndexOrCoords,
374
+ "Long press target. Can be an element index number (int, e.g."
375
+ " 3) OR normalized coordinates (list of 2 integers, e.g. [500,"
376
+ " 600]).",
377
+ ),
378
+ ParamSpec(
379
+ "target_description",
380
+ str | None,
381
+ _TARGET_DESCRIPTION,
382
+ required=False,
383
+ default=None,
384
+ ),
385
+ ParamSpec(
386
+ "duration",
387
+ int,
388
+ "Long press duration in milliseconds (default 1000).",
389
+ required=False,
390
+ default=1000,
391
+ ),
392
+ ),
393
+ ),
394
+ wire=WireDialect(
395
+ description="Long-press at a 0-1000 normalized [x, y] coordinate.",
396
+ params=(
397
+ ParamSpec("target", list[int]),
398
+ ParamSpec("duration_ms", int, required=False, default=1000),
399
+ ),
400
+ bind=_wire_long_press,
401
+ ),
402
+ differences=(
403
+ "target: the agent dialect accepts an element index or a coordinate pair;"
404
+ " the wire takes coordinates only. target_description: recorded, never"
405
+ " sent to the wire; required only for coordinate targets."
406
+ " duration: the agent dialect spells the duration parameter `duration`"
407
+ " (structured decisions and recorded traces carry that key); the wire"
408
+ " spells it `duration_ms`. Executors accept both spellings."
409
+ ),
410
+ ),
411
+ ActionSpec(
412
+ name="input_text",
413
+ operator=OperatorDialect(
414
+ description=(
415
+ "[ACTION] Type text into the target input field (supports replacing"
416
+ " whole text or appending to the end, and multi-line strings with"
417
+ " '\\n')."
418
+ ),
419
+ params=(
420
+ ParamSpec(
421
+ "text",
422
+ str,
423
+ "The text content to input. Supports multi-line content with '\\n'.",
424
+ ),
425
+ ParamSpec(
426
+ "target",
427
+ IndexOrCoords,
428
+ "Input target field. Can be an input box element index number"
429
+ " (int, e.g. 3) OR normalized coordinates (list of 2 integers,"
430
+ " e.g. [500, 600]).",
431
+ ),
432
+ ParamSpec(
433
+ "target_description",
434
+ str | None,
435
+ _TARGET_DESCRIPTION,
436
+ required=False,
437
+ default=None,
438
+ ),
439
+ ParamSpec(
440
+ "clear_exist",
441
+ bool,
442
+ "Whether to clear existing text before typing. True (default):"
443
+ " clear/replace entire text. False: append at the end of"
444
+ " existing content.",
445
+ required=False,
446
+ default=True,
447
+ ),
448
+ ),
449
+ ),
450
+ wire=WireDialect(
451
+ description=("Type text, optionally focusing a 0-1000 normalized [x, y] target first."),
452
+ params=(
453
+ ParamSpec("text", str),
454
+ ParamSpec("target", list[int] | None, required=False, default=None),
455
+ ParamSpec("clear_exist", bool, required=False, default=True),
456
+ ),
457
+ bind=_wire_input_text,
458
+ ),
459
+ differences=(
460
+ "target: the agent dialect accepts an element index or a coordinate pair,"
461
+ " and requires a target; the wire allows omitting the target to type into"
462
+ " the already-focused field. target_description: recorded, never sent to"
463
+ " the wire; required only for coordinate targets."
464
+ ),
465
+ ),
466
+ ActionSpec(
467
+ name="swipe",
468
+ operator=OperatorDialect(
469
+ description=(
470
+ "[ACTION] Perform a swipe, drag, or slider-adjustment gesture on the screen.\n"
471
+ "\n"
472
+ "• Directional Scrolling ('direction'): Recommended for general browsing and standard page scrolling in most scenarios. Automatically computes safe swipe vectors and adaptive duration, retains a ~40% visual overlap anchor for zero-omission traversal, and prevents inertial flings. Supports scoping to a sub-container via 'target'. If it fails on certain custom layouts, fall back to specifying exact coordinates ('start' and 'end') directly.\n"
473
+ "• Precise Coordinate Gestures ('start', 'end'): Best for local, fine-grained interactions such as adjusting sliders/SeekBars (e.g., volume, brightness, progress bars), drag-and-drop / list reordering, or as a reliable fallback when directional scrolling fails on specific containers. Always drag slightly PAST the target position to overcome touch slop and reliably trigger the update. When setting a slider to Maximum (100%) or Minimum (0%), swipe fully to the extreme boundary.\n"
474
+ "\n"
475
+ "Args:\n"
476
+ " direction: Smart directional scrolling ('up', 'down', 'left', 'right'). Automatically computes safe swipe vectors, retaining 40% visual overlap: 'up' (reveal content below), 'down' (reveal content above), 'left', 'right'.\n"
477
+ " start: Start normalized coordinates [start_x, start_y] in 0-1000 scale.\n"
478
+ " end: End normalized coordinates [end_x, end_y] in 0-1000 scale.\n"
479
+ " target: Optional target element index (e.g. 2) or container bounds [left, top, right, bottom] to scope the directional swipe within.\n"
480
+ " gesture: Backward-compatible parameter: direction string OR custom coordinates list [start_x, start_y, end_x, end_y] in 0-1000 scale.\n"
481
+ " target_description: What is being dragged, in a few words (e.g. 'brightness slider knob'). REQUIRED for coordinate gestures ('start'/'end' or a coordinates list); ignored for directional scrolling.\n"
482
+ " duration: Optional gesture duration in milliseconds (default 800)."
483
+ ),
484
+ params=(
485
+ ParamSpec(
486
+ "direction",
487
+ Direction | None,
488
+ "Direction for scrolling and swiping: 'up' (drags bottom-to-top, scrolling down to reveal content below),"
489
+ " 'down' (drags top-to-bottom, scrolling up to reveal content above),"
490
+ " 'left' (drags right-to-left, scrolling right),"
491
+ " 'right' (drags left-to-right, scrolling left).",
492
+ required=False,
493
+ default=None,
494
+ ),
495
+ ParamSpec(
496
+ "start",
497
+ list[int] | None,
498
+ "Start normalized coordinates [start_x, start_y] in 0-1000 scale for precise,"
499
+ " local interactions (e.g. adjusting sliders, SeekBars, fine range selection, or drag-and-drop).",
500
+ required=False,
501
+ default=None,
502
+ ),
503
+ ParamSpec(
504
+ "end",
505
+ list[int] | None,
506
+ "End normalized coordinates [end_x, end_y] in 0-1000 scale for precise,"
507
+ " local interactions (e.g. adjusting sliders, SeekBars, fine range selection, or drag-and-drop).",
508
+ required=False,
509
+ default=None,
510
+ ),
511
+ ParamSpec(
512
+ "target",
513
+ int | list[int] | str | None,
514
+ "Optional target element index (e.g. 2) or container bounds [left, top, right, bottom] to scope the directional swipe within.",
515
+ required=False,
516
+ default=None,
517
+ ),
518
+ ParamSpec(
519
+ "gesture",
520
+ Direction | list[int] | None,
521
+ "Backward-compatible swipe gesture: smart direction string ('up', 'down', 'left', 'right')"
522
+ " OR precise custom coordinates [start_x, start_y, end_x, end_y] in 0-1000 scale.",
523
+ required=False,
524
+ default=None,
525
+ ),
526
+ ParamSpec(
527
+ "target_description",
528
+ str | None,
529
+ "What is being dragged, in a few words (e.g. 'brightness slider"
530
+ " knob'). REQUIRED for coordinate gestures ('start'/'end' or a"
531
+ " coordinates list); ignored for directional scrolling.",
532
+ required=False,
533
+ default=None,
534
+ ),
535
+ ParamSpec(
536
+ "duration",
537
+ int | None,
538
+ "Optional swipe/drag duration in milliseconds (default 800; computed"
539
+ " automatically for directional swipes).",
540
+ required=False,
541
+ default=None,
542
+ ),
543
+ ),
544
+ ),
545
+ wire=WireDialect(
546
+ description="Swipe between two 0-1000 normalized [x, y] points.",
547
+ params=(
548
+ ParamSpec("start", list[int]),
549
+ ParamSpec("end", list[int]),
550
+ ParamSpec("duration_ms", int, required=False, default=800),
551
+ ),
552
+ bind=_wire_swipe,
553
+ ),
554
+ differences=(
555
+ "target_description: recorded, never sent to the wire; required for"
556
+ " coordinate gestures only. gesture: the legacy combined"
557
+ " direction-or-coordinates parameter (executors also accept the older"
558
+ " `action` spelling). Smart directional swipes exist only in the agent"
559
+ " dialect; the wire takes a resolved start/end pair, and direction"
560
+ " resolution happens client-side against the live UI tree."
561
+ ),
562
+ ),
563
+ ActionSpec(
564
+ name="press_key",
565
+ operator=OperatorDialect(
566
+ description=(
567
+ "[ACTION] Press a physical or virtual system button (e.g. ENTER, BACK,"
568
+ " HOME, APP_SWITCH)."
569
+ ),
570
+ params=(
571
+ ParamSpec(
572
+ "key",
573
+ Literal["ENTER", "BACK", "HOME", "APP_SWITCH"],
574
+ "Standard Android system button name (ENTER, BACK, HOME, APP_SWITCH).",
575
+ ),
576
+ ),
577
+ ),
578
+ wire=WireDialect(
579
+ description=(
580
+ "Press a device key (home, back, enter, delete, tab, search, menu, app_switch)."
581
+ ),
582
+ params=(ParamSpec("key", str),),
583
+ bind=_wire_press_key,
584
+ ),
585
+ differences=(
586
+ "key vocabulary: the agent dialect is a closed uppercase enum (the"
587
+ " Operator's translate step emits Android KEYCODE_* names); the wire"
588
+ " takes the actuator's bare lowercase key words. `to_canonical_call` and"
589
+ " the executors normalize between them."
590
+ ),
591
+ ),
592
+ ActionSpec(
593
+ name="manage_app",
594
+ operator=OperatorDialect(
595
+ description="[ACTION] Launch or force stop a specified application.",
596
+ params=(
597
+ ParamSpec("action", Literal["launch", "stop"], "The action type."),
598
+ ParamSpec(
599
+ "app_name",
600
+ str,
601
+ "Display name or package name of the application.",
602
+ ),
603
+ ),
604
+ ),
605
+ wire=WireDialect(
606
+ description="Launch or stop an app by human-readable name or package.",
607
+ params=(
608
+ ParamSpec("action", str),
609
+ ParamSpec("app_name", str),
610
+ ),
611
+ bind=_wire_manage_app,
612
+ ),
613
+ ),
614
+ ActionSpec(
615
+ name="wait_for_delay",
616
+ operator=OperatorDialect(
617
+ description=(
618
+ "[ACTION] Pause execution and wait for a specified duration in milliseconds.\n"
619
+ "\n"
620
+ "Use this whenever you need time to elapse—whether for UI loading, animations,\n"
621
+ "screen transitions, or longer scheduled delays and intervals specified in the task."
622
+ ),
623
+ params=(
624
+ ParamSpec(
625
+ "time_in_ms",
626
+ int,
627
+ "The exact duration to wait in milliseconds. Accurately convert the"
628
+ " required time duration into milliseconds based on your objective"
629
+ " or plan (e.g., 2000 for 2s, 5000 for 5s, 60000 for 1 minute,"
630
+ " 180000 for 3 minutes, 300000 for 5 minutes).",
631
+ ),
632
+ ),
633
+ ),
634
+ wire=WireDialect(
635
+ description="Wait for a fixed number of milliseconds.",
636
+ params=(ParamSpec("time_in_ms", int),),
637
+ bind=_wire_wait_for_delay,
638
+ ),
639
+ ),
640
+ ActionSpec(
641
+ name="wait_for_text",
642
+ declaration=None, # Listed in the legacy tool-name sets but never declared to
643
+ # any LLM; kept wire-reachable for executors and external MCP clients.
644
+ wire=WireDialect(
645
+ description="Wait for text to appear on or disappear from the screen.",
646
+ params=(
647
+ ParamSpec("text", str),
648
+ ParamSpec("wait_state", str, required=False, default="appear"),
649
+ ParamSpec("timeout_ms", int, required=False, default=5000),
650
+ ),
651
+ bind=_wire_wait_for_text,
652
+ ),
653
+ ),
654
+ ActionSpec(
655
+ name="open_link",
656
+ wire=WireDialect(
657
+ description="Open a URL on the device.",
658
+ params=(ParamSpec("url", str),),
659
+ bind=_wire_open_link,
660
+ ),
661
+ ),
662
+ ActionSpec(
663
+ name="erase_one_char",
664
+ wire=WireDialect(
665
+ description="Erase a single character in the focused field.",
666
+ params=(),
667
+ bind=_wire_erase_one_char,
668
+ ),
669
+ ),
670
+ ActionSpec(
671
+ name="focus_and_clear_text",
672
+ wire=WireDialect(
673
+ description=("Focus the field at a 0-1000 normalized [x, y] and clear its text."),
674
+ params=(ParamSpec("target", list[int]),),
675
+ bind=_wire_focus_and_clear_text,
676
+ ),
677
+ ),
678
+ )
679
+
680
+ #: Canonical manifest, keyed by action name, in declaration order.
681
+ ACTION_SPECS: dict[str, ActionSpec] = {spec.name: spec for spec in _SPECS}
682
+
683
+ #: The order the Operator binds its shells (historically the factory-dict order,
684
+ #: identical to the prompt's "Physical device actions" enumeration order).
685
+ OPERATOR_SHELL_ORDER: tuple[str, ...] = (
686
+ "click",
687
+ "input_text",
688
+ "swipe",
689
+ "press_key",
690
+ "manage_app",
691
+ "wait_for_delay",
692
+ "long_press",
693
+ )
694
+
695
+ #: Exception wording per action; matches the historical executor `except` arms.
696
+ EXCEPTION_PREFIXES: dict[str, str] = {
697
+ "click": "Error during click",
698
+ "click_sequence": "Error executing click sequence",
699
+ "long_press": "Error during long press",
700
+ "input_text": "Error during input text",
701
+ "swipe": "Error during swipe",
702
+ "press_key": "Error during press_key",
703
+ "manage_app": "Error during manage_app",
704
+ "wait_for_delay": "Error during wait_for_delay",
705
+ "wait_for_text": "Error during wait_for_text",
706
+ }
707
+
708
+
709
+ def exception_prefix(action: str) -> str:
710
+ """Returns the historical human-readable exception prefix for an action."""
711
+ return EXCEPTION_PREFIXES.get(action, f"Error during {action}")
712
+
713
+
714
+ # --- Projections ---------------------------------------------------------------------
715
+
716
+
717
+ @cache
718
+ def _operator_args_model(name: str):
719
+ """Builds (once) the pydantic argument model for an operator shell."""
720
+ dialect = ACTION_SPECS[name].operator
721
+ if dialect is None:
722
+ raise ValueError(f"Action '{name}' has no operator dialect.")
723
+ fields: dict[str, Any] = {}
724
+ for p in dialect.params:
725
+ if p.required:
726
+ fields[p.name] = (p.annotation, Field(description=p.description))
727
+ else:
728
+ fields[p.name] = (
729
+ p.annotation,
730
+ Field(default=p.default, description=p.description),
731
+ )
732
+ return create_model(name, **fields)
733
+
734
+
735
+ def operator_shell_tool(name: str) -> StructuredTool:
736
+ """Builds the Operator's declaration-only shell for one action.
737
+
738
+ The body is never executed by the Operator loop -- action calls are translated
739
+ into structured decisions instead -- but returns the historical marker string for
740
+ any caller that does invoke it.
741
+ """
742
+ dialect = ACTION_SPECS[name].operator
743
+ if dialect is None:
744
+ raise ValueError(f"Action '{name}' has no operator dialect.")
745
+ return StructuredTool(
746
+ name=name,
747
+ description=dialect.description,
748
+ args_schema=_operator_args_model(name),
749
+ func=lambda **kwargs: "Action Recorded",
750
+ )
751
+
752
+
753
+ _SCALAR_JSON: dict[Any, dict[str, str]] = {
754
+ bool: {"type": "boolean"},
755
+ int: {"type": "integer"},
756
+ float: {"type": "number"},
757
+ str: {"type": "string"},
758
+ }
759
+
760
+
761
+ def _json_schema(annotation: Any) -> dict[str, Any]:
762
+ """Projects one Python-typed parameter annotation onto JSON schema.
763
+
764
+ ``None`` members of a union only make the parameter optional (its
765
+ ``required`` flag says so); they never appear in the projected schema, so
766
+ the declaration carries no ``null`` alternatives or ``default`` noise.
767
+ """
768
+ origin = get_origin(annotation)
769
+ if origin in (Union, types.UnionType):
770
+ members = [a for a in get_args(annotation) if a is not type(None)]
771
+ schemas = [_json_schema(m) for m in members]
772
+ return schemas[0] if len(schemas) == 1 else {"anyOf": schemas}
773
+ if origin is Literal:
774
+ return {"type": "string", "enum": list(get_args(annotation))}
775
+ if origin is list:
776
+ (item,) = get_args(annotation) or (str,)
777
+ return {"type": "array", "items": _json_schema(item)}
778
+ try:
779
+ return dict(_SCALAR_JSON[annotation])
780
+ except KeyError:
781
+ raise TypeError(f"No JSON projection for parameter annotation {annotation!r}.") from None
782
+
783
+
784
+ def _projected_parameters(dialect: OperatorDialect) -> dict[str, Any]:
785
+ """The operator shell's parameters as a JSON-schema ``parameters`` object."""
786
+ properties: dict[str, Any] = {}
787
+ required: list[str] = []
788
+ for p in dialect.params:
789
+ schema = _json_schema(p.annotation)
790
+ if p.description:
791
+ schema["description"] = p.description
792
+ properties[p.name] = schema
793
+ if p.required:
794
+ required.append(p.name)
795
+ return {"type": "object", "properties": properties, "required": required}
796
+
797
+
798
+ @cache
799
+ def tool_declaration(name: str) -> ToolDeclaration:
800
+ """Builds the Flash ``ToolDeclaration`` for one action.
801
+
802
+ A shared action is the operator shell projected onto JSON (same description,
803
+ same parameters, same targeting semantics); a JSON-only action uses its own
804
+ ``DeclarationDialect``.
805
+ """
806
+ spec = ACTION_SPECS[name]
807
+ if spec.declaration is not None:
808
+ return ToolDeclaration(
809
+ name=name,
810
+ description=spec.declaration.description,
811
+ parameters=spec.declaration.parameters,
812
+ )
813
+ if spec.operator is not None:
814
+ return ToolDeclaration(
815
+ name=name,
816
+ description=spec.operator.description,
817
+ parameters=_projected_parameters(spec.operator),
818
+ )
819
+ raise ValueError(f"Action '{name}' has no agent dialect to declare.")
820
+
821
+
822
+ def wire_dialects() -> tuple[ActionSpec, ...]:
823
+ """Returns every spec that has a wire dialect, in manifest order."""
824
+ return tuple(spec for spec in _SPECS if spec.wire is not None)
825
+
826
+
827
+ def make_wire_handler(
828
+ spec: ActionSpec,
829
+ actuator: Any,
830
+ wrap: Callable[[ActionResult], CallToolResult],
831
+ wrap_exception: Callable[[str, Exception], CallToolResult],
832
+ ) -> Callable[..., Awaitable[CallToolResult]]:
833
+ """Builds the FastMCP tool function for one wire dialect.
834
+
835
+ The returned coroutine carries an explicit ``__signature__`` so FastMCP derives
836
+ the same argument model a literal ``async def`` produced historically, and the
837
+ same ``Annotated[CallToolResult, ActionResult]`` return annotation so the
838
+ structured-output schema is unchanged.
839
+ """
840
+ wire = spec.wire
841
+ if wire is None:
842
+ raise ValueError(f"Action '{spec.name}' has no wire dialect.")
843
+
844
+ async def handler(**kwargs: Any) -> Any:
845
+ try:
846
+ return wrap(await wire.bind(actuator, kwargs))
847
+ except Exception as e: # pylint: disable=broad-exception-caught
848
+ return wrap_exception(spec.name, e)
849
+
850
+ return_annotation = Annotated[CallToolResult, ActionResult]
851
+ parameters = [
852
+ inspect.Parameter(
853
+ p.name,
854
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
855
+ annotation=p.annotation,
856
+ default=inspect.Parameter.empty if p.required else p.default,
857
+ )
858
+ for p in wire.params
859
+ ]
860
+ handler.__name__ = spec.name
861
+ handler.__qualname__ = spec.name
862
+ handler.__doc__ = wire.description
863
+ handler.__signature__ = inspect.Signature( # type: ignore[attr-defined]
864
+ parameters, return_annotation=return_annotation
865
+ )
866
+ handler.__annotations__ = {p.name: p.annotation for p in wire.params}
867
+ handler.__annotations__["return"] = return_annotation
868
+ return handler