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.
Files changed (170) hide show
  1. codemaster_cli-2.2.0.dist-info/METADATA +645 -0
  2. codemaster_cli-2.2.0.dist-info/RECORD +170 -0
  3. codemaster_cli-2.2.0.dist-info/WHEEL +4 -0
  4. codemaster_cli-2.2.0.dist-info/entry_points.txt +3 -0
  5. vibe/__init__.py +6 -0
  6. vibe/acp/__init__.py +0 -0
  7. vibe/acp/acp_agent_loop.py +746 -0
  8. vibe/acp/entrypoint.py +81 -0
  9. vibe/acp/tools/__init__.py +0 -0
  10. vibe/acp/tools/base.py +100 -0
  11. vibe/acp/tools/builtins/bash.py +134 -0
  12. vibe/acp/tools/builtins/read_file.py +54 -0
  13. vibe/acp/tools/builtins/search_replace.py +129 -0
  14. vibe/acp/tools/builtins/todo.py +65 -0
  15. vibe/acp/tools/builtins/write_file.py +98 -0
  16. vibe/acp/tools/session_update.py +118 -0
  17. vibe/acp/utils.py +213 -0
  18. vibe/cli/__init__.py +0 -0
  19. vibe/cli/autocompletion/__init__.py +0 -0
  20. vibe/cli/autocompletion/base.py +22 -0
  21. vibe/cli/autocompletion/path_completion.py +177 -0
  22. vibe/cli/autocompletion/slash_command.py +99 -0
  23. vibe/cli/cli.py +188 -0
  24. vibe/cli/clipboard.py +69 -0
  25. vibe/cli/commands.py +116 -0
  26. vibe/cli/entrypoint.py +163 -0
  27. vibe/cli/history_manager.py +91 -0
  28. vibe/cli/plan_offer/adapters/http_whoami_gateway.py +67 -0
  29. vibe/cli/plan_offer/decide_plan_offer.py +87 -0
  30. vibe/cli/plan_offer/ports/whoami_gateway.py +23 -0
  31. vibe/cli/terminal_setup.py +323 -0
  32. vibe/cli/textual_ui/__init__.py +0 -0
  33. vibe/cli/textual_ui/ansi_markdown.py +58 -0
  34. vibe/cli/textual_ui/app.py +1546 -0
  35. vibe/cli/textual_ui/app.tcss +1020 -0
  36. vibe/cli/textual_ui/external_editor.py +32 -0
  37. vibe/cli/textual_ui/handlers/__init__.py +5 -0
  38. vibe/cli/textual_ui/handlers/event_handler.py +147 -0
  39. vibe/cli/textual_ui/widgets/__init__.py +0 -0
  40. vibe/cli/textual_ui/widgets/approval_app.py +192 -0
  41. vibe/cli/textual_ui/widgets/banner/banner.py +85 -0
  42. vibe/cli/textual_ui/widgets/banner/petit_chat.py +195 -0
  43. vibe/cli/textual_ui/widgets/braille_renderer.py +58 -0
  44. vibe/cli/textual_ui/widgets/chat_input/__init__.py +7 -0
  45. vibe/cli/textual_ui/widgets/chat_input/body.py +214 -0
  46. vibe/cli/textual_ui/widgets/chat_input/completion_manager.py +58 -0
  47. vibe/cli/textual_ui/widgets/chat_input/completion_popup.py +43 -0
  48. vibe/cli/textual_ui/widgets/chat_input/container.py +195 -0
  49. vibe/cli/textual_ui/widgets/chat_input/text_area.py +365 -0
  50. vibe/cli/textual_ui/widgets/compact.py +41 -0
  51. vibe/cli/textual_ui/widgets/config_app.py +171 -0
  52. vibe/cli/textual_ui/widgets/context_progress.py +30 -0
  53. vibe/cli/textual_ui/widgets/load_more.py +43 -0
  54. vibe/cli/textual_ui/widgets/loading.py +201 -0
  55. vibe/cli/textual_ui/widgets/messages.py +277 -0
  56. vibe/cli/textual_ui/widgets/no_markup_static.py +11 -0
  57. vibe/cli/textual_ui/widgets/path_display.py +28 -0
  58. vibe/cli/textual_ui/widgets/proxy_setup_app.py +127 -0
  59. vibe/cli/textual_ui/widgets/question_app.py +496 -0
  60. vibe/cli/textual_ui/widgets/spinner.py +194 -0
  61. vibe/cli/textual_ui/widgets/status_message.py +76 -0
  62. vibe/cli/textual_ui/widgets/teleport_message.py +31 -0
  63. vibe/cli/textual_ui/widgets/tool_widgets.py +371 -0
  64. vibe/cli/textual_ui/widgets/tools.py +201 -0
  65. vibe/cli/textual_ui/windowing/__init__.py +29 -0
  66. vibe/cli/textual_ui/windowing/history.py +105 -0
  67. vibe/cli/textual_ui/windowing/history_windowing.py +71 -0
  68. vibe/cli/textual_ui/windowing/state.py +105 -0
  69. vibe/cli/update_notifier/__init__.py +47 -0
  70. vibe/cli/update_notifier/adapters/filesystem_update_cache_repository.py +59 -0
  71. vibe/cli/update_notifier/adapters/github_update_gateway.py +101 -0
  72. vibe/cli/update_notifier/adapters/pypi_update_gateway.py +107 -0
  73. vibe/cli/update_notifier/ports/update_cache_repository.py +16 -0
  74. vibe/cli/update_notifier/ports/update_gateway.py +53 -0
  75. vibe/cli/update_notifier/update.py +139 -0
  76. vibe/cli/update_notifier/whats_new.py +49 -0
  77. vibe/core/__init__.py +5 -0
  78. vibe/core/agent_loop.py +1075 -0
  79. vibe/core/agents/__init__.py +31 -0
  80. vibe/core/agents/manager.py +165 -0
  81. vibe/core/agents/models.py +122 -0
  82. vibe/core/auth/__init__.py +6 -0
  83. vibe/core/auth/crypto.py +137 -0
  84. vibe/core/auth/github.py +178 -0
  85. vibe/core/autocompletion/__init__.py +0 -0
  86. vibe/core/autocompletion/completers.py +257 -0
  87. vibe/core/autocompletion/file_indexer/__init__.py +10 -0
  88. vibe/core/autocompletion/file_indexer/ignore_rules.py +156 -0
  89. vibe/core/autocompletion/file_indexer/indexer.py +179 -0
  90. vibe/core/autocompletion/file_indexer/store.py +169 -0
  91. vibe/core/autocompletion/file_indexer/watcher.py +71 -0
  92. vibe/core/autocompletion/fuzzy.py +189 -0
  93. vibe/core/autocompletion/path_prompt.py +108 -0
  94. vibe/core/autocompletion/path_prompt_adapter.py +149 -0
  95. vibe/core/config.py +673 -0
  96. vibe/core/config_PATCH_INSTRUCTIONS.md +77 -0
  97. vibe/core/llm/__init__.py +0 -0
  98. vibe/core/llm/backend/anthropic.py +630 -0
  99. vibe/core/llm/backend/base.py +38 -0
  100. vibe/core/llm/backend/factory.py +7 -0
  101. vibe/core/llm/backend/generic.py +425 -0
  102. vibe/core/llm/backend/mistral.py +381 -0
  103. vibe/core/llm/backend/vertex.py +115 -0
  104. vibe/core/llm/exceptions.py +195 -0
  105. vibe/core/llm/format.py +184 -0
  106. vibe/core/llm/message_utils.py +24 -0
  107. vibe/core/llm/types.py +120 -0
  108. vibe/core/middleware.py +209 -0
  109. vibe/core/output_formatters.py +85 -0
  110. vibe/core/paths/__init__.py +0 -0
  111. vibe/core/paths/config_paths.py +68 -0
  112. vibe/core/paths/global_paths.py +40 -0
  113. vibe/core/programmatic.py +56 -0
  114. vibe/core/prompts/__init__.py +32 -0
  115. vibe/core/prompts/cli.md +111 -0
  116. vibe/core/prompts/compact.md +48 -0
  117. vibe/core/prompts/dangerous_directory.md +5 -0
  118. vibe/core/prompts/explore.md +50 -0
  119. vibe/core/prompts/project_context.md +8 -0
  120. vibe/core/prompts/tests.md +1 -0
  121. vibe/core/proxy_setup.py +65 -0
  122. vibe/core/session/session_loader.py +222 -0
  123. vibe/core/session/session_logger.py +318 -0
  124. vibe/core/session/session_migration.py +41 -0
  125. vibe/core/skills/__init__.py +7 -0
  126. vibe/core/skills/manager.py +132 -0
  127. vibe/core/skills/models.py +92 -0
  128. vibe/core/skills/parser.py +39 -0
  129. vibe/core/system_prompt.py +466 -0
  130. vibe/core/telemetry/__init__.py +0 -0
  131. vibe/core/telemetry/send.py +185 -0
  132. vibe/core/teleport/errors.py +9 -0
  133. vibe/core/teleport/git.py +196 -0
  134. vibe/core/teleport/nuage.py +180 -0
  135. vibe/core/teleport/teleport.py +208 -0
  136. vibe/core/teleport/types.py +54 -0
  137. vibe/core/tools/base.py +336 -0
  138. vibe/core/tools/builtins/ask_user_question.py +134 -0
  139. vibe/core/tools/builtins/bash.py +357 -0
  140. vibe/core/tools/builtins/grep.py +310 -0
  141. vibe/core/tools/builtins/prompts/__init__.py +0 -0
  142. vibe/core/tools/builtins/prompts/ask_user_question.md +84 -0
  143. vibe/core/tools/builtins/prompts/bash.md +73 -0
  144. vibe/core/tools/builtins/prompts/grep.md +4 -0
  145. vibe/core/tools/builtins/prompts/read_file.md +13 -0
  146. vibe/core/tools/builtins/prompts/search_replace.md +43 -0
  147. vibe/core/tools/builtins/prompts/task.md +24 -0
  148. vibe/core/tools/builtins/prompts/todo.md +199 -0
  149. vibe/core/tools/builtins/prompts/write_file.md +42 -0
  150. vibe/core/tools/builtins/read_file.py +222 -0
  151. vibe/core/tools/builtins/search_replace.py +456 -0
  152. vibe/core/tools/builtins/task.py +154 -0
  153. vibe/core/tools/builtins/todo.py +134 -0
  154. vibe/core/tools/builtins/write_file.py +160 -0
  155. vibe/core/tools/manager.py +341 -0
  156. vibe/core/tools/mcp.py +397 -0
  157. vibe/core/tools/ui.py +68 -0
  158. vibe/core/trusted_folders.py +86 -0
  159. vibe/core/types.py +405 -0
  160. vibe/core/utils.py +396 -0
  161. vibe/setup/onboarding/__init__.py +39 -0
  162. vibe/setup/onboarding/base.py +14 -0
  163. vibe/setup/onboarding/onboarding.tcss +134 -0
  164. vibe/setup/onboarding/screens/__init__.py +5 -0
  165. vibe/setup/onboarding/screens/api_key.py +200 -0
  166. vibe/setup/onboarding/screens/provider_selection.py +87 -0
  167. vibe/setup/onboarding/screens/welcome.py +136 -0
  168. vibe/setup/trusted_folders/trust_folder_dialog.py +180 -0
  169. vibe/setup/trusted_folders/trust_folder_dialog.tcss +83 -0
  170. 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