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.
Files changed (354) hide show
  1. langchain_agentx/__init__.py +46 -0
  2. langchain_agentx/command/__init__.py +28 -0
  3. langchain_agentx/command/builtin/__init__.py +25 -0
  4. langchain_agentx/command/builtin/clear.py +33 -0
  5. langchain_agentx/command/builtin/compact.py +33 -0
  6. langchain_agentx/command/builtin/memory.py +37 -0
  7. langchain_agentx/command/builtin/reload_plugins.py +42 -0
  8. langchain_agentx/command/context.py +30 -0
  9. langchain_agentx/command/dispatcher.py +183 -0
  10. langchain_agentx/command/registry.py +110 -0
  11. langchain_agentx/command/result.py +25 -0
  12. langchain_agentx/command/types.py +41 -0
  13. langchain_agentx/config/__init__.py +14 -0
  14. langchain_agentx/loop/__init__.py +47 -0
  15. langchain_agentx/loop/config/__init__.py +20 -0
  16. langchain_agentx/loop/config/agent_config.py +66 -0
  17. langchain_agentx/loop/config/agent_loop_config.py +72 -0
  18. langchain_agentx/loop/config/model_context_resolver.py +105 -0
  19. langchain_agentx/loop/config/runtime_settings.py +50 -0
  20. langchain_agentx/loop/config/token_estimator.py +133 -0
  21. langchain_agentx/loop/context/__init__.py +66 -0
  22. langchain_agentx/loop/context/blocking_guard.py +97 -0
  23. langchain_agentx/loop/context/compaction_service.py +60 -0
  24. langchain_agentx/loop/context/message_utils.py +56 -0
  25. langchain_agentx/loop/context/pipeline.py +127 -0
  26. langchain_agentx/loop/context/settings.py +103 -0
  27. langchain_agentx/loop/context/stages/__init__.py +29 -0
  28. langchain_agentx/loop/context/stages/autocompact.py +140 -0
  29. langchain_agentx/loop/context/stages/base.py +32 -0
  30. langchain_agentx/loop/context/stages/collapse.py +76 -0
  31. langchain_agentx/loop/context/stages/microcompact.py +76 -0
  32. langchain_agentx/loop/context/stages/noop.py +33 -0
  33. langchain_agentx/loop/context/stages/snip.py +71 -0
  34. langchain_agentx/loop/context/stages/tool_result_budget.py +69 -0
  35. langchain_agentx/loop/context/types.py +79 -0
  36. langchain_agentx/loop/exit/__init__.py +1 -0
  37. langchain_agentx/loop/exit/exit_logic.py +320 -0
  38. langchain_agentx/loop/exit/reason_codes.py +39 -0
  39. langchain_agentx/loop/graph/__init__.py +5 -0
  40. langchain_agentx/loop/graph/builtin_loop_control.py +197 -0
  41. langchain_agentx/loop/graph/factory.py +1409 -0
  42. langchain_agentx/loop/graph/graph_edges.py +820 -0
  43. langchain_agentx/loop/hook/__init__.py +48 -0
  44. langchain_agentx/loop/hook/async_hook_runner.py +62 -0
  45. langchain_agentx/loop/hook/config.py +280 -0
  46. langchain_agentx/loop/hook/engine.py +321 -0
  47. langchain_agentx/loop/hook/executors/__init__.py +9 -0
  48. langchain_agentx/loop/hook/executors/agent.py +107 -0
  49. langchain_agentx/loop/hook/executors/command.py +230 -0
  50. langchain_agentx/loop/hook/executors/http.py +114 -0
  51. langchain_agentx/loop/hook/executors/prompt.py +92 -0
  52. langchain_agentx/loop/hook/graph_wiring.py +134 -0
  53. langchain_agentx/loop/hook/registry.py +262 -0
  54. langchain_agentx/loop/hook/trust.py +43 -0
  55. langchain_agentx/loop/hook/types.py +110 -0
  56. langchain_agentx/loop/injection/__init__.py +13 -0
  57. langchain_agentx/loop/injection/dedup.py +74 -0
  58. langchain_agentx/loop/loop_abort.py +36 -0
  59. langchain_agentx/loop/model/__init__.py +1 -0
  60. langchain_agentx/loop/model/model_node.py +648 -0
  61. langchain_agentx/loop/model/model_nodes.py +661 -0
  62. langchain_agentx/loop/model/orphan_tool_results.py +38 -0
  63. langchain_agentx/loop/model/retrier.py +307 -0
  64. langchain_agentx/loop/model/retry_bridge.py +58 -0
  65. langchain_agentx/loop/model/retry_events.py +35 -0
  66. langchain_agentx/loop/model/retry_policy.py +56 -0
  67. langchain_agentx/loop/model/schema_and_format.py +153 -0
  68. langchain_agentx/loop/model/tool_and_model_binding.py +227 -0
  69. langchain_agentx/loop/model/tool_call_degradation_corrector.py +443 -0
  70. langchain_agentx/loop/model/tool_transcript_guard.py +225 -0
  71. langchain_agentx/loop/prompt/__init__.py +95 -0
  72. langchain_agentx/loop/prompt/builder.py +61 -0
  73. langchain_agentx/loop/prompt/builtin.py +218 -0
  74. langchain_agentx/loop/prompt/compact.py +408 -0
  75. langchain_agentx/loop/prompt/sections.py +120 -0
  76. langchain_agentx/loop/runtime/__init__.py +19 -0
  77. langchain_agentx/loop/runtime/context.py +34 -0
  78. langchain_agentx/loop/runtime/context_factory.py +107 -0
  79. langchain_agentx/loop/runtime/subagent_execution_paths.py +68 -0
  80. langchain_agentx/loop/subagent/__init__.py +53 -0
  81. langchain_agentx/loop/subagent/async_runner.py +215 -0
  82. langchain_agentx/loop/subagent/context.py +209 -0
  83. langchain_agentx/loop/subagent/fork_worktree_notice.py +25 -0
  84. langchain_agentx/loop/subagent/graph.py +72 -0
  85. langchain_agentx/loop/subagent/orchestrator.py +391 -0
  86. langchain_agentx/loop/subagent/progress.py +30 -0
  87. langchain_agentx/loop/subagent/prompt.py +52 -0
  88. langchain_agentx/loop/subagent/runner.py +504 -0
  89. langchain_agentx/loop/subagent/transcript.py +172 -0
  90. langchain_agentx/memory/__init__.py +2 -0
  91. langchain_agentx/memory/instruction/__init__.py +12 -0
  92. langchain_agentx/memory/instruction/loader.py +325 -0
  93. langchain_agentx/memory/instruction/resolver.py +24 -0
  94. langchain_agentx/memory/instruction/runtime.py +83 -0
  95. langchain_agentx/memory/instruction/sections.py +83 -0
  96. langchain_agentx/memory/instruction/types.py +59 -0
  97. langchain_agentx/memory/memdir/__init__.py +77 -0
  98. langchain_agentx/memory/memdir/age.py +36 -0
  99. langchain_agentx/memory/memdir/agent_memory.py +380 -0
  100. langchain_agentx/memory/memdir/extractor.py +309 -0
  101. langchain_agentx/memory/memdir/loader.py +187 -0
  102. langchain_agentx/memory/memdir/paths.py +63 -0
  103. langchain_agentx/memory/memdir/recall.py +45 -0
  104. langchain_agentx/memory/memdir/runtime.py +43 -0
  105. langchain_agentx/memory/memdir/scan.py +135 -0
  106. langchain_agentx/memory/memdir/types.py +104 -0
  107. langchain_agentx/memory/session/__init__.py +76 -0
  108. langchain_agentx/memory/session/compact_bridge.py +208 -0
  109. langchain_agentx/memory/session/prompts.py +172 -0
  110. langchain_agentx/memory/session/session_memory.py +282 -0
  111. langchain_agentx/observability/__init__.py +67 -0
  112. langchain_agentx/observability/evaluation/__init__.py +17 -0
  113. langchain_agentx/observability/evaluation/checkers/__init__.py +18 -0
  114. langchain_agentx/observability/evaluation/checkers/base.py +34 -0
  115. langchain_agentx/observability/evaluation/checkers/compaction.py +38 -0
  116. langchain_agentx/observability/evaluation/checkers/degradation.py +50 -0
  117. langchain_agentx/observability/evaluation/checkers/exit_quality.py +42 -0
  118. langchain_agentx/observability/evaluation/checkers/session_memory.py +45 -0
  119. langchain_agentx/observability/evaluation/checkers/tool_behavior.py +53 -0
  120. langchain_agentx/observability/evaluation/retention_scheduler.py +67 -0
  121. langchain_agentx/observability/evaluation/service.py +102 -0
  122. langchain_agentx/observability/evaluation/state.py +32 -0
  123. langchain_agentx/observability/evaluation/store.py +258 -0
  124. langchain_agentx/observability/events/__init__.py +15 -0
  125. langchain_agentx/observability/events/langchain_agentx_event_adapter.py +832 -0
  126. langchain_agentx/observability/logging/__init__.py +15 -0
  127. langchain_agentx/observability/logging/debug_burst.py +95 -0
  128. langchain_agentx/observability/logging/logging_config.py +178 -0
  129. langchain_agentx/observability/logging/logging_contract.py +65 -0
  130. langchain_agentx/observability/replay/__init__.py +35 -0
  131. langchain_agentx/observability/replay/cli.py +91 -0
  132. langchain_agentx/observability/replay/service.py +83 -0
  133. langchain_agentx/observability/replay/store.py +278 -0
  134. langchain_agentx/observability/replay/ui.py +47 -0
  135. langchain_agentx/observability/trace/__init__.py +25 -0
  136. langchain_agentx/observability/trace/collector.py +560 -0
  137. langchain_agentx/observability/trace/event_emitter.py +183 -0
  138. langchain_agentx/observability/trace/hook_event_emitter.py +49 -0
  139. langchain_agentx/observability/trace/models.py +144 -0
  140. langchain_agentx/observability/trace/sqlite_store.py +873 -0
  141. langchain_agentx/observability/trace/trace_callback.py +295 -0
  142. langchain_agentx/observability/trace/trace_lifecycle_collector.py +114 -0
  143. langchain_agentx/plugin/__init__.py +26 -0
  144. langchain_agentx/plugin/builtin.py +53 -0
  145. langchain_agentx/plugin/config.py +113 -0
  146. langchain_agentx/plugin/loader.py +386 -0
  147. langchain_agentx/plugin/manifest.py +154 -0
  148. langchain_agentx/plugin/registries.py +211 -0
  149. langchain_agentx/plugin/types.py +142 -0
  150. langchain_agentx/provider/__init__.py +27 -0
  151. langchain_agentx/provider/anthropic.py +121 -0
  152. langchain_agentx/provider/compatible_chat_openai.py +86 -0
  153. langchain_agentx/provider/env.py +45 -0
  154. langchain_agentx/provider/model_profile.py +156 -0
  155. langchain_agentx/provider/openai.py +89 -0
  156. langchain_agentx/session/__init__.py +17 -0
  157. langchain_agentx/session/agent_session.py +320 -0
  158. langchain_agentx/session/conversation_factory.py +87 -0
  159. langchain_agentx/session/conversation_recovery.py +156 -0
  160. langchain_agentx/session/conversation_session.py +198 -0
  161. langchain_agentx/session/factory.py +143 -0
  162. langchain_agentx/session/protocol.py +25 -0
  163. langchain_agentx/task_runtime/__init__.py +113 -0
  164. langchain_agentx/task_runtime/core/__init__.py +51 -0
  165. langchain_agentx/task_runtime/core/ids.py +33 -0
  166. langchain_agentx/task_runtime/core/interfaces.py +115 -0
  167. langchain_agentx/task_runtime/core/notification_priority.py +19 -0
  168. langchain_agentx/task_runtime/core/types.py +136 -0
  169. langchain_agentx/task_runtime/integrations/__init__.py +33 -0
  170. langchain_agentx/task_runtime/integrations/loop_adapter.py +91 -0
  171. langchain_agentx/task_runtime/integrations/loop_integration.py +61 -0
  172. langchain_agentx/task_runtime/integrations/prefetch_providers.py +108 -0
  173. langchain_agentx/task_runtime/integrations/provider_factory.py +103 -0
  174. langchain_agentx/task_runtime/integrations/queued_command_provider.py +184 -0
  175. langchain_agentx/task_runtime/integrations/sqlite_queued_command_provider.py +338 -0
  176. langchain_agentx/task_runtime/integrations/tool_use_summary_provider.py +254 -0
  177. langchain_agentx/task_runtime/orchestrator/__init__.py +5 -0
  178. langchain_agentx/task_runtime/orchestrator/runtime.py +386 -0
  179. langchain_agentx/task_runtime/output/__init__.py +5 -0
  180. langchain_agentx/task_runtime/output/sink.py +64 -0
  181. langchain_agentx/task_runtime/policy/__init__.py +11 -0
  182. langchain_agentx/task_runtime/policy/withhold_visibility.py +32 -0
  183. langchain_agentx/task_runtime/queue/__init__.py +5 -0
  184. langchain_agentx/task_runtime/queue/in_memory.py +55 -0
  185. langchain_agentx/task_runtime/skill_prefetch/__init__.py +4 -0
  186. langchain_agentx/task_runtime/skill_prefetch/attachments.py +46 -0
  187. langchain_agentx/task_runtime/skill_prefetch/models.py +37 -0
  188. langchain_agentx/task_runtime/skill_prefetch/provider.py +344 -0
  189. langchain_agentx/task_runtime/store/__init__.py +6 -0
  190. langchain_agentx/task_runtime/store/in_memory.py +81 -0
  191. langchain_agentx/task_runtime/store/sqlite_store.py +281 -0
  192. langchain_agentx/task_runtime/tasks/__init__.py +76 -0
  193. langchain_agentx/task_runtime/tasks/ai_analysis/__init__.py +15 -0
  194. langchain_agentx/task_runtime/tasks/ai_analysis/base.py +41 -0
  195. langchain_agentx/task_runtime/tasks/ai_analysis/evaluation.py +67 -0
  196. langchain_agentx/task_runtime/tasks/ai_analysis/registry.py +36 -0
  197. langchain_agentx/task_runtime/tasks/ai_analysis/scheduler.py +70 -0
  198. langchain_agentx/task_runtime/tasks/base/__init__.py +6 -0
  199. langchain_agentx/task_runtime/tasks/base/contracts.py +24 -0
  200. langchain_agentx/task_runtime/tasks/custom/__init__.py +7 -0
  201. langchain_agentx/task_runtime/tasks/custom/executor.py +60 -0
  202. langchain_agentx/task_runtime/tasks/custom/notification.py +7 -0
  203. langchain_agentx/task_runtime/tasks/custom/semantics.py +13 -0
  204. langchain_agentx/task_runtime/tasks/custom/spec.py +33 -0
  205. langchain_agentx/task_runtime/tasks/dream_task/__init__.py +15 -0
  206. langchain_agentx/task_runtime/tasks/dream_task/executor.py +61 -0
  207. langchain_agentx/task_runtime/tasks/dream_task/notification.py +19 -0
  208. langchain_agentx/task_runtime/tasks/dream_task/semantics.py +13 -0
  209. langchain_agentx/task_runtime/tasks/dream_task/spec.py +35 -0
  210. langchain_agentx/task_runtime/tasks/dream_task/state.py +17 -0
  211. langchain_agentx/task_runtime/tasks/in_process_teammate/__init__.py +12 -0
  212. langchain_agentx/task_runtime/tasks/in_process_teammate/executor.py +36 -0
  213. langchain_agentx/task_runtime/tasks/in_process_teammate/notification.py +25 -0
  214. langchain_agentx/task_runtime/tasks/in_process_teammate/semantics.py +13 -0
  215. langchain_agentx/task_runtime/tasks/in_process_teammate/spec.py +63 -0
  216. langchain_agentx/task_runtime/tasks/local_agent/__init__.py +14 -0
  217. langchain_agentx/task_runtime/tasks/local_agent/executor.py +33 -0
  218. langchain_agentx/task_runtime/tasks/local_agent/notification.py +21 -0
  219. langchain_agentx/task_runtime/tasks/local_agent/runner.py +43 -0
  220. langchain_agentx/task_runtime/tasks/local_agent/semantics.py +13 -0
  221. langchain_agentx/task_runtime/tasks/local_agent/spec.py +31 -0
  222. langchain_agentx/task_runtime/tasks/local_bash/__init__.py +13 -0
  223. langchain_agentx/task_runtime/tasks/local_bash/executor.py +95 -0
  224. langchain_agentx/task_runtime/tasks/local_bash/notification.py +22 -0
  225. langchain_agentx/task_runtime/tasks/local_bash/semantics.py +13 -0
  226. langchain_agentx/task_runtime/tasks/local_bash/spec.py +55 -0
  227. langchain_agentx/task_runtime/tasks/remote_agent/__init__.py +19 -0
  228. langchain_agentx/task_runtime/tasks/remote_agent/backend.py +76 -0
  229. langchain_agentx/task_runtime/tasks/remote_agent/executor.py +37 -0
  230. langchain_agentx/task_runtime/tasks/remote_agent/notification.py +22 -0
  231. langchain_agentx/task_runtime/tasks/remote_agent/semantics.py +13 -0
  232. langchain_agentx/task_runtime/tasks/remote_agent/spec.py +34 -0
  233. langchain_agentx/task_runtime/tasks/trace_cleanup/__init__.py +19 -0
  234. langchain_agentx/task_runtime/tasks/trace_cleanup/bootstrap.py +95 -0
  235. langchain_agentx/task_runtime/tasks/trace_cleanup/executor.py +66 -0
  236. langchain_agentx/task_runtime/tasks/trace_cleanup/scheduler.py +169 -0
  237. langchain_agentx/tool_runtime/__init__.py +90 -0
  238. langchain_agentx/tool_runtime/adapter.py +365 -0
  239. langchain_agentx/tool_runtime/base.py +319 -0
  240. langchain_agentx/tool_runtime/errors.py +190 -0
  241. langchain_agentx/tool_runtime/identical_call_cache.py +110 -0
  242. langchain_agentx/tool_runtime/loader.py +195 -0
  243. langchain_agentx/tool_runtime/models.py +260 -0
  244. langchain_agentx/tool_runtime/permission_context.py +78 -0
  245. langchain_agentx/tool_runtime/pipeline.py +621 -0
  246. langchain_agentx/tool_runtime/policy.py +447 -0
  247. langchain_agentx/tool_runtime/registry.py +81 -0
  248. langchain_agentx/tool_runtime/resolvers/__init__.py +27 -0
  249. langchain_agentx/tool_runtime/resolvers/agent_session.py +125 -0
  250. langchain_agentx/tool_runtime/resolvers/background.py +32 -0
  251. langchain_agentx/tool_runtime/resolvers/base.py +20 -0
  252. langchain_agentx/tool_runtime/resolvers/conversation.py +22 -0
  253. langchain_agentx/tool_runtime/resolvers/workflow.py +73 -0
  254. langchain_agentx/tool_runtime/session_store.py +132 -0
  255. langchain_agentx/tool_runtime/smoke_test_runtime.py +294 -0
  256. langchain_agentx/tool_runtime/state_bridge.py +164 -0
  257. langchain_agentx/tools/__init__.py +26 -0
  258. langchain_agentx/tools/agent/__init__.py +9 -0
  259. langchain_agentx/tools/agent/backend.py +53 -0
  260. langchain_agentx/tools/agent/built_in/__init__.py +19 -0
  261. langchain_agentx/tools/agent/built_in/agentx_guide.py +65 -0
  262. langchain_agentx/tools/agent/built_in/explore.py +80 -0
  263. langchain_agentx/tools/agent/built_in/general.py +57 -0
  264. langchain_agentx/tools/agent/built_in/plan.py +89 -0
  265. langchain_agentx/tools/agent/built_in/statusline_setup.py +64 -0
  266. langchain_agentx/tools/agent/built_in/verification.py +120 -0
  267. langchain_agentx/tools/agent/builtin_subagent_loader.py +89 -0
  268. langchain_agentx/tools/agent/cwd_resolution.py +119 -0
  269. langchain_agentx/tools/agent/limits.py +26 -0
  270. langchain_agentx/tools/agent/loader.py +270 -0
  271. langchain_agentx/tools/agent/models.py +85 -0
  272. langchain_agentx/tools/agent/prompt.py +120 -0
  273. langchain_agentx/tools/agent/registry/__init__.py +18 -0
  274. langchain_agentx/tools/agent/registry/config.py +29 -0
  275. langchain_agentx/tools/agent/registry/registry.py +47 -0
  276. langchain_agentx/tools/agent/scope.py +137 -0
  277. langchain_agentx/tools/agent/tool.py +256 -0
  278. langchain_agentx/tools/bash/__init__.py +9 -0
  279. langchain_agentx/tools/bash/ast_security.py +571 -0
  280. langchain_agentx/tools/bash/backend.py +1447 -0
  281. langchain_agentx/tools/bash/bash_hardening.py +734 -0
  282. langchain_agentx/tools/bash/bash_runtime_contract.py +41 -0
  283. langchain_agentx/tools/bash/cwd_reporter.py +95 -0
  284. langchain_agentx/tools/bash/limits.py +71 -0
  285. langchain_agentx/tools/bash/mode_validation.py +282 -0
  286. langchain_agentx/tools/bash/models.py +131 -0
  287. langchain_agentx/tools/bash/observability.py +148 -0
  288. langchain_agentx/tools/bash/output_utils.py +200 -0
  289. langchain_agentx/tools/bash/path_security.py +2429 -0
  290. langchain_agentx/tools/bash/prompt.py +68 -0
  291. langchain_agentx/tools/bash/read_only_validation.py +589 -0
  292. langchain_agentx/tools/bash/result_presenter.py +324 -0
  293. langchain_agentx/tools/bash/sandbox_decision.py +133 -0
  294. langchain_agentx/tools/bash/security.py +311 -0
  295. langchain_agentx/tools/bash/sed_edit_parser.py +243 -0
  296. langchain_agentx/tools/bash/sed_validation.py +163 -0
  297. langchain_agentx/tools/bash/semantics.py +111 -0
  298. langchain_agentx/tools/bash/session_manager.py +205 -0
  299. langchain_agentx/tools/bash/session_runtime.py +290 -0
  300. langchain_agentx/tools/bash/shell_locator.py +191 -0
  301. langchain_agentx/tools/bash/task_runtime.py +91 -0
  302. langchain_agentx/tools/bash/tool.py +939 -0
  303. langchain_agentx/tools/bash/windows_shell_quoting.py +45 -0
  304. langchain_agentx/tools/glob/__init__.py +9 -0
  305. langchain_agentx/tools/glob/models.py +57 -0
  306. langchain_agentx/tools/glob/pagination.py +30 -0
  307. langchain_agentx/tools/glob/prompt.py +24 -0
  308. langchain_agentx/tools/glob/rg_list_backend.py +139 -0
  309. langchain_agentx/tools/glob/rg_pattern.py +44 -0
  310. langchain_agentx/tools/glob/tool.py +327 -0
  311. langchain_agentx/tools/grep/__init__.py +7 -0
  312. langchain_agentx/tools/grep/backend.py +375 -0
  313. langchain_agentx/tools/grep/models.py +127 -0
  314. langchain_agentx/tools/grep/prompt.py +30 -0
  315. langchain_agentx/tools/grep/rg_subprocess_controller.py +114 -0
  316. langchain_agentx/tools/grep/tool.py +475 -0
  317. langchain_agentx/tools/read/__init__.py +9 -0
  318. langchain_agentx/tools/read/backend.py +415 -0
  319. langchain_agentx/tools/read/limits.py +67 -0
  320. langchain_agentx/tools/read/models.py +156 -0
  321. langchain_agentx/tools/read/prompt.py +73 -0
  322. langchain_agentx/tools/read/tool.py +494 -0
  323. langchain_agentx/tools/ripgrep_plugin_exclusions.py +137 -0
  324. langchain_agentx/tools/skill/__init__.py +4 -0
  325. langchain_agentx/tools/skill/argument_substitution.py +80 -0
  326. langchain_agentx/tools/skill/loader.py +196 -0
  327. langchain_agentx/tools/skill/models.py +88 -0
  328. langchain_agentx/tools/skill/policy.py +80 -0
  329. langchain_agentx/tools/skill/prompt.py +35 -0
  330. langchain_agentx/tools/skill/tool.py +222 -0
  331. langchain_agentx/utils/__init__.py +0 -0
  332. langchain_agentx/utils/cwd.py +124 -0
  333. langchain_agentx/utils/host_platform.py +112 -0
  334. langchain_agentx/utils/path_hierarchy.py +48 -0
  335. langchain_agentx/utils/path_user_input.py +66 -0
  336. langchain_agentx/utils/rg_executable.py +18 -0
  337. langchain_agentx/utils/subprocess_text.py +101 -0
  338. langchain_agentx/utils/temp_paths.py +77 -0
  339. langchain_agentx/utils/unc_path.py +25 -0
  340. langchain_agentx/utils/win_reserved_paths.py +51 -0
  341. langchain_agentx/workflow/__init__.py +7 -0
  342. langchain_agentx/workflow/base.py +97 -0
  343. langchain_agentx/workflow/batch.py +55 -0
  344. langchain_agentx/workflow/dag.py +54 -0
  345. langchain_agentx/workspace/__init__.py +13 -0
  346. langchain_agentx/workspace/config.py +140 -0
  347. langchain_agentx/workspace/path_key_normalizer.py +30 -0
  348. langchain_agentx/workspace/resolver.py +74 -0
  349. langchain_agentx/workspace/validators.py +41 -0
  350. langchain_agentx_python-0.1.dist-info/LICENSE +201 -0
  351. langchain_agentx_python-0.1.dist-info/METADATA +513 -0
  352. langchain_agentx_python-0.1.dist-info/RECORD +354 -0
  353. langchain_agentx_python-0.1.dist-info/WHEEL +5 -0
  354. 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