@zihanw/pi-forge 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +233 -105
  2. package/README.zh-CN.md +215 -104
  3. package/dist/compiler.d.ts +10 -0
  4. package/dist/compiler.d.ts.map +1 -0
  5. package/dist/compiler.js +490 -0
  6. package/dist/compiler.js.map +1 -0
  7. package/dist/extension-registry.d.ts +25 -0
  8. package/dist/extension-registry.d.ts.map +1 -0
  9. package/dist/extension-registry.js +6 -0
  10. package/dist/extension-registry.js.map +1 -0
  11. package/dist/index.d.ts +7 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +364 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/lifecycle.d.ts +14 -0
  16. package/dist/lifecycle.d.ts.map +1 -0
  17. package/dist/lifecycle.js +139 -0
  18. package/dist/lifecycle.js.map +1 -0
  19. package/dist/loader.d.ts +8 -0
  20. package/dist/loader.d.ts.map +1 -0
  21. package/dist/loader.js +345 -0
  22. package/dist/loader.js.map +1 -0
  23. package/dist/macro-engine.d.ts +23 -0
  24. package/dist/macro-engine.d.ts.map +1 -0
  25. package/dist/macro-engine.js +262 -0
  26. package/dist/macro-engine.js.map +1 -0
  27. package/dist/payload-capture.d.ts +15 -0
  28. package/dist/payload-capture.d.ts.map +1 -0
  29. package/dist/payload-capture.js +86 -0
  30. package/dist/payload-capture.js.map +1 -0
  31. package/dist/payload-command.d.ts +10 -0
  32. package/dist/payload-command.d.ts.map +1 -0
  33. package/dist/payload-command.js +112 -0
  34. package/dist/payload-command.js.map +1 -0
  35. package/dist/policy.d.ts +6 -0
  36. package/dist/policy.d.ts.map +1 -0
  37. package/dist/policy.js +38 -0
  38. package/dist/policy.js.map +1 -0
  39. package/dist/preset-command.d.ts +14 -0
  40. package/dist/preset-command.d.ts.map +1 -0
  41. package/dist/preset-command.js +223 -0
  42. package/dist/preset-command.js.map +1 -0
  43. package/dist/preview.d.ts +12 -0
  44. package/dist/preview.d.ts.map +1 -0
  45. package/dist/preview.js +179 -0
  46. package/dist/preview.js.map +1 -0
  47. package/dist/regex.d.ts +7 -0
  48. package/dist/regex.d.ts.map +1 -0
  49. package/dist/regex.js +432 -0
  50. package/dist/regex.js.map +1 -0
  51. package/dist/render-helpers.d.ts +44 -0
  52. package/dist/render-helpers.d.ts.map +1 -0
  53. package/dist/render-helpers.js +128 -0
  54. package/dist/render-helpers.js.map +1 -0
  55. package/dist/runtime-state.d.ts +24 -0
  56. package/dist/runtime-state.d.ts.map +1 -0
  57. package/dist/runtime-state.js +13 -0
  58. package/dist/runtime-state.js.map +1 -0
  59. package/dist/sillytavern-importer/items.d.ts +3 -0
  60. package/dist/sillytavern-importer/items.d.ts.map +1 -0
  61. package/dist/sillytavern-importer/items.js +88 -0
  62. package/dist/sillytavern-importer/items.js.map +1 -0
  63. package/dist/sillytavern-importer/macros.d.ts +15 -0
  64. package/dist/sillytavern-importer/macros.d.ts.map +1 -0
  65. package/dist/sillytavern-importer/macros.js +141 -0
  66. package/dist/sillytavern-importer/macros.js.map +1 -0
  67. package/dist/sillytavern-importer/prompt-order.d.ts +6 -0
  68. package/dist/sillytavern-importer/prompt-order.d.ts.map +1 -0
  69. package/dist/sillytavern-importer/prompt-order.js +38 -0
  70. package/dist/sillytavern-importer/prompt-order.js.map +1 -0
  71. package/dist/sillytavern-importer/regex.d.ts +3 -0
  72. package/dist/sillytavern-importer/regex.d.ts.map +1 -0
  73. package/dist/sillytavern-importer/regex.js +275 -0
  74. package/dist/sillytavern-importer/regex.js.map +1 -0
  75. package/dist/sillytavern-importer/report.d.ts +21 -0
  76. package/dist/sillytavern-importer/report.d.ts.map +1 -0
  77. package/dist/sillytavern-importer/report.js +166 -0
  78. package/dist/sillytavern-importer/report.js.map +1 -0
  79. package/dist/sillytavern-importer/types.d.ts +106 -0
  80. package/dist/sillytavern-importer/types.d.ts.map +1 -0
  81. package/dist/sillytavern-importer/types.js +2 -0
  82. package/dist/sillytavern-importer/types.js.map +1 -0
  83. package/dist/sillytavern-importer.d.ts +5 -0
  84. package/dist/sillytavern-importer.d.ts.map +1 -0
  85. package/dist/sillytavern-importer.js +117 -0
  86. package/dist/sillytavern-importer.js.map +1 -0
  87. package/dist/slot-renderers.d.ts +25 -0
  88. package/dist/slot-renderers.d.ts.map +1 -0
  89. package/dist/slot-renderers.js +334 -0
  90. package/dist/slot-renderers.js.map +1 -0
  91. package/dist/stack-migration.d.ts +29 -0
  92. package/dist/stack-migration.d.ts.map +1 -0
  93. package/dist/stack-migration.js +126 -0
  94. package/dist/stack-migration.js.map +1 -0
  95. package/dist/storage.d.ts +6 -0
  96. package/dist/storage.d.ts.map +1 -0
  97. package/dist/storage.js +23 -0
  98. package/dist/storage.js.map +1 -0
  99. package/dist/types.d.ts +173 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +2 -0
  102. package/dist/types.js.map +1 -0
  103. package/dist/web-editor/index.d.ts +3 -0
  104. package/dist/web-editor/index.d.ts.map +1 -0
  105. package/dist/web-editor/index.js +2 -0
  106. package/dist/web-editor/index.js.map +1 -0
  107. package/dist/web-editor/page.d.ts +2 -0
  108. package/dist/web-editor/page.d.ts.map +1 -0
  109. package/dist/web-editor/page.js +3331 -0
  110. package/dist/web-editor/page.js.map +1 -0
  111. package/dist/web-editor/server.d.ts +4 -0
  112. package/dist/web-editor/server.d.ts.map +1 -0
  113. package/dist/web-editor/server.js +263 -0
  114. package/dist/web-editor/server.js.map +1 -0
  115. package/dist/web-editor/types.d.ts +125 -0
  116. package/dist/web-editor/types.d.ts.map +1 -0
  117. package/dist/web-editor/types.js +2 -0
  118. package/dist/web-editor/types.js.map +1 -0
  119. package/dist/web-host.d.ts +29 -0
  120. package/dist/web-host.d.ts.map +1 -0
  121. package/dist/web-host.js +174 -0
  122. package/dist/web-host.js.map +1 -0
  123. package/examples/custom-system-status-extension/README.md +78 -0
  124. package/examples/custom-system-status-extension/index.ts +176 -0
  125. package/examples/custom-system-status-extension/prompt-stack.json +39 -0
  126. package/examples/default-prompt-stack.json +52 -93
  127. package/examples/image-reader-prompt-stack.json +124 -0
  128. package/examples/reviewer-prompt-stack.json +115 -0
  129. package/examples/sillytavern-dm-writer-prompt-stack.json +190 -0
  130. package/package.json +30 -9
  131. package/src/compiler.ts +321 -420
  132. package/src/extension-registry.ts +33 -0
  133. package/src/index.ts +310 -1110
  134. package/src/lifecycle.ts +171 -0
  135. package/src/loader.ts +141 -56
  136. package/src/macro-engine.ts +358 -0
  137. package/src/payload-capture.ts +85 -0
  138. package/src/payload-command.ts +138 -0
  139. package/src/policy.ts +42 -0
  140. package/src/preset-command.ts +271 -0
  141. package/src/preview.ts +226 -0
  142. package/src/regex.ts +500 -0
  143. package/src/render-helpers.ts +169 -0
  144. package/src/runtime-state.ts +36 -0
  145. package/src/sillytavern-importer/items.ts +98 -0
  146. package/src/sillytavern-importer/macros.ts +159 -0
  147. package/src/sillytavern-importer/prompt-order.ts +54 -0
  148. package/src/sillytavern-importer/regex.ts +270 -0
  149. package/src/sillytavern-importer/report.ts +202 -0
  150. package/src/sillytavern-importer/types.ts +120 -0
  151. package/src/sillytavern-importer.ts +57 -482
  152. package/src/slot-renderers.ts +414 -0
  153. package/src/stack-migration.ts +159 -0
  154. package/src/storage.ts +28 -0
  155. package/src/types.ts +83 -52
  156. package/src/web-editor/index.ts +2 -0
  157. package/src/web-editor/page.ts +3330 -0
  158. package/src/web-editor/server.ts +294 -0
  159. package/src/web-editor/types.ts +98 -0
  160. package/src/web-host.ts +232 -0
  161. package/src/web-editor.ts +0 -1491
package/src/compiler.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
2
  import type {
3
+ CompileMessageSource,
3
4
  CompileMessagesResult,
4
5
  CompileSystemPromptResult,
5
6
  PromptRuntime,
@@ -8,11 +9,12 @@ import type {
8
9
  PromptStackItem,
9
10
  PromptStackRole,
10
11
  PromptStackSlotItem,
11
- PromptStateScope,
12
- PromptStateValue,
12
+ PromptVariableValue,
13
13
  PromptVariableStore,
14
14
  } from "./types.ts";
15
- import { SUPPORTED_SLOTS } from "./types.ts";
15
+ import { applyRegexRulesToMessages, applyRegexRulesToString } from "./regex.ts";
16
+ import { expandMacros } from "./macro-engine.ts";
17
+ import { renderSlotText } from "./slot-renderers.ts";
16
18
 
17
19
  const ZERO_USAGE = {
18
20
  input: 0,
@@ -23,7 +25,9 @@ const ZERO_USAGE = {
23
25
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
24
26
  };
25
27
 
26
- export function createPromptVariableStore(sessionVariables: Record<string, PromptStateValue> = {}): PromptVariableStore {
28
+ const SUMMARY_ROLES = new Set(["branchSummary", "compactionSummary"]);
29
+
30
+ export function createPromptVariableStore(sessionVariables: Record<string, PromptVariableValue> = {}): PromptVariableStore {
27
31
  return { turn: {}, session: { ...sessionVariables }, sessionDirty: false };
28
32
  }
29
33
 
@@ -65,20 +69,20 @@ export function compileSystemPrompt(
65
69
  const compiled = parts.join("\n\n");
66
70
  const mode = stack.mode ?? "replace";
67
71
 
72
+ let systemPrompt: string;
68
73
  if (!compiled) {
69
74
  diagnostics.push({ level: "warning", message: "Compiled system prompt is empty; preserving base system prompt." });
70
- return { systemPrompt: baseSystemPrompt, diagnostics };
71
- }
72
-
73
- if (mode === "append") {
74
- return { systemPrompt: compiled ? `${baseSystemPrompt}\n\n${compiled}` : baseSystemPrompt, diagnostics };
75
- }
76
-
77
- if (mode === "prepend") {
78
- return { systemPrompt: compiled ? `${compiled}\n\n${baseSystemPrompt}` : baseSystemPrompt, diagnostics };
75
+ systemPrompt = baseSystemPrompt;
76
+ } else if (mode === "append") {
77
+ systemPrompt = compiled ? `${baseSystemPrompt}\n\n${compiled}` : baseSystemPrompt;
78
+ } else if (mode === "prepend") {
79
+ systemPrompt = compiled ? `${compiled}\n\n${baseSystemPrompt}` : baseSystemPrompt;
80
+ } else {
81
+ systemPrompt = compiled;
79
82
  }
80
83
 
81
- return { systemPrompt: compiled, diagnostics };
84
+ systemPrompt = applyRegexRulesToString(stack, systemPrompt, "compiled", "system", diagnostics);
85
+ return { systemPrompt, diagnostics };
82
86
  }
83
87
 
84
88
  export function compileMessages(
@@ -87,7 +91,8 @@ export function compileMessages(
87
91
  originalMessages: AgentMessage[],
88
92
  ): CompileMessagesResult {
89
93
  const diagnostics: PromptStackDiagnostic[] = [];
90
- const messages: AgentMessage[] = [];
94
+ let messages: AgentMessage[] = [];
95
+ let messageSources: CompileMessageSource[] = [];
91
96
  let insertedHistory = false;
92
97
 
93
98
  for (const item of enabledItems(stack)) {
@@ -100,7 +105,10 @@ export function compileMessages(
100
105
  });
101
106
  continue;
102
107
  }
103
- messages.push(...getChatHistoryMessages(originalMessages, item));
108
+ const historyMessages = getChatHistoryMessages(originalMessages, item, diagnostics);
109
+ const transformedHistory = applyRegexRulesToMessages(stack, historyMessages, "history", diagnostics);
110
+ messages.push(...transformedHistory);
111
+ messageSources.push(...chatHistoryMessageSources(transformedHistory, item));
104
112
  insertedHistory = true;
105
113
  continue;
106
114
  }
@@ -112,14 +120,18 @@ export function compileMessages(
112
120
  : renderSlotText(item, stack, runtime, diagnostics);
113
121
 
114
122
  if (!content.trim()) continue;
115
- messages.push(createSyntheticMessage(item.role, content, stack, runtime));
123
+ const message = createSyntheticMessage(item.role, content, stack, runtime);
124
+ messages.push(message);
125
+ messageSources.push(stackItemMessageSource(message, item));
116
126
  }
117
127
 
118
128
  if (!insertedHistory) {
119
129
  messages.push(...originalMessages);
130
+ messageSources.push(...implicitHistoryMessageSources(originalMessages));
120
131
  }
121
132
 
122
- return { messages, diagnostics };
133
+ messages = applyRegexRulesToMessages(stack, messages, "compiled", diagnostics);
134
+ return { messages, messageSources, diagnostics };
123
135
  }
124
136
 
125
137
  export function getLatestUserMessage(messages: AgentMessage[]): string | undefined {
@@ -133,470 +145,382 @@ export function getLatestUserMessage(messages: AgentMessage[]): string | undefin
133
145
  return undefined;
134
146
  }
135
147
 
136
- export function renderPreviewMessages(messages: AgentMessage[], maxChars = 8000): string {
137
- let text = "";
138
- for (const message of messages) {
139
- const role = message.role;
140
- text += `\n--- ${role} ---\n`;
141
- text += contentToText((message as { content?: unknown; summary?: string; command?: string; output?: string }).content);
142
- if (role === "bashExecution") {
143
- const bash = message as { command?: string; output?: string };
144
- text += `Ran ${bash.command ?? ""}\n${bash.output ?? ""}`;
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]`;
148
+ export function agentMessageToPreviewText(message: AgentMessage): string {
149
+ let text = contentToText((message as { content?: unknown; summary?: string; command?: string; output?: string }).content);
150
+ const role = message.role;
151
+ if (role === "bashExecution") {
152
+ const bash = message as { command?: string; output?: string };
153
+ text += `Ran ${bash.command ?? ""}\n${bash.output ?? ""}`;
151
154
  }
152
- return text.trimStart();
153
- }
154
-
155
- function getChatHistoryMessages(messages: AgentMessage[], item: PromptStackSlotItem): AgentMessage[] {
156
- if (item.options?.includeLastUserMessage !== false) return messages;
157
-
158
- const lastUserIndex = findLastUserMessageIndex(messages);
159
- if (lastUserIndex === -1) return messages;
160
-
161
- return messages.filter((_message, index) => index !== lastUserIndex);
162
- }
163
-
164
- function findLastUserMessageIndex(messages: AgentMessage[]): number {
165
- for (let i = messages.length - 1; i >= 0; i--) {
166
- if (messages[i].role === "user") return i;
155
+ if (role === "branchSummary" || role === "compactionSummary") {
156
+ text += (message as { summary?: string }).summary ?? "";
167
157
  }
168
- return -1;
169
- }
170
-
171
- function enabledItems(stack: PromptStack): PromptStackItem[] {
172
- return stack.items.filter((item) => item.enabled !== false);
158
+ return text;
173
159
  }
174
160
 
175
- function renderSlotText(
161
+ function getChatHistoryMessages(
162
+ messages: AgentMessage[],
176
163
  item: PromptStackSlotItem,
177
- stack: PromptStack,
178
- runtime: PromptRuntime,
179
164
  diagnostics: PromptStackDiagnostic[],
180
- ): string {
181
- const slot = item.slot;
165
+ ): AgentMessage[] {
166
+ let result = messages;
167
+ const options = item.options ?? {};
168
+ let shouldRepairToolPairs = false;
182
169
 
183
- if (!SUPPORTED_SLOTS.has(slot as any)) {
184
- diagnostics.push({ level: "warning", message: `Unsupported slot: ${slot}`, itemId: item.id });
185
- return "";
170
+ if (options.includeLastUserMessage === false) {
171
+ const lastUserIndex = findLastUserMessageIndex(result);
172
+ if (lastUserIndex !== -1) result = result.filter((_message, index) => index !== lastUserIndex);
186
173
  }
187
174
 
188
- switch (slot) {
189
- case "tools":
190
- return renderTools(runtime);
191
- case "tool-guidelines":
192
- return renderToolGuidelines(runtime);
193
- case "skills":
194
- return renderSkills(runtime);
195
- case "project-context":
196
- return renderProjectContext(runtime);
197
- case "append-system-prompt":
198
- return runtime.options.appendSystemPrompt ?? "";
199
- case "date":
200
- return `Current date: ${formatDate(runtime.now)}`;
201
- case "cwd":
202
- return `Current working directory: ${runtime.options.cwd.replace(/\\/g, "/")}`;
203
- case "date-cwd":
204
- return [`Current date: ${formatDate(runtime.now)}`, `Current working directory: ${runtime.options.cwd.replace(/\\/g, "/")}`].join("\n");
205
- case "active-model": {
206
- const model = runtime.ctx?.model;
207
- return model ? `Current model: ${model.provider}/${model.id}` : "Current model: (none)";
208
- }
209
- case "pi-docs":
210
- return renderPiDocsGuidance();
211
- case "variables":
212
- return renderVariables(item, stack, runtime);
213
- case "chat-history":
214
- return "";
215
- default:
216
- return "";
175
+ if (options.includeSummaries === false) {
176
+ const next = result.filter((message) => !isSummaryMessage(message));
177
+ addHistoryFilterDiagnostic(diagnostics, item.id, "summary", result.length, next.length);
178
+ result = next;
217
179
  }
218
- }
219
180
 
220
- function renderTools(runtime: PromptRuntime): string {
221
- const tools = runtime.options.selectedTools ?? [];
222
- const snippets = runtime.options.toolSnippets ?? {};
223
- const lines = ["<available_tools>"];
181
+ if (isStringArray(options.roles) && options.roles.length > 0) {
182
+ const allowedRoles = new Set(options.roles);
183
+ const next = result.filter((message) => allowedRoles.has(messageRole(message)));
184
+ addHistoryFilterDiagnostic(diagnostics, item.id, "role", result.length, next.length);
185
+ result = next;
186
+ shouldRepairToolPairs = true;
187
+ }
224
188
 
225
- if (tools.length === 0) {
226
- lines.push(" (none)");
227
- } else {
228
- for (const name of tools) {
229
- const snippet = snippets[name] ?? "No prompt snippet provided.";
230
- lines.push(` <tool name=\"${escapeXml(name)}\">${escapeXml(snippet)}</tool>`);
231
- }
189
+ if (options.toolMode === "drop") {
190
+ result = dropToolHistory(result, diagnostics, item.id);
191
+ } else if (options.toolMode !== undefined && options.toolMode !== "keep") {
192
+ diagnostics.push({ level: "warning", message: `Unsupported chat-history toolMode: ${String(options.toolMode)}.`, itemId: item.id });
232
193
  }
233
194
 
234
- lines.push("</available_tools>");
235
- return lines.join("\n");
236
- }
195
+ if (options.stripAssistantThinking === true) {
196
+ result = stripAssistantThinkingFromHistory(result, diagnostics, item.id);
197
+ }
237
198
 
238
- function renderToolGuidelines(runtime: PromptRuntime): string {
239
- const tools = runtime.options.selectedTools ?? [];
240
- const guidelines: string[] = [];
241
- const seen = new Set<string>();
242
- const add = (line: string) => {
243
- const normalized = line.trim();
244
- if (!normalized || seen.has(normalized)) return;
245
- seen.add(normalized);
246
- guidelines.push(normalized);
247
- };
199
+ const limited = limitChatHistory(result, options, diagnostics, item.id);
200
+ if (limited !== result) {
201
+ result = limited;
202
+ shouldRepairToolPairs = true;
203
+ }
248
204
 
249
- if (tools.includes("bash") && !tools.includes("grep") && !tools.includes("find") && !tools.includes("ls")) {
250
- add("Use bash for file operations like ls, rg, find.");
205
+ if (shouldRepairToolPairs && options.toolMode !== "drop") {
206
+ result = repairToolHistory(result, diagnostics, item.id);
251
207
  }
252
208
 
253
- for (const guideline of runtime.options.promptGuidelines ?? []) add(guideline);
209
+ return result;
210
+ }
254
211
 
255
- if (guidelines.length === 0) return "";
256
- return ["<tool_guidelines>", ...guidelines.map((line) => `- ${line}`), "</tool_guidelines>"].join("\n");
212
+ function findLastUserMessageIndex(messages: AgentMessage[]): number {
213
+ for (let i = messages.length - 1; i >= 0; i--) {
214
+ if (messages[i].role === "user") return i;
215
+ }
216
+ return -1;
257
217
  }
258
218
 
259
- function renderSkills(runtime: PromptRuntime): string {
260
- const skills = (runtime.options.skills ?? []).filter((skill) => !skill.disableModelInvocation);
261
- if (skills.length === 0) return "";
219
+ function limitChatHistory(
220
+ messages: AgentMessage[],
221
+ options: NonNullable<PromptStackSlotItem["options"]>,
222
+ diagnostics: PromptStackDiagnostic[],
223
+ itemId: string,
224
+ ): AgentMessage[] {
225
+ let result = messages;
226
+ const maxMessages = positiveIntegerOption(options.maxMessages);
227
+ const maxChars = positiveIntegerOption(options.maxChars);
262
228
 
263
- const lines = [
264
- "The following skills provide specialized instructions for specific tasks.",
265
- "Use the read tool to load a skill's file when the task matches its description.",
266
- "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
- "",
268
- "<available_skills>",
269
- ];
229
+ if (maxMessages !== undefined && result.length > maxMessages) {
230
+ const next = result.slice(-maxMessages);
231
+ diagnostics.push({
232
+ level: "info",
233
+ message: `Trimmed chat history from ${result.length} to ${next.length} message(s) by maxMessages.`,
234
+ itemId,
235
+ });
236
+ result = next;
237
+ }
270
238
 
271
- for (const skill of skills) {
272
- lines.push(" <skill>");
273
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
274
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
275
- lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
276
- lines.push(" </skill>");
239
+ if (maxChars !== undefined) {
240
+ const next = takeRecentMessagesWithinChars(result, maxChars);
241
+ if (next.length < result.length) {
242
+ diagnostics.push({
243
+ level: "info",
244
+ message: `Trimmed chat history from ${result.length} to ${next.length} message(s) by maxChars.`,
245
+ itemId,
246
+ });
247
+ result = next;
248
+ }
277
249
  }
278
250
 
279
- lines.push("</available_skills>");
280
- return lines.join("\n");
251
+ return result;
281
252
  }
282
253
 
283
- function renderProjectContext(runtime: PromptRuntime): string {
284
- const contextFiles = runtime.options.contextFiles ?? [];
285
- if (contextFiles.length === 0) return "";
286
-
287
- const lines = ["<project_context>", "", "Project-specific instructions and guidelines:", ""];
288
- for (const file of contextFiles) {
289
- lines.push(`<project_instructions path=\"${escapeXml(file.path)}\">`);
290
- lines.push(file.content);
291
- lines.push("</project_instructions>", "");
254
+ function takeRecentMessagesWithinChars(messages: AgentMessage[], maxChars: number): AgentMessage[] {
255
+ const selected: AgentMessage[] = [];
256
+ let chars = 0;
257
+ for (let index = messages.length - 1; index >= 0; index--) {
258
+ const message = messages[index];
259
+ const messageChars = agentMessageToPreviewText(message).length;
260
+ if (selected.length > 0 && chars + messageChars > maxChars) break;
261
+ selected.push(message);
262
+ chars += messageChars;
292
263
  }
293
- lines.push("</project_context>");
294
- return lines.join("\n");
264
+ return selected.reverse();
295
265
  }
296
266
 
297
- function renderPiDocsGuidance(): string {
298
- return [
299
- "Pi documentation guidance:",
300
- "- Read Pi documentation only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI.",
301
- "- Resolve docs/... against Pi's installed documentation directory and examples/... against Pi's installed examples directory.",
302
- ].join("\n");
267
+ function addHistoryFilterDiagnostic(
268
+ diagnostics: PromptStackDiagnostic[],
269
+ itemId: string,
270
+ filter: string,
271
+ before: number,
272
+ after: number,
273
+ ): void {
274
+ if (before === after) return;
275
+ diagnostics.push({
276
+ level: "info",
277
+ message: `Filtered ${before - after} chat-history message(s) by ${filter}.`,
278
+ itemId,
279
+ });
303
280
  }
304
281
 
305
- function renderVariables(
306
- item: PromptStackSlotItem,
307
- stack: PromptStack,
308
- runtime: PromptRuntime,
309
- ): string {
310
- const options = item.options ?? {};
311
- const scopes = selectedVariableScopes(options);
312
- const includeMetadata = options.includeMetadata === true;
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;
319
- const store = runtime.variables;
320
- const grouped: Record<PromptStateScope, Record<string, PromptStateValue>> = {
321
- static: {},
322
- session: {},
323
- turn: {},
324
- };
282
+ function dropToolHistory(
283
+ messages: AgentMessage[],
284
+ diagnostics: PromptStackDiagnostic[],
285
+ itemId: string,
286
+ ): AgentMessage[] {
287
+ let removedToolCalls = 0;
288
+ let droppedToolResults = 0;
289
+ let droppedEmptyMessages = 0;
290
+ let changed = false;
291
+ const result: AgentMessage[] = [];
325
292
 
326
- if (scopes.has("static")) {
327
- grouped.static = collectStaticVariables(stack);
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 ?? {}) };
293
+ for (const message of messages) {
294
+ if (isToolResultMessage(message)) {
295
+ droppedToolResults++;
296
+ changed = true;
297
+ continue;
298
+ }
299
+ const stripped = stripToolCallParts(message, () => false);
300
+ removedToolCalls += stripped.removedCalls;
301
+ if (stripped.message !== message) changed = true;
302
+ if (!stripped.message) {
303
+ droppedEmptyMessages++;
304
+ continue;
305
+ }
306
+ result.push(stripped.message);
334
307
  }
335
308
 
336
- for (const scope of ["static", "session", "turn"] as const) {
337
- grouped[scope] = Object.fromEntries(
338
- Object.entries(grouped[scope]).filter(([name]) => shouldRenderVariable(name, includeNamespaces, excludeNamespaces)),
339
- );
309
+ if (!changed) return messages;
310
+ diagnostics.push({
311
+ level: "info",
312
+ message: `Dropped tool history from chat-history: removed ${removedToolCalls} tool call(s), dropped ${droppedToolResults} tool result message(s)` +
313
+ (droppedEmptyMessages > 0 ? `, and dropped ${droppedEmptyMessages} empty message(s).` : "."),
314
+ itemId,
315
+ });
316
+ return result;
317
+ }
318
+
319
+ function repairToolHistory(
320
+ messages: AgentMessage[],
321
+ diagnostics: PromptStackDiagnostic[],
322
+ itemId: string,
323
+ ): AgentMessage[] {
324
+ const includedCallIds = new Set<string>();
325
+ const includedResultIds = new Set<string>();
326
+ for (const message of messages) {
327
+ for (const id of toolCallIdsForMessage(message)) includedCallIds.add(id);
328
+ const resultId = toolResultMessageId(message);
329
+ if (resultId) includedResultIds.add(resultId);
340
330
  }
341
331
 
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)));
332
+ let removedToolCalls = 0;
333
+ let droppedToolResults = 0;
334
+ let droppedEmptyMessages = 0;
335
+ let changed = false;
336
+ const result: AgentMessage[] = [];
337
+
338
+ for (const message of messages) {
339
+ if (isToolResultMessage(message)) {
340
+ const resultId = toolResultMessageId(message);
341
+ if (!resultId || !includedCallIds.has(resultId)) {
342
+ droppedToolResults++;
343
+ changed = true;
344
+ continue;
345
+ }
346
+ result.push(message);
347
+ continue;
357
348
  }
358
- return `<prompt_state format=\"json\">\n${escapeXml(JSON.stringify(payload, null, 2))}\n</prompt_state>`;
359
- }
360
349
 
361
- const parts: string[] = ["<prompt_state>"];
362
-
363
- for (const scope of ["static", "session", "turn"] as const) {
364
- const entries = Object.entries(grouped[scope]).sort(([a], [b]) => a.localeCompare(b));
365
- if (entries.length === 0) continue;
366
- parts.push(` <${scope}>`);
367
- for (const [name, value] of entries) {
368
- const metadata = includeMetadata ? metadataForVariable(stack, name) : {};
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>`);
350
+ const stripped = stripToolCallParts(message, (id) => !!id && includedResultIds.has(id));
351
+ removedToolCalls += stripped.removedCalls;
352
+ if (stripped.message !== message) changed = true;
353
+ if (!stripped.message) {
354
+ droppedEmptyMessages++;
355
+ continue;
377
356
  }
378
- parts.push(` </${scope}>`);
357
+ result.push(stripped.message);
379
358
  }
380
359
 
381
- parts.push("</prompt_state>");
382
- return parts.join("\n");
360
+ if (!changed) return messages;
361
+ diagnostics.push({
362
+ level: "info",
363
+ message: `Repaired tool history after chat-history filtering: removed ${removedToolCalls} dangling tool call(s), dropped ${droppedToolResults} dangling tool result message(s)` +
364
+ (droppedEmptyMessages > 0 ? `, and dropped ${droppedEmptyMessages} empty message(s).` : "."),
365
+ itemId,
366
+ });
367
+ return result;
368
+ }
369
+
370
+ function stripToolCallParts(
371
+ message: AgentMessage,
372
+ keep: (id: string | undefined) => boolean,
373
+ ): { message?: AgentMessage; removedCalls: number } {
374
+ const content = (message as { content?: unknown }).content;
375
+ if (!Array.isArray(content)) return { message, removedCalls: 0 };
376
+
377
+ const nextContent = content.filter((part) => !isToolCallContent(part) || keep(toolCallPartId(part)));
378
+ const removedCalls = content.length - nextContent.length;
379
+ if (removedCalls === 0) return { message, removedCalls: 0 };
380
+ if (nextContent.length === 0) return { removedCalls };
381
+ return { message: { ...message, content: nextContent } as AgentMessage, removedCalls };
383
382
  }
384
383
 
385
- function selectedVariableScopes(options: Record<string, unknown>): Set<PromptStateScope> {
386
- const explicit = normalizeStringArray(options.includeScopes).filter(isStateScope);
387
- if (explicit.length > 0) return new Set(explicit);
384
+ function toolCallIdsForMessage(message: AgentMessage): string[] {
385
+ const content = (message as { content?: unknown }).content;
386
+ if (!Array.isArray(content)) return [];
387
+ return content
388
+ .map((part) => toolCallPartId(part))
389
+ .filter((id): id is string => !!id);
390
+ }
388
391
 
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;
392
+ function isToolCallContent(value: unknown): boolean {
393
+ return isPlainObject(value) && value.type === "toolCall";
394
394
  }
395
395
 
396
- function collectStaticVariables(stack: PromptStack): Record<string, PromptStateValue> {
397
- return {
398
- ...collectDefaultVariables(stack, "static"),
399
- ...(stack.variables ?? {}),
400
- };
396
+ function toolCallPartId(value: unknown): string | undefined {
397
+ if (!isPlainObject(value)) return undefined;
398
+ return firstString(value.id, value.toolCallId, value.callId);
401
399
  }
402
400
 
403
- function collectDefaultVariables(stack: PromptStack, scope: PromptStateScope): Record<string, PromptStateValue> {
404
- const values: Record<string, PromptStateValue> = {};
405
- for (const [name, definition] of Object.entries(stack.state?.definitions ?? {})) {
406
- if ((definition.scope ?? "session") !== scope) continue;
407
- if (definition.default !== undefined) values[name] = definition.default;
408
- }
409
- return values;
401
+ function isToolResultMessage(message: AgentMessage): boolean {
402
+ return messageRole(message) === "toolResult";
410
403
  }
411
404
 
412
- function shouldRenderVariable(name: string, includePatterns: string[], excludePatterns: string[]): boolean {
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;
405
+ function toolResultMessageId(message: AgentMessage): string | undefined {
406
+ if (!isToolResultMessage(message)) return undefined;
407
+ const raw = message as unknown as Record<string, unknown>;
408
+ return firstString(raw.toolCallId, raw.id, raw.callId);
416
409
  }
417
410
 
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;
411
+ function isSummaryMessage(message: AgentMessage): boolean {
412
+ return SUMMARY_ROLES.has(messageRole(message));
422
413
  }
423
414
 
424
- function metadataForVariable(stack: PromptStack, name: string): {
425
- type?: string;
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
- };
415
+ function messageRole(message: AgentMessage): string {
416
+ return String((message as { role?: unknown }).role ?? "");
438
417
  }
439
418
 
440
- function normalizeStringArray(value: unknown): string[] {
441
- return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
419
+ function positiveIntegerOption(value: unknown): number | undefined {
420
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
442
421
  }
443
422
 
444
- function isStateScope(value: string): value is PromptStateScope {
445
- return value === "static" || value === "session" || value === "turn";
423
+ function firstString(...values: unknown[]): string | undefined {
424
+ for (const value of values) {
425
+ if (typeof value === "string" && value) return value;
426
+ }
427
+ return undefined;
446
428
  }
447
429
 
448
- function inferStateValueType(value: PromptStateValue): string {
449
- if (value === null) return "null";
450
- if (Array.isArray(value)) return "array";
451
- return typeof value;
430
+ function isStringArray(value: unknown): value is string[] {
431
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
452
432
  }
453
433
 
454
- function truncateValue(value: string, maxChars: number | undefined): string {
455
- if (!maxChars || value.length <= maxChars) return value;
456
- return `${value.slice(0, maxChars)}\n[truncated]`;
434
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
435
+ return !!value && typeof value === "object" && !Array.isArray(value);
457
436
  }
458
437
 
459
- function stateValueToPromptText(value: PromptStateValue): string {
460
- if (typeof value === "string") return value;
461
- return JSON.stringify(value, null, 2);
438
+ function chatHistoryMessageSources(messages: AgentMessage[], item: PromptStackSlotItem): CompileMessageSource[] {
439
+ return messages.map((message, index) => ({
440
+ kind: "chat-history",
441
+ itemId: item.id,
442
+ itemName: item.name,
443
+ slot: item.slot,
444
+ historyIndex: index + 1,
445
+ historyCount: messages.length,
446
+ role: String((message as { role?: unknown }).role ?? ""),
447
+ }));
462
448
  }
463
449
 
464
- function stateValueToMacroText(value: PromptStateValue | undefined): string {
465
- if (value === undefined) return "";
466
- if (typeof value === "string") return value;
467
- return JSON.stringify(value);
450
+ function implicitHistoryMessageSources(messages: AgentMessage[]): CompileMessageSource[] {
451
+ return messages.map((message, index) => ({
452
+ kind: "implicit-history",
453
+ itemName: "Conversation history",
454
+ slot: "chat-history",
455
+ historyIndex: index + 1,
456
+ historyCount: messages.length,
457
+ role: String((message as { role?: unknown }).role ?? ""),
458
+ }));
468
459
  }
469
460
 
470
- function expandMacros(
471
- text: string,
472
- stack: PromptStack,
473
- runtime: PromptRuntime,
461
+ function stackItemMessageSource(message: AgentMessage, item: PromptStackItem): CompileMessageSource {
462
+ return {
463
+ kind: "stack-item",
464
+ itemId: item.id,
465
+ itemName: item.name,
466
+ role: String((message as { role?: unknown }).role ?? ""),
467
+ };
468
+ }
469
+
470
+ function stripAssistantThinkingFromHistory(
471
+ messages: AgentMessage[],
474
472
  diagnostics: PromptStackDiagnostic[],
475
473
  itemId: string,
476
- ): string {
477
- const policy = stack.defaults?.unresolvedMacroPolicy ?? "warn";
478
- const unknown = new Set<string>();
479
-
480
- const result = text.replace(/\{\{([^{}]+)\}\}/g, (full, rawName: string) => {
481
- const expression = rawName.trim();
482
- const parts = expression.split("::");
483
- const command = parts[0]?.trim();
484
-
485
- if (!command) return full;
486
-
487
- // SillyTavern-style mutable variables. By default, setvar is turn-scoped and returns empty text.
488
- if (command === "setvar" || command === "setturnvar" || command === "setsessionvar") {
489
- const scoped = command === "setvar" && (parts[1] === "turn" || parts[1] === "session");
490
- const scope = command === "setsessionvar" || (scoped && parts[1] === "session") ? "session" : "turn";
491
- const nameIndex = scoped ? 2 : 1;
492
- const valueIndex = nameIndex + 1;
493
- const name = parts[nameIndex]?.trim();
494
- const value = parts.slice(valueIndex).join("::");
495
- if (!name) {
496
- unknown.add(expression);
497
- return full;
498
- }
499
- setRuntimeVariable(runtime, scope, name, value);
500
- return "";
501
- }
474
+ ): AgentMessage[] {
475
+ let strippedBlocks = 0;
476
+ let changedMessages = 0;
477
+ let droppedMessages = 0;
478
+ let changed = false;
479
+ const result: AgentMessage[] = [];
502
480
 
503
- if (command === "getvar" || command === "var" || command === "getturnvar" || command === "getsessionvar") {
504
- const name = parts[1]?.trim();
505
- if (!name) {
506
- unknown.add(expression);
507
- return full;
508
- }
509
- if (command === "getturnvar") return stateValueToMacroText(runtime.variables?.turn[name]);
510
- if (command === "getsessionvar") return stateValueToMacroText(runtime.variables?.session[name]);
511
- return stateValueToMacroText(getRuntimeVariable(runtime, stack, name));
481
+ for (const message of messages) {
482
+ const stripped = stripAssistantThinkingFromMessage(message);
483
+ if (stripped.message !== message) {
484
+ changed = true;
485
+ strippedBlocks += stripped.removedBlocks;
486
+ changedMessages++;
512
487
  }
513
-
514
- if (command === "clearvar" || command === "clearturnvar" || command === "clearsessionvar") {
515
- const scoped = command === "clearvar" && (parts[1] === "turn" || parts[1] === "session");
516
- const scope = command === "clearsessionvar" || (scoped && parts[1] === "session") ? "session" : "turn";
517
- const name = parts[scoped ? 2 : 1]?.trim();
518
- if (!name) {
519
- unknown.add(expression);
520
- return full;
521
- }
522
- clearRuntimeVariable(runtime, scope, name);
523
- return "";
488
+ if (!stripped.message) {
489
+ droppedMessages++;
490
+ continue;
524
491
  }
492
+ result.push(stripped.message);
493
+ }
525
494
 
526
- const dynamicValue = getBuiltinMacro(command, runtime);
527
- if (dynamicValue !== undefined) return dynamicValue;
528
-
529
- const variableValue = getRuntimeVariable(runtime, stack, command);
530
- if (variableValue !== undefined) return stateValueToMacroText(variableValue);
495
+ if (!changed) return messages;
531
496
 
532
- unknown.add(expression);
533
- return full;
497
+ diagnostics.push({
498
+ level: "info",
499
+ message: `Stripped ${strippedBlocks} assistant thinking block(s) from ${changedMessages} chat-history message(s)` +
500
+ (droppedMessages > 0 ? ` and dropped ${droppedMessages} empty assistant message(s).` : "."),
501
+ itemId,
534
502
  });
535
-
536
- for (const name of unknown) {
537
- if (policy === "keep") continue;
538
- diagnostics.push({
539
- level: policy === "error" ? "error" : "warning",
540
- message: `Unresolved macro: {{${name}}}`,
541
- itemId,
542
- });
543
- }
544
-
545
503
  return result;
546
504
  }
547
505
 
548
- function getBuiltinMacro(name: string, runtime: PromptRuntime): string | undefined {
549
- switch (name) {
550
- case "cwd":
551
- return runtime.options.cwd;
552
- case "date":
553
- return formatDate(runtime.now);
554
- case "time":
555
- return formatTime(runtime.now);
556
- case "lastUserMessage":
557
- return runtime.latestUserMessage ?? "";
558
- case "selectedTools":
559
- case "tools":
560
- return (runtime.options.selectedTools ?? []).join(", ");
561
- case "activeModel": {
562
- const model = runtime.ctx?.model;
563
- return model ? `${model.provider}/${model.id}` : "";
564
- }
565
- default:
566
- return undefined;
567
- }
568
- }
506
+ function stripAssistantThinkingFromMessage(message: AgentMessage): { message?: AgentMessage; removedBlocks: number } {
507
+ if (message.role !== "assistant") return { message, removedBlocks: 0 };
508
+ const content = (message as { content?: unknown }).content;
509
+ if (!Array.isArray(content)) return { message, removedBlocks: 0 };
569
510
 
570
- function getRuntimeVariable(runtime: PromptRuntime, stack: PromptStack, name: string): PromptStateValue | undefined {
571
- if (runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.turn, name)) return runtime.variables.turn[name];
572
- if (runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.session, name)) return runtime.variables.session[name];
573
- const staticVariables = collectStaticVariables(stack);
574
- if (Object.prototype.hasOwnProperty.call(staticVariables, name)) return staticVariables[name];
575
- return undefined;
511
+ const nextContent = content.filter((part) => !isThinkingContent(part));
512
+ const removedBlocks = content.length - nextContent.length;
513
+ if (removedBlocks === 0) return { message, removedBlocks: 0 };
514
+ if (nextContent.length === 0) return { removedBlocks };
515
+ return { message: { ...message, content: nextContent } as AgentMessage, removedBlocks };
576
516
  }
577
517
 
578
- function setRuntimeVariable(runtime: PromptRuntime, scope: "turn" | "session", name: string, value: string): void {
579
- if (!runtime.variables) return;
580
- if (scope === "session") {
581
- if (runtime.variables.session[name] !== value) {
582
- runtime.variables.session[name] = value;
583
- runtime.variables.sessionDirty = true;
584
- }
585
- return;
586
- }
587
- runtime.variables.turn[name] = value;
518
+ function isThinkingContent(value: unknown): boolean {
519
+ return !!value && typeof value === "object" && !Array.isArray(value) && (value as { type?: unknown }).type === "thinking";
588
520
  }
589
521
 
590
- function clearRuntimeVariable(runtime: PromptRuntime, scope: "turn" | "session", name: string): void {
591
- if (!runtime.variables) return;
592
- if (scope === "session") {
593
- if (Object.prototype.hasOwnProperty.call(runtime.variables.session, name)) {
594
- delete runtime.variables.session[name];
595
- runtime.variables.sessionDirty = true;
596
- }
597
- return;
598
- }
599
- delete runtime.variables.turn[name];
522
+ function enabledItems(stack: PromptStack): PromptStackItem[] {
523
+ return stack.items.filter((item) => item.enabled !== false);
600
524
  }
601
525
 
602
526
  function createSyntheticMessage(role: Exclude<PromptStackRole, "system">, content: string, stack: PromptStack, runtime: PromptRuntime): AgentMessage {
@@ -652,26 +576,3 @@ function contentToText(content: unknown): string {
652
576
  .filter(Boolean)
653
577
  .join("\n");
654
578
  }
655
-
656
- function formatDate(now: Date): string {
657
- const year = now.getFullYear();
658
- const month = String(now.getMonth() + 1).padStart(2, "0");
659
- const day = String(now.getDate()).padStart(2, "0");
660
- return `${year}-${month}-${day}`;
661
- }
662
-
663
- function formatTime(now: Date): string {
664
- const hours = String(now.getHours()).padStart(2, "0");
665
- const minutes = String(now.getMinutes()).padStart(2, "0");
666
- const seconds = String(now.getSeconds()).padStart(2, "0");
667
- return `${hours}:${minutes}:${seconds}`;
668
- }
669
-
670
- function escapeXml(value: string): string {
671
- return value
672
- .replace(/&/g, "&amp;")
673
- .replace(/</g, "&lt;")
674
- .replace(/>/g, "&gt;")
675
- .replace(/\"/g, "&quot;")
676
- .replace(/'/g, "&apos;");
677
- }