pi-subagents 0.10.0 → 0.11.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/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.11.0] - 2026-02-23
6
+
7
+ ### Added
8
+ - **Background mode toggle in clarify TUI**: Press `b` to toggle background/async execution for any mode (single, parallel, chain). Shows `[b]g:ON` in footer when enabled. Previously async execution required programmatic `clarify: false, async: true` — now users can interactively choose background mode after previewing/editing parameters.
9
+ - **`--bg` flag for slash commands**: `/run scout "task" --bg`, `/chain scout "task" -> planner --bg`, `/parallel scout "a" -> scout "b" --bg` now run in background without needing the TUI.
10
+
11
+ ### Fixed
12
+ - Task edits in clarify TUI were lost when launching in background mode if no other behavior (model, output, reads) was modified. The async handoff now always applies the edited template.
13
+
5
14
  ## [0.10.0] - 2026-02-23
6
15
 
7
16
  ### Added
package/README.md CHANGED
@@ -162,6 +162,18 @@ Append `[key=value,...]` to any agent name to override its defaults:
162
162
 
163
163
  Set `output=false`, `reads=false`, or `skills=false` to explicitly disable.
164
164
 
165
+ ### Background Execution
166
+
167
+ Add `--bg` at the end of any slash command to run in the background:
168
+
169
+ ```
170
+ /run scout "full security audit of the codebase" --bg
171
+ /chain scout "analyze auth system" -> planner "design refactor plan" -> worker --bg
172
+ /parallel scout "scan frontend" -> scout "scan backend" -> scout "scan infra" --bg
173
+ ```
174
+
175
+ Background tasks run asynchronously and notify you when complete. Check status with `subagent_status`.
176
+
165
177
  ## Agents Manager
166
178
 
167
179
  Press **Ctrl+Shift+A** or type `/agents` to open the Agents Manager overlay — a TUI for browsing, viewing, editing, creating, and launching agents and chains.
@@ -271,9 +283,9 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
271
283
  |------|---------------|-------|
272
284
  | Single | Yes | `{ agent, task }` - agents with `output` write to temp dir |
273
285
  | Chain | Yes | `{ chain: [{agent, task}...] }` with `{task}`, `{previous}`, `{chain_dir}` variables |
274
- | Parallel | Sync only | `{ tasks: [{agent, task}...] }` - auto-downgrades if async requested |
286
+ | Parallel | Yes | `{ tasks: [{agent, task}...] }` - via TUI toggle or converted to chain for async |
275
287
 
276
- Chain defaults to sync with TUI clarification. Use `clarify: false` to enable async. Chains with parallel steps (`{ parallel: [...] }`) are fully supported in async mode — parallel tasks run concurrently with configurable `concurrency` and `failFast` options.
288
+ All modes support background/async execution. For programmatic async, use `clarify: false, async: true`. For interactive async, use `clarify: true` and press `b` in the TUI to toggle background mode before running. Chains with parallel steps (`{ parallel: [...] }`) run concurrently with configurable `concurrency` and `failFast` options.
277
289
 
278
290
  **Clarify TUI for single/parallel:**
279
291
 
@@ -290,13 +302,14 @@ Single and parallel modes also support the clarify TUI for previewing/editing pa
290
302
  **Clarification TUI keybindings:**
291
303
 
292
304
  *Navigation mode:*
293
- - `Enter` - Run
305
+ - `Enter` - Run (foreground) or launch in background if `b` is toggled on
294
306
  - `Esc` - Cancel
295
307
  - `↑↓` - Navigate between steps/tasks (parallel, chain)
296
308
  - `e` - Edit task/template (all modes)
297
309
  - `m` - Select model (all modes)
298
310
  - `t` - Select thinking level (all modes)
299
311
  - `s` - Select skills (all modes)
312
+ - `b` - Toggle background/async execution (all modes) — shows `[b]g:ON` when enabled
300
313
  - `w` - Edit writes/output file (single, chain only)
301
314
  - `r` - Edit reads list (chain only)
302
315
  - `p` - Toggle progress tracking (chain only)
package/chain-clarify.ts CHANGED
@@ -42,6 +42,8 @@ export interface ChainClarifyResult {
42
42
  templates: string[];
43
43
  /** User-modified behavior overrides per step (undefined = no changes) */
44
44
  behaviorOverrides: (BehaviorOverride | undefined)[];
45
+ /** User requested background/async execution */
46
+ runInBackground?: boolean;
45
47
  }
