@arhen/pi-core-subagent 1.1.15 → 1.1.16

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.
Files changed (3) hide show
  1. package/README.md +15 -1
  2. package/package.json +1 -1
  3. package/src/index.ts +36 -25
package/README.md CHANGED
@@ -8,6 +8,10 @@ Minimalist pi extension: **fast in-process subagents** with single / parallel /
8
8
 
9
9
  Built for one job: delegate work to isolated subagents **without bloating the parent context**.
10
10
 
11
+ ![Subagents widget](docs/subagents-widget.png)
12
+
13
+ *The `subagent` tool call plus the live above-editor widget: per-agent activity, tool counts, turns, token counters and timers.*
14
+
11
15
  ## Design principles
12
16
 
13
17
  - **No agent files, no discovery.** The leader defines every subagent inline per call — name, system prompt, toolset. Nothing is read from or written to disk.
@@ -15,7 +19,8 @@ Built for one job: delegate work to isolated subagents **without bloating the pa
15
19
  - **In-process** — children are `AgentSession`s in the same runtime. No process spawn, no context bleed.
16
20
  - **Zero parent-context injection.** No catalog, no context hook. 6 slim tools total.
17
21
  - **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 3 minutes; per-task timeout 10min.
22
+ - **No silent hangs** — watchdog aborts children that produce no events for 3 minutes.
23
+ - **No default runtime cap** — tasks run until done, stalled (watchdog), or aborted by the user. `maxRuntimeMs` is opt-in (default 0 = unlimited).
19
24
 
20
25
  ## Install
21
26
 
@@ -95,6 +100,15 @@ Background + intercom:
95
100
  | `send_agent_message` | message to a sibling subagent's mailbox (`to` = its task id, or `"leader"`) |
96
101
  | `poll_agent_messages` | drain this subagent's mailbox |
97
102
 
103
+ ## Peek — `/peek` or `ctrl+shift+s`
104
+
105
+ Read-only pane over the session's subagents:
106
+
107
+ - `↑`/`↓` — move between agents
108
+ - `enter` — live tail of that child's session file (`esc` goes back)
109
+ - `x` then `y` — abort ONE subagent (only mutation; `n`/any other key cancels)
110
+ - `esc` — close
111
+
98
112
  ## Context budget
99
113
 
100
114
  - Parent tools: 6 schemas with short descriptions. **No catalog, no context hook** — nothing injected per request.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arhen/pi-core-subagent",
3
- "version": "1.1.15",
3
+ "version": "1.1.16",
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/index.ts CHANGED
@@ -26,7 +26,8 @@ import { createPeekPane, type PeekTask } from "./peek.ts";
26
26
  const DEFAULT_CONCURRENCY = 3;
27
27
  const MAX_CONCURRENCY = 8;
28
28
  const MAX_TASKS = 16;
29
- const DEFAULT_RUNTIME_MS = 10 * 60 * 1000;
29
+ /** No default wall-clock cap: a subagent runs until its task is done, it stalls, or the user aborts. */
30
+ const DEFAULT_RUNTIME_MS = 0;
30
31
  const DEFAULT_STALL_MS = 180_000; // 3 min: long model thinking streams emit message_update, not message_end
31
32
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
32
33
  const READONLY_TOOLS = ["read", "grep", "find", "ls"];
