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/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", { nodeId: node.id, depth: node.depth });
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 subtasks = sanitizeSubtasks(decision.subtasks ?? [], params.task).slice(
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: "planner returned insufficient valid subtasks"
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
- return {
267
- action: "decompose",
268
- reason: "mode=decompose requested, but planner output was invalid",
269
- subtasks: []
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(stage: string, promptText: string, nodeId?: string): Promise<string> {
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
- record.promise = (async () => {
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>;
package/src/types.ts CHANGED
@@ -19,6 +19,7 @@ export interface StartRunInput {
19
19
  maxBranching: number;
20
20
  concurrency: number;
21
21
  timeoutMs: number;
22
+ tmuxUseCurrentSession: boolean;
22
23
  }
23
24
 
24
25
  export interface RlmNode {