modelmix 5.0.1 → 5.0.3

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.
Files changed (40) hide show
  1. package/README.md +111 -8
  2. package/RLM_PLUGIN_SPEC.md +465 -0
  3. package/demo/gemini.js +3 -4
  4. package/demo/grok.js +2 -2
  5. package/demo/images.js +2 -2
  6. package/demo/short.js +3 -3
  7. package/effort.js +3 -0
  8. package/index.d.ts +62 -1
  9. package/index.js +355 -49
  10. package/package.json +7 -4
  11. package/plugins/rlm/index.d.ts +194 -0
  12. package/plugins/rlm/index.js +25 -0
  13. package/plugins/rlm/lib/budget.js +153 -0
  14. package/plugins/rlm/lib/isolated-vm-sandbox.js +90 -0
  15. package/plugins/rlm/lib/markdown.js +156 -0
  16. package/plugins/rlm/lib/planner-prompt.js +137 -0
  17. package/plugins/rlm/lib/plugin.js +203 -0
  18. package/plugins/rlm/lib/runtime.js +146 -0
  19. package/plugins/rlm/lib/variable-descriptors.js +228 -0
  20. package/plugins/rlm/lib/worker-catalog.js +70 -0
  21. package/plugins/rlm/package.json +32 -0
  22. package/plugins/rlm/prompts/partials/processing-rules.md +8 -0
  23. package/plugins/rlm/prompts/planner.md +53 -0
  24. package/plugins/rlm/test/budget.test.js +86 -0
  25. package/plugins/rlm/test/fixtures/book.md +24 -0
  26. package/plugins/rlm/test/isolated-vm-sandbox.test.js +114 -0
  27. package/plugins/rlm/test/markdown.test.js +64 -0
  28. package/plugins/rlm/test/planner-template.test.js +140 -0
  29. package/plugins/rlm/test/plugin-contract.test.js +182 -0
  30. package/plugins/rlm/test/rlm-e2e.test.js +338 -0
  31. package/plugins/rlm/test/variable-descriptors.test.js +170 -0
  32. package/plugins/rlm/test/worker-catalog.test.js +104 -0
  33. package/pnpm-workspace.yaml +6 -0
  34. package/skills/modelmix/SKILL.md +23 -4
  35. package/test/effort.test.js +14 -1
  36. package/test/grok.test.js +74 -0
  37. package/test/live.mcp.js +8 -8
  38. package/test/live.test.js +9 -9
  39. package/test/plugins.test.js +356 -0
  40. package/test/tokens.test.js +37 -5
