langchain-agentx-python 2.4.7__py3-none-any.whl → 2.4.8__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/factory.py +20 -2
- langchain_agentx/loop/subagent/context.py +2 -0
- langchain_agentx/loop/subagent/runner.py +1 -1
- langchain_agentx/tool_runtime/errors.py +4 -8
- langchain_agentx/tool_runtime/loader.py +4 -1
- langchain_agentx/tool_runtime/loop_loader.py +6 -0
- langchain_agentx/tool_runtime/session_store.py +8 -0
- langchain_agentx/tools/registry_names.py +7 -0
- langchain_agentx/tools/workflow/__init__.py +18 -0
- langchain_agentx/tools/workflow/agents/__init__.py +14 -0
- langchain_agentx/tools/workflow/agents/definition.py +38 -0
- langchain_agentx/tools/workflow/agents/kernel_bridge.py +162 -0
- langchain_agentx/tools/workflow/agents/ports.py +31 -0
- langchain_agentx/tools/workflow/agents/prompt.py +35 -0
- langchain_agentx/tools/workflow/agents/structured_output.py +178 -0
- langchain_agentx/tools/workflow/agents/tool_pool.py +122 -0
- langchain_agentx/tools/workflow/agents/toolset.py +38 -0
- langchain_agentx/tools/workflow/agents/transcript_store.py +122 -0
- langchain_agentx/tools/workflow/agents/worker_runner.py +105 -0
- langchain_agentx/tools/workflow/backend.py +307 -0
- langchain_agentx/tools/workflow/constants.py +86 -0
- langchain_agentx/tools/workflow/engine/__init__.py +15 -0
- langchain_agentx/tools/workflow/engine/budget.py +28 -0
- langchain_agentx/tools/workflow/engine/cache_key.py +82 -0
- langchain_agentx/tools/workflow/engine/cancel_gate.py +72 -0
- langchain_agentx/tools/workflow/engine/concurrency.py +71 -0
- langchain_agentx/tools/workflow/engine/coordinator.py +176 -0
- langchain_agentx/tools/workflow/engine/hooks.py +401 -0
- langchain_agentx/tools/workflow/engine/journal.py +77 -0
- langchain_agentx/tools/workflow/engine/nested.py +28 -0
- langchain_agentx/tools/workflow/engine/paths.py +45 -0
- langchain_agentx/tools/workflow/engine/run_registry.py +156 -0
- langchain_agentx/tools/workflow/errors.py +53 -0
- langchain_agentx/tools/workflow/models.py +98 -0
- langchain_agentx/tools/workflow/observe/__init__.py +1 -0
- langchain_agentx/tools/workflow/observe/events.py +51 -0
- langchain_agentx/tools/workflow/observe/task_bridge.py +168 -0
- langchain_agentx/tools/workflow/prompt.py +47 -0
- langchain_agentx/tools/workflow/registry/__init__.py +6 -0
- langchain_agentx/tools/workflow/registry/bundled.py +10 -0
- langchain_agentx/tools/workflow/registry/discovery.py +88 -0
- langchain_agentx/tools/workflow/registry/models.py +17 -0
- langchain_agentx/tools/workflow/runtime/__init__.py +6 -0
- langchain_agentx/tools/workflow/runtime/entry_check.py +75 -0
- langchain_agentx/tools/workflow/runtime/executor.py +150 -0
- langchain_agentx/tools/workflow/runtime/forbidden_apis.py +104 -0
- langchain_agentx/tools/workflow/runtime/import_policy.py +70 -0
- langchain_agentx/tools/workflow/runtime/meta_validation.py +138 -0
- langchain_agentx/tools/workflow/runtime/script_gate.py +38 -0
- langchain_agentx/tools/workflow/tool.py +274 -0
- langchain_agentx/workspace/config.py +12 -0
- {langchain_agentx_python-2.4.7.dist-info → langchain_agentx_python-2.4.8.dist-info}/METADATA +1 -1
- {langchain_agentx_python-2.4.7.dist-info → langchain_agentx_python-2.4.8.dist-info}/RECORD +57 -15
- {langchain_agentx_python-2.4.7.dist-info → langchain_agentx_python-2.4.8.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-2.4.7.dist-info → langchain_agentx_python-2.4.8.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-2.4.7.dist-info → langchain_agentx_python-2.4.8.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -1593,13 +1593,25 @@ class LoopGraphBuilder:
|
|
|
1593
1593
|
patch["compaction_pipeline_meta"] = meta
|
|
1594
1594
|
meta["compact_progress"] = progress_events
|
|
1595
1595
|
|
|
1596
|
+
def _clear_file_reads_after_message_compaction(self, patch: dict[str, Any]) -> None:
|
|
1597
|
+
"""消息被 compaction 改写后清空 last_read(对齐 CC ``readFileState.clear()``)。
|
|
1598
|
+
|
|
1599
|
+
防止 Read dedup 返回 file_unchanged 却断言「内容仍在本会话」(A2)。
|
|
1600
|
+
"""
|
|
1601
|
+
if "messages" not in patch:
|
|
1602
|
+
return
|
|
1603
|
+
store = getattr(self, "_session_store", None)
|
|
1604
|
+
clearer = getattr(store, "clear_file_read_state", None)
|
|
1605
|
+
if callable(clearer):
|
|
1606
|
+
clearer()
|
|
1607
|
+
|
|
1596
1608
|
def _apply_prune_and_compress(self, state: dict[str, Any]) -> dict[str, Any]:
|
|
1597
1609
|
if self._session_memory_compact_bridge is not None:
|
|
1598
1610
|
compacted_messages = self._session_memory_compact_bridge.try_compact(
|
|
1599
1611
|
list(state.get("messages") or [])
|
|
1600
1612
|
)
|
|
1601
1613
|
if compacted_messages is not None:
|
|
1602
|
-
|
|
1614
|
+
patch = {
|
|
1603
1615
|
"messages": compacted_messages,
|
|
1604
1616
|
"collapse_retry_available": True,
|
|
1605
1617
|
"compaction_pipeline_meta": {
|
|
@@ -1607,10 +1619,13 @@ class LoopGraphBuilder:
|
|
|
1607
1619
|
"session_memory_compaction": "applied",
|
|
1608
1620
|
},
|
|
1609
1621
|
}
|
|
1622
|
+
self._clear_file_reads_after_message_compaction(patch)
|
|
1623
|
+
return patch
|
|
1610
1624
|
patch = self._loop_context_compaction.run(state, model=self._model)
|
|
1611
1625
|
compaction_meta = patch.get("compaction_pipeline_meta")
|
|
1612
1626
|
if isinstance(compaction_meta, dict):
|
|
1613
1627
|
patch["collapse_retry_available"] = bool(compaction_meta.get("tokens_freed_sum", 0))
|
|
1628
|
+
self._clear_file_reads_after_message_compaction(patch)
|
|
1614
1629
|
return patch
|
|
1615
1630
|
|
|
1616
1631
|
async def _apply_prune_and_compress_async(self, state: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -1619,7 +1634,7 @@ class LoopGraphBuilder:
|
|
|
1619
1634
|
list(state.get("messages") or [])
|
|
1620
1635
|
)
|
|
1621
1636
|
if compacted_messages is not None:
|
|
1622
|
-
|
|
1637
|
+
patch = {
|
|
1623
1638
|
"messages": compacted_messages,
|
|
1624
1639
|
"collapse_retry_available": True,
|
|
1625
1640
|
"compaction_pipeline_meta": {
|
|
@@ -1627,6 +1642,8 @@ class LoopGraphBuilder:
|
|
|
1627
1642
|
"session_memory_compaction": "applied",
|
|
1628
1643
|
},
|
|
1629
1644
|
}
|
|
1645
|
+
self._clear_file_reads_after_message_compaction(patch)
|
|
1646
|
+
return patch
|
|
1630
1647
|
progress_events: list[dict[str, Any]] = []
|
|
1631
1648
|
seen_progress = False
|
|
1632
1649
|
|
|
@@ -1660,6 +1677,7 @@ class LoopGraphBuilder:
|
|
|
1660
1677
|
compaction_meta = patch.get("compaction_pipeline_meta")
|
|
1661
1678
|
if isinstance(compaction_meta, dict):
|
|
1662
1679
|
patch["collapse_retry_available"] = bool(compaction_meta.get("tokens_freed_sum", 0))
|
|
1680
|
+
self._clear_file_reads_after_message_compaction(patch)
|
|
1663
1681
|
return patch
|
|
1664
1682
|
|
|
1665
1683
|
def _run_prune_and_compress_with_trace(self, state: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -117,6 +117,8 @@ class SubagentContext:
|
|
|
117
117
|
loop_compilation_context: LoopCompilationContext | None = None
|
|
118
118
|
can_use_tool: Callable[[str, dict[str, Any]], bool] | None = None
|
|
119
119
|
register_transcript: bool = True
|
|
120
|
+
transcript_store: Any | None = None
|
|
121
|
+
"""可选落盘后端;None → runner 使用 InMemoryTranscriptStore。Workflow 注入 workflows/<runId>/。"""
|
|
120
122
|
|
|
121
123
|
# Thinking 控制字段(request-side 生成意图,与 UI 展示意图分离)
|
|
122
124
|
thinking_generation_enabled: bool = True # 子代理 request-side thinking 生成开关
|
|
@@ -864,7 +864,7 @@ async def _run_subagent_body(
|
|
|
864
864
|
resolved_model = _resolve_subagent_model(ctx, model)
|
|
865
865
|
|
|
866
866
|
if ctx.register_transcript and ctx.agent_id:
|
|
867
|
-
store = InMemoryTranscriptStore()
|
|
867
|
+
store = getattr(ctx, "transcript_store", None) or InMemoryTranscriptStore()
|
|
868
868
|
transcript: SubagentTranscriptManager | NoOpTranscriptManager = SubagentTranscriptManager(
|
|
869
869
|
store=store,
|
|
870
870
|
agent_id=ctx.agent_id,
|
|
@@ -20,7 +20,6 @@ from typing import Literal
|
|
|
20
20
|
|
|
21
21
|
from .limits import ERROR_TRUNCATE_HALF, ERROR_TRUNCATE_THRESHOLD
|
|
22
22
|
from .messages import (
|
|
23
|
-
DENIAL_WORKAROUND_GUIDANCE,
|
|
24
23
|
INTERRUPT_MESSAGE_FOR_TOOL_USE,
|
|
25
24
|
auto_reject_message,
|
|
26
25
|
)
|
|
@@ -68,10 +67,6 @@ def truncate_error_text(text: str) -> str:
|
|
|
68
67
|
return _DEFAULT_TRUNCATOR.truncate(text)
|
|
69
68
|
|
|
70
69
|
|
|
71
|
-
def _llm_summary_already_formatted(message: str) -> bool:
|
|
72
|
-
return "STOP and explain" in message or DENIAL_WORKAROUND_GUIDANCE in message
|
|
73
|
-
|
|
74
|
-
|
|
75
70
|
# ---------------------------------------------------------------------------
|
|
76
71
|
# 框架级异常类型
|
|
77
72
|
# ---------------------------------------------------------------------------
|
|
@@ -236,12 +231,13 @@ def blocked_envelope(
|
|
|
236
231
|
"""
|
|
237
232
|
构造 status="blocked" 的 ToolResultEnvelope。
|
|
238
233
|
|
|
239
|
-
|
|
240
|
-
模型可见 summary
|
|
234
|
+
对齐 CC ``permissionDecision.message → tool_result content``:
|
|
235
|
+
模型可见 summary 优先使用决策拒因(message / llm_summary);
|
|
236
|
+
仅当拒因空时回退 AUTO_REJECT_MESSAGE(与 CC headless 无细文案时一致)。
|
|
241
237
|
"""
|
|
242
238
|
if llm_summary is not None:
|
|
243
239
|
summary_text = truncate_error_text(llm_summary)
|
|
244
|
-
elif
|
|
240
|
+
elif isinstance(message, str) and message.strip():
|
|
245
241
|
summary_text = truncate_error_text(message)
|
|
246
242
|
else:
|
|
247
243
|
summary_text = truncate_error_text(auto_reject_message(tool_name))
|
|
@@ -96,10 +96,11 @@ class ToolRuntimeLoader:
|
|
|
96
96
|
agent_home_segment: str = ".langchain_agentx",
|
|
97
97
|
include_agent: bool = True,
|
|
98
98
|
include_skill: bool = False,
|
|
99
|
+
include_workflow: bool = False,
|
|
99
100
|
agent_registry: Any | None = None,
|
|
100
101
|
) -> "ToolRuntimeLoader":
|
|
101
102
|
"""
|
|
102
|
-
注册默认工具集(Read/Grep/Glob/Bash + 可选 Agent/Skill)。
|
|
103
|
+
注册默认工具集(Read/Grep/Glob/Bash + 可选 Agent/Skill/Workflow)。
|
|
103
104
|
|
|
104
105
|
向后兼容:在 loader 自身 registry 上注册(单 loop 模式)。
|
|
105
106
|
create_loop_agent 应优先使用 ``spawn_loop_loader`` 获得 per-loop 隔离。
|
|
@@ -109,6 +110,7 @@ class ToolRuntimeLoader:
|
|
|
109
110
|
agent_home_segment: agent home 子目录名(如 .claude、.cursor)
|
|
110
111
|
include_agent: 是否包含 Agent 工具
|
|
111
112
|
include_skill: 是否包含 Skill 工具
|
|
113
|
+
include_workflow: 是否包含 Workflow 工具(默认 False,结项前不进默认池)
|
|
112
114
|
agent_registry: 可选的自定义 agent 注册表
|
|
113
115
|
"""
|
|
114
116
|
loop = self.spawn_loop_loader(
|
|
@@ -118,6 +120,7 @@ class ToolRuntimeLoader:
|
|
|
118
120
|
loop.register_default_tools(
|
|
119
121
|
include_agent=include_agent,
|
|
120
122
|
include_skill=include_skill,
|
|
123
|
+
include_workflow=include_workflow,
|
|
121
124
|
agent_registry=agent_registry,
|
|
122
125
|
)
|
|
123
126
|
for tool in loop.registry.list():
|
|
@@ -86,6 +86,7 @@ class LoopLoader:
|
|
|
86
86
|
*,
|
|
87
87
|
include_agent: bool = True,
|
|
88
88
|
include_skill: bool = False,
|
|
89
|
+
include_workflow: bool = False,
|
|
89
90
|
agent_registry: Any | None = None,
|
|
90
91
|
) -> "LoopLoader":
|
|
91
92
|
from pathlib import Path
|
|
@@ -198,6 +199,11 @@ class LoopLoader:
|
|
|
198
199
|
)
|
|
199
200
|
)
|
|
200
201
|
|
|
202
|
+
if include_workflow:
|
|
203
|
+
from langchain_agentx.tools.workflow import WorkflowRuntimeTool
|
|
204
|
+
|
|
205
|
+
self.register(WorkflowRuntimeTool(config=cfg))
|
|
206
|
+
|
|
201
207
|
return self
|
|
202
208
|
|
|
203
209
|
def attach_mcp(
|
|
@@ -163,6 +163,14 @@ class AgentSessionStore:
|
|
|
163
163
|
"""用与 LangGraph state 中 last_read 条目同构的 dict 覆盖该路径。"""
|
|
164
164
|
self._file_read_state[path] = FileReadRecord.from_bridge_dict(path, entry)
|
|
165
165
|
|
|
166
|
+
def clear_file_read_state(self) -> None:
|
|
167
|
+
"""清空文件读缓存(对齐 CC ``readFileState.clear()`` on compact)。
|
|
168
|
+
|
|
169
|
+
compaction 蒸发消息后若不清理,Read dedup 会返回 ``file_unchanged`` stub
|
|
170
|
+
却断言「内容仍在本会话」(BUG-20261107-28 A2)。
|
|
171
|
+
"""
|
|
172
|
+
self._file_read_state.clear()
|
|
173
|
+
|
|
166
174
|
def replace_all_file_reads_from_bridge_map(self, data: dict[str, Any]) -> None:
|
|
167
175
|
"""整体替换文件读映射(对应 ToolStateBridge.set_last_read_map)。"""
|
|
168
176
|
self._file_read_state.clear()
|
|
@@ -44,6 +44,7 @@ TaskUpdate = "TaskUpdate"
|
|
|
44
44
|
WebSearch = "WebSearch"
|
|
45
45
|
WebFetch = "WebFetch"
|
|
46
46
|
Agent = "Agent"
|
|
47
|
+
Workflow = "Workflow"
|
|
47
48
|
|
|
48
49
|
# CC SAFE_YOLO 中尚未在 register_default_tools 注册的工具(白名单可预留)
|
|
49
50
|
CC_ALLOWLIST_UNREGISTERED: frozenset[str] = frozenset({
|
|
@@ -120,6 +121,7 @@ class ToolNameRegistry:
|
|
|
120
121
|
*,
|
|
121
122
|
include_agent: bool = True,
|
|
122
123
|
include_skill: bool = False,
|
|
124
|
+
include_workflow: bool = False,
|
|
123
125
|
) -> frozenset[str]:
|
|
124
126
|
"""register_default_tools 目标注册名集(CC PascalCase)。"""
|
|
125
127
|
names = {
|
|
@@ -143,6 +145,8 @@ class ToolNameRegistry:
|
|
|
143
145
|
names.add(Agent)
|
|
144
146
|
if include_skill:
|
|
145
147
|
names.add(Skill)
|
|
148
|
+
if include_workflow:
|
|
149
|
+
names.add(Workflow)
|
|
146
150
|
return frozenset(names)
|
|
147
151
|
|
|
148
152
|
@staticmethod
|
|
@@ -162,11 +166,13 @@ class ToolNameRegistry:
|
|
|
162
166
|
*,
|
|
163
167
|
include_agent: bool = True,
|
|
164
168
|
include_skill: bool = False,
|
|
169
|
+
include_workflow: bool = False,
|
|
165
170
|
) -> frozenset[str]:
|
|
166
171
|
"""白名单中已注册工具子集(排除 TodoWrite / TaskStop / TaskOutput)。"""
|
|
167
172
|
registered = ToolNameRegistry.cc_default_registered_names(
|
|
168
173
|
include_agent=include_agent,
|
|
169
174
|
include_skill=include_skill,
|
|
175
|
+
include_workflow=include_workflow,
|
|
170
176
|
)
|
|
171
177
|
return ToolNameRegistry.cc_auto_mode_allowlist_full() & registered
|
|
172
178
|
|
|
@@ -221,5 +227,6 @@ __all__ = [
|
|
|
221
227
|
"ToolNameRegistry",
|
|
222
228
|
"WebFetch",
|
|
223
229
|
"WebSearch",
|
|
230
|
+
"Workflow",
|
|
224
231
|
"Write",
|
|
225
232
|
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/workflow — CC 对齐 Workflow RuntimeTool 包根
|
|
3
|
+
|
|
4
|
+
职责:Python 脚本编排 + 私有 hooks;工人经 kernel_bridge → run_subagent。
|
|
5
|
+
链路位置:可选挂入默认工具集(include_workflow=False 直至结项)。
|
|
6
|
+
当前裁剪:Phase 3 门面已导出;真内核工人在 Phase 4。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from langchain_agentx.tools.workflow.constants import WORKFLOW_TOOL_NAME
|
|
10
|
+
from langchain_agentx.tools.workflow.runtime import ScriptGate, TrustedWorkflowExecutor
|
|
11
|
+
from langchain_agentx.tools.workflow.tool import WorkflowRuntimeTool
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"WORKFLOW_TOOL_NAME",
|
|
15
|
+
"ScriptGate",
|
|
16
|
+
"TrustedWorkflowExecutor",
|
|
17
|
+
"WorkflowRuntimeTool",
|
|
18
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""tools/workflow/agents — 工人组装层(L4)。"""
|
|
2
|
+
|
|
3
|
+
from langchain_agentx.tools.workflow.agents.definition import register_workflow_subagent
|
|
4
|
+
from langchain_agentx.tools.workflow.agents.kernel_bridge import KernelBridge
|
|
5
|
+
from langchain_agentx.tools.workflow.agents.ports import FakeWorkflowWorker, WorkflowWorkerPort
|
|
6
|
+
from langchain_agentx.tools.workflow.agents.worker_runner import WorkflowWorkerRunner
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"FakeWorkflowWorker",
|
|
10
|
+
"KernelBridge",
|
|
11
|
+
"WorkflowWorkerPort",
|
|
12
|
+
"WorkflowWorkerRunner",
|
|
13
|
+
"register_workflow_subagent",
|
|
14
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/workflow/agents/definition.py — 注册 workflow-subagent 到统一定义表
|
|
3
|
+
|
|
4
|
+
职责:E1 — SubagentConfig 写入 get_global_registry();默认 acceptEdits。
|
|
5
|
+
链路位置:包加载或 Backend 启动时 register_workflow_subagent()。
|
|
6
|
+
当前裁剪:不 import AgentRuntimeTool;仅注册侧依赖 registry。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from langchain_agentx.tools.agent.registry.config import SubagentConfig
|
|
12
|
+
from langchain_agentx.tools.agent.registry.registry import SubagentRegistry, get_global_registry
|
|
13
|
+
from langchain_agentx.tools.workflow.agents.prompt import workflow_subagent_system_prompt
|
|
14
|
+
from langchain_agentx.tools.workflow.agents.toolset import filter_workflow_worker_tools
|
|
15
|
+
from langchain_agentx.tools.workflow.constants import WORKFLOW_SUBAGENT_AGENT_TYPE
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_workflow_subagent_config() -> SubagentConfig:
|
|
19
|
+
return SubagentConfig(
|
|
20
|
+
name=WORKFLOW_SUBAGENT_AGENT_TYPE,
|
|
21
|
+
description="Internal subagent for workflow script orchestration.",
|
|
22
|
+
when_to_use="Internal subagent for workflow script orchestration.",
|
|
23
|
+
system_prompt_factory=lambda: workflow_subagent_system_prompt(has_schema=False),
|
|
24
|
+
tool_filter=filter_workflow_worker_tools,
|
|
25
|
+
tools_description="All tools except SendUserMessage/Agent/Workflow",
|
|
26
|
+
allow_async=False,
|
|
27
|
+
is_builtin=True,
|
|
28
|
+
permission_mode="acceptEdits",
|
|
29
|
+
tags={"workflow", "internal"},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register_workflow_subagent(registry: SubagentRegistry | None = None) -> SubagentConfig:
|
|
34
|
+
"""幂等注册 workflow-subagent。"""
|
|
35
|
+
target = registry if registry is not None else get_global_registry()
|
|
36
|
+
cfg = build_workflow_subagent_config()
|
|
37
|
+
target.register(cfg)
|
|
38
|
+
return cfg
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/workflow/agents/kernel_bridge.py — 唯一触碰 run_subagent 的薄封装(E2)
|
|
3
|
+
|
|
4
|
+
职责:填充 SubagentContext(acceptEdits / prompt / worktree)并调用 run_subagent;
|
|
5
|
+
工人工具池按父当前表派生(含会话 MCP concat);transcript 落到 workflows/<runId>/;
|
|
6
|
+
R1-A:run 前走 hooks pipeline(全局/registered 继承;父 session 槽不复制)。
|
|
7
|
+
链路位置:WorkflowWorkerRunner → KernelBridge → loop.subagent.runner.run_subagent。
|
|
8
|
+
当前裁剪:禁 Orchestrator / AgentRuntimeTool;noop setAppState 语义由不写父 state 体现;
|
|
9
|
+
不传 allowedTools 替换(继承父权限规则)。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any, Callable, Awaitable
|
|
16
|
+
|
|
17
|
+
from langchain_agentx.loop.runtime.context import SubagentContextOverrides
|
|
18
|
+
from langchain_agentx.loop.runtime.subagent_execution_paths import SubagentExecutionPaths
|
|
19
|
+
from langchain_agentx.loop.subagent.context import create_subagent_context
|
|
20
|
+
from langchain_agentx.loop.subagent.hooks_pipeline import apply_subagent_hooks_pipeline_async
|
|
21
|
+
from langchain_agentx.loop.subagent.runner import run_subagent
|
|
22
|
+
from langchain_agentx.tools.agent.registry.registry import get_global_registry
|
|
23
|
+
from langchain_agentx.tools.workflow.agents.prompt import workflow_subagent_system_prompt
|
|
24
|
+
from langchain_agentx.tools.workflow.agents.tool_pool import assemble_workflow_worker_tool_pool
|
|
25
|
+
from langchain_agentx.tools.workflow.agents.transcript_store import WorkflowRunTranscriptStore
|
|
26
|
+
from langchain_agentx.tools.workflow.constants import WORKFLOW_SUBAGENT_AGENT_TYPE
|
|
27
|
+
from langchain_agentx.tools.workflow.engine.paths import RunPathFactory
|
|
28
|
+
from langchain_agentx.tools.workflow.models import AgentRunResult, AgentSpec
|
|
29
|
+
from langchain_agentx.workspace import resolve_agent_state_config
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
RunSubagentFn = Callable[..., Awaitable[Any]]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class KernelBridgeRequest:
|
|
37
|
+
parent_ctx: Any
|
|
38
|
+
loader: Any
|
|
39
|
+
spec: AgentSpec
|
|
40
|
+
agent_index: int
|
|
41
|
+
run_id: str
|
|
42
|
+
workflow_name: str | None = None
|
|
43
|
+
model: Any | None = None
|
|
44
|
+
permission_mode: str = "acceptEdits"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class KernelBridge:
|
|
48
|
+
"""直调 run_subagent;可注入 run_fn 供单测 mock。"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, *, run_fn: RunSubagentFn | None = None) -> None:
|
|
51
|
+
self._run_fn = run_fn or run_subagent
|
|
52
|
+
|
|
53
|
+
async def run(self, req: KernelBridgeRequest) -> AgentRunResult:
|
|
54
|
+
has_schema = req.spec.schema is not None
|
|
55
|
+
system_prompt = workflow_subagent_system_prompt(has_schema=has_schema)
|
|
56
|
+
agent_type = req.spec.agent_type or WORKFLOW_SUBAGENT_AGENT_TYPE
|
|
57
|
+
isolation = req.spec.isolation or "none"
|
|
58
|
+
|
|
59
|
+
pool = assemble_workflow_worker_tool_pool(
|
|
60
|
+
parent_ctx=req.parent_ctx,
|
|
61
|
+
loader=req.loader,
|
|
62
|
+
headless=True,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
overrides = SubagentContextOverrides(
|
|
66
|
+
isolation=isolation,
|
|
67
|
+
cwd=None,
|
|
68
|
+
)
|
|
69
|
+
path_prep = SubagentExecutionPaths.prepare(
|
|
70
|
+
parent_ctx=req.parent_ctx,
|
|
71
|
+
overrides=overrides,
|
|
72
|
+
)
|
|
73
|
+
parent_depth = int(getattr(req.parent_ctx, "agent_depth", 0) or 0)
|
|
74
|
+
spawn_depth = parent_depth + 1
|
|
75
|
+
ctx = create_subagent_context(
|
|
76
|
+
parent_ctx=req.parent_ctx,
|
|
77
|
+
task=req.spec.prompt,
|
|
78
|
+
subagent_type=agent_type,
|
|
79
|
+
system_prompt=system_prompt,
|
|
80
|
+
isolation=isolation,
|
|
81
|
+
path_prep=path_prep,
|
|
82
|
+
permission_mode=req.permission_mode,
|
|
83
|
+
agent_depth=spawn_depth,
|
|
84
|
+
is_async=False,
|
|
85
|
+
model=req.model,
|
|
86
|
+
available_tools=pool.tools,
|
|
87
|
+
inherit_hooks=True,
|
|
88
|
+
)
|
|
89
|
+
# R1-A(bug1.md):与普通 Agent/fork 共用 hooks 解析——全局 PreToolUse 仍跑;
|
|
90
|
+
# 父 session 槽不经 compose(不复制 hooks_snapshot);自挂走 definition frontmatter。
|
|
91
|
+
await apply_subagent_hooks_pipeline_async(
|
|
92
|
+
ctx,
|
|
93
|
+
req.parent_ctx,
|
|
94
|
+
subagent_type=agent_type,
|
|
95
|
+
subagent_config=self._resolve_subagent_config(agent_type),
|
|
96
|
+
)
|
|
97
|
+
# 扩展字段 + 落盘 store(runner 消费 transcript_store / workflow_run_id)
|
|
98
|
+
setattr(ctx, "workflow_run_id", req.run_id)
|
|
99
|
+
if req.workflow_name:
|
|
100
|
+
setattr(ctx, "workflow_name", req.workflow_name)
|
|
101
|
+
store = self._build_transcript_store(
|
|
102
|
+
req=req,
|
|
103
|
+
agent_type=agent_type,
|
|
104
|
+
spawn_depth=spawn_depth,
|
|
105
|
+
agent_id=getattr(ctx, "agent_id", None),
|
|
106
|
+
)
|
|
107
|
+
if store is not None:
|
|
108
|
+
ctx.transcript_store = store
|
|
109
|
+
|
|
110
|
+
result = await self._run_fn(
|
|
111
|
+
ctx,
|
|
112
|
+
pool.loader,
|
|
113
|
+
model=req.model or getattr(ctx, "model", None),
|
|
114
|
+
allowed_tool_names=set(pool.allowed_tool_names),
|
|
115
|
+
)
|
|
116
|
+
content = getattr(result, "content", "") or ""
|
|
117
|
+
status = getattr(result, "status", "completed")
|
|
118
|
+
if status == "error":
|
|
119
|
+
raise RuntimeError(getattr(result, "error_message", None) or content or "subagent error")
|
|
120
|
+
tokens = 0
|
|
121
|
+
trace = getattr(result, "execution_trace", None)
|
|
122
|
+
if trace is not None:
|
|
123
|
+
tokens = int(getattr(trace, "total_tokens", 0) or 0)
|
|
124
|
+
tool_calls = int(getattr(result, "total_steps", 0) or 0)
|
|
125
|
+
return AgentRunResult(value=content, tokens=tokens, tool_calls=tool_calls)
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _resolve_subagent_config(agent_type: str) -> Any | None:
|
|
129
|
+
"""统一定义表项(含可选 frontmatter hooks);缺省不影响继承 compose。"""
|
|
130
|
+
try:
|
|
131
|
+
return get_global_registry().get(agent_type)
|
|
132
|
+
except KeyError:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _build_transcript_store(
|
|
137
|
+
*,
|
|
138
|
+
req: KernelBridgeRequest,
|
|
139
|
+
agent_type: str,
|
|
140
|
+
spawn_depth: int,
|
|
141
|
+
agent_id: str | None,
|
|
142
|
+
) -> WorkflowRunTranscriptStore | None:
|
|
143
|
+
workspace_root = getattr(req.parent_ctx, "workspace_root", None) or ""
|
|
144
|
+
if not str(workspace_root).strip():
|
|
145
|
+
return None
|
|
146
|
+
agent_home = (
|
|
147
|
+
getattr(req.parent_ctx, "agent_home_segment", None) or ".langchain_agentx"
|
|
148
|
+
)
|
|
149
|
+
cfg = resolve_agent_state_config(
|
|
150
|
+
workspace_root=str(workspace_root),
|
|
151
|
+
agent_home_segment=str(agent_home),
|
|
152
|
+
)
|
|
153
|
+
paths = RunPathFactory(cfg).for_run(req.run_id)
|
|
154
|
+
store = WorkflowRunTranscriptStore(
|
|
155
|
+
paths.transcript_dir,
|
|
156
|
+
run_id=req.run_id,
|
|
157
|
+
agent_type=agent_type,
|
|
158
|
+
spawn_depth=spawn_depth,
|
|
159
|
+
workflow_name=req.workflow_name,
|
|
160
|
+
)
|
|
161
|
+
store.write_meta(agent_id=agent_id if isinstance(agent_id, str) else None)
|
|
162
|
+
return store
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/workflow/agents/ports.py — WorkflowWorkerPort Protocol
|
|
3
|
+
|
|
4
|
+
职责:hooks → 工人执行的唯一出口接口(Phase 1 仅接口 + 假实现)。
|
|
5
|
+
链路位置:engine.hooks 依赖本 Protocol;真实现见 worker_runner(P4)。
|
|
6
|
+
当前裁剪:不含 journal/cache。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
from langchain_agentx.tools.workflow.models import AgentRunResult, AgentSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class WorkflowWorkerPort(Protocol):
|
|
18
|
+
async def run(self, spec: AgentSpec, agent_index: int) -> AgentRunResult:
|
|
19
|
+
"""执行一个工人;抛出异常由 hooks 决定是否变 null 槽。"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FakeWorkflowWorker:
|
|
23
|
+
"""固定返回字符串的假工人(Phase 1/2)。"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, reply: str = "fake-worker-ok") -> None:
|
|
26
|
+
self.reply = reply
|
|
27
|
+
self.calls: list[AgentSpec] = []
|
|
28
|
+
|
|
29
|
+
async def run(self, spec: AgentSpec, agent_index: int) -> AgentRunResult:
|
|
30
|
+
self.calls.append(spec)
|
|
31
|
+
return AgentRunResult(value=f"{self.reply}:{agent_index}:{spec.prompt[:32]}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/workflow/agents/prompt.py — workflow-subagent 系统提示(e9g / n9g)
|
|
3
|
+
|
|
4
|
+
职责:无 schema / 有 schema 两套逐字提示。
|
|
5
|
+
链路位置:definition / KernelBridge 组装。
|
|
6
|
+
当前裁剪:不实现 wVr 后置注释器。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
STRUCTURED_OUTPUT_TOOL_NAME = "StructuredOutput"
|
|
12
|
+
|
|
13
|
+
# EVIDENCE: hooks-impl.txt e9g — verbatim (CC workflowSubagent.ts).
|
|
14
|
+
PLAIN_RETURN_SYSTEM_PROMPT = """\
|
|
15
|
+
You are a subagent spawned by a workflow orchestration script. Use the tools available to complete the task.
|
|
16
|
+
|
|
17
|
+
CRITICAL: Your final text response is returned **verbatim** as a string to the calling script — it is your return value, not a message to a human.
|
|
18
|
+
- Output the literal result (data, JSON, text). Do NOT output confirmations like "Done." or "Sent."
|
|
19
|
+
- If asked for JSON, return ONLY the raw JSON — no code fences, no prose, no markdown.
|
|
20
|
+
- Do NOT use SendUserMessage to deliver your answer. Put your answer in your final text response.
|
|
21
|
+
- Be concise. The script will parse your output."""
|
|
22
|
+
|
|
23
|
+
# EVIDENCE: hooks-impl.txt n9g — verbatim.
|
|
24
|
+
STRUCTURED_RETURN_SYSTEM_PROMPT = f"""\
|
|
25
|
+
You are a subagent spawned by a workflow orchestration script. Use the tools available to complete the task.
|
|
26
|
+
|
|
27
|
+
CRITICAL: You MUST call the {STRUCTURED_OUTPUT_TOOL_NAME} tool exactly once to return your final answer. The tool's input schema defines the required shape.
|
|
28
|
+
- Do your work (Read files, run commands, etc.), then call {STRUCTURED_OUTPUT_TOOL_NAME} with your answer.
|
|
29
|
+
- Do NOT put your answer in a text response. The script reads ONLY the {STRUCTURED_OUTPUT_TOOL_NAME} tool call.
|
|
30
|
+
- If the schema validation fails, read the error and call {STRUCTURED_OUTPUT_TOOL_NAME} again with a corrected shape.
|
|
31
|
+
- After calling {STRUCTURED_OUTPUT_TOOL_NAME} successfully, end your turn. No acknowledgment needed."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def workflow_subagent_system_prompt(*, has_schema: bool) -> str:
|
|
35
|
+
return STRUCTURED_RETURN_SYSTEM_PROMPT if has_schema else PLAIN_RETURN_SYSTEM_PROMPT
|