pi-subagents 0.25.0 → 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 +21 -0
- package/README.md +129 -17
- package/package.json +1 -1
- package/prompts/parallel-context-build.md +3 -1
- package/prompts/parallel-handoff-plan.md +3 -1
- package/skills/pi-subagents/SKILL.md +32 -17
- 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 +1 -0
- package/src/extension/index.ts +1 -0
- package/src/extension/schemas.ts +138 -5
- package/src/runs/background/async-execution.ts +84 -6
- package/src/runs/background/async-status.ts +11 -1
- package/src/runs/background/run-status.ts +10 -1
- package/src/runs/background/subagent-runner.ts +600 -31
- package/src/runs/foreground/chain-execution.ts +325 -118
- package/src/runs/foreground/execution.ts +222 -10
- package/src/runs/foreground/subagent-executor.ts +67 -0
- 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/parallel-utils.ts +31 -1
- package/src/runs/shared/pi-args.ts +11 -0
- package/src/runs/shared/structured-output.ts +77 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +53 -3
- 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 +250 -0
- package/src/slash/slash-commands.ts +41 -3
- package/src/tui/render.ts +162 -34
|
@@ -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,7 +20,12 @@ 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,
|
|
@@ -107,6 +112,7 @@ interface AsyncChainParams {
|
|
|
107
112
|
sessionRoot?: string;
|
|
108
113
|
chainSkills?: string[];
|
|
109
114
|
sessionFilesByFlatIndex?: (string | undefined)[];
|
|
115
|
+
dynamicFanoutMaxItems?: number;
|
|
110
116
|
maxSubagentDepth: number;
|
|
111
117
|
worktreeSetupHook?: string;
|
|
112
118
|
worktreeSetupHookTimeoutMs?: number;
|
|
@@ -114,6 +120,7 @@ interface AsyncChainParams {
|
|
|
114
120
|
controlIntercomTarget?: string;
|
|
115
121
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
116
122
|
nestedRoute?: NestedRouteInfo;
|
|
123
|
+
acceptance?: AcceptanceInput;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
interface AsyncSingleParams {
|
|
@@ -140,6 +147,7 @@ interface AsyncSingleParams {
|
|
|
140
147
|
controlIntercomTarget?: string;
|
|
141
148
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
142
149
|
nestedRoute?: NestedRouteInfo;
|
|
150
|
+
acceptance?: AcceptanceInput;
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
interface AsyncExecutionResult {
|
|
@@ -248,12 +256,25 @@ export function executeAsyncChain(
|
|
|
248
256
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
249
257
|
const firstStep = chain[0];
|
|
250
258
|
const originalTask = params.task ?? (firstStep
|
|
251
|
-
? (isParallelStep(firstStep)
|
|
259
|
+
? (isParallelStep(firstStep)
|
|
260
|
+
? firstStep.parallel[0]?.task
|
|
261
|
+
: isDynamicParallelStep(firstStep)
|
|
262
|
+
? firstStep.parallel.task
|
|
263
|
+
: (firstStep as SequentialStep).task)
|
|
252
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 });
|
|
253
272
|
|
|
254
273
|
for (const s of chain) {
|
|
255
274
|
const stepAgents = isParallelStep(s)
|
|
256
275
|
? s.parallel.map((t) => t.agent)
|
|
276
|
+
: isDynamicParallelStep(s)
|
|
277
|
+
? [s.parallel.agent]
|
|
257
278
|
: [(s as SequentialStep).agent];
|
|
258
279
|
for (const agentName of stepAgents) {
|
|
259
280
|
if (!agents.find((x) => x.name === agentName)) {
|
|
@@ -316,13 +337,20 @@ export function executeAsyncChain(
|
|
|
316
337
|
const outputPath = resolveSingleOutputPath(behavior.output, ctx.cwd, instructionCwd);
|
|
317
338
|
const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
|
|
318
339
|
if (validationError) throw new AsyncStartValidationError(validationError);
|
|
319
|
-
|
|
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);
|
|
320
344
|
|
|
321
345
|
const primaryModel = resolveModelCandidate(behavior.model ?? a.model, availableModels, ctx.currentModelProvider);
|
|
322
346
|
const model = applyThinkingSuffix(primaryModel, a.thinking);
|
|
323
347
|
return {
|
|
324
348
|
agent: s.agent,
|
|
325
349
|
task,
|
|
350
|
+
phase: s.phase,
|
|
351
|
+
label: s.label,
|
|
352
|
+
outputName: s.as,
|
|
353
|
+
structured: Boolean(s.outputSchema),
|
|
326
354
|
cwd: stepCwd,
|
|
327
355
|
model,
|
|
328
356
|
thinking: resolveEffectiveThinking(model, a.thinking),
|
|
@@ -342,6 +370,16 @@ export function executeAsyncChain(
|
|
|
342
370
|
outputMode: behavior.outputMode,
|
|
343
371
|
sessionFile,
|
|
344
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")) } : {}),
|
|
345
383
|
};
|
|
346
384
|
};
|
|
347
385
|
|
|
@@ -382,6 +420,24 @@ export function executeAsyncChain(
|
|
|
382
420
|
worktree: s.worktree,
|
|
383
421
|
};
|
|
384
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
|
+
}
|
|
385
441
|
return buildSeqStep(s as SequentialStep, nextSessionFile());
|
|
386
442
|
});
|
|
387
443
|
} catch (error) {
|
|
@@ -391,6 +447,10 @@ export function executeAsyncChain(
|
|
|
391
447
|
let childTargetIndex = 0;
|
|
392
448
|
const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
|
|
393
449
|
if ("parallel" in step) {
|
|
450
|
+
if (!Array.isArray(step.parallel)) {
|
|
451
|
+
childTargetIndex++;
|
|
452
|
+
return [undefined];
|
|
453
|
+
}
|
|
394
454
|
return step.parallel.map((task) => childIntercomTarget(task.agent, childTargetIndex++));
|
|
395
455
|
}
|
|
396
456
|
return [childIntercomTarget(step.agent, childTargetIndex++)];
|
|
@@ -420,6 +480,8 @@ export function executeAsyncChain(
|
|
|
420
480
|
controlIntercomTarget,
|
|
421
481
|
childIntercomTargets,
|
|
422
482
|
resultMode,
|
|
483
|
+
dynamicFanoutMaxItems: params.dynamicFanoutMaxItems,
|
|
484
|
+
workflowGraph,
|
|
423
485
|
nestedRoute: nestedRoute ?? inheritedNestedRoute,
|
|
424
486
|
nestedSelf: inheritedNestedRoute && nestedAddress ? {
|
|
425
487
|
parentRunId: nestedAddress.parentRunId,
|
|
@@ -444,6 +506,8 @@ export function executeAsyncChain(
|
|
|
444
506
|
const firstStep = chain[0];
|
|
445
507
|
const firstAgents = isParallelStep(firstStep)
|
|
446
508
|
? firstStep.parallel.map((t) => t.agent)
|
|
509
|
+
: isDynamicParallelStep(firstStep)
|
|
510
|
+
? [firstStep.parallel.agent]
|
|
447
511
|
: [(firstStep as SequentialStep).agent];
|
|
448
512
|
const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
|
|
449
513
|
const flatAgents: string[] = [];
|
|
@@ -454,6 +518,10 @@ export function executeAsyncChain(
|
|
|
454
518
|
parallelGroups.push({ start: flatStepStart, count: step.parallel.length, stepIndex });
|
|
455
519
|
flatAgents.push(...step.parallel.map((task) => task.agent));
|
|
456
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++;
|
|
457
525
|
} else {
|
|
458
526
|
flatAgents.push((step as SequentialStep).agent);
|
|
459
527
|
flatStepStart++;
|
|
@@ -502,12 +570,15 @@ export function executeAsyncChain(
|
|
|
502
570
|
agents: flatAgents,
|
|
503
571
|
task: isParallelStep(firstStep)
|
|
504
572
|
? firstStep.parallel[0]?.task?.slice(0, 50)
|
|
573
|
+
: isDynamicParallelStep(firstStep)
|
|
574
|
+
? firstStep.parallel.task?.slice(0, 50)
|
|
505
575
|
: (firstStep as SequentialStep).task?.slice(0, 50),
|
|
506
576
|
chain: chain.map((s) =>
|
|
507
|
-
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,
|
|
508
578
|
),
|
|
509
579
|
chainStepCount: chain.length,
|
|
510
580
|
parallelGroups,
|
|
581
|
+
workflowGraph,
|
|
511
582
|
cwd: runnerCwd,
|
|
512
583
|
asyncDir,
|
|
513
584
|
nestedRoute,
|
|
@@ -516,13 +587,13 @@ export function executeAsyncChain(
|
|
|
516
587
|
|
|
517
588
|
const chainDesc = chain
|
|
518
589
|
.map((s) =>
|
|
519
|
-
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,
|
|
520
591
|
)
|
|
521
592
|
.join(" -> ");
|
|
522
593
|
|
|
523
594
|
return {
|
|
524
595
|
content: [{ type: "text", text: formatAsyncStartedMessage(`Async ${resultMode}: ${chainDesc} [${id}]`) }],
|
|
525
|
-
details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir },
|
|
596
|
+
details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph },
|
|
526
597
|
};
|
|
527
598
|
}
|
|
528
599
|
|
|
@@ -618,6 +689,13 @@ export function executeAsyncSingle(
|
|
|
618
689
|
outputMode,
|
|
619
690
|
sessionFile,
|
|
620
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
|
+
}),
|
|
621
699
|
},
|
|
622
700
|
],
|
|
623
701
|
resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
|
|
@@ -12,6 +12,10 @@ import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-
|
|
|
12
12
|
interface AsyncRunStepSummary {
|
|
13
13
|
index: number;
|
|
14
14
|
agent: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
phase?: string;
|
|
17
|
+
outputName?: string;
|
|
18
|
+
structured?: boolean;
|
|
15
19
|
status: AsyncJobStep["status"];
|
|
16
20
|
activityState?: ActivityState;
|
|
17
21
|
lastActivityAt?: number;
|
|
@@ -139,6 +143,10 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
139
143
|
return {
|
|
140
144
|
index,
|
|
141
145
|
agent: step.agent,
|
|
146
|
+
...(step.label ? { label: step.label } : {}),
|
|
147
|
+
...(step.phase ? { phase: step.phase } : {}),
|
|
148
|
+
...(step.outputName ? { outputName: step.outputName } : {}),
|
|
149
|
+
...(step.structured ? { structured: step.structured } : {}),
|
|
142
150
|
status: step.status,
|
|
143
151
|
...(stepActivityState ? { activityState: stepActivityState } : {}),
|
|
144
152
|
...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
|
|
@@ -259,7 +267,9 @@ function formatActivityFacts(input: { activityState?: ActivityState; lastActivit
|
|
|
259
267
|
}
|
|
260
268
|
|
|
261
269
|
function formatStepLine(step: AsyncRunStepSummary): string {
|
|
262
|
-
const
|
|
270
|
+
const display = step.label ? `${step.label} (${step.agent})` : step.agent;
|
|
271
|
+
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
272
|
+
const parts = [`${step.index + 1}. ${phase}${display}`, step.status];
|
|
263
273
|
const activity = formatActivityFacts(step);
|
|
264
274
|
if (activity) parts.push(activity);
|
|
265
275
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
@@ -49,6 +49,11 @@ function formatResumeGuidance(runId: string | undefined, children: Array<{ agent
|
|
|
49
49
|
return "Resume: unavailable; no child session file was persisted.";
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function formatAcceptanceFinalizationSummary(finalization: NonNullable<NonNullable<AsyncStatus["steps"]>[number]["acceptance"]>["finalization"] | undefined): string {
|
|
53
|
+
if (!finalization) return "";
|
|
54
|
+
return `, finalization: ${finalization.status} after ${finalization.turns.length}/${finalization.maxTurns} turns`;
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
function stepLineLabel(status: AsyncStatus, index: number): string {
|
|
53
58
|
const steps = status.steps ?? [];
|
|
54
59
|
if (status.mode === "parallel") return `Agent ${index + 1}/${steps.length || 1}`;
|
|
@@ -217,7 +222,11 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
217
222
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
218
223
|
const modelText = modelThinking ? ` (${modelThinking})` : "";
|
|
219
224
|
const errorText = step.error ? `, error: ${step.error}` : "";
|
|
220
|
-
|
|
225
|
+
const finalizationText = formatAcceptanceFinalizationSummary(step.acceptance?.finalization);
|
|
226
|
+
const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}${finalizationText}` : "";
|
|
227
|
+
const display = step.label ? `${step.label} (${step.agent})` : step.agent;
|
|
228
|
+
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
229
|
+
lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${acceptanceText}${errorText}`);
|
|
221
230
|
lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true, maxLines: 20 }));
|
|
222
231
|
const stepOutputPath = path.join(asyncDir, `output-${index}.log`);
|
|
223
232
|
if (stepOutputPath !== outputPath && fs.existsSync(stepOutputPath)) lines.push(` Output: ${stepOutputPath}`);
|