@arhen/pi-core-subagent 1.1.6 → 1.1.8
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 +4 -4
- package/package.json +1 -1
- package/src/index.ts +56 -37
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Built for one job: delegate work to isolated subagents **without bloating the pa
|
|
|
10
10
|
|
|
11
11
|
## Design principles
|
|
12
12
|
|
|
13
|
-
- **No agent files
|
|
13
|
+
- **No agent files, no discovery.** The leader defines every subagent inline per call — name, system prompt, toolset. Nothing is read from or written to disk.
|
|
14
14
|
- **Two toolsets only.** Read-only (`read, grep, find, ls` — default) or write (`read, grep, find, ls, bash, edit, write` — `write: true`). No per-agent tool config surface.
|
|
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.
|
|
@@ -26,7 +26,7 @@ pi install npm:@arhen/pi-core-subagent
|
|
|
26
26
|
|
|
27
27
|
## Usage — the leader invents the agents
|
|
28
28
|
|
|
29
|
-
Define agents inline
|
|
29
|
+
Define agents inline per call — never creates or reads agent files. Model resolution: explicit `provider/model-id` (or bare id) via the pi model registry → agent-file `model` → the parent's current model → settings default.
|
|
30
30
|
|
|
31
31
|
```json
|
|
32
32
|
{
|
|
@@ -84,7 +84,7 @@ Background + intercom:
|
|
|
84
84
|
|
|
85
85
|
### Per-task fields
|
|
86
86
|
|
|
87
|
-
`agent` (name you invent
|
|
87
|
+
`agent` (name you invent — required), `task` (required), `prompt` (system prompt, optional — minimal default used), `write` (toolset, default read-only), plus optional `model` (`provider/model-id`), `thinking` (validated enum: `off|minimal|low|medium|high|xhigh|max`), `tools` (explicit allowlist), `cwd`, `maxRuntimeMs`, `id`. Top-level only: `background`, `notifyPerTask`, `allowIntercom`, `concurrency`.
|
|
88
88
|
|
|
89
89
|
### Child talk tools (when `allowIntercom: true`)
|
|
90
90
|
|
|
@@ -99,7 +99,7 @@ Background + intercom:
|
|
|
99
99
|
|
|
100
100
|
- Parent tools: 6 schemas with short descriptions. **No catalog, no context hook** — nothing injected per request.
|
|
101
101
|
- Background completion: 3-line notice. Full text only via `subagent_result`.
|
|
102
|
-
- Children: isolated sessions; talk tools injected only when `allowIntercom`; each child's prompt states its own task id and its siblings' so mailbox addressing works.
|
|
102
|
+
- Children: isolated sessions; talk tools injected only when `allowIntercom`; each child's prompt states its own task id and its siblings' so mailbox addressing works. Model resolution: explicit `provider/model-id` or bare id via the pi model registry → the parent's current model → settings default. Thinking levels validated against the resolved model's `thinkingLevelMap`.
|
|
103
103
|
|
|
104
104
|
## Development
|
|
105
105
|
|
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.8",
|
|
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/index.ts
CHANGED
|
@@ -98,7 +98,7 @@ interface RunSnapshot {
|
|
|
98
98
|
concurrency: number;
|
|
99
99
|
/** True once the parent awaited this run — completion notices are redundant then. */
|
|
100
100
|
awaited?: boolean;
|
|
101
|
-
/** Wake the parent (queued follow-up turn) as each task completes. Default
|
|
101
|
+
/** Wake the parent (queued follow-up turn) as each task completes. Default false. */
|
|
102
102
|
notifyPerTask: boolean;
|
|
103
103
|
tasks: TaskSnapshot[];
|
|
104
104
|
aggregateUsage: UsageStats;
|
|
@@ -135,7 +135,9 @@ function aggregateUsage(tasks: TaskSnapshot[]): UsageStats {
|
|
|
135
135
|
}
|
|
136
136
|
function truncateText(text: string, max = FINAL_OUTPUT_CAP): string {
|
|
137
137
|
if (Buffer.byteLength(text, "utf8") <= max) return text;
|
|
138
|
-
|
|
138
|
+
let out = text.slice(0, max);
|
|
139
|
+
while (Buffer.byteLength(out, "utf8") > max) out = out.slice(0, -1); // multibyte-safe
|
|
140
|
+
return `${out}\n\n[Output truncated. Full child session is available in the session file.]`;
|
|
139
141
|
}
|
|
140
142
|
function getFirstText(message: AssistantMessage): string {
|
|
141
143
|
for (const part of message?.content ?? []) {
|
|
@@ -251,8 +253,6 @@ function compactLines(run: RunSnapshot): string[] {
|
|
|
251
253
|
* └─ ✓ reviewer · 6 tools · 44s
|
|
252
254
|
* Static icons (no animation); latest activity + tool count + runtime per agent.
|
|
253
255
|
*/
|
|
254
|
-
const WIDGET_MAX_RUNS = 3;
|
|
255
|
-
const WIDGET_MAX_TASKS_PER_RUN = 4;
|
|
256
256
|
const WIDGET_MAX_LINES = 10;
|
|
257
257
|
|
|
258
258
|
class SubagentsWidget implements Component {
|
|
@@ -266,38 +266,39 @@ class SubagentsWidget implements Component {
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
render(width: number): string[] {
|
|
269
|
-
|
|
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);
|
|
270
272
|
if (runs.length === 0) return [];
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
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) {
|
|
280
281
|
const allDone = TERMINAL.includes(run.status);
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const conn = this.theme.fg("dim", last ? "└─" : "├─");
|
|
282
|
+
for (const task of run.tasks) {
|
|
283
|
+
if (shown >= budget) break outer;
|
|
284
|
+
shown += 1;
|
|
285
285
|
const activity =
|
|
286
286
|
!TERMINAL.includes(task.status) && task.lastActivity
|
|
287
287
|
? `${this.theme.fg("dim", `→ ${task.lastActivity}`)} · `
|
|
288
288
|
: "";
|
|
289
|
-
//
|
|
289
|
+
// Tasks of a finished run: dim everything except the agent name.
|
|
290
290
|
const line = allDone
|
|
291
291
|
? `${this.theme.fg("dim", `${statusIcon(task.status)} `)}${task.agent} ${this.theme.fg("dim", `· ${taskStatsWithUsage(task)} · ${taskTimer(task)}`)}`
|
|
292
292
|
: `${statusIcon(task.status)} ${task.agent} · ${activity}${taskStatsWithUsage(task)} · ${taskTimer(task)}`;
|
|
293
|
-
lines.push(truncateToWidth(`${
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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("├─", "└─");
|
|
298
301
|
}
|
|
299
|
-
const hiddenRuns = runs.length - renderedRuns;
|
|
300
|
-
if (hiddenRuns > 0) lines.push(`${this.theme.fg("dim", "└─")} ${this.theme.fg("dim", `+${hiddenRuns} more run${hiddenRuns > 1 ? "s" : ""}`)}`);
|
|
301
302
|
return lines;
|
|
302
303
|
}
|
|
303
304
|
}
|
|
@@ -460,9 +461,17 @@ class SubagentManager {
|
|
|
460
461
|
if (!Array.isArray(raw)) return;
|
|
461
462
|
runs = (raw as RunSnapshot[]).map((run) => {
|
|
462
463
|
const interrupted = run.tasks.some((t) => !TERMINAL.includes(t.status));
|
|
464
|
+
// A persisted "running" run whose tasks are all terminal (crash between
|
|
465
|
+
// task end and run end) must not stay "running" forever.
|
|
466
|
+
let status = interrupted ? ("aborted" as RunStatus) : run.status;
|
|
467
|
+
if (!TERMINAL.includes(status)) {
|
|
468
|
+
const anyFailed = run.tasks.some((t) => t.status === "failed");
|
|
469
|
+
const anyAborted = run.tasks.some((t) => t.status === "aborted");
|
|
470
|
+
status = anyFailed ? "failed" : anyAborted ? "aborted" : "completed";
|
|
471
|
+
}
|
|
463
472
|
return {
|
|
464
473
|
...run,
|
|
465
|
-
status
|
|
474
|
+
status,
|
|
466
475
|
endedAt: interrupted ? Date.now() : run.endedAt,
|
|
467
476
|
tasks: run.tasks.map((t) => (TERMINAL.includes(t.status) ? t : { ...t, status: "aborted" as TaskStatus, error: t.error || "Interrupted by session reload" })),
|
|
468
477
|
};
|
|
@@ -486,7 +495,7 @@ class SubagentManager {
|
|
|
486
495
|
const parentFile = getParentSessionFile(ctx);
|
|
487
496
|
if (!parentFile) return;
|
|
488
497
|
const sidecar = parentFile.replace(/\.jsonl$/, ".subagents.json");
|
|
489
|
-
import("fs").then(({ writeFileSync }) => writeFileSync(sidecar, JSON.stringify(this.listRuns().map(cloneRun), null, 2)));
|
|
498
|
+
import("fs").then(({ writeFileSync }) => writeFileSync(sidecar, JSON.stringify(this.listRuns().slice(0, 50).map(cloneRun), null, 2)));
|
|
490
499
|
} catch {
|
|
491
500
|
/* ignore */
|
|
492
501
|
}
|
|
@@ -850,6 +859,13 @@ class SubagentManager {
|
|
|
850
859
|
? params.tasks!
|
|
851
860
|
: params.chain!;
|
|
852
861
|
if (inputs.length > MAX_TASKS) throw new Error(`Too many subagent tasks (${inputs.length}). Max is ${MAX_TASKS}.`);
|
|
862
|
+
const ids = new Set<string>();
|
|
863
|
+
for (const input of inputs) {
|
|
864
|
+
if (input.id !== undefined) {
|
|
865
|
+
if (ids.has(input.id)) throw new Error(`Duplicate task id: ${input.id}`);
|
|
866
|
+
ids.add(input.id);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
853
869
|
|
|
854
870
|
const run: RunSnapshot = {
|
|
855
871
|
id: newId("run"),
|
|
@@ -909,7 +925,7 @@ class SubagentManager {
|
|
|
909
925
|
const input = { ...inputs[i]!, task: next };
|
|
910
926
|
task.task = input.task;
|
|
911
927
|
await this.runChild(run, task, input, ctx, signal, onUpdate);
|
|
912
|
-
if (run.notifyPerTask && TERMINAL.includes(task.status)) {
|
|
928
|
+
if (run.notifyPerTask && run.background && TERMINAL.includes(task.status)) {
|
|
913
929
|
this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
914
930
|
}
|
|
915
931
|
if (task.status !== "completed") break;
|
|
@@ -919,7 +935,7 @@ class SubagentManager {
|
|
|
919
935
|
await mapWithConcurrency(run.tasks, run.mode === "single" ? 1 : run.concurrency, async (task) => {
|
|
920
936
|
const index = run.tasks.indexOf(task);
|
|
921
937
|
await this.runChild(run, task, inputs[index]!, ctx, signal, onUpdate);
|
|
922
|
-
if (run.notifyPerTask && TERMINAL.includes(task.status)) {
|
|
938
|
+
if (run.notifyPerTask && run.background && TERMINAL.includes(task.status)) {
|
|
923
939
|
this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
924
940
|
}
|
|
925
941
|
});
|
|
@@ -969,7 +985,10 @@ class SubagentManager {
|
|
|
969
985
|
}
|
|
970
986
|
this.settleRun(run.id, run);
|
|
971
987
|
this.runControllers.delete(run.id);
|
|
988
|
+
for (const task of run.tasks) this.mailboxes.close(`${run.id}:${task.id}`);
|
|
989
|
+
this.emit("subagent:run-completed", { runId: run.id, status: "failed", run: cloneRun(run) });
|
|
972
990
|
this.notifyParent(run, "failed");
|
|
991
|
+
this.persist(ctx);
|
|
973
992
|
});
|
|
974
993
|
return { run: cloneRun(run), background: true };
|
|
975
994
|
}
|
|
@@ -988,7 +1007,7 @@ class SubagentManager {
|
|
|
988
1007
|
for (const task of run.tasks) {
|
|
989
1008
|
if (TERMINAL.includes(task.status)) continue;
|
|
990
1009
|
task.status = "aborted";
|
|
991
|
-
task.error = task.error || "Canceled by subagent_cancel";
|
|
1010
|
+
task.error = task.error || "Canceled by subagent_cancel"; // never overwrite a real error
|
|
992
1011
|
task.endedAt = Date.now();
|
|
993
1012
|
aborted += 1;
|
|
994
1013
|
}
|
|
@@ -1026,9 +1045,10 @@ class SubagentManager {
|
|
|
1026
1045
|
if (!timeoutMs) return settled;
|
|
1027
1046
|
return Promise.race([
|
|
1028
1047
|
settled,
|
|
1029
|
-
new Promise<RunSnapshot | undefined>((resolve) =>
|
|
1030
|
-
setTimeout(() => resolve(this.runs.get(runId) ? cloneRun(this.runs.get(runId)!) : undefined), timeoutMs)
|
|
1031
|
-
|
|
1048
|
+
new Promise<RunSnapshot | undefined>((resolve) => {
|
|
1049
|
+
const timer = setTimeout(() => resolve(this.runs.get(runId) ? cloneRun(this.runs.get(runId)!) : undefined), timeoutMs);
|
|
1050
|
+
settled.then(() => clearTimeout(timer));
|
|
1051
|
+
}),
|
|
1032
1052
|
]);
|
|
1033
1053
|
}
|
|
1034
1054
|
}
|
|
@@ -1113,8 +1133,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1113
1133
|
ctx.ui.notify("No subagent runs in this session.", "info");
|
|
1114
1134
|
return;
|
|
1115
1135
|
}
|
|
1116
|
-
ctx.ui.
|
|
1117
|
-
ctx.ui.notify(`Showing ${runs.length} subagent run(s).`, "info");
|
|
1136
|
+
ctx.ui.notify(runs.flatMap((run) => compactLines(run).concat("")).join("\n") || "No subagent runs in this session.", "info");
|
|
1118
1137
|
},
|
|
1119
1138
|
});
|
|
1120
1139
|
|
|
@@ -1144,7 +1163,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1144
1163
|
promptSnippet: "Define and delegate work to specialized subagents.",
|
|
1145
1164
|
promptGuidelines: [
|
|
1146
1165
|
"Use subagent when independent review, testing, research, or parallel analysis improves quality.",
|
|
1147
|
-
"Decompose parallelizable work: if the request has 2+ independent sub-tasks (separate files, separate concerns, independent research/review),
|
|
1166
|
+
"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.",
|
|
1148
1167
|
"If independent sub-tasks are sequential (each builds on the previous one's output), use chain mode with {previous}.",
|
|
1149
1168
|
"Define each subagent yourself: an invented name, a focused system prompt (prompt:), and a toolset — read-only (default) or write (write:true).",
|
|
1150
1169
|
"Prefer read-only subagents unless the task explicitly needs edits.",
|