@yusukeshib/pi-babysit 0.3.14 → 0.3.15
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 +239 -53
- 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 {
|
|
@@ -1587,13 +1603,16 @@ function taskProgressOf(id: string): { progress: Progress; offset: number } {
|
|
|
1587
1603
|
// ---------------------------------------------------------------------------
|
|
1588
1604
|
|
|
1589
1605
|
// Friendly name → unique babysit session id (babysit ids allow [\w.-]).
|
|
1590
|
-
|
|
1606
|
+
const reservedSessionIds = new Set<string>();
|
|
1607
|
+
|
|
1608
|
+
async function reserveUniqueSessionId(name: string): Promise<string> {
|
|
1591
1609
|
const base = name.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "proc";
|
|
1592
1610
|
const taken = new Set((await listSessions()).sessions.map((s) => s.id));
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1611
|
+
for (const id of reservedSessionIds) taken.add(id);
|
|
1612
|
+
let id = base;
|
|
1613
|
+
for (let i = 2; taken.has(id); i++) id = `${base}-${i}`;
|
|
1614
|
+
reservedSessionIds.add(id);
|
|
1615
|
+
return id;
|
|
1597
1616
|
}
|
|
1598
1617
|
|
|
1599
1618
|
interface ProcOpts {
|
|
@@ -1612,10 +1631,16 @@ async function spawnProcess(opts: ProcOpts): Promise<{ id: string } | { error: s
|
|
|
1612
1631
|
if (opts.timeout && opts.timeout !== "none") bsArgs.push("--timeout", opts.timeout);
|
|
1613
1632
|
if (opts.idleTimeout && opts.idleTimeout !== "none")
|
|
1614
1633
|
bsArgs.push("--idle-timeout", opts.idleTimeout);
|
|
1615
|
-
|
|
1634
|
+
const reservedId = opts.name ? await reserveUniqueSessionId(opts.name) : undefined;
|
|
1635
|
+
if (reservedId) bsArgs.push("--id", reservedId);
|
|
1616
1636
|
bsArgs.push("--", SHELL, "-c", opts.command);
|
|
1617
1637
|
|
|
1618
|
-
|
|
1638
|
+
let r: Awaited<ReturnType<typeof bs>>;
|
|
1639
|
+
try {
|
|
1640
|
+
r = await bs(bsArgs, { cwd: opts.cwd });
|
|
1641
|
+
} finally {
|
|
1642
|
+
if (reservedId) reservedSessionIds.delete(reservedId);
|
|
1643
|
+
}
|
|
1619
1644
|
if (r.code !== 0) {
|
|
1620
1645
|
return {
|
|
1621
1646
|
error:
|
|
@@ -1687,6 +1712,7 @@ function discardDelivery(delivery: DeliverableMessage): void {
|
|
|
1687
1712
|
}
|
|
1688
1713
|
|
|
1689
1714
|
interface SubagentOpts {
|
|
1715
|
+
name?: string;
|
|
1690
1716
|
agent?: AgentConfig;
|
|
1691
1717
|
task: string;
|
|
1692
1718
|
model?: string;
|
|
@@ -1720,7 +1746,15 @@ async function spawnSubagent(
|
|
|
1720
1746
|
};
|
|
1721
1747
|
}
|
|
1722
1748
|
if (tools && tools.length > 0) piArgs.push("--tools", tools.join(","));
|
|
1723
|
-
piArgs.push(
|
|
1749
|
+
piArgs.push(
|
|
1750
|
+
"--append-system-prompt",
|
|
1751
|
+
subagentGuidance(
|
|
1752
|
+
opts.depth,
|
|
1753
|
+
opts.maxDepth,
|
|
1754
|
+
isAllowedDirectBash("") && (!tools || tools.includes("bash")),
|
|
1755
|
+
!tools || tools.includes("babysit_run"),
|
|
1756
|
+
),
|
|
1757
|
+
);
|
|
1724
1758
|
let promptTempFile: string | undefined;
|
|
1725
1759
|
if (opts.agent?.systemPrompt?.trim()) {
|
|
1726
1760
|
promptTempFile = writePromptTempFile(opts.agent.name, opts.agent.systemPrompt);
|
|
@@ -1758,6 +1792,8 @@ async function spawnSubagent(
|
|
|
1758
1792
|
if (opts.idleTimeout && opts.idleTimeout !== "none") {
|
|
1759
1793
|
bsArgs.push("--idle-timeout", opts.idleTimeout);
|
|
1760
1794
|
}
|
|
1795
|
+
const reservedId = opts.name ? await reserveUniqueSessionId(opts.name) : undefined;
|
|
1796
|
+
if (reservedId) bsArgs.push("--id", reservedId);
|
|
1761
1797
|
bsArgs.push(
|
|
1762
1798
|
"--",
|
|
1763
1799
|
process.execPath,
|
|
@@ -1767,13 +1803,18 @@ async function spawnSubagent(
|
|
|
1767
1803
|
...piArgs,
|
|
1768
1804
|
);
|
|
1769
1805
|
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1806
|
+
let r: Awaited<ReturnType<typeof bs>>;
|
|
1807
|
+
try {
|
|
1808
|
+
r = await bs(bsArgs, {
|
|
1809
|
+
cwd: opts.cwd,
|
|
1810
|
+
env: {
|
|
1811
|
+
[SUBAGENT_DEPTH_ENV]: String(opts.depth),
|
|
1812
|
+
[SUBAGENT_MAX_DEPTH_ENV]: String(opts.maxDepth),
|
|
1813
|
+
},
|
|
1814
|
+
});
|
|
1815
|
+
} finally {
|
|
1816
|
+
if (reservedId) reservedSessionIds.delete(reservedId);
|
|
1817
|
+
}
|
|
1777
1818
|
if (r.code !== 0) {
|
|
1778
1819
|
cleanupPromptTemp();
|
|
1779
1820
|
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 +1833,7 @@ async function spawnSubagent(
|
|
|
1792
1833
|
// The success path overwrites this with the full task meta.
|
|
1793
1834
|
writeMeta(id, {
|
|
1794
1835
|
kind: "subagent",
|
|
1836
|
+
name: opts.name ?? id,
|
|
1795
1837
|
task: opts.task,
|
|
1796
1838
|
notified: true,
|
|
1797
1839
|
depth: opts.depth,
|
|
@@ -1851,6 +1893,7 @@ async function spawnSubagent(
|
|
|
1851
1893
|
|
|
1852
1894
|
writeMeta(id, {
|
|
1853
1895
|
kind: "subagent",
|
|
1896
|
+
name: opts.name ?? id,
|
|
1854
1897
|
task: opts.task,
|
|
1855
1898
|
promptOffset: resp.offset,
|
|
1856
1899
|
model: resolvedModel,
|
|
@@ -2136,9 +2179,15 @@ async function waitForExit(
|
|
|
2136
2179
|
|
|
2137
2180
|
if (expectPattern) {
|
|
2138
2181
|
const e = await bs(["expect", "-s", id, "--timeout", t, expectPattern], { signal });
|
|
2139
|
-
if (signal?.aborted
|
|
2182
|
+
if (signal?.aborted) {
|
|
2140
2183
|
return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
|
|
2141
2184
|
}
|
|
2185
|
+
if (e.code === 130) {
|
|
2186
|
+
const interruptedStatus = await statusOf(id);
|
|
2187
|
+
if (interruptedStatus?.state === "running") {
|
|
2188
|
+
return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2142
2191
|
if (e.code === 0) {
|
|
2143
2192
|
return {
|
|
2144
2193
|
id,
|
|
@@ -2164,10 +2213,17 @@ async function waitForExit(
|
|
|
2164
2213
|
// another wait that is still pending.
|
|
2165
2214
|
updateWaitReservation(id, "reserve");
|
|
2166
2215
|
const w = await bs(["wait", "-s", id, "--timeout", t], { signal });
|
|
2167
|
-
if (signal?.aborted
|
|
2216
|
+
if (signal?.aborted) {
|
|
2168
2217
|
updateWaitReservation(id, "abandon");
|
|
2169
2218
|
return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
|
|
2170
2219
|
}
|
|
2220
|
+
if (w.code === 130) {
|
|
2221
|
+
const interruptedStatus = await statusOf(id);
|
|
2222
|
+
if (interruptedStatus?.state === "running") {
|
|
2223
|
+
updateWaitReservation(id, "abandon");
|
|
2224
|
+
return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2171
2227
|
if (w.code === 124) {
|
|
2172
2228
|
// 124 is ambiguous (timeout vs child exiting 124) — disambiguate.
|
|
2173
2229
|
const st0 = await statusOf(id);
|
|
@@ -2258,6 +2314,44 @@ export function activeToolsWithoutDirectBash(activeTools: string[], allowDirectB
|
|
|
2258
2314
|
return allowDirectBash ? activeTools : activeTools.filter((name) => name !== "bash");
|
|
2259
2315
|
}
|
|
2260
2316
|
|
|
2317
|
+
export function automaticNotificationGroup(entry: unknown): string | undefined {
|
|
2318
|
+
const candidate = entry as {
|
|
2319
|
+
id?: unknown;
|
|
2320
|
+
type?: unknown;
|
|
2321
|
+
message?: { role?: unknown; content?: unknown };
|
|
2322
|
+
};
|
|
2323
|
+
if (
|
|
2324
|
+
candidate?.type !== "message" ||
|
|
2325
|
+
candidate.message?.role !== "assistant" ||
|
|
2326
|
+
typeof candidate.id !== "string" ||
|
|
2327
|
+
!Array.isArray(candidate.message.content)
|
|
2328
|
+
) {
|
|
2329
|
+
return undefined;
|
|
2330
|
+
}
|
|
2331
|
+
const group = `turn-${candidate.id}`;
|
|
2332
|
+
const runs = candidate.message.content.filter((part) => {
|
|
2333
|
+
if (!part || typeof part !== "object") return false;
|
|
2334
|
+
const call = part as {
|
|
2335
|
+
type?: unknown;
|
|
2336
|
+
name?: unknown;
|
|
2337
|
+
arguments?: Record<string, unknown>;
|
|
2338
|
+
};
|
|
2339
|
+
if (call.type !== "toolCall" || call.name !== "babysit_run") return false;
|
|
2340
|
+
const args = call.arguments ?? {};
|
|
2341
|
+
const existing = typeof args.notificationGroup === "string"
|
|
2342
|
+
? args.notificationGroup.trim()
|
|
2343
|
+
: "";
|
|
2344
|
+
return (
|
|
2345
|
+
typeof args.command === "string" &&
|
|
2346
|
+
args.command.length > 0 &&
|
|
2347
|
+
args.profile !== "subagent" &&
|
|
2348
|
+
args.foreground !== true &&
|
|
2349
|
+
(existing === "" || existing === group)
|
|
2350
|
+
);
|
|
2351
|
+
});
|
|
2352
|
+
return runs.length >= 2 ? group : undefined;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2261
2355
|
// ---------------------------------------------------------------------------
|
|
2262
2356
|
// extension
|
|
2263
2357
|
// ---------------------------------------------------------------------------
|
|
@@ -2688,7 +2782,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
2688
2782
|
rootLeasePath = undefined;
|
|
2689
2783
|
});
|
|
2690
2784
|
|
|
2691
|
-
pi.on("tool_call", async (event) => {
|
|
2785
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
2786
|
+
if (event.toolName === "babysit_run") {
|
|
2787
|
+
const input = event.input as {
|
|
2788
|
+
command?: unknown;
|
|
2789
|
+
profile?: unknown;
|
|
2790
|
+
foreground?: unknown;
|
|
2791
|
+
notificationGroup?: unknown;
|
|
2792
|
+
};
|
|
2793
|
+
if (
|
|
2794
|
+
typeof input.command === "string" &&
|
|
2795
|
+
input.profile !== "subagent" &&
|
|
2796
|
+
input.foreground !== true &&
|
|
2797
|
+
(typeof input.notificationGroup !== "string" || input.notificationGroup.trim() === "")
|
|
2798
|
+
) {
|
|
2799
|
+
const group = automaticNotificationGroup(ctx.sessionManager.getLeafEntry());
|
|
2800
|
+
if (group) input.notificationGroup = group;
|
|
2801
|
+
}
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2692
2804
|
if (event.toolName !== "bash") return;
|
|
2693
2805
|
const command = String((event.input as { command?: unknown }).command ?? "");
|
|
2694
2806
|
if (backgroundsItself(command)) {
|
|
@@ -2714,10 +2826,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
2714
2826
|
name: "babysit_run",
|
|
2715
2827
|
label: "Babysit: run",
|
|
2716
2828
|
description:
|
|
2717
|
-
"Run any shell command in a supervised babysit session.
|
|
2718
|
-
"
|
|
2719
|
-
"
|
|
2720
|
-
"
|
|
2829
|
+
"Run any shell command in a supervised babysit session. Set `foreground: true` when the next step " +
|
|
2830
|
+
"needs the exit result in this tool call. Otherwise commands that finish within a short grace period " +
|
|
2831
|
+
"return completion metadata immediately; longer commands continue in the background and trigger an " +
|
|
2832
|
+
"automatic notification on exit. Sibling background runs in one assistant message are grouped " +
|
|
2833
|
+
"automatically. Complete output is returned inline only when it is small; larger output stays " +
|
|
2834
|
+
"in the log path for bounded inspection with babysit_check. " +
|
|
2721
2835
|
"In non-interactive mode (`pi -p`, no UI), process mode blocks until exit because there is no " +
|
|
2722
2836
|
"notification loop. Two modes: (1) `command` — run any shell command, including builds, tests, " +
|
|
2723
2837
|
"dev servers, watchers, and interactive TUIs; you can type into it with babysit_send and read " +
|
|
@@ -2730,13 +2844,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
2730
2844
|
promptSnippet:
|
|
2731
2845
|
"Run any shell command with context-safe captured output; quick commands return metadata, longer ones continue in background",
|
|
2732
2846
|
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
|
|
2847
|
+
"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`.",
|
|
2848
|
+
"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
2849
|
"Bundle closely related tiny observations into one babysit_run command when that reduces tool turns without obscuring lifecycle or failure handling.",
|
|
2735
2850
|
"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
2851
|
"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
2852
|
"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
2853
|
"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
2854
|
"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.",
|
|
2855
|
+
"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
2856
|
"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
2857
|
"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
2858
|
],
|
|
@@ -2748,7 +2864,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2748
2864
|
),
|
|
2749
2865
|
name: Type.Optional(
|
|
2750
2866
|
Type.String({
|
|
2751
|
-
description: "Friendly stable name for a process (becomes the session id), e.g. 'cargo-build'.",
|
|
2867
|
+
description: "Friendly stable name for a process or subagent (becomes the session id), e.g. 'cargo-build' or 'review-api'.",
|
|
2752
2868
|
}),
|
|
2753
2869
|
),
|
|
2754
2870
|
profile: Type.Optional(
|
|
@@ -2814,10 +2930,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
2814
2930
|
"Process mode: run in a PTY (default true; enables interactive input/screen). false = plain pipes for cleaner line-oriented logs.",
|
|
2815
2931
|
}),
|
|
2816
2932
|
),
|
|
2933
|
+
foreground: Type.Optional(
|
|
2934
|
+
Type.Boolean({
|
|
2935
|
+
description:
|
|
2936
|
+
"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.",
|
|
2937
|
+
}),
|
|
2938
|
+
),
|
|
2817
2939
|
notificationGroup: Type.Optional(
|
|
2818
2940
|
Type.String({
|
|
2819
2941
|
description:
|
|
2820
|
-
"Process mode: defer automatic completion until every running process with this group has stopped, then send one batched notification.",
|
|
2942
|
+
"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
2943
|
}),
|
|
2822
2944
|
),
|
|
2823
2945
|
continueAfterStart: Type.Optional(
|
|
@@ -2877,6 +2999,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
2877
2999
|
details: {},
|
|
2878
3000
|
};
|
|
2879
3001
|
}
|
|
3002
|
+
if (isSubagent && params.foreground) {
|
|
3003
|
+
return {
|
|
3004
|
+
content: [{ type: "text", text: "`foreground` is available only in process mode; use babysit_wait for subagent task completion." }],
|
|
3005
|
+
isError: true,
|
|
3006
|
+
details: {},
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
if (!isSubagent && params.foreground && params.continueAfterStart) {
|
|
3010
|
+
return {
|
|
3011
|
+
content: [{ type: "text", text: "`foreground` and `continueAfterStart` are mutually exclusive." }],
|
|
3012
|
+
isError: true,
|
|
3013
|
+
details: {},
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
2880
3016
|
|
|
2881
3017
|
// Compute nesting only for subagent mode. Ordinary command processes remain
|
|
2882
3018
|
// available even when the hosting agent is at its subagent depth limit.
|
|
@@ -2913,24 +3049,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
2913
3049
|
pollNeeded = true;
|
|
2914
3050
|
await refreshWidget(ctx);
|
|
2915
3051
|
|
|
2916
|
-
// One-shot / non-interactive mode
|
|
2917
|
-
//
|
|
2918
|
-
//
|
|
2919
|
-
//
|
|
2920
|
-
//
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
3052
|
+
// One-shot / non-interactive mode has no event loop that can deliver an
|
|
3053
|
+
// exit notification. `foreground: true` provides the same single-tool-call
|
|
3054
|
+
// result in interactive mode, avoiding a separate babysit_wait model turn.
|
|
3055
|
+
// The command remains supervised, logged, killable, and subject to its
|
|
3056
|
+
// babysit timeout in either case.
|
|
3057
|
+
if (!ctx.hasUI || params.foreground) {
|
|
3058
|
+
// The babysit supervisor owns the absolute command timeout. Waiting with
|
|
3059
|
+
// the same deadline here races its terminal-state write and can return a
|
|
3060
|
+
// false "still running" result at the boundary, so wait for the
|
|
3061
|
+
// supervisor's definitive exit instead.
|
|
3062
|
+
let outcome = await waitForExit(res.id, null, _signal);
|
|
2925
3063
|
let retried = false;
|
|
2926
3064
|
if (params.retryOnWorkerDeath && outcome.status?.state === "dead" && outcome.status.exit_code == null) {
|
|
2927
3065
|
const retry = await spawnProcess(spawnOpts);
|
|
2928
3066
|
if (!("error" in retry)) {
|
|
2929
3067
|
res = retry;
|
|
2930
3068
|
retried = true;
|
|
2931
|
-
outcome = await waitForExit(res.id,
|
|
3069
|
+
outcome = await waitForExit(res.id, null, _signal);
|
|
2932
3070
|
}
|
|
2933
3071
|
}
|
|
3072
|
+
if (ctx.hasUI) await refreshWidget(ctx);
|
|
2934
3073
|
return {
|
|
2935
3074
|
content: [{ type: "text", text: `${retried ? "Retried once after external worker death.\n" : ""}${outcome.text}` }],
|
|
2936
3075
|
isError: !outcome.ok,
|
|
@@ -2946,9 +3085,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2946
3085
|
}
|
|
2947
3086
|
|
|
2948
3087
|
// Keep ordinary quick commands ergonomic. Give the process a short grace
|
|
2949
|
-
// period; if it exits, return
|
|
2950
|
-
// A
|
|
2951
|
-
//
|
|
3088
|
+
// period; if it exits, return lifecycle metadata + log path immediately.
|
|
3089
|
+
// A process still running after the grace follows the parked-turn /
|
|
3090
|
+
// automatic-notification contract below.
|
|
2952
3091
|
await bs(["wait", "-s", res.id, "--timeout", QUICK_COMMAND_GRACE], { signal: _signal });
|
|
2953
3092
|
let quickStatus = await statusOf(res.id);
|
|
2954
3093
|
let retried = false;
|
|
@@ -3036,6 +3175,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3036
3175
|
// The branch above guarantees a successful plan in subagent mode.
|
|
3037
3176
|
const subagentNesting = nesting as Extract<SubagentSpawnPlan, { allowed: true }>;
|
|
3038
3177
|
const res = await spawnSubagent({
|
|
3178
|
+
name: params.name,
|
|
3039
3179
|
agent,
|
|
3040
3180
|
task: params.task as string,
|
|
3041
3181
|
model: params.model,
|
|
@@ -3081,6 +3221,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3081
3221
|
details: {
|
|
3082
3222
|
id: res.id,
|
|
3083
3223
|
kind: "subagent",
|
|
3224
|
+
name: params.name ?? res.id,
|
|
3084
3225
|
agent: agent?.name,
|
|
3085
3226
|
model: res.model,
|
|
3086
3227
|
task: params.task,
|
|
@@ -3140,14 +3281,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
3140
3281
|
name: "babysit_check",
|
|
3141
3282
|
label: "Babysit: check",
|
|
3142
3283
|
description:
|
|
3143
|
-
"Inspect babysit session(s). Without an id: lists
|
|
3144
|
-
"With an id: a process shows state + recent
|
|
3284
|
+
"Inspect babysit session(s). Without an id: lists running sessions by default; use `state: \"all\"` " +
|
|
3285
|
+
"for history, or filter by terminal state/kind. With an id: a process shows state + recent " +
|
|
3286
|
+
"output, searches its log with `pattern`, " +
|
|
3145
3287
|
"or captures the rendered screen with `screen: true`; a subagent shows live progress " +
|
|
3146
3288
|
"(or raw log matches with `pattern`). Results are bounded by `lines` and clipped. " +
|
|
3147
3289
|
"Do NOT poll this while merely waiting for a process to end — the exit notification is automatic.",
|
|
3148
3290
|
promptSnippet: "Check status/progress of babysit sessions (processes and subagents)",
|
|
3149
3291
|
parameters: Type.Object({
|
|
3150
|
-
id: Type.Optional(Type.String({ description: "Session id. Omit to list
|
|
3292
|
+
id: Type.Optional(Type.String({ description: "Session id. Omit to list sessions." })),
|
|
3293
|
+
state: Type.Optional(
|
|
3294
|
+
StringEnum(["running", "terminal", "all"] as const, {
|
|
3295
|
+
description: "List mode only: state filter. Defaults to running; terminal means any non-running state.",
|
|
3296
|
+
}),
|
|
3297
|
+
),
|
|
3298
|
+
kind: Type.Optional(
|
|
3299
|
+
StringEnum(["process", "subagent", "all"] as const, {
|
|
3300
|
+
description: "List mode only: session kind filter. Defaults to all.",
|
|
3301
|
+
}),
|
|
3302
|
+
),
|
|
3151
3303
|
tools: Type.Optional(
|
|
3152
3304
|
Type.Number({ description: "Subagent: how many recent tool calls to show (default 8, max 50)." }),
|
|
3153
3305
|
),
|
|
@@ -3179,9 +3331,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
3179
3331
|
};
|
|
3180
3332
|
}
|
|
3181
3333
|
if (sessions.length === 0) {
|
|
3182
|
-
return { content: [{ type: "text", text: "No babysit sessions." }], details: {} };
|
|
3334
|
+
return { content: [{ type: "text", text: "No babysit sessions." }], details: { sessions: [] } };
|
|
3335
|
+
}
|
|
3336
|
+
const stateFilter = params.state ?? "running";
|
|
3337
|
+
const kindFilter = params.kind ?? "all";
|
|
3338
|
+
const stateMatches = (session: BsSession) => stateFilter === "all"
|
|
3339
|
+
? true
|
|
3340
|
+
: stateFilter === "running"
|
|
3341
|
+
? session.state === "running"
|
|
3342
|
+
: session.state !== "running";
|
|
3343
|
+
const kindOfSession = (session: BsSession) => readMeta(session.id)?.kind ?? "process";
|
|
3344
|
+
const selected = sessions.filter((session) =>
|
|
3345
|
+
stateMatches(session) && (kindFilter === "all" || kindOfSession(session) === kindFilter),
|
|
3346
|
+
);
|
|
3347
|
+
const reveal: string[] = [];
|
|
3348
|
+
if (stateFilter !== "all" && sessions.some((session) => !stateMatches(session))) {
|
|
3349
|
+
reveal.push('state: "all"');
|
|
3350
|
+
}
|
|
3351
|
+
if (kindFilter !== "all" && sessions.some((session) => kindOfSession(session) !== kindFilter)) {
|
|
3352
|
+
reveal.push('kind: "all"');
|
|
3353
|
+
}
|
|
3354
|
+
const revealHint = reveal.length > 0 ? `; use ${reveal.join(" and ")} to widen the list` : "";
|
|
3355
|
+
if (selected.length === 0) {
|
|
3356
|
+
const hidden = sessions.length;
|
|
3357
|
+
return {
|
|
3358
|
+
content: [{
|
|
3359
|
+
type: "text",
|
|
3360
|
+
text: `No ${stateFilter}${kindFilter === "all" ? "" : ` ${kindFilter}`} babysit sessions.${hidden > 0 ? ` ${hidden} session(s) hidden${revealHint}.` : ""}`,
|
|
3361
|
+
}],
|
|
3362
|
+
details: { sessions: [], total: sessions.length, hidden, state: stateFilter, kind: kindFilter },
|
|
3363
|
+
};
|
|
3183
3364
|
}
|
|
3184
|
-
const lines =
|
|
3365
|
+
const lines = selected.map((s) => {
|
|
3185
3366
|
const meta = readMeta(s.id);
|
|
3186
3367
|
const kind = meta?.kind ?? "process";
|
|
3187
3368
|
const flag = s.note ? ` ⚑ ${s.note}` : "";
|
|
@@ -3194,7 +3375,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
3194
3375
|
const preview = what.length > 60 ? `${what.slice(0, 57)}…` : what;
|
|
3195
3376
|
return `${s.id} [${kind}] ${s.state}${ec}${depth}${flag}${preview ? ` — ${preview}` : ""}`;
|
|
3196
3377
|
});
|
|
3197
|
-
|
|
3378
|
+
const hidden = sessions.length - selected.length;
|
|
3379
|
+
const suffix = hidden > 0 ? `\n… ${hidden} session(s) hidden${revealHint}.` : "";
|
|
3380
|
+
return {
|
|
3381
|
+
content: [{ type: "text", text: clip(lines.join("\n") + suffix) }],
|
|
3382
|
+
details: { sessions: selected, total: sessions.length, hidden, state: stateFilter, kind: kindFilter },
|
|
3383
|
+
};
|
|
3198
3384
|
}
|
|
3199
3385
|
|
|
3200
3386
|
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
|
|