implement /compact and /clear commands

- /compact: summarizes evicted context messages via LLM call, replaces
  them with a single summary system message
- /clear: resets session and display
- ContextBuilder.EvictedMessages(): returns messages outside bounded window
- Session.Compact(): drives the summarization and history replacement
This commit is contained in:
Levi Neely 2026-04-04 21:51:30 +02:00
parent 082444dd7e
commit e48617fc27
3 changed files with 100 additions and 1 deletions

View File

@ -96,6 +96,36 @@ func (cb *ContextBuilder) Truncate(i int) {
}
}
// EvictedMessages returns the messages that would be dropped by buildBounded.
func (cb *ContextBuilder) EvictedMessages() []backend.Message {
var rest []backend.Message
for _, m := range cb.messages {
if m.Role != "system" {
rest = append(rest, m)
}
}
ts := computeTailStart(rest, cb.cfg.TailMessages)
older := rest[:ts]
used := cb.cfg.FixedOverheadChars + msgSliceChars(cb.messages) - msgSliceChars(older)
budget := cb.cfg.SoftLimit - used
var included []backend.Message
for i := len(older) - 1; i >= 0; i-- {
size := msgChars(older[i])
if budget-size < 0 {
break
}
budget -= size
included = append(included, older[i])
}
evictCount := len(older) - len(included)
if evictCount == 0 {
return nil
}
return older[:evictCount]
}
// BoundedHistory returns a context-window-safe slice of messages.
func (cb *ContextBuilder) BoundedHistory() []backend.Message {
return cb.buildBounded(false)

View File

@ -1,6 +1,12 @@
package agent
import "ollie/backend"
import (
"context"
"fmt"
"strings"
"ollie/backend"
)
// Session is an ephemeral in-memory state backend.
// It lives only for the duration of the process; nothing is persisted.
@ -57,6 +63,53 @@ func (s *Session) MarkComplete() error {
return nil
}
// Compact summarizes messages outside the bounded window via an LLM call,
// replacing them with a single summary system message.
// Returns the number of messages replaced, or 0 if nothing to compact.
func (s *Session) Compact(ctx context.Context, b backend.Backend, model string) (int, error) {
evicted := s.ctx.EvictedMessages()
if len(evicted) == 0 {
return 0, nil
}
// Build a prompt asking the model to summarize the evicted messages.
var sb strings.Builder
for _, m := range evicted {
fmt.Fprintf(&sb, "%s: %s\n", m.Role, m.Content)
}
prompt := "Summarize the following conversation history concisely, preserving key facts, decisions, and context:\n\n" + sb.String()
ch, err := b.ChatStream(ctx, model, []backend.Message{
{Role: "user", Content: prompt},
}, nil)
if err != nil {
return 0, fmt.Errorf("compact: %w", err)
}
var summary strings.Builder
for ev := range ch {
if ev.Content != "" {
summary.WriteString(ev.Content)
}
if ev.Done {
break
}
}
// Replace evicted messages with summary.
all := s.ctx.Messages()
s.ctx.Truncate(0)
s.ctx.Append(backend.Message{
Role: "system",
Content: "[conversation summary: " + strings.TrimSpace(summary.String()) + "]",
})
// Re-append the non-evicted messages.
for _, m := range all[len(evicted):] {
s.ctx.Append(m)
}
return len(evicted), nil
}
// Rollback removes any trailing non-user messages from history, discarding
// an incomplete assistant turn caused by an interruption.
func (s *Session) Rollback() {

16
main.go
View File

@ -854,6 +854,21 @@ func (m *model) handleCommand(input string) bool {
m.display = append(m.display, fmt.Sprintf("agent: %s", name))
return true
case "/compact":
if m.session == nil {
m.display = append(m.display, "nothing to compact")
return true
}
n, err := m.session.Compact(context.Background(), m.loopcfg.Backend, m.loopcfg.Model)
if err != nil {
m.display = append(m.display, "compact error: "+err.Error())
} else if n == 0 {
m.display = append(m.display, "nothing to compact")
} else {
m.display = append(m.display, fmt.Sprintf("compacted %d messages", n))
}
return true
case "/clear":
m.session = nil
m.display = nil
@ -865,6 +880,7 @@ func (m *model) handleCommand(input string) bool {
m.display = append(m.display, " /agent [name] - Show or switch active agent")
m.display = append(m.display, " /backend <type> - Switch backend (ollama, openai)")
m.display = append(m.display, " /model <name> - Switch model")
m.display = append(m.display, " /compact - Summarize evicted context messages")
m.display = append(m.display, " /clear - Clear session and display")
m.display = append(m.display, " /help - Show this help")
return true