langchain-agentx-python 2.3.6__py3-none-any.whl → 2.3.7__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 (29) hide show
  1. langchain_agentx/__init__.py +75 -75
  2. langchain_agentx/command/prompt_shell.py +123 -123
  3. langchain_agentx/loop/graph/factory.py +11 -1
  4. langchain_agentx/loop/hook/agent_frontmatter_registration.py +51 -0
  5. langchain_agentx/loop/hook/registry.py +42 -0
  6. langchain_agentx/loop/subagent/context.py +11 -0
  7. langchain_agentx/loop/subagent/forked_runner.py +10 -0
  8. langchain_agentx/loop/subagent/graph.py +1 -0
  9. langchain_agentx/loop/subagent/hook_attachment_messages.py +46 -0
  10. langchain_agentx/loop/subagent/hooks_compose.py +74 -0
  11. langchain_agentx/loop/subagent/hooks_pipeline.py +181 -0
  12. langchain_agentx/loop/subagent/orchestrator.py +56 -0
  13. langchain_agentx/loop/subagent/parent_hook_snapshot.py +61 -0
  14. langchain_agentx/loop/subagent/subagent_start_hooks.py +39 -0
  15. langchain_agentx/tool_runtime/adapter.py +7 -0
  16. langchain_agentx/tool_runtime/models.py +2 -0
  17. langchain_agentx/tool_runtime/policy_decorator.py +70 -70
  18. langchain_agentx/tool_runtime/prompt/session_mode.py +19 -0
  19. langchain_agentx/tools/agent/loader.py +5 -0
  20. langchain_agentx/tools/agent/registry/config.py +4 -0
  21. langchain_agentx/tools/skill/fork.py +3 -0
  22. langchain_agentx/tools/skill/hook_registration.py +98 -25
  23. langchain_agentx/tools/skill/tool.py +7 -6
  24. langchain_agentx/workspace/path_canonical.py +24 -24
  25. {langchain_agentx_python-2.3.6.dist-info → langchain_agentx_python-2.3.7.dist-info}/METADATA +583 -583
  26. {langchain_agentx_python-2.3.6.dist-info → langchain_agentx_python-2.3.7.dist-info}/RECORD +29 -23
  27. {langchain_agentx_python-2.3.6.dist-info → langchain_agentx_python-2.3.7.dist-info}/LICENSE +0 -0
  28. {langchain_agentx_python-2.3.6.dist-info → langchain_agentx_python-2.3.7.dist-info}/WHEEL +0 -0
  29. {langchain_agentx_python-2.3.6.dist-info → langchain_agentx_python-2.3.7.dist-info}/top_level.txt +0 -0
