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,373 @@
|
|
|
1
|
+
"""Words to a workflow graph (docs/workflows.mdx › Make a workflow from words): the model proposes, the person confirms.
|
|
2
|
+
|
|
3
|
+
``POST /api/workflows/draft {"words"}`` asks the drafter (Claude Haiku 5.5, as Save as workflow's) for a plan: steps
|
|
4
|
+
with what each waits for. Mobster compiles the plan into a graph by code, checks it as a save would
|
|
5
|
+
(graph/schema.py), and when the check finds problems gives the drafter the list, at most ``REPAIRS`` times. The answer
|
|
6
|
+
is the best graph with any problem still in it listed, so the canvas can mark them. Nothing is saved.
|
|
7
|
+
|
|
8
|
+
With ``graph`` (the workflow as the canvas has it) the words change that workflow: the drafter returns the whole
|
|
9
|
+
plan with the change, steps it keeps keep their ids and places, and the answer adds ``patch``, each change as one
|
|
10
|
+
item the person accepts or rejects.
|
|
11
|
+
|
|
12
|
+
The words are the person's, so they may fill a step's words. Nothing from a phone's screen reaches the drafter.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
from ..catalog import APPS, pack_apps
|
|
19
|
+
from ..workflows import starters
|
|
20
|
+
from . import chain, edit, schema
|
|
21
|
+
from .catalog import MAX_TIMES
|
|
22
|
+
|
|
23
|
+
REPAIRS = 2
|
|
24
|
+
# The page waits a minute for a draft (lib/workflow-graph.ts DRAFT_MS): the model's tries and their repairs end inside
|
|
25
|
+
# 45 seconds, and the best plan so far is the answer, so a slow repair never leaves the page with nothing (ICP-10).
|
|
26
|
+
BUDGET_S, TRY_S, LEAST_S = 45, 40, 8
|
|
27
|
+
STEP_ID = re.compile(r"[^A-Za-z0-9_-]+")
|
|
28
|
+
KINDS = ("task", "read", "check", "wait", "send", "branch", "loop", "approval", "merge")
|
|
29
|
+
# The canvas draws a node 320 wide and 64 tall (geometry.ts NODE_W, NODE_H): side by side 368 apart, rows 128 (DR-66).
|
|
30
|
+
PITCH, SIDE = 128, 368
|
|
31
|
+
|
|
32
|
+
PLAN_SCHEMA = {"type": "object", "additionalProperties": False, "required": ["name", "schedule", "steps"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"name": {"type": "string"},
|
|
35
|
+
"schedule": {"type": ["string", "null"]},
|
|
36
|
+
"steps": {"type": "array", "items": {
|
|
37
|
+
"type": "object", "additionalProperties": False,
|
|
38
|
+
"required": ["id", "kind", "text", "after", "app", "reader", "seconds", "to", "cases",
|
|
39
|
+
"over", "times", "inside"],
|
|
40
|
+
"properties": {
|
|
41
|
+
"id": {"type": "string"}, "kind": {"type": "string", "enum": list(KINDS)},
|
|
42
|
+
"text": {"type": ["string", "null"]},
|
|
43
|
+
"after": {"type": "array", "items": {"type": "string"}},
|
|
44
|
+
"app": {"type": ["string", "null"]}, "reader": {"type": ["string", "null"]},
|
|
45
|
+
"seconds": {"type": ["integer", "null"]}, "to": {"type": ["string", "null"]},
|
|
46
|
+
"cases": {"type": ["array", "null"], "items": {
|
|
47
|
+
"type": "object", "additionalProperties": False, "required": ["id", "label", "when"],
|
|
48
|
+
"properties": {"id": {"type": "string"}, "label": {"type": "string"},
|
|
49
|
+
"when": {"type": "string"}}}},
|
|
50
|
+
"over": {"type": ["string", "null"]}, "times": {"type": ["integer", "null"]},
|
|
51
|
+
"inside": {"type": ["string", "null"]}}}}}}
|
|
52
|
+
|
|
53
|
+
SYSTEM = """You turn what a person wants Mobster to do on their iPhone into a workflow plan: steps, and what each \
|
|
54
|
+
step waits for. Mobster checks the plan and draws it as a graph the person edits and confirms.
|
|
55
|
+
|
|
56
|
+
Steps (kind):
|
|
57
|
+
- task: Mobster's agent does something on the phone. text: the person's own instruction, one short sentence. app: \
|
|
58
|
+
the app it starts in (one of the app ids listed), or null for the Home Screen. Write a value the person would change \
|
|
59
|
+
next time as a blank, {name: value}, like "Text {who: Sam} that I'm running late".
|
|
60
|
+
- read: read an app's rows by code with no model. app: a pack key, reader: one of its readers (listed).
|
|
61
|
+
- check: look at the phone and answer true or false. text: the question.
|
|
62
|
+
- wait: wait. seconds: 5 to 86400.
|
|
63
|
+
- send: send what arrives to "notification", "clipboard" or "file". to: one of those.
|
|
64
|
+
- branch: choose a path. cases: [{id, label, when}], when an expression over $input, like \
|
|
65
|
+
contains($input.answer, 'refund') or count($input.rows) > 3. A later step waits for "<branch id>:<case id>", or \
|
|
66
|
+
"<branch id>:else".
|
|
67
|
+
- loop: repeat the steps inside it. over: "items" (each row a read step found; put the read step in after), "times" \
|
|
68
|
+
(with times) or "phones" (each phone). A step inside a loop sets inside to the loop's id.
|
|
69
|
+
- approval: wait for the person to approve. text: what to ask. Later steps wait for "<id>:approved" or "<id>:declined".
|
|
70
|
+
- merge: join paths that split.
|
|
71
|
+
|
|
72
|
+
after: the ids a step waits for. An empty list starts when the workflow starts (or, inside a loop, each round). \
|
|
73
|
+
Steps that don't wait for each other run side by side, on different phones when they can.
|
|
74
|
+
schedule: a 5-field cron like "0 9 * * *" when the person said when it runs, else null.
|
|
75
|
+
name: two to five words in sentence case.
|
|
76
|
+
|
|
77
|
+
Plan only what the person asked for. Ask for an approval before anything that sends, posts, buys or deletes when the \
|
|
78
|
+
person asked to check first. The person's words are the request; nothing else is."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _apps():
|
|
82
|
+
"""{id: name} of the apps a drafted task may start in: the catalog's, and each app pack's app the catalog doesn't
|
|
83
|
+
name (Instagram, X, YouTube...). Without them a plan for Instagram went through Safari (ICP-10)."""
|
|
84
|
+
apps = {app["id"]: app["name"] for app in APPS}
|
|
85
|
+
named = set(apps.values())
|
|
86
|
+
apps.update({bundle: name for _key, bundle, name in pack_apps() if name not in named})
|
|
87
|
+
return apps
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _task_app(value):
|
|
91
|
+
"""A task's app: a listed id as it is, a pack's key ("instagram") as its app, else None (the Home Screen)."""
|
|
92
|
+
apps = _apps()
|
|
93
|
+
if value in apps:
|
|
94
|
+
return value
|
|
95
|
+
name = next((name for key, _bundle, name in pack_apps() if key == value), None)
|
|
96
|
+
return next((ident for ident, listed in apps.items() if listed == name), None) if name else None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _packs(context):
|
|
100
|
+
return {key: sorted(pack.get("readers") or {}) for key, pack in (context.packs or {}).items()}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def plan_of(graph):
|
|
104
|
+
"""A graph as the drafter reads it: its steps, in run order, in the plan's own shape."""
|
|
105
|
+
kinds = {"phone.task": "task", "app.read": "read", "phone.check": "check", "wait": "wait", "send": "send",
|
|
106
|
+
"branch": "branch", "loop": "loop", "approval": "approval", "merge": "merge"}
|
|
107
|
+
edges = graph.get("edges") or []
|
|
108
|
+
triggers = {n["id"] for n in graph["nodes"] if n["type"] in schema.TRIGGERS}
|
|
109
|
+
steps = []
|
|
110
|
+
for node in chain.ordered(graph):
|
|
111
|
+
kind = kinds.get(node["type"])
|
|
112
|
+
if kind is None:
|
|
113
|
+
continue
|
|
114
|
+
config = node.get("config") or {}
|
|
115
|
+
after = [e["source"] + ("" if e["sourcePort"] in ("out", "item", "rows") else ":" + e["sourcePort"])
|
|
116
|
+
for e in edges if e["target"] == node["id"] and e["source"] not in triggers
|
|
117
|
+
and not (e["sourcePort"] in ("item", "index") and e["source"] == node.get("parent"))]
|
|
118
|
+
steps.append({"id": node["id"], "kind": kind, "after": after, "inside": node.get("parent"),
|
|
119
|
+
"text": config.get("text") or config.get("question") or config.get("message"),
|
|
120
|
+
"app": config.get("appId") or config.get("app"), "reader": config.get("reader"),
|
|
121
|
+
"seconds": config.get("seconds"), "to": config.get("to"), "cases": config.get("cases"),
|
|
122
|
+
"over": config.get("over"), "times": config.get("times")})
|
|
123
|
+
schedule = next((n["config"].get("cron") for n in graph["nodes"] if n["type"] == "trigger.schedule"), None)
|
|
124
|
+
return {"steps": steps, "schedule": schedule}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def compile_plan(plan, before=None, zone="UTC"):
|
|
128
|
+
"""The graph a plan makes, by code. Steps the graph ``before`` had keep their places and titles."""
|
|
129
|
+
steps = plan.get("steps") or []
|
|
130
|
+
ids, seen = {}, set()
|
|
131
|
+
for index, step in enumerate(steps):
|
|
132
|
+
ident = STEP_ID.sub("-", str(step.get("id") or f"s{index + 1}")).strip("-")[:24] or f"s{index + 1}"
|
|
133
|
+
while ident in seen or ident in ("start", "schedule"):
|
|
134
|
+
ident = f"{ident[:20]}-{index + 1}"
|
|
135
|
+
seen.add(ident)
|
|
136
|
+
ids[str(step.get("id"))] = ident
|
|
137
|
+
nodes = [{"id": "start", "type": "trigger.manual", "config": {}}]
|
|
138
|
+
if plan.get("schedule"):
|
|
139
|
+
nodes.append({"id": "schedule", "type": "trigger.schedule",
|
|
140
|
+
"config": {"cron": plan["schedule"], "timezone": zone}})
|
|
141
|
+
edges = []
|
|
142
|
+
|
|
143
|
+
def edge(source, port, target, target_port="in"):
|
|
144
|
+
edges.append({"id": f"e{len(edges) + 1}", "source": source, "sourcePort": port, "target": target,
|
|
145
|
+
"targetPort": target_port})
|
|
146
|
+
by_id = {ids[str(step.get("id"))]: step for step in steps}
|
|
147
|
+
for step in steps:
|
|
148
|
+
ident, kind = ids[str(step.get("id"))], step.get("kind")
|
|
149
|
+
text = step.get("text") or ""
|
|
150
|
+
if kind == "task":
|
|
151
|
+
node = {"type": "phone.task", "config": {"text": text, "appId": _task_app(step.get("app"))}}
|
|
152
|
+
elif kind == "read":
|
|
153
|
+
node = {"type": "app.read", "config": {"app": step.get("app"), "reader": step.get("reader"), "n": 10}}
|
|
154
|
+
elif kind == "check":
|
|
155
|
+
node = {"type": "phone.check", "config": {"question": text}}
|
|
156
|
+
elif kind == "wait":
|
|
157
|
+
node = {"type": "wait", "config": {"seconds": step.get("seconds") or 60}}
|
|
158
|
+
elif kind == "send":
|
|
159
|
+
node = {"type": "send", "config": {"to": step.get("to") or "notification"}}
|
|
160
|
+
elif kind == "branch":
|
|
161
|
+
node = {"type": "branch", "config": {"cases": step.get("cases") or []}}
|
|
162
|
+
elif kind == "loop":
|
|
163
|
+
over = step.get("over") if step.get("over") in ("items", "times", "phones") else "times"
|
|
164
|
+
config = {"over": over}
|
|
165
|
+
if over == "times":
|
|
166
|
+
config["times"] = max(2, min(step.get("times") or 2, MAX_TIMES))
|
|
167
|
+
if over == "phones":
|
|
168
|
+
config["phones"] = "all"
|
|
169
|
+
node = {"type": "loop", "config": config}
|
|
170
|
+
elif kind == "approval":
|
|
171
|
+
node = {"type": "approval", "config": {"message": text or "Go on?"}}
|
|
172
|
+
else:
|
|
173
|
+
node = {"type": "merge", "config": {}}
|
|
174
|
+
node["id"] = ident
|
|
175
|
+
inside = ids.get(str(step.get("inside"))) if step.get("inside") is not None else None
|
|
176
|
+
if inside and by_id.get(inside, {}).get("kind") == "loop":
|
|
177
|
+
node["parent"] = inside
|
|
178
|
+
nodes.append(node)
|
|
179
|
+
waits = [str(a) for a in step.get("after") or []]
|
|
180
|
+
for wanted in waits:
|
|
181
|
+
source, _, port = wanted.partition(":")
|
|
182
|
+
source = ids.get(source)
|
|
183
|
+
if source is None:
|
|
184
|
+
continue
|
|
185
|
+
source_kind = by_id[source].get("kind")
|
|
186
|
+
if kind == "loop" and node["config"]["over"] == "items" and source_kind == "read" and not port:
|
|
187
|
+
edge(source, "rows", ident, "items")
|
|
188
|
+
else:
|
|
189
|
+
edge(source, port or "out", ident)
|
|
190
|
+
if not any(ids.get(w.partition(":")[0]) for w in waits):
|
|
191
|
+
if node.get("parent"):
|
|
192
|
+
edge(node["parent"], "item", ident)
|
|
193
|
+
else:
|
|
194
|
+
for trigger in [n["id"] for n in nodes if n["type"] in schema.TRIGGERS]:
|
|
195
|
+
edge(trigger, "out", ident)
|
|
196
|
+
graph = {"version": schema.VERSION, "inputs": [], "vars": {}, "nodes": nodes, "edges": edges,
|
|
197
|
+
"limits": dict((before or {}).get("limits") or schema.DEFAULT_LIMITS)}
|
|
198
|
+
_layout(graph, before)
|
|
199
|
+
return graph
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _layout(graph, before=None):
|
|
203
|
+
"""Places: what ``before`` had keeps its place; the rest in rows by how far from a trigger, side by side."""
|
|
204
|
+
kept = {node["id"]: node for node in (before or {}).get("nodes") or []}
|
|
205
|
+
nodes = {node["id"]: node for node in graph["nodes"]}
|
|
206
|
+
depth = {}
|
|
207
|
+
|
|
208
|
+
def level(ident, trail=()):
|
|
209
|
+
if ident in depth:
|
|
210
|
+
return depth[ident]
|
|
211
|
+
if ident in trail:
|
|
212
|
+
return 0
|
|
213
|
+
sources = [e["source"] for e in graph["edges"] if e["target"] == ident and e["source"] in nodes
|
|
214
|
+
and nodes[e["source"]].get("parent") == nodes[ident].get("parent")]
|
|
215
|
+
depth[ident] = 1 + max((level(s, trail + (ident,)) for s in sources), default=-1)
|
|
216
|
+
return depth[ident]
|
|
217
|
+
rows = {}
|
|
218
|
+
for ident, node in nodes.items():
|
|
219
|
+
old = kept.get(ident)
|
|
220
|
+
if old is not None and old.get("type") == node["type"] and old.get("parent") == node.get("parent"):
|
|
221
|
+
node["position"] = dict(old.get("position") or {"x": 0, "y": 0})
|
|
222
|
+
for key in ("title", "notes"):
|
|
223
|
+
if old.get(key):
|
|
224
|
+
node[key] = old[key]
|
|
225
|
+
continue
|
|
226
|
+
row = (node.get("parent"), level(ident))
|
|
227
|
+
slot = rows[row] = rows.get(row, -1) + 1
|
|
228
|
+
# Triggers sit in the first row at their own, narrower spacing (chain.SIDE), as the canvas draws them.
|
|
229
|
+
side = chain.SIDE if node["type"].startswith("trigger.") else SIDE
|
|
230
|
+
node["position"] = ({"x": 24 + side * slot, "y": 64 + PITCH * row[1]} if node.get("parent")
|
|
231
|
+
else {"x": side * slot, "y": PITCH * row[1]})
|
|
232
|
+
# One new step under exactly two triggers sits centred under them (chain.STEPS_X), so both lines meet it in the
|
|
233
|
+
# middle.
|
|
234
|
+
top = [n for n in nodes.values() if not n.get("parent")]
|
|
235
|
+
triggers = [n for n in top if n["type"].startswith("trigger.")]
|
|
236
|
+
under = [n for n in top if depth.get(n["id"]) == 1 and n["id"] not in kept]
|
|
237
|
+
if len(triggers) == 2 and len(under) == 1:
|
|
238
|
+
under[0]["position"]["x"] = chain.STEPS_X
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _prompt(words, context, current=None, problems=None):
|
|
242
|
+
lines = [f"What the person wants: {words}", "",
|
|
243
|
+
"App ids (name): " + ", ".join(f"{ident} ({name})" for ident, name in sorted(_apps().items())),
|
|
244
|
+
"Packs and their readers: " + "; ".join(f"{key}: {', '.join(readers)}"
|
|
245
|
+
for key, readers in _packs(context).items())]
|
|
246
|
+
if current is not None:
|
|
247
|
+
import json
|
|
248
|
+
lines += ["", "The workflow now (keep the ids of steps that stay):",
|
|
249
|
+
json.dumps(current, ensure_ascii=False)[:6000]]
|
|
250
|
+
if problems:
|
|
251
|
+
lines += ["", "Your last plan had these problems. Fix them and return the whole plan:"]
|
|
252
|
+
lines += [f"- {problem.get('node') or problem.get('edge') or ''}: {problem['message']}" for problem in problems]
|
|
253
|
+
return "\n".join(lines)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
DAYS = {"sunday": 0, "monday": 1, "tuesday": 2, "wednesday": 3, "thursday": 4, "friday": 5, "saturday": 6}
|
|
257
|
+
PARTS = {"morning": 9, "afternoon": 14, "evening": 18, "night": 21, "day": 9, "weekday": 9, "weekend": 9}
|
|
258
|
+
WHEN = re.compile(r"\b(?:every|each)\s+(?:(?P<part>morning|afternoon|evening|night|day|weekday|weekend|hour)|"
|
|
259
|
+
r"(?P<name>sunday|monday|tuesday|wednesday|thursday|friday|saturday))s?"
|
|
260
|
+
r"(?:\s+(?:at\s+)?(?P<h>\d{1,2})(?::(?P<m>\d{2}))?\s*(?P<ap>[ap])\.?m\.?)?\b\.?", re.I)
|
|
261
|
+
OPEN_APP = re.compile(r"^open\s+(?:the\s+)?(?P<name>.+?)(?:\s+app)?(?:\s+on\b.*)?$", re.I)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _cron_of(found):
|
|
265
|
+
"""The 5-field cron a schedule phrase means, or None when its time can't be read (an hour past 12 with am/pm)."""
|
|
266
|
+
part, name = (found["part"] or "").lower(), (found["name"] or "").lower()
|
|
267
|
+
if part == "hour":
|
|
268
|
+
return "0 * * * *" if not found["h"] else None
|
|
269
|
+
hour, minute = PARTS.get(part, 9), int(found["m"] or 0)
|
|
270
|
+
if found["h"]:
|
|
271
|
+
hour = int(found["h"])
|
|
272
|
+
if found["ap"]:
|
|
273
|
+
if not 1 <= hour <= 12:
|
|
274
|
+
return None
|
|
275
|
+
hour = hour % 12 + (12 if found["ap"].lower() == "p" else 0)
|
|
276
|
+
if not 0 <= hour <= 23 or not 0 <= minute <= 59:
|
|
277
|
+
return None
|
|
278
|
+
days = {"weekday": "1-5", "weekend": "0,6"}.get(part) or (str(DAYS[name]) if name else "*")
|
|
279
|
+
return f"{minute} {hour} * * {days}"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def schedule_of(words):
|
|
283
|
+
for found in WHEN.finditer(words or ""):
|
|
284
|
+
cron = _cron_of(found)
|
|
285
|
+
if cron is not None:
|
|
286
|
+
rest = (words[:found.start()] + " " + words[found.end():]).strip(" ,;")
|
|
287
|
+
return cron, re.sub(r"\s+", " ", re.sub(r"\s+,", ",", rest)).strip(" ,;") or words
|
|
288
|
+
return None, words
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _clauses(sentence):
|
|
292
|
+
"""A sentence's steps: "open X, read Y, and tell me Z" is three, split at commas and "and then"."""
|
|
293
|
+
parts = [p.strip() for p in re.split(r",\s*(?:and\s+|then\s+)?|\s+and\s+then\s+", sentence) if p.strip()]
|
|
294
|
+
return parts if len(parts) > 1 and all(len(p.split()) >= 3 for p in parts) else [sentence]
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def local(words):
|
|
298
|
+
schedule, words = schedule_of(words)
|
|
299
|
+
sentences = []
|
|
300
|
+
for part in re.split(r"(?<=[.!?])\s+|\n+|,?\s+then\s+", words or ""):
|
|
301
|
+
part = re.sub(r"^(?:and\s+)?then\s+", "", part.strip(), flags=re.I)
|
|
302
|
+
for clause in _clauses(part) if part else []:
|
|
303
|
+
sentences.append(clause[:1].upper() + clause[1:])
|
|
304
|
+
cut = len(sentences) - 12
|
|
305
|
+
sentences = sentences[:12]
|
|
306
|
+
names = {app["name"].casefold(): app["id"] for app in APPS}
|
|
307
|
+
opened = OPEN_APP.match(sentences[0].rstrip(".")) if sentences else None
|
|
308
|
+
start_app = names.get(opened["name"].casefold()) if opened else None
|
|
309
|
+
steps = [{"id": f"s{index + 1}", "kind": "task", "text": text.rstrip("."), "after": [f"s{index}"] if index else [],
|
|
310
|
+
"app": start_app if index == 0 else None, "reader": None, "seconds": None, "to": None, "cases": None,
|
|
311
|
+
"over": None, "times": None, "inside": None} for index, text in enumerate(sentences)]
|
|
312
|
+
return {"name": starters.suggest_name(sentences[0]) if sentences else "Workflow", "schedule": schedule,
|
|
313
|
+
"steps": steps, "cut": max(0, cut)}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def draft(words, context, *, client=None, current=None, zone="UTC", budget=BUDGET_S):
|
|
317
|
+
if not isinstance(words, str) or not 1 <= len(words.strip()) <= 2000:
|
|
318
|
+
raise ValueError("Say what the workflow does in 1 to 2,000 characters")
|
|
319
|
+
words = " ".join(words.split())
|
|
320
|
+
shown = plan_of(current) if current is not None else None
|
|
321
|
+
best, problems, drafter = None, [], "local"
|
|
322
|
+
if client is not None:
|
|
323
|
+
deadline = time.monotonic() + budget
|
|
324
|
+
for _attempt in range(REPAIRS + 1):
|
|
325
|
+
left = deadline - time.monotonic()
|
|
326
|
+
if left < LEAST_S:
|
|
327
|
+
break
|
|
328
|
+
messages = [{"role": "system", "content": SYSTEM},
|
|
329
|
+
{"role": "user", "content": [{"type": "text", "text": _prompt(words, context, shown,
|
|
330
|
+
problems)}]}]
|
|
331
|
+
try:
|
|
332
|
+
plan, _usage = client.complete(messages, PLAN_SCHEMA, timeout=min(TRY_S, left))
|
|
333
|
+
except Exception: # noqa: BLE001 -- the model is optional here; the rule drafts instead
|
|
334
|
+
break
|
|
335
|
+
if not isinstance(plan, dict) or not plan.get("steps"):
|
|
336
|
+
break
|
|
337
|
+
drafter = getattr(client, "model", "model")
|
|
338
|
+
graph = compile_plan(plan, current, zone)
|
|
339
|
+
try:
|
|
340
|
+
best, problems = (plan, schema.validate(graph, context)), []
|
|
341
|
+
break
|
|
342
|
+
except schema.GraphError as error:
|
|
343
|
+
best, problems = (plan, graph), error.errors
|
|
344
|
+
if best is None:
|
|
345
|
+
plan = local(words)
|
|
346
|
+
graph = compile_plan(plan, current, zone)
|
|
347
|
+
try:
|
|
348
|
+
graph, problems = schema.validate(graph, context), []
|
|
349
|
+
except schema.GraphError as error:
|
|
350
|
+
problems = error.errors
|
|
351
|
+
best, drafter = (plan, graph), "local"
|
|
352
|
+
plan, graph = best
|
|
353
|
+
name = " ".join(str(plan.get("name") or "").split()).rstrip(".")[:60] or starters.suggest_name(words)
|
|
354
|
+
out = {"draft": {"name": name, "graph": graph}, "drafter": drafter, "errors": problems,
|
|
355
|
+
"drafted_by": "rules" if drafter == "local" else "model"}
|
|
356
|
+
if drafter == "local":
|
|
357
|
+
# The rules read an English schedule, a step per sentence (twelve at most), and nothing else: the page says so.
|
|
358
|
+
out["notes"] = ["Drafted by simple rules, not a model: event triggers, loops, approvals and schedules that "
|
|
359
|
+
"aren't in English are not read. Connect an AI account or sign in to a coding agent to draft "
|
|
360
|
+
"with a model." if client is None else
|
|
361
|
+
"The model didn't answer, so simple rules drafted this: event triggers, loops, approvals and "
|
|
362
|
+
"schedules that aren't in English are not read."]
|
|
363
|
+
if plan.get("cut"):
|
|
364
|
+
out["notes"].append(f"Only the first 12 steps were kept; {plan['cut']} more were left out.")
|
|
365
|
+
if current is not None:
|
|
366
|
+
changes = edit.diff(current, graph)
|
|
367
|
+
out["patch"] = ([{"op": "add", "node": n["id"], "after": n} for n in changes["nodes"]["added"]]
|
|
368
|
+
+ [{"op": "remove", "node": n["id"], "before": n} for n in changes["nodes"]["removed"]]
|
|
369
|
+
+ [{"op": "change", "node": c["id"], "before": c["before"], "after": c["after"]}
|
|
370
|
+
for c in changes["nodes"]["changed"]]
|
|
371
|
+
+ [{"op": "add", "edge": e["id"], "after": e} for e in changes["edges"]["added"]]
|
|
372
|
+
+ [{"op": "remove", "edge": e["id"], "before": e} for e in changes["edges"]["removed"]])
|
|
373
|
+
return out
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Edits to a saved graph that aren't the canvas's own: the old schedule fields applied to its schedule trigger, and
|
|
2
|
+
what changed between two versions (a diff, and a line for the versions list)."""
|
|
3
|
+
|
|
4
|
+
from . import chain, schema
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def with_schedule(graph, cron, zone):
|
|
8
|
+
nodes = [dict(node) for node in graph["nodes"]]
|
|
9
|
+
edges = [dict(edge) for edge in graph.get("edges") or []]
|
|
10
|
+
schedule = next((node for node in nodes if node["type"] == "trigger.schedule"), None)
|
|
11
|
+
if cron is None:
|
|
12
|
+
if schedule is None:
|
|
13
|
+
return graph
|
|
14
|
+
return {**graph, "nodes": [n for n in nodes if n is not schedule],
|
|
15
|
+
"edges": [e for e in edges if e["source"] != schedule["id"]]}
|
|
16
|
+
if schedule is not None:
|
|
17
|
+
schedule["config"] = {"cron": cron, "timezone": zone}
|
|
18
|
+
return {**graph, "nodes": nodes, "edges": edges}
|
|
19
|
+
taken = {node["id"] for node in nodes} | {edge["id"] for edge in edges}
|
|
20
|
+
ident = chain._unique("schedule", taken)
|
|
21
|
+
manual = next((node for node in nodes if node["type"] == "trigger.manual"), None)
|
|
22
|
+
at = (manual or {}).get("position") or {"x": 0, "y": 0}
|
|
23
|
+
nodes.append({"id": ident, "type": "trigger.schedule", "config": {"cron": cron, "timezone": zone},
|
|
24
|
+
"position": {"x": at["x"] + chain.SIDE, "y": at["y"]}})
|
|
25
|
+
for edge in [e for e in edges if manual is not None and e["source"] == manual["id"]]:
|
|
26
|
+
edges.append({**edge, "id": chain._unique("e" + ident, taken), "source": ident})
|
|
27
|
+
return {**graph, "nodes": nodes, "edges": edges}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def diff(before, after):
|
|
31
|
+
"""{"nodes": {"added", "removed", "changed": [{"id", "before", "after"}]}, "edges": {"added", "removed"}}."""
|
|
32
|
+
old = {node["id"]: node for node in before.get("nodes") or []}
|
|
33
|
+
new = {node["id"]: node for node in after.get("nodes") or []}
|
|
34
|
+
old_edges = {edge["id"]: edge for edge in before.get("edges") or []}
|
|
35
|
+
new_edges = {edge["id"]: edge for edge in after.get("edges") or []}
|
|
36
|
+
return {"nodes": {"added": [new[i] for i in new if i not in old], "removed": [old[i] for i in old if i not in new],
|
|
37
|
+
"changed": [{"id": i, "before": old[i], "after": new[i]} for i in new if i in old
|
|
38
|
+
and old[i] != new[i]]},
|
|
39
|
+
"edges": {"added": [new_edges[i] for i in new_edges if old_edges.get(i) != new_edges[i]],
|
|
40
|
+
"removed": [old_edges[i] for i in old_edges if new_edges.get(i) != old_edges[i]]},
|
|
41
|
+
"settings": {key: {"before": before.get(key), "after": after.get(key)}
|
|
42
|
+
for key in ("inputs", "vars", "limits") if before.get(key) != after.get(key)}}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def change_line(before, after):
|
|
46
|
+
"""What changed between two versions' graphs, in a line: "Added 2 steps, changed Reply"."""
|
|
47
|
+
if before is None:
|
|
48
|
+
return "Created"
|
|
49
|
+
old = {node["id"]: node for node in before.get("nodes") or []}
|
|
50
|
+
new = {node["id"]: node for node in after.get("nodes") or []}
|
|
51
|
+
|
|
52
|
+
def label(node):
|
|
53
|
+
known = schema.TYPES.get(node["type"])
|
|
54
|
+
return node.get("title") or (known.label if known else node["id"])
|
|
55
|
+
|
|
56
|
+
added = [new[i] for i in new if i not in old]
|
|
57
|
+
removed = [old[i] for i in old if i not in new]
|
|
58
|
+
changed = [new[i] for i in new if i in old and {k: v for k, v in new[i].items() if k != "position"}
|
|
59
|
+
!= {k: v for k, v in old[i].items() if k != "position"}]
|
|
60
|
+
moved = [i for i in new if i in old and new[i].get("position") != old[i].get("position")]
|
|
61
|
+
parts = []
|
|
62
|
+
if added:
|
|
63
|
+
parts.append(f"Added {len(added)} step{'s' * (len(added) > 1)}" if len(added) > 1
|
|
64
|
+
else f"Added {label(added[0])}")
|
|
65
|
+
if removed:
|
|
66
|
+
parts.append(f"removed {len(removed)} steps" if len(removed) > 1 else f"removed {label(removed[0])}")
|
|
67
|
+
if changed:
|
|
68
|
+
parts.append(f"changed {len(changed)} steps" if len(changed) > 1 else f"changed {label(changed[0])}")
|
|
69
|
+
if (before.get("edges") or []) != (after.get("edges") or []) and not (added or removed):
|
|
70
|
+
parts.append("rewired")
|
|
71
|
+
if (not parts and (moved or before.get("limits") != after.get("limits")
|
|
72
|
+
or before.get("inputs") != after.get("inputs")
|
|
73
|
+
or before.get("vars") != after.get("vars"))):
|
|
74
|
+
parts.append("Rearranged" if moved else "Changed its settings")
|
|
75
|
+
line = ", ".join(parts) or "Saved"
|
|
76
|
+
return line[0].upper() + line[1:]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""What a workflow graph should cost before it runs (GET /api/workflows/{id}/estimate): each node's range in dollars,
|
|
2
|
+
its model calls and who decides what it does, read from the node's own last runs when it has them, else from Mobster's
|
|
3
|
+
agent's estimate for its words. A loop multiplies its steps by its rounds when the run's rounds are known before it
|
|
4
|
+
starts; when they aren't (items, until), the estimate is for one round and says so (``partial``)."""
|
|
5
|
+
|
|
6
|
+
from statistics import median
|
|
7
|
+
|
|
8
|
+
from . import catalog
|
|
9
|
+
|
|
10
|
+
HISTORY_RUNS = 5
|
|
11
|
+
PHONE = frozenset({"phone.task", "phone.taught", "phone.check"})
|
|
12
|
+
CHARS_PER_TOKEN = 4
|
|
13
|
+
ASK_OUTPUT_TOKENS = (200, 2000) # an ai.ask answer, short to long, before it has run
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def estimate(graph, *, history, agent, rounds, child=None, ask_cost=None, replays=None):
|
|
17
|
+
"""Each node's cost range and model calls, and the total, from its history or the agent's estimate."""
|
|
18
|
+
by_id = {node["id"]: node for node in graph.get("nodes") or ()}
|
|
19
|
+
children = {}
|
|
20
|
+
for node in by_id.values():
|
|
21
|
+
children.setdefault(node.get("parent"), []).append(node["id"])
|
|
22
|
+
out = {}
|
|
23
|
+
|
|
24
|
+
def one(node):
|
|
25
|
+
kind, config = node["type"], node.get("config") or {}
|
|
26
|
+
past = (history.get(node["id"]) or [])[:HISTORY_RUNS]
|
|
27
|
+
if kind == "phone.task" and config.get("mode") == "replay":
|
|
28
|
+
return {"lowUsd": 0.0, "highUsd": 0.0, "modelCalls": 0, "basis": "replay", "decides": "replays"}
|
|
29
|
+
if kind in ("phone.task", "phone.taught") and replays is not None and (decides := replays(node)):
|
|
30
|
+
return {"lowUsd": 0.0, "highUsd": 0.0, "modelCalls": 0, "basis": "replay", "decides": decides}
|
|
31
|
+
if kind in PHONE or kind == "ai.ask":
|
|
32
|
+
if past:
|
|
33
|
+
costs, calls = [p["costUsd"] for p in past], [p.get("modelCalls") for p in past]
|
|
34
|
+
return {"lowUsd": round(min(costs), 4), "highUsd": round(max(costs), 4),
|
|
35
|
+
"modelCalls": None if None in calls else round(median(calls)), "basis": "history",
|
|
36
|
+
"runs": len(past), **({"decides": _decides(kind, past[0])} if kind in PHONE else {})}
|
|
37
|
+
if kind == "ai.ask":
|
|
38
|
+
return _ask(config, ask_cost)
|
|
39
|
+
words = config.get("text") or config.get("question") or ""
|
|
40
|
+
guess = agent(words, config.get("outputSchema")) if words or kind == "phone.taught" else None
|
|
41
|
+
entry = {"lowUsd": 0.0 if kind == "phone.taught" else (guess or {}).get("lowUsd"),
|
|
42
|
+
"highUsd": (guess or {}).get("highUsd"), "modelCalls": None, "basis": "estimate",
|
|
43
|
+
"decides": "taught" if kind == "phone.taught" else "model"}
|
|
44
|
+
if (guess or {}).get("billing") == "plan":
|
|
45
|
+
entry["billing"] = "plan"
|
|
46
|
+
return entry
|
|
47
|
+
if kind == "workflow.call":
|
|
48
|
+
total = child(node) if child is not None else None
|
|
49
|
+
if total is None:
|
|
50
|
+
return {"lowUsd": None, "highUsd": None, "modelCalls": None, "basis": "child"}
|
|
51
|
+
return {**{k: total.get(k) for k in ("lowUsd", "highUsd", "modelCalls")}, "basis": "child",
|
|
52
|
+
**({"partial": True} if total.get("partial") else {})}
|
|
53
|
+
if kind == "loop":
|
|
54
|
+
return _loop(node, rounds(node))
|
|
55
|
+
return {"lowUsd": 0.0, "highUsd": 0.0, "modelCalls": 0, "basis": "none",
|
|
56
|
+
**({"decides": "code"} if kind == "app.read" else {})}
|
|
57
|
+
|
|
58
|
+
def _loop(node, count):
|
|
59
|
+
body = [walk(by_id[ident]) for ident in children.get(node["id"], ())]
|
|
60
|
+
for ident in children.get(node["id"], ()):
|
|
61
|
+
out[ident]["perRound"] = True
|
|
62
|
+
times = count if count is not None else 1
|
|
63
|
+
entry = _sum(body, times)
|
|
64
|
+
entry.update(basis="estimate", rounds=count)
|
|
65
|
+
if count is None:
|
|
66
|
+
entry["partial"] = True
|
|
67
|
+
return entry
|
|
68
|
+
|
|
69
|
+
def walk(node):
|
|
70
|
+
if node["id"] not in out:
|
|
71
|
+
out[node["id"]] = one(node)
|
|
72
|
+
return out[node["id"]]
|
|
73
|
+
|
|
74
|
+
top = [walk(node) for node in by_id.values() if node.get("parent") is None]
|
|
75
|
+
total = _sum(top, 1)
|
|
76
|
+
limit = (graph.get("limits") or {}).get("maxSpendUsd", catalog.DEFAULT_LIMITS["maxSpendUsd"])
|
|
77
|
+
total["overLimit"] = total["highUsd"] is not None and total["highUsd"] > limit
|
|
78
|
+
total.setdefault("partial", False)
|
|
79
|
+
return {"nodes": out, "total": total}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def of_workflow(owner, workflow, graph, depth=0):
|
|
83
|
+
from .. import engines
|
|
84
|
+
from ..api_errors import APIError
|
|
85
|
+
from ..model_clients import cost_usd
|
|
86
|
+
from . import nodes as graph_nodes
|
|
87
|
+
history = {}
|
|
88
|
+
for record in owner.batches.recent(workflow["id"], HISTORY_RUNS * 2):
|
|
89
|
+
for entry in owner.batches.nodes(record["id"]):
|
|
90
|
+
cost = entry.get("costUsd")
|
|
91
|
+
if entry.get("state") != "done" or entry.get("reused") or entry.get("pinned") \
|
|
92
|
+
or isinstance(cost, bool) or not isinstance(cost, (int, float)):
|
|
93
|
+
continue
|
|
94
|
+
history.setdefault(entry["node"], []).append(
|
|
95
|
+
{"costUsd": float(cost), "modelCalls": entry.get("modelCalls"), "replayed": entry.get("replayed")})
|
|
96
|
+
estimates = getattr(owner.runtime, "engine_estimates", None)
|
|
97
|
+
|
|
98
|
+
def agent(text, schema):
|
|
99
|
+
try:
|
|
100
|
+
# With no key for Mobster's agent a model step runs on the person's ready coding agent (Runtime.plan_agent,
|
|
101
|
+
# DR-56): on their plan, so it costs nothing per call, and the label says so instead of API prices (DR-33).
|
|
102
|
+
plan = getattr(owner.runtime, "plan_agent", None)
|
|
103
|
+
chosen = plan() if callable(plan) else None
|
|
104
|
+
if chosen:
|
|
105
|
+
return {"lowUsd": 0.0, "highUsd": 0.0, "billing": "plan", "agent": chosen}
|
|
106
|
+
except Exception: # noqa: BLE001 -- unknown: priced as Mobster's agent is, below
|
|
107
|
+
pass
|
|
108
|
+
try:
|
|
109
|
+
return estimates(text, schema)[engines.SMART] if callable(estimates) else None
|
|
110
|
+
except Exception: # noqa: BLE001 -- no estimate is said as unknown (None), never as free
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
def child(node):
|
|
114
|
+
called = (node.get("config") or {}).get("workflowId")
|
|
115
|
+
if depth >= catalog.MAX_CALL_DEPTH or not called:
|
|
116
|
+
return None
|
|
117
|
+
try:
|
|
118
|
+
return owner.estimate(called, _depth=depth + 1)["total"]
|
|
119
|
+
except (APIError, LookupError):
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
def rounds(node):
|
|
123
|
+
config = node.get("config") or {}
|
|
124
|
+
if config.get("over") == "times":
|
|
125
|
+
return config.get("times")
|
|
126
|
+
if config.get("over") == "rows":
|
|
127
|
+
return min(len(config["rows"]), config["max"]) if isinstance(config.get("rows"), list) else None
|
|
128
|
+
if config.get("over") == "phones":
|
|
129
|
+
try:
|
|
130
|
+
return len(graph_nodes.loop_phones(owner.runtime, config.get("phones"))[:config.get("max") or 1000])
|
|
131
|
+
except graph_nodes.NodeFailed:
|
|
132
|
+
return None
|
|
133
|
+
return None # items and until: told only as the run goes
|
|
134
|
+
|
|
135
|
+
def replays(node):
|
|
136
|
+
return replay_kind(owner, node)
|
|
137
|
+
|
|
138
|
+
return estimate(graph, history=history, agent=agent, rounds=rounds, child=child, replays=replays,
|
|
139
|
+
ask_cost=lambda model, tokens_in, tokens_out: cost_usd(
|
|
140
|
+
model, {"prompt_tokens": tokens_in, "completion_tokens": tokens_out}))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def replay_kind(owner, node):
|
|
144
|
+
from pathlib import Path
|
|
145
|
+
|
|
146
|
+
from ..learned.zero import replay_kind as kind_of
|
|
147
|
+
try:
|
|
148
|
+
state_db = getattr(getattr(owner.runtime, "config", None), "state_db", None)
|
|
149
|
+
if not state_db:
|
|
150
|
+
return None
|
|
151
|
+
config = node.get("config") or {}
|
|
152
|
+
goal = config.get("text")
|
|
153
|
+
if node.get("type") == "phone.taught":
|
|
154
|
+
with owner.journal.lock:
|
|
155
|
+
goal = (owner._get(config.get("workflowId")) or {}).get("goal")
|
|
156
|
+
if config.get("mode") == "agent":
|
|
157
|
+
return None
|
|
158
|
+
return kind_of(Path(state_db).parent, goal, learned=node.get("type") == "phone.task")
|
|
159
|
+
except Exception: # noqa: BLE001 -- unknown is priced as a model step, never as free
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _decides(kind, newest):
|
|
164
|
+
if kind == "phone.taught":
|
|
165
|
+
return "taught"
|
|
166
|
+
return "replays" if newest.get("replayed") else "model"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _ask(config, ask_cost):
|
|
170
|
+
from .nodes import AI_MODELS, AI_SYSTEM, INPUT_CHARS
|
|
171
|
+
model = AI_MODELS.get(config.get("model") or "haiku", AI_MODELS["haiku"])
|
|
172
|
+
base = (len(AI_SYSTEM) + len(str(config.get("instruction") or ""))) // CHARS_PER_TOKEN
|
|
173
|
+
low = ask_cost(model, base, ASK_OUTPUT_TOKENS[0]) if ask_cost is not None else None
|
|
174
|
+
high = ask_cost(model, base + INPUT_CHARS // CHARS_PER_TOKEN, ASK_OUTPUT_TOKENS[1]) if ask_cost is not None \
|
|
175
|
+
else None
|
|
176
|
+
return {"lowUsd": None if low is None else round(low, 4), "highUsd": None if high is None else round(high, 4),
|
|
177
|
+
"modelCalls": 1, "basis": "estimate"}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _sum(entries, times):
|
|
181
|
+
"""The entries' sum, ``times`` over; a part that can't be told (None) makes the whole None."""
|
|
182
|
+
out = {}
|
|
183
|
+
for key, digits in (("lowUsd", 4), ("highUsd", 4), ("modelCalls", None)):
|
|
184
|
+
values = [entry.get(key) for entry in entries]
|
|
185
|
+
out[key] = None if any(v is None for v in values) else \
|
|
186
|
+
(round(sum(values) * times, digits) if digits else sum(values) * times)
|
|
187
|
+
if any(entry.get("partial") for entry in entries):
|
|
188
|
+
out["partial"] = True
|
|
189
|
+
return out
|