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.
Files changed (95) hide show
  1. langchain_agentx/__init__.py +75 -64
  2. langchain_agentx/command/mcp/__init__.py +5 -0
  3. langchain_agentx/command/mcp/adapter.py +60 -0
  4. langchain_agentx/command/prompt_shell.py +123 -123
  5. langchain_agentx/command/registry.py +16 -0
  6. langchain_agentx/loop/config/compaction_config.py +38 -0
  7. langchain_agentx/loop/config/loop_config.py +10 -0
  8. langchain_agentx/loop/context/pipeline.py +25 -7
  9. langchain_agentx/loop/context/settings.py +11 -0
  10. langchain_agentx/loop/context/stages/autocompact.py +51 -9
  11. langchain_agentx/loop/context/types.py +4 -0
  12. langchain_agentx/loop/exit/exit_logic.py +2 -0
  13. langchain_agentx/loop/graph/factory.py +215 -47
  14. langchain_agentx/loop/prompt/mcp_instructions.py +40 -0
  15. langchain_agentx/loop/subagent/context.py +24 -1
  16. langchain_agentx/loop/subagent/graph.py +6 -1
  17. langchain_agentx/loop/subagent/mcp_child_servers.py +117 -0
  18. langchain_agentx/loop/subagent/orchestrator.py +104 -0
  19. langchain_agentx/loop/subagent/runner.py +7 -0
  20. langchain_agentx/mcp/__init__.py +46 -0
  21. langchain_agentx/mcp/auth/__init__.py +5 -0
  22. langchain_agentx/mcp/auth/oauth_session.py +57 -0
  23. langchain_agentx/mcp/call.py +140 -0
  24. langchain_agentx/mcp/client_manager.py +584 -0
  25. langchain_agentx/mcp/config/__init__.py +12 -0
  26. langchain_agentx/mcp/config/env_expand.py +49 -0
  27. langchain_agentx/mcp/config/merge.py +66 -0
  28. langchain_agentx/mcp/config/policy.py +49 -0
  29. langchain_agentx/mcp/config/project_file.py +43 -0
  30. langchain_agentx/mcp/config/resolver.py +194 -0
  31. langchain_agentx/mcp/controller.py +123 -0
  32. langchain_agentx/mcp/elicitation.py +201 -0
  33. langchain_agentx/mcp/errors.py +32 -0
  34. langchain_agentx/mcp/events.py +42 -0
  35. langchain_agentx/mcp/fixtures/__init__.py +0 -0
  36. langchain_agentx/mcp/fixtures/echo_server.py +25 -0
  37. langchain_agentx/mcp/fixtures/elicit_server.py +37 -0
  38. langchain_agentx/mcp/fixtures/surface_server.py +61 -0
  39. langchain_agentx/mcp/loop_driver.py +111 -0
  40. langchain_agentx/mcp/result.py +84 -0
  41. langchain_agentx/mcp/stdio_connector.py +64 -0
  42. langchain_agentx/mcp/transports/__init__.py +9 -0
  43. langchain_agentx/mcp/transports/connector.py +64 -0
  44. langchain_agentx/mcp/transports/http.py +31 -0
  45. langchain_agentx/mcp/transports/sse.py +22 -0
  46. langchain_agentx/observability/events/__init__.py +3 -0
  47. langchain_agentx/observability/events/agent_event_projector.py +14 -0
  48. langchain_agentx/observability/events/mcp_event_sink.py +53 -0
  49. langchain_agentx/protocol/__init__.py +11 -0
  50. langchain_agentx/protocol/mcp/__init__.py +58 -0
  51. langchain_agentx/protocol/mcp/constants.py +22 -0
  52. langchain_agentx/protocol/mcp/naming.py +64 -0
  53. langchain_agentx/protocol/mcp/sanitization.py +71 -0
  54. langchain_agentx/protocol/mcp/truncate.py +22 -0
  55. langchain_agentx/protocol/mcp/types.py +151 -0
  56. langchain_agentx/session/agent_session.py +13 -3
  57. langchain_agentx/session/conversation_factory.py +9 -0
  58. langchain_agentx/session/conversation_session.py +11 -1
  59. langchain_agentx/session/factory.py +9 -0
  60. langchain_agentx/tool_runtime/inner_permission_chain.py +23 -3
  61. langchain_agentx/tool_runtime/loader.py +4 -0
  62. langchain_agentx/tool_runtime/loop_loader.py +158 -0
  63. langchain_agentx/tool_runtime/permission_rule_matcher.py +49 -0
  64. langchain_agentx/tool_runtime/permission_rules.py +10 -6
  65. langchain_agentx/tool_runtime/policy_decorator.py +70 -70
  66. langchain_agentx/tool_runtime/process_loader.py +3 -0
  67. langchain_agentx/tool_runtime/registry.py +4 -0
  68. langchain_agentx/tools/agent/loader.py +61 -2
  69. langchain_agentx/tools/agent/prompt_scope.py +1 -1
  70. langchain_agentx/tools/agent/registry/config.py +5 -1
  71. langchain_agentx/tools/bash/command_allow_rules.py +8 -2
  72. langchain_agentx/tools/mcp/__init__.py +15 -0
  73. langchain_agentx/tools/mcp/backend.py +50 -0
  74. langchain_agentx/tools/mcp/loader.py +234 -0
  75. langchain_agentx/tools/mcp/pool_binder.py +81 -0
  76. langchain_agentx/tools/mcp/prompt.py +27 -0
  77. langchain_agentx/tools/mcp/tool.py +228 -0
  78. langchain_agentx/tools/mcp_resource_list/__init__.py +5 -0
  79. langchain_agentx/tools/mcp_resource_list/models.py +12 -0
  80. langchain_agentx/tools/mcp_resource_list/prompt.py +24 -0
  81. langchain_agentx/tools/mcp_resource_list/tool.py +144 -0
  82. langchain_agentx/tools/mcp_resource_read/__init__.py +5 -0
  83. langchain_agentx/tools/mcp_resource_read/models.py +10 -0
  84. langchain_agentx/tools/mcp_resource_read/prompt.py +22 -0
  85. langchain_agentx/tools/mcp_resource_read/tool.py +124 -0
  86. langchain_agentx/tools/skill/catalog.py +26 -4
  87. langchain_agentx/tools/skill/mcp_ingest.py +118 -0
  88. langchain_agentx/workspace/config.py +10 -0
  89. langchain_agentx/workspace/path_canonical.py +24 -24
  90. langchain_agentx/workspace/project_memory_layout.py +18 -4
  91. {langchain_agentx_python-2.3.3.dist-info → langchain_agentx_python-2.3.5.dist-info}/METADATA +583 -582
  92. {langchain_agentx_python-2.3.3.dist-info → langchain_agentx_python-2.3.5.dist-info}/RECORD +95 -41
  93. {langchain_agentx_python-2.3.3.dist-info → langchain_agentx_python-2.3.5.dist-info}/LICENSE +0 -0
  94. {langchain_agentx_python-2.3.3.dist-info → langchain_agentx_python-2.3.5.dist-info}/WHEEL +0 -0
  95. {langchain_agentx_python-2.3.3.dist-info → langchain_agentx_python-2.3.5.dist-info}/top_level.txt +0 -0
