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
+}