pi-subagents 0.24.4 → 0.27.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 +29 -0
- package/README.md +145 -27
- package/package.json +1 -1
- package/prompts/parallel-context-build.md +3 -1
- package/prompts/parallel-handoff-plan.md +3 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +71 -20
- package/src/agents/agent-management.ts +57 -15
- package/src/agents/agent-serializer.ts +3 -2
- package/src/agents/agents.ts +47 -16
- package/src/agents/chain-serializer.ts +120 -0
- package/src/extension/fanout-child.ts +171 -0
- package/src/extension/index.ts +7 -2
- package/src/extension/schemas.ts +138 -5
- package/src/intercom/result-intercom.ts +108 -0
- package/src/runs/background/async-execution.ts +185 -10
- 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 +71 -31
- 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 +89 -4
- package/src/runs/background/stale-run-reconciler.ts +46 -1
- package/src/runs/background/subagent-runner.ts +648 -42
- package/src/runs/foreground/chain-execution.ts +331 -118
- package/src/runs/foreground/execution.ts +226 -10
- package/src/runs/foreground/subagent-executor.ts +377 -14
- package/src/runs/shared/acceptance-contract.ts +291 -0
- package/src/runs/shared/acceptance-evaluation.ts +221 -0
- package/src/runs/shared/acceptance-finalization.ts +161 -0
- package/src/runs/shared/acceptance-reports.ts +127 -0
- package/src/runs/shared/acceptance.ts +22 -0
- package/src/runs/shared/chain-outputs.ts +101 -0
- package/src/runs/shared/completion-guard.ts +26 -3
- package/src/runs/shared/dynamic-fanout.ts +293 -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 +31 -1
- package/src/runs/shared/pi-args.ts +73 -5
- package/src/runs/shared/structured-output.ts +77 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +77 -7
- package/src/runs/shared/workflow-graph.ts +206 -0
- package/src/shared/formatters.ts +2 -2
- package/src/shared/settings.ts +53 -4
- package/src/shared/types.ts +345 -0
- package/src/slash/slash-commands.ts +41 -3
- package/src/tui/render.ts +268 -43
|
@@ -12,7 +12,7 @@ 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
14
|
import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
15
|
-
import { buildChainInstructions, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
15
|
+
import { buildChainInstructions, isDynamicParallelStep, 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";
|
|
18
18
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
@@ -20,10 +20,16 @@ import { resolveChildCwd } from "../../shared/utils.ts";
|
|
|
20
20
|
import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "../shared/model-fallback.ts";
|
|
21
21
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
22
22
|
import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
|
|
23
|
+
import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
|
|
24
|
+
import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
|
|
25
|
+
import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
|
|
26
|
+
import { resolveEffectiveAcceptance } from "../shared/acceptance.ts";
|
|
23
27
|
import {
|
|
28
|
+
type AcceptanceInput,
|
|
24
29
|
type ArtifactConfig,
|
|
25
30
|
type Details,
|
|
26
31
|
type MaxOutputConfig,
|
|
32
|
+
type NestedRouteInfo,
|
|
27
33
|
type ResolvedControlConfig,
|
|
28
34
|
type SubagentRunMode,
|
|
29
35
|
ASYNC_DIR,
|
|
@@ -33,6 +39,7 @@ import {
|
|
|
33
39
|
getAsyncConfigPath,
|
|
34
40
|
resolveChildMaxSubagentDepth,
|
|
35
41
|
} from "../../shared/types.ts";
|
|
42
|
+
import { nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
|
|
36
43
|
|
|
37
44
|
const require = createRequire(import.meta.url);
|
|
38
45
|
const piPackageRoot = resolvePiPackageRoot();
|
|
@@ -105,12 +112,15 @@ interface AsyncChainParams {
|
|
|
105
112
|
sessionRoot?: string;
|
|
106
113
|
chainSkills?: string[];
|
|
107
114
|
sessionFilesByFlatIndex?: (string | undefined)[];
|
|
115
|
+
dynamicFanoutMaxItems?: number;
|
|
108
116
|
maxSubagentDepth: number;
|
|
109
117
|
worktreeSetupHook?: string;
|
|
110
118
|
worktreeSetupHookTimeoutMs?: number;
|
|
111
119
|
controlConfig?: ResolvedControlConfig;
|
|
112
120
|
controlIntercomTarget?: string;
|
|
113
121
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
122
|
+
nestedRoute?: NestedRouteInfo;
|
|
123
|
+
acceptance?: AcceptanceInput;
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
interface AsyncSingleParams {
|
|
@@ -136,6 +146,8 @@ interface AsyncSingleParams {
|
|
|
136
146
|
controlConfig?: ResolvedControlConfig;
|
|
137
147
|
controlIntercomTarget?: string;
|
|
138
148
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
149
|
+
nestedRoute?: NestedRouteInfo;
|
|
150
|
+
acceptance?: AcceptanceInput;
|
|
139
151
|
}
|
|
140
152
|
|
|
141
153
|
interface AsyncExecutionResult {
|
|
@@ -236,6 +248,7 @@ export function executeAsyncChain(
|
|
|
236
248
|
controlConfig,
|
|
237
249
|
controlIntercomTarget,
|
|
238
250
|
childIntercomTarget,
|
|
251
|
+
nestedRoute,
|
|
239
252
|
} = params;
|
|
240
253
|
const resultMode = params.resultMode ?? "chain";
|
|
241
254
|
const chainSkills = params.chainSkills ?? [];
|
|
@@ -243,12 +256,25 @@ export function executeAsyncChain(
|
|
|
243
256
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
244
257
|
const firstStep = chain[0];
|
|
245
258
|
const originalTask = params.task ?? (firstStep
|
|
246
|
-
? (isParallelStep(firstStep)
|
|
259
|
+
? (isParallelStep(firstStep)
|
|
260
|
+
? firstStep.parallel[0]?.task
|
|
261
|
+
: isDynamicParallelStep(firstStep)
|
|
262
|
+
? firstStep.parallel.task
|
|
263
|
+
: (firstStep as SequentialStep).task)
|
|
247
264
|
: undefined);
|
|
265
|
+
try {
|
|
266
|
+
validateChainOutputBindings(chain, { maxItems: params.dynamicFanoutMaxItems });
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error instanceof ChainOutputValidationError) return formatAsyncStartError(resultMode, error.message);
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: chain });
|
|
248
272
|
|
|
249
273
|
for (const s of chain) {
|
|
250
274
|
const stepAgents = isParallelStep(s)
|
|
251
275
|
? s.parallel.map((t) => t.agent)
|
|
276
|
+
: isDynamicParallelStep(s)
|
|
277
|
+
? [s.parallel.agent]
|
|
252
278
|
: [(s as SequentialStep).agent];
|
|
253
279
|
for (const agentName of stepAgents) {
|
|
254
280
|
if (!agents.find((x) => x.name === agentName)) {
|
|
@@ -261,7 +287,11 @@ export function executeAsyncChain(
|
|
|
261
287
|
}
|
|
262
288
|
}
|
|
263
289
|
|
|
264
|
-
const
|
|
290
|
+
const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
|
|
291
|
+
const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
|
|
292
|
+
const asyncDir = inheritedNestedRoute
|
|
293
|
+
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
294
|
+
: path.join(ASYNC_DIR, id);
|
|
265
295
|
try {
|
|
266
296
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
267
297
|
} catch (error) {
|
|
@@ -307,13 +337,20 @@ export function executeAsyncChain(
|
|
|
307
337
|
const outputPath = resolveSingleOutputPath(behavior.output, ctx.cwd, instructionCwd);
|
|
308
338
|
const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
|
|
309
339
|
if (validationError) throw new AsyncStartValidationError(validationError);
|
|
310
|
-
|
|
340
|
+
let taskTemplate = s.task ?? "{previous}";
|
|
341
|
+
taskTemplate = taskTemplate.replace(/\{task\}/g, originalTask ?? "");
|
|
342
|
+
taskTemplate = taskTemplate.replace(/\{chain_dir\}/g, runnerCwd);
|
|
343
|
+
const task = injectSingleOutputInstruction(`${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`, outputPath);
|
|
311
344
|
|
|
312
345
|
const primaryModel = resolveModelCandidate(behavior.model ?? a.model, availableModels, ctx.currentModelProvider);
|
|
313
346
|
const model = applyThinkingSuffix(primaryModel, a.thinking);
|
|
314
347
|
return {
|
|
315
348
|
agent: s.agent,
|
|
316
349
|
task,
|
|
350
|
+
phase: s.phase,
|
|
351
|
+
label: s.label,
|
|
352
|
+
outputName: s.as,
|
|
353
|
+
structured: Boolean(s.outputSchema),
|
|
317
354
|
cwd: stepCwd,
|
|
318
355
|
model,
|
|
319
356
|
thinking: resolveEffectiveThinking(model, a.thinking),
|
|
@@ -333,6 +370,16 @@ export function executeAsyncChain(
|
|
|
333
370
|
outputMode: behavior.outputMode,
|
|
334
371
|
sessionFile,
|
|
335
372
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
|
|
373
|
+
effectiveAcceptance: resolveEffectiveAcceptance({
|
|
374
|
+
explicit: s.acceptance,
|
|
375
|
+
agentName: s.agent,
|
|
376
|
+
task: s.task,
|
|
377
|
+
mode: resultMode,
|
|
378
|
+
async: true,
|
|
379
|
+
dynamic: false,
|
|
380
|
+
}),
|
|
381
|
+
...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
|
|
382
|
+
...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output")) } : {}),
|
|
336
383
|
};
|
|
337
384
|
};
|
|
338
385
|
|
|
@@ -373,6 +420,24 @@ export function executeAsyncChain(
|
|
|
373
420
|
worktree: s.worktree,
|
|
374
421
|
};
|
|
375
422
|
}
|
|
423
|
+
if (isDynamicParallelStep(s)) {
|
|
424
|
+
const agent = agents.find((candidate) => candidate.name === s.parallel.agent)!;
|
|
425
|
+
const behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agent, buildStepOverrides(s.parallel), chainSkills), s.parallel.task, originalTask);
|
|
426
|
+
const progressPrecreated = behavior.progress;
|
|
427
|
+
if (progressPrecreated) {
|
|
428
|
+
writeInitialProgressFile(runnerCwd);
|
|
429
|
+
progressInstructionCreated = true;
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
expand: s.expand,
|
|
433
|
+
parallel: buildSeqStep(s.parallel as SequentialStep, undefined, undefined, progressPrecreated, behavior),
|
|
434
|
+
collect: s.collect,
|
|
435
|
+
concurrency: s.concurrency,
|
|
436
|
+
failFast: s.failFast,
|
|
437
|
+
phase: s.phase,
|
|
438
|
+
label: s.label,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
376
441
|
return buildSeqStep(s as SequentialStep, nextSessionFile());
|
|
377
442
|
});
|
|
378
443
|
} catch (error) {
|
|
@@ -382,6 +447,10 @@ export function executeAsyncChain(
|
|
|
382
447
|
let childTargetIndex = 0;
|
|
383
448
|
const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
|
|
384
449
|
if ("parallel" in step) {
|
|
450
|
+
if (!Array.isArray(step.parallel)) {
|
|
451
|
+
childTargetIndex++;
|
|
452
|
+
return [undefined];
|
|
453
|
+
}
|
|
385
454
|
return step.parallel.map((task) => childIntercomTarget(task.agent, childTargetIndex++));
|
|
386
455
|
}
|
|
387
456
|
return [childIntercomTarget(step.agent, childTargetIndex++)];
|
|
@@ -393,7 +462,7 @@ export function executeAsyncChain(
|
|
|
393
462
|
{
|
|
394
463
|
id,
|
|
395
464
|
steps,
|
|
396
|
-
resultPath: path.join(RESULTS_DIR, `${id}.json`),
|
|
465
|
+
resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
|
|
397
466
|
cwd: runnerCwd,
|
|
398
467
|
placeholder: "{previous}",
|
|
399
468
|
maxOutput,
|
|
@@ -411,6 +480,15 @@ export function executeAsyncChain(
|
|
|
411
480
|
controlIntercomTarget,
|
|
412
481
|
childIntercomTargets,
|
|
413
482
|
resultMode,
|
|
483
|
+
dynamicFanoutMaxItems: params.dynamicFanoutMaxItems,
|
|
484
|
+
workflowGraph,
|
|
485
|
+
nestedRoute: nestedRoute ?? inheritedNestedRoute,
|
|
486
|
+
nestedSelf: inheritedNestedRoute && nestedAddress ? {
|
|
487
|
+
parentRunId: nestedAddress.parentRunId,
|
|
488
|
+
parentStepIndex: nestedAddress.parentStepIndex,
|
|
489
|
+
depth: nestedAddress.depth,
|
|
490
|
+
path: nestedAddress.path,
|
|
491
|
+
} : undefined,
|
|
414
492
|
},
|
|
415
493
|
id,
|
|
416
494
|
runnerCwd,
|
|
@@ -428,6 +506,8 @@ export function executeAsyncChain(
|
|
|
428
506
|
const firstStep = chain[0];
|
|
429
507
|
const firstAgents = isParallelStep(firstStep)
|
|
430
508
|
? firstStep.parallel.map((t) => t.agent)
|
|
509
|
+
: isDynamicParallelStep(firstStep)
|
|
510
|
+
? [firstStep.parallel.agent]
|
|
431
511
|
: [(firstStep as SequentialStep).agent];
|
|
432
512
|
const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
|
|
433
513
|
const flatAgents: string[] = [];
|
|
@@ -438,11 +518,49 @@ export function executeAsyncChain(
|
|
|
438
518
|
parallelGroups.push({ start: flatStepStart, count: step.parallel.length, stepIndex });
|
|
439
519
|
flatAgents.push(...step.parallel.map((task) => task.agent));
|
|
440
520
|
flatStepStart += step.parallel.length;
|
|
521
|
+
} else if (isDynamicParallelStep(step)) {
|
|
522
|
+
parallelGroups.push({ start: flatStepStart, count: 1, stepIndex });
|
|
523
|
+
flatAgents.push(step.parallel.agent);
|
|
524
|
+
flatStepStart++;
|
|
441
525
|
} else {
|
|
442
526
|
flatAgents.push((step as SequentialStep).agent);
|
|
443
527
|
flatStepStart++;
|
|
444
528
|
}
|
|
445
529
|
}
|
|
530
|
+
if (inheritedNestedRoute && nestedAddress) {
|
|
531
|
+
const now = Date.now();
|
|
532
|
+
try {
|
|
533
|
+
writeNestedEvent(inheritedNestedRoute, {
|
|
534
|
+
type: "subagent.nested.started",
|
|
535
|
+
ts: now,
|
|
536
|
+
parentRunId: nestedAddress.parentRunId,
|
|
537
|
+
parentStepIndex: nestedAddress.parentStepIndex,
|
|
538
|
+
child: {
|
|
539
|
+
id,
|
|
540
|
+
parentRunId: nestedAddress.parentRunId,
|
|
541
|
+
parentStepIndex: nestedAddress.parentStepIndex,
|
|
542
|
+
depth: nestedAddress.depth,
|
|
543
|
+
path: nestedAddress.path,
|
|
544
|
+
asyncDir,
|
|
545
|
+
pid: spawnResult.pid,
|
|
546
|
+
ownerIntercomTarget: process.env.PI_SUBAGENT_INTERCOM_SESSION_NAME,
|
|
547
|
+
leafIntercomTarget: childIntercomTargets?.[0],
|
|
548
|
+
intercomTarget: childIntercomTargets?.[0],
|
|
549
|
+
ownerState: "live",
|
|
550
|
+
mode: resultMode,
|
|
551
|
+
state: "running",
|
|
552
|
+
agent: firstAgents[0],
|
|
553
|
+
agents: flatAgents,
|
|
554
|
+
chainStepCount: chain.length,
|
|
555
|
+
parallelGroups,
|
|
556
|
+
startedAt: now,
|
|
557
|
+
lastUpdate: now,
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.error("Failed to emit nested async start event:", error);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
446
564
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
447
565
|
id,
|
|
448
566
|
pid: spawnResult.pid,
|
|
@@ -452,26 +570,30 @@ export function executeAsyncChain(
|
|
|
452
570
|
agents: flatAgents,
|
|
453
571
|
task: isParallelStep(firstStep)
|
|
454
572
|
? firstStep.parallel[0]?.task?.slice(0, 50)
|
|
573
|
+
: isDynamicParallelStep(firstStep)
|
|
574
|
+
? firstStep.parallel.task?.slice(0, 50)
|
|
455
575
|
: (firstStep as SequentialStep).task?.slice(0, 50),
|
|
456
576
|
chain: chain.map((s) =>
|
|
457
|
-
isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : (s as SequentialStep).agent,
|
|
577
|
+
isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
|
|
458
578
|
),
|
|
459
579
|
chainStepCount: chain.length,
|
|
460
580
|
parallelGroups,
|
|
581
|
+
workflowGraph,
|
|
461
582
|
cwd: runnerCwd,
|
|
462
583
|
asyncDir,
|
|
584
|
+
nestedRoute,
|
|
463
585
|
});
|
|
464
586
|
}
|
|
465
587
|
|
|
466
588
|
const chainDesc = chain
|
|
467
589
|
.map((s) =>
|
|
468
|
-
isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : (s as SequentialStep).agent,
|
|
590
|
+
isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
|
|
469
591
|
)
|
|
470
592
|
.join(" -> ");
|
|
471
593
|
|
|
472
594
|
return {
|
|
473
595
|
content: [{ type: "text", text: formatAsyncStartedMessage(`Async ${resultMode}: ${chainDesc} [${id}]`) }],
|
|
474
|
-
details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir },
|
|
596
|
+
details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph },
|
|
475
597
|
};
|
|
476
598
|
}
|
|
477
599
|
|
|
@@ -499,6 +621,7 @@ export function executeAsyncSingle(
|
|
|
499
621
|
controlConfig,
|
|
500
622
|
controlIntercomTarget,
|
|
501
623
|
childIntercomTarget,
|
|
624
|
+
nestedRoute,
|
|
502
625
|
} = params;
|
|
503
626
|
const task = params.task ?? "";
|
|
504
627
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
@@ -512,7 +635,11 @@ export function executeAsyncSingle(
|
|
|
512
635
|
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
|
|
513
636
|
}
|
|
514
637
|
|
|
515
|
-
const
|
|
638
|
+
const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
|
|
639
|
+
const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
|
|
640
|
+
const asyncDir = inheritedNestedRoute
|
|
641
|
+
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
642
|
+
: path.join(ASYNC_DIR, id);
|
|
516
643
|
try {
|
|
517
644
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
518
645
|
} catch (error) {
|
|
@@ -562,9 +689,16 @@ export function executeAsyncSingle(
|
|
|
562
689
|
outputMode,
|
|
563
690
|
sessionFile,
|
|
564
691
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
|
|
692
|
+
effectiveAcceptance: resolveEffectiveAcceptance({
|
|
693
|
+
explicit: params.acceptance,
|
|
694
|
+
agentName: agent,
|
|
695
|
+
task,
|
|
696
|
+
mode: "single",
|
|
697
|
+
async: true,
|
|
698
|
+
}),
|
|
565
699
|
},
|
|
566
700
|
],
|
|
567
|
-
resultPath: path.join(RESULTS_DIR, `${id}.json`),
|
|
701
|
+
resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
|
|
568
702
|
cwd: runnerCwd,
|
|
569
703
|
placeholder: "{previous}",
|
|
570
704
|
maxOutput,
|
|
@@ -582,6 +716,13 @@ export function executeAsyncSingle(
|
|
|
582
716
|
controlIntercomTarget,
|
|
583
717
|
childIntercomTargets: childIntercomTarget ? [childIntercomTarget(agent, 0)] : undefined,
|
|
584
718
|
resultMode: "single",
|
|
719
|
+
nestedRoute: nestedRoute ?? inheritedNestedRoute,
|
|
720
|
+
nestedSelf: inheritedNestedRoute && nestedAddress ? {
|
|
721
|
+
parentRunId: nestedAddress.parentRunId,
|
|
722
|
+
parentStepIndex: nestedAddress.parentStepIndex,
|
|
723
|
+
depth: nestedAddress.depth,
|
|
724
|
+
path: nestedAddress.path,
|
|
725
|
+
} : undefined,
|
|
585
726
|
},
|
|
586
727
|
id,
|
|
587
728
|
runnerCwd,
|
|
@@ -596,6 +737,39 @@ export function executeAsyncSingle(
|
|
|
596
737
|
}
|
|
597
738
|
|
|
598
739
|
if (spawnResult.pid) {
|
|
740
|
+
if (inheritedNestedRoute && nestedAddress) {
|
|
741
|
+
const now = Date.now();
|
|
742
|
+
try {
|
|
743
|
+
writeNestedEvent(inheritedNestedRoute, {
|
|
744
|
+
type: "subagent.nested.started",
|
|
745
|
+
ts: now,
|
|
746
|
+
parentRunId: nestedAddress.parentRunId,
|
|
747
|
+
parentStepIndex: nestedAddress.parentStepIndex,
|
|
748
|
+
child: {
|
|
749
|
+
id,
|
|
750
|
+
parentRunId: nestedAddress.parentRunId,
|
|
751
|
+
parentStepIndex: nestedAddress.parentStepIndex,
|
|
752
|
+
depth: nestedAddress.depth,
|
|
753
|
+
path: nestedAddress.path,
|
|
754
|
+
asyncDir,
|
|
755
|
+
pid: spawnResult.pid,
|
|
756
|
+
ownerIntercomTarget: process.env.PI_SUBAGENT_INTERCOM_SESSION_NAME,
|
|
757
|
+
leafIntercomTarget: childIntercomTarget?.(agent, 0),
|
|
758
|
+
intercomTarget: childIntercomTarget?.(agent, 0),
|
|
759
|
+
ownerState: "live",
|
|
760
|
+
mode: "single",
|
|
761
|
+
state: "running",
|
|
762
|
+
agent,
|
|
763
|
+
agents: [agent],
|
|
764
|
+
chainStepCount: 1,
|
|
765
|
+
startedAt: now,
|
|
766
|
+
lastUpdate: now,
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
} catch (error) {
|
|
770
|
+
console.error("Failed to emit nested async start event:", error);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
599
773
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
600
774
|
id,
|
|
601
775
|
pid: spawnResult.pid,
|
|
@@ -605,6 +779,7 @@ export function executeAsyncSingle(
|
|
|
605
779
|
task: task?.slice(0, 50),
|
|
606
780
|
cwd: runnerCwd,
|
|
607
781
|
asyncDir,
|
|
782
|
+
nestedRoute,
|
|
608
783
|
});
|
|
609
784
|
}
|
|
610
785
|
|
|
@@ -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"] {
|