pi-prompt-template-model 0.9.3 → 0.10.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 +12 -0
- package/README.md +13 -0
- package/chain-parser.ts +1 -1
- package/deterministic-renderer.ts +3 -3
- package/deterministic-step.ts +1 -1
- package/index.ts +47 -32
- package/loop-utils.ts +3 -3
- package/model-selection.ts +2 -2
- package/notifications.ts +2 -2
- package/package.json +16 -8
- package/prompt-execution.ts +5 -5
- package/prompt-loader.ts +280 -20
- package/skill-loaded-renderer.ts +2 -2
- package/subagent-renderer.ts +2 -2
- package/subagent-runtime.ts +0 -98
- package/subagent-step.ts +15 -19
- package/subagent-widget.ts +3 -3
- package/tool-manager.ts +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.10.0] - 2026-07-01
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Updated local Pi development dependencies to `@earendil-works/*` `0.80.3` and declared Pi core packages plus `typebox` as peer dependencies for package installs.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Inline prompt templates with `restore: true` now restore the previous model and thinking level on the prompt turn's `agent_end`, instead of one unrelated user turn later.
|
|
12
|
+
- Idle `agent_end` events now skip context/model reads when there is no pending restore state or queued `run-prompt` command.
|
|
13
|
+
- Prompt commands now initialize from the active session cwd instead of `process.cwd()`, and extension-owned Pi paths use Pi's current config-directory helpers.
|
|
14
|
+
- Delegated prompt execution now uses the loaded `pi-subagents` event bridge instead of importing `pi-subagents` internals for agent discovery.
|
|
15
|
+
- Prompt templates listed in user or project `settings.json` `prompts` paths now receive this extension's frontmatter handling, including `model`, `thinking`, and `skill`.
|
|
16
|
+
|
|
5
17
|
## [0.9.3] - 2026-04-28
|
|
6
18
|
|
|
7
19
|
### Fixed
|
package/README.md
CHANGED
|
@@ -49,6 +49,19 @@ Start a Python REPL session and help me debug: $@
|
|
|
49
49
|
|
|
50
50
|
Run `/debug-python some issue` and the agent switches to Sonnet, receives the tmux skill as context, and starts working. When it finishes, your previous model is restored.
|
|
51
51
|
|
|
52
|
+
## Prompt Discovery
|
|
53
|
+
|
|
54
|
+
The extension scans the default prompt directories and the same local prompt paths Pi loads from `settings.json`:
|
|
55
|
+
|
|
56
|
+
- User defaults: `~/.pi/agent/prompts/`
|
|
57
|
+
- Project defaults: `<cwd>/.pi/prompts/`
|
|
58
|
+
- User settings: `~/.pi/agent/settings.json` `prompts` entries
|
|
59
|
+
- Project settings: `<cwd>/.pi/settings.json` `prompts` entries
|
|
60
|
+
|
|
61
|
+
Settings entries may point at directories or individual `.md` files. Absolute paths and `~/...` work. Relative user settings paths resolve from `~/.pi/agent`; relative project settings paths resolve from `<cwd>/.pi`. Pattern entries follow Pi's settings behavior: glob-style entries filter configured prompt paths, and `!`, `+`, or `-` entries exclude or force exact matches.
|
|
62
|
+
|
|
63
|
+
Precedence follows Pi's local-resource order: project settings, project defaults, user settings, then user defaults. If the same file is reachable through both settings and a default directory, it is loaded once by canonical path. Malformed settings files or invalid configured paths produce warnings while the rest of discovery continues.
|
|
64
|
+
|
|
52
65
|
## Frontmatter Reference
|
|
53
66
|
|
|
54
67
|
All fields are optional. Templates that don't use any extension features (no `model`, `skill`, `thinking`, etc.) are left to pi's default prompt loader.
|
package/chain-parser.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { MessageRenderOptions, Theme } from "@
|
|
2
|
-
import { Box, Container, Spacer, Text } from "@
|
|
3
|
-
import { formatDeterministicExecution, type DeterministicExecutionResult } from "./deterministic-step.
|
|
1
|
+
import type { MessageRenderOptions, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { formatDeterministicExecution, type DeterministicExecutionResult } from "./deterministic-step.ts";
|
|
4
4
|
|
|
5
5
|
interface DeterministicMessage {
|
|
6
6
|
content?: unknown;
|
package/deterministic-step.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
4
|
-
import type { PromptWithModel, DeterministicStep, DeterministicExecution, DeterministicEnv } from "./prompt-loader.
|
|
4
|
+
import type { PromptWithModel, DeterministicStep, DeterministicExecution, DeterministicEnv } from "./prompt-loader.ts";
|
|
5
5
|
|
|
6
6
|
export const PROMPT_TEMPLATE_DETERMINISTIC_MESSAGE_TYPE = "prompt-template-deterministic";
|
|
7
7
|
export const PROMPT_TEMPLATE_DETERMINISTIC_COMPLETION_MESSAGE_TYPE = "prompt-template-deterministic-complete";
|
package/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { resolve as resolvePath } from "node:path";
|
|
3
|
-
import type { Model } from "@
|
|
4
|
-
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@
|
|
5
|
-
import type { ThinkingLevel } from "@
|
|
3
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
4
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
6
6
|
import {
|
|
7
7
|
extractChainContextFlag,
|
|
8
8
|
extractLineupOverrides,
|
|
@@ -14,12 +14,12 @@ import {
|
|
|
14
14
|
substituteArgs,
|
|
15
15
|
type LineupOverrideAction,
|
|
16
16
|
type SubagentOverride,
|
|
17
|
-
} from "./args.
|
|
18
|
-
import { parseChainSteps, parseChainDeclaration, type ChainStep, type ChainStepOrParallel, type ParallelChainStep } from "./chain-parser.
|
|
19
|
-
import { generateBoomerangSummary, generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries, wasIterationAborted } from "./loop-utils.
|
|
20
|
-
import { selectModelCandidate } from "./model-selection.
|
|
21
|
-
import { notify, summarizePromptDiagnostics, diagnosticsFingerprint } from "./notifications.
|
|
22
|
-
import { preparePromptExecution, renderPromptForResolvedModel } from "./prompt-execution.
|
|
17
|
+
} from "./args.ts";
|
|
18
|
+
import { parseChainSteps, parseChainDeclaration, type ChainStep, type ChainStepOrParallel, type ParallelChainStep } from "./chain-parser.ts";
|
|
19
|
+
import { generateBoomerangSummary, generateChainStepSummary, generateIterationSummary, didIterationMakeChanges, getIterationEntries, wasIterationAborted } from "./loop-utils.ts";
|
|
20
|
+
import { selectModelCandidate } from "./model-selection.ts";
|
|
21
|
+
import { notify, summarizePromptDiagnostics, diagnosticsFingerprint } from "./notifications.ts";
|
|
22
|
+
import { preparePromptExecution, renderPromptForResolvedModel } from "./prompt-execution.ts";
|
|
23
23
|
import {
|
|
24
24
|
buildPromptCommandDescription,
|
|
25
25
|
expandCwdPath,
|
|
@@ -28,20 +28,20 @@ import {
|
|
|
28
28
|
resolveSkillPath,
|
|
29
29
|
type DelegationLineupSlot,
|
|
30
30
|
type PromptWithModel,
|
|
31
|
-
} from "./prompt-loader.
|
|
32
|
-
import { renderSkillLoaded, type SkillLoadedDetails } from "./skill-loaded-renderer.
|
|
33
|
-
import { createToolManager } from "./tool-manager.
|
|
34
|
-
import { executeSubagentPromptStep, type DelegatedPromptParallelResult } from "./subagent-step.
|
|
35
|
-
import { DEFAULT_SUBAGENT_NAME, PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE } from "./subagent-runtime.
|
|
36
|
-
import { renderDelegatedSubagentResult } from "./subagent-renderer.
|
|
31
|
+
} from "./prompt-loader.ts";
|
|
32
|
+
import { renderSkillLoaded, type SkillLoadedDetails } from "./skill-loaded-renderer.ts";
|
|
33
|
+
import { createToolManager } from "./tool-manager.ts";
|
|
34
|
+
import { executeSubagentPromptStep, type DelegatedPromptParallelResult } from "./subagent-step.ts";
|
|
35
|
+
import { DEFAULT_SUBAGENT_NAME, PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE } from "./subagent-runtime.ts";
|
|
36
|
+
import { renderDelegatedSubagentResult } from "./subagent-renderer.ts";
|
|
37
37
|
import {
|
|
38
38
|
PROMPT_TEMPLATE_DETERMINISTIC_COMPLETION_MESSAGE_TYPE,
|
|
39
39
|
PROMPT_TEMPLATE_DETERMINISTIC_MESSAGE_TYPE,
|
|
40
40
|
buildDeterministicPreamble,
|
|
41
41
|
runDeterministicStep,
|
|
42
42
|
shouldHandoffToLlm,
|
|
43
|
-
} from "./deterministic-step.
|
|
44
|
-
import { renderDeterministicCompletion, renderDeterministicResult } from "./deterministic-renderer.
|
|
43
|
+
} from "./deterministic-step.ts";
|
|
44
|
+
import { renderDeterministicCompletion, renderDeterministicResult } from "./deterministic-renderer.ts";
|
|
45
45
|
|
|
46
46
|
interface LoopState {
|
|
47
47
|
currentIteration: number;
|
|
@@ -84,6 +84,11 @@ interface PromptStepResult {
|
|
|
84
84
|
text?: string;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
interface PromptTurnRestore {
|
|
88
|
+
originalModel: Model<any> | undefined;
|
|
89
|
+
originalThinking: ThinkingLevel | undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
87
92
|
const DEFAULT_COMPARE_REVIEWER_TASK = [
|
|
88
93
|
"Review the worker variants and produce findings only.",
|
|
89
94
|
"Required output:",
|
|
@@ -264,6 +269,7 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
264
269
|
inheritedModel?: Model<any>,
|
|
265
270
|
taskPreamble?: string,
|
|
266
271
|
loopContext?: string,
|
|
272
|
+
promptTurnRestore?: PromptTurnRestore,
|
|
267
273
|
): Promise<PromptStepResult | "aborted"> {
|
|
268
274
|
let deterministicPreamble: string | undefined;
|
|
269
275
|
if (prompt.deterministic) {
|
|
@@ -377,6 +383,17 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
377
383
|
}
|
|
378
384
|
pendingSkillMessage = skillResolution.kind === "ready" ? skillResolution.message : undefined;
|
|
379
385
|
|
|
386
|
+
if (promptTurnRestore) {
|
|
387
|
+
const currentModel = getCurrentModel(ctx);
|
|
388
|
+
if (promptTurnRestore.originalModel && currentModel && !sameModel(promptTurnRestore.originalModel, currentModel)) {
|
|
389
|
+
previousModel = promptTurnRestore.originalModel;
|
|
390
|
+
previousThinking = promptTurnRestore.originalThinking;
|
|
391
|
+
}
|
|
392
|
+
if (prompt.thinking && previousThinking === undefined && prompt.thinking !== promptTurnRestore.originalThinking) {
|
|
393
|
+
previousThinking = promptTurnRestore.originalThinking;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
380
397
|
const startId = ctx.sessionManager.getLeafId();
|
|
381
398
|
const effectiveContent = combinedTaskPreamble
|
|
382
399
|
? `${combinedTaskPreamble}\n\n${prepared.content}`
|
|
@@ -1587,6 +1604,10 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1587
1604
|
};
|
|
1588
1605
|
const savedModel = getCurrentModel(ctx);
|
|
1589
1606
|
const savedThinking = pi.getThinkingLevel();
|
|
1607
|
+
const isDelegatedPrompt = shouldDelegatePrompt(effectivePrompt, subagent.override);
|
|
1608
|
+
const promptTurnRestore = !isDelegatedPrompt && prompt.restore
|
|
1609
|
+
? { originalModel: savedModel, originalThinking: savedThinking }
|
|
1610
|
+
: undefined;
|
|
1590
1611
|
const boomerangTargetId = effectivePrompt.boomerang ? ctx.sessionManager.getLeafId() : null;
|
|
1591
1612
|
const stepResult = await executePromptStep(
|
|
1592
1613
|
effectivePrompt,
|
|
@@ -1594,25 +1615,18 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1594
1615
|
ctx,
|
|
1595
1616
|
savedModel,
|
|
1596
1617
|
subagent.override,
|
|
1618
|
+
undefined,
|
|
1619
|
+
undefined,
|
|
1620
|
+
undefined,
|
|
1621
|
+
promptTurnRestore,
|
|
1597
1622
|
);
|
|
1598
1623
|
if (stepResult === "aborted") return;
|
|
1599
|
-
if (
|
|
1624
|
+
if (isDelegatedPrompt && stepResult.text) {
|
|
1600
1625
|
pi.sendUserMessage(`[Delegated result: ${name}]\n\n${stepResult.text}`);
|
|
1601
1626
|
await waitForTurnStart(ctx);
|
|
1602
1627
|
await ctx.waitForIdle();
|
|
1603
1628
|
}
|
|
1604
1629
|
|
|
1605
|
-
if (!shouldDelegatePrompt(effectivePrompt, subagent.override) && prompt.restore) {
|
|
1606
|
-
const currentModel = getCurrentModel(ctx);
|
|
1607
|
-
if (savedModel && currentModel && !sameModel(savedModel, currentModel)) {
|
|
1608
|
-
previousModel = savedModel;
|
|
1609
|
-
previousThinking = savedThinking;
|
|
1610
|
-
}
|
|
1611
|
-
if (effectivePrompt.thinking && previousThinking === undefined && effectivePrompt.thinking !== savedThinking) {
|
|
1612
|
-
previousThinking = savedThinking;
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
1630
|
if (effectivePrompt.boomerang) {
|
|
1617
1631
|
await collapseBoomerangPrompt(ctx, name, boomerangTargetId);
|
|
1618
1632
|
}
|
|
@@ -1670,10 +1684,12 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1670
1684
|
if (chainActive) return;
|
|
1671
1685
|
if (loopState) return;
|
|
1672
1686
|
|
|
1673
|
-
runtimeModel = ctx.model;
|
|
1674
|
-
|
|
1675
1687
|
const restoreModel = previousModel;
|
|
1676
1688
|
const restoreThinking = previousThinking;
|
|
1689
|
+
if (!restoreModel && restoreThinking === undefined && !toolManager.hasQueuedCommand()) return;
|
|
1690
|
+
|
|
1691
|
+
runtimeModel = ctx.model;
|
|
1692
|
+
|
|
1677
1693
|
previousModel = undefined;
|
|
1678
1694
|
previousThinking = undefined;
|
|
1679
1695
|
|
|
@@ -1756,7 +1772,6 @@ export default function promptModelExtension(pi: ExtensionAPI) {
|
|
|
1756
1772
|
);
|
|
1757
1773
|
}
|
|
1758
1774
|
|
|
1759
|
-
refreshPrompts(process.cwd());
|
|
1760
1775
|
if (toolManager.isEnabled()) toolManager.ensureRegistered();
|
|
1761
1776
|
|
|
1762
1777
|
pi.registerCommand("chain-prompts", {
|
package/loop-utils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { AssistantMessage, Message } from "@
|
|
2
|
-
import type { ExtensionContext, SessionEntry } from "@
|
|
3
|
-
import { PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE } from "./subagent-runtime.
|
|
1
|
+
import type { AssistantMessage, Message } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE } from "./subagent-runtime.ts";
|
|
4
4
|
|
|
5
5
|
interface DelegatedMessageDetails {
|
|
6
6
|
messages?: Message[];
|
package/model-selection.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Model } from "@
|
|
2
|
-
import type { ResolvedModelRef } from "./template-conditionals.
|
|
1
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ResolvedModelRef } from "./template-conditionals.ts";
|
|
3
3
|
|
|
4
4
|
const PREFERRED_PROVIDERS = ["openai-codex", "anthropic", "github-copilot", "openrouter"];
|
|
5
5
|
|
package/notifications.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@
|
|
2
|
-
import type { PromptLoaderDiagnostic } from "./prompt-loader.
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { PromptLoaderDiagnostic } from "./prompt-loader.ts";
|
|
3
3
|
|
|
4
4
|
export function notify(
|
|
5
5
|
ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-prompt-template-model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Prompt template model selector extension for pi coding agent",
|
|
6
6
|
"author": "Nico Bailon",
|
|
@@ -49,15 +49,20 @@
|
|
|
49
49
|
"scripts": {
|
|
50
50
|
"test": "tsx --test test/**/*.test.ts"
|
|
51
51
|
},
|
|
52
|
-
"
|
|
53
|
-
"
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@earendil-works/pi-agent-core": "*",
|
|
54
|
+
"@earendil-works/pi-ai": "*",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
56
|
+
"@earendil-works/pi-tui": "*",
|
|
57
|
+
"typebox": "*"
|
|
54
58
|
},
|
|
55
59
|
"devDependencies": {
|
|
56
|
-
"@
|
|
57
|
-
"@
|
|
58
|
-
"@
|
|
59
|
-
"@
|
|
60
|
-
"tsx": "^4.
|
|
60
|
+
"@earendil-works/pi-agent-core": "^0.80.3",
|
|
61
|
+
"@earendil-works/pi-ai": "^0.80.3",
|
|
62
|
+
"@earendil-works/pi-coding-agent": "^0.80.3",
|
|
63
|
+
"@earendil-works/pi-tui": "^0.80.3",
|
|
64
|
+
"tsx": "^4.22.4",
|
|
65
|
+
"typebox": "^1.3.2"
|
|
61
66
|
},
|
|
62
67
|
"pi": {
|
|
63
68
|
"extensions": [
|
|
@@ -66,5 +71,8 @@
|
|
|
66
71
|
"skills": [
|
|
67
72
|
"./skills"
|
|
68
73
|
]
|
|
74
|
+
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"minimatch": "^10.2.5"
|
|
69
77
|
}
|
|
70
78
|
}
|
package/prompt-execution.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Model } from "@
|
|
2
|
-
import { substituteArgs } from "./args.
|
|
3
|
-
import { getResolvedModelRef, selectModelCandidate, type RegistryLike, type SelectedModelCandidate } from "./model-selection.
|
|
4
|
-
import type { PromptWithModel } from "./prompt-loader.
|
|
5
|
-
import { renderTemplateConditionals } from "./template-conditionals.
|
|
1
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { substituteArgs } from "./args.ts";
|
|
3
|
+
import { getResolvedModelRef, selectModelCandidate, type RegistryLike, type SelectedModelCandidate } from "./model-selection.ts";
|
|
4
|
+
import type { PromptWithModel } from "./prompt-loader.ts";
|
|
5
|
+
import { renderTemplateConditionals } from "./template-conditionals.ts";
|
|
6
6
|
|
|
7
7
|
export interface PreparedPromptExecution {
|
|
8
8
|
selectedModel: SelectedModelCandidate;
|
package/prompt-loader.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { minimatch } from "minimatch";
|
|
5
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
6
|
+
import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { parseChainDeclaration } from "./chain-parser.ts";
|
|
7
8
|
|
|
8
9
|
const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
9
10
|
export const RESERVED_COMMAND_NAMES = new Set([
|
|
@@ -1343,12 +1344,201 @@ function normalizeThinkingLevels(
|
|
|
1343
1344
|
return levels.map((level) => level.toLowerCase() as ThinkingLevel);
|
|
1344
1345
|
}
|
|
1345
1346
|
|
|
1347
|
+
interface ConfiguredPromptPath {
|
|
1348
|
+
rawPath: string;
|
|
1349
|
+
resolvedPath: string;
|
|
1350
|
+
settingsPath: string;
|
|
1351
|
+
source: PromptSource;
|
|
1352
|
+
baseDir: string;
|
|
1353
|
+
patterns: string[];
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
interface ConfiguredPromptPaths {
|
|
1357
|
+
paths: ConfiguredPromptPath[];
|
|
1358
|
+
patterns: string[];
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function expandConfiguredPromptPath(rawPath: string, baseDir: string): string {
|
|
1362
|
+
const trimmed = rawPath.trim();
|
|
1363
|
+
const expanded = trimmed === "~" ? homedir() : trimmed.startsWith("~/") ? join(homedir(), trimmed.slice(2)) : trimmed;
|
|
1364
|
+
return isAbsolute(expanded) ? resolve(expanded) : resolve(baseDir, expanded);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function isSettingsPromptPattern(value: string): boolean {
|
|
1368
|
+
return value.startsWith("!") || value.startsWith("+") || value.startsWith("-") || value.includes("*") || value.includes("?");
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function isSettingsPromptOverridePattern(value: string): boolean {
|
|
1372
|
+
return value.startsWith("!") || value.startsWith("+") || value.startsWith("-");
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function toPosixPath(path: string): string {
|
|
1376
|
+
return path.replace(/\\/g, "/");
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function normalizeExactPromptPattern(pattern: string): string {
|
|
1380
|
+
const normalized = pattern.startsWith("./") || pattern.startsWith(".\\") ? pattern.slice(2) : pattern;
|
|
1381
|
+
return toPosixPath(normalized);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function matchesPromptPattern(filePath: string, pattern: string, baseDir: string): boolean {
|
|
1385
|
+
const normalizedPattern = toPosixPath(pattern);
|
|
1386
|
+
const rel = toPosixPath(relative(baseDir, filePath));
|
|
1387
|
+
const name = basename(filePath);
|
|
1388
|
+
const filePathPosix = toPosixPath(filePath);
|
|
1389
|
+
return [rel, name, filePathPosix].some((candidate) => minimatch(candidate, normalizedPattern));
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function matchesExactPromptPattern(filePath: string, pattern: string, baseDir: string): boolean {
|
|
1393
|
+
const normalizedPattern = normalizeExactPromptPattern(pattern);
|
|
1394
|
+
const rel = toPosixPath(relative(baseDir, filePath));
|
|
1395
|
+
const filePathPosix = toPosixPath(filePath);
|
|
1396
|
+
return normalizedPattern === rel || normalizedPattern === filePathPosix;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function shouldLoadPromptFile(filePath: string, patterns: string[], baseDir: string): boolean {
|
|
1400
|
+
const includes: string[] = [];
|
|
1401
|
+
const excludes: string[] = [];
|
|
1402
|
+
const forceIncludes: string[] = [];
|
|
1403
|
+
const forceExcludes: string[] = [];
|
|
1404
|
+
|
|
1405
|
+
for (const pattern of patterns) {
|
|
1406
|
+
if (pattern.startsWith("+")) {
|
|
1407
|
+
forceIncludes.push(pattern.slice(1));
|
|
1408
|
+
} else if (pattern.startsWith("-")) {
|
|
1409
|
+
forceExcludes.push(pattern.slice(1));
|
|
1410
|
+
} else if (pattern.startsWith("!")) {
|
|
1411
|
+
excludes.push(pattern.slice(1));
|
|
1412
|
+
} else {
|
|
1413
|
+
includes.push(pattern);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
let enabled = includes.length === 0 || includes.some((pattern) => matchesPromptPattern(filePath, pattern, baseDir));
|
|
1418
|
+
if (excludes.some((pattern) => matchesPromptPattern(filePath, pattern, baseDir))) enabled = false;
|
|
1419
|
+
if (forceIncludes.some((pattern) => matchesExactPromptPattern(filePath, pattern, baseDir))) enabled = true;
|
|
1420
|
+
if (forceExcludes.some((pattern) => matchesExactPromptPattern(filePath, pattern, baseDir))) enabled = false;
|
|
1421
|
+
return enabled;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function createPromptFileFilter(patterns: string[], baseDir: string): ((filePath: string) => boolean) | undefined {
|
|
1425
|
+
return patterns.length === 0 ? undefined : (filePath) => shouldLoadPromptFile(filePath, patterns, baseDir);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function loadConfiguredPromptPaths(
|
|
1429
|
+
settingsPath: string,
|
|
1430
|
+
source: PromptSource,
|
|
1431
|
+
baseDir: string,
|
|
1432
|
+
diagnostics: PromptLoaderDiagnostic[],
|
|
1433
|
+
): ConfiguredPromptPaths {
|
|
1434
|
+
if (!existsSync(settingsPath)) return { paths: [], patterns: [] };
|
|
1435
|
+
|
|
1436
|
+
let rawSettings: string;
|
|
1437
|
+
try {
|
|
1438
|
+
rawSettings = readFileSync(settingsPath, "utf-8");
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
diagnostics.push(
|
|
1441
|
+
createDiagnostic(
|
|
1442
|
+
"unreadable-settings",
|
|
1443
|
+
settingsPath,
|
|
1444
|
+
source,
|
|
1445
|
+
`Skipping prompt settings at ${settingsPath}: ${error instanceof Error ? error.message : String(error)}.`,
|
|
1446
|
+
),
|
|
1447
|
+
);
|
|
1448
|
+
return { paths: [], patterns: [] };
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
let parsed: unknown;
|
|
1452
|
+
try {
|
|
1453
|
+
parsed = JSON.parse(rawSettings);
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
diagnostics.push(
|
|
1456
|
+
createDiagnostic(
|
|
1457
|
+
"invalid-settings-json",
|
|
1458
|
+
settingsPath,
|
|
1459
|
+
source,
|
|
1460
|
+
`Skipping prompt settings at ${settingsPath}: ${error instanceof Error ? error.message : String(error)}.`,
|
|
1461
|
+
),
|
|
1462
|
+
);
|
|
1463
|
+
return { paths: [], patterns: [] };
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1467
|
+
diagnostics.push(
|
|
1468
|
+
createDiagnostic(
|
|
1469
|
+
"invalid-settings",
|
|
1470
|
+
settingsPath,
|
|
1471
|
+
source,
|
|
1472
|
+
`Skipping prompt settings at ${settingsPath}: settings must be a JSON object.`,
|
|
1473
|
+
),
|
|
1474
|
+
);
|
|
1475
|
+
return { paths: [], patterns: [] };
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
const prompts = (parsed as Record<string, unknown>).prompts;
|
|
1479
|
+
if (prompts === undefined) return { paths: [], patterns: [] };
|
|
1480
|
+
if (!Array.isArray(prompts)) {
|
|
1481
|
+
diagnostics.push(
|
|
1482
|
+
createDiagnostic(
|
|
1483
|
+
"invalid-settings-prompts",
|
|
1484
|
+
settingsPath,
|
|
1485
|
+
source,
|
|
1486
|
+
`Ignoring prompts in ${settingsPath}: expected "prompts" to be an array of strings.`,
|
|
1487
|
+
),
|
|
1488
|
+
);
|
|
1489
|
+
return { paths: [], patterns: [] };
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
const configuredPaths: Omit<ConfiguredPromptPath, "patterns">[] = [];
|
|
1493
|
+
const patterns: string[] = [];
|
|
1494
|
+
for (let index = 0; index < prompts.length; index++) {
|
|
1495
|
+
const rawPath = prompts[index];
|
|
1496
|
+
if (typeof rawPath !== "string" || rawPath.trim().length === 0) {
|
|
1497
|
+
diagnostics.push(
|
|
1498
|
+
createDiagnostic(
|
|
1499
|
+
"invalid-settings-prompt-entry",
|
|
1500
|
+
settingsPath,
|
|
1501
|
+
source,
|
|
1502
|
+
`Ignoring prompts[${index}] in ${settingsPath}: expected a non-empty string.`,
|
|
1503
|
+
),
|
|
1504
|
+
);
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
const trimmed = rawPath.trim();
|
|
1509
|
+
if (isSettingsPromptPattern(trimmed)) {
|
|
1510
|
+
patterns.push(trimmed);
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
configuredPaths.push({
|
|
1514
|
+
rawPath: trimmed,
|
|
1515
|
+
resolvedPath: expandConfiguredPromptPath(trimmed, baseDir),
|
|
1516
|
+
settingsPath,
|
|
1517
|
+
source,
|
|
1518
|
+
baseDir,
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
return { paths: configuredPaths.map((path) => ({ ...path, patterns })), patterns };
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function canonicalizePromptFile(filePath: string): string {
|
|
1526
|
+
try {
|
|
1527
|
+
return realpathSync(filePath);
|
|
1528
|
+
} catch {
|
|
1529
|
+
return resolve(filePath);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1346
1533
|
function loadPromptsWithModelFromDir(
|
|
1347
1534
|
dir: string,
|
|
1348
1535
|
source: PromptSource,
|
|
1349
1536
|
includePlainPrompts: boolean,
|
|
1350
1537
|
subdir = "",
|
|
1351
1538
|
visitedDirectories = new Set<string>(),
|
|
1539
|
+
onlyFileName?: string,
|
|
1540
|
+
seenFiles?: Set<string>,
|
|
1541
|
+
shouldLoadFile?: (filePath: string) => boolean,
|
|
1352
1542
|
): { prompts: PromptWithModel[]; diagnostics: PromptLoaderDiagnostic[] } {
|
|
1353
1543
|
const prompts: PromptWithModel[] = [];
|
|
1354
1544
|
const diagnostics: PromptLoaderDiagnostic[] = [];
|
|
@@ -1390,6 +1580,7 @@ function loadPromptsWithModelFromDir(
|
|
|
1390
1580
|
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => lexicalCompare(a.name, b.name));
|
|
1391
1581
|
|
|
1392
1582
|
for (const entry of entries) {
|
|
1583
|
+
if (onlyFileName && entry.name !== onlyFileName) continue;
|
|
1393
1584
|
const fullPath = join(dir, entry.name);
|
|
1394
1585
|
|
|
1395
1586
|
let isFile = entry.isFile();
|
|
@@ -1413,14 +1604,19 @@ function loadPromptsWithModelFromDir(
|
|
|
1413
1604
|
}
|
|
1414
1605
|
|
|
1415
1606
|
if (isDirectory) {
|
|
1607
|
+
if (onlyFileName) continue;
|
|
1416
1608
|
const nextSubdir = subdir ? `${subdir}:${entry.name}` : entry.name;
|
|
1417
|
-
const nested = loadPromptsWithModelFromDir(fullPath, source, includePlainPrompts, nextSubdir, visitedDirectories);
|
|
1609
|
+
const nested = loadPromptsWithModelFromDir(fullPath, source, includePlainPrompts, nextSubdir, visitedDirectories, undefined, seenFiles, shouldLoadFile);
|
|
1418
1610
|
prompts.push(...nested.prompts);
|
|
1419
1611
|
diagnostics.push(...nested.diagnostics);
|
|
1420
1612
|
continue;
|
|
1421
1613
|
}
|
|
1422
1614
|
|
|
1423
1615
|
if (!isFile || !entry.name.endsWith(".md")) continue;
|
|
1616
|
+
if (shouldLoadFile && !shouldLoadFile(fullPath)) continue;
|
|
1617
|
+
const canonicalFile = canonicalizePromptFile(fullPath);
|
|
1618
|
+
if (seenFiles?.has(canonicalFile)) continue;
|
|
1619
|
+
seenFiles?.add(canonicalFile);
|
|
1424
1620
|
|
|
1425
1621
|
try {
|
|
1426
1622
|
const rawContent = readFileSync(fullPath, "utf-8");
|
|
@@ -1802,11 +1998,69 @@ function loadPromptsWithModelFromDir(
|
|
|
1802
1998
|
return { prompts, diagnostics };
|
|
1803
1999
|
}
|
|
1804
2000
|
|
|
2001
|
+
function loadPromptsWithModelFromConfiguredPath(
|
|
2002
|
+
configuredPath: ConfiguredPromptPath,
|
|
2003
|
+
includePlainPrompts: boolean,
|
|
2004
|
+
seenFiles: Set<string>,
|
|
2005
|
+
): { prompts: PromptWithModel[]; diagnostics: PromptLoaderDiagnostic[] } {
|
|
2006
|
+
const diagnostics: PromptLoaderDiagnostic[] = [];
|
|
2007
|
+
const { rawPath, resolvedPath, settingsPath, source, baseDir, patterns } = configuredPath;
|
|
2008
|
+
const shouldLoadFile = createPromptFileFilter(patterns, baseDir);
|
|
2009
|
+
|
|
2010
|
+
if (!existsSync(resolvedPath)) {
|
|
2011
|
+
diagnostics.push(
|
|
2012
|
+
createDiagnostic(
|
|
2013
|
+
"missing-prompt-path",
|
|
2014
|
+
resolvedPath,
|
|
2015
|
+
source,
|
|
2016
|
+
`Skipping configured prompt path ${JSON.stringify(rawPath)} from ${settingsPath}: resolved path ${resolvedPath} does not exist.`,
|
|
2017
|
+
),
|
|
2018
|
+
);
|
|
2019
|
+
return { prompts: [], diagnostics };
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
let stats: ReturnType<typeof statSync>;
|
|
2023
|
+
try {
|
|
2024
|
+
stats = statSync(resolvedPath);
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
diagnostics.push(
|
|
2027
|
+
createDiagnostic(
|
|
2028
|
+
"unreadable-prompt-path",
|
|
2029
|
+
resolvedPath,
|
|
2030
|
+
source,
|
|
2031
|
+
`Skipping configured prompt path ${JSON.stringify(rawPath)} from ${settingsPath}: ${error instanceof Error ? error.message : String(error)}.`,
|
|
2032
|
+
),
|
|
2033
|
+
);
|
|
2034
|
+
return { prompts: [], diagnostics };
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
if (stats.isDirectory()) {
|
|
2038
|
+
return loadPromptsWithModelFromDir(resolvedPath, source, includePlainPrompts, "", new Set<string>(), undefined, seenFiles, shouldLoadFile);
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
if (!stats.isFile() || !resolvedPath.endsWith(".md")) {
|
|
2042
|
+
diagnostics.push(
|
|
2043
|
+
createDiagnostic(
|
|
2044
|
+
"invalid-prompt-path",
|
|
2045
|
+
resolvedPath,
|
|
2046
|
+
source,
|
|
2047
|
+
`Skipping configured prompt path ${JSON.stringify(rawPath)} from ${settingsPath}: expected a directory or .md file.`,
|
|
2048
|
+
),
|
|
2049
|
+
);
|
|
2050
|
+
return { prompts: [], diagnostics };
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
return loadPromptsWithModelFromDir(dirname(resolvedPath), source, includePlainPrompts, "", new Set<string>(), basename(resolvedPath), seenFiles, shouldLoadFile);
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1805
2056
|
export function loadPromptsWithModel(cwd: string, includePlainPrompts = false): LoadPromptsWithModelResult {
|
|
1806
|
-
const
|
|
1807
|
-
const
|
|
2057
|
+
const agentDir = getAgentDir();
|
|
2058
|
+
const globalDir = join(agentDir, "prompts");
|
|
2059
|
+
const projectBaseDir = resolve(cwd, CONFIG_DIR_NAME);
|
|
2060
|
+
const projectDir = join(projectBaseDir, "prompts");
|
|
1808
2061
|
const promptMap = new Map<string, PromptWithModel>();
|
|
1809
2062
|
const diagnostics: PromptLoaderDiagnostic[] = [];
|
|
2063
|
+
const seenPromptFiles = new Set<string>();
|
|
1810
2064
|
|
|
1811
2065
|
function addPrompt(prompt: PromptWithModel) {
|
|
1812
2066
|
const existing = promptMap.get(prompt.name);
|
|
@@ -1824,23 +2078,29 @@ export function loadPromptsWithModel(cwd: string, includePlainPrompts = false):
|
|
|
1824
2078
|
`Skipping ${prompt.source} prompt template "${prompt.name}" at ${prompt.filePath} because it conflicts with ${existing.filePath}.`,
|
|
1825
2079
|
),
|
|
1826
2080
|
);
|
|
1827
|
-
return;
|
|
1828
2081
|
}
|
|
1829
|
-
|
|
1830
|
-
promptMap.set(prompt.name, prompt);
|
|
1831
2082
|
}
|
|
1832
2083
|
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2084
|
+
function addResult(result: { prompts: PromptWithModel[]; diagnostics: PromptLoaderDiagnostic[] }) {
|
|
2085
|
+
diagnostics.push(...result.diagnostics);
|
|
2086
|
+
for (const prompt of result.prompts) {
|
|
2087
|
+
addPrompt(prompt);
|
|
2088
|
+
}
|
|
1837
2089
|
}
|
|
1838
2090
|
|
|
1839
|
-
const
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2091
|
+
const projectSettingsPaths = loadConfiguredPromptPaths(join(projectBaseDir, "settings.json"), "project", projectBaseDir, diagnostics);
|
|
2092
|
+
const globalSettingsPaths = loadConfiguredPromptPaths(join(agentDir, "settings.json"), "user", agentDir, diagnostics);
|
|
2093
|
+
const projectDefaultFilter = createPromptFileFilter(projectSettingsPaths.patterns.filter(isSettingsPromptOverridePattern), projectBaseDir);
|
|
2094
|
+
const globalDefaultFilter = createPromptFileFilter(globalSettingsPaths.patterns.filter(isSettingsPromptOverridePattern), agentDir);
|
|
2095
|
+
|
|
2096
|
+
for (const configuredPath of projectSettingsPaths.paths) {
|
|
2097
|
+
addResult(loadPromptsWithModelFromConfiguredPath(configuredPath, includePlainPrompts, seenPromptFiles));
|
|
2098
|
+
}
|
|
2099
|
+
addResult(loadPromptsWithModelFromDir(projectDir, "project", includePlainPrompts, "", new Set<string>(), undefined, seenPromptFiles, projectDefaultFilter));
|
|
2100
|
+
for (const configuredPath of globalSettingsPaths.paths) {
|
|
2101
|
+
addResult(loadPromptsWithModelFromConfiguredPath(configuredPath, includePlainPrompts, seenPromptFiles));
|
|
1843
2102
|
}
|
|
2103
|
+
addResult(loadPromptsWithModelFromDir(globalDir, "user", includePlainPrompts, "", new Set<string>(), undefined, seenPromptFiles, globalDefaultFilter));
|
|
1844
2104
|
|
|
1845
2105
|
return { prompts: promptMap, diagnostics };
|
|
1846
2106
|
}
|
|
@@ -1911,7 +2171,7 @@ function findFirstExisting(paths: string[]): string | undefined {
|
|
|
1911
2171
|
export function resolveSkillPath(skillName: string, cwd: string): string | undefined {
|
|
1912
2172
|
const projectDir = resolve(cwd);
|
|
1913
2173
|
|
|
1914
|
-
const projectPiSkill = findFirstExisting(getSkillCandidates(resolve(projectDir,
|
|
2174
|
+
const projectPiSkill = findFirstExisting(getSkillCandidates(resolve(projectDir, CONFIG_DIR_NAME, "skills"), skillName));
|
|
1915
2175
|
if (projectPiSkill) return projectPiSkill;
|
|
1916
2176
|
|
|
1917
2177
|
const repoRoot = findRepoRoot(projectDir);
|
|
@@ -1920,7 +2180,7 @@ export function resolveSkillPath(skillName: string, cwd: string): string | undef
|
|
|
1920
2180
|
if (projectAgentsSkill) return projectAgentsSkill;
|
|
1921
2181
|
}
|
|
1922
2182
|
|
|
1923
|
-
const globalPiSkill = findFirstExisting(getSkillCandidates(join(
|
|
2183
|
+
const globalPiSkill = findFirstExisting(getSkillCandidates(join(getAgentDir(), "skills"), skillName));
|
|
1924
2184
|
if (globalPiSkill) return globalPiSkill;
|
|
1925
2185
|
|
|
1926
2186
|
return findFirstExisting(getSkillCandidates(join(homedir(), ".agents", "skills"), skillName));
|
package/skill-loaded-renderer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { MessageRenderOptions, Theme } from "@
|
|
2
|
-
import { Box, Container, Spacer, Text } from "@
|
|
1
|
+
import type { MessageRenderOptions, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
4
|
export interface SkillLoadedDetails {
|
|
5
5
|
skillName: string;
|
package/subagent-renderer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { MessageRenderOptions, Theme } from "@
|
|
2
|
-
import { Box, Container, Spacer, Text } from "@
|
|
1
|
+
import type { MessageRenderOptions, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
4
|
interface AssistantContent {
|
|
5
5
|
type: string;
|
package/subagent-runtime.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
-
|
|
5
1
|
export const PROMPT_TEMPLATE_SUBAGENT_REQUEST_EVENT = "prompt-template:subagent:request";
|
|
6
2
|
export const PROMPT_TEMPLATE_SUBAGENT_STARTED_EVENT = "prompt-template:subagent:started";
|
|
7
3
|
export const PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT = "prompt-template:subagent:response";
|
|
@@ -93,67 +89,8 @@ export interface DelegatedSubagentLiveState {
|
|
|
93
89
|
updatedAt: number;
|
|
94
90
|
}
|
|
95
91
|
|
|
96
|
-
interface RuntimeAgent {
|
|
97
|
-
name: string;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
interface DiscoverAgentsResult {
|
|
101
|
-
agents: RuntimeAgent[];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
type DiscoverAgentsFn = (cwd: string, scope: "user" | "project" | "both") => DiscoverAgentsResult;
|
|
105
|
-
|
|
106
|
-
export interface SubagentRuntime {
|
|
107
|
-
root: string;
|
|
108
|
-
discoverAgents: DiscoverAgentsFn;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
let runtimeCache: SubagentRuntime | null = null;
|
|
112
92
|
const delegatedLiveState = new Map<string, DelegatedSubagentLiveState>();
|
|
113
93
|
|
|
114
|
-
function runtimeCandidates(cwd: string): string[] {
|
|
115
|
-
const fromEnv = process.env.PI_SUBAGENT_RUNTIME_ROOT?.trim();
|
|
116
|
-
if (fromEnv) return [resolve(fromEnv)];
|
|
117
|
-
const localSibling = resolve(dirname(fileURLToPath(import.meta.url)), "..", "pi-subagents");
|
|
118
|
-
return [
|
|
119
|
-
resolve(cwd, ".pi", "npm", "node_modules", "pi-subagents"),
|
|
120
|
-
localSibling,
|
|
121
|
-
];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function findSubagentRoot(cwd: string): string | undefined {
|
|
125
|
-
for (const candidate of runtimeCandidates(cwd)) {
|
|
126
|
-
if (existsSync(join(candidate, "agents.ts")) || existsSync(join(candidate, "agents.js"))) {
|
|
127
|
-
return candidate;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function importRuntimeModule(root: string, baseName: string): Promise<unknown> {
|
|
134
|
-
const candidates = [
|
|
135
|
-
join(root, `${baseName}.ts`),
|
|
136
|
-
join(root, `${baseName}.mts`),
|
|
137
|
-
join(root, `${baseName}.js`),
|
|
138
|
-
join(root, `${baseName}.mjs`),
|
|
139
|
-
];
|
|
140
|
-
|
|
141
|
-
let lastError: unknown;
|
|
142
|
-
for (const filePath of candidates) {
|
|
143
|
-
if (!existsSync(filePath)) continue;
|
|
144
|
-
try {
|
|
145
|
-
return await import(pathToFileURL(filePath).href);
|
|
146
|
-
} catch (error) {
|
|
147
|
-
lastError = error;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (lastError !== undefined) {
|
|
152
|
-
throw lastError;
|
|
153
|
-
}
|
|
154
|
-
throw new Error(`Missing runtime module: ${baseName}`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
94
|
export function updateDelegatedLiveState(requestId: string, update: Partial<DelegatedSubagentLiveState>): void {
|
|
158
95
|
const now = Date.now();
|
|
159
96
|
const existing = delegatedLiveState.get(requestId) ?? {
|
|
@@ -210,38 +147,3 @@ export function getDelegatedLiveState(requestId: string): DelegatedSubagentLiveS
|
|
|
210
147
|
export function clearDelegatedLiveState(requestId: string): void {
|
|
211
148
|
delegatedLiveState.delete(requestId);
|
|
212
149
|
}
|
|
213
|
-
|
|
214
|
-
export async function ensureSubagentRuntime(cwd: string): Promise<SubagentRuntime> {
|
|
215
|
-
const root = findSubagentRoot(cwd);
|
|
216
|
-
if (!root) {
|
|
217
|
-
throw new Error(
|
|
218
|
-
"Delegated prompt execution requires pi-subagents. Install it with `pi install npm:pi-subagents` or set PI_SUBAGENT_RUNTIME_ROOT.",
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (runtimeCache && runtimeCache.root === root) {
|
|
223
|
-
return runtimeCache;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const module = await importRuntimeModule(root, "agents");
|
|
227
|
-
const discoverAgents = (module as { discoverAgents?: unknown }).discoverAgents;
|
|
228
|
-
if (typeof discoverAgents !== "function") {
|
|
229
|
-
throw new Error(`Invalid subagent runtime at ${root}: expected discoverAgents(cwd, scope).`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
runtimeCache = {
|
|
233
|
-
root,
|
|
234
|
-
discoverAgents: discoverAgents as DiscoverAgentsFn,
|
|
235
|
-
};
|
|
236
|
-
return runtimeCache;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export function resolveDelegatedAgent(runtime: SubagentRuntime, cwd: string, requested: string): string {
|
|
240
|
-
const discovered = runtime.discoverAgents(cwd, "both");
|
|
241
|
-
if (!discovered.agents.some((agent) => agent.name === requested)) {
|
|
242
|
-
throw new Error(
|
|
243
|
-
`Delegated subagent \`${requested}\` not found. Available agents: ${discovered.agents.map((a) => a.name).join(", ") || "none"}.`,
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
return requested;
|
|
247
|
-
}
|
package/subagent-step.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import type { AssistantMessage, Message } from "@
|
|
4
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
5
|
-
import type { Model } from "@
|
|
6
|
-
import { Key, matchesKey } from "@
|
|
7
|
-
import { preparePromptExecution } from "./prompt-execution.
|
|
8
|
-
import type { PromptWithModel } from "./prompt-loader.
|
|
9
|
-
import { notify } from "./notifications.
|
|
3
|
+
import type { AssistantMessage, Message } from "@earendil-works/pi-ai";
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
6
|
+
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
7
|
+
import { preparePromptExecution } from "./prompt-execution.ts";
|
|
8
|
+
import type { PromptWithModel } from "./prompt-loader.ts";
|
|
9
|
+
import { notify } from "./notifications.ts";
|
|
10
10
|
import {
|
|
11
11
|
DEFAULT_SUBAGENT_NAME,
|
|
12
12
|
appendDelegatedLiveOutput,
|
|
13
13
|
clearDelegatedLiveState,
|
|
14
|
-
ensureSubagentRuntime,
|
|
15
14
|
getDelegatedLiveState,
|
|
16
15
|
PROMPT_TEMPLATE_SUBAGENT_CANCEL_EVENT,
|
|
17
16
|
PROMPT_TEMPLATE_SUBAGENT_MESSAGE_TYPE,
|
|
@@ -19,7 +18,6 @@ import {
|
|
|
19
18
|
PROMPT_TEMPLATE_SUBAGENT_RESPONSE_EVENT,
|
|
20
19
|
PROMPT_TEMPLATE_SUBAGENT_STARTED_EVENT,
|
|
21
20
|
PROMPT_TEMPLATE_SUBAGENT_UPDATE_EVENT,
|
|
22
|
-
resolveDelegatedAgent,
|
|
23
21
|
updateDelegatedLiveState,
|
|
24
22
|
type DelegatedSubagentParallelResult,
|
|
25
23
|
type DelegatedSubagentRequest,
|
|
@@ -27,9 +25,9 @@ import {
|
|
|
27
25
|
type DelegatedSubagentTask,
|
|
28
26
|
type DelegatedSubagentTaskProgress,
|
|
29
27
|
type DelegatedSubagentUpdate,
|
|
30
|
-
} from "./subagent-runtime.
|
|
31
|
-
import type { SubagentOverride } from "./args.
|
|
32
|
-
import { createDelegatedProgressWidget, DELEGATED_WIDGET_KEY } from "./subagent-widget.
|
|
28
|
+
} from "./subagent-runtime.ts";
|
|
29
|
+
import type { SubagentOverride } from "./args.ts";
|
|
30
|
+
import { createDelegatedProgressWidget, DELEGATED_WIDGET_KEY } from "./subagent-widget.ts";
|
|
33
31
|
|
|
34
32
|
interface DelegatedPromptBaseOptions {
|
|
35
33
|
pi: ExtensionAPI;
|
|
@@ -169,7 +167,6 @@ async function prepareDelegatedTask(
|
|
|
169
167
|
override: SubagentOverride | undefined,
|
|
170
168
|
inheritedModel: Model<any> | undefined,
|
|
171
169
|
taskPreamble: string | undefined,
|
|
172
|
-
runtime: Awaited<ReturnType<typeof ensureSubagentRuntime>>,
|
|
173
170
|
): Promise<PreparedDelegatedTask> {
|
|
174
171
|
const requestedAgent = resolveDelegationName(task.prompt, override);
|
|
175
172
|
if (!requestedAgent) {
|
|
@@ -179,7 +176,7 @@ async function prepareDelegatedTask(
|
|
|
179
176
|
if (effectiveCwd !== ctx.cwd && !existsSync(effectiveCwd)) {
|
|
180
177
|
throw new Error(`cwd directory does not exist: ${effectiveCwd}`);
|
|
181
178
|
}
|
|
182
|
-
const agent =
|
|
179
|
+
const agent = requestedAgent;
|
|
183
180
|
const preparationOptions = inheritedModel === undefined ? undefined : { inheritedModel };
|
|
184
181
|
const prepared = await preparePromptExecution(
|
|
185
182
|
task.prompt,
|
|
@@ -340,7 +337,7 @@ async function requestDelegatedRun(
|
|
|
340
337
|
const startTimeoutMs = Number(process.env.PI_PROMPT_SUBAGENT_START_TIMEOUT_MS ?? "15000");
|
|
341
338
|
const effectiveTimeout = Number.isFinite(startTimeoutMs) && startTimeoutMs > 0 ? startTimeoutMs : 15_000;
|
|
342
339
|
const startTimeout = setTimeout(() => {
|
|
343
|
-
finish(() => reject(new Error(`Delegated subagent \`${requestLabel}\` did not start within ${Math.round(effectiveTimeout / 1000)}s. Check that the
|
|
340
|
+
finish(() => reject(new Error(`Delegated subagent \`${requestLabel}\` did not start within ${Math.round(effectiveTimeout / 1000)}s. Check that the pi-subagents extension is loaded.`)));
|
|
344
341
|
}, effectiveTimeout);
|
|
345
342
|
|
|
346
343
|
const onStarted = (data: unknown) => {
|
|
@@ -528,8 +525,8 @@ async function requestDelegatedRun(
|
|
|
528
525
|
if (!started && done) return; // already finished (e.g. response came synchronously)
|
|
529
526
|
if (!started) {
|
|
530
527
|
finish(() => reject(new Error(
|
|
531
|
-
`No
|
|
532
|
-
`
|
|
528
|
+
`No loaded pi-subagents bridge responded for \`${requestLabel}\`. ` +
|
|
529
|
+
`Install or load pi-subagents and make sure no extension name conflicts are blocking it.`,
|
|
533
530
|
)));
|
|
534
531
|
return;
|
|
535
532
|
}
|
|
@@ -538,7 +535,6 @@ async function requestDelegatedRun(
|
|
|
538
535
|
|
|
539
536
|
export async function executeSubagentPromptStep(options: DelegatedPromptOptions): Promise<DelegatedPromptOutcome | undefined> {
|
|
540
537
|
const { pi, ctx, currentModel, override, signal, inheritedModel, taskPreamble, allowPartialFailures } = options;
|
|
541
|
-
const runtime = await ensureSubagentRuntime(ctx.cwd);
|
|
542
538
|
const isParallelRequest = "parallel" in options;
|
|
543
539
|
|
|
544
540
|
const tasks = isParallelRequest
|
|
@@ -548,7 +544,7 @@ export async function executeSubagentPromptStep(options: DelegatedPromptOptions)
|
|
|
548
544
|
|
|
549
545
|
const preparedTasks: PreparedDelegatedTask[] = [];
|
|
550
546
|
for (const task of tasks) {
|
|
551
|
-
const preparedTask = await prepareDelegatedTask(task, ctx, currentModel, override, inheritedModel, taskPreamble
|
|
547
|
+
const preparedTask = await prepareDelegatedTask(task, ctx, currentModel, override, inheritedModel, taskPreamble);
|
|
552
548
|
preparedTasks.push(preparedTask);
|
|
553
549
|
}
|
|
554
550
|
|
package/subagent-widget.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { Theme } from "@
|
|
2
|
-
import { Box, Container, Spacer, Text } from "@
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import {
|
|
4
4
|
getDelegatedLiveState,
|
|
5
5
|
type DelegatedSubagentLiveState,
|
|
6
6
|
type DelegatedSubagentTask,
|
|
7
7
|
type DelegatedSubagentTaskProgress,
|
|
8
|
-
} from "./subagent-runtime.
|
|
8
|
+
} from "./subagent-runtime.ts";
|
|
9
9
|
|
|
10
10
|
export const DELEGATED_WIDGET_KEY = "prompt-subagent-progress";
|
|
11
11
|
|
package/tool-manager.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { join } from "node:path";
|
|
4
|
-
import type
|
|
3
|
+
import { getAgentDir, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
4
|
import { Type } from "typebox";
|
|
6
|
-
import { notify } from "./notifications.
|
|
5
|
+
import { notify } from "./notifications.ts";
|
|
7
6
|
|
|
8
7
|
export interface ToolManagerDeps {
|
|
9
8
|
isActive(): boolean;
|
|
@@ -17,7 +16,7 @@ export function createToolManager(pi: ExtensionAPI, deps: ToolManagerDeps) {
|
|
|
17
16
|
let toolGuidance: string | null = null;
|
|
18
17
|
let toolRegistered = false;
|
|
19
18
|
let toolQueuedCommand: string | null = null;
|
|
20
|
-
const configPath = join(
|
|
19
|
+
const configPath = join(getAgentDir(), "prompt-template-model.json");
|
|
21
20
|
|
|
22
21
|
try {
|
|
23
22
|
const rawConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
@@ -41,7 +40,7 @@ export function createToolManager(pi: ExtensionAPI, deps: ToolManagerDeps) {
|
|
|
41
40
|
|
|
42
41
|
function saveToolConfig() {
|
|
43
42
|
try {
|
|
44
|
-
mkdirSync(
|
|
43
|
+
mkdirSync(getAgentDir(), { recursive: true });
|
|
45
44
|
writeFileSync(configPath, JSON.stringify({ toolEnabled, toolGuidance }, null, 2));
|
|
46
45
|
} catch (error) {
|
|
47
46
|
process.stderr.write(
|
|
@@ -209,6 +208,9 @@ export function createToolManager(pi: ExtensionAPI, deps: ToolManagerDeps) {
|
|
|
209
208
|
getGuidance() {
|
|
210
209
|
return toolGuidance;
|
|
211
210
|
},
|
|
211
|
+
hasQueuedCommand() {
|
|
212
|
+
return toolQueuedCommand !== null;
|
|
213
|
+
},
|
|
212
214
|
clearQueue() {
|
|
213
215
|
toolQueuedCommand = null;
|
|
214
216
|
},
|