@arhen/pi-core-subagent 1.3.24 → 1.3.26

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
@@ -50,7 +50,7 @@ flowchart LR
50
50
  - **A bad graph fails before it spawns.** Unknown ids, self-edges and cycles are rejected at call time — never halfway through a run with three children already burning tokens.
51
51
  - **Proof is an exit code, never a self-report.** Tasks are asked for a runnable `Verify:` command; the leader checks `git diff --stat`. Agents auditing their own work score ~0. ([why](#why-9-is-a-verification-command-not-a-self-report))
52
52
  - **No ceremony without edges.** Six independent reviewers stay six independent reviewers — no waves, no gates, no graph vocabulary imposed on flat work.
53
- - **No agent files, no discovery.** The leader defines every subagent inline per callname, system prompt, toolset. Nothing is read from or written to disk.
53
+ - **Agent files respected.** An `agent` name matching `.agents/agents/<name>.md`, `.claude/agents/<name>.md`, or `.pi/agents/<name>.md` loads that file body = system prompt, frontmatter `model`/`tools` apply, `model` is validated against the pi model registry. Inline params override the file. Project dirs win over home (`~/.agents` single source → `~/.claude` → `~/.pi`).
54
54
  - **Two toolsets only.** Read-only (`read, grep, find, ls` — default) or write (`read, grep, find, ls, bash, edit, write` — `write: true`). No per-agent tool config surface.
55
55
  - **In-process** — children are `AgentSession`s in the same runtime. No process spawn, no context bleed.
56
56
  - **Zero parent-context injection.** No catalog, no context hook. 6 slim tools total.
@@ -83,7 +83,7 @@ The dotted arrows are the whole point: a child may burn 200k tokens reading file
83
83
 
84
84
  ## Usage — the leader invents the agents
85
85
 
86
- Define agents inline per call never creates or reads agent files. Model resolution: explicit `provider/model-id` (or bare id) via the pi model registry → agent-file `model` → the parent's current model → settings default.
86
+ Define agents inline per call, or reference a named agent file (see [Agent files](#agent-files)). Model resolution: explicit `model` → agent-file `model` (validated against the pi model registry) → the parent's current model → settings default.
87
87
 
88
88
  ```json
89
89
  {
@@ -116,6 +116,27 @@ Chain — `{previous}` is replaced with the prior agent's output:
116
116
  }
117
117
  ```
118
118
 
119
+ ## Agent files
120
+
121
+ An `agent` name that matches `<name>.md` in an agents directory loads that file — no inline `prompt` needed. The file body becomes the system prompt; `model` and `tools` frontmatter apply.
122
+
123
+ ```md
124
+ ---
125
+ name: api-reviewer
126
+ description: Strict API reviewer — auth, rate limiting, error handling
127
+ model: claude-opus-4-6
128
+ tools: read, grep, find, ls
129
+ ---
130
+ You are a strict API reviewer. Check auth, rate limiting, and error handling. Cite file:line.
131
+ ```
132
+
133
+ **Lookup order** (first match wins):
134
+
135
+ 1. `.agents/agents/` then `.claude/agents/` then `.pi/agents/` in each directory from the task `cwd` up to the filesystem root (nearest ancestor wins).
136
+ 2. Home: `~/.agents/agents/` (single source) → `~/.claude/agents/` → `~/.pi/agents/`.
137
+
138
+ **Precedence** — inline params always win over the file: `prompt` overrides the body, `model` overrides frontmatter `model`, `tools`/`write` override frontmatter `tools`. A file `model` is validated against the pi model registry (unknown model fails the task with a catalog message). Files without frontmatter work too — the whole file is the system prompt.
139
+
119
140
  ## Graph mode — `needs`
120
141
 
121
142
  `parallel` runs everything at once; `chain` runs everything one at a time. Most real work is neither. Give a task an `id` and list the ids it `needs`:
@@ -238,7 +259,7 @@ Background (default) + intercom — the run returns a runId immediately; you sta
238
259
 
239
260
  | Tool | Purpose |
240
261
  |---|---|
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) |
262
+ | `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
263
  | `subagent_status` | live per-task snapshot (non-blocking), including each child's session file path |
243
264
  | `subagent_result` | full output of a run or one task |
244
265
  | `await_subagent` | block until a run finishes (optional `timeoutMs`) |
@@ -248,7 +269,7 @@ Background (default) + intercom — the run returns a runId immediately; you sta
248
269
 
249
270
  ### Per-task fields
250
271
 
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`.
272
+ `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
273
 
253
274
  ### Child talk tools (when `allowIntercom: true`)
254
275
 
@@ -259,13 +280,12 @@ Background (default) + intercom — the run returns a runId immediately; you sta
259
280
  | `send_agent_message` | message to a sibling subagent's mailbox (`to` = its task id, or `"leader"`) |
260
281
  | `poll_agent_messages` | drain this subagent's mailbox |
261
282
 
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.
283
+ > **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
284
 
264
285
  ## Commands
265
286
 
266
287
  - `/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.
288
+ - `/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
289
 
270
290
  ## Peek — `/subagents peek` or `ctrl+shift+a`
271
291
 
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.26",
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",
@@ -0,0 +1,61 @@
1
+ /** Named agent-file resolution (.agents/.claude/.pi agents dirs). */
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
5
+
6
+ export interface AgentFileInfo {
7
+ body: string;
8
+ model?: string;
9
+ tools?: string[];
10
+ }
11
+
12
+ const AGENT_DIRS = [".agents/agents", ".claude/agents", ".pi/agents"] as const;
13
+ /** Agent names become file paths — refuse anything that could traverse. */
14
+ const SAFE_NAME = /^[\w.-]+$/;
15
+
16
+ function readAgentFile(dir: string, name: string): AgentFileInfo | undefined {
17
+ if (!existsSync(dir)) return undefined;
18
+ const path = join(dir, `${name}.md`);
19
+ if (!existsSync(path)) return undefined;
20
+ const { frontmatter, body } = parseFrontmatter(readFileSync(path, "utf8"));
21
+ const tools =
22
+ typeof frontmatter.tools === "string"
23
+ ? frontmatter.tools
24
+ .split(",")
25
+ .map((t) => t.trim())
26
+ .filter(Boolean)
27
+ : Array.isArray(frontmatter.tools)
28
+ ? frontmatter.tools.map(String)
29
+ : undefined;
30
+ return {
31
+ body,
32
+ model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
33
+ tools: tools?.length ? tools : undefined,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Look up `<name>.md` in agent dirs. Order: for each ancestor of `cwd` (nearest
39
+ * first): `.agents/agents` (single source per the ~/.agents spec) → `.claude/agents`
40
+ * → `.pi/agents`; then home: `~/.agents/agents` → `~/.claude/agents` → `~/.pi/agents`.
41
+ * First match wins. `agentDir` is the pi agent dir (`~/.pi/agent`); home is derived from it.
42
+ */
43
+ export function resolveAgentFile(name: string, cwd: string, agentDir: string): AgentFileInfo | undefined {
44
+ if (!SAFE_NAME.test(name)) return undefined;
45
+ let dir = cwd;
46
+ while (true) {
47
+ for (const sub of AGENT_DIRS) {
48
+ const hit = readAgentFile(join(dir, sub), name);
49
+ if (hit) return hit;
50
+ }
51
+ const parent = dirname(dir);
52
+ if (parent === dir) break;
53
+ dir = parent;
54
+ }
55
+ const home = dirname(dirname(agentDir)); // ~/.pi/agent → ~
56
+ for (const sub of AGENT_DIRS) {
57
+ const hit = readAgentFile(join(home, sub), name);
58
+ if (hit) return hit;
59
+ }
60
+ return undefined;
61
+ }
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. An `agent` name matching a file in `.agents/agents`, `.claude/agents`, or `.pi/agents` (project dirs, then home) loads it: body = system prompt, frontmatter `model`/`tools` apply; inline prompt/model/tools/write override the file. 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.",
@@ -160,29 +135,34 @@ export default function (pi: ExtensionAPI) {
160
135
  "Order comes from `needs`, not from separate calls: give tasks an `id`, list the ids each depends on. Tasks with no unmet needs run in parallel; dependents receive their upstream outputs automatically — do not restate them.",
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
- "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.",
138
+ "Define each agent yourself: invented name, focused system prompt, and read-only (default) or write:true. Prefer read-only. An `agent` name that matches an existing file in `.agents/agents`, `.claude/agents`, or `.pi/agents` (project first, then home) loads that agent — body = system prompt, frontmatter `model`/`tools` apply, file `model` is validated against the model registry; inline params override the file. Files without frontmatter work too.",
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
@@ -16,6 +16,7 @@ import {
16
16
  type ToolDefinition,
17
17
  } from "@earendil-works/pi-coding-agent";
18
18
  import type { TUI } from "@earendil-works/pi-tui";
19
+ import { resolveAgentFile } from "./agentfile.ts";
19
20
  import { CHILD_TALK_TOOLS, type ChildHandlers, createChildTools, createWatchdog, type Watchdog } from "./child.ts";
20
21
  import {
21
22
  activitySnippet,
@@ -211,9 +212,6 @@ export class SubagentManager {
211
212
  private widgetRuns: RunSnapshot[] = [];
212
213
  private eventSeq = 0;
213
214
 
214
- /** Default for `background` when the agent doesn't say — toggle via `/subagents auto-bg on|off`. */
215
- private autoBg = true;
216
-
217
215
  /** When false, strip leader-imposed maxRuntimeMs so tasks run unlimited — toggle via `/subagents auto-limit on|off`. */
218
216
  private autoLimit = true;
219
217
 
@@ -222,37 +220,21 @@ export class SubagentManager {
222
220
  constructor(private readonly pi: ExtensionAPI) {
223
221
  try {
224
222
  const cfg = JSON.parse(readFileSync(join(getAgentDir(), "subagents-config.json"), "utf8"));
225
- if (typeof cfg.autoBg === "boolean") this.autoBg = cfg.autoBg;
226
223
  if (typeof cfg.autoLimit === "boolean") this.autoLimit = cfg.autoLimit;
227
224
  } catch {
228
225
  /* no config yet — defaults */
229
226
  }
230
227
  }
231
228
 
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
229
  /** Flip the auto-limit flag; persists to the agent dir. Returns the new value. */
243
230
  setAutoLimit(on: boolean): boolean {
244
231
  this.autoLimit = on;
245
- void writeFile(
246
- join(getAgentDir(), "subagents-config.json"),
247
- JSON.stringify({ autoBg: this.autoBg, autoLimit: on }, null, 2),
248
- ).catch(() => {});
232
+ void writeFile(join(getAgentDir(), "subagents-config.json"), JSON.stringify({ autoLimit: on }, null, 2)).catch(
233
+ () => {},
234
+ );
249
235
  return on;
250
236
  }
251
237
 
252
- get autoBgOn(): boolean {
253
- return this.autoBg;
254
- }
255
-
256
238
  get autoLimitOn(): boolean {
257
239
  return this.autoLimit;
258
240
  }
@@ -392,7 +374,7 @@ export class SubagentManager {
392
374
  if (kind !== "asked" && run.awaited) return; // parent already got the result via await_subagent
393
375
  const body =
394
376
  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: ...).`
377
+ ? `A subagent is asking you a question (task ${extra?.taskId}): ${extra?.question ?? ""}\nReply with reply_subagent(runId: "${run.id}", taskId: "${extra?.taskId}", message: ...).`
396
378
  : makeNotice(run, kind);
397
379
  try {
398
380
  this.pi.sendUserMessage(body, { deliverAs: "followUp" });
@@ -509,13 +491,6 @@ export class SubagentManager {
509
491
  if (this.collectParked(run.id, { kind: "ask", taskId: task.id, agent: task.agent, text: question })) {
510
492
  return "Your question was delivered to the parent (they're waiting on this run). Keep working; the answer arrives via the pending reply.";
511
493
  }
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
494
  this.notifyParent(run, "asked", { taskId: task.id, question });
520
495
  // M3: a waiting child is not stalled — keep the watchdog fed until the reply.
521
496
  const keepAlive = setInterval(() => this.liveChildren.get(`${run.id}:${task.id}`)?.touchWatchdog(), 30_000);
@@ -657,18 +632,19 @@ export class SubagentManager {
657
632
  ): Promise<void> {
658
633
  if (TERMINAL.includes(task.status)) return; // canceled while queued
659
634
 
660
- // Inline params win; otherwise fall back to an existing agent file
661
- // (~/.agents, .pi/agents, user dir). Never creates files.
662
- const prompt = input.prompt?.trim();
635
+ // Named agent file (`.agents/agents` etc.): body = system prompt, frontmatter
636
+ // model/tools fill gaps. Inline params always win over the file.
637
+ const file = resolveAgentFile(input.agent, task.cwd, getAgentDir());
638
+ const prompt = input.prompt?.trim() ?? file?.body;
663
639
  const thinking = input.thinking;
664
- const baseTools = input.tools ?? (input.write ? WRITE_TOOLS : READONLY_TOOLS);
640
+ const baseTools = input.tools ?? (input.write ? WRITE_TOOLS : (file?.tools ?? READONLY_TOOLS));
665
641
  const tools = [...baseTools, ...(run.allowIntercom ? CHILD_TALK_TOOLS : [])];
666
642
 
667
643
  // Model + thinking resolve against the pi model registry; a bad request
668
644
  // fails the TASK with a helpful message, not the whole run.
669
645
  let model: Model<Api> | undefined;
670
646
  try {
671
- model = resolveChildModel(ctx, input.model);
647
+ model = resolveChildModel(ctx, input.model ?? file?.model);
672
648
  validateThinking(model, thinking);
673
649
  } catch (err) {
674
650
  this.updateTask(
@@ -890,7 +866,6 @@ export class SubagentManager {
890
866
  id: newId("run"),
891
867
  mode,
892
868
  status: "queued",
893
- background: params.background ?? this.autoBg,
894
869
  allowIntercom: Boolean(params.allowIntercom),
895
870
  notifyPerTask: params.notifyPerTask ?? true,
896
871
  createdAt: Date.now(),
@@ -965,7 +940,7 @@ export class SubagentManager {
965
940
  onUpdate,
966
941
  );
967
942
  if (task.status === "completed") outputs.set(task.id, task.finalText ?? "");
968
- if (run.notifyPerTask && run.background && TERMINAL.includes(task.status)) {
943
+ if (run.notifyPerTask && TERMINAL.includes(task.status)) {
969
944
  this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
970
945
  }
971
946
  },
@@ -1012,17 +987,7 @@ export class SubagentManager {
1012
987
  this.persist(ctx);
1013
988
  }
1014
989
 
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
-
990
+ /** Spawn a run that keeps executing after this call returns. Every run is background. */
1026
991
  startInBackground(params: SubagentParamsShape, ctx: ExtensionContext): RunDetails {
1027
992
  const { run, inputs } = this.createRun(params, ctx);
1028
993
  void this.executeTasks(run, inputs, ctx, undefined, undefined)
@@ -1050,7 +1015,7 @@ export class SubagentManager {
1050
1015
  this.notifyParent(run, "failed");
1051
1016
  this.persist(ctx);
1052
1017
  });
1053
- return { run: cloneRun(run), background: true };
1018
+ return { run: cloneRun(run) };
1054
1019
  }
1055
1020
 
1056
1021
  /** 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 {