codemaster-cli 2.2.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.
- codemaster_cli-2.2.0.dist-info/METADATA +645 -0
- codemaster_cli-2.2.0.dist-info/RECORD +170 -0
- codemaster_cli-2.2.0.dist-info/WHEEL +4 -0
- codemaster_cli-2.2.0.dist-info/entry_points.txt +3 -0
- vibe/__init__.py +6 -0
- vibe/acp/__init__.py +0 -0
- vibe/acp/acp_agent_loop.py +746 -0
- vibe/acp/entrypoint.py +81 -0
- vibe/acp/tools/__init__.py +0 -0
- vibe/acp/tools/base.py +100 -0
- vibe/acp/tools/builtins/bash.py +134 -0
- vibe/acp/tools/builtins/read_file.py +54 -0
- vibe/acp/tools/builtins/search_replace.py +129 -0
- vibe/acp/tools/builtins/todo.py +65 -0
- vibe/acp/tools/builtins/write_file.py +98 -0
- vibe/acp/tools/session_update.py +118 -0
- vibe/acp/utils.py +213 -0
- vibe/cli/__init__.py +0 -0
- vibe/cli/autocompletion/__init__.py +0 -0
- vibe/cli/autocompletion/base.py +22 -0
- vibe/cli/autocompletion/path_completion.py +177 -0
- vibe/cli/autocompletion/slash_command.py +99 -0
- vibe/cli/cli.py +188 -0
- vibe/cli/clipboard.py +69 -0
- vibe/cli/commands.py +116 -0
- vibe/cli/entrypoint.py +163 -0
- vibe/cli/history_manager.py +91 -0
- vibe/cli/plan_offer/adapters/http_whoami_gateway.py +67 -0
- vibe/cli/plan_offer/decide_plan_offer.py +87 -0
- vibe/cli/plan_offer/ports/whoami_gateway.py +23 -0
- vibe/cli/terminal_setup.py +323 -0
- vibe/cli/textual_ui/__init__.py +0 -0
- vibe/cli/textual_ui/ansi_markdown.py +58 -0
- vibe/cli/textual_ui/app.py +1546 -0
- vibe/cli/textual_ui/app.tcss +1020 -0
- vibe/cli/textual_ui/external_editor.py +32 -0
- vibe/cli/textual_ui/handlers/__init__.py +5 -0
- vibe/cli/textual_ui/handlers/event_handler.py +147 -0
- vibe/cli/textual_ui/widgets/__init__.py +0 -0
- vibe/cli/textual_ui/widgets/approval_app.py +192 -0
- vibe/cli/textual_ui/widgets/banner/banner.py +85 -0
- vibe/cli/textual_ui/widgets/banner/petit_chat.py +195 -0
- vibe/cli/textual_ui/widgets/braille_renderer.py +58 -0
- vibe/cli/textual_ui/widgets/chat_input/__init__.py +7 -0
- vibe/cli/textual_ui/widgets/chat_input/body.py +214 -0
- vibe/cli/textual_ui/widgets/chat_input/completion_manager.py +58 -0
- vibe/cli/textual_ui/widgets/chat_input/completion_popup.py +43 -0
- vibe/cli/textual_ui/widgets/chat_input/container.py +195 -0
- vibe/cli/textual_ui/widgets/chat_input/text_area.py +365 -0
- vibe/cli/textual_ui/widgets/compact.py +41 -0
- vibe/cli/textual_ui/widgets/config_app.py +171 -0
- vibe/cli/textual_ui/widgets/context_progress.py +30 -0
- vibe/cli/textual_ui/widgets/load_more.py +43 -0
- vibe/cli/textual_ui/widgets/loading.py +201 -0
- vibe/cli/textual_ui/widgets/messages.py +277 -0
- vibe/cli/textual_ui/widgets/no_markup_static.py +11 -0
- vibe/cli/textual_ui/widgets/path_display.py +28 -0
- vibe/cli/textual_ui/widgets/proxy_setup_app.py +127 -0
- vibe/cli/textual_ui/widgets/question_app.py +496 -0
- vibe/cli/textual_ui/widgets/spinner.py +194 -0
- vibe/cli/textual_ui/widgets/status_message.py +76 -0
- vibe/cli/textual_ui/widgets/teleport_message.py +31 -0
- vibe/cli/textual_ui/widgets/tool_widgets.py +371 -0
- vibe/cli/textual_ui/widgets/tools.py +201 -0
- vibe/cli/textual_ui/windowing/__init__.py +29 -0
- vibe/cli/textual_ui/windowing/history.py +105 -0
- vibe/cli/textual_ui/windowing/history_windowing.py +71 -0
- vibe/cli/textual_ui/windowing/state.py +105 -0
- vibe/cli/update_notifier/__init__.py +47 -0
- vibe/cli/update_notifier/adapters/filesystem_update_cache_repository.py +59 -0
- vibe/cli/update_notifier/adapters/github_update_gateway.py +101 -0
- vibe/cli/update_notifier/adapters/pypi_update_gateway.py +107 -0
- vibe/cli/update_notifier/ports/update_cache_repository.py +16 -0
- vibe/cli/update_notifier/ports/update_gateway.py +53 -0
- vibe/cli/update_notifier/update.py +139 -0
- vibe/cli/update_notifier/whats_new.py +49 -0
- vibe/core/__init__.py +5 -0
- vibe/core/agent_loop.py +1075 -0
- vibe/core/agents/__init__.py +31 -0
- vibe/core/agents/manager.py +165 -0
- vibe/core/agents/models.py +122 -0
- vibe/core/auth/__init__.py +6 -0
- vibe/core/auth/crypto.py +137 -0
- vibe/core/auth/github.py +178 -0
- vibe/core/autocompletion/__init__.py +0 -0
- vibe/core/autocompletion/completers.py +257 -0
- vibe/core/autocompletion/file_indexer/__init__.py +10 -0
- vibe/core/autocompletion/file_indexer/ignore_rules.py +156 -0
- vibe/core/autocompletion/file_indexer/indexer.py +179 -0
- vibe/core/autocompletion/file_indexer/store.py +169 -0
- vibe/core/autocompletion/file_indexer/watcher.py +71 -0
- vibe/core/autocompletion/fuzzy.py +189 -0
- vibe/core/autocompletion/path_prompt.py +108 -0
- vibe/core/autocompletion/path_prompt_adapter.py +149 -0
- vibe/core/config.py +673 -0
- vibe/core/config_PATCH_INSTRUCTIONS.md +77 -0
- vibe/core/llm/__init__.py +0 -0
- vibe/core/llm/backend/anthropic.py +630 -0
- vibe/core/llm/backend/base.py +38 -0
- vibe/core/llm/backend/factory.py +7 -0
- vibe/core/llm/backend/generic.py +425 -0
- vibe/core/llm/backend/mistral.py +381 -0
- vibe/core/llm/backend/vertex.py +115 -0
- vibe/core/llm/exceptions.py +195 -0
- vibe/core/llm/format.py +184 -0
- vibe/core/llm/message_utils.py +24 -0
- vibe/core/llm/types.py +120 -0
- vibe/core/middleware.py +209 -0
- vibe/core/output_formatters.py +85 -0
- vibe/core/paths/__init__.py +0 -0
- vibe/core/paths/config_paths.py +68 -0
- vibe/core/paths/global_paths.py +40 -0
- vibe/core/programmatic.py +56 -0
- vibe/core/prompts/__init__.py +32 -0
- vibe/core/prompts/cli.md +111 -0
- vibe/core/prompts/compact.md +48 -0
- vibe/core/prompts/dangerous_directory.md +5 -0
- vibe/core/prompts/explore.md +50 -0
- vibe/core/prompts/project_context.md +8 -0
- vibe/core/prompts/tests.md +1 -0
- vibe/core/proxy_setup.py +65 -0
- vibe/core/session/session_loader.py +222 -0
- vibe/core/session/session_logger.py +318 -0
- vibe/core/session/session_migration.py +41 -0
- vibe/core/skills/__init__.py +7 -0
- vibe/core/skills/manager.py +132 -0
- vibe/core/skills/models.py +92 -0
- vibe/core/skills/parser.py +39 -0
- vibe/core/system_prompt.py +466 -0
- vibe/core/telemetry/__init__.py +0 -0
- vibe/core/telemetry/send.py +185 -0
- vibe/core/teleport/errors.py +9 -0
- vibe/core/teleport/git.py +196 -0
- vibe/core/teleport/nuage.py +180 -0
- vibe/core/teleport/teleport.py +208 -0
- vibe/core/teleport/types.py +54 -0
- vibe/core/tools/base.py +336 -0
- vibe/core/tools/builtins/ask_user_question.py +134 -0
- vibe/core/tools/builtins/bash.py +357 -0
- vibe/core/tools/builtins/grep.py +310 -0
- vibe/core/tools/builtins/prompts/__init__.py +0 -0
- vibe/core/tools/builtins/prompts/ask_user_question.md +84 -0
- vibe/core/tools/builtins/prompts/bash.md +73 -0
- vibe/core/tools/builtins/prompts/grep.md +4 -0
- vibe/core/tools/builtins/prompts/read_file.md +13 -0
- vibe/core/tools/builtins/prompts/search_replace.md +43 -0
- vibe/core/tools/builtins/prompts/task.md +24 -0
- vibe/core/tools/builtins/prompts/todo.md +199 -0
- vibe/core/tools/builtins/prompts/write_file.md +42 -0
- vibe/core/tools/builtins/read_file.py +222 -0
- vibe/core/tools/builtins/search_replace.py +456 -0
- vibe/core/tools/builtins/task.py +154 -0
- vibe/core/tools/builtins/todo.py +134 -0
- vibe/core/tools/builtins/write_file.py +160 -0
- vibe/core/tools/manager.py +341 -0
- vibe/core/tools/mcp.py +397 -0
- vibe/core/tools/ui.py +68 -0
- vibe/core/trusted_folders.py +86 -0
- vibe/core/types.py +405 -0
- vibe/core/utils.py +396 -0
- vibe/setup/onboarding/__init__.py +39 -0
- vibe/setup/onboarding/base.py +14 -0
- vibe/setup/onboarding/onboarding.tcss +134 -0
- vibe/setup/onboarding/screens/__init__.py +5 -0
- vibe/setup/onboarding/screens/api_key.py +200 -0
- vibe/setup/onboarding/screens/provider_selection.py +87 -0
- vibe/setup/onboarding/screens/welcome.py +136 -0
- vibe/setup/trusted_folders/trust_folder_dialog.py +180 -0
- vibe/setup/trusted_folders/trust_folder_dialog.tcss +83 -0
- vibe/whats_new.md +5 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import AsyncGenerator
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any, cast, override
|
|
9
|
+
|
|
10
|
+
from acp import (
|
|
11
|
+
PROTOCOL_VERSION,
|
|
12
|
+
Agent as AcpAgent,
|
|
13
|
+
Client,
|
|
14
|
+
InitializeResponse,
|
|
15
|
+
LoadSessionResponse,
|
|
16
|
+
NewSessionResponse,
|
|
17
|
+
PromptResponse,
|
|
18
|
+
RequestError,
|
|
19
|
+
SetSessionModelResponse,
|
|
20
|
+
SetSessionModeResponse,
|
|
21
|
+
run_agent,
|
|
22
|
+
)
|
|
23
|
+
from acp.helpers import ContentBlock, SessionUpdate, update_available_commands
|
|
24
|
+
from acp.schema import (
|
|
25
|
+
AgentCapabilities,
|
|
26
|
+
AgentMessageChunk,
|
|
27
|
+
AgentThoughtChunk,
|
|
28
|
+
AllowedOutcome,
|
|
29
|
+
AuthenticateResponse,
|
|
30
|
+
AuthMethod,
|
|
31
|
+
AvailableCommand,
|
|
32
|
+
AvailableCommandInput,
|
|
33
|
+
ClientCapabilities,
|
|
34
|
+
ContentToolCallContent,
|
|
35
|
+
ForkSessionResponse,
|
|
36
|
+
HttpMcpServer,
|
|
37
|
+
Implementation,
|
|
38
|
+
ListSessionsResponse,
|
|
39
|
+
McpServerStdio,
|
|
40
|
+
ModelInfo,
|
|
41
|
+
PromptCapabilities,
|
|
42
|
+
ResumeSessionResponse,
|
|
43
|
+
SessionCapabilities,
|
|
44
|
+
SessionInfo,
|
|
45
|
+
SessionListCapabilities,
|
|
46
|
+
SessionModelState,
|
|
47
|
+
SessionModeState,
|
|
48
|
+
SseMcpServer,
|
|
49
|
+
TextContentBlock,
|
|
50
|
+
TextResourceContents,
|
|
51
|
+
ToolCallProgress,
|
|
52
|
+
ToolCallUpdate,
|
|
53
|
+
UnstructuredCommandInput,
|
|
54
|
+
UserMessageChunk,
|
|
55
|
+
)
|
|
56
|
+
from pydantic import BaseModel, ConfigDict
|
|
57
|
+
|
|
58
|
+
from vibe import VIBE_ROOT, __version__
|
|
59
|
+
from vibe.acp.tools.base import BaseAcpTool
|
|
60
|
+
from vibe.acp.tools.session_update import (
|
|
61
|
+
tool_call_session_update,
|
|
62
|
+
tool_result_session_update,
|
|
63
|
+
)
|
|
64
|
+
from vibe.acp.utils import (
|
|
65
|
+
TOOL_OPTIONS,
|
|
66
|
+
ToolOption,
|
|
67
|
+
create_assistant_message_replay,
|
|
68
|
+
create_compact_end_session_update,
|
|
69
|
+
create_compact_start_session_update,
|
|
70
|
+
create_reasoning_replay,
|
|
71
|
+
create_tool_call_replay,
|
|
72
|
+
create_tool_result_replay,
|
|
73
|
+
create_user_message_replay,
|
|
74
|
+
get_all_acp_session_modes,
|
|
75
|
+
get_proxy_help_text,
|
|
76
|
+
is_valid_acp_agent,
|
|
77
|
+
)
|
|
78
|
+
from vibe.core.agent_loop import AgentLoop
|
|
79
|
+
from vibe.core.agents.models import BuiltinAgentName
|
|
80
|
+
from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt
|
|
81
|
+
from vibe.core.config import (
|
|
82
|
+
MissingAPIKeyError,
|
|
83
|
+
SessionLoggingConfig,
|
|
84
|
+
VibeConfig,
|
|
85
|
+
load_dotenv_values,
|
|
86
|
+
)
|
|
87
|
+
from vibe.core.proxy_setup import (
|
|
88
|
+
ProxySetupError,
|
|
89
|
+
parse_proxy_command,
|
|
90
|
+
set_proxy_var,
|
|
91
|
+
unset_proxy_var,
|
|
92
|
+
)
|
|
93
|
+
from vibe.core.session.session_loader import SessionLoader
|
|
94
|
+
from vibe.core.tools.base import BaseToolConfig, ToolPermission
|
|
95
|
+
from vibe.core.types import (
|
|
96
|
+
ApprovalResponse,
|
|
97
|
+
AssistantEvent,
|
|
98
|
+
AsyncApprovalCallback,
|
|
99
|
+
CompactEndEvent,
|
|
100
|
+
CompactStartEvent,
|
|
101
|
+
LLMMessage,
|
|
102
|
+
ReasoningEvent,
|
|
103
|
+
Role,
|
|
104
|
+
ToolCallEvent,
|
|
105
|
+
ToolResultEvent,
|
|
106
|
+
ToolStreamEvent,
|
|
107
|
+
UserMessageEvent,
|
|
108
|
+
)
|
|
109
|
+
from vibe.core.utils import CancellationReason, get_user_cancellation_message
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class AcpSessionLoop(BaseModel):
|
|
113
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
114
|
+
id: str
|
|
115
|
+
agent_loop: AgentLoop
|
|
116
|
+
task: asyncio.Task[None] | None = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class VibeAcpAgentLoop(AcpAgent):
|
|
120
|
+
client: Client
|
|
121
|
+
|
|
122
|
+
def __init__(self) -> None:
|
|
123
|
+
self.sessions: dict[str, AcpSessionLoop] = {}
|
|
124
|
+
self.client_capabilities = None
|
|
125
|
+
|
|
126
|
+
@override
|
|
127
|
+
async def initialize(
|
|
128
|
+
self,
|
|
129
|
+
protocol_version: int,
|
|
130
|
+
client_capabilities: ClientCapabilities | None = None,
|
|
131
|
+
client_info: Implementation | None = None,
|
|
132
|
+
**kwargs: Any,
|
|
133
|
+
) -> InitializeResponse:
|
|
134
|
+
self.client_capabilities = client_capabilities
|
|
135
|
+
|
|
136
|
+
# The ACP Agent process can be launched in 3 different ways, depending on installation
|
|
137
|
+
# - dev mode: `uv run vibe-acp`, ran from the project root
|
|
138
|
+
# - uv tool install: `vibe-acp`, similar to dev mode, but uv takes care of path resolution
|
|
139
|
+
# - bundled binary: `./vibe-acp` from binary location
|
|
140
|
+
# The 2 first modes are working similarly, under the hood uv runs `/some/python /my/entrypoint.py``
|
|
141
|
+
# The last mode is quite different as our bundler also includes the python install.
|
|
142
|
+
# So sys.executable is already /path/to/binary/vibe-acp.
|
|
143
|
+
# For this reason, we make a distinction in the way we call the setup command
|
|
144
|
+
command = sys.executable
|
|
145
|
+
if "python" not in Path(command).name:
|
|
146
|
+
# It's the case for bundled binaries, we don't need any other arguments
|
|
147
|
+
args = ["--setup"]
|
|
148
|
+
else:
|
|
149
|
+
script_name = sys.argv[0]
|
|
150
|
+
args = [script_name, "--setup"]
|
|
151
|
+
|
|
152
|
+
supports_terminal_auth = (
|
|
153
|
+
self.client_capabilities
|
|
154
|
+
and self.client_capabilities.field_meta
|
|
155
|
+
and self.client_capabilities.field_meta.get("terminal-auth") is True
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
auth_methods = (
|
|
159
|
+
[
|
|
160
|
+
AuthMethod(
|
|
161
|
+
id="vibe-setup",
|
|
162
|
+
name="Register your API Key",
|
|
163
|
+
description="Register your API Key inside codeMaster",
|
|
164
|
+
field_meta={
|
|
165
|
+
"terminal-auth": {
|
|
166
|
+
"command": command,
|
|
167
|
+
"args": args,
|
|
168
|
+
"label": "codeMaster Setup",
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
]
|
|
173
|
+
if supports_terminal_auth
|
|
174
|
+
else []
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
response = InitializeResponse(
|
|
178
|
+
agent_capabilities=AgentCapabilities(
|
|
179
|
+
load_session=True,
|
|
180
|
+
prompt_capabilities=PromptCapabilities(
|
|
181
|
+
audio=False, embedded_context=True, image=False
|
|
182
|
+
),
|
|
183
|
+
session_capabilities=SessionCapabilities(
|
|
184
|
+
list=SessionListCapabilities()
|
|
185
|
+
),
|
|
186
|
+
),
|
|
187
|
+
protocol_version=PROTOCOL_VERSION,
|
|
188
|
+
agent_info=Implementation(
|
|
189
|
+
name="@mistralai/mistral-vibe",
|
|
190
|
+
title="codeMaster",
|
|
191
|
+
version=__version__,
|
|
192
|
+
),
|
|
193
|
+
auth_methods=auth_methods,
|
|
194
|
+
)
|
|
195
|
+
return response
|
|
196
|
+
|
|
197
|
+
@override
|
|
198
|
+
async def authenticate(
|
|
199
|
+
self, method_id: str, **kwargs: Any
|
|
200
|
+
) -> AuthenticateResponse | None:
|
|
201
|
+
raise NotImplementedError("Not implemented yet")
|
|
202
|
+
|
|
203
|
+
def _load_config(self) -> VibeConfig:
|
|
204
|
+
try:
|
|
205
|
+
config = VibeConfig.load(disabled_tools=["ask_user_question"])
|
|
206
|
+
config.tool_paths.extend(self._get_acp_tool_overrides())
|
|
207
|
+
return config
|
|
208
|
+
except MissingAPIKeyError as e:
|
|
209
|
+
raise RequestError.auth_required({
|
|
210
|
+
"message": "You must be authenticated before creating a session"
|
|
211
|
+
}) from e
|
|
212
|
+
|
|
213
|
+
async def _create_acp_session(
|
|
214
|
+
self, session_id: str, agent_loop: AgentLoop
|
|
215
|
+
) -> AcpSessionLoop:
|
|
216
|
+
session = AcpSessionLoop(id=session_id, agent_loop=agent_loop)
|
|
217
|
+
self.sessions[session.id] = session
|
|
218
|
+
|
|
219
|
+
if not agent_loop.auto_approve:
|
|
220
|
+
agent_loop.set_approval_callback(self._create_approval_callback(session.id))
|
|
221
|
+
|
|
222
|
+
asyncio.create_task(self._send_available_commands(session.id))
|
|
223
|
+
|
|
224
|
+
return session
|
|
225
|
+
|
|
226
|
+
def _build_session_model_state(self, agent_loop: AgentLoop) -> SessionModelState:
|
|
227
|
+
return SessionModelState(
|
|
228
|
+
current_model_id=agent_loop.config.active_model,
|
|
229
|
+
available_models=[
|
|
230
|
+
ModelInfo(model_id=model.alias, name=model.alias)
|
|
231
|
+
for model in agent_loop.config.models
|
|
232
|
+
],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def _build_session_mode_state(self, session: AcpSessionLoop) -> SessionModeState:
|
|
236
|
+
return SessionModeState(
|
|
237
|
+
current_mode_id=session.agent_loop.agent_profile.name,
|
|
238
|
+
available_modes=get_all_acp_session_modes(session.agent_loop.agent_manager),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
@override
|
|
242
|
+
async def new_session(
|
|
243
|
+
self,
|
|
244
|
+
cwd: str,
|
|
245
|
+
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
|
|
246
|
+
**kwargs: Any,
|
|
247
|
+
) -> NewSessionResponse:
|
|
248
|
+
load_dotenv_values()
|
|
249
|
+
os.chdir(cwd)
|
|
250
|
+
|
|
251
|
+
config = self._load_config()
|
|
252
|
+
|
|
253
|
+
agent_loop = AgentLoop(
|
|
254
|
+
config=config, agent_name=BuiltinAgentName.DEFAULT, enable_streaming=True
|
|
255
|
+
)
|
|
256
|
+
# NOTE: For now, we pin session.id to agent_loop.session_id right after init time.
|
|
257
|
+
# We should just use agent_loop.session_id everywhere, but it can still change during
|
|
258
|
+
# session lifetime (e.g. agent_loop.compact is called).
|
|
259
|
+
# We should refactor agent_loop.session_id to make it immutable in ACP context.
|
|
260
|
+
session = await self._create_acp_session(agent_loop.session_id, agent_loop)
|
|
261
|
+
agent_loop.emit_new_session_telemetry("acp")
|
|
262
|
+
|
|
263
|
+
return NewSessionResponse(
|
|
264
|
+
session_id=session.id,
|
|
265
|
+
models=self._build_session_model_state(agent_loop),
|
|
266
|
+
modes=self._build_session_mode_state(session),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _get_acp_tool_overrides(self) -> list[Path]:
|
|
270
|
+
overrides = ["todo"]
|
|
271
|
+
|
|
272
|
+
if self.client_capabilities:
|
|
273
|
+
if self.client_capabilities.terminal:
|
|
274
|
+
overrides.append("bash")
|
|
275
|
+
if self.client_capabilities.fs:
|
|
276
|
+
fs = self.client_capabilities.fs
|
|
277
|
+
if fs.read_text_file:
|
|
278
|
+
overrides.append("read_file")
|
|
279
|
+
if fs.write_text_file:
|
|
280
|
+
overrides.extend(["write_file", "search_replace"])
|
|
281
|
+
|
|
282
|
+
return [
|
|
283
|
+
VIBE_ROOT / "acp" / "tools" / "builtins" / f"{override}.py"
|
|
284
|
+
for override in overrides
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
def _create_approval_callback(self, session_id: str) -> AsyncApprovalCallback:
|
|
288
|
+
session = self._get_session(session_id)
|
|
289
|
+
|
|
290
|
+
def _handle_permission_selection(
|
|
291
|
+
option_id: str, tool_name: str
|
|
292
|
+
) -> tuple[ApprovalResponse, str | None]:
|
|
293
|
+
match option_id:
|
|
294
|
+
case ToolOption.ALLOW_ONCE:
|
|
295
|
+
return (ApprovalResponse.YES, None)
|
|
296
|
+
case ToolOption.ALLOW_ALWAYS:
|
|
297
|
+
if tool_name not in session.agent_loop.config.tools:
|
|
298
|
+
session.agent_loop.config.tools[tool_name] = BaseToolConfig()
|
|
299
|
+
session.agent_loop.config.tools[
|
|
300
|
+
tool_name
|
|
301
|
+
].permission = ToolPermission.ALWAYS
|
|
302
|
+
return (ApprovalResponse.YES, None)
|
|
303
|
+
case ToolOption.REJECT_ONCE:
|
|
304
|
+
return (
|
|
305
|
+
ApprovalResponse.NO,
|
|
306
|
+
"User rejected the tool call, provide an alternative plan",
|
|
307
|
+
)
|
|
308
|
+
case _:
|
|
309
|
+
return (ApprovalResponse.NO, f"Unknown option: {option_id}")
|
|
310
|
+
|
|
311
|
+
async def approval_callback(
|
|
312
|
+
tool_name: str, args: BaseModel, tool_call_id: str
|
|
313
|
+
) -> tuple[ApprovalResponse, str | None]:
|
|
314
|
+
# Create the tool call update
|
|
315
|
+
tool_call = ToolCallUpdate(tool_call_id=tool_call_id)
|
|
316
|
+
|
|
317
|
+
response = await self.client.request_permission(
|
|
318
|
+
session_id=session_id, tool_call=tool_call, options=TOOL_OPTIONS
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Parse the response using isinstance for proper type narrowing
|
|
322
|
+
if response.outcome.outcome == "selected":
|
|
323
|
+
outcome = cast(AllowedOutcome, response.outcome)
|
|
324
|
+
return _handle_permission_selection(outcome.option_id, tool_name)
|
|
325
|
+
else:
|
|
326
|
+
return (
|
|
327
|
+
ApprovalResponse.NO,
|
|
328
|
+
str(
|
|
329
|
+
get_user_cancellation_message(
|
|
330
|
+
CancellationReason.OPERATION_CANCELLED
|
|
331
|
+
)
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return approval_callback
|
|
336
|
+
|
|
337
|
+
def _get_session(self, session_id: str) -> AcpSessionLoop:
|
|
338
|
+
if session_id not in self.sessions:
|
|
339
|
+
raise RequestError.invalid_params({"session": "Not found"})
|
|
340
|
+
return self.sessions[session_id]
|
|
341
|
+
|
|
342
|
+
async def _replay_tool_calls(self, session_id: str, msg: LLMMessage) -> None:
|
|
343
|
+
if not msg.tool_calls:
|
|
344
|
+
return
|
|
345
|
+
for tool_call in msg.tool_calls:
|
|
346
|
+
if tool_call.id and tool_call.function.name:
|
|
347
|
+
update = create_tool_call_replay(
|
|
348
|
+
tool_call.id, tool_call.function.name, tool_call.function.arguments
|
|
349
|
+
)
|
|
350
|
+
await self.client.session_update(session_id=session_id, update=update)
|
|
351
|
+
|
|
352
|
+
async def _replay_conversation_history(
|
|
353
|
+
self, session_id: str, messages: list[LLMMessage]
|
|
354
|
+
) -> None:
|
|
355
|
+
for msg in messages:
|
|
356
|
+
if msg.role == Role.user:
|
|
357
|
+
update = create_user_message_replay(msg)
|
|
358
|
+
await self.client.session_update(session_id=session_id, update=update)
|
|
359
|
+
|
|
360
|
+
elif msg.role == Role.assistant:
|
|
361
|
+
if text_update := create_assistant_message_replay(msg):
|
|
362
|
+
await self.client.session_update(
|
|
363
|
+
session_id=session_id, update=text_update
|
|
364
|
+
)
|
|
365
|
+
if reasoning_update := create_reasoning_replay(msg):
|
|
366
|
+
await self.client.session_update(
|
|
367
|
+
session_id=session_id, update=reasoning_update
|
|
368
|
+
)
|
|
369
|
+
await self._replay_tool_calls(session_id, msg)
|
|
370
|
+
|
|
371
|
+
elif msg.role == Role.tool:
|
|
372
|
+
if result_update := create_tool_result_replay(msg):
|
|
373
|
+
await self.client.session_update(
|
|
374
|
+
session_id=session_id, update=result_update
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
async def _send_available_commands(self, session_id: str) -> None:
|
|
378
|
+
commands = [
|
|
379
|
+
AvailableCommand(
|
|
380
|
+
name="proxy-setup",
|
|
381
|
+
description="Configure proxy and SSL certificate settings",
|
|
382
|
+
input=AvailableCommandInput(
|
|
383
|
+
root=UnstructuredCommandInput(
|
|
384
|
+
hint="KEY value to set, KEY to unset, or empty for help"
|
|
385
|
+
)
|
|
386
|
+
),
|
|
387
|
+
)
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
update = update_available_commands(commands)
|
|
391
|
+
await self.client.session_update(session_id=session_id, update=update)
|
|
392
|
+
|
|
393
|
+
async def _handle_proxy_setup_command(
|
|
394
|
+
self, session_id: str, text_prompt: str
|
|
395
|
+
) -> PromptResponse:
|
|
396
|
+
args = text_prompt.strip()[len("/proxy-setup") :].strip()
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
if not args:
|
|
400
|
+
message = get_proxy_help_text()
|
|
401
|
+
else:
|
|
402
|
+
key, value = parse_proxy_command(args)
|
|
403
|
+
if value is not None:
|
|
404
|
+
set_proxy_var(key, value)
|
|
405
|
+
message = f"Set `{key}={value}` in ~/.vibe/.env\n\nPlease start a new chat for changes to take effect."
|
|
406
|
+
else:
|
|
407
|
+
unset_proxy_var(key)
|
|
408
|
+
message = f"Removed `{key}` from ~/.vibe/.env\n\nPlease start a new chat for changes to take effect."
|
|
409
|
+
except ProxySetupError as e:
|
|
410
|
+
message = f"Error: {e}"
|
|
411
|
+
|
|
412
|
+
await self.client.session_update(
|
|
413
|
+
session_id=session_id,
|
|
414
|
+
update=AgentMessageChunk(
|
|
415
|
+
session_update="agent_message_chunk",
|
|
416
|
+
content=TextContentBlock(type="text", text=message),
|
|
417
|
+
),
|
|
418
|
+
)
|
|
419
|
+
return PromptResponse(stop_reason="end_turn")
|
|
420
|
+
|
|
421
|
+
@override
|
|
422
|
+
async def load_session(
|
|
423
|
+
self,
|
|
424
|
+
cwd: str,
|
|
425
|
+
session_id: str,
|
|
426
|
+
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
|
|
427
|
+
**kwargs: Any,
|
|
428
|
+
) -> LoadSessionResponse | None:
|
|
429
|
+
load_dotenv_values()
|
|
430
|
+
os.chdir(cwd)
|
|
431
|
+
|
|
432
|
+
config = self._load_config()
|
|
433
|
+
|
|
434
|
+
session_dir = SessionLoader.find_session_by_id(
|
|
435
|
+
session_id, config.session_logging
|
|
436
|
+
)
|
|
437
|
+
if session_dir is None:
|
|
438
|
+
raise RequestError.invalid_params({
|
|
439
|
+
"session_id": f"Session not found: {session_id}"
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
loaded_messages, _ = SessionLoader.load_session(session_dir)
|
|
444
|
+
except ValueError as e:
|
|
445
|
+
raise RequestError.invalid_params({
|
|
446
|
+
"session_id": f"Failed to load session: {e}"
|
|
447
|
+
}) from e
|
|
448
|
+
|
|
449
|
+
agent_loop = AgentLoop(
|
|
450
|
+
config=config, agent_name=BuiltinAgentName.DEFAULT, enable_streaming=True
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
non_system_messages = [
|
|
454
|
+
msg for msg in loaded_messages if msg.role != Role.system
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
agent_loop.messages.extend(non_system_messages)
|
|
458
|
+
|
|
459
|
+
session = await self._create_acp_session(session_id, agent_loop)
|
|
460
|
+
|
|
461
|
+
await self._replay_conversation_history(session_id, non_system_messages)
|
|
462
|
+
|
|
463
|
+
return LoadSessionResponse(
|
|
464
|
+
models=self._build_session_model_state(agent_loop),
|
|
465
|
+
modes=self._build_session_mode_state(session),
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
@override
|
|
469
|
+
async def set_session_mode(
|
|
470
|
+
self, mode_id: str, session_id: str, **kwargs: Any
|
|
471
|
+
) -> SetSessionModeResponse | None:
|
|
472
|
+
session = self._get_session(session_id)
|
|
473
|
+
|
|
474
|
+
if not is_valid_acp_agent(session.agent_loop.agent_manager, mode_id):
|
|
475
|
+
return None
|
|
476
|
+
|
|
477
|
+
await session.agent_loop.switch_agent(mode_id)
|
|
478
|
+
|
|
479
|
+
if session.agent_loop.auto_approve:
|
|
480
|
+
session.agent_loop.approval_callback = None
|
|
481
|
+
else:
|
|
482
|
+
session.agent_loop.set_approval_callback(
|
|
483
|
+
self._create_approval_callback(session.id)
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
return SetSessionModeResponse()
|
|
487
|
+
|
|
488
|
+
@override
|
|
489
|
+
async def set_session_model(
|
|
490
|
+
self, model_id: str, session_id: str, **kwargs: Any
|
|
491
|
+
) -> SetSessionModelResponse | None:
|
|
492
|
+
session = self._get_session(session_id)
|
|
493
|
+
|
|
494
|
+
model_aliases = [model.alias for model in session.agent_loop.config.models]
|
|
495
|
+
if model_id not in model_aliases:
|
|
496
|
+
return None
|
|
497
|
+
|
|
498
|
+
VibeConfig.save_updates({"active_model": model_id})
|
|
499
|
+
|
|
500
|
+
new_config = VibeConfig.load(
|
|
501
|
+
tool_paths=session.agent_loop.config.tool_paths,
|
|
502
|
+
disabled_tools=["ask_user_question"],
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
await session.agent_loop.reload_with_initial_messages(base_config=new_config)
|
|
506
|
+
|
|
507
|
+
return SetSessionModelResponse()
|
|
508
|
+
|
|
509
|
+
@override
|
|
510
|
+
async def list_sessions(
|
|
511
|
+
self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any
|
|
512
|
+
) -> ListSessionsResponse:
|
|
513
|
+
try:
|
|
514
|
+
config = VibeConfig.load()
|
|
515
|
+
session_logging_config = config.session_logging
|
|
516
|
+
except MissingAPIKeyError:
|
|
517
|
+
session_logging_config = SessionLoggingConfig()
|
|
518
|
+
|
|
519
|
+
session_data = SessionLoader.list_sessions(session_logging_config, cwd=cwd)
|
|
520
|
+
|
|
521
|
+
sessions = [
|
|
522
|
+
SessionInfo(
|
|
523
|
+
session_id=s["session_id"],
|
|
524
|
+
cwd=s["cwd"],
|
|
525
|
+
title=s.get("title"),
|
|
526
|
+
updated_at=s.get("end_time"),
|
|
527
|
+
)
|
|
528
|
+
for s in sorted(
|
|
529
|
+
session_data, key=lambda s: s.get("end_time") or "", reverse=True
|
|
530
|
+
)
|
|
531
|
+
]
|
|
532
|
+
|
|
533
|
+
return ListSessionsResponse(sessions=sessions)
|
|
534
|
+
|
|
535
|
+
@override
|
|
536
|
+
async def prompt(
|
|
537
|
+
self, prompt: list[ContentBlock], session_id: str, **kwargs: Any
|
|
538
|
+
) -> PromptResponse:
|
|
539
|
+
session = self._get_session(session_id)
|
|
540
|
+
|
|
541
|
+
if session.task is not None:
|
|
542
|
+
raise RuntimeError(
|
|
543
|
+
"Concurrent prompts are not supported yet, wait for agent loop to finish"
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
text_prompt = self._build_text_prompt(prompt)
|
|
547
|
+
|
|
548
|
+
if text_prompt.strip().lower().startswith("/proxy-setup"):
|
|
549
|
+
return await self._handle_proxy_setup_command(session_id, text_prompt)
|
|
550
|
+
|
|
551
|
+
temp_user_message_id: str | None = kwargs.get("messageId")
|
|
552
|
+
|
|
553
|
+
async def agent_loop_task() -> None:
|
|
554
|
+
async for update in self._run_agent_loop(
|
|
555
|
+
session, text_prompt, temp_user_message_id
|
|
556
|
+
):
|
|
557
|
+
await self.client.session_update(session_id=session.id, update=update)
|
|
558
|
+
|
|
559
|
+
try:
|
|
560
|
+
session.task = asyncio.create_task(agent_loop_task())
|
|
561
|
+
await session.task
|
|
562
|
+
|
|
563
|
+
except asyncio.CancelledError:
|
|
564
|
+
return PromptResponse(stop_reason="cancelled")
|
|
565
|
+
|
|
566
|
+
except Exception as e:
|
|
567
|
+
await self.client.session_update(
|
|
568
|
+
session_id=session_id,
|
|
569
|
+
update=AgentMessageChunk(
|
|
570
|
+
session_update="agent_message_chunk",
|
|
571
|
+
content=TextContentBlock(type="text", text=f"Error: {e!s}"),
|
|
572
|
+
),
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
return PromptResponse(stop_reason="refusal")
|
|
576
|
+
|
|
577
|
+
finally:
|
|
578
|
+
session.task = None
|
|
579
|
+
|
|
580
|
+
return PromptResponse(stop_reason="end_turn")
|
|
581
|
+
|
|
582
|
+
def _build_text_prompt(self, acp_prompt: list[ContentBlock]) -> str:
|
|
583
|
+
text_prompt = ""
|
|
584
|
+
for block in acp_prompt:
|
|
585
|
+
separator = "\n\n" if text_prompt else ""
|
|
586
|
+
match block.type:
|
|
587
|
+
# NOTE: ACP supports annotations, but we don't use them here yet.
|
|
588
|
+
case "text":
|
|
589
|
+
text_prompt = f"{text_prompt}{separator}{block.text}"
|
|
590
|
+
case "resource":
|
|
591
|
+
block_content = (
|
|
592
|
+
block.resource.text
|
|
593
|
+
if isinstance(block.resource, TextResourceContents)
|
|
594
|
+
else block.resource.blob
|
|
595
|
+
)
|
|
596
|
+
fields = {"path": block.resource.uri, "content": block_content}
|
|
597
|
+
parts = [
|
|
598
|
+
f"{k}: {v}"
|
|
599
|
+
for k, v in fields.items()
|
|
600
|
+
if v is not None and (v or isinstance(v, (int, float)))
|
|
601
|
+
]
|
|
602
|
+
block_prompt = "\n".join(parts)
|
|
603
|
+
text_prompt = f"{text_prompt}{separator}{block_prompt}"
|
|
604
|
+
case "resource_link":
|
|
605
|
+
# NOTE: we currently keep more information than just the URI
|
|
606
|
+
# making it more detailed than the output of the read_file tool.
|
|
607
|
+
# This is OK, but might be worth testing how it affect performance.
|
|
608
|
+
fields = {
|
|
609
|
+
"uri": block.uri,
|
|
610
|
+
"name": block.name,
|
|
611
|
+
"title": block.title,
|
|
612
|
+
"description": block.description,
|
|
613
|
+
"mime_type": block.mime_type,
|
|
614
|
+
"size": block.size,
|
|
615
|
+
}
|
|
616
|
+
parts = [
|
|
617
|
+
f"{k}: {v}"
|
|
618
|
+
for k, v in fields.items()
|
|
619
|
+
if v is not None and (v or isinstance(v, (int, float)))
|
|
620
|
+
]
|
|
621
|
+
block_prompt = "\n".join(parts)
|
|
622
|
+
text_prompt = f"{text_prompt}{separator}{block_prompt}"
|
|
623
|
+
case _:
|
|
624
|
+
raise ValueError(f"Unsupported content block type: {block.type}")
|
|
625
|
+
return text_prompt
|
|
626
|
+
|
|
627
|
+
async def _run_agent_loop(
|
|
628
|
+
self, session: AcpSessionLoop, prompt: str, user_message_id: str | None = None
|
|
629
|
+
) -> AsyncGenerator[SessionUpdate]:
|
|
630
|
+
rendered_prompt = render_path_prompt(prompt, base_dir=Path.cwd())
|
|
631
|
+
|
|
632
|
+
async for event in session.agent_loop.act(rendered_prompt):
|
|
633
|
+
if isinstance(event, UserMessageEvent):
|
|
634
|
+
yield UserMessageChunk(
|
|
635
|
+
session_update="user_message_chunk",
|
|
636
|
+
content=TextContentBlock(type="text", text=""),
|
|
637
|
+
field_meta={
|
|
638
|
+
"messageId": event.message_id,
|
|
639
|
+
**(
|
|
640
|
+
{"previousMessageId": user_message_id}
|
|
641
|
+
if user_message_id
|
|
642
|
+
else {}
|
|
643
|
+
),
|
|
644
|
+
},
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
elif isinstance(event, AssistantEvent):
|
|
648
|
+
yield AgentMessageChunk(
|
|
649
|
+
session_update="agent_message_chunk",
|
|
650
|
+
content=TextContentBlock(type="text", text=event.content),
|
|
651
|
+
field_meta={"messageId": event.message_id},
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
elif isinstance(event, ReasoningEvent):
|
|
655
|
+
yield AgentThoughtChunk(
|
|
656
|
+
session_update="agent_thought_chunk",
|
|
657
|
+
content=TextContentBlock(type="text", text=event.content),
|
|
658
|
+
field_meta={"messageId": event.message_id},
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
elif isinstance(event, ToolCallEvent):
|
|
662
|
+
if issubclass(event.tool_class, BaseAcpTool):
|
|
663
|
+
event.tool_class.update_tool_state(
|
|
664
|
+
tool_manager=session.agent_loop.tool_manager,
|
|
665
|
+
client=self.client,
|
|
666
|
+
session_id=session.id,
|
|
667
|
+
tool_call_id=event.tool_call_id,
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
session_update = tool_call_session_update(event)
|
|
671
|
+
if session_update:
|
|
672
|
+
yield session_update
|
|
673
|
+
|
|
674
|
+
elif isinstance(event, ToolResultEvent):
|
|
675
|
+
session_update = tool_result_session_update(event)
|
|
676
|
+
if session_update:
|
|
677
|
+
yield session_update
|
|
678
|
+
|
|
679
|
+
elif isinstance(event, ToolStreamEvent):
|
|
680
|
+
yield ToolCallProgress(
|
|
681
|
+
session_update="tool_call_update",
|
|
682
|
+
tool_call_id=event.tool_call_id,
|
|
683
|
+
content=[
|
|
684
|
+
ContentToolCallContent(
|
|
685
|
+
type="content",
|
|
686
|
+
content=TextContentBlock(type="text", text=event.message),
|
|
687
|
+
)
|
|
688
|
+
],
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
elif isinstance(event, CompactStartEvent):
|
|
692
|
+
yield create_compact_start_session_update(event)
|
|
693
|
+
|
|
694
|
+
elif isinstance(event, CompactEndEvent):
|
|
695
|
+
yield create_compact_end_session_update(event)
|
|
696
|
+
|
|
697
|
+
@override
|
|
698
|
+
async def cancel(self, session_id: str, **kwargs: Any) -> None:
|
|
699
|
+
session = self._get_session(session_id)
|
|
700
|
+
if session.task and not session.task.done():
|
|
701
|
+
session.task.cancel()
|
|
702
|
+
session.task = None
|
|
703
|
+
|
|
704
|
+
@override
|
|
705
|
+
async def fork_session(
|
|
706
|
+
self,
|
|
707
|
+
cwd: str,
|
|
708
|
+
session_id: str,
|
|
709
|
+
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
|
|
710
|
+
**kwargs: Any,
|
|
711
|
+
) -> ForkSessionResponse:
|
|
712
|
+
raise NotImplementedError()
|
|
713
|
+
|
|
714
|
+
@override
|
|
715
|
+
async def resume_session(
|
|
716
|
+
self,
|
|
717
|
+
cwd: str,
|
|
718
|
+
session_id: str,
|
|
719
|
+
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
|
|
720
|
+
**kwargs: Any,
|
|
721
|
+
) -> ResumeSessionResponse:
|
|
722
|
+
raise NotImplementedError()
|
|
723
|
+
|
|
724
|
+
@override
|
|
725
|
+
async def ext_method(self, method: str, params: dict) -> dict:
|
|
726
|
+
raise NotImplementedError()
|
|
727
|
+
|
|
728
|
+
@override
|
|
729
|
+
async def ext_notification(self, method: str, params: dict) -> None:
|
|
730
|
+
raise NotImplementedError()
|
|
731
|
+
|
|
732
|
+
@override
|
|
733
|
+
def on_connect(self, conn: Client) -> None:
|
|
734
|
+
self.client = conn
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def run_acp_server() -> None:
|
|
738
|
+
try:
|
|
739
|
+
asyncio.run(run_agent(agent=VibeAcpAgentLoop(), use_unstable_protocol=True))
|
|
740
|
+
except KeyboardInterrupt:
|
|
741
|
+
# This is expected when the server is terminated
|
|
742
|
+
pass
|
|
743
|
+
except Exception as e:
|
|
744
|
+
# Log any unexpected errors
|
|
745
|
+
print(f"ACP Agent Server error: {e}", file=sys.stderr)
|
|
746
|
+
raise
|