@arhen/pi-core-subagent 1.3.49 → 1.3.50
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 +5 -6
- package/package.json +1 -1
- package/src/agentfile.ts +4 -41
- package/src/child.ts +2 -17
- package/src/format.ts +11 -58
- package/src/graph.ts +7 -36
- package/src/index.ts +16 -56
- package/src/mailbox.ts +0 -7
- package/src/manager.ts +93 -348
- package/src/peek.ts +13 -69
- package/src/schemas.ts +1 -4
- package/src/types.ts +0 -19
- package/src/worktree.ts +37 -164
package/src/index.ts
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-core-subagent — in-process subagents.
|
|
3
|
-
*
|
|
4
|
-
* Fast in-process subagents (isolated AgentSessions, no process spawn).
|
|
5
|
-
* Modes: single / parallel / chain. Background runs, cancel, intercom
|
|
6
|
-
* (ask/notify/update the leader) and agent↔agent mailbox (send/poll).
|
|
7
|
-
*
|
|
8
|
-
* Context discipline: 7 slim parent tools, one-line catalog injected per
|
|
9
|
-
* request (cached), background completions notify with a 3-line summary
|
|
10
|
-
* instead of full outputs, and run updates are throttled (no per-event
|
|
11
|
-
* deep clones).
|
|
12
|
-
*
|
|
13
|
-
* Layout: schemas → schemas.ts, scheduler/graph → graph.ts, rendering →
|
|
14
|
-
* format.ts, run lifecycle → manager.ts, this file = entry + registrations.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
2
|
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
19
3
|
import { compactLines, formatUsage, makeSummary, statusIcon, taskLine, truncateText } from "./format.ts";
|
|
@@ -35,7 +19,6 @@ import { cleanupMerged, ownerAlive, reapDeadWorktrees, repoRoot, sweepStale } fr
|
|
|
35
19
|
export default function (pi: ExtensionAPI) {
|
|
36
20
|
const manager = new SubagentManager(pi);
|
|
37
21
|
|
|
38
|
-
/** Read-only peek: browse agents, enter to tail one. Never mutates run state. */
|
|
39
22
|
const openPeek = async (ctx: ExtensionContext) => {
|
|
40
23
|
if (!ctx.hasUI) return;
|
|
41
24
|
const getTasks = (): PeekTask[] =>
|
|
@@ -70,7 +53,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
70
53
|
);
|
|
71
54
|
};
|
|
72
55
|
pi.registerCommand("subagents", {
|
|
73
|
-
description:
|
|
56
|
+
description:
|
|
57
|
+
"List subagent runs. `/subagents peek` opens the browsable pane; `/subagents auto-limit on|off` toggles the 1 h default runtime ceiling (default off = 6 h).",
|
|
74
58
|
handler: async (args, ctx) => {
|
|
75
59
|
const arg = String(args ?? "")
|
|
76
60
|
.trim()
|
|
@@ -100,7 +84,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
100
84
|
ctx.ui.notify(runs.flatMap((run) => compactLines(run).concat("")).join("\n"), "info");
|
|
101
85
|
},
|
|
102
86
|
});
|
|
103
|
-
|
|
87
|
+
|
|
104
88
|
pi.registerShortcut("ctrl+shift+a", { description: "Peek at running subagents", handler: openPeek });
|
|
105
89
|
|
|
106
90
|
pi.on("agent_start", (_event, ctx) => {
|
|
@@ -110,9 +94,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
110
94
|
|
|
111
95
|
pi.on("session_start", async (_event, ctx) => {
|
|
112
96
|
await manager.restoreFromSidecar(ctx);
|
|
113
|
-
|
|
114
|
-
// Also reap branches the leader merged in a previous session (H1: cleanup can't
|
|
115
|
-
// fire at run end — the leader merges after).
|
|
97
|
+
|
|
116
98
|
const roots = new Set<string>();
|
|
117
99
|
const cwdRoot = repoRoot(ctx.cwd);
|
|
118
100
|
if (cwdRoot) roots.add(cwdRoot);
|
|
@@ -126,27 +108,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
126
108
|
}
|
|
127
109
|
for (const root of roots) {
|
|
128
110
|
try {
|
|
129
|
-
// A registered subagent worktree is a crash leftover UNLESS another pi
|
|
130
|
-
// session still owns it (pid marker) — commit its work, keep the branch,
|
|
131
|
-
// drop the dir. Then reap merged branches and dirs git no longer tracks.
|
|
132
|
-
// Pass real ownership: in a long-lived process (pi `/new`) the previous
|
|
133
|
-
// session's markers carry THIS pid, and trusting them made those dirs
|
|
134
|
-
// unreapable until the process exited.
|
|
135
111
|
reapDeadWorktrees(root, (p) => ownerAlive(p, manager.ownsWorktree));
|
|
136
112
|
cleanupMerged(root, { skipBranches: manager.liveBranches() });
|
|
137
113
|
sweepStale(root);
|
|
138
|
-
} catch {
|
|
139
|
-
/* recovery is best-effort — never block session start */
|
|
140
|
-
}
|
|
114
|
+
} catch {}
|
|
141
115
|
}
|
|
142
116
|
});
|
|
143
117
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
144
118
|
if (ctx?.hasUI) {
|
|
145
119
|
try {
|
|
146
120
|
ctx.ui.setWidget("subagents", [], { placement: "aboveEditor" });
|
|
147
|
-
} catch {
|
|
148
|
-
/* ignore */
|
|
149
|
-
}
|
|
121
|
+
} catch {}
|
|
150
122
|
}
|
|
151
123
|
manager.clearRuns();
|
|
152
124
|
});
|
|
@@ -154,8 +126,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
154
126
|
pi.registerTool<typeof SubagentParams, RunDetails>({
|
|
155
127
|
name: "subagent",
|
|
156
128
|
label: "Subagent",
|
|
157
|
-
|
|
158
|
-
// biases the model toward one shape; guidelines + JSON schema describe all of them.
|
|
129
|
+
|
|
159
130
|
description:
|
|
160
131
|
"Run isolated subagents (own context, own session). You invent each agent: name, optional system prompt, toolset (read-only default, write:true to edit). Use `agent`+`task` for one, `tasks` for many. `needs` declares dependency edges: a task waits for its needs and receives their outputs prepended to its prompt. If a user agent file in `.agents/agents`, `.claude/agents`, or `.pi/agents` (project dirs, then home) has a `description` matching the spawn goal (name + task), that file is authoritative: body = system prompt, frontmatter `model`/`tools` apply and inline prompt/model are ignored — except explicit per-call `tools`/`write`, which override the file's tools. No match → the inline definition stands. Write agents run in an isolated git worktree: on completion the result reports the branch + changed files — review, then merge with `git merge --no-ff <branch>` (merged branches are cleaned automatically). Every run is background: the call returns a runId immediately and completion notifies you — do NOT park waiting on it. If you have no other work, end your turn; the completion notice wakes you with the results. Set autoAwait:true only when the very next step in the SAME turn consumes the result. Children always carry talk tools: they can ask you questions, notify you, and message siblings.",
|
|
161
132
|
promptSnippet: "Define and delegate work to specialized subagents.",
|
|
@@ -174,28 +145,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
174
145
|
"await_subagent is for the rare case where you have parallel work of your own and need to sync at a specific point — not the default follow-up to a spawn.",
|
|
175
146
|
],
|
|
176
147
|
parameters: SubagentParams,
|
|
177
|
-
executionMode: "parallel",
|
|
148
|
+
executionMode: "parallel",
|
|
178
149
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
179
150
|
const typed = params as SubagentParamsShape;
|
|
180
151
|
const details = manager.startInBackground(typed, ctx);
|
|
181
152
|
if (typed.autoAwait) {
|
|
182
|
-
// awaitRun wakes on every child→leader message (ask/notify/done). Re-park
|
|
183
|
-
// until terminal — but surface an ask_parent: the child is waiting on the
|
|
184
|
-
// leader, so break out, reply via reply_subagent, then await again.
|
|
185
153
|
let run = details.run;
|
|
186
|
-
|
|
187
|
-
// last is lost (they were consumed by the park, never sent as followUp).
|
|
154
|
+
|
|
188
155
|
const intercom: ParkedMsg[] = [];
|
|
189
156
|
while (!TERMINAL.includes(run.status)) {
|
|
190
157
|
const awaited = await manager.awaitRun(details.run.id);
|
|
191
|
-
if (!awaited) break;
|
|
158
|
+
if (!awaited) break;
|
|
192
159
|
if (awaited.run) run = awaited.run;
|
|
193
160
|
intercom.push(...awaited.intercom);
|
|
194
161
|
if (awaited.intercom.some((m) => m.kind === "ask")) break;
|
|
195
162
|
}
|
|
196
|
-
|
|
197
|
-
// followUp notice (the park swallowed it), so showing only the first
|
|
198
|
-
// leaves the rest blocked until their 10-minute timeout.
|
|
163
|
+
|
|
199
164
|
const asks = intercom.filter((m) => m.kind === "ask");
|
|
200
165
|
const heard = intercom.filter((m) => m.kind !== "ask");
|
|
201
166
|
const text = [
|
|
@@ -227,7 +192,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
227
192
|
};
|
|
228
193
|
},
|
|
229
194
|
renderCall(args, theme) {
|
|
230
|
-
// ponytail: args stream in partially, so mode is unknowable until JSON closes. Show "preparing…" instead of a wrong "single ?".
|
|
231
195
|
const hasEdges = args.tasks?.some((t) => t.needs?.length);
|
|
232
196
|
const mode = args.chain?.length
|
|
233
197
|
? `chain ${args.chain.length}`
|
|
@@ -237,7 +201,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
237
201
|
? `single ${args.agent}`
|
|
238
202
|
: "preparing…";
|
|
239
203
|
const flags = args.autoAwait ? "await" : "bg";
|
|
240
|
-
|
|
204
|
+
|
|
241
205
|
const tasks = args.tasks ?? args.chain ?? [];
|
|
242
206
|
const writeCount = tasks.filter((t) => t.write).length;
|
|
243
207
|
const parts: string[] = [];
|
|
@@ -250,8 +214,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
250
214
|
const params = parts.length > 0 ? `\n ${theme.fg("dim", parts.join(" · "))}` : "";
|
|
251
215
|
const notation = waveNotation(tasks);
|
|
252
216
|
const graphLine = notation ? `\n ${theme.fg("muted", notation)}` : "";
|
|
253
|
-
|
|
254
|
-
// so a graph is visible before the first child spawns.
|
|
217
|
+
|
|
255
218
|
const plan = tasks
|
|
256
219
|
.filter((t) => t.agent || t.id)
|
|
257
220
|
.map((t, i: number) => {
|
|
@@ -259,7 +222,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
259
222
|
const edge = t.needs?.length ? theme.fg("muted", ` ← ${t.needs.join(", ")}`) : "";
|
|
260
223
|
const mark = t.write ? theme.fg("warning", " ✎") : "";
|
|
261
224
|
const meta = [t.model ? t.model : "", t.thinking ? t.thinking : ""].filter(Boolean).join(" ");
|
|
262
|
-
|
|
225
|
+
|
|
263
226
|
const flat = String(t.task ?? "")
|
|
264
227
|
.replace(/\s+/g, " ")
|
|
265
228
|
.trim();
|
|
@@ -276,11 +239,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
276
239
|
renderResult(result, { expanded }, theme) {
|
|
277
240
|
const run = result.details?.run;
|
|
278
241
|
if (!run) return new Text(result.content[0]?.type === "text" ? result.content[0].text : "", 0, 0);
|
|
279
|
-
|
|
242
|
+
|
|
280
243
|
const header = `${statusIcon(run.status)} ${theme.fg("accent", `${run.tasks.filter((t) => t.status === "completed").length}/${run.tasks.length} done`)} ${theme.fg("muted", run.status)}`;
|
|
281
244
|
if (!expanded) {
|
|
282
|
-
// Every run is background: the spawn snapshot is always "0 tools" noise and the footer
|
|
283
|
-
// widget already shows live per-task state — keep the card to the header only.
|
|
284
245
|
const usage = formatUsage(run.aggregateUsage);
|
|
285
246
|
return new Text(usage ? `${header}\n${theme.fg("dim", usage)}` : header, 0, 0);
|
|
286
247
|
}
|
|
@@ -312,8 +273,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
312
273
|
const { runId } = params as { runId: string };
|
|
313
274
|
const run = manager.getRun(runId);
|
|
314
275
|
if (!run) return { content: [{ type: "text", text: `Unknown runId: ${runId}` }], isError: true, details: {} };
|
|
315
|
-
|
|
316
|
-
// multiplexer pane, a log viewer, anything. Cheaper than owning a pane integration.
|
|
276
|
+
|
|
317
277
|
const files = run.tasks.filter((t) => t.sessionFile).map((t) => `${t.id} (${t.agent}): ${t.sessionFile}`);
|
|
318
278
|
const text = [
|
|
319
279
|
compactLines(run).join("\n"),
|
package/src/mailbox.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent↔agent mailbox. Pure logic, no pi imports — easily unit-tested.
|
|
3
|
-
* Agents talk by polling, not push: send() enqueues, poll() drains.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
export interface MailboxMessage {
|
|
7
2
|
from: string;
|
|
8
3
|
text: string;
|
|
@@ -11,9 +6,7 @@ export interface MailboxMessage {
|
|
|
11
6
|
|
|
12
7
|
export interface Mailbox {
|
|
13
8
|
open(taskId: string): void;
|
|
14
|
-
/** Returns false when sender or target is unknown (no silent drops). */
|
|
15
9
|
send(from: string, to: string, text: string): boolean;
|
|
16
|
-
/** Return and clear all pending messages for taskId. */
|
|
17
10
|
poll(taskId: string): MailboxMessage[];
|
|
18
11
|
close(taskId: string): void;
|
|
19
12
|
}
|