v2.4.2 (#482)
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai> Co-authored-by: Clement Sirieix <clem.sirieix@gmail.com> Co-authored-by: laurens <laurens@mistral.ai>
This commit is contained in:
parent
e9428bce23
commit
9421fbc08e
21 changed files with 370 additions and 40 deletions
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.2",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ACP Server",
|
||||
|
|
|
|||
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.4.2] - 2026-03-12
|
||||
|
||||
### Added
|
||||
|
||||
- Session ID included in telemetry events for better tracing
|
||||
|
||||
### Changed
|
||||
|
||||
- Skills now extract arguments when invoked, improving parameter handling
|
||||
- Auto-compact threshold falls back to global setting when not defined at model level
|
||||
- Update notification toast no longer times out, ensuring the user sees the restart prompt
|
||||
- Removed `file_content_before` from Vibe Code, reducing payload size
|
||||
|
||||
|
||||
## [2.4.1] - 2026-03-10
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
id = "mistral-vibe"
|
||||
name = "Mistral Vibe"
|
||||
description = "Mistral's open-source coding assistant"
|
||||
version = "2.4.1"
|
||||
version = "2.4.2"
|
||||
schema_version = 1
|
||||
authors = ["Mistral AI"]
|
||||
repository = "https://github.com/mistralai/mistral-vibe"
|
||||
|
|
@ -11,25 +11,25 @@ name = "Mistral Vibe"
|
|||
icon = "./icons/mistral_vibe.svg"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.darwin-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.1/vibe-acp-darwin-aarch64-2.4.1.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.2/vibe-acp-darwin-aarch64-2.4.2.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.darwin-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.1/vibe-acp-darwin-x86_64-2.4.1.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.2/vibe-acp-darwin-x86_64-2.4.2.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.linux-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.1/vibe-acp-linux-aarch64-2.4.1.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.2/vibe-acp-linux-aarch64-2.4.2.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.linux-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.1/vibe-acp-linux-x86_64-2.4.1.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.2/vibe-acp-linux-x86_64-2.4.2.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.windows-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.1/vibe-acp-windows-aarch64-2.4.1.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.2/vibe-acp-windows-aarch64-2.4.2.zip"
|
||||
cmd = "./vibe-acp.exe"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.windows-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.1/vibe-acp-windows-x86_64-2.4.1.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.2/vibe-acp-windows-x86_64-2.4.2.zip"
|
||||
cmd = "./vibe-acp.exe"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "mistral-vibe"
|
||||
version = "2.4.1"
|
||||
version = "2.4.2"
|
||||
description = "Minimal CLI coding agent by Mistral"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class TestACPInitialize:
|
|||
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.4.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.4.2"
|
||||
)
|
||||
|
||||
assert response.auth_methods == []
|
||||
|
|
@ -52,7 +52,7 @@ class TestACPInitialize:
|
|||
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.4.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.4.2"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
|
|
@ -114,10 +114,6 @@ class TestAcpSearchReplaceExecution:
|
|||
assert isinstance(result, SearchReplaceResult)
|
||||
assert result.file == str(test_file)
|
||||
assert result.blocks_applied == 1
|
||||
assert (
|
||||
result.file_content_before
|
||||
== "original line 1\noriginal line 2\noriginal line 3"
|
||||
)
|
||||
assert mock_client._read_text_file_called
|
||||
assert mock_client._write_text_file_called
|
||||
assert mock_client._session_update_called
|
||||
|
|
@ -318,7 +314,6 @@ class TestAcpSearchReplaceSessionUpdates:
|
|||
lines_changed=1,
|
||||
content=search_replace_content,
|
||||
warnings=[],
|
||||
file_content_before="old text",
|
||||
)
|
||||
|
||||
event = ToolResultEvent(
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ class TestAcpWriteFileExecution:
|
|||
assert result.content == "Hello, world!"
|
||||
assert result.bytes_written == len(b"Hello, world!")
|
||||
assert result.file_existed is False
|
||||
assert result.file_content_before is None
|
||||
assert mock_client._write_text_file_called
|
||||
assert mock_client._session_update_called
|
||||
|
||||
|
|
@ -114,7 +113,6 @@ class TestAcpWriteFileExecution:
|
|||
assert result.content == "New content"
|
||||
assert result.bytes_written == len(b"New content")
|
||||
assert result.file_existed is True
|
||||
assert result.file_content_before == ""
|
||||
assert mock_client._write_text_file_called
|
||||
assert mock_client._session_update_called
|
||||
|
||||
|
|
|
|||
152
tests/cli/test_ui_skill_dispatch.py
Normal file
152
tests/cli/test_ui_skill_dispatch.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import build_test_vibe_app, build_test_vibe_config
|
||||
from tests.skills.conftest import create_skill
|
||||
from vibe.cli.textual_ui.app import VibeApp
|
||||
from vibe.cli.textual_ui.widgets.chat_input.container import ChatInputContainer
|
||||
from vibe.cli.textual_ui.widgets.messages import ErrorMessage, UserMessage
|
||||
|
||||
SKILL_BODY = "## Instructions\n\nDo the thing."
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vibe_app_with_skills(tmp_path: Path) -> VibeApp:
|
||||
skills_dir = tmp_path / "skills"
|
||||
skills_dir.mkdir()
|
||||
create_skill(skills_dir, "my-skill", body=SKILL_BODY)
|
||||
return build_test_vibe_app(config=build_test_vibe_config(skill_paths=[skills_dir]))
|
||||
|
||||
|
||||
async def _wait_for_user_message_containing(
|
||||
vibe_app: VibeApp, pilot, text: str, timeout: float = 1.0
|
||||
) -> UserMessage:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
for message in vibe_app.query(UserMessage):
|
||||
if text in message._content:
|
||||
return message
|
||||
await pilot.pause(0.05)
|
||||
raise TimeoutError(
|
||||
f"UserMessage containing {text!r} did not appear within {timeout}s"
|
||||
)
|
||||
|
||||
|
||||
async def _wait_for_error_message_containing(
|
||||
vibe_app: VibeApp, pilot, text: str, timeout: float = 1.0
|
||||
) -> ErrorMessage:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
for error in vibe_app.query(ErrorMessage):
|
||||
if text in error._error:
|
||||
return error
|
||||
await pilot.pause(0.05)
|
||||
raise TimeoutError(
|
||||
f"ErrorMessage containing {text!r} did not appear within {timeout}s"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skill_without_args_sends_skill_content(
|
||||
vibe_app_with_skills: VibeApp,
|
||||
) -> None:
|
||||
async with vibe_app_with_skills.run_test() as pilot:
|
||||
await pilot.pause(0.1)
|
||||
chat_input = vibe_app_with_skills.query_one(ChatInputContainer)
|
||||
chat_input.post_message(ChatInputContainer.Submitted("/my-skill"))
|
||||
await pilot.pause(0.1)
|
||||
|
||||
message = await _wait_for_user_message_containing(
|
||||
vibe_app_with_skills, pilot, "Do the thing."
|
||||
)
|
||||
assert "Do the thing." in message._content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skill_with_args_prepends_invocation_line(
|
||||
vibe_app_with_skills: VibeApp,
|
||||
) -> None:
|
||||
async with vibe_app_with_skills.run_test() as pilot:
|
||||
await pilot.pause(0.1)
|
||||
chat_input = vibe_app_with_skills.query_one(ChatInputContainer)
|
||||
chat_input.post_message(ChatInputContainer.Submitted("/my-skill foo bar"))
|
||||
await pilot.pause(0.1)
|
||||
|
||||
message = await _wait_for_user_message_containing(
|
||||
vibe_app_with_skills, pilot, "Do the thing."
|
||||
)
|
||||
assert "/my-skill foo bar" in message._content
|
||||
assert "Do the thing." in message._content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_skill_falls_through_to_agent(
|
||||
vibe_app_with_skills: VibeApp,
|
||||
) -> None:
|
||||
async with vibe_app_with_skills.run_test() as pilot:
|
||||
await pilot.pause(0.1)
|
||||
chat_input = vibe_app_with_skills.query_one(ChatInputContainer)
|
||||
chat_input.post_message(ChatInputContainer.Submitted("/nonexistent-skill"))
|
||||
await pilot.pause(0.2)
|
||||
|
||||
skill_errors = [
|
||||
e
|
||||
for e in vibe_app_with_skills.query(ErrorMessage)
|
||||
if "skill" in str(getattr(e, "_error", "")).lower()
|
||||
]
|
||||
assert not skill_errors
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bare_slash_falls_through(vibe_app_with_skills: VibeApp) -> None:
|
||||
async with vibe_app_with_skills.run_test() as pilot:
|
||||
await pilot.pause(0.1)
|
||||
chat_input = vibe_app_with_skills.query_one(ChatInputContainer)
|
||||
chat_input.post_message(ChatInputContainer.Submitted("/"))
|
||||
await pilot.pause(0.2)
|
||||
|
||||
assert not any(
|
||||
"Do the thing." in m._content
|
||||
for m in vibe_app_with_skills.query(UserMessage)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skill_without_args_does_not_prepend_invocation_line(
|
||||
vibe_app_with_skills: VibeApp,
|
||||
) -> None:
|
||||
async with vibe_app_with_skills.run_test() as pilot:
|
||||
await pilot.pause(0.1)
|
||||
chat_input = vibe_app_with_skills.query_one(ChatInputContainer)
|
||||
chat_input.post_message(ChatInputContainer.Submitted("/my-skill"))
|
||||
await pilot.pause(0.1)
|
||||
|
||||
message = await _wait_for_user_message_containing(
|
||||
vibe_app_with_skills, pilot, "Do the thing."
|
||||
)
|
||||
assert "/my-skill" not in message._content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skill_with_missing_file_shows_error(tmp_path: Path) -> None:
|
||||
skills_dir = tmp_path / "skills"
|
||||
skills_dir.mkdir()
|
||||
skill_dir = create_skill(skills_dir, "my-skill", body=SKILL_BODY)
|
||||
vibe_app = build_test_vibe_app(
|
||||
config=build_test_vibe_config(skill_paths=[skills_dir])
|
||||
)
|
||||
(skill_dir / "SKILL.md").unlink()
|
||||
|
||||
async with vibe_app.run_test() as pilot:
|
||||
await pilot.pause(0.1)
|
||||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||
chat_input.post_message(ChatInputContainer.Submitted("/my-skill"))
|
||||
|
||||
error = await _wait_for_error_message_containing(
|
||||
vibe_app, pilot, "Failed to read skill file"
|
||||
)
|
||||
assert "Failed to read skill file" in error._error
|
||||
|
|
@ -4,6 +4,8 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import build_test_vibe_config
|
||||
from vibe.core.config import ModelConfig
|
||||
from vibe.core.config.harness_files import (
|
||||
HarnessFilesManager,
|
||||
init_harness_files_manager,
|
||||
|
|
@ -80,3 +82,40 @@ class TestResolveConfigFile:
|
|||
def test_user_only_returns_global_config(self) -> None:
|
||||
mgr = HarnessFilesManager(sources=("user",))
|
||||
assert mgr.config_file == VIBE_HOME.path / "config.toml"
|
||||
|
||||
|
||||
class TestAutoCompactThresholdFallback:
|
||||
def test_model_without_explicit_threshold_inherits_global(self) -> None:
|
||||
model = ModelConfig(name="m", provider="p", alias="m")
|
||||
cfg = build_test_vibe_config(
|
||||
auto_compact_threshold=42_000, models=[model], active_model="m"
|
||||
)
|
||||
assert cfg.get_active_model().auto_compact_threshold == 42_000
|
||||
|
||||
def test_model_with_explicit_threshold_keeps_own_value(self) -> None:
|
||||
model = ModelConfig(
|
||||
name="m", provider="p", alias="m", auto_compact_threshold=99_000
|
||||
)
|
||||
cfg = build_test_vibe_config(
|
||||
auto_compact_threshold=42_000, models=[model], active_model="m"
|
||||
)
|
||||
assert cfg.get_active_model().auto_compact_threshold == 99_000
|
||||
|
||||
def test_default_global_threshold_used_when_nothing_set(self) -> None:
|
||||
model = ModelConfig(name="m", provider="p", alias="m")
|
||||
cfg = build_test_vibe_config(models=[model], active_model="m")
|
||||
assert cfg.get_active_model().auto_compact_threshold == 200_000
|
||||
|
||||
def test_changed_global_threshold_propagates_on_reload(self) -> None:
|
||||
model = ModelConfig(name="m", provider="p", alias="m")
|
||||
|
||||
cfg1 = build_test_vibe_config(
|
||||
auto_compact_threshold=50_000, models=[model], active_model="m"
|
||||
)
|
||||
assert cfg1.get_active_model().auto_compact_threshold == 50_000
|
||||
|
||||
# Simulate config reload with a different global threshold
|
||||
cfg2 = build_test_vibe_config(
|
||||
auto_compact_threshold=75_000, models=[model], active_model="m"
|
||||
)
|
||||
assert cfg2.get_active_model().auto_compact_threshold == 75_000
|
||||
|
|
|
|||
|
|
@ -272,3 +272,103 @@ class TestTelemetryClient:
|
|||
assert properties["entrypoint"] == "cli"
|
||||
assert properties["terminal_emulator"] == "vscode"
|
||||
assert "version" in properties
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_id_added_when_getter_provided(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
TelemetryClient, "send_telemetry_event", _original_send_telemetry_event
|
||||
)
|
||||
config = build_test_vibe_config(enable_telemetry=True)
|
||||
env_key = config.get_provider_for_model(
|
||||
config.get_active_model()
|
||||
).api_key_env_var
|
||||
monkeypatch.setenv(env_key, "sk-test")
|
||||
session_id = "test-session-uuid"
|
||||
client = TelemetryClient(
|
||||
config_getter=lambda: config, session_id_getter=lambda: session_id
|
||||
)
|
||||
mock_post = AsyncMock(return_value=MagicMock(status_code=204))
|
||||
client._client = MagicMock()
|
||||
client._client.post = mock_post
|
||||
client._client.aclose = AsyncMock()
|
||||
|
||||
client.send_telemetry_event("vibe.test_event", {"key": "value"})
|
||||
await client.aclose()
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
DATALAKE_EVENTS_URL,
|
||||
json={
|
||||
"event": "vibe.test_event",
|
||||
"properties": {"session_id": session_id, "key": "value"},
|
||||
},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer sk-test",
|
||||
"User-Agent": get_user_agent(Backend.MISTRAL),
|
||||
},
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_id_absent_when_no_getter(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
TelemetryClient, "send_telemetry_event", _original_send_telemetry_event
|
||||
)
|
||||
config = build_test_vibe_config(enable_telemetry=True)
|
||||
env_key = config.get_provider_for_model(
|
||||
config.get_active_model()
|
||||
).api_key_env_var
|
||||
monkeypatch.setenv(env_key, "sk-test")
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
mock_post = AsyncMock(return_value=MagicMock(status_code=204))
|
||||
client._client = MagicMock()
|
||||
client._client.post = mock_post
|
||||
client._client.aclose = AsyncMock()
|
||||
|
||||
client.send_telemetry_event("vibe.test_event", {"key": "value"})
|
||||
await client.aclose()
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
DATALAKE_EVENTS_URL,
|
||||
json={"event": "vibe.test_event", "properties": {"key": "value"}},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer sk-test",
|
||||
"User-Agent": get_user_agent(Backend.MISTRAL),
|
||||
},
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_id_getter_reflects_latest_value(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
TelemetryClient, "send_telemetry_event", _original_send_telemetry_event
|
||||
)
|
||||
config = build_test_vibe_config(enable_telemetry=True)
|
||||
env_key = config.get_provider_for_model(
|
||||
config.get_active_model()
|
||||
).api_key_env_var
|
||||
monkeypatch.setenv(env_key, "sk-test")
|
||||
current_id = "first-session-id"
|
||||
client = TelemetryClient(
|
||||
config_getter=lambda: config, session_id_getter=lambda: current_id
|
||||
)
|
||||
mock_post = AsyncMock(return_value=MagicMock(status_code=204))
|
||||
client._client = MagicMock()
|
||||
client._client.post = mock_post
|
||||
client._client.aclose = AsyncMock()
|
||||
|
||||
client.send_telemetry_event("vibe.test_event", {})
|
||||
current_id = "second-session-id"
|
||||
client.send_telemetry_event("vibe.test_event", {})
|
||||
await client.aclose()
|
||||
|
||||
calls = mock_post.call_args_list
|
||||
assert calls[0].kwargs["json"]["properties"]["session_id"] == "first-session-id"
|
||||
assert (
|
||||
calls[1].kwargs["json"]["properties"]["session_id"] == "second-session-id"
|
||||
)
|
||||
|
|
|
|||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -779,7 +779,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "mistral-vibe"
|
||||
version = "2.4.1"
|
||||
version = "2.4.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "agent-client-protocol" },
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ from __future__ import annotations
|
|||
from pathlib import Path
|
||||
|
||||
VIBE_ROOT = Path(__file__).parent
|
||||
__version__ = "2.4.1"
|
||||
__version__ = "2.4.2"
|
||||
|
|
|
|||
|
|
@ -537,7 +537,11 @@ class VibeApp(App): # noqa: PLR0904
|
|||
if not self.agent_loop:
|
||||
return False
|
||||
|
||||
skill_name = user_input[1:].strip().lower()
|
||||
parts = user_input[1:].strip().split(None, 1)
|
||||
if not parts:
|
||||
return False
|
||||
skill_name = parts[0].lower()
|
||||
|
||||
skill_info = self.agent_loop.skill_manager.get_skill(skill_name)
|
||||
if not skill_info:
|
||||
return False
|
||||
|
|
@ -554,6 +558,9 @@ class VibeApp(App): # noqa: PLR0904
|
|||
)
|
||||
return True
|
||||
|
||||
if len(parts) > 1:
|
||||
skill_content = f"{user_input}\n\n{skill_content}"
|
||||
|
||||
await self._handle_user_message(skill_content)
|
||||
return True
|
||||
|
||||
|
|
@ -1599,7 +1606,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
f"{update_message_prefix}\nVibe was updated successfully. Please restart to use the new version.",
|
||||
title="Update successful",
|
||||
severity="information",
|
||||
timeout=10,
|
||||
timeout=float("inf"),
|
||||
)
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -199,7 +199,9 @@ class AgentLoop:
|
|||
self.session_id = str(uuid4())
|
||||
self._current_user_message_id: str | None = None
|
||||
|
||||
self.telemetry_client = TelemetryClient(config_getter=lambda: self.config)
|
||||
self.telemetry_client = TelemetryClient(
|
||||
config_getter=lambda: self.config, session_id_getter=lambda: self.session_id
|
||||
)
|
||||
self.session_logger = SessionLogger(config.session_logging, self.session_id)
|
||||
self._teleport_service: TeleportService | None = None
|
||||
|
||||
|
|
@ -297,6 +299,7 @@ class AgentLoop:
|
|||
nuage_base_url=self.config.nuage_base_url,
|
||||
nuage_workflow_id=self.config.nuage_workflow_id,
|
||||
nuage_api_key=self.config.nuage_api_key,
|
||||
nuage_task_queue=self.config.nuage_task_queue,
|
||||
)
|
||||
return self._teleport_service
|
||||
|
||||
|
|
|
|||
|
|
@ -309,11 +309,13 @@ class VibeConfig(BaseSettings):
|
|||
enable_auto_update: bool = True
|
||||
enable_notifications: bool = True
|
||||
api_timeout: float = 720.0
|
||||
auto_compact_threshold: int = 200_000
|
||||
|
||||
# TODO(vibe-nuage): remove exclude=True once the feature is publicly available
|
||||
nuage_enabled: bool = Field(default=False, exclude=True)
|
||||
nuage_base_url: str = Field(default="https://api.globalaegis.net", exclude=True)
|
||||
nuage_workflow_id: str = Field(default="__shared-nuage-workflow", exclude=True)
|
||||
nuage_task_queue: str | None = Field(default="shared-vibe-nuage", exclude=True)
|
||||
# TODO(vibe-nuage): change default value to MISTRAL_API_KEY once prod has shared vibe-nuage workers
|
||||
nuage_api_key_env_var: str = Field(default="STAGING_MISTRAL_API_KEY", exclude=True)
|
||||
|
||||
|
|
@ -466,6 +468,18 @@ class VibeConfig(BaseSettings):
|
|||
file_secret_settings,
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _apply_global_auto_compact_threshold(self) -> VibeConfig:
|
||||
self.models = [
|
||||
model
|
||||
if "auto_compact_threshold" in model.model_fields_set
|
||||
else model.model_copy(
|
||||
update={"auto_compact_threshold": self.auto_compact_threshold}
|
||||
)
|
||||
for model in self.models
|
||||
]
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_api_key(self) -> VibeConfig:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -19,8 +19,13 @@ DATALAKE_EVENTS_URL = "https://codestral.mistral.ai/v1/datalake/events"
|
|||
|
||||
|
||||
class TelemetryClient:
|
||||
def __init__(self, config_getter: Callable[[], VibeConfig]) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
config_getter: Callable[[], VibeConfig],
|
||||
session_id_getter: Callable[[], str | None] | None = None,
|
||||
) -> None:
|
||||
self._config_getter = config_getter
|
||||
self._session_id_getter = session_id_getter
|
||||
self._client: httpx.AsyncClient | None = None
|
||||
self._pending_tasks: set[asyncio.Task[Any]] = set()
|
||||
|
||||
|
|
@ -71,6 +76,11 @@ class TelemetryClient:
|
|||
if mistral_api_key is None or not self._is_enabled():
|
||||
return
|
||||
user_agent = self._get_telemetry_user_agent()
|
||||
if (
|
||||
self._session_id_getter is not None
|
||||
and (session_id := self._session_id_getter()) is not None
|
||||
):
|
||||
properties = {**properties, "session_id": session_id}
|
||||
|
||||
async def _send() -> None:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -71,12 +71,14 @@ class NuageClient:
|
|||
api_key: str,
|
||||
workflow_id: str,
|
||||
*,
|
||||
task_queue: str | None = None,
|
||||
client: httpx.AsyncClient | None = None,
|
||||
timeout: float = 60.0,
|
||||
) -> None:
|
||||
self._base_url = base_url.rstrip("/")
|
||||
self._api_key = api_key
|
||||
self._workflow_id = workflow_id
|
||||
self._task_queue = task_queue
|
||||
self._client = client
|
||||
self._owns_client = client is None
|
||||
self._timeout = timeout
|
||||
|
|
@ -113,7 +115,10 @@ class NuageClient:
|
|||
response = await self._http_client.post(
|
||||
f"{self._base_url}/v1/workflows/{self._workflow_id}/execute",
|
||||
headers=self._headers(),
|
||||
json={"input": params.model_dump(mode="json")},
|
||||
json={
|
||||
"input": params.model_dump(mode="json"),
|
||||
"task_queue": self._task_queue,
|
||||
},
|
||||
)
|
||||
if not response.is_success:
|
||||
error_msg = f"Nuage workflow trigger failed: {response.text}"
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ class TeleportService:
|
|||
nuage_api_key: str,
|
||||
workdir: Path | None = None,
|
||||
*,
|
||||
nuage_task_queue: str | None = None,
|
||||
client: httpx.AsyncClient | None = None,
|
||||
timeout: float = 60.0,
|
||||
) -> None:
|
||||
|
|
@ -55,6 +56,7 @@ class TeleportService:
|
|||
self._nuage_base_url = nuage_base_url
|
||||
self._nuage_workflow_id = nuage_workflow_id
|
||||
self._nuage_api_key = nuage_api_key
|
||||
self._nuage_task_queue = nuage_task_queue
|
||||
self._git = GitRepository(workdir)
|
||||
self._client = client
|
||||
self._owns_client = client is None
|
||||
|
|
@ -70,6 +72,7 @@ class TeleportService:
|
|||
self._nuage_base_url,
|
||||
self._nuage_api_key,
|
||||
self._nuage_workflow_id,
|
||||
task_queue=self._nuage_task_queue,
|
||||
client=self._client,
|
||||
)
|
||||
await self._git.__aenter__()
|
||||
|
|
@ -106,6 +109,7 @@ class TeleportService:
|
|||
self._nuage_base_url,
|
||||
self._nuage_api_key,
|
||||
self._nuage_workflow_id,
|
||||
task_queue=self._nuage_task_queue,
|
||||
client=self._http_client,
|
||||
)
|
||||
return self._nuage
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ class SearchReplaceResult(BaseModel):
|
|||
lines_changed: int
|
||||
content: str
|
||||
warnings: list[str] = Field(default_factory=list)
|
||||
file_content_before: str
|
||||
|
||||
|
||||
class SearchReplaceConfig(BaseToolConfig):
|
||||
|
|
@ -163,7 +162,6 @@ class SearchReplace(
|
|||
lines_changed=lines_changed,
|
||||
warnings=block_result.warnings,
|
||||
content=args.content,
|
||||
file_content_before=original_content,
|
||||
)
|
||||
|
||||
@final
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ class WriteFileResult(BaseModel):
|
|||
bytes_written: int
|
||||
file_existed: bool
|
||||
content: str
|
||||
file_content_before: str | None = None
|
||||
|
||||
|
||||
class WriteFileConfig(BaseToolConfig):
|
||||
|
|
@ -85,14 +84,6 @@ class WriteFile(
|
|||
) -> AsyncGenerator[ToolStreamEvent | WriteFileResult, None]:
|
||||
file_path, file_existed, content_bytes = self._prepare_and_validate_path(args)
|
||||
|
||||
file_content_before: str | None = None
|
||||
if file_existed and args.overwrite:
|
||||
try:
|
||||
async with await anyio.Path(file_path).open(encoding="utf-8") as f:
|
||||
file_content_before = await f.read(524_288) # 512kb
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await self._write_file(args, file_path)
|
||||
|
||||
yield WriteFileResult(
|
||||
|
|
@ -100,7 +91,6 @@ class WriteFile(
|
|||
bytes_written=content_bytes,
|
||||
file_existed=file_existed,
|
||||
content=args.content,
|
||||
file_content_before=file_content_before,
|
||||
)
|
||||
|
||||
def _prepare_and_validate_path(self, args: WriteFileArgs) -> tuple[Path, bool, int]:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# What's new in v2.4.1
|
||||
- **Prompt mode**: Disabled interactive questions in prompt mode for smoother non-interactive usage
|
||||
- **VS Code terminal fix**: Space key now works correctly in all input widgets (question prompts, proxy setup)
|
||||
# What's new in v2.4.2
|
||||
|
||||
- **Skill arguments**: Skills now extract arguments when invoked, allowing you to pass arguments
|
||||
- **Auto-compact fallback**: Auto-compact threshold falls back to the global setting when not defined at model level
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue