recursive-llm-ts 4.4.1 → 4.6.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/dist/cache.js ADDED
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ /**
3
+ * Caching layer for recursive-llm-ts completions.
4
+ *
5
+ * Provides exact-match caching to avoid redundant API calls for
6
+ * identical query+context pairs. Supports in-memory and file-based storage.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.RLMCache = exports.FileCache = exports.MemoryCache = void 0;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const crypto = __importStar(require("crypto"));
46
+ // ─── Cache Key Generator ─────────────────────────────────────────────────────
47
+ function generateCacheKey(model, query, context, config) {
48
+ const data = JSON.stringify({ model, query, context, config });
49
+ return crypto.createHash('sha256').update(data).digest('hex');
50
+ }
51
+ // ─── In-Memory Cache ─────────────────────────────────────────────────────────
52
+ class MemoryCache {
53
+ constructor(maxEntries = 1000) {
54
+ this.store = new Map();
55
+ this.maxEntries = maxEntries;
56
+ }
57
+ get(key) {
58
+ const entry = this.store.get(key);
59
+ if (!entry)
60
+ return undefined;
61
+ // Check TTL
62
+ if (Date.now() - entry.createdAt > entry.ttl * 1000) {
63
+ this.store.delete(key);
64
+ return undefined;
65
+ }
66
+ entry.hitCount++;
67
+ return entry.value;
68
+ }
69
+ set(key, value, ttl) {
70
+ // Evict oldest if at capacity
71
+ if (this.store.size >= this.maxEntries && !this.store.has(key)) {
72
+ const oldestKey = this.store.keys().next().value;
73
+ if (oldestKey !== undefined) {
74
+ this.store.delete(oldestKey);
75
+ }
76
+ }
77
+ this.store.set(key, {
78
+ key,
79
+ value,
80
+ createdAt: Date.now(),
81
+ ttl,
82
+ hitCount: 0,
83
+ });
84
+ }
85
+ has(key) {
86
+ const entry = this.store.get(key);
87
+ if (!entry)
88
+ return false;
89
+ if (Date.now() - entry.createdAt > entry.ttl * 1000) {
90
+ this.store.delete(key);
91
+ return false;
92
+ }
93
+ return true;
94
+ }
95
+ delete(key) {
96
+ return this.store.delete(key);
97
+ }
98
+ clear() {
99
+ this.store.clear();
100
+ }
101
+ size() {
102
+ // Clean expired entries
103
+ const now = Date.now();
104
+ for (const [key, entry] of this.store) {
105
+ if (now - entry.createdAt > entry.ttl * 1000) {
106
+ this.store.delete(key);
107
+ }
108
+ }
109
+ return this.store.size;
110
+ }
111
+ }
112
+ exports.MemoryCache = MemoryCache;
113
+ // ─── File-Based Cache ────────────────────────────────────────────────────────
114
+ class FileCache {
115
+ constructor(cacheDir = '.rlm-cache', maxEntries = 1000) {
116
+ this.cacheDir = path.resolve(cacheDir);
117
+ this.maxEntries = maxEntries;
118
+ if (!fs.existsSync(this.cacheDir)) {
119
+ fs.mkdirSync(this.cacheDir, { recursive: true });
120
+ }
121
+ }
122
+ filePath(key) {
123
+ return path.join(this.cacheDir, `${key}.json`);
124
+ }
125
+ get(key) {
126
+ const fp = this.filePath(key);
127
+ if (!fs.existsSync(fp))
128
+ return undefined;
129
+ try {
130
+ const data = JSON.parse(fs.readFileSync(fp, 'utf-8'));
131
+ if (Date.now() - data.createdAt > data.ttl * 1000) {
132
+ fs.unlinkSync(fp);
133
+ return undefined;
134
+ }
135
+ return data.value;
136
+ }
137
+ catch (_a) {
138
+ return undefined;
139
+ }
140
+ }
141
+ set(key, value, ttl) {
142
+ const entry = {
143
+ key,
144
+ value,
145
+ createdAt: Date.now(),
146
+ ttl,
147
+ hitCount: 0,
148
+ };
149
+ try {
150
+ fs.writeFileSync(this.filePath(key), JSON.stringify(entry), 'utf-8');
151
+ }
152
+ catch (_a) {
153
+ // Silently fail on write errors
154
+ }
155
+ }
156
+ has(key) {
157
+ return this.get(key) !== undefined;
158
+ }
159
+ delete(key) {
160
+ const fp = this.filePath(key);
161
+ if (fs.existsSync(fp)) {
162
+ fs.unlinkSync(fp);
163
+ return true;
164
+ }
165
+ return false;
166
+ }
167
+ clear() {
168
+ if (fs.existsSync(this.cacheDir)) {
169
+ const files = fs.readdirSync(this.cacheDir);
170
+ for (const file of files) {
171
+ if (file.endsWith('.json')) {
172
+ fs.unlinkSync(path.join(this.cacheDir, file));
173
+ }
174
+ }
175
+ }
176
+ }
177
+ size() {
178
+ if (!fs.existsSync(this.cacheDir))
179
+ return 0;
180
+ return fs.readdirSync(this.cacheDir).filter(f => f.endsWith('.json')).length;
181
+ }
182
+ }
183
+ exports.FileCache = FileCache;
184
+ // ─── RLM Cache Manager ──────────────────────────────────────────────────────
185
+ class RLMCache {
186
+ constructor(config = {}) {
187
+ var _a, _b, _c, _d, _e, _f;
188
+ this.stats = { hits: 0, misses: 0, size: 0, hitRate: 0, evictions: 0 };
189
+ this.config = {
190
+ enabled: (_a = config.enabled) !== null && _a !== void 0 ? _a : false,
191
+ strategy: (_b = config.strategy) !== null && _b !== void 0 ? _b : 'exact',
192
+ maxEntries: (_c = config.maxEntries) !== null && _c !== void 0 ? _c : 1000,
193
+ ttl: (_d = config.ttl) !== null && _d !== void 0 ? _d : 3600,
194
+ storage: (_e = config.storage) !== null && _e !== void 0 ? _e : 'memory',
195
+ cacheDir: (_f = config.cacheDir) !== null && _f !== void 0 ? _f : '.rlm-cache',
196
+ };
197
+ if (this.config.storage === 'file') {
198
+ this.provider = new FileCache(this.config.cacheDir, this.config.maxEntries);
199
+ }
200
+ else {
201
+ this.provider = new MemoryCache(this.config.maxEntries);
202
+ }
203
+ }
204
+ /** Check if caching is enabled */
205
+ get enabled() {
206
+ return this.config.enabled && this.config.strategy !== 'none';
207
+ }
208
+ /** Look up a cached result */
209
+ lookup(model, query, context, extra) {
210
+ if (!this.enabled)
211
+ return { hit: false };
212
+ const key = generateCacheKey(model, query, context, extra);
213
+ const value = this.provider.get(key);
214
+ if (value !== undefined) {
215
+ this.stats.hits++;
216
+ this.updateHitRate();
217
+ return { hit: true, value };
218
+ }
219
+ this.stats.misses++;
220
+ this.updateHitRate();
221
+ return { hit: false };
222
+ }
223
+ /** Store a result in the cache */
224
+ store(model, query, context, value, extra) {
225
+ if (!this.enabled)
226
+ return;
227
+ const key = generateCacheKey(model, query, context, extra);
228
+ this.provider.set(key, value, this.config.ttl);
229
+ this.stats.size = this.provider.size();
230
+ }
231
+ /** Get cache statistics */
232
+ getStats() {
233
+ this.stats.size = this.provider.size();
234
+ return Object.assign({}, this.stats);
235
+ }
236
+ /** Clear the cache */
237
+ clear() {
238
+ this.provider.clear();
239
+ this.stats = { hits: 0, misses: 0, size: 0, hitRate: 0, evictions: 0 };
240
+ }
241
+ updateHitRate() {
242
+ const total = this.stats.hits + this.stats.misses;
243
+ this.stats.hitRate = total > 0 ? this.stats.hits / total : 0;
244
+ }
245
+ }
246
+ exports.RLMCache = RLMCache;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Configuration validation for recursive-llm-ts.
3
+ *
4
+ * Validates RLMConfig at construction time with clear error messages.
5
+ */
6
+ import { RLMConfig } from './bridge-interface';
7
+ import { CacheConfig } from './cache';
8
+ import { RetryConfig, FallbackConfig } from './retry';
9
+ export interface RLMExtendedConfig extends RLMConfig {
10
+ /** Cache configuration */
11
+ cache?: CacheConfig;
12
+ /** Retry configuration */
13
+ retry?: RetryConfig;
14
+ /** Fallback model configuration */
15
+ fallback?: FallbackConfig;
16
+ /** LiteLLM passthrough parameters */
17
+ litellm_params?: Record<string, unknown>;
18
+ }
19
+ export type ValidationLevel = 'error' | 'warning' | 'info';
20
+ export interface ValidationIssue {
21
+ level: ValidationLevel;
22
+ field: string;
23
+ message: string;
24
+ }
25
+ export interface ValidationResult {
26
+ valid: boolean;
27
+ issues: ValidationIssue[];
28
+ }
29
+ /**
30
+ * Validate an RLM configuration.
31
+ * Returns issues rather than throwing, allowing callers to handle gracefully.
32
+ */
33
+ export declare function validateConfig(config: RLMExtendedConfig): ValidationResult;
34
+ /**
35
+ * Validate config and throw on errors. Logs warnings.
36
+ */
37
+ export declare function assertValidConfig(config: RLMExtendedConfig): void;
package/dist/config.js ADDED
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /**
3
+ * Configuration validation for recursive-llm-ts.
4
+ *
5
+ * Validates RLMConfig at construction time with clear error messages.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.validateConfig = validateConfig;
9
+ exports.assertValidConfig = assertValidConfig;
10
+ const errors_1 = require("./errors");
11
+ // ─── Known Config Keys ───────────────────────────────────────────────────────
12
+ const KNOWN_CONFIG_KEYS = new Set([
13
+ 'recursive_model',
14
+ 'api_base',
15
+ 'api_key',
16
+ 'max_depth',
17
+ 'max_iterations',
18
+ 'pythonia_timeout',
19
+ 'go_binary_path',
20
+ 'meta_agent',
21
+ 'observability',
22
+ 'debug',
23
+ 'cache',
24
+ 'retry',
25
+ 'fallback',
26
+ 'litellm_params',
27
+ // Legacy LiteLLM passthrough keys (commonly used)
28
+ 'api_version',
29
+ 'timeout',
30
+ 'temperature',
31
+ 'max_tokens',
32
+ ]);
33
+ // ─── Config Validator ────────────────────────────────────────────────────────
34
+ /**
35
+ * Validate an RLM configuration.
36
+ * Returns issues rather than throwing, allowing callers to handle gracefully.
37
+ */
38
+ function validateConfig(config) {
39
+ const issues = [];
40
+ // Check for unknown keys (likely typos)
41
+ for (const key of Object.keys(config)) {
42
+ if (!KNOWN_CONFIG_KEYS.has(key)) {
43
+ issues.push({
44
+ level: 'warning',
45
+ field: key,
46
+ message: `Unknown config key "${key}". This may be a typo. Known keys: ${Array.from(KNOWN_CONFIG_KEYS).join(', ')}`,
47
+ });
48
+ }
49
+ }
50
+ // Validate numeric fields
51
+ if (config.max_depth !== undefined) {
52
+ if (typeof config.max_depth !== 'number' || config.max_depth < 1) {
53
+ issues.push({ level: 'error', field: 'max_depth', message: 'max_depth must be a positive integer' });
54
+ }
55
+ }
56
+ if (config.max_iterations !== undefined) {
57
+ if (typeof config.max_iterations !== 'number' || config.max_iterations < 1) {
58
+ issues.push({ level: 'error', field: 'max_iterations', message: 'max_iterations must be a positive integer' });
59
+ }
60
+ }
61
+ if (config.pythonia_timeout !== undefined) {
62
+ if (typeof config.pythonia_timeout !== 'number' || config.pythonia_timeout < 0) {
63
+ issues.push({ level: 'error', field: 'pythonia_timeout', message: 'pythonia_timeout must be a non-negative number (milliseconds)' });
64
+ }
65
+ }
66
+ // Validate API base URL
67
+ if (config.api_base !== undefined && typeof config.api_base === 'string') {
68
+ try {
69
+ new URL(config.api_base);
70
+ }
71
+ catch (_a) {
72
+ issues.push({ level: 'error', field: 'api_base', message: `api_base "${config.api_base}" is not a valid URL` });
73
+ }
74
+ }
75
+ // Validate meta_agent config
76
+ if (config.meta_agent) {
77
+ if (typeof config.meta_agent.enabled !== 'boolean') {
78
+ issues.push({ level: 'error', field: 'meta_agent.enabled', message: 'meta_agent.enabled must be a boolean' });
79
+ }
80
+ if (config.meta_agent.max_optimize_len !== undefined) {
81
+ if (typeof config.meta_agent.max_optimize_len !== 'number' || config.meta_agent.max_optimize_len < 0) {
82
+ issues.push({ level: 'error', field: 'meta_agent.max_optimize_len', message: 'max_optimize_len must be a non-negative number' });
83
+ }
84
+ }
85
+ }
86
+ // Validate observability config
87
+ if (config.observability) {
88
+ const obs = config.observability;
89
+ if (obs.trace_enabled && !obs.trace_endpoint) {
90
+ issues.push({
91
+ level: 'warning',
92
+ field: 'observability.trace_endpoint',
93
+ message: 'trace_enabled is true but no trace_endpoint is configured. Traces will not be exported.',
94
+ });
95
+ }
96
+ if (obs.langfuse_enabled) {
97
+ if (!obs.langfuse_public_key && !process.env.LANGFUSE_PUBLIC_KEY) {
98
+ issues.push({
99
+ level: 'warning',
100
+ field: 'observability.langfuse_public_key',
101
+ message: 'langfuse_enabled is true but no public key is set (config or LANGFUSE_PUBLIC_KEY env var).',
102
+ });
103
+ }
104
+ if (!obs.langfuse_secret_key && !process.env.LANGFUSE_SECRET_KEY) {
105
+ issues.push({
106
+ level: 'warning',
107
+ field: 'observability.langfuse_secret_key',
108
+ message: 'langfuse_enabled is true but no secret key is set (config or LANGFUSE_SECRET_KEY env var).',
109
+ });
110
+ }
111
+ }
112
+ }
113
+ // Validate cache config
114
+ if (config.cache) {
115
+ if (config.cache.maxEntries !== undefined && (typeof config.cache.maxEntries !== 'number' || config.cache.maxEntries < 1)) {
116
+ issues.push({ level: 'error', field: 'cache.maxEntries', message: 'cache.maxEntries must be a positive integer' });
117
+ }
118
+ if (config.cache.ttl !== undefined && (typeof config.cache.ttl !== 'number' || config.cache.ttl < 0)) {
119
+ issues.push({ level: 'error', field: 'cache.ttl', message: 'cache.ttl must be a non-negative number (seconds)' });
120
+ }
121
+ }
122
+ // Validate retry config
123
+ if (config.retry) {
124
+ if (config.retry.maxRetries !== undefined && (typeof config.retry.maxRetries !== 'number' || config.retry.maxRetries < 0)) {
125
+ issues.push({ level: 'error', field: 'retry.maxRetries', message: 'retry.maxRetries must be a non-negative integer' });
126
+ }
127
+ if (config.retry.baseDelay !== undefined && (typeof config.retry.baseDelay !== 'number' || config.retry.baseDelay < 0)) {
128
+ issues.push({ level: 'error', field: 'retry.baseDelay', message: 'retry.baseDelay must be a non-negative number (ms)' });
129
+ }
130
+ }
131
+ // Check for missing API key
132
+ if (!config.api_key && !process.env.OPENAI_API_KEY) {
133
+ issues.push({
134
+ level: 'info',
135
+ field: 'api_key',
136
+ message: 'No API key configured (api_key or OPENAI_API_KEY env var). Ensure it is set before making completions.',
137
+ });
138
+ }
139
+ return {
140
+ valid: issues.filter(i => i.level === 'error').length === 0,
141
+ issues,
142
+ };
143
+ }
144
+ /**
145
+ * Validate config and throw on errors. Logs warnings.
146
+ */
147
+ function assertValidConfig(config) {
148
+ const result = validateConfig(config);
149
+ for (const issue of result.issues) {
150
+ if (issue.level === 'warning') {
151
+ console.warn(`[recursive-llm-ts] Warning: ${issue.message}`);
152
+ }
153
+ }
154
+ const errors = result.issues.filter(i => i.level === 'error');
155
+ if (errors.length > 0) {
156
+ throw new errors_1.RLMConfigError({
157
+ message: `Invalid RLM config: ${errors.map(e => e.message).join('; ')}`,
158
+ field: errors[0].field,
159
+ value: config[errors[0].field],
160
+ });
161
+ }
162
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Structured error hierarchy for recursive-llm-ts.
3
+ *
4
+ * All library errors extend {@link RLMError}, which adds:
5
+ * - `code` – machine-readable error identifier
6
+ * - `retryable` – whether the caller should retry
7
+ * - `suggestion` – human-readable remediation hint
8
+ */
9
+ export declare class RLMError extends Error {
10
+ /** Machine-readable error code (e.g. "RATE_LIMIT", "VALIDATION") */
11
+ readonly code: string;
12
+ /** Whether this error is likely to succeed on retry */
13
+ readonly retryable: boolean;
14
+ /** Human-readable suggestion for resolving the error */
15
+ readonly suggestion?: string;
16
+ constructor(message: string, opts: {
17
+ code: string;
18
+ retryable?: boolean;
19
+ suggestion?: string;
20
+ });
21
+ }
22
+ /** Thrown when a structured output fails schema validation. */
23
+ export declare class RLMValidationError extends RLMError {
24
+ readonly expected: unknown;
25
+ readonly received: unknown;
26
+ readonly zodErrors?: unknown;
27
+ constructor(opts: {
28
+ message: string;
29
+ expected: unknown;
30
+ received: unknown;
31
+ zodErrors?: unknown;
32
+ });
33
+ }
34
+ /** Thrown when the LLM provider returns a 429 rate limit response. */
35
+ export declare class RLMRateLimitError extends RLMError {
36
+ readonly retryAfter?: number;
37
+ constructor(opts: {
38
+ message: string;
39
+ retryAfter?: number;
40
+ });
41
+ }
42
+ /** Thrown when an operation exceeds its time limit. */
43
+ export declare class RLMTimeoutError extends RLMError {
44
+ readonly elapsed: number;
45
+ readonly limit: number;
46
+ constructor(opts: {
47
+ message: string;
48
+ elapsed: number;
49
+ limit: number;
50
+ });
51
+ }
52
+ /** Thrown when the LLM provider returns an HTTP error. */
53
+ export declare class RLMProviderError extends RLMError {
54
+ readonly statusCode: number;
55
+ readonly provider: string;
56
+ readonly responseBody?: string;
57
+ constructor(opts: {
58
+ message: string;
59
+ statusCode: number;
60
+ provider: string;
61
+ responseBody?: string;
62
+ });
63
+ }
64
+ /** Thrown when the Go binary cannot be found or fails to start. */
65
+ export declare class RLMBinaryError extends RLMError {
66
+ readonly binaryPath: string;
67
+ constructor(opts: {
68
+ message: string;
69
+ binaryPath: string;
70
+ });
71
+ }
72
+ /** Thrown when the RLM configuration is invalid. */
73
+ export declare class RLMConfigError extends RLMError {
74
+ readonly field: string;
75
+ readonly value: unknown;
76
+ constructor(opts: {
77
+ message: string;
78
+ field: string;
79
+ value: unknown;
80
+ });
81
+ }
82
+ /** Thrown when a JSON Schema is malformed or unsupported. */
83
+ export declare class RLMSchemaError extends RLMError {
84
+ readonly path: string;
85
+ readonly constraint: string;
86
+ constructor(opts: {
87
+ message: string;
88
+ path: string;
89
+ constraint: string;
90
+ });
91
+ }
92
+ /** Thrown when the request exceeds the model's context window. */
93
+ export declare class RLMContextOverflowError extends RLMError {
94
+ readonly modelLimit: number;
95
+ readonly requestTokens: number;
96
+ constructor(opts: {
97
+ message: string;
98
+ modelLimit: number;
99
+ requestTokens: number;
100
+ });
101
+ }
102
+ /** Thrown when an operation is aborted via AbortController. */
103
+ export declare class RLMAbortError extends RLMError {
104
+ constructor(message?: string);
105
+ }
106
+ /**
107
+ * Classify a raw Error into the appropriate RLM error type.
108
+ * Used internally by the bridge layer to wrap Go binary errors.
109
+ */
110
+ export declare function classifyError(err: Error | string, context?: {
111
+ binaryPath?: string;
112
+ provider?: string;
113
+ }): RLMError;