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/README.md +3 -1
- package/bin/rlm-go +0 -0
- package/dist/bridge-interface.d.ts +149 -0
- package/go/cmd/rlm/main.go +39 -6
- package/go/go.mod +13 -3
- package/go/go.sum +53 -2
- package/go/rlm/compression.go +59 -0
- package/go/rlm/context_overflow.go +21 -36
- package/go/rlm/context_savings_test.go +387 -0
- package/go/rlm/json_extraction.go +140 -0
- package/go/rlm/lcm_agentic_map.go +317 -0
- package/go/rlm/lcm_context_loop.go +309 -0
- package/go/rlm/lcm_delegation.go +257 -0
- package/go/rlm/lcm_episodes.go +313 -0
- package/go/rlm/lcm_episodes_test.go +384 -0
- package/go/rlm/lcm_files.go +424 -0
- package/go/rlm/lcm_map.go +348 -0
- package/go/rlm/lcm_store.go +615 -0
- package/go/rlm/lcm_summarizer.go +239 -0
- package/go/rlm/lcm_test.go +1407 -0
- package/go/rlm/rlm.go +124 -1
- package/go/rlm/store_backend.go +121 -0
- package/go/rlm/store_backend_test.go +428 -0
- package/go/rlm/store_sqlite.go +575 -0
- package/go/rlm/structured.go +6 -83
- package/go/rlm/token_tracking_test.go +25 -11
- package/go/rlm/tokenizer.go +216 -0
- package/go/rlm/tokenizer_test.go +305 -0
- package/go/rlm/types.go +23 -1
- package/go/rlm.test +0 -0
- package/package.json +1 -1
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
|
+
}
|