@yusukeshib/pi-babysit 0.3.14 → 0.3.16
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 +17 -12
- package/index.ts +257 -54
- package/package.json +1 -1
- package/rpc-stream-proxy.mjs +4 -4
package/README.md
CHANGED
|
@@ -56,8 +56,8 @@ programs** (installers, wizards, REPLs): type with `babysit_send`
|
|
|
56
56
|
|
|
57
57
|
| Tool | What it does |
|
|
58
58
|
| ---- | ------------ |
|
|
59
|
-
| `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`/`notificationGroup`)
|
|
60
|
-
| `babysit_check` |
|
|
59
|
+
| `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`/`notificationGroup`). Set `foreground: true` when the next step needs the result in the same tool call, avoiding a separate `babysit_wait` turn. Or start a named subagent (`profile: "subagent"`, `task`, optional `name`/`agent`/`model`/`tools`/`maxDepth` and `maxCost`/`maxTurns`/`maxToolCalls`/`maxUsageTokens` budgets). `maxDepth` defaults to 1 and can only be set by the top-level caller. Quick commands return inline; longer ones continue in the background |
|
|
60
|
+
| `babysit_check` | Without an id, list running sessions by default (`state: "all"` includes history; `state`/`kind` filters are available). With an id, inspect one session, tail bounded recent output, or search its raw log with `pattern`; `screen: true` captures TUIs and subagents otherwise show structured live progress |
|
|
61
61
|
| `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`) |
|
|
62
62
|
| `babysit_wait` | Block until done: process exit (or `expect: "regex"` readiness marker), subagent task completion. Multi-wait: up to 32 unique `ids` + `mode: "any"\|"all"` |
|
|
63
63
|
| `babysit_kill` | Terminate a session, verify terminal state, then suppress the exit notification |
|
|
@@ -106,13 +106,13 @@ a raw RPC JSON tail.
|
|
|
106
106
|
|
|
107
107
|
Subagent logs compact Pi's streaming `message_update` events before they are
|
|
108
108
|
recorded. Pi repeats the complete growing assistant message and partial snapshot
|
|
109
|
-
on every token; pi-babysit retains only the incremental delta.
|
|
110
|
-
|
|
109
|
+
on every token; pi-babysit retains only the incremental delta. Compact RPC
|
|
110
|
+
logging is the default and also removes duplicate payloads from `message_start`,
|
|
111
111
|
`turn_end`, successful `tool_execution_end`, and `agent_end`, while preserving
|
|
112
112
|
authoritative `message_end`, failures, responses, and errors. Parked-process
|
|
113
113
|
state is materialized as a small boolean so completion detection is unchanged.
|
|
114
|
-
Set `PI_BABYSIT_RPC_LOG_MODE=standard`
|
|
115
|
-
`/babysit` and attach views render either format.
|
|
114
|
+
Set `PI_BABYSIT_RPC_LOG_MODE=standard` only when legacy lifecycle payloads are
|
|
115
|
+
needed for debugging. Live `/babysit` and attach views render either format.
|
|
116
116
|
|
|
117
117
|
All shell commands, including `pwd` and Git, go through `babysit_run`. Bundle
|
|
118
118
|
closely related tiny observations when doing so safely reduces tool turns.
|
|
@@ -134,9 +134,11 @@ because blindly rerunning an arbitrary command can duplicate side effects.
|
|
|
134
134
|
`pi.sendMessage(…, { triggerTurn: true, deliverAs: "steer" })` containing every
|
|
135
135
|
deliverable exit observed in that poll (deduped via `meta/<id>.json`). Waiting
|
|
136
136
|
for idleness prevents an immediately-following `babysit_wait` from racing the
|
|
137
|
-
poller and receiving a duplicate completion.
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
poller and receiving a duplicate completion. Background process calls emitted
|
|
138
|
+
together in one assistant message automatically share a notification group;
|
|
139
|
+
an explicit `notificationGroup` overrides this. Group members wait for every
|
|
140
|
+
currently running member to stop and then share one notification even when
|
|
141
|
+
their exits span multiple polls.
|
|
140
142
|
`babysit_kill` and an exit already reported by `babysit_wait` suppress the
|
|
141
143
|
notification.
|
|
142
144
|
- **Subagent**: `babysit_wait` blocks on `babysit expect '"type":"agent_settled"'`.
|
|
@@ -152,8 +154,10 @@ because blindly rerunning an arbitrary command can duplicate side effects.
|
|
|
152
154
|
|
|
153
155
|
Subagents load `self-reap.ts`, which exits an idle finished subagent after a
|
|
154
156
|
grace window (`PI_BABYSIT_REAP_AFTER`, default 120s) using the same parked-turn
|
|
155
|
-
rule, so a subagent waiting on a long build is never false-killed.
|
|
156
|
-
|
|
157
|
+
rule, so a subagent waiting on a long build is never false-killed. Give bounded
|
|
158
|
+
recon/review tasks at least one cost, turn, tool-call, or token budget; omit
|
|
159
|
+
budgets only for intentionally open-ended work. Optional task budgets are
|
|
160
|
+
checked by the parent poller. On the first exceeded limit the worker
|
|
157
161
|
is steered to stop using tools and return its best answer; if it remains active
|
|
158
162
|
after `PI_BABYSIT_BUDGET_GRACE`, termination is verified before the task is
|
|
159
163
|
marked budget-killed. Usage shown by check/wait is cumulative for the task.
|
|
@@ -166,9 +170,10 @@ marked budget-killed. Usage shown by check/wait is cumulative for the task.
|
|
|
166
170
|
| `PI_BABYSIT_BIN` | `pi` | agent binary for subagents |
|
|
167
171
|
| `PI_BABYSIT_CLI` | `babysit` | babysit binary |
|
|
168
172
|
| `PI_BABYSIT_VIEW_CMD` | bundled `format-stream.mjs` | live-attach pretty printer for subagent JSONL (`""` disables) |
|
|
173
|
+
| `PI_BABYSIT_QUICK_GRACE` | `2s` | interactive process grace before a still-running command is returned as background work; use `foreground: true` to wait explicitly |
|
|
169
174
|
| `PI_BABYSIT_REAP_AFTER` | `120s` | idle grace before a finished subagent self-exits (`off`/`none`/`0` disables) |
|
|
170
175
|
| `PI_BABYSIT_BUDGET_GRACE` | `30s` | grace after a subagent budget is exceeded before verified termination |
|
|
171
|
-
| `PI_BABYSIT_RPC_LOG_MODE` | `
|
|
176
|
+
| `PI_BABYSIT_RPC_LOG_MODE` | `compact` | `compact` removes duplicate RPC lifecycle payloads; `standard` opts into legacy payloads |
|
|
172
177
|
| `PI_BABYSIT_RETENTION_DAYS` | unset | when set to a positive number, remove safe terminal roots older than this at session startup |
|
|
173
178
|
| `PI_BABYSIT_TAIL_MAX_BYTES` | `8000` | cap for explicit log tails/screens returned by `babysit_check` |
|
|
174
179
|
| `PI_BABYSIT_INLINE_OUTPUT_MAX_BYTES` | `8000` | cap for complete process output and aggregate multi-wait results |
|
package/index.ts
CHANGED
|
@@ -148,16 +148,32 @@ const VIEW_CMD =
|
|
|
148
148
|
// Appended to every subagent's system prompt. The subagent is a long-lived
|
|
149
149
|
// headless `pi --mode rpc` worker: turns can end and resume, so babysit_run
|
|
150
150
|
// (process kind) works normally inside it. It just cannot talk to a human.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
151
|
+
export function subagentGuidance(
|
|
152
|
+
depth: number,
|
|
153
|
+
maxDepth: number,
|
|
154
|
+
directBashAvailable: boolean,
|
|
155
|
+
babysitRunAvailable = true,
|
|
156
|
+
): string {
|
|
157
|
+
const shellGuidance = directBashAvailable
|
|
158
|
+
? "Direct bash is available, and babysit_run can supervise longer commands."
|
|
159
|
+
: babysitRunAvailable
|
|
160
|
+
? "Direct bash is unavailable; run shell commands with babysit_run { command }."
|
|
161
|
+
: "No shell execution tool is available in this task's tool allowlist; do not attempt shell commands.";
|
|
162
|
+
const nestingGuidance = depth >= maxDepth
|
|
163
|
+
? `You are at the inherited subagent depth limit (${depth}/${maxDepth}); do not attempt to spawn another subagent.`
|
|
164
|
+
: `Your inherited subagent depth is ${depth}/${maxDepth}; child subagents may not exceed maxDepth ${maxDepth}.`;
|
|
165
|
+
return [
|
|
166
|
+
"You are a headless background worker driven over pi's RPC protocol.",
|
|
167
|
+
"Work autonomously: you cannot ask the user questions, so state assumptions",
|
|
168
|
+
"in your final answer instead.",
|
|
169
|
+
shellGuidance,
|
|
170
|
+
nestingGuidance,
|
|
171
|
+
"When your task is complete, produce a final answer message summarizing the outcome —",
|
|
172
|
+
"your controller reads it from the event stream.",
|
|
173
|
+
].join(" ");
|
|
174
|
+
}
|
|
159
175
|
const POLL_MS = 2500;
|
|
160
|
-
const QUICK_COMMAND_GRACE = process.env.PI_BABYSIT_QUICK_GRACE ?? "
|
|
176
|
+
const QUICK_COMMAND_GRACE = process.env.PI_BABYSIT_QUICK_GRACE ?? "2s";
|
|
161
177
|
const KILL_CONFIRM_TIMEOUT = "4s";
|
|
162
178
|
|
|
163
179
|
interface BsSession {
|
|
@@ -1269,6 +1285,8 @@ export interface Progress {
|
|
|
1269
1285
|
turns: number;
|
|
1270
1286
|
toolCalls: ToolCall[];
|
|
1271
1287
|
finalText: string;
|
|
1288
|
+
/** Best-effort text from the currently streaming assistant message. */
|
|
1289
|
+
streamingText: string;
|
|
1272
1290
|
/** Context size reported by the most recent assistant response. */
|
|
1273
1291
|
tokens?: number;
|
|
1274
1292
|
/** Cumulative model usage for the current subagent task. */
|
|
@@ -1323,6 +1341,7 @@ function emptyProgress(): Progress {
|
|
|
1323
1341
|
turns: 0,
|
|
1324
1342
|
toolCalls: [],
|
|
1325
1343
|
finalText: "",
|
|
1344
|
+
streamingText: "",
|
|
1326
1345
|
modelCalls: 0,
|
|
1327
1346
|
usageTokens: 0,
|
|
1328
1347
|
inputTokens: 0,
|
|
@@ -1401,7 +1420,17 @@ function parseEventLine(progress: Progress, raw: string): void {
|
|
|
1401
1420
|
switch (event.type) {
|
|
1402
1421
|
case "turn_start":
|
|
1403
1422
|
progress.turns++;
|
|
1423
|
+
progress.streamingText = "";
|
|
1424
|
+
break;
|
|
1425
|
+
case "message_update": {
|
|
1426
|
+
const update = event.assistantMessageEvent as
|
|
1427
|
+
| { type?: string; delta?: string }
|
|
1428
|
+
| undefined;
|
|
1429
|
+
if (update?.type === "text_delta" && typeof update.delta === "string") {
|
|
1430
|
+
progress.streamingText += update.delta;
|
|
1431
|
+
}
|
|
1404
1432
|
break;
|
|
1433
|
+
}
|
|
1405
1434
|
case "tool_execution_start": {
|
|
1406
1435
|
const name = String(event.toolName ?? "tool");
|
|
1407
1436
|
progress.toolCalls.push({
|
|
@@ -1432,6 +1461,7 @@ function parseEventLine(progress: Progress, raw: string): void {
|
|
|
1432
1461
|
.map((content) => content.text)
|
|
1433
1462
|
.join("");
|
|
1434
1463
|
if (text.trim()) progress.finalText = text;
|
|
1464
|
+
progress.streamingText = "";
|
|
1435
1465
|
if (message.usage) {
|
|
1436
1466
|
const finite = (value: number | undefined) =>
|
|
1437
1467
|
typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
@@ -1515,6 +1545,7 @@ export function buildSubagentExitDiagnostic(
|
|
|
1515
1545
|
): string {
|
|
1516
1546
|
const body = clip(
|
|
1517
1547
|
progress.errorMsg ||
|
|
1548
|
+
progress.streamingText.trim() ||
|
|
1518
1549
|
progress.finalText.trim() ||
|
|
1519
1550
|
"(no structured error was emitted; inspect the full log)",
|
|
1520
1551
|
ANSWER_MAX_BYTES,
|
|
@@ -1587,13 +1618,16 @@ function taskProgressOf(id: string): { progress: Progress; offset: number } {
|
|
|
1587
1618
|
// ---------------------------------------------------------------------------
|
|
1588
1619
|
|
|
1589
1620
|
// Friendly name → unique babysit session id (babysit ids allow [\w.-]).
|
|
1590
|
-
|
|
1621
|
+
const reservedSessionIds = new Set<string>();
|
|
1622
|
+
|
|
1623
|
+
async function reserveUniqueSessionId(name: string): Promise<string> {
|
|
1591
1624
|
const base = name.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "proc";
|
|
1592
1625
|
const taken = new Set((await listSessions()).sessions.map((s) => s.id));
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1626
|
+
for (const id of reservedSessionIds) taken.add(id);
|
|
1627
|
+
let id = base;
|
|
1628
|
+
for (let i = 2; taken.has(id); i++) id = `${base}-${i}`;
|
|
1629
|
+
reservedSessionIds.add(id);
|
|
1630
|
+
return id;
|
|
1597
1631
|
}
|
|
1598
1632
|
|
|
1599
1633
|
interface ProcOpts {
|
|
@@ -1612,10 +1646,16 @@ async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: s
|
|
|
1612
1646
|
if (opts.timeout && opts.timeout !== "none") bsArgs.push("--timeout", opts.timeout);
|
|
1613
1647
|
if (opts.idleTimeout && opts.idleTimeout !== "none")
|
|
1614
1648
|
bsArgs.push("--idle-timeout", opts.idleTimeout);
|
|
1615
|
-
|
|
1649
|
+
const reservedId = opts.name ? await reserveUniqueSessionId(opts.name) : undefined;
|
|
1650
|
+
if (reservedId) bsArgs.push("--id", reservedId);
|
|
1616
1651
|
bsArgs.push("--", SHELL, "-c", opts.command);
|
|
1617
1652
|
|
|
1618
|
-
|
|
1653
|
+
let r: Awaited<ReturnType<typeof bs>>;
|
|
1654
|
+
try {
|
|
1655
|
+
r = await bs(bsArgs, { cwd: opts.cwd });
|
|
1656
|
+
} finally {
|
|
1657
|
+
if (reservedId) reservedSessionIds.delete(reservedId);
|
|
1658
|
+
}
|
|
1619
1659
|
if (r.code !== 0) {
|
|
1620
1660
|
return {
|
|
1621
1661
|
error:
|
|
@@ -1687,6 +1727,7 @@ function discardDelivery(delivery: DeliverableMessage): void {
|
|
|
1687
1727
|
}
|
|
1688
1728
|
|
|
1689
1729
|
interface SubagentOpts {
|
|
1730
|
+
name?: string;
|
|
1690
1731
|
agent?: AgentConfig;
|
|
1691
1732
|
task: string;
|
|
1692
1733
|
model?: string;
|
|
@@ -1720,7 +1761,15 @@ async function spawnSubagent(
|
|
|
1720
1761
|
};
|
|
1721
1762
|
}
|
|
1722
1763
|
if (tools && tools.length > 0) piArgs.push("--tools", tools.join(","));
|
|
1723
|
-
piArgs.push(
|
|
1764
|
+
piArgs.push(
|
|
1765
|
+
"--append-system-prompt",
|
|
1766
|
+
subagentGuidance(
|
|
1767
|
+
opts.depth,
|
|
1768
|
+
opts.maxDepth,
|
|
1769
|
+
isAllowedDirectBash("") && (!tools || tools.includes("bash")),
|
|
1770
|
+
!tools || tools.includes("babysit_run"),
|
|
1771
|
+
),
|
|
1772
|
+
);
|
|
1724
1773
|
let promptTempFile: string | undefined;
|
|
1725
1774
|
if (opts.agent?.systemPrompt?.trim()) {
|
|
1726
1775
|
promptTempFile = writePromptTempFile(opts.agent.name, opts.agent.systemPrompt);
|
|
@@ -1758,6 +1807,8 @@ async function spawnSubagent(
|
|
|
1758
1807
|
if (opts.idleTimeout && opts.idleTimeout !== "none") {
|
|
1759
1808
|
bsArgs.push("--idle-timeout", opts.idleTimeout);
|
|
1760
1809
|
}
|
|
1810
|
+
const reservedId = opts.name ? await reserveUniqueSessionId(opts.name) : undefined;
|
|
1811
|
+
if (reservedId) bsArgs.push("--id", reservedId);
|
|
1761
1812
|
bsArgs.push(
|
|
1762
1813
|
"--",
|
|
1763
1814
|
process.execPath,
|
|
@@ -1767,13 +1818,18 @@ async function spawnSubagent(
|
|
|
1767
1818
|
...piArgs,
|
|
1768
1819
|
);
|
|
1769
1820
|
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1821
|
+
let r: Awaited<ReturnType<typeof bs>>;
|
|
1822
|
+
try {
|
|
1823
|
+
r = await bs(bsArgs, {
|
|
1824
|
+
cwd: opts.cwd,
|
|
1825
|
+
env: {
|
|
1826
|
+
[SUBAGENT_DEPTH_ENV]: String(opts.depth),
|
|
1827
|
+
[SUBAGENT_MAX_DEPTH_ENV]: String(opts.maxDepth),
|
|
1828
|
+
},
|
|
1829
|
+
});
|
|
1830
|
+
} finally {
|
|
1831
|
+
if (reservedId) reservedSessionIds.delete(reservedId);
|
|
1832
|
+
}
|
|
1777
1833
|
if (r.code !== 0) {
|
|
1778
1834
|
cleanupPromptTemp();
|
|
1779
1835
|
return { error: r.stderr || r.stdout || `babysit run failed (exit ${r.code}, no output) — check that \`${BABYSIT_BIN}\` works and ${ROOT} is writable` };
|
|
@@ -1792,6 +1848,7 @@ async function spawnSubagent(
|
|
|
1792
1848
|
// The success path overwrites this with the full task meta.
|
|
1793
1849
|
writeMeta(id, {
|
|
1794
1850
|
kind: "subagent",
|
|
1851
|
+
name: opts.name ?? id,
|
|
1795
1852
|
task: opts.task,
|
|
1796
1853
|
notified: true,
|
|
1797
1854
|
depth: opts.depth,
|
|
@@ -1851,6 +1908,7 @@ async function spawnSubagent(
|
|
|
1851
1908
|
|
|
1852
1909
|
writeMeta(id, {
|
|
1853
1910
|
kind: "subagent",
|
|
1911
|
+
name: opts.name ?? id,
|
|
1854
1912
|
task: opts.task,
|
|
1855
1913
|
promptOffset: resp.offset,
|
|
1856
1914
|
model: resolvedModel,
|
|
@@ -2136,9 +2194,15 @@ async function waitForExit(
|
|
|
2136
2194
|
|
|
2137
2195
|
if (expectPattern) {
|
|
2138
2196
|
const e = await bs(["expect", "-s", id, "--timeout", t, expectPattern], { signal });
|
|
2139
|
-
if (signal?.aborted
|
|
2197
|
+
if (signal?.aborted) {
|
|
2140
2198
|
return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
|
|
2141
2199
|
}
|
|
2200
|
+
if (e.code === 130) {
|
|
2201
|
+
const interruptedStatus = await statusOf(id);
|
|
2202
|
+
if (interruptedStatus?.state === "running") {
|
|
2203
|
+
return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2142
2206
|
if (e.code === 0) {
|
|
2143
2207
|
return {
|
|
2144
2208
|
id,
|
|
@@ -2164,10 +2228,17 @@ async function waitForExit(
|
|
|
2164
2228
|
// another wait that is still pending.
|
|
2165
2229
|
updateWaitReservation(id, "reserve");
|
|
2166
2230
|
const w = await bs(["wait", "-s", id, "--timeout", t], { signal });
|
|
2167
|
-
if (signal?.aborted
|
|
2231
|
+
if (signal?.aborted) {
|
|
2168
2232
|
updateWaitReservation(id, "abandon");
|
|
2169
2233
|
return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
|
|
2170
2234
|
}
|
|
2235
|
+
if (w.code === 130) {
|
|
2236
|
+
const interruptedStatus = await statusOf(id);
|
|
2237
|
+
if (interruptedStatus?.state === "running") {
|
|
2238
|
+
updateWaitReservation(id, "abandon");
|
|
2239
|
+
return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2171
2242
|
if (w.code === 124) {
|
|
2172
2243
|
// 124 is ambiguous (timeout vs child exiting 124) — disambiguate.
|
|
2173
2244
|
const st0 = await statusOf(id);
|
|
@@ -2258,6 +2329,44 @@ export function activeToolsWithoutDirectBash(activeTools: string[], allowDirectB
|
|
|
2258
2329
|
return allowDirectBash ? activeTools : activeTools.filter((name) => name !== "bash");
|
|
2259
2330
|
}
|
|
2260
2331
|
|
|
2332
|
+
export function automaticNotificationGroup(entry: unknown): string | undefined {
|
|
2333
|
+
const candidate = entry as {
|
|
2334
|
+
id?: unknown;
|
|
2335
|
+
type?: unknown;
|
|
2336
|
+
message?: { role?: unknown; content?: unknown };
|
|
2337
|
+
};
|
|
2338
|
+
if (
|
|
2339
|
+
candidate?.type !== "message" ||
|
|
2340
|
+
candidate.message?.role !== "assistant" ||
|
|
2341
|
+
typeof candidate.id !== "string" ||
|
|
2342
|
+
!Array.isArray(candidate.message.content)
|
|
2343
|
+
) {
|
|
2344
|
+
return undefined;
|
|
2345
|
+
}
|
|
2346
|
+
const group = `turn-${candidate.id}`;
|
|
2347
|
+
const runs = candidate.message.content.filter((part) => {
|
|
2348
|
+
if (!part || typeof part !== "object") return false;
|
|
2349
|
+
const call = part as {
|
|
2350
|
+
type?: unknown;
|
|
2351
|
+
name?: unknown;
|
|
2352
|
+
arguments?: Record<string, unknown>;
|
|
2353
|
+
};
|
|
2354
|
+
if (call.type !== "toolCall" || call.name !== "babysit_run") return false;
|
|
2355
|
+
const args = call.arguments ?? {};
|
|
2356
|
+
const existing = typeof args.notificationGroup === "string"
|
|
2357
|
+
? args.notificationGroup.trim()
|
|
2358
|
+
: "";
|
|
2359
|
+
return (
|
|
2360
|
+
typeof args.command === "string" &&
|
|
2361
|
+
args.command.length > 0 &&
|
|
2362
|
+
args.profile !== "subagent" &&
|
|
2363
|
+
args.foreground !== true &&
|
|
2364
|
+
(existing === "" || existing === group)
|
|
2365
|
+
);
|
|
2366
|
+
});
|
|
2367
|
+
return runs.length >= 2 ? group : undefined;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2261
2370
|
// ---------------------------------------------------------------------------
|
|
2262
2371
|
// extension
|
|
2263
2372
|
// ---------------------------------------------------------------------------
|
|
@@ -2431,7 +2540,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2431
2540
|
: "failed";
|
|
2432
2541
|
const output = await inlineOutput(session.id, session, NOTIFY_OUTPUT_MAX_BYTES);
|
|
2433
2542
|
const runtime = meta.startedAt
|
|
2434
|
-
? `${Math.round((Date.now() - meta.startedAt) / 1000)}s`
|
|
2543
|
+
? `${Math.round(((meta.completionObservedAt ?? Date.now()) - meta.startedAt) / 1000)}s`
|
|
2435
2544
|
: "?";
|
|
2436
2545
|
const summary = ok
|
|
2437
2546
|
? `Process "${session.id}" completed successfully after ${runtime}.`
|
|
@@ -2451,6 +2560,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2451
2560
|
});
|
|
2452
2561
|
}
|
|
2453
2562
|
|
|
2563
|
+
if (prepared.length === 0) return;
|
|
2564
|
+
|
|
2454
2565
|
// Output loading above is asynchronous. Refresh sessions and metadata
|
|
2455
2566
|
// immediately before the single send so concurrent wait/kill calls and newly
|
|
2456
2567
|
// started notification-group members are honored.
|
|
@@ -2688,7 +2799,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
2688
2799
|
rootLeasePath = undefined;
|
|
2689
2800
|
});
|
|
2690
2801
|
|
|
2691
|
-
pi.on("tool_call", async (event) => {
|
|
2802
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
2803
|
+
if (event.toolName === "babysit_run") {
|
|
2804
|
+
const input = event.input as {
|
|
2805
|
+
command?: unknown;
|
|
2806
|
+
profile?: unknown;
|
|
2807
|
+
foreground?: unknown;
|
|
2808
|
+
notificationGroup?: unknown;
|
|
2809
|
+
};
|
|
2810
|
+
if (
|
|
2811
|
+
typeof input.command === "string" &&
|
|
2812
|
+
input.profile !== "subagent" &&
|
|
2813
|
+
input.foreground !== true &&
|
|
2814
|
+
(typeof input.notificationGroup !== "string" || input.notificationGroup.trim() === "")
|
|
2815
|
+
) {
|
|
2816
|
+
const group = automaticNotificationGroup(ctx.sessionManager.getLeafEntry());
|
|
2817
|
+
if (group) input.notificationGroup = group;
|
|
2818
|
+
}
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2692
2821
|
if (event.toolName !== "bash") return;
|
|
2693
2822
|
const command = String((event.input as { command?: unknown }).command ?? "");
|
|
2694
2823
|
if (backgroundsItself(command)) {
|
|
@@ -2714,10 +2843,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
2714
2843
|
name: "babysit_run",
|
|
2715
2844
|
label: "Babysit: run",
|
|
2716
2845
|
description:
|
|
2717
|
-
"Run any shell command in a supervised babysit session.
|
|
2718
|
-
"
|
|
2719
|
-
"
|
|
2720
|
-
"
|
|
2846
|
+
"Run any shell command in a supervised babysit session. Set `foreground: true` when the next step " +
|
|
2847
|
+
"needs the exit result in this tool call. Otherwise commands that finish within a short grace period " +
|
|
2848
|
+
"return completion metadata immediately; longer commands continue in the background and trigger an " +
|
|
2849
|
+
"automatic notification on exit. Sibling background runs in one assistant message are grouped " +
|
|
2850
|
+
"automatically. Complete output is returned inline only when it is small; larger output stays " +
|
|
2851
|
+
"in the log path for bounded inspection with babysit_check. " +
|
|
2721
2852
|
"In non-interactive mode (`pi -p`, no UI), process mode blocks until exit because there is no " +
|
|
2722
2853
|
"notification loop. Two modes: (1) `command` — run any shell command, including builds, tests, " +
|
|
2723
2854
|
"dev servers, watchers, and interactive TUIs; you can type into it with babysit_send and read " +
|
|
@@ -2730,13 +2861,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
2730
2861
|
promptSnippet:
|
|
2731
2862
|
"Run any shell command with context-safe captured output; quick commands return metadata, longer ones continue in background",
|
|
2732
2863
|
promptGuidelines: [
|
|
2733
|
-
"Use babysit_run as the default for shell commands, not only long-running work. Small output is returned directly; large stdout/stderr stays out of model context in the returned log path. Give meaningful
|
|
2864
|
+
"Use babysit_run as the default for shell commands, not only long-running work. Small output is returned directly; large stdout/stderr stays out of model context in the returned log path. Give every meaningful process or subagent a clear stable `name`.",
|
|
2865
|
+
"Use babysit_run { command, foreground: true } when the result is required before the next step; this avoids a separate babysit_wait model turn. Do not use foreground for servers, watchers, or commands of unknown duration without a timeout.",
|
|
2734
2866
|
"Bundle closely related tiny observations into one babysit_run command when that reduces tool turns without obscuring lifecycle or failure handling.",
|
|
2735
2867
|
"Inspect a babysit log with babysit_check { id, lines, pattern? }; never read or cat a potentially large log file in full. Prefer a targeted `pattern` search over returning a broad tail.",
|
|
2736
2868
|
"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').",
|
|
2737
2869
|
"If a babysit worker is killed externally, babysit_run reports it as worker-dead rather than hanging. Set retryOnWorkerDeath: true only for safe, idempotent commands; it retries at most once and may otherwise duplicate side effects.",
|
|
2738
2870
|
"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 }.",
|
|
2739
2871
|
"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.",
|
|
2872
|
+
"Set at least one subagent budget (`maxCost`, `maxTurns`, `maxToolCalls`, or `maxUsageTokens`) for bounded recon and review tasks. Omit budgets only for intentionally open-ended work; the absolute timeout remains a separate safety limit.",
|
|
2740
2873
|
"Subagents cannot create further subagents by default (maximum depth 1). Only the top-level caller can explicitly opt in by setting maxDepth when it creates the first worker; nested workers inherit that limit and cannot raise it.",
|
|
2741
2874
|
"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.",
|
|
2742
2875
|
],
|
|
@@ -2748,7 +2881,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2748
2881
|
),
|
|
2749
2882
|
name: Type.Optional(
|
|
2750
2883
|
Type.String({
|
|
2751
|
-
description: "Friendly stable name for a process (becomes the session id), e.g. 'cargo-build'.",
|
|
2884
|
+
description: "Friendly stable name for a process or subagent (becomes the session id), e.g. 'cargo-build' or 'review-api'.",
|
|
2752
2885
|
}),
|
|
2753
2886
|
),
|
|
2754
2887
|
profile: Type.Optional(
|
|
@@ -2814,10 +2947,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
2814
2947
|
"Process mode: run in a PTY (default true; enables interactive input/screen). false = plain pipes for cleaner line-oriented logs.",
|
|
2815
2948
|
}),
|
|
2816
2949
|
),
|
|
2950
|
+
foreground: Type.Optional(
|
|
2951
|
+
Type.Boolean({
|
|
2952
|
+
description:
|
|
2953
|
+
"Process mode: wait for exit and return the result in this tool call. Use when the next step needs the result; avoid for servers/watchers unless bounded by timeout.",
|
|
2954
|
+
}),
|
|
2955
|
+
),
|
|
2817
2956
|
notificationGroup: Type.Optional(
|
|
2818
2957
|
Type.String({
|
|
2819
2958
|
description:
|
|
2820
|
-
"Process mode: defer automatic completion until every running process with this group has stopped, then send one batched notification.",
|
|
2959
|
+
"Process mode: defer automatic completion until every running process with this group has stopped, then send one batched notification. Sibling background runs are auto-grouped when omitted.",
|
|
2821
2960
|
}),
|
|
2822
2961
|
),
|
|
2823
2962
|
continueAfterStart: Type.Optional(
|
|
@@ -2877,6 +3016,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
2877
3016
|
details: {},
|
|
2878
3017
|
};
|
|
2879
3018
|
}
|
|
3019
|
+
if (isSubagent && params.foreground) {
|
|
3020
|
+
return {
|
|
3021
|
+
content: [{ type: "text", text: "`foreground` is available only in process mode; use babysit_wait for subagent task completion." }],
|
|
3022
|
+
isError: true,
|
|
3023
|
+
details: {},
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
if (!isSubagent && params.foreground && params.continueAfterStart) {
|
|
3027
|
+
return {
|
|
3028
|
+
content: [{ type: "text", text: "`foreground` and `continueAfterStart` are mutually exclusive." }],
|
|
3029
|
+
isError: true,
|
|
3030
|
+
details: {},
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
2880
3033
|
|
|
2881
3034
|
// Compute nesting only for subagent mode. Ordinary command processes remain
|
|
2882
3035
|
// available even when the hosting agent is at its subagent depth limit.
|
|
@@ -2913,24 +3066,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
2913
3066
|
pollNeeded = true;
|
|
2914
3067
|
await refreshWidget(ctx);
|
|
2915
3068
|
|
|
2916
|
-
// One-shot / non-interactive mode
|
|
2917
|
-
//
|
|
2918
|
-
//
|
|
2919
|
-
//
|
|
2920
|
-
//
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
3069
|
+
// One-shot / non-interactive mode has no event loop that can deliver an
|
|
3070
|
+
// exit notification. `foreground: true` provides the same single-tool-call
|
|
3071
|
+
// result in interactive mode, avoiding a separate babysit_wait model turn.
|
|
3072
|
+
// The command remains supervised, logged, killable, and subject to its
|
|
3073
|
+
// babysit timeout in either case.
|
|
3074
|
+
if (!ctx.hasUI || params.foreground) {
|
|
3075
|
+
// The babysit supervisor owns the absolute command timeout. Waiting with
|
|
3076
|
+
// the same deadline here races its terminal-state write and can return a
|
|
3077
|
+
// false "still running" result at the boundary, so wait for the
|
|
3078
|
+
// supervisor's definitive exit instead.
|
|
3079
|
+
let outcome = await waitForExit(res.id, null, _signal);
|
|
2925
3080
|
let retried = false;
|
|
2926
3081
|
if (params.retryOnWorkerDeath && outcome.status?.state === "dead" && outcome.status.exit_code == null) {
|
|
2927
3082
|
const retry = await spawnProcess(spawnOpts);
|
|
2928
3083
|
if (!("error" in retry)) {
|
|
2929
3084
|
res = retry;
|
|
2930
3085
|
retried = true;
|
|
2931
|
-
outcome = await waitForExit(res.id,
|
|
3086
|
+
outcome = await waitForExit(res.id, null, _signal);
|
|
2932
3087
|
}
|
|
2933
3088
|
}
|
|
3089
|
+
if (ctx.hasUI) await refreshWidget(ctx);
|
|
2934
3090
|
return {
|
|
2935
3091
|
content: [{ type: "text", text: `${retried ? "Retried once after external worker death.\n" : ""}${outcome.text}` }],
|
|
2936
3092
|
isError: !outcome.ok,
|
|
@@ -2946,9 +3102,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2946
3102
|
}
|
|
2947
3103
|
|
|
2948
3104
|
// Keep ordinary quick commands ergonomic. Give the process a short grace
|
|
2949
|
-
// period; if it exits, return
|
|
2950
|
-
// A
|
|
2951
|
-
//
|
|
3105
|
+
// period; if it exits, return lifecycle metadata + log path immediately.
|
|
3106
|
+
// A process still running after the grace follows the parked-turn /
|
|
3107
|
+
// automatic-notification contract below.
|
|
2952
3108
|
await bs(["wait", "-s", res.id, "--timeout", QUICK_COMMAND_GRACE], { signal: _signal });
|
|
2953
3109
|
let quickStatus = await statusOf(res.id);
|
|
2954
3110
|
let retried = false;
|
|
@@ -3036,6 +3192,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3036
3192
|
// The branch above guarantees a successful plan in subagent mode.
|
|
3037
3193
|
const subagentNesting = nesting as Extract<SubagentSpawnPlan, { allowed: true }>;
|
|
3038
3194
|
const res = await spawnSubagent({
|
|
3195
|
+
name: params.name,
|
|
3039
3196
|
agent,
|
|
3040
3197
|
task: params.task as string,
|
|
3041
3198
|
model: params.model,
|
|
@@ -3081,6 +3238,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3081
3238
|
details: {
|
|
3082
3239
|
id: res.id,
|
|
3083
3240
|
kind: "subagent",
|
|
3241
|
+
name: params.name ?? res.id,
|
|
3084
3242
|
agent: agent?.name,
|
|
3085
3243
|
model: res.model,
|
|
3086
3244
|
task: params.task,
|
|
@@ -3140,14 +3298,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
3140
3298
|
name: "babysit_check",
|
|
3141
3299
|
label: "Babysit: check",
|
|
3142
3300
|
description:
|
|
3143
|
-
"Inspect babysit session(s). Without an id: lists
|
|
3144
|
-
"With an id: a process shows state + recent
|
|
3301
|
+
"Inspect babysit session(s). Without an id: lists running sessions by default; use `state: \"all\"` " +
|
|
3302
|
+
"for history, or filter by terminal state/kind. With an id: a process shows state + recent " +
|
|
3303
|
+
"output, searches its log with `pattern`, " +
|
|
3145
3304
|
"or captures the rendered screen with `screen: true`; a subagent shows live progress " +
|
|
3146
3305
|
"(or raw log matches with `pattern`). Results are bounded by `lines` and clipped. " +
|
|
3147
3306
|
"Do NOT poll this while merely waiting for a process to end — the exit notification is automatic.",
|
|
3148
3307
|
promptSnippet: "Check status/progress of babysit sessions (processes and subagents)",
|
|
3149
3308
|
parameters: Type.Object({
|
|
3150
|
-
id: Type.Optional(Type.String({ description: "Session id. Omit to list
|
|
3309
|
+
id: Type.Optional(Type.String({ description: "Session id. Omit to list sessions." })),
|
|
3310
|
+
state: Type.Optional(
|
|
3311
|
+
StringEnum(["running", "terminal", "all"] as const, {
|
|
3312
|
+
description: "List mode only: state filter. Defaults to running; terminal means any non-running state.",
|
|
3313
|
+
}),
|
|
3314
|
+
),
|
|
3315
|
+
kind: Type.Optional(
|
|
3316
|
+
StringEnum(["process", "subagent", "all"] as const, {
|
|
3317
|
+
description: "List mode only: session kind filter. Defaults to all.",
|
|
3318
|
+
}),
|
|
3319
|
+
),
|
|
3151
3320
|
tools: Type.Optional(
|
|
3152
3321
|
Type.Number({ description: "Subagent: how many recent tool calls to show (default 8, max 50)." }),
|
|
3153
3322
|
),
|
|
@@ -3179,9 +3348,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
3179
3348
|
};
|
|
3180
3349
|
}
|
|
3181
3350
|
if (sessions.length === 0) {
|
|
3182
|
-
return { content: [{ type: "text", text: "No babysit sessions." }], details: {} };
|
|
3351
|
+
return { content: [{ type: "text", text: "No babysit sessions." }], details: { sessions: [] } };
|
|
3183
3352
|
}
|
|
3184
|
-
const
|
|
3353
|
+
const stateFilter = params.state ?? "running";
|
|
3354
|
+
const kindFilter = params.kind ?? "all";
|
|
3355
|
+
const stateMatches = (session: BsSession) => stateFilter === "all"
|
|
3356
|
+
? true
|
|
3357
|
+
: stateFilter === "running"
|
|
3358
|
+
? session.state === "running"
|
|
3359
|
+
: session.state !== "running";
|
|
3360
|
+
const kindOfSession = (session: BsSession) => readMeta(session.id)?.kind ?? "process";
|
|
3361
|
+
const selected = sessions.filter((session) =>
|
|
3362
|
+
stateMatches(session) && (kindFilter === "all" || kindOfSession(session) === kindFilter),
|
|
3363
|
+
);
|
|
3364
|
+
const reveal: string[] = [];
|
|
3365
|
+
if (stateFilter !== "all" && sessions.some((session) => !stateMatches(session))) {
|
|
3366
|
+
reveal.push('state: "all"');
|
|
3367
|
+
}
|
|
3368
|
+
if (kindFilter !== "all" && sessions.some((session) => kindOfSession(session) !== kindFilter)) {
|
|
3369
|
+
reveal.push('kind: "all"');
|
|
3370
|
+
}
|
|
3371
|
+
const revealHint = reveal.length > 0 ? `; use ${reveal.join(" and ")} to widen the list` : "";
|
|
3372
|
+
if (selected.length === 0) {
|
|
3373
|
+
const hidden = sessions.length;
|
|
3374
|
+
return {
|
|
3375
|
+
content: [{
|
|
3376
|
+
type: "text",
|
|
3377
|
+
text: `No ${stateFilter}${kindFilter === "all" ? "" : ` ${kindFilter}`} babysit sessions.${hidden > 0 ? ` ${hidden} session(s) hidden${revealHint}.` : ""}`,
|
|
3378
|
+
}],
|
|
3379
|
+
details: { sessions: [], total: sessions.length, hidden, state: stateFilter, kind: kindFilter },
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
const lines = selected.map((s) => {
|
|
3185
3383
|
const meta = readMeta(s.id);
|
|
3186
3384
|
const kind = meta?.kind ?? "process";
|
|
3187
3385
|
const flag = s.note ? ` ⚑ ${s.note}` : "";
|
|
@@ -3194,7 +3392,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
3194
3392
|
const preview = what.length > 60 ? `${what.slice(0, 57)}…` : what;
|
|
3195
3393
|
return `${s.id} [${kind}] ${s.state}${ec}${depth}${flag}${preview ? ` — ${preview}` : ""}`;
|
|
3196
3394
|
});
|
|
3197
|
-
|
|
3395
|
+
const hidden = sessions.length - selected.length;
|
|
3396
|
+
const suffix = hidden > 0 ? `\n… ${hidden} session(s) hidden${revealHint}.` : "";
|
|
3397
|
+
return {
|
|
3398
|
+
content: [{ type: "text", text: clip(lines.join("\n") + suffix) }],
|
|
3399
|
+
details: { sessions: selected, total: sessions.length, hidden, state: stateFilter, kind: kindFilter },
|
|
3400
|
+
};
|
|
3198
3401
|
}
|
|
3199
3402
|
|
|
3200
3403
|
const st = await statusOf(params.id);
|
package/package.json
CHANGED
package/rpc-stream-proxy.mjs
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* message. Persisting every snapshot makes babysit logs grow quadratically.
|
|
9
9
|
*
|
|
10
10
|
* This proxy forwards stdin byte-for-byte. `message_update` snapshots are
|
|
11
|
-
* always compacted.
|
|
12
|
-
* lifecycle/tool events are
|
|
11
|
+
* always compacted. Compact log mode is the default: redundant payloads on
|
|
12
|
+
* lifecycle/tool events are removed while authoritative `message_end`,
|
|
13
13
|
* response, and error events remain intact. Set PI_BABYSIT_RPC_LOG_MODE=standard
|
|
14
|
-
* to
|
|
14
|
+
* to opt back into the legacy lifecycle payloads.
|
|
15
15
|
*/
|
|
16
16
|
import { spawn } from "node:child_process";
|
|
17
17
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -44,7 +44,7 @@ function isParkedMessages(messages) {
|
|
|
44
44
|
|
|
45
45
|
export function compactRpcLine(
|
|
46
46
|
line,
|
|
47
|
-
mode = process.env.PI_BABYSIT_RPC_LOG_MODE ?? "
|
|
47
|
+
mode = process.env.PI_BABYSIT_RPC_LOG_MODE ?? "compact",
|
|
48
48
|
) {
|
|
49
49
|
if (!line.trimStart().startsWith("{")) return line;
|
|
50
50
|
|