compaction

package
v0.5.1-rc.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Controller adapts the compaction service to the step-loop task seam.

Package compaction summarizes a session's older history into a state record when the context window fills, keeping the newest messages verbatim.

The compaction service runs one compaction boundary end to end. Concrete config/provider/plugin/session/processor services are represented by narrow interfaces; the state transitions and model-visible strings live here.

Post-compaction message surgery: the overflow history selection, the replay of the message that overflowed, and the auto-continue text. These helpers are pure apart from the injected ID factory.

Index

Constants

View Source
const (
	PruneMinimum = 20_000
	PruneProtect = 40_000
	// ToolOutputMaxChars caps each tool output inside the flattened transcript
	// handed to the summarizer.
	ToolOutputMaxChars = 2_000
	// SummaryTranscriptMaxChars bounds the whole flattened transcript. A head
	// that outgrows it is cut in the middle (a quarter from the start, the rest
	// from the end) so the summarizer sees how the work began and, mostly, its
	// latest state. ~60K tokens, well inside any current model's window.
	SummaryTranscriptMaxChars  = 240_000
	EvidenceToolOutputMaxChars = 2_000
	EventCompacted             = "session.compacted"
)
View Source
const (
	SummaryClassToolCall = "tool-call"
	SummaryClassDSMLText = "dsml-text"
	SummaryClassEmpty    = "empty"
	SummaryClassFormat   = "contract-format"
)

Summary failure classes, emitted on the compaction decision event so every rejected summary can be attributed from the event stream.

View Source
const (
	// DefaultPreserveRecentFraction sizes the token budget for the truncated
	// older messages of the tail as a fraction of the high watermark, so the
	// tail grows with the budget. The newest message is kept on top of it.
	DefaultPreserveRecentFraction = 0.2
	// TailToolOutputMaxChars caps each completed tool output of an OLDER tail
	// message. Head/tail truncation keeps the start and, mostly, the end, so
	// an error at the bottom of a test run survives.
	TailToolOutputMaxChars = 4_000
	// TailReasoningMaxChars caps each unsigned reasoning part of EVERY tail
	// message to its head.
	TailReasoningMaxChars = 1_000
)

The verbatim tail a compaction keeps ahead of the summary.

The newest messages are kept verbatim at every boundary and only what lies before them is summarized. Old tool outputs INSIDE the tail are truncated in the store, so a tail always fits: measured on untruncated multi-kilobyte command outputs, one assistant message could cost more than the whole budget and no split point would ever be found.

Reasoning is cut too, in every kept message including the newest. The same-model projection re-sends every reasoning part with its provider metadata, and a single message can carry far more reasoning than tool traffic. Reasoning is the model's scratch, not state; signed reasoning (Anthropic) is left alone because the provider validates it byte for byte.

View Source
const SummarySystemPrompt = `` /* 357-byte string literal not displayed */
View Source
const SummaryTemplate = `` /* 507-byte string literal not displayed */

The task itself is pinned verbatim beside every summary, so the record does not repeat it. The contract is deliberately small: current work, exact verification evidence, and the next concrete action.

Variables

View Source
var ErrContextCapacityExhausted = errors.New("compaction: context capacity exhausted")
View Source
var PruneProtectedTools = []string{"skill"}

Functions

func BuildAuthoritativeTaskPin

func BuildAuthoritativeTaskPin(task, source string) string

func BuildChangedFilesPin

func BuildChangedFilesPin(lines []string) string

BuildChangedFilesPin renders the code-computed changed-files record that is pinned beside every summary: a generated summary can forget a file, a diffstat cannot.

func BuildPrompt

func BuildPrompt(previousSummary *string, context []string) string

func CapTranscript

func CapTranscript(transcript string, maxChars float64) (string, float64)

CapTranscript bounds a flattened transcript to maxChars, cutting the middle so the beginning and the latest state both survive. It returns the number of characters removed so the boundary can report it.

func ClassifySummaryFailure

func ClassifySummaryFailure(message msgmodel.WithParts) string

ClassifySummaryFailure labels a rejected summary attempt. tool-call is a structural ToolPart; dsml-text is provider tool-call markup leaked as TEXT; empty is no usable text; contract-format is any other heading/structure violation.

func EvidenceBlocksFromMessages

