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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi / pi-ai Message dialect.
|
|
3
|
+
*
|
|
4
|
+
* convertToLlm yields {user, assistant, toolResult, system} messages.
|
|
5
|
+
* Assistant content is (text | thinking | toolCall) blocks. Tool results
|
|
6
|
+
* have their own role. Images in user/toolResult content are dropped from
|
|
7
|
+
* summaries (verbatim in head and recent turns via keep-by-reference).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Config } from "../config.ts";
|
|
11
|
+
import {
|
|
12
|
+
asBoolean,
|
|
13
|
+
asString,
|
|
14
|
+
isArray,
|
|
15
|
+
isRecord,
|
|
16
|
+
isString,
|
|
17
|
+
type JsonArray,
|
|
18
|
+
type JsonObject,
|
|
19
|
+
type JsonValue,
|
|
20
|
+
} from "../decode.ts";
|
|
21
|
+
import { digestBytes, digestObj } from "../hashing.ts";
|
|
22
|
+
import { canonicalJson } from "../json.ts";
|
|
23
|
+
import {
|
|
24
|
+
makeDialect,
|
|
25
|
+
startsWithSummaryHeader,
|
|
26
|
+
stripTaskNotifications,
|
|
27
|
+
truncate,
|
|
28
|
+
type Dialect,
|
|
29
|
+
} from "./base.ts";
|
|
30
|
+
|
|
31
|
+
function contentText(content: JsonValue | undefined): string {
|
|
32
|
+
if (isString(content)) {
|
|
33
|
+
return content;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isArray(content)) {
|
|
37
|
+
const parts: string[] = [];
|
|
38
|
+
|
|
39
|
+
for (const p of content) {
|
|
40
|
+
if (isRecord(p) && asString(p.type) === "text") {
|
|
41
|
+
parts.push(asString(p.text));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return parts.join("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (content === undefined || content === null) {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return String(content);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function canonContent(content: JsonValue | undefined): JsonArray {
|
|
56
|
+
if (isString(content)) {
|
|
57
|
+
return [["text", content]];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (isArray(content)) {
|
|
61
|
+
const out: JsonArray = [];
|
|
62
|
+
|
|
63
|
+
for (const p of content) {
|
|
64
|
+
if (!isRecord(p)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const t = asString(p.type);
|
|
69
|
+
|
|
70
|
+
if (t === "text") {
|
|
71
|
+
out.push(["text", asString(p.text)]);
|
|
72
|
+
} else if (t === "thinking") {
|
|
73
|
+
out.push(["thinking", asString(p.thinking)]);
|
|
74
|
+
} else if (t === "toolCall") {
|
|
75
|
+
out.push([
|
|
76
|
+
"toolCall",
|
|
77
|
+
asString(p.id),
|
|
78
|
+
asString(p.name),
|
|
79
|
+
canonicalJson(p.arguments ?? {}),
|
|
80
|
+
]);
|
|
81
|
+
} else if (t === "image") {
|
|
82
|
+
out.push(["image", digestBytes(asString(p.data))]);
|
|
83
|
+
} else {
|
|
84
|
+
out.push(["other", canonicalJson(p)]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function digestMessage(msg: JsonObject): string {
|
|
95
|
+
return digestObj([
|
|
96
|
+
asString(msg.role),
|
|
97
|
+
canonContent(msg.content),
|
|
98
|
+
asString(msg.toolCallId),
|
|
99
|
+
asString(msg.toolName),
|
|
100
|
+
asBoolean(msg.isError, false),
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function isAssistant(msg: JsonObject): boolean {
|
|
105
|
+
return asString(msg.role) === "assistant";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function isSummaryMessage(msg: JsonObject): boolean {
|
|
109
|
+
const role = asString(msg.role);
|
|
110
|
+
|
|
111
|
+
if (role !== "user" && role !== "compactionSummary") {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return startsWithSummaryHeader(contentText(msg.content));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function sessionKey(_body: JsonObject): string | null {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function summarizeAssistant(msg: JsonObject, cfg: Config): string[] {
|
|
123
|
+
const content = msg.content;
|
|
124
|
+
const texts: string[] = [];
|
|
125
|
+
const thinkings: string[] = [];
|
|
126
|
+
const sigs: string[] = [];
|
|
127
|
+
|
|
128
|
+
if (isString(content)) {
|
|
129
|
+
texts.push(content);
|
|
130
|
+
} else if (isArray(content)) {
|
|
131
|
+
for (const b of content) {
|
|
132
|
+
if (!isRecord(b)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const t = asString(b.type);
|
|
137
|
+
|
|
138
|
+
if (t === "text") {
|
|
139
|
+
texts.push(asString(b.text));
|
|
140
|
+
} else if (t === "thinking") {
|
|
141
|
+
if (cfg.keepThinking) {
|
|
142
|
+
thinkings.push(asString(b.thinking));
|
|
143
|
+
}
|
|
144
|
+
} else if (t === "toolCall") {
|
|
145
|
+
const args = canonicalJson(b.arguments ?? {});
|
|
146
|
+
sigs.push("[" + asString(b.name, "?") + "] " + truncate(args, cfg.cmdMaxChars));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const lines: string[] = [];
|
|
152
|
+
const thinkingBits: string[] = [];
|
|
153
|
+
|
|
154
|
+
for (const x of thinkings) {
|
|
155
|
+
if (x.trim()) {
|
|
156
|
+
thinkingBits.push(x);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const thinking = truncate(thinkingBits.join("\n").trim(), cfg.thinkingMaxChars);
|
|
161
|
+
|
|
162
|
+
if (thinking) {
|
|
163
|
+
lines.push("thinking: " + thinking);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const thoughtBits: string[] = [];
|
|
167
|
+
|
|
168
|
+
for (const t of texts) {
|
|
169
|
+
if (t.trim()) {
|
|
170
|
+
thoughtBits.push(t);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const thought = truncate(thoughtBits.join("\n").trim(), cfg.thoughtMaxChars);
|
|
175
|
+
|
|
176
|
+
if (thought) {
|
|
177
|
+
lines.push("assistant: " + thought);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (sigs.length > 0) {
|
|
181
|
+
lines.push(sigs.join("\n"));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return lines.length > 0 ? [lines.join("\n")] : [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function summarizeMessage(msg: JsonObject, cfg: Config): string[] {
|
|
188
|
+
const role = asString(msg.role);
|
|
189
|
+
|
|
190
|
+
if (role === "assistant") {
|
|
191
|
+
return summarizeAssistant(msg, cfg);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (role === "toolResult") {
|
|
195
|
+
const text = contentText(msg.content).trim();
|
|
196
|
+
|
|
197
|
+
if (text && text.length <= cfg.resultMaxChars) {
|
|
198
|
+
return ["result: " + text];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (role === "user" || role === "bashExecution" || role === "custom" || role === "compactionSummary") {
|
|
205
|
+
if (role === "compactionSummary") {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let text = contentText(msg.content).trim();
|
|
210
|
+
|
|
211
|
+
if (!text || startsWithSummaryHeader(text)) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
text = stripTaskNotifications(text).trim();
|
|
216
|
+
|
|
217
|
+
if (!text) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return ["user: " + truncate(text, cfg.humanMaxChars)];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (role === "system") {
|
|
225
|
+
const text = contentText(msg.content).trim();
|
|
226
|
+
|
|
227
|
+
if (!text) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return ["system: " + truncate(text, cfg.humanMaxChars)];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function userMessage(text: string): JsonObject {
|
|
238
|
+
return { role: "user", content: text };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export const DIALECT: Dialect = makeDialect({
|
|
242
|
+
name: "pi",
|
|
243
|
+
digestMessage,
|
|
244
|
+
isAssistant,
|
|
245
|
+
summarizeMessage,
|
|
246
|
+
userMessage,
|
|
247
|
+
isSummaryMessage,
|
|
248
|
+
sessionKey,
|
|
249
|
+
});
|