v2.9.4 (#669)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Jean-Baptiste Muscat <jeanbaptiste.muscatdupuis@mistral.ai> Co-authored-by: JeroenvdV <JeroenvdV@users.noreply.github.com> Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: allansimon-mistral <allan.simon@ext.mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
71f373c60c
commit
4972dd5694
50 changed files with 2892 additions and 842 deletions
|
|
@ -52,6 +52,7 @@ from acp.schema import (
|
|||
SessionConfigOptionSelect,
|
||||
SessionForkCapabilities,
|
||||
SessionInfo,
|
||||
SessionInfoUpdate,
|
||||
SessionListCapabilities,
|
||||
SetSessionConfigOptionResponse,
|
||||
SseMcpServer,
|
||||
|
|
@ -122,6 +123,10 @@ from vibe.core.proxy_setup import (
|
|||
set_proxy_var,
|
||||
unset_proxy_var,
|
||||
)
|
||||
from vibe.core.session.saved_sessions import (
|
||||
update_saved_session_title,
|
||||
update_saved_session_title_at_path,
|
||||
)
|
||||
from vibe.core.session.session_loader import SessionLoader
|
||||
from vibe.core.skills.manager import SkillManager
|
||||
from vibe.core.telemetry.build_metadata import build_entrypoint_metadata
|
||||
|
|
@ -159,6 +164,15 @@ class ForkSessionParams(BaseModel):
|
|||
message_id: str | None = Field(default=None, alias="messageId")
|
||||
|
||||
|
||||
class SessionSetTitleRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
session_id: str = Field(
|
||||
validation_alias=AliasChoices("session_id", "sessionId"), min_length=1
|
||||
)
|
||||
title: str = Field(min_length=1)
|
||||
|
||||
|
||||
class TelemetrySendNotification(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
|
|
@ -167,7 +181,20 @@ class TelemetrySendNotification(BaseModel):
|
|||
session_id: str = Field(validation_alias=AliasChoices("session_id", "sessionId"))
|
||||
|
||||
|
||||
_EVENT_DISPATCHERS: dict[str, Callable[[TelemetryClient, dict[str, Any]], None]] = {}
|
||||
def _dispatch_at_mention_inserted(
|
||||
client: TelemetryClient, properties: dict[str, Any]
|
||||
) -> None:
|
||||
client.send_at_mention_inserted(
|
||||
nb_mentions=properties.get("nb_mentions", 0),
|
||||
context_types=properties.get("context_types", {}),
|
||||
file_extensions=properties.get("file_extensions"),
|
||||
message_id=properties.get("message_id"),
|
||||
)
|
||||
|
||||
|
||||
_EVENT_DISPATCHERS: dict[str, Callable[[TelemetryClient, dict[str, Any]], None]] = {
|
||||
"vibe.at_mention_inserted": _dispatch_at_mention_inserted
|
||||
}
|
||||
|
||||
|
||||
def _resolved_user_message_id(client_message_id: str | None) -> str:
|
||||
|
|
@ -301,9 +328,20 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
agent_loop.set_approval_callback(self._create_approval_callback(session.id))
|
||||
|
||||
session.spawn(self._send_available_commands(session))
|
||||
session.spawn(self._warm_up_agent_loop(agent_loop))
|
||||
|
||||
return session
|
||||
|
||||
async def _warm_up_agent_loop(self, agent_loop: AgentLoop) -> None:
|
||||
"""Proactively await deferred init so `vibe.ready` telemetry is emitted
|
||||
without waiting for the user's first prompt. Errors are swallowed here
|
||||
and will resurface on the first `act()` call via `requires_init`.
|
||||
"""
|
||||
try:
|
||||
await agent_loop.wait_until_ready()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _create_agent_loop(
|
||||
self, config: VibeConfig, agent_name: str, hook_config_result: Any = None
|
||||
) -> AgentLoop:
|
||||
|
|
@ -398,6 +436,11 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
case ToolOption.ALLOW_ALWAYS:
|
||||
session.agent_loop.approve_always(tool_name, required_permissions)
|
||||
return (ApprovalResponse.YES, None)
|
||||
case ToolOption.ALLOW_ALWAYS_PERMANENT:
|
||||
session.agent_loop.approve_always(
|
||||
tool_name, required_permissions, save_permanently=True
|
||||
)
|
||||
return (ApprovalResponse.YES, None)
|
||||
case ToolOption.REJECT_ONCE:
|
||||
session.agent_loop.telemetry_client.send_user_cancelled_action(
|
||||
"reject_approval"
|
||||
|
|
@ -454,6 +497,29 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
raise SessionNotFoundError(session_id)
|
||||
return self.sessions[session_id]
|
||||
|
||||
def _find_acp_session_by_vibe_session_id(
|
||||
self, session_id: str
|
||||
) -> AcpSessionLoop | None:
|
||||
for candidate in self.sessions.values():
|
||||
if candidate.agent_loop.session_id == session_id:
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
def _load_session_logging_config(self) -> SessionLoggingConfig:
|
||||
try:
|
||||
return VibeConfig.load().session_logging
|
||||
except MissingAPIKeyError:
|
||||
try:
|
||||
persisted_config = VibeConfig.get_persisted_config()
|
||||
return SessionLoggingConfig.model_validate(
|
||||
persisted_config.get("session_logging", {})
|
||||
)
|
||||
except Exception as e:
|
||||
raise ConfigurationError(str(e)) from e
|
||||
except Exception as e:
|
||||
raise ConfigurationError(str(e)) from e
|
||||
|
||||
def _build_usage(self, session: AcpSessionLoop) -> Usage:
|
||||
stats = session.agent_loop.stats
|
||||
return Usage(
|
||||
|
|
@ -1008,9 +1074,97 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
) -> ResumeSessionResponse:
|
||||
raise NotImplementedMethodError("resume_session")
|
||||
|
||||
async def _emit_session_info_update(
|
||||
self, session_id: str, *, title: str, updated_at: str | None
|
||||
) -> None:
|
||||
update_kwargs: dict[str, Any] = {
|
||||
"session_update": "session_info_update",
|
||||
"title": title,
|
||||
}
|
||||
if updated_at is not None:
|
||||
update_kwargs["updated_at"] = updated_at
|
||||
|
||||
await self.client.session_update(
|
||||
session_id=session_id, update=SessionInfoUpdate(**update_kwargs)
|
||||
)
|
||||
|
||||
async def _persist_live_session_title(
|
||||
self, session: AcpSessionLoop, title: str
|
||||
) -> dict[str, Any] | None:
|
||||
logger = session.agent_loop.session_logger
|
||||
if not logger.enabled or logger.session_dir is None:
|
||||
return None
|
||||
if not logger.metadata_filepath.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
return await update_saved_session_title_at_path(logger.session_dir, title)
|
||||
except ValueError as exc:
|
||||
raise InternalError(
|
||||
f"Failed to persist title update for session {logger.session_id}: {exc}"
|
||||
) from exc
|
||||
|
||||
def _set_live_session_title(self, session: AcpSessionLoop, title: str) -> None:
|
||||
try:
|
||||
session.agent_loop.session_logger.set_title(title)
|
||||
except ValueError as exc:
|
||||
raise InvalidRequestError(
|
||||
f"Invalid ACP session title request: {exc}"
|
||||
) from exc
|
||||
|
||||
async def _handle_session_set_title(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
request = SessionSetTitleRequest.model_validate(params)
|
||||
except ValidationError as exc:
|
||||
raise InvalidRequestError(
|
||||
f"Invalid ACP session title request: {exc}"
|
||||
) from exc
|
||||
|
||||
live_session = self.sessions.get(
|
||||
request.session_id
|
||||
) or self._find_acp_session_by_vibe_session_id(request.session_id)
|
||||
if live_session is None:
|
||||
try:
|
||||
metadata = await update_saved_session_title(
|
||||
request.session_id,
|
||||
request.title,
|
||||
self._load_session_logging_config(),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise SessionNotFoundError(request.session_id) from exc
|
||||
|
||||
await self._emit_session_info_update(
|
||||
request.session_id,
|
||||
title=request.title,
|
||||
updated_at=metadata.get("end_time"),
|
||||
)
|
||||
return {}
|
||||
|
||||
persisted_metadata = await self._persist_live_session_title(
|
||||
live_session, request.title
|
||||
)
|
||||
self._set_live_session_title(live_session, request.title)
|
||||
updated_at = (
|
||||
persisted_metadata.get("end_time")
|
||||
if persisted_metadata is not None
|
||||
else (
|
||||
live_session.agent_loop.session_logger.session_metadata.end_time
|
||||
if live_session.agent_loop.session_logger.session_metadata is not None
|
||||
else None
|
||||
)
|
||||
)
|
||||
|
||||
await self._emit_session_info_update(
|
||||
live_session.id, title=request.title, updated_at=updated_at
|
||||
)
|
||||
return {}
|
||||
|
||||
@override
|
||||
async def ext_method(self, method: str, params: dict) -> dict:
|
||||
raise NotImplementedMethodError("ext_method")
|
||||
if method == "session/set_title":
|
||||
return await self._handle_session_set_title(params)
|
||||
|
||||
raise NotImplementedMethodError(method)
|
||||
|
||||
@override
|
||||
async def ext_notification(self, method: str, params: dict) -> None:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from acp.schema import (
|
||||
AgentMessageChunk,
|
||||
|
|
@ -9,6 +9,7 @@ from acp.schema import (
|
|||
ContentToolCallContent,
|
||||
ModelInfo,
|
||||
PermissionOption,
|
||||
PermissionOptionKind,
|
||||
SessionConfigOptionSelect,
|
||||
SessionConfigSelectOption,
|
||||
SessionMode,
|
||||
|
|
@ -34,25 +35,31 @@ if TYPE_CHECKING:
|
|||
class ToolOption(StrEnum):
|
||||
ALLOW_ONCE = "allow_once"
|
||||
ALLOW_ALWAYS = "allow_always"
|
||||
ALLOW_ALWAYS_PERMANENT = "allow_always_permanent"
|
||||
REJECT_ONCE = "reject_once"
|
||||
REJECT_ALWAYS = "reject_always"
|
||||
|
||||
|
||||
_KIND_ALLOW_ONCE: PermissionOptionKind = "allow_once"
|
||||
_KIND_ALLOW_ALWAYS: PermissionOptionKind = "allow_always"
|
||||
_KIND_REJECT_ONCE: PermissionOptionKind = "reject_once"
|
||||
|
||||
TOOL_OPTIONS = [
|
||||
PermissionOption(
|
||||
option_id=ToolOption.ALLOW_ONCE,
|
||||
name="Allow once",
|
||||
kind=cast(Literal["allow_once"], ToolOption.ALLOW_ONCE),
|
||||
option_id=ToolOption.ALLOW_ONCE, name="Allow once", kind=_KIND_ALLOW_ONCE
|
||||
),
|
||||
PermissionOption(
|
||||
option_id=ToolOption.ALLOW_ALWAYS,
|
||||
name="Allow for this session",
|
||||
kind=cast(Literal["allow_always"], ToolOption.ALLOW_ALWAYS),
|
||||
name="Allow for remainder of this session",
|
||||
kind=_KIND_ALLOW_ALWAYS,
|
||||
),
|
||||
PermissionOption(
|
||||
option_id=ToolOption.REJECT_ONCE,
|
||||
name="Reject once",
|
||||
kind=cast(Literal["reject_once"], ToolOption.REJECT_ONCE),
|
||||
option_id=ToolOption.ALLOW_ALWAYS_PERMANENT,
|
||||
name="Always allow",
|
||||
kind=_KIND_ALLOW_ALWAYS,
|
||||
),
|
||||
PermissionOption(
|
||||
option_id=ToolOption.REJECT_ONCE, name="Deny", kind=_KIND_REJECT_ONCE
|
||||
),
|
||||
]
|
||||
|
||||
|
|
@ -64,7 +71,6 @@ def build_permission_options(
|
|||
if not required_permissions:
|
||||
return TOOL_OPTIONS
|
||||
|
||||
labels = ", ".join(rp.label for rp in required_permissions)
|
||||
permissions_meta = [
|
||||
{
|
||||
"scope": rp.scope,
|
||||
|
|
@ -77,20 +83,22 @@ def build_permission_options(
|
|||
|
||||
return [
|
||||
PermissionOption(
|
||||
option_id=ToolOption.ALLOW_ONCE,
|
||||
name="Allow once",
|
||||
kind=cast(Literal["allow_once"], ToolOption.ALLOW_ONCE),
|
||||
option_id=ToolOption.ALLOW_ONCE, name="Allow once", kind=_KIND_ALLOW_ONCE
|
||||
),
|
||||
PermissionOption(
|
||||
option_id=ToolOption.ALLOW_ALWAYS,
|
||||
name=f"Allow for this session: {labels}",
|
||||
kind=cast(Literal["allow_always"], ToolOption.ALLOW_ALWAYS),
|
||||
name="Allow for remainder of this session",
|
||||
kind=_KIND_ALLOW_ALWAYS,
|
||||
field_meta={"required_permissions": permissions_meta},
|
||||
),
|
||||
PermissionOption(
|
||||
option_id=ToolOption.REJECT_ONCE,
|
||||
name="Reject once",
|
||||
kind=cast(Literal["reject_once"], ToolOption.REJECT_ONCE),
|
||||
option_id=ToolOption.ALLOW_ALWAYS_PERMANENT,
|
||||
name="Always allow",
|
||||
kind=_KIND_ALLOW_ALWAYS,
|
||||
field_meta={"required_permissions": permissions_meta},
|
||||
),
|
||||
PermissionOption(
|
||||
option_id=ToolOption.REJECT_ONCE, name="Deny", kind=_KIND_REJECT_ONCE
|
||||
),
|
||||
]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue