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
core/surfaces/surface.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Surface ABC: thin required surface (Hermes parity) + fat shared defaults.
|
|
2
|
+
|
|
3
|
+
Required (every surface MUST implement): surface_id, capabilities, send(), start(),
|
|
4
|
+
stop().
|
|
5
|
+
|
|
6
|
+
Shared, default-provided (a surface gets these for free):
|
|
7
|
+
- stream(): buffers deltas and flushes one send() on finalize, OR — when the surface
|
|
8
|
+
opts into incremental streaming AND advertises supports_edit — runs the generic
|
|
9
|
+
live-edit engine below (open one message, edit it in place as deltas arrive,
|
|
10
|
+
flood-throttle, split overflow on finalize). The engine is surface-AGNOSTIC; a
|
|
11
|
+
surface plugs in four small transport primitives (_open_stream_message,
|
|
12
|
+
_edit_stream_message, _send_stream_overflow, _stream_target) plus two policy hooks
|
|
13
|
+
(_incremental_streaming_enabled, _edit_min_interval_sec). This is what lets Telegram
|
|
14
|
+
today and WhatsApp/etc. tomorrow share one streaming state machine.
|
|
15
|
+
- identify(): defaults to None.
|
|
16
|
+
|
|
17
|
+
The principle: the base abstraction holds the maximum shared machinery; each surface
|
|
18
|
+
adds only what is genuinely transport-specific on top.
|
|
19
|
+
|
|
20
|
+
STATUS (incremental streaming): wired end-to-end. feed.py publishes a turn's deltas with a
|
|
21
|
+
stable per-TURN stream_id (the session_key); under an edit-capable surface with the flag on,
|
|
22
|
+
they open+edit ONE live message. The turn's discrete reply (the curated send_message text)
|
|
23
|
+
arrives via send(), which calls _finalize_live_on_send() to COMMIT that same bubble in place
|
|
24
|
+
— so the PERSISTED final is the clean reply, never raw stream tokens, and there is no
|
|
25
|
+
duplicate message. A bounded _MAX_LIVE_STREAMS cap bounds any un-finalized stream (e.g. a
|
|
26
|
+
turn that streams but ends via done() instead of a reply).
|
|
27
|
+
|
|
28
|
+
CAVEAT (why it stays opt-in / default-OFF): the INTERMEDIATE frames shown mid-stream are the
|
|
29
|
+
raw model deltas, scrubbed per-chunk by MessageRouter (scrub_brain_blocks) — best-effort, so
|
|
30
|
+
a brain/think block straddling a chunk boundary can briefly surface before the clean final
|
|
31
|
+
commit. The persisted final is always clean. Enable TELEGRAM_INCREMENTAL_STREAM only where
|
|
32
|
+
that intermediate exposure is acceptable; OFF keeps the buffered one-send-on-finalize path.
|
|
33
|
+
"""
|
|
34
|
+
import logging
|
|
35
|
+
import time as _time
|
|
36
|
+
from abc import ABC, abstractmethod
|
|
37
|
+
from dataclasses import dataclass, field
|
|
38
|
+
from typing import Any, List, Optional
|
|
39
|
+
|
|
40
|
+
from core.surfaces.envelopes import (
|
|
41
|
+
OutboundMessage, SendResult, SurfaceCapabilities, Identity,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class SurfaceConfigField:
|
|
49
|
+
name: str
|
|
50
|
+
secret: bool = False
|
|
51
|
+
required: bool = True
|
|
52
|
+
env_alias: str | None = None
|
|
53
|
+
help: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class SurfaceConfigSchema:
|
|
58
|
+
fields: list = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def split_message(text: str, limit: int) -> List[str]:
|
|
62
|
+
"""Split text into <=limit-sized chunks (shared by send() and the stream engine)."""
|
|
63
|
+
if not text:
|
|
64
|
+
return [text]
|
|
65
|
+
return [text[i:i + limit] for i in range(0, len(text), limit)]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _LiveStream:
|
|
69
|
+
"""In-progress streamed reply for one stream_id: the single message we keep editing
|
|
70
|
+
in place, plus accumulated text and last-render bookkeeping. Surface-agnostic —
|
|
71
|
+
``target`` is whatever address the concrete surface's _stream_target() returns."""
|
|
72
|
+
__slots__ = ("target", "message_id", "text", "rendered", "last_edit")
|
|
73
|
+
|
|
74
|
+
def __init__(self, target: Any) -> None:
|
|
75
|
+
self.target = target
|
|
76
|
+
self.message_id: Optional[Any] = None
|
|
77
|
+
self.text = ""
|
|
78
|
+
self.rendered = "" # last text actually pushed to the surface
|
|
79
|
+
self.last_edit = 0.0 # monotonic ts of last edit (flood throttle)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Surface(ABC):
|
|
83
|
+
def __init__(self) -> None:
|
|
84
|
+
self._stream_buffers: dict[str, list] = {}
|
|
85
|
+
self._live: dict[str, _LiveStream] = {}
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def surface_id(self) -> str: ...
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def capabilities(self) -> SurfaceCapabilities: ...
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
async def send(self, msg: OutboundMessage) -> SendResult: ...
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
async def start(self, container) -> None: ...
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
async def stop(self) -> None: ...
|
|
103
|
+
|
|
104
|
+
async def stream(self, msg: OutboundMessage) -> None:
|
|
105
|
+
"""Route a streamed delta: live-edit when the surface opts in + supports edit,
|
|
106
|
+
else the buffered default (commit one send() on finalize)."""
|
|
107
|
+
if self._incremental_streaming_enabled() and self.capabilities.supports_edit:
|
|
108
|
+
return await self._stream_incremental(msg)
|
|
109
|
+
key = msg.stream_id or msg.session_key
|
|
110
|
+
if msg.partial:
|
|
111
|
+
buf = self._stream_buffers.get(key)
|
|
112
|
+
if buf is None:
|
|
113
|
+
# M4: bound the number of buffered streams (evict oldest). The streaming
|
|
114
|
+
# mirror may never emit a partial=False finalize, so without this the
|
|
115
|
+
# buffer map grows for the daemon's lifetime.
|
|
116
|
+
if len(self._stream_buffers) >= self._MAX_LIVE_STREAMS:
|
|
117
|
+
oldest = next(iter(self._stream_buffers))
|
|
118
|
+
self._stream_buffers.pop(oldest, None)
|
|
119
|
+
buf = self._stream_buffers[key] = []
|
|
120
|
+
buf.append(msg.text)
|
|
121
|
+
# And bound each un-finalized stream's chunk list (drop oldest chunks) so a
|
|
122
|
+
# single never-flushed key can't grow without limit.
|
|
123
|
+
if len(buf) > self._MAX_STREAM_CHUNKS:
|
|
124
|
+
del buf[:len(buf) - self._MAX_STREAM_CHUNKS]
|
|
125
|
+
return
|
|
126
|
+
buffered = "".join(self._stream_buffers.pop(key, []))
|
|
127
|
+
final_text = buffered + (msg.text or "")
|
|
128
|
+
await self.send(OutboundMessage(
|
|
129
|
+
session_key=msg.session_key, text=final_text, kind=msg.kind,
|
|
130
|
+
partial=False, stream_id=msg.stream_id, reply_to=msg.reply_to,
|
|
131
|
+
))
|
|
132
|
+
|
|
133
|
+
async def identify(self, raw: dict) -> Optional[Identity]:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def config_schema(cls) -> "SurfaceConfigSchema":
|
|
138
|
+
return SurfaceConfigSchema(fields=[])
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def validate_config(cls, values: dict) -> tuple[bool, str]:
|
|
142
|
+
missing = [f.name for f in cls.config_schema().fields
|
|
143
|
+
if f.required and not values.get(f.name)]
|
|
144
|
+
return (not missing, "" if not missing else f"missing: {', '.join(missing)}")
|
|
145
|
+
|
|
146
|
+
def render_outbound(self, text: str) -> List[str]:
|
|
147
|
+
"""Escape + split outbound text per this surface's capabilities. Surfaces should
|
|
148
|
+
use this instead of re-implementing per-platform chunking."""
|
|
149
|
+
from core.surfaces.rendering import render_for_flavor
|
|
150
|
+
caps = self.capabilities
|
|
151
|
+
return render_for_flavor(text, caps.markdown_flavor, caps.max_message_bytes)
|
|
152
|
+
|
|
153
|
+
def can_send_now(self, session_key: str, *, now: Optional[float] = None) -> "SendDecision":
|
|
154
|
+
"""Whether a (proactive) send to this chat is allowed right now. Base default:
|
|
155
|
+
ALLOW (free outbound). A windowed surface overrides this. Pure + sync so producers
|
|
156
|
+
can consult it cheaply before enqueuing."""
|
|
157
|
+
from core.surfaces.send_policy import SendDecision
|
|
158
|
+
return SendDecision.ALLOW
|
|
159
|
+
|
|
160
|
+
async def _finalize_live_on_send(self, msg: OutboundMessage) -> bool:
|
|
161
|
+
"""If an in-flight live stream exists for this message's session, COMMIT it with
|
|
162
|
+
this (clean, curated) discrete text instead of letting the surface send a separate
|
|
163
|
+
message — so the turn's discrete reply finalizes the streamed bubble in place (no
|
|
164
|
+
duplicate, and the persisted final text is the clean reply, not raw stream tokens).
|
|
165
|
+
|
|
166
|
+
A surface's send() calls this first and returns early when it returns True. No-op
|
|
167
|
+
(returns False) when incremental streaming is off, the surface can't edit, or no
|
|
168
|
+
live stream is open for this session — so the normal send path is unchanged."""
|
|
169
|
+
if not (self._incremental_streaming_enabled() and self.capabilities.supports_edit):
|
|
170
|
+
return False
|
|
171
|
+
key = msg.stream_id or msg.session_key
|
|
172
|
+
st = self._live.pop(key, None)
|
|
173
|
+
if st is None or st.message_id is None:
|
|
174
|
+
return False
|
|
175
|
+
await self._commit_final_stream(st, msg.text or "")
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
# --- incremental streaming engine (generic; surfaces provide the primitives) ---
|
|
179
|
+
#
|
|
180
|
+
# A surface enables this by returning True from _incremental_streaming_enabled()
|
|
181
|
+
# and advertising capabilities.supports_edit, then implementing the transport
|
|
182
|
+
# primitives below. Everything here is shared.
|
|
183
|
+
|
|
184
|
+
def _incremental_streaming_enabled(self) -> bool:
|
|
185
|
+
"""Opt-in switch (default OFF -> buffered)."""
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
def _stream_target(self, msg: OutboundMessage) -> Any:
|
|
189
|
+
"""The surface-specific address to send/edit against (default: the session_key;
|
|
190
|
+
Telegram parses the chat id out of it)."""
|
|
191
|
+
return msg.session_key
|
|
192
|
+
|
|
193
|
+
def _edit_min_interval_sec(self) -> float:
|
|
194
|
+
"""Minimum seconds between in-place edits (flood-control). 0 = edit every delta."""
|
|
195
|
+
return 0.0
|
|
196
|
+
|
|
197
|
+
async def _open_stream_message(self, target: Any, text: str) -> Optional[Any]:
|
|
198
|
+
"""Send the first visible delta as a new message; return its id (for editing)."""
|
|
199
|
+
raise NotImplementedError
|
|
200
|
+
|
|
201
|
+
async def _edit_stream_message(self, target: Any, message_id: Any, text: str) -> None:
|
|
202
|
+
"""Replace the open message's text in place."""
|
|
203
|
+
raise NotImplementedError
|
|
204
|
+
|
|
205
|
+
async def _send_stream_overflow(self, target: Any, text: str) -> None:
|
|
206
|
+
"""Emit text past the first message's size limit as a fresh message."""
|
|
207
|
+
raise NotImplementedError
|
|
208
|
+
|
|
209
|
+
def _on_stream_error(self, target: Any, op: str, e: Exception) -> None:
|
|
210
|
+
"""Handle a transport error during streaming (default: log). A surface can
|
|
211
|
+
override to record provider back-off (e.g. Telegram RetryAfter)."""
|
|
212
|
+
logger.debug("surface %s stream %s failed for %s: %s", self.surface_id, op, target, e)
|
|
213
|
+
|
|
214
|
+
# Defensive cap on in-flight streams. The engine finalizes (and pops) a stream when a
|
|
215
|
+
# matching partial=False arrives; until the live producers emit such a finalize with a
|
|
216
|
+
# STABLE per-turn stream_id (current feed.py uses a per-STEP stream_id and no finalize
|
|
217
|
+
# reaches stream()), an enabled surface could otherwise accumulate one entry per step.
|
|
218
|
+
# This bound makes the worst case a bounded ring, never an unbounded leak. See the
|
|
219
|
+
# module docstring: incremental streaming is engine-correct but not yet wired live —
|
|
220
|
+
# keep the opt-in flag OFF until the producer emits per-turn finalizes.
|
|
221
|
+
_MAX_LIVE_STREAMS = 256
|
|
222
|
+
_MAX_STREAM_CHUNKS = 4096 # cap buffered deltas per un-finalized stream (M4)
|
|
223
|
+
|
|
224
|
+
async def _stream_incremental(self, msg: OutboundMessage) -> None:
|
|
225
|
+
key = msg.stream_id or msg.session_key
|
|
226
|
+
if msg.partial:
|
|
227
|
+
st = self._live.get(key)
|
|
228
|
+
if st is None:
|
|
229
|
+
if len(self._live) >= self._MAX_LIVE_STREAMS:
|
|
230
|
+
# Evict the oldest in-flight stream (insertion order) so the map stays
|
|
231
|
+
# bounded even if finalizes never arrive.
|
|
232
|
+
oldest = next(iter(self._live))
|
|
233
|
+
self._live.pop(oldest, None)
|
|
234
|
+
st = _LiveStream(self._stream_target(msg))
|
|
235
|
+
self._live[key] = st
|
|
236
|
+
st.text += (msg.text or "")
|
|
237
|
+
await self._render_live(st, final=False)
|
|
238
|
+
return
|
|
239
|
+
# Finalize: commit the full text and drop the live-stream state.
|
|
240
|
+
st = self._live.pop(key, None)
|
|
241
|
+
final_text = (st.text if st else "") + (msg.text or "")
|
|
242
|
+
if st is None or st.message_id is None:
|
|
243
|
+
# No live message was opened (all-whitespace partials, or a bare finalize)
|
|
244
|
+
# -> a normal send so the reply still lands.
|
|
245
|
+
await self.send(OutboundMessage(
|
|
246
|
+
session_key=msg.session_key, text=final_text, kind=msg.kind,
|
|
247
|
+
))
|
|
248
|
+
return
|
|
249
|
+
await self._commit_final_stream(st, final_text)
|
|
250
|
+
|
|
251
|
+
async def _render_live(self, st: _LiveStream, *, final: bool) -> None:
|
|
252
|
+
max_bytes = self.capabilities.max_message_bytes
|
|
253
|
+
text = (st.text or "")[:max_bytes]
|
|
254
|
+
if not text.strip():
|
|
255
|
+
return
|
|
256
|
+
if st.message_id is None:
|
|
257
|
+
try:
|
|
258
|
+
st.message_id = await self._open_stream_message(st.target, text)
|
|
259
|
+
st.rendered = text
|
|
260
|
+
st.last_edit = _time.monotonic()
|
|
261
|
+
except Exception as e: # fail-open: never raise into the agent loop
|
|
262
|
+
self._on_stream_error(st.target, "open", e)
|
|
263
|
+
return
|
|
264
|
+
if not final:
|
|
265
|
+
if _time.monotonic() - st.last_edit < self._edit_min_interval_sec():
|
|
266
|
+
return
|
|
267
|
+
if text == st.rendered:
|
|
268
|
+
return # an unchanged edit is rejected by some surfaces ("not modified")
|
|
269
|
+
await self._do_edit(st, text)
|
|
270
|
+
|
|
271
|
+
async def _commit_final_stream(self, st: _LiveStream, final_text: str) -> None:
|
|
272
|
+
chunks = split_message(final_text, self.capabilities.max_message_bytes)
|
|
273
|
+
first = chunks[0] if chunks else ""
|
|
274
|
+
if first and first != st.rendered:
|
|
275
|
+
await self._do_edit(st, first)
|
|
276
|
+
for chunk in chunks[1:]:
|
|
277
|
+
try:
|
|
278
|
+
await self._send_stream_overflow(st.target, chunk)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
self._on_stream_error(st.target, "overflow", e)
|
|
281
|
+
|
|
282
|
+
async def _do_edit(self, st: _LiveStream, text: str) -> None:
|
|
283
|
+
try:
|
|
284
|
+
await self._edit_stream_message(st.target, st.message_id, text)
|
|
285
|
+
st.rendered = text
|
|
286
|
+
st.last_edit = _time.monotonic()
|
|
287
|
+
except Exception as e:
|
|
288
|
+
self._on_stream_error(st.target, "edit", e)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Surface-agnostic voice→text seam. The Transcriber is built ONCE and registered on the
|
|
2
|
+
container (was a lazy per-Telegram-harness instance — invisible to Email/WhatsApp). Every
|
|
3
|
+
surface normalizes inbound audio to Media (core.surfaces.media) and calls
|
|
4
|
+
transcribe_inbound_media(); the untranscribed-voice outcome is handled by one shared guard
|
|
5
|
+
(core.surfaces.voice_guard) so no surface can silently route an empty voice turn."""
|
|
6
|
+
import logging
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
from core.surfaces.media import Media
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def voice_present(media: List[Media]) -> bool:
|
|
15
|
+
return any(getattr(m, "kind", None) in ("voice", "audio") for m in (media or []))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_transcriber(container):
|
|
19
|
+
"""Build-once, container-registered Transcriber. Falls back to NullTranscriber when
|
|
20
|
+
faster-whisper isn't importable (build_transcriber already degrades). Registered so
|
|
21
|
+
every surface shares one model load instead of each harness building its own."""
|
|
22
|
+
existing = container.get_service("transcriber") if container else None
|
|
23
|
+
if existing is not None:
|
|
24
|
+
return existing
|
|
25
|
+
from agents.task.surface_config import SurfaceConfig
|
|
26
|
+
from modules.transcription import build_transcriber
|
|
27
|
+
t = build_transcriber(SurfaceConfig.voice_transcription_model())
|
|
28
|
+
try:
|
|
29
|
+
container.register_service("transcriber", t)
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
return t
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def _audio_bytes(media: Media) -> Optional[bytes]:
|
|
36
|
+
if media.data:
|
|
37
|
+
return media.data
|
|
38
|
+
if media.url:
|
|
39
|
+
try:
|
|
40
|
+
import aiohttp
|
|
41
|
+
async with aiohttp.ClientSession() as s:
|
|
42
|
+
async with s.get(media.url) as r:
|
|
43
|
+
return await r.read()
|
|
44
|
+
except Exception as e:
|
|
45
|
+
logger.debug("transcription: media url fetch failed: %s", e)
|
|
46
|
+
return None
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def transcribe_inbound_media(container, media: List[Media]) -> Optional[str]:
|
|
51
|
+
"""Transcribe the first voice/audio Media to text, or None. Fail-open."""
|
|
52
|
+
from agents.task.surface_config import SurfaceConfig
|
|
53
|
+
if not SurfaceConfig.voice_transcription_enabled():
|
|
54
|
+
return None
|
|
55
|
+
target = next((m for m in (media or []) if getattr(m, "kind", None) in ("voice", "audio")), None)
|
|
56
|
+
if target is None:
|
|
57
|
+
return None
|
|
58
|
+
audio = await _audio_bytes(target)
|
|
59
|
+
if not audio:
|
|
60
|
+
return None
|
|
61
|
+
try:
|
|
62
|
+
transcriber = get_transcriber(container)
|
|
63
|
+
text = await transcriber.transcribe(audio, mime=target.mime or "audio/ogg")
|
|
64
|
+
except Exception as e: # belt-and-suspenders; transcriber is itself fail-open
|
|
65
|
+
logger.debug("transcribe_inbound_media failed: %s", e)
|
|
66
|
+
return None
|
|
67
|
+
return (text or "").strip() or None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def log_transcription_readiness(container) -> None:
|
|
71
|
+
"""One-line startup signal so a 'voice silently dropped' deploy is visible. WARN when
|
|
72
|
+
voice is enabled but the real engine isn't importable (the #1 root cause)."""
|
|
73
|
+
from agents.task.surface_config import SurfaceConfig
|
|
74
|
+
if not SurfaceConfig.voice_transcription_enabled():
|
|
75
|
+
logger.info("voice transcription: DISABLED (VOICE_TRANSCRIPTION_ENABLED=false)")
|
|
76
|
+
return
|
|
77
|
+
try:
|
|
78
|
+
import faster_whisper # noqa: F401
|
|
79
|
+
logger.info("voice transcription: ENABLED (faster-whisper present, model=%s)",
|
|
80
|
+
SurfaceConfig.voice_transcription_model())
|
|
81
|
+
except Exception:
|
|
82
|
+
msg = ("voice transcription ENABLED but faster-whisper is NOT installed — voice "
|
|
83
|
+
"messages will be refused with a notice. Install the 'voice' extra or set "
|
|
84
|
+
"VOICE_TRANSCRIPTION_ENABLED=false.")
|
|
85
|
+
if SurfaceConfig.voice_transcription_required():
|
|
86
|
+
logger.error("STARTUP: %s", msg)
|
|
87
|
+
else:
|
|
88
|
+
logger.warning("STARTUP: %s", msg)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Shared voice-transcript echo builder. Mirrors core.surfaces.voice_guard: one place so
|
|
2
|
+
every surface renders an identical persistent 'here's what I heard' message before the
|
|
3
|
+
agent answers. Transport-free."""
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from core.surfaces.media import Media
|
|
7
|
+
|
|
8
|
+
_ECHO_CAP = 3500 # keep under Telegram's 4096 limit; the FULL transcript still routes to the agent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def voice_transcript(media: List[Media]) -> Optional[str]:
|
|
12
|
+
"""Return the first voice/audio Media's stamped transcript (stripped), else None."""
|
|
13
|
+
for m in media or []:
|
|
14
|
+
if getattr(m, "kind", None) in ("voice", "audio"):
|
|
15
|
+
t = getattr(m, "transcript", None)
|
|
16
|
+
if t and t.strip():
|
|
17
|
+
return t.strip()
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def voice_echo_message(transcript: str) -> str:
|
|
22
|
+
"""Render the persistent echo shown before the agent's answer. Capped, ellipsis on overflow."""
|
|
23
|
+
t = (transcript or "").strip()
|
|
24
|
+
if len(t) > _ECHO_CAP:
|
|
25
|
+
t = t[:_ECHO_CAP].rstrip() + "…"
|
|
26
|
+
return f'🎙️ Transcript: "{t}"'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""One shared voice guard so NO surface can silently route an untranscribed voice note.
|
|
2
|
+
A voice message that produced no usable transcript -> the surface tells the user (loudly)
|
|
3
|
+
and does NOT dispatch an empty turn to the agent (the live bug: empty voice turns reaching
|
|
4
|
+
the agent, which then 'understood nothing')."""
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from core.surfaces.media import Media
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def voice_needs_guard(media: List[Media], inbound_text) -> bool:
|
|
11
|
+
"""True iff the inbound carries voice/audio but yielded no usable text."""
|
|
12
|
+
has_voice = any(getattr(m, "kind", None) in ("voice", "audio") for m in (media or []))
|
|
13
|
+
if not has_voice:
|
|
14
|
+
return False
|
|
15
|
+
return not (inbound_text or "").strip()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def voice_unavailable_message(enabled: bool) -> str:
|
|
19
|
+
if not enabled:
|
|
20
|
+
return ("🎤 I can't process voice messages right now — voice transcription is "
|
|
21
|
+
"turned off. Please send your message as text.")
|
|
22
|
+
return ("🎤 I couldn't transcribe that voice message (it may be empty, too noisy, or "
|
|
23
|
+
"transcription isn't available on this server). Please try again or send text.")
|
core/tickers.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Shared interval-ticker loop + lifespan supervisor (consolidates cron/goal/curator).
|
|
2
|
+
|
|
3
|
+
The canonical loop is lifted verbatim from ``cron/runner.py::CronTicker.run_forever``
|
|
4
|
+
so all four ticker variants share identical scheduling semantics.
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any, Awaitable, Callable, Optional
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _heartbeat_interval_sec() -> int:
|
|
15
|
+
try:
|
|
16
|
+
return max(30, int(os.getenv("AUTONOMY_HEARTBEAT_INTERVAL_SEC", "300")))
|
|
17
|
+
except Exception:
|
|
18
|
+
return 300
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IntervalTicker:
|
|
22
|
+
"""Run an async tick coroutine on a fixed interval until a stop event fires.
|
|
23
|
+
|
|
24
|
+
Optionally backs off the interval on consecutive idle ticks (``is_active``)
|
|
25
|
+
so a low-traffic ticker doesn't keep waking the process on a fixed cadence
|
|
26
|
+
forever -- the dominant reason a single-user local CLI session never lets
|
|
27
|
+
the OS scheduler (and on a laptop, App Nap) go idle. Backoff is strictly
|
|
28
|
+
opt-in: omit ``is_active`` (every ticker built before this was added) and
|
|
29
|
+
the loop is byte-identical to the original fixed-interval implementation.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
tick_coro: Callable[[], Awaitable[Any]],
|
|
35
|
+
interval_seconds: int,
|
|
36
|
+
*,
|
|
37
|
+
is_active: Optional[Callable[[Any], bool]] = None,
|
|
38
|
+
max_interval_seconds: Optional[int] = None,
|
|
39
|
+
backoff_factor: float = 2.0,
|
|
40
|
+
) -> None:
|
|
41
|
+
self.tick_coro = tick_coro
|
|
42
|
+
self.interval_seconds = interval_seconds
|
|
43
|
+
self._is_active = is_active
|
|
44
|
+
self._max_interval = max_interval_seconds or interval_seconds
|
|
45
|
+
self._backoff_factor = backoff_factor
|
|
46
|
+
self._current_interval = interval_seconds
|
|
47
|
+
|
|
48
|
+
async def run_forever(self, stop_event: Optional[asyncio.Event] = None) -> None:
|
|
49
|
+
while not (stop_event is not None and stop_event.is_set()):
|
|
50
|
+
active = True
|
|
51
|
+
try:
|
|
52
|
+
result = await self.tick_coro()
|
|
53
|
+
if self._is_active is not None:
|
|
54
|
+
active = bool(self._is_active(result))
|
|
55
|
+
except Exception as e: # a tick must never kill the loop
|
|
56
|
+
logger.error("ticker tick failed: %s", e, exc_info=True)
|
|
57
|
+
active = True # never back off after an error -- retry promptly
|
|
58
|
+
|
|
59
|
+
if self._is_active is not None:
|
|
60
|
+
if active:
|
|
61
|
+
self._current_interval = self.interval_seconds
|
|
62
|
+
else:
|
|
63
|
+
self._current_interval = min(
|
|
64
|
+
self._max_interval,
|
|
65
|
+
max(self.interval_seconds, self._current_interval * self._backoff_factor),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
wait_s = self._current_interval
|
|
69
|
+
try:
|
|
70
|
+
await asyncio.wait_for(
|
|
71
|
+
stop_event.wait() if stop_event else asyncio.sleep(wait_s),
|
|
72
|
+
timeout=wait_s,
|
|
73
|
+
)
|
|
74
|
+
except asyncio.TimeoutError:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TickerSupervisor:
|
|
79
|
+
"""Manage a collection of named ticker tasks for API lifespan use.
|
|
80
|
+
|
|
81
|
+
Accepts any object with a ``run_forever(stop_event)`` coroutine method —
|
|
82
|
+
:class:`IntervalTicker` instances or existing ticker classes (CronTicker,
|
|
83
|
+
GoalTicker, CuratorTicker) that delegate their own ``run_forever`` here.
|
|
84
|
+
|
|
85
|
+
Usage::
|
|
86
|
+
|
|
87
|
+
sup = TickerSupervisor()
|
|
88
|
+
sup.register("cron", cron_ticker, enabled=cron_enabled())
|
|
89
|
+
await sup.start_all() # on startup
|
|
90
|
+
...
|
|
91
|
+
await sup.stop_all() # on shutdown (in finally)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self) -> None:
|
|
95
|
+
self._entries: list = [] # [(name, ticker, enabled)]
|
|
96
|
+
self._tasks: dict = {} # name -> (task, stop_event)
|
|
97
|
+
self._hb_task: Optional[asyncio.Task] = None
|
|
98
|
+
self._hb_stop: Optional[asyncio.Event] = None
|
|
99
|
+
|
|
100
|
+
def register(self, name: str, ticker: Any, enabled: bool) -> None:
|
|
101
|
+
"""Register a ticker. Only enabled tickers are started in :meth:`start_all`."""
|
|
102
|
+
self._entries.append((name, ticker, enabled))
|
|
103
|
+
|
|
104
|
+
def _emit_heartbeats(self) -> None:
|
|
105
|
+
"""Record one autonomy_tick per running ticker (liveness signal).
|
|
106
|
+
|
|
107
|
+
The audit (2026-07-04) found NO automated 'is the loop alive' signal — ops
|
|
108
|
+
relied on a manual human tick-log. This makes idle-but-alive observable, and
|
|
109
|
+
flags a ticker whose task has died. Fail-open; lazy import keeps core clean.
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
from agents.task.telemetry.event_log import get_event_log, event_log_enabled
|
|
113
|
+
if not event_log_enabled():
|
|
114
|
+
return
|
|
115
|
+
log = get_event_log()
|
|
116
|
+
for name, (task, _stop) in self._tasks.items():
|
|
117
|
+
alive = not task.done()
|
|
118
|
+
log.record("autonomy_tick", source="supervisor", loop=name,
|
|
119
|
+
alive=alive, reason=None if alive else "task_exited")
|
|
120
|
+
# Keep the event log bounded (retention discipline; fail-open).
|
|
121
|
+
try:
|
|
122
|
+
import time as _t
|
|
123
|
+
days = max(1, int(os.getenv("TELEMETRY_EVENT_LOG_RETENTION_DAYS", "30")))
|
|
124
|
+
log.prune(older_than_ts=_t.time() - days * 86400)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
async def _heartbeat_loop(self) -> None:
|
|
131
|
+
interval = _heartbeat_interval_sec()
|
|
132
|
+
while not (self._hb_stop is not None and self._hb_stop.is_set()):
|
|
133
|
+
try:
|
|
134
|
+
await asyncio.wait_for(self._hb_stop.wait(), timeout=interval)
|
|
135
|
+
except asyncio.TimeoutError:
|
|
136
|
+
pass
|
|
137
|
+
if self._hb_stop is not None and self._hb_stop.is_set():
|
|
138
|
+
break
|
|
139
|
+
self._emit_heartbeats()
|
|
140
|
+
|
|
141
|
+
async def start_all(self) -> None:
|
|
142
|
+
"""Start all enabled tickers as background asyncio tasks."""
|
|
143
|
+
for name, ticker, enabled in self._entries:
|
|
144
|
+
if not enabled:
|
|
145
|
+
continue
|
|
146
|
+
stop = asyncio.Event()
|
|
147
|
+
task = asyncio.create_task(ticker.run_forever(stop_event=stop))
|
|
148
|
+
self._tasks[name] = (task, stop)
|
|
149
|
+
logger.info("✅ %s ticker started", name)
|
|
150
|
+
# Liveness heartbeat (only if something is actually running).
|
|
151
|
+
if self._tasks:
|
|
152
|
+
self._hb_stop = asyncio.Event()
|
|
153
|
+
self._hb_task = asyncio.create_task(self._heartbeat_loop())
|
|
154
|
+
|
|
155
|
+
async def stop_all(self) -> None:
|
|
156
|
+
"""Signal stop and cancel all running tickers, awaiting their completion."""
|
|
157
|
+
if self._hb_stop is not None:
|
|
158
|
+
self._hb_stop.set()
|
|
159
|
+
if self._hb_task is not None:
|
|
160
|
+
self._hb_task.cancel()
|
|
161
|
+
try:
|
|
162
|
+
await self._hb_task
|
|
163
|
+
except (asyncio.CancelledError, Exception):
|
|
164
|
+
pass
|
|
165
|
+
self._hb_task = None
|
|
166
|
+
for name, (task, stop) in self._tasks.items():
|
|
167
|
+
stop.set()
|
|
168
|
+
task.cancel()
|
|
169
|
+
try:
|
|
170
|
+
await task
|
|
171
|
+
except (asyncio.CancelledError, Exception):
|
|
172
|
+
pass
|
|
173
|
+
logger.info("%s ticker stopped", name)
|
|
174
|
+
self._tasks.clear()
|