@zihanw/pi-forge 0.1.0 → 0.3.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/README.md +354 -185
- package/README.zh-CN.md +453 -0
- package/examples/default-prompt-stack.json +54 -51
- package/examples/reviewer-prompt-stack.json +115 -0
- package/examples/sillytavern-dm-writer-prompt-stack.json +190 -0
- package/package.json +4 -8
- package/src/compiler.ts +588 -87
- package/src/index.ts +509 -140
- package/src/loader.ts +179 -12
- package/src/payload-capture.ts +85 -0
- package/src/policy.ts +42 -0
- package/src/regex.ts +500 -0
- package/src/sillytavern-importer.ts +484 -50
- package/src/stack-migration.ts +159 -0
- package/src/storage.ts +28 -0
- package/src/types.ts +83 -2
- package/src/web-editor/index.ts +2 -0
- package/src/web-editor/page.ts +2844 -0
- package/src/web-editor/server.ts +289 -0
- package/src/web-editor/types.ts +84 -0
- package/src/web-host.ts +229 -0
package/src/compiler.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
2
4
|
import type {
|
|
5
|
+
CompileMessageSource,
|
|
3
6
|
CompileMessagesResult,
|
|
4
7
|
CompileSystemPromptResult,
|
|
5
8
|
PromptRuntime,
|
|
@@ -8,8 +11,12 @@ import type {
|
|
|
8
11
|
PromptStackItem,
|
|
9
12
|
PromptStackRole,
|
|
10
13
|
PromptStackSlotItem,
|
|
14
|
+
PromptStackSlotFormat,
|
|
15
|
+
PromptVariableValue,
|
|
11
16
|
PromptVariableStore,
|
|
12
17
|
} from "./types.ts";
|
|
18
|
+
import { applyResourcePolicy } from "./policy.ts";
|
|
19
|
+
import { applyRegexRulesToMessages, applyRegexRulesToString } from "./regex.ts";
|
|
13
20
|
import { SUPPORTED_SLOTS } from "./types.ts";
|
|
14
21
|
|
|
15
22
|
const ZERO_USAGE = {
|
|
@@ -21,7 +28,11 @@ const ZERO_USAGE = {
|
|
|
21
28
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
22
29
|
};
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
type PromptVariableScope = "static" | "session" | "turn";
|
|
32
|
+
|
|
33
|
+
const SUMMARY_ROLES = new Set(["branchSummary", "compactionSummary"]);
|
|
34
|
+
|
|
35
|
+
export function createPromptVariableStore(sessionVariables: Record<string, PromptVariableValue> = {}): PromptVariableStore {
|
|
25
36
|
return { turn: {}, session: { ...sessionVariables }, sessionDirty: false };
|
|
26
37
|
}
|
|
27
38
|
|
|
@@ -63,20 +74,20 @@ export function compileSystemPrompt(
|
|
|
63
74
|
const compiled = parts.join("\n\n");
|
|
64
75
|
const mode = stack.mode ?? "replace";
|
|
65
76
|
|
|
77
|
+
let systemPrompt: string;
|
|
66
78
|
if (!compiled) {
|
|
67
79
|
diagnostics.push({ level: "warning", message: "Compiled system prompt is empty; preserving base system prompt." });
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (mode === "
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (mode === "prepend") {
|
|
76
|
-
return { systemPrompt: compiled ? `${compiled}\n\n${baseSystemPrompt}` : baseSystemPrompt, diagnostics };
|
|
80
|
+
systemPrompt = baseSystemPrompt;
|
|
81
|
+
} else if (mode === "append") {
|
|
82
|
+
systemPrompt = compiled ? `${baseSystemPrompt}\n\n${compiled}` : baseSystemPrompt;
|
|
83
|
+
} else if (mode === "prepend") {
|
|
84
|
+
systemPrompt = compiled ? `${compiled}\n\n${baseSystemPrompt}` : baseSystemPrompt;
|
|
85
|
+
} else {
|
|
86
|
+
systemPrompt = compiled;
|
|
77
87
|
}
|
|
78
88
|
|
|
79
|
-
|
|
89
|
+
systemPrompt = applyRegexRulesToString(stack, systemPrompt, "compiled", "system", diagnostics);
|
|
90
|
+
return { systemPrompt, diagnostics };
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
export function compileMessages(
|
|
@@ -85,7 +96,8 @@ export function compileMessages(
|
|
|
85
96
|
originalMessages: AgentMessage[],
|
|
86
97
|
): CompileMessagesResult {
|
|
87
98
|
const diagnostics: PromptStackDiagnostic[] = [];
|
|
88
|
-
|
|
99
|
+
let messages: AgentMessage[] = [];
|
|
100
|
+
let messageSources: CompileMessageSource[] = [];
|
|
89
101
|
let insertedHistory = false;
|
|
90
102
|
|
|
91
103
|
for (const item of enabledItems(stack)) {
|
|
@@ -98,7 +110,10 @@ export function compileMessages(
|
|
|
98
110
|
});
|
|
99
111
|
continue;
|
|
100
112
|
}
|
|
101
|
-
|
|
113
|
+
const historyMessages = getChatHistoryMessages(originalMessages, item, diagnostics);
|
|
114
|
+
const transformedHistory = applyRegexRulesToMessages(stack, historyMessages, "history", diagnostics);
|
|
115
|
+
messages.push(...transformedHistory);
|
|
116
|
+
messageSources.push(...chatHistoryMessageSources(transformedHistory, item));
|
|
102
117
|
insertedHistory = true;
|
|
103
118
|
continue;
|
|
104
119
|
}
|
|
@@ -110,14 +125,18 @@ export function compileMessages(
|
|
|
110
125
|
: renderSlotText(item, stack, runtime, diagnostics);
|
|
111
126
|
|
|
112
127
|
if (!content.trim()) continue;
|
|
113
|
-
|
|
128
|
+
const message = createSyntheticMessage(item.role, content, stack, runtime);
|
|
129
|
+
messages.push(message);
|
|
130
|
+
messageSources.push(stackItemMessageSource(message, item));
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
if (!insertedHistory) {
|
|
117
134
|
messages.push(...originalMessages);
|
|
135
|
+
messageSources.push(...implicitHistoryMessageSources(originalMessages));
|
|
118
136
|
}
|
|
119
137
|
|
|
120
|
-
|
|
138
|
+
messages = applyRegexRulesToMessages(stack, messages, "compiled", diagnostics);
|
|
139
|
+
return { messages, messageSources, diagnostics };
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
export function getLatestUserMessage(messages: AgentMessage[]): string | undefined {
|
|
@@ -131,32 +150,68 @@ export function getLatestUserMessage(messages: AgentMessage[]): string | undefin
|
|
|
131
150
|
return undefined;
|
|
132
151
|
}
|
|
133
152
|
|
|
134
|
-
export function
|
|
135
|
-
let text =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
text +=
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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]`;
|
|
153
|
+
export function agentMessageToPreviewText(message: AgentMessage): string {
|
|
154
|
+
let text = contentToText((message as { content?: unknown; summary?: string; command?: string; output?: string }).content);
|
|
155
|
+
const role = message.role;
|
|
156
|
+
if (role === "bashExecution") {
|
|
157
|
+
const bash = message as { command?: string; output?: string };
|
|
158
|
+
text += `Ran ${bash.command ?? ""}\n${bash.output ?? ""}`;
|
|
159
|
+
}
|
|
160
|
+
if (role === "branchSummary" || role === "compactionSummary") {
|
|
161
|
+
text += (message as { summary?: string }).summary ?? "";
|
|
149
162
|
}
|
|
150
|
-
return text
|
|
163
|
+
return text;
|
|
151
164
|
}
|
|
152
165
|
|
|
153
|
-
function getChatHistoryMessages(
|
|
154
|
-
|
|
166
|
+
function getChatHistoryMessages(
|
|
167
|
+
messages: AgentMessage[],
|
|
168
|
+
item: PromptStackSlotItem,
|
|
169
|
+
diagnostics: PromptStackDiagnostic[],
|
|
170
|
+
): AgentMessage[] {
|
|
171
|
+
let result = messages;
|
|
172
|
+
const options = item.options ?? {};
|
|
173
|
+
let shouldRepairToolPairs = false;
|
|
174
|
+
|
|
175
|
+
if (options.includeLastUserMessage === false) {
|
|
176
|
+
const lastUserIndex = findLastUserMessageIndex(result);
|
|
177
|
+
if (lastUserIndex !== -1) result = result.filter((_message, index) => index !== lastUserIndex);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (options.includeSummaries === false) {
|
|
181
|
+
const next = result.filter((message) => !isSummaryMessage(message));
|
|
182
|
+
addHistoryFilterDiagnostic(diagnostics, item.id, "summary", result.length, next.length);
|
|
183
|
+
result = next;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (isStringArray(options.roles) && options.roles.length > 0) {
|
|
187
|
+
const allowedRoles = new Set(options.roles);
|
|
188
|
+
const next = result.filter((message) => allowedRoles.has(messageRole(message)));
|
|
189
|
+
addHistoryFilterDiagnostic(diagnostics, item.id, "role", result.length, next.length);
|
|
190
|
+
result = next;
|
|
191
|
+
shouldRepairToolPairs = true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (options.toolMode === "drop") {
|
|
195
|
+
result = dropToolHistory(result, diagnostics, item.id);
|
|
196
|
+
} else if (options.toolMode !== undefined && options.toolMode !== "keep") {
|
|
197
|
+
diagnostics.push({ level: "warning", message: `Unsupported chat-history toolMode: ${String(options.toolMode)}.`, itemId: item.id });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (options.stripAssistantThinking === true) {
|
|
201
|
+
result = stripAssistantThinkingFromHistory(result, diagnostics, item.id);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const limited = limitChatHistory(result, options, diagnostics, item.id);
|
|
205
|
+
if (limited !== result) {
|
|
206
|
+
result = limited;
|
|
207
|
+
shouldRepairToolPairs = true;
|
|
208
|
+
}
|
|
155
209
|
|
|
156
|
-
|
|
157
|
-
|
|
210
|
+
if (shouldRepairToolPairs && options.toolMode !== "drop") {
|
|
211
|
+
result = repairToolHistory(result, diagnostics, item.id);
|
|
212
|
+
}
|
|
158
213
|
|
|
159
|
-
return
|
|
214
|
+
return result;
|
|
160
215
|
}
|
|
161
216
|
|
|
162
217
|
function findLastUserMessageIndex(messages: AgentMessage[]): number {
|
|
@@ -166,6 +221,309 @@ function findLastUserMessageIndex(messages: AgentMessage[]): number {
|
|
|
166
221
|
return -1;
|
|
167
222
|
}
|
|
168
223
|
|
|
224
|
+
function limitChatHistory(
|
|
225
|
+
messages: AgentMessage[],
|
|
226
|
+
options: NonNullable<PromptStackSlotItem["options"]>,
|
|
227
|
+
diagnostics: PromptStackDiagnostic[],
|
|
228
|
+
itemId: string,
|
|
229
|
+
): AgentMessage[] {
|
|
230
|
+
let result = messages;
|
|
231
|
+
const maxMessages = positiveIntegerOption(options.maxMessages);
|
|
232
|
+
const maxChars = positiveIntegerOption(options.maxChars);
|
|
233
|
+
|
|
234
|
+
if (maxMessages !== undefined && result.length > maxMessages) {
|
|
235
|
+
const next = result.slice(-maxMessages);
|
|
236
|
+
diagnostics.push({
|
|
237
|
+
level: "info",
|
|
238
|
+
message: `Trimmed chat history from ${result.length} to ${next.length} message(s) by maxMessages.`,
|
|
239
|
+
itemId,
|
|
240
|
+
});
|
|
241
|
+
result = next;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (maxChars !== undefined) {
|
|
245
|
+
const next = takeRecentMessagesWithinChars(result, maxChars);
|
|
246
|
+
if (next.length < result.length) {
|
|
247
|
+
diagnostics.push({
|
|
248
|
+
level: "info",
|
|
249
|
+
message: `Trimmed chat history from ${result.length} to ${next.length} message(s) by maxChars.`,
|
|
250
|
+
itemId,
|
|
251
|
+
});
|
|
252
|
+
result = next;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function takeRecentMessagesWithinChars(messages: AgentMessage[], maxChars: number): AgentMessage[] {
|
|
260
|
+
const selected: AgentMessage[] = [];
|
|
261
|
+
let chars = 0;
|
|
262
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
263
|
+
const message = messages[index];
|
|
264
|
+
const messageChars = agentMessageToPreviewText(message).length;
|
|
265
|
+
if (selected.length > 0 && chars + messageChars > maxChars) break;
|
|
266
|
+
selected.push(message);
|
|
267
|
+
chars += messageChars;
|
|
268
|
+
}
|
|
269
|
+
return selected.reverse();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function addHistoryFilterDiagnostic(
|
|
273
|
+
diagnostics: PromptStackDiagnostic[],
|
|
274
|
+
itemId: string,
|
|
275
|
+
filter: string,
|
|
276
|
+
before: number,
|
|
277
|
+
after: number,
|
|
278
|
+
): void {
|
|
279
|
+
if (before === after) return;
|
|
280
|
+
diagnostics.push({
|
|
281
|
+
level: "info",
|
|
282
|
+
message: `Filtered ${before - after} chat-history message(s) by ${filter}.`,
|
|
283
|
+
itemId,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function dropToolHistory(
|
|
288
|
+
messages: AgentMessage[],
|
|
289
|
+
diagnostics: PromptStackDiagnostic[],
|
|
290
|
+
itemId: string,
|
|
291
|
+
): AgentMessage[] {
|
|
292
|
+
let removedToolCalls = 0;
|
|
293
|
+
let droppedToolResults = 0;
|
|
294
|
+
let droppedEmptyMessages = 0;
|
|
295
|
+
let changed = false;
|
|
296
|
+
const result: AgentMessage[] = [];
|
|
297
|
+
|
|
298
|
+
for (const message of messages) {
|
|
299
|
+
if (isToolResultMessage(message)) {
|
|
300
|
+
droppedToolResults++;
|
|
301
|
+
changed = true;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const stripped = stripToolCallParts(message, () => false);
|
|
305
|
+
removedToolCalls += stripped.removedCalls;
|
|
306
|
+
if (stripped.message !== message) changed = true;
|
|
307
|
+
if (!stripped.message) {
|
|
308
|
+
droppedEmptyMessages++;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
result.push(stripped.message);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!changed) return messages;
|
|
315
|
+
diagnostics.push({
|
|
316
|
+
level: "info",
|
|
317
|
+
message: `Dropped tool history from chat-history: removed ${removedToolCalls} tool call(s), dropped ${droppedToolResults} tool result message(s)` +
|
|
318
|
+
(droppedEmptyMessages > 0 ? `, and dropped ${droppedEmptyMessages} empty message(s).` : "."),
|
|
319
|
+
itemId,
|
|
320
|
+
});
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function repairToolHistory(
|
|
325
|
+
messages: AgentMessage[],
|
|
326
|
+
diagnostics: PromptStackDiagnostic[],
|
|
327
|
+
itemId: string,
|
|
328
|
+
): AgentMessage[] {
|
|
329
|
+
const includedCallIds = new Set<string>();
|
|
330
|
+
const includedResultIds = new Set<string>();
|
|
331
|
+
for (const message of messages) {
|
|
332
|
+
for (const id of toolCallIdsForMessage(message)) includedCallIds.add(id);
|
|
333
|
+
const resultId = toolResultMessageId(message);
|
|
334
|
+
if (resultId) includedResultIds.add(resultId);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let removedToolCalls = 0;
|
|
338
|
+
let droppedToolResults = 0;
|
|
339
|
+
let droppedEmptyMessages = 0;
|
|
340
|
+
let changed = false;
|
|
341
|
+
const result: AgentMessage[] = [];
|
|
342
|
+
|
|
343
|
+
for (const message of messages) {
|
|
344
|
+
if (isToolResultMessage(message)) {
|
|
345
|
+
const resultId = toolResultMessageId(message);
|
|
346
|
+
if (!resultId || !includedCallIds.has(resultId)) {
|
|
347
|
+
droppedToolResults++;
|
|
348
|
+
changed = true;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
result.push(message);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const stripped = stripToolCallParts(message, (id) => !!id && includedResultIds.has(id));
|
|
356
|
+
removedToolCalls += stripped.removedCalls;
|
|
357
|
+
if (stripped.message !== message) changed = true;
|
|
358
|
+
if (!stripped.message) {
|
|
359
|
+
droppedEmptyMessages++;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
result.push(stripped.message);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!changed) return messages;
|
|
366
|
+
diagnostics.push({
|
|
367
|
+
level: "info",
|
|
368
|
+
message: `Repaired tool history after chat-history filtering: removed ${removedToolCalls} dangling tool call(s), dropped ${droppedToolResults} dangling tool result message(s)` +
|
|
369
|
+
(droppedEmptyMessages > 0 ? `, and dropped ${droppedEmptyMessages} empty message(s).` : "."),
|
|
370
|
+
itemId,
|
|
371
|
+
});
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function stripToolCallParts(
|
|
376
|
+
message: AgentMessage,
|
|
377
|
+
keep: (id: string | undefined) => boolean,
|
|
378
|
+
): { message?: AgentMessage; removedCalls: number } {
|
|
379
|
+
const content = (message as { content?: unknown }).content;
|
|
380
|
+
if (!Array.isArray(content)) return { message, removedCalls: 0 };
|
|
381
|
+
|
|
382
|
+
const nextContent = content.filter((part) => !isToolCallContent(part) || keep(toolCallPartId(part)));
|
|
383
|
+
const removedCalls = content.length - nextContent.length;
|
|
384
|
+
if (removedCalls === 0) return { message, removedCalls: 0 };
|
|
385
|
+
if (nextContent.length === 0) return { removedCalls };
|
|
386
|
+
return { message: { ...message, content: nextContent } as AgentMessage, removedCalls };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function toolCallIdsForMessage(message: AgentMessage): string[] {
|
|
390
|
+
const content = (message as { content?: unknown }).content;
|
|
391
|
+
if (!Array.isArray(content)) return [];
|
|
392
|
+
return content
|
|
393
|
+
.map((part) => toolCallPartId(part))
|
|
394
|
+
.filter((id): id is string => !!id);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function isToolCallContent(value: unknown): boolean {
|
|
398
|
+
return isPlainObject(value) && value.type === "toolCall";
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function toolCallPartId(value: unknown): string | undefined {
|
|
402
|
+
if (!isPlainObject(value)) return undefined;
|
|
403
|
+
return firstString(value.id, value.toolCallId, value.callId);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function isToolResultMessage(message: AgentMessage): boolean {
|
|
407
|
+
return messageRole(message) === "toolResult";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function toolResultMessageId(message: AgentMessage): string | undefined {
|
|
411
|
+
if (!isToolResultMessage(message)) return undefined;
|
|
412
|
+
const raw = message as unknown as Record<string, unknown>;
|
|
413
|
+
return firstString(raw.toolCallId, raw.id, raw.callId);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isSummaryMessage(message: AgentMessage): boolean {
|
|
417
|
+
return SUMMARY_ROLES.has(messageRole(message));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function messageRole(message: AgentMessage): string {
|
|
421
|
+
return String((message as { role?: unknown }).role ?? "");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function positiveIntegerOption(value: unknown): number | undefined {
|
|
425
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function firstString(...values: unknown[]): string | undefined {
|
|
429
|
+
for (const value of values) {
|
|
430
|
+
if (typeof value === "string" && value) return value;
|
|
431
|
+
}
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function isStringArray(value: unknown): value is string[] {
|
|
436
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
440
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function chatHistoryMessageSources(messages: AgentMessage[], item: PromptStackSlotItem): CompileMessageSource[] {
|
|
444
|
+
return messages.map((message, index) => ({
|
|
445
|
+
kind: "chat-history",
|
|
446
|
+
itemId: item.id,
|
|
447
|
+
itemName: item.name,
|
|
448
|
+
slot: item.slot,
|
|
449
|
+
historyIndex: index + 1,
|
|
450
|
+
historyCount: messages.length,
|
|
451
|
+
role: String((message as { role?: unknown }).role ?? ""),
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function implicitHistoryMessageSources(messages: AgentMessage[]): CompileMessageSource[] {
|
|
456
|
+
return messages.map((message, index) => ({
|
|
457
|
+
kind: "implicit-history",
|
|
458
|
+
itemName: "Conversation history",
|
|
459
|
+
slot: "chat-history",
|
|
460
|
+
historyIndex: index + 1,
|
|
461
|
+
historyCount: messages.length,
|
|
462
|
+
role: String((message as { role?: unknown }).role ?? ""),
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function stackItemMessageSource(message: AgentMessage, item: PromptStackItem): CompileMessageSource {
|
|
467
|
+
return {
|
|
468
|
+
kind: "stack-item",
|
|
469
|
+
itemId: item.id,
|
|
470
|
+
itemName: item.name,
|
|
471
|
+
role: String((message as { role?: unknown }).role ?? ""),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function stripAssistantThinkingFromHistory(
|
|
476
|
+
messages: AgentMessage[],
|
|
477
|
+
diagnostics: PromptStackDiagnostic[],
|
|
478
|
+
itemId: string,
|
|
479
|
+
): AgentMessage[] {
|
|
480
|
+
let strippedBlocks = 0;
|
|
481
|
+
let changedMessages = 0;
|
|
482
|
+
let droppedMessages = 0;
|
|
483
|
+
let changed = false;
|
|
484
|
+
const result: AgentMessage[] = [];
|
|
485
|
+
|
|
486
|
+
for (const message of messages) {
|
|
487
|
+
const stripped = stripAssistantThinkingFromMessage(message);
|
|
488
|
+
if (stripped.message !== message) {
|
|
489
|
+
changed = true;
|
|
490
|
+
strippedBlocks += stripped.removedBlocks;
|
|
491
|
+
changedMessages++;
|
|
492
|
+
}
|
|
493
|
+
if (!stripped.message) {
|
|
494
|
+
droppedMessages++;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
result.push(stripped.message);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (!changed) return messages;
|
|
501
|
+
|
|
502
|
+
diagnostics.push({
|
|
503
|
+
level: "info",
|
|
504
|
+
message: `Stripped ${strippedBlocks} assistant thinking block(s) from ${changedMessages} chat-history message(s)` +
|
|
505
|
+
(droppedMessages > 0 ? ` and dropped ${droppedMessages} empty assistant message(s).` : "."),
|
|
506
|
+
itemId,
|
|
507
|
+
});
|
|
508
|
+
return result;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function stripAssistantThinkingFromMessage(message: AgentMessage): { message?: AgentMessage; removedBlocks: number } {
|
|
512
|
+
if (message.role !== "assistant") return { message, removedBlocks: 0 };
|
|
513
|
+
const content = (message as { content?: unknown }).content;
|
|
514
|
+
if (!Array.isArray(content)) return { message, removedBlocks: 0 };
|
|
515
|
+
|
|
516
|
+
const nextContent = content.filter((part) => !isThinkingContent(part));
|
|
517
|
+
const removedBlocks = content.length - nextContent.length;
|
|
518
|
+
if (removedBlocks === 0) return { message, removedBlocks: 0 };
|
|
519
|
+
if (nextContent.length === 0) return { removedBlocks };
|
|
520
|
+
return { message: { ...message, content: nextContent } as AgentMessage, removedBlocks };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function isThinkingContent(value: unknown): boolean {
|
|
524
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && (value as { type?: unknown }).type === "thinking";
|
|
525
|
+
}
|
|
526
|
+
|
|
169
527
|
function enabledItems(stack: PromptStack): PromptStackItem[] {
|
|
170
528
|
return stack.items.filter((item) => item.enabled !== false);
|
|
171
529
|
}
|
|
@@ -185,13 +543,13 @@ function renderSlotText(
|
|
|
185
543
|
|
|
186
544
|
switch (slot) {
|
|
187
545
|
case "tools":
|
|
188
|
-
return renderTools(runtime);
|
|
546
|
+
return renderTools(item, stack, runtime);
|
|
189
547
|
case "tool-guidelines":
|
|
190
|
-
return renderToolGuidelines(runtime);
|
|
548
|
+
return renderToolGuidelines(item, stack, runtime);
|
|
191
549
|
case "skills":
|
|
192
|
-
return renderSkills(runtime);
|
|
550
|
+
return renderSkills(item, stack, runtime);
|
|
193
551
|
case "project-context":
|
|
194
|
-
return renderProjectContext(runtime);
|
|
552
|
+
return renderProjectContext(item, runtime);
|
|
195
553
|
case "append-system-prompt":
|
|
196
554
|
return runtime.options.appendSystemPrompt ?? "";
|
|
197
555
|
case "date":
|
|
@@ -215,9 +573,24 @@ function renderSlotText(
|
|
|
215
573
|
}
|
|
216
574
|
}
|
|
217
575
|
|
|
218
|
-
function renderTools(runtime: PromptRuntime): string {
|
|
219
|
-
const tools = runtime.options.selectedTools ?? [];
|
|
576
|
+
function renderTools(item: PromptStackSlotItem, stack: PromptStack, runtime: PromptRuntime): string {
|
|
220
577
|
const snippets = runtime.options.toolSnippets ?? {};
|
|
578
|
+
const tools = item.options?.onlyWithSnippets === true
|
|
579
|
+
? scopedToolNames(stack, runtime).filter((name) => !!snippets[name])
|
|
580
|
+
: scopedToolNames(stack, runtime);
|
|
581
|
+
|
|
582
|
+
if (slotTextFormat(item) === "plain") {
|
|
583
|
+
const lines = ["Available tools:"];
|
|
584
|
+
if (tools.length === 0) {
|
|
585
|
+
lines.push(item.options?.onlyWithSnippets === true ? "(none)" : "- (none)");
|
|
586
|
+
} else {
|
|
587
|
+
for (const name of tools) {
|
|
588
|
+
lines.push(plainBullet(name, snippets[name] ?? "No prompt snippet provided."));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return lines.join("\n");
|
|
592
|
+
}
|
|
593
|
+
|
|
221
594
|
const lines = ["<available_tools>"];
|
|
222
595
|
|
|
223
596
|
if (tools.length === 0) {
|
|
@@ -233,8 +606,8 @@ function renderTools(runtime: PromptRuntime): string {
|
|
|
233
606
|
return lines.join("\n");
|
|
234
607
|
}
|
|
235
608
|
|
|
236
|
-
function renderToolGuidelines(runtime: PromptRuntime): string {
|
|
237
|
-
const tools = runtime
|
|
609
|
+
function renderToolGuidelines(item: PromptStackSlotItem, stack: PromptStack, runtime: PromptRuntime): string {
|
|
610
|
+
const tools = scopedToolNames(stack, runtime);
|
|
238
611
|
const guidelines: string[] = [];
|
|
239
612
|
const seen = new Set<string>();
|
|
240
613
|
const add = (line: string) => {
|
|
@@ -245,17 +618,31 @@ function renderToolGuidelines(runtime: PromptRuntime): string {
|
|
|
245
618
|
};
|
|
246
619
|
|
|
247
620
|
if (tools.includes("bash") && !tools.includes("grep") && !tools.includes("find") && !tools.includes("ls")) {
|
|
248
|
-
add("Use bash for file operations like ls, rg, find.");
|
|
621
|
+
add(item.options?.piStyle === true ? "Use bash for file operations like ls, rg, find" : "Use bash for file operations like ls, rg, find.");
|
|
249
622
|
}
|
|
250
623
|
|
|
251
624
|
for (const guideline of runtime.options.promptGuidelines ?? []) add(guideline);
|
|
625
|
+
if (item.options?.includePiDefaultGuidelines === true) {
|
|
626
|
+
add("Be concise in your responses");
|
|
627
|
+
add("Show file paths clearly when working with files");
|
|
628
|
+
}
|
|
252
629
|
|
|
253
630
|
if (guidelines.length === 0) return "";
|
|
631
|
+
if (slotTextFormat(item) === "plain") {
|
|
632
|
+
const heading = typeof item.options?.heading === "string" ? item.options.heading.trim() : "Tool guidelines:";
|
|
633
|
+
return [
|
|
634
|
+
...(heading ? [heading] : []),
|
|
635
|
+
...guidelines.map((line) => `- ${plainContinuation(line, " ")}`),
|
|
636
|
+
].join("\n");
|
|
637
|
+
}
|
|
254
638
|
return ["<tool_guidelines>", ...guidelines.map((line) => `- ${line}`), "</tool_guidelines>"].join("\n");
|
|
255
639
|
}
|
|
256
640
|
|
|
257
|
-
function renderSkills(runtime: PromptRuntime): string {
|
|
258
|
-
|
|
641
|
+
function renderSkills(item: PromptStackSlotItem, stack: PromptStack, runtime: PromptRuntime): string {
|
|
642
|
+
if (item.options?.requireReadTool === true && !scopedToolNames(stack, runtime).includes("read")) return "";
|
|
643
|
+
const skills = (runtime.options.skills ?? [])
|
|
644
|
+
.filter((skill) => !skill.disableModelInvocation)
|
|
645
|
+
.filter((skill) => applyResourcePolicy([skill.name], stack.skills).length > 0);
|
|
259
646
|
if (skills.length === 0) return "";
|
|
260
647
|
|
|
261
648
|
const lines = [
|
|
@@ -263,9 +650,18 @@ function renderSkills(runtime: PromptRuntime): string {
|
|
|
263
650
|
"Use the read tool to load a skill's file when the task matches its description.",
|
|
264
651
|
"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
652
|
"",
|
|
266
|
-
"<available_skills>",
|
|
267
653
|
];
|
|
268
654
|
|
|
655
|
+
if (slotTextFormat(item) === "plain") {
|
|
656
|
+
lines.push("Available skills:");
|
|
657
|
+
for (const skill of skills) {
|
|
658
|
+
lines.push(plainBullet(skill.name, skill.description));
|
|
659
|
+
lines.push(` Location: ${plainContinuation(skill.filePath, " ")}`);
|
|
660
|
+
}
|
|
661
|
+
return lines.join("\n");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
lines.push("<available_skills>");
|
|
269
665
|
for (const skill of skills) {
|
|
270
666
|
lines.push(" <skill>");
|
|
271
667
|
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
@@ -278,10 +674,23 @@ function renderSkills(runtime: PromptRuntime): string {
|
|
|
278
674
|
return lines.join("\n");
|
|
279
675
|
}
|
|
280
676
|
|
|
281
|
-
function
|
|
677
|
+
function scopedToolNames(stack: PromptStack, runtime: PromptRuntime): string[] {
|
|
678
|
+
return applyResourcePolicy(runtime.options.selectedTools ?? [], stack.tools);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function renderProjectContext(item: PromptStackSlotItem, runtime: PromptRuntime): string {
|
|
282
682
|
const contextFiles = runtime.options.contextFiles ?? [];
|
|
283
683
|
if (contextFiles.length === 0) return "";
|
|
284
684
|
|
|
685
|
+
if (slotTextFormat(item) === "plain") {
|
|
686
|
+
const lines = ["Project context:", "", "Project-specific instructions and guidelines:", ""];
|
|
687
|
+
for (const file of contextFiles) {
|
|
688
|
+
lines.push(`Path: ${file.path}`);
|
|
689
|
+
lines.push(indentPlainBlock(file.content, " "), "");
|
|
690
|
+
}
|
|
691
|
+
return lines.join("\n").trimEnd();
|
|
692
|
+
}
|
|
693
|
+
|
|
285
694
|
const lines = ["<project_context>", "", "Project-specific instructions and guidelines:", ""];
|
|
286
695
|
for (const file of contextFiles) {
|
|
287
696
|
lines.push(`<project_instructions path=\"${escapeXml(file.path)}\">`);
|
|
@@ -292,58 +701,141 @@ function renderProjectContext(runtime: PromptRuntime): string {
|
|
|
292
701
|
return lines.join("\n");
|
|
293
702
|
}
|
|
294
703
|
|
|
704
|
+
function slotTextFormat(item: PromptStackSlotItem, options: { allowJson?: boolean } = {}): PromptStackSlotFormat {
|
|
705
|
+
const format = item.options?.format;
|
|
706
|
+
if (format === "plain") return "plain";
|
|
707
|
+
if (format === "json" && options.allowJson) return "json";
|
|
708
|
+
return "xml";
|
|
709
|
+
}
|
|
710
|
+
|
|
295
711
|
function renderPiDocsGuidance(): string {
|
|
712
|
+
const paths = piDocsPaths();
|
|
296
713
|
return [
|
|
297
|
-
"Pi documentation
|
|
298
|
-
|
|
299
|
-
|
|
714
|
+
"Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):",
|
|
715
|
+
`- Main documentation: ${paths.readme}`,
|
|
716
|
+
`- Additional docs: ${paths.docs}`,
|
|
717
|
+
`- Examples: ${paths.examples} (extensions, custom tools, SDK)`,
|
|
718
|
+
"- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory",
|
|
719
|
+
"- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md)",
|
|
720
|
+
"- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing",
|
|
721
|
+
"- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)",
|
|
300
722
|
].join("\n");
|
|
301
723
|
}
|
|
302
724
|
|
|
725
|
+
function piDocsPaths(): { readme: string; docs: string; examples: string } {
|
|
726
|
+
const fallbackRoot = "@earendil-works/pi-coding-agent";
|
|
727
|
+
try {
|
|
728
|
+
const resolve = (import.meta as unknown as { resolve?: (specifier: string) => string }).resolve;
|
|
729
|
+
if (!resolve) throw new Error("import.meta.resolve unavailable");
|
|
730
|
+
const resolved = resolve("@earendil-works/pi-coding-agent");
|
|
731
|
+
if (!resolved.startsWith("file:")) throw new Error("non-file package resolution");
|
|
732
|
+
const packageRoot = dirname(dirname(fileURLToPath(resolved)));
|
|
733
|
+
return {
|
|
734
|
+
readme: join(packageRoot, "README.md"),
|
|
735
|
+
docs: join(packageRoot, "docs"),
|
|
736
|
+
examples: join(packageRoot, "examples"),
|
|
737
|
+
};
|
|
738
|
+
} catch {
|
|
739
|
+
return {
|
|
740
|
+
readme: `${fallbackRoot}/README.md`,
|
|
741
|
+
docs: `${fallbackRoot}/docs`,
|
|
742
|
+
examples: `${fallbackRoot}/examples`,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
303
747
|
function renderVariables(
|
|
304
748
|
item: PromptStackSlotItem,
|
|
305
749
|
stack: PromptStack,
|
|
306
750
|
runtime: PromptRuntime,
|
|
307
751
|
): string {
|
|
308
752
|
const options = item.options ?? {};
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
const includeTurn = options.includeTurn !== false;
|
|
753
|
+
const scopes = selectedVariableScopes(options);
|
|
754
|
+
const format = slotTextFormat(item);
|
|
312
755
|
const store = runtime.variables;
|
|
313
|
-
const
|
|
756
|
+
const grouped: Record<PromptVariableScope, Record<string, PromptVariableValue>> = {
|
|
757
|
+
static: {},
|
|
758
|
+
session: {},
|
|
759
|
+
turn: {},
|
|
760
|
+
};
|
|
314
761
|
|
|
315
|
-
|
|
762
|
+
if (scopes.includes("static")) grouped.static = collectStaticVariables(stack);
|
|
763
|
+
if (scopes.includes("session")) grouped.session = { ...(store?.session ?? {}) };
|
|
764
|
+
if (scopes.includes("turn")) grouped.turn = { ...(store?.turn ?? {}) };
|
|
316
765
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
parts.push(" </static>");
|
|
766
|
+
const hasVariables = Object.values(grouped).some((values) => Object.keys(values).length > 0);
|
|
767
|
+
if (!hasVariables) return "";
|
|
768
|
+
|
|
769
|
+
if (format === "plain") {
|
|
770
|
+
return renderPlainVariables(grouped);
|
|
323
771
|
}
|
|
324
772
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
773
|
+
const parts: string[] = ["<variables>"];
|
|
774
|
+
|
|
775
|
+
for (const scope of scopes) {
|
|
776
|
+
const entries = Object.entries(grouped[scope]).sort(([a], [b]) => a.localeCompare(b));
|
|
777
|
+
if (entries.length === 0) continue;
|
|
778
|
+
parts.push(` <${scope}>`);
|
|
779
|
+
for (const [name, value] of entries) {
|
|
780
|
+
parts.push(` <var name=\"${escapeXml(name)}\">${escapeXml(variableValueToPromptText(value))}</var>`);
|
|
329
781
|
}
|
|
330
|
-
parts.push(
|
|
782
|
+
parts.push(` </${scope}>`);
|
|
331
783
|
}
|
|
332
784
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
785
|
+
parts.push("</variables>");
|
|
786
|
+
return parts.join("\n");
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function renderPlainVariables(
|
|
790
|
+
grouped: Record<PromptVariableScope, Record<string, PromptVariableValue>>,
|
|
791
|
+
): string {
|
|
792
|
+
const parts: string[] = ["Variables:"];
|
|
793
|
+
|
|
794
|
+
for (const scope of ["static", "session", "turn"] as const) {
|
|
795
|
+
const entries = Object.entries(grouped[scope]).sort(([a], [b]) => a.localeCompare(b));
|
|
796
|
+
if (entries.length === 0) continue;
|
|
797
|
+
parts.push(`${scope}:`);
|
|
798
|
+
for (const [name, value] of entries) {
|
|
799
|
+
parts.push(plainBullet(name, variableValueToPromptText(value)));
|
|
337
800
|
}
|
|
338
|
-
parts.push(" </turn>");
|
|
339
801
|
}
|
|
340
802
|
|
|
341
|
-
parts.
|
|
803
|
+
return parts.join("\n");
|
|
804
|
+
}
|
|
342
805
|
|
|
343
|
-
|
|
344
|
-
|
|
806
|
+
function plainBullet(label: string, value: string): string {
|
|
807
|
+
return `- ${label}: ${plainContinuation(value, " ")}`;
|
|
808
|
+
}
|
|
345
809
|
|
|
346
|
-
|
|
810
|
+
function plainContinuation(value: string, indent: string): string {
|
|
811
|
+
return value.split("\n").map((line, index) => index === 0 ? line : `${indent}${line}`).join("\n");
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function indentPlainBlock(value: string, indent: string): string {
|
|
815
|
+
return value.split("\n").map((line) => `${indent}${line}`).join("\n");
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function selectedVariableScopes(options: Record<string, unknown>): PromptVariableScope[] {
|
|
819
|
+
const scopes: PromptVariableScope[] = [];
|
|
820
|
+
if (options.includeStatic !== false) scopes.push("static");
|
|
821
|
+
if (options.includeSession !== false) scopes.push("session");
|
|
822
|
+
if (options.includeTurn !== false) scopes.push("turn");
|
|
823
|
+
return scopes;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function collectStaticVariables(stack: PromptStack): Record<string, PromptVariableValue> {
|
|
827
|
+
return { ...(stack.variables ?? {}) };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function variableValueToPromptText(value: PromptVariableValue): string {
|
|
831
|
+
if (typeof value === "string") return value;
|
|
832
|
+
return JSON.stringify(value, null, 2);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function variableValueToMacroText(value: PromptVariableValue | undefined): string {
|
|
836
|
+
if (value === undefined) return "";
|
|
837
|
+
if (typeof value === "string") return value;
|
|
838
|
+
return JSON.stringify(value);
|
|
347
839
|
}
|
|
348
840
|
|
|
349
841
|
function expandMacros(
|
|
@@ -354,7 +846,6 @@ function expandMacros(
|
|
|
354
846
|
itemId: string,
|
|
355
847
|
): string {
|
|
356
848
|
const policy = stack.defaults?.unresolvedMacroPolicy ?? "warn";
|
|
357
|
-
const staticVariables = stack.variables ?? {};
|
|
358
849
|
const unknown = new Set<string>();
|
|
359
850
|
|
|
360
851
|
const result = text.replace(/\{\{([^{}]+)\}\}/g, (full, rawName: string) => {
|
|
@@ -386,9 +877,9 @@ function expandMacros(
|
|
|
386
877
|
unknown.add(expression);
|
|
387
878
|
return full;
|
|
388
879
|
}
|
|
389
|
-
if (command === "getturnvar") return runtime.variables?.turn[name]
|
|
390
|
-
if (command === "getsessionvar") return runtime.variables?.session[name]
|
|
391
|
-
return getRuntimeVariable(runtime,
|
|
880
|
+
if (command === "getturnvar") return variableValueToMacroText(runtime.variables?.turn[name]);
|
|
881
|
+
if (command === "getsessionvar") return variableValueToMacroText(runtime.variables?.session[name]);
|
|
882
|
+
return variableValueToMacroText(getRuntimeVariable(runtime, stack, name));
|
|
392
883
|
}
|
|
393
884
|
|
|
394
885
|
if (command === "clearvar" || command === "clearturnvar" || command === "clearsessionvar") {
|
|
@@ -403,11 +894,11 @@ function expandMacros(
|
|
|
403
894
|
return "";
|
|
404
895
|
}
|
|
405
896
|
|
|
406
|
-
const dynamicValue = getBuiltinMacro(command, runtime);
|
|
897
|
+
const dynamicValue = getBuiltinMacro(command, stack, runtime);
|
|
407
898
|
if (dynamicValue !== undefined) return dynamicValue;
|
|
408
899
|
|
|
409
|
-
const variableValue = getRuntimeVariable(runtime,
|
|
410
|
-
if (variableValue !== undefined) return variableValue;
|
|
900
|
+
const variableValue = getRuntimeVariable(runtime, stack, command);
|
|
901
|
+
if (variableValue !== undefined) return variableValueToMacroText(variableValue);
|
|
411
902
|
|
|
412
903
|
unknown.add(expression);
|
|
413
904
|
return full;
|
|
@@ -425,17 +916,19 @@ function expandMacros(
|
|
|
425
916
|
return result;
|
|
426
917
|
}
|
|
427
918
|
|
|
428
|
-
function getBuiltinMacro(name: string, runtime: PromptRuntime): string | undefined {
|
|
919
|
+
function getBuiltinMacro(name: string, stack: PromptStack, runtime: PromptRuntime): string | undefined {
|
|
429
920
|
switch (name) {
|
|
430
921
|
case "cwd":
|
|
431
922
|
return runtime.options.cwd;
|
|
432
923
|
case "date":
|
|
433
924
|
return formatDate(runtime.now);
|
|
925
|
+
case "time":
|
|
926
|
+
return formatTime(runtime.now);
|
|
434
927
|
case "lastUserMessage":
|
|
435
928
|
return runtime.latestUserMessage ?? "";
|
|
436
929
|
case "selectedTools":
|
|
437
930
|
case "tools":
|
|
438
|
-
return (runtime
|
|
931
|
+
return scopedToolNames(stack, runtime).join(", ");
|
|
439
932
|
case "activeModel": {
|
|
440
933
|
const model = runtime.ctx?.model;
|
|
441
934
|
return model ? `${model.provider}/${model.id}` : "";
|
|
@@ -445,9 +938,10 @@ function getBuiltinMacro(name: string, runtime: PromptRuntime): string | undefin
|
|
|
445
938
|
}
|
|
446
939
|
}
|
|
447
940
|
|
|
448
|
-
function getRuntimeVariable(runtime: PromptRuntime,
|
|
941
|
+
function getRuntimeVariable(runtime: PromptRuntime, stack: PromptStack, name: string): PromptVariableValue | undefined {
|
|
449
942
|
if (runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.turn, name)) return runtime.variables.turn[name];
|
|
450
943
|
if (runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.session, name)) return runtime.variables.session[name];
|
|
944
|
+
const staticVariables = collectStaticVariables(stack);
|
|
451
945
|
if (Object.prototype.hasOwnProperty.call(staticVariables, name)) return staticVariables[name];
|
|
452
946
|
return undefined;
|
|
453
947
|
}
|
|
@@ -537,6 +1031,13 @@ function formatDate(now: Date): string {
|
|
|
537
1031
|
return `${year}-${month}-${day}`;
|
|
538
1032
|
}
|
|
539
1033
|
|
|
1034
|
+
function formatTime(now: Date): string {
|
|
1035
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
1036
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
1037
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
1038
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
540
1041
|
function escapeXml(value: string): string {
|
|
541
1042
|
return value
|
|
542
1043
|
.replace(/&/g, "&")
|