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,691 @@
|
|
|
1
|
+
"""Saved programs for Smart: a request with the same words as one that worked, but other values, repeats that
|
|
2
|
+
run with code doing the checking, and the model only where code can't.
|
|
3
|
+
|
|
4
|
+
Replaying only a run's navigation before the model starts (#215's procedure memory, removed 12 Oct 2026: off by
|
|
5
|
+
default, and its A/B left the median unchanged) still paid the contract call, every typing and commit turn and the
|
|
6
|
+
DONE turn. A saved program keeps the whole run:
|
|
7
|
+
|
|
8
|
+
- **Learning.** A Smart run that ended done (the task contract proved every item, a write only from screens after
|
|
9
|
+
it) is kept as a program:
|
|
10
|
+
- its request as a template: each typed text, each tapped control's name and each row a SCROLL_TO looked for that
|
|
11
|
+
appears in the request (case folded, whole words) is a blank ``{pN}``;
|
|
12
|
+
- its steps (replay_core path steps): the action, the app, the target's role and label, and the screen before as
|
|
13
|
+
hashed role/label pairs (``StepRecord.shape``; no screen text). A step may type only a blank's value or a
|
|
14
|
+
search typed from the request's own words; the steps end at the first one that types anything else, uses a
|
|
15
|
+
point, a macro or a skill (the model does it each run). A SCROLL_TO is kept only when the row it looked for is a
|
|
16
|
+
blank: "Open Settings › General › Fonts" scrolls to "Fonts" on the General page, and its siblings to their own
|
|
17
|
+
page (11 Oct, on an iPhone: the scroll ended the program, "Fonts" stayed literal and no sibling matched);
|
|
18
|
+
- its task contract's items, with each blank's value as its blank. An item whose quote or payload holds words
|
|
19
|
+
the template doesn't is not kept: then no contract is kept, and the run compiles one.
|
|
20
|
+
Never kept: answers, notes, screen text, text the model wrote, secrets.
|
|
21
|
+
- **Matching** (``match``): programs from the same start app and phone are candidates. Code accepts a match only
|
|
22
|
+
when every value is words of the new request and the filled template is the request (case, spacing and end
|
|
23
|
+
punctuation aside): typed values come only from the new request, and none runs across a clause (``crosses``). One
|
|
24
|
+
call to the run's model picks and fills only when code finds more than one way (below).
|
|
25
|
+
- **Replay** (``FrontierAgent(saved_program=...)``): the program's steps are decisions with no model call, through
|
|
26
|
+
the same per-action path as the model's (guards, the contract's commit check, approvals, the journal); each one
|
|
27
|
+
only on a screen like its recorded one with its target there exactly once. The contract is the program's, filled.
|
|
28
|
+
When a step doesn't fit, the model takes over with the program's remaining steps as a suggested plan; a run whose
|
|
29
|
+
contract is proven with nothing to report ends with no DONE call.
|
|
30
|
+
- **Trust.** A program is learned *provisional*: a contract can be proven on screen by a run whose write didn't land
|
|
31
|
+
(a CalTrack log that the app never kept, 11 Oct). Its first replay is supervised: the steps run, but the model's
|
|
32
|
+
DONE turn and the DONE gate decide the end, never code alone. It becomes *trusted* only after a replay that took
|
|
33
|
+
every step and whose run ended done with every contract item proven (a write only from screens after the replay's
|
|
34
|
+
own actions, #230). A provisional program that fails is forgotten at once; only a trusted one finishes in code.
|
|
35
|
+
- **Forgetting.** For a trusted program, a replay that stops before its last step or a run that doesn't end done is
|
|
36
|
+
a miss; missed twice in a row, it is forgotten. A done run after a replay is learned again.
|
|
37
|
+
- **Matching without a call.** When code fills exactly one template so that it is the request, with one way to split
|
|
38
|
+
the values, that's the match: no call. The call is made only when that's ambiguous (two templates, or two ways to
|
|
39
|
+
split one); when no template can be the request, there's no call either.
|
|
40
|
+
|
|
41
|
+
``MOBSTER_PROGRAMS``: ``on`` learns and replays, ``learn`` only learns, ``off`` turns it off. Unset, it is on for runs
|
|
42
|
+
from Mobster for Mac, ``mobster serve``'s API, your coding agent's ``phone_task`` through ``mobster mcp`` and Mobster
|
|
43
|
+
Remote (DEFAULT_ON_ORIGINS) and off for the rest; any other value is off.
|
|
44
|
+
On by default since 11 Oct 2026: on an iPhone 15 Pro (iOS 26.0.1), exact repeats took 7.87 -> 3.91 s (Settings ›
|
|
45
|
+
General › About) and 4.37 -> 1.10 s (Clock's Stopwatch tab) at the median of 4 runs each, 16/16 passed, no unsafe act,
|
|
46
|
+
approvals unchanged; on iOS Simulators, code-graded siblings took 2.33 calls instead of 5.06 at the same pass rate.
|
|
47
|
+
Programs live in the data folder's ``replays/programs/`` (0700, files 0600), at most MAX_PROGRAMS.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
import json
|
|
51
|
+
import os
|
|
52
|
+
import re
|
|
53
|
+
import time
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
|
|
56
|
+
from ..replay_core.path import ELEMENT_OPS, SWIPES, TEXT_OPS, describe, first_of, new_path
|
|
57
|
+
from ..replay_core.recorder import typed_plainly
|
|
58
|
+
from ..replay_core.store import PROGRAM_ID, Cache, env_of
|
|
59
|
+
|
|
60
|
+
MAX_PROGRAMS = 100
|
|
61
|
+
MAX_TAUGHT = 100 # taught programs, kept apart: learned ones never push one out
|
|
62
|
+
MAX_NAME = 80
|
|
63
|
+
PROVISIONAL, TRUSTED = "provisional", "trusted"
|
|
64
|
+
MISSES_TO_FORGET = 2
|
|
65
|
+
MAX_CANDIDATES = 5
|
|
66
|
+
MIN_BLANK = 3 # a tapped name shorter than this never becomes a blank ("OK", "Go")
|
|
67
|
+
MIN_LITERAL = 8
|
|
68
|
+
ITEM_FIELDS = ("kind", "app", "what", "payload", "act", "count", "condition", "quote")
|
|
69
|
+
FREE_FIELDS = frozenset({"what", "payload", "condition", "quote"})
|
|
70
|
+
PROGRAM_OPS = ELEMENT_OPS | SWIPES | frozenset({"LAUNCH_APP", "HOME"})
|
|
71
|
+
BLANK = re.compile(r"\{(p\d+)\}")
|
|
72
|
+
ROW_SPLIT = re.compile(r",|\s[·•|–—-]\s|\n")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Run origins where saved programs are on while MOBSTER_PROGRAMS is unset: Mobster for Mac, `mobster serve`'s API, a
|
|
76
|
+
# coding agent's `phone_task` through `mobster mcp` ("mcp") and Mobster Remote ("remote"). Every one of them is a run in
|
|
77
|
+
# Mobster's own agent, with the same approvals as the app's: a replayed step that sends, posts, deletes or buys asks
|
|
78
|
+
# like any other (the permission mode also rises to ask-important for "mcp" and "remote", permissions/policy.py).
|
|
79
|
+
DEFAULT_ON_ORIGINS = frozenset({"app", "api", "mcp", "remote"})
|
|
80
|
+
ORIGINS = frozenset({"app", "tui", "cli", "api", "mcp", "remote"})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def mode(env=None, origin=None):
|
|
84
|
+
env = os.environ if env is None else env
|
|
85
|
+
value = (env.get("MOBSTER_PROGRAMS") or "").strip().casefold()
|
|
86
|
+
if value in ("on", "learn", "off"):
|
|
87
|
+
return value
|
|
88
|
+
return "on" if not value and origin in DEFAULT_ON_ORIGINS else "off"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
TAUGHT_ORIGINS = frozenset({"workflow", "schedule"})
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def taught_on(env=None, origin=None):
|
|
95
|
+
env = os.environ if env is None else env
|
|
96
|
+
value = (env.get("MOBSTER_PROGRAMS") or "").strip().casefold()
|
|
97
|
+
return value not in ("off", "learn") and origin in (ORIGINS | TAUGHT_ORIGINS)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def programs_dir(data_dir):
|
|
101
|
+
return Path(data_dir) / "replays" / "programs" if data_dir else None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def normalized(text):
|
|
105
|
+
return " ".join(str(text or "").split())
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def folded(text):
|
|
109
|
+
"""For comparing a request with a filled template: case, spacing, quotes and end punctuation aside."""
|
|
110
|
+
text = normalized(text).casefold().translate(str.maketrans({"‘": "'", "’": "'", "“": '"',
|
|
111
|
+
"”": '"'}))
|
|
112
|
+
return text.rstrip(" .!?")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def blank_for(template, values, value):
|
|
116
|
+
value = normalized(value)
|
|
117
|
+
if len(value) < MIN_BLANK or "{" in value or "}" in value:
|
|
118
|
+
return template, None
|
|
119
|
+
for name, known in values.items():
|
|
120
|
+
if known.casefold() == value.casefold():
|
|
121
|
+
return template, name
|
|
122
|
+
found = re.search(r"(?<![\w{])" + re.escape(value) + r"(?![\w}])", template, flags=re.IGNORECASE)
|
|
123
|
+
if not found:
|
|
124
|
+
return template, None
|
|
125
|
+
name = f"p{len(values) + 1}"
|
|
126
|
+
values[name] = template[found.start():found.end()]
|
|
127
|
+
return template[:found.start()] + "{" + name + "}" + template[found.end():], name
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def program_from(goal, steps):
|
|
131
|
+
template, values, out = normalized(goal), {}, []
|
|
132
|
+
for step in steps:
|
|
133
|
+
op = step.get("op")
|
|
134
|
+
if op == "SCROLL_TO":
|
|
135
|
+
# The row it looked for, when that row is the request's own words: a blank, as a typed value is.
|
|
136
|
+
sought = step.get("scroll")
|
|
137
|
+
if not (isinstance(sought, str) and typed_plainly(sought)):
|
|
138
|
+
break
|
|
139
|
+
template, name = blank_for(template, values, sought)
|
|
140
|
+
if name is None:
|
|
141
|
+
break # a row the request doesn't name: the model scrolls each run
|
|
142
|
+
screen = step.get("screen") if isinstance(step.get("screen"), dict) else {}
|
|
143
|
+
out.append({"op": op, "app": step.get("app"), "target": None, "param": name, "text": None,
|
|
144
|
+
"commit": None, "point": None, "live": False,
|
|
145
|
+
"screen": {"hash": str(screen.get("hash") or "")[:64],
|
|
146
|
+
**({"shape": list(screen["shape"])} if screen.get("shape") else {})}})
|
|
147
|
+
continue
|
|
148
|
+
if step.get("live") or op not in PROGRAM_OPS:
|
|
149
|
+
break
|
|
150
|
+
step = {key: value for key, value in step.items() if key not in ("param", "label_param", "label_prefix")}
|
|
151
|
+
target = dict(step.get("target") or {}) if isinstance(step.get("target"), dict) else None
|
|
152
|
+
if op in TEXT_OPS:
|
|
153
|
+
text = step.get("text")
|
|
154
|
+
if not (isinstance(text, str) and typed_plainly(text)):
|
|
155
|
+
break
|
|
156
|
+
template, name = blank_for(template, values, text)
|
|
157
|
+
if name is None:
|
|
158
|
+
break
|
|
159
|
+
step["param"], step["text"] = name, None
|
|
160
|
+
elif op in ("TAP", "LONG_PRESS") and target is not None and not step.get("commit"):
|
|
161
|
+
# A commit's control is never a blank: "Pay" in "pay Maya $12" is the act, not a value to swap.
|
|
162
|
+
label = target.get("label") or ""
|
|
163
|
+
template, name = blank_for(template, values, label)
|
|
164
|
+
if name is not None:
|
|
165
|
+
step["label_param"] = name
|
|
166
|
+
else:
|
|
167
|
+
# A row named by the request, then its details ("Banana Bread, Recipes, ·, …"): the name is a blank
|
|
168
|
+
# and the row is found again by a name that starts the same way (``fits``).
|
|
169
|
+
head = ROW_SPLIT.split(label, 1)[0].strip()
|
|
170
|
+
if head and head != label.strip():
|
|
171
|
+
template, name = blank_for(template, values, head)
|
|
172
|
+
if name is not None:
|
|
173
|
+
step["label_prefix"] = name
|
|
174
|
+
target["label"] = head
|
|
175
|
+
if target is not None:
|
|
176
|
+
target["id"] = target.get("id")
|
|
177
|
+
step["target"] = target
|
|
178
|
+
screen = step.get("screen") if isinstance(step.get("screen"), dict) else {}
|
|
179
|
+
# No screen text: the hash and the hashed pairs only (replay_core.path.screen_shape).
|
|
180
|
+
step["screen"] = {"hash": str(screen.get("hash") or "")[:64],
|
|
181
|
+
**({"shape": list(screen["shape"])} if screen.get("shape") else {})}
|
|
182
|
+
out.append(step)
|
|
183
|
+
if not out or len(BLANK.sub("", template).strip()) < MIN_LITERAL:
|
|
184
|
+
return None
|
|
185
|
+
for kept, step in zip(out, steps):
|
|
186
|
+
# Which values the screen before a step showed, by blank name (its text is dropped): a part ending before this
|
|
187
|
+
# step checks them again (``exit_holds``).
|
|
188
|
+
kept["shows"] = shown(values, (step.get("screen") or {}).get("signature"))
|
|
189
|
+
return template, out, values
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def shown(values, pairs):
|
|
193
|
+
"""The blanks whose values a screen's role/label pairs show."""
|
|
194
|
+
labels = [folded(label) for _, label in pairs or ()]
|
|
195
|
+
return sorted(name for name, value in values.items() if value and any(folded(value) in label for label in labels))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def contract_template(items, values, template):
|
|
199
|
+
"""The items with blanks for their values, or None when an item holds words not in the template."""
|
|
200
|
+
out = []
|
|
201
|
+
words = set(re.findall(r"\w+", BLANK.sub(" ", template).casefold()))
|
|
202
|
+
for item in items or ():
|
|
203
|
+
entry = {}
|
|
204
|
+
for name in ITEM_FIELDS:
|
|
205
|
+
text = normalized(getattr(item, name, None) if not isinstance(item, dict) else item.get(name))
|
|
206
|
+
if name in FREE_FIELDS:
|
|
207
|
+
# Whole words only: "pay" is a blank's value, but never the "Pay" inside "SplitPay".
|
|
208
|
+
for blank, value in sorted(values.items(), key=lambda pair: -len(pair[1])):
|
|
209
|
+
text = re.sub(r"(?<![\w{])" + re.escape(value) + r"(?![\w}])", "{" + blank + "}", text,
|
|
210
|
+
flags=re.IGNORECASE)
|
|
211
|
+
entry[name] = text
|
|
212
|
+
for name in ("quote", "payload"):
|
|
213
|
+
rest = set(re.findall(r"\w+", BLANK.sub(" ", entry[name]).casefold()))
|
|
214
|
+
if not rest <= words:
|
|
215
|
+
return None
|
|
216
|
+
out.append(entry)
|
|
217
|
+
return out
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def fill(text, values):
|
|
221
|
+
return BLANK.sub(lambda found: values.get(found.group(1), found.group(0)), text or "")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class Programs:
|
|
225
|
+
"""Programs in ``directory``: replay_core paths of kind "program"."""
|
|
226
|
+
|
|
227
|
+
def __init__(self, directory, version=None):
|
|
228
|
+
self.directory = Path(directory)
|
|
229
|
+
self.cache = Cache(directory)
|
|
230
|
+
# The app's version when the route corpus is on (compiled_read/routes.py): saved under it, offered for it.
|
|
231
|
+
self.version = version
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def identity(app, template, device):
|
|
235
|
+
return {"kind": "program", "app": app or None, "template": template, "device": device or None}
|
|
236
|
+
|
|
237
|
+
def all(self):
|
|
238
|
+
from ..replay_core.path import PathError, validate
|
|
239
|
+
out = []
|
|
240
|
+
try:
|
|
241
|
+
files = list(self.directory.glob("*.json"))
|
|
242
|
+
except OSError:
|
|
243
|
+
return out
|
|
244
|
+
for file in files:
|
|
245
|
+
try:
|
|
246
|
+
path = validate(json.loads(file.read_text(encoding="utf-8")))
|
|
247
|
+
except (OSError, ValueError, PathError):
|
|
248
|
+
continue
|
|
249
|
+
if (path.get("identity") or {}).get("kind") == "program":
|
|
250
|
+
out.append(path)
|
|
251
|
+
return sorted(out, key=lambda p: -float(p.get("used_at") or p.get("created_at") or 0))
|
|
252
|
+
|
|
253
|
+
def candidates(self, app, device, goal, limit=MAX_CANDIDATES, taught_only=False):
|
|
254
|
+
"""Programs started in ``app`` on ``device``, the ones whose template holds most of ``goal``'s words first."""
|
|
255
|
+
words = set(re.findall(r"\w+", folded(goal)))
|
|
256
|
+
found = []
|
|
257
|
+
for path in self.all():
|
|
258
|
+
identity = path.get("identity") or {}
|
|
259
|
+
if taught_only and not path.get("taught"):
|
|
260
|
+
continue
|
|
261
|
+
if identity.get("app") != (app or None) and not path.get("taught") \
|
|
262
|
+
or identity.get("device") != (device or None):
|
|
263
|
+
continue
|
|
264
|
+
if self.version is not None and (path.get("env") or {}).get("app_version") not in (None, self.version):
|
|
265
|
+
continue
|
|
266
|
+
own = set(re.findall(r"\w+", folded(BLANK.sub(" ", identity.get("template") or ""))))
|
|
267
|
+
found.append((len(own & words) / max(1, len(own)), path))
|
|
268
|
+
found.sort(key=lambda pair: -pair[0])
|
|
269
|
+
return [path for _, path in found[:limit]]
|
|
270
|
+
|
|
271
|
+
def save(self, app, goal, device, steps, items, *, run_id, trusted=None, final_pairs=None, first=None):
|
|
272
|
+
"""Keep a done run's program: trusted only as the program this run proved or one already trusted."""
|
|
273
|
+
made = program_from(goal, steps)
|
|
274
|
+
if made is None:
|
|
275
|
+
return None
|
|
276
|
+
template, steps, values = made
|
|
277
|
+
identity = self.identity(app, template, device)
|
|
278
|
+
env = env_of(self.version, None, device)
|
|
279
|
+
path = new_path(steps, kind="program", identity=identity, env=env, source_run=run_id)
|
|
280
|
+
path["contract"] = contract_template(items, values, template)
|
|
281
|
+
path["blanks"] = sorted(values)
|
|
282
|
+
if final_pairs:
|
|
283
|
+
from ..replay_core.path import screen_shape, signature_hash
|
|
284
|
+
path["final"] = {"hash": signature_hash(final_pairs), "shape": screen_shape(final_pairs),
|
|
285
|
+
"shows": shown(values, final_pairs)}
|
|
286
|
+
path["used_at"], path["misses"] = round(time.time(), 3), 0
|
|
287
|
+
try:
|
|
288
|
+
# Learned again after a replay: the count of replays and the trust carry over.
|
|
289
|
+
previous = self.cache.load(identity, env)
|
|
290
|
+
except (OSError, ValueError):
|
|
291
|
+
previous = None
|
|
292
|
+
if isinstance(previous, dict) and previous.get("taught"):
|
|
293
|
+
# What the person taught stays as they taught it: a run that replayed it settles it (``note``), and never
|
|
294
|
+
# replaces its steps with the run's own (which end at the first coordinate step).
|
|
295
|
+
return None
|
|
296
|
+
if isinstance(previous, dict):
|
|
297
|
+
path["replays"] = int(previous.get("replays") or 0)
|
|
298
|
+
path["parts"] = previous.get("parts") or {}
|
|
299
|
+
kept = first_of(previous if isinstance(previous, dict) else None, first)
|
|
300
|
+
if kept is not None:
|
|
301
|
+
path["first"] = kept
|
|
302
|
+
proven = trusted is not None and dict(trusted) == identity
|
|
303
|
+
path["status"] = TRUSTED if proven or (isinstance(previous, dict) and previous.get("status") == TRUSTED) \
|
|
304
|
+
else PROVISIONAL
|
|
305
|
+
saved = self.cache.save(path)
|
|
306
|
+
for old in [other for other in self.all() if not other.get("taught")][MAX_PROGRAMS:]:
|
|
307
|
+
self.forget(old)
|
|
308
|
+
return saved
|
|
309
|
+
|
|
310
|
+
def note(self, path, *, missed, run_id=None):
|
|
311
|
+
"""After a replay."""
|
|
312
|
+
path = dict(path)
|
|
313
|
+
if path.get("taught"):
|
|
314
|
+
return self._note_taught(path, missed=missed, run_id=run_id)
|
|
315
|
+
if path.get("status") != TRUSTED:
|
|
316
|
+
if missed:
|
|
317
|
+
self.forget(path)
|
|
318
|
+
return None
|
|
319
|
+
path["status"] = TRUSTED
|
|
320
|
+
path["misses"] = int(path.get("misses") or 0) + 1 if missed else 0
|
|
321
|
+
path["replays"] = int(path.get("replays") or 0) + 1
|
|
322
|
+
path["used_at"], path["last_run"] = round(time.time(), 3), run_id
|
|
323
|
+
if path["misses"] >= MISSES_TO_FORGET:
|
|
324
|
+
self.forget(path)
|
|
325
|
+
return None
|
|
326
|
+
try:
|
|
327
|
+
self.cache.save(path)
|
|
328
|
+
except (OSError, ValueError):
|
|
329
|
+
pass
|
|
330
|
+
return path
|
|
331
|
+
|
|
332
|
+
def _note_taught(self, path, *, missed, run_id):
|
|
333
|
+
try:
|
|
334
|
+
current = self.cache.load(path.get("identity") or {}, path.get("env") or {}) or path
|
|
335
|
+
except (OSError, ValueError):
|
|
336
|
+
current = path
|
|
337
|
+
current = dict(current)
|
|
338
|
+
current["replays"] = int(current.get("replays") or 0) + 1
|
|
339
|
+
current["misses"] = int(current.get("misses") or 0) + 1 if missed else 0
|
|
340
|
+
if not missed:
|
|
341
|
+
current["status"] = TRUSTED
|
|
342
|
+
elif current["misses"] >= MISSES_TO_FORGET:
|
|
343
|
+
current["status"], current["misses"] = PROVISIONAL, 0
|
|
344
|
+
current["used_at"], current["last_run"] = round(time.time(), 3), run_id
|
|
345
|
+
current["last"] = {"run": run_id, "at": round(time.time(), 3), "ok": not missed}
|
|
346
|
+
try:
|
|
347
|
+
self.cache.save(current)
|
|
348
|
+
except (OSError, ValueError):
|
|
349
|
+
pass
|
|
350
|
+
return current
|
|
351
|
+
|
|
352
|
+
def forget(self, path):
|
|
353
|
+
return self.cache.forget(path.get("identity") or {}, path.get("env") or {})
|
|
354
|
+
|
|
355
|
+
@staticmethod
|
|
356
|
+
def id_of(path):
|
|
357
|
+
from ..replay_core.store import file_name
|
|
358
|
+
return file_name(path.get("identity") or {}, path.get("env") or {})[:-len(".json")]
|
|
359
|
+
|
|
360
|
+
def by_id(self, program_id):
|
|
361
|
+
if not isinstance(program_id, str) or not PROGRAM_ID.fullmatch(program_id):
|
|
362
|
+
return None
|
|
363
|
+
return next((path for path in self.all() if self.id_of(path) == program_id), None)
|
|
364
|
+
|
|
365
|
+
def rename(self, program_id, name):
|
|
366
|
+
name = " ".join(str(name or "").split())
|
|
367
|
+
if not name or len(name) > MAX_NAME:
|
|
368
|
+
raise ValueError(f"A program's name is 1 to {MAX_NAME} characters")
|
|
369
|
+
path = self.by_id(program_id)
|
|
370
|
+
if path is None:
|
|
371
|
+
raise LookupError("No saved program has that id")
|
|
372
|
+
path = dict(path)
|
|
373
|
+
path["name"] = name
|
|
374
|
+
self.cache.save(path)
|
|
375
|
+
return path
|
|
376
|
+
|
|
377
|
+
def note_part(self, path, key, outcome, *, trusted):
|
|
378
|
+
path = self.cache.load(path.get("identity") or {}, path.get("env") or {})
|
|
379
|
+
if path is None:
|
|
380
|
+
return None
|
|
381
|
+
stats = dict((path.get("parts") or {}).get(key) or {})
|
|
382
|
+
stats["hits"] = int(stats.get("hits") or 0) + (outcome == "hit")
|
|
383
|
+
stats["misses"] = int(stats.get("misses") or 0) + 1 if outcome == "miss" else 0
|
|
384
|
+
stats["status"] = TRUSTED if outcome == "hit" or trusted and stats["misses"] < MISSES_TO_FORGET else "forgotten"
|
|
385
|
+
path["parts"] = {**(path.get("parts") or {}), key: stats}
|
|
386
|
+
try:
|
|
387
|
+
self.cache.save(path)
|
|
388
|
+
except (OSError, ValueError):
|
|
389
|
+
pass
|
|
390
|
+
return stats
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def learn(store, *, app, goal, device, steps, items, done, run_id=None, trusted=None, origin=None, final_pairs=None,
|
|
394
|
+
first=None):
|
|
395
|
+
"""Keep a run as a program when it ended done; returns the saved path or None."""
|
|
396
|
+
if not done or mode(origin=origin) == "off" or not steps:
|
|
397
|
+
return None
|
|
398
|
+
try:
|
|
399
|
+
return store.save(app, goal, device, steps, items, run_id=run_id, trusted=trusted, final_pairs=final_pairs,
|
|
400
|
+
first=first)
|
|
401
|
+
except (OSError, ValueError):
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def public(path, workflows=()):
|
|
406
|
+
"""A program as the Programs list shows it: no screen text, no step details beyond plain lines."""
|
|
407
|
+
from ..replay_core.path import describe
|
|
408
|
+
identity = path.get("identity") or {}
|
|
409
|
+
template = identity.get("template") or ""
|
|
410
|
+
example = path.get("example") if isinstance(path.get("example"), dict) else {}
|
|
411
|
+
taught = path.get("taught") if isinstance(path.get("taught"), dict) else None
|
|
412
|
+
linked = [workflow["id"] for workflow in workflows if isinstance(workflow, dict)
|
|
413
|
+
and code_fills(template, workflow_words(workflow))]
|
|
414
|
+
last = path.get("last") if isinstance(path.get("last"), dict) else None
|
|
415
|
+
return {"id": Programs.id_of(path), "name": path.get("name") or template, "template": template,
|
|
416
|
+
"request": fill(template, example) if example else template,
|
|
417
|
+
"blanks": [{"name": name, "example": example.get(name)} for name in path.get("blanks") or ()],
|
|
418
|
+
"taught": taught is not None, "status": path.get("status") or PROVISIONAL,
|
|
419
|
+
"steps": len(path.get("steps") or ()), "coordinateSteps": (taught or {}).get("points", 0),
|
|
420
|
+
"asks": sum(1 for step in path.get("steps") or () if step.get("commit")),
|
|
421
|
+
"lines": [describe({**step, "text": None}) for step in (path.get("steps") or ())[:MAX_LINES]],
|
|
422
|
+
"replays": int(path.get("replays") or 0), "device": identity.get("device"),
|
|
423
|
+
"createdAt": _ms(path.get("created_at")), "usedAt": _ms(path.get("used_at")),
|
|
424
|
+
"last": {"ok": bool(last.get("ok")), "at": _ms(last.get("at"))} if last else None,
|
|
425
|
+
"first": first_of(path), "workflowIds": linked}
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
MAX_LINES = 12
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _ms(seconds):
|
|
432
|
+
return round(float(seconds) * 1000) if isinstance(seconds, (int, float)) and seconds else None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def workflow_words(workflow):
|
|
436
|
+
"""A saved workflow's words as its run sends them with each blank's default (starters.fill)."""
|
|
437
|
+
from ..workflows import starters
|
|
438
|
+
try:
|
|
439
|
+
return starters.fill(workflow.get("goal") or "", None)
|
|
440
|
+
except Exception: # noqa: BLE001
|
|
441
|
+
return workflow.get("goal") or ""
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def settle(store, *, matched, done, replayed_all, run_id=None):
|
|
445
|
+
"""After a run: note its replayed program (a miss unless it took every step and the run ended done)."""
|
|
446
|
+
if matched is None:
|
|
447
|
+
return None
|
|
448
|
+
proven = bool(done and replayed_all)
|
|
449
|
+
store.note(matched.path, missed=not proven, run_id=run_id)
|
|
450
|
+
return dict(matched.path.get("identity") or {}) if proven else None
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
MATCH_SYSTEM = """You match a phone task request to saved programs: each is the template of a request that was \
|
|
454
|
+
done before, with blanks like {p1} where that request had its own values. A program fits when the new request is \
|
|
455
|
+
its template with other values in the blanks: the same task, the same words, other values. Pick the one that fits \
|
|
456
|
+
and fill each of its blanks with the new request's words, copied exactly. If none fits, answer null."""
|
|
457
|
+
|
|
458
|
+
MATCH_SCHEMA = {"type": "object", "additionalProperties": False, "required": ["program", "values"], "properties": {
|
|
459
|
+
"program": {"type": ["integer", "null"]},
|
|
460
|
+
"values": {"type": "array", "items": {"type": "object", "additionalProperties": False,
|
|
461
|
+
"required": ["blank", "value"],
|
|
462
|
+
"properties": {"blank": {"type": "string"},
|
|
463
|
+
"value": {"type": "string"}}}}}}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def blank_roles(path):
|
|
467
|
+
"""How each blank is used, for the matching call: 'typed into "Title"', 'tapped "…" (Button)'."""
|
|
468
|
+
roles = {}
|
|
469
|
+
for step in path.get("steps") or ():
|
|
470
|
+
target = step.get("target") or {}
|
|
471
|
+
if step.get("param"):
|
|
472
|
+
roles.setdefault(step["param"],
|
|
473
|
+
f'typed into the {target.get("role") or "field"} "{target.get("label") or ""}"')
|
|
474
|
+
if step.get("label_param"):
|
|
475
|
+
roles.setdefault(step["label_param"], f'the name of a {target.get("role") or "control"} it tapped')
|
|
476
|
+
if step.get("label_prefix"):
|
|
477
|
+
roles.setdefault(step["label_prefix"], f'the name of a list row ({target.get("role") or "row"}) it opened')
|
|
478
|
+
return roles
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class Matched:
|
|
482
|
+
"""A program filled for one request: its steps and contract items with the request's values."""
|
|
483
|
+
|
|
484
|
+
def __init__(self, path, values):
|
|
485
|
+
self.path, self.values = path, dict(values)
|
|
486
|
+
self.steps = filled_steps(path, values)
|
|
487
|
+
contract = path.get("contract")
|
|
488
|
+
if not contract and path.get("taught"):
|
|
489
|
+
# A program taught before its contract was made at save time: made now, by code, from its lesson.
|
|
490
|
+
from .taught import lesson_items
|
|
491
|
+
contract = lesson_items((path.get("identity") or {}).get("template") or "", path.get("steps") or [],
|
|
492
|
+
path.get("final"))
|
|
493
|
+
self.items = ([{name: fill(entry.get(name), values) for name in ITEM_FIELDS} for entry in contract]
|
|
494
|
+
if isinstance(contract, list) and contract else None)
|
|
495
|
+
# Only a trusted program finishes in code; a provisional one's first replay ends through the model's DONE turn.
|
|
496
|
+
self.trusted = path.get("status") == TRUSTED
|
|
497
|
+
# Taught by hand on the live view (save_from_demo): the run says so, by the program's name.
|
|
498
|
+
self.taught = bool(path.get("taught"))
|
|
499
|
+
self.name = path.get("name") or None
|
|
500
|
+
# A practice run (the person watches and answers each step): ``ask`` is the run's approval request, and
|
|
501
|
+
# their corrections are kept here, then applied to the program after the run (apply_corrections).
|
|
502
|
+
self.practice, self.ask, self.corrections = False, None, []
|
|
503
|
+
self.paused_at, self.rejoins, self.helped = None, 0, False
|
|
504
|
+
self.deciding = False
|
|
505
|
+
self.at = 0
|
|
506
|
+
self.stopped = None
|
|
507
|
+
self.expect = None
|
|
508
|
+
|
|
509
|
+
@staticmethod
|
|
510
|
+
def read_by_code(snapshot, request):
|
|
511
|
+
"""A question the screen in front answers, read by code (learned/code_read.py), else None."""
|
|
512
|
+
from .code_read import answer
|
|
513
|
+
return answer(snapshot, request)
|
|
514
|
+
|
|
515
|
+
@property
|
|
516
|
+
def template(self):
|
|
517
|
+
return (self.path.get("identity") or {}).get("template") or ""
|
|
518
|
+
|
|
519
|
+
# How FrontierAgent runs a saved program's steps: it matches each to the screen and words it with these, so the
|
|
520
|
+
# engine needs nothing from this package.
|
|
521
|
+
@staticmethod
|
|
522
|
+
def fits(step, snapshot):
|
|
523
|
+
return fits(step, snapshot)
|
|
524
|
+
|
|
525
|
+
@staticmethod
|
|
526
|
+
def describe(step):
|
|
527
|
+
return describe(step)
|
|
528
|
+
|
|
529
|
+
@staticmethod
|
|
530
|
+
def correction(snapshot, x, y, among=None):
|
|
531
|
+
from ..replay_core.path import target_of
|
|
532
|
+
from .teach import element_at, screen_of
|
|
533
|
+
under = element_at(snapshot, x, y, among=among)
|
|
534
|
+
screen, _ = screen_of(snapshot)
|
|
535
|
+
if under is None:
|
|
536
|
+
screen["fingerprint"] = snapshot.content_fingerprint
|
|
537
|
+
return ({"op": "TAP" if under is not None else "TAP_XY", "target": target_of(under),
|
|
538
|
+
"point": None if under is not None else [round(x, 4), round(y, 4)], "screen": screen}, under)
|
|
539
|
+
|
|
540
|
+
def variant(self, step, snapshot):
|
|
541
|
+
alts = step.get("alts") or ()
|
|
542
|
+
if not alts:
|
|
543
|
+
return step
|
|
544
|
+
from ..replay_core.engine import screen_likeness
|
|
545
|
+
best, score = step, -1.0
|
|
546
|
+
try:
|
|
547
|
+
for candidate in [step] + [{**step, **alt, "alts": None} for alt in alts]:
|
|
548
|
+
if fits(candidate, snapshot)[0]:
|
|
549
|
+
likeness = screen_likeness(candidate, snapshot)
|
|
550
|
+
if likeness > score:
|
|
551
|
+
best, score = candidate, likeness
|
|
552
|
+
except Exception: # noqa: BLE001 -- the step as taught: its own check decides
|
|
553
|
+
return step
|
|
554
|
+
return best
|
|
555
|
+
|
|
556
|
+
def remaining(self, limit=8):
|
|
557
|
+
"""The steps not taken yet, as plain lines (no typed text): the model's suggested plan when it takes over."""
|
|
558
|
+
left = self.steps[self.at:]
|
|
559
|
+
lines = [describe({**step, "text": None}) for step in left[:limit]]
|
|
560
|
+
return "; ".join(lines) + ("; …" if len(left) > limit else "")
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def filled_steps(path, values):
|
|
564
|
+
out = []
|
|
565
|
+
for step in path.get("steps") or ():
|
|
566
|
+
step = dict(step)
|
|
567
|
+
if isinstance(step.get("purpose"), str):
|
|
568
|
+
step["purpose"] = fill(step["purpose"], values)
|
|
569
|
+
name = step.get("label_param") or step.get("label_prefix")
|
|
570
|
+
if name and values.get(name):
|
|
571
|
+
target = dict(step.get("target") or {})
|
|
572
|
+
target["label"], target["id"] = values[name], None
|
|
573
|
+
step["target"] = target
|
|
574
|
+
out.append(step)
|
|
575
|
+
return out
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def fits(step, snapshot):
|
|
579
|
+
from ..replay_core.engine import WEAK, match, plain, screen_likeness
|
|
580
|
+
if not step.get("label_prefix"):
|
|
581
|
+
return match(step, snapshot, settled=True)
|
|
582
|
+
target = step.get("target") or {}
|
|
583
|
+
want, role = plain(target.get("label")), target.get("role") or ""
|
|
584
|
+
app, now = step.get("app"), getattr(snapshot, "bundle_id", "") or ""
|
|
585
|
+
if app and now and app != now:
|
|
586
|
+
return False, None, f"{now} is in front, not {app}"
|
|
587
|
+
found = []
|
|
588
|
+
for element in getattr(snapshot, "elements", ()) or ():
|
|
589
|
+
head = ROW_SPLIT.split(normalized(element.label), 1)[0].strip().casefold()
|
|
590
|
+
if want and head == want and (not role or element.role == role):
|
|
591
|
+
found.append(element)
|
|
592
|
+
if len(found) != 1:
|
|
593
|
+
return False, None, (f'no single row named "{target.get("label")}" is on the screen')
|
|
594
|
+
likeness = screen_likeness(step, snapshot)
|
|
595
|
+
if likeness < WEAK:
|
|
596
|
+
return False, None, f"the screen is different ({round(likeness * 100)}% alike)"
|
|
597
|
+
return True, found[0], ""
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
# A value never runs across a clause: a quote mark (not an apostrophe in a word), ";", a sentence end, or "then"
|
|
601
|
+
# outside quotes. A longer request once filled a shorter template's last blank with its other clauses (12 Oct 2026).
|
|
602
|
+
CLAUSE = re.compile(r"(?<![^\W_])['\"‘’“”]|['\"‘’“”](?![^\W_])|;|[.!?](?=\s)")
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def crosses(template, name, value):
|
|
606
|
+
quoted = re.search(r"['\"‘’“”]\{" + name + r"\}['\"‘’“”]", template)
|
|
607
|
+
return bool(CLAUSE.search(value or "") or not quoted and re.search(r"\bthen\b", value or "", re.I))
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def code_fills(template, goal):
|
|
611
|
+
"""The ways code fills ``template`` into ``goal``; a value crossing a clause is no fill: the run is the model's."""
|
|
612
|
+
template, goal = normalized(template).rstrip(" .!?"), normalized(goal).rstrip(" .!?")
|
|
613
|
+
parts = re.split(r"(\{p\d+\})", template)
|
|
614
|
+
if not BLANK.search(template):
|
|
615
|
+
return [{}] if folded(template) == folded(goal) else []
|
|
616
|
+
fills = []
|
|
617
|
+
for lazy in (True, False):
|
|
618
|
+
pattern = ""
|
|
619
|
+
for part in parts:
|
|
620
|
+
found = re.fullmatch(r"\{(p\d+)\}", part)
|
|
621
|
+
pattern += f"(?P<{found.group(1)}>.+{'?' if lazy else ''})" if found else re.escape(part)
|
|
622
|
+
hit = re.fullmatch(pattern, goal, flags=re.IGNORECASE | re.S)
|
|
623
|
+
if hit:
|
|
624
|
+
fill_ = {name: normalized(value) for name, value in hit.groupdict().items()}
|
|
625
|
+
if fill_ not in fills and not any(crosses(template, name, v) for name, v in fill_.items()):
|
|
626
|
+
fills.append(fill_)
|
|
627
|
+
return fills
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def match(client, store, *, goal, app, device, secret=None, timeout=30, taught_only=False):
|
|
631
|
+
"""(Matched, usage) for ``goal``, or (None, usage)."""
|
|
632
|
+
fillable = []
|
|
633
|
+
for path in store.candidates(app, device, goal, taught_only=taught_only):
|
|
634
|
+
fills = code_fills((path.get("identity") or {}).get("template") or "", goal)
|
|
635
|
+
if fills:
|
|
636
|
+
fillable.append((path, fills))
|
|
637
|
+
if not fillable:
|
|
638
|
+
return None, {}
|
|
639
|
+
if len(fillable) == 1 and len(fillable[0][1]) == 1:
|
|
640
|
+
return accept(fillable[0][0], fillable[0][1][0], goal, secret=secret), {}
|
|
641
|
+
candidates = [path for path, _ in fillable]
|
|
642
|
+
listing = []
|
|
643
|
+
for index, path in enumerate(candidates):
|
|
644
|
+
template = (path.get("identity") or {}).get("template") or ""
|
|
645
|
+
roles = blank_roles(path)
|
|
646
|
+
uses = "; ".join(f"{{{name}}}: {roles.get(name, 'a value')}" for name in path.get("blanks") or ())
|
|
647
|
+
listing.append(f"{index}. {template}" + (f" ({uses})" if uses else ""))
|
|
648
|
+
messages = [{"role": "system", "content": MATCH_SYSTEM}, {"role": "user", "content": [
|
|
649
|
+
{"type": "text", "text": f"Request: {normalized(goal)}\n\nPrograms:\n" + "\n".join(listing)}]}]
|
|
650
|
+
try:
|
|
651
|
+
out, usage = client.complete(messages, MATCH_SCHEMA, timeout=timeout)
|
|
652
|
+
except Exception:
|
|
653
|
+
return None, {}
|
|
654
|
+
pick = (out or {}).get("program")
|
|
655
|
+
if not isinstance(pick, int) or isinstance(pick, bool) or not 0 <= pick < len(candidates):
|
|
656
|
+
return None, usage
|
|
657
|
+
path = candidates[pick]
|
|
658
|
+
values = {}
|
|
659
|
+
for pair in (out or {}).get("values") or ():
|
|
660
|
+
if isinstance(pair, dict) and isinstance(pair.get("blank"), str) and isinstance(pair.get("value"), str):
|
|
661
|
+
values[pair["blank"].strip("{} ")] = normalized(pair["value"])
|
|
662
|
+
return accept(path, values, goal, secret=secret), usage
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def accept(path, values, goal, *, secret=None):
|
|
666
|
+
"""A Matched when ``values`` fill the template into ``goal``, each the request's words and no secret; else None."""
|
|
667
|
+
template = (path.get("identity") or {}).get("template") or ""
|
|
668
|
+
names = set(BLANK.findall(template))
|
|
669
|
+
if set(values) != names:
|
|
670
|
+
return None
|
|
671
|
+
asked = folded(goal)
|
|
672
|
+
for name, value in values.items():
|
|
673
|
+
if not value or folded(value) not in asked or (secret is not None and secret(value)) \
|
|
674
|
+
or crosses(template, name, value):
|
|
675
|
+
return None
|
|
676
|
+
if folded(fill(template, values)) != asked:
|
|
677
|
+
return None
|
|
678
|
+
return Matched(path, values)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def holds_secret(value):
|
|
682
|
+
"""True for a value secret_filter would mask: it never fills a blank."""
|
|
683
|
+
try:
|
|
684
|
+
from ..secret_filter import redact
|
|
685
|
+
return redact(value) != value
|
|
686
|
+
except Exception:
|
|
687
|
+
return True
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
__all__ = ["Programs", "programs_dir", "public", "code_fills", "workflow_words", "Matched", "PROVISIONAL", "TRUSTED",
|
|
691
|
+
"mode", "taught_on"]
|