langchain-agentx-python 0.1__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.
- langchain_agentx/__init__.py +46 -0
- langchain_agentx/command/__init__.py +28 -0
- langchain_agentx/command/builtin/__init__.py +25 -0
- langchain_agentx/command/builtin/clear.py +33 -0
- langchain_agentx/command/builtin/compact.py +33 -0
- langchain_agentx/command/builtin/memory.py +37 -0
- langchain_agentx/command/builtin/reload_plugins.py +42 -0
- langchain_agentx/command/context.py +30 -0
- langchain_agentx/command/dispatcher.py +183 -0
- langchain_agentx/command/registry.py +110 -0
- langchain_agentx/command/result.py +25 -0
- langchain_agentx/command/types.py +41 -0
- langchain_agentx/config/__init__.py +14 -0
- langchain_agentx/loop/__init__.py +47 -0
- langchain_agentx/loop/config/__init__.py +20 -0
- langchain_agentx/loop/config/agent_config.py +66 -0
- langchain_agentx/loop/config/agent_loop_config.py +72 -0
- langchain_agentx/loop/config/model_context_resolver.py +105 -0
- langchain_agentx/loop/config/runtime_settings.py +50 -0
- langchain_agentx/loop/config/token_estimator.py +133 -0
- langchain_agentx/loop/context/__init__.py +66 -0
- langchain_agentx/loop/context/blocking_guard.py +97 -0
- langchain_agentx/loop/context/compaction_service.py +60 -0
- langchain_agentx/loop/context/message_utils.py +56 -0
- langchain_agentx/loop/context/pipeline.py +127 -0
- langchain_agentx/loop/context/settings.py +103 -0
- langchain_agentx/loop/context/stages/__init__.py +29 -0
- langchain_agentx/loop/context/stages/autocompact.py +140 -0
- langchain_agentx/loop/context/stages/base.py +32 -0
- langchain_agentx/loop/context/stages/collapse.py +76 -0
- langchain_agentx/loop/context/stages/microcompact.py +76 -0
- langchain_agentx/loop/context/stages/noop.py +33 -0
- langchain_agentx/loop/context/stages/snip.py +71 -0
- langchain_agentx/loop/context/stages/tool_result_budget.py +69 -0
- langchain_agentx/loop/context/types.py +79 -0
- langchain_agentx/loop/exit/__init__.py +1 -0
- langchain_agentx/loop/exit/exit_logic.py +320 -0
- langchain_agentx/loop/exit/reason_codes.py +39 -0
- langchain_agentx/loop/graph/__init__.py +5 -0
- langchain_agentx/loop/graph/builtin_loop_control.py +197 -0
- langchain_agentx/loop/graph/factory.py +1409 -0
- langchain_agentx/loop/graph/graph_edges.py +820 -0
- langchain_agentx/loop/hook/__init__.py +48 -0
- langchain_agentx/loop/hook/async_hook_runner.py +62 -0
- langchain_agentx/loop/hook/config.py +280 -0
- langchain_agentx/loop/hook/engine.py +321 -0
- langchain_agentx/loop/hook/executors/__init__.py +9 -0
- langchain_agentx/loop/hook/executors/agent.py +107 -0
- langchain_agentx/loop/hook/executors/command.py +230 -0
- langchain_agentx/loop/hook/executors/http.py +114 -0
- langchain_agentx/loop/hook/executors/prompt.py +92 -0
- langchain_agentx/loop/hook/graph_wiring.py +134 -0
- langchain_agentx/loop/hook/registry.py +262 -0
- langchain_agentx/loop/hook/trust.py +43 -0
- langchain_agentx/loop/hook/types.py +110 -0
- langchain_agentx/loop/injection/__init__.py +13 -0
- langchain_agentx/loop/injection/dedup.py +74 -0
- langchain_agentx/loop/loop_abort.py +36 -0
- langchain_agentx/loop/model/__init__.py +1 -0
- langchain_agentx/loop/model/model_node.py +648 -0
- langchain_agentx/loop/model/model_nodes.py +661 -0
- langchain_agentx/loop/model/orphan_tool_results.py +38 -0
- langchain_agentx/loop/model/retrier.py +307 -0
- langchain_agentx/loop/model/retry_bridge.py +58 -0
- langchain_agentx/loop/model/retry_events.py +35 -0
- langchain_agentx/loop/model/retry_policy.py +56 -0
- langchain_agentx/loop/model/schema_and_format.py +153 -0
- langchain_agentx/loop/model/tool_and_model_binding.py +227 -0
- langchain_agentx/loop/model/tool_call_degradation_corrector.py +443 -0
- langchain_agentx/loop/model/tool_transcript_guard.py +225 -0
- langchain_agentx/loop/prompt/__init__.py +95 -0
- langchain_agentx/loop/prompt/builder.py +61 -0
- langchain_agentx/loop/prompt/builtin.py +218 -0
- langchain_agentx/loop/prompt/compact.py +408 -0
- langchain_agentx/loop/prompt/sections.py +120 -0
- langchain_agentx/loop/runtime/__init__.py +19 -0
- langchain_agentx/loop/runtime/context.py +34 -0
- langchain_agentx/loop/runtime/context_factory.py +107 -0
- langchain_agentx/loop/runtime/subagent_execution_paths.py +68 -0
- langchain_agentx/loop/subagent/__init__.py +53 -0
- langchain_agentx/loop/subagent/async_runner.py +215 -0
- langchain_agentx/loop/subagent/context.py +209 -0
- langchain_agentx/loop/subagent/fork_worktree_notice.py +25 -0
- langchain_agentx/loop/subagent/graph.py +72 -0
- langchain_agentx/loop/subagent/orchestrator.py +391 -0
- langchain_agentx/loop/subagent/progress.py +30 -0
- langchain_agentx/loop/subagent/prompt.py +52 -0
- langchain_agentx/loop/subagent/runner.py +504 -0
- langchain_agentx/loop/subagent/transcript.py +172 -0
- langchain_agentx/memory/__init__.py +2 -0
- langchain_agentx/memory/instruction/__init__.py +12 -0
- langchain_agentx/memory/instruction/loader.py +325 -0
- langchain_agentx/memory/instruction/resolver.py +24 -0
- langchain_agentx/memory/instruction/runtime.py +83 -0
- langchain_agentx/memory/instruction/sections.py +83 -0
- langchain_agentx/memory/instruction/types.py +59 -0
- langchain_agentx/memory/memdir/__init__.py +77 -0
- langchain_agentx/memory/memdir/age.py +36 -0
- langchain_agentx/memory/memdir/agent_memory.py +380 -0
- langchain_agentx/memory/memdir/extractor.py +309 -0
- langchain_agentx/memory/memdir/loader.py +187 -0
- langchain_agentx/memory/memdir/paths.py +63 -0
- langchain_agentx/memory/memdir/recall.py +45 -0
- langchain_agentx/memory/memdir/runtime.py +43 -0
- langchain_agentx/memory/memdir/scan.py +135 -0
- langchain_agentx/memory/memdir/types.py +104 -0
- langchain_agentx/memory/session/__init__.py +76 -0
- langchain_agentx/memory/session/compact_bridge.py +208 -0
- langchain_agentx/memory/session/prompts.py +172 -0
- langchain_agentx/memory/session/session_memory.py +282 -0
- langchain_agentx/observability/__init__.py +67 -0
- langchain_agentx/observability/evaluation/__init__.py +17 -0
- langchain_agentx/observability/evaluation/checkers/__init__.py +18 -0
- langchain_agentx/observability/evaluation/checkers/base.py +34 -0
- langchain_agentx/observability/evaluation/checkers/compaction.py +38 -0
- langchain_agentx/observability/evaluation/checkers/degradation.py +50 -0
- langchain_agentx/observability/evaluation/checkers/exit_quality.py +42 -0
- langchain_agentx/observability/evaluation/checkers/session_memory.py +45 -0
- langchain_agentx/observability/evaluation/checkers/tool_behavior.py +53 -0
- langchain_agentx/observability/evaluation/retention_scheduler.py +67 -0
- langchain_agentx/observability/evaluation/service.py +102 -0
- langchain_agentx/observability/evaluation/state.py +32 -0
- langchain_agentx/observability/evaluation/store.py +258 -0
- langchain_agentx/observability/events/__init__.py +15 -0
- langchain_agentx/observability/events/langchain_agentx_event_adapter.py +832 -0
- langchain_agentx/observability/logging/__init__.py +15 -0
- langchain_agentx/observability/logging/debug_burst.py +95 -0
- langchain_agentx/observability/logging/logging_config.py +178 -0
- langchain_agentx/observability/logging/logging_contract.py +65 -0
- langchain_agentx/observability/replay/__init__.py +35 -0
- langchain_agentx/observability/replay/cli.py +91 -0
- langchain_agentx/observability/replay/service.py +83 -0
- langchain_agentx/observability/replay/store.py +278 -0
- langchain_agentx/observability/replay/ui.py +47 -0
- langchain_agentx/observability/trace/__init__.py +25 -0
- langchain_agentx/observability/trace/collector.py +560 -0
- langchain_agentx/observability/trace/event_emitter.py +183 -0
- langchain_agentx/observability/trace/hook_event_emitter.py +49 -0
- langchain_agentx/observability/trace/models.py +144 -0
- langchain_agentx/observability/trace/sqlite_store.py +873 -0
- langchain_agentx/observability/trace/trace_callback.py +295 -0
- langchain_agentx/observability/trace/trace_lifecycle_collector.py +114 -0
- langchain_agentx/plugin/__init__.py +26 -0
- langchain_agentx/plugin/builtin.py +53 -0
- langchain_agentx/plugin/config.py +113 -0
- langchain_agentx/plugin/loader.py +386 -0
- langchain_agentx/plugin/manifest.py +154 -0
- langchain_agentx/plugin/registries.py +211 -0
- langchain_agentx/plugin/types.py +142 -0
- langchain_agentx/provider/__init__.py +27 -0
- langchain_agentx/provider/anthropic.py +121 -0
- langchain_agentx/provider/compatible_chat_openai.py +86 -0
- langchain_agentx/provider/env.py +45 -0
- langchain_agentx/provider/model_profile.py +156 -0
- langchain_agentx/provider/openai.py +89 -0
- langchain_agentx/session/__init__.py +17 -0
- langchain_agentx/session/agent_session.py +320 -0
- langchain_agentx/session/conversation_factory.py +87 -0
- langchain_agentx/session/conversation_recovery.py +156 -0
- langchain_agentx/session/conversation_session.py +198 -0
- langchain_agentx/session/factory.py +143 -0
- langchain_agentx/session/protocol.py +25 -0
- langchain_agentx/task_runtime/__init__.py +113 -0
- langchain_agentx/task_runtime/core/__init__.py +51 -0
- langchain_agentx/task_runtime/core/ids.py +33 -0
- langchain_agentx/task_runtime/core/interfaces.py +115 -0
- langchain_agentx/task_runtime/core/notification_priority.py +19 -0
- langchain_agentx/task_runtime/core/types.py +136 -0
- langchain_agentx/task_runtime/integrations/__init__.py +33 -0
- langchain_agentx/task_runtime/integrations/loop_adapter.py +91 -0
- langchain_agentx/task_runtime/integrations/loop_integration.py +61 -0
- langchain_agentx/task_runtime/integrations/prefetch_providers.py +108 -0
- langchain_agentx/task_runtime/integrations/provider_factory.py +103 -0
- langchain_agentx/task_runtime/integrations/queued_command_provider.py +184 -0
- langchain_agentx/task_runtime/integrations/sqlite_queued_command_provider.py +338 -0
- langchain_agentx/task_runtime/integrations/tool_use_summary_provider.py +254 -0
- langchain_agentx/task_runtime/orchestrator/__init__.py +5 -0
- langchain_agentx/task_runtime/orchestrator/runtime.py +386 -0
- langchain_agentx/task_runtime/output/__init__.py +5 -0
- langchain_agentx/task_runtime/output/sink.py +64 -0
- langchain_agentx/task_runtime/policy/__init__.py +11 -0
- langchain_agentx/task_runtime/policy/withhold_visibility.py +32 -0
- langchain_agentx/task_runtime/queue/__init__.py +5 -0
- langchain_agentx/task_runtime/queue/in_memory.py +55 -0
- langchain_agentx/task_runtime/skill_prefetch/__init__.py +4 -0
- langchain_agentx/task_runtime/skill_prefetch/attachments.py +46 -0
- langchain_agentx/task_runtime/skill_prefetch/models.py +37 -0
- langchain_agentx/task_runtime/skill_prefetch/provider.py +344 -0
- langchain_agentx/task_runtime/store/__init__.py +6 -0
- langchain_agentx/task_runtime/store/in_memory.py +81 -0
- langchain_agentx/task_runtime/store/sqlite_store.py +281 -0
- langchain_agentx/task_runtime/tasks/__init__.py +76 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/__init__.py +15 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/base.py +41 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/evaluation.py +67 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/registry.py +36 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/scheduler.py +70 -0
- langchain_agentx/task_runtime/tasks/base/__init__.py +6 -0
- langchain_agentx/task_runtime/tasks/base/contracts.py +24 -0
- langchain_agentx/task_runtime/tasks/custom/__init__.py +7 -0
- langchain_agentx/task_runtime/tasks/custom/executor.py +60 -0
- langchain_agentx/task_runtime/tasks/custom/notification.py +7 -0
- langchain_agentx/task_runtime/tasks/custom/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/custom/spec.py +33 -0
- langchain_agentx/task_runtime/tasks/dream_task/__init__.py +15 -0
- langchain_agentx/task_runtime/tasks/dream_task/executor.py +61 -0
- langchain_agentx/task_runtime/tasks/dream_task/notification.py +19 -0
- langchain_agentx/task_runtime/tasks/dream_task/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/dream_task/spec.py +35 -0
- langchain_agentx/task_runtime/tasks/dream_task/state.py +17 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/__init__.py +12 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/executor.py +36 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/notification.py +25 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/spec.py +63 -0
- langchain_agentx/task_runtime/tasks/local_agent/__init__.py +14 -0
- langchain_agentx/task_runtime/tasks/local_agent/executor.py +33 -0
- langchain_agentx/task_runtime/tasks/local_agent/notification.py +21 -0
- langchain_agentx/task_runtime/tasks/local_agent/runner.py +43 -0
- langchain_agentx/task_runtime/tasks/local_agent/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/local_agent/spec.py +31 -0
- langchain_agentx/task_runtime/tasks/local_bash/__init__.py +13 -0
- langchain_agentx/task_runtime/tasks/local_bash/executor.py +95 -0
- langchain_agentx/task_runtime/tasks/local_bash/notification.py +22 -0
- langchain_agentx/task_runtime/tasks/local_bash/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/local_bash/spec.py +55 -0
- langchain_agentx/task_runtime/tasks/remote_agent/__init__.py +19 -0
- langchain_agentx/task_runtime/tasks/remote_agent/backend.py +76 -0
- langchain_agentx/task_runtime/tasks/remote_agent/executor.py +37 -0
- langchain_agentx/task_runtime/tasks/remote_agent/notification.py +22 -0
- langchain_agentx/task_runtime/tasks/remote_agent/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/remote_agent/spec.py +34 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/__init__.py +19 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/bootstrap.py +95 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/executor.py +66 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/scheduler.py +169 -0
- langchain_agentx/tool_runtime/__init__.py +90 -0
- langchain_agentx/tool_runtime/adapter.py +365 -0
- langchain_agentx/tool_runtime/base.py +319 -0
- langchain_agentx/tool_runtime/errors.py +190 -0
- langchain_agentx/tool_runtime/identical_call_cache.py +110 -0
- langchain_agentx/tool_runtime/loader.py +195 -0
- langchain_agentx/tool_runtime/models.py +260 -0
- langchain_agentx/tool_runtime/permission_context.py +78 -0
- langchain_agentx/tool_runtime/pipeline.py +621 -0
- langchain_agentx/tool_runtime/policy.py +447 -0
- langchain_agentx/tool_runtime/registry.py +81 -0
- langchain_agentx/tool_runtime/resolvers/__init__.py +27 -0
- langchain_agentx/tool_runtime/resolvers/agent_session.py +125 -0
- langchain_agentx/tool_runtime/resolvers/background.py +32 -0
- langchain_agentx/tool_runtime/resolvers/base.py +20 -0
- langchain_agentx/tool_runtime/resolvers/conversation.py +22 -0
- langchain_agentx/tool_runtime/resolvers/workflow.py +73 -0
- langchain_agentx/tool_runtime/session_store.py +132 -0
- langchain_agentx/tool_runtime/smoke_test_runtime.py +294 -0
- langchain_agentx/tool_runtime/state_bridge.py +164 -0
- langchain_agentx/tools/__init__.py +26 -0
- langchain_agentx/tools/agent/__init__.py +9 -0
- langchain_agentx/tools/agent/backend.py +53 -0
- langchain_agentx/tools/agent/built_in/__init__.py +19 -0
- langchain_agentx/tools/agent/built_in/agentx_guide.py +65 -0
- langchain_agentx/tools/agent/built_in/explore.py +80 -0
- langchain_agentx/tools/agent/built_in/general.py +57 -0
- langchain_agentx/tools/agent/built_in/plan.py +89 -0
- langchain_agentx/tools/agent/built_in/statusline_setup.py +64 -0
- langchain_agentx/tools/agent/built_in/verification.py +120 -0
- langchain_agentx/tools/agent/builtin_subagent_loader.py +89 -0
- langchain_agentx/tools/agent/cwd_resolution.py +119 -0
- langchain_agentx/tools/agent/limits.py +26 -0
- langchain_agentx/tools/agent/loader.py +270 -0
- langchain_agentx/tools/agent/models.py +85 -0
- langchain_agentx/tools/agent/prompt.py +120 -0
- langchain_agentx/tools/agent/registry/__init__.py +18 -0
- langchain_agentx/tools/agent/registry/config.py +29 -0
- langchain_agentx/tools/agent/registry/registry.py +47 -0
- langchain_agentx/tools/agent/scope.py +137 -0
- langchain_agentx/tools/agent/tool.py +256 -0
- langchain_agentx/tools/bash/__init__.py +9 -0
- langchain_agentx/tools/bash/ast_security.py +571 -0
- langchain_agentx/tools/bash/backend.py +1447 -0
- langchain_agentx/tools/bash/bash_hardening.py +734 -0
- langchain_agentx/tools/bash/bash_runtime_contract.py +41 -0
- langchain_agentx/tools/bash/cwd_reporter.py +95 -0
- langchain_agentx/tools/bash/limits.py +71 -0
- langchain_agentx/tools/bash/mode_validation.py +282 -0
- langchain_agentx/tools/bash/models.py +131 -0
- langchain_agentx/tools/bash/observability.py +148 -0
- langchain_agentx/tools/bash/output_utils.py +200 -0
- langchain_agentx/tools/bash/path_security.py +2429 -0
- langchain_agentx/tools/bash/prompt.py +68 -0
- langchain_agentx/tools/bash/read_only_validation.py +589 -0
- langchain_agentx/tools/bash/result_presenter.py +324 -0
- langchain_agentx/tools/bash/sandbox_decision.py +133 -0
- langchain_agentx/tools/bash/security.py +311 -0
- langchain_agentx/tools/bash/sed_edit_parser.py +243 -0
- langchain_agentx/tools/bash/sed_validation.py +163 -0
- langchain_agentx/tools/bash/semantics.py +111 -0
- langchain_agentx/tools/bash/session_manager.py +205 -0
- langchain_agentx/tools/bash/session_runtime.py +290 -0
- langchain_agentx/tools/bash/shell_locator.py +191 -0
- langchain_agentx/tools/bash/task_runtime.py +91 -0
- langchain_agentx/tools/bash/tool.py +939 -0
- langchain_agentx/tools/bash/windows_shell_quoting.py +45 -0
- langchain_agentx/tools/glob/__init__.py +9 -0
- langchain_agentx/tools/glob/models.py +57 -0
- langchain_agentx/tools/glob/pagination.py +30 -0
- langchain_agentx/tools/glob/prompt.py +24 -0
- langchain_agentx/tools/glob/rg_list_backend.py +139 -0
- langchain_agentx/tools/glob/rg_pattern.py +44 -0
- langchain_agentx/tools/glob/tool.py +327 -0
- langchain_agentx/tools/grep/__init__.py +7 -0
- langchain_agentx/tools/grep/backend.py +375 -0
- langchain_agentx/tools/grep/models.py +127 -0
- langchain_agentx/tools/grep/prompt.py +30 -0
- langchain_agentx/tools/grep/rg_subprocess_controller.py +114 -0
- langchain_agentx/tools/grep/tool.py +475 -0
- langchain_agentx/tools/read/__init__.py +9 -0
- langchain_agentx/tools/read/backend.py +415 -0
- langchain_agentx/tools/read/limits.py +67 -0
- langchain_agentx/tools/read/models.py +156 -0
- langchain_agentx/tools/read/prompt.py +73 -0
- langchain_agentx/tools/read/tool.py +494 -0
- langchain_agentx/tools/ripgrep_plugin_exclusions.py +137 -0
- langchain_agentx/tools/skill/__init__.py +4 -0
- langchain_agentx/tools/skill/argument_substitution.py +80 -0
- langchain_agentx/tools/skill/loader.py +196 -0
- langchain_agentx/tools/skill/models.py +88 -0
- langchain_agentx/tools/skill/policy.py +80 -0
- langchain_agentx/tools/skill/prompt.py +35 -0
- langchain_agentx/tools/skill/tool.py +222 -0
- langchain_agentx/utils/__init__.py +0 -0
- langchain_agentx/utils/cwd.py +124 -0
- langchain_agentx/utils/host_platform.py +112 -0
- langchain_agentx/utils/path_hierarchy.py +48 -0
- langchain_agentx/utils/path_user_input.py +66 -0
- langchain_agentx/utils/rg_executable.py +18 -0
- langchain_agentx/utils/subprocess_text.py +101 -0
- langchain_agentx/utils/temp_paths.py +77 -0
- langchain_agentx/utils/unc_path.py +25 -0
- langchain_agentx/utils/win_reserved_paths.py +51 -0
- langchain_agentx/workflow/__init__.py +7 -0
- langchain_agentx/workflow/base.py +97 -0
- langchain_agentx/workflow/batch.py +55 -0
- langchain_agentx/workflow/dag.py +54 -0
- langchain_agentx/workspace/__init__.py +13 -0
- langchain_agentx/workspace/config.py +140 -0
- langchain_agentx/workspace/path_key_normalizer.py +30 -0
- langchain_agentx/workspace/resolver.py +74 -0
- langchain_agentx/workspace/validators.py +41 -0
- langchain_agentx_python-0.1.dist-info/LICENSE +201 -0
- langchain_agentx_python-0.1.dist-info/METADATA +513 -0
- langchain_agentx_python-0.1.dist-info/RECORD +354 -0
- langchain_agentx_python-0.1.dist-info/WHEEL +5 -0
- langchain_agentx_python-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
plugin/loader.py — Plugin Discovery/Load/Register 主编排。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
协调插件发现、manifest 解析、能力注册与 hook 原子切换。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
由容器 on_start/on_end 调用,连接 workspace 配置、HookEngine 与各 Registry。
|
|
9
|
+
|
|
10
|
+
当前裁剪范围:
|
|
11
|
+
覆盖本地+内置插件;不包含远程插件下载与 MCP 连接管理。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from langchain_agentx.loop.hook.config import HookSpec, HooksConfigSnapshot
|
|
22
|
+
from langchain_agentx.loop.hook.types import HookEvent
|
|
23
|
+
from langchain_agentx.workspace.config import AgentWorkspaceConfig
|
|
24
|
+
|
|
25
|
+
from .builtin import BuiltinPluginRegistry
|
|
26
|
+
from .config import PluginConfigLoader
|
|
27
|
+
from .manifest import MinimalManifest, PluginManifestSchema, ValidationError
|
|
28
|
+
from .registries import AgentRegistry, CommandRegistry, SkillRegistry
|
|
29
|
+
from .types import (
|
|
30
|
+
GenericPluginError,
|
|
31
|
+
HookLoadFailedError,
|
|
32
|
+
LoadedPlugin,
|
|
33
|
+
ManifestParseError,
|
|
34
|
+
ManifestValidationError,
|
|
35
|
+
PluginError,
|
|
36
|
+
PluginLoadResult,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PluginLoader:
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
workspace_config: AgentWorkspaceConfig,
|
|
46
|
+
hook_engine: Any,
|
|
47
|
+
skill_registry: SkillRegistry,
|
|
48
|
+
command_registry: CommandRegistry,
|
|
49
|
+
agent_registry: AgentRegistry,
|
|
50
|
+
mcp_manager: Any | None = None,
|
|
51
|
+
builtin_registry: BuiltinPluginRegistry | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
self._cfg = workspace_config
|
|
54
|
+
self._hook_engine = hook_engine
|
|
55
|
+
self._skill_registry = skill_registry
|
|
56
|
+
self._command_registry = command_registry
|
|
57
|
+
self._agent_registry = agent_registry
|
|
58
|
+
self._mcp_manager = mcp_manager
|
|
59
|
+
self._builtin_registry = builtin_registry or BuiltinPluginRegistry.global_instance()
|
|
60
|
+
self._loaded: list[LoadedPlugin] = []
|
|
61
|
+
|
|
62
|
+
async def load_all(self, container_type: str | None = None) -> PluginLoadResult:
|
|
63
|
+
errors: list[PluginError] = []
|
|
64
|
+
candidates = await self._discover(errors)
|
|
65
|
+
if container_type:
|
|
66
|
+
candidates = [
|
|
67
|
+
plugin
|
|
68
|
+
for plugin in candidates
|
|
69
|
+
if not plugin.applicable_containers
|
|
70
|
+
or container_type in plugin.applicable_containers
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
candidates = [plugin for plugin in candidates if plugin.enabled]
|
|
74
|
+
|
|
75
|
+
errors.extend(await self._register_skills(candidates))
|
|
76
|
+
errors.extend(await self._register_commands(candidates))
|
|
77
|
+
errors.extend(await self._register_agents(candidates))
|
|
78
|
+
errors.extend(await self._register_hooks_atomic(candidates))
|
|
79
|
+
|
|
80
|
+
self._loaded = candidates
|
|
81
|
+
return PluginLoadResult(enabled=candidates, errors=errors)
|
|
82
|
+
|
|
83
|
+
async def unload_all(self) -> None:
|
|
84
|
+
for plugin in self._loaded:
|
|
85
|
+
try:
|
|
86
|
+
self._skill_registry.unregister_plugin(plugin.source)
|
|
87
|
+
self._command_registry.unregister_plugin(plugin.source)
|
|
88
|
+
self._agent_registry.unregister_plugin(plugin.source)
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
logger.error(
|
|
91
|
+
"plugin unload failed for %s: %s", plugin.source, exc, exc_info=True
|
|
92
|
+
)
|
|
93
|
+
snapshot = self._get_snapshot()
|
|
94
|
+
if snapshot is not None:
|
|
95
|
+
try:
|
|
96
|
+
snapshot.replace_plugin_hooks_atomic([])
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
logger.error("plugin hook cleanup failed: %s", exc, exc_info=True)
|
|
99
|
+
self._loaded = []
|
|
100
|
+
|
|
101
|
+
async def reload(self, container_type: str | None = None) -> PluginLoadResult:
|
|
102
|
+
errors: list[PluginError] = []
|
|
103
|
+
candidates = await self._discover(errors)
|
|
104
|
+
if container_type:
|
|
105
|
+
candidates = [
|
|
106
|
+
plugin
|
|
107
|
+
for plugin in candidates
|
|
108
|
+
if not plugin.applicable_containers
|
|
109
|
+
or container_type in plugin.applicable_containers
|
|
110
|
+
]
|
|
111
|
+
candidates = [plugin for plugin in candidates if plugin.enabled]
|
|
112
|
+
|
|
113
|
+
for plugin in self._loaded:
|
|
114
|
+
self._skill_registry.unregister_plugin(plugin.source)
|
|
115
|
+
self._command_registry.unregister_plugin(plugin.source)
|
|
116
|
+
self._agent_registry.unregister_plugin(plugin.source)
|
|
117
|
+
|
|
118
|
+
errors.extend(await self._register_skills(candidates))
|
|
119
|
+
errors.extend(await self._register_commands(candidates))
|
|
120
|
+
errors.extend(await self._register_agents(candidates))
|
|
121
|
+
errors.extend(await self._register_hooks_atomic(candidates))
|
|
122
|
+
self._loaded = candidates
|
|
123
|
+
return PluginLoadResult(enabled=candidates, errors=errors)
|
|
124
|
+
|
|
125
|
+
async def disable_plugin(self, plugin_source: str) -> None:
|
|
126
|
+
self._skill_registry.unregister_plugin(plugin_source)
|
|
127
|
+
self._command_registry.unregister_plugin(plugin_source)
|
|
128
|
+
self._agent_registry.unregister_plugin(plugin_source)
|
|
129
|
+
snapshot = self._get_snapshot()
|
|
130
|
+
if snapshot is not None:
|
|
131
|
+
snapshot.remove_plugin_hooks(plugin_source)
|
|
132
|
+
self._loaded = [plugin for plugin in self._loaded if plugin.source != plugin_source]
|
|
133
|
+
|
|
134
|
+
async def _discover(self, errors: list[PluginError]) -> list[LoadedPlugin]:
|
|
135
|
+
plugins: list[LoadedPlugin] = []
|
|
136
|
+
plugins.extend(self._discover_builtin())
|
|
137
|
+
local_plugins, local_errors = await self._discover_local()
|
|
138
|
+
plugins.extend(local_plugins)
|
|
139
|
+
errors.extend(local_errors)
|
|
140
|
+
return plugins
|
|
141
|
+
|
|
142
|
+
def _discover_builtin(self) -> list[LoadedPlugin]:
|
|
143
|
+
plugin_config = PluginConfigLoader.load(self._cfg)
|
|
144
|
+
loaded: list[LoadedPlugin] = []
|
|
145
|
+
for definition in self._builtin_registry.all():
|
|
146
|
+
plugin_id = f"{definition.name}@builtin"
|
|
147
|
+
enabled = plugin_config.is_enabled(plugin_id, default=definition.default_enabled)
|
|
148
|
+
if definition.is_available and not definition.is_available():
|
|
149
|
+
continue
|
|
150
|
+
loaded.append(
|
|
151
|
+
LoadedPlugin(
|
|
152
|
+
name=definition.name,
|
|
153
|
+
description=definition.description,
|
|
154
|
+
version=definition.version,
|
|
155
|
+
path="builtin",
|
|
156
|
+
source=plugin_id,
|
|
157
|
+
enabled=enabled,
|
|
158
|
+
is_builtin=True,
|
|
159
|
+
hooks_config=definition.hooks,
|
|
160
|
+
mcp_servers=definition.mcp_servers or {},
|
|
161
|
+
applicable_containers=definition.applicable_containers,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
return loaded
|
|
165
|
+
|
|
166
|
+
async def _discover_local(self) -> tuple[list[LoadedPlugin], list[PluginError]]:
|
|
167
|
+
plugins_dir = self._cfg.plugins_dir
|
|
168
|
+
if not plugins_dir.exists():
|
|
169
|
+
return [], []
|
|
170
|
+
|
|
171
|
+
plugin_config = PluginConfigLoader.load(self._cfg)
|
|
172
|
+
plugins: list[LoadedPlugin] = []
|
|
173
|
+
errors: list[PluginError] = []
|
|
174
|
+
for plugin_dir in sorted(plugins_dir.iterdir()):
|
|
175
|
+
if not plugin_dir.is_dir():
|
|
176
|
+
continue
|
|
177
|
+
plugin_id = f"{plugin_dir.name}@local"
|
|
178
|
+
if not plugin_config.is_enabled(plugin_id, default=True):
|
|
179
|
+
continue
|
|
180
|
+
loaded, error = await self._load_local_plugin(plugin_dir, plugin_id)
|
|
181
|
+
if loaded is not None:
|
|
182
|
+
plugins.append(loaded)
|
|
183
|
+
if error is not None:
|
|
184
|
+
errors.append(error)
|
|
185
|
+
return plugins, errors
|
|
186
|
+
|
|
187
|
+
async def _load_local_plugin(
|
|
188
|
+
self, plugin_dir: Path, plugin_id: str
|
|
189
|
+
) -> tuple[LoadedPlugin | None, PluginError | None]:
|
|
190
|
+
manifest_path = plugin_dir / "plugin.json"
|
|
191
|
+
if manifest_path.exists():
|
|
192
|
+
try:
|
|
193
|
+
raw = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
194
|
+
manifest = PluginManifestSchema.validate(raw)
|
|
195
|
+
except json.JSONDecodeError as exc:
|
|
196
|
+
return (
|
|
197
|
+
None,
|
|
198
|
+
ManifestParseError(
|
|
199
|
+
source=plugin_id,
|
|
200
|
+
manifest_path=str(manifest_path),
|
|
201
|
+
parse_error=str(exc),
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
except ValidationError as exc:
|
|
205
|
+
return (
|
|
206
|
+
None,
|
|
207
|
+
ManifestValidationError(
|
|
208
|
+
source=plugin_id,
|
|
209
|
+
manifest_path=str(manifest_path),
|
|
210
|
+
validation_errors=exc.messages,
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
except OSError as exc:
|
|
214
|
+
return (
|
|
215
|
+
None,
|
|
216
|
+
GenericPluginError(
|
|
217
|
+
source=plugin_id,
|
|
218
|
+
plugin=plugin_dir.name,
|
|
219
|
+
error=str(exc),
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
manifest = MinimalManifest(name=plugin_dir.name)
|
|
224
|
+
|
|
225
|
+
skills_dir = plugin_dir / manifest.skills_path
|
|
226
|
+
commands_dir = plugin_dir / manifest.commands_path
|
|
227
|
+
agents_dir = plugin_dir / manifest.agents_path
|
|
228
|
+
loaded = LoadedPlugin(
|
|
229
|
+
name=manifest.name,
|
|
230
|
+
description=manifest.description,
|
|
231
|
+
version=manifest.version,
|
|
232
|
+
path=str(plugin_dir),
|
|
233
|
+
source=plugin_id,
|
|
234
|
+
enabled=True,
|
|
235
|
+
is_builtin=False,
|
|
236
|
+
skills_dir=skills_dir if skills_dir.exists() else None,
|
|
237
|
+
commands_dir=commands_dir if commands_dir.exists() else None,
|
|
238
|
+
agents_dir=agents_dir if agents_dir.exists() else None,
|
|
239
|
+
hooks_config=manifest.hooks,
|
|
240
|
+
mcp_servers=manifest.mcp_servers,
|
|
241
|
+
dependencies=manifest.dependencies,
|
|
242
|
+
applicable_containers=manifest.applicable_containers,
|
|
243
|
+
)
|
|
244
|
+
return loaded, None
|
|
245
|
+
|
|
246
|
+
async def _register_skills(self, plugins: list[LoadedPlugin]) -> list[PluginError]:
|
|
247
|
+
errors: list[PluginError] = []
|
|
248
|
+
for plugin in plugins:
|
|
249
|
+
errors.extend(self._skill_registry.register(plugin))
|
|
250
|
+
return errors
|
|
251
|
+
|
|
252
|
+
async def _register_commands(self, plugins: list[LoadedPlugin]) -> list[PluginError]:
|
|
253
|
+
errors: list[PluginError] = []
|
|
254
|
+
for plugin in plugins:
|
|
255
|
+
errors.extend(self._command_registry.register(plugin))
|
|
256
|
+
return errors
|
|
257
|
+
|
|
258
|
+
async def _register_agents(self, plugins: list[LoadedPlugin]) -> list[PluginError]:
|
|
259
|
+
errors: list[PluginError] = []
|
|
260
|
+
for plugin in plugins:
|
|
261
|
+
errors.extend(self._agent_registry.register(plugin))
|
|
262
|
+
return errors
|
|
263
|
+
|
|
264
|
+
async def _register_hooks_atomic(self, plugins: list[LoadedPlugin]) -> list[PluginError]:
|
|
265
|
+
snapshot = self._get_snapshot()
|
|
266
|
+
if snapshot is None:
|
|
267
|
+
return [
|
|
268
|
+
GenericPluginError(
|
|
269
|
+
source="plugin-loader",
|
|
270
|
+
plugin="hook_engine",
|
|
271
|
+
error="hook engine snapshot is unavailable",
|
|
272
|
+
)
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
errors: list[PluginError] = []
|
|
276
|
+
specs: list[HookSpec] = []
|
|
277
|
+
for plugin in plugins:
|
|
278
|
+
if plugin.is_builtin:
|
|
279
|
+
continue
|
|
280
|
+
if not plugin.hooks_config:
|
|
281
|
+
continue
|
|
282
|
+
built_specs, built_errors = self._convert_hooks_to_specs(plugin)
|
|
283
|
+
specs.extend(built_specs)
|
|
284
|
+
errors.extend(built_errors)
|
|
285
|
+
|
|
286
|
+
snapshot.replace_plugin_hooks_atomic(specs)
|
|
287
|
+
return errors
|
|
288
|
+
|
|
289
|
+
def _convert_hooks_to_specs(
|
|
290
|
+
self, plugin: LoadedPlugin
|
|
291
|
+
) -> tuple[list[HookSpec], list[PluginError]]:
|
|
292
|
+
specs: list[HookSpec] = []
|
|
293
|
+
errors: list[PluginError] = []
|
|
294
|
+
hooks_config = plugin.hooks_config or {}
|
|
295
|
+
if not isinstance(hooks_config, dict):
|
|
296
|
+
return specs, errors
|
|
297
|
+
|
|
298
|
+
event_map = {event.name: event for event in HookEvent}
|
|
299
|
+
for event_name, matchers in hooks_config.items():
|
|
300
|
+
if event_name not in event_map:
|
|
301
|
+
logger.warning(
|
|
302
|
+
"plugin %s has unknown hook event %r, skipping",
|
|
303
|
+
plugin.source,
|
|
304
|
+
event_name,
|
|
305
|
+
)
|
|
306
|
+
continue
|
|
307
|
+
event = event_map[event_name]
|
|
308
|
+
if not isinstance(matchers, list):
|
|
309
|
+
continue
|
|
310
|
+
for matcher_entry in matchers:
|
|
311
|
+
if not isinstance(matcher_entry, dict):
|
|
312
|
+
continue
|
|
313
|
+
matcher = self._extract_matcher(matcher_entry.get("matcher", {}))
|
|
314
|
+
for hook_entry in matcher_entry.get("hooks", []):
|
|
315
|
+
spec_or_error = self._build_spec(plugin, event, matcher, hook_entry)
|
|
316
|
+
if isinstance(spec_or_error, HookSpec):
|
|
317
|
+
specs.append(spec_or_error)
|
|
318
|
+
else:
|
|
319
|
+
errors.append(spec_or_error)
|
|
320
|
+
return specs, errors
|
|
321
|
+
|
|
322
|
+
def _build_spec(
|
|
323
|
+
self,
|
|
324
|
+
plugin: LoadedPlugin,
|
|
325
|
+
event: HookEvent,
|
|
326
|
+
matcher: str,
|
|
327
|
+
hook_entry: Any,
|
|
328
|
+
) -> HookSpec | HookLoadFailedError:
|
|
329
|
+
if not isinstance(hook_entry, dict):
|
|
330
|
+
return HookLoadFailedError(
|
|
331
|
+
source=plugin.source,
|
|
332
|
+
plugin=plugin.name,
|
|
333
|
+
hook_path=f"plugin.json/hooks/{event.name}",
|
|
334
|
+
reason="hook entry must be object",
|
|
335
|
+
)
|
|
336
|
+
executor_type = hook_entry.get("type")
|
|
337
|
+
if executor_type not in ("command", "http", "prompt", "agent"):
|
|
338
|
+
return HookLoadFailedError(
|
|
339
|
+
source=plugin.source,
|
|
340
|
+
plugin=plugin.name,
|
|
341
|
+
hook_path=f"plugin.json/hooks/{event.name}",
|
|
342
|
+
reason=f"unsupported executor type in manifest: {executor_type!r}",
|
|
343
|
+
)
|
|
344
|
+
if executor_type == "http" and event == HookEvent.SESSION_START:
|
|
345
|
+
return HookLoadFailedError(
|
|
346
|
+
source=plugin.source,
|
|
347
|
+
plugin=plugin.name,
|
|
348
|
+
hook_path=f"plugin.json/hooks/{event.name}",
|
|
349
|
+
reason="http hooks cannot be used with SessionStart event",
|
|
350
|
+
)
|
|
351
|
+
timeout = hook_entry.get("timeout", self._default_timeout(executor_type))
|
|
352
|
+
return HookSpec(
|
|
353
|
+
event=event,
|
|
354
|
+
executor_type=executor_type,
|
|
355
|
+
config=dict(hook_entry),
|
|
356
|
+
matcher=matcher,
|
|
357
|
+
timeout=float(timeout),
|
|
358
|
+
source="plugin",
|
|
359
|
+
plugin_source=plugin.source,
|
|
360
|
+
once=bool(hook_entry.get("once", False)),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
@staticmethod
|
|
364
|
+
def _extract_matcher(matcher: Any) -> str:
|
|
365
|
+
if isinstance(matcher, dict):
|
|
366
|
+
tool_name = matcher.get("tool_name")
|
|
367
|
+
if isinstance(tool_name, str) and tool_name.strip():
|
|
368
|
+
return tool_name.strip()
|
|
369
|
+
return "*"
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
def _default_timeout(executor_type: str) -> float:
|
|
373
|
+
if executor_type == "http":
|
|
374
|
+
return 30.0
|
|
375
|
+
return 60.0
|
|
376
|
+
|
|
377
|
+
def _get_snapshot(self) -> HooksConfigSnapshot | None:
|
|
378
|
+
snapshot = getattr(self._hook_engine, "snapshot", None)
|
|
379
|
+
if snapshot is None:
|
|
380
|
+
snapshot = getattr(self._hook_engine, "_snapshot", None)
|
|
381
|
+
if isinstance(snapshot, HooksConfigSnapshot):
|
|
382
|
+
return snapshot
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
__all__ = ["PluginLoader"]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
plugin/manifest.py — plugin.json 解析与校验。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
定义 PluginManifest/MinimalManifest,并提供 PluginManifestSchema.validate()。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
PluginLoader._load_local_plugin() 在构建 LoadedPlugin 前调用本模块。
|
|
9
|
+
|
|
10
|
+
当前裁剪范围:
|
|
11
|
+
仅覆盖 P0/P1 校验规则,不包含 mcp_servers 深度校验与依赖拓扑解析。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
import re
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from langchain_agentx.loop.hook.types import HookEvent
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class PluginManifest:
|
|
25
|
+
name: str
|
|
26
|
+
description: str = ""
|
|
27
|
+
version: str = "0.1.0"
|
|
28
|
+
applicable_containers: list[str] = field(default_factory=list)
|
|
29
|
+
dependencies: list[str] = field(default_factory=list)
|
|
30
|
+
skills_path: str = "skills"
|
|
31
|
+
commands_path: str = "commands"
|
|
32
|
+
agents_path: str = "agents"
|
|
33
|
+
hooks: dict[str, Any] | None = None
|
|
34
|
+
mcp_servers: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class MinimalManifest(PluginManifest):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ValidationError(Exception):
|
|
43
|
+
"""manifest 校验错误。"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, messages: list[str]) -> None:
|
|
46
|
+
super().__init__("; ".join(messages))
|
|
47
|
+
self.messages = messages
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PluginManifestSchema:
|
|
51
|
+
"""plugin.json 校验器。"""
|
|
52
|
+
|
|
53
|
+
_name_pattern = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$")
|
|
54
|
+
_semver_pattern = re.compile(r"^\d+\.\d+\.\d+$")
|
|
55
|
+
_allowed_hook_types = {"command", "http", "prompt", "agent"}
|
|
56
|
+
_http_disallowed_events = {"SessionStart"}
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def validate(cls, raw: dict[str, Any]) -> PluginManifest:
|
|
60
|
+
if not isinstance(raw, dict):
|
|
61
|
+
raise ValidationError(["manifest root must be object"])
|
|
62
|
+
|
|
63
|
+
errors: list[str] = []
|
|
64
|
+
name = raw.get("name")
|
|
65
|
+
if not isinstance(name, str) or not cls._name_pattern.fullmatch(name):
|
|
66
|
+
errors.append("name must match ^[a-z0-9][a-z0-9-]{0,63}$")
|
|
67
|
+
|
|
68
|
+
description = raw.get("description", "")
|
|
69
|
+
if description is not None and not isinstance(description, str):
|
|
70
|
+
errors.append("description must be string")
|
|
71
|
+
description = description if isinstance(description, str) else ""
|
|
72
|
+
|
|
73
|
+
version = raw.get("version", "0.1.0")
|
|
74
|
+
if not isinstance(version, str) or not cls._semver_pattern.fullmatch(version):
|
|
75
|
+
errors.append("version must be semver x.y.z")
|
|
76
|
+
|
|
77
|
+
applicable = raw.get("applicable_containers", [])
|
|
78
|
+
if not isinstance(applicable, list) or not all(isinstance(v, str) for v in applicable):
|
|
79
|
+
errors.append("applicable_containers must be list[str]")
|
|
80
|
+
applicable = []
|
|
81
|
+
|
|
82
|
+
dependencies = raw.get("dependencies", [])
|
|
83
|
+
if not isinstance(dependencies, list) or not all(isinstance(v, str) for v in dependencies):
|
|
84
|
+
errors.append("dependencies must be list[str]")
|
|
85
|
+
dependencies = []
|
|
86
|
+
|
|
87
|
+
skills_path = raw.get("skills_path", "skills")
|
|
88
|
+
commands_path = raw.get("commands_path", "commands")
|
|
89
|
+
agents_path = raw.get("agents_path", "agents")
|
|
90
|
+
for field_name, value in (
|
|
91
|
+
("skills_path", skills_path),
|
|
92
|
+
("commands_path", commands_path),
|
|
93
|
+
("agents_path", agents_path),
|
|
94
|
+
):
|
|
95
|
+
if not isinstance(value, str) or not value.strip():
|
|
96
|
+
errors.append(f"{field_name} must be non-empty string")
|
|
97
|
+
|
|
98
|
+
hooks = raw.get("hooks")
|
|
99
|
+
if hooks is not None:
|
|
100
|
+
cls._validate_hooks(hooks, errors)
|
|
101
|
+
|
|
102
|
+
mcp_servers = raw.get("mcp_servers", {})
|
|
103
|
+
if not isinstance(mcp_servers, dict):
|
|
104
|
+
errors.append("mcp_servers must be object")
|
|
105
|
+
mcp_servers = {}
|
|
106
|
+
|
|
107
|
+
if errors:
|
|
108
|
+
raise ValidationError(errors)
|
|
109
|
+
|
|
110
|
+
return PluginManifest(
|
|
111
|
+
name=name,
|
|
112
|
+
description=description,
|
|
113
|
+
version=version,
|
|
114
|
+
applicable_containers=applicable,
|
|
115
|
+
dependencies=dependencies,
|
|
116
|
+
skills_path=skills_path,
|
|
117
|
+
commands_path=commands_path,
|
|
118
|
+
agents_path=agents_path,
|
|
119
|
+
hooks=hooks if isinstance(hooks, dict) else None,
|
|
120
|
+
mcp_servers=mcp_servers,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def _validate_hooks(cls, hooks: Any, errors: list[str]) -> None:
|
|
125
|
+
if not isinstance(hooks, dict):
|
|
126
|
+
errors.append("hooks must be object")
|
|
127
|
+
return
|
|
128
|
+
valid_event_names = {event.name for event in HookEvent}
|
|
129
|
+
for event_name, matchers in hooks.items():
|
|
130
|
+
if not isinstance(event_name, str):
|
|
131
|
+
errors.append("hook event key must be string")
|
|
132
|
+
continue
|
|
133
|
+
if event_name not in valid_event_names:
|
|
134
|
+
# 未知事件只告警级别,忽略该 key,不阻断整个 manifest
|
|
135
|
+
continue
|
|
136
|
+
if not isinstance(matchers, list):
|
|
137
|
+
errors.append(f"hooks.{event_name} must be list")
|
|
138
|
+
continue
|
|
139
|
+
for matcher in matchers:
|
|
140
|
+
if not isinstance(matcher, dict):
|
|
141
|
+
errors.append(f"hooks.{event_name} matcher must be object")
|
|
142
|
+
continue
|
|
143
|
+
hook_items = matcher.get("hooks", [])
|
|
144
|
+
if not isinstance(hook_items, list):
|
|
145
|
+
errors.append(f"hooks.{event_name}.hooks must be list")
|
|
146
|
+
continue
|
|
147
|
+
for hook in hook_items:
|
|
148
|
+
if not isinstance(hook, dict):
|
|
149
|
+
errors.append(f"hooks.{event_name}.hooks[] must be object")
|
|
150
|
+
# hook type/http+SessionStart 语义校验由 loader._build_spec 负责,
|
|
151
|
+
# 此处只做结构性检查,不整体拒绝 manifest。
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
__all__ = ["MinimalManifest", "PluginManifest", "PluginManifestSchema", "ValidationError"]
|