langchain-agentx-python 2.3.4__py3-none-any.whl → 2.3.5__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 -64
- langchain_agentx/command/mcp/__init__.py +5 -0
- langchain_agentx/command/mcp/adapter.py +60 -0
- langchain_agentx/command/prompt_shell.py +123 -123
- langchain_agentx/command/registry.py +16 -0
- langchain_agentx/loop/config/loop_config.py +9 -0
- langchain_agentx/loop/graph/factory.py +70 -0
- langchain_agentx/loop/prompt/mcp_instructions.py +40 -0
- langchain_agentx/loop/subagent/context.py +24 -1
- langchain_agentx/loop/subagent/graph.py +6 -1
- langchain_agentx/loop/subagent/mcp_child_servers.py +117 -0
- langchain_agentx/loop/subagent/orchestrator.py +104 -0
- langchain_agentx/loop/subagent/runner.py +7 -0
- langchain_agentx/mcp/__init__.py +46 -0
- langchain_agentx/mcp/auth/__init__.py +5 -0
- langchain_agentx/mcp/auth/oauth_session.py +57 -0
- langchain_agentx/mcp/call.py +140 -0
- langchain_agentx/mcp/client_manager.py +584 -0
- langchain_agentx/mcp/config/__init__.py +12 -0
- langchain_agentx/mcp/config/env_expand.py +49 -0
- langchain_agentx/mcp/config/merge.py +66 -0
- langchain_agentx/mcp/config/policy.py +49 -0
- langchain_agentx/mcp/config/project_file.py +43 -0
- langchain_agentx/mcp/config/resolver.py +194 -0
- langchain_agentx/mcp/controller.py +123 -0
- langchain_agentx/mcp/elicitation.py +201 -0
- langchain_agentx/mcp/errors.py +32 -0
- langchain_agentx/mcp/events.py +42 -0
- langchain_agentx/mcp/fixtures/__init__.py +0 -0
- langchain_agentx/mcp/fixtures/echo_server.py +25 -0
- langchain_agentx/mcp/fixtures/elicit_server.py +37 -0
- langchain_agentx/mcp/fixtures/surface_server.py +61 -0
- langchain_agentx/mcp/loop_driver.py +111 -0
- langchain_agentx/mcp/result.py +84 -0
- langchain_agentx/mcp/stdio_connector.py +64 -0
- langchain_agentx/mcp/transports/__init__.py +9 -0
- langchain_agentx/mcp/transports/connector.py +64 -0
- langchain_agentx/mcp/transports/http.py +31 -0
- langchain_agentx/mcp/transports/sse.py +22 -0
- langchain_agentx/observability/events/__init__.py +3 -0
- langchain_agentx/observability/events/agent_event_projector.py +3 -0
- langchain_agentx/observability/events/mcp_event_sink.py +53 -0
- langchain_agentx/protocol/__init__.py +11 -0
- langchain_agentx/protocol/mcp/__init__.py +58 -0
- langchain_agentx/protocol/mcp/constants.py +22 -0
- langchain_agentx/protocol/mcp/naming.py +64 -0
- langchain_agentx/protocol/mcp/sanitization.py +71 -0
- langchain_agentx/protocol/mcp/truncate.py +22 -0
- langchain_agentx/protocol/mcp/types.py +151 -0
- langchain_agentx/session/agent_session.py +13 -3
- langchain_agentx/session/conversation_factory.py +9 -0
- langchain_agentx/session/conversation_session.py +11 -1
- langchain_agentx/session/factory.py +9 -0
- langchain_agentx/tool_runtime/inner_permission_chain.py +23 -3
- langchain_agentx/tool_runtime/loader.py +4 -0
- langchain_agentx/tool_runtime/loop_loader.py +158 -0
- langchain_agentx/tool_runtime/permission_rule_matcher.py +49 -0
- langchain_agentx/tool_runtime/permission_rules.py +10 -6
- langchain_agentx/tool_runtime/policy_decorator.py +70 -70
- langchain_agentx/tool_runtime/process_loader.py +3 -0
- langchain_agentx/tool_runtime/registry.py +4 -0
- langchain_agentx/tools/agent/loader.py +61 -2
- langchain_agentx/tools/agent/prompt_scope.py +1 -1
- langchain_agentx/tools/agent/registry/config.py +5 -1
- langchain_agentx/tools/bash/command_allow_rules.py +8 -2
- langchain_agentx/tools/mcp/__init__.py +15 -0
- langchain_agentx/tools/mcp/backend.py +50 -0
- langchain_agentx/tools/mcp/loader.py +234 -0
- langchain_agentx/tools/mcp/pool_binder.py +81 -0
- langchain_agentx/tools/mcp/prompt.py +27 -0
- langchain_agentx/tools/mcp/tool.py +228 -0
- langchain_agentx/tools/mcp_resource_list/__init__.py +5 -0
- langchain_agentx/tools/mcp_resource_list/models.py +12 -0
- langchain_agentx/tools/mcp_resource_list/prompt.py +24 -0
- langchain_agentx/tools/mcp_resource_list/tool.py +144 -0
- langchain_agentx/tools/mcp_resource_read/__init__.py +5 -0
- langchain_agentx/tools/mcp_resource_read/models.py +10 -0
- langchain_agentx/tools/mcp_resource_read/prompt.py +22 -0
- langchain_agentx/tools/mcp_resource_read/tool.py +124 -0
- langchain_agentx/tools/skill/catalog.py +26 -4
- langchain_agentx/tools/skill/mcp_ingest.py +118 -0
- langchain_agentx/workspace/config.py +10 -0
- langchain_agentx/workspace/path_canonical.py +24 -24
- langchain_agentx/workspace/project_memory_layout.py +18 -4
- {langchain_agentx_python-2.3.4.dist-info → langchain_agentx_python-2.3.5.dist-info}/METADATA +583 -582
- {langchain_agentx_python-2.3.4.dist-info → langchain_agentx_python-2.3.5.dist-info}/RECORD +89 -36
- {langchain_agentx_python-2.3.4.dist-info → langchain_agentx_python-2.3.5.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-2.3.4.dist-info → langchain_agentx_python-2.3.5.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-2.3.4.dist-info → langchain_agentx_python-2.3.5.dist-info}/top_level.txt +0 -0
langchain_agentx/__init__.py
CHANGED
|
@@ -1,64 +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 .
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
from .
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
|
|
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.5"
|
|
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
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""MCP prompts/list → CommandDefinition。
|
|
2
|
+
|
|
3
|
+
职责:
|
|
4
|
+
对标 CC ``fetchCommandsForClient``:已连接 session 上 prompts/list,
|
|
5
|
+
映射 ``command_type=prompt``、``source=mcp``、名 ``mcp__{server}__{prompt}``。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
McpClientManager.list_prompts(connect=False) → 本适配器 → CommandRegistry.register_many。
|
|
9
|
+
|
|
10
|
+
当前裁剪:
|
|
11
|
+
不 connect / 不 ensure;slash UX 由宿主投影;不扩展 CommandType。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
from langchain_agentx.command.result import CommandResult
|
|
19
|
+
from langchain_agentx.command.types import CommandDefinition
|
|
20
|
+
from langchain_agentx.protocol.mcp.naming import build_mcp_tool_name
|
|
21
|
+
from langchain_agentx.protocol.mcp.types import McpPromptRef
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from langchain_agentx.command.context import CommandContext
|
|
25
|
+
from langchain_agentx.mcp.client_manager import McpClientManager
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class McpPromptCommandAdapter:
|
|
29
|
+
"""已连接 Manager 上的 prompts 适配;禁止在此拉起 session。"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, manager: McpClientManager) -> None:
|
|
32
|
+
self._manager = manager
|
|
33
|
+
|
|
34
|
+
async def list_definitions(self) -> list[CommandDefinition]:
|
|
35
|
+
definitions: list[CommandDefinition] = []
|
|
36
|
+
for spec in self._manager.connected_specs():
|
|
37
|
+
refs = await self._manager.list_prompts(spec.name, connect=False)
|
|
38
|
+
for ref in refs:
|
|
39
|
+
definitions.append(self._to_definition(ref))
|
|
40
|
+
return definitions
|
|
41
|
+
|
|
42
|
+
def _to_definition(self, ref: McpPromptRef) -> CommandDefinition:
|
|
43
|
+
return CommandDefinition(
|
|
44
|
+
name=build_mcp_tool_name(ref.server_name, ref.name),
|
|
45
|
+
description=ref.description,
|
|
46
|
+
command_type="prompt",
|
|
47
|
+
source="mcp",
|
|
48
|
+
handler=self._make_handler(ref),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def _make_handler(self, ref: McpPromptRef):
|
|
52
|
+
manager = self._manager
|
|
53
|
+
|
|
54
|
+
async def _handler(context: CommandContext) -> CommandResult:
|
|
55
|
+
_ = context
|
|
56
|
+
messages = await manager.get_prompt(ref.server_name, ref.name, connect=False)
|
|
57
|
+
text = "\n\n".join(messages.messages)
|
|
58
|
+
return CommandResult(should_query=True, injected_prompt=text)
|
|
59
|
+
|
|
60
|
+
return _handler
|
|
@@ -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
|
|
@@ -36,6 +36,22 @@ class CommandRegistry:
|
|
|
36
36
|
for alias in command.aliases:
|
|
37
37
|
self._alias_to_name[self._normalize(alias)] = normalized_name
|
|
38
38
|
|
|
39
|
+
def register_many(
|
|
40
|
+
self,
|
|
41
|
+
commands: Iterable[CommandDefinition],
|
|
42
|
+
*,
|
|
43
|
+
skip_existing: bool = True,
|
|
44
|
+
) -> list[str]:
|
|
45
|
+
"""批量注册。默认跳过已有名(内置优先于 MCP)。"""
|
|
46
|
+
registered: list[str] = []
|
|
47
|
+
for command in commands:
|
|
48
|
+
normalized_name = self._normalize(command.name)
|
|
49
|
+
if skip_existing and normalized_name in self._commands:
|
|
50
|
+
continue
|
|
51
|
+
self.register(command)
|
|
52
|
+
registered.append(normalized_name)
|
|
53
|
+
return registered
|
|
54
|
+
|
|
39
55
|
def register_plugin(
|
|
40
56
|
self,
|
|
41
57
|
commands: Iterable[CommandDefinition],
|
|
@@ -313,6 +313,12 @@ class AgentLoopConfig:
|
|
|
313
313
|
)
|
|
314
314
|
model_retry: ModelRetrySettings = field(default_factory=ModelRetrySettings)
|
|
315
315
|
|
|
316
|
+
# MCP:默认空,不 connect / 不扫盘。仅显式 specs / controller / plugin 片段时 Loop 接线(S2)。
|
|
317
|
+
mcp_servers: tuple[Any, ...] = ()
|
|
318
|
+
mcp_plugins: tuple[Any, ...] = ()
|
|
319
|
+
mcp_skills_enabled: bool = True
|
|
320
|
+
mcp_elicitation_auto_accept: bool = False
|
|
321
|
+
|
|
316
322
|
# -----------------------------------------------------------------------
|
|
317
323
|
# 便捷构造器
|
|
318
324
|
# -----------------------------------------------------------------------
|
|
@@ -394,6 +400,9 @@ class LoopServices:
|
|
|
394
400
|
queued_command_providers: Sequence[Any] = field(default_factory=tuple)
|
|
395
401
|
active_files_getter: Callable[[], list[str]] | None = None
|
|
396
402
|
|
|
403
|
+
mcp_controller: Any = None # McpRuntimeController;默认 None 零成本
|
|
404
|
+
command_registry: Any = None # CommandRegistry;MCP prompts register_many
|
|
405
|
+
|
|
397
406
|
|
|
398
407
|
__all__ = [
|
|
399
408
|
"PromptConfig",
|
|
@@ -317,6 +317,7 @@ def _flatten_config_for_runtime(config: AgentLoopConfig) -> _FlatLoopConfig:
|
|
|
317
317
|
trace_enabled=config.trace.enabled,
|
|
318
318
|
trace_persist=config.trace.persist,
|
|
319
319
|
headless=config.headless,
|
|
320
|
+
container_type=config.container_type,
|
|
320
321
|
model_profile=config.model_profile,
|
|
321
322
|
model_profile_registry=config.model_profile_registry,
|
|
322
323
|
compact_buffer_tokens=config.compact_buffer_tokens,
|
|
@@ -549,6 +550,15 @@ class LoopGraphBuilder:
|
|
|
549
550
|
available_tools=self._loader_tools,
|
|
550
551
|
).build_sections()
|
|
551
552
|
)
|
|
553
|
+
if (
|
|
554
|
+
self._loop_loader is not None
|
|
555
|
+
and getattr(self._loop_loader, "mcp_controller", None) is not None
|
|
556
|
+
):
|
|
557
|
+
from ...loop.prompt.mcp_instructions import mcp_instructions_section
|
|
558
|
+
|
|
559
|
+
self._dynamic_sections.append(
|
|
560
|
+
mcp_instructions_section(self._loop_loader.mcp_controller)
|
|
561
|
+
)
|
|
552
562
|
system_message = self._build_system_message(
|
|
553
563
|
base_prompt=base_prompt,
|
|
554
564
|
append_system_prompt=append_system_prompt,
|
|
@@ -783,6 +793,10 @@ class LoopGraphBuilder:
|
|
|
783
793
|
)
|
|
784
794
|
continue
|
|
785
795
|
self._loop_loader.register(tool)
|
|
796
|
+
self._mcp_bind_session_store = session_store
|
|
797
|
+
self._mcp_bind_model = model
|
|
798
|
+
self._mcp_bind_schema_ns = schema_cache_namespace
|
|
799
|
+
self._bind_optional_mcp()
|
|
786
800
|
self._runtime_tools = self._loop_loader.registry.list()
|
|
787
801
|
from ...loop.prompt.tool_aware_sections import collect_enabled_tool_names
|
|
788
802
|
|
|
@@ -799,6 +813,62 @@ class LoopGraphBuilder:
|
|
|
799
813
|
if self._skill_runtime_tool is None and rt.__class__.__name__ == "SkillRuntimeTool":
|
|
800
814
|
self._skill_runtime_tool = rt
|
|
801
815
|
|
|
816
|
+
def _bind_optional_mcp(self) -> None:
|
|
817
|
+
if self._loop_loader is None:
|
|
818
|
+
return
|
|
819
|
+
controller = self._services.mcp_controller
|
|
820
|
+
specs = self._config.mcp_servers
|
|
821
|
+
plugins = self._config.mcp_plugins
|
|
822
|
+
if controller is None and not specs and not plugins:
|
|
823
|
+
return
|
|
824
|
+
self._loop_loader.set_mcp_skills_enabled(
|
|
825
|
+
bool(getattr(self._config, "mcp_skills_enabled", True))
|
|
826
|
+
)
|
|
827
|
+
for tool in self._loop_loader.registry.list():
|
|
828
|
+
if tool.__class__.__name__ != "SkillRuntimeTool":
|
|
829
|
+
continue
|
|
830
|
+
catalog = getattr(tool, "_catalog", None)
|
|
831
|
+
if catalog is not None:
|
|
832
|
+
self._loop_loader.set_mcp_skill_catalog(catalog)
|
|
833
|
+
break
|
|
834
|
+
self._loop_loader.attach_mcp(
|
|
835
|
+
controller=controller,
|
|
836
|
+
specs=specs or None,
|
|
837
|
+
plugins=plugins or None,
|
|
838
|
+
schema_cache_namespace=getattr(self, "_mcp_bind_schema_ns", None),
|
|
839
|
+
after_refresh=self._rebuild_views_after_mcp_refresh,
|
|
840
|
+
command_registry=self._services.command_registry,
|
|
841
|
+
)
|
|
842
|
+
self._apply_mcp_elicitation_auto_accept()
|
|
843
|
+
|
|
844
|
+
def _apply_mcp_elicitation_auto_accept(self) -> None:
|
|
845
|
+
if self._loop_loader is None:
|
|
846
|
+
return
|
|
847
|
+
if not bool(getattr(self._config, "mcp_elicitation_auto_accept", False)):
|
|
848
|
+
return
|
|
849
|
+
ctrl = self._loop_loader.mcp_controller
|
|
850
|
+
if ctrl is None:
|
|
851
|
+
return
|
|
852
|
+
from langchain_agentx.mcp.elicitation import AcceptingElicitationInterceptor
|
|
853
|
+
|
|
854
|
+
ctrl.manager.elicitation.set_interceptor(AcceptingElicitationInterceptor())
|
|
855
|
+
|
|
856
|
+
def _rebuild_views_after_mcp_refresh(self) -> None:
|
|
857
|
+
if self._loop_loader is None:
|
|
858
|
+
return
|
|
859
|
+
from ...loop.prompt.tool_aware_sections import collect_enabled_tool_names
|
|
860
|
+
|
|
861
|
+
self._runtime_tools = self._loop_loader.registry.list()
|
|
862
|
+
self._enabled_tool_names = collect_enabled_tool_names(self._runtime_tools)
|
|
863
|
+
self._tool_prompt_ctx = self._build_tool_prompt_context(
|
|
864
|
+
session_store=getattr(self, "_mcp_bind_session_store", None),
|
|
865
|
+
model=getattr(self, "_mcp_bind_model", None),
|
|
866
|
+
schema_cache_namespace=getattr(self, "_mcp_bind_schema_ns", None),
|
|
867
|
+
)
|
|
868
|
+
self._loader_tools = self._loop_loader.registry.to_langchain_tools(
|
|
869
|
+
prompt_ctx=self._tool_prompt_ctx,
|
|
870
|
+
)
|
|
871
|
+
|
|
802
872
|
def refresh_permission_settings(
|
|
803
873
|
self,
|
|
804
874
|
*,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""MCP server instructions → SystemPromptSection。
|
|
2
|
+
|
|
3
|
+
职责:
|
|
4
|
+
截断 2048;只经 ModelRequest.system_message 动态段,不写入 state messages。
|
|
5
|
+
|
|
6
|
+
链路位置:
|
|
7
|
+
LoopGraphBuilder._prepare_compile_context 在 MCP 显式启用时追加。
|
|
8
|
+
|
|
9
|
+
当前裁剪:
|
|
10
|
+
无进度条;无 CC UI 折叠。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from langchain_agentx.loop.prompt.sections import SystemPromptSection, section
|
|
18
|
+
from langchain_agentx.protocol.mcp.truncate import truncate_for_model
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def format_mcp_instructions(snapshots: Any) -> str | None:
|
|
22
|
+
lines: list[str] = []
|
|
23
|
+
for item in snapshots or ():
|
|
24
|
+
text = getattr(item, "instructions", None)
|
|
25
|
+
if not text:
|
|
26
|
+
continue
|
|
27
|
+
name = getattr(item, "name", "") or "mcp"
|
|
28
|
+
lines.append(f"## MCP server `{name}`\n{truncate_for_model(str(text))}")
|
|
29
|
+
if not lines:
|
|
30
|
+
return None
|
|
31
|
+
return "# MCP server instructions\n\n" + "\n\n".join(lines)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def mcp_instructions_section(controller: Any) -> SystemPromptSection:
|
|
35
|
+
def _compute() -> str | None:
|
|
36
|
+
status = getattr(controller, "status", None)
|
|
37
|
+
snapshots = status() if callable(status) else ()
|
|
38
|
+
return format_mcp_instructions(snapshots)
|
|
39
|
+
|
|
40
|
+
return section("mcp_server_instructions", _compute)
|