Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184package tui
import ( "albedo/cli/internal/daemon" "encoding/json" "fmt")
type EntryKind string
const ( EntryUser EntryKind = "user" EntryAssistant EntryKind = "assistant" EntryThinking EntryKind = "thinking" EntryTool EntryKind = "tool" EntryNote EntryKind = "note" EntryError EntryKind = "error" EntryCompacted EntryKind = "compacted" // EntryTurnEnd closes a turn with its outcome, length, and tool count. EntryTurnEnd EntryKind = "turn_end")
type HistoryEntry struct { Kind EntryKind Speaker string Text string Timestamp int64 ClientID string ToolName string ToolArgs map[string]any ToolResult string ToolTrace *daemon.ToolTrace Evicted int SizeBytes int64 Mood mood ElapsedMs int64 Tools int // Pending is a message of yours the daemon has not echoed back yet. Pending pending}
// pending is how far a message of yours has got: sent and waiting for the// daemon, or accepted and queued behind the running turn.type pending int
const ( settled pending = iota sending queued)
func (e *HistoryEntry) ComputeSize() int64 { size := int64(len(e.Speaker) + len(e.Text) + len(e.ClientID) + len(e.ToolName) + len(e.ToolResult) + 64) if e.ToolArgs != nil { for k, v := range e.ToolArgs { size += int64(len(k) + 16) switch val := v.(type) { case string: size += int64(len(val)) default: data, _ := json.Marshal(val) size += int64(len(data)) } } } if e.ToolTrace != nil { for _, act := range e.ToolTrace.Activities { size += int64(len(act.Kind) + len(act.Target) + 16) } for _, ch := range e.ToolTrace.Changes { size += int64(len(ch.Path) + len(ch.Kind) + len(ch.Diff) + len(ch.Reason) + 32) } } e.SizeBytes = size return size}
type BoundedHistory struct { MaxEntries int MaxBytes int64 entries []HistoryEntry totalBytes int64 evictedEntries int evictedBytes int64}
func NewBoundedHistory(maxEntries int, maxBytes int64) *BoundedHistory { if maxEntries <= 0 { maxEntries = 500 } if maxBytes <= 0 { maxBytes = 2 * 1024 * 1024 // 2MB default } return &BoundedHistory{ MaxEntries: maxEntries, MaxBytes: maxBytes, entries: make([]HistoryEntry, 0, 128), }}
func (h *BoundedHistory) Len() int { return len(h.entries)}
func (h *BoundedHistory) TotalBytes() int64 { return h.totalBytes}
func (h *BoundedHistory) EvictedCount() int { return h.evictedEntries}
func (h *BoundedHistory) EvictedBytes() int64 { return h.evictedBytes}
func (h *BoundedHistory) Entries() []HistoryEntry { return h.entries}
func (h *BoundedHistory) TruncationNotice() string { if h.evictedEntries > 0 { return fmt.Sprintf("[%d older messages truncated (%d KB); durable history is in daemon]", h.evictedEntries, h.evictedBytes/1024) } return ""}
func (h *BoundedHistory) enforceBounds() { for len(h.entries) > 0 && (len(h.entries) > h.MaxEntries || h.totalBytes > h.MaxBytes) { removed := h.entries[0] h.entries[0] = HistoryEntry{} // Explicitly zero out evicted slot to drop GC references h.entries = h.entries[1:] h.totalBytes -= removed.SizeBytes if h.totalBytes < 0 { h.totalBytes = 0 } h.evictedEntries++ h.evictedBytes += removed.SizeBytes }}
func (h *BoundedHistory) Append(entry HistoryEntry) { entry.ComputeSize() h.entries = append(h.entries, entry) h.totalBytes += entry.SizeBytes h.enforceBounds()}
func (h *BoundedHistory) AppendToLast(chunk string) { if len(h.entries) == 0 { h.Append(HistoryEntry{Kind: EntryAssistant, Text: chunk}) return } lastIdx := len(h.entries) - 1 oldSize := h.entries[lastIdx].SizeBytes h.entries[lastIdx].Text += chunk newSize := h.entries[lastIdx].ComputeSize() h.totalBytes += (newSize - oldSize) h.enforceBounds()}
func (h *BoundedHistory) ReplaceLast(entry HistoryEntry) { if len(h.entries) == 0 { h.Append(entry) return } lastIdx := len(h.entries) - 1 oldSize := h.entries[lastIdx].SizeBytes entry.ComputeSize() h.entries[lastIdx] = entry h.totalBytes += (entry.SizeBytes - oldSize) h.enforceBounds()}
func (h *BoundedHistory) Clear() { for i := range h.entries { h.entries[i] = HistoryEntry{} // Zero out all slots to prevent retaining pointers } h.entries = h.entries[:0] h.totalBytes = 0 h.evictedEntries = 0 h.evictedBytes = 0}