Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai>
Co-authored-by: Clement Sirieix <clem.sirieix@gmail.com>
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@mistral.ai>
Co-authored-by: Vincent Guilloux <vincent.guilloux@mistral.ai>
Co-authored-by: Michel Thomazo <michel.thomazo@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-03-16 17:51:47 +01:00 committed by GitHub
parent 9421fbc08e
commit 5103019b01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
104 changed files with 7277 additions and 691 deletions

View file

@ -15,7 +15,6 @@ from acp import (
LoadSessionResponse,
NewSessionResponse,
PromptResponse,
RequestError,
SetSessionModelResponse,
SetSessionModeResponse,
run_agent,
@ -55,6 +54,17 @@ from pydantic import BaseModel, ConfigDict
from vibe import VIBE_ROOT, __version__
from vibe.acp.acp_logger import acp_message_observer
from vibe.acp.exceptions import (
ConfigurationError,
ConversationLimitError,
InternalError,
InvalidRequestError,
NotImplementedMethodError,
RateLimitError,
SessionLoadError,
SessionNotFoundError,
UnauthenticatedError,
)
from vibe.acp.tools.base import BaseAcpTool
from vibe.acp.tools.session_update import (
tool_call_session_update,
@ -93,13 +103,14 @@ from vibe.core.proxy_setup import (
from vibe.core.session.session_loader import SessionLoader
from vibe.core.tools.base import BaseToolConfig, ToolPermission
from vibe.core.types import (
ApprovalCallback,
ApprovalResponse,
AssistantEvent,
AsyncApprovalCallback,
CompactEndEvent,
CompactStartEvent,
EntrypointMetadata,
LLMMessage,
RateLimitError as CoreRateLimitError,
ReasoningEvent,
Role,
ToolCallEvent,
@ -107,7 +118,11 @@ from vibe.core.types import (
ToolStreamEvent,
UserMessageEvent,
)
from vibe.core.utils import CancellationReason, get_user_cancellation_message
from vibe.core.utils import (
CancellationReason,
ConversationLimitException,
get_user_cancellation_message,
)
class AcpSessionLoop(BaseModel):
@ -201,7 +216,7 @@ class VibeAcpAgentLoop(AcpAgent):
async def authenticate(
self, method_id: str, **kwargs: Any
) -> AuthenticateResponse | None:
raise NotImplementedError("Not implemented yet")
raise NotImplementedMethodError("authenticate")
def _build_entrypoint_metadata(self) -> EntrypointMetadata:
return EntrypointMetadata(
@ -219,9 +234,9 @@ class VibeAcpAgentLoop(AcpAgent):
config.tool_paths.extend(self._get_acp_tool_overrides())
return config
except MissingAPIKeyError as e:
raise RequestError.auth_required({
"message": "You must be authenticated before creating a session"
}) from e
raise UnauthenticatedError.from_missing_api_key(e) from e
except Exception as e:
raise ConfigurationError(str(e)) from e
async def _create_acp_session(
self, session_id: str, agent_loop: AgentLoop
@ -295,7 +310,7 @@ class VibeAcpAgentLoop(AcpAgent):
for override in overrides
]
def _create_approval_callback(self, session_id: str) -> AsyncApprovalCallback:
def _create_approval_callback(self, session_id: str) -> ApprovalCallback:
session = self._get_session(session_id)
def _handle_permission_selection(
@ -347,7 +362,7 @@ class VibeAcpAgentLoop(AcpAgent):
def _get_session(self, session_id: str) -> AcpSessionLoop:
if session_id not in self.sessions:
raise RequestError.invalid_params({"session": "Not found"})
raise SessionNotFoundError(session_id)
return self.sessions[session_id]
async def _replay_tool_calls(self, session_id: str, msg: LLMMessage) -> None:
@ -395,7 +410,17 @@ class VibeAcpAgentLoop(AcpAgent):
hint="KEY value to set, KEY to unset, or empty for help"
)
),
)
),
AvailableCommand(
name="leanstall",
description="Install the Lean 4 agent (leanstral)",
input=AvailableCommandInput(root=UnstructuredCommandInput(hint="")),
),
AvailableCommand(
name="unleanstall",
description="Uninstall the Lean 4 agent",
input=AvailableCommandInput(root=UnstructuredCommandInput(hint="")),
),
]
update = update_available_commands(commands)
@ -429,6 +454,60 @@ class VibeAcpAgentLoop(AcpAgent):
)
return PromptResponse(stop_reason="end_turn")
async def _handle_leanstall_command(self, session_id: str) -> PromptResponse:
session = self._get_session(session_id)
current = list(session.agent_loop.base_config.installed_agents)
if "lean" in current:
message = "Lean agent is already installed."
else:
VibeConfig.save_updates({"installed_agents": [*current, "lean"]})
new_config = VibeConfig.load(
tool_paths=session.agent_loop.config.tool_paths,
disabled_tools=["ask_user_question", "exit_plan_mode"],
)
await session.agent_loop.reload_with_initial_messages(
base_config=new_config
)
message = (
"Lean agent installed. Start a new session to switch to Lean mode."
)
await self.client.session_update(
session_id=session_id,
update=AgentMessageChunk(
session_update="agent_message_chunk",
content=TextContentBlock(type="text", text=message),
),
)
return PromptResponse(stop_reason="end_turn")
async def _handle_unleanstall_command(self, session_id: str) -> PromptResponse:
session = self._get_session(session_id)
current = list(session.agent_loop.base_config.installed_agents)
if "lean" not in current:
message = "Lean agent is not installed."
else:
VibeConfig.save_updates({
"installed_agents": [a for a in current if a != "lean"]
})
new_config = VibeConfig.load(
tool_paths=session.agent_loop.config.tool_paths,
disabled_tools=["ask_user_question", "exit_plan_mode"],
)
await session.agent_loop.reload_with_initial_messages(
base_config=new_config
)
message = "Lean agent uninstalled."
await self.client.session_update(
session_id=session_id,
update=AgentMessageChunk(
session_update="agent_message_chunk",
content=TextContentBlock(type="text", text=message),
),
)
return PromptResponse(stop_reason="end_turn")
@override
async def load_session(
self,
@ -446,16 +525,12 @@ class VibeAcpAgentLoop(AcpAgent):
session_id, config.session_logging
)
if session_dir is None:
raise RequestError.invalid_params({
"session_id": f"Session not found: {session_id}"
})
raise SessionNotFoundError(session_id)
try:
loaded_messages, _ = SessionLoader.load_session(session_dir)
except ValueError as e:
raise RequestError.invalid_params({
"session_id": f"Failed to load session: {e}"
}) from e
except Exception as e:
raise SessionLoadError(session_id, str(e)) from e
agent_loop = AgentLoop(
config=config,
@ -605,7 +680,7 @@ class VibeAcpAgentLoop(AcpAgent):
session = self._get_session(session_id)
if session.task is not None:
raise RuntimeError(
raise InvalidRequestError(
"Concurrent prompts are not supported yet, wait for agent loop to finish"
)
@ -614,6 +689,12 @@ class VibeAcpAgentLoop(AcpAgent):
if text_prompt.strip().lower().startswith("/proxy-setup"):
return await self._handle_proxy_setup_command(session_id, text_prompt)
if text_prompt.strip().lower().startswith("/unleanstall"):
return await self._handle_unleanstall_command(session_id)
if text_prompt.strip().lower().startswith("/leanstall"):
return await self._handle_leanstall_command(session_id)
temp_user_message_id: str | None = kwargs.get("messageId")
async def agent_loop_task() -> None:
@ -629,16 +710,14 @@ class VibeAcpAgentLoop(AcpAgent):
except asyncio.CancelledError:
return PromptResponse(stop_reason="cancelled")
except Exception as e:
await self.client.session_update(
session_id=session_id,
update=AgentMessageChunk(
session_update="agent_message_chunk",
content=TextContentBlock(type="text", text=f"Error: {e!s}"),
),
)
except CoreRateLimitError as e:
raise RateLimitError.from_core(e) from e
return PromptResponse(stop_reason="refusal")
except ConversationLimitException as e:
raise ConversationLimitError(str(e)) from e
except Exception as e:
raise InternalError(str(e)) from e
finally:
session.task = None
@ -687,7 +766,9 @@ class VibeAcpAgentLoop(AcpAgent):
block_prompt = "\n".join(parts)
text_prompt = f"{text_prompt}{separator}{block_prompt}"
case _:
raise ValueError(f"Unsupported content block type: {block.type}")
raise InvalidRequestError(
f"We currently don't support {block.type} content blocks"
)
return text_prompt
async def _run_agent_loop(
@ -775,7 +856,7 @@ class VibeAcpAgentLoop(AcpAgent):
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
**kwargs: Any,
) -> ForkSessionResponse:
raise NotImplementedError()
raise NotImplementedMethodError("fork_session")
@override
async def resume_session(
@ -785,15 +866,15 @@ class VibeAcpAgentLoop(AcpAgent):
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
**kwargs: Any,
) -> ResumeSessionResponse:
raise NotImplementedError()
raise NotImplementedMethodError("resume_session")
@override
async def ext_method(self, method: str, params: dict) -> dict:
raise NotImplementedError()
raise NotImplementedMethodError("ext_method")
@override
async def ext_notification(self, method: str, params: dict) -> None:
raise NotImplementedError()
raise NotImplementedMethodError("ext_notification")
@override
def on_connect(self, conn: Client) -> None:

121
vibe/acp/exceptions.py Normal file
View file

@ -0,0 +1,121 @@
"""Structured ACP error classes for the Vibe agent.
Error codes follow JSON-RPC 2.0 (https://www.jsonrpc.org/specification#error_object)
and ACP error handling (https://agentclientprotocol.com/protocol/overview#error-handling):
-32700 Parse error (JSON-RPC standard)
-32600 Invalid request (JSON-RPC standard)
-32601 Method not found (JSON-RPC standard)
-32602 Invalid params (JSON-RPC standard)
-32603 Internal error (JSON-RPC standard)
-32000 to -32099 Server errors (JSON-RPC implementation-defined)
-31xxx Application errors (Vibe-specific, outside reserved range)
"""
from __future__ import annotations
from typing import Any
from acp import RequestError
from vibe.core.config import MissingAPIKeyError
from vibe.core.types import RateLimitError as CoreRateLimitError
# JSON-RPC 2.0 standard codes
UNAUTHENTICATED = -32000
METHOD_NOT_FOUND = -32601
INVALID_PARAMS = -32602
INTERNAL_ERROR = -32603
# Vibe application codes (outside JSON-RPC reserved range)
RATE_LIMITED = -31001
CONFIGURATION_ERROR = -31002
CONVERSATION_LIMIT = -31003
class VibeRequestError(RequestError):
code: int
def __init__(self, message: str, data: dict[str, Any] | None = None) -> None:
super().__init__(self.code, message, data)
class UnauthenticatedError(VibeRequestError):
code = UNAUTHENTICATED
def __init__(self, detail: str) -> None:
super().__init__(message=detail)
@classmethod
def from_missing_api_key(cls, exc: MissingAPIKeyError) -> UnauthenticatedError:
return cls(f"Missing API key for {exc.provider_name} provider.")
class NotImplementedMethodError(VibeRequestError):
code = METHOD_NOT_FOUND
def __init__(self, method: str) -> None:
super().__init__(
message=f"Method not implemented: {method}", data={"method": method}
)
class InvalidRequestError(VibeRequestError):
code = INVALID_PARAMS
def __init__(self, detail: str) -> None:
super().__init__(message=detail)
class SessionNotFoundError(VibeRequestError):
code = INVALID_PARAMS
def __init__(self, session_id: str) -> None:
super().__init__(
message=f"Session not found: {session_id}", data={"session_id": session_id}
)
class SessionLoadError(VibeRequestError):
code = INVALID_PARAMS
def __init__(self, session_id: str, detail: str) -> None:
super().__init__(
message=f"Failed to load session {session_id}: {detail}",
data={"session_id": session_id},
)
class RateLimitError(VibeRequestError):
code = RATE_LIMITED
def __init__(self, provider: str, model: str) -> None:
super().__init__(
message=f"Rate limit exceeded for {provider} (model: {model}).",
data={"provider": provider, "model": model},
)
@classmethod
def from_core(cls, exc: CoreRateLimitError) -> RateLimitError:
return cls(exc.provider, exc.model)
class ConfigurationError(VibeRequestError):
code = CONFIGURATION_ERROR
def __init__(self, detail: str) -> None:
super().__init__(message=detail)
class ConversationLimitError(VibeRequestError):
code = CONVERSATION_LIMIT
def __init__(self, detail: str) -> None:
super().__init__(message=detail)
class InternalError(VibeRequestError):
code = INTERNAL_ERROR
def __init__(self, detail: str) -> None:
super().__init__(message=detail or "Internal error")

View file

@ -4,18 +4,19 @@ from pathlib import Path
from vibe import VIBE_ROOT
from vibe.acp.tools.base import AcpToolState, BaseAcpTool
from vibe.core.tools.base import BaseToolState, ToolError
from vibe.core.tools.base import ToolError
from vibe.core.tools.builtins.read_file import (
ReadFile as CoreReadFileTool,
ReadFileArgs,
ReadFileResult,
ReadFileState,
_ReadResult,
)
ReadFileResult = ReadFileResult
class AcpReadFileState(BaseToolState, AcpToolState):
class AcpReadFileState(ReadFileState, AcpToolState):
pass

View file

@ -57,11 +57,14 @@ def tool_call_session_update(event: ToolCallEvent) -> SessionUpdate | None:
def tool_result_session_update(event: ToolResultEvent) -> SessionUpdate | None:
if is_user_cancellation_event(event):
tool_status = "failed"
raw_output = (
TaggedText.from_string(event.skip_reason).message
if event.skip_reason
else None
)
if event.skip_reason:
raw_output = TaggedText.from_string(event.skip_reason).message
elif event.error:
raw_output = TaggedText.from_string(event.error).message
elif event.result:
raw_output = event.result.model_dump_json()
else:
raw_output = None
elif event.result:
tool_status = "completed"
raw_output = event.result.model_dump_json()