2.0.0
Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai> Co-Authored-By: Michel Thomazo <michel.thomazo@mistral.ai> Co-Authored-By: Clément Drouin <clement.drouin@mistral.ai> Co-Authored-By: Vincent Guilloux <vincent.guilloux@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: Thaddee Tyl <thaddee.tyl@gmail.com> Co-Authored-By: David Brochart <david.brochart@gmail.com> Co-Authored-By: Joseph Guhlin <joseph.guhlin@gmail.com> Co-Authored-By: Thomas Kenbeek <thomaskenbeek@gmail.com> Co-Authored-By: Remenby31 <baptiste.cruvellier31@gmail.com>
This commit is contained in:
parent
79f215d91c
commit
d33db9fff8
217 changed files with 16911 additions and 4305 deletions
|
|
@ -2,31 +2,23 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import cast, override
|
||||
from typing import Any, cast, override
|
||||
|
||||
from acp import (
|
||||
PROTOCOL_VERSION,
|
||||
Agent as AcpAgent,
|
||||
AgentSideConnection,
|
||||
AuthenticateRequest,
|
||||
CancelNotification,
|
||||
InitializeRequest,
|
||||
Client,
|
||||
InitializeResponse,
|
||||
LoadSessionRequest,
|
||||
NewSessionRequest,
|
||||
LoadSessionResponse,
|
||||
NewSessionResponse,
|
||||
PromptRequest,
|
||||
PromptResponse,
|
||||
RequestError,
|
||||
RequestPermissionRequest,
|
||||
SessionNotification,
|
||||
SetSessionModelRequest,
|
||||
SetSessionModelResponse,
|
||||
SetSessionModeRequest,
|
||||
SetSessionModeResponse,
|
||||
stdio_streams,
|
||||
run_agent,
|
||||
)
|
||||
from acp.helpers import ContentBlock, SessionUpdate
|
||||
from acp.schema import (
|
||||
|
|
@ -35,14 +27,24 @@ from acp.schema import (
|
|||
AllowedOutcome,
|
||||
AuthenticateResponse,
|
||||
AuthMethod,
|
||||
ClientCapabilities,
|
||||
ContentToolCallContent,
|
||||
ForkSessionResponse,
|
||||
HttpMcpServer,
|
||||
Implementation,
|
||||
ListSessionsResponse,
|
||||
McpServerStdio,
|
||||
ModelInfo,
|
||||
PromptCapabilities,
|
||||
ResumeSessionResponse,
|
||||
SessionModelState,
|
||||
SessionModeState,
|
||||
SseMcpServer,
|
||||
TextContentBlock,
|
||||
TextResourceContents,
|
||||
ToolCall,
|
||||
ToolCallProgress,
|
||||
ToolCallUpdate,
|
||||
UserMessageChunk,
|
||||
)
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
|
@ -55,41 +57,53 @@ from vibe.acp.tools.session_update import (
|
|||
from vibe.acp.utils import (
|
||||
TOOL_OPTIONS,
|
||||
ToolOption,
|
||||
acp_to_agent_mode,
|
||||
create_compact_end_session_update,
|
||||
create_compact_start_session_update,
|
||||
get_all_acp_session_modes,
|
||||
is_valid_acp_mode,
|
||||
is_valid_acp_agent,
|
||||
)
|
||||
from vibe.core.agent import Agent as VibeAgent
|
||||
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_api_keys_from_env
|
||||
from vibe.core.modes import AgentMode
|
||||
from vibe.core.tools.base import BaseToolConfig, ToolPermission
|
||||
from vibe.core.types import (
|
||||
ApprovalResponse,
|
||||
AssistantEvent,
|
||||
AsyncApprovalCallback,
|
||||
CompactEndEvent,
|
||||
CompactStartEvent,
|
||||
ToolCallEvent,
|
||||
ToolResultEvent,
|
||||
ToolStreamEvent,
|
||||
UserMessageEvent,
|
||||
)
|
||||
from vibe.core.utils import CancellationReason, get_user_cancellation_message
|
||||
|
||||
|
||||
class AcpSession(BaseModel):
|
||||
class AcpSessionLoop(BaseModel):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
id: str
|
||||
agent: VibeAgent
|
||||
agent_loop: AgentLoop
|
||||
task: asyncio.Task[None] | None = None
|
||||
|
||||
|
||||
class VibeAcpAgent(AcpAgent):
|
||||
def __init__(self, connection: AgentSideConnection) -> None:
|
||||
self.sessions: dict[str, AcpSession] = {}
|
||||
self.connection = connection
|
||||
class VibeAcpAgentLoop(AcpAgent):
|
||||
client: Client
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.sessions: dict[str, AcpSessionLoop] = {}
|
||||
self.client_capabilities = None
|
||||
|
||||
@override
|
||||
async def initialize(self, params: InitializeRequest) -> InitializeResponse:
|
||||
self.client_capabilities = params.clientCapabilities
|
||||
async def initialize(
|
||||
self,
|
||||
protocol_version: int,
|
||||
client_capabilities: ClientCapabilities | None = None,
|
||||
client_info: Implementation | None = None,
|
||||
**kwargs: Any,
|
||||
) -> InitializeResponse:
|
||||
self.client_capabilities = client_capabilities
|
||||
|
||||
# The ACP Agent process can be launched in 3 different ways, depending on installation
|
||||
# - dev mode: `uv run vibe-acp`, ran from the project root
|
||||
|
|
@ -133,91 +147,94 @@ class VibeAcpAgent(AcpAgent):
|
|||
)
|
||||
|
||||
response = InitializeResponse(
|
||||
agentCapabilities=AgentCapabilities(
|
||||
loadSession=False,
|
||||
promptCapabilities=PromptCapabilities(
|
||||
audio=False, embeddedContext=True, image=False
|
||||
agent_capabilities=AgentCapabilities(
|
||||
load_session=False,
|
||||
prompt_capabilities=PromptCapabilities(
|
||||
audio=False, embedded_context=True, image=False
|
||||
),
|
||||
),
|
||||
protocolVersion=PROTOCOL_VERSION,
|
||||
agentInfo=Implementation(
|
||||
protocol_version=PROTOCOL_VERSION,
|
||||
agent_info=Implementation(
|
||||
name="@mistralai/mistral-vibe",
|
||||
title="Mistral Vibe",
|
||||
version=__version__,
|
||||
),
|
||||
authMethods=auth_methods,
|
||||
auth_methods=auth_methods,
|
||||
)
|
||||
return response
|
||||
|
||||
@override
|
||||
async def authenticate(
|
||||
self, params: AuthenticateRequest
|
||||
self, method_id: str, **kwargs: Any
|
||||
) -> AuthenticateResponse | None:
|
||||
raise NotImplementedError("Not implemented yet")
|
||||
|
||||
@override
|
||||
async def newSession(self, params: NewSessionRequest) -> NewSessionResponse:
|
||||
capability_disabled_tools = self._get_disabled_tools_from_capabilities()
|
||||
async def new_session(
|
||||
self,
|
||||
cwd: str,
|
||||
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio],
|
||||
**kwargs: Any,
|
||||
) -> NewSessionResponse:
|
||||
load_api_keys_from_env()
|
||||
os.chdir(cwd)
|
||||
|
||||
cwd = Path(params.cwd)
|
||||
try:
|
||||
config = VibeConfig.load(
|
||||
workdir=cwd,
|
||||
tool_paths=[str(VIBE_ROOT / "acp" / "tools" / "builtins")],
|
||||
disabled_tools=capability_disabled_tools,
|
||||
)
|
||||
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
|
||||
|
||||
agent = VibeAgent(config=config, mode=AgentMode.DEFAULT, enable_streaming=True)
|
||||
# NOTE: For now, we pin session.id to agent.session_id right after init time.
|
||||
# We should just use agent.session_id everywhere, but it can still change during
|
||||
# session lifetime (e.g. agent.compact is called).
|
||||
# We should refactor agent.session_id to make it immutable in ACP context.
|
||||
session = AcpSession(id=agent.session_id, agent=agent)
|
||||
agent_loop = AgentLoop(
|
||||
config=config, agent_name=BuiltinAgentName.DEFAULT, enable_streaming=True
|
||||
)
|
||||
# NOTE: For now, we pin session.id to agent_loop.session_id right after init time.
|
||||
# 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
|
||||
|
||||
if not agent.auto_approve:
|
||||
agent.set_approval_callback(
|
||||
self._create_approval_callback(agent.session_id)
|
||||
if not agent_loop.auto_approve:
|
||||
agent_loop.set_approval_callback(
|
||||
self._create_approval_callback(agent_loop.session_id)
|
||||
)
|
||||
|
||||
response = NewSessionResponse(
|
||||
sessionId=agent.session_id,
|
||||
session_id=agent_loop.session_id,
|
||||
models=SessionModelState(
|
||||
currentModelId=agent.config.active_model,
|
||||
availableModels=[
|
||||
ModelInfo(modelId=model.alias, name=model.alias)
|
||||
for model in agent.config.models
|
||||
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(
|
||||
currentModeId=session.agent.mode.value,
|
||||
availableModes=get_all_acp_session_modes(),
|
||||
current_mode_id=session.agent_loop.agent_profile.name,
|
||||
available_modes=get_all_acp_session_modes(agent_loop.agent_manager),
|
||||
),
|
||||
)
|
||||
return response
|
||||
|
||||
def _get_disabled_tools_from_capabilities(self) -> list[str]:
|
||||
if not self.client_capabilities:
|
||||
return []
|
||||
def _get_acp_tool_overrides(self) -> list[Path]:
|
||||
overrides = ["todo"]
|
||||
|
||||
disabled: list[str] = []
|
||||
if self.client_capabilities:
|
||||
if self.client_capabilities.terminal:
|
||||
overrides.append("bash")
|
||||
if self.client_capabilities.fs:
|
||||
fs = self.client_capabilities.fs
|
||||
if fs.read_text_file:
|
||||
overrides.append("read_file")
|
||||
if fs.write_text_file:
|
||||
overrides.extend(["write_file", "search_replace"])
|
||||
|
||||
if not self.client_capabilities.terminal:
|
||||
disabled.append("bash")
|
||||
|
||||
if fs := self.client_capabilities.fs:
|
||||
if not fs.readTextFile:
|
||||
disabled.append("read_file")
|
||||
if not fs.writeTextFile:
|
||||
disabled.append("write_file")
|
||||
disabled.append("search_replace")
|
||||
|
||||
return disabled
|
||||
return [
|
||||
VIBE_ROOT / "acp" / "tools" / "builtins" / f"{override}.py"
|
||||
for override in overrides
|
||||
]
|
||||
|
||||
def _create_approval_callback(self, session_id: str) -> AsyncApprovalCallback:
|
||||
session = self._get_session(session_id)
|
||||
|
|
@ -229,9 +246,9 @@ class VibeAcpAgent(AcpAgent):
|
|||
case ToolOption.ALLOW_ONCE:
|
||||
return (ApprovalResponse.YES, None)
|
||||
case ToolOption.ALLOW_ALWAYS:
|
||||
if tool_name not in session.agent.config.tools:
|
||||
session.agent.config.tools[tool_name] = BaseToolConfig()
|
||||
session.agent.config.tools[
|
||||
if tool_name not in session.agent_loop.config.tools:
|
||||
session.agent_loop.config.tools[tool_name] = BaseToolConfig()
|
||||
session.agent_loop.config.tools[
|
||||
tool_name
|
||||
].permission = ToolPermission.ALWAYS
|
||||
return (ApprovalResponse.YES, None)
|
||||
|
|
@ -247,19 +264,16 @@ class VibeAcpAgent(AcpAgent):
|
|||
tool_name: str, args: BaseModel, tool_call_id: str
|
||||
) -> tuple[ApprovalResponse, str | None]:
|
||||
# Create the tool call update
|
||||
tool_call = ToolCall(toolCallId=tool_call_id)
|
||||
tool_call = ToolCallUpdate(tool_call_id=tool_call_id)
|
||||
|
||||
# Request permission from the user
|
||||
request = RequestPermissionRequest(
|
||||
sessionId=session_id, toolCall=tool_call, options=TOOL_OPTIONS
|
||||
response = await self.client.request_permission(
|
||||
session_id=session_id, tool_call=tool_call, options=TOOL_OPTIONS
|
||||
)
|
||||
|
||||
response = await self.connection.requestPermission(request)
|
||||
|
||||
# Parse the response using isinstance for proper type narrowing
|
||||
if response.outcome.outcome == "selected":
|
||||
outcome = cast(AllowedOutcome, response.outcome)
|
||||
return _handle_permission_selection(outcome.optionId, tool_name)
|
||||
return _handle_permission_selection(outcome.option_id, tool_name)
|
||||
else:
|
||||
return (
|
||||
ApprovalResponse.NO,
|
||||
|
|
@ -272,102 +286,111 @@ class VibeAcpAgent(AcpAgent):
|
|||
|
||||
return approval_callback
|
||||
|
||||
def _get_session(self, session_id: str) -> AcpSession:
|
||||
def _get_session(self, session_id: str) -> AcpSessionLoop:
|
||||
if session_id not in self.sessions:
|
||||
raise RequestError.invalid_params({"session": "Not found"})
|
||||
return self.sessions[session_id]
|
||||
|
||||
@override
|
||||
async def loadSession(self, params: LoadSessionRequest) -> None:
|
||||
async def load_session(
|
||||
self,
|
||||
cwd: str,
|
||||
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio],
|
||||
session_id: str,
|
||||
**kwargs: Any,
|
||||
) -> LoadSessionResponse | None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
async def setSessionMode(
|
||||
self, params: SetSessionModeRequest
|
||||
async def set_session_mode(
|
||||
self, mode_id: str, session_id: str, **kwargs: Any
|
||||
) -> SetSessionModeResponse | None:
|
||||
session = self._get_session(params.sessionId)
|
||||
session = self._get_session(session_id)
|
||||
|
||||
if not is_valid_acp_mode(params.modeId):
|
||||
if not is_valid_acp_agent(session.agent_loop.agent_manager, mode_id):
|
||||
return None
|
||||
|
||||
new_mode = acp_to_agent_mode(params.modeId)
|
||||
if new_mode is None:
|
||||
return None
|
||||
await session.agent_loop.switch_agent(mode_id)
|
||||
|
||||
await session.agent.switch_mode(new_mode)
|
||||
|
||||
if new_mode.auto_approve:
|
||||
session.agent.approval_callback = None
|
||||
if session.agent_loop.auto_approve:
|
||||
session.agent_loop.approval_callback = None
|
||||
else:
|
||||
session.agent.set_approval_callback(
|
||||
session.agent_loop.set_approval_callback(
|
||||
self._create_approval_callback(session.id)
|
||||
)
|
||||
|
||||
return SetSessionModeResponse()
|
||||
|
||||
@override
|
||||
async def setSessionModel(
|
||||
self, params: SetSessionModelRequest
|
||||
async def set_session_model(
|
||||
self, model_id: str, session_id: str, **kwargs: Any
|
||||
) -> SetSessionModelResponse | None:
|
||||
session = self._get_session(params.sessionId)
|
||||
session = self._get_session(session_id)
|
||||
|
||||
model_aliases = [model.alias for model in session.agent.config.models]
|
||||
if params.modelId not in model_aliases:
|
||||
model_aliases = [model.alias for model in session.agent_loop.config.models]
|
||||
if model_id not in model_aliases:
|
||||
return None
|
||||
|
||||
VibeConfig.save_updates({"active_model": params.modelId})
|
||||
VibeConfig.save_updates({"active_model": model_id})
|
||||
|
||||
new_config = VibeConfig.load(
|
||||
workdir=session.agent.config.workdir,
|
||||
tool_paths=session.agent.config.tool_paths,
|
||||
disabled_tools=self._get_disabled_tools_from_capabilities(),
|
||||
tool_paths=session.agent_loop.config.tool_paths,
|
||||
disabled_tools=["ask_user_question"],
|
||||
)
|
||||
|
||||
await session.agent.reload_with_initial_messages(config=new_config)
|
||||
await session.agent_loop.reload_with_initial_messages(base_config=new_config)
|
||||
|
||||
return SetSessionModelResponse()
|
||||
|
||||
@override
|
||||
async def prompt(self, params: PromptRequest) -> PromptResponse:
|
||||
session = self._get_session(params.sessionId)
|
||||
async def list_sessions(
|
||||
self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any
|
||||
) -> ListSessionsResponse:
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
async def prompt(
|
||||
self, prompt: list[ContentBlock], session_id: str, **kwargs: Any
|
||||
) -> PromptResponse:
|
||||
session = self._get_session(session_id)
|
||||
|
||||
if session.task is not None:
|
||||
raise RuntimeError(
|
||||
"Concurrent prompts are not supported yet, wait for agent to finish"
|
||||
"Concurrent prompts are not supported yet, wait for agent loop to finish"
|
||||
)
|
||||
|
||||
text_prompt = self._build_text_prompt(params.prompt)
|
||||
text_prompt = self._build_text_prompt(prompt)
|
||||
|
||||
async def agent_task() -> None:
|
||||
async for update in self._run_agent_loop(session, text_prompt):
|
||||
await self.connection.sessionUpdate(
|
||||
SessionNotification(sessionId=session.id, update=update)
|
||||
)
|
||||
temp_user_message_id: str | None = kwargs.get("messageId")
|
||||
|
||||
async def agent_loop_task() -> None:
|
||||
async for update in self._run_agent_loop(
|
||||
session, text_prompt, temp_user_message_id
|
||||
):
|
||||
await self.client.session_update(session_id=session.id, update=update)
|
||||
|
||||
try:
|
||||
session.task = asyncio.create_task(agent_task())
|
||||
session.task = asyncio.create_task(agent_loop_task())
|
||||
await session.task
|
||||
|
||||
except asyncio.CancelledError:
|
||||
return PromptResponse(stopReason="cancelled")
|
||||
return PromptResponse(stop_reason="cancelled")
|
||||
|
||||
except Exception as e:
|
||||
await self.connection.sessionUpdate(
|
||||
SessionNotification(
|
||||
sessionId=params.sessionId,
|
||||
update=AgentMessageChunk(
|
||||
sessionUpdate="agent_message_chunk",
|
||||
content=TextContentBlock(type="text", text=f"Error: {e!s}"),
|
||||
),
|
||||
)
|
||||
await self.client.session_update(
|
||||
session_id=session_id,
|
||||
update=AgentMessageChunk(
|
||||
session_update="agent_message_chunk",
|
||||
content=TextContentBlock(type="text", text=f"Error: {e!s}"),
|
||||
),
|
||||
)
|
||||
|
||||
return PromptResponse(stopReason="refusal")
|
||||
return PromptResponse(stop_reason="refusal")
|
||||
|
||||
finally:
|
||||
session.task = None
|
||||
|
||||
return PromptResponse(stopReason="end_turn")
|
||||
return PromptResponse(stop_reason="end_turn")
|
||||
|
||||
def _build_text_prompt(self, acp_prompt: list[ContentBlock]) -> str:
|
||||
text_prompt = ""
|
||||
|
|
@ -400,7 +423,7 @@ class VibeAcpAgent(AcpAgent):
|
|||
"name": block.name,
|
||||
"title": block.title,
|
||||
"description": block.description,
|
||||
"mimeType": block.mimeType,
|
||||
"mime_type": block.mime_type,
|
||||
"size": block.size,
|
||||
}
|
||||
parts = [
|
||||
|
|
@ -415,23 +438,37 @@ class VibeAcpAgent(AcpAgent):
|
|||
return text_prompt
|
||||
|
||||
async def _run_agent_loop(
|
||||
self, session: AcpSession, prompt: str
|
||||
self, session: AcpSessionLoop, prompt: str, user_message_id: str | None = None
|
||||
) -> AsyncGenerator[SessionUpdate]:
|
||||
rendered_prompt = render_path_prompt(
|
||||
prompt, base_dir=session.agent.config.effective_workdir
|
||||
)
|
||||
async for event in session.agent.act(rendered_prompt):
|
||||
if isinstance(event, AssistantEvent):
|
||||
rendered_prompt = render_path_prompt(prompt, base_dir=Path.cwd())
|
||||
|
||||
async for event in session.agent_loop.act(rendered_prompt):
|
||||
if isinstance(event, UserMessageEvent):
|
||||
yield UserMessageChunk(
|
||||
session_update="user_message_chunk",
|
||||
content=TextContentBlock(type="text", text=""),
|
||||
field_meta={
|
||||
"messageId": event.message_id,
|
||||
**(
|
||||
{"previousMessageId": user_message_id}
|
||||
if user_message_id
|
||||
else {}
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
elif isinstance(event, AssistantEvent):
|
||||
yield AgentMessageChunk(
|
||||
sessionUpdate="agent_message_chunk",
|
||||
session_update="agent_message_chunk",
|
||||
content=TextContentBlock(type="text", text=event.content),
|
||||
field_meta={"messageId": event.message_id},
|
||||
)
|
||||
|
||||
elif isinstance(event, ToolCallEvent):
|
||||
if issubclass(event.tool_class, BaseAcpTool):
|
||||
event.tool_class.update_tool_state(
|
||||
tool_manager=session.agent.tool_manager,
|
||||
connection=self.connection,
|
||||
tool_manager=session.agent_loop.tool_manager,
|
||||
client=self.client,
|
||||
session_id=session.id,
|
||||
tool_call_id=event.tool_call_id,
|
||||
)
|
||||
|
|
@ -445,32 +482,67 @@ class VibeAcpAgent(AcpAgent):
|
|||
if session_update:
|
||||
yield session_update
|
||||
|
||||
elif isinstance(event, ToolStreamEvent):
|
||||
yield ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content",
|
||||
content=TextContentBlock(type="text", text=event.message),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
elif isinstance(event, CompactStartEvent):
|
||||
yield create_compact_start_session_update(event)
|
||||
|
||||
elif isinstance(event, CompactEndEvent):
|
||||
yield create_compact_end_session_update(event)
|
||||
|
||||
@override
|
||||
async def cancel(self, params: CancelNotification) -> None:
|
||||
session = self._get_session(params.sessionId)
|
||||
async def cancel(self, session_id: str, **kwargs: Any) -> None:
|
||||
session = self._get_session(session_id)
|
||||
if session.task and not session.task.done():
|
||||
session.task.cancel()
|
||||
session.task = None
|
||||
|
||||
@override
|
||||
async def extMethod(self, method: str, params: dict) -> dict:
|
||||
async def fork_session(
|
||||
self,
|
||||
cwd: str,
|
||||
session_id: str,
|
||||
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ForkSessionResponse:
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
async def extNotification(self, method: str, params: dict) -> None:
|
||||
async def resume_session(
|
||||
self,
|
||||
cwd: str,
|
||||
session_id: str,
|
||||
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ResumeSessionResponse:
|
||||
raise NotImplementedError()
|
||||
|
||||
@override
|
||||
async def ext_method(self, method: str, params: dict) -> dict:
|
||||
raise NotImplementedError()
|
||||
|
||||
async def _run_acp_server() -> None:
|
||||
reader, writer = await stdio_streams()
|
||||
@override
|
||||
async def ext_notification(self, method: str, params: dict) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
AgentSideConnection(lambda connection: VibeAcpAgent(connection), writer, reader)
|
||||
await asyncio.Event().wait()
|
||||
@override
|
||||
def on_connect(self, conn: Client) -> None:
|
||||
self.client = conn
|
||||
|
||||
|
||||
def run_acp_server() -> None:
|
||||
try:
|
||||
asyncio.run(_run_acp_server())
|
||||
asyncio.run(run_agent(agent=VibeAcpAgentLoop(), use_unstable_protocol=True))
|
||||
except KeyboardInterrupt:
|
||||
# This is expected when the server is terminated
|
||||
pass
|
||||
|
|
@ -2,10 +2,13 @@ from __future__ import annotations
|
|||
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
import sys
|
||||
|
||||
from vibe import __version__
|
||||
from vibe.core.paths.config_paths import unlock_config_paths
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.paths.config_paths import CONFIG_FILE, HISTORY_FILE, unlock_config_paths
|
||||
from vibe.core.utils import logger
|
||||
|
||||
# Configure line buffering for subprocess communication
|
||||
sys.stdout.reconfigure(line_buffering=True) # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
|
@ -28,12 +31,45 @@ def parse_arguments() -> Arguments:
|
|||
return Arguments(setup=args.setup)
|
||||
|
||||
|
||||
def bootstrap_config_files() -> None:
|
||||
if not CONFIG_FILE.path.exists():
|
||||
try:
|
||||
VibeConfig.save_updates(VibeConfig.create_default())
|
||||
except Exception as e:
|
||||
logger.error(f"Could not create default config file: {e}")
|
||||
raise
|
||||
|
||||
if not HISTORY_FILE.path.exists():
|
||||
try:
|
||||
HISTORY_FILE.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
HISTORY_FILE.path.write_text("Hello Vibe!\n", "utf-8")
|
||||
except Exception as e:
|
||||
logger.error(f"Could not create history file: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def handle_debug_mode() -> None:
|
||||
if os.environ.get("DEBUG_MODE") != "true":
|
||||
return
|
||||
|
||||
try:
|
||||
import debugpy
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
debugpy.listen(("localhost", 5678))
|
||||
# uncomment this to wait for the debugger to attach
|
||||
# debugpy.wait_for_client()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
handle_debug_mode()
|
||||
unlock_config_paths()
|
||||
|
||||
from vibe.acp.acp_agent import run_acp_server
|
||||
from vibe.acp.acp_agent_loop import run_acp_server
|
||||
from vibe.setup.onboarding import run_onboarding
|
||||
|
||||
bootstrap_config_files()
|
||||
args = parse_arguments()
|
||||
if args.setup:
|
||||
run_onboarding()
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from typing import Protocol, cast, runtime_checkable
|
||||
from typing import Annotated, Protocol, cast, runtime_checkable
|
||||
|
||||
from acp import AgentSideConnection, SessionNotification
|
||||
from acp import Client
|
||||
from acp.helpers import SessionUpdate, ToolCallContentVariant
|
||||
from acp.schema import ToolCallProgress
|
||||
from pydantic import Field
|
||||
from pydantic import BaseModel, ConfigDict, Field, SkipValidation
|
||||
|
||||
from vibe.core.tools.base import BaseTool, ToolError
|
||||
from vibe.core.tools.manager import ToolManager
|
||||
|
|
@ -28,9 +28,11 @@ class ToolResultSessionUpdateProtocol(Protocol):
|
|||
) -> SessionUpdate | None: ...
|
||||
|
||||
|
||||
class AcpToolState:
|
||||
connection: AgentSideConnection | None = Field(
|
||||
default=None, description="ACP agent-side connection"
|
||||
class AcpToolState(BaseModel):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
client: Annotated[Client | None, SkipValidation] = Field(
|
||||
default=None, description="ACP Client"
|
||||
)
|
||||
session_id: str | None = Field(default=None, description="Current ACP session ID")
|
||||
tool_call_id: str | None = Field(
|
||||
|
|
@ -52,12 +54,12 @@ class BaseAcpTool[ToolState: AcpToolState](BaseTool):
|
|||
cls,
|
||||
*,
|
||||
tool_manager: ToolManager,
|
||||
connection: AgentSideConnection | None,
|
||||
client: Client | None,
|
||||
session_id: str | None,
|
||||
tool_call_id: str | None,
|
||||
) -> None:
|
||||
tool_instance = cls.get_tool_instance(cls.get_name(), tool_manager)
|
||||
tool_instance.state.connection = connection
|
||||
tool_instance.state.client = client
|
||||
tool_instance.state.session_id = session_id
|
||||
tool_instance.state.tool_call_id = tool_call_id
|
||||
|
||||
|
|
@ -65,36 +67,34 @@ class BaseAcpTool[ToolState: AcpToolState](BaseTool):
|
|||
@abstractmethod
|
||||
def _get_tool_state_class(cls) -> type[ToolState]: ...
|
||||
|
||||
def _load_state(self) -> tuple[AgentSideConnection, str, str | None]:
|
||||
if self.state.connection is None:
|
||||
def _load_state(self) -> tuple[Client, str, str | None]:
|
||||
if self.state.client is None:
|
||||
raise ToolError(
|
||||
"Connection not available in tool state. This tool can only be used within an ACP session."
|
||||
"Client not available in tool state. This tool can only be used within an ACP session."
|
||||
)
|
||||
if self.state.session_id is None:
|
||||
raise ToolError(
|
||||
"Session ID not available in tool state. This tool can only be used within an ACP session."
|
||||
)
|
||||
|
||||
return self.state.connection, self.state.session_id, self.state.tool_call_id
|
||||
return self.state.client, self.state.session_id, self.state.tool_call_id
|
||||
|
||||
async def _send_in_progress_session_update(
|
||||
self, content: list[ToolCallContentVariant] | None = None
|
||||
) -> None:
|
||||
connection, session_id, tool_call_id = self._load_state()
|
||||
client, session_id, tool_call_id = self._load_state()
|
||||
if tool_call_id is None:
|
||||
return
|
||||
|
||||
try:
|
||||
await connection.sessionUpdate(
|
||||
SessionNotification(
|
||||
sessionId=session_id,
|
||||
update=ToolCallProgress(
|
||||
sessionUpdate="tool_call_update",
|
||||
toolCallId=tool_call_id,
|
||||
status="in_progress",
|
||||
content=content,
|
||||
),
|
||||
)
|
||||
await client.session_update(
|
||||
session_id=session_id,
|
||||
update=ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=tool_call_id,
|
||||
status="in_progress",
|
||||
content=content,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update session: {e!r}")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
|
||||
from acp import CreateTerminalRequest, TerminalHandle
|
||||
from acp.schema import (
|
||||
EnvVariable,
|
||||
TerminalToolCallContent,
|
||||
|
|
@ -14,9 +15,9 @@ from acp.schema import (
|
|||
|
||||
from vibe import VIBE_ROOT
|
||||
from vibe.acp.tools.base import AcpToolState, BaseAcpTool
|
||||
from vibe.core.tools.base import BaseToolState, ToolError
|
||||
from vibe.core.tools.base import BaseToolState, InvokeContext, ToolError
|
||||
from vibe.core.tools.builtins.bash import Bash as CoreBashTool, BashArgs, BashResult
|
||||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||
from vibe.core.types import ToolCallEvent, ToolResultEvent, ToolStreamEvent
|
||||
from vibe.core.utils import logger
|
||||
|
||||
|
||||
|
|
@ -32,48 +33,54 @@ class Bash(CoreBashTool, BaseAcpTool[AcpBashState]):
|
|||
def _get_tool_state_class(cls) -> type[AcpBashState]:
|
||||
return AcpBashState
|
||||
|
||||
async def run(self, args: BashArgs) -> BashResult:
|
||||
connection, session_id, _ = self._load_state()
|
||||
async def run(
|
||||
self, args: BashArgs, ctx: InvokeContext | None = None
|
||||
) -> AsyncGenerator[ToolStreamEvent | BashResult, None]:
|
||||
client, session_id, _ = self._load_state()
|
||||
|
||||
timeout = args.timeout or self.config.default_timeout
|
||||
max_bytes = self.config.max_output_bytes
|
||||
env, command, cmd_args = self._parse_command(args.command)
|
||||
|
||||
create_request = CreateTerminalRequest(
|
||||
sessionId=session_id,
|
||||
command=command,
|
||||
args=cmd_args,
|
||||
env=env,
|
||||
cwd=str(self.config.effective_workdir),
|
||||
outputByteLimit=max_bytes,
|
||||
)
|
||||
|
||||
try:
|
||||
terminal_handle = await connection.createTerminal(create_request)
|
||||
terminal_handle = await client.create_terminal(
|
||||
session_id=session_id,
|
||||
command=command,
|
||||
args=cmd_args,
|
||||
env=env,
|
||||
cwd=str(Path.cwd()),
|
||||
output_byte_limit=max_bytes,
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolError(f"Failed to create terminal: {e!r}") from e
|
||||
|
||||
terminal_id = terminal_handle.id
|
||||
|
||||
await self._send_in_progress_session_update([
|
||||
TerminalToolCallContent(type="terminal", terminalId=terminal_handle.id)
|
||||
TerminalToolCallContent(type="terminal", terminal_id=terminal_id)
|
||||
])
|
||||
|
||||
try:
|
||||
exit_response = await self._wait_for_terminal_exit(
|
||||
terminal_handle, timeout, args.command
|
||||
terminal_id=terminal_id, timeout=timeout, command=args.command
|
||||
)
|
||||
|
||||
output_response = await terminal_handle.current_output()
|
||||
output_response = await client.terminal_output(
|
||||
session_id=session_id, terminal_id=terminal_id
|
||||
)
|
||||
|
||||
return self._build_result(
|
||||
yield self._build_result(
|
||||
command=args.command,
|
||||
stdout=output_response.output,
|
||||
stderr="",
|
||||
returncode=exit_response.exitCode or 0,
|
||||
returncode=exit_response.exit_code or 0,
|
||||
)
|
||||
|
||||
finally:
|
||||
try:
|
||||
await terminal_handle.release()
|
||||
await client.release_terminal(
|
||||
session_id=session_id, terminal_id=terminal_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to release terminal: {e!r}")
|
||||
|
||||
|
|
@ -105,15 +112,22 @@ class Bash(CoreBashTool, BaseAcpTool[AcpBashState]):
|
|||
return summary
|
||||
|
||||
async def _wait_for_terminal_exit(
|
||||
self, terminal_handle: TerminalHandle, timeout: int, command: str
|
||||
self, terminal_id: str, timeout: int, command: str
|
||||
) -> WaitForTerminalExitResponse:
|
||||
client, session_id, _ = self._load_state()
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
terminal_handle.wait_for_exit(), timeout=timeout
|
||||
client.wait_for_terminal_exit(
|
||||
session_id=session_id, terminal_id=terminal_id
|
||||
),
|
||||
timeout=timeout,
|
||||
)
|
||||
except TimeoutError:
|
||||
try:
|
||||
await terminal_handle.kill()
|
||||
await client.kill_terminal(
|
||||
session_id=session_id, terminal_id=terminal_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to kill terminal: {e!r}")
|
||||
|
||||
|
|
@ -125,12 +139,12 @@ class Bash(CoreBashTool, BaseAcpTool[AcpBashState]):
|
|||
raise ValueError(f"Unexpected tool args: {event.args}")
|
||||
|
||||
return ToolCallStart(
|
||||
sessionUpdate="tool_call",
|
||||
session_update="tool_call",
|
||||
title=Bash.get_summary(event.args),
|
||||
content=None,
|
||||
toolCallId=event.tool_call_id,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind="execute",
|
||||
rawInput=event.args.model_dump_json(),
|
||||
raw_input=event.args.model_dump_json(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -138,7 +152,7 @@ class Bash(CoreBashTool, BaseAcpTool[AcpBashState]):
|
|||
cls, event: ToolResultEvent
|
||||
) -> ToolCallProgress | None:
|
||||
return ToolCallProgress(
|
||||
sessionUpdate="tool_call_update",
|
||||
toolCallId=event.tool_call_id,
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="failed" if event.error else "completed",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ from __future__ import annotations
|
|||
|
||||
from pathlib import Path
|
||||
|
||||
from acp import ReadTextFileRequest
|
||||
|
||||
from vibe import VIBE_ROOT
|
||||
from vibe.acp.tools.base import AcpToolState, BaseAcpTool
|
||||
from vibe.core.tools.base import ToolError
|
||||
|
|
@ -31,19 +29,17 @@ class ReadFile(CoreReadFileTool, BaseAcpTool[AcpReadFileState]):
|
|||
return AcpReadFileState
|
||||
|
||||
async def _read_file(self, args: ReadFileArgs, file_path: Path) -> _ReadResult:
|
||||
connection, session_id, _ = self._load_state()
|
||||
client, session_id, _ = self._load_state()
|
||||
|
||||
line = args.offset + 1 if args.offset > 0 else None
|
||||
limit = args.limit
|
||||
|
||||
read_request = ReadTextFileRequest(
|
||||
sessionId=session_id, path=str(file_path), line=line, limit=limit
|
||||
)
|
||||
|
||||
await self._send_in_progress_session_update()
|
||||
|
||||
try:
|
||||
response = await connection.readTextFile(read_request)
|
||||
response = await client.read_text_file(
|
||||
session_id=session_id, path=str(file_path), line=line, limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolError(f"Error reading {file_path}: {e}") from e
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
|||
|
||||
from pathlib import Path
|
||||
|
||||
from acp import ReadTextFileRequest, WriteTextFileRequest
|
||||
from acp.helpers import SessionUpdate
|
||||
from acp.schema import (
|
||||
FileEditToolCallContent,
|
||||
|
|
@ -38,14 +37,14 @@ class SearchReplace(CoreSearchReplaceTool, BaseAcpTool[AcpSearchReplaceState]):
|
|||
return AcpSearchReplaceState
|
||||
|
||||
async def _read_file(self, file_path: Path) -> str:
|
||||
connection, session_id, _ = self._load_state()
|
||||
|
||||
read_request = ReadTextFileRequest(sessionId=session_id, path=str(file_path))
|
||||
client, session_id, _ = self._load_state()
|
||||
|
||||
await self._send_in_progress_session_update()
|
||||
|
||||
try:
|
||||
response = await connection.readTextFile(read_request)
|
||||
response = await client.read_text_file(
|
||||
session_id=session_id, path=str(file_path)
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolError(f"Unexpected error reading {file_path}: {e}") from e
|
||||
|
||||
|
|
@ -62,14 +61,12 @@ class SearchReplace(CoreSearchReplaceTool, BaseAcpTool[AcpSearchReplaceState]):
|
|||
)
|
||||
|
||||
async def _write_file(self, file_path: Path, content: str) -> None:
|
||||
connection, session_id, _ = self._load_state()
|
||||
|
||||
write_request = WriteTextFileRequest(
|
||||
sessionId=session_id, path=str(file_path), content=content
|
||||
)
|
||||
client, session_id, _ = self._load_state()
|
||||
|
||||
try:
|
||||
await connection.writeTextFile(write_request)
|
||||
await client.write_text_file(
|
||||
session_id=session_id, path=str(file_path), content=content
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolError(f"Error writing {file_path}: {e}") from e
|
||||
|
||||
|
|
@ -82,29 +79,29 @@ class SearchReplace(CoreSearchReplaceTool, BaseAcpTool[AcpSearchReplaceState]):
|
|||
blocks = cls._parse_search_replace_blocks(args.content)
|
||||
|
||||
return ToolCallStart(
|
||||
sessionUpdate="tool_call",
|
||||
session_update="tool_call",
|
||||
title=cls.get_call_display(event).summary,
|
||||
toolCallId=event.tool_call_id,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind="edit",
|
||||
content=[
|
||||
FileEditToolCallContent(
|
||||
type="diff",
|
||||
path=args.file_path,
|
||||
oldText=block.search,
|
||||
newText=block.replace,
|
||||
old_text=block.search,
|
||||
new_text=block.replace,
|
||||
)
|
||||
for block in blocks
|
||||
],
|
||||
locations=[ToolCallLocation(path=args.file_path)],
|
||||
rawInput=args.model_dump_json(),
|
||||
raw_input=args.model_dump_json(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tool_result_session_update(cls, event: ToolResultEvent) -> SessionUpdate | None:
|
||||
if event.error:
|
||||
return ToolCallProgress(
|
||||
sessionUpdate="tool_call_update",
|
||||
toolCallId=event.tool_call_id,
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="failed",
|
||||
)
|
||||
|
||||
|
|
@ -115,18 +112,18 @@ class SearchReplace(CoreSearchReplaceTool, BaseAcpTool[AcpSearchReplaceState]):
|
|||
blocks = cls._parse_search_replace_blocks(result.content)
|
||||
|
||||
return ToolCallProgress(
|
||||
sessionUpdate="tool_call_update",
|
||||
toolCallId=event.tool_call_id,
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="completed",
|
||||
content=[
|
||||
FileEditToolCallContent(
|
||||
type="diff",
|
||||
path=result.file,
|
||||
oldText=block.search,
|
||||
newText=block.replace,
|
||||
old_text=block.search,
|
||||
new_text=block.replace,
|
||||
)
|
||||
for block in blocks
|
||||
],
|
||||
locations=[ToolCallLocation(path=result.file)],
|
||||
rawOutput=result.model_dump_json(),
|
||||
raw_output=result.model_dump_json(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class Todo(CoreTodoTool, BaseAcpTool[AcpTodoState]):
|
|||
}
|
||||
|
||||
update = AgentPlanUpdate(
|
||||
sessionUpdate="plan",
|
||||
session_update="plan",
|
||||
entries=[
|
||||
PlanEntry(
|
||||
content=todo.content,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
|||
|
||||
from pathlib import Path
|
||||
|
||||
from acp import WriteTextFileRequest
|
||||
from acp.helpers import SessionUpdate
|
||||
from acp.schema import (
|
||||
FileEditToolCallContent,
|
||||
|
|
@ -38,16 +37,14 @@ class WriteFile(CoreWriteFileTool, BaseAcpTool[AcpWriteFileState]):
|
|||
return AcpWriteFileState
|
||||
|
||||
async def _write_file(self, args: WriteFileArgs, file_path: Path) -> None:
|
||||
connection, session_id, _ = self._load_state()
|
||||
|
||||
write_request = WriteTextFileRequest(
|
||||
sessionId=session_id, path=str(file_path), content=args.content
|
||||
)
|
||||
client, session_id, _ = self._load_state()
|
||||
|
||||
await self._send_in_progress_session_update()
|
||||
|
||||
try:
|
||||
await connection.writeTextFile(write_request)
|
||||
await client.write_text_file(
|
||||
session_id=session_id, path=str(file_path), content=args.content
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolError(f"Error writing {file_path}: {e}") from e
|
||||
|
||||
|
|
@ -58,25 +55,25 @@ class WriteFile(CoreWriteFileTool, BaseAcpTool[AcpWriteFileState]):
|
|||
return None
|
||||
|
||||
return ToolCallStart(
|
||||
sessionUpdate="tool_call",
|
||||
session_update="tool_call",
|
||||
title=cls.get_call_display(event).summary,
|
||||
toolCallId=event.tool_call_id,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind="edit",
|
||||
content=[
|
||||
FileEditToolCallContent(
|
||||
type="diff", path=args.path, oldText=None, newText=args.content
|
||||
type="diff", path=args.path, old_text=None, new_text=args.content
|
||||
)
|
||||
],
|
||||
locations=[ToolCallLocation(path=args.path)],
|
||||
rawInput=args.model_dump_json(),
|
||||
raw_input=args.model_dump_json(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tool_result_session_update(cls, event: ToolResultEvent) -> SessionUpdate | None:
|
||||
if event.error:
|
||||
return ToolCallProgress(
|
||||
sessionUpdate="tool_call_update",
|
||||
toolCallId=event.tool_call_id,
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="failed",
|
||||
)
|
||||
|
||||
|
|
@ -85,14 +82,17 @@ class WriteFile(CoreWriteFileTool, BaseAcpTool[AcpWriteFileState]):
|
|||
return None
|
||||
|
||||
return ToolCallProgress(
|
||||
sessionUpdate="tool_call_update",
|
||||
toolCallId=event.tool_call_id,
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="completed",
|
||||
content=[
|
||||
FileEditToolCallContent(
|
||||
type="diff", path=result.path, oldText=None, newText=result.content
|
||||
type="diff",
|
||||
path=result.path,
|
||||
old_text=None,
|
||||
new_text=result.content,
|
||||
)
|
||||
],
|
||||
locations=[ToolCallLocation(path=result.path)],
|
||||
rawOutput=result.model_dump_json(),
|
||||
raw_output=result.model_dump_json(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,14 @@ from vibe.core.tools.ui import ToolUIDataAdapter
|
|||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||
from vibe.core.utils import TaggedText, is_user_cancellation_event
|
||||
|
||||
TOOL_KIND: dict[str, ToolKind] = {"read_file": "read", "grep": "search"}
|
||||
TOOL_KIND: dict[str, ToolKind] = {
|
||||
"grep": "search",
|
||||
"read_file": "read",
|
||||
# Right now, jetbrains implementation of "edit" tool kind is broken
|
||||
# Leading to the tool not appearing in the chat
|
||||
# "write_file": "edit",
|
||||
# "search_replace": "edit",
|
||||
}
|
||||
|
||||
|
||||
def tool_call_session_update(event: ToolCallEvent) -> SessionUpdate | None:
|
||||
|
|
@ -38,12 +45,12 @@ def tool_call_session_update(event: ToolCallEvent) -> SessionUpdate | None:
|
|||
)
|
||||
|
||||
return ToolCallStart(
|
||||
sessionUpdate="tool_call",
|
||||
session_update="tool_call",
|
||||
title=display.summary,
|
||||
content=content,
|
||||
toolCallId=event.tool_call_id,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind=TOOL_KIND.get(event.tool_name, "other"),
|
||||
rawInput=event.args.model_dump_json(),
|
||||
raw_input=event.args.model_dump_json(),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -66,10 +73,10 @@ def tool_result_session_update(event: ToolResultEvent) -> SessionUpdate | None:
|
|||
|
||||
if event.tool_class is None:
|
||||
return ToolCallProgress(
|
||||
sessionUpdate="tool_call_update",
|
||||
toolCallId=event.tool_call_id,
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="failed",
|
||||
rawOutput=raw_output,
|
||||
raw_output=raw_output,
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content",
|
||||
|
|
@ -103,9 +110,9 @@ def tool_result_session_update(event: ToolResultEvent) -> SessionUpdate | None:
|
|||
)
|
||||
|
||||
return ToolCallProgress(
|
||||
sessionUpdate="tool_call_update",
|
||||
toolCallId=event.tool_call_id,
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status=tool_status,
|
||||
rawOutput=raw_output,
|
||||
raw_output=raw_output,
|
||||
content=content,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,23 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
from typing import Literal, cast
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
|
||||
from acp.schema import PermissionOption, SessionMode
|
||||
from acp.schema import (
|
||||
ContentToolCallContent,
|
||||
PermissionOption,
|
||||
SessionMode,
|
||||
TextContentBlock,
|
||||
ToolCallProgress,
|
||||
ToolCallStart,
|
||||
)
|
||||
|
||||
from vibe.core.modes import MODE_CONFIGS, AgentMode
|
||||
from vibe.core.agents.models import AgentProfile, AgentType
|
||||
from vibe.core.types import CompactEndEvent, CompactStartEvent
|
||||
from vibe.core.utils import compact_reduction_display
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vibe.core.agents.manager import AgentManager
|
||||
|
||||
|
||||
class ToolOption(StrEnum):
|
||||
|
|
@ -17,37 +29,85 @@ class ToolOption(StrEnum):
|
|||
|
||||
TOOL_OPTIONS = [
|
||||
PermissionOption(
|
||||
optionId=ToolOption.ALLOW_ONCE,
|
||||
option_id=ToolOption.ALLOW_ONCE,
|
||||
name="Allow once",
|
||||
kind=cast(Literal["allow_once"], ToolOption.ALLOW_ONCE),
|
||||
),
|
||||
PermissionOption(
|
||||
optionId=ToolOption.ALLOW_ALWAYS,
|
||||
option_id=ToolOption.ALLOW_ALWAYS,
|
||||
name="Allow always",
|
||||
kind=cast(Literal["allow_always"], ToolOption.ALLOW_ALWAYS),
|
||||
),
|
||||
PermissionOption(
|
||||
optionId=ToolOption.REJECT_ONCE,
|
||||
option_id=ToolOption.REJECT_ONCE,
|
||||
name="Reject once",
|
||||
kind=cast(Literal["reject_once"], ToolOption.REJECT_ONCE),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def agent_mode_to_acp(mode: AgentMode) -> SessionMode:
|
||||
config = MODE_CONFIGS[mode]
|
||||
def agent_profile_to_acp(profile: AgentProfile) -> SessionMode:
|
||||
return SessionMode(
|
||||
id=mode.value, name=config.display_name, description=config.description
|
||||
id=profile.name, name=profile.display_name, description=profile.description
|
||||
)
|
||||
|
||||
|
||||
def acp_to_agent_mode(mode_id: str) -> AgentMode | None:
|
||||
return AgentMode.from_string(mode_id)
|
||||
def is_valid_acp_agent(agent_manager: AgentManager, agent_name: str) -> bool:
|
||||
return agent_name in agent_manager.available_agents
|
||||
|
||||
|
||||
def is_valid_acp_mode(mode_id: str) -> bool:
|
||||
return AgentMode.from_string(mode_id) is not None
|
||||
def get_all_acp_session_modes(agent_manager: AgentManager) -> list[SessionMode]:
|
||||
return [
|
||||
agent_profile_to_acp(profile)
|
||||
for profile in agent_manager.available_agents.values()
|
||||
if profile.agent_type == AgentType.AGENT
|
||||
]
|
||||
|
||||
|
||||
def get_all_acp_session_modes() -> list[SessionMode]:
|
||||
return [agent_mode_to_acp(mode) for mode in AgentMode]
|
||||
def create_compact_start_session_update(event: CompactStartEvent) -> ToolCallStart:
|
||||
# WORKAROUND: Using tool_call to communicate compact events to the client.
|
||||
# This should be revisited when the ACP protocol defines how compact events
|
||||
# should be represented.
|
||||
# [RFD](https://agentclientprotocol.com/rfds/session-usage)
|
||||
return ToolCallStart(
|
||||
session_update="tool_call",
|
||||
tool_call_id=event.tool_call_id,
|
||||
title="Compacting conversation history...",
|
||||
kind="other",
|
||||
status="in_progress",
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content",
|
||||
content=TextContentBlock(
|
||||
type="text",
|
||||
text="Automatic context management, no approval required. This may take some time...",
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def create_compact_end_session_update(event: CompactEndEvent) -> ToolCallProgress:
|
||||
# WORKAROUND: Using tool_call_update to communicate compact events to the client.
|
||||
# This should be revisited when the ACP protocol defines how compact events
|
||||
# should be represented.
|
||||
# [RFD](https://agentclientprotocol.com/rfds/session-usage)
|
||||
return ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
title="Compacted conversation history",
|
||||
status="completed",
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content",
|
||||
content=TextContentBlock(
|
||||
type="text",
|
||||
text=(
|
||||
compact_reduction_display(
|
||||
event.old_context_tokens, event.new_context_tokens
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue