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:
parent
082444dd7e
commit
e48617fc27
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
16
main.go
|
|
@ -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
|
||||
|
|
|
|||
Reference in New Issue