@zihanw/pi-forge 0.2.0 → 0.3.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/README.md +233 -105
- package/README.zh-CN.md +215 -104
- package/dist/compiler.d.ts +10 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +490 -0
- package/dist/compiler.js.map +1 -0
- package/dist/extension-registry.d.ts +25 -0
- package/dist/extension-registry.d.ts.map +1 -0
- package/dist/extension-registry.js +6 -0
- package/dist/extension-registry.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +364 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +14 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +139 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/loader.d.ts +8 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +345 -0
- package/dist/loader.js.map +1 -0
- package/dist/macro-engine.d.ts +23 -0
- package/dist/macro-engine.d.ts.map +1 -0
- package/dist/macro-engine.js +262 -0
- package/dist/macro-engine.js.map +1 -0
- package/dist/payload-capture.d.ts +15 -0
- package/dist/payload-capture.d.ts.map +1 -0
- package/dist/payload-capture.js +86 -0
- package/dist/payload-capture.js.map +1 -0
- package/dist/payload-command.d.ts +10 -0
- package/dist/payload-command.d.ts.map +1 -0
- package/dist/payload-command.js +112 -0
- package/dist/payload-command.js.map +1 -0
- package/dist/policy.d.ts +6 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +38 -0
- package/dist/policy.js.map +1 -0
- package/dist/preset-command.d.ts +14 -0
- package/dist/preset-command.d.ts.map +1 -0
- package/dist/preset-command.js +223 -0
- package/dist/preset-command.js.map +1 -0
- package/dist/preview.d.ts +12 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +179 -0
- package/dist/preview.js.map +1 -0
- package/dist/regex.d.ts +7 -0
- package/dist/regex.d.ts.map +1 -0
- package/dist/regex.js +432 -0
- package/dist/regex.js.map +1 -0
- package/dist/render-helpers.d.ts +44 -0
- package/dist/render-helpers.d.ts.map +1 -0
- package/dist/render-helpers.js +128 -0
- package/dist/render-helpers.js.map +1 -0
- package/dist/runtime-state.d.ts +24 -0
- package/dist/runtime-state.d.ts.map +1 -0
- package/dist/runtime-state.js +13 -0
- package/dist/runtime-state.js.map +1 -0
- package/dist/sillytavern-importer/items.d.ts +3 -0
- package/dist/sillytavern-importer/items.d.ts.map +1 -0
- package/dist/sillytavern-importer/items.js +88 -0
- package/dist/sillytavern-importer/items.js.map +1 -0
- package/dist/sillytavern-importer/macros.d.ts +15 -0
- package/dist/sillytavern-importer/macros.d.ts.map +1 -0
- package/dist/sillytavern-importer/macros.js +141 -0
- package/dist/sillytavern-importer/macros.js.map +1 -0
- package/dist/sillytavern-importer/prompt-order.d.ts +6 -0
- package/dist/sillytavern-importer/prompt-order.d.ts.map +1 -0
- package/dist/sillytavern-importer/prompt-order.js +38 -0
- package/dist/sillytavern-importer/prompt-order.js.map +1 -0
- package/dist/sillytavern-importer/regex.d.ts +3 -0
- package/dist/sillytavern-importer/regex.d.ts.map +1 -0
- package/dist/sillytavern-importer/regex.js +275 -0
- package/dist/sillytavern-importer/regex.js.map +1 -0
- package/dist/sillytavern-importer/report.d.ts +21 -0
- package/dist/sillytavern-importer/report.d.ts.map +1 -0
- package/dist/sillytavern-importer/report.js +166 -0
- package/dist/sillytavern-importer/report.js.map +1 -0
- package/dist/sillytavern-importer/types.d.ts +106 -0
- package/dist/sillytavern-importer/types.d.ts.map +1 -0
- package/dist/sillytavern-importer/types.js +2 -0
- package/dist/sillytavern-importer/types.js.map +1 -0
- package/dist/sillytavern-importer.d.ts +5 -0
- package/dist/sillytavern-importer.d.ts.map +1 -0
- package/dist/sillytavern-importer.js +117 -0
- package/dist/sillytavern-importer.js.map +1 -0
- package/dist/slot-renderers.d.ts +25 -0
- package/dist/slot-renderers.d.ts.map +1 -0
- package/dist/slot-renderers.js +334 -0
- package/dist/slot-renderers.js.map +1 -0
- package/dist/stack-migration.d.ts +29 -0
- package/dist/stack-migration.d.ts.map +1 -0
- package/dist/stack-migration.js +126 -0
- package/dist/stack-migration.js.map +1 -0
- package/dist/storage.d.ts +6 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +23 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +173 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/web-editor/index.d.ts +3 -0
- package/dist/web-editor/index.d.ts.map +1 -0
- package/dist/web-editor/index.js +2 -0
- package/dist/web-editor/index.js.map +1 -0
- package/dist/web-editor/page.d.ts +2 -0
- package/dist/web-editor/page.d.ts.map +1 -0
- package/dist/web-editor/page.js +3331 -0
- package/dist/web-editor/page.js.map +1 -0
- package/dist/web-editor/server.d.ts +4 -0
- package/dist/web-editor/server.d.ts.map +1 -0
- package/dist/web-editor/server.js +263 -0
- package/dist/web-editor/server.js.map +1 -0
- package/dist/web-editor/types.d.ts +125 -0
- package/dist/web-editor/types.d.ts.map +1 -0
- package/dist/web-editor/types.js +2 -0
- package/dist/web-editor/types.js.map +1 -0
- package/dist/web-host.d.ts +29 -0
- package/dist/web-host.d.ts.map +1 -0
- package/dist/web-host.js +174 -0
- package/dist/web-host.js.map +1 -0
- package/examples/custom-system-status-extension/README.md +78 -0
- package/examples/custom-system-status-extension/index.ts +176 -0
- package/examples/custom-system-status-extension/prompt-stack.json +39 -0
- package/examples/default-prompt-stack.json +52 -93
- package/examples/image-reader-prompt-stack.json +124 -0
- package/examples/reviewer-prompt-stack.json +115 -0
- package/examples/sillytavern-dm-writer-prompt-stack.json +190 -0
- package/package.json +30 -9
- package/src/compiler.ts +321 -420
- package/src/extension-registry.ts +33 -0
- package/src/index.ts +310 -1110
- package/src/lifecycle.ts +171 -0
- package/src/loader.ts +141 -56
- package/src/macro-engine.ts +358 -0
- package/src/payload-capture.ts +85 -0
- package/src/payload-command.ts +138 -0
- package/src/policy.ts +42 -0
- package/src/preset-command.ts +271 -0
- package/src/preview.ts +226 -0
- package/src/regex.ts +500 -0
- package/src/render-helpers.ts +169 -0
- package/src/runtime-state.ts +36 -0
- package/src/sillytavern-importer/items.ts +98 -0
- package/src/sillytavern-importer/macros.ts +159 -0
- package/src/sillytavern-importer/prompt-order.ts +54 -0
- package/src/sillytavern-importer/regex.ts +270 -0
- package/src/sillytavern-importer/report.ts +202 -0
- package/src/sillytavern-importer/types.ts +120 -0
- package/src/sillytavern-importer.ts +57 -482
- package/src/slot-renderers.ts +414 -0
- package/src/stack-migration.ts +159 -0
- package/src/storage.ts +28 -0
- package/src/types.ts +83 -52
- package/src/web-editor/index.ts +2 -0
- package/src/web-editor/page.ts +3330 -0
- package/src/web-editor/server.ts +294 -0
- package/src/web-editor/types.ts +98 -0
- package/src/web-host.ts +232 -0
- package/src/web-editor.ts +0 -1491
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
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): 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
|
+
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
|
+
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
|
+
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
|
+
function restoreBranchScopedRuntime(ctx: ExtensionContext, state: PiForgeRuntimeState, deps: LifecycleDeps): void {
|
|
109
|
+
state.sessionVariables = getRestoredVariables(ctx);
|
|
110
|
+
state.currentVariableStore = undefined;
|
|
111
|
+
const restoredActiveId = getRestoredActiveId(ctx);
|
|
112
|
+
state.lastPersistedActiveId = restoredActiveId;
|
|
113
|
+
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
CHANGED
|
@@ -1,38 +1,51 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
|
+
import { validateRegexConfig } from "./regex.ts";
|
|
4
|
+
import { promptStackReadDirs } from "./storage.ts";
|
|
3
5
|
import type {
|
|
4
6
|
LoadedPromptStack,
|
|
7
|
+
PromptResourcePolicy,
|
|
5
8
|
PromptStack,
|
|
6
9
|
PromptStackDiagnostic,
|
|
7
10
|
PromptStackItem,
|
|
8
11
|
PromptStackRole,
|
|
9
|
-
PromptStateConfig,
|
|
10
|
-
PromptStateDefinition,
|
|
11
|
-
PromptStateScope,
|
|
12
|
-
PromptStateValue,
|
|
13
12
|
} from "./types.ts";
|
|
14
13
|
import { SUPPORTED_SLOTS } from "./types.ts";
|
|
15
14
|
|
|
16
15
|
const VALID_ROLES = new Set<PromptStackRole>(["system", "user", "assistant", "custom"]);
|
|
16
|
+
const VALID_CHAT_HISTORY_TOOL_MODES = new Set(["keep", "drop"]);
|
|
17
17
|
|
|
18
|
-
export
|
|
19
|
-
return join(cwd, ".pi", "prompt-stacks");
|
|
20
|
-
}
|
|
18
|
+
export { isInsidePromptStackStorage, legacyPromptStacksDir, promptStackPath, promptStackReadDirs, promptStacksDir } from "./storage.ts";
|
|
21
19
|
|
|
22
20
|
export function loadPromptStacks(cwd: string): LoadedPromptStack[] {
|
|
23
|
-
const
|
|
24
|
-
|
|
21
|
+
const files = promptStackFiles(cwd);
|
|
22
|
+
const loaded = files.map(loadPromptStackFile);
|
|
23
|
+
annotateDuplicateStackIds(loaded);
|
|
24
|
+
return loaded;
|
|
25
|
+
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
}
|
|
31
46
|
}
|
|
32
47
|
|
|
33
|
-
|
|
34
|
-
annotateDuplicateStackIds(loaded);
|
|
35
|
-
return loaded;
|
|
48
|
+
return files;
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
function annotateDuplicateStackIds(stacks: LoadedPromptStack[]): void {
|
|
@@ -129,7 +142,7 @@ function normalizeStack(raw: unknown, filePath: string, diagnostics: PromptStack
|
|
|
129
142
|
|
|
130
143
|
const obj = raw as Record<string, unknown>;
|
|
131
144
|
const id = typeof obj.id === "string" && obj.id.trim() ? obj.id.trim() : basename(filePath, ".json");
|
|
132
|
-
const schemaVersion =
|
|
145
|
+
const schemaVersion = 1;
|
|
133
146
|
if (obj.schemaVersion !== 1) {
|
|
134
147
|
diagnostics.push({ level: "warning", message: "Missing or unsupported schemaVersion; assuming 1." });
|
|
135
148
|
}
|
|
@@ -139,6 +152,13 @@ function normalizeStack(raw: unknown, filePath: string, diagnostics: PromptStack
|
|
|
139
152
|
diagnostics.push({ level: "error", message: "Prompt stack must contain an items array." });
|
|
140
153
|
}
|
|
141
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
|
+
|
|
142
162
|
return {
|
|
143
163
|
schemaVersion,
|
|
144
164
|
type: obj.type === "pi-forge.prompt-stack" ? "pi-forge.prompt-stack" : undefined,
|
|
@@ -149,8 +169,10 @@ function normalizeStack(raw: unknown, filePath: string, diagnostics: PromptStack
|
|
|
149
169
|
mode: obj.mode === "append" || obj.mode === "prepend" || obj.mode === "replace" ? obj.mode : undefined,
|
|
150
170
|
defaults: isPlainObject(obj.defaults) ? (obj.defaults as PromptStack["defaults"]) : undefined,
|
|
151
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),
|
|
152
174
|
variables: normalizeStringRecord(obj.variables),
|
|
153
|
-
|
|
175
|
+
regex: normalizeRegexConfig(obj.regex, diagnostics),
|
|
154
176
|
items,
|
|
155
177
|
import: isPlainObject(obj.import) ? (obj.import as Record<string, unknown>) : undefined,
|
|
156
178
|
};
|
|
@@ -213,6 +235,7 @@ export function validatePromptStack(stack: PromptStack): PromptStackDiagnostic[]
|
|
|
213
235
|
diagnostics.push({ level: "warning", message: `Unsupported slot: ${item.slot}`, itemId: item.id });
|
|
214
236
|
}
|
|
215
237
|
if (item.enabled !== false && item.slot === "chat-history") chatHistoryCount++;
|
|
238
|
+
if (item.slot === "chat-history") validateChatHistoryOptions(item, diagnostics);
|
|
216
239
|
}
|
|
217
240
|
|
|
218
241
|
if (item.kind === "block" && item.role !== "system" && item.enabled !== false && !item.role) {
|
|
@@ -232,15 +255,64 @@ export function validatePromptStack(stack: PromptStack): PromptStackDiagnostic[]
|
|
|
232
255
|
});
|
|
233
256
|
}
|
|
234
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
|
+
|
|
235
269
|
return diagnostics;
|
|
236
270
|
}
|
|
237
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
|
+
|
|
238
302
|
function normalizeId(value: unknown, fallback: string): string {
|
|
239
303
|
if (typeof value === "string" && value.trim()) return value.trim();
|
|
240
304
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
241
305
|
return fallback;
|
|
242
306
|
}
|
|
243
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
|
+
|
|
244
316
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
245
317
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
246
318
|
}
|
|
@@ -254,56 +326,69 @@ function normalizeStringRecord(value: unknown): Record<string, string> | undefin
|
|
|
254
326
|
return result;
|
|
255
327
|
}
|
|
256
328
|
|
|
257
|
-
function
|
|
329
|
+
function normalizeResourcePolicy(value: unknown, label: string, diagnostics: PromptStackDiagnostic[]): PromptResourcePolicy | undefined {
|
|
258
330
|
if (value === undefined) return undefined;
|
|
259
331
|
if (!isPlainObject(value)) {
|
|
260
|
-
diagnostics.push({ level: "
|
|
332
|
+
diagnostics.push({ level: "error", message: `${label} policy must be an object when provided.` });
|
|
261
333
|
return undefined;
|
|
262
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
|
+
}
|
|
263
345
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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;
|
|
269
351
|
}
|
|
270
352
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
+
}
|
|
277
363
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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}.` });
|
|
287
376
|
}
|
|
288
|
-
|
|
377
|
+
seen.add(value);
|
|
289
378
|
}
|
|
290
379
|
}
|
|
291
|
-
|
|
292
|
-
return {
|
|
293
|
-
schemaVersion: value.schemaVersion === 1 ? 1 : undefined,
|
|
294
|
-
definitions: Object.keys(definitions).length > 0 ? definitions : undefined,
|
|
295
|
-
};
|
|
380
|
+
return diagnostics;
|
|
296
381
|
}
|
|
297
382
|
|
|
298
|
-
function
|
|
299
|
-
return
|
|
383
|
+
function hasPolicyEntries(policy: PromptResourcePolicy | undefined): boolean {
|
|
384
|
+
return !!policy && ((policy.allow?.length ?? 0) > 0 || (policy.deny?.length ?? 0) > 0);
|
|
300
385
|
}
|
|
301
386
|
|
|
302
|
-
function
|
|
303
|
-
if (value ===
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return
|
|
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"];
|
|
309
394
|
}
|