pi-mega-compact 0.21.10 → 0.21.11
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.
|
@@ -1,5 +1,60 @@
|
|
|
1
|
-
import { estimateBlockTokens
|
|
1
|
+
import { estimateBlockTokens } from "../../../src/tokens.js";
|
|
2
2
|
import { messageContentText } from "./messageText.js";
|
|
3
|
+
/**
|
|
4
|
+
* Full-surface AgentMessage token estimate for BUDGET arithmetic (tail cap).
|
|
5
|
+
*
|
|
6
|
+
* convertToLlm (pi dist/core/messages.js) ships assistant/toolResult messages
|
|
7
|
+
* VERBATIM — every content block goes over the wire: text, thinking, toolCall
|
|
8
|
+
* (name + full `arguments` JSON), toolResult output, role wrappers. The text
|
|
9
|
+
* extractor (messageContentText) is lossy-on-purpose for analytics, and using
|
|
10
|
+
* it here made a GLM-4.7-style assistant message with ~11.6k bytes of toolCall
|
|
11
|
+
* arguments register as ~77 tokens — a 30k-token tail passed an 11.9k budget,
|
|
12
|
+
* the model overflowed, and pi's one-shot compact-and-retry failed
|
|
13
|
+
* ("Context overflow recovery failed", 2026-08-20 incident).
|
|
14
|
+
*
|
|
15
|
+
* Counts every byte the provider actually receives. Still a heuristic (len/4
|
|
16
|
+
* + 1 per block, like estimateBlockTokens) — just no longer lossy. Never
|
|
17
|
+
* throws: unknown block shapes fall back to their JSON serialization length,
|
|
18
|
+
* and a non-array/string content is counted as its serialization.
|
|
19
|
+
*/
|
|
20
|
+
export function estimateAgentMessageBudgetTokens(m) {
|
|
21
|
+
try {
|
|
22
|
+
const c = m.content;
|
|
23
|
+
let bytes = 0;
|
|
24
|
+
if (typeof c === "string") {
|
|
25
|
+
bytes += c.length;
|
|
26
|
+
}
|
|
27
|
+
else if (Array.isArray(c)) {
|
|
28
|
+
for (const b of c) {
|
|
29
|
+
if (b == null || typeof b !== "object")
|
|
30
|
+
continue;
|
|
31
|
+
const o = b;
|
|
32
|
+
if (typeof o.text === "string")
|
|
33
|
+
bytes += o.text.length;
|
|
34
|
+
if (typeof o.thinking === "string")
|
|
35
|
+
bytes += o.thinking.length;
|
|
36
|
+
if (typeof o.name === "string")
|
|
37
|
+
bytes += o.name.length;
|
|
38
|
+
if (o.arguments != null)
|
|
39
|
+
bytes += JSON.stringify(o.arguments).length;
|
|
40
|
+
if (typeof o.output === "string")
|
|
41
|
+
bytes += o.output.length;
|
|
42
|
+
// Per-block envelope overhead (role/type markers), matching the
|
|
43
|
+
// len/4+1 block accounting in estimateBlockTokens.
|
|
44
|
+
bytes += 4;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (c != null) {
|
|
48
|
+
bytes += JSON.stringify(c).length;
|
|
49
|
+
}
|
|
50
|
+
return estimateBlockTokens(" ".repeat(Math.max(0, bytes)));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// non-fatal: fall back to the legacy text-only estimate rather than
|
|
54
|
+
// disable the cap on a pathological message.
|
|
55
|
+
return estimateBlockTokens(messageContentText(m));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
3
58
|
/**
|
|
4
59
|
* The model's declared maxTokens is only trusted as the output budget when it
|
|
5
60
|
* is plausible. models.json carries sentinel junk for some entries (1e9,
|
|
@@ -91,7 +146,7 @@ export function applyTailCap(opts) {
|
|
|
91
146
|
tailTokens +=
|
|
92
147
|
msgTokens != null
|
|
93
148
|
? Math.max(0, msgTokens[i])
|
|
94
|
-
:
|
|
149
|
+
: estimateAgentMessageBudgetTokens(recentRaw[i]);
|
|
95
150
|
if (tailTokens > budget) {
|
|
96
151
|
// Keep from i+1 onward; never drop below the FINAL message.
|
|
97
152
|
start = Math.min(i + 1, recentRaw.length - 1);
|
|
@@ -119,7 +174,7 @@ export function applyTailCap(opts) {
|
|
|
119
174
|
export function recapReplayedTail(opts) {
|
|
120
175
|
return applyTailCap({
|
|
121
176
|
recentRaw: opts.recentRaw,
|
|
122
|
-
summaryTokens:
|
|
177
|
+
summaryTokens: estimateAgentMessageBudgetTokens(opts.summaryAgentMsg),
|
|
123
178
|
ctxWindow: opts.ctxWindow,
|
|
124
179
|
maxOutputTokens: opts.maxOutputTokens,
|
|
125
180
|
outputReservePct: opts.outputReservePct,
|
|
@@ -16,9 +16,56 @@
|
|
|
16
16
|
* Pure functions, no runtime dependency — trivially unit-testable headlessly.
|
|
17
17
|
*/
|
|
18
18
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
19
|
-
import { estimateBlockTokens
|
|
19
|
+
import { estimateBlockTokens } from "../../../src/tokens.js";
|
|
20
20
|
import { messageContentText } from "./messageText.js";
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Full-surface AgentMessage token estimate for BUDGET arithmetic (tail cap).
|
|
24
|
+
*
|
|
25
|
+
* convertToLlm (pi dist/core/messages.js) ships assistant/toolResult messages
|
|
26
|
+
* VERBATIM — every content block goes over the wire: text, thinking, toolCall
|
|
27
|
+
* (name + full `arguments` JSON), toolResult output, role wrappers. The text
|
|
28
|
+
* extractor (messageContentText) is lossy-on-purpose for analytics, and using
|
|
29
|
+
* it here made a GLM-4.7-style assistant message with ~11.6k bytes of toolCall
|
|
30
|
+
* arguments register as ~77 tokens — a 30k-token tail passed an 11.9k budget,
|
|
31
|
+
* the model overflowed, and pi's one-shot compact-and-retry failed
|
|
32
|
+
* ("Context overflow recovery failed", 2026-08-20 incident).
|
|
33
|
+
*
|
|
34
|
+
* Counts every byte the provider actually receives. Still a heuristic (len/4
|
|
35
|
+
* + 1 per block, like estimateBlockTokens) — just no longer lossy. Never
|
|
36
|
+
* throws: unknown block shapes fall back to their JSON serialization length,
|
|
37
|
+
* and a non-array/string content is counted as its serialization.
|
|
38
|
+
*/
|
|
39
|
+
export function estimateAgentMessageBudgetTokens(m: AgentMessage): number {
|
|
40
|
+
try {
|
|
41
|
+
const c = (m as { content?: unknown }).content;
|
|
42
|
+
let bytes = 0;
|
|
43
|
+
if (typeof c === "string") {
|
|
44
|
+
bytes += c.length;
|
|
45
|
+
} else if (Array.isArray(c)) {
|
|
46
|
+
for (const b of c) {
|
|
47
|
+
if (b == null || typeof b !== "object") continue;
|
|
48
|
+
const o = b as Record<string, unknown>;
|
|
49
|
+
if (typeof o.text === "string") bytes += o.text.length;
|
|
50
|
+
if (typeof o.thinking === "string") bytes += o.thinking.length;
|
|
51
|
+
if (typeof o.name === "string") bytes += o.name.length;
|
|
52
|
+
if (o.arguments != null) bytes += JSON.stringify(o.arguments).length;
|
|
53
|
+
if (typeof o.output === "string") bytes += o.output.length;
|
|
54
|
+
// Per-block envelope overhead (role/type markers), matching the
|
|
55
|
+
// len/4+1 block accounting in estimateBlockTokens.
|
|
56
|
+
bytes += 4;
|
|
57
|
+
}
|
|
58
|
+
} else if (c != null) {
|
|
59
|
+
bytes += JSON.stringify(c).length;
|
|
60
|
+
}
|
|
61
|
+
return estimateBlockTokens(" ".repeat(Math.max(0, bytes)));
|
|
62
|
+
} catch {
|
|
63
|
+
// non-fatal: fall back to the legacy text-only estimate rather than
|
|
64
|
+
// disable the cap on a pathological message.
|
|
65
|
+
return estimateBlockTokens(messageContentText(m));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
22
69
|
/**
|
|
23
70
|
* The model's declared maxTokens is only trusted as the output budget when it
|
|
24
71
|
* is plausible. models.json carries sentinel junk for some entries (1e9,
|
|
@@ -143,7 +190,7 @@ export function applyTailCap(opts: {
|
|
|
143
190
|
tailTokens +=
|
|
144
191
|
msgTokens != null
|
|
145
192
|
? Math.max(0, msgTokens[i])
|
|
146
|
-
:
|
|
193
|
+
: estimateAgentMessageBudgetTokens(recentRaw[i]);
|
|
147
194
|
if (tailTokens > budget) {
|
|
148
195
|
// Keep from i+1 onward; never drop below the FINAL message.
|
|
149
196
|
start = Math.min(i + 1, recentRaw.length - 1);
|
|
@@ -181,7 +228,7 @@ export function recapReplayedTail(opts: {
|
|
|
181
228
|
}): { recent: AgentMessage[]; dropped: number } {
|
|
182
229
|
return applyTailCap({
|
|
183
230
|
recentRaw: opts.recentRaw,
|
|
184
|
-
summaryTokens:
|
|
231
|
+
summaryTokens: estimateAgentMessageBudgetTokens(opts.summaryAgentMsg),
|
|
185
232
|
ctxWindow: opts.ctxWindow,
|
|
186
233
|
maxOutputTokens: opts.maxOutputTokens,
|
|
187
234
|
outputReservePct: opts.outputReservePct,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.11",
|
|
4
4
|
"description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-3-Clause",
|