@yusukeshib/pi-babysit 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/index.ts +226 -34
- package/package.json +1 -1
- package/self-reap.ts +6 -3
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ reachable from anywhere (`~/.pi-babysit/<pi-session-id>/`). Two kinds:
|
|
|
33
33
|
| kind | started by | completion | on completion |
|
|
34
34
|
| ---- | ---------- | ---------- | ------------- |
|
|
35
35
|
| **process** | `babysit_run { command }` | process **exit** | automatic notification message (`triggerTurn`), batched for all exits observed in the same poll — the agent may end its turn after starting and is resumed on exit, same contract as the old `process` tool |
|
|
36
|
-
| **subagent** | `babysit_run { profile: "subagent", task }` | `agent_settled` in the RPC event stream (worker remains reusable during its idle grace) |
|
|
36
|
+
| **subagent** | `babysit_run { profile: "subagent", task }` | `agent_settled` in the RPC event stream (worker remains reusable during its idle grace) | `foreground: true` returns the answer and nested usage in the same tool call; background workers must be collected with `babysit_wait` before the parent finishes, and remain reusable until self-reap |
|
|
37
37
|
|
|
38
38
|
The **profile is a tool parameter, not a separate tool set**: domain knowledge
|
|
39
39
|
(RPC bookkeeping, per-task byte offsets, parked-turn detection, PTY-safe
|
|
@@ -56,7 +56,7 @@ 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`). Set `foreground: true`
|
|
59
|
+
| `babysit_run` | Run any command (`command`, optional `name`/`pty`/`timeout`/`idleTimeout`/`retryOnWorkerDeath`/`notificationGroup`). Set `foreground: true` for one process or subagent whose result is needed in the same tool call; use `returnPattern`/`returnLines`/`maxBytes` to keep noisy process output bounded. Or start a named background subagent (`profile: "subagent"`, `task`, optional `name`/`agent`/`model`/`tools`/`maxDepth` and budget fields), then always collect it with `babysit_wait`. `maxDepth` defaults to 1. Quick commands return inline; longer process runs notify in the background |
|
|
60
60
|
| `babysit_check` | Without an id, list sessions with state/kind filters. With an id, inspect bounded output, search with `pattern`, or capture a TUI with `screen: true`; `maxBytes` overrides the 4 KB default up to 24 KB |
|
|
61
61
|
| `babysit_send` | Process: type `text` / press `keys` into the PTY. Subagent: steer mid-run, or send a follow-up task when confirmed settled (`mode: auto/steer/task`); explicit task mode rejects busy, parked, or unknown state |
|
|
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"` |
|
|
@@ -75,8 +75,10 @@ re-enables `bash`, including shell backgrounding (`… &`, `nohup`, `setsid`,
|
|
|
75
75
|
| `/babysit` | Arrow-key picker over all sessions. Renders an **inline snapshot** (no tmux): running **process** → current rendered screen + recent output + a copy-paste `babysit attach` take-over hint (detach `Ctrl-\ Ctrl-\`); running **subagent** → read-only progress (RPC stdin stays untouchable); finished → summary. Re-run `/babysit` to refresh |
|
|
76
76
|
| `/babysit gc [days]` | Preview and confirm deletion of old Pi-session roots (default 14 days). Active leases, live supervisor/child PIDs, unknown states, the current root, and recent roots are retained; deletion uses a GC lock and atomic rename |
|
|
77
77
|
|
|
78
|
-
A
|
|
79
|
-
|
|
78
|
+
A widget below the editor separates session **kind** from task **state** at a glance:
|
|
79
|
+
summary counts use `RUNNING` / `IDLE`, and every row is labeled
|
|
80
|
+
`[PROCESS] [RUNNING]`, `[AGENT] [RUNNING]`, or `[AGENT] [IDLE]`. The displayed
|
|
81
|
+
`age` is total session age, not time spent in the current state.
|
|
80
82
|
|
|
81
83
|
## Logs without context flooding
|
|
82
84
|
|
|
@@ -117,7 +119,12 @@ Set `PI_BABYSIT_RPC_LOG_MODE=standard` only when legacy lifecycle payloads are
|
|
|
117
119
|
needed for debugging. Live `/babysit` and attach views render either format.
|
|
118
120
|
|
|
119
121
|
All shell commands, including `pwd` and Git, go through `babysit_run`. Bundle
|
|
120
|
-
closely related tiny observations when doing so safely reduces tool turns.
|
|
122
|
+
closely related tiny observations when doing so safely reduces tool turns. For
|
|
123
|
+
parallel checks, do not issue sibling `foreground: true` calls: start them with
|
|
124
|
+
`continueAfterStart: true` and collect them in one multi-session `babysit_wait`.
|
|
125
|
+
During fix loops, prefer targeted checks after each edit and one full validation
|
|
126
|
+
suite at the end. Background subagents must likewise be collected before the
|
|
127
|
+
parent task finishes so their answer and nested usage are not lost.
|
|
121
128
|
Set `PI_BABYSIT_ALLOW_BASH=1` only as an explicit emergency escape hatch.
|
|
122
129
|
|
|
123
130
|
## Unexpected worker loss
|
|
@@ -143,7 +150,10 @@ because blindly rerunning an arbitrary command can duplicate side effects.
|
|
|
143
150
|
their exits span multiple polls.
|
|
144
151
|
`babysit_kill` and an exit already reported by `babysit_wait` suppress the
|
|
145
152
|
notification.
|
|
146
|
-
- **Subagent**: `babysit_wait` blocks on
|
|
153
|
+
- **Subagent**: `foreground: true` or `babysit_wait` blocks on
|
|
154
|
+
`babysit expect '"type":"agent_settled"'`. A background task that settles or
|
|
155
|
+
exits before it is collected emits one ready-to-collect reminder; the parent
|
|
156
|
+
must still call `babysit_wait` so the answer and nested usage are persisted.
|
|
147
157
|
Unlike `agent_end`, `agent_settled` cannot precede an automatic retry,
|
|
148
158
|
compaction retry, or queued continuation. A settled run containing a
|
|
149
159
|
**parked** toolResult — a `babysit_run { command }` result carrying the
|
package/index.ts
CHANGED
|
@@ -418,6 +418,13 @@ interface Meta {
|
|
|
418
418
|
budgetKilled?: boolean;
|
|
419
419
|
/** Prompt offset whose nested usage has already been charged to the parent session. */
|
|
420
420
|
usageReportedOffset?: number;
|
|
421
|
+
/** Prompt offset explicitly collected by foreground mode or babysit_wait. */
|
|
422
|
+
subagentCollectedOffset?: number;
|
|
423
|
+
/** Prompt offset whose ready-to-collect reminder was sent to the parent. */
|
|
424
|
+
subagentNotifiedOffset?: number;
|
|
425
|
+
/** Current task completion first observed by the reminder poller. */
|
|
426
|
+
subagentCompletionObservedOffset?: number;
|
|
427
|
+
subagentCompletionObservedAt?: number;
|
|
421
428
|
}
|
|
422
429
|
|
|
423
430
|
const metaDir = () => path.join(ROOT, "meta");
|
|
@@ -731,6 +738,16 @@ export function shouldDeliverProcessCompletion(
|
|
|
731
738
|
return meta?.kind === "process" && !meta.notified && !meta.notificationPaused;
|
|
732
739
|
}
|
|
733
740
|
|
|
741
|
+
export function shouldDeliverSubagentCompletion(meta: Meta | null): meta is Meta & { kind: "subagent" } {
|
|
742
|
+
if (meta?.kind !== "subagent") return false;
|
|
743
|
+
const offset = meta.promptOffset ?? 0;
|
|
744
|
+
return (
|
|
745
|
+
meta.usageReportedOffset !== offset &&
|
|
746
|
+
meta.subagentCollectedOffset !== offset &&
|
|
747
|
+
meta.subagentNotifiedOffset !== offset
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
734
751
|
export function isNotificationGroupReady(
|
|
735
752
|
meta: Meta,
|
|
736
753
|
sessions: BsSession[],
|
|
@@ -752,10 +769,14 @@ export function shouldKeepPolling(
|
|
|
752
769
|
sessions: Array<{ id: string; state: string }>,
|
|
753
770
|
metaFor: (id: string) => Meta | null,
|
|
754
771
|
): boolean {
|
|
755
|
-
return sessions.some(
|
|
756
|
-
(session)
|
|
757
|
-
|
|
758
|
-
|
|
772
|
+
return sessions.some((session) => {
|
|
773
|
+
const meta = metaFor(session.id);
|
|
774
|
+
return (
|
|
775
|
+
session.state === "running" ||
|
|
776
|
+
shouldDeliverProcessCompletion(meta) ||
|
|
777
|
+
(session.state !== "running" && shouldDeliverSubagentCompletion(meta))
|
|
778
|
+
);
|
|
779
|
+
});
|
|
759
780
|
}
|
|
760
781
|
|
|
761
782
|
export function shouldKeepPollingAfterList(
|
|
@@ -2167,13 +2188,56 @@ async function spawnSubagent(
|
|
|
2167
2188
|
// A subagent whose task is done stays alive as an idle RPC worker (for
|
|
2168
2189
|
// follow-ups), so "running" in babysit does NOT mean "working" — count
|
|
2169
2190
|
// processes / busy subagents / idle subagents separately.
|
|
2170
|
-
|
|
2191
|
+
export type WidgetSessionKind = "process" | "agent";
|
|
2192
|
+
export type WidgetSessionState = "running" | "idle";
|
|
2193
|
+
|
|
2194
|
+
export function widgetSummaryText(procs: number, busy: number, idle: number): string {
|
|
2195
|
+
const sections: string[] = [];
|
|
2196
|
+
const running: string[] = [];
|
|
2197
|
+
if (procs > 0) running.push(`${procs} process${procs > 1 ? "es" : ""}`);
|
|
2198
|
+
if (busy > 0) running.push(`${busy} agent${busy > 1 ? "s" : ""}`);
|
|
2199
|
+
if (running.length > 0) sections.push(`RUNNING ${running.join(" · ")}`);
|
|
2200
|
+
if (idle > 0) sections.push(`IDLE ${idle} agent${idle > 1 ? "s" : ""}`);
|
|
2201
|
+
return sections.join(" │ ");
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
export function widgetSessionHeader(
|
|
2205
|
+
id: string,
|
|
2206
|
+
kind: WidgetSessionKind,
|
|
2207
|
+
state: WidgetSessionState,
|
|
2208
|
+
elapsed?: string,
|
|
2209
|
+
): string {
|
|
2210
|
+
const icon = state === "running" ? "▶" : "○";
|
|
2211
|
+
return ` ${icon} ${id} [${kind.toUpperCase()}] [${state.toUpperCase()}]${elapsed ? ` · age ${elapsed}` : ""}`;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
function renderWidgetLines(procs: number, busy: number, idle: number, theme: Theme): string[] {
|
|
2171
2215
|
if (procs === 0 && busy === 0 && idle === 0) return [];
|
|
2172
|
-
const
|
|
2173
|
-
|
|
2174
|
-
if (
|
|
2175
|
-
if (
|
|
2176
|
-
|
|
2216
|
+
const sections: string[] = [];
|
|
2217
|
+
const running: string[] = [];
|
|
2218
|
+
if (procs > 0) running.push(`${procs} process${procs > 1 ? "es" : ""}`);
|
|
2219
|
+
if (busy > 0) running.push(`${busy} agent${busy > 1 ? "s" : ""}`);
|
|
2220
|
+
if (running.length > 0) {
|
|
2221
|
+
sections.push(`${theme.fg("success", theme.bold("RUNNING"))} ${running.join(" · ")}`);
|
|
2222
|
+
}
|
|
2223
|
+
if (idle > 0) {
|
|
2224
|
+
sections.push(`${theme.fg("muted", theme.bold("IDLE"))} ${idle} agent${idle > 1 ? "s" : ""}`);
|
|
2225
|
+
}
|
|
2226
|
+
return [theme.bg("toolPendingBg", ` ${sections.join(" │ ")} `)];
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
function renderWidgetSessionHeader(
|
|
2230
|
+
id: string,
|
|
2231
|
+
kind: WidgetSessionKind,
|
|
2232
|
+
state: WidgetSessionState,
|
|
2233
|
+
elapsed: string,
|
|
2234
|
+
theme: Theme,
|
|
2235
|
+
): string {
|
|
2236
|
+
const running = state === "running";
|
|
2237
|
+
const icon = theme.fg(running ? "success" : "muted", running ? "▶" : "○");
|
|
2238
|
+
const kindLabel = theme.fg(kind === "process" ? "accent" : "warning", theme.bold(`[${kind.toUpperCase()}]`));
|
|
2239
|
+
const stateLabel = theme.fg(running ? "success" : "muted", theme.bold(`[${state.toUpperCase()}]`));
|
|
2240
|
+
return ` ${icon} ${id} ${kindLabel} ${stateLabel}${elapsed ? theme.fg("dim", ` · age ${elapsed}`) : ""}`;
|
|
2177
2241
|
}
|
|
2178
2242
|
|
|
2179
2243
|
// How many trailing output lines to show per running session in the widget.
|
|
@@ -2457,16 +2521,27 @@ async function waitForTask(
|
|
|
2457
2521
|
}
|
|
2458
2522
|
}
|
|
2459
2523
|
|
|
2460
|
-
// Mark a
|
|
2461
|
-
//
|
|
2524
|
+
// Mark a session as already reported so completion pollers do not send a
|
|
2525
|
+
// duplicate message for something the agent just observed.
|
|
2462
2526
|
function suppressNotify(id: string, reason: "observed" | "kill" = "observed"): void {
|
|
2463
2527
|
const meta = readMeta(id);
|
|
2464
|
-
if (meta
|
|
2528
|
+
if (!meta) return;
|
|
2529
|
+
if (meta.kind === "process") {
|
|
2465
2530
|
meta.notified = true;
|
|
2466
2531
|
if (reason === "kill") meta.killNotificationSuppressed = true;
|
|
2467
2532
|
delete meta.notificationPaused;
|
|
2468
|
-
|
|
2533
|
+
} else {
|
|
2534
|
+
meta.subagentCollectedOffset = meta.promptOffset ?? 0;
|
|
2469
2535
|
}
|
|
2536
|
+
writeMeta(id, meta);
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
function collectSubagentOutcome(outcome: WaitOutcome): void {
|
|
2540
|
+
if (outcome.kind !== "done" && outcome.kind !== "exited") return;
|
|
2541
|
+
const meta = readMeta(outcome.id);
|
|
2542
|
+
if (!meta || meta.kind !== "subagent") return;
|
|
2543
|
+
meta.subagentCollectedOffset = meta.promptOffset ?? 0;
|
|
2544
|
+
writeMeta(outcome.id, meta);
|
|
2470
2545
|
}
|
|
2471
2546
|
|
|
2472
2547
|
export interface WaitReservationState {
|
|
@@ -2574,10 +2649,23 @@ async function waitForExit(
|
|
|
2574
2649
|
// one concurrent wait timing out cannot re-enable notifications underneath
|
|
2575
2650
|
// another wait that is still pending.
|
|
2576
2651
|
updateWaitReservation(id, "reserve");
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2652
|
+
let w: Awaited<ReturnType<typeof bs>>;
|
|
2653
|
+
let attempt = 0;
|
|
2654
|
+
for (;;) {
|
|
2655
|
+
w = await bs(["wait", "-s", id, "--timeout", t], { signal });
|
|
2656
|
+
if (signal?.aborted) {
|
|
2657
|
+
updateWaitReservation(id, "abandon");
|
|
2658
|
+
return { id, kind: "interrupted", ok: false, text: `wait for ${id} was interrupted.` };
|
|
2659
|
+
}
|
|
2660
|
+
if (w.code === 0 || w.code === 124 || w.code === 130) break;
|
|
2661
|
+
|
|
2662
|
+
// A freshly spawned session can be visible in `list` before the backend's
|
|
2663
|
+
// wait endpoint is ready, especially when sibling foreground tools start
|
|
2664
|
+
// concurrently. Retry that transient startup race instead of reporting the
|
|
2665
|
+
// still-running child as "exited with code ?".
|
|
2666
|
+
const retryStatus = await statusOf(id);
|
|
2667
|
+
if (retryStatus?.state !== "running" || attempt++ >= 3) break;
|
|
2668
|
+
await new Promise((resolve) => setTimeout(resolve, 50 * attempt));
|
|
2581
2669
|
}
|
|
2582
2670
|
if (w.code === 130) {
|
|
2583
2671
|
const interruptedStatus = await statusOf(id);
|
|
@@ -2619,6 +2707,18 @@ async function waitForExit(
|
|
|
2619
2707
|
if (!expectPattern) updateWaitReservation(id, "claim");
|
|
2620
2708
|
return { id, kind: "exited", ok: false, text: `No such session: ${id}` };
|
|
2621
2709
|
}
|
|
2710
|
+
if (st.state === "running") {
|
|
2711
|
+
if (!expectPattern) updateWaitReservation(id, "abandon");
|
|
2712
|
+
return {
|
|
2713
|
+
id,
|
|
2714
|
+
kind: "interrupted",
|
|
2715
|
+
ok: false,
|
|
2716
|
+
text:
|
|
2717
|
+
`The wait backend returned before process ${id} exited; the process is still running. ` +
|
|
2718
|
+
`Use babysit_wait to continue waiting.\nLog: ${logPath(id)}`,
|
|
2719
|
+
status: st,
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2622
2722
|
if (expectPattern) suppressNotify(id);
|
|
2623
2723
|
else updateWaitReservation(id, "claim"); // the agent sees the exit here; don't notify again
|
|
2624
2724
|
const meta = readMeta(id);
|
|
@@ -2964,6 +3064,64 @@ export default function (pi: ExtensionAPI) {
|
|
|
2964
3064
|
);
|
|
2965
3065
|
}
|
|
2966
3066
|
|
|
3067
|
+
async function notifySettledSubagents(
|
|
3068
|
+
ctx: ExtensionContext,
|
|
3069
|
+
snapshot?: BsSession[],
|
|
3070
|
+
): Promise<void> {
|
|
3071
|
+
if (shouldDeferCompletionNotification(ctx.isIdle())) return;
|
|
3072
|
+
const sessions = snapshot ?? (await listSessions()).sessions;
|
|
3073
|
+
const ready: Array<{ id: string; offset: number; state: string; summary: string }> = [];
|
|
3074
|
+
for (const session of sessions) {
|
|
3075
|
+
const meta = readMeta(session.id);
|
|
3076
|
+
if (!shouldDeliverSubagentCompletion(meta)) continue;
|
|
3077
|
+
let progress: Progress;
|
|
3078
|
+
try {
|
|
3079
|
+
progress = taskProgressOf(session.id).progress;
|
|
3080
|
+
} catch {
|
|
3081
|
+
progress = emptyProgress();
|
|
3082
|
+
}
|
|
3083
|
+
if (session.state === "running" && !progress.done) continue;
|
|
3084
|
+
|
|
3085
|
+
const offset = meta.promptOffset ?? 0;
|
|
3086
|
+
if (meta.subagentCompletionObservedOffset !== offset) {
|
|
3087
|
+
meta.subagentCompletionObservedOffset = offset;
|
|
3088
|
+
meta.subagentCompletionObservedAt = Date.now();
|
|
3089
|
+
writeMeta(session.id, meta);
|
|
3090
|
+
continue;
|
|
3091
|
+
}
|
|
3092
|
+
if (Date.now() - (meta.subagentCompletionObservedAt ?? 0) < POLL_MS) continue;
|
|
3093
|
+
const summary = progress.done
|
|
3094
|
+
? `task settled; ${progress.turns} turns, ${progress.toolCallCount} tools, $${progress.cost.toFixed(4)}`
|
|
3095
|
+
: `worker ${session.state} with exit code ${session.exit_code ?? "?"}; partial usage $${progress.cost.toFixed(4)}`;
|
|
3096
|
+
ready.push({ id: session.id, offset, state: session.state, summary });
|
|
3097
|
+
}
|
|
3098
|
+
if (ready.length === 0 || shouldDeferCompletionNotification(ctx.isIdle())) return;
|
|
3099
|
+
|
|
3100
|
+
const deliverable = ready.filter(({ id, offset }) => {
|
|
3101
|
+
const meta = readMeta(id);
|
|
3102
|
+
return shouldDeliverSubagentCompletion(meta) && (meta.promptOffset ?? 0) === offset;
|
|
3103
|
+
});
|
|
3104
|
+
if (deliverable.length === 0) return;
|
|
3105
|
+
pi.sendMessage(
|
|
3106
|
+
{
|
|
3107
|
+
customType: "pi-babysit-subagent-ready",
|
|
3108
|
+
content:
|
|
3109
|
+
`${deliverable.length === 1 ? "A background subagent is" : `${deliverable.length} background subagents are`} ready to collect:\n` +
|
|
3110
|
+
deliverable.map(({ id, summary }) => `- ${id}: ${summary}`).join("\n") +
|
|
3111
|
+
"\nCall babysit_wait now to retrieve the answer and charge nested usage before finishing the parent task.",
|
|
3112
|
+
display: true,
|
|
3113
|
+
details: { subagents: deliverable.map(({ id, state }) => ({ id, state })) },
|
|
3114
|
+
},
|
|
3115
|
+
{ triggerTurn: true, deliverAs: "steer" },
|
|
3116
|
+
);
|
|
3117
|
+
for (const { id, offset } of deliverable) {
|
|
3118
|
+
const meta = readMeta(id);
|
|
3119
|
+
if (!meta || meta.kind !== "subagent" || (meta.promptOffset ?? 0) !== offset) continue;
|
|
3120
|
+
meta.subagentNotifiedOffset = offset;
|
|
3121
|
+
writeMeta(id, meta);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
|
|
2967
3125
|
const refreshWidget = async (ctx: ExtensionContext, snapshot?: BsSession[]) => {
|
|
2968
3126
|
if (!ctx.hasUI) return;
|
|
2969
3127
|
const active = (snapshot ?? (await listSessions()).sessions).filter(
|
|
@@ -2980,7 +3138,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2980
3138
|
}
|
|
2981
3139
|
}
|
|
2982
3140
|
const idle = subs.filter((session) => progressById.get(session.id)?.done).length;
|
|
2983
|
-
const
|
|
3141
|
+
const theme = ctx.ui.theme;
|
|
3142
|
+
const lines = renderWidgetLines(procs, subs.length - idle, idle, theme);
|
|
2984
3143
|
const tails = await Promise.all(
|
|
2985
3144
|
active.map((session) => {
|
|
2986
3145
|
const isSubagent = kindOf(session.id) === "subagent";
|
|
@@ -2993,11 +3152,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
2993
3152
|
);
|
|
2994
3153
|
active.forEach((session, index) => {
|
|
2995
3154
|
const isSubagent = kindOf(session.id) === "subagent";
|
|
2996
|
-
const
|
|
2997
|
-
?
|
|
2998
|
-
: "
|
|
3155
|
+
const state: WidgetSessionState = isSubagent && progressById.get(session.id)?.done
|
|
3156
|
+
? "idle"
|
|
3157
|
+
: "running";
|
|
2999
3158
|
const elapsed = elapsedOf(session.id);
|
|
3000
|
-
const header =
|
|
3159
|
+
const header = renderWidgetSessionHeader(
|
|
3160
|
+
session.id,
|
|
3161
|
+
isSubagent ? "agent" : "process",
|
|
3162
|
+
state,
|
|
3163
|
+
elapsed,
|
|
3164
|
+
theme,
|
|
3165
|
+
);
|
|
3001
3166
|
if (tails[index].length === 1) {
|
|
3002
3167
|
lines.push(`${header} │ ${tails[index][0]}`);
|
|
3003
3168
|
} else {
|
|
@@ -3157,6 +3322,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3157
3322
|
await enforceSubagentBudgets(snapshot);
|
|
3158
3323
|
await Promise.all([
|
|
3159
3324
|
notifyEndedProcesses(ctx, snapshot),
|
|
3325
|
+
notifySettledSubagents(ctx, snapshot),
|
|
3160
3326
|
refreshWidget(ctx, snapshot),
|
|
3161
3327
|
]);
|
|
3162
3328
|
pruneTerminalSessionCache(taskProgressCache, snapshot);
|
|
@@ -3228,12 +3394,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
3228
3394
|
"`returnPattern`/`returnLines` bound foreground output. Sessions support check, wait, send, and kill.",
|
|
3229
3395
|
promptSnippet: "Run supervised commands or bounded pi subagents with context-safe logs",
|
|
3230
3396
|
promptGuidelines: [
|
|
3231
|
-
"Use babysit_run for shell commands and give meaningful sessions a stable name; bundle tiny related observations.",
|
|
3232
|
-
"Use babysit_run foreground mode
|
|
3397
|
+
"Use babysit_run for shell commands and give meaningful sessions a stable name; bundle tiny related read-only observations into one command.",
|
|
3398
|
+
"Use babysit_run foreground mode for one process or subagent whose result is needed now; never issue sibling foreground runs in parallel. For parallel checks, start background runs with continueAfterStart and collect them with one multi-session babysit_wait.",
|
|
3399
|
+
"Use returnPattern/returnLines for noisy commands. During edit/fix loops run targeted checks first and one full validation suite at the end instead of repeating every full gate.",
|
|
3233
3400
|
"After a background process starts, stop the turn for its automatic notification; never poll or sleep. Use continueAfterStart only for specific non-polling work.",
|
|
3234
3401
|
"Inspect large logs with a narrow babysit_check pattern and maxBytes rather than broad tails.",
|
|
3235
3402
|
"Use retryOnWorkerDeath only once and only for idempotent commands; retries may duplicate side effects.",
|
|
3236
|
-
"Delegate independent work with bounded babysit_run subagents
|
|
3403
|
+
"Delegate independent work with bounded babysit_run subagents. Prefer foreground for one result needed now; every background subagent must be collected with babysit_wait before the parent task finishes. Size budgets above the worker's initial context and expected tool count.",
|
|
3237
3404
|
"Subagent recursion defaults to depth 1; only a top-level caller may explicitly raise maxDepth.",
|
|
3238
3405
|
],
|
|
3239
3406
|
parameters: Type.Object({
|
|
@@ -3321,7 +3488,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3321
3488
|
),
|
|
3322
3489
|
foreground: Type.Optional(
|
|
3323
3490
|
Type.Boolean({
|
|
3324
|
-
description: "Process: wait for
|
|
3491
|
+
description: "Process or subagent: wait for completion and return the result in this tool call.",
|
|
3325
3492
|
}),
|
|
3326
3493
|
),
|
|
3327
3494
|
returnPattern: Type.Optional(
|
|
@@ -3396,16 +3563,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
3396
3563
|
details: {},
|
|
3397
3564
|
};
|
|
3398
3565
|
}
|
|
3399
|
-
if (isSubagent && params.
|
|
3566
|
+
if (isSubagent && (params.returnPattern || params.returnLines != null || params.maxBytes != null)) {
|
|
3400
3567
|
return {
|
|
3401
|
-
content: [{ type: "text", text: "`
|
|
3568
|
+
content: [{ type: "text", text: "`returnPattern`, `returnLines`, and `maxBytes` are process-output options." }],
|
|
3402
3569
|
isError: true,
|
|
3403
3570
|
details: {},
|
|
3404
3571
|
};
|
|
3405
3572
|
}
|
|
3406
|
-
if (isSubagent &&
|
|
3573
|
+
if (isSubagent && params.continueAfterStart != null) {
|
|
3407
3574
|
return {
|
|
3408
|
-
content: [{ type: "text", text: "`
|
|
3575
|
+
content: [{ type: "text", text: "`continueAfterStart` is available only in process mode." }],
|
|
3409
3576
|
isError: true,
|
|
3410
3577
|
details: {},
|
|
3411
3578
|
};
|
|
@@ -3625,15 +3792,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
3625
3792
|
|
|
3626
3793
|
pollNeeded = true;
|
|
3627
3794
|
await refreshWidget(ctx);
|
|
3795
|
+
if (params.foreground || !ctx.hasUI) {
|
|
3796
|
+
const outcome = await waitForTask(res.id, null, _signal);
|
|
3797
|
+
collectSubagentOutcome(outcome);
|
|
3798
|
+
const usage = claimOutcomeUsage(outcome);
|
|
3799
|
+
if (ctx.hasUI) await refreshWidget(ctx);
|
|
3800
|
+
return {
|
|
3801
|
+
content: [{ type: "text", text: outcome.text }],
|
|
3802
|
+
isError: !outcome.ok,
|
|
3803
|
+
usage,
|
|
3804
|
+
details: {
|
|
3805
|
+
id: res.id,
|
|
3806
|
+
kind: "subagent",
|
|
3807
|
+
name: params.name ?? res.id,
|
|
3808
|
+
agent: agent?.name,
|
|
3809
|
+
model: res.model,
|
|
3810
|
+
task: params.task,
|
|
3811
|
+
depth: subagentNesting.childDepth,
|
|
3812
|
+
maxDepth: subagentNesting.maxDepth,
|
|
3813
|
+
status: outcomeStatus(outcome),
|
|
3814
|
+
},
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3628
3817
|
return {
|
|
3629
3818
|
content: [
|
|
3630
3819
|
{
|
|
3631
3820
|
type: "text",
|
|
3632
3821
|
text:
|
|
3633
3822
|
`Subagent started (id: ${res.id})${agent ? ` [agent: ${agent.name}]` : ""}${res.model ? ` [model: ${res.model}]` : ""} [depth: ${subagentNesting.childDepth}/${subagentNesting.maxDepth}].\n` +
|
|
3634
|
-
`Task accepted — running in the background;
|
|
3635
|
-
`
|
|
3636
|
-
`
|
|
3823
|
+
`Task accepted — running in the background. You MUST collect it with babysit_wait before finishing the parent task; use foreground: true next time when no independent work is available.\n` +
|
|
3824
|
+
`Progress: babysit_check { id: "${res.id}" } (only when inspection is needed)\n` +
|
|
3825
|
+
`Collect: babysit_wait { id: "${res.id}" }\n` +
|
|
3637
3826
|
`Human can watch/steer: /babysit (pick ${res.id})`,
|
|
3638
3827
|
},
|
|
3639
3828
|
],
|
|
@@ -4257,6 +4446,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4257
4446
|
|
|
4258
4447
|
if (ids.length === 1) {
|
|
4259
4448
|
const r = await waitFor(ids[0], limitMs, signal, params.expect);
|
|
4449
|
+
collectSubagentOutcome(r);
|
|
4260
4450
|
const usage = claimOutcomeUsage(r);
|
|
4261
4451
|
return {
|
|
4262
4452
|
content: [{ type: "text", text: r.text }],
|
|
@@ -4277,6 +4467,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4277
4467
|
ids.map((i) => waitFor(i, limitMs, signal, params.expect)),
|
|
4278
4468
|
);
|
|
4279
4469
|
const ok = results.every((r) => r.ok);
|
|
4470
|
+
results.forEach(collectSubagentOutcome);
|
|
4280
4471
|
const usage = sumNestedUsage(results.map(claimOutcomeUsage));
|
|
4281
4472
|
return {
|
|
4282
4473
|
content: [
|
|
@@ -4306,6 +4497,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4306
4497
|
ids.map((i) => waitFor(i, limitMs, ctrl.signal, params.expect)),
|
|
4307
4498
|
);
|
|
4308
4499
|
const others = ids.filter((i) => i !== first.id);
|
|
4500
|
+
collectSubagentOutcome(first);
|
|
4309
4501
|
const usage = claimOutcomeUsage(first);
|
|
4310
4502
|
return {
|
|
4311
4503
|
content: [
|
package/package.json
CHANGED
package/self-reap.ts
CHANGED
|
@@ -100,12 +100,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
100
100
|
|
|
101
101
|
// agent_end can precede automatic retries, compaction retries, and queued
|
|
102
102
|
// continuations. Start the grace timer only once Pi is truly settled.
|
|
103
|
-
pi.on("agent_settled", (
|
|
103
|
+
pi.on("agent_settled", () => {
|
|
104
104
|
cancel();
|
|
105
105
|
if (lastEndWasParked) return;
|
|
106
106
|
timer = setTimeout(() => {
|
|
107
|
-
//
|
|
108
|
-
|
|
107
|
+
// In RPC mode ctx.shutdown() only sets a flag that is checked on the next
|
|
108
|
+
// command or agent_settled event. An already-idle worker receives neither,
|
|
109
|
+
// so it would remain alive indefinitely. At this point the grace elapsed,
|
|
110
|
+
// the task is settled, and parked turns were excluded above; exit cleanly.
|
|
111
|
+
process.exit(0);
|
|
109
112
|
}, graceMs);
|
|
110
113
|
timer.unref?.();
|
|
111
114
|
});
|