langchain-agentx-python 2.2.3__py3-none-any.whl → 2.2.4__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.
@@ -11,7 +11,7 @@ from langchain_agentx import create_loop_agent
11
11
  ```
12
12
  """
13
13
 
14
- __version__ = "2.2.3"
14
+ __version__ = "2.2.4"
15
15
 
16
16
  from .loop import ( # noqa: F401
17
17
  create_loop_agent,
@@ -6,7 +6,7 @@ tools/grep/backend.py — RipgrepBackend(ripgrep 子进程封装)
6
6
 
7
7
  在整体链路中的位置:
8
8
  GrepRuntimeTool.invoke() -> RipgrepBackend.search() -> run_ripgrep_lines(可选 ctx.cancel_event)
9
- -> RgSubprocessController TextSubprocessRunner
9
+ -> RgSubprocessController(有界 stdout)或 TextSubprocessRunner(测试注入)
10
10
 
11
11
  对应 CC:src/utils/ripgrep.ts + GrepTool.ts call() 中参数拼装。
12
12
  """
@@ -16,7 +16,6 @@ from __future__ import annotations
16
16
  import os
17
17
  import subprocess
18
18
  import threading
19
- from typing import Any
20
19
 
21
20
  from langchain_agentx.tool_runtime.errors import ToolRuntimeError
22
21
  from langchain_agentx.utils.rg_executable import default_rg_executable
@@ -26,6 +25,7 @@ from langchain_agentx.utils.subprocess_text import (
26
25
  )
27
26
 
28
27
  from .models import GrepOutputMode
28
+ from .ripgrep_stdout_cap import RipgrepStdoutCap
29
29
 
30
30
 
31
31
  def resolve_ripgrep_subprocess_cwd(search_path: str) -> str:
@@ -58,6 +58,11 @@ class GrepCancelledError(ToolRuntimeError):
58
58
  super().__init__(message, code="GREP_CANCELLED")
59
59
 
60
60
 
61
+ def _uses_injected_subprocess_run(text_runner: TextSubprocessRunner) -> bool:
62
+ """测试注入自定义 ``subprocess_run`` 时走 text_runner;生产默认走有界 controller。"""
63
+ return getattr(text_runner, "_subprocess_run", None) is not subprocess.run
64
+
65
+
61
66
  def run_ripgrep_lines(
62
67
  rg_path: str,
63
68
  args: list[str],
@@ -70,21 +75,25 @@ def run_ripgrep_lines(
70
75
  ) -> list[str]:
71
76
  """
72
77
  执行 [rg_path, *args, search_path],解析 stdout 为行列表。
73
- 对齐 CC ripgrep.ts:returncode 1 -> [];EAGAIN 时以 -j 1 重试一次。
78
+ 对齐 CC ripgrep.ts:returncode 1 -> [];EAGAIN 时以 -j 1 重试一次;
79
+ stdout 硬顶 ``MAX_STDOUT_BUFFER_SIZE``(20MB)。
74
80
  供 RipgrepBackend 与 Glob 的 rg --files 列文件共用。
75
81
 
76
82
  SDK 补充:``subprocess cwd`` 对齐搜索根目录(目录本身,或文件的父目录)。
77
83
  CC 隐式依赖进程 cwd≈searchDir;编排布局 ``state_root≠repo`` 时须显式设 cwd,
78
84
  否则 Windows 上目录前缀 glob(如 ``alpha/*``)误空。
79
85
 
80
- ``cancel_event`` 非空,使用可中断子进程路径(``RgSubprocessController``),
81
- 不再走 ``text_runner.run``(便于测试 mock 时仅覆盖无取消路径)。
86
+ 生产路径使用 ``RgSubprocessController`` 边收边截;仅当测试注入自定义
87
+ ``text_runner.subprocess_run`` 时走 ``text_runner.run``(事后硬顶)。
82
88
  """
83
89
  search_target = os.path.realpath(search_path)
84
90
  search_cwd = resolve_ripgrep_subprocess_cwd(search_path)
85
91
  cmd = [rg_path, *args, search_target]
86
- if cancel_event is not None:
87
- if cancel_event.is_set():
92
+ stdout_truncated = False
93
+
94
+ use_text_runner = cancel_event is None and _uses_injected_subprocess_run(text_runner)
95
+ if not use_text_runner:
96
+ if cancel_event is not None and cancel_event.is_set():
88
97
  raise GrepCancelledError("ripgrep cancelled before start")
89
98
  from langchain_agentx.tools.grep.rg_subprocess_controller import RgSubprocessController
90
99
 
@@ -100,6 +109,7 @@ def run_ripgrep_lines(
100
109
  raise
101
110
  except GrepCancelledError:
102
111
  raise
112
+ stdout_truncated = bool(getattr(result, "stdout_truncated", False))
103
113
  else:
104
114
  try:
105
115
  result = text_runner.run(
@@ -134,10 +144,7 @@ def run_ripgrep_lines(
134
144
  raise GrepExecutionError(f"ripgrep failed: {stderr or f'exit code {result.returncode}'}")
135
145
 
136
146
  out = result.stdout or ""
137
- if not out.strip():
138
- return []
139
- lines = [ln.rstrip("\r") for ln in out.rstrip("\n").split("\n")]
140
- return [ln for ln in lines if ln]
147
+ return RipgrepStdoutCap.lines_from_text(out, truncated=stdout_truncated)
141
148
 
142
149
 
143
150
  class RipgrepBackend:
@@ -3,10 +3,11 @@ tools/grep/rg_subprocess_controller.py — ripgrep 子进程 timeout + 协作取
3
3
 
4
4
  职责:
5
5
  在无法使用单次 ``subprocess.run(..., timeout=)`` 响应协作取消时,用 Popen + 后台
6
- ``communicate`` 与主线程轮询 ``cancel_event``,对齐 CC ``ripGrep(..., abortSignal)``。
6
+ 有界 stdout/stderr 读取与主线程轮询 ``cancel_event``,对齐 CC ``ripGrep(..., abortSignal)``
7
+ 与 ``MAX_BUFFER_SIZE`` 边收边截。
7
8
 
8
9
  链路位置:
9
- ``grep.backend.run_ripgrep_lines`` 在传入非空 ``cancel_event`` 时委托本类;
10
+ ``grep.backend.run_ripgrep_lines`` 在生产路径(或传入 ``cancel_event``)时委托本类;
10
11
  ``RipgrepBackend.search_stream_limited`` 在读取 stdout 时轮询同一事件。
11
12
 
12
13
  当前裁剪范围:
@@ -24,13 +25,17 @@ from langchain_agentx.tools.grep.backend import (
24
25
  GrepExecutionError,
25
26
  GrepTimeoutError,
26
27
  )
28
+ from langchain_agentx.tools.grep.ripgrep_stdout_cap import RipgrepStdoutCap
27
29
 
28
30
 
29
31
  class RgSubprocessController:
30
- """封装可中断的阻塞式子进程等待(timeout + ``threading.Event``)。"""
32
+ """封装可中断的阻塞式子进程等待(timeout + ``threading.Event`` + stdout 硬顶)。"""
31
33
 
32
34
  _poll_interval_s = 0.05
33
35
 
36
+ def __init__(self, *, stdout_cap_factory: type[RipgrepStdoutCap] = RipgrepStdoutCap) -> None:
37
+ self._stdout_cap_factory = stdout_cap_factory
38
+
34
39
  def run_argv_wait_completed(
35
40
  self,
36
41
  argv: list[str],
@@ -42,8 +47,11 @@ class RgSubprocessController:
42
47
  """
43
48
  启动 ``argv``,在 ``timeout`` 秒内等待结束;若 ``cancel_event`` 被 set 则 terminate/kill。
44
49
 
50
+ stdout/stderr 边收边截(``RipgrepStdoutCap``),避免无界 ``communicate`` 打爆内存。
51
+
45
52
  Returns:
46
53
  ``CompletedProcess``(text stdout/stderr),供上层解析 returncode。
54
+ 额外属性 ``stdout_truncated`` / ``stderr_truncated``(bool)。
47
55
  """
48
56
  if cancel_event is not None and cancel_event.is_set():
49
57
  raise GrepCancelledError("ripgrep cancelled before start")
@@ -64,19 +72,33 @@ class RgSubprocessController:
64
72
  "'rg.exe' is on PATH; on Unix ensure 'rg' is on PATH."
65
73
  ) from e
66
74
 
67
- out_box: list[str | None] = [None]
68
- err_box: list[str | None] = [None]
75
+ stdout_cap = self._stdout_cap_factory()
76
+ stderr_cap = self._stdout_cap_factory()
69
77
  thread_exc: list[BaseException | None] = [None]
70
78
 
71
- def _communicate_worker() -> None:
79
+ def _readers_worker() -> None:
72
80
  try:
73
- o, e = proc.communicate()
74
- out_box[0] = o
75
- err_box[0] = e
81
+ assert proc.stdout is not None
82
+ assert proc.stderr is not None
83
+ out_t = threading.Thread(
84
+ target=stdout_cap.drain_text_stream,
85
+ args=(proc.stdout,),
86
+ daemon=True,
87
+ )
88
+ err_t = threading.Thread(
89
+ target=stderr_cap.drain_text_stream,
90
+ args=(proc.stderr,),
91
+ daemon=True,
92
+ )
93
+ out_t.start()
94
+ err_t.start()
95
+ out_t.join()
96
+ err_t.join()
97
+ proc.wait()
76
98
  except BaseException as ex: # noqa: BLE001 — 必须兜住线程内异常
77
99
  thread_exc[0] = ex
78
100
 
79
- worker = threading.Thread(target=_communicate_worker, daemon=True)
101
+ worker = threading.Thread(target=_readers_worker, daemon=True)
80
102
  worker.start()
81
103
  deadline = time.monotonic() + timeout
82
104
 
@@ -98,12 +120,16 @@ class RgSubprocessController:
98
120
  if thread_exc[0] is not None:
99
121
  raise thread_exc[0]
100
122
 
101
- return subprocess.CompletedProcess(
123
+ result = subprocess.CompletedProcess(
102
124
  args=argv,
103
125
  returncode=proc.returncode if proc.returncode is not None else -1,
104
- stdout=out_box[0] or "",
105
- stderr=err_box[0] or "",
126
+ stdout=stdout_cap.text(),
127
+ stderr=stderr_cap.text(),
106
128
  )
129
+ # 供 run_ripgrep_lines 决定是否丢弃残缺末行
130
+ setattr(result, "stdout_truncated", stdout_cap.truncated)
131
+ setattr(result, "stderr_truncated", stderr_cap.truncated)
132
+ return result
107
133
  finally:
108
134
  if proc.poll() is None:
109
135
  try:
@@ -0,0 +1,93 @@
1
+ """
2
+ tools/grep/ripgrep_stdout_cap.py — ripgrep stdout/stderr 字节硬顶
3
+
4
+ 职责:
5
+ 对齐 CC ``src/utils/ripgrep.ts`` 的 ``MAX_BUFFER_SIZE = 20_000_000``:
6
+ 边收边截,超限停止 append;解析行时若已截断则丢弃可能残缺的末行。
7
+
8
+ 链路位置:
9
+ ``run_ripgrep_lines`` / ``RgSubprocessController`` 共用;Grep 与 Glob 列文件同路径受益。
10
+
11
+ 当前裁剪:
12
+ 按 Unicode 字符长度计量(对齐 CC JS string ``.length``);不在此杀进程。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # 对齐 CC ripgrep.ts:large monorepos can have 200k+ files
22
+ MAX_STDOUT_BUFFER_SIZE = 20_000_000
23
+
24
+
25
+ class RipgrepStdoutCap:
26
+ """可复用的有界 stdout/stderr 收集器(feed 后 to_lines / text)。"""
27
+
28
+ def __init__(self, max_size: int = MAX_STDOUT_BUFFER_SIZE) -> None:
29
+ if max_size <= 0:
30
+ raise ValueError("max_size must be positive")
31
+ self._max_size = max_size
32
+ self._parts: list[str] = []
33
+ self._size = 0
34
+ self.truncated = False
35
+
36
+ @property
37
+ def max_size(self) -> int:
38
+ return self._max_size
39
+
40
+ @property
41
+ def size(self) -> int:
42
+ return self._size
43
+
44
+ def feed(self, chunk: str) -> None:
45
+ """追加 chunk;已截断时忽略内容(调用方仍应继续读以排空管道)。"""
46
+ if self.truncated or not chunk:
47
+ return
48
+ remaining = self._max_size - self._size
49
+ if len(chunk) <= remaining:
50
+ self._parts.append(chunk)
51
+ self._size += len(chunk)
52
+ return
53
+ if remaining > 0:
54
+ self._parts.append(chunk[:remaining])
55
+ self._size = self._max_size
56
+ self.truncated = True
57
+
58
+ def text(self) -> str:
59
+ return "".join(self._parts)
60
+
61
+ def to_lines(self) -> list[str]:
62
+ """拆成非空行;截断时丢弃末行(对齐 CC buffer overflow / timeout 处理)。"""
63
+ return self.lines_from_text(self.text(), truncated=self.truncated)
64
+
65
+ @staticmethod
66
+ def lines_from_text(out: str, *, truncated: bool = False) -> list[str]:
67
+ """对已物化字符串做硬顶 + 分行(text_runner / mock 路径)。"""
68
+ if not out:
69
+ return []
70
+ was_truncated = truncated
71
+ if len(out) > MAX_STDOUT_BUFFER_SIZE:
72
+ out = out[:MAX_STDOUT_BUFFER_SIZE]
73
+ was_truncated = True
74
+ if not out.strip():
75
+ return []
76
+ lines = [ln.rstrip("\r") for ln in out.rstrip("\n").split("\n")]
77
+ lines = [ln for ln in lines if ln]
78
+ if was_truncated and lines:
79
+ lines = lines[:-1]
80
+ if was_truncated:
81
+ logger.warning(
82
+ "ripgrep stdout truncated at %s chars (CC MAX_BUFFER_SIZE alignment)",
83
+ MAX_STDOUT_BUFFER_SIZE,
84
+ )
85
+ return lines
86
+
87
+ def drain_text_stream(self, stream) -> None:
88
+ """从文本流按块读取并 feed,超限后继续排空但不入库。"""
89
+ while True:
90
+ chunk = stream.read(64 * 1024)
91
+ if not chunk:
92
+ break
93
+ self.feed(chunk)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langchain-agentx-python
3
- Version: 2.2.3
3
+ Version: 2.2.4
4
4
  Summary: LangChain/LangGraph-based agent utilities for CodeBaseX.
5
5
  Author-email: GoodMood2008 <GoodMood2008@users.noreply.github.com>
6
6
  License: Apache License
@@ -1,4 +1,4 @@
1
- langchain_agentx/__init__.py,sha256=bMHXxvdlfRUJK-ZT43f2Hgef2tNuOs5bc3mv-F4BQRc,1614
1
+ langchain_agentx/__init__.py,sha256=rYu7Sa7AIXQVmlKCoRc4uqxp7Lb37UIESLvYcrsemdA,1614
2
2
  langchain_agentx/command/__init__.py,sha256=Ej260S5uFQcmEtGZQG_NH7BYjHoStrpBLtGUsBTxyZk,631
3
3
  langchain_agentx/command/allowed_tools.py,sha256=TVA0VM8rm98H-QbLK_GRiX5RhEAZFxKawliMi4kk96A,3359
4
4
  langchain_agentx/command/context.py,sha256=DIGOGPBw5UGDBm0WNNJlKx1h_9rCRmcdROv4DT61Qug,731
@@ -581,10 +581,11 @@ langchain_agentx/tools/glob/rg_list_backend.py,sha256=gTyFQE0eYHl-7LkSdah76S8bQn
581
581
  langchain_agentx/tools/glob/rg_pattern.py,sha256=YOV-1gdxC52vzdAwgz27l7mzPJvVuHqpqeWhiEnWQJs,1377
582
582
  langchain_agentx/tools/glob/tool.py,sha256=m746Vl_aI9eFmrXYECkg16zMln9YPVxqjtijPlc5ALA,14868
583
583
  langchain_agentx/tools/grep/__init__.py,sha256=AA9qa42Ap9tw6lPQkNgKzYmr4hu44eP93qVdyP-eFNU,162
584
- langchain_agentx/tools/grep/backend.py,sha256=bzwRUJ_aZjs84oIG3l6IAVygGiLDyNsHPePiMaFMo-4,14539
584
+ langchain_agentx/tools/grep/backend.py,sha256=12BHyDZccr7Hzvfumj3zGpXrSRNV_LiCbYYyG2Y3BCg,15062
585
585
  langchain_agentx/tools/grep/models.py,sha256=6fTSux0COK06weQ7PWYqCGBQx60OaNtqC5q7xKtf5i0,3860
586
586
  langchain_agentx/tools/grep/prompt.py,sha256=Euo1NErphk5WHxaKXwJH-1v7joblfwEFWHIYNthKkzo,1336
587
- langchain_agentx/tools/grep/rg_subprocess_controller.py,sha256=ijMJnKPo7iEDFBz-_YipTiPusrL6OO2XbWxHUuwpMfA,4056
587
+ langchain_agentx/tools/grep/rg_subprocess_controller.py,sha256=A4RGCiZaIo8lrohqftzrOcDmU8VVDlSic1Pqy5ZF5Js,5315
588
+ langchain_agentx/tools/grep/ripgrep_stdout_cap.py,sha256=b7krKaCXYMjkecRwhpB1a8gaJ96BW0oyvxhrlDMQTtA,3149
588
589
  langchain_agentx/tools/grep/tool.py,sha256=S_nGFqFgGHx9UhnXJdNvkdoHCJxLRZBDoPDd-Pxms0g,20513
589
590
  langchain_agentx/tools/plan_mode/__init__.py,sha256=wO5FX24xEB0Dx9h6bysBt1ql3wn0rjPYsg70mbl4S_0,402
590
591
  langchain_agentx/tools/plan_mode/constants.py,sha256=vBjcx9JTSOhU9jfCJ4RKnCxVQPn0HzHapouKxRf2Zns,293
@@ -760,8 +761,8 @@ langchain_agentx/workspace/root_grant_manager.py,sha256=W3gy8HTevbERbXqIgRcjzOXO
760
761
  langchain_agentx/workspace/tool_boundary.py,sha256=UDwX6swpSLsx9HNkYuuRRiV5o7ZZG_Bru2Yn0c5kNX8,5724
761
762
  langchain_agentx/workspace/validators.py,sha256=tQt-6TOcL8Fw7Ig5ebA9S7vGWh1rby920eFW6x8Tk9E,1439
762
763
  langchain_agentx/workspace/view.py,sha256=PGasqTaqhlD03SXXazHuw4RHfV681AIdlsYqL70pEjc,4774
763
- langchain_agentx_python-2.2.3.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
764
- langchain_agentx_python-2.2.3.dist-info/METADATA,sha256=VcuvJI240NmP2VxSl2xxXD2BTvpnCLxnkXjg5aHzmQE,24250
765
- langchain_agentx_python-2.2.3.dist-info/WHEEL,sha256=51RkbunBAw4BWsgaQWTpPhg4Diwp3c9P5iaLk67Hdtg,92
766
- langchain_agentx_python-2.2.3.dist-info/top_level.txt,sha256=Ge284pniNt8xea0OLk2o9o32GqVpDhOYk20fwE-0xxA,17
767
- langchain_agentx_python-2.2.3.dist-info/RECORD,,
764
+ langchain_agentx_python-2.2.4.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
765
+ langchain_agentx_python-2.2.4.dist-info/METADATA,sha256=X5bLG_gFcxVwOPDJ6PWzNl-HYGukdx7N2B-TNv6ECsc,24250
766
+ langchain_agentx_python-2.2.4.dist-info/WHEEL,sha256=51RkbunBAw4BWsgaQWTpPhg4Diwp3c9P5iaLk67Hdtg,92
767
+ langchain_agentx_python-2.2.4.dist-info/top_level.txt,sha256=Ge284pniNt8xea0OLk2o9o32GqVpDhOYk20fwE-0xxA,17
768
+ langchain_agentx_python-2.2.4.dist-info/RECORD,,