@@ -1,75 +1,75 @@
1
- """
2
- LangChain-based agent utilities for CodeBaseX.
3
-
4
- This package provides:
5
- - LangChain-AgentX LangGraph agent factory (`loop`)
6
-
7
- Typical usage:
8
-
9
- ```python
10
- from langchain_agentx import create_loop_agent
11
- ```
12
- """
13
-
14
- __version__ = "2.3.6"
15
-
16
- from .mcp.call import McpToolCaller
17
- from .mcp.client_manager import McpClientManager, McpConnectedClient
18
- from .mcp.controller import McpRuntimeController
19
- from .mcp.config.resolver import McpConfigResolver
20
- from .mcp.events import CollectingEventSink
21
- from .loop import ( # noqa: F401
22
- create_loop_agent,
23
- )
24
-
25
- from .loop.hook import ( # noqa: F401
26
- ATTACHMENT_HOOK_BLOCKING_ERROR,
27
- ATTACHMENT_HOOK_CANCELLED,
28
- ATTACHMENT_HOOK_NON_BLOCKING_ERROR,
29
- ATTACHMENT_HOOK_STOPPED_CONTINUATION,
30
- ATTACHMENT_HOOK_SUCCESS,
31
- )
32
- from .observability import ( # noqa: F401
33
- EventAdapterState,
34
- LangchainAgentEvent,
35
- LangchainAgentEventType,
36
- LangGraphToLangchainAgentEventAdapter,
37
- TraceCollector,
38
- TraceSession,
39
- TraceSpan,
40
- )
41
- from .provider import CompatibleChatOpenAI # noqa: F401
42
- from .session import AgentSession, create_agent_session # noqa: F401
43
- from .health import HealthCheck, HealthReport, PreflightResult, preflight, sdk_health # noqa: F401
44
- from .workflow import BaseWorkflow # noqa: F401
45
-
46
- __all__ = [
47
- "ATTACHMENT_HOOK_BLOCKING_ERROR",
48
- "ATTACHMENT_HOOK_CANCELLED",
49
- "ATTACHMENT_HOOK_NON_BLOCKING_ERROR",
50
- "ATTACHMENT_HOOK_STOPPED_CONTINUATION",
51
- "ATTACHMENT_HOOK_SUCCESS",
52
- "create_loop_agent",
53
- "create_agent_session",
54
- "AgentSession",
55
- "CollectingEventSink",
56
- "McpClientManager",
57
- "McpConfigResolver",
58
- "McpConnectedClient",
59
- "McpRuntimeController",
60
- "McpToolCaller",
61
- "BaseWorkflow",
62
- "EventAdapterState",
63
- "LangchainAgentEvent",
64
- "LangchainAgentEventType",
65
- "LangGraphToLangchainAgentEventAdapter",
66
- "TraceCollector",
67
- "TraceSession",
68
- "TraceSpan",
69
- "CompatibleChatOpenAI",
70
- "HealthCheck",
71
- "HealthReport",
72
- "PreflightResult",
73
- "preflight",
74
- "sdk_health",
75
- ]
1
+ """
2
+ LangChain-based agent utilities for CodeBaseX.
3
+
4
+ This package provides:
5
+ - LangChain-AgentX LangGraph agent factory (`loop`)
6
+
7
+ Typical usage:
8
+
9
+ ```python
10
+ from langchain_agentx import create_loop_agent
11
+ ```
12
+ """
13
+
14
+ __version__ = "2.3.7"
15
+
16
+ from .mcp.call import McpToolCaller
17
+ from .mcp.client_manager import McpClientManager, McpConnectedClient
18
+ from .mcp.controller import McpRuntimeController
19
+ from .mcp.config.resolver import McpConfigResolver
20
+ from .mcp.events import CollectingEventSink
21
+ from .loop import ( # noqa: F401
22
+ create_loop_agent,
23
+ )
24
+
25
+ from .loop.hook import ( # noqa: F401
26
+ ATTACHMENT_HOOK_BLOCKING_ERROR,
27
+ ATTACHMENT_HOOK_CANCELLED,
28
+ ATTACHMENT_HOOK_NON_BLOCKING_ERROR,
29
+ ATTACHMENT_HOOK_STOPPED_CONTINUATION,
30
+ ATTACHMENT_HOOK_SUCCESS,
31
+ )
32
+ from .observability import ( # noqa: F401
33
+ EventAdapterState,
34
+ LangchainAgentEvent,
35
+ LangchainAgentEventType,
36
+ LangGraphToLangchainAgentEventAdapter,
37
+ TraceCollector,
38
+ TraceSession,
39
+ TraceSpan,
40
+ )
41
+ from .provider import CompatibleChatOpenAI # noqa: F401
42
+ from .session import AgentSession, create_agent_session # noqa: F401
43
+ from .health import HealthCheck, HealthReport, PreflightResult, preflight, sdk_health # noqa: F401
44
+ from .workflow import BaseWorkflow # noqa: F401
45
+
46
+ __all__ = [
47
+ "ATTACHMENT_HOOK_BLOCKING_ERROR",
48
+ "ATTACHMENT_HOOK_CANCELLED",
49
+ "ATTACHMENT_HOOK_NON_BLOCKING_ERROR",
50
+ "ATTACHMENT_HOOK_STOPPED_CONTINUATION",
51
+ "ATTACHMENT_HOOK_SUCCESS",
52
+ "create_loop_agent",
53
+ "create_agent_session",
54
+ "AgentSession",
55
+ "CollectingEventSink",
56
+ "McpClientManager",
57
+ "McpConfigResolver",
58
+ "McpConnectedClient",
59
+ "McpRuntimeController",
60
+ "McpToolCaller",
61
+ "BaseWorkflow",
62
+ "EventAdapterState",
63
+ "LangchainAgentEvent",
64
+ "LangchainAgentEventType",
65
+ "LangGraphToLangchainAgentEventAdapter",
66
+ "TraceCollector",
67
+ "TraceSession",
68
+ "TraceSpan",
69
+ "CompatibleChatOpenAI",
70
+ "HealthCheck",
71
+ "HealthReport",
72
+ "PreflightResult",
73
+ "preflight",
74
+ "sdk_health",
75
+ ]
@@ -1,123 +1,123 @@
1
- """
2
- command/prompt_shell.py — Prompt 内嵌 shell 命令执行(CC executeShellCommandsInPrompt 对齐)。
3
-
4
- 职责:
5
- 解析并执行 prompt 中的 !`command` 占位,将输出内联替换;执行前走路由权限检查。
6
-
7
- 链路位置:
8
- 由 command/builtin/commit* 在展开 prompt 前调用。
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import re
14
- from collections.abc import Callable, Sequence
15
- from pathlib import Path
16
- from typing import Protocol
17
-
18
- from langchain_agentx.tool_runtime.models import ToolExecutionContext
19
- from langchain_agentx.tool_runtime.permission_context import set_command_bash_allow_rules
20
- from langchain_agentx.tool_runtime.session_store import AgentSessionStore
21
- from langchain_agentx.utils.subprocess_text import TextSubprocessRunner, default_text_subprocess_runner
22
-
23
- _INLINE_PATTERN = re.compile(r"!`([^`]+)`")
24
-
25
-
26
- class PromptShellPermissionError(RuntimeError):
27
- """prompt 内嵌 shell 命令未通过 Bash 权限检查。"""
28
-
29
-
30
- class _ShellRunner(Protocol):
31
- def run_text(
32
- self,
33
- argv: list[str],
34
- *,
35
- cwd: str | Path | None = None,
36
- timeout_sec: float | None = None,
37
- ) -> tuple[int, str, str]: ...
38
-
39
-
40
- def _default_runner_call(
41
- runner: TextSubprocessRunner,
42
- command: str,
43
- *,
44
- cwd: str | Path | None,
45
- ) -> tuple[int, str, str]:
46
- completed = runner.run(
47
- ["bash", "-lc", command],
48
- cwd=cwd,
49
- timeout=30.0,
50
- capture_output=True,
51
- )
52
- stdout = completed.stdout or ""
53
- stderr = completed.stderr or ""
54
- if completed.returncode != 0 and not stdout.strip():
55
- return completed.returncode, "", stderr.strip() or f"exit {completed.returncode}"
56
- return completed.returncode, stdout, stderr
57
-
58
-
59
- def _assert_prompt_shell_permission(
60
- command: str,
61
- *,
62
- bash_allow_rules: Sequence[str] | None,
63
- cwd: str | Path | None,
64
- ) -> None:
65
- from langchain_agentx.tools.bash.tool import BashRuntimeTool
66
-
67
- store = AgentSessionStore("prompt-shell")
68
- if bash_allow_rules:
69
- set_command_bash_allow_rules(store, bash_allow_rules)
70
- workspace_root = str(cwd) if cwd is not None else None
71
- ctx = ToolExecutionContext(
72
- tool_name="Bash",
73
- tool_call_id="prompt-shell",
74
- input_args={"command": command},
75
- state={},
76
- session_store=store,
77
- workspace_root=workspace_root,
78
- cwd=workspace_root,
79
- )
80
- l1 = BashRuntimeTool().run_l1_permission_check({"command": command}, ctx)
81
- if l1.outcome == "deny":
82
- message = (l1.decision.message if l1.decision else None) or "Permission denied"
83
- raise PromptShellPermissionError(
84
- f'Shell command permission check failed for "!`{command}`": {message}',
85
- )
86
-
87
-
88
- async def execute_shell_commands_in_prompt(
89
- text: str,
90
- *,
91
- cwd: str | Path | None = None,
92
- runner: TextSubprocessRunner | None = None,
93
- shell_runner: Callable[[str], tuple[int, str, str]] | None = None,
94
- bash_allow_rules: Sequence[str] | None = None,
95
- check_permissions: bool = True,
96
- ) -> str:
97
- """将 text 中所有 !`command` 替换为命令 stdout(失败时内联 stderr/exit)。"""
98
- if "!`" not in text:
99
- return text
100
-
101
- text_runner = runner or default_text_subprocess_runner()
102
- result = text
103
-
104
- for match in list(_INLINE_PATTERN.finditer(text)):
105
- command = match.group(1).strip()
106
- if not command:
107
- continue
108
- if check_permissions and shell_runner is None:
109
- _assert_prompt_shell_permission(
110
- command,
111
- bash_allow_rules=bash_allow_rules,
112
- cwd=cwd,
113
- )
114
- if shell_runner is not None:
115
- _code, stdout, stderr = shell_runner(command)
116
- else:
117
- _code, stdout, stderr = _default_runner_call(text_runner, command, cwd=cwd)
118
- replacement = stdout.strip() if stdout.strip() else stderr.strip()
119
- if not replacement:
120
- replacement = "(no output)"
121
- result = result.replace(match.group(0), replacement, 1)
122
-
123
- return result
1
+ """
2
+ command/prompt_shell.py — Prompt 内嵌 shell 命令执行(CC executeShellCommandsInPrompt 对齐)。
3
+
4
+ 职责:
5
+ 解析并执行 prompt 中的 !`command` 占位,将输出内联替换;执行前走路由权限检查。
6
+
7
+ 链路位置:
8
+ 由 command/builtin/commit* 在展开 prompt 前调用。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from collections.abc import Callable, Sequence
15
+ from pathlib import Path
16
+ from typing import Protocol
17
+
18
+ from langchain_agentx.tool_runtime.models import ToolExecutionContext
19
+ from langchain_agentx.tool_runtime.permission_context import set_command_bash_allow_rules
20
+ from langchain_agentx.tool_runtime.session_store import AgentSessionStore
21
+ from langchain_agentx.utils.subprocess_text import TextSubprocessRunner, default_text_subprocess_runner
22
+
23
+ _INLINE_PATTERN = re.compile(r"!`([^`]+)`")
24
+
25
+
26
+ class PromptShellPermissionError(RuntimeError):
27
+ """prompt 内嵌 shell 命令未通过 Bash 权限检查。"""
28
+
29
+
30
+ class _ShellRunner(Protocol):
31
+ def run_text(
32
+ self,
33
+ argv: list[str],
34
+ *,
35
+ cwd: str | Path | None = None,
36
+ timeout_sec: float | None = None,
37
+ ) -> tuple[int, str, str]: ...
38
+
39
+
40
+ def _default_runner_call(
41
+ runner: TextSubprocessRunner,
42
+ command: str,
43
+ *,
44
+ cwd: str | Path | None,
45
+ ) -> tuple[int, str, str]:
46
+ completed = runner.run(
47
+ ["bash", "-lc", command],
48
+ cwd=cwd,
49
+ timeout=30.0,
50
+ capture_output=True,
51
+ )
52
+ stdout = completed.stdout or ""
53
+ stderr = completed.stderr or ""
54
+ if completed.returncode != 0 and not stdout.strip():
55
+ return completed.returncode, "", stderr.strip() or f"exit {completed.returncode}"
56
+ return completed.returncode, stdout, stderr
57
+
58
+
59
+ def _assert_prompt_shell_permission(
60
+ command: str,
61
+ *,
62
+ bash_allow_rules: Sequence[str] | None,
63
+ cwd: str | Path | None,
64
+ ) -> None:
65
+ from langchain_agentx.tools.bash.tool import BashRuntimeTool
66
+
67
+ store = AgentSessionStore("prompt-shell")
68
+ if bash_allow_rules:
69
+ set_command_bash_allow_rules(store, bash_allow_rules)
70
+ workspace_root = str(cwd) if cwd is not None else None
71
+ ctx = ToolExecutionContext(
72
+ tool_name="Bash",
73
+ tool_call_id="prompt-shell",
74
+ input_args={"command": command},
75
+ state={},
76
+ session_store=store,
77
+ workspace_root=workspace_root,
78
+ cwd=workspace_root,
79
+ )
80
+ l1 = BashRuntimeTool().run_l1_permission_check({"command": command}, ctx)
81
+ if l1.outcome == "deny":
82
+ message = (l1.decision.message if l1.decision else None) or "Permission denied"
83
+ raise PromptShellPermissionError(
84
+ f'Shell command permission check failed for "!`{command}`": {message}',
85
+ )
86
+
87
+
88
+ async def execute_shell_commands_in_prompt(
89
+ text: str,
90
+ *,
91
+ cwd: str | Path | None = None,
92
+ runner: TextSubprocessRunner | None = None,
93
+ shell_runner: Callable[[str], tuple[int, str, str]] | None = None,
94
+ bash_allow_rules: Sequence[str] | None = None,
95
+ check_permissions: bool = True,
96
+ ) -> str:
97
+ """将 text 中所有 !`command` 替换为命令 stdout(失败时内联 stderr/exit)。"""
98
+ if "!`" not in text:
99
+ return text
100
+
101
+ text_runner = runner or default_text_subprocess_runner()
102
+ result = text
103
+
104
+ for match in list(_INLINE_PATTERN.finditer(text)):
105
+ command = match.group(1).strip()
106
+ if not command:
107
+ continue
108
+ if check_permissions and shell_runner is None:
109
+ _assert_prompt_shell_permission(
110
+ command,
111
+ bash_allow_rules=bash_allow_rules,
112
+ cwd=cwd,
113
+ )
114
+ if shell_runner is not None:
115
+ _code, stdout, stderr = shell_runner(command)
116
+ else:
117
+ _code, stdout, stderr = _default_runner_call(text_runner, command, cwd=cwd)
118
+ replacement = stdout.strip() if stdout.strip() else stderr.strip()
119
+ if not replacement:
120
+ replacement = "(no output)"
121
+ result = result.replace(match.group(0), replacement, 1)
122
+
123
+ return result
@@ -602,7 +602,9 @@ class LoopGraphBuilder:
602
602
  else HooksConfigSnapshot()
