14 KiB
AnviLLM
LLM orchestrator using 9P — scriptable, multi-backend, crash-resilient
Architecture
Core: anvilsrv (9P daemon), anvilmcp (MCP server) Frontends: Any 9P-speaking program — currently Assist (Acme) Benefits: Shared sessions, crash recovery, scriptable via 9P, cross-backend agent communication
Why 9P?
9P turns orchestration into file operations (read, write, ls, stat). Control agents with standard tools: cat, echo, shell scripts, or any language with file I/O. Compose workflows with Unix pipes, grep, awk, jq. Only requirement: a 9P client (plan9port's 9p or compatible). Built with 9fans.net/go.
Requirements
Go 1.21+, plan9port, tmux, landrun (kernel 5.13+), backend (Claude Code, Kiro, or Ollama + ollie)
Installation
git clone https://github.com/lneely/anvillm && cd anvillm && mk
Service integration (optional, see services/*/README.md):
# systemd user
cp services/systemd/anvilsrv-user.service ~/.config/systemd/user/
systemctl --user enable --now anvilsrv
# systemd system
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/
Usage
anvilsrv start # background
anvilsrv fgstart # foreground
anvilsrv status
anvilsrv stop
Assist auto-starts if needed.
Namespaces: Run multiple instances via $NAMESPACE (default: /tmp/ns.$USER.:0)
NAMESPACE=/tmp/ns.$USER.:1 anvilsrv start
NAMESPACE=/tmp/ns.$USER.:1 Assist
Frontends
Any program that speaks 9P can be a frontend. The 9P filesystem exposes session management, state, messaging, and configuration as plain files — so building a new frontend is just reading and writing files. Past prototypes have included a TUI, a web UI with HTTP gateway, and an Emacs major mode, all built against the same API. The current frontend is:
- Assist — an Acme client (middle-click
Assistto launch)
For web frontends that can't speak 9P directly, anvilwebgw bridges HTTP to 9P.
The mcptools scripts (served via 9P at tools/) also provide convenient building blocks for frontends:
bash <(9p read tools/<scriptname>)
Configuration
Environment Variables
| Variable | Default | Description |
|---|---|---|
NAMESPACE |
/tmp/ns.$USER.:0 |
9P namespace for server/client communication |
ANVILLM_BEADS_PATH |
~/.beads |
Beads database location (shared across namespaces) |
ANVILLM_TERMINAL |
foot |
Terminal command for tmux attach (Assist) |
ANTHROPIC_API_KEY |
— | Claude API key (optional if using claude /login) |
CLAUDE_AGENT_NAME |
anvillm-agent |
Claude agent configuration name |
KIRO_API_KEY |
— | Kiro API key (optional if using kiro-cli login) |
ANVILLM_OLLAMA_MODEL |
qwen3:8b |
Ollama model to use for ollama backend |
ANVILLM_SKILLS_DIR |
$CLAUDE_CONFIG_DIR/skills:~/.kiro/skills:~/.config/anvillm/skills |
Colon-separated skill directories (searched in order) |
Skills System
Skills are loaded from multiple directories via the anvillm/skills 9pfs. By default, searches:
$CLAUDE_CONFIG_DIR/skills(ifCLAUDE_CONFIG_DIRset)~/.kiro/skills~/.config/anvillm/skills
Override with ANVILLM_SKILLS_DIR (colon-separated paths). Skills are organized by intent (virtual directories) and discovered via SKILL.md front-matter.
Usage:
9p ls anvillm/skills # list intents
9p read anvillm/skills/help # search index
9p read anvillm/skills/tasks/beads/SKILL.md
Sandbox Config Templates
Available in sandbox YAML files (~/.config/anvillm/):
| Template | Expands To | Description |
|---|---|---|
{CWD} |
Current working directory | Session's working directory |
{HOME} |
User home directory | $HOME |
{TMPDIR} |
Temp directory | $TMPDIR or /tmp |
{XDG_CONFIG_HOME} |
XDG config | $XDG_CONFIG_HOME or ~/.config |
{XDG_DATA_HOME} |
XDG data | $XDG_DATA_HOME or ~/.local/share |
{XDG_CACHE_HOME} |
XDG cache | $XDG_CACHE_HOME or ~/.cache |
{XDG_STATE_HOME} |
XDG state | $XDG_STATE_HOME or ~/.local/state |
{ANY_ENV_VAR} |
Environment variable | Any $ENV_VAR from the environment |
Templates use {VARNAME} syntax. Any environment variable can be referenced.
Backends & Sandboxing
Backends: Claude (npm install -g @anthropic-ai/claude-code), Kiro (kiro.dev), Ollama (local models via ollie)
Sandbox: landrun (always enabled) — Defaults: CWD//tmp/config (rw), /usr//lib//bin (ro+exec), no network
Config (~/.config/anvillm/): Layered YAML files, most permissive wins:
- Default sandbox:
sandbox/default.yaml
network: {enabled: true, unrestricted: true}
filesystem: {rw: ["{CWD}", "{HOME}/.npm"]}
Templates: {CWD}, {HOME}, {TMPDIR}, {XDG_*} (see Configuration)
Kernel requirements: 5.13+ (Landlock v1), 6.7+ (v4), 6.10+ (v5 network)
Set best_effort: true for unsandboxed fallback (⚠️ if no Landlock support)
Session lifecycle:
State transitions: idle ↔ running cycle via CLI hooks (userPromptSubmit when user sends prompt, stop when agent finishes). Crash → error → auto-restart → starting. Note: any state can transition to stopped or killed (not shown); stopped can restart → starting.
Self-healing: Auto-restarts crashes every 5s (preserves context/alias/cwd), skips intentional stops
Restored sessions automatically resume the latest conversation (kiro: -r, claude: -c).
Add backend: Implement CommandHandler/StateInspector in internal/backends/yourbackend.go, register in main.go
Ollama Backend
Run local LLMs via Ollama.
Requirements:
-
Ollama: Install from ollama.com
curl -fsSL https://ollama.com/install.sh | sh ollama serve ollama pull qwen2.5-coder:7b -
Ollie CLI: Install from github.com/lneely/ollie
git clone https://github.com/lneely/ollie && cd ollie && go install
Usage:
echo 'new ollama /path/to/project' | 9p write anvillm/ctl
Configuration: Set model via ANVILLM_OLLAMA_MODEL (default: qwen3:8b)
ANVILLM_OLLAMA_MODEL=llama3.2 echo 'new ollama /path' | 9p write anvillm/ctl
9P Filesystem
$NAMESPACE/agent:
anvillm/
├── ctl # "new <backend> <cwd>" creates session
├── list # id, alias, state, pid, cwd
├── events # Event stream (state changes, messages)
├── beads/ # Task tracking (persistent across namespaces)
│ ├── ctl # Commands: init, new, claim, unclaim, complete, fail,
│ │ # dep, undep, update, delete, comment, label, unlabel,
│ │ # set-capability, open, defer, reopen, mount, umount, sync
│ ├── mtab # Mounted projects
│ ├── list # All beads as JSON (optional limit arg)
│ ├── ready # Ready beads (no blockers) as JSON
│ ├── deferred # Deferred beads as JSON
│ ├── blocked # Blocked beads as JSON
│ ├── stale # Stale beads as JSON
│ ├── search/ # search/<query> — full-text search
│ ├── by-ref/ # by-ref/<ref> — lookup by external reference
│ ├── batch/ # batch/<id1,id2,...> — fetch multiple beads
│ ├── label/ # label/<name> — filter by label
│ ├── children/ # children/<id> — subtasks of a bead
│ └── <bead-id>/
│ ├── status
│ ├── title
│ ├── description
│ ├── assignee
│ ├── json
│ ├── comments
│ ├── labels
│ ├── dependents
│ ├── tree
│ └── events
└── <id>/
├── 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
├── role # Role name
├── tasks # Task names
├── tmux # Tmux session name
├── inbox # Incoming messages (JSON)
├── outbox # Outgoing messages (JSON)
├── completed # Archived messages (JSON, "Archive" in Assist)
└── mail # Write messages (convenience)
Client Interactions:
Different clients interact with different parts of the filesystem: frontends read state, control files manage sessions, scripts consume events.
Beads
Persistent task tracking via beads — Dependency-aware graph
echo 'init' | 9p write anvillm/beads/ctl
echo 'new "Implement login" "Add JWT auth"' | 9p write anvillm/beads/ctl
echo 'claim bd-a1b2' | 9p write anvillm/beads/ctl
echo 'complete bd-a1b2' | 9p write anvillm/beads/ctl
echo 'dep bd-child bd-parent' | 9p write anvillm/beads/ctl # child blocks parent
9p read anvillm/beads/ready | jq -r '.[] | "\(.id): \(.title)"'
Config: ~/.beads/ (override: ANVILLM_BEADS_PATH) — Shared across namespaces — See internal/beads/README.md
Events & Mailbox
Mailbox Flow:
Cross-backend communication: messages route between any participants (user, Claude agents, Kiro agents, Ollama agents) via the mailbox system.
Examples:
# Events
9p read anvillm/events # {"type":"state_change","session_id":"...","state":"running",...}
# Mailbox
echo '{"to":"a3f2b9d1","type":"REVIEW_REQUEST","subject":"...","body":"..."}' | 9p write anvillm/b4e3c8f2/mail
9p read anvillm/a3f2b9d1/inbox
9p read anvillm/a3f2b9d1/completed
Basic Session Example
# Create session
echo 'new claude /home/user/project' | 9p write anvillm/ctl
# List sessions (newest first)
9p read anvillm/list
# Get most recent session ID
ID=$(9p read anvillm/list | head -1 | awk '{print $1}')
# Send prompt via mailbox
echo '{"to":"'$ID'","type":"PROMPT_REQUEST","subject":"User prompt","body":"Hello"}' | 9p write user/mail
# Check state
9p read anvillm/$ID/state
# Read response from inbox
9p read user/inbox
See SECURITY.md
Spawning Agents
anvilspawn creates a new agent session with a given backend, role, and working directory:
anvilspawn <backend> <role> [workdir]
For example:
anvilspawn kiro developer /path/to/project
anvilspawn claude reviewer
If workdir is omitted, it defaults to the current directory. The script creates the session, assigns an alias derived from the directory and role, and sets the role. The agent ID is printed to stdout.
Autonomous Workflows
Automate workflows with Taskmaster and Conductor bots. Input project plan → Taskmaster creates tasks with dependencies → Conductor orchestrates parallel execution.
Conductor receives the top-level bead ID, analyzes dependencies, and spawns agents to work in parallel. Agents notify Conductor when blocked; Conductor signals them to resume when dependencies resolve.
Usage
Examples use raw 9P calls. These actions can also be performed from Assist.
1. Taskmaster: Create tasks from project plan
./bot-templates/Taskmaster kiro /path/to/project
TASKMASTER_ID=$(9p read anvillm/list | head -1 | awk '{print $1}')
# Send project plan
echo '{"to":"'$TASKMASTER_ID'","type":"PROMPT_REQUEST","subject":"Create tasks","body":"<your project plan>"}' | 9p write user/mail
# Get top-level bead ID
BEAD_ID=$(9p read anvillm/beads/list | jq -r '.[0].id')
2. Conductor: Execute work
./bot-templates/Conductor kiro /path/to/project
CONDUCTOR_ID=$(9p read anvillm/list | head -1 | awk '{print $1}')
# Send work request
echo '{"to":"'$CONDUCTOR_ID'","type":"WORK_REQUEST","subject":"Execute","body":"Complete bead '$BEAD_ID'"}' | 9p write user/mail
Conductor analyzes dependencies, spawns specialized bots, and delegates work in parallel.
Monitoring: Use Assist or any 9P client. For custom integrations, see EVENTS.md.
MCP Integration
anvilmcp exposes AnviLLM via Model Context Protocol for Claude Desktop, Cline, etc.
Install:
./kiro-cli/install-mcp.sh # Adds to ~/.kiro/settings/cli.json
Tool: execute_code — the only tool needed. Executes bash scripts in an isolated subprocess (sandboxed via landrun). Agents discover and invoke all AnviLLM functionality through 9P and helper scripts:
9p read anvillm/tools/check_inbox.sh # discover scripts
bash <(9p read tools/send_message.sh) ... # invoke them
See Code Execution User Guide for details.
Troubleshooting
| Problem | Solution |
|---|---|
| Can't connect | anvilsrv status; try anvilsrv start |
| Session won't start | Check stderr; verify backend installed |
| Landlock ABI error | Set best_effort: true or upgrade kernel |
| Permission denied | Add paths to layered config |
| Orphaned tmux | tmux kill-session -t anvillm-0 |
| 9P not working | 9p ls agent |
| Stale PID | anvilsrv stop auto-cleans |
| Daemon won't stop | anvilsrv fgstart for logs |
| See Configuration section for environment variables. |