CodeDocs Vault

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 = None

ToolUIData 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 permission

Tool 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_paths

Tool 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:

2. Read File Tool (read_file.py)

Reads UTF-8 files with offset and limit support.

Key Features:

3. Write File Tool (write_file.py)

Creates or overwrites files.

Key Features:

4. Search Replace Tool (search_replace.py)

Semantic code patching with fuzzy matching.

Key Features:

5. Grep Tool (grep.py)

Code search using ripgrep or GNU grep.

Key Features:

6. Todo Tool (todo.py)

Task list management.

Key Features:

7. Ask User Question Tool (ask_user_question.py)

Structured question/answer with the user.

Key Features:

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 = False

8. Task Tool (task.py)

Delegate work to a subagent for independent execution.

Key Features:

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 normally

Execution 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:

  1. ./.vibe/tools/ - Project-specific
  2. ~/.vibe/tools/ - User-global
  3. 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:

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