pi-tuck 1.0.16 → 1.0.18
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 +75 -0
- package/README.md +7 -4
- package/dist/src/budget.d.ts +14 -17
- package/dist/src/budget.js +22 -29
- package/dist/src/build-id.d.ts +1 -1
- package/dist/src/build-id.js +1 -1
- package/dist/src/compaction.js +113 -20
- package/dist/src/diagnostics.d.ts +1 -1
- package/dist/src/history-tools.d.ts +16 -0
- package/dist/src/history-tools.js +59 -0
- package/dist/src/ledger.d.ts +0 -2
- package/dist/src/ledger.js +0 -2
- package/dist/src/omissions.d.ts +1 -1
- package/dist/src/omissions.js +88 -6
- package/dist/src/tuck/core/brief.d.ts +6 -3
- package/dist/src/tuck/core/brief.js +27 -9
- package/dist/src/tuck/core/filter-noise.d.ts +1 -1
- package/dist/src/tuck/core/filter-noise.js +1 -3
- package/dist/src/tuck/core/normalize.d.ts +2 -5
- package/dist/src/tuck/core/normalize.js +15 -33
- package/dist/src/tuck/core/shrink.d.ts +6 -14
- package/dist/src/tuck/core/shrink.js +162 -102
- package/dist/src/tuck/core/summarize.d.ts +3 -14
- package/dist/src/tuck/core/summarize.js +9 -20
- package/dist/src/tuck/core/transcript.d.ts +5 -0
- package/dist/src/tuck/core/transcript.js +27 -4
- package/dist/src/tuck/types.d.ts +1 -1
- package/dist/src/tuck.d.ts +9 -17
- package/dist/src/tuck.js +384 -212
- package/package.json +1 -1
- package/src/budget.ts +24 -26
- package/src/build-id.ts +1 -1
- package/src/compaction.ts +102 -21
- package/src/history-tools.ts +57 -0
- package/src/ledger.ts +0 -2
- package/src/omissions.ts +64 -7
- package/src/tuck/core/brief.ts +25 -10
- package/src/tuck/core/filter-noise.ts +1 -3
- package/src/tuck/core/normalize.ts +11 -27
- package/src/tuck/core/shrink.ts +141 -91
- package/src/tuck/core/summarize.ts +9 -24
- package/src/tuck/core/transcript.ts +26 -4
- package/src/tuck/types.ts +3 -3
- package/src/tuck.ts +336 -185
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { blockRecords, decorateRecord, isDialogue, isOmittable, omissionRecord } from "./brief.js";
|
|
1
|
+
import { blockRecords, decorateRecord, isDialogue, isOmittable, omissionRecord, userPrefixRecords } from "./brief.js";
|
|
2
2
|
import { DIALOGUE_SUFFIX } from "./format.js";
|
|
3
3
|
import { orderByOriginalSource } from "./source-order.js";
|
|
4
4
|
import { serializeRecord, serializeTranscript, TRANSCRIPT_HEADER } from "./transcript.js";
|
|
@@ -6,10 +6,9 @@ const ROLES = ["user", "assistant", "notice"];
|
|
|
6
6
|
/** The lossy variants of a unit, least clipped first: the words kept from the start of its text and from its end. */
|
|
7
7
|
const TIERS = {
|
|
8
8
|
assistant: [[80, 120], [40, 40], [16, 0]],
|
|
9
|
-
user: [
|
|
9
|
+
user: [],
|
|
10
10
|
notice: [[256, 0]],
|
|
11
11
|
};
|
|
12
|
-
const TOOLS_OMITTED = { kind: "omission", reason: "Historical tool actions and observations omitted; read original sources with tuck_recall" };
|
|
13
12
|
const rendered = (records) => ({ records, cost: records.reduce((n, r) => n + serializeRecord(r).length + 1, 0) });
|
|
14
13
|
const WHOLE = { drop: 0, tier: 0, end: 0 };
|
|
15
14
|
/** The units of one role in chronological order, with each variant's cost summed over every prefix. */
|
|
@@ -23,7 +22,26 @@ const createLane = (role, units, mandatory, decorate) => {
|
|
|
23
22
|
// nothing and its unit is counted in the role's one omission notice, like a dropped unit: a notice per empty clip would cost more than text.
|
|
24
23
|
const NONE = { records: [], cost: 0 };
|
|
25
24
|
const emptied = [new Uint8Array(n)];
|
|
26
|
-
const
|
|
25
|
+
const counts = new Int32Array(n + 1), unknown = new Int32Array(n + 1);
|
|
26
|
+
const minimum = new Float64Array(n + 1).fill(Infinity), maximum = new Float64Array(n + 1).fill(-Infinity);
|
|
27
|
+
const firstBySource = new Map(), lastBySource = new Map();
|
|
28
|
+
let monotone = true, previousSource = -1;
|
|
29
|
+
units.forEach((block, at) => {
|
|
30
|
+
const selected = isOmittable(block, mandatory), source = block.sourceIndex;
|
|
31
|
+
counts[at + 1] = counts[at] + Number(selected);
|
|
32
|
+
unknown[at + 1] = unknown[at] + Number(selected && source === undefined);
|
|
33
|
+
minimum[at + 1] = Math.min(minimum[at], selected && source !== undefined ? source : Infinity);
|
|
34
|
+
maximum[at + 1] = Math.max(maximum[at], selected && source !== undefined ? source : -Infinity);
|
|
35
|
+
if (source !== undefined) {
|
|
36
|
+
if (source < previousSource)
|
|
37
|
+
monotone = false;
|
|
38
|
+
previousSource = source;
|
|
39
|
+
if (!firstBySource.has(source))
|
|
40
|
+
firstBySource.set(source, at);
|
|
41
|
+
lastBySource.set(source, at);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const tallies = [];
|
|
27
45
|
/** Level t holds each unit's tier-t variant. A clip that would not make the unit smaller is not taken, so sizes never grow with t. */
|
|
28
46
|
const level = (t) => {
|
|
29
47
|
while (levels.length <= t) {
|
|
@@ -41,65 +59,76 @@ const createLane = (role, units, mandatory, decorate) => {
|
|
|
41
59
|
}
|
|
42
60
|
return clipped.cost < previous[i].cost ? clipped : previous[i];
|
|
43
61
|
});
|
|
44
|
-
const
|
|
45
|
-
|
|
62
|
+
const tally = { count: new Int32Array(n + 1), unknown: new Int32Array(n + 1),
|
|
63
|
+
minSuffix: new Float64Array(n + 1).fill(Infinity), maxSuffix: new Float64Array(n + 1).fill(-Infinity), partial: new Int32Array(n + 1) };
|
|
64
|
+
units.forEach((block, at) => {
|
|
65
|
+
tally.count[at + 1] = tally.count[at] + empty[at];
|
|
66
|
+
tally.unknown[at + 1] = tally.unknown[at] + Number(Boolean(empty[at]) && block.sourceIndex === undefined);
|
|
67
|
+
tally.partial[at + 1] = tally.partial[at] + Number(isOmittable(block, mandatory) && block.sourceIndex !== undefined &&
|
|
68
|
+
next[at].records.some(record => record.kind === "excerpt") && next[at].records.some(record => record.kind === "omission"));
|
|
69
|
+
});
|
|
70
|
+
for (let at = n - 1; at >= 0; at--) {
|
|
71
|
+
const source = empty[at] ? units[at].sourceIndex : undefined;
|
|
72
|
+
tally.minSuffix[at] = Math.min(tally.minSuffix[at + 1], source ?? Infinity);
|
|
73
|
+
tally.maxSuffix[at] = Math.max(tally.maxSuffix[at + 1], source ?? -Infinity);
|
|
74
|
+
}
|
|
75
|
+
tallies[levels.length] = tally;
|
|
46
76
|
levels.push(next);
|
|
47
77
|
sums.push(prefix(next));
|
|
48
78
|
emptied.push(empty);
|
|
49
|
-
emptyCounts.push(counts);
|
|
50
79
|
}
|
|
51
80
|
return levels[t];
|
|
52
81
|
};
|
|
53
82
|
const sum = (t) => { level(t); return sums[t]; };
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const omission = ({ drop, tier, end }) => {
|
|
83
|
+
/** Prefix and clip tallies price every setting in O(1) for chronological
|
|
84
|
+
* session indices. Rendering a candidate never walks its dropped prefix. */
|
|
85
|
+
const omission = ({ drop, tier, end, take }) => {
|
|
58
86
|
if (role === "notice")
|
|
59
87
|
return undefined;
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
if (tier > 0)
|
|
89
|
+
level(tier);
|
|
90
|
+
let count = counts[drop], unlocated = unknown[drop] > 0;
|
|
91
|
+
let min = minimum[drop], max = maximum[drop];
|
|
92
|
+
if (end > drop && tier > 0) {
|
|
93
|
+
const tally = tallies[tier];
|
|
94
|
+
count += tally.count[end] - tally.count[drop];
|
|
95
|
+
unlocated ||= tally.unknown[end] > tally.unknown[drop];
|
|
96
|
+
if (end === n) {
|
|
97
|
+
min = Math.min(min, tally.minSuffix[drop]);
|
|
98
|
+
max = Math.max(max, tally.maxSuffix[drop]);
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
for (let at = drop; at < end; at++)
|
|
102
|
+
if (emptied[tier][at] && units[at].sourceIndex !== undefined) {
|
|
103
|
+
min = Math.min(min, units[at].sourceIndex);
|
|
104
|
+
max = Math.max(max, units[at].sourceIndex);
|
|
72
105
|
}
|
|
106
|
+
if (Number.isFinite(min)) {
|
|
107
|
+
if (monotone) {
|
|
108
|
+
const from = Math.max(drop, firstBySource.get(min)), through = Math.min(end, lastBySource.get(max) + 1);
|
|
109
|
+
if (through > from)
|
|
110
|
+
count += tally.partial[through] - tally.partial[from];
|
|
73
111
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (end > drop && tier > 0) {
|
|
81
|
-
level(tier);
|
|
82
|
-
const empty = emptied[tier], counts = emptyCounts[tier];
|
|
83
|
-
count += counts[end] - counts[drop];
|
|
84
|
-
if (counts[end] > counts[drop]) {
|
|
85
|
-
// Text blocks of one message share a locator: collect distinct ones, enough to tell whether the shown list is complete.
|
|
86
|
-
const extra = new Set();
|
|
87
|
-
for (let i = drop; i < end && (extra.size < 9 || !unlocated); i++)
|
|
88
|
-
if (empty[i]) {
|
|
89
|
-
const source = units[i].sourceIndex;
|
|
90
|
-
if (source === undefined)
|
|
91
|
-
unlocated = true;
|
|
92
|
-
else if (extra.size < 9)
|
|
93
|
-
extra.add(source);
|
|
112
|
+
else
|
|
113
|
+
for (let at = drop; at < end; at++) {
|
|
114
|
+
const source = units[at].sourceIndex, records = levels[tier][at].records;
|
|
115
|
+
if (source !== undefined && source >= min && source <= max && isOmittable(units[at], mandatory) &&
|
|
116
|
+
records.some(record => record.kind === "excerpt") && records.some(record => record.kind === "omission"))
|
|
117
|
+
count++;
|
|
94
118
|
}
|
|
95
|
-
sources = [...new Set([...sources, ...extra])].sort((a, b) => a - b).slice(0, 9);
|
|
96
119
|
}
|
|
97
120
|
}
|
|
98
|
-
|
|
121
|
+
if (take !== undefined && Number.isFinite(min)) {
|
|
122
|
+
const block = units[drop], source = block.sourceIndex;
|
|
123
|
+
if (source !== undefined && source >= min && source <= max && isOmittable(block, mandatory) &&
|
|
124
|
+
userPrefixRecords(block, mandatory, take).some(record => record.kind === "omission"))
|
|
125
|
+
count++;
|
|
126
|
+
}
|
|
127
|
+
return count ? decorate(omissionRecord(role, count, Number.isFinite(min) ? [min, max] : [], unlocated)) : undefined;
|
|
99
128
|
};
|
|
100
129
|
const omissionCosts = new Map();
|
|
101
130
|
const omissionCost = (setting) => {
|
|
102
|
-
const key = `${setting.drop}:${setting.tier}:${setting.end}`;
|
|
131
|
+
const key = `${setting.drop}:${setting.tier}:${setting.end}:${setting.take ?? ""}`;
|
|
103
132
|
let cost = omissionCosts.get(key);
|
|
104
133
|
if (cost === undefined) {
|
|
105
134
|
const record = omission(setting);
|
|
@@ -107,20 +136,25 @@ const createLane = (role, units, mandatory, decorate) => {
|
|
|
107
136
|
}
|
|
108
137
|
return cost;
|
|
109
138
|
};
|
|
139
|
+
const prefixAt = (at, take) => rendered(userPrefixRecords(units[at], mandatory, take).map(decorate));
|
|
110
140
|
return {
|
|
111
|
-
n, level, sum, omission,
|
|
112
|
-
cost: ({ drop, tier, end }) =>
|
|
141
|
+
n, level, sum, omission, prefixAt,
|
|
142
|
+
cost: ({ drop, tier, end, take }) => take !== undefined
|
|
143
|
+
? sums[0][n] - sums[0][drop + 1] + prefixAt(drop, take).cost + omissionCost({ drop, tier: 0, end: 0, take })
|
|
144
|
+
: sums[0][n] - sums[0][Math.max(drop, end)] + (end > drop ? sum(tier)[end] - sum(tier)[drop] : 0) + omissionCost({ drop, tier, end }),
|
|
113
145
|
};
|
|
114
146
|
};
|
|
115
147
|
/** The units of a dialogue and what any configuration of them renders, by arithmetic over per-unit lengths or as the summary text. */
|
|
116
|
-
const
|
|
148
|
+
const prepareDialogue = (input) => {
|
|
117
149
|
const dialogue = input.blocks.filter(isDialogue);
|
|
118
150
|
const times = dialogue.find(block => block.sourceTimes)?.sourceTimes;
|
|
119
151
|
const decorate = (record) => decorateRecord(record, times);
|
|
120
152
|
const lanes = Object.fromEntries(ROLES.map(role => [role, createLane(role, dialogue.filter(block => block.kind === role), input.mandatorySourceIndices, decorate)]));
|
|
121
153
|
const seen = { user: 0, assistant: 0, notice: 0 };
|
|
122
154
|
const order = dialogue.map(block => ({ role: block.kind, i: seen[block.kind]++ }));
|
|
123
|
-
const
|
|
155
|
+
const receipts = input.blocks.filter(block => block.kind === "tool_result" &&
|
|
156
|
+
(block.sourceIndex === undefined || !input.mandatorySourceIndices?.has(block.sourceIndex)));
|
|
157
|
+
const tools = receipts.length ? rendered([omissionRecord("toolResult", receipts.length, [...new Set(receipts.flatMap(block => block.sourceIndex === undefined ? [] : [block.sourceIndex]))], receipts.some(block => block.sourceIndex === undefined))]) : undefined;
|
|
124
158
|
const overhead = TRANSCRIPT_HEADER.length + DIALOGUE_SUFFIX.length;
|
|
125
159
|
/** The length of the summary text of a configuration: its header, one line per record, and the closing section; 0 when it has no record. */
|
|
126
160
|
const size = (config) => {
|
|
@@ -132,7 +166,8 @@ const createDialogue = (input) => {
|
|
|
132
166
|
for (const { role, i } of order) {
|
|
133
167
|
const { drop, tier, end } = config[role];
|
|
134
168
|
if (i >= drop)
|
|
135
|
-
records.push(...
|
|
169
|
+
records.push(...(i === drop && config[role].take !== undefined
|
|
170
|
+
? lanes[role].prefixAt(i, config[role].take).records : lanes[role].level(i < end ? tier : 0)[i].records));
|
|
136
171
|
}
|
|
137
172
|
for (const role of ROLES) {
|
|
138
173
|
const record = lanes[role].omission(config[role]);
|
|
@@ -146,73 +181,98 @@ const createDialogue = (input) => {
|
|
|
146
181
|
};
|
|
147
182
|
return { lanes, size, text };
|
|
148
183
|
};
|
|
184
|
+
const dialogues = new WeakMap();
|
|
185
|
+
const createDialogue = (input) => {
|
|
186
|
+
let dialogue = dialogues.get(input);
|
|
187
|
+
if (!dialogue)
|
|
188
|
+
dialogues.set(input, dialogue = prepareDialogue(input));
|
|
189
|
+
return dialogue;
|
|
190
|
+
};
|
|
149
191
|
/** The whole pre-cut dialogue: user, assistant and notice records; tool actions and observations are omitted. "" when it renders nothing. */
|
|
150
192
|
export const wholeDialogue = (input) => {
|
|
151
193
|
const { text } = createDialogue(input);
|
|
152
194
|
return text({ user: WHOLE, assistant: WHOLE, notice: WHOLE });
|
|
153
195
|
};
|
|
154
|
-
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* B. the newest assistant units that fit a third of the limit whole, the older ones at tier 3 with the oldest d dropped;
|
|
158
|
-
* C. the newest assistant units that fit a third of the limit at tier 3, the rest dropped; then the oldest u user units clipped to tier 1, 2, 3;
|
|
159
|
-
* D. every user unit at tier 3, the oldest dropped;
|
|
160
|
-
* E. the oldest notices dropped, then the oldest of the remaining assistant units.
|
|
161
|
-
* Every step is a one-parameter family whose size is read from prefix sums of the per-unit lengths, so a step costs O(units). A notice is clipped
|
|
162
|
-
* to its one tier in every configuration but the whole dialogue. The size is arithmetic over unit lengths; the rendered text has the last say:
|
|
163
|
-
* a configuration whose text is over the limit is passed over.
|
|
164
|
-
*/
|
|
196
|
+
/** Lower ranks of the single fill: the existing recent assistant third, then
|
|
197
|
+
* more user turns newest first, then the remaining dialogue newest first.
|
|
198
|
+
* Each lane grows only from its recent end; complete passages remain atomic. */
|
|
165
199
|
export const shrinkDialogue = (input, limit) => {
|
|
166
200
|
const { lanes, size, text } = createDialogue(input);
|
|
167
201
|
const { user, assistant, notice } = lanes;
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
if (length > limit)
|
|
172
|
-
return undefined;
|
|
202
|
+
const gone = (lane) => ({ drop: lane.n, tier: 0, end: 0 });
|
|
203
|
+
const config = { user: gone(user), assistant: gone(assistant), notice: gone(notice) };
|
|
204
|
+
const result = (step) => {
|
|
173
205
|
const summary = text(config);
|
|
174
|
-
return summary.length <= limit ? { text: summary, step, size: length } :
|
|
206
|
+
return summary.length <= limit ? { text: summary, step, size: summary.length } : { text: "", step: "none", size: 0 };
|
|
175
207
|
};
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return hit;
|
|
185
|
-
// The newest assistant units that fit a third of the limit whole are kept until users have given way: B clips and drops only older ones.
|
|
186
|
-
// A clip can keep no text at all, so the third is sized on whole units; when the newest alone is over the third, it keeps its largest clip
|
|
187
|
-
// that fits and still has text.
|
|
208
|
+
if (size({ user: WHOLE, assistant: WHOLE, notice: WHOLE }) <= limit) {
|
|
209
|
+
Object.assign(config, { user: WHOLE, assistant: WHOLE, notice: WHOLE });
|
|
210
|
+
return result("whole");
|
|
211
|
+
}
|
|
212
|
+
if (size(config) > limit)
|
|
213
|
+
return { text: "", step: "none", size: 0 };
|
|
214
|
+
// Preserve exactly shrinkDialogue's recent-third definition: newest whole
|
|
215
|
+
// units that fit a third, or the newest unit's largest nonempty passage clip.
|
|
188
216
|
let kept = 0;
|
|
189
217
|
while (kept < assistant.n && assistant.sum(0)[assistant.n] - assistant.sum(0)[kept] > limit / 3)
|
|
190
218
|
kept++;
|
|
191
|
-
let recent = { drop: kept, tier:
|
|
219
|
+
let recent = { drop: kept, tier: 0, end: 0 };
|
|
192
220
|
if (kept === assistant.n && assistant.n > 0)
|
|
193
|
-
for (const
|
|
194
|
-
const cost = assistant.level(
|
|
221
|
+
for (const tier of [1, 2, 3]) {
|
|
222
|
+
const cost = assistant.level(tier)[assistant.n - 1].cost;
|
|
195
223
|
if (cost > 0 && cost <= limit / 3) {
|
|
196
|
-
recent = { drop: assistant.n - 1, tier
|
|
224
|
+
recent = { drop: assistant.n - 1, tier, end: assistant.n };
|
|
197
225
|
break;
|
|
198
226
|
}
|
|
199
227
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
228
|
+
const trySetting = (role, setting) => {
|
|
229
|
+
const previous = config[role];
|
|
230
|
+
config[role] = setting;
|
|
231
|
+
if (size(config) <= limit)
|
|
232
|
+
return true;
|
|
233
|
+
config[role] = previous;
|
|
234
|
+
return false;
|
|
235
|
+
};
|
|
236
|
+
if (!trySetting("assistant", recent)) {
|
|
237
|
+
for (let drop = recent.drop + 1; drop <= assistant.n; drop++)
|
|
238
|
+
if (trySetting("assistant", { ...recent, drop }))
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
// The user lane is filled before older assistant units. If its next whole
|
|
242
|
+
// unit cannot fit, keep its largest exact prefix that fits, then continue
|
|
243
|
+
// with the lower ranks. No fixed task/head character cap participates.
|
|
244
|
+
for (let drop = user.n - 1; drop >= 0; drop--) {
|
|
245
|
+
if (trySetting("user", { drop, tier: 0, end: 0 }))
|
|
246
|
+
continue;
|
|
247
|
+
const block = input.blocks.filter(isDialogue).filter(block => block.kind === "user")[drop];
|
|
248
|
+
if (block.kind === "user" && block.image)
|
|
249
|
+
break;
|
|
250
|
+
let low = 0, high = (block.originalText ?? block.text).length - 1;
|
|
251
|
+
while (low < high) {
|
|
252
|
+
const take = Math.ceil((low + high) / 2);
|
|
253
|
+
if (trySetting("user", { drop, tier: 0, end: 0, take }))
|
|
254
|
+
low = take;
|
|
255
|
+
else
|
|
256
|
+
high = take - 1;
|
|
257
|
+
}
|
|
258
|
+
if (low > 0)
|
|
259
|
+
trySetting("user", { drop, tier: 0, end: 0, take: low });
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
for (let drop = config.assistant.drop - 1; drop >= 0; drop--) {
|
|
263
|
+
if (trySetting("assistant", { drop, tier: 0, end: 0 }))
|
|
264
|
+
continue;
|
|
265
|
+
for (const tier of [1, 2, 3])
|
|
266
|
+
if (assistant.level(tier)[drop].records.some(record => record.kind === "excerpt") &&
|
|
267
|
+
trySetting("assistant", { drop, tier, end: Math.max(drop + 1, recent.end) }))
|
|
268
|
+
break;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
for (let drop = notice.n - 1; drop >= 0; drop--) {
|
|
272
|
+
if (trySetting("notice", { drop, tier: 0, end: 0 }))
|
|
273
|
+
continue;
|
|
274
|
+
trySetting("notice", { drop, tier: 1, end: drop + 1 });
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
return result("ranked");
|
|
218
278
|
};
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import type { CompilerMessage } from "../types.js";
|
|
2
2
|
export interface CompileInput {
|
|
3
3
|
messages: CompilerMessage[];
|
|
4
|
-
/** Eligible original branch only: receipt identity and order. */
|
|
5
|
-
receiptContext?: {
|
|
6
|
-
messages: CompilerMessage[];
|
|
7
|
-
sourceIndices: Array<number | undefined>;
|
|
8
|
-
};
|
|
9
4
|
/** Native session-global source indices parallel to messages. */
|
|
10
5
|
sourceIndices?: Array<number | undefined>;
|
|
11
6
|
/** Actual native entry IDs parallel to originals, not inferred from text. */
|
|
@@ -18,12 +13,6 @@ export interface CompileInput {
|
|
|
18
13
|
mandatorySourceIndices?: ReadonlySet<number>;
|
|
19
14
|
};
|
|
20
15
|
}
|
|
21
|
-
/**
|
|
22
|
-
*
|
|
23
|
-
export declare const compileDialogue: (input: CompileInput) =>
|
|
24
|
-
/** The dialogue shrunk to `ranking.maxBriefChars`, oldest assistant text first (see `shrinkDialogue`); "" when nothing fits. */
|
|
25
|
-
export declare const compileShrunkDialogue: (input: CompileInput & {
|
|
26
|
-
ranking: {
|
|
27
|
-
maxBriefChars: number;
|
|
28
|
-
};
|
|
29
|
-
}) => string;
|
|
16
|
+
/** Compile the lower ranks once under their remaining serialized amount.
|
|
17
|
+
* `step` describes the result for diagnostics; it does not select another plan. */
|
|
18
|
+
export declare const compileDialogue: (input: CompileInput) => import("./shrink.js").Shrunk;
|
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
import { normalize } from "./normalize.js";
|
|
2
2
|
import { filterNoise } from "./filter-noise.js";
|
|
3
|
-
import { shrinkDialogue
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
mandatorySourceIndices: input.ranking?.mandatorySourceIndices,
|
|
13
|
-
sourceIndices: input.sourceIndices ?? input.messages.map((_, index) => index),
|
|
14
|
-
sourceEntryIds: input.sourceEntryIds,
|
|
15
|
-
});
|
|
16
|
-
return prepared;
|
|
17
|
-
};
|
|
18
|
-
/** The whole pre-cut dialogue: user, assistant and notice records; tool actions and observations are omitted.
|
|
19
|
-
* This disposable view grants neither present authority nor native tool execution. */
|
|
20
|
-
export const compileDialogue = (input) => wholeDialogue(dialogueInput(input));
|
|
21
|
-
/** The dialogue shrunk to `ranking.maxBriefChars`, oldest assistant text first (see `shrinkDialogue`); "" when nothing fits. */
|
|
22
|
-
export const compileShrunkDialogue = (input) => shrinkDialogue(dialogueInput(input), input.ranking.maxBriefChars).text;
|
|
3
|
+
import { shrinkDialogue } from "./shrink.js";
|
|
4
|
+
/** Compile the lower ranks once under their remaining serialized amount.
|
|
5
|
+
* `step` describes the result for diagnostics; it does not select another plan. */
|
|
6
|
+
export const compileDialogue = (input) => shrinkDialogue({
|
|
7
|
+
blocks: filterNoise(normalize(input.messages, input.sourceIndices, input.sourceEntryIds, input.sourceTimes)),
|
|
8
|
+
mandatorySourceIndices: input.ranking?.mandatorySourceIndices,
|
|
9
|
+
sourceIndices: input.sourceIndices ?? input.messages.map((_, index) => index),
|
|
10
|
+
sourceEntryIds: input.sourceEntryIds,
|
|
11
|
+
}, input.ranking?.maxBriefChars ?? Infinity);
|
|
@@ -13,6 +13,11 @@ interface RecordFields {
|
|
|
13
13
|
role?: "user" | "assistant" | "custom" | "bashExecution" | "toolResult";
|
|
14
14
|
sourceIndex?: number;
|
|
15
15
|
sourceIndices?: number[];
|
|
16
|
+
/** Inclusive original message-index bounds of a counted omission. */
|
|
17
|
+
sourceRange?: {
|
|
18
|
+
start: number;
|
|
19
|
+
end: number;
|
|
20
|
+
};
|
|
16
21
|
text?: string;
|
|
17
22
|
span?: {
|
|
18
23
|
coordinateSpace: "sanitized-block";
|
|
@@ -15,6 +15,13 @@ export const smallOriginalArguments = (value, maxCharacters = MAX_PRODUCER_ARGUM
|
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
17
|
};
|
|
18
|
+
const validSourceRange = (value) => {
|
|
19
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
20
|
+
return false;
|
|
21
|
+
const range = value;
|
|
22
|
+
return Object.keys(range).length === 2 && Object.keys(range).every(key => key === "start" || key === "end") &&
|
|
23
|
+
Number.isSafeInteger(range.start) && range.start >= 0 && Number.isSafeInteger(range.end) && range.start <= range.end;
|
|
24
|
+
};
|
|
18
25
|
/** The format this build writes. */
|
|
19
26
|
export const TRANSCRIPT_HEADER = "TUCK-TRANSCRIPT/2";
|
|
20
27
|
/** The format of 1.0.x summaries: one JSON object per line. Read, never written. */
|
|
@@ -48,15 +55,19 @@ const LEGACY_RECEIPT_RANGE_NOTICE = RECEIPT_RANGE_NOTICE.replace("tuck_recall",
|
|
|
48
55
|
* record = [ locator " " ] role [ " " id ] [ " " time ] [ " " span ] ":" ... excerpt
|
|
49
56
|
* | [ locator " " ] [ role [ " " id ] " " ] "omitted" [ " " time ] [ " " span ] ":" ... omission of part of one source,
|
|
50
57
|
* or, with no role, of the tool history
|
|
58
|
+
* | role " omitted " count " #" index "-#" index ":" ... counted inclusive index range
|
|
51
59
|
* | role " omitted " count [ " " locator-time { " " locator-time } ] ":" ... omission of unselected text blocks
|
|
52
60
|
* | [ locator " " ] title ":" ... section (file ledger, action trail)
|
|
53
61
|
* locator = "#" index the source's session-wide index, as tuck_recall {"index"} takes it
|
|
54
62
|
* locator-time = locator [ "@" time-value ] every locator of a record has a time, or none does
|
|
55
|
-
* role = "user" | "assistant" | "custom"
|
|
63
|
+
* role = "user" | "assistant" | "custom" | "toolResult" (omissions only)
|
|
56
64
|
* id = a JSON string the native entry id of a custom notice (role "custom" only)
|
|
57
65
|
* time = "@" time-value the recorded time of the locator that precedes the role (a record that has one)
|
|
58
66
|
* time-value = UTC | "journal:" UTC | "unknown" UTC is the message time, "journal:" UTC the session file's time of the entry
|
|
59
67
|
* span = "[chars " start "-" end " of " total "]" the characters of the source text block the text covers or the omission leaves out
|
|
68
|
+
* // New grouped omissions carry a count and inclusive index bounds. The count
|
|
69
|
+
* // is dropped units of that role inside the bounds, not the width of the range.
|
|
70
|
+
* // Older per-source pointer lists and generic tool notices remain readable.
|
|
60
71
|
* count = index how many text blocks the omission counts
|
|
61
72
|
* title = capital letter, then letters, digits, "_" and "-"
|
|
62
73
|
* index = "0" | nonzero digit, then digits a safe integer, no sign, no leading zero
|
|
@@ -120,12 +131,18 @@ export const serializeRecord = (record) => {
|
|
|
120
131
|
if (record.kind !== "excerpt" && record.kind !== "omission")
|
|
121
132
|
return refuse(record);
|
|
122
133
|
const { role, sourceIndex, sourceEntryId, span } = record, omission = record.kind === "omission";
|
|
123
|
-
if (role !== undefined && !["user", "assistant", "custom"].includes(role) || role === undefined && (!omission || sourceEntryId !== undefined) ||
|
|
134
|
+
if (role !== undefined && !["user", "assistant", "custom", ...(omission ? ["toolResult"] : [])].includes(role) || role === undefined && (!omission || sourceEntryId !== undefined) ||
|
|
124
135
|
sourceEntryId !== undefined && (role !== "custom" || typeof sourceEntryId !== "string"))
|
|
125
136
|
refuse(record);
|
|
126
137
|
const who = (sourceEntryId === undefined ? "" : ` ${JSON.stringify(sourceEntryId)}`);
|
|
127
138
|
if (omission && record.count !== undefined) {
|
|
128
|
-
only("kind", "role", "count", "sourceIndices", "reason", "sourceTimes");
|
|
139
|
+
only("kind", "role", "count", "sourceIndices", "sourceRange", "reason", "sourceTimes");
|
|
140
|
+
if (record.sourceRange !== undefined) {
|
|
141
|
+
const range = record.sourceRange;
|
|
142
|
+
if (!validSourceRange(range) || record.sourceIndices !== undefined || record.sourceTimes !== undefined || role === undefined || typeof record.reason !== "string")
|
|
143
|
+
refuse(record);
|
|
144
|
+
return line(`${role} omitted ${index(record.count)} #${index(range.start)}-#${index(range.end)}`, record.reason);
|
|
145
|
+
}
|
|
129
146
|
const locators = record.sourceIndices ?? [], stamped = times(locators);
|
|
130
147
|
if (role === undefined || typeof record.reason !== "string")
|
|
131
148
|
refuse(record);
|
|
@@ -148,8 +165,9 @@ const unescapeText = (text) => text.replace(/\\([\\nr])?/g, (_, c) => c === unde
|
|
|
148
165
|
// Sticky patterns: each is matched at the position the line has been read up to.
|
|
149
166
|
const INDEX = "(0|[1-9]\\d*)";
|
|
150
167
|
const TIME = "(unknown|(?:journal:)?\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?Z)";
|
|
151
|
-
const LOCATOR = new RegExp(`#${INDEX} `, "y"), ROLE = /user|assistant|custom/y, OMITTED = /omitted/y;
|
|
168
|
+
const LOCATOR = new RegExp(`#${INDEX} `, "y"), ROLE = /user|assistant|custom|toolResult/y, OMITTED = /omitted/y;
|
|
152
169
|
const ID = / ("(?:[^"\\\u0000-\u001f]|\\["\\/bfnrt]|\\u[0-9a-fA-F]{4})*")/y, SPACE_OMITTED = / omitted/y, COUNT = new RegExp(` ${INDEX}`, "y");
|
|
170
|
+
const INDEX_RANGE = new RegExp(` #${INDEX}-#${INDEX}`, "y");
|
|
153
171
|
const LOCATOR_TIME = new RegExp(` #${INDEX}(?:@${TIME})?`, "y"), TIME_TOKEN = new RegExp(` @${TIME}`, "y");
|
|
154
172
|
const SPAN = new RegExp(` \\[chars ${INDEX}-${INDEX} of ${INDEX}\\]`, "y"), TITLE_TOKEN = /[A-Z][A-Za-z0-9_-]*/y;
|
|
155
173
|
/** One line of TUCK-TRANSCRIPT/2, as the record the build that wrote it held (its fields in the order that build emits them). */
|
|
@@ -185,6 +203,9 @@ const parseLine = (line) => {
|
|
|
185
203
|
record = { kind: "section", title, text, ...common };
|
|
186
204
|
}
|
|
187
205
|
else if (count) {
|
|
206
|
+
const range = take(INDEX_RANGE);
|
|
207
|
+
if (range)
|
|
208
|
+
return { kind: "omission", role, count: integer(count[1]), sourceRange: { start: integer(range[1]), end: integer(range[2]) }, reason: end() };
|
|
188
209
|
const items = [];
|
|
189
210
|
for (let item; (item = take(LOCATOR_TIME));)
|
|
190
211
|
items.push({ sourceIndex: integer(item[1]), ...(item[2] !== undefined ? { time: timeOf(item[2]) } : {}) });
|
|
@@ -223,6 +244,8 @@ const validate = (r) => {
|
|
|
223
244
|
(v.sourceIndex !== undefined && !integer(v.sourceIndex)) ||
|
|
224
245
|
(v.sourceEntryId !== undefined && (typeof v.sourceEntryId !== "string" || !v.sourceEntryId || v.sourceEntryId.length > 200 ||
|
|
225
246
|
v.role !== "custom" || !["excerpt", "omission"].includes(v.kind))) ||
|
|
247
|
+
(v.sourceRange !== undefined && (v.kind !== "omission" || v.count === undefined || v.sourceIndices !== undefined || v.sourceTimes !== undefined ||
|
|
248
|
+
v.role === undefined || !validSourceRange(v.sourceRange))) ||
|
|
226
249
|
(v.sourceIndices !== undefined && (!Array.isArray(v.sourceIndices) || !v.sourceIndices.every(integer))) ||
|
|
227
250
|
(v.kind !== "omission" && typeof v.text !== "string") ||
|
|
228
251
|
(v.kind === "omission" && typeof v.reason !== "string") ||
|
package/dist/src/tuck/types.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export type NormalizedBlock = {
|
|
|
21
21
|
sourceTimes?: Record<number, import("./core/source-time.js").SourceTime>;
|
|
22
22
|
} & ({
|
|
23
23
|
kind: "user";
|
|
24
|
+
image?: true;
|
|
24
25
|
text: string;
|
|
25
26
|
originalText?: string;
|
|
26
27
|
sourceIndex?: number;
|
|
@@ -47,7 +48,6 @@ export type NormalizedBlock = {
|
|
|
47
48
|
kind: "tool_result";
|
|
48
49
|
name: string;
|
|
49
50
|
sourceIndex?: number;
|
|
50
|
-
recallExport?: true;
|
|
51
51
|
} | {
|
|
52
52
|
kind: "bash";
|
|
53
53
|
sourceIndex?: number;
|
package/dist/src/tuck.d.ts
CHANGED
|
@@ -5,8 +5,11 @@ import { type RecallPolicy, type PreparedKeywordQuery, type RecordedAttempt, typ
|
|
|
5
5
|
type AgentMessage = Parameters<typeof convertToLlm>[0][number];
|
|
6
6
|
type Cut = import("./tuck/cut.js").OwnCutResult;
|
|
7
7
|
/** Counts a projection of the conversation exactly as the triggers, the post-cut checks and the guard do (budget.ts). */
|
|
8
|
-
export type Measure = (messages: AgentMessage[]) => number
|
|
9
|
-
/** The
|
|
8
|
+
export type Measure = ((messages: AgentMessage[]) => number) & {
|
|
9
|
+
/** The same per-message estimate, including any temporary native result clip. */
|
|
10
|
+
messageTokens?: (message: AgentMessage) => number;
|
|
11
|
+
};
|
|
12
|
+
/** Diagnostic description of the single ranked compiler result. Never a decision input. */
|
|
10
13
|
export type DraftMode = "dialogue" | "shrunk";
|
|
11
14
|
type Draft = {
|
|
12
15
|
compaction: Pick<Extract<SessionEntry, {
|
|
@@ -49,23 +52,17 @@ export interface Tuck {
|
|
|
49
52
|
charsPerToken: number;
|
|
50
53
|
};
|
|
51
54
|
buildGlobalIndexById(entries: readonly SessionEntry[]): Map<string, number>;
|
|
52
|
-
/**
|
|
53
|
-
|
|
55
|
+
/** Fill the lower ranks once within the remaining serialized amount. */
|
|
56
|
+
compileDialogue(input: {
|
|
54
57
|
messages: AgentMessage[];
|
|
55
58
|
sourceIndices: (number | undefined)[];
|
|
56
59
|
sourceEntryIds?: readonly string[];
|
|
57
60
|
sourceTimes?: readonly SourceTime[];
|
|
58
|
-
receiptContext?: {
|
|
59
|
-
messages: AgentMessage[];
|
|
60
|
-
sourceIndices: (number | undefined)[];
|
|
61
|
-
};
|
|
62
61
|
ranking: {
|
|
63
62
|
maxBriefChars: number;
|
|
64
63
|
mandatorySourceIndices?: ReadonlySet<number>;
|
|
65
64
|
};
|
|
66
|
-
}):
|
|
67
|
-
/** The whole pre-cut dialogue view; "" offers none, and the shrunk view is used. */
|
|
68
|
-
compileDialogue(input: Parameters<Tuck["compileShrunkDialogue"]>[0]): string;
|
|
65
|
+
}): ReturnType<typeof compileDialogue>;
|
|
69
66
|
prepareKeywordQuery(query: string): PreparedKeywordQuery;
|
|
70
67
|
searchEntriesDetailed(entries: SearchHit[], messages: Message[], query: string, tuning: {
|
|
71
68
|
relativeFloor: number;
|
|
@@ -74,13 +71,8 @@ export interface Tuck {
|
|
|
74
71
|
hits: SearchHit[];
|
|
75
72
|
};
|
|
76
73
|
}
|
|
74
|
+
import { compileDialogue } from "./tuck/core/summarize.js";
|
|
77
75
|
export declare const tuck: Tuck;
|
|
78
|
-
/** The one historical allowance: at most 25% of remaining estimated room, for both views.
|
|
79
|
-
* The whole dialogue is offered only within it (plus actual preview/headroom guards);
|
|
80
|
-
* otherwise the same dialogue is shrunk to fit it. The 4-character conversion matches pinned Pi's
|
|
81
|
-
* summary estimator, not a provider-token guarantee. Mandatory sources (current input,
|
|
82
|
-
* initiating prompt, file ledger) are already charged in base. */
|
|
83
|
-
export declare function historicalAllowance(window: number, reserve: number, base: number, before: number): number;
|
|
84
76
|
/** Uses pi-tuck's own cut, calibrated maximum tail and dialogue compiler; Pi owns persistence and continuation. */
|
|
85
77
|
export declare function tuckDraft(tuck: Tuck, event: SessionBeforeCompactEvent, ctx: ExtensionContext, reserveTokens: number, measure: Measure, policy?: RecallPolicy, retainCurrentInput?: boolean, operatorManual?: boolean, newPrompt?: boolean): Draft;
|
|
86
78
|
/** Shared compiler observes canonical branch and current token estimate; no native preparation utility. */
|