Skip to content

Commit c72fbd1

Browse files
committed
feat(extended-memory): wire memory LLM resolution and MemoryManager integration
1 parent 2029187 commit c72fbd1

2 files changed

Lines changed: 116 additions & 0 deletions

File tree

internal/memory/memory.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"sync"
1010
"time"
1111

12+
"github.com/BackendStack21/odek/internal/memory/extended"
1213
"github.com/BackendStack21/odek/internal/session"
1314
)
1415

@@ -102,6 +103,12 @@ type MemoryConfig struct {
102103
// See EmbeddingConfig for the "http" provider that enables real
103104
// semantic similarity via any OpenAI-compatible embeddings API.
104105
Embedding *EmbeddingConfig `json:"embedding,omitempty"`
106+
107+
// Extended configures the Extended Memory subsystem (atomic facts/
108+
// observations extracted from user messages and recalled via semantic
109+
// search). nil means "use subsystem defaults"; the subsystem is opt-in
110+
// and invisible when Extended.Enabled is false.
111+
Extended *extended.Config `json:"extended,omitempty"`
105112
}
106113

107114
// BoolPtr returns a pointer to a bool value.
@@ -131,6 +138,7 @@ func DefaultMemoryConfig() MemoryConfig {
131138
EpisodeDedupThreshold: defaultEpisodeDedupThreshold,
132139
MaxEpisodes: defaultMaxEpisodes,
133140
EpisodeTTLDays: 0, // TTL disabled by default
141+
Extended: nil,
134142
}
135143
}
136144

@@ -150,6 +158,13 @@ type MemoryManager struct {
150158
merge *MergeDetector
151159
llm LLMClient
152160
cfg MemoryConfig
161+
extended *extended.ExtendedMemory
162+
163+
// extendedContext caches the current session context for user-message
164+
// extraction callbacks that arrive without explicit provenance.
165+
extSessionID string
166+
extProject string
167+
extTurn int
153168

154169
// notifier receives memory lifecycle events (facts + episodes). Defaults to
155170
// a NoopMemoryNotifier so the fire path is always safe without a nil check.
@@ -230,6 +245,9 @@ func NewMemoryManager(memoryDir string, llc LLMClient, cfg MemoryConfig) *Memory
230245
if cfg.Embedding != nil {
231246
def.Embedding = cfg.Embedding
232247
}
248+
if cfg.Extended != nil {
249+
def.Extended = cfg.Extended
250+
}
233251
cfg = def
234252

235253
factsDir := memoryDir
@@ -281,6 +299,76 @@ func (m *MemoryManager) SetNotifier(n MemoryNotifier) {
281299
}
282300
}
283301