@@ -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.3"
15
-
16
- from .loop import ( # noqa: F401
17
- create_loop_agent,
18
- )
19
-
20
- from .loop.hook import ( # noqa: F401
21
- ATTACHMENT_HOOK_BLOCKING_ERROR,
22
- ATTACHMENT_HOOK_CANCELLED,
23
- ATTACHMENT_HOOK_NON_BLOCKING_ERROR,
24
- ATTACHMENT_HOOK_STOPPED_CONTINUATION,
25
- ATTACHMENT_HOOK_SUCCESS,
26
- )
27
- from .observability import ( # noqa: F401
28
- EventAdapterState,
29
- LangchainAgentEvent,
30
- LangchainAgentEventType,
31
- LangGraphToLangchainAgentEventAdapter,
32
- TraceCollector,
33
- TraceSession,
34
- TraceSpan,
35
- )
36
- from .provider import CompatibleChatOpenAI # noqa: F401
37
- from .session import AgentSession, create_agent_session # noqa: F401
38
- from .health import HealthCheck, HealthReport, PreflightResult, preflight, sdk_health # noqa: F401
39
- from .workflow import BaseWorkflow # noqa: F401
40
-
41
- __all__ = [
42
- "ATTACHMENT_HOOK_BLOCKING_ERROR",
43
- "ATTACHMENT_HOOK_CANCELLED",
44
- "ATTACHMENT_HOOK_NON_BLOCKING_ERROR",
45
- "ATTACHMENT_HOOK_STOPPED_CONTINUATION",
46
- "ATTACHMENT_HOOK_SUCCESS",
47
- "create_loop_agent",
48
- "create_agent_session",
49
- "AgentSession",
50
- "BaseWorkflow",
51
- "EventAdapterState",
52
- "LangchainAgentEvent",
53
- "LangchainAgentEventType",
54
- "LangGraphToLangchainAgentEventAdapter",
55
- "TraceCollector",
56
- "TraceSession",
57
- "TraceSpan",
58
- "CompatibleChatOpenAI",
59
- "HealthCheck",
60
- "HealthReport",
61
- "PreflightResult",
62
- "preflight",
63
- "sdk_health",
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,5 @@
1
+ """command/mcp — MCP prompts → CommandDefinition 适配。"""
2
+
3
+ from langchain_agentx.command.mcp.adapter import McpPromptCommandAdapter
4
+
5
+ __all__ = ["McpPromptCommandAdapter"]
@@ -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
  ]