@shanepadgett/tau-agent 0.16.0 → 0.17.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/docs/subagents.md +25 -6
- package/extensions/attention/README.md +1 -0
- package/extensions/attention/index.ts +38 -0
- package/extensions/context/README.md +13 -3
- package/extensions/context/definitions.ts +3 -15
- package/extensions/context/evidence.ts +516 -0
- package/extensions/context/index.ts +114 -116
- package/extensions/context/panel.ts +60 -0
- package/extensions/context/settings.ts +30 -1
- package/extensions/context/sync.ts +171 -686
- package/extensions/context/validation.ts +4 -1
- package/extensions/context/write-scope.ts +109 -0
- package/extensions/context-pruning/README.md +22 -0
- package/extensions/context-pruning/file-evidence.ts +265 -0
- package/extensions/context-pruning/index.ts +379 -0
- package/extensions/context-pruning/projection.ts +108 -0
- package/extensions/context-pruning/prune.ts +346 -0
- package/extensions/context-pruning/render.ts +179 -0
- package/extensions/context-pruning/settings.ts +41 -0
- package/extensions/explore/README.md +3 -1
- package/extensions/explore/autoread.ts +85 -22
- package/extensions/explore/full-file-knowledge.ts +234 -0
- package/extensions/explore/index.ts +2 -3
- package/extensions/explore/read-cache.ts +150 -86
- package/extensions/explore/read-snapshots.ts +17 -4
- package/extensions/explore/read.ts +57 -38
- package/extensions/footer/index.ts +62 -60
- package/extensions/run-summary/index.ts +5 -5
- package/extensions/silent-command-runner/README.md +1 -1
- package/extensions/silent-command-runner/index.ts +45 -28
- package/extensions/soul/prompt.ts +3 -1
- package/extensions/subagent/README.md +21 -5
- package/extensions/subagent/agents/context-sync.md +90 -0
- package/extensions/subagent/agents/{generalist.md → dormant/generalist.md} +6 -0
- package/extensions/subagent/agents/{scout.md → dormant/scout.md} +6 -0
- package/extensions/subagent/agents/review.md +48 -0
- package/extensions/subagent/agents/web-research.md +6 -0
- package/extensions/subagent/agents.ts +15 -2
- package/extensions/subagent/cmux-dashboard.ts +454 -0
- package/extensions/subagent/index.ts +181 -234
- package/extensions/subagent/render.ts +1 -1
- package/extensions/subagent/resume.ts +78 -0
- package/extensions/subagent/run.ts +213 -118
- package/extensions/subagent/runtime.ts +856 -0
- package/extensions/subagent/session-resource.ts +169 -0
- package/extensions/tau-help/help.md +6 -2
- package/extensions/turn-budget/index.ts +8 -36
- package/package.json +2 -2
- package/schemas/tau.schema.json +48 -1
- package/shared/context-pruning-state.ts +364 -0
- package/shared/events.ts +18 -0
- package/shared/model-fallback/index.ts +21 -10
- package/shared/model-fallback/types.ts +5 -3
- package/shared/settings/load.ts +78 -1
- package/shared/tool-row-state.ts +21 -1
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineTool,
|
|
3
|
+
type ContextEvent,
|
|
4
|
+
type ExtensionAPI,
|
|
5
|
+
type ExtensionContext,
|
|
6
|
+
type SessionEntry,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
replayContextPruningState,
|
|
10
|
+
setContextPruningEnabled,
|
|
11
|
+
type ContextPruneDetailsV1,
|
|
12
|
+
} from "../../shared/context-pruning-state.ts";
|
|
13
|
+
import { emitTauEvent, onTauEvent } from "../../shared/events.ts";
|
|
14
|
+
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
15
|
+
import { createToolRowStateStore } from "../../shared/tool-row-state.ts";
|
|
16
|
+
import { contextPruneParameters, executeContextPrune } from "./prune.ts";
|
|
17
|
+
import { projectContext } from "./projection.ts";
|
|
18
|
+
import {
|
|
19
|
+
parseContextPruningNudgeDetailsV1,
|
|
20
|
+
renderContextPruneCall,
|
|
21
|
+
renderContextPruneResult,
|
|
22
|
+
renderContextPruningNudge,
|
|
23
|
+
type ContextPruningNudgeDetailsV1,
|
|
24
|
+
} from "./render.ts";
|
|
25
|
+
import contextPruningSettings from "./settings.ts";
|
|
26
|
+
|
|
27
|
+
interface ProjectionCache {
|
|
28
|
+
generation: number;
|
|
29
|
+
input: ContextEvent["messages"];
|
|
30
|
+
stateKey: string;
|
|
31
|
+
messages: ContextEvent["messages"];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TOOL_DESCRIPTION =
|
|
35
|
+
"Create a context anchor after broad exploration converges, after a context-pressure nudge, or when substantial irrelevant tool evidence has accumulated. Do not use after routine tool batches or when expected savings are small. Immediately before calling, state retained conclusions, conditional relevance, and the next action in visible prose. Call context_prune alone in its tool batch.";
|
|
36
|
+
const NUDGE_MESSAGE_TYPE = "tau.context-pruning.nudge";
|
|
37
|
+
const NUDGE_BASELINE_ENTRY_TYPE = "tau.context-pruning.nudge-baseline";
|
|
38
|
+
|
|
39
|
+
interface NudgeState {
|
|
40
|
+
anchorToolCallId: string | undefined;
|
|
41
|
+
growthBaselinePercent: number | undefined;
|
|
42
|
+
highestBoundary: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function contextPruningExtension(pi: ExtensionAPI): void {
|
|
46
|
+
let enabled = false;
|
|
47
|
+
let lifecycleGeneration = 0;
|
|
48
|
+
let projectionCache: ProjectionCache | undefined;
|
|
49
|
+
let minimumReclaimTokens = contextPruningSettings.defaults.minimumReclaimTokens;
|
|
50
|
+
let nudgeEveryPercent = contextPruningSettings.defaults.nudgeEveryPercent;
|
|
51
|
+
let pressurePercent = contextPruningSettings.defaults.pressurePercent;
|
|
52
|
+
let toolRegistered = false;
|
|
53
|
+
let commandRegistered = false;
|
|
54
|
+
let visualRows = new Set<string>();
|
|
55
|
+
let nudgeState: NudgeState = {
|
|
56
|
+
anchorToolCallId: undefined,
|
|
57
|
+
growthBaselinePercent: 0,
|
|
58
|
+
highestBoundary: 0,
|
|
59
|
+
};
|
|
60
|
+
const rowState = createToolRowStateStore(pi, "context-pruning.tool-row-state");
|
|
61
|
+
pi.registerMessageRenderer<ContextPruningNudgeDetailsV1>(NUDGE_MESSAGE_TYPE, (message, _options, theme) =>
|
|
62
|
+
renderContextPruningNudge(message.details, theme),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const pushVisualSnapshot = () => {
|
|
66
|
+
emitTauEvent(pi, "tau:tool-row-state.snapshot", {
|
|
67
|
+
states: [...visualRows].map((rowId) => ({ rowId, state: "pruned" as const })),
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
onTauEvent(
|
|
71
|
+
pi,
|
|
72
|
+
"context-pruning.tool-row-state-producer",
|
|
73
|
+
"tau:tool-row-state.snapshot.requested",
|
|
74
|
+
pushVisualSnapshot,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const clearEphemeralState = () => {
|
|
78
|
+
lifecycleGeneration += 1;
|
|
79
|
+
projectionCache = undefined;
|
|
80
|
+
};
|
|
81
|
+
const setContextPruneToolActive = (active: boolean) => {
|
|
82
|
+
if (!toolRegistered) return;
|
|
83
|
+
const activeTools = pi.getActiveTools();
|
|
84
|
+
const currentlyActive = activeTools.includes("context_prune");
|
|
85
|
+
if (active === currentlyActive) return;
|
|
86
|
+
pi.setActiveTools(
|
|
87
|
+
active ? [...activeTools, "context_prune"] : activeTools.filter((toolName) => toolName !== "context_prune"),
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
const syncBranchState = (ctx: ExtensionContext) => {
|
|
91
|
+
const state = replayContextPruningState(ctx.sessionManager.getBranch(), enabled);
|
|
92
|
+
visualRows = new Set([...state.prunedToolCallIds, ...state.prunedAutoreadRowIds]);
|
|
93
|
+
nudgeState = reconstructNudgeState(ctx.sessionManager.getBranch(), state.latestAnchorToolCallId);
|
|
94
|
+
pushVisualSnapshot();
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
98
|
+
clearEphemeralState();
|
|
99
|
+
enabled = false;
|
|
100
|
+
setContextPruneToolActive(false);
|
|
101
|
+
visualRows.clear();
|
|
102
|
+
setContextPruningEnabled(false);
|
|
103
|
+
pushVisualSnapshot();
|
|
104
|
+
const generation = lifecycleGeneration;
|
|
105
|
+
const settings = await loadTauExtensionSettings(ctx, contextPruningSettings);
|
|
106
|
+
if (generation !== lifecycleGeneration) return;
|
|
107
|
+
enabled = settings.enabled;
|
|
108
|
+
minimumReclaimTokens = settings.minimumReclaimTokens;
|
|
109
|
+
nudgeEveryPercent = settings.nudgeEveryPercent;
|
|
110
|
+
pressurePercent = settings.pressurePercent;
|
|
111
|
+
setContextPruningEnabled(enabled);
|
|
112
|
+
if (enabled && !toolRegistered) {
|
|
113
|
+
pi.registerTool(
|
|
114
|
+
defineTool<typeof contextPruneParameters, ContextPruneDetailsV1>({
|
|
115
|
+
name: "context_prune",
|
|
116
|
+
label: "context_prune",
|
|
117
|
+
description: TOOL_DESCRIPTION,
|
|
118
|
+
promptSnippet:
|
|
119
|
+
"Prune substantial stale tool evidence after stating durable conclusions and the next action",
|
|
120
|
+
promptGuidelines: [
|
|
121
|
+
"Use context_prune after broad exploration converges, after a context-pressure nudge, or when substantial irrelevant evidence has accumulated.",
|
|
122
|
+
"Do not use context_prune after routine tool batches or when expected savings are small.",
|
|
123
|
+
"Immediately before context_prune, write visible prose with retained conclusions, conditional relevance, and the next action, then call context_prune alone in its tool batch.",
|
|
124
|
+
],
|
|
125
|
+
parameters: contextPruneParameters,
|
|
126
|
+
executionMode: "sequential",
|
|
127
|
+
async execute(toolCallId, params, signal, _onUpdate, executionContext) {
|
|
128
|
+
const cache = projectionCache;
|
|
129
|
+
return executeContextPrune({
|
|
130
|
+
pi,
|
|
131
|
+
toolCallId,
|
|
132
|
+
params,
|
|
133
|
+
signal,
|
|
134
|
+
ctx: executionContext,
|
|
135
|
+
projection: cache && { generation: cache.generation, messages: cache.messages },
|
|
136
|
+
currentGeneration: () => lifecycleGeneration,
|
|
137
|
+
currentEnabled: () => enabled,
|
|
138
|
+
minimumReclaimTokens,
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
renderCall(args, theme, context) {
|
|
142
|
+
return renderContextPruneCall(args, theme, {
|
|
143
|
+
rowState,
|
|
144
|
+
rowId: context.toolCallId,
|
|
145
|
+
invalidate: context.invalidate,
|
|
146
|
+
lastComponent: context.lastComponent,
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
renderResult(result, options, theme, context) {
|
|
150
|
+
return renderContextPruneResult(result, options.expanded, theme, context.lastComponent);
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
toolRegistered = true;
|
|
155
|
+
}
|
|
156
|
+
setContextPruneToolActive(enabled);
|
|
157
|
+
if (enabled && !commandRegistered) {
|
|
158
|
+
pi.registerCommand("prune", {
|
|
159
|
+
description: "Ask the agent to create a context-pruning anchor and continue its task",
|
|
160
|
+
async handler(args, commandContext) {
|
|
161
|
+
if (!enabled) {
|
|
162
|
+
commandContext.ui.notify("Context pruning is disabled.", "info");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (args.trim().length > 0) {
|
|
166
|
+
commandContext.ui.notify("Usage: /prune", "info");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const anchorToolCallId = replayContextPruningState(
|
|
170
|
+
commandContext.sessionManager.getBranch(),
|
|
171
|
+
true,
|
|
172
|
+
).latestAnchorToolCallId;
|
|
173
|
+
pi.sendMessage<ContextPruningNudgeDetailsV1>(
|
|
174
|
+
{
|
|
175
|
+
customType: NUDGE_MESSAGE_TYPE,
|
|
176
|
+
content: manualPruneSteeringMessage(),
|
|
177
|
+
display: true,
|
|
178
|
+
details: {
|
|
179
|
+
v: 1,
|
|
180
|
+
kind: "manual",
|
|
181
|
+
percent: null,
|
|
182
|
+
boundary: null,
|
|
183
|
+
pressure: false,
|
|
184
|
+
anchorToolCallId: anchorToolCallId ?? null,
|
|
185
|
+
growthBaselinePercent: null,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
commandRegistered = true;
|
|
193
|
+
}
|
|
194
|
+
syncBranchState(ctx);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
198
|
+
clearEphemeralState();
|
|
199
|
+
syncBranchState(ctx);
|
|
200
|
+
});
|
|
201
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
202
|
+
clearEphemeralState();
|
|
203
|
+
syncBranchState(ctx);
|
|
204
|
+
});
|
|
205
|
+
pi.on("session_shutdown", () => {
|
|
206
|
+
clearEphemeralState();
|
|
207
|
+
enabled = false;
|
|
208
|
+
setContextPruneToolActive(false);
|
|
209
|
+
visualRows.clear();
|
|
210
|
+
nudgeState = { anchorToolCallId: undefined, growthBaselinePercent: 0, highestBoundary: 0 };
|
|
211
|
+
setContextPruningEnabled(false);
|
|
212
|
+
pushVisualSnapshot();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
pi.on("turn_end", (event, ctx) => {
|
|
216
|
+
if (!enabled || event.toolResults.length === 0) return undefined;
|
|
217
|
+
const usage = ctx.getContextUsage();
|
|
218
|
+
if (!usage || usage.percent === null || !Number.isFinite(usage.percent)) return undefined;
|
|
219
|
+
const rawPercent = Math.max(0, Math.min(100, usage.percent));
|
|
220
|
+
const percent = Math.floor(rawPercent);
|
|
221
|
+
const activeAnchor = replayContextPruningState(ctx.sessionManager.getBranch(), true).latestAnchorToolCallId;
|
|
222
|
+
if (activeAnchor !== nudgeState.anchorToolCallId) {
|
|
223
|
+
nudgeState = reconstructNudgeState(ctx.sessionManager.getBranch(), activeAnchor);
|
|
224
|
+
}
|
|
225
|
+
if (activeAnchor !== undefined && nudgeState.growthBaselinePercent === undefined) {
|
|
226
|
+
const baselinePercent = Math.ceil(rawPercent);
|
|
227
|
+
pi.appendEntry(NUDGE_BASELINE_ENTRY_TYPE, {
|
|
228
|
+
v: 1,
|
|
229
|
+
anchorToolCallId: activeAnchor,
|
|
230
|
+
baselinePercent,
|
|
231
|
+
});
|
|
232
|
+
nudgeState.growthBaselinePercent = baselinePercent;
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
const baseline = nudgeState.growthBaselinePercent ?? 0;
|
|
236
|
+
if (percent - baseline < nudgeEveryPercent) return undefined;
|
|
237
|
+
const boundary = Math.floor(percent / nudgeEveryPercent) * nudgeEveryPercent;
|
|
238
|
+
if (boundary <= nudgeState.highestBoundary) return undefined;
|
|
239
|
+
const pressure = rawPercent > pressurePercent;
|
|
240
|
+
const details: ContextPruningNudgeDetailsV1 = {
|
|
241
|
+
v: 1,
|
|
242
|
+
kind: "automatic",
|
|
243
|
+
percent,
|
|
244
|
+
boundary,
|
|
245
|
+
pressure,
|
|
246
|
+
anchorToolCallId: activeAnchor ?? null,
|
|
247
|
+
growthBaselinePercent: baseline,
|
|
248
|
+
};
|
|
249
|
+
pi.sendMessage<ContextPruningNudgeDetailsV1>(
|
|
250
|
+
{
|
|
251
|
+
customType: NUDGE_MESSAGE_TYPE,
|
|
252
|
+
content: automaticPruneSteeringMessage(pressure),
|
|
253
|
+
display: true,
|
|
254
|
+
details,
|
|
255
|
+
},
|
|
256
|
+
{ deliverAs: "steer" },
|
|
257
|
+
);
|
|
258
|
+
nudgeState.highestBoundary = boundary;
|
|
259
|
+
return undefined;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
pi.on("context", (event, ctx) => {
|
|
263
|
+
if (!enabled) return undefined;
|
|
264
|
+
const state = replayContextPruningState(ctx.sessionManager.getBranch(), true);
|
|
265
|
+
const stateKey = JSON.stringify([
|
|
266
|
+
state.latestAnchorToolCallId,
|
|
267
|
+
[...state.prunedToolCallIds],
|
|
268
|
+
[...state.prunedAutoreadRowIds],
|
|
269
|
+
]);
|
|
270
|
+
if (
|
|
271
|
+
projectionCache?.generation === lifecycleGeneration &&
|
|
272
|
+
projectionCache.input === event.messages &&
|
|
273
|
+
projectionCache.stateKey === stateKey
|
|
274
|
+
) {
|
|
275
|
+
return { messages: projectionCache.messages };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const messages = projectContext(event.messages, state);
|
|
280
|
+
projectionCache = { generation: lifecycleGeneration, input: event.messages, stateKey, messages };
|
|
281
|
+
const nextRows = new Set([...state.prunedToolCallIds, ...state.prunedAutoreadRowIds]);
|
|
282
|
+
if (!setsEqual(visualRows, nextRows)) {
|
|
283
|
+
visualRows = nextRows;
|
|
284
|
+
pushVisualSnapshot();
|
|
285
|
+
}
|
|
286
|
+
return { messages };
|
|
287
|
+
} catch {
|
|
288
|
+
projectionCache = undefined;
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function setsEqual(left: ReadonlySet<string>, right: ReadonlySet<string>): boolean {
|
|
295
|
+
if (left.size !== right.size) return false;
|
|
296
|
+
for (const item of left) if (!right.has(item)) return false;
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function reconstructNudgeState(branch: readonly SessionEntry[], anchorToolCallId: string | undefined): NudgeState {
|
|
301
|
+
let growthBaselinePercent = anchorToolCallId === undefined ? 0 : undefined;
|
|
302
|
+
let highestBoundary = 0;
|
|
303
|
+
let anchorResultIndex = -1;
|
|
304
|
+
if (anchorToolCallId !== undefined) {
|
|
305
|
+
anchorResultIndex = branch.findIndex(
|
|
306
|
+
(entry) =>
|
|
307
|
+
entry.type === "message" &&
|
|
308
|
+
entry.message.role === "toolResult" &&
|
|
309
|
+
entry.message.toolName === "context_prune" &&
|
|
310
|
+
entry.message.toolCallId === anchorToolCallId,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
for (let index = 0; index < branch.length; index += 1) {
|
|
314
|
+
const entry = branch[index];
|
|
315
|
+
if (!entry) continue;
|
|
316
|
+
if (entry.type === "custom" && entry.customType === NUDGE_BASELINE_ENTRY_TYPE) {
|
|
317
|
+
const baseline = parseNudgeBaseline(entry.data);
|
|
318
|
+
if (
|
|
319
|
+
baseline &&
|
|
320
|
+
growthBaselinePercent === undefined &&
|
|
321
|
+
index > anchorResultIndex &&
|
|
322
|
+
baseline.anchorToolCallId === anchorToolCallId
|
|
323
|
+
) {
|
|
324
|
+
growthBaselinePercent = baseline.baselinePercent;
|
|
325
|
+
}
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (entry.type !== "custom_message" || entry.customType !== NUDGE_MESSAGE_TYPE) continue;
|
|
329
|
+
const details = parseContextPruningNudgeDetailsV1(entry.details);
|
|
330
|
+
if (
|
|
331
|
+
!details ||
|
|
332
|
+
details.kind !== "automatic" ||
|
|
333
|
+
details.anchorToolCallId !== (anchorToolCallId ?? null) ||
|
|
334
|
+
details.boundary === null ||
|
|
335
|
+
details.growthBaselinePercent === null
|
|
336
|
+
)
|
|
337
|
+
continue;
|
|
338
|
+
highestBoundary = Math.max(highestBoundary, details.boundary);
|
|
339
|
+
growthBaselinePercent = details.growthBaselinePercent;
|
|
340
|
+
}
|
|
341
|
+
return { anchorToolCallId, growthBaselinePercent, highestBoundary };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function parseNudgeBaseline(value: unknown): { v: 1; anchorToolCallId: string; baselinePercent: number } | undefined {
|
|
345
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
346
|
+
const record = value as Record<string, unknown>;
|
|
347
|
+
if (
|
|
348
|
+
Object.keys(record).length !== 3 ||
|
|
349
|
+
!Object.hasOwn(record, "v") ||
|
|
350
|
+
!Object.hasOwn(record, "anchorToolCallId") ||
|
|
351
|
+
!Object.hasOwn(record, "baselinePercent") ||
|
|
352
|
+
record.v !== 1 ||
|
|
353
|
+
typeof record.anchorToolCallId !== "string" ||
|
|
354
|
+
record.anchorToolCallId.length === 0 ||
|
|
355
|
+
typeof record.baselinePercent !== "number" ||
|
|
356
|
+
!Number.isInteger(record.baselinePercent) ||
|
|
357
|
+
record.baselinePercent < 0 ||
|
|
358
|
+
record.baselinePercent > 100
|
|
359
|
+
)
|
|
360
|
+
return undefined;
|
|
361
|
+
return {
|
|
362
|
+
v: 1,
|
|
363
|
+
anchorToolCallId: record.anchorToolCallId,
|
|
364
|
+
baselinePercent: record.baselinePercent,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function automaticPruneSteeringMessage(pressure: boolean): string {
|
|
369
|
+
const silent =
|
|
370
|
+
"Internal context-management instruction. Follow it silently. Do not mention or acknowledge context percentages, prune messages, or internal context management.";
|
|
371
|
+
if (pressure) {
|
|
372
|
+
return `${silent} Finish the current coherent step. When projected savings are substantial, preserve durable conclusions and the next action in visible prose, then call context_prune alone. Do not prune after a routine tool batch or retry immediately after a skipped prune.`;
|
|
373
|
+
}
|
|
374
|
+
return `${silent} No prune is required unless broad exploration has converged or substantial evidence is already irrelevant. Continue coherent work. If pruning becomes worthwhile, preserve durable conclusions and the next action in visible prose, then call context_prune alone.`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function manualPruneSteeringMessage(): string {
|
|
378
|
+
return "Internal context-management instruction. Follow it silently without mentioning this request. Create a context anchor with context_prune, then continue unfinished work. First preserve durable conclusions, conditional relevance, and the next action in visible prose. Call context_prune alone. If projected savings are too small, continue work without immediately retrying.";
|
|
379
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ContextEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ActiveContextPruningState } from "../../shared/context-pruning-state.ts";
|
|
3
|
+
|
|
4
|
+
type ContextMessage = ContextEvent["messages"][number];
|
|
5
|
+
|
|
6
|
+
const NUDGE_MESSAGE_TYPE = "tau.context-pruning.nudge";
|
|
7
|
+
const DEFERRED_MESSAGE_TYPE = "tau.context-pruning.deferred";
|
|
8
|
+
|
|
9
|
+
export function projectContext(
|
|
10
|
+
messages: readonly ContextMessage[],
|
|
11
|
+
state: ActiveContextPruningState,
|
|
12
|
+
): ContextMessage[] {
|
|
13
|
+
const inputPairs = indexToolPairs(messages);
|
|
14
|
+
const anchorBoundary = visibleAnchorBoundary(messages, state.latestAnchorToolCallId, inputPairs);
|
|
15
|
+
const prunedToolCallIds = state.latestAnchorToolCallId === undefined ? new Set<string>() : state.prunedToolCallIds;
|
|
16
|
+
const prunedAutoreadRowIds =
|
|
17
|
+
state.latestAnchorToolCallId === undefined ? new Set<string>() : state.prunedAutoreadRowIds;
|
|
18
|
+
const projected: ContextMessage[] = [];
|
|
19
|
+
|
|
20
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
21
|
+
const message = messages[index];
|
|
22
|
+
if (message.role === "toolResult" && prunedToolCallIds.has(message.toolCallId)) continue;
|
|
23
|
+
if (
|
|
24
|
+
message.role === "custom" &&
|
|
25
|
+
message.customType === "tau.autoread" &&
|
|
26
|
+
isPrunedAutoread(message.details, prunedAutoreadRowIds)
|
|
27
|
+
)
|
|
28
|
+
continue;
|
|
29
|
+
if (
|
|
30
|
+
anchorBoundary !== undefined &&
|
|
31
|
+
index <= anchorBoundary &&
|
|
32
|
+
message.role === "custom" &&
|
|
33
|
+
(message.customType === NUDGE_MESSAGE_TYPE || message.customType === DEFERRED_MESSAGE_TYPE)
|
|
34
|
+
)
|
|
35
|
+
continue;
|
|
36
|
+
if (message.role !== "assistant") {
|
|
37
|
+
projected.push(message);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const removeThinking = anchorBoundary !== undefined && index <= anchorBoundary;
|
|
42
|
+
let changed = false;
|
|
43
|
+
const content = message.content.filter((block) => {
|
|
44
|
+
const remove =
|
|
45
|
+
(removeThinking && block.type === "thinking") ||
|
|
46
|
+
(block.type === "toolCall" && prunedToolCallIds.has(block.id));
|
|
47
|
+
if (remove) changed = true;
|
|
48
|
+
return !remove;
|
|
49
|
+
});
|
|
50
|
+
if (content.length === 0) continue;
|
|
51
|
+
projected.push(changed ? { ...message, content } : message);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
indexToolPairs(projected);
|
|
55
|
+
return projected;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isPrunedAutoread(value: unknown, prunedRowIds: ReadonlySet<string>): boolean {
|
|
59
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
60
|
+
const rowId = (value as Record<string, unknown>).rowId;
|
|
61
|
+
return typeof rowId === "string" && prunedRowIds.has(rowId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function visibleAnchorBoundary(
|
|
65
|
+
messages: readonly ContextMessage[],
|
|
66
|
+
anchorToolCallId: string | undefined,
|
|
67
|
+
pairs: ReadonlyMap<string, { callIndex: number; resultIndex: number }>,
|
|
68
|
+
): number | undefined {
|
|
69
|
+
if (anchorToolCallId === undefined) return undefined;
|
|
70
|
+
const pair = pairs.get(anchorToolCallId);
|
|
71
|
+
if (!pair) return undefined;
|
|
72
|
+
const call = messages[pair.callIndex];
|
|
73
|
+
const result = messages[pair.resultIndex];
|
|
74
|
+
if (call?.role !== "assistant" || result?.role !== "toolResult" || result.toolName !== "context_prune")
|
|
75
|
+
return undefined;
|
|
76
|
+
const block = call.content.find((item) => item.type === "toolCall" && item.id === anchorToolCallId);
|
|
77
|
+
return block?.type === "toolCall" && block.name === "context_prune" ? pair.resultIndex : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function indexToolPairs(messages: readonly ContextMessage[]): Map<string, { callIndex: number; resultIndex: number }> {
|
|
81
|
+
const calls = new Map<string, number>();
|
|
82
|
+
const results = new Map<string, number>();
|
|
83
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
84
|
+
const message = messages[index];
|
|
85
|
+
if (message.role === "assistant") {
|
|
86
|
+
for (const block of message.content) {
|
|
87
|
+
if (block.type !== "toolCall") continue;
|
|
88
|
+
if (calls.has(block.id)) throw new Error(`Duplicate tool call in projected context: ${block.id}`);
|
|
89
|
+
calls.set(block.id, index);
|
|
90
|
+
}
|
|
91
|
+
} else if (message.role === "toolResult") {
|
|
92
|
+
if (results.has(message.toolCallId))
|
|
93
|
+
throw new Error(`Duplicate tool result in projected context: ${message.toolCallId}`);
|
|
94
|
+
results.set(message.toolCallId, index);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const pairs = new Map<string, { callIndex: number; resultIndex: number }>();
|
|
99
|
+
for (const [id, callIndex] of calls) {
|
|
100
|
+
const resultIndex = results.get(id);
|
|
101
|
+
if (resultIndex === undefined) throw new Error(`Orphaned tool call in projected context: ${id}`);
|
|
102
|
+
pairs.set(id, { callIndex, resultIndex });
|
|
103
|
+
}
|
|
104
|
+
for (const id of results.keys()) {
|
|
105
|
+
if (!calls.has(id)) throw new Error(`Orphaned tool result in projected context: ${id}`);
|
|
106
|
+
}
|
|
107
|
+
return pairs;
|
|
108
|
+
}
|