v2.7.1 (#551)
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Jean-Malo Delignon <56539593+jean-malo@users.noreply.github.com> Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai> Co-authored-by: Quentin <torroba.q@gmail.com> Co-authored-by: angelapopopo <angele.lenglemetz@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
6a50d1d521
commit
54b9a17457
30 changed files with 1000 additions and 200 deletions
|
|
@ -3,4 +3,4 @@ from __future__ import annotations
|
|||
from pathlib import Path
|
||||
|
||||
VIBE_ROOT = Path(__file__).parent
|
||||
__version__ = "2.7.0"
|
||||
__version__ = "2.7.1"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import os
|
|||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any, cast, override
|
||||
from uuid import uuid4
|
||||
|
||||
from acp import (
|
||||
PROTOCOL_VERSION,
|
||||
|
|
@ -26,11 +27,14 @@ from acp.schema import (
|
|||
AgentThoughtChunk,
|
||||
AllowedOutcome,
|
||||
AuthenticateResponse,
|
||||
AuthMethod,
|
||||
AuthMethodAgent,
|
||||
AvailableCommand,
|
||||
AvailableCommandInput,
|
||||
ClientCapabilities,
|
||||
CloseSessionResponse,
|
||||
ContentToolCallContent,
|
||||
Cost,
|
||||
EnvVarAuthMethod,
|
||||
ForkSessionResponse,
|
||||
HttpMcpServer,
|
||||
Implementation,
|
||||
|
|
@ -43,12 +47,14 @@ from acp.schema import (
|
|||
SessionListCapabilities,
|
||||
SetSessionConfigOptionResponse,
|
||||
SseMcpServer,
|
||||
TerminalAuthMethod,
|
||||
TextContentBlock,
|
||||
TextResourceContents,
|
||||
ToolCallProgress,
|
||||
ToolCallUpdate,
|
||||
UnstructuredCommandInput,
|
||||
UserMessageChunk,
|
||||
Usage,
|
||||
UsageUpdate,
|
||||
)
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
|
@ -117,7 +123,6 @@ from vibe.core.types import (
|
|||
ToolCallEvent,
|
||||
ToolResultEvent,
|
||||
ToolStreamEvent,
|
||||
UserMessageEvent,
|
||||
)
|
||||
from vibe.core.utils import (
|
||||
CancellationReason,
|
||||
|
|
@ -126,6 +131,12 @@ from vibe.core.utils import (
|
|||
)
|
||||
|
||||
|
||||
def _resolved_user_message_id(client_message_id: str | None) -> str:
|
||||
if client_message_id is not None:
|
||||
return client_message_id
|
||||
return str(uuid4())
|
||||
|
||||
|
||||
class AcpSessionLoop(BaseModel):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
id: str
|
||||
|
|
@ -174,9 +185,10 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
and self.client_capabilities.field_meta.get("terminal-auth") is True
|
||||
)
|
||||
|
||||
auth_methods = (
|
||||
auth_methods: list[EnvVarAuthMethod | TerminalAuthMethod | AuthMethodAgent] = (
|
||||
[
|
||||
AuthMethod(
|
||||
TerminalAuthMethod(
|
||||
type="terminal",
|
||||
id="vibe-setup",
|
||||
name="Register your API Key",
|
||||
description="Register your API Key inside Mistral Vibe",
|
||||
|
|
@ -380,6 +392,39 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
raise SessionNotFoundError(session_id)
|
||||
return self.sessions[session_id]
|
||||
|
||||
def _build_usage(self, session: AcpSessionLoop) -> Usage:
|
||||
stats = session.agent_loop.stats
|
||||
return Usage(
|
||||
input_tokens=stats.session_prompt_tokens,
|
||||
output_tokens=stats.session_completion_tokens,
|
||||
total_tokens=stats.session_total_llm_tokens,
|
||||
)
|
||||
|
||||
def _build_usage_update(self, session: AcpSessionLoop) -> UsageUpdate:
|
||||
stats = session.agent_loop.stats
|
||||
active_model = session.agent_loop.config.get_active_model()
|
||||
cost = (
|
||||
Cost(amount=stats.session_cost, currency="USD")
|
||||
if stats.input_price_per_million > 0 or stats.output_price_per_million > 0
|
||||
else None
|
||||
)
|
||||
return UsageUpdate(
|
||||
session_update="usage_update",
|
||||
used=stats.context_tokens,
|
||||
size=active_model.auto_compact_threshold,
|
||||
cost=cost,
|
||||
)
|
||||
|
||||
def _send_usage_update(self, session: AcpSessionLoop) -> None:
|
||||
async def _send() -> None:
|
||||
try:
|
||||
update = self._build_usage_update(session)
|
||||
await self.client.session_update(session_id=session.id, update=update)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
asyncio.create_task(_send())
|
||||
|
||||
async def _replay_tool_calls(self, session_id: str, msg: LLMMessage) -> None:
|
||||
if not msg.tool_calls:
|
||||
return
|
||||
|
|
@ -442,7 +487,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
await self.client.session_update(session_id=session_id, update=update)
|
||||
|
||||
async def _handle_proxy_setup_command(
|
||||
self, session_id: str, text_prompt: str
|
||||
self, session_id: str, text_prompt: str, message_id: str
|
||||
) -> PromptResponse:
|
||||
args = text_prompt.strip()[len("/proxy-setup") :].strip()
|
||||
|
||||
|
|
@ -465,11 +510,14 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
update=AgentMessageChunk(
|
||||
session_update="agent_message_chunk",
|
||||
content=TextContentBlock(type="text", text=message),
|
||||
message_id=str(uuid4()),
|
||||
),
|
||||
)
|
||||
return PromptResponse(stop_reason="end_turn")
|
||||
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)
|
||||
|
||||
async def _handle_leanstall_command(self, session_id: str) -> PromptResponse:
|
||||
async def _handle_leanstall_command(
|
||||
self, session_id: str, message_id: str
|
||||
) -> PromptResponse:
|
||||
session = self._get_session(session_id)
|
||||
current = list(session.agent_loop.base_config.installed_agents)
|
||||
if "lean" in current:
|
||||
|
|
@ -492,11 +540,14 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
update=AgentMessageChunk(
|
||||
session_update="agent_message_chunk",
|
||||
content=TextContentBlock(type="text", text=message),
|
||||
message_id=str(uuid4()),
|
||||
),
|
||||
)
|
||||
return PromptResponse(stop_reason="end_turn")
|
||||
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)
|
||||
|
||||
async def _handle_unleanstall_command(self, session_id: str) -> PromptResponse:
|
||||
async def _handle_unleanstall_command(
|
||||
self, session_id: str, message_id: str
|
||||
) -> PromptResponse:
|
||||
session = self._get_session(session_id)
|
||||
current = list(session.agent_loop.base_config.installed_agents)
|
||||
if "lean" not in current:
|
||||
|
|
@ -519,9 +570,10 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
update=AgentMessageChunk(
|
||||
session_update="agent_message_chunk",
|
||||
content=TextContentBlock(type="text", text=message),
|
||||
message_id=str(uuid4()),
|
||||
),
|
||||
)
|
||||
return PromptResponse(stop_reason="end_turn")
|
||||
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)
|
||||
|
||||
@override
|
||||
async def load_session(
|
||||
|
|
@ -564,6 +616,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
session = await self._create_acp_session(session_id, agent_loop)
|
||||
|
||||
await self._replay_conversation_history(session_id, non_system_messages)
|
||||
self._send_usage_update(session)
|
||||
|
||||
modes_state, modes_config = make_mode_response(
|
||||
list(agent_loop.agent_manager.available_agents.values()),
|
||||
|
|
@ -635,14 +688,14 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
|
||||
@override
|
||||
async def set_config_option(
|
||||
self, config_id: str, session_id: str, value: str, **kwargs: Any
|
||||
self, config_id: str, session_id: str, value: str | bool, **kwargs: Any
|
||||
) -> SetSessionConfigOptionResponse | None:
|
||||
session = self._get_session(session_id)
|
||||
|
||||
match config_id:
|
||||
case "mode":
|
||||
case "mode" if isinstance(value, str):
|
||||
success = await self._apply_mode_change(session, value)
|
||||
case "model":
|
||||
case "model" if isinstance(value, str):
|
||||
success = await self._apply_model_change(session, value)
|
||||
case _:
|
||||
success = False
|
||||
|
|
@ -690,7 +743,11 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
|
||||
@override
|
||||
async def prompt(
|
||||
self, prompt: list[ContentBlock], session_id: str, **kwargs: Any
|
||||
self,
|
||||
prompt: list[ContentBlock],
|
||||
session_id: str,
|
||||
message_id: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> PromptResponse:
|
||||
session = self._get_session(session_id)
|
||||
|
||||
|
|
@ -700,21 +757,24 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
)
|
||||
|
||||
text_prompt = self._build_text_prompt(prompt)
|
||||
resolved_message_id = _resolved_user_message_id(message_id)
|
||||
|
||||
if text_prompt.strip().lower().startswith("/proxy-setup"):
|
||||
return await self._handle_proxy_setup_command(session_id, text_prompt)
|
||||
return await self._handle_proxy_setup_command(
|
||||
session_id, text_prompt, resolved_message_id
|
||||
)
|
||||
|
||||
if text_prompt.strip().lower().startswith("/unleanstall"):
|
||||
return await self._handle_unleanstall_command(session_id)
|
||||
return await self._handle_unleanstall_command(
|
||||
session_id, resolved_message_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")
|
||||
return await self._handle_leanstall_command(session_id, resolved_message_id)
|
||||
|
||||
async def agent_loop_task() -> None:
|
||||
async for update in self._run_agent_loop(
|
||||
session, text_prompt, temp_user_message_id
|
||||
session, text_prompt, resolved_message_id
|
||||
):
|
||||
await self.client.session_update(session_id=session.id, update=update)
|
||||
|
||||
|
|
@ -723,7 +783,12 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
await session.task
|
||||
|
||||
except asyncio.CancelledError:
|
||||
return PromptResponse(stop_reason="cancelled")
|
||||
self._send_usage_update(session)
|
||||
return PromptResponse(
|
||||
stop_reason="cancelled",
|
||||
usage=self._build_usage(session),
|
||||
user_message_id=resolved_message_id,
|
||||
)
|
||||
|
||||
except CoreRateLimitError as e:
|
||||
raise RateLimitError.from_core(e) from e
|
||||
|
|
@ -737,7 +802,12 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
finally:
|
||||
session.task = None
|
||||
|
||||
return PromptResponse(stop_reason="end_turn")
|
||||
self._send_usage_update(session)
|
||||
return PromptResponse(
|
||||
stop_reason="end_turn",
|
||||
usage=self._build_usage(session),
|
||||
user_message_id=resolved_message_id,
|
||||
)
|
||||
|
||||
def _build_text_prompt(self, acp_prompt: list[ContentBlock]) -> str:
|
||||
text_prompt = ""
|
||||
|
|
@ -787,37 +857,25 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
return text_prompt
|
||||
|
||||
async def _run_agent_loop(
|
||||
self, session: AcpSessionLoop, prompt: str, user_message_id: str | None = None
|
||||
self, session: AcpSessionLoop, prompt: str, client_message_id: str | None = None
|
||||
) -> AsyncGenerator[SessionUpdate]:
|
||||
rendered_prompt = render_path_prompt(prompt, base_dir=Path.cwd())
|
||||
|
||||
async for event in session.agent_loop.act(rendered_prompt):
|
||||
if isinstance(event, UserMessageEvent):
|
||||
yield UserMessageChunk(
|
||||
session_update="user_message_chunk",
|
||||
content=TextContentBlock(type="text", text=""),
|
||||
field_meta={
|
||||
"messageId": event.message_id,
|
||||
**(
|
||||
{"previousMessageId": user_message_id}
|
||||
if user_message_id
|
||||
else {}
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
elif isinstance(event, AssistantEvent):
|
||||
async for event in session.agent_loop.act(
|
||||
rendered_prompt, client_message_id=client_message_id
|
||||
):
|
||||
if isinstance(event, AssistantEvent):
|
||||
yield AgentMessageChunk(
|
||||
session_update="agent_message_chunk",
|
||||
content=TextContentBlock(type="text", text=event.content),
|
||||
field_meta={"messageId": event.message_id},
|
||||
message_id=event.message_id,
|
||||
)
|
||||
|
||||
elif isinstance(event, ReasoningEvent):
|
||||
yield AgentThoughtChunk(
|
||||
session_update="agent_thought_chunk",
|
||||
content=TextContentBlock(type="text", text=event.content),
|
||||
field_meta={"messageId": event.message_id},
|
||||
message_id=event.message_id,
|
||||
)
|
||||
|
||||
elif isinstance(event, ToolCallEvent):
|
||||
|
|
@ -859,6 +917,12 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
elif isinstance(event, AgentProfileChangedEvent):
|
||||
pass
|
||||
|
||||
@override
|
||||
async def close_session(
|
||||
self, session_id: str, **kwargs: Any
|
||||
) -> CloseSessionResponse | None:
|
||||
raise NotImplementedMethodError("close_session")
|
||||
|
||||
@override
|
||||
async def cancel(self, session_id: str, **kwargs: Any) -> None:
|
||||
session = self._get_session(session_id)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from acp.schema import (
|
|||
ContentToolCallContent,
|
||||
ModelInfo,
|
||||
PermissionOption,
|
||||
SessionConfigOption,
|
||||
SessionConfigOptionSelect,
|
||||
SessionConfigSelectOption,
|
||||
SessionMode,
|
||||
|
|
@ -103,7 +102,7 @@ def is_valid_acp_mode(profiles: list[AgentProfile], mode_name: str) -> bool:
|
|||
|
||||
def make_mode_response(
|
||||
profiles: list[AgentProfile], current_mode_id: str
|
||||
) -> tuple[SessionModeState, SessionConfigOption]:
|
||||
) -> tuple[SessionModeState, SessionConfigOptionSelect]:
|
||||
session_modes: list[SessionMode] = []
|
||||
config_options: list[SessionConfigSelectOption] = []
|
||||
|
||||
|
|
@ -128,22 +127,20 @@ def make_mode_response(
|
|||
state = SessionModeState(
|
||||
current_mode_id=current_mode_id, available_modes=session_modes
|
||||
)
|
||||
config = SessionConfigOption(
|
||||
root=SessionConfigOptionSelect(
|
||||
id="mode",
|
||||
name="Session Mode",
|
||||
current_value=current_mode_id,
|
||||
category="mode",
|
||||
type="select",
|
||||
options=config_options,
|
||||
)
|
||||
config = SessionConfigOptionSelect(
|
||||
id="mode",
|
||||
name="Session Mode",
|
||||
current_value=current_mode_id,
|
||||
category="mode",
|
||||
type="select",
|
||||
options=config_options,
|
||||
)
|
||||
return state, config
|
||||
|
||||
|
||||
def make_model_response(
|
||||
models: list[ModelConfig], current_model_id: str
|
||||
) -> tuple[SessionModelState, SessionConfigOption]:
|
||||
) -> tuple[SessionModelState, SessionConfigOptionSelect]:
|
||||
model_infos: list[ModelInfo] = []
|
||||
config_options: list[SessionConfigSelectOption] = []
|
||||
|
||||
|
|
@ -158,15 +155,13 @@ def make_model_response(
|
|||
state = SessionModelState(
|
||||
current_model_id=current_model_id, available_models=model_infos
|
||||
)
|
||||
config_option = SessionConfigOption(
|
||||
root=SessionConfigOptionSelect(
|
||||
id="model",
|
||||
name="Model",
|
||||
current_value=current_model_id,
|
||||
category="model",
|
||||
type="select",
|
||||
options=config_options,
|
||||
)
|
||||
config_option = SessionConfigOptionSelect(
|
||||
id="model",
|
||||
name="Model",
|
||||
current_value=current_model_id,
|
||||
category="model",
|
||||
type="select",
|
||||
options=config_options,
|
||||
)
|
||||
return state, config_option
|
||||
|
||||
|
|
@ -257,7 +252,7 @@ def create_user_message_replay(msg: LLMMessage) -> UserMessageChunk:
|
|||
return UserMessageChunk(
|
||||
session_update="user_message_chunk",
|
||||
content=TextContentBlock(type="text", text=content),
|
||||
field_meta={"messageId": msg.message_id} if msg.message_id else {},
|
||||
message_id=msg.message_id,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -269,7 +264,7 @@ def create_assistant_message_replay(msg: LLMMessage) -> AgentMessageChunk | None
|
|||
return AgentMessageChunk(
|
||||
session_update="agent_message_chunk",
|
||||
content=TextContentBlock(type="text", text=content),
|
||||
field_meta={"messageId": msg.message_id} if msg.message_id else {},
|
||||
message_id=msg.message_id,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -280,7 +275,7 @@ def create_reasoning_replay(msg: LLMMessage) -> AgentThoughtChunk | None:
|
|||
return AgentThoughtChunk(
|
||||
session_update="agent_thought_chunk",
|
||||
content=TextContentBlock(type="text", text=msg.reasoning_content),
|
||||
field_meta={"messageId": msg.message_id} if msg.message_id else {},
|
||||
message_id=msg.reasoning_message_id,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -128,12 +128,10 @@ def _get_selected_texts(app: App) -> list[str]:
|
|||
selected_texts = []
|
||||
|
||||
for widget in app.query("*"):
|
||||
if not hasattr(widget, "text_selection") or not widget.text_selection:
|
||||
continue
|
||||
|
||||
selection = widget.text_selection
|
||||
|
||||
try:
|
||||
if not hasattr(widget, "text_selection") or not widget.text_selection:
|
||||
continue
|
||||
selection = widget.text_selection
|
||||
result = widget.get_selection(selection)
|
||||
except Exception:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.scroll_sensitivity_y = 1.0
|
||||
self.agent_loop = agent_loop
|
||||
self._voice_manager: VoiceManagerPort = (
|
||||
voice_manager or self._make_default_voice_manager()
|
||||
|
|
|
|||
|
|
@ -344,7 +344,9 @@ class AgentLoop:
|
|||
self.agent_profile,
|
||||
)
|
||||
|
||||
async def act(self, msg: str) -> AsyncGenerator[BaseEvent]:
|
||||
async def act(
|
||||
self, msg: str, client_message_id: str | None = None
|
||||
) -> AsyncGenerator[BaseEvent]:
|
||||
self._clean_message_history()
|
||||
self.rewind_manager.create_checkpoint()
|
||||
try:
|
||||
|
|
@ -352,7 +354,9 @@ class AgentLoop:
|
|||
except ValueError:
|
||||
model_name = None
|
||||
async with agent_span(model=model_name, session_id=self.session_id):
|
||||
async for event in self._conversation_loop(msg):
|
||||
async for event in self._conversation_loop(
|
||||
msg, client_message_id=client_message_id
|
||||
):
|
||||
yield event
|
||||
|
||||
@property
|
||||
|
|
@ -511,8 +515,12 @@ class AgentLoop:
|
|||
}
|
||||
return headers
|
||||
|
||||
async def _conversation_loop(self, user_msg: str) -> AsyncGenerator[BaseEvent]:
|
||||
user_message = LLMMessage(role=Role.user, content=user_msg)
|
||||
async def _conversation_loop(
|
||||
self, user_msg: str, client_message_id: str | None = None
|
||||
) -> AsyncGenerator[BaseEvent]:
|
||||
user_message = LLMMessage(
|
||||
role=Role.user, content=user_msg, message_id=client_message_id
|
||||
)
|
||||
self.messages.append(user_message)
|
||||
self.stats.steps += 1
|
||||
self._current_user_message_id = user_message.message_id
|
||||
|
|
@ -604,11 +612,14 @@ class AgentLoop:
|
|||
self,
|
||||
) -> AsyncGenerator[AssistantEvent | ReasoningEvent | ToolCallEvent]:
|
||||
message_id: str | None = None
|
||||
reasoning_message_id: str | None = None
|
||||
emitted_tool_call_ids = set[str]()
|
||||
|
||||
async for chunk in self._chat_streaming():
|
||||
if message_id is None:
|
||||
message_id = chunk.message.message_id
|
||||
if reasoning_message_id is None:
|
||||
reasoning_message_id = chunk.message.reasoning_message_id
|
||||
|
||||
for event in self._build_tool_call_events(
|
||||
chunk.message.tool_calls, emitted_tool_call_ids
|
||||
|
|
@ -618,7 +629,8 @@ class AgentLoop:
|
|||
|
||||
if chunk.message.reasoning_content:
|
||||
yield ReasoningEvent(
|
||||
content=chunk.message.reasoning_content, message_id=message_id
|
||||
content=chunk.message.reasoning_content,
|
||||
message_id=reasoning_message_id,
|
||||
)
|
||||
|
||||
if chunk.message.content:
|
||||
|
|
@ -1066,7 +1078,6 @@ class AgentLoop:
|
|||
if len(self.messages) < ACCEPTABLE_HISTORY_SIZE:
|
||||
return
|
||||
self._fill_missing_tool_responses()
|
||||
self._ensure_assistant_after_tools()
|
||||
|
||||
def _fill_missing_tool_responses(self) -> None:
|
||||
i = 1
|
||||
|
|
@ -1114,16 +1125,6 @@ class AgentLoop:
|
|||
|
||||
i += 1
|
||||
|
||||
def _ensure_assistant_after_tools(self) -> None:
|
||||
MIN_MESSAGE_SIZE = 2
|
||||
if len(self.messages) < MIN_MESSAGE_SIZE:
|
||||
return
|
||||
|
||||
last_msg = self.messages[-1]
|
||||
if last_msg.role is Role.tool:
|
||||
empty_assistant_msg = LLMMessage(role=Role.assistant, content="Understood.")
|
||||
self.messages.append(empty_assistant_msg)
|
||||
|
||||
def _reset_session(self) -> None:
|
||||
self.session_id = str(uuid4())
|
||||
self.session_logger.reset_session(self.session_id)
|
||||
|
|
|
|||
|
|
@ -97,7 +97,11 @@ class OpenAIAdapter(APIAdapter):
|
|||
field_name = provider.reasoning_field_name
|
||||
converted_messages = [
|
||||
self._reasoning_to_api(
|
||||
msg.model_dump(exclude_none=True, exclude={"message_id"}), field_name
|
||||
msg.model_dump(
|
||||
exclude_none=True,
|
||||
exclude={"message_id", "reasoning_message_id", "injected"},
|
||||
),
|
||||
field_name,
|
||||
)
|
||||
for msg in merged_messages
|
||||
]
|
||||
|
|
|
|||
|
|
@ -61,9 +61,13 @@ class BackendError(RuntimeError):
|
|||
return "Rate limit exceeded. Please wait a moment before trying again."
|
||||
|
||||
rid = self.headers.get("x-request-id") or self.headers.get("request-id")
|
||||
status_label = (
|
||||
f"{self.status} {HTTPStatus(self.status).phrase}" if self.status else "N/A"
|
||||
)
|
||||
if self.status:
|
||||
try:
|
||||
status_label = f"{self.status} {HTTPStatus(self.status).phrase}"
|
||||
except ValueError:
|
||||
status_label = str(self.status)
|
||||
else:
|
||||
status_label = "N/A"
|
||||
parts = [
|
||||
f"LLM backend error [{self.provider}]",
|
||||
f" status: {status_label}",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
You are Mistral Vibe, a CLI coding agent built by Mistral AI. You interact with a local codebase through tools.
|
||||
CRITICAL: Users complain you are too verbose. Your responses must be minimal. Most tasks need <100 words. Code speaks for itself.
|
||||
CRITICAL: Users complain you are too verbose. Your responses must be minimal. Most tasks need <150 words. Code speaks for itself.
|
||||
|
||||
Phase 1 — Orient
|
||||
Phase 1 - Orient
|
||||
Before ANY action:
|
||||
Restate the goal in one line.
|
||||
Determine the task type:
|
||||
|
|
@ -13,25 +13,25 @@ Explore. Use available tools to understand affected code, dependencies, and conv
|
|||
Identify constraints: language, framework, test setup, and any user restrictions on scope.
|
||||
When given multiple file paths or a complex task: Do not start reading files immediately. First, summarize your understanding of the task and propose a short plan. Wait for the user to confirm before exploring any files. This prevents wasted effort on the wrong path.
|
||||
|
||||
Phase 2 — Plan (Change tasks only)
|
||||
Phase 2 - Plan (Change tasks only)
|
||||
State your plan before writing code:
|
||||
List files to change and the specific change per file.
|
||||
Multi-file changes: numbered checklist. Single-file fix: one-line plan.
|
||||
No time estimates. Concrete actions only.
|
||||
|
||||
Phase 3 — Execute & Verify (Change tasks only)
|
||||
Phase 3 - Execute & Verify (Change tasks only)
|
||||
Apply changes, then confirm they work:
|
||||
Edit one logical unit at a time.
|
||||
After each unit, verify: run tests, or read back the file to confirm the edit landed.
|
||||
Never claim completion without verification — a passing test, correct read-back, or successful build.
|
||||
|
||||
Hard Rules
|
||||
Hard Rules:
|
||||
|
||||
Never Commit
|
||||
Do not run `git commit`, `git push`, or `git add` unless the user explicitly asks you to. Saving files is sufficient — the user will review changes and commit themselves.
|
||||
|
||||
Respect User Constraints
|
||||
"No writes", "just analyze", "plan only", "don't touch X" — these are hard constraints. Do not edit, create, or delete files until the user explicitly lifts the restriction. Violation of explicit user instructions is the worst failure mode.
|
||||
"No writes", "just analyze", "plan only", "don't touch X" - these are hard constraints. Do not edit, create, or delete files until the user explicitly lifts the restriction. Violation of explicit user instructions is the worst failure mode.
|
||||
|
||||
Don't Remove What Wasn't Asked
|
||||
If user asks to fix X, do not rewrite, delete, or restructure Y. When in doubt, change less.
|
||||
|
|
@ -58,21 +58,17 @@ No unsolicited tutorials. Do not explain concepts the user clearly knows.
|
|||
|
||||
Structure First
|
||||
Lead every response with the most useful structured element — code, diagram, table, or tree. Prose comes after, not before.
|
||||
For change tasks:
|
||||
file_path:line_number
|
||||
langcode
|
||||
For change tasks, cite as: `file_path:line_number` followed by a fenced code block.
|
||||
|
||||
Prefer Brevity
|
||||
State only what's necessary to complete the task. Code + file reference > explanation.
|
||||
If your response exceeds 300 words, remove explanations the user didn't request.
|
||||
|
||||
For investigate tasks:
|
||||
Start with a diagram, code reference, tree, or table — whichever conveys the answer fastest.
|
||||
request → auth.verify() → permissions.check() → handler
|
||||
See middleware/auth.py:45. Then 1-2 sentences of context if needed.
|
||||
Start with a diagram, code reference, tree, or table - whichever conveys the answer fastest.
|
||||
Then 1-2 sentences of context if needed.
|
||||
BAD: "The authentication flow works by first checking the token…"
|
||||
GOOD: request → auth.verify() → permissions.check() → handler — see middleware/auth.py:45.
|
||||
Visual Formats
|
||||
GOOD: request → auth.verify() → permissions.check() → handler — see middleware/auth.py:45
|
||||
|
||||
Before responding with structural data, choose the right format:
|
||||
BAD: Bullet lists for hierarchy/tree
|
||||
|
|
@ -84,15 +80,13 @@ GOOD: → A → B → C diagrams
|
|||
|
||||
Interaction Design
|
||||
After completing a task, evaluate: does the user face a decision or tradeoff? If yes, end with ONE specific question or 2-3 options:
|
||||
|
||||
Good: "Apply this fix to the other 3 endpoints?"
|
||||
Good: "Two approaches: (a) migration, (b) recreate table. Which?"
|
||||
Bad: "Does this look good?", "Anything else?", "Let me know"
|
||||
|
||||
GOOD: "Apply this fix to the other 3 endpoints?"
|
||||
GOOD: "Two approaches: (a) migration, (b) recreate table. Which?"
|
||||
BAD: "Does this look good?", "Anything else?", "Let me know"
|
||||
If unambiguous and complete, end with the result.
|
||||
|
||||
Length
|
||||
Default to minimal responses. One-line fix → one-line response. Most tasks need <200 words.
|
||||
Default to minimal responses. One-line fix → one-line response. Most tasks need <150 words.
|
||||
Elaborate only when: (1) user asks for explanation, (2) task involves architectural decisions, (3) multiple valid approaches exist.
|
||||
|
||||
Code Modifications (Change tasks)
|
||||
|
|
@ -107,12 +101,9 @@ When removing code, delete completely. No _unused renames, // removed comments,
|
|||
Security
|
||||
Fix injection, XSS, SQLi vulnerabilities immediately if spotted.
|
||||
|
||||
Code References
|
||||
Cite as file_path:line_number.
|
||||
|
||||
Professional Conduct
|
||||
Prioritize technical accuracy over validating beliefs. Disagree when necessary.
|
||||
When uncertain, investigate before confirming.
|
||||
Your output must contain zero emoji. This includes smiley faces, icons, flags, symbols like ✅❌💡, and all other Unicode emoji.
|
||||
No over-the-top validation.
|
||||
Stay focused on solving the problem regardless of user tone. Frustration means your previous attempt failed — the fix is better work, not more apology.
|
||||
Stay focused on solving the problem regardless of user tone. Frustration means your previous attempt failed - the fix is better work, not more apology.
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ class LLMMessage(BaseModel):
|
|||
injected: bool = False
|
||||
reasoning_content: Content | None = None
|
||||
reasoning_signature: str | None = None
|
||||
reasoning_message_id: str | None = None
|
||||
tool_calls: list[ToolCall] | None = None
|
||||
name: str | None = None
|
||||
tool_call_id: str | None = None
|
||||
|
|
@ -229,15 +230,20 @@ class LLMMessage(BaseModel):
|
|||
if isinstance(v, dict):
|
||||
v.setdefault("content", "")
|
||||
v.setdefault("role", "assistant")
|
||||
if "message_id" not in v and v.get("role") != "tool":
|
||||
if v.get("message_id") is None and v.get("role") != "tool":
|
||||
v["message_id"] = str(uuid4())
|
||||
if v.get("reasoning_message_id") is None and v.get("reasoning_content"):
|
||||
v["reasoning_message_id"] = str(uuid4())
|
||||
return v
|
||||
role = str(getattr(v, "role", "assistant"))
|
||||
reasoning_content = getattr(v, "reasoning_content", None)
|
||||
return {
|
||||
"role": role,
|
||||
"content": getattr(v, "content", ""),
|
||||
"reasoning_content": getattr(v, "reasoning_content", None),
|
||||
"reasoning_content": reasoning_content,
|
||||
"reasoning_signature": getattr(v, "reasoning_signature", None),
|
||||
"reasoning_message_id": getattr(v, "reasoning_message_id", None)
|
||||
or (str(uuid4()) if reasoning_content else None),
|
||||
"tool_calls": getattr(v, "tool_calls", None),
|
||||
"name": getattr(v, "name", None),
|
||||
"tool_call_id": getattr(v, "tool_call_id", None),
|
||||
|
|
@ -298,6 +304,8 @@ class LLMMessage(BaseModel):
|
|||
content=content,
|
||||
reasoning_content=reasoning_content,
|
||||
reasoning_signature=reasoning_signature,
|
||||
reasoning_message_id=self.reasoning_message_id
|
||||
or other.reasoning_message_id,
|
||||
tool_calls=list(tool_calls_map.values()) or None,
|
||||
name=self.name,
|
||||
tool_call_id=self.tool_call_id,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import httpx
|
|||
|
||||
def _is_retryable_http_error(e: Exception) -> bool:
|
||||
if isinstance(e, httpx.HTTPStatusError):
|
||||
return e.response.status_code in {408, 409, 425, 429, 500, 502, 503, 504}
|
||||
return e.response.status_code in {408, 409, 425, 429, 500, 502, 503, 504, 529}
|
||||
return False
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue