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/cli.ts
ADDED
|
@@ -0,0 +1,1027 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { promises as fs } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
|
|
11
|
+
type RlmBackend = "sdk" | "cli" | "tmux";
|
|
12
|
+
type RlmMode = "auto" | "solve" | "decompose";
|
|
13
|
+
type RlmToolsProfile = "coding" | "read-only";
|
|
14
|
+
|
|
15
|
+
interface CliOptions {
|
|
16
|
+
backend: RlmBackend;
|
|
17
|
+
mode: RlmMode;
|
|
18
|
+
cwd: string;
|
|
19
|
+
toolsProfile: RlmToolsProfile;
|
|
20
|
+
maxDepth: number;
|
|
21
|
+
maxNodes: number;
|
|
22
|
+
maxBranching: number;
|
|
23
|
+
concurrency: number;
|
|
24
|
+
timeoutMs: number;
|
|
25
|
+
json: boolean;
|
|
26
|
+
live: boolean;
|
|
27
|
+
liveRefreshMs: number;
|
|
28
|
+
piBin: string;
|
|
29
|
+
tmuxUseCurrentSession: boolean;
|
|
30
|
+
task: string;
|
|
31
|
+
help: boolean;
|
|
32
|
+
version: boolean;
|
|
33
|
+
model?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ToolStartParams {
|
|
37
|
+
op: "start";
|
|
38
|
+
task: string;
|
|
39
|
+
backend: RlmBackend;
|
|
40
|
+
mode: RlmMode;
|
|
41
|
+
cwd: string;
|
|
42
|
+
toolsProfile: RlmToolsProfile;
|
|
43
|
+
maxDepth: number;
|
|
44
|
+
maxNodes: number;
|
|
45
|
+
maxBranching: number;
|
|
46
|
+
concurrency: number;
|
|
47
|
+
timeoutMs: number;
|
|
48
|
+
async: false;
|
|
49
|
+
tmuxUseCurrentSession: boolean;
|
|
50
|
+
model?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type JsonRecord = Record<string, any>;
|
|
54
|
+
|
|
55
|
+
type ToolContent = { type?: string; text?: string };
|
|
56
|
+
|
|
57
|
+
interface ToolResultPayload {
|
|
58
|
+
content?: ToolContent[];
|
|
59
|
+
details?: JsonRecord;
|
|
60
|
+
[key: string]: any;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface RunResult {
|
|
64
|
+
code: number | null;
|
|
65
|
+
stderr: string;
|
|
66
|
+
toolResult?: ToolResultPayload;
|
|
67
|
+
toolError: boolean;
|
|
68
|
+
toolArgsMatch: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface LiveNode {
|
|
72
|
+
id: string;
|
|
73
|
+
depth: number;
|
|
74
|
+
task: string;
|
|
75
|
+
status: string;
|
|
76
|
+
action?: string;
|
|
77
|
+
reason?: string;
|
|
78
|
+
error?: string;
|
|
79
|
+
parentId?: string;
|
|
80
|
+
children: string[];
|
|
81
|
+
order: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface LiveRunMeta {
|
|
85
|
+
backend?: string;
|
|
86
|
+
mode?: string;
|
|
87
|
+
maxDepth?: number;
|
|
88
|
+
maxNodes?: number;
|
|
89
|
+
maxBranching?: number;
|
|
90
|
+
concurrency?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface LiveRunEnd {
|
|
94
|
+
durationMs?: number;
|
|
95
|
+
nodesVisited?: number;
|
|
96
|
+
maxDepthSeen?: number;
|
|
97
|
+
finalChars?: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface LiveMonitor {
|
|
101
|
+
refreshMs: number;
|
|
102
|
+
runId?: string;
|
|
103
|
+
eventsPath?: string;
|
|
104
|
+
offset: number;
|
|
105
|
+
remainder: string;
|
|
106
|
+
polling: boolean;
|
|
107
|
+
timer?: NodeJS.Timeout;
|
|
108
|
+
nodes: Map<string, LiveNode>;
|
|
109
|
+
nextOrder: number;
|
|
110
|
+
runMeta?: LiveRunMeta;
|
|
111
|
+
runEnd?: LiveRunEnd;
|
|
112
|
+
toolError: boolean;
|
|
113
|
+
lastFrame: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const defaults: Omit<CliOptions, "task" | "help" | "version" | "model"> = {
|
|
117
|
+
backend: "sdk",
|
|
118
|
+
mode: "auto",
|
|
119
|
+
cwd: process.cwd(),
|
|
120
|
+
toolsProfile: "coding",
|
|
121
|
+
maxDepth: 2,
|
|
122
|
+
maxNodes: 24,
|
|
123
|
+
maxBranching: 3,
|
|
124
|
+
concurrency: 2,
|
|
125
|
+
timeoutMs: 180000,
|
|
126
|
+
json: false,
|
|
127
|
+
live: false,
|
|
128
|
+
liveRefreshMs: 250,
|
|
129
|
+
piBin: "pi",
|
|
130
|
+
tmuxUseCurrentSession: false
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const allowedBackends = new Set<RlmBackend>(["sdk", "cli", "tmux"]);
|
|
134
|
+
const allowedModes = new Set<RlmMode>(["auto", "solve", "decompose"]);
|
|
135
|
+
const allowedProfiles = new Set<RlmToolsProfile>(["coding", "read-only"]);
|
|
136
|
+
|
|
137
|
+
async function main(): Promise<void> {
|
|
138
|
+
try {
|
|
139
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
140
|
+
|
|
141
|
+
if (opts.help) {
|
|
142
|
+
printHelp();
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (opts.version) {
|
|
147
|
+
process.stdout.write("pi-rlm-cli/0.1.0\n");
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!opts.task || !opts.task.trim()) {
|
|
152
|
+
fail('Missing task. Pass --task "..." or a positional task string.');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (opts.live && opts.json) {
|
|
156
|
+
fail("--live and --json cannot be used together.");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (opts.live && !process.stdout.isTTY) {
|
|
160
|
+
fail("--live requires a TTY terminal.");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
164
|
+
const extensionPath = resolve(__dirname, "..", "index.ts");
|
|
165
|
+
|
|
166
|
+
const toolParams: ToolStartParams = {
|
|
167
|
+
op: "start",
|
|
168
|
+
task: opts.task,
|
|
169
|
+
backend: opts.backend,
|
|
170
|
+
mode: opts.mode,
|
|
171
|
+
cwd: opts.cwd,
|
|
172
|
+
toolsProfile: opts.toolsProfile,
|
|
173
|
+
maxDepth: opts.maxDepth,
|
|
174
|
+
maxNodes: opts.maxNodes,
|
|
175
|
+
maxBranching: opts.maxBranching,
|
|
176
|
+
concurrency: opts.concurrency,
|
|
177
|
+
timeoutMs: opts.timeoutMs,
|
|
178
|
+
async: false,
|
|
179
|
+
tmuxUseCurrentSession: opts.tmuxUseCurrentSession,
|
|
180
|
+
...(opts.model ? { model: opts.model } : {})
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const prompt = createPrompt(toolParams);
|
|
184
|
+
const args = createPiArgs(extensionPath, prompt);
|
|
185
|
+
|
|
186
|
+
const run: RunResult = opts.live
|
|
187
|
+
? await runPiLive(opts.piBin, args, toolParams, opts.liveRefreshMs)
|
|
188
|
+
: await runPi(opts.piBin, args, toolParams);
|
|
189
|
+
|
|
190
|
+
if (!run.toolResult) {
|
|
191
|
+
const stderr = run.stderr.trim();
|
|
192
|
+
if (stderr) {
|
|
193
|
+
fail(`Failed to capture rlm tool result.\n${stderr}`);
|
|
194
|
+
}
|
|
195
|
+
fail("Failed to capture rlm tool result.");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!run.toolArgsMatch) {
|
|
199
|
+
fail("The underlying pi agent did not execute rlm with the exact requested arguments.");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (opts.json) {
|
|
203
|
+
process.stdout.write(
|
|
204
|
+
`${JSON.stringify(
|
|
205
|
+
{
|
|
206
|
+
ok: !run.toolError,
|
|
207
|
+
params: toolParams,
|
|
208
|
+
result: run.toolResult
|
|
209
|
+
},
|
|
210
|
+
null,
|
|
211
|
+
2
|
|
212
|
+
)}\n`
|
|
213
|
+
);
|
|
214
|
+
} else {
|
|
215
|
+
if (opts.live) {
|
|
216
|
+
process.stdout.write("\n");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const text = extractText(run.toolResult.content);
|
|
220
|
+
if (text) {
|
|
221
|
+
process.stdout.write(`${text}\n`);
|
|
222
|
+
} else {
|
|
223
|
+
process.stdout.write(`${JSON.stringify(run.toolResult, null, 2)}\n`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const failed = run.toolError || run.code !== 0;
|
|
228
|
+
process.exit(failed ? 1 : 0);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function createPrompt(toolParams: ToolStartParams): string {
|
|
235
|
+
return [
|
|
236
|
+
"You MUST call the rlm tool exactly once using these exact arguments.",
|
|
237
|
+
"Do not modify any value. Do not call any other tool.",
|
|
238
|
+
"After the tool call, respond with exactly: __PI_RLM_DONE__",
|
|
239
|
+
"Arguments JSON:",
|
|
240
|
+
JSON.stringify(toolParams)
|
|
241
|
+
].join("\n");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function createPiArgs(extensionPath: string, prompt: string): string[] {
|
|
245
|
+
return [
|
|
246
|
+
"-p",
|
|
247
|
+
"--no-session",
|
|
248
|
+
"--no-extensions",
|
|
249
|
+
"--no-tools",
|
|
250
|
+
"-e",
|
|
251
|
+
extensionPath,
|
|
252
|
+
"--mode",
|
|
253
|
+
"json",
|
|
254
|
+
prompt
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function parseArgs(argv: string[]): CliOptions {
|
|
259
|
+
const opts: CliOptions = {
|
|
260
|
+
...defaults,
|
|
261
|
+
task: "",
|
|
262
|
+
help: false,
|
|
263
|
+
version: false
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const positional: string[] = [];
|
|
267
|
+
|
|
268
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
269
|
+
const arg = argv[i];
|
|
270
|
+
|
|
271
|
+
if (arg === "--help" || arg === "-h") {
|
|
272
|
+
opts.help = true;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (arg === "--version" || arg === "-v") {
|
|
276
|
+
opts.version = true;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (arg === "--json") {
|
|
280
|
+
opts.json = true;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (arg === "--live") {
|
|
284
|
+
opts.live = true;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (arg === "--tmux-current-session") {
|
|
288
|
+
opts.tmuxUseCurrentSession = true;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (arg === "--task") {
|
|
293
|
+
opts.task = expectValue(argv, ++i, "--task");
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (arg.startsWith("--task=")) {
|
|
297
|
+
opts.task = arg.slice("--task=".length);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (arg === "--backend") {
|
|
302
|
+
opts.backend = expectValue(argv, ++i, "--backend") as RlmBackend;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (arg.startsWith("--backend=")) {
|
|
306
|
+
opts.backend = arg.slice("--backend=".length) as RlmBackend;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (arg === "--mode") {
|
|
311
|
+
opts.mode = expectValue(argv, ++i, "--mode") as RlmMode;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (arg.startsWith("--mode=")) {
|
|
315
|
+
opts.mode = arg.slice("--mode=".length) as RlmMode;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (arg === "--model") {
|
|
320
|
+
opts.model = expectValue(argv, ++i, "--model");
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (arg.startsWith("--model=")) {
|
|
324
|
+
opts.model = arg.slice("--model=".length);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (arg === "--cwd") {
|
|
329
|
+
opts.cwd = expectValue(argv, ++i, "--cwd");
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (arg.startsWith("--cwd=")) {
|
|
333
|
+
opts.cwd = arg.slice("--cwd=".length);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (arg === "--tools-profile") {
|
|
338
|
+
opts.toolsProfile = expectValue(argv, ++i, "--tools-profile") as RlmToolsProfile;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (arg.startsWith("--tools-profile=")) {
|
|
342
|
+
opts.toolsProfile = arg.slice("--tools-profile=".length) as RlmToolsProfile;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (arg === "--max-depth") {
|
|
347
|
+
opts.maxDepth = parseIntArg(expectValue(argv, ++i, "--max-depth"), "--max-depth");
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (arg.startsWith("--max-depth=")) {
|
|
351
|
+
opts.maxDepth = parseIntArg(arg.slice("--max-depth=".length), "--max-depth");
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (arg === "--max-nodes") {
|
|
356
|
+
opts.maxNodes = parseIntArg(expectValue(argv, ++i, "--max-nodes"), "--max-nodes");
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (arg.startsWith("--max-nodes=")) {
|
|
360
|
+
opts.maxNodes = parseIntArg(arg.slice("--max-nodes=".length), "--max-nodes");
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (arg === "--max-branching") {
|
|
365
|
+
opts.maxBranching = parseIntArg(expectValue(argv, ++i, "--max-branching"), "--max-branching");
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (arg.startsWith("--max-branching=")) {
|
|
369
|
+
opts.maxBranching = parseIntArg(arg.slice("--max-branching=".length), "--max-branching");
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (arg === "--concurrency") {
|
|
374
|
+
opts.concurrency = parseIntArg(expectValue(argv, ++i, "--concurrency"), "--concurrency");
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (arg.startsWith("--concurrency=")) {
|
|
378
|
+
opts.concurrency = parseIntArg(arg.slice("--concurrency=".length), "--concurrency");
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (arg === "--timeout-ms") {
|
|
383
|
+
opts.timeoutMs = parseIntArg(expectValue(argv, ++i, "--timeout-ms"), "--timeout-ms");
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (arg.startsWith("--timeout-ms=")) {
|
|
387
|
+
opts.timeoutMs = parseIntArg(arg.slice("--timeout-ms=".length), "--timeout-ms");
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (arg === "--live-refresh-ms") {
|
|
392
|
+
opts.liveRefreshMs = parseIntArg(expectValue(argv, ++i, "--live-refresh-ms"), "--live-refresh-ms");
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (arg.startsWith("--live-refresh-ms=")) {
|
|
396
|
+
opts.liveRefreshMs = parseIntArg(
|
|
397
|
+
arg.slice("--live-refresh-ms=".length),
|
|
398
|
+
"--live-refresh-ms"
|
|
399
|
+
);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (arg === "--pi-bin") {
|
|
404
|
+
opts.piBin = expectValue(argv, ++i, "--pi-bin");
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (arg.startsWith("--pi-bin=")) {
|
|
408
|
+
opts.piBin = arg.slice("--pi-bin=".length);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (arg.startsWith("-")) {
|
|
413
|
+
fail(`Unknown argument: ${arg}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
positional.push(arg);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (!opts.task && positional.length > 0) {
|
|
420
|
+
opts.task = positional.join(" ");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!allowedBackends.has(opts.backend)) {
|
|
424
|
+
fail(`Invalid --backend '${opts.backend}'. Expected one of: sdk, cli, tmux`);
|
|
425
|
+
}
|
|
426
|
+
if (!allowedModes.has(opts.mode)) {
|
|
427
|
+
fail(`Invalid --mode '${opts.mode}'. Expected one of: auto, solve, decompose`);
|
|
428
|
+
}
|
|
429
|
+
if (!allowedProfiles.has(opts.toolsProfile)) {
|
|
430
|
+
fail(`Invalid --tools-profile '${opts.toolsProfile}'. Expected one of: coding, read-only`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
ensureRange(opts.maxDepth, 0, 8, "--max-depth");
|
|
434
|
+
ensureRange(opts.maxNodes, 1, 300, "--max-nodes");
|
|
435
|
+
ensureRange(opts.maxBranching, 1, 8, "--max-branching");
|
|
436
|
+
ensureRange(opts.concurrency, 1, 8, "--concurrency");
|
|
437
|
+
ensureRange(opts.timeoutMs, 1000, 3600000, "--timeout-ms");
|
|
438
|
+
ensureRange(opts.liveRefreshMs, 100, 5000, "--live-refresh-ms");
|
|
439
|
+
|
|
440
|
+
return opts;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function expectValue(argv: string[], index: number, flag: string): string {
|
|
444
|
+
const value = argv[index];
|
|
445
|
+
if (!value || value.startsWith("-")) {
|
|
446
|
+
fail(`Missing value for ${flag}`);
|
|
447
|
+
}
|
|
448
|
+
return value;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function parseIntArg(value: string, flag: string): number {
|
|
452
|
+
const parsed = Number.parseInt(value, 10);
|
|
453
|
+
if (!Number.isFinite(parsed)) {
|
|
454
|
+
fail(`Invalid integer for ${flag}: '${value}'`);
|
|
455
|
+
}
|
|
456
|
+
return parsed;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function ensureRange(value: number, min: number, max: number, flag: string): void {
|
|
460
|
+
if (value < min || value > max) {
|
|
461
|
+
fail(`Invalid ${flag} value '${value}'. Expected ${min}..${max}.`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function runPi(command: string, args: string[], expectedToolParams: ToolStartParams): Promise<RunResult> {
|
|
466
|
+
const child = spawn(command, args, {
|
|
467
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
468
|
+
env: process.env
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const state: Omit<RunResult, "code"> = {
|
|
472
|
+
stderr: "",
|
|
473
|
+
toolResult: undefined,
|
|
474
|
+
toolError: false,
|
|
475
|
+
toolArgsMatch: false
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const rl = createInterface({ input: child.stdout });
|
|
479
|
+
rl.on("line", (line: string) => {
|
|
480
|
+
const event = parseJsonLine(line);
|
|
481
|
+
if (!event) return;
|
|
482
|
+
|
|
483
|
+
if (event.type === "tool_execution_start" && event.toolName === "rlm") {
|
|
484
|
+
state.toolArgsMatch = deepEqual(event.args, expectedToolParams);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (event.type === "tool_execution_end" && event.toolName === "rlm" && event.result) {
|
|
488
|
+
state.toolResult = event.result as ToolResultPayload;
|
|
489
|
+
state.toolError = Boolean(event.isError);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
494
|
+
state.stderr += chunk.toString();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const code = await waitForChild(child);
|
|
498
|
+
rl.close();
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
code,
|
|
502
|
+
...state
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function runPiLive(
|
|
507
|
+
command: string,
|
|
508
|
+
args: string[],
|
|
509
|
+
expectedToolParams: ToolStartParams,
|
|
510
|
+
refreshMs: number
|
|
511
|
+
): Promise<RunResult> {
|
|
512
|
+
const child = spawn(command, args, {
|
|
513
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
514
|
+
env: process.env
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const state: Omit<RunResult, "code"> & { runId?: string; liveMonitor: LiveMonitor } = {
|
|
518
|
+
stderr: "",
|
|
519
|
+
toolResult: undefined,
|
|
520
|
+
toolError: false,
|
|
521
|
+
toolArgsMatch: false,
|
|
522
|
+
runId: undefined,
|
|
523
|
+
liveMonitor: createLiveMonitor(refreshMs)
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const rl = createInterface({ input: child.stdout });
|
|
527
|
+
rl.on("line", (line: string) => {
|
|
528
|
+
const event = parseJsonLine(line);
|
|
529
|
+
if (!event) return;
|
|
530
|
+
|
|
531
|
+
if (event.type === "tool_execution_start" && event.toolName === "rlm") {
|
|
532
|
+
state.toolArgsMatch = deepEqual(event.args, expectedToolParams);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (event.type === "tool_execution_update" && event.toolName === "rlm") {
|
|
536
|
+
const runIdFromUpdate = extractRunIdFromUpdateEvent(event);
|
|
537
|
+
if (!state.runId && runIdFromUpdate) {
|
|
538
|
+
state.runId = runIdFromUpdate;
|
|
539
|
+
startLiveMonitor(state.liveMonitor, runIdFromUpdate);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (event.type === "tool_execution_end" && event.toolName === "rlm" && event.result) {
|
|
544
|
+
state.toolResult = event.result as ToolResultPayload;
|
|
545
|
+
state.toolError = Boolean(event.isError);
|
|
546
|
+
state.liveMonitor.toolError = state.toolError;
|
|
547
|
+
|
|
548
|
+
if (!state.runId) {
|
|
549
|
+
const runIdFromResult = extractRunIdFromToolResult(event.result as ToolResultPayload);
|
|
550
|
+
if (runIdFromResult) {
|
|
551
|
+
state.runId = runIdFromResult;
|
|
552
|
+
startLiveMonitor(state.liveMonitor, runIdFromResult);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
559
|
+
state.stderr += chunk.toString();
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const code = await waitForChild(child);
|
|
563
|
+
rl.close();
|
|
564
|
+
await stopLiveMonitor(state.liveMonitor);
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
code,
|
|
568
|
+
stderr: state.stderr,
|
|
569
|
+
toolResult: state.toolResult,
|
|
570
|
+
toolError: state.toolError,
|
|
571
|
+
toolArgsMatch: state.toolArgsMatch
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function waitForChild(child: ReturnType<typeof spawn>): Promise<number | null> {
|
|
576
|
+
return new Promise((resolve, reject) => {
|
|
577
|
+
child.on("error", reject);
|
|
578
|
+
child.on("close", resolve);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function parseJsonLine(line: string): JsonRecord | undefined {
|
|
583
|
+
const trimmed = line.trim();
|
|
584
|
+
if (!trimmed) return undefined;
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
588
|
+
if (!isRecord(parsed)) return undefined;
|
|
589
|
+
return parsed;
|
|
590
|
+
} catch {
|
|
591
|
+
return undefined;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function extractRunIdFromUpdateEvent(event: JsonRecord): string | undefined {
|
|
596
|
+
const partialResult = isRecord(event.partialResult) ? event.partialResult : undefined;
|
|
597
|
+
const text = extractText(partialResult?.content as ToolContent[] | undefined);
|
|
598
|
+
if (!text) return undefined;
|
|
599
|
+
|
|
600
|
+
const match = text.match(/RLM run\s+([a-f0-9]{8})\s+started/i);
|
|
601
|
+
return match?.[1];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function extractRunIdFromToolResult(result: ToolResultPayload): string | undefined {
|
|
605
|
+
const details = isRecord(result?.details) ? result.details : undefined;
|
|
606
|
+
if (typeof details?.run_id === "string") {
|
|
607
|
+
return details.run_id;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const text = extractText(result?.content);
|
|
611
|
+
if (!text) return undefined;
|
|
612
|
+
|
|
613
|
+
const match = text.match(/run_id:\s*([a-f0-9]{8})/i);
|
|
614
|
+
return match?.[1];
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function createLiveMonitor(refreshMs: number): LiveMonitor {
|
|
618
|
+
return {
|
|
619
|
+
refreshMs,
|
|
620
|
+
runId: undefined,
|
|
621
|
+
eventsPath: undefined,
|
|
622
|
+
offset: 0,
|
|
623
|
+
remainder: "",
|
|
624
|
+
polling: false,
|
|
625
|
+
timer: undefined,
|
|
626
|
+
nodes: new Map(),
|
|
627
|
+
nextOrder: 1,
|
|
628
|
+
runMeta: undefined,
|
|
629
|
+
runEnd: undefined,
|
|
630
|
+
toolError: false,
|
|
631
|
+
lastFrame: ""
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function startLiveMonitor(monitor: LiveMonitor, runId: string): void {
|
|
636
|
+
if (monitor.timer) return;
|
|
637
|
+
|
|
638
|
+
monitor.runId = runId;
|
|
639
|
+
monitor.eventsPath = join(tmpdir(), "pi-rlm-runs", runId, "events.jsonl");
|
|
640
|
+
|
|
641
|
+
renderLiveMonitor(monitor, true);
|
|
642
|
+
void pollLiveEvents(monitor);
|
|
643
|
+
|
|
644
|
+
monitor.timer = setInterval(() => {
|
|
645
|
+
void pollLiveEvents(monitor);
|
|
646
|
+
}, monitor.refreshMs);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function stopLiveMonitor(monitor: LiveMonitor): Promise<void> {
|
|
650
|
+
if (monitor.timer) {
|
|
651
|
+
clearInterval(monitor.timer);
|
|
652
|
+
monitor.timer = undefined;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
await pollLiveEvents(monitor);
|
|
656
|
+
renderLiveMonitor(monitor, true);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async function pollLiveEvents(monitor: LiveMonitor): Promise<void> {
|
|
660
|
+
if (!monitor.eventsPath || monitor.polling) return;
|
|
661
|
+
|
|
662
|
+
monitor.polling = true;
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
let stat;
|
|
666
|
+
try {
|
|
667
|
+
stat = await fs.stat(monitor.eventsPath);
|
|
668
|
+
} catch {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (stat.size < monitor.offset) {
|
|
673
|
+
monitor.offset = 0;
|
|
674
|
+
monitor.remainder = "";
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (stat.size === monitor.offset) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const length = stat.size - monitor.offset;
|
|
682
|
+
const handle = await fs.open(monitor.eventsPath, "r");
|
|
683
|
+
let chunk = "";
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
const buffer = Buffer.alloc(length);
|
|
687
|
+
await handle.read(buffer, 0, length, monitor.offset);
|
|
688
|
+
monitor.offset = stat.size;
|
|
689
|
+
chunk = buffer.toString("utf8");
|
|
690
|
+
} finally {
|
|
691
|
+
await handle.close();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
applyLiveChunk(monitor, chunk);
|
|
695
|
+
renderLiveMonitor(monitor);
|
|
696
|
+
} finally {
|
|
697
|
+
monitor.polling = false;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function applyLiveChunk(monitor: LiveMonitor, chunk: string): void {
|
|
702
|
+
const data = `${monitor.remainder}${chunk}`;
|
|
703
|
+
const lines = data.split("\n");
|
|
704
|
+
monitor.remainder = lines.pop() ?? "";
|
|
705
|
+
|
|
706
|
+
for (const line of lines) {
|
|
707
|
+
const event = parseJsonLine(line);
|
|
708
|
+
if (!event) continue;
|
|
709
|
+
applyLiveEvent(monitor, event);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function applyLiveEvent(monitor: LiveMonitor, event: JsonRecord): void {
|
|
714
|
+
if (event.type === "run_start") {
|
|
715
|
+
monitor.runMeta = {
|
|
716
|
+
backend: asString(event.backend),
|
|
717
|
+
mode: asString(event.mode),
|
|
718
|
+
maxDepth: asNumber(event.maxDepth),
|
|
719
|
+
maxNodes: asNumber(event.maxNodes),
|
|
720
|
+
maxBranching: asNumber(event.maxBranching),
|
|
721
|
+
concurrency: asNumber(event.concurrency)
|
|
722
|
+
};
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (event.type === "run_end") {
|
|
727
|
+
monitor.runEnd = {
|
|
728
|
+
durationMs: asNumber(event.durationMs),
|
|
729
|
+
nodesVisited: asNumber(event.nodesVisited),
|
|
730
|
+
maxDepthSeen: asNumber(event.maxDepthSeen),
|
|
731
|
+
finalChars: asNumber(event.finalChars)
|
|
732
|
+
};
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const nodeId = asString(event.nodeId);
|
|
737
|
+
if (!nodeId) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const node = getOrCreateLiveNode(monitor, nodeId);
|
|
742
|
+
|
|
743
|
+
const depth = asNumber(event.depth);
|
|
744
|
+
if (typeof depth === "number") {
|
|
745
|
+
node.depth = depth;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const task = asString(event.task);
|
|
749
|
+
if (task) {
|
|
750
|
+
node.task = task;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (Object.prototype.hasOwnProperty.call(event, "parentId")) {
|
|
754
|
+
linkLiveParent(monitor, node, normalizeParentId(event.parentId));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
switch (event.type) {
|
|
758
|
+
case "node_start":
|
|
759
|
+
node.status = "running";
|
|
760
|
+
break;
|
|
761
|
+
case "node_decompose":
|
|
762
|
+
node.action = "decompose";
|
|
763
|
+
if (typeof event.reason === "string") node.reason = event.reason;
|
|
764
|
+
break;
|
|
765
|
+
case "node_end":
|
|
766
|
+
node.status = "completed";
|
|
767
|
+
if (typeof event.action === "string") node.action = event.action;
|
|
768
|
+
if (typeof event.reason === "string") node.reason = event.reason;
|
|
769
|
+
break;
|
|
770
|
+
case "node_cancelled":
|
|
771
|
+
node.status = "cancelled";
|
|
772
|
+
if (typeof event.error === "string") {
|
|
773
|
+
node.error = event.error;
|
|
774
|
+
node.reason = node.reason || event.error;
|
|
775
|
+
}
|
|
776
|
+
break;
|
|
777
|
+
case "node_error":
|
|
778
|
+
node.status = "failed";
|
|
779
|
+
if (typeof event.error === "string") {
|
|
780
|
+
node.error = event.error;
|
|
781
|
+
}
|
|
782
|
+
break;
|
|
783
|
+
case "node_skipped":
|
|
784
|
+
node.status = "cancelled";
|
|
785
|
+
if (typeof event.reason === "string") {
|
|
786
|
+
node.reason = event.reason;
|
|
787
|
+
}
|
|
788
|
+
break;
|
|
789
|
+
default:
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function getOrCreateLiveNode(monitor: LiveMonitor, nodeId: string): LiveNode {
|
|
795
|
+
let node = monitor.nodes.get(nodeId);
|
|
796
|
+
if (node) return node;
|
|
797
|
+
|
|
798
|
+
node = {
|
|
799
|
+
id: nodeId,
|
|
800
|
+
depth: 0,
|
|
801
|
+
task: "",
|
|
802
|
+
status: "pending",
|
|
803
|
+
action: undefined,
|
|
804
|
+
reason: undefined,
|
|
805
|
+
error: undefined,
|
|
806
|
+
parentId: undefined,
|
|
807
|
+
children: [],
|
|
808
|
+
order: monitor.nextOrder++
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
monitor.nodes.set(nodeId, node);
|
|
812
|
+
return node;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function normalizeParentId(parentId: unknown): string | undefined {
|
|
816
|
+
if (parentId === null || parentId === undefined) return undefined;
|
|
817
|
+
return String(parentId);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function linkLiveParent(monitor: LiveMonitor, node: LiveNode, parentId: string | undefined): void {
|
|
821
|
+
if (node.parentId === parentId) return;
|
|
822
|
+
|
|
823
|
+
if (node.parentId) {
|
|
824
|
+
const previousParent = monitor.nodes.get(node.parentId);
|
|
825
|
+
if (previousParent) {
|
|
826
|
+
previousParent.children = previousParent.children.filter((childId) => childId !== node.id);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
node.parentId = parentId;
|
|
831
|
+
|
|
832
|
+
if (!parentId) return;
|
|
833
|
+
|
|
834
|
+
const parent = getOrCreateLiveNode(monitor, parentId);
|
|
835
|
+
if (!parent.children.includes(node.id)) {
|
|
836
|
+
parent.children.push(node.id);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function renderLiveMonitor(monitor: LiveMonitor, force = false): void {
|
|
841
|
+
const frame = buildLiveFrame(monitor);
|
|
842
|
+
if (!force && frame === monitor.lastFrame) return;
|
|
843
|
+
|
|
844
|
+
monitor.lastFrame = frame;
|
|
845
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
846
|
+
process.stdout.write(frame);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function buildLiveFrame(monitor: LiveMonitor): string {
|
|
850
|
+
const lines: string[] = [];
|
|
851
|
+
|
|
852
|
+
lines.push(`pi-rlm live tree | run_id: ${monitor.runId ?? "(waiting...)"}`);
|
|
853
|
+
|
|
854
|
+
if (monitor.runMeta) {
|
|
855
|
+
lines.push(
|
|
856
|
+
`backend=${monitor.runMeta.backend} mode=${monitor.runMeta.mode} depth<=${monitor.runMeta.maxDepth} nodes<=${monitor.runMeta.maxNodes}`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (monitor.runEnd) {
|
|
861
|
+
const finalStatus = monitor.toolError ? "failed" : "completed";
|
|
862
|
+
lines.push(
|
|
863
|
+
`status=${finalStatus} nodes=${monitor.runEnd.nodesVisited} maxDepthSeen=${monitor.runEnd.maxDepthSeen} durationMs=${monitor.runEnd.durationMs}`
|
|
864
|
+
);
|
|
865
|
+
} else {
|
|
866
|
+
lines.push(`status=running observed_nodes=${monitor.nodes.size}`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
lines.push("");
|
|
870
|
+
|
|
871
|
+
const roots = Array.from(monitor.nodes.values())
|
|
872
|
+
.filter((node) => !node.parentId)
|
|
873
|
+
.sort((a, b) => a.order - b.order);
|
|
874
|
+
|
|
875
|
+
if (roots.length === 0) {
|
|
876
|
+
lines.push("(waiting for node events...)");
|
|
877
|
+
lines.push("\nLegend: [status] (action) task");
|
|
878
|
+
return `${lines.join("\n")}\n`;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const visited = new Set<string>();
|
|
882
|
+
roots.forEach((root, idx) => {
|
|
883
|
+
renderLiveNode(monitor, root.id, "", idx === roots.length - 1, visited, lines);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
lines.push("");
|
|
887
|
+
lines.push("Legend: [status] (action) task");
|
|
888
|
+
return `${lines.join("\n")}\n`;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function renderLiveNode(
|
|
892
|
+
monitor: LiveMonitor,
|
|
893
|
+
nodeId: string,
|
|
894
|
+
prefix: string,
|
|
895
|
+
isLast: boolean,
|
|
896
|
+
visited: Set<string>,
|
|
897
|
+
lines: string[]
|
|
898
|
+
): void {
|
|
899
|
+
const node = monitor.nodes.get(nodeId);
|
|
900
|
+
if (!node) return;
|
|
901
|
+
|
|
902
|
+
const connector = isLast ? "└─" : "├─";
|
|
903
|
+
const action = node.action ? ` (${node.action})` : "";
|
|
904
|
+
const line = `${prefix}${connector} ${node.id} [d=${node.depth}] [${node.status}]${action} ${short(node.task || "(task pending)", 90)}`;
|
|
905
|
+
lines.push(line);
|
|
906
|
+
|
|
907
|
+
const childPrefix = `${prefix}${isLast ? " " : "│ "}`;
|
|
908
|
+
|
|
909
|
+
if (node.reason) {
|
|
910
|
+
lines.push(`${childPrefix}reason: ${short(node.reason, 76)}`);
|
|
911
|
+
}
|
|
912
|
+
if (node.error && node.error !== node.reason) {
|
|
913
|
+
lines.push(`${childPrefix}error: ${short(node.error, 76)}`);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (visited.has(node.id)) {
|
|
917
|
+
lines.push(`${childPrefix}(cycle detected in live event graph)`);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
visited.add(node.id);
|
|
922
|
+
const children = node.children
|
|
923
|
+
.map((childId) => monitor.nodes.get(childId))
|
|
924
|
+
.filter((child): child is LiveNode => Boolean(child))
|
|
925
|
+
.sort((a, b) => a.order - b.order);
|
|
926
|
+
|
|
927
|
+
children.forEach((child, idx) => {
|
|
928
|
+
renderLiveNode(monitor, child.id, childPrefix, idx === children.length - 1, visited, lines);
|
|
929
|
+
});
|
|
930
|
+
visited.delete(node.id);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function short(value: string, maxChars = 80): string {
|
|
934
|
+
const text = String(value).replace(/\s+/g, " ").trim();
|
|
935
|
+
if (text.length <= maxChars) return text;
|
|
936
|
+
return `${text.slice(0, maxChars - 1)}…`;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function extractText(content: ToolContent[] | undefined): string {
|
|
940
|
+
if (!Array.isArray(content)) return "";
|
|
941
|
+
return content
|
|
942
|
+
.filter((item) => item && item.type === "text" && typeof item.text === "string")
|
|
943
|
+
.map((item) => item.text as string)
|
|
944
|
+
.join("\n")
|
|
945
|
+
.trim();
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
949
|
+
return typeof value === "object" && value !== null;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function asString(value: unknown): string | undefined {
|
|
953
|
+
return typeof value === "string" ? value : undefined;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function asNumber(value: unknown): number | undefined {
|
|
957
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
961
|
+
if (Object.is(a, b)) return true;
|
|
962
|
+
|
|
963
|
+
if (typeof a !== typeof b) return false;
|
|
964
|
+
if (typeof a !== "object" || a === null || b === null) return false;
|
|
965
|
+
|
|
966
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
967
|
+
|
|
968
|
+
if (Array.isArray(a)) {
|
|
969
|
+
const arrA = a as unknown[];
|
|
970
|
+
const arrB = b as unknown[];
|
|
971
|
+
if (arrA.length !== arrB.length) return false;
|
|
972
|
+
for (let i = 0; i < arrA.length; i += 1) {
|
|
973
|
+
if (!deepEqual(arrA[i], arrB[i])) return false;
|
|
974
|
+
}
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const objA = a as JsonRecord;
|
|
979
|
+
const objB = b as JsonRecord;
|
|
980
|
+
|
|
981
|
+
const aKeys = Object.keys(objA);
|
|
982
|
+
const bKeys = Object.keys(objB);
|
|
983
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
984
|
+
|
|
985
|
+
for (const key of aKeys) {
|
|
986
|
+
if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;
|
|
987
|
+
if (!deepEqual(objA[key], objB[key])) return false;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
return true;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function printHelp(): void {
|
|
994
|
+
process.stdout.write("pi-rlm - run Recursive Language Model tasks directly\n\n");
|
|
995
|
+
process.stdout.write("Usage:\n");
|
|
996
|
+
process.stdout.write(" pi-rlm --task \"Analyze src architecture\" [options]\n");
|
|
997
|
+
process.stdout.write(" pi-rlm \"Analyze src architecture\" [options]\n\n");
|
|
998
|
+
process.stdout.write("Options:\n");
|
|
999
|
+
process.stdout.write(" --backend <sdk|cli|tmux> Backend for subcalls (default: sdk)\n");
|
|
1000
|
+
process.stdout.write(" --mode <auto|solve|decompose> Recursion mode (default: auto)\n");
|
|
1001
|
+
process.stdout.write(" --model <provider/model[:thinking]> Optional model override\n");
|
|
1002
|
+
process.stdout.write(" --cwd <path> Working directory for subcalls (default: current dir)\n");
|
|
1003
|
+
process.stdout.write(" --tools-profile <coding|read-only> Tool profile (default: coding)\n");
|
|
1004
|
+
process.stdout.write(" --max-depth <n> Max recursion depth (default: 2)\n");
|
|
1005
|
+
process.stdout.write(" --max-nodes <n> Max total nodes (default: 24)\n");
|
|
1006
|
+
process.stdout.write(" --max-branching <n> Max subtasks per decomposition (default: 3)\n");
|
|
1007
|
+
process.stdout.write(" --concurrency <n> Child concurrency (default: 2)\n");
|
|
1008
|
+
process.stdout.write(" --timeout-ms <n> Timeout per model call (default: 180000)\n");
|
|
1009
|
+
process.stdout.write(" --live Show live tree visualization (TTY only)\n");
|
|
1010
|
+
process.stdout.write(" --live-refresh-ms <n> Live refresh interval in ms (default: 250)\n");
|
|
1011
|
+
process.stdout.write(" --tmux-current-session For backend=tmux, use current tmux session windows\n");
|
|
1012
|
+
process.stdout.write(" --json Print machine-readable JSON\n");
|
|
1013
|
+
process.stdout.write(" --pi-bin <path> Override pi binary path (default: pi)\n");
|
|
1014
|
+
process.stdout.write(" -h, --help Show help\n");
|
|
1015
|
+
process.stdout.write(" -v, --version Show version\n\n");
|
|
1016
|
+
process.stdout.write("Notes:\n");
|
|
1017
|
+
process.stdout.write(" - This wrapper runs a single synchronous rlm start operation.\n");
|
|
1018
|
+
process.stdout.write(" - It shells out to the installed 'pi' CLI and loads this extension.\n");
|
|
1019
|
+
process.stdout.write(" - --live reads /tmp/pi-rlm-runs/<runId>/events.jsonl for real-time tree updates.\n");
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function fail(message: string): never {
|
|
1023
|
+
process.stderr.write(`pi-rlm: ${message}\n`);
|
|
1024
|
+
process.exit(1);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
void main();
|