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,798 @@
|
|
|
1
|
+
"""A run's tape: every call the agent made to the phone and to its model, in order, with what came back, so the run
|
|
2
|
+
can be replayed on today's code with no phone and no model.
|
|
3
|
+
|
|
4
|
+
The rule is DeepSeek Harness's, applied to a phone: everything the agent saw is on the tape. Recording wraps the
|
|
5
|
+
driver (``TapeDriver``) and the model client (``TapeClient``) at the two boundaries the loop already has. ``replay``
|
|
6
|
+
serves the tape back (``ReplayDriver``, ``ReplayClient``) on a virtual clock and runs FrontierAgent on the same
|
|
7
|
+
request. Code that would act differently on a recorded run stops the replay at the first action the recording didn't
|
|
8
|
+
send (a ``Divergence``), with the turn it happened in. Code that acts the same replays the whole run in seconds, for
|
|
9
|
+
free. That makes a recorded run a regression test for the loop, its gates and its target resolution.
|
|
10
|
+
|
|
11
|
+
Format: ``tape.jsonl``, one JSON object a line.
|
|
12
|
+
|
|
13
|
+
- ``{"kind": "header", "version", "request", "apps", "model", "frontier", "env", "capabilities", "t", "hooks"?}``
|
|
14
|
+
(``hooks``: what the recorded run had that a replay doesn't: approvals, steering, tools)
|
|
15
|
+
- ``{"kind": "call", "on", "name", "args", "t0", "t1", "result" | "error"}``: a driver method (``on`` is ``driver``, or
|
|
16
|
+
``driver.frame_clock`` and so on for an object the driver hands out)
|
|
17
|
+
- ``{"kind": "value", "on", "name", "value"}``: a driver attribute the agent read
|
|
18
|
+
- ``{"kind": "model", "purpose", "prompt", "t0", "t1", "reply" | "error", "usage"}``: ``prompt`` is a hash of the
|
|
19
|
+
prompt's text (images left out), so a replay also says whether the model would have been asked the same thing
|
|
20
|
+
- ``{"kind": "end", "status", "answer", "steps", "actions"}``
|
|
21
|
+
|
|
22
|
+
What it holds: the request and the screens as data (labels, values, frames), the model's replies. A picture is kept
|
|
23
|
+
as its size only: a replayed model doesn't look. Benchmarks on simulators write it (bench/iosworld.py). A run in the
|
|
24
|
+
app or the terminal writes it too, when traces are on (traces.py): masked by the secret filter as it is written, in
|
|
25
|
+
a private file beside the run's frames.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import dataclasses
|
|
29
|
+
import gzip
|
|
30
|
+
import hashlib
|
|
31
|
+
import importlib
|
|
32
|
+
import inspect
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import shutil
|
|
36
|
+
import sys
|
|
37
|
+
import threading
|
|
38
|
+
import time
|
|
39
|
+
import zipfile
|
|
40
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
41
|
+
from contextlib import contextmanager
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from unittest import mock
|
|
44
|
+
|
|
45
|
+
from .. import state as state_module
|
|
46
|
+
from ..drivers import Driver
|
|
47
|
+
from ..transport import TransportError, error_name
|
|
48
|
+
from ..wda.guards import DriverRejection
|
|
49
|
+
|
|
50
|
+
VERSION = 1
|
|
51
|
+
NAME = "tape.jsonl" # gzipped to tape.jsonl.gz when the run ends
|
|
52
|
+
# Calls that change the phone: a replay sends these only in the recorded order, with the same arguments. Every other
|
|
53
|
+
# call (a read, a settle, tune, recover) is served from the tape by name.
|
|
54
|
+
ACTS = frozenset({"execute", "tap_point", "long_press", "set_value", "write_text", "replace_text", "clear_text",
|
|
55
|
+
"relaunch_app", "drag_next", "dismiss_at", "open_url", "launch"})
|
|
56
|
+
BASE = ("observe", "execute", "close", "can_type", "last_image")
|
|
57
|
+
_PIXEL = []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def pixel():
|
|
61
|
+
"""A small grey JPEG: what a replay hands over for any picture the recording took."""
|
|
62
|
+
if not _PIXEL:
|
|
63
|
+
import io
|
|
64
|
+
|
|
65
|
+
from PIL import Image
|
|
66
|
+
out = io.BytesIO()
|
|
67
|
+
Image.new("RGB", (8, 8), (128, 128, 128)).save(out, "JPEG")
|
|
68
|
+
_PIXEL.append(out.getvalue())
|
|
69
|
+
return _PIXEL[0]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
DATACLASSES = {cls.__name__: cls for cls in (state_module.Element, state_module.Snapshot)}
|
|
73
|
+
# Environment kept with the header: the switches that steer the loop, never a key.
|
|
74
|
+
SECRET_WORDS = ("KEY", "TOKEN", "SECRET", "PASSWORD")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Divergence(BaseException):
|
|
78
|
+
"""The replay asked for something the tape doesn't have: an action the recording didn't send, or a read past its
|
|
79
|
+
end. A BaseException, so the loop's own error handling can't turn it into a run that "failed"."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, why, expected=None, got=None, index=None):
|
|
82
|
+
self.why, self.expected, self.got, self.index = why, expected, got, index
|
|
83
|
+
super().__init__(why)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def encode(value, depth=0):
|
|
87
|
+
"""``value`` as JSON: dataclasses by name, bytes by size, an exception by type and message."""
|
|
88
|
+
if depth > 12:
|
|
89
|
+
return None
|
|
90
|
+
if isinstance(value, str) and value.startswith("data:image/"):
|
|
91
|
+
return {"__t": "bytes", "n": len(value)} # a picture as a data URL: its size only, like bytes
|
|
92
|
+
if value is None or isinstance(value, (bool, int, str)):
|
|
93
|
+
return value
|
|
94
|
+
if isinstance(value, float):
|
|
95
|
+
return value if value == value and abs(value) != float("inf") else None
|
|
96
|
+
if isinstance(value, (bytes, bytearray)):
|
|
97
|
+
return {"__t": "bytes", "n": len(value)}
|
|
98
|
+
if dataclasses.is_dataclass(value) and not isinstance(value, type) and type(value).__name__ in DATACLASSES:
|
|
99
|
+
return {"__t": type(value).__name__,
|
|
100
|
+
**{f.name: encode(getattr(value, f.name), depth + 1) for f in dataclasses.fields(value)}}
|
|
101
|
+
if isinstance(value, dict):
|
|
102
|
+
return {str(k): encode(v, depth + 1) for k, v in value.items()}
|
|
103
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
104
|
+
return [encode(v, depth + 1) for v in value]
|
|
105
|
+
return {"__t": "object", "type": type(value).__name__}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _tupled(value):
|
|
109
|
+
return tuple(_tupled(v) for v in value) if isinstance(value, list) else value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def decode(value):
|
|
113
|
+
"""What ``encode`` wrote, back as the objects the loop reads (lists stay lists outside dataclasses)."""
|
|
114
|
+
if isinstance(value, list):
|
|
115
|
+
return [decode(v) for v in value]
|
|
116
|
+
if not isinstance(value, dict):
|
|
117
|
+
return value
|
|
118
|
+
kind = value.get("__t")
|
|
119
|
+
if kind == "bytes":
|
|
120
|
+
return pixel()
|
|
121
|
+
if kind == "object":
|
|
122
|
+
return None
|
|
123
|
+
if kind in DATACLASSES:
|
|
124
|
+
cls = DATACLASSES[kind]
|
|
125
|
+
fields = {}
|
|
126
|
+
for f in dataclasses.fields(cls):
|
|
127
|
+
if f.name not in value:
|
|
128
|
+
continue
|
|
129
|
+
item = decode(value[f.name])
|
|
130
|
+
fields[f.name] = item if f.name == "elements" or not isinstance(item, list) else _tupled(item)
|
|
131
|
+
return cls(**fields)
|
|
132
|
+
return {k: decode(v) for k, v in value.items()}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def encode_error(error):
|
|
136
|
+
out = {"type": type(error).__name__, "module": type(error).__module__, "message": str(error)[:500]}
|
|
137
|
+
if isinstance(error, DriverRejection):
|
|
138
|
+
out["code"] = error.code
|
|
139
|
+
elif isinstance(error, TransportError) and (error.status is not None or error.wda_error is not None):
|
|
140
|
+
out.update(status=error.status, wda_error=error.wda_error)
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def decode_error(data):
|
|
145
|
+
"""The recorded exception, as close to its class as can be rebuilt; else a RuntimeError naming it."""
|
|
146
|
+
if data.get("type") == "DriverRejection":
|
|
147
|
+
return DriverRejection(data.get("code"))
|
|
148
|
+
module, name, message = data.get("module") or "", data.get("type") or "", data.get("message") or ""
|
|
149
|
+
if module == "builtins" or module.startswith("mobile_agent."):
|
|
150
|
+
try:
|
|
151
|
+
cls = getattr(importlib.import_module(module), name)
|
|
152
|
+
if isinstance(cls, type) and issubclass(cls, Exception):
|
|
153
|
+
error = cls(message)
|
|
154
|
+
if isinstance(error, TransportError):
|
|
155
|
+
# Set after building, since a subclass's own __init__ (MirroringError, CableBtError) takes
|
|
156
|
+
# neither. A legacy record has neither, and reads as unproven.
|
|
157
|
+
status = data.get("status")
|
|
158
|
+
error.status = status if type(status) is int else None
|
|
159
|
+
error.wda_error = error_name({"error": data.get("wda_error")})
|
|
160
|
+
return error
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
return RuntimeError(f"{name}: {message}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _element_key(value):
|
|
167
|
+
return {"label": value.label, "role": value.role} if isinstance(value, state_module.Element) else value
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def act_key(name, args, kwargs):
|
|
171
|
+
"""What makes two calls the same action: the method, its operation and target (by label and role), its text and
|
|
172
|
+
flags. Snapshots and timeouts are left out: the loop's own reads decide those."""
|
|
173
|
+
parts = [name]
|
|
174
|
+
for value in list(args) + [kwargs[k] for k in sorted(kwargs) if k not in ("timeout", "purpose")]:
|
|
175
|
+
if isinstance(value, state_module.Snapshot):
|
|
176
|
+
continue
|
|
177
|
+
if isinstance(value, float):
|
|
178
|
+
value = round(value, 3)
|
|
179
|
+
parts.append(encode(_element_key(value)))
|
|
180
|
+
return parts
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def prompt_hash(messages):
|
|
184
|
+
"""A hash of the prompt's text parts, in order (images left out)."""
|
|
185
|
+
digest = hashlib.sha256()
|
|
186
|
+
for message in messages or ():
|
|
187
|
+
content = message.get("content")
|
|
188
|
+
parts = [content] if isinstance(content, str) else content or ()
|
|
189
|
+
for part in parts:
|
|
190
|
+
text = part if isinstance(part, str) else part.get("text") if isinstance(part, dict) else None
|
|
191
|
+
if isinstance(text, str):
|
|
192
|
+
digest.update(text.encode("utf-8", "replace"))
|
|
193
|
+
return digest.hexdigest()[:16]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def purpose_of(schema):
|
|
197
|
+
"""A model call's purpose: its reply schema's property names (a decision has ``actions``, a contract ``items``)."""
|
|
198
|
+
return ",".join(sorted((schema or {}).get("properties") or {}))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def switches(env=None):
|
|
202
|
+
env = os.environ if env is None else env
|
|
203
|
+
return {k: v for k, v in sorted(env.items())
|
|
204
|
+
if k.startswith("MOBSTER_") and not any(word in k for word in SECRET_WORDS)}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class Tape:
|
|
208
|
+
"""The writer: one JSONL file, appended under a lock (the verify read and the contract compile run on their own
|
|
209
|
+
threads). A write that fails stops the tape, never the run. The file is private (0600) and stops growing at
|
|
210
|
+
MAX_BYTES.
|
|
211
|
+
|
|
212
|
+
``scrub(entry)`` rewrites each entry before it is written (traces.py passes the secret filter, which costs tens of
|
|
213
|
+
milliseconds on a screen). With one, the entries go to a single worker thread in order, so the agent never waits
|
|
214
|
+
for it, and ``close`` waits for the queue; without one, each write is made before the call returns, as a
|
|
215
|
+
benchmark's tape needs to survive a crash."""
|
|
216
|
+
|
|
217
|
+
MAX_BYTES = 32_000_000
|
|
218
|
+
|
|
219
|
+
def __init__(self, path, scrub=None):
|
|
220
|
+
self.path = Path(path)
|
|
221
|
+
self.scrub = scrub
|
|
222
|
+
self._worker = ThreadPoolExecutor(max_workers=1, thread_name_prefix="mobster-tape") if scrub else None
|
|
223
|
+
self._lock = threading.Lock()
|
|
224
|
+
self._file = None
|
|
225
|
+
self._screens = set()
|
|
226
|
+
self._size = 0
|
|
227
|
+
self.broken = None
|
|
228
|
+
|
|
229
|
+
def _shared(self, result):
|
|
230
|
+
"""A screen read again unchanged is written once: later reads refer to it by hash, with their own times."""
|
|
231
|
+
if not (isinstance(result, dict) and result.get("__t") == "Snapshot"):
|
|
232
|
+
return result
|
|
233
|
+
times = {k: result.get(k) for k in ("captured_at", "read_started")}
|
|
234
|
+
body = {k: v for k, v in result.items() if k not in times}
|
|
235
|
+
digest = hashlib.sha256(json.dumps(body, sort_keys=True, ensure_ascii=False).encode()).hexdigest()[:20]
|
|
236
|
+
with self._lock:
|
|
237
|
+
seen = digest in self._screens
|
|
238
|
+
self._screens.add(digest)
|
|
239
|
+
return {"__t": "ref", "h": digest, **times} if seen else {**result, "__h": digest}
|
|
240
|
+
|
|
241
|
+
def write(self, entry):
|
|
242
|
+
if self._worker is None:
|
|
243
|
+
self._write(entry)
|
|
244
|
+
return
|
|
245
|
+
try:
|
|
246
|
+
self._worker.submit(self._write, entry)
|
|
247
|
+
except RuntimeError:
|
|
248
|
+
pass # a late call (a hedged one that lost) after the tape ended
|
|
249
|
+
|
|
250
|
+
def _write(self, entry):
|
|
251
|
+
if self.broken:
|
|
252
|
+
return
|
|
253
|
+
try:
|
|
254
|
+
if self.scrub is not None:
|
|
255
|
+
entry = self.scrub(entry)
|
|
256
|
+
if entry.get("kind") == "call" and "result" in entry:
|
|
257
|
+
entry = {**entry, "result": self._shared(entry["result"])}
|
|
258
|
+
line = json.dumps(entry, ensure_ascii=False, separators=(",", ":"))
|
|
259
|
+
with self._lock:
|
|
260
|
+
self._size += len(line) + 1
|
|
261
|
+
if self._size > self.MAX_BYTES:
|
|
262
|
+
self.broken = "the tape passed its size limit"
|
|
263
|
+
return
|
|
264
|
+
if self._file is None:
|
|
265
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
266
|
+
# a line a write: a crash keeps the tape
|
|
267
|
+
descriptor = os.open(self.path, os.O_WRONLY | os.O_CREAT | os.O_APPEND | os.O_NOFOLLOW, 0o600)
|
|
268
|
+
self._file = os.fdopen(descriptor, "a", encoding="utf-8", buffering=1)
|
|
269
|
+
self._file.write(line + "\n")
|
|
270
|
+
except Exception as error: # noqa: BLE001
|
|
271
|
+
self.broken = f"{type(error).__name__}: {error}"
|
|
272
|
+
|
|
273
|
+
def header(self, *, request, apps, model, frontier, driver, env=None, hooks=()):
|
|
274
|
+
kinds = capabilities(driver)
|
|
275
|
+
values = {name: encode(getattr(driver, name, None)) for name, kind in kinds.items() if kind == "value"}
|
|
276
|
+
self.write({"kind": "header", "version": VERSION, "request": request, "apps": dict(apps or {}),
|
|
277
|
+
"model": model, "frontier": encode(frontier), "env": switches(env), "capabilities": kinds,
|
|
278
|
+
"values": values, "t": time.monotonic(), **({"hooks": sorted(hooks)} if hooks else {})})
|
|
279
|
+
|
|
280
|
+
def end(self, outcome):
|
|
281
|
+
"""The run's end, then the tape gzipped (tape.jsonl.gz): a tape that never ends stays plain."""
|
|
282
|
+
outcome = outcome or {}
|
|
283
|
+
self.write({"kind": "end", "status": outcome.get("status"), "answer": encode(outcome.get("answer")),
|
|
284
|
+
"steps": outcome.get("steps"), "actions": outcome.get("actions")})
|
|
285
|
+
self.close()
|
|
286
|
+
if self.broken or not self.path.exists():
|
|
287
|
+
return
|
|
288
|
+
try:
|
|
289
|
+
descriptor = os.open(str(self.path) + ".gz", os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW, 0o600)
|
|
290
|
+
with open(self.path, "rb") as plain, os.fdopen(descriptor, "wb") as raw, \
|
|
291
|
+
gzip.GzipFile(fileobj=raw, mode="wb", compresslevel=6) as packed:
|
|
292
|
+
shutil.copyfileobj(plain, packed)
|
|
293
|
+
self.path.unlink()
|
|
294
|
+
except OSError as error:
|
|
295
|
+
self.broken = f"{type(error).__name__}: {error}"
|
|
296
|
+
|
|
297
|
+
def close(self):
|
|
298
|
+
if self._worker is not None:
|
|
299
|
+
self._worker.shutdown(wait=True)
|
|
300
|
+
with self._lock:
|
|
301
|
+
if self._file is not None:
|
|
302
|
+
self._file.close()
|
|
303
|
+
self._file = None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def capabilities(driver):
|
|
307
|
+
"""{name: "call" | "value" | "object"} for the base contract and every Driver.OPTIONAL name ``driver`` has."""
|
|
308
|
+
out = {}
|
|
309
|
+
for name in (*BASE, *Driver.OPTIONAL):
|
|
310
|
+
try:
|
|
311
|
+
value = getattr(driver, name)
|
|
312
|
+
except Exception: # noqa: BLE001
|
|
313
|
+
continue
|
|
314
|
+
# Present means the attribute exists, set or not: settled_by is None until the first settle.
|
|
315
|
+
out[name] = ("call" if callable(value) else "object" if name == "frame_clock" and value is not None
|
|
316
|
+
else "value")
|
|
317
|
+
return out
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class _Recorded:
|
|
321
|
+
"""A proxy that writes each call and attribute read of ``inner`` to the tape."""
|
|
322
|
+
|
|
323
|
+
_OBJECTS = frozenset({"frame_clock", "video"})
|
|
324
|
+
|
|
325
|
+
def __init__(self, inner, tape, on):
|
|
326
|
+
object.__setattr__(self, "_inner", inner)
|
|
327
|
+
object.__setattr__(self, "_tape", tape)
|
|
328
|
+
object.__setattr__(self, "_on", on)
|
|
329
|
+
|
|
330
|
+
def __getattr__(self, name):
|
|
331
|
+
value = getattr(self._inner, name)
|
|
332
|
+
if name.startswith("__"):
|
|
333
|
+
return value
|
|
334
|
+
if callable(value) and not isinstance(value, type):
|
|
335
|
+
return self._call(name, value)
|
|
336
|
+
if name in self._OBJECTS and value is not None:
|
|
337
|
+
return _Recorded(value, self._tape, f"{self._on}.{name}")
|
|
338
|
+
self._tape.write({"kind": "value", "on": self._on, "name": name, "value": encode(value)})
|
|
339
|
+
return value
|
|
340
|
+
|
|
341
|
+
def __setattr__(self, name, value):
|
|
342
|
+
setattr(self._inner, name, value)
|
|
343
|
+
|
|
344
|
+
def _call(self, name, method):
|
|
345
|
+
tape, on = self._tape, self._on
|
|
346
|
+
|
|
347
|
+
def call(*args, **kwargs):
|
|
348
|
+
entry = {"kind": "call", "on": on, "name": name, "args": act_key(name, args, kwargs),
|
|
349
|
+
"thread": threading.current_thread().name, "t0": time.monotonic()}
|
|
350
|
+
try:
|
|
351
|
+
result = method(*args, **kwargs)
|
|
352
|
+
except BaseException as error:
|
|
353
|
+
entry.update(t1=time.monotonic(), error=encode_error(error))
|
|
354
|
+
tape.write(entry)
|
|
355
|
+
raise
|
|
356
|
+
entry.update(t1=time.monotonic(), result=encode(result))
|
|
357
|
+
tape.write(entry)
|
|
358
|
+
return result
|
|
359
|
+
|
|
360
|
+
return call
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class TapeDriver(_Recorded):
|
|
364
|
+
"""``driver`` with every call and read on ``tape``."""
|
|
365
|
+
|
|
366
|
+
def __init__(self, driver, tape):
|
|
367
|
+
super().__init__(driver, tape, "driver")
|
|
368
|
+
# Declared on the proxy: reading it must not write a tape entry (the header holds the set).
|
|
369
|
+
object.__setattr__(self, "capabilities", driver.capabilities)
|
|
370
|
+
|
|
371
|
+
# The abstract three, spelled out so the proxy is a complete driver even to code that looks them up on the class.
|
|
372
|
+
def observe(self, timeout=10):
|
|
373
|
+
return self.__getattr__("observe")(timeout=timeout)
|
|
374
|
+
|
|
375
|
+
def execute(self, operation, target, snapshot, text=None, timeout=10):
|
|
376
|
+
return self.__getattr__("execute")(operation, target, snapshot, text=text, timeout=timeout)
|
|
377
|
+
|
|
378
|
+
def close(self):
|
|
379
|
+
return self._inner.close()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class TapeClient:
|
|
383
|
+
"""The model client with every call on ``tape``: its purpose, a hash of the prompt's text, the reply or the
|
|
384
|
+
error, and the client's usage after it (hedges included), so a replay's cost is the recording's."""
|
|
385
|
+
|
|
386
|
+
def __init__(self, client, tape):
|
|
387
|
+
object.__setattr__(self, "_inner", client)
|
|
388
|
+
object.__setattr__(self, "_tape", tape)
|
|
389
|
+
|
|
390
|
+
def __getattr__(self, name):
|
|
391
|
+
return getattr(self._inner, name)
|
|
392
|
+
|
|
393
|
+
def __setattr__(self, name, value):
|
|
394
|
+
setattr(self._inner, name, value)
|
|
395
|
+
|
|
396
|
+
def complete(self, messages, schema, **kwargs):
|
|
397
|
+
entry = {"kind": "model", "purpose": purpose_of(schema), "prompt": prompt_hash(messages),
|
|
398
|
+
"t0": time.monotonic()}
|
|
399
|
+
try:
|
|
400
|
+
reply, usage = self._inner.complete(messages, schema, **kwargs)
|
|
401
|
+
except BaseException as error:
|
|
402
|
+
entry.update(t1=time.monotonic(), error=encode_error(error),
|
|
403
|
+
usage=encode(dict(getattr(self._inner, "usage", None) or {})))
|
|
404
|
+
self._tape.write(entry)
|
|
405
|
+
raise
|
|
406
|
+
entry.update(t1=time.monotonic(), reply=encode(reply), call_usage=encode(usage),
|
|
407
|
+
usage=encode(dict(getattr(self._inner, "usage", None) or {})))
|
|
408
|
+
self._tape.write(entry)
|
|
409
|
+
return reply, usage
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
BUNDLE_TAPE_LIMIT = 200_000_000 # unpacked bytes: a bundle is a person's own, but a zip can lie about its size
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def read(path):
|
|
416
|
+
"""The tape's text from a tape.jsonl, a tape.jsonl.gz, a folder holding one, or a trace bundle (traces.py: a zip
|
|
417
|
+
with tape.jsonl.gz in it). ValueError when a bundle holds no tape."""
|
|
418
|
+
path = Path(path)
|
|
419
|
+
if path.is_dir():
|
|
420
|
+
path = path / NAME if (path / NAME).exists() else path / (NAME + ".gz")
|
|
421
|
+
if path.suffix == ".zip":
|
|
422
|
+
try:
|
|
423
|
+
with zipfile.ZipFile(path) as archive:
|
|
424
|
+
info = archive.getinfo(NAME + ".gz")
|
|
425
|
+
if info.file_size > BUNDLE_TAPE_LIMIT:
|
|
426
|
+
raise ValueError(f"{path}: the bundle's tape is too large")
|
|
427
|
+
packed = archive.read(info)
|
|
428
|
+
except (KeyError, zipfile.BadZipFile):
|
|
429
|
+
raise ValueError(f"{path} holds no tape (the task ran without MOBSTER_EXPERIMENTAL_TRACES=1)") from None
|
|
430
|
+
return gzip.decompress(packed).decode("utf-8")
|
|
431
|
+
if path.suffix == ".gz":
|
|
432
|
+
return gzip.decompress(path.read_bytes()).decode("utf-8")
|
|
433
|
+
return path.read_text(encoding="utf-8")
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def load(path):
|
|
437
|
+
"""The tape's entries; ValueError when it has no header."""
|
|
438
|
+
path = Path(path)
|
|
439
|
+
text = read(path)
|
|
440
|
+
entries = [json.loads(line) for line in text.splitlines() if line.strip()]
|
|
441
|
+
if not entries or entries[0].get("kind") != "header":
|
|
442
|
+
raise ValueError(f"{path} is not a tape (no header)")
|
|
443
|
+
screens = {}
|
|
444
|
+
for entry in entries:
|
|
445
|
+
result = entry.get("result")
|
|
446
|
+
if isinstance(result, dict) and result.get("__t") == "Snapshot" and "__h" in result:
|
|
447
|
+
screens[result.pop("__h")] = result
|
|
448
|
+
elif isinstance(result, dict) and result.get("__t") == "ref":
|
|
449
|
+
shared = screens.get(result.get("h"))
|
|
450
|
+
if shared is None:
|
|
451
|
+
raise ValueError(f"{path}: a screen refers to one the tape doesn't hold")
|
|
452
|
+
entry["result"] = {**shared, "captured_at": result.get("captured_at"),
|
|
453
|
+
"read_started": result.get("read_started")}
|
|
454
|
+
return entries
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class Clock:
|
|
458
|
+
"""The replay's time: the recording's monotonic clock, moved to each served call's end; sleeping moves it."""
|
|
459
|
+
|
|
460
|
+
def __init__(self, start):
|
|
461
|
+
self.now = float(start)
|
|
462
|
+
self._lock = threading.Lock()
|
|
463
|
+
|
|
464
|
+
def reach(self, t):
|
|
465
|
+
if isinstance(t, (int, float)):
|
|
466
|
+
with self._lock:
|
|
467
|
+
self.now = max(self.now, float(t))
|
|
468
|
+
|
|
469
|
+
def monotonic(self):
|
|
470
|
+
return self.now
|
|
471
|
+
|
|
472
|
+
def sleep(self, seconds):
|
|
473
|
+
with self._lock:
|
|
474
|
+
self.now += max(0.0, float(seconds or 0))
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class _VirtualTime:
|
|
478
|
+
"""The ``time`` module as the loop sees it in a replay: monotonic and sleep from the Clock, the rest real."""
|
|
479
|
+
|
|
480
|
+
def __init__(self, clock):
|
|
481
|
+
self._clock = clock
|
|
482
|
+
|
|
483
|
+
def monotonic(self):
|
|
484
|
+
return self._clock.monotonic()
|
|
485
|
+
|
|
486
|
+
def sleep(self, seconds):
|
|
487
|
+
self._clock.sleep(seconds)
|
|
488
|
+
|
|
489
|
+
def __getattr__(self, name):
|
|
490
|
+
return getattr(time, name)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
class Player:
|
|
494
|
+
"""The tape served back: acts in order and compared, reads by name in order, values by name (the last one again
|
|
495
|
+
once they run out), model replies by purpose in order."""
|
|
496
|
+
|
|
497
|
+
def __init__(self, entries):
|
|
498
|
+
self.header = entries[0]
|
|
499
|
+
self.clock = Clock(self.header.get("t") or 0.0)
|
|
500
|
+
self.acts, self.reads, self.values, self.models = [], {}, {}, {}
|
|
501
|
+
self.decisions = []
|
|
502
|
+
self.end = next((e for e in entries if e.get("kind") == "end"), None)
|
|
503
|
+
self.recorded_kinds = {}
|
|
504
|
+
for entry in entries[1:]:
|
|
505
|
+
kind = entry.get("kind")
|
|
506
|
+
if kind == "call":
|
|
507
|
+
key = (entry["on"], entry["name"])
|
|
508
|
+
self.recorded_kinds[key] = "call"
|
|
509
|
+
if self._is_act(entry["name"], entry.get("args")):
|
|
510
|
+
self.acts.append(entry)
|
|
511
|
+
else:
|
|
512
|
+
self.reads.setdefault(key, []).append(entry)
|
|
513
|
+
elif kind == "value":
|
|
514
|
+
key = (entry["on"], entry["name"])
|
|
515
|
+
self.recorded_kinds.setdefault(key, "value")
|
|
516
|
+
self.values.setdefault(key, []).append(entry)
|
|
517
|
+
elif kind == "model":
|
|
518
|
+
self.models.setdefault(entry.get("purpose") or "", []).append(entry)
|
|
519
|
+
if "actions" in (entry.get("purpose") or "").split(","):
|
|
520
|
+
self.decisions.append(entry.get("t1") or 0) # when each decision came back, for ``steps_of``
|
|
521
|
+
self.acts_done = 0
|
|
522
|
+
self.prompts_same = self.prompts_seen = 0
|
|
523
|
+
self._served_values = {}
|
|
524
|
+
self._lock = threading.Lock()
|
|
525
|
+
|
|
526
|
+
@staticmethod
|
|
527
|
+
def _is_act(name, args):
|
|
528
|
+
if name in ACTS:
|
|
529
|
+
return True
|
|
530
|
+
# A raw WebDriverAgent request acts unless it's a GET.
|
|
531
|
+
return name == "call" and bool(args) and len(args) > 1 and str(args[1]).upper() != "GET"
|
|
532
|
+
|
|
533
|
+
def _answer(self, entry):
|
|
534
|
+
self.clock.reach(entry.get("t1"))
|
|
535
|
+
if "error" in entry:
|
|
536
|
+
raise decode_error(entry["error"])
|
|
537
|
+
return decode(entry.get("result"))
|
|
538
|
+
|
|
539
|
+
def call(self, on, name, args, kwargs):
|
|
540
|
+
key = act_key(name, args, kwargs)
|
|
541
|
+
with self._lock:
|
|
542
|
+
if self._is_act(name, key):
|
|
543
|
+
index = self.acts_done
|
|
544
|
+
if index >= len(self.acts):
|
|
545
|
+
raise Divergence("an action the recording never sent", None, key, index)
|
|
546
|
+
expected = self.acts[index]
|
|
547
|
+
if expected["on"] != on or expected["args"] != json.loads(json.dumps(key)):
|
|
548
|
+
raise Divergence("a different action", expected["args"], key, index)
|
|
549
|
+
self.acts_done += 1
|
|
550
|
+
entry = expected
|
|
551
|
+
else:
|
|
552
|
+
queue = self.reads.get((on, name))
|
|
553
|
+
if not queue:
|
|
554
|
+
raise Divergence(f"a read the recording didn't make ({on}.{name})", None, key, self.acts_done)
|
|
555
|
+
entry = queue.pop(0)
|
|
556
|
+
return self._answer(entry)
|
|
557
|
+
|
|
558
|
+
def value(self, on, name):
|
|
559
|
+
with self._lock:
|
|
560
|
+
queue = self.values.get((on, name))
|
|
561
|
+
if queue:
|
|
562
|
+
self._served_values[(on, name)] = queue.pop(0)
|
|
563
|
+
entry = self._served_values.get((on, name))
|
|
564
|
+
if entry is None:
|
|
565
|
+
raise AttributeError(name)
|
|
566
|
+
return decode(entry.get("value"))
|
|
567
|
+
|
|
568
|
+
def model(self, messages, schema):
|
|
569
|
+
purpose = purpose_of(schema)
|
|
570
|
+
with self._lock:
|
|
571
|
+
queue = self.models.get(purpose)
|
|
572
|
+
if not queue:
|
|
573
|
+
raise Divergence(f"a model call the recording didn't make ({purpose or 'no schema'})", None,
|
|
574
|
+
purpose, self.acts_done)
|
|
575
|
+
entry = queue.pop(0)
|
|
576
|
+
self.prompts_seen += 1
|
|
577
|
+
self.prompts_same += prompt_hash(messages) == entry.get("prompt")
|
|
578
|
+
self.clock.reach(entry.get("t1"))
|
|
579
|
+
if "error" in entry:
|
|
580
|
+
raise decode_error(entry["error"])
|
|
581
|
+
return decode(entry.get("reply")), decode(entry.get("call_usage") or {}), decode(entry.get("usage") or {})
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
class _Served:
|
|
585
|
+
"""The replayed side of an object the driver handed out (``on`` names it)."""
|
|
586
|
+
|
|
587
|
+
def __init__(self, player, on):
|
|
588
|
+
object.__setattr__(self, "_player", player)
|
|
589
|
+
object.__setattr__(self, "_on", on)
|
|
590
|
+
|
|
591
|
+
def __getattr__(self, name):
|
|
592
|
+
if name.startswith("__"):
|
|
593
|
+
raise AttributeError(name)
|
|
594
|
+
on, player = self._on, self._player
|
|
595
|
+
kind = player.recorded_kinds.get((on, name))
|
|
596
|
+
if kind == "call":
|
|
597
|
+
return lambda *args, **kwargs: player.call(on, name, args, kwargs)
|
|
598
|
+
if (on, name) in player.values or (on, name) in player._served_values:
|
|
599
|
+
return player.value(on, name)
|
|
600
|
+
if any(key[0] == f"{on}.{name}" for key in player.recorded_kinds):
|
|
601
|
+
return _Served(player, f"{on}.{name}")
|
|
602
|
+
raise AttributeError(name)
|
|
603
|
+
|
|
604
|
+
def __setattr__(self, name, value):
|
|
605
|
+
pass # what the loop sets on a driver changes nothing a replay serves
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
class ReplayDriver(_Served):
|
|
609
|
+
"""The recorded driver: the capabilities it had, its reads and its answers."""
|
|
610
|
+
|
|
611
|
+
def __init__(self, player):
|
|
612
|
+
super().__init__(player, "driver")
|
|
613
|
+
kinds = dict(player.header.get("capabilities") or {})
|
|
614
|
+
object.__setattr__(self, "_kinds", kinds)
|
|
615
|
+
object.__setattr__(self, "capabilities", frozenset(kinds).intersection(Driver.OPTIONAL))
|
|
616
|
+
|
|
617
|
+
def __getattr__(self, name):
|
|
618
|
+
kind = self._kinds.get(name)
|
|
619
|
+
if kind is None and not name.startswith("__"):
|
|
620
|
+
raise AttributeError(name)
|
|
621
|
+
if kind == "call":
|
|
622
|
+
player = self._player
|
|
623
|
+
return lambda *args, **kwargs: player.call("driver", name, args, kwargs)
|
|
624
|
+
if kind == "object":
|
|
625
|
+
return _Served(self._player, f"driver.{name}")
|
|
626
|
+
try:
|
|
627
|
+
return self._player.value("driver", name)
|
|
628
|
+
except AttributeError:
|
|
629
|
+
return decode((self._player.header.get("values") or {}).get(name))
|
|
630
|
+
|
|
631
|
+
def observe(self, timeout=10):
|
|
632
|
+
return self._player.call("driver", "observe", (), {"timeout": timeout})
|
|
633
|
+
|
|
634
|
+
def execute(self, operation, target, snapshot, text=None, timeout=10):
|
|
635
|
+
return self._player.call("driver", "execute", (operation, target, snapshot), {"text": text,
|
|
636
|
+
"timeout": timeout})
|
|
637
|
+
|
|
638
|
+
def close(self):
|
|
639
|
+
pass
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
class ReplayClient:
|
|
643
|
+
"""The recorded model: each call answered with the recorded reply for its purpose, usage as it was after it."""
|
|
644
|
+
|
|
645
|
+
per_call_reasoning = True
|
|
646
|
+
cancelled = None
|
|
647
|
+
on_hedge_usage = None
|
|
648
|
+
|
|
649
|
+
def __init__(self, player):
|
|
650
|
+
self.player = player
|
|
651
|
+
self.model = player.header.get("model") or ""
|
|
652
|
+
self.reasoning = "low"
|
|
653
|
+
self.usage = {"prompt_tokens": 0, "completion_tokens": 0, "cached_tokens": 0, "cache_write_tokens": 0,
|
|
654
|
+
"calls": 0}
|
|
655
|
+
|
|
656
|
+
def complete(self, messages, schema, **kwargs):
|
|
657
|
+
reply, call_usage, usage = self.player.model(messages, schema)
|
|
658
|
+
if usage:
|
|
659
|
+
self.usage.clear()
|
|
660
|
+
self.usage.update(usage)
|
|
661
|
+
return reply, call_usage
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@dataclasses.dataclass
|
|
665
|
+
class Report:
|
|
666
|
+
tape: str
|
|
667
|
+
request: str
|
|
668
|
+
recorded_acts: int
|
|
669
|
+
replayed_acts: int
|
|
670
|
+
divergence: dict = None # {"why", "expected", "got", "index", "step"} or None
|
|
671
|
+
recorded_status: str = None
|
|
672
|
+
replayed_status: str = None
|
|
673
|
+
same_answer: bool = None
|
|
674
|
+
prompts_same: int = 0
|
|
675
|
+
prompts_seen: int = 0
|
|
676
|
+
seconds: float = 0.0
|
|
677
|
+
error: str = None
|
|
678
|
+
steps: list = dataclasses.field(default_factory=list) # [{"index", "turn", "act", "now"}], see ``steps_of``
|
|
679
|
+
hooks: list = dataclasses.field(default_factory=list) # what the live run had that a replay doesn't
|
|
680
|
+
|
|
681
|
+
@property
|
|
682
|
+
def identical(self):
|
|
683
|
+
return (self.divergence is None and self.error is None and self.replayed_acts == self.recorded_acts
|
|
684
|
+
and self.recorded_status == self.replayed_status)
|
|
685
|
+
|
|
686
|
+
def public(self):
|
|
687
|
+
return {**dataclasses.asdict(self), "identical": self.identical}
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
# Modules whose ``time`` a replay swaps for the virtual one: the loop and the code it calls between two driver calls.
|
|
691
|
+
# A package stands for every module in it, so a new frontier module is on the clock without being listed.
|
|
692
|
+
# wda.guards holds the stream-still check (``unchanged_since``) the refresh before an action compares with a
|
|
693
|
+
# read's start: on the host's clock it reads now as hours after the recording and makes reads the recording didn't.
|
|
694
|
+
TIMED = ("frontier", "contract", "effect_ledger", "wda.system_dialogs", "wda.guards")
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _timed_modules():
|
|
698
|
+
for name in TIMED:
|
|
699
|
+
try:
|
|
700
|
+
importlib.import_module(f"mobile_agent.{name}")
|
|
701
|
+
except Exception: # noqa: BLE001
|
|
702
|
+
continue
|
|
703
|
+
names = tuple(f"mobile_agent.{name}" for name in TIMED)
|
|
704
|
+
return [module for key, module in sorted(sys.modules.items())
|
|
705
|
+
if module is not None and (key in names or key.startswith(tuple(n + "." for n in names)))]
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
@contextmanager
|
|
709
|
+
def _virtual_time(clock):
|
|
710
|
+
shim = _VirtualTime(clock)
|
|
711
|
+
patches = []
|
|
712
|
+
for module in _timed_modules():
|
|
713
|
+
if getattr(module, "time", None) is time:
|
|
714
|
+
patches.append(mock.patch.object(module, "time", shim))
|
|
715
|
+
for patch in patches:
|
|
716
|
+
patch.start()
|
|
717
|
+
try:
|
|
718
|
+
yield
|
|
719
|
+
finally:
|
|
720
|
+
for patch in reversed(patches):
|
|
721
|
+
patch.stop()
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def act_words(args):
|
|
725
|
+
"""A recorded action's key in words: the method, then its target's label and its text."""
|
|
726
|
+
parts = [str(args[0])] if args else []
|
|
727
|
+
for item in args[1:]:
|
|
728
|
+
if isinstance(item, dict) and "label" in item:
|
|
729
|
+
parts.append(repr(item["label"]))
|
|
730
|
+
elif isinstance(item, (str, int, float)) and not isinstance(item, bool):
|
|
731
|
+
parts.append(str(item)[:60])
|
|
732
|
+
return " ".join(parts)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def steps_of(player, divergence):
|
|
736
|
+
"""The recorded actions one by one against the replay: ``now`` is "same" for each the replay sent as recorded,
|
|
737
|
+
"differs: ..." at the one it didn't (the replay stops there; a read or a model call the recording never made says
|
|
738
|
+
so), "never asked" for an action the replay ended before, and "not reached" for the rest. An action the recording
|
|
739
|
+
never sent comes last, as a row of its own. ``turn`` counts the decisions the recording took before the action."""
|
|
740
|
+
differs = None
|
|
741
|
+
if divergence is not None:
|
|
742
|
+
asked = divergence["got"] and divergence["why"] in ("a different action", "an action the recording never sent")
|
|
743
|
+
differs = "differs: " + (act_words(divergence["got"]) if asked else divergence["why"])
|
|
744
|
+
out = []
|
|
745
|
+
for index, act in enumerate(player.acts):
|
|
746
|
+
if index < player.acts_done:
|
|
747
|
+
now = "same"
|
|
748
|
+
elif divergence is not None and index == divergence["index"]:
|
|
749
|
+
now = differs
|
|
750
|
+
else:
|
|
751
|
+
now = "not reached" if divergence is not None else "never asked"
|
|
752
|
+
turn = sum(1 for t1 in player.decisions if t1 <= (act.get("t0") or 0)) or None
|
|
753
|
+
out.append({"index": index, "turn": turn, "act": act_words(act.get("args") or ()), "now": now})
|
|
754
|
+
if divergence is not None and divergence["index"] >= len(player.acts):
|
|
755
|
+
step = divergence.get("step")
|
|
756
|
+
out.append({"index": len(player.acts), "turn": None if step is None else step + 1,
|
|
757
|
+
"act": "(the recording sent none)", "now": differs})
|
|
758
|
+
return out
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def build_agent(driver, client, header, emit):
|
|
762
|
+
"""FrontierAgent as the recording built it: the same apps and limits, the engine's switches as they were."""
|
|
763
|
+
from ..frontier import FrontierAgent
|
|
764
|
+
frontier = dict(decode(header.get("frontier")) or {})
|
|
765
|
+
accepted = set(inspect.signature(FrontierAgent.__init__).parameters)
|
|
766
|
+
return FrontierAgent(driver, client, apps=dict(header.get("apps") or {}), emit=emit,
|
|
767
|
+
**{k: v for k, v in frontier.items() if k in accepted})
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def replay(path, *, build=build_agent):
|
|
771
|
+
"""Replay the tape at ``path`` (a file, or a folder holding tape.jsonl) on today's code; a ``Report``."""
|
|
772
|
+
entries = load(path)
|
|
773
|
+
player = Player(entries)
|
|
774
|
+
header = player.header
|
|
775
|
+
events = []
|
|
776
|
+
report = Report(str(path), header.get("request") or "", recorded_acts=len(player.acts), replayed_acts=0,
|
|
777
|
+
recorded_status=(player.end or {}).get("status"))
|
|
778
|
+
started = time.monotonic()
|
|
779
|
+
env = {**{k: v for k, v in os.environ.items() if not k.startswith("MOBSTER_")}, **(header.get("env") or {})}
|
|
780
|
+
with mock.patch.dict(os.environ, env, clear=True), _virtual_time(player.clock):
|
|
781
|
+
agent = build(ReplayDriver(player), ReplayClient(player), header, events.append)
|
|
782
|
+
try:
|
|
783
|
+
outcome = agent.run(header.get("request") or "")
|
|
784
|
+
report.replayed_status = outcome.get("status")
|
|
785
|
+
recorded_answer = decode((player.end or {}).get("answer"))
|
|
786
|
+
report.same_answer = outcome.get("answer") == recorded_answer
|
|
787
|
+
except Divergence as divergence:
|
|
788
|
+
step = next((e.get("step") for e in reversed(events) if e.get("event") == "frontier_decision"), None)
|
|
789
|
+
report.divergence = {"why": divergence.why, "expected": divergence.expected, "got": divergence.got,
|
|
790
|
+
"index": divergence.index, "step": step}
|
|
791
|
+
except Exception as error: # noqa: BLE001
|
|
792
|
+
report.error = f"{type(error).__name__}: {error}"[:300]
|
|
793
|
+
report.replayed_acts = player.acts_done
|
|
794
|
+
report.steps = steps_of(player, report.divergence)
|
|
795
|
+
report.hooks = list(header.get("hooks") or ())
|
|
796
|
+
report.prompts_same, report.prompts_seen = player.prompts_same, player.prompts_seen
|
|
797
|
+
report.seconds = round(time.monotonic() - started, 3)
|
|
798
|
+
return report
|