~kris/9p

llm9p

ref: 0afa911aad2ddf1b824c5e829bd12541e622e09e llm9p/CLAUDE.md -rw-r--r-- 9.6 KiB
0afa911a — pdfinn feat(llmfs): native Anthropic tool_use protocol support 6 months ago

#llm9p - Development Guide

This guide is for Claude Code and developers working on the llm9p codebase.

#Quick Reference

#Build and Run

# Build
go build -o llm9p ./cmd/llm9p

# Run with Anthropic API
ANTHROPIC_API_KEY=sk-... ./llm9p -addr :5640

# Run with Claude Max subscription (via CLI)
./llm9p -addr :5640 -backend cli

# Run with debug logging
ANTHROPIC_API_KEY=sk-... ./llm9p -addr :5640 -debug

#Testing

# Run all tests
go test ./...

# Run with coverage
go test -cover ./...

# Test specific package
go test ./internal/protocol/...
go test ./internal/llmfs/...

#Development Workflow

# Install dependencies
go mod tidy

# Format code
go fmt ./...

# Vet code
go vet ./...

# Build
go build -o llm9p ./cmd/llm9p

#Architecture Overview

#Project Structure

llm9p/
├── cmd/
│   └── llm9p/
│       └── main.go           # Entry point, CLI flags, server setup
├── internal/
│   ├── protocol/             # 9P2000 protocol implementation
│   │   ├── protocol.go       # Message types, constants, encoding
│   │   ├── message.go        # Individual message types
│   │   ├── server.go         # Connection handling
│   │   └── fs.go             # File/Dir interfaces, base implementations
│   ├── llm/                  # LLM client wrapper
│   │   └── client.go         # Anthropic API integration
│   └── llmfs/                # LLM filesystem implementation
│       ├── root.go           # Root directory construction
│       ├── ask.go            # Ask file (shim pattern)
│       ├── state.go          # Model, temperature files
│       ├── system.go         # System prompt file
│       ├── tokens.go         # Read-only token counter
│       ├── new.go            # Conversation reset trigger
│       ├── context.go        # Conversation history
│       ├── example.go        # Usage examples
│       └── stream.go         # Streaming interface
├── go.mod
├── go.sum
├── README.md
└── CLAUDE.md

#Key Components

  1. Protocol Layer (internal/protocol/)

    • Implements 9P2000 protocol
    • No external dependencies (stdlib only)
    • File and Dir interfaces define the filesystem abstraction
  2. LLM Client (internal/llm/)

    • backend.go - Backend interface for swappable LLM providers
    • client.go - Anthropic API client (requires API key)
    • cli_client.go - Claude Code CLI client (uses Max subscription)
    • Manages conversation state
    • Supports both sync and streaming responses
    • Tracks token usage (API only)
  3. LLM Filesystem (internal/llmfs/)

    • Implements each file in the LLM filesystem
    • AskFile is the core interaction point
    • State files (model, temperature) modify client settings
    • ChunkFile provides streaming access

#Adding a New File

  1. Create a new file in internal/llmfs/:
package llmfs

import (
    "github.com/NERVsystems/llm9p/internal/llm"
    "github.com/NERVsystems/llm9p/internal/protocol"
)

type MyFile struct {
    *protocol.BaseFile
    client *llm.Client
}

func NewMyFile(client *llm.Client) *MyFile {
    return &MyFile{
        BaseFile: protocol.NewBaseFile("myfile", 0666),
        client:   client,
    }
}

func (f *MyFile) Read(p []byte, offset int64) (int, error) {
    // Implement read
}

func (f *MyFile) Write(p []byte, offset int64) (int, error) {
    // Implement write
}

func (f *MyFile) Stat() protocol.Stat {
    s := f.BaseFile.Stat()
    // Update s.Length if dynamic
    return s
}
  1. Add to root directory in internal/llmfs/root.go:
root.AddChild(NewMyFile(client))

#Protocol Implementation Notes

#Message Flow

  1. Client sends T-message (request)
  2. Server responds with R-message (response)
  3. Each message has a tag for matching requests/responses

#Key 9P Operations

  • Tversion/Rversion - Protocol negotiation
  • Tattach/Rattach - Connect to filesystem
  • Twalk/Rwalk - Navigate directory tree
  • Topen/Ropen - Open a file
  • Tread/Rread - Read from file
  • Twrite/Rwrite - Write to file
  • Tclunk/Rclunk - Close a fid

#File Interfaces

// File is the interface that files must implement
type File interface {
    Stat() Stat
    Open(mode uint8) error
    Read(p []byte, offset int64) (int, error)
    Write(p []byte, offset int64) (int, error)
    Close() error
}

// Dir extends File with directory operations
type Dir interface {
    File
    Children() []File
    Lookup(name string) (File, error)
}

#Debugging

#Enable Debug Logging

ANTHROPIC_API_KEY=sk-... ./llm9p -debug

This logs all 9P messages sent and received.

#Test with 9p Client (plan9port)