func EvidenceBlocksFromMessages(
	messages []msgmodel.WithParts, maxToolChars ...float64,
) []string

func HeadTailTruncate

func HeadTailTruncate(text string, maxChars float64) string

func IsSyntheticUser

func IsSyntheticUser(message msgmodel.WithParts) bool

func NormalizeOffFormatSummary

func NormalizeOffFormatSummary(message msgmodel.WithParts) (string, bool)

NormalizeOffFormatSummary recovers a stateful answer (headed "Summary of Changes" or similar) that would otherwise be discarded solely because it missed the report template. Coding continuations and tool calls still fail closed; only an explicitly summary/state-shaped text is wrapped.

func SerializeTranscript

func SerializeTranscript(messages []msgmodel.WithParts, maxToolChars float64) string

SerializeTranscript flattens the conversation being summarized into ONE plain-text block for the summary model call (the caller wraps it in <conversation> tags). Handing the summarizer the session as a live message array invites the model to keep coding — leaking tool-call markup as text or narrating its next action — instead of serializing state. A flattened transcript cannot be "continued": there is no open tool call, no assistant turn to extend, only data to read.

Each tool output is head/tail truncated (HeadTailTruncate) so the serialized block is bounded and, in practice, smaller than the message array it replaces.

func TailBudget

func TailBudget(cfg overflow.Config, marks overflow.CompactionWatermarks) float64

tailBudget is the token budget for the truncated older messages of the verbatim tail. An explicit preserve_recent_tokens always wins. Otherwise the tail is preserve_recent_fraction (default DefaultPreserveRecentFraction) of the high watermark, so a 300K trigger keeps a 60K tail rather than a fixed one.

func ValidateSummary

func ValidateSummary(message msgmodel.WithParts) error

ValidateSummary rejects a response before it can become a compaction boundary. Structural step/reasoning parts are harmless provider metadata, but a tool-shaped response is never a serialized session state.

func ValidateSummaryText

func ValidateSummaryText(text string) error

Types

type Agent

type Agent struct {
	Name  string
	Model *ModelRef
}

type AgentProvider

type AgentProvider interface {
	GetAgent(ctx context.Context, name string) (Agent, error)
}

type AgentProviderFunc

type AgentProviderFunc func(ctx context.Context, name string) (Agent, error)

func (AgentProviderFunc) GetAgent

func (f AgentProviderFunc) GetAgent(ctx context.Context, name string) (Agent, error)

type AutoContinueInput

type AutoContinueInput struct {
	SessionID string
	Agent     string
	Model     Model
	Provider  ProviderInfo
	Message   msgmodel.User
	Overflow  bool
}

type CompactingResult

type CompactingResult struct {
	Context []string
	Prompt  *string
}

type CompactionDecision

type CompactionDecision struct {
	SessionID string  `json:"sessionID"`
	Status    string  `json:"status"`
	Before    float64 `json:"beforeTokens"`
	After     float64 `json:"afterTokens"`
	Capacity  float64 `json:"capacityTokens"`
	Low       float64 `json:"lowTokens"`
	High      float64 `json:"highTokens"`
	// DroppedTail: the watermark rebuild removed the verbatim tail.
	// StubbedSummary: it also had to replace the summary with the capacity
	// stub because the summary block alone did not fit.
	DroppedTail    bool `json:"droppedTail"`
	StubbedSummary bool `json:"stubbedSummary,omitempty"`
	// SummaryStatus is valid, normalized, fallback, no-head (nothing to
	// summarize, no call made), overflow (the summary request itself exceeded
	// the model), summary-error (the call failed), summary-stopped, or
	// capacity-fallback (the watermark rebuild replaced the record). Every
	// status except valid and normalized installs the deterministic record.
	SummaryStatus string `json:"summaryStatus"`
	SummaryClass  string `json:"summaryClass,omitempty"`
	SummaryError  string `json:"summaryError,omitempty"`

	TranscriptMessages  int     `json:"transcriptMessages"`
	TranscriptChars     int     `json:"transcriptChars"`
	TranscriptCutChars  float64 `json:"transcriptCutChars,omitempty"`
	PromptChars         int     `json:"promptChars"`
	SummaryPromptTokens uint64  `json:"summaryPromptTokens"`
	SummaryOutputTokens uint64  `json:"summaryOutputTokens"`
	SummaryWallMs       int64   `json:"summaryWallMs"`

	TailBudget             float64 `json:"tailBudget"`
	TailMessages           int     `json:"tailMessages"`
	TailTokens             float64 `json:"tailTokens"`
	TailTruncatedOutputs   int     `json:"tailTruncatedOutputs"`
	TailTruncatedReasoning int     `json:"tailTruncatedReasoning,omitempty"`
	// PreviousSummaryCarried is set when a deterministic record carried the
	// previous boundary's summary forward verbatim.
	PreviousSummaryCarried bool `json:"previousSummaryCarried,omitempty"`
}

