pi-midcompact 0.5.3 → 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 +27 -10
- package/README.zh-CN.md +16 -1
- package/package.json +4 -2
- package/skills/midcompact/SKILL.md +53 -86
- package/skills/midcompact/references/tool-interface.md +41 -59
- package/src/SPEC.md +35 -7
- package/src/content-metrics.ts +75 -3
- package/src/index.ts +232 -118
- package/src/inventory.ts +2 -2
- package/src/plan.ts +10 -10
- package/src/projection.ts +12 -3
- package/src/review-ui.ts +2 -2
- package/src/review-webui.html +837 -481
- package/src/review-webui.ts +150 -57
- package/src/selection-ui.ts +1 -1
- package/src/start-ui.ts +2 -2
- package/src/telemetry.ts +1 -1
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);
|