langchain-agentx-python 0.2.7__py3-none-any.whl → 0.2.8__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 (32) hide show
  1. langchain_agentx/loop/config/agent_loop_config.py +2 -0
  2. langchain_agentx/loop/subagent/context.py +10 -4
  3. langchain_agentx/tool_runtime/adapter.py +21 -9
  4. langchain_agentx/tool_runtime/loader.py +5 -0
  5. langchain_agentx/tool_runtime/models.py +8 -0
  6. langchain_agentx/tool_runtime/session_store.py +137 -2
  7. langchain_agentx/tool_runtime/smoke_test_runtime.py +18 -2
  8. langchain_agentx/tool_runtime/state_bridge.py +35 -103
  9. langchain_agentx/tools/bash/tool.py +10 -4
  10. langchain_agentx/tools/glob/tool.py +2 -2
  11. langchain_agentx/tools/webfetch/__init__.py +78 -0
  12. langchain_agentx/tools/webfetch/backend.py +382 -0
  13. langchain_agentx/tools/webfetch/limits.py +64 -0
  14. langchain_agentx/tools/webfetch/loader.py +34 -0
  15. langchain_agentx/tools/webfetch/models.py +103 -0
  16. langchain_agentx/tools/webfetch/preapproved.py +152 -0
  17. langchain_agentx/tools/webfetch/prompt.py +71 -0
  18. langchain_agentx/tools/webfetch/summary.py +35 -0
  19. langchain_agentx/tools/webfetch/tool.py +309 -0
  20. langchain_agentx/tools/websearch/__init__.py +37 -0
  21. langchain_agentx/tools/websearch/backend.py +255 -0
  22. langchain_agentx/tools/websearch/events.py +58 -0
  23. langchain_agentx/tools/websearch/limits.py +64 -0
  24. langchain_agentx/tools/websearch/loader.py +49 -0
  25. langchain_agentx/tools/websearch/models.py +133 -0
  26. langchain_agentx/tools/websearch/prompt.py +69 -0
  27. langchain_agentx/tools/websearch/tool.py +253 -0
  28. {langchain_agentx_python-0.2.7.dist-info → langchain_agentx_python-0.2.8.dist-info}/METADATA +516 -516
  29. {langchain_agentx_python-0.2.7.dist-info → langchain_agentx_python-0.2.8.dist-info}/RECORD +32 -15
  30. {langchain_agentx_python-0.2.7.dist-info → langchain_agentx_python-0.2.8.dist-info}/LICENSE +0 -0
  31. {langchain_agentx_python-0.2.7.dist-info → langchain_agentx_python-0.2.8.dist-info}/WHEEL +0 -0
  32. {langchain_agentx_python-0.2.7.dist-info → langchain_agentx_python-0.2.8.dist-info}/top_level.txt +0 -0
@@ -47,6 +47,8 @@ class AgentLoopConfig:
47
47
 
48
48
  # 工具与权限
49
49
  available_tools: list[Any] = field(default_factory=list)
50
+ # 为 False 时禁止需要外网的工具(如 WebSearch);只读/离线模式由宿主设置。
51
+ network_access_allowed: bool = True
50
52
  permission_mode: str = "default"
51
53
  permission_resolver: Any | None = None
52
54
  workspace_root: str | None = None # 工具层 workspace 根路径,权限检查边界
@@ -5,17 +5,19 @@
5
5
  CC 对照:src/agent/subagent/context.ts SubagentContext 接口
6
6
  当前裁剪范围:v1 全量;system_prompt / is_async 字段驱动执行路径选择;
7
7
  ``path_prep`` 与遗留 ``cwd``/worktree 自建二选一(编排层须传 ``path_prep``);
