@arhen/pi-core-subagent 1.3.24 → 1.3.25

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
@@ -238,7 +238,7 @@ Background (default) + intercom — the run returns a runId immediately; you sta
238
238
 
239
239
  | Tool | Purpose |
240
240
  |---|---|
241
- | `subagent` | single / `tasks` (parallel or graph via `needs`) / `chain` (`{previous}`); background is the default (`background:false` for inline result in this turn); `allowIntercom:true` enables child talk tools; `notifyPerTask` (default true) wakes you as each task completes (background runs only) |
241
+ | `subagent` | single / `tasks` (parallel or graph via `needs`) / `chain` (`{previous}`); every run is background returns a runId, completion notifies you; `autoAwait:true` parks the call until the run finishes and returns the final result inline; `allowIntercom:true` enables child talk tools; `notifyPerTask` (default true) wakes you as each task completes |
242
242
  | `subagent_status` | live per-task snapshot (non-blocking), including each child's session file path |
243
243
  | `subagent_result` | full output of a run or one task |
244
244
  | `await_subagent` | block until a run finishes (optional `timeoutMs`) |
@@ -248,7 +248,7 @@ Background (default) + intercom — the run returns a runId immediately; you sta
248
248
 
249
249
  ### Per-task fields
250
250
 
251
- `agent` (name you invent — required), `task` (required), `prompt` (system prompt, optional — minimal default used), `write` (toolset, default read-only), plus optional `model` (`provider/model-id`), `thinking` (validated enum: `off|minimal|low|medium|high|xhigh|max`), `tools` (explicit allowlist), `cwd`, `maxRuntimeMs`, `id`, `needs` (dependency edges — see [Graph mode](#graph-mode--needs)). Top-level only: `background`, `notifyPerTask`, `allowIntercom`, `concurrency`.
251
+ `agent` (name you invent — required), `task` (required), `prompt` (system prompt, optional — minimal default used), `write` (toolset, default read-only), plus optional `model` (`provider/model-id`), `thinking` (validated enum: `off|minimal|low|medium|high|xhigh|max`), `tools` (explicit allowlist), `cwd`, `maxRuntimeMs`, `id`, `needs` (dependency edges — see [Graph mode](#graph-mode--needs)). Top-level only: `autoAwait`, `notifyPerTask`, `allowIntercom`, `concurrency`.
252
252
 
253
253
  ### Child talk tools (when `allowIntercom: true`)
254
254
 
@@ -259,13 +259,12 @@ Background (default) + intercom — the run returns a runId immediately; you sta
259
259
  | `send_agent_message` | message to a sibling subagent's mailbox (`to` = its task id, or `"leader"`) |
260
260
  | `poll_agent_messages` | drain this subagent's mailbox |
261
261
 
262
- > **Intercom anti-deadlock:** children are told to never block indefinitely on intercom replies — `ask_parent` keeps the stall watchdog fed while awaiting a parent reply (background runs), and sibling polls are capped (~5 tries) with a proceed-with-best-judgment fallback. Gated siblings (later waves) may not be running yet — waiting on them is the top stall cause, so children are instructed not to.
262
+ > **Intercom anti-deadlock:** children are told to never block indefinitely on intercom replies — `ask_parent` keeps the stall watchdog fed while awaiting a parent reply, and sibling polls are capped (~5 tries) with a proceed-with-best-judgment fallback. Gated siblings (later waves) may not be running yet — waiting on them is the top stall cause, so children are instructed not to.
263
263
 
264
264
  ## Commands
265
265
 
266
266
  - `/subagents` — list runs; `/subagents peek` (or `ctrl+shift+a`) — browsable pane
267
- - `/subagents auto-bg on|off` — toggle background-by-default for subagent calls (persists to `~/.pi/agent/subagents-config.json`; default on). `off` makes calls block until the run finishes, result inline in the same turn. Bare `/subagents auto-bg` shows the current state.
268
- - `/subagents auto-limit on|off` — toggle leader-imposed `maxRuntimeMs` caps (persists to the same config; default on). `off` strips ALL task timeouts: tasks run unlimited until done, stalled, or aborted — only for runs where a hard bound is genuinely required is a cap kept (none, when off). Bare `/subagents auto-limit` shows the current state.
267
+ - `/subagents auto-limit on|off` — toggle leader-imposed `maxRuntimeMs` caps (persists to `~/.pi/agent/subagents-config.json`; default on). `off` strips ALL task timeouts: tasks run unlimited until done, stalled, or aborted only for runs where a hard bound is genuinely required is a cap kept (none, when off). Bare `/subagents auto-limit` shows the current state.
269
268
 
270
269
  ## Peek — `/subagents peek` or `ctrl+shift+a`
271
270
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arhen/pi-core-subagent",
3
- "version": "1.3.24",
3
+ "version": "1.3.25",
4
4
  "type": "module",
5
5
  "description": "pi extension: fast in-process subagents with a dependency-graph scheduler (needs edges gate tasks and carry upstream output into dependent prompts), plus background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
6
6
  "license": "MIT",
package/src/format.ts CHANGED
@@ -213,7 +213,7 @@ export function makeSummary(run: RunSnapshot): string {
213
213
  const aborted = run.tasks.filter((t) => t.status === "aborted").length;
214
214
  const done = TERMINAL.includes(run.status) ? "finished" : "running";
215
215
  const lines = [
216
- `Run ${run.id}: Subagents ${run.mode}${run.background ? " (background)" : ""} ${done}: ${succeeded}/${run.tasks.length} succeeded${failed ? `, ${failed} failed` : ""}${aborted ? `, ${aborted} aborted` : ""}.`,
216
+ `Run ${run.id}: Subagents ${run.mode} ${done}: ${succeeded}/${run.tasks.length} succeeded${failed ? `, ${failed} failed` : ""}${aborted ? `, ${aborted} aborted` : ""}.`,
217
217
  ];
218
218
  const usage = formatUsage(run.aggregateUsage);
219
219
  if (usage) lines.push(`Usage: ${usage}`);
package/src/index.ts CHANGED
@@ -16,15 +16,7 @@
16
16
 
17
17
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
18
18
  import { Text, truncateToWidth } from "@earendil-works/pi-tui";
19
- import {
20
- compactLines,
21
- formatUsage,
22
- makeSummary,
23
- statusIcon,
24
- taskLine,
25
- themedTaskLine,
26
- truncateText,
27
- } from "./format.ts";
19
+ import { compactLines, formatUsage, makeSummary, statusIcon, taskLine, truncateText } from "./format.ts";
28
20
  import { waveNotation } from "./graph.ts";
29
21
  import { cloneRun, SubagentManager } from "./manager.ts";
30
22
  import { createPeekPane, type PeekTask } from "./peek.ts";
@@ -77,8 +69,7 @@ export default function (pi: ExtensionAPI) {
77
69
  );
78
70
  };
79
71
  pi.registerCommand("subagents", {
80
- description:
81
- "List subagent runs. `/subagents peek` opens the browsable pane; `/subagents auto-bg on|off` toggles background-by-default.",
72
+ description: "List subagent runs. `/subagents peek` opens the browsable pane.",
82
73
  handler: async (args, ctx) => {
83
74
  const arg = String(args ?? "")
84
75
  .trim()
@@ -100,22 +91,6 @@ export default function (pi: ExtensionAPI) {
100
91
  }
101
92
  return;
102
93
  }
103
- if (arg === "auto-bg" || arg.startsWith("auto-bg ")) {
104
- const value = arg.split(/\s+/)[1];
105
- if (value === "on" || value === "off") {
106
- const next = manager.setAutoBg(value === "on");
107
- ctx.ui.notify(
108
- `auto-bg ${next ? "on" : "off"} — subagent calls default to ${next ? "background" : "blocking (inline result)"}.`,
109
- "info",
110
- );
111
- } else {
112
- ctx.ui.notify(
113
- `auto-bg is ${manager.autoBgOn ? "on" : "off"} — use \`/subagents auto-bg on|off\` to change it.`,
114
- "info",
115
- );
116
- }
117
- return;
118
- }
119
94
  const runs = manager.listRuns().slice(0, 10);
120
95
  if (runs.length === 0) {
121
96
  ctx.ui.notify("No subagent runs in this session.", "info");
@@ -152,7 +127,7 @@ export default function (pi: ExtensionAPI) {
152
127
  // ponytail: this string is billed on every request. No example block — an example
153
128
  // biases the model toward one shape; guidelines + JSON schema describe all of them.
154
129
  description:
155
- "Run isolated subagents (own context, own session). You invent each agent: name, optional system prompt, toolset (read-only default, write:true to edit). Use `agent`+`task` for one, `tasks` for many. `needs` declares dependency edges: a task waits for its needs and receives their outputs prepended to its prompt. background is the default (returns a runId immediately; toggle via `/subagents auto-bg off`); set background:false when you need the result inline in this turn. allowIntercom:true lets children talk to you and each other.",
130
+ "Run isolated subagents (own context, own session). You invent each agent: name, optional system prompt, toolset (read-only default, write:true to edit). Use `agent`+`task` for one, `tasks` for many. `needs` declares dependency edges: a task waits for its needs and receives their outputs prepended to its prompt. Every run is background: the call returns a runId immediately and completion notifies you. Set autoAwait:true when you need the result before your next step — the call parks until the run finishes and returns runId + final result in one response. allowIntercom:true lets children talk to you and each other.",
156
131
  promptSnippet: "Define and delegate work to specialized subagents.",
157
132
  promptGuidelines: [
158
133
  "Use subagent when independent review, testing, research, or parallel analysis improves quality.",
@@ -161,28 +136,33 @@ export default function (pi: ExtensionAPI) {
161
136
  "Prefer flat `tasks` (plain parallel) unless a real dependency exists — only add `needs` edges when ordering genuinely matters.",
162
137
  "End each task with a runnable check, e.g. 'Verify: npx tsc --noEmit && bun test'. A subagent's claim of success is not evidence.",
163
138
  "Define each agent yourself: invented name, focused system prompt, and read-only (default) or write:true. Prefer read-only.",
164
- "Prefer blocking (background:false) whenever the run's result is something you must wait for before your next step — do not default to background for work you depend on inline. When a background run is active, settle its pending results and task dependencies (await_subagent / subagent_result, then continue dependent work) before starting unrelated work.",
165
- "For long multi-task runs, don't park the whole turn on one blocking call: start it in the background, then loop await_subagent with short timeoutMs slices (e.g. 20s), processing whichever tasks completed in each slice while the rest keep running. You get incremental results instead of one big blocking wait.",
139
+ "When you need a run's result before your next step, spawn with autoAwait:true the call returns runId + final result in one response. Otherwise spawn background and settle results (await_subagent / subagent_result) before continuing dependent work.",
140
+ "For long multi-task runs, don't autoAwait the whole run: spawn background, then loop await_subagent with short timeoutMs slices (e.g. 20s), processing whichever tasks completed in each slice while the rest keep running. You get incremental results instead of one big wait.",
166
141
  "allowIntercom:true only when a child may need to ask you something.",
167
142
  ],
168
143
  parameters: SubagentParams,
169
144
  executionMode: "parallel", // sibling subagent calls run concurrently, not serialized
170
145
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
171
146
  const typed = params as SubagentParamsShape;
172
- if ((typed.background ?? manager.autoBgOn) && typed.background !== false) {
173
- const details = manager.startInBackground(typed, ctx);
174
- return {
175
- content: [
176
- {
177
- type: "text",
178
- text: `Background run started: ${details.run.id} (${details.run.mode}, ${details.run.tasks.length} task${details.run.tasks.length > 1 ? "s" : ""}).\nUse subagent_status / subagent_result / await_subagent / reply_subagent / subagent_cancel to interact.`,
179
- },
180
- ],
181
- details,
182
- };
147
+ const details = manager.startInBackground(typed, ctx);
148
+ if (typed.autoAwait) {
149
+ // awaitRun wakes on every child→leader message (ask/notify/done) — that's the
150
+ // slice-loop feature. autoAwait wants the final result: re-park until terminal.
151
+ let run = details.run;
152
+ while (!TERMINAL.includes(run.status)) {
153
+ run = (await manager.awaitRun(details.run.id))?.run ?? run;
154
+ }
155
+ return { content: [{ type: "text", text: makeSummary(run) }], details: { run } };
183
156
  }
184
- const details = await manager.runBlocking(typed, signal, onUpdate, ctx);
185
- return { content: [{ type: "text", text: makeSummary(details.run) }], details };
157
+ return {
158
+ content: [
159
+ {
160
+ type: "text",
161
+ text: `Background run started: ${details.run.id} (${details.run.mode}, ${details.run.tasks.length} task${details.run.tasks.length > 1 ? "s" : ""}).\nUse subagent_status / subagent_result / await_subagent / reply_subagent / subagent_cancel to interact.`,
162
+ },
163
+ ],
164
+ details,
165
+ };
186
166
  },
187
167
  renderCall(args, theme) {
188
168
  // ponytail: args stream in partially, so mode is unknowable until JSON closes. Show "preparing…" instead of a wrong "single ?".
@@ -194,9 +174,7 @@ export default function (pi: ExtensionAPI) {
194
174
  : args.agent
195
175
  ? `single ${args.agent}`
196
176
  : "preparing…";
197
- const flags = [(args.background ?? manager.autoBgOn) ? "bg" : "blocking", args.allowIntercom ? "a2a" : ""]
198
- .filter(Boolean)
199
- .join(" · ");
177
+ const flags = [args.autoAwait ? "await" : "bg", args.allowIntercom ? "a2a" : ""].filter(Boolean).join(" · ");
200
178
  // Params used, dimmed: model, thinking, toolset, per-task write count.
201
179
  const tasks = args.tasks ?? args.chain ?? [];
202
180
  const writeCount = tasks.filter((t) => t.write).length;
@@ -237,18 +215,12 @@ export default function (pi: ExtensionAPI) {
237
215
  const run = result.details?.run;
238
216
  if (!run) return new Text(result.content[0]?.type === "text" ? result.content[0].text : "", 0, 0);
239
217
  // ponytail: mode/count already shown on the call line above; result header only adds progress + status.
240
- 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)}`;
218
+ const header = `${statusIcon(run.status)} ${theme.fg("accent", `${run.tasks.filter((t) => t.status === "completed").length}/${run.tasks.length} done`)} ${theme.fg("muted", run.status)}`;
241
219
  if (!expanded) {
242
- // Background: the spawn snapshot is always "0 tools" noise and the footer widget
243
- // already shows live per-task state — keep the card to the header only.
244
- if (run.background) {
245
- const usage = formatUsage(run.aggregateUsage);
246
- return new Text(usage ? `${header}\n${theme.fg("dim", usage)}` : header, 0, 0);
247
- }
248
- const lines = [header, ...run.tasks.map((task) => ` ${themedTaskLine(task, theme)}`)];
220
+ // Every run is background: the spawn snapshot is always "0 tools" noise and the footer
221
+ // widget already shows live per-task state — keep the card to the header only.
249
222
  const usage = formatUsage(run.aggregateUsage);
250
- if (usage) lines.push(theme.fg("dim", usage));
251
- return new Text(lines.join("\n"), 0, 0);
223
+ return new Text(usage ? `${header}\n${theme.fg("dim", usage)}` : header, 0, 0);
252
224
  }
253
225
  const lines = [header];
254
226
  for (const task of run.tasks) {
package/src/manager.ts CHANGED
@@ -211,9 +211,6 @@ export class SubagentManager {
211
211
  private widgetRuns: RunSnapshot[] = [];
212
212
  private eventSeq = 0;
213
213
 
214
- /** Default for `background` when the agent doesn't say — toggle via `/subagents auto-bg on|off`. */
215
- private autoBg = true;
216
-
217
214
  /** When false, strip leader-imposed maxRuntimeMs so tasks run unlimited — toggle via `/subagents auto-limit on|off`. */
218
215
  private autoLimit = true;
219
216
 
@@ -222,37 +219,21 @@ export class SubagentManager {
222
219
  constructor(private readonly pi: ExtensionAPI) {
223
220
  try {
224
221
  const cfg = JSON.parse(readFileSync(join(getAgentDir(), "subagents-config.json"), "utf8"));
225
- if (typeof cfg.autoBg === "boolean") this.autoBg = cfg.autoBg;
226
222
  if (typeof cfg.autoLimit === "boolean") this.autoLimit = cfg.autoLimit;
227
223
  } catch {
228
224
  /* no config yet — defaults */
229
225
  }
230
226
  }
231
227
 
232
- /** Flip the background-by-default flag; persists to the agent dir. Returns the new value. */
233
- setAutoBg(on: boolean): boolean {
234
- this.autoBg = on;
235
- void writeFile(
236
- join(getAgentDir(), "subagents-config.json"),
237
- JSON.stringify({ autoBg: on, autoLimit: this.autoLimit }, null, 2),
238
- ).catch(() => {});
239
- return on;
240
- }
241
-
242
228
  /** Flip the auto-limit flag; persists to the agent dir. Returns the new value. */
243
229
  setAutoLimit(on: boolean): boolean {
244
230
  this.autoLimit = on;
245
- void writeFile(
246
- join(getAgentDir(), "subagents-config.json"),
247
- JSON.stringify({ autoBg: this.autoBg, autoLimit: on }, null, 2),
248
- ).catch(() => {});
231
+ void writeFile(join(getAgentDir(), "subagents-config.json"), JSON.stringify({ autoLimit: on }, null, 2)).catch(
232
+ () => {},
233
+ );
249
234
  return on;
250
235
  }
251
236
 
252
- get autoBgOn(): boolean {
253
- return this.autoBg;
254
- }
255
-
256
237
  get autoLimitOn(): boolean {
257
238
  return this.autoLimit;
258
239
  }
@@ -392,7 +373,7 @@ export class SubagentManager {
392
373
  if (kind !== "asked" && run.awaited) return; // parent already got the result via await_subagent
393
374
  const body =
394
375
  kind === "asked"
395
- ? `A background subagent is asking you a question (task ${extra?.taskId}): ${extra?.question ?? ""}\nReply with reply_subagent(runId: "${run.id}", taskId: "${extra?.taskId}", message: ...).`
376
+ ? `A subagent is asking you a question (task ${extra?.taskId}): ${extra?.question ?? ""}\nReply with reply_subagent(runId: "${run.id}", taskId: "${extra?.taskId}", message: ...).`
396
377
  : makeNotice(run, kind);
397
378
  try {
398
379
  this.pi.sendUserMessage(body, { deliverAs: "followUp" });
@@ -509,13 +490,6 @@ export class SubagentManager {
509
490
  if (this.collectParked(run.id, { kind: "ask", taskId: task.id, agent: task.agent, text: question })) {
510
491
  return "Your question was delivered to the parent (they're waiting on this run). Keep working; the answer arrives via the pending reply.";
511
492
  }
512
- // A blocking run's parent can't reply mid-tool (followUp only fires after the
513
- // tool returns) — only background runs can truly wait for the answer.
514
- if (!run.background) {
515
- this.updateTask(run, task, { status: "running" }, ctx);
516
- this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog();
517
- return "Parent cannot answer while this run is blocking. Continue autonomously with your best judgment.";
518
- }
519
493
  this.notifyParent(run, "asked", { taskId: task.id, question });
520
494
  // M3: a waiting child is not stalled — keep the watchdog fed until the reply.
521
495
  const keepAlive = setInterval(() => this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog(), 30_000);
@@ -890,7 +864,6 @@ export class SubagentManager {
890
864
  id: newId("run"),
891
865
  mode,
892
866
  status: "queued",
893
- background: params.background ?? this.autoBg,
894
867
  allowIntercom: Boolean(params.allowIntercom),
895
868
  notifyPerTask: params.notifyPerTask ?? true,
896
869
  createdAt: Date.now(),
@@ -965,7 +938,7 @@ export class SubagentManager {
965
938
  onUpdate,
966
939
  );
967
940
  if (task.status === "completed") outputs.set(task.id, task.finalText ?? "");
968
- if (run.notifyPerTask && run.background && TERMINAL.includes(task.status)) {
941
+ if (run.notifyPerTask && TERMINAL.includes(task.status)) {
969
942
  this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
970
943
  }
971
944
  },
@@ -1012,17 +985,7 @@ export class SubagentManager {
1012
985
  this.persist(ctx);
1013
986
  }
1014
987
 
1015
- async runBlocking(
1016
- params: SubagentParamsShape,
1017
- signal: AbortSignal | undefined,
1018
- onUpdate: ((partial: any) => void) | undefined,
1019
- ctx: ExtensionContext,
1020
- ): Promise<RunDetails> {
1021
- const { run, inputs } = this.createRun(params, ctx);
1022
- await this.executeTasks(run, inputs, ctx, signal, onUpdate);
1023
- return { run: cloneRun(run) };
1024
- }
1025
-
988
+ /** Spawn a run that keeps executing after this call returns. Every run is background. */
1026
989
  startInBackground(params: SubagentParamsShape, ctx: ExtensionContext): RunDetails {
1027
990
  const { run, inputs } = this.createRun(params, ctx);
1028
991
  void this.executeTasks(run, inputs, ctx, undefined, undefined)
@@ -1050,7 +1013,7 @@ export class SubagentManager {
1050
1013
  this.notifyParent(run, "failed");
1051
1014
  this.persist(ctx);
1052
1015
  });
1053
- return { run: cloneRun(run), background: true };
1016
+ return { run: cloneRun(run) };
1054
1017
  }
1055
1018
 
1056
1019
  /** Push a steering message into a live child's session. Returns false when unknown or not running. */
package/src/schemas.ts CHANGED
@@ -55,17 +55,15 @@ export const SubagentParams = Type.Object({
55
55
  "Per-task timeout, ms. Omit for no cap (default): tasks run until done, stalled, or user-aborted. Do not add arbitrary caps — only set when a hard bound is genuinely required.",
56
56
  }),
57
57
  ),
58
- background: Type.Optional(
58
+ autoAwait: Type.Optional(
59
59
  Type.Boolean({
60
60
  description:
61
- "Fire-and-forget: return immediately with a runId; you'll be notified on completion. Default true set false when you need the result inline in this turn.",
62
- default: true,
61
+ "Start the run in the background, then park this tool call until it finishes and return the final result inline (runId + summary in one response). Default false.",
63
62
  }),
64
63
  ),
65
64
  notifyPerTask: Type.Optional(
66
65
  Type.Boolean({
67
- description:
68
- "Wake you (queued follow-up turn) as each task completes — background runs only, since blocking runs can't be woken mid-tool. Default true.",
66
+ description: "Wake you (queued follow-up turn) as each task completes. Default true.",
69
67
  default: true,
70
68
  }),
71
69
  ),
package/src/types.ts CHANGED
@@ -47,7 +47,6 @@ export interface RunSnapshot {
47
47
  id: string;
48
48
  mode: RunMode;
49
49
  status: RunStatus;
50
- background: boolean;
51
50
  allowIntercom: boolean;
52
51
  notifyPerTask: boolean;
53
52
  createdAt: number;
@@ -62,7 +61,6 @@ export interface RunSnapshot {
62
61
 
63
62
  export interface RunDetails {
64
63
  run: RunSnapshot;
65
- background?: boolean;
66
64
  }
67
65
 
68
66
  export interface PendingReply {