langchain-agentx-python 2.1.9__py3-none-any.whl → 2.2.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/__init__.py +8 -1
- langchain_agentx/loop/graph/builtin_loop_control.py +21 -5
- langchain_agentx/loop/graph/runtime_tools_node.py +11 -3
- langchain_agentx/loop/loop_abort.py +34 -14
- langchain_agentx/loop/stream/loop_driver.py +68 -0
- langchain_agentx/loop/subagent/runner.py +9 -2
- langchain_agentx/workflow/event_adapter/scope_resolver.py +53 -32
- langchain_agentx/workflow/event_adapter/session_router.py +8 -1
- {langchain_agentx_python-2.1.9.dist-info → langchain_agentx_python-2.2.1.dist-info}/METADATA +1 -1
- {langchain_agentx_python-2.1.9.dist-info → langchain_agentx_python-2.2.1.dist-info}/RECORD +14 -14
- {langchain_agentx_python-2.1.9.dist-info → langchain_agentx_python-2.2.1.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-2.1.9.dist-info → langchain_agentx_python-2.2.1.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-2.1.9.dist-info → langchain_agentx_python-2.2.1.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -35,13 +35,20 @@ for chunk in agent.stream({"messages": [{"role": "user", "content": "Hello"}]}):
|
|
|
35
35
|
from .graph.factory import (
|
|
36
36
|
create_loop_agent,
|
|
37
37
|
)
|
|
38
|
-
from .loop_abort import
|
|
38
|
+
from .loop_abort import (
|
|
39
|
+
LoopAbortSignal,
|
|
40
|
+
abort_signal_for_phase,
|
|
41
|
+
is_cancel_requested,
|
|
42
|
+
raise_if_cancel_requested,
|
|
43
|
+
)
|
|
39
44
|
from .config import build_agent_config
|
|
40
45
|
|
|
41
46
|
__all__ = [
|
|
42
47
|
# 主要接口
|
|
43
48
|
"create_loop_agent",
|
|
44
49
|
"LoopAbortSignal",
|
|
50
|
+
"abort_signal_for_phase",
|
|
51
|
+
"is_cancel_requested",
|
|
45
52
|
"raise_if_cancel_requested",
|
|
46
53
|
# 配置助手
|
|
47
54
|
"build_agent_config",
|
|
@@ -23,7 +23,12 @@ from ..exit.reason_codes import (
|
|
|
23
23
|
TERMINAL_REASON_CLASSIFIER_DENIAL_LIMIT,
|
|
24
24
|
TERMINAL_REASON_HOOK_STOPPED,
|
|
25
25
|
)
|
|
26
|
-
from ..loop_abort import
|
|
26
|
+
from ..loop_abort import (
|
|
27
|
+
LoopAbortSignal,
|
|
28
|
+
abort_signal_for_phase,
|
|
29
|
+
is_cancel_requested,
|
|
30
|
+
raise_if_cancel_requested,
|
|
31
|
+
)
|
|
27
32
|
from ..model.orphan_tool_results import synthetic_tool_messages_for_orphan_tool_calls
|
|
28
33
|
from ...tool_runtime.classifier_denial_limit import ClassifierDenialLimitAbort
|
|
29
34
|
from ...tool_runtime.permission_context import clear_auto_approved_window
|
|
@@ -72,7 +77,9 @@ class _LoopBuiltinModelControl:
|
|
|
72
77
|
raise_if_cancel_requested(phase="model")
|
|
73
78
|
return handler(request)
|
|
74
79
|
except asyncio.CancelledError:
|
|
75
|
-
raise_if_cancel_requested
|
|
80
|
+
# D-A′-1:就地 abort;禁止 raise_if_cancel_requested 逃逸
|
|
81
|
+
if is_cancel_requested():
|
|
82
|
+
return _model_abort_response(abort_signal_for_phase("model"))
|
|
76
83
|
raise
|
|
77
84
|
except LoopAbortSignal as sig:
|
|
78
85
|
if sig.terminal_reason not in (
|
|
@@ -88,7 +95,8 @@ class _LoopBuiltinModelControl:
|
|
|
88
95
|
raise_if_cancel_requested(phase="model")
|
|
89
96
|
return await handler(request)
|
|
90
97
|
except asyncio.CancelledError:
|
|
91
|
-
|
|
98
|
+
if is_cancel_requested():
|
|
99
|
+
return _model_abort_response(abort_signal_for_phase("model"))
|
|
92
100
|
raise
|
|
93
101
|
except LoopAbortSignal as sig:
|
|
94
102
|
if sig.terminal_reason not in (
|
|
@@ -184,7 +192,11 @@ def builtin_sync_tool_wrapper(request: Any, handler: Any) -> Any:
|
|
|
184
192
|
result = handler(_with_injected_agent_tool_model(request))
|
|
185
193
|
return apply_tool_message_status_to_result(result)
|
|
186
194
|
except asyncio.CancelledError:
|
|
187
|
-
|
|
195
|
+
if is_cancel_requested():
|
|
196
|
+
return _tool_abort_command(
|
|
197
|
+
request,
|
|
198
|
+
terminal_reason=TERMINAL_REASON_ABORTED_TOOLS,
|
|
199
|
+
)
|
|
188
200
|
raise
|
|
189
201
|
except ClassifierDenialLimitAbort as exc:
|
|
190
202
|
return _tool_abort_command(
|
|
@@ -207,7 +219,11 @@ async def builtin_async_tool_wrapper(request: Any, handler: Any) -> Any:
|
|
|
207
219
|
result = await handler(_with_injected_agent_tool_model(request))
|
|
208
220
|
return apply_tool_message_status_to_result(result)
|
|
209
221
|
except asyncio.CancelledError:
|
|
210
|
-
|
|
222
|
+
if is_cancel_requested():
|
|
223
|
+
return _tool_abort_command(
|
|
224
|
+
request,
|
|
225
|
+
terminal_reason=TERMINAL_REASON_ABORTED_TOOLS,
|
|
226
|
+
)
|
|
211
227
|
raise
|
|
212
228
|
except ClassifierDenialLimitAbort as exc:
|
|
213
229
|
return _tool_abort_command(
|
|
@@ -37,7 +37,7 @@ from ..exit.reason_codes import (
|
|
|
37
37
|
TERMINAL_REASON_ABORTED_TOOLS,
|
|
38
38
|
TERMINAL_REASON_CLASSIFIER_DENIAL_LIMIT,
|
|
39
39
|
)
|
|
40
|
-
from ..loop_abort import LoopAbortSignal, raise_if_cancel_requested
|
|
40
|
+
from ..loop_abort import LoopAbortSignal, is_cancel_requested, raise_if_cancel_requested
|
|
41
41
|
from .builtin_loop_control import tools_batch_abort_command
|
|
42
42
|
|
|
43
43
|
|
|
@@ -112,7 +112,11 @@ class RuntimeToolsNode(Runnable):
|
|
|
112
112
|
tool_messages = self._message_adapter.records_to_graph_messages(records)
|
|
113
113
|
return {"messages": tool_messages}
|
|
114
114
|
except asyncio.CancelledError:
|
|
115
|
-
|
|
115
|
+
if is_cancel_requested():
|
|
116
|
+
return tools_batch_abort_command(
|
|
117
|
+
tool_calls,
|
|
118
|
+
terminal_reason=TERMINAL_REASON_ABORTED_TOOLS,
|
|
119
|
+
)
|
|
116
120
|
raise
|
|
117
121
|
except ClassifierDenialLimitAbort as exc:
|
|
118
122
|
return tools_batch_abort_command(
|
|
@@ -147,7 +151,11 @@ class RuntimeToolsNode(Runnable):
|
|
|
147
151
|
tool_messages = self._message_adapter.records_to_graph_messages(records)
|
|
148
152
|
return {"messages": tool_messages}
|
|
149
153
|
except asyncio.CancelledError:
|
|
150
|
-
|
|
154
|
+
if is_cancel_requested():
|
|
155
|
+
return tools_batch_abort_command(
|
|
156
|
+
tool_calls,
|
|
157
|
+
terminal_reason=TERMINAL_REASON_ABORTED_TOOLS,
|
|
158
|
+
)
|
|
151
159
|
raise
|
|
152
160
|
except ClassifierDenialLimitAbort as exc:
|
|
153
161
|
return tools_batch_abort_command(
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
P1(Doc-35 D-ABORT-1):``raise_if_cancel_requested`` 读 ``RunContext.cancel_requested``,
|
|
4
4
|
真则抛既有 ``LoopAbortSignal``(单轨);禁止平行 Abort 异常族。
|
|
5
|
+
|
|
6
|
+
层 A′(2026-07-21):``except CancelledError`` 路径禁止二次 ``raise_if_cancel_requested``
|
|
7
|
+
逃逸;用 ``is_cancel_requested`` + 就地 abort 响应(见 ``abort_signal_for_phase``)。
|
|
5
8
|
"""
|
|
6
9
|
|
|
7
10
|
from __future__ import annotations
|
|
@@ -41,27 +44,44 @@ class LoopAbortSignal(Exception):
|
|
|
41
44
|
super().__init__(detail or terminal_reason)
|
|
42
45
|
|
|
43
46
|
|
|
47
|
+
def is_cancel_requested() -> bool:
|
|
48
|
+
"""当前 RunContext 是否已请求取消(无 context 则为 False)。"""
|
|
49
|
+
from .run_context import get_run_context
|
|
50
|
+
|
|
51
|
+
ctx = get_run_context()
|
|
52
|
+
return ctx is not None and bool(ctx.cancel_requested)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def abort_signal_for_phase(
|
|
56
|
+
phase: CancelAbortPhase,
|
|
57
|
+
*,
|
|
58
|
+
detail: str = "cancel_requested",
|
|
59
|
+
) -> LoopAbortSignal:
|
|
60
|
+
"""构造相位对应的 ``LoopAbortSignal``(不抛出;供 CancelledError 就地收口)。"""
|
|
61
|
+
if phase == "tools":
|
|
62
|
+
return LoopAbortSignal(TERMINAL_REASON_ABORTED_TOOLS, detail=detail)
|
|
63
|
+
return LoopAbortSignal(TERMINAL_REASON_ABORTED_STREAMING, detail=detail)
|
|
64
|
+
|
|
65
|
+
|
|
44
66
|
def raise_if_cancel_requested(*, phase: CancelAbortPhase) -> None:
|
|
45
67
|
"""若当前 ``RunContext.cancel_requested`` 为真,抛既有 ``LoopAbortSignal``。
|
|
46
68
|
|
|
47
69
|
对齐 CC ``query.ts`` 读 ``signal.aborted``;SDK 原语为 bool(D-SEM-1),
|
|
48
70
|
退出语义仅走 ``LoopAbortSignal`` → ``builtin_loop_control``(D-ABORT-1)。
|
|
49
71
|
无 RunContext 时 no-op。
|
|
50
|
-
"""
|
|
51
|
-
from .run_context import get_run_context
|
|
52
72
|
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
**禁止**在 ``except CancelledError`` 内调用本函数再让异常逃逸;该路径应
|
|
74
|
+
``is_cancel_requested`` + 就地 abort 响应(D-A′-1)。
|
|
75
|
+
"""
|
|
76
|
+
if not is_cancel_requested():
|
|
55
77
|
return
|
|
56
|
-
|
|
57
|
-
raise LoopAbortSignal(
|
|
58
|
-
TERMINAL_REASON_ABORTED_TOOLS,
|
|
59
|
-
detail="cancel_requested",
|
|
60
|
-
)
|
|
61
|
-
raise LoopAbortSignal(
|
|
62
|
-
TERMINAL_REASON_ABORTED_STREAMING,
|
|
63
|
-
detail="cancel_requested",
|
|
64
|
-
)
|
|
78
|
+
raise abort_signal_for_phase(phase)
|
|
65
79
|
|
|
66
80
|
|
|
67
|
-
__all__ = [
|
|
81
|
+
__all__ = [
|
|
82
|
+
"CancelAbortPhase",
|
|
83
|
+
"LoopAbortSignal",
|
|
84
|
+
"abort_signal_for_phase",
|
|
85
|
+
"is_cancel_requested",
|
|
86
|
+
"raise_if_cancel_requested",
|
|
87
|
+
]
|
|
@@ -21,8 +21,12 @@ from typing import Any, AsyncIterator, Literal
|
|
|
21
21
|
|
|
22
22
|
from langgraph.errors import GraphRecursionError
|
|
23
23
|
|
|
24
|
+
from langchain_agentx.loop.loop_abort import LoopAbortSignal, is_cancel_requested
|
|
24
25
|
from ..exit.reason_codes import (
|
|
26
|
+
TERMINAL_REASON_ABORTED_STREAMING,
|
|
27
|
+
TERMINAL_REASON_ABORTED_TOOLS,
|
|
25
28
|
TERMINAL_REASON_GRAPH_RUNTIME_ERROR,
|
|
29
|
+
TERMINAL_REASON_HOOK_STOPPED,
|
|
26
30
|
TERMINAL_REASON_RUNTIME_BUDGET_EXHAUSTED,
|
|
27
31
|
TRANSITION_REACTIVE_COMPACT_RETRY,
|
|
28
32
|
)
|
|
@@ -43,6 +47,14 @@ logger = logging.getLogger(__name__)
|
|
|
43
47
|
# CC `tengu_query_error` 的 SDK trace 等价 event_type
|
|
44
48
|
LOOP_STREAM_DRIVER_ERROR_EVENT = "loop.stream_driver.error"
|
|
45
49
|
|
|
50
|
+
_ABORT_TERMINAL_REASONS = frozenset(
|
|
51
|
+
{
|
|
52
|
+
TERMINAL_REASON_ABORTED_STREAMING,
|
|
53
|
+
TERMINAL_REASON_ABORTED_TOOLS,
|
|
54
|
+
TERMINAL_REASON_HOOK_STOPPED,
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
46
58
|
|
|
47
59
|
class LoopStreamDriver:
|
|
48
60
|
def __init__(self) -> None:
|
|
@@ -123,9 +135,65 @@ class LoopStreamDriver:
|
|
|
123
135
|
self._log_missing_root_chain_end_anomaly()
|
|
124
136
|
except asyncio.CancelledError as exc:
|
|
125
137
|
self.result.final_error = exc
|
|
138
|
+
# 用户 Stop 触发的 Task.cancel:合成 abort closeout(D-A′-3 / I7 区分裸 cancel)
|
|
139
|
+
if is_cancel_requested() and not self.result.saw_finish_surface:
|
|
140
|
+
reason = TERMINAL_REASON_ABORTED_STREAMING
|
|
141
|
+
terminal = build_loop_terminal(
|
|
142
|
+
reason=reason,
|
|
143
|
+
error_kind="aborted",
|
|
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(
|
|
147
|
+
"finish_reason"
|
|
148
|
+
),
|
|
149
|
+
errors=[str(exc) or "cancel_requested"],
|
|
150
|
+
)
|
|
151
|
+
self.result.terminal = terminal
|
|
152
|
+
synthetic_output = self._build_synthetic_graph_output(terminal)
|
|
153
|
+
self.result.graph_output = synthetic_output
|
|
154
|
+
self.result.saw_finish_surface = True
|
|
155
|
+
yield build_synthetic_root_chain_end(graph_output=synthetic_output)
|
|
126
156
|
for repair in self._orphan_tool_result_repairs(closeout_kind="cancelled"):
|
|
127
157
|
yield repair
|
|
128
158
|
raise
|
|
159
|
+
except LoopAbortSignal as exc:
|
|
160
|
+
# D-A′-3:同轨 abort closeout;禁止落入 graph_runtime_error
|
|
161
|
+
self.result.final_error = exc
|
|
162
|
+
reason = (
|
|
163
|
+
exc.terminal_reason
|
|
164
|
+
if exc.terminal_reason in _ABORT_TERMINAL_REASONS
|
|
165
|
+
else TERMINAL_REASON_ABORTED_STREAMING
|
|
166
|
+
)
|
|
167
|
+
logger.info(
|
|
168
|
+
"LoopStreamDriver graph stream aborted: %s",
|
|
169
|
+
exc.detail or reason,
|
|
170
|
+
extra={"loop_stream_driver_abort": {"terminal_reason": reason}},
|
|
171
|
+
)
|
|
172
|
+
for repair in self._orphan_tool_result_repairs(closeout_kind="interrupted"):
|
|
173
|
+
yield repair
|
|
174
|
+
if not self.result.saw_error_visible_surface:
|
|
175
|
+
self.result.saw_error_visible_surface = True
|
|
176
|
+
yield build_synthetic_error_visible_event(
|
|
177
|
+
message=str(exc.detail or reason),
|
|
178
|
+
error_kind="aborted",
|
|
179
|
+
)
|
|
180
|
+
if not self.result.saw_finish_surface:
|
|
181
|
+
terminal = build_loop_terminal(
|
|
182
|
+
reason=reason,
|
|
183
|
+
error_kind="aborted",
|
|
184
|
+
step=(self.result.last_loop_snapshot or {}).get("step"),
|
|
185
|
+
turn_count=(self.result.last_loop_snapshot or {}).get("turn_count"),
|
|
186
|
+
provider_finish_reason=(self.result.last_loop_snapshot or {}).get(
|
|
187
|
+
"finish_reason"
|
|
188
|
+
),
|
|
189
|
+
errors=[str(exc)],
|
|
190
|
+
)
|
|
191
|
+
self.result.terminal = terminal
|
|
192
|
+
synthetic_output = self._build_synthetic_graph_output(terminal)
|
|
193
|
+
self.result.graph_output = synthetic_output
|
|
194
|
+
self.result.saw_finish_surface = True
|
|
195
|
+
yield build_synthetic_root_chain_end(graph_output=synthetic_output)
|
|
196
|
+
raise
|
|
129
197
|
except GraphRecursionError as exc:
|
|
130
198
|
self.result.final_error = exc
|
|
131
199
|
self._log_catch_error(exc)
|
|
@@ -43,7 +43,12 @@ from langchain_agentx.loop.exit.reason_codes import (
|
|
|
43
43
|
TERMINAL_REASON_HOOK_STOPPED,
|
|
44
44
|
)
|
|
45
45
|
from langchain_agentx.loop.exit.terminal import apply_terminal_reason_projection, read_terminal_reason
|
|
46
|
-
from langchain_agentx.loop.loop_abort import
|
|
46
|
+
from langchain_agentx.loop.loop_abort import (
|
|
47
|
+
LoopAbortSignal,
|
|
48
|
+
abort_signal_for_phase,
|
|
49
|
+
is_cancel_requested,
|
|
50
|
+
raise_if_cancel_requested,
|
|
51
|
+
)
|
|
47
52
|
from langchain_agentx.loop.run_context import register_spawned_task
|
|
48
53
|
from langchain_agentx.loop.stream.loop_driver import LoopStreamDriver
|
|
49
54
|
from langchain_agentx.loop.subagent.context import (
|
|
@@ -598,7 +603,9 @@ async def _collect_subagent_events(
|
|
|
598
603
|
child_run_id=child_run_id,
|
|
599
604
|
)
|
|
600
605
|
except asyncio.CancelledError:
|
|
601
|
-
|
|
606
|
+
# D-A′-1:就地转为 LoopAbortSignal 再抛给 body(勿在 except 内 raise_if 逃逸)
|
|
607
|
+
if is_cancel_requested():
|
|
608
|
+
raise abort_signal_for_phase("model") from None
|
|
602
609
|
raise
|
|
603
610
|
except LoopAbortSignal:
|
|
604
611
|
# 上抛给 _run_subagent_body → status=interrupted(勿吞成 completed)
|
|
@@ -3,8 +3,8 @@ scope_resolver.py — Workflow event adapter active scope 解析
|
|
|
3
3
|
|
|
4
4
|
职责:
|
|
5
5
|
- 根据 WorkflowStack.build_path() 从 flat scopes[path] registry 解析当前 active scope
|
|
6
|
-
-
|
|
7
|
-
-
|
|
6
|
+
- miss 时按 D-SCOPE-A′ 降级到最近非 root / 非 workflow_root 已注册祖先
|
|
7
|
+
- 无非 root 祖先时 hard-fail(禁 blanket root)
|
|
8
8
|
|
|
9
9
|
链路位置:
|
|
10
10
|
- Layer 3: Workflow 事件适配器
|
|
@@ -14,18 +14,18 @@ scope_resolver.py — Workflow event adapter active scope 解析
|
|
|
14
14
|
- 仅 dict lookup,不扫描 workflow 实例或 graph
|
|
15
15
|
- scope key 与 WorkflowStack.build_path() / RuntimeScopePath.key() 一致
|
|
16
16
|
- legacy config 无 scopes 时由 root 字段合成仅含 root 的 registry
|
|
17
|
-
-
|
|
17
|
+
- 禁止把 root 当 degrade 目标;与 SessionRouter 共享 nearest_degrade_ancestor
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
import logging
|
|
23
|
+
from collections.abc import Callable
|
|
23
24
|
from typing import Any
|
|
24
25
|
|
|
25
26
|
from langchain_agentx.workflow.runtime_scope_registry import resolve_effective_scopes
|
|
26
27
|
from langchain_agentx.workflow.structure_errors import WorkflowScopeResolutionError
|
|
27
28
|
|
|
28
|
-
from .depth_resolver import LANGGRAPH_INTERNAL_LOOP_NODE_NAMES
|
|
29
29
|
from .stack import WorkflowStack
|
|
30
30
|
|
|
31
31
|
logger = logging.getLogger(__name__)
|
|
@@ -34,9 +34,30 @@ _SCOPE_METADATA_FIELDS = frozenset(
|
|
|
34
34
|
{"workflow_id", "scope_key", "scope_kind", "scope_origin", "graph_key"}
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
+
ScopeLookup = Callable[[str], dict[str, Any] | None]
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
|
|
40
|
+
def nearest_degrade_ancestor(
|
|
41
|
+
path: str,
|
|
42
|
+
*,
|
|
43
|
+
root_scope: str,
|
|
44
|
+
lookup: ScopeLookup,
|
|
45
|
+
) -> dict[str, Any] | None:
|
|
46
|
+
"""沿 '>' 前缀向上;跳过 root_scope / workflow_root(D-SCOPE-A′ · 禁 blanket root)。"""
|
|
47
|
+
if ">" not in path:
|
|
48
|
+
return None
|
|
49
|
+
parts = path.split(">")
|
|
50
|
+
for depth in range(len(parts) - 1, 0, -1):
|
|
51
|
+
prefix = ">".join(parts[:depth])
|
|
52
|
+
if prefix == root_scope:
|
|
53
|
+
continue
|
|
54
|
+
entry = lookup(prefix)
|
|
55
|
+
if entry is None:
|
|
56
|
+
continue
|
|
57
|
+
if entry.get("scope_kind") == "workflow_root":
|
|
58
|
+
continue
|
|
59
|
+
return entry
|
|
60
|
+
return None
|
|
40
61
|
|
|
41
62
|
|
|
42
63
|
class WorkflowScopeResolver:
|
|
@@ -51,6 +72,12 @@ class WorkflowScopeResolver:
|
|
|
51
72
|
root_scope=self._root_scope,
|
|
52
73
|
)
|
|
53
74
|
self._scope_misses: list[str] = []
|
|
75
|
+
self._degrade_warned_paths: set[str] = set()
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def root_scope(self) -> str:
|
|
79
|
+
"""开流 root_scope(SessionRouter 祖先 walk 唯一来源)。"""
|
|
80
|
+
return self._root_scope
|
|
54
81
|
|
|
55
82
|
@property
|
|
56
83
|
def scope_misses(self) -> tuple[str, ...]:
|
|
@@ -81,16 +108,13 @@ class WorkflowScopeResolver:
|
|
|
81
108
|
|
|
82
109
|
self._record_scope_miss(scope_key)
|
|
83
110
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
111
|
+
ancestor = nearest_degrade_ancestor(
|
|
112
|
+
scope_key,
|
|
113
|
+
root_scope=self._root_scope,
|
|
114
|
+
lookup=self.scope_entry_at,
|
|
115
|
+
)
|
|
88
116
|
if ancestor is not None:
|
|
89
|
-
|
|
90
|
-
"workflow scope miss degraded to ancestor: path=%r ancestor=%r",
|
|
91
|
-
scope_key,
|
|
92
|
-
ancestor.get("scope_key"),
|
|
93
|
-
)
|
|
117
|
+
self._warn_degraded_once(scope_key, ancestor)
|
|
94
118
|
return ancestor
|
|
95
119
|
|
|
96
120
|
raise self._build_scope_resolution_error(scope_key)
|
|
@@ -103,22 +127,15 @@ class WorkflowScopeResolver:
|
|
|
103
127
|
if key not in _SCOPE_METADATA_FIELDS
|
|
104
128
|
}
|
|
105
129
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
parts = scope_key.split(">")
|
|
116
|
-
for depth in range(len(parts) - 1, 0, -1):
|
|
117
|
-
prefix = ">".join(parts[:depth])
|
|
118
|
-
entry = self.scope_entry_at(prefix)
|
|
119
|
-
if entry is not None:
|
|
120
|
-
return entry
|
|
121
|
-
return None
|
|
130
|
+
def _warn_degraded_once(self, scope_key: str, ancestor: dict[str, Any]) -> None:
|
|
131
|
+
if scope_key in self._degrade_warned_paths:
|
|
132
|
+
return
|
|
133
|
+
self._degrade_warned_paths.add(scope_key)
|
|
134
|
+
logger.warning(
|
|
135
|
+
"workflow scope miss degraded to ancestor: path=%r ancestor=%r",
|
|
136
|
+
scope_key,
|
|
137
|
+
ancestor.get("scope_key"),
|
|
138
|
+
)
|
|
122
139
|
|
|
123
140
|
def _build_scope_resolution_error(self, scope_key: str) -> WorkflowScopeResolutionError:
|
|
124
141
|
detail_parts = [
|
|
@@ -144,4 +161,8 @@ class WorkflowScopeResolver:
|
|
|
144
161
|
)
|
|
145
162
|
|
|
146
163
|
|
|
147
|
-
__all__ = [
|
|
164
|
+
__all__ = [
|
|
165
|
+
"WorkflowScopeResolver",
|
|
166
|
+
"WorkflowScopeResolutionError",
|
|
167
|
+
"nearest_degrade_ancestor",
|
|
168
|
+
]
|
|
@@ -13,6 +13,7 @@ session_router.py — Workflow 结构/agent 事件 session_id 路由
|
|
|
13
13
|
当前裁剪范围:
|
|
14
14
|
- 结构容器事件仍走 stack/path;agent/loop 事件优先无状态 resolve_loop_session_id_for_event
|
|
15
15
|
- aggregate 使用 aggregate_key 作为 task_key,无 special case
|
|
16
|
+
- D-SCOPE-A-SESSION:ns path 精确 miss 时与 ScopeResolver 同 walk(排除 root)
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
19
|
from __future__ import annotations
|
|
@@ -20,7 +21,7 @@ from __future__ import annotations
|
|
|
20
21
|
from typing import Any
|
|
21
22
|
|
|
22
23
|
from .depth_resolver import DepthResolver
|
|
23
|
-
from .scope_resolver import WorkflowScopeResolver
|
|
24
|
+
from .scope_resolver import WorkflowScopeResolver, nearest_degrade_ancestor
|
|
24
25
|
from .stack import WorkflowStack
|
|
25
26
|
from .types import WorkflowEvent, WorkflowEventType
|
|
26
27
|
|
|
@@ -130,6 +131,12 @@ class WorkflowSessionRouter:
|
|
|
130
131
|
scope_path = self._scope_path_from_raw_event(raw_event)
|
|
131
132
|
if scope_path:
|
|
132
133
|
entry = self._scope_resolver.scope_entry_at(scope_path)
|
|
134
|
+
if entry is None:
|
|
135
|
+
entry = nearest_degrade_ancestor(
|
|
136
|
+
scope_path,
|
|
137
|
+
root_scope=self._scope_resolver.root_scope,
|
|
138
|
+
lookup=self._scope_resolver.scope_entry_at,
|
|
139
|
+
)
|
|
133
140
|
if entry is not None:
|
|
134
141
|
return self._loop_session_from_scope_entry(
|
|
135
142
|
entry,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
langchain_agentx/__init__.py,sha256=
|
|
1
|
+
langchain_agentx/__init__.py,sha256=MJpPR7tuUrTR76iSfQUpDD67ydVKB93o5UEY78FBs28,1614
|
|
2
2
|
langchain_agentx/command/__init__.py,sha256=Ej260S5uFQcmEtGZQG_NH7BYjHoStrpBLtGUsBTxyZk,631
|
|
3
3
|
langchain_agentx/command/allowed_tools.py,sha256=TVA0VM8rm98H-QbLK_GRiX5RhEAZFxKawliMi4kk96A,3359
|
|
4
4
|
langchain_agentx/command/context.py,sha256=DIGOGPBw5UGDBm0WNNJlKx1h_9rCRmcdROv4DT61Qug,731
|
|
@@ -24,9 +24,9 @@ langchain_agentx/health/__init__.py,sha256=fbav8Q7Yhd5Ykr_C6xjMeJccIip6FyeGkNTeg
|
|
|
24
24
|
langchain_agentx/health/checks.py,sha256=eUPT4lDjSBEiwRF8o_5wnEEIQArCWEL3M338OQoKwfA,17965
|
|
25
25
|
langchain_agentx/health/models.py,sha256=lHwdvn0w-qs-c8mI6I3nlnoCk8--y7gADj7C9u7eO9k,2055
|
|
26
26
|
langchain_agentx/health/preflight.py,sha256=wGysAnhT-jR1LfyjdaX9diO573Zzt5Z1M3vxDbpdcK0,5612
|
|
27
|
-
langchain_agentx/loop/__init__.py,sha256=
|
|
27
|
+
langchain_agentx/loop/__init__.py,sha256=JoFPFrlllvGCYFEk2RUuqq9AfUIdLVSNbrRdTRy6g_I,1428
|
|
28
28
|
langchain_agentx/loop/benchmark_frozen_config.py,sha256=YI4IaDVMBOEucF36F2oNv5mw1TqMG_rm_LJSpKAnFWM,13942
|
|
29
|
-
langchain_agentx/loop/loop_abort.py,sha256=
|
|
29
|
+
langchain_agentx/loop/loop_abort.py,sha256=cKlHxZh0u7-drCVqEiRtzkO270MHw6grJuZ1NpKoGfo,3250
|
|
30
30
|
langchain_agentx/loop/message_contract.py,sha256=Twl83TneU2PRdxgGRBxs6S9_ovRT6cb5WeCUzd6oJnE,2196
|
|
31
31
|
langchain_agentx/loop/run_context.py,sha256=dhNiLhvqD_QYacGMA0B5P3wWpSe3O6wU_XcVEzb86Z4,8303
|
|
32
32
|
langchain_agentx/loop/config/__init__.py,sha256=WdRQVXuvGU4myaGOZ8guFFvUb66jfsAIp9DLANJGieg,1083
|
|
@@ -67,11 +67,11 @@ langchain_agentx/loop/exit/resolution.py,sha256=taIMHzAUtgrQoU9446-CUXBN0PyCujhl
|
|
|
67
67
|
langchain_agentx/loop/exit/terminal.py,sha256=_DAZk-kS4Xd0nxMlayLo60-uj1Y5ozgBjLlWUP6RDdc,3524
|
|
68
68
|
langchain_agentx/loop/exit/withheld_error.py,sha256=LfX3j7Rgd9VoMzlz7GOs9TPuRe7RXr_Gxweyotx3tqQ,1846
|
|
69
69
|
langchain_agentx/loop/graph/__init__.py,sha256=9pUUqn7G87n3lV45iYIU24j0wcTHyCD--SSdtv3urh4,136
|
|
70
|
-
langchain_agentx/loop/graph/builtin_loop_control.py,sha256=
|
|
70
|
+
langchain_agentx/loop/graph/builtin_loop_control.py,sha256=31o4H6SDGC_vECSliXecuVEnzlD1ahlVZ5JzEpxYY04,9248
|
|
71
71
|
langchain_agentx/loop/graph/factory.py,sha256=TthwQtNiOeT5K-bgBhuW89yrkdP6iyYXCcL4tn-puJk,103958
|
|
72
72
|
langchain_agentx/loop/graph/graph_edges.py,sha256=kW6Npx-nMIPzcaisCxVPkTwf5Y9GOKy1NWCNkJWeJBQ,47370
|
|
73
73
|
langchain_agentx/loop/graph/reactive_compact_node.py,sha256=633b5hTnR3BK8GBEKWhzOwGB0BQrVRKozNxDomWFjLo,8762
|
|
74
|
-
langchain_agentx/loop/graph/runtime_tools_node.py,sha256=
|
|
74
|
+
langchain_agentx/loop/graph/runtime_tools_node.py,sha256=wQkbc0ophyefpPZEc0q1vKoJOIdS4S6d6hSsrkdRLDk,7387
|
|
75
75
|
langchain_agentx/loop/hook/__init__.py,sha256=rOjc1cqWBxrcp651w_SQX50dWY7o28L2c-r8iVs-SWU,2167
|
|
76
76
|
langchain_agentx/loop/hook/async_hook_runner.py,sha256=SNyuYnQoowZzBTBHQb6o5L9wTG2Rl7qryeMtqFyEsxI,3195
|
|
77
77
|
langchain_agentx/loop/hook/config.py,sha256=f8XtGGHtyjuRvZNhkU8ohbhyqOHD7mHZn_w0rVXMX_o,9157
|
|
@@ -132,7 +132,7 @@ langchain_agentx/loop/runtime/context_factory.py,sha256=m3ics_dOTqEKNlf6ETIOnVj6
|
|
|
132
132
|
langchain_agentx/loop/runtime/subagent_execution_paths.py,sha256=Bgyiom48uta1FZcnIE7fIxyrD-VFCRsw7s8IxIxmLb4,4033
|
|
133
133
|
langchain_agentx/loop/stream/graph_driver.py,sha256=qMZHEw3wJ19Tfbz31-VPLy8Mt6e90xKougIjAoaEFCE,3935
|
|
134
134
|
langchain_agentx/loop/stream/helpers.py,sha256=b2sqtwrAa5ic5B5AzNqItU8pmDHw13iGs3hBiShSIZk,1390
|
|
135
|
-
langchain_agentx/loop/stream/loop_driver.py,sha256=
|
|
135
|
+
langchain_agentx/loop/stream/loop_driver.py,sha256=v9XbmYv4O9ad-SLBXM3FN50V96_77bY8Tlvam13GnzQ,25028
|
|
136
136
|
langchain_agentx/loop/stream/result.py,sha256=xQb9r1cxGCQpoAZ0gNo71CjhSE3eYOiYp5KnoOBzBF8,1131
|
|
137
137
|
langchain_agentx/loop/stream/synthetic_events.py,sha256=Z3JT3iTuB9UQOoQXUprAYvaFV0vXEAuf5iqXYH9YSyI,2175
|
|
138
138
|
langchain_agentx/loop/stream/transcript_gate.py,sha256=4AH3O0l8OrY_GQdqf-TOKOVS3selPLUC0jYLWQUTfkg,4690
|
|
@@ -153,7 +153,7 @@ langchain_agentx/loop/subagent/progress.py,sha256=_4YCDnVDH8sBiwXhCQVB8OHaUQwsCO
|
|
|
153
153
|
langchain_agentx/loop/subagent/progress_dispatcher.py,sha256=e1Kd16WRUOAJSeAjupukrlxHoyCR_JO84SHqntMCzdQ,3831
|
|
154
154
|
langchain_agentx/loop/subagent/prompt.py,sha256=p7RgzibNfEJdfX8nBnZGEwQLa3_EgOn4E56T9MBRIEc,1722
|
|
155
155
|
langchain_agentx/loop/subagent/reasoning_extractor.py,sha256=KrCQtBdKzsjAEfA7H5COuXzBhuGGonT2Su8IEKYlUpA,1334
|
|
156
|
-
langchain_agentx/loop/subagent/runner.py,sha256=
|
|
156
|
+
langchain_agentx/loop/subagent/runner.py,sha256=kUge-LUA2scfSGkocCVACfIC_OApZeOCYG9JTJgGFsI,33446
|
|
157
157
|
langchain_agentx/loop/subagent/subagent_termination_classifier.py,sha256=ULArQeAibgy6ETSf4vRR0L6X1rsDM7-ZuLzRWQ_pmeY,3158
|
|
158
158
|
langchain_agentx/loop/subagent/transcript.py,sha256=-S0VjZcvqlJQGkVGu8VDMTYwTwOePdjDHfjYi3uu7BY,7187
|
|
159
159
|
langchain_agentx/loop/subagent/transcript_message_builder.py,sha256=gcDdbVOospJ9f0b-eM_0Y1uc4yTstaraWZdejokDyDQ,2081
|
|
@@ -724,8 +724,8 @@ langchain_agentx/workflow/validation.py,sha256=3I2jrSgOLAufe3kIzftE_Tfq0ZS-74bIx
|
|
|
724
724
|
langchain_agentx/workflow/event_adapter/__init__.py,sha256=PkG6dP5mAjukYOFEtNFVDn00q_zPmS506dapZxpDgG8,1736
|
|
725
725
|
langchain_agentx/workflow/event_adapter/adapter.py,sha256=eoOgcSos_cu12BRg3a4P0lQvarwr9jCcgt3OrC68A58,25188
|
|
726
726
|
langchain_agentx/workflow/event_adapter/depth_resolver.py,sha256=1xI44dNk9cPotmk1Up14s0QV12WpkRPAQPYnVDY3ZlQ,3991
|
|
727
|
-
langchain_agentx/workflow/event_adapter/scope_resolver.py,sha256=
|
|
728
|
-
langchain_agentx/workflow/event_adapter/session_router.py,sha256=
|
|
727
|
+
langchain_agentx/workflow/event_adapter/scope_resolver.py,sha256=J4duUIdMfNB87zq_0q2sGBgokg0t0UIaxlBGRYJEv7w,5813
|
|
728
|
+
langchain_agentx/workflow/event_adapter/session_router.py,sha256=v2MZieOXDpcioeskSFswnXGKtT_Aw0M0sY9W3oCFmUY,9292
|
|
729
729
|
langchain_agentx/workflow/event_adapter/stack.py,sha256=VCpJekxlGOg0CU2Unxcq7-TeyAHUajPyakKoGnUlT7g,4396
|
|
730
730
|
langchain_agentx/workflow/event_adapter/structure_lifecycle_guard.py,sha256=0inUI-fw-xgWQHuGVVoWRM3a_gpys9JgvOxG5P4bTdw,6658
|
|
731
731
|
langchain_agentx/workflow/event_adapter/structure_utils.py,sha256=y2gS-Tj9FajAqP8kgygaxrosVIRYNjhEtpnp-OIccB8,2820
|
|
@@ -759,8 +759,8 @@ langchain_agentx/workspace/root_grant_manager.py,sha256=W3gy8HTevbERbXqIgRcjzOXO
|
|
|
759
759
|
langchain_agentx/workspace/tool_boundary.py,sha256=UDwX6swpSLsx9HNkYuuRRiV5o7ZZG_Bru2Yn0c5kNX8,5724
|
|
760
760
|
langchain_agentx/workspace/validators.py,sha256=tQt-6TOcL8Fw7Ig5ebA9S7vGWh1rby920eFW6x8Tk9E,1439
|
|
761
761
|
langchain_agentx/workspace/view.py,sha256=PGasqTaqhlD03SXXazHuw4RHfV681AIdlsYqL70pEjc,4774
|
|
762
|
-
langchain_agentx_python-2.1.
|
|
763
|
-
langchain_agentx_python-2.1.
|
|
764
|
-
langchain_agentx_python-2.1.
|
|
765
|
-
langchain_agentx_python-2.1.
|
|
766
|
-
langchain_agentx_python-2.1.
|
|
762
|
+
langchain_agentx_python-2.2.1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
763
|
+
langchain_agentx_python-2.2.1.dist-info/METADATA,sha256=UfgeVWTPqOZJ3Nps3E7gtcupQdCo0YhTRY8uLS3uUiM,24250
|
|
764
|
+
langchain_agentx_python-2.2.1.dist-info/WHEEL,sha256=51RkbunBAw4BWsgaQWTpPhg4Diwp3c9P5iaLk67Hdtg,92
|
|
765
|
+
langchain_agentx_python-2.2.1.dist-info/top_level.txt,sha256=Ge284pniNt8xea0OLk2o9o32GqVpDhOYk20fwE-0xxA,17
|
|
766
|
+
langchain_agentx_python-2.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{langchain_agentx_python-2.1.9.dist-info → langchain_agentx_python-2.2.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|