pi-midcompact 0.5.3 → 0.6.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/README.md +18 -1
- package/README.zh-CN.md +16 -1
- package/package.json +4 -2
- package/src/SPEC.md +24 -1
- package/src/content-metrics.ts +75 -3
- package/src/projection.ts +12 -3
- package/src/review-webui.html +837 -481
- package/src/review-webui.ts +150 -57
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ The raw session JSONL still contains:
|
|
|
55
55
|
|
|
56
56
|
### A reviewed draft can reclaim meaningful context
|
|
57
57
|
|
|
58
|
-
The earlier browser and TUI captures below illustrate a draft with **2 ranges** covering **42 of 73 atoms**, while the other 31 atoms remain verbatim. The current UI reports Pi-provided anchor usage
|
|
58
|
+
The earlier browser and TUI captures below illustrate a draft with **2 ranges** covering **42 of 73 atoms**, while the other 31 atoms remain verbatim. The current UI reports Pi-provided anchor usage as the baseline and derives a **display-only** projection of post-commit usage from documented char-class assumptions (labeled `est.`, shown as a range, never used for gating); factual content chars and image counts stay alongside it. Click either image to open it at full resolution.
|
|
59
59
|
|
|
60
60
|
<p align="center">
|
|
61
61
|
<a href="./figures/review-webui.png">
|
|
@@ -222,6 +222,23 @@ Enter/Esc/q close
|
|
|
222
222
|
|
|
223
223
|
The extension shows planning status in Pi's footer only while a transaction is active. It disappears after commit or abort.
|
|
224
224
|
|
|
225
|
+
## Web UI Development
|
|
226
|
+
|
|
227
|
+
From a source checkout, run the browser workbench against in-memory fixtures
|
|
228
|
+
without starting Pi:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
npm run dev:webui
|
|
232
|
+
npm run dev:webui -- --port=4180 --no-open
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The command opens a fixture router for `review-ready`, `review-pending`,
|
|
236
|
+
`selection-mixed`, `no-telemetry`, and `wide-content`. Each button opens an
|
|
237
|
+
isolated workbench with its own in-memory draft. Fixture pages survive browser
|
|
238
|
+
refresh and the page's Close action, reload HTML changes automatically, and
|
|
239
|
+
restart for imported TypeScript changes. Stop the router with `Ctrl+C`. Use
|
|
240
|
+
`dev/midcompact-debug-ui.ts` only when validating against an actual Pi session.
|
|
241
|
+
|
|
225
242
|
## Guarantees and Limits
|
|
226
243
|
|
|
227
244
|
- **Original history is retained.** Compression changes what later model requests see, not the stored Pi messages.
|
package/README.zh-CN.md
CHANGED
|
@@ -55,7 +55,7 @@ Pi 内置的 `/compact` 可理解为**前缀压缩**(prefix compaction):
|
|
|
55
55
|
|
|
56
56
|
### 实际压缩效果
|
|
57
57
|
|
|
58
|
-
下面的早期浏览器和 TUI 截图展示了一份包含 **2 个区段**、覆盖 **73 个 atom 中 42 个**的草案,其余 31 个 atom 保留原文。当前 UI
|
|
58
|
+
下面的早期浏览器和 TUI 截图展示了一份包含 **2 个区段**、覆盖 **73 个 atom 中 42 个**的草案,其余 31 个 atom 保留原文。当前 UI 以 Pi 上报的锚点 usage 为基线,并根据文档化的字符分类假设推导**仅供展示**的 commit 后占用预计(标注 `est.`、以区间呈现、绝不参与门禁);事实性的 content chars 与图片数量仍然并列展示。点击图片可查看原图。
|
|
59
59
|
|
|
60
60
|
<p align="center">
|
|
61
61
|
<a href="./figures/review-webui.png">
|
|
@@ -222,6 +222,21 @@ Enter/Esc/q 关闭
|
|
|
222
222
|
|
|
223
223
|
扩展只在事务进行期间在 Pi 页脚显示规划状态;提交或放弃后会自动清除。
|
|
224
224
|
|
|
225
|
+
## Web UI 开发
|
|
226
|
+
|
|
227
|
+
在源码 checkout 中,无需启动 Pi 即可用内存 fixture 运行浏览器工作台:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
npm run dev:webui
|
|
231
|
+
npm run dev:webui -- --port=4180 --no-open
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
命令会打开一个 fixture router,其中包含 `review-ready`、`review-pending`、
|
|
235
|
+
`selection-mixed`、`no-telemetry` 和 `wide-content`。每个按钮都会打开一套独立的
|
|
236
|
+
工作台和内存草案。刷新浏览器或点击 Close 不会终止 fixture;HTML 修改会自动刷新
|
|
237
|
+
页面,导入的 TypeScript 修改会触发进程重启。用 `Ctrl+C` 停止 router。只有在验证
|
|
238
|
+
真实 Pi 会话集成时,才需要使用 `dev/midcompact-debug-ui.ts`。
|
|
239
|
+
|
|
225
240
|
## 保证与限制
|
|
226
241
|
|
|
227
242
|
- **保留原始历史。** 压缩只改变后续模型请求看到的内容,不改写存储的 Pi 消息。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-midcompact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Branch-aware mid-context compression for the Pi coding agent",
|
|
5
5
|
"author": "frostime",
|
|
6
6
|
"repository": {
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
34
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
35
|
-
"
|
|
35
|
+
"dev:webui": "tsx watch dev/review-webui-preview.ts",
|
|
36
|
+
"test": "rm -rf .test-dist && tsc -p tsconfig.test.json && node test/install-mocks.mjs && node --test test/schema-contract.test.mjs test/core.test.mjs test/runtime-start.test.mjs test/runtime-agent.test.mjs test/runtime-user.test.mjs test/runtime-transaction.test.mjs test/runtime-lock.test.mjs && node test/review-webui-dom.test.mjs",
|
|
36
37
|
"typecheck:contract": "tsc -p tsconfig.test.json --noEmit",
|
|
37
38
|
"pack:check": "npm pack --dry-run"
|
|
38
39
|
},
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"@earendil-works/pi-ai": "^0.84.1",
|
|
46
47
|
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
47
48
|
"typescript": "^5.9.3",
|
|
49
|
+
"tsx": "^4.23.13",
|
|
48
50
|
"@earendil-works/pi-tui": "^0.84.1"
|
|
49
51
|
},
|
|
50
52
|
"pi": {
|
package/src/SPEC.md
CHANGED
|
@@ -32,7 +32,12 @@ package.json → `pi.extensions`).
|
|
|
32
32
|
then appends state, abort appends nothing. Both refuse to run while the
|
|
33
33
|
Agent holds the planning lock.
|
|
34
34
|
- `anchorUsage` is informational only — Pi-reported awareness, never an
|
|
35
|
-
optimization target
|
|
35
|
+
optimization target. The web workbench may additionally render a
|
|
36
|
+
**display-level** post-commit projection derived from the documented
|
|
37
|
+
char-class assumption table in `content-metrics` (`TOKEN_ESTIMATE`): always
|
|
38
|
+
labeled `est.`, shown as a propagated band, and never feeding commit
|
|
39
|
+
gating, range validation, or any decision (same rule as `projectedTokens`
|
|
40
|
+
/ `approxTokens`).
|
|
36
41
|
- Atom refs are transaction-local: re-run inspect/locate in a later
|
|
37
42
|
transaction; group refs (`g...`) are never locate refs.
|
|
38
43
|
|
|
@@ -78,6 +83,24 @@ package.json → `pi.extensions`).
|
|
|
78
83
|
`skill:<name>`).
|
|
79
84
|
- The tool never starts a transaction and never commits; both are command-
|
|
80
85
|
or user-gated. Recall is the only action valid without a transaction.
|
|
86
|
+
- Web workbench (`review-webui.html` + `review-webui.ts`): the state payload
|
|
87
|
+
carries per-atom char-class counts (`narrowChars`/`wideChars`), per-range
|
|
88
|
+
replacement char-class counts (including the actual wrapper), and the
|
|
89
|
+
assumption table (`est`), so the page renders the projection band and can
|
|
90
|
+
update estimates while a summary is edited; `GET /api/atom/:ref` serves the
|
|
91
|
+
frozen atom's full text for the original-text drawer (read-only, snapshot-local).
|
|
92
|
+
`startReviewWebUiServer` owns the Pi-independent loopback HTTP contract;
|
|
93
|
+
`showReviewWebUi` adapts it to Pi notification, browser launch, and page-bound
|
|
94
|
+
lifetime. Development may opt into a persistent server without changing the
|
|
95
|
+
production defaults.
|
|
96
|
+
User-facing copy says "can't compress" for protected atoms; "protected"
|
|
97
|
+
stays the agent/tool-side term.
|
|
98
|
+
Page invariants that broke once and must hold: the `<!--MIDCOMPACT_STATE-->`
|
|
99
|
+
script tag is the server's template injection point (renaming it breaks
|
|
100
|
+
state loading); `selectionRefs` initialization depends on helpers defined
|
|
101
|
+
later in the page script (order is load-bearing, TDZ); `gbody` visibility
|
|
102
|
+
is driven by the render-time `hidden` attribute, so collapse handlers must
|
|
103
|
+
sync that attribute, not just a class.
|
|
81
104
|
|
|
82
105
|
## Change rules
|
|
83
106
|
|
package/src/content-metrics.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
// Sole owner of
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Sole owner of message content statistics. Factual char/image counts are the
|
|
2
|
+
// authority; the only token conversion allowed is the explicitly scoped,
|
|
3
|
+
// display-level estimator at the bottom of this file (web UI presentation
|
|
4
|
+
// only — it never gates decisions, commits, or range validity). Image base64
|
|
5
|
+
// never contributes to text char counts.
|
|
4
6
|
|
|
5
7
|
import type { ContentMetrics, ImageFact, MessageLike } from "./types.js";
|
|
6
8
|
|
|
@@ -183,3 +185,73 @@ export function aggregateMetrics(parts: readonly ContentMetrics[]): ContentMetri
|
|
|
183
185
|
}
|
|
184
186
|
return { contentChars, imageCount, images };
|
|
185
187
|
}
|
|
188
|
+
|
|
189
|
+
// ---- Display-level token estimation (web UI presentation only) ----
|
|
190
|
+
//
|
|
191
|
+
// The web UI shows a projected post-commit usage band. Because the consumer
|
|
192
|
+
// model is provider-dependent, tokens are estimated from char classes with a
|
|
193
|
+
// documented assumption table instead of a real tokenizer: the [low, high]
|
|
194
|
+
// pairs express published tokenizer spread and are propagated as a band, never
|
|
195
|
+
// collapsed into a single authoritative number. The band is derived from the
|
|
196
|
+
// content mix of the atoms involved (ASCII-heavy content lands near the tight
|
|
197
|
+
// end, CJK-heavy near the wide end). Nothing here feeds commit gating, range
|
|
198
|
+
// validation, or any decision.
|
|
199
|
+
|
|
200
|
+
/** Per-char-class token-cost assumptions: [low, high] tokens per char. */
|
|
201
|
+
export const TOKEN_ESTIMATE = {
|
|
202
|
+
/** ASCII/code: roughly 3.3–4.5 chars per token across common tokenizers. */
|
|
203
|
+
narrowTokPerChar: [0.22, 0.3],
|
|
204
|
+
/** Non-ASCII scripts (CJK, kana, hangul, emoji, …): the dominant spread. */
|
|
205
|
+
wideTokPerChar: [0.5, 1.0],
|
|
206
|
+
/** Images: provider- and resolution-dependent flat allowance. */
|
|
207
|
+
imageTok: [700, 1600],
|
|
208
|
+
} as const;
|
|
209
|
+
|
|
210
|
+
/** Char classes the estimator distinguishes. */
|
|
211
|
+
export interface CharMix {
|
|
212
|
+
/** ASCII code points. */
|
|
213
|
+
narrowChars: number;
|
|
214
|
+
/** Everything non-ASCII (counted conservatively at the wide rate). */
|
|
215
|
+
wideChars: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Classification is deliberately coarse: ASCII is estimated at the narrow
|
|
219
|
+
// rate; every non-ASCII code point (CJK, kana, hangul, emoji, Cyrillic, …) is
|
|
220
|
+
// counted at the wide rate, which is the conservative side for CJK-heavy
|
|
221
|
+
// content and keeps the table honest without per-script modeling.
|
|
222
|
+
|
|
223
|
+
/** Split a text into narrow (ASCII) and wide (everything else) code points. */
|
|
224
|
+
export function charClassCounts(text: string): CharMix {
|
|
225
|
+
let narrowChars = 0;
|
|
226
|
+
let wideChars = 0;
|
|
227
|
+
for (const ch of text) {
|
|
228
|
+
const cp = ch.codePointAt(0)!;
|
|
229
|
+
if (cp <= 0x7f) {
|
|
230
|
+
narrowChars += 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
wideChars += 1;
|
|
234
|
+
}
|
|
235
|
+
return { narrowChars, wideChars };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface TokenEstimate {
|
|
239
|
+
point: number;
|
|
240
|
+
low: number;
|
|
241
|
+
high: number;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Estimate tokens for a char mix plus images, as a propagated band. */
|
|
245
|
+
export function estimateTokens(mix: CharMix, imageCount: number): TokenEstimate {
|
|
246
|
+
const [aLo, aHi] = TOKEN_ESTIMATE.narrowTokPerChar;
|
|
247
|
+
const [cLo, cHi] = TOKEN_ESTIMATE.wideTokPerChar;
|
|
248
|
+
const [iLo, iHi] = TOKEN_ESTIMATE.imageTok;
|
|
249
|
+
const mid = (lo: number, hi: number) => (lo + hi) / 2;
|
|
250
|
+
return {
|
|
251
|
+
point: Math.round(mix.narrowChars * mid(aLo, aHi)
|
|
252
|
+
+ mix.wideChars * mid(cLo, cHi)
|
|
253
|
+
+ imageCount * mid(iLo, iHi)),
|
|
254
|
+
low: Math.round(mix.narrowChars * aLo + mix.wideChars * cLo + imageCount * iLo),
|
|
255
|
+
high: Math.round(mix.narrowChars * aHi + mix.wideChars * cHi + imageCount * iHi),
|
|
256
|
+
};
|
|
257
|
+
}
|
package/src/projection.ts
CHANGED
|
@@ -74,8 +74,8 @@ export function replacementMetrics(block: CompressionBlock): ContentMetrics {
|
|
|
74
74
|
* Factual replacement content chars for a range, given its topic and summary.
|
|
75
75
|
* Computed from the actual summary message wrapper text that projection emits.
|
|
76
76
|
*/
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
function draftReplacementBlock(summary: string, topic?: string): CompressionBlock {
|
|
78
|
+
return {
|
|
79
79
|
id: "draft",
|
|
80
80
|
summary,
|
|
81
81
|
topic,
|
|
@@ -89,7 +89,16 @@ export function replacementContentChars(summary: string, topic?: string): number
|
|
|
89
89
|
originalApproxTokens: 0,
|
|
90
90
|
compressedApproxTokens: 0,
|
|
91
91
|
};
|
|
92
|
-
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Render the exact replacement content used for draft display estimates. */
|
|
95
|
+
export function replacementContentText(summary: string, topic?: string): string {
|
|
96
|
+
const content = summaryMessage(draftReplacementBlock(summary, topic)).content;
|
|
97
|
+
return typeof content === "string" ? content : "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function replacementContentChars(summary: string, topic?: string): number {
|
|
101
|
+
return replacementMetrics(draftReplacementBlock(summary, topic)).contentChars;
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
/** @deprecated legacy heuristic; kept only for old approximate-token field compat. */
|