@zihanw/pi-forge 0.1.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/LICENSE +21 -0
- package/README.md +286 -0
- package/examples/default-prompt-stack.json +117 -0
- package/examples/validation-issues-prompt-stack.json +38 -0
- package/package.json +58 -0
- package/src/compiler.ts +547 -0
- package/src/index.ts +521 -0
- package/src/loader.ts +227 -0
- package/src/sillytavern-importer.ts +528 -0
- package/src/types.ts +139 -0
package/src/compiler.ts
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type {
|
|
3
|
+
CompileMessagesResult,
|
|
4
|
+
CompileSystemPromptResult,
|
|
5
|
+
PromptRuntime,
|
|
6
|
+
PromptStack,
|
|
7
|
+
PromptStackDiagnostic,
|
|
8
|
+
PromptStackItem,
|
|
9
|
+
PromptStackRole,
|
|
10
|
+
PromptStackSlotItem,
|
|
11
|
+
PromptVariableStore,
|
|
12
|
+
} from "./types.ts";
|
|
13
|
+
import { SUPPORTED_SLOTS } from "./types.ts";
|
|
14
|
+
|
|
15
|
+
const ZERO_USAGE = {
|
|
16
|
+
input: 0,
|
|
17
|
+
output: 0,
|
|
18
|
+
cacheRead: 0,
|
|
19
|
+
cacheWrite: 0,
|
|
20
|
+
totalTokens: 0,
|
|
21
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function createPromptVariableStore(sessionVariables: Record<string, string> = {}): PromptVariableStore {
|
|
25
|
+
return { turn: {}, session: { ...sessionVariables }, sessionDirty: false };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resetTurnVariables(store: PromptVariableStore): void {
|
|
29
|
+
store.turn = {};
|
|
30
|
+
store.sessionDirty = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function markSessionVariablesClean(store: PromptVariableStore): void {
|
|
34
|
+
store.sessionDirty = false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function compileSystemPrompt(
|
|
38
|
+
stack: PromptStack,
|
|
39
|
+
runtime: PromptRuntime,
|
|
40
|
+
baseSystemPrompt: string,
|
|
41
|
+
): CompileSystemPromptResult {
|
|
42
|
+
const diagnostics: PromptStackDiagnostic[] = [];
|
|
43
|
+
const parts: string[] = [];
|
|
44
|
+
|
|
45
|
+
for (const item of enabledItems(stack)) {
|
|
46
|
+
if (item.role !== "system") continue;
|
|
47
|
+
|
|
48
|
+
if (item.kind === "block") {
|
|
49
|
+
const text = expandMacros(item.content, stack, runtime, diagnostics, item.id).trim();
|
|
50
|
+
if (text) parts.push(text);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (item.slot === "chat-history") {
|
|
55
|
+
diagnostics.push({ level: "warning", message: "chat-history slot cannot be placed in the system prompt.", itemId: item.id });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rendered = renderSlotText(item, stack, runtime, diagnostics).trim();
|
|
60
|
+
if (rendered) parts.push(rendered);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const compiled = parts.join("\n\n");
|
|
64
|
+
const mode = stack.mode ?? "replace";
|
|
65
|
+
|
|
66
|
+
if (!compiled) {
|
|
67
|
+
diagnostics.push({ level: "warning", message: "Compiled system prompt is empty; preserving base system prompt." });
|
|
68
|
+
return { systemPrompt: baseSystemPrompt, diagnostics };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (mode === "append") {
|
|
72
|
+
return { systemPrompt: compiled ? `${baseSystemPrompt}\n\n${compiled}` : baseSystemPrompt, diagnostics };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (mode === "prepend") {
|
|
76
|
+
return { systemPrompt: compiled ? `${compiled}\n\n${baseSystemPrompt}` : baseSystemPrompt, diagnostics };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { systemPrompt: compiled, diagnostics };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function compileMessages(
|
|
83
|
+
stack: PromptStack,
|
|
84
|
+
runtime: PromptRuntime,
|
|
85
|
+
originalMessages: AgentMessage[],
|
|
86
|
+
): CompileMessagesResult {
|
|
87
|
+
const diagnostics: PromptStackDiagnostic[] = [];
|
|
88
|
+
const messages: AgentMessage[] = [];
|
|
89
|
+
let insertedHistory = false;
|
|
90
|
+
|
|
91
|
+
for (const item of enabledItems(stack)) {
|
|
92
|
+
if (item.kind === "slot" && item.slot === "chat-history") {
|
|
93
|
+
if (insertedHistory && !stack.context?.allowDuplicateChatHistory) {
|
|
94
|
+
diagnostics.push({
|
|
95
|
+
level: "warning",
|
|
96
|
+
message: "Skipped duplicate chat-history slot.",
|
|
97
|
+
itemId: item.id,
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
messages.push(...getChatHistoryMessages(originalMessages, item));
|
|
102
|
+
insertedHistory = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!item.role || item.role === "system") continue;
|
|
107
|
+
|
|
108
|
+
const content = item.kind === "block"
|
|
109
|
+
? expandMacros(item.content, stack, runtime, diagnostics, item.id)
|
|
110
|
+
: renderSlotText(item, stack, runtime, diagnostics);
|
|
111
|
+
|
|
112
|
+
if (!content.trim()) continue;
|
|
113
|
+
messages.push(createSyntheticMessage(item.role, content, stack, runtime));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!insertedHistory) {
|
|
117
|
+
messages.push(...originalMessages);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { messages, diagnostics };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getLatestUserMessage(messages: AgentMessage[]): string | undefined {
|
|
124
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
125
|
+
const message = messages[i];
|
|
126
|
+
if (message.role === "user") {
|
|
127
|
+
const text = contentToText((message as { content?: unknown }).content);
|
|
128
|
+
if (text.trim()) return text;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function renderPreviewMessages(messages: AgentMessage[], maxChars = 8000): string {
|
|
135
|
+
let text = "";
|
|
136
|
+
for (const message of messages) {
|
|
137
|
+
const role = message.role;
|
|
138
|
+
text += `\n--- ${role} ---\n`;
|
|
139
|
+
text += contentToText((message as { content?: unknown; summary?: string; command?: string; output?: string }).content);
|
|
140
|
+
if (role === "bashExecution") {
|
|
141
|
+
const bash = message as { command?: string; output?: string };
|
|
142
|
+
text += `Ran ${bash.command ?? ""}\n${bash.output ?? ""}`;
|
|
143
|
+
}
|
|
144
|
+
if (role === "branchSummary" || role === "compactionSummary") {
|
|
145
|
+
text += (message as { summary?: string }).summary ?? "";
|
|
146
|
+
}
|
|
147
|
+
text += "\n";
|
|
148
|
+
if (text.length > maxChars) return `${text.slice(0, maxChars)}\n\n[preview truncated]`;
|
|
149
|
+
}
|
|
150
|
+
return text.trimStart();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getChatHistoryMessages(messages: AgentMessage[], item: PromptStackSlotItem): AgentMessage[] {
|
|
154
|
+
if (item.options?.includeLastUserMessage !== false) return messages;
|
|
155
|
+
|
|
156
|
+
const lastUserIndex = findLastUserMessageIndex(messages);
|
|
157
|
+
if (lastUserIndex === -1) return messages;
|
|
158
|
+
|
|
159
|
+
return messages.filter((_message, index) => index !== lastUserIndex);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function findLastUserMessageIndex(messages: AgentMessage[]): number {
|
|
163
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
164
|
+
if (messages[i].role === "user") return i;
|
|
165
|
+
}
|
|
166
|
+
return -1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function enabledItems(stack: PromptStack): PromptStackItem[] {
|
|
170
|
+
return stack.items.filter((item) => item.enabled !== false);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function renderSlotText(
|
|
174
|
+
item: PromptStackSlotItem,
|
|
175
|
+
stack: PromptStack,
|
|
176
|
+
runtime: PromptRuntime,
|
|
177
|
+
diagnostics: PromptStackDiagnostic[],
|
|
178
|
+
): string {
|
|
179
|
+
const slot = item.slot;
|
|
180
|
+
|
|
181
|
+
if (!SUPPORTED_SLOTS.has(slot as any)) {
|
|
182
|
+
diagnostics.push({ level: "warning", message: `Unsupported slot: ${slot}`, itemId: item.id });
|
|
183
|
+
return "";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
switch (slot) {
|
|
187
|
+
case "tools":
|
|
188
|
+
return renderTools(runtime);
|
|
189
|
+
case "tool-guidelines":
|
|
190
|
+
return renderToolGuidelines(runtime);
|
|
191
|
+
case "skills":
|
|
192
|
+
return renderSkills(runtime);
|
|
193
|
+
case "project-context":
|
|
194
|
+
return renderProjectContext(runtime);
|
|
195
|
+
case "append-system-prompt":
|
|
196
|
+
return runtime.options.appendSystemPrompt ?? "";
|
|
197
|
+
case "date":
|
|
198
|
+
return `Current date: ${formatDate(runtime.now)}`;
|
|
199
|
+
case "cwd":
|
|
200
|
+
return `Current working directory: ${runtime.options.cwd.replace(/\\/g, "/")}`;
|
|
201
|
+
case "date-cwd":
|
|
202
|
+
return [`Current date: ${formatDate(runtime.now)}`, `Current working directory: ${runtime.options.cwd.replace(/\\/g, "/")}`].join("\n");
|
|
203
|
+
case "active-model": {
|
|
204
|
+
const model = runtime.ctx?.model;
|
|
205
|
+
return model ? `Current model: ${model.provider}/${model.id}` : "Current model: (none)";
|
|
206
|
+
}
|
|
207
|
+
case "pi-docs":
|
|
208
|
+
return renderPiDocsGuidance();
|
|
209
|
+
case "variables":
|
|
210
|
+
return renderVariables(item, stack, runtime);
|
|
211
|
+
case "chat-history":
|
|
212
|
+
return "";
|
|
213
|
+
default:
|
|
214
|
+
return "";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function renderTools(runtime: PromptRuntime): string {
|
|
219
|
+
const tools = runtime.options.selectedTools ?? [];
|
|
220
|
+
const snippets = runtime.options.toolSnippets ?? {};
|
|
221
|
+
const lines = ["<available_tools>"];
|
|
222
|
+
|
|
223
|
+
if (tools.length === 0) {
|
|
224
|
+
lines.push(" (none)");
|
|
225
|
+
} else {
|
|
226
|
+
for (const name of tools) {
|
|
227
|
+
const snippet = snippets[name] ?? "No prompt snippet provided.";
|
|
228
|
+
lines.push(` <tool name=\"${escapeXml(name)}\">${escapeXml(snippet)}</tool>`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
lines.push("</available_tools>");
|
|
233
|
+
return lines.join("\n");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function renderToolGuidelines(runtime: PromptRuntime): string {
|
|
237
|
+
const tools = runtime.options.selectedTools ?? [];
|
|
238
|
+
const guidelines: string[] = [];
|
|
239
|
+
const seen = new Set<string>();
|
|
240
|
+
const add = (line: string) => {
|
|
241
|
+
const normalized = line.trim();
|
|
242
|
+
if (!normalized || seen.has(normalized)) return;
|
|
243
|
+
seen.add(normalized);
|
|
244
|
+
guidelines.push(normalized);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (tools.includes("bash") && !tools.includes("grep") && !tools.includes("find") && !tools.includes("ls")) {
|
|
248
|
+
add("Use bash for file operations like ls, rg, find.");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const guideline of runtime.options.promptGuidelines ?? []) add(guideline);
|
|
252
|
+
|
|
253
|
+
if (guidelines.length === 0) return "";
|
|
254
|
+
return ["<tool_guidelines>", ...guidelines.map((line) => `- ${line}`), "</tool_guidelines>"].join("\n");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function renderSkills(runtime: PromptRuntime): string {
|
|
258
|
+
const skills = (runtime.options.skills ?? []).filter((skill) => !skill.disableModelInvocation);
|
|
259
|
+
if (skills.length === 0) return "";
|
|
260
|
+
|
|
261
|
+
const lines = [
|
|
262
|
+
"The following skills provide specialized instructions for specific tasks.",
|
|
263
|
+
"Use the read tool to load a skill's file when the task matches its description.",
|
|
264
|
+
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
|
|
265
|
+
"",
|
|
266
|
+
"<available_skills>",
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
for (const skill of skills) {
|
|
270
|
+
lines.push(" <skill>");
|
|
271
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
272
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
273
|
+
lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
|
|
274
|
+
lines.push(" </skill>");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
lines.push("</available_skills>");
|
|
278
|
+
return lines.join("\n");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function renderProjectContext(runtime: PromptRuntime): string {
|
|
282
|
+
const contextFiles = runtime.options.contextFiles ?? [];
|
|
283
|
+
if (contextFiles.length === 0) return "";
|
|
284
|
+
|
|
285
|
+
const lines = ["<project_context>", "", "Project-specific instructions and guidelines:", ""];
|
|
286
|
+
for (const file of contextFiles) {
|
|
287
|
+
lines.push(`<project_instructions path=\"${escapeXml(file.path)}\">`);
|
|
288
|
+
lines.push(file.content);
|
|
289
|
+
lines.push("</project_instructions>", "");
|
|
290
|
+
}
|
|
291
|
+
lines.push("</project_context>");
|
|
292
|
+
return lines.join("\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function renderPiDocsGuidance(): string {
|
|
296
|
+
return [
|
|
297
|
+
"Pi documentation guidance:",
|
|
298
|
+
"- Read Pi documentation only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI.",
|
|
299
|
+
"- Resolve docs/... against Pi's installed documentation directory and examples/... against Pi's installed examples directory.",
|
|
300
|
+
].join("\n");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function renderVariables(
|
|
304
|
+
item: PromptStackSlotItem,
|
|
305
|
+
stack: PromptStack,
|
|
306
|
+
runtime: PromptRuntime,
|
|
307
|
+
): string {
|
|
308
|
+
const options = item.options ?? {};
|
|
309
|
+
const includeStatic = options.includeStatic !== false;
|
|
310
|
+
const includeSession = options.includeSession !== false;
|
|
311
|
+
const includeTurn = options.includeTurn !== false;
|
|
312
|
+
const store = runtime.variables;
|
|
313
|
+
const staticVars = stack.variables ?? {};
|
|
314
|
+
|
|
315
|
+
const parts: string[] = ["<prompt_variables>"];
|
|
316
|
+
|
|
317
|
+
if (includeStatic && Object.keys(staticVars).length > 0) {
|
|
318
|
+
parts.push(" <static>");
|
|
319
|
+
for (const [key, value] of Object.entries(staticVars).sort(([a], [b]) => a.localeCompare(b))) {
|
|
320
|
+
parts.push(` <${escapeXml(key)}>${escapeXml(value)}</${escapeXml(key)}>`);
|
|
321
|
+
}
|
|
322
|
+
parts.push(" </static>");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (includeSession && store && Object.keys(store.session).length > 0) {
|
|
326
|
+
parts.push(" <session>");
|
|
327
|
+
for (const [key, value] of Object.entries(store.session).sort(([a], [b]) => a.localeCompare(b))) {
|
|
328
|
+
parts.push(` <${escapeXml(key)}>${escapeXml(value)}</${escapeXml(key)}>`);
|
|
329
|
+
}
|
|
330
|
+
parts.push(" </session>");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (includeTurn && store && Object.keys(store.turn).length > 0) {
|
|
334
|
+
parts.push(" <turn>");
|
|
335
|
+
for (const [key, value] of Object.entries(store.turn).sort(([a], [b]) => a.localeCompare(b))) {
|
|
336
|
+
parts.push(` <${escapeXml(key)}>${escapeXml(value)}</${escapeXml(key)}>`);
|
|
337
|
+
}
|
|
338
|
+
parts.push(" </turn>");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
parts.push("</prompt_variables>");
|
|
342
|
+
|
|
343
|
+
// Don't emit empty variables block
|
|
344
|
+
if (parts.length <= 2) return "";
|
|
345
|
+
|
|
346
|
+
return parts.join("\n");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function expandMacros(
|
|
350
|
+
text: string,
|
|
351
|
+
stack: PromptStack,
|
|
352
|
+
runtime: PromptRuntime,
|
|
353
|
+
diagnostics: PromptStackDiagnostic[],
|
|
354
|
+
itemId: string,
|
|
355
|
+
): string {
|
|
356
|
+
const policy = stack.defaults?.unresolvedMacroPolicy ?? "warn";
|
|
357
|
+
const staticVariables = stack.variables ?? {};
|
|
358
|
+
const unknown = new Set<string>();
|
|
359
|
+
|
|
360
|
+
const result = text.replace(/\{\{([^{}]+)\}\}/g, (full, rawName: string) => {
|
|
361
|
+
const expression = rawName.trim();
|
|
362
|
+
const parts = expression.split("::");
|
|
363
|
+
const command = parts[0]?.trim();
|
|
364
|
+
|
|
365
|
+
if (!command) return full;
|
|
366
|
+
|
|
367
|
+
// SillyTavern-style mutable variables. By default, setvar is turn-scoped and returns empty text.
|
|
368
|
+
if (command === "setvar" || command === "setturnvar" || command === "setsessionvar") {
|
|
369
|
+
const scoped = command === "setvar" && (parts[1] === "turn" || parts[1] === "session");
|
|
370
|
+
const scope = command === "setsessionvar" || (scoped && parts[1] === "session") ? "session" : "turn";
|
|
371
|
+
const nameIndex = scoped ? 2 : 1;
|
|
372
|
+
const valueIndex = nameIndex + 1;
|
|
373
|
+
const name = parts[nameIndex]?.trim();
|
|
374
|
+
const value = parts.slice(valueIndex).join("::");
|
|
375
|
+
if (!name) {
|
|
376
|
+
unknown.add(expression);
|
|
377
|
+
return full;
|
|
378
|
+
}
|
|
379
|
+
setRuntimeVariable(runtime, scope, name, value);
|
|
380
|
+
return "";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (command === "getvar" || command === "var" || command === "getturnvar" || command === "getsessionvar") {
|
|
384
|
+
const name = parts[1]?.trim();
|
|
385
|
+
if (!name) {
|
|
386
|
+
unknown.add(expression);
|
|
387
|
+
return full;
|
|
388
|
+
}
|
|
389
|
+
if (command === "getturnvar") return runtime.variables?.turn[name] ?? "";
|
|
390
|
+
if (command === "getsessionvar") return runtime.variables?.session[name] ?? "";
|
|
391
|
+
return getRuntimeVariable(runtime, staticVariables, name) ?? "";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (command === "clearvar" || command === "clearturnvar" || command === "clearsessionvar") {
|
|
395
|
+
const scoped = command === "clearvar" && (parts[1] === "turn" || parts[1] === "session");
|
|
396
|
+
const scope = command === "clearsessionvar" || (scoped && parts[1] === "session") ? "session" : "turn";
|
|
397
|
+
const name = parts[scoped ? 2 : 1]?.trim();
|
|
398
|
+
if (!name) {
|
|
399
|
+
unknown.add(expression);
|
|
400
|
+
return full;
|
|
401
|
+
}
|
|
402
|
+
clearRuntimeVariable(runtime, scope, name);
|
|
403
|
+
return "";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const dynamicValue = getBuiltinMacro(command, runtime);
|
|
407
|
+
if (dynamicValue !== undefined) return dynamicValue;
|
|
408
|
+
|
|
409
|
+
const variableValue = getRuntimeVariable(runtime, staticVariables, command);
|
|
410
|
+
if (variableValue !== undefined) return variableValue;
|
|
411
|
+
|
|
412
|
+
unknown.add(expression);
|
|
413
|
+
return full;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
for (const name of unknown) {
|
|
417
|
+
if (policy === "keep") continue;
|
|
418
|
+
diagnostics.push({
|
|
419
|
+
level: policy === "error" ? "error" : "warning",
|
|
420
|
+
message: `Unresolved macro: {{${name}}}`,
|
|
421
|
+
itemId,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function getBuiltinMacro(name: string, runtime: PromptRuntime): string | undefined {
|
|
429
|
+
switch (name) {
|
|
430
|
+
case "cwd":
|
|
431
|
+
return runtime.options.cwd;
|
|
432
|
+
case "date":
|
|
433
|
+
return formatDate(runtime.now);
|
|
434
|
+
case "lastUserMessage":
|
|
435
|
+
return runtime.latestUserMessage ?? "";
|
|
436
|
+
case "selectedTools":
|
|
437
|
+
case "tools":
|
|
438
|
+
return (runtime.options.selectedTools ?? []).join(", ");
|
|
439
|
+
case "activeModel": {
|
|
440
|
+
const model = runtime.ctx?.model;
|
|
441
|
+
return model ? `${model.provider}/${model.id}` : "";
|
|
442
|
+
}
|
|
443
|
+
default:
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function getRuntimeVariable(runtime: PromptRuntime, staticVariables: Record<string, string>, name: string): string | undefined {
|
|
449
|
+
if (runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.turn, name)) return runtime.variables.turn[name];
|
|
450
|
+
if (runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.session, name)) return runtime.variables.session[name];
|
|
451
|
+
if (Object.prototype.hasOwnProperty.call(staticVariables, name)) return staticVariables[name];
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function setRuntimeVariable(runtime: PromptRuntime, scope: "turn" | "session", name: string, value: string): void {
|
|
456
|
+
if (!runtime.variables) return;
|
|
457
|
+
if (scope === "session") {
|
|
458
|
+
if (runtime.variables.session[name] !== value) {
|
|
459
|
+
runtime.variables.session[name] = value;
|
|
460
|
+
runtime.variables.sessionDirty = true;
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
runtime.variables.turn[name] = value;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function clearRuntimeVariable(runtime: PromptRuntime, scope: "turn" | "session", name: string): void {
|
|
468
|
+
if (!runtime.variables) return;
|
|
469
|
+
if (scope === "session") {
|
|
470
|
+
if (Object.prototype.hasOwnProperty.call(runtime.variables.session, name)) {
|
|
471
|
+
delete runtime.variables.session[name];
|
|
472
|
+
runtime.variables.sessionDirty = true;
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
delete runtime.variables.turn[name];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function createSyntheticMessage(role: Exclude<PromptStackRole, "system">, content: string, stack: PromptStack, runtime: PromptRuntime): AgentMessage {
|
|
480
|
+
const timestamp = runtime.now.getTime();
|
|
481
|
+
const visible = stack.defaults?.syntheticMessagesVisible ?? false;
|
|
482
|
+
|
|
483
|
+
if (role === "custom") {
|
|
484
|
+
return {
|
|
485
|
+
role: "custom",
|
|
486
|
+
customType: "pi-forge",
|
|
487
|
+
content: [{ type: "text", text: content }],
|
|
488
|
+
display: visible,
|
|
489
|
+
details: { stackId: stack.id },
|
|
490
|
+
timestamp,
|
|
491
|
+
} as AgentMessage;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (role === "assistant") {
|
|
495
|
+
const model = runtime.ctx?.model;
|
|
496
|
+
return {
|
|
497
|
+
role: "assistant",
|
|
498
|
+
content: [{ type: "text", text: content }],
|
|
499
|
+
api: model?.api ?? "unknown",
|
|
500
|
+
provider: model?.provider ?? "unknown",
|
|
501
|
+
model: model?.id ?? "unknown",
|
|
502
|
+
usage: ZERO_USAGE,
|
|
503
|
+
stopReason: "stop",
|
|
504
|
+
timestamp,
|
|
505
|
+
} as AgentMessage;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
role: "user",
|
|
510
|
+
content: [{ type: "text", text: content }],
|
|
511
|
+
timestamp,
|
|
512
|
+
} as AgentMessage;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function contentToText(content: unknown): string {
|
|
516
|
+
if (typeof content === "string") return content;
|
|
517
|
+
if (!Array.isArray(content)) return "";
|
|
518
|
+
|
|
519
|
+
return content
|
|
520
|
+
.map((part) => {
|
|
521
|
+
if (!part || typeof part !== "object") return "";
|
|
522
|
+
const obj = part as Record<string, unknown>;
|
|
523
|
+
if (obj.type === "text" && typeof obj.text === "string") return obj.text;
|
|
524
|
+
if (obj.type === "thinking" && typeof obj.thinking === "string") return `<thinking>\n${obj.thinking}\n</thinking>`;
|
|
525
|
+
if (obj.type === "toolCall") return `[toolCall: ${String(obj.name ?? "unknown")}]`;
|
|
526
|
+
if (obj.type === "image") return "[image]";
|
|
527
|
+
return "";
|
|
528
|
+
})
|
|
529
|
+
.filter(Boolean)
|
|
530
|
+
.join("\n");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function formatDate(now: Date): string {
|
|
534
|
+
const year = now.getFullYear();
|
|
535
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
536
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
537
|
+
return `${year}-${month}-${day}`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function escapeXml(value: string): string {
|
|
541
|
+
return value
|
|
542
|
+
.replace(/&/g, "&")
|
|
543
|
+
.replace(/</g, "<")
|
|
544
|
+
.replace(/>/g, ">")
|
|
545
|
+
.replace(/\"/g, """)
|
|
546
|
+
.replace(/'/g, "'");
|
|
547
|
+
}
|