603
603
  )
604
604
  if session_store is not None:
605
- session_store.hooks_snapshot = self._hook_snapshot
605
+ # G-2 / LOCK-7:子图不得覆盖父 session_store.hooks_snapshot
606
+ if not self._is_subagent:
607
+ session_store.hooks_snapshot = self._hook_snapshot
606
608
  from langchain_agentx.tools.skill.repository_factory import build_skill_repository
607
609
 
608
610
  session_store.skill_repository = build_skill_repository(
@@ -970,6 +972,7 @@ class LoopGraphBuilder:
970
972
  tool_registry=self._loop_loader.registry,
971
973
  tool_loader=self._services.loader,
972
974
  model=model,
975
+ hooks=self._services.hooks,
973
976
  workspace_root=str(self._workspace_root),
974
977
  agent_home_segment=self._workspace_cfg.agent_home_segment,
975
978
  parent_run_id=session_id or None,
@@ -1032,6 +1035,7 @@ class LoopGraphBuilder:
1032
1035
  tool_registry=self._loop_loader.registry,
1033
1036
  tool_loader=self._services.loader,
1034
1037
  model=model,
1038
+ hooks=self._services.hooks,
1035
1039
  workspace_root=str(self._workspace_root),
1036
1040
  agent_home_segment=self._workspace_cfg.agent_home_segment,
1037
1041
  parent_run_id=session_id or None,
@@ -2429,6 +2433,12 @@ class LoopGraphBuilder:
2429
2433
  configurable_dict["capabilities"] = self._services.capabilities
2430
2434
  if self._session_memory_manager is not None:
2431
2435
  configurable_dict["session_memory_manager"] = self._session_memory_manager
2436
+ if self._services.hooks is not None:
2437
+ from langchain_agentx.tool_runtime.prompt.session_mode import (
2438
+ PARENT_HOOK_REGISTRY_CONFIG_KEY,
2439
+ )
2440
+
2441
+ configurable_dict[PARENT_HOOK_REGISTRY_CONFIG_KEY] = self._services.hooks
2432
2442
  if "configurable" in config:
2433
2443
  # Merge existing configurable keys (e.g., tool_loader, session_store)
2434
2444
  configurable_dict = {**config["configurable"], **configurable_dict}
@@ -0,0 +1,51 @@
1
+ """loop/hook/agent_frontmatter_registration.py — 自定义 agent frontmatter hooks
2
+
3
+ 职责:将 ``agents/*.md`` frontmatter ``hooks`` 注册到子 ``HookRegistry``(D-SELF 路径 B)。
4
+ 链路:``SubagentOrchestrator`` compose 之后 · LOCK-5 Stop→SUBAGENT_STOP。
5
+ CC:``registerFrontmatterHooks(..., isAgent=true)``。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any, TYPE_CHECKING
12
+
13
+ from langchain_agentx.loop.hook.types import HookEvent
14
+ from langchain_agentx.tools.skill.hook_registration import iter_skill_hook_specs
15
+
16
+ if TYPE_CHECKING:
17
+ from langchain_agentx.loop.hook.registry import HookRegistry
18
+
19
+
20
+ def register_agent_frontmatter_hooks(
21
+ target: HookRegistry | None,
22
+ *,
23
+ hooks_config: dict[str, Any] | None,
24
+ agent_root: Path | str | None = None,
25
+ agent_name: str | None = None,
26
+ is_subagent: bool = True,
27
+ ) -> int:
28
+ """注册 agent markdown frontmatter hooks;返回注册条数。"""
29
+ _ = agent_name
30
+ if target is None or not hooks_config:
31
+ return 0
32
+
33
+ stop_event = HookEvent.SUBAGENT_STOP if is_subagent else None
34
+ root = agent_root if agent_root is not None else "."
35
+ specs = iter_skill_hook_specs(
36
+ skill_root=root,
37
+ frontmatter={"hooks": hooks_config},
38
+ stop_hook_event=stop_event,
39
+ extra_env_defaults={"CLAUDE_AGENT_DIR": str(Path(root))},
40
+ )
41
+ if not specs:
42
+ return 0
43
+
44
+ registered = 0
45
+ for spec in specs:
46
+ target.register_session_spec(spec)
47
+ registered += 1
48
+ return registered
49
+
50
+
51
+ __all__ = ["register_agent_frontmatter_hooks"]
@@ -17,7 +17,10 @@ registry.py — HookRegistry 用户侧入口。
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import copy
21
+ from dataclasses import replace
20
22
  from typing import Any, Callable
23
+ from uuid import uuid4
21
24
 
22
25
  from .config import HookSpec, HooksConfigSnapshot
23
26
  from .types import HookEvent
@@ -145,6 +148,22 @@ class HookRegistry:
145
148
  for snapshot in self._snapshots:
146
149
  snapshot.unregister_session_hook(hook_id)
147
150
 
151
+ def register_session_spec(self, spec: HookSpec) -> str:
152
+ """追加 session 来源 HookSpec(skill/agent frontmatter 等)。"""
153
+ session_spec = HookSpec(
154
+ event=spec.event,
155
+ executor_type=spec.executor_type,
156
+ config=dict(spec.config),
157
+ matcher=spec.matcher,
158
+ timeout=spec.timeout,
159
+ managed=spec.managed,
160
+ source="session",
161
+ once=spec.once,
162
+ on_success=spec.on_success,
163
+ )
164
+ self._specs.append(session_spec)
165
+ return session_spec.hook_id
166
+
148
167
  # ── P2 注册 API ──
149
168
 
150
169
  def add_command(
@@ -301,6 +320,29 @@ class HookRegistry:
301
320
  self._snapshots.append(snapshot)
302
321
  return snapshot
303
322
 
323
+ def fork_for_subagent(
324
+ self,
325
+ inherited_events: frozenset[HookEvent],
326
+ *,
327
+ fresh_state: bool = True,
328
+ ) -> HookRegistry:
329
+ """过滤 clone 父 registry specs 到子 loop(LOCK-2;fresh_state 预留计数器隔离扩展)。"""
330
+ child = HookRegistry()
331
+ for spec in self._specs:
332
+ if spec.event not in inherited_events:
333
+ continue
334
+ if fresh_state:
335
+ config = copy.deepcopy(spec.config)
336
+ else:
337
+ config = dict(spec.config)
338
+ cloned = replace(
339
+ spec,
340
+ hook_id=uuid4().hex,
341
+ config=config,
342
+ )
343
+ child._specs.append(cloned)
344
+ return child
345
+
304
346
  def __len__(self) -> int:
305
347
  return len(self._specs)
306
348
 
@@ -30,6 +30,8 @@ from collections.abc import Callable
30
30
  from dataclasses import dataclass, field
31
31
  from typing import Any, Literal
32
32
 
33
+ from langchain_agentx.loop.hook.types import HookEvent
34
+
33
35
  from langchain_agentx.observability.events.finish_outcome import (
34
36
  RecoveryPolicy,
35
37
  RecoveryPolicyMode,
@@ -120,6 +122,11 @@ class SubagentContext:
120
122
  mcp_child_session: Any = None
121
123
  """子 Agent inline MCP 会话;cleanup / runner finally detach。"""
122
124
 
125
+ # D-INHERIT:子 loop HookRegistry(orchestrator compose 后注入)
126
+ hooks: Any = None # HookRegistry | None
127
+ inherit_hooks: bool | frozenset[HookEvent] | None = True
128
+ """True/None → DEFAULT_SUBAGENT_INHERITED_EVENTS;False → 不继承;frozenset → 自定义事件集。"""
129
+
123
130
 
124
131
  @dataclass
125
132
  class SubagentResult:
@@ -164,6 +171,7 @@ def create_subagent_context(
164
171
  query_source: str | None = None,
165
172
  is_builtin_subagent: bool = False,
166
173
  recovery: RecoveryPolicy | RecoveryPolicyMode | None = None,
174
+ inherit_hooks: bool | frozenset[HookEvent] | None = True,
167
175
  ) -> SubagentContext:
168
176
  """工厂函数:从父上下文创建子 Agent 上下文
169
177
 
@@ -341,6 +349,7 @@ def create_subagent_context(
341
349
  thinking_generation_enabled=thinking_generation_enabled,
342
350
  query_source=resolved_query_source,
343
351
  recovery=resolved_recovery,
352
+ inherit_hooks=inherit_hooks,
344
353
  )
345
354
 
346
355
 
@@ -378,6 +387,8 @@ class ForkedParentRuntime:
378
387
  tool_registry: Any
379
388
  tool_loader: Any
380
389
  model: Any
390
+ hooks: Any = None
391
+ """父 loop HookRegistry(后台 fork D-INHERIT · P4)。"""
381
392
  workspace_root: str = ""
382
393
  agent_home_segment: str = ".langchain_agentx"
383
394
  parent_run_id: str | None = None
@@ -123,6 +123,16 @@ class ForkedAgentRunner:
123
123
  can_use_tool=params.can_use_tool,
124
124
  share_abort_controller=params.share_abort_controller,
125
125
  )
