mobster-cli 0.4.3__py3-none-any.whl

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 (1009) hide show
  1. mobile_agent/__init__.py +4 -0
  2. mobile_agent/__main__.py +598 -0
  3. mobile_agent/accounts/__init__.py +1 -0
  4. mobile_agent/accounts/context.py +80 -0
  5. mobile_agent/accounts/mcp.py +55 -0
  6. mobile_agent/accounts/pacing.py +318 -0
  7. mobile_agent/accounts/register.py +78 -0
  8. mobile_agent/accounts/routes.py +212 -0
  9. mobile_agent/accounts/routing.py +69 -0
  10. mobile_agent/accounts/screen.py +273 -0
  11. mobile_agent/accounts/stats.py +54 -0
  12. mobile_agent/accounts/store.py +394 -0
  13. mobile_agent/agent_hooks.py +123 -0
  14. mobile_agent/ai_plans/__init__.py +7 -0
  15. mobile_agent/ai_plans/agents.py +96 -0
  16. mobile_agent/ai_plans/claude_code.py +208 -0
  17. mobile_agent/ai_plans/cli.py +400 -0
  18. mobile_agent/ai_plans/codex.py +258 -0
  19. mobile_agent/ai_plans/compat_chat.py +307 -0
  20. mobile_agent/ai_plans/device_login.py +164 -0
  21. mobile_agent/ai_plans/draft_client.py +68 -0
  22. mobile_agent/ai_plans/gemini.py +137 -0
  23. mobile_agent/ai_plans/instructions.py +65 -0
  24. mobile_agent/ai_plans/mcp_http.py +160 -0
  25. mobile_agent/ai_plans/phone_tools.py +892 -0
  26. mobile_agent/ai_plans/providers.py +235 -0
  27. mobile_agent/ai_plans/routes.py +160 -0
  28. mobile_agent/ai_plans/runner.py +400 -0
  29. mobile_agent/ai_plans/vault.py +114 -0
  30. mobile_agent/alerts.py +337 -0
  31. mobile_agent/android/__init__.py +18 -0
  32. mobile_agent/android/adb.py +268 -0
  33. mobile_agent/android/conditions.py +128 -0
  34. mobile_agent/android/doctor.py +54 -0
  35. mobile_agent/android/driver.py +137 -0
  36. mobile_agent/android/emulator.py +216 -0
  37. mobile_agent/android/logs.py +85 -0
  38. mobile_agent/android/manager.py +225 -0
  39. mobile_agent/android/records.py +31 -0
  40. mobile_agent/android/tree.py +96 -0
  41. mobile_agent/answer_shapes.py +230 -0
  42. mobile_agent/api_devices.py +333 -0
  43. mobile_agent/api_errors.py +17 -0
  44. mobile_agent/api_get.py +249 -0
  45. mobile_agent/api_http.py +467 -0
  46. mobile_agent/api_post.py +453 -0
  47. mobile_agent/api_routes.py +173 -0
  48. mobile_agent/app_api/__init__.py +4 -0
  49. mobile_agent/app_api/cli.py +151 -0
  50. mobile_agent/app_api/endpoints.py +193 -0
  51. mobile_agent/app_api/mcp.py +54 -0
  52. mobile_agent/app_api/openapi.py +146 -0
  53. mobile_agent/app_api/reading.py +172 -0
  54. mobile_agent/app_api/register.py +10 -0
  55. mobile_agent/app_api/screen.py +216 -0
  56. mobile_agent/app_api/service.py +382 -0
  57. mobile_agent/app_api/sessions.py +404 -0
  58. mobile_agent/app_api/tokens.py +167 -0
  59. mobile_agent/app_choice.py +131 -0
  60. mobile_agent/app_icons.py +183 -0
  61. mobile_agent/app_killer.py +69 -0
  62. mobile_agent/artifacts.py +606 -0
  63. mobile_agent/attach/__init__.py +13 -0
  64. mobile_agent/attach/cli.py +46 -0
  65. mobile_agent/attach/flutter.py +28 -0
  66. mobile_agent/attach/score.py +119 -0
  67. mobile_agent/attach/web.py +13 -0
  68. mobile_agent/attachments/__init__.py +1 -0
  69. mobile_agent/attachments/agent.py +418 -0
  70. mobile_agent/attachments/extract.py +217 -0
  71. mobile_agent/attachments/mcp.py +104 -0
  72. mobile_agent/attachments/register.py +73 -0
  73. mobile_agent/attachments/routes.py +326 -0
  74. mobile_agent/attachments/service.py +170 -0
  75. mobile_agent/attachments/sniff.py +152 -0
  76. mobile_agent/attachments/store.py +441 -0
  77. mobile_agent/audit_verify.py +436 -0
  78. mobile_agent/bench/README.md +410 -0
  79. mobile_agent/bench/__init__.py +0 -0
  80. mobile_agent/bench/__main__.py +394 -0
  81. mobile_agent/bench/ab.py +168 -0
  82. mobile_agent/bench/actuation.py +368 -0
  83. mobile_agent/bench/agents/__init__.py +9 -0
  84. mobile_agent/bench/agents/base.py +105 -0
  85. mobile_agent/bench/agents/plan_clients.py +245 -0
  86. mobile_agent/bench/agents/smart.py +533 -0
  87. mobile_agent/bench/checks.py +358 -0
  88. mobile_agent/bench/decide_gate.py +214 -0
  89. mobile_agent/bench/diagnose.py +181 -0
  90. mobile_agent/bench/evals/__init__.py +14 -0
  91. mobile_agent/bench/evals/build.py +565 -0
  92. mobile_agent/bench/evals/checkpoint.py +214 -0
  93. mobile_agent/bench/evals/irt.py +219 -0
  94. mobile_agent/bench/evals/l0.py +647 -0
  95. mobile_agent/bench/evals/l1.py +866 -0
  96. mobile_agent/bench/evals/models.py +124 -0
  97. mobile_agent/bench/evals/pairwise.py +189 -0
  98. mobile_agent/bench/evals/proxy.py +90 -0
  99. mobile_agent/bench/evals/sets.py +149 -0
  100. mobile_agent/bench/evals/traces.py +446 -0
  101. mobile_agent/bench/fixtures.py +176 -0
  102. mobile_agent/bench/guard_replay.py +129 -0
  103. mobile_agent/bench/h2-manifest.json +20 -0
  104. mobile_agent/bench/h3-manifest.json +20 -0
  105. mobile_agent/bench/heldout-manifest.json +32 -0
  106. mobile_agent/bench/heldout.py +375 -0
  107. mobile_agent/bench/iosworld.py +901 -0
  108. mobile_agent/bench/iosworld_stats.py +510 -0
  109. mobile_agent/bench/llm_units.py +116 -0
  110. mobile_agent/bench/manifest.json +93 -0
  111. mobile_agent/bench/metrics.py +66 -0
  112. mobile_agent/bench/phone.py +44 -0
  113. mobile_agent/bench/replay.py +172 -0
  114. mobile_agent/bench/report.py +207 -0
  115. mobile_agent/bench/runner.py +456 -0
  116. mobile_agent/bench/safety.py +310 -0
  117. mobile_agent/bench/stream_timing.py +213 -0
  118. mobile_agent/bench/suite.py +444 -0
  119. mobile_agent/bench/sweep.py +176 -0
  120. mobile_agent/bench/tape.py +798 -0
  121. mobile_agent/bench/tape_replay.py +88 -0
  122. mobile_agent/bench/trace_evals.py +128 -0
  123. mobile_agent/bench/trace_view.py +81 -0
  124. mobile_agent/bench/truth/iphone15pro.json +138 -0
  125. mobile_agent/bench/truth.py +340 -0
  126. mobile_agent/bench/tuning.py +543 -0
  127. mobile_agent/bench/tuning_analysis.py +656 -0
  128. mobile_agent/bench/vague-heldout-manifest.json +12 -0
  129. mobile_agent/bench/vague_rubric.py +135 -0
  130. mobile_agent/bench/vague_run.py +243 -0
  131. mobile_agent/brand.py +454 -0
  132. mobile_agent/brand_fonts.py +756 -0
  133. mobile_agent/build_identity.py +21 -0
  134. mobile_agent/cable_bt.py +623 -0
  135. mobile_agent/cable_bt_runtime.py +128 -0
  136. mobile_agent/catalog.py +69 -0
  137. mobile_agent/chart_svg.py +415 -0
  138. mobile_agent/chat_here.py +189 -0
  139. mobile_agent/cli_constants.py +5 -0
  140. mobile_agent/cli_parser.py +611 -0
  141. mobile_agent/cli_run.py +115 -0
  142. mobile_agent/compiled_read/__init__.py +21 -0
  143. mobile_agent/compiled_read/delta.py +89 -0
  144. mobile_agent/compiled_read/extractors.py +202 -0
  145. mobile_agent/compiled_read/routes.py +56 -0
  146. mobile_agent/completion.py +267 -0
  147. mobile_agent/compose.py +48 -0
  148. mobile_agent/config.py +114 -0
  149. mobile_agent/connectors/__init__.py +1 -0
  150. mobile_agent/connectors/api_session.py +76 -0
  151. mobile_agent/connectors/callback_page.py +146 -0
  152. mobile_agent/connectors/client.py +582 -0
  153. mobile_agent/connectors/github_app.py +55 -0
  154. mobile_agent/connectors/graph_api.py +396 -0
  155. mobile_agent/connectors/imports.py +170 -0
  156. mobile_agent/connectors/issue.py +66 -0
  157. mobile_agent/connectors/keystore.py +85 -0
  158. mobile_agent/connectors/marks.py +222 -0
  159. mobile_agent/connectors/oauth.py +476 -0
  160. mobile_agent/connectors/picker.py +228 -0
  161. mobile_agent/connectors/presets.py +591 -0
  162. mobile_agent/connectors/register.py +48 -0
  163. mobile_agent/connectors/routes.py +153 -0
  164. mobile_agent/connectors/service.py +966 -0
  165. mobile_agent/connectors/shared.py +136 -0
  166. mobile_agent/connectors/skill.py +446 -0
  167. mobile_agent/connectors/slack_api.py +269 -0
  168. mobile_agent/connectors/steps.py +92 -0
  169. mobile_agent/connectors/store.py +144 -0
  170. mobile_agent/connectors/vault.py +72 -0
  171. mobile_agent/console.py +178 -0
  172. mobile_agent/content_queue/__init__.py +1 -0
  173. mobile_agent/content_queue/client.py +263 -0
  174. mobile_agent/content_queue/importing.py +153 -0
  175. mobile_agent/content_queue/media_check.py +224 -0
  176. mobile_agent/content_queue/prompt.py +64 -0
  177. mobile_agent/content_queue/register.py +91 -0
  178. mobile_agent/content_queue/routes.py +370 -0
  179. mobile_agent/content_queue/runner.py +475 -0
  180. mobile_agent/content_queue/store.py +521 -0
  181. mobile_agent/contract.py +996 -0
  182. mobile_agent/contract_asks.py +248 -0
  183. mobile_agent/contract_prompt.py +45 -0
  184. mobile_agent/contract_screen.py +981 -0
  185. mobile_agent/contract_view.py +84 -0
  186. mobile_agent/corpus/__init__.py +26 -0
  187. mobile_agent/corpus/cli.py +63 -0
  188. mobile_agent/corpus/consent.py +55 -0
  189. mobile_agent/corpus/listener.py +76 -0
  190. mobile_agent/corpus/register.py +9 -0
  191. mobile_agent/corpus/skeleton.py +72 -0
  192. mobile_agent/corpus/store.py +143 -0
  193. mobile_agent/costs.py +161 -0
  194. mobile_agent/decider_scope.py +40 -0
  195. mobile_agent/decision_provider.py +189 -0
  196. mobile_agent/deeplinks.py +352 -0
  197. mobile_agent/device_info.py +116 -0
  198. mobile_agent/device_manager.py +978 -0
  199. mobile_agent/device_status.py +195 -0
  200. mobile_agent/device_targets.py +217 -0
  201. mobile_agent/devices.py +676 -0
  202. mobile_agent/devkit/__init__.py +64 -0
  203. mobile_agent/devkit/account_cli.py +90 -0
  204. mobile_agent/devkit/asc.py +562 -0
  205. mobile_agent/devkit/asc_cli.py +214 -0
  206. mobile_agent/devkit/ci.py +737 -0
  207. mobile_agent/devkit/ci_cli.py +196 -0
  208. mobile_agent/devkit/ci_hosts.py +194 -0
  209. mobile_agent/devkit/es256.py +197 -0
  210. mobile_agent/devkit/expo.py +440 -0
  211. mobile_agent/devkit/expo_cli.py +405 -0
  212. mobile_agent/devkit/expo_dev.py +203 -0
  213. mobile_agent/devkit/expo_routes.py +90 -0
  214. mobile_agent/devkit/fixtures_cli.py +133 -0
  215. mobile_agent/devkit/fixtures_routes.py +82 -0
  216. mobile_agent/devkit/links.py +375 -0
  217. mobile_agent/devkit/links_cli.py +123 -0
  218. mobile_agent/devkit/links_routes.py +83 -0
  219. mobile_agent/devkit/nightly.py +152 -0
  220. mobile_agent/devkit/shots.py +203 -0
  221. mobile_agent/devkit/testflight.py +155 -0
  222. mobile_agent/devkit/testflight_cli.py +127 -0
  223. mobile_agent/devkit/testflight_routes.py +111 -0
  224. mobile_agent/devkit/xcode.py +236 -0
  225. mobile_agent/devkit/xcode_cli.py +101 -0
  226. mobile_agent/devtools.py +419 -0
  227. mobile_agent/doctor.py +509 -0
  228. mobile_agent/doctor_fix.py +467 -0
  229. mobile_agent/downloads.py +55 -0
  230. mobile_agent/drivers.py +793 -0
  231. mobile_agent/dry_run.py +102 -0
  232. mobile_agent/effect_ledger.py +83 -0
  233. mobile_agent/engine_health.py +378 -0
  234. mobile_agent/engine_hooks.py +96 -0
  235. mobile_agent/engines.py +408 -0
  236. mobile_agent/env_file.py +63 -0
  237. mobile_agent/env_secrets.py +149 -0
  238. mobile_agent/errors.py +15 -0
  239. mobile_agent/evals/__init__.py +3 -0
  240. mobile_agent/evals/app_survey.py +149 -0
  241. mobile_agent/evals/development/__init__.py +1 -0
  242. mobile_agent/evals/development/score.py +276 -0
  243. mobile_agent/evals/latency_report.py +312 -0
  244. mobile_agent/evals/offline_report.py +374 -0
  245. mobile_agent/evals/oracles.py +298 -0
  246. mobile_agent/evals/redteam/__init__.py +15 -0
  247. mobile_agent/evals/redteam/__main__.py +72 -0
  248. mobile_agent/evals/redteam/cases.py +730 -0
  249. mobile_agent/evals/redteam/harness.py +604 -0
  250. mobile_agent/evals/redteam/live.py +139 -0
  251. mobile_agent/evals/redteam/plain.py +85 -0
  252. mobile_agent/evals/vision/__init__.py +0 -0
  253. mobile_agent/evals/vision/images/dog_001.jpg +0 -0
  254. mobile_agent/evals/vision/images/dog_002.jpg +0 -0
  255. mobile_agent/evals/vision/images/dog_003.jpg +0 -0
  256. mobile_agent/evals/vision/images/dog_004.jpg +0 -0
  257. mobile_agent/evals/vision/images/dog_005.jpg +0 -0
  258. mobile_agent/evals/vision/images/dog_006.jpg +0 -0
  259. mobile_agent/evals/vision/images/dog_007.jpg +0 -0
  260. mobile_agent/evals/vision/images/dog_010.jpg +0 -0
  261. mobile_agent/evals/vision/images/dog_011.jpg +0 -0
  262. mobile_agent/evals/vision/images/dog_013.jpg +0 -0
  263. mobile_agent/evals/vision/images/dog_014.jpg +0 -0
  264. mobile_agent/evals/vision/images/dog_015.jpg +0 -0
  265. mobile_agent/evals/vision/images/dog_016.jpg +0 -0
  266. mobile_agent/evals/vision/images/dog_017.jpg +0 -0
  267. mobile_agent/evals/vision/images/dog_019.jpg +0 -0
  268. mobile_agent/evals/vision/images/dog_020.jpg +0 -0
  269. mobile_agent/evals/vision/images/dog_021.jpg +0 -0
  270. mobile_agent/evals/vision/images/dog_022.jpg +0 -0
  271. mobile_agent/evals/vision/images/dog_023.jpg +0 -0
  272. mobile_agent/evals/vision/images/dog_024.jpg +0 -0
  273. mobile_agent/evals/vision/images/dog_026.jpg +0 -0
  274. mobile_agent/evals/vision/images/dog_027.jpg +0 -0
  275. mobile_agent/evals/vision/images/dog_034.jpg +0 -0
  276. mobile_agent/evals/vision/images/dog_035.jpg +0 -0
  277. mobile_agent/evals/vision/images/dog_038.jpg +0 -0
  278. mobile_agent/evals/vision/images/dog_042.jpg +0 -0
  279. mobile_agent/evals/vision/images/dog_047.jpg +0 -0
  280. mobile_agent/evals/vision/images/dog_052.jpg +0 -0
  281. mobile_agent/evals/vision/images/dog_058.jpg +0 -0
  282. mobile_agent/evals/vision/images/dog_062.jpg +0 -0
  283. mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
  284. mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
  285. mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
  286. mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
  287. mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
  288. mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
  289. mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
  290. mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
  291. mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
  292. mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
  293. mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
  294. mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
  295. mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
  296. mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
  297. mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
  298. mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
  299. mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
  300. mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
  301. mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
  302. mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
  303. mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
  304. mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
  305. mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
  306. mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
  307. mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
  308. mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
  309. mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
  310. mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
  311. mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
  312. mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
  313. mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
  314. mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
  315. mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
  316. mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
  317. mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
  318. mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
  319. mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
  320. mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
  321. mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
  322. mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
  323. mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
  324. mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
  325. mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
  326. mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
  327. mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
  328. mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
  329. mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
  330. mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
  331. mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
  332. mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
  333. mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
  334. mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
  335. mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
  336. mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
  337. mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
  338. mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
  339. mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
  340. mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
  341. mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
  342. mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
  343. mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
  344. mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
  345. mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
  346. mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
  347. mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
  348. mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
  349. mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
  350. mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
  351. mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
  352. mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
  353. mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
  354. mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
  355. mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
  356. mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
  357. mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
  358. mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
  359. mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
  360. mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
  361. mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
  362. mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
  363. mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
  364. mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
  365. mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
  366. mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
  367. mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
  368. mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
  369. mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
  370. mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
  371. mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
  372. mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
  373. mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
  374. mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
  375. mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
  376. mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
  377. mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
  378. mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
  379. mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
  380. mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
  381. mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
  382. mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
  383. mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
  384. mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
  385. mobile_agent/evals/vision/manifest.json +2355 -0
  386. mobile_agent/eventbus.py +110 -0
  387. mobile_agent/explain.py +34 -0
  388. mobile_agent/failure_line.py +197 -0
  389. mobile_agent/fanout.py +663 -0
  390. mobile_agent/feedback/__init__.py +2 -0
  391. mobile_agent/feedback/bundle.py +204 -0
  392. mobile_agent/feedback/outbox.py +196 -0
  393. mobile_agent/feedback/register.py +9 -0
  394. mobile_agent/feedback/routes.py +177 -0
  395. mobile_agent/fleet.py +841 -0
  396. mobile_agent/fleet_health.py +729 -0
  397. mobile_agent/fleet_phone.py +186 -0
  398. mobile_agent/frontier/__init__.py +79 -0
  399. mobile_agent/frontier/act.py +380 -0
  400. mobile_agent/frontier/actions.py +70 -0
  401. mobile_agent/frontier/agent.py +256 -0
  402. mobile_agent/frontier/approvals.py +366 -0
  403. mobile_agent/frontier/hooks.py +199 -0
  404. mobile_agent/frontier/interrupts.py +125 -0
  405. mobile_agent/frontier/observe.py +331 -0
  406. mobile_agent/frontier/phases.py +381 -0
  407. mobile_agent/frontier/pipeline.py +61 -0
  408. mobile_agent/frontier/prompt.py +384 -0
  409. mobile_agent/frontier/protocol.py +91 -0
  410. mobile_agent/frontier/safety.py +467 -0
  411. mobile_agent/frontier/saved.py +375 -0
  412. mobile_agent/frontier/screen.py +419 -0
  413. mobile_agent/frontier/skills.py +147 -0
  414. mobile_agent/frontier/stages/__init__.py +1 -0
  415. mobile_agent/frontier/stages/after.py +192 -0
  416. mobile_agent/frontier/stages/approve.py +138 -0
  417. mobile_agent/frontier/stages/controls.py +115 -0
  418. mobile_agent/frontier/stages/ends.py +102 -0
  419. mobile_agent/frontier/stages/gate.py +92 -0
  420. mobile_agent/frontier/stages/guards.py +132 -0
  421. mobile_agent/frontier/stages/points.py +155 -0
  422. mobile_agent/frontier/stages/send.py +177 -0
  423. mobile_agent/frontier/turn.py +151 -0
  424. mobile_agent/full_disk_access.py +83 -0
  425. mobile_agent/gemini.py +69 -0
  426. mobile_agent/genuine.py +120 -0
  427. mobile_agent/graph/__init__.py +4 -0
  428. mobile_agent/graph/catalog.py +789 -0
  429. mobile_agent/graph/chain.py +306 -0
  430. mobile_agent/graph/dispatch.py +98 -0
  431. mobile_agent/graph/draft.py +373 -0
  432. mobile_agent/graph/edit.py +76 -0
  433. mobile_agent/graph/estimate.py +189 -0
  434. mobile_agent/graph/expr.py +562 -0
  435. mobile_agent/graph/functions.py +271 -0
  436. mobile_agent/graph/infer.py +144 -0
  437. mobile_agent/graph/nodes.py +936 -0
  438. mobile_agent/graph/phone_steps.py +178 -0
  439. mobile_agent/graph/rerun.py +327 -0
  440. mobile_agent/graph/run.py +899 -0
  441. mobile_agent/graph/schema.py +842 -0
  442. mobile_agent/graph/state.py +161 -0
  443. mobile_agent/graph/values.py +198 -0
  444. mobile_agent/harness/__init__.py +2 -0
  445. mobile_agent/harness/checkpoints.py +468 -0
  446. mobile_agent/harness/control.py +101 -0
  447. mobile_agent/harness/prewarm.py +213 -0
  448. mobile_agent/harness/register.py +43 -0
  449. mobile_agent/harness/routes.py +114 -0
  450. mobile_agent/harness/settings.py +66 -0
  451. mobile_agent/harness/tools/__init__.py +67 -0
  452. mobile_agent/harness/tools/ask_user.py +102 -0
  453. mobile_agent/harness/tools/watch.py +120 -0
  454. mobile_agent/harness_api.py +815 -0
  455. mobile_agent/hedge.py +203 -0
  456. mobile_agent/help_topics.py +305 -0
  457. mobile_agent/hints.py +248 -0
  458. mobile_agent/history.py +69 -0
  459. mobile_agent/home.py +214 -0
  460. mobile_agent/http_pool.py +175 -0
  461. mobile_agent/image_policy.py +139 -0
  462. mobile_agent/inbox.py +148 -0
  463. mobile_agent/installed_apps.py +344 -0
  464. mobile_agent/integrations/__init__.py +1 -0
  465. mobile_agent/integrations/cli.py +472 -0
  466. mobile_agent/integrations/client_files.py +65 -0
  467. mobile_agent/integrations/clients.py +998 -0
  468. mobile_agent/integrations/doctor.py +185 -0
  469. mobile_agent/integrations/jsonc.py +383 -0
  470. mobile_agent/integrations/routes.py +135 -0
  471. mobile_agent/integrations/rules.py +112 -0
  472. mobile_agent/integrations/server_options.py +46 -0
  473. mobile_agent/integrations/skill.py +152 -0
  474. mobile_agent/integrations/skill_files.py +10 -0
  475. mobile_agent/integrations/textedit.py +309 -0
  476. mobile_agent/intents/__init__.py +17 -0
  477. mobile_agent/intents/atlas.py +216 -0
  478. mobile_agent/intents/callback.py +44 -0
  479. mobile_agent/intents/cli.py +55 -0
  480. mobile_agent/intents/harvest.py +91 -0
  481. mobile_agent/intents/plan.py +93 -0
  482. mobile_agent/ios/__init__.py +1 -0
  483. mobile_agent/ios/guide.py +16 -0
  484. mobile_agent/ios/kb.py +212 -0
  485. mobile_agent/ios/kb_data.py +1588 -0
  486. mobile_agent/ios/mcp.py +44 -0
  487. mobile_agent/ios/register.py +8 -0
  488. mobile_agent/ios/system.py +369 -0
  489. mobile_agent/journal.py +531 -0
  490. mobile_agent/keychain_acl.py +374 -0
  491. mobile_agent/keys.py +395 -0
  492. mobile_agent/latency_overlap.py +36 -0
  493. mobile_agent/latency_trace.py +170 -0
  494. mobile_agent/learned/__init__.py +15 -0
  495. mobile_agent/learned/app_path.py +160 -0
  496. mobile_agent/learned/code_read.py +38 -0
  497. mobile_agent/learned/parts.py +546 -0
  498. mobile_agent/learned/programs.py +691 -0
  499. mobile_agent/learned/register.py +171 -0
  500. mobile_agent/learned/routes.py +61 -0
  501. mobile_agent/learned/taught.py +329 -0
  502. mobile_agent/learned/teach.py +538 -0
  503. mobile_agent/learned/workflows.py +647 -0
  504. mobile_agent/learned/zero.py +41 -0
  505. mobile_agent/links.py +37 -0
  506. mobile_agent/listen.py +607 -0
  507. mobile_agent/lock_spike.py +278 -0
  508. mobile_agent/lockscreen.py +526 -0
  509. mobile_agent/login.py +354 -0
  510. mobile_agent/longform/__init__.py +15 -0
  511. mobile_agent/longform/ads.py +191 -0
  512. mobile_agent/longform/blocks.py +117 -0
  513. mobile_agent/longform/budget.py +265 -0
  514. mobile_agent/longform/context.py +285 -0
  515. mobile_agent/longform/costs.py +93 -0
  516. mobile_agent/longform/digest.py +145 -0
  517. mobile_agent/longform/extract.py +508 -0
  518. mobile_agent/longform/feed.py +505 -0
  519. mobile_agent/longform/followup.py +168 -0
  520. mobile_agent/longform/frames.py +82 -0
  521. mobile_agent/longform/identity.py +87 -0
  522. mobile_agent/longform/inbox.py +221 -0
  523. mobile_agent/longform/items.py +425 -0
  524. mobile_agent/longform/models.py +115 -0
  525. mobile_agent/longform/notebook.py +329 -0
  526. mobile_agent/longform/plan.py +614 -0
  527. mobile_agent/longform/recall.py +195 -0
  528. mobile_agent/longform/receipts.py +81 -0
  529. mobile_agent/longform/register.py +124 -0
  530. mobile_agent/longform/report.py +343 -0
  531. mobile_agent/longform/routes.py +102 -0
  532. mobile_agent/longform/session.py +644 -0
  533. mobile_agent/longform/store.py +146 -0
  534. mobile_agent/longform/synthesis.py +347 -0
  535. mobile_agent/mac_tools.py +223 -0
  536. mobile_agent/manual_control.py +143 -0
  537. mobile_agent/maps.py +149 -0
  538. mobile_agent/marks.py +92 -0
  539. mobile_agent/mcp_server/__init__.py +3 -0
  540. mobile_agent/mcp_server/__main__.py +29 -0
  541. mobile_agent/mcp_server/action_journal.py +24 -0
  542. mobile_agent/mcp_server/action_tools.py +638 -0
  543. mobile_agent/mcp_server/approvals.py +219 -0
  544. mobile_agent/mcp_server/catalog.py +92 -0
  545. mobile_agent/mcp_server/cli.py +194 -0
  546. mobile_agent/mcp_server/codes.py +147 -0
  547. mobile_agent/mcp_server/device_tools.py +306 -0
  548. mobile_agent/mcp_server/direct.py +272 -0
  549. mobile_agent/mcp_server/expo_tools.py +105 -0
  550. mobile_agent/mcp_server/phone_tools.py +510 -0
  551. mobile_agent/mcp_server/protocol.py +364 -0
  552. mobile_agent/mcp_server/registry.py +55 -0
  553. mobile_agent/mcp_server/run_tools.py +531 -0
  554. mobile_agent/mcp_server/schemas.py +425 -0
  555. mobile_agent/mcp_server/screen_tools.py +168 -0
  556. mobile_agent/mcp_server/session.py +469 -0
  557. mobile_agent/mcp_server/support.py +145 -0
  558. mobile_agent/mcp_server/tools.py +493 -0
  559. mobile_agent/memory/__init__.py +9 -0
  560. mobile_agent/memory/cli.py +207 -0
  561. mobile_agent/memory/extract.py +191 -0
  562. mobile_agent/memory/listener.py +100 -0
  563. mobile_agent/memory/model.py +144 -0
  564. mobile_agent/memory/project.py +96 -0
  565. mobile_agent/memory/provider.py +127 -0
  566. mobile_agent/memory/register.py +21 -0
  567. mobile_agent/memory/retrieval.py +172 -0
  568. mobile_agent/memory/routes.py +216 -0
  569. mobile_agent/memory/runs.py +124 -0
  570. mobile_agent/memory/store.py +806 -0
  571. mobile_agent/memory/writer.py +278 -0
  572. mobile_agent/mirroring.py +585 -0
  573. mobile_agent/mirroring_runtime.py +221 -0
  574. mobile_agent/model_clients.py +604 -0
  575. mobile_agent/narrate.py +813 -0
  576. mobile_agent/native_helpers.py +84 -0
  577. mobile_agent/netxray/__init__.py +1 -0
  578. mobile_agent/netxray/analyze.py +322 -0
  579. mobile_agent/netxray/attribution.py +62 -0
  580. mobile_agent/netxray/capture.py +88 -0
  581. mobile_agent/netxray/cli.py +142 -0
  582. mobile_agent/netxray/job.py +219 -0
  583. mobile_agent/netxray/mobster-pcap.c +560 -0
  584. mobile_agent/netxray/packets.py +410 -0
  585. mobile_agent/netxray/pcapng.py +223 -0
  586. mobile_agent/netxray/register.py +93 -0
  587. mobile_agent/netxray/report.py +214 -0
  588. mobile_agent/netxray/store.py +48 -0
  589. mobile_agent/netxray/summary.py +157 -0
  590. mobile_agent/netxray/target.py +67 -0
  591. mobile_agent/netxray/tool.py +66 -0
  592. mobile_agent/netxray/trackers.py +194 -0
  593. mobile_agent/notify_triggers/__init__.py +1 -0
  594. mobile_agent/notify_triggers/rules.py +39 -0
  595. mobile_agent/notify_triggers/sources.py +315 -0
  596. mobile_agent/notify_triggers/watch.py +135 -0
  597. mobile_agent/offers.py +86 -0
  598. mobile_agent/orchestrate.py +215 -0
  599. mobile_agent/outlets/__init__.py +1 -0
  600. mobile_agent/outlets/connect.py +249 -0
  601. mobile_agent/outlets/folder.py +180 -0
  602. mobile_agent/outlets/hooks.py +453 -0
  603. mobile_agent/outlets/inbound.py +472 -0
  604. mobile_agent/outlets/inbound_reply.py +57 -0
  605. mobile_agent/outlets/inbound_routes.py +81 -0
  606. mobile_agent/outlets/kinds.py +308 -0
  607. mobile_agent/outlets/mail.py +110 -0
  608. mobile_agent/outlets/net.py +86 -0
  609. mobile_agent/outlets/notion.py +244 -0
  610. mobile_agent/outlets/register.py +56 -0
  611. mobile_agent/outlets/routes.py +135 -0
  612. mobile_agent/outlets/service.py +580 -0
  613. mobile_agent/outlets/sheets.py +169 -0
  614. mobile_agent/outlets/slack.py +177 -0
  615. mobile_agent/outlets/store.py +194 -0
  616. mobile_agent/outlets/vault.py +80 -0
  617. mobile_agent/outlets/webhook.py +316 -0
  618. mobile_agent/output_contract.py +141 -0
  619. mobile_agent/owner_presence.py +211 -0
  620. mobile_agent/ownership.py +219 -0
  621. mobile_agent/packs/__init__.py +1 -0
  622. mobile_agent/packs/calendar.py +243 -0
  623. mobile_agent/packs/context.py +68 -0
  624. mobile_agent/packs/core.py +757 -0
  625. mobile_agent/packs/front_door.py +316 -0
  626. mobile_agent/packs/heal.py +214 -0
  627. mobile_agent/packs/health.py +122 -0
  628. mobile_agent/packs/insights.py +76 -0
  629. mobile_agent/packs/instagram.py +640 -0
  630. mobile_agent/packs/linkedin.py +106 -0
  631. mobile_agent/packs/maps.py +196 -0
  632. mobile_agent/packs/mcp.py +149 -0
  633. mobile_agent/packs/messages.py +270 -0
  634. mobile_agent/packs/notes.py +226 -0
  635. mobile_agent/packs/photos.py +166 -0
  636. mobile_agent/packs/pins.py +47 -0
  637. mobile_agent/packs/reddit.py +110 -0
  638. mobile_agent/packs/register.py +276 -0
  639. mobile_agent/packs/rows.py +108 -0
  640. mobile_agent/packs/safari.py +143 -0
  641. mobile_agent/packs/settings.py +253 -0
  642. mobile_agent/packs/social.py +300 -0
  643. mobile_agent/packs/threads.py +97 -0
  644. mobile_agent/packs/tiktok.py +455 -0
  645. mobile_agent/packs/tools.py +290 -0
  646. mobile_agent/packs/x.py +127 -0
  647. mobile_agent/packs/youtube.py +199 -0
  648. mobile_agent/parity.py +270 -0
  649. mobile_agent/paths.py +99 -0
  650. mobile_agent/perception.py +470 -0
  651. mobile_agent/permissions/__init__.py +32 -0
  652. mobile_agent/permissions/consequence.py +535 -0
  653. mobile_agent/permissions/exceptions.py +99 -0
  654. mobile_agent/permissions/gate.py +316 -0
  655. mobile_agent/permissions/outside.py +316 -0
  656. mobile_agent/permissions/policy.py +288 -0
  657. mobile_agent/permissions/project_policy.py +411 -0
  658. mobile_agent/permissions/register.py +148 -0
  659. mobile_agent/permissions/rules.py +291 -0
  660. mobile_agent/phone_io/__init__.py +146 -0
  661. mobile_agent/phone_io/cli.py +427 -0
  662. mobile_agent/phone_io/clipboard.py +78 -0
  663. mobile_agent/phone_io/controls.py +78 -0
  664. mobile_agent/phone_io/files.py +289 -0
  665. mobile_agent/phone_io/handoff.py +123 -0
  666. mobile_agent/phone_io/icloud.py +118 -0
  667. mobile_agent/phone_io/install.py +188 -0
  668. mobile_agent/phone_io/network_capture.py +188 -0
  669. mobile_agent/phone_io/network_cli.py +138 -0
  670. mobile_agent/phone_io/network_profiles.py +138 -0
  671. mobile_agent/phone_io/photos.py +512 -0
  672. mobile_agent/phone_io/recording.py +115 -0
  673. mobile_agent/phone_io/secrets.py +146 -0
  674. mobile_agent/phone_io/transfer.py +895 -0
  675. mobile_agent/phone_io/tunnel.py +129 -0
  676. mobile_agent/phone_ready.py +91 -0
  677. mobile_agent/pinning.py +158 -0
  678. mobile_agent/plan_gate.py +807 -0
  679. mobile_agent/plugins/__init__.py +3 -0
  680. mobile_agent/plugins/install.py +163 -0
  681. mobile_agent/plugins/manifest.py +242 -0
  682. mobile_agent/plugins/register.py +15 -0
  683. mobile_agent/plugins/routes.py +82 -0
  684. mobile_agent/plugins/service.py +322 -0
  685. mobile_agent/plugins/skills.py +76 -0
  686. mobile_agent/plugins/store.py +79 -0
  687. mobile_agent/pool.py +285 -0
  688. mobile_agent/power_watch.py +275 -0
  689. mobile_agent/premium_model.py +22 -0
  690. mobile_agent/project_env.py +291 -0
  691. mobile_agent/project_env_cli.py +74 -0
  692. mobile_agent/project_env_routes.py +55 -0
  693. mobile_agent/projects/__init__.py +27 -0
  694. mobile_agent/projects/ci.py +332 -0
  695. mobile_agent/projects/clone.py +339 -0
  696. mobile_agent/projects/drafts.py +158 -0
  697. mobile_agent/projects/folder.py +436 -0
  698. mobile_agent/projects/github.py +131 -0
  699. mobile_agent/projects/register.py +31 -0
  700. mobile_agent/projects/revisions.py +253 -0
  701. mobile_agent/projects/routes.py +234 -0
  702. mobile_agent/projects/runs.py +297 -0
  703. mobile_agent/projects/service.py +695 -0
  704. mobile_agent/projects/setup.py +84 -0
  705. mobile_agent/projects/store.py +379 -0
  706. mobile_agent/projects/sync.py +380 -0
  707. mobile_agent/projects/workflow_files.py +270 -0
  708. mobile_agent/replay_core/__init__.py +26 -0
  709. mobile_agent/replay_core/checks.py +594 -0
  710. mobile_agent/replay_core/engine.py +596 -0
  711. mobile_agent/replay_core/heal.py +364 -0
  712. mobile_agent/replay_core/path.py +240 -0
  713. mobile_agent/replay_core/recorder.py +206 -0
  714. mobile_agent/replay_core/store.py +171 -0
  715. mobile_agent/result_blocks.py +941 -0
  716. mobile_agent/run.py +642 -0
  717. mobile_agent/run_delivery.py +123 -0
  718. mobile_agent/run_executor.py +200 -0
  719. mobile_agent/run_export.py +284 -0
  720. mobile_agent/run_gif.py +891 -0
  721. mobile_agent/runner_hints.py +86 -0
  722. mobile_agent/runner_renewal.py +205 -0
  723. mobile_agent/runtime_create.py +549 -0
  724. mobile_agent/runtime_driver.py +458 -0
  725. mobile_agent/runtime_limits.py +319 -0
  726. mobile_agent/runtime_queue.py +394 -0
  727. mobile_agent/runtime_runs.py +383 -0
  728. mobile_agent/runtime_status.py +181 -0
  729. mobile_agent/runtime_teach.py +295 -0
  730. mobile_agent/runtime_work.py +599 -0
  731. mobile_agent/screen_text.py +101 -0
  732. mobile_agent/secret_filter.py +113 -0
  733. mobile_agent/serve.py +160 -0
  734. mobile_agent/server.py +363 -0
  735. mobile_agent/service_support.py +42 -0
  736. mobile_agent/settle/__init__.py +1 -0
  737. mobile_agent/settle/predict.py +102 -0
  738. mobile_agent/settle/replay.py +92 -0
  739. mobile_agent/settle/store.py +93 -0
  740. mobile_agent/settle/stream.py +43 -0
  741. mobile_agent/setup_cli.py +225 -0
  742. mobile_agent/setup_errors.py +87 -0
  743. mobile_agent/setup_service.py +527 -0
  744. mobile_agent/share.py +104 -0
  745. mobile_agent/signing.py +184 -0
  746. mobile_agent/sim/__init__.py +6 -0
  747. mobile_agent/sim/__main__.py +26 -0
  748. mobile_agent/sim/api.py +952 -0
  749. mobile_agent/sim/apps.py +172 -0
  750. mobile_agent/sim/biometrics.py +84 -0
  751. mobile_agent/sim/cli.py +271 -0
  752. mobile_agent/sim/doctor.py +293 -0
  753. mobile_agent/sim/governor.py +90 -0
  754. mobile_agent/sim/holders.py +88 -0
  755. mobile_agent/sim/models.py +62 -0
  756. mobile_agent/sim/registry.py +161 -0
  757. mobile_agent/sim/simctl.py +269 -0
  758. mobile_agent/sim/wda.py +618 -0
  759. mobile_agent/sim/xcode.py +74 -0
  760. mobile_agent/skills/__init__.py +12 -0
  761. mobile_agent/skills/codes.py +597 -0
  762. mobile_agent/skills/notifications.py +281 -0
  763. mobile_agent/skills/open_url.py +127 -0
  764. mobile_agent/smart_events.py +588 -0
  765. mobile_agent/smart_result.py +490 -0
  766. mobile_agent/smart_run.py +413 -0
  767. mobile_agent/state.py +662 -0
  768. mobile_agent/states.py +217 -0
  769. mobile_agent/steering.py +70 -0
  770. mobile_agent/style.py +178 -0
  771. mobile_agent/supervised.py +234 -0
  772. mobile_agent/swift/helper_bt_hid.swift +534 -0
  773. mobile_agent/swift/helper_doc_text.swift +184 -0
  774. mobile_agent/swift/helper_draft_reply.swift +170 -0
  775. mobile_agent/swift/helper_map.swift +110 -0
  776. mobile_agent/swift/helper_mirroring.swift +1099 -0
  777. mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
  778. mobile_agent/swift/helper_pdf.swift +100 -0
  779. mobile_agent/swift/helper_phone_audio.swift +993 -0
  780. mobile_agent/swift/helper_phone_screen.swift +416 -0
  781. mobile_agent/swift/helper_screen_text.swift +24 -0
  782. mobile_agent/swift/helper_transfer.swift +246 -0
  783. mobile_agent/swift/ocr.swift +22 -0
  784. mobile_agent/task_policy.py +337 -0
  785. mobile_agent/team_signing/__init__.py +34 -0
  786. mobile_agent/team_signing/apple_ca.py +37 -0
  787. mobile_agent/team_signing/bundle.py +136 -0
  788. mobile_agent/team_signing/cli.py +208 -0
  789. mobile_agent/team_signing/identity.py +248 -0
  790. mobile_agent/team_signing/ledger.py +105 -0
  791. mobile_agent/team_signing/prebuilt.py +61 -0
  792. mobile_agent/team_signing/resign.py +256 -0
  793. mobile_agent/team_signing/service.py +382 -0
  794. mobile_agent/team_signing/tooling.py +243 -0
  795. mobile_agent/team_signing/writes.py +268 -0
  796. mobile_agent/terminal_image.py +81 -0
  797. mobile_agent/testkit/__init__.py +1 -0
  798. mobile_agent/testkit/a11y_baseline.py +132 -0
  799. mobile_agent/testkit/api.py +403 -0
  800. mobile_agent/testkit/capsule.py +400 -0
  801. mobile_agent/testkit/capsule_cli.py +57 -0
  802. mobile_agent/testkit/cli.py +948 -0
  803. mobile_agent/testkit/cli_arguments.py +193 -0
  804. mobile_agent/testkit/comparison.py +288 -0
  805. mobile_agent/testkit/comparison_cli.py +41 -0
  806. mobile_agent/testkit/discover.py +257 -0
  807. mobile_agent/testkit/editor.py +243 -0
  808. mobile_agent/testkit/execute.py +515 -0
  809. mobile_agent/testkit/github_checks.py +248 -0
  810. mobile_agent/testkit/gitref.py +66 -0
  811. mobile_agent/testkit/heal_review.py +176 -0
  812. mobile_agent/testkit/health.py +480 -0
  813. mobile_agent/testkit/history.py +255 -0
  814. mobile_agent/testkit/hosted_results.py +321 -0
  815. mobile_agent/testkit/input_files.py +173 -0
  816. mobile_agent/testkit/jobs.py +285 -0
  817. mobile_agent/testkit/junit_verdict.py +33 -0
  818. mobile_agent/testkit/maestro.py +802 -0
  819. mobile_agent/testkit/mcp.py +562 -0
  820. mobile_agent/testkit/mcp_comparison.py +88 -0
  821. mobile_agent/testkit/minimize.py +471 -0
  822. mobile_agent/testkit/minimizer_cli.py +48 -0
  823. mobile_agent/testkit/notify.py +212 -0
  824. mobile_agent/testkit/phone_video.py +191 -0
  825. mobile_agent/testkit/pr.py +491 -0
  826. mobile_agent/testkit/pr_report.py +562 -0
  827. mobile_agent/testkit/prbuild.py +148 -0
  828. mobile_agent/testkit/proof_report.py +55 -0
  829. mobile_agent/testkit/provenance.py +119 -0
  830. mobile_agent/testkit/prrun.py +244 -0
  831. mobile_agent/testkit/recent.py +166 -0
  832. mobile_agent/testkit/recipes.py +146 -0
  833. mobile_agent/testkit/record.py +204 -0
  834. mobile_agent/testkit/register.py +288 -0
  835. mobile_agent/testkit/reports.py +998 -0
  836. mobile_agent/testkit/repro.py +432 -0
  837. mobile_agent/testkit/scenario.py +440 -0
  838. mobile_agent/testkit/scenario_cli.py +37 -0
  839. mobile_agent/testkit/snapshots_cli.py +60 -0
  840. mobile_agent/testkit/suite.py +670 -0
  841. mobile_agent/testkit/targets.py +497 -0
  842. mobile_agent/testkit/xctest.py +378 -0
  843. mobile_agent/threads/__init__.py +1 -0
  844. mobile_agent/threads/chat_once.py +264 -0
  845. mobile_agent/threads/cli.py +740 -0
  846. mobile_agent/threads/client.py +326 -0
  847. mobile_agent/threads/context.py +246 -0
  848. mobile_agent/threads/mcp.py +739 -0
  849. mobile_agent/threads/recent.py +34 -0
  850. mobile_agent/threads/register.py +54 -0
  851. mobile_agent/threads/routes.py +188 -0
  852. mobile_agent/threads/service.py +975 -0
  853. mobile_agent/threads/store.py +361 -0
  854. mobile_agent/threads/summary.py +117 -0
  855. mobile_agent/threads/term.py +101 -0
  856. mobile_agent/threads/workflow_cli.py +104 -0
  857. mobile_agent/tls.py +36 -0
  858. mobile_agent/traces.py +271 -0
  859. mobile_agent/tracks.py +235 -0
  860. mobile_agent/transport.py +271 -0
  861. mobile_agent/triggers/__init__.py +1 -0
  862. mobile_agent/triggers/actions.py +204 -0
  863. mobile_agent/triggers/model.py +146 -0
  864. mobile_agent/triggers/register.py +29 -0
  865. mobile_agent/triggers/routes.py +101 -0
  866. mobile_agent/triggers/service.py +387 -0
  867. mobile_agent/triggers/store.py +142 -0
  868. mobile_agent/tui/__init__.py +89 -0
  869. mobile_agent/tui/app.py +582 -0
  870. mobile_agent/tui/chrome.py +321 -0
  871. mobile_agent/tui/commands.py +375 -0
  872. mobile_agent/tui/demo_phone.py +369 -0
  873. mobile_agent/tui/images.py +29 -0
  874. mobile_agent/tui/keys.py +64 -0
  875. mobile_agent/tui/prompt.py +195 -0
  876. mobile_agent/tui/render.py +395 -0
  877. mobile_agent/tui/screens.py +180 -0
  878. mobile_agent/tui/session.py +308 -0
  879. mobile_agent/tui/tasks.py +287 -0
  880. mobile_agent/tui/theme.py +118 -0
  881. mobile_agent/tui/widgets.py +272 -0
  882. mobile_agent/update_check.py +213 -0
  883. mobile_agent/usage.py +184 -0
  884. mobile_agent/verify/__init__.py +0 -0
  885. mobile_agent/verify/__main__.py +41 -0
  886. mobile_agent/verify/a11y.py +189 -0
  887. mobile_agent/verify/actions.py +290 -0
  888. mobile_agent/verify/assertions.py +570 -0
  889. mobile_agent/verify/audit.py +312 -0
  890. mobile_agent/verify/business.py +221 -0
  891. mobile_agent/verify/business_run.py +131 -0
  892. mobile_agent/verify/business_state.py +168 -0
  893. mobile_agent/verify/check_schema.py +398 -0
  894. mobile_agent/verify/checks.py +614 -0
  895. mobile_agent/verify/cli.py +423 -0
  896. mobile_agent/verify/conditions.py +483 -0
  897. mobile_agent/verify/crashes.py +321 -0
  898. mobile_agent/verify/device_audit.py +246 -0
  899. mobile_agent/verify/dialogs.py +287 -0
  900. mobile_agent/verify/evidence.py +705 -0
  901. mobile_agent/verify/fixtures.py +305 -0
  902. mobile_agent/verify/flow.py +213 -0
  903. mobile_agent/verify/l10n.py +141 -0
  904. mobile_agent/verify/netcerts.py +67 -0
  905. mobile_agent/verify/netproxy.py +356 -0
  906. mobile_agent/verify/netrun.py +81 -0
  907. mobile_agent/verify/netshim.m +28 -0
  908. mobile_agent/verify/netshim.py +48 -0
  909. mobile_agent/verify/netstubs.py +132 -0
  910. mobile_agent/verify/overlay.py +174 -0
  911. mobile_agent/verify/perf.py +239 -0
  912. mobile_agent/verify/proof.py +192 -0
  913. mobile_agent/verify/replay_admission.py +48 -0
  914. mobile_agent/verify/report.py +952 -0
  915. mobile_agent/verify/run_cleanup.py +67 -0
  916. mobile_agent/verify/run_links.py +48 -0
  917. mobile_agent/verify/run_perf.py +44 -0
  918. mobile_agent/verify/run_secrets.py +39 -0
  919. mobile_agent/verify/runner.py +976 -0
  920. mobile_agent/verify/runs.py +57 -0
  921. mobile_agent/verify/screen_coverage.py +181 -0
  922. mobile_agent/verify/smart.py +106 -0
  923. mobile_agent/verify/snapshot_run.py +114 -0
  924. mobile_agent/verify/snapshots.py +306 -0
  925. mobile_agent/verify/sound.py +335 -0
  926. mobile_agent/verify/source_observation.py +130 -0
  927. mobile_agent/verify/system_surfaces/__init__.py +7 -0
  928. mobile_agent/verify/system_surfaces/play.py +106 -0
  929. mobile_agent/verify/system_surfaces/spec.py +139 -0
  930. mobile_agent/verify/system_surfaces/stage.py +138 -0
  931. mobile_agent/verify/system_surfaces/surfaces.py +121 -0
  932. mobile_agent/verify/tree.py +223 -0
  933. mobile_agent/verify/verdict.py +377 -0
  934. mobile_agent/verify/wire.py +392 -0
  935. mobile_agent/verify/wirestore.py +176 -0
  936. mobile_agent/waiting/__init__.py +1 -0
  937. mobile_agent/waiting/contacts.py +73 -0
  938. mobile_agent/waiting/drafts.py +136 -0
  939. mobile_agent/waiting/job.py +145 -0
  940. mobile_agent/waiting/messages.py +174 -0
  941. mobile_agent/waiting/rank.py +115 -0
  942. mobile_agent/waiting/register.py +26 -0
  943. mobile_agent/watch.py +502 -0
  944. mobile_agent/wda/__init__.py +2 -0
  945. mobile_agent/wda/frame_clock.py +646 -0
  946. mobile_agent/wda/frame_source.py +325 -0
  947. mobile_agent/wda/frame_store.py +115 -0
  948. mobile_agent/wda/gate.py +163 -0
  949. mobile_agent/wda/gestures.py +291 -0
  950. mobile_agent/wda/guards.py +94 -0
  951. mobile_agent/wda/identity.py +55 -0
  952. mobile_agent/wda/observe.py +884 -0
  953. mobile_agent/wda/patches.py +856 -0
  954. mobile_agent/wda/point_gestures.py +150 -0
  955. mobile_agent/wda/source.py +32 -0
  956. mobile_agent/wda/system_dialogs.py +571 -0
  957. mobile_agent/wda/text.py +694 -0
  958. mobile_agent/wda/timeouts.py +77 -0
  959. mobile_agent/wda/video.py +505 -0
  960. mobile_agent/wda/watchdog.py +243 -0
  961. mobile_agent/wireless/__init__.py +15 -0
  962. mobile_agent/wireless/api.py +354 -0
  963. mobile_agent/wireless/cli.py +287 -0
  964. mobile_agent/wireless/devicectl.py +229 -0
  965. mobile_agent/wireless/exposure.py +33 -0
  966. mobile_agent/wireless/mobster-wifi-pair.c +122 -0
  967. mobile_agent/wireless/monitor.py +332 -0
  968. mobile_agent/wireless/network.py +334 -0
  969. mobile_agent/wireless/onboarding.py +293 -0
  970. mobile_agent/wireless/pair.py +124 -0
  971. mobile_agent/wireless/probe.py +322 -0
  972. mobile_agent/wireless/register.py +8 -0
  973. mobile_agent/wireless/relay.py +179 -0
  974. mobile_agent/wireless/result.py +87 -0
  975. mobile_agent/wireless/store.py +85 -0
  976. mobile_agent/wireless/switch.py +77 -0
  977. mobile_agent/wireless/transport.py +118 -0
  978. mobile_agent/wireless/words.py +81 -0
  979. mobile_agent/wireless/xctestrun.py +83 -0
  980. mobile_agent/workflow_tools.py +59 -0
  981. mobile_agent/workflows/__init__.py +2 -0
  982. mobile_agent/workflows/brief.py +201 -0
  983. mobile_agent/workflows/cron.py +110 -0
  984. mobile_agent/workflows/health.py +257 -0
  985. mobile_agent/workflows/history.py +126 -0
  986. mobile_agent/workflows/records.py +442 -0
  987. mobile_agent/workflows/runs.py +1000 -0
  988. mobile_agent/workflows/scheduler.py +134 -0
  989. mobile_agent/workflows/service.py +973 -0
  990. mobile_agent/workflows/starters.py +285 -0
  991. mobile_agent/workflows/triggers.py +248 -0
  992. mobile_agent/xray/__init__.py +1 -0
  993. mobile_agent/xray/app_cli.py +116 -0
  994. mobile_agent/xray/cli.py +21 -0
  995. mobile_agent/xray/crawl.py +754 -0
  996. mobile_agent/xray/diff.py +171 -0
  997. mobile_agent/xray/diff_cli.py +43 -0
  998. mobile_agent/xray/job.py +184 -0
  999. mobile_agent/xray/paywall.py +188 -0
  1000. mobile_agent/xray/policy.py +305 -0
  1001. mobile_agent/xray/register.py +43 -0
  1002. mobile_agent/xray/report.py +283 -0
  1003. mobile_agent/xray/screens.py +117 -0
  1004. mobster_cli-0.4.3.dist-info/METADATA +93 -0
  1005. mobster_cli-0.4.3.dist-info/RECORD +1009 -0
  1006. mobster_cli-0.4.3.dist-info/WHEEL +5 -0
  1007. mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
  1008. mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
  1009. mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,739 @@
