// Package session enthält das Baum-Modell der Konversation (E-03) und dessen // Persistenz als Append-only JSONL (Abschnitt 4.5). package session import ( "time" "github.com/oklog/ulid/v2" "nub/internal/llm" ) type NodeKind string const ( NodeMessage NodeKind = "message" NodeSummary NodeKind = "summary" ) type Node struct { ID string `json:"id"` ParentID string `json:"parent_id"` // "" = Wurzel Kind NodeKind `json:"kind"` Message llm.Message `json:"message"` Replaces []string `json:"replaces,omitempty"` // nur bei NodeSummary Meta NodeMeta `json:"meta"` } type NodeMeta struct { Model string `json:"model,omitempty"` Usage llm.Usage `json:"usage"` CreatedAt time.Time `json:"created_at"` Label string `json:"label,omitempty"` Bookmark bool `json:"bookmark,omitempty"` } // NewNode erzeugt einen neuen Message-Node mit frischer ULID. parentID ist // typischerweise der aktuelle Head. func NewNode(parentID string, msg llm.Message, meta NodeMeta) *Node { if meta.CreatedAt.IsZero() { meta.CreatedAt = time.Now() } return &Node{ ID: ulid.Make().String(), ParentID: parentID, Kind: NodeMessage, Message: msg, Meta: meta, } } // NewSummaryNode erzeugt einen Summary-Node, der die Nodes in replaces aus // dem aktiven Kontext ersetzt (E-08). Die ersetzten Nodes bleiben im Baum // erhalten — kein destruktives Löschen, Rewind auf sie funktioniert weiter. func NewSummaryNode(parentID, text string, replaces []string, meta NodeMeta) *Node { if meta.CreatedAt.IsZero() { meta.CreatedAt = time.Now() } return &Node{ ID: ulid.Make().String(), ParentID: parentID, Kind: NodeSummary, Message: llm.Message{Role: llm.RoleUser, Content: []llm.Block{{Kind: llm.KindText, Text: text}}}, Replaces: replaces, Meta: meta, } }