8
- fork + worktree 时可选注入 CC 等价 ``buildWorktreeNotice`` user 消息(见 ``fork_worktree_notice``)。
8
+ fork + worktree 时可选注入 CC 等价 ``buildWorktreeNotice`` user 消息(见 ``fork_worktree_notice``);
9
+ ``fork_from_parent=True`` 且父持有 ``AgentSessionStore`` 时子上下文使用 ``clone(file_state_only=True)``。
9
10
  """
10
11
 
11
12
  import os
12
13
  import shutil
13
14
  import uuid
14
- from dataclasses import dataclass, field
15
+ from dataclasses import dataclass
15
16
  from typing import Any
16
17
 
17
18
  from langchain_agentx.loop.runtime.subagent_execution_paths import PreparedSubagentPaths
18
19
  from langchain_agentx.loop.subagent.fork_worktree_notice import build_worktree_notice
20
+ from langchain_agentx.tool_runtime.session_store import AgentSessionStore
19
21
  from langchain_agentx.workspace import resolve_agent_workspace_config
20
22
 
21
23
 
@@ -57,7 +59,6 @@ class SubagentContext:
57
59
  workspace_root: str = ""
58
60
  agent_home: str = ".langchain_agentx"
59
61
  cwd: str | None = None
60
- _file_read_state: dict[str, Any] = field(default_factory=dict)
61
62
  capabilities: Any = None # RuntimeCapabilities,从父会话继承
62
63
 
63
64
 
@@ -178,6 +179,11 @@ def create_subagent_context(
178
179
  os.makedirs(worktree_dir, exist_ok=False)
179
180
  effective_cwd = (cwd.strip() if isinstance(cwd, str) and cwd.strip() else None) or worktree_dir
180
181
 
182
+ parent_store = getattr(parent_ctx, "session_store", None)
183
+ session_store: Any = parent_store
184
+ if fork_from_parent and isinstance(parent_store, AgentSessionStore):
185
+ session_store = parent_store.clone(file_state_only=True)
186
+
181
187
  return SubagentContext(
182
188
  agent_id=agent_id,
183
189
  initial_messages=initial_messages,
@@ -188,7 +194,7 @@ def create_subagent_context(
188
194
  system_prompt=system_prompt,
189
195
  fork_from_parent=fork_from_parent,
190
196
  is_async=is_async,
191
- session_store=getattr(parent_ctx, "session_store", None),
197
+ session_store=session_store,
192
198
  tool_registry=getattr(parent_ctx, "tool_registry", None),
193
199
  tool_loader=getattr(parent_ctx, "tool_loader", None),
194
200
  parent_agent_id=getattr(parent_ctx, "agent_id", None),
@@ -94,18 +94,20 @@ class LangChainAdapter:
94
94
  conversation_session_id = getattr(loop_config, "conversation_session_id", None) or session_id
95
95
  workspace_root = getattr(loop_config, "workspace_root", None)
96
96
  agent_home = getattr(loop_config, "agent_home", ".langchain_agentx")
97
- # cwd 优先级(v0.2.0):loop_config.cwd > state_bridge cwd > workspace_root > get_cwd()
98
- # state_bridge cwd 由 BashRuntimeTool.set_cwd() 维护,支持 bash cd 后其他工具自动感知
97
+ # cwdloop_config.cwd session_store.cwd_override workspace_root get_cwd()
99
98
  from langchain_agentx.utils.cwd import get_cwd as _utils_get_cwd
100
- from langchain_agentx.tool_runtime.state_bridge import ToolStateBridge
101
99
 
102
100
  cwd = getattr(loop_config, "cwd", None)
103
- if not cwd and isinstance(state, dict):
104
- # 尝试从 state_bridge 读取存储的 cwd(bash cd 设置的)
105
- bridge_cwd_key = ToolStateBridge.CWD_KEY
106
- bridge_cwd = state.get(bridge_cwd_key)
107
- if isinstance(bridge_cwd, str) and bridge_cwd:
108
- cwd = bridge_cwd
101
+ if not cwd:
102
+ sess = configurable.get("session_store")
103
+ from langchain_agentx.tool_runtime.session_store import (
104
+ AgentSessionStore as _AgentSessionStore,
105
+ )
106
+
107
+ if isinstance(sess, _AgentSessionStore):
108
+ sess_cwd = sess.cwd_override
109
+ if isinstance(sess_cwd, str) and sess_cwd:
110
+ cwd = sess_cwd
109
111
  if not cwd:
110
112
  cwd = workspace_root or _utils_get_cwd()
111
113
  trace_enabled = bool(getattr(loop_config, "trace_enabled", True))
@@ -185,6 +187,14 @@ class LangChainAdapter:
185
187
  if low in ("markdown", "html"):
186
188
  question_preview_format = low
187
189
 
190
+ network_access_allowed = bool(getattr(loop_config, "network_access_allowed", True))
191
+ if "network_access_allowed" in configurable:
192
+ network_access_allowed = bool(configurable.get("network_access_allowed"))
193
+
194
+ tool_progress_callback: Any | None = configurable.get("tool_progress_callback")
195
+ if tool_progress_callback is None and loop_config is not None:
196
+ tool_progress_callback = getattr(loop_config, "tool_progress_callback", None)
197
+
188
198
  return ToolExecutionContext(
189
199
  tool_name=tool_name,
190
200
  # 回退顺序:显式 tool_call_id -> config.tool_call_id -> input.tool_call_id -> input.id -> run_id
@@ -217,6 +227,8 @@ class LangChainAdapter:
217
227
  trace_emit_session_id=trace_emit_session_id,
218
228
  question_preview_format=question_preview_format,
219
229
  capabilities=capabilities,
230
+ network_access_allowed=network_access_allowed,
231
+ tool_progress_callback=tool_progress_callback,
220
232
  )
221
233
 
222
234
  def build_structured_tool(
@@ -117,6 +117,8 @@ class ToolRuntimeLoader:
117
117
  from langchain_agentx.tools.ripgrep_plugin_exclusions import PluginCacheGlobExclusions
118
118
  from langchain_agentx.tools.task_list.tool import TaskListRuntimeTool
119
119
  from langchain_agentx.tools.user_message import UserMessageTool
120
+ from langchain_agentx.tools.webfetch import loader as webfetch_loader
121
+ from langchain_agentx.tools.websearch import loader as websearch_loader
120
122
  from langchain_agentx.tools.write import WriteRuntimeTool
121
123
  from langchain_agentx.tool_runtime.state_bridge import ToolStateBridge
122
124
  from langchain_agentx.workspace.config import AgentWorkspaceConfig
@@ -153,6 +155,9 @@ class ToolRuntimeLoader:
153
155
  self.register(AskUserQuestionTool())
154
156
  self.register(TaskListRuntimeTool(config=cfg))
155
157
 
158
+ websearch_loader.register(self)
159
+ webfetch_loader.register(self)
160
+
156
161
  if include_agent:
157
162
  from langchain_agentx.tools.agent import AgentRuntimeTool
158
163
 
@@ -14,6 +14,7 @@ runtime/models.py — 工具运行时框架核心数据模型
14
14
  readFileState、getAppState、messages 等大量 UI/应用层字段。
15
15
  本框架的 ToolExecutionContext 只保留框架稳定依赖的最小字段集,
16
16
  UI/应用层依赖由 LangChainAdapter 在边界处消费,不透传到工具内部。
17
+ ``session_store``(AgentSessionStore)为会话级工具态后端,供 ToolStateBridge 等读写。
17
18
  协作取消:``cancel_event`` 对应 CC ``AbortSignal``(由上层 ``set()`` 终止 ripgrep 等子进程)。
18
19
 
19
20
  CC 的 ValidationResult / PermissionResult 是 TypeScript union type。
@@ -114,6 +115,13 @@ class ToolExecutionContext:
114
115
  """
