Victor Tools API Reference¶
This document provides comprehensive API documentation for creating and registering tools in Victor.
Table of Contents¶
- BaseTool Interface
- Tool Registration
- Parameter Schema
- Cost Tiers
- Access Modes
- Tool Implementation Patterns
- Tool Decorator
- Advanced Features
BaseTool Interface¶
All Victor tools inherit from the BaseTool abstract base class defined in victor/tools/base.py.
Required Attributes¶
from abc import ABC, abstractmethod
from typing import Any, Dict
class BaseTool(ABC):
@property
@abstractmethod
def name(self) -> str:
"""Tool name - unique identifier used for tool calls."""
pass
@property
@abstractmethod
def description(self) -> str:
"""Tool description - explains what the tool does."""
pass
@property
@abstractmethod
def parameters(self) -> Dict[str, Any]:
"""JSON Schema for tool parameters."""
pass
Optional Attributes¶
from victor.tools.enums import CostTier, AccessMode, DangerLevel, Priority, ExecutionCategory
class BaseTool(ABC):
@property
def cost_tier(self) -> CostTier:
"""Cost tier for budget-aware tool selection.
Default: CostTier.FREE
"""
return CostTier.FREE
@property
def is_idempotent(self) -> bool:
"""Whether tool execution is idempotent (same input = same output, no side effects).
Enables optimizations: caching, safe retries, parallel execution.
Default: False
"""
return False
@property
def metadata(self) -> Optional[ToolMetadata]:
"""Semantic metadata for tool selection.
Return None for auto-generation from tool properties.
"""
return None
The execute() Method¶
The core method every tool must implement:
from victor.tools.base import ToolResult
@abstractmethod
async def execute(self, _exec_ctx: Dict[str, Any], **kwargs: Any) -> ToolResult:
"""Execute the tool.
Args:
_exec_ctx: Framework execution context (reserved name to avoid collision
with tool parameters). Contains shared resources like code_manager.
**kwargs: Tool parameters as defined in the parameters schema.
Returns:
ToolResult with execution outcome.
"""
pass
ToolResult Return Type¶
All tools return a ToolResult object:
from pydantic import BaseModel, Field
from typing import Any, Dict, Optional
class ToolResult(BaseModel):
"""Result from tool execution."""
success: bool = Field(..., description="Whether execution succeeded")
output: Any = Field(..., description="Tool output data")
error: Optional[str] = Field(default=None, description="Error message if failed")
metadata: Optional[Dict[str, Any]] = Field(default=None, description="Additional metadata")
Example usage:
# Success case
return ToolResult(
success=True,
output={"files": ["main.py", "utils.py"]},
metadata={"count": 2}
)
# Error case
return ToolResult(
success=False,
output=None,
error="File not found: config.yaml",
metadata={"exception": "FileNotFoundError"}
)
Tool Registration¶
Tools are registered via the ToolRegistry class defined in victor/tools/registry.py.
ToolRegistry Class¶
register() Method¶
def register(self, *args, enabled: bool = True, **kwargs) -> None:
"""Register a tool - supports both signatures for flexibility.
Signatures:
register(tool) - Auto-extracts name from tool object (common case)
register(key, value) - Explicit key-value (LSP-compatible with BaseRegistry)
Args:
*args: Either (tool,) or (key, value)
enabled: Whether the tool is enabled by default (default: True)
Examples:
registry.register(my_tool) # Auto-extract name
registry.register("custom_name", my_tool) # Explicit name
registry.register(decorated_function) # From @tool decorator
"""
Tool State Management¶
# Enable/disable tools
registry.enable_tool("git") -> bool # Returns True if successful
registry.disable_tool("web_search") -> bool
# Check if tool is enabled
registry.is_tool_enabled("git") -> bool
# Batch state management
registry.set_tool_states({"git": True, "web_search": False})
registry.get_tool_states() -> Dict[str, bool]
Retrieving Tools¶
# Get single tool
tool = registry.get("git") -> Optional[BaseTool]
# List all tools
all_tools = registry.list_tools(only_enabled=True) -> List[BaseTool]
# Get JSON schemas for LLM
schemas = registry.get_tool_schemas(only_enabled=True) -> List[Dict[str, Any]]
Cost-Based Filtering¶
from victor.tools.enums import CostTier
# Get tools at or below a cost tier
cheap_tools = registry.get_tools_by_cost(max_tier=CostTier.LOW, only_enabled=True)
# Get cost summary
summary = registry.get_cost_summary(only_enabled=True)
# Returns: {"free": ["read", "ls", ...], "low": [...], "medium": [...], "high": [...]}
Hook Registration¶
Hooks allow you to intercept tool execution for logging, validation, or modification:
from victor.tools.registry import Hook
# Before-execution hook
def before_hook(tool_name: str, arguments: Dict[str, Any]) -> None:
print(f"Executing {tool_name} with {arguments}")
registry.register_before_hook(before_hook)
# After-execution hook
def after_hook(result: ToolResult) -> None:
if not result.success:
log_error(result.error)
registry.register_after_hook(after_hook)
# Critical hooks (failure blocks execution)
registry.register_before_hook(
validation_hook,
critical=True,
name="security_validation"
)
Hook Class¶
class Hook:
"""Tool execution hook with metadata."""
def __init__(
self,
callback: Callable,
name: str = "",
critical: bool = False,
description: str = "",
):
"""Initialize hook.
Args:
callback: The hook function to call
name: Human-readable name for the hook
critical: If True, hook failure blocks tool execution
description: Description of what the hook does
"""
Parameter Schema¶
Tool parameters are defined using JSON Schema format (Draft 7).
Basic Schema Structure¶
parameters = {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "File path to read"
},
"limit": {
"type": "integer",
"description": "Maximum lines to read",
"default": 100
},
"recursive": {
"type": "boolean",
"description": "Search recursively",
"default": False
}
},
"required": ["path"],
"additionalProperties": False # Reject unknown/hallucinated arguments
}
Type Definitions¶
| Python Type | JSON Schema Type |
|---|---|
str |
{"type": "string"} |
int |
{"type": "integer"} |
float |
{"type": "number"} |
bool |
{"type": "boolean"} |
List[str] |
{"type": "array", "items": {"type": "string"}} |
Dict[str, Any] |
{"type": "object"} |
Optional[str] |
{"type": "string"} (not in required) |
Enum Parameters¶
"operation": {
"type": "string",
"description": "Git operation to perform",
"enum": ["status", "diff", "stage", "commit", "log", "branch"]
}
Nested Objects¶
"options": {
"type": "object",
"properties": {
"verbose": {"type": "boolean"},
"format": {"type": "string", "enum": ["json", "text"]}
}
}
Array with Typed Items¶
"files": {
"type": "array",
"items": {"type": "string"},
"description": "List of file paths to process"
}
Complete Example¶
parameters = {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Directory path to search"
},
"pattern": {
"type": "string",
"description": "Glob pattern (e.g., '*.py')",
"default": "*"
},
"recursive": {
"type": "boolean",
"description": "Search recursively",
"default": False
},
"limit": {
"type": "integer",
"description": "Maximum results",
"default": 100
},
"types": {
"type": "array",
"items": {"type": "string", "enum": ["file", "dir"]},
"description": "Filter by type"
}
},
"required": ["path"],
"additionalProperties": False
}
Cost Tiers¶
The CostTier enum (from victor/tools/enums.py) enables cost-aware tool selection:
from victor.tools.enums import CostTier
class CostTier(Enum):
"""Cost tier for tools - used for budget-aware selection."""
FREE = "free" # weight: 0.0
LOW = "low" # weight: 1.0
MEDIUM = "medium" # weight: 2.0
HIGH = "high" # weight: 3.0
@property
def weight(self) -> float:
"""Return numeric weight for cost comparison."""
When to Use Each Tier¶
| Tier | Use Case | Examples |
|---|---|---|
FREE |
Local operations with no external costs | read, ls, grep, git status, shell |
LOW |
Compute-only operations (no API calls) | code_review, refactor_analysis, test_generation |
MEDIUM |
External API calls | web_search, web_fetch, slack_post |
HIGH |
Resource-intensive operations | batch_process (100+ files), full_codebase_scan |
Budget Implications¶
- Tools are deprioritized based on cost tier when cheaper alternatives exist
- The
get_tools_by_cost()method filters tools by maximum tier - Agent modes may restrict available cost tiers (e.g., EXPLORE mode limits HIGH tier)
Access Modes¶
The AccessMode enum defines what kind of system access a tool requires:
from victor.tools.enums import AccessMode
class AccessMode(Enum):
"""Tool access mode for approval tracking and security policies."""
READONLY = "readonly" # Only reads data, no side effects
WRITE = "write" # Modifies files or state
EXECUTE = "execute" # Runs external commands or code
NETWORK = "network" # Makes external network calls
MIXED = "mixed" # Multiple access types in same operation
Safety Properties¶
access_mode = AccessMode.WRITE
# Check if requires approval
access_mode.requires_approval # True for WRITE, EXECUTE, MIXED
# Check if safe (read-only)
access_mode.is_safe # True only for READONLY
# Get approval level (higher = stricter)
access_mode.approval_level()
# 0: Auto-approve (readonly)
# 1: Log only (network)
# 2: Confirm on first use (write)
# 3: Always confirm (execute, mixed)
Examples by Mode¶
| Mode | Examples | Approval |
|---|---|---|
READONLY |
read, ls, grep, code_search |
Auto-approve |
WRITE |
write, edit, create_file |
Confirm on first use |
EXECUTE |
shell, sandbox, run_tests |
Always confirm |
NETWORK |
web_search, web_fetch, api_call |
Log only |
MIXED |
git (read+write+network), docker |
Always confirm |
Mode Restrictions¶
In certain agent modes, tools with specific access modes may be restricted:
- EXPLORE mode: WRITE and EXECUTE tools restricted to sandbox
- PLAN mode: Only READONLY and NETWORK allowed
- BUILD mode: All access modes available
Tool Implementation Patterns¶
Pattern 1: Class-Based Tool (BaseTool)¶
For complex tools with state or multiple methods:
from typing import Any, Dict, Optional
from victor.tools.base import BaseTool, ToolResult, ToolMetadata
from victor.tools.enums import CostTier, AccessMode, DangerLevel
class CustomTool(BaseTool):
"""A custom tool implementation."""
@property
def name(self) -> str:
return "custom_tool"
@property
def description(self) -> str:
return "Performs custom operations on the codebase."
@property
def parameters(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"operation": {
"type": "string",
"description": "Operation to perform",
"enum": ["analyze", "transform", "validate"]
},
"target": {
"type": "string",
"description": "Target path or identifier"
},
"options": {
"type": "object",
"description": "Additional options"
}
},
"required": ["operation", "target"],
"additionalProperties": False
}
@property
def cost_tier(self) -> CostTier:
return CostTier.LOW
@property
def metadata(self) -> Optional[ToolMetadata]:
"""Provide explicit semantic metadata."""
return ToolMetadata(
category="analysis",
keywords=["analyze", "transform", "validate", "custom"],
use_cases=[
"analyzing code structure",
"transforming code patterns",
"validating configurations"
],
examples=[
"analyze the utils module",
"transform all deprecated calls",
"validate the config file"
],
priority_hints=["Use for custom analysis operations"]
)
async def execute(self, _exec_ctx: Dict[str, Any], **kwargs: Any) -> ToolResult:
operation = kwargs.get("operation")
target = kwargs.get("target")
options = kwargs.get("options", {})
try:
if operation == "analyze":
result = await self._analyze(target, options)
elif operation == "transform":
result = await self._transform(target, options)
elif operation == "validate":
result = await self._validate(target, options)
else:
return ToolResult(
success=False,
output=None,
error=f"Unknown operation: {operation}"
)
return ToolResult(
success=True,
output=result,
metadata={"operation": operation}
)
except Exception as e:
return ToolResult(
success=False,
output=None,
error=str(e),
metadata={"exception": type(e).__name__}
)
async def _analyze(self, target: str, options: Dict) -> Dict:
# Implementation...
pass
async def _transform(self, target: str, options: Dict) -> Dict:
# Implementation...
pass
async def _validate(self, target: str, options: Dict) -> Dict:
# Implementation...
pass
Pattern 2: Decorator-Based Tool (@tool)¶
For simpler tools, use the @tool decorator:
from typing import List, Optional, Dict, Any
from victor.tools.decorators import tool
from victor.tools.enums import CostTier, Priority, AccessMode, DangerLevel
@tool(
name="find_files",
cost_tier=CostTier.FREE,
category="filesystem",
priority=Priority.HIGH,
access_mode=AccessMode.READONLY,
danger_level=DangerLevel.SAFE,
keywords=["find", "search", "locate", "files"],
use_cases=[
"finding files by name pattern",
"locating specific files",
"searching for files recursively"
],
examples=[
"find all Python files",
"locate the config file",
"find files matching *.test.js"
],
stages=["initial", "planning", "reading"],
task_types=["search", "analysis"],
progress_params=["path", "pattern"],
)
async def find_files(
pattern: str,
path: str = ".",
recursive: bool = True,
limit: int = 100,
) -> List[Dict[str, Any]]:
"""Find files matching a pattern.
Args:
pattern: Glob pattern to match (e.g., '*.py', 'test_*.js')
path: Directory to search in (default: current directory)
recursive: Search recursively (default: True)
limit: Maximum results to return (default: 100)
Returns:
List of matching files with path, type, and size.
"""
from pathlib import Path
import fnmatch
results = []
base = Path(path).resolve()
for p in base.rglob("*") if recursive else base.glob("*"):
if fnmatch.fnmatch(p.name, pattern):
results.append({
"path": str(p.relative_to(base)),
"type": "directory" if p.is_dir() else "file",
"size": p.stat().st_size if p.is_file() else 0
})
if len(results) >= limit:
break
return results
Tool Decorator¶
The @tool decorator (from victor/tools/decorators.py) provides a concise way to define tools.
Decorator Parameters¶
@tool(
# Naming
name: Optional[str] = None, # Explicit tool name (overrides function name)
aliases: Optional[List[str]] = None, # Backward compatibility names
# Cost and Budget
cost_tier: CostTier = CostTier.FREE, # Cost tier for budget-aware selection
# Semantic Metadata
category: Optional[str] = None, # Tool category for grouping
keywords: Optional[List[str]] = None, # Keywords for semantic matching
use_cases: Optional[List[str]] = None,# High-level use cases
examples: Optional[List[str]] = None, # Example requests
priority_hints: Optional[List[str]] = None, # Usage hints
# Selection and Approval
priority: Priority = Priority.MEDIUM, # Selection priority
access_mode: AccessMode = AccessMode.READONLY, # Access type
danger_level: DangerLevel = DangerLevel.SAFE, # Danger level
# Stage and Task Affinity
stages: Optional[List[str]] = None, # Conversation stages where relevant
task_types: Optional[List[str]] = None, # Task types for classification
# Advanced
mandatory_keywords: Optional[List[str]] = None, # Keywords that force inclusion
progress_params: Optional[List[str]] = None, # Params for loop detection
execution_category: Optional[str] = None, # Parallel execution category
availability_check: Optional[Callable[[], bool]] = None, # Dynamic availability
)
Priority Levels¶
from victor.tools.enums import Priority
class Priority(Enum):
CRITICAL = 1 # Always available (read, ls, grep, shell)
HIGH = 2 # Most tasks (write, edit, git)
MEDIUM = 3 # Task-specific (docker, db, test)
LOW = 4 # Specialized (batch, scaffold)
CONTEXTUAL = 5 # Based on task classification
Danger Levels¶
from victor.tools.enums import DangerLevel
class DangerLevel(Enum):
SAFE = "safe" # No risk of data loss
LOW = "low" # Minor risk, easily reversible
MEDIUM = "medium" # Moderate risk, may require effort to reverse
HIGH = "high" # Significant risk, difficult to reverse
CRITICAL = "critical" # Irreversible or system-wide impact
Conversation Stages¶
Tools can specify which conversation stages they're most relevant for:
initial- First interaction, exploring the requestplanning- Understanding scope, searching for filesreading- Examining files, gathering contextanalysis- Reviewing code, analyzing structureexecution- Making changes, running commandsverification- Testing, validating changescompletion- Summarizing, wrapping up
Execution Categories¶
For parallel execution planning:
from victor.tools.enums import ExecutionCategory
class ExecutionCategory(Enum):
READ_ONLY = "read_only" # Pure reads, safe to parallelize
WRITE = "write" # File modifications, may conflict
COMPUTE = "compute" # CPU-intensive but isolated
NETWORK = "network" # External calls, rate-limited
EXECUTE = "execute" # Shell commands, may have side effects
MIXED = "mixed" # Multiple categories
Advanced Features¶
ToolMetadata¶
Semantic metadata for tool selection and discovery:
from victor.tools.metadata import ToolMetadata
from victor.tools.enums import Priority, AccessMode, DangerLevel, ExecutionCategory
metadata = ToolMetadata(
category="filesystem",
keywords=["read", "file", "content", "view"],
use_cases=["reading source files", "viewing configuration"],
examples=["read main.py", "show config.yaml contents"],
priority_hints=["Use for TEXT files only", "Use ls first to check size"],
priority=Priority.CRITICAL,
access_mode=AccessMode.READONLY,
danger_level=DangerLevel.SAFE,
stages=["reading", "initial", "analysis"],
mandatory_keywords=["read file", "show file"],
task_types=["analysis", "search"],
progress_params=["path", "offset", "limit"],
execution_category=ExecutionCategory.READ_ONLY
)
ToolMetadataRegistry¶
Centralized registry for tool metadata:
from victor.tools.metadata_registry import ToolMetadataRegistry
# Get singleton instance
registry = ToolMetadataRegistry.get_instance()
# Refresh from tools (smart reindexing)
if registry.needs_reindex(tools):
registry.refresh_from_tools(tools)
# Access metadata
metadata = registry.get_metadata("git")
tools_in_category = registry.get_tools_by_category("filesystem")
tools_with_keyword = registry.get_by_keyword("search")
# Search across all metadata
matches = registry.get_tools_matching_text("file")
# Get statistics
stats = registry.get_statistics()
# Returns: {
# "total_tools": 55,
# "tools_with_explicit_metadata": 42,
# "tools_with_auto_metadata": 13,
# "total_categories": 12,
# "total_keywords": 234,
# "categories": ["filesystem", "git", "web", ...]
# }
Parameter Validation¶
Tools automatically validate parameters against their schema:
# Simple validation (returns bool)
is_valid = tool.validate_parameters(path="/tmp/test.txt", limit=100)
# Detailed validation (returns ToolValidationResult)
result = tool.validate_parameters_detailed(path="/tmp/test.txt", limit="invalid")
if not result.valid:
print(f"Errors: {result.errors}")
# ["Parameter 'limit' has wrong type: expected integer, got str"]
print(f"Invalid params: {result.invalid_params}")
# {"limit": "type: expected integer"}
Schema Levels¶
Control schema verbosity for LLM context management:
from victor.tools.enums import SchemaLevel
# FULL: Complete schema (~100-150 tokens)
full_schema = tool.to_schema(SchemaLevel.FULL)
# COMPACT: All params, shorter descriptions (~60-80 tokens)
compact_schema = tool.to_schema(SchemaLevel.COMPACT)
# STUB: Required params only (~25-40 tokens)
stub_schema = tool.to_schema(SchemaLevel.STUB)
Availability Checks¶
For tools requiring external configuration:
def is_slack_configured() -> bool:
"""Check if Slack API token is available."""
import os
return bool(os.getenv("SLACK_API_TOKEN"))
@tool(
name="slack_post",
availability_check=is_slack_configured,
)
async def slack_post(channel: str, message: str):
"""Post a message to Slack."""
# ...
Tools with availability checks are excluded from tool selection when unavailable.
Tool Aliases¶
Support backward compatibility with renamed tools:
@tool(
name="shell", # Canonical name
aliases=["execute_bash"], # Legacy names that still work
)
async def shell(command: str):
"""Execute a shell command."""
# ...
Example: Complete Tool Implementation¶
Here's a complete example showing all features:
from typing import Any, Dict, List, Optional
from victor.tools.decorators import tool
from victor.tools.enums import (
CostTier,
Priority,
AccessMode,
DangerLevel,
ExecutionCategory,
)
@tool(
name="code_metrics",
cost_tier=CostTier.LOW,
category="analysis",
priority=Priority.MEDIUM,
access_mode=AccessMode.READONLY,
danger_level=DangerLevel.SAFE,
keywords=[
"metrics",
"complexity",
"lines of code",
"loc",
"cyclomatic",
"analyze",
],
use_cases=[
"analyzing code complexity",
"measuring code metrics",
"calculating lines of code",
"assessing code quality",
],
examples=[
"show metrics for main.py",
"calculate complexity of utils module",
"how many lines of code in src/",
],
priority_hints=[
"Use for code quality assessment",
"Supports Python, JavaScript, TypeScript",
],
stages=["analysis", "verification"],
task_types=["analysis"],
progress_params=["path", "metric_type"],
execution_category="compute",
mandatory_keywords=["code metrics", "cyclomatic complexity"],
)
async def code_metrics(
path: str,
metric_type: str = "all",
recursive: bool = True,
context: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Calculate code metrics for files or directories.
Computes various code quality metrics including lines of code,
cyclomatic complexity, and maintainability index.
Args:
path: File or directory path to analyze.
metric_type: Type of metrics to calculate.
Options: "loc" (lines of code), "complexity", "all".
Default: "all"
recursive: Analyze subdirectories recursively. Default: True.
context: Tool execution context (injected by framework).
Returns:
Dictionary containing:
- files_analyzed: Number of files processed
- total_lines: Total lines of code
- complexity: Average cyclomatic complexity
- metrics_by_file: Per-file breakdown
"""
from pathlib import Path
target = Path(path).resolve()
if not target.exists():
return {"error": f"Path not found: {path}"}
files = []
if target.is_file():
files = [target]
else:
pattern = "**/*" if recursive else "*"
files = [f for f in target.glob(pattern) if f.is_file()]
results = {
"files_analyzed": len(files),
"total_lines": 0,
"complexity": 0.0,
"metrics_by_file": {}
}
for file in files:
if file.suffix in {".py", ".js", ".ts"}:
try:
content = file.read_text()
lines = len(content.splitlines())
results["total_lines"] += lines
results["metrics_by_file"][str(file)] = {
"lines": lines,
"complexity": 1.0 # Placeholder
}
except Exception:
pass
if results["metrics_by_file"]:
avg_complexity = sum(
m["complexity"] for m in results["metrics_by_file"].values()
) / len(results["metrics_by_file"])
results["complexity"] = round(avg_complexity, 2)
return results
See Also¶
- CLAUDE.md - Project conventions and tool patterns
- victor/tools/base.py - BaseTool implementation
- victor/tools/registry.py - ToolRegistry implementation
- victor/tools/decorators.py - @tool decorator
- victor/tools/enums.py - CostTier, AccessMode, Priority enums
- victor/tools/metadata.py - ToolMetadata and registry