pi-prompt-template-model 0.6.4 → 0.6.5
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 +5 -0
- package/README.md +29 -0
- package/args.ts +44 -0
- package/chain-parser.ts +18 -8
- package/index.ts +62 -22
- package/loop-utils.ts +31 -2
- package/package.json +1 -1
- package/prompt-loader.ts +28 -1
- package/subagent-step.ts +9 -3
- package/tool-manager.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.6.5] - 2026-03-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added delegated chain-step context summaries via `chainContext: summary` (chain frontmatter), `/chain-prompts ... --chain-context` (command-level), and per-step `--with-context` (single delegated chain steps).
|
|
9
|
+
|
|
5
10
|
## [0.6.4] - 2026-03-23
|
|
6
11
|
|
|
7
12
|
### Fixed
|
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ All fields are optional. Templates that don't use any extension features (no `mo
|
|
|
62
62
|
| `thinking` | — | Thinking level for the model: `off`, `minimal`, `low`, `medium`, `high`, or `xhigh`. |
|
|
63
63
|
| `description` | — | Short text shown next to the command in autocomplete. |
|
|
64
64
|
| `chain` | — | Declares a reusable pipeline of templates (`step -> step`). When set, the body is ignored. See [Chain Templates](#chain-templates). |
|
|
65
|
+
| `chainContext` | — | Chain templates only. Set to `summary` so delegated steps receive a compact summary of what previous steps did. Steps with `inheritContext: true` are excluded. See [Chain context for delegated steps](#chain-context-for-delegated-steps). |
|
|
65
66
|
|
|
66
67
|
### Execution Control
|
|
67
68
|
|
|
@@ -328,6 +329,33 @@ This runs the full analyze → fix chain 3 times, with fresh context between ite
|
|
|
328
329
|
|
|
329
330
|
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.
|
|
330
331
|
|
|
332
|
+
### Chain context for delegated steps
|
|
333
|
+
|
|
334
|
+
Delegated chain steps start fresh — they don't see what earlier steps did. Chain context prepends a compact summary of previous steps to each delegated task so later steps can build on earlier work.
|
|
335
|
+
|
|
336
|
+
Enable it chain-wide with `chainContext: summary` in frontmatter or `--chain-context` on the CLI:
|
|
337
|
+
|
|
338
|
+
```markdown
|
|
339
|
+
---
|
|
340
|
+
chain: analyze -> fix
|
|
341
|
+
chainContext: summary
|
|
342
|
+
---
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
/chain-prompts analyze -> fix --chain-context
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
To enable it for a single step, attach `--with-context` to that step name:
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
/chain-prompts analyze -> reviewer --with-context -> summarize
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Here only `reviewer` receives the summary of `analyze`. The `summarize` step does not.
|
|
356
|
+
|
|
357
|
+
Steps using `inheritContext: true` already fork the full parent conversation and skip the summary preamble. `--with-context` is not supported inside `parallel(...)` groups. When a chain uses `loop`, summaries reset each iteration.
|
|
358
|
+
|
|
331
359
|
### Parallel and looping from the CLI
|
|
332
360
|
|
|
333
361
|
Parallel groups work in `/chain-prompts` too:
|
|
@@ -359,6 +387,7 @@ Once enabled, the agent sees `run-prompt` in its tool list:
|
|
|
359
387
|
|
|
360
388
|
```
|
|
361
389
|
run-prompt({ command: "deslop --loop 5 --fresh" })
|
|
390
|
+
run-prompt({ command: "chain-prompts analyze -> fix --chain-context" })
|
|
362
391
|
run-prompt({ command: "chain-prompts analyze -> fix --loop 3" })
|
|
363
392
|
run-prompt({ command: "deslop --subagent" })
|
|
364
393
|
```
|
package/args.ts
CHANGED
|
@@ -163,6 +163,50 @@ export function extractLoopFlags(argsString: string): LoopFlags {
|
|
|
163
163
|
return { args: cleaned.trim(), fresh, converge: !noConverge };
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
export function extractChainContextFlag(argsString: string): { args: string; chainContext: boolean } {
|
|
167
|
+
let chainContext = false;
|
|
168
|
+
const tokensToRemove: Array<{ start: number; end: number }> = [];
|
|
169
|
+
|
|
170
|
+
let i = 0;
|
|
171
|
+
while (i < argsString.length) {
|
|
172
|
+
const char = argsString[i];
|
|
173
|
+
|
|
174
|
+
if (char === '"' || char === "'") {
|
|
175
|
+
const quote = char;
|
|
176
|
+
i++;
|
|
177
|
+
while (i < argsString.length && argsString[i] !== quote) i++;
|
|
178
|
+
if (i < argsString.length) i++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (/\s/.test(char)) {
|
|
183
|
+
i++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const tokenStart = i;
|
|
188
|
+
while (i < argsString.length && !/\s/.test(argsString[i])) i++;
|
|
189
|
+
const token = argsString.slice(tokenStart, i);
|
|
190
|
+
|
|
191
|
+
if (token === "--chain-context") {
|
|
192
|
+
chainContext = true;
|
|
193
|
+
tokensToRemove.push({ start: tokenStart, end: i });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (tokensToRemove.length === 0) {
|
|
198
|
+
return { args: argsString.trim(), chainContext: false };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
tokensToRemove.sort((a, b) => b.start - a.start);
|
|
202
|
+
let cleaned = argsString;
|
|
203
|
+
for (const { start, end } of tokensToRemove) {
|
|
204
|
+
cleaned = cleaned.slice(0, start) + cleaned.slice(end);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { args: cleaned.trim(), chainContext };
|
|
208
|
+
}
|
|
209
|
+
|
|
166
210
|
export function extractSubagentOverride(argsString: string): SubagentOverrideExtraction {
|
|
167
211
|
let override: SubagentOverride | undefined;
|
|
168
212
|
let cwdRaw: string | undefined;
|
package/chain-parser.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface ChainStep {
|
|
|
4
4
|
name: string;
|
|
5
5
|
args: string[];
|
|
6
6
|
loopCount?: number;
|
|
7
|
+
withContext?: boolean;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export interface ParallelChainStep {
|
|
@@ -80,15 +81,23 @@ function scanSegmentTokens(segment: string): SegmentToken[] {
|
|
|
80
81
|
return tokens;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
function
|
|
84
|
+
function extractStepFlags(segment: string): { cleanedSegment: string; loopCount?: number; withContext: boolean } {
|
|
84
85
|
const tokens = scanSegmentTokens(segment);
|
|
85
86
|
const loopTokenRanges: Array<{ start: number; end: number }> = [];
|
|
87
|
+
const withContextTokenRanges: Array<{ start: number; end: number }> = [];
|
|
86
88
|
let loopCount: number | undefined;
|
|
89
|
+
let withContext = false;
|
|
87
90
|
|
|
88
91
|
for (let i = 1; i < tokens.length; i++) {
|
|
89
92
|
const token = tokens[i];
|
|
90
93
|
if (token.quoted) continue;
|
|
91
94
|
|
|
95
|
+
if (token.value === "--with-context") {
|
|
96
|
+
withContext = true;
|
|
97
|
+
withContextTokenRanges.push({ start: token.start, end: token.end });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
if (token.value.startsWith("--loop=")) {
|
|
93
102
|
loopTokenRanges.push({ start: token.start, end: token.end });
|
|
94
103
|
const value = token.value.slice("--loop=".length);
|
|
@@ -114,17 +123,18 @@ function extractStepLoopCount(segment: string): { cleanedSegment: string; loopCo
|
|
|
114
123
|
}
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
const loopRangesToRemove = loopCount !== undefined ? loopTokenRanges : [];
|
|
127
|
+
if (loopRangesToRemove.length === 0 && withContextTokenRanges.length === 0) {
|
|
128
|
+
return { cleanedSegment: segment, withContext: false };
|
|
119
129
|
}
|
|
120
130
|
|
|
121
|
-
|
|
131
|
+
const rangesToRemove = [...loopRangesToRemove, ...withContextTokenRanges].sort((a, b) => b.start - a.start);
|
|
122
132
|
let cleanedSegment = segment;
|
|
123
|
-
for (const { start, end } of
|
|
133
|
+
for (const { start, end } of rangesToRemove) {
|
|
124
134
|
cleanedSegment = `${cleanedSegment.slice(0, start)}${cleanedSegment.slice(end)}`;
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
return { cleanedSegment: cleanedSegment.trim(), loopCount };
|
|
137
|
+
return { cleanedSegment: cleanedSegment.trim(), loopCount, withContext };
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
function splitByTopLevelSeparator(input: string, separator: string): string[] {
|
|
@@ -192,10 +202,10 @@ function findMatchingParen(segment: string, openIndex: number): number {
|
|
|
192
202
|
}
|
|
193
203
|
|
|
194
204
|
function parseSingleStepSegment(segment: string): ChainStep | undefined {
|
|
195
|
-
const { cleanedSegment, loopCount } =
|
|
205
|
+
const { cleanedSegment, loopCount, withContext } = extractStepFlags(segment);
|
|
196
206
|
const tokens = parseCommandArgs(cleanedSegment);
|
|
197
207
|
if (tokens.length === 0) return undefined;
|
|
198
|
-
return { name: tokens[0], args: tokens.slice(1), loopCount };
|
|
208
|
+
return { name: tokens[0], args: tokens.slice(1), loopCount, ...(withContext ? { withContext: true } : {}) };
|
|
199
209
|
}
|
|
200
210
|
|
|
201
211
|
function parseParallelStepSegment(segment: string): ParallelChainStep | undefined {
|
package/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
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
|
-
import { extractLoopCount, extractLoopFlags, extractSubagentOverride, parseCommandArgs, type SubagentOverride } from "./args.js";
|
|
4
|
+
import { extractChainContextFlag, extractLoopCount, extractLoopFlags, extractSubagentOverride, parseCommandArgs, type SubagentOverride } from "./args.js";
|
|
5
5
|
import { parseChainSteps, parseChainDeclaration, type ChainStep, type ChainStepOrParallel, type ParallelChainStep } from "./chain-parser.js";
|
|
6
|
-
import { generateIterationSummary, didIterationMakeChanges, getIterationEntries } from "./loop-utils.js";
|
|
6
|
+
import { generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries } from "./loop-utils.js";
|
|
7
7
|
import { notify, summarizePromptDiagnostics, diagnosticsFingerprint } from "./notifications.js";
|
|
8
8
|
import { preparePromptExecution } from "./prompt-execution.js";
|
|
9
9
|
import { buildPromptCommandDescription, expandCwdPath, loadPromptsWithModel, readSkillContent, resolveSkillPath, type PromptWithModel } from "./prompt-loader.js";
|
|
@@ -195,6 +195,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
195
195
|
currentModel: Model<any> | undefined,
|
|
196
196
|
override?: SubagentOverride,
|
|
197
197
|
inheritedModel?: Model<any>,
|
|
198
|
+
taskPreamble?: string,
|
|
198
199
|
): Promise<PromptStepResult | "aborted"> {
|
|
199
200
|
if (shouldDelegatePrompt(prompt, override)) {
|
|
200
201
|
try {
|
|
@@ -206,6 +207,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
206
207
|
currentModel,
|
|
207
208
|
override,
|
|
208
209
|
inheritedModel,
|
|
210
|
+
taskPreamble,
|
|
209
211
|
});
|
|
210
212
|
if (!delegated) {
|
|
211
213
|
notify(ctx, `Prompt \`${prompt.name}\` is not configured for delegated execution.`, "error");
|
|
@@ -496,6 +498,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
496
498
|
ctx: ExtensionCommandContext,
|
|
497
499
|
subagentOverride?: SubagentOverride,
|
|
498
500
|
cwdOverride?: string,
|
|
501
|
+
chainContextEnabled = false,
|
|
499
502
|
) {
|
|
500
503
|
const flattenChainSteps = (): ChainStep[] => {
|
|
501
504
|
const flattened: ChainStep[] = [];
|
|
@@ -524,6 +527,10 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
524
527
|
notify(ctx, `Step "${parallelStep.name}" in parallel() does not support per-task --loop.`, "error");
|
|
525
528
|
return false;
|
|
526
529
|
}
|
|
530
|
+
if (parallelStep.withContext === true) {
|
|
531
|
+
notify(ctx, `Step "${parallelStep.name}" in parallel() does not support per-task --with-context.`, "error");
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
527
534
|
const stepPrompt = prompts.get(parallelStep.name);
|
|
528
535
|
if (!stepPrompt) continue;
|
|
529
536
|
if (stepPrompt.chain) {
|
|
@@ -599,14 +606,18 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
599
606
|
}
|
|
600
607
|
: {
|
|
601
608
|
kind: "single" as const,
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
609
|
+
singleStep: {
|
|
610
|
+
prompt: {
|
|
611
|
+
...prompts.get(step.name)!,
|
|
612
|
+
...(cwdOverride ? { cwd: cwdOverride } : {}),
|
|
613
|
+
},
|
|
605
614
|
stepArgs: step.args,
|
|
606
615
|
stepLoop: step.loopCount ?? 1,
|
|
616
|
+
stepWithContext: step.withContext === true,
|
|
607
617
|
},
|
|
608
618
|
},
|
|
609
619
|
);
|
|
620
|
+
const chainStepSummaries: string[] = [];
|
|
610
621
|
let aborted = false;
|
|
611
622
|
let iterationChanged = false;
|
|
612
623
|
let loopPrefix = "";
|
|
@@ -619,10 +630,17 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
619
630
|
const stepNumber = index + 1;
|
|
620
631
|
if (stepTemplate.kind === "parallel") {
|
|
621
632
|
const stepNames = stepTemplate.tasks.map((task) => task.name).join(", ");
|
|
633
|
+
const stepLabel = `parallel(${stepNames})`;
|
|
622
634
|
notify(ctx, `${loopPrefix}Step ${stepNumber}/${templates.length}: parallel(${stepNames})`, "info");
|
|
623
635
|
if (ctx.hasUI) {
|
|
624
636
|
ctx.ui.setStatus("prompt-chain", ctx.ui.theme.fg("warning", `step ${stepNumber}/${templates.length}: parallel(${stepNames})`));
|
|
625
637
|
}
|
|
638
|
+
const stepStartId = ctx.sessionManager.getLeafId();
|
|
639
|
+
let taskPreamble: string | undefined;
|
|
640
|
+
const isForkedParallelContext = stepTemplate.tasks.some((task) => task.prompt.inheritContext === true);
|
|
641
|
+
if (chainContextEnabled && !isForkedParallelContext && chainStepSummaries.length > 0) {
|
|
642
|
+
taskPreamble = `[Previous chain steps]\n\n${chainStepSummaries.join("\n\n")}`;
|
|
643
|
+
}
|
|
626
644
|
|
|
627
645
|
let delegated;
|
|
628
646
|
try {
|
|
@@ -636,6 +654,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
636
654
|
prompt: task.prompt,
|
|
637
655
|
args: task.args.length > 0 ? task.args : sharedArgs,
|
|
638
656
|
})),
|
|
657
|
+
taskPreamble,
|
|
639
658
|
});
|
|
640
659
|
} catch (error) {
|
|
641
660
|
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
@@ -650,14 +669,21 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
650
669
|
|
|
651
670
|
currentModel = getCurrentModel(ctx);
|
|
652
671
|
currentThinking = pi.getThinkingLevel();
|
|
653
|
-
|
|
672
|
+
const stepEntries = getIterationEntries(ctx, stepStartId);
|
|
673
|
+
if (didIterationMakeChanges(stepEntries)) iterationChanged = true;
|
|
674
|
+
chainStepSummaries.push(generateChainStepSummary(stepEntries, stepLabel, stepNumber));
|
|
654
675
|
continue;
|
|
655
676
|
}
|
|
656
677
|
|
|
657
|
-
const
|
|
658
|
-
const stepLoop =
|
|
659
|
-
const effectiveArgs =
|
|
678
|
+
const singleStep = stepTemplate.singleStep;
|
|
679
|
+
const stepLoop = singleStep.stepLoop;
|
|
680
|
+
const effectiveArgs = singleStep.stepArgs.length > 0 ? singleStep.stepArgs : sharedArgs;
|
|
681
|
+
const shouldInjectSummary =
|
|
682
|
+
shouldDelegatePrompt(singleStep.prompt, subagentOverride) &&
|
|
683
|
+
singleStep.prompt.inheritContext !== true &&
|
|
684
|
+
(chainContextEnabled || singleStep.stepWithContext === true);
|
|
660
685
|
const outerLoopState = loopState ? { ...loopState } : null;
|
|
686
|
+
const stepStartId = ctx.sessionManager.getLeafId();
|
|
661
687
|
if (stepLoop > 1) {
|
|
662
688
|
loopState = { currentIteration: 1, totalIterations: stepLoop };
|
|
663
689
|
updateLoopStatus(ctx);
|
|
@@ -671,19 +697,27 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
671
697
|
}
|
|
672
698
|
|
|
673
699
|
const iterSuffix = stepLoop > 1 ? ` (iter ${stepIteration + 1}/${stepLoop})` : "";
|
|
674
|
-
notify(
|
|
700
|
+
notify(
|
|
701
|
+
ctx,
|
|
702
|
+
`${loopPrefix}Step ${stepNumber}/${templates.length}: ${singleStep.prompt.name}${iterSuffix} ${buildPromptCommandDescription(singleStep.prompt)}`,
|
|
703
|
+
"info",
|
|
704
|
+
);
|
|
675
705
|
if (ctx.hasUI) {
|
|
676
|
-
ctx.ui.setStatus("prompt-chain", ctx.ui.theme.fg("warning", `step ${stepNumber}/${templates.length}: ${
|
|
706
|
+
ctx.ui.setStatus("prompt-chain", ctx.ui.theme.fg("warning", `step ${stepNumber}/${templates.length}: ${singleStep.prompt.name}`));
|
|
677
707
|
}
|
|
708
|
+
const taskPreamble = shouldInjectSummary && chainStepSummaries.length > 0
|
|
709
|
+
? `[Previous chain steps]\n\n${chainStepSummaries.join("\n\n")}`
|
|
710
|
+
: undefined;
|
|
678
711
|
|
|
679
712
|
const stepIterationStartId = ctx.sessionManager.getLeafId();
|
|
680
713
|
const stepResult = await executePromptStep(
|
|
681
|
-
|
|
714
|
+
singleStep.prompt,
|
|
682
715
|
effectiveArgs,
|
|
683
716
|
ctx,
|
|
684
717
|
currentModel,
|
|
685
718
|
subagentOverride,
|
|
686
719
|
chainInheritedModel,
|
|
720
|
+
taskPreamble,
|
|
687
721
|
);
|
|
688
722
|
if (stepResult === "aborted") {
|
|
689
723
|
aborted = true;
|
|
@@ -693,11 +727,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
693
727
|
currentModel = getCurrentModel(ctx);
|
|
694
728
|
currentThinking = pi.getThinkingLevel();
|
|
695
729
|
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (stepChanged) iterationChanged = true;
|
|
700
|
-
if (stepLoop > 1 && template.converge !== false && !stepChanged) {
|
|
730
|
+
const stepIterationEntries = getIterationEntries(ctx, stepIterationStartId);
|
|
731
|
+
const stepIterationChanged = didIterationMakeChanges(stepIterationEntries);
|
|
732
|
+
if (stepLoop > 1 && singleStep.prompt.converge !== false && !stepIterationChanged) {
|
|
701
733
|
break;
|
|
702
734
|
}
|
|
703
735
|
}
|
|
@@ -709,6 +741,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
709
741
|
}
|
|
710
742
|
|
|
711
743
|
if (aborted) break;
|
|
744
|
+
const stepEntries = getIterationEntries(ctx, stepStartId);
|
|
745
|
+
if (didIterationMakeChanges(stepEntries)) iterationChanged = true;
|
|
746
|
+
chainStepSummaries.push(generateChainStepSummary(stepEntries, singleStep.prompt.name, stepNumber));
|
|
712
747
|
}
|
|
713
748
|
|
|
714
749
|
if (aborted) break;
|
|
@@ -782,11 +817,13 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
782
817
|
const argsWithoutSubagent = subagent.args;
|
|
783
818
|
|
|
784
819
|
if (prompt.chain) {
|
|
785
|
-
const
|
|
820
|
+
const extracted = extractChainContextFlag(argsWithoutSubagent);
|
|
821
|
+
const chainContextEnabled = extracted.chainContext || prompt.chainContext === "summary";
|
|
822
|
+
const loop = extractLoopCount(extracted.args);
|
|
786
823
|
let totalIterations: number | null = prompt.loop ?? 1;
|
|
787
824
|
let fresh = false;
|
|
788
825
|
let converge = true;
|
|
789
|
-
let cleanedArgs =
|
|
826
|
+
let cleanedArgs = extracted.args;
|
|
790
827
|
|
|
791
828
|
if (loop) {
|
|
792
829
|
totalIterations = loop.loopCount;
|
|
@@ -794,7 +831,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
794
831
|
converge = loop.converge;
|
|
795
832
|
cleanedArgs = loop.args;
|
|
796
833
|
} else if (prompt.loop !== undefined) {
|
|
797
|
-
const flags = extractLoopFlags(
|
|
834
|
+
const flags = extractLoopFlags(extracted.args);
|
|
798
835
|
fresh = flags.fresh;
|
|
799
836
|
converge = flags.converge;
|
|
800
837
|
cleanedArgs = flags.args;
|
|
@@ -821,6 +858,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
821
858
|
ctx,
|
|
822
859
|
subagent.override,
|
|
823
860
|
cwdOverride,
|
|
861
|
+
chainContextEnabled,
|
|
824
862
|
);
|
|
825
863
|
return;
|
|
826
864
|
}
|
|
@@ -962,8 +1000,9 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
962
1000
|
notify(ctx, `Invalid --cwd path: must be absolute`, "error");
|
|
963
1001
|
return;
|
|
964
1002
|
}
|
|
965
|
-
const
|
|
966
|
-
const
|
|
1003
|
+
const extracted = extractChainContextFlag(subagent.args);
|
|
1004
|
+
const loop = extractLoopCount(extracted.args);
|
|
1005
|
+
const cleanedArgs = loop ? loop.args : extracted.args;
|
|
967
1006
|
|
|
968
1007
|
const { steps, sharedArgs, invalidSegments } = parseChainSteps(cleanedArgs);
|
|
969
1008
|
if (invalidSegments.length > 0) {
|
|
@@ -985,6 +1024,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
985
1024
|
ctx,
|
|
986
1025
|
subagent.override,
|
|
987
1026
|
runtimeCwd,
|
|
1027
|
+
extracted.chainContext,
|
|
988
1028
|
);
|
|
989
1029
|
}
|
|
990
1030
|
|
package/loop-utils.ts
CHANGED
|
@@ -7,6 +7,13 @@ interface DelegatedMessageDetails {
|
|
|
7
7
|
parallelResults?: Array<{ messages?: Message[] }>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
interface CollectedSummaryData {
|
|
11
|
+
filesRead: Set<string>;
|
|
12
|
+
filesWritten: Set<string>;
|
|
13
|
+
commandCount: number;
|
|
14
|
+
lastAssistantText: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
function collectAssistantActions(messages: Message[], filesRead: Set<string>, filesWritten: Set<string>): { commandCount: number; lastText: string } {
|
|
11
18
|
let commandCount = 0;
|
|
12
19
|
let lastText = "";
|
|
@@ -39,7 +46,7 @@ function delegatedDetails(entry: SessionEntry): DelegatedMessageDetails | undefi
|
|
|
39
46
|
return entry.details as DelegatedMessageDetails;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
function collectSummaryData(entries: SessionEntry[]): CollectedSummaryData {
|
|
43
50
|
const filesRead = new Set<string>();
|
|
44
51
|
const filesWritten = new Set<string>();
|
|
45
52
|
let commandCount = 0;
|
|
@@ -68,7 +75,18 @@ export function generateIterationSummary(entries: SessionEntry[], task: string,
|
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
|
|
78
|
+
return {
|
|
79
|
+
filesRead,
|
|
80
|
+
filesWritten,
|
|
81
|
+
commandCount,
|
|
82
|
+
lastAssistantText,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatSummary(header: string, entries: SessionEntry[]): string {
|
|
87
|
+
const { filesRead, filesWritten, commandCount, lastAssistantText } = collectSummaryData(entries);
|
|
88
|
+
|
|
89
|
+
let summary = header;
|
|
72
90
|
|
|
73
91
|
const actionParts: string[] = [];
|
|
74
92
|
if (filesRead.size > 0) actionParts.push(`read ${filesRead.size} file(s)`);
|
|
@@ -87,6 +105,17 @@ export function generateIterationSummary(entries: SessionEntry[], task: string,
|
|
|
87
105
|
return summary;
|
|
88
106
|
}
|
|
89
107
|
|
|
108
|
+
export function generateIterationSummary(entries: SessionEntry[], task: string, iteration: number, totalIterations: number | null): string {
|
|
109
|
+
const header = totalIterations !== null
|
|
110
|
+
? `[Loop iteration ${iteration}/${totalIterations}]\nTask: "${task}"`
|
|
111
|
+
: `[Loop iteration ${iteration}]\nTask: "${task}"`;
|
|
112
|
+
return formatSummary(header, entries);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function generateChainStepSummary(entries: SessionEntry[], stepLabel: string, stepNumber: number): string {
|
|
116
|
+
return formatSummary(`Step ${stepNumber} — ${stepLabel}:`, entries);
|
|
117
|
+
}
|
|
118
|
+
|
|
90
119
|
export function didIterationMakeChanges(entries: SessionEntry[]): boolean {
|
|
91
120
|
for (const entry of entries) {
|
|
92
121
|
if (entry.type === "message") {
|
package/package.json
CHANGED
package/prompt-loader.ts
CHANGED
|
@@ -38,6 +38,7 @@ export interface PromptWithModel {
|
|
|
38
38
|
content: string;
|
|
39
39
|
models: string[];
|
|
40
40
|
chain?: string;
|
|
41
|
+
chainContext?: "summary";
|
|
41
42
|
restore: boolean;
|
|
42
43
|
skill?: string;
|
|
43
44
|
thinking?: ThinkingLevel;
|
|
@@ -432,6 +433,29 @@ function normalizeChain(
|
|
|
432
433
|
return undefined;
|
|
433
434
|
}
|
|
434
435
|
|
|
436
|
+
function normalizeChainContext(
|
|
437
|
+
value: unknown,
|
|
438
|
+
filePath: string,
|
|
439
|
+
source: PromptSource,
|
|
440
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
441
|
+
): "summary" | undefined {
|
|
442
|
+
if (value === undefined) return undefined;
|
|
443
|
+
if (typeof value === "string") {
|
|
444
|
+
const normalized = value.trim().toLowerCase();
|
|
445
|
+
if (normalized === "summary") return "summary";
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
diagnostics.push(
|
|
449
|
+
createDiagnostic(
|
|
450
|
+
"invalid-chain-context",
|
|
451
|
+
filePath,
|
|
452
|
+
source,
|
|
453
|
+
`Ignoring invalid chainContext value in ${filePath}: frontmatter field "chainContext" must be "summary".`,
|
|
454
|
+
),
|
|
455
|
+
);
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
|
|
435
459
|
function normalizeThinking(
|
|
436
460
|
value: unknown,
|
|
437
461
|
filePath: string,
|
|
@@ -542,6 +566,7 @@ function loadPromptsWithModelFromDir(
|
|
|
542
566
|
if (!frontmatter) continue;
|
|
543
567
|
const { body } = parsed;
|
|
544
568
|
const chain = normalizeChain(frontmatter.chain, fullPath, source, diagnostics);
|
|
569
|
+
const chainContext = chain ? normalizeChainContext(frontmatter.chainContext, fullPath, source, diagnostics) : undefined;
|
|
545
570
|
if (chain && /\bparallel\s*\(/.test(chain)) {
|
|
546
571
|
const parsedChain = parseChainDeclaration(chain);
|
|
547
572
|
if (parsedChain.invalidSegments.length > 0 || parsedChain.steps.length === 0) {
|
|
@@ -637,6 +662,7 @@ function loadPromptsWithModelFromDir(
|
|
|
637
662
|
content: body,
|
|
638
663
|
models,
|
|
639
664
|
chain: chain || undefined,
|
|
665
|
+
chainContext,
|
|
640
666
|
restore,
|
|
641
667
|
skill,
|
|
642
668
|
thinking,
|
|
@@ -721,8 +747,9 @@ export function loadPromptsWithModel(cwd: string): LoadPromptsWithModelResult {
|
|
|
721
747
|
export function buildPromptCommandDescription(prompt: PromptWithModel): string {
|
|
722
748
|
const sourceLabel = prompt.subdir ? `(${prompt.source}:${prompt.subdir})` : `(${prompt.source})`;
|
|
723
749
|
if (prompt.chain) {
|
|
750
|
+
const chainContextLabel = prompt.chainContext ? ` ${prompt.chainContext}` : "";
|
|
724
751
|
const cwdLabel = prompt.cwd ? ` cwd:${prompt.cwd}` : "";
|
|
725
|
-
const details = `[chain: ${prompt.chain}${cwdLabel}] ${sourceLabel}`;
|
|
752
|
+
const details = `[chain: ${prompt.chain}${chainContextLabel}${cwdLabel}] ${sourceLabel}`;
|
|
726
753
|
return prompt.description ? `${prompt.description} ${details}` : details;
|
|
727
754
|
}
|
|
728
755
|
const modelLabel = prompt.models.length > 0 ? prompt.models.map((model) => model.split("/").pop() || model).join("|") : "current";
|
package/subagent-step.ts
CHANGED
|
@@ -38,6 +38,7 @@ interface DelegatedPromptBaseOptions {
|
|
|
38
38
|
override?: SubagentOverride;
|
|
39
39
|
signal?: AbortSignal;
|
|
40
40
|
inheritedModel?: Model<any>;
|
|
41
|
+
taskPreamble?: string;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
interface DelegatedSinglePromptOptions extends DelegatedPromptBaseOptions {
|
|
@@ -156,6 +157,7 @@ async function prepareDelegatedTask(
|
|
|
156
157
|
currentModel: Model<any> | undefined,
|
|
157
158
|
override: SubagentOverride | undefined,
|
|
158
159
|
inheritedModel: Model<any> | undefined,
|
|
160
|
+
taskPreamble: string | undefined,
|
|
159
161
|
runtime: Awaited<ReturnType<typeof ensureSubagentRuntime>>,
|
|
160
162
|
): Promise<PreparedDelegatedTask> {
|
|
161
163
|
const requestedAgent = resolveDelegationName(task.prompt, override);
|
|
@@ -183,11 +185,15 @@ async function prepareDelegatedTask(
|
|
|
183
185
|
if (effectiveCwd !== ctx.cwd && !existsSync(effectiveCwd)) {
|
|
184
186
|
throw new Error(`cwd directory does not exist: ${effectiveCwd}`);
|
|
185
187
|
}
|
|
188
|
+
let taskText = prepared.content;
|
|
189
|
+
if (!task.prompt.inheritContext && taskPreamble) {
|
|
190
|
+
taskText = `${taskPreamble}\n\n---\n\n${prepared.content}`;
|
|
191
|
+
}
|
|
186
192
|
|
|
187
193
|
return {
|
|
188
194
|
promptName: task.prompt.name,
|
|
189
195
|
agent,
|
|
190
|
-
task:
|
|
196
|
+
task: taskText,
|
|
191
197
|
context: task.prompt.inheritContext ? "fork" : "fresh",
|
|
192
198
|
model: `${prepared.selectedModel.model.provider}/${prepared.selectedModel.model.id}`,
|
|
193
199
|
cwd: effectiveCwd,
|
|
@@ -429,7 +435,7 @@ async function requestDelegatedRun(
|
|
|
429
435
|
}
|
|
430
436
|
|
|
431
437
|
export async function executeSubagentPromptStep(options: DelegatedPromptOptions): Promise<DelegatedPromptOutcome | undefined> {
|
|
432
|
-
const { pi, ctx, currentModel, override, signal, inheritedModel } = options;
|
|
438
|
+
const { pi, ctx, currentModel, override, signal, inheritedModel, taskPreamble } = options;
|
|
433
439
|
const runtime = await ensureSubagentRuntime(ctx.cwd);
|
|
434
440
|
const isParallelRequest = "parallel" in options;
|
|
435
441
|
|
|
@@ -440,7 +446,7 @@ export async function executeSubagentPromptStep(options: DelegatedPromptOptions)
|
|
|
440
446
|
|
|
441
447
|
const preparedTasks: PreparedDelegatedTask[] = [];
|
|
442
448
|
for (const task of tasks) {
|
|
443
|
-
const preparedTask = await prepareDelegatedTask(task, ctx, currentModel, override, inheritedModel, runtime);
|
|
449
|
+
const preparedTask = await prepareDelegatedTask(task, ctx, currentModel, override, inheritedModel, taskPreamble, runtime);
|
|
444
450
|
preparedTasks.push(preparedTask);
|
|
445
451
|
}
|
|
446
452
|
|
package/tool-manager.ts
CHANGED
|
@@ -61,10 +61,10 @@ export function createToolManager(pi: ExtensionAPI, deps: ToolManagerDeps) {
|
|
|
61
61
|
"Supports --loop for loops (e.g. 'deslop --loop 5', 'deslop --loop=5', 'deslop --loop' for unlimited until convergence with a 50-iteration cap), " +
|
|
62
62
|
"--fresh for context collapse between iterations, and --no-converge to disable early stopping for bounded loops. " +
|
|
63
63
|
"Supports runtime delegation override via --subagent, --subagent=<name>, or --subagent:<name>. " +
|
|
64
|
-
"Use 'chain-prompts template1 -> template2' for chaining.",
|
|
64
|
+
"Use 'chain-prompts template1 -> template2' for chaining and add --chain-context to pass previous step summaries into delegated steps.",
|
|
65
65
|
parameters: Type.Object({
|
|
66
66
|
command: Type.String({
|
|
67
|
-
description: "Template name and arguments (e.g. 'deslop --loop 5 --fresh', 'deslop --subagent:worker', 'deslop --subagent', 'chain-prompts analyze -> fix --loop=3')",
|
|
67
|
+
description: "Template name and arguments (e.g. 'deslop --loop 5 --fresh', 'deslop --subagent:worker', 'deslop --subagent', 'chain-prompts analyze -> fix --chain-context', 'chain-prompts analyze -> fix --loop=3')",
|
|
68
68
|
}),
|
|
69
69
|
}),
|
|
70
70
|
execute: async (_id, params) => {
|