115
116
  capabilities: Any | None = None
116
117
  """可选;RuntimeCapabilities 实例,供子 Agent 继承全局 model_profile_registry。"""
118
+ network_access_allowed: bool = True
119
+ """为 False 时 WebSearch 等外网工具应在 check_permissions 中 deny。"""
120
+ tool_progress_callback: Any | None = None
121
+ """
122
+ 可选;``RunnableConfig.configurable["tool_progress_callback"]`` 或 AgentLoopConfig 注入。
123
+ 签名为 ``(event: ProgressEvent) -> None``(具体类型由工具包定义,避免 runtime 循环依赖)。
124
+ """
117
125
 
118
126
 
119
127
  # ---------------------------------------------------------------------------
@@ -6,14 +6,24 @@ runtime/session_store.py — 会话级状态存储
6
6
  它独立于 LangGraph graph state(turn 级)和 ToolExecutionContext(call 级),
7
7
  负责跨 turn 持久化的会话状态:
8
8
 
9
- file_read_state — 文件读取记录(为 read-before-write 约束服务)
9
+ file_read_state — 文件读取记录(为 read-before-write / Edit staleness 服务)
10
10
  file_history — 文件写入/编辑历史
11
- audit_log — 工具调用审计日志
11
+ audit_log — 工具调用审计日志(含权限窗口事件与工具框架审计)
12
12
  permissions_cache — 权限决策缓存(避免重复询问)
