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.
- langchain_agentx_cli/__init__.py +3 -0
- langchain_agentx_cli/__main__.py +8 -0
- langchain_agentx_cli/app.py +149 -0
- langchain_agentx_cli/bootstrap.py +49 -0
- langchain_agentx_cli/bridge/__init__.py +5 -0
- langchain_agentx_cli/bridge/session_bridge.py +261 -0
- langchain_agentx_cli/cli.py +223 -0
- langchain_agentx_cli/commands/__init__.py +19 -0
- langchain_agentx_cli/commands/builtin/__init__.py +26 -0
- langchain_agentx_cli/commands/builtin/clear.py +17 -0
- langchain_agentx_cli/commands/builtin/compact.py +26 -0
- langchain_agentx_cli/commands/builtin/help.py +27 -0
- langchain_agentx_cli/commands/builtin/model.py +24 -0
- langchain_agentx_cli/commands/builtin/quit.py +18 -0
- langchain_agentx_cli/commands/builtin/theme.py +82 -0
- langchain_agentx_cli/commands/parser.py +70 -0
- langchain_agentx_cli/commands/providers/__init__.py +3 -0
- langchain_agentx_cli/commands/providers/sdk_commands.py +44 -0
- langchain_agentx_cli/commands/registry.py +95 -0
- langchain_agentx_cli/completion/__init__.py +10 -0
- langchain_agentx_cli/completion/base.py +26 -0
- langchain_agentx_cli/completion/command_source.py +42 -0
- langchain_agentx_cli/completion/history_source.py +34 -0
- langchain_agentx_cli/config.py +131 -0
- langchain_agentx_cli/history/__init__.py +3 -0
- langchain_agentx_cli/history/store.py +129 -0
- langchain_agentx_cli/keybindings/__init__.py +3 -0
- langchain_agentx_cli/keybindings/bindings.py +43 -0
- langchain_agentx_cli/llm_config.py +257 -0
- langchain_agentx_cli/permissions/__init__.py +32 -0
- langchain_agentx_cli/permissions/factory.py +93 -0
- langchain_agentx_cli/permissions/policy.py +119 -0
- langchain_agentx_cli/prompts/__init__.py +47 -0
- langchain_agentx_cli/prompts/assembler.py +63 -0
- langchain_agentx_cli/prompts/sections.py +59 -0
- langchain_agentx_cli/prompts/system_prompt.py +105 -0
- langchain_agentx_cli/screens/__init__.py +5 -0
- langchain_agentx_cli/screens/repl.py +238 -0
- langchain_agentx_cli/session_factory.py +110 -0
- langchain_agentx_cli/session_options.py +56 -0
- langchain_agentx_cli/theme/__init__.py +104 -0
- langchain_agentx_cli/theme/colors.py +209 -0
- langchain_agentx_cli/theme/detection.py +31 -0
- langchain_agentx_cli/theme/manager.py +160 -0
- langchain_agentx_cli/theme/settings.py +56 -0
- langchain_agentx_cli/theme/system_theme.py +116 -0
- langchain_agentx_cli/theme/themes.py +167 -0
- langchain_agentx_cli/theme/watcher.py +56 -0
- langchain_agentx_cli/tools/__init__.py +13 -0
- langchain_agentx_cli/tools/registry.py +126 -0
- langchain_agentx_cli/tui/__init__.py +12 -0
- langchain_agentx_cli/tui/message_selection.py +64 -0
- langchain_agentx_cli/tui/permissions/__init__.py +39 -0
- langchain_agentx_cli/tui/permissions/cache.py +39 -0
- langchain_agentx_cli/tui/permissions/config.py +14 -0
- langchain_agentx_cli/tui/permissions/consumer.py +158 -0
- langchain_agentx_cli/tui/permissions/dialog.py +125 -0
- langchain_agentx_cli/tui/permissions/inject.py +57 -0
- langchain_agentx_cli/tui/permissions/models.py +27 -0
- langchain_agentx_cli/tui/permissions/presenters/__init__.py +63 -0
- langchain_agentx_cli/tui/permissions/presenters/base.py +68 -0
- langchain_agentx_cli/tui/permissions/presenters/bash.py +91 -0
- langchain_agentx_cli/tui/permissions/presenters/fallback.py +25 -0
- langchain_agentx_cli/tui/permissions/presenters/file.py +87 -0
- langchain_agentx_cli/tui/safe_screen.py +50 -0
- langchain_agentx_cli/welcome.py +48 -0
- langchain_agentx_cli/widgets/__init__.py +25 -0
- langchain_agentx_cli/widgets/completion_overlay.py +145 -0
- langchain_agentx_cli/widgets/input_area.py +492 -0
- langchain_agentx_cli/widgets/message_events.py +137 -0
- langchain_agentx_cli/widgets/message_list.py +458 -0
- langchain_agentx_cli/widgets/messages/__init__.py +57 -0
- langchain_agentx_cli/widgets/messages/assistant_message.py +224 -0
- langchain_agentx_cli/widgets/messages/error_message.py +33 -0
- langchain_agentx_cli/widgets/messages/rendering.py +137 -0
- langchain_agentx_cli/widgets/messages/thinking_message.py +78 -0
- langchain_agentx_cli/widgets/pending_permission.py +69 -0
- langchain_agentx_cli/widgets/permission_inline.py +141 -0
- langchain_agentx_cli/widgets/spinner.py +318 -0
- langchain_agentx_cli/widgets/tools/__init__.py +103 -0
- langchain_agentx_cli/widgets/tools/agent/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/agent/widget.py +68 -0
- langchain_agentx_cli/widgets/tools/ask_user_question/__init__.py +7 -0
- langchain_agentx_cli/widgets/tools/ask_user_question/widget.py +61 -0
- langchain_agentx_cli/widgets/tools/base.py +178 -0
- langchain_agentx_cli/widgets/tools/bash/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/bash/widget.py +77 -0
- langchain_agentx_cli/widgets/tools/batch_edit/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/batch_edit/widget.py +62 -0
- langchain_agentx_cli/widgets/tools/edit/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/edit/widget.py +66 -0
- langchain_agentx_cli/widgets/tools/glob/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/glob/widget.py +49 -0
- langchain_agentx_cli/widgets/tools/grep/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/grep/widget.py +49 -0
- langchain_agentx_cli/widgets/tools/helpers.py +260 -0
- langchain_agentx_cli/widgets/tools/read/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/read/widget.py +92 -0
- langchain_agentx_cli/widgets/tools/skill/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/skill/widget.py +53 -0
- langchain_agentx_cli/widgets/tools/task_create/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/task_create/widget.py +43 -0
- langchain_agentx_cli/widgets/tools/task_get/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/task_get/widget.py +52 -0
- langchain_agentx_cli/widgets/tools/task_list/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/task_list/widget.py +49 -0
- langchain_agentx_cli/widgets/tools/task_update/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/task_update/widget.py +45 -0
- langchain_agentx_cli/widgets/tools/user_message/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/user_message/widget.py +93 -0
- langchain_agentx_cli/widgets/tools/webfetch/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/webfetch/widget.py +58 -0
- langchain_agentx_cli/widgets/tools/websearch/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/websearch/widget.py +53 -0
- langchain_agentx_cli/widgets/tools/write/__init__.py +5 -0
- langchain_agentx_cli/widgets/tools/write/widget.py +71 -0
- langchain_agentx_cli-0.1.0.dist-info/METADATA +140 -0
- langchain_agentx_cli-0.1.0.dist-info/RECORD +122 -0
- langchain_agentx_cli-0.1.0.dist-info/WHEEL +5 -0
- langchain_agentx_cli-0.1.0.dist-info/entry_points.txt +2 -0
- langchain_agentx_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- langchain_agentx_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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,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)
|