@@ -225,6 +226,21 @@ function taskStatsWithUsage(task: TaskSnapshot): string {
225
226
  function taskLine(task: TaskSnapshot): string {
226
227
  return `${statusIcon(task.status)} ${task.agent} · ${taskStatsWithUsage(task)} · ${taskTimer(task)}`;
227
228
  }
229
+ /** Numbers get the theme's number color, same as the footer's token counters. */
230
+ function colorNums(text: string, theme: Theme): string {
231
+ return text.replace(/\d+(?:\.\d+)?/g, (n) => theme.fg("syntaxNumber", n));
232
+ }
233
+ /**
234
+ * Themed one-liner. Finished tasks dim entirely (stats included); live tasks
235
+ * keep the agent name readable with themed numbers.
236
+ */
237
+ function themedTaskLine(task: TaskSnapshot, theme: Theme, activity = ""): string {
238
+ const tail = `${taskStatsWithUsage(task)} · ${taskTimer(task)}`;
239
+ if (TERMINAL.includes(task.status)) {
240
+ return theme.fg("dim", `${statusIcon(task.status)} ${task.agent} · ${tail}`);
241
+ }
242
+ return `${statusIcon(task.status)} ${task.agent} · ${activity}${colorNums(theme.fg("muted", tail), theme)}`;
243
+ }
228
244
  function argsSuffix(args: unknown): string {
229
245
  try {
230
246
  const s = JSON.stringify(args);
@@ -280,19 +296,12 @@ class SubagentsWidget implements Component {
280
296
  const budget = WIDGET_MAX_LINES - 1;
281
297
  let shown = 0;
282
298
  outer: for (const run of runs) {
283
- const allDone = TERMINAL.includes(run.status);
284
299
  for (const task of run.tasks) {
285
300
  if (shown >= budget) break outer;
286
301
  shown += 1;
287
- const activity =
288
- !TERMINAL.includes(task.status) && task.lastActivity
289
- ? `${this.theme.fg("dim", `→ ${task.lastActivity}`)} · `
290
- : "";
291
- // Tasks of a finished run: dim everything except the agent name.
292
- const line = allDone
293
- ? `${this.theme.fg("dim", `${statusIcon(task.status)} `)}${task.agent} ${this.theme.fg("dim", `· ${taskStatsWithUsage(task)} · ${taskTimer(task)}`)}`
294
- : `${statusIcon(task.status)} ${task.agent} · ${activity}${taskStatsWithUsage(task)} · ${taskTimer(task)}`;
295
- lines.push(truncateToWidth(`${this.theme.fg("dim", "├─")} ${line}`, width, "…"));
302
+ const activity = task.lastActivity ? `${this.theme.fg("dim", `→ ${task.lastActivity}`)} · ` : "";
303
+ // Per-TASK status drives dimming: a finished agent stays dim even while siblings run.
304
+ lines.push(truncateToWidth(`${this.theme.fg("dim", "├─")} ${themedTaskLine(task, this.theme, activity)}`, width, "…"));
296
305
  }
297
306
  }
298
307
  const hidden = total - shown;
@@ -576,7 +585,6 @@ class SubagentManager {
576
585
  this.ensureWidget(ctx);
577
586
  this.widgetTui?.requestRender();
578
587
  }
579
- onUpdate?.({ content: [{ type: "text", text: compactLines(run).join("\n") }] });
580
588
  }, WIDGET_THROTTLE_MS));
581
589
  }
582
590
  private flushWidget(run: RunSnapshot | undefined, ctx?: ExtensionContext, onUpdate?: (partial: any) => void): void {
@@ -592,7 +600,8 @@ class SubagentManager {
592
600
  this.ensureWidget(ctx);
593
601
  this.widgetTui?.requestRender();
594
602
  }
595
- onUpdate?.({ content: [{ type: "text", text: compactLines(run).join("\n") }] });
603
+ // Transcript gets one status line only the live per-task view is the widget's job.
604
+ onUpdate?.({ content: [{ type: "text", text: `${run.tasks.filter((t) => TERMINAL.includes(t.status)).length}/${run.tasks.length} done · ${run.status}` }] });
596
605
  }
