langchain-agentx-cli 0.1.0__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 (122) hide show
  1. langchain_agentx_cli/__init__.py +3 -0
  2. langchain_agentx_cli/__main__.py +8 -0
  3. langchain_agentx_cli/app.py +149 -0
  4. langchain_agentx_cli/bootstrap.py +49 -0
  5. langchain_agentx_cli/bridge/__init__.py +5 -0
  6. langchain_agentx_cli/bridge/session_bridge.py +261 -0
  7. langchain_agentx_cli/cli.py +223 -0
  8. langchain_agentx_cli/commands/__init__.py +19 -0
  9. langchain_agentx_cli/commands/builtin/__init__.py +26 -0
  10. langchain_agentx_cli/commands/builtin/clear.py +17 -0
  11. langchain_agentx_cli/commands/builtin/compact.py +26 -0
  12. langchain_agentx_cli/commands/builtin/help.py +27 -0
  13. langchain_agentx_cli/commands/builtin/model.py +24 -0
  14. langchain_agentx_cli/commands/builtin/quit.py +18 -0
  15. langchain_agentx_cli/commands/builtin/theme.py +82 -0
  16. langchain_agentx_cli/commands/parser.py +70 -0
  17. langchain_agentx_cli/commands/providers/__init__.py +3 -0
  18. langchain_agentx_cli/commands/providers/sdk_commands.py +44 -0
  19. langchain_agentx_cli/commands/registry.py +95 -0
  20. langchain_agentx_cli/completion/__init__.py +10 -0
  21. langchain_agentx_cli/completion/base.py +26 -0
  22. langchain_agentx_cli/completion/command_source.py +42 -0
  23. langchain_agentx_cli/completion/history_source.py +34 -0
  24. langchain_agentx_cli/config.py +131 -0
  25. langchain_agentx_cli/history/__init__.py +3 -0
  26. langchain_agentx_cli/history/store.py +129 -0
  27. langchain_agentx_cli/keybindings/__init__.py +3 -0
  28. langchain_agentx_cli/keybindings/bindings.py +43 -0
  29. langchain_agentx_cli/llm_config.py +257 -0
  30. langchain_agentx_cli/permissions/__init__.py +32 -0
  31. langchain_agentx_cli/permissions/factory.py +93 -0
  32. langchain_agentx_cli/permissions/policy.py +119 -0
  33. langchain_agentx_cli/prompts/__init__.py +47 -0
  34. langchain_agentx_cli/prompts/assembler.py +63 -0
  35. langchain_agentx_cli/prompts/sections.py +59 -0
  36. langchain_agentx_cli/prompts/system_prompt.py +105 -0
  37. langchain_agentx_cli/screens/__init__.py +5 -0
  38. langchain_agentx_cli/screens/repl.py +238 -0
  39. langchain_agentx_cli/session_factory.py +110 -0
  40. langchain_agentx_cli/session_options.py +56 -0
  41. langchain_agentx_cli/theme/__init__.py +104 -0
  42. langchain_agentx_cli/theme/colors.py +209 -0
  43. langchain_agentx_cli/theme/detection.py +31 -0
  44. langchain_agentx_cli/theme/manager.py +160 -0
  45. langchain_agentx_cli/theme/settings.py +56 -0
  46. langchain_agentx_cli/theme/system_theme.py +116 -0
  47. langchain_agentx_cli/theme/themes.py +167 -0
  48. langchain_agentx_cli/theme/watcher.py +56 -0
  49. langchain_agentx_cli/tools/__init__.py +13 -0
  50. langchain_agentx_cli/tools/registry.py +126 -0
  51. langchain_agentx_cli/tui/__init__.py +12 -0
  52. langchain_agentx_cli/tui/message_selection.py +64 -0
  53. langchain_agentx_cli/tui/permissions/__init__.py +39 -0
  54. langchain_agentx_cli/tui/permissions/cache.py +39 -0
  55. langchain_agentx_cli/tui/permissions/config.py +14 -0
  56. langchain_agentx_cli/tui/permissions/consumer.py +158 -0
  57. langchain_agentx_cli/tui/permissions/dialog.py +125 -0
  58. langchain_agentx_cli/tui/permissions/inject.py +57 -0
  59. langchain_agentx_cli/tui/permissions/models.py +27 -0
  60. langchain_agentx_cli/tui/permissions/presenters/__init__.py +63 -0
  61. langchain_agentx_cli/tui/permissions/presenters/base.py +68 -0
  62. langchain_agentx_cli/tui/permissions/presenters/bash.py +91 -0
  63. langchain_agentx_cli/tui/permissions/presenters/fallback.py +25 -0
  64. langchain_agentx_cli/tui/permissions/presenters/file.py +87 -0
  65. langchain_agentx_cli/tui/safe_screen.py +50 -0
  66. langchain_agentx_cli/welcome.py +48 -0
  67. langchain_agentx_cli/widgets/__init__.py +25 -0
  68. langchain_agentx_cli/widgets/completion_overlay.py +145 -0
  69. langchain_agentx_cli/widgets/input_area.py +492 -0
  70. langchain_agentx_cli/widgets/message_events.py +137 -0
  71. langchain_agentx_cli/widgets/message_list.py +458 -0
  72. langchain_agentx_cli/widgets/messages/__init__.py +57 -0
  73. langchain_agentx_cli/widgets/messages/assistant_message.py +224 -0
  74. langchain_agentx_cli/widgets/messages/error_message.py +33 -0
  75. langchain_agentx_cli/widgets/messages/rendering.py +137 -0
  76. langchain_agentx_cli/widgets/messages/thinking_message.py +78 -0
  77. langchain_agentx_cli/widgets/pending_permission.py +69 -0
  78. langchain_agentx_cli/widgets/permission_inline.py +141 -0
  79. langchain_agentx_cli/widgets/spinner.py +318 -0
  80. langchain_agentx_cli/widgets/tools/__init__.py +103 -0
  81. langchain_agentx_cli/widgets/tools/agent/__init__.py +5 -0
  82. langchain_agentx_cli/widgets/tools/agent/widget.py +68 -0
  83. langchain_agentx_cli/widgets/tools/ask_user_question/__init__.py +7 -0
  84. langchain_agentx_cli/widgets/tools/ask_user_question/widget.py +61 -0
  85. langchain_agentx_cli/widgets/tools/base.py +178 -0
  86. langchain_agentx_cli/widgets/tools/bash/__init__.py +5 -0
  87. langchain_agentx_cli/widgets/tools/bash/widget.py +77 -0
  88. langchain_agentx_cli/widgets/tools/batch_edit/__init__.py +5 -0
  89. langchain_agentx_cli/widgets/tools/batch_edit/widget.py +62 -0
  90. langchain_agentx_cli/widgets/tools/edit/__init__.py +5 -0
  91. langchain_agentx_cli/widgets/tools/edit/widget.py +66 -0
  92. langchain_agentx_cli/widgets/tools/glob/__init__.py +5 -0
  93. langchain_agentx_cli/widgets/tools/glob/widget.py +49 -0
  94. langchain_agentx_cli/widgets/tools/grep/__init__.py +5 -0
  95. langchain_agentx_cli/widgets/tools/grep/widget.py +49 -0
  96. langchain_agentx_cli/widgets/tools/helpers.py +260 -0
  97. langchain_agentx_cli/widgets/tools/read/__init__.py +5 -0
  98. langchain_agentx_cli/widgets/tools/read/widget.py +92 -0
  99. langchain_agentx_cli/widgets/tools/skill/__init__.py +5 -0
  100. langchain_agentx_cli/widgets/tools/skill/widget.py +53 -0
  101. langchain_agentx_cli/widgets/tools/task_create/__init__.py +5 -0
  102. langchain_agentx_cli/widgets/tools/task_create/widget.py +43 -0
  103. langchain_agentx_cli/widgets/tools/task_get/__init__.py +5 -0
  104. langchain_agentx_cli/widgets/tools/task_get/widget.py +52 -0
  105. langchain_agentx_cli/widgets/tools/task_list/__init__.py +5 -0
  106. langchain_agentx_cli/widgets/tools/task_list/widget.py +49 -0
  107. langchain_agentx_cli/widgets/tools/task_update/__init__.py +5 -0
  108. langchain_agentx_cli/widgets/tools/task_update/widget.py +45 -0
  109. langchain_agentx_cli/widgets/tools/user_message/__init__.py +5 -0
  110. langchain_agentx_cli/widgets/tools/user_message/widget.py +93 -0
  111. langchain_agentx_cli/widgets/tools/webfetch/__init__.py +5 -0
  112. langchain_agentx_cli/widgets/tools/webfetch/widget.py +58 -0
  113. langchain_agentx_cli/widgets/tools/websearch/__init__.py +5 -0
  114. langchain_agentx_cli/widgets/tools/websearch/widget.py +53 -0
  115. langchain_agentx_cli/widgets/tools/write/__init__.py +5 -0
  116. langchain_agentx_cli/widgets/tools/write/widget.py +71 -0
  117. langchain_agentx_cli-0.1.0.dist-info/METADATA +140 -0
  118. langchain_agentx_cli-0.1.0.dist-info/RECORD +122 -0
  119. langchain_agentx_cli-0.1.0.dist-info/WHEEL +5 -0
  120. langchain_agentx_cli-0.1.0.dist-info/entry_points.txt +2 -0
  121. langchain_agentx_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  122. langchain_agentx_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ """langchain_agentx_cli:依托 langchain_agentx_python SDK 的终端 CLI/TUI(CC Ink 迁移)。"""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,8 @@
