@henryqw/pi-subagent 2.0.2 → 2.1.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/README.md +3 -3
- package/extensions/subagent.ts +34 -15
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@henryqw/pi-subagent`
|
|
2
2
|
|
|
3
|
-
Delegate one bounded task to one isolated Pi process. Main chooses the role and model
|
|
3
|
+
Delegate one bounded task to one isolated Pi process. Main chooses the role and may override shared task-model effort per call.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -23,9 +23,9 @@ pi install npm:@henryqw/pi-subagent
|
|
|
23
23
|
| --- | --- | --- |
|
|
24
24
|
| `delegate_task` | tool | Start one isolated child for `role`, `task`, and optional `modelClass`. |
|
|
25
25
|
|
|
26
|
-
`modelClass` is `fast`, `balanced`, or `frontier`. Omitted class uses `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.
|
|
26
|
+
`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
27
|
|
|
28
|
-
Each call starts one isolated child (`pi --mode json -p --no-session`). Ambient extensions and skills are off; only role resources load. Child uses the delegated working directory and Main's project approval. Abort kills the child process group. Streaming output is capped at 50 KiB.
|
|
28
|
+
Each call starts one isolated child (`pi --mode json -p --no-session`). Ambient extensions and skills are off; only role resources load. Child uses the delegated working directory and Main's project approval. Abort kills the child process group. 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.
|
|
29
29
|
|
|
30
30
|
TUI shows one row per Subagent with role, route, task, tokens, and elapsed time. Terminal rows drop after one second.
|
|
31
31
|
|
package/extensions/subagent.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
|
|
8
8
|
import { type ExtensionAPI, type ExtensionContext, getAgentDir, parseFrontmatter, type Theme } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { type Component, truncateToWidth, type TUI, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
10
|
import {
|
|
11
|
+
DEFAULT_TASK_ASSIGNMENTS,
|
|
11
12
|
modelReference,
|
|
12
13
|
orderedProfileRoutes,
|
|
13
14
|
PROFILE_NAMES,
|
|
@@ -19,10 +20,14 @@ import {
|
|
|
19
20
|
import { Type } from "typebox";
|
|
20
21
|
|
|
21
22
|
const MODEL_CLASSES = PROFILE_NAMES;
|
|
23
|
+
const SUBAGENT_TASK = "pi-subagent/delegateTask";
|
|
24
|
+
const DEFAULT_MODEL_CLASS = DEFAULT_TASK_ASSIGNMENTS[SUBAGENT_TASK];
|
|
22
25
|
const CODEX_ALIAS = /^openai-codex-(?:[2-9]|[1-9]\d+)$/;
|
|
23
26
|
const MULTI_CODEX_EXTENSION = fileURLToPath(import.meta.resolve("@henryqw/pi-multi-codex/extensions/multi-codex.ts"));
|
|
24
27
|
const MAX_OUTPUT_BYTES = 50 * 1024;
|
|
25
28
|
const MAX_JSON_EVENT_BYTES = 1024 * 1024;
|
|
29
|
+
const CONSUMED_JSON_EVENTS = new Set(["message_start", "message_update", "message_end"]);
|
|
30
|
+
const JSON_EVENT_TYPE = /^\s*\{\s*"type"\s*:\s*"([^"\\]+)"/;
|
|
26
31
|
const WIDGET_KEY = "subagent-status";
|
|
27
32
|
const WIDGET_INTERVAL_MS = 80;
|
|
28
33
|
const TERMINAL_DISPLAY_MS = 1_000;
|
|
@@ -60,7 +65,7 @@ const isModelClass = (value: unknown): value is ModelClass =>
|
|
|
60
65
|
typeof value === "string" && MODEL_CLASSES.includes(value as ModelClass);
|
|
61
66
|
const isNumberedCodexProvider = (provider: string): boolean => CODEX_ALIAS.test(provider);
|
|
62
67
|
|
|
63
|
-
function resolveTaskRoute(ctx: ExtensionContext, modelClass
|
|
68
|
+
function resolveTaskRoute(ctx: ExtensionContext, modelClass?: ModelClass): ResolvedTaskRoute {
|
|
64
69
|
let config;
|
|
65
70
|
try {
|
|
66
71
|
config = readTaskModelsConfig();
|
|
@@ -68,13 +73,14 @@ function resolveTaskRoute(ctx: ExtensionContext, modelClass: ModelClass): Resolv
|
|
|
68
73
|
throw new Error("Couldn't read task model config. Run /task-models.");
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
const
|
|
72
|
-
|
|
76
|
+
const profileName = modelClass ?? config.tasks[SUBAGENT_TASK] ?? DEFAULT_MODEL_CLASS;
|
|
77
|
+
const profile = config.profiles[profileName];
|
|
78
|
+
if (!profile) throw new Error(`No ${profileName} task model profile is configured. Run /task-models.`);
|
|
73
79
|
for (const route of orderedProfileRoutes(profile)) {
|
|
74
80
|
const resolved = resolveTaskModelRoute(ctx, route);
|
|
75
81
|
if (resolved) return resolved;
|
|
76
82
|
}
|
|
77
|
-
throw new Error(`No usable ${
|
|
83
|
+
throw new Error(`No usable ${profileName} task model route. Run /task-models.`);
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
const cleanText = (value: unknown, field: string, file: string): string => {
|
|
@@ -306,6 +312,8 @@ async function runPi(
|
|
|
306
312
|
child.stderr.setEncoding("utf8");
|
|
307
313
|
let lineParts: string[] = [];
|
|
308
314
|
let lineBytes = 0;
|
|
315
|
+
let linePrefix = "";
|
|
316
|
+
let ignoreLine = false;
|
|
309
317
|
let output = "";
|
|
310
318
|
const stderr = { prefix: "", totalBytes: 0 };
|
|
311
319
|
const partial = { prefix: "", totalBytes: 0 };
|
|
@@ -396,17 +404,29 @@ async function runPi(
|
|
|
396
404
|
const newline = data.indexOf("\n", offset);
|
|
397
405
|
const end = newline === -1 ? data.length : newline;
|
|
398
406
|
const part = data.slice(offset, end);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
407
|
+
if (!ignoreLine) {
|
|
408
|
+
linePrefix += part.slice(0, Math.max(0, 256 - linePrefix.length));
|
|
409
|
+
const eventType = JSON_EVENT_TYPE.exec(linePrefix)?.[1];
|
|
410
|
+
if (eventType && !CONSUMED_JSON_EVENTS.has(eventType)) {
|
|
411
|
+
ignoreLine = true;
|
|
412
|
+
lineParts = [];
|
|
413
|
+
lineBytes = 0;
|
|
414
|
+
} else {
|
|
415
|
+
lineBytes += Buffer.byteLength(part, "utf8");
|
|
416
|
+
if (lineBytes > MAX_JSON_EVENT_BYTES) {
|
|
417
|
+
protocolError = new Error(`Subagent JSON event exceeds ${MAX_JSON_EVENT_BYTES} bytes.`);
|
|
418
|
+
killTree(true);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (part) lineParts.push(part);
|
|
422
|
+
}
|
|
404
423
|
}
|
|
405
|
-
if (part) lineParts.push(part);
|
|
406
424
|
if (newline === -1) return;
|
|
407
|
-
processLine(lineParts.join(""));
|
|
425
|
+
if (!ignoreLine) processLine(lineParts.join(""));
|
|
408
426
|
lineParts = [];
|
|
409
427
|
lineBytes = 0;
|
|
428
|
+
linePrefix = "";
|
|
429
|
+
ignoreLine = false;
|
|
410
430
|
offset = newline + 1;
|
|
411
431
|
}
|
|
412
432
|
});
|
|
@@ -438,7 +458,7 @@ const Parameters = Type.Object({
|
|
|
438
458
|
role: Type.String({ description: "Configured Subagent role name" }),
|
|
439
459
|
task: Type.String({ description: "One bounded task with needed context and expected result" }),
|
|
440
460
|
modelClass: Type.Optional(StringEnum(MODEL_CLASSES, {
|
|
441
|
-
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
|
|
461
|
+
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.",
|
|
442
462
|
})),
|
|
443
463
|
});
|
|
444
464
|
|
|
@@ -557,7 +577,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
557
577
|
pi.registerTool({
|
|
558
578
|
name: "delegate_task",
|
|
559
579
|
label: "Subagent",
|
|
560
|
-
description: `Delegate one bounded 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. Request concise conclusions and file/line references; split broad scouting work.`,
|
|
580
|
+
description: `Delegate one bounded 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. Request concise conclusions and file/line references; split broad scouting work.`,
|
|
561
581
|
parameters: Parameters,
|
|
562
582
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
563
583
|
const task = cleanText(params.task, "task", "delegate_task");
|
|
@@ -570,8 +590,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
570
590
|
if (params.modelClass !== undefined && !isModelClass(params.modelClass)) {
|
|
571
591
|
throw new Error("delegate_task modelClass must be fast, balanced, or frontier.");
|
|
572
592
|
}
|
|
573
|
-
const
|
|
574
|
-
const resolvedRoute = resolveTaskRoute(ctx, modelClass);
|
|
593
|
+
const resolvedRoute = resolveTaskRoute(ctx, params.modelClass);
|
|
575
594
|
const model = resolvedRoute.model;
|
|
576
595
|
const modelReferenceValue = modelReference(model);
|
|
577
596
|
const thinkingLevel = resolvedRoute.thinkingLevel;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-subagent",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Delegate one task to an isolated Pi role with explicit extensions and skills.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -47,6 +47,6 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@henryqw/pi-multi-codex": "^0.3.8",
|
|
50
|
-
"@henryqw/pi-task-models": "^0.
|
|
50
|
+
"@henryqw/pi-task-models": "^0.2.0"
|
|
51
51
|
}
|
|
52
52
|
}
|