matryoshka-rlm 0.2.30 → 0.2.32
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 +97 -0
- package/dist/adapters/nucleus.d.ts.map +1 -1
- package/dist/adapters/nucleus.js +52 -55
- package/dist/adapters/nucleus.js.map +1 -1
- package/dist/engine/handle-session.d.ts +19 -0
- package/dist/engine/handle-session.d.ts.map +1 -1
- package/dist/engine/handle-session.js +2 -0
- package/dist/engine/handle-session.js.map +1 -1
- package/dist/engine/nucleus-engine.d.ts +24 -0
- package/dist/engine/nucleus-engine.d.ts.map +1 -1
- package/dist/engine/nucleus-engine.js +15 -2
- package/dist/engine/nucleus-engine.js.map +1 -1
- package/dist/fsm/rlm-states.d.ts +31 -0
- package/dist/fsm/rlm-states.d.ts.map +1 -1
- package/dist/fsm/rlm-states.js +246 -7
- package/dist/fsm/rlm-states.js.map +1 -1
- package/dist/lattice-mcp-server.d.ts +7 -1
- package/dist/lattice-mcp-server.d.ts.map +1 -1
- package/dist/lattice-mcp-server.js +123 -2
- package/dist/lattice-mcp-server.js.map +1 -1
- package/dist/logic/lc-interpreter.d.ts.map +1 -1
- package/dist/logic/lc-interpreter.js +6 -1
- package/dist/logic/lc-interpreter.js.map +1 -1
- package/dist/logic/lc-parser.d.ts.map +1 -1
- package/dist/logic/lc-parser.js +129 -1
- package/dist/logic/lc-parser.js.map +1 -1
- package/dist/logic/lc-solver.d.ts +66 -0
- package/dist/logic/lc-solver.d.ts.map +1 -1
- package/dist/logic/lc-solver.js +352 -4
- package/dist/logic/lc-solver.js.map +1 -1
- package/dist/logic/type-inference.d.ts.map +1 -1
- package/dist/logic/type-inference.js +8 -0
- package/dist/logic/type-inference.js.map +1 -1
- package/dist/logic/types.d.ts +118 -2
- package/dist/logic/types.d.ts.map +1 -1
- package/dist/persistence/handle-registry.d.ts +4 -1
- package/dist/persistence/handle-registry.d.ts.map +1 -1
- package/dist/persistence/handle-registry.js +17 -8
- package/dist/persistence/handle-registry.js.map +1 -1
- package/dist/persistence/session-db.d.ts +13 -0
- package/dist/persistence/session-db.d.ts.map +1 -1
- package/dist/persistence/session-db.js +36 -4
- package/dist/persistence/session-db.js.map +1 -1
- package/dist/rlm.d.ts +51 -1
- package/dist/rlm.d.ts.map +1 -1
- package/dist/rlm.js +209 -6
- package/dist/rlm.js.map +1 -1
- package/package.json +1 -1
package/dist/logic/lc-solver.js
CHANGED
|
@@ -19,6 +19,60 @@ import { synthesizeExtractor, compileToFunction, prettyPrint } from "../synthesi
|
|
|
19
19
|
import { synthesizeFromExamples } from "./relational-solver.js";
|
|
20
20
|
import { SynthesisIntegrator } from "./synthesis-integrator.js";
|
|
21
21
|
const moduleQStore = new QValueStore();
|
|
22
|
+
/**
|
|
23
|
+
* Walk the pattern with a paren stack to detect any group that has
|
|
24
|
+
* an internal quantifier AND a trailing quantifier — the canonical
|
|
25
|
+
* shape of catastrophic backtracking, e.g. (a+)+, (a*)+, (.*(.*).*)+,
|
|
26
|
+
* ((a|b)+)+, (?:a+)+. A regex-only check can't see through nested
|
|
27
|
+
* parens; the scanner skips escapes and character classes so it
|
|
28
|
+
* doesn't false-positive on literal `(`/`)`/quantifier chars.
|
|
29
|
+
*/
|
|
30
|
+
function hasNestedQuantifier(pattern) {
|
|
31
|
+
const stack = [];
|
|
32
|
+
const isQuant = (c) => c === "+" || c === "*" || c === "{";
|
|
33
|
+
let i = 0;
|
|
34
|
+
while (i < pattern.length) {
|
|
35
|
+
const c = pattern[i];
|
|
36
|
+
if (c === "\\") {
|
|
37
|
+
i += 2;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (c === "[") {
|
|
41
|
+
// Skip character class — its contents aren't group structure.
|
|
42
|
+
i++;
|
|
43
|
+
while (i < pattern.length && pattern[i] !== "]") {
|
|
44
|
+
if (pattern[i] === "\\")
|
|
45
|
+
i++;
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
i++;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (c === "(") {
|
|
52
|
+
stack.push({ hasQuantInside: false });
|
|
53
|
+
i++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (c === ")") {
|
|
57
|
+
const top = stack.pop();
|
|
58
|
+
i++;
|
|
59
|
+
if (i < pattern.length && isQuant(pattern[i])) {
|
|
60
|
+
if (top?.hasQuantInside)
|
|
61
|
+
return true;
|
|
62
|
+
// Trailing quantifier on this group counts as a quantifier
|
|
63
|
+
// in the parent group's body for the next iteration.
|
|
64
|
+
if (stack.length > 0)
|
|
65
|
+
stack[stack.length - 1].hasQuantInside = true;
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (isQuant(c) && stack.length > 0) {
|
|
70
|
+
stack[stack.length - 1].hasQuantInside = true;
|
|
71
|
+
}
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
22
76
|
/**
|
|
23
77
|
* Validate a regex pattern for safety (ReDoS protection)
|
|
24
78
|
*/
|
|
@@ -31,9 +85,8 @@ export function validateRegex(pattern) {
|
|
|
31
85
|
if (pattern.length > 500) {
|
|
32
86
|
return { valid: false, error: `Regex pattern too long (${pattern.length} chars, max 500)` };
|
|
33
87
|
}
|
|
34
|
-
// Reject nested quantifiers that cause catastrophic backtracking
|
|
35
|
-
|
|
36
|
-
if (/(\((?:[^()]*[+*{])[^()]*\))[+*{]|\(\?[^)]*[+*{][^)]*\)[+*{]/.test(pattern)) {
|
|
88
|
+
// Reject nested quantifiers that cause catastrophic backtracking.
|
|
89
|
+
if (hasNestedQuantifier(pattern)) {
|
|
37
90
|
return { valid: false, error: "Regex contains nested quantifiers which may cause catastrophic backtracking" };
|
|
38
91
|
}
|
|
39
92
|
// Verify the pattern is a valid regex
|
|
@@ -76,6 +129,80 @@ function canonicalizeOneOf(response, oneOf) {
|
|
|
76
129
|
}
|
|
77
130
|
return null;
|
|
78
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Convert a resolved `(context EXPR)` value into a clean line-oriented
|
|
134
|
+
* document for a child Nucleus session.
|
|
135
|
+
*
|
|
136
|
+
* Strings pass through untouched. Arrays join their stringified items
|
|
137
|
+
* by newlines so the child's `^anchor` regexes work — the primary
|
|
138
|
+
* differentiator from the existing llm_query path, which JSON-
|
|
139
|
+
* stringifies the binding and pollutes line starts with `[`/`{`/quotes.
|
|
140
|
+
*
|
|
141
|
+
* Per-item stringification: grep-result-like objects (anything with a
|
|
142
|
+
* string `.line` field) yield that field's value, since that's the
|
|
143
|
+
* "one item per line" representation users expect when piping grep
|
|
144
|
+
* results into a child. Other objects fall back to compact JSON.
|
|
145
|
+
* Strings, numbers, booleans become their natural string form;
|
|
146
|
+
* null/undefined become "".
|
|
147
|
+
*
|
|
148
|
+
* Output is capped at MAX_CONTEXT_DOC_CHARS to bound child memory
|
|
149
|
+
* footprint; longer values are silently truncated. The cap is
|
|
150
|
+
* intentionally larger than llm_query's 500k interpolation cap
|
|
151
|
+
* because rlm_query passes the value as a child DOCUMENT (where the
|
|
152
|
+
* matryoshka 50MB doc limit applies), not as prompt text.
|
|
153
|
+
*/
|
|
154
|
+
const MAX_CONTEXT_DOC_CHARS = 5_000_000;
|
|
155
|
+
function stringifyItem(item) {
|
|
156
|
+
if (item === null || item === undefined)
|
|
157
|
+
return "";
|
|
158
|
+
if (typeof item === "string")
|
|
159
|
+
return item;
|
|
160
|
+
if (typeof item === "number" || typeof item === "boolean")
|
|
161
|
+
return String(item);
|
|
162
|
+
if (typeof item === "object") {
|
|
163
|
+
const obj = item;
|
|
164
|
+
if (typeof obj.line === "string")
|
|
165
|
+
return obj.line;
|
|
166
|
+
try {
|
|
167
|
+
return JSON.stringify(item);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return String(item);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return String(item);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Top-level dispatch for `(rlm_query …)` context materialization.
|
|
177
|
+
* Returns a string; an empty string ("") is a valid output (e.g., the
|
|
178
|
+
* user passed `(context [])` — empty array). Callers MUST distinguish
|
|
179
|
+
* this from `null` (no `(context …)` form supplied) when deciding
|
|
180
|
+
* whether to fall back to using the prompt as the child's document.
|
|
181
|
+
*/
|
|
182
|
+
function materializeContext(value) {
|
|
183
|
+
let out;
|
|
184
|
+
if (value === null || value === undefined) {
|
|
185
|
+
out = "";
|
|
186
|
+
}
|
|
187
|
+
else if (typeof value === "string") {
|
|
188
|
+
out = value;
|
|
189
|
+
}
|
|
190
|
+
else if (Array.isArray(value)) {
|
|
191
|
+
out = value.map(stringifyItem).join("\n");
|
|
192
|
+
}
|
|
193
|
+
else if (typeof value === "number" || typeof value === "boolean") {
|
|
194
|
+
out = String(value);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// Object that isn't an array — use the same line-extraction
|
|
198
|
+
// heuristic as for array items.
|
|
199
|
+
out = stringifyItem(value);
|
|
200
|
+
}
|
|
201
|
+
if (out.length > MAX_CONTEXT_DOC_CHARS) {
|
|
202
|
+
out = out.slice(0, MAX_CONTEXT_DOC_CHARS);
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
79
206
|
/**
|
|
80
207
|
* Solve an LC term using miniKanren as the logic engine
|
|
81
208
|
* @param term The LC term to evaluate
|
|
@@ -176,6 +303,68 @@ async function evaluate(term, tools, bindings, log, depth = 0) {
|
|
|
176
303
|
if (!regexValidation.valid) {
|
|
177
304
|
throw new Error(`Invalid regex pattern: ${regexValidation.error}`);
|
|
178
305
|
}
|
|
306
|
+
// Phase 3 — optional haystack arg. The haystack MUST evaluate
|
|
307
|
+
// to a string. Numbers/booleans get rendered cleanly via
|
|
308
|
+
// String(); arrays/objects/null/undefined are rejected with a
|
|
309
|
+
// specific error so a typo (e.g. `(grep "X" RESULTS)` where
|
|
310
|
+
// RESULTS is the latest grep result array) doesn't silently
|
|
311
|
+
// coerce to "[object Object]" and produce garbage matches.
|
|
312
|
+
// Per project rule: correctness > performance — surface user
|
|
313
|
+
// mistakes loudly.
|
|
314
|
+
if (term.haystack) {
|
|
315
|
+
const hayValue = await evaluate(term.haystack, tools, bindings, log, depth + 1);
|
|
316
|
+
let haystack;
|
|
317
|
+
if (typeof hayValue === "string") {
|
|
318
|
+
haystack = hayValue;
|
|
319
|
+
}
|
|
320
|
+
else if (typeof hayValue === "number" || typeof hayValue === "boolean") {
|
|
321
|
+
haystack = String(hayValue);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const shape = Array.isArray(hayValue)
|
|
325
|
+
? `array(${hayValue.length})`
|
|
326
|
+
: hayValue === null
|
|
327
|
+
? "null"
|
|
328
|
+
: hayValue === undefined
|
|
329
|
+
? "undefined"
|
|
330
|
+
: typeof hayValue;
|
|
331
|
+
throw new Error(`(grep "${term.pattern}" HAYSTACK): haystack must evaluate to a string, got ${shape}. ` +
|
|
332
|
+
`If you have an array, materialize it first (e.g., join lines) or grep an individual element.`);
|
|
333
|
+
}
|
|
334
|
+
log(`[Solver] Executing grep("${pattern}") over haystack (${haystack.length} chars)`);
|
|
335
|
+
if (tools.grepIn) {
|
|
336
|
+
const results = tools.grepIn(pattern, haystack);
|
|
337
|
+
log(`[Solver] Found ${results.length} matches in haystack`);
|
|
338
|
+
return results;
|
|
339
|
+
}
|
|
340
|
+
// Fallback in-line search — same shape as the production
|
|
341
|
+
// grep callback. Used by test stubs that don't provide
|
|
342
|
+
// grepIn AND legacy SolverTools instances. ReDoS validated
|
|
343
|
+
// upstream by the same validateRegex call we already ran.
|
|
344
|
+
const flags = "gmi";
|
|
345
|
+
const regex = new RegExp(pattern, flags);
|
|
346
|
+
const lines = haystack.split("\n");
|
|
347
|
+
const results = [];
|
|
348
|
+
const MAX_HAYSTACK_MATCHES = 10_000;
|
|
349
|
+
let m;
|
|
350
|
+
while ((m = regex.exec(haystack)) !== null) {
|
|
351
|
+
const beforeMatch = haystack.slice(0, m.index);
|
|
352
|
+
const lineNum = (beforeMatch.match(/\n/g) || []).length + 1;
|
|
353
|
+
results.push({
|
|
354
|
+
match: m[0],
|
|
355
|
+
line: lines[lineNum - 1] ?? "",
|
|
356
|
+
lineNum,
|
|
357
|
+
index: m.index,
|
|
358
|
+
groups: m.slice(1).filter((g) => g !== undefined),
|
|
359
|
+
});
|
|
360
|
+
if (m[0].length === 0)
|
|
361
|
+
regex.lastIndex++;
|
|
362
|
+
if (results.length >= MAX_HAYSTACK_MATCHES)
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
log(`[Solver] Found ${results.length} matches in haystack (in-line fallback)`);
|
|
366
|
+
return results;
|
|
367
|
+
}
|
|
179
368
|
log(`[Solver] Executing grep("${pattern}")`);
|
|
180
369
|
const results = tools.grep(pattern);
|
|
181
370
|
log(`[Solver] Found ${results.length} matches`);
|
|
@@ -187,6 +376,71 @@ async function evaluate(term, tools, bindings, log, depth = 0) {
|
|
|
187
376
|
}
|
|
188
377
|
return results;
|
|
189
378
|
}
|
|
379
|
+
case "show_vars": {
|
|
380
|
+
// Phase 4 — binding introspection. Returns a string summary
|
|
381
|
+
// that can be inlined into a sub-LLM prompt, used in a
|
|
382
|
+
// FINAL_VAR substitution, or just passed back to the user as
|
|
383
|
+
// the answer. Format mirrors `lattice_bindings` in spirit but
|
|
384
|
+
// is reachable from inside a query.
|
|
385
|
+
//
|
|
386
|
+
// Internal bindings start with `_` followed by a non-digit
|
|
387
|
+
// character (e.g., `_sessionDB`, `_compaction_trace`). These
|
|
388
|
+
// are private plumbing the user shouldn't see — surfacing
|
|
389
|
+
// them would expose internal state and confuse the LLM about
|
|
390
|
+
// what it can FINAL_VAR-reference. User-visible bindings:
|
|
391
|
+
// RESULTS, plain names, and `_<digits>` (per-turn results).
|
|
392
|
+
const isInternal = (name) => name.startsWith("_") && name.length > 1 && !/^_\d/.test(name);
|
|
393
|
+
const visibleEntries = [...bindings].filter(([n]) => !isInternal(n));
|
|
394
|
+
if (visibleEntries.length === 0) {
|
|
395
|
+
return "No bindings yet — run a query to populate RESULTS or a binding name.";
|
|
396
|
+
}
|
|
397
|
+
const lines = [];
|
|
398
|
+
for (const [name, value] of visibleEntries) {
|
|
399
|
+
let shape;
|
|
400
|
+
if (Array.isArray(value)) {
|
|
401
|
+
shape = `Array(${value.length})`;
|
|
402
|
+
}
|
|
403
|
+
else if (typeof value === "string") {
|
|
404
|
+
shape = `String(${value.length})`;
|
|
405
|
+
}
|
|
406
|
+
else if (typeof value === "number") {
|
|
407
|
+
shape = `Number(${value})`;
|
|
408
|
+
}
|
|
409
|
+
else if (typeof value === "boolean") {
|
|
410
|
+
shape = `Boolean(${value})`;
|
|
411
|
+
}
|
|
412
|
+
else if (value === null) {
|
|
413
|
+
shape = "null";
|
|
414
|
+
}
|
|
415
|
+
else if (value === undefined) {
|
|
416
|
+
shape = "undefined";
|
|
417
|
+
}
|
|
418
|
+
else if (typeof value === "object") {
|
|
419
|
+
shape = "object";
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
shape = typeof value;
|
|
423
|
+
}
|
|
424
|
+
lines.push(` ${name}: ${shape}`);
|
|
425
|
+
}
|
|
426
|
+
return `Bindings (${visibleEntries.length}):\n${lines.join("\n")}`;
|
|
427
|
+
}
|
|
428
|
+
case "context": {
|
|
429
|
+
// Phase 3 — `(context N)` selector. Returns the Nth loaded
|
|
430
|
+
// context's content. Falls back to `tools.context` when
|
|
431
|
+
// `contexts` is unset and the index is 0 — back-compat for
|
|
432
|
+
// single-doc workflows that haven't wired the new field.
|
|
433
|
+
const idx = term.index;
|
|
434
|
+
if (tools.contexts) {
|
|
435
|
+
if (idx < 0 || idx >= tools.contexts.length) {
|
|
436
|
+
throw new Error(`(context ${idx}): index out of range — ${tools.contexts.length} context(s) loaded`);
|
|
437
|
+
}
|
|
438
|
+
return tools.contexts[idx];
|
|
439
|
+
}
|
|
440
|
+
if (idx === 0)
|
|
441
|
+
return tools.context;
|
|
442
|
+
throw new Error(`(context ${idx}): only 1 context loaded — use runRLMFromContent with an array to enable multi-context`);
|
|
443
|
+
}
|
|
190
444
|
case "fuzzy_search": {
|
|
191
445
|
const fuzzyLimit = Math.min(Math.max(1, term.limit ?? 10), 1000);
|
|
192
446
|
log(`[Solver] Executing fuzzy_search("${term.query}", ${fuzzyLimit})`);
|
|
@@ -615,6 +869,9 @@ async function evaluate(term, tools, bindings, log, depth = 0) {
|
|
|
615
869
|
if (typeof str !== "string") {
|
|
616
870
|
throw new Error(`replace: expected string, got ${typeof str}`);
|
|
617
871
|
}
|
|
872
|
+
if (typeof term.to !== "string") {
|
|
873
|
+
throw new Error(`replace: expected string for 'to', got ${typeof term.to}`);
|
|
874
|
+
}
|
|
618
875
|
const replaceValidation = validateRegex(term.from);
|
|
619
876
|
if (!replaceValidation.valid) {
|
|
620
877
|
throw new Error(`replace: ${replaceValidation.error}`);
|
|
@@ -637,7 +894,12 @@ async function evaluate(term, tools, bindings, log, depth = 0) {
|
|
|
637
894
|
if (!term.delim || term.delim.length === 0 || term.delim.length > 1000)
|
|
638
895
|
return null;
|
|
639
896
|
const MAX_SPLIT_PARTS = 10_000;
|
|
640
|
-
|
|
897
|
+
// Bound the split itself, not just the post-allocation check —
|
|
898
|
+
// an unbounded `str.split(delim)` on a multi-MB string with a
|
|
899
|
+
// common delimiter materializes the full part array (potentially
|
|
900
|
+
// millions of strings) before we ever look at .length. Asking
|
|
901
|
+
// for one extra part is enough to detect overflow.
|
|
902
|
+
const parts = str.split(term.delim, MAX_SPLIT_PARTS + 1);
|
|
641
903
|
if (parts.length > MAX_SPLIT_PARTS)
|
|
642
904
|
return null;
|
|
643
905
|
return parts[term.index] ?? null;
|
|
@@ -1184,6 +1446,89 @@ async function evaluate(term, tools, bindings, log, depth = 0) {
|
|
|
1184
1446
|
}
|
|
1185
1447
|
return response;
|
|
1186
1448
|
}
|
|
1449
|
+
case "rlm_query": {
|
|
1450
|
+
// Phase 1 recursive primitive — spawn a child Nucleus FSM with
|
|
1451
|
+
// an explicit query and (optional) clean line-oriented document.
|
|
1452
|
+
// The actual child-spawning logic lives behind the `rlmQuery`
|
|
1453
|
+
// callback the parent's RLM loop wires up; this case is just
|
|
1454
|
+
// the materialization-and-dispatch layer.
|
|
1455
|
+
if (!tools.rlmQuery) {
|
|
1456
|
+
throw new Error("rlm_query is not available in this execution context. " +
|
|
1457
|
+
"The RLM loop provides it once the parent threads an " +
|
|
1458
|
+
"rlmQuery callback through SolverTools. Standalone " +
|
|
1459
|
+
"NucleusEngine / HandleSession instances must pass an " +
|
|
1460
|
+
"rlmQuery option to enable it.");
|
|
1461
|
+
}
|
|
1462
|
+
// Resolve (context EXPR) when present. The materialized form is
|
|
1463
|
+
// line-oriented: arrays join their stringified items by \n;
|
|
1464
|
+
// strings pass through unchanged; null/undefined become null.
|
|
1465
|
+
// This is the structural difference from llm_query — the child
|
|
1466
|
+
// gets a clean document its grep/lines/chunk primitives can
|
|
1467
|
+
// operate on, not a JSON-stringified blob.
|
|
1468
|
+
let contextDoc = null;
|
|
1469
|
+
if (term.context) {
|
|
1470
|
+
const resolved = await evaluate(term.context, tools, bindings, log, depth + 1);
|
|
1471
|
+
contextDoc = materializeContext(resolved);
|
|
1472
|
+
}
|
|
1473
|
+
log(`[Solver] rlm_query prompt length: ${term.prompt.length} chars, ` +
|
|
1474
|
+
`context: ${contextDoc === null ? "null" : `${contextDoc.length} chars`}`);
|
|
1475
|
+
const response = await tools.rlmQuery(term.prompt, contextDoc);
|
|
1476
|
+
log(`[Solver] rlm_query response length: ${response.length} chars`);
|
|
1477
|
+
return response;
|
|
1478
|
+
}
|
|
1479
|
+
case "rlm_batch": {
|
|
1480
|
+
// Phase 2 — concurrent variant of `(map COLL (lambda x (rlm_query …)))`.
|
|
1481
|
+
// Builds N (prompt, contextDoc) pairs from the inner rlm_query
|
|
1482
|
+
// template and dispatches them through tools.rlmBatch in a
|
|
1483
|
+
// single call. The implementation fans them out concurrently
|
|
1484
|
+
// (typically Promise.all over child sessions); the solver
|
|
1485
|
+
// itself is just the materialization-and-dispatch layer.
|
|
1486
|
+
if (!tools.rlmBatch) {
|
|
1487
|
+
throw new Error("rlm_batch is not available in this execution context. " +
|
|
1488
|
+
"The RLM loop provides it once the parent threads an " +
|
|
1489
|
+
"rlmBatch callback through SolverTools. Standalone " +
|
|
1490
|
+
"NucleusEngine / HandleSession instances must pass an " +
|
|
1491
|
+
"rlmBatch option to enable it.");
|
|
1492
|
+
}
|
|
1493
|
+
const collection = await evaluate(term.collection, tools, bindings, log, depth + 1);
|
|
1494
|
+
if (!Array.isArray(collection)) {
|
|
1495
|
+
throw new Error(`rlm_batch: expected array, got ${typeof collection}`);
|
|
1496
|
+
}
|
|
1497
|
+
// Empty collection short-circuit — match llm_batch's behavior
|
|
1498
|
+
// so callers don't pay a dispatch round-trip for zero work.
|
|
1499
|
+
if (collection.length === 0) {
|
|
1500
|
+
log(`[Solver] rlm_batch: empty collection, skipping dispatch`);
|
|
1501
|
+
return [];
|
|
1502
|
+
}
|
|
1503
|
+
const items = [];
|
|
1504
|
+
for (const item of collection) {
|
|
1505
|
+
// Bind the lambda param to the RAW item (no toItemString
|
|
1506
|
+
// coercion). rlm_batch funnels each item through
|
|
1507
|
+
// materializeContext, which knows how to render arrays as
|
|
1508
|
+
// line-oriented documents and objects with a `.line` field
|
|
1509
|
+
// as their line text. Stringifying the item up-front would
|
|
1510
|
+
// collapse all that structure into a flat string and defeat
|
|
1511
|
+
// the handle-as-document property we built for rlm_query.
|
|
1512
|
+
const scopedBindings = new Map(bindings);
|
|
1513
|
+
scopedBindings.set(term.param, item);
|
|
1514
|
+
let contextDoc = null;
|
|
1515
|
+
if (term.context) {
|
|
1516
|
+
const resolved = await evaluate(term.context, tools, scopedBindings, log, depth + 1);
|
|
1517
|
+
contextDoc = materializeContext(resolved);
|
|
1518
|
+
}
|
|
1519
|
+
items.push({ prompt: term.prompt, contextDoc });
|
|
1520
|
+
}
|
|
1521
|
+
log(`[Solver] rlm_batch dispatching ${items.length} child sessions in a single call`);
|
|
1522
|
+
const responses = await tools.rlmBatch(items);
|
|
1523
|
+
if (!Array.isArray(responses)) {
|
|
1524
|
+
throw new Error(`rlm_batch: tools.rlmBatch must return an array, got ${typeof responses}`);
|
|
1525
|
+
}
|
|
1526
|
+
if (responses.length !== items.length) {
|
|
1527
|
+
throw new Error(`rlm_batch: expected ${items.length} responses, got ${responses.length}`);
|
|
1528
|
+
}
|
|
1529
|
+
log(`[Solver] rlm_batch received ${responses.length} responses`);
|
|
1530
|
+
return responses;
|
|
1531
|
+
}
|
|
1187
1532
|
case "llm_batch": {
|
|
1188
1533
|
// Batched sibling of `llm_query`. Drop-in replacement for
|
|
1189
1534
|
// (map COLL (lambda x (llm_query "…" …bindings)))
|
|
@@ -1388,6 +1733,9 @@ async function evaluateWithBinding(body, param, value, tools, bindings, log, dep
|
|
|
1388
1733
|
const str = body.str.tag === "var" && body.str.name === param
|
|
1389
1734
|
? String(value)
|
|
1390
1735
|
: String(await evaluateWithBinding(body.str, param, value, tools, bindings, log, depth + 1));
|
|
1736
|
+
if (typeof body.to !== "string") {
|
|
1737
|
+
throw new Error(`replace: expected string for 'to', got ${typeof body.to}`);
|
|
1738
|
+
}
|
|
1391
1739
|
const replaceVal = validateRegex(body.from);
|
|
1392
1740
|
if (!replaceVal.valid) {
|
|
1393
1741
|
throw new Error(`replace: ${replaceVal.error}`);
|