@henryqw/pi-subagent 2.3.4 → 2.4.0
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/CONTEXT.md +1 -0
- package/README.md +4 -2
- package/extensions/subagent.ts +61 -6
- package/package.json +1 -1
package/CONTEXT.md
CHANGED
|
@@ -18,6 +18,7 @@ Provide validated user Roles, shared task-model Pi launch policy, generic manage
|
|
|
18
18
|
## Invariants
|
|
19
19
|
|
|
20
20
|
- One Delegated Task creates one ephemeral child process and no saved session. Its soft deadline is 10 minutes; active model/tool execution or activity within the last minute grants one 5-minute grace period before a hard stop.
|
|
21
|
+
- Up to four active ephemeral `delegate_task` children run per Main; excess calls wait FIFO. Queued calls do not start a child or consume child timeout. Managed Herdr workers are unaffected.
|
|
21
22
|
- Ambient child extensions and Skills stay disabled; Role explicitly selects extension sources and named Skills. Pi loads Skills supplied by those extension packages or their resource discovery. Omitted Role tools use Pi's effective `defaultTools`; an explicit list sets base tools while loaded extension tools activate automatically.
|
|
22
23
|
- Role Skill names resolve through Main's effective Pi Skill registry; unavailable names warn and skip without blocking delegation.
|
|
23
24
|
- Main selects Role and may override Model Class per task; omitted class uses shared `pi-subagent/delegateTask` assignment, initially `balanced`. Library callers select Role plus their own shared task ID.
|
package/README.md
CHANGED
|
@@ -21,11 +21,13 @@ pi install npm:@henryqw/pi-subagent
|
|
|
21
21
|
|
|
22
22
|
| Surface | Type | Purpose |
|
|
23
23
|
| --- | --- | --- |
|
|
24
|
-
| `delegate_task` | tool | Start one isolated child for `role`, `task`, and optional `modelClass`. |
|
|
24
|
+
| `delegate_task` | tool | Start one isolated child for `role`, `task`, and optional `model` or `modelClass`. |
|
|
25
|
+
|
|
26
|
+
An explicit `model` (`provider/modelId`) overrides `modelClass` and resolves against the currently available text models; an unknown reference rejects with the list of available models. Thinking level defaults to `medium` when supported, otherwise the highest supported level.
|
|
25
27
|
|
|
26
28
|
`modelClass` is `fast`, `balanced`, or `frontier`. Omitted class uses the shared `pi-subagent/delegateTask` assignment, which defaults to `balanced`. Primary route is resolved against current scoped text models; fallback is tried only before launch. If no route is usable, delegation rejects with `Run /task-models`. A started child is never retried.
|
|
27
29
|
|
|
28
|
-
Main splits broad work into independent bounded tasks and keeps integration and cross-cutting decisions. Each `task` states its objective, exact scope and exclusions, relevant context and constraints, expected deliverable, and validation. Each call uses the least capable `modelClass` that can reliably complete its task. Independent sibling calls can run concurrently; concurrent edit tasks must own non-overlapping files.
|
|
30
|
+
Main splits broad work into independent bounded tasks and keeps integration and cross-cutting decisions. Each `task` states its objective, exact scope and exclusions, relevant context and constraints, expected deliverable, and validation. Each call uses the least capable `modelClass` that can reliably complete its task. Independent sibling calls can run concurrently; concurrent edit tasks must own non-overlapping files. Up to four active ephemeral `delegate_task` children run per Main; excess calls wait FIFO. Queued calls do not start a child or consume child timeout. Managed Herdr workers are unaffected.
|
|
29
31
|
|
|
30
32
|
Each call starts one isolated child (`pi --mode json -p --no-session`). Ambient extensions and Skills are off. Role/caller extensions load; those packages' tools and Skills auto-load, plus any extra `skills` names. Child uses the delegated working directory and Main's project approval. Abort kills the child process group. An inactive child times out after 10 minutes; current model/tool execution or activity in the last minute grants one 5-minute grace period, then the child stops. Streaming output is capped at 50 KiB. Unused JSON event types are discarded before payload buffering; consumed or unclassifiable events above 1 MiB fail delegation.
|
|
31
33
|
|
package/extensions/subagent.ts
CHANGED
|
@@ -4,7 +4,15 @@ import { basename } from "node:path";
|
|
|
4
4
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
5
5
|
import { type ExtensionAPI, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { type Component, truncateToWidth, type TUI, visibleWidth } from "@earendil-works/pi-tui";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
availableTaskModels,
|
|
9
|
+
modelReference,
|
|
10
|
+
PROFILE_NAMES,
|
|
11
|
+
type ProfileName,
|
|
12
|
+
resolveAvailableModel,
|
|
13
|
+
type ResolvedTaskRoute,
|
|
14
|
+
taskThinkingLevels,
|
|
15
|
+
} from "@henryqw/pi-task-models";
|
|
8
16
|
import { Type } from "typebox";
|
|
9
17
|
import { createRoleLaunch, isProfileName, loadRoles, resolveRoleLaunch, resolveTaskRoute } from "@henryqw/pi-subagent";
|
|
10
18
|
|
|
@@ -12,6 +20,7 @@ const MODEL_CLASSES = PROFILE_NAMES;
|
|
|
12
20
|
const SUBAGENT_TASK = "pi-subagent/delegateTask";
|
|
13
21
|
const MAX_OUTPUT_BYTES = 50 * 1024;
|
|
14
22
|
const MAX_JSON_EVENT_BYTES = 1024 * 1024;
|
|
23
|
+
const MAX_ACTIVE_CHILDREN = 4;
|
|
15
24
|
const CONSUMED_JSON_EVENTS = new Set(["message_start", "message_update", "message_end"]);
|
|
16
25
|
const JSON_EVENT_TYPE = /^\s*\{\s*"type"\s*:\s*"([^"\\]+)"/;
|
|
17
26
|
const WIDGET_KEY = "subagent-status";
|
|
@@ -414,11 +423,24 @@ const Parameters = Type.Object({
|
|
|
414
423
|
task: Type.String({
|
|
415
424
|
description: "Bounded task packet: objective; exact scope and exclusions; relevant context and constraints; expected deliverable; validation. Never the whole parent request.",
|
|
416
425
|
}),
|
|
426
|
+
model: Type.Optional(Type.String({
|
|
427
|
+
description: "Designated model as provider/modelId; overrides modelClass. Unknown references reject with the list of available models.",
|
|
428
|
+
})),
|
|
417
429
|
modelClass: Type.Optional(StringEnum(MODEL_CLASSES, {
|
|
418
430
|
description: "Classify task complexity: fast for narrow lookups or mechanical edits; balanced for normal bounded work; frontier for ambiguous, cross-cutting, or high-risk reasoning. Defaults to the shared pi-subagent/delegateTask assignment.",
|
|
419
431
|
})),
|
|
420
432
|
});
|
|
421
433
|
|
|
434
|
+
function resolveDesignatedRoute(ctx: ExtensionContext, reference: string): ResolvedTaskRoute {
|
|
435
|
+
const models = availableTaskModels(ctx);
|
|
436
|
+
const model = resolveAvailableModel(models, reference, ctx.model?.provider);
|
|
437
|
+
if (!model) {
|
|
438
|
+
throw new Error(`Unknown delegate_task model: ${reference}. Available models: ${models.map((candidate) => modelReference(candidate)).join(", ") || "none"}.`);
|
|
439
|
+
}
|
|
440
|
+
const levels = taskThinkingLevels(ctx, model);
|
|
441
|
+
return { model, thinkingLevel: levels.includes("medium") ? "medium" : levels.at(-1)! };
|
|
442
|
+
}
|
|
443
|
+
|
|
422
444
|
const roleSummary = (): string => {
|
|
423
445
|
try {
|
|
424
446
|
const roles = loadRoles();
|
|
@@ -433,6 +455,35 @@ export default function subagentExtension(
|
|
|
433
455
|
timeoutPolicy: TimeoutPolicy = DEFAULT_TIMEOUT_POLICY,
|
|
434
456
|
): void {
|
|
435
457
|
const widgetItems = new Map<string, WidgetItem>();
|
|
458
|
+
let activeChildren = 0;
|
|
459
|
+
const queuedChildren: Array<() => void> = [];
|
|
460
|
+
const acquireChildPermit = (signal: AbortSignal | undefined): Promise<void> => {
|
|
461
|
+
if (signal?.aborted) return Promise.reject(new Error("Subagent was aborted."));
|
|
462
|
+
if (activeChildren < MAX_ACTIVE_CHILDREN) {
|
|
463
|
+
activeChildren++;
|
|
464
|
+
return Promise.resolve();
|
|
465
|
+
}
|
|
466
|
+
return new Promise<void>((resolve, reject) => {
|
|
467
|
+
function abort() {
|
|
468
|
+
const index = queuedChildren.indexOf(grant);
|
|
469
|
+
if (index < 0) return;
|
|
470
|
+
queuedChildren.splice(index, 1);
|
|
471
|
+
signal?.removeEventListener("abort", abort);
|
|
472
|
+
reject(new Error("Subagent was aborted."));
|
|
473
|
+
}
|
|
474
|
+
const grant = () => {
|
|
475
|
+
signal?.removeEventListener("abort", abort);
|
|
476
|
+
resolve();
|
|
477
|
+
};
|
|
478
|
+
queuedChildren.push(grant);
|
|
479
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
const releaseChildPermit = () => {
|
|
483
|
+
const grant = queuedChildren.shift();
|
|
484
|
+
if (grant) grant();
|
|
485
|
+
else activeChildren--;
|
|
486
|
+
};
|
|
436
487
|
let widgetInstalled = false;
|
|
437
488
|
let widgetTimer: ReturnType<typeof setInterval> | undefined;
|
|
438
489
|
let spinnerIndex = 0;
|
|
@@ -523,7 +574,7 @@ export default function subagentExtension(
|
|
|
523
574
|
pi.registerTool({
|
|
524
575
|
name: "delegate_task",
|
|
525
576
|
label: "Subagent",
|
|
526
|
-
description: `Delegate one bounded, independently executable task to one isolated Pi Subagent. Roles: ${roleSummary()}. Choose fast for narrow work, balanced for normal work, or frontier for ambiguous and high-risk work; omit modelClass to use shared task-model settings.`,
|
|
577
|
+
description: `Delegate one bounded, independently executable task to one isolated Pi Subagent. Roles: ${roleSummary()}. Choose fast for narrow work, balanced for normal work, or frontier for ambiguous and high-risk work; omit modelClass to use shared task-model settings. When the user designates a specific model, pass it as provider/modelId in model.`,
|
|
527
578
|
promptSnippet: "Delegate one bounded, independently executable task to an isolated role",
|
|
528
579
|
promptGuidelines: [
|
|
529
580
|
"Before calling delegate_task, split broad work into the smallest independent bounded tasks; keep integration and cross-cutting decisions in Main.",
|
|
@@ -543,9 +594,11 @@ export default function subagentExtension(
|
|
|
543
594
|
if (params.modelClass !== undefined && !isModelClass(params.modelClass)) {
|
|
544
595
|
throw new Error("delegate_task modelClass must be fast, balanced, or frontier.");
|
|
545
596
|
}
|
|
546
|
-
const launch = params.
|
|
547
|
-
?
|
|
548
|
-
:
|
|
597
|
+
const launch = params.model !== undefined
|
|
598
|
+
? createRoleLaunch(pi, ctx, { role, route: resolveDesignatedRoute(ctx, cleanText(params.model, "model", "delegate_task")) })
|
|
599
|
+
: params.modelClass === undefined
|
|
600
|
+
? resolveRoleLaunch(pi, ctx, { role, taskId: SUBAGENT_TASK })
|
|
601
|
+
: createRoleLaunch(pi, ctx, { role, route: resolveTaskRoute(ctx, params.modelClass) });
|
|
549
602
|
const modelReferenceValue = modelReference(launch.model);
|
|
550
603
|
const thinkingLevel = launch.thinkingLevel;
|
|
551
604
|
if (launch.missingSkills.length) {
|
|
@@ -555,9 +608,10 @@ export default function subagentExtension(
|
|
|
555
608
|
);
|
|
556
609
|
}
|
|
557
610
|
|
|
611
|
+
const args = ["--mode", "json", "-p", ...launch.args, `Task: ${task}`];
|
|
612
|
+
await acquireChildPermit(signal);
|
|
558
613
|
let widgetStatus: Exclude<WidgetStatus, "working"> = "failure";
|
|
559
614
|
try {
|
|
560
|
-
const args = ["--mode", "json", "-p", ...launch.args, `Task: ${task}`];
|
|
561
615
|
startWidgetItem(toolCallId, role.name, launch.model.id, thinkingLevel, task, ctx);
|
|
562
616
|
const details = { role: role.name, model: modelReferenceValue, thinkingLevel };
|
|
563
617
|
const result = await runPi(
|
|
@@ -578,6 +632,7 @@ export default function subagentExtension(
|
|
|
578
632
|
if (signal?.aborted && !(error instanceof SubagentTimeoutError)) widgetStatus = "aborted";
|
|
579
633
|
throw error;
|
|
580
634
|
} finally {
|
|
635
|
+
releaseChildPermit();
|
|
581
636
|
finishWidgetItem(toolCallId, widgetStatus);
|
|
582
637
|
}
|
|
583
638
|
},
|