polyrob 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/README.md +453 -0
- agents/__init__.py +164 -0
- agents/base_agent.py +743 -0
- agents/personality/character.py +119 -0
- agents/personality/character_manager.py +309 -0
- agents/personality/characters/rob.character.json +56 -0
- agents/personality/characters/trump.character.json +63 -0
- agents/personality/persona_render.py +78 -0
- agents/personality/persona_resolver.py +115 -0
- agents/prompt/__init__.py +58 -0
- agents/prompt/base_prompt.py +92 -0
- agents/prompt/system.py +417 -0
- agents/task/README_PATH_MANAGEMENT.md +141 -0
- agents/task/__init__.py +162 -0
- agents/task/agent/__init__.py +70 -0
- agents/task/agent/agent_state.py +408 -0
- agents/task/agent/async_delegation.py +274 -0
- agents/task/agent/autonomy_state.py +275 -0
- agents/task/agent/conversation.py +75 -0
- agents/task/agent/core/__init__.py +6 -0
- agents/task/agent/core/aux_metering.py +61 -0
- agents/task/agent/core/background_review.py +157 -0
- agents/task/agent/core/construction.py +1179 -0
- agents/task/agent/core/conversational_exit.py +49 -0
- agents/task/agent/core/correspondent_gate.py +227 -0
- agents/task/agent/core/curator.py +246 -0
- agents/task/agent/core/episodic_digest.py +103 -0
- agents/task/agent/core/error_recovery.py +480 -0
- agents/task/agent/core/history_io.py +257 -0
- agents/task/agent/core/llm_provisioning.py +287 -0
- agents/task/agent/core/llm_runner.py +547 -0
- agents/task/agent/core/logging_io.py +276 -0
- agents/task/agent/core/loop_detection.py +262 -0
- agents/task/agent/core/memory_prefetch.py +325 -0
- agents/task/agent/core/memory_writer.py +667 -0
- agents/task/agent/core/model_introspection.py +82 -0
- agents/task/agent/core/model_swap.py +177 -0
- agents/task/agent/core/next_action_internal.py +1331 -0
- agents/task/agent/core/output_validation.py +303 -0
- agents/task/agent/core/project_context.py +272 -0
- agents/task/agent/core/resources.py +93 -0
- agents/task/agent/core/result_processing.py +389 -0
- agents/task/agent/core/run_loop.py +661 -0
- agents/task/agent/core/safety_lifecycle.py +228 -0
- agents/task/agent/core/secret_guard.py +293 -0
- agents/task/agent/core/self_wake.py +280 -0
- agents/task/agent/core/session_metadata.py +104 -0
- agents/task/agent/core/step.py +781 -0
- agents/task/agent/core/step_execution.py +398 -0
- agents/task/agent/core/step_telemetry.py +222 -0
- agents/task/agent/core/test_aux_metering.py +50 -0
- agents/task/agent/core/test_billing_failover.py +68 -0
- agents/task/agent/core/test_fatal_error_classifier.py +38 -0
- agents/task/agent/core/test_judge_validation.py +275 -0
- agents/task/agent/core/test_model_swap.py +190 -0
- agents/task/agent/core/test_reflection_offload.py +54 -0
- agents/task/agent/core/turn_input.py +8 -0
- agents/task/agent/core/untrusted_wrap.py +103 -0
- agents/task/agent/core/user_ingress.py +228 -0
- agents/task/agent/hitl_manager.py +396 -0
- agents/task/agent/log_sanitize.py +36 -0
- agents/task/agent/message_manager/config.py +97 -0
- agents/task/agent/message_manager/service.py +758 -0
- agents/task/agent/message_manager/tests.py +305 -0
- agents/task/agent/message_manager/tool_call_builder.py +539 -0
- agents/task/agent/message_manager/tool_message_repair.py +364 -0
- agents/task/agent/message_manager/views.py +143 -0
- agents/task/agent/messages/__init__.py +8 -0
- agents/task/agent/messages/builders.py +309 -0
- agents/task/agent/messages/compactor.py +611 -0
- agents/task/agent/messages/context_references.py +271 -0
- agents/task/agent/messages/filters.py +333 -0
- agents/task/agent/messages/guidance.py +328 -0
- agents/task/agent/messages/persistence.py +448 -0
- agents/task/agent/messages/retrieval.py +506 -0
- agents/task/agent/messages/sqlite_persistence.py +85 -0
- agents/task/agent/messages/test_compaction_upgrade.py +349 -0
- agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
- agents/task/agent/messages/test_sqlite_persistence.py +54 -0
- agents/task/agent/messages/token_counter.py +532 -0
- agents/task/agent/orchestrator.py +776 -0
- agents/task/agent/profile_manager.py +105 -0
- agents/task/agent/profile_registry.py +173 -0
- agents/task/agent/prompts.py +1090 -0
- agents/task/agent/scenario_registry.py +226 -0
- agents/task/agent/service.py +509 -0
- agents/task/agent/session.py +984 -0
- agents/task/agent/skill_discovery.py +110 -0
- agents/task/agent/skill_frontmatter.py +94 -0
- agents/task/agent/skill_manager.py +1201 -0
- agents/task/agent/skill_store.py +627 -0
- agents/task/agent/skill_validation.py +56 -0
- agents/task/agent/skill_writer.py +492 -0
- agents/task/agent/sub_agent_manager.py +1137 -0
- agents/task/agent/test_session_disk_recovery.py +34 -0
- agents/task/agent/tests.py +513 -0
- agents/task/agent/tool_call_tracker.py +511 -0
- agents/task/agent/views.py +469 -0
- agents/task/config.py +432 -0
- agents/task/constants.py +1384 -0
- agents/task/flag_defaults.py +63 -0
- agents/task/goals/__init__.py +1 -0
- agents/task/goals/autonomy_marker.py +28 -0
- agents/task/goals/board.py +690 -0
- agents/task/goals/completion_judge.py +247 -0
- agents/task/goals/context.py +62 -0
- agents/task/goals/dispatcher.py +848 -0
- agents/task/goals/escalation.py +102 -0
- agents/task/goals/planner.py +114 -0
- agents/task/logging_config.py +155 -0
- agents/task/path.py +991 -0
- agents/task/robust_parse_config.py +400 -0
- agents/task/runtime/__init__.py +0 -0
- agents/task/runtime/run_as_session.py +130 -0
- agents/task/runtime_safety.py +308 -0
- agents/task/session/__init__.py +5 -0
- agents/task/session/browser_pool.py +130 -0
- agents/task/session/cleanup.py +612 -0
- agents/task/session/execution.py +397 -0
- agents/task/session/feed.py +200 -0
- agents/task/session/hitl_ingress.py +201 -0
- agents/task/session/hooks.py +83 -0
- agents/task/session/multi_agent.py +243 -0
- agents/task/session/workspace.py +91 -0
- agents/task/session_registry.py +91 -0
- agents/task/session_route.py +35 -0
- agents/task/sqlite_session_registry.py +261 -0
- agents/task/surface_config.py +185 -0
- agents/task/telemetry/__init__.py +58 -0
- agents/task/telemetry/event_log.py +180 -0
- agents/task/telemetry/formatters.py +642 -0
- agents/task/telemetry/manager.py +571 -0
- agents/task/telemetry/memory_events.py +66 -0
- agents/task/telemetry/self_events.py +53 -0
- agents/task/telemetry/sequence.py +160 -0
- agents/task/telemetry/service.py +1265 -0
- agents/task/telemetry/views.py +882 -0
- agents/task/templates.py +98 -0
- agents/task/test_aux_chain.py +151 -0
- agents/task/test_aux_router.py +41 -0
- agents/task/tests/test_parse_robustness.py +170 -0
- agents/task/tool_defaults.py +110 -0
- agents/task/utils.py +762 -0
- agents/task/utils_json.py +768 -0
- agents/task/utils_webview.py +86 -0
- agents/task/workspace_context.py +229 -0
- agents/task_agent_lite.py +2398 -0
- api/README.md +683 -0
- api/TASK_API_DOCS.md +151 -0
- api/__init__.py +0 -0
- api/a2a/__init__.py +37 -0
- api/a2a/agent_card.py +337 -0
- api/a2a/client.py +657 -0
- api/a2a/endpoints.py +375 -0
- api/a2a/models.py +268 -0
- api/a2a/streaming.py +419 -0
- api/a2a/task_handler.py +929 -0
- api/admin_endpoints.py +1584 -0
- api/app.py +923 -0
- api/auth_constants.py +28 -0
- api/auth_endpoints.py +402 -0
- api/auth_state.py +36 -0
- api/chat_via_task.py +42 -0
- api/conversation_manager.py +231 -0
- api/dependencies.py +186 -0
- api/eip8004_endpoints.py +452 -0
- api/hyperliquid_models.py +174 -0
- api/hyperliquid_routes.py +450 -0
- api/interfaces.py +207 -0
- api/jwt_middleware.py +83 -0
- api/kb/__init__.py +1 -0
- api/kb/endpoints.py +314 -0
- api/mcp_models.py +279 -0
- api/mcp_routes.py +438 -0
- api/middleware.py +526 -0
- api/models.py +176 -0
- api/openai_compat/__init__.py +0 -0
- api/openai_compat/model_map.py +25 -0
- api/openai_compat/models.py +59 -0
- api/openai_compat/router.py +140 -0
- api/payment_endpoints.py +444 -0
- api/payment_verification.py +138 -0
- api/polymarket_models.py +155 -0
- api/polymarket_routes.py +501 -0
- api/pricing_endpoints.py +89 -0
- api/session_routing.py +54 -0
- api/skill_endpoints.py +453 -0
- api/task_http_api.py +1856 -0
- api/webhooks.py +52 -0
- api/x402_endpoints.py +280 -0
- avatar/README.md +44 -0
- avatar/__init__.py +1 -0
- avatar/config/rob.json +7 -0
- avatar/mindprint.js +395 -0
- avatar/renders/rob.meta.json +20 -0
- avatar/renders/rob.png +0 -0
- avatar/studio.html +373 -0
- avatar/webview/avatar-live.js +44 -0
- cli/README.md +89 -0
- cli/__init__.py +0 -0
- cli/cli_surface.py +67 -0
- cli/commands/__init__.py +0 -0
- cli/commands/_bootstrap.py +49 -0
- cli/commands/_errors.py +53 -0
- cli/commands/chat.py +973 -0
- cli/commands/config.py +100 -0
- cli/commands/dashboard.py +103 -0
- cli/commands/doctor.py +222 -0
- cli/commands/email.py +174 -0
- cli/commands/gateway.py +346 -0
- cli/commands/goals.py +509 -0
- cli/commands/init.py +276 -0
- cli/commands/journey.py +30 -0
- cli/commands/kb.py +280 -0
- cli/commands/model.py +107 -0
- cli/commands/owner.py +431 -0
- cli/commands/pfp.py +232 -0
- cli/commands/run.py +414 -0
- cli/commands/serve.py +41 -0
- cli/commands/session.py +684 -0
- cli/commands/skill_install.py +786 -0
- cli/commands/skills.py +128 -0
- cli/commands/subagents.py +100 -0
- cli/commands/surface.py +81 -0
- cli/commands/telegram.py +185 -0
- cli/commands/todos.py +209 -0
- cli/commands/tools.py +102 -0
- cli/commands/update.py +316 -0
- cli/commands/whatsapp.py +175 -0
- cli/config_store.py +276 -0
- cli/gitignore.py +43 -0
- cli/inventory.py +46 -0
- cli/keys.py +101 -0
- cli/persona.py +17 -0
- cli/polyrob.py +137 -0
- cli/ui/__init__.py +64 -0
- cli/ui/activity.py +141 -0
- cli/ui/app.py +538 -0
- cli/ui/banner.py +233 -0
- cli/ui/blocks.py +534 -0
- cli/ui/bootstrap_notice.py +47 -0
- cli/ui/commands/__init__.py +41 -0
- cli/ui/commands/h_cron.py +64 -0
- cli/ui/commands/h_journey.py +164 -0
- cli/ui/commands/h_kb.py +121 -0
- cli/ui/commands/h_learn.py +100 -0
- cli/ui/commands/h_mcp.py +215 -0
- cli/ui/commands/h_self.py +113 -0
- cli/ui/commands/h_skills.py +314 -0
- cli/ui/commands/handlers.py +1672 -0
- cli/ui/commands/registry.py +426 -0
- cli/ui/dialog.py +425 -0
- cli/ui/event_registry.py +110 -0
- cli/ui/event_specs.py +45 -0
- cli/ui/events.py +433 -0
- cli/ui/identity.py +15 -0
- cli/ui/lifecycle.py +159 -0
- cli/ui/live_hooks.py +43 -0
- cli/ui/model_selector.py +532 -0
- cli/ui/persistent_loop.py +181 -0
- cli/ui/pick.py +113 -0
- cli/ui/plain_renderer.py +351 -0
- cli/ui/renderer.py +384 -0
- cli/ui/rich_renderer.py +533 -0
- cli/ui/secrets.py +115 -0
- cli/ui/state.py +376 -0
- cli/ui/statusbar.py +231 -0
- cli/ui/streaming.py +254 -0
- cli/ui/terminal_render.py +105 -0
- cli/ui/theme.py +132 -0
- cli/update/__init__.py +6 -0
- cli/update/context.py +121 -0
- cli/update/detect.py +176 -0
- cli/update/engine.py +95 -0
- cli/update/migrate_guarded.py +55 -0
- cli/update/process_guard.py +215 -0
- cli/update/runners.py +78 -0
- cli/update/snapshot.py +270 -0
- cli/update/versions.py +205 -0
- core/README.md +377 -0
- core/__init__.py +174 -0
- core/assets.py +87 -0
- core/async_bridge.py +76 -0
- core/autonomy_runtime.py +259 -0
- core/base_component.py +302 -0
- core/bootstrap.py +681 -0
- core/bot.py +310 -0
- core/config.py +833 -0
- core/constants.py +126 -0
- core/container.py +462 -0
- core/db_manifest.py +101 -0
- core/embedding.py +57 -0
- core/env.py +39 -0
- core/exceptions.py +287 -0
- core/flags.py +144 -0
- core/flags_catalog.py +345 -0
- core/home_migration.py +62 -0
- core/identity.py +118 -0
- core/initialization.py +1004 -0
- core/instance.py +537 -0
- core/interactive_gate.py +96 -0
- core/logging.py +436 -0
- core/owner_doc_writer.py +176 -0
- core/pairing.py +193 -0
- core/path_safety.py +20 -0
- core/paths.py +27 -0
- core/payment_config.py +21 -0
- core/permissions.py +322 -0
- core/runtime_config.py +96 -0
- core/runtime_paths.py +135 -0
- core/seams.py +35 -0
- core/secret_scan.py +25 -0
- core/secret_scrub.py +71 -0
- core/secrets.py +35 -0
- core/security_logging_filter.py +148 -0
- core/self_context_writer.py +333 -0
- core/self_evolution.py +294 -0
- core/session_context.py +47 -0
- core/sqlite_util.py +59 -0
- core/surfaces/__init__.py +16 -0
- core/surfaces/access.py +118 -0
- core/surfaces/binding.py +88 -0
- core/surfaces/bootstrap.py +78 -0
- core/surfaces/circuit.py +168 -0
- core/surfaces/continuity.py +77 -0
- core/surfaces/correspondents.py +228 -0
- core/surfaces/dispatcher.py +216 -0
- core/surfaces/envelopes.py +75 -0
- core/surfaces/gc.py +30 -0
- core/surfaces/idempotency.py +57 -0
- core/surfaces/inbound_webhook.py +124 -0
- core/surfaces/media.py +32 -0
- core/surfaces/message_router.py +91 -0
- core/surfaces/outbound_allowlist.py +80 -0
- core/surfaces/outbound_dispatcher.py +110 -0
- core/surfaces/outbound_mirror.py +37 -0
- core/surfaces/outbound_queue.py +107 -0
- core/surfaces/outbound_target.py +12 -0
- core/surfaces/owner_admin.py +29 -0
- core/surfaces/proactive.py +78 -0
- core/surfaces/progress.py +108 -0
- core/surfaces/rate_bucket.py +21 -0
- core/surfaces/registry.py +51 -0
- core/surfaces/rendering.py +53 -0
- core/surfaces/send_policy.py +14 -0
- core/surfaces/serialize.py +24 -0
- core/surfaces/session_chat_registry.py +112 -0
- core/surfaces/session_policy.py +60 -0
- core/surfaces/surface.py +288 -0
- core/surfaces/transcription.py +88 -0
- core/surfaces/voice_echo.py +26 -0
- core/surfaces/voice_guard.py +23 -0
- core/tickers.py +174 -0
- core/tool_catalog.py +192 -0
- core/version.py +75 -0
- core/wallet/__init__.py +5 -0
- core/wallet/agent_wallet.py +59 -0
- core/wallet/audit_sink.py +61 -0
- core/wallet/config.py +70 -0
- core/wallet/factory.py +87 -0
- core/wallet/policy.py +104 -0
- core/wallet/signer.py +51 -0
- cron/README.md +65 -0
- cron/__init__.py +0 -0
- cron/delivery.py +277 -0
- cron/digest.py +124 -0
- cron/jobs.py +207 -0
- cron/runner.py +297 -0
- cron/schedule.py +194 -0
- cron/scheduler.py +201 -0
- cron/service.py +57 -0
- cron/wake_gate.py +223 -0
- data/__init__.py +1 -0
- data/prompts/autov2_prompts.json +87 -0
- data/prompts/skills/browser-automation/SKILL.md +43 -0
- data/prompts/skills/coding-workflow/SKILL.md +36 -0
- data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
- data/prompts/skills/document-writing/SKILL.md +33 -0
- data/prompts/skills/email-comms/SKILL.md +33 -0
- data/prompts/skills/file-data-ops/SKILL.md +35 -0
- data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
- data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
- data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
- data/prompts/skills/lead-research/SKILL.md +86 -0
- data/prompts/skills/market-research-brief/SKILL.md +104 -0
- data/prompts/skills/person-analyzer/SKILL.md +124 -0
- data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
- data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
- data/prompts/skills/polymarket-trading/SKILL.md +55 -0
- data/prompts/skills/presentation-creator/SKILL.md +36 -0
- data/prompts/skills/project-analyzer/SKILL.md +169 -0
- data/prompts/skills/rules.json +268 -0
- data/prompts/skills/secret-handling/SKILL.md +20 -0
- data/prompts/skills/skill-authoring/SKILL.md +31 -0
- data/prompts/skills/skill-security-review/SKILL.md +27 -0
- data/prompts/skills/social-discovery/SKILL.md +57 -0
- data/prompts/skills/task-planning/SKILL.md +33 -0
- data/prompts/skills/web-research/SKILL.md +39 -0
- data/prompts/skills/web-scraping/SKILL.md +73 -0
- data/prompts/skills/x-engagement/SKILL.md +69 -0
- data/prompts/system_prompts.json +4 -0
- data/subscription_channels.json +5 -0
- modules/README.md +503 -0
- modules/__init__.py +124 -0
- modules/auth/__init__.py +13 -0
- modules/auth/api_key_manager.py +157 -0
- modules/auth/identity_mapper.py +380 -0
- modules/auth/siwe_auth.py +273 -0
- modules/auth/tier_manager.py +173 -0
- modules/base_module.py +190 -0
- modules/credits/__init__.py +29 -0
- modules/credits/balance_manager.py +237 -0
- modules/credits/cost_utils.py +129 -0
- modules/credits/pricing.py +139 -0
- modules/credits/unified_ledger.py +148 -0
- modules/credits/usage_meter.py +310 -0
- modules/credits/usage_tracker.py +674 -0
- modules/database/__init__.py +33 -0
- modules/database/audit_log.py +322 -0
- modules/database/auth_tables.py +276 -0
- modules/database/connection.py +644 -0
- modules/database/connection_pool.py +305 -0
- modules/database/conversation_contexts.py +446 -0
- modules/database/database_manager.py +237 -0
- modules/database/hyperliquid.py +375 -0
- modules/database/polymarket.py +574 -0
- modules/database/user_mcp_servers.py +728 -0
- modules/database/user_profiles.py +562 -0
- modules/database/utils.py +107 -0
- modules/database/x402_tables.py +92 -0
- modules/eip8004/README.md +1069 -0
- modules/eip8004/__init__.py +65 -0
- modules/eip8004/contracts.py +498 -0
- modules/eip8004/models.py +271 -0
- modules/eip8004/registration.py +148 -0
- modules/eip8004/reputation.py +367 -0
- modules/eip8004/validation.py +277 -0
- modules/llm/__init__.py +97 -0
- modules/llm/adapters.py +963 -0
- modules/llm/anthropic_client.py +831 -0
- modules/llm/available_models.py +106 -0
- modules/llm/brain_scrubber.py +137 -0
- modules/llm/cache_hints.py +162 -0
- modules/llm/deepseek_client.py +562 -0
- modules/llm/gemini_client.py +1367 -0
- modules/llm/llm_client.py +1139 -0
- modules/llm/llm_client_registry.py +188 -0
- modules/llm/llm_factory.py +237 -0
- modules/llm/llm_manager.py +1036 -0
- modules/llm/messages.py +374 -0
- modules/llm/model_registry.py +1935 -0
- modules/llm/nvidia_client.py +125 -0
- modules/llm/openai_client.py +800 -0
- modules/llm/openrouter_client.py +969 -0
- modules/llm/profiles.py +178 -0
- modules/llm/test_anthropic_cache_capture.py +35 -0
- modules/llm/test_cached_pricing_derivation.py +24 -0
- modules/llm/think_scrubber.py +444 -0
- modules/llm/token_counter.py +268 -0
- modules/memory/__init__.py +35 -0
- modules/memory/backend_factory.py +83 -0
- modules/memory/cache_manager.py +186 -0
- modules/memory/episodic.py +108 -0
- modules/memory/local_vector_memory_provider.py +552 -0
- modules/memory/memory_manager.py +225 -0
- modules/memory/models.py +312 -0
- modules/memory/provider.py +210 -0
- modules/memory/registry.py +205 -0
- modules/memory/sqlite_memory_provider.py +767 -0
- modules/memory/task/__init__.py +52 -0
- modules/memory/task/compaction_manager.py +157 -0
- modules/memory/task/context_retriever.py +809 -0
- modules/memory/task/hierarchical_memory.py +1002 -0
- modules/memory/task/lexical_retriever.py +71 -0
- modules/memory/task/null_context_manager.py +213 -0
- modules/memory/task/phase_manager.py +625 -0
- modules/memory/task/reflection_service.py +157 -0
- modules/memory/task/semantic_retriever.py +218 -0
- modules/memory/task/task_context_manager.py +1165 -0
- modules/memory/task/test_forgetting_engages.py +24 -0
- modules/memory/task/test_reflection_llm.py +39 -0
- modules/memory/task/test_threat_scan.py +53 -0
- modules/memory/task/threat_scan.py +90 -0
- modules/memory/test_sqlite_memory_provider.py +106 -0
- modules/memory/user_profile_manager.py +527 -0
- modules/payments/__init__.py +11 -0
- modules/payments/deposit_monitor.py +478 -0
- modules/payments/price_oracle.py +70 -0
- modules/payments/treasury_sweeper.py +401 -0
- modules/payments/wallet_generator.py +145 -0
- modules/pfp/__init__.py +1 -0
- modules/pfp/config.py +92 -0
- modules/pfp/mesh.py +565 -0
- modules/pfp/push.py +69 -0
- modules/pfp/renderer.py +85 -0
- modules/pfp/store.py +82 -0
- modules/skills/__init__.py +1 -0
- modules/skills/skill_usage.py +248 -0
- modules/transcription/__init__.py +24 -0
- modules/transcription/base.py +20 -0
- modules/transcription/faster_whisper_transcriber.py +55 -0
- modules/x402/README.md +249 -0
- modules/x402/__init__.py +22 -0
- modules/x402/invoicing.py +416 -0
- modules/x402/middleware.py +364 -0
- modules/x402/settlement_watcher.py +145 -0
- modules/x402/x402_integration.py +344 -0
- polyrob-0.5.0.dist-info/METADATA +482 -0
- polyrob-0.5.0.dist-info/RECORD +745 -0
- polyrob-0.5.0.dist-info/WHEEL +5 -0
- polyrob-0.5.0.dist-info/entry_points.txt +2 -0
- polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
- polyrob-0.5.0.dist-info/top_level.txt +12 -0
- surfaces/README.md +70 -0
- surfaces/__init__.py +0 -0
- surfaces/email/__init__.py +0 -0
- surfaces/email/dedup.py +44 -0
- surfaces/email/harness.py +187 -0
- surfaces/email/inbound.py +159 -0
- surfaces/email/seed.py +60 -0
- surfaces/email/surface.py +105 -0
- surfaces/telegram/__init__.py +0 -0
- surfaces/telegram/dedup.py +83 -0
- surfaces/telegram/harness.py +886 -0
- surfaces/telegram/inbound.py +145 -0
- surfaces/telegram/interactive_tools.py +49 -0
- surfaces/telegram/markdown.py +347 -0
- surfaces/telegram/rate_limit.py +167 -0
- surfaces/telegram/surface.py +187 -0
- surfaces/telegram/voice.py +52 -0
- surfaces/whatsapp/__init__.py +0 -0
- surfaces/whatsapp/client.py +51 -0
- surfaces/whatsapp/harness.py +62 -0
- surfaces/whatsapp/inbound.py +132 -0
- surfaces/whatsapp/surface.py +64 -0
- surfaces/whatsapp/window.py +34 -0
- tools/README.md +578 -0
- tools/__init__.py +424 -0
- tools/alchemy/__init__.py +5 -0
- tools/alchemy/alchemy_tool.py +389 -0
- tools/anysite/__init__.py +17 -0
- tools/anysite/client.py +88 -0
- tools/anysite/tool.py +89 -0
- tools/base_tool.py +468 -0
- tools/browser/__init__.py +22 -0
- tools/browser/actions.py +118 -0
- tools/browser/browser.py +1457 -0
- tools/browser/browser_manager.py +690 -0
- tools/browser/context.py +1748 -0
- tools/browser/playwright_utils.py +137 -0
- tools/browser/views.py +56 -0
- tools/code_exec/SANDBOX_SECURITY.md +144 -0
- tools/code_exec/__init__.py +178 -0
- tools/code_exec/backend.py +73 -0
- tools/code_exec/backends/__init__.py +1 -0
- tools/code_exec/backends/docker.py +779 -0
- tools/code_exec/backends/local_subprocess.py +154 -0
- tools/code_exec/env_policy.py +45 -0
- tools/code_exec/result.py +42 -0
- tools/code_exec/sandbox_guard.py +59 -0
- tools/code_exec/tool.py +227 -0
- tools/coding/__init__.py +50 -0
- tools/coding/edit.py +107 -0
- tools/coding/search.py +113 -0
- tools/coding/tool.py +353 -0
- tools/collabland/__init__.py +5 -0
- tools/collabland/collabland_tool.py +471 -0
- tools/controller/__init__.py +0 -0
- tools/controller/_helpers.py +165 -0
- tools/controller/action_registration.py +1765 -0
- tools/controller/approval.py +244 -0
- tools/controller/approval_interactive.py +90 -0
- tools/controller/delegation.py +195 -0
- tools/controller/execution.py +797 -0
- tools/controller/execution_context.py +141 -0
- tools/controller/hooks.py +135 -0
- tools/controller/introspection.py +344 -0
- tools/controller/mcp_registrar.py +190 -0
- tools/controller/message_send.py +31 -0
- tools/controller/registry/__init__.py +4 -0
- tools/controller/registry/schema_generators.py +395 -0
- tools/controller/registry/schema_sanitizer.py +406 -0
- tools/controller/registry/service.py +1266 -0
- tools/controller/registry/views.py +151 -0
- tools/controller/service.py +409 -0
- tools/controller/tool_management.py +334 -0
- tools/controller/types.py +28 -0
- tools/controller/views.py +450 -0
- tools/cronjob_tools.py +166 -0
- tools/crypto_trade_gate.py +59 -0
- tools/descriptors.py +452 -0
- tools/dom/__init__.py +0 -0
- tools/dom/history_tree_processor/service.py +244 -0
- tools/dom/history_tree_processor/view.py +99 -0
- tools/dom/service.py +471 -0
- tools/dom/views.py +346 -0
- tools/email_tool.py +403 -0
- tools/exceptions.py +169 -0
- tools/filesystem.py +1038 -0
- tools/filesystem_docproc.py +331 -0
- tools/filesystem_pdf.py +755 -0
- tools/git/__init__.py +42 -0
- tools/git/tool.py +258 -0
- tools/github/__init__.py +33 -0
- tools/github/client.py +94 -0
- tools/github/tool.py +237 -0
- tools/goal_tools.py +309 -0
- tools/hyperliquid/__init__.py +56 -0
- tools/hyperliquid/models.py +233 -0
- tools/hyperliquid/service.py +1680 -0
- tools/knowledge_ingest.py +950 -0
- tools/mcp/README.md +535 -0
- tools/mcp/__init__.py +52 -0
- tools/mcp/catalog.py +115 -0
- tools/mcp/config.py +337 -0
- tools/mcp/mcp_tool.py +1415 -0
- tools/mcp/param_coercion.py +289 -0
- tools/mcp/protocol.py +1520 -0
- tools/mcp/rate_limit.py +50 -0
- tools/mcp/security.py +424 -0
- tools/mcp/self_install.py +111 -0
- tools/mcp/server_manager.py +829 -0
- tools/mcp/subscriptions.py +72 -0
- tools/mcp/user_mcp_service.py +899 -0
- tools/mcp/validation_tracker.py +89 -0
- tools/mcp/views.py +346 -0
- tools/oauth/__init__.py +17 -0
- tools/oauth/manager.py +75 -0
- tools/oauth/provider.py +69 -0
- tools/oauth/providers/__init__.py +1 -0
- tools/oauth/providers/generic_oauth2.py +96 -0
- tools/perplexity_tool.py +280 -0
- tools/polymarket/__init__.py +41 -0
- tools/polymarket/clob_adapter.py +76 -0
- tools/polymarket/models.py +182 -0
- tools/polymarket/service.py +2082 -0
- tools/self_env/__init__.py +61 -0
- tools/self_env/tool.py +274 -0
- tools/shell/__init__.py +80 -0
- tools/shell/backend_pool.py +118 -0
- tools/shell/discipline.py +62 -0
- tools/shell/executor.py +157 -0
- tools/shell/loopback_allow.py +87 -0
- tools/shell/process_registry.py +113 -0
- tools/shell/process_tool.py +131 -0
- tools/shell/state.py +129 -0
- tools/shell/tool.py +151 -0
- tools/task_tool.py +637 -0
- tools/twitter_tool.py +2086 -0
- tools/user_directory.py +179 -0
- tools/web_fetch/__init__.py +3 -0
- tools/web_fetch/fetcher.py +144 -0
- tools/web_fetch/render.py +44 -0
- tools/web_fetch/tool.py +60 -0
- tools/x402/__init__.py +60 -0
- tools/x402/client.py +47 -0
- tools/x402/invoice_tool.py +143 -0
- tools/x402/real_client.py +250 -0
- tools/x402/service.py +131 -0
- utils/README.md +498 -0
- utils/__init__.py +46 -0
- utils/auth_utils.py +114 -0
- utils/bounded_collections.py +162 -0
- utils/circuit_breaker.py +389 -0
- utils/gif_utils.py +435 -0
- utils/markdown_utils.py +291 -0
- utils/message_utils.py +147 -0
- utils/metrics.py +36 -0
- utils/path_validator.py +227 -0
- utils/rate_limit_manager.py +399 -0
- utils/result_size.py +296 -0
- utils/time_utils.py +206 -0
- utils/user_utils.py +188 -0
- webview/README.md +111 -0
- webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
- webview/__init__.py +8 -0
- webview/activity.py +636 -0
- webview/owner_auth.py +108 -0
- webview/pages.py +448 -0
- webview/repair_sessions.py +399 -0
- webview/server.py +4323 -0
- webview/server_launcher.py +112 -0
- webview/static/css/activity.css +89 -0
- webview/static/css/chat.css +1772 -0
- webview/static/css/components.css +1190 -0
- webview/static/css/config-panel.css +651 -0
- webview/static/css/pages/admin.css +766 -0
- webview/static/css/pages/profile.css +231 -0
- webview/static/css/pages/settings.css +297 -0
- webview/static/css/pages/signin.css +316 -0
- webview/static/css/style.css +4250 -0
- webview/static/css/variables.css +147 -0
- webview/static/css/workspace-fullwidth-fix.css +109 -0
- webview/static/img/favicon.ico +0 -0
- webview/static/js/activity.js +394 -0
- webview/static/js/admin/activity.js +186 -0
- webview/static/js/admin/dashboard.js +163 -0
- webview/static/js/admin/user_detail.js +400 -0
- webview/static/js/admin/users.js +174 -0
- webview/static/js/admin/utils.js +214 -0
- webview/static/js/chat.js +3178 -0
- webview/static/js/config-panel.js +522 -0
- webview/static/js/constants.js +20 -0
- webview/static/js/error-handler.js +226 -0
- webview/static/js/ethers.min.js +1 -0
- webview/static/js/event-filter.js +137 -0
- webview/static/js/event-store.js +351 -0
- webview/static/js/file-attachments.js +374 -0
- webview/static/js/file-loader.js +163 -0
- webview/static/js/index.js +103 -0
- webview/static/js/performance-utils.js +196 -0
- webview/static/js/profile.js +396 -0
- webview/static/js/screenshot.js +386 -0
- webview/static/js/session.js +1527 -0
- webview/static/js/settings.js +1288 -0
- webview/static/js/sidebar-data.js +766 -0
- webview/static/js/sidebar-toggle.js +296 -0
- webview/static/js/socket.io.min.js +7 -0
- webview/static/js/stats.js +355 -0
- webview/static/js/ui-utils.js +420 -0
- webview/static/js/workspace.js +966 -0
- webview/stats_service.py +613 -0
- webview/templates/__init__.py +1 -0
- webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- webview/templates/activity.html +36 -0
- webview/templates/admin/activity.html +122 -0
- webview/templates/admin/dashboard.html +112 -0
- webview/templates/admin/user_detail.html +228 -0
- webview/templates/admin/users.html +105 -0
- webview/templates/autonomy.html +60 -0
- webview/templates/error.html +133 -0
- webview/templates/finance.html +90 -0
- webview/templates/identity.html +73 -0
- webview/templates/index.html +42 -0
- webview/templates/layout.html +125 -0
- webview/templates/memory.html +59 -0
- webview/templates/owner_login.html +15 -0
- webview/templates/profile.html +121 -0
- webview/templates/session.html +312 -0
- webview/templates/settings.html +534 -0
- webview/templates/sidebar.html +112 -0
- webview/templates/signin.html +213 -0
- webview/templates/status.html +14 -0
- webview/templates/system.html +45 -0
- webview/webgate.py +196 -0
|
@@ -0,0 +1,1765 @@
|
|
|
1
|
+
"""ActionRegistrationMixin — Controller one-shot action registration (UP-11, verbatim
|
|
2
|
+
code-motion). Registers send_message/done/load_skill/session_search/memory/delegation
|
|
3
|
+
closures + backward-compat aliases. Methods use ``self`` (the composed Controller)
|
|
4
|
+
exactly as before via the MRO."""
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from core.env import bool_env as _bool_env
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from tools.controller.types import ActionResult
|
|
13
|
+
from tools.controller.views import DoneAction, SendMessageAction
|
|
14
|
+
from tools.controller._helpers import build_load_skill_result, read_skill_resource_confined
|
|
15
|
+
from modules.llm.messages import AIMessage
|
|
16
|
+
from agents.task.agent.core.self_wake import FORGED_TURN_KINDS as _FORGED_TURN_KINDS
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _is_forged_or_autonomous_turn(execution_context, controller_self):
|
|
20
|
+
"""C7/W2/SK-F10: a turn that must NEVER auto-activate or promote its own content.
|
|
21
|
+
|
|
22
|
+
True for a sub-agent / leaf turn (delegated worker, background-review reviewer),
|
|
23
|
+
an autonomous goal/cron/planner-spawned session, OR a forged self-wake /
|
|
24
|
+
async-delegation-result re-entry into the MAIN agent.
|
|
25
|
+
|
|
26
|
+
SK-F10: a self-wake or delegation-result re-entry resolves to
|
|
27
|
+
role='orchestrator', is_sub_agent=False — the SAME shape as a genuine owner
|
|
28
|
+
turn — so without a turn-kind signal it could auto-activate a skill (review
|
|
29
|
+
off) or self-promote its own pending draft. The run loop stamps
|
|
30
|
+
`execution_context.metadata["turn_kind"]` from the orchestrator's
|
|
31
|
+
`_forged_turn_kind` marker (set when a drained HITL message is
|
|
32
|
+
kind="self_wake"/"delegation_result", cleared on a genuine drained turn) —
|
|
33
|
+
see `agents/task/agent/core/user_ingress.py::_drain_user_messages` and
|
|
34
|
+
`agents/task/agent/core/step_execution.py::_build_execution_context`.
|
|
35
|
+
"""
|
|
36
|
+
is_sub = bool(getattr(execution_context, 'is_sub_agent', False)
|
|
37
|
+
or getattr(controller_self, '_is_sub_agent', False))
|
|
38
|
+
# H7: default to the least-privileged role when unset (treat as forged).
|
|
39
|
+
role = getattr(execution_context, 'role', 'leaf')
|
|
40
|
+
if is_sub or role == 'leaf':
|
|
41
|
+
return True
|
|
42
|
+
metadata = getattr(execution_context, 'metadata', None) or {}
|
|
43
|
+
if metadata.get('turn_kind') in _FORGED_TURN_KINDS:
|
|
44
|
+
return True
|
|
45
|
+
try:
|
|
46
|
+
from agents.task.goals.autonomy_marker import is_autonomous
|
|
47
|
+
sid = (getattr(execution_context, 'session_id', None)
|
|
48
|
+
or getattr(controller_self, 'session_id', ''))
|
|
49
|
+
return bool(is_autonomous(sid))
|
|
50
|
+
except Exception:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ActionRegistrationMixin:
|
|
55
|
+
def _register_default_actions(self):
|
|
56
|
+
"""Register core 'done' action for task completion.
|
|
57
|
+
|
|
58
|
+
The 'done' action is a CORE completion primitive that must always be available,
|
|
59
|
+
regardless of which tools are loaded. It opportunistically checks TodoManager
|
|
60
|
+
if TaskTool is available, but never blocks completion.
|
|
61
|
+
|
|
62
|
+
Todo-specific actions (task_todo_list, task_todo_add, etc.) remain in TaskTool.
|
|
63
|
+
"""
|
|
64
|
+
# Register the core 'done' action
|
|
65
|
+
# Support multiple output models for flexibility
|
|
66
|
+
if self.output_model is None:
|
|
67
|
+
@self.registry.action(
|
|
68
|
+
'Send a message to the user (status update, question, or information)',
|
|
69
|
+
param_model=SendMessageAction
|
|
70
|
+
)
|
|
71
|
+
async def send_message(params: SendMessageAction, execution_context=None):
|
|
72
|
+
"""Send message to user - continuous chat mode.
|
|
73
|
+
|
|
74
|
+
When wait_for_response=True:
|
|
75
|
+
- Message is sent to user
|
|
76
|
+
- Execution STOPS (returns is_done=True)
|
|
77
|
+
- User replies via continuous chat whenever they want
|
|
78
|
+
- Session resumes with reply in context
|
|
79
|
+
|
|
80
|
+
No timeouts, no waiting loops, no retries.
|
|
81
|
+
|
|
82
|
+
SUB-AGENT ISOLATION: Sub-agents skip adding to main context and feed.
|
|
83
|
+
Their messages are captured in their own context and returned as output.
|
|
84
|
+
"""
|
|
85
|
+
import time
|
|
86
|
+
|
|
87
|
+
# Check if this is a sub-agent (skip side effects for isolation)
|
|
88
|
+
is_sub_agent = execution_context.is_sub_agent if execution_context else False
|
|
89
|
+
|
|
90
|
+
if is_sub_agent:
|
|
91
|
+
# Sub-agent: Just log and return - output captured via normal result flow
|
|
92
|
+
self.logger.debug(f"💬 Sub-agent message (captured in result): {params.text[:100]}...")
|
|
93
|
+
return ActionResult(
|
|
94
|
+
extracted_content=f"Message: {params.text}",
|
|
95
|
+
include_in_memory=True
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.logger.info(f"💬 Message to user: {params.text[:100]}...")
|
|
99
|
+
|
|
100
|
+
# Add to conversation history - find the correct agent by agent_id
|
|
101
|
+
if hasattr(self, 'orchestrator') and self.orchestrator:
|
|
102
|
+
target_agent = None
|
|
103
|
+
agent_id = execution_context.agent_id if execution_context else None
|
|
104
|
+
|
|
105
|
+
if agent_id and agent_id in self.orchestrator.agents:
|
|
106
|
+
target_agent = self.orchestrator.agents[agent_id]
|
|
107
|
+
else:
|
|
108
|
+
# Fallback: use first agent (legacy behavior)
|
|
109
|
+
agents = list(self.orchestrator.agents.values())
|
|
110
|
+
if agents:
|
|
111
|
+
target_agent = agents[0]
|
|
112
|
+
|
|
113
|
+
if target_agent and hasattr(target_agent, 'message_manager'):
|
|
114
|
+
try:
|
|
115
|
+
target_agent.message_manager._add_message_with_tokens(
|
|
116
|
+
AIMessage(content=params.text),
|
|
117
|
+
_internal=True # Bypass single-writer enforcement for display messages
|
|
118
|
+
)
|
|
119
|
+
self.logger.debug("Added message to conversation history")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
self.logger.warning(f"Could not add message to history: {e}")
|
|
122
|
+
|
|
123
|
+
# Add to session feed for WebView
|
|
124
|
+
if hasattr(self, 'orchestrator') and self.orchestrator:
|
|
125
|
+
try:
|
|
126
|
+
self.orchestrator.session_manager.add_to_feed(
|
|
127
|
+
self.session_id,
|
|
128
|
+
'agent_message',
|
|
129
|
+
{
|
|
130
|
+
'text': params.text,
|
|
131
|
+
'wait_for_response': params.wait_for_response,
|
|
132
|
+
'timestamp': time.time()
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
self.logger.debug(f"Could not add to feed: {e}")
|
|
137
|
+
|
|
138
|
+
# P1a outbound-collapse: mirror the discrete message into the unified
|
|
139
|
+
# MessageRouter seam (committed/partial=False) when a router + chat
|
|
140
|
+
# session_key are bound (later phases). No-op + fail-open otherwise.
|
|
141
|
+
try:
|
|
142
|
+
from core.surfaces.outbound_mirror import build_discrete_publish
|
|
143
|
+
_mirror = build_discrete_publish(
|
|
144
|
+
getattr(self.orchestrator, "_message_router", None),
|
|
145
|
+
getattr(self.orchestrator, "_chat_session_key", None),
|
|
146
|
+
)
|
|
147
|
+
await _mirror(params.text)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
self.logger.debug(f"Could not mirror message to router: {e}")
|
|
150
|
+
|
|
151
|
+
# CONTINUOUS CHAT MODE: If waiting for response, stop execution
|
|
152
|
+
# User will reply via continuous chat, session will resume with their response
|
|
153
|
+
if params.wait_for_response:
|
|
154
|
+
# P3 (Task 3.2): guard the wait_for_response deadlock. If a chat surface
|
|
155
|
+
# is bound AND it cannot collect a reply (supports_interactive_ask=False),
|
|
156
|
+
# pausing the session would hang it forever. Degrade to a non-blocking
|
|
157
|
+
# notify in that case. Unknown/unbound (the legacy default) -> pause as
|
|
158
|
+
# before, so flag-OFF behavior is byte-identical. Lazy import: this file
|
|
159
|
+
# must never get `from __future__ import annotations` (registry closures).
|
|
160
|
+
try:
|
|
161
|
+
from core.surfaces.binding import surface_ask_capability
|
|
162
|
+
_ask_ok = surface_ask_capability(getattr(self, 'orchestrator', None))
|
|
163
|
+
except Exception:
|
|
164
|
+
_ask_ok = None
|
|
165
|
+
if _ask_ok is False:
|
|
166
|
+
self.logger.info("💬 Message sent. Surface can't collect a reply; not pausing (non-blocking notify).")
|
|
167
|
+
return ActionResult(
|
|
168
|
+
extracted_content="Message sent to user. This surface cannot collect a reply, so the task was not paused.",
|
|
169
|
+
include_in_memory=True,
|
|
170
|
+
metadata={'conversational_reply': True}
|
|
171
|
+
)
|
|
172
|
+
self.logger.info("💬 Message sent. Stopping execution to wait for user response via continuous chat")
|
|
173
|
+
return ActionResult(
|
|
174
|
+
extracted_content=f"Message sent to user. Task paused - will resume when user responds.",
|
|
175
|
+
include_in_memory=True,
|
|
176
|
+
is_done=True # Stop execution, wait for user via continuous chat
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Non-blocking message sent.
|
|
180
|
+
# R1: tag it so the run loop can recognize a turn whose only output was a
|
|
181
|
+
# user-facing reply (no productive tool work) and end it — otherwise a
|
|
182
|
+
# chat reply loops and re-greets, since non-blocking sends keep is_done False.
|
|
183
|
+
return ActionResult(
|
|
184
|
+
extracted_content=f"Message sent to user (non-blocking)",
|
|
185
|
+
include_in_memory=False,
|
|
186
|
+
metadata={'conversational_reply': True}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
@self.registry.action('Complete the current task and provide final output', param_model=DoneAction)
|
|
190
|
+
async def done(params: DoneAction, execution_context=None):
|
|
191
|
+
"""Complete the current task.
|
|
192
|
+
|
|
193
|
+
SUB-AGENT ISOLATION: Sub-agents skip adding to main context and feed.
|
|
194
|
+
Their completion is captured in the result and returned to parent.
|
|
195
|
+
"""
|
|
196
|
+
import time
|
|
197
|
+
|
|
198
|
+
# Check if this is a sub-agent (skip side effects for isolation)
|
|
199
|
+
is_sub_agent = execution_context.is_sub_agent if execution_context else False
|
|
200
|
+
completion_msg = params.text or "Task completed"
|
|
201
|
+
|
|
202
|
+
if is_sub_agent:
|
|
203
|
+
# Sub-agent: Just return done - output captured via normal result flow
|
|
204
|
+
self.logger.debug(f"✅ Sub-agent done: {completion_msg[:100]}...")
|
|
205
|
+
return ActionResult(is_done=True, extracted_content=completion_msg)
|
|
206
|
+
|
|
207
|
+
# Main agent: Full processing with todos check and feed updates
|
|
208
|
+
# Opportunistically check todos if TaskTool available
|
|
209
|
+
task_tool = self.get_tool('task')
|
|
210
|
+
if task_tool and hasattr(task_tool, '_get_or_create_todo_manager'):
|
|
211
|
+
try:
|
|
212
|
+
todo_mgr = task_tool._get_or_create_todo_manager(
|
|
213
|
+
self.session_id,
|
|
214
|
+
getattr(self, 'user_id', None)
|
|
215
|
+
)
|
|
216
|
+
if todo_mgr:
|
|
217
|
+
progress = todo_mgr.get_progress()
|
|
218
|
+
if progress['total'] > 0 and progress['percentage'] < 80:
|
|
219
|
+
warning_msg = (
|
|
220
|
+
f"⚠️ WARNING: Completing with only {progress['percentage']:.0f}% "
|
|
221
|
+
f"todos done ({progress['completed']}/{progress['total']}). "
|
|
222
|
+
f"Consider completing more todos for thorough task completion."
|
|
223
|
+
)
|
|
224
|
+
self.logger.warning(warning_msg)
|
|
225
|
+
return ActionResult(
|
|
226
|
+
is_done=True,
|
|
227
|
+
extracted_content=f"{params.text}\n\n{warning_msg}"
|
|
228
|
+
)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
self.logger.debug(f"Could not check todo progress: {e}")
|
|
231
|
+
# Continue anyway - don't block completion
|
|
232
|
+
|
|
233
|
+
# Add completion message to conversation history - find correct agent
|
|
234
|
+
if hasattr(self, 'orchestrator') and self.orchestrator:
|
|
235
|
+
target_agent = None
|
|
236
|
+
agent_id = execution_context.agent_id if execution_context else None
|
|
237
|
+
|
|
238
|
+
if agent_id and agent_id in self.orchestrator.agents:
|
|
239
|
+
target_agent = self.orchestrator.agents[agent_id]
|
|
240
|
+
else:
|
|
241
|
+
# Fallback: use first agent (legacy behavior)
|
|
242
|
+
agents = list(self.orchestrator.agents.values())
|
|
243
|
+
if agents:
|
|
244
|
+
target_agent = agents[0]
|
|
245
|
+
|
|
246
|
+
if target_agent and hasattr(target_agent, 'message_manager'):
|
|
247
|
+
try:
|
|
248
|
+
target_agent.message_manager._add_message_with_tokens(
|
|
249
|
+
AIMessage(content=f"✅ Task Complete\n\n{completion_msg}"),
|
|
250
|
+
_internal=True # Bypass single-writer enforcement for display messages
|
|
251
|
+
)
|
|
252
|
+
self.logger.info("Added completion message to conversation history")
|
|
253
|
+
except Exception as e:
|
|
254
|
+
self.logger.warning(f"Could not add completion to history: {e}")
|
|
255
|
+
|
|
256
|
+
# Add to session feed for WebView
|
|
257
|
+
try:
|
|
258
|
+
self.orchestrator.session_manager.add_to_feed(
|
|
259
|
+
self.session_id,
|
|
260
|
+
'task_complete',
|
|
261
|
+
{
|
|
262
|
+
'message': completion_msg,
|
|
263
|
+
'timestamp': time.time()
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
self.logger.debug(f"Could not add completion to feed: {e}")
|
|
268
|
+
|
|
269
|
+
# C10: mirror the completion text into the unified MessageRouter seam so a
|
|
270
|
+
# bound chat surface (Singular Chat) delivers it once — matching send_message.
|
|
271
|
+
# Without this, the harness skipping its post-run deliver for bound sessions
|
|
272
|
+
# would silence done()-terminated turns (done doesn't otherwise reach the
|
|
273
|
+
# router). No-op + fail-open when unbound (build_discrete_publish → no router).
|
|
274
|
+
try:
|
|
275
|
+
from core.surfaces.outbound_mirror import build_discrete_publish
|
|
276
|
+
_mirror = build_discrete_publish(
|
|
277
|
+
getattr(self.orchestrator, "_message_router", None),
|
|
278
|
+
getattr(self.orchestrator, "_chat_session_key", None),
|
|
279
|
+
)
|
|
280
|
+
await _mirror(completion_msg)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
self.logger.debug(f"Could not mirror completion to router: {e}")
|
|
283
|
+
|
|
284
|
+
self.logger.info(f'Task marked as done: {completion_msg}')
|
|
285
|
+
return ActionResult(is_done=True, extracted_content=completion_msg)
|
|
286
|
+
|
|
287
|
+
elif self.output_model is not None:
|
|
288
|
+
# For agents with custom output models
|
|
289
|
+
@self.registry.action('Complete the current task with structured output', param_model=self.output_model)
|
|
290
|
+
async def done(_params: BaseModel):
|
|
291
|
+
self.logger.info(f'Task marked as done with structured output')
|
|
292
|
+
return ActionResult(is_done=True)
|
|
293
|
+
|
|
294
|
+
# S-1: progressive skill disclosure. When enabled, the system injects only a
|
|
295
|
+
# compact <skill-catalog>; the agent pulls a skill's full body on demand with
|
|
296
|
+
# this tool. Off by default (skills are eager-injected full-body, no tool).
|
|
297
|
+
from agents.task.constants import skill_progressive_disclosure
|
|
298
|
+
if skill_progressive_disclosure():
|
|
299
|
+
class LoadSkillAction(BaseModel):
|
|
300
|
+
skill_id: str
|
|
301
|
+
|
|
302
|
+
@self.registry.action(
|
|
303
|
+
"Load a skill's full instructions on demand (see <skill-catalog>). "
|
|
304
|
+
"Call before doing work the skill covers.",
|
|
305
|
+
param_model=LoadSkillAction,
|
|
306
|
+
)
|
|
307
|
+
async def load_skill(params: LoadSkillAction, execution_context=None):
|
|
308
|
+
# Task 17: resolve the skill's on-disk dir (fail-open to None — an
|
|
309
|
+
# in-memory-only skill or a lookup error just means no resource list
|
|
310
|
+
# is advertised, not a broken load_skill).
|
|
311
|
+
skill_dir = None
|
|
312
|
+
uid = getattr(execution_context, 'user_id', None) or getattr(self, 'user_id', None)
|
|
313
|
+
_skill_manager = None
|
|
314
|
+
try:
|
|
315
|
+
from agents.task.agent.skill_manager import get_skill_manager
|
|
316
|
+
_skill_manager = get_skill_manager()
|
|
317
|
+
skill_dir = _skill_manager.resolve_skill_dir(params.skill_id, uid)
|
|
318
|
+
except Exception:
|
|
319
|
+
skill_dir = None
|
|
320
|
+
result = build_load_skill_result(
|
|
321
|
+
getattr(self, '_session_skills', {}), params.skill_id,
|
|
322
|
+
activated=getattr(self, '_activated_skills', None),
|
|
323
|
+
skill_dir=skill_dir,
|
|
324
|
+
)
|
|
325
|
+
# SK-F1: a skill that never made it into (or was capped out of)
|
|
326
|
+
# _session_skills is still on disk — fall back to a tenant-scoped
|
|
327
|
+
# disk load via the manager's existing resolver (excludes
|
|
328
|
+
# `.pending/` drafts, since those live at a different path)
|
|
329
|
+
# instead of erroring on every skill the eager/catalog pass didn't
|
|
330
|
+
# preload. Only attempted on the "unknown id" failure shape.
|
|
331
|
+
if result.error and _skill_manager is not None:
|
|
332
|
+
try:
|
|
333
|
+
sid = (params.skill_id or "").strip().strip('"')
|
|
334
|
+
# SK-F1: sid is LLM-controlled and _load_skill_content does no
|
|
335
|
+
# traversal filtering of its own (it just Path-joins). Gate the
|
|
336
|
+
# fallback on resolve_skill_dir's existing guard (rejects '/',
|
|
337
|
+
# '\\', '..', whitespace-padding, absolute-path-shaped ids) so a
|
|
338
|
+
# crafted id like "../other_uid/secret-skill" cannot escape into
|
|
339
|
+
# another tenant's directory. A rejected id falls through with
|
|
340
|
+
# content="" -> same "Unknown skill_id" error shape as before,
|
|
341
|
+
# no exception leaks to the model.
|
|
342
|
+
content = ""
|
|
343
|
+
# P1-1: make the auto_activate gate REAL on the load path. A gated
|
|
344
|
+
# skill (auto_activate:false — the money/trading playbooks) is only
|
|
345
|
+
# loadable when the session has actually loaded its required tools;
|
|
346
|
+
# otherwise refuse (same "Unknown skill_id" shape) so a model that
|
|
347
|
+
# guesses the id cannot pull the gated playbook. A legitimately-
|
|
348
|
+
# available gated skill is preloaded into _session_skills via the
|
|
349
|
+
# catalog and never reaches this fallback.
|
|
350
|
+
_loaded_tool_ids = []
|
|
351
|
+
try:
|
|
352
|
+
_loaded_tool_ids = self.list_tools()
|
|
353
|
+
except Exception:
|
|
354
|
+
_loaded_tool_ids = []
|
|
355
|
+
_gate_ok = True
|
|
356
|
+
try:
|
|
357
|
+
_gate_ok = _skill_manager.may_load_skill(sid, tool_ids=_loaded_tool_ids, user_id=uid)
|
|
358
|
+
except Exception:
|
|
359
|
+
_gate_ok = True # fail-open on the gate check; path/tenant guards still apply
|
|
360
|
+
if _gate_ok and _skill_manager.resolve_skill_dir(sid, uid) is not None:
|
|
361
|
+
content = _skill_manager._load_skill_content(sid, user_id=uid)
|
|
362
|
+
if content:
|
|
363
|
+
from agents.task.agent.skill_manager import MatchedSkill
|
|
364
|
+
fallback_skill = MatchedSkill(
|
|
365
|
+
skill_id=sid,
|
|
366
|
+
priority=0,
|
|
367
|
+
match_reasons=["disk-fallback"],
|
|
368
|
+
content=content,
|
|
369
|
+
)
|
|
370
|
+
# P2-22: register the fallback-loaded skill into _session_skills
|
|
371
|
+
# so a follow-up read_skill_resource (which gates on membership
|
|
372
|
+
# there) can serve the resources this load result advertises —
|
|
373
|
+
# previously it refused "Call load_skill first" for a skill the
|
|
374
|
+
# model had JUST loaded via the disk fallback.
|
|
375
|
+
_ss = getattr(self, '_session_skills', None)
|
|
376
|
+
if isinstance(_ss, dict):
|
|
377
|
+
_ss[sid] = fallback_skill
|
|
378
|
+
result = build_load_skill_result(
|
|
379
|
+
{sid: fallback_skill}, sid,
|
|
380
|
+
activated=getattr(self, '_activated_skills', None),
|
|
381
|
+
skill_dir=skill_dir,
|
|
382
|
+
)
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
if result.metadata and result.metadata.get('skill_loaded'):
|
|
386
|
+
self.logger.info(f"📖 Loaded skill on demand: {result.metadata['skill_loaded']}")
|
|
387
|
+
# W2-D: provenance/reuse metric — bumped at the closure (which has
|
|
388
|
+
# execution_context), NOT the pure build_load_skill_result helper.
|
|
389
|
+
# Fail-open: a metrics write must never break a skill load.
|
|
390
|
+
try:
|
|
391
|
+
uid = getattr(execution_context, 'user_id', None) or getattr(self, 'user_id', None)
|
|
392
|
+
if uid:
|
|
393
|
+
from modules.skills.skill_usage import get_skill_usage_store
|
|
394
|
+
# SK-F1: use the actually-resolved/loaded id
|
|
395
|
+
# (result.metadata['skill_loaded'] is always the
|
|
396
|
+
# stripped/unquoted sid -- set identically by
|
|
397
|
+
# build_load_skill_result on both the eager and
|
|
398
|
+
# disk-fallback paths), not the raw params.skill_id,
|
|
399
|
+
# so quote/whitespace-padded ids don't fragment
|
|
400
|
+
# curator metrics under two different keys. The enclosing
|
|
401
|
+
# `if ... get('skill_loaded')` guarantees this is truthy
|
|
402
|
+
# (D1: the old `or params.skill_id` fallback was dead).
|
|
403
|
+
loaded_id = result.metadata['skill_loaded']
|
|
404
|
+
get_skill_usage_store().bump_load(loaded_id, uid)
|
|
405
|
+
except Exception:
|
|
406
|
+
pass
|
|
407
|
+
return result
|
|
408
|
+
|
|
409
|
+
# Task 17: read-only resource access for an already-loaded skill (references/
|
|
410
|
+
# assets, e.g. an authored skill's REFERENCE.md). NEVER executes anything —
|
|
411
|
+
# a skill's scripts/*.sh are readable as text, never run. Gated by the same
|
|
412
|
+
# SKILL_PROGRESSIVE_DISCLOSURE flag as load_skill since it's only useful once
|
|
413
|
+
# _session_skills is populated (refusal is a no-op otherwise).
|
|
414
|
+
class ReadSkillResourceAction(BaseModel):
|
|
415
|
+
skill_id: str
|
|
416
|
+
resource_path: str
|
|
417
|
+
|
|
418
|
+
@self.registry.action(
|
|
419
|
+
"Read a resource file (reference doc, asset, etc.) that belongs to a skill "
|
|
420
|
+
"you have already loaded via load_skill (see metadata.skill_resources on that "
|
|
421
|
+
"call's result for the available paths). Read-only — this NEVER executes "
|
|
422
|
+
"scripts or other files, it only returns their text content.",
|
|
423
|
+
param_model=ReadSkillResourceAction,
|
|
424
|
+
)
|
|
425
|
+
async def read_skill_resource(params: ReadSkillResourceAction, execution_context=None):
|
|
426
|
+
session_skills = getattr(self, '_session_skills', {}) or {}
|
|
427
|
+
sid = (params.skill_id or "").strip().strip('"')
|
|
428
|
+
if sid not in session_skills:
|
|
429
|
+
available = ", ".join(session_skills.keys()) or "(none)"
|
|
430
|
+
return ActionResult(
|
|
431
|
+
error=(
|
|
432
|
+
f"Skill '{sid}' is not loaded this session (available: {available}). "
|
|
433
|
+
"Call load_skill first."
|
|
434
|
+
),
|
|
435
|
+
include_in_memory=True,
|
|
436
|
+
)
|
|
437
|
+
skill_dir = None
|
|
438
|
+
try:
|
|
439
|
+
from agents.task.agent.skill_manager import get_skill_manager
|
|
440
|
+
uid = getattr(execution_context, 'user_id', None) or getattr(self, 'user_id', None)
|
|
441
|
+
skill_dir = get_skill_manager().resolve_skill_dir(sid, uid)
|
|
442
|
+
except Exception:
|
|
443
|
+
skill_dir = None
|
|
444
|
+
if skill_dir is None:
|
|
445
|
+
return ActionResult(
|
|
446
|
+
error=f"Skill '{sid}' has no on-disk resources available.",
|
|
447
|
+
include_in_memory=True,
|
|
448
|
+
)
|
|
449
|
+
ok, content = read_skill_resource_confined(skill_dir, params.resource_path)
|
|
450
|
+
if not ok:
|
|
451
|
+
return ActionResult(error=content, include_in_memory=True)
|
|
452
|
+
return ActionResult(extracted_content=content, include_in_memory=True)
|
|
453
|
+
|
|
454
|
+
# P0-1: agent-callable cross-session recall. Registered only when an external
|
|
455
|
+
# memory provider is active (MEMORY_BACKEND=sqlite); inert in the default config
|
|
456
|
+
# (NullMemoryProvider), so the tool never appears unless a backend is configured.
|
|
457
|
+
self._register_session_search_action()
|
|
458
|
+
|
|
459
|
+
# UP-09: optional bounded curated memory tool (read/add/remove), gated
|
|
460
|
+
# MEMORY_TOOL_ENABLED (default false) AND external provider active.
|
|
461
|
+
self._register_memory_tool_action()
|
|
462
|
+
|
|
463
|
+
# Task 4: agent-callable recall of the agent's OWN activity (chat/goal/cron
|
|
464
|
+
# episodes), gated EPISODIC_MEMORY_ENABLED AND external provider active.
|
|
465
|
+
self._register_recent_activity_action()
|
|
466
|
+
|
|
467
|
+
# W2: optional writable-skills tool (create/patch/delete), gated SKILLS_WRITABLE
|
|
468
|
+
# (default false). Lets the agent author durable procedures; quarantined to
|
|
469
|
+
# .pending/ + threat-scanned + tenant-confined.
|
|
470
|
+
self._register_skill_manage_action()
|
|
471
|
+
|
|
472
|
+
# polyrob C-write: optional evolving SELF-identity tool, gated
|
|
473
|
+
# SELF_CONTEXT_WRITABLE (default false; ON under POLYROB_LOCAL). Lets the agent
|
|
474
|
+
# refine its per-(instance,user) self.md; identity-scanned + .pending +
|
|
475
|
+
# tenant-confined; SOUL tier stays operator-only.
|
|
476
|
+
self._register_self_context_manage_action()
|
|
477
|
+
|
|
478
|
+
# Bounded owner-facts doc (USER.md-equivalent), gated OWNER_DOC_WRITABLE
|
|
479
|
+
# (default false; ON under POLYROB_LOCAL). Same quarantine-then-promote model.
|
|
480
|
+
self._register_owner_doc_manage_action()
|
|
481
|
+
|
|
482
|
+
# W7: optional read-only insights tool (authored-skill reuse %), gated
|
|
483
|
+
# INSIGHTS_TOOL (default false). Tenant-scoped.
|
|
484
|
+
self._register_insights_action()
|
|
485
|
+
|
|
486
|
+
# T3-01: agent-initiated MCP install from the reviewed catalog, gated
|
|
487
|
+
# MCP_SELF_INSTALL_ENABLED (default false). Owner-in-the-loop: forged/leaf
|
|
488
|
+
# turns refused; approver is Deny-by-default unless APPROVAL_PROVIDER is set.
|
|
489
|
+
self._register_mcp_install_action()
|
|
490
|
+
|
|
491
|
+
# Ensure normalize_path method exists for file operations
|
|
492
|
+
self._ensure_normalize_path_exists()
|
|
493
|
+
|
|
494
|
+
# Task 5: gated `message` action (owner/allowlist -> MessageRouter send),
|
|
495
|
+
# gated MESSAGE_TOOL_ENABLED (default false; ON under POLYROB_LOCAL).
|
|
496
|
+
self._register_message_action()
|
|
497
|
+
|
|
498
|
+
# Register subtask action for sub-agent delegation
|
|
499
|
+
self._register_subtask_action()
|
|
500
|
+
|
|
501
|
+
def _register_session_search_action(self):
|
|
502
|
+
"""Register `session_search` when a cross-session memory backend is active.
|
|
503
|
+
|
|
504
|
+
Gives the agent on-demand recall over its own past sessions (tenant-scoped via
|
|
505
|
+
user_id, P0-0). Mirrors Reference's agent-callable session_search. Fail-open and
|
|
506
|
+
only advertised when an external provider is registered, so the default
|
|
507
|
+
NullMemoryProvider config is byte-identical (no new tool surfaced).
|
|
508
|
+
"""
|
|
509
|
+
try:
|
|
510
|
+
from modules.memory.registry import get_memory_registry
|
|
511
|
+
provider = get_memory_registry().active()
|
|
512
|
+
if provider is None or not getattr(provider, 'is_external', False):
|
|
513
|
+
return # no external backend -> don't surface the tool
|
|
514
|
+
except Exception:
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
class SessionSearchAction(BaseModel):
|
|
518
|
+
query: str = "" # empty => browse most-recent
|
|
519
|
+
limit: int = 5 # provider clamps to [1, 20]
|
|
520
|
+
sort: Optional[str] = None # "newest" | "oldest" | None (rank)
|
|
521
|
+
collection: Optional[str] = None # set to search a named knowledge-base collection instead of past sessions
|
|
522
|
+
|
|
523
|
+
_SEARCH_DESC = (
|
|
524
|
+
"Recall your durable memory of PAST sessions. WHEN TO USE: reach for this "
|
|
525
|
+
"BEFORE web/filesystem on 'what did we do about X', 'where did we leave Y', "
|
|
526
|
+
"or 'what was I working on'. Two shapes: pass a `query` to DISCOVER relevant "
|
|
527
|
+
"past work (facts, decisions); leave `query` empty to BROWSE your most-recent "
|
|
528
|
+
"sessions. Optional `limit` (1-20) and `sort` ('newest'/'oldest'). "
|
|
529
|
+
"Optional `collection`: set to search a named knowledge-base collection instead "
|
|
530
|
+
"of past sessions (requires KB to be enabled)."
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
def _kb_on() -> bool:
|
|
534
|
+
try:
|
|
535
|
+
from agents.task.constants import AutonomyConfig
|
|
536
|
+
return AutonomyConfig.kb_enabled()
|
|
537
|
+
except Exception:
|
|
538
|
+
return False
|
|
539
|
+
|
|
540
|
+
async def _run_session_search(params: SessionSearchAction, execution_context=None) -> ActionResult:
|
|
541
|
+
# Tenant scoping + empty-user_id refusal are enforced in the provider
|
|
542
|
+
# (_anon_blocked, UP-03); the controller does NOT re-read MEMORY_REQUIRE_USER_ID.
|
|
543
|
+
user_id = getattr(execution_context, 'user_id', None) or getattr(self, 'user_id', None)
|
|
544
|
+
session_id = getattr(execution_context, 'session_id', None) or getattr(self, 'session_id', '')
|
|
545
|
+
|
|
546
|
+
# Task 7: KB recall path — routes to kb_search when collection is set and KB is enabled.
|
|
547
|
+
# Fail-open: any exception falls through to the normal memory_search path below.
|
|
548
|
+
if params.collection and _kb_on():
|
|
549
|
+
try:
|
|
550
|
+
from modules.memory.registry import kb_search
|
|
551
|
+
recalled = await kb_search(
|
|
552
|
+
params.query, user_id=user_id,
|
|
553
|
+
collection=params.collection, limit=params.limit,
|
|
554
|
+
)
|
|
555
|
+
if not recalled or not recalled.strip():
|
|
556
|
+
return ActionResult(
|
|
557
|
+
extracted_content=f"No knowledge-base results for {params.query!r} in {params.collection!r}",
|
|
558
|
+
include_in_memory=True,
|
|
559
|
+
)
|
|
560
|
+
try:
|
|
561
|
+
from agents.task.agent.core.untrusted_wrap import wrap_untrusted
|
|
562
|
+
recalled = wrap_untrusted("knowledge_base", recalled)
|
|
563
|
+
except Exception:
|
|
564
|
+
pass # fail-open
|
|
565
|
+
return ActionResult(
|
|
566
|
+
extracted_content=f"## Recalled from knowledge base\n{recalled}",
|
|
567
|
+
include_in_memory=True,
|
|
568
|
+
)
|
|
569
|
+
except Exception as e:
|
|
570
|
+
self.logger.debug(f"kb_search failed, falling back to memory_search: {e}")
|
|
571
|
+
# fall through to normal session memory path
|
|
572
|
+
|
|
573
|
+
try:
|
|
574
|
+
from modules.memory.registry import memory_search
|
|
575
|
+
recalled = await memory_search(
|
|
576
|
+
params.query, session_id=session_id, user_id=user_id,
|
|
577
|
+
limit=params.limit, sort=params.sort,
|
|
578
|
+
)
|
|
579
|
+
except Exception as e:
|
|
580
|
+
self.logger.debug(f"session_search failed: {e}")
|
|
581
|
+
return ActionResult(extracted_content="No memory available.", include_in_memory=False)
|
|
582
|
+
if not recalled or not recalled.strip():
|
|
583
|
+
label = params.query if params.query else "(recent sessions)"
|
|
584
|
+
return ActionResult(
|
|
585
|
+
extracted_content=f"No past-session memory matched: {label!r}",
|
|
586
|
+
include_in_memory=True,
|
|
587
|
+
)
|
|
588
|
+
# W6/UP-06: recalled cross-session content may carry indirect injection
|
|
589
|
+
# (it can include previously-ingested web/tool output) — frame it as DATA.
|
|
590
|
+
# session_search is NOT in the automatic untrusted-tool set, so wrap here.
|
|
591
|
+
try:
|
|
592
|
+
from agents.task.agent.core.untrusted_wrap import wrap_untrusted
|
|
593
|
+
recalled = wrap_untrusted("session_search", recalled)
|
|
594
|
+
except Exception:
|
|
595
|
+
pass # fail-open: never block recall on a wrap import error
|
|
596
|
+
header = ("## Recalled from past sessions" if params.query
|
|
597
|
+
else "## Your most-recent sessions")
|
|
598
|
+
return ActionResult(
|
|
599
|
+
extracted_content=f"{header}\n{recalled}",
|
|
600
|
+
include_in_memory=True,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
@self.registry.action(_SEARCH_DESC, param_model=SessionSearchAction)
|
|
604
|
+
async def session_search(params: SessionSearchAction, execution_context=None) -> ActionResult:
|
|
605
|
+
return await _run_session_search(params, execution_context)
|
|
606
|
+
|
|
607
|
+
# W6: Reference exposes the same recall under both names. Register `memory_search`
|
|
608
|
+
# as a thin alias (default-on via MEMORY_SEARCH_TOOL) so prompts/tools that use
|
|
609
|
+
# either shape resolve. Same handler, same tenant scoping — zero new surface.
|
|
610
|
+
try:
|
|
611
|
+
from agents.task.constants import AutonomyConfig
|
|
612
|
+
alias_on = AutonomyConfig.memory_search_tool()
|
|
613
|
+
except Exception:
|
|
614
|
+
alias_on = True
|
|
615
|
+
if alias_on:
|
|
616
|
+
@self.registry.action(_SEARCH_DESC, param_model=SessionSearchAction)
|
|
617
|
+
async def memory_search(params: SessionSearchAction, execution_context=None) -> ActionResult:
|
|
618
|
+
return await _run_session_search(params, execution_context)
|
|
619
|
+
|
|
620
|
+
def _register_recent_activity_action(self):
|
|
621
|
+
"""Register `recent_activity`: time-ordered recall of the agent's OWN runs
|
|
622
|
+
(chat/goal/cron), tenant-scoped. Answers 'what did I do since X'. Only surfaced
|
|
623
|
+
when EPISODIC_MEMORY_ENABLED and an external provider is active (byte-identical
|
|
624
|
+
default config otherwise). NO `from __future__` in this module (registry-closure
|
|
625
|
+
landmine)."""
|
|
626
|
+
try:
|
|
627
|
+
from agents.task.constants import AutonomyConfig
|
|
628
|
+
if not AutonomyConfig.episodic_memory_enabled():
|
|
629
|
+
return
|
|
630
|
+
from modules.memory.registry import get_memory_registry
|
|
631
|
+
provider = get_memory_registry().active()
|
|
632
|
+
if provider is None or not getattr(provider, 'is_external', False):
|
|
633
|
+
return
|
|
634
|
+
except Exception:
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
class RecentActivityAction(BaseModel):
|
|
638
|
+
since: Optional[str] = None # "8h" | "2d" | "30m" | ISO8601; None => last 24h
|
|
639
|
+
until: Optional[str] = None
|
|
640
|
+
kind: Optional[str] = None # "chat" | "goal" | "cron"
|
|
641
|
+
limit: int = 20
|
|
642
|
+
|
|
643
|
+
_DESC = (
|
|
644
|
+
"Recall YOUR OWN recent activity — the goals, cron jobs, and chats you ran, "
|
|
645
|
+
"newest-first, with outcome and spend. USE THIS (not the filesystem, not notes) "
|
|
646
|
+
"for 'what did I do', 'what ran since X', 'what have I been up to'. Optional "
|
|
647
|
+
"`since` ('8h'/'2d'/ISO), `kind` ('goal'/'cron'/'chat'), `limit` (1-20)."
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
async def _run_recent_activity(params, execution_context=None) -> ActionResult:
|
|
651
|
+
from modules.memory.registry import memory_recall_episodes
|
|
652
|
+
from modules.memory.episodic import parse_since
|
|
653
|
+
import time as _t
|
|
654
|
+
user_id = getattr(execution_context, 'user_id', None) or getattr(self, 'user_id', None)
|
|
655
|
+
since_ts = (parse_since(params.since) if params.since else None) or (int(_t.time()) - 24 * 3600)
|
|
656
|
+
until_ts = parse_since(params.until) if params.until else None
|
|
657
|
+
rows = await memory_recall_episodes(
|
|
658
|
+
user_id=user_id, since_ts=since_ts, until_ts=until_ts,
|
|
659
|
+
kind=params.kind, limit=params.limit, order="newest")
|
|
660
|
+
if not rows:
|
|
661
|
+
return ActionResult(
|
|
662
|
+
extracted_content="No recorded activity in that window.",
|
|
663
|
+
include_in_memory=True)
|
|
664
|
+
lines, total = [], 0.0
|
|
665
|
+
for e in rows:
|
|
666
|
+
total += float(e.spend_usd or 0)
|
|
667
|
+
arts = ", ".join(a.get("path", "") for a in (e.artifacts or [])[:3])
|
|
668
|
+
lines.append(
|
|
669
|
+
f"- {e.kind}:{e.outcome or '?'} ${float(e.spend_usd or 0):.2f} "
|
|
670
|
+
f"\"{(e.task or '')[:60]}\"" + (f" -> {arts}" if arts else ""))
|
|
671
|
+
body = "\n".join(lines) + f"\n({len(rows)} runs, ${total:.2f} total)"
|
|
672
|
+
try:
|
|
673
|
+
from agents.task.agent.core.untrusted_wrap import wrap_untrusted
|
|
674
|
+
body = wrap_untrusted("recent_activity", body)
|
|
675
|
+
except Exception:
|
|
676
|
+
pass
|
|
677
|
+
return ActionResult(extracted_content=f"## Your recent activity\n{body}",
|
|
678
|
+
include_in_memory=True)
|
|
679
|
+
|
|
680
|
+
@self.registry.action(_DESC, param_model=RecentActivityAction)
|
|
681
|
+
async def recent_activity(params: RecentActivityAction, execution_context=None) -> ActionResult:
|
|
682
|
+
return await _run_recent_activity(params, execution_context)
|
|
683
|
+
|
|
684
|
+
def _register_memory_tool_action(self):
|
|
685
|
+
"""Register the bounded curated `memory` tool (UP-09) when enabled.
|
|
686
|
+
|
|
687
|
+
Gated on MEMORY_TOOL_ENABLED (default false) AND an external memory provider
|
|
688
|
+
with a curated store. Tenant scoping + empty-user_id refusal live in the
|
|
689
|
+
provider (UP-03 _anon_blocked). v1: read/add/remove only, no system-prompt
|
|
690
|
+
snapshot (keeps the cached prompt untouched, PR13 principle).
|
|
691
|
+
"""
|
|
692
|
+
if not _bool_env("MEMORY_TOOL_ENABLED", False):
|
|
693
|
+
return
|
|
694
|
+
try:
|
|
695
|
+
from modules.memory.registry import get_memory_registry
|
|
696
|
+
provider = get_memory_registry().active()
|
|
697
|
+
if provider is None or not getattr(provider, 'is_external', False):
|
|
698
|
+
return
|
|
699
|
+
if not hasattr(provider, 'curated_add'):
|
|
700
|
+
return # provider has no curated store
|
|
701
|
+
except Exception:
|
|
702
|
+
return
|
|
703
|
+
|
|
704
|
+
from typing import Literal as _Literal
|
|
705
|
+
|
|
706
|
+
class MemoryToolAction(BaseModel):
|
|
707
|
+
action: _Literal["read", "add", "remove"]
|
|
708
|
+
content: Optional[str] = None # required for add (entry) / remove (substring)
|
|
709
|
+
|
|
710
|
+
@self.registry.action(
|
|
711
|
+
"Your durable, curated long-term notes (separate from automatic session "
|
|
712
|
+
"recall). action='add' to save a short fact worth remembering across "
|
|
713
|
+
"sessions; action='read' to list your notes; action='remove' to delete "
|
|
714
|
+
"notes containing a substring. Keep entries short and factual.",
|
|
715
|
+
param_model=MemoryToolAction,
|
|
716
|
+
)
|
|
717
|
+
async def memory(params: MemoryToolAction, execution_context=None) -> ActionResult:
|
|
718
|
+
user_id = getattr(execution_context, 'user_id', None) or getattr(self, 'user_id', None)
|
|
719
|
+
from modules.memory.registry import get_memory_registry
|
|
720
|
+
prov = get_memory_registry().active()
|
|
721
|
+
if prov is None or not hasattr(prov, 'curated_add'):
|
|
722
|
+
return ActionResult(extracted_content="Memory tool unavailable.", include_in_memory=False)
|
|
723
|
+
if params.action == "read":
|
|
724
|
+
notes = await prov.curated_read(user_id)
|
|
725
|
+
if notes.strip():
|
|
726
|
+
# P1-7: curated notes are agent-authored (possibly under injection
|
|
727
|
+
# influence) and read back in FUTURE sessions — a durable persistence-
|
|
728
|
+
# laundering channel. Frame as untrusted DATA like every other recall
|
|
729
|
+
# surface (session_search/kb_search/recent_activity).
|
|
730
|
+
from agents.task.agent.core.untrusted_wrap import wrap_untrusted
|
|
731
|
+
content = f"## Your curated memory\n{wrap_untrusted('memory', notes)}"
|
|
732
|
+
else:
|
|
733
|
+
content = "Your curated memory is empty."
|
|
734
|
+
return ActionResult(extracted_content=content, include_in_memory=True)
|
|
735
|
+
# T4-02: curated writes shape the agent's own future recall — emit a
|
|
736
|
+
# first-class memory_write event so they are auditable (fail-open).
|
|
737
|
+
def _memory_write_ev(op: str, ok: bool, removed: int = 0):
|
|
738
|
+
try:
|
|
739
|
+
from agents.task.telemetry.memory_events import emit_memory_event
|
|
740
|
+
emit_memory_event(
|
|
741
|
+
"memory_write", user_id=user_id or "",
|
|
742
|
+
session_id=(getattr(execution_context, 'session_id', None)
|
|
743
|
+
or getattr(self, 'session_id', '') or ""),
|
|
744
|
+
source="memory_tool", scope="curated",
|
|
745
|
+
content=params.content or "", op=op, ok=ok, removed=removed)
|
|
746
|
+
except Exception:
|
|
747
|
+
pass
|
|
748
|
+
|
|
749
|
+
if params.action == "add":
|
|
750
|
+
ok = await prov.curated_add(user_id, params.content or "")
|
|
751
|
+
_memory_write_ev("add", ok)
|
|
752
|
+
return ActionResult(
|
|
753
|
+
extracted_content=("Saved to curated memory." if ok
|
|
754
|
+
else "Could not save (empty, over size/entry cap, or no tenant)."),
|
|
755
|
+
include_in_memory=True,
|
|
756
|
+
)
|
|
757
|
+
# remove
|
|
758
|
+
n = await prov.curated_remove(user_id, params.content or "")
|
|
759
|
+
_memory_write_ev("remove", n > 0, removed=n)
|
|
760
|
+
return ActionResult(
|
|
761
|
+
extracted_content=f"Removed {n} curated memory entr{'y' if n == 1 else 'ies'}.",
|
|
762
|
+
include_in_memory=True,
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
def _register_skill_manage_action(self):
|
|
766
|
+
"""Register the writable-skills `skill_manage` tool (W2), gated SKILLS_WRITABLE.
|
|
767
|
+
|
|
768
|
+
Lets the agent author/refine/retire durable skills. Default OFF. All safety is
|
|
769
|
+
enforced in SkillWriterMixin (tenant-confined writes, validators, threat-scan,
|
|
770
|
+
.pending/ quarantine, atomic write, archive-never-delete). A non-user-initiated
|
|
771
|
+
turn can never auto-activate a skill (forced quarantine). Registered in this
|
|
772
|
+
module — which deliberately omits `from __future__ import annotations` so the
|
|
773
|
+
Registry can introspect the closure's first-param model.
|
|
774
|
+
"""
|
|
775
|
+
try:
|
|
776
|
+
from agents.task.constants import AutonomyConfig
|
|
777
|
+
if not AutonomyConfig.skills_writable():
|
|
778
|
+
return
|
|
779
|
+
except Exception:
|
|
780
|
+
return
|
|
781
|
+
|
|
782
|
+
from typing import Literal as _Literal
|
|
783
|
+
|
|
784
|
+
class SkillManageAction(BaseModel):
|
|
785
|
+
action: _Literal["create", "patch", "delete", "promote"]
|
|
786
|
+
skill_id: str
|
|
787
|
+
content: Optional[str] = None # create: full SKILL.md body
|
|
788
|
+
old_string: Optional[str] = None # patch: exact text to replace
|
|
789
|
+
new_string: Optional[str] = None # patch: replacement
|
|
790
|
+
replace_all: bool = False
|
|
791
|
+
description: str = ""
|
|
792
|
+
|
|
793
|
+
@self.registry.action(
|
|
794
|
+
"Author your own durable SKILLS (procedures you can reload in future "
|
|
795
|
+
"sessions). action='create' writes a new SKILL.md (markdown, starts with a "
|
|
796
|
+
"# heading); action='patch' edits one by exact-string replace; "
|
|
797
|
+
"action='delete' archives one; action='promote' activates a pending draft "
|
|
798
|
+
"(owner-only — only the bound owner principal can promote; neither a "
|
|
799
|
+
"background/sub-agent turn nor a non-owner turn can self-promote). New/edited "
|
|
800
|
+
"skills are quarantined for review before they activate. Use to capture a "
|
|
801
|
+
"reusable procedure you just worked out.",
|
|
802
|
+
param_model=SkillManageAction,
|
|
803
|
+
)
|
|
804
|
+
async def skill_manage(params: SkillManageAction, execution_context=None) -> ActionResult:
|
|
805
|
+
user_id = getattr(execution_context, 'user_id', None) or getattr(self, 'user_id', None)
|
|
806
|
+
if not user_id:
|
|
807
|
+
return ActionResult(error="skill authoring requires a user (tenant scope).",
|
|
808
|
+
include_in_memory=True)
|
|
809
|
+
|
|
810
|
+
# T4-06: every effected skill mutation records a first-class
|
|
811
|
+
# self_modification event (durable log → /telemetry + /activity). Fail-open.
|
|
812
|
+
def _self_mod_ev(action: str, *, pending=None, created_by: str = "", ok: bool = True):
|
|
813
|
+
try:
|
|
814
|
+
from agents.task.telemetry.self_events import emit_self_modification
|
|
815
|
+
emit_self_modification(
|
|
816
|
+
kind="skill", action=action, item_id=params.skill_id,
|
|
817
|
+
user_id=user_id or "",
|
|
818
|
+
session_id=(getattr(execution_context, 'session_id', None)
|
|
819
|
+
or getattr(self, 'session_id', '') or ""),
|
|
820
|
+
pending=pending, created_by=created_by,
|
|
821
|
+
source="skill_manage", ok=ok)
|
|
822
|
+
except Exception:
|
|
823
|
+
pass
|
|
824
|
+
# A forged (non-user-initiated) turn must never auto-activate a skill. The
|
|
825
|
+
# RELIABLE signal is the execution context: the background-review reviewer
|
|
826
|
+
# (and any delegated worker) runs as a sub-agent / leaf role — those fields
|
|
827
|
+
# ARE set in _build_execution_context. A self-wake/delegation-result
|
|
828
|
+
# re-entry into the MAIN agent is additionally detected via
|
|
829
|
+
# `execution_context.metadata['turn_kind']` being one of `FORGED_TURN_KINDS`
|
|
830
|
+
# (SK-F10, stamped live from the drained HITL message kind — see
|
|
831
|
+
# `_is_forged_or_autonomous_turn` above). A sub-agent/leaf author → background_review (ALWAYS
|
|
832
|
+
# quarantined); a normal interactive main-agent turn → agent (follows
|
|
833
|
+
# SKILLS_WRITABLE_REQUIRE_REVIEW).
|
|
834
|
+
from agents.task.agent.skill_writer import PROVENANCE_AGENT, PROVENANCE_BACKGROUND
|
|
835
|
+
# C7: fold autonomous (goal/cron) runs into "forged" so they can't
|
|
836
|
+
# auto-activate or self-promote a skill either.
|
|
837
|
+
is_forged = _is_forged_or_autonomous_turn(execution_context, self)
|
|
838
|
+
created_by = PROVENANCE_BACKGROUND if is_forged else PROVENANCE_AGENT
|
|
839
|
+
try:
|
|
840
|
+
from agents.task.agent.skill_manager import get_skill_manager
|
|
841
|
+
sm = get_skill_manager()
|
|
842
|
+
if params.action == "promote":
|
|
843
|
+
# Owner-gated (T3-02): promoting a pending draft into ACTIVE content
|
|
844
|
+
# is the entire security boundary of the writable-skills quarantine —
|
|
845
|
+
# an active skill auto-activates in future sessions. A forged turn is
|
|
846
|
+
# blocked, AND (mirroring self_context_manage promote, permissions
|
|
847
|
+
# audit F4) a non-owner genuine turn is blocked too: otherwise the
|
|
848
|
+
# agent could `create` (-> .pending under REQUIRE_REVIEW) then
|
|
849
|
+
# `promote` in the SAME turn, activating a body the owner never saw —
|
|
850
|
+
# e.g. an injected "author skill X and promote it" from fetched content.
|
|
851
|
+
# is_owner_local_safe is the surface-independent owner check (a
|
|
852
|
+
# forgeable network sender's uid is never the local tenant).
|
|
853
|
+
owner_ok = False
|
|
854
|
+
try:
|
|
855
|
+
from agents.task.constants import local_mode_enabled
|
|
856
|
+
from core.instance import is_owner_local_safe, resolve_owner_principal
|
|
857
|
+
owner_ok = is_owner_local_safe(
|
|
858
|
+
user_id, owner_principal=resolve_owner_principal(),
|
|
859
|
+
local_enabled=local_mode_enabled())
|
|
860
|
+
except Exception:
|
|
861
|
+
owner_ok = False
|
|
862
|
+
if is_forged or not owner_ok:
|
|
863
|
+
return ActionResult(
|
|
864
|
+
error="promote is owner-only; your pending draft awaits operator review.",
|
|
865
|
+
include_in_memory=True)
|
|
866
|
+
res = sm.promote_pending_skill(params.skill_id, user_id=user_id,
|
|
867
|
+
description=params.description)
|
|
868
|
+
if not res.ok:
|
|
869
|
+
return ActionResult(error=f"promote failed: {'; '.join(res.errors)}",
|
|
870
|
+
include_in_memory=True)
|
|
871
|
+
_self_mod_ev("promote", pending=False, created_by="owner")
|
|
872
|
+
return ActionResult(
|
|
873
|
+
extracted_content=f"Skill `{params.skill_id}` promoted (active next session).",
|
|
874
|
+
include_in_memory=True)
|
|
875
|
+
if params.action == "create":
|
|
876
|
+
if not params.content:
|
|
877
|
+
return ActionResult(error="create requires `content`.", include_in_memory=True)
|
|
878
|
+
res = sm.create_skill(params.skill_id, params.content, user_id=user_id,
|
|
879
|
+
description=params.description, created_by=created_by)
|
|
880
|
+
elif params.action == "patch":
|
|
881
|
+
if params.old_string is None or params.new_string is None:
|
|
882
|
+
return ActionResult(error="patch requires `old_string` and `new_string`.",
|
|
883
|
+
include_in_memory=True)
|
|
884
|
+
res = sm.patch_skill(params.skill_id, user_id=user_id,
|
|
885
|
+
old_string=params.old_string, new_string=params.new_string,
|
|
886
|
+
replace_all=params.replace_all, created_by=created_by)
|
|
887
|
+
else: # delete
|
|
888
|
+
ok = sm.delete_skill(params.skill_id, user_id=user_id, created_by=created_by)
|
|
889
|
+
if ok:
|
|
890
|
+
_self_mod_ev("delete", created_by=created_by)
|
|
891
|
+
return ActionResult(
|
|
892
|
+
extracted_content=(f"Archived skill `{params.skill_id}`." if ok
|
|
893
|
+
else f"No skill `{params.skill_id}` to archive."),
|
|
894
|
+
include_in_memory=True,
|
|
895
|
+
)
|
|
896
|
+
except Exception as e:
|
|
897
|
+
self.logger.debug(f"skill_manage failed: {e}")
|
|
898
|
+
return ActionResult(error=f"skill_manage failed: {e}", include_in_memory=True)
|
|
899
|
+
|
|
900
|
+
if not res.ok:
|
|
901
|
+
return ActionResult(error=f"Skill rejected: {'; '.join(res.errors)}",
|
|
902
|
+
include_in_memory=True)
|
|
903
|
+
where = "pending review" if res.pending else "active"
|
|
904
|
+
_self_mod_ev(params.action, pending=bool(res.pending), created_by=created_by)
|
|
905
|
+
# §7.1: notify the owner when a skill lands in pending (fail-open, gated).
|
|
906
|
+
if res.pending:
|
|
907
|
+
try:
|
|
908
|
+
from core import self_evolution as _se
|
|
909
|
+
from core.instance import resolve_instance_id as _rii
|
|
910
|
+
_cfg = getattr(getattr(self, 'container', None), 'config', None)
|
|
911
|
+
_dd = getattr(_cfg, 'data_dir', 'data') or 'data'
|
|
912
|
+
await _se.maybe_notify_owner_pending(
|
|
913
|
+
getattr(self, 'container', None), user_id,
|
|
914
|
+
home_dir=_dd, instance_id=_rii(), skill_manager=sm)
|
|
915
|
+
except Exception as _e:
|
|
916
|
+
self.logger.debug(f"self-evolution notify skipped: {_e}")
|
|
917
|
+
return ActionResult(
|
|
918
|
+
extracted_content=f"Skill `{params.skill_id}` saved ({where}).",
|
|
919
|
+
include_in_memory=True,
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
def _register_message_action(self):
|
|
923
|
+
"""Register the gated `message` action: resolve the target's tier
|
|
924
|
+
(owner/allowlisted/denied) and route an approved send through the
|
|
925
|
+
existing MessageRouter. Gated MESSAGE_TOOL_ENABLED (default OFF; ON
|
|
926
|
+
under POLYROB_LOCAL). Registered in this module — no `from __future__
|
|
927
|
+
import annotations` (registry-closure introspection)."""
|
|
928
|
+
try:
|
|
929
|
+
from agents.task.constants import message_tool_enabled
|
|
930
|
+
if not message_tool_enabled():
|
|
931
|
+
return
|
|
932
|
+
except Exception:
|
|
933
|
+
return
|
|
934
|
+
|
|
935
|
+
from tools.controller.message_send import perform_message_send
|
|
936
|
+
from tools.controller.views import MessageTargetAction
|
|
937
|
+
|
|
938
|
+
@self.registry.action(
|
|
939
|
+
'Send a message to a specific chat/recipient on a given surface '
|
|
940
|
+
'(telegram/email/whatsapp). Only the owner and owner-allowlisted targets '
|
|
941
|
+
'are permitted; other targets are denied.',
|
|
942
|
+
param_model=MessageTargetAction,
|
|
943
|
+
)
|
|
944
|
+
async def message(params: MessageTargetAction, execution_context=None) -> ActionResult:
|
|
945
|
+
import os
|
|
946
|
+
from core.instance import resolve_owner_telegram_id, resolve_owner_email
|
|
947
|
+
|
|
948
|
+
# Forged/untrusted/autonomous turns must not reach arbitrary targets
|
|
949
|
+
# (sub-agent, self-wake/delegation-result re-entry into the main agent,
|
|
950
|
+
# or an autonomous goal/cron/planner-spawned session).
|
|
951
|
+
if _is_forged_or_autonomous_turn(execution_context, self):
|
|
952
|
+
return ActionResult(
|
|
953
|
+
extracted_content=(
|
|
954
|
+
"message: not permitted for forged/autonomous turns "
|
|
955
|
+
"(owner must be in the loop)"),
|
|
956
|
+
include_in_memory=True)
|
|
957
|
+
|
|
958
|
+
user_id = getattr(execution_context, 'user_id', None) or getattr(self, 'user_id', None) or ""
|
|
959
|
+
container = getattr(self, "container", None)
|
|
960
|
+
router = container.get_service("message_router") if container else None
|
|
961
|
+
allowlist = container.get_service("outbound_allowlist") if container else None
|
|
962
|
+
|
|
963
|
+
owner_targets = {}
|
|
964
|
+
tid = resolve_owner_telegram_id(os.environ)
|
|
965
|
+
if tid:
|
|
966
|
+
owner_targets["telegram"] = str(tid)
|
|
967
|
+
oem = resolve_owner_email(os.environ)
|
|
968
|
+
if oem:
|
|
969
|
+
owner_targets["email"] = oem
|
|
970
|
+
|
|
971
|
+
res = await perform_message_send(
|
|
972
|
+
router=router, allowlist=allowlist, owner_targets=owner_targets,
|
|
973
|
+
user_id=user_id, surface=params.surface, target=params.target,
|
|
974
|
+
text=params.text, action=params.action, reply_to=params.reply_to,
|
|
975
|
+
message_id=params.message_id)
|
|
976
|
+
return ActionResult(extracted_content=(
|
|
977
|
+
f"message[{res['tier']}] -> {params.surface}:{params.target} "
|
|
978
|
+
f"{'OK' if res['success'] else 'FAILED: ' + (res.get('error') or '')}"),
|
|
979
|
+
include_in_memory=True)
|
|
980
|
+
|
|
981
|
+
def _register_self_context_manage_action(self):
|
|
982
|
+
"""Register the evolving SELF-identity tool `self_context_manage`, gated
|
|
983
|
+
SELF_CONTEXT_WRITABLE (default OFF; ON under POLYROB_LOCAL).
|
|
984
|
+
|
|
985
|
+
Lets the agent refine its own per-(instance,user) ``self.md`` — the learned
|
|
986
|
+
"how I work with this user" layer. Safety lives in SelfContextWriter:
|
|
987
|
+
tenant-confined + anon-blocked, identity-scanned fail-CLOSED (self-voice
|
|
988
|
+
subversion + invisible-unicode), over-cap ERRORS (forces consolidation),
|
|
989
|
+
forged turns forced to .pending and barred from active docs, atomic write,
|
|
990
|
+
archive-never-delete. The SOUL tier (identity.md/operating.md) is NEVER
|
|
991
|
+
reachable here — it stays operator-only. Writes apply NEXT session (the
|
|
992
|
+
foundation snapshot is frozen at session start). Registered in this module —
|
|
993
|
+
no `from __future__ import annotations` (registry-closure introspection)."""
|
|
994
|
+
try:
|
|
995
|
+
from agents.task.constants import AutonomyConfig
|
|
996
|
+
if not AutonomyConfig.self_context_writable():
|
|
997
|
+
return
|
|
998
|
+
except Exception:
|
|
999
|
+
return
|
|
1000
|
+
|
|
1001
|
+
from typing import Literal as _Literal
|
|
1002
|
+
|
|
1003
|
+
class SelfContextManageAction(BaseModel):
|
|
1004
|
+
action: _Literal["update", "patch", "read", "promote"]
|
|
1005
|
+
content: Optional[str] = None # update: full self.md body (≤2200 chars)
|
|
1006
|
+
old_string: Optional[str] = None # patch: exact text to replace
|
|
1007
|
+
new_string: Optional[str] = None # patch: replacement
|
|
1008
|
+
replace_all: bool = False
|
|
1009
|
+
|
|
1010
|
+
@self.registry.action(
|
|
1011
|
+
"Refine your evolving SELF context — durable notes about how you work with "
|
|
1012
|
+
"THIS user (preferences, conventions, what you've learned). action='read' "
|
|
1013
|
+
"returns the current text; action='update' replaces it (≤2200 chars — "
|
|
1014
|
+
"consolidate, don't sprawl); action='patch' edits by exact-string replace; "
|
|
1015
|
+
"action='promote' activates your pending draft (owner-only). Updates/patches "
|
|
1016
|
+
"are QUARANTINED for review and apply next session. This is NOT your core "
|
|
1017
|
+
"identity/boundaries (those are operator-owned).",
|
|
1018
|
+
param_model=SelfContextManageAction,
|
|
1019
|
+
)
|
|
1020
|
+
async def self_context_manage(params: SelfContextManageAction, execution_context=None) -> ActionResult:
|
|
1021
|
+
user_id = getattr(execution_context, 'user_id', None) or getattr(self, 'user_id', None)
|
|
1022
|
+
if not user_id:
|
|
1023
|
+
return ActionResult(error="self-context requires a user (tenant scope).",
|
|
1024
|
+
include_in_memory=True)
|
|
1025
|
+
|
|
1026
|
+
# T4-06: every effected self-context mutation records a first-class
|
|
1027
|
+
# self_modification event (durable log → /telemetry + /activity). Fail-open.
|
|
1028
|
+
def _self_mod_ev(action: str, *, pending=None, created_by: str = "", ok: bool = True):
|
|
1029
|
+
try:
|
|
1030
|
+
from agents.task.telemetry.self_events import emit_self_modification
|
|
1031
|
+
emit_self_modification(
|
|
1032
|
+
kind="self_context", action=action, item_id=user_id or "",
|
|
1033
|
+
user_id=user_id or "",
|
|
1034
|
+
session_id=(getattr(execution_context, 'session_id', None)
|
|
1035
|
+
or getattr(self, 'session_id', '') or ""),
|
|
1036
|
+
pending=pending, created_by=created_by,
|
|
1037
|
+
source="self_context_manage", ok=ok)
|
|
1038
|
+
except Exception:
|
|
1039
|
+
pass
|
|
1040
|
+
# Resolve the instance home dir (same as construction).
|
|
1041
|
+
_cfg = getattr(getattr(self, 'container', None), 'config', None)
|
|
1042
|
+
data_dir = getattr(_cfg, 'data_dir', 'data') or 'data'
|
|
1043
|
+
try:
|
|
1044
|
+
from core.instance import resolve_instance_id
|
|
1045
|
+
from core.self_context_writer import (
|
|
1046
|
+
SelfContextWriter, PROVENANCE_AGENT, PROVENANCE_BACKGROUND,
|
|
1047
|
+
)
|
|
1048
|
+
writer = SelfContextWriter(data_dir, instance_id=resolve_instance_id())
|
|
1049
|
+
except Exception as e:
|
|
1050
|
+
self.logger.debug(f"self_context_manage init failed: {e}")
|
|
1051
|
+
return ActionResult(error=f"self_context_manage unavailable: {e}", include_in_memory=True)
|
|
1052
|
+
|
|
1053
|
+
if params.action == "read":
|
|
1054
|
+
body = writer.read(user_id)
|
|
1055
|
+
# Apply the same load-side [BLOCKED] guard as session-start injection
|
|
1056
|
+
# (load_self_doc) so a direct-FS-poisoned doc is never returned raw to
|
|
1057
|
+
# the model mid-session. Fail-closed on a missing/raising scanner.
|
|
1058
|
+
if body:
|
|
1059
|
+
try:
|
|
1060
|
+
from modules.memory.task.threat_scan import is_identity_suspicious
|
|
1061
|
+
if is_identity_suspicious(body):
|
|
1062
|
+
body = "[BLOCKED: self-context failed the identity safety scan]"
|
|
1063
|
+
except Exception:
|
|
1064
|
+
body = "[BLOCKED: identity scanner unavailable]"
|
|
1065
|
+
return ActionResult(
|
|
1066
|
+
extracted_content=(body or "(no self-context yet)"),
|
|
1067
|
+
include_in_memory=True,
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
# Forged/autonomous detection (C7): a sub-agent/leaf OR an autonomous
|
|
1071
|
+
# goal/cron/planner-spawned run must never promote its own pending
|
|
1072
|
+
# self-context (autonomous top-level runs are owner_ok under POLYROB_LOCAL).
|
|
1073
|
+
is_forged = _is_forged_or_autonomous_turn(execution_context, self)
|
|
1074
|
+
|
|
1075
|
+
if params.action == "promote":
|
|
1076
|
+
# Activation is OWNER-only (Phase D). The caller is the owner when this
|
|
1077
|
+
# is the single-user local CLI OR their user_id matches the bound owner
|
|
1078
|
+
# principal (POLYROB_OWNER_USER_ID / first SURFACE_SUPER_ADMIN_USER_IDS).
|
|
1079
|
+
# A non-owner or any forged turn can never self-promote — that is what
|
|
1080
|
+
# keeps a self-wake / injected / sub-agent turn from activating its own
|
|
1081
|
+
# pending identity, on the server as well as locally.
|
|
1082
|
+
try:
|
|
1083
|
+
from agents.task.constants import local_mode_enabled
|
|
1084
|
+
from core.instance import is_owner_local_safe, resolve_owner_principal
|
|
1085
|
+
# The local bypass is honored ONLY for the genuine single-user local
|
|
1086
|
+
# operator tenant — NOT any uid under the global POLYROB_LOCAL flag.
|
|
1087
|
+
# This action runs inside a session and has no surface id, so it can't
|
|
1088
|
+
# use the _LOCAL_OWNER_SURFACES filter that access.py/pairing.py apply;
|
|
1089
|
+
# is_owner_local_safe is the surface-independent equivalent (a forgeable
|
|
1090
|
+
# network sender's uid is never the local tenant). See permissions audit F4.
|
|
1091
|
+
owner_ok = is_owner_local_safe(
|
|
1092
|
+
user_id, owner_principal=resolve_owner_principal(),
|
|
1093
|
+
local_enabled=local_mode_enabled())
|
|
1094
|
+
except Exception:
|
|
1095
|
+
owner_ok = False
|
|
1096
|
+
if is_forged or not owner_ok:
|
|
1097
|
+
return ActionResult(
|
|
1098
|
+
error="promote is owner-only; your pending self-context awaits operator review.",
|
|
1099
|
+
include_in_memory=True)
|
|
1100
|
+
res = writer.promote(user_id=user_id)
|
|
1101
|
+
if not res.ok:
|
|
1102
|
+
return ActionResult(error=f"Promote failed: {'; '.join(res.errors)}",
|
|
1103
|
+
include_in_memory=True)
|
|
1104
|
+
_self_mod_ev("promote", pending=False, created_by="owner")
|
|
1105
|
+
return ActionResult(extracted_content="Self-context promoted (active next session).",
|
|
1106
|
+
include_in_memory=True)
|
|
1107
|
+
|
|
1108
|
+
# update / patch: ALWAYS quarantine to .pending (pending=True below) — the
|
|
1109
|
+
# action never writes the active doc directly; activation is the owner-gated
|
|
1110
|
+
# `promote` above. `created_by` still reflects real forged status so the
|
|
1111
|
+
# writer additionally bars a forged turn from even reading/patching an active
|
|
1112
|
+
# doc, while a normal turn may patch the active doc INTO a pending edit.
|
|
1113
|
+
created_by = PROVENANCE_BACKGROUND if is_forged else PROVENANCE_AGENT
|
|
1114
|
+
try:
|
|
1115
|
+
if params.action == "update":
|
|
1116
|
+
if not params.content:
|
|
1117
|
+
return ActionResult(error="update requires `content`.", include_in_memory=True)
|
|
1118
|
+
res = writer.propose(params.content, user_id=user_id, created_by=created_by,
|
|
1119
|
+
pending=True)
|
|
1120
|
+
else: # patch
|
|
1121
|
+
if params.old_string is None or params.new_string is None:
|
|
1122
|
+
return ActionResult(error="patch requires `old_string` and `new_string`.",
|
|
1123
|
+
include_in_memory=True)
|
|
1124
|
+
res = writer.patch(user_id=user_id, old_string=params.old_string,
|
|
1125
|
+
new_string=params.new_string, replace_all=params.replace_all,
|
|
1126
|
+
created_by=created_by, pending=True)
|
|
1127
|
+
except Exception as e:
|
|
1128
|
+
self.logger.debug(f"self_context_manage failed: {e}")
|
|
1129
|
+
return ActionResult(error=f"self_context_manage failed: {e}", include_in_memory=True)
|
|
1130
|
+
|
|
1131
|
+
if not res.ok:
|
|
1132
|
+
return ActionResult(error=f"Self-context rejected: {'; '.join(res.errors)}",
|
|
1133
|
+
include_in_memory=True)
|
|
1134
|
+
_self_mod_ev(params.action, pending=True, created_by=created_by)
|
|
1135
|
+
# §7.1: proactively tell the owner a proposal is waiting (fail-open,
|
|
1136
|
+
# gated SELF_EVOLUTION_TRANSPARENCY). Closes the "owner never told" gap.
|
|
1137
|
+
try:
|
|
1138
|
+
from core import self_evolution as _se
|
|
1139
|
+
await _se.maybe_notify_owner_pending(
|
|
1140
|
+
getattr(self, 'container', None), user_id,
|
|
1141
|
+
home_dir=data_dir, instance_id=resolve_instance_id())
|
|
1142
|
+
except Exception as _e:
|
|
1143
|
+
self.logger.debug(f"self-evolution notify skipped: {_e}")
|
|
1144
|
+
return ActionResult(
|
|
1145
|
+
extracted_content="Self-context saved (pending review; applies next session).",
|
|
1146
|
+
include_in_memory=True,
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
def _register_owner_doc_manage_action(self):
|
|
1150
|
+
"""Register the bounded owner-facts tool `owner_doc_manage`, gated
|
|
1151
|
+
OWNER_DOC_WRITABLE (default OFF; ON under POLYROB_LOCAL).
|
|
1152
|
+
|
|
1153
|
+
Lets the agent maintain a small per-(instance,user) ``owner.md`` — durable
|
|
1154
|
+
facts/preferences about the OWNER, injected each session alongside SOUL/SELF.
|
|
1155
|
+
Same safety as self-context (OwnerDocWriter): tenant-confined + anon-blocked,
|
|
1156
|
+
identity-scanned fail-CLOSED, over-cap ERRORS, forged turns forced .pending
|
|
1157
|
+
and barred from active docs, atomic write, archive-never-delete. Writes apply
|
|
1158
|
+
NEXT session. No `from __future__ import annotations` (registry-closure
|
|
1159
|
+
introspection)."""
|
|
1160
|
+
try:
|
|
1161
|
+
from agents.task.constants import AutonomyConfig
|
|
1162
|
+
if not AutonomyConfig.owner_doc_writable():
|
|
1163
|
+
return
|
|
1164
|
+
except Exception:
|
|
1165
|
+
return
|
|
1166
|
+
|
|
1167
|
+
from typing import Literal as _Literal
|
|
1168
|
+
|
|
1169
|
+
class OwnerDocManageAction(BaseModel):
|
|
1170
|
+
action: _Literal["update", "patch", "read", "promote"]
|
|
1171
|
+
content: Optional[str] = None # update: full owner.md body (≤1600 chars)
|
|
1172
|
+
old_string: Optional[str] = None # patch: exact text to replace
|
|
1173
|
+
new_string: Optional[str] = None # patch: replacement
|
|
1174
|
+
replace_all: bool = False
|
|
1175
|
+
|
|
1176
|
+
@self.registry.action(
|
|
1177
|
+
"Maintain durable facts about your OWNER — their preferences, timezone, "
|
|
1178
|
+
"projects, how they like to be helped (a small owner.md, ≤1600 chars). "
|
|
1179
|
+
"action='read' returns it; action='update' replaces it (consolidate, keep "
|
|
1180
|
+
"only durable facts); action='patch' edits by exact-string replace; "
|
|
1181
|
+
"action='promote' activates your pending draft (owner-only). Updates/patches "
|
|
1182
|
+
"are QUARANTINED for review and apply next session.",
|
|
1183
|
+
param_model=OwnerDocManageAction,
|
|
1184
|
+
)
|
|
1185
|
+
async def owner_doc_manage(params: OwnerDocManageAction, execution_context=None) -> ActionResult:
|
|
1186
|
+
user_id = getattr(execution_context, 'user_id', None) or getattr(self, 'user_id', None)
|
|
1187
|
+
if not user_id:
|
|
1188
|
+
return ActionResult(error="owner-facts doc requires a user (tenant scope).",
|
|
1189
|
+
include_in_memory=True)
|
|
1190
|
+
|
|
1191
|
+
def _self_mod_ev(action: str, *, pending=None, created_by: str = "", ok: bool = True):
|
|
1192
|
+
try:
|
|
1193
|
+
from agents.task.telemetry.self_events import emit_self_modification
|
|
1194
|
+
emit_self_modification(
|
|
1195
|
+
kind="owner_doc", action=action, item_id=user_id or "",
|
|
1196
|
+
user_id=user_id or "",
|
|
1197
|
+
session_id=(getattr(execution_context, 'session_id', None)
|
|
1198
|
+
or getattr(self, 'session_id', '') or ""),
|
|
1199
|
+
pending=pending, created_by=created_by,
|
|
1200
|
+
source="owner_doc_manage", ok=ok)
|
|
1201
|
+
except Exception:
|
|
1202
|
+
pass
|
|
1203
|
+
|
|
1204
|
+
_cfg = getattr(getattr(self, 'container', None), 'config', None)
|
|
1205
|
+
data_dir = getattr(_cfg, 'data_dir', 'data') or 'data'
|
|
1206
|
+
try:
|
|
1207
|
+
from core.instance import resolve_instance_id
|
|
1208
|
+
from core.owner_doc_writer import (
|
|
1209
|
+
OwnerDocWriter, PROVENANCE_AGENT, PROVENANCE_BACKGROUND,
|
|
1210
|
+
)
|
|
1211
|
+
writer = OwnerDocWriter(data_dir, instance_id=resolve_instance_id())
|
|
1212
|
+
except Exception as e:
|
|
1213
|
+
self.logger.debug(f"owner_doc_manage init failed: {e}")
|
|
1214
|
+
return ActionResult(error=f"owner_doc_manage unavailable: {e}", include_in_memory=True)
|
|
1215
|
+
|
|
1216
|
+
if params.action == "read":
|
|
1217
|
+
body = writer.read(user_id)
|
|
1218
|
+
if body:
|
|
1219
|
+
try:
|
|
1220
|
+
from modules.memory.task.threat_scan import is_identity_suspicious
|
|
1221
|
+
if is_identity_suspicious(body):
|
|
1222
|
+
body = "[BLOCKED: owner-facts doc failed the identity safety scan]"
|
|
1223
|
+
except Exception:
|
|
1224
|
+
body = "[BLOCKED: identity scanner unavailable]"
|
|
1225
|
+
return ActionResult(
|
|
1226
|
+
extracted_content=(body or "(no owner-facts doc yet)"),
|
|
1227
|
+
include_in_memory=True,
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
is_forged = _is_forged_or_autonomous_turn(execution_context, self)
|
|
1231
|
+
|
|
1232
|
+
if params.action == "promote":
|
|
1233
|
+
try:
|
|
1234
|
+
from agents.task.constants import local_mode_enabled
|
|
1235
|
+
from core.instance import is_owner_local_safe, resolve_owner_principal
|
|
1236
|
+
owner_ok = is_owner_local_safe(
|
|
1237
|
+
user_id, owner_principal=resolve_owner_principal(),
|
|
1238
|
+
local_enabled=local_mode_enabled())
|
|
1239
|
+
except Exception:
|
|
1240
|
+
owner_ok = False
|
|
1241
|
+
if is_forged or not owner_ok:
|
|
1242
|
+
return ActionResult(
|
|
1243
|
+
error="promote is owner-only; your pending owner-facts doc awaits operator review.",
|
|
1244
|
+
include_in_memory=True)
|
|
1245
|
+
res = writer.promote(user_id=user_id)
|
|
1246
|
+
if not res.ok:
|
|
1247
|
+
return ActionResult(error=f"Promote failed: {'; '.join(res.errors)}",
|
|
1248
|
+
include_in_memory=True)
|
|
1249
|
+
_self_mod_ev("promote", pending=False, created_by="owner")
|
|
1250
|
+
return ActionResult(extracted_content="Owner-facts doc promoted (active next session).",
|
|
1251
|
+
include_in_memory=True)
|
|
1252
|
+
|
|
1253
|
+
created_by = PROVENANCE_BACKGROUND if is_forged else PROVENANCE_AGENT
|
|
1254
|
+
try:
|
|
1255
|
+
if params.action == "update":
|
|
1256
|
+
if not params.content:
|
|
1257
|
+
return ActionResult(error="update requires `content`.", include_in_memory=True)
|
|
1258
|
+
res = writer.propose(params.content, user_id=user_id, created_by=created_by,
|
|
1259
|
+
pending=True)
|
|
1260
|
+
else: # patch
|
|
1261
|
+
if params.old_string is None or params.new_string is None:
|
|
1262
|
+
return ActionResult(error="patch requires `old_string` and `new_string`.",
|
|
1263
|
+
include_in_memory=True)
|
|
1264
|
+
res = writer.patch(user_id=user_id, old_string=params.old_string,
|
|
1265
|
+
new_string=params.new_string, replace_all=params.replace_all,
|
|
1266
|
+
created_by=created_by, pending=True)
|
|
1267
|
+
except Exception as e:
|
|
1268
|
+
self.logger.debug(f"owner_doc_manage failed: {e}")
|
|
1269
|
+
return ActionResult(error=f"owner_doc_manage failed: {e}", include_in_memory=True)
|
|
1270
|
+
|
|
1271
|
+
if not res.ok:
|
|
1272
|
+
return ActionResult(error=f"Owner-facts doc rejected: {'; '.join(res.errors)}",
|
|
1273
|
+
include_in_memory=True)
|
|
1274
|
+
_self_mod_ev(params.action, pending=True, created_by=created_by)
|
|
1275
|
+
try:
|
|
1276
|
+
from core import self_evolution as _se
|
|
1277
|
+
await _se.maybe_notify_owner_pending(
|
|
1278
|
+
getattr(self, 'container', None), user_id,
|
|
1279
|
+
home_dir=data_dir, instance_id=resolve_instance_id())
|
|
1280
|
+
except Exception as _e:
|
|
1281
|
+
self.logger.debug(f"self-evolution notify skipped: {_e}")
|
|
1282
|
+
return ActionResult(
|
|
1283
|
+
extracted_content="Owner-facts doc saved (pending review; applies next session).",
|
|
1284
|
+
include_in_memory=True,
|
|
1285
|
+
)
|
|
1286
|
+
|
|
1287
|
+
def _register_mcp_install_action(self):
|
|
1288
|
+
"""Register the agent-callable `mcp_install` action (T3-01/W4-1).
|
|
1289
|
+
|
|
1290
|
+
Wires the previously-orphaned tools/mcp/self_install.py::perform_mcp_install
|
|
1291
|
+
pipeline (gate → allowlist → screen → approve → add_server → persist).
|
|
1292
|
+
Safety posture:
|
|
1293
|
+
- action only registered when MCP_SELF_INSTALL_ENABLED=true (default OFF);
|
|
1294
|
+
- a forged/leaf/autonomous turn can never install (owner in the loop);
|
|
1295
|
+
- the approver is resolved EXPLICITLY: Deny-by-default unless the operator
|
|
1296
|
+
set APPROVAL_PROVIDER — the enable flag alone never silently auto-approves;
|
|
1297
|
+
- only NAMED, reviewed catalog entries install (builtins + the operator's
|
|
1298
|
+
MCP_INSTALL_CATALOG_FILE, T3-03) — never an agent-written config;
|
|
1299
|
+
- persisted via the tenant-scoped user_mcp_service row (T3-04), NEVER the
|
|
1300
|
+
global config/mcp_config.json; stdio entries stay session-only;
|
|
1301
|
+
- direct {server}_{tool} actions re-registered post-install (T3-05) so the
|
|
1302
|
+
new tools are callable the SAME session;
|
|
1303
|
+
- every attempt writes an event_log audit row.
|
|
1304
|
+
Registered in this module — no `from __future__ import annotations`
|
|
1305
|
+
(registry-closure introspection).
|
|
1306
|
+
"""
|
|
1307
|
+
try:
|
|
1308
|
+
from tools.mcp.self_install import self_install_enabled
|
|
1309
|
+
if not self_install_enabled():
|
|
1310
|
+
return
|
|
1311
|
+
except Exception:
|
|
1312
|
+
return
|
|
1313
|
+
|
|
1314
|
+
from typing import Literal as _Literal
|
|
1315
|
+
|
|
1316
|
+
class MCPInstallAction(BaseModel):
|
|
1317
|
+
action: _Literal["list", "install"] = "install"
|
|
1318
|
+
server_id: Optional[str] = None
|
|
1319
|
+
|
|
1320
|
+
@self.registry.action(
|
|
1321
|
+
"Install a vetted MCP server from the reviewed catalog to gain new tools "
|
|
1322
|
+
"this session. action='list' shows the installable catalog; "
|
|
1323
|
+
"action='install' with server_id installs one (owner approval required). "
|
|
1324
|
+
"Only named catalog entries can be installed — never an arbitrary config.",
|
|
1325
|
+
param_model=MCPInstallAction,
|
|
1326
|
+
)
|
|
1327
|
+
async def mcp_install(params: MCPInstallAction, execution_context=None) -> ActionResult:
|
|
1328
|
+
import os as _os
|
|
1329
|
+
|
|
1330
|
+
from tools.mcp.catalog import MCPCatalog
|
|
1331
|
+
from tools.mcp.self_install import perform_mcp_install
|
|
1332
|
+
|
|
1333
|
+
user_id = getattr(execution_context, 'user_id', None) or getattr(self, 'user_id', None) or ""
|
|
1334
|
+
session_id = (getattr(execution_context, 'session_id', None)
|
|
1335
|
+
or getattr(self, 'session_id', '') or "")
|
|
1336
|
+
catalog = MCPCatalog()
|
|
1337
|
+
|
|
1338
|
+
def _audit(server_id: str, outcome: str, **extra):
|
|
1339
|
+
try:
|
|
1340
|
+
from agents.task.telemetry.event_log import event_log_enabled, get_event_log
|
|
1341
|
+
if event_log_enabled():
|
|
1342
|
+
get_event_log().record(
|
|
1343
|
+
"mcp_install", user_id=user_id, session_id=session_id,
|
|
1344
|
+
source="mcp_install", server_id=server_id, outcome=outcome,
|
|
1345
|
+
**extra)
|
|
1346
|
+
except Exception:
|
|
1347
|
+
pass
|
|
1348
|
+
|
|
1349
|
+
if params.action == "list":
|
|
1350
|
+
lines = ["Installable MCP servers (reviewed catalog):"]
|
|
1351
|
+
for sid in catalog.ids():
|
|
1352
|
+
e = catalog.get(sid)
|
|
1353
|
+
lines.append(f"- {sid} [{e.trust}] ({e.transport}): {e.description}")
|
|
1354
|
+
return ActionResult(extracted_content="\n".join(lines), include_in_memory=True)
|
|
1355
|
+
|
|
1356
|
+
if not params.server_id:
|
|
1357
|
+
return ActionResult(error="install requires `server_id` (see action='list').",
|
|
1358
|
+
include_in_memory=True)
|
|
1359
|
+
|
|
1360
|
+
# Owner in the loop: a self-wake/delegated/autonomous turn never installs.
|
|
1361
|
+
if _is_forged_or_autonomous_turn(execution_context, self):
|
|
1362
|
+
_audit(params.server_id, "refused_forged_turn")
|
|
1363
|
+
return ActionResult(
|
|
1364
|
+
extracted_content=(
|
|
1365
|
+
"mcp_install: not permitted for forged/autonomous turns — ask the "
|
|
1366
|
+
"owner to install it (the owner must be in the loop for new tools)."),
|
|
1367
|
+
include_in_memory=True)
|
|
1368
|
+
|
|
1369
|
+
# The MCP subsystem must be loaded this session to host the new server.
|
|
1370
|
+
mcp_info = (self._tools or {}).get("mcp")
|
|
1371
|
+
mcp_tool = getattr(mcp_info, "instance", None) if mcp_info else None
|
|
1372
|
+
server_manager = getattr(mcp_tool, "server_manager", None) if mcp_tool else None
|
|
1373
|
+
if server_manager is None:
|
|
1374
|
+
_audit(params.server_id, "refused_no_mcp_tool")
|
|
1375
|
+
return ActionResult(
|
|
1376
|
+
error="mcp_install: the mcp tool is not loaded this session "
|
|
1377
|
+
"(tool_ids must include 'mcp').",
|
|
1378
|
+
include_in_memory=True)
|
|
1379
|
+
|
|
1380
|
+
# Explicit approver: Deny-by-default unless the operator opted into a
|
|
1381
|
+
# provider — MCP_SELF_INSTALL_ENABLED alone never silently auto-approves.
|
|
1382
|
+
from tools.controller.approval import (
|
|
1383
|
+
DenyByDefaultApprover, get_approval_provider_or_deny)
|
|
1384
|
+
_prov_name = (_os.getenv("APPROVAL_PROVIDER") or "").strip()
|
|
1385
|
+
provider = (get_approval_provider_or_deny(_prov_name)
|
|
1386
|
+
if _prov_name else DenyByDefaultApprover())
|
|
1387
|
+
|
|
1388
|
+
ok, msg = await perform_mcp_install(
|
|
1389
|
+
params.server_id,
|
|
1390
|
+
catalog=catalog,
|
|
1391
|
+
server_manager=server_manager,
|
|
1392
|
+
approve=provider.request,
|
|
1393
|
+
persist=None, # tenant-scoped persist handled below (T3-04)
|
|
1394
|
+
context=execution_context,
|
|
1395
|
+
)
|
|
1396
|
+
if not ok:
|
|
1397
|
+
_audit(params.server_id, "failed", reason=msg)
|
|
1398
|
+
return ActionResult(error=msg, include_in_memory=True)
|
|
1399
|
+
|
|
1400
|
+
# T3-05: surface the new server's tools as direct actions THIS session.
|
|
1401
|
+
try:
|
|
1402
|
+
await self._register_mcp_tools_as_direct_actions(mcp_tool)
|
|
1403
|
+
except Exception as e:
|
|
1404
|
+
self.logger.debug(f"mcp_install: direct-action re-register skipped: {e}")
|
|
1405
|
+
|
|
1406
|
+
# T3-04: persist tenant-scoped (url transports only — user_mcp_service
|
|
1407
|
+
# blocks stdio by design; a stdio entry stays session-only).
|
|
1408
|
+
persisted = False
|
|
1409
|
+
entry = catalog.get(params.server_id)
|
|
1410
|
+
try:
|
|
1411
|
+
ums = (self.container.get_service("user_mcp_service")
|
|
1412
|
+
if getattr(self, "container", None) else None)
|
|
1413
|
+
if ums is not None and entry is not None and entry.url:
|
|
1414
|
+
res = await ums.add_server(
|
|
1415
|
+
user_id, params.server_id, entry.url,
|
|
1416
|
+
server_type=entry.transport if entry.transport in ("sse", "http") else "sse",
|
|
1417
|
+
display_name=entry.description or params.server_id,
|
|
1418
|
+
)
|
|
1419
|
+
persisted = bool(getattr(res, "success", False))
|
|
1420
|
+
except Exception as e:
|
|
1421
|
+
self.logger.debug(f"mcp_install: tenant persist skipped: {e}")
|
|
1422
|
+
|
|
1423
|
+
_audit(params.server_id, "installed", persisted=persisted)
|
|
1424
|
+
tail = " Persisted for future sessions." if persisted else " Session-only (not persisted)."
|
|
1425
|
+
return ActionResult(extracted_content=msg + tail, include_in_memory=True)
|
|
1426
|
+
|
|
1427
|
+
def _register_insights_action(self):
|
|
1428
|
+
"""Register the read-only `insights` tool (W7), gated INSIGHTS_TOOL.
|
|
1429
|
+
|
|
1430
|
+
Reports whether the agent's self-authored skills actually get reused — the
|
|
1431
|
+
measurement the writable-skills safety brief requires. Tenant-scoped, no writes.
|
|
1432
|
+
"""
|
|
1433
|
+
try:
|
|
1434
|
+
from agents.task.constants import AutonomyConfig
|
|
1435
|
+
if not AutonomyConfig.insights_tool():
|
|
1436
|
+
return
|
|
1437
|
+
except Exception:
|
|
1438
|
+
return
|
|
1439
|
+
|
|
1440
|
+
class InsightsAction(BaseModel):
|
|
1441
|
+
pass
|
|
1442
|
+
|
|
1443
|
+
@self.registry.action(
|
|
1444
|
+
"Show insights about your own learning: how many durable skills you've "
|
|
1445
|
+
"authored and how often you reuse them (authored-skill reuse rate).",
|
|
1446
|
+
param_model=InsightsAction,
|
|
1447
|
+
)
|
|
1448
|
+
async def insights(params: InsightsAction, execution_context=None) -> ActionResult:
|
|
1449
|
+
user_id = getattr(execution_context, 'user_id', None) or getattr(self, 'user_id', None)
|
|
1450
|
+
try:
|
|
1451
|
+
from modules.skills.skill_usage import get_skill_usage_store
|
|
1452
|
+
summary = get_skill_usage_store().authored_reuse_summary(user_id=user_id)
|
|
1453
|
+
except Exception as e:
|
|
1454
|
+
self.logger.debug(f"insights failed: {e}")
|
|
1455
|
+
return ActionResult(extracted_content="No insights available.", include_in_memory=False)
|
|
1456
|
+
rate = round(summary["reuse_rate"] * 100)
|
|
1457
|
+
top = ", ".join(f"{t['skill_id']}({t['loads']})" for t in summary["top"][:5]) or "—"
|
|
1458
|
+
return ActionResult(
|
|
1459
|
+
extracted_content=(
|
|
1460
|
+
f"## Skill insights\n"
|
|
1461
|
+
f"- authored skills: {summary['authored_total']}\n"
|
|
1462
|
+
f"- reused at least once: {summary['authored_reused']} ({rate}%)\n"
|
|
1463
|
+
f"- by author: {summary['by_author']}\n"
|
|
1464
|
+
f"- most-used: {top}"
|
|
1465
|
+
),
|
|
1466
|
+
include_in_memory=True,
|
|
1467
|
+
)
|
|
1468
|
+
|
|
1469
|
+
def _register_subtask_action(self):
|
|
1470
|
+
"""Register subtask action for sub-agent delegation.
|
|
1471
|
+
|
|
1472
|
+
Allows the main agent to spawn sub-agents for specialized tasks.
|
|
1473
|
+
Only registers actions when sub-agents are enabled to prevent
|
|
1474
|
+
leaking sub-agent instructions to the LLM when disabled.
|
|
1475
|
+
"""
|
|
1476
|
+
# Gate registration on sub-agents being enabled
|
|
1477
|
+
# This prevents the LLM from seeing subtask tools when disabled
|
|
1478
|
+
from agents.task.constants import TimeoutConfig
|
|
1479
|
+
if not TimeoutConfig.get_sub_agents_enabled():
|
|
1480
|
+
self.logger.debug("Sub-agents disabled - skipping subtask action registration")
|
|
1481
|
+
return
|
|
1482
|
+
|
|
1483
|
+
from tools.controller.views import SubtaskAction, ParallelSubtasksAction, DelegateTaskAction
|
|
1484
|
+
from tools.controller.delegation import evaluate_delegation
|
|
1485
|
+
|
|
1486
|
+
# UP-10 2.4: single gated delegation core. subtask / parallel_subtasks /
|
|
1487
|
+
# delegate_task all route through _delegate so the role+depth gate
|
|
1488
|
+
# (evaluate_delegation) is enforced for ALL three verbs — previously only
|
|
1489
|
+
# delegate_task ran the gate, so a leaf could still delegate via subtask.
|
|
1490
|
+
async def _delegate(*, goal=None, tasks=None, role="leaf", profile="executor",
|
|
1491
|
+
max_steps=30, background=False, model=None, provider=None,
|
|
1492
|
+
execution_context=None, label="delegate_task") -> ActionResult:
|
|
1493
|
+
"""Gated delegation dispatch. Provide goal XOR tasks (list of SubtaskAction).
|
|
1494
|
+
|
|
1495
|
+
UP-12: background=True (goal-only) detaches the child and returns immediately;
|
|
1496
|
+
its result re-enters the session as a new turn when it finishes.
|
|
1497
|
+
|
|
1498
|
+
B4: optional per-sub-agent `model`/`provider` builds an isolated child LLM
|
|
1499
|
+
instead of inheriting the parent's. Goal shape: `model` applies to the one
|
|
1500
|
+
spawned child. Tasks shape: each SubtaskAction's own `model` wins; a task
|
|
1501
|
+
without one falls back to the top-level `model` (else inherits the parent's
|
|
1502
|
+
LLM, unchanged). Not supported together with `background=true` in v1 — the
|
|
1503
|
+
async delegation registry doesn't yet thread a model through, so that
|
|
1504
|
+
combination is rejected before dispatch rather than silently ignored.
|
|
1505
|
+
"""
|
|
1506
|
+
from agents.task.constants import TimeoutConfig
|
|
1507
|
+
|
|
1508
|
+
# B4: `provider` only has meaning paired with a `model`. Reject a
|
|
1509
|
+
# provider given WITHOUT a model (top level, or on any parallel task)
|
|
1510
|
+
# with a clear error instead of silently discarding it.
|
|
1511
|
+
if provider and not model:
|
|
1512
|
+
return ActionResult(
|
|
1513
|
+
error="`provider` requires `model` — a provider without a model is "
|
|
1514
|
+
"discarded, so this is rejected. Set both, or neither.",
|
|
1515
|
+
include_in_memory=True,
|
|
1516
|
+
)
|
|
1517
|
+
if tasks:
|
|
1518
|
+
for _i, _st in enumerate(tasks):
|
|
1519
|
+
if getattr(_st, "provider", None) and not getattr(_st, "model", None):
|
|
1520
|
+
return ActionResult(
|
|
1521
|
+
error=f"task[{_i}] sets `provider` without `model` — a provider "
|
|
1522
|
+
"without a model is discarded. Set both, or neither.",
|
|
1523
|
+
include_in_memory=True,
|
|
1524
|
+
)
|
|
1525
|
+
|
|
1526
|
+
caller_is_sub = execution_context.is_sub_agent if execution_context else False
|
|
1527
|
+
# H7: fail closed to the least-privileged role — a delegation call with no
|
|
1528
|
+
# context (or a context missing role) must NOT inherit orchestrator privilege.
|
|
1529
|
+
caller_role = getattr(execution_context, 'role', 'leaf') if execution_context else 'leaf'
|
|
1530
|
+
|
|
1531
|
+
decision = evaluate_delegation(
|
|
1532
|
+
enabled=TimeoutConfig.get_sub_agents_enabled(),
|
|
1533
|
+
caller_is_sub_agent=caller_is_sub,
|
|
1534
|
+
caller_role=caller_role,
|
|
1535
|
+
requested_child_role=role,
|
|
1536
|
+
max_depth=TimeoutConfig.get_max_sub_agent_depth(),
|
|
1537
|
+
)
|
|
1538
|
+
if not decision.allowed:
|
|
1539
|
+
self.logger.info(f"🚫 {label} denied: {decision.reason}")
|
|
1540
|
+
return ActionResult(error=decision.reason, include_in_memory=True)
|
|
1541
|
+
|
|
1542
|
+
# Resolve parent agent id from context (fallback to first active agent)
|
|
1543
|
+
parent_agent_id = None
|
|
1544
|
+
if execution_context and getattr(execution_context, 'agent_id', None):
|
|
1545
|
+
parent_agent_id = execution_context.agent_id
|
|
1546
|
+
elif getattr(self, 'orchestrator', None) and getattr(self.orchestrator, 'agents', None):
|
|
1547
|
+
parent_agent_id = list(self.orchestrator.agents.keys())[0]
|
|
1548
|
+
if not parent_agent_id:
|
|
1549
|
+
return ActionResult(error="Cannot delegate: no parent agent context", include_in_memory=True)
|
|
1550
|
+
|
|
1551
|
+
mgr = getattr(self.orchestrator, 'sub_agent_manager', None) if getattr(self, 'orchestrator', None) else None
|
|
1552
|
+
if not mgr:
|
|
1553
|
+
return ActionResult(error="Sub-agent system not initialized", include_in_memory=True)
|
|
1554
|
+
|
|
1555
|
+
# B4: background delegation doesn't (yet) thread a model override through the
|
|
1556
|
+
# async delegation registry — reject cleanly before dispatch rather than
|
|
1557
|
+
# silently ignoring the requested model.
|
|
1558
|
+
if background and model:
|
|
1559
|
+
return ActionResult(
|
|
1560
|
+
error="model override not yet supported for background delegation "
|
|
1561
|
+
"(background=true always inherits the parent's model)",
|
|
1562
|
+
include_in_memory=True,
|
|
1563
|
+
)
|
|
1564
|
+
|
|
1565
|
+
# UP-12: background dispatch (goal-only) — detach and return immediately.
|
|
1566
|
+
if background and goal:
|
|
1567
|
+
registry = getattr(self.orchestrator, 'async_delegation', None) if getattr(self, 'orchestrator', None) else None
|
|
1568
|
+
if registry is None:
|
|
1569
|
+
return ActionResult(error="Background delegation not available in this session", include_in_memory=True)
|
|
1570
|
+
deleg = await registry.dispatch(
|
|
1571
|
+
goal=goal, profile=profile, max_steps=max_steps,
|
|
1572
|
+
parent_agent_id=parent_agent_id, caller_is_sub=caller_is_sub,
|
|
1573
|
+
)
|
|
1574
|
+
if deleg.get("status") == "rejected":
|
|
1575
|
+
return ActionResult(error=deleg["error"], include_in_memory=True)
|
|
1576
|
+
return ActionResult(
|
|
1577
|
+
extracted_content=(
|
|
1578
|
+
f"## Background task dispatched 🛫\n\n"
|
|
1579
|
+
f"**delegation_id:** {deleg['delegation_id']}\n**goal:** {goal}\n\n"
|
|
1580
|
+
"The task is running in the background; its result will arrive as a new "
|
|
1581
|
+
"message when it finishes. Continue with other work."
|
|
1582
|
+
),
|
|
1583
|
+
include_in_memory=True,
|
|
1584
|
+
)
|
|
1585
|
+
|
|
1586
|
+
def _resolve_parent_agent():
|
|
1587
|
+
"""Look up the parent Agent object (for building a child LLM). Mirrors
|
|
1588
|
+
the dual lookup pattern used elsewhere (plain id, then id_sessionid)."""
|
|
1589
|
+
orch = getattr(self, 'orchestrator', None)
|
|
1590
|
+
agents = getattr(orch, 'agents', None) if orch else None
|
|
1591
|
+
if not agents:
|
|
1592
|
+
return None
|
|
1593
|
+
session_id = getattr(orch, 'session_id', '')
|
|
1594
|
+
for lookup_id in (parent_agent_id, f"{parent_agent_id}_{session_id}"):
|
|
1595
|
+
if lookup_id in agents:
|
|
1596
|
+
return agents[lookup_id]
|
|
1597
|
+
return None
|
|
1598
|
+
|
|
1599
|
+
async def _build_child_llm(m, p):
|
|
1600
|
+
"""Build an isolated child LLM for model `m` (+ optional provider `p`).
|
|
1601
|
+
|
|
1602
|
+
Returns (llm, error_message) — error_message is None on success.
|
|
1603
|
+
Isolated so the child never shares the parent's cached HTTP client.
|
|
1604
|
+
"""
|
|
1605
|
+
parent_agent = _resolve_parent_agent()
|
|
1606
|
+
build_fn = getattr(parent_agent, '_create_llm_from_config_async', None) if parent_agent else None
|
|
1607
|
+
if build_fn is None:
|
|
1608
|
+
return None, f"Could not build sub-agent model '{m}': parent agent unavailable"
|
|
1609
|
+
cfg = {"model": m}
|
|
1610
|
+
if p:
|
|
1611
|
+
cfg["provider"] = p
|
|
1612
|
+
try:
|
|
1613
|
+
child_llm = await build_fn(cfg, isolated=True)
|
|
1614
|
+
except Exception as e:
|
|
1615
|
+
self.logger.error(f"{label}: failed to build child LLM for '{m}': {e}", exc_info=True)
|
|
1616
|
+
return None, f"Could not build sub-agent model '{m}': {e}"
|
|
1617
|
+
if child_llm is None:
|
|
1618
|
+
return None, f"Could not build sub-agent model '{m}'"
|
|
1619
|
+
return child_llm, None
|
|
1620
|
+
|
|
1621
|
+
try:
|
|
1622
|
+
if goal:
|
|
1623
|
+
child_llm = None
|
|
1624
|
+
if model:
|
|
1625
|
+
child_llm, build_err = await _build_child_llm(model, provider)
|
|
1626
|
+
if build_err:
|
|
1627
|
+
return ActionResult(error=build_err, include_in_memory=True)
|
|
1628
|
+
|
|
1629
|
+
self.logger.info(f"🚀 {label} (goal): {goal[:100]}... (profile: {profile})")
|
|
1630
|
+
result = await mgr.run_subtask(
|
|
1631
|
+
task=goal,
|
|
1632
|
+
parent_agent_id=parent_agent_id,
|
|
1633
|
+
profile_id=profile,
|
|
1634
|
+
max_steps=max_steps,
|
|
1635
|
+
parent_llm=child_llm,
|
|
1636
|
+
is_parent_sub_agent=caller_is_sub,
|
|
1637
|
+
)
|
|
1638
|
+
if result.success:
|
|
1639
|
+
return ActionResult(
|
|
1640
|
+
extracted_content=(
|
|
1641
|
+
f"## Delegated goal completed ✅\n\n**Goal:** {goal}\n\n"
|
|
1642
|
+
f"**Result:**\n{result.output}"
|
|
1643
|
+
),
|
|
1644
|
+
include_in_memory=True,
|
|
1645
|
+
)
|
|
1646
|
+
return ActionResult(error=f"Delegated goal failed: {result.error}", include_in_memory=True)
|
|
1647
|
+
|
|
1648
|
+
# tasks shape -> parallel. Per-task `model` wins; a task without its own
|
|
1649
|
+
# falls back to the top-level `model` (if set), else inherits the parent's
|
|
1650
|
+
# LLM unchanged. All required child LLMs are built BEFORE dispatch so a
|
|
1651
|
+
# bad model name aborts the whole call rather than a silent partial run.
|
|
1652
|
+
self.logger.info(f"🚀 {label} (parallel): {len(tasks)} tasks")
|
|
1653
|
+
subtask_dicts = []
|
|
1654
|
+
for st in tasks:
|
|
1655
|
+
st_model = st.model or model
|
|
1656
|
+
st_provider = st.provider if st.model else provider
|
|
1657
|
+
st_llm = None
|
|
1658
|
+
if st_model:
|
|
1659
|
+
st_llm, build_err = await _build_child_llm(st_model, st_provider)
|
|
1660
|
+
if build_err:
|
|
1661
|
+
return ActionResult(error=build_err, include_in_memory=True)
|
|
1662
|
+
subtask_dicts.append({
|
|
1663
|
+
'task': st.task, 'profile': st.profile, 'max_steps': st.max_steps,
|
|
1664
|
+
'llm': st_llm,
|
|
1665
|
+
})
|
|
1666
|
+
results = await mgr.run_parallel_subtasks(
|
|
1667
|
+
subtasks=subtask_dicts,
|
|
1668
|
+
parent_agent_id=parent_agent_id,
|
|
1669
|
+
)
|
|
1670
|
+
output = mgr.format_results_for_prompt(results)
|
|
1671
|
+
successful = sum(1 for r in results if r.success)
|
|
1672
|
+
return ActionResult(
|
|
1673
|
+
extracted_content=(
|
|
1674
|
+
f"## Delegated tasks complete\n\n"
|
|
1675
|
+
f"**Results:** {successful}/{len(results)} succeeded\n\n{output}"
|
|
1676
|
+
),
|
|
1677
|
+
include_in_memory=True,
|
|
1678
|
+
)
|
|
1679
|
+
except Exception as e:
|
|
1680
|
+
self.logger.error(f"{label} failed: {e}", exc_info=True)
|
|
1681
|
+
return ActionResult(error=f"{label} error: {str(e)}", include_in_memory=True)
|
|
1682
|
+
|
|
1683
|
+
# Expose the gated core so UP-12 (background delegation) and tests can reach it.
|
|
1684
|
+
self._delegate_core = _delegate
|
|
1685
|
+
|
|
1686
|
+
# One-shot deprecation notices for the legacy verbs.
|
|
1687
|
+
_deprecation_warned = {"subtask": False, "parallel_subtasks": False}
|
|
1688
|
+
|
|
1689
|
+
@self.registry.action(
|
|
1690
|
+
'Delegate a subtask to a sub-agent for focused execution '
|
|
1691
|
+
'(deprecated alias of delegate_task; use delegate_task with goal=...)',
|
|
1692
|
+
param_model=SubtaskAction
|
|
1693
|
+
)
|
|
1694
|
+
async def subtask(params: SubtaskAction, execution_context=None) -> ActionResult:
|
|
1695
|
+
"""Deprecated alias — forwards into the gated delegate_task core."""
|
|
1696
|
+
if not _deprecation_warned["subtask"]:
|
|
1697
|
+
_deprecation_warned["subtask"] = True
|
|
1698
|
+
self.logger.info("subtask is deprecated; use delegate_task (goal=...)")
|
|
1699
|
+
return await _delegate(
|
|
1700
|
+
goal=params.task, profile=params.profile, max_steps=params.max_steps,
|
|
1701
|
+
execution_context=execution_context, label="subtask",
|
|
1702
|
+
)
|
|
1703
|
+
|
|
1704
|
+
@self.registry.action(
|
|
1705
|
+
'Run multiple subtasks in parallel using sub-agents '
|
|
1706
|
+
'(deprecated alias of delegate_task; use delegate_task with tasks=[...])',
|
|
1707
|
+
param_model=ParallelSubtasksAction
|
|
1708
|
+
)
|
|
1709
|
+
async def parallel_subtasks(params: ParallelSubtasksAction, execution_context=None) -> ActionResult:
|
|
1710
|
+
"""Deprecated alias — forwards into the gated delegate_task core."""
|
|
1711
|
+
if not _deprecation_warned["parallel_subtasks"]:
|
|
1712
|
+
_deprecation_warned["parallel_subtasks"] = True
|
|
1713
|
+
self.logger.info("parallel_subtasks is deprecated; use delegate_task (tasks=[...])")
|
|
1714
|
+
return await _delegate(
|
|
1715
|
+
tasks=params.subtasks, execution_context=execution_context, label="parallel_subtasks",
|
|
1716
|
+
)
|
|
1717
|
+
|
|
1718
|
+
# Unified Reference-style delegation surface (roadmap P1): one ergonomic tool
|
|
1719
|
+
# (goal XOR tasks) with an explicit role/depth gate.
|
|
1720
|
+
@self.registry.action(
|
|
1721
|
+
'Delegate work to sub-agent(s): one focused "goal", or 2-5 parallel "tasks". '
|
|
1722
|
+
'Expensive. Synchronous by default (blocks this turn until done). Set '
|
|
1723
|
+
'background=true (goal only) to run it detached and get the result as a new '
|
|
1724
|
+
'message later — durable across the turn but NOT across restart (use the '
|
|
1725
|
+
'scheduler for durable scheduled work). Set "model" (and optionally '
|
|
1726
|
+
'"provider") to run the sub-agent on a different model than yours — e.g. a '
|
|
1727
|
+
'cheaper/faster model for a simple task; omit to inherit your model. With '
|
|
1728
|
+
'"tasks", each task can set its own "model", falling back to the top-level '
|
|
1729
|
+
'"model" if unset. Not supported together with background=true.',
|
|
1730
|
+
param_model=DelegateTaskAction
|
|
1731
|
+
)
|
|
1732
|
+
async def delegate_task(params: DelegateTaskAction, execution_context=None) -> ActionResult:
|
|
1733
|
+
"""Spawn sub-agent(s) for delegated work, gated by role + depth."""
|
|
1734
|
+
return await _delegate(
|
|
1735
|
+
goal=params.goal, tasks=params.tasks, role=params.role,
|
|
1736
|
+
profile=params.profile, max_steps=params.max_steps,
|
|
1737
|
+
background=params.background, model=params.model, provider=params.provider,
|
|
1738
|
+
execution_context=execution_context, label="delegate_task",
|
|
1739
|
+
)
|
|
1740
|
+
|
|
1741
|
+
def _register_backward_compat_aliases(self):
|
|
1742
|
+
"""Register backward compatibility aliases for renamed task actions.
|
|
1743
|
+
|
|
1744
|
+
This allows LLMs to use old todo action names (todo_list, todo_add, etc.)
|
|
1745
|
+
which automatically map to new namespaced names (task_todo_list, task_todo_add, etc.).
|
|
1746
|
+
|
|
1747
|
+
NOTE: 'done' is NOT aliased - it's a core Controller action, not part of TaskTool.
|
|
1748
|
+
"""
|
|
1749
|
+
aliases = {
|
|
1750
|
+
# Todo action aliases (old -> new)
|
|
1751
|
+
'todo_list': 'task_todo_list',
|
|
1752
|
+
'todo_add': 'task_todo_add',
|
|
1753
|
+
'todo_complete': 'task_todo_complete',
|
|
1754
|
+
'todo_progress': 'task_todo_progress',
|
|
1755
|
+
'todo_next': 'task_todo_next',
|
|
1756
|
+
# NOTE: 'done' is NOT here - it's a native Controller action
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
for old_name, new_name in aliases.items():
|
|
1760
|
+
# Use thread-safe create_alias method from Registry
|
|
1761
|
+
if self.registry.create_alias(old_name, new_name):
|
|
1762
|
+
self.logger.debug(f"Created backward compat alias: {old_name} -> {new_name}")
|
|
1763
|
+
else:
|
|
1764
|
+
# Don't warn if task tool not loaded yet - just skip
|
|
1765
|
+
self.logger.debug(f"Skipping alias {old_name} -> {new_name}: target not registered yet")
|