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.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
"""iOSWorld, run by Mobster: the published iOS agent benchmark, on its own terms.
|
|
2
|
+
|
|
3
|
+
iOSWorld (arXiv 2606.09764; github.com/ljang0/iOSWorld, CC BY 4.0): 133 tasks over 26
|
|
4
|
+
SwiftUI apps on an iPhone 17 Pro simulator, scored by its own LLM judge (gpt-5.4-mini by
|
|
5
|
+
default) from each run's trajectory -- per-step screenshots, actions, final answer --
|
|
6
|
+
against the task's rubric. Published best: 51.9% (Claude Opus 4.6 + XCUITest tree).
|
|
7
|
+
|
|
8
|
+
What is iOSWorld's and unchanged: the tasks (goals verbatim), the start state (its own
|
|
9
|
+
``reset_app_data`` + ``reseed_apps``, then Home, as its runner does), the 50-step limit,
|
|
10
|
+
the trajectory format and the judge (``scripts/judge_trajectories.py``, run as is).
|
|
11
|
+
|
|
12
|
+
What is Mobster's: its agent (mobile_agent.frontier.FrontierAgent, composed as the app's
|
|
13
|
+
Smart engine with FrameClock), driving the simulator through WDA, capped per task by
|
|
14
|
+
``task_cost_cap``. The task's app list becomes the apps the agent may open -- iOSWorld hands
|
|
15
|
+
its own agent the same list -- and the first one is opened, recorded as a ``launch_app`` step
|
|
16
|
+
as iOSWorld's agent would take it. ``--policy`` takes one value, ``frontier``, the default.
|
|
17
|
+
|
|
18
|
+
Screenshots exist only for the judge: taken with ``simctl io`` (never through WDA, never
|
|
19
|
+
shown to Mobster) before each action and once at the end; their time is excluded from
|
|
20
|
+
Mobster's reported latency. The simulator runs headless: nothing here opens a window.
|
|
21
|
+
|
|
22
|
+
python -m mobile_agent.bench.iosworld run --repo ../iOSWorld --udid <UDID> \\
|
|
23
|
+
--wda-url http://127.0.0.1:8200 --mjpeg-url http://127.0.0.1:9200 \\
|
|
24
|
+
--out research/iosworld-runs/NAME [--only caltrack-001,...] [--limit N] --env-file <agent.env> \\
|
|
25
|
+
[--policy frontier --model gpt-5.6-luna --reasoning low]
|
|
26
|
+
python -m mobile_agent.bench.iosworld pool --repo ../iOSWorld --sims UDID:WDA_PORT:MJPEG_PORT,... \\
|
|
27
|
+
--out research/iosworld-runs/NAME [--only ...] [--limit N] [--policy frontier ...] --env-file <agent.env>
|
|
28
|
+
python -m mobile_agent.bench.iosworld judge --repo ../iOSWorld --run research/iosworld-runs/NAME
|
|
29
|
+
python -m mobile_agent.bench.iosworld report --run research/iosworld-runs/NAME
|
|
30
|
+
python -m mobile_agent.bench.iosworld compare --a research/iosworld-runs/OLD-* --b research/iosworld-runs/NEW-*
|
|
31
|
+
python -m mobile_agent.bench.iosworld estimate research/iosworld-runs/* --model gpt-5.6-sol
|
|
32
|
+
|
|
33
|
+
A task passes by iOSWorld's own rule: every rubric criterion satisfied (judge_trajectories.py
|
|
34
|
+
``_aggregate``), not the judge's ``success`` flag; see bench/iosworld_stats.py.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import argparse
|
|
38
|
+
import base64
|
|
39
|
+
import dataclasses
|
|
40
|
+
import glob
|
|
41
|
+
import json
|
|
42
|
+
import os
|
|
43
|
+
import pathlib
|
|
44
|
+
import plistlib
|
|
45
|
+
import re
|
|
46
|
+
import statistics
|
|
47
|
+
import subprocess
|
|
48
|
+
import sys
|
|
49
|
+
import time
|
|
50
|
+
import urllib.request
|
|
51
|
+
|
|
52
|
+
from ..engines import SMART_CONFIG, frontier_kwargs
|
|
53
|
+
from ..sim import simctl
|
|
54
|
+
from . import tape
|
|
55
|
+
|
|
56
|
+
# The frontier policy is the app's Smart engine: its model, reasoning, and limits come from
|
|
57
|
+
# engines.SMART_CONFIG (tests/test_engines.py proves both build the same agent).
|
|
58
|
+
DEFAULT_FRONTIER_MODEL = SMART_CONFIG.model
|
|
59
|
+
MAX_TASK_COST_USD = 0.60 # a runaway task stops here (24 Sep: 50-step loops cost ~$0.80 each on gpt-5.5)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def task_cost_cap(bundles, model=None):
|
|
63
|
+
"""A spend cap that grows with the apps a task spans: a flat $0.25 cut five-app tasks off
|
|
64
|
+
mid-way (multi-006, mem-005, 24 Sep) while one-app tasks finish for $0.03-0.13. It is set in
|
|
65
|
+
gpt-5.6-terra dollars and scaled up by a model's input price, so a pricier model gets as many
|
|
66
|
+
turns: iOSWorld limits turns, not spend (the cap stopped 9 of 21 gpt-5.6-sol runs, 28 Sep).
|
|
67
|
+
None, no cap, for a model Mobster has no price for (a provider's "<provider>:<model>"): the frontier would count it
|
|
68
|
+
at the highest listed rates, every cached token at the dearest input rate (model_clients.ceiling_cost_usd), and
|
|
69
|
+
stop it after a fraction of a priced model's turns. On 17 Oct a Surplus claude-sonnet-5.5 run stopped at a
|
|
70
|
+
"$2.63" ceiling count for $0.29 of tokens. Such a run is bounded by iOSWorld's own 50 steps and the 900 s limit
|
|
71
|
+
(engines.SMART_CONFIG); its spend is read from its tokens afterwards."""
|
|
72
|
+
from ..model_clients import PRICES, cost_usd
|
|
73
|
+
base, rate = PRICES.get("gpt-5.6-terra"), PRICES.get(model or "")
|
|
74
|
+
if rate is None and model and cost_usd(model, {"prompt_tokens": 1}) is None:
|
|
75
|
+
return None
|
|
76
|
+
scale = rate[0] / base[0] if base and rate and base[0] else 1.0
|
|
77
|
+
return min(MAX_TASK_COST_USD, 0.10 + 0.08 * max(1, len(bundles))) * max(1.0, scale)
|
|
78
|
+
TASK_DIR_WIDTH = 3
|
|
79
|
+
_APP_ALIASES = {"letterboxd": "cinephile", "whatsapp": "quickchat", "linkedin": "lockedin"}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_tasks(repo):
|
|
83
|
+
return json.loads((pathlib.Path(repo) / "tasks.json").read_text(encoding="utf-8"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def load_manifest(repo):
|
|
87
|
+
"""{app: {bundle_id, app_path, ...}} written by iOSWorld's bootstrap."""
|
|
88
|
+
for candidate in sorted(pathlib.Path(repo).rglob(".app_manifest.json")):
|
|
89
|
+
return json.loads(candidate.read_text(encoding="utf-8"))
|
|
90
|
+
raise FileNotFoundError("No .app_manifest.json: run iOSWorld's bootstrap first")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def register_names(manifest):
|
|
94
|
+
"""Each app's display name (its Info.plist), so the agent's app-switch choices read "CalTrack", not a bundle."""
|
|
95
|
+
import plistlib
|
|
96
|
+
|
|
97
|
+
from ..catalog import register_app_names
|
|
98
|
+
names = {}
|
|
99
|
+
for key, entry in manifest.items():
|
|
100
|
+
name = key
|
|
101
|
+
try:
|
|
102
|
+
with open(pathlib.Path(entry.get("app_path", "")) / "Info.plist", "rb") as handle:
|
|
103
|
+
info = plistlib.load(handle)
|
|
104
|
+
name = info.get("CFBundleDisplayName") or info.get("CFBundleName") or key
|
|
105
|
+
except (OSError, ValueError, plistlib.InvalidFileException):
|
|
106
|
+
pass
|
|
107
|
+
if entry.get("bundle_id"):
|
|
108
|
+
names[entry["bundle_id"]] = name
|
|
109
|
+
register_app_names(names)
|
|
110
|
+
return names
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def iosworld_module(repo):
|
|
114
|
+
"""iOSWorld's own runner module (stdlib-only at import), for its reset functions."""
|
|
115
|
+
scripts = str(pathlib.Path(repo) / "scripts")
|
|
116
|
+
if scripts not in sys.path:
|
|
117
|
+
sys.path.insert(0, scripts)
|
|
118
|
+
import appium_agent
|
|
119
|
+
return appium_agent
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def app_entry(manifest, name):
|
|
123
|
+
return manifest.get(_APP_ALIASES.get(name, name)) or manifest.get(name) or {}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
SIM_DEVICES = pathlib.Path.home() / "Library/Developer/CoreSimulator/Devices"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def foreign_containers(udid, manifest):
|
|
130
|
+
"""Apps whose data containers on ``udid`` resolve outside that simulator's own folder, as [(bundle, path)].
|
|
131
|
+
A ``simctl clone`` keeps the source's LaunchServices paths, so one worker's reset wipes another's task."""
|
|
132
|
+
own = os.path.realpath(SIM_DEVICES / udid) + os.sep # both sides resolved: a home under /var is /private/var
|
|
133
|
+
foreign = []
|
|
134
|
+
for entry in manifest.values():
|
|
135
|
+
bundle = entry.get("bundle_id")
|
|
136
|
+
if not bundle:
|
|
137
|
+
continue
|
|
138
|
+
proc = subprocess.run(["xcrun", "simctl", "get_app_container", udid, bundle, "data"],
|
|
139
|
+
capture_output=True, text=True)
|
|
140
|
+
path = proc.stdout.strip()
|
|
141
|
+
if proc.returncode == 0 and path and not os.path.realpath(path).startswith(own):
|
|
142
|
+
foreign.append((bundle, path))
|
|
143
|
+
return foreign
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def check_isolation(udid, manifest):
|
|
147
|
+
"""Refuse a simulator whose apps live in another simulator's containers (see foreign_containers)."""
|
|
148
|
+
foreign = foreign_containers(udid, manifest)
|
|
149
|
+
if foreign:
|
|
150
|
+
bundle, path = foreign[0]
|
|
151
|
+
raise SystemExit(f"simulator {udid} is not isolated: {len(foreign)} apps resolve to another device's "
|
|
152
|
+
f"containers (e.g. {bundle} -> {path}). It was likely made with `simctl clone`: "
|
|
153
|
+
f"create a fresh simulator and run iOSWorld's bootstrap on it.")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
CONTAINER_METADATA = ".com.apple.mobile_container_manager.metadata.plist"
|
|
157
|
+
STATE_PARTS = ("Documents", "Library/Application Support") # JSON state files and SQLite stores (with -wal, -shm)
|
|
158
|
+
STATE_SETTLE_SECONDS = 1.0 # apps write on leaving the foreground (SwiftData's autosave) or on a background queue
|
|
159
|
+
SECRET_DEFAULTS = re.compile(r"api[_-]?key|token|secret|password", re.I) # iOSWorld writes openai_api_key into each app
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def containers(root):
|
|
163
|
+
"""{identifier: folder} for the containers under ``root``, named by each one's metadata plist, the way
|
|
164
|
+
iOSWorld's mcps/_data_layer.py finds an app's data (Data/Application) or the shared envelope (Shared/AppGroup)."""
|
|
165
|
+
found = {}
|
|
166
|
+
for folder in sorted(root.iterdir()) if root.is_dir() else ():
|
|
167
|
+
try:
|
|
168
|
+
identifier = plistlib.loads((folder / CONTAINER_METADATA).read_bytes()).get("MCMMetadataIdentifier")
|
|
169
|
+
except (OSError, ValueError, plistlib.InvalidFileException):
|
|
170
|
+
continue
|
|
171
|
+
if identifier:
|
|
172
|
+
found[identifier] = folder
|
|
173
|
+
return found
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def utc(epoch):
|
|
177
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(epoch)) if epoch is not None else None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def save_end_state(udid, bundles, folder, wda_url=None, started=None):
|
|
181
|
+
"""Copy the task's apps' end state into ``folder``/state, before the next task's reset wipes it, so a run can be
|
|
182
|
+
graded by code: each app's Documents/, Library/Application Support/ and UserDefaults plist (state/apps/<bundle>/),
|
|
183
|
+
and the app groups of the apps' vendor (state/groups/<group>/: the cloud apps' shared workspace envelope). With
|
|
184
|
+
``wda_url``, the app in front is sent home first and given STATE_SETTLE_SECONDS to write; then the task's apps are
|
|
185
|
+
closed, so nothing writes during the copy, and their preferences get as long again to reach disk. ``started`` (the
|
|
186
|
+
task's start, epoch seconds) is recorded, since the shared app groups are never reset and keep earlier runs'
|
|
187
|
+
records. It never fails a run: any error is written to state/ERROR."""
|
|
188
|
+
import shutil
|
|
189
|
+
out = pathlib.Path(folder) / "state"
|
|
190
|
+
began = time.monotonic()
|
|
191
|
+
try:
|
|
192
|
+
if wda_url:
|
|
193
|
+
try: # WDA's sessionless home button: the run's session is closed by now
|
|
194
|
+
urllib.request.urlopen(urllib.request.Request(wda_url + "/wda/homescreen", data=b"{}", method="POST",
|
|
195
|
+
headers={"Content-Type": "application/json"}), timeout=10)
|
|
196
|
+
except (OSError, ValueError):
|
|
197
|
+
pass
|
|
198
|
+
time.sleep(STATE_SETTLE_SECONDS)
|
|
199
|
+
for bundle in bundles:
|
|
200
|
+
try:
|
|
201
|
+
subprocess.run(["xcrun", "simctl", "terminate", udid, bundle], capture_output=True, timeout=20)
|
|
202
|
+
except (OSError, subprocess.SubprocessError):
|
|
203
|
+
pass
|
|
204
|
+
time.sleep(STATE_SETTLE_SECONDS)
|
|
205
|
+
root = SIM_DEVICES / udid / "data/Containers"
|
|
206
|
+
apps = containers(root / "Data/Application")
|
|
207
|
+
saved, missing = {}, []
|
|
208
|
+
for bundle in bundles:
|
|
209
|
+
home = apps.get(bundle)
|
|
210
|
+
if home is None:
|
|
211
|
+
missing.append(bundle)
|
|
212
|
+
continue
|
|
213
|
+
for rel in (*STATE_PARTS, f"Library/Preferences/{bundle}.plist"):
|
|
214
|
+
source, target = home / rel, out / "apps" / bundle / rel
|
|
215
|
+
if source.is_dir():
|
|
216
|
+
shutil.copytree(source, target, dirs_exist_ok=True)
|
|
217
|
+
elif source.is_file(): # the UserDefaults plist, without keys
|
|
218
|
+
prefs = plistlib.loads(source.read_bytes())
|
|
219
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
220
|
+
target.write_bytes(plistlib.dumps({k: v for k, v in prefs.items() if not SECRET_DEFAULTS.search(k)},
|
|
221
|
+
fmt=plistlib.FMT_BINARY))
|
|
222
|
+
else:
|
|
223
|
+
continue
|
|
224
|
+
saved.setdefault(bundle, []).append(rel)
|
|
225
|
+
vendors = {"group." + bundle.rsplit(".", 1)[0] for bundle in bundles if "." in bundle}
|
|
226
|
+
shared = containers(root / "Shared/AppGroup")
|
|
227
|
+
groups = [group for group in shared if group.rsplit(".", 1)[0] in vendors]
|
|
228
|
+
for group in groups:
|
|
229
|
+
shutil.copytree(shared[group], out / "groups" / group, dirs_exist_ok=True,
|
|
230
|
+
ignore=shutil.ignore_patterns(CONTAINER_METADATA))
|
|
231
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
record = {"udid": udid, "started": utc(started), "saved": utc(time.time()),
|
|
233
|
+
"seconds": round(time.monotonic() - began, 2), "apps": saved, "missing": missing, "groups": groups}
|
|
234
|
+
(out / "state.json").write_text(json.dumps(record, indent=1), encoding="utf-8")
|
|
235
|
+
except Exception as error: # a snapshot never costs a run
|
|
236
|
+
try:
|
|
237
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
238
|
+
(out / "ERROR").write_text(f"{type(error).__name__}: {error}\n", encoding="utf-8")
|
|
239
|
+
except OSError:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
RESET_ATTEMPTS = 2
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# MOBSTER_BENCH_RESET: how each task's start state is made.
|
|
247
|
+
# - "iosworld" (default): iOSWorld's own reset, wiping and re-seeding every app (43-66 s a task).
|
|
248
|
+
# - "restore": put back a checkpoint of the freshly seeded apps (bench/evals/checkpoint.py, ~4.5 s), checked by
|
|
249
|
+
# digest. The checkpoint is made once per simulator, from iOSWorld's own reset, in MOBSTER_BENCH_CHECKPOINTS
|
|
250
|
+
# (seed-<UDID>). A restore whose digest differs from the save falls back to iOSWorld's reset.
|
|
251
|
+
RESET_MODES = ("iosworld", "restore")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def reset_mode():
|
|
255
|
+
mode = os.environ.get("MOBSTER_BENCH_RESET", "").strip() or "iosworld"
|
|
256
|
+
if mode not in RESET_MODES:
|
|
257
|
+
raise ValueError(f"MOBSTER_BENCH_RESET={mode!r}: use iosworld or restore")
|
|
258
|
+
return mode
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def reset(repo, udid, manifest, mode=None):
|
|
262
|
+
"""The task's clean slate (see MOBSTER_BENCH_RESET); returns {"mode", "seconds", ...} for the record."""
|
|
263
|
+
mode = reset_mode() if mode is None else mode
|
|
264
|
+
started = time.monotonic()
|
|
265
|
+
if mode == "restore":
|
|
266
|
+
from .evals import checkpoint
|
|
267
|
+
bundles = {app: entry["bundle_id"] for app, entry in manifest.items() if entry.get("bundle_id")}
|
|
268
|
+
root = pathlib.Path(os.environ.get("MOBSTER_BENCH_CHECKPOINTS", "")).expanduser()
|
|
269
|
+
if not str(root) or str(root) == ".":
|
|
270
|
+
raise ValueError("MOBSTER_BENCH_RESET=restore needs MOBSTER_BENCH_CHECKPOINTS (a folder for the seeds)")
|
|
271
|
+
seed = root / f"seed-{udid}"
|
|
272
|
+
if not (seed / "checkpoint.json").exists():
|
|
273
|
+
iosworld_reset(repo, udid, manifest)
|
|
274
|
+
checkpoint.save(udid, bundles, seed)
|
|
275
|
+
return {"mode": "restore", "seeded": True, "seconds": round(time.monotonic() - started, 1)}
|
|
276
|
+
seconds, ok = checkpoint.restore(udid, bundles, seed)
|
|
277
|
+
if ok:
|
|
278
|
+
return {"mode": "restore", "seconds": seconds, "digest_ok": True}
|
|
279
|
+
iosworld_reset(repo, udid, manifest)
|
|
280
|
+
return {"mode": "restore", "digest_ok": False, "fell_back": True,
|
|
281
|
+
"seconds": round(time.monotonic() - started, 1)}
|
|
282
|
+
iosworld_reset(repo, udid, manifest)
|
|
283
|
+
return {"mode": "iosworld", "seconds": round(time.monotonic() - started, 1)}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def iosworld_reset(repo, udid, manifest):
|
|
287
|
+
"""iOSWorld's clean slate: wipe every app's data, re-seed each, as its runner does per task."""
|
|
288
|
+
runner = iosworld_module(repo)
|
|
289
|
+
apps = list(manifest)
|
|
290
|
+
for attempt in range(RESET_ATTEMPTS):
|
|
291
|
+
try:
|
|
292
|
+
runner.reset_app_data(udid, apps, manifest)
|
|
293
|
+
runner.reseed_apps(udid, apps, manifest)
|
|
294
|
+
return
|
|
295
|
+
except RuntimeError:
|
|
296
|
+
# Seeding under load can leave apps empty ("no seed data after 3 attempts": four
|
|
297
|
+
# simulators busy, 24 Sep); a full second pass cleared it.
|
|
298
|
+
if attempt + 1 == RESET_ATTEMPTS:
|
|
299
|
+
raise
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class Shots:
|
|
303
|
+
"""PNG screenshots through ``simctl io`` (not WDA, so Mobster's device channel is untouched)."""
|
|
304
|
+
|
|
305
|
+
def __init__(self, udid, folder):
|
|
306
|
+
self.udid, self.folder = udid, pathlib.Path(folder).resolve()
|
|
307
|
+
self.folder.mkdir(parents=True, exist_ok=True)
|
|
308
|
+
self.count = 0
|
|
309
|
+
self.seconds = 0.0
|
|
310
|
+
self.trace = None # the driver's call_trace: shots go in it, to be told apart from the agent's time
|
|
311
|
+
|
|
312
|
+
def take(self, name):
|
|
313
|
+
started = time.monotonic()
|
|
314
|
+
path = self.folder / f"{name}.png"
|
|
315
|
+
if not simctl.screenshot(self.udid, path, timeout=20).ok:
|
|
316
|
+
path = None
|
|
317
|
+
self.seconds += time.monotonic() - started
|
|
318
|
+
self.count += 1
|
|
319
|
+
if self.trace is not None:
|
|
320
|
+
self.trace.append(("shot", started, (time.monotonic() - started) * 1000))
|
|
321
|
+
return str(path) if path else None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def describe(operation, target=None, text=None):
|
|
325
|
+
"""One iOSWorld-style action dict for a Mobster operation."""
|
|
326
|
+
if operation == "LAUNCH_APP":
|
|
327
|
+
return {"type": "launch_app", "bundle_id": str(target)}
|
|
328
|
+
if operation.startswith("SWIPE_"):
|
|
329
|
+
return {"type": "swipe", "direction": operation.split("_", 1)[1].lower()}
|
|
330
|
+
if operation in ("HOME", "BACK", "VOLUME_UP", "VOLUME_DOWN"):
|
|
331
|
+
return {"type": operation.lower()}
|
|
332
|
+
action = {"type": {"TAP": "tap", "TYPE": "type", "TYPE_SUBMIT": "type", "SUBMIT": "submit",
|
|
333
|
+
"LONG_PRESS": "long_press"}.get(operation, operation.lower())}
|
|
334
|
+
if target is not None and hasattr(target, "label"):
|
|
335
|
+
action["element"] = {"label": (target.label or "")[:120], "role": target.role}
|
|
336
|
+
if text is not None:
|
|
337
|
+
action["text"] = text
|
|
338
|
+
if operation == "TYPE_SUBMIT":
|
|
339
|
+
action["submit"] = True
|
|
340
|
+
return action
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class RecordingDriver:
|
|
344
|
+
"""The simulator's WDA driver, with each dispatch recorded as an iOSWorld trajectory step."""
|
|
345
|
+
|
|
346
|
+
def __init__(self, driver, shots):
|
|
347
|
+
self._driver, self.shots = driver, shots
|
|
348
|
+
self.steps = []
|
|
349
|
+
# The model turn now acting (set from frontier_decision events): screenshots are named by
|
|
350
|
+
# it, since a turn may dispatch several actions and a refused turn none.
|
|
351
|
+
self.turn = None
|
|
352
|
+
|
|
353
|
+
def __getattr__(self, name):
|
|
354
|
+
return getattr(self._driver, name)
|
|
355
|
+
|
|
356
|
+
def _record(self, action):
|
|
357
|
+
"""Screenshot and record one step; returns the screenshot's seconds."""
|
|
358
|
+
index = len(self.steps) + 1
|
|
359
|
+
name = f"step-{index:03d}" + (f"-turn-{self.turn:02d}" if self.turn is not None else "")
|
|
360
|
+
started = time.monotonic()
|
|
361
|
+
self.steps.append({"step": index, "screenshot": self.shots.take(name), "actions": [action],
|
|
362
|
+
**({"turn": self.turn} if self.turn is not None else {})})
|
|
363
|
+
return time.monotonic() - started
|
|
364
|
+
|
|
365
|
+
def execute(self, operation, target, snapshot, text=None, timeout=10):
|
|
366
|
+
spent = self._record(describe(operation, target, text))
|
|
367
|
+
# The judge's screenshot (0.2-2.5 s) does not age the observation: without it the action goes
|
|
368
|
+
# that much sooner, so the driver's re-read of an observation over 1 s old (WDA_FRESH_SECONDS)
|
|
369
|
+
# would not run (it ran after 0.6-1.8 s screenshots in exec-new2's megamart-001).
|
|
370
|
+
if hasattr(snapshot, "captured_at"):
|
|
371
|
+
snapshot = dataclasses.replace(snapshot, captured_at=snapshot.captured_at + spent)
|
|
372
|
+
return self._driver.execute(operation, target, snapshot, text=text, timeout=timeout)
|
|
373
|
+
|
|
374
|
+
def long_press(self, target, snapshot, timeout=10):
|
|
375
|
+
self._record({"type": "long_press", "element": {"label": (target.label or "")[:120], "role": target.role}})
|
|
376
|
+
return self._driver.long_press(target, snapshot, timeout=timeout)
|
|
377
|
+
|
|
378
|
+
def tap_point(self, x, y, snapshot, timeout=10, purpose="dismiss"):
|
|
379
|
+
self._record({"type": "tap_xy", "x": round(x, 3), "y": round(y, 3), "purpose": purpose})
|
|
380
|
+
return self._driver.tap_point(x, y, snapshot, timeout=timeout)
|
|
381
|
+
|
|
382
|
+
def set_value(self, target, value, timeout=10):
|
|
383
|
+
self._record({"type": "set_picker", "element": {"label": (target.value or "")[:120], "role": target.role},
|
|
384
|
+
"value": str(value)[:60]})
|
|
385
|
+
return self._driver.set_value(target, value, timeout=timeout)
|
|
386
|
+
|
|
387
|
+
def clear_text(self, target, timeout=10):
|
|
388
|
+
self._record({"type": "clear_text", "element": {"label": (target.label or "")[:120], "role": target.role}})
|
|
389
|
+
return self._driver.clear_text(target, timeout=timeout)
|
|
390
|
+
|
|
391
|
+
def write_text(self, target, snapshot, text, append=False, submit=False, timeout=30):
|
|
392
|
+
# One step, as the typed text: its paste, read-back and repair are how the typing is done.
|
|
393
|
+
action = describe("TYPE_SUBMIT" if submit else "TYPE", target, text)
|
|
394
|
+
if not append:
|
|
395
|
+
action["replace"] = True
|
|
396
|
+
self._record(action if text else {"type": "clear_text", "element": action.get("element")})
|
|
397
|
+
return self._driver.write_text(target, snapshot, text, append=append, submit=submit, timeout=timeout)
|
|
398
|
+
|
|
399
|
+
def replace_text(self, target, text, timeout=10):
|
|
400
|
+
self._record({**describe("TYPE", target, text), "replace": True})
|
|
401
|
+
done = self._driver.replace_text(target, text, timeout=timeout)
|
|
402
|
+
if not done:
|
|
403
|
+
self.steps.pop() # nothing was typed: the caller clears and types, recorded as such
|
|
404
|
+
return done
|
|
405
|
+
|
|
406
|
+
def call(self, method, path, body=None, timeout=10):
|
|
407
|
+
if method == "POST" and path == "/wda/apps/activate" and isinstance(body, dict):
|
|
408
|
+
self._record(describe("LAUNCH_APP", body.get("bundleId")))
|
|
409
|
+
elif method == "POST" and path == "/url" and isinstance(body, dict):
|
|
410
|
+
self._record({"type": "open_url", "url": str(body.get("url", ""))[:300]})
|
|
411
|
+
return self._driver.call(method, path, body, timeout)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def judge_shots_apart(timing, seconds):
|
|
415
|
+
"""The frontier's timing with the judge's screenshots (taken before each dispatch, inside its
|
|
416
|
+
exec bucket) moved to their own bucket: they are not the agent's time (0.4-2.5 s each)."""
|
|
417
|
+
if not timing or "exec" not in timing:
|
|
418
|
+
return timing
|
|
419
|
+
return {**timing, "exec": round(timing["exec"] - seconds, 2), "judge_shots": round(seconds, 2)}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def turn_tokens(usage):
|
|
423
|
+
"""One call's usage, compact, for the trace: input, output, cached and cache-written tokens, and with
|
|
424
|
+
MOBSTER_BENCH_TTFT=on the time to first token and the call's total (bench/stream_timing.py)."""
|
|
425
|
+
out = {"in": usage.get("prompt_tokens"), "out": usage.get("completion_tokens"),
|
|
426
|
+
"cached": usage.get("cached_tokens"), "write": usage.get("cache_write_tokens")}
|
|
427
|
+
for key in ("ttft_ms", "text_ttft_ms", "first_action_ms", "total_ms"):
|
|
428
|
+
if usage.get(key) is not None:
|
|
429
|
+
out[key] = usage[key]
|
|
430
|
+
return out
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def build_id(path=None):
|
|
434
|
+
"""The code's commit (and whether its tree has local changes), so every record names its build."""
|
|
435
|
+
folder = pathlib.Path(path or __file__).resolve().parent
|
|
436
|
+
try:
|
|
437
|
+
sha = subprocess.run(["git", "-C", str(folder), "rev-parse", "HEAD"], capture_output=True, text=True,
|
|
438
|
+
timeout=10).stdout.strip()
|
|
439
|
+
dirty = subprocess.run(["git", "-C", str(folder), "status", "--porcelain", "--untracked-files=no"],
|
|
440
|
+
capture_output=True, text=True, timeout=10).stdout.strip()
|
|
441
|
+
except (OSError, subprocess.SubprocessError):
|
|
442
|
+
return None
|
|
443
|
+
return (sha[:12] + ("+dirty" if dirty else "")) if sha else None
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def answer_text(result):
|
|
447
|
+
data = result.get("data")
|
|
448
|
+
if data is None:
|
|
449
|
+
return None
|
|
450
|
+
if isinstance(data, str):
|
|
451
|
+
return data
|
|
452
|
+
return json.dumps(data, ensure_ascii=False)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def clear_alerts(driver, attempts=5):
|
|
456
|
+
"""Accept system prompts the re-seed launches left up (CityRide's location prompt covered
|
|
457
|
+
CalTrack in the first smoke run). iOSWorld's bootstrap clicks these with AppleScript, which
|
|
458
|
+
needs the Simulator window; here WDA's alert endpoint does it headlessly, before the task."""
|
|
459
|
+
from ..transport import TransportError
|
|
460
|
+
for _ in range(attempts):
|
|
461
|
+
try:
|
|
462
|
+
text = driver.call("GET", "/alert/text", timeout=5)
|
|
463
|
+
except TransportError:
|
|
464
|
+
return
|
|
465
|
+
if not text:
|
|
466
|
+
return
|
|
467
|
+
try:
|
|
468
|
+
driver.call("POST", "/alert/accept", {"name": "Allow While Using App"}, timeout=5)
|
|
469
|
+
except TransportError:
|
|
470
|
+
driver.call("POST", "/alert/accept", {}, timeout=5)
|
|
471
|
+
time.sleep(.5)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# MOBSTER_BENCH_SESSION: the capabilities of each task's WDA session. Every task starts a new session, so two arms of
|
|
475
|
+
# an A/B on one simulator never share one.
|
|
476
|
+
# - "product" (the default since 10 Oct): as the app makes it (drivers.resolve_wda_session): no quiescence wait, so the
|
|
477
|
+
# bench measures the session users run. (A first SWIPE_UP on a live score feed still hung 95 s with it, 1 of 1
|
|
478
|
+
# such runs on 10 Oct: that hang is the swipe's, not the session's.)
|
|
479
|
+
# - "legacy": WDA's default capabilities, as every run before 10 Oct had them.
|
|
480
|
+
SESSION_CAPABILITIES = {"product": {"platformName": "iOS", "shouldWaitForQuiescence": False, "waitForIdleTimeout": 0},
|
|
481
|
+
"legacy": {}}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def session_mode():
|
|
485
|
+
mode = os.environ.get("MOBSTER_BENCH_SESSION", "").strip() or "product"
|
|
486
|
+
if mode not in SESSION_CAPABILITIES:
|
|
487
|
+
raise ValueError(f"MOBSTER_BENCH_SESSION={mode!r}: use product or legacy")
|
|
488
|
+
return mode
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def wda_session(wda_url, mode=None):
|
|
492
|
+
"""A new WDA session with ``mode``'s capabilities (session_mode() by default)."""
|
|
493
|
+
mode = session_mode() if mode is None else mode
|
|
494
|
+
if mode not in SESSION_CAPABILITIES:
|
|
495
|
+
raise ValueError(f"MOBSTER_BENCH_SESSION={mode!r}: use product or legacy")
|
|
496
|
+
request = urllib.request.Request(wda_url + "/session", method="POST",
|
|
497
|
+
data=json.dumps(
|
|
498
|
+
{"capabilities": {"alwaysMatch": SESSION_CAPABILITIES[mode]}}).encode(),
|
|
499
|
+
headers={"Content-Type": "application/json"})
|
|
500
|
+
created = json.load(urllib.request.urlopen(request, timeout=30))
|
|
501
|
+
return created.get("sessionId") or (created.get("value") or {}).get("sessionId")
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def sim_restarter(udid, wda_url, mjpeg_url, xctestrun, log_path=None):
|
|
505
|
+
"""The simulator's WDA restarter (sim/wda.py), or None without its xctestrun or in the open-source tree,
|
|
506
|
+
which leaves restarting a runner to whoever started it."""
|
|
507
|
+
if not xctestrun or not mjpeg_url:
|
|
508
|
+
return None
|
|
509
|
+
from urllib.parse import urlsplit
|
|
510
|
+
try:
|
|
511
|
+
from ..sim.wda import SimulatorWDA
|
|
512
|
+
except ImportError:
|
|
513
|
+
return None
|
|
514
|
+
return SimulatorWDA(udid, urlsplit(wda_url).port, urlsplit(mjpeg_url).port, xctestrun, log_path=log_path)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def frontier_agent(driver, names, emit, bundles, model=None, reasoning=None, recorder=None, request=None):
|
|
518
|
+
"""(FrontierAgent, client): engines.SMART_CONFIG, the app's Smart engine, with this harness's per-task cost cap."""
|
|
519
|
+
from ..frontier import FrontierAgent
|
|
520
|
+
from ..model_clients import chat_client
|
|
521
|
+
client = chat_client(model or DEFAULT_FRONTIER_MODEL, reasoning=reasoning or SMART_CONFIG.reasoning)
|
|
522
|
+
if os.environ.get("MOBSTER_BENCH_TTFT", "").lower() in ("1", "on", "true", "yes"):
|
|
523
|
+
from .stream_timing import timed
|
|
524
|
+
client = timed(client) # the same request, streamed, to time the first token (both arms alike)
|
|
525
|
+
kwargs = frontier_kwargs(max_cost_usd=task_cost_cap(bundles, client.model))
|
|
526
|
+
if recorder is not None:
|
|
527
|
+
recorder.header(request=request, apps=names, model=client.model, frontier=kwargs, driver=driver)
|
|
528
|
+
driver, client = tape.TapeDriver(driver, recorder), tape.TapeClient(client, recorder)
|
|
529
|
+
return FrontierAgent(driver, client, apps=names, emit=emit, **kwargs), client
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def taping():
|
|
533
|
+
"""Whether a run keeps its tape (tape.jsonl, for bench/tape_replay.py): on unless MOBSTER_TAPE is off."""
|
|
534
|
+
return os.environ.get("MOBSTER_TAPE", "").strip().casefold() not in ("off", "0", "false", "no")
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def run_task(task, *, repo, udid, manifest, wda_url, mjpeg_url, folder, policy="frontier", frontier_model=None,
|
|
538
|
+
reasoning="low", wda_xctestrun=None):
|
|
539
|
+
from ..catalog import app_label
|
|
540
|
+
from ..compose import build_target_driver, close_all
|
|
541
|
+
from ..model_clients import cost_usd
|
|
542
|
+
from ..wda.frame_clock import attach_frame_clock
|
|
543
|
+
|
|
544
|
+
folder = pathlib.Path(folder)
|
|
545
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
546
|
+
task_started = time.time()
|
|
547
|
+
t_reset = time.monotonic()
|
|
548
|
+
reset_info = reset(repo, udid, manifest)
|
|
549
|
+
reset_s = time.monotonic() - t_reset
|
|
550
|
+
|
|
551
|
+
bundles = [app_entry(manifest, app).get("bundle_id") for app in task.get("apps", [])]
|
|
552
|
+
bundles = [b for b in bundles if b]
|
|
553
|
+
costs, events = [], []
|
|
554
|
+
|
|
555
|
+
def emit(event):
|
|
556
|
+
kind = event.get("event")
|
|
557
|
+
if kind == "frontier_prompt":
|
|
558
|
+
# Kept out of task.json: prompts.jsonl, one line per turn, and the image the model saw as
|
|
559
|
+
# screens/prompt-turn-NN.jpg, so a decision can be replayed offline (bench/replay.py).
|
|
560
|
+
event = dict(event)
|
|
561
|
+
image = event.pop("image", None)
|
|
562
|
+
if image and image.startswith("data:image/jpeg;base64,"):
|
|
563
|
+
name = f"prompt-turn-{event.get('step', 0):02d}.jpg"
|
|
564
|
+
(folder / "screens").mkdir(parents=True, exist_ok=True)
|
|
565
|
+
(folder / "screens" / name).write_bytes(base64.b64decode(image.split(",", 1)[1]))
|
|
566
|
+
event["image"] = f"screens/{name}"
|
|
567
|
+
with open(folder / "prompts.jsonl", "a", encoding="utf-8") as prompts:
|
|
568
|
+
prompts.write(json.dumps(event, ensure_ascii=False) + "\n")
|
|
569
|
+
return
|
|
570
|
+
if kind == "inference_finished" and isinstance(event.get("cost_nanodollars"), int):
|
|
571
|
+
costs.append(event["cost_nanodollars"] / 1e9)
|
|
572
|
+
if kind in ("frontier_decision", "frontier_error", "frontier_refused", "frontier_action", "frontier_failed",
|
|
573
|
+
"frontier_chunk_stop", "frontier_checklist", "frontier_verify",
|
|
574
|
+
"frontier_text", "frontier_tuned", "frontier_wda_recovery",
|
|
575
|
+
"frontier_contract", "frontier_contract_update", "frontier_screen",
|
|
576
|
+
"frontier_open_url", "frontier_retry", "frontier_split",
|
|
577
|
+
"frontier_app_relaunched"):
|
|
578
|
+
usage = event.get("usage")
|
|
579
|
+
event = {k: v for k, v in event.items() if k != "usage"}
|
|
580
|
+
if kind == "frontier_decision" and isinstance(usage, dict) and usage:
|
|
581
|
+
event["tokens"] = turn_tokens(usage)
|
|
582
|
+
trace = getattr(driver, "call_trace", None)
|
|
583
|
+
if trace and kind in ("frontier_decision", "frontier_action", "frontier_failed"):
|
|
584
|
+
# Every WDA request, read and judge screenshot since the last event: [kind, start ms, ms].
|
|
585
|
+
items = trace[:]
|
|
586
|
+
del trace[:len(items)] # the verify read's thread may append meanwhile
|
|
587
|
+
event["wda"] = [[name, round((at - started) * 1000), round(ms)] for name, at, ms in items]
|
|
588
|
+
events.append(event)
|
|
589
|
+
if kind == "frontier_decision" and recording is not None:
|
|
590
|
+
recording.turn = event.get("step")
|
|
591
|
+
return
|
|
592
|
+
if kind in ("result", "error"):
|
|
593
|
+
kept = {k: v for k, v in event.items() if isinstance(v, (str, int, float, bool)) or v is None}
|
|
594
|
+
if isinstance(kept.get("text"), str):
|
|
595
|
+
kept["text"] = kept["text"][:600]
|
|
596
|
+
events.append(kept)
|
|
597
|
+
|
|
598
|
+
shots = Shots(udid, folder / "screens")
|
|
599
|
+
driver = recording = calls = None
|
|
600
|
+
model_name = frontier_model or DEFAULT_FRONTIER_MODEL
|
|
601
|
+
started = time.monotonic()
|
|
602
|
+
try:
|
|
603
|
+
session = wda_session(wda_url)
|
|
604
|
+
driver = build_target_driver(wda_url=wda_url, session=session)
|
|
605
|
+
driver.call_trace = shots.trace = []
|
|
606
|
+
# A runner that dies mid-task is restarted as it was started, and its session taken the way this
|
|
607
|
+
# harness takes one (frontier.FrontierAgent._recover).
|
|
608
|
+
driver.restarter = sim_restarter(udid, wda_url, mjpeg_url, wda_xctestrun or os.environ.get(
|
|
609
|
+
"MOBSTER_SIM_WDA_XCTESTRUN"), log_path=str(folder / "wda-restart.log"))
|
|
610
|
+
driver.new_session = lambda: wda_session(wda_url)
|
|
611
|
+
from ..app_killer import simulator as simulator_killer
|
|
612
|
+
driver.killer = simulator_killer(udid) # a stuck app WDA can't close is closed by simctl
|
|
613
|
+
attach_frame_clock(driver, wda_url=wda_url, session=session, mjpeg_url=mjpeg_url,
|
|
614
|
+
log_path=str(folder / "frameclock.jsonl"))
|
|
615
|
+
clear_alerts(driver)
|
|
616
|
+
driver.call("POST", "/wda/pressButton", {"name": "home"}, timeout=10)
|
|
617
|
+
recording = RecordingDriver(driver, shots)
|
|
618
|
+
if bundles:
|
|
619
|
+
recording.call("POST", "/wda/apps/activate", {"bundleId": bundles[0]}, timeout=20)
|
|
620
|
+
names = {b: app_label(b).split(" (")[0] for b in bundles}
|
|
621
|
+
started, shot_seconds = time.monotonic(), shots.seconds
|
|
622
|
+
recorder = tape.Tape(folder / tape.NAME) if taping() else None
|
|
623
|
+
agent, client = frontier_agent(recording, names, emit, bundles, model_name, reasoning, recorder=recorder,
|
|
624
|
+
request=task["goal"])
|
|
625
|
+
outcome = agent.run(task["goal"])
|
|
626
|
+
if recorder is not None:
|
|
627
|
+
recorder.end(outcome)
|
|
628
|
+
costs.append(cost_usd(client.model, outcome["usage"]) or 0.0)
|
|
629
|
+
calls = outcome["usage"].get("calls") # FrontierAgent emits no inference_finished: one cost, many calls
|
|
630
|
+
result = {"status": outcome["status"], "data": outcome.get("answer"),
|
|
631
|
+
"reason": outcome.get("reason", ""), "actions": outcome["actions"],
|
|
632
|
+
"decisions": outcome["steps"], "usage": outcome["usage"], "notes": outcome["notes"],
|
|
633
|
+
"timing": judge_shots_apart(outcome.get("timing"), shots.seconds - shot_seconds)}
|
|
634
|
+
except Exception as error:
|
|
635
|
+
result = {"status": "error", "reason": f"{type(error).__name__}: {error}"}
|
|
636
|
+
finally:
|
|
637
|
+
elapsed = time.monotonic() - started
|
|
638
|
+
close_all(driver)
|
|
639
|
+
steps = recording.steps if recording is not None else []
|
|
640
|
+
final = shots.take("final")
|
|
641
|
+
save_end_state(udid, bundles, folder, wda_url=wda_url, started=task_started)
|
|
642
|
+
if steps:
|
|
643
|
+
steps[-1]["post_action_screenshot"] = final
|
|
644
|
+
else:
|
|
645
|
+
steps = [{"step": 1, "screenshot": final, "actions": [], "post_action_screenshot": final}]
|
|
646
|
+
answer = answer_text(result)
|
|
647
|
+
if answer is not None:
|
|
648
|
+
steps.append({"step": len(steps) + 1, "actions": [{"type": "stop", "answer": answer}],
|
|
649
|
+
"post_action_screenshot": final})
|
|
650
|
+
(folder / "trajectory.json").write_text(json.dumps(steps, indent=1), encoding="utf-8")
|
|
651
|
+
# "ok" means the agent ran to a result; iOSWorld's judge then decides success.
|
|
652
|
+
record = {"task": task["name"], "goal": task["goal"], "apps": task.get("apps", []),
|
|
653
|
+
"category": task.get("category"), "difficulty": task.get("difficulty"),
|
|
654
|
+
"status": "ok", "agent_answer": answer,
|
|
655
|
+
"mobster": {"status": result.get("status"), "reason": str(result.get("reason") or "")[:400],
|
|
656
|
+
"data_status": result.get("data_status"), "actions": result.get("actions"),
|
|
657
|
+
"decisions": result.get("decisions"),
|
|
658
|
+
"agent_seconds": round(max(0.0, elapsed - shots.seconds), 2),
|
|
659
|
+
"screenshot_seconds": round(shots.seconds, 2), "reset_seconds": round(reset_s, 1),
|
|
660
|
+
"reset": reset_info,
|
|
661
|
+
"cost_usd": round(sum(costs), 6),
|
|
662
|
+
"model_calls": calls if calls is not None else len(costs), "policy": policy,
|
|
663
|
+
"model": model_name, "usage": result.get("usage"), "build": build_id(),
|
|
664
|
+
"ttft": os.environ.get("MOBSTER_BENCH_TTFT", "") or None,
|
|
665
|
+
"wda_session": session_mode(),
|
|
666
|
+
"notes": result.get("notes"), "timing": result.get("timing"),
|
|
667
|
+
"events": events[-400:]}}
|
|
668
|
+
(folder / "task.json").write_text(json.dumps(record, indent=1), encoding="utf-8")
|
|
669
|
+
return record
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def load_envs(paths):
|
|
673
|
+
from ..config import load_env_file
|
|
674
|
+
for path in paths or ():
|
|
675
|
+
load_env_file(os.path.expanduser(path))
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def cmd_run(args):
|
|
679
|
+
# OPENAI_API_KEY goes into the apps' UserDefaults, as iOSWorld's runner does (its apps' in-app replies).
|
|
680
|
+
load_envs(args.env_file)
|
|
681
|
+
tasks = all_tasks = load_tasks(args.repo)
|
|
682
|
+
all_names = [t["name"] for t in all_tasks] # folder numbers follow the full task list
|
|
683
|
+
if args.only:
|
|
684
|
+
wanted = set(args.only.split(","))
|
|
685
|
+
tasks = [t for t in tasks if t["name"] in wanted]
|
|
686
|
+
if args.limit:
|
|
687
|
+
tasks = tasks[:args.limit]
|
|
688
|
+
manifest = load_manifest(args.repo)
|
|
689
|
+
register_names(manifest)
|
|
690
|
+
check_isolation(args.udid, manifest)
|
|
691
|
+
out = pathlib.Path(args.out)
|
|
692
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
693
|
+
for task in tasks:
|
|
694
|
+
folder = out / f"{all_names.index(task['name']) + 1:0{TASK_DIR_WIDTH}d}-{task['name']}"
|
|
695
|
+
if (folder / "task.json").exists() and not args.force:
|
|
696
|
+
print(f"skip {task['name']} (done)", flush=True)
|
|
697
|
+
continue
|
|
698
|
+
try:
|
|
699
|
+
record = run_task(task, repo=args.repo, udid=args.udid, manifest=manifest, wda_url=args.wda_url,
|
|
700
|
+
mjpeg_url=args.mjpeg_url, folder=folder, policy=args.policy, frontier_model=args.model,
|
|
701
|
+
reasoning=args.reasoning, wda_xctestrun=args.wda_xctestrun)
|
|
702
|
+
except RuntimeError as error:
|
|
703
|
+
# A failed reset is the harness's, not the agent's: no task.json, so a rerun retries it,
|
|
704
|
+
# and the rest of this simulator's queue still runs.
|
|
705
|
+
print(f"{task['name']:24} NOT RUN (reset failed: {str(error)[:120]})", flush=True)
|
|
706
|
+
continue
|
|
707
|
+
m = record["mobster"]
|
|
708
|
+
print(f"{task['name']:24} {m['status']:24} {m['agent_seconds']:6.1f}s ${m['cost_usd']:.4f} "
|
|
709
|
+
f"steps={m['decisions']} answer={str(record['agent_answer'])[:80]!r}", flush=True)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def cmd_pool(args):
|
|
713
|
+
"""One ``run`` per simulator (UDID:WDA-port:MJPEG-port), the tasks dealt round-robin."""
|
|
714
|
+
tasks = [t["name"] for t in load_tasks(args.repo)]
|
|
715
|
+
if args.only:
|
|
716
|
+
wanted = set(args.only.split(","))
|
|
717
|
+
tasks = [t for t in tasks if t in wanted]
|
|
718
|
+
if args.limit:
|
|
719
|
+
tasks = tasks[:args.limit]
|
|
720
|
+
workers = [spec.split(":") for spec in args.sims.split(",")]
|
|
721
|
+
if len({udid for udid, _, _ in workers}) < len(workers):
|
|
722
|
+
raise SystemExit("--sims lists a simulator twice")
|
|
723
|
+
manifest = load_manifest(args.repo)
|
|
724
|
+
for udid, _, _ in workers: # before any worker starts: one shared simulator taints every worker's results
|
|
725
|
+
check_isolation(udid, manifest)
|
|
726
|
+
procs = []
|
|
727
|
+
for index, (udid, port, mjpeg) in enumerate(workers):
|
|
728
|
+
share = tasks[index::len(workers)]
|
|
729
|
+
if not share:
|
|
730
|
+
continue
|
|
731
|
+
command = [sys.executable, "-u", "-m", "mobile_agent.bench.iosworld", "run", "--repo", args.repo,
|
|
732
|
+
"--udid", udid, "--wda-url", f"http://127.0.0.1:{port}", "--mjpeg-url", f"http://127.0.0.1:{mjpeg}",
|
|
733
|
+
"--out", args.out, "--only", ",".join(share)]
|
|
734
|
+
for env_file in args.env_file or ():
|
|
735
|
+
command += ["--env-file", env_file]
|
|
736
|
+
command += (["--policy", args.policy, "--reasoning", args.reasoning]
|
|
737
|
+
+ (["--model", args.model] if args.model else []))
|
|
738
|
+
if args.wda_xctestrun:
|
|
739
|
+
command += ["--wda-xctestrun", args.wda_xctestrun]
|
|
740
|
+
if args.force:
|
|
741
|
+
command.append("--force")
|
|
742
|
+
log = open(pathlib.Path(args.out).with_suffix(f".worker{index}.log"), "w")
|
|
743
|
+
procs.append((subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT), log))
|
|
744
|
+
for proc, log in procs:
|
|
745
|
+
proc.wait()
|
|
746
|
+
log.close()
|
|
747
|
+
return max((proc.returncode for proc, _ in procs), default=0)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def cmd_judge(args):
|
|
751
|
+
"""iOSWorld's own judge, unmodified, in its own environment."""
|
|
752
|
+
load_envs(args.env_file) # the judge reads OPENAI_API_KEY from this process's environment
|
|
753
|
+
python = pathlib.Path(args.repo) / ".venv" / "bin" / "python"
|
|
754
|
+
command = [str(python if python.exists() else sys.executable), "scripts/judge_trajectories.py",
|
|
755
|
+
"--run-dir", str(pathlib.Path(args.run).resolve())]
|
|
756
|
+
if args.force:
|
|
757
|
+
command.append("--re-judge") # judge_trajectories.py has no --force
|
|
758
|
+
code = subprocess.call(command, cwd=args.repo, env=dict(os.environ))
|
|
759
|
+
# The judge's own summary counts passes by the official rule; this adds where its success flag differs.
|
|
760
|
+
from .iosworld_stats import load_runs, report_lines
|
|
761
|
+
for line in report_lines(load_runs([args.run])):
|
|
762
|
+
print(line)
|
|
763
|
+
return code
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def cmd_report(args):
|
|
767
|
+
"""One run folder: official passes (iOSWorld's every-criterion rule), the judge's own flag where
|
|
768
|
+
it disagrees, by category and difficulty, then time and cost."""
|
|
769
|
+
from .iosworld_stats import load_runs, report_lines
|
|
770
|
+
runs = load_runs([args.run])
|
|
771
|
+
for line in report_lines(runs):
|
|
772
|
+
print(line)
|
|
773
|
+
judged = [r for r in runs if r.judged]
|
|
774
|
+
for key in ("category", "difficulty"):
|
|
775
|
+
groups = {}
|
|
776
|
+
for r in judged:
|
|
777
|
+
groups.setdefault(getattr(r, key), []).append(r.official)
|
|
778
|
+
print(key + ": " + ", ".join(f"{k} {sum(v)}/{len(v)}" for k, v in sorted(groups.items(), key=str)))
|
|
779
|
+
from ..model_clients import cost_usd
|
|
780
|
+
rows = [json.loads(p.read_text(encoding="utf-8")) for p in sorted(pathlib.Path(args.run).glob("*/task.json"))]
|
|
781
|
+
seconds = [r["mobster"]["agent_seconds"] for r in rows]
|
|
782
|
+
costs = [r["mobster"]["cost_usd"] or cost_usd(r["mobster"].get("model"), r["mobster"].get("usage")) or 0.0
|
|
783
|
+
for r in rows]
|
|
784
|
+
if seconds:
|
|
785
|
+
print(f"agent seconds p50 {statistics.median(seconds):.1f}, mean {statistics.mean(seconds):.1f}; "
|
|
786
|
+
f"$/task mean {statistics.mean(costs):.4f}, total ${sum(costs):.2f}")
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _run_dirs(patterns):
|
|
790
|
+
"""Run folders from arguments that may be globs (a folder holds NNN-task/task.json)."""
|
|
791
|
+
dirs = []
|
|
792
|
+
for pattern in patterns:
|
|
793
|
+
for path in sorted(glob.glob(pattern)) or [pattern]:
|
|
794
|
+
if pathlib.Path(path).is_dir() and path not in dirs:
|
|
795
|
+
dirs.append(path)
|
|
796
|
+
return dirs
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def cmd_compare(args):
|
|
800
|
+
"""A paired A/B: arm A's run folders against arm B's, on the tasks both judged."""
|
|
801
|
+
from .iosworld_stats import compare, format_compare, load_overrides, load_runs, read_worker_logs
|
|
802
|
+
overrides = load_overrides(args.commit_labels)
|
|
803
|
+
arms = {arm: _run_dirs(getattr(args, arm)) for arm in ("a", "b")}
|
|
804
|
+
runs = {arm: load_runs(dirs, model=args.model, overrides=overrides) for arm, dirs in arms.items()}
|
|
805
|
+
logs = {}
|
|
806
|
+
for arm, dirs in arms.items():
|
|
807
|
+
merged = {"not_run": [], "tracebacks": 0}
|
|
808
|
+
for folder in dirs:
|
|
809
|
+
info = read_worker_logs(folder)
|
|
810
|
+
merged["not_run"] += info["not_run"]
|
|
811
|
+
merged["tracebacks"] += info["tracebacks"]
|
|
812
|
+
logs[arm] = merged
|
|
813
|
+
result = compare(runs["a"], runs["b"], n_boot=args.bootstrap, seed=args.seed)
|
|
814
|
+
if args.json:
|
|
815
|
+
print(json.dumps({**result, "logs": logs, "arms": arms}, indent=1, default=str))
|
|
816
|
+
return 0
|
|
817
|
+
print(f"A: {', '.join(arms['a'])}\nB: {', '.join(arms['b'])}\n")
|
|
818
|
+
print(format_compare(result, args.a_name, args.b_name, logs=logs))
|
|
819
|
+
return 0
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def cmd_estimate(args):
|
|
823
|
+
"""The official pass rate over any set of run folders, reweighted to iOSWorld's 133-task mix."""
|
|
824
|
+
from .iosworld_stats import FULL_MIX, estimate, format_estimate, load_runs
|
|
825
|
+
mix = dict(FULL_MIX)
|
|
826
|
+
if args.repo:
|
|
827
|
+
mix = {c: 0 for c in FULL_MIX}
|
|
828
|
+
for task in load_tasks(args.repo):
|
|
829
|
+
mix[task.get("category")] = mix.get(task.get("category"), 0) + 1
|
|
830
|
+
runs = load_runs(_run_dirs(args.runs), model=args.model)
|
|
831
|
+
for weighting in ("task", "run") if args.weighting == "both" else (args.weighting,):
|
|
832
|
+
result = estimate(runs, mix=mix, weighting=weighting, n_boot=args.bootstrap, seed=args.seed)
|
|
833
|
+
if args.json:
|
|
834
|
+
print(json.dumps(result, indent=1, default=str))
|
|
835
|
+
else:
|
|
836
|
+
print(format_estimate(result))
|
|
837
|
+
return 0
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def main(argv=None):
|
|
841
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
842
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
843
|
+
run = sub.add_parser("run")
|
|
844
|
+
run.add_argument("--repo", required=True)
|
|
845
|
+
run.add_argument("--udid", required=True)
|
|
846
|
+
run.add_argument("--wda-url", required=True)
|
|
847
|
+
run.add_argument("--mjpeg-url")
|
|
848
|
+
run.add_argument("--out", required=True)
|
|
849
|
+
run.add_argument("--only")
|
|
850
|
+
run.add_argument("--limit", type=int)
|
|
851
|
+
run.add_argument("--force", action="store_true")
|
|
852
|
+
run.add_argument("--env-file", action="append")
|
|
853
|
+
run.add_argument("--policy", choices=("frontier",), default="frontier")
|
|
854
|
+
run.add_argument("--model")
|
|
855
|
+
run.add_argument("--reasoning", default=SMART_CONFIG.reasoning)
|
|
856
|
+
run.add_argument("--wda-xctestrun", help="the simulator runner's .xctestrun: a runner that dies is restarted "
|
|
857
|
+
"(also MOBSTER_SIM_WDA_XCTESTRUN)")
|
|
858
|
+
pool = sub.add_parser("pool")
|
|
859
|
+
pool.add_argument("--repo", required=True)
|
|
860
|
+
pool.add_argument("--sims", required=True, help="UDID:WDA_PORT:MJPEG_PORT,...")
|
|
861
|
+
pool.add_argument("--out", required=True)
|
|
862
|
+
pool.add_argument("--only")
|
|
863
|
+
pool.add_argument("--limit", type=int)
|
|
864
|
+
pool.add_argument("--force", action="store_true")
|
|
865
|
+
pool.add_argument("--env-file", action="append")
|
|
866
|
+
pool.add_argument("--policy", choices=("frontier",), default="frontier")
|
|
867
|
+
pool.add_argument("--model")
|
|
868
|
+
pool.add_argument("--reasoning", default=SMART_CONFIG.reasoning)
|
|
869
|
+
pool.add_argument("--wda-xctestrun")
|
|
870
|
+
judge = sub.add_parser("judge")
|
|
871
|
+
judge.add_argument("--repo", required=True)
|
|
872
|
+
judge.add_argument("--run", required=True)
|
|
873
|
+
judge.add_argument("--force", action="store_true")
|
|
874
|
+
judge.add_argument("--env-file", action="append")
|
|
875
|
+
report = sub.add_parser("report")
|
|
876
|
+
report.add_argument("--run", required=True)
|
|
877
|
+
ab = sub.add_parser("compare", help="paired A/B over two sets of run folders (offline)")
|
|
878
|
+
ab.add_argument("--a", nargs="+", required=True, help="arm A's run folders (globs allowed)")
|
|
879
|
+
ab.add_argument("--b", nargs="+", required=True, help="arm B's run folders (globs allowed)")
|
|
880
|
+
ab.add_argument("--a-name", default="A")
|
|
881
|
+
ab.add_argument("--b-name", default="B")
|
|
882
|
+
ab.add_argument("--model", help="only runs of this model")
|
|
883
|
+
ab.add_argument("--commit-labels", help='JSON {task: {control label: true|false}}: hand labels for "requested"')
|
|
884
|
+
ab.add_argument("--bootstrap", type=int, default=10000)
|
|
885
|
+
ab.add_argument("--seed", type=int, default=20260926)
|
|
886
|
+
ab.add_argument("--json", action="store_true")
|
|
887
|
+
pooled = sub.add_parser("estimate", help="official pass rate reweighted to the 133-task mix (offline)")
|
|
888
|
+
pooled.add_argument("runs", nargs="+", help="run folders (globs allowed)")
|
|
889
|
+
pooled.add_argument("--model", help="only runs of this model, e.g. gpt-5.6-sol")
|
|
890
|
+
pooled.add_argument("--repo", help="iOSWorld checkout: take the category mix from its tasks.json")
|
|
891
|
+
pooled.add_argument("--weighting", choices=("task", "run", "both"), default="both")
|
|
892
|
+
pooled.add_argument("--bootstrap", type=int, default=10000)
|
|
893
|
+
pooled.add_argument("--seed", type=int, default=20260926)
|
|
894
|
+
pooled.add_argument("--json", action="store_true")
|
|
895
|
+
args = parser.parse_args(argv)
|
|
896
|
+
return {"run": cmd_run, "pool": cmd_pool, "judge": cmd_judge, "report": cmd_report, "compare": cmd_compare,
|
|
897
|
+
"estimate": cmd_estimate}[args.command](args) or 0
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
if __name__ == "__main__":
|
|
901
|
+
sys.exit(main())
|