From 5ea69b547b8eb5de5d0947543bd517e40c13dde8 Mon Sep 17 00:00:00 2001 From: Mathias Gesbert Date: Mon, 23 Mar 2026 21:59:24 +0100 Subject: [PATCH] v2.6.2 (#526) Co-authored-by: Mistral Vibe --- .vscode/launch.json | 2 +- CHANGELOG.md | 11 ++ distribution/zed/extension.toml | 14 +- pyproject.toml | 5 +- tests/acp/test_initialize.py | 4 +- tests/acp/test_load_session.py | 24 +-- tests/acp/test_new_session.py | 20 +-- tests/acp/test_set_config_option.py | 12 +- tests/acp/test_usage_update.py | 254 ---------------------------- tests/stubs/fake_client.py | 4 +- uv.lock | 10 +- vibe/__init__.py | 2 +- vibe/acp/acp_agent_loop.py | 67 +------- vibe/acp/utils.py | 37 ++-- 14 files changed, 88 insertions(+), 378 deletions(-) delete mode 100644 tests/acp/test_usage_update.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 22b37c7..a7a74a7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,5 +1,5 @@ { - "version": "2.6.1", + "version": "2.6.2", "configurations": [ { "name": "ACP Server", diff --git a/CHANGELOG.md b/CHANGELOG.md index d96d0da..e83ddf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ 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.6.2] - 2026-03-23 + +### Changed + +- Pinned agent-client-protocol dependency back to 0.8.1 + +### Removed + +- Context usage updates via ACP + + ## [2.6.1] - 2026-03-23 ### Changed diff --git a/distribution/zed/extension.toml b/distribution/zed/extension.toml index d669911..bf7df08 100644 --- a/distribution/zed/extension.toml +++ b/distribution/zed/extension.toml @@ -1,7 +1,7 @@ id = "mistral-vibe" name = "Mistral Vibe" description = "Mistral's open-source coding assistant" -version = "2.6.1" +version = "2.6.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.6.1/vibe-acp-darwin-aarch64-2.6.1.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.6.2/vibe-acp-darwin-aarch64-2.6.2.zip" cmd = "./vibe-acp" [agent_servers.mistral-vibe.targets.darwin-x86_64] -archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.6.1/vibe-acp-darwin-x86_64-2.6.1.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.6.2/vibe-acp-darwin-x86_64-2.6.2.zip" cmd = "./vibe-acp" [agent_servers.mistral-vibe.targets.linux-aarch64] -archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.6.1/vibe-acp-linux-aarch64-2.6.1.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.6.2/vibe-acp-linux-aarch64-2.6.2.zip" cmd = "./vibe-acp" [agent_servers.mistral-vibe.targets.linux-x86_64] -archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.6.1/vibe-acp-linux-x86_64-2.6.1.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.6.2/vibe-acp-linux-x86_64-2.6.2.zip" cmd = "./vibe-acp" [agent_servers.mistral-vibe.targets.windows-aarch64] -archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.6.1/vibe-acp-windows-aarch64-2.6.1.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.6.2/vibe-acp-windows-aarch64-2.6.2.zip" cmd = "./vibe-acp.exe" [agent_servers.mistral-vibe.targets.windows-x86_64] -archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.6.1/vibe-acp-windows-x86_64-2.6.1.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.6.2/vibe-acp-windows-x86_64-2.6.2.zip" cmd = "./vibe-acp.exe" diff --git a/pyproject.toml b/pyproject.toml index ddd0fbf..767bbf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mistral-vibe" -version = "2.6.1" +version = "2.6.2" description = "Minimal CLI coding agent by Mistral" readme = "README.md" requires-python = ">=3.12" @@ -27,7 +27,7 @@ classifiers = [ "Topic :: Utilities", ] dependencies = [ - "agent-client-protocol>=0.9.0a1", + "agent-client-protocol==0.8.1", "anyio>=4.12.0", "cachetools>=5.5.0", "cryptography>=44.0.0,<=46.0.3", @@ -113,7 +113,6 @@ build = ["pyinstaller>=6.17.0", "truststore>=0.10.4"] pythonVersion = "3.12" reportMissingTypeStubs = false reportPrivateImportUsage = false - include = ["vibe/**/*.py", "tests/**/*.py"] exclude = ["pyinstaller/"] venvPath = "." diff --git a/tests/acp/test_initialize.py b/tests/acp/test_initialize.py index 3d2db81..6f94a98 100644 --- a/tests/acp/test_initialize.py +++ b/tests/acp/test_initialize.py @@ -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.6.1" + name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.6.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.6.1" + name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.6.2" ) assert response.auth_methods is not None diff --git a/tests/acp/test_load_session.py b/tests/acp/test_load_session.py index 5f7b0f7..302b65c 100644 --- a/tests/acp/test_load_session.py +++ b/tests/acp/test_load_session.py @@ -100,11 +100,13 @@ class TestLoadSession: assert response.config_options is not None assert len(response.config_options) == 2 - assert response.config_options[0].id == "mode" - assert response.config_options[0].category == "mode" - assert response.config_options[0].current_value == BuiltinAgentName.DEFAULT - assert len(response.config_options[0].options) == 5 - mode_option_values = {opt.value for opt in response.config_options[0].options} + assert response.config_options[0].root.id == "mode" + assert response.config_options[0].root.category == "mode" + assert response.config_options[0].root.current_value == BuiltinAgentName.DEFAULT + assert len(response.config_options[0].root.options) == 5 + mode_option_values = { + opt.value for opt in response.config_options[0].root.options + } assert mode_option_values == { BuiltinAgentName.DEFAULT, BuiltinAgentName.CHAT, @@ -112,11 +114,13 @@ class TestLoadSession: BuiltinAgentName.PLAN, BuiltinAgentName.ACCEPT_EDITS, } - assert response.config_options[1].id == "model" - assert response.config_options[1].category == "model" - assert response.config_options[1].current_value == "devstral-latest" - assert len(response.config_options[1].options) == 2 - model_option_values = {opt.value for opt in response.config_options[1].options} + assert response.config_options[1].root.id == "model" + assert response.config_options[1].root.category == "model" + assert response.config_options[1].root.current_value == "devstral-latest" + assert len(response.config_options[1].root.options) == 2 + model_option_values = { + opt.value for opt in response.config_options[1].root.options + } assert model_option_values == {"devstral-latest", "devstral-small"} @pytest.mark.asyncio diff --git a/tests/acp/test_new_session.py b/tests/acp/test_new_session.py index c57f09e..52942d8 100644 --- a/tests/acp/test_new_session.py +++ b/tests/acp/test_new_session.py @@ -103,11 +103,11 @@ class TestACPNewSession: # Mode config option mode_config = session_response.config_options[0] - assert mode_config.id == "mode" - assert mode_config.category == "mode" - assert mode_config.current_value == BuiltinAgentName.DEFAULT - assert len(mode_config.options) == 5 - mode_option_values = {opt.value for opt in mode_config.options} + assert mode_config.root.id == "mode" + assert mode_config.root.category == "mode" + assert mode_config.root.current_value == BuiltinAgentName.DEFAULT + assert len(mode_config.root.options) == 5 + mode_option_values = {opt.value for opt in mode_config.root.options} assert mode_option_values == { BuiltinAgentName.DEFAULT, BuiltinAgentName.CHAT, @@ -118,11 +118,11 @@ class TestACPNewSession: # Model config option model_config = session_response.config_options[1] - assert model_config.id == "model" - assert model_config.category == "model" - assert model_config.current_value == "devstral-latest" - assert len(model_config.options) == 2 - model_option_values = {opt.value for opt in model_config.options} + assert model_config.root.id == "model" + assert model_config.root.category == "model" + assert model_config.root.current_value == "devstral-latest" + assert len(model_config.root.options) == 2 + model_option_values = {opt.value for opt in model_config.root.options} assert model_option_values == {"devstral-latest", "devstral-small"} @pytest.mark.skip(reason="TODO: Fix this test") diff --git a/tests/acp/test_set_config_option.py b/tests/acp/test_set_config_option.py index 5f3866f..92bb7b1 100644 --- a/tests/acp/test_set_config_option.py +++ b/tests/acp/test_set_config_option.py @@ -83,8 +83,8 @@ class TestACPSetConfigOptionMode: # Verify config_options reflect the new state mode_config = response.config_options[0] - assert mode_config.id == "mode" - assert mode_config.current_value == BuiltinAgentName.AUTO_APPROVE + assert mode_config.root.id == "mode" + assert mode_config.root.current_value == BuiltinAgentName.AUTO_APPROVE @pytest.mark.asyncio async def test_set_config_option_mode_to_plan( @@ -133,8 +133,8 @@ class TestACPSetConfigOptionMode: ) # Chat mode auto-approves read-only tools mode_config = response.config_options[0] - assert mode_config.id == "mode" - assert mode_config.current_value == BuiltinAgentName.CHAT + assert mode_config.root.id == "mode" + assert mode_config.root.current_value == BuiltinAgentName.CHAT @pytest.mark.asyncio async def test_set_config_option_mode_invalid_returns_none( @@ -205,8 +205,8 @@ class TestACPSetConfigOptionModel: # Verify config_options reflect the new state model_config = response.config_options[1] - assert model_config.id == "model" - assert model_config.current_value == "devstral-small" + assert model_config.root.id == "model" + assert model_config.root.current_value == "devstral-small" @pytest.mark.asyncio async def test_set_config_option_model_invalid_returns_none( diff --git a/tests/acp/test_usage_update.py b/tests/acp/test_usage_update.py deleted file mode 100644 index 856f5a5..0000000 --- a/tests/acp/test_usage_update.py +++ /dev/null @@ -1,254 +0,0 @@ -from __future__ import annotations - -import asyncio -import json -from pathlib import Path -from unittest.mock import patch - -from acp.schema import TextContentBlock, UsageUpdate -import pytest - -from tests.acp.conftest import _create_acp_agent -from tests.conftest import build_test_vibe_config -from tests.stubs.fake_backend import FakeBackend -from tests.stubs.fake_client import FakeClient -from vibe.acp.acp_agent_loop import VibeAcpAgentLoop -from vibe.core.agent_loop import AgentLoop -from vibe.core.config import SessionLoggingConfig -from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role - - -def _make_backend(prompt_tokens: int = 100, completion_tokens: int = 50) -> FakeBackend: - return FakeBackend( - LLMChunk( - message=LLMMessage(role=Role.assistant, content="Hi"), - usage=LLMUsage( - prompt_tokens=prompt_tokens, completion_tokens=completion_tokens - ), - ) - ) - - -def _make_acp_agent(backend: FakeBackend) -> VibeAcpAgentLoop: - config = build_test_vibe_config() - - class PatchedAgentLoop(AgentLoop): - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **{**kwargs, "backend": backend}) - self._base_config = config - self.agent_manager.invalidate_config() - - patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=PatchedAgentLoop).start() - return _create_acp_agent() - - -def _get_fake_client(agent: VibeAcpAgentLoop) -> FakeClient: - return agent.client # type: ignore[return-value] - - -def _get_usage_updates(client: FakeClient) -> list[UsageUpdate]: - return [ - update.update - for update in client._session_updates - if isinstance(update.update, UsageUpdate) - ] - - -class TestPromptResponseUsage: - @pytest.mark.asyncio - async def test_prompt_returns_usage_in_response(self) -> None: - agent = _make_acp_agent(_make_backend(prompt_tokens=100, completion_tokens=50)) - session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[]) - - response = await agent.prompt( - session_id=session.session_id, - prompt=[TextContentBlock(type="text", text="Hello")], - ) - - assert response.usage is not None - assert response.usage.input_tokens == 100 - assert response.usage.output_tokens == 50 - assert response.usage.total_tokens == 150 - - @pytest.mark.asyncio - async def test_prompt_usage_optional_fields_are_none(self) -> None: - agent = _make_acp_agent(_make_backend()) - session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[]) - - response = await agent.prompt( - session_id=session.session_id, - prompt=[TextContentBlock(type="text", text="Hello")], - ) - - assert response.usage is not None - assert response.usage.thought_tokens is None - assert response.usage.cached_read_tokens is None - assert response.usage.cached_write_tokens is None - - @pytest.mark.asyncio - async def test_prompt_usage_accumulates_across_turns(self) -> None: - backend = _make_backend(prompt_tokens=100, completion_tokens=50) - agent = _make_acp_agent(backend) - session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[]) - - first = await agent.prompt( - session_id=session.session_id, - prompt=[TextContentBlock(type="text", text="Hello")], - ) - - second = await agent.prompt( - session_id=session.session_id, - prompt=[TextContentBlock(type="text", text="Hello again")], - ) - - assert first.usage is not None - assert second.usage is not None - # Second turn should have strictly more cumulative tokens - assert second.usage.input_tokens > first.usage.input_tokens - assert second.usage.output_tokens > first.usage.output_tokens - assert second.usage.total_tokens > first.usage.total_tokens - - -class TestUsageUpdateNotification: - @pytest.mark.asyncio - async def test_prompt_sends_usage_update(self) -> None: - agent = _make_acp_agent(_make_backend()) - session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[]) - - await agent.prompt( - session_id=session.session_id, - prompt=[TextContentBlock(type="text", text="Hello")], - ) - await asyncio.sleep(0) - - usage_updates = _get_usage_updates(_get_fake_client(agent)) - assert len(usage_updates) == 1 - assert usage_updates[0].session_update == "usage_update" - - @pytest.mark.asyncio - async def test_usage_update_contains_context_window_info(self) -> None: - agent = _make_acp_agent(_make_backend(prompt_tokens=100, completion_tokens=50)) - session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[]) - - await agent.prompt( - session_id=session.session_id, - prompt=[TextContentBlock(type="text", text="Hello")], - ) - await asyncio.sleep(0) - - usage_updates = _get_usage_updates(_get_fake_client(agent)) - assert len(usage_updates) == 1 - assert usage_updates[0].size > 0 - assert usage_updates[0].used > 0 - - @pytest.mark.asyncio - async def test_usage_update_contains_cost_when_pricing_set(self) -> None: - agent = _make_acp_agent( - _make_backend(prompt_tokens=1_000_000, completion_tokens=500_000) - ) - session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[]) - - # Set pricing directly on the session stats (config loading uses fixture defaults) - acp_session = agent.sessions[session.session_id] - acp_session.agent_loop.stats.update_pricing(input_price=0.4, output_price=2.0) - - await agent.prompt( - session_id=session.session_id, - prompt=[TextContentBlock(type="text", text="Hello")], - ) - await asyncio.sleep(0) - - usage_updates = _get_usage_updates(_get_fake_client(agent)) - assert len(usage_updates) == 1 - cost = usage_updates[0].cost - assert cost is not None - assert cost.currency == "USD" - assert cost.amount > 0 - - @pytest.mark.asyncio - async def test_usage_update_no_cost_when_zero_pricing(self) -> None: - agent = _make_acp_agent(_make_backend()) - session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[]) - - await agent.prompt( - session_id=session.session_id, - prompt=[TextContentBlock(type="text", text="Hello")], - ) - await asyncio.sleep(0) - - usage_updates = _get_usage_updates(_get_fake_client(agent)) - assert len(usage_updates) == 1 - assert usage_updates[0].cost is None - - @pytest.mark.asyncio - async def test_usage_update_sent_per_prompt(self) -> None: - backend = _make_backend() - agent = _make_acp_agent(backend) - session = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[]) - - await agent.prompt( - session_id=session.session_id, - prompt=[TextContentBlock(type="text", text="Hello")], - ) - await asyncio.sleep(0) - await agent.prompt( - session_id=session.session_id, - prompt=[TextContentBlock(type="text", text="Hello again")], - ) - await asyncio.sleep(0) - - usage_updates = _get_usage_updates(_get_fake_client(agent)) - assert len(usage_updates) == 2 - - -class TestLoadSessionUsageUpdate: - def _make_session_dir(self, tmp_path: Path, session_id: str, cwd: str) -> Path: - session_folder = tmp_path / f"session_20240101_120000_{session_id[:8]}" - session_folder.mkdir() - messages_file = session_folder / "messages.jsonl" - with messages_file.open("w") as f: - f.write(json.dumps({"role": "user", "content": "Hello"}) + "\n") - meta = { - "session_id": session_id, - "start_time": "2024-01-01T12:00:00Z", - "end_time": "2024-01-01T12:05:00Z", - "environment": {"working_directory": cwd}, - } - with (session_folder / "meta.json").open("w") as f: - json.dump(meta, f) - return session_folder - - def _make_agent_with_session_logging( - self, backend: FakeBackend, session_dir: Path - ) -> VibeAcpAgentLoop: - session_config = SessionLoggingConfig( - save_dir=str(session_dir), session_prefix="session", enabled=True - ) - config = build_test_vibe_config(session_logging=session_config) - - class PatchedAgentLoop(AgentLoop): - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **{**kwargs, "backend": backend}) - self._base_config = config - self.agent_manager.invalidate_config() - - patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=PatchedAgentLoop).start() - agent = _create_acp_agent() - patch.object(agent, "_load_config", return_value=config).start() - return agent - - @pytest.mark.asyncio - async def test_load_session_sends_usage_update(self, tmp_path: Path) -> None: - backend = _make_backend() - agent = self._make_agent_with_session_logging(backend, tmp_path) - session_id = "test-session-load-usage" - self._make_session_dir(tmp_path, session_id, str(Path.cwd())) - - await agent.load_session(cwd=str(Path.cwd()), session_id=session_id) - await asyncio.sleep(0) - - client = _get_fake_client(agent) - usage_updates = _get_usage_updates(client) - assert len(usage_updates) == 1 - assert usage_updates[0].session_update == "usage_update" - assert usage_updates[0].size > 0 diff --git a/tests/stubs/fake_client.py b/tests/stubs/fake_client.py index 9660bb7..ef6294b 100644 --- a/tests/stubs/fake_client.py +++ b/tests/stubs/fake_client.py @@ -6,7 +6,7 @@ from acp import ( Agent as AcpAgent, Client, CreateTerminalResponse, - KillTerminalResponse, + KillTerminalCommandResponse, ReadTextFileResponse, ReleaseTerminalResponse, RequestPermissionResponse, @@ -112,7 +112,7 @@ class FakeClient(Client): async def kill_terminal( self, session_id: str, terminal_id: str, **kwargs: Any - ) -> KillTerminalResponse | None: + ) -> KillTerminalCommandResponse | None: raise NotImplementedError() async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]: diff --git a/uv.lock b/uv.lock index d84a435..ebd7f17 100644 --- a/uv.lock +++ b/uv.lock @@ -4,14 +4,14 @@ requires-python = ">=3.12" [[package]] name = "agent-client-protocol" -version = "0.9.0a1" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/dc/1ec56897b461fbdb844c9bff3abbbe225bfe8cda020dc2449101a6d76592/agent_client_protocol-0.9.0a1.tar.gz", hash = "sha256:9e6fc8b72df465279470920d679c871e0c658f69212e345563cc69b17906b606", size = 70423, upload-time = "2026-03-19T18:44:47.117Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7b/7cdac86db388809d9e3bc58cac88cc7dfa49b7615b98fab304a828cd7f8a/agent_client_protocol-0.8.1.tar.gz", hash = "sha256:1bbf15663bf51f64942597f638e32a6284c5da918055d9672d3510e965143dbd", size = 68866, upload-time = "2026-02-13T15:34:54.567Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/59/b794d5247aac2693a562ddd6eb2092581331496815967a6bca6bbf086b4f/agent_client_protocol-0.9.0a1-py3-none-any.whl", hash = "sha256:3e0962df15c3c7dd2957daea2f47db5e644fd897e77180e492f0a27d9fdb7bf4", size = 55945, upload-time = "2026-03-19T18:44:45.924Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f3/219eeca0ad4a20843d4b9eaac5532f87018b9d25730a62a16f54f6c52d1a/agent_client_protocol-0.8.1-py3-none-any.whl", hash = "sha256:9421a11fd435b4831660272d169c3812d553bb7247049c138c3ca127e4b8af8e", size = 54529, upload-time = "2026-02-13T15:34:53.344Z" }, ] [[package]] @@ -770,7 +770,7 @@ wheels = [ [[package]] name = "mistral-vibe" -version = "2.6.1" +version = "2.6.2" source = { editable = "." } dependencies = [ { name = "agent-client-protocol" }, @@ -832,7 +832,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "agent-client-protocol", specifier = ">=0.9.0a1" }, + { name = "agent-client-protocol", specifier = "==0.8.1" }, { name = "anyio", specifier = ">=4.12.0" }, { name = "cachetools", specifier = ">=5.5.0" }, { name = "cryptography", specifier = ">=44.0.0,<=46.0.3" }, diff --git a/vibe/__init__.py b/vibe/__init__.py index 6665f55..f68104e 100644 --- a/vibe/__init__.py +++ b/vibe/__init__.py @@ -3,4 +3,4 @@ from __future__ import annotations from pathlib import Path VIBE_ROOT = Path(__file__).parent -__version__ = "2.6.1" +__version__ = "2.6.2" diff --git a/vibe/acp/acp_agent_loop.py b/vibe/acp/acp_agent_loop.py index 74794ea..3aa156c 100644 --- a/vibe/acp/acp_agent_loop.py +++ b/vibe/acp/acp_agent_loop.py @@ -26,14 +26,11 @@ from acp.schema import ( AgentThoughtChunk, AllowedOutcome, AuthenticateResponse, - AuthMethodAgent, + AuthMethod, AvailableCommand, AvailableCommandInput, ClientCapabilities, - CloseSessionResponse, ContentToolCallContent, - Cost, - EnvVarAuthMethod, ForkSessionResponse, HttpMcpServer, Implementation, @@ -46,14 +43,11 @@ from acp.schema import ( SessionListCapabilities, SetSessionConfigOptionResponse, SseMcpServer, - TerminalAuthMethod, TextContentBlock, TextResourceContents, ToolCallProgress, ToolCallUpdate, UnstructuredCommandInput, - Usage, - UsageUpdate, UserMessageChunk, ) from pydantic import BaseModel, ConfigDict @@ -180,10 +174,9 @@ class VibeAcpAgentLoop(AcpAgent): and self.client_capabilities.field_meta.get("terminal-auth") is True ) - auth_methods: list[EnvVarAuthMethod | TerminalAuthMethod | AuthMethodAgent] = ( + auth_methods = ( [ - TerminalAuthMethod( - type="terminal", + AuthMethod( id="vibe-setup", name="Register your API Key", description="Register your API Key inside Mistral Vibe", @@ -387,39 +380,6 @@ 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 @@ -604,7 +564,6 @@ 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()), @@ -731,11 +690,7 @@ class VibeAcpAgentLoop(AcpAgent): @override async def prompt( - self, - prompt: list[ContentBlock], - session_id: str, - message_id: str | None = None, - **kwargs: Any, + self, prompt: list[ContentBlock], session_id: str, **kwargs: Any ) -> PromptResponse: session = self._get_session(session_id) @@ -768,10 +723,7 @@ class VibeAcpAgentLoop(AcpAgent): await session.task except asyncio.CancelledError: - self._send_usage_update(session) - return PromptResponse( - stop_reason="cancelled", usage=self._build_usage(session) - ) + return PromptResponse(stop_reason="cancelled") except CoreRateLimitError as e: raise RateLimitError.from_core(e) from e @@ -785,8 +737,7 @@ class VibeAcpAgentLoop(AcpAgent): finally: session.task = None - self._send_usage_update(session) - return PromptResponse(stop_reason="end_turn", usage=self._build_usage(session)) + return PromptResponse(stop_reason="end_turn") def _build_text_prompt(self, acp_prompt: list[ContentBlock]) -> str: text_prompt = "" @@ -908,12 +859,6 @@ 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) diff --git a/vibe/acp/utils.py b/vibe/acp/utils.py index a8527d3..cdf34a8 100644 --- a/vibe/acp/utils.py +++ b/vibe/acp/utils.py @@ -9,6 +9,7 @@ from acp.schema import ( ContentToolCallContent, ModelInfo, PermissionOption, + SessionConfigOption, SessionConfigOptionSelect, SessionConfigSelectOption, SessionMode, @@ -102,7 +103,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, SessionConfigOptionSelect]: +) -> tuple[SessionModeState, SessionConfigOption]: session_modes: list[SessionMode] = [] config_options: list[SessionConfigSelectOption] = [] @@ -127,20 +128,22 @@ def make_mode_response( state = SessionModeState( current_mode_id=current_mode_id, available_modes=session_modes ) - config = SessionConfigOptionSelect( - id="mode", - name="Session Mode", - current_value=current_mode_id, - category="mode", - type="select", - options=config_options, + config = SessionConfigOption( + root=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, SessionConfigOptionSelect]: +) -> tuple[SessionModelState, SessionConfigOption]: model_infos: list[ModelInfo] = [] config_options: list[SessionConfigSelectOption] = [] @@ -155,13 +158,15 @@ def make_model_response( state = SessionModelState( current_model_id=current_model_id, available_models=model_infos ) - config_option = SessionConfigOptionSelect( - id="model", - name="Model", - current_value=current_model_id, - category="model", - type="select", - options=config_options, + config_option = SessionConfigOption( + root=SessionConfigOptionSelect( + id="model", + name="Model", + current_value=current_model_id, + category="model", + type="select", + options=config_options, + ) ) return state, config_option