126
+ from langchain_agentx.loop.subagent.hooks_pipeline import (
127
+ SubagentHookParentView,
128
+ apply_subagent_hooks_pipeline_async,
129
+ )
130
+
131
+ await apply_subagent_hooks_pipeline_async(
132
+ sub_ctx,
133
+ SubagentHookParentView.from_forked_parent_runtime(self._parent_runtime),
134
+ subagent_type=sub_ctx.subagent_type,
135
+ )
126
136
  result = await self._get_orchestrator().run_isolated(
127
137
  sub_ctx,
128
138
  loader=self._fork_loader(sub_ctx, params.can_use_tool),
@@ -139,5 +139,6 @@ def create_subagent_graph(
139
139
  loader=loader,
140
140
  session_store=ctx.session_store,
141
141
  capabilities=ctx.capabilities,
142
+ hooks=getattr(ctx, "hooks", None),
142
143
  )
143
144
  return create_loop_agent(model, config=config, services=services)
@@ -0,0 +1,46 @@
1
+ """loop/subagent/hook_attachment_messages.py — CC hook_additional_context 消息形态(P5)
2
+
3
+ 对齐 CC ``createAttachmentMessage({ type: 'hook_additional_context', ... })``;
4
+ SDK 使用 ``role: user`` + ``metadata.attachment``(与 plan_mode / memdir 一致)。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import uuid
10
+ from typing import Any
11
+
12
+ from langchain_agentx.loop.message_contract import to_record
13
+
14
+
15
+ def build_subagent_start_hook_messages(
16
+ contexts: list[str],
17
+ *,
18
+ hook_name: str = "SubagentStart",
19
+ hook_event: str = "SubagentStart",
20
+ tool_use_id: str | None = None,
21
+ ) -> list[dict[str, Any]]:
22
+ """将 SUBAGENT_START additional_context 转为可注入 initial_messages 的记录。"""
23
+ if not contexts:
24
+ return []
25
+ tid = tool_use_id if tool_use_id else str(uuid.uuid4())
26
+ text = "\n\n".join(c.strip() for c in contexts if isinstance(c, str) and c.strip())
27
+ return [
28
+ to_record(
29
+ {
30
+ "role": "user",
31
+ "content": text,
32
+ "metadata": {
33
+ "attachment": {
34
+ "type": "hook_additional_context",
35
+ "content": list(contexts),
36
+ "hookName": hook_name,
37
+ "toolUseID": tid,
38
+ "hookEvent": hook_event,
39
+ }
40
+ },
41
+ }
42
+ )
43
+ ]
44
+
45
+
46
+ __all__ = ["build_subagent_start_hook_messages"]