pi-subagents 0.24.3 → 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 +26 -5
- package/README.md +19 -11
- package/package.json +4 -8
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +46 -10
- package/src/agents/agent-management.ts +5 -0
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +30 -6
- package/src/agents/skills.ts +25 -23
- package/src/extension/config.ts +16 -0
- package/src/extension/fanout-child.ts +170 -0
- package/src/extension/index.ts +13 -25
- package/src/intercom/intercom-bridge.ts +2 -1
- package/src/intercom/result-intercom.ts +108 -0
- package/src/runs/background/async-execution.ts +107 -7
- package/src/runs/background/async-job-tracker.ts +57 -14
- 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 +66 -18
- package/src/runs/foreground/chain-execution.ts +6 -0
- package/src/runs/foreground/execution.ts +21 -5
- package/src/runs/foreground/subagent-executor.ts +314 -18
- package/src/runs/shared/completion-guard.ts +23 -1
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
- 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/parallel-utils.ts +1 -0
- package/src/runs/shared/pi-args.ts +67 -5
- package/src/runs/shared/run-history.ts +12 -7
- package/src/runs/shared/single-output.ts +12 -2
- package/src/runs/shared/subagent-prompt-runtime.ts +25 -5
- package/src/shared/artifacts.ts +2 -2
- package/src/shared/types.ts +95 -0
- package/src/shared/utils.ts +11 -1
- package/src/tui/render.ts +254 -153
|
@@ -11,7 +11,7 @@ import { createRequire } from "node:module";
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
13
13
|
import { applyThinkingSuffix } from "../shared/pi-args.ts";
|
|
14
|
-
import { injectSingleOutputInstruction, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
14
|
+
import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
15
15
|
import { buildChainInstructions, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
16
16
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
17
17
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
@@ -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 {
|
|
@@ -126,7 +129,7 @@ interface AsyncSingleParams {
|
|
|
126
129
|
sessionRoot?: string;
|
|
127
130
|
sessionFile?: string;
|
|
128
131
|
skills?: string[];
|
|
129
|
-
output?: string |
|
|
132
|
+
output?: string | boolean;
|
|
130
133
|
outputMode?: "inline" | "file-only";
|
|
131
134
|
modelOverride?: string;
|
|
132
135
|
availableModels?: AvailableModelInfo[];
|
|
@@ -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) {
|
|
@@ -323,6 +332,7 @@ export function executeAsyncChain(
|
|
|
323
332
|
tools: a.tools,
|
|
324
333
|
extensions: a.extensions,
|
|
325
334
|
mcpDirectTools: a.mcpDirectTools,
|
|
335
|
+
completionGuard: a.completionGuard,
|
|
326
336
|
systemPrompt,
|
|
327
337
|
systemPromptMode: a.systemPromptMode,
|
|
328
338
|
inheritProjectContext: a.inheritProjectContext,
|
|
@@ -392,7 +402,7 @@ export function executeAsyncChain(
|
|
|
392
402
|
{
|
|
393
403
|
id,
|
|
394
404
|
steps,
|
|
395
|
-
resultPath: path.join(RESULTS_DIR, `${id}.json`),
|
|
405
|
+
resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
|
|
396
406
|
cwd: runnerCwd,
|
|
397
407
|
placeholder: "{previous}",
|
|
398
408
|
maxOutput,
|
|
@@ -410,6 +420,13 @@ export function executeAsyncChain(
|
|
|
410
420
|
controlIntercomTarget,
|
|
411
421
|
childIntercomTargets,
|
|
412
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,
|
|
413
430
|
},
|
|
414
431
|
id,
|
|
415
432
|
runnerCwd,
|
|
@@ -442,6 +459,40 @@ export function executeAsyncChain(
|
|
|
442
459
|
flatStepStart++;
|
|
443
460
|
}
|
|
444
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
|
+
}
|
|
445
496
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
446
497
|
id,
|
|
447
498
|
pid: spawnResult.pid,
|
|
@@ -459,6 +510,7 @@ export function executeAsyncChain(
|
|
|
459
510
|
parallelGroups,
|
|
460
511
|
cwd: runnerCwd,
|
|
461
512
|
asyncDir,
|
|
513
|
+
nestedRoute,
|
|
462
514
|
});
|
|
463
515
|
}
|
|
464
516
|
|
|
@@ -498,6 +550,7 @@ export function executeAsyncSingle(
|
|
|
498
550
|
controlConfig,
|
|
499
551
|
controlIntercomTarget,
|
|
500
552
|
childIntercomTarget,
|
|
553
|
+
nestedRoute,
|
|
501
554
|
} = params;
|
|
502
555
|
const task = params.task ?? "";
|
|
503
556
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
@@ -511,7 +564,11 @@ export function executeAsyncSingle(
|
|
|
511
564
|
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
|
|
512
565
|
}
|
|
513
566
|
|
|
514
|
-
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);
|
|
515
572
|
try {
|
|
516
573
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
517
574
|
} catch (error) {
|
|
@@ -523,7 +580,8 @@ export function executeAsyncSingle(
|
|
|
523
580
|
};
|
|
524
581
|
}
|
|
525
582
|
|
|
526
|
-
const
|
|
583
|
+
const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
|
|
584
|
+
const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, runnerCwd);
|
|
527
585
|
const outputMode = params.outputMode ?? "inline";
|
|
528
586
|
const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
|
|
529
587
|
if (validationError) return formatAsyncStartError("single", validationError);
|
|
@@ -550,6 +608,7 @@ export function executeAsyncSingle(
|
|
|
550
608
|
tools: agentConfig.tools,
|
|
551
609
|
extensions: agentConfig.extensions,
|
|
552
610
|
mcpDirectTools: agentConfig.mcpDirectTools,
|
|
611
|
+
completionGuard: agentConfig.completionGuard,
|
|
553
612
|
systemPrompt,
|
|
554
613
|
systemPromptMode: agentConfig.systemPromptMode,
|
|
555
614
|
inheritProjectContext: agentConfig.inheritProjectContext,
|
|
@@ -561,7 +620,7 @@ export function executeAsyncSingle(
|
|
|
561
620
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
|
|
562
621
|
},
|
|
563
622
|
],
|
|
564
|
-
resultPath: path.join(RESULTS_DIR, `${id}.json`),
|
|
623
|
+
resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
|
|
565
624
|
cwd: runnerCwd,
|
|
566
625
|
placeholder: "{previous}",
|
|
567
626
|
maxOutput,
|
|
@@ -579,6 +638,13 @@ export function executeAsyncSingle(
|
|
|
579
638
|
controlIntercomTarget,
|
|
580
639
|
childIntercomTargets: childIntercomTarget ? [childIntercomTarget(agent, 0)] : undefined,
|
|
581
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,
|
|
582
648
|
},
|
|
583
649
|
id,
|
|
584
650
|
runnerCwd,
|
|
@@ -593,6 +659,39 @@ export function executeAsyncSingle(
|
|
|
593
659
|
}
|
|
594
660
|
|
|
595
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
|
+
}
|
|
596
695
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
597
696
|
id,
|
|
598
697
|
pid: spawnResult.pid,
|
|
@@ -602,6 +701,7 @@ export function executeAsyncSingle(
|
|
|
602
701
|
task: task?.slice(0, 50),
|
|
603
702
|
cwd: runnerCwd,
|
|
604
703
|
asyncDir,
|
|
704
|
+
nestedRoute,
|
|
605
705
|
});
|
|
606
706
|
}
|
|
607
707
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { renderWidget } from "../../tui/render.ts";
|
|
4
|
+
import { renderWidget, widgetRenderKey } from "../../tui/render.ts";
|
|
5
5
|
import { formatControlNoticeMessage } from "../shared/subagent-control.ts";
|
|
6
6
|
import {
|
|
7
7
|
type AsyncJobState,
|
|
@@ -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);
|
|
@@ -118,9 +124,30 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
118
124
|
return;
|
|
119
125
|
}
|
|
120
126
|
|
|
127
|
+
let widgetChanged = false;
|
|
121
128
|
for (const job of state.asyncJobs.values()) {
|
|
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
|
+
};
|
|
122
148
|
try {
|
|
123
149
|
emitNewControlEvents(job);
|
|
150
|
+
reconcileNestedDescendants();
|
|
124
151
|
const reconciliation = reconcileAsyncRun(job.asyncDir, {
|
|
125
152
|
resultsDir,
|
|
126
153
|
kill: options.kill,
|
|
@@ -141,6 +168,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
141
168
|
if (status) {
|
|
142
169
|
const previousStatus = job.status;
|
|
143
170
|
job.status = status.state;
|
|
171
|
+
if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused") cancelCleanup(job.asyncId);
|
|
144
172
|
job.sessionId = status.sessionId ?? job.sessionId;
|
|
145
173
|
job.activityState = status.activityState;
|
|
146
174
|
job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
|
|
@@ -153,7 +181,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
153
181
|
job.currentStep = status.currentStep ?? job.currentStep;
|
|
154
182
|
job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
|
|
155
183
|
job.startedAt = status.startedAt ?? job.startedAt;
|
|
156
|
-
job.updatedAt = status.lastUpdate
|
|
184
|
+
if (status.lastUpdate !== undefined) job.updatedAt = status.lastUpdate;
|
|
157
185
|
if (status.steps?.length) {
|
|
158
186
|
const groups = normalizeParallelGroups(status.parallelGroups, status.steps.length, status.chainStepCount ?? status.steps.length);
|
|
159
187
|
job.parallelGroups = groups.length ? groups : job.parallelGroups;
|
|
@@ -167,6 +195,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
167
195
|
job.activeParallelGroup = Boolean(activeGroup);
|
|
168
196
|
job.agents = visibleSteps.map((step) => step.agent);
|
|
169
197
|
job.steps = visibleSteps;
|
|
198
|
+
refreshNestedProjection();
|
|
170
199
|
job.stepsTotal = visibleSteps.length;
|
|
171
200
|
job.runningSteps = visibleSteps.filter((step) => step.status === "running").length;
|
|
172
201
|
job.completedSteps = visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length;
|
|
@@ -176,24 +205,30 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
176
205
|
job.outputFile = status.outputFile ?? job.outputFile;
|
|
177
206
|
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
178
207
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
179
|
-
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))) {
|
|
180
209
|
scheduleCleanup(job.asyncId);
|
|
181
210
|
}
|
|
211
|
+
if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
|
|
182
212
|
continue;
|
|
183
213
|
}
|
|
184
|
-
|
|
185
|
-
|
|
214
|
+
if (job.status === "queued") {
|
|
215
|
+
job.status = "running";
|
|
216
|
+
job.updatedAt = Date.now();
|
|
217
|
+
}
|
|
186
218
|
} catch (error) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
219
|
+
if (job.status !== "failed") {
|
|
220
|
+
console.error(`Failed to read async status for '${job.asyncDir}':`, error);
|
|
221
|
+
job.status = "failed";
|
|
222
|
+
job.updatedAt = Date.now();
|
|
223
|
+
}
|
|
224
|
+
if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) {
|
|
191
225
|
scheduleCleanup(job.asyncId);
|
|
192
226
|
}
|
|
193
227
|
}
|
|
228
|
+
if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
|
|
194
229
|
}
|
|
195
230
|
|
|
196
|
-
if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext);
|
|
231
|
+
if (widgetChanged && state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext);
|
|
197
232
|
}, pollIntervalMs);
|
|
198
233
|
state.poller.unref?.();
|
|
199
234
|
};
|
|
@@ -220,6 +255,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
220
255
|
agents,
|
|
221
256
|
chainStepCount: info.chainStepCount,
|
|
222
257
|
parallelGroups: validParallelGroups,
|
|
258
|
+
nestedRoute: info.nestedRoute,
|
|
223
259
|
stepsTotal: firstGroupCount ?? agents?.length,
|
|
224
260
|
hasParallelGroups: validParallelGroups.length > 0,
|
|
225
261
|
activeParallelGroup: Boolean(firstGroupCount && firstGroupCount > 0),
|
|
@@ -237,15 +273,22 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
237
273
|
const asyncId = result.id;
|
|
238
274
|
if (!asyncId) return;
|
|
239
275
|
const job = state.asyncJobs.get(asyncId);
|
|
276
|
+
let nestedRefreshFailed = false;
|
|
240
277
|
if (job) {
|
|
241
278
|
job.status = result.success ? "complete" : "failed";
|
|
242
279
|
job.updatedAt = Date.now();
|
|
243
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
|
+
}
|
|
244
287
|
}
|
|
245
288
|
if (state.lastUiContext) {
|
|
246
289
|
rerenderWidget(state.lastUiContext);
|
|
247
290
|
}
|
|
248
|
-
scheduleCleanup(asyncId);
|
|
291
|
+
if (!nestedRefreshFailed && !hasLiveNestedDescendants(job?.nestedChildren)) scheduleCleanup(asyncId);
|
|
249
292
|
};
|
|
250
293
|
|
|
251
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)}`);
|