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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
package rlm
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"strings"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// ─── Five-Level Summarization Escalation ────────────────────────────────────
|
|
9
|
+
// Implements the guaranteed-convergence summarization protocol from the LCM paper.
|
|
10
|
+
// Level 1 (Normal): LLM-Summarize with preserve_details mode
|
|
11
|
+
// Level 2 (Aggressive): LLM-Summarize with bullet_points mode, half target tokens
|
|
12
|
+
// Level 3 (TF-IDF): Extractive compression, no LLM, preserves actual sentences
|
|
13
|
+
// Level 4 (TextRank): Graph-based extractive compression, no LLM, better coherence
|
|
14
|
+
// Level 5 (Deterministic): DeterministicTruncate, no LLM, guaranteed reduction
|
|
15
|
+
|
|
16
|
+
// LCMSummarizer handles the five-level escalation for context compaction.
|
|
17
|
+
type LCMSummarizer struct {
|
|
18
|
+
model string
|
|
19
|
+
apiBase string
|
|
20
|
+
apiKey string
|
|
21
|
+
timeout int
|
|
22
|
+
extraParams map[string]interface{}
|
|
23
|
+
observer *Observer
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// NewLCMSummarizer creates a summarizer with the given LLM configuration.
|
|
27
|
+
func NewLCMSummarizer(model, apiBase, apiKey string, timeout int, extraParams map[string]interface{}, observer *Observer) *LCMSummarizer {
|
|
28
|
+
return &LCMSummarizer{
|
|
29
|
+
model: model,
|
|
30
|
+
apiBase: apiBase,
|
|
31
|
+
apiKey: apiKey,
|
|
32
|
+
timeout: timeout,
|
|
33
|
+
extraParams: extraParams,
|
|
34
|
+
observer: observer,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// SummarizeResult contains the output of a summarization attempt.
|
|
39
|
+
type SummarizeResult struct {
|
|
40
|
+
Content string // The summary text
|
|
41
|
+
Tokens int // Token count of the summary
|
|
42
|
+
Level int // Escalation level used (1-5)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Summarize applies the five-level escalation to compress text to targetTokens.
|
|
46
|
+
// Guaranteed to converge: Level 5 is deterministic truncation.
|
|
47
|
+
func (ls *LCMSummarizer) Summarize(input string, targetTokens int) (*SummarizeResult, error) {
|
|
48
|
+
inputTokens := EstimateTokens(input)
|
|
49
|
+
|
|
50
|
+
// If already within budget, no summarization needed
|
|
51
|
+
if inputTokens <= targetTokens {
|
|
52
|
+
return &SummarizeResult{
|
|
53
|
+
Content: input,
|
|
54
|
+
Tokens: inputTokens,
|
|
55
|
+
Level: 0,
|
|
56
|
+
}, nil
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ls.observer.Debug("lcm.summarizer", "Starting escalation: %d tokens → target %d", inputTokens, targetTokens)
|
|
60
|
+
|
|
61
|
+
// Level 1: Normal - preserve details
|
|
62
|
+
result, err := ls.summarizeLevel1(input, targetTokens)
|
|
63
|
+
if err == nil && result.Tokens < inputTokens {
|
|
64
|
+
ls.observer.Debug("lcm.summarizer", "Level 1 succeeded: %d → %d tokens", inputTokens, result.Tokens)
|
|
65
|
+
return result, nil
|
|
66
|
+
}
|
|
67
|
+
if err != nil {
|
|
68
|
+
ls.observer.Debug("lcm.summarizer", "Level 1 failed: %v, escalating", err)
|
|
69
|
+
} else {
|
|
70
|
+
ls.observer.Debug("lcm.summarizer", "Level 1 did not reduce (%d >= %d), escalating", result.Tokens, inputTokens)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Level 2: Aggressive - bullet points at half target
|
|
74
|
+
result, err = ls.summarizeLevel2(input, targetTokens/2)
|
|
75
|
+
if err == nil && result.Tokens < inputTokens {
|
|
76
|
+
ls.observer.Debug("lcm.summarizer", "Level 2 succeeded: %d → %d tokens", inputTokens, result.Tokens)
|
|
77
|
+
return result, nil
|
|
78
|
+
}
|
|
79
|
+
if err != nil {
|
|
80
|
+
ls.observer.Debug("lcm.summarizer", "Level 2 failed: %v, escalating", err)
|
|
81
|
+
} else {
|
|
82
|
+
ls.observer.Debug("lcm.summarizer", "Level 2 did not reduce (%d >= %d), escalating", result.Tokens, inputTokens)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Level 3: TF-IDF extractive compression
|
|
86
|
+
result = ls.summarizeLevel3TFIDF(input, targetTokens)
|
|
87
|
+
if result.Tokens < inputTokens {
|
|
88
|
+
ls.observer.Debug("lcm.summarizer", "Level 3 (TF-IDF) succeeded: %d → %d tokens", inputTokens, result.Tokens)
|
|
89
|
+
return result, nil
|
|
90
|
+
}
|
|
91
|
+
ls.observer.Debug("lcm.summarizer", "Level 3 (TF-IDF) did not reduce enough (%d >= %d), escalating", result.Tokens, inputTokens)
|
|
92
|
+
|
|
93
|
+
// Level 4: TextRank graph-based compression
|
|
94
|
+
result = ls.summarizeLevel4TextRank(input, targetTokens)
|
|
95
|
+
if result.Tokens < inputTokens {
|
|
96
|
+
ls.observer.Debug("lcm.summarizer", "Level 4 (TextRank) succeeded: %d → %d tokens", inputTokens, result.Tokens)
|
|
97
|
+
return result, nil
|
|
98
|
+
}
|
|
99
|
+
ls.observer.Debug("lcm.summarizer", "Level 4 (TextRank) did not reduce enough (%d >= %d), escalating to deterministic", result.Tokens, inputTokens)
|
|
100
|
+
|
|
101
|
+
// Level 5: Deterministic truncation - guaranteed convergence
|
|
102
|
+
result = ls.deterministicTruncate(input, targetTokens)
|
|
103
|
+
ls.observer.Debug("lcm.summarizer", "Level 5 (deterministic): %d → %d tokens", inputTokens, result.Tokens)
|
|
104
|
+
return result, nil
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// SummarizeMessages applies three-level escalation to a slice of messages.
|
|
108
|
+
func (ls *LCMSummarizer) SummarizeMessages(messages []*StoreMessage, targetTokens int) (*SummarizeResult, error) {
|
|
109
|
+
// Build a formatted input from messages
|
|
110
|
+
var sb strings.Builder
|
|
111
|
+
for _, msg := range messages {
|
|
112
|
+
sb.WriteString(fmt.Sprintf("[%s] %s\n", msg.Role, msg.Content))
|
|
113
|
+
}
|
|
114
|
+
return ls.Summarize(sb.String(), targetTokens)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Level 1: Normal LLM Summarization ──────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
func (ls *LCMSummarizer) summarizeLevel1(input string, targetTokens int) (*SummarizeResult, error) {
|
|
120
|
+
prompt := fmt.Sprintf(`Summarize the following content in approximately %d tokens.
|
|
121
|
+
Preserve all key details, decisions, and specific information.
|
|
122
|
+
Maintain the logical flow and any action items or conclusions.
|
|
123
|
+
|
|
124
|
+
Content:
|
|
125
|
+
%s
|
|
126
|
+
|
|
127
|
+
Provide a comprehensive summary that retains the most important information:`, targetTokens, input)
|
|
128
|
+
|
|
129
|
+
content, err := ls.callLLM(prompt, targetTokens)
|
|
130
|
+
if err != nil {
|
|
131
|
+
return nil, err
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return &SummarizeResult{
|
|
135
|
+
Content: content,
|
|
136
|
+
Tokens: EstimateTokens(content),
|
|
137
|
+
Level: 1,
|
|
138
|
+
}, nil
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Level 2: Aggressive LLM Summarization ──────────────────────────────────
|
|
142
|
+
|
|
143
|
+
func (ls *LCMSummarizer) summarizeLevel2(input string, targetTokens int) (*SummarizeResult, error) {
|
|
144
|
+
prompt := fmt.Sprintf(`Aggressively summarize the following content as bullet points.
|
|
145
|
+
Target: approximately %d tokens. Be extremely concise.
|
|
146
|
+
Keep only: key decisions, critical facts, action items, and conclusions.
|
|
147
|
+
Drop: examples, explanations, context, and elaboration.
|
|
148
|
+
|
|
149
|
+
Content:
|
|
150
|
+
%s
|
|
151
|
+
|
|
152
|
+
Bullet-point summary:`, targetTokens, input)
|
|
153
|
+
|
|
154
|
+
content, err := ls.callLLM(prompt, targetTokens)
|
|
155
|
+
if err != nil {
|
|
156
|
+
return nil, err
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return &SummarizeResult{
|
|
160
|
+
Content: content,
|
|
161
|
+
Tokens: EstimateTokens(content),
|
|
162
|
+
Level: 2,
|
|
163
|
+
}, nil
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Level 3: TF-IDF Extractive Compression ─────────────────────────────────
|
|
167
|
+
|
|
168
|
+
func (ls *LCMSummarizer) summarizeLevel3TFIDF(input string, targetTokens int) *SummarizeResult {
|
|
169
|
+
compressed := CompressContextTFIDF(input, targetTokens)
|
|
170
|
+
return &SummarizeResult{
|
|
171
|
+
Content: compressed,
|
|
172
|
+
Tokens: EstimateTokens(compressed),
|
|
173
|
+
Level: 3,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Level 4: TextRank Graph-Based Compression ─────────────────────────────
|
|
178
|
+
|
|
179
|
+
func (ls *LCMSummarizer) summarizeLevel4TextRank(input string, targetTokens int) *SummarizeResult {
|
|
180
|
+
compressed := CompressContextTextRank(input, targetTokens)
|
|
181
|
+
return &SummarizeResult{
|
|
182
|
+
Content: compressed,
|
|
183
|
+
Tokens: EstimateTokens(compressed),
|
|
184
|
+
Level: 4,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ─── Level 5: Deterministic Truncation ──────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
// deterministicTruncate is the guaranteed-convergence fallback.
|
|
191
|
+
// No LLM call involved — uses the shared TruncateText utility (compression.go).
|
|
192
|
+
func (ls *LCMSummarizer) deterministicTruncate(input string, maxTokens int) *SummarizeResult {
|
|
193
|
+
truncated := TruncateText(input, TruncateTextParams{
|
|
194
|
+
MaxTokens: maxTokens,
|
|
195
|
+
MarkerText: "\n[... content truncated for context management ...]\n",
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
return &SummarizeResult{
|
|
199
|
+
Content: truncated,
|
|
200
|
+
Tokens: EstimateTokens(truncated),
|
|
201
|
+
Level: 5,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── LLM Helper ─────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
func (ls *LCMSummarizer) callLLM(prompt string, maxTokens int) (string, error) {
|
|
208
|
+
// Cap max_tokens for summarization
|
|
209
|
+
if maxTokens > 4096 {
|
|
210
|
+
maxTokens = 4096
|
|
211
|
+
}
|
|
212
|
+
if maxTokens < 256 {
|
|
213
|
+
maxTokens = 256
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
params := make(map[string]interface{})
|
|
217
|
+
for k, v := range ls.extraParams {
|
|
218
|
+
params[k] = v
|
|
219
|
+
}
|
|
220
|
+
params["max_tokens"] = maxTokens
|
|
221
|
+
|
|
222
|
+
request := ChatRequest{
|
|
223
|
+
Model: ls.model,
|
|
224
|
+
Messages: []Message{
|
|
225
|
+
{Role: "user", Content: prompt},
|
|
226
|
+
},
|
|
227
|
+
APIBase: ls.apiBase,
|
|
228
|
+
APIKey: ls.apiKey,
|
|
229
|
+
Timeout: ls.timeout,
|
|
230
|
+
ExtraParams: params,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
result, err := CallChatCompletion(request)
|
|
234
|
+
if err != nil {
|
|
235
|
+
return "", fmt.Errorf("summarization LLM call failed: %w", err)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return result.Content, nil
|
|
239
|
+
}
|