polyrob 0.5.0__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.
- agents/README.md +453 -0
- agents/__init__.py +164 -0
- agents/base_agent.py +743 -0
- agents/personality/character.py +119 -0
- agents/personality/character_manager.py +309 -0
- agents/personality/characters/rob.character.json +56 -0
- agents/personality/characters/trump.character.json +63 -0
- agents/personality/persona_render.py +78 -0
- agents/personality/persona_resolver.py +115 -0
- agents/prompt/__init__.py +58 -0
- agents/prompt/base_prompt.py +92 -0
- agents/prompt/system.py +417 -0
- agents/task/README_PATH_MANAGEMENT.md +141 -0
- agents/task/__init__.py +162 -0
- agents/task/agent/__init__.py +70 -0
- agents/task/agent/agent_state.py +408 -0
- agents/task/agent/async_delegation.py +274 -0
- agents/task/agent/autonomy_state.py +275 -0
- agents/task/agent/conversation.py +75 -0
- agents/task/agent/core/__init__.py +6 -0
- agents/task/agent/core/aux_metering.py +61 -0
- agents/task/agent/core/background_review.py +157 -0
- agents/task/agent/core/construction.py +1179 -0
- agents/task/agent/core/conversational_exit.py +49 -0
- agents/task/agent/core/correspondent_gate.py +227 -0
- agents/task/agent/core/curator.py +246 -0
- agents/task/agent/core/episodic_digest.py +103 -0
- agents/task/agent/core/error_recovery.py +480 -0
- agents/task/agent/core/history_io.py +257 -0
- agents/task/agent/core/llm_provisioning.py +287 -0
- agents/task/agent/core/llm_runner.py +547 -0
- agents/task/agent/core/logging_io.py +276 -0
- agents/task/agent/core/loop_detection.py +262 -0
- agents/task/agent/core/memory_prefetch.py +325 -0
- agents/task/agent/core/memory_writer.py +667 -0
- agents/task/agent/core/model_introspection.py +82 -0
- agents/task/agent/core/model_swap.py +177 -0
- agents/task/agent/core/next_action_internal.py +1331 -0
- agents/task/agent/core/output_validation.py +303 -0
- agents/task/agent/core/project_context.py +272 -0
- agents/task/agent/core/resources.py +93 -0
- agents/task/agent/core/result_processing.py +389 -0
- agents/task/agent/core/run_loop.py +661 -0
- agents/task/agent/core/safety_lifecycle.py +228 -0
- agents/task/agent/core/secret_guard.py +293 -0
- agents/task/agent/core/self_wake.py +280 -0
- agents/task/agent/core/session_metadata.py +104 -0
- agents/task/agent/core/step.py +781 -0
- agents/task/agent/core/step_execution.py +398 -0
- agents/task/agent/core/step_telemetry.py +222 -0
- agents/task/agent/core/test_aux_metering.py +50 -0
- agents/task/agent/core/test_billing_failover.py +68 -0
- agents/task/agent/core/test_fatal_error_classifier.py +38 -0
- agents/task/agent/core/test_judge_validation.py +275 -0
- agents/task/agent/core/test_model_swap.py +190 -0
- agents/task/agent/core/test_reflection_offload.py +54 -0
- agents/task/agent/core/turn_input.py +8 -0
- agents/task/agent/core/untrusted_wrap.py +103 -0
- agents/task/agent/core/user_ingress.py +228 -0
- agents/task/agent/hitl_manager.py +396 -0
- agents/task/agent/log_sanitize.py +36 -0
- agents/task/agent/message_manager/config.py +97 -0
- agents/task/agent/message_manager/service.py +758 -0
- agents/task/agent/message_manager/tests.py +305 -0
- agents/task/agent/message_manager/tool_call_builder.py +539 -0
- agents/task/agent/message_manager/tool_message_repair.py +364 -0
- agents/task/agent/message_manager/views.py +143 -0
- agents/task/agent/messages/__init__.py +8 -0
- agents/task/agent/messages/builders.py +309 -0
- agents/task/agent/messages/compactor.py +611 -0
- agents/task/agent/messages/context_references.py +271 -0
- agents/task/agent/messages/filters.py +333 -0
- agents/task/agent/messages/guidance.py +328 -0
- agents/task/agent/messages/persistence.py +448 -0
- agents/task/agent/messages/retrieval.py +506 -0
- agents/task/agent/messages/sqlite_persistence.py +85 -0
- agents/task/agent/messages/test_compaction_upgrade.py +349 -0
- agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
- agents/task/agent/messages/test_sqlite_persistence.py +54 -0
- agents/task/agent/messages/token_counter.py +532 -0
- agents/task/agent/orchestrator.py +776 -0
- agents/task/agent/profile_manager.py +105 -0
- agents/task/agent/profile_registry.py +173 -0
- agents/task/agent/prompts.py +1090 -0
- agents/task/agent/scenario_registry.py +226 -0
- agents/task/agent/service.py +509 -0
- agents/task/agent/session.py +984 -0
- agents/task/agent/skill_discovery.py +110 -0
- agents/task/agent/skill_frontmatter.py +94 -0
- agents/task/agent/skill_manager.py +1201 -0
- agents/task/agent/skill_store.py +627 -0
- agents/task/agent/skill_validation.py +56 -0
- agents/task/agent/skill_writer.py +492 -0
- agents/task/agent/sub_agent_manager.py +1137 -0
- agents/task/agent/test_session_disk_recovery.py +34 -0
- agents/task/agent/tests.py +513 -0
- agents/task/agent/tool_call_tracker.py +511 -0
- agents/task/agent/views.py +469 -0
- agents/task/config.py +432 -0
- agents/task/constants.py +1384 -0
- agents/task/flag_defaults.py +63 -0
- agents/task/goals/__init__.py +1 -0
- agents/task/goals/autonomy_marker.py +28 -0
- agents/task/goals/board.py +690 -0
- agents/task/goals/completion_judge.py +247 -0
- agents/task/goals/context.py +62 -0
- agents/task/goals/dispatcher.py +848 -0
- agents/task/goals/escalation.py +102 -0
- agents/task/goals/planner.py +114 -0
- agents/task/logging_config.py +155 -0
- agents/task/path.py +991 -0
- agents/task/robust_parse_config.py +400 -0
- agents/task/runtime/__init__.py +0 -0
- agents/task/runtime/run_as_session.py +130 -0
- agents/task/runtime_safety.py +308 -0
- agents/task/session/__init__.py +5 -0
- agents/task/session/browser_pool.py +130 -0
- agents/task/session/cleanup.py +612 -0
- agents/task/session/execution.py +397 -0
- agents/task/session/feed.py +200 -0
- agents/task/session/hitl_ingress.py +201 -0
- agents/task/session/hooks.py +83 -0
- agents/task/session/multi_agent.py +243 -0
- agents/task/session/workspace.py +91 -0
- agents/task/session_registry.py +91 -0
- agents/task/session_route.py +35 -0
- agents/task/sqlite_session_registry.py +261 -0
- agents/task/surface_config.py +185 -0
- agents/task/telemetry/__init__.py +58 -0
- agents/task/telemetry/event_log.py +180 -0
- agents/task/telemetry/formatters.py +642 -0
- agents/task/telemetry/manager.py +571 -0
- agents/task/telemetry/memory_events.py +66 -0
- agents/task/telemetry/self_events.py +53 -0
- agents/task/telemetry/sequence.py +160 -0
- agents/task/telemetry/service.py +1265 -0
- agents/task/telemetry/views.py +882 -0
- agents/task/templates.py +98 -0
- agents/task/test_aux_chain.py +151 -0
- agents/task/test_aux_router.py +41 -0
- agents/task/tests/test_parse_robustness.py +170 -0
- agents/task/tool_defaults.py +110 -0
- agents/task/utils.py +762 -0
- agents/task/utils_json.py +768 -0
- agents/task/utils_webview.py +86 -0
- agents/task/workspace_context.py +229 -0
- agents/task_agent_lite.py +2398 -0
- api/README.md +683 -0
- api/TASK_API_DOCS.md +151 -0
- api/__init__.py +0 -0
- api/a2a/__init__.py +37 -0
- api/a2a/agent_card.py +337 -0
- api/a2a/client.py +657 -0
- api/a2a/endpoints.py +375 -0
- api/a2a/models.py +268 -0
- api/a2a/streaming.py +419 -0
- api/a2a/task_handler.py +929 -0
- api/admin_endpoints.py +1584 -0
- api/app.py +923 -0
- api/auth_constants.py +28 -0
- api/auth_endpoints.py +402 -0
- api/auth_state.py +36 -0
- api/chat_via_task.py +42 -0
- api/conversation_manager.py +231 -0
- api/dependencies.py +186 -0
- api/eip8004_endpoints.py +452 -0
- api/hyperliquid_models.py +174 -0
- api/hyperliquid_routes.py +450 -0
- api/interfaces.py +207 -0
- api/jwt_middleware.py +83 -0
- api/kb/__init__.py +1 -0
- api/kb/endpoints.py +314 -0
- api/mcp_models.py +279 -0
- api/mcp_routes.py +438 -0
- api/middleware.py +526 -0
- api/models.py +176 -0
- api/openai_compat/__init__.py +0 -0
- api/openai_compat/model_map.py +25 -0
- api/openai_compat/models.py +59 -0
- api/openai_compat/router.py +140 -0
- api/payment_endpoints.py +444 -0
- api/payment_verification.py +138 -0
- api/polymarket_models.py +155 -0
- api/polymarket_routes.py +501 -0
- api/pricing_endpoints.py +89 -0
- api/session_routing.py +54 -0
- api/skill_endpoints.py +453 -0
- api/task_http_api.py +1856 -0
- api/webhooks.py +52 -0
- api/x402_endpoints.py +280 -0
- avatar/README.md +44 -0
- avatar/__init__.py +1 -0
- avatar/config/rob.json +7 -0
- avatar/mindprint.js +395 -0
- avatar/renders/rob.meta.json +20 -0
- avatar/renders/rob.png +0 -0
- avatar/studio.html +373 -0
- avatar/webview/avatar-live.js +44 -0
- cli/README.md +89 -0
- cli/__init__.py +0 -0
- cli/cli_surface.py +67 -0
- cli/commands/__init__.py +0 -0
- cli/commands/_bootstrap.py +49 -0
- cli/commands/_errors.py +53 -0
- cli/commands/chat.py +973 -0
- cli/commands/config.py +100 -0
- cli/commands/dashboard.py +103 -0
- cli/commands/doctor.py +222 -0
- cli/commands/email.py +174 -0
- cli/commands/gateway.py +346 -0
- cli/commands/goals.py +509 -0
- cli/commands/init.py +276 -0
- cli/commands/journey.py +30 -0
- cli/commands/kb.py +280 -0
- cli/commands/model.py +107 -0
- cli/commands/owner.py +431 -0
- cli/commands/pfp.py +232 -0
- cli/commands/run.py +414 -0
- cli/commands/serve.py +41 -0
- cli/commands/session.py +684 -0
- cli/commands/skill_install.py +786 -0
- cli/commands/skills.py +128 -0
- cli/commands/subagents.py +100 -0
- cli/commands/surface.py +81 -0
- cli/commands/telegram.py +185 -0
- cli/commands/todos.py +209 -0
- cli/commands/tools.py +102 -0
- cli/commands/update.py +316 -0
- cli/commands/whatsapp.py +175 -0
- cli/config_store.py +276 -0
- cli/gitignore.py +43 -0
- cli/inventory.py +46 -0
- cli/keys.py +101 -0
- cli/persona.py +17 -0
- cli/polyrob.py +137 -0
- cli/ui/__init__.py +64 -0
- cli/ui/activity.py +141 -0
- cli/ui/app.py +538 -0
- cli/ui/banner.py +233 -0
- cli/ui/blocks.py +534 -0
- cli/ui/bootstrap_notice.py +47 -0
- cli/ui/commands/__init__.py +41 -0
- cli/ui/commands/h_cron.py +64 -0
- cli/ui/commands/h_journey.py +164 -0
- cli/ui/commands/h_kb.py +121 -0
- cli/ui/commands/h_learn.py +100 -0
- cli/ui/commands/h_mcp.py +215 -0
- cli/ui/commands/h_self.py +113 -0
- cli/ui/commands/h_skills.py +314 -0
- cli/ui/commands/handlers.py +1672 -0
- cli/ui/commands/registry.py +426 -0
- cli/ui/dialog.py +425 -0
- cli/ui/event_registry.py +110 -0
- cli/ui/event_specs.py +45 -0
- cli/ui/events.py +433 -0
- cli/ui/identity.py +15 -0
- cli/ui/lifecycle.py +159 -0
- cli/ui/live_hooks.py +43 -0
- cli/ui/model_selector.py +532 -0
- cli/ui/persistent_loop.py +181 -0
- cli/ui/pick.py +113 -0
- cli/ui/plain_renderer.py +351 -0
- cli/ui/renderer.py +384 -0
- cli/ui/rich_renderer.py +533 -0
- cli/ui/secrets.py +115 -0
- cli/ui/state.py +376 -0
- cli/ui/statusbar.py +231 -0
- cli/ui/streaming.py +254 -0
- cli/ui/terminal_render.py +105 -0
- cli/ui/theme.py +132 -0
- cli/update/__init__.py +6 -0
- cli/update/context.py +121 -0
- cli/update/detect.py +176 -0
- cli/update/engine.py +95 -0
- cli/update/migrate_guarded.py +55 -0
- cli/update/process_guard.py +215 -0
- cli/update/runners.py +78 -0
- cli/update/snapshot.py +270 -0
- cli/update/versions.py +205 -0
- core/README.md +377 -0
- core/__init__.py +174 -0
- core/assets.py +87 -0
- core/async_bridge.py +76 -0
- core/autonomy_runtime.py +259 -0
- core/base_component.py +302 -0
- core/bootstrap.py +681 -0
- core/bot.py +310 -0
- core/config.py +833 -0
- core/constants.py +126 -0
- core/container.py +462 -0
- core/db_manifest.py +101 -0
- core/embedding.py +57 -0
- core/env.py +39 -0
- core/exceptions.py +287 -0
- core/flags.py +144 -0
- core/flags_catalog.py +345 -0
- core/home_migration.py +62 -0
- core/identity.py +118 -0
- core/initialization.py +1004 -0
- core/instance.py +537 -0
- core/interactive_gate.py +96 -0
- core/logging.py +436 -0
- core/owner_doc_writer.py +176 -0
- core/pairing.py +193 -0
- core/path_safety.py +20 -0
- core/paths.py +27 -0
- core/payment_config.py +21 -0
- core/permissions.py +322 -0
- core/runtime_config.py +96 -0
- core/runtime_paths.py +135 -0
- core/seams.py +35 -0
- core/secret_scan.py +25 -0
- core/secret_scrub.py +71 -0
- core/secrets.py +35 -0
- core/security_logging_filter.py +148 -0
- core/self_context_writer.py +333 -0
- core/self_evolution.py +294 -0
- core/session_context.py +47 -0
- core/sqlite_util.py +59 -0
- core/surfaces/__init__.py +16 -0
- core/surfaces/access.py +118 -0
- core/surfaces/binding.py +88 -0
- core/surfaces/bootstrap.py +78 -0
- core/surfaces/circuit.py +168 -0
- core/surfaces/continuity.py +77 -0
- core/surfaces/correspondents.py +228 -0
- core/surfaces/dispatcher.py +216 -0
- core/surfaces/envelopes.py +75 -0
- core/surfaces/gc.py +30 -0
- core/surfaces/idempotency.py +57 -0
- core/surfaces/inbound_webhook.py +124 -0
- core/surfaces/media.py +32 -0
- core/surfaces/message_router.py +91 -0
- core/surfaces/outbound_allowlist.py +80 -0
- core/surfaces/outbound_dispatcher.py +110 -0
- core/surfaces/outbound_mirror.py +37 -0
- core/surfaces/outbound_queue.py +107 -0
- core/surfaces/outbound_target.py +12 -0
- core/surfaces/owner_admin.py +29 -0
- core/surfaces/proactive.py +78 -0
- core/surfaces/progress.py +108 -0
- core/surfaces/rate_bucket.py +21 -0
- core/surfaces/registry.py +51 -0
- core/surfaces/rendering.py +53 -0
- core/surfaces/send_policy.py +14 -0
- core/surfaces/serialize.py +24 -0
- core/surfaces/session_chat_registry.py +112 -0
- core/surfaces/session_policy.py +60 -0
- core/surfaces/surface.py +288 -0
- core/surfaces/transcription.py +88 -0
- core/surfaces/voice_echo.py +26 -0
- core/surfaces/voice_guard.py +23 -0
- core/tickers.py +174 -0
- core/tool_catalog.py +192 -0
- core/version.py +75 -0
- core/wallet/__init__.py +5 -0
- core/wallet/agent_wallet.py +59 -0
- core/wallet/audit_sink.py +61 -0
- core/wallet/config.py +70 -0
- core/wallet/factory.py +87 -0
- core/wallet/policy.py +104 -0
- core/wallet/signer.py +51 -0
- cron/README.md +65 -0
- cron/__init__.py +0 -0
- cron/delivery.py +277 -0
- cron/digest.py +124 -0
- cron/jobs.py +207 -0
- cron/runner.py +297 -0
- cron/schedule.py +194 -0
- cron/scheduler.py +201 -0
- cron/service.py +57 -0
- cron/wake_gate.py +223 -0
- data/__init__.py +1 -0
- data/prompts/autov2_prompts.json +87 -0
- data/prompts/skills/browser-automation/SKILL.md +43 -0
- data/prompts/skills/coding-workflow/SKILL.md +36 -0
- data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
- data/prompts/skills/document-writing/SKILL.md +33 -0
- data/prompts/skills/email-comms/SKILL.md +33 -0
- data/prompts/skills/file-data-ops/SKILL.md +35 -0
- data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
- data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
- data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
- data/prompts/skills/lead-research/SKILL.md +86 -0
- data/prompts/skills/market-research-brief/SKILL.md +104 -0
- data/prompts/skills/person-analyzer/SKILL.md +124 -0
- data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
- data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
- data/prompts/skills/polymarket-trading/SKILL.md +55 -0
- data/prompts/skills/presentation-creator/SKILL.md +36 -0
- data/prompts/skills/project-analyzer/SKILL.md +169 -0
- data/prompts/skills/rules.json +268 -0
- data/prompts/skills/secret-handling/SKILL.md +20 -0
- data/prompts/skills/skill-authoring/SKILL.md +31 -0
- data/prompts/skills/skill-security-review/SKILL.md +27 -0
- data/prompts/skills/social-discovery/SKILL.md +57 -0
- data/prompts/skills/task-planning/SKILL.md +33 -0
- data/prompts/skills/web-research/SKILL.md +39 -0
- data/prompts/skills/web-scraping/SKILL.md +73 -0
- data/prompts/skills/x-engagement/SKILL.md +69 -0
- data/prompts/system_prompts.json +4 -0
- data/subscription_channels.json +5 -0
- modules/README.md +503 -0
- modules/__init__.py +124 -0
- modules/auth/__init__.py +13 -0
- modules/auth/api_key_manager.py +157 -0
- modules/auth/identity_mapper.py +380 -0
- modules/auth/siwe_auth.py +273 -0
- modules/auth/tier_manager.py +173 -0
- modules/base_module.py +190 -0
- modules/credits/__init__.py +29 -0
- modules/credits/balance_manager.py +237 -0
- modules/credits/cost_utils.py +129 -0
- modules/credits/pricing.py +139 -0
- modules/credits/unified_ledger.py +148 -0
- modules/credits/usage_meter.py +310 -0
- modules/credits/usage_tracker.py +674 -0
- modules/database/__init__.py +33 -0
- modules/database/audit_log.py +322 -0
- modules/database/auth_tables.py +276 -0
- modules/database/connection.py +644 -0
- modules/database/connection_pool.py +305 -0
- modules/database/conversation_contexts.py +446 -0
- modules/database/database_manager.py +237 -0
- modules/database/hyperliquid.py +375 -0
- modules/database/polymarket.py +574 -0
- modules/database/user_mcp_servers.py +728 -0
- modules/database/user_profiles.py +562 -0
- modules/database/utils.py +107 -0
- modules/database/x402_tables.py +92 -0
- modules/eip8004/README.md +1069 -0
- modules/eip8004/__init__.py +65 -0
- modules/eip8004/contracts.py +498 -0
- modules/eip8004/models.py +271 -0
- modules/eip8004/registration.py +148 -0
- modules/eip8004/reputation.py +367 -0
- modules/eip8004/validation.py +277 -0
- modules/llm/__init__.py +97 -0
- modules/llm/adapters.py +963 -0
- modules/llm/anthropic_client.py +831 -0
- modules/llm/available_models.py +106 -0
- modules/llm/brain_scrubber.py +137 -0
- modules/llm/cache_hints.py +162 -0
- modules/llm/deepseek_client.py +562 -0
- modules/llm/gemini_client.py +1367 -0
- modules/llm/llm_client.py +1139 -0
- modules/llm/llm_client_registry.py +188 -0
- modules/llm/llm_factory.py +237 -0
- modules/llm/llm_manager.py +1036 -0
- modules/llm/messages.py +374 -0
- modules/llm/model_registry.py +1935 -0
- modules/llm/nvidia_client.py +125 -0
- modules/llm/openai_client.py +800 -0
- modules/llm/openrouter_client.py +969 -0
- modules/llm/profiles.py +178 -0
- modules/llm/test_anthropic_cache_capture.py +35 -0
- modules/llm/test_cached_pricing_derivation.py +24 -0
- modules/llm/think_scrubber.py +444 -0
- modules/llm/token_counter.py +268 -0
- modules/memory/__init__.py +35 -0
- modules/memory/backend_factory.py +83 -0
- modules/memory/cache_manager.py +186 -0
- modules/memory/episodic.py +108 -0
- modules/memory/local_vector_memory_provider.py +552 -0
- modules/memory/memory_manager.py +225 -0
- modules/memory/models.py +312 -0
- modules/memory/provider.py +210 -0
- modules/memory/registry.py +205 -0
- modules/memory/sqlite_memory_provider.py +767 -0
- modules/memory/task/__init__.py +52 -0
- modules/memory/task/compaction_manager.py +157 -0
- modules/memory/task/context_retriever.py +809 -0
- modules/memory/task/hierarchical_memory.py +1002 -0
- modules/memory/task/lexical_retriever.py +71 -0
- modules/memory/task/null_context_manager.py +213 -0
- modules/memory/task/phase_manager.py +625 -0
- modules/memory/task/reflection_service.py +157 -0
- modules/memory/task/semantic_retriever.py +218 -0
- modules/memory/task/task_context_manager.py +1165 -0
- modules/memory/task/test_forgetting_engages.py +24 -0
- modules/memory/task/test_reflection_llm.py +39 -0
- modules/memory/task/test_threat_scan.py +53 -0
- modules/memory/task/threat_scan.py +90 -0
- modules/memory/test_sqlite_memory_provider.py +106 -0
- modules/memory/user_profile_manager.py +527 -0
- modules/payments/__init__.py +11 -0
- modules/payments/deposit_monitor.py +478 -0
- modules/payments/price_oracle.py +70 -0
- modules/payments/treasury_sweeper.py +401 -0
- modules/payments/wallet_generator.py +145 -0
- modules/pfp/__init__.py +1 -0
- modules/pfp/config.py +92 -0
- modules/pfp/mesh.py +565 -0
- modules/pfp/push.py +69 -0
- modules/pfp/renderer.py +85 -0
- modules/pfp/store.py +82 -0
- modules/skills/__init__.py +1 -0
- modules/skills/skill_usage.py +248 -0
- modules/transcription/__init__.py +24 -0
- modules/transcription/base.py +20 -0
- modules/transcription/faster_whisper_transcriber.py +55 -0
- modules/x402/README.md +249 -0
- modules/x402/__init__.py +22 -0
- modules/x402/invoicing.py +416 -0
- modules/x402/middleware.py +364 -0
- modules/x402/settlement_watcher.py +145 -0
- modules/x402/x402_integration.py +344 -0
- polyrob-0.5.0.dist-info/METADATA +482 -0
- polyrob-0.5.0.dist-info/RECORD +745 -0
- polyrob-0.5.0.dist-info/WHEEL +5 -0
- polyrob-0.5.0.dist-info/entry_points.txt +2 -0
- polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
- polyrob-0.5.0.dist-info/top_level.txt +12 -0
- surfaces/README.md +70 -0
- surfaces/__init__.py +0 -0
- surfaces/email/__init__.py +0 -0
- surfaces/email/dedup.py +44 -0
- surfaces/email/harness.py +187 -0
- surfaces/email/inbound.py +159 -0
- surfaces/email/seed.py +60 -0
- surfaces/email/surface.py +105 -0
- surfaces/telegram/__init__.py +0 -0
- surfaces/telegram/dedup.py +83 -0
- surfaces/telegram/harness.py +886 -0
- surfaces/telegram/inbound.py +145 -0
- surfaces/telegram/interactive_tools.py +49 -0
- surfaces/telegram/markdown.py +347 -0
- surfaces/telegram/rate_limit.py +167 -0
- surfaces/telegram/surface.py +187 -0
- surfaces/telegram/voice.py +52 -0
- surfaces/whatsapp/__init__.py +0 -0
- surfaces/whatsapp/client.py +51 -0
- surfaces/whatsapp/harness.py +62 -0
- surfaces/whatsapp/inbound.py +132 -0
- surfaces/whatsapp/surface.py +64 -0
- surfaces/whatsapp/window.py +34 -0
- tools/README.md +578 -0
- tools/__init__.py +424 -0
- tools/alchemy/__init__.py +5 -0
- tools/alchemy/alchemy_tool.py +389 -0
- tools/anysite/__init__.py +17 -0
- tools/anysite/client.py +88 -0
- tools/anysite/tool.py +89 -0
- tools/base_tool.py +468 -0
- tools/browser/__init__.py +22 -0
- tools/browser/actions.py +118 -0
- tools/browser/browser.py +1457 -0
- tools/browser/browser_manager.py +690 -0
- tools/browser/context.py +1748 -0
- tools/browser/playwright_utils.py +137 -0
- tools/browser/views.py +56 -0
- tools/code_exec/SANDBOX_SECURITY.md +144 -0
- tools/code_exec/__init__.py +178 -0
- tools/code_exec/backend.py +73 -0
- tools/code_exec/backends/__init__.py +1 -0
- tools/code_exec/backends/docker.py +779 -0
- tools/code_exec/backends/local_subprocess.py +154 -0
- tools/code_exec/env_policy.py +45 -0
- tools/code_exec/result.py +42 -0
- tools/code_exec/sandbox_guard.py +59 -0
- tools/code_exec/tool.py +227 -0
- tools/coding/__init__.py +50 -0
- tools/coding/edit.py +107 -0
- tools/coding/search.py +113 -0
- tools/coding/tool.py +353 -0
- tools/collabland/__init__.py +5 -0
- tools/collabland/collabland_tool.py +471 -0
- tools/controller/__init__.py +0 -0
- tools/controller/_helpers.py +165 -0
- tools/controller/action_registration.py +1765 -0
- tools/controller/approval.py +244 -0
- tools/controller/approval_interactive.py +90 -0
- tools/controller/delegation.py +195 -0
- tools/controller/execution.py +797 -0
- tools/controller/execution_context.py +141 -0
- tools/controller/hooks.py +135 -0
- tools/controller/introspection.py +344 -0
- tools/controller/mcp_registrar.py +190 -0
- tools/controller/message_send.py +31 -0
- tools/controller/registry/__init__.py +4 -0
- tools/controller/registry/schema_generators.py +395 -0
- tools/controller/registry/schema_sanitizer.py +406 -0
- tools/controller/registry/service.py +1266 -0
- tools/controller/registry/views.py +151 -0
- tools/controller/service.py +409 -0
- tools/controller/tool_management.py +334 -0
- tools/controller/types.py +28 -0
- tools/controller/views.py +450 -0
- tools/cronjob_tools.py +166 -0
- tools/crypto_trade_gate.py +59 -0
- tools/descriptors.py +452 -0
- tools/dom/__init__.py +0 -0
- tools/dom/history_tree_processor/service.py +244 -0
- tools/dom/history_tree_processor/view.py +99 -0
- tools/dom/service.py +471 -0
- tools/dom/views.py +346 -0
- tools/email_tool.py +403 -0
- tools/exceptions.py +169 -0
- tools/filesystem.py +1038 -0
- tools/filesystem_docproc.py +331 -0
- tools/filesystem_pdf.py +755 -0
- tools/git/__init__.py +42 -0
- tools/git/tool.py +258 -0
- tools/github/__init__.py +33 -0
- tools/github/client.py +94 -0
- tools/github/tool.py +237 -0
- tools/goal_tools.py +309 -0
- tools/hyperliquid/__init__.py +56 -0
- tools/hyperliquid/models.py +233 -0
- tools/hyperliquid/service.py +1680 -0
- tools/knowledge_ingest.py +950 -0
- tools/mcp/README.md +535 -0
- tools/mcp/__init__.py +52 -0
- tools/mcp/catalog.py +115 -0
- tools/mcp/config.py +337 -0
- tools/mcp/mcp_tool.py +1415 -0
- tools/mcp/param_coercion.py +289 -0
- tools/mcp/protocol.py +1520 -0
- tools/mcp/rate_limit.py +50 -0
- tools/mcp/security.py +424 -0
- tools/mcp/self_install.py +111 -0
- tools/mcp/server_manager.py +829 -0
- tools/mcp/subscriptions.py +72 -0
- tools/mcp/user_mcp_service.py +899 -0
- tools/mcp/validation_tracker.py +89 -0
- tools/mcp/views.py +346 -0
- tools/oauth/__init__.py +17 -0
- tools/oauth/manager.py +75 -0
- tools/oauth/provider.py +69 -0
- tools/oauth/providers/__init__.py +1 -0
- tools/oauth/providers/generic_oauth2.py +96 -0
- tools/perplexity_tool.py +280 -0
- tools/polymarket/__init__.py +41 -0
- tools/polymarket/clob_adapter.py +76 -0
- tools/polymarket/models.py +182 -0
- tools/polymarket/service.py +2082 -0
- tools/self_env/__init__.py +61 -0
- tools/self_env/tool.py +274 -0
- tools/shell/__init__.py +80 -0
- tools/shell/backend_pool.py +118 -0
- tools/shell/discipline.py +62 -0
- tools/shell/executor.py +157 -0
- tools/shell/loopback_allow.py +87 -0
- tools/shell/process_registry.py +113 -0
- tools/shell/process_tool.py +131 -0
- tools/shell/state.py +129 -0
- tools/shell/tool.py +151 -0
- tools/task_tool.py +637 -0
- tools/twitter_tool.py +2086 -0
- tools/user_directory.py +179 -0
- tools/web_fetch/__init__.py +3 -0
- tools/web_fetch/fetcher.py +144 -0
- tools/web_fetch/render.py +44 -0
- tools/web_fetch/tool.py +60 -0
- tools/x402/__init__.py +60 -0
- tools/x402/client.py +47 -0
- tools/x402/invoice_tool.py +143 -0
- tools/x402/real_client.py +250 -0
- tools/x402/service.py +131 -0
- utils/README.md +498 -0
- utils/__init__.py +46 -0
- utils/auth_utils.py +114 -0
- utils/bounded_collections.py +162 -0
- utils/circuit_breaker.py +389 -0
- utils/gif_utils.py +435 -0
- utils/markdown_utils.py +291 -0
- utils/message_utils.py +147 -0
- utils/metrics.py +36 -0
- utils/path_validator.py +227 -0
- utils/rate_limit_manager.py +399 -0
- utils/result_size.py +296 -0
- utils/time_utils.py +206 -0
- utils/user_utils.py +188 -0
- webview/README.md +111 -0
- webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
- webview/__init__.py +8 -0
- webview/activity.py +636 -0
- webview/owner_auth.py +108 -0
- webview/pages.py +448 -0
- webview/repair_sessions.py +399 -0
- webview/server.py +4323 -0
- webview/server_launcher.py +112 -0
- webview/static/css/activity.css +89 -0
- webview/static/css/chat.css +1772 -0
- webview/static/css/components.css +1190 -0
- webview/static/css/config-panel.css +651 -0
- webview/static/css/pages/admin.css +766 -0
- webview/static/css/pages/profile.css +231 -0
- webview/static/css/pages/settings.css +297 -0
- webview/static/css/pages/signin.css +316 -0
- webview/static/css/style.css +4250 -0
- webview/static/css/variables.css +147 -0
- webview/static/css/workspace-fullwidth-fix.css +109 -0
- webview/static/img/favicon.ico +0 -0
- webview/static/js/activity.js +394 -0
- webview/static/js/admin/activity.js +186 -0
- webview/static/js/admin/dashboard.js +163 -0
- webview/static/js/admin/user_detail.js +400 -0
- webview/static/js/admin/users.js +174 -0
- webview/static/js/admin/utils.js +214 -0
- webview/static/js/chat.js +3178 -0
- webview/static/js/config-panel.js +522 -0
- webview/static/js/constants.js +20 -0
- webview/static/js/error-handler.js +226 -0
- webview/static/js/ethers.min.js +1 -0
- webview/static/js/event-filter.js +137 -0
- webview/static/js/event-store.js +351 -0
- webview/static/js/file-attachments.js +374 -0
- webview/static/js/file-loader.js +163 -0
- webview/static/js/index.js +103 -0
- webview/static/js/performance-utils.js +196 -0
- webview/static/js/profile.js +396 -0
- webview/static/js/screenshot.js +386 -0
- webview/static/js/session.js +1527 -0
- webview/static/js/settings.js +1288 -0
- webview/static/js/sidebar-data.js +766 -0
- webview/static/js/sidebar-toggle.js +296 -0
- webview/static/js/socket.io.min.js +7 -0
- webview/static/js/stats.js +355 -0
- webview/static/js/ui-utils.js +420 -0
- webview/static/js/workspace.js +966 -0
- webview/stats_service.py +613 -0
- webview/templates/__init__.py +1 -0
- webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- webview/templates/activity.html +36 -0
- webview/templates/admin/activity.html +122 -0
- webview/templates/admin/dashboard.html +112 -0
- webview/templates/admin/user_detail.html +228 -0
- webview/templates/admin/users.html +105 -0
- webview/templates/autonomy.html +60 -0
- webview/templates/error.html +133 -0
- webview/templates/finance.html +90 -0
- webview/templates/identity.html +73 -0
- webview/templates/index.html +42 -0
- webview/templates/layout.html +125 -0
- webview/templates/memory.html +59 -0
- webview/templates/owner_login.html +15 -0
- webview/templates/profile.html +121 -0
- webview/templates/session.html +312 -0
- webview/templates/settings.html +534 -0
- webview/templates/sidebar.html +112 -0
- webview/templates/signin.html +213 -0
- webview/templates/status.html +14 -0
- webview/templates/system.html +45 -0
- webview/webgate.py +196 -0
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
"""Goal dispatcher + ticker (W4).
|
|
2
|
+
|
|
3
|
+
Turns ``ready`` goals on the board into agent runs, bounded by concurrency, safe
|
|
4
|
+
under ``workers>1`` (the cron ``TickLock`` gates a tick across processes), and
|
|
5
|
+
feeding completions back to the W1 self-wake rail so a finished goal can forge a
|
|
6
|
+
follow-up turn.
|
|
7
|
+
|
|
8
|
+
Mirrors ``cron/runner.py``'s shape: a pure ``GoalDispatcher.dispatch_once`` (unit
|
|
9
|
+
tested with a fake board/agent) + a ``GoalTicker.run_forever`` loop the FastAPI
|
|
10
|
+
lifespan starts/stops exactly like the cron ticker. Gated ``GOALS_ENABLED``.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
from agents.task.goals.board import GoalBoard, Goal, STATUS_BLOCKED, STATUS_DONE, STATUS_READY
|
|
20
|
+
from agents.task.runtime.run_as_session import run_task_as_session as _run_task_as_session
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _goal_ev(goal, outcome: str, reason: Optional[str] = None, **extra) -> None:
|
|
26
|
+
"""T4-03: emit a goal_run event to the durable event log (fail-open).
|
|
27
|
+
|
|
28
|
+
Goal runs previously wrote only the episodes table, never the event_log, so
|
|
29
|
+
`polyrob telemetry` — the tool built to answer "what ran?" — showed cron and
|
|
30
|
+
self-wake but not the goals that did most of the autonomous work. Mirror _cron_ev
|
|
31
|
+
so goal lifecycle rides the same uniform autonomy/governance stream."""
|
|
32
|
+
try:
|
|
33
|
+
from agents.task.telemetry.event_log import get_event_log, event_log_enabled
|
|
34
|
+
if event_log_enabled():
|
|
35
|
+
get_event_log().record(
|
|
36
|
+
"goal_run", user_id=getattr(goal, "user_id", ""),
|
|
37
|
+
source="goal", goal_id=getattr(goal, "id", None),
|
|
38
|
+
outcome=outcome, reason=reason, **extra)
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Tools a self-decomposed CHILD goal may inherit from its parent when it set none.
|
|
44
|
+
# Deliberately excludes money/social/trading tools (wallet, x402, hyperliquid,
|
|
45
|
+
# polymarket, twitter) so the agent can't self-grant spend/post capability by
|
|
46
|
+
# spawning a child goal. Server-side allowlist, NOT agent-controllable.
|
|
47
|
+
CHILD_INHERITABLE_TOOLS = frozenset(
|
|
48
|
+
{"filesystem", "task", "browser", "perplexity", "mcp", "anysite", "coding"}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Safe default toolset when a goal sets no tools and has nothing inheritable.
|
|
52
|
+
_DEFAULT_GOAL_TOOLS = ["filesystem", "task"]
|
|
53
|
+
|
|
54
|
+
# WS-8 (compute posture): the compute tools an autonomous goal/cron run needs to
|
|
55
|
+
# actually build/run/serve. Provisioned ONLY at AGENT_COMPUTE_POSTURE>=1 (and each
|
|
56
|
+
# call still passes compute_posture_allows in-session — an owner-tenant autonomous
|
|
57
|
+
# run does). Sandbox-contained at posture 1; never money/social. Resolved at CALL
|
|
58
|
+
# time so posture 0 is byte-identical.
|
|
59
|
+
_COMPUTE_GOAL_TOOLS = ["code_execution", "shell", "coding"]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _compute_posture_at_least_1() -> bool:
|
|
63
|
+
try:
|
|
64
|
+
from agents.task.constants import compute_posture
|
|
65
|
+
return compute_posture() >= 1
|
|
66
|
+
except Exception:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def default_goal_tools() -> list:
|
|
71
|
+
"""Posture-aware default goal toolset. Posture 0: ['filesystem','task']
|
|
72
|
+
(byte-identical). Posture>=1: + the compute tools (code_execution/shell/coding)."""
|
|
73
|
+
tools = list(_DEFAULT_GOAL_TOOLS)
|
|
74
|
+
if _compute_posture_at_least_1():
|
|
75
|
+
for t in _COMPUTE_GOAL_TOOLS:
|
|
76
|
+
if t not in tools:
|
|
77
|
+
tools.append(t)
|
|
78
|
+
return tools
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def child_inheritable_tools() -> frozenset:
|
|
82
|
+
"""Posture-aware child-inheritable set. Posture 0: the frozen module constant.
|
|
83
|
+
Posture>=1: + code_execution/shell so a self-decomposed compute goal isn't
|
|
84
|
+
tool-starved (still sandbox-contained; money/social remain excluded)."""
|
|
85
|
+
if _compute_posture_at_least_1():
|
|
86
|
+
return CHILD_INHERITABLE_TOOLS | {"code_execution", "shell"}
|
|
87
|
+
return CHILD_INHERITABLE_TOOLS
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def effective_goal_concurrency() -> int:
|
|
91
|
+
"""Goal in-flight cap, clamped to single-flight on a SHARED project folder.
|
|
92
|
+
|
|
93
|
+
When the installed pm() serves one project-root workspace (CLI/headless project
|
|
94
|
+
mode), concurrent goal runs would interleave read-modify-write edits on the same
|
|
95
|
+
files (the battle-test "read INDEX.md, append" corruption). Serialize them.
|
|
96
|
+
Keyed off the installed pm() — NOT an env var — so the multi-tenant server (whose
|
|
97
|
+
global pm() is per-session) keeps its full GOAL_MAX_CONCURRENT (MT-5). Fail-open
|
|
98
|
+
to the unclamped cap on any pm() error.
|
|
99
|
+
"""
|
|
100
|
+
from agents.task.constants import AutonomyConfig
|
|
101
|
+
cap = AutonomyConfig.goal_max_concurrent()
|
|
102
|
+
try:
|
|
103
|
+
from agents.task.path import pm
|
|
104
|
+
if pm().is_project_root_workspace:
|
|
105
|
+
return min(cap, 1)
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
return cap
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _heartbeat_interval(ttl: int) -> int:
|
|
112
|
+
"""Heartbeat cadence for an in-flight goal claim: ttl/3, floored at 30s (F8).
|
|
113
|
+
|
|
114
|
+
Pinging at a third of the TTL gives two missed-beat margins before a claim
|
|
115
|
+
would expire, while staying infrequent enough to be cheap.
|
|
116
|
+
"""
|
|
117
|
+
return max(30, int(ttl) // 3)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class GoalDispatcher:
|
|
121
|
+
def __init__(self, board: GoalBoard, task_agent: Any, *, lock_path: Optional[str] = None):
|
|
122
|
+
self.board = board
|
|
123
|
+
self.task_agent = task_agent
|
|
124
|
+
self.lock_path = lock_path
|
|
125
|
+
# Strong refs to in-flight goal runs — without this an unawaited create_task
|
|
126
|
+
# can be GC'd mid-run (CPython drops weakly-referenced tasks), cancelling a
|
|
127
|
+
# goal that "may run minutes". Cleared via done-callback.
|
|
128
|
+
self._inflight: set = set()
|
|
129
|
+
self._quota_logged = False
|
|
130
|
+
# §7.2 tail: consecutive planner runs that left the ready queue empty, and
|
|
131
|
+
# whether the resulting stall was already escalated (escalate ONCE per stall;
|
|
132
|
+
# both reset the moment the board refills). In-memory: resets on restart,
|
|
133
|
+
# which at worst re-escalates one stall after a deploy — acceptable.
|
|
134
|
+
self._empty_planner_runs = 0
|
|
135
|
+
self._empty_pipeline_escalated = False
|
|
136
|
+
# Budget-gate push latch: tenants for whom an owner "over budget" push has
|
|
137
|
+
# already gone out this over-budget episode. The durable ask is dedup-
|
|
138
|
+
# refreshed every tick (fine), but the owner PUSH must fire ONCE per episode,
|
|
139
|
+
# not every 60s tick per held goal. Cleared when the tenant is back under
|
|
140
|
+
# budget (a fresh episode may push again). In-memory: resets on restart.
|
|
141
|
+
self._budget_pushed: set = set()
|
|
142
|
+
|
|
143
|
+
async def dispatch_once(self) -> int:
|
|
144
|
+
"""Claim and run up to GOAL_MAX_CONCURRENT ready goals. Returns #dispatched.
|
|
145
|
+
|
|
146
|
+
Cross-process safe: a single tick is gated by the cron-style TickLock so two
|
|
147
|
+
workers don't both fan out the same ready set. ``reclaim_stale`` runs FIRST
|
|
148
|
+
and UNCONDITIONALLY — before the ``GOALS_ENABLED`` gate (AU-F4.3) — so a
|
|
149
|
+
crashed ``running`` row isn't stuck forever if the flag was flipped off.
|
|
150
|
+
"""
|
|
151
|
+
from agents.task.constants import AutonomyConfig
|
|
152
|
+
|
|
153
|
+
# AU-F4.3: reclaim expired claims BEFORE the enabled-gate so a crashed
|
|
154
|
+
# `running` row doesn't stay stuck forever just because GOALS_ENABLED was
|
|
155
|
+
# (temporarily) flipped off. reclaim_stale is a self-contained retry-safe SQL
|
|
156
|
+
# UPDATE (agents/task/goals/board.py) -- it needs no external tick-lock /
|
|
157
|
+
# workspace-lock precondition, so it's safe to run unconditionally here.
|
|
158
|
+
try:
|
|
159
|
+
self.board.reclaim_stale()
|
|
160
|
+
except Exception:
|
|
161
|
+
logger.warning("goal dispatch: reclaim_stale failed", exc_info=True)
|
|
162
|
+
|
|
163
|
+
if not AutonomyConfig.goals_enabled():
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
from core.interactive_gate import is_interactive_busy
|
|
167
|
+
if is_interactive_busy():
|
|
168
|
+
return 0 # a human is mid-turn; don't run a goal in the shared workspace
|
|
169
|
+
|
|
170
|
+
lock = None
|
|
171
|
+
if self.lock_path:
|
|
172
|
+
from cron.scheduler import TickLock
|
|
173
|
+
lock = TickLock(self.lock_path)
|
|
174
|
+
if not lock.acquire():
|
|
175
|
+
return 0
|
|
176
|
+
# C2: gate the dispatch decision behind the cross-process workspace lock too,
|
|
177
|
+
# so a 2nd `rob` process doesn't start goals in the shared CWD while another
|
|
178
|
+
# process's REPL is mid-turn. Non-blocking; defer the tick if contended.
|
|
179
|
+
from core.interactive_gate import workspace_turn_lock
|
|
180
|
+
_ws_lock = workspace_turn_lock(timeout=0)
|
|
181
|
+
try:
|
|
182
|
+
_ws_lock.__enter__()
|
|
183
|
+
except Exception:
|
|
184
|
+
# Fail-open: any workspace-lock contention/error defers the tick.
|
|
185
|
+
if lock is not None:
|
|
186
|
+
lock.release()
|
|
187
|
+
return 0
|
|
188
|
+
try:
|
|
189
|
+
# (reclaim_stale already ran above, before the enabled-gate — AU-F4.3)
|
|
190
|
+
limit = effective_goal_concurrency()
|
|
191
|
+
# GOAL_MAX_CONCURRENT is a global in-flight cap, NOT a per-tick claim
|
|
192
|
+
# quota. Subtract goals ALREADY RUNNING from the cross-process DB count
|
|
193
|
+
# (not the per-process self._inflight, which can't see other workers'
|
|
194
|
+
# runs) so a backlog can't grow concurrent agent sessions without bound
|
|
195
|
+
# — under workers>1 the per-process count let total reach cap x workers.
|
|
196
|
+
try:
|
|
197
|
+
running = self.board.count_running()
|
|
198
|
+
except Exception:
|
|
199
|
+
running = len(self._inflight) # fail-open to per-process count
|
|
200
|
+
slots = max(0, limit - running)
|
|
201
|
+
quota = AutonomyConfig.goal_daily_quota()
|
|
202
|
+
if quota > 0:
|
|
203
|
+
try:
|
|
204
|
+
used = self.board.count_started_since(86400)
|
|
205
|
+
except Exception:
|
|
206
|
+
used = 0 # fail-open: never let quota accounting kill dispatch
|
|
207
|
+
headroom = max(0, quota - used)
|
|
208
|
+
if headroom == 0:
|
|
209
|
+
if not self._quota_logged:
|
|
210
|
+
logger.warning(
|
|
211
|
+
"goal daily quota exhausted (%d started/24h >= %d) — pausing dispatch",
|
|
212
|
+
used, quota)
|
|
213
|
+
self._quota_logged = True
|
|
214
|
+
return 0
|
|
215
|
+
self._quota_logged = False
|
|
216
|
+
slots = min(slots, headroom)
|
|
217
|
+
if slots == 0:
|
|
218
|
+
return 0
|
|
219
|
+
ready = self.board.ready(limit=slots)
|
|
220
|
+
ttl = AutonomyConfig.goal_claim_ttl_sec()
|
|
221
|
+
worker = f"goal-dispatch-{os.getpid()}"
|
|
222
|
+
dispatched = 0
|
|
223
|
+
budget_aware = AutonomyConfig.budget_aware_autonomy()
|
|
224
|
+
for g in ready:
|
|
225
|
+
if budget_aware and await self._over_budget(g):
|
|
226
|
+
continue # held — an owner-visible ask was raised instead of burning
|
|
227
|
+
claimed = self.board.claim(g.id, worker, ttl_seconds=ttl)
|
|
228
|
+
if claimed is None:
|
|
229
|
+
continue # another worker won the race
|
|
230
|
+
t = asyncio.create_task(self._run_goal(claimed))
|
|
231
|
+
self._inflight.add(t)
|
|
232
|
+
t.add_done_callback(self._inflight.discard)
|
|
233
|
+
dispatched += 1
|
|
234
|
+
# Fire-and-forget: the ticker doesn't block on goal completion (goals may
|
|
235
|
+
# run minutes). The runs self-report via record_success/failure + self-wake.
|
|
236
|
+
try:
|
|
237
|
+
await self._maybe_plan(headroom_after=slots - dispatched)
|
|
238
|
+
except Exception:
|
|
239
|
+
logger.debug("planner check failed (non-fatal)", exc_info=True)
|
|
240
|
+
return dispatched
|
|
241
|
+
finally:
|
|
242
|
+
try:
|
|
243
|
+
_ws_lock.__exit__(None, None, None)
|
|
244
|
+
except Exception:
|
|
245
|
+
pass
|
|
246
|
+
if lock is not None:
|
|
247
|
+
lock.release()
|
|
248
|
+
|
|
249
|
+
async def _heartbeat_claim(self, goal_id: str, worker: str, ttl: int) -> None:
|
|
250
|
+
"""Keep a long-running goal's claim alive (F8).
|
|
251
|
+
|
|
252
|
+
Goals run fire-and-forget for up to many minutes, but the claim TTL
|
|
253
|
+
(GOAL_CLAIM_TTL_SEC, default 900s) is fixed at claim time. Without a
|
|
254
|
+
heartbeat, a goal that runs longer than the TTL is reclaimed by
|
|
255
|
+
``reclaim_stale`` and DOUBLE-dispatched. Ping the claim every ttl/3.
|
|
256
|
+
"""
|
|
257
|
+
interval = _heartbeat_interval(ttl)
|
|
258
|
+
try:
|
|
259
|
+
while True:
|
|
260
|
+
await asyncio.sleep(interval)
|
|
261
|
+
try:
|
|
262
|
+
self.board.heartbeat(goal_id, worker, ttl_seconds=ttl)
|
|
263
|
+
except Exception as e: # never let heartbeat kill the run
|
|
264
|
+
logger.warning("goal %s heartbeat failed: %s", goal_id, e)
|
|
265
|
+
except asyncio.CancelledError:
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
async def _run_goal(self, goal: Goal) -> None:
|
|
269
|
+
"""Run one claimed goal on the task-agent core, then record + self-wake."""
|
|
270
|
+
session_id = None
|
|
271
|
+
# FIX3 (T3-M1): once the success episode row is durably recorded, a LATER
|
|
272
|
+
# incidental raise in this same try block (e.g. extract_outcome_line,
|
|
273
|
+
# self-wake) must not let the outer except's failed-write flip that row's
|
|
274
|
+
# outcome from "done" back to "failed" — see the guard at the bottom.
|
|
275
|
+
recorded_success = False
|
|
276
|
+
from agents.task.constants import AutonomyConfig
|
|
277
|
+
worker = f"goal-dispatch-{os.getpid()}"
|
|
278
|
+
ttl = AutonomyConfig.goal_claim_ttl_sec()
|
|
279
|
+
hb_task = asyncio.create_task(self._heartbeat_claim(goal.id, worker, ttl))
|
|
280
|
+
# C6: on a SHARED project-root workspace (local/CLI), hold the in-process
|
|
281
|
+
# busy gate for the RUN's full duration — not just the dispatch decision.
|
|
282
|
+
# dispatch_once acquires the gate only to CLAIM, then releases it before this
|
|
283
|
+
# fire-and-forget run; without this, a cron tick or the next goal dispatch
|
|
284
|
+
# would start a second file-mutating run in the SAME CWD concurrently. The
|
|
285
|
+
# server (per-session workspaces) does not clamp, so this is scoped to the
|
|
286
|
+
# shared-workspace case to avoid needlessly serializing server throughput.
|
|
287
|
+
# (NB: a live REPL turn does not consult this gate, so goal-vs-live-turn on
|
|
288
|
+
# the shared CWD is still only fully closed once the REPL waits on it.)
|
|
289
|
+
_shared_ws = False
|
|
290
|
+
try:
|
|
291
|
+
from agents.task.path import pm
|
|
292
|
+
_shared_ws = bool(pm().is_project_root_workspace)
|
|
293
|
+
except Exception:
|
|
294
|
+
_shared_ws = False
|
|
295
|
+
if _shared_ws:
|
|
296
|
+
from core.interactive_gate import mark_busy
|
|
297
|
+
mark_busy()
|
|
298
|
+
_goal_ev(goal, "started")
|
|
299
|
+
try:
|
|
300
|
+
payload = goal.payload or {}
|
|
301
|
+
from core.runtime_config import resolve_runtime_config
|
|
302
|
+
default_provider = resolve_runtime_config(None, None)[0]
|
|
303
|
+
provider = payload.get("provider") or default_provider
|
|
304
|
+
# Autonomous runs have no interactive config to pick a model, so fill the
|
|
305
|
+
# provider's default model from the registry when the goal doesn't pin one.
|
|
306
|
+
# (A None model crashes session setup downstream — '.lower()' on None.)
|
|
307
|
+
model = payload.get("model")
|
|
308
|
+
if not model:
|
|
309
|
+
try:
|
|
310
|
+
from modules.llm.llm_client_registry import get_default_model
|
|
311
|
+
model = get_default_model(provider)
|
|
312
|
+
except Exception:
|
|
313
|
+
model = None
|
|
314
|
+
from agents.task.goals.context import build_goal_run_task
|
|
315
|
+
objective = None
|
|
316
|
+
if goal.parent_id:
|
|
317
|
+
try:
|
|
318
|
+
parent = self.board.get(goal.parent_id)
|
|
319
|
+
if parent is not None and parent.kind == "objective":
|
|
320
|
+
objective = parent
|
|
321
|
+
except Exception:
|
|
322
|
+
objective = None
|
|
323
|
+
request = {
|
|
324
|
+
"task": build_goal_run_task(goal, objective),
|
|
325
|
+
"provider": provider,
|
|
326
|
+
"model": model,
|
|
327
|
+
"tools": self._resolve_tools(goal),
|
|
328
|
+
"max_steps": payload.get("max_steps", 20),
|
|
329
|
+
"temperature": 0.0,
|
|
330
|
+
"goal_id": goal.id,
|
|
331
|
+
}
|
|
332
|
+
# Route through the shared helper: create_session → run_session → refusal-check.
|
|
333
|
+
# H11: hard wall-clock cap (mirrors cron's per-job wait_for). max_steps alone
|
|
334
|
+
# doesn't bound wall time — a single hung step would occupy a slot forever. On
|
|
335
|
+
# timeout the TimeoutError is handled by the except below (record_failure) and
|
|
336
|
+
# the finally cancels the claim heartbeat, so reclaim_stale can recover the slot.
|
|
337
|
+
_max_run = AutonomyConfig.goal_max_run_seconds()
|
|
338
|
+
session_id, final = await asyncio.wait_for(
|
|
339
|
+
_run_task_as_session(
|
|
340
|
+
self.task_agent, user_id=goal.user_id, request=request, autonomous=True
|
|
341
|
+
),
|
|
342
|
+
timeout=_max_run,
|
|
343
|
+
)
|
|
344
|
+
# Back-half: goal-specific record calls (no is_refusal needed — helper normalised).
|
|
345
|
+
if session_id is None:
|
|
346
|
+
_g = self.board.record_failure(goal.id, error="create_session returned no id")
|
|
347
|
+
await self._maybe_escalate_blocked(_g)
|
|
348
|
+
return
|
|
349
|
+
if final is None:
|
|
350
|
+
_g = self.board.record_failure(
|
|
351
|
+
goal.id,
|
|
352
|
+
error="run did not complete (refusal or empty)",
|
|
353
|
+
session_id=session_id,
|
|
354
|
+
)
|
|
355
|
+
await self._maybe_escalate_blocked(_g)
|
|
356
|
+
try:
|
|
357
|
+
from modules.memory.episodic import finalize_episode
|
|
358
|
+
await finalize_episode(
|
|
359
|
+
session_id=session_id, user_id=goal.user_id, kind="goal",
|
|
360
|
+
task=getattr(goal, "title", None), outcome="failed",
|
|
361
|
+
goal_id=goal.id, meta={"source": "goal"},
|
|
362
|
+
)
|
|
363
|
+
except Exception:
|
|
364
|
+
logger.warning("goal episodic write failed", exc_info=True)
|
|
365
|
+
return
|
|
366
|
+
# §3.1: an agent-declared 'OUTCOME: BLOCKED — <need>' is an honest
|
|
367
|
+
# failure exit, never a success. Checked BEFORE record_success so the
|
|
368
|
+
# goal routes through the breaker/escalation rail with its stated need.
|
|
369
|
+
from agents.task.goals.context import extract_outcome_line, parse_blocked_outcome
|
|
370
|
+
# Fail-open: a crash in the outcome PARSER must never fail a completed
|
|
371
|
+
# run (FIX3 — the success path below still records honestly).
|
|
372
|
+
try:
|
|
373
|
+
outcome = extract_outcome_line(final)
|
|
374
|
+
blocked_need = parse_blocked_outcome(outcome)
|
|
375
|
+
except Exception:
|
|
376
|
+
logger.debug("outcome parse failed for %s", goal.id, exc_info=True)
|
|
377
|
+
outcome, blocked_need = None, None
|
|
378
|
+
if blocked_need is not None:
|
|
379
|
+
await self._fail_blocked_declared(goal, session_id, outcome, blocked_need)
|
|
380
|
+
return
|
|
381
|
+
# T2-01: a run that finished the loop but never called done() (max_steps
|
|
382
|
+
# exhaustion, or a reply-only conversational exit) returns a non-refusal
|
|
383
|
+
# status string that looks identical to a genuine completion. Recording it
|
|
384
|
+
# as board success was the prod "marked done, never posted" failure. Inspect
|
|
385
|
+
# the resident orchestrator's main-agent done signal; only a POSITIVE
|
|
386
|
+
# "ran but no done()" (False) routes to the failure/escalation rail —
|
|
387
|
+
# None (undeterminable) falls through to the legacy path unchanged. The
|
|
388
|
+
# orchestrator is fetched once here and reused for provenance below.
|
|
389
|
+
orchestrator = None
|
|
390
|
+
try:
|
|
391
|
+
orchestrator = self.task_agent.get_orchestrator(session_id)
|
|
392
|
+
except Exception:
|
|
393
|
+
orchestrator = None
|
|
394
|
+
from agents.task.runtime.run_as_session import completed_via_done
|
|
395
|
+
if completed_via_done(orchestrator) is False:
|
|
396
|
+
await self._fail_run(
|
|
397
|
+
goal, session_id,
|
|
398
|
+
error="run ended without completing (no done() — likely ran out of steps)",
|
|
399
|
+
outcome=outcome)
|
|
400
|
+
return
|
|
401
|
+
# §3.2 (fail-open — never block on uncertainty): with the judge on and
|
|
402
|
+
# an acceptance set, 'unmet' -> record_failure (normal breaker retries);
|
|
403
|
+
# 'met'/'unclear'/error -> the legacy success path.
|
|
404
|
+
import agents.task.goals.completion_judge as _cj
|
|
405
|
+
acceptance = (payload.get("acceptance") or "").strip()
|
|
406
|
+
if AutonomyConfig.goal_completion_judge() and acceptance:
|
|
407
|
+
verdict, reason = await _cj.judge_goal_completion(
|
|
408
|
+
self.task_agent, session_id, goal, final)
|
|
409
|
+
if verdict == "unmet":
|
|
410
|
+
await self._fail_run(goal, session_id,
|
|
411
|
+
error=f"completion judge: {reason}"[:2000],
|
|
412
|
+
outcome=outcome)
|
|
413
|
+
return
|
|
414
|
+
self.board.record_success(goal.id, session_id=session_id, result=str(final)[:4000])
|
|
415
|
+
# Stale-completion skip: an owner may have cancelled/paused the goal
|
|
416
|
+
# mid-run (T2 guards keep that status through record_success — see
|
|
417
|
+
# test_intervention_guards.py). Re-read the row; if it isn't 'done'
|
|
418
|
+
# the owner's decision wins — never write an outcome or self-wake for
|
|
419
|
+
# a run they already walked away from. Fail-open (get() error ->
|
|
420
|
+
# proceed) so a board without a working get() (fakes in older tests)
|
|
421
|
+
# doesn't lose success recording.
|
|
422
|
+
try:
|
|
423
|
+
refreshed = self.board.get(goal.id)
|
|
424
|
+
except Exception:
|
|
425
|
+
refreshed = None
|
|
426
|
+
if refreshed is not None and refreshed.status != STATUS_DONE:
|
|
427
|
+
return
|
|
428
|
+
try:
|
|
429
|
+
from modules.memory.episodic import finalize_episode, collect_provenance
|
|
430
|
+
# Reuse the orchestrator fetched for the T2-01 done-check above.
|
|
431
|
+
prov = await collect_provenance(orchestrator)
|
|
432
|
+
await finalize_episode(
|
|
433
|
+
session_id=session_id, user_id=goal.user_id, kind="goal",
|
|
434
|
+
task=getattr(goal, "title", None), outcome="done", goal_id=goal.id,
|
|
435
|
+
summary=str(final)[:2000] if final is not None else None,
|
|
436
|
+
spend_usd=prov["spend_usd"], steps=prov["steps"],
|
|
437
|
+
artifacts=prov["artifacts"], meta={"source": "goal"},
|
|
438
|
+
)
|
|
439
|
+
recorded_success = True
|
|
440
|
+
_goal_ev(goal, "done", session_id=session_id,
|
|
441
|
+
spend_usd=prov["spend_usd"], steps=prov["steps"])
|
|
442
|
+
except Exception:
|
|
443
|
+
logger.warning("goal episodic write failed", exc_info=True)
|
|
444
|
+
if outcome:
|
|
445
|
+
try:
|
|
446
|
+
self.board.set_outcome(goal.id, outcome)
|
|
447
|
+
except Exception:
|
|
448
|
+
logger.debug("set_outcome failed for %s", goal.id, exc_info=True)
|
|
449
|
+
if AutonomyConfig.goal_self_wake_enabled():
|
|
450
|
+
await self._self_wake(goal, session_id, final)
|
|
451
|
+
except Exception as e:
|
|
452
|
+
logger.error("goal %s run failed: %s", goal.id, e, exc_info=True)
|
|
453
|
+
_goal_ev(goal, "failed", reason=str(e)[:200], session_id=session_id)
|
|
454
|
+
try:
|
|
455
|
+
_g = self.board.record_failure(goal.id, error=str(e), session_id=session_id)
|
|
456
|
+
await self._maybe_escalate_blocked(_g)
|
|
457
|
+
except Exception:
|
|
458
|
+
pass
|
|
459
|
+
if session_id and not recorded_success:
|
|
460
|
+
try:
|
|
461
|
+
from modules.memory.episodic import finalize_episode
|
|
462
|
+
await finalize_episode(
|
|
463
|
+
session_id=session_id, user_id=goal.user_id, kind="goal",
|
|
464
|
+
task=getattr(goal, "title", None), outcome="failed",
|
|
465
|
+
goal_id=goal.id, summary=str(e)[:2000],
|
|
466
|
+
meta={"source": "goal"},
|
|
467
|
+
)
|
|
468
|
+
except Exception:
|
|
469
|
+
logger.warning("goal episodic write failed", exc_info=True)
|
|
470
|
+
finally:
|
|
471
|
+
hb_task.cancel()
|
|
472
|
+
if _shared_ws:
|
|
473
|
+
from core.interactive_gate import mark_idle
|
|
474
|
+
mark_idle()
|
|
475
|
+
|
|
476
|
+
async def _fail_blocked_declared(self, goal: Goal, session_id: Optional[str],
|
|
477
|
+
outcome: Optional[str], need: str) -> None:
|
|
478
|
+
"""§3.1: route an agent-declared BLOCKED outcome to the failure/escalation rail.
|
|
479
|
+
|
|
480
|
+
The agent already concluded retrying won't help, so after the standard
|
|
481
|
+
record_failure (whose CAS respects owner cancel/pause) a row that came back
|
|
482
|
+
'ready' is flipped straight to 'blocked' — skipping the breaker's remaining
|
|
483
|
+
retries. A non-ready row means the owner intervened; their decision wins.
|
|
484
|
+
"""
|
|
485
|
+
error = f"agent declared BLOCKED: {need or 'unspecified need'}"
|
|
486
|
+
await self._fail_run(goal, session_id, error=error, block=True, outcome=outcome)
|
|
487
|
+
|
|
488
|
+
async def _fail_run(self, goal: Goal, session_id: Optional[str], *, error: str,
|
|
489
|
+
block: bool = False, outcome: Optional[str] = None) -> None:
|
|
490
|
+
"""Shared verified-failure path for a run that finished but didn't deliver.
|
|
491
|
+
|
|
492
|
+
record_failure's CAS respects owner cancel/pause; with ``block=True`` a row
|
|
493
|
+
that came back 'ready' is flipped straight to 'blocked' (skipping the
|
|
494
|
+
breaker's remaining retries — used when retrying provably won't help).
|
|
495
|
+
"""
|
|
496
|
+
_g = self.board.record_failure(goal.id, error=error, session_id=session_id)
|
|
497
|
+
if block and getattr(_g, "status", None) == STATUS_READY:
|
|
498
|
+
try:
|
|
499
|
+
if self.board.block_from_ready(goal.id, error=error):
|
|
500
|
+
_g = self.board.get(goal.id) or _g
|
|
501
|
+
except Exception:
|
|
502
|
+
logger.debug("block_from_ready failed for %s", goal.id, exc_info=True)
|
|
503
|
+
# T4-03: surface the verified-failure outcome (blocked vs failed) in the durable
|
|
504
|
+
# event log so `polyrob telemetry` reflects it, not just the episodes table.
|
|
505
|
+
_goal_ev(goal, "blocked" if getattr(_g, "status", None) == STATUS_BLOCKED else "failed",
|
|
506
|
+
reason=str(error)[:200], session_id=session_id)
|
|
507
|
+
if outcome:
|
|
508
|
+
try:
|
|
509
|
+
self.board.set_outcome(goal.id, outcome)
|
|
510
|
+
except Exception:
|
|
511
|
+
logger.debug("set_outcome failed for %s", goal.id, exc_info=True)
|
|
512
|
+
await self._maybe_escalate_blocked(_g)
|
|
513
|
+
if session_id:
|
|
514
|
+
try:
|
|
515
|
+
from modules.memory.episodic import finalize_episode
|
|
516
|
+
await finalize_episode(
|
|
517
|
+
session_id=session_id, user_id=goal.user_id, kind="goal",
|
|
518
|
+
task=getattr(goal, "title", None), outcome="failed",
|
|
519
|
+
goal_id=goal.id, summary=error[:2000], meta={"source": "goal"},
|
|
520
|
+
)
|
|
521
|
+
except Exception:
|
|
522
|
+
logger.warning("goal episodic write failed", exc_info=True)
|
|
523
|
+
|
|
524
|
+
def _resolve_tools(self, goal: Goal) -> list:
|
|
525
|
+
"""Resolve the toolset for a goal run.
|
|
526
|
+
|
|
527
|
+
Precedence: the goal's own ``payload.tools`` if set; else, for a CHILD goal
|
|
528
|
+
(has ``parent_id``), inherit the parent's tools intersected with
|
|
529
|
+
``CHILD_INHERITABLE_TOOLS`` (so a self-decomposed goal isn't tool-starved
|
|
530
|
+
while never self-granting money/social tools); else the safe default.
|
|
531
|
+
"""
|
|
532
|
+
payload = goal.payload or {}
|
|
533
|
+
own = payload.get("tools")
|
|
534
|
+
if own:
|
|
535
|
+
return own
|
|
536
|
+
if goal.parent_id:
|
|
537
|
+
try:
|
|
538
|
+
parent = self.board.get(goal.parent_id)
|
|
539
|
+
except Exception:
|
|
540
|
+
parent = None
|
|
541
|
+
if parent is not None:
|
|
542
|
+
parent_tools = (parent.payload or {}).get("tools") or []
|
|
543
|
+
inheritable = child_inheritable_tools() # posture-aware (WS-8)
|
|
544
|
+
inherited = [t for t in parent_tools if t in inheritable]
|
|
545
|
+
if inherited:
|
|
546
|
+
return inherited
|
|
547
|
+
return default_goal_tools() # posture-aware (WS-8)
|
|
548
|
+
|
|
549
|
+
async def _over_budget(self, goal) -> bool:
|
|
550
|
+
"""True when the goal's tenant has spent >= the trailing-window budget.
|
|
551
|
+
|
|
552
|
+
Consults the unified ledger per-goal (the ledger is per-tenant; the ready
|
|
553
|
+
set is cross-tenant). Over budget -> raise a durable owner-visible ask and
|
|
554
|
+
optionally push, then hold the goal (left 'ready'). Fail-open: any ledger
|
|
555
|
+
error returns False so a metering hiccup never stalls dispatch."""
|
|
556
|
+
try:
|
|
557
|
+
from agents.task.constants import AutonomyConfig
|
|
558
|
+
budget = AutonomyConfig.autonomy_budget_usd()
|
|
559
|
+
if budget <= 0:
|
|
560
|
+
return False
|
|
561
|
+
window = AutonomyConfig.autonomy_budget_window_days()
|
|
562
|
+
from modules.credits.unified_ledger import build_ledger
|
|
563
|
+
ledger = await build_ledger(goal.user_id, days=window)
|
|
564
|
+
spent = float(ledger.get("total_spend_usd", 0.0) or 0.0)
|
|
565
|
+
if spent < budget:
|
|
566
|
+
# Back under budget — clear the push latch so a future over-budget
|
|
567
|
+
# episode for this tenant escalates again.
|
|
568
|
+
self._budget_pushed.discard(goal.user_id)
|
|
569
|
+
return False
|
|
570
|
+
except Exception:
|
|
571
|
+
logger.debug("budget gate check failed (fail-open)", exc_info=True)
|
|
572
|
+
return False
|
|
573
|
+
# Over budget — raise a durable, dedup-refreshing ask (owner-visible) and
|
|
574
|
+
# optionally push. Never claim/run the goal.
|
|
575
|
+
try:
|
|
576
|
+
self.board.create_ask(
|
|
577
|
+
user_id=goal.user_id,
|
|
578
|
+
what="Autonomous goals paused — spend budget reached",
|
|
579
|
+
why=(f"Spent ${spent:.2f} of ${budget:.2f} over the last {window}d; "
|
|
580
|
+
f"goal '{goal.title}' held. Raise AUTONOMY_BUDGET_USD or fulfill "
|
|
581
|
+
"this ask to resume."),
|
|
582
|
+
blocks_goal_ids=[goal.id],
|
|
583
|
+
)
|
|
584
|
+
except Exception:
|
|
585
|
+
logger.debug("budget ask creation skipped", exc_info=True)
|
|
586
|
+
# Owner PUSH once per over-budget episode (the ask row above is the durable,
|
|
587
|
+
# dedup-refreshed record; the push must not spam every tick per held goal).
|
|
588
|
+
if goal.user_id not in self._budget_pushed:
|
|
589
|
+
try:
|
|
590
|
+
from core.self_evolution import push_owner_message
|
|
591
|
+
container = getattr(self.task_agent, "container", None)
|
|
592
|
+
if container is not None:
|
|
593
|
+
await push_owner_message(
|
|
594
|
+
container,
|
|
595
|
+
f"[budget] ${spent:.2f}/${budget:.2f} spent over {window}d — autonomous "
|
|
596
|
+
f"goals paused. Goal '{goal.title}' held.")
|
|
597
|
+
self._budget_pushed.add(goal.user_id)
|
|
598
|
+
except Exception:
|
|
599
|
+
logger.debug("budget push skipped", exc_info=True)
|
|
600
|
+
logger.info("goal %s held: tenant over budget ($%.2f >= $%.2f)",
|
|
601
|
+
goal.id, spent, budget)
|
|
602
|
+
return True
|
|
603
|
+
|
|
604
|
+
async def _maybe_escalate_blocked(self, goal) -> None:
|
|
605
|
+
"""§7.2: when record_failure tripped the breaker (goal now 'blocked'), surface
|
|
606
|
+
a concrete ask to the owner instead of letting it die silently. Fail-open."""
|
|
607
|
+
try:
|
|
608
|
+
from agents.task.goals.escalation import maybe_escalate_blocked
|
|
609
|
+
await maybe_escalate_blocked(self.task_agent, goal)
|
|
610
|
+
except Exception:
|
|
611
|
+
logger.debug("blocker escalation skipped", exc_info=True)
|
|
612
|
+
# §7.2b / T2-03 / T4-04: ALWAYS leave a TRACKED ask for a blocked goal so the
|
|
613
|
+
# need survives even when no owner push went out. This ask creation was gated on
|
|
614
|
+
# the SAME goal_blocker_escalation() flag as the push, so with the default OFF a
|
|
615
|
+
# blocked goal left NO ask and `owner asks/fulfill` had nothing to consume — the
|
|
616
|
+
# need evaporated silently (the prod "X-write gap never escalated" shape). The
|
|
617
|
+
# ask is silent + durable + tenant-scoped and create_ask dedup-refreshes, so it
|
|
618
|
+
# is safe unconditionally; only the PUSH above stays posture-gated.
|
|
619
|
+
try:
|
|
620
|
+
from agents.task.goals.board import STATUS_BLOCKED
|
|
621
|
+
if getattr(goal, "status", None) == STATUS_BLOCKED:
|
|
622
|
+
self.board.create_ask(
|
|
623
|
+
user_id=goal.user_id,
|
|
624
|
+
what=f"Unblock goal: {goal.title}",
|
|
625
|
+
why=(goal.last_failure_error or "repeated failures"),
|
|
626
|
+
blocks_goal_ids=[goal.id],
|
|
627
|
+
)
|
|
628
|
+
except Exception:
|
|
629
|
+
logger.debug("blocked-goal ask creation skipped", exc_info=True)
|
|
630
|
+
|
|
631
|
+
async def _self_wake(self, goal: Goal, session_id: str, final: str) -> None:
|
|
632
|
+
"""Surface a completed goal's result (W1 rail). Fail-open.
|
|
633
|
+
|
|
634
|
+
T2-02: ``deliver_self_wake`` re-enters the goal's OWN just-finished session — an
|
|
635
|
+
empty room the owner never watches — so a completed goal told no one. In
|
|
636
|
+
addition to the (best-effort, agent-continuation) self-wake, PUSH the result to
|
|
637
|
+
the OWNER via ``push_owner_message`` (durable: a sink-less local owner still sees
|
|
638
|
+
it via ``polyrob telemetry``, per T4-04), so the owner is reliably told
|
|
639
|
+
regardless of whether a live session is watching. (Retargeting the
|
|
640
|
+
agent-continuation wake at the owner's live chat session needs a
|
|
641
|
+
latest-session-for-user resolver on the chat registry — deferred.)
|
|
642
|
+
"""
|
|
643
|
+
try:
|
|
644
|
+
text = (f"✅ Background goal '{goal.title}' completed.\n"
|
|
645
|
+
f"Result:\n{str(final)[:1500]}")
|
|
646
|
+
# T2-02: tell the OWNER (surface-independent, durable).
|
|
647
|
+
owner_told = False
|
|
648
|
+
try:
|
|
649
|
+
from core.self_evolution import push_owner_message
|
|
650
|
+
owner_told = await push_owner_message(
|
|
651
|
+
getattr(self.task_agent, "container", None), text)
|
|
652
|
+
except Exception:
|
|
653
|
+
logger.debug("goal completion owner-push skipped for %s", goal.id,
|
|
654
|
+
exc_info=True)
|
|
655
|
+
# Best-effort agent-continuation via the existing self-wake rail.
|
|
656
|
+
deliver = getattr(self.task_agent, "deliver_self_wake", None)
|
|
657
|
+
delivered = False
|
|
658
|
+
if deliver is not None:
|
|
659
|
+
delivered = await deliver(session_id, goal.user_id, text,
|
|
660
|
+
metadata={"source": "goal", "goal_id": goal.id})
|
|
661
|
+
if not (delivered or owner_told):
|
|
662
|
+
# Nobody was actually told (self-wake off/dropped/budget-exhausted AND no
|
|
663
|
+
# owner push landed) -- don't mark the episode surfaced (FIX1: that would
|
|
664
|
+
# make the session-start digest silently omit this run).
|
|
665
|
+
return
|
|
666
|
+
# Task 7: the self-wake delivery IS "telling the owner" — mark this
|
|
667
|
+
# goal's episode surfaced so the digest doesn't repeat it. Runs after
|
|
668
|
+
# finalize_episode (already written earlier in _run_goal), so the row
|
|
669
|
+
# exists for the UPDATE to find. Scoped to the goal's own user_id
|
|
670
|
+
# (FIX2) since episodes are keyed on the composite (user_id,
|
|
671
|
+
# session_id) and a bare session_id UPDATE could flip another
|
|
672
|
+
# tenant's row on a collision.
|
|
673
|
+
try:
|
|
674
|
+
from modules.memory.registry import get_memory_registry
|
|
675
|
+
prov = get_memory_registry().active()
|
|
676
|
+
if prov is not None and hasattr(prov, "mark_episode_surfaced"):
|
|
677
|
+
prov.mark_episode_surfaced(session_id=session_id, user_id=goal.user_id)
|
|
678
|
+
except Exception:
|
|
679
|
+
logger.debug("goal self-wake surfaced-mark skipped for %s", goal.id,
|
|
680
|
+
exc_info=True)
|
|
681
|
+
except Exception as e:
|
|
682
|
+
logger.debug("goal self-wake skipped for %s: %s", goal.id, e)
|
|
683
|
+
|
|
684
|
+
async def _maybe_plan(self, *, headroom_after: int) -> None:
|
|
685
|
+
"""Fire ONE planning session when the queue is thin. All gates mechanical."""
|
|
686
|
+
from agents.task.constants import AutonomyConfig
|
|
687
|
+
if not AutonomyConfig.goal_planner_enabled():
|
|
688
|
+
return
|
|
689
|
+
if headroom_after <= 0 and AutonomyConfig.goal_daily_quota() > 0:
|
|
690
|
+
return
|
|
691
|
+
min_ready = AutonomyConfig.goal_planner_min_ready()
|
|
692
|
+
if len(self.board.ready(limit=min_ready)) >= min_ready:
|
|
693
|
+
return
|
|
694
|
+
# any tenant with an active objective (v1: single-user)
|
|
695
|
+
users = {o.user_id for o in self._active_objective_owners()}
|
|
696
|
+
if not users:
|
|
697
|
+
return
|
|
698
|
+
last = self.board.last_planner_run_at()
|
|
699
|
+
cooldown = AutonomyConfig.goal_planner_cooldown_sec()
|
|
700
|
+
import time as _time
|
|
701
|
+
if last is not None and (_time.time() - last) < cooldown:
|
|
702
|
+
return
|
|
703
|
+
self.board.mark_planner_run() # BEFORE dispatch: no double-fire while running
|
|
704
|
+
user_id = sorted(users)[0]
|
|
705
|
+
t = asyncio.create_task(self._run_planner(user_id))
|
|
706
|
+
self._inflight.add(t)
|
|
707
|
+
t.add_done_callback(self._inflight.discard)
|
|
708
|
+
|
|
709
|
+
def _active_objective_owners(self):
|
|
710
|
+
from agents.task.goals.board import OBJ_ACTIVE
|
|
711
|
+
try:
|
|
712
|
+
rows = self.board.list(status=OBJ_ACTIVE, limit=50)
|
|
713
|
+
return [r for r in rows if r.kind == "objective"]
|
|
714
|
+
except Exception:
|
|
715
|
+
return []
|
|
716
|
+
|
|
717
|
+
async def _run_planner(self, user_id: str) -> None:
|
|
718
|
+
try:
|
|
719
|
+
from agents.task.constants import AutonomyConfig
|
|
720
|
+
from agents.task.goals.planner import (
|
|
721
|
+
PLANNER_MAX_STEPS, PLANNER_TOOLS, build_planner_prompt,
|
|
722
|
+
)
|
|
723
|
+
deliverables_root = None
|
|
724
|
+
try:
|
|
725
|
+
from agents.task.path import pm
|
|
726
|
+
if pm().is_project_root_workspace:
|
|
727
|
+
deliverables_root = pm().project_root
|
|
728
|
+
except Exception:
|
|
729
|
+
deliverables_root = None
|
|
730
|
+
prompt = build_planner_prompt(
|
|
731
|
+
self.board, user_id, deliverables_root,
|
|
732
|
+
history_n=AutonomyConfig.goal_planner_history_n())
|
|
733
|
+
from core.runtime_config import resolve_runtime_config
|
|
734
|
+
provider = resolve_runtime_config(None, None)[0]
|
|
735
|
+
model = None
|
|
736
|
+
try:
|
|
737
|
+
from modules.llm.llm_client_registry import get_default_model
|
|
738
|
+
model = get_default_model(provider)
|
|
739
|
+
except Exception:
|
|
740
|
+
model = None
|
|
741
|
+
request = {
|
|
742
|
+
"task": prompt,
|
|
743
|
+
"provider": provider,
|
|
744
|
+
"model": model,
|
|
745
|
+
"tools": list(PLANNER_TOOLS),
|
|
746
|
+
"max_steps": PLANNER_MAX_STEPS,
|
|
747
|
+
"temperature": 0.0,
|
|
748
|
+
}
|
|
749
|
+
session_id, final = await _run_task_as_session(
|
|
750
|
+
self.task_agent, user_id=user_id, request=request, autonomous=True)
|
|
751
|
+
logger.info("goal planner ran (session=%s): %s",
|
|
752
|
+
session_id, (final or "no result")[:200])
|
|
753
|
+
await self._maybe_escalate_empty_pipeline(user_id, planner_summary=final)
|
|
754
|
+
except Exception as e:
|
|
755
|
+
logger.error("goal planner run failed: %s", e, exc_info=True)
|
|
756
|
+
|
|
757
|
+
async def _maybe_escalate_empty_pipeline(self, user_id: str, *,
|
|
758
|
+
planner_summary: Optional[str] = None) -> None:
|
|
759
|
+
"""§7.2 tail: a planner run that STILL leaves the board empty is a stall.
|
|
760
|
+
|
|
761
|
+
After ``GOAL_EMPTY_PIPELINE_ESCALATE_AFTER`` consecutive such runs, surface
|
|
762
|
+
the stall (with the planner's own last word — usually the concrete blocker)
|
|
763
|
+
to the owner exactly once. Fail-open; both counters reset on refill.
|
|
764
|
+
"""
|
|
765
|
+
try:
|
|
766
|
+
if self.board.ready(limit=1):
|
|
767
|
+
self._empty_planner_runs = 0
|
|
768
|
+
self._empty_pipeline_escalated = False
|
|
769
|
+
return
|
|
770
|
+
self._empty_planner_runs += 1
|
|
771
|
+
from agents.task.constants import AutonomyConfig
|
|
772
|
+
if self._empty_planner_runs < AutonomyConfig.goal_empty_pipeline_escalate_after():
|
|
773
|
+
return
|
|
774
|
+
if self._empty_pipeline_escalated:
|
|
775
|
+
return
|
|
776
|
+
objective_title = None
|
|
777
|
+
for o in self._active_objective_owners():
|
|
778
|
+
if o.user_id == user_id:
|
|
779
|
+
objective_title = o.title
|
|
780
|
+
break
|
|
781
|
+
# T2-03/T4-04: mark the stall escalated once the threshold is reached,
|
|
782
|
+
# independent of whether the owner PUSH lands — the durable ask below is the
|
|
783
|
+
# owner-visible artifact and must be created even under the silent posture.
|
|
784
|
+
# (_empty_pipeline_escalated resets when the board refills, so this stays
|
|
785
|
+
# once-per-stall and never spams.)
|
|
786
|
+
self._empty_pipeline_escalated = True
|
|
787
|
+
from agents.task.goals.escalation import maybe_escalate_empty_pipeline
|
|
788
|
+
await maybe_escalate_empty_pipeline(
|
|
789
|
+
self.task_agent, objective_title=objective_title,
|
|
790
|
+
planner_summary=planner_summary)
|
|
791
|
+
# §7.2b: track the stall as an ask so it is fulfillable regardless of push.
|
|
792
|
+
try:
|
|
793
|
+
self.board.create_ask(
|
|
794
|
+
user_id=user_id,
|
|
795
|
+
what=f"Goal pipeline empty for '{objective_title or 'the objective'}'",
|
|
796
|
+
why=(planner_summary or "")[:2000],
|
|
797
|
+
)
|
|
798
|
+
except Exception:
|
|
799
|
+
logger.debug("empty-pipeline ask creation skipped", exc_info=True)
|
|
800
|
+
except Exception:
|
|
801
|
+
logger.debug("empty-pipeline escalation skipped", exc_info=True)
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
class GoalTicker:
|
|
805
|
+
"""Periodically run dispatch_once until stopped (mirrors CronTicker)."""
|
|
806
|
+
|
|
807
|
+
def __init__(self, dispatcher: GoalDispatcher, interval_seconds: int = 60):
|
|
808
|
+
self.dispatcher = dispatcher
|
|
809
|
+
self.interval_seconds = interval_seconds
|
|
810
|
+
|
|
811
|
+
async def tick_once(self) -> int:
|
|
812
|
+
return await self.dispatcher.dispatch_once()
|
|
813
|
+
|
|
814
|
+
async def run_forever(self, stop_event: Optional[asyncio.Event] = None) -> None:
|
|
815
|
+
from core.tickers import IntervalTicker
|
|
816
|
+
from agents.task.constants import (
|
|
817
|
+
ticker_idle_backoff_enabled,
|
|
818
|
+
ticker_idle_backoff_max_multiplier,
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
is_active = None
|
|
822
|
+
max_interval = None
|
|
823
|
+
if ticker_idle_backoff_enabled():
|
|
824
|
+
is_active = lambda dispatched: bool(dispatched)
|
|
825
|
+
max_interval = self.interval_seconds * ticker_idle_backoff_max_multiplier()
|
|
826
|
+
|
|
827
|
+
await IntervalTicker(
|
|
828
|
+
self.dispatcher.dispatch_once,
|
|
829
|
+
self.interval_seconds,
|
|
830
|
+
is_active=is_active,
|
|
831
|
+
max_interval_seconds=max_interval,
|
|
832
|
+
).run_forever(stop_event=stop_event)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def build_goal_ticker(task_agent: Any, *, data_dir: str = "data",
|
|
836
|
+
interval_seconds: Optional[int] = None) -> GoalTicker:
|
|
837
|
+
"""Assemble the goal stack into a GoalTicker the app lifespan can start/stop.
|
|
838
|
+
|
|
839
|
+
Shares ``<data_dir>/goals.db`` with the agent-facing ``goal`` tool so goals the
|
|
840
|
+
tool creates are picked up by the ticker; the tick lock keeps it safe under
|
|
841
|
+
``workers>1``.
|
|
842
|
+
"""
|
|
843
|
+
from agents.task.constants import AutonomyConfig
|
|
844
|
+
board = GoalBoard(os.path.join(data_dir, "goals.db"))
|
|
845
|
+
dispatcher = GoalDispatcher(board, task_agent,
|
|
846
|
+
lock_path=os.path.join(data_dir, "goals.tick.lock"))
|
|
847
|
+
return GoalTicker(dispatcher,
|
|
848
|
+
interval_seconds=interval_seconds or AutonomyConfig.goal_dispatch_interval_sec())
|