597
606
  private ensureWidget(ctx: ExtensionContext): void {
598
607
  if (this.widgetTui !== null || !ctx.hasUI) return;
@@ -832,10 +841,15 @@ class SubagentManager {
832
841
 
833
842
  const maxRuntimeMs = input.maxRuntimeMs ?? DEFAULT_RUNTIME_MS;
834
843
  const promptPromise = child.prompt(task.task, { source: "extension" });
835
- const timeoutPromise = new Promise<never>((_, reject) => {
836
- timeout = setTimeout(() => reject(new Error(`Subagent timed out after ${maxRuntimeMs}ms`)), maxRuntimeMs);
837
- });
838
- await Promise.race([promptPromise, childFailurePromise, childEndPromise, watchdog.promise, timeoutPromise]);
844
+ const races: Promise<unknown>[] = [promptPromise, childFailurePromise, childEndPromise, watchdog.promise];
845
+ if (maxRuntimeMs > 0) {
846
+ races.push(
847
+ new Promise<never>((_, reject) => {
848
+ timeout = setTimeout(() => reject(new Error(`Subagent timed out after ${maxRuntimeMs}ms`)), maxRuntimeMs);
849
+ }),
850
+ );
851
+ }
852
+ await Promise.race(races);
839
853
  if (timeout) clearTimeout(timeout);
840
854
 
841
855
  pendingFailure ??= lastAssistantFailure(child.messages as AssistantMessage[]);
@@ -977,13 +991,10 @@ class SubagentManager {
977
991
  run.status = aborted ? "aborted" : failed ? "failed" : "completed";
978
992
  run.endedAt = Date.now();
979
993
  this.flushWidget(run, ctx, onUpdate);
980
- // Run done: keep the widget only while another run is still live.
994
+ // Finished runs (including aborted ones) stay on screen so the outcome is readable.
995
+ // The agent_start handler clears them on the next turn that spawns nothing.
981
996
  const live = this.listRuns().find((r) => !TERMINAL.includes(r.status));
982
- if (live) {
983
- this.scheduleWidget(live, ctx, onUpdate);
984
- } else {
985
- this.clearWidget(ctx);
986
- }
997
+ if (live) this.scheduleWidget(live, ctx, onUpdate);
987
998
  // L7: cancelRun already emitted + settled — don't double-report.
988
999
  if (this.settlers.has(run.id)) {
989
1000
  this.emit("subagent:run-completed", { runId: run.id, status: run.status, run: cloneRun(run), aggregateUsage: run.aggregateUsage });
@@ -1149,7 +1160,7 @@ const SubagentParams = Type.Object({
1149
1160
  thinking: Type.Optional(StringEnum(THINKING_LEVELS, { description: "Thinking level override (single mode)" })),
1150
1161
  cwd: Type.Optional(Type.String({ description: "Working directory (single mode). Default: current project." })),
1151
1162
  concurrency: Type.Optional(Type.Number({ description: `Parallel concurrency (default ${DEFAULT_CONCURRENCY}, max ${MAX_CONCURRENCY})` })),
1152
- maxRuntimeMs: Type.Optional(Type.Number({ description: `Per-task timeout, ms (default ${DEFAULT_RUNTIME_MS / 60000} min)` })),
1163
+ maxRuntimeMs: Type.Optional(Type.Number({ description: "Per-task timeout, ms. Omit for no cap (default): tasks run until done, stalled, or user-aborted." })),
1153
1164
  background: Type.Optional(Type.Boolean({ description: "Fire-and-forget: return immediately with a runId; you'll be notified on completion" })),
1154
1165
  notifyPerTask: Type.Optional(Type.Boolean({ description: "Wake you (queued follow-up turn) as each task completes, even mid-run. Default false." })),
1155
1166
  allowIntercom: Type.Optional(Type.Boolean({ description: "Let children ask you questions, notify you, and message sibling subagents" })),
@@ -1294,7 +1305,7 @@ export default function (pi: ExtensionAPI) {
1294
1305
  // ponytail: mode/count already shown on the call line above; result header only adds progress + status.
1295
1306
  const header = `${statusIcon(run.status)} ${theme.fg("accent", `${run.tasks.filter((t) => t.status === "completed").length}/${run.tasks.length} done`)}${run.background ? ` ${theme.fg("muted", "(background)")}` : ""} ${theme.fg("muted", run.status)}`;
1296
1307
  if (!expanded) {
1297
- const lines = [header, ...run.tasks.map((task) => ` ${taskLine(task)}`)];
1308
+ const lines = [header, ...run.tasks.map((task) => ` ${themedTaskLine(task, theme)}`)];
1298
1309
  const usage = formatUsage(run.aggregateUsage);
1299
1310
  if (usage) lines.push(theme.fg("dim", usage));
1300
1311
  return new Text(lines.join("\n"), 0, 0);