v2.2.0 (#395)
Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai> Co-authored-by: Clément Siriex <clement.sirieix@mistral.ai> Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai> Co-authored-by: Michel Thomazo <michel.thomazo@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
This commit is contained in:
parent
51fecc67d9
commit
ec7f3b25ea
107 changed files with 8002 additions and 535 deletions
|
|
@ -20,7 +20,7 @@ from acp import (
|
|||
SetSessionModeResponse,
|
||||
run_agent,
|
||||
)
|
||||
from acp.helpers import ContentBlock, SessionUpdate
|
||||
from acp.helpers import ContentBlock, SessionUpdate, update_available_commands
|
||||
from acp.schema import (
|
||||
AgentCapabilities,
|
||||
AgentMessageChunk,
|
||||
|
|
@ -28,6 +28,8 @@ from acp.schema import (
|
|||
AllowedOutcome,
|
||||
AuthenticateResponse,
|
||||
AuthMethod,
|
||||
AvailableCommand,
|
||||
AvailableCommandInput,
|
||||
ClientCapabilities,
|
||||
ContentToolCallContent,
|
||||
ForkSessionResponse,
|
||||
|
|
@ -38,6 +40,9 @@ from acp.schema import (
|
|||
ModelInfo,
|
||||
PromptCapabilities,
|
||||
ResumeSessionResponse,
|
||||
SessionCapabilities,
|
||||
SessionInfo,
|
||||
SessionListCapabilities,
|
||||
SessionModelState,
|
||||
SessionModeState,
|
||||
SseMcpServer,
|
||||
|
|
@ -45,6 +50,7 @@ from acp.schema import (
|
|||
TextResourceContents,
|
||||
ToolCallProgress,
|
||||
ToolCallUpdate,
|
||||
UnstructuredCommandInput,
|
||||
UserMessageChunk,
|
||||
)
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
|
@ -58,15 +64,33 @@ from vibe.acp.tools.session_update import (
|
|||
from vibe.acp.utils import (
|
||||
TOOL_OPTIONS,
|
||||
ToolOption,
|
||||
create_assistant_message_replay,
|
||||
create_compact_end_session_update,
|
||||
create_compact_start_session_update,
|
||||
create_reasoning_replay,
|
||||
create_tool_call_replay,
|
||||
create_tool_result_replay,
|
||||
create_user_message_replay,
|
||||
get_all_acp_session_modes,
|
||||
get_proxy_help_text,
|
||||
is_valid_acp_agent,
|
||||
)
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
from vibe.core.agents.models import BuiltinAgentName
|
||||
from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt
|
||||
from vibe.core.config import MissingAPIKeyError, VibeConfig, load_dotenv_values
|
||||
from vibe.core.config import (
|
||||
MissingAPIKeyError,
|
||||
SessionLoggingConfig,
|
||||
VibeConfig,
|
||||
load_dotenv_values,
|
||||
)
|
||||
from vibe.core.proxy_setup import (
|
||||
ProxySetupError,
|
||||
parse_proxy_command,
|
||||
set_proxy_var,
|
||||
unset_proxy_var,
|
||||
)
|
||||
from vibe.core.session.session_loader import SessionLoader
|
||||
from vibe.core.tools.base import BaseToolConfig, ToolPermission
|
||||
from vibe.core.types import (
|
||||
ApprovalResponse,
|
||||
|
|
@ -74,7 +98,9 @@ from vibe.core.types import (
|
|||
AsyncApprovalCallback,
|
||||
CompactEndEvent,
|
||||
CompactStartEvent,
|
||||
LLMMessage,
|
||||
ReasoningEvent,
|
||||
Role,
|
||||
ToolCallEvent,
|
||||
ToolResultEvent,
|
||||
ToolStreamEvent,
|
||||
|
|
@ -150,10 +176,13 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
|
||||
response = InitializeResponse(
|
||||
agent_capabilities=AgentCapabilities(
|
||||
load_session=False,
|
||||
load_session=True,
|
||||
prompt_capabilities=PromptCapabilities(
|
||||
audio=False, embedded_context=True, image=False
|
||||
),
|
||||
session_capabilities=SessionCapabilities(
|
||||
list=SessionListCapabilities()
|
||||
),
|
||||
),
|
||||
protocol_version=PROTOCOL_VERSION,
|
||||
agent_info=Implementation(
|
||||
|
|
@ -171,6 +200,44 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
) -> AuthenticateResponse | None:
|
||||
raise NotImplementedError("Not implemented yet")
|
||||
|
||||
def _load_config(self) -> VibeConfig:
|
||||
try:
|
||||
config = VibeConfig.load(disabled_tools=["ask_user_question"])
|
||||
config.tool_paths.extend(self._get_acp_tool_overrides())
|
||||
return config
|
||||
except MissingAPIKeyError as e:
|
||||
raise RequestError.auth_required({
|
||||
"message": "You must be authenticated before creating a session"
|
||||
}) from e
|
||||
|
||||
async def _create_acp_session(
|
||||
self, session_id: str, agent_loop: AgentLoop
|
||||
) -> AcpSessionLoop:
|
||||
session = AcpSessionLoop(id=session_id, agent_loop=agent_loop)
|
||||
self.sessions[session.id] = session
|
||||
|
||||
if not agent_loop.auto_approve:
|
||||
agent_loop.set_approval_callback(self._create_approval_callback(session.id))
|
||||
|
||||
asyncio.create_task(self._send_available_commands(session.id))
|
||||
|
||||
return session
|
||||
|
||||
def _build_session_model_state(self, agent_loop: AgentLoop) -> SessionModelState:
|
||||
return SessionModelState(
|
||||
current_model_id=agent_loop.config.active_model,
|
||||
available_models=[
|
||||
ModelInfo(model_id=model.alias, name=model.alias)
|
||||
for model in agent_loop.config.models
|
||||
],
|
||||
)
|
||||
|
||||
def _build_session_mode_state(self, session: AcpSessionLoop) -> SessionModeState:
|
||||
return SessionModeState(
|
||||
current_mode_id=session.agent_loop.agent_profile.name,
|
||||
available_modes=get_all_acp_session_modes(session.agent_loop.agent_manager),
|
||||
)
|
||||
|
||||
@override
|
||||
async def new_session(
|
||||
self,
|
||||
|
|
@ -181,13 +248,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
load_dotenv_values()
|
||||
os.chdir(cwd)
|
||||
|
||||
try:
|
||||
config = VibeConfig.load(disabled_tools=["ask_user_question"])
|
||||
config.tool_paths.extend(self._get_acp_tool_overrides())
|
||||
except MissingAPIKeyError as e:
|
||||
raise RequestError.auth_required({
|
||||
"message": "You must be authenticated before creating a new session"
|
||||
}) from e
|
||||
config = self._load_config()
|
||||
|
||||
agent_loop = AgentLoop(
|
||||
config=config, agent_name=BuiltinAgentName.DEFAULT, enable_streaming=True
|
||||
|
|
@ -196,29 +257,14 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
# We should just use agent_loop.session_id everywhere, but it can still change during
|
||||
# session lifetime (e.g. agent_loop.compact is called).
|
||||
# We should refactor agent_loop.session_id to make it immutable in ACP context.
|
||||
session = AcpSessionLoop(id=agent_loop.session_id, agent_loop=agent_loop)
|
||||
self.sessions[session.id] = session
|
||||
session = await self._create_acp_session(agent_loop.session_id, agent_loop)
|
||||
agent_loop.emit_new_session_telemetry("acp")
|
||||
|
||||
if not agent_loop.auto_approve:
|
||||
agent_loop.set_approval_callback(
|
||||
self._create_approval_callback(agent_loop.session_id)
|
||||
)
|
||||
|
||||
response = NewSessionResponse(
|
||||
session_id=agent_loop.session_id,
|
||||
models=SessionModelState(
|
||||
current_model_id=agent_loop.config.active_model,
|
||||
available_models=[
|
||||
ModelInfo(model_id=model.alias, name=model.alias)
|
||||
for model in agent_loop.config.models
|
||||
],
|
||||
),
|
||||
modes=SessionModeState(
|
||||
current_mode_id=session.agent_loop.agent_profile.name,
|
||||
available_modes=get_all_acp_session_modes(agent_loop.agent_manager),
|
||||
),
|
||||
return NewSessionResponse(
|
||||
session_id=session.id,
|
||||
models=self._build_session_model_state(agent_loop),
|
||||
modes=self._build_session_mode_state(session),
|
||||
)
|
||||
return response
|
||||
|
||||
def _get_acp_tool_overrides(self) -> list[Path]:
|
||||
overrides = ["todo"]
|
||||
|
|
@ -293,6 +339,85 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
raise RequestError.invalid_params({"session": "Not found"})
|
||||
return self.sessions[session_id]
|
||||
|
||||
async def _replay_tool_calls(self, session_id: str, msg: LLMMessage) -> None:
|
||||
if not msg.tool_calls:
|
||||
return
|
||||
for tool_call in msg.tool_calls:
|
||||
if tool_call.id and tool_call.function.name:
|
||||
update = create_tool_call_replay(
|
||||
tool_call.id, tool_call.function.name, tool_call.function.arguments
|
||||
)
|
||||
await self.client.session_update(session_id=session_id, update=update)
|
||||
|
||||
async def _replay_conversation_history(
|
||||
self, session_id: str, messages: list[LLMMessage]
|
||||
) -> None:
|
||||
for msg in messages:
|
||||
if msg.role == Role.user:
|
||||
update = create_user_message_replay(msg)
|
||||
await self.client.session_update(session_id=session_id, update=update)
|
||||
|
||||
elif msg.role == Role.assistant:
|
||||
if text_update := create_assistant_message_replay(msg):
|
||||
await self.client.session_update(
|
||||
session_id=session_id, update=text_update
|
||||
)
|
||||
if reasoning_update := create_reasoning_replay(msg):
|
||||
await self.client.session_update(
|
||||
session_id=session_id, update=reasoning_update
|
||||
)
|
||||
await self._replay_tool_calls(session_id, msg)
|
||||
|
||||
elif msg.role == Role.tool:
|
||||
if result_update := create_tool_result_replay(msg):
|
||||
await self.client.session_update(
|
||||
session_id=session_id, update=result_update
|
||||
)
|
||||
|
||||
async def _send_available_commands(self, session_id: str) -> None:
|
||||
commands = [
|
||||
AvailableCommand(
|
||||
name="proxy-setup",
|
||||
description="Configure proxy and SSL certificate settings",
|
||||
input=AvailableCommandInput(
|
||||
root=UnstructuredCommandInput(
|
||||
hint="KEY value to set, KEY to unset, or empty for help"
|
||||
)
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
update = update_available_commands(commands)
|
||||
await self.client.session_update(session_id=session_id, update=update)
|
||||
|
||||
async def _handle_proxy_setup_command(
|
||||
self, session_id: str, text_prompt: str
|
||||
) -> PromptResponse:
|
||||
args = text_prompt.strip()[len("/proxy-setup") :].strip()
|
||||
|
||||
try:
|
||||
if not args:
|
||||
message = get_proxy_help_text()
|
||||
else:
|
||||
key, value = parse_proxy_command(args)
|
||||
if value is not None:
|
||||
set_proxy_var(key, value)
|
||||
message = f"Set `{key}={value}` in ~/.vibe/.env\n\nPlease start a new chat for changes to take effect."
|
||||
else:
|
||||
unset_proxy_var(key)
|
||||
message = f"Removed `{key}` from ~/.vibe/.env\n\nPlease start a new chat for changes to take effect."
|
||||
except ProxySetupError as e:
|
||||
message = f"Error: {e}"
|
||||
|
||||
await self.client.session_update(
|
||||
session_id=session_id,
|
||||
update=AgentMessageChunk(
|
||||
session_update="agent_message_chunk",
|
||||
content=TextContentBlock(type="text", text=message),
|
||||
),
|
||||
)
|
||||
return PromptResponse(stop_reason="end_turn")
|
||||
|
||||
@override
|
||||
async def load_session(
|
||||
self,
|
||||
|
|
@ -301,7 +426,44 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> LoadSessionResponse | None:
|
||||
raise NotImplementedError()
|
||||
load_dotenv_values()
|
||||
os.chdir(cwd)
|
||||
|
||||
config = self._load_config()
|
||||
|
||||
session_dir = SessionLoader.find_session_by_id(
|
||||
session_id, config.session_logging
|
||||
)
|
||||
if session_dir is None:
|
||||
raise RequestError.invalid_params({
|
||||
"session_id": f"Session not found: {session_id}"
|
||||
})
|
||||
|
||||
try:
|
||||
loaded_messages, _ = SessionLoader.load_session(session_dir)
|
||||
except ValueError as e:
|
||||
raise RequestError.invalid_params({
|
||||
"session_id": f"Failed to load session: {e}"
|
||||
}) from e
|
||||
|
||||
agent_loop = AgentLoop(
|
||||
config=config, agent_name=BuiltinAgentName.DEFAULT, enable_streaming=True
|
||||
)
|
||||
|
||||
non_system_messages = [
|
||||
msg for msg in loaded_messages if msg.role != Role.system
|
||||
]
|
||||
|
||||
agent_loop.messages.extend(non_system_messages)
|
||||
|
||||
session = await self._create_acp_session(session_id, agent_loop)
|
||||
|
||||
await self._replay_conversation_history(session_id, non_system_messages)
|
||||
|
||||
return LoadSessionResponse(
|
||||
models=self._build_session_model_state(agent_loop),
|
||||
modes=self._build_session_mode_state(session),
|
||||
)
|
||||
|
||||
@override
|
||||
async def set_session_mode(
|
||||
|
|
@ -348,7 +510,27 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
async def list_sessions(
|
||||
self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any
|
||||
) -> ListSessionsResponse:
|
||||
raise NotImplementedError()
|
||||
try:
|
||||
config = VibeConfig.load()
|
||||
session_logging_config = config.session_logging
|
||||
except MissingAPIKeyError:
|
||||
session_logging_config = SessionLoggingConfig()
|
||||
|
||||
session_data = SessionLoader.list_sessions(session_logging_config, cwd=cwd)
|
||||
|
||||
sessions = [
|
||||
SessionInfo(
|
||||
session_id=s["session_id"],
|
||||
cwd=s["cwd"],
|
||||
title=s.get("title"),
|
||||
updated_at=s.get("end_time"),
|
||||
)
|
||||
for s in sorted(
|
||||
session_data, key=lambda s: s.get("end_time") or "", reverse=True
|
||||
)
|
||||
]
|
||||
|
||||
return ListSessionsResponse(sessions=sessions)
|
||||
|
||||
@override
|
||||
async def prompt(
|
||||
|
|
@ -363,6 +545,9 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
|
||||
text_prompt = self._build_text_prompt(prompt)
|
||||
|
||||
if text_prompt.strip().lower().startswith("/proxy-setup"):
|
||||
return await self._handle_proxy_setup_command(session_id, text_prompt)
|
||||
|
||||
temp_user_message_id: str | None = kwargs.get("messageId")
|
||||
|
||||
async def agent_loop_task() -> None:
|
||||
|
|
|
|||
|
|
@ -4,16 +4,20 @@ from enum import StrEnum
|
|||
from typing import TYPE_CHECKING, Literal, cast
|
||||
|
||||
from acp.schema import (
|
||||
AgentMessageChunk,
|
||||
AgentThoughtChunk,
|
||||
ContentToolCallContent,
|
||||
PermissionOption,
|
||||
SessionMode,
|
||||
TextContentBlock,
|
||||
ToolCallProgress,
|
||||
ToolCallStart,
|
||||
UserMessageChunk,
|
||||
)
|
||||
|
||||
from vibe.core.agents.models import AgentProfile, AgentType
|
||||
from vibe.core.types import CompactEndEvent, CompactStartEvent
|
||||
from vibe.core.proxy_setup import SUPPORTED_PROXY_VARS, get_current_proxy_settings
|
||||
from vibe.core.types import CompactEndEvent, CompactStartEvent, LLMMessage
|
||||
from vibe.core.utils import compact_reduction_display
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -111,3 +115,99 @@ def create_compact_end_session_update(event: CompactEndEvent) -> ToolCallProgres
|
|||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_proxy_help_text() -> str:
|
||||
lines = [
|
||||
"## Proxy Configuration",
|
||||
"",
|
||||
"Configure proxy and SSL settings for HTTP requests.",
|
||||
"",
|
||||
"### Usage:",
|
||||
"- `/proxy-setup` - Show this help and current settings",
|
||||
"- `/proxy-setup KEY value` - Set an environment variable",
|
||||
"- `/proxy-setup KEY` - Remove an environment variable",
|
||||
"",
|
||||
"### Supported Variables:",
|
||||
]
|
||||
|
||||
for key, description in SUPPORTED_PROXY_VARS.items():
|
||||
lines.append(f"- `{key}`: {description}")
|
||||
|
||||
lines.extend(["", "### Current Settings:"])
|
||||
|
||||
current = get_current_proxy_settings()
|
||||
any_set = False
|
||||
for key, value in current.items():
|
||||
if value:
|
||||
lines.append(f"- `{key}={value}`")
|
||||
any_set = True
|
||||
|
||||
if not any_set:
|
||||
lines.append("- (none configured)")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def create_user_message_replay(msg: LLMMessage) -> UserMessageChunk:
|
||||
content = msg.content if isinstance(msg.content, str) else ""
|
||||
return UserMessageChunk(
|
||||
session_update="user_message_chunk",
|
||||
content=TextContentBlock(type="text", text=content),
|
||||
field_meta={"messageId": msg.message_id} if msg.message_id else {},
|
||||
)
|
||||
|
||||
|
||||
def create_assistant_message_replay(msg: LLMMessage) -> AgentMessageChunk | None:
|
||||
content = msg.content if isinstance(msg.content, str) else ""
|
||||
if not content:
|
||||
return 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 {},
|
||||
)
|
||||
|
||||
|
||||
def create_reasoning_replay(msg: LLMMessage) -> AgentThoughtChunk | None:
|
||||
if not isinstance(msg.reasoning_content, str) or not msg.reasoning_content:
|
||||
return 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 {},
|
||||
)
|
||||
|
||||
|
||||
def create_tool_call_replay(
|
||||
tool_call_id: str, tool_name: str, arguments: str | None
|
||||
) -> ToolCallStart:
|
||||
return ToolCallStart(
|
||||
session_update="tool_call",
|
||||
title=tool_name,
|
||||
tool_call_id=tool_call_id,
|
||||
kind="other",
|
||||
raw_input=arguments,
|
||||
)
|
||||
|
||||
|
||||
def create_tool_result_replay(msg: LLMMessage) -> ToolCallProgress | None:
|
||||
if not msg.tool_call_id:
|
||||
return None
|
||||
|
||||
content = msg.content if isinstance(msg.content, str) else ""
|
||||
return ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=msg.tool_call_id,
|
||||
status="completed",
|
||||
raw_output=content,
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content", content=TextContentBlock(type="text", text=content)
|
||||
)
|
||||
]
|
||||
if content
|
||||
else None,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue