pi-condense 2.6.0 → 2.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/CHANGELOG.md +10 -0
- package/PRUNING.md +27 -0
- package/README.md +11 -0
- package/index.ts +288 -109
- package/package.json +1 -1
- package/src/commands.test.ts +138 -4
- package/src/commands.ts +27 -9
- package/src/context-metrics.test.ts +335 -0
- package/src/context-metrics.ts +152 -0
- package/src/reload-rearm.integration.test.ts +647 -0
- package/src/summarizer-wiring.test.ts +2 -0
- package/src/types.ts +33 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { detectChains } from "./chain-detector.js";
|
|
2
|
+
import { occKey, resultTimestampOf } from "./occurrence-key.js";
|
|
3
|
+
import type { ContextMetricsSnapshot, PruneFrontier } from "./types.js";
|
|
4
|
+
|
|
5
|
+
function charsOf(msg: any): number {
|
|
6
|
+
return JSON.stringify(msg).length;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function tokensOf(msg: any): number {
|
|
10
|
+
return Math.round(charsOf(msg) / 4);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isTextOnlyAssistant(msg: any): boolean {
|
|
14
|
+
if (msg.role !== "assistant") return false;
|
|
15
|
+
if (!Array.isArray(msg.content)) return true;
|
|
16
|
+
return !msg.content.some((b: any) => b.type === "toolCall");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toolCallBlocksOf(msg: any): { id: string; input?: unknown; arguments?: unknown }[] {
|
|
20
|
+
if (!Array.isArray(msg.content)) return [];
|
|
21
|
+
return msg.content.filter((b: any) => b.type === "toolCall");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findArgsForToolCallId(branch: any[], resultIdx: number, toolCallId: string): unknown {
|
|
25
|
+
for (let i = resultIdx - 1; i >= 0; i--) {
|
|
26
|
+
const m = branch[i];
|
|
27
|
+
if (m.role !== "assistant") continue;
|
|
28
|
+
const call = toolCallBlocksOf(m).find((c) => c.id === toolCallId);
|
|
29
|
+
if (call) return (call as any).input ?? (call as any).arguments;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pure snapshot of what the pruner cannot (yet) reclaim: thinking tokens
|
|
36
|
+
* trapped in the trailing open cycle, the largest single chain's share of
|
|
37
|
+
* the branch, and unsummarized toolResult tokens past the prune frontier.
|
|
38
|
+
*/
|
|
39
|
+
export function computeContextMetrics(
|
|
40
|
+
branch: any[],
|
|
41
|
+
frontier: PruneFrontier | null,
|
|
42
|
+
isSummarized: (occurrenceKey: string) => boolean,
|
|
43
|
+
isProtected: (toolName: string, args: unknown) => boolean,
|
|
44
|
+
): ContextMetricsSnapshot {
|
|
45
|
+
if (branch.length === 0) {
|
|
46
|
+
return { openCycleThinkingTokens: 0, largestChainSharePct: 0, frontierGapTokens: 0 };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Open segment: strictly after the last text-only assistant ──────────
|
|
50
|
+
let lastTextOnlyIdx = -1;
|
|
51
|
+
for (let i = 0; i < branch.length; i++) {
|
|
52
|
+
if (isTextOnlyAssistant(branch[i])) lastTextOnlyIdx = i;
|
|
53
|
+
}
|
|
54
|
+
const openStart = lastTextOnlyIdx + 1;
|
|
55
|
+
|
|
56
|
+
let thinkingChars = 0;
|
|
57
|
+
for (let i = openStart; i < branch.length; i++) {
|
|
58
|
+
const m = branch[i];
|
|
59
|
+
if (m.role !== "assistant" || !Array.isArray(m.content)) continue;
|
|
60
|
+
for (const block of m.content) {
|
|
61
|
+
if (block.type === "thinking") thinkingChars += JSON.stringify(block).length;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const openCycleThinkingTokens = Math.round(thinkingChars / 4);
|
|
65
|
+
|
|
66
|
+
// ── Largest chain share ─────────────────────────────────────────────────
|
|
67
|
+
const branchChars = branch.map(charsOf);
|
|
68
|
+
const sumChars = (start: number, end: number): number => {
|
|
69
|
+
let sum = 0;
|
|
70
|
+
for (let i = start; i <= end; i++) sum += branchChars[i];
|
|
71
|
+
return sum;
|
|
72
|
+
};
|
|
73
|
+
const totalChars = branchChars.reduce((a, b) => a + b, 0);
|
|
74
|
+
|
|
75
|
+
const chains = detectChains(branch, isProtected);
|
|
76
|
+
let largestClosedChainChars = 0;
|
|
77
|
+
for (const range of chains) {
|
|
78
|
+
const startIdx = branch.findIndex((m) => m.role === "user" && m.timestamp === range.startUserTimestamp);
|
|
79
|
+
if (startIdx === -1) continue;
|
|
80
|
+
let endIdx: number;
|
|
81
|
+
if (range.finalAssistantTimestamp !== null) {
|
|
82
|
+
endIdx = branch.findIndex(
|
|
83
|
+
(m, i) => i >= startIdx && m.role === "assistant" && m.timestamp === range.finalAssistantTimestamp,
|
|
84
|
+
);
|
|
85
|
+
if (endIdx === -1) continue;
|
|
86
|
+
} else {
|
|
87
|
+
let nextUserIdx = -1;
|
|
88
|
+
for (let i = startIdx + 1; i < branch.length; i++) {
|
|
89
|
+
if (branch[i].role === "user") {
|
|
90
|
+
nextUserIdx = i;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
endIdx = nextUserIdx === -1 ? branch.length - 1 : nextUserIdx - 1;
|
|
95
|
+
}
|
|
96
|
+
if (endIdx < startIdx) continue;
|
|
97
|
+
const chainChars = sumChars(startIdx, endIdx);
|
|
98
|
+
if (chainChars > largestClosedChainChars) largestClosedChainChars = chainChars;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const openSegmentChars = openStart < branch.length ? sumChars(openStart, branch.length - 1) : 0;
|
|
102
|
+
const numerator = Math.max(largestClosedChainChars, openSegmentChars);
|
|
103
|
+
const largestChainSharePct = totalChars === 0 ? 0 : Math.round((100 * numerator) / totalChars);
|
|
104
|
+
|
|
105
|
+
// ── Frontier gap ─────────────────────────────────────────────────────────
|
|
106
|
+
// Exclusion is positional: only toolResults belonging to the boundary turn's
|
|
107
|
+
// own calls (up to and including the last-attempted call) are excluded.
|
|
108
|
+
// Ids are only unique per turn (see occurrence-key.ts), so a later turn may
|
|
109
|
+
// legally reuse a bare id — its result must still count toward the gap.
|
|
110
|
+
let boundaryIdx = -1;
|
|
111
|
+
let boundaryTurnEndIdx = branch.length;
|
|
112
|
+
const boundaryExcludedIds = new Set<string>();
|
|
113
|
+
if (frontier) {
|
|
114
|
+
let counter = 0;
|
|
115
|
+
for (let i = 0; i < branch.length; i++) {
|
|
116
|
+
const m = branch[i];
|
|
117
|
+
if (m.role !== "assistant") continue;
|
|
118
|
+
const turnIdx = counter;
|
|
119
|
+
counter++;
|
|
120
|
+
if (turnIdx === frontier.lastAttemptedTurnIndex) {
|
|
121
|
+
const calls = toolCallBlocksOf(m);
|
|
122
|
+
const k = calls.findIndex((c) => c.id === frontier.lastAttemptedToolCallId);
|
|
123
|
+
if (k !== -1) {
|
|
124
|
+
boundaryIdx = i;
|
|
125
|
+
for (let j = 0; j <= k; j++) boundaryExcludedIds.add(calls[j].id);
|
|
126
|
+
for (let j = i + 1; j < branch.length; j++) {
|
|
127
|
+
if (branch[j].role === "assistant") {
|
|
128
|
+
boundaryTurnEndIdx = j;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let frontierGapTokens = 0;
|
|
139
|
+
const scanStart = boundaryIdx === -1 ? 0 : boundaryIdx + 1;
|
|
140
|
+
for (let i = scanStart; i < branch.length; i++) {
|
|
141
|
+
const m = branch[i];
|
|
142
|
+
if (m.role !== "toolResult") continue;
|
|
143
|
+
if (i < boundaryTurnEndIdx && boundaryExcludedIds.has(m.toolCallId)) continue;
|
|
144
|
+
const key = occKey(m.toolCallId, resultTimestampOf(m.timestamp));
|
|
145
|
+
if (isSummarized(key)) continue;
|
|
146
|
+
const args = findArgsForToolCallId(branch, i, m.toolCallId);
|
|
147
|
+
if (isProtected(m.toolName, args)) continue;
|
|
148
|
+
frontierGapTokens += tokensOf(m);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { openCycleThinkingTokens, largestChainSharePct, frontierGapTokens };
|
|
152
|
+
}
|