This guide provides comprehensive instructions for creating custom ESA agents. Agents are TOML configuration files that define specialized AI assistants with custom functions and behaviors.
- Overview
- Agent Structure
- Basic Agent Example
- Configuration Reference
- Function Definition
- Parameter Handling
- Advanced Features
- Best Practices
- Example Agents
- Debugging and Testing
ESA agents are defined in TOML files that specify:
- System Prompt: Instructions that guide the AI's behavior
- Functions: Command-line tools the agent can execute
- Parameters: Input validation and formatting
- Safety Settings: Confirmation levels and command classification
Agents are stored in ~/.config/esa/agents/ and can be invoked using the +agent-name syntax.
Every agent configuration follows this basic structure:
# Optional: Agent metadata
name = "Agent Name"
description = "Brief description of what this agent does"
default_model = "provider/model-name"
# Core configuration
system_prompt = """Instructions for the AI assistant"""
initial_message = "Default message when no input provided" # optional
ask = "unsafe" # Confirmation level(global): none, unsafe, all
# Function definitions
[[functions]]
name = "function_name"
description = "What this function does"
command = "shell command with {{parameters}}"
safe = true
# ... additional function properties
[[functions.parameters]]
name = "param_name"
type = "string"
description = "Parameter description"
required = trueHere's a simple file management agent:
name = "File Manager"
description = "Basic file operations assistant"
default_model = "openai/gpt-4o-mini"
system_prompt = """
You are a file management assistant. Help users with basic file operations
like listing, reading, and organizing files safely.
Keep responses concise and always confirm before performing destructive operations.
"""
ask = "unsafe"
[[functions]]
name = "list_files"
description = "List files in a directory"
command = "ls -la {{path}}"
safe = true
[[functions.parameters]]
name = "path"
type = "string"
description = "Directory path to list (defaults to current directory)"
required = false
[[functions]]
name = "read_file"
description = "Display the contents of a file"
command = "cat {{filename}}"
safe = true
[[functions.parameters]]
name = "filename"
type = "string"
description = "Path to the file to read"
required = true
[[functions]]
name = "create_directory"
description = "Create a new directory"
command = "mkdir -p {{dirname}}"
safe = false
[[functions.parameters]]
name = "dirname"
type = "string"
description = "Name of the directory to create"
required = trueSave this as ~/.config/esa/agents/files.toml and use it with:
esa +files "is the current directory a go project?"
esa +files "based on README.md how can I use this project"| Property | Type | Required | Description |
|---|---|---|---|
name |
string | No | Human-readable agent name |
description |
string | No | Brief description for list-agents |
system_prompt |
string | Yes | Core instructions for the AI |
initial_message |
string | No | Default message when no input provided |
ask |
string | No | Confirmation level: none, unsafe, all |
default_model |
string | No | Preferred model for this agent (e.g., openai/gpt-4o-mini) |
ESA uses the following priority order to determine which model to use:
- CLI Model Flag (
--modelor-m) - Highest priority - Agent Default Model (
agent.toml→default_model) - Global Config Default (
config.toml→settings.default_model) - Built-in Fallback (
openai/gpt-4o-mini) - Lowest priority
This hierarchy allows you to:
- Set reasonable defaults for specific agents
- Override globally via configuration
- Override per-command via CLI flags
Example Usage:
# Uses agent's default model
esa +code-analysis "review this function"
# Overrides with specific model
esa --model "openai/gpt-4" +code-analysis "review this function"The system prompt is crucial for agent behavior. It should:
- Clearly define the agent's role and purpose
- Provide specific instructions for handling tasks
- Include any domain-specific knowledge or constraints
- Mention response style preferences (concise, detailed, etc.)
TIP: While not strictly necessary, providing output examples in XML tags are greatly beneficial to explalin how to format the output to the LLM
Template Variables in System Prompt:
You can run bash command to generate output that will be templated out in the system prompt by doing {{$<command>}}. Here are a few examples:
{{$date '+%Y-%m-%d %A'}}- Current date{{$uname}}- Operating system info{{$pwd}}- Current working directory{{$whoami}}- Current user{{$jira me}}- Get current Jira user
Example with Input/Output Examples:
The most effective system prompts include input/output examples in XML tags to guide the LLM's behavior:
system_prompt = """
You are a Kubernetes assistant helping manage clusters and workloads.
Working in {{$pwd}} on {{$date '+%A, %B %d, %Y'}}.
Current context: {{$kubectl config current-context 2>/dev/null || echo 'Not configured'}}
When users ask about Kubernetes operations, analyze their request and use the appropriate functions.
Always provide clear explanations of what commands will do before executing them.
<examples>
<example>
<input>show me running pods in the default namespace</input>
*calls list_pods function with namespace="default"*
</output>
Here are the running pods in the default namespace:
- web-app-1234 (Running, Ready 1/1)
- api-service-5678 (Running, Ready 2/2)
- database-9012 (Running, Ready 1/1)
All pods appear healthy and ready to serve traffic.</output>
</example>
<example>
<input>what's wrong with my failing pod?</input>
<output>
The pod is in CrashLoopBackOff state. Looking at the logs, I can see:
- Error: Connection refused to database on port 5432
- The application is failing to connect to the database
This suggests a connectivity issue. Check:
1. Database service is running
2. Network policies allow communication
3. Connection string is correct</output>
</example>
</examples>
Keep responses concise but informative. Always explain what actions you're taking and why.
"""| Level | Behavior |
|---|---|
none |
No confirmation required (default) |
unsafe |
Confirm commands marked as safe = false |
all |
Confirm every command execution |
Functions are the core capability of agents. Each function maps to a shell command or script with parameters.
[[functions]]
name = "function_name"
description = "Clear description of what this function does"
command = "shell command with {{param}} placeholders"
safe = true| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string | Yes | - | Unique function identifier |
description |
string | Yes | - | Detailed function description |
command |
string | Yes | - | Shell command template |
safe |
boolean | No | false |
Whether command is safe to run |
stdin |
string | No | - | Input to pass to command's stdin |
output |
string | No | - | Show output to user during execution |
pwd |
string | No | - | Working directory for command |
timeout |
integer | No | 30 | Command timeout in seconds |
Commands use {{parameter}} placeholders that are replaced with user-provided values:
command = "grep '{{pattern}}' {{file}} {{flags}}"Special Shell Blocks:
{{$command}}- Execute shell command and insert output{{#prompt}}- Prompt user for input
Examples:
# Get current date in command
command = "echo 'Today is {{$date}}'"
# Use environment variable
command = "curl -H 'Authorization: Bearer {{$GITHUB_TOKEN}}' {{url}}"
# Prompt user for input
command = "git commit -m '{{#Enter commit message:}}'"Parameters define inputs for your functions with validation and formatting.
[[functions.parameters]]
name = "param_name"
type = "string"
description = "Parameter description"
required = true
format = "format_string"
options = ["option1", "option2"]| Property | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Parameter name for {{name}} placeholder |
type |
string | Yes | Data type: string, number, boolean |
description |
string | Yes | Clear description for the AI |
required |
boolean | No | Whether parameter is required |
format |
string | No | Format string for parameter substitution |
options |
array | No | Allowed values (creates enum) |
String Parameters:
[[functions.parameters]]
name = "filename"
type = "string"
description = "Path to the file"
required = trueBoolean Parameters:
[[functions.parameters]]
name = "verbose"
type = "boolean"
description = "Enable verbose output"
required = false
format = "-v" # Only added if trueEnum Parameters:
[[functions.parameters]]
name = "log_level"
type = "string"
description = "Logging level"
options = ["debug", "info", "warn", "error"]
required = trueNumber Parameters:
[[functions.parameters]]
name = "port"
type = "number"
description = "Port number"
required = trueThe format property provides powerful control over how parameters are inserted into commands. ESA supports several formatting patterns:
[[functions.parameters]]
name = "filename"
type = "string"
format = "--file=%s" # Results in: --file=myfile.txtBoolean parameters have special handling - they only appear in the command when true:
[[functions.parameters]]
name = "verbose"
type = "boolean"
format = "-v" # Only appears if verbose=true
# Usage in command: ls {{verbose}} {{path}}
# If verbose=true: ls -v /path
# If verbose=false: ls /pathFor parameters that should appear as flags with values:
[[functions.parameters]]
name = "output_format"
type = "string"
format = "--output %s" # Results in: --output json
# Alternative formats:
format = "--output=%s" # Results in: --output=json
format = "-o %s" # Results in: -o jsonWhen parameters are not required, they're simply omitted from the command:
[[functions.parameters]]
name = "port"
type = "number"
required = false
format = "--port %d"
# If port provided: command --port 8080
# If port omitted: command# Conditional file inclusion
[[functions.parameters]]
name = "config_file"
type = "string"
required = false
format = "--config=%s"
# Multiple format styles
[[functions.parameters]]
name = "log_level"
type = "string"
options = ["debug", "info", "warn", "error"]
format = "--log-level=%s"
# Boolean with custom flag
[[functions.parameters]]
name = "force"
type = "boolean"
format = "--force" # Only appears when true
# Numeric with validation
[[functions.parameters]]
name = "timeout"
type = "number"
format = "--timeout=%d"| Pattern | Description | Example |
|---|---|---|
"--flag %s" |
Space-separated flag and value | --output json |
"--flag=%s" |
Equals-separated flag and value | --output=json |
"-f" |
Simple flag (boolean only) | -f (if true) |
"%s" |
Raw value substitution | myfile.txt |
| No format | Direct parameter replacement | {{param}} → value |
Note: The
%s,%d,%fformat specifiers follow Go's fmt package conventions for string, integer, and float formatting respectively.
Set the working directory for specific functions. This is useful when commands need to run in a specific location:
[[functions]]
name = "git_status"
command = "git status"
pwd = "{{repo_path}}"
[[functions.parameters]]
name = "repo_path"
type = "string"
description = "Path to git repository"
required = trueAdvanced pwd usage:
# Use environment variables
pwd = "$HOME/projects/{{project_name}}"
# Relative paths work too
pwd = "./subdir/{{folder}}"
# Multiple parameter substitution
pwd = "{{base_path}}/{{project}}/{{branch}}"Pass data to command's standard input with full parameter and shell substitution support:
[[functions]]
name = "format_json"
command = "jq '.'"
stdin = "{{json_data}}"
[[functions.parameters]]
name = "json_data"
type = "string"
description = "JSON data to format"
required = trueAdvanced stdin examples:
# Multi-line stdin with parameter substitution
[[functions]]
name = "send_email"
command = "sendmail {{recipient}}"
stdin = """Subject: {{subject}}
From: {{sender}}
{{message_body}}
"""
# Stdin with shell command execution
[[functions]]
name = "process_with_context"
command = "grep '{{pattern}}'"
stdin = "{{$cat /path/to/file}}" # Execute shell command for stdin
# Template-heavy stdin for config generation
[[functions]]
name = "generate_config"
command = "tee {{config_name}}"
stdin = """
[server]
host = "{{host}}"
port = {{port}}
debug = {{debug}}
[database]
url = "{{db_url}}"
"""Control execution time limits for different types of operations:
# Quick operations (default: 60 seconds)
[[functions]]
name = "list_files"
command = "ls -la"
# timeout defaults to 60
# Long-running operations
[[functions]]
name = "large_download"
command = "wget {{url}}"
timeout = 300 # 5 minutes
# Very long operations
[[functions]]
name = "backup_database"
command = "pg_dump {{database}} > backup.sql"
timeout = 3600 # 1 hourESA supports dynamic content generation using shell command blocks:
system_prompt = """
You are a Git assistant working in {{$pwd}} on {{$date '+%A, %B %d, %Y'}}.
Current branch: {{$git branch --show-current 2>/dev/null || echo 'Not a git repo'}}
Git status: {{$git status --porcelain | wc -l}} files changed
Current user: {{$whoami}}
System: {{$uname -s}}
Help with git operations while maintaining repository safety.
"""[[functions]]
name = "create_timestamped_file"
command = "touch {{filename}}_{{$date '+%Y%m%d_%H%M%S'}}.{{extension}}"
[[functions]]
name = "backup_with_user"
command = "cp {{file}} {{file}}.bak.{{$whoami}}.{{$date '+%Y%m%d'}}"Use the {{#prompt}} syntax to interactively collect input during command execution:
[[functions]]
name = "create_issue"
command = "gh issue create --title '{{title}}' --body '{{#Enter issue description (end with empty line):}}'"The output field documents expected output and can be used for user communication:
[[function]]
name = "ask_user_question"
description = "Ask a question to the user. Use this instead of directly asking the user a question."
output = "{{query}}"
command = "read answer && echo $answer"
safe = true
[[functions.parameters]]
name = "query"
type = "string"
description = "Question to ask the user"
required = trueESA supports environment variable expansion in commands:
[[functions]]
name = "deploy_to_server"
command = "scp {{file}} $DEPLOY_USER@$DEPLOY_HOST:{{remote_path}}"
safe = false
[[functions]]
name = "query_database"
command = "psql $DATABASE_URL -c '{{query}}'"
safe = trueBuild robust functions with error handling:
[[functions]]
name = "smart_git_status"
command = "jj status 2>/dev/null || git status 2>/dev/null || echo 'Not a version-controlled directory'"
safe = true
[[functions]]
name = "safe_package_install"
command = "npm install {{package}} || yarn add {{package}} || echo 'No package manager found'"
safe = false
[[functions]]
name = "cross_platform_open"
command = "open {{file}} 2>/dev/null || xdg-open {{file}} 2>/dev/null || start {{file}} 2>/dev/null || echo 'Cannot open file on this platform'"
safe = trueTips for efficient function design:
# Use specific commands instead of general ones
[[functions]]
name = "count_files"
command = "find {{directory}} -type f | wc -l" # Better than ls | wc
safe = true
# Limit output for large datasets
[[functions]]
name = "recent_logs"
command = "tail -n {{lines}} {{logfile}}" # Better than cat for large files
safe = true
[[functions.parameters]]
name = "lines"
type = "number"
required = false
format = "%d"
# Use grep for filtering instead of processing all data
[[functions]]
name = "search_logs"
command = "grep '{{pattern}}' {{logfile}} | head -20"
safe = true- Mark read-only operations as
safe = true - Mark destructive operations as
safe = false - Use appropriate
asklevels for your use case - Validate parameters thoroughly
# Safe operation
[[functions]]
name = "list_files"
command = "ls {{path}}"
safe = true
# Unsafe operation
[[functions]]
name = "delete_file"
command = "rm {{file}}"
safe = falseWrite detailed descriptions that help the AI understand when and how to use functions:
[[functions]]
name = "git_commit"
description = "Create a git commit with the specified message. Use this after staging changes with git add."
command = "git commit -m '{{message}}'"Use options for enum-like parameters and clear description fields:
[[functions.parameters]]
name = "priority"
type = "string"
description = "Issue priority level"
options = ["low", "medium", "high", "critical"]
required = trueHandle edge cases and provide fallbacks:
# Use || for command fallbacks
command = "jj status || git status"
# Redirect errors to avoid noise
command = "command 2>/dev/null || echo 'Command failed'"Use clear, consistent naming conventions:
- Functions:
verb_noun(e.g.,list_files,create_branch) - Parameters:
snake_case(e.g.,file_path,commit_message) - Agents:
descriptive-name(e.g.,git-ops,k8s-admin)
Include helpful comments in your agent files:
# This agent helps with Kubernetes cluster management
# Requires kubectl to be installed and configured
system_prompt = """..."""
# Core cluster information
[[functions]]
name = "get_nodes"
# ... function definitionname = "Git Assistant"
description = "Git repository management and operations"
system_prompt = """
You are a Git operations assistant. Help users with git commands,
repository management, and version control workflows.
Always check repository status before performing operations.
Provide clear explanations of what each command does.
"""
ask = "unsafe"
[[functions]]
name = "git_status"
description = "Show the working tree status"
command = "git status --porcelain"
safe = true
[[functions]]
name = "git_log"
description = "Show commit history"
command = "git log --oneline -n {{count}}"
safe = true
[[functions.parameters]]
name = "count"
type = "number"
description = "Number of commits to show"
required = false
[[functions]]
name = "git_add"
description = "Add files to staging area"
command = "git add {{files}}"
safe = false
[[functions.parameters]]
name = "files"
type = "string"
description = "Files to add (use . for all files)"
required = true
[[functions]]
name = "git_commit"
description = "Create a commit with message"
command = "git commit -m '{{message}}'"
safe = false
[[functions.parameters]]
name = "message"
type = "string"
description = "Commit message"
required = truename = "System Monitor"
description = "System resource monitoring and information"
system_prompt = """
You are a system monitoring assistant. Provide information about
system resources, processes, and performance metrics.
Present information in a clear, organized format.
"""
[[functions]]
name = "cpu_usage"
description = "Show CPU usage information"
command = "top -l 1 | grep 'CPU usage'"
safe = true
[[functions]]
name = "memory_usage"
description = "Show memory usage statistics"
command = "vm_stat"
safe = true
[[functions]]
name = "disk_usage"
description = "Show disk space usage"
command = "df -h {{path}}"
safe = true
[[functions.parameters]]
name = "path"
type = "string"
description = "Path to check (defaults to root filesystem)"
required = false
[[functions]]
name = "list_processes"
description = "List running processes"
command = "ps aux | head -{{lines}}"
safe = true
[[functions.parameters]]
name = "lines"
type = "number"
description = "Number of processes to show"
required = falseUse debug mode to see what's happening:
esa --debug +myagent "test command"This shows:
- System prompt processing
- Function calls and parameters
- Command execution details
- Error messages
See the actual commands being executed:
esa --show-commands +myagent "test command"Check your agent configuration:
# View agent details
esa show-agent +myagent
# List all agents
esa list-agentsTest individual functions by creating simple requests:
esa +myagent "use the list_files function to show current directory"Parameter Not Found:
- Check parameter names match exactly between function command and parameter definition
- Ensure required parameters are marked correctly
Command Failures:
- Test commands manually in terminal first
- Check file paths and permissions
- Verify required tools are installed
AI Not Using Functions:
- Make function descriptions more specific
- Ensure system prompt mentions when to use functions
- Check that function names are descriptive
- Start with simple functions
- Test each function individually
- Add complexity gradually
- Use debug mode to troubleshoot
- Refine system prompt based on behavior
- User agents:
~/.config/esa/agents/ - Built-in agents: Embedded in ESA binary
- Example agents:
examples/directory in source
~/.config/esa/
├── config.toml # Global configuration
└── agents/
├── git-ops.toml # Git operations
├── k8s-admin.toml # Kubernetes admin
├── dev-tools.toml # Development utilities
└── personal.toml # Personal tasks
Agents are portable TOML files that can be easily shared:
# Share an agent
cp ~/.config/esa/agents/myagent.toml ~/shared/
# Install a shared agent
cp ~/shared/myagent.toml ~/.config/esa/agents/ESA agents provide a powerful way to create specialized AI assistants for any domain. Start with simple agents and gradually add complexity as you learn the system.
Key points to remember:
- Focus on clear, descriptive system prompts
- Mark functions as safe/unsafe appropriately
- Use parameter validation to prevent errors
- Test thoroughly with debug mode
- Follow consistent naming conventions
For more examples, check the examples/ directory and the built-in agents.
Happy agent building! 🚀