@zihanw/pi-forge 0.2.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 +154 -104
- package/README.zh-CN.md +151 -103
- package/examples/default-prompt-stack.json +52 -93
- package/examples/reviewer-prompt-stack.json +115 -0
- package/examples/sillytavern-dm-writer-prompt-stack.json +190 -0
- package/package.json +3 -8
- package/src/compiler.ts +546 -175
- package/src/index.ts +320 -660
- package/src/loader.ts +141 -56
- package/src/payload-capture.ts +85 -0
- package/src/policy.ts +42 -0
- package/src/regex.ts +500 -0
- package/src/sillytavern-importer.ts +404 -19
- package/src/stack-migration.ts +159 -0
- package/src/storage.ts +28 -0
- package/src/types.ts +80 -38
- 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/web-editor.ts +0 -1491
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,10 +11,12 @@ import type {
|
|
|
8
11
|
PromptStackItem,
|
|
9
12
|
PromptStackRole,
|
|
10
13
|
PromptStackSlotItem,
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
PromptStackSlotFormat,
|
|
15
|
+
PromptVariableValue,
|
|
13
16
|
PromptVariableStore,
|
|
14
17
|
} from "./types.ts";
|
|
18
|
+
import { applyResourcePolicy } from "./policy.ts";
|
|
19
|
+
import { applyRegexRulesToMessages, applyRegexRulesToString } from "./regex.ts";
|
|
15
20
|
import { SUPPORTED_SLOTS } from "./types.ts";
|
|
16
21
|
|
|
17
22
|
const ZERO_USAGE = {
|
|
@@ -23,7 +28,11 @@ const ZERO_USAGE = {
|
|
|
23
28
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
24
29
|
};
|
|
25
30
|
|
|
26
|
-
|
|
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 {
|
|
27
36
|
return { turn: {}, session: { ...sessionVariables }, sessionDirty: false };
|
|
28
37
|
}
|
|
29
38
|
|
|
@@ -65,20 +74,20 @@ export function compileSystemPrompt(
|
|
|
65
74
|
const compiled = parts.join("\n\n");
|
|
66
75
|
const mode = stack.mode ?? "replace";
|
|
67
76
|
|
|
77
|
+
let systemPrompt: string;
|
|
68
78
|
if (!compiled) {
|
|
69
79
|
diagnostics.push({ level: "warning", message: "Compiled system prompt is empty; preserving base system prompt." });
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (mode === "
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (mode === "prepend") {
|
|
78
|
-
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;
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
|
|
89
|
+
systemPrompt = applyRegexRulesToString(stack, systemPrompt, "compiled", "system", diagnostics);
|
|
90
|
+
return { systemPrompt, diagnostics };
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
export function compileMessages(
|
|
@@ -87,7 +96,8 @@ export function compileMessages(
|
|
|
87
96
|
originalMessages: AgentMessage[],
|
|
88
97
|
): CompileMessagesResult {
|
|
89
98
|
const diagnostics: PromptStackDiagnostic[] = [];
|
|
90
|
-
|
|
99
|
+
let messages: AgentMessage[] = [];
|
|
100
|
+
let messageSources: CompileMessageSource[] = [];
|
|
91
101
|
let insertedHistory = false;
|
|
92
102
|
|
|
93
103
|
for (const item of enabledItems(stack)) {
|
|
@@ -100,7 +110,10 @@ export function compileMessages(
|
|
|
100
110
|
});
|
|
101
111
|
continue;
|
|
102
112
|
}
|
|
103
|
-
|
|
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));
|
|
104
117
|
insertedHistory = true;
|
|
105
118
|
continue;
|
|
106
119
|
}
|
|
@@ -112,14 +125,18 @@ export function compileMessages(
|
|
|
112
125
|
: renderSlotText(item, stack, runtime, diagnostics);
|
|
113
126
|
|
|
114
127
|
if (!content.trim()) continue;
|
|
115
|
-
|
|
128
|
+
const message = createSyntheticMessage(item.role, content, stack, runtime);
|
|
129
|
+
messages.push(message);
|
|
130
|
+
messageSources.push(stackItemMessageSource(message, item));
|
|
116
131
|
}
|
|
117
132
|
|
|
118
133
|
if (!insertedHistory) {
|
|
119
134
|
messages.push(...originalMessages);
|
|
135
|
+
messageSources.push(...implicitHistoryMessageSources(originalMessages));
|
|
120
136
|
}
|
|
121
137
|
|
|
122
|
-
|
|
138
|
+
messages = applyRegexRulesToMessages(stack, messages, "compiled", diagnostics);
|
|
139
|
+
return { messages, messageSources, diagnostics };
|
|
123
140
|
}
|
|
124
141
|
|
|
125
142
|
export function getLatestUserMessage(messages: AgentMessage[]): string | undefined {
|
|
@@ -133,32 +150,68 @@ export function getLatestUserMessage(messages: AgentMessage[]): string | undefin
|
|
|
133
150
|
return undefined;
|
|
134
151
|
}
|
|
135
152
|
|
|
136
|
-
export function
|
|
137
|
-
let text =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
text +=
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
if (role === "branchSummary" || role === "compactionSummary") {
|
|
147
|
-
text += (message as { summary?: string }).summary ?? "";
|
|
148
|
-
}
|
|
149
|
-
text += "\n";
|
|
150
|
-
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 ?? "";
|
|
151
162
|
}
|
|
152
|
-
return text
|
|
163
|
+
return text;
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
function getChatHistoryMessages(
|
|
156
|
-
|
|
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
|
+
}
|
|
157
199
|
|
|
158
|
-
|
|
159
|
-
|
|
200
|
+
if (options.stripAssistantThinking === true) {
|
|
201
|
+
result = stripAssistantThinkingFromHistory(result, diagnostics, item.id);
|
|
202
|
+
}
|
|
160
203
|
|
|
161
|
-
|
|
204
|
+
const limited = limitChatHistory(result, options, diagnostics, item.id);
|
|
205
|
+
if (limited !== result) {
|
|
206
|
+
result = limited;
|
|
207
|
+
shouldRepairToolPairs = true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (shouldRepairToolPairs && options.toolMode !== "drop") {
|
|
211
|
+
result = repairToolHistory(result, diagnostics, item.id);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return result;
|
|
162
215
|
}
|
|
163
216
|
|
|
164
217
|
function findLastUserMessageIndex(messages: AgentMessage[]): number {
|
|
@@ -168,6 +221,309 @@ function findLastUserMessageIndex(messages: AgentMessage[]): number {
|
|
|
168
221
|
return -1;
|
|
169
222
|
}
|
|
170
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
|
+
|
|
171
527
|
function enabledItems(stack: PromptStack): PromptStackItem[] {
|
|
172
528
|
return stack.items.filter((item) => item.enabled !== false);
|
|
173
529
|
}
|
|
@@ -187,13 +543,13 @@ function renderSlotText(
|
|
|
187
543
|
|
|
188
544
|
switch (slot) {
|
|
189
545
|
case "tools":
|
|
190
|
-
return renderTools(runtime);
|
|
546
|
+
return renderTools(item, stack, runtime);
|
|
191
547
|
case "tool-guidelines":
|
|
192
|
-
return renderToolGuidelines(runtime);
|
|
548
|
+
return renderToolGuidelines(item, stack, runtime);
|
|
193
549
|
case "skills":
|
|
194
|
-
return renderSkills(runtime);
|
|
550
|
+
return renderSkills(item, stack, runtime);
|
|
195
551
|
case "project-context":
|
|
196
|
-
return renderProjectContext(runtime);
|
|
552
|
+
return renderProjectContext(item, runtime);
|
|
197
553
|
case "append-system-prompt":
|
|
198
554
|
return runtime.options.appendSystemPrompt ?? "";
|
|
199
555
|
case "date":
|
|
@@ -217,9 +573,24 @@ function renderSlotText(
|
|
|
217
573
|
}
|
|
218
574
|
}
|
|
219
575
|
|
|
220
|
-
function renderTools(runtime: PromptRuntime): string {
|
|
221
|
-
const tools = runtime.options.selectedTools ?? [];
|
|
576
|
+
function renderTools(item: PromptStackSlotItem, stack: PromptStack, runtime: PromptRuntime): string {
|
|
222
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
|
+
|
|
223
594
|
const lines = ["<available_tools>"];
|
|
224
595
|
|
|
225
596
|
if (tools.length === 0) {
|
|
@@ -235,8 +606,8 @@ function renderTools(runtime: PromptRuntime): string {
|
|
|
235
606
|
return lines.join("\n");
|
|
236
607
|
}
|
|
237
608
|
|
|
238
|
-
function renderToolGuidelines(runtime: PromptRuntime): string {
|
|
239
|
-
const tools = runtime
|
|
609
|
+
function renderToolGuidelines(item: PromptStackSlotItem, stack: PromptStack, runtime: PromptRuntime): string {
|
|
610
|
+
const tools = scopedToolNames(stack, runtime);
|
|
240
611
|
const guidelines: string[] = [];
|
|
241
612
|
const seen = new Set<string>();
|
|
242
613
|
const add = (line: string) => {
|
|
@@ -247,17 +618,31 @@ function renderToolGuidelines(runtime: PromptRuntime): string {
|
|
|
247
618
|
};
|
|
248
619
|
|
|
249
620
|
if (tools.includes("bash") && !tools.includes("grep") && !tools.includes("find") && !tools.includes("ls")) {
|
|
250
|
-
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.");
|
|
251
622
|
}
|
|
252
623
|
|
|
253
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
|
+
}
|
|
254
629
|
|
|
255
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
|
+
}
|
|
256
638
|
return ["<tool_guidelines>", ...guidelines.map((line) => `- ${line}`), "</tool_guidelines>"].join("\n");
|
|
257
639
|
}
|
|
258
640
|
|
|
259
|
-
function renderSkills(runtime: PromptRuntime): string {
|
|
260
|
-
|
|
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);
|
|
261
646
|
if (skills.length === 0) return "";
|
|
262
647
|
|
|
263
648
|
const lines = [
|
|
@@ -265,9 +650,18 @@ function renderSkills(runtime: PromptRuntime): string {
|
|
|
265
650
|
"Use the read tool to load a skill's file when the task matches its description.",
|
|
266
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.",
|
|
267
652
|
"",
|
|
268
|
-
"<available_skills>",
|
|
269
653
|
];
|
|
270
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>");
|
|
271
665
|
for (const skill of skills) {
|
|
272
666
|
lines.push(" <skill>");
|
|
273
667
|
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
@@ -280,10 +674,23 @@ function renderSkills(runtime: PromptRuntime): string {
|
|
|
280
674
|
return lines.join("\n");
|
|
281
675
|
}
|
|
282
676
|
|
|
283
|
-
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 {
|
|
284
682
|
const contextFiles = runtime.options.contextFiles ?? [];
|
|
285
683
|
if (contextFiles.length === 0) return "";
|
|
286
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
|
+
|
|
287
694
|
const lines = ["<project_context>", "", "Project-specific instructions and guidelines:", ""];
|
|
288
695
|
for (const file of contextFiles) {
|
|
289
696
|
lines.push(`<project_instructions path=\"${escapeXml(file.path)}\">`);
|
|
@@ -294,14 +701,49 @@ function renderProjectContext(runtime: PromptRuntime): string {
|
|
|
294
701
|
return lines.join("\n");
|
|
295
702
|
}
|
|
296
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
|
+
|
|
297
711
|
function renderPiDocsGuidance(): string {
|
|
712
|
+
const paths = piDocsPaths();
|
|
298
713
|
return [
|
|
299
|
-
"Pi documentation
|
|
300
|
-
|
|
301
|
-
|
|
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)",
|
|
302
722
|
].join("\n");
|
|
303
723
|
}
|
|
304
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
|
+
|
|
305
747
|
function renderVariables(
|
|
306
748
|
item: PromptStackSlotItem,
|
|
307
749
|
stack: PromptStack,
|
|
@@ -309,159 +751,88 @@ function renderVariables(
|
|
|
309
751
|
): string {
|
|
310
752
|
const options = item.options ?? {};
|
|
311
753
|
const scopes = selectedVariableScopes(options);
|
|
312
|
-
const
|
|
313
|
-
const format = options.format === "json" ? "json" : "xml";
|
|
314
|
-
const includeNamespaces = normalizeStringArray(options.includeNamespaces);
|
|
315
|
-
const excludeNamespaces = normalizeStringArray(options.excludeNamespaces);
|
|
316
|
-
const maxValueChars = typeof options.maxValueChars === "number" && Number.isFinite(options.maxValueChars) && options.maxValueChars > 0
|
|
317
|
-
? Math.floor(options.maxValueChars)
|
|
318
|
-
: undefined;
|
|
754
|
+
const format = slotTextFormat(item);
|
|
319
755
|
const store = runtime.variables;
|
|
320
|
-
const grouped: Record<
|
|
756
|
+
const grouped: Record<PromptVariableScope, Record<string, PromptVariableValue>> = {
|
|
321
757
|
static: {},
|
|
322
758
|
session: {},
|
|
323
759
|
turn: {},
|
|
324
760
|
};
|
|
325
761
|
|
|
326
|
-
if (scopes.
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
if (scopes.has("session")) {
|
|
330
|
-
grouped.session = { ...collectDefaultVariables(stack, "session"), ...(store?.session ?? {}) };
|
|
331
|
-
}
|
|
332
|
-
if (scopes.has("turn")) {
|
|
333
|
-
grouped.turn = { ...collectDefaultVariables(stack, "turn"), ...(store?.turn ?? {}) };
|
|
334
|
-
}
|
|
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 ?? {}) };
|
|
335
765
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (!Object.values(grouped).some((values) => Object.keys(values).length > 0)) return "";
|
|
343
|
-
|
|
344
|
-
if (format === "json") {
|
|
345
|
-
const payload: Record<string, unknown> = {};
|
|
346
|
-
for (const scope of ["static", "session", "turn"] as const) {
|
|
347
|
-
if (Object.keys(grouped[scope]).length === 0) continue;
|
|
348
|
-
payload[scope] = includeMetadata
|
|
349
|
-
? Object.fromEntries(Object.entries(grouped[scope]).sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => [
|
|
350
|
-
name,
|
|
351
|
-
{
|
|
352
|
-
value,
|
|
353
|
-
...metadataForVariable(stack, name),
|
|
354
|
-
},
|
|
355
|
-
]))
|
|
356
|
-
: Object.fromEntries(Object.entries(grouped[scope]).sort(([a], [b]) => a.localeCompare(b)));
|
|
357
|
-
}
|
|
358
|
-
return `<prompt_state format=\"json\">\n${escapeXml(JSON.stringify(payload, null, 2))}\n</prompt_state>`;
|
|
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);
|
|
359
771
|
}
|
|
360
772
|
|
|
361
|
-
const parts: string[] = ["<
|
|
773
|
+
const parts: string[] = ["<variables>"];
|
|
362
774
|
|
|
363
|
-
for (const scope of
|
|
775
|
+
for (const scope of scopes) {
|
|
364
776
|
const entries = Object.entries(grouped[scope]).sort(([a], [b]) => a.localeCompare(b));
|
|
365
777
|
if (entries.length === 0) continue;
|
|
366
778
|
parts.push(` <${scope}>`);
|
|
367
779
|
for (const [name, value] of entries) {
|
|
368
|
-
|
|
369
|
-
const attrs = [
|
|
370
|
-
`name=\"${escapeXml(name)}\"`,
|
|
371
|
-
`type=\"${escapeXml(metadata.type ?? inferStateValueType(value))}\"`,
|
|
372
|
-
];
|
|
373
|
-
if (metadata.description) attrs.push(`description=\"${escapeXml(metadata.description)}\"`);
|
|
374
|
-
if (metadata.agentWritable !== undefined) attrs.push(`agentWritable=\"${metadata.agentWritable ? "true" : "false"}\"`);
|
|
375
|
-
if (metadata.userWritable !== undefined) attrs.push(`userWritable=\"${metadata.userWritable ? "true" : "false"}\"`);
|
|
376
|
-
parts.push(` <var ${attrs.join(" ")}>${escapeXml(truncateValue(stateValueToPromptText(value), maxValueChars))}</var>`);
|
|
780
|
+
parts.push(` <var name=\"${escapeXml(name)}\">${escapeXml(variableValueToPromptText(value))}</var>`);
|
|
377
781
|
}
|
|
378
782
|
parts.push(` </${scope}>`);
|
|
379
783
|
}
|
|
380
784
|
|
|
381
|
-
parts.push("</
|
|
785
|
+
parts.push("</variables>");
|
|
382
786
|
return parts.join("\n");
|
|
383
787
|
}
|
|
384
788
|
|
|
385
|
-
function
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const scopes = new Set<PromptStateScope>();
|
|
390
|
-
if (options.includeStatic !== false) scopes.add("static");
|
|
391
|
-
if (options.includeSession !== false) scopes.add("session");
|
|
392
|
-
if (options.includeTurn !== false) scopes.add("turn");
|
|
393
|
-
return scopes;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function collectStaticVariables(stack: PromptStack): Record<string, PromptStateValue> {
|
|
397
|
-
return {
|
|
398
|
-
...collectDefaultVariables(stack, "static"),
|
|
399
|
-
...(stack.variables ?? {}),
|
|
400
|
-
};
|
|
401
|
-
}
|
|
789
|
+
function renderPlainVariables(
|
|
790
|
+
grouped: Record<PromptVariableScope, Record<string, PromptVariableValue>>,
|
|
791
|
+
): string {
|
|
792
|
+
const parts: string[] = ["Variables:"];
|
|
402
793
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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)));
|
|
800
|
+
}
|
|
408
801
|
}
|
|
409
|
-
return values;
|
|
410
|
-
}
|
|
411
802
|
|
|
412
|
-
|
|
413
|
-
if (includePatterns.length > 0 && !includePatterns.some((pattern) => matchesNamespace(name, pattern))) return false;
|
|
414
|
-
if (excludePatterns.some((pattern) => matchesNamespace(name, pattern))) return false;
|
|
415
|
-
return true;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function matchesNamespace(name: string, pattern: string): boolean {
|
|
419
|
-
if (!pattern) return false;
|
|
420
|
-
if (pattern.endsWith("*")) return name.startsWith(pattern.slice(0, -1));
|
|
421
|
-
return name === pattern;
|
|
803
|
+
return parts.join("\n");
|
|
422
804
|
}
|
|
423
805
|
|
|
424
|
-
function
|
|
425
|
-
|
|
426
|
-
description?: string;
|
|
427
|
-
agentWritable?: boolean;
|
|
428
|
-
userWritable?: boolean;
|
|
429
|
-
} {
|
|
430
|
-
const definition = stack.state?.definitions?.[name];
|
|
431
|
-
if (!definition) return {};
|
|
432
|
-
return {
|
|
433
|
-
type: definition.type,
|
|
434
|
-
description: definition.description,
|
|
435
|
-
agentWritable: definition.agentWritable,
|
|
436
|
-
userWritable: definition.userWritable,
|
|
437
|
-
};
|
|
806
|
+
function plainBullet(label: string, value: string): string {
|
|
807
|
+
return `- ${label}: ${plainContinuation(value, " ")}`;
|
|
438
808
|
}
|
|
439
809
|
|
|
440
|
-
function
|
|
441
|
-
return
|
|
810
|
+
function plainContinuation(value: string, indent: string): string {
|
|
811
|
+
return value.split("\n").map((line, index) => index === 0 ? line : `${indent}${line}`).join("\n");
|
|
442
812
|
}
|
|
443
813
|
|
|
444
|
-
function
|
|
445
|
-
return value
|
|
814
|
+
function indentPlainBlock(value: string, indent: string): string {
|
|
815
|
+
return value.split("\n").map((line) => `${indent}${line}`).join("\n");
|
|
446
816
|
}
|
|
447
817
|
|
|
448
|
-
function
|
|
449
|
-
|
|
450
|
-
if (
|
|
451
|
-
|
|
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;
|
|
452
824
|
}
|
|
453
825
|
|
|
454
|
-
function
|
|
455
|
-
|
|
456
|
-
return `${value.slice(0, maxChars)}\n[truncated]`;
|
|
826
|
+
function collectStaticVariables(stack: PromptStack): Record<string, PromptVariableValue> {
|
|
827
|
+
return { ...(stack.variables ?? {}) };
|
|
457
828
|
}
|
|
458
829
|
|
|
459
|
-
function
|
|
830
|
+
function variableValueToPromptText(value: PromptVariableValue): string {
|
|
460
831
|
if (typeof value === "string") return value;
|
|
461
832
|
return JSON.stringify(value, null, 2);
|
|
462
833
|
}
|
|
463
834
|
|
|
464
|
-
function
|
|
835
|
+
function variableValueToMacroText(value: PromptVariableValue | undefined): string {
|
|
465
836
|
if (value === undefined) return "";
|
|
466
837
|
if (typeof value === "string") return value;
|
|
467
838
|
return JSON.stringify(value);
|
|
@@ -506,9 +877,9 @@ function expandMacros(
|
|
|
506
877
|
unknown.add(expression);
|
|
507
878
|
return full;
|
|
508
879
|
}
|
|
509
|
-
if (command === "getturnvar") return
|
|
510
|
-
if (command === "getsessionvar") return
|
|
511
|
-
return
|
|
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));
|
|
512
883
|
}
|
|
513
884
|
|
|
514
885
|
if (command === "clearvar" || command === "clearturnvar" || command === "clearsessionvar") {
|
|
@@ -523,11 +894,11 @@ function expandMacros(
|
|
|
523
894
|
return "";
|
|
524
895
|
}
|
|
525
896
|
|
|
526
|
-
const dynamicValue = getBuiltinMacro(command, runtime);
|
|
897
|
+
const dynamicValue = getBuiltinMacro(command, stack, runtime);
|
|
527
898
|
if (dynamicValue !== undefined) return dynamicValue;
|
|
528
899
|
|
|
529
900
|
const variableValue = getRuntimeVariable(runtime, stack, command);
|
|
530
|
-
if (variableValue !== undefined) return
|
|
901
|
+
if (variableValue !== undefined) return variableValueToMacroText(variableValue);
|
|
531
902
|
|
|
532
903
|
unknown.add(expression);
|
|
533
904
|
return full;
|
|
@@ -545,7 +916,7 @@ function expandMacros(
|
|
|
545
916
|
return result;
|
|
546
917
|
}
|
|
547
918
|
|
|
548
|
-
function getBuiltinMacro(name: string, runtime: PromptRuntime): string | undefined {
|
|
919
|
+
function getBuiltinMacro(name: string, stack: PromptStack, runtime: PromptRuntime): string | undefined {
|
|
549
920
|
switch (name) {
|
|
550
921
|
case "cwd":
|
|
551
922
|
return runtime.options.cwd;
|
|
@@ -557,7 +928,7 @@ function getBuiltinMacro(name: string, runtime: PromptRuntime): string | undefin
|
|
|
557
928
|
return runtime.latestUserMessage ?? "";
|
|
558
929
|
case "selectedTools":
|
|
559
930
|
case "tools":
|
|
560
|
-
return (runtime
|
|
931
|
+
return scopedToolNames(stack, runtime).join(", ");
|
|
561
932
|
case "activeModel": {
|
|
562
933
|
const model = runtime.ctx?.model;
|
|
563
934
|
return model ? `${model.provider}/${model.id}` : "";
|
|
@@ -567,7 +938,7 @@ function getBuiltinMacro(name: string, runtime: PromptRuntime): string | undefin
|
|
|
567
938
|
}
|
|
568
939
|
}
|
|
569
940
|
|
|
570
|
-
function getRuntimeVariable(runtime: PromptRuntime, stack: PromptStack, name: string):
|
|
941
|
+
function getRuntimeVariable(runtime: PromptRuntime, stack: PromptStack, name: string): PromptVariableValue | undefined {
|
|
571
942
|
if (runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.turn, name)) return runtime.variables.turn[name];
|
|
572
943
|
if (runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.session, name)) return runtime.variables.session[name];
|
|
573
944
|
const staticVariables = collectStaticVariables(stack);
|