1package core
2
3import (
4 "context"
5 "fmt"
6 "strings"
7 "time"
8
9 "github.com/patriceckhart/zot/internal/provider"
10)
11
12// Compact summarizes the agent's transcript via the LLM and replaces
13// it with a single synthetic user message carrying the summary. A
14// small tail of recent messages is optionally preserved for continuity.
15//
16// keepTail is the number of most-recent messages to keep verbatim after
17// the summary. 0 means summarize everything; a typical useful value is
18// 4-8 (last couple of exchanges).
19//
20// The method blocks until the summary request completes. Emitted
21// events via sink are limited to text deltas from the summary call so
22// the UI can show progress.
23func (a *Agent) Compact(ctx context.Context, keepTail int, sink func(delta string)) (summary string, err error) {
24 a.mu.Lock()
25 msgs := append([]provider.Message(nil), a.messages...)
26 a.mu.Unlock()
27
28 if len(msgs) == 0 {
29 return "", fmt.Errorf("nothing to compact")
30 }
31 if keepTail < 0 {
32 keepTail = 0
33 }
34 if keepTail > len(msgs) {
35 keepTail = len(msgs)
36 }
37 summarizable := msgs[:len(msgs)-keepTail]
38 if len(summarizable) == 0 {
39 return "", fmt.Errorf("nothing to compact: keep-tail covers the whole transcript")
40 }
41
42 // Serialize the summarizable transcript to text and wrap it in tags
43 // so the model treats it as material to summarize, not to continue.
44 transcript := serializeTranscript(summarizable)
45
46 prompt := "<conversation>\n" + transcript + "\n</conversation>\n\n" + compactionPrompt
47
48 req := provider.Request{
49 Model: a.Model,
50 System: summarizationSystem,
51 MaxTokens: 4096,
52 Messages: []provider.Message{
53 {
54 Role: provider.RoleUser,
55 Content: []provider.Content{provider.TextBlock{Text: prompt}},
56 Time: time.Now(),
57 },
58 },
59 }
60
61 stream, err := a.Client.Stream(ctx, req)
62 if err != nil {
63 return "", err
64 }
65
66 var sb strings.Builder
67 for ev := range stream {
68 switch e := ev.(type) {
69 case provider.EventTextDelta:
70 sb.WriteString(e.Delta)
71 if sink != nil {
72 sink(e.Delta)
73 }
74 case provider.EventDone:
75 if e.Err != nil {
76 return "", e.Err
77 }
78 }
79 }
80 summary = strings.TrimSpace(sb.String())
81 if summary == "" {
82 return "", fmt.Errorf("empty summary from model")
83 }
84
85 // Replace transcript: one synthetic user message with the summary,
86 // followed by the preserved tail (if any).
87 synthetic := provider.Message{
88 Role: provider.RoleUser,
89 Content: []provider.Content{
90 provider.TextBlock{Text: "## Context Summary (compacted)\n\n" + summary},
91 },
92 Time: time.Now(),
93 }
94
95 tail := msgs[len(msgs)-keepTail:]
96 next := make([]provider.Message, 0, 1+len(tail))
97 next = append(next, synthetic)
98 next = append(next, tail...)
99
100 a.mu.Lock()
101 a.messages = next
102 a.mu.Unlock()
103
104 return summary, nil
105}
106
107// serializeTranscript renders a list of provider.Message into a plain
108// text transcript the summarization model can read without trying to
109// continue the conversation.
110func serializeTranscript(msgs []provider.Message) string {
111 var sb strings.Builder
112 for _, m := range msgs {
113 switch m.Role {
114 case provider.RoleUser:
115 sb.WriteString("\n--- user ---\n")
116 case provider.RoleAssistant:
117 sb.WriteString("\n--- assistant ---\n")
118 case provider.RoleTool:
119 sb.WriteString("\n--- tool ---\n")
120 }
121 for _, c := range m.Content {
122 switch v := c.(type) {
123 case provider.TextBlock:
124 sb.WriteString(v.Text)
125 sb.WriteString("\n")
126 case provider.ImageBlock:
127 fmt.Fprintf(&sb, "[image: %s, %d bytes]\n", v.MimeType, len(v.Data))
128 case provider.ToolCallBlock:
129 fmt.Fprintf(&sb, "[tool_call %s %s]\n", v.Name, string(v.Arguments))
130 case provider.ToolResultBlock:
131 for _, inner := range v.Content {
132 if tb, ok := inner.(provider.TextBlock); ok {
133 sb.WriteString("[tool_result] ")
134 sb.WriteString(tb.Text)
135 sb.WriteString("\n")
136 }
137 }
138 }
139 }
140 }
141 return sb.String()
142}
143
144const summarizationSystem = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.
145
146Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`
147
148const compactionPrompt = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.
149
150Use this EXACT format:
151
152## Goal
153[What is the user trying to accomplish? Can be multiple items if the session covers different tasks.]
154
155## Constraints & Preferences
156- [Any constraints, preferences, or requirements mentioned by user]
157- [Or "(none)" if none were mentioned]
158
159## Progress
160### Done
161- [x] [Completed tasks/changes]
162
163### In Progress
164- [ ] [Current work]
165
166### Blocked
167- [Issues preventing progress, if any]
168
169## Key Decisions
170- **[Decision]**: [Brief rationale]
171
172## Next Steps
1731. [Ordered list of what should happen next]
174
175## Critical Context
176- [Any data, examples, or references needed to continue]
177- [Or "(none)" if not applicable]
178
179Keep each section concise. Preserve exact file paths, function names, and error messages.`