pi-rlm 0.1.0 → 0.1.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.
- package/README.md +56 -1
- package/bin/pi-rlm.mjs +794 -0
- package/index.ts +2 -1
- package/package.json +8 -1
- package/src/backends.ts +473 -19
- package/src/cli.ts +1027 -0
- package/src/engine.ts +87 -17
- package/src/runs.ts +5 -1
- package/src/schema.ts +6 -1
- package/src/types.ts +1 -0
package/src/engine.ts
CHANGED
|
@@ -47,7 +47,7 @@ export async function runRlmEngine(
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
try {
|
|
50
|
-
const root = await runNode({ task: input.task, depth: 0, lineage: [] });
|
|
50
|
+
const root = await runNode({ task: input.task, depth: 0, lineage: [], parentId: undefined });
|
|
51
51
|
|
|
52
52
|
const finalOutput = root.result ?? "(no final output)";
|
|
53
53
|
|
|
@@ -90,7 +90,36 @@ export async function runRlmEngine(
|
|
|
90
90
|
task: string;
|
|
91
91
|
depth: number;
|
|
92
92
|
lineage: string[];
|
|
93
|
+
parentId: string | undefined;
|
|
93
94
|
}): Promise<RlmNode> {
|
|
95
|
+
if (state.nodesVisited >= input.maxNodes) {
|
|
96
|
+
const startedAt = Date.now();
|
|
97
|
+
const skippedNode: RlmNode = {
|
|
98
|
+
id: `n${++state.nodeCounter}`,
|
|
99
|
+
depth: params.depth,
|
|
100
|
+
task: params.task,
|
|
101
|
+
status: "cancelled",
|
|
102
|
+
startedAt,
|
|
103
|
+
finishedAt: startedAt,
|
|
104
|
+
error: "maxNodes reached",
|
|
105
|
+
result: "Node skipped: maxNodes reached",
|
|
106
|
+
children: []
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
progress?.(
|
|
110
|
+
`[${skippedNode.id}] skipped (maxNodes reached) ${shortTask(params.task, 72)}`
|
|
111
|
+
);
|
|
112
|
+
log("node_skipped", {
|
|
113
|
+
nodeId: skippedNode.id,
|
|
114
|
+
parentId: params.parentId ?? null,
|
|
115
|
+
depth: skippedNode.depth,
|
|
116
|
+
task: skippedNode.task,
|
|
117
|
+
reason: "maxNodes reached",
|
|
118
|
+
nodesVisited: state.nodesVisited
|
|
119
|
+
});
|
|
120
|
+
return skippedNode;
|
|
121
|
+
}
|
|
122
|
+
|
|
94
123
|
const nodeId = `n${++state.nodeCounter}`;
|
|
95
124
|
state.nodesVisited += 1;
|
|
96
125
|
state.maxDepthSeen = Math.max(state.maxDepthSeen, params.depth);
|
|
@@ -107,6 +136,7 @@ export async function runRlmEngine(
|
|
|
107
136
|
progress?.(`[${node.id}] depth=${params.depth} ${shortTask(params.task, 72)}`);
|
|
108
137
|
log("node_start", {
|
|
109
138
|
nodeId: node.id,
|
|
139
|
+
parentId: params.parentId ?? null,
|
|
110
140
|
depth: params.depth,
|
|
111
141
|
task: params.task,
|
|
112
142
|
nodesVisited: state.nodesVisited
|
|
@@ -116,7 +146,11 @@ export async function runRlmEngine(
|
|
|
116
146
|
node.status = "cancelled";
|
|
117
147
|
node.error = "Run cancelled";
|
|
118
148
|
node.finishedAt = Date.now();
|
|
119
|
-
log("node_cancelled", {
|
|
149
|
+
log("node_cancelled", {
|
|
150
|
+
nodeId: node.id,
|
|
151
|
+
parentId: params.parentId ?? null,
|
|
152
|
+
depth: node.depth
|
|
153
|
+
});
|
|
120
154
|
throw new Error("RLM run cancelled");
|
|
121
155
|
}
|
|
122
156
|
|
|
@@ -127,7 +161,8 @@ export async function runRlmEngine(
|
|
|
127
161
|
const forcedReason = getForcedSolveReason({
|
|
128
162
|
depth: params.depth,
|
|
129
163
|
normalizedTask: normalized,
|
|
130
|
-
lineage: params.lineage
|
|
164
|
+
lineage: params.lineage,
|
|
165
|
+
remainingNodeBudget
|
|
131
166
|
});
|
|
132
167
|
|
|
133
168
|
if (forcedReason || input.mode === "solve") {
|
|
@@ -138,6 +173,7 @@ export async function runRlmEngine(
|
|
|
138
173
|
node.finishedAt = Date.now();
|
|
139
174
|
log("node_end", {
|
|
140
175
|
nodeId: node.id,
|
|
176
|
+
parentId: params.parentId ?? null,
|
|
141
177
|
action: "solve",
|
|
142
178
|
reason,
|
|
143
179
|
chars: node.result.length,
|
|
@@ -148,6 +184,7 @@ export async function runRlmEngine(
|
|
|
148
184
|
|
|
149
185
|
const decision = await planNode({
|
|
150
186
|
task: params.task,
|
|
187
|
+
nodeId: node.id,
|
|
151
188
|
depth: params.depth,
|
|
152
189
|
maxDepth: input.maxDepth,
|
|
153
190
|
maxBranching: input.maxBranching,
|
|
@@ -165,6 +202,7 @@ export async function runRlmEngine(
|
|
|
165
202
|
node.finishedAt = Date.now();
|
|
166
203
|
log("node_end", {
|
|
167
204
|
nodeId: node.id,
|
|
205
|
+
parentId: params.parentId ?? null,
|
|
168
206
|
action: "solve",
|
|
169
207
|
reason: decision.reason,
|
|
170
208
|
chars: node.result.length,
|
|
@@ -173,21 +211,33 @@ export async function runRlmEngine(
|
|
|
173
211
|
return node;
|
|
174
212
|
}
|
|
175
213
|
|
|
176
|
-
const
|
|
214
|
+
const requestedSubtasks = sanitizeSubtasks(decision.subtasks ?? [], params.task).slice(
|
|
177
215
|
0,
|
|
178
216
|
input.maxBranching
|
|
179
217
|
);
|
|
218
|
+
const remainingChildBudget = Math.max(0, input.maxNodes - state.nodesVisited);
|
|
219
|
+
const subtasks = requestedSubtasks.slice(0, remainingChildBudget);
|
|
180
220
|
|
|
181
221
|
if (subtasks.length < 2) {
|
|
222
|
+
const fallbackReason =
|
|
223
|
+
requestedSubtasks.length < 2
|
|
224
|
+
? "planner returned insufficient valid subtasks"
|
|
225
|
+
: "insufficient remaining node budget for decomposition";
|
|
226
|
+
|
|
227
|
+
if (input.mode === "decompose") {
|
|
228
|
+
throw new Error(`mode=decompose requires valid decomposition: ${fallbackReason}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
182
231
|
node.decision = {
|
|
183
232
|
action: "solve",
|
|
184
|
-
reason:
|
|
233
|
+
reason: fallbackReason
|
|
185
234
|
};
|
|
186
235
|
node.result = await solveNode(node, node.decision.reason);
|
|
187
236
|
node.status = "completed";
|
|
188
237
|
node.finishedAt = Date.now();
|
|
189
238
|
log("node_end", {
|
|
190
239
|
nodeId: node.id,
|
|
240
|
+
parentId: params.parentId ?? null,
|
|
191
241
|
action: "solve",
|
|
192
242
|
reason: node.decision.reason,
|
|
193
243
|
chars: node.result.length,
|
|
@@ -199,6 +249,7 @@ export async function runRlmEngine(
|
|
|
199
249
|
progress?.(`[${node.id}] decomposing into ${subtasks.length} subtasks`);
|
|
200
250
|
log("node_decompose", {
|
|
201
251
|
nodeId: node.id,
|
|
252
|
+
parentId: params.parentId ?? null,
|
|
202
253
|
subtasks,
|
|
203
254
|
reason: decision.reason
|
|
204
255
|
});
|
|
@@ -207,7 +258,8 @@ export async function runRlmEngine(
|
|
|
207
258
|
return runNode({
|
|
208
259
|
task: subtask,
|
|
209
260
|
depth: params.depth + 1,
|
|
210
|
-
lineage: [...params.lineage, normalized]
|
|
261
|
+
lineage: [...params.lineage, normalized],
|
|
262
|
+
parentId: node.id
|
|
211
263
|
});
|
|
212
264
|
});
|
|
213
265
|
|
|
@@ -216,6 +268,7 @@ export async function runRlmEngine(
|
|
|
216
268
|
node.finishedAt = Date.now();
|
|
217
269
|
log("node_end", {
|
|
218
270
|
nodeId: node.id,
|
|
271
|
+
parentId: params.parentId ?? null,
|
|
219
272
|
action: "decompose",
|
|
220
273
|
chars: node.result.length,
|
|
221
274
|
children: node.children.length,
|
|
@@ -230,6 +283,7 @@ export async function runRlmEngine(
|
|
|
230
283
|
node.finishedAt = Date.now();
|
|
231
284
|
log("node_cancelled", {
|
|
232
285
|
nodeId: node.id,
|
|
286
|
+
parentId: params.parentId ?? null,
|
|
233
287
|
error: message,
|
|
234
288
|
durationMs: node.finishedAt - node.startedAt
|
|
235
289
|
});
|
|
@@ -241,6 +295,7 @@ export async function runRlmEngine(
|
|
|
241
295
|
node.finishedAt = Date.now();
|
|
242
296
|
log("node_error", {
|
|
243
297
|
nodeId: node.id,
|
|
298
|
+
parentId: params.parentId ?? null,
|
|
244
299
|
error: message,
|
|
245
300
|
durationMs: node.finishedAt - node.startedAt
|
|
246
301
|
});
|
|
@@ -252,25 +307,25 @@ export async function runRlmEngine(
|
|
|
252
307
|
|
|
253
308
|
async function planNode(args: {
|
|
254
309
|
task: string;
|
|
310
|
+
nodeId: string;
|
|
255
311
|
depth: number;
|
|
256
312
|
maxDepth: number;
|
|
257
313
|
maxBranching: number;
|
|
258
314
|
remainingNodeBudget: number;
|
|
259
315
|
}): Promise<PlannerDecision> {
|
|
260
316
|
if (input.mode === "decompose") {
|
|
261
|
-
const forced = await callModel("planner", plannerPrompt(args));
|
|
317
|
+
const forced = await callModel("planner", plannerPrompt(args), args.nodeId, args.depth);
|
|
262
318
|
const parsedForced = parsePlannerDecision(forced);
|
|
263
319
|
if (parsedForced.action === "decompose") {
|
|
264
320
|
return parsedForced;
|
|
265
321
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
};
|
|
322
|
+
|
|
323
|
+
throw new Error(
|
|
324
|
+
`mode=decompose requires planner action=decompose; got ${parsedForced.action} (${parsedForced.reason})`
|
|
325
|
+
);
|
|
271
326
|
}
|
|
272
327
|
|
|
273
|
-
const raw = await callModel("planner", plannerPrompt(args));
|
|
328
|
+
const raw = await callModel("planner", plannerPrompt(args), args.nodeId, args.depth);
|
|
274
329
|
return parsePlannerDecision(raw);
|
|
275
330
|
}
|
|
276
331
|
|
|
@@ -281,7 +336,7 @@ export async function runRlmEngine(
|
|
|
281
336
|
maxDepth: input.maxDepth,
|
|
282
337
|
forceReason
|
|
283
338
|
});
|
|
284
|
-
return callModel("solver", prompt, node.id);
|
|
339
|
+
return callModel("solver", prompt, node.id, node.depth);
|
|
285
340
|
}
|
|
286
341
|
|
|
287
342
|
async function synthesizeNode(node: RlmNode): Promise<string> {
|
|
@@ -290,10 +345,15 @@ export async function runRlmEngine(
|
|
|
290
345
|
depth: node.depth,
|
|
291
346
|
children: node.children
|
|
292
347
|
});
|
|
293
|
-
return callModel("synthesizer", prompt, node.id);
|
|
348
|
+
return callModel("synthesizer", prompt, node.id, node.depth);
|
|
294
349
|
}
|
|
295
350
|
|
|
296
|
-
async function callModel(
|
|
351
|
+
async function callModel(
|
|
352
|
+
stage: string,
|
|
353
|
+
promptText: string,
|
|
354
|
+
nodeId?: string,
|
|
355
|
+
depth?: number
|
|
356
|
+
): Promise<string> {
|
|
297
357
|
if (activeSignal.aborted) {
|
|
298
358
|
throw new Error("RLM run cancelled");
|
|
299
359
|
}
|
|
@@ -314,7 +374,12 @@ export async function runRlmEngine(
|
|
|
314
374
|
model: input.model,
|
|
315
375
|
toolsProfile: input.toolsProfile,
|
|
316
376
|
timeoutMs: input.timeoutMs,
|
|
317
|
-
signal: activeSignal
|
|
377
|
+
signal: activeSignal,
|
|
378
|
+
runId: input.runId,
|
|
379
|
+
nodeId,
|
|
380
|
+
depth,
|
|
381
|
+
stage,
|
|
382
|
+
tmuxUseCurrentSession: input.tmuxUseCurrentSession
|
|
318
383
|
},
|
|
319
384
|
ctx
|
|
320
385
|
);
|
|
@@ -332,6 +397,7 @@ export async function runRlmEngine(
|
|
|
332
397
|
depth: number;
|
|
333
398
|
normalizedTask: string;
|
|
334
399
|
lineage: string[];
|
|
400
|
+
remainingNodeBudget: number;
|
|
335
401
|
}): string | undefined {
|
|
336
402
|
if (args.depth >= input.maxDepth) {
|
|
337
403
|
return "maxDepth reached";
|
|
@@ -341,6 +407,10 @@ export async function runRlmEngine(
|
|
|
341
407
|
return "maxNodes reached";
|
|
342
408
|
}
|
|
343
409
|
|
|
410
|
+
if (args.remainingNodeBudget < 2) {
|
|
411
|
+
return "insufficient node budget for decomposition";
|
|
412
|
+
}
|
|
413
|
+
|
|
344
414
|
if (args.lineage.includes(args.normalizedTask)) {
|
|
345
415
|
return "cycle detected in task lineage";
|
|
346
416
|
}
|
package/src/runs.ts
CHANGED
|
@@ -38,7 +38,7 @@ export class RunStore {
|
|
|
38
38
|
promise: Promise.resolve(null as unknown as RlmRunResult)
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
const runPromise = (async () => {
|
|
42
42
|
try {
|
|
43
43
|
const result = await executor(id, controller.signal);
|
|
44
44
|
record.status = "completed";
|
|
@@ -60,6 +60,10 @@ export class RunStore {
|
|
|
60
60
|
}
|
|
61
61
|
})();
|
|
62
62
|
|
|
63
|
+
// Prevent unhandled-rejection crashes when async callers start a run and don't await it.
|
|
64
|
+
void runPromise.catch(() => undefined);
|
|
65
|
+
record.promise = runPromise;
|
|
66
|
+
|
|
63
67
|
this.records.set(id, record);
|
|
64
68
|
this.prune();
|
|
65
69
|
return record;
|
package/src/schema.ts
CHANGED
|
@@ -23,7 +23,12 @@ export const rlmToolParamsSchema = Type.Object({
|
|
|
23
23
|
maxBranching: Type.Optional(Type.Integer({ minimum: 1, maximum: 8 })),
|
|
24
24
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 8 })),
|
|
25
25
|
timeoutMs: Type.Optional(Type.Integer({ minimum: 1000, maximum: 3600000 })),
|
|
26
|
-
waitTimeoutMs: Type.Optional(Type.Integer({ minimum: 100, maximum: 3600000 }))
|
|
26
|
+
waitTimeoutMs: Type.Optional(Type.Integer({ minimum: 100, maximum: 3600000 })),
|
|
27
|
+
tmuxUseCurrentSession: Type.Optional(
|
|
28
|
+
Type.Boolean({
|
|
29
|
+
description: "For backend=tmux, place depth windows/panes in the current tmux session"
|
|
30
|
+
})
|
|
31
|
+
)
|
|
27
32
|
});
|
|
28
33
|
|
|
29
34
|
export type RlmToolParams = Static<typeof rlmToolParamsSchema>;
|