Agents were getting stuck in 'running' state after sending messages to user. Previously only peer messages triggered idle transition. Now all outbox messages (user and peer) trigger transition to idle, allowing agents to receive new messages and preventing stuck states. Verified by comprehensive state machine simulation covering all transitions, edge cases, concurrency, and session lifecycle. |
||
|---|---|---|
| cfg | ||
| cmd | ||
| elisp | ||
| internal | ||
| scripts | ||
| services | ||
| skills | ||
| workflows | ||
| .gitignore | ||
| IDEAS.md | ||
| LICENSE.md | ||
| README.md | ||
| SECURITY.md | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| mkfile | ||
README.md
AnviLLM
9P-based LLM orchestrator. Front-ends for Acme, Emacs, TUI, and web.
Architecture
AnviLLM consists of:
- anvilsrv: Background daemon managing sessions via 9P
- Assist: Acme UI client (auto-starts daemon if needed)
- anvillm.el: For our emacs friends ;)
- anvillm: Terminal UI (curses-based) for everyone else
- anvilweb: Web interface for browser-based access
This separation allows:
- Multiple clients to connect to the same sessions
- Server survives client crashes/restarts
- Scriptable workflows via 9P without UI dependencies
- Service management (systemd/runit integration)
- Choice of interface: Acme, terminal, Emacs, or web browser
Requirements
- Go 1.21+, plan9port, tmux
- landrun (sandboxing, Linux kernel 5.13+)
- Backend CLI: Claude Code or Kiro CLI
Installation
git clone https://github.com/lneely/anvillm
cd anvillm
mk # installs anvilsrv and Assist to $HOME/bin
Service Integration
Install as system service (optional):
systemd:
# User service (recommended)
cp services/systemd/anvilsrv-user.service ~/.config/systemd/user/
systemctl --user enable --now anvilsrv
# System service
sudo cp services/systemd/anvilsrv.service /etc/systemd/system/
sudo systemctl enable --now anvilsrv
runit:
sudo cp -r services/runit /etc/sv/anvilsrv
sudo ln -s /etc/sv/anvilsrv /var/service/
See services/*/README.md for details.
Usage
Starting the Server
Automatic: Assist auto-starts anvilsrv if not running.
Manual:
anvilsrv start # Daemonize (background)
anvilsrv fgstart # Foreground (for debugging)
anvilsrv status # Check if running
anvilsrv stop # Shutdown
Multiple Namespaces
anvilsrv respects the $NAMESPACE environment variable, allowing multiple independent instances for different Acme sessions (similar to running multiple Acme instances with acme -a).
Default namespace: /tmp/ns.$USER.:0
Example: Running multiple instances
# Start default instance (namespace :0)
anvilsrv start
# Start second instance (namespace :1)
NAMESPACE=/tmp/ns.$USER.:1 anvilsrv start
# Check status of each instance
anvilsrv status # :0
NAMESPACE=/tmp/ns.$USER.:1 anvilsrv status # :1
# Each instance has isolated:
# - PID file: $NAMESPACE/anvilsrv.pid
# - 9P socket: $NAMESPACE/agent
# - Session list (completely independent)
# Connect Assist to specific instance
NAMESPACE=/tmp/ns.$USER.:1 Assist
This enables running separate AnviLLM environments for different projects or Acme workspaces.
Terminal UI (anvillm)
Run anvillm for a curses-based interface (no Acme required):
anvillm
Keyboard shortcuts:
s- Start new session (shows backend menu)p- Send prompt to selected sessiont- Stop selected sessionR- Restart selected sessionK- Kill selected sessiona- Set session aliasr- Refresh session listd- Daemon status?- Helpq- Quit
To attach to the anvillm session, simply open a different terminal and use tmux attach.
Navigation:
- Arrow keys (
↑/↓) - Vim-style (
j/k) - Emacs-style (
C-n/C-p)
Like Assist, the TUI reads/writes the 9P filesystem for all operations.
Emacs Interface (anvillm.el)
Run M-x anvillm in Emacs for a native Emacs interface:
Installation:
Copy anvillm.el to ~/.emacs.d/lisp/ or wherever your elisp files live. Then, add to your emacs init:
(add-to-list 'load-path "/path/to/anvillm.el/")
(require 'anvillm)
Keybindings:
- s - Start new session (select backend)
- p - Send prompt to selected session
- P - Send prompt to selected session using minibuffer
- t - Stop selected session
- R - Restart selected session
- K - Kill selected session
- a - Set alias for selected session
- r - Refresh session list
- g - Refresh session list (standard Emacs binding)
- d - Daemon status
- q - Quit
- ? - Help
Navigation:
- Standard Emacs navigation (
n,p,C-n,C-p) - Sessions displayed in a sortable table (click column headers)
The Emacs interface uses the 9p command from plan9port to interact with the filesystem.
Web Interface (anvilweb)
Run anvilweb for a browser-based interface:
anvilweb # starts on :8080
anvilweb -addr :3000 # custom port
Features:
- Start new sessions (Kiro/Claude)
- View all sessions with live state updates
- Send prompts, edit context and aliases
- Stop/restart/kill sessions
- Auto-refreshes every 5 seconds
Security Warning:
⚠️ The web interface has NO authentication. Anyone who can reach the server port can:
- View all sessions
- Send prompts to any session
- Start/stop/kill sessions
- Modify session contexts
Only run anvilweb on localhost or trusted networks. For remote access, use SSH port forwarding:
# On remote machine
anvilweb -addr localhost:8080
# On local machine
ssh -L 8080:localhost:8080 user@remote
# Then browse to http://localhost:8080
Or use a reverse proxy with authentication (nginx, caddy, etc.).
Acme Interface (Assist)
Type Assist in Acme and middle-click to open the /AnviLLM/ session manager.
Commands
Main window (/AnviLLM/) tag: Get Attach Stop Restart Kill Alias Context Log Daemon Sandbox
| Command | Description |
|---|---|
Kiro <dir> |
Start kiro-cli session |
Claude <dir> |
Start Claude session |
Stop <id> |
Stop session (preserves tmux) |
Restart <id> |
Restart stopped session |
Kill <id> |
Terminate session |
Attach <id> |
Open tmux terminal |
Alias <id> <name> |
Name a session |
Context <id> |
Edit context (prepended to prompts) |
Log <id> |
View session log |
Daemon |
Manage anvilsrv daemon (start/stop/status) |
Sandbox |
Configure sandbox settings |
Right-click a session ID to open its prompt window. Select text anywhere and 2-1 chord on a session ID for fire-and-forget prompts.
Note: Assist reconnects to the running anvilsrv daemon, so sessions persist across Assist restarts.
Prompt window (+Prompt.<id>) tag: Send
Type prompt, click Send.
9P Filesystem
The anvilsrv daemon exposes sessions via a 9P filesystem at $NAMESPACE/agent (typically /tmp/ns.$USER/agent).
agent/
├── ctl # "new <backend> <cwd>" creates session
├── list # id, alias, state, pid, cwd
└── <id>/
├── in # Write prompts
├── out # Read responses
├── ctl # "stop", "restart", "kill"
├── state # starting, idle, running, stopped, error, exited
├── context # Prepended to prompts (r/w)
├── alias # Session name (r/w)
├── pid # Process ID
├── cwd # Working directory
└── backend # Backend name
Path Validation: Session creation validates and cleans paths (e.g., /../../../etc → /etc), and rejects nonexistent directories.
Example:
echo 'new claude /home/user/project' | 9p write agent/ctl
echo 'Hello' | 9p write agent/a3f2b9d1/in
9p read agent/a3f2b9d1/state # → "running"
echo 'stop' | 9p write agent/a3f2b9d1/ctl
9p read agent/a3f2b9d1/state # → "stopped"
Security: See SECURITY.md for 9P socket authentication limitations.
Backends
| Backend | Install |
|---|---|
| Claude | npm install -g @anthropic-ai/claude-code |
| Kiro | Kiro CLI |
Both run with full permissions inside the sandbox (--dangerously-skip-permissions, --trust-all-tools).
Adding Backends
Create internal/backends/yourbackend.go, implement CommandHandler and StateInspector interfaces, register in main.go. See existing backends for examples.
Self-Healing
The anvilsrv daemon monitors all sessions every 5 seconds and automatically restarts sessions that crash unexpectedly. This ensures long-running workflows remain resilient to backend failures.
Behavior:
- Detects when a session process terminates without explicit
Stopcommand - Automatically restarts the session in the same working directory
- Preserves session context and alias
- Rate-limited to prevent restart loops (minimum 5 seconds between attempts)
- Does not restart sessions stopped intentionally via
Stopcommand
Sessions that crash are transparently restarted without user intervention. Check logs with anvilsrv fgstart to see auto-restart activity.
Sandboxing
Backends run inside landrun sandboxes. Sandboxing cannot be disabled — it's the only safety layer.
Defaults
- Filesystem: CWD,
/tmp, config dirs (rw);/usr,/lib,/bin(ro+exec) - Network: disabled
- Mode: strict (
best_effort: false) — sessions fail if sandbox unavailable
Configuration
Click Sandbox in main window, then Edit to modify ~/.config/anvillm/sandbox.yaml.
Path templates: {CWD}, {HOME}, {TMPDIR}
# Enable network
network:
enabled: true
unrestricted: true
# Add paths
filesystem:
rw:
- "{CWD}"
- "{HOME}/.npm"
Changes apply to new sessions only.
Kernel Requirements
| Kernel | Landlock ABI | Features |
|---|---|---|
| 5.13+ | v1 | Filesystem |
| 6.7+ | v4 | Improved FS |
| 6.10+ | v5 | Network |
Best-Effort Mode
Set best_effort: true for graceful degradation on older kernels.
Warning: If landrun is missing or kernel lacks Landlock, sessions run completely unsandboxed. Only enable if you accept this risk.
Reproducible Workflows
The 9P filesystem enables scripted multi-agent workflows. Create sessions, set contexts, and wire agents together programmatically.
Example: Developer-Reviewer
Two agents collaborate on code changes — one implements, one reviews:
./scripts/DevReview claude /path/to/project
Creates paired sessions with contexts that instruct agents to:
- Developer implements and stages changes
- Developer sends review request via
agent/{reviewer-id}/in - Reviewer examines diff, sends feedback or "LGTM"
- Loop until approved
Example: Planning
Three-agent workflow for documentation tasks:
./scripts/Planning kiro /path/to/docs
- Research: Queries codebase and knowledge base
- Engineering: Writes/updates documentation, queries research when needed
- Tech-editor: Reviews for quality, requests changes from engineering
Writing Your Own
Key patterns from the example scripts:
# Create session, capture ID
echo "new claude /project" | 9p write agent/ctl
ID=$(9p read agent/list | tail -1 | awk '{print $1}')
# Set context (defines agent behavior)
cat <<EOF | 9p write agent/$ID/context
You are a code reviewer. When you receive code...
EOF
# Agents communicate via mailboxes
cat > /tmp/msg.json <<'EOF'
{"to":"$PEER_ID","type":"REVIEW_REQUEST","subject":"Review","body":"Please review staged changes"}
EOF
9p write agent/$ID/outbox/msg-$(date +%s).json < /tmp/msg.json
See workflows/DevReview and workflows/Planning for complete examples.
Troubleshooting
| Problem | Solution |
|---|---|
| "Failed to connect to anvilsrv" | Check anvilsrv status; try anvilsrv start |
| Session won't start | Check stderr; verify which claude or which kiro-cli |
| Landlock ABI error | Set best_effort: true or upgrade kernel |
| Permission denied | Add paths to sandbox config |
| Orphaned tmux | tmux kill-session -t anvillm-claude |
| 9P not working | Verify plan9port: 9p ls agent |
| Stale PID file | anvilsrv stop cleans up automatically |
| Daemon won't stop | Check logs with anvilsrv fgstart |
| anvilweb can't connect | Ensure anvilsrv is running; check namespace matches |
| anvilweb port in use | Use -addr :PORT to specify different port |
Debugging: Run anvilsrv fgstart to see daemon logs in foreground.
Terminal: Attach command uses $ANVILLM_TERMINAL or defaults to foot. Configure in environment or see cmd/Assist/main.go.