CompactionDecision is the one record a boundary leaves behind: what the summarizer was handed (transcript and prompt sizes), what the provider reported back (prompt and output tokens), how the summary was judged, and how much verbatim tail was kept.

type CompletedCompaction

type CompletedCompaction struct {
	UserIndex      int
	AssistantIndex int
	Summary        *string
}

type ConfigProvider

type ConfigProvider interface {
	GetConfig(ctx context.Context) (overflow.Config, error)
}

type ConfigProviderFunc

type ConfigProviderFunc func(ctx context.Context) (overflow.Config, error)

func (ConfigProviderFunc) GetConfig

func (f ConfigProviderFunc) GetConfig(ctx context.Context) (overflow.Config, error)

type ContextCapacityError

type ContextCapacityError struct {
	After float64
	High  float64
}

func (ContextCapacityError) Error

func (err ContextCapacityError) Error() string

func (ContextCapacityError) Unwrap

func (ContextCapacityError) Unwrap() error

type ContextSizer

type ContextSizer interface {
	EstimateContext(ctx context.Context, messages []msgmodel.WithParts, model Model) (float64, error)
}

ContextSizer measures the complete model-visible request represented by a projected message list. The senior-dev adapter includes its system prompt and tool schemas; the default service implementation measures messages alone.

type ContextSizerFunc

type ContextSizerFunc func(
	ctx context.Context, messages []msgmodel.WithParts, model Model,
) (float64, error)

func (ContextSizerFunc) EstimateContext

func (f ContextSizerFunc) EstimateContext(
	ctx context.Context, messages []msgmodel.WithParts, model Model,
) (float64, error)

type Controller

type Controller struct {
	Compaction *Service
}

func (Controller) CreateCompaction

func (c Controller) CreateCompaction(
	ctx context.Context,
	sessionID string,
	user msgmodel.User,
	overflowed bool,
) error

func (Controller) IsOverflow

func (c Controller) IsOverflow(
	ctx context.Context,
	assistant msgmodel.Assistant,
	model steploop.Model,
) (bool, error)

func (Controller) ProcessCompaction

func (c Controller) ProcessCompaction(
	ctx context.Context,
	input steploop.TaskInput,
	task msgmodel.CompactionPart,
) (steploop.Result, error)

func (Controller) Prune

func (c Controller) Prune(ctx context.Context, sessionID string) error

type CreateInput

type CreateInput struct {
	SessionID string
	Agent     string
	Model     ModelRef
	Auto      bool
	Overflow  *bool
}

type DecisionSink

type DecisionSink interface {
	CompactionDecision(decision CompactionDecision)
}

type DecisionSinkFunc

type DecisionSinkFunc func(decision CompactionDecision)

func (DecisionSinkFunc) CompactionDecision

func (f DecisionSinkFunc) CompactionDecision(decision CompactionDecision)

type Dependencies

type Dependencies struct {
	Store      steploop.Store
	Config     ConfigProvider
	Agents     AgentProvider
	Provider   ModelProvider
	Plugin     Plugin
	Processors ProcessorFactory
	Evidence   EvidenceSelector
	Sizer      ContextSizer
	Decisions  DecisionSink
	Events     EventSink
	Instance   InstanceContext
	// ChangedFiles reports the workspace's changed files as preformatted
	// lines, computed by code (a diffstat against the starting tree plus the
	// status). It is pinned beside every summary as a record the model cannot
	// misremember; nil disables the pin.
	ChangedFiles func(ctx context.Context) []string

	NewID func(prefix string) string
	Now   func() uint64
}

type EstimateFunc

type EstimateFunc func(messages []msgmodel.WithParts, model Model) (float64, error)

type EventSink

