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:
Clément Drouin 2026-03-31 16:28:55 +02:00 committed by GitHub
parent 6a50d1d521
commit 54b9a17457
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1000 additions and 200 deletions

View file

@ -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"

View file

@ -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)

View file

@ -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,
)

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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
]

View file

@ -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}",

View file

@ -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.

View file

@ -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,

View file

@ -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