langchain-agentx-python 0.9.2__py3-none-any.whl → 0.9.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.
- langchain_agentx/__init__.py +64 -64
- langchain_agentx/loop/context/compaction_service.py +24 -5
- langchain_agentx/loop/context/pipeline.py +2 -0
- langchain_agentx/loop/context/recovery_mode.py +37 -0
- langchain_agentx/loop/context/types.py +4 -1
- langchain_agentx/loop/graph/factory.py +22 -1
- langchain_agentx/loop/graph/graph_edges.py +16 -12
- langchain_agentx/loop/graph/reactive_compact_node.py +230 -0
- langchain_agentx/observability/events/finish_event_payload_composer.py +30 -4
- langchain_agentx/session/__init__.py +10 -0
- langchain_agentx/session/agent_session.py +81 -9
- langchain_agentx/session/recovery.py +111 -0
- langchain_agentx/tool_runtime/path_safety.py +7 -2
- {langchain_agentx_python-0.9.2.dist-info → langchain_agentx_python-0.9.4.dist-info}/METADATA +519 -519
- {langchain_agentx_python-0.9.2.dist-info → langchain_agentx_python-0.9.4.dist-info}/RECORD +18 -15
- {langchain_agentx_python-0.9.2.dist-info → langchain_agentx_python-0.9.4.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-0.9.2.dist-info → langchain_agentx_python-0.9.4.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-0.9.2.dist-info → langchain_agentx_python-0.9.4.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
"""
|
|
2
|
-
LangChain-based agent utilities for CodeBaseX.
|
|
3
|
-
|
|
4
|
-
This package provides:
|
|
5
|
-
- LangChain-AgentX LangGraph agent factory (`loop`)
|
|
6
|
-
|
|
7
|
-
Typical usage:
|
|
8
|
-
|
|
9
|
-
```python
|
|
10
|
-
from langchain_agentx import create_loop_agent
|
|
11
|
-
```
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
__version__ = "0.9.
|
|
15
|
-
|
|
16
|
-
from .loop import ( # noqa: F401
|
|
17
|
-
create_loop_agent,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
from .loop.hook import ( # noqa: F401
|
|
21
|
-
ATTACHMENT_HOOK_BLOCKING_ERROR,
|
|
22
|
-
ATTACHMENT_HOOK_CANCELLED,
|
|
23
|
-
ATTACHMENT_HOOK_NON_BLOCKING_ERROR,
|
|
24
|
-
ATTACHMENT_HOOK_STOPPED_CONTINUATION,
|
|
25
|
-
ATTACHMENT_HOOK_SUCCESS,
|
|
26
|
-
)
|
|
27
|
-
from .observability import ( # noqa: F401
|
|
28
|
-
EventAdapterState,
|
|
29
|
-
LangchainAgentEvent,
|
|
30
|
-
LangchainAgentEventType,
|
|
31
|
-
LangGraphToLangchainAgentEventAdapter,
|
|
32
|
-
TraceCollector,
|
|
33
|
-
TraceSession,
|
|
34
|
-
TraceSpan,
|
|
35
|
-
)
|
|
36
|
-
from .provider import CompatibleChatOpenAI # noqa: F401
|
|
37
|
-
from .session import AgentSession, create_agent_session # noqa: F401
|
|
38
|
-
from .health import HealthCheck, HealthReport, PreflightResult, preflight, sdk_health # noqa: F401
|
|
39
|
-
from .workflow import BaseWorkflow # noqa: F401
|
|
40
|
-
|
|
41
|
-
__all__ = [
|
|
42
|
-
"ATTACHMENT_HOOK_BLOCKING_ERROR",
|
|
43
|
-
"ATTACHMENT_HOOK_CANCELLED",
|
|
44
|
-
"ATTACHMENT_HOOK_NON_BLOCKING_ERROR",
|
|
45
|
-
"ATTACHMENT_HOOK_STOPPED_CONTINUATION",
|
|
46
|
-
"ATTACHMENT_HOOK_SUCCESS",
|
|
47
|
-
"create_loop_agent",
|
|
48
|
-
"create_agent_session",
|
|
49
|
-
"AgentSession",
|
|
50
|
-
"BaseWorkflow",
|
|
51
|
-
"EventAdapterState",
|
|
52
|
-
"LangchainAgentEvent",
|
|
53
|
-
"LangchainAgentEventType",
|
|
54
|
-
"LangGraphToLangchainAgentEventAdapter",
|
|
55
|
-
"TraceCollector",
|
|
56
|
-
"TraceSession",
|
|
57
|
-
"TraceSpan",
|
|
58
|
-
"CompatibleChatOpenAI",
|
|
59
|
-
"HealthCheck",
|
|
60
|
-
"HealthReport",
|
|
61
|
-
"PreflightResult",
|
|
62
|
-
"preflight",
|
|
63
|
-
"sdk_health",
|
|
64
|
-
]
|
|
1
|
+
"""
|
|
2
|
+
LangChain-based agent utilities for CodeBaseX.
|
|
3
|
+
|
|
4
|
+
This package provides:
|
|
5
|
+
- LangChain-AgentX LangGraph agent factory (`loop`)
|
|
6
|
+
|
|
7
|
+
Typical usage:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from langchain_agentx import create_loop_agent
|
|
11
|
+
```
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "0.9.4"
|
|
15
|
+
|
|
16
|
+
from .loop import ( # noqa: F401
|
|
17
|
+
create_loop_agent,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .loop.hook import ( # noqa: F401
|
|
21
|
+
ATTACHMENT_HOOK_BLOCKING_ERROR,
|
|
22
|
+
ATTACHMENT_HOOK_CANCELLED,
|
|
23
|
+
ATTACHMENT_HOOK_NON_BLOCKING_ERROR,
|
|
24
|
+
ATTACHMENT_HOOK_STOPPED_CONTINUATION,
|
|
25
|
+
ATTACHMENT_HOOK_SUCCESS,
|
|
26
|
+
)
|
|
27
|
+
from .observability import ( # noqa: F401
|
|
28
|
+
EventAdapterState,
|
|
29
|
+
LangchainAgentEvent,
|
|
30
|
+
LangchainAgentEventType,
|
|
31
|
+
LangGraphToLangchainAgentEventAdapter,
|
|
32
|
+
TraceCollector,
|
|
33
|
+
TraceSession,
|
|
34
|
+
TraceSpan,
|
|
35
|
+
)
|
|
36
|
+
from .provider import CompatibleChatOpenAI # noqa: F401
|
|
37
|
+
from .session import AgentSession, create_agent_session # noqa: F401
|
|
38
|
+
from .health import HealthCheck, HealthReport, PreflightResult, preflight, sdk_health # noqa: F401
|
|
39
|
+
from .workflow import BaseWorkflow # noqa: F401
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"ATTACHMENT_HOOK_BLOCKING_ERROR",
|
|
43
|
+
"ATTACHMENT_HOOK_CANCELLED",
|
|
44
|
+
"ATTACHMENT_HOOK_NON_BLOCKING_ERROR",
|
|
45
|
+
"ATTACHMENT_HOOK_STOPPED_CONTINUATION",
|
|
46
|
+
"ATTACHMENT_HOOK_SUCCESS",
|
|
47
|
+
"create_loop_agent",
|
|
48
|
+
"create_agent_session",
|
|
49
|
+
"AgentSession",
|
|
50
|
+
"BaseWorkflow",
|
|
51
|
+
"EventAdapterState",
|
|
52
|
+
"LangchainAgentEvent",
|
|
53
|
+
"LangchainAgentEventType",
|
|
54
|
+
"LangGraphToLangchainAgentEventAdapter",
|
|
55
|
+
"TraceCollector",
|
|
56
|
+
"TraceSession",
|
|
57
|
+
"TraceSpan",
|
|
58
|
+
"CompatibleChatOpenAI",
|
|
59
|
+
"HealthCheck",
|
|
60
|
+
"HealthReport",
|
|
61
|
+
"PreflightResult",
|
|
62
|
+
"preflight",
|
|
63
|
+
"sdk_health",
|
|
64
|
+
]
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
设计要点:
|
|
9
9
|
- `LoopContextCompaction` 构造注入 `ContextPipeline`,便于单测替换阶段序列;
|
|
10
|
-
- `default_loop_context_compaction()`
|
|
10
|
+
- `default_loop_context_compaction()` 返回进程内单例,避免每轮重复装配;
|
|
11
|
+
- 支持 `mode` 参数区分 PROACTIVE(常规)与 REACTIVE_CONTEXT_WINDOW(溢出恢复)。
|
|
11
12
|
|
|
12
13
|
注意:
|
|
13
14
|
- 不修改 `should_end` / `finish_reason` 等 exit 字段;只产出 `messages` 增量补丁。
|
|
@@ -19,6 +20,7 @@ from __future__ import annotations
|
|
|
19
20
|
from typing import Any, Dict, Mapping
|
|
20
21
|
|
|
21
22
|
from .pipeline import ContextPipeline
|
|
23
|
+
from .recovery_mode import CompactionMode
|
|
22
24
|
from .settings import CompactionSettings
|
|
23
25
|
from .types import CompactProgressCallback, ContextCompactionContext
|
|
24
26
|
|
|
@@ -38,9 +40,22 @@ class LoopContextCompaction:
|
|
|
38
40
|
"""使用 CC 默认五段顺序装配管道。"""
|
|
39
41
|
return cls(ContextPipeline.with_default_stages(settings=settings))
|
|
40
42
|
|
|
41
|
-
def run(
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
def run(
|
|
44
|
+
self,
|
|
45
|
+
state: Mapping[str, Any],
|
|
46
|
+
model: Any | None = None,
|
|
47
|
+
*,
|
|
48
|
+
mode: str | CompactionMode = CompactionMode.PROACTIVE,
|
|
49
|
+
) -> Dict[str, Any]:
|
|
50
|
+
"""返回 `Command(update=...)` 可用的补丁(常见为 ``{}`` 或 ``{"messages": ...}``)。
|
|
51
|
+
|
|
52
|
+
参数:
|
|
53
|
+
state: LangGraph state
|
|
54
|
+
model: 可选的 LLM model(供 autocompact 使用)
|
|
55
|
+
mode: 压缩模式,默认 PROACTIVE
|
|
56
|
+
"""
|
|
57
|
+
mode_str = mode if isinstance(mode, str) else mode.value
|
|
58
|
+
ctx = ContextCompactionContext.from_state(state, model, mode=mode_str)
|
|
44
59
|
return self._pipeline.run(state, ctx)
|
|
45
60
|
|
|
46
61
|
async def arun(
|
|
@@ -48,6 +63,8 @@ class LoopContextCompaction:
|
|
|
48
63
|
state: Mapping[str, Any],
|
|
49
64
|
model: Any | None = None,
|
|
50
65
|
progress_callback: CompactProgressCallback = None,
|
|
66
|
+
*,
|
|
67
|
+
mode: str | CompactionMode = CompactionMode.PROACTIVE,
|
|
51
68
|
) -> Dict[str, Any]:
|
|
52
69
|
"""Async 版本(供 /compact 命令使用),支持进度回调。
|
|
53
70
|
|
|
@@ -55,11 +72,13 @@ class LoopContextCompaction:
|
|
|
55
72
|
state: LangGraph state
|
|
56
73
|
model: 可选的 LLM model(供 autocompact 使用)
|
|
57
74
|
progress_callback: 进度回调函数,接收 CompactProgress 对象
|
|
75
|
+
mode: 压缩模式,默认 PROACTIVE
|
|
58
76
|
|
|
59
77
|
返回:
|
|
60
78
|
与 run() 相同的补丁格式
|
|
61
79
|
"""
|
|
62
|
-
|
|
80
|
+
mode_str = mode if isinstance(mode, str) else mode.value
|
|
81
|
+
ctx = ContextCompactionContext.from_state(state, model, mode=mode_str)
|
|
63
82
|
ctx.progress_callback = progress_callback
|
|
64
83
|
return await self._pipeline.arun(state, ctx)
|
|
65
84
|
|
|
@@ -114,6 +114,7 @@ class ContextPipeline:
|
|
|
114
114
|
"snip_tokens_freed_carry_final": ctx.snip_tokens_freed_carry,
|
|
115
115
|
"token_warning_level": tw.level,
|
|
116
116
|
"stages": stages_meta,
|
|
117
|
+
"mode": ctx.mode,
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
if not patch:
|
|
@@ -194,6 +195,7 @@ class ContextPipeline:
|
|
|
194
195
|
"snip_tokens_freed_carry_final": ctx.snip_tokens_freed_carry,
|
|
195
196
|
"token_warning_level": tw.level,
|
|
196
197
|
"stages": stages_meta,
|
|
198
|
+
"mode": ctx.mode,
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
if not patch:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
recovery_mode.py — 上下文压缩模式枚举。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
定义 PROACTIVE(常规 pre-model)与 REACTIVE_CONTEXT_WINDOW(溢出恢复)两种模式,
|
|
6
|
+
供 ContextPipeline 与 ReactiveCompactNode 使用。
|
|
7
|
+
|
|
8
|
+
链路位置:
|
|
9
|
+
- CompactionMode 传入 ContextCompactionContext.mode
|
|
10
|
+
- 写入 compaction_pipeline_meta.mode 字段
|
|
11
|
+
- ReactiveCompactNode 使用 REACTIVE_CONTEXT_WINDOW 模式
|
|
12
|
+
|
|
13
|
+
当前裁剪范围:
|
|
14
|
+
当前仅定义枚举类型;未来可扩展 mode 切换检测逻辑。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from enum import Enum
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CompactionMode(str, Enum):
|
|
23
|
+
"""上下文压缩模式。"""
|
|
24
|
+
|
|
25
|
+
PROACTIVE = "proactive"
|
|
26
|
+
"""常规 pre-model 治理模式(prune_and_compress 节点默认)。"""
|
|
27
|
+
|
|
28
|
+
REACTIVE_CONTEXT_WINDOW = "reactive_context_window"
|
|
29
|
+
"""上下文窗口溢出恢复模式(reactive_compact 节点专用)。"""
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def is_valid(cls, value: str) -> bool:
|
|
33
|
+
"""检查字符串是否为有效 CompactionMode。"""
|
|
34
|
+
return value in {m.value for m in cls}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = ["CompactionMode"]
|
|
@@ -40,16 +40,19 @@ class ContextCompactionContext:
|
|
|
40
40
|
query_source: str | None = None
|
|
41
41
|
config: CompactionConfig = field(default_factory=CompactionConfig)
|
|
42
42
|
progress_callback: CompactProgressCallback = None
|
|
43
|
+
mode: str = "proactive"
|
|
44
|
+
"""压缩模式:proactive(常规)或 reactive_context_window(溢出恢复)。"""
|
|
43
45
|
|
|
44
46
|
@classmethod
|
|
45
47
|
def from_state(
|
|
46
|
-
cls, state: Mapping[str, Any], model: Any | None
|
|
48
|
+
cls, state: Mapping[str, Any], model: Any | None, mode: str = "proactive"
|
|
47
49
|
) -> ContextCompactionContext:
|
|
48
50
|
return cls(
|
|
49
51
|
model=model,
|
|
50
52
|
query_source=state.get("query_source")
|
|
51
53
|
if isinstance(state.get("query_source"), str)
|
|
52
54
|
else None,
|
|
55
|
+
mode=mode,
|
|
53
56
|
)
|
|
54
57
|
|
|
55
58
|
|
|
@@ -52,6 +52,7 @@ from ..exit.exit_logic import (
|
|
|
52
52
|
LoopAgentStateFields,
|
|
53
53
|
)
|
|
54
54
|
from .graph_edges import _make_model_to_model_edge, _make_tools_to_model_edge, _make_loop_controller_edge
|
|
55
|
+
from .reactive_compact_node import ReactiveCompactNode
|
|
55
56
|
from ..model.schema_and_format import (
|
|
56
57
|
STRUCTURED_OUTPUT_ERROR_TEMPLATE,
|
|
57
58
|
FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT,
|
|
@@ -1372,7 +1373,27 @@ class LoopGraphBuilder:
|
|
|
1372
1373
|
graph_inner.add_edge("hook_after_model", "hook_stop")
|
|
1373
1374
|
graph_inner.add_edge("hook_stop", "loop_controller")
|
|
1374
1375
|
|
|
1375
|
-
|
|
1376
|
+
# 添加 reactive_compact 节点(上下文窗口溢出恢复专用节点)
|
|
1377
|
+
def _reactive_compact_node(state, runtime):
|
|
1378
|
+
node = ReactiveCompactNode(
|
|
1379
|
+
compaction_service=default_loop_context_compaction(),
|
|
1380
|
+
loop_entry_destination=loop_entry_node,
|
|
1381
|
+
end_destination=exit_node,
|
|
1382
|
+
)
|
|
1383
|
+
return node(state)
|
|
1384
|
+
|
|
1385
|
+
graph_inner.add_node(
|
|
1386
|
+
"reactive_compact",
|
|
1387
|
+
RunnableCallable(_reactive_compact_node, None, trace=False),
|
|
1388
|
+
)
|
|
1389
|
+
# reactive_compact 成功路径 → loop_entry_node
|
|
1390
|
+
graph_inner.add_conditional_edges(
|
|
1391
|
+
"reactive_compact",
|
|
1392
|
+
lambda state: state.get("destination", loop_entry_node),
|
|
1393
|
+
[loop_entry_node, exit_node],
|
|
1394
|
+
)
|
|
1395
|
+
|
|
1396
|
+
loop_controller_destinations = ["tools", loop_entry_node, exit_node, "reactive_compact"]
|
|
1376
1397
|
if provider_chain and "task_event_commit_ack" not in loop_controller_destinations:
|
|
1377
1398
|
loop_controller_destinations.append("task_event_commit_ack")
|
|
1378
1399
|
graph_inner.add_conditional_edges(
|
|
@@ -328,10 +328,12 @@ class LoopControllerEdge:
|
|
|
328
328
|
withheld_error = state.get("withheld_error")
|
|
329
329
|
if not withheld_error:
|
|
330
330
|
return None
|
|
331
|
+
# 先检查是否为 context_window 溢出(CC 对齐:prompt_too_long 或 max_tokens 都可能)
|
|
332
|
+
overflow_kind = resolve_overflow_kind(withheld_error)
|
|
333
|
+
if overflow_kind == "context_window":
|
|
334
|
+
return self._handle_context_window_recovery(state, withheld_error)
|
|
335
|
+
# 再检查是否为 max_tokens(输出限制)
|
|
331
336
|
if withheld_error.get("reason") == TERMINAL_REASON_MAX_TOKENS:
|
|
332
|
-
overflow_kind = resolve_overflow_kind(withheld_error)
|
|
333
|
-
if overflow_kind == "context_window":
|
|
334
|
-
return self._handle_context_window_recovery(state, withheld_error)
|
|
335
337
|
return self._handle_max_tokens_recovery(state, withheld_error)
|
|
336
338
|
return self._handle_generic_withheld(state, withheld_error)
|
|
337
339
|
|
|
@@ -340,25 +342,27 @@ class LoopControllerEdge:
|
|
|
340
342
|
state: dict[str, Any],
|
|
341
343
|
withheld_error: dict[str, Any],
|
|
342
344
|
) -> str:
|
|
345
|
+
"""处理上下文窗口溢出恢复。
|
|
346
|
+
|
|
347
|
+
CC 对齐设计:
|
|
348
|
+
- 路由到 reactive_compact 节点,由节点决定成功或 exhausted
|
|
349
|
+
- transition 在 reactive_compact 成功后才写入
|
|
350
|
+
- 已尝试过时直接走 exhausted 路径
|
|
351
|
+
"""
|
|
343
352
|
if not state.get("has_attempted_reactive_compact", False):
|
|
344
|
-
state["has_attempted_reactive_compact"] = True
|
|
345
|
-
state["transition"] = {
|
|
346
|
-
"reason": TRANSITION_REACTIVE_COMPACT_RETRY,
|
|
347
|
-
"error_reason": TERMINAL_REASON_MAX_TOKENS,
|
|
348
|
-
"overflow_kind": "context_window",
|
|
349
|
-
}
|
|
350
353
|
emit_loop_decision_event(
|
|
351
354
|
state=state,
|
|
352
355
|
decision="continue",
|
|
353
|
-
selected_edge=
|
|
354
|
-
reason_code="
|
|
356
|
+
selected_edge="reactive_compact",
|
|
357
|
+
reason_code="context_window_recovery_to_compact",
|
|
355
358
|
evidence={
|
|
356
359
|
"withheld_error": withheld_error,
|
|
357
360
|
"overflow_kind": "context_window",
|
|
358
361
|
"step": state.get("step", 0),
|
|
359
362
|
},
|
|
360
363
|
)
|
|
361
|
-
return
|
|
364
|
+
return "reactive_compact"
|
|
365
|
+
# 已尝试过,走 exhausted 路径
|
|
362
366
|
state["withheld_error"] = None
|
|
363
367
|
state["should_end"] = True
|
|
364
368
|
state["terminal_reason"] = TERMINAL_REASON_MAX_TOKENS
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
reactive_compact_node.py — 上下文窗口溢出恢复节点。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
作为专用 overflow recovery 节点,仅在 context_window 溢出时触发,
|
|
6
|
+
执行 recovery compact 成功后才写入 reactive_compact_retry transition。
|
|
7
|
+
|
|
8
|
+
链路位置:
|
|
9
|
+
loop_controller (检测 context_window 溢出) → reactive_compact → loop_entry
|
|
10
|
+
失败路径: reactive_compact → exhausted → END
|
|
11
|
+
|
|
12
|
+
当前裁剪范围:
|
|
13
|
+
- 实现 _should_attempt_recovery 判断
|
|
14
|
+
- 实现 _execute_recovery_compact 核心逻辑
|
|
15
|
+
- 成功路径:写 transition、回 loop_entry
|
|
16
|
+
- 失败路径:exhausted、走 END
|
|
17
|
+
- 暂不实现 LLM 摘要(mode=REACTIVE 时只走 prune 路径)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from ..context.compaction_service import LoopContextCompaction
|
|
26
|
+
from ..context.recovery_mode import CompactionMode
|
|
27
|
+
from ..exit.reason_codes import (
|
|
28
|
+
TERMINAL_REASON_MAX_TOKENS,
|
|
29
|
+
TRANSITION_CONTEXT_WINDOW_RECOVERY_EXHAUSTED,
|
|
30
|
+
TRANSITION_REACTIVE_COMPACT_RETRY,
|
|
31
|
+
)
|
|
32
|
+
from ...observability.trace.event_emitter import emit_loop_decision_event
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# 状态键常量
|
|
37
|
+
STATE_HAS_ATTEMPTED_REACTIVE_COMPACT = "has_attempted_reactive_compact"
|
|
38
|
+
STATE_MESSAGES = "messages"
|
|
39
|
+
STATE_WITHHELD_ERROR = "withheld_error"
|
|
40
|
+
STATE_SHOULD_END = "should_end"
|
|
41
|
+
STATE_TERMINAL_REASON = "terminal_reason"
|
|
42
|
+
STATE_TRANSITION = "transition"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ReactiveCompactNode:
|
|
46
|
+
"""上下文窗口溢出恢复专用节点。
|
|
47
|
+
|
|
48
|
+
与 prune_and_compress 的区别:
|
|
49
|
+
- prune_and_compress:通用 pre-model 治理节点,PROACTIVE 模式
|
|
50
|
+
- reactive_compact:仅在 context_window 溢出时触发,REACTIVE_CONTEXT_WINDOW 模式
|
|
51
|
+
|
|
52
|
+
关键契约:
|
|
53
|
+
- 只在 compact 成功(messages 实际被压缩)后才写 transition
|
|
54
|
+
- 失败时设置 exhausted=True,走 terminal 分支
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
compaction_service: LoopContextCompaction | None = None,
|
|
60
|
+
*,
|
|
61
|
+
loop_entry_destination: str = "loop_entry_node",
|
|
62
|
+
end_destination: str = "__end__",
|
|
63
|
+
) -> None:
|
|
64
|
+
"""初始化 ReactiveCompactNode。
|
|
65
|
+
|
|
66
|
+
参数:
|
|
67
|
+
compaction_service: 压缩服务,默认使用 default_loop_context_compaction()
|
|
68
|
+
loop_entry_destination: 成功后跳转目标(默认 loop_entry_node)
|
|
69
|
+
end_destination: 失败后跳转目标(默认 __end__)
|
|
70
|
+
"""
|
|
71
|
+
if compaction_service is None:
|
|
72
|
+
from ..context.compaction_service import default_loop_context_compaction
|
|
73
|
+
|
|
74
|
+
compaction_service = default_loop_context_compaction()
|
|
75
|
+
self._compaction_service = compaction_service
|
|
76
|
+
self._loop_entry_destination = loop_entry_destination
|
|
77
|
+
self._end_destination = end_destination
|
|
78
|
+
|
|
79
|
+
def _should_attempt_recovery(self, state: dict[str, Any]) -> bool:
|
|
80
|
+
"""判断是否应该尝试恢复。
|
|
81
|
+
|
|
82
|
+
检查 state.has_attempted_reactive_compact 标记:
|
|
83
|
+
- False:首次溢出,应尝试 recovery
|
|
84
|
+
- True:已尝试过,应走 exhausted 分支
|
|
85
|
+
|
|
86
|
+
参数:
|
|
87
|
+
state: LangGraph state
|
|
88
|
+
|
|
89
|
+
返回:
|
|
90
|
+
True 表示应尝试 recovery,False 表示应直接 exhausted
|
|
91
|
+
"""
|
|
92
|
+
return not state.get(STATE_HAS_ATTEMPTED_REACTIVE_COMPACT, False)
|
|
93
|
+
|
|
94
|
+
def _execute_recovery_compact(
|
|
95
|
+
self, state: dict[str, Any]
|
|
96
|
+
) -> dict[str, Any]:
|
|
97
|
+
"""执行 recovery compact 并返回状态补丁。
|
|
98
|
+
|
|
99
|
+
参数:
|
|
100
|
+
state: LangGraph state
|
|
101
|
+
|
|
102
|
+
返回:
|
|
103
|
+
状态补丁,可能包含:
|
|
104
|
+
- 成功:{messages: [...], transition: {...}, has_attempted_reactive_compact: True}
|
|
105
|
+
- 失败:{exhausted: True, error_kind: "context_window", should_end: True, ...}
|
|
106
|
+
"""
|
|
107
|
+
messages = state.get(STATE_MESSAGES, [])
|
|
108
|
+
if not isinstance(messages, list):
|
|
109
|
+
logger.warning("Recovery compact: messages 不是列表,跳过 compact")
|
|
110
|
+
return self._build_exhausted_patch(state)
|
|
111
|
+
|
|
112
|
+
# 记录原始 messages 用于比较
|
|
113
|
+
original_id = id(messages)
|
|
114
|
+
original_len = len(messages)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
# 使用 REACTIVE_CONTEXT_WINDOW 模式执行 compact
|
|
118
|
+
patch = self._compaction_service.run(
|
|
119
|
+
state,
|
|
120
|
+
model=None, # 暂不支持 LLM 摘要
|
|
121
|
+
mode=CompactionMode.REACTIVE_CONTEXT_WINDOW,
|
|
122
|
+
)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.exception(f"Recovery compact 异常: {e}")
|
|
125
|
+
return self._build_exhausted_patch(state)
|
|
126
|
+
|
|
127
|
+
# 检查 compact 是否实际压缩了 messages
|
|
128
|
+
# 成功标准:messages 被替换(id 变化)或数量减少
|
|
129
|
+
if not patch:
|
|
130
|
+
logger.warning("Recovery compact: 无变化,视为失败")
|
|
131
|
+
return self._build_exhausted_patch(state)
|
|
132
|
+
|
|
133
|
+
new_messages = patch.get(STATE_MESSAGES, messages)
|
|
134
|
+
if not isinstance(new_messages, list):
|
|
135
|
+
logger.warning("Recovery compact: 返回 messages 不是列表")
|
|
136
|
+
return self._build_exhausted_patch(state)
|
|
137
|
+
|
|
138
|
+
# 检查是否实际压缩
|
|
139
|
+
is_compacted = (
|
|
140
|
+
id(new_messages) != original_id or len(new_messages) < original_len
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if not is_compacted:
|
|
144
|
+
logger.warning("Recovery compact: messages 未实际压缩,视为失败")
|
|
145
|
+
return self._build_exhausted_patch(state)
|
|
146
|
+
|
|
147
|
+
# 成功:构建返回补丁
|
|
148
|
+
result = dict(patch)
|
|
149
|
+
result[STATE_HAS_ATTEMPTED_REACTIVE_COMPACT] = True
|
|
150
|
+
result[STATE_TRANSITION] = {
|
|
151
|
+
"reason": TRANSITION_REACTIVE_COMPACT_RETRY,
|
|
152
|
+
"error_reason": TERMINAL_REASON_MAX_TOKENS,
|
|
153
|
+
"overflow_kind": "context_window",
|
|
154
|
+
}
|
|
155
|
+
result["destination"] = self._loop_entry_destination
|
|
156
|
+
|
|
157
|
+
logger.info(f"Recovery compact 成功: {original_len} → {len(new_messages)} 条消息")
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
def _build_exhausted_patch(
|
|
161
|
+
self, state: dict[str, Any]
|
|
162
|
+
) -> dict[str, Any]:
|
|
163
|
+
"""构建 exhausted 状态补丁。
|
|
164
|
+
|
|
165
|
+
参数:
|
|
166
|
+
state: LangGraph state
|
|
167
|
+
|
|
168
|
+
返回:
|
|
169
|
+
exhausted 状态补丁
|
|
170
|
+
"""
|
|
171
|
+
return {
|
|
172
|
+
STATE_HAS_ATTEMPTED_REACTIVE_COMPACT: True,
|
|
173
|
+
STATE_WITHHELD_ERROR: None,
|
|
174
|
+
STATE_SHOULD_END: True,
|
|
175
|
+
STATE_TERMINAL_REASON: TERMINAL_REASON_MAX_TOKENS,
|
|
176
|
+
STATE_TRANSITION: {
|
|
177
|
+
"reason": TRANSITION_CONTEXT_WINDOW_RECOVERY_EXHAUSTED,
|
|
178
|
+
"error_reason": TERMINAL_REASON_MAX_TOKENS,
|
|
179
|
+
"overflow_kind": "context_window",
|
|
180
|
+
},
|
|
181
|
+
"error_kind": "context_window",
|
|
182
|
+
"exhausted": True,
|
|
183
|
+
"destination": self._end_destination,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
def __call__(self, state: dict[str, Any]) -> dict[str, Any]:
|
|
187
|
+
"""节点入口(LangGraph 调用约定)。
|
|
188
|
+
|
|
189
|
+
参数:
|
|
190
|
+
state: LangGraph state
|
|
191
|
+
|
|
192
|
+
返回:
|
|
193
|
+
状态补丁
|
|
194
|
+
"""
|
|
195
|
+
if not self._should_attempt_recovery(state):
|
|
196
|
+
logger.info("Recovery compact: 已尝试过,直接 exhausted")
|
|
197
|
+
result = self._build_exhausted_patch(state)
|
|
198
|
+
emit_loop_decision_event(
|
|
199
|
+
state=state,
|
|
200
|
+
decision="terminal",
|
|
201
|
+
selected_edge=self._end_destination,
|
|
202
|
+
reason_code="context_window_recovery_exhausted",
|
|
203
|
+
evidence={"step": state.get("step", 0), "already_attempted": True},
|
|
204
|
+
)
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
logger.info("Recovery compact: 首次溢出,尝试 recovery")
|
|
208
|
+
result = self._execute_recovery_compact(state)
|
|
209
|
+
|
|
210
|
+
if result.get("exhausted"):
|
|
211
|
+
emit_loop_decision_event(
|
|
212
|
+
state=state,
|
|
213
|
+
decision="terminal",
|
|
214
|
+
selected_edge=self._end_destination,
|
|
215
|
+
reason_code="context_window_recovery_exhausted",
|
|
216
|
+
evidence={"step": state.get("step", 0), "compact_failed": True},
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
emit_loop_decision_event(
|
|
220
|
+
state=state,
|
|
221
|
+
decision="continue",
|
|
222
|
+
selected_edge=self._loop_entry_destination,
|
|
223
|
+
reason_code="reactive_compact_retry",
|
|
224
|
+
evidence={"step": state.get("step", 0)},
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return result
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
__all__ = ["ReactiveCompactNode"]
|
|
@@ -22,8 +22,10 @@ from langchain_core.messages import AIMessage
|
|
|
22
22
|
|
|
23
23
|
from langchain_agentx.loop.exit.reason_codes import (
|
|
24
24
|
ERROR_TERMINAL_REASONS,
|
|
25
|
+
TERMINAL_REASON_COMPLETED,
|
|
25
26
|
TERMINAL_REASON_ERROR,
|
|
26
27
|
TERMINAL_REASON_MAX_TOKENS,
|
|
28
|
+
TRANSITION_API_ERROR_TERMINAL,
|
|
27
29
|
TRANSITION_CONTEXT_WINDOW_RECOVERY_EXHAUSTED,
|
|
28
30
|
TRANSITION_MAX_OUTPUT_TOKENS_RECOVERY_EXHAUSTED,
|
|
29
31
|
TRANSITION_WITHHOLD_RECOVERY_EXHAUSTED,
|
|
@@ -117,7 +119,9 @@ class FinishEventPayloadComposer:
|
|
|
117
119
|
|
|
118
120
|
from langchain_agentx.loop.exit.exit_logic import RECOVERABLE_TERMINAL_REASONS
|
|
119
121
|
|
|
120
|
-
is_error = self._resolve_is_error(
|
|
122
|
+
is_error = self._resolve_is_error(
|
|
123
|
+
terminal_reason, withheld_error, snap, messages=messages
|
|
124
|
+
)
|
|
121
125
|
recoverable = (
|
|
122
126
|
terminal_reason in RECOVERABLE_TERMINAL_REASONS if terminal_reason else False
|
|
123
127
|
)
|
|
@@ -125,6 +129,14 @@ class FinishEventPayloadComposer:
|
|
|
125
129
|
recovery_exhausted = (
|
|
126
130
|
transition_reason in _RECOVERY_EXHAUSTED_TRANSITIONS if is_error else False
|
|
127
131
|
)
|
|
132
|
+
if (
|
|
133
|
+
not recovery_exhausted
|
|
134
|
+
and is_error
|
|
135
|
+
and terminal_reason == TERMINAL_REASON_MAX_TOKENS
|
|
136
|
+
and resolve_overflow_kind(withheld_error) == "context_window"
|
|
137
|
+
and bool(snap.get("has_attempted_reactive_compact"))
|
|
138
|
+
):
|
|
139
|
+
recovery_exhausted = True
|
|
128
140
|
error_kind = self._infer_error_kind(
|
|
129
141
|
terminal_reason=terminal_reason,
|
|
130
142
|
is_error=is_error,
|
|
@@ -166,7 +178,14 @@ class FinishEventPayloadComposer:
|
|
|
166
178
|
terminal_reason: str | None,
|
|
167
179
|
withheld_error: dict[str, Any] | None,
|
|
168
180
|
snap: dict[str, Any],
|
|
181
|
+
*,
|
|
182
|
+
messages: list[Any] | None = None,
|
|
169
183
|
) -> bool:
|
|
184
|
+
transition_reason = _transition_reason(snap)
|
|
185
|
+
if transition_reason == TRANSITION_API_ERROR_TERMINAL:
|
|
186
|
+
return True
|
|
187
|
+
if _is_api_error_message(_last_ai_message(messages)):
|
|
188
|
+
return True
|
|
170
189
|
if terminal_reason in ERROR_TERMINAL_REASONS:
|
|
171
190
|
return True
|
|
172
191
|
if isinstance(withheld_error, dict) and withheld_error and snap.get("should_end"):
|
|
@@ -185,6 +204,13 @@ class FinishEventPayloadComposer:
|
|
|
185
204
|
if not is_error:
|
|
186
205
|
return None
|
|
187
206
|
|
|
207
|
+
last_ai = _last_ai_message(messages)
|
|
208
|
+
if (
|
|
209
|
+
transition_reason == TRANSITION_API_ERROR_TERMINAL
|
|
210
|
+
or _is_api_error_message(last_ai)
|
|
211
|
+
):
|
|
212
|
+
return "api_error"
|
|
213
|
+
|
|
188
214
|
overflow_kind = resolve_overflow_kind(withheld_error)
|
|
189
215
|
if terminal_reason == TERMINAL_REASON_MAX_TOKENS:
|
|
190
216
|
if overflow_kind == "context_window" or (
|
|
@@ -198,9 +224,9 @@ class FinishEventPayloadComposer:
|
|
|
198
224
|
return "unknown"
|
|
199
225
|
|
|
200
226
|
if terminal_reason == TERMINAL_REASON_ERROR:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
227
|
+
return "unknown"
|
|
228
|
+
|
|
229
|
+
if terminal_reason == TERMINAL_REASON_COMPLETED:
|
|
204
230
|
return "unknown"
|
|
205
231
|
|
|
206
232
|
return "unknown"
|