|
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 |
} |