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,647 @@
1
+ """L0: trace analytics on runs we already have. $0, seconds, no device, no model.
2
+
3
+ python -m mobile_agent.bench.evals.l0 --runs DIR... [--batches aa1,ac1] [--app-groups groups.json] \\
4
+ [--labels labels.jsonl] --out OUT_DIR
5
+
6
+ Mines every recorded run (``traces.discover``) for four things and writes one ranked table:
7
+
8
+ 1. **Wasted actions** (per turn, so nothing is counted twice within a mechanism):
9
+ - ``round_trip``: a turn opened a screen and the next turn left it for the screen before, with nothing written down
10
+ on the way (no note, no checklist item found or done, no answer).
11
+ - ``repeat_action``: the same action on the same screen as earlier in the run, with nothing gained in between.
12
+ - ``no_change``: an action the driver ran that changed nothing (typing excepted).
13
+ - ``guard_refusal`` and ``loop_refusal``: the code refused the turn's action (commit guard; loop, repeat, idle).
14
+ - ``failed_action``: the driver raised.
15
+ - ``done_deferred``: a DONE the code sent back (proof gate, verify before done).
16
+ - ``chainable_turn``: a turn on the same screen as the turn before, whose target was already on that screen and
17
+ whose turn before ran cleanly: one more action the previous turn could have chained.
18
+ - ``done_only_final``: the run's last turn has only DONE: the fixed ~2 s finishing call.
19
+ 2. **Step inefficiency**: a passing run's turns and actions beyond the fewest any passing run of the same task took.
20
+ 3. **Time sinks** by step type (ground, type, scroll, switch, end) and by app group: model, generation, exec, settle,
21
+ refresh. Settle after the screen stopped moving comes from ``frameclock.jsonl`` where a run has it.
22
+ 4. **First divergence** of each failing run from the closest passing run of the same task: actions are aligned by
23
+ their longest common subsequence; the divergence is where the failing run leaves the passing path for good, and
24
+ its kind (stopped early, a different app or order, a different element, scrolled instead...).
25
+
26
+ The mechanisms overlap across rows (a refused repeat is both): each row is a ceiling on what removing that one
27
+ mechanism could save, not a share of a partition.
28
+ """
29
+
30
+ import argparse
31
+ import collections
32
+ import json
33
+ import pathlib
34
+ import statistics
35
+
36
+ from . import traces as T
37
+
38
+ # Which hypothesis (the tuning plan's ranked list) each mechanism would feed.
39
+ HYPOTHESES = {
40
+ "round_trip": "H2 stall recovery, H5 screen-diff history",
41
+ "repeat_action": "H2 stall recovery",
42
+ "loop_refusal": "H2 stall recovery",
43
+ "no_change": "H10 device time, grounding",
44
+ "guard_refusal": "commit guard (#224)",
45
+ "failed_action": "infra, H10 device time",
46
+ "done_deferred": "H3 completion check, contract quality",
47
+ "chainable_turn": "H7 token diet, H9 speculative call (chain more)",
48
+ "done_only_final": "H8 DONE folded into the last action",
49
+ "excess_turns": "H1 typed slots, H2, H5 (shorter paths)",
50
+ "settle_after_still": "H10 settle on frame stability",
51
+ "typing_exec": "H10 device time (typing)",
52
+ "source_reads": "H4 fewer device reads (overlaps settle, refresh, exec)",
53
+ "repeated_reads": "H4 fewer device reads",
54
+ "generation": "H7 token diet, H6 streaming dispatch",
55
+ "first_token": "H11 tail control, H6",
56
+ }
57
+ WASTE = ("round_trip", "repeat_action", "no_change", "guard_refusal", "loop_refusal", "failed_action",
58
+ "done_deferred", "chainable_turn", "done_only_final", "repeated_reads")
59
+ LOOP_LABELS = frozenset({"loop", "repeat", "idle_wait", "dismiss_exhausted"})
60
+ DEFER_STOPS = frozenset({"proof_gate", "verify_before_done", "done_not_arrived"})
61
+
62
+
63
+ def _labels_on(turn_text_rows):
64
+ return {T.norm(label) for _, _, label in turn_text_rows}
65
+
66
+
67
+ def wasted(run, screens=None):
68
+ """[(mechanism, step, turns, actions, seconds)] for one run. ``screens``: {step: set of labels on that screen}."""
69
+ out, turns = [], run.turns
70
+ screens = screens or {}
71
+ seen = {}
72
+ gained_since = 0
73
+ for i, t in enumerate(turns):
74
+ executed = [a for a in t.actions if a.executed]
75
+ model_s = ((t.model_ms or 0.0) + (t.prep_ms or 0.0)) / 1000
76
+ nothing_ran = not executed
77
+ guards = T.guard_refusals(t)
78
+ loops = [r for r in t.refused if r in LOOP_LABELS]
79
+ if guards:
80
+ out.append(("guard_refusal", t.step, 1 if nothing_ran else 0, len(guards), model_s if nothing_ran else 0.0))
81
+ if loops:
82
+ out.append(("loop_refusal", t.step, 1 if nothing_ran else 0, len(loops), model_s if nothing_ran else 0.0))
83
+ if t.failed:
84
+ out.append(("failed_action", t.step, 1 if nothing_ran else 0, len(t.failed),
85
+ (model_s if nothing_ran else 0.0)))
86
+ if set(t.stops) & DEFER_STOPS and t.done:
87
+ out.append(("done_deferred", t.step, 1, 0, t.seconds))
88
+ dead = [a for a in executed if a.changed is False and a.op not in T.TYPED_OPS
89
+ and a.op not in ("WAIT", "READ_LIST")
90
+ and a.op not in T.END_OPS]
91
+ if dead:
92
+ whole = len(dead) == len(executed) and not t.gained
93
+ out.append(("no_change", t.step, 1 if whole else 0, len(dead),
94
+ sum(a.device_ms for a in dead) / 1000 + (model_s if whole else 0.0)))
95
+ # the same action on the same screen as before, nothing gained in between
96
+ for a in executed:
97
+ key = (t.screen, a.token())
98
+ if t.screen and key in seen and seen[key] >= gained_since and not t.gained:
99
+ out.append(("repeat_action", t.step, 1, 1, t.seconds))
100
+ break
101
+ for a in executed:
102
+ seen.setdefault((t.screen, a.token()), i)
103
+ if t.gained:
104
+ gained_since = i
105
+ # A -> B -> A, nothing written down at B, B's way out a back control (or a tap on A's own title)
106
+ if 0 < i < len(turns) - 1:
107
+ a_turn, b_turn, c_turn = turns[i - 1], t, turns[i + 1]
108
+ if (a_turn.screen and b_turn.screen and c_turn.screen == a_turn.screen and b_turn.screen != a_turn.screen
109
+ and not b_turn.gained and b_turn.actions
110
+ and all(x.is_back or (a_turn.title and T.norm(x.label) == T.norm(a_turn.title))
111
+ or x.op in ("SWIPE_RIGHT", "HOME") for x in b_turn.actions)):
112
+ # the opening turn's model call counts only when it wrote nothing down about its own screen
113
+ a_cost = a_turn.device_ms / 1000 if a_turn.gained else a_turn.seconds
114
+ out.append(("round_trip", b_turn.step, 1 + (not a_turn.gained),
115
+ len(a_turn.actions) + len(b_turn.actions), a_cost + b_turn.seconds))
116
+ # one more action the previous turn could have chained
117
+ if i > 0 and not t.done:
118
+ prev = turns[i - 1]
119
+ labels = screens.get(prev.step)
120
+ if (prev.screen and t.screen == prev.screen and prev.actions and not prev.refused and not prev.failed
121
+ and all(x.changed is not False or x.op in T.TYPED_OPS for x in prev.actions)
122
+ and labels is not None and executed and all(T.norm(x.label) in labels for x in executed
123
+ if x.op in ("TAP", "LONG_PRESS")
124
+ or x.op in T.TYPED_OPS)
125
+ and any(x.op in ("TAP", "LONG_PRESS") or x.op in T.TYPED_OPS for x in executed)):
126
+ out.append(("chainable_turn", t.step, 1, 0, model_s))
127
+ if turns and turns[-1].done_only and run.passed:
128
+ last = turns[-1]
129
+ out.append(("done_only_final", last.step, 1, 0, ((last.model_ms or 0.0) + (last.prep_ms or 0.0)) / 1000))
130
+ return out
131
+
132
+
133
+ def screen_labels(run):
134
+ """{step: labels on the screen the model saw} from the prompts (the loader keeps only the hash)."""
135
+ out = {}
136
+ for p in T.read_jsonl(pathlib.Path(run.folder) / "prompts.jsonl"):
137
+ out[p.get("step")] = _labels_on(T.screen_rows(p.get("text") or ""))
138
+ return out
139
+
140
+
141
+ def settle_after_still(run):
142
+ """(seconds, actions) of settle wait after the screen's last motion, from the run's frameclock.jsonl."""
143
+ total, n = 0.0, 0
144
+ for row in T.read_jsonl(pathlib.Path(run.folder) / "frameclock.jsonl"):
145
+ if row.get("kind") != "settle":
146
+ continue
147
+ settle, motion = row.get("settle_ms"), row.get("motion_after_dispatch_ms")
148
+ if isinstance(settle, (int, float)) and isinstance(motion, (int, float)):
149
+ total += max(0.0, settle - max(motion, 0.0))
150
+ n += 1
151
+ return total / 1000, n
152
+
153
+
154
+ def shortest(runs):
155
+ """{(suite, task): (turns, actions)}: the fewest of any passing run."""
156
+ best = {}
157
+ for r in runs:
158
+ if r.passed and r.turns:
159
+ key = (r.suite, r.task)
160
+ cand = (len(r.turns), len(r.actions))
161
+ if key not in best:
162
+ best[key] = cand
163
+ else:
164
+ best[key] = (min(best[key][0], cand[0]), min(best[key][1], cand[1]))
165
+ return best
166
+
167
+
168
+ def excess(run, best):
169
+ b = best.get((run.suite, run.task))
170
+ if not b or not run.passed:
171
+ return None
172
+ turns, actions = len(run.turns) - b[0], len(run.actions) - b[1]
173
+ per_turn = statistics.mean([t.seconds for t in run.turns if t.timed] or [0.0])
174
+ return {"turns": turns, "actions": actions, "seconds": max(turns, 0) * per_turn, "best_turns": b[0]}
175
+
176
+
177
+ def lcs_pairs(a, b):
178
+ """Index pairs of a longest common subsequence of ``a`` and ``b``."""
179
+ n, m = len(a), len(b)
180
+ dp = [[0] * (m + 1) for _ in range(n + 1)]
181
+ for i in range(n - 1, -1, -1):
182
+ for j in range(m - 1, -1, -1):
183
+ dp[i][j] = dp[i + 1][j + 1] + 1 if a[i] == b[j] else max(dp[i + 1][j], dp[i][j + 1])
184
+ pairs, i, j = [], 0, 0
185
+ while i < n and j < m:
186
+ if a[i] == b[j]:
187
+ pairs.append((i, j))
188
+ i, j = i + 1, j + 1
189
+ elif dp[i + 1][j] >= dp[i][j + 1]:
190
+ i += 1
191
+ else:
192
+ j += 1
193
+ return pairs
194
+
195
+
196
+ def _steps(run):
197
+ """[(turn, action)] in order, ending actions excluded."""
198
+ return [(t, a) for t in run.turns for a in (t.actions or t.planned) if a.op not in T.END_OPS]
199
+
200
+
201
+ OP_CLASS = {"TYPE": "TYPE", "TYPE_SUBMIT": "TYPE", "SET_TEXT": "TYPE", "TAP": "TAP", "LONG_PRESS": "TAP",
202
+ "LAUNCH_APP": "OPEN", "OPEN_URL": "OPEN", "SUBMIT": "SUBMIT", "READ_NOTIFICATIONS": "READ_NOTIFICATIONS"}
203
+
204
+
205
+ def milestone(action):
206
+ """The action as a milestone, or None for moves that carry no decision (scrolls, dismissals, home, waits)."""
207
+ kind = OP_CLASS.get(action.op)
208
+ if kind is None:
209
+ return None
210
+ app, op, label, text = action.token()
211
+ if kind == "OPEN":
212
+ return ("", "OPEN", label, "")
213
+ return (app, kind, label, text)
214
+
215
+
216
+ def milestones(passing, closest):
217
+ """The milestones every passing run reached, in the order ``closest`` reached them."""
218
+ sets = [{milestone(a) for _, a in _steps(r)} - {None} for r in passing]
219
+ common = set.intersection(*sets) if sets else set()
220
+ order = []
221
+ for _, a in _steps(closest):
222
+ m = milestone(a)
223
+ if m in common and m not in order:
224
+ order.append(m)
225
+ return order
226
+
227
+
228
+ def divergence(fail, passing):
229
+ """Where ``fail`` first leaves the passing path: the first milestone (an action every passing run of the task
230
+ took) it never reached, and what it did instead at that point. ``passing``: the task's passing runs."""
231
+ f = _steps(fail)
232
+ f_ms = [milestone(a) for _, a in f]
233
+ closest = max(passing, key=lambda p: (len(lcs_pairs([m for m in f_ms if m], [m for m in (milestone(a) for _, a in
234
+ _steps(p)) if m])),
235
+ p.rubric or 0, -len(p.actions)))
236
+ order = milestones(passing, closest)
237
+ reached = set(f_ms)
238
+ missed = next((m for m in order if m not in reached), None)
239
+ p_steps = _steps(closest)
240
+ ending = fail.turns[-1] if fail.turns else None
241
+ turn = f_action = p_turn = p_action = None
242
+ if missed is None:
243
+ i0, kind = len(f), "reached_every_milestone" # the miss is in the answer, or in an action it added
244
+ turn = ending
245
+ else:
246
+ before = order[:order.index(missed)]
247
+ hits = [i for i, m in enumerate(f_ms) if m in before]
248
+ i0 = hits[-1] + 1 if hits else 0
249
+ p_turn, p_action = next(((t, a) for t, a in p_steps if milestone(a) == missed), (None, None))
250
+ # skip the failing run's moves that carry no decision: the divergence is its next real choice
251
+ while i0 < len(f) and f_ms[i0] is None:
252
+ i0 += 1
253
+ if i0 >= len(f):
254
+ turn = ending
255
+ if ending is not None and any(a.op == "BLOCKED" for a in ending.planned):
256
+ kind = "gave_up"
257
+ elif ending is not None and ending.done:
258
+ kind = "stopped_early_done"
259
+ else:
260
+ kind = "stopped_early_no_done"
261
+ else:
262
+ turn, f_action = f[i0]
263
+ fm = f_ms[i0]
264
+ if fm[1] == "OPEN" or missed[1] == "OPEN" or T.app_key(f_action.app) != T.app_key(p_action.app):
265
+ kind = "different_app_or_order"
266
+ elif fm[1] == "TYPE" and missed[1] == "TYPE" and fm[2] == missed[2]:
267
+ kind = "different_text"
268
+ elif fm[1] == missed[1]:
269
+ kind = "different_element"
270
+ else:
271
+ kind = "different_operation"
272
+ if p_turn is not None and turn.screen and turn.screen == p_turn.screen:
273
+ kind += "@same_screen"
274
+ return {"run": fail.id, "ref": closest.id, "task": fail.task, "suite": fail.suite, "category": fail.category,
275
+ "kind": kind, "index": i0, "of": len(f), "milestones": len(order),
276
+ "reached": sum(m in reached for m in order),
277
+ "position": round(i0 / len(f), 3) if f else None,
278
+ "step": turn.step if turn else None, "ref_step": p_turn.step if p_turn else None,
279
+ "app": turn.app if turn else None, "screen": turn.title if turn else None,
280
+ "same_screen": bool(p_turn is not None and turn is not None and turn.screen
281
+ and turn.screen == p_turn.screen),
282
+ "fail_action": [f_action.op, f_action.label[:80], f_action.app] if f_action else None,
283
+ "ref_action": [p_action.op, p_action.label[:80], p_action.app] if p_action else None}
284
+
285
+
286
+ def divergences(runs, only_clean=True):
287
+ """One divergence per failing judged run that has a passing run of the same task to compare with."""
288
+ passing = collections.defaultdict(list)
289
+ for r in runs:
290
+ if r.passed and r.turns:
291
+ passing[(r.suite, r.task)].append(r)
292
+ out = []
293
+ for r in runs:
294
+ if not r.judged or r.passed or not r.turns or (only_clean and not r.clean):
295
+ continue
296
+ refs = passing.get((r.suite, r.task))
297
+ if refs:
298
+ out.append(divergence(r, refs))
299
+ return out
300
+
301
+
302
+ def time_sinks(runs, groups=None):
303
+ """{"by_step_type": {...}, "by_app_group": {...}}: seconds by bucket, over timed turns."""
304
+ groups = {T.app_key(k): v for k, v in (groups or {}).items()}
305
+ by_kind, by_group = collections.defaultdict(collections.Counter), collections.defaultdict(collections.Counter)
306
+ for r in runs:
307
+ for t in r.turns:
308
+ if not t.timed:
309
+ continue
310
+ group = groups.get(T.app_key(t.app), "other") if t.app else "none"
311
+ for table, key in ((by_kind, t.kind), (by_group, group)):
312
+ c = table[key]
313
+ c["turns"] += 1
314
+ c["model_s"] += (t.model_ms or 0.0) / 1000
315
+ c["prep_s"] += (t.prep_ms or 0.0) / 1000
316
+ if t.ttft_ms is not None:
317
+ c["ttft_turns"] += 1
318
+ c["ttft_s"] += t.ttft_ms / 1000
319
+ c["gen_s"] += (t.gen_ms or 0.0) / 1000
320
+ if t.out_tokens:
321
+ c["out_tokens"] += t.out_tokens
322
+ for a in t.actions:
323
+ c["actions"] += 1
324
+ c["exec_s"] += (a.exec_ms or 0.0) / 1000
325
+ c["settle_s"] += (a.settle_ms or 0.0) / 1000
326
+ c["refresh_s"] += (a.refresh_ms or 0.0) / 1000
327
+ def finish(table):
328
+ out = {}
329
+ for key, c in table.items():
330
+ total = c["model_s"] + c["prep_s"] + c["exec_s"] + c["settle_s"] + c["refresh_s"]
331
+ out[key] = {"turns": c["turns"], "actions": c["actions"], "total_s": round(total, 1),
332
+ "s_per_turn": round(total / c["turns"], 2) if c["turns"] else None,
333
+ "model_s_per_turn": round(c["model_s"] / c["turns"], 2) if c["turns"] else None,
334
+ "ttft_s_per_turn": round(c["ttft_s"] / c["ttft_turns"], 2) if c["ttft_turns"] else None,
335
+ "gen_s_per_turn": round(c["gen_s"] / c["ttft_turns"], 2) if c["ttft_turns"] else None,
336
+ "out_tokens_per_turn": round(c["out_tokens"] / c["turns"], 1) if c["turns"] else None,
337
+ "exec_s_per_action": round(c["exec_s"] / c["actions"], 2) if c["actions"] else None,
338
+ "settle_s_per_action": round(c["settle_s"] / c["actions"], 2) if c["actions"] else None,
339
+ "refresh_s_per_action": round(c["refresh_s"] / c["actions"], 2) if c["actions"] else None,
340
+ "share": None, **{k: round(c[k], 1) for k in ("model_s", "ttft_s", "gen_s", "exec_s",
341
+ "settle_s", "refresh_s")}}
342
+ grand = sum(v["total_s"] for v in out.values()) or 1.0
343
+ for v in out.values():
344
+ v["share"] = round(v["total_s"] / grand, 3)
345
+ return dict(sorted(out.items(), key=lambda kv: -kv[1]["total_s"]))
346
+ return {"by_step_type": finish(by_kind), "by_app_group": finish(by_group)}
347
+
348
+
349
+ def partition(runs):
350
+ """Where the timed seconds go, without overlap: model (first token, generation, or whole where the run has no
351
+ stream timing), prep, and per action kind exec, settle and refresh."""
352
+ c = collections.Counter()
353
+ for r in runs:
354
+ for t in r.turns:
355
+ if not t.timed:
356
+ continue
357
+ if t.ttft_ms is not None:
358
+ c["model: first token"] += t.ttft_ms / 1000
359
+ c["model: generation"] += (t.gen_ms or 0.0) / 1000
360
+ c["model: rest of call"] += max(0.0, (t.model_ms or 0.0) - t.ttft_ms - (t.gen_ms or 0.0)) / 1000
361
+ else:
362
+ c["model: whole call"] += (t.model_ms or 0.0) / 1000
363
+ c["prep"] += (t.prep_ms or 0.0) / 1000
364
+ for a in t.actions:
365
+ kind = T.step_type(a.op)
366
+ c[f"exec: {kind}"] += (a.exec_ms or 0.0) / 1000
367
+ c["settle"] += (a.settle_ms or 0.0) / 1000
368
+ c["refresh"] += (a.refresh_ms or 0.0) / 1000
369
+ total = sum(c.values()) or 1.0
370
+ return [{"bucket": k, "seconds": round(v, 1), "share": round(v / total, 3)} for k, v in c.most_common() if v > 0]
371
+
372
+
373
+ def _fit(xs, ys):
374
+ """(intercept, slope, r) of a least-squares line, or Nones."""
375
+ if len(xs) < 3 or len(set(xs)) < 2:
376
+ return None, None, None
377
+ mx, my = statistics.mean(xs), statistics.mean(ys)
378
+ sxx = sum((x - mx) ** 2 for x in xs)
379
+ slope = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) / sxx
380
+ r = statistics.correlation(xs, ys) if len(set(ys)) > 1 else None
381
+ return my - slope * mx, slope, r
382
+
383
+
384
+ def ttft_table(runs, chars_per_token=3.0):
385
+ """Where time to first token goes, from turns with streamed timing: cache hits, the fit on uncached tokens, and
386
+ the turns whose output tokens exceed what their visible reply needs (hidden thinking, estimated at
387
+ ``chars_per_token``)."""
388
+ turns = [t for r in runs for t in r.turns if t.ttft_ms is not None and t.in_tokens]
389
+ if not turns:
390
+ return None
391
+ uncached = [t.in_tokens - (t.cached_tokens or 0) - (t.write_tokens or 0) for t in turns]
392
+ ttft = [t.ttft_ms for t in turns]
393
+ hidden = [(t.out_tokens or 0) - (t.out_chars or 0) / chars_per_token if t.out_chars else None for t in turns]
394
+ a, b, r = _fit(uncached, ttft)
395
+ plain = [(u, y) for u, y, h in zip(uncached, ttft, hidden) if h is not None and h < 20]
396
+ a2, b2, r2 = _fit([u for u, _ in plain], [y for _, y in plain])
397
+ bands = []
398
+ for lo, hi in ((-1e9, 20), (20, 120), (120, 250), (250, 1e9)):
399
+ sel = [y for y, h in zip(ttft, hidden) if h is not None and lo <= h < hi]
400
+ if sel:
401
+ bands.append({"hidden_tokens": [None if lo < -1e8 else lo, None if hi > 1e8 else hi], "turns": len(sel),
402
+ "ttft_p50_ms": round(statistics.median(sel)), "ttft_mean_ms": round(statistics.mean(sel))})
403
+ by_step = []
404
+ for lo, hi in ((0, 1), (1, 10), (10, 20), (20, 35), (35, 10**6)):
405
+ sel = [t.ttft_ms for t in turns if t.step is not None and lo <= t.step < hi]
406
+ if sel:
407
+ by_step.append({"turns_from": lo, "turns_to": hi, "n": len(sel),
408
+ "ttft_p50_ms": round(statistics.median(sel))})
409
+ p75 = statistics.quantiles(ttft, n=4)[2]
410
+ return {"turns": len(turns), "cache_missed_turns": sum(not (t.cached_tokens or 0) for t in turns),
411
+ "cache_write_turns": sum(bool(t.write_tokens) for t in turns),
412
+ "cached_share_mean": round(statistics.mean((t.cached_tokens or 0) / t.in_tokens for t in turns), 3),
413
+ "uncached_p50": statistics.median(uncached), "ttft_p50_ms": statistics.median(ttft),
414
+ "ttft_p90_ms": statistics.quantiles(ttft, n=10)[-1], "ttft_mean_ms": round(statistics.mean(ttft)),
415
+ "fit_ms_per_1k_uncached": round(b * 1000, 1) if b is not None else None,
416
+ "fit_intercept_ms": round(a) if a is not None else None, "fit_r": round(r, 3) if r is not None else None,
417
+ "fit_no_thinking_ms_per_1k": round(b2 * 1000, 1) if b2 is not None else None,
418
+ "fit_no_thinking_r": round(r2, 3) if r2 is not None else None,
419
+ "ms_above_p75_per_turn": round(sum(max(0.0, y - p75) for y in ttft) / len(ttft)),
420
+ "by_hidden_thinking": bands, "by_turn_index": by_step}
421
+
422
+
423
+ def mechanism_table(runs, use_screens=True):
424
+ """Rows {mechanism, runs, occurrences, turns, actions, seconds, s_per_run, share_time, share_turns}, ranked."""
425
+ rows = collections.defaultdict(lambda: {"runs": set(), "occurrences": 0, "turns": 0, "actions": 0, "seconds": 0.0})
426
+ best = shortest(runs)
427
+ total_s = total_turns = 0.0
428
+ timed = [r for r in runs if r.timed]
429
+ for r in timed:
430
+ total_s += sum(t.seconds for t in r.turns)
431
+ total_turns += len(r.turns)
432
+ screens = screen_labels(r) if use_screens else None
433
+ for mech, _, turns, actions, seconds in wasted(r, screens):
434
+ row = rows[mech]
435
+ row["runs"].add(r.id)
436
+ row["occurrences"] += 1
437
+ row["turns"] += turns
438
+ row["actions"] += actions
439
+ row["seconds"] += seconds
440
+ ex = excess(r, best)
441
+ if ex and ex["turns"] > 0:
442
+ row = rows["excess_turns"]
443
+ row["runs"].add(r.id)
444
+ row["occurrences"] += 1
445
+ row["turns"] += ex["turns"]
446
+ row["actions"] += max(ex["actions"], 0)
447
+ row["seconds"] += ex["seconds"]
448
+ settle_s, n = settle_after_still(r)
449
+ if n:
450
+ row = rows["settle_after_still"]
451
+ row["runs"].add(r.id)
452
+ row["occurrences"] += n
453
+ row["seconds"] += settle_s
454
+ typing = [a for t in r.turns for a in t.actions if a.op in T.TYPED_OPS]
455
+ if typing:
456
+ row = rows["typing_exec"]
457
+ row["runs"].add(r.id)
458
+ row["occurrences"] += len(typing)
459
+ row["actions"] += len(typing)
460
+ row["seconds"] += sum((a.exec_ms or 0.0) for a in typing) / 1000
461
+ src, reads = (r.timing or {}).get("source_seconds"), int((r.timing or {}).get("source_reads") or 0)
462
+ if src:
463
+ row = rows["source_reads"]
464
+ row["runs"].add(r.id)
465
+ row["occurrences"] += reads
466
+ row["seconds"] += float(src)
467
+ extra = reads - len(r.turns) # one read per turn is the observation; the rest re-read the screen
468
+ if extra > 0:
469
+ row = rows["repeated_reads"]
470
+ row["runs"].add(r.id)
471
+ row["occurrences"] += extra
472
+ row["seconds"] += float(src) * extra / reads
473
+ gen = [t.gen_ms for t in r.turns if t.gen_ms is not None]
474
+ ttft = [t.ttft_ms for t in r.turns if t.ttft_ms is not None]
475
+ if gen:
476
+ row = rows["generation"]
477
+ row["runs"].add(r.id)
478
+ row["occurrences"] += len(gen)
479
+ row["seconds"] += sum(gen) / 1000
480
+ if ttft:
481
+ row = rows["first_token"]
482
+ row["runs"].add(r.id)
483
+ row["occurrences"] += len(ttft)
484
+ row["seconds"] += sum(ttft) / 1000
485
+ out = []
486
+ for mech, row in rows.items():
487
+ out.append({"mechanism": mech, "kind": "waste" if mech in WASTE or mech == "excess_turns" else "sink",
488
+ "runs": len(row["runs"]), "occurrences": row["occurrences"], "turns": row["turns"],
489
+ "actions": row["actions"], "seconds": round(row["seconds"], 1),
490
+ "s_per_run": round(row["seconds"] / len(timed), 2) if timed else None,
491
+ "share_time": round(row["seconds"] / total_s, 3) if total_s else None,
492
+ "share_turns": round(row["turns"] / total_turns, 3) if total_turns else None,
493
+ "feeds": HYPOTHESES.get(mech, "")})
494
+ out.sort(key=lambda r: -r["seconds"])
495
+ return out, {"runs": len(timed), "turns": int(total_turns), "seconds": round(total_s, 1)}
496
+
497
+
498
+ def outcome_table(runs, labels=None):
499
+ """Failing runs: their seconds and turns by labelled cause (when labels are given) and by divergence kind."""
500
+ by_cause = collections.defaultdict(lambda: {"runs": 0, "turns": 0, "seconds": 0.0})
501
+ index = {pathlib.Path(r.folder).resolve(): r for r in runs}
502
+ for lab in labels or ():
503
+ folder = lab.get("folder")
504
+ run = None
505
+ for cand in (pathlib.Path(folder) if folder else None,):
506
+ if cand is not None:
507
+ run = index.get(cand.resolve()) if cand.is_absolute() else None
508
+ if run is None:
509
+ run = next((r for r in runs if r.task == lab.get("task") and r.arm == lab.get("arm")
510
+ and r.repeat == lab.get("run") and r.batch == lab.get("batch", r.batch)), None)
511
+ row = by_cause[lab.get("cause")]
512
+ row["runs"] += 1
513
+ if run is not None:
514
+ row["turns"] += len(run.turns)
515
+ row["seconds"] += sum(t.seconds for t in run.turns)
516
+ return {k: {**v, "seconds": round(v["seconds"], 1)} for k,
517
+ v in sorted(by_cause.items(), key=lambda kv: -kv[1]["runs"])}
518
+
519
+
520
+ def _md_table(rows, cols):
521
+ head = "| " + " | ".join(c for c, _ in cols) + " |\n|" + "---|" * len(cols) + "\n"
522
+ return head + "".join("| " + " | ".join(fmt(r) for _, fmt in cols) + " |\n" for r in rows)
523
+
524
+
525
+ def report(runs, groups=None, labels=None, title="L0 trace analytics"):
526
+ table, totals = mechanism_table(runs)
527
+ sinks = time_sinks(runs, groups)
528
+ divs = divergences(runs)
529
+ kinds = collections.Counter(d["kind"] for d in divs)
530
+ positions = [d["position"] for d in divs if d["position"] is not None]
531
+ judged = [r for r in runs if r.judged]
532
+ best = shortest(runs)
533
+ ex = [excess(r, best) for r in runs if r.passed]
534
+ ex = [e for e in ex if e]
535
+ lines = [f"# {title}", "",
536
+ f"Runs: {len(runs)} ({len(judged)} judged, {sum(bool(r.passed) for r in judged)} passing); "
537
+ f"timed: {totals['runs']} runs, {totals['turns']} turns, {totals['seconds']:.0f} s "
538
+ "of model and device time.",
539
+ "",
540
+ "## Mechanisms ranked by seconds", "",
541
+ "Each row is a ceiling on what removing that one mechanism could save; rows overlap.", ""]
542
+ lines.append(_md_table(table, [
543
+ ("mechanism", lambda r: r["mechanism"]), ("kind", lambda r: r["kind"]), ("runs", lambda r: str(r["runs"])),
544
+ ("occurrences", lambda r: str(r["occurrences"])), ("turns", lambda r: str(r["turns"])),
545
+ ("seconds", lambda r: f"{r['seconds']:.0f}"), ("s/run", lambda r: f"{r['s_per_run']:.1f}"),
546
+ ("% time", lambda r: f"{100 * (r['share_time'] or 0):.1f}"),
547
+ ("% turns", lambda r: f"{100 * (r['share_turns'] or 0):.1f}"), ("feeds", lambda r: r["feeds"])]))
548
+ lines += ["", "## Where the seconds go (no overlap)", ""]
549
+ lines.append(_md_table(partition([r for r in runs if r.timed]), [
550
+ ("bucket", lambda r: r["bucket"]), ("seconds", lambda r: f"{r['seconds']:.0f}"),
551
+ ("share", lambda r: f"{100 * r['share']:.1f}%")]))
552
+ lines += ["", "## Step inefficiency (passing runs against the fewest turns of any passing run of the task)", ""]
553
+ if ex:
554
+ lines.append(f"Passing runs: {len(ex)}; at the shortest: {sum(e['turns'] <= 0 for e in ex)}; "
555
+ f"excess turns mean {statistics.mean(max(e['turns'], 0) for e in ex):.1f}, "
556
+ f"p50 {statistics.median(max(e['turns'], 0) for e in ex):.0f}; "
557
+ f"excess seconds {sum(e['seconds'] for e in ex):.0f} s in all.")
558
+ lines += ["", "## Time sinks by step type", ""]
559
+ cols = [("", lambda kv: kv[0]), ("turns", lambda kv: str(kv[1]["turns"])),
560
+ ("total s", lambda kv: f"{kv[1]['total_s']:.0f}"),
561
+ ("share", lambda kv: f"{100 * kv[1]['share']:.0f}%"), ("s/turn", lambda kv: str(kv[1]["s_per_turn"])),
562
+ ("model s/turn", lambda kv: str(kv[1]["model_s_per_turn"])),
563
+ ("ttft s", lambda kv: str(kv[1]["ttft_s_per_turn"])), ("gen s", lambda kv: str(kv[1]["gen_s_per_turn"])),
564
+ ("out tok", lambda kv: str(kv[1]["out_tokens_per_turn"])),
565
+ ("exec s/act", lambda kv: str(kv[1]["exec_s_per_action"])),
566
+ ("settle s/act", lambda kv: str(kv[1]["settle_s_per_action"])),
567
+ ("refresh s/act", lambda kv: str(kv[1]["refresh_s_per_action"]))]
568
+ lines.append(_md_table(list(sinks["by_step_type"].items()), cols))
569
+ lines += ["", "## Time sinks by app group", ""]
570
+ lines.append(_md_table(list(sinks["by_app_group"].items()), cols))
571
+ lines += ["", f"## First divergence of failing runs ({len(divs)} clean failing runs with a passing run to compare)",
572
+ ""]
573
+ if divs:
574
+ lines.append(_md_table(kinds.most_common(), [("kind", lambda kv: kv[0]), ("runs", lambda kv: str(kv[1])),
575
+ ("share", lambda kv: f"{100 * kv[1] / len(divs):.0f}%")]))
576
+ lines.append(f"\nWhere in the run (share of its actions done before leaving the path): "
577
+ f"p25 {statistics.quantiles(positions, n=4)[0]:.2f}, median {statistics.median(positions):.2f}, "
578
+ f"p75 {statistics.quantiles(positions, n=4)[2]:.2f}." if len(positions) >= 4 else "")
579
+ ttft = ttft_table(runs)
580
+ if ttft:
581
+ lines += ["", "## Time to first token", "",
582
+ f"{ttft['turns']} turns with streamed timing; {ttft['cache_missed_turns']} missed the cache; "
583
+ f"cached share of input {100 * ttft['cached_share_mean']:.0f}%. TTFT p50 {ttft['ttft_p50_ms']:.0f} "
584
+ f"ms, p90 {ttft['ttft_p90_ms']:.0f} ms, mean {ttft['ttft_mean_ms']} ms. Fit on uncached tokens: "
585
+ f"{ttft['fit_intercept_ms']} ms + {ttft['fit_ms_per_1k_uncached']} ms per 1k (r {ttft['fit_r']}); on "
586
+ f"turns without hidden thinking {ttft['fit_no_thinking_ms_per_1k']} ms per 1k (r "
587
+ f"{ttft['fit_no_thinking_r']}).", ""]
588
+ lines.append(_md_table(ttft["by_hidden_thinking"], [
589
+ ("hidden thinking tokens (est.)", lambda b: f"{b['hidden_tokens'][0]}..{b['hidden_tokens'][1]}"),
590
+ ("turns", lambda b: str(b["turns"])), ("TTFT p50 ms", lambda b: str(b["ttft_p50_ms"])),
591
+ ("TTFT mean ms", lambda b: str(b["ttft_mean_ms"]))]))
592
+ if labels:
593
+ lines += ["", "## Failing runs by labelled cause", ""]
594
+ causes = outcome_table(runs, labels)
595
+ lines.append(_md_table(list(causes.items()),
596
+ [("cause", lambda kv: str(kv[0])), ("runs", lambda kv: str(kv[1]["runs"])),
597
+ ("turns", lambda kv: str(kv[1]["turns"])),
598
+ ("seconds", lambda kv: f"{kv[1]['seconds']:.0f}")]))
599
+ return "\n".join(lines) + "\n", {"mechanisms": table, "totals": totals, "time_sinks": sinks,
600
+ "partition": partition([r for r in runs if r.timed]), "ttft": ttft,
601
+ "divergences": divs, "divergence_kinds": dict(kinds),
602
+ "excess": {"passing_runs": len(ex),
603
+ "at_shortest": sum(e["turns"] <= 0 for e in ex),
604
+ "excess_seconds": round(sum(e["seconds"] for e in ex), 1)}}
605
+
606
+
607
+ def select(runs, batches=None, suites=None, clean=None, models=None):
608
+ out = []
609
+ for r in runs:
610
+ if batches and r.batch not in batches:
611
+ continue
612
+ if suites and r.suite not in suites:
613
+ continue
614
+ if clean is not None and r.clean != clean:
615
+ continue
616
+ if models and r.model not in models:
617
+ continue
618
+ out.append(r)
619
+ return out
620
+
621
+
622
+ def main(argv=None):
623
+ parser = argparse.ArgumentParser(prog="python -m mobile_agent.bench.evals.l0", description=__doc__.split("\n\n")[0])
624
+ parser.add_argument("--runs", nargs="+", required=True, help="batch folders, or folders holding them")
625
+ parser.add_argument("--batches", help="comma-separated batch names to keep (default: all)")
626
+ parser.add_argument("--suite", choices=("iosworld", "mobsterbench"), help="keep one suite")
627
+ parser.add_argument("--app-groups", help="JSON {app name: group} for the by-app table")
628
+ parser.add_argument("--labels", help="failure labels JSONL (the tuning loop's labeller) for the cause table")
629
+ parser.add_argument("--title", default="L0 trace analytics")
630
+ parser.add_argument("--out", required=True, help="folder for l0.md and l0.json")
631
+ args = parser.parse_args(argv)
632
+ runs = T.discover(args.runs)
633
+ runs = select(runs, batches=set(args.batches.split(",")) if args.batches else None,
634
+ suites={args.suite} if args.suite else None)
635
+ groups = json.loads(pathlib.Path(args.app_groups).read_text()) if args.app_groups else None
636
+ labels = T.read_jsonl(args.labels) if args.labels else None
637
+ text, data = report(runs, groups, labels, args.title)
638
+ out = pathlib.Path(args.out)
639
+ out.mkdir(parents=True, exist_ok=True)
640
+ (out / "l0.md").write_text(text)
641
+ (out / "l0.json").write_text(json.dumps(data, indent=1, ensure_ascii=False))
642
+ print(text)
643
+ return 0
644
+
645
+
646
+ if __name__ == "__main__":
647
+ raise SystemExit(main())