langchain-agentx-python 2.3.6__py3-none-any.whl → 2.3.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 +75 -75
- langchain_agentx/command/prompt_shell.py +123 -123
- langchain_agentx/loop/graph/factory.py +11 -1
- langchain_agentx/loop/hook/agent_frontmatter_registration.py +51 -0
- langchain_agentx/loop/hook/registry.py +42 -0
- langchain_agentx/loop/subagent/context.py +16 -0
- langchain_agentx/loop/subagent/forked_runner.py +10 -0
- langchain_agentx/loop/subagent/graph.py +1 -0
- langchain_agentx/loop/subagent/hook_attachment_messages.py +46 -0
- langchain_agentx/loop/subagent/hooks_compose.py +74 -0
- langchain_agentx/loop/subagent/hooks_pipeline.py +181 -0
- langchain_agentx/loop/subagent/orchestrator.py +84 -1
- langchain_agentx/loop/subagent/parent_hook_snapshot.py +61 -0
- langchain_agentx/loop/subagent/progress.py +2 -0
- langchain_agentx/loop/subagent/progress_dispatcher.py +26 -0
- langchain_agentx/loop/subagent/runner.py +61 -13
- langchain_agentx/loop/subagent/subagent_start_hooks.py +39 -0
- langchain_agentx/observability/events/agent_event_projector.py +14 -0
- langchain_agentx/observability/events/skill_progress_event_mapper.py +87 -0
- langchain_agentx/observability/events/subagent_event_mapper.py +10 -1
- langchain_agentx/tool_runtime/adapter.py +7 -0
- langchain_agentx/tool_runtime/models.py +2 -0
- langchain_agentx/tool_runtime/policy_decorator.py +70 -70
- langchain_agentx/tool_runtime/prompt/session_mode.py +19 -0
- langchain_agentx/tools/agent/loader.py +5 -0
- langchain_agentx/tools/agent/registry/config.py +4 -0
- langchain_agentx/tools/skill/fork.py +3 -0
- langchain_agentx/tools/skill/hook_registration.py +98 -25
- langchain_agentx/tools/skill/tool.py +7 -6
- langchain_agentx/workspace/path_canonical.py +24 -24
- {langchain_agentx_python-2.3.6.dist-info → langchain_agentx_python-2.3.8.dist-info}/METADATA +583 -583
- {langchain_agentx_python-2.3.6.dist-info → langchain_agentx_python-2.3.8.dist-info}/RECORD +35 -28
- {langchain_agentx_python-2.3.6.dist-info → langchain_agentx_python-2.3.8.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-2.3.6.dist-info → langchain_agentx_python-2.3.8.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-2.3.6.dist-info → langchain_agentx_python-2.3.8.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -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.
|
|
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.8"
|
|
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
|
|
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,16 @@ 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
|
+
|
|
130
|
+
progress_host_kind: str | None = None
|
|
131
|
+
"""Skill fork:``skill_progress`` 时 runner 只冒泡 CC 同构 skill progress,不发 subagent 双通道。"""
|
|
132
|
+
skill_prompt: str = ""
|
|
133
|
+
"""Skill fork:↔ CC ``onProgress`` ``data.prompt``(skill 正文)。"""
|
|
134
|
+
|
|
123
135
|
|
|
124
136
|
@dataclass
|
|
125
137
|
class SubagentResult:
|
|
@@ -164,6 +176,7 @@ def create_subagent_context(
|
|
|
164
176
|
query_source: str | None = None,
|
|
165
177
|
is_builtin_subagent: bool = False,
|
|
166
178
|
recovery: RecoveryPolicy | RecoveryPolicyMode | None = None,
|
|
179
|
+
inherit_hooks: bool | frozenset[HookEvent] | None = True,
|
|
167
180
|
) -> SubagentContext:
|
|
168
181
|
"""工厂函数:从父上下文创建子 Agent 上下文
|
|
169
182
|
|
|
@@ -341,6 +354,7 @@ def create_subagent_context(
|
|
|
341
354
|
thinking_generation_enabled=thinking_generation_enabled,
|
|
342
355
|
query_source=resolved_query_source,
|
|
343
356
|
recovery=resolved_recovery,
|
|
357
|
+
inherit_hooks=inherit_hooks,
|
|
344
358
|
)
|
|
345
359
|
|
|
346
360
|
|
|
@@ -378,6 +392,8 @@ class ForkedParentRuntime:
|
|
|
378
392
|
tool_registry: Any
|
|
379
393
|
tool_loader: Any
|
|
380
394
|
model: Any
|
|
395
|
+
hooks: Any = None
|
|
396
|
+
"""父 loop HookRegistry(后台 fork D-INHERIT · P4)。"""
|
|
381
397
|
workspace_root: str = ""
|
|
382
398
|
agent_home_segment: str = ".langchain_agentx"
|
|
383
399
|
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),
|
|
@@ -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"]
|