package session import ( "fmt" "net/http" "sort" "strings" "time" "agentu/internal/agent" "agentu/pkg/config" "agentu/pkg/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 | /model model | /model thinking ") 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 (m *Manager) AvailableModels() []string { return append([]string(nil), m.provider.Models...) } func contains(values []string, value string) bool { for _, v := range values { if v == value { return true } } return false } func fitName(name string, width int) string { if len(name) <= width { return name } return name[:width-3] + "..." }