13
+ cwd — Bash cd 等工作目录(与 ToolStateBridge 对齐的会话后端字段)
14
+ last_glob_summary — 最近一次 Glob 摘要(hint,非授权依据)
15
+
16
+ 链路位置:
17
+ GraphFactory 注入 configurable["session_store"] → LangChainAdapter.build_tool_execution_context
18
+ → ToolExecutionContext.session_store;ToolStateBridge **仅**读写本对象。
19
+
20
+ 当前裁剪范围:
21
+ 不做磁盘持久化;不实现设计文档中的 content LRU(仅保留字段,由 Read 侧截断占位)。
13
22
  """
14
23
 
15
24
  from __future__ import annotations
16
25
 
26
+ import copy
17
27
  import time
18
28
  import uuid
19
29
  from dataclasses import dataclass
@@ -27,6 +37,50 @@ class FileReadRecord:
27
37
  ts: float
28
38
  line_start: int | None = None
29
39
  line_end: int | None = None
40
+ offset: int | None = None
41
+ limit: int | None = None
42
+ mtime_ms: int = 0
43
+ content: str | None = None
44
+
45
+ def to_bridge_dict(self) -> dict[str, Any]:
46
+ """转为 ToolStateBridge / StalenessChecker 消费的 dict 形态。"""
47
+ out: dict[str, Any] = {
48
+ "tool_call_id": self.tool_call_id,
49
+ "ts": self.ts,
50
+ "offset": self.offset,
51
+ "limit": self.limit,
52
+ "mtime_ms": self.mtime_ms,
53
+ "line_start": self.line_start,
54
+ "line_end": self.line_end,
55
+ }
56
+ if self.content is not None:
57
+ out["content"] = self.content
58
+ return out
59
+
60
+ @classmethod
61
+ def from_bridge_dict(cls, path: str, d: dict[str, Any]) -> FileReadRecord:
62
+ ts_raw = d.get("ts")
63
+ ts = float(ts_raw) if isinstance(ts_raw, (int, float)) else time.time()
64
+ mtime_raw = d.get("mtime_ms", 0)
65
+ try:
66
+ mtime_ms = int(mtime_raw) if mtime_raw is not None else 0
67
+ except (TypeError, ValueError):
68
+ mtime_ms = 0
69
+ raw_tid = d.get("tool_call_id")
70
+ tool_call_id = raw_tid if isinstance(raw_tid, str) or raw_tid is None else None
71
+ raw_content = d.get("content")
72
+ content = raw_content if isinstance(raw_content, str) or raw_content is None else None
73
+ return cls(
74
+ path=path,
75
+ tool_call_id=tool_call_id,
76
+ ts=ts,
77
+ line_start=d.get("line_start"),
78
+ line_end=d.get("line_end"),
79
+ offset=d.get("offset"),
80
+ limit=d.get("limit"),
81
+ mtime_ms=mtime_ms,
82
+ content=content,
83
+ )
30
84
 
31
85
 
32
86
  @dataclass
@@ -47,6 +101,8 @@ class AgentSessionStore:
47
101
  self._file_history: list[FileWriteRecord] = []
48
102
  self._audit_log: list[dict[str, Any]] = []
49
103
  self._permissions_cache: dict[str, Any] = {}
104
+ self._cwd: str | None = None
105
+ self._last_glob_summary: dict[str, Any] | None = None
50
106
 
51
107
  def record_file_read(
52
108
  self,
@@ -56,6 +112,10 @@ class AgentSessionStore:
56
112
  ts: float | None = None,
57
113
  line_start: int | None = None,
58
114
  line_end: int | None = None,
115
+ offset: int | None = None,
116
+ limit: int | None = None,
117
+ mtime_ms: int = 0,
118
+ content: str | None = None,
59
119
  ) -> None:
60
120
  self._file_read_state[path] = FileReadRecord(
61
121
  path=path,
@@ -63,8 +123,31 @@ class AgentSessionStore:
63
123
  ts=ts if ts is not None else time.time(),
64
124
  line_start=line_start,
65
125
  line_end=line_end,
126
+ offset=offset,
127
+ limit=limit,
128
+ mtime_ms=mtime_ms,
129
+ content=content,
66
130
  )
67
131
 
132
+ def upsert_last_read_from_bridge_entry(
133
+ self,
134
+ path: str,
135
+ entry: dict[str, Any],
136
+ ) -> None:
137
+ """用与 LangGraph state 中 last_read 条目同构的 dict 覆盖该路径。"""
138
+ self._file_read_state[path] = FileReadRecord.from_bridge_dict(path, entry)
139
+
140
+ def replace_all_file_reads_from_bridge_map(self, data: dict[str, Any]) -> None:
141
+ """整体替换文件读映射(对应 ToolStateBridge.set_last_read_map)。"""
142
+ self._file_read_state.clear()
143
+ for path, raw in data.items():
144
+ if isinstance(raw, dict):
145
+ self._file_read_state[path] = FileReadRecord.from_bridge_dict(str(path), raw)
146
+
147
+ def get_last_read_map_bridge_view(self) -> dict[str, dict[str, Any]]:
148
+ """返回 path → bridge dict,供 ToolStateBridge 与 StalenessChecker 使用。"""
149
+ return {p: rec.to_bridge_dict() for p, rec in self._file_read_state.items()}
150
+
68
151
  def has_been_read(self, path: str) -> bool:
69
152
  return path in self._file_read_state
70
153
 
@@ -110,6 +193,56 @@ class AgentSessionStore:
110
193
  def get_cached_permission(self, key: str) -> Any | None:
111
194
  return self._permissions_cache.get(key)
112
195
 
196
+ # ---- cwd(与 ToolStateBridge 会话后端对齐)----
197
+
198
+ def get_cwd(self, *, initial: str | None = None) -> str:
199
+ """已 set 的会话 cwd → initial → os.getcwd()。"""
200
+ if isinstance(self._cwd, str) and self._cwd:
201
+ return self._cwd
202
+ if isinstance(initial, str) and initial:
203
+ return initial
204
+ import os
205
+
206
+ return os.getcwd()
207
+
208
+ def set_cwd(self, cwd: str) -> None:
209
+ self._cwd = cwd
210
+
211
+ def clear_cwd(self) -> None:
212
+ self._cwd = None
213
+
214
+ @property
215
+ def cwd_override(self) -> str | None:
216
+ """Bash cd 等显式设置过的 cwd;未设置时为 None。"""
217
+ if isinstance(self._cwd, str) and self._cwd:
218
+ return self._cwd
219
+ return None
220
+
221
+ # ---- last_glob_summary ----
222
+
223
+ def set_last_glob_summary(self, payload: dict[str, Any]) -> None:
224
+ self._last_glob_summary = dict(payload)
225
+
226
+ def get_last_glob_summary(self) -> dict[str, Any] | None:
227
+ if self._last_glob_summary is None:
228
+ return None
229
+ return dict(self._last_glob_summary)
230
+
231
+ def clear_last_glob_summary(self) -> None:
232
+ self._last_glob_summary = None
233
+
234
+ # ---- fork:仅克隆文件读状态(权限 / cwd / glob 不共享)----
235
+
236
+ def clone(self, *, file_state_only: bool = True) -> AgentSessionStore:
237
+ new = AgentSessionStore(session_id=str(uuid.uuid4()))
238
+ new._file_read_state = {
239
+ k: copy.deepcopy(v) for k, v in self._file_read_state.items()
240
+ }
241
+ if not file_state_only:
242
+ new._audit_log = copy.deepcopy(self._audit_log)
243
+ new._file_history = copy.deepcopy(self._file_history)
244
+ return new
245
+
113
246
  @property
114
247
  def created_at(self) -> float:
115
248
  return self._created_at
@@ -122,6 +255,8 @@ class AgentSessionStore:
122
255
  "file_writes": len(self._file_history),
123
256
  "audit_entries": len(self._audit_log),
124
257
  "cached_permissions": len(self._permissions_cache),
258
+ "has_cwd_override": bool(self._cwd),
259
+ "has_glob_summary": self._last_glob_summary is not None,
125
260
  }
126
261
 
127
262
  def __repr__(self) -> str:
@@ -52,7 +52,17 @@ def _load_module(sub: str) -> None:
52
52
  spec.loader.exec_module(mod)
53
53
 
54
54
 
55
- for _sub in ["models", "errors", "base", "policy", "state_bridge", "pipeline", "adapter", "registry"]:
55
+ for _sub in [
56
+ "models",
57
+ "errors",
58
+ "base",
59
+ "policy",
60
+ "session_store",
61
+ "state_bridge",
62
+ "pipeline",
63
+ "adapter",
64
+ "registry",
65
+ ]:
56
66
  _load_module(_sub)
57
67
 
58
68
  # ---------------------------------------------------------------------------
@@ -65,6 +75,7 @@ from langchain_agentx.tool_runtime.pipeline import ToolExecutorPipeline
65
75
  from langchain_agentx.tool_runtime.adapter import LangChainAdapter # noqa: E402
66
76
  from langchain_agentx.tool_runtime.registry import RuntimeToolRegistry # noqa: E402
67
77
  from langchain_agentx.tool_runtime.policy import ToolPolicyConfig, DefaultPolicyEngine # noqa: E402
78
+ from langchain_agentx.tool_runtime.session_store import AgentSessionStore # noqa: E402
68
79
  from langchain_agentx.tool_runtime.state_bridge import ToolStateBridge # noqa: E402
69
80
  from pydantic import BaseModel # noqa: E402
70
81
 
@@ -104,6 +115,7 @@ def _make_ctx(state=None) -> ToolExecutionContext:
104
115
  tool_call_id="call_001",
105
116
  input_args={"message": "hello"},
106
117
  state=state if state is not None else {},
118
+ session_store=AgentSessionStore("smoke"),
107
119
  )
108
120
 
109
121
 
@@ -217,7 +229,11 @@ def test_state_bridge() -> None:
217
229
  bridge = ToolStateBridge()
218
230
  state = {}
219
231
  ctx = ToolExecutionContext(
220
- tool_name="echo", tool_call_id="c1", input_args={}, state=state
232
+ tool_name="echo",
233
+ tool_call_id="c1",
234
+ input_args={},
235
+ state=state,
236
+ session_store=AgentSessionStore("smoke-bridge"),
221
237
  )
222
238
 
223
239
  _td = pathlib.Path(tempfile.gettempdir())
@@ -2,23 +2,16 @@
2
2
  runtime/state_bridge.py — 工具级最小共享状态管理
3
3
 
4
4
  职责:
5
- 管理工具执行过程中的跨工具共享状态,维护:
6
- - last_read_map:记录每个文件最近一次被读取的信息,
7
- 为 write/edit 工具的 read-before-write 约束提供基础
8
- - tool_audit:工具调用审计日志,记录每次工具执行的关键信息
9
- - last_glob_summary(v2-D):最近一次 Glob 的摘要,供 Read/Grep 等读侧 **hint**(不替代授权)
10
-
11
- 状态存储在 ToolExecutionContext.state(LangGraph state)中,
12
- 通过固定 key 读写,不引入独立的状态存储。
13
-
14
- CC 对比:
15
- CC 中通过 ToolUseContext.readFileState 维护文件读取状态,
16
- 通过 getAppState / setAppState 读写应用级状态。
17
- 本框架将这些能力收敛到 ToolStateBridge,通过 LangGraph state
18
- 作为底层存储,保持与 agent loop 的状态一致性。
19
- v1:last_read_map(含 mtime_ms、可选 content,供 Edit staleness 方案 B)、tool_audit;
20
- v2-D:last_glob_summary(可选写入,见 GlobRuntimeTool.after_invoke)。
21
- 不承担业务 workflow state。
5
+ 管理工具执行过程中的跨工具共享状态(last_read_map、tool_audit、last_glob_summary、cwd),
6
+ 为 write/edit 的 read-before-write、Edit staleness、Glob hint、Bash cd 等提供统一入口。
7
+
8
+ 链路位置:
9
+ ToolRuntimeLoader.register_default_tools 注入共享 ToolStateBridge;Read/Edit/Bash/Glob
10
+ after_invoke 或校验阶段调用本类。存储后端**仅**为 ``ToolExecutionContext.session_store``
11
+ (``AgentSessionStore``),由 GraphFactory 经 ``configurable`` 注入。
12
+
13
+ 当前裁剪范围:
14
+ 不再读写 LangGraph ``ctx.state`` 中 ``_tool_*`` 键(迁移完成);不在此模块内做 LRU。
22
15
  """
