@docyrus/docyrus 0.0.22 → 0.0.24
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 +12 -0
- package/main.js +234 -11
- package/main.js.map +4 -4
- package/package.json +6 -4
- package/resources/pi-agent/assets/docyrus-logo.svg +16 -0
- package/resources/pi-agent/extensions/architect.ts +771 -0
- package/resources/pi-agent/extensions/notify.ts +57 -55
- package/resources/pi-agent/extensions/pi-custom-compaction/CHANGELOG.md +27 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/LICENSE +21 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/README.md +244 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/VENDORED_FROM.md +6 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/banner.png +0 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/commands/register-commands.ts +63 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/events/register-events.ts +229 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/index.ts +10 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/package.json +57 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/paths.ts +13 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/policy/config.ts +32 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/policy/merge.ts +67 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/policy/parse.ts +354 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/policy/types.ts +131 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/runtime/model-resolution.ts +77 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/runtime/pure.ts +56 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/runtime/session-state.ts +244 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/summary/generate.ts +184 -0
- package/resources/pi-agent/extensions/pi-custom-compaction/summary/template.ts +124 -0
- package/server-loader.js +4017 -0
- package/server-loader.js.map +7 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { setPatchValue } from "./merge.js";
|
|
2
|
+
import {
|
|
3
|
+
type ICompactionPolicyPatch,
|
|
4
|
+
type IModelEntry,
|
|
5
|
+
type IParseResult,
|
|
6
|
+
type IPolicyKey,
|
|
7
|
+
POLICY_KEYS,
|
|
8
|
+
type IProfileOverride,
|
|
9
|
+
type ISummaryModelOverride,
|
|
10
|
+
type ISummaryThinkingLevel,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
|
|
13
|
+
const POLICY_SECTIONS = new Set(["trigger", "ui", "summary"]);
|
|
14
|
+
|
|
15
|
+
function parsePolicyValue(key: IPolicyKey, value: unknown): IParseResult<unknown> {
|
|
16
|
+
switch (key) {
|
|
17
|
+
case "trigger.builtinSkipMarginPercent":
|
|
18
|
+
return parsePercent(value);
|
|
19
|
+
case "trigger.maxTokens":
|
|
20
|
+
case "trigger.minTokens":
|
|
21
|
+
case "trigger.cooldownMs":
|
|
22
|
+
case "trigger.builtinReserveTokens":
|
|
23
|
+
return parseNonNegativeInt(value);
|
|
24
|
+
case "ui.name":
|
|
25
|
+
return parseUiName(value);
|
|
26
|
+
case "ui.quiet":
|
|
27
|
+
case "ui.showStatus":
|
|
28
|
+
case "ui.minimalStatus":
|
|
29
|
+
return parseBooleanLiteral(value);
|
|
30
|
+
case "summary.thinkingLevel":
|
|
31
|
+
return parseSummaryThinkingLevel(value);
|
|
32
|
+
case "summary.preservationInstruction":
|
|
33
|
+
return parseInstructionText(value);
|
|
34
|
+
default:
|
|
35
|
+
return { ok: false, error: `Unsupported policy key: ${key}` };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parsePercent(value: unknown): IParseResult<number> {
|
|
40
|
+
const numeric = parseNumberLike(value);
|
|
41
|
+
if (!numeric.ok) {return numeric;}
|
|
42
|
+
if (numeric.value < 0 || numeric.value > 100) {
|
|
43
|
+
return { ok: false, error: "expected percent in [0,100]" };
|
|
44
|
+
}
|
|
45
|
+
return numeric;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseNonNegativeInt(value: unknown): IParseResult<number> {
|
|
49
|
+
if (typeof value === "number") {
|
|
50
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
51
|
+
return { ok: false, error: "expected non-negative integer" };
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, value };
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === "string") {
|
|
56
|
+
if (!/^\d+$/.test(value)) {
|
|
57
|
+
return { ok: false, error: "expected base-10 non-negative integer" };
|
|
58
|
+
}
|
|
59
|
+
return { ok: true, value: Number(value) };
|
|
60
|
+
}
|
|
61
|
+
return { ok: false, error: "expected non-negative integer" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parseModelSelector(value: unknown): IParseResult<string> {
|
|
65
|
+
if (typeof value !== "string") {
|
|
66
|
+
return { ok: false, error: "expected model selector provider/modelId" };
|
|
67
|
+
}
|
|
68
|
+
if (value.trim() !== value) {
|
|
69
|
+
return { ok: false, error: "expected model selector provider/modelId" };
|
|
70
|
+
}
|
|
71
|
+
const slashIndex = value.indexOf("/");
|
|
72
|
+
if (slashIndex <= 0 || slashIndex >= value.length - 1) {
|
|
73
|
+
return { ok: false, error: "expected model selector provider/modelId" };
|
|
74
|
+
}
|
|
75
|
+
const provider = value.slice(0, slashIndex);
|
|
76
|
+
const modelId = value.slice(slashIndex + 1);
|
|
77
|
+
if (/\s/.test(provider) || /\s/.test(modelId)) {
|
|
78
|
+
return { ok: false, error: "expected model selector provider/modelId" };
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, value };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseModelEntry(value: unknown): IParseResult<IModelEntry> {
|
|
84
|
+
if (typeof value === "string") {
|
|
85
|
+
const parsed = parseModelSelector(value);
|
|
86
|
+
if (!parsed.ok) {return parsed;}
|
|
87
|
+
return { ok: true, value: { model: parsed.value } };
|
|
88
|
+
}
|
|
89
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
90
|
+
return { ok: false, error: "expected model selector string or { model, ...overrides } object" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const raw = value as Record<string, unknown>;
|
|
94
|
+
if (!("model" in raw)) {
|
|
95
|
+
return { ok: false, error: 'model entry missing required "model" field' };
|
|
96
|
+
}
|
|
97
|
+
const parsedModel = parseModelSelector(raw.model);
|
|
98
|
+
if (!parsedModel.ok) {return { ok: false, error: `model entry: ${parsedModel.error}` };}
|
|
99
|
+
|
|
100
|
+
const entry: IModelEntry = { model: parsedModel.value };
|
|
101
|
+
const knownKeys = new Set(["model", "thinkingLevel", "preservationInstruction"]);
|
|
102
|
+
for (const key of Object.keys(raw)) {
|
|
103
|
+
if (!knownKeys.has(key)) {
|
|
104
|
+
return { ok: false, error: `model entry: unknown key "${key}"` };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if ("thinkingLevel" in raw && raw.thinkingLevel !== undefined) {
|
|
109
|
+
const parsed = parseSummaryThinkingLevel(raw.thinkingLevel);
|
|
110
|
+
if (!parsed.ok) {return { ok: false, error: `model entry thinkingLevel: ${parsed.error}` };}
|
|
111
|
+
entry.thinkingLevel = parsed.value;
|
|
112
|
+
}
|
|
113
|
+
if ("preservationInstruction" in raw && raw.preservationInstruction !== undefined) {
|
|
114
|
+
const parsed = parseInstructionText(raw.preservationInstruction);
|
|
115
|
+
if (!parsed.ok) {return { ok: false, error: `model entry preservationInstruction: ${parsed.error}` };}
|
|
116
|
+
entry.preservationInstruction = parsed.value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { ok: true, value: entry };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseModelSelectorList(value: unknown): IParseResult<IModelEntry[]> {
|
|
123
|
+
if (!Array.isArray(value)) {
|
|
124
|
+
return { ok: false, error: "expected array of model entries" };
|
|
125
|
+
}
|
|
126
|
+
if (value.length === 0) {
|
|
127
|
+
return { ok: false, error: "models array must not be empty" };
|
|
128
|
+
}
|
|
129
|
+
const entries: IModelEntry[] = [];
|
|
130
|
+
for (const item of value) {
|
|
131
|
+
const parsed = parseModelEntry(item);
|
|
132
|
+
if (!parsed.ok) {return parsed;}
|
|
133
|
+
entries.push(parsed.value);
|
|
134
|
+
}
|
|
135
|
+
return { ok: true, value: entries };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function parseSummaryThinkingLevel(value: unknown): IParseResult<ISummaryThinkingLevel> {
|
|
139
|
+
if (value === "off" || value === "low" || value === "medium" || value === "high") {
|
|
140
|
+
return { ok: true, value };
|
|
141
|
+
}
|
|
142
|
+
return { ok: false, error: "expected one of: off, low, medium, high" };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseInstructionText(value: unknown): IParseResult<string> {
|
|
146
|
+
if (typeof value !== "string") {
|
|
147
|
+
return { ok: false, error: "expected instruction string" };
|
|
148
|
+
}
|
|
149
|
+
if (value.trim() !== value) {
|
|
150
|
+
return { ok: false, error: "expected instruction string without surrounding whitespace" };
|
|
151
|
+
}
|
|
152
|
+
return { ok: true, value };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseUiName(value: unknown): IParseResult<string> {
|
|
156
|
+
const parsed = parseInstructionText(value);
|
|
157
|
+
if (!parsed.ok) {return parsed;}
|
|
158
|
+
if (parsed.value.length === 0) {
|
|
159
|
+
return { ok: false, error: "expected non-empty status name" };
|
|
160
|
+
}
|
|
161
|
+
return parsed;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseSummaryModelOverride(value: unknown): IParseResult<ISummaryModelOverride> {
|
|
165
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
166
|
+
return { ok: false, error: "expected object" };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const override: ISummaryModelOverride = {};
|
|
170
|
+
for (const [field, fieldValue] of Object.entries(value)) {
|
|
171
|
+
switch (field) {
|
|
172
|
+
case "thinkingLevel": {
|
|
173
|
+
const parsed = parseSummaryThinkingLevel(fieldValue);
|
|
174
|
+
if (!parsed.ok) {return { ok: false, error: `Invalid thinkingLevel: ${parsed.error}` };}
|
|
175
|
+
override.thinkingLevel = parsed.value;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case "preservationInstruction": {
|
|
179
|
+
const parsed = parseInstructionText(fieldValue);
|
|
180
|
+
if (!parsed.ok) {return { ok: false, error: `Invalid preservationInstruction: ${parsed.error}` };}
|
|
181
|
+
override.preservationInstruction = parsed.value;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
default:
|
|
185
|
+
return { ok: false, error: `Unknown key: ${field}` };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { ok: true, value: override };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function parseProfileOverride(name: string, value: unknown): IParseResult<IProfileOverride> {
|
|
193
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
194
|
+
return { ok: false, error: `profiles.${name}: expected object` };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const raw = value as Record<string, unknown>;
|
|
198
|
+
if (!("match" in raw)) {
|
|
199
|
+
return { ok: false, error: `profiles.${name}: missing required "match" field` };
|
|
200
|
+
}
|
|
201
|
+
const parsedMatch = parseModelSelector(raw.match);
|
|
202
|
+
if (!parsedMatch.ok) {
|
|
203
|
+
return { ok: false, error: `profiles.${name}.match: ${parsedMatch.error}` };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const profile: IProfileOverride = { match: parsedMatch.value };
|
|
207
|
+
|
|
208
|
+
if ("trigger" in raw && raw.trigger !== undefined) {
|
|
209
|
+
if (typeof raw.trigger !== "object" || raw.trigger === null || Array.isArray(raw.trigger)) {
|
|
210
|
+
return { ok: false, error: `profiles.${name}.trigger: expected object` };
|
|
211
|
+
}
|
|
212
|
+
const triggerOverride: Partial<Record<string, unknown>> = {};
|
|
213
|
+
for (const [field, fieldValue] of Object.entries(raw.trigger)) {
|
|
214
|
+
const fullKey = `trigger.${field}`;
|
|
215
|
+
if (!POLICY_KEYS.includes(fullKey as IPolicyKey)) {
|
|
216
|
+
return { ok: false, error: `profiles.${name}: unknown trigger key: ${field}` };
|
|
217
|
+
}
|
|
218
|
+
const parsed = parsePolicyValue(fullKey as IPolicyKey, fieldValue);
|
|
219
|
+
if (!parsed.ok) {
|
|
220
|
+
return { ok: false, error: `profiles.${name}.trigger.${field}: ${parsed.error}` };
|
|
221
|
+
}
|
|
222
|
+
triggerOverride[field] = parsed.value;
|
|
223
|
+
}
|
|
224
|
+
profile.trigger = triggerOverride as IProfileOverride["trigger"];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if ("models" in raw && raw.models !== undefined) {
|
|
228
|
+
const parsedModels = parseModelSelectorList(raw.models);
|
|
229
|
+
if (!parsedModels.ok) {
|
|
230
|
+
return { ok: false, error: `profiles.${name}.models: ${parsedModels.error}` };
|
|
231
|
+
}
|
|
232
|
+
profile.models = parsedModels.value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if ("summary" in raw && raw.summary !== undefined) {
|
|
236
|
+
const parsedSummary = parseSummaryModelOverride(raw.summary);
|
|
237
|
+
if (!parsedSummary.ok) {
|
|
238
|
+
return { ok: false, error: `profiles.${name}.summary: ${parsedSummary.error}` };
|
|
239
|
+
}
|
|
240
|
+
profile.summary = parsedSummary.value;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if ("template" in raw && raw.template !== undefined) {
|
|
244
|
+
if (typeof raw.template !== "string" || !raw.template.trim() || raw.template.trim() !== raw.template) {
|
|
245
|
+
return { ok: false, error: `profiles.${name}.template: expected non-empty path string without surrounding whitespace` };
|
|
246
|
+
}
|
|
247
|
+
profile.template = raw.template;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if ("updateTemplate" in raw && raw.updateTemplate !== undefined) {
|
|
251
|
+
if (typeof raw.updateTemplate !== "string" || !raw.updateTemplate.trim() || raw.updateTemplate.trim() !== raw.updateTemplate) {
|
|
252
|
+
return { ok: false, error: `profiles.${name}.updateTemplate: expected non-empty path string without surrounding whitespace` };
|
|
253
|
+
}
|
|
254
|
+
profile.updateTemplate = raw.updateTemplate;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const knownKeys = new Set(["match", "trigger", "models", "summary", "template", "updateTemplate"]);
|
|
258
|
+
for (const key of Object.keys(raw)) {
|
|
259
|
+
if (!knownKeys.has(key)) {
|
|
260
|
+
return { ok: false, error: `profiles.${name}: unknown key: ${key}` };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { ok: true, value: profile };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function parseProfiles(value: unknown): IParseResult<Record<string, IProfileOverride>> {
|
|
268
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
269
|
+
return { ok: false, error: "profiles: expected object" };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const profiles: Record<string, IProfileOverride> = {};
|
|
273
|
+
for (const [name, profileValue] of Object.entries(value)) {
|
|
274
|
+
const parsed = parseProfileOverride(name, profileValue);
|
|
275
|
+
if (!parsed.ok) {return parsed;}
|
|
276
|
+
profiles[name] = parsed.value;
|
|
277
|
+
}
|
|
278
|
+
return { ok: true, value: profiles };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function parseBooleanLiteral(value: unknown): IParseResult<boolean> {
|
|
282
|
+
if (value === true || value === "true") {return { ok: true, value: true };}
|
|
283
|
+
if (value === false || value === "false") {return { ok: true, value: false };}
|
|
284
|
+
return { ok: false, error: "expected literal true or false" };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function parseNumberLike(value: unknown): IParseResult<number> {
|
|
288
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
289
|
+
return { ok: true, value };
|
|
290
|
+
}
|
|
291
|
+
if (typeof value === "string") {
|
|
292
|
+
const trimmed = value.trim();
|
|
293
|
+
if (!trimmed) {
|
|
294
|
+
return { ok: false, error: "expected number" };
|
|
295
|
+
}
|
|
296
|
+
const parsed = Number(trimmed);
|
|
297
|
+
if (!Number.isFinite(parsed)) {
|
|
298
|
+
return { ok: false, error: "expected number" };
|
|
299
|
+
}
|
|
300
|
+
return { ok: true, value: parsed };
|
|
301
|
+
}
|
|
302
|
+
return { ok: false, error: "expected number" };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function parsePolicyPatch(input: unknown): IParseResult<ICompactionPolicyPatch> {
|
|
306
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
307
|
+
return { ok: false, error: "Policy patch must be an object" };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const patch: ICompactionPolicyPatch = {};
|
|
311
|
+
for (const [sectionKey, sectionValue] of Object.entries(input)) {
|
|
312
|
+
if (sectionKey === "enabled") {
|
|
313
|
+
const parsedEnabled = parseBooleanLiteral(sectionValue);
|
|
314
|
+
if (!parsedEnabled.ok) {
|
|
315
|
+
return { ok: false, error: `Invalid enabled: ${parsedEnabled.error}` };
|
|
316
|
+
}
|
|
317
|
+
patch.enabled = parsedEnabled.value;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (sectionKey === "models") {
|
|
321
|
+
const parsedModels = parseModelSelectorList(sectionValue);
|
|
322
|
+
if (!parsedModels.ok) {
|
|
323
|
+
return { ok: false, error: `Invalid models: ${parsedModels.error}` };
|
|
324
|
+
}
|
|
325
|
+
patch.models = parsedModels.value;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (sectionKey === "profiles") {
|
|
329
|
+
const parsedProfiles = parseProfiles(sectionValue);
|
|
330
|
+
if (!parsedProfiles.ok) {return parsedProfiles;}
|
|
331
|
+
patch.profiles = parsedProfiles.value;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (!POLICY_SECTIONS.has(sectionKey)) {
|
|
335
|
+
return { ok: false, error: `Unknown policy key: ${sectionKey}` };
|
|
336
|
+
}
|
|
337
|
+
if (typeof sectionValue !== "object" || sectionValue === null || Array.isArray(sectionValue)) {
|
|
338
|
+
return { ok: false, error: `Policy section must be an object: ${sectionKey}` };
|
|
339
|
+
}
|
|
340
|
+
for (const [field, value] of Object.entries(sectionValue)) {
|
|
341
|
+
const fullKey = `${sectionKey}.${field}`;
|
|
342
|
+
if (!POLICY_KEYS.includes(fullKey as IPolicyKey)) {
|
|
343
|
+
return { ok: false, error: `Unknown policy key: ${fullKey}` };
|
|
344
|
+
}
|
|
345
|
+
const parsedValue = parsePolicyValue(fullKey as IPolicyKey, value);
|
|
346
|
+
if (!parsedValue.ok) {
|
|
347
|
+
return { ok: false, error: `Invalid ${fullKey}: ${parsedValue.error}` };
|
|
348
|
+
}
|
|
349
|
+
setPatchValue(patch, fullKey as IPolicyKey, parsedValue.value);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { ok: true, value: patch };
|
|
354
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
2
|
+
import type { ContextUsage } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
export type ISummaryThinkingLevel = "off" | "low" | "medium" | "high";
|
|
5
|
+
|
|
6
|
+
export type IPolicyKey =
|
|
7
|
+
| "trigger.maxTokens"
|
|
8
|
+
| "trigger.minTokens"
|
|
9
|
+
| "trigger.cooldownMs"
|
|
10
|
+
| "trigger.builtinReserveTokens"
|
|
11
|
+
| "trigger.builtinSkipMarginPercent"
|
|
12
|
+
| "ui.name"
|
|
13
|
+
| "ui.quiet"
|
|
14
|
+
| "ui.showStatus"
|
|
15
|
+
| "ui.minimalStatus"
|
|
16
|
+
| "summary.thinkingLevel"
|
|
17
|
+
| "summary.preservationInstruction";
|
|
18
|
+
|
|
19
|
+
export interface ISummaryModelOverride {
|
|
20
|
+
thinkingLevel?: ISummaryThinkingLevel;
|
|
21
|
+
preservationInstruction?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface IModelEntry extends ISummaryModelOverride {
|
|
25
|
+
model: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ISummaryPolicy {
|
|
29
|
+
thinkingLevel: ISummaryThinkingLevel;
|
|
30
|
+
preservationInstruction: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ICompactionPolicy {
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
trigger: {
|
|
36
|
+
maxTokens?: number;
|
|
37
|
+
minTokens: number;
|
|
38
|
+
cooldownMs: number;
|
|
39
|
+
builtinReserveTokens: number;
|
|
40
|
+
builtinSkipMarginPercent: number;
|
|
41
|
+
};
|
|
42
|
+
models: IModelEntry[];
|
|
43
|
+
ui: {
|
|
44
|
+
name: string;
|
|
45
|
+
quiet: boolean;
|
|
46
|
+
showStatus: boolean;
|
|
47
|
+
minimalStatus: boolean;
|
|
48
|
+
};
|
|
49
|
+
summary: ISummaryPolicy;
|
|
50
|
+
profiles?: Record<string, IProfileOverride>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ICompactionPolicyPatch {
|
|
54
|
+
enabled?: boolean;
|
|
55
|
+
trigger?: Partial<ICompactionPolicy["trigger"]>;
|
|
56
|
+
models?: IModelEntry[];
|
|
57
|
+
ui?: Partial<ICompactionPolicy["ui"]>;
|
|
58
|
+
summary?: Partial<ICompactionPolicy["summary"]>;
|
|
59
|
+
profiles?: Record<string, IProfileOverride>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type IParseResult<T> = { ok: true; value: T } | { ok: false; error: string };
|
|
63
|
+
|
|
64
|
+
export interface IProactiveTriggerInput {
|
|
65
|
+
lastAssistantMessage: AssistantMessage | undefined;
|
|
66
|
+
usage: ContextUsage | undefined;
|
|
67
|
+
inFlight: boolean;
|
|
68
|
+
nowMs: number;
|
|
69
|
+
lastProactiveAtMs: number | undefined;
|
|
70
|
+
policy: ICompactionPolicy;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ITemplateResolution {
|
|
74
|
+
template?: string;
|
|
75
|
+
updateTemplate?: string;
|
|
76
|
+
resolvedPath?: string;
|
|
77
|
+
updateResolvedPath?: string;
|
|
78
|
+
fallbackReason?: string;
|
|
79
|
+
updateFallbackReason?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface IProfileOverride {
|
|
83
|
+
match: string;
|
|
84
|
+
trigger?: Partial<ICompactionPolicy["trigger"]>;
|
|
85
|
+
models?: IModelEntry[];
|
|
86
|
+
summary?: ISummaryModelOverride;
|
|
87
|
+
template?: string;
|
|
88
|
+
updateTemplate?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ICompactionDetails {
|
|
92
|
+
readFiles: string[];
|
|
93
|
+
modifiedFiles: string[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const CONFIG_FILE = ".pi/compaction-policy.json";
|
|
97
|
+
|
|
98
|
+
export const DEFAULT_POLICY: ICompactionPolicy = {
|
|
99
|
+
enabled: false,
|
|
100
|
+
trigger: {
|
|
101
|
+
minTokens: 100_000,
|
|
102
|
+
cooldownMs: 60_000,
|
|
103
|
+
builtinReserveTokens: 16_384,
|
|
104
|
+
builtinSkipMarginPercent: 5,
|
|
105
|
+
},
|
|
106
|
+
models: [{ model: "openai-codex/gpt-5.3-codex" }],
|
|
107
|
+
ui: {
|
|
108
|
+
name: "compact",
|
|
109
|
+
quiet: false,
|
|
110
|
+
showStatus: true,
|
|
111
|
+
minimalStatus: false,
|
|
112
|
+
},
|
|
113
|
+
summary: {
|
|
114
|
+
thinkingLevel: "low",
|
|
115
|
+
preservationInstruction: "Preserve exact file paths, function names, and error messages.",
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const POLICY_KEYS: IPolicyKey[] = [
|
|
120
|
+
"trigger.maxTokens",
|
|
121
|
+
"trigger.minTokens",
|
|
122
|
+
"trigger.cooldownMs",
|
|
123
|
+
"trigger.builtinReserveTokens",
|
|
124
|
+
"trigger.builtinSkipMarginPercent",
|
|
125
|
+
"ui.name",
|
|
126
|
+
"ui.quiet",
|
|
127
|
+
"ui.showStatus",
|
|
128
|
+
"ui.minimalStatus",
|
|
129
|
+
"summary.thinkingLevel",
|
|
130
|
+
"summary.preservationInstruction",
|
|
131
|
+
];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import type { Api, AssistantMessage, Model } from "@mariozechner/pi-ai";
|
|
3
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import { parseModelSelector } from "../policy/parse.js";
|
|
5
|
+
import type { ICompactionPolicy, IModelEntry, IParseResult } from "../policy/types.js";
|
|
6
|
+
|
|
7
|
+
type INotifyFn = (
|
|
8
|
+
ctx: ExtensionContext,
|
|
9
|
+
policy: ICompactionPolicy,
|
|
10
|
+
level: "info" | "warning" | "error",
|
|
11
|
+
message: string,
|
|
12
|
+
options?: { critical?: boolean; dedupeKey?: string },
|
|
13
|
+
) => boolean;
|
|
14
|
+
|
|
15
|
+
function parseSelector(selector: string): IParseResult<{ provider: string; modelId: string }> {
|
|
16
|
+
const parsed = parseModelSelector(selector);
|
|
17
|
+
if (!parsed.ok) {return parsed;}
|
|
18
|
+
const slashIndex = parsed.value.indexOf("/");
|
|
19
|
+
return {
|
|
20
|
+
ok: true,
|
|
21
|
+
value: {
|
|
22
|
+
provider: parsed.value.slice(0, slashIndex),
|
|
23
|
+
modelId: parsed.value.slice(slashIndex + 1),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getLastAssistantMessage(messages: AgentMessage[]): AssistantMessage | undefined {
|
|
29
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
30
|
+
const candidate = messages[index];
|
|
31
|
+
if (candidate?.role === "assistant") {return candidate;}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function tryResolveModel(
|
|
37
|
+
ctx: ExtensionContext,
|
|
38
|
+
selector: string,
|
|
39
|
+
): Promise<{ model: Model<Api>; apiKey: string } | undefined> {
|
|
40
|
+
const parts = parseSelector(selector);
|
|
41
|
+
if (!parts.ok) {return undefined;}
|
|
42
|
+
|
|
43
|
+
const model = ctx.modelRegistry.find(parts.value.provider, parts.value.modelId);
|
|
44
|
+
if (!model) {return undefined;}
|
|
45
|
+
|
|
46
|
+
let apiKey: string | undefined;
|
|
47
|
+
try {
|
|
48
|
+
apiKey = await ctx.modelRegistry.getApiKey(model);
|
|
49
|
+
} catch {
|
|
50
|
+
// Caller iterates models and reports the full list if all fail
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if (!apiKey) {return undefined;}
|
|
54
|
+
|
|
55
|
+
return { model, apiKey };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function resolveSummaryModel(
|
|
59
|
+
ctx: ExtensionContext,
|
|
60
|
+
policy: ICompactionPolicy,
|
|
61
|
+
notify: INotifyFn,
|
|
62
|
+
): Promise<{ entry: IModelEntry; model: Model<Api>; apiKey: string } | undefined> {
|
|
63
|
+
for (const entry of policy.models) {
|
|
64
|
+
const resolved = await tryResolveModel(ctx, entry.model);
|
|
65
|
+
if (resolved) {return { entry, model: resolved.model, apiKey: resolved.apiKey };}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tried = policy.models.map((e) => e.model).join(", ");
|
|
69
|
+
notify(
|
|
70
|
+
ctx,
|
|
71
|
+
policy,
|
|
72
|
+
"warning",
|
|
73
|
+
`No compaction models could be resolved (tried: ${tried}). Falling back to default compaction.`,
|
|
74
|
+
{ dedupeKey: `no-models:${tried}` },
|
|
75
|
+
);
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { applyProfileOverrides } from "../policy/merge.js";
|
|
2
|
+
import type { ICompactionPolicy, IProfileOverride, IProactiveTriggerInput } from "../policy/types.js";
|
|
3
|
+
|
|
4
|
+
export function resolveEffectivePolicy(
|
|
5
|
+
ctx: { model?: { provider: string; id: string } },
|
|
6
|
+
basePolicy: ICompactionPolicy,
|
|
7
|
+
): {
|
|
8
|
+
policy: ICompactionPolicy;
|
|
9
|
+
profileName: string | undefined;
|
|
10
|
+
sessionModel: string | undefined;
|
|
11
|
+
profileTemplates?: { template?: string; updateTemplate?: string };
|
|
12
|
+
} {
|
|
13
|
+
const sessionModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
|
|
14
|
+
const profile = findMatchingProfile(basePolicy.profiles, sessionModel);
|
|
15
|
+
const policy = profile ? applyProfileOverrides(basePolicy, profile.override) : basePolicy;
|
|
16
|
+
const profileTemplates = profile?.override.template || profile?.override.updateTemplate
|
|
17
|
+
? { template: profile.override.template, updateTemplate: profile.override.updateTemplate }
|
|
18
|
+
: undefined;
|
|
19
|
+
return { policy, profileName: profile?.name, sessionModel, profileTemplates };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function shouldTriggerProactiveCompact(input: IProactiveTriggerInput): boolean {
|
|
23
|
+
const { lastAssistantMessage, usage, inFlight, nowMs, lastProactiveAtMs, policy } = input;
|
|
24
|
+
if (!lastAssistantMessage) {return false;}
|
|
25
|
+
if (lastAssistantMessage.stopReason === "error" || lastAssistantMessage.stopReason === "aborted") {return false;}
|
|
26
|
+
if (!usage) {return false;}
|
|
27
|
+
if (usage.tokens === null || usage.percent === null) {return false;}
|
|
28
|
+
if (inFlight) {return false;}
|
|
29
|
+
if (typeof lastProactiveAtMs === "number" && nowMs - lastProactiveAtMs < policy.trigger.cooldownMs) {return false;}
|
|
30
|
+
const { maxTokens } = policy.trigger;
|
|
31
|
+
if (maxTokens === undefined || maxTokens <= 0) {return false;}
|
|
32
|
+
if (usage.tokens < policy.trigger.minTokens) {return false;}
|
|
33
|
+
if (usage.tokens < maxTokens) {return false;}
|
|
34
|
+
|
|
35
|
+
const builtinPercentRaw =
|
|
36
|
+
usage.contextWindow > 0 ? 100 * (1 - policy.trigger.builtinReserveTokens / usage.contextWindow) : 100;
|
|
37
|
+
const builtinPercent = Math.max(0, Math.min(100, builtinPercentRaw));
|
|
38
|
+
if (usage.percent >= builtinPercent - policy.trigger.builtinSkipMarginPercent) {return false;}
|
|
39
|
+
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function findMatchingProfile(
|
|
44
|
+
profiles: Record<string, IProfileOverride> | undefined,
|
|
45
|
+
modelSelector: string | undefined,
|
|
46
|
+
): { name: string; override: IProfileOverride } | undefined {
|
|
47
|
+
if (!profiles || !modelSelector) {return undefined;}
|
|
48
|
+
for (const name of Object.keys(profiles).sort()) {
|
|
49
|
+
const profile = profiles[name];
|
|
50
|
+
if (profile && profile.match === modelSelector) {
|
|
51
|
+
return { name, override: profile };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|