Add `--experimental` / `--beta` flag to enable an agent loop that allows LLMs to use tools (bash, web_search) with interactive user approval. Features: - Built-in tools: bash command execution, web search via Ollama API - Interactive approval UI with arrow key navigation - Auto-allowlist for safe commands (pwd, git status, npm run, etc.) - Denylist for dangerous patterns (rm -rf, sudo, credential access) - Prefix-based allowlist for approved directories (cat src/ approves cat src/*) - Warning box for commands targeting paths outside project directory Architecture: - x/tools/: Tool registry, bash executor, web search client - x/agent/: Approval manager with TUI selector - x/cmd/: Agent loop orchestration The readline change fixes a stdin race condition where background goroutine consumption caused double-keypress issues in the approval UI. |
||
|---|---|---|
| .. | ||
| agent | ||
| cmd | ||
| tools | ||
| APPROVAL_UX.md | ||
| DOUBLE_PRESS_FIX.md | ||
| README.md | ||
README.md
Experimental Agent Loop (x/)
This directory contains the experimental agent loop implementation for Ollama. It enables LLMs to use tools (bash commands, web search) in an interactive loop with user approval.
Quick Start
# Run with experimental agent loop (use a model that supports tools)
ollama run qwen2.5 --experimental
# Or use --beta alias
ollama run qwen2.5 --beta
For web search, set your API key:
export OLLAMA_API_KEY=your_key_here
ollama run qwen2.5 --experimental
Note: The model must support tool calling. If it doesn't, the agent loop will run in chat-only mode:
Note: Model does not support tools - running in chat-only mode
Models with tool support include: qwen2.5, llama3.1, mistral, command-r, etc.
Features
Built-in Tools
| Tool | Description |
|---|---|
bash |
Execute shell commands with 60s timeout |
web_search |
Search the web via Ollama's hosted API |
Interactive Approval
Every tool execution requires user approval:
┌──────────────────────────────────────────────────────────┐
│ Tool: bash │
│ Command: ls -la src/ │
├──────────────────────────────────────────────────────────┤
│ > 1. Execute once │
│ 2. Always allow │
│ 3. Deny: │
└──────────────────────────────────────────────────────────┘
↑/↓ navigate, Enter confirm, 1-3 quick, Ctrl+C cancel
Input options:
↑/↓- Navigate between optionsEnter- Confirm selection1,2,3- Quick selectCtrl+C- Cancel (deny)- Type any text - Enters deny reason (auto-selects Deny option)
Backspace- Delete from reasonEsc- Clear reason
Warning for Commands Outside Project
Commands that target paths outside the current working directory show a red warning box:
┌────────────────────────────────────────┐
│ !! OUTSIDE PROJECT !! │
│ Tool: bash │
│ Command: cat /etc/passwd │
├────────────────────────────────────────┤
│ > 1. Execute once │
│ 2. Always allow │
│ 3. Deny: │
└────────────────────────────────────────┘
This alerts you when the model tries to:
- Access absolute paths outside cwd (e.g.,
/etc/passwd) - Navigate to parent directories (e.g.,
../../../etc/passwd) - Access home directory paths (e.g.,
~/.bashrc)
Prefix-Based Allowlist
When you select "Always allow" for safe commands (cat, ls, grep, etc.), the system saves the directory prefix rather than the exact command:
| Command Approved | Saved as Prefix |
|---|---|
cat src/main.go |
cat:src/ |
ls -la tools/ |
ls:tools/ |
grep -r "pattern" api/ |
grep:api/ |
This means future commands to the same directory are auto-approved.
Safe commands (eligible for prefix matching):
cat,ls,head,tail,less,morefile,wc,grep,find,tree,stat,sed
Auto-Allowed Commands
These commands run without prompting (zero risk):
| Category | Commands |
|---|---|
| System info | pwd, echo, date, whoami, hostname, uname |
| Git (read-only) | git status, git log, git diff, git branch, git show |
| Package managers | npm run, npm test, bun run, uv run, yarn run, pnpm run |
| Build/test | go build, go test, make, cargo build, cargo test |
Blocked Commands (Denylist)
Dangerous commands are automatically blocked and return an error to the model:
| Category | Patterns |
|---|---|
| Destructive | rm -rf, rm -r, mkfs, dd, shred |
| Privilege escalation | sudo, su, chmod 777, chown |
| Credential access | .ssh/id_*, .aws/credentials, .env, *secret*, *password* |
| Network exfil | curl -d, curl --data, scp, rsync, nc |
When blocked, the model receives:
Command blocked: this command matches a dangerous pattern (rm -rf) and cannot be executed.
If this command is necessary, please ask the user to run it manually.
Session Commands
| Command | Description |
|---|---|
/tools |
Show available tools and current approvals |
/clear |
Clear conversation history and approvals |
/bye |
Exit |
/? |
Help |
Architecture
x/
├── README.md # This file
├── APPROVAL_UX.md # Detailed approval UX documentation
├── DOUBLE_PRESS_FIX.md # Fix for stdin race condition
├── agent/
│ ├── approval.go # Interactive approval system
│ └── approval_test.go # Tests
├── tools/
│ ├── registry.go # Tool interface and registry
│ ├── registry_test.go # Tests
│ ├── bash.go # Bash command execution
│ └── websearch.go # Web search via Ollama API
└── cmd/
└── run.go # Agent loop and interactive session
How It Works
Agent Loop
- User sends a message
- LLM responds, optionally requesting tool calls
- For each tool call:
- Check if already approved (exact match or prefix)
- If not, show interactive approval dialog
- Execute tool if approved
- Return result to LLM
- LLM continues with tool results
- Repeat until LLM responds without tool calls
Tool Execution Flow
User Message
↓
LLM Response (with tool calls)
↓
┌─────────────────────────────┐
│ For each tool call: │
│ ├─ Check allowlist │
│ ├─ Request approval (UI) │
│ ├─ Execute tool │
│ └─ Collect result │
└─────────────────────────────┘
↓
Tool Results → LLM
↓
LLM Final Response
Files Changed (from main)
New Files (x/)
| File | Purpose |
|---|---|
x/cmd/run.go |
Agent loop, interactive session, tool execution |
x/tools/registry.go |
Tool interface and registry |
x/tools/bash.go |
Bash command execution with timeout |
x/tools/websearch.go |
Web search using Ollama API |
x/agent/approval.go |
Interactive approval UI |
x/agent/approval_test.go |
Approval tests |
x/tools/registry_test.go |
Registry tests |
Modified Files
| File | Changes |
|---|---|
cmd/cmd.go |
Added --experimental/--beta flags, routes to x/cmd |
cmd/interactive.go |
Added /tools command info |
readline/readline.go |
Fixed stdin race condition (see DOUBLE_PRESS_FIX.md) |
API Reference
Tool Interface
type Tool interface {
Name() string
Description() string
Schema() api.ToolFunction
Execute(args map[string]any) (string, error)
}
Registry
// Create registry with built-in tools
registry := tools.DefaultRegistry()
// Or create custom registry
registry := tools.NewRegistry()
registry.Register(&MyCustomTool{})
// Get tool definitions for LLM
apiTools := registry.Tools()
// Execute a tool call
result, err := registry.Execute(toolCall)
Approval Manager
approval := agent.NewApprovalManager()
// Check if allowed
if !approval.IsAllowed("bash", args) {
result, err := approval.RequestApproval("bash", args)
if result.Decision == agent.ApprovalAlways {
approval.AddToAllowlist("bash", args)
}
}
// Get allowed tools/prefixes
allowed := approval.AllowedTools()
// Reset session
approval.Reset()
Configuration
Environment Variables
| Variable | Description |
|---|---|
OLLAMA_API_KEY |
Required for web_search tool |
OLLAMA_HOST |
Ollama server URL (default: http://localhost:11434) |
Bash Tool Limits
| Setting | Value |
|---|---|
| Timeout | 60 seconds |
| Max output | 50,000 bytes |
Web Search Limits
| Setting | Value |
|---|---|
| Max results | 5 |
| Content truncation | 300 chars per result |
Development
Running Tests
# Run all x/ tests
go test ./x/...
# Run with verbose output
go test -v ./x/agent/...
go test -v ./x/tools/...
Test Program
An isolated test program for the approval UI is available:
cd /tmp/selector_test
go build -o selector_test .
./selector_test
Expect Tests
Automated UI tests using expect:
/tmp/test_exhaustive.exp # Multiple input scenarios
/tmp/test_autohighlight.exp # Auto-highlight on typing
Known Issues
Double-Press Fix
The approval UI initially had an issue where keystrokes required double-pressing. This was caused by the readline library's background goroutine consuming stdin input. See DOUBLE_PRESS_FIX.md for details.
Fix: readline now reads stdin synchronously instead of via a background goroutine.
Future Enhancements
- More built-in tools (file read/write, HTTP requests)
- Tool result caching
- Persistent allowlist across sessions
- Custom tool loading from config
- Streaming tool output
- Tool execution sandboxing