nub/internal/session/session.go
Tom a97013d876 initial commit
- v 0.1.0 siehe CHANGELOG.md
2026-07-25 11:08:01 +02:00

92 lines
2.4 KiB
Go

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
}