ScuttleBot

scuttlebot / pkg / toon / toon.go
Source Blame History 121 lines
a027855… noreply 1 // Package toon implements the TOON format — Token-Optimized Object Notation
a027855… noreply 2 // for compact LLM context windows.
a027855… noreply 3 //
a027855… noreply 4 // TOON is designed for feeding IRC conversation history to language models.
a027855… noreply 5 // It strips noise (joins, parts, status messages, repeated tool calls),
a027855… noreply 6 // deduplicates, and compresses timestamps into relative offsets.
a027855… noreply 7 //
a027855… noreply 8 // Example output:
a027855… noreply 9 //
a027855… noreply 10 // #fleet 50msg 2h window
a027855… noreply 11 // ---
a027855… noreply 12 // claude-kohakku [orch] +0m
a027855… noreply 13 // task.create {file: main.go, action: edit}
a027855… noreply 14 // "editing main.go to add error handling"
a027855… noreply 15 // leo [op] +2m
a027855… noreply 16 // "looks good, ship it"
a027855… noreply 17 // claude-kohakku [orch] +3m
a027855… noreply 18 // task.complete {file: main.go, status: done}
a027855… noreply 19 // ---
a027855… noreply 20 // decisions: edit main.go error handling
a027855… noreply 21 // actions: task.create → task.complete (main.go)
a027855… noreply 22 package toon
a027855… noreply 23
a027855… noreply 24 import (
a027855… noreply 25 "fmt"
a027855… noreply 26 "strings"
a027855… noreply 27 "time"
a027855… noreply 28 )
a027855… noreply 29
a027855… noreply 30 // Entry is a single message to include in the TOON output.
a027855… noreply 31 type Entry struct {
a027855… noreply 32 Nick string
a027855… noreply 33 Type string // agent type: "orch", "worker", "op", "bot", "" for unknown
a027855… noreply 34 MessageType string // envelope type (e.g. "task.create"), empty for plain text
a027855… noreply 35 Text string
a027855… noreply 36 At time.Time
a027855… noreply 37 }
a027855… noreply 38
a027855… noreply 39 // Options controls TOON formatting.
a027855… noreply 40 type Options struct {
a027855… noreply 41 Channel string
a027855… noreply 42 MaxEntries int // 0 = no limit
a027855… noreply 43 }
a027855… noreply 44
a027855… noreply 45 // Format renders a slice of entries into TOON format.
a027855… noreply 46 func Format(entries []Entry, opts Options) string {
a027855… noreply 47 if len(entries) == 0 {
a027855… noreply 48 return ""
a027855… noreply 49 }
a027855… noreply 50
a027855… noreply 51 var b strings.Builder
a027855… noreply 52
a027855… noreply 53 // Header.
a027855… noreply 54 window := ""
a027855… noreply 55 if len(entries) >= 2 {
a027855… noreply 56 dur := entries[len(entries)-1].At.Sub(entries[0].At)
a027855… noreply 57 window = " " + compactDuration(dur) + " window"
a027855… noreply 58 }
a027855… noreply 59 ch := opts.Channel
a027855… noreply 60 if ch == "" {
a027855… noreply 61 ch = "channel"
a027855… noreply 62 }
a027855… noreply 63 fmt.Fprintf(&b, "%s %dmsg%s\n---\n", ch, len(entries), window)
a027855… noreply 64
a027855… noreply 65 // Body — group consecutive messages from same nick.
a027855… noreply 66 baseTime := entries[0].At
a027855… noreply 67 var lastNick string
a027855… noreply 68 for _, e := range entries {
a027855… noreply 69 offset := e.At.Sub(baseTime)
a027855… noreply 70 if e.Nick != lastNick {
a027855… noreply 71 tag := ""
a027855… noreply 72 if e.Type != "" {
a027855… noreply 73 tag = " [" + e.Type + "]"
a027855… noreply 74 }
a027855… noreply 75 fmt.Fprintf(&b, "%s%s +%s\n", e.Nick, tag, compactDuration(offset))
a027855… noreply 76 lastNick = e.Nick
a027855… noreply 77 }
a027855… noreply 78
a027855… noreply 79 if e.MessageType != "" {
a027855… noreply 80 fmt.Fprintf(&b, " %s\n", e.MessageType)
a027855… noreply 81 }
a027855… noreply 82 text := strings.TrimSpace(e.Text)
a027855… noreply 83 if text != "" && text != e.MessageType {
a027855… noreply 84 // Truncate very long messages to save tokens.
a027855… noreply 85 if len(text) > 200 {
a027855… noreply 86 text = text[:197] + "..."
a027855… noreply 87 }
a027855… noreply 88 fmt.Fprintf(&b, " \"%s\"\n", text)
a027855… noreply 89 }
a027855… noreply 90 }
a027855… noreply 91
a027855… noreply 92 b.WriteString("---\n")
a027855… noreply 93 return b.String()
a027855… noreply 94 }
a027855… noreply 95
a027855… noreply 96 // FormatPrompt wraps TOON-formatted history into an LLM summarization prompt.
a027855… noreply 97 func FormatPrompt(channel string, entries []Entry) string {
a027855… noreply 98 toon := Format(entries, Options{Channel: channel})
a027855… noreply 99 var b strings.Builder
a027855… noreply 100 fmt.Fprintf(&b, "Summarize this IRC conversation. Focus on decisions, actions, and outcomes. Be concise.\n\n")
a027855… noreply 101 b.WriteString(toon)
a027855… noreply 102 return b.String()
a027855… noreply 103 }
a027855… noreply 104
a027855… noreply 105 func compactDuration(d time.Duration) string {
a027855… noreply 106 if d < time.Minute {
a027855… noreply 107 return fmt.Sprintf("%ds", int(d.Seconds()))
a027855… noreply 108 }
a027855… noreply 109 if d < time.Hour {
a027855… noreply 110 return fmt.Sprintf("%dm", int(d.Minutes()))
a027855… noreply 111 }
a027855… noreply 112 if d < 24*time.Hour {
a027855… noreply 113 h := int(d.Hours())
a027855… noreply 114 m := int(d.Minutes()) % 60
a027855… noreply 115 if m == 0 {
a027855… noreply 116 return fmt.Sprintf("%dh", h)
a027855… noreply 117 }
a027855… noreply 118 return fmt.Sprintf("%dh%dm", h, m)
a027855… noreply 119 }
a027855… noreply 120 return fmt.Sprintf("%dd", int(d.Hours()/24))
a027855… noreply 121 }

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button