pi-prompt-template-model 0.8.2 → 0.9.1
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 +16 -0
- package/README.md +2 -1
- package/index.ts +56 -3
- package/loop-utils.ts +10 -4
- package/package.json +4 -1
- package/prompt-loader.ts +42 -1
- package/tool-manager.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.1] - 2026-04-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- `boomerang: true` prompt collapses now signal rewind to keep current files automatically, avoiding the restore-options prompt during collapse.
|
|
9
|
+
|
|
10
|
+
## [0.9.0] - 2026-04-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Added `boomerang: true` prompt frontmatter so non-chain templates, including looped templates, can run through prompt-template-model and then collapse their execution context back to the pre-run branch.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Migrated extension tool schemas from `@sinclair/typebox` to `typebox` 1.x so packaged installs follow Pi's current extension runtime contract.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Added `typebox` as a runtime dependency for packaged installs.
|
|
20
|
+
|
|
5
21
|
## [0.8.2] - 2026-04-21
|
|
6
22
|
|
|
7
23
|
### Added
|
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ All fields are optional. Templates that don't use any extension features (no `mo
|
|
|
73
73
|
| `rotate` | `false` | When `true` and looping, cycle through models in the `model` list instead of using fallback semantics. Thinking levels can also be comma-separated to pair with each model. |
|
|
74
74
|
| `fresh` | `false` | When looping, collapse the conversation between iterations to a brief summary instead of carrying the full context forward. Saves tokens on long loops. |
|
|
75
75
|
| `converge` | `true` | When looping, stop early if an iteration makes no file changes. Set `false` to always run every iteration. |
|
|
76
|
+
| `boomerang` | `false` | After a non-chain prompt finishes, collapse its execution context back to the branch point with a brief summary. Works with loops, including `fresh` loop summaries. Useful for review prompts like `/double-check`. |
|
|
76
77
|
| `worktree` | `false` | When `true`, parallel delegated work runs in separate git worktrees. Valid on chain templates with `parallel()` steps, on delegated prompts with `parallel: N`, and on compare templates via `bestOfN.worktree`. |
|
|
77
78
|
|
|
78
79
|
### Delegation
|
|
@@ -291,7 +292,7 @@ This repo ships one example compare prompt under `examples/`:
|
|
|
291
292
|
- `examples/best-of-n.md` installs as `/best-of-n`, runs in the current repo, and shows mixed workers, mixed reviewers, and an optional final apply phase.
|
|
292
293
|
- Smoke test: `/best-of-n smoke test`.
|
|
293
294
|
|
|
294
|
-
Install
|
|
295
|
+
Install it manually from this repo checkout (or from the installed package directory):
|
|
295
296
|
|
|
296
297
|
```bash
|
|
297
298
|
PTM_DIR=/path/to/pi-prompt-template-model
|
package/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type SubagentOverride,
|
|
17
17
|
} from "./args.js";
|
|
18
18
|
import { parseChainSteps, parseChainDeclaration, type ChainStep, type ChainStepOrParallel, type ParallelChainStep } from "./chain-parser.js";
|
|
19
|
-
import { generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries, wasIterationAborted } from "./loop-utils.js";
|
|
19
|
+
import { generateBoomerangSummary, generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries, wasIterationAborted } from "./loop-utils.js";
|
|
20
20
|
import { selectModelCandidate } from "./model-selection.js";
|
|
21
21
|
import { notify, summarizePromptDiagnostics, diagnosticsFingerprint } from "./notifications.js";
|
|
22
22
|
import { preparePromptExecution, renderPromptForResolvedModel } from "./prompt-execution.js";
|
|
@@ -56,6 +56,12 @@ interface FreshCollapse {
|
|
|
56
56
|
totalIterations: number | null;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
interface BoomerangCollapse {
|
|
60
|
+
targetId: string;
|
|
61
|
+
task: string;
|
|
62
|
+
previousSummaries: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
interface PendingSkillMessage {
|
|
60
66
|
customType: "skill-loaded";
|
|
61
67
|
content: string;
|
|
@@ -108,6 +114,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
108
114
|
let chainActive = false;
|
|
109
115
|
let loopState: LoopState | null = null;
|
|
110
116
|
let freshCollapse: FreshCollapse | null = null;
|
|
117
|
+
let boomerangCollapse: BoomerangCollapse | null = null;
|
|
111
118
|
let accumulatedSummaries: string[] = [];
|
|
112
119
|
let lastDiagnostics = "";
|
|
113
120
|
let storedCommandCtx: ExtensionCommandContext | null = null;
|
|
@@ -918,6 +925,28 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
918
925
|
}
|
|
919
926
|
}
|
|
920
927
|
|
|
928
|
+
async function collapseBoomerangPrompt(
|
|
929
|
+
ctx: ExtensionContext,
|
|
930
|
+
name: string,
|
|
931
|
+
targetId: string | null,
|
|
932
|
+
previousSummaries: string[] = [],
|
|
933
|
+
) {
|
|
934
|
+
if (!targetId) {
|
|
935
|
+
notify(ctx, `Cannot boomerang prompt \`${name}\`: no session entry to return to.`, "warning");
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
boomerangCollapse = { targetId, task: name, previousSummaries };
|
|
940
|
+
try {
|
|
941
|
+
(globalThis as typeof globalThis & { __boomerangCollapseInProgress?: boolean }).__boomerangCollapseInProgress = true;
|
|
942
|
+
const result = await ctx.navigateTree(targetId, { summarize: true });
|
|
943
|
+
if (result.cancelled) notify(ctx, `Boomerang cancelled for prompt \`${name}\``, "warning");
|
|
944
|
+
} finally {
|
|
945
|
+
(globalThis as typeof globalThis & { __boomerangCollapseInProgress?: boolean }).__boomerangCollapseInProgress = false;
|
|
946
|
+
boomerangCollapse = null;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
921
950
|
async function runPromptLoop(
|
|
922
951
|
name: string,
|
|
923
952
|
cleanedArgs: string,
|
|
@@ -942,10 +971,11 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
942
971
|
let currentThinking = savedThinking;
|
|
943
972
|
const shouldRestore = initialPrompt.restore;
|
|
944
973
|
const useFresh = freshFlag || initialPrompt.fresh === true;
|
|
974
|
+
const shouldBoomerang = initialPrompt.boomerang === true;
|
|
945
975
|
const effectiveMax = totalIterations ?? UNLIMITED_LOOP_CAP;
|
|
946
976
|
const isUnlimited = totalIterations === null;
|
|
947
977
|
const useConverge = converge && initialPrompt.converge !== false;
|
|
948
|
-
const anchorId = useFresh ? ctx.sessionManager.getLeafId() : null;
|
|
978
|
+
const anchorId = useFresh || shouldBoomerang ? ctx.sessionManager.getLeafId() : null;
|
|
949
979
|
|
|
950
980
|
loopState = { currentIteration: 1, totalIterations };
|
|
951
981
|
accumulatedSummaries = [];
|
|
@@ -955,6 +985,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
955
985
|
let loopErrorState: ExecutionErrorState = { hasError: false, error: undefined };
|
|
956
986
|
let lastDelegatedText: string | undefined;
|
|
957
987
|
let loopAborted = false;
|
|
988
|
+
let boomerangPreviousSummaries: string[] = [];
|
|
958
989
|
|
|
959
990
|
try {
|
|
960
991
|
for (let i = 0; i < effectiveMax; i++) {
|
|
@@ -1024,7 +1055,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1024
1055
|
break;
|
|
1025
1056
|
}
|
|
1026
1057
|
|
|
1027
|
-
if (anchorId && i < effectiveMax - 1) {
|
|
1058
|
+
if (useFresh && anchorId && i < effectiveMax - 1) {
|
|
1028
1059
|
freshCollapse = { targetId: anchorId, task: name, iteration: i + 1, totalIterations };
|
|
1029
1060
|
const result = await ctx.navigateTree(anchorId, { summarize: true });
|
|
1030
1061
|
freshCollapse = null;
|
|
@@ -1049,9 +1080,11 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1049
1080
|
"loop",
|
|
1050
1081
|
);
|
|
1051
1082
|
|
|
1083
|
+
boomerangPreviousSummaries = accumulatedSummaries;
|
|
1052
1084
|
loopState = null;
|
|
1053
1085
|
pendingSkillMessage = undefined;
|
|
1054
1086
|
freshCollapse = null;
|
|
1087
|
+
boomerangCollapse = null;
|
|
1055
1088
|
accumulatedSummaries = [];
|
|
1056
1089
|
updateLoopStatus(ctx);
|
|
1057
1090
|
|
|
@@ -1069,6 +1102,10 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1069
1102
|
await ctx.waitForIdle();
|
|
1070
1103
|
}
|
|
1071
1104
|
|
|
1105
|
+
if (!loopErrorState.hasError && !loopAborted && shouldBoomerang) {
|
|
1106
|
+
await collapseBoomerangPrompt(ctx, name, anchorId, boomerangPreviousSummaries);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1072
1109
|
if (loopErrorState.hasError) {
|
|
1073
1110
|
throw loopErrorState.error;
|
|
1074
1111
|
}
|
|
@@ -1402,6 +1439,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1402
1439
|
chainActive = false;
|
|
1403
1440
|
loopState = null;
|
|
1404
1441
|
freshCollapse = null;
|
|
1442
|
+
boomerangCollapse = null;
|
|
1405
1443
|
accumulatedSummaries = [];
|
|
1406
1444
|
updateLoopStatus(ctx);
|
|
1407
1445
|
if (ctx.hasUI) {
|
|
@@ -1549,6 +1587,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1549
1587
|
};
|
|
1550
1588
|
const savedModel = getCurrentModel(ctx);
|
|
1551
1589
|
const savedThinking = pi.getThinkingLevel();
|
|
1590
|
+
const boomerangTargetId = effectivePrompt.boomerang ? ctx.sessionManager.getLeafId() : null;
|
|
1552
1591
|
const stepResult = await executePromptStep(
|
|
1553
1592
|
effectivePrompt,
|
|
1554
1593
|
parseCommandArgs(argsWithoutSubagent),
|
|
@@ -1573,6 +1612,10 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1573
1612
|
previousThinking = savedThinking;
|
|
1574
1613
|
}
|
|
1575
1614
|
}
|
|
1615
|
+
|
|
1616
|
+
if (effectivePrompt.boomerang) {
|
|
1617
|
+
await collapseBoomerangPrompt(ctx, name, boomerangTargetId);
|
|
1618
|
+
}
|
|
1576
1619
|
}
|
|
1577
1620
|
|
|
1578
1621
|
function resetSessionScopedState(ctx: ExtensionContext) {
|
|
@@ -1581,6 +1624,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1581
1624
|
previousModel = undefined;
|
|
1582
1625
|
previousThinking = undefined;
|
|
1583
1626
|
runtimeModel = ctx.model;
|
|
1627
|
+
boomerangCollapse = null;
|
|
1584
1628
|
toolManager.clearQueue();
|
|
1585
1629
|
refreshPrompts(ctx.cwd, ctx);
|
|
1586
1630
|
}
|
|
@@ -1644,6 +1688,15 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1644
1688
|
});
|
|
1645
1689
|
|
|
1646
1690
|
pi.on("session_before_tree", async (event) => {
|
|
1691
|
+
if (boomerangCollapse && event.preparation.targetId === boomerangCollapse.targetId) {
|
|
1692
|
+
const summary = generateBoomerangSummary(event.preparation.entriesToSummarize, boomerangCollapse.task);
|
|
1693
|
+
return {
|
|
1694
|
+
summary: {
|
|
1695
|
+
summary: [...boomerangCollapse.previousSummaries, summary].join("\n\n---\n\n"),
|
|
1696
|
+
},
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1647
1700
|
if (!freshCollapse) return;
|
|
1648
1701
|
if (event.preparation.targetId !== freshCollapse.targetId) return;
|
|
1649
1702
|
|
package/loop-utils.ts
CHANGED
|
@@ -88,7 +88,7 @@ function collectSummaryData(entries: SessionEntry[]): CollectedSummaryData {
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
function formatSummary(header: string, entries: SessionEntry[]): string {
|
|
91
|
+
function formatSummary(header: string, entries: SessionEntry[], preserveOutcome = false): string {
|
|
92
92
|
const { filesRead, filesWritten, commandCount, lastAssistantText } = collectSummaryData(entries);
|
|
93
93
|
|
|
94
94
|
let summary = header;
|
|
@@ -102,9 +102,11 @@ function formatSummary(header: string, entries: SessionEntry[]): string {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
if (lastAssistantText) {
|
|
105
|
-
const cleaned =
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
const cleaned = preserveOutcome
|
|
106
|
+
? lastAssistantText.replace(/\r\n?/g, "\n").trim()
|
|
107
|
+
: lastAssistantText.replace(/\n+/g, " ").trim();
|
|
108
|
+
const outcome = preserveOutcome || cleaned.length <= 500 ? cleaned : `${cleaned.slice(0, 500)}...`;
|
|
109
|
+
summary += `\nOutcome: ${outcome}`;
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
return summary;
|
|
@@ -117,6 +119,10 @@ export function generateIterationSummary(entries: SessionEntry[], task: string,
|
|
|
117
119
|
return formatSummary(header, entries);
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
export function generateBoomerangSummary(entries: SessionEntry[], task: string): string {
|
|
123
|
+
return formatSummary(`[Boomerang]\nTask: "${task}"`, entries, true);
|
|
124
|
+
}
|
|
125
|
+
|
|
120
126
|
export function generateChainStepSummary(entries: SessionEntry[], stepLabel: string, stepNumber: number): string {
|
|
121
127
|
return formatSummary(`Step ${stepNumber} — ${stepLabel}:`, entries);
|
|
122
128
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-prompt-template-model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Prompt template model selector extension for pi coding agent",
|
|
6
6
|
"author": "Nico Bailon",
|
|
@@ -49,6 +49,9 @@
|
|
|
49
49
|
"scripts": {
|
|
50
50
|
"test": "tsx --test test/**/*.test.ts"
|
|
51
51
|
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"typebox": "^1.1.24"
|
|
54
|
+
},
|
|
52
55
|
"devDependencies": {
|
|
53
56
|
"@mariozechner/pi-agent-core": "^0.65.0",
|
|
54
57
|
"@mariozechner/pi-ai": "^0.65.0",
|
package/prompt-loader.ts
CHANGED
|
@@ -74,6 +74,7 @@ export interface PromptWithModel {
|
|
|
74
74
|
fresh?: boolean;
|
|
75
75
|
loop?: number | null;
|
|
76
76
|
converge?: boolean;
|
|
77
|
+
boomerang?: boolean;
|
|
77
78
|
parallel?: number;
|
|
78
79
|
worktree?: boolean;
|
|
79
80
|
deterministic?: DeterministicStep;
|
|
@@ -303,6 +304,31 @@ function normalizeRotate(
|
|
|
303
304
|
return false;
|
|
304
305
|
}
|
|
305
306
|
|
|
307
|
+
function normalizeBoomerang(
|
|
308
|
+
value: unknown,
|
|
309
|
+
filePath: string,
|
|
310
|
+
source: PromptSource,
|
|
311
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
312
|
+
): boolean {
|
|
313
|
+
if (value === undefined) return false;
|
|
314
|
+
if (typeof value === "boolean") return value;
|
|
315
|
+
if (typeof value === "string") {
|
|
316
|
+
const normalized = value.trim().toLowerCase();
|
|
317
|
+
if (normalized === "true") return true;
|
|
318
|
+
if (normalized === "false") return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
diagnostics.push(
|
|
322
|
+
createDiagnostic(
|
|
323
|
+
"invalid-boomerang",
|
|
324
|
+
filePath,
|
|
325
|
+
source,
|
|
326
|
+
`Using default boomerang=false for ${filePath}: frontmatter field "boomerang" must be true or false.`,
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
306
332
|
function normalizeLoop(
|
|
307
333
|
value: unknown,
|
|
308
334
|
filePath: string,
|
|
@@ -1643,6 +1669,18 @@ function loadPromptsWithModelFromDir(
|
|
|
1643
1669
|
const fresh = normalizeFresh(frontmatter.fresh, fullPath, source, diagnostics);
|
|
1644
1670
|
const loop = normalizeLoop(frontmatter.loop, fullPath, source, diagnostics);
|
|
1645
1671
|
const converge = normalizeConverge(frontmatter.converge, fullPath, source, diagnostics);
|
|
1672
|
+
let boomerang = normalizeBoomerang(frontmatter.boomerang, fullPath, source, diagnostics);
|
|
1673
|
+
if (chain && boomerang) {
|
|
1674
|
+
diagnostics.push(
|
|
1675
|
+
createDiagnostic(
|
|
1676
|
+
"invalid-boomerang-chain",
|
|
1677
|
+
fullPath,
|
|
1678
|
+
source,
|
|
1679
|
+
`Ignoring boomerang in ${fullPath}: frontmatter fields "chain" and "boomerang" cannot be combined.`,
|
|
1680
|
+
),
|
|
1681
|
+
);
|
|
1682
|
+
boomerang = false;
|
|
1683
|
+
}
|
|
1646
1684
|
if (loop !== undefined && deterministic !== undefined) {
|
|
1647
1685
|
diagnostics.push(
|
|
1648
1686
|
createDiagnostic(
|
|
@@ -1695,6 +1733,7 @@ function loadPromptsWithModelFromDir(
|
|
|
1695
1733
|
fresh === true ||
|
|
1696
1734
|
loop !== undefined ||
|
|
1697
1735
|
converge === false ||
|
|
1736
|
+
boomerang === true ||
|
|
1698
1737
|
safeParallel !== undefined ||
|
|
1699
1738
|
deterministic !== undefined ||
|
|
1700
1739
|
hasLineup ||
|
|
@@ -1721,6 +1760,7 @@ function loadPromptsWithModelFromDir(
|
|
|
1721
1760
|
fresh: fresh || undefined,
|
|
1722
1761
|
loop: loop !== undefined ? loop : undefined,
|
|
1723
1762
|
converge: converge === false ? false : undefined,
|
|
1763
|
+
boomerang: boomerang || undefined,
|
|
1724
1764
|
parallel: safeParallel,
|
|
1725
1765
|
worktree: safeWorktree,
|
|
1726
1766
|
deterministic,
|
|
@@ -1821,6 +1861,7 @@ export function buildPromptCommandDescription(prompt: PromptWithModel): string {
|
|
|
1821
1861
|
const thinkingValue = prompt.thinkingLevels ? prompt.thinkingLevels.join(",") : prompt.thinking;
|
|
1822
1862
|
const thinkingLabel = thinkingValue ? ` ${thinkingValue}` : "";
|
|
1823
1863
|
const loopLabel = prompt.loop !== undefined ? ` loop:${prompt.loop === null ? "unlimited" : prompt.loop}` : "";
|
|
1864
|
+
const boomerangLabel = prompt.boomerang ? " boomerang" : "";
|
|
1824
1865
|
const subagentLabel = prompt.subagent ? ` subagent:${prompt.subagent === true ? "delegate" : prompt.subagent}` : "";
|
|
1825
1866
|
const parallelLabel = prompt.parallel !== undefined ? ` parallel:${prompt.parallel}` : "";
|
|
1826
1867
|
const deterministicLabel = prompt.deterministic ? ` deterministic-step:${prompt.deterministic.handoff}` : "";
|
|
@@ -1831,7 +1872,7 @@ export function buildPromptCommandDescription(prompt: PromptWithModel): string {
|
|
|
1831
1872
|
const inheritContextLabel = prompt.inheritContext ? " fork" : "";
|
|
1832
1873
|
const worktreeLabel = prompt.worktree ? " worktree" : "";
|
|
1833
1874
|
const details =
|
|
1834
|
-
`[${modelLabel}${rotateLabel}${thinkingLabel}${skillLabel}${loopLabel}${subagentLabel}${parallelLabel}${deterministicLabel}${workersLabel}${reviewersLabel}${finalApplierLabel}${cwdLabel}${inheritContextLabel}${worktreeLabel}] ${sourceLabel}`;
|
|
1875
|
+
`[${modelLabel}${rotateLabel}${thinkingLabel}${skillLabel}${loopLabel}${boomerangLabel}${subagentLabel}${parallelLabel}${deterministicLabel}${workersLabel}${reviewersLabel}${finalApplierLabel}${cwdLabel}${inheritContextLabel}${worktreeLabel}] ${sourceLabel}`;
|
|
1835
1876
|
return prompt.description ? `${prompt.description} ${details}` : details;
|
|
1836
1877
|
}
|
|
1837
1878
|
|
package/tool-manager.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
|
-
import { Type } from "
|
|
5
|
+
import { Type } from "typebox";
|
|
6
6
|
import { notify } from "./notifications.js";
|
|
7
7
|
|
|
8
8
|
export interface ToolManagerDeps {
|