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,375 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/grep/backend.py — RipgrepBackend(ripgrep 子进程封装)
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
构建 rg 参数、执行子进程、解析退出码与 stdout 行列表。
|
|
6
|
+
|
|
7
|
+
在整体链路中的位置:
|
|
8
|
+
GrepRuntimeTool.invoke() -> RipgrepBackend.search() -> run_ripgrep_lines(可选 ctx.cancel_event)
|
|
9
|
+
-> RgSubprocessController 或 TextSubprocessRunner
|
|
10
|
+
|
|
11
|
+
对应 CC:src/utils/ripgrep.ts + GrepTool.ts call() 中参数拼装。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import subprocess
|
|
18
|
+
import threading
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from langchain_agentx.tool_runtime.errors import ToolRuntimeError
|
|
22
|
+
from langchain_agentx.utils.rg_executable import default_rg_executable
|
|
23
|
+
from langchain_agentx.utils.subprocess_text import (
|
|
24
|
+
TextSubprocessRunner,
|
|
25
|
+
default_text_subprocess_runner,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from .models import GrepOutputMode
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GrepTimeoutError(ToolRuntimeError):
|
|
32
|
+
def __init__(self, message: str) -> None:
|
|
33
|
+
super().__init__(message, code="GREP_TIMEOUT")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GrepExecutionError(ToolRuntimeError):
|
|
37
|
+
def __init__(self, message: str) -> None:
|
|
38
|
+
super().__init__(message, code="GREP_EXECUTION_ERROR")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GrepCancelledError(ToolRuntimeError):
|
|
42
|
+
"""协作取消(对齐 CC AbortSignal);由 ``threading.Event`` 触发。"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, message: str = "ripgrep cancelled") -> None:
|
|
45
|
+
super().__init__(message, code="GREP_CANCELLED")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run_ripgrep_lines(
|
|
49
|
+
rg_path: str,
|
|
50
|
+
args: list[str],
|
|
51
|
+
search_path: str,
|
|
52
|
+
*,
|
|
53
|
+
timeout: int,
|
|
54
|
+
text_runner: TextSubprocessRunner,
|
|
55
|
+
is_retry: bool = False,
|
|
56
|
+
cancel_event: threading.Event | None = None,
|
|
57
|
+
) -> list[str]:
|
|
58
|
+
"""
|
|
59
|
+
执行 [rg_path, *args, search_path],解析 stdout 为行列表。
|
|
60
|
+
对齐 CC ripgrep.ts:returncode 1 -> [];EAGAIN 时以 -j 1 重试一次。
|
|
61
|
+
供 RipgrepBackend 与 Glob 的 rg --files 列文件共用。
|
|
62
|
+
|
|
63
|
+
若 ``cancel_event`` 非空,使用可中断子进程路径(``RgSubprocessController``),
|
|
64
|
+
不再走 ``text_runner.run``(便于测试 mock 时仅覆盖无取消路径)。
|
|
65
|
+
"""
|
|
66
|
+
cmd = [rg_path, *args, search_path]
|
|
67
|
+
if cancel_event is not None:
|
|
68
|
+
if cancel_event.is_set():
|
|
69
|
+
raise GrepCancelledError("ripgrep cancelled before start")
|
|
70
|
+
from langchain_agentx.tools.grep.rg_subprocess_controller import RgSubprocessController
|
|
71
|
+
|
|
72
|
+
ctrl = RgSubprocessController()
|
|
73
|
+
try:
|
|
74
|
+
result = ctrl.run_argv_wait_completed(
|
|
75
|
+
cmd, timeout=float(timeout), cancel_event=cancel_event
|
|
76
|
+
)
|
|
77
|
+
except GrepTimeoutError:
|
|
78
|
+
raise
|
|
79
|
+
except GrepCancelledError:
|
|
80
|
+
raise
|
|
81
|
+
else:
|
|
82
|
+
try:
|
|
83
|
+
result = text_runner.run(
|
|
84
|
+
cmd,
|
|
85
|
+
capture_output=True,
|
|
86
|
+
timeout=timeout,
|
|
87
|
+
)
|
|
88
|
+
except subprocess.TimeoutExpired:
|
|
89
|
+
raise GrepTimeoutError(f"ripgrep search timed out after {timeout}s")
|
|
90
|
+
except FileNotFoundError as e:
|
|
91
|
+
raise GrepExecutionError(
|
|
92
|
+
"ripgrep executable not found. On Windows install ripgrep and ensure "
|
|
93
|
+
"'rg.exe' is on PATH; on Unix ensure 'rg' is on PATH."
|
|
94
|
+
) from e
|
|
95
|
+
|
|
96
|
+
if result.returncode == 1:
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
if result.returncode >= 2:
|
|
100
|
+
stderr = (result.stderr or "").strip()
|
|
101
|
+
if (not is_retry) and RipgrepBackend._is_eagain_error(stderr):
|
|
102
|
+
return run_ripgrep_lines(
|
|
103
|
+
rg_path,
|
|
104
|
+
["-j", "1", *args],
|
|
105
|
+
search_path,
|
|
106
|
+
timeout=timeout,
|
|
107
|
+
text_runner=text_runner,
|
|
108
|
+
is_retry=True,
|
|
109
|
+
cancel_event=cancel_event,
|
|
110
|
+
)
|
|
111
|
+
raise GrepExecutionError(f"ripgrep failed: {stderr or f'exit code {result.returncode}'}")
|
|
112
|
+
|
|
113
|
+
out = result.stdout or ""
|
|
114
|
+
if not out.strip():
|
|
115
|
+
return []
|
|
116
|
+
lines = [ln.rstrip("\r") for ln in out.rstrip("\n").split("\n")]
|
|
117
|
+
return [ln for ln in lines if ln]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class RipgrepBackend:
|
|
121
|
+
DEFAULT_TIMEOUT = 20
|
|
122
|
+
MAX_COLUMNS = 500
|
|
123
|
+
VCS_EXCLUDE_DIRS = (".git", ".svn", ".hg", ".bzr", ".jj", ".sl")
|
|
124
|
+
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
rg_path: str | None = None,
|
|
129
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
130
|
+
text_runner: TextSubprocessRunner | None = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
self._rg_path = rg_path if rg_path is not None else default_rg_executable()
|
|
133
|
+
self.timeout = timeout
|
|
134
|
+
self._text_runner = text_runner or default_text_subprocess_runner()
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _parse_glob_patterns(glob_str: str) -> list[str]:
|
|
138
|
+
"""与 CC GrepTool.ts 一致:空格分段,逗号拆分,保留 {...} 段。"""
|
|
139
|
+
patterns: list[str] = []
|
|
140
|
+
for raw in glob_str.split():
|
|
141
|
+
if "{" in raw and "}" in raw:
|
|
142
|
+
patterns.append(raw)
|
|
143
|
+
else:
|
|
144
|
+
patterns.extend(p for p in raw.split(",") if p)
|
|
145
|
+
return patterns
|
|
146
|
+
|
|
147
|
+
def _build_args(
|
|
148
|
+
self,
|
|
149
|
+
pattern: str,
|
|
150
|
+
*,
|
|
151
|
+
glob: str | None,
|
|
152
|
+
ignore_patterns: list[str] | None,
|
|
153
|
+
file_type: str | None,
|
|
154
|
+
output_mode: GrepOutputMode,
|
|
155
|
+
context_before: int | None,
|
|
156
|
+
context_after: int | None,
|
|
157
|
+
context: int | None,
|
|
158
|
+
show_line_numbers: bool,
|
|
159
|
+
case_insensitive: bool,
|
|
160
|
+
multiline: bool,
|
|
161
|
+
plugin_glob_exclusions: list[str] | None = None,
|
|
162
|
+
) -> list[str]:
|
|
163
|
+
args: list[str] = ["--hidden"]
|
|
164
|
+
for d in self.VCS_EXCLUDE_DIRS:
|
|
165
|
+
args.extend(["--glob", f"!{d}"])
|
|
166
|
+
args.extend(["--max-columns", str(self.MAX_COLUMNS)])
|
|
167
|
+
if multiline:
|
|
168
|
+
args.extend(["-U", "--multiline-dotall"])
|
|
169
|
+
if case_insensitive:
|
|
170
|
+
args.append("-i")
|
|
171
|
+
if output_mode == GrepOutputMode.files_with_matches:
|
|
172
|
+
args.append("-l")
|
|
173
|
+
elif output_mode == GrepOutputMode.count:
|
|
174
|
+
args.append("-c")
|
|
175
|
+
if output_mode == GrepOutputMode.content and show_line_numbers:
|
|
176
|
+
args.append("-n")
|
|
177
|
+
if output_mode == GrepOutputMode.content:
|
|
178
|
+
if context is not None:
|
|
179
|
+
args.extend(["-C", str(context)])
|
|
180
|
+
else:
|
|
181
|
+
if context_before is not None:
|
|
182
|
+
args.extend(["-B", str(context_before)])
|
|
183
|
+
if context_after is not None:
|
|
184
|
+
args.extend(["-A", str(context_after)])
|
|
185
|
+
if pattern.startswith("-"):
|
|
186
|
+
args.extend(["-e", pattern])
|
|
187
|
+
else:
|
|
188
|
+
args.append(pattern)
|
|
189
|
+
if file_type:
|
|
190
|
+
args.extend(["--type", file_type])
|
|
191
|
+
if glob:
|
|
192
|
+
for g in self._parse_glob_patterns(glob):
|
|
193
|
+
args.extend(["--glob", g])
|
|
194
|
+
# Add ignore patterns (CC: getFileReadIgnorePatterns + normalizePatternsToPath).
|
|
195
|
+
# We implement the same ripgrep glob semantics:
|
|
196
|
+
# - patterns starting with '/' are treated as repo-root anchored (pass through)
|
|
197
|
+
# - otherwise prefix with '**/' so ripgrep applies them from the search root
|
|
198
|
+
# - negate with '!' to exclude them
|
|
199
|
+
if ignore_patterns:
|
|
200
|
+
for p in ignore_patterns:
|
|
201
|
+
p = str(p).strip()
|
|
202
|
+
if not p:
|
|
203
|
+
continue
|
|
204
|
+
rg_pat = f"!{p}" if p.startswith("/") else f"!**/{p}"
|
|
205
|
+
args.extend(["--glob", rg_pat])
|
|
206
|
+
if plugin_glob_exclusions:
|
|
207
|
+
for ex in plugin_glob_exclusions:
|
|
208
|
+
if ex:
|
|
209
|
+
args.extend(["--glob", ex])
|
|
210
|
+
return args
|
|
211
|
+
|
|
212
|
+
def search(
|
|
213
|
+
self,
|
|
214
|
+
pattern: str,
|
|
215
|
+
search_path: str,
|
|
216
|
+
*,
|
|
217
|
+
glob: str | None = None,
|
|
218
|
+
ignore_patterns: list[str] | None = None,
|
|
219
|
+
file_type: str | None = None,
|
|
220
|
+
output_mode: GrepOutputMode = GrepOutputMode.files_with_matches,
|
|
221
|
+
context_before: int | None = None,
|
|
222
|
+
context_after: int | None = None,
|
|
223
|
+
context: int | None = None,
|
|
224
|
+
show_line_numbers: bool = True,
|
|
225
|
+
case_insensitive: bool = False,
|
|
226
|
+
multiline: bool = False,
|
|
227
|
+
plugin_glob_exclusions: list[str] | None = None,
|
|
228
|
+
cancel_event: threading.Event | None = None,
|
|
229
|
+
) -> list[str]:
|
|
230
|
+
args = self._build_args(
|
|
231
|
+
pattern,
|
|
232
|
+
glob=glob,
|
|
233
|
+
ignore_patterns=ignore_patterns,
|
|
234
|
+
file_type=file_type,
|
|
235
|
+
output_mode=output_mode,
|
|
236
|
+
context_before=context_before,
|
|
237
|
+
context_after=context_after,
|
|
238
|
+
context=context,
|
|
239
|
+
show_line_numbers=show_line_numbers,
|
|
240
|
+
case_insensitive=case_insensitive,
|
|
241
|
+
multiline=multiline,
|
|
242
|
+
plugin_glob_exclusions=plugin_glob_exclusions,
|
|
243
|
+
)
|
|
244
|
+
return self._execute(args, search_path, cancel_event=cancel_event)
|
|
245
|
+
|
|
246
|
+
def _execute(
|
|
247
|
+
self, args: list[str], search_path: str, *, cancel_event: threading.Event | None
|
|
248
|
+
) -> list[str]:
|
|
249
|
+
return self._execute_with_retry(args, search_path, cancel_event=cancel_event)
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def _is_eagain_error(stderr: str) -> bool:
|
|
253
|
+
s = (stderr or "").lower()
|
|
254
|
+
return ("eagain" in s) or ("resource temporarily unavailable" in s)
|
|
255
|
+
|
|
256
|
+
def _execute_with_retry(
|
|
257
|
+
self,
|
|
258
|
+
args: list[str],
|
|
259
|
+
search_path: str,
|
|
260
|
+
*,
|
|
261
|
+
cancel_event: threading.Event | None,
|
|
262
|
+
) -> list[str]:
|
|
263
|
+
"""
|
|
264
|
+
对齐 CC ripgrep.ts 的关键鲁棒性:
|
|
265
|
+
- exit code 1: no matches -> []
|
|
266
|
+
- 遇到疑似 EAGAIN 启动错误时,额外重试一次单线程(-j 1)
|
|
267
|
+
"""
|
|
268
|
+
return run_ripgrep_lines(
|
|
269
|
+
self._rg_path,
|
|
270
|
+
args,
|
|
271
|
+
search_path,
|
|
272
|
+
timeout=self.timeout,
|
|
273
|
+
text_runner=self._text_runner,
|
|
274
|
+
is_retry=False,
|
|
275
|
+
cancel_event=cancel_event,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def search_stream_limited(
|
|
279
|
+
self,
|
|
280
|
+
pattern: str,
|
|
281
|
+
search_path: str,
|
|
282
|
+
*,
|
|
283
|
+
glob: str | None = None,
|
|
284
|
+
ignore_patterns: list[str] | None = None,
|
|
285
|
+
file_type: str | None = None,
|
|
286
|
+
output_mode: GrepOutputMode,
|
|
287
|
+
context_before: int | None = None,
|
|
288
|
+
context_after: int | None = None,
|
|
289
|
+
context: int | None = None,
|
|
290
|
+
show_line_numbers: bool = True,
|
|
291
|
+
case_insensitive: bool = False,
|
|
292
|
+
multiline: bool = False,
|
|
293
|
+
max_lines: int,
|
|
294
|
+
plugin_glob_exclusions: list[str] | None = None,
|
|
295
|
+
cancel_event: threading.Event | None = None,
|
|
296
|
+
) -> list[str]:
|
|
297
|
+
"""
|
|
298
|
+
准流式读取 stdout,达到 max_lines 后提前终止进程。
|
|
299
|
+
仅用于 content / count 两种模式(files_with_matches 需要全量结果排序)。
|
|
300
|
+
"""
|
|
301
|
+
args = self._build_args(
|
|
302
|
+
pattern,
|
|
303
|
+
glob=glob,
|
|
304
|
+
ignore_patterns=ignore_patterns,
|
|
305
|
+
file_type=file_type,
|
|
306
|
+
output_mode=output_mode,
|
|
307
|
+
context_before=context_before,
|
|
308
|
+
context_after=context_after,
|
|
309
|
+
context=context,
|
|
310
|
+
show_line_numbers=show_line_numbers,
|
|
311
|
+
case_insensitive=case_insensitive,
|
|
312
|
+
multiline=multiline,
|
|
313
|
+
plugin_glob_exclusions=plugin_glob_exclusions,
|
|
314
|
+
)
|
|
315
|
+
cmd = [self._rg_path, *args, search_path]
|
|
316
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
317
|
+
raise GrepCancelledError("ripgrep cancelled before start")
|
|
318
|
+
try:
|
|
319
|
+
proc = subprocess.Popen(
|
|
320
|
+
cmd,
|
|
321
|
+
stdout=subprocess.PIPE,
|
|
322
|
+
stderr=subprocess.PIPE,
|
|
323
|
+
text=True,
|
|
324
|
+
)
|
|
325
|
+
except FileNotFoundError as e:
|
|
326
|
+
raise GrepExecutionError(
|
|
327
|
+
"ripgrep executable not found. On Windows install ripgrep and ensure "
|
|
328
|
+
"'rg.exe' is on PATH; on Unix ensure 'rg' is on PATH."
|
|
329
|
+
) from e
|
|
330
|
+
|
|
331
|
+
collected: list[str] = []
|
|
332
|
+
try:
|
|
333
|
+
assert proc.stdout is not None
|
|
334
|
+
for line in proc.stdout:
|
|
335
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
336
|
+
proc.terminate()
|
|
337
|
+
raise GrepCancelledError("ripgrep cancelled")
|
|
338
|
+
if len(collected) >= max_lines:
|
|
339
|
+
break
|
|
340
|
+
ln = line.rstrip("\n").rstrip("\r")
|
|
341
|
+
if ln:
|
|
342
|
+
collected.append(ln)
|
|
343
|
+
|
|
344
|
+
if len(collected) >= max_lines:
|
|
345
|
+
# We intentionally stop early to cap output.
|
|
346
|
+
proc.terminate()
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
stdout_tail, stderr_tail = proc.communicate(timeout=self.timeout)
|
|
350
|
+
except subprocess.TimeoutExpired:
|
|
351
|
+
proc.kill()
|
|
352
|
+
raise GrepTimeoutError(f"ripgrep search timed out after {self.timeout}s")
|
|
353
|
+
|
|
354
|
+
# Merge any remaining stdout (rare) if we didn't early stop
|
|
355
|
+
if stdout_tail and len(collected) < max_lines:
|
|
356
|
+
for ln in stdout_tail.rstrip("\n").split("\n"):
|
|
357
|
+
if not ln or len(collected) >= max_lines:
|
|
358
|
+
break
|
|
359
|
+
collected.append(ln.rstrip("\r"))
|
|
360
|
+
|
|
361
|
+
rc = proc.returncode or 0
|
|
362
|
+
if rc == 1:
|
|
363
|
+
return []
|
|
364
|
+
if rc >= 2:
|
|
365
|
+
stderr = (stderr_tail or "").strip()
|
|
366
|
+
raise GrepExecutionError(f"ripgrep failed: {stderr or f'exit code {rc}'}")
|
|
367
|
+
return collected
|
|
368
|
+
finally:
|
|
369
|
+
# Ensure no zombie
|
|
370
|
+
try:
|
|
371
|
+
if proc.poll() is None:
|
|
372
|
+
proc.kill()
|
|
373
|
+
proc.wait(timeout=1)
|
|
374
|
+
except Exception:
|
|
375
|
+
pass
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/grep/models.py — GrepToolInput / GrepToolOutput
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
面向模型与工具内部的 Pydantic 模型;阈值常量(默认 head_limit)。
|
|
6
|
+
|
|
7
|
+
对应 CC:GrepTool.ts 中 inputSchema / outputSchema 的 Python 侧表达。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from enum import Enum
|
|
13
|
+
|
|
14
|
+
from pydantic import AliasChoices, BaseModel, Field
|
|
15
|
+
|
|
16
|
+
DEFAULT_HEAD_LIMIT = 250
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GrepOutputMode(str, Enum):
|
|
20
|
+
content = "content"
|
|
21
|
+
files_with_matches = "files_with_matches"
|
|
22
|
+
count = "count"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GrepToolInput(BaseModel):
|
|
26
|
+
pattern: str = Field(
|
|
27
|
+
...,
|
|
28
|
+
description=(
|
|
29
|
+
"The regular expression pattern to search for in file contents. "
|
|
30
|
+
'Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+").'
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
path: str | None = Field(
|
|
34
|
+
default=None,
|
|
35
|
+
description=(
|
|
36
|
+
"File or directory to search in. Defaults to workspace root. "
|
|
37
|
+
"Omit this field to search the default directory."
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
glob: str | None = Field(
|
|
41
|
+
default=None,
|
|
42
|
+
description=(
|
|
43
|
+
'Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}"). Maps to ripgrep --glob.'
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
output_mode: GrepOutputMode = Field(
|
|
47
|
+
default=GrepOutputMode.files_with_matches,
|
|
48
|
+
description=(
|
|
49
|
+
'Output mode. "content" shows matching lines (context, line numbers, head_limit). '
|
|
50
|
+
'"files_with_matches" shows file paths sorted by modification time. '
|
|
51
|
+
'"count" shows match counts per file.'
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
context_before: int | None = Field(
|
|
55
|
+
default=None,
|
|
56
|
+
ge=0,
|
|
57
|
+
description='Lines before each match (rg -B). Requires output_mode "content".',
|
|
58
|
+
validation_alias=AliasChoices("context_before", "-B"),
|
|
59
|
+
)
|
|
60
|
+
context_after: int | None = Field(
|
|
61
|
+
default=None,
|
|
62
|
+
ge=0,
|
|
63
|
+
description='Lines after each match (rg -A). Requires output_mode "content".',
|
|
64
|
+
validation_alias=AliasChoices("context_after", "-A"),
|
|
65
|
+
)
|
|
66
|
+
context: int | None = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
ge=0,
|
|
69
|
+
description="Lines before and after each match (rg -C). Overrides context_before/after.",
|
|
70
|
+
validation_alias=AliasChoices("context", "-C"),
|
|
71
|
+
)
|
|
72
|
+
show_line_numbers: bool = Field(
|
|
73
|
+
default=True,
|
|
74
|
+
description='Show line numbers. Requires output_mode "content".',
|
|
75
|
+
validation_alias=AliasChoices("show_line_numbers", "-n"),
|
|
76
|
+
)
|
|
77
|
+
case_insensitive: bool = Field(
|
|
78
|
+
default=False,
|
|
79
|
+
description="Case insensitive search (rg -i).",
|
|
80
|
+
validation_alias=AliasChoices("case_insensitive", "-i"),
|
|
81
|
+
)
|
|
82
|
+
file_type: str | None = Field(
|
|
83
|
+
default=None,
|
|
84
|
+
description='File type for rg --type (e.g. "js", "py", "rust").',
|
|
85
|
+
validation_alias=AliasChoices("file_type", "type"),
|
|
86
|
+
)
|
|
87
|
+
head_limit: int = Field(
|
|
88
|
+
default=DEFAULT_HEAD_LIMIT,
|
|
89
|
+
ge=0,
|
|
90
|
+
description=(
|
|
91
|
+
"Limit to first N lines/entries. Default 250. Use 0 for unlimited (large results cost context)."
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
offset: int = Field(
|
|
95
|
+
default=0,
|
|
96
|
+
ge=0,
|
|
97
|
+
description="Skip first N entries before applying head_limit.",
|
|
98
|
+
)
|
|
99
|
+
multiline: bool = Field(
|
|
100
|
+
default=False,
|
|
101
|
+
description="Multiline mode: . matches newlines (rg -U --multiline-dotall).",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class GrepToolOutput(BaseModel):
|
|
106
|
+
"""工具 invoke() 返回值,供 present() 消费。"""
|
|
107
|
+
|
|
108
|
+
model_config = {"extra": "forbid"}
|
|
109
|
+
|
|
110
|
+
mode: GrepOutputMode
|
|
111
|
+
pattern: str
|
|
112
|
+
search_root: str
|
|
113
|
+
|
|
114
|
+
filenames: list[str] = Field(default_factory=list)
|
|
115
|
+
num_files: int = 0
|
|
116
|
+
|
|
117
|
+
content: str = ""
|
|
118
|
+
num_lines: int = 0
|
|
119
|
+
|
|
120
|
+
num_matches: int = 0
|
|
121
|
+
count_content: str = ""
|
|
122
|
+
|
|
123
|
+
truncated: bool = False
|
|
124
|
+
applied_limit: int | None = None
|
|
125
|
+
applied_offset: int | None = None
|
|
126
|
+
|
|
127
|
+
duration_ms: float = 0.0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/grep/prompt.py — 工具名与模型可见描述
|
|
3
|
+
|
|
4
|
+
对应 CC:src/tools/GrepTool/prompt.ts(getDescription)。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
TOOL_NAME = "grep"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_prompt() -> str:
|
|
13
|
+
"""面向 StructuredTool.description 的完整说明。"""
|
|
14
|
+
return (
|
|
15
|
+
"A powerful search tool built on ripgrep\n\n"
|
|
16
|
+
"Usage:\n"
|
|
17
|
+
f"- ALWAYS use {TOOL_NAME} for search tasks. NEVER invoke `grep` or `rg` as a bash command. "
|
|
18
|
+
f"The {TOOL_NAME} tool respects read permissions and workspace policy.\n"
|
|
19
|
+
'- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")\n'
|
|
20
|
+
'- Filter files with glob (e.g., "*.js", "**/*.tsx") or file_type (e.g., "js", "py", "rust")\n'
|
|
21
|
+
'- Output modes: "content" = matching lines; "files_with_matches" = paths only (default); '
|
|
22
|
+
'"count" = per-file counts\n'
|
|
23
|
+
"- For open-ended searches needing multiple rounds, use an Agent / subagent workflow if available.\n"
|
|
24
|
+
"- Pattern syntax: ripgrep (not grep) — literal braces need escaping "
|
|
25
|
+
"(use `interface\\{\\}` to find `interface{}` in Go)\n"
|
|
26
|
+
"- Multiline: default patterns are single-line; for cross-line patterns use multiline: true\n"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
DESCRIPTION = get_prompt()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/grep/rg_subprocess_controller.py — ripgrep 子进程 timeout + 协作取消
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
在无法使用单次 ``subprocess.run(..., timeout=)`` 响应协作取消时,用 Popen + 后台
|
|
6
|
+
``communicate`` 与主线程轮询 ``cancel_event``,对齐 CC ``ripGrep(..., abortSignal)``。
|
|
7
|
+
|
|
8
|
+
链路位置:
|
|
9
|
+
``grep.backend.run_ripgrep_lines`` 在传入非空 ``cancel_event`` 时委托本类;
|
|
10
|
+
``RipgrepBackend.search_stream_limited`` 在读取 stdout 时轮询同一事件。
|
|
11
|
+
|
|
12
|
+
当前裁剪范围:
|
|
13
|
+
仅 argv 列表进程;不处理 shell=True;编码固定 utf-8/errors=replace(与 TextSubprocessRunner 一致)。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import subprocess
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
from langchain_agentx.tools.grep.backend import (
|
|
23
|
+
GrepCancelledError,
|
|
24
|
+
GrepExecutionError,
|
|
25
|
+
GrepTimeoutError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RgSubprocessController:
|
|
30
|
+
"""封装可中断的阻塞式子进程等待(timeout + ``threading.Event``)。"""
|
|
31
|
+
|
|
32
|
+
_poll_interval_s = 0.05
|
|
33
|
+
|
|
34
|
+
def run_argv_wait_completed(
|
|
35
|
+
self,
|
|
36
|
+
argv: list[str],
|
|
37
|
+
*,
|
|
38
|
+
timeout: float,
|
|
39
|
+
cancel_event: threading.Event | None,
|
|
40
|
+
) -> subprocess.CompletedProcess[str]:
|
|
41
|
+
"""
|
|
42
|
+
启动 ``argv``,在 ``timeout`` 秒内等待结束;若 ``cancel_event`` 被 set 则 terminate/kill。
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
``CompletedProcess``(text stdout/stderr),供上层解析 returncode。
|
|
46
|
+
"""
|
|
47
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
48
|
+
raise GrepCancelledError("ripgrep cancelled before start")
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
proc = subprocess.Popen(
|
|
52
|
+
argv,
|
|
53
|
+
stdout=subprocess.PIPE,
|
|
54
|
+
stderr=subprocess.PIPE,
|
|
55
|
+
text=True,
|
|
56
|
+
encoding="utf-8",
|
|
57
|
+
errors="replace",
|
|
58
|
+
)
|
|
59
|
+
except FileNotFoundError as e:
|
|
60
|
+
raise GrepExecutionError(
|
|
61
|
+
"ripgrep executable not found. On Windows install ripgrep and ensure "
|
|
62
|
+
"'rg.exe' is on PATH; on Unix ensure 'rg' is on PATH."
|
|
63
|
+
) from e
|
|
64
|
+
|
|
65
|
+
out_box: list[str | None] = [None]
|
|
66
|
+
err_box: list[str | None] = [None]
|
|
67
|
+
thread_exc: list[BaseException | None] = [None]
|
|
68
|
+
|
|
69
|
+
def _communicate_worker() -> None:
|
|
70
|
+
try:
|
|
71
|
+
o, e = proc.communicate()
|
|
72
|
+
out_box[0] = o
|
|
73
|
+
err_box[0] = e
|
|
74
|
+
except BaseException as ex: # noqa: BLE001 — 必须兜住线程内异常
|
|
75
|
+
thread_exc[0] = ex
|
|
76
|
+
|
|
77
|
+
worker = threading.Thread(target=_communicate_worker, daemon=True)
|
|
78
|
+
worker.start()
|
|
79
|
+
deadline = time.monotonic() + timeout
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
while worker.is_alive():
|
|
83
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
84
|
+
proc.terminate()
|
|
85
|
+
worker.join(timeout=2.0)
|
|
86
|
+
if worker.is_alive():
|
|
87
|
+
proc.kill()
|
|
88
|
+
worker.join(timeout=2.0)
|
|
89
|
+
raise GrepCancelledError("ripgrep cancelled")
|
|
90
|
+
if time.monotonic() >= deadline:
|
|
91
|
+
proc.kill()
|
|
92
|
+
worker.join(timeout=2.0)
|
|
93
|
+
raise GrepTimeoutError(f"ripgrep search timed out after {timeout:g}s")
|
|
94
|
+
worker.join(timeout=self._poll_interval_s)
|
|
95
|
+
|
|
96
|
+
if thread_exc[0] is not None:
|
|
97
|
+
raise thread_exc[0]
|
|
98
|
+
|
|
99
|
+
return subprocess.CompletedProcess(
|
|
100
|
+
args=argv,
|
|
101
|
+
returncode=proc.returncode if proc.returncode is not None else -1,
|
|
102
|
+
stdout=out_box[0] or "",
|
|
103
|
+
stderr=err_box[0] or "",
|
|
104
|
+
)
|
|
105
|
+
finally:
|
|
106
|
+
if proc.poll() is None:
|
|
107
|
+
try:
|
|
108
|
+
proc.kill()
|
|
109
|
+
except OSError:
|
|
110
|
+
pass
|
|
111
|
+
try:
|
|
112
|
+
proc.wait(timeout=1.0)
|
|
113
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
114
|
+
pass
|