type EventSink interface {
	CompactionStarted(sessionID string, timestamp uint64, reason string)
	CompactionEnded(sessionID string, timestamp uint64, text string, include *string)
	PublishCompacted(ctx context.Context, sessionID string) error
}

type EvidenceSelector

type EvidenceSelector interface {
	SelectEvidence(ctx context.Context, blocks []string) (*string, error)
}

type EvidenceSelectorFunc

type EvidenceSelectorFunc func(ctx context.Context, blocks []string) (*string, error)

func (EvidenceSelectorFunc) SelectEvidence

func (f EvidenceSelectorFunc) SelectEvidence(ctx context.Context, blocks []string) (*string, error)

type FallbackEvidenceSelector

type FallbackEvidenceSelector struct {
	MaxChars float64
}

FallbackEvidenceSelector exposes the deterministic evidence harvest through the compaction service's evidence seam.

func (FallbackEvidenceSelector) SelectEvidence

func (selector FallbackEvidenceSelector) SelectEvidence(
	_ context.Context, blocks []string,
) (*string, error)

type InstanceContext

type InstanceContext struct {
	Directory string
	Worktree  string
}

type Model

type Model struct {
	Message  msgmodel.Model
	Overflow overflow.Model
}

type ModelProvider

type ModelProvider interface {
	GetModel(ctx context.Context, providerID, modelID string) (Model, error)
	GetProvider(ctx context.Context, providerID string) (ProviderInfo, error)
}

type ModelRef

type ModelRef struct {
	ProviderID string
	ModelID    string
}

type OverflowHistory

type OverflowHistory struct {
	Messages []msgmodel.WithParts
	Replay   *Replay
}

type Plugin

type Plugin interface {
	Compacting(ctx context.Context, sessionID string) (CompactingResult, error)
	TransformMessages(ctx context.Context, messages []msgmodel.WithParts) error
	AutoContinue(ctx context.Context, input AutoContinueInput) (bool, error)
}

type ProcessInput

type ProcessInput struct {
	ParentID  string
	Messages  []msgmodel.WithParts
	SessionID string
	Auto      bool
	Overflow  *bool
}

type ProcessorFactory

type ProcessorFactory interface {
	Create(
		ctx context.Context,
		assistant *msgmodel.Assistant,
		sessionID string,
		model Model,
	) (SummaryProcessor, error)
}

type ProcessorFactoryFunc

type ProcessorFactoryFunc func(
	ctx context.Context,
	assistant *msgmodel.Assistant,
	sessionID string,
	model Model,
) (SummaryProcessor, error)

func (ProcessorFactoryFunc) Create

func (f ProcessorFactoryFunc) Create(
	ctx context.Context,
	assistant *msgmodel.Assistant,
	sessionID string,
	model Model,
) (SummaryProcessor, error)

type ProviderInfo

type ProviderInfo struct {
	Source  string
	Options any
}

type Replay

type Replay struct {
	Info  msgmodel.User
	Parts msgmodel.Parts
}

type Service

type Service struct {
	// contains filtered or unexported fields
}

func NewService

func NewService(deps Dependencies) *Service

func (*Service) Create

func (s *Service) Create(ctx context.Context, input CreateInput) error

func (*Service) Estimate

func (s *Service) Estimate(
	messages []msgmodel.WithParts, model Model,
) (float64, error)

func (*Service) IsOverflow

func (s *Service) IsOverflow(
	ctx context.Context, tokens msgmodel.Tokens, model Model,
) (bool, error)

func (*Service) Process

func (s *Service) Process(ctx context.Context, input ProcessInput) (steploop.Result, error)

func (*Service) Prune

func (s *Service) Prune(ctx context.Context, sessionID string) error

type SummaryProcessor

type SummaryProcessor interface {
	Process(ctx context.Context, request SummaryRequest) (steploop.Result, error)
	Message() msgmodel.Assistant
}

type SummaryRequest

type SummaryRequest struct {
	User      msgmodel.User
	Agent     Agent
	SessionID string
	Messages  []msgmodel.ModelMessage
	Model     Model
}

type Turn

type Turn struct {
	Start int    `json:"start"`
	End   int    `json:"end"`
	ID    string `json:"id"`
}

func Turns

func Turns(messages []msgmodel.WithParts) []Turn

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL