~kris/9p

llm9p

ref: ed43a61bf88f3238d4844391e1f7926d8490ae50 llm9p/internal/llm/session.go -rw-r--r-- 4.9 KiB
ed43a61b — pdfinn feat(llm9p): Add per-fid session isolation and prefill support 7 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
// 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()
}