Files
cursor-byok/internal/backend/forwarder/compaction.go
T
leookun 9373e57ebf Enhance checkpoint handling and error management in forwarder
- Added flushing of assistant text during provider completion to ensure no output is lost on transport failure.
- Updated checkpoint blob synchronization tests to validate behavior under various conditions, including terminal and non-terminal states.
- Introduced new functions for managing checkpoint terminal actions, improving clarity and maintainability of the code.
- Implemented additional tests for imported blob handling and conversation state restoration, ensuring robustness in data integrity across operations.
2026-08-10 22:25:37 +08:00

1927 lines
64 KiB
Go

package forwarder
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"log"
"strings"
"time"
"github.com/google/uuid"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"cursor/gen/agentv1"
modeladapter "cursor/internal/backend/agent/model"
promptengine "cursor/internal/backend/agent/prompt"
promptassets "cursor/prompt"
)
const (
compactionAutoReserveTokens = 10000
compactionTriggerRemainingTokens = 8192
compactionPreferredTailTurns = 4
compactionMinimumTailTurns = 1
compactionReserveFloorTokens = 8192
compactionSummaryMaxChars = 12000
compactionSummaryOutputMaxTokens = 4096
compactionTurnSnippetMaxChars = 900
autoCompactionPreservedToolResultLimitBytes = 16 * 1024
autoCompactionFallbackToolResultLimitBytes = 4 * 1024
)
const (
compactionRequestSourcePromptAsset = "prompt_asset"
compactionRequestSourceCurrentTurn = "current_turn_compaction"
compactionOverflowTerminalCode = "context_overflow_after_compaction"
compactionSummaryUserMessage = "现在上下文已满,触发了压缩对话。请把我们到目前为止的对话历史整理成一个 Markdown 表格返回给我。你的回复会直接作为后续对话的压缩内容,请只保留继续协作必需的事实、约束、决定、文件路径、命令、报错、结果和待办。不要调用工具,不要输出表格外说明。"
)
type compactionPlan struct {
Trigger string
ContextTokens int64
ContextWindowSize int64
ContextUsagePercent float64
ReserveTokens int64
MessageCount int32
MessagesToCompact int32
CompactTurnCount int32
IsFirstCompaction bool
ExistingSummary string
CompactedTurns []compactedTurnSummary
ManualInstruction string
RequestSource string
CurrentTurnSeq int64
CurrentRequestID string
CurrentUserText string
PreserveCurrentTurnInputs bool
}
type compactedTurnSummary struct {
UserText string
Steps []string
}
type compactionCandidateTurn struct {
Summary compactedTurnSummary
ReplayCount int32
EstimatedTokens int64
}
func (service *Service) maybeCompactBeforeProvider(stream *ActiveStream, conversation *ConversationFile, compiled CompiledConversation) (bool, error) {
if service == nil || stream == nil || conversation == nil {
return false, nil
}
manualInstruction, manual := streamManualCompactionDirective(stream)
plan, err := service.buildCompactionPlan(stream, conversation, compiled, manual, manualInstruction)
if err != nil {
return false, err
}
if plan == nil {
if manual {
return true, service.finishManualCompactionNoop(stream)
}
return false, nil
}
return true, service.beginPendingCompaction(stream, plan)
}
func (service *Service) buildCompactionPlan(stream *ActiveStream, conversation *ConversationFile, compiled CompiledConversation, manual bool, manualInstruction string) (*compactionPlan, error) {
if stream == nil || conversation == nil {
return nil, nil
}
if manual {
return service.buildManualCompactionPlan(stream, conversation, compiled, manualInstruction)
}
return service.buildAutoCompactionPlan(stream, conversation, compiled)
}
func (service *Service) buildManualCompactionPlan(stream *ActiveStream, conversation *ConversationFile, compiled CompiledConversation, manualInstruction string) (*compactionPlan, error) {
if stream == nil || conversation == nil {
return nil, nil
}
contextWindowSize := compactionContextWindowSize(conversation)
if contextWindowSize <= 0 {
return nil, nil
}
contextTokens, err := service.resolveCompactionBaselineTokens(stream.ConversationID, compiled, conversation)
if err != nil {
return nil, err
}
if contextTokens <= 0 {
return nil, nil
}
usagePercent := 0.0
if contextWindowSize > 0 {
usagePercent = float64(contextTokens) / float64(contextWindowSize)
}
base := &compactionPlan{
Trigger: "manual",
ContextTokens: contextTokens,
ContextWindowSize: contextWindowSize,
ContextUsagePercent: usagePercent,
ReserveTokens: compactionAutoReserveTokens,
MessageCount: clampInt64ToInt32(int64(len(compiled.Messages))),
IsFirstCompaction: len(compactionSummaryTexts(conversation)) == 0,
ExistingSummary: existingConversationSummaryText(conversation),
ManualInstruction: strings.TrimSpace(manualInstruction),
CurrentTurnSeq: stream.TurnSeq,
CurrentRequestID: strings.TrimSpace(stream.RequestID),
CurrentUserText: strings.TrimSpace(stream.LatestUserText),
PreserveCurrentTurnInputs: false,
}
return service.buildLegacyCompactionPlan(base, conversation, true, 0)
}
func (service *Service) buildAutoCompactionPlan(stream *ActiveStream, conversation *ConversationFile, compiled CompiledConversation) (*compactionPlan, error) {
if stream == nil || conversation == nil {
return nil, nil
}
contextWindowSize := compactionContextWindowSize(conversation)
if contextWindowSize <= 0 {
return nil, nil
}
estimatedCompiledTokens := estimateCompiledPromptTokens(compiled)
reserveTokens := service.resolveCompactionReserveTokens(stream.ModelID)
if reserveTokens <= 0 {
reserveTokens = conversation.AutoCompactionReserveTokens
}
if reserveTokens <= 0 {
reserveTokens = compactionAutoReserveTokens
}
budgetTokens := contextWindowSize - reserveTokens
preflightExceeded := estimatedCompiledTokens > 0 && estimatedCompiledTokens > budgetTokens
contextTokens := maxPositiveInt64(
conversation.AutoCompactionPromptTokens,
estimatedCompiledTokens,
int64(conversation.TokenDetailsUsedTokens),
)
pendingExceeded := conversation.AutoCompactionPending && contextTokens > 0 && contextTokens > budgetTokens
if !pendingExceeded && !preflightExceeded {
return nil, nil
}
usagePercent := 0.0
if contextWindowSize > 0 && contextTokens > 0 {
usagePercent = float64(contextTokens) / float64(contextWindowSize)
}
base := &compactionPlan{
Trigger: "auto",
ContextTokens: contextTokens,
ContextWindowSize: contextWindowSize,
ContextUsagePercent: usagePercent,
ReserveTokens: reserveTokens,
MessageCount: clampInt64ToInt32(int64(len(compiled.Messages))),
IsFirstCompaction: len(compactionSummaryTexts(conversation)) == 0,
ExistingSummary: existingConversationSummaryText(conversation),
CurrentTurnSeq: stream.TurnSeq,
CurrentRequestID: strings.TrimSpace(stream.RequestID),
CurrentUserText: strings.TrimSpace(stream.LatestUserText),
PreserveCurrentTurnInputs: true,
}
plan, err := service.buildAutoCompactionPlanFromHistory(base, conversation)
if err != nil {
return nil, err
}
if plan == nil && preflightExceeded {
return nil, compactionTerminalError{
code: compactionOverflowTerminalCode,
message: fmt.Sprintf(
"compiled prompt exceeds context budget before provider request (estimated=%d budget=%d)",
estimatedCompiledTokens,
budgetTokens,
),
}
}
return plan, nil
}
func (service *Service) resolveCompactionBaselineTokens(conversationID string, compiled CompiledConversation, conversation *ConversationFile) (int64, error) {
contextTokens := estimateCompiledPromptTokens(compiled)
if contextTokens > 0 {
return contextTokens, nil
}
if conversation != nil && conversation.TokenDetailsUsedTokens > 0 {
return int64(conversation.TokenDetailsUsedTokens), nil
}
if conversationHasLocalCarryForwardState(conversation) {
return 0, nil
}
if service != nil && strings.TrimSpace(conversationID) != "" {
promptTokens, ok, err := service.loadLatestSummaryPromptTokens(conversationID)
if err != nil {
return 0, err
}
if ok && promptTokens > 0 {
return promptTokens, nil
}
}
return 0, nil
}
func conversationHasLocalCarryForwardState(conversation *ConversationFile) bool {
if conversation == nil {
return false
}
return len(compactionSummaryTexts(conversation)) > 0
}
func (service *Service) buildLegacyCompactionPlan(base *compactionPlan, conversation *ConversationFile, _ bool, _ int64) (*compactionPlan, error) {
if conversation == nil || base == nil {
return nil, nil
}
candidates := buildContextCompactionCandidates(replayablePromptProjectionEntries(conversation.Entries), base.CurrentTurnSeq, base.CurrentRequestID)
if len(candidates) == 0 {
return nil, nil
}
compactedTurns := make([]compactedTurnSummary, 0, len(candidates))
messagesToCompact := int32(0)
for _, candidate := range candidates {
compactedTurns = append(compactedTurns, candidate.Summary)
messagesToCompact += candidate.ReplayCount
}
plan := cloneCompactionPlanBase(base)
plan.RequestSource = compactionRequestSourcePromptAsset
plan.MessagesToCompact = messagesToCompact
plan.CompactTurnCount = clampInt64ToInt32(int64(len(candidates)))
plan.CompactedTurns = compactedTurns
return &plan, nil
}
func (service *Service) buildAutoCompactionPlanFromHistory(base *compactionPlan, conversation *ConversationFile) (*compactionPlan, error) {
if conversation == nil || base == nil {
return nil, nil
}
legacyPlan, err := service.buildLegacyCompactionPlan(base, conversation, false, 0)
if err != nil {
return nil, err
}
currentCandidate, hasCurrentCandidate := buildCurrentTurnCompactionCandidate(replayablePromptProjectionEntries(conversation.Entries), base.CurrentTurnSeq, base.CurrentRequestID)
if !hasCurrentCandidate {
return legacyPlan, nil
}
if legacyPlan == nil {
plan := cloneCompactionPlanBase(base)
plan.RequestSource = compactionRequestSourceCurrentTurn
plan.MessagesToCompact = currentCandidate.ReplayCount
plan.CompactTurnCount = 1
plan.CompactedTurns = []compactedTurnSummary{currentCandidate.Summary}
return &plan, nil
}
legacyPlan.RequestSource = compactionRequestSourceCurrentTurn
legacyPlan.MessagesToCompact += currentCandidate.ReplayCount
legacyPlan.CompactTurnCount++
legacyPlan.CompactedTurns = append(legacyPlan.CompactedTurns, currentCandidate.Summary)
return legacyPlan, nil
}
func (service *Service) beginPendingCompaction(stream *ActiveStream, plan *compactionPlan) error {
if service == nil || stream == nil || plan == nil {
return nil
}
request := buildPreCompactHookRequest(stream, plan)
serverMessage, pendingExec, err := service.execBridge.OpenExecuteHook(request, "execute_hook_pre_compact")
if err != nil {
return err
}
stream.mu.Lock()
pendingExec.ModelCallID = strings.TrimSpace(stream.CurrentModelCallID)
pendingExec.ProviderPass = stream.ProviderPassCount
stream.PendingCompaction = newPendingCompaction(plan)
stream.PendingProviderAction = providerActionNone
stream.PendingExecs[pendingExec.ExecID] = pendingExec
stream.Phase = TurnPhaseCompacting
stream.UpdatedAt = time.Now().UTC()
stream.mu.Unlock()
if err := service.broker.Publish(stream.RequestID, StreamEvent{
Message: buildSummaryStartedMessage(),
}); err != nil {
return err
}
if err := service.publishCheckpoint(stream.RequestID, stream.ConversationID); err != nil {
return err
}
return service.broker.Publish(stream.RequestID, StreamEvent{Message: serverMessage})
}
func (service *Service) handlePreCompactTerminal(stream *ActiveStream, sourcePass int, hookMessage string) error {
if service == nil || stream == nil {
return nil
}
_ = sourcePass
stream.mu.Lock()
if stream.PendingCompaction == nil {
stream.mu.Unlock()
if strings.TrimSpace(hookMessage) != "" {
if err := service.publishSummaryCompleted(stream, hookMessage); err != nil {
return err
}
}
return service.requestProviderAction(stream, providerActionResume)
}
plan := clonePendingCompaction(stream.PendingCompaction)
plan.HookMessage = strings.TrimSpace(hookMessage)
stream.PendingCompaction = plan
stream.mu.Unlock()
return service.startPendingCompactionSummary(stream, plan)
}
func (service *Service) startPendingCompactionSummary(stream *ActiveStream, plan *PendingCompaction) error {
if service == nil || stream == nil || plan == nil {
return nil
}
summaryModelCallID := uuid.NewString()
ctx, cancel := context.WithCancel(context.Background())
stream.mu.Lock()
if stream.Status == StreamStatusCanceled || stream.Status == StreamStatusCompleted || stream.Status == StreamStatusFailed {
stream.PendingCompaction = nil
stream.mu.Unlock()
cancel()
return nil
}
stream.CurrentCompactionToken++
token := stream.CurrentCompactionToken
stream.CurrentModelCallID = summaryModelCallID
stream.ProviderActive = true
stream.ProviderCancel = cancel
stream.PendingProviderAction = providerActionNone
stream.PendingCompaction.SummaryModelCallID = summaryModelCallID
plan.SummaryModelCallID = summaryModelCallID
stream.Phase = TurnPhaseCompacting
stream.UpdatedAt = time.Now().UTC()
stream.mu.Unlock()
if _, err := service.appendConversationEntries(stream, stream.ConversationID, []HistoryEntry{
newCompactionRequestEntry(plan),
}); err != nil {
cancel()
return err
}
go service.runPendingCompaction(stream, token, clonePendingCompaction(plan), summaryModelCallID, ctx)
return nil
}
func (service *Service) runPendingCompaction(stream *ActiveStream, token uint64, plan *PendingCompaction, modelCallID string, ctx context.Context) {
if service == nil || stream == nil || plan == nil {
return
}
summaryText, err := service.generateCompactionSummary(ctx, stream, plan, modelCallID)
if err == nil {
if trimmed := strings.TrimSpace(summaryText); trimmed != "" {
summaryText = trimmed
} else {
summaryText = buildFallbackCompactionSummary(plan)
}
}
if postErr := service.postStreamCommandWait(stream, streamCommand{
Kind: streamCommandCompactionEvent,
Compaction: &streamCompactionEvent{
Token: token,
Plan: plan,
SummaryText: strings.TrimSpace(summaryText),
Err: err,
},
}); postErr != nil && !errors.Is(postErr, errProviderLoopInterrupted) {
log.Printf("forwarder compaction completion post failed request_id=%s token=%d err=%v", strings.TrimSpace(stream.RequestID), token, postErr)
_ = service.failStreamIfNonTerminal(stream, "unknown", postErr)
}
}
func (service *Service) handleCompactionEvent(stream *ActiveStream, payload *streamCompactionEvent) error {
if service == nil || stream == nil || payload == nil {
return nil
}
stream.mu.Lock()
if stream.CurrentCompactionToken != payload.Token {
stream.mu.Unlock()
return nil
}
status := stream.Status
stream.ProviderActive = false
stream.ProviderCancel = nil
stream.PendingProviderAction = providerActionNone
stream.PendingCompaction = nil
stream.UpdatedAt = time.Now().UTC()
stream.mu.Unlock()
if errors.Is(payload.Err, errProviderLoopInterrupted) || isTerminalStreamStatus(status) {
return nil
}
if payload.Err != nil {
if _, err := service.appendConversationEntries(stream, stream.ConversationID, []HistoryEntry{
newCompactionFailedEntry(payload.Plan, payload.Err),
}); err != nil {
return err
}
service.setTurnPhase(stream, TurnPhaseFailed)
return service.failStream(stream, "unknown", payload.Err)
}
if payload.Plan == nil {
return fmt.Errorf("pending compaction plan is missing")
}
if err := service.applyCompactionPlan(stream, stream.ConversationID, payload.Plan, payload.SummaryText); err != nil {
service.setTurnPhase(stream, TurnPhaseFailed)
return service.failStream(stream, "unknown", err)
}
if err := service.syncSummaryCarryForward(stream.ConversationID, stream.RequestID, stream.CurrentModelCallID); err != nil {
service.setTurnPhase(stream, TurnPhaseFailed)
return service.failStream(stream, "unknown", err)
}
if err := service.broker.Publish(stream.RequestID, StreamEvent{
Message: buildSummaryMessage(payload.SummaryText),
}); err != nil {
service.setTurnPhase(stream, TurnPhaseFailed)
return service.failStream(stream, "unknown", err)
}
if err := service.publishCheckpoint(stream.RequestID, stream.ConversationID); err != nil {
service.setTurnPhase(stream, TurnPhaseFailed)
return service.failStream(stream, "unknown", err)
}
if err := service.publishSummaryCompleted(stream, firstNonEmpty(payload.Plan.HookMessage, "Conversation context compacted.")); err != nil {
service.setTurnPhase(stream, TurnPhaseFailed)
return service.failStream(stream, "unknown", err)
}
if payload.Plan.Trigger == "manual" {
if err := service.completeManualCompactionTurn(stream); err != nil {
return service.failStream(stream, "unknown", err)
}
completion := manualCompactionTurnCompletion(stream)
return service.publishCheckpointWithCompletion(stream.RequestID, stream.ConversationID, &completion)
}
return service.requestProviderAction(stream, providerActionResume)
}
func (service *Service) finishCompactionWithError(stream *ActiveStream, cancel context.CancelFunc, err error) {
if cancel != nil {
cancel()
}
if stream == nil {
return
}
stream.mu.Lock()
stream.ProviderActive = false
stream.ProviderCancel = nil
stream.PendingCompaction = nil
stream.UpdatedAt = time.Now().UTC()
stream.mu.Unlock()
terminalCode := "unknown"
var coded interface{ TerminalCode() string }
if errors.As(err, &coded) && strings.TrimSpace(coded.TerminalCode()) != "" {
terminalCode = strings.TrimSpace(coded.TerminalCode())
}
_ = service.failStream(stream, terminalCode, err)
}
func (service *Service) finishManualCompactionNoop(stream *ActiveStream) error {
if service == nil || stream == nil {
return nil
}
if err := service.broker.Publish(stream.RequestID, StreamEvent{
Message: buildSummaryStartedMessage(),
}); err != nil {
return err
}
if err := service.publishCheckpoint(stream.RequestID, stream.ConversationID); err != nil {
return err
}
if err := service.publishSummaryCompleted(stream, "Nothing to compact."); err != nil {
return err
}
if err := service.completeManualCompactionTurn(stream); err != nil {
return err
}
completion := manualCompactionTurnCompletion(stream)
return service.publishCheckpointWithCompletion(stream.RequestID, stream.ConversationID, &completion)
}
func (service *Service) completeManualCompactionTurn(stream *ActiveStream) error {
if service == nil || stream == nil {
return nil
}
requestID := strings.TrimSpace(stream.RequestID)
conversationID := strings.TrimSpace(stream.ConversationID)
turnSeq := stream.TurnSeq
modelCallID := "turn:" + requestID
if conversationID == "" {
return nil
}
if _, err := service.appendConversationEntries(stream, conversationID, []HistoryEntry{
newMetadataEntry(turnSeq, requestID, "turn_completed", map[string]any{
"model_call_id": modelCallID,
"provider_call": false,
}),
}); err != nil {
return err
}
if err := service.syncSummaryCarryForward(conversationID, requestID, modelCallID); err != nil {
return err
}
return nil
}
func manualCompactionTurnCompletion(stream *ActiveStream) pendingTurnCompletion {
if stream == nil {
return pendingTurnCompletion{}
}
return pendingTurnCompletion{
ConversationID: strings.TrimSpace(stream.ConversationID),
RequestID: strings.TrimSpace(stream.RequestID),
TurnSeq: stream.TurnSeq,
ModelCallID: "turn:" + strings.TrimSpace(stream.RequestID),
}
}
func (service *Service) publishSummaryCompleted(stream *ActiveStream, hookMessage string) error {
if service == nil || stream == nil {
return nil
}
thought := strings.TrimSpace(hookMessage)
if thought == "" {
thought = defaultSummaryCompletedThought
}
if strings.TrimSpace(stream.ConversationID) != "" {
if _, err := service.appendConversationEntries(stream, stream.ConversationID, []HistoryEntry{
newMetadataEntry(stream.TurnSeq, stream.RequestID, "thought_annotation", map[string]any{
"kind": "summary_completed",
"thought": thought,
}),
}); err != nil {
return err
}
if err := service.syncSummaryCarryForward(stream.ConversationID, stream.RequestID, stream.CurrentModelCallID); err != nil {
return err
}
}
return service.broker.Publish(stream.RequestID, StreamEvent{
Message: buildSummaryCompletedMessage(stream.RequestID),
})
}
func (service *Service) applyCompactionPlan(stream *ActiveStream, conversationID string, plan *PendingCompaction, summaryText string) error {
if service == nil || stream == nil || service.compiler == nil || plan == nil {
return nil
}
candidateConversation, err := service.snapshotCompactionCandidate(stream, conversationID)
if err != nil {
return err
}
originalEntryCount := len(candidateConversation.Entries)
if err := applyCompactionToConversation(candidateConversation, plan, summaryText); err != nil {
return err
}
latestUserText := ""
if plan.PreserveCurrentTurnInputs {
latestUserText = plan.CurrentUserText
}
recompiled, err := service.compiler.Compile(candidateConversation, stream.Mode, latestUserText, stream.ModelName)
if err != nil {
return err
}
if validationErr := validateCompactionCandidateBudget(recompiled, plan); validationErr != nil {
return validationErr
}
compactionEntries := append([]HistoryEntry(nil), candidateConversation.Entries[originalEntryCount:]...)
if service.store != nil {
persisted, _, err := service.store.AppendEntriesWithUpdate(conversationID, resetEntrySequences(compactionEntries), func(item *ConversationFile) error {
if item == nil {
return nil
}
item.TokenDetailsUsedTokens = 0
clearConversationAutoCompactionState(item)
return nil
})
if err != nil {
return err
}
stream.mu.Lock()
stream.CheckpointConversation = persisted
stream.UpdatedAt = time.Now().UTC()
stream.mu.Unlock()
return nil
}
_, err = service.updateConversationMetaAndCheckpoint(stream, conversationID, func(item *ConversationFile) error {
if item == nil {
return nil
}
appendEntriesInPlace(item, resetEntrySequences(compactionEntries))
item.TokenDetailsUsedTokens = 0
clearConversationAutoCompactionState(item)
return nil
})
return err
}
func (service *Service) snapshotCompactionCandidate(stream *ActiveStream, conversationID string) (*ConversationFile, error) {
if service == nil {
return nil, nil
}
candidate, _, _, err := service.snapshotCheckpointConversation(stream)
if err == nil && candidate != nil {
return candidate, nil
}
if service.recorder != nil {
latestState, ok, loadErr := service.loadLatestSummaryState(conversationID)
if loadErr != nil {
return nil, loadErr
}
if ok && latestState != nil && latestState.RuntimeSnapshot != nil {
return cloneConversationFile(latestState.RuntimeSnapshot), nil
}
}
if err != nil {
return nil, err
}
return nil, fmt.Errorf("conversation %q not found for compaction", strings.TrimSpace(conversationID))
}
func applyCompactionToConversation(conversation *ConversationFile, plan *PendingCompaction, summaryText string) error {
if conversation == nil || plan == nil {
return nil
}
compactionEntries, err := buildCompactedContextEntries(conversation, plan, summaryText)
if err != nil {
return err
}
// Canonical history stays append-only. The prompt projector applies the
// latest summary marker when constructing model-visible replay.
appendEntriesInPlace(conversation, resetEntrySequences(compactionEntries))
conversation.TokenDetailsUsedTokens = 0
clearConversationAutoCompactionState(conversation)
if conversation.TokenDetailsMaxTokens == 0 {
conversation.TokenDetailsMaxTokens = projectedConversationMaxTokens
}
return nil
}
func buildCompactedContextEntries(conversation *ConversationFile, plan *PendingCompaction, summaryText string) ([]HistoryEntry, error) {
if plan == nil {
return nil, nil
}
entries := []HistoryEntry{newCompactionSummaryEntry(plan, summaryText)}
runtimeEntry, ok, err := newCompactedRuntimeStateEntry(conversation, plan)
if err != nil {
return nil, err
}
if ok {
entries = append(entries, runtimeEntry)
}
return entries, nil
}
func newCompactionSummaryEntry(plan *PendingCompaction, summaryText string) HistoryEntry {
payload, _ := json.Marshal(compactionSummaryEntryPayload{
Summary: strings.TrimSpace(summaryText),
Trigger: strings.TrimSpace(plan.Trigger),
CurrentTurnSeq: plan.CurrentTurnSeq,
CurrentRequestID: strings.TrimSpace(plan.CurrentRequestID),
CompactTurnCount: plan.CompactTurnCount,
MessagesToCompact: plan.MessagesToCompact,
PreserveCurrentTurnInputs: plan.PreserveCurrentTurnInputs,
})
return HistoryEntry{
TurnSeq: plan.CurrentTurnSeq,
RequestID: strings.TrimSpace(plan.CurrentRequestID),
Role: "system",
Kind: "compacted_summary",
Payload: payload,
}
}
func newCompactedRuntimeStateEntry(conversation *ConversationFile, plan *PendingCompaction) (HistoryEntry, bool, error) {
state, err := projectConversationStructuredState(conversation)
if err != nil {
return HistoryEntry{}, false, err
}
payload := runtimeStateEntryPayload{
PlanText: state.PlanText,
Plans: clonePlanRegistryEntries(state.Plans),
Todos: cloneTodoItems(state.Todos),
}
if strings.TrimSpace(payload.PlanText) == "" && len(payload.Plans) == 0 && len(payload.Todos) == 0 {
return HistoryEntry{}, false, nil
}
encoded, err := json.Marshal(payload)
if err != nil {
return HistoryEntry{}, false, fmt.Errorf("encode compacted runtime state: %w", err)
}
return HistoryEntry{
TurnSeq: plan.CurrentTurnSeq,
RequestID: strings.TrimSpace(plan.CurrentRequestID),
Role: "system",
Kind: "runtime_state",
Payload: encoded,
}, true, nil
}
func newCompactionRequestEntry(plan *PendingCompaction) HistoryEntry {
payload, _ := json.Marshal(map[string]any{
"trigger": strings.TrimSpace(plan.Trigger),
"context_tokens": plan.ContextTokens,
"context_window_size": plan.ContextWindowSize,
"reserve_tokens": plan.ReserveTokens,
"messages_to_compact": plan.MessagesToCompact,
"compact_turn_count": plan.CompactTurnCount,
"request_source": strings.TrimSpace(plan.RequestSource),
"summary_model_call_id": strings.TrimSpace(plan.SummaryModelCallID),
})
return HistoryEntry{
TurnSeq: plan.CurrentTurnSeq,
RequestID: strings.TrimSpace(plan.CurrentRequestID),
Role: "system",
Kind: "compaction_request",
Payload: payload,
}
}
func newCompactionFailedEntry(plan *PendingCompaction, cause error) HistoryEntry {
payload := map[string]any{
"error": "compaction failed",
}
entry := HistoryEntry{
Role: "system",
Kind: "compaction_failed",
}
if cause != nil && strings.TrimSpace(cause.Error()) != "" {
payload["error"] = strings.TrimSpace(cause.Error())
}
if plan != nil {
payload["trigger"] = strings.TrimSpace(plan.Trigger)
payload["request_source"] = strings.TrimSpace(plan.RequestSource)
payload["summary_model_call_id"] = strings.TrimSpace(plan.SummaryModelCallID)
entry.TurnSeq = plan.CurrentTurnSeq
entry.RequestID = strings.TrimSpace(plan.CurrentRequestID)
}
entry.Payload, _ = json.Marshal(payload)
return entry
}
func buildFallbackCompactionSummary(plan *PendingCompaction) string {
sections := []string{
"Conversation summary",
"Earlier context was compacted into this summary. Preserve the facts, decisions, tool results, and user intent below when continuing the conversation.",
}
if plan == nil {
return strings.Join(sections, "\n\n")
}
if strings.TrimSpace(plan.ExistingSummary) != "" {
sections = append(sections, "Previous summary:\n"+truncateCompactionText(plan.ExistingSummary, compactionSummaryMaxChars/4))
}
lines := make([]string, 0, len(plan.CompactedTurns))
for index, item := range plan.CompactedTurns {
parts := make([]string, 0, len(item.Steps)+1)
if strings.TrimSpace(item.UserText) != "" {
parts = append(parts, "user="+truncateCompactionText(item.UserText, 400))
}
for _, step := range item.Steps {
if strings.TrimSpace(step) == "" {
continue
}
parts = append(parts, truncateCompactionText(step, 400))
}
if len(parts) == 0 {
continue
}
lines = append(lines, fmt.Sprintf("%d. %s", index+1, strings.Join(parts, " | ")))
}
if len(lines) > 0 {
sections = append(sections, "Compacted turns:\n"+truncateCompactionText(strings.Join(lines, "\n"), compactionSummaryMaxChars/2))
}
if strings.TrimSpace(plan.HookMessage) != "" {
sections = append(sections, "Compaction note:\n"+truncateCompactionText(plan.HookMessage, 800))
}
if strings.TrimSpace(plan.ManualInstruction) != "" {
sections = append(sections, "Manual summarize instruction:\n"+truncateCompactionText(plan.ManualInstruction, 800))
}
return strings.TrimSpace(truncateCompactionText(strings.Join(sections, "\n\n"), compactionSummaryMaxChars))
}
func validateCompactionCandidateBudget(compiled CompiledConversation, plan *PendingCompaction) error {
if plan == nil {
return nil
}
budgetTokens := plan.ContextWindowSize - plan.ReserveTokens
estimatedTokens := estimateCompiledPromptTokens(compiled)
if budgetTokens > 0 && estimatedTokens <= budgetTokens {
return nil
}
message := fmt.Sprintf(
"compaction result still exceeds context budget after rebuilding the summary (estimated=%d budget=%d)",
estimatedTokens,
budgetTokens,
)
if plan.PreserveCurrentTurnInputs {
message = fmt.Sprintf(
"compaction result still exceeds context budget after preserving the current user input (estimated=%d budget=%d)",
estimatedTokens,
budgetTokens,
)
}
return compactionTerminalError{
code: compactionOverflowTerminalCode,
message: message,
}
}
func compactionContextWindowSize(conversation *ConversationFile) int64 {
if conversation == nil {
return projectedConversationMaxTokens
}
if conversation.TokenDetailsMaxTokens > 0 {
return int64(conversation.TokenDetailsMaxTokens)
}
return projectedConversationMaxTokens
}
func (service *Service) resolveCompactionReserveTokens(modelID string) int64 {
_ = service
_ = modelID
return compactionAutoReserveTokens
}
func parseManualCompactionRequest(userMessage *agentv1.UserMessage) (string, bool) {
if userMessage == nil {
return "", false
}
userText := strings.TrimSpace(userMessage.GetText())
if instruction, ok := parseManualCompactionDirective(userText); ok {
return instruction, true
}
if userText != "" {
return "", false
}
selectedContext := userMessage.GetSelectedContext()
if selectedContext == nil {
return "", false
}
for _, command := range selectedContext.GetCursorCommands() {
if !isCursorSummarizeCommand(command) {
continue
}
instruction, _ := parseManualCompactionDirective(command.GetContent())
return instruction, true
}
return "", false
}
func streamManualCompactionDirective(stream *ActiveStream) (string, bool) {
if stream == nil {
return "", false
}
stream.mu.Lock()
defer stream.mu.Unlock()
if stream.ManualCompaction.Requested {
return strings.TrimSpace(stream.ManualCompaction.Instruction), true
}
return parseManualCompactionDirective(stream.LatestUserText)
}
func isCursorSummarizeCommand(command *agentv1.SelectedCursorCommand) bool {
if command == nil {
return false
}
if strings.EqualFold(strings.TrimSpace(command.GetName()), "glass-action-summarize") {
return true
}
_, ok := parseManualCompactionDirective(command.GetContent())
return ok
}
func parseManualCompactionDirective(latestUserText string) (string, bool) {
trimmed := strings.TrimSpace(latestUserText)
const directive = "/summarize"
switch {
case trimmed == directive:
return "", true
case strings.HasPrefix(trimmed, directive+" "):
return strings.TrimSpace(strings.TrimPrefix(trimmed, directive)), true
default:
return "", false
}
}
func buildPreCompactHookRequest(stream *ActiveStream, plan *compactionPlan) *agentv1.ExecuteHookRequest {
if stream == nil || plan == nil {
return nil
}
query := &agentv1.PreCompactRequestQuery{
Trigger: strings.TrimSpace(plan.Trigger),
ContextUsagePercent: plan.ContextUsagePercent,
ContextTokens: plan.ContextTokens,
ContextWindowSize: plan.ContextWindowSize,
MessageCount: plan.MessageCount,
MessagesToCompact: plan.MessagesToCompact,
IsFirstCompaction: plan.IsFirstCompaction,
ConversationId: &stream.ConversationID,
Model: &stream.ModelID,
}
if generationID := strings.TrimSpace(stream.CurrentModelCallID); generationID != "" {
query.GenerationId = &generationID
}
return &agentv1.ExecuteHookRequest{
Request: &agentv1.ExecuteHookRequest_PreCompact{
PreCompact: query,
},
}
}
func newPendingCompaction(plan *compactionPlan) *PendingCompaction {
if plan == nil {
return nil
}
clonedTurns := append([]compactedTurnSummary(nil), plan.CompactedTurns...)
return &PendingCompaction{
Trigger: plan.Trigger,
ContextTokens: plan.ContextTokens,
ContextWindowSize: plan.ContextWindowSize,
ContextUsagePercent: plan.ContextUsagePercent,
ReserveTokens: plan.ReserveTokens,
MessageCount: plan.MessageCount,
MessagesToCompact: plan.MessagesToCompact,
CompactTurnCount: plan.CompactTurnCount,
IsFirstCompaction: plan.IsFirstCompaction,
ExistingSummary: plan.ExistingSummary,
CompactedTurns: clonedTurns,
ManualInstruction: plan.ManualInstruction,
RequestSource: plan.RequestSource,
CurrentTurnSeq: plan.CurrentTurnSeq,
CurrentRequestID: plan.CurrentRequestID,
CurrentUserText: plan.CurrentUserText,
PreserveCurrentTurnInputs: plan.PreserveCurrentTurnInputs,
}
}
func clonePendingCompaction(plan *PendingCompaction) *PendingCompaction {
if plan == nil {
return nil
}
clonedTurns := append([]compactedTurnSummary(nil), plan.CompactedTurns...)
return &PendingCompaction{
Trigger: plan.Trigger,
ContextTokens: plan.ContextTokens,
ContextWindowSize: plan.ContextWindowSize,
ContextUsagePercent: plan.ContextUsagePercent,
ReserveTokens: plan.ReserveTokens,
MessageCount: plan.MessageCount,
MessagesToCompact: plan.MessagesToCompact,
CompactTurnCount: plan.CompactTurnCount,
IsFirstCompaction: plan.IsFirstCompaction,
ExistingSummary: plan.ExistingSummary,
CompactedTurns: clonedTurns,
ManualInstruction: plan.ManualInstruction,
RequestSource: plan.RequestSource,
CurrentTurnSeq: plan.CurrentTurnSeq,
CurrentRequestID: plan.CurrentRequestID,
CurrentUserText: plan.CurrentUserText,
PreserveCurrentTurnInputs: plan.PreserveCurrentTurnInputs,
HookMessage: plan.HookMessage,
SummaryModelCallID: plan.SummaryModelCallID,
StartedAt: plan.StartedAt,
}
}
func cloneCompactionPlanBase(base *compactionPlan) compactionPlan {
if base == nil {
return compactionPlan{}
}
return compactionPlan{
Trigger: base.Trigger,
ContextTokens: base.ContextTokens,
ContextWindowSize: base.ContextWindowSize,
ContextUsagePercent: base.ContextUsagePercent,
ReserveTokens: base.ReserveTokens,
MessageCount: base.MessageCount,
MessagesToCompact: base.MessagesToCompact,
CompactTurnCount: base.CompactTurnCount,
IsFirstCompaction: base.IsFirstCompaction,
ExistingSummary: base.ExistingSummary,
CompactedTurns: append([]compactedTurnSummary(nil), base.CompactedTurns...),
ManualInstruction: base.ManualInstruction,
RequestSource: base.RequestSource,
CurrentTurnSeq: base.CurrentTurnSeq,
CurrentRequestID: base.CurrentRequestID,
CurrentUserText: base.CurrentUserText,
PreserveCurrentTurnInputs: base.PreserveCurrentTurnInputs,
}
}
func selectTurnsForCompaction(candidates []compactionCandidateTurn, manual bool, reclaimTokens int64) (int, int32, []compactedTurnSummary) {
if len(candidates) <= compactionMinimumTailTurns {
return 0, 0, nil
}
preferredCompactCount := len(candidates) - compactionPreferredTailTurns
if preferredCompactCount < 0 {
preferredCompactCount = 0
}
maxCompactCount := preferredCompactCount
if maxCompactCount == 0 {
maxCompactCount = len(candidates) - compactionMinimumTailTurns
}
if maxCompactCount <= 0 {
return 0, 0, nil
}
selectedCount := 0
messageCount := int32(0)
summaries := make([]compactedTurnSummary, 0, maxCompactCount)
if manual {
for index := 0; index < maxCompactCount; index++ {
selectedCount++
messageCount += candidates[index].ReplayCount
summaries = append(summaries, candidates[index].Summary)
}
return selectedCount, messageCount, summaries
}
requiredTokens := reclaimTokens
if requiredTokens <= 0 {
requiredTokens = 1
}
reclaimedTokens := int64(0)
for index := 0; index < maxCompactCount; index++ {
selectedCount++
messageCount += candidates[index].ReplayCount
reclaimedTokens += candidates[index].EstimatedTokens
summaries = append(summaries, candidates[index].Summary)
if reclaimedTokens >= requiredTokens {
return selectedCount, messageCount, summaries
}
}
for index := maxCompactCount; index < len(candidates)-compactionMinimumTailTurns; index++ {
selectedCount++
messageCount += candidates[index].ReplayCount
reclaimedTokens += candidates[index].EstimatedTokens
summaries = append(summaries, candidates[index].Summary)
if reclaimedTokens >= requiredTokens {
return selectedCount, messageCount, summaries
}
}
if selectedCount <= 0 {
return 0, 0, nil
}
return selectedCount, messageCount, summaries
}
func buildCompactionCandidates(rawTurns [][]byte) ([]compactionCandidateTurn, error) {
candidates := make([]compactionCandidateTurn, 0, len(rawTurns))
for _, rawTurn := range rawTurns {
if len(rawTurn) == 0 {
continue
}
turn := &agentv1.ConversationTurnStructure{}
if err := proto.Unmarshal(rawTurn, turn); err != nil {
return nil, fmt.Errorf("decode compacted turn candidate: %w", err)
}
agentTurn := turn.GetAgentConversationTurn()
if agentTurn == nil {
continue
}
replayMessages := buildReplayMessagesFromAgentTurn(agentTurn)
candidates = append(candidates, compactionCandidateTurn{
Summary: buildCompactedTurnSummary(agentTurn),
ReplayCount: clampInt64ToInt32(int64(len(replayMessages))),
EstimatedTokens: estimatePromptReplayMessagesTokens(replayMessages),
})
}
return candidates, nil
}
func buildCurrentTurnCompactionCandidate(entries []HistoryEntry, turnSeq int64, requestID string) (compactionCandidateTurn, bool) {
if len(entries) == 0 || turnSeq <= 0 || strings.TrimSpace(requestID) == "" {
return compactionCandidateTurn{}, false
}
normalizedRequestID := strings.TrimSpace(requestID)
latestToolCallID := latestCompletedToolCallIDForTurn(entries, turnSeq, normalizedRequestID)
if latestToolCallID == "" {
return compactionCandidateTurn{}, false
}
preservedEntryIndexes := autoCompactionPreservedEntryIndexes(entries, turnSeq, normalizedRequestID, latestToolCallID)
summary := compactedTurnSummary{}
replayCount := int32(0)
estimatedTokens := int64(0)
removedToolHistory := false
for index, entry := range entries {
if entry.TurnSeq != turnSeq || strings.TrimSpace(entry.RequestID) != normalizedRequestID {
continue
}
if strings.TrimSpace(entry.Kind) == "user_message" && strings.TrimSpace(summary.UserText) == "" {
summary.UserText = currentTurnUserText(entry)
}
if _, ok := preservedEntryIndexes[index]; ok {
continue
}
switch strings.TrimSpace(entry.Kind) {
case "assistant_text":
if step := summarizeCurrentTurnAssistantEntry(entry); step != "" {
summary.Steps = append(summary.Steps, step)
}
replayCount++
estimatedTokens += estimateTextTokens(string(entry.Payload))
case "tool_call":
removedToolHistory = true
if step := summarizeCurrentTurnToolCallEntry(entry); step != "" {
summary.Steps = append(summary.Steps, step)
}
replayCount++
estimatedTokens += estimateTextTokens(string(entry.Payload))
case "tool_result":
removedToolHistory = true
if step := summarizeCurrentTurnToolResultEntry(entry); step != "" {
summary.Steps = append(summary.Steps, step)
}
replayCount++
estimatedTokens += estimateTextTokens(string(entry.Payload))
}
}
if !removedToolHistory || replayCount <= 0 {
return compactionCandidateTurn{}, false
}
if len(summary.Steps) == 0 {
summary.Steps = append(summary.Steps, "tool_history=earlier current-turn tool history compacted")
}
return compactionCandidateTurn{
Summary: summary,
ReplayCount: replayCount,
EstimatedTokens: estimatedTokens,
}, true
}
func buildContextCompactionCandidates(entries []HistoryEntry, currentTurnSeq int64, currentRequestID string) []compactionCandidateTurn {
if len(entries) == 0 {
return nil
}
turnOrder := make([]int64, 0)
grouped := make(map[int64][]HistoryEntry)
for _, entry := range entries {
if entry.TurnSeq <= 0 || isCompactionSummaryKind(entry.Kind) {
continue
}
if entry.TurnSeq == currentTurnSeq && strings.TrimSpace(entry.RequestID) == strings.TrimSpace(currentRequestID) {
continue
}
if _, ok := grouped[entry.TurnSeq]; !ok {
turnOrder = append(turnOrder, entry.TurnSeq)
}
grouped[entry.TurnSeq] = append(grouped[entry.TurnSeq], entry)
}
candidates := make([]compactionCandidateTurn, 0, len(turnOrder))
for _, turnSeq := range turnOrder {
if candidate, ok := buildContextTurnCompactionCandidate(grouped[turnSeq]); ok {
candidates = append(candidates, candidate)
}
}
return candidates
}
func buildContextTurnCompactionCandidate(entries []HistoryEntry) (compactionCandidateTurn, bool) {
if len(entries) == 0 {
return compactionCandidateTurn{}, false
}
summary := compactedTurnSummary{}
replayCount := int32(0)
estimatedTokens := int64(0)
for _, entry := range entries {
switch strings.TrimSpace(entry.Kind) {
case "user_message":
if strings.TrimSpace(summary.UserText) == "" {
summary.UserText = currentTurnUserText(entry)
}
replayCount++
estimatedTokens += estimateTextTokens(string(entry.Payload))
case "assistant_text":
if step := summarizeCurrentTurnAssistantEntry(entry); step != "" {
summary.Steps = append(summary.Steps, step)
}
replayCount++
estimatedTokens += estimateTextTokens(string(entry.Payload))
case "tool_call":
if step := summarizeCurrentTurnToolCallEntry(entry); step != "" {
summary.Steps = append(summary.Steps, step)
}
replayCount++
estimatedTokens += estimateTextTokens(string(entry.Payload))
case "tool_result":
if step := summarizeCurrentTurnToolResultEntry(entry); step != "" {
summary.Steps = append(summary.Steps, step)
}
replayCount++
estimatedTokens += estimateTextTokens(string(entry.Payload))
}
}
if replayCount <= 0 || (strings.TrimSpace(summary.UserText) == "" && len(summary.Steps) == 0) {
return compactionCandidateTurn{}, false
}
return compactionCandidateTurn{
Summary: summary,
ReplayCount: replayCount,
EstimatedTokens: estimatedTokens,
}, true
}
func countCompactableContextTurns(entries []HistoryEntry, currentTurnSeq int64, currentRequestID string) int32 {
return clampInt64ToInt32(int64(len(buildContextCompactionCandidates(entries, currentTurnSeq, currentRequestID))))
}
func currentTurnUserText(entry HistoryEntry) string {
userMessage := &agentv1.UserMessage{}
if err := protojson.Unmarshal(entry.Payload, userMessage); err != nil {
return ""
}
return truncateCompactionText(userMessage.GetText(), compactionTurnSnippetMaxChars/3)
}
func summarizeCurrentTurnAssistantEntry(entry HistoryEntry) string {
var payload assistantTextPayload
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
return ""
}
if text := truncateCompactionText(payload.Text, compactionTurnSnippetMaxChars/3); text != "" {
return "assistant=" + text
}
if text := truncateCompactionText(payload.ReasoningContent, compactionTurnSnippetMaxChars/4); text != "" {
return "thinking=" + text
}
return ""
}
func summarizeCurrentTurnToolCallEntry(entry HistoryEntry) string {
var payload toolCallEntryPayload
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
return ""
}
toolName := strings.TrimSpace(payload.ToolName)
if toolName == "" {
toolName = "tool_call"
}
return toolName + "=called"
}
func summarizeCurrentTurnToolResultEntry(entry HistoryEntry) string {
var payload toolResultEntryPayload
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
return ""
}
if len(payload.ToolCall) > 0 {
toolCall := &agentv1.ToolCall{}
if err := protojson.Unmarshal(payload.ToolCall, toolCall); err == nil {
if toolName, detail := summarizeCompactedToolCall(toolCall); toolName != "" {
return toolName + "=" + detail
}
}
}
toolName := strings.TrimSpace(payload.ToolName)
if toolName == "" {
toolName = "tool_result"
}
if result := truncateCompactionText(payload.ResultText, compactionTurnSnippetMaxChars/3); result != "" {
return toolName + "=" + result
}
return toolName + "=completed"
}
func buildReplayMessagesFromAgentTurn(agentTurn *agentv1.AgentConversationTurnStructure) []promptengine.Message {
if agentTurn == nil {
return nil
}
messages := make([]promptengine.Message, 0, 4)
if rawUser := agentTurn.GetUserMessage(); len(rawUser) > 0 {
userMessage := &agentv1.UserMessage{}
if err := proto.Unmarshal(rawUser, userMessage); err == nil {
if replay, ok := promptengine.BuildUserMessageReplayMessage(userMessage); ok {
messages = append(messages, replay)
}
}
}
for _, rawStep := range agentTurn.GetSteps() {
if len(rawStep) == 0 {
continue
}
step := &agentv1.ConversationStep{}
if err := proto.Unmarshal(rawStep, step); err != nil {
continue
}
messages = append(messages, promptengine.BuildLegacyMessagesFromConversationStep(step)...)
}
return messages
}
func estimatePromptReplayMessagesTokens(messages []promptengine.Message) int64 {
total := int64(0)
for _, message := range messages {
total += estimateTextTokens(message.Role)
total += estimateTextTokens(message.Content)
total += estimatePromptContentPartsTokens(message.Content, message.ContentParts)
total += estimateTextTokens(message.ReasoningContent)
total += estimateTextTokens(message.ReasoningSignature)
total += estimateTextTokens(message.ToolCallID)
total += estimateTextTokens(message.Name)
}
return total
}
func buildCompactedTurnSummary(agentTurn *agentv1.AgentConversationTurnStructure) compactedTurnSummary {
if agentTurn == nil {
return compactedTurnSummary{}
}
item := compactedTurnSummary{}
if rawUser := agentTurn.GetUserMessage(); len(rawUser) > 0 {
userMessage := &agentv1.UserMessage{}
if err := proto.Unmarshal(rawUser, userMessage); err == nil {
item.UserText = truncateCompactionText(userMessage.GetText(), compactionTurnSnippetMaxChars/3)
if imageCount := len(userMessage.GetSelectedContext().GetSelectedImages()); imageCount > 0 {
suffix := fmt.Sprintf(" [attached_images=%d]", imageCount)
if strings.TrimSpace(item.UserText) == "" {
item.UserText = strings.TrimSpace(suffix)
} else {
item.UserText += suffix
}
}
}
}
for _, rawStep := range agentTurn.GetSteps() {
if len(rawStep) == 0 {
continue
}
step := &agentv1.ConversationStep{}
if err := proto.Unmarshal(rawStep, step); err != nil {
continue
}
switch message := step.GetMessage().(type) {
case *agentv1.ConversationStep_AssistantMessage:
if text := truncateCompactionText(message.AssistantMessage.GetText(), compactionTurnSnippetMaxChars/3); text != "" {
item.Steps = append(item.Steps, "assistant="+text)
}
case *agentv1.ConversationStep_ThinkingMessage:
if text := truncateCompactionText(message.ThinkingMessage.GetText(), compactionTurnSnippetMaxChars/4); text != "" {
item.Steps = append(item.Steps, "thinking="+text)
}
case *agentv1.ConversationStep_ToolCall:
if toolName, toolDetail := summarizeCompactedToolCall(message.ToolCall); toolName != "" {
item.Steps = append(item.Steps, toolName+"="+toolDetail)
}
}
}
return item
}
func rewriteAutoCompactionToolResultEntry(entry HistoryEntry, limitBytes int, minimal bool) (HistoryEntry, bool) {
if strings.TrimSpace(entry.Kind) != "tool_result" {
return entry, false
}
var payload toolResultEntryPayload
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
return entry, false
}
toolName := firstNonEmpty(strings.TrimSpace(payload.ToolName), "tool")
resultText := strings.TrimSpace(payload.ResultText)
if compacted, ok := compactProjectedEditToolResultReplay(toolName, resultText); ok {
resultText = compacted
}
if minimal {
resultText = autoCompactionOmittedToolResultText(toolName)
payload.ToolCall = nil
} else {
if resultText == "" {
resultText = autoCompactionOmittedToolResultText(toolName)
} else {
resultText = truncateProjectedReplayText(toolName, resultText, limitBytes)
}
if len(payload.ToolCall) > limitBytes || len(payload.ResultText) > limitBytes {
payload.ToolCall = nil
}
}
payload.ResultText = resultText
encoded, err := json.Marshal(payload)
if err != nil {
return entry, false
}
if bytes.Equal(bytes.TrimSpace(entry.Payload), bytes.TrimSpace(encoded)) {
return entry, false
}
entry.Payload = encoded
return entry, true
}
func autoCompactionOmittedToolResultText(toolName string) string {
return fmt.Sprintf(
"[%s result omitted by auto compaction because the preserved current turn still exceeded the context budget; rerun the relevant tool if exact output is needed]",
firstNonEmpty(strings.TrimSpace(toolName), "tool"),
)
}
func autoCompactionPreservedEntryIndexes(entries []HistoryEntry, turnSeq int64, requestID string, latestToolCallID string) map[int]struct{} {
preserved := make(map[int]struct{})
if len(entries) == 0 || turnSeq <= 0 {
return preserved
}
normalizedRequestID := strings.TrimSpace(requestID)
normalizedToolCallID := strings.TrimSpace(latestToolCallID)
latestToolCallIndex := -1
for index, entry := range entries {
if entry.TurnSeq != turnSeq || strings.TrimSpace(entry.RequestID) != normalizedRequestID {
continue
}
if shouldPreserveAutoCompactionEntry(entry, normalizedToolCallID) {
preserved[index] = struct{}{}
}
if strings.TrimSpace(entry.Kind) == "tool_call" && historyEntryToolCallID(entry) == normalizedToolCallID {
latestToolCallIndex = index
}
}
if latestToolCallIndex < 0 || !toolCallEntryNeedsReasoningCarrier(entries[latestToolCallIndex]) {
return preserved
}
if carrierIndex := latestAssistantReasoningCarrierIndex(entries, latestToolCallIndex, turnSeq, normalizedRequestID); carrierIndex >= 0 {
preserved[carrierIndex] = struct{}{}
}
return preserved
}
func toolCallEntryNeedsReasoningCarrier(entry HistoryEntry) bool {
if strings.TrimSpace(entry.Kind) != "tool_call" {
return false
}
var payload toolCallEntryPayload
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
return false
}
return strings.TrimSpace(payload.ReasoningContent) == "" || strings.TrimSpace(payload.ReasoningSignature) == ""
}
func latestAssistantReasoningCarrierIndex(entries []HistoryEntry, beforeIndex int, turnSeq int64, requestID string) int {
if len(entries) == 0 || beforeIndex <= 0 {
return -1
}
for index := beforeIndex - 1; index >= 0; index-- {
entry := entries[index]
if entry.TurnSeq != turnSeq || strings.TrimSpace(entry.RequestID) != strings.TrimSpace(requestID) {
continue
}
switch strings.TrimSpace(entry.Kind) {
case "assistant_text":
var payload assistantTextPayload
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
continue
}
if strings.TrimSpace(payload.ReasoningContent) != "" && strings.TrimSpace(payload.ReasoningSignature) != "" {
return index
}
case "user_message", "request_context":
return -1
}
}
return -1
}
func historyEntryToolCallID(entry HistoryEntry) string {
if toolCallID := strings.TrimSpace(entry.ToolCallID); toolCallID != "" {
return toolCallID
}
switch strings.TrimSpace(entry.Kind) {
case "tool_call":
var payload toolCallEntryPayload
if err := json.Unmarshal(entry.Payload, &payload); err == nil {
return strings.TrimSpace(payload.ToolCallID)
}
case "tool_result":
var payload toolResultEntryPayload
if err := json.Unmarshal(entry.Payload, &payload); err == nil {
return strings.TrimSpace(payload.ToolCallID)
}
}
return ""
}
func shouldPreserveAutoCompactionEntry(entry HistoryEntry, latestToolCallID string) bool {
switch strings.TrimSpace(entry.Kind) {
case "request_context", "user_message":
return true
case "tool_call", "tool_result":
toolCallID := historyEntryToolCallID(entry)
return toolCallID != "" && toolCallID == strings.TrimSpace(latestToolCallID)
case "metadata":
var payload metadataPayload
if err := json.Unmarshal(entry.Payload, &payload); err != nil {
return false
}
switch strings.TrimSpace(payload.Type) {
case "mode", "run_request":
return true
default:
return false
}
default:
return false
}
}
func latestCompletedToolCallIDForTurn(entries []HistoryEntry, turnSeq int64, requestID string) string {
normalizedRequestID := strings.TrimSpace(requestID)
latest := ""
for _, entry := range entries {
if entry.TurnSeq != turnSeq || strings.TrimSpace(entry.RequestID) != normalizedRequestID {
continue
}
if strings.TrimSpace(entry.Kind) != "tool_result" {
continue
}
toolCallID := strings.TrimSpace(entry.ToolCallID)
if toolCallID == "" {
var payload toolResultEntryPayload
if err := json.Unmarshal(entry.Payload, &payload); err == nil {
toolCallID = strings.TrimSpace(payload.ToolCallID)
}
}
if toolCallID != "" {
latest = toolCallID
}
}
return latest
}
type compactionTerminalError struct {
code string
message string
}
func (err compactionTerminalError) Error() string {
if strings.TrimSpace(err.message) != "" {
return strings.TrimSpace(err.message)
}
if strings.TrimSpace(err.code) != "" {
return strings.TrimSpace(err.code)
}
return "compaction failed"
}
func (err compactionTerminalError) TerminalCode() string {
return strings.TrimSpace(err.code)
}
func (service *Service) buildCompactionSummaryMessages(plan *PendingCompaction) ([]modeladapter.Message, error) {
if plan == nil {
return nil, nil
}
systemText, err := promptassets.ReadCompactionPrompt()
if err != nil {
return nil, err
}
systemText = strings.TrimSpace(systemText)
if systemText == "" {
return nil, fmt.Errorf("compaction prompt asset is empty")
}
sections := make([]string, 0, len(plan.CompactedTurns)+4)
if strings.TrimSpace(plan.ExistingSummary) != "" {
sections = append(sections, "Existing summary:\n"+strings.TrimSpace(plan.ExistingSummary))
}
lines := make([]string, 0, len(plan.CompactedTurns))
for index, item := range plan.CompactedTurns {
parts := make([]string, 0, 1+len(item.Steps))
if strings.TrimSpace(item.UserText) != "" {
parts = append(parts, "user="+strings.TrimSpace(item.UserText))
}
parts = append(parts, item.Steps...)
if len(parts) == 0 {
continue
}
lines = append(lines, fmt.Sprintf("%d. %s", index+1, strings.Join(parts, " | ")))
}
if len(lines) > 0 {
sections = append(sections, "History to compact:\n"+strings.Join(lines, "\n"))
}
if strings.TrimSpace(plan.HookMessage) != "" {
sections = append(sections, "Pre-compact hook guidance:\n"+strings.TrimSpace(plan.HookMessage))
}
if strings.TrimSpace(plan.ManualInstruction) != "" {
sections = append(sections, "User emphasis for this manual compact:\n"+strings.TrimSpace(plan.ManualInstruction))
}
sections = append(sections, "Return only the replacement summary text.")
return []modeladapter.Message{
{Role: "system", Content: systemText},
{Role: "user", Content: strings.Join(sections, "\n\n")},
}, nil
}
func (service *Service) generateCompactionSummary(ctx context.Context, stream *ActiveStream, plan *PendingCompaction, modelCallID string) (string, error) {
if service == nil || stream == nil || plan == nil {
return "", nil
}
messages, err := service.buildCompactionSummaryMessages(plan)
if err != nil {
return "", err
}
if len(messages) == 0 {
return "", nil
}
accumulated := ""
usage := turnUsageSnapshot{}
err = service.provider.StartStream(ctx, ProviderRequest{
RequestID: stream.RequestID,
ConversationID: stream.ConversationID,
RunID: stream.RequestID,
ModelCallID: modelCallID,
ModelID: stream.ModelID,
Mode: agentv1.AgentMode_AGENT_MODE_AGENT,
Messages: messages,
MaxTokens: compactionSummaryOutputMaxTokens,
CompileSummary: fmt.Sprintf("compaction trigger=%s source=%s turns=%d messages=%d", plan.Trigger, plan.RequestSource, plan.CompactTurnCount, plan.MessagesToCompact),
Observer: service.recorder,
ArtifactPaths: &modeladapter.LLMArtifactPaths{},
}, func(event modeladapter.ModelEvent) error {
if err := providerLoopInterruptErr(ctx, stream, modelCallID); err != nil {
return err
}
switch event.Kind {
case modeladapter.ModelEventKindTextDelta:
accumulated += event.Text
if strings.TrimSpace(accumulated) == "" {
return nil
}
return service.broker.Publish(stream.RequestID, StreamEvent{
Message: buildSummaryMessage(accumulated),
})
case modeladapter.ModelEventKindThinkingDelta, modeladapter.ModelEventKindThinkingCompleted:
return nil
case modeladapter.ModelEventKindToolLikeCompleted:
return fmt.Errorf("compaction summary generation must not invoke tools")
case modeladapter.ModelEventKindTurnFinished:
usage = turnUsageSnapshot{
Provider: event.Provider,
Model: event.Model,
InputTokens: event.InputTokens,
OutputTokens: event.OutputTokens,
CacheReadTokens: event.CacheReadTokens,
CacheWriteTokens: event.CacheWriteTokens,
UsagePresent: event.UsagePresent,
CacheReadPresent: event.CacheReadPresent,
CacheWritePresent: event.CacheWritePresent,
}
return nil
case modeladapter.ModelEventKindProviderError:
if event.Err != nil {
return providerTerminalError{cause: event.Err}
}
return providerTerminalError{cause: fmt.Errorf("provider error")}
default:
return nil
}
})
if err != nil {
if !errors.Is(err, errProviderLoopInterrupted) {
stream.mu.Lock()
conversationID := stream.ConversationID
requestID := stream.RequestID
turnSeq := stream.TurnSeq
stream.mu.Unlock()
if usageErr := service.recordTurnUsageSnapshot(stream, conversationID, turnSeq, requestID, modelCallID, "provider_error", usage, err.Error(), false); usageErr != nil {
return "", fmt.Errorf("record compaction provider error usage: %w", usageErr)
}
}
return "", err
}
stream.mu.Lock()
conversationID := stream.ConversationID
requestID := stream.RequestID
turnSeq := stream.TurnSeq
stream.mu.Unlock()
if err := service.recordTurnUsageSnapshot(stream, conversationID, turnSeq, requestID, modelCallID, "completed", usage, "", false); err != nil {
return "", fmt.Errorf("record compaction provider usage: %w", err)
}
return strings.TrimSpace(accumulated), nil
}
func existingConversationSummaryText(conversation *ConversationFile) string {
texts := compactionSummaryTexts(conversation)
if len(texts) == 0 {
return ""
}
return strings.TrimSpace(texts[len(texts)-1])
}
func encodeConversationSummaryBytes(summary string) []byte {
text := strings.TrimSpace(summary)
if text == "" {
return nil
}
payload, err := proto.Marshal(&agentv1.ConversationSummary{Summary: text})
if err != nil {
return nil
}
return payload
}
func decodeCompactedTurnSummaries(rawTurns [][]byte) ([]compactedTurnSummary, error) {
summaries := make([]compactedTurnSummary, 0, len(rawTurns))
for _, rawTurn := range rawTurns {
if len(rawTurn) == 0 {
continue
}
turn := &agentv1.ConversationTurnStructure{}
if err := proto.Unmarshal(rawTurn, turn); err != nil {
return nil, fmt.Errorf("decode compacted turn: %w", err)
}
agentTurn := turn.GetAgentConversationTurn()
if agentTurn == nil {
continue
}
summaries = append(summaries, buildCompactedTurnSummary(agentTurn))
}
return summaries, nil
}
func summarizeCompactedToolCall(toolCall *agentv1.ToolCall) (string, string) {
shape, ok := extractCompactToolCallShape(toolCall)
if !ok {
return "", ""
}
return shape.ToolName, truncateCompactionText(shape.ResultJSON, compactionTurnSnippetMaxChars/3)
}
type compactToolCallShape struct {
ArgsJSON string
ToolName string
ResultJSON string
}
func extractCompactToolCallShape(toolCall *agentv1.ToolCall) (compactToolCallShape, bool) {
if toolCall == nil {
return compactToolCallShape{}, false
}
value := toolCall.ProtoReflect()
oneof := value.Descriptor().Oneofs().ByName("tool")
if oneof == nil {
return compactToolCallShape{}, false
}
selected := value.WhichOneof(oneof)
if selected == nil {
return compactToolCallShape{}, false
}
selectedValue := value.Get(selected)
if !selectedValue.IsValid() {
return compactToolCallShape{}, false
}
selectedMessage := selectedValue.Message()
if !selectedMessage.IsValid() {
return compactToolCallShape{}, false
}
argsJSON, _ := extractCompactFieldJSON(selectedMessage, "args")
resultJSON, _ := extractCompactFieldJSON(selectedMessage, "result")
toolName := canonicalCompactToolName(string(selected.Name()), string(selectedMessage.Descriptor().Name()), argsJSON, resultJSON)
if toolName == "" {
return compactToolCallShape{}, false
}
return compactToolCallShape{
ArgsJSON: argsJSON,
ToolName: toolName,
ResultJSON: resultJSON,
}, true
}
func canonicalCompactToolName(fieldName string, messageName string, argsJSON string, resultJSON string) string {
switch strings.TrimSpace(fieldName) {
case "mcp_tool_call":
return "CallMcpTool"
case "read_mcp_resource_tool_call":
return "FetchMcpResource"
case "update_todos_tool_call":
return "TodoWrite"
case "read_todos_tool_call":
return "ReadTodos"
case "sem_search_tool_call":
return "SemanticSearch"
case "edit_tool_call":
return compactEditToolName(argsJSON, resultJSON)
}
trimmed := strings.TrimSuffix(strings.TrimSpace(messageName), "ToolCall")
return strings.TrimSpace(trimmed)
}
func compactEditToolName(argsJSON string, resultJSON string) string {
if editResultJSONLooksLikeStructuredEdit(resultJSON) {
return "Edit"
}
if compactEditArgsIndicateWrite(argsJSON) {
return "Write"
}
return "Edit"
}
func compactEditArgsIndicateWrite(argsJSON string) bool {
trimmed := strings.TrimSpace(argsJSON)
if trimmed == "" || trimmed == "{}" || trimmed == "null" {
return false
}
var args map[string]any
if err := json.Unmarshal([]byte(trimmed), &args); err != nil {
return false
}
for _, key := range []string{"stream_content", "streamContent"} {
if _, ok := args[key]; ok {
return true
}
}
return false
}
func editResultJSONLooksLikeStructuredEdit(resultJSON string) bool {
trimmed := strings.TrimSpace(resultJSON)
if trimmed == "" || trimmed == "{}" || trimmed == "null" {
return false
}
var payload map[string]any
if err := json.Unmarshal([]byte(trimmed), &payload); err != nil {
return false
}
success, ok := payload["success"].(map[string]any)
if !ok || len(success) == 0 {
return false
}
if _, ok := success["beforeFullFileContent"]; ok {
return true
}
if _, ok := success["before_full_file_content"]; ok {
return true
}
if _, ok := success["diffString"]; ok {
return true
}
if _, ok := success["diff_string"]; ok {
return true
}
return false
}
func extractCompactFieldJSON(message protoreflect.Message, fieldName string) (string, bool) {
if !message.IsValid() {
return "", false
}
field := message.Descriptor().Fields().ByName(protoreflect.Name(fieldName))
if field == nil || !message.Has(field) {
return "", false
}
value := message.Get(field)
if !value.IsValid() {
return "", false
}
child := value.Message()
if !child.IsValid() {
return "", false
}
item, ok := child.Interface().(proto.Message)
if !ok {
return "", false
}
payload, err := protojson.MarshalOptions{EmitUnpopulated: false}.Marshal(item)
if err != nil {
return "", false
}
return strings.TrimSpace(string(payload)), true
}
func buildCompactedConversationSummary(existingSummary string, turns []compactedTurnSummary) string {
parts := make([]string, 0, len(turns)+1)
if strings.TrimSpace(existingSummary) != "" {
parts = append(parts, strings.TrimSpace(existingSummary))
}
for index, item := range turns {
segments := make([]string, 0, 1+len(item.Steps))
if strings.TrimSpace(item.UserText) != "" {
segments = append(segments, "user="+strings.TrimSpace(item.UserText))
}
segments = append(segments, item.Steps...)
if len(segments) == 0 {
continue
}
parts = append(parts, fmt.Sprintf("%d. %s", index+1, strings.Join(segments, " | ")))
}
return truncateCompactionText(strings.Join(parts, "\n"), compactionSummaryMaxChars)
}
func estimateCompactedTurnSummariesTokens(rawTurns [][]byte) int64 {
summaries, err := decodeCompactedTurnSummaries(rawTurns)
if err != nil {
return 0
}
total := int64(0)
for _, item := range summaries {
total += estimateTextTokens(item.UserText)
for _, step := range item.Steps {
total += estimateTextTokens(step)
}
}
return total
}
func truncateCompactionText(text string, maxChars int) string {
trimmed := strings.TrimSpace(text)
if trimmed == "" || maxChars <= 0 {
return ""
}
runes := []rune(trimmed)
if len(runes) <= maxChars {
return trimmed
}
return string(runes[:maxChars]) + "..."
}