first commit
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
// Memory/checkpoint/compaction config: the nested TOML tables [memory],
|
||||
// [checkpoint], and compaction.max_context (spec-persistent-memory-infinite-
|
||||
// context §3/§4/§5.2). This file is pure config plumbing — parse, defaults, and
|
||||
// resolve helpers only. The actual memory Store, checkpoint persistence, and
|
||||
// compaction trigger live in later layers (internal/memory, internal/runtime,
|
||||
// internal/compaction) and consume the resolved values overlaid in cmd/pigo.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Built-in defaults for the [memory] table. Exposed so overlay and tests share
|
||||
// one source of truth (mirrors the flat-config defaults documented in main.go).
|
||||
const (
|
||||
DefaultMemoryEnabled = true
|
||||
DefaultMemoryReconcileOnSearch = true
|
||||
DefaultMemorySearchScoreFloor = 0.15
|
||||
DefaultMemoryCCIndex = false
|
||||
)
|
||||
|
||||
// DefaultCheckpointThresholds is the built-in compaction trigger ladder as
|
||||
// percentage strings; ResolveThresholds parses them into fractions in (0,1].
|
||||
func DefaultCheckpointThresholds() []string {
|
||||
return []string{"40%", "60%", "80%"}
|
||||
}
|
||||
|
||||
// MemoryConfig is the [memory] TOML table. Bool fields whose default is true
|
||||
// (Enabled, ReconcileOnSearch) and the float ScoreFloor use pointers so an
|
||||
// absent key is distinguishable from an explicit false/0 — nil means "apply the
|
||||
// default", which is what makes memory.enabled=false representable and
|
||||
// default-safe. CCIndex defaults to false, so a plain bool suffices.
|
||||
type MemoryConfig struct {
|
||||
Enabled *bool `toml:"enabled"`
|
||||
ReconcileOnSearch *bool `toml:"reconcile_on_search"`
|
||||
SearchScoreFloor *float64 `toml:"search_score_floor"`
|
||||
CCIndex bool `toml:"cc_index"`
|
||||
}
|
||||
|
||||
// ResolvedMemory is MemoryConfig with defaults applied and the score floor
|
||||
// clamped to [0,1]. It is the shape downstream memory wiring consumes.
|
||||
type ResolvedMemory struct {
|
||||
Enabled bool
|
||||
ReconcileOnSearch bool
|
||||
SearchScoreFloor float64
|
||||
CCIndex bool
|
||||
}
|
||||
|
||||
// Resolve applies the [memory] defaults: absent keys fall back to
|
||||
// true/true/0.15/false; an explicit search_score_floor outside [0,1] is clamped
|
||||
// into range.
|
||||
func (m MemoryConfig) Resolve() ResolvedMemory {
|
||||
r := ResolvedMemory{
|
||||
Enabled: DefaultMemoryEnabled,
|
||||
ReconcileOnSearch: DefaultMemoryReconcileOnSearch,
|
||||
SearchScoreFloor: DefaultMemorySearchScoreFloor,
|
||||
CCIndex: m.CCIndex,
|
||||
}
|
||||
if m.Enabled != nil {
|
||||
r.Enabled = *m.Enabled
|
||||
}
|
||||
if m.ReconcileOnSearch != nil {
|
||||
r.ReconcileOnSearch = *m.ReconcileOnSearch
|
||||
}
|
||||
if m.SearchScoreFloor != nil {
|
||||
f := *m.SearchScoreFloor
|
||||
switch {
|
||||
case f < 0:
|
||||
f = 0
|
||||
case f > 1:
|
||||
f = 1
|
||||
}
|
||||
r.SearchScoreFloor = f
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// IntOrString accepts either a TOML integer or string for the optional
|
||||
// [checkpoint].reserved key (e.g. reserved = 4096 or reserved = "10%"). Set
|
||||
// reports whether the key was present; IsInt selects the populated field.
|
||||
type IntOrString struct {
|
||||
Set bool
|
||||
IsInt bool
|
||||
Int int
|
||||
Str string
|
||||
}
|
||||
|
||||
// UnmarshalTOML implements toml.Unmarshaler so a bare int or a quoted string
|
||||
// both decode without failing the whole file.
|
||||
func (v *IntOrString) UnmarshalTOML(data any) error {
|
||||
v.Set = true
|
||||
switch t := data.(type) {
|
||||
case int64:
|
||||
v.IsInt, v.Int = true, int(t)
|
||||
case int:
|
||||
v.IsInt, v.Int = true, t
|
||||
case float64:
|
||||
v.IsInt, v.Int = true, int(t)
|
||||
case string:
|
||||
v.Str = t
|
||||
default:
|
||||
return fmt.Errorf("reserved: unsupported type %T (want int or string)", data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckpointConfig is the [checkpoint] TOML table. push_caps is a nested table
|
||||
// of per-section token caps (e.g. [checkpoint.push_caps] with memory = 800,
|
||||
// recall = 1200), modeled as a map.
|
||||
type CheckpointConfig struct {
|
||||
Thresholds []string `toml:"thresholds"`
|
||||
Reserved IntOrString `toml:"reserved"`
|
||||
PushCaps map[string]int `toml:"push_caps"`
|
||||
}
|
||||
|
||||
// ResolveThresholds parses the configured threshold percentage strings into
|
||||
// fractions in (0,1], skipping out-of-range/unparseable entries. An empty list
|
||||
// — or one where every entry is invalid — falls back to the built-in defaults.
|
||||
func (c CheckpointConfig) ResolveThresholds() []float64 {
|
||||
src := c.Thresholds
|
||||
if len(src) == 0 {
|
||||
src = DefaultCheckpointThresholds()
|
||||
}
|
||||
out := parseThresholds(src)
|
||||
if len(out) == 0 {
|
||||
out = parseThresholds(DefaultCheckpointThresholds())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseThresholds(ss []string) []float64 {
|
||||
var out []float64
|
||||
for _, s := range ss {
|
||||
if f, ok := ParseThresholdFraction(s); ok {
|
||||
out = append(out, f)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ParseThresholdFraction parses a percentage string like "80%" into a fraction
|
||||
// in (0,1]. Values outside that range (<=0, >100%) or lacking a % suffix are
|
||||
// rejected with ok=false so callers fall back to a default.
|
||||
func ParseThresholdFraction(s string) (float64, bool) {
|
||||
s = strings.TrimSpace(s)
|
||||
if !strings.HasSuffix(s, "%") {
|
||||
return 0, false
|
||||
}
|
||||
n, err := strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(s, "%")), 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
f := n / 100
|
||||
if f <= 0 || f > 1 {
|
||||
return 0, false
|
||||
}
|
||||
return f, true
|
||||
}
|
||||
|
||||
// CompactionConfig is the [compaction] TOML table. Only max_context is wired
|
||||
// here; it lowers the auto-compaction trigger point and is always clamped by
|
||||
// the provider window by the consumer.
|
||||
type CompactionConfig struct {
|
||||
MaxContext string `toml:"max_context"`
|
||||
}
|
||||
|
||||
// ResolveMaxContext parses the max_context string form. An empty value yields
|
||||
// an unset MaxContext (no error).
|
||||
func (c CompactionConfig) ResolveMaxContext() (MaxContext, error) {
|
||||
return ParseMaxContext(c.MaxContext)
|
||||
}
|
||||
|
||||
// MaxContext is a parsed compaction.max_context value: either an absolute token
|
||||
// count or a fraction of the provider window. The zero value is "unset" and
|
||||
// Resolve returns 0. Resolve is a pure function; the provider-limit clamp is
|
||||
// applied by the consumer.
|
||||
type MaxContext struct {
|
||||
set bool
|
||||
fraction float64 // >0 for a "N%" form
|
||||
tokens int // absolute token count when fraction == 0
|
||||
}
|
||||
|
||||
// IsSet reports whether max_context was configured.
|
||||
func (m MaxContext) IsSet() bool { return m.set }
|
||||
|
||||
// Resolve returns the token budget for the given provider window: window*
|
||||
// fraction (rounded) for a percentage form, or the absolute token count
|
||||
// otherwise. An unset value returns 0. This is intentionally unclamped — the
|
||||
// consumer applies the provider-limit clamp.
|
||||
func (m MaxContext) Resolve(window int) int {
|
||||
if !m.set {
|
||||
return 0
|
||||
}
|
||||
if m.fraction > 0 {
|
||||
return int(float64(window)*m.fraction + 0.5)
|
||||
}
|
||||
return m.tokens
|
||||
}
|
||||
|
||||
// ParseMaxContext parses the accepted max_context forms: a plain token count
|
||||
// ("300000"), a K/M-suffixed count ("300K", "1M", case-insensitive, fractions
|
||||
// allowed like "1.5M"), or a percentage of the provider window ("50%"). An
|
||||
// empty string is unset (no error); other malformed or non-positive values are
|
||||
// errors.
|
||||
func ParseMaxContext(s string) (MaxContext, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return MaxContext{}, nil
|
||||
}
|
||||
if strings.HasSuffix(s, "%") {
|
||||
n, err := strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(s, "%")), 64)
|
||||
if err != nil {
|
||||
return MaxContext{}, fmt.Errorf("max_context: invalid percent %q: %w", s, err)
|
||||
}
|
||||
f := n / 100
|
||||
if f <= 0 || f > 1 {
|
||||
return MaxContext{}, fmt.Errorf("max_context: percent out of range %q (want (0%%,100%%])", s)
|
||||
}
|
||||
return MaxContext{set: true, fraction: f}, nil
|
||||
}
|
||||
mult := 1.0
|
||||
body := s
|
||||
switch last := s[len(s)-1]; last {
|
||||
case 'k', 'K':
|
||||
mult, body = 1_000, s[:len(s)-1]
|
||||
case 'm', 'M':
|
||||
mult, body = 1_000_000, s[:len(s)-1]
|
||||
}
|
||||
n, err := strconv.ParseFloat(strings.TrimSpace(body), 64)
|
||||
if err != nil {
|
||||
return MaxContext{}, fmt.Errorf("max_context: invalid token count %q: %w", s, err)
|
||||
}
|
||||
if n <= 0 {
|
||||
return MaxContext{}, fmt.Errorf("max_context: must be positive %q", s)
|
||||
}
|
||||
return MaxContext{set: true, tokens: int(n*mult + 0.5)}, nil
|
||||
}
|
||||
|
||||
// MemorySettings bundles the resolved [memory]/[checkpoint]/[compaction] config
|
||||
// for overlay into runtime options: defaults applied, string forms pre-parsed.
|
||||
// It is produced by FileConfig.ResolveMemorySettings and is always well-formed
|
||||
// (an invalid max_context is treated as unset rather than failing the overlay).
|
||||
type MemorySettings struct {
|
||||
Memory ResolvedMemory
|
||||
CheckpointThresholds []float64
|
||||
CheckpointReserved IntOrString
|
||||
CheckpointPushCaps map[string]int
|
||||
MaxContext MaxContext
|
||||
}
|
||||
|
||||
// ResolveMemorySettings resolves the three nested tables into MemorySettings,
|
||||
// applying defaults. It never fails: an unparseable compaction.max_context is
|
||||
// dropped to unset so a single bad key cannot break config overlay.
|
||||
func (c FileConfig) ResolveMemorySettings() MemorySettings {
|
||||
mc, _ := c.Compaction.ResolveMaxContext()
|
||||
return MemorySettings{
|
||||
Memory: c.Memory.Resolve(),
|
||||
CheckpointThresholds: c.Checkpoint.ResolveThresholds(),
|
||||
CheckpointReserved: c.Checkpoint.Reserved,
|
||||
CheckpointPushCaps: c.Checkpoint.PushCaps,
|
||||
MaxContext: mc,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user