pi-condense 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/LICENSE +22 -0
- package/PRUNING.md +1028 -0
- package/README.md +243 -0
- package/index.ts +858 -0
- package/package.json +56 -0
- package/src/batch-capture.ts +226 -0
- package/src/block-refs.test.ts +42 -0
- package/src/block-refs.ts +16 -0
- package/src/budget.test.ts +66 -0
- package/src/budget.ts +39 -0
- package/src/chain-compressor.test.ts +283 -0
- package/src/chain-compressor.ts +132 -0
- package/src/chain-detector.test.ts +302 -0
- package/src/chain-detector.ts +128 -0
- package/src/chain-range-prune.test.ts +522 -0
- package/src/chain-range-prune.ts +128 -0
- package/src/commands.test.ts +67 -0
- package/src/commands.ts +1207 -0
- package/src/config.ts +126 -0
- package/src/content-hash.ts +35 -0
- package/src/error-purge.test.ts +186 -0
- package/src/error-purge.ts +71 -0
- package/src/frontier.ts +62 -0
- package/src/indexer.ts +393 -0
- package/src/nested-placeholders.test.ts +82 -0
- package/src/nested-placeholders.ts +20 -0
- package/src/oversized-spill.integration.test.ts +73 -0
- package/src/protected.test.ts +62 -0
- package/src/protected.ts +51 -0
- package/src/pruner.test.ts +508 -0
- package/src/pruner.ts +156 -0
- package/src/query-tool.ts +78 -0
- package/src/range-compression.integration.test.ts +252 -0
- package/src/spill.test.ts +102 -0
- package/src/spill.ts +90 -0
- package/src/stats.test.ts +114 -0
- package/src/stats.ts +190 -0
- package/src/summarizer.test.ts +17 -0
- package/src/summarizer.ts +262 -0
- package/src/summary-refs.ts +61 -0
- package/src/thinking-strip.test.ts +175 -0
- package/src/thinking-strip.ts +42 -0
- package/src/tree-browser.ts +382 -0
- package/src/types.ts +764 -0
package/README.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# pi-condense
|
|
2
|
+
|
|
3
|
+
A [Pi coding-agent](https://github.com/badlogic/pi-mono) extension that summarizes completed tool-call batches, replaces raw tool outputs with short stubs in future context, and lets the LLM recover any original via the `context_tree_query` tool.
|
|
4
|
+
|
|
5
|
+
The session JSONL file is never modified β pruning only affects what each *next* request sees.
|
|
6
|
+
|
|
7
|
+
Fork of [`championswimmer/pi-context-prune`](https://github.com/championswimmer/pi-context-prune) with additional pre-flush safeguards, agent-message batching, chain compression, and an npm release flow.
|
|
8
|
+
|
|
9
|
+
π For the algorithm, design rationale, prompt-cache interaction, and the research behind summarization-based context management, see **[PRUNING.md](PRUNING.md)**.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Published to npm as [`pi-condense`](https://www.npmjs.com/package/pi-condense).
|
|
14
|
+
|
|
15
|
+
**User scope** (all repos under your pi profile):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pi install npm:pi-condense
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Project scope** (current repo only, committable via `.pi/settings.json`):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pi install -l npm:pi-condense
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Try without installing**:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pi -e npm:pi-condense
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**From a local checkout** (for hacking on the extension itself):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone git@github.com:jjuraszek/pi-condense.git ~/repos/pi-condense
|
|
37
|
+
cd ~/path/to/your/repo
|
|
38
|
+
pi install -l ~/repos/pi-condense
|
|
39
|
+
# or one-shot, no install:
|
|
40
|
+
pi -e ~/repos/pi-condense/index.ts
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Pin a specific version with `npm:pi-condense@X.Y.Z`. Upgrade by re-running `pi install`. Remove with `pi remove pi-condense`. Once installed, the extension auto-loads on every `pi` invocation; no flags needed.
|
|
44
|
+
|
|
45
|
+
> Diverges from upstream `championswimmer/pi-context-prune`; see [CHANGELOG.md](CHANGELOG.md) for what differs.
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
/pruner on # enable pruning
|
|
51
|
+
/pruner status # see current mode + cumulative cost
|
|
52
|
+
/pruner model openai/gpt-4.1-mini # pick a cheap summarizer
|
|
53
|
+
/pruner now # flush pending batches immediately
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
By default the extension is **off**. Enable it once and it stays enabled across sessions in the same pi agent directory.
|
|
57
|
+
|
|
58
|
+
## How it decides when to prune
|
|
59
|
+
|
|
60
|
+
Two trigger modes. The mode controls *when* summarization fires; the algorithm is the same in each.
|
|
61
|
+
|
|
62
|
+
| Mode | Trigger | Cache impact | Use when |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| `agent-message` (default) | When the agent sends a final text-only reply | One cache rewrite per task batch | Normal coding-agent work β best balance |
|
|
65
|
+
| `on-demand` | Only when you run `/pruner now` | None until you ask | Long investigations; manual control |
|
|
66
|
+
|
|
67
|
+
Why `agent-message` is the default: provider prefix caches (Anthropic, OpenAI, Bedrock, vLLM) only hit when the prompt prefix matches exactly. Every prune rewrites that prefix. Batching tool turns and pruning once per agent reply means roughly one cache miss per task instead of one per turn. See [PRUNING.md Β§ The Sweet Spot](PRUNING.md#the-sweet-spot-batch-and-prune) for the full argument.
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
Settings live under the `contextPrune` key in `<agent-dir>/settings.json` (i.e. pi's own settings file). `<agent-dir>` is `$PI_CODING_AGENT_DIR` if set, otherwise `~/.pi/agent`. Each pi preset gets its own settings, so you can run different summarizer models per preset.
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"contextPrune": {
|
|
76
|
+
"enabled": false,
|
|
77
|
+
"showPruneStatusLine": true,
|
|
78
|
+
"summarizerModel": "default",
|
|
79
|
+
"summarizerThinking": "default",
|
|
80
|
+
"pruneOn": "agent-message",
|
|
81
|
+
"batchingMode": "turn",
|
|
82
|
+
"quietOversizedSkips": false,
|
|
83
|
+
"minBatchChars": 1000,
|
|
84
|
+
"protectedTools": [],
|
|
85
|
+
"protectedPaths": ["**/skills/**/*.md"],
|
|
86
|
+
"dedupByContentHash": true,
|
|
87
|
+
"autoBudgetThreshold": null,
|
|
88
|
+
"spillThreshold": 65536,
|
|
89
|
+
"spillPreviewBytes": 2048,
|
|
90
|
+
"budgetTurnDelta": null,
|
|
91
|
+
"chainCompression": {
|
|
92
|
+
"enabled": true,
|
|
93
|
+
"rollingWindow": 3,
|
|
94
|
+
"stripFinalAssistantThinking": true,
|
|
95
|
+
"fuseRangeSummary": true
|
|
96
|
+
},
|
|
97
|
+
"thinkingStrip": {
|
|
98
|
+
"enabled": true,
|
|
99
|
+
"keepLastTurns": 16
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
| Key | Values | Default | Notes |
|
|
106
|
+
|---|---|---|---|
|
|
107
|
+
| `enabled` | `true` / `false` | `false` | Master switch |
|
|
108
|
+
| `showPruneStatusLine` | `true` / `false` | `true` | Footer widget + queued-turn notifications |
|
|
109
|
+
| `summarizerModel` | `"default"` or `"provider/model-id"` | `"default"` | `default` = your active pi model. See [Choosing a summarizer model](#choosing-a-summarizer-model) |
|
|
110
|
+
| `summarizerThinking` | `default`/`off`/`minimal`/`low`/`medium`/`high`/`xhigh` | `default` | Provider-specific reasoning effort knob |
|
|
111
|
+
| `pruneOn` | see table above | `agent-message` | Trigger mode |
|
|
112
|
+
| `batchingMode` | `turn` / `agent-message` | `turn` | How coarse each summary is (independent of `pruneOn`) |
|
|
113
|
+
| `quietOversizedSkips` | `true` / `false` | `false` | Silences `skipped-oversized` / `skipped-trivial` info notifications |
|
|
114
|
+
| `minBatchChars` | non-negative integer, `0` disables | `1000` | Pre-flush guard β batches smaller than this skip the LLM entirely |
|
|
115
|
+
| `protectedTools` | `string[]` | `[]` | Never-pruned tool names (e.g. `["todowrite","todoread"]`). When a protected tool's chain is range-compressed, its output is preserved verbatim inside the `<compressed-chain>` block as `<protected-output>` β protected outputs are never lost. |
|
|
116
|
+
| `protectedPaths` | `string[]` | `["**/skills/**/*.md"]` | Globs matched against a tool call's `args.path`; matching outputs are never pruned (same semantics as `protectedTools`, including `<protected-output>` relocation in compressed chains). Already-summarized matching reads are repaired on the next turn; chain-compressed ones are not. Set `[]` to disable. |
|
|
117
|
+
| `dedupByContentHash` | `true` / `false` | `true` | Re-reads of identical (toolName, content) skip the LLM and alias the original |
|
|
118
|
+
| `autoBudgetThreshold` | fraction `0`β`1`, or `null` | `null` | Token-budget auto-flush: force a prune when context usage reaches this share of the window, regardless of `pruneOn`. `0.8` = 80%, not `80`. `null` = off. See [Token-budget auto-flush](#token-budget-auto-flush) |
|
|
119
|
+
| `spillThreshold` | positive integer | `65536` | Minimum chars (`resultText.length`) for a single tool result to be spilled eagerly to a sidecar file at capture time rather than waiting for normal summarization. Non-positive / invalid values fall back to the default; to effectively disable spilling, set it above any result you expect. See [Spilled outputs](#spilled-outputs) |
|
|
120
|
+
| `spillPreviewBytes` | non-negative integer | `2048` | Head preview (bytes) kept inline in the stub and index record for a spilled result. Full body is on disk. |
|
|
121
|
+
| `budgetTurnDelta` | fraction `0`β`1`, or `null` | `null` | Force a flush when a single turn's context-usage fraction jumps by at least this amount, ORed with `autoBudgetThreshold`. Catches sudden spikes a static threshold would miss until the next turn. `null` = off. |
|
|
122
|
+
| `chainCompression.enabled` | `true` / `false` | `true` | Master toggle for chain-level range compression |
|
|
123
|
+
| `chainCompression.rollingWindow` | positive integer | `3` | Keep this many most-recent closed chains raw; compress older ones |
|
|
124
|
+
| `chainCompression.stripFinalAssistantThinking` | `true` / `false` | `true` | Strip thinking blocks from the kept final text-only assistant when compressing |
|
|
125
|
+
| `chainCompression.fuseRangeSummary` | `true` / `false` | `true` | Fuse a compressed chain's per-batch summaries into one cohesive LLM summary (one extra summarizer call per multi-batch span); off keeps the per-batch concatenation |
|
|
126
|
+
| `purgeErrors.enabled` | `true` / `false` | `true` | Replace failed toolCall argument bodies with compact stubs after cooldown |
|
|
127
|
+
| `purgeErrors.cooldownTurns` | positive integer | `2` | Turns to wait after a tool error before purging its argument body |
|
|
128
|
+
| `purgeErrors.minArgChars` | non-negative integer | `500` | Only purge arg bodies at least this many characters long |
|
|
129
|
+
| `thinkingStrip.enabled` | `true` / `false` | `true` | Strip `thinking` blocks from assistant turns older than the last `keepLastTurns` |
|
|
130
|
+
| `thinkingStrip.keepLastTurns` | positive integer | `16` | Keep thinking on the last N assistant turns; strip older. Counts assistant turns, not chains. No-op under N turns |
|
|
131
|
+
|
|
132
|
+
See [PRUNING.md Β§ Chain Compression](PRUNING.md#chain-compression), [PRUNING.md Β§ Error Purge](PRUNING.md#error-purge), and [PRUNING.md Β§ Main-loop Thinking Strip](PRUNING.md#main-loop-thinking-strip) for the full algorithms.
|
|
133
|
+
|
|
134
|
+
The three pre-flush features (`minBatchChars`, `protectedTools`, `dedupByContentHash`) are explained in [PRUNING.md Β§ Pre-flush Pipeline & Safeguards](PRUNING.md#pre-flush-pipeline--safeguards). They run BEFORE any summarizer LLM call and can each drop a batch outright while still advancing the prune frontier.
|
|
135
|
+
|
|
136
|
+
### Token-budget auto-flush
|
|
137
|
+
|
|
138
|
+
When `autoBudgetThreshold` is set to a value in `(0, 1]`, the extension checks context usage at the end of every tool-using turn. If `tokens / contextWindow` reaches the threshold, ALL pending batches are flushed immediately β regardless of `pruneOn` mode. This is an **additional** trigger layered on top of `pruneOn`, not a replacement.
|
|
139
|
+
|
|
140
|
+
- `0.8` means 80% of the context window β it is a **fraction**, not a percentage. `0.8 β 80`.
|
|
141
|
+
- The trigger is a no-op when `tokens` is `null` (right after a provider-side compaction); it resumes once usage is known again.
|
|
142
|
+
- Editable live via `/pruner settings` (row "Auto-flush at context %", presets Off / 60 / 70 / 80 / 90%).
|
|
143
|
+
- Default `null` = off.
|
|
144
|
+
|
|
145
|
+
Inspired by DCP's `maxContextLimit` nudging; simplified to a single threshold that forces a flush rather than separate nudge/force levels.
|
|
146
|
+
|
|
147
|
+
### Spilled outputs
|
|
148
|
+
|
|
149
|
+
Single tool results larger than `spillThreshold` chars are written to `<session-dir>/<sessionId>-blobs/<toolCallId>.txt` at capture time and replaced in context with a short stub (tool name, byte size, head preview, file path). The full body is recoverable via the native `read` tool at the embedded path (offset/limit supported) or via `context_tree_query` by id, which falls back to the inline preview if the sidecar is missing. Moving a session `.jsonl` without its `-blobs/` directory loses only the giant-blob recovery path; bodies under `spillThreshold` stay inline in the index entry as usual.
|
|
150
|
+
|
|
151
|
+
### Choosing a summarizer model
|
|
152
|
+
|
|
153
|
+
The `default` setting reuses whatever model you have active in pi β convenient but wasteful, since summary writing doesn't need a top-tier coding model. Picking the smallest/fastest model on your plan saves both latency and cost.
|
|
154
|
+
|
|
155
|
+
| Plan | Suggested summarizer |
|
|
156
|
+
|---|---|
|
|
157
|
+
| OpenAI / Codex / Copilot | `openai/gpt-4.1-mini`, `google/gemini-2.5-flash`, `xai/grok-3-fast` |
|
|
158
|
+
| OpenRouter | `openrouter/qwen/qwen3-30b-a3b` (cheap MoE) |
|
|
159
|
+
| Anthropic direct | `anthropic/claude-haiku-3-5` |
|
|
160
|
+
| Google AI direct | `google/gemini-2.5-flash` |
|
|
161
|
+
|
|
162
|
+
Set it from the slash command (saves immediately):
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
/pruner model openai/gpt-4.1-mini
|
|
166
|
+
/pruner thinking low
|
|
167
|
+
# or both in one go:
|
|
168
|
+
/pruner model openai/gpt-4.1-mini:low
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Commands
|
|
172
|
+
|
|
173
|
+
| Command | Effect |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `/pruner` | Interactive picker over all subcommands |
|
|
176
|
+
| `/pruner settings` | Settings overlay (toggle / cycle every option) |
|
|
177
|
+
| `/pruner on` / `off` | Enable / disable pruning |
|
|
178
|
+
| `/pruner status` | Show mode, model, trigger, cumulative stats |
|
|
179
|
+
| `/pruner stats` | Detailed cumulative summarizer token/cost stats |
|
|
180
|
+
| `/pruner model [id\[:thinking\]]` | Get / set summarizer model (and optionally thinking level) |
|
|
181
|
+
| `/pruner thinking [level]` | Get / set summarizer reasoning effort |
|
|
182
|
+
| `/pruner prune-on [mode]` | Get / set trigger mode |
|
|
183
|
+
| `/pruner batching [mode]` | Get / set batching granularity (`turn` / `agent-message`) |
|
|
184
|
+
| `/pruner protected-tools [names]` | Show or edit the never-pruned tool allowlist (comma- or space-separated; `none` clears) |
|
|
185
|
+
| `/pruner protected-paths [globs]` | Show or edit the never-pruned path globs (`none` clears) |
|
|
186
|
+
| `/pruner min-batch-chars [n]` | Show or set the pre-flush trivial-batch threshold (`0` disables) |
|
|
187
|
+
| `/pruner dedup [on\|off\|status]` | Toggle pre-flush content-hash dedup |
|
|
188
|
+
| `/pruner tree` | Foldable browser of pruned tool calls; `Ctrl-O` opens the full summary in an overlay |
|
|
189
|
+
| `/pruner compact` | Retroactively compress every eligible closed chain (bypasses `rollingWindow`) |
|
|
190
|
+
| `/pruner now` | Flush pending batches immediately with a multi-row progress widget above the input |
|
|
191
|
+
| `/pruner help` | Full help text |
|
|
192
|
+
|
|
193
|
+
## Tools surfaced to the LLM
|
|
194
|
+
|
|
195
|
+
**`context_tree_query`** β always available when the extension is loaded. Pruned summaries end with short refs like `Summarized tool refs: \`t1\`, \`t2\`. Use \`context_tree_query\` with these refs to retrieve the original full outputs.` The model passes those refs (or full `toolCallId`s) and gets back the original tool result text from the session index. Content-hash-deduped duplicates resolve to the original's record automatically.
|
|
196
|
+
|
|
197
|
+
## Footer status widget
|
|
198
|
+
|
|
199
|
+
A footer widget shows the current state, controlled by `showPruneStatusLine`:
|
|
200
|
+
|
|
201
|
+
Every rendered state is wrapped in `β β¦ β` so the segment stays visually isolated in the shared footer regardless of where other extensions' status segments land (load-order independent).
|
|
202
|
+
|
|
203
|
+
- `β prune: OFF β` β disabled
|
|
204
|
+
- `β prune: ON β` β enabled, no flushes yet
|
|
205
|
+
- `β prune: ON Β· 92k->14k (-85%) β` β enabled; live reclaim ratio (estimated tokens beforeβafter, percent reduction). Updates on every `pruneMessages` call.
|
|
206
|
+
- `β prune: 3 pending β` β batches queued, waiting for the trigger
|
|
207
|
+
- `β prune: summarizingβ¦ β` β flush in progress
|
|
208
|
+
|
|
209
|
+
Setting `showPruneStatusLine: false` hides the widget and silences the queued-turn notice; pruning still runs.
|
|
210
|
+
|
|
211
|
+
Cost no longer appears on the status line. Full token/cost detail is available via `/pruner stats`. The extension also emits cumulative session cost on the `cost:external` pi.events channel for external aggregators β see [External cost channel](#external-cost-channel).
|
|
212
|
+
|
|
213
|
+
## External cost channel
|
|
214
|
+
|
|
215
|
+
Every time the summarizer cost updates, the extension emits on the shared `pi.events` channel identified by the constant `EXTERNAL_COST_CHANNEL = "cost:external"`. Payload shape:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
interface ExternalCostUpdate {
|
|
219
|
+
source: string; // EXTERNAL_COST_SOURCE = "pi-condense"
|
|
220
|
+
totalCost: number; // cumulative cost for the current session (USD)
|
|
221
|
+
inputTokens?: number;
|
|
222
|
+
outputTokens?: number;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Semantics:
|
|
227
|
+
- **Cumulative per session**, not all-time. Re-emitted on every update; aggregators key by `source` and replace the previous value.
|
|
228
|
+
- **Live only.** Not persisted; not re-emitted on `session_start`. An aggregator that restarts mid-session sees cost from zero until the next summarizer call.
|
|
229
|
+
- Designed for aggregators like pi-cohort that show a unified Ξ£$ total across extensions.
|
|
230
|
+
|
|
231
|
+
## Limitations
|
|
232
|
+
|
|
233
|
+
- Pruning only applies to batches captured *while enabled*. Enabling mid-session does not retroactively summarize earlier turns.
|
|
234
|
+
- Summarizer calls are synchronous inside `turn_end` (or `message_end` for `agent-message` mode), so they add latency between turns proportional to the summarizer model's response time. Pick a fast model.
|
|
235
|
+
- Content-hash dedup only matches against records already in the indexer (cross-flush). Two identical outputs within the *same* flush are not deduped β both go through the summarizer.
|
|
236
|
+
- The tree browser does not inline original tool outputs β use `context_tree_query` for that.
|
|
237
|
+
|
|
238
|
+
## References
|
|
239
|
+
|
|
240
|
+
- Anthropic prompt caching: <https://docs.claude.com/en/docs/build-with-claude/prompt-caching>
|
|
241
|
+
- AWS Bedrock prompt caching: <https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html>
|
|
242
|
+
- OpenAI prompt caching: <https://platform.openai.com/docs/guides/prompt-caching>
|
|
243
|
+
- Research backing summarization-based context management: see [PRUNING.md Β§ Research Evidence](PRUNING.md#why-summarization-works-research-evidence)
|