46
48
 
47
49
  type EditMode = "template" | "output" | "reads" | "model" | "thinking" | "skills";
@@ -88,6 +90,8 @@ export class ChainClarifyComponent implements Component {
88
90
  private saveMessageTimer: ReturnType<typeof setTimeout> | null = null;
89
91
  private saveChainNameState: TextEditorState = createEditorState();
90
92
  private savingChain = false;
93
+ /** Run in background (async) mode */
94
+ private runInBackground = false;
91
95
 
92
96
  constructor(
93
97
  private tui: TUI,
@@ -463,7 +467,7 @@ export class ChainClarifyComponent implements Component {
463
467
  for (let i = 0; i < this.agentConfigs.length; i++) {
464
468
  overrides.push(this.behaviorOverrides.get(i));
465
469
  }
466
- this.done({ confirmed: true, templates: this.templates, behaviorOverrides: overrides });
470
+ this.done({ confirmed: true, templates: this.templates, behaviorOverrides: overrides, runInBackground: this.runInBackground });
467
471
  return;
468
472
  }
469
473
 
@@ -539,6 +543,13 @@ export class ChainClarifyComponent implements Component {
539
543
  return;
540
544
  }
541
545
 
546
+ // 'b' to toggle background/async execution (all modes)
547
+ if (data === "b") {
548
+ this.runInBackground = !this.runInBackground;
549
+ this.tui.requestRender();
550
+ return;
551
+ }
552
+
542
553
  if (data === "S") {
543
554
  this.saveOverridesToAgent();
544
555
  return;
@@ -1179,13 +1190,14 @@ export class ChainClarifyComponent implements Component {
1179
1190
 
1180
1191
  /** Get footer text based on mode */
1181
1192
  private getFooterText(): string {
1193
+ const bgLabel = this.runInBackground ? '[b]g:ON' : '[b]g';
1182
1194
  switch (this.mode) {
1183
1195
  case 'single':
1184
- return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hink [w]rite [s]kill [S]ave ';
1196
+ return ` [Enter] Run • [Esc] Cancel • e m t w s ${bgLabel} S `;
1185
1197
  case 'parallel':
1186
- return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hink [s]kill [S]ave • ↑↓ Nav ';
1198
+ return ` [Enter] Run • [Esc] Cancel • e m t s ${bgLabel} S • ↑↓ Nav `;
1187
1199
  case 'chain':
1188
- return ' [Enter] Run • [Esc] Cancel • e m t w r p s S W • ↑↓ Nav ';
1200
+ return ` [Enter] Run • [Esc] Cancel • e m t w r p s ${bgLabel} S W • ↑↓ Nav `;
1189
1201
  }
1190
1202
  }
1191
1203
 
@@ -83,6 +83,11 @@ export interface ChainExecutionResult {
83
83
  content: Array<{ type: "text"; text: string }>;
84
84
  details: Details;
85
85
  isError?: boolean;
86
+ /** User requested async execution via TUI - caller should dispatch to executeAsyncChain */
87
+ requestedAsync?: {
88
+ chain: ChainStep[];
89
+ chainSkills: string[];
90
+ };
86
91
  }
87
92
 
88
93
  /**
@@ -210,6 +215,31 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
210
215
  details: { mode: "chain", results: [] },
211
216
  };
212
217
  }
218
+
219
+ // User requested background execution - return early so caller can dispatch to async
220
+ if (result.runInBackground) {
221
+ removeChainDir(chainDir); // Will be recreated by async runner
222
+ // Apply TUI edits (templates + behavior overrides) to chain steps
223
+ const updatedChain = chainSteps.map((step, i) => {
224
+ if (isParallelStep(step)) return step; // Parallel steps unchanged (TUI skipped for parallel chains)
225
+ const override = result.behaviorOverrides[i];
226
+ return {
227
+ ...step,
228
+ task: result.templates[i] as string, // Always use edited template
229
+ ...(override?.model ? { model: override.model } : {}),
230
+ ...(override?.output !== undefined ? { output: override.output } : {}),
231
+ ...(override?.reads !== undefined ? { reads: override.reads } : {}),
232
+ ...(override?.progress !== undefined ? { progress: override.progress } : {}),
233
+ ...(override?.skills !== undefined ? { skill: override.skills } : {}),
234
+ };
235
+ });
236
+ return {
237
+ content: [{ type: "text", text: "Launching in background..." }],
238
+ details: { mode: "chain", results: [] },
239
+ requestedAsync: { chain: updatedChain as ChainStep[], chainSkills },
240
+ };
241
+ }
242
+
213
243
  // Update templates from TUI result
214
244
  templates = result.templates;
215
245
  // Store behavior overrides from TUI (used below in sequential step execution)
package/index.ts CHANGED
@@ -451,7 +451,7 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
451
451
  const normalized = normalizeSkillInput(params.skill);
452
452
  const chainSkills = normalized === false ? [] : (normalized ?? []);
453
453
  // Use extracted chain execution module
454
- return executeChain({
454
+ const chainResult = await executeChain({
455
455
  chain: params.chain as ChainStep[],
456
456
  task: params.task,
457
457
  agents,
@@ -469,6 +469,33 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
469
469
  chainSkills,
470
470
  chainDir: params.chainDir,
471
471
  });
472
+
473
+ // User requested async via TUI - dispatch to async executor
474
+ if (chainResult.requestedAsync) {
475
+ if (!isAsyncAvailable()) {
476
+ return {
477
+ content: [{ type: "text", text: "Background mode requires jiti for TypeScript execution but it could not be found." }],
478
+ isError: true,
479
+ details: { mode: "chain" as const, results: [] },
480
+ };
481
+ }
482
+ const id = randomUUID();
483
+ const asyncCtx = { pi, cwd: ctx.cwd, currentSessionId: currentSessionId! };
484
+ return executeAsyncChain(id, {
485
+ chain: chainResult.requestedAsync.chain,
486
+ agents,
487
+ ctx: asyncCtx,
488
+ cwd: params.cwd,
489
+ maxOutput: params.maxOutput,
490
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
491
+ artifactConfig,
492
+ shareEnabled,
493
+ sessionRoot,
494
+ chainSkills: chainResult.requestedAsync.chainSkills,
495
+ });
496
+ }
497
+
498
+ return chainResult;
472
499
  }
473
500
 
474
501
  if (hasTasks && params.tasks) {
@@ -545,6 +572,39 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
545
572
  if (override?.model) modelOverrides[i] = override.model;
546
573
  if (override?.skills !== undefined) skillOverrides[i] = override.skills;
547
574
  }
575
+
576
+ // User requested background execution
577
+ if (result.runInBackground) {
578
+ if (!isAsyncAvailable()) {
579
+ return {
580
+ content: [{ type: "text", text: "Background mode requires jiti for TypeScript execution but it could not be found." }],
581
+ isError: true,
582
+ details: { mode: "parallel" as const, results: [] },
583
+ };
584
+ }
585
+ const id = randomUUID();
586
+ const asyncCtx = { pi, cwd: ctx.cwd, currentSessionId: currentSessionId! };
587
+ // Convert parallel tasks to a chain with a single parallel step
588
+ const parallelTasks = params.tasks!.map((t, i) => ({
589
+ agent: t.agent,
590
+ task: tasks[i],
591
+ cwd: t.cwd,
592
+ ...(modelOverrides[i] ? { model: modelOverrides[i] } : {}),
593
+ ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
594
+ }));
595
+ return executeAsyncChain(id, {
596
+ chain: [{ parallel: parallelTasks }],
597
+ agents,
598
+ ctx: asyncCtx,
599
+ cwd: params.cwd,
600
+ maxOutput: params.maxOutput,
601
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
602
+ artifactConfig,
603
+ shareEnabled,
604
+ sessionRoot,
605
+ chainSkills: [],
606
+ });
607
+ }
548
608
  }
549
609
 
550
610
  // Execute with overrides (tasks array has same length as params.tasks)
@@ -691,6 +751,33 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
691
751
  if (override?.model) modelOverride = override.model;
692
752
  if (override?.output !== undefined) effectiveOutput = override.output;
693
753
  if (override?.skills !== undefined) skillOverride = override.skills;
754
+
755
+ // User requested background execution
756
+ if (result.runInBackground) {
757
+ if (!isAsyncAvailable()) {
758
+ return {
759
+ content: [{ type: "text", text: "Background mode requires jiti for TypeScript execution but it could not be found." }],
760
+ isError: true,
761
+ details: { mode: "single" as const, results: [] },
762
+ };
763
+ }
764
+ const id = randomUUID();
765
+ const asyncCtx = { pi, cwd: ctx.cwd, currentSessionId: currentSessionId! };
766
+ return executeAsyncSingle(id, {
767
+ agent: params.agent!,
768
+ task,
769
+ agentConfig,
770
+ ctx: asyncCtx,
771
+ cwd: params.cwd,
772
+ maxOutput: params.maxOutput,
773
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
774
+ artifactConfig,
775
+ shareEnabled,
776
+ sessionRoot,
777
+ skills: skillOverride === false ? [] : skillOverride,
778
+ output: effectiveOutput,
779
+ });
780
+ }
694
781
  }
695
782
 
696
783
  const cleanTask = task;
@@ -921,6 +1008,15 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
921
1008
  return { name: token.slice(0, bracket), config: parseInlineConfig(token.slice(bracket + 1, end !== -1 ? end : undefined)) };
922
1009
  };
923
1010
 
1011
+ /** Extract --bg flag from end of args, return cleaned args and whether flag was present */
1012
+ const extractBgFlag = (args: string): { args: string; bg: boolean } => {
1013
+ // Only match --bg at the very end to avoid false positives in quoted strings
1014
+ if (args.endsWith(" --bg") || args === "--bg") {
1015
+ return { args: args.slice(0, args.length - (args === "--bg" ? 4 : 5)).trim(), bg: true };
1016
+ }
1017
+ return { args, bg: false };
1018
+ };
1019
+
924
1020
  const setupDirectRun = (ctx: ExtensionContext) => {
925
1021
  const runId = randomUUID().slice(0, 8);
926
1022
  const sessionRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-session-"));
@@ -980,7 +1076,35 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
980
1076
  ...(i === 0 ? { task: result.task } : {}),
981
1077
  }));
982
1078
  executeChain({ chain, task: result.task, agents, ctx, ...exec, clarify: true })
983
- .then((r) => pi.sendUserMessage(r.content[0]?.text || "(no output)"))
1079
+ .then((r) => {
1080
+ // User requested async via TUI - dispatch to async executor
1081
+ if (r.requestedAsync) {
1082
+ if (!isAsyncAvailable()) {
1083
+ pi.sendUserMessage("Background mode requires jiti for TypeScript execution but it could not be found.");
1084
+ return;
1085
+ }
1086
+ const id = randomUUID();
1087
+ const asyncCtx = { pi, cwd: ctx.cwd, currentSessionId: ctx.sessionManager.getSessionId() ?? id };
1088
+ const sessionRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-session-"));
1089
+ executeAsyncChain(id, {
1090
+ chain: r.requestedAsync.chain,
1091
+ agents,
1092
+ ctx: asyncCtx,
1093
+ maxOutput: undefined,
1094
+ artifactsDir: exec.artifactsDir,
1095
+ artifactConfig: exec.artifactConfig,
1096
+ shareEnabled: false,
1097
+ sessionRoot,
1098
+ chainSkills: r.requestedAsync.chainSkills,
1099
+ }).then((asyncResult) => {
1100
+ pi.sendUserMessage(asyncResult.content[0]?.text || "(launched in background)");
1101
+ }).catch((err) => {
1102
+ pi.sendUserMessage(`Async launch failed: ${err instanceof Error ? err.message : String(err)}`);
1103
+ });
1104
+ return;
1105
+ }
1106
+ pi.sendUserMessage(r.content[0]?.text || "(no output)");
1107
+ })
984
1108
  .catch((err) => pi.sendUserMessage(`Chain failed: ${err instanceof Error ? err.message : String(err)}`));
985
1109
  return;
986
1110
  }
@@ -1017,15 +1141,16 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
1017
1141
  });
