pi-subagents 0.11.3 → 0.11.5

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,39 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.11.5] - 2026-03-20
6
+
7
+ ### Added
8
+ - Added fork context preamble: tasks run with `context: "fork"` are now wrapped with a default preamble that anchors the subagent to its task, preventing it from continuing the parent conversation. The default is `DEFAULT_FORK_PREAMBLE` in `types.ts`. Internal/programmatic callers can use `wrapForkTask(task, false)` to disable it or pass a custom string (this is not exposed as a tool parameter).
9
+ - Added a prompt-template delegation bridge (`prompt-template-bridge.ts`) on the shared extension event bus. The subagent extension now listens for `prompt-template:subagent:request` and emits correlated `started`/`response`/`update` events, with cwd safety checks and race-safe cancellation handling.
10
+ - Added delegated progress streaming via `prompt-template:subagent:update`, mapped from subagent executor `onUpdate` progress payloads.
11
+
12
+ ### Changed
13
+ - Session lifecycle reset now preserves the latest extension context for event-bus delegated runs.
14
+ - `[fork]` badge is now shown only on the result row, not duplicated on both the tool-call and result rows.
15
+
16
+ ## [0.11.4] - 2026-03-19
17
+
18
+ ### Added
19
+ - Added explicit execution context mode for tool calls: `context: "fresh" | "fork"` (default: `fresh`).
20
+ - Added true forked-context execution for single, parallel, and chain runs. In `fork` mode each child run now starts from a real branched session file created from the parent session's current leaf.
21
+ - Added `--fork` slash-command flag for `/run`, `/chain`, and `/parallel` to forward `context: "fork"`.
22
+ - Added regression coverage for fork execution/session wiring and fork badge rendering, including slash command forwarding tests.
23
+
24
+ ### Changed
25
+ - Session argument wiring now supports `--session <file>` in addition to `--session-dir`, enabling exact leaf-preserving forks without summary injection.
26
+ - Async runner step payloads now carry per-step session files so background single/chain/parallel executions can also honor `context: "fork"`.
27
+ - Clarified docs for foreground vs background semantics so `--bg` behavior is explicit.
28
+
29
+ ### Fixed
30
+ - `context: "fork"` now fails fast with explicit errors when parent session state is unavailable (missing persisted session, missing current leaf, or failed branch extraction), with no silent fallback to `fresh`.
31
+ - Fork-session creation errors are now surfaced as tool errors instead of bubbling as uncaught exceptions during execution.
32
+ - Session directory preparation now fails loudly with actionable errors (instead of silently swallowing mkdir failures).
33
+ - Async launch now fails with explicit errors when the async run directory cannot be created.
34
+ - Share logs now correctly include forked session files even when no session directory exists.
35
+ - Tool-call and result rendering now explicitly show `[fork]` when `context: "fork"` is used, including empty-result responses.
36
+ - `subagent_status` now surfaces async result-file read failures instead of returning a misleading missing-status message.
37
+
5
38
  ## [0.11.3] - 2026-03-17
6
39
 
7
40
  ### Changed
package/README.md CHANGED
@@ -172,7 +172,24 @@ Add `--bg` at the end of any slash command to run in the background:
172
172
  /parallel scout "scan frontend" -> scout "scan backend" -> scout "scan infra" --bg
173
173
  ```
174
174
 
175
- Background tasks run asynchronously and notify you when complete. Check status with `subagent_status`.
175
+ Without `--bg`, the run is foreground: the tool call stays active and streams progress until completion. With `--bg`, the run is launched asynchronously: control returns immediately, and completion arrives later via notification. In both cases subagents run as separate processes. Check status with `subagent_status`.
176
+
177
+ ### Forked Context Execution
178
+
179
+ Add `--fork` at the end of `/run`, `/chain`, or `/parallel` to run with `context: "fork"`:
180
+
181
+ ```
182
+ /run reviewer "review this diff" --fork
183
+ /chain scout "analyze this branch" -> planner "plan next steps" --fork
184
+ /parallel scout "audit frontend" -> reviewer "audit backend" --fork
185
+ ```
186
+
187
+ You can combine `--fork` and `--bg` in any order:
188
+
189
+ ```
190
+ /run reviewer "review this diff" --fork --bg
191
+ /run reviewer "review this diff" --bg --fork
192
+ ```
176
193
 
177
194
  ## Agents Manager
178
195
 
@@ -285,7 +302,9 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
285
302
  | Chain | Yes | `{ chain: [{agent, task}...] }` with `{task}`, `{previous}`, `{chain_dir}` variables |
286
303
  | Parallel | Yes | `{ tasks: [{agent, task}...] }` - via TUI toggle or converted to chain for async |
287
304
 
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.
305
+ Execution context defaults to `context: "fresh"`, which starts each child run from a clean session. Set `context: "fork"` to start each child from a real branched session created from the parent's current leaf.
306
+
307
+ All modes support foreground and background execution. Foreground is the default (the call waits and streams progress). For programmatic background launch, use `clarify: false, async: true`. For interactive background launch, use `clarify: true` and press `b` in the TUI before running. Chains with parallel steps (`{ parallel: [...] }`) run concurrently with configurable `concurrency` and `failFast` options.
289
308
 
290
309
  **Clarify TUI for single/parallel:**
291
310
 
@@ -397,9 +416,15 @@ Skills are specialized instructions loaded from SKILL.md files and injected into
397
416
  { agent: "scout", task: "find todos", maxOutput: { lines: 1000 } }
398
417
  { agent: "scout", task: "investigate", output: false } // disable file output
399
418
 
400
- // Parallel (sync only)
419
+ // Single agent from parent-session fork (real branched session at current leaf)
420
+ { agent: "worker", task: "continue this thread", context: "fork" }
421
+
422
+ // Parallel
401
423
  { tasks: [{ agent: "scout", task: "a" }, { agent: "scout", task: "b" }] }
402
424
 
425
+ // Parallel with forked context (each task gets its own isolated fork)
426
+ { tasks: [{ agent: "scout", task: "audit frontend" }, { agent: "reviewer", task: "audit backend" }], context: "fork" }
427
+
403
428
  // Chain with TUI clarification (default)
404
429
  { chain: [
405
430
  { agent: "scout", task: "Gather context for auth refactor" },
@@ -408,6 +433,12 @@ Skills are specialized instructions loaded from SKILL.md files and injected into
408
433
  { agent: "reviewer" }
409
434
  ]}
410
435
 
436
+ // Chain with forked context (each step gets its own isolated fork of the same parent leaf)
437
+ { chain: [
438
+ { agent: "scout", task: "Analyze current branch decisions" },
439
+ { agent: "planner", task: "Plan from {previous}" }
440
+ ], context: "fork" }
441
+
411
442
  // Chain without TUI (enables async)
412
443
  { chain: [...], clarify: false, async: true }
413
444
 
@@ -529,6 +560,7 @@ Notes:
529
560
  | `model` | string | agent default | Override model for single agent |
530
561
  | `tasks` | `{agent, task, cwd?, skill?}[]` | - | Parallel tasks (sync only) |
531
562
  | `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
563
+ | `context` | `"fresh" \| "fork"` | `fresh` | Execution context mode. `fork` uses a real branched session from the parent's current leaf for each child run |
532
564
  | `chainDir` | string | `<tmpdir>/pi-chain-runs/` | Persistent directory for chain artifacts (default auto-cleaned after 24h) |
533
565
  | `clarify` | boolean | true (chains) | Show TUI to preview/edit chain; implies sync mode |
534
566
  | `agentScope` | `"user" \| "project" \| "both"` | `both` | Agent discovery scope (project wins on name collisions) |
@@ -540,6 +572,8 @@ Notes:
540
572
  | `share` | boolean | false | Upload session to GitHub Gist (see [Session Sharing](#session-sharing)) |
541
573
  | `sessionDir` | string | - | Override session log directory (takes precedence over `defaultSessionDir` and parent-session-derived path) |
542
574
 
575
+ `context: "fork"` fails fast when the parent session is not persisted, the current leaf is missing, or a branched child session cannot be created. It never silently downgrades to `fresh`.
576
+
543
577
  **ChainItem** can be either a sequential step or a parallel step:
544
578
 
545
579
  *Sequential step fields:*
@@ -651,6 +685,8 @@ Files per task:
651
685
 
652
686
  Session files (JSONL) are stored under a per-run session directory. Directory selection follows the same precedence as session root resolution: explicit `sessionDir` > `config.defaultSessionDir` > parent-session-derived path. The session file path is shown in output.
653
687
 
688
+ When `context: "fork"` is used, each child run starts with `--session <branched-session-file>` produced from the parent's current leaf. This is a real session fork, not injected summary text.
689
+
654
690
  ## Session Sharing
655
691
 
656
692
  When `share: true` is passed, the extension will:
@@ -12,7 +12,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
12
  import type { AgentConfig } from "./agents.js";
13
13
  import { applyThinkingSuffix } from "./pi-args.js";
14
14
  import { injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.js";
15
- import { isParallelStep, resolveStepBehavior, type ChainStep, type ParallelStep, type SequentialStep, type StepOverrides } from "./settings.js";
15
+ import { isParallelStep, resolveStepBehavior, type ChainStep, type SequentialStep, type StepOverrides } from "./settings.js";
16
16
  import type { RunnerStep } from "./parallel-utils.js";
17
17
  import { resolvePiPackageRoot } from "./pi-spawn.js";
18
18
  import { buildSkillInjection, normalizeSkillInput, resolveSkills } from "./skills.js";
@@ -40,7 +40,9 @@ const jitiCliPath: string | undefined = (() => {
40
40
  try {
41
41
  const p = candidate();
42
42
  if (fs.existsSync(p)) return p;
43
- } catch {}
43
+ } catch {
44
+ // Candidate not available in this install, continue probing.
45
+ }
44
46
  }
45
47
  return undefined;
46
48
  })();
@@ -62,6 +64,7 @@ export interface AsyncChainParams {
62
64
  shareEnabled: boolean;
63
65
  sessionRoot?: string;
64
66
  chainSkills?: string[];
67
+ sessionFilesByFlatIndex?: (string | undefined)[];
65
68
  }
66
69
 
67
70
  export interface AsyncSingleParams {
@@ -75,6 +78,7 @@ export interface AsyncSingleParams {
75
78
  artifactConfig: ArtifactConfig;
76
79
  shareEnabled: boolean;
77
80
  sessionRoot?: string;
81
+ sessionFile?: string;
78
82
  skills?: string[];
79
83
  output?: string | false;
80
84
  }
@@ -119,7 +123,18 @@ export function executeAsyncChain(
119
123
  id: string,
120
124
  params: AsyncChainParams,
121
125
  ): AsyncExecutionResult {
122
- const { chain, agents, ctx, cwd, maxOutput, artifactsDir, artifactConfig, shareEnabled, sessionRoot } = params;
126
+ const {
127
+ chain,
128
+ agents,
129
+ ctx,
130
+ cwd,
131
+ maxOutput,
132
+ artifactsDir,
133
+ artifactConfig,
134
+ shareEnabled,
135
+ sessionRoot,
136
+ sessionFilesByFlatIndex,
137
+ } = params;
123
138
  const chainSkills = params.chainSkills ?? [];
124
139
 
125
140
  // Validate all agents exist before building steps
@@ -141,10 +156,17 @@ export function executeAsyncChain(
141
156
  const asyncDir = path.join(ASYNC_DIR, id);
142
157
  try {
143
158
  fs.mkdirSync(asyncDir, { recursive: true });
144
- } catch {}
159
+ } catch (error) {
160
+ const message = error instanceof Error ? error.message : String(error);
161
+ return {
162
+ content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
163
+ isError: true,
164
+ details: { mode: "chain" as const, results: [] },
165
+ };
166
+ }
145
167
 
146
168
  /** Build a resolved runner step from a SequentialStep */
147
- const buildSeqStep = (s: SequentialStep) => {
169
+ const buildSeqStep = (s: SequentialStep, sessionFile?: string) => {
148
170
  const a = agents.find((x) => x.name === s.agent)!;
149
171
  const stepSkillInput = normalizeSkillInput(s.skill);
150
172
  const stepOverrides: StepOverrides = { skills: stepSkillInput };
@@ -174,9 +196,17 @@ export function executeAsyncChain(
174
196
  systemPrompt,
175
197
  skills: resolvedSkills.map((r) => r.name),
176
198
  outputPath,
199
+ sessionFile,
177
200
  };
178
201
  };
179
202
 
203
+ let flatStepIndex = 0;
204
+ const nextSessionFile = (): string | undefined => {
205
+ const sessionFile = sessionFilesByFlatIndex?.[flatStepIndex];
206
+ flatStepIndex++;
207
+ return sessionFile;
208
+ };
209
+
180
210
  // Build runner steps — sequential steps become flat objects,
181
211
  // parallel steps become { parallel: [...], concurrency?, failFast? }
182
212
  const steps: RunnerStep[] = chain.map((s) => {
@@ -189,12 +219,12 @@ export function executeAsyncChain(
189
219
  skill: t.skill,
190
220
  model: t.model,
191
221
  output: t.output,
192
- })),
222
+ }, nextSessionFile())),
193
223
  concurrency: s.concurrency,
194
224
  failFast: s.failFast,
195
225
  };
196
226
  }
197
- return buildSeqStep(s as SequentialStep);
227
+ return buildSeqStep(s as SequentialStep, nextSessionFile());
198
228
  });
199
229
 
200
230
  const runnerCwd = cwd ?? ctx.cwd;
@@ -258,7 +288,19 @@ export function executeAsyncSingle(
258
288
  id: string,
259
289
  params: AsyncSingleParams,
260
290
  ): AsyncExecutionResult {
261
- const { agent, task, agentConfig, ctx, cwd, maxOutput, artifactsDir, artifactConfig, shareEnabled, sessionRoot } = params;
291
+ const {
292
+ agent,
293
+ task,
294
+ agentConfig,
295
+ ctx,
296
+ cwd,
297
+ maxOutput,
298
+ artifactsDir,
299
+ artifactConfig,
300
+ shareEnabled,
301
+ sessionRoot,
302
+ sessionFile,
303
+ } = params;
262
304
  const skillNames = params.skills ?? agentConfig.skills ?? [];
263
305
  const { resolved: resolvedSkills } = resolveSkills(skillNames, ctx.cwd);
264
306
  let systemPrompt = agentConfig.systemPrompt?.trim() || null;
@@ -270,7 +312,14 @@ export function executeAsyncSingle(
270
312
  const asyncDir = path.join(ASYNC_DIR, id);
271
313
  try {
272
314
  fs.mkdirSync(asyncDir, { recursive: true });
273
- } catch {}
315
+ } catch (error) {
316
+ const message = error instanceof Error ? error.message : String(error);
317
+ return {
318
+ content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
319
+ isError: true,
320
+ details: { mode: "single" as const, results: [] },
321
+ };
322
+ }
274
323
 
275
324
  const runnerCwd = cwd ?? ctx.cwd;
276
325
  const outputPath = resolveSingleOutputPath(params.output, ctx.cwd, cwd);
@@ -290,6 +339,7 @@ export function executeAsyncSingle(
290
339
  systemPrompt,
291
340
  skills: resolvedSkills.map((r) => r.name),
292
341
  outputPath,
342
+ sessionFile,
293
343
  },
294
344
  ],
295
345
  resultPath: path.join(RESULTS_DIR, `${id}.json`),
@@ -70,6 +70,7 @@ export interface ChainExecutionParams {
70
70
  cwd?: string;
71
71
  shareEnabled: boolean;
72
72
  sessionDirForIndex: (idx?: number) => string | undefined;
73
+ sessionFileForIndex?: (idx?: number) => string | undefined;
73
74
  artifactsDir: string;
74
75
  artifactConfig: ArtifactConfig;
75
76
  includeProgress?: boolean;
@@ -103,6 +104,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
103
104
  cwd,
104
105
  shareEnabled,
105
106
  sessionDirForIndex,
107
+ sessionFileForIndex,
106
108
  artifactsDir,
107
109
  artifactConfig,
108
110
  includeProgress,
@@ -333,6 +335,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
333
335
  runId,
334
336
  index: globalTaskIndex + taskIndex,
335
337
  sessionDir: sessionDirForIndex(globalTaskIndex + taskIndex),
338
+ sessionFile: sessionFileForIndex?.(globalTaskIndex + taskIndex),
336
339
  share: shareEnabled,
337
340
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
338
341
  artifactConfig,
@@ -489,6 +492,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
489
492
  runId,
490
493
  index: globalTaskIndex,
491
494
  sessionDir: sessionDirForIndex(globalTaskIndex),
495
+ sessionFile: sessionFileForIndex?.(globalTaskIndex),
492
496
  share: shareEnabled,
493
497
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
494
498
  artifactConfig,
package/execution.ts CHANGED
@@ -56,7 +56,7 @@ export async function runSync(
56
56
  }
57
57
 
58
58
  const shareEnabled = options.share === true;
59
- const sessionEnabled = Boolean(options.sessionDir) || shareEnabled;
59
+ const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
60
60
  const effectiveModel = modelOverride ?? agent.model;
61
61
  const modelArg = applyThinkingSuffix(effectiveModel, agent.thinking);
62
62
 
@@ -74,6 +74,7 @@ export async function runSync(
74
74
  task,
75
75
  sessionEnabled,
76
76
  sessionDir: options.sessionDir,
77
+ sessionFile: options.sessionFile,
77
78
  model: effectiveModel,
78
79
  thinking: agent.thinking,
79
80
  tools: agent.tools,
@@ -263,7 +264,9 @@ export async function runSync(
263
264
  }
264
265
  scheduleUpdate();
265
266
  }
266
- } catch {}
267
+ } catch {
268
+ // Non-JSON stdout lines are expected; only structured events are parsed.
269
+ }
267
270
  };
268
271
 
269
272
  let stderrBuf = "";
@@ -307,7 +310,9 @@ export async function runSync(
307
310
  if (closeJsonlWriter) {
308
311
  try {
309
312
  await closeJsonlWriter();
310
- } catch {}
313
+ } catch {
314
+ // JSONL artifact flush is best effort.
315
+ }
311
316
  }
312
317
 
313
318
  cleanupTempDir(tempDir);
@@ -379,8 +384,9 @@ export async function runSync(
379
384
  }
380
385
  }
381
386
 
382
- if (shareEnabled && options.sessionDir) {
383
- const sessionFile = findLatestSessionFile(options.sessionDir);
387
+ if (shareEnabled) {
388
+ const sessionFile = options.sessionFile
389
+ ?? (options.sessionDir ? findLatestSessionFile(options.sessionDir) : null);
384
390
  if (sessionFile) {
385
391
  result.sessionFile = sessionFile;
386
392
  // HTML export disabled - module resolution issues with global pi installation
@@ -0,0 +1,56 @@
1
+ export type SubagentExecutionContext = "fresh" | "fork";
2
+
3
+ export interface ForkableSessionManager {
4
+ getSessionFile(): string | undefined;
5
+ getLeafId(): string | null;
6
+ createBranchedSession(leafId: string): string | undefined;
7
+ }
8
+
9
+ export interface ForkContextResolver {
10
+ sessionFileForIndex(index?: number): string | undefined;
11
+ }
12
+
13
+ export function resolveSubagentContext(value: unknown): SubagentExecutionContext {
14
+ return value === "fork" ? "fork" : "fresh";
15
+ }
16
+
17
+ export function createForkContextResolver(
18
+ sessionManager: ForkableSessionManager,
19
+ requestedContext: unknown,
20
+ ): ForkContextResolver {
21
+ if (resolveSubagentContext(requestedContext) !== "fork") {
22
+ return {
23
+ sessionFileForIndex: () => undefined,
24
+ };
25
+ }
26
+
27
+ const parentSessionFile = sessionManager.getSessionFile();
28
+ if (!parentSessionFile) {
29
+ throw new Error("Forked subagent context requires a persisted parent session.");
30
+ }
31
+
32
+ const leafId = sessionManager.getLeafId();
33
+ if (!leafId) {
34
+ throw new Error("Forked subagent context requires a current leaf to fork from.");
35
+ }
36
+
37
+ const cachedSessionFiles = new Map<number, string>();
38
+
39
+ return {
40
+ sessionFileForIndex(index = 0): string | undefined {
41
+ const cached = cachedSessionFiles.get(index);
42
+ if (cached) return cached;
43
+ try {
44
+ const sessionFile = sessionManager.createBranchedSession(leafId);
45
+ if (!sessionFile) {
46
+ throw new Error("Session manager did not return a session file.");
47
+ }
48
+ cachedSessionFiles.set(index, sessionFile);
49
+ return sessionFile;
50
+ } catch (error) {
51
+ const cause = error instanceof Error ? error : new Error(String(error));
52
+ throw new Error(`Failed to create forked subagent session: ${cause.message}`, { cause });
53
+ }
54
+ },
55
+ };
56
+ }
package/index.ts CHANGED
@@ -27,6 +27,7 @@ import { createSubagentExecutor } from "./subagent-executor.js";
27
27
  import { createAsyncJobTracker } from "./async-job-tracker.js";
28
28
  import { createResultWatcher } from "./result-watcher.js";
29
29
  import { registerSlashCommands } from "./slash-commands.js";
30
+ import { registerPromptTemplateDelegationBridge } from "./prompt-template-bridge.js";
30
31
  import {
31
32
  type Details,
32
33
  type ExtensionConfig,
@@ -59,7 +60,9 @@ function loadConfig(): ExtensionConfig {
59
60
  if (fs.existsSync(configPath)) {
60
61
  return JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
61
62
  }
62
- } catch {}
63
+ } catch (error) {
64
+ console.error(`Failed to load subagent config from '${configPath}':`, error);
65
+ }
63
66
  return {};
64
67
  }
65
68
 
@@ -81,7 +84,9 @@ function ensureAccessibleDir(dirPath: string): void {
81
84
  } catch {
82
85
  try {
83
86
  fs.rmSync(dirPath, { recursive: true, force: true });
84
- } catch {}
87
+ } catch {
88
+ // Best effort: retry mkdir/access even if cleanup fails.
89
+ }
85
90
  fs.mkdirSync(dirPath, { recursive: true });
86
91
  fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
87
92
  }
@@ -134,6 +139,27 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
134
139
  discoverAgents,
135
140
  });
136
141
 
142
+ const promptTemplateBridge = registerPromptTemplateDelegationBridge({
143
+ events: pi.events,
144
+ getContext: () => state.lastUiContext,
145
+ execute: async (requestId, request, signal, ctx, onUpdate) =>
146
+ executor.execute(
147
+ requestId,
148
+ {
149
+ agent: request.agent,
150
+ task: request.task,
151
+ context: request.context,
152
+ cwd: request.cwd,
153
+ model: request.model,
154
+ async: false,
155
+ clarify: false,
156
+ },
157
+ signal,
158
+ onUpdate,
159
+ ctx,
160
+ ),
161
+ });
162
+
137
163
  const tool: ToolDefinition<typeof SubagentParams, Details> = {
138
164
  name: "subagent",
139
165
  label: "Subagent",
@@ -143,6 +169,7 @@ EXECUTION (use exactly ONE mode):
143
169
  • SINGLE: { agent, task } - one task
144
170
  • CHAIN: { chain: [{agent:"scout"}, {agent:"planner"}] } - sequential pipeline
145
171
  • PARALLEL: { tasks: [{agent,task}, ...] } - concurrent execution
172
+ • Optional context: { context: "fresh" | "fork" } (default: "fresh")
146
173
 
147
174
  CHAIN TEMPLATE VARIABLES (use in task strings):
148
175
  • {task} - The original task/request from the user
@@ -277,7 +304,14 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
277
304
  const lines = [`Run: ${data.id ?? params.id}`, `State: ${status}`, `Result: ${resultPath}`];
278
305
  if (data.summary) lines.push("", data.summary);
279
306
  return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [] } };
280
- } catch {}
307
+ } catch (error) {
308
+ const message = error instanceof Error ? error.message : String(error);
309
+ return {
310
+ content: [{ type: "text", text: `Failed to read async result file: ${message}` }],
311
+ isError: true,
312
+ details: { mode: "single" as const, results: [] },
313
+ };
314
+ }
281
315
  }
282
316
 
283
317
  return {
@@ -311,12 +345,15 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
311
345
  if (sessionFile) {
312
346
  cleanupOldArtifacts(getArtifactsDir(sessionFile), DEFAULT_ARTIFACT_CONFIG.cleanupDays);
313
347
  }
314
- } catch {}
348
+ } catch {
349
+ // Cleanup failures should not block session lifecycle events.
350
+ }
315
351
  };
316
352
 
317
353
  const resetSessionState = (ctx: ExtensionContext) => {
318
354
  state.baseCwd = ctx.cwd;
319
355
  state.currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
356
+ state.lastUiContext = ctx;
320
357
  cleanupSessionArtifacts(ctx);
321
358
  resetJobs(ctx);
322
359
  };
@@ -339,6 +376,8 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
339
376
  }
340
377
  state.cleanupTimers.clear();
341
378
  state.asyncJobs.clear();
379
+ promptTemplateBridge.cancelAll();
380
+ promptTemplateBridge.dispose();
342
381
  if (state.lastUiContext?.hasUI) {
343
382
  state.lastUiContext.ui.setWidget(WIDGET_KEY, undefined);
344
383
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.11.3",
3
+ "version": "0.11.5",
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",
package/parallel-utils.ts CHANGED
@@ -16,6 +16,7 @@ export interface RunnerSubagentStep {
16
16
  systemPrompt?: string | null;
17
17
  skills?: string[];
18
18
  outputPath?: string;
19
+ sessionFile?: string;
19
20
  }
20
21
 
21
22
  /** Parallel step group — multiple agents running concurrently */
package/pi-args.ts CHANGED
@@ -10,6 +10,7 @@ export interface BuildPiArgsInput {
10
10
  task: string;
11
11
  sessionEnabled: boolean;
12
12
  sessionDir?: string;
13
+ sessionFile?: string;
13
14
  model?: string;
14
15
  thinking?: string;
15
16
  tools?: string[];
@@ -36,14 +37,16 @@ export function applyThinkingSuffix(model: string | undefined, thinking: string
36
37
  export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
37
38
  const args = [...input.baseArgs];
38
39
 
39
- if (!input.sessionEnabled) {
40
- args.push("--no-session");
41
- }
42
- if (input.sessionDir) {
43
- try {
40
+ if (input.sessionFile) {
41
+ args.push("--session", input.sessionFile);
42
+ } else {
43
+ if (!input.sessionEnabled) {
44
+ args.push("--no-session");
45
+ }
46
+ if (input.sessionDir) {
44
47
  fs.mkdirSync(input.sessionDir, { recursive: true });
45
- } catch {}
46
- args.push("--session-dir", input.sessionDir);
48
+ args.push("--session-dir", input.sessionDir);
49
+ }
47
50
  }
48
51
 
49
52
  const modelArg = applyThinkingSuffix(input.model, input.thinking);
@@ -118,5 +121,7 @@ export function cleanupTempDir(tempDir: string | null | undefined): void {
118
121
  if (!tempDir) return;
119
122
  try {
120
123
  fs.rmSync(tempDir, { recursive: true, force: true });
121
- } catch {}
124
+ } catch {
125
+ // Temp cleanup is best effort.
126
+ }
122
127
  }