1
+ """MCP tools that hand a task to Mobster's agent in a conversation (provider ``threads``, seam S10)."""
2
+
3
+ import json
4
+ import logging
5
+ import re
6
+ import time
7
+ import traceback
8
+ import uuid
9
+
10
+ from .. import engine_health
11
+ from .client import Client, NoService, ServiceError
12
+
13
+ log = logging.getLogger("mobster.threads")
14
+
15
+ NO_SERVICE = "Open Mobster or run `mobster serve` so your agent can hand tasks to Mobster's agent."
16
+ WAIT_MAX = 45
17
+ HEADROOM = 7.0 # of the 44 s call budget: the HTTP round trips and the answer
18
+ ID = r"^[a-f0-9]{12}$"
19
+ DONE = frozenset({"completed"})
20
+ BLOCKS_INLINE = 12000 # characters of result blocks a task's answer carries as they are
21
+
22
+
23
+ def accepted_unavailable(ToolResult, *, text, **handles):
24
+ """A POST succeeded; losing its subsequent status read must not invite another POST."""
25
+ return ToolResult(text, structured={**handles, "status": "status_unavailable"})
26
+
27
+
28
+ def known_refusal(error):
29
+ """Whether a failed start POST certainly started nothing: Mobster refused it with a 4xx other than a timeout (408)
30
+ or its own "outcome unknown", or said so with ``"started": false`` (api_errors.nothing_started), as it does for a
31
+ 503 such as the iPhone helper restarting. No reply, any other server fault (5xx) or a malformed answer leaves the
32
+ outcome unknown."""
33
+ if not isinstance(error, ServiceError) or error.code == "request_outcome_unknown":
34
+ return False
35
+ if error.data.get("started") is False: # only the literal false: a missing or odd value stays unknown
36
+ return True
37
+ # A workflow's 409 "queued" (workflows/runs.py: the run starts when the one going ends) would count as a refusal
38
+ # here, but only a scheduled key gets it: run_workflow and workflow_* send a fresh key on every call.
39
+ return 400 <= error.status < 500 and error.status != 408
40
+
41
+
42
+ def well_formed(answer):
43
+ """Whether a start's 2xx answer has the shapes the tools read: an object whose run, item, group and batch are absent
44
+ or objects, with string ids where a tool reads one. Anything else names no task the tool could follow."""
45
+ if not isinstance(answer, dict):
46
+ return False
47
+ run, item, group, batch = (answer.get(key) for key in ("run", "item", "group", "batch"))
48
+ if any(part and not isinstance(part, dict) for part in (run, item, group, batch)):
49
+ return False
50
+ needed = [part.get("id") for part in (group, batch if batch and batch.get("graph") == 2 else None) if part]
51
+ optional = [(run or {}).get("id"), (item or {}).get("runId")]
52
+ return (all(isinstance(value, str) and value for value in needed)
53
+ and all(value is None or isinstance(value, str) for value in optional))
54
+
55
+
56
+ def log_fault(what, error):
57
+ """Log a fault a tool answered past: its type and stack, never its message, which can carry the phone's text."""
58
+ stack = "".join(traceback.format_tb(error.__traceback__, limit=8))
59
+ log.warning("%s (%s)\n%s", what, type(error).__name__, stack.rstrip())
60
+
61
+
62
+ def mask_handles(mask, name, arguments, result):
63
+ """Keep only canonical continuation handles usable when a remembered code overlaps their bytes."""
64
+ state = result.structured if isinstance(result.structured, dict) and not result.is_error else {}
65
+ keys = {"thread_id", "run_id", "group_id"} if name in ("phone_task", "phone_task_status") else (
66
+ {"workflow_id", "thread_id", "run_id", "batch_id"} if name == "run_workflow" or name == "graph_status"
67
+ or name.startswith(TOOL_PREFIX) else set())
68
+ selectors = ({"thread_id"} if name == "phone_task" else
69
+ {"thread_id", "run_id", "group_id"} if name == "phone_task_status" else
70
+ {"thread_id"} if name == "run_workflow" else
71
+ {"workflow_id", "batch_id"} if name == "graph_status" else set())
72
+ handles = {key: value for key in keys if isinstance(value := state.get(key), str)
73
+ and re.fullmatch(ID, value) and (key not in selectors or arguments.get(key) is None
74
+ or arguments[key] == value)}
75
+ if name == "run_workflow" and "workflow_id" in handles and re.fullmatch(ID, str(arguments.get("workflow") or "")):
76
+ if handles["workflow_id"] != arguments["workflow"]:
77
+ handles.pop("workflow_id")
78
+ uri = state.get("status_uri")
79
+ expected = ({f"/api/groups/{handles['group_id']}"} if "group_id" in handles else set()) | (
80
+ {f"/api/runs/{handles['run_id']}"} if "run_id" in handles else set()) | (
81
+ {f"/api/workflows/{handles['workflow_id']}/runs"} if "workflow_id" in handles
82
+ and "batch_id" in handles else set())
83
+ masked = mask(result)
84
+ if isinstance(masked.structured, dict):
85
+ masked.structured.update(handles)
86
+ if isinstance(uri, str) and uri in expected:
87
+ masked.structured["status_uri"] = uri
88
+ return masked
89
+
90
+
91
+ def schema_task():
92
+ return {"type": "object", "properties": {
93
+ "message": {"type": "string", "minLength": 1, "maxLength": 4000,
94
+ "description": "what Mobster's agent should do on the iPhone, in plain words; in a "
95
+ "conversation, a follow-up such as \"now reply to her\" (it sees what the earlier "
96
+ "tasks found)"},
97
+ "thread_id": {"type": "string", "pattern": ID,
98
+ "description": "continue this conversation (from an earlier phone_task); leave it out to "
99
+ "start a new one. While its task runs, the message steers it, or answers "
100
+ "Mobster's question"},
101
+ "device": {"type": "string", "maxLength": 128,
102
+ "description": "the iPhone or simulator by id, UDID or name (default: the conversation's, else "
103
+ "the user's default device)"},
104
+ "profile": {"type": "string", "minLength": 1, "maxLength": 160,
105
+ "description": "act as this profile (from list_profiles, such as "
106
+ "\"@getmobster on Instagram\"): the "
107
+ "task runs on its iPhone, and Mobster confirms the profile on screen before it "
108
+ "posts or sends"},
109
+ "account": {"type": "string", "minLength": 1, "maxLength": 160,
110
+ "description": "deprecated: use profile, which takes the same value"},
111
+ "phones": {"type": "array", "minItems": 2, "maxItems": 100,
112
+ "items": {"type": "string", "minLength": 1, "maxLength": 128},
113
+ "description": "run the same task on each of these devices at once (ids, UDIDs or names from "
114
+ "list_devices), as one task: each phone's answer, then one summary that merges "
115
+ "them. Leave device out"},
116
+ "profiles": {"description": "run the same task as each of these profiles at once, each on its own iPhone, as "
117
+ "one task: \"all\", or a list of profiles (from list_profiles). Mobster "
118
+ "confirms each profile on screen before it posts or sends; a profile that "
119
+ "fails doesn't stop the others. Leave device, phones and profile out",
120
+ "oneOf": [{"const": "all"},
121
+ {"type": "array", "minItems": 1, "maxItems": 100,
122
+ "items": {"type": "string", "minLength": 1, "maxLength": 160}}]},
123
+ "queue": {"type": "boolean",
124
+ "description": "true (the default): when the phone is in use, wait in line "
125
+ "and start when it is free; false: fail at once instead. "
126
+ "The status says who holds the phone and your number"},
127
+ "wait_seconds": {"type": "integer", "minimum": 0, "maximum": WAIT_MAX,
128
+ "description": "how long to wait for the task before returning (default 30); call "
129
+ "phone_task_status for the rest"}},
130
+ "required": ["message"], "additionalProperties": False}
131
+
132
+
133
+ def schema_status():
134
+ return {"type": "object", "properties": {
135
+ "thread_id": {"type": "string", "pattern": ID, "description": "the conversation's latest task"},
136
+ "run_id": {"type": "string", "pattern": ID, "description": "one task"},
137
+ "group_id": {"type": "string", "pattern": ID, "description": "a task run on several phones (from phone_task "
138
+ "with phones)"},
139
+ "wait_seconds": {"type": "integer", "minimum": 0, "maximum": WAIT_MAX,
140
+ "description": "wait up to this long for it to finish or need the user (default 0)"}},
141
+ "required": [], "additionalProperties": False}
142
+
143
+
144
+ def describe(run):
145
+ """{status, answer?, waiting?} for a run: running, waiting_for_approval, waiting_for_answer, waiting_for_phone, or
146
+ how it ended."""
147
+ approval = run.get("approval")
148
+ if run.get("finishedAt") is None:
149
+ line = run.get("queue")
150
+ if isinstance(line, dict) and not approval:
151
+ return {"status": "queued", "position": line.get("position"), "behind": line.get("behind"),
152
+ "queue_message": line.get("message"), "expires_at": line.get("expiresAt")}
153
+ if approval and approval.get("kind") == "clarify":
154
+ return {"status": "waiting_for_answer", "question": approval.get("question") or approval.get("label"),
155
+ "choices": [c.get("label") for c in approval.get("choices") or () if isinstance(c, dict)]}
156
+ if approval:
157
+ return {"status": "waiting_for_approval", "approval": approval.get("title") or approval.get("label"),
158
+ "reason": approval.get("reason"), "explain": approval.get("explain"),
159
+ "expires_at": approval.get("expiresAt")}
160
+ wait = run.get("phoneWait")
161
+ # What the task waits on, in the words every surface uses (states.ROWS).
162
+ found = engine_health.task_wait(wait.get("kind"), run.get("deviceName")) if isinstance(wait, dict) else None
163
+ if found:
164
+ need, line = found
165
+ since, seconds = wait.get("since"), wait.get("seconds")
166
+ return {"status": "waiting_for_phone", "need": need, "message": line,
167
+ "expires_at": since + seconds * 1000 if isinstance(since, (int, float)) and isinstance(seconds, int)
168
+ else None}
169
+ return {"status": "running"}
170
+ summary = run.get("summary") or {}
171
+ out = {"status": run.get("status") or summary.get("status") or "error"}
172
+ data = summary.get("data")
173
+ answer = data if isinstance(data, str) and data.strip() else summary.get("answer")
174
+ if answer:
175
+ out["answer"] = str(answer)[:3000]
176
+ elif summary.get("reason") and out["status"] not in DONE:
177
+ out["reason"] = str(summary["reason"])[:500]
178
+ if isinstance(data, (dict, list)):
179
+ out["data"] = data
180
+ blocks = summary.get("blocks")
181
+ if isinstance(blocks, dict) and blocks.get("blocks"):
182
+ # The answer's result blocks (docs/results.mdx): as they are when small, else as Markdown, so a client's
183
+ # context isn't flooded by a 200-row table.
184
+ if len(json.dumps(blocks, ensure_ascii=False)) <= BLOCKS_INLINE:
185
+ out["blocks"] = blocks
186
+ else:
187
+ from ..result_blocks import to_markdown
188
+ out["blocks_markdown"] = to_markdown(blocks)[:BLOCKS_INLINE]
189
+ return out
190
+
191
+
192
+ def sentence(state, thread_id, answerable=True, origin="mcp"):
193
+ """The tool's text."""
194
+ status = state["status"]
195
+ remote = origin == "remote"
196
+ where = "in Mobster on their Mac" if remote else "in the Mobster app"
197
+ if status == "queued":
198
+ return (f"{state.get('queue_message') or 'Waiting for the phone.'} Nothing has been done on the phone yet. It "
199
+ f"starts when the phone is free; call phone_task_status with thread_id {thread_id} to see when.")
200
+ if status == "running":
201
+ return ("Mobster's agent is still working on it. Call phone_task_status with thread_id "
202
+ f"{thread_id} to see how it ends.")
203
+ if status == "waiting_for_approval":
204
+ if remote:
205
+ label = str(state.get("approval") or "an action").strip()
206
+ end = "" if label.endswith((".", "?", "!")) else "."
207
+ return (f"It's waiting for the user to approve this in Mobster on their Mac: {label}{end} You can't "
208
+ "approve it. Tell the user, then call phone_task_status.")
209
+ left = ""
210
+ if isinstance(state.get("expires_at"), (int, float)):
211
+ minutes = max(0, int((state["expires_at"] / 1000 - time.time()) // 60))
212
+ left = f" It waits about {minutes} more minutes, then ends without doing it."
213
+ why = f" ({state['reason']})" if state.get("reason") else ""
214
+ said = f" {state['explain']}" if state.get("explain") else ""
215
+ return (f"It's waiting for the user's OK in the Mobster app, which notified them: "
216
+ f"{state.get('approval') or 'an action'}{why}.{said} Nothing happens until they answer.{left} "
217
+ "Tell the user; then call phone_task_status with wait_seconds to keep waiting.")
218
+ if status == "waiting_for_phone":
219
+ left = ""
220
+ if isinstance(state.get("expires_at"), (int, float)):
221
+ seconds = max(0, int(state["expires_at"] / 1000 - time.time()))
222
+ left = f" It waits about {seconds} more seconds, then stops."
223
+ return (f"It's waiting for the user on the iPhone itself. {state.get('message') or ''} Nothing is done on the "
224
+ f"phone meanwhile.{left} Tell the user now; then call phone_task_status with thread_id {thread_id} and "
225
+ "wait_seconds to keep waiting.")
226
+ if status == "approval_timeout":
227
+ return "Nobody answered the approval in the Mobster app in time, so Mobster didn't do it."
228
+ if status == "waiting_for_answer":
229
+ choices = state.get("choices") or []
230
+ asks = f"Mobster's agent asks: {state.get('question')}" + (f" (choices: {', '.join(choices)})" if choices
231
+ else "")
232
+ if not answerable:
233
+ return asks + f". The user answers this {where}; then call phone_task_status."
234
+ return asks + f". Answer with phone_task, thread_id {thread_id}, or let the user answer {where}."
235
+ if status in DONE:
236
+ return "Done." + (f" {state['answer']}" if state.get("answer") else "")
237
+ return f"It ended {status.replace('_', ' ')}." + (f" {state.get('answer') or state.get('reason') or ''}".rstrip())
238
+
239
+
240
+ class ThreadsProvider:
241
+ """mcp_server.registry.ToolProvider for conversations."""
242
+
243
+ name = "threads"
244
+
245
+ def __init__(self, connect=None, *, origin="mcp", wait_max=None, extra_headers=None, default_wait=30):
246
+ """``origin``: "mcp" (`mobster mcp`), or "remote" for Mobster Remote's tools, which word approvals for an
247
+ assistant away from the Mac."""
248
+ self.origin = origin
249
+ self.wait_cap = WAIT_MAX - HEADROOM if wait_max is None else float(wait_max)
250
+ self.default_wait = default_wait
251
+ headers = dict(extra_headers or {})
252
+ self._connect = connect or (lambda: self._connect_default(headers))
253
+
254
+ def _connect_default(self, headers):
255
+ client = Client.connect(origin=self.origin)
256
+ client.extra_headers.update(headers)
257
+ return client
258
+
259
+ def definitions(self, toolset):
260
+ return [
261
+ {"name": "phone_task",
262
+ "description": "Hand a task to Mobster's agent on the user's iPhone. It asks the user in the Mobster app "
263
+ "before it sends, buys, posts or deletes. Pass thread_id for a follow-up that sees what "
264
+ "earlier tasks found.",
265
+ "inputSchema": schema_task()},
266
+ {"name": "phone_task_status",
267
+ "description": "Where a phone_task task is: running, waiting for the user's approval or answer, or "
268
+ "finished with its answer.",
269
+ "inputSchema": schema_status()}]
270
+
271
+ def annotations(self):
272
+ from ..mcp_server.tools import ACTS, READ_ONLY
273
+ return {"phone_task": ACTS, "phone_task_status": READ_ONLY}
274
+
275
+ def instructions(self):
276
+ return ("phone_task hands a whole task to Mobster's agent in the user's Mobster app, in a conversation that "
277
+ "remembers; the user approves sends, buys, posts and deletes there.")
278
+
279
+ def client(self):
280
+ from ..mcp_server.tools import ToolError
281
+ try:
282
+ return self._connect()
283
+ except (NoService, ServiceError):
284
+ raise ToolError(NO_SERVICE) from None
285
+
286
+ def call(self, name, arguments, call, toolset):
287
+ from ..mcp_server.protocol import ToolResult
288
+ from ..mcp_server.tools import ToolError
289
+ client = self.client()
290
+ if not client.has_threads:
291
+ raise ToolError("This Mobster doesn't have conversations yet. Ask the user to update Mobster.")
292
+ if self.origin == "mcp":
293
+ # The holder label on the phone ("Claude Code is using this phone"): the client's own name for itself.
294
+ from ..ownership import clean_client
295
+ label = clean_client(toolset.client_name()) if callable(getattr(toolset, "client_name", None)) else None
296
+ if label:
297
+ client.extra_headers["X-Mobster-Client"] = label
298
+ try:
299
+ if name == "phone_task":
300
+ return self.task(client, arguments, toolset, ToolResult, call=call)
301
+ return self.status(client, arguments, ToolResult)
302
+ except ServiceError as error:
303
+ if error.code == "approval_pending":
304
+ raise ToolError(self.approval_pending()) from None
305
+ if error.code == "steer_not_allowed":
306
+ raise ToolError("That task was started somewhere else, so it takes messages only there. Start a "
307
+ "new conversation without thread_id.") from None
308
+ raise ToolError(str(error)) from None
309
+ except NoService:
310
+ raise ToolError(NO_SERVICE) from None
311
+
312
+ def approval_pending(self):
313
+ if self.origin == "remote":
314
+ return ("That conversation's task is waiting for the user's approval in Mobster on their Mac. You can't "
315
+ "approve it. Tell the user, then call phone_task_status.")
316
+ return ("That conversation's task is waiting for the user's approval in the Mobster app. Tell the user, then "
317
+ "call phone_task_status.")
318
+
319
+ @staticmethod
320
+ def allowed(toolset, *names):
321
+ allow = getattr(toolset, "allow_devices", None)
322
+ if allow is None:
323
+ return True
324
+ return any(isinstance(n, str) and n.strip().casefold() in allow for n in names)
325
+
326
+ @staticmethod
327
+ def profile(arguments):
328
+ """The profile a task acts as: ``profile``, or ``account``, its deprecated name. ToolError when they differ."""
329
+ from ..mcp_server.tools import ToolError
330
+ profile, account = arguments.get("profile"), arguments.get("account")
331
+ if profile and account and profile.strip().casefold() != account.strip().casefold():
332
+ raise ToolError("Name the profile once, with profile. account is its old name.")
333
+ return profile or account
334
+
335
+ def poll_seconds(self, arguments, call, toolset, client):
336
+ """Leave one HTTP timeout for the last status read before the MCP call budget ends."""
337
+ wait = min(float(arguments.get("wait_seconds", self.default_wait)), self.wait_cap)
338
+ started = getattr(call, "started", None)
339
+ budget = getattr(toolset, "call_budget", None)
340
+ if isinstance(started, (int, float)) and isinstance(budget, (int, float)):
341
+ reserve = max(0.0, float(getattr(client, "timeout", 10.0))) + 2.0
342
+ remaining = budget - (time.monotonic() - started) - reserve
343
+ if remaining <= 0:
344
+ return None
345
+ wait = min(wait, remaining)
346
+ return wait
347
+
348
+ def in_time(self, tool, kind, arguments, call, toolset, client):
349
+ """Refuse before the start POST when the call's budget can't fit it and one status read: nothing is sent."""
350
+ from ..mcp_server.tools import ToolError
351
+ if self.poll_seconds(arguments, call, toolset, client) is None:
352
+ raise ToolError(f"{tool} ran out of time before it sent the {kind}, so nothing started. Call {tool} again.")
353
+
354
+ def submit(self, post, kind):
355
+ """(thread id, answer) of ``post()``, the one POST that starts a ``kind``. Any failure but Mobster's own refusal
356
+ leaves the outcome unknown, and the error says so instead of inviting another POST."""
357
+ from ..mcp_server.tools import ToolError
358
+ try:
359
+ thread_id, answer = post()
360
+ if not well_formed(answer):
361
+ raise ValueError("the answer doesn't have the shape a start answers with")
362
+ return thread_id, answer
363
+ except Exception as error: # noqa: BLE001 -- no reply, a 5xx or a malformed answer: the outcome is unknown
364
+ if known_refusal(error):
365
+ raise
366
+ if not isinstance(error, (NoService, ServiceError)):
367
+ log_fault(f"a {kind}'s start request failed, so its outcome is unknown", error)
368
+ check = "Ask the user to check Mobster on their Mac" if self.origin == "remote" else "Check the Mobster app"
369
+ raise ToolError(f"Mobster didn't confirm this {kind}, so whether it started is unknown. {check} before you "
370
+ "start it again; don't start another to find out.") from None
371
+
372
+ @staticmethod
373
+ def follow(wait, read, unavailable):
374
+ """``read(wait)``, the accepted task's answer; ``unavailable``, its handles with status_unavailable, when the
375
+ call has no time left or the read fails in any way. A lost status read never invites another POST."""
376
+ if wait is None:
377
+ return unavailable
378
+ try:
379
+ return read(wait)
380
+ except Exception as error: # noqa: BLE001 -- the task was accepted: any failed read keeps its handle
381
+ if not isinstance(error, (NoService, ServiceError)):
382
+ log_fault("a status read after an accepted task failed", error)
383
+ return unavailable
384
+
385
+ def task(self, client, arguments, toolset, ToolResult, *, call=None):
386
+ from ..mcp_server.tools import ToolError
387
+ thread_id, device, phones = arguments.get("thread_id"), arguments.get("device"), arguments.get("phones")
388
+ if device and phones:
389
+ raise ToolError("Name one phone with device, or several with phones, not both.")
390
+ profile = self.profile(arguments)
391
+ profiles = arguments.get("profiles")
392
+ if profile and phones:
393
+ raise ToolError("A profile runs on its own iPhone: leave phones out.")
394
+ if profiles is not None and (profile or phones or device):
395
+ raise ToolError("Run on several profiles with profiles alone: each runs on its own iPhone.")
396
+ thread = client.thread(thread_id)["thread"] if thread_id else None
397
+ if getattr(toolset, "allow_devices", None) is not None:
398
+ if profiles is not None:
399
+ raise ToolError("This server may use only the devices given with --allow-device: run profiles one "
400
+ "at a time, naming each profile's phone with device.")
401
+ if profile and not device:
402
+ # The profile's phone isn't known here: naming it lets the allowlist check it, and the task is
403
+ # refused if it isn't the profile's phone (accounts/routing.py).
404
+ raise ToolError("This server may use only the devices given with --allow-device: name the "
405
+ "profile's phone with device too.")
406
+ if phones:
407
+ if not all(self.allowed(toolset, name) for name in phones):
408
+ raise ToolError("This server may use only the devices given with --allow-device: name only "
409
+ "those in phones.")
410
+ else:
411
+ names = (device,) if device else ((thread or {}).get("device"), (thread or {}).get("deviceName"))
412
+ if not self.allowed(toolset, *names):
413
+ raise ToolError("This server may use only the devices given with --allow-device: name one of "
414
+ "them with device.")
415
+ body = {"text": arguments["message"]}
416
+ if device:
417
+ body["device"] = device
418
+ if phones:
419
+ body["phones"] = list(phones)
420
+ if profile:
421
+ body["account"] = profile
422
+ if profiles is not None:
423
+ body["profiles"] = profiles if profiles == "all" else list(profiles)
424
+ if arguments.get("queue") is False:
425
+ body["queue"] = False
426
+ self.in_time("phone_task", "task", arguments, call, toolset, client)
427
+ key = uuid.uuid4().hex # with a key, Mobster answers "outcome unknown" for a start it can't confirm
428
+ thread_id, answer = self.submit(lambda: client.send(thread_id, body, key=key), "task")
429
+ wait = self.poll_seconds(arguments, call, toolset, client)
430
+ if answer.get("group"):
431
+ group_id = answer["group"]["id"]
432
+ unavailable = accepted_unavailable(
433
+ ToolResult, text=f"Mobster accepted the task, but its status is unavailable. Call "
434
+ f"phone_task_status with group_id {group_id}; do not start it again.",
435
+ thread_id=thread_id, group_id=group_id, status_uri=f"/api/groups/{group_id}")
436
+ return self.follow(wait, lambda seconds: self.group_result(client.wait_group(group_id, seconds),
437
+ thread_id, ToolResult), unavailable)
438
+ run = answer.get("run") or {}
439
+ routed = answer.get("routed") or ("new_run" if run else None)
440
+ run_id = run.get("id") or (answer.get("item") or {}).get("runId")
441
+ if routed in ("steer", "answer"):
442
+ text = ("Mobster's agent got your message while it works on the task." if routed == "steer"
443
+ else "Mobster's agent has your answer and goes on.")
444
+ return ToolResult(text, structured={"thread_id": thread_id, "run_id": run_id, "status": "running",
445
+ "routed": routed})
446
+ if not run_id:
447
+ state = {"status": "running"}
448
+ return ToolResult(sentence(state, thread_id, origin=self.origin),
449
+ structured={"thread_id": thread_id, "run_id": None, **state})
450
+
451
+ def read(seconds):
452
+ state = describe(client.wait_run(run_id, seconds, through_approval=True))
453
+ return ToolResult(sentence(state, thread_id, origin=self.origin),
454
+ structured={"thread_id": thread_id, "run_id": run_id, **state})
455
+ return self.follow(wait, read, accepted_unavailable(
456
+ ToolResult, text=f"Mobster accepted the task, but its status is unavailable. Call "
457
+ f"phone_task_status with run_id {run_id}; do not start it again.",
458
+ thread_id=thread_id, run_id=run_id, status_uri=f"/api/runs/{run_id}"))
459
+
460
+ def group_result(self, group, thread_id, ToolResult):
461
+ """phone_task's answer for a task on several phones: each phone's state and answer, and when every phone is
462
+ done, the merged summary (the lines several phones found, once)."""
463
+ phones = []
464
+ summary = group.get("summary") or {}
465
+ answers = {item.get("profile") or item.get("device"): item for item in summary.get("answers") or ()}
466
+ for member in group.get("members") or ():
467
+ approval = member.get("approval") or {}
468
+ if member.get("state") == "refused":
469
+ state = {"status": "not_started", "reason": member.get("error")}
470
+ elif member.get("state") == "held":
471
+ state = {"status": "waiting_for_mac"}
472
+ elif approval:
473
+ state = {"status": "waiting_for_answer" if approval.get("kind") == "clarify" else
474
+ "waiting_for_approval", "approval": approval.get("title") or approval.get("label")}
475
+ elif member.get("finishedAt") is None:
476
+ state = {"status": "running"}
477
+ else:
478
+ found = answers.get(member.get("profile") or member.get("device")) or {}
479
+ state = {"status": member.get("status") or found.get("status") or "error",
480
+ **({"answer": found["answer"]} if found.get("answer") else {})}
481
+ phones.append({"device": member.get("device"), "name": member.get("deviceName"),
482
+ **({"profile": member.get("profileName") or member["profile"]}
483
+ if member.get("profile") else {}),
484
+ "run_id": member.get("runId"), **state})
485
+ counts = group.get("counts") or {}
486
+ total = counts.get("phones") or len(phones)
487
+ if group.get("state") == "finished":
488
+ lines = [line["text"] + (f" ({len(line['phones'])} phones)" if len(line.get("phones") or ()) > 1 else "")
489
+ for line in summary.get("lines") or ()][:40]
490
+ text = (f"Done on {summary.get('completed', 0)} of {total} phones." +
491
+ (" " + " ".join(lines) if lines else ""))
492
+ elif group.get("state") == "waiting":
493
+ text = (f"{counts.get('waiting', 0)} of {total} phones wait for the user in the Mobster app. Tell the "
494
+ "user; "
495
+ f"then call phone_task_status with group_id {group.get('id')}.")
496
+ else:
497
+ text = (f"Mobster's agent is working on {total} phones. Call phone_task_status with group_id "
498
+ f"{group.get('id')} to see how it ends.")
499
+ structured = {"thread_id": thread_id, "group_id": group.get("id"), "status": group.get("state"),
500
+ "phones": phones}
501
+ if group.get("state") == "finished":
502
+ structured["summary"] = {"lines": summary.get("lines") or [], "rows": summary.get("rows"),
503
+ "completed": summary.get("completed"), "costUsd": summary.get("costUsd")}
504
+ return ToolResult(text, structured=structured)
505
+
506
+ def status(self, client, arguments, ToolResult):
507
+ from ..mcp_server.tools import ToolError
508
+ run_id, thread_id, group_id = arguments.get("run_id"), arguments.get("thread_id"), arguments.get("group_id")
509
+ if not run_id and not thread_id and not group_id:
510
+ raise ToolError("Give the thread_id, run_id or group_id that phone_task returned.")
511
+ if group_id:
512
+ group = client.wait_group(group_id, min(float(arguments.get("wait_seconds", 0)), self.wait_cap))
513
+ return self.group_result(group, thread_id or group.get("threadId"), ToolResult)
514
+ if not run_id:
515
+ thread = client.thread(thread_id)["thread"]
516
+ if thread.get("lastGroupId") and thread.get("group"):
517
+ group = client.wait_group(thread["lastGroupId"],
518
+ min(float(arguments.get("wait_seconds", 0)), self.wait_cap))
519
+ return self.group_result(group, thread_id, ToolResult)
520
+ run_id = thread.get("lastRunId")
521
+ if not run_id:
522
+ return ToolResult("No task has started in this conversation yet.",
523
+ structured={"thread_id": thread_id, "run_id": None, "status": "idle"})
524
+ run = client.wait_run(run_id, min(float(arguments.get("wait_seconds", 0)), self.wait_cap),
525
+ through_approval=True)
526
+ thread_id = thread_id or ((run.get("extras") or {}).get("threadId"))
527
+ if not re.fullmatch(r"[a-f0-9]{12}", str(thread_id or "")):
528
+ thread_id = None
529
+ state = describe(run)
530
+ return ToolResult(sentence(state, thread_id or "(none)", answerable=run.get("origin") in (None, self.origin),
531
+ origin=self.origin),
532
+ structured={"thread_id": thread_id, "run_id": run_id, **state})
533
+
534
+
535
+ TOOL_PREFIX = "workflow_" # a workflow made a tool (workflows.tools names it)
536
+
537
+
538
+ def graph_state(batch, limit=60):
539
+ """A graph run as a tool answers: its status, its result and each step's state, reason and output preview."""
540
+ nodes = sorted((batch.get("nodes") or {}).values(), key=lambda node: node.get("seq", 0))
541
+ steps = [{key: node[key] for key in ("instance", "type", "state", "reason", "output", "runId") if key in node}
542
+ for node in nodes if not str(node.get("type")).startswith("trigger.")][:limit]
543
+ return {"batch_id": batch.get("id"), "status": batch.get("status"), "reason": batch.get("reason"),
544
+ "result": batch.get("result"), "steps": steps}
545
+
546
+
547
+ def schema_workflows():
548
+ return {"type": "object", "properties": {}, "required": [], "additionalProperties": False}
549
+
550
+
551
+ def schema_run_workflow():
552
+ return {"type": "object", "properties": {
553
+ "workflow": {"type": "string", "minLength": 1, "maxLength": 120,
554
+ "description": "the workflow's name or id, from list_workflows"},
555
+ "values": {"type": "object", "additionalProperties": {"type": "string", "maxLength": 300},
556
+ "description": "a value for each blank, by name, like {\"topic\": \"basketball\"}; a blank left "
557
+ "out takes its default"},
558
+ "thread_id": {"type": "string", "pattern": ID,
559
+ "description": "run it in this conversation (from phone_task); leave it out for a new "
560
+ "conversation, or the workflow's own one when it always continues one"},
561
+ "new_conversation": {"type": "boolean",
562
+ "description": "start a new conversation even when the workflow always continues one"},
563
+ "wait_seconds": {"type": "integer", "minimum": 0, "maximum": WAIT_MAX,
564
+ "description": "how long to wait for the task before returning (default 30); call "
565
+ "phone_task_status for the rest"}},
566
+ "required": ["workflow"], "additionalProperties": False}
567
+
568
+
569
+ def schema_graph_status():
570
+ return {"type": "object", "properties": {
571
+ "workflow_id": {"type": "string", "pattern": ID, "description": "the saved workflow's id from run_workflow"},
572
+ "batch_id": {"type": "string", "pattern": ID, "description": "the graph batch's id from run_workflow"},
573
+ "wait_seconds": {"type": "integer", "minimum": 0, "maximum": WAIT_MAX,
574
+ "description": "wait up to this long for the graph to finish (default 0)"}},
575
+ "required": ["workflow_id", "batch_id"], "additionalProperties": False}
576
+
577
+
578
+ def shape(workflow):
579
+ """{"nodes", "branches", "loops"}: how many steps its graph has, and how many branch or loop."""
580
+ nodes = [n for n in ((workflow.get("graph") or {}).get("nodes") or [])
581
+ if not str(n.get("type")).startswith("trigger.")]
582
+ return {"nodes": len(nodes), "branches": sum(1 for n in nodes if n.get("type") == "branch"),
583
+ "loops": sum(1 for n in nodes if n.get("type") == "loop")}
584
+
585
+
586
+ def describe_workflow(workflow):
587
+ """One line: its name, id, steps, its graph's shape and its blanks."""
588
+ blanks = workflow.get("blanks") or []
589
+ words = ", ".join(f"{b['name']}" + (f" (default {b['default']})" if b.get("default") else " (no default)")
590
+ for b in blanks)
591
+ steps = " ".join(str(workflow.get("goal") or "").split())
592
+ line = f"“{workflow.get('name')}” ({workflow.get('id')}): {steps[:300]}"
593
+ counts = shape(workflow)
594
+ if counts["nodes"] > 1:
595
+ singular = {"branches": "branch", "loops": "loop"}
596
+ parts = [f"{counts['nodes']} steps"] + [f"{counts[key]} {key if counts[key] > 1 else singular[key]}"
597
+ for key in ("branches", "loops") if counts[key]]
598
+ line += " Graph: " + ", ".join(parts) + "."
599
+ if words:
600
+ line += f" Blanks: {words}."
601
+ if workflow.get("sameConversation"):
602
+ line += " It always continues one conversation."
603
+ return line
604
+
605
+
606
+ class WorkflowsProvider(ThreadsProvider):
607
+ """mcp_server.registry.ToolProvider for saved workflows: list them, and run one with its blanks filled, in a new
608
+ conversation or one you name."""
609
+
610
+ name = "workflows"
611
+
612
+ def definitions(self, toolset):
613
+ return self.static_definitions() + [tool for tool, _workflow in self.workflow_tools()]
614
+
615
+ def workflow_tools(self):
616
+ """[(definition, tool)] for each published workflow the person made a tool of (``asTool``), as Mobster defines
617
+ it (GET /api/workflows/tools; the same tool is its REST route), or none when Mobster isn't reachable: a tool
618
+ list never fails for them."""
619
+ try:
620
+ listed = self.client().workflow_tools()
621
+ except Exception: # noqa: BLE001 -- no Mobster to ask, or one too old to say: the fixed tools only
622
+ return []
623
+ return [({"name": tool["name"], "description": tool["description"], "inputSchema": tool["inputSchema"]}, tool)
624
+ for tool in listed if str(tool.get("name") or "").startswith(TOOL_PREFIX)]
625
+
626
+ def static_definitions(self):
627
+ return [
628
+ {"name": "list_workflows",
629
+ "description": "The user's saved workflows in Mobster: each one's name, steps and blanks.",
630
+ "inputSchema": schema_workflows()},
631
+ {"name": "run_workflow",
632
+ "description": "Run one of the user's saved workflows on their iPhone, with a value for each blank, in "
633
+ "a new conversation or the one you name with thread_id. Mobster asks the user before it "
634
+ "sends, buys, posts or deletes.",
635
+ "inputSchema": schema_run_workflow()},
636
+ {"name": "graph_status",
637
+ "description": "Read a graph workflow's accepted batch by workflow_id and batch_id, without "
638
+ "starting it again.",
639
+ "inputSchema": schema_graph_status()}]
640
+
641
+ def annotations(self):
642
+ from ..mcp_server.tools import ACTS, READ_ONLY
643
+ return {"list_workflows": READ_ONLY, "run_workflow": ACTS, "graph_status": READ_ONLY,
644
+ **{tool["name"]: ACTS for tool, _workflow in self.workflow_tools()}}
645
+
646
+ def instructions(self):
647
+ return "run_workflow runs one of the user's saved workflows (list_workflows) with its blanks filled in."
648
+
649
+ def call(self, name, arguments, call, toolset):
650
+ from ..mcp_server.protocol import ToolResult
651
+ from ..mcp_server.tools import ToolError
652
+ client = self.client()
653
+ try:
654
+ if name == "list_workflows":
655
+ listed = client.workflows()
656
+ if not listed:
657
+ return ToolResult("The user has no saved workflows yet.", structured={"workflows": []})
658
+ return ToolResult("\n".join(describe_workflow(w) for w in listed), structured={"workflows": [
659
+ {"id": w.get("id"), "name": w.get("name"), "steps": w.get("goal"), "blanks": w.get("blanks") or [],
660
+ "sameConversation": bool(w.get("sameConversation")), **shape(w),
661
+ "published": (w.get("published") or {}).get("revision")} for w in listed]})
662
+ if name == "graph_status":
663
+ workflow_id, batch_id = arguments["workflow_id"], arguments["batch_id"]
664
+ wait = self.poll_seconds({"wait_seconds": arguments.get("wait_seconds", 0)}, call, toolset, client)
665
+ if wait is None:
666
+ return ToolResult("There is no time to read this graph in the current call. Call graph_status "
667
+ "again with the same workflow_id and batch_id; do not start the workflow again.",
668
+ structured={"workflow_id": workflow_id, "batch_id": batch_id,
669
+ "status": "status_unavailable",
670
+ "status_uri": f"/api/workflows/{workflow_id}/runs"})
671
+ batch = client.wait_batch(workflow_id, batch_id, wait)
672
+ if batch is None:
673
+ raise ToolError("Mobster cannot find that graph batch in recent history. Inspect the Mobster "
674
+ "app; do not restart it to check.")
675
+ state = graph_state(batch)
676
+ return ToolResult(f"Graph batch {batch_id} is {str(state['status']).replace('_', ' ')}.",
677
+ structured={"workflow_id": workflow_id, **state,
678
+ "status_uri": f"/api/workflows/{workflow_id}/runs"})
679
+ if name.startswith(TOOL_PREFIX):
680
+ found = next((tool for definition, tool in self.workflow_tools() if definition["name"] == name), None)
681
+ if found is None:
682
+ raise ToolError(f"No workflow is the tool {name} any more. Call list_workflows.")
683
+ values = {k: v for k, v in arguments.items() if k != "wait_seconds"}
684
+ arguments = {"workflow": found["workflowId"], "values": values, **(
685
+ {"wait_seconds": arguments["wait_seconds"]} if "wait_seconds" in arguments else {})}
686
+ if getattr(toolset, "allow_devices", None) is not None:
687
+ raise ToolError("This server may use only the devices given with --allow-device. Saved workflows "
688
+ "can route to other phones, so this server cannot start them. Run the workflow in "
689
+ "the Mobster app.")
690
+ try:
691
+ workflow = client.find_workflow(arguments["workflow"])
692
+ except LookupError as error:
693
+ raise ToolError(str(error)) from None
694
+ thread_id = arguments.get("thread_id")
695
+ self.in_time(name, "workflow run", arguments, call, toolset, client)
696
+ thread_id, answer = self.submit(lambda: client.run_workflow(
697
+ workflow["id"], values=arguments.get("values"), thread_id=thread_id,
698
+ new_conversation=bool(arguments.get("new_conversation"))), "workflow run")
699
+ run_id = (answer.get("run") or {}).get("id")
700
+ wait = self.poll_seconds(arguments, call, toolset, client)
701
+ batch = answer.get("batch") or {}
702
+ if batch.get("graph") == 2:
703
+ # A graph run: follow it to its end, then say how each step ended.
704
+ def graph(seconds):
705
+ state = graph_state(client.wait_batch(workflow["id"], batch["id"], seconds) or batch)
706
+ done = sum(1 for step in state["steps"] if step.get("state") == "done")
707
+ reason = f" {state['reason']}" if state.get("reason") else ""
708
+ waits = " Approvals wait for the user in the Mobster app." if state["status"] == "running" else ""
709
+ return ToolResult(f"“{workflow['name']}” {str(state['status']).replace('_', ' ')}: {done} of "
710
+ f"{len(state['steps'])} steps done.{reason}{waits}", structured={
711
+ "workflow_id": workflow["id"], "thread_id": thread_id, "run_id": run_id,
712
+ "status_uri": f"/api/workflows/{workflow['id']}/runs", **state})
713
+ return self.follow(wait, graph, accepted_unavailable(
714
+ ToolResult, text=f"Mobster accepted the graph workflow, but its status is unavailable. "
715
+ f"Call graph_status with workflow_id {workflow['id']} and batch_id {batch['id']}; "
716
+ "do not run it again.", workflow_id=workflow["id"], thread_id=thread_id, run_id=run_id,
717
+ batch_id=batch["id"], status_uri=f"/api/workflows/{workflow['id']}/runs"))
718
+ if not run_id:
719
+ state = {"status": "running"}
720
+ return ToolResult(f"Started “{workflow['name']}”. " + sentence(state, thread_id or "(none)",
721
+ origin=self.origin),
722
+ structured={"workflow_id": workflow["id"], "thread_id": thread_id,
723
+ "run_id": None, **state})
724
+
725
+ def task(seconds):
726
+ state = describe(client.wait_run(run_id, seconds))
727
+ return ToolResult(f"Started “{workflow['name']}”. " + sentence(state, thread_id
728
+ or "(none)", origin=self.origin),
729
+ structured={"workflow_id": workflow["id"], "thread_id": thread_id, "run_id": run_id,
730
+ **state})
731
+ return self.follow(wait, task, accepted_unavailable(
732
+ ToolResult, text=f"Mobster accepted the workflow, but its status is unavailable. Call "
733
+ f"phone_task_status with run_id {run_id}; do not run it again.",
734
+ workflow_id=workflow["id"], thread_id=thread_id, run_id=run_id,
735
+ status_uri=f"/api/runs/{run_id}"))
736
+ except ServiceError as error:
737
+ raise ToolError(str(error)) from None
738
+ except NoService:
739
+ raise ToolError(NO_SERVICE) from None