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
agents/task/utils.py
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for the task package.
|
|
3
|
+
|
|
4
|
+
Note: All session directory and path management is now handled through
|
|
5
|
+
the centralized path management system in agents.task.path:
|
|
6
|
+
|
|
7
|
+
from agents.task.path import pm
|
|
8
|
+
|
|
9
|
+
# Examples:
|
|
10
|
+
clean_id = pm().clean_session_id(session_id)
|
|
11
|
+
workspace_dir = pm().get_workspace_dir(session_id)
|
|
12
|
+
file_path = pm().create_file_path(session_id, "workspace", "output.json")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import time
|
|
17
|
+
import json
|
|
18
|
+
from typing import Any, Callable, Coroutine, Dict, Optional, ParamSpec, TypeVar, Union, Generic, List
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
import threading
|
|
22
|
+
import warnings
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
def extract_session_from_agent_id(agent_id: str) -> Optional[str]:
|
|
27
|
+
"""Extract session ID from agent_id format: name_sessionid
|
|
28
|
+
|
|
29
|
+
This utility consolidates duplicate logic found in 5+ places across the codebase.
|
|
30
|
+
Agent IDs follow the format "agent_name_session_id" where the session ID is
|
|
31
|
+
everything after the first underscore.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
agent_id: Agent identifier in format "name_sessionid"
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Cleaned session ID or None if agent_id is invalid
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
>>> extract_session_from_agent_id("agent_abc123")
|
|
41
|
+
"abc123"
|
|
42
|
+
>>> extract_session_from_agent_id("task_user_session_xyz")
|
|
43
|
+
"user_session_xyz"
|
|
44
|
+
>>> extract_session_from_agent_id("invalid")
|
|
45
|
+
None
|
|
46
|
+
"""
|
|
47
|
+
from agents.task.path import pm
|
|
48
|
+
|
|
49
|
+
if not agent_id or '_' not in agent_id:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
# Split only on first underscore to preserve underscores in session_id
|
|
53
|
+
session_id = agent_id.split('_', 1)[1]
|
|
54
|
+
return pm().clean_session_id(session_id)
|
|
55
|
+
|
|
56
|
+
def fix_schema_for_provider(schema: Dict[str, Any], provider: str) -> Dict[str, Any]:
|
|
57
|
+
"""Enforce additionalProperties: false recursively for LLM provider compatibility.
|
|
58
|
+
|
|
59
|
+
Both OpenAI and Anthropic require all object-typed nodes to carry
|
|
60
|
+
``additionalProperties: false``; the only exception is when it is explicitly
|
|
61
|
+
set to ``True`` (indicating a ``Dict[str, Any]`` that must accept arbitrary keys,
|
|
62
|
+
e.g. MCP tool arguments). All other providers currently get the same treatment.
|
|
63
|
+
|
|
64
|
+
Mutates *schema* in place and also returns it (matches the prior contract of
|
|
65
|
+
fix_openai_schema / fix_anthropic_schema so existing callers are unaffected).
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
schema: JSON schema fragment to fix (mutated in place).
|
|
69
|
+
provider: Provider name string (e.g. "openai", "anthropic"). Currently
|
|
70
|
+
unused because both providers need the same normalisation, but kept for
|
|
71
|
+
future per-provider branching without changing the call sites.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The same schema dict, normalised.
|
|
75
|
+
"""
|
|
76
|
+
if isinstance(schema, dict):
|
|
77
|
+
# Enforce additionalProperties: false on object-typed nodes, UNLESS the
|
|
78
|
+
# caller explicitly opted in to True (Dict[str, Any] / open MCP args).
|
|
79
|
+
if schema.get('type') == 'object':
|
|
80
|
+
if schema.get('additionalProperties') is not True:
|
|
81
|
+
schema['additionalProperties'] = False
|
|
82
|
+
|
|
83
|
+
# Recurse into all nested schema locations.
|
|
84
|
+
for key, value in schema.items():
|
|
85
|
+
if key == 'properties' and isinstance(value, dict):
|
|
86
|
+
for prop_name, prop_schema in value.items():
|
|
87
|
+
schema['properties'][prop_name] = fix_schema_for_provider(prop_schema, provider)
|
|
88
|
+
elif key == 'items' and isinstance(value, dict):
|
|
89
|
+
schema['items'] = fix_schema_for_provider(value, provider)
|
|
90
|
+
elif key == 'definitions' and isinstance(value, dict):
|
|
91
|
+
for def_name, def_schema in value.items():
|
|
92
|
+
schema['definitions'][def_name] = fix_schema_for_provider(def_schema, provider)
|
|
93
|
+
elif key == '$defs' and isinstance(value, dict):
|
|
94
|
+
for def_name, def_schema in value.items():
|
|
95
|
+
schema['$defs'][def_name] = fix_schema_for_provider(def_schema, provider)
|
|
96
|
+
elif key == 'anyOf' and isinstance(value, list):
|
|
97
|
+
schema['anyOf'] = [fix_schema_for_provider(s, provider) for s in value]
|
|
98
|
+
elif key == 'oneOf' and isinstance(value, list):
|
|
99
|
+
schema['oneOf'] = [fix_schema_for_provider(s, provider) for s in value]
|
|
100
|
+
elif key == 'allOf' and isinstance(value, list):
|
|
101
|
+
schema['allOf'] = [fix_schema_for_provider(s, provider) for s in value]
|
|
102
|
+
elif isinstance(value, dict):
|
|
103
|
+
schema[key] = fix_schema_for_provider(value, provider)
|
|
104
|
+
|
|
105
|
+
return schema
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def fix_openai_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
109
|
+
"""Backward-compat wrapper — delegates to fix_schema_for_provider."""
|
|
110
|
+
return fix_schema_for_provider(schema, "openai")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def fix_anthropic_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
114
|
+
"""Backward-compat wrapper — delegates to fix_schema_for_provider."""
|
|
115
|
+
return fix_schema_for_provider(schema, "anthropic")
|
|
116
|
+
|
|
117
|
+
def detect_llm_provider(result: Any, model_name: Optional[str] = None) -> str:
|
|
118
|
+
"""Detect LLM provider from model name using the model_registry.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
result: LLM response object (not used, kept for compatibility)
|
|
122
|
+
model_name: Model name for detection
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Provider name ('openai', 'anthropic', 'gemini', 'deepseek', 'openrouter', 'llama', or 'generic')
|
|
126
|
+
"""
|
|
127
|
+
if not model_name:
|
|
128
|
+
return 'generic'
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
# Use model_registry as the SINGLE SOURCE OF TRUTH (WS-2.3): the canonical
|
|
132
|
+
# enum→string map lives there, so a new provider is added in one place and
|
|
133
|
+
# GOOGLE→'gemini' can never drift back to 'google' here.
|
|
134
|
+
from modules.llm.model_registry import get_model_config, canonical_provider_name
|
|
135
|
+
|
|
136
|
+
model_config = get_model_config(model_name)
|
|
137
|
+
if model_config and model_config.provider:
|
|
138
|
+
return canonical_provider_name(model_config.provider, default='generic')
|
|
139
|
+
|
|
140
|
+
except ImportError:
|
|
141
|
+
logger.debug("model_registry not available for provider detection")
|
|
142
|
+
|
|
143
|
+
return 'generic'
|
|
144
|
+
|
|
145
|
+
def extract_token_usage(result: Any, provider: str) -> Dict[str, Optional[int]]:
|
|
146
|
+
"""Extract token usage from LLM response using provider-specific paths.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
result: LLM response object (could be raw LLM response or structured output)
|
|
150
|
+
provider: Provider name from detect_llm_provider
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Dictionary with token counts (total_tokens, prompt_tokens, completion_tokens, cached_tokens)
|
|
154
|
+
"""
|
|
155
|
+
token_usage = {'total_tokens': None, 'prompt_tokens': None, 'completion_tokens': None, 'cached_tokens': None, 'cache_creation_tokens': None}
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
# Handle structured output format: {'parsed': ..., 'raw': <llm_response>}
|
|
159
|
+
actual_result = result
|
|
160
|
+
if isinstance(result, dict) and 'raw' in result:
|
|
161
|
+
# Structured output - extract the raw LLM response
|
|
162
|
+
actual_result = result['raw']
|
|
163
|
+
|
|
164
|
+
# Try AIMessage format first (usage_metadata attribute)
|
|
165
|
+
if hasattr(actual_result, 'usage_metadata') and actual_result.usage_metadata:
|
|
166
|
+
usage = actual_result.usage_metadata
|
|
167
|
+
# Handle both dict and object formats (adapters may pass dict or object)
|
|
168
|
+
if isinstance(usage, dict):
|
|
169
|
+
# Dict format (from our adapters)
|
|
170
|
+
token_usage['prompt_tokens'] = (
|
|
171
|
+
usage.get('input_tokens') or
|
|
172
|
+
usage.get('prompt_tokens')
|
|
173
|
+
)
|
|
174
|
+
token_usage['completion_tokens'] = (
|
|
175
|
+
usage.get('output_tokens') or
|
|
176
|
+
usage.get('completion_tokens')
|
|
177
|
+
)
|
|
178
|
+
token_usage['total_tokens'] = usage.get('total_tokens')
|
|
179
|
+
token_usage['cached_tokens'] = usage.get('cache_read_input_tokens') or usage.get('cached_tokens')
|
|
180
|
+
token_usage['cache_creation_tokens'] = usage.get('cache_creation_input_tokens')
|
|
181
|
+
else:
|
|
182
|
+
# Object format
|
|
183
|
+
token_usage['prompt_tokens'] = (
|
|
184
|
+
getattr(usage, 'input_tokens', None) or
|
|
185
|
+
getattr(usage, 'prompt_tokens', None)
|
|
186
|
+
)
|
|
187
|
+
token_usage['completion_tokens'] = (
|
|
188
|
+
getattr(usage, 'output_tokens', None) or
|
|
189
|
+
getattr(usage, 'completion_tokens', None)
|
|
190
|
+
)
|
|
191
|
+
token_usage['cached_tokens'] = getattr(usage, 'cache_read_input_tokens', None)
|
|
192
|
+
# Calculate total if not already set
|
|
193
|
+
if token_usage['total_tokens'] is None and token_usage['prompt_tokens'] and token_usage['completion_tokens']:
|
|
194
|
+
token_usage['total_tokens'] = token_usage['prompt_tokens'] + token_usage['completion_tokens']
|
|
195
|
+
|
|
196
|
+
# Try response_metadata format
|
|
197
|
+
elif hasattr(actual_result, 'response_metadata'):
|
|
198
|
+
metadata = actual_result.response_metadata
|
|
199
|
+
if 'token_usage' in metadata:
|
|
200
|
+
usage = metadata['token_usage']
|
|
201
|
+
token_usage['prompt_tokens'] = usage.get('prompt_tokens')
|
|
202
|
+
token_usage['completion_tokens'] = usage.get('completion_tokens')
|
|
203
|
+
token_usage['total_tokens'] = usage.get('total_tokens')
|
|
204
|
+
token_usage['cached_tokens'] = usage.get('cached_tokens')
|
|
205
|
+
|
|
206
|
+
# Direct attribute access based on known provider patterns
|
|
207
|
+
elif provider == 'openai':
|
|
208
|
+
# OpenAI format: result.usage.{total_tokens, prompt_tokens, completion_tokens}
|
|
209
|
+
if hasattr(actual_result, 'usage') and actual_result.usage:
|
|
210
|
+
usage = actual_result.usage
|
|
211
|
+
token_usage['total_tokens'] = getattr(usage, 'total_tokens', None)
|
|
212
|
+
token_usage['prompt_tokens'] = getattr(usage, 'prompt_tokens', None)
|
|
213
|
+
token_usage['completion_tokens'] = getattr(usage, 'completion_tokens', None)
|
|
214
|
+
token_usage['cached_tokens'] = getattr(usage, 'cached_tokens', None)
|
|
215
|
+
|
|
216
|
+
elif provider == 'anthropic':
|
|
217
|
+
# Anthropic format: result.usage.{input_tokens, output_tokens}
|
|
218
|
+
if hasattr(actual_result, 'usage') and actual_result.usage:
|
|
219
|
+
usage = actual_result.usage
|
|
220
|
+
token_usage['prompt_tokens'] = getattr(usage, 'input_tokens', None)
|
|
221
|
+
token_usage['completion_tokens'] = getattr(usage, 'output_tokens', None)
|
|
222
|
+
token_usage['cached_tokens'] = getattr(usage, 'cache_read_input_tokens', None)
|
|
223
|
+
# Sometimes Anthropic puts these directly on result
|
|
224
|
+
elif hasattr(actual_result, 'input_tokens'):
|
|
225
|
+
token_usage['prompt_tokens'] = getattr(actual_result, 'input_tokens', None)
|
|
226
|
+
token_usage['completion_tokens'] = getattr(actual_result, 'output_tokens', None)
|
|
227
|
+
|
|
228
|
+
else:
|
|
229
|
+
# Generic fallback - try multiple common patterns
|
|
230
|
+
if hasattr(actual_result, 'usage') and actual_result.usage:
|
|
231
|
+
usage = actual_result.usage
|
|
232
|
+
# Try OpenAI pattern first
|
|
233
|
+
token_usage['total_tokens'] = getattr(usage, 'total_tokens', None)
|
|
234
|
+
token_usage['prompt_tokens'] = getattr(usage, 'prompt_tokens', None)
|
|
235
|
+
token_usage['completion_tokens'] = getattr(usage, 'completion_tokens', None)
|
|
236
|
+
token_usage['cached_tokens'] = getattr(usage, 'cached_tokens', None) or getattr(usage, 'cache_read_input_tokens', None)
|
|
237
|
+
# Try Anthropic pattern if OpenAI didn't work
|
|
238
|
+
if not token_usage['prompt_tokens']:
|
|
239
|
+
token_usage['prompt_tokens'] = getattr(usage, 'input_tokens', None)
|
|
240
|
+
if not token_usage['completion_tokens']:
|
|
241
|
+
token_usage['completion_tokens'] = getattr(usage, 'output_tokens', None)
|
|
242
|
+
|
|
243
|
+
# Calculate total if we have prompt + completion but no total
|
|
244
|
+
if (token_usage['total_tokens'] is None and
|
|
245
|
+
token_usage['prompt_tokens'] is not None and
|
|
246
|
+
token_usage['completion_tokens'] is not None):
|
|
247
|
+
token_usage['total_tokens'] = token_usage['prompt_tokens'] + token_usage['completion_tokens']
|
|
248
|
+
|
|
249
|
+
# Ensure all values are proper integers or None
|
|
250
|
+
for key in token_usage:
|
|
251
|
+
if token_usage[key] is not None:
|
|
252
|
+
try:
|
|
253
|
+
token_usage[key] = int(token_usage[key])
|
|
254
|
+
except (ValueError, TypeError):
|
|
255
|
+
token_usage[key] = None
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
logging.getLogger('task.utils').debug(f"Error extracting token usage: {e}")
|
|
259
|
+
|
|
260
|
+
return token_usage
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# Define generic type variables for return type and parameters
|
|
264
|
+
R = TypeVar('R')
|
|
265
|
+
P = ParamSpec('P')
|
|
266
|
+
T = TypeVar('T')
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def handle_with_fallback(
|
|
270
|
+
primary_func: Callable[[], T],
|
|
271
|
+
fallback_func: Callable[[], T],
|
|
272
|
+
logger: logging.Logger,
|
|
273
|
+
error_msg: str
|
|
274
|
+
) -> T:
|
|
275
|
+
"""Standard error handling with fallback.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
primary_func: Primary function to try first
|
|
279
|
+
fallback_func: Fallback function to use if primary fails
|
|
280
|
+
logger: Logger to use for error messages
|
|
281
|
+
error_msg: Message to log on error
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Result from either primary or fallback function
|
|
285
|
+
"""
|
|
286
|
+
try:
|
|
287
|
+
return primary_func()
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.warning(f"{error_msg}: {e}")
|
|
290
|
+
return fallback_func()
|
|
291
|
+
|
|
292
|
+
def safe_operation(
|
|
293
|
+
func: Callable[[], T],
|
|
294
|
+
logger: logging.Logger,
|
|
295
|
+
error_msg: str,
|
|
296
|
+
default_value: Optional[T] = None
|
|
297
|
+
) -> Optional[T]:
|
|
298
|
+
"""Execute operation safely, returning default value on failure.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
func: Function to execute
|
|
302
|
+
logger: Logger to use for error messages
|
|
303
|
+
error_msg: Message to log on error
|
|
304
|
+
default_value: Value to return if function fails
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Result from function or default value on error
|
|
308
|
+
"""
|
|
309
|
+
try:
|
|
310
|
+
return func()
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"{error_msg}: {e}")
|
|
313
|
+
return default_value
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# Timing decorators have been moved to utils/time_utils.py
|
|
317
|
+
# Import from there for backwards compatibility
|
|
318
|
+
from utils.time_utils import time_execution_sync, time_execution_async
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def log_llm_request(component: str, purpose: Optional[str] = None) -> Callable:
|
|
322
|
+
"""
|
|
323
|
+
Decorator to log and track LLM requests with telemetry.
|
|
324
|
+
|
|
325
|
+
This decorator provides consistent logging and telemetry capture for LLM operations.
|
|
326
|
+
Used primarily in tests and theatre scripts for tracking.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
component: Component name making the request
|
|
330
|
+
purpose: Purpose of the request (optional)
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Decorated function with logging and telemetry
|
|
334
|
+
"""
|
|
335
|
+
import asyncio
|
|
336
|
+
import time
|
|
337
|
+
import logging
|
|
338
|
+
from functools import wraps
|
|
339
|
+
|
|
340
|
+
logger = logging.getLogger(__name__)
|
|
341
|
+
|
|
342
|
+
def decorator(func: Callable) -> Callable:
|
|
343
|
+
@wraps(func)
|
|
344
|
+
async def async_wrapper(*args, **kwargs):
|
|
345
|
+
start_time = time.time()
|
|
346
|
+
|
|
347
|
+
# Extract LLM info
|
|
348
|
+
llm = kwargs.get('llm') or (args[1] if len(args) > 1 else None)
|
|
349
|
+
agent = args[0] if args else None
|
|
350
|
+
|
|
351
|
+
model_name = getattr(llm, 'model_name', 'unknown') if llm else 'unknown'
|
|
352
|
+
|
|
353
|
+
# Log the request
|
|
354
|
+
purpose_str = f"/{purpose}" if purpose else ""
|
|
355
|
+
logger.info(f"🔄 LLM REQUEST [{component}{purpose_str}] - Model: {model_name}")
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
# Execute the function
|
|
359
|
+
result = await func(*args, **kwargs)
|
|
360
|
+
|
|
361
|
+
# Extract token usage if available
|
|
362
|
+
if result and isinstance(result, dict):
|
|
363
|
+
raw = result.get('raw')
|
|
364
|
+
if raw and hasattr(raw, 'usage'):
|
|
365
|
+
usage = raw.usage
|
|
366
|
+
if usage:
|
|
367
|
+
prompt_tokens = getattr(usage, 'prompt_tokens', 0)
|
|
368
|
+
completion_tokens = getattr(usage, 'completion_tokens', 0)
|
|
369
|
+
total_tokens = getattr(usage, 'total_tokens', 0)
|
|
370
|
+
|
|
371
|
+
duration = time.time() - start_time
|
|
372
|
+
logger.info(f"Token usage - Prompt: {prompt_tokens}, Completion: {completion_tokens}, Total: {total_tokens}")
|
|
373
|
+
logger.info(f"Completed in {duration:.2f}s")
|
|
374
|
+
|
|
375
|
+
# Capture telemetry
|
|
376
|
+
try:
|
|
377
|
+
from agents.task import capture_llm_request
|
|
378
|
+
session_id = getattr(agent, 'agent_id', None) if agent else None
|
|
379
|
+
|
|
380
|
+
capture_llm_request(
|
|
381
|
+
component=component,
|
|
382
|
+
purpose=purpose,
|
|
383
|
+
model_name=model_name,
|
|
384
|
+
duration_seconds=duration,
|
|
385
|
+
success=True,
|
|
386
|
+
prompt_tokens=prompt_tokens,
|
|
387
|
+
completion_tokens=completion_tokens,
|
|
388
|
+
total_tokens=total_tokens,
|
|
389
|
+
session_id=session_id,
|
|
390
|
+
temperature=getattr(llm, 'temperature', None) if llm else None,
|
|
391
|
+
max_tokens=getattr(llm, 'max_tokens', None) if llm else None
|
|
392
|
+
)
|
|
393
|
+
except Exception as e:
|
|
394
|
+
logger.debug(f"Failed to capture telemetry: {e}")
|
|
395
|
+
|
|
396
|
+
return result
|
|
397
|
+
|
|
398
|
+
except Exception as e:
|
|
399
|
+
duration = time.time() - start_time
|
|
400
|
+
logger.error(f"LLM request failed after {duration:.2f}s: {e}")
|
|
401
|
+
|
|
402
|
+
# Capture telemetry for failure
|
|
403
|
+
try:
|
|
404
|
+
from agents.task import capture_llm_request
|
|
405
|
+
session_id = getattr(agent, 'agent_id', None) if agent else None
|
|
406
|
+
|
|
407
|
+
capture_llm_request(
|
|
408
|
+
component=component,
|
|
409
|
+
purpose=purpose,
|
|
410
|
+
model_name=model_name,
|
|
411
|
+
duration_seconds=duration,
|
|
412
|
+
success=False,
|
|
413
|
+
session_id=session_id
|
|
414
|
+
)
|
|
415
|
+
except Exception:
|
|
416
|
+
pass
|
|
417
|
+
|
|
418
|
+
raise
|
|
419
|
+
|
|
420
|
+
@wraps(func)
|
|
421
|
+
def sync_wrapper(*args, **kwargs):
|
|
422
|
+
# For sync functions, just pass through (tests mostly use async)
|
|
423
|
+
return func(*args, **kwargs)
|
|
424
|
+
|
|
425
|
+
# Return appropriate wrapper based on function type
|
|
426
|
+
if asyncio.iscoroutinefunction(func):
|
|
427
|
+
return async_wrapper
|
|
428
|
+
else:
|
|
429
|
+
return sync_wrapper
|
|
430
|
+
|
|
431
|
+
return decorator
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# get_safe_singleton removed - use get_safe_singleton from agents.task.path instead
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def save_registry_items(items: Dict[str, Any], directory: Path, item_type: str = "item") -> None:
|
|
438
|
+
"""
|
|
439
|
+
Save registry items (profiles, scenarios, etc.) to disk.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
items: Dictionary of items to save (id -> model)
|
|
443
|
+
directory: Directory to save items to
|
|
444
|
+
item_type: Type of item for logging messages
|
|
445
|
+
"""
|
|
446
|
+
import json
|
|
447
|
+
import logging
|
|
448
|
+
logger = logging.getLogger(__name__)
|
|
449
|
+
|
|
450
|
+
for item_id, item in items.items():
|
|
451
|
+
item_file = directory / f"{item_id}.json"
|
|
452
|
+
try:
|
|
453
|
+
with open(item_file, 'w') as f:
|
|
454
|
+
# Handle both Pydantic models and dicts
|
|
455
|
+
data = item.model_dump() if hasattr(item, 'model_dump') else item
|
|
456
|
+
json.dump(data, f, indent=2)
|
|
457
|
+
logger.info(f"Saved default {item_type} '{item_id}' to {item_file}")
|
|
458
|
+
except Exception as e:
|
|
459
|
+
logger.error(f"Failed to save {item_type} '{item_id}': {e}")
|
|
460
|
+
|
|
461
|
+
def send_telemetry(event: Any, agent_id: Optional[str] = None, session_id: Optional[str] = None, user_id: Optional[str] = None) -> bool:
|
|
462
|
+
"""
|
|
463
|
+
Send telemetry with consistent session/agent ID handling.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
event: The telemetry event to send
|
|
467
|
+
agent_id: Optional agent ID for tracking
|
|
468
|
+
session_id: Optional session ID for tracking
|
|
469
|
+
user_id: Optional user ID for multi-tenant tracking
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
True if telemetry was sent successfully, False otherwise
|
|
473
|
+
"""
|
|
474
|
+
# Don't send telemetry if disabled in environment
|
|
475
|
+
if os.environ.get('ANONYMIZED_TELEMETRY', 'true').lower() != 'true':
|
|
476
|
+
return False
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
# Use the get_telemetry function instead of directly importing ProductTelemetry
|
|
480
|
+
from agents.task.telemetry import get_telemetry
|
|
481
|
+
# Import path manager for clean_session_id
|
|
482
|
+
from agents.task.path import pm
|
|
483
|
+
|
|
484
|
+
# Try to get correct ID to use for tracking
|
|
485
|
+
tracking_id = None
|
|
486
|
+
|
|
487
|
+
# Use session_id as first choice if provided
|
|
488
|
+
if session_id:
|
|
489
|
+
tracking_id = pm().clean_session_id(session_id)
|
|
490
|
+
# Use agent_id as fallback, extract session ID from it if possible
|
|
491
|
+
elif agent_id:
|
|
492
|
+
tracking_id = extract_session_from_agent_id(agent_id) or pm().clean_session_id(agent_id)
|
|
493
|
+
|
|
494
|
+
# Send telemetry with proper tracking ID. NOTE: ProductTelemetry.capture()
|
|
495
|
+
# takes only (event, session_id) — telemetry is tenant-scoped by the
|
|
496
|
+
# session's on-disk path, not a user_id arg. Passing user_id= raised a
|
|
497
|
+
# swallowed TypeError, so this always returned False (audit 2026-07-04).
|
|
498
|
+
telemetry = get_telemetry()
|
|
499
|
+
telemetry.capture(event, session_id=tracking_id)
|
|
500
|
+
return True
|
|
501
|
+
except Exception as e:
|
|
502
|
+
# Silent fail for telemetry to avoid affecting main operation
|
|
503
|
+
logger.debug(f"Failed to send telemetry: {e}")
|
|
504
|
+
return False
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# GIF creation utilities have been moved to utils/gif_utils.py
|
|
508
|
+
# For backwards compatibility, import them here
|
|
509
|
+
from utils.gif_utils import create_gif_with_retry, create_text_only_gif as _create_text_only_gif
|
|
510
|
+
|
|
511
|
+
# DEPRECATED FUNCTIONS REMOVED - Use modules.llm.llm_factory instead
|
|
512
|
+
# These functions were causing conflicts with the main LLM package and only supported OpenAI
|
|
513
|
+
# Legacy LLM factory functions have been completely removed
|
|
514
|
+
# Use the centralized LLM factory: modules.llm.llm_factory.create_chat_model
|
|
515
|
+
|
|
516
|
+
# Add the unified file locking implementation after the singleton decorator code
|
|
517
|
+
|
|
518
|
+
class SafeFileLock:
|
|
519
|
+
"""
|
|
520
|
+
Cross-platform file locking implementation with fallback mechanism.
|
|
521
|
+
|
|
522
|
+
This provides a unified locking mechanism that works across:
|
|
523
|
+
1. Linux/macOS (using filelock package if available)
|
|
524
|
+
2. Windows (using filelock package if available)
|
|
525
|
+
3. Any platform (using a simple lock file as fallback)
|
|
526
|
+
|
|
527
|
+
Usage:
|
|
528
|
+
with SafeFileLock('/path/to/file.lock'):
|
|
529
|
+
# Perform operations requiring exclusive access
|
|
530
|
+
"""
|
|
531
|
+
__slots__ = ('lock_file', 'timeout', 'poll_interval', '_lock', '_locked', '_use_filelock', '_filelock')
|
|
532
|
+
|
|
533
|
+
# FIXED: Track acquired locks with bounded cache and proper cleanup
|
|
534
|
+
_active_locks = {} # Maps lock file paths to lock counters
|
|
535
|
+
_REGISTRY_LOCK = threading.RLock() # Lock for modifying _active_locks
|
|
536
|
+
_last_cleanup = 0 # Track last cleanup time
|
|
537
|
+
_MAX_CACHE_SIZE = 1000 # FIXED: Limit cache size to prevent memory leaks
|
|
538
|
+
_CLEANUP_INTERVAL = 60 # FIX 9: Reduced from 300 to 60 seconds for more aggressive cleanup
|
|
539
|
+
_STALE_LOCK_AGE = 300 # FIX 9: Consider locks stale after 5 minutes (was 3600/1 hour)
|
|
540
|
+
|
|
541
|
+
def __init__(self, lock_file: str, timeout: float = 60.0, poll_interval: float = 0.1):
|
|
542
|
+
"""Initialize the SafeFileLock.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
lock_file: Path to the lock file
|
|
546
|
+
timeout: Maximum time to wait for lock acquisition
|
|
547
|
+
poll_interval: Time between lock acquisition attempts
|
|
548
|
+
"""
|
|
549
|
+
self.lock_file = os.path.abspath(lock_file)
|
|
550
|
+
self.timeout = timeout
|
|
551
|
+
self.poll_interval = poll_interval
|
|
552
|
+
self._lock = None
|
|
553
|
+
self._locked = False
|
|
554
|
+
self._use_filelock = False
|
|
555
|
+
self._filelock = None
|
|
556
|
+
|
|
557
|
+
# FIX 9: Check if our specific lock file is stale before trying to acquire
|
|
558
|
+
if os.path.exists(self.lock_file):
|
|
559
|
+
try:
|
|
560
|
+
file_age = time.time() - os.path.getmtime(self.lock_file)
|
|
561
|
+
if file_age > self._STALE_LOCK_AGE:
|
|
562
|
+
# Try to remove our specific stale lock file
|
|
563
|
+
try:
|
|
564
|
+
os.unlink(self.lock_file)
|
|
565
|
+
logger.debug(f"Removed stale lock on init: {self.lock_file} (age: {file_age:.1f}s)")
|
|
566
|
+
except (OSError, PermissionError):
|
|
567
|
+
logger.debug(f"Could not remove stale lock on init: {self.lock_file}")
|
|
568
|
+
except (OSError, FileNotFoundError):
|
|
569
|
+
pass
|
|
570
|
+
|
|
571
|
+
# Try to use filelock package if available
|
|
572
|
+
try:
|
|
573
|
+
import filelock
|
|
574
|
+
self._filelock = filelock.FileLock(self.lock_file, timeout=timeout)
|
|
575
|
+
self._use_filelock = True
|
|
576
|
+
except ImportError:
|
|
577
|
+
# Fall back to simple file-based locking
|
|
578
|
+
pass
|
|
579
|
+
|
|
580
|
+
# FIXED: Trigger cleanup periodically to prevent memory leaks
|
|
581
|
+
self._maybe_cleanup_stale_locks()
|
|
582
|
+
|
|
583
|
+
@classmethod
|
|
584
|
+
def _maybe_cleanup_stale_locks(cls):
|
|
585
|
+
"""Cleanup stale locks and limit cache size to prevent memory leaks."""
|
|
586
|
+
current_time = time.time()
|
|
587
|
+
|
|
588
|
+
# Only cleanup every _CLEANUP_INTERVAL seconds
|
|
589
|
+
if current_time - cls._last_cleanup < cls._CLEANUP_INTERVAL:
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
with cls._REGISTRY_LOCK:
|
|
593
|
+
try:
|
|
594
|
+
# FIXED: Remove locks that are no longer held
|
|
595
|
+
stale_locks = []
|
|
596
|
+
for lock_path, count in list(cls._active_locks.items()):
|
|
597
|
+
# Remove locks with zero or negative count
|
|
598
|
+
if count <= 0:
|
|
599
|
+
stale_locks.append(lock_path)
|
|
600
|
+
continue
|
|
601
|
+
|
|
602
|
+
# FIX 9: More aggressive stale lock detection and cleanup
|
|
603
|
+
try:
|
|
604
|
+
if os.path.exists(lock_path):
|
|
605
|
+
# Check if lock file is older than _STALE_LOCK_AGE seconds
|
|
606
|
+
file_age = current_time - os.path.getmtime(lock_path)
|
|
607
|
+
if file_age > cls._STALE_LOCK_AGE: # FIX 9: Use configurable timeout
|
|
608
|
+
# Try to remove stale lock file
|
|
609
|
+
try:
|
|
610
|
+
os.unlink(lock_path)
|
|
611
|
+
stale_locks.append(lock_path)
|
|
612
|
+
logger.debug(f"Removed stale lock file {lock_path} (age: {file_age:.1f}s)")
|
|
613
|
+
except (OSError, PermissionError):
|
|
614
|
+
# Can't remove, but don't count it as active
|
|
615
|
+
logger.debug(f"Could not remove stale lock {lock_path}: permission denied")
|
|
616
|
+
pass
|
|
617
|
+
else:
|
|
618
|
+
# Lock file doesn't exist, remove from tracking
|
|
619
|
+
stale_locks.append(lock_path)
|
|
620
|
+
except (OSError, FileNotFoundError):
|
|
621
|
+
# File system error, remove from tracking
|
|
622
|
+
stale_locks.append(lock_path)
|
|
623
|
+
|
|
624
|
+
# Remove stale locks from tracking
|
|
625
|
+
for lock_path in stale_locks:
|
|
626
|
+
cls._active_locks.pop(lock_path, None)
|
|
627
|
+
|
|
628
|
+
# FIXED: Implement bounded cache - if cache is too large, remove oldest entries
|
|
629
|
+
if len(cls._active_locks) > cls._MAX_CACHE_SIZE:
|
|
630
|
+
# Sort by path and remove oldest (we don't have timestamps, so use lexicographic)
|
|
631
|
+
sorted_locks = sorted(cls._active_locks.items())
|
|
632
|
+
locks_to_remove = len(cls._active_locks) - (cls._MAX_CACHE_SIZE // 2) # Remove half
|
|
633
|
+
|
|
634
|
+
for i in range(locks_to_remove):
|
|
635
|
+
if i < len(sorted_locks):
|
|
636
|
+
lock_path, count = sorted_locks[i]
|
|
637
|
+
# Only remove if not currently held
|
|
638
|
+
if count <= 0:
|
|
639
|
+
cls._active_locks.pop(lock_path, None)
|
|
640
|
+
|
|
641
|
+
# Update last cleanup time
|
|
642
|
+
cls._last_cleanup = current_time
|
|
643
|
+
|
|
644
|
+
if stale_locks:
|
|
645
|
+
# Use basic logging to avoid circular imports
|
|
646
|
+
print(f"SafeFileLock: Cleaned up {len(stale_locks)} stale locks")
|
|
647
|
+
|
|
648
|
+
except Exception as e:
|
|
649
|
+
# Log cleanup errors but don't raise
|
|
650
|
+
print(f"SafeFileLock cleanup error: {e}")
|
|
651
|
+
|
|
652
|
+
def __enter__(self):
|
|
653
|
+
"""Acquire the lock."""
|
|
654
|
+
if self._use_filelock and self._filelock:
|
|
655
|
+
try:
|
|
656
|
+
self._filelock.acquire(timeout=self.timeout)
|
|
657
|
+
self._locked = True
|
|
658
|
+
|
|
659
|
+
# Track in registry
|
|
660
|
+
with self._REGISTRY_LOCK:
|
|
661
|
+
self._active_locks[self.lock_file] = self._active_locks.get(self.lock_file, 0) + 1
|
|
662
|
+
|
|
663
|
+
return self
|
|
664
|
+
except Exception as e:
|
|
665
|
+
# Fall back to manual locking if filelock fails
|
|
666
|
+
self._use_filelock = False
|
|
667
|
+
|
|
668
|
+
# Manual file-based locking. Attempt acquisition at least once regardless of
|
|
669
|
+
# timeout (do-while), so a FREE lock is acquired even with timeout=0; only a
|
|
670
|
+
# CONTENDED lock fails fast at timeout=0. (The old `while elapsed < timeout`
|
|
671
|
+
# never ran the body at timeout=0, so a free lock could never be acquired —
|
|
672
|
+
# which silently disabled the goal dispatcher / cron tick on machines without
|
|
673
|
+
# the `filelock` package, where this fallback path is taken.)
|
|
674
|
+
start_time = time.time()
|
|
675
|
+
|
|
676
|
+
while True:
|
|
677
|
+
try:
|
|
678
|
+
# FIXED: Use exclusive create mode to prevent race conditions
|
|
679
|
+
fd = os.open(self.lock_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
|
|
680
|
+
try:
|
|
681
|
+
# Write PID to lock file for debugging
|
|
682
|
+
os.write(fd, f"{os.getpid()}\n".encode())
|
|
683
|
+
finally:
|
|
684
|
+
os.close(fd)
|
|
685
|
+
|
|
686
|
+
self._locked = True
|
|
687
|
+
|
|
688
|
+
# Track in registry
|
|
689
|
+
with self._REGISTRY_LOCK:
|
|
690
|
+
self._active_locks[self.lock_file] = self._active_locks.get(self.lock_file, 0) + 1
|
|
691
|
+
|
|
692
|
+
return self
|
|
693
|
+
|
|
694
|
+
except FileExistsError:
|
|
695
|
+
# Lock file already exists; retry until the timeout budget is spent.
|
|
696
|
+
if time.time() - start_time >= self.timeout:
|
|
697
|
+
break
|
|
698
|
+
time.sleep(self.poll_interval)
|
|
699
|
+
continue
|
|
700
|
+
except OSError as e:
|
|
701
|
+
# Other file system error
|
|
702
|
+
raise RuntimeError(f"Failed to acquire lock {self.lock_file}: {e}")
|
|
703
|
+
|
|
704
|
+
# Timeout reached
|
|
705
|
+
raise TimeoutError(f"Failed to acquire lock {self.lock_file} within {self.timeout} seconds")
|
|
706
|
+
|
|
707
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
708
|
+
"""Release the lock."""
|
|
709
|
+
if not self._locked:
|
|
710
|
+
return
|
|
711
|
+
|
|
712
|
+
try:
|
|
713
|
+
if self._use_filelock and self._filelock:
|
|
714
|
+
self._filelock.release()
|
|
715
|
+
else:
|
|
716
|
+
# Manual lock release
|
|
717
|
+
try:
|
|
718
|
+
# FIXED: Verify we still own the lock before removing
|
|
719
|
+
if os.path.exists(self.lock_file):
|
|
720
|
+
# Check if we own this lock by reading PID
|
|
721
|
+
try:
|
|
722
|
+
with open(self.lock_file, 'r') as f:
|
|
723
|
+
lock_pid = f.read().strip()
|
|
724
|
+
if lock_pid == str(os.getpid()):
|
|
725
|
+
os.unlink(self.lock_file)
|
|
726
|
+
else:
|
|
727
|
+
# Lock was taken by another process, don't remove
|
|
728
|
+
pass
|
|
729
|
+
except (OSError, ValueError):
|
|
730
|
+
# Can't read lock file, try to remove anyway
|
|
731
|
+
try:
|
|
732
|
+
os.unlink(self.lock_file)
|
|
733
|
+
except (OSError, FileNotFoundError):
|
|
734
|
+
pass
|
|
735
|
+
except (OSError, FileNotFoundError):
|
|
736
|
+
# Lock file already removed or can't be removed
|
|
737
|
+
pass
|
|
738
|
+
|
|
739
|
+
# Update registry
|
|
740
|
+
with self._REGISTRY_LOCK:
|
|
741
|
+
if self.lock_file in self._active_locks:
|
|
742
|
+
self._active_locks[self.lock_file] -= 1
|
|
743
|
+
# FIXED: Clean up zero-count entries immediately
|
|
744
|
+
if self._active_locks[self.lock_file] <= 0:
|
|
745
|
+
self._active_locks.pop(self.lock_file, None)
|
|
746
|
+
|
|
747
|
+
finally:
|
|
748
|
+
self._locked = False
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def get_safe_file_lock(lock_file: str, timeout: float = 60.0) -> Any:
|
|
752
|
+
"""Get a SafeFileLock instance.
|
|
753
|
+
|
|
754
|
+
Args:
|
|
755
|
+
lock_file: Path to the lock file
|
|
756
|
+
timeout: Timeout for lock acquisition
|
|
757
|
+
|
|
758
|
+
Returns:
|
|
759
|
+
SafeFileLock instance
|
|
760
|
+
"""
|
|
761
|
+
return SafeFileLock(lock_file, timeout)
|
|
762
|
+
|