package session import ( "fmt" "nub/internal/llm" ) // Session ist der reine In-Memory-Baum, ab Tag 1 (E-03): jeder Zugriff auf // "die Historie" geht über PathToHead(), nie über einen Slice. type Session struct { ID string Nodes map[string]*Node Head string } func New(id string) *Session { return &Session{ID: id, Nodes: make(map[string]*Node)} } // HeadID gibt den aktuellen Head zurück (Methode statt nur Feld, damit // Session und Store gemeinsam gegen ein schmales Interface programmierbar // sind, siehe internal/agent.Sessioner). func (s *Session) HeadID() string { return s.Head } // Append hängt einen Node an. ParentID muss "" (Wurzel, nur beim allerersten // Node zulässig) oder ein existierender Node sein. Setzt Head auf den neuen // Node. func (s *Session) Append(n *Node) error { if n.ParentID != "" { if _, ok := s.Nodes[n.ParentID]; !ok { return fmt.Errorf("session: unknown parent %q", n.ParentID) } } if _, exists := s.Nodes[n.ID]; exists { return fmt.Errorf("session: node %q already exists", n.ID) } s.Nodes[n.ID] = n s.Head = n.ID return nil } // Branch setzt Head auf einen früheren Knoten. Weiterarbeit danach hängt // neue Nodes an diesem Knoten an — ein zweiter Ast entsteht implizit, weil // mehrere Nodes denselben ParentID referenzieren können. func (s *Session) Branch(from string) error { if from == "" { s.Head = "" return nil } if _, ok := s.Nodes[from]; !ok { return fmt.Errorf("session: unknown node %q", from) } s.Head = from return nil } // PathNodes läuft von Head Richtung Wurzel und stoppt am ersten Summary-Node // (E-08); das Ergebnis ist in Wurzel→Head-Reihenfolge. Anders als // PathToHead liefert es die Nodes selbst — Grundlage für Compaction, die // Node-IDs für Replaces braucht. func (s *Session) PathNodes() []*Node { var nodes []*Node id := s.Head for id != "" { n, ok := s.Nodes[id] if !ok { break } nodes = append(nodes, n) if n.Kind == NodeSummary { break } id = n.ParentID } for i, j := 0, len(nodes)-1; i < j; i, j = i+1, j-1 { nodes[i], nodes[j] = nodes[j], nodes[i] } return nodes } // PathToHead läuft von Head Richtung Wurzel und stoppt am ersten // Summary-Node (E-08); das Ergebnis ist in Wurzel→Head-Reihenfolge. func (s *Session) PathToHead() []llm.Message { nodes := s.PathNodes() messages := make([]llm.Message, len(nodes)) for i, n := range nodes { messages[i] = n.Message } return messages }