langchain-agentx-python 2.0.1__py3-none-any.whl → 2.1.1__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 +20 -1
- langchain_agentx/loop/subagent/orchestrator.py +3 -2
- langchain_agentx/loop/subagent/runner.py +13 -1
- langchain_agentx/observability/__init__.py +4 -0
- langchain_agentx/observability/events/__init__.py +4 -0
- langchain_agentx/observability/events/agent_event_projector.py +29 -1
- langchain_agentx/observability/events/finish_outcome.py +30 -0
- langchain_agentx/observability/events/subagent_window_text_guard.py +45 -6
- 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/agent/tool.py +2 -2
- langchain_agentx/tools/glob/tool.py +13 -2
- 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.1.dist-info → langchain_agentx_python-2.1.1.dist-info}/METADATA +1 -1
- {langchain_agentx_python-2.0.1.dist-info → langchain_agentx_python-2.1.1.dist-info}/RECORD +28 -27
- {langchain_agentx_python-2.0.1.dist-info → langchain_agentx_python-2.1.1.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-2.0.1.dist-info → langchain_agentx_python-2.1.1.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-2.0.1.dist-info → langchain_agentx_python-2.1.1.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 (
|
|
@@ -85,7 +91,7 @@ class SubagentContext:
|
|
|
85
91
|
parent_tool_call_id: str | None = None # 触发本子 Agent 的 tool_call_id
|
|
86
92
|
parent_conversation_session_id: str | None = None
|
|
87
93
|
enable_trace: bool = True
|
|
88
|
-
runnable_config: Any = None # 父
|
|
94
|
+
runnable_config: Any = None # 父 runnable_config:仅供 tool_progress custom event 回调链;非子 raw 合并
|
|
89
95
|
isolation: str = "none"
|
|
90
96
|
worktree_dir: str | None = None
|
|
91
97
|
workspace_root: str = ""
|
|
@@ -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
|
|
|
@@ -307,7 +307,8 @@ class SubagentOrchestrator:
|
|
|
307
307
|
) -> "AgentToolOutput":
|
|
308
308
|
"""异步执行子代理。
|
|
309
309
|
|
|
310
|
-
子 graph
|
|
310
|
+
子 graph 在独立 LoopStreamDriver 消费(Phase-4);progress 经 custom event 冒泡,
|
|
311
|
+
裸子 on_tool_* 不并进父 astream。
|
|
311
312
|
"""
|
|
312
313
|
from langchain_agentx.tools.agent.models import AgentToolOutput
|
|
313
314
|
|
|
@@ -595,7 +596,7 @@ class SubagentOrchestrator:
|
|
|
595
596
|
) -> Any:
|
|
596
597
|
"""异步启动子 agent。
|
|
597
598
|
|
|
598
|
-
子 graph
|
|
599
|
+
子 graph Phase-4 独立 Driver 消费;progress 经 custom event 冒泡到父流。
|
|
599
600
|
"""
|
|
600
601
|
owner_run_ctx = get_run_context()
|
|
601
602
|
handle = self._register_background_agent(
|
|
@@ -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,
|
|
@@ -221,7 +226,7 @@ async def _emit_progress(
|
|
|
221
226
|
"""发射子 Agent teaser lane 到父 graph(CC 对齐:仅 tool_use / tool_result)。
|
|
222
227
|
|
|
223
228
|
同步 dispatch 优先:并行线程池 / _run_coroutine_sync 子线程内 adispatch 无法触达父回调链。
|
|
224
|
-
父
|
|
229
|
+
父 config 经 ctx.runnable_config 仅服务 custom event 派发(Phase-4 不合并父 callbacks)。
|
|
225
230
|
"""
|
|
226
231
|
if event.type not in ("tool_use", "tool_result"):
|
|
227
232
|
return
|
|
@@ -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",
|
|
@@ -219,6 +219,10 @@ class AgentEventProjector:
|
|
|
219
219
|
self.enable_subagent_events: bool = config.get("enable_subagent_events", True)
|
|
220
220
|
self.synthesize_tool_call: bool = config.get("synthesize_tool_call", True)
|
|
221
221
|
self.enable_tool_input_start: bool = config.get("enable_tool_input_start", False)
|
|
222
|
+
# Doc-34 F5:活跃子窗内抑制非 Agent 主台 tool-*(默认 on;关则恢复旧投影)
|
|
223
|
+
self.suppress_subagent_child_main_tier_tools: bool = bool(
|
|
224
|
+
config.get("suppress_subagent_child_main_tier_tools", True)
|
|
225
|
+
)
|
|
222
226
|
self.filter_unparented_subgraph_events: bool = config.get(
|
|
223
227
|
"filter_unparented_subgraph_events", False
|
|
224
228
|
)
|
|
@@ -528,9 +532,17 @@ class AgentEventProjector:
|
|
|
528
532
|
self._sync_accumulated_answer_mirror()
|
|
529
533
|
|
|
530
534
|
def _should_suppress_main_tier_text_stream(self) -> bool:
|
|
531
|
-
"""
|
|
535
|
+
"""Doc-33:子 Agent 窗口内不 emit 父 main-tier TEXT_* / REASONING_*。"""
|
|
532
536
|
return self.state.subagent_window_text_guard.should_suppress_main_tier_stream
|
|
533
537
|
|
|
538
|
+
def _should_suppress_main_tier_child_tool_events(self, tool_name: str) -> bool:
|
|
539
|
+
"""Doc-34 F2/F3/F5:flag on 且非 Agent 且 open_runs>0 → 抑制主台 tool-*。"""
|
|
540
|
+
if not self.suppress_subagent_child_main_tier_tools:
|
|
541
|
+
return False
|
|
542
|
+
if self._is_agent_tool(tool_name):
|
|
543
|
+
return False
|
|
544
|
+
return self.state.subagent_window_text_guard.should_suppress_main_tier_tool_events
|
|
545
|
+
|
|
534
546
|
def _on_subagent_start_boundary(self) -> None:
|
|
535
547
|
self.state.subagent_window_text_guard.on_subagent_start()
|
|
536
548
|
|
|
@@ -888,6 +900,7 @@ class AgentEventProjector:
|
|
|
888
900
|
messages=messages if isinstance(messages, list) else None,
|
|
889
901
|
)
|
|
890
902
|
self._closeout_saw_finish_emitted = True
|
|
903
|
+
self.state.subagent_window_text_guard.warn_if_unbalanced_at_finish(logger)
|
|
891
904
|
yield self._event(
|
|
892
905
|
event_type=LangchainAgentEventType.FINISH,
|
|
893
906
|
data=finish_data,
|
|
@@ -1113,6 +1126,16 @@ class AgentEventProjector:
|
|
|
1113
1126
|
|
|
1114
1127
|
self.state.current_tool = name
|
|
1115
1128
|
|
|
1129
|
+
# Doc-34 F2:非 Agent 且窗开 → 在任何 TOOL_INPUT_* / TOOL_CALL 之前 return
|
|
1130
|
+
if self._should_suppress_main_tier_child_tool_events(name):
|
|
1131
|
+
self.state.subagent_window_text_guard.note_suppressed_child_tool_start()
|
|
1132
|
+
logger.debug(
|
|
1133
|
+
"Doc-34: suppress main-tier tool_start name=%s open_runs=%s",
|
|
1134
|
+
name,
|
|
1135
|
+
self.state.subagent_window_text_guard.open_runs,
|
|
1136
|
+
)
|
|
1137
|
+
return
|
|
1138
|
+
|
|
1116
1139
|
if self.enable_tool_input_start:
|
|
1117
1140
|
yield self._event(
|
|
1118
1141
|
event_type=LangchainAgentEventType.TOOL_INPUT_START,
|
|
@@ -1192,6 +1215,11 @@ class AgentEventProjector:
|
|
|
1192
1215
|
data = event.get("data", {}) or {}
|
|
1193
1216
|
tool_call_id = ToolCallIdResolver.resolve_from_langgraph_tool_event(event)
|
|
1194
1217
|
|
|
1218
|
+
# Doc-34 F2:窗内非 Agent 对称抑制 RESULT/ERROR(不碰 Agent end → on_subagent_end)
|
|
1219
|
+
if self._should_suppress_main_tier_child_tool_events(name):
|
|
1220
|
+
self.state.current_tool = None
|
|
1221
|
+
return
|
|
1222
|
+
|
|
1195
1223
|
output = data.get("output")
|
|
1196
1224
|
|
|
1197
1225
|
event_type, event_data = self._classify_tool_end_event(
|
|
@@ -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
|
]
|