Skip to content

Commit a05d972

Browse files
committed
feat: configurable LLM features + thresholds for memory
MemoryConfig gains: - llm_search (default true): LLM-based episode ranking - llm_extract (default true): LLM-based fact extraction - llm_consolidate (default true): LLM-based fact consolidation - merge_threshold (default 0.7): go-vector cosine for auto-merge - add_threshold (default 0.3): go-vector cosine for auto-add MergeDetector supports per-instance thresholds via NewMergeDetectorWithThresholds().
1 parent d7a69e8 commit a05d972

3 files changed

Lines changed: 70 additions & 22 deletions

File tree

docs/CONFIG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,12 @@ The `memory` section controls the persistent memory system (see [docs/MEMORY.md]
123123
"buffer_lines": 20,
124124
"buffer_enabled": true,
125125
"merge_on_write": true,
126-
"extract_on_end": true
126+
"extract_on_end": true,
127+
"llm_search": true,
128+
"llm_extract": true,
129+
"llm_consolidate": true,
130+
"merge_threshold": 0.7,
131+
"add_threshold": 0.3
127132
}
128133
}
129134
```
@@ -137,6 +142,11 @@ The `memory` section controls the persistent memory system (see [docs/MEMORY.md]
137142
| `buffer_enabled` | true | Enable the turn-level buffer |
138143
| `merge_on_write` | true | Use go-vector RP similarity to auto-merge related entries |
139144
| `extract_on_end` | true | Extract durable facts via LLM at session end (≥3 turns) |
145+
| `llm_search` | true | Use LLM to rank episode search results by relevance |
146+
| `llm_extract` | true | Use LLM for end-of-session fact extraction |
147+
| `llm_consolidate` | true | Use LLM to merge related fact entries |
148+
| `merge_threshold` | 0.7 | go-vector cosine threshold for auto-merge (0.0–1.0) |
149+
| `add_threshold` | 0.3 | go-vector cosine threshold for auto-add (0.0–1.0) |
140150

141151
## Sub-agent configuration
142152

internal/memory/memory.go

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@ const defaultBufferLines = 20
1515
// MemoryConfig holds configuration for the memory system.
1616
// Mirrors the JSON config section.
1717
type MemoryConfig struct {
18-
Enabled bool `json:"enabled"`
19-
FactsLimitUser int `json:"facts_limit_user"`
20-
FactsLimitEnv int `json:"facts_limit_env"`
21-
BufferLines int `json:"buffer_lines"`
22-
BufferEnabled bool `json:"buffer_enabled"`
23-
MergeOnWrite bool `json:"merge_on_write"`
24-
ExtractOnEnd bool `json:"extract_on_end"`
18+
Enabled bool `json:"enabled"`
19+
FactsLimitUser int `json:"facts_limit_user"`
20+
FactsLimitEnv int `json:"facts_limit_env"`
21+
BufferLines int `json:"buffer_lines"`
22+
BufferEnabled bool `json:"buffer_enabled"`
23+
MergeOnWrite bool `json:"merge_on_write"`
24+
ExtractOnEnd bool `json:"extract_on_end"`
25+
LLMSearch bool `json:"llm_search"`
26+
LLMExtract bool `json:"llm_extract"`
27+
LLMConsolidate bool `json:"llm_consolidate"`
28+
MergeThreshold float32 `json:"merge_threshold"`
29+
AddThreshold float32 `json:"add_threshold"`
2530
}
2631

2732
// DefaultMemoryConfig returns sensible defaults.
@@ -34,6 +39,11 @@ func DefaultMemoryConfig() MemoryConfig {
3439
BufferEnabled: true,
3540
MergeOnWrite: true,
3641
ExtractOnEnd: true,
42+
LLMSearch: true,
43+
LLMExtract: true,
44+
LLMConsolidate: true,
45+
MergeThreshold: MergeThreshold,
46+
AddThreshold: AddThreshold,
3747
}
3848
}
3949

@@ -68,18 +78,24 @@ func NewMemoryManager(memoryDir string, llc LLMClient, cfg MemoryConfig) *Memory
6878
if cfg.BufferLines <= 0 {
6979
cfg.BufferLines = defaultBufferLines
7080
}
81+
if cfg.MergeThreshold <= 0 {
82+
cfg.MergeThreshold = MergeThreshold
83+
}
84+
if cfg.AddThreshold <= 0 {
85+
cfg.AddThreshold = AddThreshold
86+
}
7187

7288
factsDir := memoryDir
7389
episodesDir := memoryDir
7490

7591
factStore := NewFactStore(factsDir, cfg.FactsLimitUser, cfg.FactsLimitEnv)
76-
// Use LLM-based episode ranker when an LLM client is available
92+
// Use LLM-based episode ranker when an LLM client is available and enabled
7793
var rankFn RankStrategy
78-
if llc != nil {
94+
if llc != nil && cfg.LLMSearch {
7995
rankFn = NewLLMRanker(llc)
8096
}
8197
episodeStore := NewEpisodeStore(episodesDir, rankFn)
82-
mergeDetector := NewMergeDetector(0) // default 256 dims
98+
mergeDetector := NewMergeDetectorWithThresholds(0, cfg.MergeThreshold, cfg.AddThreshold)
8399

84100
return &MemoryManager{
85101
facts: factStore,
@@ -206,12 +222,13 @@ func (m *MemoryManager) ReadFacts() (userContent, envContent string, err error)
206222
}
207223

208224
// Consolidate uses the LLM to merge related entries in a target file
209-
// for better density. Falls back to no-op if no LLM is available.
225+
// for better density. Falls back to no-op if LLM is unavailable or
226+
// LLMConsolidate is disabled in config.
210227
func (m *MemoryManager) Consolidate(target string) error {
211228
if !m.cfg.Enabled {
212229
return fmt.Errorf("memory: disabled")
213230
}
214-
if m.llm == nil {
231+
if m.llm == nil || !m.cfg.LLMConsolidate {
215232
return fmt.Errorf("memory: consolidation requires LLM client")
216233
}
217234

@@ -288,7 +305,7 @@ func (m *MemoryManager) ClearBuffer() {
288305
// OnSessionEnd is called when a session ends. If turns >= threshold,
289306
// extracts durable facts using the LLM and stores them as an episode.
290307
func (m *MemoryManager) OnSessionEnd(sessionID string, turns int, messages []string) {
291-
if !m.cfg.ExtractOnEnd || m.llm == nil || turns < 3 || len(messages) == 0 {
308+
if !m.cfg.ExtractOnEnd || !m.cfg.LLMExtract || m.llm == nil || turns < 3 || len(messages) == 0 {
292309
return
293310
}
294311

internal/memory/merge.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,43 @@ const (
2929
// 2. Fit(corpus) — builds vocabulary from existing entries
3030
// 3. Classify(entry) → action + similarIdx + similarity
3131
// 4. After facts change → Fit(newCorpus) to rebuild vocabulary
32+
//
33+
// Thresholds control the classification:
34+
// - mergeThreshold: cosine above this → auto-merge (default 0.7)
35+
// - addThreshold: cosine below this → auto-add (default 0.3)
36+
// - Between thresholds: "judge" — requires LLM to decide
3237
type MergeDetector struct {
33-
rp *vector.RandomProjections
34-
corpus []string
35-
vecs []vector.Vector // precomputed embeddings of corpus
36-
dims int
38+
rp *vector.RandomProjections
39+
corpus []string
40+
vecs []vector.Vector // precomputed embeddings of corpus
41+
dims int
42+
mergeThreshold float32
43+
addThreshold float32
3744
}
3845

3946
// NewMergeDetector creates a MergeDetector with the given output
4047
// dimensionality for the RP embedder. Pass 0 for default (256).
48+
// Uses default thresholds (0.7 merge, 0.3 add).
4149
func NewMergeDetector(dims int) *MergeDetector {
50+
return NewMergeDetectorWithThresholds(dims, MergeThreshold, AddThreshold)
51+
}
52+
53+
// NewMergeDetectorWithThresholds creates a MergeDetector with custom thresholds.
54+
func NewMergeDetectorWithThresholds(dims int, mergeThreshold, addThreshold float32) *MergeDetector {
4255
if dims <= 0 {
4356
dims = defaultOutputDim
4457
}
58+
if mergeThreshold <= 0 {
59+
mergeThreshold = MergeThreshold
60+
}
61+
if addThreshold <= 0 || addThreshold >= mergeThreshold {
62+
addThreshold = AddThreshold
63+
}
4564
return &MergeDetector{
46-
rp: vector.NewRandomProjections(dims),
47-
dims: dims,
65+
rp: vector.NewRandomProjections(dims),
66+
dims: dims,
67+
mergeThreshold: mergeThreshold,
68+
addThreshold: addThreshold,
4869
}
4970
}
5071

@@ -111,9 +132,9 @@ func (m *MergeDetector) Classify(entry string) (action string, similarIdx int, s
111132
similarIdx = bestIdx
112133

113134
switch {
114-
case bestSim >= MergeThreshold:
135+
case bestSim >= m.mergeThreshold:
115136
return "merge", bestIdx, bestSim
116-
case bestSim <= AddThreshold:
137+
case bestSim <= m.addThreshold:
117138
return "add", bestIdx, bestSim
118139
default:
119140
return "judge", bestIdx, bestSim

0 commit comments

Comments
 (0)