v2.5.0 (#495)
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:
parent
9421fbc08e
commit
5103019b01
104 changed files with 7277 additions and 691 deletions
|
|
@ -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
121
vibe/acp/exceptions.py
Normal 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")
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue