@yusukeshib/pi-babysit 0.3.8 → 0.3.9
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 +8 -1
- package/index.ts +136 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,13 @@ The **profile is a tool parameter, not a separate tool set**: domain knowledge
|
|
|
40
40
|
message delivery, spawn validation) lives in code, while the LLM sees one small
|
|
41
41
|
generic surface.
|
|
42
42
|
|
|
43
|
+
Subagent recursion is **disabled by default**: a top-level pi may create workers
|
|
44
|
+
(depth 1), but those workers cannot create more workers. A top-level caller can
|
|
45
|
+
explicitly opt in for a specific tree with `maxDepth: 2` (or higher) when it
|
|
46
|
+
creates the first worker. Descendants inherit that ceiling and cannot raise it.
|
|
47
|
+
Normal `babysit_run { command }` process execution remains available at every
|
|
48
|
+
depth.
|
|
49
|
+
|
|
43
50
|
Because sessions are real PTYs, the agent can also **drive interactive
|
|
44
51
|
programs** (installers, wizards, REPLs): type with `babysit_send`
|
|
45
52
|
(text or named keys) and read the rendered screen with
|
|
@@ -49,7 +56,7 @@ programs** (installers, wizards, REPLs): type with `babysit_send`
|
|
|
49
56
|
|
|
50
57
|
| Tool | What it does |
|
|
51
58
|
| ---- | ------------ |
|
|
52
|
-
| `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`) or start a subagent (`profile: "subagent"`, `task`, optional `agent`/`model`/`tools`). Quick commands return inline; longer ones continue in the background |
|
|
59
|
+
| `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`) or start a subagent (`profile: "subagent"`, `task`, optional `agent`/`model`/`tools`/`maxDepth`). `maxDepth` defaults to 1 and can only be set by the top-level caller. Quick commands return inline; longer ones continue in the background |
|
|
53
60
|
| `babysit_check` | List all sessions, inspect one, tail its bounded recent output, or search its raw log with `pattern`; `screen: true` captures TUIs and subagents otherwise show structured live progress |
|
|
54
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`) |
|
|
55
62
|
| `babysit_wait` | Block until done: process exit (or `expect: "regex"` readiness marker), subagent task completion. Multi-wait: `ids` + `mode: "any"\|"all"` |
|
package/index.ts
CHANGED
|
@@ -52,6 +52,78 @@ let ROOT = ROOT_BASE;
|
|
|
52
52
|
const PI_BIN = process.env.PI_BABYSIT_BIN ?? "pi";
|
|
53
53
|
const BABYSIT_BIN = process.env.PI_BABYSIT_CLI ?? "babysit";
|
|
54
54
|
const SHELL = process.env.SHELL ?? "sh";
|
|
55
|
+
const SUBAGENT_DEPTH_ENV = "PI_BABYSIT_INTERNAL_SUBAGENT_DEPTH";
|
|
56
|
+
const SUBAGENT_MAX_DEPTH_ENV = "PI_BABYSIT_INTERNAL_SUBAGENT_MAX_DEPTH";
|
|
57
|
+
const DEFAULT_SUBAGENT_MAX_DEPTH = 1;
|
|
58
|
+
|
|
59
|
+
export type SubagentSpawnPlan =
|
|
60
|
+
| { allowed: true; childDepth: number; maxDepth: number }
|
|
61
|
+
| { allowed: false; error: string };
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Plan a subagent spawn without letting an already-spawned worker raise its
|
|
65
|
+
* inherited recursion allowance. Depth 0 is the user-facing pi process; the
|
|
66
|
+
* first worker is depth 1 and is allowed by default, but that worker cannot
|
|
67
|
+
* create depth 2 unless its top-level parent explicitly opted in.
|
|
68
|
+
*/
|
|
69
|
+
export function planSubagentSpawn(
|
|
70
|
+
requestedMaxDepth?: number,
|
|
71
|
+
env: Record<string, string | undefined> = process.env,
|
|
72
|
+
): SubagentSpawnPlan {
|
|
73
|
+
if (
|
|
74
|
+
requestedMaxDepth !== undefined &&
|
|
75
|
+
(!Number.isInteger(requestedMaxDepth) || requestedMaxDepth < 1)
|
|
76
|
+
) {
|
|
77
|
+
return { allowed: false, error: "`maxDepth` must be a positive integer." };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const rawDepth = env[SUBAGENT_DEPTH_ENV];
|
|
81
|
+
let currentDepth = 0;
|
|
82
|
+
if (rawDepth !== undefined) {
|
|
83
|
+
currentDepth = Number(rawDepth);
|
|
84
|
+
if (!Number.isInteger(currentDepth) || currentDepth < 0) {
|
|
85
|
+
return {
|
|
86
|
+
allowed: false,
|
|
87
|
+
error: `Invalid inherited subagent depth ${JSON.stringify(rawDepth)}; refusing to spawn recursively.`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const nested = currentDepth > 0;
|
|
93
|
+
if (nested && requestedMaxDepth !== undefined) {
|
|
94
|
+
return {
|
|
95
|
+
allowed: false,
|
|
96
|
+
error:
|
|
97
|
+
`Nested subagents cannot override \`maxDepth\` (current depth ${currentDepth}). ` +
|
|
98
|
+
"Only the top-level parent may opt in when it creates the first subagent.",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let maxDepth = requestedMaxDepth ?? DEFAULT_SUBAGENT_MAX_DEPTH;
|
|
103
|
+
if (nested) {
|
|
104
|
+
const rawMaxDepth = env[SUBAGENT_MAX_DEPTH_ENV];
|
|
105
|
+
// Missing/corrupt inherited state fails closed at the current depth.
|
|
106
|
+
maxDepth = rawMaxDepth === undefined ? currentDepth : Number(rawMaxDepth);
|
|
107
|
+
if (!Number.isInteger(maxDepth) || maxDepth < currentDepth) {
|
|
108
|
+
return {
|
|
109
|
+
allowed: false,
|
|
110
|
+
error: `Invalid inherited max subagent depth ${JSON.stringify(rawMaxDepth)}; refusing to spawn recursively.`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const childDepth = currentDepth + 1;
|
|
116
|
+
if (childDepth > maxDepth) {
|
|
117
|
+
return {
|
|
118
|
+
allowed: false,
|
|
119
|
+
error:
|
|
120
|
+
`Nested subagent creation is disabled at depth ${currentDepth}: spawning would reach depth ${childDepth}, ` +
|
|
121
|
+
`but the inherited maxDepth is ${maxDepth}. Have the top-level parent explicitly opt in with ` +
|
|
122
|
+
`babysit_run { profile: "subagent", task, maxDepth: ${childDepth} } when creating the first worker.`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return { allowed: true, childDepth, maxDepth };
|
|
126
|
+
}
|
|
55
127
|
|
|
56
128
|
// Marker embedded in babysit_run's tool RESULT text for kind=process runs.
|
|
57
129
|
// It is how "the turn parked awaiting a process-exit notification" is told
|
|
@@ -116,7 +188,11 @@ function normalizeSession(s: BsSession): BsSession {
|
|
|
116
188
|
// long wait be interrupted (Ctrl-C) by killing the child.
|
|
117
189
|
function bs(
|
|
118
190
|
args: string[],
|
|
119
|
-
opts: {
|
|
191
|
+
opts: {
|
|
192
|
+
cwd?: string;
|
|
193
|
+
signal?: AbortSignal;
|
|
194
|
+
env?: Record<string, string | undefined>;
|
|
195
|
+
} = {},
|
|
120
196
|
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
121
197
|
return new Promise((resolve) => {
|
|
122
198
|
if (opts.signal?.aborted) {
|
|
@@ -125,7 +201,7 @@ function bs(
|
|
|
125
201
|
}
|
|
126
202
|
const child = spawn(BABYSIT_BIN, args, {
|
|
127
203
|
cwd: opts.cwd,
|
|
128
|
-
env: { ...process.env, BABYSIT_DIR: ROOT },
|
|
204
|
+
env: { ...process.env, ...opts.env, BABYSIT_DIR: ROOT },
|
|
129
205
|
});
|
|
130
206
|
let stdout = "";
|
|
131
207
|
let stderr = "";
|
|
@@ -295,6 +371,8 @@ interface Meta {
|
|
|
295
371
|
task?: string;
|
|
296
372
|
promptOffset?: number;
|
|
297
373
|
model?: string;
|
|
374
|
+
depth?: number;
|
|
375
|
+
maxDepth?: number;
|
|
298
376
|
}
|
|
299
377
|
|
|
300
378
|
const metaDir = () => path.join(ROOT, "meta");
|
|
@@ -1003,6 +1081,8 @@ interface SubagentOpts {
|
|
|
1003
1081
|
model?: string;
|
|
1004
1082
|
tools?: string[];
|
|
1005
1083
|
cwd: string;
|
|
1084
|
+
depth: number;
|
|
1085
|
+
maxDepth: number;
|
|
1006
1086
|
// Idle-timeout is OFF by default: an RPC-mode pi is silent while it works,
|
|
1007
1087
|
// so idle detection would false-kill a busy subagent. The absolute timeout
|
|
1008
1088
|
// is the safety valve instead.
|
|
@@ -1050,7 +1130,13 @@ async function spawnSubagent(
|
|
|
1050
1130
|
}
|
|
1051
1131
|
bsArgs.push("--", PI_BIN, ...piArgs);
|
|
1052
1132
|
|
|
1053
|
-
const r = await bs(bsArgs, {
|
|
1133
|
+
const r = await bs(bsArgs, {
|
|
1134
|
+
cwd: opts.cwd,
|
|
1135
|
+
env: {
|
|
1136
|
+
[SUBAGENT_DEPTH_ENV]: String(opts.depth),
|
|
1137
|
+
[SUBAGENT_MAX_DEPTH_ENV]: String(opts.maxDepth),
|
|
1138
|
+
},
|
|
1139
|
+
});
|
|
1054
1140
|
if (r.code !== 0) {
|
|
1055
1141
|
return { error: r.stderr || r.stdout || `babysit run failed (exit ${r.code}, no output) — check that \`${BABYSIT_BIN}\` works and ${ROOT} is writable` };
|
|
1056
1142
|
}
|
|
@@ -1065,7 +1151,13 @@ async function spawnSubagent(
|
|
|
1065
1151
|
// below fails and we kill the session, the exit poller does NOT mistake it
|
|
1066
1152
|
// for an un-notified process and fire a spurious process-end notification.
|
|
1067
1153
|
// The success path overwrites this with the full task meta.
|
|
1068
|
-
writeMeta(id, {
|
|
1154
|
+
writeMeta(id, {
|
|
1155
|
+
kind: "subagent",
|
|
1156
|
+
task: opts.task,
|
|
1157
|
+
notified: true,
|
|
1158
|
+
depth: opts.depth,
|
|
1159
|
+
maxDepth: opts.maxDepth,
|
|
1160
|
+
});
|
|
1069
1161
|
|
|
1070
1162
|
// Wait for pi to boot (first JSON event in the log), then inject the task.
|
|
1071
1163
|
await bs(["expect", "-s", id, "--timeout", "30s", '\\{"type"']);
|
|
@@ -1107,6 +1199,8 @@ async function spawnSubagent(
|
|
|
1107
1199
|
task: opts.task,
|
|
1108
1200
|
promptOffset: sent.offset,
|
|
1109
1201
|
model: resolvedModel,
|
|
1202
|
+
depth: opts.depth,
|
|
1203
|
+
maxDepth: opts.maxDepth,
|
|
1110
1204
|
startedAt: Date.now(),
|
|
1111
1205
|
});
|
|
1112
1206
|
return { id, model: resolvedModel };
|
|
@@ -1726,7 +1820,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1726
1820
|
"`retryOnWorkerDeath` can retry one idempotent command once. " +
|
|
1727
1821
|
"(2) `profile: \"subagent\"` + `task` — spawn a pi subagent that works on the task in the " +
|
|
1728
1822
|
"background; poll with babysit_check, steer with babysit_send, block with babysit_wait, " +
|
|
1729
|
-
"stop with babysit_kill."
|
|
1823
|
+
"stop with babysit_kill. Subagents cannot recursively spawn more subagents by default; " +
|
|
1824
|
+
"the top-level caller must explicitly raise `maxDepth` when creating the first worker.",
|
|
1730
1825
|
promptSnippet:
|
|
1731
1826
|
"Run any shell command with context-safe captured output; quick commands return metadata, longer ones continue in background",
|
|
1732
1827
|
promptGuidelines: [
|
|
@@ -1736,6 +1831,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1736
1831
|
"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.",
|
|
1737
1832
|
"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 }.",
|
|
1738
1833
|
"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.",
|
|
1834
|
+
"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.",
|
|
1739
1835
|
"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.",
|
|
1740
1836
|
],
|
|
1741
1837
|
parameters: Type.Object({
|
|
@@ -1764,6 +1860,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1764
1860
|
tools: Type.Optional(
|
|
1765
1861
|
Type.Array(Type.String(), { description: "Tool allowlist for the subagent." }),
|
|
1766
1862
|
),
|
|
1863
|
+
maxDepth: Type.Optional(
|
|
1864
|
+
Type.Integer({
|
|
1865
|
+
minimum: 1,
|
|
1866
|
+
description:
|
|
1867
|
+
"Maximum subagent nesting depth. Top-level subagent mode only; default 1 prevents workers from spawning workers. Nested workers inherit this limit and cannot override it.",
|
|
1868
|
+
}),
|
|
1869
|
+
),
|
|
1767
1870
|
agentScope: Type.Optional(
|
|
1768
1871
|
StringEnum(["user", "project", "both"] as const, {
|
|
1769
1872
|
description: "Where to discover named agents. Default 'user'.",
|
|
@@ -1826,6 +1929,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
1826
1929
|
};
|
|
1827
1930
|
}
|
|
1828
1931
|
|
|
1932
|
+
// Compute nesting only for subagent mode. Ordinary command processes remain
|
|
1933
|
+
// available even when the hosting agent is at its subagent depth limit.
|
|
1934
|
+
const nesting = isSubagent
|
|
1935
|
+
? planSubagentSpawn(params.maxDepth)
|
|
1936
|
+
: undefined;
|
|
1937
|
+
if (nesting && !nesting.allowed) {
|
|
1938
|
+
return {
|
|
1939
|
+
content: [{ type: "text", text: nesting.error }],
|
|
1940
|
+
isError: true,
|
|
1941
|
+
details: {},
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1829
1945
|
// --- process mode ---
|
|
1830
1946
|
if (!isSubagent) {
|
|
1831
1947
|
const spawnOpts: ProcOpts = {
|
|
@@ -1966,12 +2082,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
1966
2082
|
}
|
|
1967
2083
|
}
|
|
1968
2084
|
|
|
2085
|
+
// The branch above guarantees a successful plan in subagent mode.
|
|
2086
|
+
const subagentNesting = nesting as Extract<SubagentSpawnPlan, { allowed: true }>;
|
|
1969
2087
|
const res = await spawnSubagent({
|
|
1970
2088
|
agent,
|
|
1971
2089
|
task: params.task as string,
|
|
1972
2090
|
model: params.model,
|
|
1973
2091
|
tools: params.tools,
|
|
1974
2092
|
cwd: ctx.cwd,
|
|
2093
|
+
depth: subagentNesting.childDepth,
|
|
2094
|
+
maxDepth: subagentNesting.maxDepth,
|
|
1975
2095
|
timeout: params.timeout ?? "15m",
|
|
1976
2096
|
idleTimeout: params.idleTimeout,
|
|
1977
2097
|
});
|
|
@@ -1990,7 +2110,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1990
2110
|
{
|
|
1991
2111
|
type: "text",
|
|
1992
2112
|
text:
|
|
1993
|
-
`Subagent started (id: ${res.id})${agent ? ` [agent: ${agent.name}]` : ""}${res.model ? ` [model: ${res.model}]` : ""}.\n` +
|
|
2113
|
+
`Subagent started (id: ${res.id})${agent ? ` [agent: ${agent.name}]` : ""}${res.model ? ` [model: ${res.model}]` : ""} [depth: ${subagentNesting.childDepth}/${subagentNesting.maxDepth}].\n` +
|
|
1994
2114
|
`Task accepted — running in the background; keep working (do NOT end your turn just to wait for it).\n` +
|
|
1995
2115
|
`Poll: babysit_check { id: "${res.id}" }\n` +
|
|
1996
2116
|
`Wait: babysit_wait { id: "${res.id}" }\n` +
|
|
@@ -2003,6 +2123,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2003
2123
|
agent: agent?.name,
|
|
2004
2124
|
model: res.model,
|
|
2005
2125
|
task: params.task,
|
|
2126
|
+
depth: subagentNesting.childDepth,
|
|
2127
|
+
maxDepth: subagentNesting.maxDepth,
|
|
2006
2128
|
status: "started" satisfies DisplayStatus,
|
|
2007
2129
|
},
|
|
2008
2130
|
};
|
|
@@ -2094,9 +2216,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
2094
2216
|
const kind = meta?.kind ?? "process";
|
|
2095
2217
|
const flag = s.note ? ` ⚑ ${s.note}` : "";
|
|
2096
2218
|
const ec = s.exit_code != null ? ` exit=${s.exit_code}` : "";
|
|
2219
|
+
const depth =
|
|
2220
|
+
kind === "subagent" && meta?.depth != null
|
|
2221
|
+
? ` depth=${meta.depth}/${meta.maxDepth ?? "?"}`
|
|
2222
|
+
: "";
|
|
2097
2223
|
const what = (kind === "subagent" ? meta?.task : meta?.command) ?? "";
|
|
2098
2224
|
const preview = what.length > 60 ? `${what.slice(0, 57)}…` : what;
|
|
2099
|
-
return `${s.id} [${kind}] ${s.state}${ec}${flag}${preview ? ` — ${preview}` : ""}`;
|
|
2225
|
+
return `${s.id} [${kind}] ${s.state}${ec}${depth}${flag}${preview ? ` — ${preview}` : ""}`;
|
|
2100
2226
|
});
|
|
2101
2227
|
return { content: [{ type: "text", text: lines.join("\n") }], details: { sessions } };
|
|
2102
2228
|
}
|
|
@@ -2182,6 +2308,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2182
2308
|
|
|
2183
2309
|
const parts: string[] = [];
|
|
2184
2310
|
let header = `[subagent] state=${st.state}`;
|
|
2311
|
+
if (meta.depth != null) header += ` depth=${meta.depth}/${meta.maxDepth ?? "?"}`;
|
|
2185
2312
|
if (st.state === "running") {
|
|
2186
2313
|
const el = elapsedOf(params.id);
|
|
2187
2314
|
if (el) header += ` elapsed=${el}`;
|
|
@@ -2362,6 +2489,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2362
2489
|
task: params.text,
|
|
2363
2490
|
promptOffset: sent.offset,
|
|
2364
2491
|
model: meta?.model,
|
|
2492
|
+
depth: meta?.depth,
|
|
2493
|
+
maxDepth: meta?.maxDepth,
|
|
2365
2494
|
});
|
|
2366
2495
|
}
|
|
2367
2496
|
return {
|