@arhen/pi-core-subagent 1.1.8 → 1.1.10
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 +60 -27
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.10",
|
|
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
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { AgentSessionEvent, ExtensionAPI, ExtensionContext, Theme, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { createAgentSession, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
17
17
|
import { StringEnum, type Api, type AssistantMessage, type Model } from "@earendil-works/pi-ai";
|
|
18
18
|
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
19
19
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
20
20
|
import { Type } from "typebox";
|
|
21
|
+
import { join } from "node:path";
|
|
21
22
|
import { CHILD_TALK_TOOLS, createChildTools, createWatchdog, type ChildHandlers } from "./child.ts";
|
|
22
23
|
import { createMailbox, type Mailbox } from "./mailbox.ts";
|
|
23
24
|
|
|
@@ -307,7 +308,8 @@ function makeSummary(run: RunSnapshot): string {
|
|
|
307
308
|
const succeeded = run.tasks.filter((t) => t.status === "completed").length;
|
|
308
309
|
const failed = run.tasks.filter((t) => t.status === "failed").length;
|
|
309
310
|
const aborted = run.tasks.filter((t) => t.status === "aborted").length;
|
|
310
|
-
const
|
|
311
|
+
const done = TERMINAL.includes(run.status) ? "finished" : "running";
|
|
312
|
+
const lines = [`Run ${run.id}: Subagents ${run.mode}${run.background ? " (background)" : ""} ${done}: ${succeeded}/${run.tasks.length} succeeded${failed ? `, ${failed} failed` : ""}${aborted ? `, ${aborted} aborted` : ""}.`];
|
|
311
313
|
const usage = formatUsage(run.aggregateUsage);
|
|
312
314
|
if (usage) lines.push(`Usage: ${usage}`);
|
|
313
315
|
for (const task of run.tasks) {
|
|
@@ -340,20 +342,40 @@ function cloneRun(run: RunSnapshot): RunSnapshot {
|
|
|
340
342
|
* Order: explicit "provider/model-id" or bare id (searched across available
|
|
341
343
|
* models) → agent file model → parent's current model (ctx.model) → undefined
|
|
342
344
|
* (createAgentSession falls back to settings). */
|
|
343
|
-
function resolveChildModel(ctx: ExtensionContext, explicit: string | undefined) {
|
|
344
|
-
if (explicit?.trim())
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
345
|
+
export function resolveChildModel(ctx: ExtensionContext, explicit: string | undefined) {
|
|
346
|
+
if (!explicit?.trim()) return ctx.model; // inherit the parent's active model
|
|
347
|
+
const ref = explicit.trim();
|
|
348
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
349
|
+
// Model ids can contain slashes (e.g. 9router/cc/claude-opus-5), so a bare id
|
|
350
|
+
// match and every provider/id split point must be tried, not just the first.
|
|
351
|
+
const byId = available.find((m) => m.id === ref);
|
|
352
|
+
if (byId) return byId;
|
|
353
|
+
for (let slash = ref.indexOf("/"); slash > 0; slash = ref.indexOf("/", slash + 1)) {
|
|
354
|
+
const model = ctx.modelRegistry.find(ref.slice(0, slash), ref.slice(slash + 1));
|
|
355
|
+
if (model) return model;
|
|
356
|
+
}
|
|
357
|
+
throw new Error(`Model not found: ${ref}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Extension-registered providers (e.g. 9router) live only in the parent's
|
|
361
|
+
* in-memory runtime. A child builds its runtime from disk and would lose them,
|
|
362
|
+
* so replay the parent's registrations before the child resolves auth. */
|
|
363
|
+
async function createChildModelRuntime(ctx: ExtensionContext) {
|
|
364
|
+
const ids = ctx.modelRegistry.getRegisteredProviderIds?.() ?? [];
|
|
365
|
+
if (ids.length === 0) return undefined; // no extension providers: disk runtime is enough
|
|
366
|
+
const agentDir = getAgentDir();
|
|
367
|
+
const runtime = await ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json") });
|
|
368
|
+
for (const id of ids) {
|
|
369
|
+
const native = ctx.modelRegistry.getRegisteredNativeProvider?.(id);
|
|
370
|
+
if (native) {
|
|
371
|
+
runtime.registerNativeProvider(native);
|
|
372
|
+
continue;
|
|
351
373
|
}
|
|
352
|
-
const
|
|
353
|
-
if (
|
|
354
|
-
return byId;
|
|
374
|
+
const config = ctx.modelRegistry.getRegisteredProviderConfig?.(id);
|
|
375
|
+
if (config) runtime.registerProvider(id, config);
|
|
355
376
|
}
|
|
356
|
-
|
|
377
|
+
await runtime.refresh({ allowNetwork: false });
|
|
378
|
+
return runtime;
|
|
357
379
|
}
|
|
358
380
|
|
|
359
381
|
/** Validate a thinking level against the RESOLVED model's registry entry.
|
|
@@ -495,7 +517,9 @@ class SubagentManager {
|
|
|
495
517
|
const parentFile = getParentSessionFile(ctx);
|
|
496
518
|
if (!parentFile) return;
|
|
497
519
|
const sidecar = parentFile.replace(/\.jsonl$/, ".subagents.json");
|
|
498
|
-
import("fs")
|
|
520
|
+
import("fs")
|
|
521
|
+
.then(({ writeFileSync }) => writeFileSync(sidecar, JSON.stringify(this.listRuns().slice(0, 50).map(cloneRun), null, 2)))
|
|
522
|
+
.catch(() => {}); // never surface as an unhandled rejection
|
|
499
523
|
} catch {
|
|
500
524
|
/* ignore */
|
|
501
525
|
}
|
|
@@ -533,8 +557,7 @@ class SubagentManager {
|
|
|
533
557
|
}
|
|
534
558
|
|
|
535
559
|
// Widget: register-once + requestRender (todo-overlay pattern).
|
|
536
|
-
//
|
|
537
|
-
// scheduleWidget just throttles status changes into requestRender calls.
|
|
560
|
+
// scheduleWidget throttles status changes into requestRender calls.
|
|
538
561
|
private widgetTui: TUI | null = null;
|
|
539
562
|
/** Upsert a run into the widget's visible set (all runs, not just the latest). */
|
|
540
563
|
private upsertWidgetRun(run: RunSnapshot | undefined): void {
|
|
@@ -600,7 +623,6 @@ class SubagentManager {
|
|
|
600
623
|
onAskParent: async (_taskId, question) => {
|
|
601
624
|
this.updateTask(run, task, { status: "awaiting_parent" }, ctx);
|
|
602
625
|
this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
|
|
603
|
-
this.notifyParent(run, "asked", { taskId: task.id, question });
|
|
604
626
|
// A blocking run's parent can't reply mid-tool (followUp only fires after the
|
|
605
627
|
// tool returns) — only background runs can truly wait for the answer.
|
|
606
628
|
if (!run.background) {
|
|
@@ -608,10 +630,17 @@ class SubagentManager {
|
|
|
608
630
|
this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
|
|
609
631
|
return "Parent cannot answer while this run is blocking. Continue autonomously with your best judgment.";
|
|
610
632
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
|
|
614
|
-
|
|
633
|
+
this.notifyParent(run, "asked", { taskId: task.id, question });
|
|
634
|
+
// M3: a waiting child is not stalled — keep the watchdog fed until the reply.
|
|
635
|
+
const keepAlive = setInterval(() => this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog(), 30_000);
|
|
636
|
+
try {
|
|
637
|
+
const reply = await this.awaitParentReply(run.id, task.id);
|
|
638
|
+
this.updateTask(run, task, { status: "running" }, ctx);
|
|
639
|
+
this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
|
|
640
|
+
return reply;
|
|
641
|
+
} finally {
|
|
642
|
+
clearInterval(keepAlive);
|
|
643
|
+
}
|
|
615
644
|
},
|
|
616
645
|
onNotifyParent: (_taskId, message, level) => {
|
|
617
646
|
this.emit("subagent:intercom", { runId: run.id, taskId: task.id, kind: "notify", level, message });
|
|
@@ -723,6 +752,7 @@ class SubagentManager {
|
|
|
723
752
|
const created = await createAgentSession({
|
|
724
753
|
cwd: task.cwd,
|
|
725
754
|
agentDir: getAgentDir(),
|
|
755
|
+
modelRuntime: await createChildModelRuntime(ctx),
|
|
726
756
|
resourceLoader: loader,
|
|
727
757
|
sessionManager: SessionManager.create(task.cwd, undefined, { parentSession: getParentSessionFile(ctx) }),
|
|
728
758
|
model,
|
|
@@ -793,7 +823,7 @@ class SubagentManager {
|
|
|
793
823
|
runController?.signal.removeEventListener("abort", abortChild);
|
|
794
824
|
};
|
|
795
825
|
// Cancel may have landed during session creation — honor it before prompting.
|
|
796
|
-
if (run.status === "aborted" || TERMINAL.includes(task.status)) {
|
|
826
|
+
if (run.status === "aborted" || TERMINAL.includes(task.status) || signal?.aborted) {
|
|
797
827
|
await child.abort();
|
|
798
828
|
throw new Error("Canceled by subagent_cancel");
|
|
799
829
|
}
|
|
@@ -928,7 +958,7 @@ class SubagentManager {
|
|
|
928
958
|
if (run.notifyPerTask && run.background && TERMINAL.includes(task.status)) {
|
|
929
959
|
this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
930
960
|
}
|
|
931
|
-
if (task.status !== "completed") break;
|
|
961
|
+
if (task.status !== "completed") break; // aborted/failed link stops the chain
|
|
932
962
|
previous = task.finalText ?? "";
|
|
933
963
|
}
|
|
934
964
|
} else {
|
|
@@ -953,8 +983,11 @@ class SubagentManager {
|
|
|
953
983
|
} else {
|
|
954
984
|
this.clearWidget(ctx);
|
|
955
985
|
}
|
|
956
|
-
|
|
957
|
-
this.
|
|
986
|
+
// L7: cancelRun already emitted + settled — don't double-report.
|
|
987
|
+
if (this.settlers.has(run.id)) {
|
|
988
|
+
this.emit("subagent:run-completed", { runId: run.id, status: run.status, run: cloneRun(run), aggregateUsage: run.aggregateUsage });
|
|
989
|
+
this.settleRun(run.id, run);
|
|
990
|
+
}
|
|
958
991
|
this.runControllers.delete(run.id);
|
|
959
992
|
for (const task of run.tasks) this.mailboxes.close(`${run.id}:${task.id}`);
|
|
960
993
|
this.persist(ctx);
|
|
@@ -1138,7 +1171,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1138
1171
|
});
|
|
1139
1172
|
|
|
1140
1173
|
pi.on("agent_start", (_event, ctx) => {
|
|
1141
|
-
if (!manager.turnActivity) manager.clearWidget(ctx);
|
|
1174
|
+
if (!manager.turnActivity && !manager.hasActiveRun()) manager.clearWidget(ctx);
|
|
1142
1175
|
manager.turnActivity = false;
|
|
1143
1176
|
});
|
|
1144
1177
|
|