23
16
 
24
17
  from __future__ import annotations
@@ -26,27 +19,17 @@ from __future__ import annotations
26
19
  from typing import Any
27
20
 
28
21
  from .models import ToolExecutionContext
22
+ from .session_store import AgentSessionStore
29
23
 
30
24
 
31
25
  class ToolStateBridge:
32
26
  """
33
27
  工具级最小共享状态管理器。
34
28
 
35
- 通过 ToolExecutionContext.state(LangGraph state dict)读写状态,
36
- 使用固定 key 隔离工具框架状态与业务状态。
37
-
38
- 维护:
39
- last_read_map — 文件最近读取记录(为 read-before-write 规则服务)
40
- tool_audit — 工具调用审计日志
41
- last_glob_summary — 最近一次 Glob 摘要(仅路径样本,防 state 膨胀)
42
- cwd — 当前工作目录(跨工具共享,bash cd 后 read/grep 自动感知)
43
-
44
- 不承担:
45
- 业务 workflow state
46
- UI 临时态
47
- task state
29
+ 要求 ``ctx.session_store`` ``AgentSessionStore``;否则各方法抛出 ``ValueError``。
48
30
  """
49
31
 
32
+ # 历史 LangGraph ctx.state 键名;bridge 已不读写 state,常量保留供设计文档/外部对照与 rg。
50
33
  LAST_READ_MAP_KEY = "_tool_last_read_map"
