pi-subagents 0.24.4 → 0.25.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 +8 -0
- package/README.md +16 -10
- package/package.json +1 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +46 -10
- package/src/extension/fanout-child.ts +170 -0
- package/src/extension/index.ts +6 -2
- package/src/intercom/result-intercom.ts +108 -0
- package/src/runs/background/async-execution.ts +101 -4
- package/src/runs/background/async-job-tracker.ts +41 -6
- package/src/runs/background/async-resume.ts +28 -15
- package/src/runs/background/async-status.ts +60 -30
- package/src/runs/background/result-watcher.ts +111 -54
- package/src/runs/background/run-id-resolver.ts +83 -0
- package/src/runs/background/run-status.ts +79 -3
- package/src/runs/background/stale-run-reconciler.ts +46 -1
- package/src/runs/background/subagent-runner.ts +48 -11
- package/src/runs/foreground/chain-execution.ts +6 -0
- package/src/runs/foreground/execution.ts +4 -0
- package/src/runs/foreground/subagent-executor.ts +310 -14
- package/src/runs/shared/nested-events.ts +819 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +115 -0
- package/src/runs/shared/pi-args.ts +62 -5
- package/src/runs/shared/subagent-prompt-runtime.ts +25 -5
- package/src/shared/types.ts +95 -0
- package/src/tui/render.ts +107 -10
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type ArtifactConfig,
|
|
25
25
|
type Details,
|
|
26
26
|
type MaxOutputConfig,
|
|
27
|
+
type NestedRouteInfo,
|
|
27
28
|
type ResolvedControlConfig,
|
|
28
29
|
type SubagentRunMode,
|
|
29
30
|
ASYNC_DIR,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
getAsyncConfigPath,
|
|
34
35
|
resolveChildMaxSubagentDepth,
|
|
35
36
|
} from "../../shared/types.ts";
|
|
37
|
+
import { nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
|
|
36
38
|
|
|
37
39
|
const require = createRequire(import.meta.url);
|
|
38
40
|
const piPackageRoot = resolvePiPackageRoot();
|
|
@@ -111,6 +113,7 @@ interface AsyncChainParams {
|
|
|
111
113
|
controlConfig?: ResolvedControlConfig;
|
|
112
114
|
controlIntercomTarget?: string;
|
|
113
115
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
116
|
+
nestedRoute?: NestedRouteInfo;
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
interface AsyncSingleParams {
|
|
@@ -136,6 +139,7 @@ interface AsyncSingleParams {
|
|
|
136
139
|
controlConfig?: ResolvedControlConfig;
|
|
137
140
|
controlIntercomTarget?: string;
|
|
138
141
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
142
|
+
nestedRoute?: NestedRouteInfo;
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
interface AsyncExecutionResult {
|
|
@@ -236,6 +240,7 @@ export function executeAsyncChain(
|
|
|
236
240
|
controlConfig,
|
|
237
241
|
controlIntercomTarget,
|
|
238
242
|
childIntercomTarget,
|
|
243
|
+
nestedRoute,
|
|
239
244
|
} = params;
|
|
240
245
|
const resultMode = params.resultMode ?? "chain";
|
|
241
246
|
const chainSkills = params.chainSkills ?? [];
|
|
@@ -261,7 +266,11 @@ export function executeAsyncChain(
|
|
|
261
266
|
}
|
|
262
267
|
}
|
|
263
268
|
|
|
264
|
-
const
|
|
269
|
+
const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
|
|
270
|
+
const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
|
|
271
|
+
const asyncDir = inheritedNestedRoute
|
|
272
|
+
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
273
|
+
: path.join(ASYNC_DIR, id);
|
|
265
274
|
try {
|
|
266
275
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
267
276
|
} catch (error) {
|
|
@@ -393,7 +402,7 @@ export function executeAsyncChain(
|
|
|
393
402
|
{
|
|
394
403
|
id,
|
|
395
404
|
steps,
|
|
396
|
-
resultPath: path.join(RESULTS_DIR, `${id}.json`),
|
|
405
|
+
resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
|
|
397
406
|
cwd: runnerCwd,
|
|
398
407
|
placeholder: "{previous}",
|
|
399
408
|
maxOutput,
|
|
@@ -411,6 +420,13 @@ export function executeAsyncChain(
|
|
|
411
420
|
controlIntercomTarget,
|
|
412
421
|
childIntercomTargets,
|
|
413
422
|
resultMode,
|
|
423
|
+
nestedRoute: nestedRoute ?? inheritedNestedRoute,
|
|
424
|
+
nestedSelf: inheritedNestedRoute && nestedAddress ? {
|
|
425
|
+
parentRunId: nestedAddress.parentRunId,
|
|
426
|
+
parentStepIndex: nestedAddress.parentStepIndex,
|
|
427
|
+
depth: nestedAddress.depth,
|
|
428
|
+
path: nestedAddress.path,
|
|
429
|
+
} : undefined,
|
|
414
430
|
},
|
|
415
431
|
id,
|
|
416
432
|
runnerCwd,
|
|
@@ -443,6 +459,40 @@ export function executeAsyncChain(
|
|
|
443
459
|
flatStepStart++;
|
|
444
460
|
}
|
|
445
461
|
}
|
|
462
|
+
if (inheritedNestedRoute && nestedAddress) {
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
try {
|
|
465
|
+
writeNestedEvent(inheritedNestedRoute, {
|
|
466
|
+
type: "subagent.nested.started",
|
|
467
|
+
ts: now,
|
|
468
|
+
parentRunId: nestedAddress.parentRunId,
|
|
469
|
+
parentStepIndex: nestedAddress.parentStepIndex,
|
|
470
|
+
child: {
|
|
471
|
+
id,
|
|
472
|
+
parentRunId: nestedAddress.parentRunId,
|
|
473
|
+
parentStepIndex: nestedAddress.parentStepIndex,
|
|
474
|
+
depth: nestedAddress.depth,
|
|
475
|
+
path: nestedAddress.path,
|
|
476
|
+
asyncDir,
|
|
477
|
+
pid: spawnResult.pid,
|
|
478
|
+
ownerIntercomTarget: process.env.PI_SUBAGENT_INTERCOM_SESSION_NAME,
|
|
479
|
+
leafIntercomTarget: childIntercomTargets?.[0],
|
|
480
|
+
intercomTarget: childIntercomTargets?.[0],
|
|
481
|
+
ownerState: "live",
|
|
482
|
+
mode: resultMode,
|
|
483
|
+
state: "running",
|
|
484
|
+
agent: firstAgents[0],
|
|
485
|
+
agents: flatAgents,
|
|
486
|
+
chainStepCount: chain.length,
|
|
487
|
+
parallelGroups,
|
|
488
|
+
startedAt: now,
|
|
489
|
+
lastUpdate: now,
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
} catch (error) {
|
|
493
|
+
console.error("Failed to emit nested async start event:", error);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
446
496
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
447
497
|
id,
|
|
448
498
|
pid: spawnResult.pid,
|
|
@@ -460,6 +510,7 @@ export function executeAsyncChain(
|
|
|
460
510
|
parallelGroups,
|
|
461
511
|
cwd: runnerCwd,
|
|
462
512
|
asyncDir,
|
|
513
|
+
nestedRoute,
|
|
463
514
|
});
|
|
464
515
|
}
|
|
465
516
|
|
|
@@ -499,6 +550,7 @@ export function executeAsyncSingle(
|
|
|
499
550
|
controlConfig,
|
|
500
551
|
controlIntercomTarget,
|
|
501
552
|
childIntercomTarget,
|
|
553
|
+
nestedRoute,
|
|
502
554
|
} = params;
|
|
503
555
|
const task = params.task ?? "";
|
|
504
556
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
@@ -512,7 +564,11 @@ export function executeAsyncSingle(
|
|
|
512
564
|
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
|
|
513
565
|
}
|
|
514
566
|
|
|
515
|
-
const
|
|
567
|
+
const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
|
|
568
|
+
const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
|
|
569
|
+
const asyncDir = inheritedNestedRoute
|
|
570
|
+
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
571
|
+
: path.join(ASYNC_DIR, id);
|
|
516
572
|
try {
|
|
517
573
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
518
574
|
} catch (error) {
|
|
@@ -564,7 +620,7 @@ export function executeAsyncSingle(
|
|
|
564
620
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
|
|
565
621
|
},
|
|
566
622
|
],
|
|
567
|
-
resultPath: path.join(RESULTS_DIR, `${id}.json`),
|
|
623
|
+
resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
|
|
568
624
|
cwd: runnerCwd,
|
|
569
625
|
placeholder: "{previous}",
|
|
570
626
|
maxOutput,
|
|
@@ -582,6 +638,13 @@ export function executeAsyncSingle(
|
|
|
582
638
|
controlIntercomTarget,
|
|
583
639
|
childIntercomTargets: childIntercomTarget ? [childIntercomTarget(agent, 0)] : undefined,
|
|
584
640
|
resultMode: "single",
|
|
641
|
+
nestedRoute: nestedRoute ?? inheritedNestedRoute,
|
|
642
|
+
nestedSelf: inheritedNestedRoute && nestedAddress ? {
|
|
643
|
+
parentRunId: nestedAddress.parentRunId,
|
|
644
|
+
parentStepIndex: nestedAddress.parentStepIndex,
|
|
645
|
+
depth: nestedAddress.depth,
|
|
646
|
+
path: nestedAddress.path,
|
|
647
|
+
} : undefined,
|
|
585
648
|
},
|
|
586
649
|
id,
|
|
587
650
|
runnerCwd,
|
|
@@ -596,6 +659,39 @@ export function executeAsyncSingle(
|
|
|
596
659
|
}
|
|
597
660
|
|
|
598
661
|
if (spawnResult.pid) {
|
|
662
|
+
if (inheritedNestedRoute && nestedAddress) {
|
|
663
|
+
const now = Date.now();
|
|
664
|
+
try {
|
|
665
|
+
writeNestedEvent(inheritedNestedRoute, {
|
|
666
|
+
type: "subagent.nested.started",
|
|
667
|
+
ts: now,
|
|
668
|
+
parentRunId: nestedAddress.parentRunId,
|
|
669
|
+
parentStepIndex: nestedAddress.parentStepIndex,
|
|
670
|
+
child: {
|
|
671
|
+
id,
|
|
672
|
+
parentRunId: nestedAddress.parentRunId,
|
|
673
|
+
parentStepIndex: nestedAddress.parentStepIndex,
|
|
674
|
+
depth: nestedAddress.depth,
|
|
675
|
+
path: nestedAddress.path,
|
|
676
|
+
asyncDir,
|
|
677
|
+
pid: spawnResult.pid,
|
|
678
|
+
ownerIntercomTarget: process.env.PI_SUBAGENT_INTERCOM_SESSION_NAME,
|
|
679
|
+
leafIntercomTarget: childIntercomTarget?.(agent, 0),
|
|
680
|
+
intercomTarget: childIntercomTarget?.(agent, 0),
|
|
681
|
+
ownerState: "live",
|
|
682
|
+
mode: "single",
|
|
683
|
+
state: "running",
|
|
684
|
+
agent,
|
|
685
|
+
agents: [agent],
|
|
686
|
+
chainStepCount: 1,
|
|
687
|
+
startedAt: now,
|
|
688
|
+
lastUpdate: now,
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
} catch (error) {
|
|
692
|
+
console.error("Failed to emit nested async start event:", error);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
599
695
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
600
696
|
id,
|
|
601
697
|
pid: spawnResult.pid,
|
|
@@ -605,6 +701,7 @@ export function executeAsyncSingle(
|
|
|
605
701
|
task: task?.slice(0, 50),
|
|
606
702
|
cwd: runnerCwd,
|
|
607
703
|
asyncDir,
|
|
704
|
+
nestedRoute,
|
|
608
705
|
});
|
|
609
706
|
}
|
|
610
707
|
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
} from "../../shared/types.ts";
|
|
16
16
|
import { readStatus } from "../../shared/utils.ts";
|
|
17
17
|
import { normalizeParallelGroups } from "./parallel-groups.ts";
|
|
18
|
-
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
18
|
+
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
19
|
+
import { hasLiveNestedDescendants, updateAsyncJobNestedProjection } from "../shared/nested-events.ts";
|
|
19
20
|
|
|
20
21
|
interface AsyncJobTrackerOptions {
|
|
21
22
|
completionRetentionMs?: number;
|
|
@@ -38,9 +39,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
38
39
|
renderWidget(ctx, jobs);
|
|
39
40
|
ctx.ui.requestRender?.();
|
|
40
41
|
};
|
|
41
|
-
const
|
|
42
|
+
const cancelCleanup = (asyncId: string) => {
|
|
42
43
|
const existingTimer = state.cleanupTimers.get(asyncId);
|
|
43
|
-
if (existingTimer)
|
|
44
|
+
if (!existingTimer) return;
|
|
45
|
+
clearTimeout(existingTimer);
|
|
46
|
+
state.cleanupTimers.delete(asyncId);
|
|
47
|
+
};
|
|
48
|
+
const scheduleCleanup = (asyncId: string) => {
|
|
49
|
+
cancelCleanup(asyncId);
|
|
44
50
|
const timer = setTimeout(() => {
|
|
45
51
|
state.cleanupTimers.delete(asyncId);
|
|
46
52
|
state.asyncJobs.delete(asyncId);
|
|
@@ -121,8 +127,27 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
121
127
|
let widgetChanged = false;
|
|
122
128
|
for (const job of state.asyncJobs.values()) {
|
|
123
129
|
const widgetStateBefore = widgetRenderKey(job);
|
|
130
|
+
let nestedRefreshFailed = false;
|
|
131
|
+
const refreshNestedProjection = () => {
|
|
132
|
+
try {
|
|
133
|
+
updateAsyncJobNestedProjection(job);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
nestedRefreshFailed = true;
|
|
136
|
+
console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const reconcileNestedDescendants = () => {
|
|
140
|
+
try {
|
|
141
|
+
if (job.nestedRoute) reconcileNestedAsyncDescendants(job.nestedRoute, { resultsDir, kill: options.kill, now: options.now });
|
|
142
|
+
} catch (error) {
|
|
143
|
+
nestedRefreshFailed = true;
|
|
144
|
+
console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
|
|
145
|
+
}
|
|
146
|
+
refreshNestedProjection();
|
|
147
|
+
};
|
|
124
148
|
try {
|
|
125
149
|
emitNewControlEvents(job);
|
|
150
|
+
reconcileNestedDescendants();
|
|
126
151
|
const reconciliation = reconcileAsyncRun(job.asyncDir, {
|
|
127
152
|
resultsDir,
|
|
128
153
|
kill: options.kill,
|
|
@@ -143,6 +168,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
143
168
|
if (status) {
|
|
144
169
|
const previousStatus = job.status;
|
|
145
170
|
job.status = status.state;
|
|
171
|
+
if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused") cancelCleanup(job.asyncId);
|
|
146
172
|
job.sessionId = status.sessionId ?? job.sessionId;
|
|
147
173
|
job.activityState = status.activityState;
|
|
148
174
|
job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
|
|
@@ -169,6 +195,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
169
195
|
job.activeParallelGroup = Boolean(activeGroup);
|
|
170
196
|
job.agents = visibleSteps.map((step) => step.agent);
|
|
171
197
|
job.steps = visibleSteps;
|
|
198
|
+
refreshNestedProjection();
|
|
172
199
|
job.stepsTotal = visibleSteps.length;
|
|
173
200
|
job.runningSteps = visibleSteps.filter((step) => step.status === "running").length;
|
|
174
201
|
job.completedSteps = visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length;
|
|
@@ -178,7 +205,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
178
205
|
job.outputFile = status.outputFile ?? job.outputFile;
|
|
179
206
|
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
180
207
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
181
|
-
if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
208
|
+
if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
182
209
|
scheduleCleanup(job.asyncId);
|
|
183
210
|
}
|
|
184
211
|
if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
|
|
@@ -194,7 +221,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
194
221
|
job.status = "failed";
|
|
195
222
|
job.updatedAt = Date.now();
|
|
196
223
|
}
|
|
197
|
-
if (!state.cleanupTimers.has(job.asyncId)) {
|
|
224
|
+
if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) {
|
|
198
225
|
scheduleCleanup(job.asyncId);
|
|
199
226
|
}
|
|
200
227
|
}
|
|
@@ -228,6 +255,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
228
255
|
agents,
|
|
229
256
|
chainStepCount: info.chainStepCount,
|
|
230
257
|
parallelGroups: validParallelGroups,
|
|
258
|
+
nestedRoute: info.nestedRoute,
|
|
231
259
|
stepsTotal: firstGroupCount ?? agents?.length,
|
|
232
260
|
hasParallelGroups: validParallelGroups.length > 0,
|
|
233
261
|
activeParallelGroup: Boolean(firstGroupCount && firstGroupCount > 0),
|
|
@@ -245,15 +273,22 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
245
273
|
const asyncId = result.id;
|
|
246
274
|
if (!asyncId) return;
|
|
247
275
|
const job = state.asyncJobs.get(asyncId);
|
|
276
|
+
let nestedRefreshFailed = false;
|
|
248
277
|
if (job) {
|
|
249
278
|
job.status = result.success ? "complete" : "failed";
|
|
250
279
|
job.updatedAt = Date.now();
|
|
251
280
|
if (result.asyncDir) job.asyncDir = result.asyncDir;
|
|
281
|
+
try {
|
|
282
|
+
updateAsyncJobNestedProjection(job);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
nestedRefreshFailed = true;
|
|
285
|
+
console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
|
|
286
|
+
}
|
|
252
287
|
}
|
|
253
288
|
if (state.lastUiContext) {
|
|
254
289
|
rerenderWidget(state.lastUiContext);
|
|
255
290
|
}
|
|
256
|
-
scheduleCleanup(asyncId);
|
|
291
|
+
if (!nestedRefreshFailed && !hasLiveNestedDescendants(job?.nestedChildren)) scheduleCleanup(asyncId);
|
|
257
292
|
};
|
|
258
293
|
|
|
259
294
|
const resetJobs = (ctx?: ExtensionContext) => {
|
|
@@ -149,6 +149,29 @@ function exactResultPath(resultsDir: string, runId: string): string | null {
|
|
|
149
149
|
return fs.existsSync(resultPath) ? resultPath : null;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
export function findAsyncRunPrefixMatches(prefix: string, asyncDirRoot: string, resultsDir: string): Array<{ id: string; location: AsyncRunLocation }> {
|
|
153
|
+
const requestedId = assertRunId(prefix, "id");
|
|
154
|
+
if (!requestedId) return [];
|
|
155
|
+
const asyncRoot = path.resolve(asyncDirRoot);
|
|
156
|
+
const resultRoot = path.resolve(resultsDir);
|
|
157
|
+
const matchingIds = [...new Set([
|
|
158
|
+
...prefixedRunIds(asyncRoot, requestedId),
|
|
159
|
+
...prefixedRunIds(resultRoot, requestedId, ".json"),
|
|
160
|
+
])].sort();
|
|
161
|
+
return matchingIds.map((id) => {
|
|
162
|
+
const asyncDir = path.join(asyncRoot, id);
|
|
163
|
+
assertInsideRoot(asyncRoot, asyncDir, "Async run directory");
|
|
164
|
+
return {
|
|
165
|
+
id,
|
|
166
|
+
location: {
|
|
167
|
+
asyncDir: fs.existsSync(asyncDir) ? asyncDir : null,
|
|
168
|
+
resultPath: exactResultPath(resultRoot, id),
|
|
169
|
+
resolvedId: id,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
152
175
|
export function resolveAsyncRunLocation(params: AsyncResumeParams, asyncDirRoot: string, resultsDir: string): AsyncRunLocation {
|
|
153
176
|
const asyncRoot = path.resolve(asyncDirRoot);
|
|
154
177
|
const resultRoot = path.resolve(resultsDir);
|
|
@@ -175,22 +198,12 @@ export function resolveAsyncRunLocation(params: AsyncResumeParams, asyncDirRoot:
|
|
|
175
198
|
};
|
|
176
199
|
}
|
|
177
200
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (matchingIds.length === 0) return { asyncDir: null, resultPath: null, resolvedId: requestedId };
|
|
183
|
-
if (matchingIds.length > 1) {
|
|
184
|
-
throw new Error(`Ambiguous async run id prefix '${requestedId}' matched: ${matchingIds.join(", ")}. Provide a longer id.`);
|
|
201
|
+
const matching = findAsyncRunPrefixMatches(requestedId, asyncRoot, resultRoot);
|
|
202
|
+
if (matching.length === 0) return { asyncDir: null, resultPath: null, resolvedId: requestedId };
|
|
203
|
+
if (matching.length > 1) {
|
|
204
|
+
throw new Error(`Ambiguous async run id prefix '${requestedId}' matched: ${matching.map((match) => match.id).join(", ")}. Provide a longer id.`);
|
|
185
205
|
}
|
|
186
|
-
|
|
187
|
-
const asyncDir = path.join(asyncRoot, resolvedId);
|
|
188
|
-
assertInsideRoot(asyncRoot, asyncDir, "Async run directory");
|
|
189
|
-
return {
|
|
190
|
-
asyncDir: fs.existsSync(asyncDir) ? asyncDir : null,
|
|
191
|
-
resultPath: exactResultPath(resultRoot, resolvedId),
|
|
192
|
-
resolvedId,
|
|
193
|
-
};
|
|
206
|
+
return matching[0]!.location;
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
function resultState(result: AsyncResultFile): AsyncStatus["state"] {
|
|
@@ -2,10 +2,12 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
4
4
|
import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
|
|
5
|
-
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
|
|
5
|
+
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
|
|
6
6
|
import { readStatus } from "../../shared/utils.ts";
|
|
7
|
+
import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot } from "../shared/nested-events.ts";
|
|
8
|
+
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
7
9
|
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
8
|
-
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
10
|
+
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
9
11
|
|
|
10
12
|
interface AsyncRunStepSummary {
|
|
11
13
|
index: number;
|
|
@@ -28,6 +30,7 @@ interface AsyncRunStepSummary {
|
|
|
28
30
|
thinking?: string;
|
|
29
31
|
attemptedModels?: string[];
|
|
30
32
|
error?: string;
|
|
33
|
+
children?: NestedRunSummary[];
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export interface AsyncRunSummary {
|
|
@@ -55,6 +58,8 @@ export interface AsyncRunSummary {
|
|
|
55
58
|
outputFile?: string;
|
|
56
59
|
totalTokens?: TokenUsage;
|
|
57
60
|
sessionFile?: string;
|
|
61
|
+
nestedChildren?: NestedRunSummary[];
|
|
62
|
+
nestedWarnings?: string[];
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
interface AsyncRunListOptions {
|
|
@@ -112,7 +117,7 @@ function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { acti
|
|
|
112
117
|
};
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }): AsyncRunSummary {
|
|
120
|
+
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }, nestedWarnings: string[] = []): AsyncRunSummary {
|
|
116
121
|
if (status.sessionId !== undefined && typeof status.sessionId !== "string") {
|
|
117
122
|
throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
|
|
118
123
|
}
|
|
@@ -120,6 +125,42 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
120
125
|
const steps = status.steps ?? [];
|
|
121
126
|
const chainStepCount = status.chainStepCount ?? steps.length;
|
|
122
127
|
const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
|
|
128
|
+
let nestedChildren: NestedRunSummary[] = [];
|
|
129
|
+
if (nestedWarnings.length === 0) {
|
|
130
|
+
try {
|
|
131
|
+
nestedChildren = projectNestedRegistryForRoot(status.runId || path.basename(asyncDir))?.children ?? [];
|
|
132
|
+
} catch (error) {
|
|
133
|
+
nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const summarizedSteps = steps.map((step, index) => {
|
|
137
|
+
const stepActivityState = step.activityState;
|
|
138
|
+
const stepLastActivityAt = step.lastActivityAt;
|
|
139
|
+
return {
|
|
140
|
+
index,
|
|
141
|
+
agent: step.agent,
|
|
142
|
+
status: step.status,
|
|
143
|
+
...(stepActivityState ? { activityState: stepActivityState } : {}),
|
|
144
|
+
...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
|
|
145
|
+
...(step.currentTool ? { currentTool: step.currentTool } : {}),
|
|
146
|
+
...(step.currentToolArgs ? { currentToolArgs: step.currentToolArgs } : {}),
|
|
147
|
+
...(step.currentToolStartedAt ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
|
|
148
|
+
...(step.currentPath ? { currentPath: step.currentPath } : {}),
|
|
149
|
+
...(step.recentTools ? { recentTools: step.recentTools.map((tool) => ({ ...tool })) } : {}),
|
|
150
|
+
...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
|
|
151
|
+
...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
|
|
152
|
+
...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
|
|
153
|
+
...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
|
|
154
|
+
...(step.tokens ? { tokens: step.tokens } : {}),
|
|
155
|
+
...(step.skills ? { skills: step.skills } : {}),
|
|
156
|
+
...(step.model ? { model: step.model } : {}),
|
|
157
|
+
...(step.thinking ? { thinking: step.thinking } : {}),
|
|
158
|
+
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
159
|
+
...(step.error ? { error: step.error } : {}),
|
|
160
|
+
...(step.children?.length ? { children: step.children } : {}),
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
attachRootChildrenToSteps(status.runId || path.basename(asyncDir), summarizedSteps, nestedChildren);
|
|
123
164
|
return {
|
|
124
165
|
id: status.runId || path.basename(asyncDir),
|
|
125
166
|
asyncDir,
|
|
@@ -140,32 +181,9 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
140
181
|
currentStep: status.currentStep,
|
|
141
182
|
...(status.chainStepCount !== undefined ? { chainStepCount: status.chainStepCount } : {}),
|
|
142
183
|
...(parallelGroups.length ? { parallelGroups } : {}),
|
|
143
|
-
steps:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
index,
|
|
148
|
-
agent: step.agent,
|
|
149
|
-
status: step.status,
|
|
150
|
-
...(stepActivityState ? { activityState: stepActivityState } : {}),
|
|
151
|
-
...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
|
|
152
|
-
...(step.currentTool ? { currentTool: step.currentTool } : {}),
|
|
153
|
-
...(step.currentToolArgs ? { currentToolArgs: step.currentToolArgs } : {}),
|
|
154
|
-
...(step.currentToolStartedAt ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
|
|
155
|
-
...(step.currentPath ? { currentPath: step.currentPath } : {}),
|
|
156
|
-
...(step.recentTools ? { recentTools: step.recentTools.map((tool) => ({ ...tool })) } : {}),
|
|
157
|
-
...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
|
|
158
|
-
...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
|
|
159
|
-
...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
|
|
160
|
-
...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
|
|
161
|
-
...(step.tokens ? { tokens: step.tokens } : {}),
|
|
162
|
-
...(step.skills ? { skills: step.skills } : {}),
|
|
163
|
-
...(step.model ? { model: step.model } : {}),
|
|
164
|
-
...(step.thinking ? { thinking: step.thinking } : {}),
|
|
165
|
-
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
166
|
-
...(step.error ? { error: step.error } : {}),
|
|
167
|
-
};
|
|
168
|
-
}),
|
|
184
|
+
steps: summarizedSteps,
|
|
185
|
+
...(nestedChildren.length ? { nestedChildren } : {}),
|
|
186
|
+
...(nestedWarnings.length ? { nestedWarnings } : {}),
|
|
169
187
|
...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
|
|
170
188
|
...(status.outputFile ? { outputFile: status.outputFile } : {}),
|
|
171
189
|
...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
|
|
@@ -212,7 +230,14 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
212
230
|
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
213
231
|
const status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
|
|
214
232
|
if (!status) continue;
|
|
215
|
-
const
|
|
233
|
+
const nestedWarnings: string[] = [];
|
|
234
|
+
try {
|
|
235
|
+
const nestedRoute = findNestedRouteForRootId(status.runId || path.basename(asyncDir));
|
|
236
|
+
if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
237
|
+
} catch (error) {
|
|
238
|
+
nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
|
|
239
|
+
}
|
|
240
|
+
const summary = statusToSummary(asyncDir, status, nestedWarnings);
|
|
216
241
|
if (allowedStates && !allowedStates.has(summary.state)) continue;
|
|
217
242
|
if (options.sessionId && summary.sessionId !== options.sessionId) continue;
|
|
218
243
|
runs.push(summary);
|
|
@@ -285,7 +310,12 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
|
|
|
285
310
|
lines.push(`- ${formatRunHeader(run)}`);
|
|
286
311
|
for (const step of run.steps) {
|
|
287
312
|
lines.push(` ${formatStepLine(step)}`);
|
|
313
|
+
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", maxLines: 12 }));
|
|
288
314
|
}
|
|
315
|
+
const attached = new Set(run.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
316
|
+
const unattached = run.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
|
|
317
|
+
lines.push(...formatNestedRunStatusLines(unattached, { indent: " ", maxLines: 12 }));
|
|
318
|
+
for (const warning of run.nestedWarnings ?? []) lines.push(` Warning: ${warning}`);
|
|
289
319
|
const outputPath = formatAsyncRunOutputPath(run);
|
|
290
320
|
if (outputPath) lines.push(` output: ${shortenPath(outputPath)}`);
|
|
291
321
|
if (run.sessionFile) lines.push(` session: ${shortenPath(run.sessionFile)}`);
|