langchain-agentx-python 2.2.4__py3-none-any.whl → 2.2.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langchain_agentx/__init__.py +1 -1
- langchain_agentx/tool_runtime/base.py +15 -0
- langchain_agentx/tool_runtime/inner_permission_chain.py +9 -1
- langchain_agentx/tool_runtime/outer_permission_resolver.py +12 -3
- langchain_agentx/tools/ask_user_question/tool.py +1 -0
- langchain_agentx/tools/plan_mode/tool.py +2 -0
- langchain_agentx/workflow/base.py +16 -13
- langchain_agentx/workflow/node.py +22 -16
- langchain_agentx/workflow/state.py +258 -25
- {langchain_agentx_python-2.2.4.dist-info → langchain_agentx_python-2.2.6.dist-info}/METADATA +1 -1
- {langchain_agentx_python-2.2.4.dist-info → langchain_agentx_python-2.2.6.dist-info}/RECORD +14 -14
- {langchain_agentx_python-2.2.4.dist-info → langchain_agentx_python-2.2.6.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-2.2.4.dist-info → langchain_agentx_python-2.2.6.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-2.2.4.dist-info → langchain_agentx_python-2.2.6.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -176,6 +176,12 @@ class RuntimeTool(ABC):
|
|
|
176
176
|
is_concurrency_safe: bool = False
|
|
177
177
|
"""是否支持并发执行。"""
|
|
178
178
|
|
|
179
|
+
requires_user_interaction: bool = False
|
|
180
|
+
"""
|
|
181
|
+
对齐 CC ``Tool.requiresUserInteraction()``。
|
|
182
|
+
True 时 bypassPermissions / auto allow 不得吞掉上游 ask(如 AskUser gather)。
|
|
183
|
+
"""
|
|
184
|
+
|
|
179
185
|
never_truncate: bool = False
|
|
180
186
|
"""
|
|
181
187
|
禁止 ToolOutputManager 截断输出。
|
|
@@ -272,6 +278,15 @@ class RuntimeTool(ABC):
|
|
|
272
278
|
_ = data, ctx
|
|
273
279
|
return self.is_concurrency_safe
|
|
274
280
|
|
|
281
|
+
def requires_user_interaction_for(
|
|
282
|
+
self,
|
|
283
|
+
data: dict[str, Any],
|
|
284
|
+
ctx: ToolExecutionContext,
|
|
285
|
+
) -> bool:
|
|
286
|
+
"""该次调用是否强制用户交互;默认读取类属性(对齐 CC requiresUserInteraction)。"""
|
|
287
|
+
_ = data, ctx
|
|
288
|
+
return bool(self.requires_user_interaction)
|
|
289
|
+
|
|
275
290
|
def resolve_capabilities(
|
|
276
291
|
self,
|
|
277
292
|
data: dict[str, Any],
|
|
@@ -4,12 +4,13 @@ inner_permission_chain.py — CC inner 段 1–5(hasPermissionsToUseToolInner
|
|
|
4
4
|
职责:
|
|
5
5
|
在 PermissionOrchestrator 内编排 global deny/ask → L1 → mode 短路 → path policy
|
|
6
6
|
→ acceptEdits 快路径 → passthrough→ask。
|
|
7
|
+
RUI(requires_user_interaction)上游 ask 对齐 CC step 1e,不被 bypass 吞掉。
|
|
7
8
|
|
|
8
9
|
链路位置:
|
|
9
10
|
permission_orchestrator._evaluate_inner / _aevaluate_inner
|
|
10
11
|
|
|
11
12
|
当前裁剪范围:
|
|
12
|
-
段 6(auto / L3
|
|
13
|
+
段 6(auto / L3)不在此模块。
|
|
13
14
|
"""
|
|
14
15
|
|
|
15
16
|
from __future__ import annotations
|
|
@@ -64,6 +65,9 @@ class InnerPermissionChain:
|
|
|
64
65
|
seg12 = self._segment_1_2_global(tool_name, data, ctx, tool_flags)
|
|
65
66
|
if seg12 is not None:
|
|
66
67
|
if seg12.behavior == "ask":
|
|
68
|
+
# CC step 1e:RUI + ask 不被 bypassPermissions 吞掉
|
|
69
|
+
if tool.requires_user_interaction_for(data, ctx):
|
|
70
|
+
return seg12
|
|
67
71
|
seg4 = self._segment_4_mode_shortcircuit(tool, data, ctx, tool_flags)
|
|
68
72
|
if seg4 is not None:
|
|
69
73
|
return seg4
|
|
@@ -72,6 +76,10 @@ class InnerPermissionChain:
|
|
|
72
76
|
l1 = tool.run_l1_permission_check(data, ctx)
|
|
73
77
|
if l1.outcome != "continue":
|
|
74
78
|
if l1.outcome == "ask":
|
|
79
|
+
if l1.decision is None:
|
|
80
|
+
raise ValueError(f"L1 outcome={l1.outcome!r} requires decision")
|
|
81
|
+
if tool.requires_user_interaction_for(data, ctx):
|
|
82
|
+
return l1.decision
|
|
75
83
|
seg4 = self._segment_4_mode_shortcircuit(tool, data, ctx, tool_flags)
|
|
76
84
|
if seg4 is not None:
|
|
77
85
|
return seg4
|
|
@@ -4,6 +4,7 @@ outer_permission_resolver.py — 段 6 outer 裁决(GAP-3)
|
|
|
4
4
|
职责:
|
|
5
5
|
inner 产出 ask 后:auto 分类器消化 / headless fail-close deny;非 ask 原样返回。
|
|
6
6
|
classifier 熔断到顶:CLI → ask;headless → ClassifierDenialLimitAbort。
|
|
7
|
+
RUI 工具的 ask 不被 auto allow 吞掉(对齐 CC requiresUserInteraction)。
|
|
7
8
|
|
|
8
9
|
链路位置:
|
|
9
10
|
PermissionOrchestrator._resolve_outer
|
|
@@ -61,6 +62,8 @@ class OuterPermissionResolver:
|
|
|
61
62
|
tool_flags = ctx.tool_flags if hasattr(ctx, "tool_flags") else {}
|
|
62
63
|
mode = str(tool_flags.get("permission_mode", "default"))
|
|
63
64
|
|
|
65
|
+
inner_ask = decision
|
|
66
|
+
|
|
64
67
|
if mode == "auto":
|
|
65
68
|
auto_decision = await self._run_auto_mode_hook(tool, data, ctx)
|
|
66
69
|
if auto_decision is not None:
|
|
@@ -70,12 +73,18 @@ class OuterPermissionResolver:
|
|
|
70
73
|
ctx,
|
|
71
74
|
headless=headless,
|
|
72
75
|
)
|
|
73
|
-
if
|
|
76
|
+
if (
|
|
77
|
+
auto_decision.behavior == "allow"
|
|
78
|
+
and tool.requires_user_interaction_for(data, ctx)
|
|
79
|
+
):
|
|
80
|
+
# CC:忽略 auto allow,保留 ask;继续走下方 headless / 透传
|
|
81
|
+
pass
|
|
82
|
+
elif auto_decision.behavior in ("allow", "deny", "ask"):
|
|
74
83
|
return auto_decision
|
|
75
84
|
|
|
76
85
|
if headless:
|
|
77
|
-
return self._headless_deny(
|
|
78
|
-
return
|
|
86
|
+
return self._headless_deny(inner_ask)
|
|
87
|
+
return inner_ask
|
|
79
88
|
|
|
80
89
|
@staticmethod
|
|
81
90
|
async def _run_auto_mode_hook(
|
|
@@ -84,6 +84,7 @@ class AskUserQuestionTool(RuntimeTool):
|
|
|
84
84
|
is_read_only: bool = True
|
|
85
85
|
is_destructive: bool = False
|
|
86
86
|
is_concurrency_safe: bool = True
|
|
87
|
+
requires_user_interaction: bool = True
|
|
87
88
|
never_truncate: bool = False
|
|
88
89
|
dedupe_identical_calls: bool = False
|
|
89
90
|
max_result_size_chars: int = 100_000
|
|
@@ -183,6 +183,8 @@ class ExitPlanModeRuntimeTool(_PlanModeRuntimeToolBase):
|
|
|
183
183
|
description: str = EXIT_PLAN_MODE_DESCRIPTION
|
|
184
184
|
input_model = ExitPlanModeInput
|
|
185
185
|
output_model = ExitPlanModeOutput
|
|
186
|
+
# 对齐 CC ExitPlanMode.requiresUserInteraction:bypass/auto 不得静默放行确认
|
|
187
|
+
requires_user_interaction: bool = True
|
|
186
188
|
|
|
187
189
|
def prompt_for_model(self, ctx: ToolPromptContext) -> str:
|
|
188
190
|
_ = ctx
|
|
@@ -65,7 +65,7 @@ from .execution import (
|
|
|
65
65
|
)
|
|
66
66
|
from .memory_drain_registry import MemoryDrainRegistry
|
|
67
67
|
from .memory_session_policy import collect_agent_nodes, validate_loop_session_ids
|
|
68
|
-
from .state import WorkflowState
|
|
68
|
+
from .state import WorkflowState, project_workflow_node_update
|
|
69
69
|
|
|
70
70
|
# Lazy import for event_adapter(避免循环依赖)
|
|
71
71
|
# 实际使用时通过 _get_event_adapter() 延迟导入
|
|
@@ -453,21 +453,22 @@ class BaseWorkflow(ABC):
|
|
|
453
453
|
|
|
454
454
|
与 run() 的区别:
|
|
455
455
|
- run(): 返回完整 WorkflowState(用于顶层调用)
|
|
456
|
-
- as_node():
|
|
456
|
+
- as_node(): 返回相对父 state 的 channel delta(供 LangGraph reducer 合并)
|
|
457
457
|
|
|
458
458
|
实现原理:
|
|
459
|
-
-
|
|
459
|
+
- 调用 run() 得到子图终态
|
|
460
|
+
- 经 project_workflow_node_update 将 messages / item_results 投影为增量
|
|
460
461
|
- 传递 parent_workflow 构建层级关系
|
|
461
462
|
- 使用 outer_semaphore 控制并发(如果提供)
|
|
462
|
-
- LangGraph 的 reducer 会自动处理合并
|
|
463
463
|
|
|
464
464
|
并发控制(问题 19):
|
|
465
465
|
- 如果 outer_workflow 提供 semaphore,子 workflow 的执行会被限流
|
|
466
466
|
- 这解决了 DeepSearch 场景下 45 个 sub-workflow 同时启动的问题
|
|
467
467
|
|
|
468
|
-
|
|
469
|
-
-
|
|
470
|
-
-
|
|
468
|
+
注意(聚合通道契约 · 2026-09-04):
|
|
469
|
+
- 节点必须返回 delta,禁止把父 state 全量回写进 messages / item_results
|
|
470
|
+
- 手写节点请用 delta_messages / delta_item_results
|
|
471
|
+
- as_node 已自动投影,避免嵌套子图 2^N 膨胀
|
|
471
472
|
"""
|
|
472
473
|
async def node_fn(state: dict) -> dict:
|
|
473
474
|
# 入口即快照父 _active_run_config,避免 run() 内 set/restore 与父并发时读漂移。
|
|
@@ -475,16 +476,18 @@ class BaseWorkflow(ABC):
|
|
|
475
476
|
# === 并发控制(问题 19)===
|
|
476
477
|
if outer_semaphore is not None:
|
|
477
478
|
async with outer_semaphore:
|
|
478
|
-
|
|
479
|
+
result = await self.run(
|
|
479
480
|
state,
|
|
480
481
|
parent_workflow=parent_workflow,
|
|
481
482
|
run_config=inherited_run_config,
|
|
482
483
|
)
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
484
|
+
else:
|
|
485
|
+
result = await self.run(
|
|
486
|
+
state,
|
|
487
|
+
parent_workflow=parent_workflow,
|
|
488
|
+
run_config=inherited_run_config,
|
|
489
|
+
)
|
|
490
|
+
return project_workflow_node_update(state, result)
|
|
488
491
|
|
|
489
492
|
return node_fn
|
|
490
493
|
|
|
@@ -93,16 +93,22 @@ class AgentNode:
|
|
|
93
93
|
ctx: WorkflowNodeContext,
|
|
94
94
|
invocation: LoopInvocation,
|
|
95
95
|
) -> dict[str, Any]:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
from .state import delta_item_results, project_workflow_node_update
|
|
97
|
+
|
|
98
|
+
del ctx
|
|
99
|
+
update = project_workflow_node_update(
|
|
100
|
+
{"messages": state.get("messages", [])},
|
|
101
|
+
{"messages": graph_result.get("messages", [])},
|
|
102
|
+
)
|
|
103
|
+
update.update(
|
|
104
|
+
delta_item_results(
|
|
100
105
|
{
|
|
101
106
|
"node": self.name,
|
|
102
107
|
"session_id": invocation.session_id,
|
|
103
108
|
}
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
return update
|
|
106
112
|
|
|
107
113
|
def map_error(
|
|
108
114
|
self,
|
|
@@ -110,16 +116,16 @@ class AgentNode:
|
|
|
110
116
|
exc: Exception,
|
|
111
117
|
ctx: WorkflowNodeContext,
|
|
112
118
|
) -> dict[str, Any]:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
from .state import delta_item_results
|
|
120
|
+
|
|
121
|
+
del state
|
|
122
|
+
return delta_item_results(
|
|
123
|
+
{
|
|
124
|
+
"node": self.name,
|
|
125
|
+
"session_id": ctx.loop_session_id,
|
|
126
|
+
"error": str(exc),
|
|
127
|
+
}
|
|
128
|
+
)
|
|
123
129
|
|
|
124
130
|
def _resolve_graph_factory(
|
|
125
131
|
self,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""
|
|
2
|
-
state.py — Workflow
|
|
2
|
+
state.py — Workflow 状态定义与聚合通道契约
|
|
3
3
|
|
|
4
4
|
职责:
|
|
5
5
|
- 定义 WorkflowState 基类(TypedDict + Annotated reducer)
|
|
6
|
-
-
|
|
7
|
-
-
|
|
6
|
+
- item_results 按身份键幂等合并(防 2^N 全量回写 OOM)
|
|
7
|
+
- 提供 delta_messages / delta_item_results 增量构造接口(防误用)
|
|
8
|
+
- as_node 用 project_workflow_node_update 将终态投影为 delta
|
|
8
9
|
|
|
9
10
|
链路位置:
|
|
10
11
|
- 被 BaseWorkflow 和所有特殊 Workflow 使用
|
|
@@ -13,44 +14,268 @@ state.py — Workflow 状态定义
|
|
|
13
14
|
当前裁剪范围:
|
|
14
15
|
- 仅包含基础字段(messages, item_results, workflow_id 等)
|
|
15
16
|
- 业务特定字段由具体 Workflow 自行扩展
|
|
16
|
-
|
|
17
|
-
设计说明(问题 10):
|
|
18
|
-
- WorkflowState 使用 TypedDict,这是 LangGraph 的最佳实践
|
|
19
|
-
- TypedDict + Annotated reducer 是 LangGraph 官方推荐的状态定义方式
|
|
20
|
-
- 不要使用普通 dict(没有类型检查和 reducer 支持)
|
|
21
|
-
- 不要使用 dataclass(LangGraph StateGraph 不直接支持)
|
|
17
|
+
- 通道预算上限(P2)未做
|
|
22
18
|
"""
|
|
23
19
|
|
|
24
20
|
from __future__ import annotations
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
from typing import Annotated, Any, Hashable, Mapping, Sequence, TypedDict
|
|
28
25
|
|
|
29
26
|
from langgraph.graph.message import add_messages
|
|
30
27
|
from langchain_core.messages import AnyMessage
|
|
31
28
|
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
_STRICT_DELTA_ENV = "LANGCHAIN_AGENTX_WORKFLOW_STRICT_DELTA"
|
|
32
|
+
|
|
33
|
+
# 显式主键优先;其次 task(编排任务);再 node/repo/session 等复合键
|
|
34
|
+
_EXPLICIT_IDENTITY_KEYS: tuple[str, ...] = ("result_id", "item_key", "id")
|
|
35
|
+
_COMPOSITE_IDENTITY_KEYS: tuple[str, ...] = (
|
|
36
|
+
"node",
|
|
37
|
+
"repo_id",
|
|
38
|
+
"repo",
|
|
39
|
+
"session_id",
|
|
40
|
+
"from",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def item_result_identity(item: Any) -> Hashable | None:
|
|
45
|
+
"""提取 item_results 条目的合并身份;无身份则返回 None(走 append)。"""
|
|
46
|
+
if not isinstance(item, dict):
|
|
47
|
+
try:
|
|
48
|
+
hash(item)
|
|
49
|
+
except TypeError:
|
|
50
|
+
return None
|
|
51
|
+
return ("value", item)
|
|
52
|
+
|
|
53
|
+
for key in _EXPLICIT_IDENTITY_KEYS:
|
|
54
|
+
value = item.get(key)
|
|
55
|
+
if value is not None:
|
|
56
|
+
return (key, value)
|
|
57
|
+
|
|
58
|
+
task = item.get("task")
|
|
59
|
+
if task is not None:
|
|
60
|
+
return ("task", task)
|
|
61
|
+
|
|
62
|
+
parts = tuple(
|
|
63
|
+
(key, item[key])
|
|
64
|
+
for key in _COMPOSITE_IDENTITY_KEYS
|
|
65
|
+
if item.get(key) is not None
|
|
66
|
+
)
|
|
67
|
+
if parts:
|
|
68
|
+
return ("composite", parts)
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _looks_like_full_rewrite(left: Sequence[Any], right: Sequence[Any]) -> bool:
|
|
73
|
+
"""检测典型反模式:提交值 = 旧列表前缀 + 增量(state.get(...) + [...])。"""
|
|
74
|
+
if not left or len(right) <= len(left):
|
|
75
|
+
return False
|
|
76
|
+
return list(right[: len(left)]) == list(left)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _maybe_raise_or_warn_full_rewrite(left: list[Any], right: list[Any]) -> None:
|
|
80
|
+
if not _looks_like_full_rewrite(left, right):
|
|
81
|
+
return
|
|
82
|
+
message = (
|
|
83
|
+
"item_results full-rewrite antipattern detected: "
|
|
84
|
+
f"submitted len={len(right)} looks like prior len={len(left)} + suffix. "
|
|
85
|
+
"Return delta only via delta_item_results(...); "
|
|
86
|
+
"do not submit state.get('item_results', []) + [...]."
|
|
87
|
+
)
|
|
88
|
+
if os.environ.get(_STRICT_DELTA_ENV, "").strip() in {"1", "true", "TRUE", "yes", "YES"}:
|
|
89
|
+
raise ValueError(message)
|
|
90
|
+
logger.warning(message)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ItemResultsMerge:
|
|
94
|
+
"""item_results 通道归约器:按身份键幂等合并,并检测全量回写。"""
|
|
95
|
+
|
|
96
|
+
def __call__(
|
|
97
|
+
self,
|
|
98
|
+
left: list[Any] | None,
|
|
99
|
+
right: list[Any] | None,
|
|
100
|
+
) -> list[Any]:
|
|
101
|
+
return self.merge(left, right)
|
|
102
|
+
|
|
103
|
+
def merge(
|
|
104
|
+
self,
|
|
105
|
+
left: list[Any] | None,
|
|
106
|
+
right: list[Any] | None,
|
|
107
|
+
) -> list[Any]:
|
|
108
|
+
left_list = list(left or [])
|
|
109
|
+
right_list = list(right or [])
|
|
110
|
+
if not right_list:
|
|
111
|
+
return left_list
|
|
112
|
+
if not left_list:
|
|
113
|
+
return list(right_list)
|
|
114
|
+
|
|
115
|
+
_maybe_raise_or_warn_full_rewrite(left_list, right_list)
|
|
116
|
+
|
|
117
|
+
keyed: dict[Hashable, int] = {}
|
|
118
|
+
merged: list[Any] = []
|
|
119
|
+
for item in left_list:
|
|
120
|
+
identity = item_result_identity(item)
|
|
121
|
+
if identity is None:
|
|
122
|
+
merged.append(item)
|
|
123
|
+
continue
|
|
124
|
+
if identity in keyed:
|
|
125
|
+
merged[keyed[identity]] = item
|
|
126
|
+
else:
|
|
127
|
+
keyed[identity] = len(merged)
|
|
128
|
+
merged.append(item)
|
|
129
|
+
|
|
130
|
+
for item in right_list:
|
|
131
|
+
identity = item_result_identity(item)
|
|
132
|
+
if identity is None:
|
|
133
|
+
merged.append(item)
|
|
134
|
+
continue
|
|
135
|
+
if identity in keyed:
|
|
136
|
+
merged[keyed[identity]] = item
|
|
137
|
+
else:
|
|
138
|
+
keyed[identity] = len(merged)
|
|
139
|
+
merged.append(item)
|
|
140
|
+
return merged
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
merge_item_results = ItemResultsMerge()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _normalize_varargs(items: tuple[Any, ...]) -> list[Any]:
|
|
147
|
+
if len(items) == 1 and isinstance(items[0], list):
|
|
148
|
+
return list(items[0])
|
|
149
|
+
return list(items)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def delta_messages(*messages: Any) -> dict[str, list[Any]]:
|
|
153
|
+
"""构造 messages 通道增量(节点应 return 本 dict 或其展开)。
|
|
154
|
+
|
|
155
|
+
只接收本次新增消息,禁止传入 state['messages'] 全量历史。
|
|
156
|
+
"""
|
|
157
|
+
return {"messages": _normalize_varargs(messages)}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def delta_item_results(*items: Any) -> dict[str, list[Any]]:
|
|
161
|
+
"""构造 item_results 通道增量(节点应 return 本 dict 或其展开)。
|
|
162
|
+
|
|
163
|
+
只接收本次新增/更新条目;条目宜带身份键(result_id / item_key / task / node+…)。
|
|
164
|
+
禁止传入 state.get('item_results', []) + [...] 全量历史。
|
|
165
|
+
"""
|
|
166
|
+
return {"item_results": _normalize_varargs(items)}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _message_identity(message: Any) -> Hashable | None:
|
|
170
|
+
msg_id = getattr(message, "id", None)
|
|
171
|
+
if msg_id is not None:
|
|
172
|
+
return ("id", msg_id)
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _messages_delta(before: Sequence[Any] | None, after: Sequence[Any] | None) -> list[Any]:
|
|
177
|
+
before_list = list(before or [])
|
|
178
|
+
after_list = list(after or [])
|
|
179
|
+
if not after_list:
|
|
180
|
+
return []
|
|
181
|
+
seen: set[Hashable] = set()
|
|
182
|
+
for message in before_list:
|
|
183
|
+
identity = _message_identity(message)
|
|
184
|
+
if identity is not None:
|
|
185
|
+
seen.add(identity)
|
|
186
|
+
delta: list[Any] = []
|
|
187
|
+
for message in after_list:
|
|
188
|
+
identity = _message_identity(message)
|
|
189
|
+
if identity is None:
|
|
190
|
+
# 无 id:仅当不在 before 相等集合中时纳入(弱 diff)
|
|
191
|
+
if message not in before_list:
|
|
192
|
+
delta.append(message)
|
|
193
|
+
continue
|
|
194
|
+
if identity not in seen:
|
|
195
|
+
seen.add(identity)
|
|
196
|
+
delta.append(message)
|
|
197
|
+
return delta
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _item_results_delta(
|
|
201
|
+
before: Sequence[Any] | None,
|
|
202
|
+
after: Sequence[Any] | None,
|
|
203
|
+
) -> list[Any]:
|
|
204
|
+
before_list = list(before or [])
|
|
205
|
+
after_list = list(after or [])
|
|
206
|
+
if not after_list:
|
|
207
|
+
return []
|
|
208
|
+
|
|
209
|
+
before_by_key: dict[Hashable, Any] = {}
|
|
210
|
+
before_unkeyed: list[Any] = []
|
|
211
|
+
for item in before_list:
|
|
212
|
+
identity = item_result_identity(item)
|
|
213
|
+
if identity is None:
|
|
214
|
+
before_unkeyed.append(item)
|
|
215
|
+
else:
|
|
216
|
+
before_by_key[identity] = item
|
|
217
|
+
|
|
218
|
+
delta: list[Any] = []
|
|
219
|
+
after_unkeyed: list[Any] = []
|
|
220
|
+
for item in after_list:
|
|
221
|
+
identity = item_result_identity(item)
|
|
222
|
+
if identity is None:
|
|
223
|
+
after_unkeyed.append(item)
|
|
224
|
+
continue
|
|
225
|
+
prior = before_by_key.get(identity)
|
|
226
|
+
if prior is None or prior != item:
|
|
227
|
+
delta.append(item)
|
|
228
|
+
|
|
229
|
+
remaining_unkeyed = list(before_unkeyed)
|
|
230
|
+
for item in after_unkeyed:
|
|
231
|
+
if item in remaining_unkeyed:
|
|
232
|
+
remaining_unkeyed.remove(item)
|
|
233
|
+
else:
|
|
234
|
+
delta.append(item)
|
|
235
|
+
return delta
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def project_workflow_node_update(
|
|
239
|
+
before: Mapping[str, Any],
|
|
240
|
+
after: Mapping[str, Any],
|
|
241
|
+
) -> dict[str, Any]:
|
|
242
|
+
"""将子 workflow 终态投影为父图节点应提交的 channel delta。
|
|
243
|
+
|
|
244
|
+
聚合通道(messages / item_results)只提交相对 before 的增量;
|
|
245
|
+
其它字段在值变化时原样写入(last-write-wins)。
|
|
246
|
+
"""
|
|
247
|
+
update: dict[str, Any] = {}
|
|
248
|
+
for key, value in after.items():
|
|
249
|
+
if key == "messages":
|
|
250
|
+
delta = _messages_delta(before.get("messages"), value)
|
|
251
|
+
if delta:
|
|
252
|
+
update["messages"] = delta
|
|
253
|
+
continue
|
|
254
|
+
if key == "item_results":
|
|
255
|
+
delta = _item_results_delta(before.get("item_results"), value)
|
|
256
|
+
if delta:
|
|
257
|
+
update["item_results"] = delta
|
|
258
|
+
continue
|
|
259
|
+
if before.get(key) != value:
|
|
260
|
+
update[key] = value
|
|
261
|
+
return update
|
|
262
|
+
|
|
32
263
|
|
|
33
264
|
class WorkflowState(TypedDict, total=False):
|
|
34
265
|
"""Workflow 状态 schema,支持并发写入。
|
|
35
266
|
|
|
36
267
|
字段说明:
|
|
37
|
-
- messages:
|
|
38
|
-
- item_results:
|
|
39
|
-
- workflow_id
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
使用方式:
|
|
45
|
-
- 基础类型:WorkflowState(本定义)
|
|
46
|
-
- 扩展类型:class MyState(WorkflowState, total=False) 添加业务字段
|
|
268
|
+
- messages: 消息列表(add_messages 按 id 幂等合并)
|
|
269
|
+
- item_results: 并行/叶节点产出(merge_item_results 按身份键幂等合并)
|
|
270
|
+
- workflow_id / task_key / current_step / orchestration_round / error: 单写字段
|
|
271
|
+
|
|
272
|
+
节点写入约定:
|
|
273
|
+
- 使用 delta_messages / delta_item_results 构造增量,禁止 {**state} 全量回写聚合通道
|
|
47
274
|
"""
|
|
48
275
|
|
|
49
|
-
# === 并发聚合字段(使用 reducer)===
|
|
50
276
|
messages: Annotated[list[AnyMessage], add_messages]
|
|
51
|
-
item_results: Annotated[list[dict],
|
|
277
|
+
item_results: Annotated[list[dict], merge_item_results]
|
|
52
278
|
|
|
53
|
-
# === 单写字段(无需 reducer)===
|
|
54
279
|
workflow_id: str
|
|
55
280
|
task_key: str | None
|
|
56
281
|
current_step: str | None
|
|
@@ -58,4 +283,12 @@ class WorkflowState(TypedDict, total=False):
|
|
|
58
283
|
error: str | None
|
|
59
284
|
|
|
60
285
|
|
|
61
|
-
__all__ = [
|
|
286
|
+
__all__ = [
|
|
287
|
+
"WorkflowState",
|
|
288
|
+
"ItemResultsMerge",
|
|
289
|
+
"merge_item_results",
|
|
290
|
+
"item_result_identity",
|
|
291
|
+
"delta_messages",
|
|
292
|
+
"delta_item_results",
|
|
293
|
+
"project_workflow_node_update",
|
|
294
|
+
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
langchain_agentx/__init__.py,sha256=
|
|
1
|
+
langchain_agentx/__init__.py,sha256=w5E568-acwfUArEGRc1Fs10Ni10VOEs-jLn4UGZVjeE,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
|
|
@@ -393,7 +393,7 @@ langchain_agentx/tool_runtime/__init__.py,sha256=C4ZrS6UWSL65ErfNWMvBuZBnht6ZuGb
|
|
|
393
393
|
langchain_agentx/tool_runtime/accept_edits_fast_path.py,sha256=UMCOF9Ws7hx3pcYvL-Kq7vzIAGvOGdEfx69ccUDXT-k,2281
|
|
394
394
|
langchain_agentx/tool_runtime/adapter.py,sha256=rM-Y2zSZ--N-TIvx1HtI1J7ralPU0w4jJuX3bBvIVxM,33847
|
|
395
395
|
langchain_agentx/tool_runtime/agent_home_bypass.py,sha256=na9xQPgJ02qA3YiJ1F8jN0Uv4uBDPzqRk3lfPPE089g,8010
|
|
396
|
-
langchain_agentx/tool_runtime/base.py,sha256=
|
|
396
|
+
langchain_agentx/tool_runtime/base.py,sha256=asjZ0Rsxchh51OQbt5-h61xoWgC-i5sUbH0e_uSf-X0,21922
|
|
397
397
|
langchain_agentx/tool_runtime/cancel_projection.py,sha256=81_Bvg8ZiuPdl1usePt7ykXFuO6Sg7UXogYQBX6aysQ,2788
|
|
398
398
|
langchain_agentx/tool_runtime/capabilities.py,sha256=KHwu5KLqrpPKLOTwHhnvIIs9MTmQYeuS2hAFmI_Nsy8,3837
|
|
399
399
|
langchain_agentx/tool_runtime/classifier_denial_bridge.py,sha256=Wv40rYc5U3Mn7G3H7sUWrcG78HyLKnrRXvv7Yv9wI5U,2743
|
|
@@ -404,14 +404,14 @@ langchain_agentx/tool_runtime/denial_tracking_bridge.py,sha256=qssnZ_tzCIrDeJvtx
|
|
|
404
404
|
langchain_agentx/tool_runtime/errors.py,sha256=Ed1FMjBdPfr0WG-YwLIm_7jokl9k_DjHFNiewZTeQfg,8883
|
|
405
405
|
langchain_agentx/tool_runtime/failure_event_emitter.py,sha256=C7c4Gc7Vo_R6_ah2Xxa2eso7l9Iuz_KTgRV0j0B08rA,10467
|
|
406
406
|
langchain_agentx/tool_runtime/identical_call_cache.py,sha256=v8WWuTHWR5ThujcMO4Ty28mZS_w-JN24267fZ4PHWK8,3865
|
|
407
|
-
langchain_agentx/tool_runtime/inner_permission_chain.py,sha256=
|
|
407
|
+
langchain_agentx/tool_runtime/inner_permission_chain.py,sha256=k0uf-Tvr4NitX9xsSvGki2iuwBn0IZZ0t_rQf49CbnE,17075
|
|
408
408
|
langchain_agentx/tool_runtime/limits.py,sha256=imE1IetmyKQM82INR0VkP3qZNM4bjUJuf1CG5BkY0uw,356
|
|
409
409
|
langchain_agentx/tool_runtime/loader.py,sha256=ZeT-xIB6TDkehqyYWx1gkvI0aKx9_xKDKJhqyIvlAvw,9397
|
|
410
410
|
langchain_agentx/tool_runtime/loop_loader.py,sha256=C_4T9XxlDmKaTzGv4MO6mJuR-rd6uueb1ZWwEKvvFaE,9087
|
|
411
411
|
langchain_agentx/tool_runtime/loop_runtime_context.py,sha256=HBZMdtKD7MpU8o7JmURFZGPTfxr2aFFlFF2nBZR_n8s,3613
|
|
412
412
|
langchain_agentx/tool_runtime/messages.py,sha256=Fp-ZiLAbOQAQa8uIv-owE8PXtWyhpYSXxtzy3PVbKSw,3631
|
|
413
413
|
langchain_agentx/tool_runtime/models.py,sha256=cO9EgLKPXiktHl_xXtBwLQrCo6mj4tbT4WoPmuFLE0w,20345
|
|
414
|
-
langchain_agentx/tool_runtime/outer_permission_resolver.py,sha256=
|
|
414
|
+
langchain_agentx/tool_runtime/outer_permission_resolver.py,sha256=t6vq1f6K0EYZFAlEBV-Ic2hoGB3miVZWqvrZOQv-Z0A,4169
|
|
415
415
|
langchain_agentx/tool_runtime/override.py,sha256=D8TRftoeWzqlTuWCGUxNkFY7XzWx_d5oxI_ltNibqPA,10258
|
|
416
416
|
langchain_agentx/tool_runtime/path_safety.py,sha256=dUrl2SObKmRDwdm9k8veO_Eh0iXw0LPLAMZJGxeKnTE,8548
|
|
417
417
|
langchain_agentx/tool_runtime/permission_context.py,sha256=ReVV2kjVWAPTsPaYXxRB-dICqualEvi2zzwF8NX_Wkk,6229
|
|
@@ -524,7 +524,7 @@ langchain_agentx/tools/ask_user_question/constants.py,sha256=vK5QraZBBVj7woW1TF0
|
|
|
524
524
|
langchain_agentx/tools/ask_user_question/html_preview.py,sha256=OwNpsH9qm8iJ-TTbzkMFvG49T4-oNJXJXH4OZ8r55wc,3913
|
|
525
525
|
langchain_agentx/tools/ask_user_question/models.py,sha256=uLc2UK_v8iMFEL1Zk0mUOBYdcg2HfbaLdH68o8kr7GE,3376
|
|
526
526
|
langchain_agentx/tools/ask_user_question/prompt.py,sha256=0RdVuUeNzQBzPeVrLcbiBqh1uizFgUANvaKOZpVy_qg,2803
|
|
527
|
-
langchain_agentx/tools/ask_user_question/tool.py,sha256
|
|
527
|
+
langchain_agentx/tools/ask_user_question/tool.py,sha256=KGIjeLD9J_3RR-C3fGpOsuqjULmuobohG0F2ie8i8UI,13405
|
|
528
528
|
langchain_agentx/tools/ask_user_question/validators.py,sha256=gUYuuKJNrExWGvhFlmhqqSU8ax7s4EhXMvTEjapHeiY,5974
|
|
529
529
|
langchain_agentx/tools/bash/__init__.py,sha256=molPHFDylE2wWG_aQx7fb0ciAx3wWpdXzIixTZV5ne4,214
|
|
530
530
|
langchain_agentx/tools/bash/ast_security.py,sha256=6DFd_TwT986dbGkKT3UnDIZioBdmi8mbJcYSI-XEtdM,22444
|
|
@@ -591,7 +591,7 @@ langchain_agentx/tools/plan_mode/__init__.py,sha256=wO5FX24xEB0Dx9h6bysBt1ql3wn0
|
|
|
591
591
|
langchain_agentx/tools/plan_mode/constants.py,sha256=vBjcx9JTSOhU9jfCJ4RKnCxVQPn0HzHapouKxRf2Zns,293
|
|
592
592
|
langchain_agentx/tools/plan_mode/models.py,sha256=kRHPJetlB9X5nAq7-bWm1WcC68YsAj17UxY7fR6TpZ4,1204
|
|
593
593
|
langchain_agentx/tools/plan_mode/prompt.py,sha256=69o04P598fYs70EaHmpx6_XTk6fgxj06njn5Qq51YF0,3935
|
|
594
|
-
langchain_agentx/tools/plan_mode/tool.py,sha256=
|
|
594
|
+
langchain_agentx/tools/plan_mode/tool.py,sha256=IDOPKUn2cdvO43Evsu5Ns0U6x1T3aji8IOtqdBDDyYU,8852
|
|
595
595
|
langchain_agentx/tools/read/__init__.py,sha256=82jaYnSYVxREh7HR3tiPAoNLQGmdCsWlkFXHyUg9-ME,217
|
|
596
596
|
langchain_agentx/tools/read/backend.py,sha256=YqKJ5jzMIrND3WC582jDvVA2mh2rKZ12LK9Vt-j3bz0,14524
|
|
597
597
|
langchain_agentx/tools/read/limits.py,sha256=JrCEv1UCzBYevf0rDgGOFFfOAHCo04LjLOCj5m1rmbc,2843
|
|
@@ -696,7 +696,7 @@ langchain_agentx/utils/permissions/filesystem.py,sha256=79vRSRPnPadHlDOOYvvbuJaJ
|
|
|
696
696
|
langchain_agentx/utils/settings/__init__.py,sha256=UXzHZmrBJsO8ZpDmHfclHXpS7rfpmqRoXEnmTFbtVpE,172
|
|
697
697
|
langchain_agentx/utils/settings/managed_path.py,sha256=MVcw4F9bTrhKqTLom8ZiiqSqCREMbGcAK9Lt8hkD67M,1155
|
|
698
698
|
langchain_agentx/workflow/__init__.py,sha256=wgjJM2wW0DbFHjsNbq2bKYgFs3BIPFRuQbH8Ry5kfys,4324
|
|
699
|
-
langchain_agentx/workflow/base.py,sha256=
|
|
699
|
+
langchain_agentx/workflow/base.py,sha256=g4U7hO4fNRBiwh8Y3yLiU8Rgo1AgahaXV1L7lPob32c,33600
|
|
700
700
|
langchain_agentx/workflow/batch.py,sha256=rdE9xbQ2-SI3-6yNTsKoSylDoTD5B0djLSKwqy1BdZ8,5474
|
|
701
701
|
langchain_agentx/workflow/catalog_registry.py,sha256=tYUny5FkHwQU-RRUxOdxaJhiRV_9VRXU636Yn_Gzt-g,2109
|
|
702
702
|
langchain_agentx/workflow/dag.py,sha256=Y1IR1IwnM79Xuev6HZnOvAE3Q23MRnA9lu4vg2Cie48,4469
|
|
@@ -706,7 +706,7 @@ langchain_agentx/workflow/execution.py,sha256=iCEkPgLkaoEM4cB6o7KAqIaBpFuY5HtGGI
|
|
|
706
706
|
langchain_agentx/workflow/loop_factory.py,sha256=hCShf_gDnesW2A9JspaUY3upp3NCIguII_wRiXnSIUM,3954
|
|
707
707
|
langchain_agentx/workflow/memory_drain_registry.py,sha256=Xm_ytbB-BbRIu3-h-KhptsCsmmueOiNZePUuG1hk8zk,1681
|
|
708
708
|
langchain_agentx/workflow/memory_session_policy.py,sha256=2FI0ZbAA6MnxJexnLYLadQpiqipPQwVnUrTWgl55A74,3333
|
|
709
|
-
langchain_agentx/workflow/node.py,sha256=
|
|
709
|
+
langchain_agentx/workflow/node.py,sha256=03DtGBYkpFIv_XWMwLQsgWgS0aK4eHxvMfCI5DiBEmE,5069
|
|
710
710
|
langchain_agentx/workflow/node_adapter.py,sha256=3ZjXhN-Yqg2JzYpRsvnhOvw8-gvGdzmFYkDG2AFndX4,5330
|
|
711
711
|
langchain_agentx/workflow/projection_isolation.py,sha256=kx6cM8ZtZAFA2VZ8yBIgVaIQE5o4ZfvSoY3mcxSe3hk,634
|
|
712
712
|
langchain_agentx/workflow/r8_event_stream_benchmark.py,sha256=VdDOWoK3yoGjYyVpAIN3qoQrChz6Ot8sxDw7Uh8RdhU,6562
|
|
@@ -716,7 +716,7 @@ langchain_agentx/workflow/run_session.py,sha256=LuFY52uvYfOBGD_Yi878pO5N2bVWJnVG
|
|
|
716
716
|
langchain_agentx/workflow/runtime.py,sha256=DdL66SlcvTyU9KxA-JAS4dFxWuJXlYGQdtmBmZfk3dc,3267
|
|
717
717
|
langchain_agentx/workflow/runtime_scope_path.py,sha256=k1PLnHK-xFEui6birSCWPZeKKV4_oXIip3mst_xDICo,2687
|
|
718
718
|
langchain_agentx/workflow/runtime_scope_registry.py,sha256=WgyWAWTuM6xZl6ik2pAZCWBwToeTOaSu94kYqJKY2jU,2945
|
|
719
|
-
langchain_agentx/workflow/state.py,sha256=
|
|
719
|
+
langchain_agentx/workflow/state.py,sha256=PCSqwlJIP2rMg-TFhDFK2KzWXTC2dEn1q7eVCK1z4SY,9155
|
|
720
720
|
langchain_agentx/workflow/structure_collector.py,sha256=w2ryitiR9t48DrYGBbR4zlSBOaa7dtrD0XqR23BbSHk,13574
|
|
721
721
|
langchain_agentx/workflow/structure_errors.py,sha256=yYulfRRc70RU2rKjyJYEIhIspCH5taRa72kAwl3nOAA,941
|
|
722
722
|
langchain_agentx/workflow/structure_validation.py,sha256=resy8jK_V1mKd_BQykwmpbesg6Yl2QbLavEUEQB2O_w,17698
|
|
@@ -761,8 +761,8 @@ langchain_agentx/workspace/root_grant_manager.py,sha256=W3gy8HTevbERbXqIgRcjzOXO
|
|
|
761
761
|
langchain_agentx/workspace/tool_boundary.py,sha256=UDwX6swpSLsx9HNkYuuRRiV5o7ZZG_Bru2Yn0c5kNX8,5724
|
|
762
762
|
langchain_agentx/workspace/validators.py,sha256=tQt-6TOcL8Fw7Ig5ebA9S7vGWh1rby920eFW6x8Tk9E,1439
|
|
763
763
|
langchain_agentx/workspace/view.py,sha256=PGasqTaqhlD03SXXazHuw4RHfV681AIdlsYqL70pEjc,4774
|
|
764
|
-
langchain_agentx_python-2.2.
|
|
765
|
-
langchain_agentx_python-2.2.
|
|
766
|
-
langchain_agentx_python-2.2.
|
|
767
|
-
langchain_agentx_python-2.2.
|
|
768
|
-
langchain_agentx_python-2.2.
|
|
764
|
+
langchain_agentx_python-2.2.6.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
765
|
+
langchain_agentx_python-2.2.6.dist-info/METADATA,sha256=aFUHiJcRrYfmX1zz5EcopI4JNyrKygDP2OeT6HlaEPg,24250
|
|
766
|
+
langchain_agentx_python-2.2.6.dist-info/WHEEL,sha256=51RkbunBAw4BWsgaQWTpPhg4Diwp3c9P5iaLk67Hdtg,92
|
|
767
|
+
langchain_agentx_python-2.2.6.dist-info/top_level.txt,sha256=Ge284pniNt8xea0OLk2o9o32GqVpDhOYk20fwE-0xxA,17
|
|
768
|
+
langchain_agentx_python-2.2.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{langchain_agentx_python-2.2.4.dist-info → langchain_agentx_python-2.2.6.dist-info}/top_level.txt
RENAMED
|
File without changes
|