302+
// InitExtended creates the Extended Memory subsystem using the provided
303+
// dedicated memory LLM client. It is safe to call multiple times; subsequent
304+
// calls are ignored. Callers should invoke this after NewMemoryManager once
305+
// the memory LLM has been resolved.
306+
func (m *MemoryManager) InitExtended(memoryLLM extended.LLMClient, memoryDir string) {
307+
if m.extended != nil {
308+
return
309+
}
310+
if m.cfg.Extended == nil {
311+
m.cfg.Extended = &extended.Config{}
312+
}
313+
cfg := extended.Resolve(*m.cfg.Extended)
314+
if cfg.Embedding == nil && m.cfg.Embedding != nil {
315+
cfg.Embedding = m.cfg.Embedding
316+
}
317+
if memoryDir == "" {
318+
memoryDir = m.facts.dir
319+
}
320+
extDir := filepath.Join(memoryDir, "extended")
321+
m.extended = extended.New(extDir, memoryLLM, cfg)
322+
}
323+
324+
// OnUserMessage routes a user message to Extended Memory for atom extraction.
325+
func (m *MemoryManager) OnUserMessage(ctx extended.AtomContext, msg string) {
326+
if m.extended == nil {
327+
return
328+
}
329+
m.extended.OnUserMessage(ctx, msg)
330+
}
331+
332+
// FormatExtendedContext returns ranked Extended Memory context for the query.
333+
func (m *MemoryManager) FormatExtendedContext(query string) string {
334+
if m.extended == nil {
335+
return ""
336+
}
337+
return m.extended.FormatExtendedContext(query)
338+
}
339+
340+
// SetSessionContext propagates session/project identifiers to all memory tiers
341+
// that need them (currently Extended Memory).
342+
func (m *MemoryManager) SetSessionContext(sessionID, project string) {
343+
m.extSessionID = sessionID
344+
m.extProject = project
345+
if m.extended != nil {
346+
m.extended.SetSessionContext(sessionID, project)
347+
}
348+
}
349+
350+
// OnUserMessageLoop routes a user message to Extended Memory using the
351+
// session context previously set via SetSessionContext. It is the callback
352+
// used by the agent loop to trigger atom extraction when a new user message
353+
// arrives.
354+
func (m *MemoryManager) OnUserMessageLoop(msg string) {
355+
if m.extended == nil {
356+
return
357+
}
358+
m.extTurn++
359+
ctx := extended.AtomContext{
360+
SessionID: m.extSessionID,
361+
Project: m.extProject,
362+
Turn: m.extTurn,
363+
}
364+
m.extended.OnUserMessage(ctx, msg)
365+
}
366+
367+
// Extended returns the Extended Memory subsystem, or nil if not initialized.
368+
func (m *MemoryManager) Extended() *extended.ExtendedMemory {
369+
return m.extended
370+
}
371+
284372
// notify fires an event on the configured notifier, stamping the UTC timestamp
285373
// when the caller left it zero. Safe even before SetNotifier (nil → no-op).
286374
func (m *MemoryManager) notify(ev MemoryEvent) {

odek.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/BackendStack21/odek/internal/llm"
3232
"github.com/BackendStack21/odek/internal/loop"
3333
"github.com/BackendStack21/odek/internal/memory"
34+
"github.com/BackendStack21/odek/internal/memory/extended"
3435
"github.com/BackendStack21/odek/internal/narrate"
3536
"github.com/BackendStack21/odek/internal/render"
3637
"github.com/BackendStack21/odek/internal/skills"
@@ -519,6 +520,15 @@ func New(cfg Config) (*Agent, error) {
519520
}
520521
memoryManager := memory.NewMemoryManager(memoryDir, client, cfg.MemoryConfig)
521522

523+
// Resolve a dedicated LLM for Extended Memory. Falls back to the main agent
524+
// LLM when not configured; warns if the main model has thinking enabled
525+
// because reasoning tokens are wasted on memory-only calls.
526+
var memoryLLM extended.LLMClient = client
527+
if cfg.MemoryConfig.Extended != nil {
528+
memoryLLM = extended.ResolveLLM(*cfg.MemoryConfig.Extended, client, cfg.Thinking)
529+
}
530+
memoryManager.InitExtended(memoryLLM, memoryDir)
531+
522532
// Wire memory lifecycle observability: fan out events to the programmatic
523533
// handler (WebUI/Telegram/embedders) and the terminal renderer. Mirrors the
524534
// skills notifier pattern so memory activity is no longer silent.
@@ -650,6 +660,24 @@ func New(cfg Config) (*Agent, error) {
650660
return memoryManager.FormatEpisodeContext(userInput)
651661
})
652662

663+
// Wire per-turn Extended Memory search. Injected after the legacy memory
664+
// prompt block so recent facts/buffer take precedence.
665+
engine.SetExtendedMemoryContextFunc(func(userInput string) string {
666+
return memoryManager.FormatExtendedContext(userInput)
667+
})
668+
669+
// Notify memory manager when a new user message arrives so Extended Memory
670+
// can extract atomic facts/preferences.
671+
engine.SetUserMessageHandler(func(msg string) {
672+
memoryManager.OnUserMessageLoop(msg)
673+
})
674+
675+
// Wire per-turn Extended Memory search. Injected after the legacy memory
676+
// prompt block so recent facts/buffer take precedence.
677+
engine.SetExtendedMemoryContextFunc(func(userInput string) string {
678+
return memoryManager.FormatExtendedContext(userInput)
679+
})
680+
653681
agent.engine = engine
654682
agent.registry = registry
655683
agent.sandboxCleanup = cfg.SandboxCleanup

0 commit comments

Comments
 (0)