@yusukeshib/pi-babysit 0.2.0
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/LICENSE +21 -0
- package/README.md +105 -0
- package/agents.ts +124 -0
- package/format-stream.mjs +167 -0
- package/index.ts +1881 -0
- package/package.json +43 -0
- package/self-reap.ts +92 -0
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yusukeshib/pi-babysit",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Run anything long-lived under babysit — one supervision substrate for processes and pi subagents, surfaced to the agent as babysit_run/check/send/wait/kill.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"babysit"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"author": "Yusuke Shibata <yusukeshib@gmail.com>",
|
|
12
|
+
"homepage": "https://github.com/yusukeshib/pi-babysit#readme",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/yusukeshib/pi-babysit.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/yusukeshib/pi-babysit/issues"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"files": [
|
|
22
|
+
"index.ts",
|
|
23
|
+
"agents.ts",
|
|
24
|
+
"self-reap.ts",
|
|
25
|
+
"format-stream.mjs",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"release": "npm publish --access public"
|
|
31
|
+
},
|
|
32
|
+
"pi": {
|
|
33
|
+
"extensions": [
|
|
34
|
+
"./index.ts"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@earendil-works/pi-ai": "*",
|
|
39
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
40
|
+
"@earendil-works/pi-tui": "*",
|
|
41
|
+
"typebox": "*"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/self-reap.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* self-reap.ts — loaded INTO each subagent (`pi --mode rpc --extension …`).
|
|
3
|
+
*
|
|
4
|
+
* A subagent whose task is done becomes an idle RPC worker and, without this,
|
|
5
|
+
* lingers until the absolute `--timeout` (default 15m) or an explicit
|
|
6
|
+
* `babysit_kill`. That leaves dead-weight `pi` processes around for minutes.
|
|
7
|
+
*
|
|
8
|
+
* This reaper makes a finished subagent self-terminate after a short grace
|
|
9
|
+
* window, WITHOUT breaking the two reasons it's normally kept alive:
|
|
10
|
+
*
|
|
11
|
+
* 1. Resume — `babysit_send` injects a follow-up task, which fires
|
|
12
|
+
* `before_agent_start` and CANCELS the pending reap. So a subagent you
|
|
13
|
+
* keep talking to never dies; only a genuinely-abandoned one does.
|
|
14
|
+
* 2. Process parks — a turn that ends only to await a background-process
|
|
15
|
+
* exit notification (babysit_run inside the subagent, or the legacy
|
|
16
|
+
* `process` tool) also emits `agent_end`, but pi resumes on its own. We
|
|
17
|
+
* detect that (last message is a parked toolResult, same rule the parent
|
|
18
|
+
* uses) and DON'T schedule a reap, so a subagent waiting on a long
|
|
19
|
+
* build/test is never false-killed.
|
|
20
|
+
*
|
|
21
|
+
* Grace window: $PI_BABYSIT_REAP_AFTER (e.g. "30s", "2m"), default 120s.
|
|
22
|
+
* Set to "off"/"none"/"0" to disable (falls back to the absolute --timeout).
|
|
23
|
+
*/
|
|
24
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
|
|
26
|
+
// Keep in sync with NOTIFY_MARKER in index.ts (not imported: this file is
|
|
27
|
+
// loaded standalone into the subagent process via --extension).
|
|
28
|
+
const NOTIFY_MARKER = "[notify-on-exit]";
|
|
29
|
+
|
|
30
|
+
function parseDurMs(s: string | undefined): number | null {
|
|
31
|
+
if (!s || s === "none" || s === "off" || s === "0") return null;
|
|
32
|
+
const m = /^(\d+)(ms|s|m|h)?$/.exec(s.trim());
|
|
33
|
+
if (!m) return null;
|
|
34
|
+
const n = Number(m[1]);
|
|
35
|
+
const u = m[2] ?? "s";
|
|
36
|
+
return n * (u === "ms" ? 1 : u === "s" ? 1000 : u === "m" ? 60_000 : 3_600_000);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Same parked-turn rule as the parent (see index.ts isParkedToolResult):
|
|
40
|
+
// a babysit_run process start stamps NOTIFY_MARKER into its tool result; that
|
|
41
|
+
// as the LAST message means "parked awaiting the exit notification".
|
|
42
|
+
function isParked(
|
|
43
|
+
last: { role?: string; toolName?: string; content?: unknown } | undefined,
|
|
44
|
+
): boolean {
|
|
45
|
+
if (!last || last.role !== "toolResult") return false;
|
|
46
|
+
if (last.toolName === "process") return true; // legacy pi-processes
|
|
47
|
+
if (last.toolName !== "babysit_run") return false;
|
|
48
|
+
try {
|
|
49
|
+
const s = JSON.stringify(last.content ?? null);
|
|
50
|
+
if (s === "null") return true; // unsure — never reap a possible build wait
|
|
51
|
+
return s.includes(NOTIFY_MARKER);
|
|
52
|
+
} catch {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default function (pi: ExtensionAPI) {
|
|
58
|
+
const graceMs = parseDurMs(
|
|
59
|
+
process.env.PI_BABYSIT_REAP_AFTER ?? process.env.PI_SUBAGENT_REAP_AFTER ?? "120s",
|
|
60
|
+
);
|
|
61
|
+
if (graceMs == null) return; // reaping disabled
|
|
62
|
+
|
|
63
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
64
|
+
const cancel = () => {
|
|
65
|
+
if (timer) {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
timer = undefined;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// A new task/turn is starting — the subagent is wanted again. Stand down.
|
|
72
|
+
pi.on("before_agent_start", () => {
|
|
73
|
+
cancel();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
pi.on("agent_end", (event) => {
|
|
77
|
+
cancel(); // supersede any earlier schedule with this end's decision
|
|
78
|
+
|
|
79
|
+
const msgs = (
|
|
80
|
+
event as { messages?: { role?: string; toolName?: string; content?: unknown }[] }
|
|
81
|
+
).messages;
|
|
82
|
+
if (isParked(msgs?.[msgs.length - 1])) return;
|
|
83
|
+
|
|
84
|
+
timer = setTimeout(() => {
|
|
85
|
+
// Clean exit: babysit sees the child exit and marks the session done,
|
|
86
|
+
// so it drops out of the session list/widget instead of idling.
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}, graceMs);
|
|
89
|
+
// Don't let this timer keep the event loop alive on its own.
|
|
90
|
+
timer.unref?.();
|
|
91
|
+
});
|
|
92
|
+
}
|