pi-prompt-template-model 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +19 -1
- package/chain-parser.ts +124 -22
- package/index.ts +137 -32
- package/loop-utils.ts +23 -10
- package/package.json +1 -1
- package/prompt-loader.ts +15 -0
- package/subagent-renderer.ts +74 -21
- package/subagent-runtime.ts +32 -0
- package/subagent-step.ts +299 -50
- package/subagent-widget.ts +55 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.6.4] - 2026-03-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Updated skill command resolution to use `sourceInfo.path` instead of the removed `path` field on `SlashCommandInfo`, fixing compatibility with pi-coding-agent 0.62.0 source provenance changes.
|
|
9
|
+
|
|
10
|
+
## [0.6.3] - 2026-03-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Chain step progress now shows in the status bar (e.g., `step 2/3: simplify`) and persists until the chain completes, instead of only appearing as a notification that scrolls away.
|
|
14
|
+
- Added `parallel(...)` chain step support for delegated prompt templates, including parser/frontmatter validation, delegated task fan-out, aggregated result rendering, and per-task progress display.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Delegated subagent errors now show as clean notifications instead of extension crash messages with stack traces, matching how other validation errors (missing skill, missing template, etc.) are presented.
|
|
18
|
+
- When no subagent bridge is listening (extension not loaded, name collision with another extension), delegation now fails immediately with a diagnostic message instead of silently waiting 15 seconds before timing out.
|
|
19
|
+
- Timeout error message no longer dumps the full rendered prompt content; uses the agent name instead.
|
|
20
|
+
- Parallel delegated status now prefers aggregate `parallel X/Y running` updates over first-task tool labels, avoiding misleading `running <tool>` status lines during multi-task execution.
|
|
21
|
+
|
|
5
22
|
## [0.6.2] - 2026-03-20
|
|
6
23
|
|
|
7
24
|
### Added
|
package/README.md
CHANGED
|
@@ -301,6 +301,16 @@ chain: double-check --loop 2 -> deslop --loop 2
|
|
|
301
301
|
|
|
302
302
|
This registers the file's name as a command that runs `double-check` twice, then `deslop` twice. Per-step `--loop N` repeats that step before moving to the next, with per-step convergence (stops early if no changes, unless the step's template has `converge: false`).
|
|
303
303
|
|
|
304
|
+
Chain declarations also support parallel groups with `parallel(...)`:
|
|
305
|
+
|
|
306
|
+
```markdown
|
|
307
|
+
---
|
|
308
|
+
chain: parallel(scan-frontend, scan-backend) -> consolidate
|
|
309
|
+
---
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Each entry inside `parallel(...)` runs as a delegated subagent task concurrently. Parallel entries can include per-step args (for example `parallel(scan-frontend, scan-backend "auth")`), but per-step `--loop` is not supported inside parallel groups. Nested `parallel(...)` is rejected. Parallel entries must be delegated templates (`subagent: ...` or runtime `--subagent` override), and all entries in the same parallel group must resolve to the same `inheritContext` mode and `cwd`.
|
|
313
|
+
|
|
304
314
|
Steps with a `model` field use their own model. Steps without one inherit a snapshot of whatever model was active when the chain started — not the previous step's model. This keeps behavior deterministic regardless of what earlier steps do.
|
|
305
315
|
|
|
306
316
|
Chain templates support `loop`, `fresh`, `converge`, `restore`, and `cwd` in their frontmatter for controlling the overall execution:
|
|
@@ -318,7 +328,15 @@ This runs the full analyze → fix chain 3 times, with fresh context between ite
|
|
|
318
328
|
|
|
319
329
|
When a chain template sets `cwd`, it becomes the default delegated subprocess working directory for all delegated steps in that chain. Runtime `--cwd=<path>` overrides the chain template value.
|
|
320
330
|
|
|
321
|
-
###
|
|
331
|
+
### Parallel and looping from the CLI
|
|
332
|
+
|
|
333
|
+
Parallel groups work in `/chain-prompts` too:
|
|
334
|
+
|
|
335
|
+
```
|
|
336
|
+
/chain-prompts parallel(scan-fe, scan-be) -> review
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Looping applies to the entire chain:
|
|
322
340
|
|
|
323
341
|
```
|
|
324
342
|
/chain-prompts analyze -> fix --loop 3
|
package/chain-parser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseCommandArgs
|
|
1
|
+
import { parseCommandArgs } from "./args.js";
|
|
2
2
|
|
|
3
3
|
export interface ChainStep {
|
|
4
4
|
name: string;
|
|
@@ -6,14 +6,20 @@ export interface ChainStep {
|
|
|
6
6
|
loopCount?: number;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface ParallelChainStep {
|
|
10
|
+
parallel: ChainStep[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ChainStepOrParallel = ChainStep | ParallelChainStep;
|
|
14
|
+
|
|
9
15
|
export interface ParsedChainSteps {
|
|
10
|
-
steps:
|
|
16
|
+
steps: ChainStepOrParallel[];
|
|
11
17
|
sharedArgs: string[];
|
|
12
18
|
invalidSegments: string[];
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export interface ParsedChainDeclaration {
|
|
16
|
-
steps:
|
|
22
|
+
steps: ChainStepOrParallel[];
|
|
17
23
|
invalidSegments: string[];
|
|
18
24
|
}
|
|
19
25
|
|
|
@@ -121,27 +127,130 @@ function extractStepLoopCount(segment: string): { cleanedSegment: string; loopCo
|
|
|
121
127
|
return { cleanedSegment: cleanedSegment.trim(), loopCount };
|
|
122
128
|
}
|
|
123
129
|
|
|
130
|
+
function splitByTopLevelSeparator(input: string, separator: string): string[] {
|
|
131
|
+
const parts: string[] = [];
|
|
132
|
+
let start = 0;
|
|
133
|
+
let inQuote: string | null = null;
|
|
134
|
+
let parenDepth = 0;
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < input.length; i++) {
|
|
137
|
+
const char = input[i];
|
|
138
|
+
if (inQuote) {
|
|
139
|
+
if (char === inQuote) inQuote = null;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (char === '"' || char === "'") {
|
|
144
|
+
inQuote = char;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (char === "(") {
|
|
148
|
+
parenDepth++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (char === ")" && parenDepth > 0) {
|
|
152
|
+
parenDepth--;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (parenDepth === 0 && i <= input.length - separator.length && input.startsWith(separator, i)) {
|
|
157
|
+
parts.push(input.slice(start, i));
|
|
158
|
+
start = i + separator.length;
|
|
159
|
+
i += separator.length - 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
parts.push(input.slice(start));
|
|
164
|
+
return parts;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function findMatchingParen(segment: string, openIndex: number): number {
|
|
168
|
+
let inQuote: string | null = null;
|
|
169
|
+
let depth = 0;
|
|
170
|
+
|
|
171
|
+
for (let i = openIndex; i < segment.length; i++) {
|
|
172
|
+
const char = segment[i];
|
|
173
|
+
if (inQuote) {
|
|
174
|
+
if (char === inQuote) inQuote = null;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (char === '"' || char === "'") {
|
|
179
|
+
inQuote = char;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (char === "(") {
|
|
183
|
+
depth++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (char !== ")") continue;
|
|
187
|
+
depth--;
|
|
188
|
+
if (depth === 0) return i;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return -1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseSingleStepSegment(segment: string): ChainStep | undefined {
|
|
195
|
+
const { cleanedSegment, loopCount } = extractStepLoopCount(segment);
|
|
196
|
+
const tokens = parseCommandArgs(cleanedSegment);
|
|
197
|
+
if (tokens.length === 0) return undefined;
|
|
198
|
+
return { name: tokens[0], args: tokens.slice(1), loopCount };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseParallelStepSegment(segment: string): ParallelChainStep | undefined {
|
|
202
|
+
if (!/^parallel\s*\(/.test(segment)) return undefined;
|
|
203
|
+
const openIndex = segment.indexOf("(");
|
|
204
|
+
if (openIndex < 0) return undefined;
|
|
205
|
+
|
|
206
|
+
const closeIndex = findMatchingParen(segment, openIndex);
|
|
207
|
+
if (closeIndex < 0) return undefined;
|
|
208
|
+
if (segment.slice(closeIndex + 1).trim().length > 0) return undefined;
|
|
209
|
+
|
|
210
|
+
const inner = segment.slice(openIndex + 1, closeIndex).trim();
|
|
211
|
+
if (!inner) return undefined;
|
|
212
|
+
|
|
213
|
+
const parsedSteps: ChainStep[] = [];
|
|
214
|
+
for (const rawEntry of splitByTopLevelSeparator(inner, ",")) {
|
|
215
|
+
const entry = rawEntry.trim();
|
|
216
|
+
if (!entry) return undefined;
|
|
217
|
+
if (/^parallel\s*\(/.test(entry)) return undefined;
|
|
218
|
+
const parsed = parseSingleStepSegment(entry);
|
|
219
|
+
if (!parsed) return undefined;
|
|
220
|
+
parsedSteps.push(parsed);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (parsedSteps.length === 0) return undefined;
|
|
224
|
+
return { parallel: parsedSteps };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseChainSegment(segment: string): ChainStepOrParallel | undefined {
|
|
228
|
+
const parallelStep = parseParallelStepSegment(segment);
|
|
229
|
+
if (parallelStep) return parallelStep;
|
|
230
|
+
if (/^parallel\s*\(/.test(segment)) return undefined;
|
|
231
|
+
return parseSingleStepSegment(segment);
|
|
232
|
+
}
|
|
233
|
+
|
|
124
234
|
export function parseChainSteps(args: string): ParsedChainSteps {
|
|
125
|
-
const sharedArgsSplit =
|
|
235
|
+
const sharedArgsSplit = splitByTopLevelSeparator(args, " -- ");
|
|
126
236
|
const templatesPart = sharedArgsSplit[0];
|
|
127
237
|
const argsPart = sharedArgsSplit.length > 1 ? sharedArgsSplit.slice(1).join(" -- ") : "";
|
|
128
238
|
|
|
129
239
|
const invalidSegments: string[] = [];
|
|
130
|
-
const steps:
|
|
240
|
+
const steps: ChainStepOrParallel[] = [];
|
|
131
241
|
|
|
132
|
-
for (const rawSegment of
|
|
242
|
+
for (const rawSegment of splitByTopLevelSeparator(templatesPart, "->")) {
|
|
133
243
|
const segment = rawSegment.trim();
|
|
134
244
|
if (!segment) {
|
|
135
245
|
invalidSegments.push(rawSegment);
|
|
136
246
|
continue;
|
|
137
247
|
}
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
if (tokens.length === 0) {
|
|
248
|
+
const parsedSegment = parseChainSegment(segment);
|
|
249
|
+
if (!parsedSegment) {
|
|
141
250
|
invalidSegments.push(segment);
|
|
142
251
|
continue;
|
|
143
252
|
}
|
|
144
|
-
steps.push(
|
|
253
|
+
steps.push(parsedSegment);
|
|
145
254
|
}
|
|
146
255
|
|
|
147
256
|
return { steps, sharedArgs: parseCommandArgs(argsPart), invalidSegments };
|
|
@@ -149,27 +258,20 @@ export function parseChainSteps(args: string): ParsedChainSteps {
|
|
|
149
258
|
|
|
150
259
|
export function parseChainDeclaration(chain: string): ParsedChainDeclaration {
|
|
151
260
|
const invalidSegments: string[] = [];
|
|
152
|
-
const steps:
|
|
261
|
+
const steps: ChainStepOrParallel[] = [];
|
|
153
262
|
|
|
154
|
-
for (const rawSegment of
|
|
263
|
+
for (const rawSegment of splitByTopLevelSeparator(chain, "->")) {
|
|
155
264
|
const segment = rawSegment.trim();
|
|
156
265
|
if (!segment) {
|
|
157
266
|
invalidSegments.push(rawSegment);
|
|
158
267
|
continue;
|
|
159
268
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const tokens = parseCommandArgs(cleanedSegment);
|
|
163
|
-
if (tokens.length === 0) {
|
|
269
|
+
const parsedSegment = parseChainSegment(segment);
|
|
270
|
+
if (!parsedSegment) {
|
|
164
271
|
invalidSegments.push(segment);
|
|
165
272
|
continue;
|
|
166
273
|
}
|
|
167
|
-
|
|
168
|
-
steps.push({
|
|
169
|
-
name: tokens[0],
|
|
170
|
-
args: tokens.slice(1),
|
|
171
|
-
loopCount,
|
|
172
|
-
});
|
|
274
|
+
steps.push(parsedSegment);
|
|
173
275
|
}
|
|
174
276
|
|
|
175
277
|
return { steps, invalidSegments };
|
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Model } from "@mariozechner/pi-ai";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
3
3
|
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
4
4
|
import { extractLoopCount, extractLoopFlags, extractSubagentOverride, parseCommandArgs, type SubagentOverride } from "./args.js";
|
|
5
|
-
import { parseChainSteps, parseChainDeclaration, type ChainStep } from "./chain-parser.js";
|
|
5
|
+
import { parseChainSteps, parseChainDeclaration, type ChainStep, type ChainStepOrParallel, type ParallelChainStep } from "./chain-parser.js";
|
|
6
6
|
import { generateIterationSummary, didIterationMakeChanges, getIterationEntries } from "./loop-utils.js";
|
|
7
7
|
import { notify, summarizePromptDiagnostics, diagnosticsFingerprint } from "./notifications.js";
|
|
8
8
|
import { preparePromptExecution } from "./prompt-execution.js";
|
|
@@ -131,9 +131,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
131
131
|
|
|
132
132
|
for (const command of pi.getCommands()) {
|
|
133
133
|
if (command.source !== "skill") continue;
|
|
134
|
-
if (!command.path) continue;
|
|
134
|
+
if (!command.sourceInfo.path) continue;
|
|
135
135
|
if (!candidates.has(command.name)) continue;
|
|
136
|
-
return command.path;
|
|
136
|
+
return command.sourceInfo.path;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
return undefined;
|
|
@@ -184,6 +184,10 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
184
184
|
return prompt.subagent !== undefined || override?.enabled === true;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
function isParallelChainStep(step: ChainStepOrParallel): step is ParallelChainStep {
|
|
188
|
+
return "parallel" in step;
|
|
189
|
+
}
|
|
190
|
+
|
|
187
191
|
async function executePromptStep(
|
|
188
192
|
prompt: PromptWithModel,
|
|
189
193
|
args: string[],
|
|
@@ -193,19 +197,25 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
193
197
|
inheritedModel?: Model<any>,
|
|
194
198
|
): Promise<PromptStepResult | "aborted"> {
|
|
195
199
|
if (shouldDelegatePrompt(prompt, override)) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
200
|
+
try {
|
|
201
|
+
const delegated = await executeSubagentPromptStep({
|
|
202
|
+
pi,
|
|
203
|
+
prompt,
|
|
204
|
+
args,
|
|
205
|
+
ctx,
|
|
206
|
+
currentModel,
|
|
207
|
+
override,
|
|
208
|
+
inheritedModel,
|
|
209
|
+
});
|
|
210
|
+
if (!delegated) {
|
|
211
|
+
notify(ctx, `Prompt \`${prompt.name}\` is not configured for delegated execution.`, "error");
|
|
212
|
+
return "aborted";
|
|
213
|
+
}
|
|
214
|
+
return { changed: delegated.changed };
|
|
215
|
+
} catch (error) {
|
|
216
|
+
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
217
|
+
return "aborted";
|
|
207
218
|
}
|
|
208
|
-
return { changed: delegated.changed };
|
|
209
219
|
}
|
|
210
220
|
|
|
211
221
|
const prepared =
|
|
@@ -477,7 +487,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
477
487
|
}
|
|
478
488
|
|
|
479
489
|
async function runSharedChainExecution(
|
|
480
|
-
steps:
|
|
490
|
+
steps: ChainStepOrParallel[],
|
|
481
491
|
sharedArgs: string[],
|
|
482
492
|
totalIterations: number | null,
|
|
483
493
|
fresh: boolean,
|
|
@@ -487,14 +497,47 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
487
497
|
subagentOverride?: SubagentOverride,
|
|
488
498
|
cwdOverride?: string,
|
|
489
499
|
) {
|
|
500
|
+
const flattenChainSteps = (): ChainStep[] => {
|
|
501
|
+
const flattened: ChainStep[] = [];
|
|
502
|
+
for (const step of steps) {
|
|
503
|
+
if (isParallelChainStep(step)) {
|
|
504
|
+
flattened.push(...step.parallel);
|
|
505
|
+
} else {
|
|
506
|
+
flattened.push(step);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return flattened;
|
|
510
|
+
};
|
|
511
|
+
|
|
490
512
|
const validateChainSteps = (): boolean => {
|
|
491
|
-
const
|
|
513
|
+
const flattened = flattenChainSteps();
|
|
514
|
+
const missingTemplates = flattened.filter((step) => !prompts.has(step.name));
|
|
492
515
|
if (missingTemplates.length > 0) {
|
|
493
516
|
notify(ctx, `Templates not found: ${missingTemplates.map((step) => step.name).join(", ")}`, "error");
|
|
494
517
|
return false;
|
|
495
518
|
}
|
|
496
519
|
|
|
497
520
|
for (const step of steps) {
|
|
521
|
+
if (isParallelChainStep(step)) {
|
|
522
|
+
for (const parallelStep of step.parallel) {
|
|
523
|
+
if (parallelStep.loopCount !== undefined) {
|
|
524
|
+
notify(ctx, `Step "${parallelStep.name}" in parallel() does not support per-task --loop.`, "error");
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
const stepPrompt = prompts.get(parallelStep.name);
|
|
528
|
+
if (!stepPrompt) continue;
|
|
529
|
+
if (stepPrompt.chain) {
|
|
530
|
+
notify(ctx, `Step "${parallelStep.name}" is a chain template. Chain nesting is not supported.`, "error");
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
if (!shouldDelegatePrompt(stepPrompt, subagentOverride)) {
|
|
534
|
+
notify(ctx, `Step "${parallelStep.name}" in parallel() must use delegated execution (subagent).`, "error");
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
|
|
498
541
|
const stepPrompt = prompts.get(step.name);
|
|
499
542
|
if (!stepPrompt) continue;
|
|
500
543
|
if (stepPrompt.chain) {
|
|
@@ -509,7 +552,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
509
552
|
if (!validateChainSteps()) return;
|
|
510
553
|
|
|
511
554
|
const originalModel = getCurrentModel(ctx);
|
|
512
|
-
const chainInheritedModel =
|
|
555
|
+
const chainInheritedModel = originalModel;
|
|
513
556
|
const originalThinking = pi.getThinkingLevel();
|
|
514
557
|
let currentModel = originalModel;
|
|
515
558
|
let currentThinking = originalThinking;
|
|
@@ -520,7 +563,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
520
563
|
const useConverge = isUnlimited ? true : converge;
|
|
521
564
|
|
|
522
565
|
const anchorId = fresh ? ctx.sessionManager.getLeafId() : null;
|
|
523
|
-
const chainStepNames = steps
|
|
566
|
+
const chainStepNames = steps
|
|
567
|
+
.map((step) => (isParallelChainStep(step) ? `parallel(${step.parallel.map((item) => item.name).join(", ")})` : step.name))
|
|
568
|
+
.join(" -> ");
|
|
524
569
|
let completedIterations = 0;
|
|
525
570
|
let converged = false;
|
|
526
571
|
let chainErrorState: ExecutionErrorState = { hasError: false, error: undefined };
|
|
@@ -539,17 +584,77 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
539
584
|
if (!validateChainSteps()) break;
|
|
540
585
|
}
|
|
541
586
|
|
|
542
|
-
const templates = steps.map((step) =>
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
587
|
+
const templates = steps.map((step) =>
|
|
588
|
+
isParallelChainStep(step)
|
|
589
|
+
? {
|
|
590
|
+
kind: "parallel" as const,
|
|
591
|
+
tasks: step.parallel.map((item) => ({
|
|
592
|
+
name: item.name,
|
|
593
|
+
args: item.args,
|
|
594
|
+
prompt: {
|
|
595
|
+
...prompts.get(item.name)!,
|
|
596
|
+
...(cwdOverride ? { cwd: cwdOverride } : {}),
|
|
597
|
+
},
|
|
598
|
+
})),
|
|
599
|
+
}
|
|
600
|
+
: {
|
|
601
|
+
kind: "single" as const,
|
|
602
|
+
template: {
|
|
603
|
+
...prompts.get(step.name)!,
|
|
604
|
+
...(cwdOverride ? { cwd: cwdOverride } : {}),
|
|
605
|
+
stepArgs: step.args,
|
|
606
|
+
stepLoop: step.loopCount ?? 1,
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
);
|
|
548
610
|
let aborted = false;
|
|
549
611
|
let iterationChanged = false;
|
|
612
|
+
let loopPrefix = "";
|
|
613
|
+
if (effectiveMax > 1) {
|
|
614
|
+
const label = totalIterations !== null ? `${iteration + 1}/${totalIterations}` : `${iteration + 1}`;
|
|
615
|
+
loopPrefix = `Loop ${label}, `;
|
|
616
|
+
}
|
|
550
617
|
|
|
551
|
-
for (const [index,
|
|
618
|
+
for (const [index, stepTemplate] of templates.entries()) {
|
|
552
619
|
const stepNumber = index + 1;
|
|
620
|
+
if (stepTemplate.kind === "parallel") {
|
|
621
|
+
const stepNames = stepTemplate.tasks.map((task) => task.name).join(", ");
|
|
622
|
+
notify(ctx, `${loopPrefix}Step ${stepNumber}/${templates.length}: parallel(${stepNames})`, "info");
|
|
623
|
+
if (ctx.hasUI) {
|
|
624
|
+
ctx.ui.setStatus("prompt-chain", ctx.ui.theme.fg("warning", `step ${stepNumber}/${templates.length}: parallel(${stepNames})`));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
let delegated;
|
|
628
|
+
try {
|
|
629
|
+
delegated = await executeSubagentPromptStep({
|
|
630
|
+
pi,
|
|
631
|
+
ctx,
|
|
632
|
+
currentModel,
|
|
633
|
+
override: subagentOverride,
|
|
634
|
+
inheritedModel: chainInheritedModel,
|
|
635
|
+
parallel: stepTemplate.tasks.map((task) => ({
|
|
636
|
+
prompt: task.prompt,
|
|
637
|
+
args: task.args.length > 0 ? task.args : sharedArgs,
|
|
638
|
+
})),
|
|
639
|
+
});
|
|
640
|
+
} catch (error) {
|
|
641
|
+
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
642
|
+
aborted = true;
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
if (!delegated) {
|
|
646
|
+
notify(ctx, "Parallel chain step was not delegated.", "error");
|
|
647
|
+
aborted = true;
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
currentModel = getCurrentModel(ctx);
|
|
652
|
+
currentThinking = pi.getThinkingLevel();
|
|
653
|
+
if (delegated.changed) iterationChanged = true;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const template = stepTemplate.template;
|
|
553
658
|
const stepLoop = template.stepLoop;
|
|
554
659
|
const effectiveArgs = template.stepArgs.length > 0 ? template.stepArgs : sharedArgs;
|
|
555
660
|
const outerLoopState = loopState ? { ...loopState } : null;
|
|
@@ -565,14 +670,11 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
565
670
|
updateLoopStatus(ctx);
|
|
566
671
|
}
|
|
567
672
|
|
|
568
|
-
const loopPrefix =
|
|
569
|
-
effectiveMax > 1
|
|
570
|
-
? totalIterations !== null
|
|
571
|
-
? `Loop ${iteration + 1}/${totalIterations}, `
|
|
572
|
-
: `Loop ${iteration + 1}, `
|
|
573
|
-
: "";
|
|
574
673
|
const iterSuffix = stepLoop > 1 ? ` (iter ${stepIteration + 1}/${stepLoop})` : "";
|
|
575
674
|
notify(ctx, `${loopPrefix}Step ${stepNumber}/${templates.length}: ${template.name}${iterSuffix} ${buildPromptCommandDescription(template)}`, "info");
|
|
675
|
+
if (ctx.hasUI) {
|
|
676
|
+
ctx.ui.setStatus("prompt-chain", ctx.ui.theme.fg("warning", `step ${stepNumber}/${templates.length}: ${template.name}`));
|
|
677
|
+
}
|
|
576
678
|
|
|
577
679
|
const stepIterationStartId = ctx.sessionManager.getLeafId();
|
|
578
680
|
const stepResult = await executePromptStep(
|
|
@@ -648,6 +750,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
648
750
|
freshCollapse = null;
|
|
649
751
|
accumulatedSummaries = [];
|
|
650
752
|
updateLoopStatus(ctx);
|
|
753
|
+
if (ctx.hasUI) {
|
|
754
|
+
ctx.ui.setStatus("prompt-chain", undefined);
|
|
755
|
+
}
|
|
651
756
|
|
|
652
757
|
if (!chainErrorState.hasError) {
|
|
653
758
|
notifyLoopCompletion(ctx, completedIterations, totalIterations, effectiveMax, converged, true);
|
package/loop-utils.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE } from "./subagent-runtime.js";
|
|
|
4
4
|
|
|
5
5
|
interface DelegatedMessageDetails {
|
|
6
6
|
messages?: Message[];
|
|
7
|
+
parallelResults?: Array<{ messages?: Message[] }>;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
function collectAssistantActions(messages: Message[], filesRead: Set<string>, filesWritten: Set<string>): { commandCount: number; lastText: string } {
|
|
@@ -55,10 +56,16 @@ export function generateIterationSummary(entries: SessionEntry[], task: string,
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
const delegated = delegatedDetails(entry);
|
|
58
|
-
if (!delegated
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
if (!delegated) continue;
|
|
60
|
+
const messageGroups =
|
|
61
|
+
delegated.parallelResults && delegated.parallelResults.length > 0
|
|
62
|
+
? delegated.parallelResults.map((result) => result.messages ?? [])
|
|
63
|
+
: delegated.messages ? [delegated.messages] : [];
|
|
64
|
+
for (const messages of messageGroups) {
|
|
65
|
+
const collected = collectAssistantActions(messages, filesRead, filesWritten);
|
|
66
|
+
commandCount += collected.commandCount;
|
|
67
|
+
if (collected.lastText) lastAssistantText = collected.lastText;
|
|
68
|
+
}
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
let summary = totalIterations !== null ? `[Loop iteration ${iteration}/${totalIterations}]\nTask: "${task}"` : `[Loop iteration ${iteration}]\nTask: "${task}"`;
|
|
@@ -92,12 +99,18 @@ export function didIterationMakeChanges(entries: SessionEntry[]): boolean {
|
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
const delegated = delegatedDetails(entry);
|
|
95
|
-
if (!delegated
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
if (!delegated) continue;
|
|
103
|
+
const delegatedGroups =
|
|
104
|
+
delegated.parallelResults && delegated.parallelResults.length > 0
|
|
105
|
+
? delegated.parallelResults.map((result) => result.messages ?? [])
|
|
106
|
+
: [delegated.messages ?? []];
|
|
107
|
+
for (const messages of delegatedGroups) {
|
|
108
|
+
for (const message of messages) {
|
|
109
|
+
if (message.role !== "assistant") continue;
|
|
110
|
+
for (const block of (message as AssistantMessage).content) {
|
|
111
|
+
if (block.type !== "toolCall") continue;
|
|
112
|
+
if (block.name === "write" || block.name === "edit") return true;
|
|
113
|
+
}
|
|
101
114
|
}
|
|
102
115
|
}
|
|
103
116
|
}
|
package/package.json
CHANGED
package/prompt-loader.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
5
5
|
import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { parseChainDeclaration } from "./chain-parser.js";
|
|
6
7
|
|
|
7
8
|
const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
8
9
|
export const RESERVED_COMMAND_NAMES = new Set([
|
|
@@ -541,6 +542,20 @@ function loadPromptsWithModelFromDir(
|
|
|
541
542
|
if (!frontmatter) continue;
|
|
542
543
|
const { body } = parsed;
|
|
543
544
|
const chain = normalizeChain(frontmatter.chain, fullPath, source, diagnostics);
|
|
545
|
+
if (chain && /\bparallel\s*\(/.test(chain)) {
|
|
546
|
+
const parsedChain = parseChainDeclaration(chain);
|
|
547
|
+
if (parsedChain.invalidSegments.length > 0 || parsedChain.steps.length === 0) {
|
|
548
|
+
diagnostics.push(
|
|
549
|
+
createDiagnostic(
|
|
550
|
+
"invalid-chain-declaration",
|
|
551
|
+
fullPath,
|
|
552
|
+
source,
|
|
553
|
+
`Skipping prompt template at ${fullPath}: invalid chain declaration segment ${JSON.stringify(parsedChain.invalidSegments[0] ?? chain)}.`,
|
|
554
|
+
),
|
|
555
|
+
);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
544
559
|
let subagent = normalizeSubagent(frontmatter.subagent, fullPath, source, diagnostics);
|
|
545
560
|
const cwd = normalizeCwd(frontmatter.cwd, fullPath, source, diagnostics);
|
|
546
561
|
const inheritContext = normalizeInheritContext(frontmatter.inheritContext, fullPath, source, diagnostics);
|