@docyrus/docyrus 0.0.21 → 0.0.23
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 +237 -11
- package/main.js.map +4 -4
- package/package.json +9 -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-bash-live-view/LICENSE +21 -0
- package/resources/pi-agent/extensions/pi-bash-live-view/README.md +19 -0
- package/resources/pi-agent/extensions/pi-bash-live-view/index.ts +52 -0
- package/resources/pi-agent/extensions/pi-bash-live-view/package.json +61 -0
- package/resources/pi-agent/extensions/pi-bash-live-view/pty-execute.ts +97 -0
- package/resources/pi-agent/extensions/pi-bash-live-view/pty-kill.ts +25 -0
- package/resources/pi-agent/extensions/pi-bash-live-view/pty-session.ts +143 -0
- package/resources/pi-agent/extensions/pi-bash-live-view/spawn-helper.ts +31 -0
- package/resources/pi-agent/extensions/pi-bash-live-view/terminal-emulator.ts +439 -0
- package/resources/pi-agent/extensions/pi-bash-live-view/truncate.ts +68 -0
- package/resources/pi-agent/extensions/pi-bash-live-view/widget.ts +114 -0
- 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 +3841 -0
- package/server-loader.js.map +7 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
<img src="banner.png" alt="pi-custom-compaction" width="1100">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# pi-custom-compaction
|
|
6
|
+
|
|
7
|
+
Swap the model and template Pi uses for compaction. Optionally trigger compaction at a specific token count.
|
|
8
|
+
|
|
9
|
+
Once enabled, the extension intercepts every compaction — whether triggered by Pi's built-in or by the extension itself — and uses your configured model and template to generate the summary. If all configured models fail to resolve, it falls back to Pi's built-in compaction silently.
|
|
10
|
+
|
|
11
|
+
Off by default. Pi's built-in compaction works normally until you enable it.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pi install npm:pi-custom-compaction
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
Create `~/.pi/agent/compaction-policy.json` (global) or `<project>/.pi/compaction-policy.json` (project, takes priority):
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"enabled": true,
|
|
26
|
+
"models": [
|
|
27
|
+
{ "model": "anthropic/claude-sonnet-4", "thinkingLevel": "medium" }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Run `/reload`. Done. Pi still decides when to compact — you just swapped the model.
|
|
33
|
+
|
|
34
|
+
To also control **when** it triggers:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"enabled": true,
|
|
39
|
+
"trigger": { "maxTokens": 350000 },
|
|
40
|
+
"ui": { "name": "ctx" },
|
|
41
|
+
"models": [
|
|
42
|
+
{ "model": "anthropic/claude-haiku-4-5", "thinkingLevel": "medium" },
|
|
43
|
+
{ "model": "openai-codex/gpt-5.3-codex", "thinkingLevel": "low" }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Status bar shows: `ctx · 24.7% (86426/350000)`
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
| Command | What it does |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `/compact-policy` | Shows effective config (models, trigger, profile, template) |
|
|
55
|
+
| `/compact-now [focus]` | Triggers compaction immediately |
|
|
56
|
+
|
|
57
|
+
## Models
|
|
58
|
+
|
|
59
|
+
Ordered fallback chain. Tries each model in order, uses the first one that resolves. If credits run out on one, the next takes over.
|
|
60
|
+
|
|
61
|
+
Plain strings inherit base `summary` settings. Objects let you override per model:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
"models": [
|
|
65
|
+
{ "model": "anthropic/claude-sonnet-4", "thinkingLevel": "medium" },
|
|
66
|
+
"openai-codex/gpt-5.3-codex"
|
|
67
|
+
]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Trigger
|
|
71
|
+
|
|
72
|
+
| Key | What it does | Default |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| `maxTokens` | Compact at this token count. Omit to let Pi decide. | — |
|
|
75
|
+
| `minTokens` | Won't trigger below this count | 100000 |
|
|
76
|
+
| `cooldownMs` | Min time between proactive compactions | 60000 |
|
|
77
|
+
|
|
78
|
+
`maxTokens` makes compaction happen sooner. Without it, Pi waits until the context window is almost full (~984K on a 1M model). With `maxTokens: 350000`, compaction fires at 350K instead.
|
|
79
|
+
|
|
80
|
+
## How maxTokens interacts with the context window
|
|
81
|
+
|
|
82
|
+
You don't need to tune `maxTokens` per model. One config works across models with different context sizes:
|
|
83
|
+
|
|
84
|
+
- **Well below context window** (350K on a 1M model) — extension fires at 350K, plenty of room for the summary.
|
|
85
|
+
- **Close to context window** (260K on a 272K model) — extension backs off and lets Pi's built-in fire at 256K with its own reserve, preventing double-compaction.
|
|
86
|
+
- **Larger than context window** (350K on a 272K model) — proactive trigger can never fire, Pi handles it at 256K.
|
|
87
|
+
|
|
88
|
+
<details>
|
|
89
|
+
<summary>Advanced trigger tuning</summary>
|
|
90
|
+
|
|
91
|
+
| Key | What it does | Default |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| `builtinReserveTokens` | Tokens Pi's built-in reserves before triggering | 16384 |
|
|
94
|
+
| `builtinSkipMarginPercent` | Skip if Pi's builtin would trigger within this margin | 5 |
|
|
95
|
+
|
|
96
|
+
</details>
|
|
97
|
+
|
|
98
|
+
## Profiles
|
|
99
|
+
|
|
100
|
+
Override trigger, models, and summary settings per session model:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
"profiles": {
|
|
104
|
+
"opus-large-ctx": {
|
|
105
|
+
"match": "anthropic/claude-opus-4-6",
|
|
106
|
+
"trigger": { "maxTokens": 500000 },
|
|
107
|
+
"models": [{ "model": "anthropic/claude-haiku-4-5", "thinkingLevel": "medium" }],
|
|
108
|
+
"template": "~/.pi/agent/templates/opus.md",
|
|
109
|
+
"updateTemplate": "~/.pi/agent/templates/opus-update.md"
|
|
110
|
+
},
|
|
111
|
+
"fast-codex": {
|
|
112
|
+
"match": "openai-codex/gpt-5.3-codex",
|
|
113
|
+
"models": ["openai-codex/gpt-5.3-codex"],
|
|
114
|
+
"summary": { "thinkingLevel": "low" }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`match` is the exact `provider/modelId` of the session model. First alphabetical match wins. Profile `models` replaces the base models list for that session. `template` and `updateTemplate` override template discovery with explicit paths (tilde-expanded). Active profile shows in the status bar.
|
|
120
|
+
|
|
121
|
+
## Templates (optional)
|
|
122
|
+
|
|
123
|
+
Without a template, Pi's built-in compaction format is used. To customize the summary structure, drop markdown files at convention paths — no config needed.
|
|
124
|
+
|
|
125
|
+
Two template types:
|
|
126
|
+
- **Initial** (`compaction-template.md`) — first compaction, brackets describe what to extract
|
|
127
|
+
- **Update** (`compaction-template-update.md`) — subsequent compactions, brackets describe how to merge (optional, falls back to initial)
|
|
128
|
+
|
|
129
|
+
Discovery order (first found wins):
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
Initial template:
|
|
133
|
+
<project>/.pi/compaction-templates/PROFILE.md
|
|
134
|
+
~/.pi/agent/compaction-templates/PROFILE.md
|
|
135
|
+
<project>/.pi/compaction-template.md
|
|
136
|
+
~/.pi/agent/compaction-template.md
|
|
137
|
+
|
|
138
|
+
Update template:
|
|
139
|
+
<project>/.pi/compaction-templates/PROFILE-update.md
|
|
140
|
+
~/.pi/agent/compaction-templates/PROFILE-update.md
|
|
141
|
+
<project>/.pi/compaction-template-update.md
|
|
142
|
+
~/.pi/agent/compaction-template-update.md
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Profile-specific paths are only checked when a profile is active. Template files are never modified — they're format definitions read on every compaction. The model fills in the brackets based on the conversation.
|
|
146
|
+
|
|
147
|
+
Example initial template (`compaction-template.md`):
|
|
148
|
+
|
|
149
|
+
```markdown
|
|
150
|
+
## Goal
|
|
151
|
+
[What is the user trying to accomplish? Can be multiple items if the session covers different tasks.]
|
|
152
|
+
|
|
153
|
+
## Constraints & Preferences
|
|
154
|
+
- [Any constraints, preferences, or requirements mentioned by user]
|
|
155
|
+
- [Or "(none)" if none were mentioned]
|
|
156
|
+
|
|
157
|
+
## Progress
|
|
158
|
+
### Done
|
|
159
|
+
- [x] [Completed tasks/changes]
|
|
160
|
+
|
|
161
|
+
### In Progress
|
|
162
|
+
- [ ] [Current work]
|
|
163
|
+
|
|
164
|
+
### Blocked
|
|
165
|
+
- [Issues preventing progress, if any]
|
|
166
|
+
|
|
167
|
+
## Key Decisions
|
|
168
|
+
- **[Decision]**: [Brief rationale]
|
|
169
|
+
|
|
170
|
+
## Next Steps
|
|
171
|
+
1. [Ordered list of what should happen next]
|
|
172
|
+
|
|
173
|
+
## Critical Context
|
|
174
|
+
- [Any data, examples, or references needed to continue]
|
|
175
|
+
- [Or "(none)" if not applicable]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Example update template (`compaction-template-update.md`):
|
|
179
|
+
|
|
180
|
+
```markdown
|
|
181
|
+
## Goal
|
|
182
|
+
[Preserve existing goals, add new ones if the task expanded]
|
|
183
|
+
|
|
184
|
+
## Constraints & Preferences
|
|
185
|
+
- [Preserve existing, add new ones discovered]
|
|
186
|
+
|
|
187
|
+
## Progress
|
|
188
|
+
### Done
|
|
189
|
+
- [x] [Include previously done items AND newly completed items]
|
|
190
|
+
|
|
191
|
+
### In Progress
|
|
192
|
+
- [ ] [Current work - update based on progress]
|
|
193
|
+
|
|
194
|
+
### Blocked
|
|
195
|
+
- [Current blockers - remove if resolved]
|
|
196
|
+
|
|
197
|
+
## Key Decisions
|
|
198
|
+
- **[Decision]**: [Brief rationale] (preserve all previous, add new)
|
|
199
|
+
|
|
200
|
+
## Next Steps
|
|
201
|
+
1. [Update based on current state]
|
|
202
|
+
|
|
203
|
+
## Critical Context
|
|
204
|
+
- [Preserve important context, add new if needed]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`summary.preservationInstruction` is appended as an extra directive after the template.
|
|
208
|
+
|
|
209
|
+
## UI options
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
"ui": {
|
|
213
|
+
"name": "ctx",
|
|
214
|
+
"showStatus": true,
|
|
215
|
+
"minimalStatus": false,
|
|
216
|
+
"quiet": false
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
| Key | What it does | Default |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| `name` | Status bar label | `"compact"` |
|
|
223
|
+
| `showStatus` | Show status bar | `true` |
|
|
224
|
+
| `minimalStatus` | Short format (just percentage) | `false` |
|
|
225
|
+
| `quiet` | Suppress non-critical notifications | `false` |
|
|
226
|
+
|
|
227
|
+
Status bar examples:
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
ctx · 24.7% (86426/350000)
|
|
231
|
+
ctx: opus-large-ctx · 50.1% (250500/500000)
|
|
232
|
+
ctx · 31%
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Summary options
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
"summary": {
|
|
239
|
+
"thinkingLevel": "medium",
|
|
240
|
+
"preservationInstruction": "Preserve exact file paths, function names, and error messages."
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
These are base settings. Model entries and profiles can override `thinkingLevel` and `preservationInstruction` individually.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Vendored from [`nicobailon/pi-custom-compaction`](https://github.com/nicobailon/pi-custom-compaction)
|
|
2
|
+
|
|
3
|
+
- Upstream commit: `ac69e833c4dc00b4961c65daa2807017bed86e8b`
|
|
4
|
+
- Vendored for: `docyrus agent` / `docyrus coder`
|
|
5
|
+
- Local adaptation:
|
|
6
|
+
- prefer `PI_CODING_AGENT_DIR` for global policy/template lookup so scoped Docyrus agent state works
|
|
Binary file
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { resolveEffectivePolicy } from "../runtime/pure.js";
|
|
3
|
+
import type { IRuntimeServices } from "../runtime/session-state.js";
|
|
4
|
+
import { discoverTemplate } from "../summary/template.js";
|
|
5
|
+
|
|
6
|
+
function formatModels(policy: { models: Array<{ model: string; thinkingLevel?: string }> }): string {
|
|
7
|
+
return policy.models
|
|
8
|
+
.map((e) => (e.thinkingLevel ? `${e.model} (thinking: ${e.thinkingLevel})` : e.model))
|
|
9
|
+
.join(" → ");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatTrigger(policy: {
|
|
13
|
+
trigger: { maxTokens?: number; minTokens: number; cooldownMs: number };
|
|
14
|
+
}): string {
|
|
15
|
+
const { maxTokens, minTokens, cooldownMs } = policy.trigger;
|
|
16
|
+
const threshold = maxTokens && maxTokens > 0 ? `${maxTokens} tokens` : "inherited from pi";
|
|
17
|
+
return `${threshold} | minTokens: ${minTokens} | cooldown: ${cooldownMs}ms`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function registerCommands(pi: ExtensionAPI, runtime: IRuntimeServices): void {
|
|
21
|
+
pi.registerCommand("compact-policy", {
|
|
22
|
+
description: "Show current compaction policy",
|
|
23
|
+
handler: async(_args, ctx: ExtensionCommandContext) => {
|
|
24
|
+
const basePolicy = runtime.loadEffectivePolicy(ctx);
|
|
25
|
+
const { policy, profileName, sessionModel, profileTemplates } = resolveEffectivePolicy(ctx, basePolicy);
|
|
26
|
+
const template = discoverTemplate(ctx.cwd, profileName, profileTemplates);
|
|
27
|
+
const templatePath = template.fallbackReason
|
|
28
|
+
? `${template.resolvedPath ?? "(unknown)"} (invalid: ${template.fallbackReason}; using built-in)`
|
|
29
|
+
: template.resolvedPath ?? "(none — using built-in)";
|
|
30
|
+
const updateTemplatePath = template.updateFallbackReason
|
|
31
|
+
? `${template.updateResolvedPath ?? "(unknown)"} (invalid: ${template.updateFallbackReason}; using initial template)`
|
|
32
|
+
: template.updateResolvedPath ?? "(none — using initial template)";
|
|
33
|
+
|
|
34
|
+
const lines = [
|
|
35
|
+
`enabled: ${policy.enabled}`,
|
|
36
|
+
`models: ${formatModels(policy)}`,
|
|
37
|
+
`trigger: ${formatTrigger(policy)}`,
|
|
38
|
+
`summary: thinking=${policy.summary.thinkingLevel}`,
|
|
39
|
+
`session model: ${sessionModel ?? "unknown"}`,
|
|
40
|
+
`profile: ${profileName ?? "none"}`,
|
|
41
|
+
`template: ${templatePath}`,
|
|
42
|
+
`template update: ${updateTemplatePath}`,
|
|
43
|
+
];
|
|
44
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
pi.registerCommand("compact-now", {
|
|
49
|
+
description: "Trigger compaction immediately",
|
|
50
|
+
handler: async(args, ctx: ExtensionCommandContext) => {
|
|
51
|
+
const basePolicy = runtime.loadEffectivePolicy(ctx);
|
|
52
|
+
const { policy, profileName } = resolveEffectivePolicy(ctx, basePolicy);
|
|
53
|
+
runtime.setActiveProfileName(profileName);
|
|
54
|
+
|
|
55
|
+
const focus = args.trim() || undefined;
|
|
56
|
+
if (!policy.enabled) {
|
|
57
|
+
ctx.compact({ customInstructions: focus });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
runtime.triggerCompaction(ctx, policy, "compact-now", focus);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
2
|
+
import { compact, type ExtensionAPI, type ExtensionContext, type SessionBeforeCompactEvent } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import type { ISummaryPolicy } from "../policy/types.js";
|
|
4
|
+
import {
|
|
5
|
+
computeFileLists,
|
|
6
|
+
formatFileOperations,
|
|
7
|
+
generateTemplateSummary,
|
|
8
|
+
generateTurnPrefixSummary,
|
|
9
|
+
getReserveTokens,
|
|
10
|
+
} from "../summary/generate.js";
|
|
11
|
+
import { buildSummaryPrompt, discoverTemplate, resolveSummarySettings } from "../summary/template.js";
|
|
12
|
+
import { getLastAssistantMessage, resolveSummaryModel } from "../runtime/model-resolution.js";
|
|
13
|
+
import { resolveEffectivePolicy, shouldTriggerProactiveCompact } from "../runtime/pure.js";
|
|
14
|
+
import type { IRuntimeServices } from "../runtime/session-state.js";
|
|
15
|
+
|
|
16
|
+
async function generateCustomCompaction(
|
|
17
|
+
event: SessionBeforeCompactEvent,
|
|
18
|
+
template: string,
|
|
19
|
+
updateTemplate: string | undefined,
|
|
20
|
+
summarySettings: ISummaryPolicy,
|
|
21
|
+
model: Model<Api>,
|
|
22
|
+
apiKey: string,
|
|
23
|
+
) {
|
|
24
|
+
const reserveTokens = getReserveTokens(event);
|
|
25
|
+
const summaryPrompt = buildSummaryPrompt(
|
|
26
|
+
template,
|
|
27
|
+
updateTemplate,
|
|
28
|
+
event.preparation.previousSummary,
|
|
29
|
+
event.customInstructions,
|
|
30
|
+
summarySettings.preservationInstruction,
|
|
31
|
+
);
|
|
32
|
+
const historySummary =
|
|
33
|
+
event.preparation.messagesToSummarize.length > 0
|
|
34
|
+
? await generateTemplateSummary(
|
|
35
|
+
event.preparation.messagesToSummarize,
|
|
36
|
+
model,
|
|
37
|
+
apiKey,
|
|
38
|
+
summaryPrompt,
|
|
39
|
+
reserveTokens,
|
|
40
|
+
event.signal,
|
|
41
|
+
summarySettings.thinkingLevel,
|
|
42
|
+
event.preparation.previousSummary,
|
|
43
|
+
)
|
|
44
|
+
: event.preparation.previousSummary ?? "No prior history.";
|
|
45
|
+
if (!historySummary.trim()) {
|
|
46
|
+
throw new Error("Custom template summarization returned empty summary");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const turnPrefixSummary =
|
|
50
|
+
event.preparation.isSplitTurn && event.preparation.turnPrefixMessages.length > 0
|
|
51
|
+
? await generateTurnPrefixSummary(
|
|
52
|
+
event.preparation.turnPrefixMessages,
|
|
53
|
+
model,
|
|
54
|
+
apiKey,
|
|
55
|
+
reserveTokens,
|
|
56
|
+
event.signal,
|
|
57
|
+
summarySettings.thinkingLevel,
|
|
58
|
+
)
|
|
59
|
+
: undefined;
|
|
60
|
+
if (turnPrefixSummary !== undefined && !turnPrefixSummary.trim()) {
|
|
61
|
+
throw new Error("Turn prefix summarization returned empty summary");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const mergedSummary = turnPrefixSummary
|
|
65
|
+
? `${historySummary}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixSummary}`
|
|
66
|
+
: historySummary;
|
|
67
|
+
const details = computeFileLists(event.preparation.fileOps);
|
|
68
|
+
const summary = `${mergedSummary}${formatFileOperations(details)}`;
|
|
69
|
+
return {
|
|
70
|
+
summary,
|
|
71
|
+
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
72
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
73
|
+
details,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function initializeSessionStatus(ctx: ExtensionContext, runtime: IRuntimeServices): void {
|
|
78
|
+
const basePolicy = runtime.loadEffectivePolicy(ctx, { warnOnInvalidConfig: false });
|
|
79
|
+
if (!basePolicy.enabled) {return;}
|
|
80
|
+
const { policy, profileName } = resolveEffectivePolicy(ctx, basePolicy);
|
|
81
|
+
runtime.setActiveProfileName(profileName);
|
|
82
|
+
runtime.updateStatus(ctx, policy);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function registerEvents(pi: ExtensionAPI, runtime: IRuntimeServices): void {
|
|
86
|
+
pi.on("agent_end", async(event, ctx) => {
|
|
87
|
+
const basePolicy = runtime.loadEffectivePolicy(ctx, { warnOnInvalidConfig: false });
|
|
88
|
+
if (!basePolicy.enabled) {
|
|
89
|
+
runtime.clearInFlight();
|
|
90
|
+
runtime.setActiveProfileName(undefined);
|
|
91
|
+
runtime.updateStatus(ctx, basePolicy);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { policy, profileName } = resolveEffectivePolicy(ctx, basePolicy);
|
|
96
|
+
runtime.setActiveProfileName(profileName);
|
|
97
|
+
|
|
98
|
+
const shouldTrigger = shouldTriggerProactiveCompact({
|
|
99
|
+
lastAssistantMessage: getLastAssistantMessage(event.messages),
|
|
100
|
+
usage: ctx.getContextUsage(),
|
|
101
|
+
inFlight: runtime.isInFlight(),
|
|
102
|
+
nowMs: Date.now(),
|
|
103
|
+
lastProactiveAtMs: runtime.getLastProactiveAtMs(),
|
|
104
|
+
policy,
|
|
105
|
+
});
|
|
106
|
+
if (!shouldTrigger) {
|
|
107
|
+
runtime.updateStatus(ctx, policy);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const triggered = runtime.triggerCompaction(ctx, policy, "proactive");
|
|
112
|
+
if (triggered) {
|
|
113
|
+
runtime.setLastProactiveAtMs(Date.now());
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
pi.on("session_before_compact", async(event, ctx) => {
|
|
118
|
+
const basePolicy = runtime.loadEffectivePolicy(ctx, { warnOnInvalidConfig: false });
|
|
119
|
+
if (!basePolicy.enabled) {
|
|
120
|
+
runtime.clearInFlight();
|
|
121
|
+
runtime.setActiveProfileName(undefined);
|
|
122
|
+
runtime.updateStatus(ctx, basePolicy);
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { policy, profileName, profileTemplates } = resolveEffectivePolicy(ctx, basePolicy);
|
|
127
|
+
runtime.setActiveProfileName(profileName);
|
|
128
|
+
runtime.setInFlight("session_before_compact");
|
|
129
|
+
runtime.updateStatus(ctx, policy);
|
|
130
|
+
|
|
131
|
+
const resolved = await resolveSummaryModel(ctx, policy, runtime.notify);
|
|
132
|
+
if (!resolved) {
|
|
133
|
+
runtime.clearInFlight();
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const summarySettings = resolveSummarySettings(policy, resolved.entry);
|
|
138
|
+
const templateResolution = discoverTemplate(ctx.cwd, profileName, profileTemplates);
|
|
139
|
+
if (templateResolution.fallbackReason) {
|
|
140
|
+
runtime.notify(
|
|
141
|
+
ctx,
|
|
142
|
+
policy,
|
|
143
|
+
"warning",
|
|
144
|
+
`Could not load summary template ${templateResolution.resolvedPath} (${templateResolution.fallbackReason}). Falling back to built-in compaction format.`,
|
|
145
|
+
{ dedupeKey: `template-fallback:${templateResolution.resolvedPath}:${templateResolution.fallbackReason}` },
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (templateResolution.updateFallbackReason) {
|
|
149
|
+
runtime.notify(
|
|
150
|
+
ctx,
|
|
151
|
+
policy,
|
|
152
|
+
"warning",
|
|
153
|
+
`Could not load update summary template ${templateResolution.updateResolvedPath} (${templateResolution.updateFallbackReason}). Falling back to initial summary template.`,
|
|
154
|
+
{
|
|
155
|
+
dedupeKey: `template-update-fallback:${templateResolution.updateResolvedPath}:${templateResolution.updateFallbackReason}`,
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const result = templateResolution.template
|
|
162
|
+
? await generateCustomCompaction(
|
|
163
|
+
event,
|
|
164
|
+
templateResolution.template,
|
|
165
|
+
templateResolution.updateTemplate,
|
|
166
|
+
summarySettings,
|
|
167
|
+
resolved.model,
|
|
168
|
+
resolved.apiKey,
|
|
169
|
+
)
|
|
170
|
+
: await compact(
|
|
171
|
+
event.preparation,
|
|
172
|
+
resolved.model,
|
|
173
|
+
resolved.apiKey,
|
|
174
|
+
event.customInstructions,
|
|
175
|
+
event.signal,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
compaction: {
|
|
180
|
+
summary: result.summary,
|
|
181
|
+
tokensBefore: result.tokensBefore,
|
|
182
|
+
details: result.details,
|
|
183
|
+
firstKeptEntryId: result.firstKeptEntryId,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
runtime.clearInFlight();
|
|
188
|
+
if (event.signal.aborted) {return undefined;}
|
|
189
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
190
|
+
runtime.notify(ctx, policy, "error", `Compaction policy summary failed: ${message}`, {
|
|
191
|
+
critical: true,
|
|
192
|
+
});
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
pi.on("session_compact", async(_event, ctx) => {
|
|
198
|
+
runtime.clearInFlight();
|
|
199
|
+
runtime.markPostCompact();
|
|
200
|
+
const basePolicy = runtime.loadEffectivePolicy(ctx, { warnOnInvalidConfig: false });
|
|
201
|
+
const { policy, profileName } = resolveEffectivePolicy(ctx, basePolicy);
|
|
202
|
+
runtime.setActiveProfileName(profileName);
|
|
203
|
+
runtime.updateStatus(ctx, policy);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
pi.on("session_start", async(_event, ctx) => {
|
|
207
|
+
runtime.clearSessionScopedState(ctx);
|
|
208
|
+
initializeSessionStatus(ctx, runtime);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
pi.on("session_switch", async(_event, ctx) => {
|
|
212
|
+
runtime.clearSessionScopedState(ctx);
|
|
213
|
+
initializeSessionStatus(ctx, runtime);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
pi.on("session_fork", async(_event, ctx) => {
|
|
217
|
+
runtime.clearSessionScopedState(ctx);
|
|
218
|
+
initializeSessionStatus(ctx, runtime);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
pi.on("session_tree", async(_event, ctx) => {
|
|
222
|
+
runtime.clearSessionScopedState(ctx);
|
|
223
|
+
initializeSessionStatus(ctx, runtime);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
pi.on("session_shutdown", async(_event, ctx) => {
|
|
227
|
+
runtime.clearSessionScopedState(ctx);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { registerCommands } from "./commands/register-commands.js";
|
|
3
|
+
import { registerEvents } from "./events/register-events.js";
|
|
4
|
+
import { createRuntimeServices } from "./runtime/session-state.js";
|
|
5
|
+
|
|
6
|
+
export default function compactionPolicyExtension(pi: ExtensionAPI) {
|
|
7
|
+
const runtime = createRuntimeServices();
|
|
8
|
+
registerCommands(pi, runtime);
|
|
9
|
+
registerEvents(pi, runtime);
|
|
10
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-custom-compaction",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Custom compaction for pi — swap the model, template, and trigger for context compaction",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"pi-coding-agent",
|
|
9
|
+
"compaction",
|
|
10
|
+
"context-management",
|
|
11
|
+
"extension"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/nicobailon/pi-custom-compaction#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/nicobailon/pi-custom-compaction/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/nicobailon/pi-custom-compaction.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Nico Bailon",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"files": [
|
|
25
|
+
"index.ts",
|
|
26
|
+
"commands/register-commands.ts",
|
|
27
|
+
"events/register-events.ts",
|
|
28
|
+
"policy/config.ts",
|
|
29
|
+
"policy/merge.ts",
|
|
30
|
+
"policy/parse.ts",
|
|
31
|
+
"policy/types.ts",
|
|
32
|
+
"runtime/model-resolution.ts",
|
|
33
|
+
"runtime/pure.ts",
|
|
34
|
+
"runtime/session-state.ts",
|
|
35
|
+
"summary/generate.ts",
|
|
36
|
+
"summary/template.ts",
|
|
37
|
+
"banner.png",
|
|
38
|
+
"README.md",
|
|
39
|
+
"CHANGELOG.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "tsx --test test/**/*.test.ts"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@mariozechner/pi-agent-core": "^0.57.1",
|
|
47
|
+
"@mariozechner/pi-ai": "^0.57.1",
|
|
48
|
+
"@mariozechner/pi-coding-agent": "^0.57.1",
|
|
49
|
+
"@mariozechner/pi-tui": "^0.57.1",
|
|
50
|
+
"tsx": "^4.20.5"
|
|
51
|
+
},
|
|
52
|
+
"pi": {
|
|
53
|
+
"extensions": [
|
|
54
|
+
"./index.ts"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const LEGACY_PI_AGENT_ROOT = join(homedir(), ".pi", "agent");
|
|
5
|
+
|
|
6
|
+
export function resolveAgentRootPath(): string {
|
|
7
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
8
|
+
return agentDir && agentDir.length > 0 ? agentDir : LEGACY_PI_AGENT_ROOT;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveGlobalCompactionPolicyPath(): string {
|
|
12
|
+
return join(resolveAgentRootPath(), "compaction-policy.json");
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parsePolicyPatch } from "./parse.js";
|
|
4
|
+
import { resolveGlobalCompactionPolicyPath } from "../paths.js";
|
|
5
|
+
import {
|
|
6
|
+
CONFIG_FILE,
|
|
7
|
+
type ICompactionPolicyPatch,
|
|
8
|
+
type IParseResult,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
function readConfigFile(configPath: string): IParseResult<ICompactionPolicyPatch> {
|
|
12
|
+
try {
|
|
13
|
+
const raw = readFileSync(configPath, "utf8");
|
|
14
|
+
const json = JSON.parse(raw);
|
|
15
|
+
const parsed = parsePolicyPatch(json);
|
|
16
|
+
if (!parsed.ok) {
|
|
17
|
+
return { ok: false, error: `Invalid ${configPath}: ${parsed.error}` };
|
|
18
|
+
}
|
|
19
|
+
return parsed;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
return { ok: false, error: `Invalid ${configPath}: ${message}` };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function readProjectPolicyPatch(cwd: string): IParseResult<ICompactionPolicyPatch> {
|
|
27
|
+
const globalConfigPath = resolveGlobalCompactionPolicyPath();
|
|
28
|
+
const projectPath = join(cwd, CONFIG_FILE);
|
|
29
|
+
if (existsSync(projectPath)) {return readConfigFile(projectPath);}
|
|
30
|
+
if (existsSync(globalConfigPath)) {return readConfigFile(globalConfigPath);}
|
|
31
|
+
return { ok: true, value: {} };
|
|
32
|
+
}
|