pi-cliffcompaction 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +291 -0
- package/config.example.json +12 -0
- package/index.ts +185 -0
- package/lib/cliff.ts +134 -0
- package/lib/config.ts +225 -0
- package/lib/decode.ts +103 -0
- package/lib/dialects/anthropic.ts +364 -0
- package/lib/dialects/base.ts +79 -0
- package/lib/dialects/index.ts +48 -0
- package/lib/dialects/openai-chat.ts +225 -0
- package/lib/dialects/openai-responses.ts +331 -0
- package/lib/dialects/pi.ts +249 -0
- package/lib/engine.ts +558 -0
- package/lib/hashing.ts +31 -0
- package/lib/images.ts +268 -0
- package/lib/index.ts +62 -0
- package/lib/json.ts +256 -0
- package/lib/pi-hook.ts +253 -0
- package/lib/store.ts +94 -0
- package/package.json +76 -0
package/lib/pi-hook.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map CliffCompaction onto Pi's session_before_compact contract.
|
|
3
|
+
*
|
|
4
|
+
* Pi persists a CompactionEntry as [summary string] + contiguous suffix
|
|
5
|
+
* from firstKeptEntryId. There is no separate head slot, so the verbatim
|
|
6
|
+
* head (task) is folded into the summary text. Previous compaction
|
|
7
|
+
* entries are skipped so we never compact a compaction: each pass
|
|
8
|
+
* compresses only original live-session messages (Lt).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { compact } from "./cliff.ts";
|
|
12
|
+
import { replaceConfig, type Config } from "./config.ts";
|
|
13
|
+
import { isRecord, isString, type JsonObject } from "./decode.ts";
|
|
14
|
+
import { SUMMARY_HEADER } from "./dialects/base.ts";
|
|
15
|
+
import { DIALECT as piDialect } from "./dialects/pi.ts";
|
|
16
|
+
import { estimateTokens } from "./engine.ts";
|
|
17
|
+
|
|
18
|
+
export type SessionMessageRef = {
|
|
19
|
+
entryId: string;
|
|
20
|
+
message: JsonObject;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type CompactSessionInput = {
|
|
24
|
+
live: SessionMessageRef[];
|
|
25
|
+
tokensBefore: number;
|
|
26
|
+
fallbackFirstKeptEntryId: string;
|
|
27
|
+
reason: "manual" | "threshold" | "overflow";
|
|
28
|
+
cfg: Config;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type CliffDetails = {
|
|
32
|
+
version: number;
|
|
33
|
+
method: "cliffcompaction";
|
|
34
|
+
reason: string;
|
|
35
|
+
headLen: number;
|
|
36
|
+
cut: number;
|
|
37
|
+
keepRecent: number;
|
|
38
|
+
rung: number;
|
|
39
|
+
liveMessages: number;
|
|
40
|
+
keptMessages: number;
|
|
41
|
+
estTokensOut: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type CompactSessionOutput = {
|
|
45
|
+
summary: string;
|
|
46
|
+
firstKeptEntryId: string;
|
|
47
|
+
tokensBefore: number;
|
|
48
|
+
details: CliffDetails;
|
|
49
|
+
rung: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function summaryBody(summary: JsonObject): string {
|
|
53
|
+
const content = summary.content;
|
|
54
|
+
|
|
55
|
+
if (isString(content)) {
|
|
56
|
+
if (content.startsWith(SUMMARY_HEADER)) {
|
|
57
|
+
return content.slice(SUMMARY_HEADER.length).trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return content;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (Array.isArray(content)) {
|
|
64
|
+
for (const b of content) {
|
|
65
|
+
if (isRecord(b) && isString(b.text)) {
|
|
66
|
+
const text = b.text;
|
|
67
|
+
|
|
68
|
+
if (text.startsWith(SUMMARY_HEADER)) {
|
|
69
|
+
return text.slice(SUMMARY_HEADER.length).trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return text;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function foldHeadIntoSummary(resultMessages: JsonObject[], headLen: number, body: string, cfg: Config): string {
|
|
81
|
+
const parts: string[] = [];
|
|
82
|
+
const headCfg = replaceConfig(cfg, { thoughtMaxChars: 0, thinkingMaxChars: 0, humanMaxChars: cfg.humanMaxChars });
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < headLen; i++) {
|
|
85
|
+
const piece = piDialect.summarizeMessage(resultMessages[i], headCfg);
|
|
86
|
+
|
|
87
|
+
for (const p of piece) {
|
|
88
|
+
parts.push(p);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (body) {
|
|
93
|
+
parts.push(body);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (parts.length === 0) {
|
|
97
|
+
return SUMMARY_HEADER;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return SUMMARY_HEADER + "\n\n" + parts.join("\n\n---\n\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Mechanical cliff compaction for one Pi session_before_compact event.
|
|
105
|
+
* Returns null when there is nothing to gain (fail-open: caller should
|
|
106
|
+
* not rewrite). Overflow walks the escalation ladder.
|
|
107
|
+
*/
|
|
108
|
+
export function compactSession(input: CompactSessionInput): CompactSessionOutput | null {
|
|
109
|
+
const msgs: JsonObject[] = [];
|
|
110
|
+
|
|
111
|
+
for (const ref of input.live) {
|
|
112
|
+
msgs.push(ref.message);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (msgs.length === 0) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const force = input.reason === "overflow";
|
|
120
|
+
let rung = 0;
|
|
121
|
+
let cfg = input.cfg;
|
|
122
|
+
let result = compact(msgs, piDialect, cfg);
|
|
123
|
+
|
|
124
|
+
if (result === null && (force || input.reason === "threshold")) {
|
|
125
|
+
rung = 1;
|
|
126
|
+
cfg = replaceConfig(input.cfg, { keepRecent: 1 });
|
|
127
|
+
result = compact(msgs, piDialect, cfg);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (result === null && force) {
|
|
131
|
+
rung = 2;
|
|
132
|
+
const cap = input.cfg.thoughtMaxChars;
|
|
133
|
+
cfg = replaceConfig(input.cfg, {
|
|
134
|
+
keepRecent: 1,
|
|
135
|
+
thoughtMaxChars: cap <= 0 ? 300 : Math.min(cap, 300),
|
|
136
|
+
keepThinking: false,
|
|
137
|
+
});
|
|
138
|
+
result = compact(msgs, piDialect, cfg);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (result === null) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const body = summaryBody(result.summary);
|
|
146
|
+
let summary = foldHeadIntoSummary(result.messages, result.headLen, body, input.cfg);
|
|
147
|
+
const kept = result.messages.length - result.headLen - 1;
|
|
148
|
+
const cut = result.cut;
|
|
149
|
+
let firstKeptEntryId = input.fallbackFirstKeptEntryId;
|
|
150
|
+
|
|
151
|
+
if (cut >= 0 && cut < input.live.length) {
|
|
152
|
+
firstKeptEntryId = input.live[cut].entryId;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const outgoing: JsonObject = { messages: result.messages };
|
|
156
|
+
let est = estimateTokens(outgoing);
|
|
157
|
+
|
|
158
|
+
if (input.cfg.strict && est > input.cfg.thresholdTokens && rung < 3) {
|
|
159
|
+
rung = 3;
|
|
160
|
+
const header = SUMMARY_HEADER;
|
|
161
|
+
const rest = summary.slice(header.length).trim();
|
|
162
|
+
const parts = rest.length > 0 ? rest.split("\n\n---\n\n") : [];
|
|
163
|
+
const budget = input.cfg.thresholdTokens * 4 - 64 - header.length;
|
|
164
|
+
const keptParts: string[] = [];
|
|
165
|
+
let used = 0;
|
|
166
|
+
|
|
167
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
168
|
+
if (used + parts[i].length > Math.max(budget, 0)) {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
keptParts.push(parts[i]);
|
|
173
|
+
used += parts[i].length + 9;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
keptParts.reverse();
|
|
177
|
+
summary = keptParts.length > 0 ? header + "\n\n" + keptParts.join("\n\n---\n\n") : header;
|
|
178
|
+
est = estimateTokens({ messages: [piDialect.userMessage(summary), ...result.messages.slice(result.headLen + 1)] });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
summary,
|
|
183
|
+
firstKeptEntryId,
|
|
184
|
+
tokensBefore: input.tokensBefore,
|
|
185
|
+
rung,
|
|
186
|
+
details: {
|
|
187
|
+
version: 1,
|
|
188
|
+
method: "cliffcompaction",
|
|
189
|
+
reason: input.reason,
|
|
190
|
+
headLen: result.headLen,
|
|
191
|
+
cut,
|
|
192
|
+
keepRecent: input.cfg.keepRecent,
|
|
193
|
+
rung,
|
|
194
|
+
liveMessages: msgs.length,
|
|
195
|
+
keptMessages: kept,
|
|
196
|
+
estTokensOut: est,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export type HookEntry = {
|
|
202
|
+
id: string;
|
|
203
|
+
kind: "message" | "compaction" | "other";
|
|
204
|
+
message?: JsonObject;
|
|
205
|
+
firstKeptEntryId?: string;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Live session Lt: original messages from the previous cliff's
|
|
210
|
+
* firstKeptEntryId (or the start of the session) forward. Compaction
|
|
211
|
+
* entries themselves are skipped so prior summaries are discarded.
|
|
212
|
+
*/
|
|
213
|
+
export function liveFromEntries(entries: HookEntry[]): SessionMessageRef[] {
|
|
214
|
+
let start = 0;
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < entries.length; i++) {
|
|
217
|
+
const e = entries[i];
|
|
218
|
+
|
|
219
|
+
if (e.kind !== "compaction") {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const kept = e.firstKeptEntryId;
|
|
224
|
+
|
|
225
|
+
if (!kept) {
|
|
226
|
+
start = i + 1;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let idx = -1;
|
|
231
|
+
|
|
232
|
+
for (let j = 0; j < entries.length; j++) {
|
|
233
|
+
if (entries[j].id === kept) {
|
|
234
|
+
idx = j;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
start = idx >= 0 ? idx : i + 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const live: SessionMessageRef[] = [];
|
|
243
|
+
|
|
244
|
+
for (let i = start; i < entries.length; i++) {
|
|
245
|
+
const e = entries[i];
|
|
246
|
+
|
|
247
|
+
if (e.kind === "message" && e.message) {
|
|
248
|
+
live.push({ entryId: e.id, message: e.message });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return live;
|
|
253
|
+
}
|
package/lib/store.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefix store: chain_hash(original prefix S) -> compacted replacement.
|
|
3
|
+
*
|
|
4
|
+
* An entry holds only (headLen, summary, cut); the substitution for a
|
|
5
|
+
* request extending prefix S (|S| = cut) is msgs[:headLen] + [summary] +
|
|
6
|
+
* msgs[cut:]. Head bytes come from the CURRENT request. The store is a
|
|
7
|
+
* cache: the compactor is deterministic, so lost entries are recomputed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { dumpsLen } from "./json.ts";
|
|
11
|
+
import type { JsonObject } from "./decode.ts";
|
|
12
|
+
|
|
13
|
+
export type Entry = {
|
|
14
|
+
headLen: number;
|
|
15
|
+
summary: JsonObject;
|
|
16
|
+
cut: number;
|
|
17
|
+
size: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function makeEntry(headLen: number, summary: JsonObject, cut: number): Entry {
|
|
21
|
+
return { headLen, summary, cut, size: 0 };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function entrySize(summary: JsonObject): number {
|
|
25
|
+
return dumpsLen(summary);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class PrefixStore {
|
|
29
|
+
private readonly maxEntries: number;
|
|
30
|
+
private readonly maxBytes: number;
|
|
31
|
+
private readonly data = new Map<string, Entry>();
|
|
32
|
+
private bytes = 0;
|
|
33
|
+
|
|
34
|
+
constructor(maxEntries = 4096, maxBytes = 64 * 1024 * 1024) {
|
|
35
|
+
this.maxEntries = maxEntries;
|
|
36
|
+
this.maxBytes = maxBytes;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get(chainHash: string): Entry | undefined {
|
|
40
|
+
const entry = this.data.get(chainHash);
|
|
41
|
+
|
|
42
|
+
if (entry !== undefined) {
|
|
43
|
+
this.data.delete(chainHash);
|
|
44
|
+
this.data.set(chainHash, entry);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return entry;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
put(chainHash: string, entry: Entry): void {
|
|
51
|
+
entry.size = entrySize(entry.summary);
|
|
52
|
+
const old = this.data.get(chainHash);
|
|
53
|
+
|
|
54
|
+
if (old !== undefined) {
|
|
55
|
+
this.data.delete(chainHash);
|
|
56
|
+
this.bytes -= old.size;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.data.set(chainHash, entry);
|
|
60
|
+
this.bytes += entry.size;
|
|
61
|
+
|
|
62
|
+
while (this.data.size > 1 && (this.data.size > this.maxEntries || this.bytes > this.maxBytes)) {
|
|
63
|
+
const first = this.data.keys().next().value;
|
|
64
|
+
|
|
65
|
+
if (first === undefined) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const evicted = this.data.get(first);
|
|
70
|
+
|
|
71
|
+
this.data.delete(first);
|
|
72
|
+
|
|
73
|
+
if (evicted !== undefined) {
|
|
74
|
+
this.bytes -= evicted.size;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get nbytes(): number {
|
|
80
|
+
return this.bytes;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get size(): number {
|
|
84
|
+
return this.data.size;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get max(): number {
|
|
88
|
+
return this.maxEntries;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get maxBytesLimit(): number {
|
|
92
|
+
return this.maxBytes;
|
|
93
|
+
}
|
|
94
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-cliffcompaction",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CliffCompaction for Pi/OMP: mechanical autocompaction. Truncate or drop, never rephrase, never compact a compaction. Port of arXiv:2609.26779.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "AdityaVG13",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi",
|
|
11
|
+
"omp",
|
|
12
|
+
"oh-my-pi",
|
|
13
|
+
"pi-coding-agent",
|
|
14
|
+
"pi-extension",
|
|
15
|
+
"extension",
|
|
16
|
+
"compaction",
|
|
17
|
+
"context-compaction",
|
|
18
|
+
"context",
|
|
19
|
+
"cliffcompaction",
|
|
20
|
+
"coding-agent"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22.0.0"
|
|
24
|
+
},
|
|
25
|
+
"main": "./index.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./index.ts",
|
|
28
|
+
"./lib": "./lib/index.ts"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"index.ts",
|
|
32
|
+
"lib/",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"CHANGELOG.md",
|
|
36
|
+
"config.example.json"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "node --test test/*.test.mjs",
|
|
40
|
+
"prepublishOnly": "npm test && node ../../scripts/preflight.mjs"
|
|
41
|
+
},
|
|
42
|
+
"pi": {
|
|
43
|
+
"extensions": [
|
|
44
|
+
"./index.ts"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"omp": {
|
|
48
|
+
"extensions": [
|
|
49
|
+
"./index.ts"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
54
|
+
"@earendil-works/pi-ai": "*"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"@earendil-works/pi-coding-agent": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"@earendil-works/pi-ai": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
},
|
|
67
|
+
"repository": {
|
|
68
|
+
"type": "git",
|
|
69
|
+
"url": "git+https://github.com/AdityaVG13/pi-stack.git",
|
|
70
|
+
"directory": "packages/pi-cliffcompaction"
|
|
71
|
+
},
|
|
72
|
+
"bugs": {
|
|
73
|
+
"url": "https://github.com/AdityaVG13/pi-stack/issues"
|
|
74
|
+
},
|
|
75
|
+
"homepage": "https://github.com/AdityaVG13/pi-stack/tree/main/packages/pi-cliffcompaction#readme"
|
|
76
|
+
}
|