Standard Tools
Agent Air provides a comprehensive set of built-in tools for file operations, command execution, search, and user interaction. These tools are ready to use and can be registered with your agent.
File Operations
The file operation tools provide safe, controlled access to the filesystem. These tools handle reading, writing, and editing files with built-in permission checks, binary file detection, and support for large files through pagination.
read_file
Reads files from the local filesystem with pagination support.
Input Schema:
{
"file_path": "string (required) - Absolute path to the file",
"offset": "integer - Line number to start from (0-based, default: 0)",
"limit": "integer - Maximum lines to read (default: 2000)"
}
Files are read asynchronously and returned with 1-based line numbering. By default, up to 2000 lines are read starting from the beginning. Each line is truncated to 2000 characters if longer, and total output is capped at 50KB.
Before reading begins, binary file detection runs through a two-stage process. First, the file extension is checked against known binary formats (.zip, .exe, .dll, .png, .jpg, .pdf, .doc, .mp3, .mp4, and others). If that check passes, the first 4KB of content is examined for null bytes (a strong indicator of binary data) and whether more than 30% of characters are non-printable. Binary files are rejected with a clear message explaining the format limitation.
When a file isn’t found, similar files are located and up to three alternatives are suggested. Only absolute paths are accepted - relative paths are rejected. If the path points to a directory, you’ll be directed to use ls instead. Read permission is required before accessing the file.
Output includes position information showing the total line count. If more content exists beyond the read limit, you’ll see “Use ‘offset’ parameter to read beyond line N”. When the entire file has been read, it shows “(End of file)”.
Example:
{
"file_path": "/home/user/project/src/main.rs",
"offset": 0,
"limit": 100
}
write_file
Writes content to a file, creating or overwriting as needed.
Input Schema:
{
"file_path": "string (required) - Absolute path to the file",
"content": "string (required) - Content to write",
"create_directories": "boolean - Create parent directories (default: true)"
}
Content is written to the specified path, creating or overwriting the file as needed. When create_directories is true (the default), any missing parent directories are automatically created using recursive directory creation. If directory creation fails, the operation stops with a specific error message.
Before writing, a check determines whether the file already exists - this distinguishes between “Create” and “Overwrite” operations. This distinction appears in the permission request so users understand what action will occur. The permission request also includes the content size in bytes.
Only absolute paths are accepted; relative paths are rejected with a clear error message. Write permission must be granted before the operation proceeds. On success, the exact number of bytes written is reported.
Example:
{
"file_path": "/home/user/project/config.yaml",
"content": "database:\n host: localhost\n port: 5432",
"create_directories": true
}
edit_file
Performs find-and-replace operations with optional fuzzy matching.
Input Schema:
{
"file_path": "string (required) - Absolute path to the file",
"old_string": "string (required) - String to find",
"new_string": "string (required) - Replacement string",
"replace_all": "boolean - Replace all occurrences (default: false)",
"fuzzy_match": "boolean - Enable fuzzy matching (default: false)",
"fuzzy_threshold": "number - Similarity threshold 0.0-1.0 (default: 0.7)"
}
Find-and-replace operations use a three-stage matching process. First, exact string matching is attempted. If that fails and fuzzy matching is enabled, whitespace-normalized matching is tried (where all whitespace sequences are collapsed to single spaces). If that also fails, fuzzy matching using Levenshtein distance runs with a sliding window over file lines.
Fuzzy matching calculates a similarity score between 0.0 and 1.0. Exact matches score 1.0, whitespace-insensitive matches score 0.95, and fuzzy matches score based on string similarity. Matches below the configured threshold (default 0.7) are rejected. When a fuzzy match succeeds, the similarity percentage is reported.
By default, only the first occurrence is replaced. Set replace_all to true to replace all occurrences. No-op operations where old_string and new_string are identical are rejected.
An absolute path is required, and the file must exist before permission is requested. If the search string isn’t found, the error message includes a truncated version (first 50 characters) of what was searched for. Write permission is required before making changes.
Example:
{
"file_path": "/home/user/project/src/lib.rs",
"old_string": "fn old_function(",
"new_string": "fn new_function(",
"replace_all": false
}
multi_edit
Performs multiple find-and-replace operations atomically. All edits are validated before application - all succeed or none do.
Input Schema:
{
"file_path": "string (required) - Absolute path to the file",
"edits": "array (required) - Array of edit objects"
}
Each edit object:
{
"old_string": "string (required)",
"new_string": "string (required)",
"replace_all": "boolean (default: false)",
"fuzzy_match": "boolean (default: false)",
"fuzzy_threshold": "number (default: 0.7)"
}
All edits are validated before any changes are applied, ensuring atomicity. If any edit fails to find its target string, the entire operation fails and the file remains unchanged. This prevents partial modifications that could leave files in inconsistent states.
Edits are applied in reverse byte position order to prevent position shift issues. When one edit changes file content, it could shift the positions of subsequent edits. By processing from end to beginning, earlier edits don’t affect the positions of later ones.
Overlapping edit regions are detected and rejected - if any edits would modify the same portion of the file, the operation fails. Overlap detection checks whether any edit’s start position falls within another edit’s range.
A maximum of 50 edits can be performed in a single operation. Dry-run mode is available, which plans all edits without writing changes. Dry-run reports the number of edits that would apply, the count of fuzzy matches with average similarity, and the byte size difference between original and modified content.
Each individual edit supports the same options as the single edit operation: replace_all, fuzzy_match, and fuzzy_threshold. An absolute path and write permission covering the entire operation are required.
Example:
{
"file_path": "/home/user/project/src/main.rs",
"edits": [
{ "old_string": "use old_crate;", "new_string": "use new_crate;" },
{ "old_string": "old_crate::init()", "new_string": "new_crate::init()" }
]
}
Directory Operations
Directory operation tools help navigate and search codebases efficiently. These tools support pattern matching, filtering, and sorting to quickly locate files and content across large projects.
ls
Lists directory contents with optional detailed information, filtering, and sorting.
Input Schema:
{
"path": "string (required) - Absolute path to directory",
"long_format": "boolean - Show detailed info (default: false)",
"include_hidden": "boolean - Include dot files (default: false)",
"pattern": "string - Glob filter (e.g., '*.rs')",
"sort_by": "string - 'name', 'size', or 'modified' (default: 'name')",
"reverse": "boolean - Reverse sort order (default: false)",
"directories_first": "boolean - List dirs before files (default: true)"
}
Directory contents are listed with configurable output format and filtering. In long format mode, each entry shows Unix-style permissions, human-readable file size (KB/MB/GB), modification timestamp, and the filename. Directories are marked with a trailing ”/” suffix.
Sorting follows a two-level process. When directories_first is enabled (the default), directories are grouped before files. Within each group, entries are sorted by the specified criterion: name (alphabetical), size (bytes), or modified (timestamp). The reverse flag inverts the sort order.
Use the pattern parameter to filter entries with glob syntax. Only entries matching the pattern appear in output. Hidden files (those starting with ”.”) are excluded by default but can be included with include_hidden.
Results are limited to 1000 entries by default to prevent overwhelming output for large directories. When the limit is reached, output indicates how many entries were shown versus the total count with a ”… and N more entries” note.
Permission display follows Unix conventions, showing read/write/execute flags for owner, group, and others (e.g., “-rw-r—r—”). On Windows, simplified permissions based on read-only status are shown.
An absolute path to a directory is required. If the path points to a file, you’ll be directed to use read_file instead. Read permission is required.
Example:
{
"path": "/home/user/project/src",
"long_format": true,
"pattern": "*.rs",
"sort_by": "modified"
}
glob
Fast file pattern matching that works with any codebase size. Returns paths sorted by modification time.
Input Schema:
{
"pattern": "string (required) - Glob pattern (e.g., '**/*.rs')",
"path": "string - Directory to search (default: current directory)",
"limit": "integer - Max results (default: 1000)",
"include_hidden": "boolean - Include hidden files (default: false)"
}
Efficient glob pattern compilation matches files across directory trees. Pattern matching supports standard glob syntax: * matches any characters except path separators, ** matches recursively including path separators, ? matches a single character, [abc] matches character classes, [!abc] matches negated classes, and {a,b} matches alternations.
Directory trees are walked recursively, skipping symbolic links to avoid infinite loops. Hidden files and directories (those starting with ”.”) are excluded by default. When a hidden directory is encountered, the entire subtree is skipped unless include_hidden is enabled.
Matching is performed against relative paths from the search root, but results are returned as absolute paths. Results are sorted by file modification time with the most recently modified files appearing first. If modification time metadata is unavailable, those files sort to the end.
Results are limited to 1000 files by default. When more files match, output indicates the limit was reached with ”… and N more files”. An empty result set returns a clear message indicating no files matched the pattern in the specified directory.
Read permission for the search directory is required.
Example:
{
"pattern": "**/*.rs",
"path": "/home/user/project",
"limit": 50
}
grep
High-performance file content search with regex support. Returns matching files or content.
Input Schema:
{
"pattern": "string (required) - Regex pattern to search",
"path": "string - File or directory to search (default: current directory)",
"glob": "string - Glob pattern filter (e.g., '*.js')",
"type": "string - File type (e.g., 'rust', 'js', 'py')",
"output_mode": "string - 'files_with_matches' (default), 'content', or 'count'",
"-i": "boolean - Case insensitive (default: false)",
"-A": "integer - Lines after match",
"-B": "integer - Lines before match",
"-C": "integer - Context lines before and after",
"-n": "boolean - Show line numbers (default: true)"
}
High-performance content search uses compiled regex patterns with three output modes: files_with_matches (default) returns only file paths containing matches, content returns matching lines with context, and count returns match counts per file.
In content mode, output includes the filename, line number, and matched line. Context lines can be added with -A (lines after), -B (lines before), or -C (lines both before and after). Line numbers are shown by default but can be disabled with -n: false.
Case-insensitive matching is available with the -i flag. Multiline mode enables patterns to span line boundaries, making . match newlines and enabling cross-line pattern matching.
File filtering works through the glob parameter (filename patterns like “*.js”) or the type parameter (predefined file types). Over 40 file types are predefined including js, ts, tsx, py, rust, go, java, c, cpp, rb, php, swift, and many more.
Binary file detection automatically skips files containing null bytes, preventing garbled output from non-text files. Hidden files and directories are skipped during traversal unless they are part of the explicit search path.
Results are limited to 1000 matches by default. Read permission for the search path is required.
Example:
{
"pattern": "fn\\s+main",
"path": "/home/user/project",
"type": "rust",
"output_mode": "content",
"-C": 3
}
Command Execution
The command execution tools allow running shell commands with safety controls. These tools include timeout management, dangerous command detection, and background execution support for long-running processes.
bash
Executes bash commands with timeout, working directory management, and output capturing.
Input Schema:
{
"command": "string (required) - Bash command to execute",
"timeout": "integer - Timeout in ms (default: 120000, max: 600000)",
"working_dir": "string - Working directory (must be absolute)",
"run_in_background": "boolean - Run in background (default: false)",
"background_timeout": "integer - Background task timeout in ms",
"env": "object - Additional environment variables"
}
Commands execute in a bash shell using bash -c, with stdout and stderr captured separately. The default timeout is 120 seconds (2 minutes) with a maximum of 600 seconds (10 minutes). Timeouts below 1 second are clamped to 1 second.
Before execution, dangerous command detection kicks in. Commands containing patterns like rm -rf /, mkfs, dd if=, chmod -R 777 /, fork bombs (:(){ :|:& };:), or piped remote execution (curl ... | bash) are rejected immediately with a clear warning.
Output is limited to 100KB for stdout and 50KB for stderr. Content exceeding these limits is truncated with a “[Output truncated due to size limit]” notice. If a command exits with a non-zero code, “[Exit code: N]” appears at the end of the output.
Working directory state persists across command invocations within the same session. Setting working_dir changes the directory for that command and all subsequent commands in the session. Working directories must be absolute paths pointing to existing directories.
Background execution returns immediately with a task ID and process ID. An optional background_timeout can kill long-running background processes. Without a timeout, background commands run until completion.
Custom environment variables can be passed through the env parameter as key-value pairs. These apply only to the current command execution. Execute permission is required before running any command.
Example:
{
"command": "cargo build --release",
"working_dir": "/home/user/project",
"timeout": 300000
}
Background execution:
{
"command": "cargo test",
"run_in_background": true,
"background_timeout": 600000
}
Search and Discovery
Search and discovery tools extend the agent’s knowledge beyond local files. These tools enable web searches for current information and discovery of available agent capabilities.
web_search
Wrapper around Claude’s native web search capability. The actual search execution happens on Claude’s side.
Input Schema:
{
"query": "string (required) - The search query"
}
This wraps Claude’s native web search capability. Unlike other tools, actual search execution happens on Claude’s side, not within the agent. The execute() method is a no-op that returns an empty string.
The wrapper exists to provide a consistent interface for the LLM and to enable display configuration in the UI. When registered, it appears in the tool list like any other tool, allowing Claude to invoke it when current information is needed.
Search results are returned as citations embedded in Claude’s response, not as tool output. The UI displays “Search completed (results shown via citations)” while the actual search findings appear in the response text with source attributions.
Domain filtering is supported but managed by Claude. Web search availability may be limited to certain regions.
Example:
{
"query": "Rust async runtime comparison 2024"
}
Note: The execute() method is a no-op. Search results appear in Claude’s response with citations to sources.
list_skills
Lists all available skills from the skill registry.
Input Schema:
{}
No parameters required.
Returns:
- Skill name
- Description
- Path to SKILL.md file
Querying the skill registry returns a JSON array containing metadata for all registered skills. Each entry includes the skill name, description, and path to the skill’s SKILL.md file.
No dependencies or permissions are required. A direct registry query is performed and results are formatted as JSON. If no skills are registered, “No skills available.” is returned.
This is useful for agents discovering their available capabilities at runtime. Skills can provide specialized functionality that extends the agent’s base tool set.
User Interaction
User interaction tools enable the agent to communicate with users during execution. These tools support structured questions, permission requests, and blocking operations that wait for user input before proceeding.
ask_user_questions
Asks the user structured questions with multiple response types.
Input Schema:
{
"questions": "array (required) - Array of question objects"
}
Each question object:
{
"text": "string (required) - Question text",
"type": "string (required) - 'SingleChoice', 'MultiChoice', or 'FreeText'",
"choices": "array - Options for choice questions",
"defaultValue": "string - Default for FreeText questions",
"required": "boolean - Whether answer is required (default: false)"
}
Three question types are supported. SingleChoice presents options where the user selects exactly one. MultiChoice allows selecting zero or more options. FreeText accepts any text input with an optional default value.
For choice questions, the choices array must be non-empty. This is validated before presenting questions to the user. However, users aren’t restricted to the provided choices - custom answers can be entered for any question type.
Validation occurs at two stages. Before display, choice questions are checked for non-empty choice arrays. After the user responds, validation confirms that required questions have answers, that SingleChoice questions have at most one selection, and that all answered questions exist in the original request.
Execution blocks until the user provides answers. The question request is registered with the UserInteractionRegistry and the response is awaited through a channel. Results are returned as JSON containing all answers.
Validation errors return a structured response with error codes: required_field_empty for unanswered required questions, too_many_selections for SingleChoice with multiple answers, empty_choices for choice questions without options, and unknown_question for answers to non-existent questions.
Example:
{
"questions": [
{
"text": "Which database should we use?",
"type": "SingleChoice",
"choices": ["PostgreSQL", "MySQL", "SQLite"],
"required": true
},
{
"text": "Any additional requirements?",
"type": "FreeText",
"required": false
}
]
}
ask_for_permissions
Requests user permission for sensitive operations with session-level caching.
Input Schema:
{
"target_type": "string (required) - 'path', 'domain', or 'command'",
"target": "string (required) - Resource being accessed",
"level": "string (required) - 'read', 'write', 'execute', or 'admin'",
"recursive": "boolean - For paths, include subdirectories (default: false)",
"description": "string (required) - Human-readable action description",
"reason": "string - Why this action is needed"
}
Explicit permission is requested for sensitive operations using three target types. path targets file system locations and supports the recursive flag to include subdirectories. domain targets network hosts and supports pattern matching. command targets shell commands.
Four permission levels indicate the type of access: read for viewing, write for modification, execute for running commands, and admin for full control including deletion. Higher levels imply greater potential impact.
The description field is required and should clearly explain what action will be performed. The optional reason field can explain why the action is needed, helping users make informed decisions.
Users can grant permission with two scopes. once approves only the current request. session approves this and all similar requests for the remainder of the session. Session-scoped grants are cached, so matching future requests are automatically approved without prompting.
This manages its own permission flow rather than going through the standard permission system. The registry is checked for existing grants before prompting, new requests are registered when needed, and execution blocks until the user responds.
The response includes whether permission was granted, the grant details (target, level, scope), and an optional user message. Denied permissions return a clear indication that can be communicated back to the LLM.
Example:
{
"target_type": "path",
"target": "/home/user/project/config.yaml",
"level": "write",
"description": "Write database configuration",
"reason": "Save the connection settings you specified"
}
Response:
{
"granted": true,
"scope": "session",
"message": null
}
Planning
The planning tools provide a durable markdown-based planning system for agents. Plans are stored as markdown files in .agent-air/plans/ and are internal agent artifacts - no user permission prompts are required. All plan tools share a thread-safe PlanStore that handles directory management, sequential ID generation, and per-file locking.
markdown_plan
Creates or updates a durable markdown plan file in the workspace.
Input Schema:
{
"plan_id": "string - Plan ID to update. Omit to create a new plan.",
"title": "string (required) - Plan title",
"steps": "array (required) - Array of step objects",
"status": "string - 'draft', 'active', 'completed', or 'abandoned' (default: 'draft')"
}
Each step object:
{
"description": "string (required) - Step description",
"notes": "string - Optional notes for the step"
}
When plan_id is omitted, a sequential ID is automatically generated (plan-001, plan-002, etc.) by scanning existing files in the plans directory. When provided, the existing plan file is overwritten with the new content.
Each step starts as pending with [ ] checkbox syntax. The steps array must contain at least one step. The generated markdown includes the plan title, ID, status, creation date, and numbered steps with optional notes.
Plan files are written to .agent-air/plans/ in the workspace root. The directory is created automatically if it doesn’t exist. File operations use per-file mutex locking for thread safety across concurrent sessions.
This tool handles its own permissions internally and never prompts the user.
Example:
{
"title": "Refactor authentication module",
"steps": [
{ "description": "Extract auth logic into separate module", "notes": "Watch out for circular dependencies" },
{ "description": "Add unit tests for token validation" },
{ "description": "Update API routes to use new module" }
],
"status": "active"
}
update_plan_step
Updates the status of a single step in an existing plan.
Input Schema:
{
"plan_id": "string (required) - Plan ID",
"step": "integer (required) - Step number (1-indexed)",
"status": "string (required) - 'pending', 'in_progress', 'completed', or 'skipped'"
}
Finds the step by its 1-indexed number and replaces the checkbox marker with the requested status. Status markers map as follows: pending = [ ], in_progress = [~], completed = [x], skipped = [-].
The plan file must already exist in .agent-air/plans/. If the step number is out of range, an error is returned indicating how many steps the plan has. Steps can transition between any status in any direction.
File operations use per-file mutex locking to prevent concurrent modification. This tool handles its own permissions internally.
Example:
{
"plan_id": "plan-001",
"step": 1,
"status": "completed"
}
list_plans
Lists all plans in the workspace with summary metadata.
Input Schema:
{}
No parameters required. Scans the .agent-air/plans/ directory for plan files matching the plan-NNN.md naming convention. Non-plan files in the directory are ignored.
Each plan summary includes the plan ID, title, status, creation date, total step count, and completion progress. When steps are in multiple states, a detailed breakdown is shown (e.g., “2 completed, 1 in progress, 3 pending”).
Results are sorted by plan ID for consistent ordering. If no plans exist or the plans directory hasn’t been created, a message directing the user to markdown_plan is returned.
This tool handles its own permissions internally.
read_plan
Reads a plan by its ID and returns the full markdown content.
Input Schema:
{
"plan_id": "string (required) - Plan ID to read (e.g., plan-001)"
}
Returns the complete markdown content of the specified plan file. If the plan doesn’t exist, an error message suggests using list_plans to discover available plan IDs.
This tool handles its own permissions internally.
Example:
{
"plan_id": "plan-001"
}
Registering Standard Tools
To use standard tools in your agent, register them during agent initialization. The registration closure provides access to the tool registry and interaction registries needed by different tool types.
Register all standard tools with your agent:
use agent_air::controller::tools::*;
core.register_tools(|registry, user_reg, perm_reg| {
// File operations
registry.register(Arc::new(ReadFileTool::new())).await?;
registry.register(Arc::new(WriteFileTool::new(perm_reg.clone()))).await?;
registry.register(Arc::new(EditFileTool::new(perm_reg.clone()))).await?;
registry.register(Arc::new(MultiEditTool::new(perm_reg.clone()))).await?;
// Directory operations
registry.register(Arc::new(LsTool::new())).await?;
registry.register(Arc::new(GlobTool::new())).await?;
registry.register(Arc::new(GrepTool::new())).await?;
// Command execution
registry.register(Arc::new(BashTool::new(perm_reg.clone()))).await?;
// Search
registry.register(Arc::new(WebSearchTool::new())).await?;
registry.register(Arc::new(ListSkillsTool::new(skill_registry))).await?;
// User interaction
registry.register(Arc::new(AskUserQuestionsTool::new(user_reg.clone()))).await?;
registry.register(Arc::new(AskForPermissionsTool::new(perm_reg.clone()))).await?;
// Planning
let plan_store = Arc::new(PlanStore::new(workspace_root));
registry.register(Arc::new(MarkdownPlanTool::new(plan_store.clone()))).await?;
registry.register(Arc::new(UpdatePlanStepTool::new(plan_store.clone()))).await?;
registry.register(Arc::new(ListPlansTool::new(plan_store.clone()))).await?;
registry.register(Arc::new(ReadPlanTool::new(plan_store))).await?;
Ok(tools_list)
})?;
See Tool Registration & Context for details on registries and registration.
