langchain-agentx-python 2.0.0__py3-none-any.whl → 2.1.0__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 +1 -1
- langchain_agentx/loop/graph/graph_edges.py +2 -18
- langchain_agentx/loop/graph/reactive_compact_node.py +1 -0
- langchain_agentx/loop/model/model_node.py +23 -0
- langchain_agentx/loop/stream/loop_driver.py +162 -69
- langchain_agentx/loop/stream/result.py +1 -0
- langchain_agentx/loop/subagent/context.py +19 -0
- langchain_agentx/loop/subagent/runner.py +12 -0
- langchain_agentx/observability/__init__.py +4 -0
- langchain_agentx/observability/events/__init__.py +4 -0
- langchain_agentx/observability/events/finish_outcome.py +30 -0
- langchain_agentx/session/__init__.py +2 -6
- langchain_agentx/session/agent_session.py +10 -35
- langchain_agentx/session/conversation_session.py +8 -0
- langchain_agentx/session/recovery.py +6 -86
- langchain_agentx/tools/bash/path_security.py +89 -21
- langchain_agentx/tools/bash/semantic_matrix.py +7 -5
- langchain_agentx/tools/glob/tool.py +30 -2
- langchain_agentx/tools/grep/backend.py +13 -3
- langchain_agentx/tools/grep/rg_subprocess_controller.py +2 -0
- langchain_agentx/tools/grep/tool.py +22 -2
- langchain_agentx/tools/search_path_resolver.py +132 -0
- langchain_agentx/workflow/execution.py +14 -0
- {langchain_agentx_python-2.0.0.dist-info → langchain_agentx_python-2.1.0.dist-info}/METADATA +1 -1
- {langchain_agentx_python-2.0.0.dist-info → langchain_agentx_python-2.1.0.dist-info}/RECORD +28 -27
- {langchain_agentx_python-2.0.0.dist-info → langchain_agentx_python-2.1.0.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-2.0.0.dist-info → langchain_agentx_python-2.1.0.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-2.0.0.dist-info → langchain_agentx_python-2.1.0.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -70,7 +70,6 @@ from ..exit.reason_codes import (
|
|
|
70
70
|
TRANSITION_MAX_OUTPUT_TOKENS_ESCALATE,
|
|
71
71
|
TRANSITION_MAX_OUTPUT_TOKENS_RECOVERY,
|
|
72
72
|
TRANSITION_MAX_OUTPUT_TOKENS_RECOVERY_EXHAUSTED,
|
|
73
|
-
TRANSITION_REACTIVE_COMPACT_RETRY,
|
|
74
73
|
TRANSITION_STOP_HOOK_BLOCKING,
|
|
75
74
|
TRANSITION_STOP_HOOK_PREVENTED,
|
|
76
75
|
TRANSITION_TASK_NOTIFICATION_ARRIVED,
|
|
@@ -452,6 +451,8 @@ class LoopControllerEdge:
|
|
|
452
451
|
step=state.get("step", 0),
|
|
453
452
|
turn_count=int(state.get("turn_count", 0) or 0),
|
|
454
453
|
provider_finish_reason=state.get("finish_reason"),
|
|
454
|
+
error_kind="context_window",
|
|
455
|
+
recovery_exhausted=True,
|
|
455
456
|
)
|
|
456
457
|
state["transition"] = {
|
|
457
458
|
"reason": TRANSITION_CONTEXT_WINDOW_RECOVERY_EXHAUSTED,
|
|
@@ -585,23 +586,6 @@ class LoopControllerEdge:
|
|
|
585
586
|
state,
|
|
586
587
|
destination=self._model_destination,
|
|
587
588
|
)
|
|
588
|
-
if not state.get("has_attempted_reactive_compact", False):
|
|
589
|
-
state["has_attempted_reactive_compact"] = True
|
|
590
|
-
state["transition"] = {
|
|
591
|
-
"reason": TRANSITION_REACTIVE_COMPACT_RETRY,
|
|
592
|
-
"error_reason": withheld_error.get("reason"),
|
|
593
|
-
}
|
|
594
|
-
emit_loop_decision_event(
|
|
595
|
-
state=state,
|
|
596
|
-
decision="continue",
|
|
597
|
-
selected_edge=self._model_destination,
|
|
598
|
-
reason_code="reactive_compact_retry",
|
|
599
|
-
evidence={"withheld_error": withheld_error, "step": state.get("step", 0)},
|
|
600
|
-
)
|
|
601
|
-
return self._set_continue_resolution(
|
|
602
|
-
state,
|
|
603
|
-
destination=self._model_destination,
|
|
604
|
-
)
|
|
605
589
|
state["transition"] = {
|
|
606
590
|
"reason": TRANSITION_WITHHOLD_RECOVERY_RETRY,
|
|
607
591
|
"error_reason": withheld_error.get("reason"),
|
|
@@ -1248,6 +1248,29 @@ class ModelNode(Runnable):
|
|
|
1248
1248
|
and max_output_tokens_override is not None
|
|
1249
1249
|
and max_output_tokens_recovery_count < MAX_OUTPUT_TOKENS_RECOVERY_LIMIT
|
|
1250
1250
|
)
|
|
1251
|
+
if (
|
|
1252
|
+
should_end
|
|
1253
|
+
and terminal_reason in RECOVERABLE_TERMINAL_REASONS
|
|
1254
|
+
and overflow_kind == "context_window"
|
|
1255
|
+
):
|
|
1256
|
+
withheld_error = build_withheld_error(
|
|
1257
|
+
terminal_reason=terminal_reason,
|
|
1258
|
+
finish_reason=finish_reason,
|
|
1259
|
+
last_ai_message=last_ai_message,
|
|
1260
|
+
)
|
|
1261
|
+
return {
|
|
1262
|
+
"should_end": False,
|
|
1263
|
+
"terminal_reason": None,
|
|
1264
|
+
"withheld_error": withheld_error,
|
|
1265
|
+
"withhold_retry_count": withhold_retry_count,
|
|
1266
|
+
"max_output_tokens_override": max_output_tokens_override,
|
|
1267
|
+
"max_output_tokens_recovery_count": max_output_tokens_recovery_count,
|
|
1268
|
+
"transition": {
|
|
1269
|
+
"reason": TRANSITION_WITHHOLD_RECOVERY_RETRY,
|
|
1270
|
+
"error_reason": withheld_error["reason"],
|
|
1271
|
+
"attempt": withhold_retry_count,
|
|
1272
|
+
},
|
|
1273
|
+
}
|
|
1251
1274
|
if should_end and terminal_reason in RECOVERABLE_TERMINAL_REASONS and (
|
|
1252
1275
|
can_default_withhold_retry or can_max_tokens_recovery_retry
|
|
1253
1276
|
):
|
|
@@ -10,6 +10,7 @@ loop_driver.py — Loop-aware shared stream driver.
|
|
|
10
10
|
当前裁剪范围:
|
|
11
11
|
root closeout 捕获、synthetic error/finish、orphan tool_result repair(stream surface)。
|
|
12
12
|
正常结束但缺 root on_chain_end(LangGraph) 时打 anomaly WARNING(不伪造 synthetic finish)。
|
|
13
|
+
R1 反应恢复:单次 drain 内 budgeted continue 重入(S2,对齐 CC queryLoop continue)。
|
|
13
14
|
"""
|
|
14
15
|
|
|
15
16
|
from __future__ import annotations
|
|
@@ -23,6 +24,7 @@ from langgraph.errors import GraphRecursionError
|
|
|
23
24
|
from ..exit.reason_codes import (
|
|
24
25
|
TERMINAL_REASON_GRAPH_RUNTIME_ERROR,
|
|
25
26
|
TERMINAL_REASON_RUNTIME_BUDGET_EXHAUSTED,
|
|
27
|
+
TRANSITION_REACTIVE_COMPACT_RETRY,
|
|
26
28
|
)
|
|
27
29
|
from ..exit.terminal import apply_terminal_reason_projection, build_loop_terminal, read_terminal_reason
|
|
28
30
|
from langchain_agentx.observability.events.tool_call_id_resolver import ToolCallIdResolver
|
|
@@ -57,6 +59,7 @@ class LoopStreamDriver:
|
|
|
57
59
|
*,
|
|
58
60
|
config: dict[str, Any] | None = None,
|
|
59
61
|
version: str = "v2",
|
|
62
|
+
max_reactive_continue_per_drain: int = 0,
|
|
60
63
|
) -> AsyncIterator[dict[str, Any]]:
|
|
61
64
|
self._graph_input = dict(graph_input)
|
|
62
65
|
self._assistant_turn_count = 0
|
|
@@ -66,83 +69,173 @@ class LoopStreamDriver:
|
|
|
66
69
|
self.result.last_raw_event = None
|
|
67
70
|
self.result.saw_chat_model_end_last_turn = False
|
|
68
71
|
self.result.saw_partial_superseded = False
|
|
72
|
+
self.result.reactive_continue_drains = 0
|
|
73
|
+
current_input = dict(graph_input)
|
|
74
|
+
reactive_continues = 0
|
|
69
75
|
try:
|
|
70
|
-
|
|
71
|
-
async for event in
|
|
72
|
-
|
|
76
|
+
while True:
|
|
77
|
+
async for event in self._stream_single_drain(
|
|
78
|
+
graph,
|
|
79
|
+
current_input,
|
|
73
80
|
config=config,
|
|
74
81
|
version=version,
|
|
75
82
|
):
|
|
76
|
-
self._observe_event(event)
|
|
77
83
|
yield event
|
|
78
|
-
self.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
terminal = build_loop_terminal(
|
|
98
|
-
reason=TERMINAL_REASON_RUNTIME_BUDGET_EXHAUSTED,
|
|
99
|
-
error_kind="graph_recursion_error",
|
|
100
|
-
step=(self.result.last_loop_snapshot or {}).get("step"),
|
|
101
|
-
turn_count=(self.result.last_loop_snapshot or {}).get("turn_count"),
|
|
102
|
-
provider_finish_reason=(self.result.last_loop_snapshot or {}).get("finish_reason"),
|
|
103
|
-
errors=[str(exc)],
|
|
104
|
-
metadata={
|
|
105
|
-
"langgraph_error_type": "GraphRecursionError",
|
|
106
|
-
},
|
|
107
|
-
)
|
|
108
|
-
self.result.terminal = terminal
|
|
109
|
-
synthetic_output = self._build_synthetic_graph_output(terminal)
|
|
110
|
-
self.result.graph_output = synthetic_output
|
|
111
|
-
self.result.saw_finish_surface = True
|
|
112
|
-
yield build_synthetic_root_chain_end(graph_output=synthetic_output)
|
|
113
|
-
self._log_catch_error_diagnostic(exc)
|
|
114
|
-
raise
|
|
115
|
-
except Exception as exc:
|
|
116
|
-
self.result.final_error = exc
|
|
117
|
-
# CC catch 协议 §5.3.3:先记录日志 / 观测,再补 stream surface,最后 re-raise
|
|
118
|
-
self._log_catch_error(exc)
|
|
119
|
-
for repair in self._orphan_tool_result_repairs(closeout_kind="interrupted"):
|
|
120
|
-
yield repair
|
|
121
|
-
if not self.result.saw_error_visible_surface:
|
|
122
|
-
self.result.saw_error_visible_surface = True
|
|
123
|
-
yield build_synthetic_error_visible_event(
|
|
124
|
-
message=str(exc),
|
|
125
|
-
error_kind="graph_runtime_error",
|
|
126
|
-
)
|
|
127
|
-
if not self.result.saw_finish_surface:
|
|
128
|
-
terminal = build_loop_terminal(
|
|
129
|
-
reason=TERMINAL_REASON_GRAPH_RUNTIME_ERROR,
|
|
130
|
-
error_kind="graph_runtime_error",
|
|
131
|
-
step=(self.result.last_loop_snapshot or {}).get("step"),
|
|
132
|
-
turn_count=(self.result.last_loop_snapshot or {}).get("turn_count"),
|
|
133
|
-
provider_finish_reason=(self.result.last_loop_snapshot or {}).get("finish_reason"),
|
|
134
|
-
errors=[str(exc)],
|
|
135
|
-
)
|
|
136
|
-
self.result.terminal = terminal
|
|
137
|
-
synthetic_output = self._build_synthetic_graph_output(terminal)
|
|
138
|
-
self.result.graph_output = synthetic_output
|
|
139
|
-
self.result.saw_finish_surface = True
|
|
140
|
-
yield build_synthetic_root_chain_end(graph_output=synthetic_output)
|
|
141
|
-
self._log_catch_error_diagnostic(exc)
|
|
142
|
-
raise
|
|
84
|
+
if not self._should_reactive_continue_drain(
|
|
85
|
+
continues_used=reactive_continues,
|
|
86
|
+
max_continues=max_reactive_continue_per_drain,
|
|
87
|
+
):
|
|
88
|
+
break
|
|
89
|
+
reactive_continues += 1
|
|
90
|
+
self.result.reactive_continue_drains = reactive_continues
|
|
91
|
+
current_input = self._build_continue_graph_input(
|
|
92
|
+
current_input,
|
|
93
|
+
self.result.graph_output or {},
|
|
94
|
+
)
|
|
95
|
+
self._graph_input = dict(current_input)
|
|
96
|
+
self._reset_for_continue_drain()
|
|
97
|
+
logger.info(
|
|
98
|
+
"LoopStreamDriver R1 continue: drain %s/%s transition=%s",
|
|
99
|
+
reactive_continues,
|
|
100
|
+
max_reactive_continue_per_drain,
|
|
101
|
+
(self.result.graph_output or {}).get("transition", {}).get("reason"),
|
|
102
|
+
)
|
|
143
103
|
finally:
|
|
144
104
|
self._log_stream_closeout_summary()
|
|
145
105
|
|
|
106
|
+
async def _stream_single_drain(
|
|
107
|
+
self,
|
|
108
|
+
graph: Any,
|
|
109
|
+
graph_input: dict[str, Any],
|
|
110
|
+
*,
|
|
111
|
+
config: dict[str, Any] | None = None,
|
|
112
|
+
version: str = "v2",
|
|
113
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
114
|
+
try:
|
|
115
|
+
async for event in graph.astream_events(
|
|
116
|
+
graph_input,
|
|
117
|
+
config=config,
|
|
118
|
+
version=version,
|
|
119
|
+
):
|
|
120
|
+
self._observe_event(event)
|
|
121
|
+
yield event
|
|
122
|
+
self.result.graph_completed = True
|
|
123
|
+
self._log_missing_root_chain_end_anomaly()
|
|
124
|
+
except asyncio.CancelledError as exc:
|
|
125
|
+
self.result.final_error = exc
|
|
126
|
+
for repair in self._orphan_tool_result_repairs(closeout_kind="cancelled"):
|
|
127
|
+
yield repair
|
|
128
|
+
raise
|
|
129
|
+
except GraphRecursionError as exc:
|
|
130
|
+
self.result.final_error = exc
|
|
131
|
+
self._log_catch_error(exc)
|
|
132
|
+
for repair in self._orphan_tool_result_repairs(closeout_kind="interrupted"):
|
|
133
|
+
yield repair
|
|
134
|
+
if not self.result.saw_error_visible_surface:
|
|
135
|
+
self.result.saw_error_visible_surface = True
|
|
136
|
+
yield build_synthetic_error_visible_event(
|
|
137
|
+
message="Runtime budget exhausted (recursion limit reached)",
|
|
138
|
+
error_kind="graph_recursion_error",
|
|
139
|
+
)
|
|
140
|
+
if not self.result.saw_finish_surface:
|
|
141
|
+
terminal = build_loop_terminal(
|
|
142
|
+
reason=TERMINAL_REASON_RUNTIME_BUDGET_EXHAUSTED,
|
|
143
|
+
error_kind="graph_recursion_error",
|
|
144
|
+
step=(self.result.last_loop_snapshot or {}).get("step"),
|
|
145
|
+
turn_count=(self.result.last_loop_snapshot or {}).get("turn_count"),
|
|
146
|
+
provider_finish_reason=(self.result.last_loop_snapshot or {}).get("finish_reason"),
|
|
147
|
+
errors=[str(exc)],
|
|
148
|
+
metadata={
|
|
149
|
+
"langgraph_error_type": "GraphRecursionError",
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
self.result.terminal = terminal
|
|
153
|
+
synthetic_output = self._build_synthetic_graph_output(terminal)
|
|
154
|
+
self.result.graph_output = synthetic_output
|
|
155
|
+
self.result.saw_finish_surface = True
|
|
156
|
+
yield build_synthetic_root_chain_end(graph_output=synthetic_output)
|
|
157
|
+
self._log_catch_error_diagnostic(exc)
|
|
158
|
+
raise
|
|
159
|
+
except Exception as exc:
|
|
160
|
+
self.result.final_error = exc
|
|
161
|
+
# CC catch 协议 §5.3.3:先记录日志 / 观测,再补 stream surface,最后 re-raise
|
|
162
|
+
self._log_catch_error(exc)
|
|
163
|
+
for repair in self._orphan_tool_result_repairs(closeout_kind="interrupted"):
|
|
164
|
+
yield repair
|
|
165
|
+
if not self.result.saw_error_visible_surface:
|
|
166
|
+
self.result.saw_error_visible_surface = True
|
|
167
|
+
yield build_synthetic_error_visible_event(
|
|
168
|
+
message=str(exc),
|
|
169
|
+
error_kind="graph_runtime_error",
|
|
170
|
+
)
|
|
171
|
+
if not self.result.saw_finish_surface:
|
|
172
|
+
terminal = build_loop_terminal(
|
|
173
|
+
reason=TERMINAL_REASON_GRAPH_RUNTIME_ERROR,
|
|
174
|
+
error_kind="graph_runtime_error",
|
|
175
|
+
step=(self.result.last_loop_snapshot or {}).get("step"),
|
|
176
|
+
turn_count=(self.result.last_loop_snapshot or {}).get("turn_count"),
|
|
177
|
+
provider_finish_reason=(self.result.last_loop_snapshot or {}).get("finish_reason"),
|
|
178
|
+
errors=[str(exc)],
|
|
179
|
+
)
|
|
180
|
+
self.result.terminal = terminal
|
|
181
|
+
synthetic_output = self._build_synthetic_graph_output(terminal)
|
|
182
|
+
self.result.graph_output = synthetic_output
|
|
183
|
+
self.result.saw_finish_surface = True
|
|
184
|
+
yield build_synthetic_root_chain_end(graph_output=synthetic_output)
|
|
185
|
+
self._log_catch_error_diagnostic(exc)
|
|
186
|
+
raise
|
|
187
|
+
|
|
188
|
+
def _should_reactive_continue_drain(
|
|
189
|
+
self,
|
|
190
|
+
*,
|
|
191
|
+
continues_used: int,
|
|
192
|
+
max_continues: int,
|
|
193
|
+
) -> bool:
|
|
194
|
+
if max_continues <= 0 or continues_used >= max_continues:
|
|
195
|
+
return False
|
|
196
|
+
if self.result.final_error is not None:
|
|
197
|
+
return False
|
|
198
|
+
if not self.result.saw_root_chain_end or not self.result.graph_completed:
|
|
199
|
+
return False
|
|
200
|
+
output = self.result.graph_output
|
|
201
|
+
if not isinstance(output, dict):
|
|
202
|
+
return False
|
|
203
|
+
resolution = output.get("resolution")
|
|
204
|
+
if not isinstance(resolution, dict) or resolution.get("kind") != "continue":
|
|
205
|
+
return False
|
|
206
|
+
transition = resolution.get("transition")
|
|
207
|
+
if not isinstance(transition, dict):
|
|
208
|
+
transition = output.get("transition")
|
|
209
|
+
if not isinstance(transition, dict):
|
|
210
|
+
return False
|
|
211
|
+
return transition.get("reason") == TRANSITION_REACTIVE_COMPACT_RETRY
|
|
212
|
+
|
|
213
|
+
def _build_continue_graph_input(
|
|
214
|
+
self,
|
|
215
|
+
prior_input: dict[str, Any],
|
|
216
|
+
graph_output: dict[str, Any],
|
|
217
|
+
) -> dict[str, Any]:
|
|
218
|
+
continued = dict(prior_input)
|
|
219
|
+
messages = graph_output.get("messages")
|
|
220
|
+
if isinstance(messages, list):
|
|
221
|
+
continued["messages"] = list(messages)
|
|
222
|
+
for key in (
|
|
223
|
+
"has_attempted_reactive_compact",
|
|
224
|
+
"_session_id",
|
|
225
|
+
"_run_id",
|
|
226
|
+
"_trace_collector",
|
|
227
|
+
):
|
|
228
|
+
if key in graph_output:
|
|
229
|
+
continued[key] = graph_output[key]
|
|
230
|
+
return continued
|
|
231
|
+
|
|
232
|
+
def _reset_for_continue_drain(self) -> None:
|
|
233
|
+
self._pending_tool_calls.clear()
|
|
234
|
+
self.result.saw_root_chain_end = False
|
|
235
|
+
self.result.saw_finish_surface = False
|
|
236
|
+
self.result.graph_completed = False
|
|
237
|
+
self.result.final_error = None
|
|
238
|
+
|
|
146
239
|
def _observe_event(self, event: dict[str, Any]) -> None:
|
|
147
240
|
event_name = str(event.get("event") or "")
|
|
148
241
|
name = str(event.get("name") or "")
|
|
@@ -29,6 +29,12 @@ from collections.abc import Callable
|
|
|
29
29
|
from dataclasses import dataclass, field
|
|
30
30
|
from typing import Any, Literal
|
|
31
31
|
|
|
32
|
+
from langchain_agentx.observability.events.finish_outcome import (
|
|
33
|
+
RecoveryPolicy,
|
|
34
|
+
RecoveryPolicyMode,
|
|
35
|
+
coerce_recovery_policy,
|
|
36
|
+
)
|
|
37
|
+
|
|
32
38
|
from langchain_agentx.loop.context.query_source import LoopCompilationContext
|
|
33
39
|
from langchain_agentx.loop.exit.query_source import QuerySource
|
|
34
40
|
from langchain_agentx.loop.runtime.subagent_execution_paths import (
|
|
@@ -107,6 +113,9 @@ class SubagentContext:
|
|
|
107
113
|
# Thinking 控制字段(request-side 生成意图,与 UI 展示意图分离)
|
|
108
114
|
thinking_generation_enabled: bool = True # 子代理 request-side thinking 生成开关
|
|
109
115
|
|
|
116
|
+
# R1 恢复策略(S4→S2;None 在 runner 层按 interactive 解析)
|
|
117
|
+
recovery: RecoveryPolicy | None = None
|
|
118
|
+
|
|
110
119
|
|
|
111
120
|
@dataclass
|
|
112
121
|
class SubagentResult:
|
|
@@ -150,6 +159,7 @@ def create_subagent_context(
|
|
|
150
159
|
thinking_generation_enabled: bool = True,
|
|
151
160
|
query_source: str | None = None,
|
|
152
161
|
is_builtin_subagent: bool = False,
|
|
162
|
+
recovery: RecoveryPolicy | RecoveryPolicyMode | None = None,
|
|
153
163
|
) -> SubagentContext:
|
|
154
164
|
"""工厂函数:从父上下文创建子 Agent 上下文
|
|
155
165
|
|
|
@@ -285,6 +295,14 @@ def create_subagent_context(
|
|
|
285
295
|
effective_cwd=effective_cwd,
|
|
286
296
|
)
|
|
287
297
|
|
|
298
|
+
resolved_recovery: RecoveryPolicy | None = None
|
|
299
|
+
if recovery is not None:
|
|
300
|
+
resolved_recovery = coerce_recovery_policy(recovery)
|
|
301
|
+
else:
|
|
302
|
+
parent_recovery = getattr(parent_ctx, "recovery", None)
|
|
303
|
+
if isinstance(parent_recovery, (RecoveryPolicy, str)):
|
|
304
|
+
resolved_recovery = coerce_recovery_policy(parent_recovery)
|
|
305
|
+
|
|
288
306
|
return SubagentContext(
|
|
289
307
|
agent_id=agent_id,
|
|
290
308
|
initial_messages=initial_messages,
|
|
@@ -317,6 +335,7 @@ def create_subagent_context(
|
|
|
317
335
|
available_tools=list(available_tools or []),
|
|
318
336
|
thinking_generation_enabled=thinking_generation_enabled,
|
|
319
337
|
query_source=resolved_query_source,
|
|
338
|
+
recovery=resolved_recovery,
|
|
320
339
|
)
|
|
321
340
|
|
|
322
341
|
|
|
@@ -55,6 +55,11 @@ from langchain_agentx.loop.message_contract import (
|
|
|
55
55
|
is_synthetic_transcript_tool_id,
|
|
56
56
|
to_record,
|
|
57
57
|
)
|
|
58
|
+
from langchain_agentx.observability.events.finish_outcome import (
|
|
59
|
+
RecoveryPolicy,
|
|
60
|
+
RecoveryPolicyMode,
|
|
61
|
+
coerce_recovery_policy,
|
|
62
|
+
)
|
|
58
63
|
from langchain_agentx.observability.events.tool_call_id_resolver import ToolCallIdResolver
|
|
59
64
|
from langchain_agentx.observability.events.tool_outcome_resolver import (
|
|
60
65
|
error_message_for_outcome,
|
|
@@ -504,6 +509,7 @@ async def _collect_subagent_events(
|
|
|
504
509
|
run_tags: list[str],
|
|
505
510
|
transcript: SubagentTranscriptManager,
|
|
506
511
|
trace: SubagentExecutionTrace,
|
|
512
|
+
recovery: RecoveryPolicy | RecoveryPolicyMode | None = None,
|
|
507
513
|
) -> _EventCollectionResult:
|
|
508
514
|
collected_assistant_messages: list[Any] = []
|
|
509
515
|
terminal_reason = "completed"
|
|
@@ -533,12 +539,16 @@ async def _collect_subagent_events(
|
|
|
533
539
|
config["configurable"] = dict(configurable)
|
|
534
540
|
|
|
535
541
|
try:
|
|
542
|
+
recovery_policy = coerce_recovery_policy(
|
|
543
|
+
recovery if recovery is not None else ctx.recovery
|
|
544
|
+
)
|
|
536
545
|
driver = LoopStreamDriver()
|
|
537
546
|
async for event in driver.run(
|
|
538
547
|
graph,
|
|
539
548
|
graph_input,
|
|
540
549
|
config=config,
|
|
541
550
|
version="v2",
|
|
551
|
+
max_reactive_continue_per_drain=recovery_policy.reactive_continue_budget(),
|
|
542
552
|
):
|
|
543
553
|
event_name = event.get("event", "")
|
|
544
554
|
data = event.get("data") or {}
|
|
@@ -667,6 +677,7 @@ async def run_subagent(
|
|
|
667
677
|
loader: Any,
|
|
668
678
|
model: Any | None = None,
|
|
669
679
|
allowed_tool_names: set[str] | None = None,
|
|
680
|
+
recovery: RecoveryPolicy | RecoveryPolicyMode | None = None,
|
|
670
681
|
) -> SubagentResult:
|
|
671
682
|
"""运行子 Agent(plain/fork 同步路径)。
|
|
672
683
|
|
|
@@ -715,6 +726,7 @@ async def run_subagent(
|
|
|
715
726
|
run_tags=run_tags,
|
|
716
727
|
transcript=transcript,
|
|
717
728
|
trace=trace,
|
|
729
|
+
recovery=recovery,
|
|
718
730
|
)
|
|
719
731
|
return await _build_subagent_result(
|
|
720
732
|
ctx=ctx,
|
|
@@ -12,7 +12,9 @@ from .events import (
|
|
|
12
12
|
LangGraphToLangchainAgentEventAdapter,
|
|
13
13
|
RecoveryAction,
|
|
14
14
|
RecoveryActionResolver,
|
|
15
|
+
RecoveryPolicy,
|
|
15
16
|
RecoveryPolicyMode,
|
|
17
|
+
coerce_recovery_policy,
|
|
16
18
|
create_langgraph_event_adapter,
|
|
17
19
|
enrich_run_result,
|
|
18
20
|
)
|
|
@@ -69,7 +71,9 @@ __all__ = [
|
|
|
69
71
|
"create_langgraph_event_adapter",
|
|
70
72
|
"RecoveryAction",
|
|
71
73
|
"RecoveryActionResolver",
|
|
74
|
+
"RecoveryPolicy",
|
|
72
75
|
"RecoveryPolicyMode",
|
|
76
|
+
"coerce_recovery_policy",
|
|
73
77
|
"enrich_run_result",
|
|
74
78
|
"REASON_CODES",
|
|
75
79
|
"export_call_chain",
|
|
@@ -13,7 +13,9 @@ from .finish_outcome import (
|
|
|
13
13
|
FinishOutcomeParser,
|
|
14
14
|
RecoveryAction,
|
|
15
15
|
RecoveryActionResolver,
|
|
16
|
+
RecoveryPolicy,
|
|
16
17
|
RecoveryPolicyMode,
|
|
18
|
+
coerce_recovery_policy,
|
|
17
19
|
enrich_run_result,
|
|
18
20
|
)
|
|
19
21
|
from .factory import (
|
|
@@ -32,7 +34,9 @@ __all__ = [
|
|
|
32
34
|
"FinishOutcomeParser",
|
|
33
35
|
"RecoveryAction",
|
|
34
36
|
"RecoveryActionResolver",
|
|
37
|
+
"RecoveryPolicy",
|
|
35
38
|
"RecoveryPolicyMode",
|
|
39
|
+
"coerce_recovery_policy",
|
|
36
40
|
"enrich_run_result",
|
|
37
41
|
"ILangGraphEventAdapter",
|
|
38
42
|
"create_langgraph_event_adapter",
|
|
@@ -54,6 +54,34 @@ FINISH_OUTCOME_KEYS = frozenset(
|
|
|
54
54
|
RecoveryPolicyMode = Literal["interactive", "auto"]
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class RecoveryPolicy:
|
|
59
|
+
"""Stream 恢复策略配置面(S4 消费方 → S2 LoopStreamDriver 注入)。
|
|
60
|
+
|
|
61
|
+
非 Session 编排 API:仅控制 R1 reactive continue 预算与 finish 消费模式。
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
mode: RecoveryPolicyMode = "interactive"
|
|
65
|
+
max_reactive_continue_per_drain: int = 1
|
|
66
|
+
|
|
67
|
+
def reactive_continue_budget(self) -> int:
|
|
68
|
+
"""返回 LoopStreamDriver R1 continue 预算(interactive 禁用自动 continue)。"""
|
|
69
|
+
if self.mode == "interactive":
|
|
70
|
+
return 0
|
|
71
|
+
return self.max_reactive_continue_per_drain
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def coerce_recovery_policy(
|
|
75
|
+
recovery: RecoveryPolicy | RecoveryPolicyMode | None = None,
|
|
76
|
+
) -> RecoveryPolicy:
|
|
77
|
+
"""将消费方传入的 recovery 规范化为 RecoveryPolicy。"""
|
|
78
|
+
if recovery is None:
|
|
79
|
+
return RecoveryPolicy()
|
|
80
|
+
if isinstance(recovery, str):
|
|
81
|
+
return RecoveryPolicy(mode=recovery)
|
|
82
|
+
return recovery
|
|
83
|
+
|
|
84
|
+
|
|
57
85
|
class RecoveryAction(str, Enum):
|
|
58
86
|
"""消费方建议动作(不含具体 UI 文案)。"""
|
|
59
87
|
|
|
@@ -248,6 +276,8 @@ __all__ = [
|
|
|
248
276
|
"FinishOutcomeParser",
|
|
249
277
|
"RecoveryAction",
|
|
250
278
|
"RecoveryActionResolver",
|
|
279
|
+
"RecoveryPolicy",
|
|
251
280
|
"RecoveryPolicyMode",
|
|
281
|
+
"coerce_recovery_policy",
|
|
252
282
|
"enrich_run_result",
|
|
253
283
|
]
|
|
@@ -20,10 +20,8 @@ from .plan_mode import (
|
|
|
20
20
|
)
|
|
21
21
|
from .protocol import LoopContainer
|
|
22
22
|
from .recovery import (
|
|
23
|
-
SessionRecoveryRunResult,
|
|
24
23
|
finish_outcome_from_graph_output,
|
|
25
|
-
|
|
26
|
-
run_with_recovery_loop,
|
|
24
|
+
finish_outcome_from_stream_driver_result,
|
|
27
25
|
)
|
|
28
26
|
|
|
29
27
|
__all__ = [
|
|
@@ -35,15 +33,13 @@ __all__ = [
|
|
|
35
33
|
"DEFAULT_CLEANUP_ACTION_TIMEOUT",
|
|
36
34
|
"ConversationSession",
|
|
37
35
|
"LoopContainer",
|
|
38
|
-
"SessionRecoveryRunResult",
|
|
39
36
|
"create_agent_session",
|
|
40
37
|
"create_conversation_session",
|
|
41
38
|
"build_plan_mode_transition_event",
|
|
42
39
|
"enter_plan_mode",
|
|
43
40
|
"exit_plan_mode",
|
|
44
41
|
"finish_outcome_from_graph_output",
|
|
42
|
+
"finish_outcome_from_stream_driver_result",
|
|
45
43
|
"load_session_messages",
|
|
46
|
-
"parse_result_finish_outcome",
|
|
47
44
|
"PlanModeTransition",
|
|
48
|
-
"run_with_recovery_loop",
|
|
49
45
|
]
|
|
@@ -38,13 +38,16 @@ from ..command.builtin import (
|
|
|
38
38
|
make_memory_command,
|
|
39
39
|
make_reload_plugins_command,
|
|
40
40
|
)
|
|
41
|
-
from ..observability.events.finish_outcome import
|
|
41
|
+
from ..observability.events.finish_outcome import (
|
|
42
|
+
RecoveryPolicy,
|
|
43
|
+
RecoveryPolicyMode,
|
|
44
|
+
coerce_recovery_policy,
|
|
45
|
+
enrich_run_result,
|
|
46
|
+
)
|
|
42
47
|
from ..loop.config import merge_run_config
|
|
43
48
|
from .recovery import (
|
|
44
|
-
SessionRecoveryRunResult,
|
|
45
49
|
finish_outcome_from_graph_output,
|
|
46
50
|
finish_outcome_from_stream_driver_result,
|
|
47
|
-
run_with_recovery_loop,
|
|
48
51
|
)
|
|
49
52
|
from ..loop.config.loop_runtime_overlay import LoopRuntimeOverlay
|
|
50
53
|
from ..loop.context.compaction_service import default_loop_context_compaction
|
|
@@ -226,37 +229,6 @@ class AgentSession:
|
|
|
226
229
|
finally:
|
|
227
230
|
reset_command_tool_scope(store)
|
|
228
231
|
|
|
229
|
-
async def resume_after_compact(self) -> dict[str, Any]:
|
|
230
|
-
"""compact 后重跑当前轮:不再 append 新的 HumanMessage。
|
|
231
|
-
|
|
232
|
-
适用于 CLI `/compact` 或 headless auto 恢复的第二跳;与 ``run_loop(same_query)``
|
|
233
|
-
不同,不会把同一用户问题重复写入 messages / transcript。
|
|
234
|
-
"""
|
|
235
|
-
return await self._run_loop_turn(user_input=None)
|
|
236
|
-
|
|
237
|
-
async def run_with_recovery(
|
|
238
|
-
self,
|
|
239
|
-
user_input: str,
|
|
240
|
-
*,
|
|
241
|
-
mode: RecoveryPolicyMode = "auto",
|
|
242
|
-
max_outer_retries: int = 1,
|
|
243
|
-
progress_callback: Any = None,
|
|
244
|
-
) -> SessionRecoveryRunResult:
|
|
245
|
-
"""B1 外层自动恢复:recoverable 时 compact + resume,受 ``max_outer_retries`` 约束。
|
|
246
|
-
|
|
247
|
-
- ``mode="auto"``:可恢复且未 exhausted 时自动 compact 并 resume。
|
|
248
|
-
- ``mode="interactive"``:只跑一轮,把 ``RecoveryAction`` 留给 CLI 决策。
|
|
249
|
-
"""
|
|
250
|
-
return await run_with_recovery_loop(
|
|
251
|
-
user_input=user_input,
|
|
252
|
-
mode=mode,
|
|
253
|
-
max_outer_retries=max_outer_retries,
|
|
254
|
-
run_new_turn=lambda inp: self._run_loop_turn(user_input=inp),
|
|
255
|
-
resume_current_turn=lambda: self._run_loop_turn(user_input=None),
|
|
256
|
-
compact=self.compact_context,
|
|
257
|
-
progress_callback=progress_callback,
|
|
258
|
-
)
|
|
259
|
-
|
|
260
232
|
async def _run_loop_turn(self, *, user_input: str | None) -> dict[str, Any]:
|
|
261
233
|
if self._graph is None:
|
|
262
234
|
raise RuntimeError("AgentSession graph is not initialized")
|
|
@@ -302,6 +274,7 @@ class AgentSession:
|
|
|
302
274
|
user_input: str,
|
|
303
275
|
*,
|
|
304
276
|
config: dict[str, Any] | None = None,
|
|
277
|
+
recovery: RecoveryPolicy | RecoveryPolicyMode | None = None,
|
|
305
278
|
) -> AsyncIterator[dict[str, Any]]:
|
|
306
279
|
"""
|
|
307
280
|
流式执行一轮 loop,复用 AgentSession 的消息上下文。
|
|
@@ -322,6 +295,7 @@ class AgentSession:
|
|
|
322
295
|
output_messages: list[Any] | None = None
|
|
323
296
|
graph_output: dict[str, Any] | None = None
|
|
324
297
|
run_config = merge_run_config(self._graph, config)
|
|
298
|
+
recovery_policy = coerce_recovery_policy(recovery)
|
|
325
299
|
driver = LoopStreamDriver()
|
|
326
300
|
try:
|
|
327
301
|
try:
|
|
@@ -333,6 +307,7 @@ class AgentSession:
|
|
|
333
307
|
},
|
|
334
308
|
config=run_config,
|
|
335
309
|
version="v2",
|
|
310
|
+
max_reactive_continue_per_drain=recovery_policy.reactive_continue_budget(),
|
|
336
311
|
):
|
|
337
312
|
if event.get("event") == "on_chain_end" and event.get("name") == "LangGraph":
|
|
338
313
|
data = event.get("data", {}) or {}
|
|
@@ -594,4 +569,4 @@ class AgentSession:
|
|
|
594
569
|
self._runtime_command_registry.register(make_reload_plugins_command())
|
|
595
570
|
|
|
596
571
|
|
|
597
|
-
__all__ = ["AgentSession"
|
|
572
|
+
__all__ = ["AgentSession"]
|