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,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/ripgrep_plugin_exclusions.py — Plugin 缓存孤儿版本的 ripgrep --glob 排除
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
对齐 CC src/utils/plugins/orphanedPluginFilter.ts:在搜索路径与 plugin cache 重叠时,
|
|
6
|
+
生成 `!**/<relativeVersionDir>/**` 列表,避免 Grep/Glob 命中带 `.orphaned_at` 的旧版本目录。
|
|
7
|
+
|
|
8
|
+
链路位置:
|
|
9
|
+
ToolRuntimeLoader.register_default_tools -> 构造 PluginCacheGlobExclusions
|
|
10
|
+
-> GrepRuntimeTool / GlobRuntimeTool.invoke -> RipgrepBackend._build_args / build_rg_files_args
|
|
11
|
+
|
|
12
|
+
当前裁剪范围:
|
|
13
|
+
仅基于 workspace 的 `plugin_cache_dir`;不实现 CC 的 main.tsx 预热与 /reload-plugins 命令
|
|
14
|
+
(clear_cache 供后续插件热重载接线)。失败时 best-effort 返回空列表,不阻断搜索。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
import threading
|
|
22
|
+
|
|
23
|
+
from langchain_agentx.tools.grep.backend import run_ripgrep_lines
|
|
24
|
+
from langchain_agentx.utils.rg_executable import default_rg_executable
|
|
25
|
+
from langchain_agentx.utils.subprocess_text import (
|
|
26
|
+
TextSubprocessRunner,
|
|
27
|
+
default_text_subprocess_runner,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_ORPHANED_AT = ".orphaned_at"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PluginCacheGlobExclusions:
|
|
34
|
+
"""会话级缓存的 plugin cache `--glob` 排除串(与 CC getGlobExclusionsForPluginCache 语义对齐)。"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
cache_root: str | None,
|
|
40
|
+
rg_path: str | None = None,
|
|
41
|
+
timeout: int = 20,
|
|
42
|
+
text_runner: TextSubprocessRunner | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
if cache_root:
|
|
45
|
+
self._cache_root = os.path.normpath(os.path.realpath(os.path.expanduser(cache_root)))
|
|
46
|
+
else:
|
|
47
|
+
self._cache_root = None
|
|
48
|
+
self._rg_path = rg_path if rg_path is not None else default_rg_executable()
|
|
49
|
+
self._timeout = int(timeout)
|
|
50
|
+
self._text_runner = text_runner or default_text_subprocess_runner()
|
|
51
|
+
self._cached: list[str] | None = None
|
|
52
|
+
self._lock = threading.Lock()
|
|
53
|
+
|
|
54
|
+
def clear_cache(self) -> None:
|
|
55
|
+
"""对齐 CC clearPluginCacheExclusions:插件集重载后应调用以重新扫描 marker。"""
|
|
56
|
+
with self._lock:
|
|
57
|
+
self._cached = None
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _normalize_for_compare(path: str) -> str:
|
|
61
|
+
n = os.path.normpath(os.path.realpath(os.path.expanduser(path)))
|
|
62
|
+
if sys.platform == "win32":
|
|
63
|
+
return n.lower()
|
|
64
|
+
return n
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def paths_overlap(search_path: str, cache_root: str) -> bool:
|
|
68
|
+
"""与 CC orphanedPluginFilter.pathsOverlap 等价(含根路径与前缀关系)。"""
|
|
69
|
+
na = PluginCacheGlobExclusions._normalize_for_compare(search_path)
|
|
70
|
+
nb = PluginCacheGlobExclusions._normalize_for_compare(cache_root)
|
|
71
|
+
sep = os.sep
|
|
72
|
+
return (
|
|
73
|
+
na == nb
|
|
74
|
+
or na == sep
|
|
75
|
+
or nb == sep
|
|
76
|
+
or na.startswith(nb + sep)
|
|
77
|
+
or nb.startswith(na + sep)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def get_exclusions_for_search(self, search_path: str) -> list[str]:
|
|
81
|
+
"""
|
|
82
|
+
若 search_path 与 plugin cache 根无路径重叠,返回 [](与 CC 短路一致)。
|
|
83
|
+
否则返回缓存的排除 glob 列表(首次调用时扫描 `.orphaned_at`)。
|
|
84
|
+
"""
|
|
85
|
+
if not self._cache_root:
|
|
86
|
+
return []
|
|
87
|
+
sp = os.path.normpath(os.path.realpath(os.path.expanduser(search_path)))
|
|
88
|
+
if not self.paths_overlap(sp, self._cache_root):
|
|
89
|
+
return []
|
|
90
|
+
with self._lock:
|
|
91
|
+
if self._cached is not None:
|
|
92
|
+
return list(self._cached)
|
|
93
|
+
self._cached = self._compute_exclusions_locked()
|
|
94
|
+
return list(self._cached)
|
|
95
|
+
|
|
96
|
+
def _compute_exclusions_locked(self) -> list[str]:
|
|
97
|
+
try:
|
|
98
|
+
if not os.path.isdir(self._cache_root):
|
|
99
|
+
return []
|
|
100
|
+
args = [
|
|
101
|
+
"--files",
|
|
102
|
+
"--hidden",
|
|
103
|
+
"--no-ignore",
|
|
104
|
+
"--max-depth",
|
|
105
|
+
"4",
|
|
106
|
+
"--glob",
|
|
107
|
+
_ORPHANED_AT,
|
|
108
|
+
]
|
|
109
|
+
lines = run_ripgrep_lines(
|
|
110
|
+
self._rg_path,
|
|
111
|
+
args,
|
|
112
|
+
self._cache_root,
|
|
113
|
+
timeout=self._timeout,
|
|
114
|
+
text_runner=self._text_runner,
|
|
115
|
+
is_retry=False,
|
|
116
|
+
)
|
|
117
|
+
out: list[str] = []
|
|
118
|
+
cache = self._cache_root
|
|
119
|
+
for marker_path in lines:
|
|
120
|
+
mp = marker_path.strip()
|
|
121
|
+
if not mp:
|
|
122
|
+
continue
|
|
123
|
+
version_dir = os.path.dirname(mp)
|
|
124
|
+
if os.path.isabs(version_dir):
|
|
125
|
+
try:
|
|
126
|
+
rel = os.path.relpath(version_dir, cache)
|
|
127
|
+
except ValueError:
|
|
128
|
+
continue
|
|
129
|
+
else:
|
|
130
|
+
rel = version_dir
|
|
131
|
+
posix_rel = rel.replace("\\", "/")
|
|
132
|
+
if not posix_rel or posix_rel == ".":
|
|
133
|
+
continue
|
|
134
|
+
out.append(f"!**/{posix_rel}/**")
|
|
135
|
+
return out
|
|
136
|
+
except Exception:
|
|
137
|
+
return []
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/skill/argument_substitution.py — skill 参数展开(CC argumentSubstitution.ts 对齐子集)
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
``parse_arguments`` / ``parse_argument_names`` / ``substitute_arguments``,
|
|
6
|
+
支持 ``$ARGUMENTS``、``$ARGUMENTS[n]``、``$n`` 及 frontmatter ``arguments`` 定义的具名 ``$name``。
|
|
7
|
+
|
|
8
|
+
链路位置:
|
|
9
|
+
``SkillContentExpander.expand()`` → ``substitute_arguments()``。
|
|
10
|
+
|
|
11
|
+
CC 对照:
|
|
12
|
+
``src/utils/argumentSubstitution.ts``;本实现以 ``shlex.split`` 近似 CC 的 shell-quote 解析。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
import shlex
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_arguments(args: str | None) -> list[str]:
|
|
24
|
+
"""将参数字符串拆成 token 列表(对齐 CC ``parseArguments``)。"""
|
|
25
|
+
if args is None or not args.strip():
|
|
26
|
+
return []
|
|
27
|
+
try:
|
|
28
|
+
return shlex.split(args, posix=True)
|
|
29
|
+
except ValueError:
|
|
30
|
+
return [p for p in args.split() if p]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_argument_names(argument_names: Any) -> list[str]:
|
|
34
|
+
"""解析 frontmatter ``arguments``(对齐 CC ``parseArgumentNames``)。"""
|
|
35
|
+
|
|
36
|
+
def _valid(name: str) -> bool:
|
|
37
|
+
return bool(name.strip()) and not name.strip().isdigit()
|
|
38
|
+
|
|
39
|
+
if not argument_names:
|
|
40
|
+
return []
|
|
41
|
+
if isinstance(argument_names, list):
|
|
42
|
+
return [str(x).strip() for x in argument_names if _valid(str(x))]
|
|
43
|
+
if isinstance(argument_names, str):
|
|
44
|
+
return [p for p in argument_names.split() if _valid(p)]
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def substitute_arguments(
|
|
49
|
+
content: str,
|
|
50
|
+
args: str | None,
|
|
51
|
+
*,
|
|
52
|
+
append_if_no_placeholder: bool = True,
|
|
53
|
+
argument_names: Sequence[str] | None = None,
|
|
54
|
+
) -> str:
|
|
55
|
+
"""替换正文中的参数占位符(对齐 CC ``substituteArguments`` 顺序与语义)。"""
|
|
56
|
+
if args is None:
|
|
57
|
+
return content
|
|
58
|
+
|
|
59
|
+
names = list(argument_names or [])
|
|
60
|
+
parsed = parse_arguments(args)
|
|
61
|
+
original = content
|
|
62
|
+
|
|
63
|
+
for i, name in enumerate(names):
|
|
64
|
+
if not name:
|
|
65
|
+
continue
|
|
66
|
+
pat = re.compile(rf"\${re.escape(name)}(?!\[|\w)")
|
|
67
|
+
rep = parsed[i] if i < len(parsed) else ""
|
|
68
|
+
content = pat.sub(rep, content)
|
|
69
|
+
|
|
70
|
+
def _tok_at(parsed_args: list[str], m: re.Match[str]) -> str:
|
|
71
|
+
idx = int(m.group(1))
|
|
72
|
+
return parsed_args[idx] if 0 <= idx < len(parsed_args) else ""
|
|
73
|
+
|
|
74
|
+
content = re.sub(r"\$ARGUMENTS\[(\d+)\]", lambda m: _tok_at(parsed, m), content)
|
|
75
|
+
content = re.sub(r"\$(\d+)(?!\w)", lambda m: _tok_at(parsed, m), content)
|
|
76
|
+
content = content.replace("$ARGUMENTS", args)
|
|
77
|
+
|
|
78
|
+
if content == original and append_if_no_placeholder and args:
|
|
79
|
+
content = f"{content}\n\nARGUMENTS: {args}"
|
|
80
|
+
return content
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/skill/loader.py — Skill 加载协作者
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
- SkillFrontmatterParser:解析 SKILL.md frontmatter + body
|
|
6
|
+
- SkillCommandRepository:从 skills 根目录加载并查找技能
|
|
7
|
+
- SkillContentExpander:注入 ``Base directory for this skill:`` 前缀后执行参数展开
|
|
8
|
+
|
|
9
|
+
链路位置:
|
|
10
|
+
SkillRuntimeTool.invoke()
|
|
11
|
+
-> SkillCommandRepository.find_by_name()
|
|
12
|
+
-> SkillContentExpander.expand()
|
|
13
|
+
|
|
14
|
+
CC 对照:
|
|
15
|
+
对齐 CC SkillTool 的 command lookup + prompt expansion 思路,
|
|
16
|
+
但实现为本工程 RuntimeTool 的 OOP 协作者,不走 middleware 旁路。
|
|
17
|
+
frontmatter:支持 ``when_to_use`` / ``whenToUse`` 与 ``description`` 的
|
|
18
|
+
单行值及常见块标量(``|`` / ``>-``,见 loadSkillsDir 的 whenToUse 语义)。
|
|
19
|
+
块标量限制:``_read_literal_block`` 以顶格 ``word:`` 作为块结束;块正文内若出现
|
|
20
|
+
顶格 ``key: value`` 行会被误截断(已知限制,复杂 YAML 请用单行或换用完整 YAML 解析)。
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from .argument_substitution import parse_argument_names, substitute_arguments
|
|
30
|
+
from .models import SkillCommand
|
|
31
|
+
|
|
32
|
+
# CC loadSkillsDir:when_to_use / description 常为块标量(| / >-);须在 parser 层取出独立字符串
|
|
33
|
+
_BLOCK_TEXT_KEYS = frozenset({"when_to_use", "whenToUse", "description", "argument-hint", "argument_hint"})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def normalize_skill_name(skill_name: str) -> str:
|
|
37
|
+
return skill_name.strip().lstrip("/")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalize_when_to_use_raw(raw: Any) -> str | None:
|
|
41
|
+
"""将 frontmatter 中的 when_to_use 规范为单行或多行字符串(CC whenToUse)。"""
|
|
42
|
+
if isinstance(raw, str) and raw.strip():
|
|
43
|
+
return raw.strip()
|
|
44
|
+
if isinstance(raw, list):
|
|
45
|
+
parts = [str(x).strip() for x in raw if str(x).strip()]
|
|
46
|
+
if parts:
|
|
47
|
+
return "\n".join(parts)
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_block_scalar_header(value: str) -> bool:
|
|
52
|
+
"""YAML 块标量起始行:空、``|``、``|-``、``|+``、``>``、``>-`` 等(与 CC SKILL 常见写法一致)。"""
|
|
53
|
+
v = value.strip()
|
|
54
|
+
if not v:
|
|
55
|
+
return True
|
|
56
|
+
return v.startswith("|") or v.startswith(">")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SkillFrontmatterParser:
|
|
60
|
+
_FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n?", re.DOTALL)
|
|
61
|
+
|
|
62
|
+
def parse(self, text: str) -> tuple[dict[str, Any], str]:
|
|
63
|
+
m = self._FRONTMATTER_RE.match(text)
|
|
64
|
+
if not m:
|
|
65
|
+
return {}, text
|
|
66
|
+
frontmatter_text = m.group(1)
|
|
67
|
+
body = text[m.end():]
|
|
68
|
+
return self._parse_simple_yaml(frontmatter_text), body
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _read_literal_block(lines: list[str], start: int) -> tuple[str, int]:
|
|
72
|
+
"""读取 ``key: |`` 后的块,直到下一个顶格 ``word:``(与 CC 常见 SKILL.md 一致)。
|
|
73
|
+
|
|
74
|
+
限制:块内不得出现顶格 ``foo: bar`` 示例行,否则会被当作下一个 frontmatter 键而提前结束。
|
|
75
|
+
"""
|
|
76
|
+
parts: list[str] = []
|
|
77
|
+
j = start
|
|
78
|
+
while j < len(lines):
|
|
79
|
+
raw = lines[j]
|
|
80
|
+
if re.match(r"^[A-Za-z0-9_-]+:\s*", raw) and not raw[:1].isspace():
|
|
81
|
+
break
|
|
82
|
+
parts.append(raw)
|
|
83
|
+
j += 1
|
|
84
|
+
text = "\n".join(parts)
|
|
85
|
+
return text.strip("\n"), j
|
|
86
|
+
|
|
87
|
+
def _parse_simple_yaml(self, frontmatter_text: str) -> dict[str, Any]:
|
|
88
|
+
data: dict[str, Any] = {}
|
|
89
|
+
lines = frontmatter_text.splitlines()
|
|
90
|
+
i = 0
|
|
91
|
+
while i < len(lines):
|
|
92
|
+
stripped = lines[i].strip()
|
|
93
|
+
i += 1
|
|
94
|
+
if not stripped or stripped.startswith("#"):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
m = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", stripped)
|
|
98
|
+
if not m:
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
key, value = m.group(1), m.group(2).strip()
|
|
102
|
+
if key in _BLOCK_TEXT_KEYS and _is_block_scalar_header(value):
|
|
103
|
+
block, i = self._read_literal_block(lines, i)
|
|
104
|
+
if block.strip():
|
|
105
|
+
data[key] = block.strip()
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
if value:
|
|
109
|
+
data[key] = value
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
items: list[str] = []
|
|
113
|
+
while i < len(lines):
|
|
114
|
+
nxt = lines[i].strip()
|
|
115
|
+
if not nxt:
|
|
116
|
+
i += 1
|
|
117
|
+
continue
|
|
118
|
+
if nxt.startswith("- "):
|
|
119
|
+
items.append(nxt[2:].strip())
|
|
120
|
+
i += 1
|
|
121
|
+
continue
|
|
122
|
+
break
|
|
123
|
+
data[key] = items
|
|
124
|
+
return data
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class SkillCommandRepository:
|
|
128
|
+
def __init__(self, skills_root: Path, parser: SkillFrontmatterParser | None = None) -> None:
|
|
129
|
+
self._skills_root = skills_root
|
|
130
|
+
self._parser = parser or SkillFrontmatterParser()
|
|
131
|
+
|
|
132
|
+
def list_commands(self) -> list[SkillCommand]:
|
|
133
|
+
if not self._skills_root.exists():
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
commands: list[SkillCommand] = []
|
|
137
|
+
for skill_dir in self._skills_root.iterdir():
|
|
138
|
+
if not skill_dir.is_dir():
|
|
139
|
+
continue
|
|
140
|
+
skill_md = skill_dir / "SKILL.md"
|
|
141
|
+
if not skill_md.exists():
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
content = skill_md.read_text(encoding="utf-8")
|
|
145
|
+
frontmatter, body = self._parser.parse(content)
|
|
146
|
+
name = str(frontmatter.get("name") or skill_dir.name).strip()
|
|
147
|
+
raw_desc = frontmatter.get("description")
|
|
148
|
+
has_user_desc = isinstance(raw_desc, str) and bool(raw_desc.strip())
|
|
149
|
+
description = raw_desc.strip() if has_user_desc else ""
|
|
150
|
+
raw_wtu = frontmatter.get("when_to_use") or frontmatter.get("whenToUse")
|
|
151
|
+
when_to_use = _normalize_when_to_use_raw(raw_wtu)
|
|
152
|
+
arg_names = parse_argument_names(frontmatter.get("arguments"))
|
|
153
|
+
commands.append(
|
|
154
|
+
SkillCommand(
|
|
155
|
+
name=name,
|
|
156
|
+
description=description,
|
|
157
|
+
content=body.strip(),
|
|
158
|
+
path=skill_md,
|
|
159
|
+
source="local",
|
|
160
|
+
frontmatter=frontmatter,
|
|
161
|
+
has_user_specified_description=has_user_desc,
|
|
162
|
+
when_to_use=when_to_use,
|
|
163
|
+
argument_names=arg_names,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
dedup: dict[str, SkillCommand] = {}
|
|
168
|
+
for cmd in commands:
|
|
169
|
+
dedup.setdefault(normalize_skill_name(cmd.name), cmd)
|
|
170
|
+
return list(dedup.values())
|
|
171
|
+
|
|
172
|
+
def find_by_name(self, skill_name: str) -> SkillCommand | None:
|
|
173
|
+
normalized = normalize_skill_name(skill_name)
|
|
174
|
+
for cmd in self.list_commands():
|
|
175
|
+
if normalize_skill_name(cmd.name) == normalized:
|
|
176
|
+
return cmd
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SkillContentExpander:
|
|
181
|
+
def expand(self, command: SkillCommand, args: str | None) -> str:
|
|
182
|
+
# CC createSkillCommand.getPromptForCommand:先注入 skill 根目录,再 substituteArguments。
|
|
183
|
+
body = command.content
|
|
184
|
+
skill_root = command.path.parent
|
|
185
|
+
try:
|
|
186
|
+
skill_root_s = str(skill_root.resolve())
|
|
187
|
+
except OSError:
|
|
188
|
+
skill_root_s = str(skill_root)
|
|
189
|
+
markdown = f"Base directory for this skill: {skill_root_s}\n\n{body}"
|
|
190
|
+
return substitute_arguments(
|
|
191
|
+
markdown,
|
|
192
|
+
args,
|
|
193
|
+
append_if_no_placeholder=True,
|
|
194
|
+
argument_names=command.argument_names,
|
|
195
|
+
)
|
|
196
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field, field_validator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SkillToolInput(BaseModel):
|
|
11
|
+
skill_name: str = Field(
|
|
12
|
+
...,
|
|
13
|
+
description="Skill name (leading slash is optional).",
|
|
14
|
+
)
|
|
15
|
+
args: str | None = Field(
|
|
16
|
+
default=None,
|
|
17
|
+
description="Optional arguments passed to skill expansion.",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
@field_validator("skill_name")
|
|
21
|
+
@classmethod
|
|
22
|
+
def _validate_skill_name(cls, value: str) -> str:
|
|
23
|
+
name = value.strip()
|
|
24
|
+
if not name:
|
|
25
|
+
raise ValueError("skill_name cannot be empty")
|
|
26
|
+
return name
|
|
27
|
+
|
|
28
|
+
@field_validator("args")
|
|
29
|
+
@classmethod
|
|
30
|
+
def _validate_args_len(cls, value: str | None) -> str | None:
|
|
31
|
+
if value and len(value) > 2000:
|
|
32
|
+
raise ValueError("args too long (max 2000 chars)")
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SkillToolOutput(BaseModel):
|
|
37
|
+
skill_name: str
|
|
38
|
+
content: str
|
|
39
|
+
source: Literal["local", "mcp"] = "local"
|
|
40
|
+
skill_path: str | None = None
|
|
41
|
+
command_name_tag: str | None = None
|
|
42
|
+
allowed_tools: list[str] = Field(default_factory=list)
|
|
43
|
+
model_override: str | None = None
|
|
44
|
+
constraints_applied: dict[str, Any] = Field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _frontmatter_bool(value: Any) -> bool:
|
|
48
|
+
if value is True:
|
|
49
|
+
return True
|
|
50
|
+
if isinstance(value, str) and value.strip().lower() in ("true", "1", "yes", "on"):
|
|
51
|
+
return True
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(slots=True)
|
|
56
|
+
class SkillCommand:
|
|
57
|
+
name: str
|
|
58
|
+
description: str
|
|
59
|
+
content: str
|
|
60
|
+
path: Path
|
|
61
|
+
source: Literal["local", "mcp"] = "local"
|
|
62
|
+
frontmatter: dict[str, Any] = field(default_factory=dict)
|
|
63
|
+
# CC getSlashCommandToolSkills:仅当 frontmatter 显式给出非空 description 时为 True(非正文推导)
|
|
64
|
+
has_user_specified_description: bool = False
|
|
65
|
+
# CC whenToUse / frontmatter when_to_use
|
|
66
|
+
when_to_use: str | None = None
|
|
67
|
+
# CC frontmatter ``arguments`` → parseArgumentNames → $name 占位符
|
|
68
|
+
argument_names: list[str] = field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
def visible_in_slash_command_tool_listing(self) -> bool:
|
|
71
|
+
"""是否出现在模型可见的 skill listing 中。
|
|
72
|
+
|
|
73
|
+
与 CC ``commands/init`` / ``SkillTool`` 侧一致:``disable-model-invocation: true`` 表示
|
|
74
|
+
仅用户侧触发,**不**对模型展示 listing(亦由 ``SkillRuntimeTool.validate_input`` 拦截工具调用)。
|
|
75
|
+
"""
|
|
76
|
+
if not (self.name or "").strip():
|
|
77
|
+
return False
|
|
78
|
+
if not (self.has_user_specified_description or bool((self.when_to_use or "").strip())):
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
disable_mi = _frontmatter_bool(
|
|
82
|
+
self.frontmatter.get("disable-model-invocation")
|
|
83
|
+
or self.frontmatter.get("disable_model_invocation")
|
|
84
|
+
)
|
|
85
|
+
if disable_mi:
|
|
86
|
+
return False
|
|
87
|
+
return self.source in ("local", "mcp")
|
|
88
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/skill/policy.py — Skill frontmatter 约束协作者
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
解析并评估 skill frontmatter 中与执行约束相关的字段,
|
|
6
|
+
当前 v1 支持:
|
|
7
|
+
- allowed-tools
|
|
8
|
+
- paths
|
|
9
|
+
|
|
10
|
+
链路位置:
|
|
11
|
+
SkillRuntimeTool.check_permissions()
|
|
12
|
+
-> SkillConstraintEvaluator.evaluate()
|
|
13
|
+
|
|
14
|
+
注意:
|
|
15
|
+
- 本模块不替代全局 PolicyEngine;全局 deny/allow 仍由 RuntimeTool 基类链路兜底。
|
|
16
|
+
- 这里仅做 skill 自身约束判定,返回 AuthorizationDecision。
|
|
17
|
+
- ``paths`` 是否在 workspace 内:用 ``Path.resolve()`` + ``Path.relative_to``,**不用** ``str.startswith``(避免 Windows 大小写/规范化误判)。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from langchain_agentx.tool_runtime.models import AuthorizationDecision
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _path_is_descendant(candidate: Path, ancestor: Path) -> bool:
|
|
29
|
+
"""candidate 与 ancestor 均已 resolve;在 ancestor 目录树内(含自身)则 True。"""
|
|
30
|
+
try:
|
|
31
|
+
candidate.relative_to(ancestor)
|
|
32
|
+
return True
|
|
33
|
+
except ValueError:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SkillConstraintEvaluator:
|
|
38
|
+
def __init__(self, workspace_root: Path | str) -> None:
|
|
39
|
+
root = Path(workspace_root).expanduser()
|
|
40
|
+
try:
|
|
41
|
+
self._workspace_root = root.resolve()
|
|
42
|
+
except OSError:
|
|
43
|
+
self._workspace_root = root
|
|
44
|
+
|
|
45
|
+
def evaluate(self, frontmatter: dict[str, Any]) -> AuthorizationDecision:
|
|
46
|
+
paths = frontmatter.get("paths")
|
|
47
|
+
if isinstance(paths, list):
|
|
48
|
+
ws = self._workspace_root
|
|
49
|
+
for raw in paths:
|
|
50
|
+
text = str(raw).strip()
|
|
51
|
+
if not text:
|
|
52
|
+
continue
|
|
53
|
+
try:
|
|
54
|
+
candidate = (ws / text).resolve()
|
|
55
|
+
except OSError:
|
|
56
|
+
return AuthorizationDecision(
|
|
57
|
+
behavior="deny",
|
|
58
|
+
message=(
|
|
59
|
+
"Permission denied: skill paths constraint could not "
|
|
60
|
+
f"resolve path entry: {text!r}."
|
|
61
|
+
),
|
|
62
|
+
policy_id="skill_paths_resolve_error",
|
|
63
|
+
)
|
|
64
|
+
if not _path_is_descendant(candidate, ws):
|
|
65
|
+
return AuthorizationDecision(
|
|
66
|
+
behavior="deny",
|
|
67
|
+
message="Permission denied: skill paths constraint escapes workspace root.",
|
|
68
|
+
policy_id="skill_paths_outside_workspace",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
allowed_tools = frontmatter.get("allowed-tools")
|
|
72
|
+
if allowed_tools is not None and not isinstance(allowed_tools, list):
|
|
73
|
+
return AuthorizationDecision(
|
|
74
|
+
behavior="deny",
|
|
75
|
+
message="Permission denied: invalid 'allowed-tools' frontmatter format.",
|
|
76
|
+
policy_id="skill_allowed_tools_invalid",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return AuthorizationDecision(behavior="allow")
|
|
80
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
TOOL_NAME = "skill"
|
|
4
|
+
COMMAND_NAME_TAG = "command-name"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_prompt() -> str:
|
|
8
|
+
return (
|
|
9
|
+
"Execute a skill within the main conversation.\n\n"
|
|
10
|
+
"When users ask you to perform tasks, check whether any available skill matches.\n"
|
|
11
|
+
"Skills provide specialized capabilities and domain knowledge.\n"
|
|
12
|
+
"If users reference slash-command style requests such as '/commit' or '/review-pr', "
|
|
13
|
+
"treat them as skill invocations.\n\n"
|
|
14
|
+
"How to invoke:\n"
|
|
15
|
+
'- Use this tool with `skill_name` and optional `args`.\n'
|
|
16
|
+
'- Example: `skill_name=\"commit\"`\n'
|
|
17
|
+
'- Example: `skill_name=\"pdf\"`\n'
|
|
18
|
+
'- Example: `skill_name=\"commit\", args=\"-m \'Fix bug\'\"`\n'
|
|
19
|
+
'- Example: `skill_name=\"review-pr\", args=\"123\"`\n'
|
|
20
|
+
'- Example: `skill_name=\"ms-office-suite:pdf\"`\n'
|
|
21
|
+
'- Leading slash is allowed (e.g. `/commit`).\n\n'
|
|
22
|
+
"Important:\n"
|
|
23
|
+
"- Available skills are provided via skill listing reminders/messages in the conversation.\n"
|
|
24
|
+
"- If a matching skill exists, this is a BLOCKING REQUIREMENT: call this tool before "
|
|
25
|
+
"giving any task-specific guidance.\n"
|
|
26
|
+
"- Never mention a skill without calling this tool.\n"
|
|
27
|
+
"- Do not invoke a skill that is already running in the current turn.\n"
|
|
28
|
+
f"- If current turn already contains <{COMMAND_NAME_TAG}>...</{COMMAND_NAME_TAG}>, "
|
|
29
|
+
"the skill is already loaded; follow it directly instead of re-invoking.\n"
|
|
30
|
+
"- Do not use this tool for built-in CLI commands (for example /help, /clear).\n"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DESCRIPTION = get_prompt()
|
|
35
|
+
|