@zihanw/pi-forge 0.3.2 → 0.4.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.
Files changed (231) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/PUBLIC_API.md +5 -0
  3. package/README.md +93 -478
  4. package/README.zh-CN.md +88 -461
  5. package/SUBAGENT_ADAPTER_CONTRACT.md +5 -0
  6. package/dist/agent-profile.d.ts +71 -0
  7. package/dist/agent-profile.d.ts.map +1 -0
  8. package/dist/agent-profile.js +303 -0
  9. package/dist/agent-profile.js.map +1 -0
  10. package/dist/forge-config.d.ts +88 -0
  11. package/dist/forge-config.d.ts.map +1 -0
  12. package/dist/forge-config.js +290 -0
  13. package/dist/forge-config.js.map +1 -0
  14. package/dist/forge-extensions.d.ts.map +1 -1
  15. package/dist/forge-extensions.js +19 -3
  16. package/dist/forge-extensions.js.map +1 -1
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +97 -358
  20. package/dist/index.js.map +1 -1
  21. package/dist/lifecycle.d.ts +11 -3
  22. package/dist/lifecycle.d.ts.map +1 -1
  23. package/dist/lifecycle.js +90 -4
  24. package/dist/lifecycle.js.map +1 -1
  25. package/dist/loader.d.ts.map +1 -1
  26. package/dist/loader.js +116 -5
  27. package/dist/loader.js.map +1 -1
  28. package/dist/payload-capture.d.ts.map +1 -1
  29. package/dist/payload-capture.js +27 -0
  30. package/dist/payload-capture.js.map +1 -1
  31. package/dist/payload-command.d.ts +3 -3
  32. package/dist/payload-command.d.ts.map +1 -1
  33. package/dist/payload-command.js.map +1 -1
  34. package/dist/preview.d.ts +2 -2
  35. package/dist/preview.d.ts.map +1 -1
  36. package/dist/preview.js +2 -3
  37. package/dist/preview.js.map +1 -1
  38. package/dist/profile-command.d.ts +12 -0
  39. package/dist/profile-command.d.ts.map +1 -0
  40. package/dist/profile-command.js +291 -0
  41. package/dist/profile-command.js.map +1 -0
  42. package/dist/profile-service.d.ts +103 -0
  43. package/dist/profile-service.d.ts.map +1 -0
  44. package/dist/profile-service.js +220 -0
  45. package/dist/profile-service.js.map +1 -0
  46. package/dist/runtime/profile-runtime.d.ts +13 -0
  47. package/dist/runtime/profile-runtime.d.ts.map +1 -0
  48. package/dist/runtime/profile-runtime.js +47 -0
  49. package/dist/runtime/profile-runtime.js.map +1 -0
  50. package/dist/runtime/prompt-stack-runtime.d.ts +22 -0
  51. package/dist/runtime/prompt-stack-runtime.d.ts.map +1 -0
  52. package/dist/runtime/prompt-stack-runtime.js +104 -0
  53. package/dist/runtime/prompt-stack-runtime.js.map +1 -0
  54. package/dist/runtime/subagent-runtime.d.ts +45 -0
  55. package/dist/runtime/subagent-runtime.d.ts.map +1 -0
  56. package/dist/runtime/subagent-runtime.js +335 -0
  57. package/dist/runtime/subagent-runtime.js.map +1 -0
  58. package/dist/runtime/tool-policy-runtime.d.ts +15 -0
  59. package/dist/runtime/tool-policy-runtime.d.ts.map +1 -0
  60. package/dist/runtime/tool-policy-runtime.js +170 -0
  61. package/dist/runtime/tool-policy-runtime.js.map +1 -0
  62. package/dist/runtime/web-editor-runtime.d.ts +9 -0
  63. package/dist/runtime/web-editor-runtime.d.ts.map +1 -0
  64. package/dist/runtime/web-editor-runtime.js +131 -0
  65. package/dist/runtime/web-editor-runtime.js.map +1 -0
  66. package/dist/runtime-state.d.ts +4 -0
  67. package/dist/runtime-state.d.ts.map +1 -1
  68. package/dist/runtime-state.js +2 -0
  69. package/dist/runtime-state.js.map +1 -1
  70. package/dist/storage.d.ts +4 -0
  71. package/dist/storage.d.ts.map +1 -1
  72. package/dist/storage.js +24 -1
  73. package/dist/storage.js.map +1 -1
  74. package/dist/subagent/canonical.d.ts +22 -0
  75. package/dist/subagent/canonical.d.ts.map +1 -0
  76. package/dist/subagent/canonical.js +24 -0
  77. package/dist/subagent/canonical.js.map +1 -0
  78. package/dist/subagent/context.d.ts +8 -0
  79. package/dist/subagent/context.d.ts.map +1 -0
  80. package/dist/subagent/context.js +125 -0
  81. package/dist/subagent/context.js.map +1 -0
  82. package/dist/subagent/contract.d.ts +10 -0
  83. package/dist/subagent/contract.d.ts.map +1 -0
  84. package/dist/subagent/contract.js +10 -0
  85. package/dist/subagent/contract.js.map +1 -0
  86. package/dist/subagent/index.d.ts +14 -0
  87. package/dist/subagent/index.d.ts.map +1 -0
  88. package/dist/subagent/index.js +14 -0
  89. package/dist/subagent/index.js.map +1 -0
  90. package/dist/subagent/plan.d.ts +18 -0
  91. package/dist/subagent/plan.d.ts.map +1 -0
  92. package/dist/subagent/plan.js +157 -0
  93. package/dist/subagent/plan.js.map +1 -0
  94. package/dist/subagent/preflight.d.ts +4 -0
  95. package/dist/subagent/preflight.d.ts.map +1 -0
  96. package/dist/subagent/preflight.js +108 -0
  97. package/dist/subagent/preflight.js.map +1 -0
  98. package/dist/subagent/request.d.ts +4 -0
  99. package/dist/subagent/request.d.ts.map +1 -0
  100. package/dist/subagent/request.js +122 -0
  101. package/dist/subagent/request.js.map +1 -0
  102. package/dist/subagent/response.d.ts +8 -0
  103. package/dist/subagent/response.d.ts.map +1 -0
  104. package/dist/subagent/response.js +155 -0
  105. package/dist/subagent/response.js.map +1 -0
  106. package/dist/subagent/tools.d.ts +4 -0
  107. package/dist/subagent/tools.d.ts.map +1 -0
  108. package/dist/subagent/tools.js +42 -0
  109. package/dist/subagent/tools.js.map +1 -0
  110. package/dist/subagent/types.d.ts +268 -0
  111. package/dist/subagent/types.d.ts.map +1 -0
  112. package/dist/subagent/types.js +3 -0
  113. package/dist/subagent/types.js.map +1 -0
  114. package/dist/subagent/validation.d.ts +35 -0
  115. package/dist/subagent/validation.d.ts.map +1 -0
  116. package/dist/subagent/validation.js +314 -0
  117. package/dist/subagent/validation.js.map +1 -0
  118. package/dist/subagent-command.d.ts +4 -0
  119. package/dist/subagent-command.d.ts.map +1 -0
  120. package/dist/subagent-command.js +246 -0
  121. package/dist/subagent-command.js.map +1 -0
  122. package/dist/subagent-host.d.ts +44 -0
  123. package/dist/subagent-host.d.ts.map +1 -0
  124. package/dist/subagent-host.js +292 -0
  125. package/dist/subagent-host.js.map +1 -0
  126. package/dist/subagent-profile-tool.d.ts +49 -0
  127. package/dist/subagent-profile-tool.d.ts.map +1 -0
  128. package/dist/subagent-profile-tool.js +124 -0
  129. package/dist/subagent-profile-tool.js.map +1 -0
  130. package/dist/subagent-tool.d.ts +53 -0
  131. package/dist/subagent-tool.d.ts.map +1 -0
  132. package/dist/subagent-tool.js +456 -0
  133. package/dist/subagent-tool.js.map +1 -0
  134. package/dist/web-editor/client-script.d.ts +2 -0
  135. package/dist/web-editor/client-script.d.ts.map +1 -0
  136. package/dist/web-editor/client-script.generated.d.ts +2 -0
  137. package/dist/web-editor/client-script.generated.d.ts.map +1 -0
  138. package/dist/web-editor/client-script.generated.js +3 -0
  139. package/dist/web-editor/client-script.generated.js.map +1 -0
  140. package/dist/web-editor/client-script.js +2 -0
  141. package/dist/web-editor/client-script.js.map +1 -0
  142. package/dist/web-editor/client-styles.d.ts +2 -0
  143. package/dist/web-editor/client-styles.d.ts.map +1 -0
  144. package/dist/web-editor/client-styles.generated.d.ts +2 -0
  145. package/dist/web-editor/client-styles.generated.d.ts.map +1 -0
  146. package/dist/web-editor/client-styles.generated.js +3 -0
  147. package/dist/web-editor/client-styles.generated.js.map +1 -0
  148. package/dist/web-editor/client-styles.js +2 -0
  149. package/dist/web-editor/client-styles.js.map +1 -0
  150. package/dist/web-editor/page.d.ts +2 -0
  151. package/dist/web-editor/page.d.ts.map +1 -1
  152. package/dist/web-editor/page.js +14 -3319
  153. package/dist/web-editor/page.js.map +1 -1
  154. package/dist/web-editor/server.d.ts.map +1 -1
  155. package/dist/web-editor/server.js +148 -0
  156. package/dist/web-editor/server.js.map +1 -1
  157. package/dist/web-editor/styles.d.ts +2 -0
  158. package/dist/web-editor/styles.d.ts.map +1 -0
  159. package/dist/web-editor/styles.js +1053 -0
  160. package/dist/web-editor/styles.js.map +1 -0
  161. package/dist/web-editor/types.d.ts +79 -0
  162. package/dist/web-editor/types.d.ts.map +1 -1
  163. package/dist/web-host.d.ts +16 -5
  164. package/dist/web-host.d.ts.map +1 -1
  165. package/dist/web-host.js +307 -0
  166. package/dist/web-host.js.map +1 -1
  167. package/docs/README.md +41 -0
  168. package/docs/concepts/agent-profiles.md +60 -0
  169. package/docs/concepts/prompt-stacks.md +90 -0
  170. package/docs/design/README.md +17 -0
  171. package/docs/design/roadmap-0.4-archive.md +216 -0
  172. package/docs/design/subagents/design-review.md +220 -0
  173. package/docs/design/subagents/interface-design.md +274 -0
  174. package/docs/design/subagents/sdk-spike-findings.md +117 -0
  175. package/docs/development/complexity-review.md +86 -0
  176. package/docs/development/release.md +31 -0
  177. package/docs/development/roadmap.md +42 -0
  178. package/docs/development/setup.md +75 -0
  179. package/docs/getting-started.md +93 -0
  180. package/docs/guides/custom-macros-and-slots.md +68 -0
  181. package/docs/guides/debugging.md +39 -0
  182. package/docs/guides/delegation.md +99 -0
  183. package/docs/guides/sillytavern-import.md +47 -0
  184. package/docs/guides/use-cases.md +65 -0
  185. package/docs/guides/web-editor.md +75 -0
  186. package/docs/reference/commands.md +60 -0
  187. package/docs/reference/configuration.md +64 -0
  188. package/docs/reference/features.md +279 -0
  189. package/docs/reference/macros-and-slots.md +82 -0
  190. package/docs/reference/public-api.md +28 -0
  191. package/docs/reference/stack-schema.md +167 -0
  192. package/docs/reference/subagent-adapter.md +204 -0
  193. package/docs/zh-CN/README.md +37 -0
  194. package/docs/zh-CN/concepts/agent-profiles.md +44 -0
  195. package/docs/zh-CN/concepts/prompt-stacks.md +40 -0
  196. package/docs/zh-CN/getting-started.md +79 -0
  197. package/docs/zh-CN/guides/delegation.md +66 -0
  198. package/docs/zh-CN/guides/web-editor.md +45 -0
  199. package/docs/zh-CN/reference/commands.md +58 -0
  200. package/package.json +55 -13
  201. package/src/compiler.ts +0 -578
  202. package/src/extension-registry.ts +0 -33
  203. package/src/forge-extensions.ts +0 -223
  204. package/src/index.ts +0 -445
  205. package/src/lifecycle.ts +0 -171
  206. package/src/loader.ts +0 -394
  207. package/src/macro-engine.ts +0 -358
  208. package/src/payload-capture.ts +0 -85
  209. package/src/payload-command.ts +0 -138
  210. package/src/policy.ts +0 -42
  211. package/src/preset-command.ts +0 -280
  212. package/src/preview.ts +0 -226
  213. package/src/regex.ts +0 -500
  214. package/src/render-helpers.ts +0 -169
  215. package/src/runtime-state.ts +0 -40
  216. package/src/sillytavern-importer/items.ts +0 -98
  217. package/src/sillytavern-importer/macros.ts +0 -159
  218. package/src/sillytavern-importer/prompt-order.ts +0 -54
  219. package/src/sillytavern-importer/regex.ts +0 -270
  220. package/src/sillytavern-importer/report.ts +0 -202
  221. package/src/sillytavern-importer/types.ts +0 -120
  222. package/src/sillytavern-importer.ts +0 -152
  223. package/src/slot-renderers.ts +0 -414
  224. package/src/stack-migration.ts +0 -159
  225. package/src/storage.ts +0 -45
  226. package/src/types.ts +0 -209
  227. package/src/web-editor/index.ts +0 -2
  228. package/src/web-editor/page.ts +0 -3330
  229. package/src/web-editor/server.ts +0 -294
  230. package/src/web-editor/types.ts +0 -98
  231. package/src/web-host.ts +0 -232
