langchain-agentx-python 1.3.4__py3-none-any.whl → 1.3.6__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 (26) hide show
  1. langchain_agentx/__init__.py +1 -1
  2. langchain_agentx/loop/graph/factory.py +2 -0
  3. langchain_agentx/loop/hook/graph_wiring.py +27 -27
  4. langchain_agentx/memory/memdir/extractor.py +27 -6
  5. langchain_agentx/memory/memdir/tunables.py +69 -0
  6. langchain_agentx/observability/__init__.py +4 -0
  7. langchain_agentx/observability/events/__init__.py +6 -0
  8. langchain_agentx/observability/events/agent_event_projector.py +1355 -0
  9. langchain_agentx/observability/events/factory.py +46 -0
  10. langchain_agentx/observability/events/langchain_agentx_event_adapter.py +71 -1428
  11. langchain_agentx/observability/events/workflow_aware_event_adapter.py +86 -0
  12. langchain_agentx/observability/events/workflow_event_mapper.py +84 -0
  13. langchain_agentx/workflow/__init__.py +50 -18
  14. langchain_agentx/workflow/base.py +28 -136
  15. langchain_agentx/workflow/event_adapter/__init__.py +20 -0
  16. langchain_agentx/workflow/event_adapter/adapter.py +12 -14
  17. langchain_agentx/workflow/event_adapter/handlers/parallel_handler.py +19 -14
  18. langchain_agentx/workflow/event_adapter/handlers/route_handler.py +11 -11
  19. langchain_agentx/workflow/event_adapter/handlers/stage_handler.py +12 -10
  20. langchain_agentx/workflow/event_adapter/types.py +86 -13
  21. langchain_agentx/workspace/path_canonical.py +24 -24
  22. {langchain_agentx_python-1.3.4.dist-info → langchain_agentx_python-1.3.6.dist-info}/METADATA +1 -1
  23. {langchain_agentx_python-1.3.4.dist-info → langchain_agentx_python-1.3.6.dist-info}/RECORD +26 -21
  24. {langchain_agentx_python-1.3.4.dist-info → langchain_agentx_python-1.3.6.dist-info}/LICENSE +0 -0
  25. {langchain_agentx_python-1.3.4.dist-info → langchain_agentx_python-1.3.6.dist-info}/WHEEL +0 -0
  26. {langchain_agentx_python-1.3.4.dist-info → langchain_agentx_python-1.3.6.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,7 @@ from langchain_agentx import create_loop_agent
11
11
  ```
12
12
  """
13
13
 
14
- __version__ = "1.3.4"
14
+ __version__ = "1.3.6"
15
15
 
16
16
  from .loop import ( # noqa: F401
17
17
  create_loop_agent,
@@ -876,6 +876,7 @@ class LoopGraphBuilder:
876
876
  )
877
877
  from ...memory.memdir.fork_concurrency import default_fork_concurrency_limiter
878
878
  from ...memory.memdir.metrics import default_memory_fork_metrics
879
+ from ...memory.memdir.tunables import get_memory_extraction_turn_threshold
879
880
 
880
881
  metrics = default_memory_fork_metrics()
881
882
  return MemoryExtractionCoordinator(
@@ -889,6 +890,7 @@ class LoopGraphBuilder:
889
890
  trace_collector=trace_collector,
890
891
  session_id=session_id,
891
892
  ),
893
+ turn_threshold=get_memory_extraction_turn_threshold(self._workspace_cfg),
892
894
  )
893
895
 
894
896
  def _build_session_memory_extractor(
@@ -123,34 +123,27 @@ class HookGraphWiring:
123
123
  return patch
124
124
 
125
125
  async def loop_end_node_async(self, state: dict[str, Any], runtime: Any) -> dict[str, Any]:
126
- """async loop_end:在事件循环上 await drain,避免 sync stop 节点 blocking 死锁。"""
127
- memory_patch = await self._drain_memory_extraction_async()
128
- base_patch = self.loop_end_node(state, runtime)
129
- if not memory_patch:
130
- return base_patch
131
- if not base_patch:
132
- return memory_patch
133
- merged = dict(base_patch)
134
- if "_hook_ui_events" in memory_patch:
135
- merged.setdefault("_hook_ui_events", []).extend(
136
- memory_patch.get("_hook_ui_events") or []
137
- )
138
- return merged
126
+ """async loop_end:等价 CC ``stopHooks.ts:149`` ``void executeExtractMemories(...)``
127
+ fork-and-forget 语义 —— 主回合不等抽取任务。
139
128
 
140
- async def _drain_memory_extraction_async(self) -> dict[str, Any]:
141
- if self.memory_extractor is None:
142
- return {}
143
- drain = getattr(self.memory_extractor, "drain_pending_extraction", None)
144
- if callable(drain):
145
- from langchain_agentx.memory.memdir.extractor import STOP_HOOK_DRAIN_TIMEOUT_MS
146
-
147
- await drain(STOP_HOOK_DRAIN_TIMEOUT_MS)
148
- drain_saved = getattr(self.memory_extractor, "drain_memory_saved_ui_events", None)
149
- if callable(drain_saved):
150
- saved_ui = drain_saved()
151
- if saved_ui:
152
- return {"_hook_ui_events": saved_ui}
153
- return {}
129
+ 抽取任务已由 ``stop_hooks_node`` 通过
130
+ ``Coordinator.trigger_from_state() → loop.create_task(...)`` 调度到事件循环;
131
+ task ownership 跟随顶层 owner,而非单个 turn。drain 由更高层负责:
132
+
133
+ * ``AgentSession.__aexit__`` / ``ConversationSession.__aexit__`` 60 s drain
134
+ * ``Workflow.run() finally`` ``_drain_pending_memory_extractions()`` 60 s drain
135
+ * ``BackgroundAgent`` 收尾路径独立 owner
136
+
137
+ 以下场景不在本节点处理:
138
+
139
+ * **本回合内已就绪的 ``memory_saved`` UI 事件**:由 ``loop_end_node`` 内部
140
+ 调用 ``drain_memory_saved_ui_events()`` 回收;async 路径完全对称走 sync 实现。
141
+ * **未就绪的抽取任务**:留给下一回合 trailing 回收 或 owner 边界 drain。
142
+
143
+ 历史:曾经在此 ``await drain(5_000)``,等价 ``fork()`` 后立即 ``waitpid()``,
144
+ 把 fork-and-forget 设计破坏;慢模型下必然超时 abort 导致记忆丢失。已删除。
145
+ """
146
+ return self.loop_end_node(state, runtime)
154
147
 
155
148
  def loop_end_node(self, state: dict[str, Any], runtime: Any) -> dict[str, Any]:
156
149
  if self.memory_prefetch_coordinator is not None:
@@ -161,6 +154,13 @@ class HookGraphWiring:
161
154
  patch.update(self._drain_hook_ui_events())
162
155
  if result.state_patch:
163
156
  patch.update(dict(result.state_patch))
157
+ # 回收已就绪的 memory_saved UI 事件(抽取完成,下一 turn trailing)
158
+ if self.memory_extractor is not None:
159
+ drain_saved = getattr(self.memory_extractor, "drain_memory_saved_ui_events", None)
160
+ if callable(drain_saved):
161
+ saved_ui = drain_saved()
162
+ if saved_ui:
163
+ patch.setdefault("_hook_ui_events", []).extend(saved_ui)
164
164
  if self.trace_lifecycle_collector is not None:
165
165
  patch.update(self.trace_lifecycle_collector.loop_end_node(state))
166
166
  return patch
@@ -41,7 +41,6 @@ logger = logging.getLogger(__name__)
41
41
 
42
42
  _WRITE_TOOLS = {"Write", "Edit"}
43
43
  DEFAULT_EXTRACTION_WINDOW_MESSAGES = 30
44
- STOP_HOOK_DRAIN_TIMEOUT_MS = 5_000
45
44
 
46
45
 
47
46
  def _as_int(value: Any) -> int:
@@ -95,12 +94,14 @@ class MemoryExtractionCoordinator:
95
94
  cursor_store: MemoryExtractionCursorStore | None = None,
96
95
  metrics: MemoryForkMetrics | None = None,
97
96
  on_memory_saved: MemorySavedCallback | None = None,
97
+ turn_threshold: int = 3, # ← 闸门 7: 默认每 3 轮 trigger 才执行一次 fork 抽取
98
98
  ) -> None:
99
99
  self._executor = extraction_executor
100
100
  self._forked_runner = forked_runner
101
101
  self._cursor_store = cursor_store or MemoryExtractionCursorStore()
102
102
  self._metrics = metrics or default_memory_fork_metrics()
103
103
  self._on_memory_saved = on_memory_saved
104
+ self._turn_threshold = max(1, int(turn_threshold)) # 钳位 ≥1
104
105
  self._memory_saved_ui_pending: list[dict[str, Any]] = []
105
106
  self._in_progress = False
106
107
  self._pending_context: MemoryExtractionContext | None = None
@@ -118,7 +119,7 @@ class MemoryExtractionCoordinator:
118
119
  container_type: str,
119
120
  force: bool = True,
120
121
  ) -> None:
121
- """stop hook:L2 唯一触发入口;force=True,无 token 门槛(对齐 CC stopHooks)。"""
122
+ """stop hook:L2 唯一触发入口;force=True token 门槛,turn 节流仍生效。"""
122
123
  if not self._can_extract_from_state(state, container_type, workspace_cfg):
123
124
  return
124
125
  context = self._build_context(
@@ -127,7 +128,7 @@ class MemoryExtractionCoordinator:
127
128
  session_id=session_id,
128
129
  container_type=container_type,
129
130
  force=force,
130
- skip_throttle=True,
131
+ skip_throttle=False, # 闸门 7 生效;pending 重放的 trailing 路径仍设 True
131
132
  )
132
133
  self.trigger(context)
133
134
 
@@ -166,10 +167,19 @@ class MemoryExtractionCoordinator:
166
167
  self._tasks.add(task)
167
168
  task.add_done_callback(self._tasks.discard)
168
169
 
169
- def drain_on_stop(self, timeout_ms: int = STOP_HOOK_DRAIN_TIMEOUT_MS) -> None:
170
+ def drain_on_stop(self, timeout_ms: int = 60_000) -> None:
170
171
  """跨线程阻塞等待 in-flight 抽取(勿在 owner 事件循环线程调用,会死锁)。
171
172
 
172
- Loop graph 路径应使用 ``loop_end_node_async`` ``await drain_pending_extraction``。
173
+ 语义:仅供 **进程退出 hook / sync owner 关闭** 等"无 event loop 可用"的
174
+ 外部 caller 使用。等价 CC ``print.ts:968`` 进程退出前的一次性 drain。
175
+
176
+ Loop graph 路径**不再**调用本方法(Phase 2 已删除 turn-级 drain):
177
+ async owner(AgentSession / ConversationSession.__aexit__、
178
+ Workflow.run() finally、BackgroundAgent 收尾)应直接
179
+ ``await drain_pending_extraction(60_000)``。
180
+
181
+ 默认 ``timeout_ms`` 与 owner-级 60 s 对齐,对齐 CC
182
+ ``extractMemories.ts:579`` 的 ``drainer`` 默认值。
173
183
  """
174
184
  if not self._tasks:
175
185
  return
@@ -279,6 +289,18 @@ class MemoryExtractionCoordinator:
279
289
  self._advance_cursor(context, new_messages, cursor)
280
290
  return
281
291
 
292
+ # —— 闸门 7: turn 节流(对齐 CC tengu_bramble_lintel) ——
293
+ if not context.skip_throttle:
294
+ cursor.turns_since_last_extraction += 1
295
+ if cursor.turns_since_last_extraction < self._turn_threshold:
296
+ self._cursor_store.save(
297
+ workspace_cfg=context.workspace_cfg,
298
+ session_id=context.session_id,
299
+ container_type=context.container_type,
300
+ state=cursor,
301
+ )
302
+ return
303
+
282
304
  succeeded = True
283
305
  if self._executor is not None:
284
306
  await self._executor(context, new_messages)
@@ -603,7 +625,6 @@ def build_trace_memory_saved_callback(
603
625
  __all__ = [
604
626
  "MemoryExtractionContext",
605
627
  "MemoryExtractionCoordinator",
606
- "STOP_HOOK_DRAIN_TIMEOUT_MS",
607
628
  "build_extract_prompt",
608
629
  "build_trace_memory_saved_callback",
609
630
  "create_auto_mem_can_use_tool",
@@ -0,0 +1,69 @@
1
+ """
2
+ tunables.py — L2 记忆抽取可调参数。
3
+
4
+ 职责:
5
+ 提供 get_memory_extraction_turn_threshold(),封装 env / settings.json / 默认值
6
+ 三层读取,作为闸门 7(turn 节流)的配置入口。
7
+
8
+ 链路位置:
9
+ factory.py _build_memory_extraction_coordinator
10
+ → get_memory_extraction_turn_threshold(workspace_cfg)
11
+ → MemoryExtractionCoordinator(turn_threshold=...)。
12
+
13
+ 当前裁剪范围:
14
+ 仅 turn 节流阈值;其它 L2 可调参数(fork concurrency / drain timeout 等)保持现状。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import logging
21
+ import os
22
+ from typing import Any
23
+
24
+ from langchain_agentx.workspace import ResolvedAgentStateConfig
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ _ENV_TURN_THRESHOLD = "LANGCHAIN_AGENTX_MEMORY_TURN_THRESHOLD"
29
+ _SETTINGS_KEY = "memoryExtractionTurnThreshold"
30
+ _DEFAULT_TURN_THRESHOLD = 3
31
+
32
+
33
+ def get_memory_extraction_turn_threshold(
34
+ workspace_cfg: ResolvedAgentStateConfig,
35
+ ) -> int:
36
+ """L2 闸门 7 阈值;env > settings.json > 默认 3,钳位 ≥1。"""
37
+ raw = os.environ.get(_ENV_TURN_THRESHOLD)
38
+ if raw is not None:
39
+ try:
40
+ return max(1, int(raw))
41
+ except ValueError:
42
+ logger.warning(
43
+ "ignore non-integer %s=%r, falling back to settings/default",
44
+ _ENV_TURN_THRESHOLD,
45
+ raw,
46
+ )
47
+
48
+ settings = _read_workspace_settings(workspace_cfg)
49
+ value = settings.get(_SETTINGS_KEY)
50
+ if isinstance(value, int) and not isinstance(value, bool) and value >= 1:
51
+ return value
52
+
53
+ return _DEFAULT_TURN_THRESHOLD
54
+
55
+
56
+ def _read_workspace_settings(workspace_cfg: ResolvedAgentStateConfig) -> dict[str, Any]:
57
+ """读取 workspace_config.json;缺失或损坏时返回空 dict。"""
58
+ path = workspace_cfg.workspace_config_path
59
+ if not path.exists():
60
+ return {}
61
+ try:
62
+ data = json.loads(path.read_text(encoding="utf-8"))
63
+ except (OSError, json.JSONDecodeError) as exc:
64
+ logger.warning("ignore corrupt workspace settings %s: %s", path, exc)
65
+ return {}
66
+ return data if isinstance(data, dict) else {}
67
+
68
+
69
+ __all__ = ["get_memory_extraction_turn_threshold"]
@@ -6,12 +6,14 @@ from .events import (
6
6
  FINISH_OUTCOME_KEYS,
7
7
  FinishOutcome,
8
8
  FinishOutcomeParser,
9
+ ILangGraphEventAdapter,
9
10
  LangchainAgentEvent,
10
11
  LangchainAgentEventType,
11
12
  LangGraphToLangchainAgentEventAdapter,
12
13
  RecoveryAction,
13
14
  RecoveryActionResolver,
14
15
  RecoveryPolicyMode,
16
+ create_langgraph_event_adapter,
15
17
  enrich_run_result,
16
18
  )
17
19
  from .replay.service import (
@@ -54,6 +56,7 @@ __all__ = [
54
56
  "DECISION_LAYERS",
55
57
  "DebugBurstController",
56
58
  "DebugBurstSession",
59
+ "ILangGraphEventAdapter",
57
60
  "LogContext",
58
61
  "LoggingConfigBuilder",
59
62
  "OBS_SCHEMA_VERSION",
@@ -63,6 +66,7 @@ __all__ = [
63
66
  "LangchainAgentEvent",
64
67
  "LangchainAgentEventType",
65
68
  "LangGraphToLangchainAgentEventAdapter",
69
+ "create_langgraph_event_adapter",
66
70
  "RecoveryAction",
67
71
  "RecoveryActionResolver",
68
72
  "RecoveryPolicyMode",
@@ -16,6 +16,10 @@ from .finish_outcome import (
16
16
  RecoveryPolicyMode,
17
17
  enrich_run_result,
18
18
  )
19
+ from .factory import (
20
+ ILangGraphEventAdapter,
21
+ create_langgraph_event_adapter,
22
+ )
19
23
 
20
24
  __all__ = [
21
25
  "LangchainAgentEventType",
@@ -30,4 +34,6 @@ __all__ = [
30
34
  "RecoveryActionResolver",
31
35
  "RecoveryPolicyMode",
32
36
  "enrich_run_result",
37
+ "ILangGraphEventAdapter",
38
+ "create_langgraph_event_adapter",
33
39
  ]