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,899 @@
1
+ """A graph run: one run of a workflow's published graph (docs/workflows.mdx › How a graph runs).
2
+
3
+ One coordinator thread decides what runs; worker threads run it. Nothing else changes a run's state.
4
+
5
+ - **Entry.** The trigger that started the run is done at once with what it carries; the other triggers are absent, so a
6
+ node only they lead to is skipped.
7
+ - **Readiness.** A node runs when every edge into it is live; it is skipped when one is dead (a path not taken), and
8
+ blocked when one is bad (something it needs failed). A merge joins paths: ``all`` waits for every path that runs,
9
+ ``first`` goes on with the first. A loop counts the edges into its body from outside as its own.
10
+ - **Order.** Nodes that are ready together start in one fixed order (their loop round, then their place at their
11
+ level, then the order they were made), so the same graph with the same outcomes runs the same way every time.
12
+ - **Phones.** A step on a phone holds that phone's slot from admission to its task's end: never two of a run's steps on
13
+ one phone at once, and steps on different phones side by side. Non-phone nodes run side by side too, ``MAX_PARALLEL``
14
+ at most. Every phone step is an ordinary task (``Runtime.create``): its approvals, the phone's rules and the spending
15
+ stops all apply.
16
+ - **Journaled before it acts.** A node that acts is written as running, with its idempotency key, before it acts;
17
+ then with its task's id; then how it ended. A run that was going when Mobster quit ends interrupted at the next
18
+ launch, and nothing in it is run again.
19
+ - **Failure.** A node that fails fires its error edges when it has some (handled), else blocks what depends on it.
20
+ A node whose outcome is unknown is never retried and never fires an error edge. The run ends with the first unhandled
21
+ failure's own status (``approval_denied``, ``spend_cap``...), as a chain of steps always has.
22
+ - **Spending.** Each phone step reserves its cap before it starts, its share of what is left; a run never spends past
23
+ its limit.
24
+ """
25
+
26
+ import logging
27
+ import threading
28
+ import time
29
+ from collections import defaultdict
30
+
31
+ from ..api_errors import APIError
32
+ from ..workflows import records as batch
33
+ from ..workflows import starters
34
+ from . import expr, nodes
35
+ from .catalog import TRIGGERS, TYPES
36
+ from .schema import _topological
37
+ from .state import Debug, Instance, LoopState, Scope, Slots, kept, ports_of, preview # noqa: F401 -- re-exported
38
+
39
+ log = logging.getLogger("mobster.graph")
40
+
41
+ MAX_PARALLEL = 8
42
+ MAX_INSTANCES = 1000
43
+ MAX_PHONE_TASKS = 400
44
+ MAX_PACES = 20 # times one step waits for its profile's pace before it fails
45
+ FIRST_WAIT_S = 10.0 # how long a start waits for the run's first task, to open it
46
+ TERMINAL = frozenset({"done", "skipped", "failed", "blocked", "cancelled", "unknown"})
47
+ GOING = frozenset({"running", "waiting"})
48
+ ITERATION = object()
49
+
50
+
51
+ class GraphRun:
52
+ """One run of ``graph`` for ``owner`` (workflows.Workflows)."""
53
+
54
+ monotonic = staticmethod(time.monotonic)
55
+
56
+ def __init__(self, owner, record, workflow, graph, *, entry, payload, values, create, origin, clock=time.time,
57
+ debug=None, slots=None, parent=None):
58
+ self.owner, self.runtime = owner, owner.runtime
59
+ # A test run or a re-run from the editor (graph/rerun.py): pins, outputs reused from an earlier run, nodes
60
+ # that don't run, an edited input, one round of a loop.
61
+ self.debug = debug if debug is not None else Debug()
62
+ self.record, self.workflow, self.graph = record, workflow, graph
63
+ self.entry, self.payload, self.values = entry, payload, values or {}
64
+ self.create, self.origin, self.clock = create, origin, clock
65
+ self.limits = graph.get("limits") or {"maxSpendUsd": 5.0, "maxMinutes": 240}
66
+ self.cond = threading.Condition(threading.RLock())
67
+ self.stop = threading.Event()
68
+ self.stop_status = None
69
+ self.first = threading.Event()
70
+ self.first_run = None
71
+ self.last = None
72
+ self.current = {}
73
+ self.task_runs = {}
74
+ self.instances = {}
75
+ self.workers = 0
76
+ self.slots = slots if slots is not None else Slots()
77
+ self.parent = parent
78
+ self.children = {}
79
+ self.call_stack = (parent.call_stack if parent is not None else []) + [workflow["id"]]
80
+ self.approvals = {}
81
+ self.approve_all = set()
82
+ self.spent = self.reserved = 0.0
83
+ self.model_calls = 0
84
+ self.runs = 0
85
+ self.seq = 0
86
+ self.result = None
87
+ self.finished_order = []
88
+ self.started = clock()
89
+ self.thread = threading.Thread(target=self._main, name=f"mobster-graph-{record['id']}", daemon=True)
90
+ self.nodes = {node["id"]: node for node in graph["nodes"]}
91
+ self.incoming = defaultdict(list)
92
+ for edge in graph.get("edges") or []:
93
+ self.incoming[edge["target"]].append(edge)
94
+ self.error_edges = {edge["source"] for edge in graph.get("edges") or [] if edge["sourcePort"] == "error"}
95
+ self.rank = {}
96
+ for level in [None] + [n["id"] for n in graph["nodes"] if n["type"] == "loop"]:
97
+ members = [n["id"] for n in graph["nodes"] if n.get("parent") == level]
98
+ for index, ident in enumerate(_topological(members, self._level_edges(level)) or members):
99
+ self.rank[ident] = index
100
+ self.top = Scope(None)
101
+ # A blank's default holds in every step that names it, wherever the default was written.
102
+ self.blank_defaults = {}
103
+ for node in graph["nodes"]:
104
+ for name, field in TYPES[node["type"]].fields.items():
105
+ value = (node.get("config") or {}).get(name)
106
+ if not field.blanks or value is None:
107
+ continue
108
+ for text in _texts(value):
109
+ try:
110
+ found = expr.template(text).blanks
111
+ except expr.ExprError:
112
+ continue
113
+ for _kind, blank, default in found:
114
+ key = starters._key(blank)
115
+ if default and not self.blank_defaults.get(key):
116
+ self.blank_defaults[key] = default
117
+
118
+ def _parent(self, ident):
119
+ return self.nodes[ident].get("parent")
120
+
121
+ def _within(self, ident, loop):
122
+ at = self._parent(ident)
123
+ while at is not None:
124
+ if at == loop:
125
+ return True
126
+ at = self._parent(at)
127
+ return False
128
+
129
+ def _lift(self, ident, level):
130
+ at = ident
131
+ while at is not None:
132
+ if self._parent(at) == level:
133
+ return at
134
+ at = self._parent(at)
135
+ return None
136
+
137
+ def _level_edges(self, level):
138
+ out = set()
139
+ for edge in self.graph.get("edges") or []:
140
+ if self.nodes[edge["source"]]["type"] == "loop" and edge["sourcePort"] in ("item", "index") \
141
+ and self._within(edge["target"], edge["source"]):
142
+ continue
143
+ a, b = self._lift(edge["source"], level), self._lift(edge["target"], level)
144
+ if a is not None and b is not None and a != b:
145
+ out.add((a, b))
146
+ return out
147
+
148
+ def _now_ms(self):
149
+ return round(self.clock() * 1000)
150
+
151
+ def _publish(self, event):
152
+ publish = getattr(self.runtime, "publish", None)
153
+ if callable(publish):
154
+ publish("workflows", {"workflowId": self.workflow["id"], "batchId": self.record["id"],
155
+ "parentBatchId": self.parent.record["id"] if self.parent is not None else None,
156
+ **event})
157
+
158
+ def _save(self, inst, io=None):
159
+ """Journal ``inst`` as it is now (and its input and output when it ended), then say so. Under the lock."""
160
+ entry = inst.public()
161
+ self.owner.batches.save_node(self.record["id"], entry, io)
162
+ self._publish({"event": "batch_node", **entry})
163
+ if inst.kind in batch.LEGACY_KINDS:
164
+ self._publish({"event": "batch_step", "entry": batch.legacy_entry(entry), "index": inst.seq})
165
+
166
+ def start(self):
167
+ self.thread.start()
168
+
169
+ def _main(self):
170
+ try:
171
+ with self.cond:
172
+ self._open(self.top)
173
+ self._publish({"event": "batch_started", "graph": 2, "trigger": self.entry,
174
+ "revision": self.record.get("revision"),
175
+ "nodes": {i.id: i.state for i in self.instances.values()}})
176
+ self._run()
177
+ status, reason = self._outcome()
178
+ except Exception as error: # noqa: BLE001 -- a bug ends the run, said plainly; nothing is retried
179
+ log.exception("graph run %s failed", self.record["id"])
180
+ status, reason = "error", f"Mobster hit a problem running the workflow: {type(error).__name__}."
181
+ self.stop.set()
182
+ self._stop_tasks()
183
+ self._drain()
184
+ with self.cond:
185
+ for inst in self.instances.values():
186
+ if inst.state not in TERMINAL:
187
+ self._end(inst, "cancelled", "The run stopped on a problem.")
188
+ finally:
189
+ self.first.set()
190
+ if status == "completed":
191
+ self._send_to()
192
+ with self.cond:
193
+ self.record.update(status=status, reason=reason, finishedAt=self._now_ms(),
194
+ spentUsd=round(self.spent, 6), runs=self.runs,
195
+ modelCalls=self.model_calls,
196
+ result=preview(self.result) if self.result is not None else None)
197
+ if self.parent is not None: # a sub-workflow's run answers to its parent, not to a dispatch
198
+ try:
199
+ self.owner.batches.save(self.record)
200
+ except Exception: # noqa: BLE001 -- the parent's own end still says how it ended
201
+ log.exception("graph run %s: saving its end failed", self.record["id"])
202
+ if self.parent is None:
203
+ # The owner commits this record with the dispatch's outcome in one transaction, then publishes.
204
+ self.owner.batch_finished(self, status, reason)
205
+ else:
206
+ self.publish_end(status, reason)
207
+
208
+ def publish_end(self, status, reason):
209
+ self._publish({"event": "batch_finished", "status": status, "reason": reason,
210
+ "result": self.record.get("result")})
211
+
212
+ def _run(self):
213
+ with self.cond:
214
+ while True:
215
+ self._check_limits()
216
+ if self.stop.is_set():
217
+ self._cancel_waiting()
218
+ moved = self._settle()
219
+ moved = self._start_ready() or moved
220
+ if self._finished():
221
+ return
222
+ if not moved:
223
+ self.cond.wait(0.25)
224
+
225
+ def _drain(self, seconds=5.0):
226
+ deadline = self.monotonic() + seconds
227
+ with self.cond:
228
+ while self.workers and self.monotonic() < deadline:
229
+ self.cond.wait(0.1)
230
+
231
+ def _finished(self):
232
+ return self.workers == 0 and all(inst.state in TERMINAL for inst in self.instances.values())
233
+
234
+ def _check_limits(self):
235
+ if self.stop.is_set():
236
+ if self.stop_status is None:
237
+ self.stop_status = "interrupted" if getattr(self.runtime, "closing", False) else "stopped"
238
+ return
239
+ if getattr(self.runtime, "closing", False):
240
+ self.stop_status = "interrupted"
241
+ elif self.clock() - self.started >= self.limits["maxMinutes"] * 60:
242
+ self.stop_status = "time_limit"
243
+ else:
244
+ return
245
+ self.stop.set()
246
+ self._stop_tasks()
247
+
248
+ def stop_all(self):
249
+ self.stop.set()
250
+ self._stop_tasks()
251
+ for child in list(self.children.values()):
252
+ child.stop_all()
253
+ with self.cond:
254
+ self.cond.notify_all()
255
+
256
+ def _stop_tasks(self):
257
+ for run in list(self.current.values()):
258
+ try:
259
+ self.runtime.stop_run(run.id)
260
+ except Exception: # noqa: BLE001 -- it may have just finished
261
+ pass
262
+
263
+ def stop_reason(self):
264
+ return {"stopped": "You stopped the workflow.", "interrupted": "Mobster quit while this workflow ran.",
265
+ "time_limit": f"The workflow reached its time limit of {self.limits['maxMinutes']} minutes."
266
+ }.get(self.stop_status, "The workflow stopped.")
267
+
268
+ def _cancel_waiting(self):
269
+ for inst in list(self.instances.values()):
270
+ if inst.state in ("pending", "queued"):
271
+ self._end(inst, "cancelled", self.stop_reason())
272
+
273
+ def _outcome(self):
274
+ if self.stop_status:
275
+ return self.stop_status, self.stop_reason()
276
+ for inst in sorted(self.instances.values(), key=lambda i: (i.finished_at or 0, i.seq)):
277
+ if inst.reused:
278
+ continue # how an earlier run ended isn't how this one did
279
+ if inst.state == "failed" and not inst.handled or inst.state == "unknown":
280
+ return inst.status or ("error" if inst.state == "unknown" else "failed"), inst.reason
281
+ if any(inst.state == "blocked" and not inst.reused for inst in self.instances.values()):
282
+ # Stopped behind a step kept as it ended in the run this one picks up from (a resume).
283
+ kept = next((inst for inst in self.instances.values() if inst.reused and
284
+ (inst.state == "unknown" or inst.state == "failed" and not inst.handled)), None)
285
+ return (kept.status or "failed", kept.reason) if kept else ("failed", None)
286
+ output = next((node for node in self.graph["nodes"] if node["type"] == "output"), None)
287
+ advertised = (output or {}).get("config", {}).get("schema")
288
+ if advertised is not None:
289
+ result = next((inst for inst in self.instances.values() if inst.kind == "output"), None)
290
+ if result is None or result.state != "done":
291
+ if result is None or result.node["id"] not in self.debug.skip:
292
+ return "output_schema", "This workflow did not produce a result matching its saved shape."
293
+ else:
294
+ try:
295
+ nodes.result_value(advertised, self.result)
296
+ except nodes.NodeFailed as failed:
297
+ return failed.status, failed.reason
298
+ return "completed", None
299
+
300
+ def _open(self, scope):
301
+ """Make an instance of every node at the scope's level. At the top, the entry trigger is done at once."""
302
+ members = sorted((n for n in self.graph["nodes"] if n.get("parent") == scope.level),
303
+ key=lambda n: self.rank.get(n["id"], 0))
304
+ if len(self.instances) + len(members) > MAX_INSTANCES:
305
+ raise _Limit(f"The workflow reached its limit of {MAX_INSTANCES:,} steps in one run.")
306
+ for node in members:
307
+ self.seq += 1
308
+ inst = Instance(node["id"] + scope.suffix, node, scope, self.seq)
309
+ scope.members[node["id"]] = inst
310
+ self.instances[inst.id] = inst
311
+ recorded = self.debug.reuse.get(inst.id)
312
+ if recorded is not None:
313
+ self._reuse(inst, recorded)
314
+ continue
315
+ if node["type"] in TRIGGERS:
316
+ if node["type"] == self.entry:
317
+ inst.started_at = self._now_ms()
318
+ pin = node.get("pin") if self.debug.test else None
319
+ payload = pin["out"] if isinstance(pin, dict) and "out" in pin else self.payload
320
+ inst.pinned = payload is not self.payload
321
+ self._end(inst, "done", output=payload)
322
+ else:
323
+ self._end(inst, "skipped", "This trigger didn't start this run.")
324
+ continue
325
+ self._save(inst)
326
+
327
+ def _reuse(self, inst, recorded):
328
+ """An instance whose result comes from an earlier run: it ends as it did then, and nothing in it runs again."""
329
+ inst.reused, inst.started_at = True, recorded.get("startedAt")
330
+ inst.input, inst.run_id = recorded.get("input"), recorded.get("runId")
331
+ inst.answer, inst.cost = recorded.get("answer"), None
332
+ state, output = recorded["state"], recorded.get("output")
333
+ if state == "done":
334
+ inst.port = recorded.get("port")
335
+ self._end(inst, "done", output=output, ports=ports_of(inst.kind, output, inst.port))
336
+ elif state == "failed":
337
+ inst.handled = bool(recorded.get("handled"))
338
+ inst.ports = {"error": recorded.get("error")} if inst.handled else {}
339
+ self._end(inst, "failed", recorded.get("reason"), status=recorded.get("status"))
340
+ else:
341
+ self._end(inst, state, recorded.get("reason") or "It ended this way in the run this one picks up from.",
342
+ status=recorded.get("status"))
343
+
344
+ def _source(self, edge, scope):
345
+ """The instance an edge comes from, seen from ``scope``: ITERATION for a loop's item and index."""
346
+ source = edge["source"]
347
+ level = self._parent(source)
348
+ for each in scope.chain():
349
+ if each.loop is not None and each.loop.node["id"] == source and edge["sourcePort"] in ("item", "index"):
350
+ return ITERATION, each
351
+ if each.level == level:
352
+ return each.members.get(source), each
353
+ return None, None
354
+
355
+ def _edge_state(self, edge, scope):
356
+ source, where = self._source(edge, scope)
357
+ if source is ITERATION:
358
+ return "live"
359
+ if source is None:
360
+ return "pending"
361
+ if source.kind in TRIGGERS and source.state == "skipped":
362
+ return "absent"
363
+ state = source.state
364
+ if state == "done":
365
+ return "live" if edge["sourcePort"] in source.ports else "dead"
366
+ if state == "failed":
367
+ if source.handled:
368
+ return "live" if edge["sourcePort"] == "error" else "dead"
369
+ return "bad"
370
+ if state == "skipped":
371
+ return "dead"
372
+ if state in ("blocked", "cancelled", "unknown"):
373
+ return "bad"
374
+ return "pending"
375
+
376
+ def _edges(self, inst):
377
+ """(edge, state) of every edge inst waits for: its own, and for a loop those into its body from outside."""
378
+ out = [(edge, self._edge_state(edge, inst.scope)) for edge in self.incoming[inst.node["id"]]]
379
+ if inst.kind == "loop":
380
+ loop = inst.node["id"]
381
+ for target, edges in self.incoming.items():
382
+ if not self._within(target, loop):
383
+ continue
384
+ for edge in edges:
385
+ if edge["source"] == loop or self._within(edge["source"], loop):
386
+ continue
387
+ out.append((edge, self._edge_state(edge, inst.scope)))
388
+ return out
389
+
390
+ def _value(self, edge, scope):
391
+ """What ``edge`` carries into its target in ``scope``, after its map."""
392
+ source, where = self._source(edge, scope)
393
+ if source is ITERATION:
394
+ value = where.item if edge["sourcePort"] == "item" else where.index
395
+ else:
396
+ value = source.ports.get(edge["sourcePort"])
397
+ if edge.get("map"):
398
+ value = expr.evaluate(edge["map"], {**self.scope_values(scope), "input": value},
399
+ timezone=self.workflow.get("timezone"))
400
+ return value
401
+
402
+ def scope_values(self, scope):
403
+ """The roots an expression in ``scope`` reads (all but $input)."""
404
+ done = {}
405
+ for each in reversed(list(scope.chain())):
406
+ for ident, member in each.members.items():
407
+ if member.state == "done":
408
+ done[ident] = member.ports.get("out", member.output)
409
+ values = {"inputs": self.values, "vars": self.graph.get("vars") or {}, "node": done,
410
+ "run": {"id": self.record["id"], "workflowId": self.workflow["id"], "trigger": self.entry,
411
+ "startedAt": round(self.started * 1000), "test": bool(self.record.get("test"))}}
412
+ loop = next((each for each in scope.chain() if each.loop is not None), None)
413
+ if loop is not None:
414
+ values["item"], values["index"] = loop.item, loop.index
415
+ return values
416
+
417
+ def blank_values(self, scope):
418
+ """The values a node's {blanks} fill from: the run's, then each enclosing row's over them."""
419
+ out = {**self.blank_defaults, **self.values}
420
+ for each in reversed(list(scope.chain())):
421
+ out.update(each.row)
422
+ return out
423
+
424
+ def _settle(self):
425
+ moved = False
426
+ for inst in list(self.instances.values()):
427
+ if inst.state == "pending":
428
+ moved = self._resolve(inst) or moved
429
+ elif inst.kind == "loop" and inst.state == "running":
430
+ moved = self._drive_loop(inst) or moved
431
+ return moved
432
+
433
+ def _resolve(self, inst):
434
+ edges = self._edges(inst)
435
+ present = [(edge, state) for edge, state in edges if state != "absent"]
436
+ if edges and not present:
437
+ self._end(inst, "skipped", "It starts from a trigger that didn't start this run.")
438
+ return True
439
+ states = [state for _edge, state in present]
440
+ bad = next((edge for edge, state in present if state == "bad"), None)
441
+ if inst.kind == "merge":
442
+ live = [edge for edge, state in present if state == "live"]
443
+ if inst.config.get("mode") == "first" and live:
444
+ first = min(live, key=lambda e: (self._finished_at(e, inst.scope), e["id"]))
445
+ return self._ready(inst, [first])
446
+ if "pending" in states:
447
+ return False
448
+ if bad is not None:
449
+ return self._block(inst, bad)
450
+ if live and inst.config.get("mode", "all") == "all":
451
+ return self._ready(inst, live)
452
+ self._end(inst, "skipped", "No path into it ran.")
453
+ return True
454
+ if bad is not None:
455
+ return self._block(inst, bad)
456
+ if "pending" in states:
457
+ return False
458
+ if "dead" in states:
459
+ self._end(inst, "skipped", "It is on a path this run didn't take.")
460
+ return True
461
+ return self._ready(inst, [edge for edge, _state in present if edge["target"] == inst.node["id"]])
462
+
463
+ def _finished_at(self, edge, scope):
464
+ source, _where = self._source(edge, scope)
465
+ return getattr(source, "finished_at", None) or 0
466
+
467
+ def _block(self, inst, edge):
468
+ source, _where = self._source(edge, inst.scope)
469
+ title = source.title if isinstance(source, Instance) else edge["source"]
470
+ why = source.reason if isinstance(source, Instance) and source.reason else "it didn't finish"
471
+ self._end(inst, "blocked", f"Stopped because “{title}” didn't finish: {why}"[:300])
472
+ return True
473
+
474
+ def _ready(self, inst, edges):
475
+ try:
476
+ values = [self._value(edge, inst.scope) for edge in edges if edge["targetPort"] == "in"]
477
+ inst.input = values[0] if len(values) == 1 else values if values else None
478
+ items = [edge for edge in edges if edge["targetPort"] == "items"]
479
+ inst.items = self._value(items[0], inst.scope) if items else None
480
+ except expr.ExprError as error:
481
+ self._fail(inst, f"An edge's map couldn't work out its value: {error}", "failed")
482
+ return True
483
+ if self.stop.is_set():
484
+ self._end(inst, "cancelled", self.stop_reason())
485
+ return True
486
+ ident = inst.node["id"]
487
+ if ident in self.debug.inputs and not inst.scope.path:
488
+ inst.input = self.debug.inputs[ident]
489
+ if ident in self.debug.skip:
490
+ self._end(inst, "skipped", self.debug.skip[ident])
491
+ return True
492
+ pin = inst.node.get("pin")
493
+ if self.debug.test and isinstance(pin, dict) and pin:
494
+ inst.pinned, inst.started_at = True, self._now_ms()
495
+ self._end(inst, "done", output=pin.get("out"), ports=dict(pin))
496
+ return True
497
+ inst.state, inst.ready_at = "queued", self._now_ms()
498
+ self._save(inst)
499
+ return True
500
+
501
+ def _start_ready(self):
502
+ if self.stop.is_set():
503
+ return False
504
+ moved = False
505
+ queued = sorted((i for i in self.instances.values() if i.state == "queued"),
506
+ key=lambda i: (i.scope.path, self.rank.get(i.node["id"], 0), i.seq))
507
+ now = self._now_ms()
508
+ for inst in queued:
509
+ if inst.queued_reason == "pace" and inst.until is not None and now < inst.until:
510
+ continue
511
+ if inst.kind in nodes.INLINE:
512
+ self._inline(inst)
513
+ moved = True
514
+ elif inst.kind == "loop":
515
+ moved = self._start_loop(inst) or moved
516
+ elif TYPES[inst.kind].phone:
517
+ moved = self._start_phone(inst) or moved
518
+ elif self.workers < MAX_PARALLEL:
519
+ if inst.kind in nodes.SPENDS and not self._reserve(inst):
520
+ moved = True
521
+ continue
522
+ self._spawn(inst)
523
+ moved = True
524
+ elif inst.queued_reason != "slot":
525
+ inst.queued_reason = "slot"
526
+ self._save(inst)
527
+ return moved
528
+
529
+ def _inline(self, inst):
530
+ inst.state, inst.started_at = "running", self._now_ms()
531
+ try:
532
+ output, ports = nodes.INLINE[inst.kind](self, inst)
533
+ except nodes.NodeFailed as failed:
534
+ self._fail(inst, failed.reason, failed.status)
535
+ return
536
+ except expr.ExprError as error:
537
+ self._fail(inst, f"{error}", "failed")
538
+ return
539
+ self._end(inst, "done", output=output, ports=ports)
540
+
541
+ def _spawn(self, inst):
542
+ self.workers += 1
543
+ inst.state, inst.started_at, inst.queued_reason, inst.until = "running", self._now_ms(), None, None
544
+ self._save(inst)
545
+ threading.Thread(target=self._work, args=(inst,), name=f"mobster-graph-{inst.id}", daemon=True).start()
546
+
547
+ def _start_phone(self, inst):
548
+ slot, device = self._slot(inst)
549
+ if slot is None:
550
+ if inst.queued_reason != "phone":
551
+ inst.queued_reason = "phone"
552
+ self._save(inst)
553
+ return False
554
+ if self.runs >= MAX_PHONE_TASKS:
555
+ self._fail(inst, f"The workflow reached its limit of {MAX_PHONE_TASKS} phone tasks in one run.",
556
+ "loop_limit")
557
+ return True
558
+ if inst.kind in nodes.SPENDS and not self._reserve(inst):
559
+ return True
560
+ if not self.slots.take(slot, (self.record["id"], inst.id)):
561
+ self.reserved = max(0.0, self.reserved - inst.reserved)
562
+ inst.reserved = 0.0
563
+ return False
564
+ inst.slot, inst.device_id = slot, device
565
+ self._spawn(inst)
566
+ return True
567
+
568
+ def _reserve(self, inst):
569
+ """Set aside ``inst``'s share of what is left to spend; False (and the step failed) when nothing is."""
570
+ unreserved = self.limits["maxSpendUsd"] - self.spent - self.reserved
571
+ if unreserved <= 0.0001:
572
+ self._fail(inst, f"The workflow spent its limit of ${self.limits['maxSpendUsd']:.2f}.", "spend_cap")
573
+ return False
574
+ ready = sum(1 for i in self.instances.values()
575
+ if i.state == "queued" and i.kind in nodes.SPENDS and i is not inst) + 1
576
+ inst.reserved = round(unreserved / ready, 4)
577
+ self.reserved += inst.reserved
578
+ return True
579
+
580
+ def _slot(self, inst):
581
+ """(slot key, device id to run on) for a phone step, or (None, None) while its phone is held."""
582
+ device = inst.config.get("device")
583
+ fleet = getattr(self.runtime, "fleet", None)
584
+ if device == "loop":
585
+ phone = next((s.phone for s in inst.scope.chain() if s.phone is not None), None) or {}
586
+ key, device = phone.get("deviceId") or "primary", phone.get("deviceId")
587
+ elif device == "any":
588
+ if fleet is None:
589
+ key, device = "primary", None
590
+ else:
591
+ free = nodes.free_phones(self.runtime, self.slots.taken())
592
+ if not free:
593
+ return None, None
594
+ key = device = free[0]["id"]
595
+ elif device:
596
+ key = device
597
+ elif inst.config.get("account") or self.workflow.get("account"):
598
+ key, device = f"account:{inst.config.get('account') or self.workflow.get('account')}", None
599
+ else:
600
+ key, device = self.default_slot(), None
601
+ return (None, None) if key in self.slots.taken() else (key, device)
602
+
603
+ def default_slot(self):
604
+ return self.workflow.get("device") or getattr(self, "admitted_device", None) or "primary"
605
+
606
+ @staticmethod
607
+ def ai_client(model):
608
+ """The client an Ask the model step calls (graph/nodes.ai_client); a test gives its own."""
609
+ return nodes.ai_client(model)
610
+
611
+ def default_device(self):
612
+ """The phone the workflow's steps use when they name none, or None for the primary."""
613
+ return self.workflow.get("device") or getattr(self, "admitted_device", None)
614
+
615
+ def answer(self, instance, decision, *, value=None, has_value=False, note=None, by=None, request_id=None):
616
+ """The first answer wins; one naming ``request_id`` counts only while the step still waits for that request."""
617
+ with self.cond:
618
+ waiting = self.approvals.get(instance)
619
+ if waiting is None:
620
+ for child in self.children.values():
621
+ if instance in child.approvals:
622
+ return child.answer(instance, decision, value=value, has_value=has_value, note=note, by=by,
623
+ request_id=request_id)
624
+ raise APIError("That step isn't waiting for you.", 409, "not_waiting")
625
+ request, answered, box = waiting
626
+ if request_id is not None and request["id"] != request_id:
627
+ raise APIError("That step isn't waiting for you.", 409, "not_waiting")
628
+ if answered.is_set():
629
+ raise APIError("That step has been answered.", 409, "answered")
630
+ if has_value:
631
+ if not request["editable"]:
632
+ raise APIError("This step can't be edited before you approve it.", 409, "not_editable")
633
+ current = self.instances[instance].input
634
+ if current is not None and value is not None and expr.kind_of(value) != expr.kind_of(current):
635
+ raise ValueError(f"The edited value is {expr.kind_of(value)}, and this step's is "
636
+ f"{expr.kind_of(current)}")
637
+ box["value"] = value
638
+ box.update(decision=decision, note=note, by=by)
639
+ answered.set()
640
+ return request
641
+
642
+ def _work(self, inst):
643
+ tries = (inst.node.get("retry") or {}).get("tries", 0)
644
+ backoff = (inst.node.get("retry") or {}).get("backoffSeconds", 30)
645
+ try:
646
+ for attempt in range(tries + 1):
647
+ try:
648
+ output, ports = nodes.WORKERS[inst.kind](self, inst)
649
+ except nodes.NodeFailed as failed:
650
+ if failed.retryable and attempt < tries and not self.stop.is_set():
651
+ with self.cond:
652
+ inst.reason = f"Try {attempt + 1} didn't finish: {failed.reason}"[:300]
653
+ inst.attempt += 1
654
+ self._save(inst)
655
+ self.stop.wait(backoff)
656
+ continue
657
+ with self.cond:
658
+ inst.acted = False if failed.refused else failed.acted
659
+ self._fail(inst, failed.reason, failed.status, failed.run_id, refused=failed.refused)
660
+ return
661
+ except nodes.Paced as paced:
662
+ with self.cond:
663
+ self._paced(inst, paced)
664
+ return
665
+ except nodes.OutcomeUnknown as unknown:
666
+ with self.cond:
667
+ self._end(inst, "unknown", unknown.reason, status=unknown.status)
668
+ return
669
+ except nodes.Cancelled as cancelled:
670
+ with self.cond:
671
+ self._end(inst, "cancelled", cancelled.reason, status=cancelled.status)
672
+ return
673
+ except Exception as error: # noqa: BLE001 -- it may have acted: unknown, never retried
674
+ log.exception("graph run %s: %s failed", self.record["id"], inst.id)
675
+ with self.cond:
676
+ self._end(inst, "unknown", f"Mobster hit a problem running this step: {type(error).__name__}. "
677
+ "It may have acted, so it won't run again.", status="error")
678
+ return
679
+ with self.cond:
680
+ self._end(inst, "done", output=output, ports=ports)
681
+ return
682
+ finally:
683
+ with self.cond:
684
+ self.workers -= 1
685
+ self.current.pop(inst.id, None)
686
+ if inst.slot is not None:
687
+ self.slots.free(inst.slot, (self.record["id"], inst.id))
688
+ self.reserved = max(0.0, self.reserved - inst.reserved)
689
+ inst.reserved = 0.0
690
+ self.cond.notify_all()
691
+
692
+ def _paced(self, inst, paced):
693
+ """A step its pace held before it acted waits in ``queued``, then runs again as a new attempt with a new key."""
694
+ inst.paces += 1
695
+ left_ms = (self.started + self.limits["maxMinutes"] * 60 - self.clock()) * 1000
696
+ if paced.until - self._now_ms() > left_ms:
697
+ why, status = f"Waiting for it would pass the workflow's time limit of {self.limits['maxMinutes']} " \
698
+ "minutes.", "time_limit"
699
+ elif inst.paces > MAX_PACES:
700
+ why, status = f"The profile's pace held this step {MAX_PACES} times in this run.", "blocked"
701
+ else:
702
+ why = status = None
703
+ if status is not None:
704
+ inst.acted = False # nodes.phone raises Paced only for a task that didn't act outward
705
+ self._fail(inst, f"{paced.reason} {why}", status, paced.run_id)
706
+ return
707
+ inst.state, inst.queued_reason, inst.until = "queued", "pace", paced.until
708
+ inst.reason, inst.attempt = paced.reason, inst.attempt + 1
709
+ self._save(inst)
710
+
711
+ def _fail(self, inst, reason, status=None, run_id=None, refused=False):
712
+ inst.handled = inst.node["id"] in self.error_edges
713
+ if inst.handled:
714
+ inst.ports = {"error": {"node": inst.node["id"], "reason": reason, "status": status, "runId": run_id
715
+ or inst.run_id}}
716
+ inst.refused = refused
717
+ self._end(inst, "failed", reason, status=status)
718
+
719
+ def _end(self, inst, state, reason=None, *, output=None, ports=None, status=None):
720
+ inst.state, inst.finished_at = state, self._now_ms()
721
+ if reason is not None:
722
+ inst.reason = str(reason)[:300]
723
+ if status is not None:
724
+ inst.status = status
725
+ if state == "done":
726
+ inst.output = output
727
+ inst.ports = ports if ports is not None else {"out": output}
728
+ inst.reason = None
729
+ if inst.answer is None and isinstance(output, dict):
730
+ found = output.get("answer") if inst.kind != "connector.call" else output.get("text")
731
+ inst.answer = found if isinstance(found, str) else None
732
+ if inst.kind == "output":
733
+ self.result = output
734
+ inst.queued_reason = None
735
+ self.finished_order.append(inst.id)
736
+ io = None
737
+ if state in ("done", "failed", "unknown", "cancelled"):
738
+ io = {"input": kept(inst.input), "output": kept(inst.output) if state == "done" else None,
739
+ **({"items": kept(inst.items)} if inst.kind == "loop" and inst.items is not None else {}),
740
+ "error": inst.ports.get("error") if state == "failed" else
741
+ ({"reason": inst.reason, "status": inst.status} if state != "done" else None)}
742
+ self._save(inst, io)
743
+ self.cond.notify_all()
744
+
745
+ def _start_loop(self, inst):
746
+ config = inst.config
747
+ over = config["over"]
748
+ try:
749
+ if inst.node["id"] in self.debug.items and not inst.scope.path:
750
+ index, item = self.debug.items[inst.node["id"]]
751
+ items = [item]
752
+ elif over == "times":
753
+ items = list(range(config["times"]))
754
+ elif over == "rows":
755
+ rows = self.payload.get("rows") if isinstance(self.payload, dict) else None
756
+ items = (rows if rows else config.get("rows")) or []
757
+ if not items:
758
+ self._fail(inst, "Give the rows to repeat over, as a list or a CSV.", "missing_values")
759
+ return True
760
+ items = items[:config["max"]]
761
+ elif over == "items":
762
+ if not isinstance(inst.items, list):
763
+ self._fail(inst, "The loop's items aren't a list.", "failed")
764
+ return True
765
+ items = inst.items[:config["max"]]
766
+ elif over == "until":
767
+ items = None
768
+ else:
769
+ items = nodes.loop_phones(self.runtime, config["phones"])[:config.get("max") or MAX_INSTANCES]
770
+ if not items:
771
+ self._fail(inst, "No phone is ready to run the loop on.", "offline")
772
+ return True
773
+ except nodes.NodeFailed as failed:
774
+ self._fail(inst, failed.reason, failed.status)
775
+ return True
776
+ inst.items = items
777
+ concurrency = config.get("concurrency") or (len(items) if over == "phones" and items else 1)
778
+ inst.loop = LoopState(items, 1 if over == "until" else concurrency)
779
+ if inst.node["id"] in self.debug.items and not inst.scope.path:
780
+ inst.loop.offset, inst.loop.single = self.debug.items[inst.node["id"]][0], True
781
+ if over == "rows" and not inst.scope.path:
782
+ self.record["roundValues"] = [dict(row) for row in items]
783
+ inst.state, inst.started_at = "running", self._now_ms()
784
+ self._save(inst)
785
+ self._drive_loop(inst)
786
+ return True
787
+
788
+ def _round(self, inst, index):
789
+ config, state = inst.config, inst.loop
790
+ item = index if state.items is None else state.items[index]
791
+ over = config["over"]
792
+ row = item if over == "rows" else None
793
+ phone = item if over == "phones" else None
794
+ scope = Scope(inst.node["id"], inst.scope, inst, state.offset + index, item, row, phone)
795
+ self._open(scope)
796
+ return scope
797
+
798
+ def _drive_loop(self, inst):
799
+ state, config = inst.loop, inst.config
800
+ moved = False
801
+ for scope in list(state.open):
802
+ members = list(scope.members.values())
803
+ if any(member.state not in TERMINAL for member in members):
804
+ continue
805
+ state.open.remove(scope)
806
+ moved = True
807
+ failed = next((m for m in sorted(members, key=lambda m: m.finished_at or 0)
808
+ if m.state in ("blocked", "unknown", "cancelled") or m.state == "failed" and not m.handled),
809
+ None)
810
+ if failed is not None:
811
+ if state.failed is None:
812
+ state.failed = (failed.reason or "A step didn't finish.", failed.status or (
813
+ "error" if failed.state == "unknown" else "stopped" if failed.state == "cancelled" else None))
814
+ continue
815
+ collected = scope.members.get(config.get("collect")) if config.get("collect") else None
816
+ state.results.append(collected.ports.get("out") if collected is not None and collected.state == "done"
817
+ else None)
818
+ if config["over"] == "until" and not state.single:
819
+ watched = scope.members.get(config["until"])
820
+ seen = nodes.pointer(watched.output, config.get("path") or "/observed") \
821
+ if watched is not None and watched.state == "done" else None
822
+ if seen is True:
823
+ state.met = True
824
+ elif seen is not False:
825
+ question = (watched.config.get("question") if watched is not None else None) or "the check"
826
+ state.failed = (f"Mobster couldn't tell whether this is true: {question}", "condition_unknown")
827
+ stop_new = state.failed is not None or state.met or self.stop.is_set()
828
+ total = config["max"] if state.items is None else len(state.items)
829
+ while not stop_new and len(state.open) < state.concurrency and state.next < total:
830
+ try:
831
+ state.open.append(self._round(inst, state.next))
832
+ except _Limit as limit:
833
+ state.failed = (str(limit), "loop_limit")
834
+ break
835
+ state.next += 1
836
+ moved = True
837
+ if state.open:
838
+ return moved
839
+ if self.stop.is_set() and state.failed is None and not state.met and state.next < total:
840
+ self._end(inst, "cancelled", self.stop_reason())
841
+ elif state.failed is not None:
842
+ self._fail(inst, state.failed[0], state.failed[1])
843
+ elif config["over"] == "until" and not state.met and not state.single:
844
+ question = self.nodes.get(config["until"], {}).get("config", {}).get("question") or "the check"
845
+ self._fail(inst, f"The workflow ran {config['max']} times without seeing: {question}", "loop_limit")
846
+ else:
847
+ output = {"results": state.results, "count": len(state.results)}
848
+ self._end(inst, "done", output=output, ports={"out": output})
849
+ return True
850
+
851
+ @staticmethod
852
+ def _fill(text, values):
853
+ return text # a graph node's text was worked out before it ran
854
+
855
+ def public(self):
856
+ with self.cond:
857
+ return self.owner.batches.public(self.record)
858
+
859
+ def _send_to(self):
860
+ """The workflow's own destination (sendTo) gets the newest task's result when the run completes."""
861
+ destination = self.workflow.get("sendTo")
862
+ if not destination or self.last is None:
863
+ return
864
+ try:
865
+ from ..outlets.service import service_for
866
+ service = service_for(self.runtime)
867
+ if service is not None:
868
+ service.enqueue(self.last.id, destination)
869
+ except Exception: # noqa: BLE001 -- a result that can't be sent never changes how the run ended
870
+ log.exception("graph run %s: sendTo failed", self.record["id"])
871
+
872
+
873
+ class _Limit(Exception):
874
+ pass
875
+
876
+
877
+ def _texts(value):
878
+ if isinstance(value, str):
879
+ yield value
880
+ elif isinstance(value, dict):
881
+ for item in value.values():
882
+ yield from _texts(item)
883
+ elif isinstance(value, list):
884
+ for item in value:
885
+ yield from _texts(item)
886
+
887
+
888
+ def unsupported(graph):
889
+ """Why this Mobster can't run ``graph`` yet, or None: a node type it has no runner for."""
890
+ for node in graph.get("nodes") or []:
891
+ kind = node["type"]
892
+ if kind in TRIGGERS:
893
+ continue
894
+ if kind not in nodes.INLINE and kind not in nodes.WORKERS and kind != "loop":
895
+ return f"This Mobster can't run a {TYPES[kind].label} step yet. Update Mobster, then run it again."
896
+ return None
897
+
898
+
899
+ __all__ = ["GraphRun", "Instance", "Scope", "unsupported", "APIError"]