|
|
||
|---|---|---|
| agents | ||
| cfg | ||
| docs | ||
| hooks/claude | ||
| internal | ||
| pkg | ||
| roles | ||
| scripts | ||
| services | ||
| tools | ||
| .gitignore | ||
| EVENTS.md | ||
| LICENSE.md | ||
| README.md | ||
| SECURITY.md | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| mkfile | ||
README.md
AnviLLM
LLM orchestrator using 9P — scriptable, multi-backend, crash-resilient
Research Context
AnviLLM is part of a research effort into using the Plan 9 file protocol (9P) as the foundation for multi-agent LLM systems.
AnviLLM tackled the first question: can you orchestrate multiple agentic CLIs (Claude Code, Kiro, Ollama) and get them to talk to each other using nothing but file operations? It runs each agent in a tmux session, exposes everything through a 9P filesystem — session state, control, inter-agent messaging — and lets you compose multi-agent workflows with shell scripts. The answer was yes, with caveats. The main limitation is uneven CLI hook support across backends — for example, Copilot lacks a hook for when the LLM finishes its turn, making it hard to reliably detect the running→idle transition.
The next steps build on what we learned here:
- ollie — a Go library for building LLM agents from scratch, rather than wrapping existing CLIs. Handles backends, tools, sandboxing, and skills. Knows nothing about 9P or orchestration. (Started as an effort to make local Ollama models more useful, and evolved from there.)
- ollie-9p — the core of the next research phase: how far can we take the idea of filesystem-as-agent-surface? Wraps ollie sessions in 9P, exposing meaningful agent state and interactions as a virtual filesystem. Any program that can read and write files — a frontend, an orchestrator, a supervisor, a shell script — can interact with agents directly. Maybe. Hopefully. We'll see.
Architecture
Core: anvillm (9P daemon), anvilmcp (optional MCP server, see anvillm-mcp) 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 (wayland-9pfuse-truncate branch, provides 9pfuse with truncate fix), tmux, landrun (kernel 5.13+), backend (Claude Code, Kiro, or Ollama + ollie)
Installation
git clone https://lneely.de/lkn/anvillm && cd anvillm && mk
Service integration (optional, see services/*/README.md):
# systemd user
cp services/systemd/anvillm-user.service ~/.config/systemd/user/
systemctl --user enable --now anvillm
# systemd system
sudo cp services/systemd/anvillm.service /etc/systemd/system/
sudo systemctl enable --now anvillm
# runit
sudo cp -r services/runit /etc/sv/anvillm
sudo ln -s /etc/sv/anvillm /var/service/
Usage
anvillm start # background
anvillm fgstart # foreground
anvillm status
anvillm stop
On startup, the server automatically mounts at ~/mnt/anvillm via 9pfuse.
Assist auto-starts if needed.
Namespaces: Run multiple instances via $NAMESPACE (default: /tmp/ns.$USER.:0)
NAMESPACE=/tmp/ns.$USER.:1 anvillm 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.
- Assist — Acme client (anvillm-acme)
For web frontends that can't speak 9P directly, anvilwebgw bridges HTTP to 9P.
Helper scripts served via 9P at tools/ provide 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 (used by 9beads) |
ANVILLM_TERMINAL |
foot |
Terminal command for tmux attach |
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).
Daemon recovery: If the daemon itself crashes but tmux sessions are still running, use Recover in Assist or manually restore sessions.
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 lneely.de/lkn/ollie
git clone https://lneely.de/lkn/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)
└── <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
Task tracking is provided by the separate 9beads service, which exposes a beads/ 9P filesystem.
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.
Roles
Roles define agent behavior and constraints. Each role is a Markdown file in ~/.config/anvillm/roles/ with YAML front-matter:
---
name: Developer
description: Code implementation agent
focus-areas: coding, development, implementation
worker: true
---
You are a developer. Your ONLY job is to write code. ...
Front-matter fields:
name— display namedescription— what the agent doesfocus-areas— comma-separated areas of responsibilityworker: true— marks the role as eligible for autonomous nudging byanvillm-supervisor
The body of the file is injected as agent context, defining the agent's responsibilities and constraints. Assign a role to a session by writing to its role file:
echo "developer" | 9p write anvillm/$ID/role
Or use anvilspawn, which sets the role automatically.
Available roles: developer, solo-developer, reviewer, tester, researcher, devops, pkgmgr, author, technical-editor, conductor
Autonomous Workflows
There are two approaches to autonomous workflows:
1. Supervisor (cron-based)
anvillm-supervisor runs as a cron job and performs periodic maintenance:
--nudge— sends work prompts to idle sessions whose role hasworker: truein its front-matter--orphans— unclaims beads assigned to sessions that no longer exist--auto-mount <workdir>— mounts a beads database for a working directory
Install the cron jobs:
mk cron-install
This is a lightweight, hands-off approach: spawn workers with anvilspawn, and the supervisor keeps them busy as long as there are ready beads.
2. Conductor (agent-based)
The Conductor role is an orchestration agent that decomposes goals into beads, spawns workers, and coordinates them to completion. Unlike the supervisor, the Conductor actively plans and adapts.
anvilspawn --role conductor kiro /path/to/project
CONDUCTOR_ID=$(9p read anvillm/list | head -1 | awk '{print $1}')
echo '{"to":"'$CONDUCTOR_ID'","type":"WORK_REQUEST","subject":"Execute","body":"Complete bead '$BEAD_ID'"}' | 9p write user/mail
The Conductor analyzes dependencies, spawns specialized bots, and delegates work in parallel. Agents notify the Conductor when blocked; it signals them to resume when dependencies resolve.
Monitoring:
- Event stream:
9p read anvillm/events(state changes, messages) - Debug logs:
~/.config/anvillm/logs/(setANVILLM_DEBUG=1for verbose output) - Foreground mode:
anvillm fgstartfor live stderr output
MCP Integration
anvilmcp (anvillm-mcp) provides sandboxed code execution for MCP clients (Claude Desktop, Kiro, etc.).
Note: anvilmcp is optional. Tools are served by anvillm via 9P and can be invoked directly:
bash <(9p read anvillm/tools/check_inbox.sh)
anvilmcp adds sandbox isolation (landlock/landrun) around execution for MCP clients.
Install: See anvillm-mcp for backend-specific setup.
See Code Execution User Guide for details.
Integrations
| Project | Description |
|---|---|
| 9beads | Task management via 9P — beads-based workflow tracking |
| anvillm-acme | Acme frontend for session management |
| 9beads-acme | Acme frontend for 9beads task management |
| anvillm-mcp | MCP server with sandboxed execution |
| agent-skills | Discoverable skill definitions for agents |
Troubleshooting
| Problem | Solution |
|---|---|
| Can't connect | anvillm status; try anvillm 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 | anvillm stop auto-cleans |
| Daemon won't stop | anvillm fgstart for logs |
| Bot stuck in "running" | Attach to the session's tmux window and type "stop work." to interrupt it and return to idle |
| See Configuration section for environment variables. |