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