# Using 9pfuse
9pfuse localhost:5640 /mnt/llm

# Using Plan 9's 9p tool (no mount needed)
9p -a localhost:5640 ls llm
9p -a localhost:5640 read llm/model
9p -a localhost:5640 read llm/temperature
9p -a localhost:5640 write llm/ask "What is 2+2?"
9p -a localhost:5640 read llm/ask        # Returns "4"
9p -a localhost:5640 read llm/tokens     # Returns token count

# Multi-turn conversation
9p -a localhost:5640 write llm/ask "Remember the number 42"
9p -a localhost:5640 write llm/ask "What number did I just mention?"
9p -a localhost:5640 read llm/ask        # Returns "42"

# Set system prompt (e.g., persona)
9p -a localhost:5640 write llm/system "Respond like a pirate"
9p -a localhost:5640 write llm/ask "Hello"
9p -a localhost:5640 read llm/ask        # Pirate-style response

# System prompt persists across resets
9p -a localhost:5640 write llm/new "reset"
9p -a localhost:5640 read llm/system     # Still "Respond like a pirate"

# Reset conversation
9p -a localhost:5640 write llm/new "reset"

#Test with Infernode (Inferno OS)

# Start infernode (from infernode directory)
./emu

# Inside infernode shell:
mkdir /n/llm
mount -A tcp!127.0.0.1!5640 /n/llm
ls -l /n/llm
cat /n/llm/model
echo 'What is the capital of France?' > /n/llm/ask
cat /n/llm/ask

Infernode Notes:

  • Use 127.0.0.1 not localhost (DNS resolution differs)
  • Create mount point before mounting: mkdir /n/llm
  • The -A flag enables anonymous auth

#Common Issues

"file not found"

  • Check file name spelling
  • Ensure file is added to root directory

"permission denied"

  • Check file mode (read-only files have mode 0444)
  • Write-only files have mode 0222

Connection refused

  • Ensure server is running
  • Check address/port

API errors

  • Check ANTHROPIC_API_KEY is set
  • Check API key is valid
  • Check rate limits

#Code Style

#Error Handling

Files should handle errors gracefully and expose them to the user:

func (f *AskFile) Write(p []byte, offset int64) (int, error) {
    response, err := f.client.Ask(ctx, prompt)
    if err != nil {
        // Store error so it can be read back
        f.lastResponse = "Error: " + err.Error()
        return len(p), nil
    }
    f.lastResponse = response
    return len(p), nil
}

#Stat Implementation

Always implement Stat() to return accurate Length:

func (f *MyFile) Stat() protocol.Stat {
    s := f.BaseFile.Stat()
    s.Length = uint64(len(f.content))
    return s
}

#Verified Test Cases

The following scenarios have been tested and verified working:

#plan9port (9p tool)

  • [x] ls llm - List filesystem root
  • [x] read llm/model - Returns model name
  • [x] read llm/temperature - Returns temperature
  • [x] read llm/tokens - Returns 0 initially, updates after queries
  • [x] read llm/_example - Returns usage examples
  • [x] write llm/temperature "0.5" - Updates temperature setting
  • [x] write llm/ask "What is 2+2?" followed by read llm/ask - Returns "4"
  • [x] Multi-turn conversation maintains context
  • [x] write llm/system "Respond like a pirate" - System prompt works
  • [x] System prompt persists across conversation resets
  • [x] write llm/new "reset" - Clears conversation history (keeps system prompt)

#Infernode (Inferno OS)

  • [x] mount -A tcp!127.0.0.1!5640 /n/llm - Mounts successfully
  • [x] ls -l /n/llm - Lists all files with correct permissions
  • [x] cat /n/llm/model - Returns model name
  • [x] cat /n/llm/temperature - Returns temperature
  • [x] echo 'prompt' > /n/llm/ask followed by cat /n/llm/ask - Full LLM interaction works
  • [x] LLM correctly identifies client as Inferno OS when asked

#Streaming (plan9port)

  • [x] ls stream - Lists ask and chunk files
  • [x] echo "prompt" | 9p write stream/ask - Starts streaming request
  • [x] 9p read stream/chunk - Returns streamed chunks
  • [x] Multiple chunks received for longer responses
  • [x] EOF returned when stream completes
  • [x] Short response ("Write a haiku") streams correctly
  • [x] Long response ("Count 1 to 20") streams all content

#CLI Backend (Claude Max subscription)

  • [x] Server starts with -backend cli flag
  • [x] Model returns "sonnet" (normalized name)
  • [x] echo "What is 2+2?" | 9p write ask returns correct answer
  • [x] Multi-turn conversation maintains context
  • [x] Model switching works (haiku, sonnet, opus)
  • [x] Conversation reset works via new file
  • [x] System messages work via context file

#Future Enhancements

  • [ ] Multiple conversation support (via subdirectories)
  • [ ] Prompt templates
  • [ ] Response caching
  • [ ] Rate limiting
  • [ ] Authentication
  • [ ] Unix socket support
  • [ ] Integration tests

#Resources