306 lines
7.8 KiB
Go
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] + "..."
|
|
}
|