v0.0.56 9a32f9c

compact.go RawHistoryFolder

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.`

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close