// This file (US-003) ties the compaction pieces together: given a message list // and settings, it finds the cut point, extracts file operations from the // summarized range, generates the structured summary, and returns a // CompactionResult ready to be persisted as a session compaction entry and used // to rebuild the agent context. Mirrors pi's prepareCompaction + compact. package compaction import ( "context" "encoding/json" "github.com/smallnest/pigo/internal/agentcore" "github.com/smallnest/pigo/internal/provider" ) // CompactionDetails records the files touched in the compacted history, stored // alongside a compaction entry so a later iterative compaction can seed its // file lists. Mirrors pi's CompactionDetails. type CompactionDetails struct { // ReadFiles are files read but not modified in the compacted range. ReadFiles []string `json:"readFiles"` // ModifiedFiles are files written or edited in the compacted range. ModifiedFiles []string `json:"modifiedFiles"` } // CompactionResult is the outcome of a compaction: the summary text (with file // metadata appended), the index where retained history begins, the estimated // tokens before compaction, and the extracted file details. Mirrors pi's // CompactionResult, adapted to pigo's flat message list (index vs entry id). type CompactionResult struct { // Summary is the structured summary that replaces the compacted history. Summary string // FirstKeptIndex is the index of the first retained message. FirstKeptIndex int // TokensBefore is the estimated context tokens before compaction. TokensBefore int // Details holds the file operations extracted from the compacted range. Details CompactionDetails } // Compact prepares and generates a compaction over msgs. It cuts at // FindCutPoint(msgs, settings.KeepRecentTokens), summarizes messages in // [prevCompactionIndex+1, firstKeptIndex) (seeding file ops from a prior // compaction's details when provided), and appends / // metadata to the summary. previousSummary, when non-empty, switches to the // iterative update template. // // prevCompactionIndex is the index of the last already-applied compaction point // (-1 when none); summarization starts just after it so each compaction only // covers the newly accumulated history. prevDetails carries that compaction's // file lists so long-lived reads/edits survive across successive compactions. // // It returns (nil, nil) when there is nothing to compact (no valid cut point or // an empty summarization range). func Compact( ctx context.Context, stream provider.StreamFn, model provider.Model, msgs []agentcore.Message, settings CompactionSettings, prevCompactionIndex int, prevDetails *CompactionDetails, previousSummary string, cfg provider.StreamConfig, ) (*CompactionResult, error) { cut := FindCutPoint(msgs, settings.KeepRecentTokens) start := prevCompactionIndex + 1 if start < 0 { start = 0 } if start >= cut.FirstKeptIndex { // Nothing new to summarize. return nil, nil } toSummarize := msgs[start:cut.FirstKeptIndex] // Seed file ops from the previous compaction, then fold in this range. ops := NewFileOps() if prevDetails != nil { for _, f := range prevDetails.ReadFiles { ops.Read[f] = struct{}{} } for _, f := range prevDetails.ModifiedFiles { ops.Edited[f] = struct{}{} } } for _, m := range toSummarize { extractFileOpsFromMessage(m, ops) } readFiles, modifiedFiles := computeFileLists(ops) summary, err := GenerateSummary(ctx, stream, model, toSummarize, settings.ReserveTokens, previousSummary, cfg) if err != nil { return nil, err } summary += formatFileOperations(readFiles, modifiedFiles) return &CompactionResult{ Summary: summary, FirstKeptIndex: cut.FirstKeptIndex, TokensBefore: EstimateContextTokens(msgs).Tokens, Details: CompactionDetails{ReadFiles: readFiles, ModifiedFiles: modifiedFiles}, }, nil } // Message builds the CompactionMessage to persist for this result: the summary // text plus the estimated tokens-before and the file details (as raw JSON). func (r *CompactionResult) Message(now int64) agentcore.CompactionMessage { var details json.RawMessage if b, err := json.Marshal(r.Details); err == nil { details = b } return agentcore.CompactionMessage{ RoleField: agentcore.RoleCompaction, Summary: r.Summary, TokensBefore: r.TokensBefore, Details: details, Timestamp: now, } } // RebuildContext returns the post-compaction message list: the compaction // checkpoint followed by the retained recent messages (msgs[FirstKeptIndex:]). // The summarized prefix is dropped and replaced by the single checkpoint, // keeping context continuous while staying within the window. Mirrors pi's // post-compact context reconstruction (summary entry + retained tail). func (r *CompactionResult) RebuildContext(msgs []agentcore.Message, now int64) agentcore.MessageList { out := make(agentcore.MessageList, 0, len(msgs)-r.FirstKeptIndex+1) out = append(out, r.Message(now)) out = append(out, msgs[r.FirstKeptIndex:]...) return out }