@arhen/pi-core-subagent 1.1.6 → 1.1.7
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 +32 -12
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.7",
|
|
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 ?? []) {
|
|
@@ -460,9 +462,17 @@ class SubagentManager {
|
|
|
460
462
|
if (!Array.isArray(raw)) return;
|
|
461
463
|
runs = (raw as RunSnapshot[]).map((run) => {
|
|
462
464
|
const interrupted = run.tasks.some((t) => !TERMINAL.includes(t.status));
|
|
465
|
+
// A persisted "running" run whose tasks are all terminal (crash between
|
|
466
|
+
// task end and run end) must not stay "running" forever.
|
|
467
|
+
let status = interrupted ? ("aborted" as RunStatus) : run.status;
|
|
468
|
+
if (!TERMINAL.includes(status)) {
|
|
469
|
+
const anyFailed = run.tasks.some((t) => t.status === "failed");
|
|
470
|
+
const anyAborted = run.tasks.some((t) => t.status === "aborted");
|
|
471
|
+
status = anyFailed ? "failed" : anyAborted ? "aborted" : "completed";
|
|
472
|
+
}
|
|
463
473
|
return {
|
|
464
474
|
...run,
|
|
465
|
-
status
|
|
475
|
+
status,
|
|
466
476
|
endedAt: interrupted ? Date.now() : run.endedAt,
|
|
467
477
|
tasks: run.tasks.map((t) => (TERMINAL.includes(t.status) ? t : { ...t, status: "aborted" as TaskStatus, error: t.error || "Interrupted by session reload" })),
|
|
468
478
|
};
|
|
@@ -486,7 +496,7 @@ class SubagentManager {
|
|
|
486
496
|
const parentFile = getParentSessionFile(ctx);
|
|
487
497
|
if (!parentFile) return;
|
|
488
498
|
const sidecar = parentFile.replace(/\.jsonl$/, ".subagents.json");
|
|
489
|
-
import("fs").then(({ writeFileSync }) => writeFileSync(sidecar, JSON.stringify(this.listRuns().map(cloneRun), null, 2)));
|
|
499
|
+
import("fs").then(({ writeFileSync }) => writeFileSync(sidecar, JSON.stringify(this.listRuns().slice(0, 50).map(cloneRun), null, 2)));
|
|
490
500
|
} catch {
|
|
491
501
|
/* ignore */
|
|
492
502
|
}
|
|
@@ -850,6 +860,13 @@ class SubagentManager {
|
|
|
850
860
|
? params.tasks!
|
|
851
861
|
: params.chain!;
|
|
852
862
|
if (inputs.length > MAX_TASKS) throw new Error(`Too many subagent tasks (${inputs.length}). Max is ${MAX_TASKS}.`);
|
|
863
|
+
const ids = new Set<string>();
|
|
864
|
+
for (const input of inputs) {
|
|
865
|
+
if (input.id !== undefined) {
|
|
866
|
+
if (ids.has(input.id)) throw new Error(`Duplicate task id: ${input.id}`);
|
|
867
|
+
ids.add(input.id);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
853
870
|
|
|
854
871
|
const run: RunSnapshot = {
|
|
855
872
|
id: newId("run"),
|
|
@@ -909,7 +926,7 @@ class SubagentManager {
|
|
|
909
926
|
const input = { ...inputs[i]!, task: next };
|
|
910
927
|
task.task = input.task;
|
|
911
928
|
await this.runChild(run, task, input, ctx, signal, onUpdate);
|
|
912
|
-
if (run.notifyPerTask && TERMINAL.includes(task.status)) {
|
|
929
|
+
if (run.notifyPerTask && run.background && TERMINAL.includes(task.status)) {
|
|
913
930
|
this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
914
931
|
}
|
|
915
932
|
if (task.status !== "completed") break;
|
|
@@ -919,7 +936,7 @@ class SubagentManager {
|
|
|
919
936
|
await mapWithConcurrency(run.tasks, run.mode === "single" ? 1 : run.concurrency, async (task) => {
|
|
920
937
|
const index = run.tasks.indexOf(task);
|
|
921
938
|
await this.runChild(run, task, inputs[index]!, ctx, signal, onUpdate);
|
|
922
|
-
if (run.notifyPerTask && TERMINAL.includes(task.status)) {
|
|
939
|
+
if (run.notifyPerTask && run.background && TERMINAL.includes(task.status)) {
|
|
923
940
|
this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
924
941
|
}
|
|
925
942
|
});
|
|
@@ -969,7 +986,10 @@ class SubagentManager {
|
|
|
969
986
|
}
|
|
970
987
|
this.settleRun(run.id, run);
|
|
971
988
|
this.runControllers.delete(run.id);
|
|
989
|
+
for (const task of run.tasks) this.mailboxes.close(`${run.id}:${task.id}`);
|
|
990
|
+
this.emit("subagent:run-completed", { runId: run.id, status: "failed", run: cloneRun(run) });
|
|
972
991
|
this.notifyParent(run, "failed");
|
|
992
|
+
this.persist(ctx);
|
|
973
993
|
});
|
|
974
994
|
return { run: cloneRun(run), background: true };
|
|
975
995
|
}
|
|
@@ -988,7 +1008,7 @@ class SubagentManager {
|
|
|
988
1008
|
for (const task of run.tasks) {
|
|
989
1009
|
if (TERMINAL.includes(task.status)) continue;
|
|
990
1010
|
task.status = "aborted";
|
|
991
|
-
task.error = task.error || "Canceled by subagent_cancel";
|
|
1011
|
+
task.error = task.error || "Canceled by subagent_cancel"; // never overwrite a real error
|
|
992
1012
|
task.endedAt = Date.now();
|
|
993
1013
|
aborted += 1;
|
|
994
1014
|
}
|
|
@@ -1026,9 +1046,10 @@ class SubagentManager {
|
|
|
1026
1046
|
if (!timeoutMs) return settled;
|
|
1027
1047
|
return Promise.race([
|
|
1028
1048
|
settled,
|
|
1029
|
-
new Promise<RunSnapshot | undefined>((resolve) =>
|
|
1030
|
-
setTimeout(() => resolve(this.runs.get(runId) ? cloneRun(this.runs.get(runId)!) : undefined), timeoutMs)
|
|
1031
|
-
|
|
1049
|
+
new Promise<RunSnapshot | undefined>((resolve) => {
|
|
1050
|
+
const timer = setTimeout(() => resolve(this.runs.get(runId) ? cloneRun(this.runs.get(runId)!) : undefined), timeoutMs);
|
|
1051
|
+
settled.then(() => clearTimeout(timer));
|
|
1052
|
+
}),
|
|
1032
1053
|
]);
|
|
1033
1054
|
}
|
|
1034
1055
|
}
|
|
@@ -1113,8 +1134,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1113
1134
|
ctx.ui.notify("No subagent runs in this session.", "info");
|
|
1114
1135
|
return;
|
|
1115
1136
|
}
|
|
1116
|
-
ctx.ui.
|
|
1117
|
-
ctx.ui.notify(`Showing ${runs.length} subagent run(s).`, "info");
|
|
1137
|
+
ctx.ui.notify(runs.flatMap((run) => compactLines(run).concat("")).join("\n") || "No subagent runs in this session.", "info");
|
|
1118
1138
|
},
|
|
1119
1139
|
});
|
|
1120
1140
|
|