@yusukeshib/pi-babysit 0.2.2 → 0.2.4
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 +35 -0
- package/index.ts +244 -16
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -50,6 +50,7 @@ programs** (installers, wizards, REPLs): type with `babysit_send`
|
|
|
50
50
|
| ---- | ------------ |
|
|
51
51
|
| `babysit_run` | Start a process (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`) or a subagent (`profile: "subagent"`, `task`, optional `agent`/`model`/`tools`). Non-blocking; returns a session id |
|
|
52
52
|
| `babysit_check` | List all sessions, or inspect one: process → state + log tail (or `screen: true` for TUIs); subagent → live progress (turns, recent tool calls, partial answer) |
|
|
53
|
+
| `babysit_analyze` | Run a local JavaScript, Python, or shell analyzer over a process's complete captured log; only its bounded report returns to the model |
|
|
53
54
|
| `babysit_send` | Process: type `text` / press `keys` into the PTY. Subagent: steer mid-run, or send a follow-up task when idle (`mode: auto/steer/task`) |
|
|
54
55
|
| `babysit_wait` | Block until done: process exit (or `expect: "regex"` readiness marker), subagent task completion. Multi-wait: `ids` + `mode: "any"\|"all"` |
|
|
55
56
|
| `babysit_kill` | Terminate a session (suppresses the exit notification) |
|
|
@@ -67,6 +68,40 @@ A `tool_call` hook also blocks bash commands that background themselves
|
|
|
67
68
|
A minimal widget above the editor shows live counts
|
|
68
69
|
(`N processes · M subagents working · K idle`).
|
|
69
70
|
|
|
71
|
+
## Log reports without context flooding
|
|
72
|
+
|
|
73
|
+
A process run can include a `report` program. On completion, babysit writes the
|
|
74
|
+
complete recorded log to a temporary file, runs the program in a separate local
|
|
75
|
+
child process, and sends **only the program's bounded stdout** in the automatic
|
|
76
|
+
completion notification. This avoids putting a full test/build log in the model
|
|
77
|
+
context while preserving the original log for `babysit_check`, `/babysit`, or
|
|
78
|
+
`babysit attach`.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
babysit_run({
|
|
82
|
+
name: "test",
|
|
83
|
+
command: "npm test",
|
|
84
|
+
report: {
|
|
85
|
+
language: "javascript",
|
|
86
|
+
code: `
|
|
87
|
+
const failures = FILE_CONTENT.split("\\n")
|
|
88
|
+
.filter(line => /FAIL|Error:|✗/.test(line));
|
|
89
|
+
console.log(`failures: ${failures.length}`);
|
|
90
|
+
console.log(failures.slice(-20).join("\\n"));
|
|
91
|
+
`,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
JavaScript and Python reports receive `FILE_CONTENT` and `INPUT`; shell reports
|
|
97
|
+
read the log path from `$BABYSIT_REPORT_INPUT`. `report.timeout` defaults to
|
|
98
|
+
30 seconds. Input is capped at 64 MiB and returned report output at 12 KB.
|
|
99
|
+
Use `babysit_analyze` with the same `language`, `code`, and optional `timeout`
|
|
100
|
+
fields to analyze a running process's output so far or re-analyze a completed
|
|
101
|
+
process. Report code executes with your local user permissions, just like any
|
|
102
|
+
other extension tool command; it is isolated from the pi extension host but is
|
|
103
|
+
not an OS security sandbox.
|
|
104
|
+
|
|
70
105
|
## How completion detection works
|
|
71
106
|
|
|
72
107
|
- **Process**: a 2.5s poller watches for running→exited transitions and injects
|
package/index.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* surface (babysit_run/check/send/wait/kill) covers both, and domain knowledge
|
|
27
27
|
* (RPC bookkeeping, byte offsets, parked-turn detection) stays in code.
|
|
28
28
|
*
|
|
29
|
-
* Tools (LLM): babysit_run, babysit_check, babysit_send, babysit_wait, babysit_kill
|
|
29
|
+
* Tools (LLM): babysit_run, babysit_check, babysit_analyze, babysit_send, babysit_wait, babysit_kill
|
|
30
30
|
* Commands: /babysit (arrow-key picker: attach/tail/inspect)
|
|
31
31
|
* Widget: live counts (processes running · subagents working · idle)
|
|
32
32
|
*/
|
|
@@ -225,11 +225,20 @@ async function statusOf(id: string): Promise<BsSession | null> {
|
|
|
225
225
|
// kind=process: name/command + `notified` (exit notification dedup).
|
|
226
226
|
// kind=subagent: task + the raw-log byte offset of the last prompt, which lets
|
|
227
227
|
// check/wait analyze only the CURRENT task's events (important for follow-ups).
|
|
228
|
+
type ReportLanguage = "javascript" | "python" | "shell";
|
|
229
|
+
|
|
230
|
+
interface ReportSpec {
|
|
231
|
+
language: ReportLanguage;
|
|
232
|
+
code: string;
|
|
233
|
+
timeout?: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
228
236
|
interface Meta {
|
|
229
237
|
kind: "process" | "subagent";
|
|
230
238
|
// process
|
|
231
239
|
name?: string;
|
|
232
240
|
command?: string;
|
|
241
|
+
report?: ReportSpec;
|
|
233
242
|
notified?: boolean;
|
|
234
243
|
startedAt?: number;
|
|
235
244
|
// subagent
|
|
@@ -366,6 +375,9 @@ function parseDurMs(s?: string): number | null {
|
|
|
366
375
|
|
|
367
376
|
const TAIL_MAX_BYTES = 8_000; // log tails / screens
|
|
368
377
|
const ANSWER_MAX_BYTES = 24_000; // subagent answers / error messages
|
|
378
|
+
const REPORT_MAX_INPUT_BYTES = 64 * 1024 * 1024; // analyzer input log
|
|
379
|
+
const REPORT_MAX_OUTPUT_BYTES = 12_000; // analyzer stdout/stderr returned to the agent
|
|
380
|
+
const REPORT_DEFAULT_TIMEOUT_MS = 30_000;
|
|
369
381
|
|
|
370
382
|
function clip(s: string, maxBytes = TAIL_MAX_BYTES): string {
|
|
371
383
|
const buf = Buffer.from(s, "utf8");
|
|
@@ -377,6 +389,132 @@ function clip(s: string, maxBytes = TAIL_MAX_BYTES): string {
|
|
|
377
389
|
return `${head}\n… [${buf.length - maxBytes} bytes elided] …\n${tail}`;
|
|
378
390
|
}
|
|
379
391
|
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
// Report analyzers — a report receives the complete captured process log, but
|
|
394
|
+
// only its bounded stdout is returned to the model. It intentionally runs in
|
|
395
|
+
// a child process: report code is arbitrary user/model-authored code and must
|
|
396
|
+
// never be evaluated inside pi's extension host.
|
|
397
|
+
|
|
398
|
+
interface ReportOutcome {
|
|
399
|
+
ok: boolean;
|
|
400
|
+
text: string;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function saveSessionLog(id: string, destination: string, signal?: AbortSignal): Promise<string | null> {
|
|
404
|
+
return new Promise((resolve) => {
|
|
405
|
+
const child = spawn(BABYSIT_BIN, ["log", "-s", id], {
|
|
406
|
+
env: { ...process.env, BABYSIT_DIR: ROOT },
|
|
407
|
+
});
|
|
408
|
+
const output = fs.createWriteStream(destination);
|
|
409
|
+
let bytes = 0;
|
|
410
|
+
let finished = false;
|
|
411
|
+
const finish = (error: string | null) => {
|
|
412
|
+
if (finished) return;
|
|
413
|
+
finished = true;
|
|
414
|
+
signal?.removeEventListener("abort", abort);
|
|
415
|
+
output.end(() => resolve(error));
|
|
416
|
+
};
|
|
417
|
+
const abort = () => {
|
|
418
|
+
child.kill("SIGTERM");
|
|
419
|
+
finish("report analysis was cancelled");
|
|
420
|
+
};
|
|
421
|
+
if (signal?.aborted) return abort();
|
|
422
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
423
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
424
|
+
if (finished) return;
|
|
425
|
+
bytes += chunk.length;
|
|
426
|
+
if (bytes > REPORT_MAX_INPUT_BYTES) {
|
|
427
|
+
child.kill("SIGTERM");
|
|
428
|
+
finish(`process log exceeds the ${REPORT_MAX_INPUT_BYTES / 1024 / 1024} MiB report limit`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
output.write(chunk);
|
|
432
|
+
});
|
|
433
|
+
child.on("error", (error) => finish(`could not read process log: ${error.message}`));
|
|
434
|
+
child.on("close", (code) => {
|
|
435
|
+
if (finished) return;
|
|
436
|
+
finish(code === 0 ? null : `could not read process log (babysit log exited ${code ?? 1})`);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function executeReport(spec: ReportSpec, inputPath: string, signal?: AbortSignal): Promise<ReportOutcome> {
|
|
442
|
+
const dir = path.dirname(inputPath);
|
|
443
|
+
const scriptPath = path.join(dir, spec.language === "python" ? "report.py" : spec.language === "shell" ? "report.sh" : "report.cjs");
|
|
444
|
+
const script =
|
|
445
|
+
spec.language === "javascript"
|
|
446
|
+
? `const fs = require("node:fs");\nconst FILE_CONTENT = fs.readFileSync(process.env.BABYSIT_REPORT_INPUT, "utf8");\nconst INPUT = FILE_CONTENT;\n(async () => {\n${spec.code}\n})().catch((error) => { console.error(error?.stack ?? String(error)); process.exitCode = 1; });\n`
|
|
447
|
+
: spec.language === "python"
|
|
448
|
+
? `import os\nfrom pathlib import Path\nFILE_CONTENT = Path(os.environ["BABYSIT_REPORT_INPUT"]).read_text()\nINPUT = FILE_CONTENT\n${spec.code}\n`
|
|
449
|
+
: `# Read the complete log from $BABYSIT_REPORT_INPUT.\n${spec.code}\n`;
|
|
450
|
+
fs.writeFileSync(scriptPath, script, { mode: 0o700 });
|
|
451
|
+
const command = spec.language === "javascript" ? process.execPath : spec.language === "python" ? "python3" : SHELL;
|
|
452
|
+
const args = spec.language === "shell" ? [scriptPath] : [scriptPath];
|
|
453
|
+
const timeoutMs = parseDurMs(spec.timeout) ?? REPORT_DEFAULT_TIMEOUT_MS;
|
|
454
|
+
|
|
455
|
+
return new Promise((resolve) => {
|
|
456
|
+
const child = spawn(command, args, {
|
|
457
|
+
env: { ...process.env, BABYSIT_REPORT_INPUT: inputPath },
|
|
458
|
+
});
|
|
459
|
+
let stdout = "";
|
|
460
|
+
let stderr = "";
|
|
461
|
+
let outputTruncated = false;
|
|
462
|
+
let timedOut = false;
|
|
463
|
+
let settled = false;
|
|
464
|
+
const append = (current: string, chunk: Buffer) => {
|
|
465
|
+
const remaining = REPORT_MAX_OUTPUT_BYTES - Buffer.byteLength(current);
|
|
466
|
+
if (remaining <= 0) {
|
|
467
|
+
outputTruncated = true;
|
|
468
|
+
return current;
|
|
469
|
+
}
|
|
470
|
+
const text = chunk.subarray(0, remaining).toString("utf8");
|
|
471
|
+
if (text.length < chunk.length) outputTruncated = true;
|
|
472
|
+
return current + text;
|
|
473
|
+
};
|
|
474
|
+
const finish = (code: number | null) => {
|
|
475
|
+
if (settled) return;
|
|
476
|
+
settled = true;
|
|
477
|
+
clearTimeout(timer);
|
|
478
|
+
signal?.removeEventListener("abort", abort);
|
|
479
|
+
const body = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
|
|
480
|
+
const suffix = outputTruncated ? "\n[report output truncated]" : "";
|
|
481
|
+
if (timedOut) {
|
|
482
|
+
resolve({ ok: false, text: `Report timed out after ${Math.ceil(timeoutMs / 1000)}s.${body ? `\n${clip(body, REPORT_MAX_OUTPUT_BYTES)}` : ""}${suffix}` });
|
|
483
|
+
} else if (code === 0) {
|
|
484
|
+
resolve({ ok: true, text: `${body || "(report produced no output)"}${suffix}` });
|
|
485
|
+
} else {
|
|
486
|
+
resolve({ ok: false, text: `Report failed (exit ${code ?? 1}).${body ? `\n${clip(body, REPORT_MAX_OUTPUT_BYTES)}` : ""}${suffix}` });
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
const abort = () => child.kill("SIGTERM");
|
|
490
|
+
const timer = setTimeout(() => {
|
|
491
|
+
timedOut = true;
|
|
492
|
+
child.kill("SIGTERM");
|
|
493
|
+
}, timeoutMs);
|
|
494
|
+
if (signal?.aborted) abort();
|
|
495
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
496
|
+
child.stdout?.on("data", (chunk: Buffer) => { stdout = append(stdout, chunk); });
|
|
497
|
+
child.stderr?.on("data", (chunk: Buffer) => { stderr = append(stderr, chunk); });
|
|
498
|
+
child.on("error", (error) => {
|
|
499
|
+
stderr = append(stderr, Buffer.from(error.message));
|
|
500
|
+
finish(1);
|
|
501
|
+
});
|
|
502
|
+
child.on("close", finish);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function runReport(id: string, spec: ReportSpec, signal?: AbortSignal): Promise<ReportOutcome> {
|
|
507
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-babysit-report-"));
|
|
508
|
+
try {
|
|
509
|
+
const inputPath = path.join(dir, "process.log");
|
|
510
|
+
const error = await saveSessionLog(id, inputPath, signal);
|
|
511
|
+
if (error) return { ok: false, text: `Could not prepare report input: ${error}` };
|
|
512
|
+
return await executeReport(spec, inputPath, signal);
|
|
513
|
+
} finally {
|
|
514
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
380
518
|
// ---------------------------------------------------------------------------
|
|
381
519
|
// parked-turn detection (shared rule with self-reap.ts)
|
|
382
520
|
// ---------------------------------------------------------------------------
|
|
@@ -553,6 +691,7 @@ interface ProcOpts {
|
|
|
553
691
|
timeout?: string; // default: none — dev servers may run indefinitely
|
|
554
692
|
idleTimeout?: string;
|
|
555
693
|
pty: boolean;
|
|
694
|
+
report?: ReportSpec;
|
|
556
695
|
}
|
|
557
696
|
|
|
558
697
|
async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: string }> {
|
|
@@ -582,6 +721,7 @@ async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: s
|
|
|
582
721
|
kind: "process",
|
|
583
722
|
name: opts.name ?? id,
|
|
584
723
|
command: opts.command,
|
|
724
|
+
report: opts.report,
|
|
585
725
|
notified: false,
|
|
586
726
|
startedAt: Date.now(),
|
|
587
727
|
});
|
|
@@ -1009,9 +1149,10 @@ async function waitForExit(
|
|
|
1009
1149
|
return { id, kind: "exited", ok: false, text: `No such session: ${id}` };
|
|
1010
1150
|
}
|
|
1011
1151
|
suppressNotify(id); // the agent sees the exit here; don't notify again
|
|
1012
|
-
const tail = clip((await bs(["log", "-s", id, "--tail", "20"])).stdout.trim());
|
|
1013
|
-
const ok = st.exit_code === 0;
|
|
1014
1152
|
const meta = readMeta(id);
|
|
1153
|
+
const report = meta?.report ? await runReport(id, meta.report, signal) : null;
|
|
1154
|
+
const tail = report ? "" : clip((await bs(["log", "-s", id, "--tail", "20"])).stdout.trim());
|
|
1155
|
+
const ok = st.exit_code === 0;
|
|
1015
1156
|
return {
|
|
1016
1157
|
id,
|
|
1017
1158
|
kind: "exited",
|
|
@@ -1020,7 +1161,9 @@ async function waitForExit(
|
|
|
1020
1161
|
`Process ${id}${meta?.command ? ` (${meta.command})` : ""} ` +
|
|
1021
1162
|
`${ok ? "completed successfully" : `exited with code ${st.exit_code ?? "?"}`}` +
|
|
1022
1163
|
`${expectPattern ? ` before /${expectPattern}/ appeared` : ""}.` +
|
|
1023
|
-
(
|
|
1164
|
+
(report
|
|
1165
|
+
? `\n\nReport${report.ok ? "" : " (failed)"}:\n${report.text}`
|
|
1166
|
+
: tail ? `\n\nLast output:\n${tail}` : ""),
|
|
1024
1167
|
status: st,
|
|
1025
1168
|
};
|
|
1026
1169
|
}
|
|
@@ -1069,7 +1212,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1069
1212
|
if (!meta || meta.kind !== "process" || meta.notified) continue;
|
|
1070
1213
|
meta.notified = true;
|
|
1071
1214
|
writeMeta(s.id, meta);
|
|
1072
|
-
const
|
|
1215
|
+
const report = meta.report ? await runReport(s.id, meta.report) : null;
|
|
1216
|
+
const tail = report ? "" : clip((await bs(["log", "-s", s.id, "--tail", "20"])).stdout.trim());
|
|
1073
1217
|
const ok = s.exit_code === 0;
|
|
1074
1218
|
const runtime = meta.startedAt
|
|
1075
1219
|
? `${Math.round((Date.now() - meta.startedAt) / 1000)}s`
|
|
@@ -1084,10 +1228,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1084
1228
|
customType: "pi-babysit-process-end",
|
|
1085
1229
|
content:
|
|
1086
1230
|
`${summary}\nCommand: ${meta.command ?? "?"}` +
|
|
1087
|
-
(
|
|
1231
|
+
(report
|
|
1232
|
+
? `\n\nReport${report.ok ? "" : " (failed)"}:\n${report.text}`
|
|
1233
|
+
: tail ? `\n\nRecent output:\n${tail}` : "") +
|
|
1088
1234
|
`\n\nThis is the automatic process-end notification. Do not call babysit_check just to re-verify; use it once only if you need more logs for debugging.`,
|
|
1089
1235
|
display: true,
|
|
1090
|
-
details: { id: s.id, exitCode: s.exit_code, success: ok, runtime },
|
|
1236
|
+
details: { id: s.id, exitCode: s.exit_code, success: ok, runtime, report },
|
|
1091
1237
|
},
|
|
1092
1238
|
{ triggerTurn: true, deliverAs: "steer" },
|
|
1093
1239
|
);
|
|
@@ -1201,7 +1347,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1201
1347
|
"exits and returns its output inline — there is no notification loop to resume a parked turn. " +
|
|
1202
1348
|
"Two modes: (1) `command` — run any long-lived or slow command (build, tests, dev server, " +
|
|
1203
1349
|
"watcher, interactive TUI) in a PTY; you get an AUTOMATIC notification when it exits, and " +
|
|
1204
|
-
"you can type into it with babysit_send and read its screen with babysit_check. " +
|
|
1350
|
+
"you can type into it with babysit_send and read its screen with babysit_check. Optional `report` " +
|
|
1351
|
+
"runs local code over the full captured log and returns only its concise output on completion. " +
|
|
1205
1352
|
"(2) `profile: \"subagent\"` + `task` — spawn a pi subagent that works on the task in the " +
|
|
1206
1353
|
"background; poll with babysit_check, steer with babysit_send, block with babysit_wait, " +
|
|
1207
1354
|
"stop with babysit_kill.",
|
|
@@ -1209,7 +1356,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1209
1356
|
"Run a command or a pi subagent as a supervised background session (non-blocking); returns a session id",
|
|
1210
1357
|
promptGuidelines: [
|
|
1211
1358
|
"Run every long-lived or slow command through babysit_run instead of bash: builds, test suites, dev servers, watchers, `tail -f`, anything expected to take more than a few seconds. Give it a clear stable `name` (e.g. cargo-build).",
|
|
1212
|
-
"babysit_run { command }
|
|
1359
|
+
"After babysit_run { command } starts a process, end your response immediately so the automatic process-end notification can resume you; NEVER poll with babysit_check or sleep. Set continueAfterStart: true only when you have immediate, specific, non-polling work to do next. Call babysit_wait when you must consume the result inside the current turn (optionally with `expect` to wait for a readiness line like 'listening on').",
|
|
1360
|
+
"For a build/test/API command with potentially large output, give babysit_run a `report` program that extracts the needed facts from FILE_CONTENT; the automatic completion notification will contain only that report rather than a raw log tail.",
|
|
1213
1361
|
"babysit_run gives full PTY control: drive interactive programs (installers, wizards, REPLs) with babysit_send (text or named keys) and read the rendered screen with babysit_check { screen: true }.",
|
|
1214
1362
|
"Delegate self-contained tasks (codebase recon, a parallelizable subtask, work that would pollute your context) with babysit_run { profile: \"subagent\", task }. Launch several for independent subtasks; they run concurrently.",
|
|
1215
1363
|
"After spawning subagents, do not idle-wait and do not end your turn to wait for them: keep making progress, then call babysit_wait (ids + mode any/all) when you need their results. Steer or send follow-up tasks with babysit_send; kill runaways with babysit_kill.",
|
|
@@ -1269,6 +1417,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
1269
1417
|
"Process mode only. Default false: starting a process ENDS the current turn (you are resumed by the exit notification). Set true only when you have immediate, specific, non-polling work to do after starting.",
|
|
1270
1418
|
}),
|
|
1271
1419
|
),
|
|
1420
|
+
report: Type.Optional(
|
|
1421
|
+
Type.Object({
|
|
1422
|
+
language: StringEnum(["javascript", "python", "shell"] as const, {
|
|
1423
|
+
description: "Language for the local report program.",
|
|
1424
|
+
}),
|
|
1425
|
+
code: Type.String({
|
|
1426
|
+
description:
|
|
1427
|
+
"Program that analyzes the complete captured log. JavaScript/Python receive FILE_CONTENT and INPUT strings; shell receives $BABYSIT_REPORT_INPUT. Print only the concise report.",
|
|
1428
|
+
}),
|
|
1429
|
+
timeout: Type.Optional(
|
|
1430
|
+
Type.String({ description: "Report-program timeout (default 30s, e.g. '10s')." }),
|
|
1431
|
+
),
|
|
1432
|
+
}),
|
|
1433
|
+
),
|
|
1272
1434
|
}),
|
|
1273
1435
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1274
1436
|
await requireBabysit();
|
|
@@ -1294,6 +1456,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1294
1456
|
details: {},
|
|
1295
1457
|
};
|
|
1296
1458
|
}
|
|
1459
|
+
if (isSubagent && params.report) {
|
|
1460
|
+
return {
|
|
1461
|
+
content: [{ type: "text", text: "`report` is available only for process sessions." }],
|
|
1462
|
+
isError: true,
|
|
1463
|
+
details: {},
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1297
1466
|
|
|
1298
1467
|
// --- process mode ---
|
|
1299
1468
|
if (!isSubagent) {
|
|
@@ -1304,6 +1473,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1304
1473
|
timeout: params.timeout,
|
|
1305
1474
|
idleTimeout: params.idleTimeout,
|
|
1306
1475
|
pty: params.pty ?? true,
|
|
1476
|
+
report: params.report,
|
|
1307
1477
|
});
|
|
1308
1478
|
if ("error" in res) {
|
|
1309
1479
|
return {
|
|
@@ -1326,7 +1496,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1326
1496
|
return {
|
|
1327
1497
|
content: [{ type: "text", text: outcome.text }],
|
|
1328
1498
|
isError: !outcome.ok,
|
|
1329
|
-
details: { id: res.id, kind: "process", command: params.command },
|
|
1499
|
+
details: { id: res.id, kind: "process", command: params.command, report: params.report },
|
|
1330
1500
|
};
|
|
1331
1501
|
}
|
|
1332
1502
|
|
|
@@ -1345,12 +1515,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1345
1515
|
`Human can watch/take over: /babysit`,
|
|
1346
1516
|
},
|
|
1347
1517
|
],
|
|
1348
|
-
details: { id: res.id, kind: "process", command: params.command },
|
|
1349
|
-
//
|
|
1350
|
-
//
|
|
1351
|
-
//
|
|
1352
|
-
//
|
|
1353
|
-
|
|
1518
|
+
details: { id: res.id, kind: "process", command: params.command, report: params.report },
|
|
1519
|
+
// Do not return `terminate: true` here. In RPC/subagent hosts that hint
|
|
1520
|
+
// can shut down the hosting pi worker, whose process-tree cleanup then
|
|
1521
|
+
// kills the otherwise detached babysit supervisor and closes its PTY
|
|
1522
|
+
// (observed as an immediate `^D`). The prompt contract tells the model
|
|
1523
|
+
// to stop after this result instead; the NOTIFY_MARKER still identifies
|
|
1524
|
+
// a parked turn to the parent/self-reaper logic.
|
|
1525
|
+
terminate: false,
|
|
1354
1526
|
};
|
|
1355
1527
|
}
|
|
1356
1528
|
|
|
@@ -1704,6 +1876,62 @@ export default function (pi: ExtensionAPI) {
|
|
|
1704
1876
|
},
|
|
1705
1877
|
});
|
|
1706
1878
|
|
|
1879
|
+
// ----- babysit_analyze ----------------------------------------------------
|
|
1880
|
+
pi.registerTool({
|
|
1881
|
+
name: "babysit_analyze",
|
|
1882
|
+
label: "Babysit: analyze",
|
|
1883
|
+
description:
|
|
1884
|
+
"Run a local analyzer over a babysit process's complete captured log. The raw log remains outside " +
|
|
1885
|
+
"the model context; only the analyzer's bounded stdout is returned. JavaScript/Python receive " +
|
|
1886
|
+
"FILE_CONTENT and INPUT strings; shell receives $BABYSIT_REPORT_INPUT. It can inspect a running " +
|
|
1887
|
+
"session's output so far or a completed session.",
|
|
1888
|
+
promptSnippet: "Analyze a babysit process log locally and return only a concise report",
|
|
1889
|
+
promptGuidelines: [
|
|
1890
|
+
"Use babysit_analyze to extract failures, counts, or other concise facts from a large babysit process log instead of calling babysit_check repeatedly or returning raw logs.",
|
|
1891
|
+
],
|
|
1892
|
+
parameters: Type.Object({
|
|
1893
|
+
id: Type.String({ description: "Process session id whose captured log should be analyzed." }),
|
|
1894
|
+
language: StringEnum(["javascript", "python", "shell"] as const, {
|
|
1895
|
+
description: "Language for the local analyzer program.",
|
|
1896
|
+
}),
|
|
1897
|
+
code: Type.String({
|
|
1898
|
+
description:
|
|
1899
|
+
"Analyzer program. JavaScript/Python receive FILE_CONTENT and INPUT strings; shell reads $BABYSIT_REPORT_INPUT. Print only the concise result.",
|
|
1900
|
+
}),
|
|
1901
|
+
timeout: Type.Optional(
|
|
1902
|
+
Type.String({ description: "Analyzer timeout (default 30s, e.g. '10s')." }),
|
|
1903
|
+
),
|
|
1904
|
+
}),
|
|
1905
|
+
async execute(_id, params, signal) {
|
|
1906
|
+
await requireBabysit();
|
|
1907
|
+
const status = await statusOf(params.id);
|
|
1908
|
+
if (!status) {
|
|
1909
|
+
return {
|
|
1910
|
+
content: [{ type: "text", text: `No such session: ${params.id}` }],
|
|
1911
|
+
isError: true,
|
|
1912
|
+
details: {},
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
if (kindOf(params.id) === "subagent") {
|
|
1916
|
+
return {
|
|
1917
|
+
content: [{ type: "text", text: "babysit_analyze supports process sessions only; use babysit_check for subagent progress." }],
|
|
1918
|
+
isError: true,
|
|
1919
|
+
details: {},
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
const report = await runReport(params.id, {
|
|
1923
|
+
language: params.language,
|
|
1924
|
+
code: params.code,
|
|
1925
|
+
timeout: params.timeout,
|
|
1926
|
+
}, signal);
|
|
1927
|
+
return {
|
|
1928
|
+
content: [{ type: "text", text: report.text }],
|
|
1929
|
+
isError: !report.ok,
|
|
1930
|
+
details: {},
|
|
1931
|
+
};
|
|
1932
|
+
},
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1707
1935
|
// ----- babysit_wait -------------------------------------------------------
|
|
1708
1936
|
pi.registerTool({
|
|
1709
1937
|
name: "babysit_wait",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yusukeshib/pi-babysit",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Run
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"description": "Run long-lived processes and pi subagents under babysit, with bounded local reports over captured logs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi-extension",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"LICENSE"
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
|
+
"test": "bun test",
|
|
30
31
|
"release": "npm publish --access public"
|
|
31
32
|
},
|
|
32
33
|
"pi": {
|