recursive-llm-ts 2.0.12 → 3.0.1

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.
@@ -0,0 +1,168 @@
1
+ package rlm
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ // Benchmark parser performance
9
+ func BenchmarkIsFinal(b *testing.B) {
10
+ responses := []string{
11
+ `FINAL("answer")`,
12
+ `FINAL_VAR(result)`,
13
+ `x = 1`,
14
+ `console.log("test")`,
15
+ }
16
+ b.ResetTimer()
17
+ for i := 0; i < b.N; i++ {
18
+ IsFinal(responses[i%len(responses)])
19
+ }
20
+ }
21
+
22
+ func BenchmarkExtractFinal(b *testing.B) {
23
+ response := `FINAL("This is a test answer with some content")`
24
+ b.ResetTimer()
25
+ for i := 0; i < b.N; i++ {
26
+ extractFinal(response)
27
+ }
28
+ }
29
+
30
+ func BenchmarkParseResponse(b *testing.B) {
31
+ response := `FINAL("Test answer")`
32
+ env := map[string]interface{}{
33
+ "result": "test",
34
+ }
35
+ b.ResetTimer()
36
+ for i := 0; i < b.N; i++ {
37
+ ParseResponse(response, env)
38
+ }
39
+ }
40
+
41
+ // Benchmark REPL performance
42
+ func BenchmarkREPLSimpleExecution(b *testing.B) {
43
+ repl := NewREPLExecutor()
44
+ code := `console.log("Hello World")`
45
+ env := map[string]interface{}{}
46
+
47
+ b.ResetTimer()
48
+ for i := 0; i < b.N; i++ {
49
+ _, _ = repl.Execute(code, env)
50
+ }
51
+ }
52
+
53
+ func BenchmarkREPLContextAccess(b *testing.B) {
54
+ repl := NewREPLExecutor()
55
+ code := `console.log(context.slice(0, 10))`
56
+ env := map[string]interface{}{
57
+ "context": strings.Repeat("Lorem ipsum dolor sit amet. ", 1000),
58
+ }
59
+
60
+ b.ResetTimer()
61
+ for i := 0; i < b.N; i++ {
62
+ _, _ = repl.Execute(code, env)
63
+ }
64
+ }
65
+
66
+ func BenchmarkREPLRegex(b *testing.B) {
67
+ repl := NewREPLExecutor()
68
+ code := `const matches = re.findall("ERROR", context); console.log(matches.length)`
69
+ context := strings.Repeat("INFO ERROR WARNING ", 100)
70
+ env := map[string]interface{}{
71
+ "context": context,
72
+ "re": NewRegexHelper(),
73
+ }
74
+
75
+ b.ResetTimer()
76
+ for i := 0; i < b.N; i++ {
77
+ _, _ = repl.Execute(code, env)
78
+ }
79
+ }
80
+
81
+ func BenchmarkREPLJSBootstrap(b *testing.B) {
82
+ repl := NewREPLExecutor()
83
+ code := `const arr = range(100); const s = sum(arr); console.log(s)`
84
+ env := map[string]interface{}{}
85
+
86
+ b.ResetTimer()
87
+ for i := 0; i < b.N; i++ {
88
+ _, _ = repl.Execute(code, env)
89
+ }
90
+ }
91
+
92
+ // Benchmark regex helper
93
+ func BenchmarkRegexFindall(b *testing.B) {
94
+ re := NewRegexHelper()
95
+ text := strings.Repeat("ERROR INFO WARNING ERROR ", 100)
96
+
97
+ b.ResetTimer()
98
+ for i := 0; i < b.N; i++ {
99
+ re["findall"]("ERROR", text)
100
+ }
101
+ }
102
+
103
+ func BenchmarkRegexSearch(b *testing.B) {
104
+ re := NewRegexHelper()
105
+ text := strings.Repeat("INFO WARNING ", 50) + "ERROR" + strings.Repeat(" INFO WARNING", 50)
106
+
107
+ b.ResetTimer()
108
+ for i := 0; i < b.N; i++ {
109
+ re["search"]("ERROR", text)
110
+ }
111
+ }
112
+
113
+ // Benchmark config parsing
114
+ func BenchmarkConfigFromMap(b *testing.B) {
115
+ config := map[string]interface{}{
116
+ "recursive_model": "gpt-4o-mini",
117
+ "api_base": "https://api.openai.com/v1",
118
+ "api_key": "sk-test",
119
+ "max_depth": 5,
120
+ "max_iterations": 30,
121
+ "temperature": 0.7,
122
+ "extra_param": "value",
123
+ }
124
+
125
+ b.ResetTimer()
126
+ for i := 0; i < b.N; i++ {
127
+ ConfigFromMap(config)
128
+ }
129
+ }
130
+
131
+ // Benchmark code extraction
132
+ func BenchmarkExtractCode(b *testing.B) {
133
+ code := "```javascript\nconsole.log('test')\nconst x = 42\n```"
134
+
135
+ b.ResetTimer()
136
+ for i := 0; i < b.N; i++ {
137
+ extractCode(code)
138
+ }
139
+ }
140
+
141
+ // Memory allocation benchmarks
142
+ func BenchmarkREPLMemoryAllocation(b *testing.B) {
143
+ repl := NewREPLExecutor()
144
+ code := `const arr = []; for (let i = 0; i < 1000; i++) arr.push(i); console.log(arr.length)`
145
+ env := map[string]interface{}{}
146
+
147
+ b.ReportAllocs()
148
+ b.ResetTimer()
149
+ for i := 0; i < b.N; i++ {
150
+ _, _ = repl.Execute(code, env)
151
+ }
152
+ }
153
+
154
+ func BenchmarkLargeContextAccess(b *testing.B) {
155
+ repl := NewREPLExecutor()
156
+ // Simulate 100KB context
157
+ largeContext := strings.Repeat("Lorem ipsum dolor sit amet, consectetur adipiscing elit. ", 2000)
158
+ code := `const first = context.slice(0, 100); const last = context.slice(-100); console.log(first.length + last.length)`
159
+ env := map[string]interface{}{
160
+ "context": largeContext,
161
+ }
162
+
163
+ b.ReportAllocs()
164
+ b.ResetTimer()
165
+ for i := 0; i < b.N; i++ {
166
+ _, _ = repl.Execute(code, env)
167
+ }
168
+ }
@@ -0,0 +1,83 @@
1
+ package rlm
2
+
3
+ import "fmt"
4
+
5
+ // RLMError is the base error type for all RLM errors
6
+ type RLMError struct {
7
+ Message string
8
+ Cause error
9
+ }
10
+
11
+ func (e *RLMError) Error() string {
12
+ if e.Cause != nil {
13
+ return fmt.Sprintf("%s: %v", e.Message, e.Cause)
14
+ }
15
+ return e.Message
16
+ }
17
+
18
+ func (e *RLMError) Unwrap() error {
19
+ return e.Cause
20
+ }
21
+
22
+ // MaxIterationsError is returned when max iterations are exceeded
23
+ type MaxIterationsError struct {
24
+ MaxIterations int
25
+ *RLMError
26
+ }
27
+
28
+ func NewMaxIterationsError(maxIterations int) *MaxIterationsError {
29
+ return &MaxIterationsError{
30
+ MaxIterations: maxIterations,
31
+ RLMError: &RLMError{
32
+ Message: fmt.Sprintf("max iterations (%d) exceeded without FINAL()", maxIterations),
33
+ },
34
+ }
35
+ }
36
+
37
+ // MaxDepthError is returned when max recursion depth is exceeded
38
+ type MaxDepthError struct {
39
+ MaxDepth int
40
+ *RLMError
41
+ }
42
+
43
+ func NewMaxDepthError(maxDepth int) *MaxDepthError {
44
+ return &MaxDepthError{
45
+ MaxDepth: maxDepth,
46
+ RLMError: &RLMError{
47
+ Message: fmt.Sprintf("max recursion depth (%d) exceeded", maxDepth),
48
+ },
49
+ }
50
+ }
51
+
52
+ // REPLError is returned when REPL execution fails
53
+ type REPLError struct {
54
+ Code string
55
+ *RLMError
56
+ }
57
+
58
+ func NewREPLError(message string, code string, cause error) *REPLError {
59
+ return &REPLError{
60
+ Code: code,
61
+ RLMError: &RLMError{
62
+ Message: message,
63
+ Cause: cause,
64
+ },
65
+ }
66
+ }
67
+
68
+ // APIError is returned when LLM API calls fail
69
+ type APIError struct {
70
+ StatusCode int
71
+ Response string
72
+ *RLMError
73
+ }
74
+
75
+ func NewAPIError(statusCode int, response string) *APIError {
76
+ return &APIError{
77
+ StatusCode: statusCode,
78
+ Response: response,
79
+ RLMError: &RLMError{
80
+ Message: fmt.Sprintf("LLM request failed (%d): %s", statusCode, response),
81
+ },
82
+ }
83
+ }
@@ -0,0 +1,128 @@
1
+ package rlm
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "io"
9
+ "net/http"
10
+ "strings"
11
+ "time"
12
+ )
13
+
14
+ type Message struct {
15
+ Role string `json:"role"`
16
+ Content string `json:"content"`
17
+ }
18
+
19
+ type ChatRequest struct {
20
+ Model string
21
+ Messages []Message
22
+ APIBase string
23
+ APIKey string
24
+ Timeout int
25
+ ExtraParams map[string]interface{}
26
+ }
27
+
28
+ type chatResponse struct {
29
+ Choices []struct {
30
+ Message struct {
31
+ Content string `json:"content"`
32
+ } `json:"message"`
33
+ } `json:"choices"`
34
+ Error *struct {
35
+ Message string `json:"message"`
36
+ } `json:"error"`
37
+ }
38
+
39
+ var (
40
+ // defaultHTTPClient is a shared HTTP client with connection pooling
41
+ defaultHTTPClient = &http.Client{
42
+ Timeout: 60 * time.Second,
43
+ Transport: &http.Transport{
44
+ MaxIdleConns: 100,
45
+ MaxIdleConnsPerHost: 10,
46
+ IdleConnTimeout: 90 * time.Second,
47
+ },
48
+ }
49
+ )
50
+
51
+ func CallChatCompletion(request ChatRequest) (string, error) {
52
+ endpoint := buildEndpoint(request.APIBase)
53
+ payload := map[string]interface{}{
54
+ "model": request.Model,
55
+ "messages": request.Messages,
56
+ }
57
+
58
+ for key, value := range request.ExtraParams {
59
+ payload[key] = value
60
+ }
61
+
62
+ body, err := json.Marshal(payload)
63
+ if err != nil {
64
+ return "", err
65
+ }
66
+
67
+ // Use shared client with connection pooling
68
+ client := defaultHTTPClient
69
+ if request.Timeout > 0 {
70
+ // Create custom client for non-default timeout
71
+ client = &http.Client{
72
+ Timeout: time.Duration(request.Timeout) * time.Second,
73
+ Transport: defaultHTTPClient.Transport,
74
+ }
75
+ }
76
+
77
+ req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
78
+ if err != nil {
79
+ return "", err
80
+ }
81
+ req.Header.Set("Content-Type", "application/json")
82
+ if request.APIKey != "" {
83
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", request.APIKey))
84
+ }
85
+
86
+ resp, err := client.Do(req)
87
+ if err != nil {
88
+ return "", err
89
+ }
90
+ defer resp.Body.Close()
91
+
92
+ responseBody, err := io.ReadAll(resp.Body)
93
+ if err != nil {
94
+ return "", err
95
+ }
96
+
97
+ if resp.StatusCode >= http.StatusBadRequest {
98
+ return "", NewAPIError(resp.StatusCode, strings.TrimSpace(string(responseBody)))
99
+ }
100
+
101
+ var parsed chatResponse
102
+ if err := json.Unmarshal(responseBody, &parsed); err != nil {
103
+ return "", err
104
+ }
105
+
106
+ if parsed.Error != nil && parsed.Error.Message != "" {
107
+ return "", errors.New(parsed.Error.Message)
108
+ }
109
+
110
+ if len(parsed.Choices) == 0 {
111
+ return "", errors.New("no choices returned by LLM")
112
+ }
113
+
114
+ return parsed.Choices[0].Message.Content, nil
115
+ }
116
+
117
+ func buildEndpoint(apiBase string) string {
118
+ base := strings.TrimSpace(apiBase)
119
+ if base == "" {
120
+ base = "https://api.openai.com/v1"
121
+ }
122
+
123
+ if strings.Contains(base, "/chat/completions") {
124
+ return base
125
+ }
126
+
127
+ return strings.TrimRight(base, "/") + "/chat/completions"
128
+ }
@@ -0,0 +1,53 @@
1
+ package rlm
2
+
3
+ import (
4
+ "fmt"
5
+ "regexp"
6
+ "strings"
7
+ )
8
+
9
+ var (
10
+ finalTripleDouble = regexp.MustCompile(`(?s)FINAL\s*\(\s*"""(.*)"""`)
11
+ finalTripleSingle = regexp.MustCompile(`(?s)FINAL\s*\(\s*'''(.*)'''`)
12
+ finalDouble = regexp.MustCompile(`(?s)FINAL\s*\(\s*"([^"]*)"`)
13
+ finalSingle = regexp.MustCompile(`(?s)FINAL\s*\(\s*'([^']*)'`)
14
+ finalVar = regexp.MustCompile(`FINAL_VAR\s*\(\s*(\w+)\s*\)`)
15
+ finalAny = regexp.MustCompile(`FINAL\(|FINAL_VAR\(`)
16
+ )
17
+
18
+ func IsFinal(response string) bool {
19
+ return finalAny.MatchString(response)
20
+ }
21
+
22
+ func ParseResponse(response string, env map[string]interface{}) (string, bool) {
23
+ answer, ok := extractFinal(response)
24
+ if ok {
25
+ return answer, true
26
+ }
27
+
28
+ return extractFinalVar(response, env)
29
+ }
30
+
31
+ func extractFinal(response string) (string, bool) {
32
+ matchers := []*regexp.Regexp{finalTripleDouble, finalTripleSingle, finalDouble, finalSingle}
33
+ for _, matcher := range matchers {
34
+ match := matcher.FindStringSubmatch(response)
35
+ if len(match) > 1 {
36
+ return strings.TrimSpace(match[1]), true
37
+ }
38
+ }
39
+ return "", false
40
+ }
41
+
42
+ func extractFinalVar(response string, env map[string]interface{}) (string, bool) {
43
+ match := finalVar.FindStringSubmatch(response)
44
+ if len(match) < 2 {
45
+ return "", false
46
+ }
47
+
48
+ value, ok := env[match[1]]
49
+ if !ok {
50
+ return "", false
51
+ }
52
+ return fmt.Sprint(value), true
53
+ }
@@ -0,0 +1,202 @@
1
+ package rlm
2
+
3
+ import (
4
+ "testing"
5
+ )
6
+
7
+ func TestIsFinal(t *testing.T) {
8
+ tests := []struct {
9
+ name string
10
+ response string
11
+ want bool
12
+ }{
13
+ {"FINAL with double quotes", `FINAL("answer")`, true},
14
+ {"FINAL with single quotes", `FINAL('answer')`, true},
15
+ {"FINAL with triple double quotes", `FINAL("""answer""")`, true},
16
+ {"FINAL_VAR", `FINAL_VAR(result)`, true},
17
+ {"No FINAL", `x = 1`, false},
18
+ {"Contains FINAL as substring", `This is FINALLY done`, false},
19
+ }
20
+
21
+ for _, tt := range tests {
22
+ t.Run(tt.name, func(t *testing.T) {
23
+ if got := IsFinal(tt.response); got != tt.want {
24
+ t.Errorf("IsFinal() = %v, want %v", got, tt.want)
25
+ }
26
+ })
27
+ }
28
+ }
29
+
30
+ func TestExtractFinal(t *testing.T) {
31
+ tests := []struct {
32
+ name string
33
+ response string
34
+ want string
35
+ wantOk bool
36
+ }{
37
+ {
38
+ name: "Double quotes",
39
+ response: `FINAL("The answer is 42")`,
40
+ want: "The answer is 42",
41
+ wantOk: true,
42
+ },
43
+ {
44
+ name: "Single quotes",
45
+ response: `FINAL('The answer is 42')`,
46
+ want: "The answer is 42",
47
+ wantOk: true,
48
+ },
49
+ {
50
+ name: "Triple double quotes",
51
+ response: `FINAL("""The answer is 42""")`,
52
+ want: "The answer is 42",
53
+ wantOk: true,
54
+ },
55
+ {
56
+ name: "Triple single quotes",
57
+ response: `FINAL('''The answer is 42''')`,
58
+ want: "The answer is 42",
59
+ wantOk: true,
60
+ },
61
+ {
62
+ name: "Multiline with triple quotes",
63
+ response: `FINAL("""Line 1
64
+ Line 2
65
+ Line 3""")`,
66
+ want: "Line 1\nLine 2\nLine 3",
67
+ wantOk: true,
68
+ },
69
+ {
70
+ name: "With whitespace",
71
+ response: `FINAL( "The answer" )`,
72
+ want: "The answer",
73
+ wantOk: true,
74
+ },
75
+ {
76
+ name: "No FINAL",
77
+ response: `x = 1`,
78
+ want: "",
79
+ wantOk: false,
80
+ },
81
+ {
82
+ name: "FINAL_VAR should not match",
83
+ response: `FINAL_VAR(result)`,
84
+ want: "",
85
+ wantOk: false,
86
+ },
87
+ }
88
+
89
+ for _, tt := range tests {
90
+ t.Run(tt.name, func(t *testing.T) {
91
+ got, gotOk := extractFinal(tt.response)
92
+ if gotOk != tt.wantOk {
93
+ t.Errorf("extractFinal() ok = %v, want %v", gotOk, tt.wantOk)
94
+ }
95
+ if got != tt.want {
96
+ t.Errorf("extractFinal() = %q, want %q", got, tt.want)
97
+ }
98
+ })
99
+ }
100
+ }
101
+
102
+ func TestExtractFinalVar(t *testing.T) {
103
+ tests := []struct {
104
+ name string
105
+ response string
106
+ env map[string]interface{}
107
+ want string
108
+ wantOk bool
109
+ }{
110
+ {
111
+ name: "Simple variable",
112
+ response: `FINAL_VAR(result)`,
113
+ env: map[string]interface{}{"result": "The answer"},
114
+ want: "The answer",
115
+ wantOk: true,
116
+ },
117
+ {
118
+ name: "Integer variable",
119
+ response: `FINAL_VAR(count)`,
120
+ env: map[string]interface{}{"count": 42},
121
+ want: "42",
122
+ wantOk: true,
123
+ },
124
+ {
125
+ name: "Variable not found",
126
+ response: `FINAL_VAR(missing)`,
127
+ env: map[string]interface{}{"result": "The answer"},
128
+ want: "",
129
+ wantOk: false,
130
+ },
131
+ {
132
+ name: "With whitespace",
133
+ response: `FINAL_VAR( result )`,
134
+ env: map[string]interface{}{"result": "The answer"},
135
+ want: "The answer",
136
+ wantOk: true,
137
+ },
138
+ {
139
+ name: "No FINAL_VAR",
140
+ response: `x = 1`,
141
+ env: map[string]interface{}{},
142
+ want: "",
143
+ wantOk: false,
144
+ },
145
+ }
146
+
147
+ for _, tt := range tests {
148
+ t.Run(tt.name, func(t *testing.T) {
149
+ got, gotOk := extractFinalVar(tt.response, tt.env)
150
+ if gotOk != tt.wantOk {
151
+ t.Errorf("extractFinalVar() ok = %v, want %v", gotOk, tt.wantOk)
152
+ }
153
+ if got != tt.want {
154
+ t.Errorf("extractFinalVar() = %q, want %q", got, tt.want)
155
+ }
156
+ })
157
+ }
158
+ }
159
+
160
+ func TestParseResponse(t *testing.T) {
161
+ tests := []struct {
162
+ name string
163
+ response string
164
+ env map[string]interface{}
165
+ want string
166
+ wantOk bool
167
+ }{
168
+ {
169
+ name: "FINAL takes precedence",
170
+ response: `FINAL("Direct answer")`,
171
+ env: map[string]interface{}{"result": "Var answer"},
172
+ want: "Direct answer",
173
+ wantOk: true,
174
+ },
175
+ {
176
+ name: "FINAL_VAR fallback",
177
+ response: `FINAL_VAR(result)`,
178
+ env: map[string]interface{}{"result": "Var answer"},
179
+ want: "Var answer",
180
+ wantOk: true,
181
+ },
182
+ {
183
+ name: "Neither",
184
+ response: `x = 1`,
185
+ env: map[string]interface{}{},
186
+ want: "",
187
+ wantOk: false,
188
+ },
189
+ }
190
+
191
+ for _, tt := range tests {
192
+ t.Run(tt.name, func(t *testing.T) {
193
+ got, gotOk := ParseResponse(tt.response, tt.env)
194
+ if gotOk != tt.wantOk {
195
+ t.Errorf("ParseResponse() ok = %v, want %v", gotOk, tt.wantOk)
196
+ }
197
+ if got != tt.want {
198
+ t.Errorf("ParseResponse() = %q, want %q", got, tt.want)
199
+ }
200
+ })
201
+ }
202
+ }
@@ -0,0 +1,68 @@
1
+ package rlm
2
+
3
+ import "fmt"
4
+
5
+ // BuildSystemPrompt creates the system prompt for RLM
6
+ // useMetacognitive enables step-by-step reasoning guidance
7
+ func BuildSystemPrompt(contextSize int, depth int, query string, useMetacognitive bool) string {
8
+ if useMetacognitive {
9
+ return buildMetacognitivePrompt(contextSize, depth, query)
10
+ }
11
+ return buildMinimalPrompt(contextSize, depth, query)
12
+ }
13
+
14
+ func buildMinimalPrompt(contextSize int, depth int, query string) string {
15
+ return fmt.Sprintf(`You are a Recursive Language Model. You interact with context through a JavaScript REPL environment.
16
+
17
+ The context is stored in variable "context" (not in this prompt). Size: %d characters.
18
+
19
+ Available in environment:
20
+ - context: string (the document to analyze)
21
+ - query: string (the question: %q)
22
+ - recursive_llm(sub_query, sub_context) -> string (recursively process sub-context)
23
+ - re: regex helper with findall(pattern, text) and search(pattern, text)
24
+ - print(value, ...) -> output text
25
+ - len(value) -> length of arrays/strings
26
+ - json: helper with loads() and dumps()
27
+ - math: Math helper
28
+ - datetime: Date helper
29
+ - Counter(iterable) -> object of counts
30
+ - defaultdict(defaultFactory) -> object with defaults
31
+
32
+ Write JavaScript code to answer the query. The last expression or console.log output will be shown to you.
33
+
34
+ Examples:
35
+ - console.log(context.slice(0, 100)); // See first 100 chars
36
+ - const errors = re.findall("ERROR", context);
37
+ - const count = errors.length; console.log(count);
38
+
39
+ When you have the answer, use FINAL("answer") - this is NOT a function, just write it as text.
40
+
41
+ Depth: %d`, contextSize, query, depth)
42
+ }
43
+
44
+ func buildMetacognitivePrompt(contextSize int, depth int, query string) string {
45
+ return fmt.Sprintf(`You are a Recursive Language Model. You interact with context through a JavaScript REPL environment.
46
+
47
+ The context is stored in variable "context" (not in this prompt). Size: %d characters.
48
+
49
+ Available in environment:
50
+ - context: string (the document to analyze)
51
+ - query: string (the question: %q)
52
+ - recursive_llm(sub_query, sub_context) -> string (recursively process sub-context)
53
+ - re: regex helper with findall(pattern, text) and search(pattern, text)
54
+ - print(value, ...) -> output text
55
+ - len(value) -> length of arrays/strings
56
+ - json: helper with loads() and dumps()
57
+ - math: Math helper
58
+ - Counter(iterable) -> object of counts
59
+
60
+ STRATEGY TIP: You can peek at context first to understand its structure before processing.
61
+ Example: console.log(context.slice(0, 100))
62
+
63
+ Write JavaScript code to answer the query. The last expression or console.log output will be shown to you.
64
+
65
+ When you have the answer, use FINAL("answer") - this is NOT a function, just write it as text.
66
+
67
+ Depth: %d`, contextSize, query, depth)
68
+ }