1018
1142
 
1019
1143
  pi.registerCommand("run", {
1020
- description: "Run a subagent directly: /run agent[output=file] task",
1144
+ description: "Run a subagent directly: /run agent[output=file] task [--bg]",
1021
1145
  getArgumentCompletions: makeAgentCompletions(false),
1022
1146
  handler: async (args, ctx) => {
1023
- const input = args.trim();
1147
+ const { args: cleanedArgs, bg } = extractBgFlag(args);
1148
+ const input = cleanedArgs.trim();
1024
1149
  const firstSpace = input.indexOf(" ");
1025
- if (firstSpace === -1) { ctx.ui.notify("Usage: /run <agent> <task>", "error"); return; }
1150
+ if (firstSpace === -1) { ctx.ui.notify("Usage: /run <agent> <task> [--bg]", "error"); return; }
1026
1151
  const { name: agentName, config: inline } = parseAgentToken(input.slice(0, firstSpace));
1027
1152
  const task = input.slice(firstSpace + 1).trim();
1028
- if (!task) { ctx.ui.notify("Usage: /run <agent> <task>", "error"); return; }
1153
+ if (!task) { ctx.ui.notify("Usage: /run <agent> <task> [--bg]", "error"); return; }
1029
1154
 
1030
1155
  const agents = discoverAgents(baseCwd, "both").agents;
1031
1156
  if (!agents.find((a) => a.name === agentName)) { ctx.ui.notify(`Unknown agent: ${agentName}`, "error"); return; }
@@ -1038,6 +1163,7 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
1038
1163
  if (inline.output !== undefined) params.output = inline.output;
1039
1164
  if (inline.skill !== undefined) params.skill = inline.skill;
1040
1165
  if (inline.model) params.model = inline.model;
1166
+ if (bg) params.async = true;
1041
1167
  pi.sendUserMessage(`Call the subagent tool with these exact parameters: ${JSON.stringify({ ...params, agentScope: "both" })}`);
1042
1168
  },
1043
1169
  });
