@arhen/pi-core-subagent 1.1.7 → 1.1.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 +1 -1
- package/package.json +1 -1
- package/src/child.ts +2 -2
- package/src/index.ts +49 -39
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Built for one job: delegate work to isolated subagents **without bloating the pa
|
|
|
15
15
|
- **In-process** — children are `AgentSession`s in the same runtime. No process spawn, no context bleed.
|
|
16
16
|
- **Zero parent-context injection.** No catalog, no context hook. 6 slim tools total.
|
|
17
17
|
- **Throttled updates** — widget/stream updates coalesce to ~6/s; no per-event deep clones.
|
|
18
|
-
- **No silent hangs** — watchdog aborts children that produce no events for
|
|
18
|
+
- **No silent hangs** — watchdog aborts children that produce no events for 3 minutes; per-task timeout 10min.
|
|
19
19
|
|
|
20
20
|
## Install
|
|
21
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arhen/pi-core-subagent",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi extension: fast in-process subagents with single/parallel/chain, background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
|
|
6
6
|
"license": "MIT",
|
package/src/child.ts
CHANGED
|
@@ -98,8 +98,8 @@ export function createChildTools(taskId: string, handlers: ChildHandlers): ToolD
|
|
|
98
98
|
if (messages.length === 0) return { content: [{ type: "text" as const, text: "No messages." }], details: {} };
|
|
99
99
|
const body = messages
|
|
100
100
|
.map((m) => `from ${m.from}: ${m.text}`)
|
|
101
|
-
.join("\n")
|
|
102
|
-
|
|
101
|
+
.join("\n");
|
|
102
|
+
const capped = body.length > 4000 ? body.slice(0, 4000).replace(/[\uD800-\uDBFF]$/, "") : body; // multibyte-safe
|
|
103
103
|
return { content: [{ type: "text" as const, text: body }], details: { messages } };
|
|
104
104
|
},
|
|
105
105
|
},
|
package/src/index.ts
CHANGED
|
@@ -253,8 +253,6 @@ function compactLines(run: RunSnapshot): string[] {
|
|
|
253
253
|
* └─ ✓ reviewer · 6 tools · 44s
|
|
254
254
|
* Static icons (no animation); latest activity + tool count + runtime per agent.
|
|
255
255
|
*/
|
|
256
|
-
const WIDGET_MAX_RUNS = 3;
|
|
257
|
-
const WIDGET_MAX_TASKS_PER_RUN = 4;
|
|
258
256
|
const WIDGET_MAX_LINES = 10;
|
|
259
257
|
|
|
260
258
|
class SubagentsWidget implements Component {
|
|
@@ -268,38 +266,39 @@ class SubagentsWidget implements Component {
|
|
|
268
266
|
}
|
|
269
267
|
|
|
270
268
|
render(width: number): string[] {
|
|
271
|
-
|
|
269
|
+
// ONE flat tree: every run's tasks concatenated under a single heading.
|
|
270
|
+
// Whether the model spawned N runs or one tasks[] call, the pane reads the same.
|
|
271
|
+
const runs = this.getRuns().filter((r) => r.tasks.length > 0);
|
|
272
272
|
if (runs.length === 0) return [];
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
lines.push(truncateToWidth(`${this.theme.fg(head, active ? "●" : "○")} ${this.theme.fg(head, `Subagents (${done}/${run.tasks.length})`)}`, width, "…"));
|
|
273
|
+
const total = runs.reduce((n, r) => n + r.tasks.length, 0);
|
|
274
|
+
const done = runs.reduce((n, r) => n + r.tasks.filter((t) => TERMINAL.includes(t.status)).length, 0);
|
|
275
|
+
const live = total - done;
|
|
276
|
+
const head = live > 0 ? "accent" : "dim";
|
|
277
|
+
const lines = [truncateToWidth(`${this.theme.fg(head, live > 0 ? "●" : "○")} ${this.theme.fg(head, `Subagents (${done}/${total})`)}`, width, "…")];
|
|
278
|
+
const budget = WIDGET_MAX_LINES - 1;
|
|
279
|
+
let shown = 0;
|
|
280
|
+
outer: for (const run of runs) {
|
|
282
281
|
const allDone = TERMINAL.includes(run.status);
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const conn = this.theme.fg("dim", last ? "└─" : "├─");
|
|
282
|
+
for (const task of run.tasks) {
|
|
283
|
+
if (shown >= budget) break outer;
|
|
284
|
+
shown += 1;
|
|
287
285
|
const activity =
|
|
288
286
|
!TERMINAL.includes(task.status) && task.lastActivity
|
|
289
287
|
? `${this.theme.fg("dim", `→ ${task.lastActivity}`)} · `
|
|
290
288
|
: "";
|
|
291
|
-
//
|
|
289
|
+
// Tasks of a finished run: dim everything except the agent name.
|
|
292
290
|
const line = allDone
|
|
293
291
|
? `${this.theme.fg("dim", `${statusIcon(task.status)} `)}${task.agent} ${this.theme.fg("dim", `· ${taskStatsWithUsage(task)} · ${taskTimer(task)}`)}`
|
|
294
292
|
: `${statusIcon(task.status)} ${task.agent} · ${activity}${taskStatsWithUsage(task)} · ${taskTimer(task)}`;
|
|
295
|
-
lines.push(truncateToWidth(`${
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
293
|
+
lines.push(truncateToWidth(`${this.theme.fg("dim", "├─")} ${line}`, width, "…"));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const hidden = total - shown;
|
|
297
|
+
if (hidden > 0) {
|
|
298
|
+
lines.push(`${this.theme.fg("dim", "└─")} ${this.theme.fg("dim", `+${hidden} more`)}`);
|
|
299
|
+
} else if (lines.length > 1) {
|
|
300
|
+
lines[lines.length - 1] = lines[lines.length - 1]!.replace("├─", "└─");
|
|
300
301
|
}
|
|
301
|
-
const hiddenRuns = runs.length - renderedRuns;
|
|
302
|
-
if (hiddenRuns > 0) lines.push(`${this.theme.fg("dim", "└─")} ${this.theme.fg("dim", `+${hiddenRuns} more run${hiddenRuns > 1 ? "s" : ""}`)}`);
|
|
303
302
|
return lines;
|
|
304
303
|
}
|
|
305
304
|
}
|
|
@@ -308,7 +307,8 @@ function makeSummary(run: RunSnapshot): string {
|
|
|
308
307
|
const succeeded = run.tasks.filter((t) => t.status === "completed").length;
|
|
309
308
|
const failed = run.tasks.filter((t) => t.status === "failed").length;
|
|
310
309
|
const aborted = run.tasks.filter((t) => t.status === "aborted").length;
|
|
311
|
-
const
|
|
310
|
+
const done = TERMINAL.includes(run.status) ? "finished" : "running";
|
|
311
|
+
const lines = [`Run ${run.id}: Subagents ${run.mode}${run.background ? " (background)" : ""} ${done}: ${succeeded}/${run.tasks.length} succeeded${failed ? `, ${failed} failed` : ""}${aborted ? `, ${aborted} aborted` : ""}.`];
|
|
312
312
|
const usage = formatUsage(run.aggregateUsage);
|
|
313
313
|
if (usage) lines.push(`Usage: ${usage}`);
|
|
314
314
|
for (const task of run.tasks) {
|
|
@@ -496,7 +496,9 @@ class SubagentManager {
|
|
|
496
496
|
const parentFile = getParentSessionFile(ctx);
|
|
497
497
|
if (!parentFile) return;
|
|
498
498
|
const sidecar = parentFile.replace(/\.jsonl$/, ".subagents.json");
|
|
499
|
-
import("fs")
|
|
499
|
+
import("fs")
|
|
500
|
+
.then(({ writeFileSync }) => writeFileSync(sidecar, JSON.stringify(this.listRuns().slice(0, 50).map(cloneRun), null, 2)))
|
|
501
|
+
.catch(() => {}); // never surface as an unhandled rejection
|
|
500
502
|
} catch {
|
|
501
503
|
/* ignore */
|
|
502
504
|
}
|
|
@@ -534,8 +536,7 @@ class SubagentManager {
|
|
|
534
536
|
}
|
|
535
537
|
|
|
536
538
|
// Widget: register-once + requestRender (todo-overlay pattern).
|
|
537
|
-
//
|
|
538
|
-
// scheduleWidget just throttles status changes into requestRender calls.
|
|
539
|
+
// scheduleWidget throttles status changes into requestRender calls.
|
|
539
540
|
private widgetTui: TUI | null = null;
|
|
540
541
|
/** Upsert a run into the widget's visible set (all runs, not just the latest). */
|
|
541
542
|
private upsertWidgetRun(run: RunSnapshot | undefined): void {
|
|
@@ -601,7 +602,6 @@ class SubagentManager {
|
|
|
601
602
|
onAskParent: async (_taskId, question) => {
|
|
602
603
|
this.updateTask(run, task, { status: "awaiting_parent" }, ctx);
|
|
603
604
|
this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
|
|
604
|
-
this.notifyParent(run, "asked", { taskId: task.id, question });
|
|
605
605
|
// A blocking run's parent can't reply mid-tool (followUp only fires after the
|
|
606
606
|
// tool returns) — only background runs can truly wait for the answer.
|
|
607
607
|
if (!run.background) {
|
|
@@ -609,10 +609,17 @@ class SubagentManager {
|
|
|
609
609
|
this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
|
|
610
610
|
return "Parent cannot answer while this run is blocking. Continue autonomously with your best judgment.";
|
|
611
611
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
|
|
615
|
-
|
|
612
|
+
this.notifyParent(run, "asked", { taskId: task.id, question });
|
|
613
|
+
// M3: a waiting child is not stalled — keep the watchdog fed until the reply.
|
|
614
|
+
const keepAlive = setInterval(() => this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog(), 30_000);
|
|
615
|
+
try {
|
|
616
|
+
const reply = await this.awaitParentReply(run.id, task.id);
|
|
617
|
+
this.updateTask(run, task, { status: "running" }, ctx);
|
|
618
|
+
this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
|
|
619
|
+
return reply;
|
|
620
|
+
} finally {
|
|
621
|
+
clearInterval(keepAlive);
|
|
622
|
+
}
|
|
616
623
|
},
|
|
617
624
|
onNotifyParent: (_taskId, message, level) => {
|
|
618
625
|
this.emit("subagent:intercom", { runId: run.id, taskId: task.id, kind: "notify", level, message });
|
|
@@ -794,7 +801,7 @@ class SubagentManager {
|
|
|
794
801
|
runController?.signal.removeEventListener("abort", abortChild);
|
|
795
802
|
};
|
|
796
803
|
// Cancel may have landed during session creation — honor it before prompting.
|
|
797
|
-
if (run.status === "aborted" || TERMINAL.includes(task.status)) {
|
|
804
|
+
if (run.status === "aborted" || TERMINAL.includes(task.status) || signal?.aborted) {
|
|
798
805
|
await child.abort();
|
|
799
806
|
throw new Error("Canceled by subagent_cancel");
|
|
800
807
|
}
|
|
@@ -929,7 +936,7 @@ class SubagentManager {
|
|
|
929
936
|
if (run.notifyPerTask && run.background && TERMINAL.includes(task.status)) {
|
|
930
937
|
this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
931
938
|
}
|
|
932
|
-
if (task.status !== "completed") break;
|
|
939
|
+
if (task.status !== "completed") break; // aborted/failed link stops the chain
|
|
933
940
|
previous = task.finalText ?? "";
|
|
934
941
|
}
|
|
935
942
|
} else {
|
|
@@ -954,8 +961,11 @@ class SubagentManager {
|
|
|
954
961
|
} else {
|
|
955
962
|
this.clearWidget(ctx);
|
|
956
963
|
}
|
|
957
|
-
|
|
958
|
-
this.
|
|
964
|
+
// L7: cancelRun already emitted + settled — don't double-report.
|
|
965
|
+
if (this.settlers.has(run.id)) {
|
|
966
|
+
this.emit("subagent:run-completed", { runId: run.id, status: run.status, run: cloneRun(run), aggregateUsage: run.aggregateUsage });
|
|
967
|
+
this.settleRun(run.id, run);
|
|
968
|
+
}
|
|
959
969
|
this.runControllers.delete(run.id);
|
|
960
970
|
for (const task of run.tasks) this.mailboxes.close(`${run.id}:${task.id}`);
|
|
961
971
|
this.persist(ctx);
|
|
@@ -1139,7 +1149,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1139
1149
|
});
|
|
1140
1150
|
|
|
1141
1151
|
pi.on("agent_start", (_event, ctx) => {
|
|
1142
|
-
if (!manager.turnActivity) manager.clearWidget(ctx);
|
|
1152
|
+
if (!manager.turnActivity && !manager.hasActiveRun()) manager.clearWidget(ctx);
|
|
1143
1153
|
manager.turnActivity = false;
|
|
1144
1154
|
});
|
|
1145
1155
|
|
|
@@ -1164,7 +1174,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1164
1174
|
promptSnippet: "Define and delegate work to specialized subagents.",
|
|
1165
1175
|
promptGuidelines: [
|
|
1166
1176
|
"Use subagent when independent review, testing, research, or parallel analysis improves quality.",
|
|
1167
|
-
"Decompose parallelizable work: if the request has 2+ independent sub-tasks (separate files, separate concerns, independent research/review),
|
|
1177
|
+
"Decompose parallelizable work: if the request has 2+ independent sub-tasks (separate files, separate concerns, independent research/review), spawn N agents with a SINGLE call: subagent({ tasks: [{agent, task}, ...] }). NEVER make multiple parallel subagent calls for parallel work — one call, one run, N tasks.",
|
|
1168
1178
|
"If independent sub-tasks are sequential (each builds on the previous one's output), use chain mode with {previous}.",
|
|
1169
1179
|
"Define each subagent yourself: an invented name, a focused system prompt (prompt:), and a toolset — read-only (default) or write (write:true).",
|
|
1170
1180
|
"Prefer read-only subagents unless the task explicitly needs edits.",
|