package/src/regex.ts DELETED
@@ -1,500 +0,0 @@
1
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
- import type {
3
- PromptRegexEffect,
4
- PromptRegexRule,
5
- PromptRegexStage,
6
- PromptRegexTarget,
7
- PromptStack,
8
- PromptStackDiagnostic,
9
- } from "./types.ts";
10
-
11
- const ALLOWED_REGEX_FLAGS = new Set(["g", "i", "m", "s", "u"]);
12
- const VALID_STAGES = new Set(["history", "compiled"]);
13
- const VALID_EFFECTS = new Set(["outgoing", "display", "both", "finalize"]);
14
- const VALID_TARGETS = new Set(["system", "messages"]);
15
-
16
- interface CompiledRegexRule {
17
- id: string;
18
- stage: PromptRegexStage;
19
- effect: PromptRegexEffect;
20
- targets?: PromptRegexTarget[];
21
- roles?: string[];
22
- maxMessages?: number;
23
- maxChars?: number;
24
- minDepth?: number;
25
- maxDepth?: number;
26
- regexp: RegExp;
27
- replace: string;
28
- trimStrings?: string[];
29
- }
30
-
31
- interface StringTransformResult {
32
- text: string;
33
- matches: number;
34
- changed: boolean;
35
- }
36
-
37
- interface RegexStats {
38
- matches: number;
39
- changedSegments: number;
40
- }
41
-
42
- export function validateRegexConfig(config: unknown): PromptStackDiagnostic[] {
43
- const diagnostics: PromptStackDiagnostic[] = [];
44
- if (config === undefined) return diagnostics;
45
- if (!isPlainObject(config)) {
46
- diagnostics.push({ level: "warning", message: "regex must be an object when provided." });
47
- return diagnostics;
48
- }
49
-
50
- if (config.schemaVersion !== undefined && config.schemaVersion !== 1) {
51
- diagnostics.push({ level: "warning", message: "Missing or unsupported regex.schemaVersion; assuming 1." });
52
- }
53
-
54
- const rawRules = config.rules;
55
- if (rawRules === undefined) return diagnostics;
56
- if (!Array.isArray(rawRules)) {
57
- diagnostics.push({ level: "error", message: "regex.rules must be an array when provided." });
58
- return diagnostics;
59
- }
60
-
61
- const seenIds = new Set<string>();
62
- for (const [index, rawRule] of rawRules.entries()) {
63
- validateRule(rawRule, index, seenIds, diagnostics);
64
- }
65
- return diagnostics;
66
- }
67
-
68
- export function applyRegexRulesToString(
69
- stack: PromptStack,
70
- text: string,
71
- stage: PromptRegexStage,
72
- target: PromptRegexTarget,
73
- diagnostics: PromptStackDiagnostic[],
74
- ): string {
75
- let result = text;
76
- for (const rule of regexRulesFor(stack, stage, target, "outgoing", diagnostics)) {
77
- const transformed = transformString(result, rule);
78
- result = transformed.text;
79
- addRuleStats(diagnostics, rule, stage, target, {
80
- matches: transformed.matches,
81
- changedSegments: transformed.changed ? 1 : 0,
82
- });
83
- }
84
- return result;
85
- }
86
-
87
- export function applyRegexRulesToMessages(
88
- stack: PromptStack,
89
- messages: AgentMessage[],
90
- stage: PromptRegexStage,
91
- diagnostics: PromptStackDiagnostic[],
92
- ): AgentMessage[] {
93
- let result = messages;
94
- for (const rule of regexRulesFor(stack, stage, "messages", "outgoing", diagnostics)) {
95
- const stats: RegexStats = { matches: 0, changedSegments: 0 };
96
- result = transformMessages(result, rule, stats);
97
- addRuleStats(diagnostics, rule, stage, "messages", stats);
98
- }
99
- return result;
100
- }
101
-
102
- export function applyFinalizeRegexRulesToMessage(
103
- stack: PromptStack,
104
- message: AgentMessage,
105
- diagnostics: PromptStackDiagnostic[],
106
- ): AgentMessage | undefined {
107
- if (String((message as { role?: unknown }).role) !== "assistant") return undefined;
108
-
109
- let result = message;
110
- for (const rule of regexRulesFor(stack, "compiled", "messages", "finalize", diagnostics)) {
111
- const stats: RegexStats = { matches: 0, changedSegments: 0 };
112
- const [next = result] = transformMessages([result], rule, stats);
113
- result = next;
114
- addRuleStats(diagnostics, rule, "finalize", "messages", stats);
115
- }
116
-
117
- if (result === message) return undefined;
118
- diagnostics.push({
119
- level: "warning",
120
- message: "Finalize regex replaced finalized assistant message content; original model output is not preserved in the stored transcript.",
121
- });
122
- return result;
123
- }
124
-
125
- function validateRule(rawRule: unknown, index: number, seenIds: Set<string>, diagnostics: PromptStackDiagnostic[]): void {
126
- const label = `regex rule ${index + 1}`;
127
- if (!isPlainObject(rawRule)) {
128
- diagnostics.push({ level: "error", message: `${label} must be an object.` });
129
- return;
130
- }
131
-
132
- const id = typeof rawRule.id === "string" ? rawRule.id.trim() : "";
133
- if (!id) {
134
- diagnostics.push({ level: "error", message: `${label} must have a non-empty id.` });
135
- } else if (seenIds.has(id)) {
136
- diagnostics.push({ level: "error", message: `Duplicate regex rule id: ${id}.` });
137
- } else {
138
- seenIds.add(id);
139
- }
140
-
141
- if (rawRule.enabled !== undefined && typeof rawRule.enabled !== "boolean") {
142
- diagnostics.push({ level: "warning", message: `${regexRuleLabel(id, index)} enabled must be a boolean when provided.` });
143
- }
144
-
145
- if (!VALID_STAGES.has(rawRule.stage as string)) {
146
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} stage must be "history" or "compiled".` });
147
- }
148
-
149
- const effect = typeof rawRule.effect === "string" ? rawRule.effect : undefined;
150
- if (rawRule.effect !== undefined) {
151
- if (!VALID_EFFECTS.has(rawRule.effect as string)) {
152
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} effect must be "outgoing", "display", "both", or "finalize".` });
153
- } else if (rawRule.effect === "display") {
154
- diagnostics.push({ level: "warning", message: `${regexRuleLabel(id, index)} effect "display" is not a runtime effect in pi-forge yet and will be ignored. Use "finalize" only when destructive transcript cleanup is intended.` });
155
- } else if (rawRule.effect === "both") {
156
- diagnostics.push({ level: "warning", message: `${regexRuleLabel(id, index)} effect "both" is not implemented and will be ignored; use separate outgoing and finalize rules.` });
157
- } else if (rawRule.effect === "finalize") {
158
- diagnostics.push({ level: "warning", message: `${regexRuleLabel(id, index)} effect "finalize" rewrites finalized assistant messages and replaces the original model output in the stored transcript.` });
159
- }
160
- }
161
- if (effect === "finalize" && rawRule.stage !== "compiled") {
162
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} effect "finalize" requires stage "compiled".` });
163
- }
164
-
165
- if (typeof rawRule.pattern !== "string" || rawRule.pattern.length === 0) {
166
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} pattern must be a non-empty string.` });
167
- } else {
168
- const flagsError = validateRegexFlags(rawRule.flags, id, index);
169
- if (flagsError) diagnostics.push({ level: "error", message: flagsError });
170
- else {
171
- try {
172
- new RegExp(rawRule.pattern, typeof rawRule.flags === "string" ? rawRule.flags : "");
173
- } catch (error) {
174
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} pattern failed to compile: ${error instanceof Error ? error.message : String(error)}` });
175
- }
176
- }
177
- }
178
-
179
- if (rawRule.replace !== undefined && typeof rawRule.replace !== "string") {
180
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} replace must be a string when provided.` });
181
- } else if (typeof rawRule.replace === "string") {
182
- if (/\{\{\s*match\s*\}\}/i.test(rawRule.replace)) {
183
- diagnostics.push({ level: "warning", message: `${regexRuleLabel(id, index)} replacement contains SillyTavern {{match}}; use JavaScript $& or re-import the SillyTavern preset to convert it.` });
184
- }
185
- }
186
- if (rawRule.trimStrings !== undefined && !isStringArray(rawRule.trimStrings)) {
187
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} trimStrings must be an array of strings when provided.` });
188
- }
189
- if (rawRule.roles !== undefined && !isStringArray(rawRule.roles)) {
190
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} roles must be an array of strings when provided.` });
191
- }
192
- if (rawRule.targets !== undefined) {
193
- if (!isStringArray(rawRule.targets)) {
194
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} targets must be an array of strings when provided.` });
195
- } else {
196
- for (const target of rawRule.targets) {
197
- if (!VALID_TARGETS.has(target)) {
198
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} target must be "system" or "messages": ${target}.` });
199
- } else if (effect === "finalize" && target !== "messages") {
200
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} effect "finalize" only supports target "messages": ${target}.` });
201
- }
202
- }
203
- }
204
- }
205
- if (effect === "finalize" && isStringArray(rawRule.roles) && !rawRule.roles.includes("assistant")) {
206
- diagnostics.push({ level: "warning", message: `${regexRuleLabel(id, index)} effect "finalize" only runs for finalized assistant messages, but roles does not include "assistant".` });
207
- }
208
- if (rawRule.maxMessages !== undefined && !isPositiveInteger(rawRule.maxMessages)) {
209
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} maxMessages must be a positive integer when provided.` });
210
- }
211
- if (rawRule.maxChars !== undefined && !isPositiveInteger(rawRule.maxChars)) {
212
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} maxChars must be a positive integer when provided.` });
213
- }
214
- if (rawRule.minDepth !== undefined && !isNonNegativeInteger(rawRule.minDepth)) {
215
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} minDepth must be a non-negative integer when provided.` });
216
- }
217
- if (rawRule.maxDepth !== undefined && !isNonNegativeInteger(rawRule.maxDepth)) {
218
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} maxDepth must be a non-negative integer when provided.` });
219
- }
220
- if (isNonNegativeInteger(rawRule.minDepth) && isNonNegativeInteger(rawRule.maxDepth) && rawRule.maxDepth < rawRule.minDepth) {
221
- diagnostics.push({ level: "error", message: `${regexRuleLabel(id, index)} maxDepth must be greater than or equal to minDepth.` });
222
- }
223
- }
224
-
225
- function regexRulesFor(
226
- stack: PromptStack,
227
- stage: PromptRegexStage,
228
- target: PromptRegexTarget,
229
- effect: PromptRegexEffect,
230
- diagnostics: PromptStackDiagnostic[],
231
- ): CompiledRegexRule[] {
232
- const rules = Array.isArray(stack.regex?.rules) ? stack.regex.rules : [];
233
- const compiled: CompiledRegexRule[] = [];
234
-
235
- for (const rawRule of rules) {
236
- if (!isPlainObject(rawRule)) continue;
237
- if (rawRule.enabled === false) continue;
238
- if ((rawRule.effect ?? "outgoing") !== effect) continue;
239
- if (rawRule.stage !== stage) continue;
240
- if (stage === "compiled" && Array.isArray(rawRule.targets) && !rawRule.targets.includes(target)) continue;
241
-
242
- const rule = compileRuntimeRule(rawRule as unknown as PromptRegexRule, diagnostics);
243
- if (rule) compiled.push(rule);
244
- }
245
-
246
- return compiled;
247
- }
248
-
249
- function compileRuntimeRule(rule: PromptRegexRule, diagnostics: PromptStackDiagnostic[]): CompiledRegexRule | undefined {
250
- if (typeof rule.id !== "string" || !rule.id.trim()) return undefined;
251
- if (rule.stage !== "history" && rule.stage !== "compiled") return undefined;
252
- const effect = rule.effect ?? "outgoing";
253
- if (!VALID_EFFECTS.has(effect)) return undefined;
254
- if (typeof rule.pattern !== "string" || rule.pattern.length === 0) return undefined;
255
- const flags = typeof rule.flags === "string" ? rule.flags : "";
256
- const flagsError = validateRegexFlags(flags, rule.id, -1);
257
- if (flagsError) return undefined;
258
- try {
259
- return {
260
- id: rule.id.trim(),
261
- stage: rule.stage,
262
- effect,
263
- targets: normalizeTargets(rule.targets),
264
- roles: isStringArray(rule.roles) ? rule.roles : undefined,
265
- maxMessages: isPositiveInteger(rule.maxMessages) ? Math.floor(rule.maxMessages) : undefined,
266
- maxChars: isPositiveInteger(rule.maxChars) ? Math.floor(rule.maxChars) : undefined,
267
- minDepth: isNonNegativeInteger(rule.minDepth) ? Math.floor(rule.minDepth) : undefined,
268
- maxDepth: isNonNegativeInteger(rule.maxDepth) ? Math.floor(rule.maxDepth) : undefined,
269
- regexp: new RegExp(rule.pattern, flags),
270
- replace: typeof rule.replace === "string" ? rule.replace : "",
271
- trimStrings: isStringArray(rule.trimStrings) ? rule.trimStrings : undefined,
272
- };
273
- } catch (error) {
274
- diagnostics.push({ level: "error", message: `Regex rule ${rule.id} failed to compile: ${error instanceof Error ? error.message : String(error)}` });
275
- return undefined;
276
- }
277
- }
278
-
279
- function transformMessages(messages: AgentMessage[], rule: CompiledRegexRule, stats: RegexStats): AgentMessage[] {
280
- const eligible = eligibleMessageIndexes(messages, rule);
281
- const eligibleSet = new Set(rule.maxMessages ? eligible.slice(-rule.maxMessages) : eligible);
282
- let changed = false;
283
-
284
- const transformed = messages.map((message, index) => {
285
- if (!eligibleSet.has(index)) return message;
286
- const next = transformMessage(message, rule, stats);
287
- if (next !== message) changed = true;
288
- return next;
289
- });
290
-
291
- return changed ? transformed : messages;
292
- }
293
-
294
- function eligibleMessageIndexes(messages: AgentMessage[], rule: CompiledRegexRule): number[] {
295
- const indexes: number[] = [];
296
- for (const [index, message] of messages.entries()) {
297
- if (rule.roles && !rule.roles.includes(String((message as { role?: unknown }).role))) continue;
298
- const depth = messages.length - 1 - index;
299
- if (rule.minDepth !== undefined && depth < rule.minDepth) continue;
300
- if (rule.maxDepth !== undefined && depth > rule.maxDepth) continue;
301
- indexes.push(index);
302
- }
303
- return indexes;
304
- }
305
-
306
- function transformMessage(message: AgentMessage, rule: CompiledRegexRule, stats: RegexStats): AgentMessage {
307
- const content = (message as { content?: unknown }).content;
308
- if (typeof content === "string") {
309
- const result = transformString(content, rule);
310
- mergeStringStats(stats, result);
311
- return result.changed ? { ...message, content: result.text } as AgentMessage : message;
312
- }
313
-
314
- if (!Array.isArray(content)) return message;
315
- let changed = false;
316
- const nextContent = content.map((part) => {
317
- if (!isPlainObject(part) || part.type !== "text" || typeof part.text !== "string") return part;
318
- const result = transformString(part.text, rule);
319
- mergeStringStats(stats, result);
320
- if (!result.changed) return part;
321
- changed = true;
322
- return { ...part, text: result.text };
323
- });
324
-
325
- return changed ? { ...message, content: nextContent } as AgentMessage : message;
326
- }
327
-
328
- function transformString(text: string, rule: CompiledRegexRule): StringTransformResult {
329
- const headLength = rule.maxChars && text.length > rule.maxChars ? text.length - rule.maxChars : 0;
330
- const head = headLength > 0 ? text.slice(0, headLength) : "";
331
- const body = headLength > 0 ? text.slice(headLength) : text;
332
- const matches = countReplacementMatches(body, rule.regexp);
333
- const replaced = rule.trimStrings && rule.trimStrings.length > 0
334
- ? replaceWithTrimStrings(body, rule)
335
- : body.replace(rule.regexp, convertDollarZeroToFullMatch(rule.replace));
336
- const result = head + replaced;
337
- return {
338
- text: result,
339
- matches,
340
- changed: result !== text,
341
- };
342
- }
343
-
344
- /**
345
- * Normalizes a JavaScript replacement string so that `$0` (not followed by another
346
- * digit) behaves as the full match, matching the custom trimStrings expander and
347
- * SillyTavern conventions. `$$0` stays a literal `$0`. This keeps `$0` consistent
348
- * across the native and trimStrings replacement paths.
349
- */
350
- function convertDollarZeroToFullMatch(replace: string): string {
351
- let result = "";
352
- for (let i = 0; i < replace.length;) {
353
- const ch = replace[i];
354
- if (ch !== undefined && ch !== "$") {
355
- result += ch;
356
- i++;
357
- continue;
358
- }
359
- const next = replace[i + 1];
360
- if (next === "$") {
361
- result += "$$";
362
- i += 2;
363
- continue;
364
- }
365
- if (next === "0" && !isDigitCode(replace.charCodeAt(i + 2))) {
366
- result += "$&";
367
- i += 2;
368
- continue;
369
- }
370
- result += "$";
371
- i++;
372
- }
373
- return result;
374
- }
375
-
376
- function isDigitCode(code: number): boolean {
377
- return code >= 48 && code <= 57;
378
- }
379
-
380
- function replaceWithTrimStrings(text: string, rule: CompiledRegexRule): string {
381
- return text.replace(rule.regexp, (...args: unknown[]) => {
382
- const groups = isPlainObject(args.at(-1)) ? args.pop() as Record<string, unknown> : undefined;
383
- const input = String(args.pop() ?? "");
384
- const offset = Number(args.pop() ?? 0);
385
- const [match = "", ...captures] = args.map((arg) => typeof arg === "string" ? arg : "");
386
- return expandReplacementTemplate(rule.replace, {
387
- match,
388
- captures,
389
- offset,
390
- input,
391
- groups,
392
- trimStrings: rule.trimStrings ?? [],
393
- });
394
- });
395
- }
396
-
397
- function expandReplacementTemplate(
398
- template: string,
399
- context: {
400
- match: string;
401
- captures: string[];
402
- offset: number;
403
- input: string;
404
- groups?: Record<string, unknown>;
405
- trimStrings: string[];
406
- },
407
- ): string {
408
- return template.replace(/\$([$&`']|0(?!\d)|[1-9]\d?|<[^>]+>)/g, (token) => {
409
- if (token === "$$") return "$";
410
- if (token === "$&" || token === "$0") return trimMatchedValue(context.match, context.trimStrings);
411
- if (token === "$`") return context.input.slice(0, context.offset);
412
- if (token === "$'") return context.input.slice(context.offset + context.match.length);
413
- if (token.startsWith("$<")) {
414
- const name = token.slice(2, -1);
415
- const value = context.groups?.[name];
416
- return typeof value === "string" ? trimMatchedValue(value, context.trimStrings) : "";
417
- }
418
- const captureIndex = Number(token.slice(1)) - 1;
419
- const capture = context.captures[captureIndex];
420
- return capture === undefined ? "" : trimMatchedValue(capture, context.trimStrings);
421
- });
422
- }
423
-
424
- function trimMatchedValue(value: string, trimStrings: string[]): string {
425
- let result = value;
426
- for (const trimString of trimStrings) {
427
- if (!trimString) continue;
428
- result = result.split(trimString).join("");
429
- }
430
- return result;
431
- }
432
-
433
- function countReplacementMatches(text: string, regexp: RegExp): number {
434
- if (!regexp.global) return new RegExp(regexp.source, regexp.flags.replace("g", "")).test(text) ? 1 : 0;
435
-
436
- const matcher = new RegExp(regexp.source, regexp.flags);
437
- let count = 0;
438
- let match: RegExpExecArray | null;
439
- while ((match = matcher.exec(text)) !== null) {
440
- count++;
441
- if (match[0] === "") matcher.lastIndex++;
442
- }
443
- return count;
444
- }
445
-
446
- function mergeStringStats(stats: RegexStats, result: StringTransformResult): void {
447
- stats.matches += result.matches;
448
- if (result.changed) stats.changedSegments++;
449
- }
450
-
451
- function addRuleStats(
452
- diagnostics: PromptStackDiagnostic[],
453
- rule: CompiledRegexRule,
454
- stage: string,
455
- target: PromptRegexTarget,
456
- stats: RegexStats,
457
- ): void {
458
- if (stats.matches === 0) return;
459
- diagnostics.push({
460
- level: "info",
461
- message: `Regex rule ${rule.id} matched ${stats.matches} time(s) and changed ${stats.changedSegments} text segment(s) in ${stage}/${target}.`,
462
- });
463
- }
464
-
465
- function validateRegexFlags(value: unknown, id: string, index: number): string | undefined {
466
- if (value === undefined) return undefined;
467
- if (typeof value !== "string") return `${regexRuleLabel(id, index)} flags must be a string when provided.`;
468
- const seen = new Set<string>();
469
- for (const flag of value) {
470
- if (!ALLOWED_REGEX_FLAGS.has(flag)) return `${regexRuleLabel(id, index)} has unsupported regex flag: ${flag}.`;
471
- if (seen.has(flag)) return `${regexRuleLabel(id, index)} has duplicate regex flag: ${flag}.`;
472
- seen.add(flag);
473
- }
474
- return undefined;
475
- }
476
-
477
- function normalizeTargets(value: unknown): PromptRegexTarget[] | undefined {
478
- if (!isStringArray(value)) return undefined;
479
- return value.filter((target): target is PromptRegexTarget => target === "system" || target === "messages");
480
- }
481
-
482
- function regexRuleLabel(id: string, index: number): string {
483
- return id ? `Regex rule ${id}` : `regex rule ${index + 1}`;
484
- }
485
-
486
- function isStringArray(value: unknown): value is string[] {
487
- return Array.isArray(value) && value.every((item) => typeof item === "string");
488
- }
489
-
490
- function isPositiveInteger(value: unknown): value is number {
491
- return typeof value === "number" && Number.isInteger(value) && value > 0;
492
- }
493
-
494
- function isNonNegativeInteger(value: unknown): value is number {
495
- return typeof value === "number" && Number.isInteger(value) && value >= 0;
496
- }
497
-
498
- function isPlainObject(value: unknown): value is Record<string, unknown> {
499
- return !!value && typeof value === "object" && !Array.isArray(value);
500
- }
@@ -1,169 +0,0 @@
1
- import { applyResourcePolicy } from "./policy.ts";
2
- import type {
3
- PromptRuntime,
4
- PromptStack,
5
- PromptStackSlotFormat,
6
- PromptStackSlotItem,
7
- PromptVariableValue,
8
- } from "./types.ts";
9
-
10
- export type PromptVariableScope = "static" | "session" | "turn";
11
- export type PromptWritableVariableScope = "session" | "turn";
12
-
13
- export interface PromptVariableAccess {
14
- get(name: string, scope?: PromptVariableScope | "any"): PromptVariableValue | undefined;
15
- set(scope: PromptWritableVariableScope, name: string, value: string): void;
16
- clear(scope: PromptWritableVariableScope, name: string): void;
17
- toMacroText(value: PromptVariableValue | undefined): string;
18
- toPromptText(value: PromptVariableValue): string;
19
- }
20
-
21
- export interface PromptRenderHelpers {
22
- escapeXml(value: string): string;
23
- formatDate(now: Date): string;
24
- formatTime(now: Date): string;
25
- normalizePath(value: string): string;
26
- selectedToolNames(stack: PromptStack, runtime: PromptRuntime): string[];
27
- slotTextFormat(item: PromptStackSlotItem, options?: { allowJson?: boolean }): PromptStackSlotFormat;
28
- plainBullet(label: string, value: string): string;
29
- plainContinuation(value: string, indent: string): string;
30
- indentPlainBlock(value: string, indent: string): string;
31
- }
32
-
33
- export const promptRenderHelpers: PromptRenderHelpers = {
34
- escapeXml,
35
- formatDate,
36
- formatTime,
37
- normalizePath,
38
- selectedToolNames,
39
- slotTextFormat,
40
- plainBullet,
41
- plainContinuation,
42
- indentPlainBlock,
43
- };
44
-
45
- export function createVariableAccess(runtime: PromptRuntime, stack: PromptStack): PromptVariableAccess {
46
- return {
47
- get: (name, scope = "any") => getRuntimeVariable(runtime, stack, name, scope),
48
- set: (scope, name, value) => setRuntimeVariable(runtime, scope, name, value),
49
- clear: (scope, name) => clearRuntimeVariable(runtime, scope, name),
50
- toMacroText: variableValueToMacroText,
51
- toPromptText: variableValueToPromptText,
52
- };
53
- }
54
-
55
- export function selectedToolNames(stack: PromptStack, runtime: PromptRuntime): string[] {
56
- return applyResourcePolicy(runtime.options.selectedTools ?? [], stack.tools);
57
- }
58
-
59
- export function slotTextFormat(item: PromptStackSlotItem, options: { allowJson?: boolean } = {}): PromptStackSlotFormat {
60
- const format = item.options?.format;
61
- if (format === "plain") return "plain";
62
- if (format === "json" && options.allowJson) return "json";
63
- return "xml";
64
- }
65
-
66
- export function selectedVariableScopes(options: Record<string, unknown>): PromptVariableScope[] {
67
- const scopes: PromptVariableScope[] = [];
68
- if (options.includeStatic !== false) scopes.push("static");
69
- if (options.includeSession !== false) scopes.push("session");
70
- if (options.includeTurn !== false) scopes.push("turn");
71
- return scopes;
72
- }
73
-
74
- export function collectStaticVariables(stack: PromptStack): Record<string, PromptVariableValue> {
75
- return { ...(stack.variables ?? {}) };
76
- }
77
-
78
- export function getRuntimeVariable(
79
- runtime: PromptRuntime,
80
- stack: PromptStack,
81
- name: string,
82
- scope: PromptVariableScope | "any" = "any",
83
- ): PromptVariableValue | undefined {
84
- if ((scope === "turn" || scope === "any") && runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.turn, name)) {
85
- return runtime.variables.turn[name];
86
- }
87
- if ((scope === "session" || scope === "any") && runtime.variables && Object.prototype.hasOwnProperty.call(runtime.variables.session, name)) {
88
- return runtime.variables.session[name];
89
- }
90
- if (scope === "static" || scope === "any") {
91
- const staticVariables = collectStaticVariables(stack);
92
- if (Object.prototype.hasOwnProperty.call(staticVariables, name)) return staticVariables[name];
93
- }
94
- return undefined;
95
- }
96
-
97
- export function setRuntimeVariable(runtime: PromptRuntime, scope: PromptWritableVariableScope, name: string, value: string): void {
98
- if (!runtime.variables) return;
99
- if (scope === "session") {
100
- if (runtime.variables.session[name] !== value) {
101
- runtime.variables.session[name] = value;
102
- runtime.variables.sessionDirty = true;
103
- }
104
- return;
105
- }
106
- runtime.variables.turn[name] = value;
107
- }
108
-
109
- export function clearRuntimeVariable(runtime: PromptRuntime, scope: PromptWritableVariableScope, name: string): void {
110
- if (!runtime.variables) return;
111
- if (scope === "session") {
112
- if (Object.prototype.hasOwnProperty.call(runtime.variables.session, name)) {
113
- delete runtime.variables.session[name];
114
- runtime.variables.sessionDirty = true;
115
- }
116
- return;
117
- }
118
- delete runtime.variables.turn[name];
119
- }
120
-
121
- export function variableValueToMacroText(value: PromptVariableValue | undefined): string {
122
- if (value === undefined) return "";
123
- if (typeof value === "string") return value;
124
- return JSON.stringify(value);
125
- }
126
-
127
- export function variableValueToPromptText(value: PromptVariableValue): string {
128
- if (typeof value === "string") return value;
129
- return JSON.stringify(value, null, 2);
130
- }
131
-
132
- export function plainBullet(label: string, value: string): string {
133
- return `- ${label}: ${plainContinuation(value, " ")}`;
134
- }
135
-
136
- export function plainContinuation(value: string, indent: string): string {
137
- return value.split("\n").map((line, index) => index === 0 ? line : `${indent}${line}`).join("\n");
138
- }
139
-
140
- export function indentPlainBlock(value: string, indent: string): string {
141
- return value.split("\n").map((line) => `${indent}${line}`).join("\n");
142
- }
143
-
144
- export function normalizePath(value: string): string {
145
- return value.replace(/\\/g, "/");
146
- }
147
-
148
- export function formatDate(now: Date): string {
149
- const year = now.getFullYear();
150
- const month = String(now.getMonth() + 1).padStart(2, "0");
151
- const day = String(now.getDate()).padStart(2, "0");
152
- return `${year}-${month}-${day}`;
153
- }
154
-
155
- export function formatTime(now: Date): string {
156
- const hours = String(now.getHours()).padStart(2, "0");
157
- const minutes = String(now.getMinutes()).padStart(2, "0");
158
- const seconds = String(now.getSeconds()).padStart(2, "0");
159
- return `${hours}:${minutes}:${seconds}`;
160
- }
161
-
162
- export function escapeXml(value: string): string {
163
- return value
164
- .replace(/&/g, "&amp;")
165
- .replace(/</g, "&lt;")
166
- .replace(/>/g, "&gt;")
167
- .replace(/\"/g, "&quot;")
168
- .replace(/'/g, "&apos;");
169
- }