@shanepadgett/tau-agent 0.20.1 → 0.21.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/extensions/context-pruning/README.md +18 -114
- package/extensions/context-pruning/index.ts +74 -74
- package/extensions/context-pruning/projection.ts +40 -96
- package/extensions/context-pruning/prune.ts +138 -283
- package/extensions/context-pruning/render.ts +99 -35
- package/extensions/context-pruning/settings.ts +14 -15
- package/extensions/explore/read-cache.ts +31 -7
- package/extensions/handoff/README.md +7 -0
- package/extensions/handoff/index.ts +139 -0
- package/extensions/handoff/model.ts +71 -0
- package/extensions/subagent/index.ts +3 -1
- package/extensions/tau-help/help.md +5 -1
- package/package.json +2 -2
- package/schemas/tau.schema.json +15 -12
- package/shared/context-pruning-state.ts +69 -273
- package/extensions/context-pruning/file-evidence.ts +0 -265
|
@@ -1,43 +1,27 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import {
|
|
2
|
-
estimateTokens,
|
|
3
|
-
type ContextEvent,
|
|
4
|
-
type ExtensionAPI,
|
|
5
3
|
type ExtensionContext,
|
|
6
4
|
type SessionEntry,
|
|
7
5
|
} from "@earendil-works/pi-coding-agent";
|
|
8
6
|
import { type Static, Type } from "typebox";
|
|
9
|
-
import type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
import type {
|
|
8
|
+
ContextPruneDeferredFileV2,
|
|
9
|
+
ContextPruneDetailsV2,
|
|
10
|
+
} from "../../shared/context-pruning-state.ts";
|
|
11
|
+
import { prepareAutoreadMessage, type PreparedAutoreadMessage } from "../explore/autoread.ts";
|
|
12
|
+
import { MAX_COMPLETE_FILE_SNAPSHOT_BYTES } from "../explore/full-file-knowledge.ts";
|
|
13
13
|
|
|
14
14
|
export const contextPruneParameters = Type.Object(
|
|
15
15
|
{
|
|
16
16
|
keepFiles: Type.Array(
|
|
17
|
-
Type.Object(
|
|
18
|
-
{
|
|
19
|
-
path: Type.String(),
|
|
20
|
-
relevance: Type.String(),
|
|
21
|
-
},
|
|
22
|
-
{ additionalProperties: false },
|
|
23
|
-
),
|
|
17
|
+
Type.Object({ path: Type.String(), relevance: Type.String() }, { additionalProperties: false }),
|
|
24
18
|
),
|
|
25
19
|
keepToolCalls: Type.Array(
|
|
26
|
-
Type.Object(
|
|
27
|
-
{
|
|
28
|
-
toolCallId: Type.String(),
|
|
29
|
-
relevance: Type.String(),
|
|
30
|
-
},
|
|
31
|
-
{ additionalProperties: false },
|
|
32
|
-
),
|
|
20
|
+
Type.Object({ toolCallId: Type.String(), relevance: Type.String() }, { additionalProperties: false }),
|
|
33
21
|
),
|
|
34
22
|
deferFiles: Type.Array(
|
|
35
23
|
Type.Object(
|
|
36
|
-
{
|
|
37
|
-
path: Type.String(),
|
|
38
|
-
reason: Type.String(),
|
|
39
|
-
relevantWhen: Type.String(),
|
|
40
|
-
},
|
|
24
|
+
{ path: Type.String(), reason: Type.String(), relevantWhen: Type.String() },
|
|
41
25
|
{ additionalProperties: false },
|
|
42
26
|
),
|
|
43
27
|
),
|
|
@@ -47,294 +31,165 @@ export const contextPruneParameters = Type.Object(
|
|
|
47
31
|
|
|
48
32
|
export type ContextPruneInput = Static<typeof contextPruneParameters>;
|
|
49
33
|
|
|
50
|
-
interface ProjectionSnapshot {
|
|
51
|
-
generation: number;
|
|
52
|
-
messages: ContextEvent["messages"];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
34
|
interface ContextPruneExecutionOptions {
|
|
56
|
-
pi: Pick<ExtensionAPI, "sendMessage">;
|
|
57
35
|
toolCallId: string;
|
|
58
36
|
params: ContextPruneInput;
|
|
59
37
|
signal: AbortSignal | undefined;
|
|
60
38
|
ctx: ExtensionContext;
|
|
61
|
-
|
|
39
|
+
generation: number;
|
|
62
40
|
currentGeneration: () => number;
|
|
63
|
-
currentEnabled: () => boolean;
|
|
64
|
-
minimumReclaimTokens: number;
|
|
65
41
|
}
|
|
66
42
|
|
|
67
43
|
interface ContextPruneExecutionResult {
|
|
68
44
|
content: Array<{ type: "text"; text: string }>;
|
|
69
|
-
details:
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
interface PreparedPrune {
|
|
73
|
-
messages: Array<Parameters<ExtensionAPI["sendMessage"]>[0]>;
|
|
74
|
-
result: ContextPruneExecutionResult;
|
|
45
|
+
details: ContextPruneDetailsV2;
|
|
75
46
|
}
|
|
76
47
|
|
|
77
48
|
export async function executeContextPrune(options: ContextPruneExecutionOptions): Promise<ContextPruneExecutionResult> {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
currentToolCalls[0]?.id !== options.toolCallId ||
|
|
108
|
-
currentToolCalls[0]?.name !== "context_prune"
|
|
109
|
-
) {
|
|
110
|
-
return skipped("context_prune must be the only tool call in its assistant message.");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
let pairs: Set<string>;
|
|
114
|
-
let autoreads: Map<string, ContextMessage>;
|
|
115
|
-
let preparedPrune: PreparedPrune;
|
|
116
|
-
try {
|
|
117
|
-
pairs = indexCompleteToolPairs(projection.messages);
|
|
118
|
-
autoreads = indexAutoreads(projection.messages);
|
|
119
|
-
} catch (error) {
|
|
120
|
-
return skipped(errorMessage(error));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const explicitlyRetained = new Set<string>();
|
|
124
|
-
for (const selection of options.params.keepToolCalls) {
|
|
125
|
-
if (explicitlyRetained.has(selection.toolCallId)) {
|
|
126
|
-
return skipped(`tool-call ID ${selection.toolCallId} was selected more than once.`);
|
|
127
|
-
}
|
|
128
|
-
if (!pairs.has(selection.toolCallId)) {
|
|
129
|
-
return skipped(`tool-call ID ${selection.toolCallId} is not a complete currently projected exchange.`);
|
|
130
|
-
}
|
|
131
|
-
explicitlyRetained.add(selection.toolCallId);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
const canonical = await canonicalizeFileSelections({
|
|
136
|
-
cwd: options.ctx.cwd,
|
|
137
|
-
keepFiles: options.params.keepFiles,
|
|
138
|
-
deferFiles: options.params.deferFiles,
|
|
139
|
-
});
|
|
140
|
-
const fileEvidence = await selectFileEvidence({
|
|
141
|
-
cwd: options.ctx.cwd,
|
|
142
|
-
messages: projection.messages,
|
|
143
|
-
files: canonical.keepFiles,
|
|
144
|
-
anchorToolCallId: options.toolCallId,
|
|
145
|
-
signal: options.signal,
|
|
146
|
-
isLifecycleCurrent: () => projection.generation === options.currentGeneration(),
|
|
147
|
-
});
|
|
148
|
-
options.signal?.throwIfAborted();
|
|
149
|
-
if (projection.generation !== options.currentGeneration()) {
|
|
150
|
-
throw new Error("Prune preparation crossed a session lifecycle boundary");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const retainedTools = new Set([...explicitlyRetained, ...fileEvidence.retainedToolCallIds]);
|
|
154
|
-
const retainedAutoreads = new Set(fileEvidence.retainedAutoreadRowIds);
|
|
155
|
-
const retainedToolCallIds = [...pairs.keys()].filter((id) => retainedTools.has(id));
|
|
156
|
-
const retainedAutoreadRowIds = [
|
|
157
|
-
...[...autoreads.keys()].filter((id) => retainedAutoreads.has(id)),
|
|
158
|
-
...fileEvidence.preparedSnapshots.map((snapshot) => snapshot.details.rowId),
|
|
159
|
-
];
|
|
160
|
-
const newlyPrunedToolCallIds = [...pairs.keys()].filter((id) => !retainedTools.has(id));
|
|
161
|
-
const newlyPrunedAutoreadRowIds = [...autoreads.keys()].filter((id) => !retainedAutoreads.has(id));
|
|
162
|
-
const deferredFiles: ContextPruneDeferredFileV1[] = canonical.deferFiles.map((file) => ({
|
|
163
|
-
path: file.displayPath,
|
|
164
|
-
reason: file.request.reason,
|
|
165
|
-
relevantWhen: file.request.relevantWhen,
|
|
166
|
-
}));
|
|
167
|
-
const deferredMessage = deferredFiles.length === 0 ? undefined : createDeferredMessage(deferredFiles);
|
|
168
|
-
const beforeMessages = [...projection.messages, currentAssistant];
|
|
169
|
-
const afterMessages = projectCandidate(
|
|
170
|
-
beforeMessages,
|
|
171
|
-
new Set(newlyPrunedToolCallIds),
|
|
172
|
-
new Set(newlyPrunedAutoreadRowIds),
|
|
173
|
-
);
|
|
174
|
-
for (const prepared of fileEvidence.preparedSnapshots) {
|
|
175
|
-
afterMessages.push({
|
|
176
|
-
role: "custom",
|
|
177
|
-
customType: prepared.customType,
|
|
178
|
-
content: prepared.content,
|
|
179
|
-
display: prepared.display,
|
|
180
|
-
details: prepared.details,
|
|
181
|
-
timestamp: 0,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
if (deferredMessage) afterMessages.push({ role: "custom", ...deferredMessage, timestamp: 0 });
|
|
185
|
-
const tokensBefore = beforeMessages.reduce((total, message) => total + estimateTokens(message), 0);
|
|
186
|
-
const tokensAfter = afterMessages.reduce((total, message) => total + estimateTokens(message), 0);
|
|
187
|
-
const tokensReclaimed = tokensBefore - tokensAfter;
|
|
188
|
-
if (tokensReclaimed < options.minimumReclaimTokens) {
|
|
189
|
-
return skipped(
|
|
190
|
-
`estimated reclaim is ${tokensReclaimed} tokens, below the ${options.minimumReclaimTokens}-token minimum.`,
|
|
191
|
-
{ before: tokensBefore, after: tokensAfter },
|
|
49
|
+
assertCurrent(options);
|
|
50
|
+
const retainedToolCallIds = [...new Set(options.params.keepToolCalls.map((selection) => selection.toolCallId))];
|
|
51
|
+
const retainedTools = new Set(retainedToolCallIds);
|
|
52
|
+
const { prunedToolCallIds, prunedAutoreadRowIds } = collectPrunedRows(
|
|
53
|
+
options.ctx.sessionManager.getBranch(),
|
|
54
|
+
options.toolCallId,
|
|
55
|
+
retainedTools,
|
|
56
|
+
);
|
|
57
|
+
const warnings: string[] = [];
|
|
58
|
+
const preparedSnapshots: PreparedAutoreadMessage[] = [];
|
|
59
|
+
const keptPaths = new Set<string>();
|
|
60
|
+
for (const selection of options.params.keepFiles) {
|
|
61
|
+
assertCurrent(options);
|
|
62
|
+
const path = normalizePath(selection.path);
|
|
63
|
+
const pathKey = resolve(options.ctx.cwd, path);
|
|
64
|
+
if (keptPaths.has(pathKey)) continue;
|
|
65
|
+
keptPaths.add(pathKey);
|
|
66
|
+
try {
|
|
67
|
+
preparedSnapshots.push(
|
|
68
|
+
await prepareAutoreadMessage({
|
|
69
|
+
rowId: `${options.toolCallId}:${preparedSnapshots.length}`,
|
|
70
|
+
path,
|
|
71
|
+
cwd: options.ctx.cwd,
|
|
72
|
+
source: "context-pruning",
|
|
73
|
+
batchId: options.toolCallId,
|
|
74
|
+
signal: options.signal,
|
|
75
|
+
isLifecycleCurrent: () => options.generation === options.currentGeneration(),
|
|
76
|
+
maximumBytes: MAX_COMPLETE_FILE_SNAPSHOT_BYTES,
|
|
77
|
+
}),
|
|
192
78
|
);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (options.signal?.aborted || options.generation !== options.currentGeneration()) throw error;
|
|
81
|
+
warnings.push(`${path}: ${errorMessage(error)}`);
|
|
193
82
|
}
|
|
194
|
-
|
|
195
|
-
options.signal?.throwIfAborted();
|
|
196
|
-
if (projection.generation !== options.currentGeneration()) {
|
|
197
|
-
throw new Error("Prune preparation crossed a session lifecycle boundary");
|
|
198
|
-
}
|
|
199
|
-
const details: ContextPruneDetailsV1 = {
|
|
200
|
-
v: 1,
|
|
201
|
-
status: "applied",
|
|
202
|
-
anchorToolCallId: options.toolCallId,
|
|
203
|
-
newlyPrunedToolCallIds,
|
|
204
|
-
newlyPrunedAutoreadRowIds,
|
|
205
|
-
retainedToolCallIds,
|
|
206
|
-
retainedAutoreadRowIds,
|
|
207
|
-
refreshedFiles: [...fileEvidence.refreshedFiles],
|
|
208
|
-
deferredFiles,
|
|
209
|
-
tokensBefore,
|
|
210
|
-
tokensAfter,
|
|
211
|
-
tokensReclaimed,
|
|
212
|
-
};
|
|
213
|
-
preparedPrune = {
|
|
214
|
-
messages: [...fileEvidence.preparedSnapshots, ...(deferredMessage === undefined ? [] : [deferredMessage])],
|
|
215
|
-
result: {
|
|
216
|
-
content: [
|
|
217
|
-
{
|
|
218
|
-
type: "text",
|
|
219
|
-
text: `Prune applied: reclaimed about ${tokensReclaimed} tokens. Continue with the next action stated before this call.`,
|
|
220
|
-
},
|
|
221
|
-
],
|
|
222
|
-
details,
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
} catch (error) {
|
|
226
|
-
if (options.signal?.aborted) throw error;
|
|
227
|
-
if (projection.generation !== options.currentGeneration()) throw error;
|
|
228
|
-
return skipped(errorMessage(error));
|
|
229
83
|
}
|
|
230
84
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
for (const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const results = new Map<string, { name: string; index: number }>();
|
|
240
|
-
for (let index = 0; index < messages.length; index += 1) {
|
|
241
|
-
const message = messages[index];
|
|
242
|
-
if (!message) continue;
|
|
243
|
-
if (message.role === "assistant") {
|
|
244
|
-
for (const block of message.content) {
|
|
245
|
-
if (block.type !== "toolCall") continue;
|
|
246
|
-
if (calls.has(block.id)) throw new Error(`Duplicate projected tool-call ID: ${block.id}.`);
|
|
247
|
-
calls.set(block.id, { name: block.name, index });
|
|
248
|
-
}
|
|
249
|
-
} else if (message.role === "toolResult") {
|
|
250
|
-
if (results.has(message.toolCallId))
|
|
251
|
-
throw new Error(`Duplicate projected tool result: ${message.toolCallId}.`);
|
|
252
|
-
results.set(message.toolCallId, { name: message.toolName, index });
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
const pairs = new Set<string>();
|
|
256
|
-
for (const [id, call] of calls) {
|
|
257
|
-
const result = results.get(id);
|
|
258
|
-
if (!result || result.name !== call.name || result.index <= call.index) {
|
|
259
|
-
throw new Error(`Incomplete projected tool exchange: ${id}.`);
|
|
260
|
-
}
|
|
261
|
-
pairs.add(id);
|
|
85
|
+
const deferredFiles: ContextPruneDeferredFileV2[] = [];
|
|
86
|
+
const deferredPaths = new Set<string>();
|
|
87
|
+
for (const selection of options.params.deferFiles) {
|
|
88
|
+
const path = normalizePath(selection.path);
|
|
89
|
+
const pathKey = resolve(options.ctx.cwd, path);
|
|
90
|
+
if (keptPaths.has(pathKey) || deferredPaths.has(pathKey)) continue;
|
|
91
|
+
deferredPaths.add(pathKey);
|
|
92
|
+
deferredFiles.push({ path, reason: selection.reason, relevantWhen: selection.relevantWhen });
|
|
262
93
|
}
|
|
263
|
-
|
|
264
|
-
|
|
94
|
+
assertCurrent(options);
|
|
95
|
+
|
|
96
|
+
const retainedAutoreadRowIds = preparedSnapshots.map((snapshot) => snapshot.details.rowId);
|
|
97
|
+
const refreshedFiles = preparedSnapshots.map((snapshot) => ({
|
|
98
|
+
path: snapshot.details.path,
|
|
99
|
+
rowId: snapshot.details.rowId,
|
|
100
|
+
servedHash: snapshot.details.readCache.servedHash,
|
|
101
|
+
autoreadDetails: { ...snapshot.details },
|
|
102
|
+
}));
|
|
103
|
+
const details: ContextPruneDetailsV2 = {
|
|
104
|
+
v: 2,
|
|
105
|
+
anchorToolCallId: options.toolCallId,
|
|
106
|
+
prunedToolCallIds,
|
|
107
|
+
prunedAutoreadRowIds,
|
|
108
|
+
retainedToolCallIds,
|
|
109
|
+
retainedAutoreadRowIds,
|
|
110
|
+
refreshedFiles,
|
|
111
|
+
deferredFiles,
|
|
112
|
+
warnings,
|
|
113
|
+
};
|
|
114
|
+
const status =
|
|
115
|
+
warnings.length === 0
|
|
116
|
+
? "Context checkpoint applied. Continue with the next action stated before this call."
|
|
117
|
+
: `Context checkpoint applied with ${warnings.length} warning${warnings.length === 1 ? "" : "s"}:\n${warnings.map((warning) => `- ${warning}`).join("\n")}\nContinue with the next action stated before this call.`;
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{ type: "text", text: status },
|
|
121
|
+
...preparedSnapshots.map((snapshot) => ({ type: "text" as const, text: snapshot.content })),
|
|
122
|
+
...(deferredFiles.length === 0
|
|
123
|
+
? []
|
|
124
|
+
: [{ type: "text" as const, text: deferredFileText(deferredFiles) }]),
|
|
125
|
+
],
|
|
126
|
+
details,
|
|
127
|
+
};
|
|
265
128
|
}
|
|
266
129
|
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
130
|
+
function collectPrunedRows(
|
|
131
|
+
branch: readonly SessionEntry[],
|
|
132
|
+
anchorToolCallId: string,
|
|
133
|
+
retainedToolCallIds: ReadonlySet<string>,
|
|
134
|
+
): { prunedToolCallIds: string[]; prunedAutoreadRowIds: string[] } {
|
|
135
|
+
let anchorIndex = -1;
|
|
136
|
+
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
137
|
+
const entry = branch[index];
|
|
271
138
|
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
message.
|
|
139
|
+
entry?.type === "message" &&
|
|
140
|
+
entry.message.role === "assistant" &&
|
|
141
|
+
entry.message.content.some(
|
|
142
|
+
(block) => block.type === "toolCall" && block.id === anchorToolCallId && block.name === "context_prune",
|
|
143
|
+
)
|
|
275
144
|
) {
|
|
276
|
-
|
|
145
|
+
anchorIndex = index;
|
|
146
|
+
break;
|
|
277
147
|
}
|
|
278
|
-
if (rows.has(message.details.rowId))
|
|
279
|
-
throw new Error(`Duplicate projected autoread row: ${message.details.rowId}.`);
|
|
280
|
-
rows.set(message.details.rowId, message);
|
|
281
148
|
}
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
branch: readonly SessionEntry[],
|
|
287
|
-
toolCallId: string,
|
|
288
|
-
): Extract<ContextMessage, { role: "assistant" }> | undefined {
|
|
289
|
-
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
149
|
+
if (anchorIndex < 0) return { prunedToolCallIds: [], prunedAutoreadRowIds: [] };
|
|
150
|
+
const prunedToolCallIds = new Set<string>();
|
|
151
|
+
const prunedAutoreadRowIds = new Set<string>();
|
|
152
|
+
for (let index = 0; index < anchorIndex; index += 1) {
|
|
290
153
|
const entry = branch[index];
|
|
291
|
-
if (entry?.type
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return undefined;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function projectCandidate(
|
|
299
|
-
messages: readonly ContextMessage[],
|
|
300
|
-
prunedToolIds: ReadonlySet<string>,
|
|
301
|
-
prunedAutoreadIds: ReadonlySet<string>,
|
|
302
|
-
): ContextMessage[] {
|
|
303
|
-
const projected: ContextMessage[] = [];
|
|
304
|
-
for (const message of messages) {
|
|
305
|
-
if (message.role === "toolResult" && prunedToolIds.has(message.toolCallId)) continue;
|
|
306
|
-
if (
|
|
307
|
-
message.role === "custom" &&
|
|
308
|
-
(message.customType === "tau.context-pruning.nudge" || message.customType === "tau.context-pruning.deferred")
|
|
309
|
-
)
|
|
154
|
+
if (entry?.type === "message" && entry.message.role === "assistant") {
|
|
155
|
+
for (const block of entry.message.content) {
|
|
156
|
+
if (block.type === "toolCall" && !retainedToolCallIds.has(block.id)) prunedToolCallIds.add(block.id);
|
|
157
|
+
}
|
|
310
158
|
continue;
|
|
311
|
-
if (message.role === "custom" && message.customType === "tau.autoread" && isRecord(message.details)) {
|
|
312
|
-
const rowId = message.details.rowId;
|
|
313
|
-
if (typeof rowId === "string" && prunedAutoreadIds.has(rowId)) continue;
|
|
314
159
|
}
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
160
|
+
if (entry?.type !== "custom_message" || entry.customType !== "tau.autoread") continue;
|
|
161
|
+
if (isRecord(entry.details) && typeof entry.details.rowId === "string") {
|
|
162
|
+
prunedAutoreadRowIds.add(entry.details.rowId);
|
|
318
163
|
}
|
|
319
|
-
|
|
320
|
-
(block) => block.type !== "thinking" && !(block.type === "toolCall" && prunedToolIds.has(block.id)),
|
|
321
|
-
);
|
|
322
|
-
if (content.length > 0)
|
|
323
|
-
projected.push(content.length === message.content.length ? message : { ...message, content });
|
|
164
|
+
continue;
|
|
324
165
|
}
|
|
325
|
-
|
|
166
|
+
for (let index = 0; index < anchorIndex; index += 1) {
|
|
167
|
+
const entry = branch[index];
|
|
168
|
+
if (entry?.type !== "message" || entry.message.role !== "toolResult") continue;
|
|
169
|
+
if (entry.message.toolName !== "context_prune" || !isRecord(entry.message.details)) continue;
|
|
170
|
+
const files = entry.message.details.refreshedFiles;
|
|
171
|
+
if (!Array.isArray(files)) continue;
|
|
172
|
+
for (const file of files) if (isRecord(file) && typeof file.rowId === "string") prunedAutoreadRowIds.add(file.rowId);
|
|
173
|
+
}
|
|
174
|
+
return { prunedToolCallIds: [...prunedToolCallIds], prunedAutoreadRowIds: [...prunedAutoreadRowIds] };
|
|
326
175
|
}
|
|
327
176
|
|
|
328
|
-
function
|
|
329
|
-
return
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
177
|
+
function deferredFileText(files: readonly ContextPruneDeferredFileV2[]): string {
|
|
178
|
+
return [
|
|
179
|
+
"Deferred files are advisory. Reconsider them only when their condition applies:",
|
|
180
|
+
...files.map((file) => `- ${file.path}: ${file.reason} Relevant when: ${file.relevantWhen}`),
|
|
181
|
+
].join("\n");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizePath(path: string): string {
|
|
185
|
+
return path.replace(/^@/, "");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function assertCurrent(options: ContextPruneExecutionOptions): void {
|
|
189
|
+
options.signal?.throwIfAborted();
|
|
190
|
+
if (options.generation !== options.currentGeneration()) {
|
|
191
|
+
throw new Error("Context checkpoint crossed a session lifecycle boundary");
|
|
192
|
+
}
|
|
338
193
|
}
|
|
339
194
|
|
|
340
195
|
function errorMessage(error: unknown): string {
|