@zihanw/pi-forge 0.3.2 → 0.4.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 +196 -0
- package/PUBLIC_API.md +5 -0
- package/README.md +93 -478
- package/README.zh-CN.md +88 -461
- package/SUBAGENT_ADAPTER_CONTRACT.md +5 -0
- package/dist/agent-profile.d.ts +71 -0
- package/dist/agent-profile.d.ts.map +1 -0
- package/dist/agent-profile.js +303 -0
- package/dist/agent-profile.js.map +1 -0
- package/dist/forge-config.d.ts +88 -0
- package/dist/forge-config.d.ts.map +1 -0
- package/dist/forge-config.js +290 -0
- package/dist/forge-config.js.map +1 -0
- package/dist/forge-extensions.d.ts.map +1 -1
- package/dist/forge-extensions.js +19 -3
- package/dist/forge-extensions.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +97 -358
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +11 -3
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +90 -4
- package/dist/lifecycle.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +116 -5
- package/dist/loader.js.map +1 -1
- package/dist/payload-capture.d.ts.map +1 -1
- package/dist/payload-capture.js +27 -0
- package/dist/payload-capture.js.map +1 -1
- package/dist/payload-command.d.ts +3 -3
- package/dist/payload-command.d.ts.map +1 -1
- package/dist/payload-command.js.map +1 -1
- package/dist/preview.d.ts +2 -2
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +2 -3
- package/dist/preview.js.map +1 -1
- package/dist/profile-command.d.ts +12 -0
- package/dist/profile-command.d.ts.map +1 -0
- package/dist/profile-command.js +291 -0
- package/dist/profile-command.js.map +1 -0
- package/dist/profile-service.d.ts +103 -0
- package/dist/profile-service.d.ts.map +1 -0
- package/dist/profile-service.js +220 -0
- package/dist/profile-service.js.map +1 -0
- package/dist/runtime/profile-runtime.d.ts +13 -0
- package/dist/runtime/profile-runtime.d.ts.map +1 -0
- package/dist/runtime/profile-runtime.js +47 -0
- package/dist/runtime/profile-runtime.js.map +1 -0
- package/dist/runtime/prompt-stack-runtime.d.ts +22 -0
- package/dist/runtime/prompt-stack-runtime.d.ts.map +1 -0
- package/dist/runtime/prompt-stack-runtime.js +104 -0
- package/dist/runtime/prompt-stack-runtime.js.map +1 -0
- package/dist/runtime/subagent-runtime.d.ts +45 -0
- package/dist/runtime/subagent-runtime.d.ts.map +1 -0
- package/dist/runtime/subagent-runtime.js +335 -0
- package/dist/runtime/subagent-runtime.js.map +1 -0
- package/dist/runtime/tool-policy-runtime.d.ts +15 -0
- package/dist/runtime/tool-policy-runtime.d.ts.map +1 -0
- package/dist/runtime/tool-policy-runtime.js +170 -0
- package/dist/runtime/tool-policy-runtime.js.map +1 -0
- package/dist/runtime/web-editor-runtime.d.ts +9 -0
- package/dist/runtime/web-editor-runtime.d.ts.map +1 -0
- package/dist/runtime/web-editor-runtime.js +131 -0
- package/dist/runtime/web-editor-runtime.js.map +1 -0
- package/dist/runtime-state.d.ts +4 -0
- package/dist/runtime-state.d.ts.map +1 -1
- package/dist/runtime-state.js +2 -0
- package/dist/runtime-state.js.map +1 -1
- package/dist/storage.d.ts +4 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +24 -1
- package/dist/storage.js.map +1 -1
- package/dist/subagent/canonical.d.ts +22 -0
- package/dist/subagent/canonical.d.ts.map +1 -0
- package/dist/subagent/canonical.js +24 -0
- package/dist/subagent/canonical.js.map +1 -0
- package/dist/subagent/context.d.ts +8 -0
- package/dist/subagent/context.d.ts.map +1 -0
- package/dist/subagent/context.js +125 -0
- package/dist/subagent/context.js.map +1 -0
- package/dist/subagent/contract.d.ts +10 -0
- package/dist/subagent/contract.d.ts.map +1 -0
- package/dist/subagent/contract.js +10 -0
- package/dist/subagent/contract.js.map +1 -0
- package/dist/subagent/index.d.ts +14 -0
- package/dist/subagent/index.d.ts.map +1 -0
- package/dist/subagent/index.js +14 -0
- package/dist/subagent/index.js.map +1 -0
- package/dist/subagent/plan.d.ts +18 -0
- package/dist/subagent/plan.d.ts.map +1 -0
- package/dist/subagent/plan.js +157 -0
- package/dist/subagent/plan.js.map +1 -0
- package/dist/subagent/preflight.d.ts +4 -0
- package/dist/subagent/preflight.d.ts.map +1 -0
- package/dist/subagent/preflight.js +108 -0
- package/dist/subagent/preflight.js.map +1 -0
- package/dist/subagent/request.d.ts +4 -0
- package/dist/subagent/request.d.ts.map +1 -0
- package/dist/subagent/request.js +122 -0
- package/dist/subagent/request.js.map +1 -0
- package/dist/subagent/response.d.ts +8 -0
- package/dist/subagent/response.d.ts.map +1 -0
- package/dist/subagent/response.js +155 -0
- package/dist/subagent/response.js.map +1 -0
- package/dist/subagent/tools.d.ts +4 -0
- package/dist/subagent/tools.d.ts.map +1 -0
- package/dist/subagent/tools.js +42 -0
- package/dist/subagent/tools.js.map +1 -0
- package/dist/subagent/types.d.ts +268 -0
- package/dist/subagent/types.d.ts.map +1 -0
- package/dist/subagent/types.js +3 -0
- package/dist/subagent/types.js.map +1 -0
- package/dist/subagent/validation.d.ts +35 -0
- package/dist/subagent/validation.d.ts.map +1 -0
- package/dist/subagent/validation.js +314 -0
- package/dist/subagent/validation.js.map +1 -0
- package/dist/subagent-command.d.ts +4 -0
- package/dist/subagent-command.d.ts.map +1 -0
- package/dist/subagent-command.js +246 -0
- package/dist/subagent-command.js.map +1 -0
- package/dist/subagent-host.d.ts +44 -0
- package/dist/subagent-host.d.ts.map +1 -0
- package/dist/subagent-host.js +292 -0
- package/dist/subagent-host.js.map +1 -0
- package/dist/subagent-profile-tool.d.ts +49 -0
- package/dist/subagent-profile-tool.d.ts.map +1 -0
- package/dist/subagent-profile-tool.js +124 -0
- package/dist/subagent-profile-tool.js.map +1 -0
- package/dist/subagent-tool.d.ts +53 -0
- package/dist/subagent-tool.d.ts.map +1 -0
- package/dist/subagent-tool.js +456 -0
- package/dist/subagent-tool.js.map +1 -0
- package/dist/web-editor/client-script.d.ts +2 -0
- package/dist/web-editor/client-script.d.ts.map +1 -0
- package/dist/web-editor/client-script.generated.d.ts +2 -0
- package/dist/web-editor/client-script.generated.d.ts.map +1 -0
- package/dist/web-editor/client-script.generated.js +3 -0
- package/dist/web-editor/client-script.generated.js.map +1 -0
- package/dist/web-editor/client-script.js +2 -0
- package/dist/web-editor/client-script.js.map +1 -0
- package/dist/web-editor/client-styles.d.ts +2 -0
- package/dist/web-editor/client-styles.d.ts.map +1 -0
- package/dist/web-editor/client-styles.generated.d.ts +2 -0
- package/dist/web-editor/client-styles.generated.d.ts.map +1 -0
- package/dist/web-editor/client-styles.generated.js +3 -0
- package/dist/web-editor/client-styles.generated.js.map +1 -0
- package/dist/web-editor/client-styles.js +2 -0
- package/dist/web-editor/client-styles.js.map +1 -0
- package/dist/web-editor/page.d.ts +2 -0
- package/dist/web-editor/page.d.ts.map +1 -1
- package/dist/web-editor/page.js +14 -3319
- package/dist/web-editor/page.js.map +1 -1
- package/dist/web-editor/server.d.ts.map +1 -1
- package/dist/web-editor/server.js +148 -0
- package/dist/web-editor/server.js.map +1 -1
- package/dist/web-editor/styles.d.ts +2 -0
- package/dist/web-editor/styles.d.ts.map +1 -0
- package/dist/web-editor/styles.js +1053 -0
- package/dist/web-editor/styles.js.map +1 -0
- package/dist/web-editor/types.d.ts +79 -0
- package/dist/web-editor/types.d.ts.map +1 -1
- package/dist/web-host.d.ts +16 -5
- package/dist/web-host.d.ts.map +1 -1
- package/dist/web-host.js +307 -0
- package/dist/web-host.js.map +1 -1
- package/docs/README.md +41 -0
- package/docs/concepts/agent-profiles.md +60 -0
- package/docs/concepts/prompt-stacks.md +90 -0
- package/docs/design/README.md +17 -0
- package/docs/design/roadmap-0.4-archive.md +216 -0
- package/docs/design/subagents/design-review.md +220 -0
- package/docs/design/subagents/interface-design.md +274 -0
- package/docs/design/subagents/sdk-spike-findings.md +117 -0
- package/docs/development/complexity-review.md +86 -0
- package/docs/development/release.md +31 -0
- package/docs/development/roadmap.md +42 -0
- package/docs/development/setup.md +75 -0
- package/docs/getting-started.md +93 -0
- package/docs/guides/custom-macros-and-slots.md +68 -0
- package/docs/guides/debugging.md +39 -0
- package/docs/guides/delegation.md +99 -0
- package/docs/guides/sillytavern-import.md +47 -0
- package/docs/guides/use-cases.md +65 -0
- package/docs/guides/web-editor.md +75 -0
- package/docs/reference/commands.md +60 -0
- package/docs/reference/configuration.md +64 -0
- package/docs/reference/features.md +279 -0
- package/docs/reference/macros-and-slots.md +82 -0
- package/docs/reference/public-api.md +28 -0
- package/docs/reference/stack-schema.md +167 -0
- package/docs/reference/subagent-adapter.md +204 -0
- package/docs/zh-CN/README.md +37 -0
- package/docs/zh-CN/concepts/agent-profiles.md +44 -0
- package/docs/zh-CN/concepts/prompt-stacks.md +40 -0
- package/docs/zh-CN/getting-started.md +79 -0
- package/docs/zh-CN/guides/delegation.md +66 -0
- package/docs/zh-CN/guides/web-editor.md +45 -0
- package/docs/zh-CN/reference/commands.md +58 -0
- package/package.json +55 -13
- package/src/compiler.ts +0 -578
- package/src/extension-registry.ts +0 -33
- package/src/forge-extensions.ts +0 -223
- package/src/index.ts +0 -445
- package/src/lifecycle.ts +0 -171
- package/src/loader.ts +0 -394
- package/src/macro-engine.ts +0 -358
- package/src/payload-capture.ts +0 -85
- package/src/payload-command.ts +0 -138
- package/src/policy.ts +0 -42
- package/src/preset-command.ts +0 -280
- package/src/preview.ts +0 -226
- package/src/regex.ts +0 -500
- package/src/render-helpers.ts +0 -169
- package/src/runtime-state.ts +0 -40
- package/src/sillytavern-importer/items.ts +0 -98
- package/src/sillytavern-importer/macros.ts +0 -159
- package/src/sillytavern-importer/prompt-order.ts +0 -54
- package/src/sillytavern-importer/regex.ts +0 -270
- package/src/sillytavern-importer/report.ts +0 -202
- package/src/sillytavern-importer/types.ts +0 -120
- package/src/sillytavern-importer.ts +0 -152
- package/src/slot-renderers.ts +0 -414
- package/src/stack-migration.ts +0 -159
- package/src/storage.ts +0 -45
- package/src/types.ts +0 -209
- package/src/web-editor/index.ts +0 -2
- package/src/web-editor/page.ts +0 -3330
- package/src/web-editor/server.ts +0 -294
- package/src/web-editor/types.ts +0 -98
- package/src/web-host.ts +0 -232
package/src/lifecycle.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
compileMessages,
|
|
4
|
-
compileSystemPrompt,
|
|
5
|
-
createPromptVariableStore,
|
|
6
|
-
getLatestUserMessage,
|
|
7
|
-
markSessionVariablesClean,
|
|
8
|
-
resetTurnVariables,
|
|
9
|
-
} from "./compiler.ts";
|
|
10
|
-
import { applyFinalizeRegexRulesToMessage } from "./regex.ts";
|
|
11
|
-
import { STATE_ENTRY_TYPE, VARIABLE_ENTRY_TYPE, type PiForgeRuntimeState } from "./runtime-state.ts";
|
|
12
|
-
import type { PromptStackDiagnostic, PromptVariableStore, PromptVariableValue } from "./types.ts";
|
|
13
|
-
|
|
14
|
-
export interface LifecycleDeps {
|
|
15
|
-
reloadStacks(ctx: ExtensionContext, preferredId?: string): Promise<void>;
|
|
16
|
-
refreshWebEditorHost(ctx: ExtensionContext): void;
|
|
17
|
-
notifyActivePreset(ctx: ExtensionContext, detail: string): void;
|
|
18
|
-
syncActiveToolPolicy(ctx?: ExtensionContext): void;
|
|
19
|
-
activeId(): string | undefined;
|
|
20
|
-
persistActiveSelection(id: string): void;
|
|
21
|
-
recordCompileDiagnostics(ctx: ExtensionContext, diagnostics: PromptStackDiagnostic[]): void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function registerLifecycleHandlers(pi: ExtensionAPI, state: PiForgeRuntimeState, deps: LifecycleDeps): void {
|
|
25
|
-
pi.on("session_start", async (event, ctx) => {
|
|
26
|
-
await restoreBranchScopedRuntime(ctx, state, deps);
|
|
27
|
-
deps.refreshWebEditorHost(ctx);
|
|
28
|
-
deps.notifyActivePreset(ctx, "after session " + event.reason);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
pi.on("session_tree", async (_event, ctx) => {
|
|
32
|
-
await restoreBranchScopedRuntime(ctx, state, deps);
|
|
33
|
-
deps.refreshWebEditorHost(ctx);
|
|
34
|
-
deps.notifyActivePreset(ctx, "after tree navigation");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
pi.on("session_compact", async (_event, ctx) => {
|
|
38
|
-
await restoreBranchScopedRuntime(ctx, state, deps);
|
|
39
|
-
deps.refreshWebEditorHost(ctx);
|
|
40
|
-
deps.notifyActivePreset(ctx, "after compaction");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
pi.on("turn_start", async () => {
|
|
44
|
-
deps.syncActiveToolPolicy();
|
|
45
|
-
const id = deps.activeId();
|
|
46
|
-
if (id) deps.persistActiveSelection(id);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
50
|
-
state.currentSystemPromptOptions = event.systemPromptOptions;
|
|
51
|
-
state.currentLatestUserMessage = event.prompt;
|
|
52
|
-
state.currentVariableStore = createPromptVariableStore(state.sessionVariables);
|
|
53
|
-
resetTurnVariables(state.currentVariableStore);
|
|
54
|
-
state.contextRewritePending = true;
|
|
55
|
-
|
|
56
|
-
if (!state.active) return;
|
|
57
|
-
|
|
58
|
-
const result = compileSystemPrompt(
|
|
59
|
-
state.active.stack,
|
|
60
|
-
{ options: event.systemPromptOptions, ctx, latestUserMessage: event.prompt, now: new Date(), variables: state.currentVariableStore },
|
|
61
|
-
event.systemPrompt,
|
|
62
|
-
);
|
|
63
|
-
deps.recordCompileDiagnostics(ctx, result.diagnostics);
|
|
64
|
-
persistVariablesIfDirty(pi, state, state.currentVariableStore);
|
|
65
|
-
|
|
66
|
-
return { systemPrompt: result.systemPrompt };
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
pi.on("context", async (event, ctx) => {
|
|
70
|
-
if (!state.active || !state.currentSystemPromptOptions || !state.contextRewritePending) return;
|
|
71
|
-
|
|
72
|
-
// Rewrite the message layout only for the first provider request of a user-submitted prompt.
|
|
73
|
-
// Tool-result follow-up turns must receive Pi's natural context; otherwise post-history
|
|
74
|
-
// prompt blocks such as COT / {{lastUserMessage}} are re-appended after every tool call
|
|
75
|
-
// and the model restarts its planning instead of continuing from the tool result.
|
|
76
|
-
state.contextRewritePending = false;
|
|
77
|
-
|
|
78
|
-
if (!state.currentVariableStore) state.currentVariableStore = createPromptVariableStore(state.sessionVariables);
|
|
79
|
-
const latestUserMessage = getLatestUserMessage(event.messages) ?? state.currentLatestUserMessage;
|
|
80
|
-
const result = compileMessages(
|
|
81
|
-
state.active.stack,
|
|
82
|
-
{ options: state.currentSystemPromptOptions, ctx, latestUserMessage, now: new Date(), variables: state.currentVariableStore },
|
|
83
|
-
event.messages,
|
|
84
|
-
);
|
|
85
|
-
deps.recordCompileDiagnostics(ctx, [...state.latestCompileDiagnostics, ...result.diagnostics]);
|
|
86
|
-
persistVariablesIfDirty(pi, state, state.currentVariableStore);
|
|
87
|
-
return { messages: result.messages };
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
pi.on("message_end", async (event, ctx) => {
|
|
91
|
-
if (!state.active) return;
|
|
92
|
-
const diagnostics: PromptStackDiagnostic[] = [];
|
|
93
|
-
const message = applyFinalizeRegexRulesToMessage(state.active.stack, event.message, diagnostics);
|
|
94
|
-
if (diagnostics.length > 0) deps.recordCompileDiagnostics(ctx, [...state.latestCompileDiagnostics, ...diagnostics]);
|
|
95
|
-
if (!message) return;
|
|
96
|
-
return { message };
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
pi.on("agent_end", async () => {
|
|
100
|
-
persistVariablesIfDirty(pi, state, state.currentVariableStore);
|
|
101
|
-
state.currentSystemPromptOptions = undefined;
|
|
102
|
-
state.currentLatestUserMessage = undefined;
|
|
103
|
-
state.currentVariableStore = undefined;
|
|
104
|
-
state.contextRewritePending = false;
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function restoreBranchScopedRuntime(ctx: ExtensionContext, state: PiForgeRuntimeState, deps: LifecycleDeps): Promise<void> {
|
|
109
|
-
state.sessionVariables = getRestoredVariables(ctx);
|
|
110
|
-
state.currentVariableStore = undefined;
|
|
111
|
-
const restoredActiveId = getRestoredActiveId(ctx);
|
|
112
|
-
state.lastPersistedActiveId = restoredActiveId;
|
|
113
|
-
await deps.reloadStacks(ctx, restoredActiveId);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function getCurrentBranchEntries(ctx: ExtensionContext): unknown[] {
|
|
117
|
-
const leafId = ctx.sessionManager.getLeafId();
|
|
118
|
-
if (leafId === null) return [];
|
|
119
|
-
const sessionManager = ctx.sessionManager as {
|
|
120
|
-
getBranch?: (fromId?: string) => unknown[];
|
|
121
|
-
getEntries: () => unknown[];
|
|
122
|
-
};
|
|
123
|
-
return sessionManager.getBranch ? sessionManager.getBranch(leafId ?? undefined) : sessionManager.getEntries();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function getRestoredActiveId(ctx: ExtensionContext): string | undefined {
|
|
127
|
-
const entries = getCurrentBranchEntries(ctx);
|
|
128
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
129
|
-
const entry = entries[i] as { type?: string; customType?: string; data?: { activeStackId?: unknown } };
|
|
130
|
-
if (entry.type === "custom" && entry.customType === STATE_ENTRY_TYPE) {
|
|
131
|
-
return typeof entry.data?.activeStackId === "string" ? entry.data.activeStackId : undefined;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return undefined;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function getRestoredVariables(ctx: ExtensionContext): Record<string, PromptVariableValue> {
|
|
138
|
-
const entries = getCurrentBranchEntries(ctx);
|
|
139
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
140
|
-
const entry = entries[i] as { type?: string; customType?: string; data?: { variables?: unknown } };
|
|
141
|
-
if (entry.type !== "custom" || entry.customType !== VARIABLE_ENTRY_TYPE) continue;
|
|
142
|
-
if (!entry.data || typeof entry.data.variables !== "object" || Array.isArray(entry.data.variables)) return {};
|
|
143
|
-
return normalizeVariableRecord(entry.data.variables as Record<string, unknown>);
|
|
144
|
-
}
|
|
145
|
-
return {};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function persistVariablesIfDirty(pi: ExtensionAPI, state: PiForgeRuntimeState, store: PromptVariableStore | undefined): void {
|
|
149
|
-
if (!store?.sessionDirty) return;
|
|
150
|
-
state.sessionVariables = { ...store.session };
|
|
151
|
-
pi.appendEntry(VARIABLE_ENTRY_TYPE, { variables: state.sessionVariables });
|
|
152
|
-
markSessionVariablesClean(store);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function normalizeVariableRecord(value: Record<string, unknown>): Record<string, PromptVariableValue> {
|
|
156
|
-
const result: Record<string, PromptVariableValue> = {};
|
|
157
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
158
|
-
if (isPromptVariableValue(raw)) result[key] = raw;
|
|
159
|
-
}
|
|
160
|
-
return result;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function isPromptVariableValue(value: unknown): value is PromptVariableValue {
|
|
164
|
-
if (value === null) return true;
|
|
165
|
-
const type = typeof value;
|
|
166
|
-
if (type === "string" || type === "boolean") return true;
|
|
167
|
-
if (type === "number") return Number.isFinite(value);
|
|
168
|
-
if (Array.isArray(value)) return value.every(isPromptVariableValue);
|
|
169
|
-
if (!value || typeof value !== "object") return false;
|
|
170
|
-
return Object.values(value as Record<string, unknown>).every(isPromptVariableValue);
|
|
171
|
-
}
|
package/src/loader.ts
DELETED
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { basename, join } from "node:path";
|
|
3
|
-
import { validateRegexConfig } from "./regex.ts";
|
|
4
|
-
import { promptStackReadDirs } from "./storage.ts";
|
|
5
|
-
import type {
|
|
6
|
-
LoadedPromptStack,
|
|
7
|
-
PromptResourcePolicy,
|
|
8
|
-
PromptStack,
|
|
9
|
-
PromptStackDiagnostic,
|
|
10
|
-
PromptStackItem,
|
|
11
|
-
PromptStackRole,
|
|
12
|
-
} from "./types.ts";
|
|
13
|
-
import { SUPPORTED_SLOTS } from "./types.ts";
|
|
14
|
-
|
|
15
|
-
const VALID_ROLES = new Set<PromptStackRole>(["system", "user", "assistant", "custom"]);
|
|
16
|
-
const VALID_CHAT_HISTORY_TOOL_MODES = new Set(["keep", "drop"]);
|
|
17
|
-
|
|
18
|
-
export { isInsidePromptStackStorage, legacyPromptStacksDir, promptStackPath, promptStackReadDirs, promptStacksDir } from "./storage.ts";
|
|
19
|
-
|
|
20
|
-
export function loadPromptStacks(cwd: string): LoadedPromptStack[] {
|
|
21
|
-
const files = promptStackFiles(cwd);
|
|
22
|
-
const loaded = files.map(loadPromptStackFile);
|
|
23
|
-
annotateDuplicateStackIds(loaded);
|
|
24
|
-
return loaded;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function promptStackFiles(cwd: string): string[] {
|
|
28
|
-
const files: string[] = [];
|
|
29
|
-
const shadowedNames = new Set<string>();
|
|
30
|
-
|
|
31
|
-
for (const dir of promptStackReadDirs(cwd)) {
|
|
32
|
-
if (!existsSync(dir)) continue;
|
|
33
|
-
|
|
34
|
-
let entries: string[];
|
|
35
|
-
try {
|
|
36
|
-
entries = readdirSync(dir).filter((name) => name.endsWith(".json"));
|
|
37
|
-
} catch {
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
for (const name of entries.sort()) {
|
|
42
|
-
if (shadowedNames.has(name)) continue;
|
|
43
|
-
shadowedNames.add(name);
|
|
44
|
-
files.push(join(dir, name));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return files;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function annotateDuplicateStackIds(stacks: LoadedPromptStack[]): void {
|
|
52
|
-
const byId = new Map<string, LoadedPromptStack[]>();
|
|
53
|
-
for (const loaded of stacks) {
|
|
54
|
-
const id = loaded.stack.id;
|
|
55
|
-
const matches = byId.get(id) ?? [];
|
|
56
|
-
matches.push(loaded);
|
|
57
|
-
byId.set(id, matches);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
for (const [id, matches] of byId) {
|
|
61
|
-
if (matches.length <= 1) continue;
|
|
62
|
-
const files = matches.map((loaded) => basename(loaded.filePath)).join(", ");
|
|
63
|
-
for (const loaded of matches) {
|
|
64
|
-
loaded.diagnostics.push({
|
|
65
|
-
level: "error",
|
|
66
|
-
message: `Duplicate stack id: ${id} appears in multiple files (${files}).`,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function chooseDefaultStack(
|
|
73
|
-
stacks: LoadedPromptStack[],
|
|
74
|
-
preferredId?: string,
|
|
75
|
-
): LoadedPromptStack | undefined {
|
|
76
|
-
if (isDisabledPromptStackId(preferredId)) return undefined;
|
|
77
|
-
|
|
78
|
-
if (preferredId) {
|
|
79
|
-
const preferred = stacks.find((loaded) => loaded.stack.id === preferredId);
|
|
80
|
-
if (preferred && isUsablePromptStack(preferred)) return preferred;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const defaultStack = stacks.find(
|
|
84
|
-
(loaded) =>
|
|
85
|
-
basename(loaded.filePath) === "default.json" &&
|
|
86
|
-
loaded.stack.autoActivate !== false &&
|
|
87
|
-
isUsablePromptStack(loaded),
|
|
88
|
-
);
|
|
89
|
-
if (defaultStack) return defaultStack;
|
|
90
|
-
|
|
91
|
-
return stacks.find((loaded) => loaded.stack.autoActivate === true && isUsablePromptStack(loaded));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function isUsablePromptStack(loaded: LoadedPromptStack): boolean {
|
|
95
|
-
return !loaded.diagnostics.some((diagnostic) => diagnostic.level === "error");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function isDisabledPromptStackId(id: string | undefined): boolean {
|
|
99
|
-
return id === "none" || id === "off";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function loadPromptStackFile(filePath: string): LoadedPromptStack {
|
|
103
|
-
const diagnostics: PromptStackDiagnostic[] = [];
|
|
104
|
-
let raw: unknown;
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
108
|
-
} catch (error) {
|
|
109
|
-
return {
|
|
110
|
-
filePath,
|
|
111
|
-
stack: fallbackStack(filePath),
|
|
112
|
-
diagnostics: [
|
|
113
|
-
{
|
|
114
|
-
level: "error",
|
|
115
|
-
message: `Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const stack = normalizeStack(raw, filePath, diagnostics);
|
|
122
|
-
diagnostics.push(...validatePromptStack(stack));
|
|
123
|
-
|
|
124
|
-
return { filePath, stack, diagnostics };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function fallbackStack(filePath: string): PromptStack {
|
|
128
|
-
return {
|
|
129
|
-
schemaVersion: 1,
|
|
130
|
-
type: "pi-forge.prompt-stack",
|
|
131
|
-
id: basename(filePath, ".json"),
|
|
132
|
-
name: basename(filePath),
|
|
133
|
-
items: [],
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function normalizeStack(raw: unknown, filePath: string, diagnostics: PromptStackDiagnostic[]): PromptStack {
|
|
138
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
139
|
-
diagnostics.push({ level: "error", message: "Prompt stack root must be a JSON object." });
|
|
140
|
-
return fallbackStack(filePath);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const obj = raw as Record<string, unknown>;
|
|
144
|
-
const id = typeof obj.id === "string" && obj.id.trim() ? obj.id.trim() : basename(filePath, ".json");
|
|
145
|
-
const schemaVersion = 1;
|
|
146
|
-
if (obj.schemaVersion !== 1) {
|
|
147
|
-
diagnostics.push({ level: "warning", message: "Missing or unsupported schemaVersion; assuming 1." });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const items = Array.isArray(obj.items) ? obj.items.map((item, index) => normalizeItem(item, index, diagnostics)) : [];
|
|
151
|
-
if (!Array.isArray(obj.items)) {
|
|
152
|
-
diagnostics.push({ level: "error", message: "Prompt stack must contain an items array." });
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (isPlainObject(obj.state) && Object.keys(obj.state).length > 0) {
|
|
156
|
-
diagnostics.push({
|
|
157
|
-
level: "info",
|
|
158
|
-
message: "state is no longer supported and was ignored; use stack.variables and template variable macros instead.",
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
schemaVersion,
|
|
164
|
-
type: obj.type === "pi-forge.prompt-stack" ? "pi-forge.prompt-stack" : undefined,
|
|
165
|
-
id,
|
|
166
|
-
name: typeof obj.name === "string" ? obj.name : undefined,
|
|
167
|
-
description: typeof obj.description === "string" ? obj.description : undefined,
|
|
168
|
-
autoActivate: typeof obj.autoActivate === "boolean" ? obj.autoActivate : undefined,
|
|
169
|
-
mode: obj.mode === "append" || obj.mode === "prepend" || obj.mode === "replace" ? obj.mode : undefined,
|
|
170
|
-
defaults: isPlainObject(obj.defaults) ? (obj.defaults as PromptStack["defaults"]) : undefined,
|
|
171
|
-
context: isPlainObject(obj.context) ? (obj.context as PromptStack["context"]) : undefined,
|
|
172
|
-
tools: normalizeResourcePolicy(obj.tools, "tools", diagnostics),
|
|
173
|
-
skills: normalizeResourcePolicy(obj.skills, "skills", diagnostics),
|
|
174
|
-
variables: normalizeStringRecord(obj.variables),
|
|
175
|
-
regex: normalizeRegexConfig(obj.regex, diagnostics),
|
|
176
|
-
items,
|
|
177
|
-
import: isPlainObject(obj.import) ? (obj.import as Record<string, unknown>) : undefined,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function normalizeItem(raw: unknown, index: number, diagnostics: PromptStackDiagnostic[]): PromptStackItem {
|
|
182
|
-
const fallbackId = `item-${index + 1}`;
|
|
183
|
-
|
|
184
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
185
|
-
diagnostics.push({ level: "error", message: `Item ${index + 1} must be an object.`, itemId: fallbackId });
|
|
186
|
-
return { kind: "block", id: fallbackId, enabled: false, content: "" };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const obj = raw as Record<string, unknown>;
|
|
190
|
-
const kind = obj.kind === "slot" ? "slot" : "block";
|
|
191
|
-
const id = normalizeId(obj.id, fallbackId);
|
|
192
|
-
const base = {
|
|
193
|
-
kind,
|
|
194
|
-
id,
|
|
195
|
-
name: typeof obj.name === "string" ? obj.name : undefined,
|
|
196
|
-
enabled: typeof obj.enabled === "boolean" ? obj.enabled : undefined,
|
|
197
|
-
role: VALID_ROLES.has(obj.role as PromptStackRole) ? (obj.role as PromptStackRole) : undefined,
|
|
198
|
-
tags: Array.isArray(obj.tags) ? obj.tags.filter((tag): tag is string => typeof tag === "string") : undefined,
|
|
199
|
-
source: isPlainObject(obj.source) ? (obj.source as Record<string, unknown>) : undefined,
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
if (kind === "slot") {
|
|
203
|
-
return {
|
|
204
|
-
...base,
|
|
205
|
-
kind: "slot",
|
|
206
|
-
slot: typeof obj.slot === "string" ? obj.slot : "",
|
|
207
|
-
options: isPlainObject(obj.options) ? obj.options : undefined,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
...base,
|
|
213
|
-
kind: "block",
|
|
214
|
-
content: typeof obj.content === "string" ? obj.content : "",
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function validatePromptStack(stack: PromptStack): PromptStackDiagnostic[] {
|
|
219
|
-
const diagnostics: PromptStackDiagnostic[] = [];
|
|
220
|
-
const ids = new Set<string>();
|
|
221
|
-
let chatHistoryCount = 0;
|
|
222
|
-
|
|
223
|
-
if (!stack.id.trim()) diagnostics.push({ level: "error", message: "Stack id must not be empty." });
|
|
224
|
-
|
|
225
|
-
for (const item of stack.items) {
|
|
226
|
-
if (ids.has(item.id)) diagnostics.push({ level: "error", message: `Duplicate item id: ${item.id}`, itemId: item.id });
|
|
227
|
-
ids.add(item.id);
|
|
228
|
-
|
|
229
|
-
if (item.role && !VALID_ROLES.has(item.role)) {
|
|
230
|
-
diagnostics.push({ level: "error", message: `Invalid role: ${item.role}`, itemId: item.id });
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (item.kind === "slot") {
|
|
234
|
-
if (!SUPPORTED_SLOTS.has(item.slot as any)) {
|
|
235
|
-
diagnostics.push({ level: "warning", message: `Unsupported slot: ${item.slot}`, itemId: item.id });
|
|
236
|
-
}
|
|
237
|
-
if (item.enabled !== false && item.slot === "chat-history") chatHistoryCount++;
|
|
238
|
-
if (item.slot === "chat-history") validateChatHistoryOptions(item, diagnostics);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (item.kind === "block" && item.role !== "system" && item.enabled !== false && !item.role) {
|
|
242
|
-
diagnostics.push({ level: "warning", message: "Enabled block has no role and will be ignored.", itemId: item.id });
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (chatHistoryCount === 0) {
|
|
247
|
-
diagnostics.push({
|
|
248
|
-
level: "warning",
|
|
249
|
-
message: "No enabled chat-history slot found; Pi chat context will be appended at the end.",
|
|
250
|
-
});
|
|
251
|
-
} else if (chatHistoryCount > 1 && !stack.context?.allowDuplicateChatHistory) {
|
|
252
|
-
diagnostics.push({
|
|
253
|
-
level: "warning",
|
|
254
|
-
message: "Multiple enabled chat-history slots found; only the first will be expanded unless context.allowDuplicateChatHistory is true.",
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
diagnostics.push(...validateResourcePolicy(stack.tools, "tools"));
|
|
259
|
-
diagnostics.push(...validateResourcePolicy(stack.skills, "skills"));
|
|
260
|
-
if ((stack.mode === "append" || stack.mode === "prepend") && hasPolicyEntries(stack.skills)) {
|
|
261
|
-
diagnostics.push({
|
|
262
|
-
level: "warning",
|
|
263
|
-
message: "skills policy only filters pi-forge skills slots. Use mode \"replace\" if you need the base Pi prompt to omit filtered skills.",
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
diagnostics.push(...validateRegexConfig(stack.regex));
|
|
268
|
-
|
|
269
|
-
return diagnostics;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function validateChatHistoryOptions(
|
|
273
|
-
item: Extract<PromptStackItem, { kind: "slot" }>,
|
|
274
|
-
diagnostics: PromptStackDiagnostic[],
|
|
275
|
-
): void {
|
|
276
|
-
const options = item.options;
|
|
277
|
-
if (!options) return;
|
|
278
|
-
|
|
279
|
-
for (const key of ["includeLastUserMessage", "stripAssistantThinking", "includeSummaries"]) {
|
|
280
|
-
const value = options[key];
|
|
281
|
-
if (value !== undefined && typeof value !== "boolean") {
|
|
282
|
-
diagnostics.push({ level: "warning", message: `chat-history option ${key} should be a boolean.`, itemId: item.id });
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (options.roles !== undefined && !isStringArray(options.roles)) {
|
|
287
|
-
diagnostics.push({ level: "error", message: "chat-history option roles must be an array of strings.", itemId: item.id });
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (options.toolMode !== undefined && (typeof options.toolMode !== "string" || !VALID_CHAT_HISTORY_TOOL_MODES.has(options.toolMode))) {
|
|
291
|
-
diagnostics.push({ level: "error", message: 'chat-history option toolMode must be "keep" or "drop".', itemId: item.id });
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
for (const key of ["maxMessages", "maxChars"]) {
|
|
295
|
-
const value = options[key];
|
|
296
|
-
if (value !== undefined && !isPositiveInteger(value)) {
|
|
297
|
-
diagnostics.push({ level: "error", message: `chat-history option ${key} must be a positive integer.`, itemId: item.id });
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function normalizeId(value: unknown, fallback: string): string {
|
|
303
|
-
if (typeof value === "string" && value.trim()) return value.trim();
|
|
304
|
-
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
305
|
-
return fallback;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function isStringArray(value: unknown): value is string[] {
|
|
309
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function isPositiveInteger(value: unknown): value is number {
|
|
313
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
317
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function normalizeStringRecord(value: unknown): Record<string, string> | undefined {
|
|
321
|
-
if (!isPlainObject(value)) return undefined;
|
|
322
|
-
const result: Record<string, string> = {};
|
|
323
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
324
|
-
if (typeof raw === "string") result[key] = raw;
|
|
325
|
-
}
|
|
326
|
-
return result;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function normalizeResourcePolicy(value: unknown, label: string, diagnostics: PromptStackDiagnostic[]): PromptResourcePolicy | undefined {
|
|
330
|
-
if (value === undefined) return undefined;
|
|
331
|
-
if (!isPlainObject(value)) {
|
|
332
|
-
diagnostics.push({ level: "error", message: `${label} policy must be an object when provided.` });
|
|
333
|
-
return undefined;
|
|
334
|
-
}
|
|
335
|
-
const allow = normalizePolicyPatterns(value.allow, `${label}.allow`, diagnostics);
|
|
336
|
-
const deny = normalizePolicyPatterns(value.deny, `${label}.deny`, diagnostics);
|
|
337
|
-
if (allow && deny) {
|
|
338
|
-
diagnostics.push({ level: "error", message: `${label} policy must use either allow or deny, not both.` });
|
|
339
|
-
return { allow };
|
|
340
|
-
}
|
|
341
|
-
if (allow) return { allow };
|
|
342
|
-
if (deny) return { deny };
|
|
343
|
-
return {};
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function normalizePolicyPatterns(value: unknown, label: string, diagnostics: PromptStackDiagnostic[]): string[] | undefined {
|
|
347
|
-
if (value === undefined) return undefined;
|
|
348
|
-
if (!Array.isArray(value)) {
|
|
349
|
-
diagnostics.push({ level: "error", message: `${label} must be an array of strings when provided.` });
|
|
350
|
-
return undefined;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const patterns: string[] = [];
|
|
354
|
-
for (const [index, item] of value.entries()) {
|
|
355
|
-
if (typeof item !== "string" || !item.trim()) {
|
|
356
|
-
diagnostics.push({ level: "error", message: `${label}[${index}] must be a non-empty string.` });
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
patterns.push(item.trim());
|
|
360
|
-
}
|
|
361
|
-
return patterns.length > 0 ? patterns : undefined;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function validateResourcePolicy(policy: PromptResourcePolicy | undefined, label: string): PromptStackDiagnostic[] {
|
|
365
|
-
const diagnostics: PromptStackDiagnostic[] = [];
|
|
366
|
-
if ((policy?.allow?.length ?? 0) > 0 && (policy?.deny?.length ?? 0) > 0) {
|
|
367
|
-
diagnostics.push({ level: "error", message: `${label} policy must use either allow or deny, not both.` });
|
|
368
|
-
}
|
|
369
|
-
for (const key of ["allow", "deny"] as const) {
|
|
370
|
-
const values = policy?.[key];
|
|
371
|
-
if (!values) continue;
|
|
372
|
-
const seen = new Set<string>();
|
|
373
|
-
for (const value of values) {
|
|
374
|
-
if (seen.has(value)) {
|
|
375
|
-
diagnostics.push({ level: "warning", message: `Duplicate ${label}.${key} pattern: ${value}.` });
|
|
376
|
-
}
|
|
377
|
-
seen.add(value);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
return diagnostics;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function hasPolicyEntries(policy: PromptResourcePolicy | undefined): boolean {
|
|
384
|
-
return !!policy && ((policy.allow?.length ?? 0) > 0 || (policy.deny?.length ?? 0) > 0);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function normalizeRegexConfig(value: unknown, diagnostics: PromptStackDiagnostic[]): PromptStack["regex"] | undefined {
|
|
388
|
-
if (value === undefined) return undefined;
|
|
389
|
-
if (!isPlainObject(value)) {
|
|
390
|
-
diagnostics.push({ level: "warning", message: "regex must be an object when provided." });
|
|
391
|
-
return undefined;
|
|
392
|
-
}
|
|
393
|
-
return value as PromptStack["regex"];
|
|
394
|
-
}
|