pi-midcompact 0.6.0 → 0.7.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 +11 -11
- package/package.json +1 -1
- package/skills/midcompact/SKILL.md +53 -86
- package/skills/midcompact/references/tool-interface.md +41 -59
- package/src/SPEC.md +11 -6
- package/src/index.ts +232 -118
- package/src/inventory.ts +2 -2
- package/src/plan.ts +10 -10
- package/src/review-ui.ts +2 -2
- package/src/selection-ui.ts +1 -1
- package/src/start-ui.ts +2 -2
- package/src/telemetry.ts +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ The name `pi-midcompact` points to the other option: **mid-context compression**
|
|
|
17
17
|
|
|
18
18
|
## What it does
|
|
19
19
|
|
|
20
|
-
At a natural checkpoint, `pi-midcompact` freezes the active session leaf as an anchor and opens a separate transaction. The Agent proposes ranges for completed low-value phases, preserves load-bearing messages verbatim, and prepares a
|
|
20
|
+
At a natural checkpoint, `pi-midcompact` freezes the active session leaf as an anchor and opens a separate transaction. The Agent proposes ranges for completed low-value phases, preserves load-bearing messages verbatim, and prepares a plan for review. Nothing changes until you explicitly commit the plan.
|
|
21
21
|
|
|
22
22
|
### User-directed depth, Agent-designed plan
|
|
23
23
|
|
|
@@ -29,7 +29,7 @@ That is planning guidance, not an enforced token target: semantic importance win
|
|
|
29
29
|
|
|
30
30
|
### A selective projection, prepared on a temporary branch
|
|
31
31
|
|
|
32
|
-
`/midcompact:start` freezes the current session leaf as an **anchor**. Planning happens on a disposable child branch, so the discussion used to create and edit the
|
|
32
|
+
`/midcompact:start` freezes the current session leaf as an **anchor**. Planning happens on a disposable child branch, so the discussion used to create and edit the plan never becomes part of the committed working context.
|
|
33
33
|
|
|
34
34
|
```text
|
|
35
35
|
Frozen anchor: raw session history
|
|
@@ -53,9 +53,9 @@ The raw session JSONL still contains:
|
|
|
53
53
|
[original d1]──[decision to KEEP]──[original d2]──[latest work]
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
### A reviewed
|
|
56
|
+
### A reviewed plan can reclaim meaningful context
|
|
57
57
|
|
|
58
|
-
The earlier browser and TUI captures below illustrate a
|
|
58
|
+
The earlier browser and TUI captures below illustrate a plan 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">
|
|
@@ -94,7 +94,7 @@ pi-midcompact — mid-context compression, review, then human commit
|
|
|
94
94
|
| Starts | Automatically near the context limit, or with `/compact` | At an explicit natural checkpoint with `/midcompact:start` |
|
|
95
95
|
| Selects | One older contiguous prefix; keeps a recent token-budgeted tail | One or more reviewed ranges, including non-contiguous ranges and `KEEP` holes |
|
|
96
96
|
| Planning | Optional one-shot instruction to focus the generated summary | User states scope and retention depth; the Agent discusses trade-offs and drafts selective ranges and summaries |
|
|
97
|
-
| Decision gate | Generates a compaction checkpoint directly |
|
|
97
|
+
| Decision gate | Generates a compaction checkpoint directly | Plan → TUI or browser review → explicit human `/midcompact:commit` |
|
|
98
98
|
| Best fit | Automatic context maintenance and overflow recovery | Deliberate cleanup of completed phases while retaining specific decisions verbatim |
|
|
99
99
|
|
|
100
100
|
`pi-midcompact` does not disable or replace Pi's automatic compaction; it gives you a separate, human-reviewed way to make selective reductions. See [Pi's compaction documentation](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/compaction.md) for the built-in mechanism.
|
|
@@ -127,7 +127,7 @@ Run:
|
|
|
127
127
|
/midcompact:start
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
Pi opens a three-way chooser before creating transaction state: **Agent direct**, **User manual**, or **Drop**. Agent direct is the first and default-highlighted option, matching the previous fast path. The chooser is the standard `select` dialog, identical in interactive and RPC mode; RPC carries it as an extension UI `select` message with a bounded timeout, so an unresponsive client cancels instead of blocking. print/JSON modes have no dialog and default to **Agent direct**. Agent direct starts the existing inventory-first Agent workflow. User manual sends the same transaction guidance with a final “acknowledge only” instruction; after the Agent replies briefly, the Selection workbench opens (browser-based outside interactive mode). It does not start planning or mutate the
|
|
130
|
+
Pi opens a three-way chooser before creating transaction state: **Agent direct**, **User manual**, or **Drop**. Agent direct is the first and default-highlighted option, matching the previous fast path. The chooser is the standard `select` dialog, identical in interactive and RPC mode; RPC carries it as an extension UI `select` message with a bounded timeout, so an unresponsive client cancels instead of blocking. print/JSON modes have no dialog and default to **Agent direct**. Agent direct starts the existing inventory-first Agent workflow. User manual sends the same transaction guidance with a final “acknowledge only” instruction; after the Agent replies briefly, the Selection workbench opens (browser-based outside interactive mode). It does not start planning or mutate the plan until the user hands off later. Save the initial plan, close the UI, then tell the Agent to continue when you are ready. You can include an initial focus in the same command:
|
|
131
131
|
|
|
132
132
|
```text
|
|
133
133
|
/midcompact:start Compress the early repository exploration, but keep user requirements verbatim.
|
|
@@ -168,7 +168,7 @@ Use Selection to create or change ranges and KEEP holes:
|
|
|
168
168
|
/midcompact:select-webui
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
Use the TUI or local browser Review surface to edit summaries/topics and reject ranges. Review deliberately does not create or resize ranges; reopen Selection for boundary changes. To continue with Agent after a user-created plan, send a normal message asking it to continue the current midcompact
|
|
171
|
+
Use the TUI or local browser Review surface to edit summaries/topics and reject ranges. Review deliberately does not create or resize ranges; reopen Selection for boundary changes. To continue with Agent after a user-created plan, send a normal message asking it to continue the current midcompact plan; the Agent is prompted to read the existing plan first.
|
|
172
172
|
|
|
173
173
|
### 4. Commit the reviewed compression
|
|
174
174
|
|
|
@@ -216,9 +216,9 @@ Enter/Esc/q close
|
|
|
216
216
|
| `/midcompact:select-webui` | Opens the local browser Selection workbench. |
|
|
217
217
|
| `/midcompact:review` | Opens summary/topic review in the native TUI. |
|
|
218
218
|
| `/midcompact:review-webui` | Opens summary/topic review in a local browser. |
|
|
219
|
-
| `/midcompact:commit` | Commits the reviewed
|
|
219
|
+
| `/midcompact:commit` | Commits the reviewed plan. Human only. |
|
|
220
220
|
| `/midcompact:abort` | Abandons the transaction and returns to the anchor. |
|
|
221
|
-
| `/midcompact:status` | Displays the current
|
|
221
|
+
| `/midcompact:status` | Displays the current plan, or the committed compression state on this branch. |
|
|
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
|
|
|
@@ -234,7 +234,7 @@ npm run dev:webui -- --port=4180 --no-open
|
|
|
234
234
|
|
|
235
235
|
The command opens a fixture router for `review-ready`, `review-pending`,
|
|
236
236
|
`selection-mixed`, `no-telemetry`, and `wide-content`. Each button opens an
|
|
237
|
-
isolated workbench with its own in-memory
|
|
237
|
+
isolated workbench with its own in-memory plan. Fixture pages survive browser
|
|
238
238
|
refresh and the page's Close action, reload HTML changes automatically, and
|
|
239
239
|
restart for imported TypeScript changes. Stop the router with `Ctrl+C`. Use
|
|
240
240
|
`dev/midcompact-debug-ui.ts` only when validating against an actual Pi session.
|
|
@@ -250,4 +250,4 @@ restart for imported TypeScript changes. Stop the router with `Ctrl+C`. Use
|
|
|
250
250
|
- **Native Pi `/compact` interaction needs more real-session validation.** Avoid relying on mixed automatic/native compaction behavior for critical work until it has been exercised in your environment.
|
|
251
251
|
- **Provider and extension interoperability needs more real-session validation.** Unusual message shapes, third-party context-transform ordering, and long-lived exact message fingerprints have not been broadly exercised.
|
|
252
252
|
- **Very long sessions are not stress-tested.** Large review snapshots and repeated block accumulation may eventually require consolidation.
|
|
253
|
-
- **Browser workbenches are local.** `select-webui` and `review-webui` bind to loopback and mutate the same branch-local
|
|
253
|
+
- **Browser workbenches are local.** `select-webui` and `review-webui` bind to loopback and mutate the same branch-local plan as the native TUI surfaces.
|
package/package.json
CHANGED
|
@@ -1,134 +1,101 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: midcompact
|
|
3
|
-
description: Use when
|
|
3
|
+
description: "Use when runtime starts or hands off midcompact planning, or current work needs originals from a committed block. Guides selective compression, local summaries, and recall."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Midcompact
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
This skill handles two independent tasks: planning compression and recalling committed content. Choose the route before calling the tool.
|
|
11
|
-
|
|
12
|
-
| Signal | Immediate duty | First action |
|
|
13
|
-
|--------|----------------|--------------|
|
|
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 | `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
|
-
| The user or a projected summary needs detail from a committed block | Retrieve that history only | Follow **Recall workflow** |
|
|
18
|
-
|
|
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.
|
|
20
|
-
|
|
21
|
-
The tool groups parameters by `action`: each action accepts only its own fields, and fields from other actions are rejected. Read `references/tool-interface.md` before adding workload-specific parameters, and treat the action-specific field lists there as exhaustive.
|
|
22
|
-
|
|
23
|
-
## Plan compression
|
|
8
|
+
The frozen **anchor** contains **atoms**, the smallest selectable units (ref `a0001`); tool calls and their results stay together. Agent and user share one **plan** of contiguous **ranges** with replacement summaries (id `d1`). **Measure** compares candidates without changing the plan. Human commit turns each range into a **block** (`c0001`), replacing its originals in future context. **Recall** retrieves those originals.
|
|
24
9
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
The start mode controls how the first draft is created. It does not determine whether a user selection is final, how much initiative the Agent should take, or how deeply the history should be investigated.
|
|
28
|
-
|
|
29
|
-
Apply one invariant:
|
|
30
|
-
|
|
31
|
-
> Every fact that can still affect future work must remain with sufficient fidelity, either outside the selected ranges or in their replacement summaries.
|
|
32
|
-
|
|
33
|
-
### Planning workflow
|
|
34
|
-
|
|
35
|
-
#### 1. Read the entry state and user intent
|
|
10
|
+
## Route the activation
|
|
36
11
|
|
|
37
|
-
|
|
12
|
+
| Signal | Immediate action |
|
|
13
|
+
|---|---|
|
|
14
|
+
| Runtime says `FINAL STATE: USER MANUAL` | Reply exactly `OK`; call no midcompact tool. |
|
|
15
|
+
| Runtime says `FINAL STATE: AGENT DIRECT` | Call `request={action:"inspect"}`. |
|
|
16
|
+
| Handoff reports a persisted plan and user asks to continue | Call `request={action:"plan_show"}` before other midcompact actions. |
|
|
17
|
+
| Current work needs originals from a committed block | Follow **Recall**; do not enter planning. |
|
|
38
18
|
|
|
39
|
-
|
|
19
|
+
Runtime instructions are authoritative. User-manual acknowledgement also forbids questions. Otherwise, complete the required first action before establishing direction.
|
|
40
20
|
|
|
41
|
-
|
|
21
|
+
Read `references/tool-interface.md` when composing calls or handling protection/truncation. Use the schema for fields and defaults.
|
|
42
22
|
|
|
43
|
-
|
|
23
|
+
## Establish the compression direction
|
|
44
24
|
|
|
45
|
-
|
|
25
|
+
If the user has not expressed a compression preference, ask before selecting ranges or investigating deeply:
|
|
46
26
|
|
|
47
|
-
|
|
27
|
+
> Do you want light cleanup or substantial room for what comes next? What should I focus on, and what must stay verbatim? You can give a target usage or delegate the choice.
|
|
48
28
|
|
|
49
|
-
|
|
29
|
+
Honor existing preferences; otherwise accept qualitative goals or delegation. Clarify ambiguous handoffs: refine the selection or work only within it? Respect explicit scope limits.
|
|
50
30
|
|
|
51
|
-
|
|
52
|
-
|-----------------------------------|--------------------|
|
|
53
|
-
| Repetitive or superseded exploration | Current intent and active constraints |
|
|
54
|
-
| Tool output absorbed into a conclusion | Exact errors, evidence, or wording whose form matters |
|
|
55
|
-
| A completed subtask's intermediate process | Decisions and rationale still governing the work |
|
|
56
|
-
| A rejected attempt's mechanics | Its still-relevant failure reason |
|
|
31
|
+
Match compression and investigation effort to that direction. Compare factual character/image measurements; do not promise exact post-commit token savings. If meeting a target requires sacrificing important information, explain the tradeoff.
|
|
57
32
|
|
|
58
|
-
|
|
33
|
+
## Select worthwhile ranges
|
|
59
34
|
|
|
60
|
-
|
|
35
|
+
Keep originals unless replacement serves the agreed goal; full-history coverage is not success.
|
|
61
36
|
|
|
62
|
-
|
|
37
|
+
Use `inspect` for structure and volume, not to infer unseen content. Ground candidates in visible conversation or targeted `locate_ref`/`locate_search` calls. Investigate only what could change selection; omit uncertain candidates or ask if investigation exceeds the agreed effort. Use `measure` when relative sizes affect the choice.
|
|
63
38
|
|
|
64
|
-
|
|
39
|
+
Weigh bulk removed against original detail lost and what retained context already carries. Technical eligibility alone does not justify compression.
|
|
65
40
|
|
|
66
|
-
|
|
41
|
+
### Common patterns
|
|
67
42
|
|
|
68
|
-
|
|
43
|
+
These are examples, not fixed rules about message roles or age.
|
|
69
44
|
|
|
70
|
-
|
|
45
|
+
- **Long execution between user messages.** Replace repetitive search, reading, editing, and testing when only results matter. Retain useful request/conclusion text and summarize key findings absent from it; tool output is not automatically noise.
|
|
46
|
+
- **Multi-turn discussion.** Summarize superseded proposals, repeated clarification, and detours. Keep failure reasons that explain decisions or prevent repeated mistakes; age alone does not make discussion obsolete.
|
|
71
47
|
|
|
72
|
-
|
|
48
|
+
One range reduces repetition in coherent discussion but may flatten distinctions. Semantic ranges allow different detail levels and KEEP holes, but risk duplication or broken causal links. Choose by content, not turn count; split around important originals.
|
|
73
49
|
|
|
74
|
-
|
|
50
|
+
### Preserve important user expression
|
|
75
51
|
|
|
76
|
-
|
|
52
|
+
Keep important user goals, constraints, preferences, corrections, authorizations, and refusals verbatim by default. Summaries can alter their scope or force. Ask for explicit agreement before summarizing them; general permission for aggressive compression is insufficient.
|
|
77
53
|
|
|
78
|
-
|
|
54
|
+
Ask Question, questionnaire, and similar tool results may contain user expression. Judge by the speaker, not entry type. Retain the whole indivisible tool-exchange atom containing an important answer; split around it.
|
|
79
55
|
|
|
80
|
-
|
|
56
|
+
Routine acknowledgements can be candidates; task completion alone does not invalidate important statements.
|
|
81
57
|
|
|
82
|
-
|
|
58
|
+
### Propose and stop
|
|
83
59
|
|
|
84
|
-
|
|
60
|
+
Use conversation landmarks to explain replacements, originals kept, and summary contents. Offer alternatives for meaningful tradeoffs, not every delegated choice.
|
|
85
61
|
|
|
86
|
-
|
|
62
|
+
Stop when the goal is reasonably served or further compression is not worth the loss, not when all eligible content is covered. Re-align if findings materially change the treatment.
|
|
87
63
|
|
|
88
|
-
|
|
89
|
-
- decisions, conclusions, and necessary rationale;
|
|
90
|
-
- relevant files, symbols, interfaces, commands, or configuration;
|
|
91
|
-
- completed changes and validation results;
|
|
92
|
-
- rejected approaches only when their failure reason still matters;
|
|
93
|
-
- unresolved issues and any established next step or continuation state.
|
|
64
|
+
## Write a local replacement
|
|
94
65
|
|
|
95
|
-
|
|
66
|
+
Summaries belong between retained earlier and later context, not as compression instructions or standalone handoffs.
|
|
96
67
|
|
|
97
|
-
|
|
68
|
+
Preserve necessary causes, consequential actions and findings, and resulting knowledge. Name important files, symbols, commands, or artifacts with their roles. Distinguish proposals from changes and assumptions from observed results. Length follows information needs, not a fixed ratio.
|
|
98
69
|
|
|
99
|
-
|
|
100
|
-
Goal and constraints:
|
|
101
|
-
Established state and decisions:
|
|
102
|
-
Artifacts and validation:
|
|
103
|
-
Open issues and established next state:
|
|
104
|
-
```
|
|
70
|
+
Preserve consequential process, not activity logs. Replace vague “files changed, tests passed” with specific evidence: “Compared caller and parser, found ignored boundary arguments, added rejection, and verified an invalid-argument test.” Include relevant paths and results from the source.
|
|
105
71
|
|
|
106
|
-
|
|
72
|
+
### Fit the retained surroundings
|
|
107
73
|
|
|
108
|
-
|
|
74
|
+
Read enough surrounding context to avoid duplication and preserve what it depends on from this range. Resolve dangling references without inventing facts. Recall is recovery, not a substitute for sufficient summaries.
|
|
109
75
|
|
|
110
|
-
|
|
76
|
+
A range's end is a historical boundary, not automatically the present state:
|
|
111
77
|
|
|
112
|
-
|
|
78
|
+
- Do not turn an old unfinished task into a current TODO. Omit it if later retained context resolves it and its earlier status adds nothing.
|
|
79
|
+
- If the earlier status explains later events, describe it as historical. Do not attribute a later result to work within this range.
|
|
80
|
+
- Include current open issues and established next steps only when the range reaches the current working frontier and continuation needs them. Never invent a next action.
|
|
113
81
|
|
|
114
|
-
|
|
82
|
+
If later retained turns test and reject approach A, “Next: validate A” is stale. Preserve why A was proposed if needed to explain the rejection; leave later outcomes where they belong.
|
|
115
83
|
|
|
116
|
-
|
|
84
|
+
## Build, check, and hand off
|
|
117
85
|
|
|
118
|
-
|
|
86
|
+
Resolve boundaries with targeted lookups; split around protected atoms. Use `plan_add`, `plan_update`, and `plan_remove`. Updates change summary/topic; boundaries require remove + add. Fill pending summaries before handoff.
|
|
119
87
|
|
|
120
|
-
|
|
88
|
+
Call `plan_show`; use `plan_read` when a stored summary needs full review. Check both:
|
|
121
89
|
|
|
122
|
-
|
|
90
|
+
- **Selection:** Does each range serve the user's direction? Are important originals and intended KEEP holes retained? Has unnecessary full-history coverage crept in?
|
|
91
|
+
- **Replacement:** Mentally remove the originals. Do summaries plus retained context explain the necessary causes, actions, evidence, and outcomes without gaps, vague claims, stale instructions, or state conflicts?
|
|
123
92
|
|
|
124
|
-
|
|
93
|
+
Report replacements, retained originals, and stopping rationale. Boundaries: `/midcompact:select` or `/midcompact:select-webui`. Summaries: `/midcompact:review` or `/midcompact:review-webui`. Prefer browser variants when TUI is unavailable or unwanted. Only the user runs `/midcompact:commit`.
|
|
125
94
|
|
|
126
|
-
|
|
95
|
+
## Recall
|
|
127
96
|
|
|
128
|
-
|
|
97
|
+
Recall reads active committed blocks without changing a plan and needs no transaction. If the id is unknown, use `request={action:"recall_list",pattern:"..."}` to search ids, topics, and summaries, not originals. Projected summaries carry block ids and recall calls.
|
|
129
98
|
|
|
130
|
-
|
|
131
|
-
- `a0001` is a transaction-local atom ref; `d1` is a DraftPlan range id; `c0001` is an active committed-block id.
|
|
132
|
-
- Protected atoms cannot enter a range. Split around them.
|
|
99
|
+
Use `request={action:"recall_read",block:"c0001"}`; retry truncation with `detail:"full"`. If still truncated, read `references/tool-interface.md`. Retrieve only needed content.
|
|
133
100
|
|
|
134
|
-
|
|
101
|
+
`a...` refs are transaction-local; `d...` identifies a plan range; `c...` a committed block. Inventory `g...` labels are display-only; use listed atom refs for calls.
|
|
@@ -1,87 +1,69 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Tool Call Patterns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use the schema for fields/defaults. Follow only the steps needed. Example refs and summaries must be replaced with verified content.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Survey where context space goes
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Start a fresh Agent-direct transaction with:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
- `page_size`: default 20 groups, maximum 50.
|
|
14
|
-
- `cursor`: opaque value returned by the previous page.
|
|
15
|
-
- Results include group refs, atom spans, content chars, image facts, protected/compressible counts, and Pi-reported anchor usage.
|
|
16
|
-
|
|
17
|
-
Stop paging after the candidate regions are covered.
|
|
18
|
-
|
|
19
|
-
To compare explicit candidates without mutating the DraftPlan, pass one or more possibly overlapping spans:
|
|
20
|
-
|
|
21
|
-
```text
|
|
22
|
-
midcompact(request={action:"inspect", spans=[
|
|
23
|
-
{"start":"a0006","end":"a0014"},
|
|
24
|
-
{"start":"a0006","end":"a0020"}
|
|
25
|
-
]})
|
|
9
|
+
```json
|
|
10
|
+
{"request":{"action":"inspect"}}
|
|
26
11
|
```
|
|
27
12
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## Locate
|
|
13
|
+
Read overall Pi token usage and chronological groups: user landmarks, endpoints, characters, images, protection counts. This paginated volume distribution is not a per-group token curve. Large groups invite investigation, not automatic compression.
|
|
31
14
|
|
|
32
|
-
`
|
|
15
|
+
Continue with the returned `cursor` only where relevant. Use listed `a...` refs, not display-only `g...` labels. Landmarks are not full content; combine the overview with user preferences before selection.
|
|
33
16
|
|
|
34
|
-
|
|
35
|
-
- at least one real filter: `pattern`, `tool_name`, or `source` other than `any`.
|
|
17
|
+
## Find evidence and check boundaries
|
|
36
18
|
|
|
37
|
-
|
|
19
|
+
For a recent bash result containing a known phrase:
|
|
38
20
|
|
|
39
|
-
|
|
21
|
+
```json
|
|
22
|
+
{"request":{"action":"locate_search","source":"tool_result","tool_name":"bash","pattern":"timeout","direction":"newest"}}
|
|
23
|
+
```
|
|
40
24
|
|
|
41
|
-
|
|
25
|
+
Filters combine with AND. Use a returned atom ref to inspect the match:
|
|
42
26
|
|
|
43
|
-
|
|
27
|
+
```json
|
|
28
|
+
{"request":{"action":"locate_ref","ref":"a0007","detail":"full"}}
|
|
29
|
+
```
|
|
44
30
|
|
|
45
|
-
|
|
31
|
+
Known refs need no search. Excerpts are incomplete; `full` can omit the middle. Check markers before drawing conclusions. Narrow excessive matches with filters. Recover unknown refs using `inspect` or `locate_search`; `measure` needs known refs.
|
|
46
32
|
|
|
47
|
-
|
|
48
|
-
|----|-----------------|
|
|
49
|
-
| `show` | none; optional `draft_id` for one range |
|
|
50
|
-
| `add` | `start`, `end`; optional `summary`, `topic` |
|
|
51
|
-
| `update` | `draft_id` and at least one of `summary`, `topic` |
|
|
52
|
-
| `remove` | `draft_id` |
|
|
33
|
+
## Compare candidates without changing the plan
|
|
53
34
|
|
|
54
|
-
|
|
35
|
+
```json
|
|
36
|
+
{"request":{"action":"measure","candidates":[{"start":"a0006","end":"a0014"},{"start":"a0006","end":"a0020"}]}}
|
|
37
|
+
```
|
|
55
38
|
|
|
56
|
-
|
|
39
|
+
Candidates may overlap without changing the plan. Compare landmarks, content share, images, and protection, not assumed token savings. If fewer candidates are reported than requested, measure the remainder separately.
|
|
57
40
|
|
|
58
|
-
|
|
41
|
+
## Keep important originals between ranges
|
|
59
42
|
|
|
60
|
-
|
|
43
|
+
If an important user answer occupies `a0010`, locate it and verify boundaries, then add separate ranges on its two sides:
|
|
61
44
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
| `start` occurs after `end` | Correct the positional order |
|
|
67
|
-
| An atom ref is unknown | Re-run inspect/locate against the current transaction snapshot |
|
|
45
|
+
```json
|
|
46
|
+
{"request":{"action":"plan_add","start":"a0006","end":"a0009","summary":"<replacement for the earlier work>"}}
|
|
47
|
+
{"request":{"action":"plan_add","start":"a0011","end":"a0014","summary":"<replacement for the later work>"}}
|
|
48
|
+
```
|
|
68
49
|
|
|
69
|
-
|
|
50
|
+
Question-tool answers are user expression. Keep the whole tool-exchange atom containing an important answer; do not split a call from its results. Unlike measurement candidates, plan ranges cannot overlap or contain protected atoms.
|
|
70
51
|
|
|
71
|
-
##
|
|
52
|
+
## Refine an existing plan
|
|
72
53
|
|
|
73
|
-
|
|
54
|
+
Start with `plan_show`. Use `plan_read` with its returned `range_id` when the stored summary or endpoints need full review. Change summary/topic with `plan_update`.
|
|
74
55
|
|
|
75
|
-
|
|
56
|
+
For new boundaries, first read and retain the existing summary/topic and verify the replacement endpoints. Then `plan_remove` the old range and `plan_add` its replacement. Do not assume the new id is unchanged. Check the resulting plan; mutation replies show the changed item and compact totals, not all ranges. Fill pending summaries before handoff; only the user commits.
|
|
76
57
|
|
|
77
|
-
|
|
58
|
+
## Work around protected content
|
|
78
59
|
|
|
79
|
-
|
|
60
|
+
Split around protected atoms. Causes include incomplete tool exchanges, unsupported message kinds, missing persistent entries, and existing committed blocks. Later transactions can compress remaining raw history around those blocks, not recompress the blocks themselves. Obtain current atom refs from `inspect`; refs are transaction-local.
|
|
80
61
|
|
|
81
|
-
|
|
62
|
+
## Retrieve committed evidence
|
|
82
63
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
64
|
+
```json
|
|
65
|
+
{"request":{"action":"recall_list","pattern":"timeout"}}
|
|
66
|
+
{"request":{"action":"recall_read","block":"c0001"}}
|
|
67
|
+
```
|
|
86
68
|
|
|
87
|
-
|
|
69
|
+
Use the actual block id returned by the list; skip listing when known. Listing searches ids/topics/summaries, not originals. Recall needs no transaction. If truncated, retry the same block with `detail:"full"`. There is no paging: if still truncated, report that recall cannot expose the omitted evidence. Do not infer it or start another transaction as a recovery workaround. Inspecting the source session requires a separate available means; this tool offers none.
|
package/src/SPEC.md
CHANGED
|
@@ -71,12 +71,16 @@ package.json → `pi.extensions`).
|
|
|
71
71
|
## External contracts (reference, don't duplicate)
|
|
72
72
|
|
|
73
73
|
- Tool: one `midcompact` tool whose parameters are `{ request: <union> }` —
|
|
74
|
-
a root `type: "object"` wrapping a
|
|
75
|
-
(inspect/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
a root `type: "object"` wrapping a union of 11 single-purpose actions
|
|
75
|
+
(inspect/measure/locate_ref/locate_search/plan_show/plan_read/plan_add/
|
|
76
|
+
plan_update/plan_remove/recall_list/recall_read); each branch is closed
|
|
77
|
+
(`additionalProperties: false`), so extra fields are rejected (schema where
|
|
78
|
+
enforced, runtime backstop everywhere — a silently ignored field is a bug).
|
|
79
|
+
Former second-level discriminators (plan `op`, locate ref-vs-filter, recall
|
|
80
|
+
list-vs-render, inspect inventory-vs-spans) are flattened into branches so
|
|
81
|
+
field legality is schema-visible. The `request` wrapper exists because some
|
|
82
|
+
providers (e.g. DeepSeek) reject a root-level `anyOf` before the model sees
|
|
83
|
+
the schema.
|
|
80
84
|
Details: `skills/midcompact/references/tool-interface.md`.
|
|
81
85
|
- Commands: `midcompact:start|abort|commit|review|review-webui|select|select-webui|status`;
|
|
82
86
|
no composite `/midcompact`; native naming convention `name:sub` (Pi's
|
|
@@ -104,6 +108,7 @@ package.json → `pi.extensions`).
|
|
|
104
108
|
|
|
105
109
|
## Change rules
|
|
106
110
|
|
|
111
|
+
- Model-facing copy (schema descriptions, prompts, errors, notices, skill docs) follows `.dev/docs/agent-vocabulary.md`; a new action, field, or concept is registered there **before** implementation.
|
|
107
112
|
- Adding a tool action → new request branch + handler type + tool-interface
|
|
108
113
|
section + SKILL.md routing; adding parameters to an action → its branch
|
|
109
114
|
only (the union stays nested under `request`).
|
package/src/index.ts
CHANGED
|
@@ -63,13 +63,16 @@ import {
|
|
|
63
63
|
|
|
64
64
|
const TOOL_NAME = "midcompact";
|
|
65
65
|
const TOOL_DESCRIPTION =
|
|
66
|
-
"
|
|
66
|
+
"Inspect, measure, locate, plan, or recall mid-context compression. Use the `midcompact` skill to route planning versus recall; during an active transaction, follow the runtime prompt for the state-specific first action.";
|
|
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
|
-
// Canonical request model: one branch per
|
|
71
|
-
// its own fields (additionalProperties: false).
|
|
72
|
-
//
|
|
70
|
+
// Canonical request model: one branch per operation, and each branch owns
|
|
71
|
+
// exactly its own fields (additionalProperties: false). Second-level operation
|
|
72
|
+
// discriminators (the former plan `op`, locate ref-vs-filter, recall
|
|
73
|
+
// list-vs-render, inspect inventory-vs-spans) are flattened into top-level
|
|
74
|
+
// branches so field legality is visible in the schema itself. The discriminant
|
|
75
|
+
// is a single-value StringEnum instead of Type.Literal so it serializes as
|
|
73
76
|
// string+enum, which restricted JSON-Schema subsets (e.g. DeepSeek) accept
|
|
74
77
|
// more readily than const.
|
|
75
78
|
//
|
|
@@ -80,66 +83,165 @@ const START_PROMPT_PREFIX = "A mid-compaction transaction is active on a frozen
|
|
|
80
83
|
// skills/midcompact/references/tool-interface.md.
|
|
81
84
|
const InspectRequest = Type.Object(
|
|
82
85
|
{
|
|
83
|
-
action: StringEnum(["inspect"] as const, { description: "
|
|
84
|
-
|
|
85
|
-
page_size: Type.Optional(Type.Number({ description: "Inventory groups per page (default 20, max 50)." })),
|
|
86
|
+
action: StringEnum(["inspect"] as const, { description: "Page through the frozen anchor inventory: groups, refs, sizes, protected/compressible counts." }),
|
|
87
|
+
page_size: Type.Optional(Type.Number({ description: "Groups per page (default 20, max 50; out-of-range values are clamped)." })),
|
|
86
88
|
cursor: Type.Optional(Type.String({ description: "Pagination cursor from the previous page." })),
|
|
87
89
|
},
|
|
88
90
|
{ additionalProperties: false },
|
|
89
91
|
);
|
|
90
92
|
|
|
91
|
-
const
|
|
93
|
+
const MeasureRequest = Type.Object(
|
|
92
94
|
{
|
|
93
|
-
action: StringEnum(["
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
action: StringEnum(["measure"] as const, { description: "Measure candidate {start,end} atom spans without changing the plan." }),
|
|
96
|
+
candidates: Type.Array(
|
|
97
|
+
Type.Object({ start: Type.String({ description: "Candidate start atom ref." }), end: Type.String({ description: "Candidate end atom ref." }) }),
|
|
98
|
+
{ minItems: 1, description: "Candidate spans to measure, e.g. [{start:\"a0006\",end:\"a0014\"}]." },
|
|
99
|
+
),
|
|
100
|
+
},
|
|
101
|
+
{ additionalProperties: false },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const LocateRefRequest = Type.Object(
|
|
105
|
+
{
|
|
106
|
+
action: StringEnum(["locate_ref"] as const, { description: "Look up one atom by ref in the frozen anchor." }),
|
|
107
|
+
ref: Type.String({ description: "Atom ref, e.g. a0001. Group labels shown by inspect are not atom refs." }),
|
|
108
|
+
detail: Type.Optional(StringEnum(["brief", "full"] as const, { description: "brief (default) bounded preview; full atom text up to 12,000 characters." })),
|
|
109
|
+
},
|
|
110
|
+
{ additionalProperties: false },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const LocateSearchRequest = Type.Object(
|
|
114
|
+
{
|
|
115
|
+
action: StringEnum(["locate_search"] as const, { description: "Search anchor atoms with at least one filter; filters combine conjunctively (AND)." }),
|
|
116
|
+
pattern: Type.Optional(Type.String({ description: "Case-insensitive substring filter over atom text." })),
|
|
117
|
+
source: Type.Optional(StringEnum(["user", "assistant", "tool_call", "tool_result"] as const, { description: "Filter by entry source class." })),
|
|
118
|
+
tool_name: Type.Optional(Type.String({ description: "Exact (case-insensitive) match on the originating tool name." })),
|
|
98
119
|
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
|
|
100
|
-
|
|
120
|
+
limit: Type.Optional(Type.Number({ description: "1-3 results (out-of-range values are clamped)." })),
|
|
121
|
+
},
|
|
122
|
+
{ additionalProperties: false },
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const PlanShowRequest = Type.Object(
|
|
126
|
+
{
|
|
127
|
+
action: StringEnum(["plan_show"] as const, { description: "List every plan range in brief form with plan telemetry." }),
|
|
101
128
|
},
|
|
102
129
|
{ additionalProperties: false },
|
|
103
130
|
);
|
|
104
131
|
|
|
105
|
-
const
|
|
132
|
+
const PlanReadRequest = Type.Object(
|
|
106
133
|
{
|
|
107
|
-
action: StringEnum(["
|
|
108
|
-
|
|
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." })),
|
|
134
|
+
action: StringEnum(["plan_read"] as const, { description: "Read one plan range in full: stored summary and endpoint previews under a 40,000-character budget." }),
|
|
135
|
+
range_id: Type.String({ description: "Target range id from plan_show, e.g. d1." }),
|
|
115
136
|
},
|
|
116
137
|
{ additionalProperties: false },
|
|
117
138
|
);
|
|
118
139
|
|
|
119
|
-
const
|
|
140
|
+
const PlanAddRequest = Type.Object(
|
|
120
141
|
{
|
|
121
|
-
action: StringEnum(["
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
142
|
+
action: StringEnum(["plan_add"] as const, { description: "Add one range over contiguous atoms; boundaries are immutable after add." }),
|
|
143
|
+
start: Type.String({ description: "Range start atom ref." }),
|
|
144
|
+
end: Type.String({ description: "Range end atom ref (inclusive)." }),
|
|
145
|
+
summary: Type.Optional(Type.String({ description: "Replacement summary; omitted or empty leaves the range pending." })),
|
|
146
|
+
topic: Type.Optional(Type.String({ description: "Optional range topic." })),
|
|
147
|
+
},
|
|
148
|
+
{ additionalProperties: false },
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const PlanUpdateRequest = Type.Object(
|
|
152
|
+
{
|
|
153
|
+
action: StringEnum(["plan_update"] as const, { description: "Update one range's summary and/or topic; boundaries change via plan_remove + plan_add." }),
|
|
154
|
+
range_id: Type.String({ description: "Target range id, e.g. d1." }),
|
|
155
|
+
summary: Type.Optional(Type.String({ description: "New summary; empty string marks the range pending." })),
|
|
156
|
+
topic: Type.Optional(Type.String({ description: "New topic." })),
|
|
157
|
+
},
|
|
158
|
+
{ additionalProperties: false },
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const PlanRemoveRequest = Type.Object(
|
|
162
|
+
{
|
|
163
|
+
action: StringEnum(["plan_remove"] as const, { description: "Remove one range from the plan." }),
|
|
164
|
+
range_id: Type.String({ description: "Target range id, e.g. d1." }),
|
|
165
|
+
},
|
|
166
|
+
{ additionalProperties: false },
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const RecallListRequest = Type.Object(
|
|
170
|
+
{
|
|
171
|
+
action: StringEnum(["recall_list"] as const, { description: "List committed blocks; works without a transaction." }),
|
|
172
|
+
pattern: Type.Optional(Type.String({ description: "Case-insensitive filter over block id, topic, and summary (not original content)." })),
|
|
173
|
+
limit: Type.Optional(Type.Number({ description: "Blocks to list (default 8, max 20; out-of-range values are clamped)." })),
|
|
174
|
+
},
|
|
175
|
+
{ additionalProperties: false },
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const RecallReadRequest = Type.Object(
|
|
179
|
+
{
|
|
180
|
+
action: StringEnum(["recall_read"] as const, { description: "Render one committed block's original messages." }),
|
|
181
|
+
block: Type.String({ description: "Committed block id, e.g. c0001." }),
|
|
182
|
+
detail: Type.Optional(StringEnum(["brief", "full"] as const, { description: "full raises the rendering cap from 12,000 to 40,000 characters on truncated blocks." })),
|
|
126
183
|
},
|
|
127
184
|
{ additionalProperties: false },
|
|
128
185
|
);
|
|
129
186
|
|
|
130
187
|
const Params = Type.Object(
|
|
131
|
-
{
|
|
188
|
+
{
|
|
189
|
+
request: Type.Union([
|
|
190
|
+
InspectRequest,
|
|
191
|
+
MeasureRequest,
|
|
192
|
+
LocateRefRequest,
|
|
193
|
+
LocateSearchRequest,
|
|
194
|
+
PlanShowRequest,
|
|
195
|
+
PlanReadRequest,
|
|
196
|
+
PlanAddRequest,
|
|
197
|
+
PlanUpdateRequest,
|
|
198
|
+
PlanRemoveRequest,
|
|
199
|
+
RecallListRequest,
|
|
200
|
+
RecallReadRequest,
|
|
201
|
+
]),
|
|
202
|
+
},
|
|
132
203
|
{
|
|
133
204
|
additionalProperties: false,
|
|
134
|
-
description: "`request.action` selects exactly one request shape;
|
|
205
|
+
description: "`request.action` selects exactly one request shape; each shape accepts only its own fields.",
|
|
135
206
|
},
|
|
136
207
|
);
|
|
137
208
|
|
|
138
209
|
type ToolParams = Static<typeof Params>;
|
|
139
210
|
type InspectRequestType = Static<typeof InspectRequest>;
|
|
140
|
-
type
|
|
141
|
-
type
|
|
142
|
-
type
|
|
211
|
+
type MeasureRequestType = Static<typeof MeasureRequest>;
|
|
212
|
+
type LocateRefRequestType = Static<typeof LocateRefRequest>;
|
|
213
|
+
type LocateSearchRequestType = Static<typeof LocateSearchRequest>;
|
|
214
|
+
type PlanShowRequestType = Static<typeof PlanShowRequest>;
|
|
215
|
+
type PlanReadRequestType = Static<typeof PlanReadRequest>;
|
|
216
|
+
type PlanAddRequestType = Static<typeof PlanAddRequest>;
|
|
217
|
+
type PlanUpdateRequestType = Static<typeof PlanUpdateRequest>;
|
|
218
|
+
type PlanRemoveRequestType = Static<typeof PlanRemoveRequest>;
|
|
219
|
+
type RecallListRequestType = Static<typeof RecallListRequest>;
|
|
220
|
+
type RecallReadRequestType = Static<typeof RecallReadRequest>;
|
|
221
|
+
|
|
222
|
+
// Runtime closure backstop: providers are not trusted to enforce
|
|
223
|
+
// additionalProperties at call time, and a silently ignored field is worse
|
|
224
|
+
// than a rejection. Keys are the per-branch optional/required fields besides
|
|
225
|
+
// the discriminant.
|
|
226
|
+
const BRANCH_FIELDS: Record<ToolParams["request"]["action"], readonly string[]> = {
|
|
227
|
+
inspect: ["page_size", "cursor"],
|
|
228
|
+
measure: ["candidates"],
|
|
229
|
+
locate_ref: ["ref", "detail"],
|
|
230
|
+
locate_search: ["pattern", "source", "tool_name", "direction", "limit"],
|
|
231
|
+
plan_show: [],
|
|
232
|
+
plan_read: ["range_id"],
|
|
233
|
+
plan_add: ["start", "end", "summary", "topic"],
|
|
234
|
+
plan_update: ["range_id", "summary", "topic"],
|
|
235
|
+
plan_remove: ["range_id"],
|
|
236
|
+
recall_list: ["pattern", "limit"],
|
|
237
|
+
recall_read: ["block", "detail"],
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
function rejectExtraFields(request: ToolParams["request"]): void {
|
|
241
|
+
const allowed: readonly string[] = BRANCH_FIELDS[request.action];
|
|
242
|
+
const extras = Object.keys(request).filter((key) => key !== "action" && !allowed.includes(key));
|
|
243
|
+
if (extras.length > 0) throw new Error(`${request.action} does not accept: ${extras.join(", ")}.`);
|
|
244
|
+
}
|
|
143
245
|
|
|
144
246
|
type RuntimeSnapshot = { atoms: Atom[]; anchorState?: CompressionState };
|
|
145
247
|
|
|
@@ -193,9 +295,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
193
295
|
message: {
|
|
194
296
|
customType: "midcompact-handoff",
|
|
195
297
|
content: [
|
|
196
|
-
"An active midcompact transaction exists with a persisted
|
|
197
|
-
`
|
|
198
|
-
"If the current user request asks to continue midcompact, read the `midcompact` skill first, then call midcompact(request={action:\"
|
|
298
|
+
"An active midcompact transaction exists with a persisted plan.",
|
|
299
|
+
`Plan revision ${currentDraft.revision}; ${currentDraft.ranges.length} existing range(s), which may have been created by the user.`,
|
|
300
|
+
"If the current user request asks to continue midcompact, read the `midcompact` skill first, then call midcompact(request={action:\"plan_show\"}) before any other midcompact action. Treat the existing plan as the shared starting point. Infer from the user's request whether to preserve, refine, or extend it; ask only if materially ambiguous.",
|
|
199
301
|
].join("\n"),
|
|
200
302
|
display: false,
|
|
201
303
|
},
|
|
@@ -239,21 +341,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
239
341
|
},
|
|
240
342
|
});
|
|
241
343
|
pi.registerCommand("midcompact:commit", {
|
|
242
|
-
description: "Commit the current
|
|
344
|
+
description: "Commit the current plan to the branch state",
|
|
243
345
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
244
346
|
await ctx.waitForIdle();
|
|
245
347
|
return commitTransaction(ctx);
|
|
246
348
|
},
|
|
247
349
|
});
|
|
248
350
|
pi.registerCommand("midcompact:review", {
|
|
249
|
-
description: "Open the interactive TUI review to inspect and edit the
|
|
351
|
+
description: "Open the interactive TUI review to inspect and edit the plan",
|
|
250
352
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
251
353
|
await ctx.waitForIdle();
|
|
252
354
|
return reviewTransaction(ctx, "tui");
|
|
253
355
|
},
|
|
254
356
|
});
|
|
255
357
|
pi.registerCommand("midcompact:review-webui", {
|
|
256
|
-
description: "Open a local web page to inspect and edit the
|
|
358
|
+
description: "Open a local web page to inspect and edit the plan (works without TUI)",
|
|
257
359
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
258
360
|
await ctx.waitForIdle();
|
|
259
361
|
return reviewTransaction(ctx, "web");
|
|
@@ -274,7 +376,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
274
376
|
},
|
|
275
377
|
});
|
|
276
378
|
pi.registerCommand("midcompact:status", {
|
|
277
|
-
description: "Show current transaction and
|
|
379
|
+
description: "Show current transaction and plan status",
|
|
278
380
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
279
381
|
await ctx.waitForIdle();
|
|
280
382
|
return showStatus(ctx);
|
|
@@ -346,7 +448,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
346
448
|
return;
|
|
347
449
|
}
|
|
348
450
|
if (!tryAcquireUi(planningLock)) {
|
|
349
|
-
ctx.ui.notify("The Agent is currently processing the midcompact
|
|
451
|
+
ctx.ui.notify("The Agent is currently processing the midcompact plan. Try Selection after the Agent turn ends.", "warning");
|
|
350
452
|
return;
|
|
351
453
|
}
|
|
352
454
|
|
|
@@ -364,12 +466,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
364
466
|
if (action.action === "save") {
|
|
365
467
|
try {
|
|
366
468
|
applySelection(action.spans ?? [], action.keepRefs ?? []);
|
|
367
|
-
ctx.ui.notify("
|
|
469
|
+
ctx.ui.notify("Plan saved. Tell the Agent to continue processing it when ready.", "info");
|
|
368
470
|
} catch (error) {
|
|
369
471
|
ctx.ui.notify(`Selection could not be saved: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
370
472
|
}
|
|
371
473
|
} else {
|
|
372
|
-
ctx.ui.notify("Selection closed. The
|
|
474
|
+
ctx.ui.notify("Selection closed. The plan remains available; reopen select or tell the Agent to continue.", "info");
|
|
373
475
|
}
|
|
374
476
|
return;
|
|
375
477
|
}
|
|
@@ -386,7 +488,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
386
488
|
updateStatus(ctx, currentTx, draft, planningLock.owner);
|
|
387
489
|
},
|
|
388
490
|
}, "selection", { openBrowser: openReviewWebBrowser });
|
|
389
|
-
ctx.ui.notify("Selection closed. The
|
|
491
|
+
ctx.ui.notify("Selection closed. The plan is saved; tell the Agent to continue when ready.", "info");
|
|
390
492
|
} finally {
|
|
391
493
|
releaseUi(planningLock);
|
|
392
494
|
}
|
|
@@ -397,17 +499,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
397
499
|
const promptLines = [
|
|
398
500
|
START_PROMPT_PREFIX,
|
|
399
501
|
awareness,
|
|
400
|
-
"The extension provides inspect for bounded inventory, locate for
|
|
401
|
-
"The user owns the final compression decision. You may edit the
|
|
502
|
+
"The extension provides inspect for the bounded inventory, measure for candidate spans, locate for atom details, plan_show/plan_read/plan_add/plan_update/plan_remove for one shared plan, and recall_list/recall_read for committed blocks.",
|
|
503
|
+
"The user owns the final compression decision. You may edit the plan, but you must not commit. Preserve facts that future work still needs; local character and image counts are not token estimates.",
|
|
402
504
|
];
|
|
403
505
|
if (customInstructions) promptLines.push(`User focus: ${customInstructions}`);
|
|
404
506
|
if (mode === "agent") {
|
|
405
507
|
promptLines.push(
|
|
406
|
-
"FINAL STATE: AGENT DIRECT. The new
|
|
508
|
+
"FINAL STATE: AGENT DIRECT. The new plan is empty. Read the `midcompact` skill before doing any planning work, then call inspect first and use measure, locate, and the plan actions to create ranges and summaries. Stop before commit.",
|
|
407
509
|
);
|
|
408
510
|
} else {
|
|
409
511
|
promptLines.push(
|
|
410
|
-
"FINAL STATE: USER MANUAL. The user is about to edit the initial
|
|
512
|
+
"FINAL STATE: USER MANUAL. The user is about to edit the initial plan. Acknowledge with OK only. Do not call any midcompact action; do not change the plan or commit. Wait until the user finishes editing and sends a later request. On that later request, read the `midcompact` skill before doing any planning work, then call plan_show first.",
|
|
411
513
|
);
|
|
412
514
|
}
|
|
413
515
|
await pi.sendUserMessage(promptLines.join("\n"));
|
|
@@ -415,7 +517,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
415
517
|
|
|
416
518
|
async function abortTransaction(ctx: ExtensionCommandContext): Promise<void> {
|
|
417
519
|
if (planningLock.owner === "agent") {
|
|
418
|
-
ctx.ui.notify("The Agent is currently processing the midcompact
|
|
520
|
+
ctx.ui.notify("The Agent is currently processing the midcompact plan. Abort after the Agent turn ends.", "warning");
|
|
419
521
|
return;
|
|
420
522
|
}
|
|
421
523
|
const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
|
|
@@ -439,7 +541,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
439
541
|
|
|
440
542
|
async function commitTransaction(ctx: ExtensionCommandContext): Promise<void> {
|
|
441
543
|
if (planningLock.owner === "agent") {
|
|
442
|
-
ctx.ui.notify("The Agent is currently processing the midcompact
|
|
544
|
+
ctx.ui.notify("The Agent is currently processing the midcompact plan. Commit after the Agent turn ends.", "warning");
|
|
443
545
|
return;
|
|
444
546
|
}
|
|
445
547
|
const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
|
|
@@ -450,7 +552,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
450
552
|
return;
|
|
451
553
|
}
|
|
452
554
|
if (!currentDraft?.ranges.length) {
|
|
453
|
-
ctx.ui.notify("
|
|
555
|
+
ctx.ui.notify("Plan is empty; nothing to commit.", "warning");
|
|
454
556
|
return;
|
|
455
557
|
}
|
|
456
558
|
// Commit validation: reject empty summary, invalid boundaries, overlaps, protected atoms.
|
|
@@ -508,7 +610,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
508
610
|
return;
|
|
509
611
|
}
|
|
510
612
|
if (!tryAcquireUi(planningLock)) {
|
|
511
|
-
ctx.ui.notify("The Agent is currently processing the midcompact
|
|
613
|
+
ctx.ui.notify("The Agent is currently processing the midcompact plan. Try opening review after the Agent turn ends.", "warning");
|
|
512
614
|
return;
|
|
513
615
|
}
|
|
514
616
|
try {
|
|
@@ -579,7 +681,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
579
681
|
/** Agent tool path: all active-transaction operations yield to an editing UI. */
|
|
580
682
|
function requireAgentAccess(ctx: ExtensionContext): boolean {
|
|
581
683
|
if (!acquireAgent(planningLock)) {
|
|
582
|
-
ctx.ui.notify("A Selection/Review UI is currently editing the midcompact
|
|
684
|
+
ctx.ui.notify("A Selection/Review UI is currently editing the midcompact plan. Close it before the Agent can continue.", "warning");
|
|
583
685
|
return false;
|
|
584
686
|
}
|
|
585
687
|
return true;
|
|
@@ -600,7 +702,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
600
702
|
async execute(_id: string, params: ToolParams, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
|
|
601
703
|
try {
|
|
602
704
|
const request = params.request;
|
|
603
|
-
|
|
705
|
+
rejectExtraFields(request);
|
|
706
|
+
if (request.action === "recall_list") return toolResult(handleRecallList(request, ctx));
|
|
707
|
+
if (request.action === "recall_read") return toolResult(handleRecallRead(request, ctx));
|
|
604
708
|
const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
|
|
605
709
|
const currentTx = withCompatDefaults(restored.transaction ?? transaction);
|
|
606
710
|
if (!currentTx) return toolResult("No active midcompact transaction. Ask the user to run `/midcompact:start` first.");
|
|
@@ -611,17 +715,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
611
715
|
}
|
|
612
716
|
const snapshot = buildAnchorSnapshot(ctx.sessionManager, currentTx);
|
|
613
717
|
|
|
614
|
-
if (request.action === "inspect") return toolResult(
|
|
615
|
-
if (request.action === "
|
|
616
|
-
if (request.action === "
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
718
|
+
if (request.action === "inspect") return toolResult(handleInventory(request, snapshot.atoms, currentTx));
|
|
719
|
+
if (request.action === "measure") return toolResult(handleMeasure(request, snapshot.atoms));
|
|
720
|
+
if (request.action === "locate_ref") return toolResult(handleLocateRef(request, snapshot.atoms));
|
|
721
|
+
if (request.action === "locate_search") return toolResult(handleLocateSearch(request, snapshot.atoms));
|
|
722
|
+
if (request.action === "plan_show") {
|
|
723
|
+
return toolResult(formatDraft(draft!, draftTelemetry(transaction, draft), { atoms: snapshot.atoms }));
|
|
724
|
+
}
|
|
725
|
+
if (request.action === "plan_read") {
|
|
726
|
+
return toolResult(formatDraft(draft!, draftTelemetry(transaction, draft), {
|
|
727
|
+
detail: "full",
|
|
728
|
+
draftId: request.range_id,
|
|
729
|
+
atoms: snapshot.atoms,
|
|
730
|
+
}));
|
|
731
|
+
}
|
|
732
|
+
if (request.action === "plan_add" || request.action === "plan_update" || request.action === "plan_remove") {
|
|
733
|
+
const result = handlePlanMutation(draft!, snapshot.atoms, request);
|
|
625
734
|
draft = result.draft;
|
|
626
735
|
pi.appendEntry(DRAFT_ENTRY, draft);
|
|
627
736
|
updateStatus(ctx, transaction, draft, planningLock.owner);
|
|
@@ -634,31 +743,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
634
743
|
},
|
|
635
744
|
});
|
|
636
745
|
|
|
637
|
-
function
|
|
638
|
-
if (params.spans) {
|
|
639
|
-
if (params.page_size !== undefined || params.cursor !== undefined) {
|
|
640
|
-
throw new Error("inspect spans cannot be combined with inventory pagination.");
|
|
641
|
-
}
|
|
642
|
-
return formatSpanInspection(atoms, params.spans);
|
|
643
|
-
}
|
|
746
|
+
function handleInventory(params: InspectRequestType, atoms: Atom[], tx: TransactionState): string {
|
|
644
747
|
const page = buildInventory(atoms, { pageSize: params.page_size, cursor: params.cursor }, { transaction: tx });
|
|
645
748
|
return formatInventory(page);
|
|
646
749
|
}
|
|
647
750
|
|
|
648
|
-
function
|
|
751
|
+
function handleMeasure(params: MeasureRequestType, atoms: Atom[]): string {
|
|
752
|
+
if (!params.candidates?.length) throw new Error("measure requires at least one start/end candidate.");
|
|
753
|
+
return formatSpanInspection(atoms, params.candidates);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function restoreBranchState(ctx: ExtensionContext): CompressionState | undefined {
|
|
757
|
+
return restoreCompressionState(ctx.sessionManager.getBranch() as SessionEntry[]) ?? activeState;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function handleRecallList(params: RecallListRequestType, ctx: ExtensionContext): string {
|
|
761
|
+
const branchState = restoreBranchState(ctx);
|
|
762
|
+
if (!branchState?.blocks.length) return "No compressed blocks are active on this branch.";
|
|
763
|
+
const query = (params.pattern ?? "").trim().toLocaleLowerCase();
|
|
764
|
+
const matches = branchState.blocks.filter((block) => !query || `${block.id}\n${block.topic ?? ""}\n${block.summary}`.toLocaleLowerCase().includes(query));
|
|
765
|
+
if (!matches.length) return "No compressed blocks matched.";
|
|
766
|
+
return matches.slice(0, Math.max(1, Math.min(params.limit ?? 8, 20))).map((block) =>
|
|
767
|
+
`${block.id}${block.topic ? ` | ${block.topic}` : ""} | ${block.originalContentChars ?? 0} original chars${block.originalImageCount ? ` · ${block.originalImageCount} images` : ""}\n${block.summary}`
|
|
768
|
+
).join("\n\n");
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function handleRecallRead(params: RecallReadRequestType, ctx: ExtensionContext): string {
|
|
649
772
|
const sm = ctx.sessionManager;
|
|
650
|
-
const branchState =
|
|
773
|
+
const branchState = restoreBranchState(ctx);
|
|
651
774
|
if (!branchState?.blocks.length) return "No compressed blocks are active on this branch.";
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const matches = branchState.blocks.filter((block) => !query || `${block.id}\n${block.topic ?? ""}\n${block.summary}`.toLocaleLowerCase().includes(query));
|
|
655
|
-
if (!matches.length) return "No compressed blocks matched.";
|
|
656
|
-
return matches.slice(0, Math.max(1, Math.min(params.limit ?? 8, 20))).map((block) =>
|
|
657
|
-
`${block.id}${block.topic ? ` | ${block.topic}` : ""} | ${block.originalContentChars ?? 0} original chars${block.originalImageCount ? ` · ${block.originalImageCount} images` : ""}\n${block.summary}`
|
|
658
|
-
).join("\n\n");
|
|
659
|
-
}
|
|
660
|
-
const block = branchState.blocks.find((candidate) => candidate.id === params.ref);
|
|
661
|
-
if (!block) return `Unknown compressed block ${params.ref}.`;
|
|
775
|
+
const block = branchState.blocks.find((candidate) => candidate.id === params.block);
|
|
776
|
+
if (!block) return `Unknown compressed block ${params.block}.`;
|
|
662
777
|
const byId = new Map((sm.getEntries() as SessionEntry[]).map((entry) => [entry.id, entry]));
|
|
663
778
|
const parts: string[] = [];
|
|
664
779
|
for (const id of block.entryIds) {
|
|
@@ -681,59 +796,58 @@ export default function (pi: ExtensionAPI) {
|
|
|
681
796
|
|
|
682
797
|
// ---- Pure handlers ----
|
|
683
798
|
|
|
684
|
-
function
|
|
685
|
-
const
|
|
686
|
-
if (
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
799
|
+
function handleLocateRef(params: LocateRefRequestType, atoms: Atom[]): string {
|
|
800
|
+
const result = locateAtomMatches(atoms, { ref: params.ref });
|
|
801
|
+
if (!result.atoms.length) return "No matching atoms in the frozen anchor snapshot.";
|
|
802
|
+
return formatLocatedAtom(result.atoms[0]!, params.detail ?? "brief");
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function handleLocateSearch(params: LocateSearchRequestType, atoms: Atom[]): string {
|
|
806
|
+
if (!params.pattern && !params.tool_name && !params.source) {
|
|
807
|
+
throw new Error("locate_search requires at least one filter: pattern, tool_name, or source.");
|
|
691
808
|
}
|
|
692
809
|
const result = locateAtomMatches(atoms, {
|
|
693
|
-
ref: params.ref,
|
|
694
810
|
pattern: params.pattern,
|
|
695
811
|
source: params.source,
|
|
696
812
|
toolName: params.tool_name,
|
|
697
813
|
direction: params.direction,
|
|
698
814
|
limit: params.limit,
|
|
699
|
-
detail: params.detail,
|
|
700
815
|
});
|
|
701
816
|
if (!result.atoms.length) return "No matching atoms in the frozen anchor snapshot.";
|
|
702
817
|
const rendered = result.atoms
|
|
703
|
-
.map((atom) => formatLocatedAtom(atom,
|
|
818
|
+
.map((atom) => formatLocatedAtom(atom, "brief", params.pattern))
|
|
704
819
|
.join("\n\n---\n\n");
|
|
705
820
|
if (result.totalMatches <= result.atoms.length) return rendered;
|
|
706
821
|
return [
|
|
707
|
-
`Showing ${result.atoms.length} of ${result.totalMatches} matches (${params.direction ?? "oldest"} first). Refine pattern or add source
|
|
822
|
+
`Showing ${result.atoms.length} of ${result.totalMatches} matches (${params.direction ?? "oldest"} first). Refine pattern or add source or tool_name.`,
|
|
708
823
|
rendered,
|
|
709
824
|
].join("\n\n");
|
|
710
825
|
}
|
|
711
826
|
|
|
712
|
-
type
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
if (
|
|
720
|
-
|
|
721
|
-
|
|
827
|
+
type PlanMutationOp = "add" | "update" | "remove";
|
|
828
|
+
|
|
829
|
+
function handlePlanMutation(
|
|
830
|
+
current: DraftPlan,
|
|
831
|
+
atoms: Atom[],
|
|
832
|
+
request: PlanAddRequestType | PlanUpdateRequestType | PlanRemoveRequestType,
|
|
833
|
+
): { op: PlanMutationOp; draft: DraftPlan; changedId: string } {
|
|
834
|
+
if (request.action === "plan_add") {
|
|
835
|
+
const draft = addDraftRange(current, atoms, { start: request.start, end: request.end, summary: request.summary, topic: request.topic });
|
|
836
|
+
const previousIds = new Set(current.ranges.map((range) => range.id));
|
|
837
|
+
const changedId = draft.ranges.find((range) => !previousIds.has(range.id))!.id;
|
|
838
|
+
return { op: "add", draft, changedId };
|
|
722
839
|
}
|
|
723
|
-
if (
|
|
724
|
-
if (
|
|
725
|
-
|
|
840
|
+
if (request.action === "plan_update") {
|
|
841
|
+
if (request.summary === undefined && request.topic === undefined) {
|
|
842
|
+
throw new Error("plan_update requires summary and/or topic; boundaries change via plan_remove + plan_add.");
|
|
843
|
+
}
|
|
726
844
|
return {
|
|
727
|
-
op,
|
|
728
|
-
draft: updateDraftRange(current,
|
|
729
|
-
changedId:
|
|
845
|
+
op: "update",
|
|
846
|
+
draft: updateDraftRange(current, request.range_id, { summary: request.summary, topic: request.topic }),
|
|
847
|
+
changedId: request.range_id,
|
|
730
848
|
};
|
|
731
849
|
}
|
|
732
|
-
|
|
733
|
-
const next = addDraftRange(current, atoms, { start: params.start, end: params.end, summary: params.summary, topic: params.topic });
|
|
734
|
-
const previousIds = new Set(current.ranges.map((range) => range.id));
|
|
735
|
-
const changedId = next.ranges.find((range) => !previousIds.has(range.id))!.id;
|
|
736
|
-
return { op, draft: next, changedId };
|
|
850
|
+
return { op: "remove", draft: removeDraftRange(current, request.range_id), changedId: request.range_id };
|
|
737
851
|
}
|
|
738
852
|
|
|
739
853
|
function validateDraftForCommit(draft: DraftPlan, atoms: Atom[]): void {
|
package/src/inventory.ts
CHANGED
|
@@ -217,7 +217,7 @@ export function formatInventory(page: InventoryPage): string {
|
|
|
217
217
|
|
|
218
218
|
/** Measure explicit, possibly overlapping candidate spans without mutating the DraftPlan. */
|
|
219
219
|
export function formatSpanInspection(atoms: readonly Atom[], spans: readonly InspectSpan[]): string {
|
|
220
|
-
if (spans.length === 0) throw new Error("
|
|
220
|
+
if (spans.length === 0) throw new Error("measure requires at least one start/end candidate.");
|
|
221
221
|
const byRef = new Map(atoms.map((atom) => [atom.ref, atom]));
|
|
222
222
|
const anchorChars = aggregateMetrics(atoms.map((atom) => atom.metrics)).contentChars;
|
|
223
223
|
const lines = [
|
|
@@ -228,7 +228,7 @@ export function formatSpanInspection(atoms: readonly Atom[], spans: readonly Ins
|
|
|
228
228
|
for (const span of spans) {
|
|
229
229
|
const start = byRef.get(span.start);
|
|
230
230
|
const end = byRef.get(span.end);
|
|
231
|
-
if (!start || !end) throw new Error(`Unknown span ref ${!start ? span.start : span.end}; re-run
|
|
231
|
+
if (!start || !end) throw new Error(`Unknown span ref ${!start ? span.start : span.end}; re-run measure against the current snapshot.`);
|
|
232
232
|
if (start.index > end.index) throw new Error(`Span ${span.start} → ${span.end} is reversed.`);
|
|
233
233
|
|
|
234
234
|
const selected = atoms.slice(start.index, end.index + 1);
|
package/src/plan.ts
CHANGED
|
@@ -22,14 +22,14 @@ export function addDraftRange(
|
|
|
22
22
|
): DraftPlan {
|
|
23
23
|
const start = atoms.find((atom) => atom.ref === input.start);
|
|
24
24
|
const end = atoms.find((atom) => atom.ref === input.end);
|
|
25
|
-
if (!start || !end) throw new Error("Unknown atom ref; run
|
|
25
|
+
if (!start || !end) throw new Error("Unknown atom ref; run inspect or locate again against the current anchor snapshot.");
|
|
26
26
|
if (start.index > end.index) throw new Error("start must not occur after end.");
|
|
27
27
|
const selected = atoms.slice(start.index, end.index + 1);
|
|
28
28
|
if (selected.length === 0) throw new Error("Empty range.");
|
|
29
29
|
const unsafe = selected.find((atom) => isProtectedAtom(atom));
|
|
30
30
|
if (unsafe) throw new Error(`Range crosses protected atom ${unsafe.ref} (${unsafe.kind}). Split the plan around it.`);
|
|
31
31
|
const overlaps = draft.ranges.some((range) => !(end.index < range.startIndex || start.index > range.endIndex));
|
|
32
|
-
if (overlaps) throw new Error("Range overlaps an existing
|
|
32
|
+
if (overlaps) throw new Error("Range overlaps an existing plan range; remove or replace it first.");
|
|
33
33
|
|
|
34
34
|
const metrics = rangeMetricsForAtoms(selected);
|
|
35
35
|
const summary = input.summary ?? "";
|
|
@@ -76,7 +76,7 @@ export function updateDraftRange(
|
|
|
76
76
|
patch: { summary?: string; topic?: string },
|
|
77
77
|
): DraftPlan {
|
|
78
78
|
const target = draft.ranges.find((range) => range.id === draftId);
|
|
79
|
-
if (!target) throw new Error(`Unknown
|
|
79
|
+
if (!target) throw new Error(`Unknown plan range ${draftId}.`);
|
|
80
80
|
const summary = patch.summary ?? target.summary;
|
|
81
81
|
const topic = patch.topic ?? target.topic;
|
|
82
82
|
const replacement = replacementContentChars(summary, topic);
|
|
@@ -90,7 +90,7 @@ export function updateDraftRange(
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export function removeDraftRange(draft: DraftPlan, draftId: string): DraftPlan {
|
|
93
|
-
if (!draft.ranges.some((range) => range.id === draftId)) throw new Error(`Unknown
|
|
93
|
+
if (!draft.ranges.some((range) => range.id === draftId)) throw new Error(`Unknown plan range ${draftId}.`);
|
|
94
94
|
return { ...draft, revision: draft.revision + 1, ranges: draft.ranges.filter((range) => range.id !== draftId) };
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -153,18 +153,18 @@ export interface DraftFormatOptions {
|
|
|
153
153
|
/** Agent-facing plan output with bounded semantic landmarks and summaries. */
|
|
154
154
|
export function formatDraft(draft: DraftPlan, telemetry?: DraftTelemetry, options: DraftFormatOptions = {}): string {
|
|
155
155
|
if (options.detail === "full" && !options.draftId) {
|
|
156
|
-
throw new Error("plan
|
|
156
|
+
throw new Error("Full plan output requires a known range id.");
|
|
157
157
|
}
|
|
158
158
|
const selected = options.draftId
|
|
159
159
|
? draft.ranges.filter((range) => range.id === options.draftId)
|
|
160
160
|
: draft.ranges;
|
|
161
161
|
const atomsByRef = options.atoms ? new Map(options.atoms.map((atom) => [atom.ref, atom])) : undefined;
|
|
162
|
-
if (options.draftId && selected.length === 0) throw new Error(`Unknown
|
|
162
|
+
if (options.draftId && selected.length === 0) throw new Error(`Unknown plan range ${options.draftId}.`);
|
|
163
163
|
|
|
164
164
|
const lines: string[] = [];
|
|
165
165
|
if (telemetry) lines.push(formatTelemetry(telemetry));
|
|
166
166
|
if (draft.ranges.length === 0) {
|
|
167
|
-
lines.push(`
|
|
167
|
+
lines.push(`Plan v${draft.revision}: no compression ranges.`);
|
|
168
168
|
return lines.join("\n\n");
|
|
169
169
|
}
|
|
170
170
|
lines.push(draftHeader(draft));
|
|
@@ -180,7 +180,7 @@ export function formatDraft(draft: DraftPlan, telemetry?: DraftTelemetry, option
|
|
|
180
180
|
const block = formatRangeBrief(range, atomsByRef);
|
|
181
181
|
const currentLength = lines.join("\n\n").length;
|
|
182
182
|
if (currentLength + 2 + block.length > DRAFT_OUTPUT_LIMIT) {
|
|
183
|
-
const notice = `Output budget reached: showed ${shown} of ${selected.length} range(s). Use
|
|
183
|
+
const notice = `Output budget reached: showed ${shown} of ${selected.length} range(s). Use plan_read to inspect one range.`;
|
|
184
184
|
if (currentLength + 2 + notice.length <= DRAFT_OUTPUT_LIMIT) lines.push(notice);
|
|
185
185
|
break;
|
|
186
186
|
}
|
|
@@ -199,7 +199,7 @@ export function formatPlanMutation(
|
|
|
199
199
|
): string {
|
|
200
200
|
const pendingCount = draft.ranges.filter((range) => range.summary.trim().length === 0).length;
|
|
201
201
|
const verb = op === "add" ? "added" : op === "update" ? "updated" : "removed";
|
|
202
|
-
const lines = [`
|
|
202
|
+
const lines = [`Plan v${draft.revision}: ${verb} ${changedId} · ${draft.ranges.length} range(s) · ${pendingCount} pending summary.`];
|
|
203
203
|
if (op !== "remove") {
|
|
204
204
|
const changed = draft.ranges.find((range) => range.id === changedId);
|
|
205
205
|
const atomsByRef = atoms ? new Map(atoms.map((atom) => [atom.ref, atom])) : undefined;
|
|
@@ -211,7 +211,7 @@ export function formatPlanMutation(
|
|
|
211
211
|
function draftHeader(draft: DraftPlan): string {
|
|
212
212
|
const pendingCount = draft.ranges.filter((range) => range.summary.trim().length === 0).length;
|
|
213
213
|
const reviewState = pendingCount === 0 ? "ready for review" : `${pendingCount} pending summary`;
|
|
214
|
-
return `
|
|
214
|
+
return `Plan v${draft.revision}: ${draft.ranges.length} range(s) (${reviewState}).`;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
function formatRangeBrief(range: DraftRange, atomsByRef?: ReadonlyMap<string, Atom>): string {
|
package/src/review-ui.ts
CHANGED
|
@@ -46,7 +46,7 @@ export async function showReviewUi(
|
|
|
46
46
|
|
|
47
47
|
const header: string[] = [
|
|
48
48
|
h("╭"),
|
|
49
|
-
framed(accent(theme.bold(`Midcompact Review ·
|
|
49
|
+
framed(accent(theme.bold(`Midcompact Review · Plan v${draft.revision} · ${draft.ranges.length} range(s)`))),
|
|
50
50
|
framed(usageLine(telemetry, theme)),
|
|
51
51
|
h("├"),
|
|
52
52
|
];
|
|
@@ -195,7 +195,7 @@ export async function showReviewUi(
|
|
|
195
195
|
|
|
196
196
|
export function buildReviewText(atoms: Atom[], draft: DraftPlan, telemetry: DraftTelemetry): string {
|
|
197
197
|
const lines = [
|
|
198
|
-
`Midcompact Review ·
|
|
198
|
+
`Midcompact Review · Plan v${draft.revision}`,
|
|
199
199
|
plainUsageLine(telemetry),
|
|
200
200
|
"This is awareness, not a target.",
|
|
201
201
|
"",
|
package/src/selection-ui.ts
CHANGED
|
@@ -159,7 +159,7 @@ export async function showSelectionUi(
|
|
|
159
159
|
|
|
160
160
|
const header = [
|
|
161
161
|
rule(w, "+", "+"),
|
|
162
|
-
frame(accent(theme.bold(`Midcompact Selection |
|
|
162
|
+
frame(accent(theme.bold(`Midcompact Selection | Plan v${draft.revision}`)), w),
|
|
163
163
|
frame(dim(`Anchor Pi usage: ${usageLine(telemetry)}`), w),
|
|
164
164
|
frame(dim(`Selected ${selectedAtoms.length}/${atoms.length} atoms | ${chars}/${totalChars} chars (${charShare} of anchor) | up to ${charShare} fewer anchor chars | ${images} images`), w),
|
|
165
165
|
rule(w, "+", "+"),
|
package/src/start-ui.ts
CHANGED
|
@@ -16,8 +16,8 @@ interface StartChoiceOption {
|
|
|
16
16
|
* Agent-first start path, and RPC clients see the recommended entry first.
|
|
17
17
|
*/
|
|
18
18
|
const START_CHOICES: StartChoiceOption[] = [
|
|
19
|
-
{ value: "agent", label: "Agent direct — Inspect and
|
|
20
|
-
{ value: "user", label: "User manual — Select the initial
|
|
19
|
+
{ value: "agent", label: "Agent direct — Inspect and plan with Agent" },
|
|
20
|
+
{ value: "user", label: "User manual — Select the initial plan yourself" },
|
|
21
21
|
{ value: "cancelled", label: "Drop — Leave the session unchanged" },
|
|
22
22
|
];
|
|
23
23
|
|
package/src/telemetry.ts
CHANGED
|
@@ -100,7 +100,7 @@ export function formatTelemetry(telemetry: DraftTelemetry): string {
|
|
|
100
100
|
lines.push("anchor: Pi context usage unavailable [Pi reported; not derived from local char counts].");
|
|
101
101
|
}
|
|
102
102
|
lines.push(
|
|
103
|
-
`
|
|
103
|
+
`plan: ${telemetry.rangeCount} range(s) · ${telemetry.selectedOriginalContentChars} → ${telemetry.selectedReplacementContentChars} content chars · ${telemetry.selectedImageCount} images (${telemetry.selectedImagePayloadBytes} payload bytes) · ${telemetry.pendingSummaryCount} pending summary`,
|
|
104
104
|
);
|
|
105
105
|
return lines.join("\n");
|
|
106
106
|
}
|