@@ -0,0 +1,194 @@
1
+ export interface NumericStats {
2
+ min: number;
3
+ max: number;
4
+ average: number;
5
+ total: number;
6
+ }
7
+
8
+ export interface StringSizeStats {
9
+ characters: NumericStats;
10
+ utf8Bytes: NumericStats;
11
+ lines: NumericStats;
12
+ paragraphs: NumericStats;
13
+ }
14
+
15
+ export interface VariableDescriptor {
16
+ path: string;
17
+ type: string;
18
+ estimatedBytes: number;
19
+ characters?: number;
20
+ utf16CodeUnits?: number;
21
+ utf8Bytes?: number;
22
+ lines?: number;
23
+ paragraphs?: number;
24
+ items?: number;
25
+ itemSize?: NumericStats;
26
+ itemShape?: Record<string, unknown>;
27
+ properties?: number;
28
+ children?: Record<string, VariableDescriptor>;
29
+ }
30
+
31
+ export interface VariableManifest {
32
+ sizeBasis: 'serialized-json-utf8';
33
+ variables: number;
34
+ estimatedBytes: number;
35
+ descriptors: Record<string, VariableDescriptor>;
36
+ }
37
+
38
+ export interface PlannerLimits {
39
+ maxQueryBytes: number;
40
+ sandboxMemoryBytes: number;
41
+ maxConcurrentQueries: number;
42
+ maxCalls: number;
43
+ maxOutputBytes: number;
44
+ maxGeneratedTokens: number;
45
+ maxWallTimeMs: number;
46
+ }
47
+
48
+ export interface MarkdownListItem {
49
+ text: string;
50
+ source: string;
51
+ lists: MarkdownList[];
52
+ }
53
+
54
+ export interface MarkdownList {
55
+ ordered: boolean;
56
+ start: number | null;
57
+ items: MarkdownListItem[];
58
+ }
59
+
60
+ export interface MarkdownSection {
61
+ id: string;
62
+ path: string[];
63
+ title: string;
64
+ depth: number;
65
+ order: number;
66
+ heading: string;
67
+ body: string;
68
+ lists: MarkdownList[];
69
+ children: MarkdownSection[];
70
+ }
71
+
72
+ export interface MarkdownDocument {
73
+ format: 'markdown';
74
+ preamble: {
75
+ source: string;
76
+ lists: MarkdownList[];
77
+ };
78
+ sections: MarkdownSection[];
79
+ stats: {
80
+ characters: number;
81
+ utf16CodeUnits: number;
82
+ utf8Bytes: number;
83
+ lines: number;
84
+ sectionCount: number;
85
+ };
86
+ }
87
+
88
+ export declare function parseMarkdownDocument(source: string): Promise<MarkdownDocument>;
89
+
90
+ export declare function reconstructMarkdownDocument(document: MarkdownDocument): string;
91
+
92
+ export declare function describeVariables(
93
+ variables: Record<string, unknown>
94
+ ): VariableManifest;
95
+
96
+ export interface PlannerTemplateData {
97
+ variableManifest: string;
98
+ processingLimits: string;
99
+ planningHints: string;
100
+ workerManifest: string;
101
+ outputRequirements: string;
102
+ maxQueryBytes: number;
103
+ maxConcurrentQueries: number;
104
+ }
105
+
106
+ export interface PlannerInvocation {
107
+ systemFile: string;
108
+ assign: PlannerTemplateData;
109
+ messages: Array<{
110
+ role: 'user';
111
+ content: Array<{ type: 'text'; text: string }>;
112
+ }>;
113
+ plugins: { exclude: ['rlm'] };
114
+ history: false;
115
+ outputMode: 'raw';
116
+ }
117
+
118
+ export declare function plannerTemplateData(input: {
119
+ variables: Record<string, unknown>;
120
+ limits: PlannerLimits;
121
+ workerManifest: Record<string, unknown>;
122
+ outputMode?: 'message' | 'json' | 'block' | 'raw';
123
+ outputSchema?: Record<string, unknown> | null;
124
+ }): PlannerTemplateData;
125
+
126
+ export declare function createPlannerInvocation(input: {
127
+ task: string;
128
+ variables: Record<string, unknown>;
129
+ limits: PlannerLimits;
130
+ workerManifest: Record<string, unknown>;
131
+ outputMode?: 'message' | 'json' | 'block' | 'raw';
132
+ outputSchema?: Record<string, unknown> | null;
133
+ }): PlannerInvocation;
134
+
135
+ export interface RlmWorkerMetadata {
136
+ intelligence: number;
137
+ cost: number;
138
+ speed: number;
139
+ description: string;
140
+ }
141
+
142
+ export type RlmWorker = RlmWorkerMetadata & (
143
+ | { model: object; useParent?: never }
144
+ | { model?: never; useParent: true }
145
+ );
146
+
147
+ export interface RlmSandbox {
148
+ execute(input: {
149
+ code: string;
150
+ variables: Record<string, unknown>;
151
+ query: (input: {
152
+ worker: string;
153
+ system: string;
154
+ message: string;
155
+ }) => Promise<string>;
156
+ limits: PlannerLimits;
157
+ execution: {
158
+ executionId: string;
159
+ parentExecutionId: string | null;
160
+ depth: number;
161
+ };
162
+ timeoutMs: number;
163
+ }): Promise<unknown>;
164
+ }
165
+
166
+ export declare function createIsolatedVmSandbox(): RlmSandbox;
167
+
168
+ export interface RlmDocumentInput {
169
+ format: 'markdown';
170
+ content: string;
171
+ }
172
+
173
+ export interface RlmOptions {
174
+ maxDepth: number;
175
+ variables?: Record<string, unknown>;
176
+ documents?: Record<string, RlmDocumentInput>;
177
+ workers: Record<string, RlmWorker>;
178
+ limits: PlannerLimits;
179
+ sandbox?: RlmSandbox;
180
+ }
181
+
182
+ export declare class RlmLimitError extends Error {
183
+ limit: string;
184
+ }
185
+
186
+ export declare function createWorkerCatalog(workers: Record<string, RlmWorker>): {
187
+ get(name: string): object | undefined;
188
+ manifest: Record<string, RlmWorkerMetadata>;
189
+ };
190
+
191
+ export declare function rlm(options: RlmOptions): {
192
+ name: 'rlm';
193
+ execute(context: unknown): Promise<Record<string, unknown>>;
194
+ };
@@ -0,0 +1,25 @@
1
+ const { describeVariables } = require('./lib/variable-descriptors');
2
+ const {
3
+ createPlannerInvocation,
4
+ plannerTemplateData
5
+ } = require('./lib/planner-prompt');
6
+ const { rlm } = require('./lib/plugin');
7
+ const { RlmLimitError } = require('./lib/budget');
8
+ const { createIsolatedVmSandbox } = require('./lib/isolated-vm-sandbox');
9
+ const {
10
+ parseMarkdownDocument,
11
+ reconstructMarkdownDocument
12
+ } = require('./lib/markdown');
13
+ const { createWorkerCatalog } = require('./lib/worker-catalog');
14
+
15
+ module.exports = {
16
+ RlmLimitError,
17
+ createIsolatedVmSandbox,
18
+ createWorkerCatalog,
19
+ describeVariables,
20
+ createPlannerInvocation,
21
+ plannerTemplateData,
22
+ parseMarkdownDocument,
23
+ reconstructMarkdownDocument,
24
+ rlm
25
+ };
@@ -0,0 +1,153 @@
1
+ class RlmLimitError extends Error {
2
+ constructor(limit, message) {
3
+ super(message);
4
+ this.name = 'RlmLimitError';
5
+ this.limit = limit;
6
+ }
7
+ }
8
+
9
+ function positiveInteger(value, name) {
10
+ if (!Number.isInteger(value) || value <= 0) {
11
+ throw new TypeError(`${name} must be a positive integer.`);
12
+ }
13
+ return value;
14
+ }
15
+
16
+ function validateRuntimeLimits(limits) {
17
+ if (!limits || typeof limits !== 'object' || Array.isArray(limits)) {
18
+ throw new TypeError('limits must be a plain object.');
19
+ }
20
+ return Object.freeze({
21
+ maxQueryBytes: positiveInteger(limits.maxQueryBytes, 'limits.maxQueryBytes'),
22
+ sandboxMemoryBytes: positiveInteger(limits.sandboxMemoryBytes, 'limits.sandboxMemoryBytes'),
23
+ maxConcurrentQueries: positiveInteger(
24
+ limits.maxConcurrentQueries,
25
+ 'limits.maxConcurrentQueries'
26
+ ),
27
+ maxCalls: positiveInteger(limits.maxCalls, 'limits.maxCalls'),
28
+ maxOutputBytes: positiveInteger(limits.maxOutputBytes, 'limits.maxOutputBytes'),
29
+ maxGeneratedTokens: positiveInteger(
30
+ limits.maxGeneratedTokens,
31
+ 'limits.maxGeneratedTokens'
32
+ ),
33
+ maxWallTimeMs: positiveInteger(limits.maxWallTimeMs, 'limits.maxWallTimeMs')
34
+ });
35
+ }
36
+
37
+ function createRuntimeBudget(limits, now = Date.now) {
38
+ const validated = validateRuntimeLimits(limits);
39
+ const startedAt = now();
40
+ const queue = [];
41
+ let calls = 0;
42
+ let active = 0;
43
+ let peakConcurrency = 0;
44
+ let outputBytes = 0;
45
+ let generatedTokens = 0;
46
+
47
+ const checkTime = () => {
48
+ if (now() - startedAt > validated.maxWallTimeMs) {
49
+ throw new RlmLimitError('maxWallTimeMs', 'RLM wall-time limit exceeded.');
50
+ }
51
+ };
52
+ const acquire = () => new Promise(resolve => {
53
+ if (active < validated.maxConcurrentQueries) {
54
+ active += 1;
55
+ peakConcurrency = Math.max(peakConcurrency, active);
56
+ resolve();
57
+ } else {
58
+ queue.push(resolve);
59
+ }
60
+ });
61
+ const release = () => {
62
+ const next = queue.shift();
63
+ if (next) {
64
+ next();
65
+ } else {
66
+ active -= 1;
67
+ }
68
+ };
69
+
70
+ const accountResult = result => {
71
+ const message = result?.message ?? '';
72
+ const resultBytes = Buffer.byteLength(
73
+ typeof message === 'string' ? message : JSON.stringify(message),
74
+ 'utf8'
75
+ );
76
+ outputBytes += resultBytes;
77
+ generatedTokens += Number.isFinite(result?.tokens?.output)
78
+ ? Math.max(0, result.tokens.output)
79
+ : 0;
80
+ if (outputBytes > validated.maxOutputBytes) {
81
+ throw new RlmLimitError('maxOutputBytes', 'RLM output byte limit exceeded.');
82
+ }
83
+ if (generatedTokens > validated.maxGeneratedTokens) {
84
+ throw new RlmLimitError(
85
+ 'maxGeneratedTokens',
86
+ 'RLM generated-token limit exceeded.'
87
+ );
88
+ }
89
+ };
90
+ const runCall = async ({ payloadBytes, enforcePayload }, operation) => {
91
+ checkTime();
92
+ if (enforcePayload && payloadBytes > validated.maxQueryBytes) {
93
+ throw new RlmLimitError(
94
+ 'maxQueryBytes',
95
+ `RLM query payload is ${payloadBytes} bytes; limit is ${validated.maxQueryBytes}.`
96
+ );
97
+ }
98
+ if (calls >= validated.maxCalls) {
99
+ throw new RlmLimitError('maxCalls', 'RLM call limit exceeded.');
100
+ }
101
+ calls += 1;
102
+ await acquire();
103
+ try {
104
+ checkTime();
105
+ const result = await operation();
106
+ checkTime();
107
+ accountResult(result);
108
+ return result;
109
+ } finally {
110
+ release();
111
+ }
112
+ };
113
+
114
+ return {
115
+ limits: validated,
116
+ assertQueryPayload(payloadBytes) {
117
+ checkTime();
118
+ if (payloadBytes > validated.maxQueryBytes) {
119
+ throw new RlmLimitError(
120
+ 'maxQueryBytes',
121
+ `RLM query payload is ${payloadBytes} bytes; limit is ${validated.maxQueryBytes}.`
122
+ );
123
+ }
124
+ },
125
+ async runQuery({ payloadBytes }, operation) {
126
+ return runCall({ payloadBytes, enforcePayload: true }, operation);
127
+ },
128
+ async runPlanner(operation) {
129
+ return runCall({ payloadBytes: 0, enforcePayload: false }, operation);
130
+ },
131
+ accountFinalOutput(value) {
132
+ const message = typeof value === 'string' ? value : JSON.stringify(value);
133
+ accountResult({ message });
134
+ checkTime();
135
+ },
136
+ snapshot() {
137
+ return {
138
+ calls,
139
+ active,
140
+ peakConcurrency,
141
+ outputBytes,
142
+ generatedTokens,
143
+ elapsedMs: now() - startedAt
144
+ };
145
+ }
146
+ };
147
+ }
148
+
149
+ module.exports = {
150
+ RlmLimitError,
151
+ createRuntimeBudget,
152
+ validateRuntimeLimits
153
+ };
@@ -0,0 +1,90 @@
1
+ const ivm = require('isolated-vm');
2
+ const { RlmLimitError } = require('./budget');
3
+
4
+ const MINIMUM_ISOLATE_MEMORY_BYTES = 8 * 1024 * 1024;
5
+
6
+ function timeoutError() {
7
+ return new RlmLimitError('maxWallTimeMs', 'RLM wall-time limit exceeded.');
8
+ }
9
+
10
+ function memoryError(error) {
11
+ return /memory|heap|array buffer allocation/i.test(error.message);
12
+ }
13
+
14
+ function executionTimeoutError(error) {
15
+ return /timed out|execution terminated/i.test(error.message);
16
+ }
17
+
18
+ function sandboxSource(code) {
19
+ return `'use strict';\n${code}`;
20
+ }
21
+
22
+ function createIsolatedVmSandbox() {
23
+ return {
24
+ async execute({ code, variables, query, limits, timeoutMs }) {
25
+ if (limits.sandboxMemoryBytes < MINIMUM_ISOLATE_MEMORY_BYTES) {
26
+ throw new TypeError(
27
+ `limits.sandboxMemoryBytes must be at least ${MINIMUM_ISOLATE_MEMORY_BYTES}.`
28
+ );
29
+ }
30
+ const isolate = new ivm.Isolate({
31
+ memoryLimit: Math.ceil(limits.sandboxMemoryBytes / (1024 * 1024))
32
+ });
33
+ let timedOut = false;
34
+ const timeout = setTimeout(() => {
35
+ timedOut = true;
36
+ if (!isolate.isDisposed) isolate.dispose();
37
+ }, timeoutMs);
38
+
39
+ try {
40
+ const context = await isolate.createContext();
41
+ const jail = context.global;
42
+ await jail.set(
43
+ 'variables',
44
+ new ivm.ExternalCopy(variables).copyInto()
45
+ );
46
+ await jail.set('__queryReference', new ivm.Reference(query));
47
+ await context.eval(`
48
+ (() => {
49
+ const queryReference = globalThis.__queryReference;
50
+ Object.defineProperty(globalThis, 'query', {
51
+ configurable: false,
52
+ enumerable: true,
53
+ writable: false,
54
+ value(input) {
55
+ return queryReference.apply(undefined, [input], {
56
+ arguments: { copy: true },
57
+ result: { promise: true, copy: true }
58
+ });
59
+ }
60
+ });
61
+ delete globalThis.__queryReference;
62
+ })();
63
+ `, { timeout: timeoutMs });
64
+ return await context.eval(sandboxSource(code), {
65
+ copy: true,
66
+ filename: 'rlm-planner.js',
67
+ promise: true,
68
+ timeout: timeoutMs
69
+ });
70
+ } catch (error) {
71
+ if (memoryError(error)) {
72
+ throw new RlmLimitError(
73
+ 'sandboxMemoryBytes',
74
+ 'RLM sandbox memory limit exceeded.'
75
+ );
76
+ }
77
+ if (timedOut || executionTimeoutError(error)) throw timeoutError();
78
+ throw error;
79
+ } finally {
80
+ clearTimeout(timeout);
81
+ if (!isolate.isDisposed) isolate.dispose();
82
+ }
83
+ }
84
+ };
85
+ }
86
+
87
+ module.exports = {
88
+ MINIMUM_ISOLATE_MEMORY_BYTES,
89
+ createIsolatedVmSandbox
90
+ };
@@ -0,0 +1,156 @@
1
+ let parserPromise;
2
+
3
+ function loadParser() {
4
+ if (!parserPromise) {
5
+ parserPromise = import('mdast-util-from-markdown')
6
+ .then(module => module.fromMarkdown);
7
+ }
8
+ return parserPromise;
9
+ }
10
+
11
+ function nodeText(node) {
12
+ if (typeof node.value === 'string') return node.value;
13
+ if (!Array.isArray(node.children)) return '';
14
+ return node.children.map(nodeText).join('');
15
+ }
16
+
17
+ function sourceSlice(source, node) {
18
+ const start = node.position?.start?.offset;
19
+ const end = node.position?.end?.offset;
20
+ if (!Number.isInteger(start) || !Number.isInteger(end)) return '';
21
+ return source.slice(start, end);
22
+ }
23
+
24
+ function listValue(node, source) {
25
+ return {
26
+ ordered: node.ordered,
27
+ start: node.ordered ? (node.start || 1) : null,
28
+ items: node.children.map(item => {
29
+ const nestedLists = (item.children || [])
30
+ .filter(child => child.type === 'list')
31
+ .map(child => listValue(child, source));
32
+ const text = (item.children || [])
33
+ .filter(child => child.type !== 'list')
34
+ .map(nodeText)
35
+ .join('\n');
36
+ return {
37
+ text,
38
+ source: sourceSlice(source, item),
39
+ lists: nestedLists
40
+ };
41
+ })
42
+ };
43
+ }
44
+
45
+ function listsBetween(children, startIndex, endIndex, source) {
46
+ return children
47
+ .slice(startIndex, endIndex)
48
+ .filter(node => node.type === 'list')
49
+ .map(node => listValue(node, source));
50
+ }
51
+
52
+ function slugify(value) {
53
+ const slug = value
54
+ .normalize('NFKD')
55
+ .replace(/[\u0300-\u036f]/g, '')
56
+ .toLowerCase()
57
+ .replace(/[^a-z0-9]+/g, '-')
58
+ .replace(/^-+|-+$/g, '');
59
+ return slug || 'section';
60
+ }
61
+
62
+ function uniqueId(title, counts) {
63
+ const base = slugify(title);
64
+ const count = (counts.get(base) || 0) + 1;
65
+ counts.set(base, count);
66
+ return count === 1 ? base : `${base}-${count}`;
67
+ }
68
+
69
+ function markdownStats(source, sectionCount) {
70
+ return {
71
+ characters: Array.from(source).length,
72
+ utf16CodeUnits: source.length,
73
+ utf8Bytes: Buffer.byteLength(source, 'utf8'),
74
+ lines: source.length === 0 ? 0 : source.split(/\r\n|\n|\r/).length,
75
+ sectionCount
76
+ };
77
+ }
78
+
79
+ async function parseMarkdownDocument(source) {
80
+ if (typeof source !== 'string') {
81
+ throw new TypeError('Markdown document content must be a string.');
82
+ }
83
+ const fromMarkdown = await loadParser();
84
+ const tree = fromMarkdown(source);
85
+ const headings = [];
86
+ for (let index = 0; index < tree.children.length; index += 1) {
87
+ const node = tree.children[index];
88
+ if (node.type === 'heading') headings.push({ index, node });
89
+ }
90
+
91
+ const firstHeadingOffset = headings[0]?.node.position?.start?.offset ?? source.length;
92
+ const preambleEndIndex = headings[0]?.index ?? tree.children.length;
93
+ const document = {
94
+ format: 'markdown',
95
+ preamble: {
96
+ source: source.slice(0, firstHeadingOffset),
97
+ lists: listsBetween(tree.children, 0, preambleEndIndex, source)
98
+ },
99
+ sections: [],
100
+ stats: markdownStats(source, headings.length)
101
+ };
102
+ const stack = [];
103
+ const counts = new Map();
104
+
105
+ for (let headingIndex = 0; headingIndex < headings.length; headingIndex += 1) {
106
+ const { index, node } = headings[headingIndex];
107
+ const next = headings[headingIndex + 1];
108
+ const headingStart = node.position.start.offset;
109
+ const headingEnd = node.position.end.offset;
110
+ const bodyEnd = next?.node.position.start.offset ?? source.length;
111
+ const bodyEndIndex = next?.index ?? tree.children.length;
112
+ const title = nodeText(node);
113
+ const id = uniqueId(title, counts);
114
+
115
+ while (stack.length > 0 && stack[stack.length - 1].depth >= node.depth) {
116
+ stack.pop();
117
+ }
118
+ const parent = stack[stack.length - 1] || null;
119
+ const section = {
120
+ id,
121
+ path: parent ? [...parent.path, id] : [id],
122
+ title,
123
+ depth: node.depth,
124
+ order: headingIndex,
125
+ heading: source.slice(headingStart, headingEnd),
126
+ body: source.slice(headingEnd, bodyEnd),
127
+ lists: listsBetween(tree.children, index + 1, bodyEndIndex, source),
128
+ children: []
129
+ };
130
+ if (parent) parent.children.push(section);
131
+ else document.sections.push(section);
132
+ stack.push(section);
133
+ }
134
+
135
+ return document;
136
+ }
137
+
138
+ function renderSections(sections) {
139
+ return sections.map(section => (
140
+ section.heading
141
+ + section.body
142
+ + renderSections(section.children)
143
+ )).join('');
144
+ }
145
+
146
+ function reconstructMarkdownDocument(document) {
147
+ if (!document || document.format !== 'markdown' || !Array.isArray(document.sections)) {
148
+ throw new TypeError('document must be a parsed Markdown document.');
149
+ }
150
+ return `${document.preamble?.source || ''}${renderSections(document.sections)}`;
151
+ }
152
+
153
+ module.exports = {
154
+ parseMarkdownDocument,
155
+ reconstructMarkdownDocument
156
+ };