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