1
+ """
2
+ __main__.py — python -m langchain_agentx_cli 入口。
3
+ """
4
+
5
+ from langchain_agentx_cli.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -0,0 +1,149 @@
1
+ """
2
+ app.py — Textual ReplApp 容器。
3
+
4
+ 职责:
5
+ 应用生命周期、SessionBridge、命令注册表、历史、双次 Ctrl+C 退出。
6
+
7
+ 链路位置:
8
+ CliEntry._run_tui() 实例化并 run();session 关闭由 CliEntry finally 负责。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ from textual import on
16
+ from textual.app import App
17
+ from textual.binding import Binding
18
+
19
+ from langchain_agentx_cli.bridge.session_bridge import SessionBridge
20
+ from langchain_agentx_cli.commands.builtin import register_builtin_commands
21
+ from langchain_agentx_cli.commands.providers import register_sdk_commands
22
+ from langchain_agentx_cli.commands.registry import CommandRegistry
23
+ from langchain_agentx_cli.config import (
24
+ AppPreferencesStore,
25
+ AppUserPreferences,
26
+ ReplLaunchConfig,
27
+ )
28
+ from langchain_agentx_cli.theme import ThemeManager
29
+ from langchain_agentx_cli.history.store import HistoryStore
30
+ from langchain_agentx_cli.screens import ReplScreen
31
+ from langchain_agentx_cli.tui.message_selection import try_copy_message_selection
32
+ from langchain_agentx_cli.tui.safe_screen import ReplScreenMessenger
33
+ from langchain_agentx_cli.widgets.permission_inline import InlinePermissionWidget
34
+ from langchain_agentx_cli.widgets.messages import UserMessage
35
+
36
+ if TYPE_CHECKING:
37
+ from langchain_agentx import AgentSession
38
+
39
+ _CANCEL_HINT = "已取消当前生成。再次按 Ctrl+C 退出。"
40
+ _EXIT_HINT = "再次按 Ctrl+C 退出。"
41
+
42
+
43
+ class ReplApp(App[None]):
44
+ """AgentX 终端 REPL 应用(Textual)。"""
45
+
46
+ TITLE = "langchain-agentx-cli"
47
+ BINDINGS = [
48
+ ("q", "quit", "Quit"),
49
+ ("ctrl+c", "cancel_stream", "Cancel / Quit"),
50
+ Binding("ctrl+l", "clear_screen", "Clear", show=False, priority=True),
51
+ Binding(
52
+ "ctrl+shift+l",
53
+ "clear_screen",
54
+ "Clear (Win 备用)",
55
+ show=False,
56
+ priority=True,
57
+ ),
58
+ ]
59
+
60
+ def __init__(
61
+ self,
62
+ launch_config: ReplLaunchConfig,
63
+ session: AgentSession | None = None,
64
+ ) -> None:
65
+ super().__init__()
66
+ self._launch_config = launch_config
67
+ self._session = session
68
+ self._bridge: SessionBridge | None = None
69
+ self._exit_armed = False
70
+ self.command_registry = CommandRegistry()
71
+ register_builtin_commands(self.command_registry)
72
+ self.history_store = HistoryStore()
73
+ self.theme_manager: ThemeManager | None = None
74
+ self._screen_messenger = ReplScreenMessenger(self)
75
+ # Phase 3: 加载用户偏好(供 MessageListWidget 和工具 Widget 使用)
76
+ self.preferences: AppUserPreferences = AppPreferencesStore().load()
77
+
78
+ @property
79
+ def screen_messenger(self) -> ReplScreenMessenger:
80
+ return self._screen_messenger
81
+
82
+ @property
83
+ def workspace_root(self):
84
+ return self._launch_config.workspace_root
85
+
86
+ @property
87
+ def launch_config(self) -> ReplLaunchConfig:
88
+ return self._launch_config
89
+
90
+ @property
91
+ def bridge(self) -> SessionBridge | None:
92
+ return self._bridge
93
+
94
+ def on_mount(self) -> None:
95
+ prefs = AppPreferencesStore().load()
96
+ initial_setting = self._launch_config.theme or prefs.theme
97
+ self.theme_manager = ThemeManager(self)
98
+ self.theme_manager.setup(initial=initial_setting)
99
+ if self._session is not None:
100
+ self._bridge = SessionBridge(self, self._session)
101
+ register_sdk_commands(self.command_registry, self._session)
102
+ self._bridge.permission_consumer.start()
103
+ self.push_screen(ReplScreen())
104
+
105
+ def on_unmount(self) -> None:
106
+ if self.theme_manager is not None:
107
+ self.theme_manager.stop_auto_watch()
108
+ if self._bridge is not None:
109
+ self._bridge.permission_consumer.stop()
110
+ self._bridge.cancel_stream()
111
+
112
+ def clear_messages(self) -> None:
113
+ screen = self.screen
114
+ if isinstance(screen, ReplScreen):
115
+ screen.clear_messages()
116
+
117
+ def action_clear_screen(self) -> None:
118
+ self.clear_messages()
119
+ self.notify("消息已清空", severity="information", timeout=2)
120
+
121
+ @on(UserMessage)
122
+ def _on_user_message_reset_exit_arm(self, _event: UserMessage) -> None:
123
+ self._exit_armed = False
124
+
125
+ def action_cancel_stream(self) -> None:
126
+ if try_copy_message_selection(self) is not None:
127
+ return
128
+ # Dismiss inline permission widget if active
129
+ try:
130
+ widget = self.screen.query_one(InlinePermissionWidget)
131
+ widget.dismiss_externally()
132
+ return
133
+ except Exception:
134
+ pass
135
+ if self._bridge is not None and self._bridge.is_streaming():
136
+ self._bridge.cancel_stream()
137
+ self._exit_armed = True
138
+ self.notify(_CANCEL_HINT, severity="warning", timeout=4)
139
+ return
140
+ if self._exit_armed:
141
+ self.action_quit()
142
+ return
143
+ self._exit_armed = True
144
+ self.notify(_EXIT_HINT, severity="information", timeout=4)
145
+
146
+ def action_quit(self) -> None:
147
+ if self._bridge is not None:
148
+ self._bridge.cancel_stream()
149
+ self.exit(0)
@@ -0,0 +1,49 @@
1
+ """
2
+ bootstrap.py — CLI 启动前环境检查。
3
+
4
+ 职责:
5
+ 校验 SDK 可导入、workspace_root 为有效目录。
6
+
7
+ 链路位置:
8
+ CliEntry.run() 在创建 ReplApp 之前调用。
9
+
10
+ 当前裁剪范围:
11
+ 不校验 API Key;AgentSession 创建失败由 session_factory 抛出 CliEnvironmentError。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from pathlib import Path
18
+
19
+
20
+ class CliEnvironmentError(Exception):
21
+ """启动前环境不满足,应打印 stderr 并以非零码退出。"""
22
+
23
+
24
+ class EnvironmentChecker:
25
+ """CLI 启动环境检查(OOP 协作者)。"""
26
+
27
+ def check_sdk_importable(self) -> None:
28
+ try:
29
+ import langchain_agentx # noqa: F401
30
+ except ImportError as exc:
31
+ raise CliEnvironmentError(
32
+ "无法导入 langchain_agentx。请安装 SDK:\n"
33
+ ' pip install "langchain-agentx-python>=0.2.3"\n'
34
+ "或 editable:pip install -e <langchain_agentx_python 路径>"
35
+ ) from exc
36
+
37
+ def check_workspace_root(self, workspace_root: Path) -> None:
38
+ if not workspace_root.exists():
39
+ raise CliEnvironmentError(
40
+ f"workspace_root 不存在: {workspace_root}"
41
+ )
42
+ if not workspace_root.is_dir():
43
+ raise CliEnvironmentError(
44
+ f"workspace_root 不是目录: {workspace_root}"
45
+ )
46
+
47
+ @staticmethod
48
+ def emit_error(message: str) -> None:
49
+ print(message, file=sys.stderr)
@@ -0,0 +1,5 @@
1
+ """TUI 与 SDK 桥接层。"""
2
+
3
+ from .session_bridge import MVP_ADAPTER_CONFIG, SessionBridge
4
+
5
+ __all__ = ["MVP_ADAPTER_CONFIG", "SessionBridge"]
@@ -0,0 +1,261 @@
1
+ """
2
+ bridge/session_bridge.py — SessionBridge(SDK 事件 → Textual Message)。
3
+
4
+ 职责:
5
+ 消费 LangchainAgentEvent,经 Worker 异步调用 SDK,向 App 投递 UI Message;
6
+ 创建权限 Queue/Resolver 并注入 AgentSession。
7
+
8
+ 链路位置:
9
+ ReplApp → SessionBridge → AgentSession.stream_loop_events
10
+ → LangGraphToLangchainAgentEventAdapter.adapt();
11
+ 权限:AgentSessionPermissionResolver ← PermissionQueueConsumer。
12
+
13
+ 当前裁剪范围:
14
+ 不解析 LangGraph 原始事件名;Resolver 超时 300s(见 tui/permissions/config.py)。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ from collections.abc import Callable
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ from langchain_agentx.tool_runtime.resolvers import (
24
+ AgentSessionPermissionResolver,
25
+ PermissionRequest,
26
+ )
27
+ from langchain_agentx_cli.tui.permissions import (
28
+ PermissionQueueConsumer,
29
+ SessionPermissionCache,
30
+ inject_permission_resolver,
31
+ )
32
+ from langchain_agentx_cli.tui.permissions.config import DEFAULT_PERMISSION_TIMEOUT_SECONDS
33
+ from langchain_agentx_cli.tui.permissions.dialog import DialogPermissionDecisionPort
34
+
35
+ from langchain_agentx import (
36
+ LangGraphToLangchainAgentEventAdapter,
37
+ LangchainAgentEvent,
38
+ LangchainAgentEventType,
39
+ )
40
+
41
+ from langchain_agentx_cli.widgets.messages import (
42
+ AssistantMessageChunk,
43
+ ErrorMessageOccurred,
44
+ ThinkingContentDelta,
45
+ ThinkingEnded,
46
+ ThinkingStarted,
47
+ ToolUseCompleted,
48
+ ToolUseStarted,
49
+ TurnInProgress,
50
+ UserMessage,
51
+ )
52
+
53
+ if TYPE_CHECKING:
54
+ from langchain_agentx import AgentSession
55
+
56
+ from langchain_agentx_cli.app import ReplApp
57
+
58
+ MVP_ADAPTER_CONFIG: dict[str, bool] = {
59
+ "enable_reasoning_events": True,
60
+ "enable_step_events": False,
61
+ "enable_hook_events": False,
62
+ "enable_compact_events": True,
63
+ "enable_subagent_events": False,
64
+ "synthesize_tool_call": True,
65
+ }
66
+
67
+ _LLM_FAILED_PREFIX = "[LLM request failed]"
68
+
69
+
70
+ class SessionBridge:
71
+ """TUI 与 SDK 之间的桥接层(消费 LangchainAgentEvent)。"""
72
+
73
+ def __init__(self, app: ReplApp, session: AgentSession) -> None:
74
+ self._app = app
75
+ self._screen_messenger = app.screen_messenger
76
+ self._session = session
77
+ self._adapter = LangGraphToLangchainAgentEventAdapter(config=dict(MVP_ADAPTER_CONFIG))
78
+ self._current_worker: Any = None
79
+ self._streamed_text = False
80
+ self._permission_queue: asyncio.Queue[PermissionRequest] = asyncio.Queue()
81
+ self._permission_cache = SessionPermissionCache()
82
+ self._permission_resolver = AgentSessionPermissionResolver(
83
+ request_queue=self._permission_queue,
84
+ timeout=DEFAULT_PERMISSION_TIMEOUT_SECONDS,
85
+ )
86
+ inject_permission_resolver(session, self._permission_resolver)
87
+ self._permission_consumer = PermissionQueueConsumer(
88
+ queue=self._permission_queue,
89
+ cache=self._permission_cache,
90
+ app=app,
91
+ decision_port=DialogPermissionDecisionPort(app),
92
+ )
93
+
94
+ def _post_ui(self, message: object) -> None:
95
+ self._screen_messenger.post(message)
96
+
97
+ def submit_input(self, text: str) -> None:
98
+ self._post_ui(UserMessage(content=text))
99
+ self._post_ui(TurnInProgress(in_progress=True))
100
+ self._current_worker = self._app.run_worker(
101
+ self._stream_turn(text),
102
+ name="sdk-stream",
103
+ exit_on_error=False,
104
+ )
105
+
106
+ async def _stream_turn(self, user_input: str) -> None:
107
+ self._adapter._reset_state()
108
+ self._streamed_text = False
109
+ raw_events = self._session.stream_loop_events(user_input)
110
+ try:
111
+ async for event in self._adapter.adapt(raw_events):
112
+ self._dispatch_event(event)
113
+ except Exception as exc:
114
+ self._post_ui(ErrorMessageOccurred(error=str(exc)))
115
+ finally:
116
+ self._post_ui(TurnInProgress(in_progress=False))
117
+ self._current_worker = None
118
+
119
+ def _dispatch_event(self, event: LangchainAgentEvent) -> None:
120
+ data = event.data or {}
121
+ match event.event_type:
122
+ case LangchainAgentEventType.TEXT_START:
123
+ self._post_ui(
124
+ AssistantMessageChunk(delta="", is_complete=False)
125
+ )
126
+ case LangchainAgentEventType.TEXT_DELTA:
127
+ delta = str(data.get("text", ""))
128
+ if delta:
129
+ self._streamed_text = True
130
+ self._post_ui(
131
+ AssistantMessageChunk(
132
+ delta=delta,
133
+ is_complete=False,
134
+ )
135
+ )
136
+ case LangchainAgentEventType.TEXT_END:
137
+ self._post_ui(
138
+ AssistantMessageChunk(delta="", is_complete=True)
139
+ )
140
+ case LangchainAgentEventType.TOOL_INPUT:
141
+ tool_input = data.get("input")
142
+ if not isinstance(tool_input, dict):
143
+ tool_input = data.get("params") if isinstance(data.get("params"), dict) else {}
144
+ self._post_ui(
145
+ ToolUseStarted(
146
+ tool_name=str(data.get("tool_name", "")),
147
+ tool_input=tool_input,
148
+ )
149
+ )
150
+ case LangchainAgentEventType.TOOL_CALL:
151
+ pass
152
+ case LangchainAgentEventType.TOOL_RESULT:
153
+ self._post_ui(
154
+ ToolUseCompleted(
155
+ tool_name=str(data.get("tool_name", "")),
156
+ output=str(data.get("output", "")),
157
+ )
158
+ )
159
+ case LangchainAgentEventType.TOOL_ERROR:
160
+ error_text = str(data.get("error", ""))
161
+ self._post_ui(
162
+ ToolUseCompleted(
163
+ tool_name=str(data.get("tool_name", "")),
164
+ output=f"Error: {error_text}",
165
+ error=error_text or None,
166
+ )
167
+ )
168
+ case LangchainAgentEventType.ERROR:
169
+ self._post_ui(
170
+ ErrorMessageOccurred(
171
+ error=str(data.get("error", "Unknown error")),
172
+ )
173
+ )
174
+ case LangchainAgentEventType.REASONING_START:
175
+ self._post_ui(ThinkingStarted())
176
+ case LangchainAgentEventType.REASONING_DELTA:
177
+ delta = str(data.get("text", ""))
178
+ if delta:
179
+ self._post_ui(ThinkingContentDelta(delta=delta))
180
+ case LangchainAgentEventType.REASONING_END:
181
+ self._post_ui(ThinkingEnded())
182
+ case LangchainAgentEventType.START:
183
+ self._post_ui(TurnInProgress(in_progress=True))
184
+ case LangchainAgentEventType.FINISH:
185
+ self._emit_finish_answer(str(data.get("answer", "")))
186
+ self._post_ui(TurnInProgress(in_progress=False))
187
+ case _:
188
+ pass
189
+
190
+ def _emit_finish_answer(self, answer: str) -> None:
191
+ """非流式路径(如 API 失败合成 AIMessage)在 FINISH.answer 带回文案。"""
192
+ text = answer.strip()
193
+ if not text:
194
+ return
195
+ if text.startswith(_LLM_FAILED_PREFIX):
196
+ self._post_ui(ErrorMessageOccurred(error=text))
197
+ return
198
+ if not self._streamed_text:
199
+ self._post_ui(
200
+ AssistantMessageChunk(delta=text, is_complete=True)
201
+ )
202
+
203
+ def is_streaming(self) -> bool:
204
+ return self._current_worker is not None
205
+
206
+ def is_session_ready(self) -> bool:
207
+ return self._session is not None and getattr(self._session, "_graph", None) is not None
208
+
209
+ @property
210
+ def session(self) -> AgentSession:
211
+ return self._session
212
+
213
+ @property
214
+ def permission_queue(self) -> asyncio.Queue[PermissionRequest]:
215
+ return self._permission_queue
216
+
217
+ @property
218
+ def permission_cache(self) -> SessionPermissionCache:
219
+ return self._permission_cache
220
+
221
+ @property
222
+ def permission_resolver(self) -> AgentSessionPermissionResolver:
223
+ return self._permission_resolver
224
+
225
+ @property
226
+ def permission_consumer(self) -> PermissionQueueConsumer:
227
+ return self._permission_consumer
228
+
229
+ async def dispatch_command(
230
+ self,
231
+ raw_input: str,
232
+ *,
233
+ output: Callable[[str], None] | None = None,
234
+ ) -> str | None:
235
+ if not self.is_session_ready():
236
+ message = "错误:会话未建立,无法执行该命令"
237
+ if output is not None:
238
+ output(message)
239
+ return message
240
+ result = await self._session.dispatch_command(raw_input)
241
+ if result is None:
242
+ return None
243
+ if result.error:
244
+ message = str(result.error)
245
+ if output is not None:
246
+ output(message)
247
+ return message
248
+ if result.output:
249
+ message = str(result.output)
250
+ if output is not None:
251
+ output(message)
252
+ return message
253
+ return None
254
+
255
+ def cancel_stream(self) -> None:
256
+ if self._current_worker is not None:
257
+ cancel = getattr(self._current_worker, "cancel", None)
258
+ if callable(cancel):
259
+ cancel()
260
+ self._current_worker = None
261
+ self._post_ui(TurnInProgress(in_progress=False))
@@ -0,0 +1,223 @@
1
+ """
2
+ cli.py — CLI 入口(click)。
3
+
4
+ 职责:
5
+ 解析命令行、环境检查、解析 workspace、创建 AgentSession、启动 ReplApp。
6
+
7
+ 链路位置:
8
+ python -m langchain_agentx_cli / 未来 console_scripts。
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ import click
18
+
19
+ from langchain_agentx_cli.app import ReplApp
20
+ from langchain_agentx_cli.bootstrap import CliEnvironmentError, EnvironmentChecker
21
+ from langchain_agentx_cli.config import AppPreferencesStore, ReplLaunchConfig
22
+ from langchain_agentx_cli.llm_config import (
23
+ ANTHROPIC_MODEL_ENV,
24
+ format_resolved_config,
25
+ resolve_llm_config,
26
+ user_config_path,
27
+ )
28
+ from langchain_agentx_cli.session_factory import close_agent_session, open_agent_session
29
+ from langchain_agentx_cli.session_options import ENABLE_GIT_SNAPSHOT_ENV
30
+
31
+
32
+ class CliEntry:
33
+ """CLI 主编排:参数 → 校验 → Session → TUI。"""
34
+
35
+ def __init__(self, checker: EnvironmentChecker | None = None) -> None:
36
+ self._checker = checker or EnvironmentChecker()
37
+
38
+ def run(
39
+ self,
40
+ *,
41
+ workspace_root: Path | None,
42
+ mode: str | None,
43
+ agent_home: str | None,
44
+ model: str | None,
45
+ provider: str | None,
46
+ show_config: bool,
47
+ enable_git_snapshot: bool | None,
48
+ skip_permissions: bool = False,
49
+ ) -> int:
50
+ # Mode 到 agent_home 的映射(优先级高于 --agent-home)
51
+ if mode:
52
+ agent_home = self._resolve_mode_to_agent_home(mode)
53
+ try:
54
+ launch_config = self._build_launch_config(
55
+ workspace_root=workspace_root,
56
+ agent_home=agent_home,
57
+ model=model,
58
+ provider=provider,
59
+ skip_permissions=skip_permissions,
60
+ )
61
+ except CliEnvironmentError as exc:
62
+ EnvironmentChecker.emit_error(str(exc))
63
+ return 2
64
+
65
+ if show_config:
66
+ cfg = resolve_llm_config(
67
+ cli_provider=provider,
68
+ cli_model=model,
69
+ )
70
+ print(format_resolved_config(cfg))
71
+ return 0
72
+
73
+ try:
74
+ session = asyncio.run(
75
+ open_agent_session(
76
+ launch_config,
77
+ cli_provider=provider,
78
+ cli_model=model,
79
+ enable_git_snapshot=enable_git_snapshot,
80
+ )
81
+ )
82
+ except CliEnvironmentError as exc:
83
+ EnvironmentChecker.emit_error(str(exc))
84
+ return 2
85
+
86
+ try:
87
+ return self._run_tui(launch_config, session)
88
+ finally:
89
+ asyncio.run(close_agent_session(session))
90
+
91
+ def _build_launch_config(
92
+ self,
93
+ *,
94
+ workspace_root: Path | None,
95
+ agent_home: str | None,
96
+ model: str | None,
97
+ provider: str | None,
98
+ skip_permissions: bool = False,
99
+ ) -> ReplLaunchConfig:
100
+ self._checker.check_sdk_importable()
101
+
102
+ from langchain_agentx.workspace import (
103
+ resolve_agent_workspace_config,
104
+ resolve_workspace_root_path,
105
+ )
106
+
107
+ resolved_root = resolve_workspace_root_path(workspace_root)
108
+ self._checker.check_workspace_root(resolved_root)
109
+
110
+ workspace_cfg = resolve_agent_workspace_config(
111
+ workspace_root=resolved_root,
112
+ agent_home=agent_home,
113
+ )
114
+
115
+ llm_cfg = resolve_llm_config(cli_provider=provider, cli_model=model)
116
+ ui_prefs = AppPreferencesStore().load()
117
+
118
+ return ReplLaunchConfig(
119
+ workspace_root=Path(workspace_cfg.workspace_root),
120
+ agent_home=workspace_cfg.agent_home,
121
+ workspace_cfg=workspace_cfg,
122
+ model=llm_cfg.model,
123
+ provider=llm_cfg.provider,
124
+ theme=ui_prefs.theme,
125
+ skip_permissions=skip_permissions,
126
+ )
127
+
128
+ def _resolve_mode_to_agent_home(self, mode: str) -> str:
129
+ """将 mode 映射到 agent_home 目录名。"""
130
+ mode_map = {
131
+ "agentx": ".langchain_agentx",
132
+ "claude": ".claude",
133
+ "cursor": ".cursor",
134
+ }
135
+ return mode_map.get(mode.lower(), ".langchain_agentx")
136
+
137
+ def _run_tui(self, launch_config: ReplLaunchConfig, session) -> int: # noqa: ANN001
138
+ app = ReplApp(launch_config=launch_config, session=session)
139
+ app.run()
140
+ return 0
141
+
142
+
143
+ _CONFIG_PATH = user_config_path()
144
+
145
+
146
+ @click.command(context_settings={"help_option_names": ["-h", "--help"]})
147
+ @click.option(
148
+ "--workspace-root",
149
+ type=click.Path(exists=False, file_okay=False, dir_okay=True, path_type=Path),
150
+ default=None,
151
+ help="工作区根目录(默认:当前目录或 LANGCHAIN_AGENTX_WORKSPACE_ROOT)",
152
+ )
153
+ @click.option(
154
+ "--mode",
155
+ type=click.Choice(["agentx", "claude", "cursor"], case_sensitive=False),
156
+ default=None,
157
+ help="配置目录模式:agentx(.langchain_agentx) / claude(.claude) / cursor(.cursor)",
158
+ )
159
+ @click.option(
160
+ "--agent-home",
161
+ default=None,
162
+ help="Agent 配置目录名(相对 workspace,优先级低于 --mode)",
163
+ )
164
+ @click.option(
165
+ "--model",
166
+ default=None,
167
+ help=(
168
+ f"模型名(claude:未指定时读 {ANTHROPIC_MODEL_ENV}、配置文件或默认;"
169
+ f"openai:配置文件或默认;见 {_CONFIG_PATH})"
170
+ ),
171
+ )
172
+ @click.option(
173
+ "--provider",
174
+ type=click.Choice(["claude", "openai"], case_sensitive=False),
175
+ default=None,
176
+ help=(
177
+ "LLM provider:claude(Anthropic/CC 变量)或 openai(OpenAI 兼容);"
178
+ f"未指定时读配置文件或按模型名启发式;见 {_CONFIG_PATH}"
179
+ ),
180
+ )
181
+ @click.option(
182
+ "--show-config",
183
+ is_flag=True,
184
+ default=False,
185
+ help="打印合并后的 LLM 配置(api_key 脱敏)后退出,不启动 TUI",
186
+ )
187
+ @click.option(
188
+ "--enable-git-snapshot/--no-git-snapshot",
189
+ default=None,
190
+ help=(
191
+ "是否在 system prompt 注入 git 启动快照(默认关);"
192
+ f"未指定时读环境变量 {ENABLE_GIT_SNAPSHOT_ENV}=1"
193
+ ),
194
+ )
195
+ @click.option(
196
+ "-p",
197
+ "--skip-permissions",
198
+ is_flag=True,
199
+ default=False,
200
+ help="跳过工具权限确认(对齐 CC --dangerously-skip-permissions)",
201
+ )
202
+ def main(
203
+ workspace_root: Path | None,
204
+ mode: str | None,
205
+ agent_home: str | None,
206
+ model: str | None,
207
+ provider: str | None,
208
+ show_config: bool,
209
+ enable_git_snapshot: bool | None,
210
+ skip_permissions: bool,
211
+ ) -> None:
212
+ """langchain-agentx 终端 REPL(MVP)。"""
213
+ code = CliEntry().run(
214
+ workspace_root=workspace_root,
215
+ mode=mode,
216
+ agent_home=agent_home,
217
+ model=model,
218
+ provider=provider,
219
+ show_config=show_config,
220
+ enable_git_snapshot=enable_git_snapshot,
221
+ skip_permissions=skip_permissions,
222
+ )
223
+ raise SystemExit(code)