pi-midcompact 0.5.2 → 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/skills/midcompact/SKILL.md +7 -7
- package/skills/midcompact/references/tool-interface.md +7 -7
- package/src/SPEC.md +116 -0
- package/src/content-metrics.ts +75 -3
- package/src/index.ts +87 -62
- package/src/projection.ts +13 -4
- 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": {
|
|
@@ -12,8 +12,8 @@ This skill handles two independent tasks: planning compression and recalling com
|
|
|
12
12
|
| Signal | Immediate duty | First action |
|
|
13
13
|
|--------|----------------|--------------|
|
|
14
14
|
| Runtime prompt says `FINAL STATE: USER MANUAL` | Let the user create the initial DraftPlan | Reply exactly `OK`; call no midcompact tool |
|
|
15
|
-
| Runtime prompt says `FINAL STATE: AGENT DIRECT` | Start from the new empty draft | `action
|
|
16
|
-
| A handoff reports a persisted DraftPlan and the user asks to continue | Read the shared selection and determine what help the user wants | `action
|
|
15
|
+
| Runtime prompt says `FINAL STATE: AGENT DIRECT` | Start from the new empty draft | `request={action:"inspect"}` |
|
|
16
|
+
| A handoff reports a persisted DraftPlan and the user asks to continue | Read the shared selection and determine what help the user wants | `request={action:"plan", op:"show"}` |
|
|
17
17
|
| The user or a projected summary needs detail from a committed block | Retrieve that history only | Follow **Recall workflow** |
|
|
18
18
|
|
|
19
19
|
The state-specific runtime prompt is authoritative. Recall does not enter planning or mutate the DraftPlan. During the User-manual acknowledgement turn, the no-tool instruction overrides every other route.
|
|
@@ -34,7 +34,7 @@ Apply one invariant:
|
|
|
34
34
|
|
|
35
35
|
#### 1. Read the entry state and user intent
|
|
36
36
|
|
|
37
|
-
For a fresh Agent-direct transaction, begin with `action
|
|
37
|
+
For a fresh Agent-direct transaction, begin with `request={action:"inspect"}`. For a handed-off DraftPlan, begin with `request={action:"plan", op:"show"}`; inspect the anchor only if the requested work needs broader context.
|
|
38
38
|
|
|
39
39
|
Establish the user's desired scope, fidelity, and planning effort from their words, current selection, `User focus: ...`, and surrounding interaction. Treat answers collected through question, questionnaire, or similar tools as user-originated input even when represented as tool results.
|
|
40
40
|
|
|
@@ -71,7 +71,7 @@ When treatments involve a meaningful tradeoff, present concise alternatives and
|
|
|
71
71
|
|
|
72
72
|
#### 4. Resolve boundaries and build the DraftPlan
|
|
73
73
|
|
|
74
|
-
After the intended treatment is clear, use `action
|
|
74
|
+
After the intended treatment is clear, use `request={action:"locate"}` for targeted content and boundary checks. An atom is the smallest selectable unit; a tool call and its matching results form one indivisible `tool_exchange` atom. Keep source text outside a range when exact wording or provenance matters and a summary cannot preserve it equivalently.
|
|
75
75
|
|
|
76
76
|
Choose boundaries from the information that must survive, not from a fixed category. A range may replace a whole semantic phase, including its initiating and concluding messages. It may instead retain a load-bearing user instruction and concluding Agent response while replacing only the execution between them. It may split around important material to leave KEEP holes. These are reasoning patterns, not rules tied to start mode, message age, or one prescribed kind of work.
|
|
77
77
|
|
|
@@ -107,7 +107,7 @@ Length follows the information that must survive, not a target ratio. Final test
|
|
|
107
107
|
|
|
108
108
|
#### 6. Verify and hand off
|
|
109
109
|
|
|
110
|
-
Call `action
|
|
110
|
+
Call `request={action:"plan", op:"show"}`. Check that the intended semantic phases are covered, KEEP holes remain outside ranges, every range has a summary, and the summaries conserve the future working state.
|
|
111
111
|
|
|
112
112
|
Describe the completed proposal with the same recognizable landmarks used during alignment. Direct the user to `/midcompact:select` or `/midcompact:select-webui` for boundaries and KEEP holes, and to `/midcompact:review` or `/midcompact:review-webui` for summary inspection or rejection. Use browser variants when the TUI is unavailable or preferred. Ask the user to run `/midcompact:commit` when ready; never commit for them.
|
|
113
113
|
|
|
@@ -119,11 +119,11 @@ Recall works with or without an active planning transaction. It reads committed
|
|
|
119
119
|
|
|
120
120
|
#### 1. Find the block
|
|
121
121
|
|
|
122
|
-
If its id is unknown, call `action
|
|
122
|
+
If its id is unknown, call `request={action:"recall", pattern:"..."}` to search active topics and summaries. A projected summary also states its block id and exact recall call.
|
|
123
123
|
|
|
124
124
|
#### 2. Retrieve the detail
|
|
125
125
|
|
|
126
|
-
Call `action
|
|
126
|
+
Call `request={action:"recall", ref:"c0001"}`. If the readable, structure-flattened result ends with a truncation marker, retry with `detail="full"`. Retrieve only what the current task needs; do not start or change a plan merely to recall history.
|
|
127
127
|
|
|
128
128
|
## Tool conventions
|
|
129
129
|
|
|
@@ -4,11 +4,11 @@ Read this reference when exact call requirements, limits, rejection behavior, re
|
|
|
4
4
|
|
|
5
5
|
## Parameter grouping
|
|
6
6
|
|
|
7
|
-
The
|
|
7
|
+
The parameters are one object with a single `request` field; `request` is a discriminated union where each branch binds one `action` value to exactly that action's fields and is closed (`additionalProperties: false`). A call that mixes actions (for example `locate` fields on `plan`) is rejected by the schema itself; do not repair it by dropping fields, re-issue the call with only the selected action's parameters. Shared field names (`ref`, `pattern`, `limit`, `detail`) are defined independently per action with the meaning documented in that section.
|
|
8
8
|
|
|
9
9
|
## Inspect
|
|
10
10
|
|
|
11
|
-
Without `spans`, `action="inspect"` inventories the frozen anchor. It returns factual structure and bounded user landmarks, not full message bodies, assistant/tool previews, summaries, or image base64.
|
|
11
|
+
Without `spans`, `request.action="inspect"` inventories the frozen anchor. It returns factual structure and bounded user landmarks, not full message bodies, assistant/tool previews, summaries, or image base64.
|
|
12
12
|
|
|
13
13
|
- `page_size`: default 20 groups, maximum 50.
|
|
14
14
|
- `cursor`: opaque value returned by the previous page.
|
|
@@ -19,17 +19,17 @@ Stop paging after the candidate regions are covered.
|
|
|
19
19
|
To compare explicit candidates without mutating the DraftPlan, pass one or more possibly overlapping spans:
|
|
20
20
|
|
|
21
21
|
```text
|
|
22
|
-
midcompact(action
|
|
22
|
+
midcompact(request={action:"inspect", spans=[
|
|
23
23
|
{"start":"a0006","end":"a0014"},
|
|
24
24
|
{"start":"a0006","end":"a0020"}
|
|
25
|
-
])
|
|
25
|
+
]})
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Span inspection reports bounded endpoint landmarks, atom/message and role counts, tool exchanges and calls, factual content share, images, and protected/compressible counts. It has a 12,000-character total output budget and reports how many requested spans fit. It does not report per-span tokens: Pi supplies usage for the whole anchor, not token attribution by range. Do not combine `spans` with `page_size` or `cursor`.
|
|
29
29
|
|
|
30
30
|
## Locate
|
|
31
31
|
|
|
32
|
-
`action="locate"` returns atoms from the frozen anchor. Supply either:
|
|
32
|
+
`request.action="locate"` returns atoms from the frozen anchor. Supply either:
|
|
33
33
|
|
|
34
34
|
- `ref`: one direct atom lookup; or
|
|
35
35
|
- at least one real filter: `pattern`, `tool_name`, or `source` other than `any`.
|
|
@@ -42,7 +42,7 @@ A `g...` inventory ref is not a locate ref; use the group's `a...` start/end lan
|
|
|
42
42
|
|
|
43
43
|
## Plan
|
|
44
44
|
|
|
45
|
-
`action="plan"` uses `op="show"` by default. Agent and user mutate the same DraftPlan.
|
|
45
|
+
`request.action="plan"` uses `op="show"` by default. Agent and user mutate the same DraftPlan.
|
|
46
46
|
|
|
47
47
|
| op | Required fields |
|
|
48
48
|
|----|-----------------|
|
|
@@ -78,7 +78,7 @@ Committed blocks appear as protected atoms in later transaction snapshots and ca
|
|
|
78
78
|
|
|
79
79
|
## Recall
|
|
80
80
|
|
|
81
|
-
`action="recall"` works independently of a planning transaction and reads committed blocks active on the current branch.
|
|
81
|
+
`request.action="recall"` works independently of a planning transaction and reads committed blocks active on the current branch.
|
|
82
82
|
|
|
83
83
|
- Without `ref`, `pattern` searches block topics and summaries; `limit` defaults to 8 and has a maximum of 20.
|
|
84
84
|
- With `ref="c0001"`, the tool renders that block's stored messages.
|
package/src/SPEC.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# SPEC — midcompact extension module (`src/`)
|
|
2
|
+
|
|
3
|
+
Maintenance contract for `src/`. The model-facing usage contract lives in
|
|
4
|
+
`skills/midcompact/` (SKILL.md + references/tool-interface.md) and is
|
|
5
|
+
authoritative for observable tool/command behavior; this spec records the
|
|
6
|
+
durable invariants a future maintainer must preserve, which are not reliably
|
|
7
|
+
inferable from code at a glance. Entry point: `src/index.ts` (factory in
|
|
8
|
+
package.json → `pi.extensions`).
|
|
9
|
+
|
|
10
|
+
## Persistence
|
|
11
|
+
|
|
12
|
+
- Exactly three custom entry types are ever persisted on the session branch:
|
|
13
|
+
`midcompact-transaction`, `midcompact-draft`, `midcompact-state`
|
|
14
|
+
(`state.ts`). All are `version: 1`, shape-checked before use.
|
|
15
|
+
- Restore is *latest entry wins* over the branch. A draft restores only when
|
|
16
|
+
a transaction entry precedes it with a matching `transactionId`; the two
|
|
17
|
+
form one unit. State entries are independent of transactions.
|
|
18
|
+
- Commit appends a new state entry — never mutates old ones. Abort writes
|
|
19
|
+
nothing.
|
|
20
|
+
- Compatibility is a standing constraint: old transactions without
|
|
21
|
+
`startMode` default to `"agent"`; old ranges lacking factual char/image
|
|
22
|
+
fields are coerced (`coerceDraftRange`). Legacy `approxTokens` fields are
|
|
23
|
+
deprecated: never authoritative for decisions or UI.
|
|
24
|
+
- The planning lock is deliberately not persisted: reload clears the owner.
|
|
25
|
+
|
|
26
|
+
## Transactions
|
|
27
|
+
|
|
28
|
+
- Start freezes the current leaf as anchor, appends TXN + DRAFT, and routes
|
|
29
|
+
Agent-direct or User-manual. All planning happens on the child branch; the
|
|
30
|
+
discussion never enters the working context.
|
|
31
|
+
- Commit and abort both navigate back to the anchor (`navigateTree`); commit
|
|
32
|
+
then appends state, abort appends nothing. Both refuse to run while the
|
|
33
|
+
Agent holds the planning lock.
|
|
34
|
+
- `anchorUsage` is informational only — Pi-reported awareness, never an
|
|
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`).
|
|
41
|
+
- Atom refs are transaction-local: re-run inspect/locate in a later
|
|
42
|
+
transaction; group refs (`g...`) are never locate refs.
|
|
43
|
+
|
|
44
|
+
## Projection
|
|
45
|
+
|
|
46
|
+
- While blocks are active, the context hook replaces committed blocks with
|
|
47
|
+
their summary wrapper. The original history is restored only by recall;
|
|
48
|
+
recall never re-projects. Committed blocks appear as protected atoms in
|
|
49
|
+
later snapshots (no double compression).
|
|
50
|
+
|
|
51
|
+
## Atoms and ranges
|
|
52
|
+
|
|
53
|
+
- The atom is the smallest selectable unit; a tool call plus its matching
|
|
54
|
+
results is one indivisible `tool_exchange` atom.
|
|
55
|
+
- Protected (never compressible): incomplete/orphaned tool protocol,
|
|
56
|
+
existing compressed blocks, unsupported message kinds, entries lacking
|
|
57
|
+
the persistent anchor entry.
|
|
58
|
+
- Draft ranges never overlap and never contain protected atoms; boundaries
|
|
59
|
+
cannot be updated in place (remove + re-add). Empty `summary` = pending;
|
|
60
|
+
commit rejects pending, reversed, overlapping, or protected-crossing
|
|
61
|
+
ranges. `KEEP` is expressed by leaving atoms outside all ranges.
|
|
62
|
+
|
|
63
|
+
## Concurrency
|
|
64
|
+
|
|
65
|
+
- One runtime mutex over DraftPlan edits: owner ∈ {agent, ui}. Agent turns
|
|
66
|
+
hold it for their whole lifetime (`agent_start` → `agent_settled`); UIs
|
|
67
|
+
hold it per session via the `midcompactPlanningLock` API object (exposed
|
|
68
|
+
for UI and tests).
|
|
69
|
+
- Blocked operations notify and return — there is no queue.
|
|
70
|
+
|
|
71
|
+
## External contracts (reference, don't duplicate)
|
|
72
|
+
|
|
73
|
+
- Tool: one `midcompact` tool whose parameters are `{ request: <union> }` —
|
|
74
|
+
a root `type: "object"` wrapping a discriminated union on `action`
|
|
75
|
+
(inspect/locate/plan/recall); each branch is closed
|
|
76
|
+
(`additionalProperties: false`), so cross-action parameters are
|
|
77
|
+
schema-rejected. The `request` wrapper exists because some providers
|
|
78
|
+
(e.g. DeepSeek) reject a root-level `anyOf` before the model sees the
|
|
79
|
+
schema.
|
|
80
|
+
Details: `skills/midcompact/references/tool-interface.md`.
|
|
81
|
+
- Commands: `midcompact:start|abort|commit|review|review-webui|select|select-webui|status`;
|
|
82
|
+
no composite `/midcompact`; native naming convention `name:sub` (Pi's
|
|
83
|
+
`skill:<name>`).
|
|
84
|
+
- The tool never starts a transaction and never commits; both are command-
|
|
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.
|
|
104
|
+
|
|
105
|
+
## Change rules
|
|
106
|
+
|
|
107
|
+
- Adding a tool action → new request branch + handler type + tool-interface
|
|
108
|
+
section + SKILL.md routing; adding parameters to an action → its branch
|
|
109
|
+
only (the union stays nested under `request`).
|
|
110
|
+
- Changing persistence shapes → keep `version: 1` readable (coerce) or add
|
|
111
|
+
a migration; restore predicates (`state.ts`) are the compatibility gate.
|
|
112
|
+
- Renaming commands → update SKILL.md/README/tests together; the stale-name
|
|
113
|
+
failure mode is a doc-arbitrated contract violation.
|
|
114
|
+
- Test seams: `setOpenReviewWebBrowser` and the mocks in `test/` must stay
|
|
115
|
+
behavior-faithful to the real extension API; suites drive commands via
|
|
116
|
+
`pi.commands.get(...)` and the tool via plain param objects.
|
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/index.ts
CHANGED
|
@@ -67,55 +67,79 @@ const TOOL_DESCRIPTION =
|
|
|
67
67
|
const STATUS_KEY = "midcompact";
|
|
68
68
|
const START_PROMPT_PREFIX = "A mid-compaction transaction is active on a frozen anchor snapshot.";
|
|
69
69
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
70
|
+
// Canonical request model: one branch per action, and each branch owns exactly
|
|
71
|
+
// its own fields (additionalProperties: false). The discriminant is a
|
|
72
|
+
// single-value StringEnum instead of Type.Literal so it serializes as
|
|
73
|
+
// string+enum, which restricted JSON-Schema subsets (e.g. DeepSeek) accept
|
|
74
|
+
// more readily than const.
|
|
75
|
+
//
|
|
76
|
+
// The union sits under a root `request` property instead of being the
|
|
77
|
+
// parameters root: some providers validate that a tool's parameters root is
|
|
78
|
+
// `type: "object"` and reject a root-level anyOf before the model ever sees
|
|
79
|
+
// the schema (observed on DeepSeek). Field descriptions stay in sync with
|
|
80
|
+
// skills/midcompact/references/tool-interface.md.
|
|
81
|
+
const InspectRequest = Type.Object(
|
|
82
|
+
{
|
|
83
|
+
action: StringEnum(["inspect"] as const, { description: "Inventory the frozen anchor, or measure explicit candidate spans." }),
|
|
84
|
+
spans: Type.Optional(Type.Array(Type.Object({ start: Type.String(), end: Type.String() }), { description: "Candidate spans to measure, as {start,end} atom refs." })),
|
|
85
|
+
page_size: Type.Optional(Type.Number({ description: "Inventory groups per page (default 20, max 50)." })),
|
|
86
|
+
cursor: Type.Optional(Type.String({ description: "Pagination cursor from the previous page." })),
|
|
87
|
+
},
|
|
88
|
+
{ additionalProperties: false },
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const LocateRequest = Type.Object(
|
|
92
|
+
{
|
|
93
|
+
action: StringEnum(["locate"] as const, { description: "Locate atoms in the frozen anchor by ref or filters." }),
|
|
94
|
+
ref: Type.Optional(Type.String({ description: "One direct atom ref; mutually exclusive with search filters." })),
|
|
95
|
+
pattern: Type.Optional(Type.String({ description: "Content filter over anchor atoms." })),
|
|
96
|
+
source: Type.Optional(StringEnum(["any", "user", "assistant", "tool_call", "tool_result"] as const, { description: "Filter by entry source." })),
|
|
97
|
+
tool_name: Type.Optional(Type.String({ description: "Filter by originating tool name." })),
|
|
98
|
+
direction: Type.Optional(StringEnum(["oldest", "newest"] as const, { description: "Match ordering, oldest (default) or newest." })),
|
|
99
|
+
limit: Type.Optional(Type.Number({ description: "1-3 results for filtered searches." })),
|
|
100
|
+
detail: Type.Optional(StringEnum(["brief", "full"] as const, { description: "brief (default) or full atom output." })),
|
|
101
|
+
},
|
|
102
|
+
{ additionalProperties: false },
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const PlanRequest = Type.Object(
|
|
106
|
+
{
|
|
107
|
+
action: StringEnum(["plan"] as const, { description: "Show or mutate the shared DraftPlan." }),
|
|
108
|
+
op: Type.Optional(StringEnum(["show", "add", "update", "remove"] as const, { description: "show (default) / add / update / remove." })),
|
|
109
|
+
start: Type.Optional(Type.String({ description: "add: range start atom ref." })),
|
|
110
|
+
end: Type.Optional(Type.String({ description: "add: range end atom ref." })),
|
|
111
|
+
draft_id: Type.Optional(Type.String({ description: "show/update/remove: target draft range id." })),
|
|
112
|
+
topic: Type.Optional(Type.String({ description: "add/update: range topic." })),
|
|
113
|
+
summary: Type.Optional(Type.String({ description: "add/update: range summary (omitted or empty = pending range)." })),
|
|
114
|
+
detail: Type.Optional(StringEnum(["brief", "full"] as const, { description: "show: brief (default) or full range output." })),
|
|
115
|
+
},
|
|
116
|
+
{ additionalProperties: false },
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const RecallRequest = Type.Object(
|
|
120
|
+
{
|
|
121
|
+
action: StringEnum(["recall"] as const, { description: "Read committed compression blocks; works without a transaction." }),
|
|
122
|
+
ref: Type.Optional(Type.String({ description: "One committed block id, e.g. c0001; renders its messages." })),
|
|
123
|
+
pattern: Type.Optional(Type.String({ description: "Filter block topics and summaries." })),
|
|
124
|
+
limit: Type.Optional(Type.Number({ description: "Blocks to list (default 8, max 20)." })),
|
|
125
|
+
detail: Type.Optional(StringEnum(["brief", "full"] as const, { description: "full raises the rendering cap on truncated blocks." })),
|
|
126
|
+
},
|
|
127
|
+
{ additionalProperties: false },
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const Params = Type.Object(
|
|
131
|
+
{ request: Type.Union([InspectRequest, LocateRequest, PlanRequest, RecallRequest]) },
|
|
132
|
+
{
|
|
133
|
+
additionalProperties: false,
|
|
134
|
+
description: "`request.action` selects exactly one request shape; fields of the other actions are not valid.",
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
type ToolParams = Static<typeof Params>;
|
|
139
|
+
type InspectRequestType = Static<typeof InspectRequest>;
|
|
140
|
+
type LocateRequestType = Static<typeof LocateRequest>;
|
|
141
|
+
type PlanRequestType = Static<typeof PlanRequest>;
|
|
142
|
+
type RecallRequestType = Static<typeof RecallRequest>;
|
|
119
143
|
|
|
120
144
|
type RuntimeSnapshot = { atoms: Atom[]; anchorState?: CompressionState };
|
|
121
145
|
|
|
@@ -171,7 +195,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
171
195
|
content: [
|
|
172
196
|
"An active midcompact transaction exists with a persisted DraftPlan.",
|
|
173
197
|
`Draft revision ${currentDraft.revision}; ${currentDraft.ranges.length} existing range(s), which may have been created by the user.`,
|
|
174
|
-
"If the current user request asks to continue midcompact, read the `midcompact` skill first, then call midcompact(action
|
|
198
|
+
"If the current user request asks to continue midcompact, read the `midcompact` skill first, then call midcompact(request={action:\"plan\", op:\"show\"}) before any other midcompact action. Treat the existing plan as the current shared draft. Infer from the user's request whether to preserve, refine, or extend it; ask only if materially ambiguous.",
|
|
175
199
|
].join("\n"),
|
|
176
200
|
display: false,
|
|
177
201
|
},
|
|
@@ -573,9 +597,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
573
597
|
label: "Midcompact",
|
|
574
598
|
description: TOOL_DESCRIPTION,
|
|
575
599
|
parameters: Params,
|
|
576
|
-
async execute(_id: string, params:
|
|
600
|
+
async execute(_id: string, params: ToolParams, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
|
|
577
601
|
try {
|
|
578
|
-
|
|
602
|
+
const request = params.request;
|
|
603
|
+
if (request.action === "recall") return toolResult(handleRecall(request, ctx));
|
|
579
604
|
const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
|
|
580
605
|
const currentTx = withCompatDefaults(restored.transaction ?? transaction);
|
|
581
606
|
if (!currentTx) return toolResult("No active midcompact transaction. Ask the user to run `/midcompact:start` first.");
|
|
@@ -586,14 +611,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
586
611
|
}
|
|
587
612
|
const snapshot = buildAnchorSnapshot(ctx.sessionManager, currentTx);
|
|
588
613
|
|
|
589
|
-
if (
|
|
590
|
-
if (
|
|
591
|
-
if (
|
|
592
|
-
const result = handlePlan(
|
|
614
|
+
if (request.action === "inspect") return toolResult(handleInspect(request, snapshot.atoms, currentTx));
|
|
615
|
+
if (request.action === "locate") return toolResult(handleLocate(request, snapshot.atoms));
|
|
616
|
+
if (request.action === "plan") {
|
|
617
|
+
const result = handlePlan(request, draft!, snapshot.atoms);
|
|
593
618
|
if (result.op === "show") {
|
|
594
619
|
return toolResult(formatDraft(draft!, draftTelemetry(transaction, draft), {
|
|
595
|
-
detail:
|
|
596
|
-
draftId:
|
|
620
|
+
detail: request.detail,
|
|
621
|
+
draftId: request.draft_id,
|
|
597
622
|
atoms: snapshot.atoms,
|
|
598
623
|
}));
|
|
599
624
|
}
|
|
@@ -609,7 +634,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
609
634
|
},
|
|
610
635
|
});
|
|
611
636
|
|
|
612
|
-
function handleInspect(params:
|
|
637
|
+
function handleInspect(params: InspectRequestType, atoms: Atom[], tx: TransactionState): string {
|
|
613
638
|
if (params.spans) {
|
|
614
639
|
if (params.page_size !== undefined || params.cursor !== undefined) {
|
|
615
640
|
throw new Error("inspect spans cannot be combined with inventory pagination.");
|
|
@@ -620,7 +645,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
620
645
|
return formatInventory(page);
|
|
621
646
|
}
|
|
622
647
|
|
|
623
|
-
function handleRecall(params:
|
|
648
|
+
function handleRecall(params: RecallRequestType, ctx: ExtensionContext): string {
|
|
624
649
|
const sm = ctx.sessionManager;
|
|
625
650
|
const branchState = restoreCompressionState(sm.getBranch() as SessionEntry[]) ?? activeState;
|
|
626
651
|
if (!branchState?.blocks.length) return "No compressed blocks are active on this branch.";
|
|
@@ -656,7 +681,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
656
681
|
|
|
657
682
|
// ---- Pure handlers ----
|
|
658
683
|
|
|
659
|
-
function handleLocate(params:
|
|
684
|
+
function handleLocate(params: LocateRequestType, atoms: Atom[]): string {
|
|
660
685
|
const hasFilter = Boolean(params.pattern || params.tool_name || (params.source && params.source !== "any"));
|
|
661
686
|
if (params.ref && hasFilter) {
|
|
662
687
|
throw new Error("locate accepts either one direct ref or search filters, not both.");
|
|
@@ -688,7 +713,7 @@ type PlanHandleResult =
|
|
|
688
713
|
| { op: "show"; draft: DraftPlan }
|
|
689
714
|
| { op: "add" | "update" | "remove"; draft: DraftPlan; changedId: string };
|
|
690
715
|
|
|
691
|
-
function handlePlan(params:
|
|
716
|
+
function handlePlan(params: PlanRequestType, current: DraftPlan, atoms: Atom[]): PlanHandleResult {
|
|
692
717
|
const op = params.op ?? "show";
|
|
693
718
|
if (op === "show") return { op, draft: current };
|
|
694
719
|
if (op === "remove") {
|