@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 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 class per call.
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
 
@@ -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: ModelClass): ResolvedTaskRoute {
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 profile = config.profiles[modelClass];
72
- if (!profile) throw new Error(`No ${modelClass} task model profile is configured. Run /task-models.`);
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 ${modelClass} task model route. Run /task-models.`);
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
- lineBytes += Buffer.byteLength(part, "utf8");
400
- if (lineBytes > MAX_JSON_EVENT_BYTES) {
401
- protocolError = new Error(`Subagent JSON event exceeds ${MAX_JSON_EVENT_BYTES} bytes.`);
402
- killTree(true);
403
- return;
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 balanced profile.",
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 modelClass = params.modelClass ?? "balanced";
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.2",
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.1.0"
50
+ "@henryqw/pi-task-models": "^0.2.0"
51
51
  }
52
52
  }