@@ -1115,10 +1241,11 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
1115
1241
  };
1116
1242
 
1117
1243
  pi.registerCommand("chain", {
1118
- description: "Run agents in sequence: /chain scout \"scan code\" -> planner \"analyze auth\"",
1244
+ description: "Run agents in sequence: /chain scout \"task\" -> planner [--bg]",
1119
1245
  getArgumentCompletions: makeAgentCompletions(true),
1120
1246
  handler: async (args, ctx) => {
1121
- const parsed = parseAgentArgs(args, "chain", ctx);
1247
+ const { args: cleanedArgs, bg } = extractBgFlag(args);
1248
+ const parsed = parseAgentArgs(cleanedArgs, "chain", ctx);
1122
1249
  if (!parsed) return;
1123
1250
  const chain = parsed.steps.map(({ name, config, task: stepTask }, i) => ({
1124
1251
  agent: name,
@@ -1129,15 +1256,18 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
1129
1256
  ...(config.skill !== undefined ? { skill: config.skill } : {}),
1130
1257
  ...(config.progress !== undefined ? { progress: config.progress } : {}),
1131
1258
  }));
1132
- pi.sendUserMessage(`Call the subagent tool with these exact parameters: ${JSON.stringify({ chain, task: parsed.task, clarify: false, agentScope: "both" })}`);
1259
+ const params: Record<string, unknown> = { chain, task: parsed.task, clarify: false, agentScope: "both" };
1260
+ if (bg) params.async = true;
1261
+ pi.sendUserMessage(`Call the subagent tool with these exact parameters: ${JSON.stringify(params)}`);
1133
1262
  },
1134
1263
  });
