recursive-llm-ts 4.9.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/go/rlm/rlm.go CHANGED
@@ -21,6 +21,7 @@ type RLM struct {
21
21
  observer *Observer
22
22
  metaAgent *MetaAgent
23
23
  contextOverflow *ContextOverflowConfig
24
+ lcmEngine *LCMEngine // Lossless Context Management engine (optional)
24
25
  }
25
26
 
26
27
  func New(model string, config Config) *RLM {
@@ -37,6 +38,9 @@ func New(model string, config Config) *RLM {
37
38
  obs = NewNoopObserver()
38
39
  }
39
40
 
41
+ // Configure tokenizer for accurate token counting with this model
42
+ SetDefaultTokenizer(model)
43
+
40
44
  r := &RLM{
41
45
  model: model,
42
46
  recursiveModel: recursiveModel,
@@ -52,7 +56,6 @@ func New(model string, config Config) *RLM {
52
56
  stats: RLMStats{},
53
57
  observer: obs,
54
58
  }
55
-
56
59
  // Setup meta-agent if enabled
57
60
  if config.MetaAgent != nil && config.MetaAgent.Enabled {
58
61
  r.metaAgent = NewMetaAgent(r, *config.MetaAgent, obs)
@@ -67,6 +70,19 @@ func New(model string, config Config) *RLM {
67
70
  r.contextOverflow = &defaultConfig
68
71
  }
69
72
 
73
+ // Setup LCM engine if enabled
74
+ if config.LCM != nil && config.LCM.Enabled {
75
+ store := NewLCMStore(fmt.Sprintf("session_%d", time.Now().UnixNano()))
76
+ summarizer := NewLCMSummarizer(model, config.APIBase, config.APIKey, config.TimeoutSeconds, config.ExtraParams, obs)
77
+ modelLimit := 0
78
+ if config.ContextOverflow != nil && config.ContextOverflow.MaxModelTokens > 0 {
79
+ modelLimit = config.ContextOverflow.MaxModelTokens
80
+ } else {
81
+ modelLimit = LookupModelTokenLimit(model)
82
+ }
83
+ r.lcmEngine = NewLCMEngine(*config.LCM, store, summarizer, obs, modelLimit)
84
+ }
85
+
70
86
  return r
71
87
  }
72
88
 
@@ -100,6 +116,13 @@ func (r *RLM) Completion(query string, context string) (string, RLMStats, error)
100
116
  r.stats.Depth = r.currentDepth
101
117
  replEnv := r.buildREPLEnv(query, context)
102
118
  systemPrompt := BuildSystemPrompt(len(context), r.currentDepth, query, r.useMetacognitive)
119
+
120
+ // ─── LCM-managed completion flow ────────────────────────────────────
121
+ if r.lcmEngine != nil && r.lcmEngine.IsEnabled() {
122
+ return r.completionWithLCM(query, systemPrompt, replEnv)
123
+ }
124
+
125
+ // ─── Legacy completion flow (no LCM) ────────────────────────────────
103
126
  messages := []Message{
104
127
  {Role: "system", Content: systemPrompt},
105
128
  {Role: "user", Content: query},
@@ -175,6 +198,89 @@ func (r *RLM) Completion(query string, context string) (string, RLMStats, error)
175
198
  return "", r.stats, NewMaxIterationsError(r.maxIterations)
176
199
  }
177
200
 
201
+ // completionWithLCM runs the completion loop using the LCM engine for context management.
202
+ // Messages flow through the LCM store: persisted verbatim in the immutable store,
203
+ // active context assembled from recent messages + summary nodes, and compaction
204
+ // triggered via the context control loop after each turn.
205
+ func (r *RLM) completionWithLCM(query string, systemPrompt string, replEnv map[string]interface{}) (string, RLMStats, error) {
206
+ store := r.lcmEngine.GetStore()
207
+
208
+ // Persist system prompt and initial query in the immutable store
209
+ store.PersistMessage(RoleSystem, systemPrompt, nil)
210
+ store.PersistMessage(RoleUser, query, nil)
211
+
212
+ r.observer.Debug("rlm.lcm", "Starting LCM-managed completion, initial tokens: %d",
213
+ store.ActiveContextTokens())
214
+
215
+ for iteration := 0; iteration < r.maxIterations; iteration++ {
216
+ r.stats.Iterations = iteration + 1
217
+ r.observer.Debug("rlm.lcm", "Iteration %d/%d at depth %d (active tokens: %d)",
218
+ iteration+1, r.maxIterations, r.currentDepth, store.ActiveContextTokens())
219
+
220
+ // Run the LCM context control loop (may trigger async or blocking compaction)
221
+ if err := r.lcmEngine.OnNewItem(); err != nil {
222
+ r.observer.Error("rlm.lcm", "Context control loop error: %v", err)
223
+ // Non-fatal: continue with current context
224
+ }
225
+
226
+ // Build messages from the active context (includes summaries with IDs)
227
+ messages := store.BuildMessages()
228
+
229
+ response, err := r.callLLM(messages)
230
+ if err != nil {
231
+ // Check for context overflow — LCM should handle this via compaction,
232
+ // but fall back to blocking compaction if the API still rejects
233
+ if r.contextOverflow != nil && r.contextOverflow.Enabled {
234
+ if _, isOverflow := IsContextOverflow(err); isOverflow {
235
+ r.observer.Debug("rlm.lcm", "Context overflow despite LCM, forcing blocking compaction")
236
+ if compactErr := r.lcmEngine.blockingCompaction(); compactErr != nil {
237
+ r.observer.Error("rlm.lcm", "Emergency compaction failed: %v", compactErr)
238
+ return "", r.stats, err
239
+ }
240
+ // Also try condensing old summaries to free more space
241
+ _ = r.lcmEngine.CondenseOldSummaries()
242
+ iteration-- // Retry
243
+ continue
244
+ }
245
+ }
246
+ r.observer.Error("rlm.lcm", "LLM call failed on iteration %d: %v", iteration+1, err)
247
+ return "", r.stats, err
248
+ }
249
+
250
+ // Persist assistant response in the immutable store
251
+ store.PersistMessage(RoleAssistant, response, nil)
252
+
253
+ if IsFinal(response) {
254
+ answer, ok := ParseResponse(response, replEnv)
255
+ if ok {
256
+ r.observer.Debug("rlm.lcm", "FINAL answer on iteration %d (store: %d msgs, %d summaries)",
257
+ iteration+1, store.MessageCount(), store.Stats().TotalSummaries)
258
+ r.observer.Event("rlm.lcm.completion_success", map[string]string{
259
+ "iterations": fmt.Sprintf("%d", iteration+1),
260
+ "llm_calls": fmt.Sprintf("%d", r.stats.LlmCalls),
261
+ "total_messages": fmt.Sprintf("%d", store.MessageCount()),
262
+ "total_summaries": fmt.Sprintf("%d", store.Stats().TotalSummaries),
263
+ "compression_ratio": fmt.Sprintf("%.2f", store.Stats().CompressionRatio),
264
+ })
265
+ return answer, r.stats, nil
266
+ }
267
+ }
268
+
269
+ execResult, err := r.repl.Execute(response, replEnv)
270
+ if err != nil {
271
+ r.observer.Debug("rlm.lcm", "REPL execution error: %v", err)
272
+ execResult = fmt.Sprintf("Error: %s", err.Error())
273
+ } else {
274
+ r.observer.Debug("rlm.lcm", "REPL output: %s", truncateStr(execResult, 200))
275
+ }
276
+
277
+ // Persist REPL result as user message in the immutable store
278
+ store.PersistMessage(RoleUser, execResult, nil)
279
+ }
280
+
281
+ return "", r.stats, NewMaxIterationsError(r.maxIterations)
282
+ }
283
+
178
284
  func (r *RLM) callLLM(messages []Message) (string, error) {
179
285
  r.stats.LlmCalls++
180
286
  defaultModel := r.model
@@ -302,6 +408,23 @@ func (r *RLM) GetObserver() *Observer {
302
408
  return r.observer
303
409
  }
304
410
 
411
+ // GetLCMEngine returns the LCM engine if enabled, nil otherwise.
412
+ func (r *RLM) GetLCMEngine() *LCMEngine {
413
+ return r.lcmEngine
414
+ }
415
+
416
+ // LLMMap executes an LLM-Map operation for parallel batch processing.
417
+ func (r *RLM) LLMMap(config LLMMapConfig) (*LLMMapResult, error) {
418
+ mapper := NewLLMMapper(r.model, r.apiBase, r.apiKey, r.timeoutSeconds, r.extraParams, r.observer)
419
+ return mapper.Execute(config)
420
+ }
421
+
422
+ // AgenticMap executes an Agentic-Map operation with full sub-agent sessions per item.
423
+ func (r *RLM) AgenticMap(config AgenticMapConfig) (*AgenticMapResult, error) {
424
+ mapper := NewAgenticMapper(r.model, r.apiBase, r.apiKey, r.timeoutSeconds, r.extraParams, r.observer)
425
+ return mapper.Execute(config)
426
+ }
427
+
305
428
  // Shutdown gracefully shuts down the RLM engine and its observer.
306
429
  func (r *RLM) Shutdown() {
307
430
  if r.observer != nil {
@@ -0,0 +1,121 @@
1
+ package rlm
2
+
3
+ import "io"
4
+
5
+ // ─── Store Backend Interface ─────────────────────────────────────────────────
6
+ // Abstracts the persistence layer for LCM's dual-state memory.
7
+ // The in-memory implementation (LCMStore) remains the default.
8
+ // A SQLite implementation provides crash recovery, indexed full-text search,
9
+ // and transactional writes as described in the LCM paper (Section 2.1).
10
+
11
+ // StoreBackend defines the persistence operations for the LCM store.
12
+ // Implementations must be safe for concurrent use.
13
+ type StoreBackend interface {
14
+ // ─── Message Operations ──────────────────────────────────────────
15
+ // PersistMessage stores a message in the immutable store.
16
+ PersistMessage(msg *StoreMessage) error
17
+ // GetMessage retrieves a message by ID.
18
+ GetMessage(id string) (*StoreMessage, error)
19
+ // GetAllMessages returns all messages in chronological order.
20
+ GetAllMessages() ([]*StoreMessage, error)
21
+ // MessageCount returns the total number of persisted messages.
22
+ MessageCount() (int, error)
23
+
24
+ // ─── Summary Operations ─────────────────────────────────────────
25
+ // PersistSummary stores a summary node in the DAG.
26
+ PersistSummary(node *SummaryNode) error
27
+ // GetSummary retrieves a summary by ID.
28
+ GetSummary(id string) (*SummaryNode, error)
29
+ // GetAllSummaries returns all summary nodes.
30
+ GetAllSummaries() ([]*SummaryNode, error)
31
+ // UpdateSummaryParent sets the parent ID on a summary (for condensation).
32
+ UpdateSummaryParent(summaryID, parentID string) error
33
+
34
+ // ─── Search ─────────────────────────────────────────────────────
35
+ // GrepMessages searches message content with a regex pattern.
36
+ // Returns matching messages with optional summary scope filtering.
37
+ GrepMessages(pattern string, summaryScope *string, maxResults int) ([]*StoreMessage, error)
38
+
39
+ // ─── Lifecycle ──────────────────────────────────────────────────
40
+ io.Closer
41
+ }
42
+
43
+ // ─── In-Memory Backend ───────────────────────────────────────────────────────
44
+ // MemoryBackend wraps the existing in-memory maps as a StoreBackend.
45
+ // This is the default backend and requires no external dependencies.
46
+
47
+ type MemoryBackend struct {
48
+ messages map[string]*StoreMessage
49
+ messageSeq []*StoreMessage
50
+ summaries map[string]*SummaryNode
51
+ }
52
+
53
+ // NewMemoryBackend creates a new in-memory backend.
54
+ func NewMemoryBackend() *MemoryBackend {
55
+ return &MemoryBackend{
56
+ messages: make(map[string]*StoreMessage),
57
+ messageSeq: make([]*StoreMessage, 0),
58
+ summaries: make(map[string]*SummaryNode),
59
+ }
60
+ }
61
+
62
+ func (m *MemoryBackend) PersistMessage(msg *StoreMessage) error {
63
+ m.messages[msg.ID] = msg
64
+ m.messageSeq = append(m.messageSeq, msg)
65
+ return nil
66
+ }
67
+
68
+ func (m *MemoryBackend) GetMessage(id string) (*StoreMessage, error) {
69
+ msg, ok := m.messages[id]
70
+ if !ok {
71
+ return nil, nil
72
+ }
73
+ return msg, nil
74
+ }
75
+
76
+ func (m *MemoryBackend) GetAllMessages() ([]*StoreMessage, error) {
77
+ result := make([]*StoreMessage, len(m.messageSeq))
78
+ copy(result, m.messageSeq)
79
+ return result, nil
80
+ }
81
+
82
+ func (m *MemoryBackend) MessageCount() (int, error) {
83
+ return len(m.messageSeq), nil
84
+ }
85
+
86
+ func (m *MemoryBackend) PersistSummary(node *SummaryNode) error {
87
+ m.summaries[node.ID] = node
88
+ return nil
89
+ }
90
+
91
+ func (m *MemoryBackend) GetSummary(id string) (*SummaryNode, error) {
92
+ sum, ok := m.summaries[id]
93
+ if !ok {
94
+ return nil, nil
95
+ }
96
+ return sum, nil
97
+ }
98
+
99
+ func (m *MemoryBackend) GetAllSummaries() ([]*SummaryNode, error) {
100
+ result := make([]*SummaryNode, 0, len(m.summaries))
101
+ for _, s := range m.summaries {
102
+ result = append(result, s)
103
+ }
104
+ return result, nil
105
+ }
106
+
107
+ func (m *MemoryBackend) UpdateSummaryParent(summaryID, parentID string) error {
108
+ if sum, ok := m.summaries[summaryID]; ok {
109
+ sum.ParentID = parentID
110
+ }
111
+ return nil
112
+ }
113
+
114
+ func (m *MemoryBackend) GrepMessages(pattern string, summaryScope *string, maxResults int) ([]*StoreMessage, error) {
115
+ // For in-memory, just return all messages (filtering happens in LCMStore.Grep)
116
+ return m.messageSeq, nil
117
+ }
118
+
119
+ func (m *MemoryBackend) Close() error {
120
+ return nil
121
+ }