// Package llm provides LLM backends for the 9P filesystem. package llm import ( "context" "encoding/json" "sync" ) // Session holds per-fid conversation state. // Each fid that opens the ask file gets its own session with isolated history. type Session struct { ID uint32 messages []Message lastResponse string lastTokens int totalTokens int mu sync.RWMutex } // NewSession creates a new session for the given fid. func NewSession(fid uint32) *Session { return &Session{ ID: fid, messages: make([]Message, 0), } } // Messages returns a copy of the session's conversation history. func (s *Session) Messages() []Message { s.mu.RLock() defer s.mu.RUnlock() result := make([]Message, len(s.messages)) copy(result, s.messages) return result } // MessagesJSON returns the session's conversation history as JSON. func (s *Session) MessagesJSON() ([]byte, error) { s.mu.RLock() defer s.mu.RUnlock() return json.MarshalIndent(s.messages, "", " ") } // AddMessage adds a message to the session's history. func (s *Session) AddMessage(role, content string) { s.mu.Lock() defer s.mu.Unlock() s.messages = append(s.messages, Message{Role: role, Content: content}) } // AddSystemMessage adds a system message to the session's history. func (s *Session) AddSystemMessage(content string) { s.mu.Lock() defer s.mu.Unlock() s.messages = append([]Message{{Role: "system", Content: content}}, s.messages...) } // SetLastResponse sets the last response for this session. func (s *Session) SetLastResponse(response string) { s.mu.Lock() defer s.mu.Unlock() s.lastResponse = response } // LastResponse returns the last response for this session. func (s *Session) LastResponse() string { s.mu.RLock() defer s.mu.RUnlock() return s.lastResponse } // LastTokens returns the token count from the last response. func (s *Session) LastTokens() int { s.mu.RLock() defer s.mu.RUnlock() return s.lastTokens } // TotalTokens returns cumulative token count for this session. func (s *Session) TotalTokens() int { s.mu.RLock() defer s.mu.RUnlock() return s.totalTokens } // SetTokens updates the token counts for this session. func (s *Session) SetTokens(last, total int) { s.mu.Lock() defer s.mu.Unlock() s.lastTokens = last s.totalTokens = total } // AddTokens adds to the token counts for this session. func (s *Session) AddTokens(tokens int) { s.mu.Lock() defer s.mu.Unlock() s.lastTokens = tokens s.totalTokens += tokens } // Reset clears the session's conversation history. func (s *Session) Reset() { s.mu.Lock() defer s.mu.Unlock() s.messages = make([]Message, 0) s.lastResponse = "" s.lastTokens = 0 s.totalTokens = 0 } // SessionManager maps fids to sessions and delegates to a shared backend. type SessionManager struct { sessions map[uint32]*Session backend Backend // shared backend for API calls and global settings mu sync.RWMutex } // NewSessionManager creates a new session manager with the given backend. func NewSessionManager(backend Backend) *SessionManager { return &SessionManager{ sessions: make(map[uint32]*Session), backend: backend, } } // Backend returns the underlying shared backend. func (sm *SessionManager) Backend() Backend { return sm.backend } // GetOrCreate returns the session for the given fid, creating one if necessary. func (sm *SessionManager) GetOrCreate(fid uint32) *Session { sm.mu.Lock() defer sm.mu.Unlock() if s, ok := sm.sessions[fid]; ok { return s } s := NewSession(fid) sm.sessions[fid] = s return s } // Get returns the session for the given fid, or nil if it doesn't exist. func (sm *SessionManager) Get(fid uint32) *Session { sm.mu.RLock() defer sm.mu.RUnlock() return sm.sessions[fid] } // Remove removes the session for the given fid. func (sm *SessionManager) Remove(fid uint32) { sm.mu.Lock() defer sm.mu.Unlock() delete(sm.sessions, fid) } // Reset clears the session for the given fid (but keeps the session). func (sm *SessionManager) Reset(fid uint32) { session := sm.GetOrCreate(fid) session.Reset() } // Ask sends a prompt using the session's conversation history. // The response is stored in the session and returned. func (sm *SessionManager) Ask(ctx context.Context, fid uint32, prompt string) (string, error) { session := sm.GetOrCreate(fid) // Get current history before adding new message history := session.Messages() // Use backend's AskWithHistory - it doesn't modify backend state response, tokens, err := sm.backend.AskWithHistory(ctx, history, prompt) if err != nil { session.SetLastResponse("Error: " + err.Error()) return "", err } // Add user message and assistant response to session history session.AddMessage("user", prompt) session.AddMessage("assistant", response) session.AddTokens(tokens) session.SetLastResponse(response) return response, nil } // ContextLimit returns the model's context window limit from the backend. func (sm *SessionManager) ContextLimit() int { return sm.backend.ContextLimit() }