Files
agentu/internal/session/manager.go
T

306 lines
7.8 KiB
Go

package session
import (
"fmt"
"net/http"
"sort"
"strings"
"time"
"agentu/internal/agent"
"agentu/internal/config"
"agentu/internal/llm"
)
type Manager struct {
cfg *config.Config
agent *agent.Agent
httpClient *http.Client
providerName string
provider config.ProviderConfig
store *Store
currentSession *Session
}
func NewManager(cfg *config.Config, assistant *agent.Agent, httpClient *http.Client, store *Store) (*Manager, error) {
if httpClient == nil {
httpClient = http.DefaultClient
}
providerName := cfg.DefaultProviderName()
provider, ok := cfg.Providers[providerName]
if !ok {
return nil, fmt.Errorf("no provider is configured")
}
m := &Manager{
cfg: cfg,
agent: assistant,
httpClient: httpClient,
providerName: providerName,
provider: provider,
store: store,
}
return m, nil
}
// ResumeLatest loads the most recent session, or creates a new one.
func (m *Manager) ResumeLatest() error {
if m.store == nil {
return nil
}
metas, err := m.store.List()
if err != nil {
return err
}
if len(metas) > 0 {
return m.Resume(metas[0].ID)
}
m.newSession()
return nil
}
// Resume loads a session by ID and restores it into the agent.
func (m *Manager) Resume(id string) error {
if m.store == nil {
return fmt.Errorf("session storage is not configured")
}
sess, err := m.store.Load(id)
if err != nil {
return err
}
m.currentSession = sess
m.agent.SetMessages(sess.Messages)
m.agent.SetLastUsage(sess.LastUsage)
return nil
}
// Save persists the current session to disk.
func (m *Manager) Save() error {
if m.store == nil || m.currentSession == nil {
return nil
}
m.currentSession.Messages = m.agent.Messages()
m.currentSession.LastUsage = m.agent.LastUsage()
m.currentSession.Provider = m.providerName
m.currentSession.Model = m.provider.Model
// Auto-name if empty
if m.currentSession.Name == "" {
m.currentSession.Name = DefaultName(m.currentSession.Messages)
}
return m.store.Save(m.currentSession)
}
// Rename changes the current session name and saves.
func (m *Manager) Rename(name string) (string, error) {
name = strings.TrimSpace(name)
if name == "" {
return "", fmt.Errorf("name is required")
}
if m.currentSession == nil {
return "", fmt.Errorf("no active session")
}
m.currentSession.Name = name
if err := m.Save(); err != nil {
return "", err
}
return fmt.Sprintf("Renamed session to %q.", name), nil
}
// Sessions returns a formatted list of all sessions.
func (m *Manager) Sessions() (string, error) {
if m.store == nil {
return "", fmt.Errorf("session storage is not configured")
}
metas, err := m.store.List()
if err != nil {
return "", err
}
if len(metas) == 0 {
return "No saved sessions.", nil
}
var b strings.Builder
b.WriteString("Sessions:\n")
for _, meta := range metas {
name := meta.Name
if name == "" {
name = "(unnamed)"
}
current := ""
if m.currentSession != nil && meta.ID == m.currentSession.ID {
current = " (current)"
}
ts := meta.UpdatedAt.Local().Format("2006-01-02 15:04")
fmt.Fprintf(&b, " %s %-30s %s %d msgs%s\n", meta.ID, fitName(name, 30), ts, meta.MessageCount, current)
}
return b.String(), nil
}
// NewSession starts a fresh session, saving the current one first.
func (m *Manager) NewSession() (string, error) {
// Save current if it has user messages
if m.currentSession != nil {
msgs := m.agent.Messages()
hasUser := false
for _, msg := range msgs {
if msg.Role == llm.RoleUser {
hasUser = true
break
}
}
if hasUser {
m.currentSession.Messages = msgs
m.currentSession.Provider = m.providerName
m.currentSession.Model = m.provider.Model
if m.currentSession.Name == "" {
m.currentSession.Name = DefaultName(msgs)
}
if m.store != nil {
_ = m.store.Save(m.currentSession)
}
}
}
m.newSession()
return "Session saved. Starting new session.", nil
}
func (m *Manager) newSession() {
m.currentSession = &Session{
ID: GenerateID(),
CreatedAt: time.Now().UTC(),
Provider: m.providerName,
Model: m.provider.Model,
}
m.agent.Clear()
}
func (m *Manager) CurrentSessionID() string {
if m.currentSession == nil {
return ""
}
return m.currentSession.ID
}
func (m *Manager) CurrentSessionName() string {
if m.currentSession == nil {
return ""
}
if m.currentSession.Name != "" {
return m.currentSession.Name
}
return m.currentSession.ID
}
func (m *Manager) CurrentProvider() string {
return m.providerName
}
func (m *Manager) CurrentModel() string {
return m.provider.Model
}
func (m *Manager) CurrentThinking() string {
if !m.provider.ThinkingConfigured {
return "unset"
}
return m.provider.Thinking
}
func (m *Manager) Info() string {
var b strings.Builder
fmt.Fprintf(&b, "provider: %s\n", m.CurrentProvider())
fmt.Fprintf(&b, "model: %s\n", m.CurrentModel())
fmt.Fprintf(&b, "thinking: %s\n", m.CurrentThinking())
fmt.Fprintf(&b, "providers: %s\n", strings.Join(m.providerNames(), ", "))
fmt.Fprintf(&b, "models: %s\n", strings.Join(m.provider.Models, ", "))
fmt.Fprintf(&b, "thinking levels: %s\n", strings.Join(config.ThinkingLevels(), ", "))
b.WriteString("usage: /model provider <name> | /model model <name> | /model thinking <level>")
return b.String()
}
func (m *Manager) SetProvider(name string) (string, error) {
provider, ok := m.cfg.Providers[name]
if !ok {
return "", fmt.Errorf("unknown provider %q; available: %s", name, strings.Join(m.providerNames(), ", "))
}
m.providerName = name
m.provider = provider
m.syncAgent(llm.NewOpenAICompatibleClient(provider.BaseURL, provider.APIKey, m.httpClient))
return "Switched provider.\n" + m.summary(), nil
}
func (m *Manager) SetModel(model string) (string, error) {
model = strings.TrimSpace(model)
if model == "" {
return "", fmt.Errorf("model is required")
}
if !contains(m.provider.Models, model) {
return "", fmt.Errorf("model %q is not configured for provider %q; available: %s", model, m.providerName, strings.Join(m.provider.Models, ", "))
}
m.provider = m.provider.WithModel(model)
m.cfg.Providers[m.providerName] = m.provider
m.syncAgent(nil)
return "Switched model.\n" + m.summary(), nil
}
func (m *Manager) SetThinking(level string) (string, error) {
level = strings.ToLower(strings.TrimSpace(level))
if !config.IsThinkingLevel(level) {
return "", fmt.Errorf("thinking must be one of: %s", strings.Join(config.ThinkingLevels(), ", "))
}
m.provider.Thinking = level
m.provider.ThinkingConfigured = true
if m.provider.ThinkingParam == "" {
m.provider.ThinkingParam = "thinking"
}
m.cfg.Providers[m.providerName] = m.provider
m.syncAgent(nil)
return "Updated thinking.\n" + m.summary(), nil
}
func (m *Manager) summary() string {
return fmt.Sprintf("provider: %s\nmodel: %s\nthinking: %s", m.CurrentProvider(), m.CurrentModel(), m.CurrentThinking())
}
func (m *Manager) maxContextTokens() int {
if m.cfg != nil && m.cfg.Agent.MaxContextTokens > 0 {
return m.cfg.Agent.MaxContextTokens
}
return m.provider.ContextTokens
}
func (m *Manager) syncAgent(provider llm.Provider) {
m.agent.SetRuntime(agent.RuntimeOptions{
Provider: provider,
ProviderName: m.providerName,
Model: m.provider.Model,
Thinking: m.provider.Thinking,
ThinkingParam: m.provider.ThinkingParam,
ThinkingEnabled: m.provider.ThinkingConfigured,
MaxContextTokens: m.maxContextTokens(),
})
}
func (m *Manager) providerNames() []string {
names := make([]string, 0, len(m.cfg.Providers))
for name := range m.cfg.Providers {
names = append(names, name)
}
sort.Strings(names)
return names
}
func contains(values []string, value string) bool {
for _, item := range values {
if item == value {
return true
}
}
return false
}
func fitName(name string, width int) string {
if len(name) <= width {
return name
}
return name[:width-3] + "..."
}