~kris/9p

llm9p

4e8ff2745844ec0f5dd9ec095944aeb8ca1f9def — pdfinn 7 months ago 5165f54
feat: Add system prompt file for persistent persona configuration

- Add system file (read/write) to set system prompt
- System prompt persists across conversation resets
- Add SystemPrompt() and SetSystemPrompt() to Backend interface
- Update both API and CLI clients to support dedicated system prompt
- Update documentation and examples

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
M CLAUDE.md => CLAUDE.md +10 -4
@@ 71,6 71,7 @@ llm9p/
│       ├── 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


@@ 215,11 216,15 @@ This logs all 9P messages sent and received.
9p -a localhost:5640 write llm/ask "What number did I just mention?"
9p -a localhost:5640 read llm/ask        # Returns "42"

# Add system message (e.g., persona)
9p -a localhost:5640 write llm/context "Respond like a pirate"
# 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"
```


@@ 307,8 312,9 @@ The following scenarios have been tested and verified working:
- [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/context "Respond like a pirate"` - System message works
- [x] `write llm/new "reset"` - Clears conversation history
- [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

M README.md => README.md +10 -2
@@ 141,6 141,12 @@ echo "claude-3-haiku-20240307" > /mnt/llm/model
# Adjust temperature
echo "0.5" > /mnt/llm/temperature

# Set a system prompt (persists across conversation resets)
echo "You are a helpful coding assistant." > /mnt/llm/system

# View current system prompt
cat /mnt/llm/system

# View conversation history
cat /mnt/llm/context



@@ 161,6 167,7 @@ cat /mnt/llm/_example
├── ask              # Write prompt, read response (same file)
├── model            # Read/write: current model name
├── temperature      # Read/write: temperature float (0.0-2.0)
├── system           # Read/write: system prompt (persists across resets)
├── tokens           # Read-only: last response token count
├── new              # Write anything to start fresh conversation
├── context          # Read: conversation history; Write: add system message


@@ 177,9 184,10 @@ cat /mnt/llm/_example
| `ask` | Returns last LLM response | Sends prompt to LLM (sync), stores response |
| `model` | Returns current model name | Sets model for subsequent requests |
| `temperature` | Returns current temperature | Sets temperature (0.0-2.0) |
| `system` | Returns current system prompt | Sets system prompt (persists across resets) |
| `tokens` | Returns last response token count | Permission denied |
| `new` | Permission denied | Any write resets conversation state |
| `context` | Returns JSON conversation history | Appends system message to context |
| `new` | Permission denied | Any write resets conversation (keeps system prompt) |
| `context` | Returns JSON conversation history | Appends system message to history |
| `_example` | Returns usage examples | Permission denied |
| `stream/ask` | Permission denied | Starts a streaming request |
| `stream/chunk` | Blocks until next chunk, returns it | Permission denied |

M internal/llm/backend.go => internal/llm/backend.go +6 -2
@@ 14,15 14,19 @@ type Backend interface {
	Temperature() float64
	// SetTemperature sets the temperature (0.0-2.0)
	SetTemperature(temp float64) error
	// SystemPrompt returns the current system prompt
	SystemPrompt() string
	// SetSystemPrompt sets the system prompt
	SetSystemPrompt(prompt string)
	// LastTokens returns token count from last response
	LastTokens() int
	// Messages returns conversation history
	Messages() []Message
	// MessagesJSON returns conversation history as JSON
	MessagesJSON() ([]byte, error)
	// AddSystemMessage adds a system message
	// AddSystemMessage adds a system message to conversation history
	AddSystemMessage(content string)
	// Reset clears conversation history
	// Reset clears conversation history (but preserves system prompt)
	Reset()
	// Ask sends a prompt and returns the response (blocking)
	Ask(ctx context.Context, prompt string) (string, error)

M internal/llm/cli_client.go => internal/llm/cli_client.go +29 -9
@@ 15,14 15,15 @@ import (
// CLIClient uses the Claude Code CLI for LLM requests.
// This allows using a Claude Max subscription instead of API tokens.
type CLIClient struct {
	mu          sync.RWMutex
	model       string
	temperature float64
	messages    []Message
	lastTokens  int
	streaming   bool
	streamChan  chan string
	streamDone  chan struct{}
	mu           sync.RWMutex
	model        string
	temperature  float64
	systemPrompt string
	messages     []Message
	lastTokens   int
	streaming    bool
	streamChan   chan string
	streamDone   chan struct{}
}

// cliResponse represents the JSON response from claude CLI


@@ 85,6 86,20 @@ func (c *CLIClient) SetTemperature(temp float64) error {
	return nil
}

// SystemPrompt returns the current system prompt
func (c *CLIClient) SystemPrompt() string {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.systemPrompt
}

// SetSystemPrompt sets the system prompt for subsequent requests
func (c *CLIClient) SetSystemPrompt(prompt string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.systemPrompt = prompt
}

// LastTokens returns the token count from the last response
// Note: CLI doesn't provide token counts, so this is always 0
func (c *CLIClient) LastTokens() int {


@@ 138,9 153,14 @@ func (c *CLIClient) buildPrompt() string {
	return strings.Join(parts, "\n\n")
}

// getSystemPrompt extracts system messages as a single string
// getSystemPrompt builds the full system prompt from dedicated prompt and history
func (c *CLIClient) getSystemPrompt() string {
	var systems []string
	// Add dedicated system prompt first
	if c.systemPrompt != "" {
		systems = append(systems, c.systemPrompt)
	}
	// Also include system messages from conversation history
	for _, msg := range c.messages {
		if msg.Role == "system" {
			systems = append(systems, msg.Content)

M internal/llm/client.go => internal/llm/client.go +42 -12
@@ 19,15 19,16 @@ type Message struct {

// Client wraps the Anthropic API client with conversation state
type Client struct {
	client      anthropic.Client
	mu          sync.RWMutex
	model       string
	temperature float64
	messages    []Message
	lastTokens  int
	streaming   bool
	streamChan  chan string
	streamDone  chan struct{}
	client       anthropic.Client
	mu           sync.RWMutex
	model        string
	temperature  float64
	systemPrompt string
	messages     []Message
	lastTokens   int
	streaming    bool
	streamChan   chan string
	streamDone   chan struct{}
}

// NewClient creates a new LLM client


@@ 73,6 74,20 @@ func (c *Client) SetTemperature(temp float64) error {
	return nil
}

// SystemPrompt returns the current system prompt
func (c *Client) SystemPrompt() string {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.systemPrompt
}

// SetSystemPrompt sets the system prompt for subsequent requests
func (c *Client) SetSystemPrompt(prompt string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.systemPrompt = prompt
}

// LastTokens returns the token count from the last response
func (c *Client) LastTokens() int {
	c.mu.RLock()


@@ 118,14 133,21 @@ func (c *Client) Ask(ctx context.Context, prompt string) (string, error) {
	// Add user message to history
	c.messages = append(c.messages, Message{Role: "user", Content: prompt})

	// Build the API messages
	// Build the API messages from conversation history
	apiMessages := make([]anthropic.MessageParam, 0, len(c.messages))
	var systemBlocks []anthropic.TextBlockParam

	// Add dedicated system prompt first
	if c.systemPrompt != "" {
		systemBlocks = append(systemBlocks, anthropic.TextBlockParam{
			Text: c.systemPrompt,
		})
	}

	for _, msg := range c.messages {
		switch msg.Role {
		case "system":
			// Collect system messages
			// Also include system messages from conversation history
			systemBlocks = append(systemBlocks, anthropic.TextBlockParam{
				Text: msg.Content,
			})


@@ 197,13 219,21 @@ func (c *Client) StartStream(ctx context.Context, prompt string) error {
	// Add user message to history
	c.messages = append(c.messages, Message{Role: "user", Content: prompt})

	// Build the API messages
	// Build the API messages from conversation history
	apiMessages := make([]anthropic.MessageParam, 0, len(c.messages))
	var systemBlocks []anthropic.TextBlockParam

	// Add dedicated system prompt first
	if c.systemPrompt != "" {
		systemBlocks = append(systemBlocks, anthropic.TextBlockParam{
			Text: c.systemPrompt,
		})
	}

	for _, msg := range c.messages {
		switch msg.Role {
		case "system":
			// Also include system messages from conversation history
			systemBlocks = append(systemBlocks, anthropic.TextBlockParam{
				Text: msg.Content,
			})

M internal/llmfs/example.go => internal/llmfs/example.go +7 -4
@@ 16,11 16,13 @@ Configuration:
  echo "claude-3-haiku-20240307" > model   # Change model
  cat temperature                # View current temperature (0.0-2.0)
  echo "0.5" > temperature       # Set temperature
  cat system                     # View current system prompt
  echo "You are a helpful coding assistant." > system  # Set system prompt

Conversation Management:
  cat context                    # View conversation history (JSON)
  echo "You are a helpful assistant." > context  # Add system message
  echo "" > new                  # Reset conversation
  echo "Additional context..." > context  # Add system message to history
  echo "" > new                  # Reset conversation (keeps system prompt)

Token Usage:
  cat tokens                     # View tokens from last response


@@ 51,9 53,10 @@ Files:
  ask          Read/write: prompt goes in, response comes out (sync)
  model        Read/write: current model name
  temperature  Read/write: sampling temperature (0.0-2.0)
  system       Read/write: system prompt (persists across resets)
  tokens       Read-only: token count from last response
  new          Write-only: any write resets conversation
  context      Read: JSON history; Write: add system message
  new          Write-only: any write resets conversation (keeps system prompt)
  context      Read: JSON history; Write: add system message to history
  _example     Read-only: this help text
  stream/ask   Write-only: starts a streaming request
  stream/chunk Read-only: returns next chunk (blocks), EOF when done

M internal/llmfs/root.go => internal/llmfs/root.go +1 -0
@@ 14,6 14,7 @@ func NewRoot(client llm.Backend) protocol.Dir {
	root.AddChild(NewAskFile(client))
	root.AddChild(NewModelFile(client))
	root.AddChild(NewTemperatureFile(client))
	root.AddChild(NewSystemFile(client))
	root.AddChild(NewTokensFile(client))
	root.AddChild(NewNewFile(client))
	root.AddChild(NewContextFile(client))

A internal/llmfs/system.go => internal/llmfs/system.go +52 -0
@@ 0,0 1,52 @@
package llmfs

import (
	"io"
	"strings"

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

// SystemFile exposes the system prompt (read/write)
type SystemFile struct {
	*protocol.BaseFile
	client llm.Backend
}

// NewSystemFile creates the system file
func NewSystemFile(client llm.Backend) *SystemFile {
	return &SystemFile{
		BaseFile: protocol.NewBaseFile("system", 0666),
		client:   client,
	}
}

func (f *SystemFile) Read(p []byte, offset int64) (int, error) {
	content := f.client.SystemPrompt()
	if content != "" {
		content += "\n"
	}
	if offset >= int64(len(content)) {
		return 0, io.EOF
	}
	n := copy(p, content[offset:])
	return n, nil
}

func (f *SystemFile) Write(p []byte, offset int64) (int, error) {
	prompt := strings.TrimSpace(string(p))
	f.client.SetSystemPrompt(prompt)
	return len(p), nil
}

func (f *SystemFile) Stat() protocol.Stat {
	s := f.BaseFile.Stat()
	content := f.client.SystemPrompt()
	if content != "" {
		s.Length = uint64(len(content) + 1) // +1 for newline
	} else {
		s.Length = 0
	}
	return s
}