51
34
  AUDIT_LOG_KEY = "_tool_audit_log"
52
35
  LAST_GLOB_SUMMARY_KEY = "_tool_last_glob_summary"
@@ -61,32 +44,30 @@ class ToolStateBridge:
61
44
  """
62
45
  self._initial_cwd = initial_cwd
63
46
 
47
+ def _require_store(self, ctx: ToolExecutionContext) -> AgentSessionStore:
48
+ raw = ctx.session_store
49
+ if not isinstance(raw, AgentSessionStore):
50
+ raise ValueError(
51
+ "ToolStateBridge requires ToolExecutionContext.session_store to be an "
52
+ "AgentSessionStore instance (LangGraph state _tool_* keys are no longer used)."
53
+ )
54
+ return raw
55
+
64
56
  # ---- last_read_map ----
65
57
 
66
58
  def get_last_read_map(self, ctx: ToolExecutionContext) -> dict[str, Any]:
67
- """
68
- 获取 last_read_map。
69
-
70
- 结构(每条 file_path):至少含 ``tool_call_id``、``ts``;Read 写入时还含
71
- ``offset``、``limit``、``mtime_ms``;可选 ``content``(文本快照或占位符,
72
- 供 Edit staleness)、``line_start`` / ``line_end``(Read 元数据)。
73
-
74
- 不存在时返回空 dict(不修改 state)。
75
- """
76
- return ctx.state.get(self.LAST_READ_MAP_KEY) or {}
59
+ return self._require_store(ctx).get_last_read_map_bridge_view()
77
60
 
78
61
  def set_last_read_map(
79
62
  self,
80
63
  ctx: ToolExecutionContext,
81
64
  value: dict[str, Any],
82
65
  ) -> None:
83
- """更新 last_read_map,整体替换。"""
84
- ctx.state[self.LAST_READ_MAP_KEY] = value
66
+ self._require_store(ctx).replace_all_file_reads_from_bridge_map(value)
85
67
 
86
68
  def get_last_read(
87
69
  self, ctx: ToolExecutionContext, file_path: str
88
70
  ) -> dict[str, Any] | None:
89
- """返回 ``file_path`` 对应的最近一次读取记录;无则 ``None``。"""
90
71
  raw = self.get_last_read_map(ctx).get(file_path)
91
72
  return raw if isinstance(raw, dict) else None
92
73
 
@@ -102,13 +83,6 @@ class ToolStateBridge:
102
83
  line_start: int | None = None,
103
84
  line_end: int | None = None,
104
85
  ) -> None:
105
- """
106
- 写入/覆盖某路径的最近一次读取记录(Read ``after_invoke`` 主入口)。
107
-
108
- ``content``:可选;UTF-8 文本快照或 ``<image:...>`` 等占位符,供 Edit
109
- staleness 内容对比;过大文本由调用方截断为占位符。
110
- """
111
- last_read_map = self.get_last_read_map(ctx)
112
86
  entry: dict[str, Any] = {
113
87
  "tool_call_id": ctx.tool_call_id,
114
88
  "ts": ctx.now_ts,
@@ -120,8 +94,7 @@ class ToolStateBridge:
120
94
  }
121
95
  if content is not None:
122
96
  entry["content"] = content
123
- last_read_map[file_path] = entry
124
- self.set_last_read_map(ctx, last_read_map)
97
+ self._require_store(ctx).upsert_last_read_from_bridge_entry(file_path, entry)
125
98
 
126
99
  def record_read(
127
100
  self,
@@ -131,10 +104,6 @@ class ToolStateBridge:
131
104
  line_start: int | None = None,
132
105
  line_end: int | None = None,
133
106
  ) -> None:
134
- """
135
- 记录一次文件读取(兼容旧 API / 单测),等价于 ``offset=0``、``mtime_ms=0``
136
- 的 ``set_last_read``。
137
- """
138
107
  self.set_last_read(
139
108
  ctx,
140
109
  file_path,
@@ -146,9 +115,6 @@ class ToolStateBridge:
146
115
  )
147
116
 
148
117
  def has_been_read(self, ctx: ToolExecutionContext, file_path: str) -> bool:
149
- """
150
- 检查文件是否已被读取过(用于 read-before-write 约束)。
151
- """
152
118
  return file_path in self.get_last_read_map(ctx)
153
119
 
154
120
  # ---- tool_audit ----
@@ -158,24 +124,12 @@ class ToolStateBridge:
158
124
  ctx: ToolExecutionContext,
159
125
  record: dict[str, Any],
160
126
  ) -> None:
161
- """
162
- 追加一条审计记录到 tool_audit 列表。
163
-
164
- record 建议包含:
165
- tool — 工具名
166
- tool_call_id — 调用 ID
167
- ts — 时间戳
168
- 以及工具特定的关键字段(如 file_path、lines_read 等)
169
- """
170
- audit_log: list = ctx.state.get(self.AUDIT_LOG_KEY) or []
171
- audit_log.append(record)
172
- ctx.state[self.AUDIT_LOG_KEY] = audit_log
127
+ self._require_store(ctx).append_audit(record)
173
128
 
174
129
  def get_audit_log(self, ctx: ToolExecutionContext) -> list[dict[str, Any]]:
175
- """获取完整审计日志,不存在时返回空列表。"""
176
- return ctx.state.get(self.AUDIT_LOG_KEY) or []
130
+ return self._require_store(ctx).get_audit_log()
177
131
 
178
- # ---- last_glob_summary(v2-D,本工程专有;CC 无对等)----
132
+ # ---- last_glob_summary ----
179
133
 
180
134
  def record_last_glob(
181
135
  self,
@@ -189,13 +143,8 @@ class ToolStateBridge:
189
143
  truncated: bool,
190
144
  offset: int,
191
145
  ) -> None:
192
- """
193
- 覆盖写入最近一次 Glob 摘要(**不**用于 PolicyEngine,仅供 hint)。
194
-
195
- ``filenames`` 为当前页相对路径列表;仅持久化前 ``MAX_GLOB_SAMPLE_PATHS`` 条。
196
- """
197
146
  sample = list(filenames[: self.MAX_GLOB_SAMPLE_PATHS])
198
- ctx.state[self.LAST_GLOB_SUMMARY_KEY] = {
147
+ payload = {
199
148
  "pattern": pattern,
200
149
  "search_root": search_root,
201
150
  "sample_paths": sample,
@@ -206,35 +155,18 @@ class ToolStateBridge:
206
155
  "tool_call_id": ctx.tool_call_id,
207
156
  "ts": ctx.now_ts,
208
157
  }
158
+ self._require_store(ctx).set_last_glob_summary(payload)
209
159
 
210
160
  def get_last_glob_summary(self, ctx: ToolExecutionContext) -> dict[str, Any] | None:
211
- """读取最近一次 Glob 摘要;不存在或类型不对时返回 ``None``。"""
212
- raw = ctx.state.get(self.LAST_GLOB_SUMMARY_KEY)
213
- return raw if isinstance(raw, dict) else None
161
+ return self._require_store(ctx).get_last_glob_summary()
214
162
 
215
163
  def clear_last_glob_summary(self, ctx: ToolExecutionContext) -> None:
216
- """清除摘要(测试或显式重置会话工具态时使用)。"""
217
- ctx.state.pop(self.LAST_GLOB_SUMMARY_KEY, None)
164
+ self._require_store(ctx).clear_last_glob_summary()
218
165
 
219
- # ---- cwd(跨工具共享工作目录,v0.2.0 新增)----
166
+ # ---- cwd ----
220
167
 
221
168
  def get_cwd(self, ctx: ToolExecutionContext) -> str:
222
- """获取当前工作目录。
223
-
224
- 优先级:
225
- 1. state 中已存储的 cwd(之前 cd 命令设置的)
226
- 2. initial_cwd(构造时传入)
227
- 3. os.getcwd()(进程当前目录)
228
- """
229
- stored = ctx.state.get(self.CWD_KEY)
230
- if isinstance(stored, str) and stored:
231
- return stored
232
- if self._initial_cwd:
233
- return self._initial_cwd
234
- import os
235
-
236
- return os.getcwd()
169
+ return self._require_store(ctx).get_cwd(initial=self._initial_cwd)
237
170
 
238
171
  def set_cwd(self, ctx: ToolExecutionContext, cwd: str) -> None:
239
- """设置当前工作目录(bash cd 命令后调用)。"""
240
- ctx.state[self.CWD_KEY] = cwd
172
+ self._require_store(ctx).set_cwd(cwd)
@@ -266,7 +266,7 @@ class BashRuntimeTool(RuntimeTool):
266
266
  trace_context=TraceContext.from_ctx(ctx),
267
267
  )
268
268
 
269
- current_cwd = self._get_current_cwd()
269
+ current_cwd = self._get_current_cwd(ctx)
270
270
  analysis = self._ast_analyzer.analyze(command)
271
271
  hardening_decision = self._security_hardener.check_command(
272
272
  command=command,
@@ -855,12 +855,18 @@ class BashRuntimeTool(RuntimeTool):
855
855
  ask_prompt=prompt,
856
856
  )
857
857
 
858
- def _get_current_cwd(self) -> str:
858
+ def _get_current_cwd(self, ctx: ToolExecutionContext | None = None) -> str:
859
859
  """获取当前工作目录(用于 check_permissions 阶段)。
860
860
 
861
- 注意:此时 ctx 不可用,因此使用初始 cwd 或 os.getcwd()
862
- 实际执行时 invoke() 会用 ctx 获取最新的 cwd。
861
+ ``invoke()`` 一致:优先 ``state_bridge.get_cwd(ctx)``;否则
862
+ ``_initial_cwd``(构造时从 bridge 属性读取);最后 ``os.getcwd()``。
863
863
  """
864
+ if self._state_bridge is not None and ctx is not None:
865
+ getter = getattr(self._state_bridge, "get_cwd", None)
866
+ if callable(getter):
867
+ cwd = getter(ctx)
868
+ if cwd:
869
+ return str(cwd)
864
870
  if self._initial_cwd:
865
871
  return self._initial_cwd
866
872
  return os.getcwd()