Tool System Architecture
The tool system is a plugin-based architecture that allows extensibility through built-in, custom, and MCP (Model Context Protocol) tools.
Base Tool Class
File: vibe/core/tools/base.py
Class: BaseTool at line 93
Generic Type Parameters
# base.py:93-98
class BaseTool[
ToolArgs: BaseModel, # Input arguments schema
ToolResult: BaseModel, # Output result schema
ToolConfig: BaseToolConfig, # Tool configuration
ToolState: BaseToolState, # Persistent state
](ABC):Abstract Interface
@abstractmethod
async def run(self, args: ToolArgs, ctx: InvokeContext | None = None) -> AsyncGenerator[ToolStreamEvent | ToolResult, None]:
"""Invoke the tool. Yields ToolStreamEvent items and a final ToolResult."""
...InvokeContext
File: vibe/core/tools/base.py:35
@dataclass
class InvokeContext:
"""Context passed to tools during invocation."""
tool_call_id: str
approval_callback: ApprovalCallback | None = None
agent_manager: AgentManager | None = None
user_input_callback: UserInputCallback | None = NoneToolUIData Protocol
File: vibe/core/tools/ui.py:23
@runtime_checkable
class ToolUIData[TArgs, TResult](Protocol):
@classmethod
def get_call_display(cls, event: ToolCallEvent) -> ToolCallDisplay: ...
@classmethod
def get_result_display(cls, event: ToolResultEvent) -> ToolResultDisplay: ...
@classmethod
def get_status_text(cls) -> str: ...Tools can implement ToolUIData to customize how they appear in the TUI.
Key Methods
| Method | Line | Purpose |
|---|---|---|
run() |
111 | Abstract - execute tool logic |
invoke() |
137 | Validate args and call run() |
get_tool_prompt() |
116 | Load .md prompt file |
get_parameters() |
237 | Return JSON schema for args |
get_name() |
260 | Return snake_case tool name |
from_config() |
151 | Factory to create instance |
check_allowlist_denylist() |
273 | Pattern-based permission check |
Tool Execution Flow
Agent calls tool
│
▼
invoke(**raw_args) [base.py:137]
│
├─1─► Validate args against schema [base.py:141-147]
│ args_model.model_validate(raw)
│
└─2─► Call run(args) [base.py:149]
Abstract method implemented by concrete tools
Tool Configuration
Class: BaseToolConfig at base.py:54
# base.py:54-84
class BaseToolConfig(BaseModel):
model_config = ConfigDict(extra="allow") # Allow tool-specific fields
permission: ToolPermission = ToolPermission.ASK
workdir: Path | None = Field(default=None, exclude=True)
allowlist: list[str] = Field(default_factory=list)
denylist: list[str] = Field(default_factory=list)
@property
def effective_workdir(self) -> Path:
return self.workdir if self.workdir is not None else Path.cwd()Permission Levels
Enum: ToolPermission at base.py:39
# base.py:39-51
class ToolPermission(StrEnum):
ALWAYS = auto() # Auto-execute without asking
NEVER = auto() # Block execution
ASK = auto() # Ask user for permissionTool Manager
File: vibe/core/tools/manager.py
Class: ToolManager at line 37
Initialization
ToolManager.__init__(config) [manager.py:44]
│
├─1─► Compute search paths [manager.py:47]
│ _compute_search_paths(config)
│
├─2─► Discover tool classes [manager.py:49-51]
│ _available = {cls.get_name(): cls for cls in _iter_tool_classes(paths)}
│
└─3─► Integrate MCP tools [manager.py:52]
_integrate_mcp()
Search Path Resolution
Method: _compute_search_paths() at manager.py:54
# manager.py:54-81
@staticmethod
def _compute_search_paths(config: VibeConfig) -> list[Path]:
paths: list[Path] = [DEFAULT_TOOL_DIR] # Built-in tools
# 1. Config-specified paths
for p in config.tool_paths:
path = Path(p).expanduser().resolve()
if path.is_dir():
paths.append(path)
# 2. Project-local .vibe/tools/ (trust-aware)
if (local_tools := resolve_local_tools_dir(cwd)) is not None:
paths.append(local_tools)
# 3. Global ~/.vibe/tools/
global_tools = get_vibe_home() / "tools"
if global_tools.is_dir():
paths.append(global_tools)
return unique_pathsTool Discovery
Method: _iter_tool_classes() at manager.py:83
_iter_tool_classes(search_paths) [manager.py:83-116]
│
for each path in search_paths:
│
├─► Find all .py files (recursive) [manager.py:89]
│ for path in base.rglob("*.py"):
│
├─► Skip files starting with _ [manager.py:93-94]
│
├─► Dynamic import module [manager.py:96-107]
│ spec = importlib.util.spec_from_file_location(...)
│ module = importlib.util.module_from_spec(spec)
│ spec.loader.exec_module(module)
│
└─► Find BaseTool subclasses [manager.py:109-116]
for obj in vars(module).values():
if issubclass(obj, BaseTool) and not isabstract(obj):
yield obj
Getting Tool Instances
Method: get() at manager.py:253
# manager.py:253-270
def get(self, tool_name: str) -> BaseTool:
"""Get a tool instance, creating it lazily on first call."""
# Check cache first
if tool_name in self._instances:
return self._instances[tool_name]
# Verify tool exists
if tool_name not in self._available:
raise NoSuchToolError(f"Unknown tool: {tool_name}")
# Create instance
tool_class = self._available[tool_name]
tool_config = self.get_tool_config(tool_name)
self._instances[tool_name] = tool_class.from_config(tool_config)
return self._instances[tool_name]Tool Configuration Resolution
Method: get_tool_config() at manager.py:232
get_tool_config(tool_name) [manager.py:232-251]
│
├─1─► Get tool's config class [manager.py:233-240]
│ tool_class._get_tool_config_class()
│
├─2─► Get default config [manager.py:237-240]
│ default_config = config_class()
│
├─3─► Merge with user overrides [manager.py:242-246]
│ user_overrides = self._config.tools.get(tool_name)
│ merged = {**defaults, **overrides}
│
└─4─► Apply global workdir if set [manager.py:248-249]
if config.workdir: merged["workdir"] = config.workdir
MCP Tool Integration
File: vibe/core/tools/mcp.py
MCP allows connecting to external tool servers via HTTP or stdio.
MCP Integration Flow
_integrate_mcp() [manager.py:141]
│
├─► _integrate_mcp_async() [manager.py:146]
│
for each server in config.mcp_servers:
│
├─── HTTP transport [manager.py:154]:
│ _register_http_server(srv)
│
└─── Stdio transport [manager.py:156]:
_register_stdio_server(srv)
HTTP Server Registration
Method: _register_http_server() at manager.py:169
_register_http_server(srv) [manager.py:169-201]
│
├─1─► Get URL and headers [manager.py:170-175]
│
├─2─► Discover available tools [manager.py:177]
│ tools = await list_tools_http(url, headers)
│
└─3─► Create proxy classes [manager.py:183-193]
for remote in tools:
proxy_cls = create_mcp_http_proxy_tool_class(
url, remote, alias, server_hint, headers
)
self._available[proxy_cls.get_name()] = proxy_cls
Stdio Server Registration
Method: _register_stdio_server() at manager.py:203
Similar to HTTP but uses subprocess communication.
Built-in Tools
Located in vibe/core/tools/builtins/:
1. Bash Tool (bash.py)
Executes shell commands in a stateful terminal session.
Key Features:
- Cross-platform (Windows/Unix)
- Process tree management
- Signal handling
- Streaming output
- Configurable timeout (default 60s)
2. Read File Tool (read_file.py)
Reads UTF-8 files with offset and limit support.
Key Features:
- 64KB default read limit
- Line offset and limit parameters
- Tracks recently-read files in state
3. Write File Tool (write_file.py)
Creates or overwrites files.
Key Features:
- Append mode support
- Automatic directory creation
4. Search Replace Tool (search_replace.py)
Semantic code patching with fuzzy matching.
Key Features:
- Fuzzy matching (tolerates minor differences)
- Multi-block editing
- YAML/JSON support with reformatting
- Diff preview
5. Grep Tool (grep.py)
Code search using ripgrep or GNU grep.
Key Features:
- Ripgrep backend (preferred)
- Configurable exclude patterns
- Custom
.vibeignoresupport - Max output/match limits
6. Todo Tool (todo.py)
Task list management.
Key Features:
- Status tracking (pending/in_progress/completed)
- Persistent state
- UI integration
7. Ask User Question Tool (ask_user_question.py)
Structured question/answer with the user.
Key Features:
- 1-4 questions per invocation
- 2-4 choices per question with automatic "Other" free-text option
- Multi-select support
- Short headers for tab-style display
- Always-allowed permission (no approval needed)
- Requires interactive UI via
user_input_callbackinInvokeContext
Models:
class Question(BaseModel):
question: str
header: str # Short label (max 12 chars)
options: list[Choice] # 2-4 choices
multi_select: bool = False
class AskUserQuestionResult(BaseModel):
answers: list[Answer]
cancelled: bool = False8. Task Tool (task.py)
Delegate work to a subagent for independent execution.
Key Features:
- Spawns a new
AgentLoopfor the delegated task - Agent parameter defaults to
"explore"(read-only subagent) - Security constraint: only
AgentType.SUBAGENTprofiles allowed - Streams
ToolStreamEventitems with subagent progress - Session logging disabled for subagent runs
- Inherits approval callback from parent context
Models:
class TaskArgs(BaseModel):
task: str # Task description for subagent
agent: str = "explore" # Agent profile name (must be subagent)
class TaskResult(BaseModel):
response: str # Accumulated text from subagent
turns_used: int # LLM turns consumed
completed: bool # Whether task completed normallyExecution Flow:
Task.run(args, ctx) [task.py:90]
│
├─1─► Validate agent_manager exists in context
│
├─2─► Look up agent profile [task.py:99]
│ agent_manager.get_agent(args.agent)
│
├─3─► Verify AgentType.SUBAGENT [task.py:103-108]
│ Reject non-subagent profiles (security)
│
├─4─► Create subagent AgentLoop [task.py:110-113]
│ AgentLoop(config, agent_name=args.agent)
│ Session logging disabled
│
├─5─► Run subagent.act(args.task) [task.py:121]
│ │
│ ├── Accumulate AssistantEvent content
│ └── Yield ToolStreamEvent for tool results
│
└─6─► Yield TaskResult with response, turns, status
Creating Custom Tools
Basic Structure
from pydantic import BaseModel
from vibe.core.tools.base import BaseTool, BaseToolConfig, BaseToolState
class MyToolArgs(BaseModel):
"""Input arguments."""
input_text: str
class MyToolResult(BaseModel):
"""Output result."""
output_text: str
class MyToolConfig(BaseToolConfig):
"""Tool configuration."""
custom_option: str = "default"
class MyToolState(BaseToolState):
"""Persistent state."""
call_count: int = 0
class MyTool(BaseTool[MyToolArgs, MyToolResult, MyToolConfig, MyToolState]):
description = "Description shown to LLM"
async def run(self, args: MyToolArgs) -> MyToolResult:
self.state.call_count += 1
return MyToolResult(output_text=f"Processed: {args.input_text}")Placement
Custom tools can be placed in:
./.vibe/tools/- Project-specific~/.vibe/tools/- User-global- Paths specified in
config.tool_paths
Tool Prompts
Create a .md file alongside the tool for usage hints:
my_tool.py
prompts/
my_tool.md # Tool usage instructions for LLM
Loaded via get_tool_prompt() at base.py:116-135.
Tool Enable/Disable Patterns
Config fields in VibeConfig:
# config.py:334-350
enabled_tools: list[str] # Explicit whitelist
disabled_tools: list[str] # Blacklist (ignored if enabled_tools set)Supports:
- Exact names:
"bash" - Glob patterns:
"mcp_*" - Regex:
"re:^serena_.*"
Tool Permission Flow
Agent requests tool execution
│
▼
_should_execute_tool() [agent_loop.py]
│
├─1─► If auto_approve: EXECUTE
│
├─2─► Check allowlist/denylist patterns
│ tool.check_allowlist_denylist(args)
│ │
│ ├── Match allowlist: EXECUTE
│ └── Match denylist: SKIP
│
├─3─► Check tool permission config
│ tool_manager.get_tool_config(name).permission
│ │
│ ├── ALWAYS: EXECUTE
│ └── NEVER: SKIP
│
└─4─► ASK: Request user approval
_ask_approval()
│
└── ApprovalCallback → User decision
Source File References
| File | Key Lines | Description |
|---|---|---|
base.py:17-19 |
ToolError |
Tool execution error |
base.py:39-51 |
ToolPermission |
Permission enum |
base.py:54-84 |
BaseToolConfig |
Tool configuration base |
base.py:87-91 |
BaseToolState |
Tool state base |
base.py:93-283 |
BaseTool |
Abstract tool base class |
base.py:111-114 |
run() |
Abstract execution method |
base.py:137-149 |
invoke() |
Validation and execution |
base.py:116-135 |
get_tool_prompt() |
Load prompt file |
base.py:237-258 |
get_parameters() |
JSON schema generation |
manager.py:37-273 |
ToolManager |
Tool discovery and management |
manager.py:54-81 |
_compute_search_paths() |
Search path resolution |
manager.py:83-116 |
_iter_tool_classes() |
Dynamic tool discovery |
manager.py:141-167 |
_integrate_mcp() |
MCP tool integration |
manager.py:232-251 |
get_tool_config() |
Config resolution |
manager.py:253-270 |
get() |
Instance retrieval |