langchain-agentx-python 2.3.3__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/compaction_config.py +38 -0
- langchain_agentx/loop/config/loop_config.py +10 -0
- langchain_agentx/loop/context/pipeline.py +25 -7
- langchain_agentx/loop/context/settings.py +11 -0
- langchain_agentx/loop/context/stages/autocompact.py +51 -9
- langchain_agentx/loop/context/types.py +4 -0
- langchain_agentx/loop/exit/exit_logic.py +2 -0
- langchain_agentx/loop/graph/factory.py +215 -47
- 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 +14 -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.3.dist-info → langchain_agentx_python-2.3.5.dist-info}/METADATA +583 -582
- {langchain_agentx_python-2.3.3.dist-info → langchain_agentx_python-2.3.5.dist-info}/RECORD +95 -41
- {langchain_agentx_python-2.3.3.dist-info → langchain_agentx_python-2.3.5.dist-info}/LICENSE +0 -0
- {langchain_agentx_python-2.3.3.dist-info → langchain_agentx_python-2.3.5.dist-info}/WHEEL +0 -0
- {langchain_agentx_python-2.3.3.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],
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Compaction 控制面 env 解析(对齐 CC DISABLE_COMPACT / DISABLE_AUTO_COMPACT)。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _parse_bool_env(name: str) -> bool | None:
|
|
9
|
+
raw = os.environ.get(name, "").strip().lower()
|
|
10
|
+
if not raw:
|
|
11
|
+
return None
|
|
12
|
+
return raw in ("1", "true", "yes", "on")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def resolve_is_auto_compact_enabled(*, config_is_auto_compact: bool = True) -> bool:
|
|
16
|
+
"""CC:DISABLE_COMPACT 关全部;DISABLE_AUTO_COMPACT 仅关 proactive autocompact。"""
|
|
17
|
+
if _parse_bool_env("DISABLE_COMPACT") is True:
|
|
18
|
+
return False
|
|
19
|
+
if _parse_bool_env("DISABLE_AUTO_COMPACT") is True:
|
|
20
|
+
return False
|
|
21
|
+
return config_is_auto_compact
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def compute_autocompact_trigger_token_threshold(
|
|
25
|
+
*,
|
|
26
|
+
context_window_tokens: int,
|
|
27
|
+
max_output_tokens_reserved: int,
|
|
28
|
+
compact_buffer_tokens: int,
|
|
29
|
+
) -> int:
|
|
30
|
+
"""对齐 CC:effectiveWindow - AUTOCOMPACT_BUFFER_TOKENS。"""
|
|
31
|
+
effective = max(0, int(context_window_tokens) - int(max_output_tokens_reserved))
|
|
32
|
+
return max(0, effective - int(compact_buffer_tokens))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"compute_autocompact_trigger_token_threshold",
|
|
37
|
+
"resolve_is_auto_compact_enabled",
|
|
38
|
+
]
|
|
@@ -253,6 +253,7 @@ class AgentLoopConfig:
|
|
|
253
253
|
token_budget_max_continuations: int = 3
|
|
254
254
|
compact_buffer_tokens: int = 13_000
|
|
255
255
|
max_output_tokens_reserved: int = 20_000
|
|
256
|
+
is_auto_compact: bool = True
|
|
256
257
|
|
|
257
258
|
# -----------------------------------------------------------------------
|
|
258
259
|
# 运行模式
|
|
@@ -312,6 +313,12 @@ class AgentLoopConfig:
|
|
|
312
313
|
)
|
|
313
314
|
model_retry: ModelRetrySettings = field(default_factory=ModelRetrySettings)
|
|
314
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
|
+
|
|
315
322
|
# -----------------------------------------------------------------------
|
|
316
323
|
# 便捷构造器
|
|
317
324
|
# -----------------------------------------------------------------------
|
|
@@ -393,6 +400,9 @@ class LoopServices:
|
|
|
393
400
|
queued_command_providers: Sequence[Any] = field(default_factory=tuple)
|
|
394
401
|
active_files_getter: Callable[[], list[str]] | None = None
|
|
395
402
|
|
|
403
|
+
mcp_controller: Any = None # McpRuntimeController;默认 None 零成本
|
|
404
|
+
command_registry: Any = None # CommandRegistry;MCP prompts register_many
|
|
405
|
+
|
|
396
406
|
|
|
397
407
|
__all__ = [
|
|
398
408
|
"PromptConfig",
|
|
@@ -33,17 +33,33 @@ from .stages.base import CompactionStage
|
|
|
33
33
|
from .types import CompactProgress, ContextCompactionContext
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _apply_autocompact_failure_patch(
|
|
37
|
+
state: dict[str, Any],
|
|
38
|
+
stages_meta: list[dict[str, Any]],
|
|
39
|
+
patch: dict[str, Any],
|
|
40
|
+
) -> None:
|
|
41
|
+
ac = next((m for m in stages_meta if m.get("name") == "autocompact"), None)
|
|
42
|
+
if ac is None:
|
|
43
|
+
return
|
|
44
|
+
extra = ac.get("extra") or {}
|
|
45
|
+
prev = int(state.get("consecutive_autocompact_failures") or 0)
|
|
46
|
+
if extra.get("autocompact_llm_failed"):
|
|
47
|
+
patch["consecutive_autocompact_failures"] = prev + 1
|
|
48
|
+
elif extra.get("autocompact_mode") == "llm":
|
|
49
|
+
patch["consecutive_autocompact_failures"] = 0
|
|
50
|
+
|
|
51
|
+
|
|
36
52
|
class DefaultCompactionStages:
|
|
37
53
|
"""CC 五段默认装配(顺序即契约)。"""
|
|
38
54
|
|
|
39
55
|
@staticmethod
|
|
40
|
-
def build() -> tuple[CompactionStage, ...]:
|
|
56
|
+
def build(settings: CompactionSettings | None = None) -> tuple[CompactionStage, ...]:
|
|
41
57
|
return (
|
|
42
|
-
ToolResultBudgetStage(),
|
|
43
|
-
SnipCompactStage(),
|
|
44
|
-
MicrocompactStage(),
|
|
45
|
-
ContextCollapseStage(),
|
|
46
|
-
AutocompactStage(),
|
|
58
|
+
ToolResultBudgetStage(settings),
|
|
59
|
+
SnipCompactStage(settings),
|
|
60
|
+
MicrocompactStage(settings),
|
|
61
|
+
ContextCollapseStage(settings),
|
|
62
|
+
AutocompactStage(settings),
|
|
47
63
|
)
|
|
48
64
|
|
|
49
65
|
|
|
@@ -65,7 +81,7 @@ class ContextPipeline:
|
|
|
65
81
|
settings: CompactionSettings | None = None,
|
|
66
82
|
) -> ContextPipeline:
|
|
67
83
|
"""使用 `DefaultCompactionStages` 构建管道。"""
|
|
68
|
-
return cls(DefaultCompactionStages.build(), settings=settings)
|
|
84
|
+
return cls(DefaultCompactionStages.build(settings), settings=settings)
|
|
69
85
|
|
|
70
86
|
@property
|
|
71
87
|
def stages(self) -> tuple[CompactionStage, ...]:
|
|
@@ -117,6 +133,7 @@ class ContextPipeline:
|
|
|
117
133
|
"mode": ctx.mode,
|
|
118
134
|
}
|
|
119
135
|
|
|
136
|
+
_apply_autocompact_failure_patch(state, stages_meta, patch)
|
|
120
137
|
if not patch:
|
|
121
138
|
return {}
|
|
122
139
|
return patch
|
|
@@ -198,6 +215,7 @@ class ContextPipeline:
|
|
|
198
215
|
"mode": ctx.mode,
|
|
199
216
|
}
|
|
200
217
|
|
|
218
|
+
_apply_autocompact_failure_patch(state, stages_meta, patch)
|
|
201
219
|
if not patch:
|
|
202
220
|
return {}
|
|
203
221
|
return patch
|
|
@@ -32,6 +32,8 @@ class CompactionSettings:
|
|
|
32
32
|
prune_protected_tools: FrozenSet[str] = field(default_factory=lambda: frozenset({"Skill"}))
|
|
33
33
|
default_max_context_tokens: int = 120_000
|
|
34
34
|
default_compress_trigger_fraction: float = 0.8
|
|
35
|
+
# 非 None 时 AutocompactStage 用绝对触发线(由 AgentLoopConfig compact_buffer 派生)。
|
|
36
|
+
autocompact_trigger_token_threshold: int | None = None
|
|
35
37
|
default_compress_keep_recent_messages: int = 8
|
|
36
38
|
# --- autocompact LLM 提示词(对齐 `compact.ts` + `prompt.ts`)---
|
|
37
39
|
# 摘要插在保留 tail 之前 → 默认 `partial_up_to`;全量折叠无尾窗时用 `full`;与 CC partial pivot「from」一致时用 `partial_from`。
|
|
@@ -62,6 +64,8 @@ class CompactionSettings:
|
|
|
62
64
|
default_factory=QuerySourcePolicy.autocompact_skip_sources
|
|
63
65
|
)
|
|
64
66
|
autocompact_use_llm_when_available: bool = True
|
|
67
|
+
is_auto_compact_enabled: bool = True
|
|
68
|
+
max_consecutive_autocompact_failures: int = 3
|
|
65
69
|
autocompact_llm_excerpt_chars_per_message: int = 20_000
|
|
66
70
|
# --- snip carry → autocompact 阈值放宽(对齐 CC snipTokensFreed 计入阈值)---
|
|
67
71
|
# True:将本流水线已累计的 `snip_tokens_freed_carry` 加到触发阈值上,避免 snip 已释放后仍立刻 autocompact。
|
|
@@ -89,6 +93,12 @@ class CompactionSettings:
|
|
|
89
93
|
active_compaction_settings: CompactionSettings = CompactionSettings()
|
|
90
94
|
|
|
91
95
|
|
|
96
|
+
def autocompact_trigger_threshold_tokens(settings: CompactionSettings) -> int:
|
|
97
|
+
if settings.autocompact_trigger_token_threshold is not None:
|
|
98
|
+
return settings.autocompact_trigger_token_threshold
|
|
99
|
+
return int(settings.default_max_context_tokens * settings.default_compress_trigger_fraction)
|
|
100
|
+
|
|
101
|
+
|
|
92
102
|
def get_active_compaction_settings() -> CompactionSettings:
|
|
93
103
|
"""返回当前生效的策略(默认即模块级 `active_compaction_settings`)。"""
|
|
94
104
|
return active_compaction_settings
|
|
@@ -104,6 +114,7 @@ __all__ = [
|
|
|
104
114
|
"AutocompactLlmPromptKind",
|
|
105
115
|
"CompactionSettings",
|
|
106
116
|
"active_compaction_settings",
|
|
117
|
+
"autocompact_trigger_threshold_tokens",
|
|
107
118
|
"get_active_compaction_settings",
|
|
108
119
|
"set_active_compaction_settings",
|
|
109
120
|
]
|