@arhen/pi-core-subagent 1.1.8 → 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 +25 -14
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
|
@@ -307,7 +307,8 @@ function makeSummary(run: RunSnapshot): string {
|
|
|
307
307
|
const succeeded = run.tasks.filter((t) => t.status === "completed").length;
|
|
308
308
|
const failed = run.tasks.filter((t) => t.status === "failed").length;
|
|
309
309
|
const aborted = run.tasks.filter((t) => t.status === "aborted").length;
|
|
310
|
-
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` : ""}.`];
|
|
311
312
|
const usage = formatUsage(run.aggregateUsage);
|
|
312
313
|
if (usage) lines.push(`Usage: ${usage}`);
|
|
313
314
|
for (const task of run.tasks) {
|
|
@@ -495,7 +496,9 @@ class SubagentManager {
|
|
|
495
496
|
const parentFile = getParentSessionFile(ctx);
|
|
496
497
|
if (!parentFile) return;
|
|
497
498
|
const sidecar = parentFile.replace(/\.jsonl$/, ".subagents.json");
|
|
498
|
-
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
|
|
499
502
|
} catch {
|
|
500
503
|
/* ignore */
|
|
501
504
|
}
|
|
@@ -533,8 +536,7 @@ class SubagentManager {
|
|
|
533
536
|
}
|
|
534
537
|
|
|
535
538
|
// Widget: register-once + requestRender (todo-overlay pattern).
|
|
536
|
-
//
|
|
537
|
-
// scheduleWidget just throttles status changes into requestRender calls.
|
|
539
|
+
// scheduleWidget throttles status changes into requestRender calls.
|
|
538
540
|
private widgetTui: TUI | null = null;
|
|
539
541
|
/** Upsert a run into the widget's visible set (all runs, not just the latest). */
|
|
540
542
|
private upsertWidgetRun(run: RunSnapshot | undefined): void {
|
|
@@ -600,7 +602,6 @@ class SubagentManager {
|
|
|
600
602
|
onAskParent: async (_taskId, question) => {
|
|
601
603
|
this.updateTask(run, task, { status: "awaiting_parent" }, ctx);
|
|
602
604
|
this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
|
|
603
|
-
this.notifyParent(run, "asked", { taskId: task.id, question });
|
|
604
605
|
// A blocking run's parent can't reply mid-tool (followUp only fires after the
|
|
605
606
|
// tool returns) — only background runs can truly wait for the answer.
|
|
606
607
|
if (!run.background) {
|
|
@@ -608,10 +609,17 @@ class SubagentManager {
|
|
|
608
609
|
this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
|
|
609
610
|
return "Parent cannot answer while this run is blocking. Continue autonomously with your best judgment.";
|
|
610
611
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
|
|
614
|
-
|
|
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
|
+
}
|
|
615
623
|
},
|
|
616
624
|
onNotifyParent: (_taskId, message, level) => {
|
|
617
625
|
this.emit("subagent:intercom", { runId: run.id, taskId: task.id, kind: "notify", level, message });
|
|
@@ -793,7 +801,7 @@ class SubagentManager {
|
|
|
793
801
|
runController?.signal.removeEventListener("abort", abortChild);
|
|
794
802
|
};
|
|
795
803
|
// Cancel may have landed during session creation — honor it before prompting.
|
|
796
|
-
if (run.status === "aborted" || TERMINAL.includes(task.status)) {
|
|
804
|
+
if (run.status === "aborted" || TERMINAL.includes(task.status) || signal?.aborted) {
|
|
797
805
|
await child.abort();
|
|
798
806
|
throw new Error("Canceled by subagent_cancel");
|
|
799
807
|
}
|
|
@@ -928,7 +936,7 @@ class SubagentManager {
|
|
|
928
936
|
if (run.notifyPerTask && run.background && TERMINAL.includes(task.status)) {
|
|
929
937
|
this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
930
938
|
}
|
|
931
|
-
if (task.status !== "completed") break;
|
|
939
|
+
if (task.status !== "completed") break; // aborted/failed link stops the chain
|
|
932
940
|
previous = task.finalText ?? "";
|
|
933
941
|
}
|
|
934
942
|
} else {
|
|
@@ -953,8 +961,11 @@ class SubagentManager {
|
|
|
953
961
|
} else {
|
|
954
962
|
this.clearWidget(ctx);
|
|
955
963
|
}
|
|
956
|
-
|
|
957
|
-
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
|
+
}
|
|
958
969
|
this.runControllers.delete(run.id);
|
|
959
970
|
for (const task of run.tasks) this.mailboxes.close(`${run.id}:${task.id}`);
|
|
960
971
|
this.persist(ctx);
|
|
@@ -1138,7 +1149,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1138
1149
|
});
|
|
1139
1150
|
|
|
1140
1151
|
pi.on("agent_start", (_event, ctx) => {
|
|
1141
|
-
if (!manager.turnActivity) manager.clearWidget(ctx);
|
|
1152
|
+
if (!manager.turnActivity && !manager.hasActiveRun()) manager.clearWidget(ctx);
|
|
1142
1153
|
manager.turnActivity = false;
|
|
1143
1154
|
});
|
|
1144
1155
|
|