1135
1264
 
1136
1265
  pi.registerCommand("parallel", {
1137
- description: "Run agents in parallel: /parallel scout \"scan bugs\" -> reviewer \"check style\"",
1266
+ description: "Run agents in parallel: /parallel scout \"task1\" -> reviewer \"task2\" [--bg]",
1138
1267
  getArgumentCompletions: makeAgentCompletions(true),
1139
1268
  handler: async (args, ctx) => {
1140
- const parsed = parseAgentArgs(args, "parallel", ctx);
1269
+ const { args: cleanedArgs, bg } = extractBgFlag(args);
1270
+ const parsed = parseAgentArgs(cleanedArgs, "parallel", ctx);
1141
1271
  if (!parsed) return;
1142
1272
  if (parsed.steps.length > MAX_PARALLEL) { ctx.ui.notify(`Max ${MAX_PARALLEL} parallel tasks`, "error"); return; }
1143
1273
  const tasks = parsed.steps.map(({ name, config, task: stepTask }) => ({
@@ -1149,7 +1279,9 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
1149
1279
  ...(config.skill !== undefined ? { skill: config.skill } : {}),
1150
1280
  ...(config.progress !== undefined ? { progress: config.progress } : {}),
1151
1281
  }));
1152
- pi.sendUserMessage(`Call the subagent tool with these exact parameters: ${JSON.stringify({ chain: [{ parallel: tasks }], task: parsed.task, clarify: false, agentScope: "both" })}`);
1282
+ const params: Record<string, unknown> = { chain: [{ parallel: tasks }], task: parsed.task, clarify: false, agentScope: "both" };
1283
+ if (bg) params.async = true;
1284
+ pi.sendUserMessage(`Call the subagent tool with these exact parameters: ${JSON.stringify(params)}`);
1153
1285
  },
1154
1286
  });
1155
1287
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",