v2.18.4 (#866)
Co-authored-by: Albert Jiang <aj@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Laure Hugo <201583486+laure0303@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Mert Unsal <mert.unsal@mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
4e495f658d
commit
ac8f1a09fd
83 changed files with 1979 additions and 572 deletions
|
|
@ -29,6 +29,7 @@ from vibe.core.teleport.types import (
|
|||
# agent manager re-derives config via model_dump(); the default url is unavoidable.
|
||||
SESSIONS_BASE_URL = "https://chat.mistral.ai"
|
||||
SESSIONS_URL = f"{SESSIONS_BASE_URL}{TELEPORT_SESSIONS_PATH}"
|
||||
GITHUB_REMOTE_URL = "https://github.com/owner/repo.git"
|
||||
|
||||
|
||||
def _sessions_ok() -> httpx.Response:
|
||||
|
|
@ -46,27 +47,28 @@ def _sessions_ok() -> httpx.Response:
|
|||
|
||||
def _commit(repo: Repo, message: str) -> str:
|
||||
(Path(repo.working_dir) / "file.txt").write_text(f"{message}\n")
|
||||
repo.index.add(["file.txt"])
|
||||
repo.index.commit(message)
|
||||
repo.git.add("file.txt")
|
||||
repo.git.commit("-m", message)
|
||||
return repo.head.commit.hexsha
|
||||
|
||||
|
||||
def _init_repo(workdir: Path) -> Repo:
|
||||
# origin is a local bare repo so fetch/push stay offline and instant; a
|
||||
# separate github-url remote satisfies Teleport's GitHub-only detection.
|
||||
# origin is intentionally not GitHub. The GitHub-looking `hub` remote is
|
||||
# rewritten to a local bare repo so fetch/push stay offline and instant.
|
||||
bare = Repo.init(workdir.with_name(f"{workdir.name}_origin.git"), bare=True)
|
||||
repo = Repo.init(workdir, initial_branch="work")
|
||||
repo.config_writer().set_value("user", "name", "Tester").release()
|
||||
repo.config_writer().set_value("user", "email", "t@example.com").release()
|
||||
repo.create_remote("origin", str(bare.git_dir))
|
||||
repo.create_remote("hub", "https://github.com/owner/repo.git")
|
||||
repo.git.config(f"url.{bare.git_dir}.insteadOf", GITHUB_REMOTE_URL)
|
||||
repo.create_remote("hub", GITHUB_REMOTE_URL)
|
||||
return repo
|
||||
|
||||
|
||||
def _repo_with_pushed_branch(workdir: Path) -> Repo:
|
||||
repo = _init_repo(workdir)
|
||||
_commit(repo, "initial")
|
||||
repo.git.push("origin", "work")
|
||||
repo.git.push("hub", "work")
|
||||
return repo
|
||||
|
||||
|
||||
|
|
@ -150,7 +152,7 @@ async def test_teleport_pushes_then_completes_when_approved(
|
|||
TeleportStartingWorkflowEvent,
|
||||
TeleportCompleteEvent,
|
||||
]
|
||||
assert repo.remote("origin").refs["work"].commit.hexsha == head
|
||||
assert repo.remote("hub").refs["work"].commit.hexsha == head
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -172,7 +174,7 @@ async def test_teleport_fails_when_push_fails(
|
|||
) -> None:
|
||||
repo = _repo_with_pushed_branch(tmp_working_directory)
|
||||
_commit(repo, "second")
|
||||
repo.git.remote("set-url", "--push", "origin", "/nonexistent/repo.git")
|
||||
repo.git.remote("set-url", "--push", "hub", "/nonexistent/repo.git")
|
||||
|
||||
with pytest.raises(TeleportError, match="Failed to push"):
|
||||
await _drain(build_e2e_agent_loop(), "ship it", approve=True)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,18 @@ def _get_auto_compact_properties(
|
|||
return cast(dict[str, object], auto_compact[0]["properties"])
|
||||
|
||||
|
||||
def _get_compaction_failed_properties(
|
||||
telemetry_events: list[dict[str, object]],
|
||||
) -> dict[str, object]:
|
||||
failed = [
|
||||
event
|
||||
for event in telemetry_events
|
||||
if event.get("event_name") == "vibe.compaction_failed"
|
||||
]
|
||||
assert len(failed) == 1
|
||||
return cast(dict[str, object], failed[0]["properties"])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_compact_emits_correct_events(telemetry_events: list[dict]) -> None:
|
||||
backend = FakeBackend([
|
||||
|
|
@ -306,7 +318,9 @@ async def test_compact_without_extra_instructions_has_no_additional_section() ->
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_compact_raises_on_tool_call_when_flag_enabled() -> None:
|
||||
async def test_compact_raises_on_tool_call_when_flag_enabled(
|
||||
telemetry_events: list[dict],
|
||||
) -> None:
|
||||
"""With the flag on, a compaction that returns a tool call raises."""
|
||||
backend = FakeBackend([
|
||||
[
|
||||
|
|
@ -333,10 +347,13 @@ async def test_compact_raises_on_tool_call_when_flag_enabled() -> None:
|
|||
with pytest.raises(CompactionFailedError) as exc_info:
|
||||
await agent.compact()
|
||||
assert exc_info.value.reason == "tool_call"
|
||||
assert _get_compaction_failed_properties(telemetry_events)["reason"] == "tool_call"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_compact_raises_on_empty_summary_when_flag_enabled() -> None:
|
||||
async def test_compact_raises_on_empty_summary_when_flag_enabled(
|
||||
telemetry_events: list[dict],
|
||||
) -> None:
|
||||
"""With the flag on, a compaction with empty content raises."""
|
||||
backend = FakeBackend([[mock_llm_chunk(content=" ")]])
|
||||
cfg = build_test_vibe_config(
|
||||
|
|
@ -350,11 +367,20 @@ async def test_compact_raises_on_empty_summary_when_flag_enabled() -> None:
|
|||
with pytest.raises(CompactionFailedError) as exc_info:
|
||||
await agent.compact()
|
||||
assert exc_info.value.reason == "empty_summary"
|
||||
assert (
|
||||
_get_compaction_failed_properties(telemetry_events)["reason"] == "empty_summary"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_compact_falls_back_when_flag_disabled() -> None:
|
||||
"""With the flag off (default), empty content uses the legacy fallback."""
|
||||
async def test_compact_falls_back_when_flag_disabled(
|
||||
telemetry_events: list[dict],
|
||||
) -> None:
|
||||
"""With the flag off (default), empty content uses the legacy fallback.
|
||||
|
||||
The compaction failure telemetry event must still be sent even though the
|
||||
flag is off and compact() returns normally.
|
||||
"""
|
||||
backend = FakeBackend([[mock_llm_chunk(content="")]])
|
||||
cfg = build_test_vibe_config(models=make_test_models(auto_compact_threshold=999))
|
||||
agent = build_test_agent_loop(config=cfg, backend=backend)
|
||||
|
|
@ -363,6 +389,9 @@ async def test_compact_falls_back_when_flag_disabled() -> None:
|
|||
|
||||
summary = await agent.compact()
|
||||
assert summary == "(no summary available)"
|
||||
assert (
|
||||
_get_compaction_failed_properties(telemetry_events)["reason"] == "empty_summary"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ from tests.conftest import (
|
|||
)
|
||||
from tests.mock.utils import mock_llm_chunk
|
||||
from tests.stubs.fake_backend import FakeBackend
|
||||
from vibe import __version__
|
||||
from vibe.core.agents.models import BuiltinAgentName
|
||||
from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig
|
||||
from vibe.core.telemetry.types import EntrypointMetadata
|
||||
from vibe.core.telemetry.types import LaunchContext, TerminalEmulator
|
||||
from vibe.core.tools.base import BaseToolConfig, ToolPermission
|
||||
from vibe.core.types import (
|
||||
Backend,
|
||||
|
|
@ -32,6 +33,7 @@ from vibe.core.types import (
|
|||
StopInfo,
|
||||
ToolCall,
|
||||
)
|
||||
from vibe.core.utils import get_platform_id, get_platform_version
|
||||
|
||||
|
||||
def _two_model_vibe_config(active_model: str) -> VibeConfig:
|
||||
|
|
@ -191,19 +193,20 @@ async def test_passes_parent_session_id_to_backend_after_reset(vibe_config: Vibe
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_passes_entrypoint_metadata_to_backend(vibe_config: VibeConfig):
|
||||
metadata = EntrypointMetadata(
|
||||
async def test_passes_launch_context_to_backend(vibe_config: VibeConfig):
|
||||
launch_context = LaunchContext(
|
||||
agent_entrypoint="acp",
|
||||
agent_version="2.0.0",
|
||||
client_name="vibe_ide",
|
||||
client_version="0.5.0",
|
||||
terminal_emulator=TerminalEmulator.GHOSTTY,
|
||||
)
|
||||
backend = FakeBackend([mock_llm_chunk(content="Response")])
|
||||
agent = build_test_agent_loop(
|
||||
config=vibe_config,
|
||||
backend=backend,
|
||||
enable_streaming=True,
|
||||
entrypoint_metadata=metadata,
|
||||
launch_context=launch_context,
|
||||
)
|
||||
|
||||
[_ async for _ in agent.act("Hello")]
|
||||
|
|
@ -215,6 +218,13 @@ async def test_passes_entrypoint_metadata_to_backend(vibe_config: VibeConfig):
|
|||
assert meta["agent_version"] == "2.0.0"
|
||||
assert meta["client_name"] == "vibe_ide"
|
||||
assert meta["client_version"] == "0.5.0"
|
||||
assert meta["os"] == get_platform_id()
|
||||
if os_version := get_platform_version():
|
||||
assert meta["os_version"] == os_version
|
||||
else:
|
||||
assert "os_version" not in meta
|
||||
assert meta["version"] == __version__
|
||||
assert meta["terminal_emulator"] == TerminalEmulator.GHOSTTY
|
||||
assert meta["session_id"] == agent.session_id
|
||||
assert "message_id" in meta
|
||||
assert meta["call_type"] == "main_call"
|
||||
|
|
@ -272,11 +282,12 @@ async def test_mcp_sampling_handler_uses_updated_config_when_agent_config_change
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_sampling_handler_sends_secondary_call_telemetry_metadata():
|
||||
metadata = EntrypointMetadata(
|
||||
launch_context = LaunchContext(
|
||||
agent_entrypoint="acp",
|
||||
agent_version="2.0.0",
|
||||
client_name="vibe_ide",
|
||||
client_version="0.5.0",
|
||||
terminal_emulator=TerminalEmulator.GHOSTTY,
|
||||
)
|
||||
backend = FakeBackend([
|
||||
[mock_llm_chunk(content="Response")],
|
||||
|
|
@ -285,7 +296,7 @@ async def test_mcp_sampling_handler_sends_secondary_call_telemetry_metadata():
|
|||
agent = build_test_agent_loop(
|
||||
config=_two_model_vibe_config("devstral-latest"),
|
||||
backend=backend,
|
||||
entrypoint_metadata=metadata,
|
||||
launch_context=launch_context,
|
||||
)
|
||||
|
||||
[_ async for _ in agent.act("Hello")]
|
||||
|
|
@ -302,6 +313,13 @@ async def test_mcp_sampling_handler_sends_secondary_call_telemetry_metadata():
|
|||
assert sampling_metadata["agent_version"] == "2.0.0"
|
||||
assert sampling_metadata["client_name"] == "vibe_ide"
|
||||
assert sampling_metadata["client_version"] == "0.5.0"
|
||||
assert sampling_metadata["os"] == get_platform_id()
|
||||
if os_version := get_platform_version():
|
||||
assert sampling_metadata["os_version"] == os_version
|
||||
else:
|
||||
assert "os_version" not in sampling_metadata
|
||||
assert sampling_metadata["version"] == __version__
|
||||
assert sampling_metadata["terminal_emulator"] == TerminalEmulator.GHOSTTY
|
||||
assert sampling_metadata["session_id"] == agent.session_id
|
||||
assert sampling_metadata["parent_session_id"] == "parent-session-456"
|
||||
assert sampling_metadata["message_id"] == next(
|
||||
|
|
|
|||
|
|
@ -344,6 +344,15 @@ class TestAgentProfileOverrides:
|
|||
overrides = BUILTIN_AGENTS[BuiltinAgentName.AUTO_APPROVE].overrides
|
||||
assert overrides.get("bypass_tool_permissions") is True
|
||||
|
||||
def test_lean_agent_keeps_tool_permissions_configurable(self) -> None:
|
||||
overrides = BUILTIN_AGENTS[BuiltinAgentName.LEAN].overrides
|
||||
assert "bypass_tool_permissions" not in overrides
|
||||
|
||||
def test_lean_agent_uses_latest_model(self) -> None:
|
||||
overrides = BUILTIN_AGENTS[BuiltinAgentName.LEAN].overrides
|
||||
models = overrides["models"]
|
||||
assert models[0]["name"] == "labs-leanstral-1-5"
|
||||
|
||||
def test_plan_agent_restricts_tools(self) -> None:
|
||||
overrides = BUILTIN_AGENTS[BuiltinAgentName.PLAN].overrides
|
||||
assert "tools" in overrides
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from tests.stubs.fake_mcp_registry import FakeMCPRegistry
|
|||
from vibe.core import agent_loop as agent_loop_module
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
from vibe.core.config import MCPStdio
|
||||
from vibe.core.telemetry.types import TerminalEmulator
|
||||
from vibe.core.telemetry.types import LaunchContext, TerminalEmulator
|
||||
from vibe.core.tools.manager import ToolManager
|
||||
from vibe.core.tools.mcp import AuthStatus
|
||||
from vibe.core.tools.remote import RemoteTool
|
||||
|
|
@ -371,7 +371,15 @@ class TestStartInitializeExperiments:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refreshes_system_prompt_when_experiments_update(self) -> None:
|
||||
loop = build_test_agent_loop(terminal_emulator=TerminalEmulator.VSCODE)
|
||||
loop = build_test_agent_loop(
|
||||
launch_context=LaunchContext(
|
||||
agent_entrypoint="cli",
|
||||
agent_version="1.0.0",
|
||||
client_name="vibe_cli",
|
||||
client_version="1.0.0",
|
||||
terminal_emulator=TerminalEmulator.VSCODE,
|
||||
)
|
||||
)
|
||||
refresh_mock = AsyncMock()
|
||||
init_mock = AsyncMock(return_value=True)
|
||||
|
||||
|
|
@ -387,21 +395,31 @@ class TestStartInitializeExperiments:
|
|||
init_mock.assert_awaited_once()
|
||||
init_args = init_mock.await_args
|
||||
assert init_args is not None
|
||||
assert init_args.kwargs["terminal_emulator"] is TerminalEmulator.VSCODE
|
||||
assert (
|
||||
init_args.kwargs["launch_context"].terminal_emulator
|
||||
is TerminalEmulator.VSCODE
|
||||
)
|
||||
|
||||
def test_new_session_telemetry_uses_provided_terminal_emulator(self) -> None:
|
||||
loop = build_test_agent_loop(terminal_emulator=TerminalEmulator.VSCODE)
|
||||
send_new_session = MagicMock()
|
||||
loop = build_test_agent_loop(
|
||||
launch_context=LaunchContext(
|
||||
agent_entrypoint="cli",
|
||||
agent_version="1.0.0",
|
||||
client_name="vibe_cli",
|
||||
client_version="1.0.0",
|
||||
terminal_emulator=TerminalEmulator.VSCODE,
|
||||
)
|
||||
)
|
||||
send_event = MagicMock()
|
||||
|
||||
with patch.object(
|
||||
loop.telemetry_client, "send_new_session", new=send_new_session
|
||||
loop.telemetry_client, "send_telemetry_event", new=send_event
|
||||
):
|
||||
loop.emit_new_session_telemetry()
|
||||
|
||||
assert (
|
||||
send_new_session.call_args.kwargs["terminal_emulator"]
|
||||
is TerminalEmulator.VSCODE
|
||||
)
|
||||
payload = send_event.call_args.args[1]
|
||||
assert payload["terminal_emulator"] == "vscode"
|
||||
assert type(payload["terminal_emulator"]) is str
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_does_not_refresh_system_prompt_when_experiments_unchanged(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue