pi-subagents 0.11.3 → 0.11.4
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 +22 -0
- package/README.md +39 -3
- package/async-execution.ts +59 -9
- package/chain-execution.ts +4 -0
- package/execution.ts +11 -5
- package/fork-context.ts +56 -0
- package/index.ts +22 -7
- package/package.json +1 -1
- package/parallel-utils.ts +1 -0
- package/pi-args.ts +13 -8
- package/render.ts +6 -3
- package/schemas.ts +4 -0
- package/slash-commands.ts +35 -14
- package/subagent-executor.ts +184 -50
- package/subagent-runner.ts +39 -12
- package/types.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.11.4] - 2026-03-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added explicit execution context mode for tool calls: `context: "fresh" | "fork"` (default: `fresh`).
|
|
9
|
+
- 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.
|
|
10
|
+
- Added `--fork` slash-command flag for `/run`, `/chain`, and `/parallel` to forward `context: "fork"`.
|
|
11
|
+
- Added regression coverage for fork execution/session wiring and fork badge rendering, including slash command forwarding tests.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Session argument wiring now supports `--session <file>` in addition to `--session-dir`, enabling exact leaf-preserving forks without summary injection.
|
|
15
|
+
- Async runner step payloads now carry per-step session files so background single/chain/parallel executions can also honor `context: "fork"`.
|
|
16
|
+
- Clarified docs for foreground vs background semantics so `--bg` behavior is explicit.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- `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`.
|
|
20
|
+
- Fork-session creation errors are now surfaced as tool errors instead of bubbling as uncaught exceptions during execution.
|
|
21
|
+
- Session directory preparation now fails loudly with actionable errors (instead of silently swallowing mkdir failures).
|
|
22
|
+
- Async launch now fails with explicit errors when the async run directory cannot be created.
|
|
23
|
+
- Share logs now correctly include forked session files even when no session directory exists.
|
|
24
|
+
- Tool-call and result rendering now explicitly show `[fork]` when `context: "fork"` is used, including empty-result responses.
|
|
25
|
+
- `subagent_status` now surfaces async result-file read failures instead of returning a misleading missing-status message.
|
|
26
|
+
|
|
5
27
|
## [0.11.3] - 2026-03-17
|
|
6
28
|
|
|
7
29
|
### 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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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:
|
package/async-execution.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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 {
|
|
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`),
|
package/chain-execution.ts
CHANGED
|
@@ -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
|
|
383
|
-
const sessionFile =
|
|
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
|
package/fork-context.ts
ADDED
|
@@ -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
|
@@ -59,7 +59,9 @@ function loadConfig(): ExtensionConfig {
|
|
|
59
59
|
if (fs.existsSync(configPath)) {
|
|
60
60
|
return JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
|
|
61
61
|
}
|
|
62
|
-
} catch {
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`Failed to load subagent config from '${configPath}':`, error);
|
|
64
|
+
}
|
|
63
65
|
return {};
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -81,7 +83,9 @@ function ensureAccessibleDir(dirPath: string): void {
|
|
|
81
83
|
} catch {
|
|
82
84
|
try {
|
|
83
85
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
84
|
-
} catch {
|
|
86
|
+
} catch {
|
|
87
|
+
// Best effort: retry mkdir/access even if cleanup fails.
|
|
88
|
+
}
|
|
85
89
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
86
90
|
fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
87
91
|
}
|
|
@@ -143,6 +147,7 @@ EXECUTION (use exactly ONE mode):
|
|
|
143
147
|
• SINGLE: { agent, task } - one task
|
|
144
148
|
• CHAIN: { chain: [{agent:"scout"}, {agent:"planner"}] } - sequential pipeline
|
|
145
149
|
• PARALLEL: { tasks: [{agent,task}, ...] } - concurrent execution
|
|
150
|
+
• Optional context: { context: "fresh" | "fork" } (default: "fresh")
|
|
146
151
|
|
|
147
152
|
CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
148
153
|
• {task} - The original task/request from the user
|
|
@@ -179,20 +184,21 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
|
|
|
179
184
|
}
|
|
180
185
|
const isParallel = (args.tasks?.length ?? 0) > 0;
|
|
181
186
|
const asyncLabel = args.async === true && !isParallel ? theme.fg("warning", " [async]") : "";
|
|
187
|
+
const contextLabel = args.context === "fork" ? theme.fg("warning", " [fork]") : "";
|
|
182
188
|
if (args.chain?.length)
|
|
183
189
|
return new Text(
|
|
184
|
-
`${theme.fg("toolTitle", theme.bold("subagent "))}chain (${args.chain.length})${asyncLabel}`,
|
|
190
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}chain (${args.chain.length})${asyncLabel}${contextLabel}`,
|
|
185
191
|
0,
|
|
186
192
|
0,
|
|
187
193
|
);
|
|
188
194
|
if (isParallel)
|
|
189
195
|
return new Text(
|
|
190
|
-
`${theme.fg("toolTitle", theme.bold("subagent "))}parallel (${args.tasks!.length})`,
|
|
196
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}parallel (${args.tasks!.length})${contextLabel}`,
|
|
191
197
|
0,
|
|
192
198
|
0,
|
|
193
199
|
);
|
|
194
200
|
return new Text(
|
|
195
|
-
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
|
|
201
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent || "?")}${asyncLabel}${contextLabel}`,
|
|
196
202
|
0,
|
|
197
203
|
0,
|
|
198
204
|
);
|
|
@@ -277,7 +283,14 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
|
|
|
277
283
|
const lines = [`Run: ${data.id ?? params.id}`, `State: ${status}`, `Result: ${resultPath}`];
|
|
278
284
|
if (data.summary) lines.push("", data.summary);
|
|
279
285
|
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [] } };
|
|
280
|
-
} catch {
|
|
286
|
+
} catch (error) {
|
|
287
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
288
|
+
return {
|
|
289
|
+
content: [{ type: "text", text: `Failed to read async result file: ${message}` }],
|
|
290
|
+
isError: true,
|
|
291
|
+
details: { mode: "single" as const, results: [] },
|
|
292
|
+
};
|
|
293
|
+
}
|
|
281
294
|
}
|
|
282
295
|
|
|
283
296
|
return {
|
|
@@ -311,7 +324,9 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
|
|
|
311
324
|
if (sessionFile) {
|
|
312
325
|
cleanupOldArtifacts(getArtifactsDir(sessionFile), DEFAULT_ARTIFACT_CONFIG.cleanupDays);
|
|
313
326
|
}
|
|
314
|
-
} catch {
|
|
327
|
+
} catch {
|
|
328
|
+
// Cleanup failures should not block session lifecycle events.
|
|
329
|
+
}
|
|
315
330
|
};
|
|
316
331
|
|
|
317
332
|
const resetSessionState = (ctx: ExtensionContext) => {
|
package/package.json
CHANGED
package/parallel-utils.ts
CHANGED
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 (
|
|
40
|
-
args.push("--
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
}
|
package/render.ts
CHANGED
|
@@ -184,7 +184,8 @@ export function renderSubagentResult(
|
|
|
184
184
|
if (!d || !d.results.length) {
|
|
185
185
|
const t = result.content[0];
|
|
186
186
|
const text = t?.type === "text" ? t.text : "(no output)";
|
|
187
|
-
|
|
187
|
+
const contextPrefix = d?.context === "fork" ? `${theme.fg("warning", "[fork]")} ` : "";
|
|
188
|
+
return new Text(truncLine(`${contextPrefix}${text}`, getTermWidth() - 4), 0, 0);
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
const mdTheme = getMarkdownTheme();
|
|
@@ -197,6 +198,7 @@ export function renderSubagentResult(
|
|
|
197
198
|
: r.exitCode === 0
|
|
198
199
|
? theme.fg("success", "ok")
|
|
199
200
|
: theme.fg("error", "X");
|
|
201
|
+
const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
|
|
200
202
|
const output = r.truncation?.text || getFinalOutput(r.messages);
|
|
201
203
|
|
|
202
204
|
const progressInfo = isRunning && r.progress
|
|
@@ -207,7 +209,7 @@ export function renderSubagentResult(
|
|
|
207
209
|
|
|
208
210
|
const w = getTermWidth() - 4;
|
|
209
211
|
const c = new Container();
|
|
210
|
-
c.addChild(new Text(truncLine(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${progressInfo}`, w), 0, 0));
|
|
212
|
+
c.addChild(new Text(truncLine(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${progressInfo}`, w), 0, 0));
|
|
211
213
|
c.addChild(new Spacer(1));
|
|
212
214
|
const taskMaxLen = Math.max(20, w - 8);
|
|
213
215
|
const taskPreview = r.task.length > taskMaxLen
|
|
@@ -285,6 +287,7 @@ export function renderSubagentResult(
|
|
|
285
287
|
: "";
|
|
286
288
|
|
|
287
289
|
const modeLabel = d.mode;
|
|
290
|
+
const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
|
|
288
291
|
// For parallel-in-chain, show task count (results) for consistency with step display
|
|
289
292
|
// For sequential chains, show logical step count
|
|
290
293
|
const hasParallelInChain = d.chainAgents?.some((a) => a.startsWith("["));
|
|
@@ -324,7 +327,7 @@ export function renderSubagentResult(
|
|
|
324
327
|
const c = new Container();
|
|
325
328
|
c.addChild(
|
|
326
329
|
new Text(
|
|
327
|
-
truncLine(`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${stepInfo}${summaryStr}`, w),
|
|
330
|
+
truncLine(`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${contextBadge}${stepInfo}${summaryStr}`, w),
|
|
328
331
|
0,
|
|
329
332
|
0,
|
|
330
333
|
),
|
package/schemas.ts
CHANGED
|
@@ -76,6 +76,10 @@ export const SubagentParams = Type.Object({
|
|
|
76
76
|
})),
|
|
77
77
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task}, ...]" })),
|
|
78
78
|
chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential pipeline where each step's response becomes {previous} for the next. Use {task}, {previous}, {chain_dir} in task templates." })),
|
|
79
|
+
context: Type.Optional(Type.String({
|
|
80
|
+
enum: ["fresh", "fork"],
|
|
81
|
+
description: "Execution context mode: 'fresh' (default) starts clean, 'fork' starts from a real fork of the parent session leaf",
|
|
82
|
+
})),
|
|
79
83
|
chainDir: Type.Optional(Type.String({ description: "Persistent directory for chain artifacts. Default: <tmpdir>/pi-chain-runs/ (auto-cleaned after 24h)" })),
|
|
80
84
|
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
81
85
|
agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
|
package/slash-commands.ts
CHANGED
|
@@ -49,11 +49,26 @@ const parseAgentToken = (token: string): { name: string; config: InlineConfig }
|
|
|
49
49
|
return { name: token.slice(0, bracket), config: parseInlineConfig(token.slice(bracket + 1, end !== -1 ? end : undefined)) };
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const extractExecutionFlags = (rawArgs: string): { args: string; bg: boolean; fork: boolean } => {
|
|
53
|
+
let args = rawArgs.trim();
|
|
54
|
+
let bg = false;
|
|
55
|
+
let fork = false;
|
|
56
|
+
|
|
57
|
+
while (true) {
|
|
58
|
+
if (args.endsWith(" --bg") || args === "--bg") {
|
|
59
|
+
bg = true;
|
|
60
|
+
args = args === "--bg" ? "" : args.slice(0, -5).trim();
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (args.endsWith(" --fork") || args === "--fork") {
|
|
64
|
+
fork = true;
|
|
65
|
+
args = args === "--fork" ? "" : args.slice(0, -7).trim();
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
55
69
|
}
|
|
56
|
-
|
|
70
|
+
|
|
71
|
+
return { args, bg, fork };
|
|
57
72
|
};
|
|
58
73
|
|
|
59
74
|
function setupDirectRun(ctx: ExtensionContext, getSubagentSessionRoot: (parentSessionFile: string | null) => string) {
|
|
@@ -62,7 +77,10 @@ function setupDirectRun(ctx: ExtensionContext, getSubagentSessionRoot: (parentSe
|
|
|
62
77
|
const sessionRoot = path.join(getSubagentSessionRoot(parentSessionFile), runId);
|
|
63
78
|
try {
|
|
64
79
|
fs.mkdirSync(sessionRoot, { recursive: true });
|
|
65
|
-
} catch {
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
+
throw new Error(`Failed to create session directory '${sessionRoot}': ${message}`);
|
|
83
|
+
}
|
|
66
84
|
return {
|
|
67
85
|
runId,
|
|
68
86
|
shareEnabled: false,
|
|
@@ -130,7 +148,7 @@ async function openAgentManager(
|
|
|
130
148
|
const id = randomUUID();
|
|
131
149
|
const asyncCtx = { pi, cwd: ctx.cwd, currentSessionId: ctx.sessionManager.getSessionId() ?? id };
|
|
132
150
|
const asyncSessionRoot = getSubagentSessionRoot(ctx.sessionManager.getSessionFile() ?? null);
|
|
133
|
-
|
|
151
|
+
fs.mkdirSync(asyncSessionRoot, { recursive: true });
|
|
134
152
|
executeAsyncChain(id, {
|
|
135
153
|
chain: r.requestedAsync.chain,
|
|
136
154
|
agents,
|
|
@@ -268,16 +286,16 @@ export function registerSlashCommands(
|
|
|
268
286
|
});
|
|
269
287
|
|
|
270
288
|
pi.registerCommand("run", {
|
|
271
|
-
description: "Run a subagent directly: /run agent[output=file] task [--bg]",
|
|
289
|
+
description: "Run a subagent directly: /run agent[output=file] task [--bg] [--fork]",
|
|
272
290
|
getArgumentCompletions: makeAgentCompletions(state, false),
|
|
273
291
|
handler: async (args, ctx) => {
|
|
274
|
-
const { args: cleanedArgs, bg } =
|
|
292
|
+
const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
|
|
275
293
|
const input = cleanedArgs.trim();
|
|
276
294
|
const firstSpace = input.indexOf(" ");
|
|
277
|
-
if (firstSpace === -1) { ctx.ui.notify("Usage: /run <agent> <task> [--bg]", "error"); return; }
|
|
295
|
+
if (firstSpace === -1) { ctx.ui.notify("Usage: /run <agent> <task> [--bg] [--fork]", "error"); return; }
|
|
278
296
|
const { name: agentName, config: inline } = parseAgentToken(input.slice(0, firstSpace));
|
|
279
297
|
const task = input.slice(firstSpace + 1).trim();
|
|
280
|
-
if (!task) { ctx.ui.notify("Usage: /run <agent> <task> [--bg]", "error"); return; }
|
|
298
|
+
if (!task) { ctx.ui.notify("Usage: /run <agent> <task> [--bg] [--fork]", "error"); return; }
|
|
281
299
|
|
|
282
300
|
const agents = discoverAgents(state.baseCwd, "both").agents;
|
|
283
301
|
if (!agents.find((a) => a.name === agentName)) { ctx.ui.notify(`Unknown agent: ${agentName}`, "error"); return; }
|
|
@@ -291,15 +309,16 @@ export function registerSlashCommands(
|
|
|
291
309
|
if (inline.skill !== undefined) params.skill = inline.skill;
|
|
292
310
|
if (inline.model) params.model = inline.model;
|
|
293
311
|
if (bg) params.async = true;
|
|
312
|
+
if (fork) params.context = "fork";
|
|
294
313
|
pi.sendUserMessage(`Call the subagent tool with these exact parameters: ${JSON.stringify({ ...params, agentScope: "both" })}`);
|
|
295
314
|
},
|
|
296
315
|
});
|
|
297
316
|
|
|
298
317
|
pi.registerCommand("chain", {
|
|
299
|
-
description: "Run agents in sequence: /chain scout \"task\" -> planner [--bg]",
|
|
318
|
+
description: "Run agents in sequence: /chain scout \"task\" -> planner [--bg] [--fork]",
|
|
300
319
|
getArgumentCompletions: makeAgentCompletions(state, true),
|
|
301
320
|
handler: async (args, ctx) => {
|
|
302
|
-
const { args: cleanedArgs, bg } =
|
|
321
|
+
const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
|
|
303
322
|
const parsed = parseAgentArgs(state, cleanedArgs, "chain", ctx);
|
|
304
323
|
if (!parsed) return;
|
|
305
324
|
const chain = parsed.steps.map(({ name, config, task: stepTask }, i) => ({
|
|
@@ -313,15 +332,16 @@ export function registerSlashCommands(
|
|
|
313
332
|
}));
|
|
314
333
|
const params: Record<string, unknown> = { chain, task: parsed.task, clarify: false, agentScope: "both" };
|
|
315
334
|
if (bg) params.async = true;
|
|
335
|
+
if (fork) params.context = "fork";
|
|
316
336
|
pi.sendUserMessage(`Call the subagent tool with these exact parameters: ${JSON.stringify(params)}`);
|
|
317
337
|
},
|
|
318
338
|
});
|
|
319
339
|
|
|
320
340
|
pi.registerCommand("parallel", {
|
|
321
|
-
description: "Run agents in parallel: /parallel scout \"task1\" -> reviewer \"task2\" [--bg]",
|
|
341
|
+
description: "Run agents in parallel: /parallel scout \"task1\" -> reviewer \"task2\" [--bg] [--fork]",
|
|
322
342
|
getArgumentCompletions: makeAgentCompletions(state, true),
|
|
323
343
|
handler: async (args, ctx) => {
|
|
324
|
-
const { args: cleanedArgs, bg } =
|
|
344
|
+
const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
|
|
325
345
|
const parsed = parseAgentArgs(state, cleanedArgs, "parallel", ctx);
|
|
326
346
|
if (!parsed) return;
|
|
327
347
|
if (parsed.steps.length > MAX_PARALLEL) { ctx.ui.notify(`Max ${MAX_PARALLEL} parallel tasks`, "error"); return; }
|
|
@@ -336,6 +356,7 @@ export function registerSlashCommands(
|
|
|
336
356
|
}));
|
|
337
357
|
const params: Record<string, unknown> = { chain: [{ parallel: tasks }], task: parsed.task, clarify: false, agentScope: "both" };
|
|
338
358
|
if (bg) params.async = true;
|
|
359
|
+
if (fork) params.context = "fork";
|
|
339
360
|
pi.sendUserMessage(`Call the subagent tool with these exact parameters: ${JSON.stringify(params)}`);
|
|
340
361
|
},
|
|
341
362
|
});
|
package/subagent-executor.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "./settings.js";
|
|
22
22
|
import { discoverAvailableSkills, normalizeSkillInput } from "./skills.js";
|
|
23
23
|
import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.js";
|
|
24
|
+
import { createForkContextResolver } from "./fork-context.js";
|
|
24
25
|
import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.js";
|
|
25
26
|
import { getFinalOutput, mapConcurrent } from "./utils.js";
|
|
26
27
|
import {
|
|
@@ -55,6 +56,7 @@ interface SubagentParamsLike {
|
|
|
55
56
|
task?: string;
|
|
56
57
|
chain?: ChainStep[];
|
|
57
58
|
tasks?: TaskParam[];
|
|
59
|
+
context?: "fresh" | "fork";
|
|
58
60
|
async?: boolean;
|
|
59
61
|
clarify?: boolean;
|
|
60
62
|
share?: boolean;
|
|
@@ -91,6 +93,7 @@ interface ExecutionContextData {
|
|
|
91
93
|
shareEnabled: boolean;
|
|
92
94
|
sessionRoot: string;
|
|
93
95
|
sessionDirForIndex: (idx?: number) => string;
|
|
96
|
+
sessionFileForIndex: (idx?: number) => string | undefined;
|
|
94
97
|
artifactConfig: ArtifactConfig;
|
|
95
98
|
artifactsDir: string;
|
|
96
99
|
parallelDowngraded: boolean;
|
|
@@ -167,8 +170,71 @@ function validateExecutionInput(
|
|
|
167
170
|
return null;
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
|
|
174
|
+
if ((params.chain?.length ?? 0) > 0) return "chain";
|
|
175
|
+
if ((params.tasks?.length ?? 0) > 0) return "parallel";
|
|
176
|
+
if (params.agent && params.task) return "single";
|
|
177
|
+
return "single";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function withForkContext(
|
|
181
|
+
result: AgentToolResult<Details>,
|
|
182
|
+
context: SubagentParamsLike["context"],
|
|
183
|
+
): AgentToolResult<Details> {
|
|
184
|
+
if (context !== "fork" || !result.details) return result;
|
|
185
|
+
return {
|
|
186
|
+
...result,
|
|
187
|
+
details: {
|
|
188
|
+
...result.details,
|
|
189
|
+
context: "fork",
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function toExecutionErrorResult(params: SubagentParamsLike, error: unknown): AgentToolResult<Details> {
|
|
195
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
196
|
+
return withForkContext(
|
|
197
|
+
{
|
|
198
|
+
content: [{ type: "text", text: message }],
|
|
199
|
+
isError: true,
|
|
200
|
+
details: { mode: getRequestedModeLabel(params), results: [] },
|
|
201
|
+
},
|
|
202
|
+
params.context,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function collectChainSessionFiles(
|
|
207
|
+
chain: ChainStep[],
|
|
208
|
+
sessionFileForIndex: (idx?: number) => string | undefined,
|
|
209
|
+
): (string | undefined)[] {
|
|
210
|
+
const sessionFiles: (string | undefined)[] = [];
|
|
211
|
+
let flatIndex = 0;
|
|
212
|
+
for (const step of chain) {
|
|
213
|
+
if (isParallelStep(step)) {
|
|
214
|
+
for (let i = 0; i < step.parallel.length; i++) {
|
|
215
|
+
sessionFiles.push(sessionFileForIndex(flatIndex));
|
|
216
|
+
flatIndex++;
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
sessionFiles.push(sessionFileForIndex(flatIndex));
|
|
221
|
+
flatIndex++;
|
|
222
|
+
}
|
|
223
|
+
return sessionFiles;
|
|
224
|
+
}
|
|
225
|
+
|
|
170
226
|
function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentToolResult<Details> | null {
|
|
171
|
-
const {
|
|
227
|
+
const {
|
|
228
|
+
params,
|
|
229
|
+
agents,
|
|
230
|
+
ctx,
|
|
231
|
+
shareEnabled,
|
|
232
|
+
sessionRoot,
|
|
233
|
+
sessionFileForIndex,
|
|
234
|
+
artifactConfig,
|
|
235
|
+
artifactsDir,
|
|
236
|
+
effectiveAsync,
|
|
237
|
+
} = data;
|
|
172
238
|
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
173
239
|
const hasSingle = Boolean(params.agent && params.task);
|
|
174
240
|
if (!effectiveAsync) return null;
|
|
@@ -197,6 +263,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
197
263
|
shareEnabled,
|
|
198
264
|
sessionRoot,
|
|
199
265
|
chainSkills,
|
|
266
|
+
sessionFilesByFlatIndex: collectChainSessionFiles(params.chain as ChainStep[], sessionFileForIndex),
|
|
200
267
|
});
|
|
201
268
|
}
|
|
202
269
|
|
|
@@ -204,13 +271,15 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
204
271
|
const a = agents.find((x) => x.name === params.agent);
|
|
205
272
|
if (!a) {
|
|
206
273
|
return {
|
|
207
|
-
content: [{ type: "text", text: `Unknown: ${params.agent}` }],
|
|
274
|
+
content: [{ type: "text", text: `Unknown agent: ${params.agent}` }],
|
|
208
275
|
isError: true,
|
|
209
276
|
details: { mode: "single" as const, results: [] },
|
|
210
277
|
};
|
|
211
278
|
}
|
|
212
279
|
const rawOutput = params.output !== undefined ? params.output : a.output;
|
|
213
280
|
const effectiveOutput: string | false | undefined = rawOutput === true ? a.output : (rawOutput as string | false | undefined);
|
|
281
|
+
const normalizedSkills = normalizeSkillInput(params.skill);
|
|
282
|
+
const skills = normalizedSkills === false ? [] : normalizedSkills;
|
|
214
283
|
return executeAsyncSingle(id, {
|
|
215
284
|
agent: params.agent!,
|
|
216
285
|
task: params.task!,
|
|
@@ -222,12 +291,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
222
291
|
artifactConfig,
|
|
223
292
|
shareEnabled,
|
|
224
293
|
sessionRoot,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (normalized === false) return [];
|
|
228
|
-
if (normalized === undefined) return undefined;
|
|
229
|
-
return normalized;
|
|
230
|
-
})(),
|
|
294
|
+
sessionFile: sessionFileForIndex(0),
|
|
295
|
+
skills,
|
|
231
296
|
output: effectiveOutput,
|
|
232
297
|
});
|
|
233
298
|
}
|
|
@@ -236,7 +301,20 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
236
301
|
}
|
|
237
302
|
|
|
238
303
|
async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details>> {
|
|
239
|
-
const {
|
|
304
|
+
const {
|
|
305
|
+
params,
|
|
306
|
+
agents,
|
|
307
|
+
ctx,
|
|
308
|
+
signal,
|
|
309
|
+
runId,
|
|
310
|
+
shareEnabled,
|
|
311
|
+
sessionDirForIndex,
|
|
312
|
+
sessionFileForIndex,
|
|
313
|
+
artifactsDir,
|
|
314
|
+
artifactConfig,
|
|
315
|
+
onUpdate,
|
|
316
|
+
sessionRoot,
|
|
317
|
+
} = data;
|
|
240
318
|
const normalized = normalizeSkillInput(params.skill);
|
|
241
319
|
const chainSkills = normalized === false ? [] : (normalized ?? []);
|
|
242
320
|
const chainResult = await executeChain({
|
|
@@ -249,6 +327,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
249
327
|
cwd: params.cwd,
|
|
250
328
|
shareEnabled,
|
|
251
329
|
sessionDirForIndex,
|
|
330
|
+
sessionFileForIndex,
|
|
252
331
|
artifactsDir,
|
|
253
332
|
artifactConfig,
|
|
254
333
|
includeProgress: params.includeProgress,
|
|
@@ -279,6 +358,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
279
358
|
shareEnabled,
|
|
280
359
|
sessionRoot,
|
|
281
360
|
chainSkills: chainResult.requestedAsync.chainSkills,
|
|
361
|
+
sessionFilesByFlatIndex: collectChainSessionFiles(chainResult.requestedAsync.chain, sessionFileForIndex),
|
|
282
362
|
});
|
|
283
363
|
}
|
|
284
364
|
|
|
@@ -286,7 +366,21 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
286
366
|
}
|
|
287
367
|
|
|
288
368
|
async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details>> {
|
|
289
|
-
const {
|
|
369
|
+
const {
|
|
370
|
+
params,
|
|
371
|
+
agents,
|
|
372
|
+
ctx,
|
|
373
|
+
signal,
|
|
374
|
+
runId,
|
|
375
|
+
sessionDirForIndex,
|
|
376
|
+
sessionFileForIndex,
|
|
377
|
+
shareEnabled,
|
|
378
|
+
artifactConfig,
|
|
379
|
+
artifactsDir,
|
|
380
|
+
parallelDowngraded,
|
|
381
|
+
onUpdate,
|
|
382
|
+
sessionRoot,
|
|
383
|
+
} = data;
|
|
290
384
|
const allProgress: AgentProgress[] = [];
|
|
291
385
|
const allArtifactPaths: ArtifactPaths[] = [];
|
|
292
386
|
const tasks = params.tasks!;
|
|
@@ -385,6 +479,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
385
479
|
shareEnabled,
|
|
386
480
|
sessionRoot,
|
|
387
481
|
chainSkills: [],
|
|
482
|
+
sessionFilesByFlatIndex: tasks.map((_, index) => sessionFileForIndex(index)),
|
|
388
483
|
});
|
|
389
484
|
}
|
|
390
485
|
}
|
|
@@ -401,6 +496,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
401
496
|
runId,
|
|
402
497
|
index: i,
|
|
403
498
|
sessionDir: sessionDirForIndex(i),
|
|
499
|
+
sessionFile: sessionFileForIndex(i),
|
|
404
500
|
share: shareEnabled,
|
|
405
501
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
406
502
|
artifactConfig,
|
|
@@ -465,7 +561,20 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
465
561
|
}
|
|
466
562
|
|
|
467
563
|
async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details>> {
|
|
468
|
-
const {
|
|
564
|
+
const {
|
|
565
|
+
params,
|
|
566
|
+
agents,
|
|
567
|
+
ctx,
|
|
568
|
+
signal,
|
|
569
|
+
runId,
|
|
570
|
+
sessionDirForIndex,
|
|
571
|
+
sessionFileForIndex,
|
|
572
|
+
shareEnabled,
|
|
573
|
+
artifactConfig,
|
|
574
|
+
artifactsDir,
|
|
575
|
+
onUpdate,
|
|
576
|
+
sessionRoot,
|
|
577
|
+
} = data;
|
|
469
578
|
const allProgress: AgentProgress[] = [];
|
|
470
579
|
const allArtifactPaths: ArtifactPaths[] = [];
|
|
471
580
|
const agentConfig = agents.find((a) => a.name === params.agent);
|
|
@@ -541,6 +650,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
541
650
|
artifactConfig,
|
|
542
651
|
shareEnabled,
|
|
543
652
|
sessionRoot,
|
|
653
|
+
sessionFile: sessionFileForIndex(0),
|
|
544
654
|
skills: skillOverride === false ? [] : skillOverride,
|
|
545
655
|
output: effectiveOutput,
|
|
546
656
|
});
|
|
@@ -551,17 +661,19 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
551
661
|
const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, params.cwd);
|
|
552
662
|
task = injectSingleOutputInstruction(task, outputPath);
|
|
553
663
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
664
|
+
let effectiveSkills: string[] | undefined;
|
|
665
|
+
if (skillOverride === false) {
|
|
666
|
+
effectiveSkills = [];
|
|
667
|
+
} else {
|
|
668
|
+
effectiveSkills = skillOverride;
|
|
669
|
+
}
|
|
559
670
|
|
|
560
671
|
const r = await runSync(ctx.cwd, agents, params.agent!, task, {
|
|
561
672
|
cwd: params.cwd,
|
|
562
673
|
signal,
|
|
563
674
|
runId,
|
|
564
675
|
sessionDir: sessionDirForIndex(0),
|
|
676
|
+
sessionFile: sessionFileForIndex(0),
|
|
565
677
|
share: shareEnabled,
|
|
566
678
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
567
679
|
artifactConfig,
|
|
@@ -659,31 +771,26 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
659
771
|
const agents = deps.discoverAgents(ctx.cwd, scope).agents;
|
|
660
772
|
const runId = randomUUID().slice(0, 8);
|
|
661
773
|
const shareEnabled = params.share === true;
|
|
662
|
-
const sessionRoot = params.sessionDir
|
|
663
|
-
? path.resolve(deps.expandTilde(params.sessionDir))
|
|
664
|
-
: path.join(
|
|
665
|
-
deps.config.defaultSessionDir
|
|
666
|
-
? path.resolve(deps.expandTilde(deps.config.defaultSessionDir))
|
|
667
|
-
: deps.getSubagentSessionRoot(parentSessionFile),
|
|
668
|
-
runId,
|
|
669
|
-
);
|
|
670
|
-
try {
|
|
671
|
-
fs.mkdirSync(sessionRoot, { recursive: true });
|
|
672
|
-
} catch {}
|
|
673
|
-
const sessionDirForIndex = (idx?: number) =>
|
|
674
|
-
path.join(sessionRoot, `run-${idx ?? 0}`);
|
|
675
|
-
|
|
676
774
|
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
677
775
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
678
776
|
const hasSingle = Boolean(params.agent && params.task);
|
|
679
777
|
|
|
778
|
+
const validationError = validateExecutionInput(params, agents, hasChain, hasTasks, hasSingle);
|
|
779
|
+
if (validationError) return validationError;
|
|
780
|
+
|
|
781
|
+
let sessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
|
|
782
|
+
try {
|
|
783
|
+
sessionFileForIndex = createForkContextResolver(ctx.sessionManager, params.context).sessionFileForIndex;
|
|
784
|
+
} catch (error) {
|
|
785
|
+
return toExecutionErrorResult(params, error);
|
|
786
|
+
}
|
|
787
|
+
|
|
680
788
|
const requestedAsync = params.async ?? deps.asyncByDefault;
|
|
681
789
|
const parallelDowngraded = hasTasks && requestedAsync;
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
);
|
|
790
|
+
let effectiveAsync = false;
|
|
791
|
+
if (requestedAsync && !hasTasks) {
|
|
792
|
+
effectiveAsync = hasChain ? params.clarify === false : params.clarify !== true;
|
|
793
|
+
}
|
|
687
794
|
|
|
688
795
|
const artifactConfig: ArtifactConfig = {
|
|
689
796
|
...DEFAULT_ARTIFACT_CONFIG,
|
|
@@ -691,45 +798,72 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
691
798
|
};
|
|
692
799
|
const artifactsDir = effectiveAsync ? deps.tempArtifactsDir : getArtifactsDir(parentSessionFile);
|
|
693
800
|
|
|
694
|
-
|
|
695
|
-
if (
|
|
801
|
+
let sessionRoot: string;
|
|
802
|
+
if (params.sessionDir) {
|
|
803
|
+
sessionRoot = path.resolve(deps.expandTilde(params.sessionDir));
|
|
804
|
+
} else {
|
|
805
|
+
const baseSessionRoot = deps.config.defaultSessionDir
|
|
806
|
+
? path.resolve(deps.expandTilde(deps.config.defaultSessionDir))
|
|
807
|
+
: deps.getSubagentSessionRoot(parentSessionFile);
|
|
808
|
+
sessionRoot = path.join(baseSessionRoot, runId);
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
fs.mkdirSync(sessionRoot, { recursive: true });
|
|
812
|
+
} catch (error) {
|
|
813
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
814
|
+
return toExecutionErrorResult(
|
|
815
|
+
params,
|
|
816
|
+
new Error(`Failed to create session directory '${sessionRoot}': ${message}`),
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
const sessionDirForIndex = (idx?: number) =>
|
|
820
|
+
path.join(sessionRoot, `run-${idx ?? 0}`);
|
|
821
|
+
|
|
822
|
+
const onUpdateWithContext = onUpdate
|
|
823
|
+
? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, params.context))
|
|
824
|
+
: undefined;
|
|
696
825
|
|
|
697
826
|
const execData: ExecutionContextData = {
|
|
698
827
|
params,
|
|
699
828
|
ctx,
|
|
700
829
|
signal,
|
|
701
|
-
onUpdate,
|
|
830
|
+
onUpdate: onUpdateWithContext,
|
|
702
831
|
agents,
|
|
703
832
|
runId,
|
|
704
833
|
shareEnabled,
|
|
705
834
|
sessionRoot,
|
|
706
835
|
sessionDirForIndex,
|
|
836
|
+
sessionFileForIndex,
|
|
707
837
|
artifactConfig,
|
|
708
838
|
artifactsDir,
|
|
709
839
|
parallelDowngraded,
|
|
710
840
|
effectiveAsync,
|
|
711
841
|
};
|
|
712
842
|
|
|
713
|
-
|
|
714
|
-
|
|
843
|
+
try {
|
|
844
|
+
const asyncResult = runAsyncPath(execData, deps);
|
|
845
|
+
if (asyncResult) return withForkContext(asyncResult, params.context);
|
|
715
846
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
847
|
+
if (hasChain && params.chain) {
|
|
848
|
+
return withForkContext(await runChainPath(execData, deps), params.context);
|
|
849
|
+
}
|
|
719
850
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
851
|
+
if (hasTasks && params.tasks) {
|
|
852
|
+
return withForkContext(await runParallelPath(execData, deps), params.context);
|
|
853
|
+
}
|
|
723
854
|
|
|
724
|
-
|
|
725
|
-
|
|
855
|
+
if (hasSingle) {
|
|
856
|
+
return withForkContext(await runSinglePath(execData, deps), params.context);
|
|
857
|
+
}
|
|
858
|
+
} catch (error) {
|
|
859
|
+
return toExecutionErrorResult(params, error);
|
|
726
860
|
}
|
|
727
861
|
|
|
728
|
-
return {
|
|
862
|
+
return withForkContext({
|
|
729
863
|
content: [{ type: "text", text: "Invalid params" }],
|
|
730
864
|
isError: true,
|
|
731
865
|
details: { mode: "single" as const, results: [] },
|
|
732
|
-
};
|
|
866
|
+
}, params.context);
|
|
733
867
|
};
|
|
734
868
|
|
|
735
869
|
return { execute };
|
package/subagent-runner.ts
CHANGED
|
@@ -64,6 +64,7 @@ function findLatestSessionFile(sessionDir: string): string | null {
|
|
|
64
64
|
files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
65
65
|
return files[0] ?? null;
|
|
66
66
|
} catch {
|
|
67
|
+
// Session lookup is optional metadata.
|
|
67
68
|
return null;
|
|
68
69
|
}
|
|
69
70
|
}
|
|
@@ -89,10 +90,13 @@ function parseSessionTokens(sessionDir: string): TokenUsage | null {
|
|
|
89
90
|
input += entry.usage.inputTokens ?? entry.usage.input ?? 0;
|
|
90
91
|
output += entry.usage.outputTokens ?? entry.usage.output ?? 0;
|
|
91
92
|
}
|
|
92
|
-
} catch {
|
|
93
|
+
} catch {
|
|
94
|
+
// Ignore malformed lines while scanning usage entries.
|
|
95
|
+
}
|
|
93
96
|
}
|
|
94
97
|
return { input, output, total: input + output };
|
|
95
98
|
} catch {
|
|
99
|
+
// Usage extraction should not fail the run.
|
|
96
100
|
return null;
|
|
97
101
|
}
|
|
98
102
|
}
|
|
@@ -143,7 +147,9 @@ function resolvePiPackageRootFallback(): string {
|
|
|
143
147
|
try {
|
|
144
148
|
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
145
149
|
if (pkg.name === "@mariozechner/pi-coding-agent") return dir;
|
|
146
|
-
} catch {
|
|
150
|
+
} catch {
|
|
151
|
+
// Keep walking up until a readable package.json is found.
|
|
152
|
+
}
|
|
147
153
|
dir = path.dirname(dir);
|
|
148
154
|
}
|
|
149
155
|
throw new Error("Could not resolve @mariozechner/pi-coding-agent package root");
|
|
@@ -277,11 +283,14 @@ async function runSingleStep(
|
|
|
277
283
|
): Promise<{ agent: string; output: string; exitCode: number | null; artifactPaths?: ArtifactPaths }> {
|
|
278
284
|
const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
|
|
279
285
|
const task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
|
|
286
|
+
const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
|
|
287
|
+
const sessionDir = step.sessionFile ? undefined : ctx.sessionDir;
|
|
280
288
|
const { args, env, tempDir } = buildPiArgs({
|
|
281
289
|
baseArgs: ["-p"],
|
|
282
290
|
task,
|
|
283
|
-
sessionEnabled
|
|
284
|
-
sessionDir
|
|
291
|
+
sessionEnabled,
|
|
292
|
+
sessionDir,
|
|
293
|
+
sessionFile: step.sessionFile,
|
|
285
294
|
model: step.model,
|
|
286
295
|
tools: step.tools,
|
|
287
296
|
extensions: step.extensions,
|
|
@@ -349,15 +358,18 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
349
358
|
const results: StepResult[] = [];
|
|
350
359
|
const overallStartTime = Date.now();
|
|
351
360
|
const shareEnabled = config.share === true;
|
|
352
|
-
const sessionEnabled = Boolean(config.sessionDir) || shareEnabled;
|
|
353
361
|
const asyncDir = config.asyncDir;
|
|
354
362
|
const statusPath = path.join(asyncDir, "status.json");
|
|
355
363
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
356
364
|
const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
|
|
357
365
|
let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
|
|
366
|
+
let latestSessionFile: string | undefined;
|
|
358
367
|
|
|
359
368
|
// Flatten steps for status tracking (parallel groups expand to individual entries)
|
|
360
369
|
const flatSteps = flattenSteps(steps);
|
|
370
|
+
const sessionEnabled = Boolean(config.sessionDir)
|
|
371
|
+
|| shareEnabled
|
|
372
|
+
|| flatSteps.some((step) => Boolean(step.sessionFile));
|
|
361
373
|
const statusPayload: {
|
|
362
374
|
runId: string;
|
|
363
375
|
mode: "single" | "chain";
|
|
@@ -480,6 +492,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
480
492
|
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
481
493
|
piPackageRoot: config.piPackageRoot,
|
|
482
494
|
});
|
|
495
|
+
if (task.sessionFile) {
|
|
496
|
+
latestSessionFile = task.sessionFile;
|
|
497
|
+
}
|
|
483
498
|
|
|
484
499
|
const taskEndTime = Date.now();
|
|
485
500
|
const taskDuration = taskEndTime - taskStartTime;
|
|
@@ -580,6 +595,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
580
595
|
outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
|
|
581
596
|
piPackageRoot: config.piPackageRoot,
|
|
582
597
|
});
|
|
598
|
+
if (seqStep.sessionFile) {
|
|
599
|
+
latestSessionFile = seqStep.sessionFile;
|
|
600
|
+
}
|
|
583
601
|
|
|
584
602
|
previousOutput = singleResult.output;
|
|
585
603
|
results.push({
|
|
@@ -652,11 +670,17 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
652
670
|
let gistUrl: string | undefined;
|
|
653
671
|
let shareError: string | undefined;
|
|
654
672
|
|
|
655
|
-
if (shareEnabled
|
|
656
|
-
sessionFile =
|
|
673
|
+
if (shareEnabled) {
|
|
674
|
+
sessionFile = config.sessionDir
|
|
675
|
+
? (findLatestSessionFile(config.sessionDir) ?? undefined)
|
|
676
|
+
: undefined;
|
|
677
|
+
if (!sessionFile && latestSessionFile) {
|
|
678
|
+
sessionFile = latestSessionFile;
|
|
679
|
+
}
|
|
657
680
|
if (sessionFile) {
|
|
658
681
|
try {
|
|
659
|
-
const
|
|
682
|
+
const exportDir = config.sessionDir ?? path.dirname(sessionFile);
|
|
683
|
+
const htmlPath = await exportSessionHtml(sessionFile, exportDir, config.piPackageRoot);
|
|
660
684
|
const share = createShareLink(htmlPath);
|
|
661
685
|
if ("error" in share) shareError = share.error;
|
|
662
686
|
else {
|
|
@@ -671,11 +695,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
671
695
|
}
|
|
672
696
|
}
|
|
673
697
|
|
|
698
|
+
const effectiveSessionFile = sessionFile ?? latestSessionFile;
|
|
674
699
|
const runEndedAt = Date.now();
|
|
675
700
|
statusPayload.state = results.every((r) => r.success) ? "complete" : "failed";
|
|
676
701
|
statusPayload.endedAt = runEndedAt;
|
|
677
702
|
statusPayload.lastUpdate = runEndedAt;
|
|
678
|
-
statusPayload.sessionFile =
|
|
703
|
+
statusPayload.sessionFile = effectiveSessionFile;
|
|
679
704
|
statusPayload.shareUrl = shareUrl;
|
|
680
705
|
statusPayload.gistUrl = gistUrl;
|
|
681
706
|
statusPayload.shareError = shareError;
|
|
@@ -710,7 +735,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
710
735
|
summary,
|
|
711
736
|
truncated,
|
|
712
737
|
artifactsDir,
|
|
713
|
-
sessionFile,
|
|
738
|
+
sessionFile: effectiveSessionFile,
|
|
714
739
|
shareUrl,
|
|
715
740
|
shareError,
|
|
716
741
|
});
|
|
@@ -740,7 +765,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
740
765
|
cwd,
|
|
741
766
|
asyncDir,
|
|
742
767
|
sessionId: config.sessionId,
|
|
743
|
-
sessionFile,
|
|
768
|
+
sessionFile: effectiveSessionFile,
|
|
744
769
|
shareUrl,
|
|
745
770
|
gistUrl,
|
|
746
771
|
shareError,
|
|
@@ -760,7 +785,9 @@ if (configArg) {
|
|
|
760
785
|
const config = JSON.parse(configJson) as SubagentRunConfig;
|
|
761
786
|
try {
|
|
762
787
|
fs.unlinkSync(configArg);
|
|
763
|
-
} catch {
|
|
788
|
+
} catch {
|
|
789
|
+
// Temp config cleanup is best effort.
|
|
790
|
+
}
|
|
764
791
|
runSubagent(config).catch((runErr) => {
|
|
765
792
|
console.error("Subagent runner error:", runErr);
|
|
766
793
|
process.exit(1);
|
package/types.ts
CHANGED
|
@@ -101,6 +101,7 @@ export interface SingleResult {
|
|
|
101
101
|
|
|
102
102
|
export interface Details {
|
|
103
103
|
mode: "single" | "parallel" | "chain" | "management";
|
|
104
|
+
context?: "fresh" | "fork";
|
|
104
105
|
results: SingleResult[];
|
|
105
106
|
asyncId?: string;
|
|
106
107
|
asyncDir?: string;
|
|
@@ -226,6 +227,7 @@ export interface RunSyncOptions {
|
|
|
226
227
|
runId: string;
|
|
227
228
|
index?: number;
|
|
228
229
|
sessionDir?: string;
|
|
230
|
+
sessionFile?: string;
|
|
229
231
|
share?: boolean;
|
|
230
232
|
/** Override the agent's default model (format: "provider/id" or just "id") */
|
|
231
233
|
modelOverride?: string;
|