@yusukeshib/pi-babysit 0.3.12 → 0.3.14
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 +44 -25
- package/agents.ts +23 -9
- package/index.ts +1203 -242
- package/package.json +1 -1
- package/rpc-stream-proxy.mjs +70 -10
- package/self-reap.ts +48 -28
package/package.json
CHANGED
package/rpc-stream-proxy.mjs
CHANGED
|
@@ -7,15 +7,45 @@
|
|
|
7
7
|
* are useful only while streaming; `message_end` is the authoritative final
|
|
8
8
|
* message. Persisting every snapshot makes babysit logs grow quadratically.
|
|
9
9
|
*
|
|
10
|
-
* This proxy forwards stdin byte-for-byte
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* This proxy forwards stdin byte-for-byte. `message_update` snapshots are
|
|
11
|
+
* always compacted. In opt-in `compact` log mode, redundant payloads on
|
|
12
|
+
* lifecycle/tool events are also removed while authoritative `message_end`,
|
|
13
|
+
* response, and error events remain intact. Set PI_BABYSIT_RPC_LOG_MODE=standard
|
|
14
|
+
* to retain the legacy lifecycle payloads.
|
|
13
15
|
*/
|
|
14
16
|
import { spawn } from "node:child_process";
|
|
15
17
|
import { StringDecoder } from "node:string_decoder";
|
|
16
18
|
import { pathToFileURL } from "node:url";
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
function isParkedMessages(messages) {
|
|
21
|
+
if (!Array.isArray(messages)) return false;
|
|
22
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
23
|
+
const message = messages[index];
|
|
24
|
+
if (message?.role !== "toolResult") continue;
|
|
25
|
+
if (message.toolName === "process") return true;
|
|
26
|
+
if (message.toolName !== "babysit_run") continue;
|
|
27
|
+
if (message.details?.kind === "process" && message.details.status === "started") {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
const text = Array.isArray(message.content)
|
|
31
|
+
? message.content
|
|
32
|
+
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
33
|
+
.map((part) => part.text)
|
|
34
|
+
.join("")
|
|
35
|
+
: typeof message.content === "string"
|
|
36
|
+
? message.content
|
|
37
|
+
: "";
|
|
38
|
+
if (/^Process started \(id: [^)]+\)\. \[notify-on-exit\]\nLog: /.test(text)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function compactRpcLine(
|
|
46
|
+
line,
|
|
47
|
+
mode = process.env.PI_BABYSIT_RPC_LOG_MODE ?? "standard",
|
|
48
|
+
) {
|
|
19
49
|
if (!line.trimStart().startsWith("{")) return line;
|
|
20
50
|
|
|
21
51
|
let event;
|
|
@@ -24,14 +54,44 @@ export function compactRpcLine(line) {
|
|
|
24
54
|
} catch {
|
|
25
55
|
return line;
|
|
26
56
|
}
|
|
27
|
-
if (event?.type !== "message_update") return line;
|
|
28
57
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
58
|
+
if (event?.type === "message_update") {
|
|
59
|
+
const { message: _cumulativeMessage, assistantMessageEvent, ...rest } = event;
|
|
60
|
+
if (!assistantMessageEvent || typeof assistantMessageEvent !== "object") {
|
|
61
|
+
return JSON.stringify(rest);
|
|
62
|
+
}
|
|
63
|
+
const { partial: _cumulativePartial, ...deltaEvent } = assistantMessageEvent;
|
|
64
|
+
return JSON.stringify({ ...rest, assistantMessageEvent: deltaEvent });
|
|
65
|
+
}
|
|
66
|
+
if (mode !== "compact") return line;
|
|
67
|
+
|
|
68
|
+
switch (event?.type) {
|
|
69
|
+
case "message_start": {
|
|
70
|
+
const { message, ...rest } = event;
|
|
71
|
+
return JSON.stringify({
|
|
72
|
+
...rest,
|
|
73
|
+
message:
|
|
74
|
+
message && typeof message === "object"
|
|
75
|
+
? { role: message.role, toolName: message.toolName, toolCallId: message.toolCallId }
|
|
76
|
+
: message,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
case "turn_end": {
|
|
80
|
+
const { message: _message, toolResults: _toolResults, ...rest } = event;
|
|
81
|
+
return JSON.stringify(rest);
|
|
82
|
+
}
|
|
83
|
+
case "agent_end": {
|
|
84
|
+
const { messages, ...rest } = event;
|
|
85
|
+
return JSON.stringify({ ...rest, piBabysitParked: isParkedMessages(messages) });
|
|
86
|
+
}
|
|
87
|
+
case "tool_execution_end": {
|
|
88
|
+
if (event.isError) return line;
|
|
89
|
+
const { result: _result, ...rest } = event;
|
|
90
|
+
return JSON.stringify(rest);
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
return line;
|
|
32
94
|
}
|
|
33
|
-
const { partial: _cumulativePartial, ...deltaEvent } = assistantMessageEvent;
|
|
34
|
-
return JSON.stringify({ ...rest, assistantMessageEvent: deltaEvent });
|
|
35
95
|
}
|
|
36
96
|
|
|
37
97
|
function transformRpcOutput(input, output) {
|
package/self-reap.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* keep talking to never dies; only a genuinely-abandoned one does.
|
|
14
14
|
* 2. Process parks — a turn that ends only to await a background-process
|
|
15
15
|
* exit notification (babysit_run inside the subagent, or the legacy
|
|
16
|
-
* `process` tool) also
|
|
17
|
-
* detect
|
|
16
|
+
* `process` tool) also settles the agent, but pi resumes on its own. We
|
|
17
|
+
* detect a parked toolResult anywhere in that run (same rule the parent
|
|
18
18
|
* uses) and DON'T schedule a reap, so a subagent waiting on a long
|
|
19
19
|
* build/test is never false-killed.
|
|
20
20
|
*
|
|
@@ -23,10 +23,8 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
const NOTIFY_MARKER = "[notify-on-exit]";
|
|
29
|
-
|
|
26
|
+
// Loaded standalone into the subagent process via --extension, so the parked
|
|
27
|
+
// result-shape check below intentionally mirrors index.ts without importing it.
|
|
30
28
|
function parseDurMs(s: string | undefined): number | null {
|
|
31
29
|
if (!s || s === "none" || s === "off" || s === "0") return null;
|
|
32
30
|
const m = /^(\d+)(ms|s|m|h)?$/.exec(s.trim());
|
|
@@ -36,22 +34,40 @@ function parseDurMs(s: string | undefined): number | null {
|
|
|
36
34
|
return n * (u === "ms" ? 1 : u === "s" ? 1000 : u === "m" ? 60_000 : 3_600_000);
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
// Same parked-turn rule as the parent
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
// Same parked-turn rule as the parent: scan the run because models sometimes
|
|
38
|
+
// add an assistant note after the marker-bearing tool result.
|
|
39
|
+
export function isParked(
|
|
40
|
+
messages:
|
|
41
|
+
| {
|
|
42
|
+
role?: string;
|
|
43
|
+
toolName?: string;
|
|
44
|
+
content?: unknown;
|
|
45
|
+
details?: { kind?: string; status?: string };
|
|
46
|
+
}[]
|
|
47
|
+
| undefined,
|
|
44
48
|
): boolean {
|
|
45
|
-
if (!
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
if (!messages) return false;
|
|
50
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
51
|
+
const message = messages[index];
|
|
52
|
+
if (message?.role !== "toolResult") continue;
|
|
53
|
+
if (message.toolName === "process") return true; // legacy pi-processes
|
|
54
|
+
if (message.toolName !== "babysit_run") continue;
|
|
55
|
+
if (message.details?.kind === "process" && message.details.status === "started") {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const text = Array.isArray(message.content)
|
|
59
|
+
? message.content
|
|
60
|
+
.filter((part): part is { type: "text"; text: string } =>
|
|
61
|
+
Boolean(part && typeof part === "object" && part.type === "text" && typeof part.text === "string"),
|
|
62
|
+
)
|
|
63
|
+
.map((part) => part.text)
|
|
64
|
+
.join("")
|
|
65
|
+
: typeof message.content === "string"
|
|
66
|
+
? message.content
|
|
67
|
+
: "";
|
|
68
|
+
if (/^Process started \(id: [^)]+\)\. \[notify-on-exit\]\nLog: /.test(text)) return true;
|
|
54
69
|
}
|
|
70
|
+
return false;
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
export default function (pi: ExtensionAPI) {
|
|
@@ -61,6 +77,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
61
77
|
if (graceMs == null) return; // reaping disabled
|
|
62
78
|
|
|
63
79
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
80
|
+
let lastEndWasParked = false;
|
|
64
81
|
const cancel = () => {
|
|
65
82
|
if (timer) {
|
|
66
83
|
clearTimeout(timer);
|
|
@@ -71,22 +88,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
71
88
|
// A new task/turn is starting — the subagent is wanted again. Stand down.
|
|
72
89
|
pi.on("before_agent_start", () => {
|
|
73
90
|
cancel();
|
|
91
|
+
lastEndWasParked = false;
|
|
74
92
|
});
|
|
75
93
|
|
|
76
94
|
pi.on("agent_end", (event) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const msgs = (
|
|
95
|
+
const messages = (
|
|
80
96
|
event as { messages?: { role?: string; toolName?: string; content?: unknown }[] }
|
|
81
97
|
).messages;
|
|
82
|
-
|
|
98
|
+
lastEndWasParked = isParked(messages);
|
|
99
|
+
});
|
|
83
100
|
|
|
101
|
+
// agent_end can precede automatic retries, compaction retries, and queued
|
|
102
|
+
// continuations. Start the grace timer only once Pi is truly settled.
|
|
103
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
104
|
+
cancel();
|
|
105
|
+
if (lastEndWasParked) return;
|
|
84
106
|
timer = setTimeout(() => {
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
process.exit(0);
|
|
107
|
+
// Graceful shutdown emits session_shutdown for every loaded extension.
|
|
108
|
+
ctx.shutdown();
|
|
88
109
|
}, graceMs);
|
|
89
|
-
// Don't let this timer keep the event loop alive on its own.
|
|
90
110
|
timer.unref?.();
|
|
91
111
|
});
|
|
92
112
|
}
|