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.
Files changed (57) hide show
  1. langchain_agentx/__init__.py +1 -1
  2. langchain_agentx/loop/graph/factory.py +20 -2
  3. langchain_agentx/loop/subagent/context.py +2 -0
  4. langchain_agentx/loop/subagent/runner.py +1 -1
  5. langchain_agentx/tool_runtime/errors.py +4 -8
  6. langchain_agentx/tool_runtime/loader.py +4 -1
  7. langchain_agentx/tool_runtime/loop_loader.py +6 -0
  8. langchain_agentx/tool_runtime/session_store.py +8 -0
  9. langchain_agentx/tools/registry_names.py +7 -0
  10. langchain_agentx/tools/workflow/__init__.py +18 -0
  11. langchain_agentx/tools/workflow/agents/__init__.py +14 -0
  12. langchain_agentx/tools/workflow/agents/definition.py +38 -0
  13. langchain_agentx/tools/workflow/agents/kernel_bridge.py +162 -0
  14. langchain_agentx/tools/workflow/agents/ports.py +31 -0
  15. langchain_agentx/tools/workflow/agents/prompt.py +35 -0
  16. langchain_agentx/tools/workflow/agents/structured_output.py +178 -0
  17. langchain_agentx/tools/workflow/agents/tool_pool.py +122 -0
  18. langchain_agentx/tools/workflow/agents/toolset.py +38 -0
  19. langchain_agentx/tools/workflow/agents/transcript_store.py +122 -0
  20. langchain_agentx/tools/workflow/agents/worker_runner.py +105 -0
  21. langchain_agentx/tools/workflow/backend.py +307 -0
  22. langchain_agentx/tools/workflow/constants.py +86 -0
  23. langchain_agentx/tools/workflow/engine/__init__.py +15 -0
  24. langchain_agentx/tools/workflow/engine/budget.py +28 -0
  25. langchain_agentx/tools/workflow/engine/cache_key.py +82 -0
  26. langchain_agentx/tools/workflow/engine/cancel_gate.py +72 -0
  27. langchain_agentx/tools/workflow/engine/concurrency.py +71 -0
  28. langchain_agentx/tools/workflow/engine/coordinator.py +176 -0
  29. langchain_agentx/tools/workflow/engine/hooks.py +401 -0
  30. langchain_agentx/tools/workflow/engine/journal.py +77 -0
  31. langchain_agentx/tools/workflow/engine/nested.py +28 -0
  32. langchain_agentx/tools/workflow/engine/paths.py +45 -0
  33. langchain_agentx/tools/workflow/engine/run_registry.py +156 -0
  34. langchain_agentx/tools/workflow/errors.py +53 -0
  35. langchain_agentx/tools/workflow/models.py +98 -0
  36. langchain_agentx/tools/workflow/observe/__init__.py +1 -0
  37. langchain_agentx/tools/workflow/observe/events.py +51 -0
  38. langchain_agentx/tools/workflow/observe/task_bridge.py +168 -0
  39. langchain_agentx/tools/workflow/prompt.py +47 -0
  40. langchain_agentx/tools/workflow/registry/__init__.py +6 -0
  41. langchain_agentx/tools/workflow/registry/bundled.py +10 -0
  42. langchain_agentx/tools/workflow/registry/discovery.py +88 -0
  43. langchain_agentx/tools/workflow/registry/models.py +17 -0
  44. langchain_agentx/tools/workflow/runtime/__init__.py +6 -0
  45. langchain_agentx/tools/workflow/runtime/entry_check.py +75 -0
  46. langchain_agentx/tools/workflow/runtime/executor.py +150 -0
  47. langchain_agentx/tools/workflow/runtime/forbidden_apis.py +104 -0
  48. langchain_agentx/tools/workflow/runtime/import_policy.py +70 -0
  49. langchain_agentx/tools/workflow/runtime/meta_validation.py +138 -0
  50. langchain_agentx/tools/workflow/runtime/script_gate.py +38 -0
  51. langchain_agentx/tools/workflow/tool.py +274 -0
  52. langchain_agentx/workspace/config.py +12 -0
  53. {langchain_agentx_python-2.4.7.dist-info → langchain_agentx_python-2.4.8.dist-info}/METADATA +1 -1
  54. {langchain_agentx_python-2.4.7.dist-info → langchain_agentx_python-2.4.8.dist-info}/RECORD +57 -15
  55. {langchain_agentx_python-2.4.7.dist-info → langchain_agentx_python-2.4.8.dist-info}/LICENSE +0 -0
  56. {langchain_agentx_python-2.4.7.dist-info → langchain_agentx_python-2.4.8.dist-info}/WHEEL +0 -0
  57. {langchain_agentx_python-2.4.7.dist-info → langchain_agentx_python-2.4.8.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,7 @@ from langchain_agentx import create_loop_agent
11
11
  ```
12
12
  """
13
13
 
14
- __version__ = "2.4.7"
14
+ __version__ = "2.4.8"
15
15
 
16
16
  from .mcp.call import McpToolCaller
17
17
  from .mcp.client_manager import McpClientManager, McpConnectedClient
@@ -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
- return {
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
- return {
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
- check_permissions() 返回 deny 时由 pipeline 调用。
240
- 模型可见 summary 默认使用 AUTO_REJECT_MESSAGE;L3 已格式化的拒因可经 llm_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 _llm_summary_already_formatted(message):
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