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:
Mathias Gesbert 2026-04-29 17:20:27 +02:00 committed by GitHub
parent 632ea8c032
commit 1fd7eea289
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 3482 additions and 301 deletions

View file

@ -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

View file

@ -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},
)

View 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},
)

View file

@ -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()

View file

@ -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},
)

View 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},
)

View 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,
},
)

View 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},
)

View 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},
)

View file

@ -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},
)

View file

@ -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},
)

View file

@ -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},
)