v2.9.1 (#644)
Co-authored-by: Brice Carpentier <brice.carpentier@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
632ea8c032
commit
1fd7eea289
63 changed files with 3482 additions and 301 deletions
|
|
@ -367,7 +367,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
)
|
||||
|
||||
def _get_acp_tool_overrides(self) -> list[Path]:
|
||||
overrides = ["todo"]
|
||||
overrides = ["todo", "grep", "web_fetch", "web_search", "skill", "task"]
|
||||
|
||||
if self.client_capabilities:
|
||||
if self.client_capabilities.terminal:
|
||||
|
|
@ -399,6 +399,9 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
session.agent_loop.approve_always(tool_name, required_permissions)
|
||||
return (ApprovalResponse.YES, None)
|
||||
case ToolOption.REJECT_ONCE:
|
||||
session.agent_loop.telemetry_client.send_user_cancelled_action(
|
||||
"reject_approval"
|
||||
)
|
||||
return (
|
||||
ApprovalResponse.NO,
|
||||
"User rejected the tool call, provide an alternative plan",
|
||||
|
|
@ -946,6 +949,9 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
@override
|
||||
async def cancel(self, session_id: str, **kwargs: Any) -> None:
|
||||
session = self._get_session(session_id)
|
||||
session.agent_loop.telemetry_client.send_user_cancelled_action(
|
||||
"interrupt_agent"
|
||||
)
|
||||
await session.cancel_prompt()
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ from collections.abc import AsyncGenerator
|
|||
from pathlib import Path
|
||||
|
||||
from acp.schema import (
|
||||
ContentToolCallContent,
|
||||
TerminalToolCallContent,
|
||||
TextContentBlock,
|
||||
ToolCallProgress,
|
||||
ToolCallStart,
|
||||
WaitForTerminalExitResponse,
|
||||
|
|
@ -13,6 +15,7 @@ from acp.schema import (
|
|||
|
||||
from vibe import VIBE_ROOT
|
||||
from vibe.acp.tools.base import AcpToolState, BaseAcpTool
|
||||
from vibe.acp.tools.session_update import resolve_kind
|
||||
from vibe.core.logger import logger
|
||||
from vibe.core.tools.base import BaseToolState, InvokeContext, ToolError
|
||||
from vibe.core.tools.builtins.bash import Bash as CoreBashTool, BashArgs, BashResult
|
||||
|
|
@ -116,9 +119,10 @@ class Bash(CoreBashTool, BaseAcpTool[AcpBashState]):
|
|||
session_update="tool_call",
|
||||
title="bash",
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind="execute",
|
||||
kind=resolve_kind(event.tool_name),
|
||||
content=None,
|
||||
raw_input=None,
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
if not isinstance(event.args, BashArgs):
|
||||
raise ValueError(f"Unexpected tool args: {event.args}")
|
||||
|
|
@ -128,8 +132,9 @@ class Bash(CoreBashTool, BaseAcpTool[AcpBashState]):
|
|||
title=Bash.get_summary(event.args),
|
||||
content=None,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind="execute",
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_input=event.args.model_dump_json(),
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -140,4 +145,14 @@ class Bash(CoreBashTool, BaseAcpTool[AcpBashState]):
|
|||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="failed" if event.error else "completed",
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content",
|
||||
content=TextContentBlock(
|
||||
type="text", text=cls.get_result_display(event).message
|
||||
),
|
||||
)
|
||||
],
|
||||
kind=resolve_kind(event.tool_name),
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
|
|
|||
78
vibe/acp/tools/builtins/grep.py
Normal file
78
vibe/acp/tools/builtins/grep.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from acp.helpers import SessionUpdate
|
||||
from acp.schema import (
|
||||
ContentToolCallContent,
|
||||
TextContentBlock,
|
||||
ToolCallLocation,
|
||||
ToolCallProgress,
|
||||
ToolCallStart,
|
||||
)
|
||||
|
||||
from vibe import VIBE_ROOT
|
||||
from vibe.acp.tools.base import (
|
||||
ToolCallSessionUpdateProtocol,
|
||||
ToolResultSessionUpdateProtocol,
|
||||
)
|
||||
from vibe.acp.tools.session_update import (
|
||||
failed_tool_result,
|
||||
fallback_tool_call,
|
||||
resolve_kind,
|
||||
)
|
||||
from vibe.core.tools.builtins.grep import Grep as CoreGrepTool, GrepArgs, GrepResult
|
||||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||
|
||||
|
||||
class Grep(
|
||||
CoreGrepTool, ToolCallSessionUpdateProtocol, ToolResultSessionUpdateProtocol
|
||||
):
|
||||
prompt_path = VIBE_ROOT / "core" / "tools" / "builtins" / "prompts" / "grep.md"
|
||||
|
||||
@classmethod
|
||||
def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None:
|
||||
if not isinstance(event.args, GrepArgs):
|
||||
return fallback_tool_call(event, "grep")
|
||||
|
||||
search_path = str(Path(event.args.path).resolve())
|
||||
|
||||
return ToolCallStart(
|
||||
session_update="tool_call",
|
||||
title=cls.get_call_display(event).summary,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_input=event.args.model_dump_json(),
|
||||
locations=[ToolCallLocation(path=search_path)],
|
||||
field_meta={"tool_name": event.tool_name, "query": event.args.pattern},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tool_result_session_update(cls, event: ToolResultEvent) -> SessionUpdate | None:
|
||||
if failure := failed_tool_result(event, GrepResult):
|
||||
return failure
|
||||
|
||||
result = event.result
|
||||
assert isinstance(result, GrepResult)
|
||||
|
||||
locations = [
|
||||
ToolCallLocation(path=m.path, line=m.line) for m in result.parsed_matches
|
||||
]
|
||||
|
||||
return ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="completed",
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content",
|
||||
content=TextContentBlock(
|
||||
type="text", text=cls.get_result_display(event).message
|
||||
),
|
||||
)
|
||||
],
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_output=result.model_dump_json(),
|
||||
locations=locations if locations else None,
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
|
@ -2,8 +2,27 @@ from __future__ import annotations
|
|||
|
||||
from pathlib import Path
|
||||
|
||||
from acp.helpers import SessionUpdate
|
||||
from acp.schema import (
|
||||
ContentToolCallContent,
|
||||
TextContentBlock,
|
||||
ToolCallLocation,
|
||||
ToolCallProgress,
|
||||
ToolCallStart,
|
||||
)
|
||||
|
||||
from vibe import VIBE_ROOT
|
||||
from vibe.acp.tools.base import AcpToolState, BaseAcpTool
|
||||
from vibe.acp.tools.base import (
|
||||
AcpToolState,
|
||||
BaseAcpTool,
|
||||
ToolCallSessionUpdateProtocol,
|
||||
ToolResultSessionUpdateProtocol,
|
||||
)
|
||||
from vibe.acp.tools.session_update import (
|
||||
failed_tool_result,
|
||||
fallback_tool_call,
|
||||
resolve_kind,
|
||||
)
|
||||
from vibe.core.tools.base import ToolError
|
||||
from vibe.core.tools.builtins.read_file import (
|
||||
ReadFile as CoreReadFileTool,
|
||||
|
|
@ -12,6 +31,7 @@ from vibe.core.tools.builtins.read_file import (
|
|||
ReadFileState,
|
||||
_ReadResult,
|
||||
)
|
||||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||
|
||||
ReadFileResult = ReadFileResult
|
||||
|
||||
|
|
@ -20,7 +40,12 @@ class AcpReadFileState(ReadFileState, AcpToolState):
|
|||
pass
|
||||
|
||||
|
||||
class ReadFile(CoreReadFileTool, BaseAcpTool[AcpReadFileState]):
|
||||
class ReadFile(
|
||||
CoreReadFileTool,
|
||||
BaseAcpTool[AcpReadFileState],
|
||||
ToolCallSessionUpdateProtocol,
|
||||
ToolResultSessionUpdateProtocol,
|
||||
):
|
||||
state: AcpReadFileState
|
||||
prompt_path = VIBE_ROOT / "core" / "tools" / "builtins" / "prompts" / "read_file.md"
|
||||
|
||||
|
|
@ -28,6 +53,69 @@ class ReadFile(CoreReadFileTool, BaseAcpTool[AcpReadFileState]):
|
|||
def _get_tool_state_class(cls) -> type[AcpReadFileState]:
|
||||
return AcpReadFileState
|
||||
|
||||
@classmethod
|
||||
def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None:
|
||||
if not isinstance(event.args, ReadFileArgs):
|
||||
return fallback_tool_call(event, "read_file")
|
||||
|
||||
resolved = str(Path(event.args.path).resolve())
|
||||
|
||||
return ToolCallStart(
|
||||
session_update="tool_call",
|
||||
title=cls.format_call_display(event.args).summary,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_input=event.args.model_dump_json(),
|
||||
locations=[
|
||||
ToolCallLocation(
|
||||
path=resolved,
|
||||
field_meta={
|
||||
"type": "file_range",
|
||||
"offset": event.args.offset,
|
||||
"limit": event.args.limit,
|
||||
},
|
||||
)
|
||||
],
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tool_result_session_update(cls, event: ToolResultEvent) -> SessionUpdate | None:
|
||||
if failure := failed_tool_result(event, ReadFileResult):
|
||||
return failure
|
||||
|
||||
result = event.result
|
||||
assert isinstance(result, ReadFileResult)
|
||||
resolved = str(Path(result.path).resolve())
|
||||
locations = [
|
||||
ToolCallLocation(
|
||||
path=resolved,
|
||||
field_meta={
|
||||
"type": "file_range",
|
||||
"offset": result.offset,
|
||||
"limit": result.lines_read,
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
return ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="completed",
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content",
|
||||
content=TextContentBlock(
|
||||
type="text", text=cls.get_result_display(event).message
|
||||
),
|
||||
)
|
||||
],
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_output=result.model_dump_json(),
|
||||
locations=locations,
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
||||
async def _read_file(self, args: ReadFileArgs, file_path: Path) -> _ReadResult:
|
||||
client, session_id, _ = self._load_state()
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ from acp.schema import (
|
|||
|
||||
from vibe import VIBE_ROOT
|
||||
from vibe.acp.tools.base import AcpToolState, BaseAcpTool
|
||||
from vibe.acp.tools.session_update import (
|
||||
failed_tool_result,
|
||||
fallback_tool_call,
|
||||
resolve_kind,
|
||||
)
|
||||
from vibe.core.tools.base import BaseToolState, ToolError
|
||||
from vibe.core.tools.builtins.search_replace import (
|
||||
SearchReplace as CoreSearchReplaceTool,
|
||||
|
|
@ -75,18 +80,10 @@ class SearchReplace(CoreSearchReplaceTool, BaseAcpTool[AcpSearchReplaceState]):
|
|||
|
||||
@classmethod
|
||||
def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None:
|
||||
if not isinstance(event.args, SearchReplaceArgs):
|
||||
return fallback_tool_call(event, "search_replace")
|
||||
|
||||
args = event.args
|
||||
if args is None:
|
||||
return ToolCallStart(
|
||||
session_update="tool_call",
|
||||
title="search_replace",
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind="edit",
|
||||
content=None,
|
||||
raw_input=None,
|
||||
)
|
||||
if not isinstance(args, SearchReplaceArgs):
|
||||
return None
|
||||
|
||||
blocks = cls._parse_search_replace_blocks(args.content)
|
||||
|
||||
|
|
@ -94,7 +91,7 @@ class SearchReplace(CoreSearchReplaceTool, BaseAcpTool[AcpSearchReplaceState]):
|
|||
session_update="tool_call",
|
||||
title=cls.get_call_display(event).summary,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind="edit",
|
||||
kind=resolve_kind(event.tool_name),
|
||||
content=[
|
||||
FileEditToolCallContent(
|
||||
type="diff",
|
||||
|
|
@ -104,22 +101,18 @@ class SearchReplace(CoreSearchReplaceTool, BaseAcpTool[AcpSearchReplaceState]):
|
|||
)
|
||||
for block in blocks
|
||||
],
|
||||
locations=[ToolCallLocation(path=args.file_path)],
|
||||
locations=[ToolCallLocation(path=str(Path(args.file_path).resolve()))],
|
||||
raw_input=args.model_dump_json(),
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tool_result_session_update(cls, event: ToolResultEvent) -> SessionUpdate | None:
|
||||
if event.error:
|
||||
return ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="failed",
|
||||
)
|
||||
if failure := failed_tool_result(event, SearchReplaceResult):
|
||||
return failure
|
||||
|
||||
result = event.result
|
||||
if not isinstance(result, SearchReplaceResult):
|
||||
return None
|
||||
assert isinstance(result, SearchReplaceResult)
|
||||
|
||||
blocks = cls._parse_search_replace_blocks(result.content)
|
||||
|
||||
|
|
@ -127,6 +120,7 @@ class SearchReplace(CoreSearchReplaceTool, BaseAcpTool[AcpSearchReplaceState]):
|
|||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="completed",
|
||||
kind=resolve_kind(event.tool_name),
|
||||
content=[
|
||||
FileEditToolCallContent(
|
||||
type="diff",
|
||||
|
|
@ -136,6 +130,7 @@ class SearchReplace(CoreSearchReplaceTool, BaseAcpTool[AcpSearchReplaceState]):
|
|||
)
|
||||
for block in blocks
|
||||
],
|
||||
locations=[ToolCallLocation(path=result.file)],
|
||||
locations=[ToolCallLocation(path=str(Path(result.file).resolve()))],
|
||||
raw_output=result.model_dump_json(),
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
|
|
|||
79
vibe/acp/tools/builtins/skill.py
Normal file
79
vibe/acp/tools/builtins/skill.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from acp.helpers import SessionUpdate
|
||||
from acp.schema import (
|
||||
ContentToolCallContent,
|
||||
TextContentBlock,
|
||||
ToolCallLocation,
|
||||
ToolCallProgress,
|
||||
ToolCallStart,
|
||||
)
|
||||
|
||||
from vibe import VIBE_ROOT
|
||||
from vibe.acp.tools.base import (
|
||||
ToolCallSessionUpdateProtocol,
|
||||
ToolResultSessionUpdateProtocol,
|
||||
)
|
||||
from vibe.acp.tools.session_update import (
|
||||
failed_tool_result,
|
||||
fallback_tool_call,
|
||||
resolve_kind,
|
||||
)
|
||||
from vibe.core.tools.builtins.skill import (
|
||||
Skill as CoreSkillTool,
|
||||
SkillArgs,
|
||||
SkillResult,
|
||||
)
|
||||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||
|
||||
|
||||
class Skill(
|
||||
CoreSkillTool, ToolCallSessionUpdateProtocol, ToolResultSessionUpdateProtocol
|
||||
):
|
||||
prompt_path = VIBE_ROOT / "core" / "tools" / "builtins" / "prompts" / "skill.md"
|
||||
|
||||
@classmethod
|
||||
def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None:
|
||||
if not isinstance(event.args, SkillArgs):
|
||||
return fallback_tool_call(event, "skill")
|
||||
|
||||
return ToolCallStart(
|
||||
session_update="tool_call",
|
||||
title=cls.get_call_display(event).summary,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_input=event.args.model_dump_json(),
|
||||
field_meta={"tool_name": event.tool_name, "skill_name": event.args.name},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tool_result_session_update(cls, event: ToolResultEvent) -> SessionUpdate | None:
|
||||
if failure := failed_tool_result(event, SkillResult):
|
||||
return failure
|
||||
|
||||
result = event.result
|
||||
assert isinstance(result, SkillResult)
|
||||
|
||||
locations: list[ToolCallLocation] | None = None
|
||||
if result.skill_dir:
|
||||
locations = [ToolCallLocation(path=str(Path(result.skill_dir).resolve()))]
|
||||
|
||||
return ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="completed",
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content",
|
||||
content=TextContentBlock(
|
||||
type="text", text=cls.get_result_display(event).message
|
||||
),
|
||||
)
|
||||
],
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_output=result.model_dump_json(),
|
||||
locations=locations,
|
||||
field_meta={"tool_name": event.tool_name, "skill_name": result.name},
|
||||
)
|
||||
75
vibe/acp/tools/builtins/task.py
Normal file
75
vibe/acp/tools/builtins/task.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from acp.helpers import SessionUpdate
|
||||
from acp.schema import (
|
||||
ContentToolCallContent,
|
||||
TextContentBlock,
|
||||
ToolCallProgress,
|
||||
ToolCallStart,
|
||||
)
|
||||
|
||||
from vibe import VIBE_ROOT
|
||||
from vibe.acp.tools.base import (
|
||||
ToolCallSessionUpdateProtocol,
|
||||
ToolResultSessionUpdateProtocol,
|
||||
)
|
||||
from vibe.acp.tools.session_update import (
|
||||
failed_tool_result,
|
||||
fallback_tool_call,
|
||||
resolve_kind,
|
||||
)
|
||||
from vibe.core.tools.builtins.task import Task as CoreTaskTool, TaskArgs, TaskResult
|
||||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||
|
||||
|
||||
class Task(
|
||||
CoreTaskTool, ToolCallSessionUpdateProtocol, ToolResultSessionUpdateProtocol
|
||||
):
|
||||
prompt_path = VIBE_ROOT / "core" / "tools" / "builtins" / "prompts" / "task.md"
|
||||
|
||||
@classmethod
|
||||
def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None:
|
||||
if not isinstance(event.args, TaskArgs):
|
||||
return fallback_tool_call(event, "task")
|
||||
|
||||
return ToolCallStart(
|
||||
session_update="tool_call",
|
||||
title=cls.get_call_display(event).summary,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_input=event.args.model_dump_json(),
|
||||
field_meta={
|
||||
"tool_name": event.tool_name,
|
||||
"agent": event.args.agent,
|
||||
"task": event.args.task,
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tool_result_session_update(cls, event: ToolResultEvent) -> SessionUpdate | None:
|
||||
if failure := failed_tool_result(event, TaskResult):
|
||||
return failure
|
||||
|
||||
result = event.result
|
||||
assert isinstance(result, TaskResult)
|
||||
|
||||
return ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="completed" if result.completed else "failed",
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content",
|
||||
content=TextContentBlock(
|
||||
type="text", text=cls.get_result_display(event).message
|
||||
),
|
||||
)
|
||||
],
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_output=result.model_dump_json(),
|
||||
field_meta={
|
||||
"tool_name": event.tool_name,
|
||||
"turn_count": result.turns_used,
|
||||
"response": result.response,
|
||||
},
|
||||
)
|
||||
85
vibe/acp/tools/builtins/web_fetch.py
Normal file
85
vibe/acp/tools/builtins/web_fetch.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from acp.helpers import SessionUpdate
|
||||
from acp.schema import (
|
||||
ContentToolCallContent,
|
||||
TextContentBlock,
|
||||
ToolCallLocation,
|
||||
ToolCallProgress,
|
||||
ToolCallStart,
|
||||
)
|
||||
|
||||
from vibe import VIBE_ROOT
|
||||
from vibe.acp.tools.base import (
|
||||
ToolCallSessionUpdateProtocol,
|
||||
ToolResultSessionUpdateProtocol,
|
||||
)
|
||||
from vibe.acp.tools.session_update import (
|
||||
failed_tool_result,
|
||||
fallback_tool_call,
|
||||
resolve_kind,
|
||||
)
|
||||
from vibe.core.tools.builtins.webfetch import (
|
||||
WebFetch as CoreWebFetchTool,
|
||||
WebFetchArgs,
|
||||
WebFetchResult,
|
||||
)
|
||||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||
|
||||
|
||||
class WebFetch(
|
||||
CoreWebFetchTool, ToolCallSessionUpdateProtocol, ToolResultSessionUpdateProtocol
|
||||
):
|
||||
prompt_path = VIBE_ROOT / "core" / "tools" / "builtins" / "prompts" / "webfetch.md"
|
||||
|
||||
@classmethod
|
||||
def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None:
|
||||
if not isinstance(event.args, WebFetchArgs):
|
||||
return fallback_tool_call(event, "web_fetch")
|
||||
|
||||
url = cls._normalize_url(event.args.url)
|
||||
|
||||
return ToolCallStart(
|
||||
session_update="tool_call",
|
||||
title=cls.get_call_display(event).summary,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_input=event.args.model_dump_json(),
|
||||
locations=[ToolCallLocation(path=url, field_meta={"type": "url"})],
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tool_result_session_update(cls, event: ToolResultEvent) -> SessionUpdate | None:
|
||||
if failure := failed_tool_result(event, WebFetchResult):
|
||||
return failure
|
||||
|
||||
result = event.result
|
||||
assert isinstance(result, WebFetchResult)
|
||||
|
||||
return ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="completed",
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content",
|
||||
content=TextContentBlock(
|
||||
type="text", text=cls.get_result_display(event).message
|
||||
),
|
||||
)
|
||||
],
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_output=result.model_dump_json(),
|
||||
locations=[
|
||||
ToolCallLocation(
|
||||
path=result.url,
|
||||
field_meta={
|
||||
"type": "url",
|
||||
"char_count": len(result.content),
|
||||
"truncated": result.was_truncated,
|
||||
},
|
||||
)
|
||||
],
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
80
vibe/acp/tools/builtins/web_search.py
Normal file
80
vibe/acp/tools/builtins/web_search.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from acp.helpers import SessionUpdate
|
||||
from acp.schema import (
|
||||
ContentToolCallContent,
|
||||
TextContentBlock,
|
||||
ToolCallLocation,
|
||||
ToolCallProgress,
|
||||
ToolCallStart,
|
||||
)
|
||||
|
||||
from vibe import VIBE_ROOT
|
||||
from vibe.acp.tools.base import (
|
||||
ToolCallSessionUpdateProtocol,
|
||||
ToolResultSessionUpdateProtocol,
|
||||
)
|
||||
from vibe.acp.tools.session_update import (
|
||||
failed_tool_result,
|
||||
fallback_tool_call,
|
||||
resolve_kind,
|
||||
)
|
||||
from vibe.core.tools.builtins.websearch import (
|
||||
WebSearch as CoreWebSearchTool,
|
||||
WebSearchArgs,
|
||||
WebSearchResult,
|
||||
)
|
||||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||
|
||||
|
||||
class WebSearch(
|
||||
CoreWebSearchTool, ToolCallSessionUpdateProtocol, ToolResultSessionUpdateProtocol
|
||||
):
|
||||
prompt_path = VIBE_ROOT / "core" / "tools" / "builtins" / "prompts" / "websearch.md"
|
||||
|
||||
@classmethod
|
||||
def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None:
|
||||
if not isinstance(event.args, WebSearchArgs):
|
||||
return fallback_tool_call(event, "web_search")
|
||||
|
||||
return ToolCallStart(
|
||||
session_update="tool_call",
|
||||
title=cls.get_call_display(event).summary,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_input=event.args.model_dump_json(),
|
||||
field_meta={"tool_name": event.tool_name, "query": event.args.query},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tool_result_session_update(cls, event: ToolResultEvent) -> SessionUpdate | None:
|
||||
if failure := failed_tool_result(event, WebSearchResult):
|
||||
return failure
|
||||
|
||||
result = event.result
|
||||
assert isinstance(result, WebSearchResult)
|
||||
|
||||
locations = [
|
||||
ToolCallLocation(
|
||||
path=source.url, field_meta={"type": "url", "title": source.title}
|
||||
)
|
||||
for source in result.sources
|
||||
]
|
||||
|
||||
return ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="completed",
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content",
|
||||
content=TextContentBlock(
|
||||
type="text", text=cls.get_result_display(event).message
|
||||
),
|
||||
)
|
||||
],
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_output=result.model_dump_json(),
|
||||
locations=locations if locations else None,
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
|
@ -12,6 +12,11 @@ from acp.schema import (
|
|||
|
||||
from vibe import VIBE_ROOT
|
||||
from vibe.acp.tools.base import AcpToolState, BaseAcpTool
|
||||
from vibe.acp.tools.session_update import (
|
||||
failed_tool_result,
|
||||
fallback_tool_call,
|
||||
resolve_kind,
|
||||
)
|
||||
from vibe.core.tools.base import BaseToolState, ToolError
|
||||
from vibe.core.tools.builtins.write_file import (
|
||||
WriteFile as CoreWriteFileTool,
|
||||
|
|
@ -49,50 +54,40 @@ class WriteFile(CoreWriteFileTool, BaseAcpTool[AcpWriteFileState]):
|
|||
|
||||
@classmethod
|
||||
def tool_call_session_update(cls, event: ToolCallEvent) -> SessionUpdate | None:
|
||||
args = event.args
|
||||
if args is None:
|
||||
return ToolCallStart(
|
||||
session_update="tool_call",
|
||||
title="write_file",
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind="edit",
|
||||
content=None,
|
||||
raw_input=None,
|
||||
)
|
||||
if not isinstance(args, WriteFileArgs):
|
||||
return None
|
||||
if not isinstance(event.args, WriteFileArgs):
|
||||
return fallback_tool_call(event, "write_file")
|
||||
|
||||
return ToolCallStart(
|
||||
session_update="tool_call",
|
||||
title=cls.get_call_display(event).summary,
|
||||
title=cls.format_call_display(event.args).summary,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind="edit",
|
||||
kind=resolve_kind(event.tool_name),
|
||||
content=[
|
||||
FileEditToolCallContent(
|
||||
type="diff", path=args.path, old_text=None, new_text=args.content
|
||||
type="diff",
|
||||
path=event.args.path,
|
||||
old_text=None,
|
||||
new_text=event.args.content,
|
||||
)
|
||||
],
|
||||
locations=[ToolCallLocation(path=args.path)],
|
||||
raw_input=args.model_dump_json(),
|
||||
locations=[ToolCallLocation(path=str(Path(event.args.path).resolve()))],
|
||||
raw_input=event.args.model_dump_json(),
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tool_result_session_update(cls, event: ToolResultEvent) -> SessionUpdate | None:
|
||||
if event.error:
|
||||
return ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="failed",
|
||||
)
|
||||
if failure := failed_tool_result(event, WriteFileResult):
|
||||
return failure
|
||||
|
||||
result = event.result
|
||||
if not isinstance(result, WriteFileResult):
|
||||
return None
|
||||
assert isinstance(result, WriteFileResult)
|
||||
|
||||
return ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="completed",
|
||||
kind=resolve_kind(event.tool_name),
|
||||
content=[
|
||||
FileEditToolCallContent(
|
||||
type="diff",
|
||||
|
|
@ -101,6 +96,7 @@ class WriteFile(CoreWriteFileTool, BaseAcpTool[AcpWriteFileState]):
|
|||
new_text=result.content,
|
||||
)
|
||||
],
|
||||
locations=[ToolCallLocation(path=result.path)],
|
||||
locations=[ToolCallLocation(path=str(Path(result.path).resolve()))],
|
||||
raw_output=result.model_dump_json(),
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from acp.schema import (
|
|||
ToolCallStart,
|
||||
ToolKind,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
|
||||
from vibe.acp.tools.base import (
|
||||
ToolCallSessionUpdateProtocol,
|
||||
|
|
@ -17,16 +18,75 @@ 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] = {
|
||||
"grep": "search",
|
||||
|
||||
def _cancellation_raw_output(event: ToolResultEvent) -> str | None:
|
||||
if event.skip_reason:
|
||||
return TaggedText.from_string(event.skip_reason).message
|
||||
if event.error:
|
||||
return TaggedText.from_string(event.error).message
|
||||
return None
|
||||
|
||||
|
||||
TOOL_KIND_MAP: dict[str, ToolKind] = {
|
||||
"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",
|
||||
"grep": "search",
|
||||
"web_search": "search",
|
||||
"web_fetch": "fetch",
|
||||
"write_file": "edit",
|
||||
"search_replace": "edit",
|
||||
"bash": "execute",
|
||||
"skill": "read",
|
||||
}
|
||||
|
||||
|
||||
def resolve_kind(tool_name: str) -> ToolKind:
|
||||
return TOOL_KIND_MAP.get(tool_name, "other")
|
||||
|
||||
|
||||
def failed_tool_result(
|
||||
event: ToolResultEvent, expected_type: type[BaseModel]
|
||||
) -> ToolCallProgress | None:
|
||||
"""Return a failed ToolCallProgress if event is cancelled or has unexpected result type.
|
||||
|
||||
Returns None when the result is valid (caller handles the success path).
|
||||
"""
|
||||
kind = resolve_kind(event.tool_name)
|
||||
|
||||
if is_user_cancellation_event(event):
|
||||
return ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="failed",
|
||||
kind=kind,
|
||||
raw_output=_cancellation_raw_output(event),
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
||||
if not isinstance(event.result, expected_type):
|
||||
return ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status="failed",
|
||||
kind=kind,
|
||||
raw_output=event.error or event.skip_reason,
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def fallback_tool_call(event: ToolCallEvent, title: str) -> ToolCallStart:
|
||||
"""Default ToolCallStart when args are None or an unexpected type."""
|
||||
return ToolCallStart(
|
||||
session_update="tool_call",
|
||||
title=title,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_input=None,
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
||||
|
||||
def tool_call_session_update(event: ToolCallEvent) -> SessionUpdate | None:
|
||||
if issubclass(event.tool_class, ToolCallSessionUpdateProtocol):
|
||||
return event.tool_class.tool_call_session_update(event)
|
||||
|
|
@ -49,8 +109,9 @@ def tool_call_session_update(event: ToolCallEvent) -> SessionUpdate | None:
|
|||
title=display.summary,
|
||||
content=content,
|
||||
tool_call_id=event.tool_call_id,
|
||||
kind=TOOL_KIND.get(event.tool_name, "other"),
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_input=event.args.model_dump_json() if event.args else None,
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -116,6 +177,8 @@ def tool_result_session_update(event: ToolResultEvent) -> SessionUpdate | None:
|
|||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
status=tool_status,
|
||||
kind=resolve_kind(event.tool_name),
|
||||
raw_output=raw_output,
|
||||
content=content,
|
||||
field_meta={"tool_name": event.tool_name},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@ def create_tool_call_replay(
|
|||
tool_call_id=tool_call_id,
|
||||
kind="other",
|
||||
raw_input=arguments,
|
||||
field_meta={"tool_name": tool_name},
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue