@shanepadgett/tau-agent 0.28.1 → 0.29.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 +1 -1
- package/extensions/cache-diagnostics/index.ts +1 -1
- package/extensions/explore/README.md +1 -1
- package/extensions/explore/ast/read/hook.ts +2 -34
- package/extensions/explore/ast/read/policy.ts +0 -32
- package/extensions/explore/index.ts +2 -0
- package/extensions/explore/outline-injection.ts +151 -0
- package/extensions/explore/settings.ts +0 -8
- package/extensions/runtime-context/README.md +1 -1
- package/extensions/runtime-context/index.ts +3 -66
- package/extensions/subagent/agents/review.md +25 -18
- package/extensions/subagent/agents/scout.md +10 -0
- package/extensions/tau-help/help.md +2 -2
- package/extensions/working-memory/README.md +9 -0
- package/extensions/working-memory/checkpoint.ts +175 -0
- package/extensions/working-memory/index.ts +320 -0
- package/extensions/working-memory/memory.ts +248 -0
- package/extensions/working-memory/render.ts +178 -0
- package/extensions/working-memory/settings.ts +38 -0
- package/extensions/working-memory/state.ts +152 -0
- package/package.json +2 -2
- package/schemas/tau.schema.json +33 -39
- package/shared/events.ts +5 -0
- package/shared/full-file-knowledge.ts +0 -1
- package/shared/outline-injection.ts +56 -0
- package/extensions/context-pruning/README.md +0 -39
- package/extensions/context-pruning/index.ts +0 -382
- package/extensions/context-pruning/projection.ts +0 -60
- package/extensions/context-pruning/prune.ts +0 -199
- package/extensions/context-pruning/render.ts +0 -251
- package/extensions/context-pruning/settings.ts +0 -39
- package/shared/context-pruning-state.ts +0 -152
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import { type ExtensionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { type Static, Type } from "typebox";
|
|
4
|
-
import type { ContextPruneDeferredFileV2, ContextPruneDetailsV2 } from "../../shared/context-pruning-state.ts";
|
|
5
|
-
import { prepareAutoreadMessage, type PreparedAutoreadMessage } from "../../shared/autoread.ts";
|
|
6
|
-
import { MAX_COMPLETE_FILE_SNAPSHOT_BYTES } from "../../shared/full-file-knowledge.ts";
|
|
7
|
-
|
|
8
|
-
export const contextPruneParameters = Type.Object(
|
|
9
|
-
{
|
|
10
|
-
keepFiles: Type.Array(
|
|
11
|
-
Type.Object({ path: Type.String(), relevance: Type.String() }, { additionalProperties: false }),
|
|
12
|
-
),
|
|
13
|
-
keepToolCalls: Type.Array(
|
|
14
|
-
Type.Object({ toolCallId: Type.String(), relevance: Type.String() }, { additionalProperties: false }),
|
|
15
|
-
),
|
|
16
|
-
deferFiles: Type.Array(
|
|
17
|
-
Type.Object(
|
|
18
|
-
{ path: Type.String(), reason: Type.String(), relevantWhen: Type.String() },
|
|
19
|
-
{ additionalProperties: false },
|
|
20
|
-
),
|
|
21
|
-
),
|
|
22
|
-
},
|
|
23
|
-
{ additionalProperties: false },
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
export type ContextPruneInput = Static<typeof contextPruneParameters>;
|
|
27
|
-
|
|
28
|
-
interface ContextPruneExecutionOptions {
|
|
29
|
-
toolCallId: string;
|
|
30
|
-
params: ContextPruneInput;
|
|
31
|
-
signal: AbortSignal | undefined;
|
|
32
|
-
ctx: ExtensionContext;
|
|
33
|
-
generation: number;
|
|
34
|
-
currentGeneration: () => number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface ContextPruneToolResult {
|
|
38
|
-
content: Array<{ type: "text"; text: string }>;
|
|
39
|
-
details: ContextPruneDetailsV2;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface ContextPruneExecutionResult {
|
|
43
|
-
result: ContextPruneToolResult;
|
|
44
|
-
autoreads: PreparedAutoreadMessage[];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function executeContextPrune(options: ContextPruneExecutionOptions): Promise<ContextPruneExecutionResult> {
|
|
48
|
-
assertCurrent(options);
|
|
49
|
-
const retainedToolCallIds = [...new Set(options.params.keepToolCalls.map((selection) => selection.toolCallId))];
|
|
50
|
-
const retainedTools = new Set(retainedToolCallIds);
|
|
51
|
-
const { prunedToolCallIds, prunedAutoreadRowIds } = collectPrunedRows(
|
|
52
|
-
options.ctx.sessionManager.getBranch(),
|
|
53
|
-
options.toolCallId,
|
|
54
|
-
retainedTools,
|
|
55
|
-
);
|
|
56
|
-
const warnings: string[] = [];
|
|
57
|
-
const preparedSnapshots: PreparedAutoreadMessage[] = [];
|
|
58
|
-
const keptPaths = new Set<string>();
|
|
59
|
-
for (const selection of options.params.keepFiles) {
|
|
60
|
-
assertCurrent(options);
|
|
61
|
-
const path = normalizePath(selection.path);
|
|
62
|
-
const pathKey = resolve(options.ctx.cwd, path);
|
|
63
|
-
if (keptPaths.has(pathKey)) continue;
|
|
64
|
-
keptPaths.add(pathKey);
|
|
65
|
-
try {
|
|
66
|
-
preparedSnapshots.push(
|
|
67
|
-
await prepareAutoreadMessage({
|
|
68
|
-
rowId: `${options.toolCallId}:${preparedSnapshots.length}`,
|
|
69
|
-
path,
|
|
70
|
-
cwd: options.ctx.cwd,
|
|
71
|
-
source: "context-pruning",
|
|
72
|
-
batchId: options.toolCallId,
|
|
73
|
-
signal: options.signal,
|
|
74
|
-
isLifecycleCurrent: () => options.generation === options.currentGeneration(),
|
|
75
|
-
maximumBytes: MAX_COMPLETE_FILE_SNAPSHOT_BYTES,
|
|
76
|
-
}),
|
|
77
|
-
);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
if (options.signal?.aborted || options.generation !== options.currentGeneration()) throw error;
|
|
80
|
-
warnings.push(`${path}: ${errorMessage(error)}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const deferredFiles: ContextPruneDeferredFileV2[] = [];
|
|
85
|
-
const deferredPaths = new Set<string>();
|
|
86
|
-
for (const selection of options.params.deferFiles) {
|
|
87
|
-
const path = normalizePath(selection.path);
|
|
88
|
-
const pathKey = resolve(options.ctx.cwd, path);
|
|
89
|
-
if (keptPaths.has(pathKey) || deferredPaths.has(pathKey)) continue;
|
|
90
|
-
deferredPaths.add(pathKey);
|
|
91
|
-
deferredFiles.push({ path, reason: selection.reason, relevantWhen: selection.relevantWhen });
|
|
92
|
-
}
|
|
93
|
-
assertCurrent(options);
|
|
94
|
-
|
|
95
|
-
const retainedAutoreadRowIds = preparedSnapshots.map((snapshot) => snapshot.details.rowId);
|
|
96
|
-
const refreshedFiles = preparedSnapshots.map((snapshot) => ({
|
|
97
|
-
path: snapshot.details.path,
|
|
98
|
-
rowId: snapshot.details.rowId,
|
|
99
|
-
}));
|
|
100
|
-
const details: ContextPruneDetailsV2 = {
|
|
101
|
-
v: 2,
|
|
102
|
-
anchorToolCallId: options.toolCallId,
|
|
103
|
-
prunedToolCallIds,
|
|
104
|
-
prunedAutoreadRowIds,
|
|
105
|
-
retainedToolCallIds,
|
|
106
|
-
retainedAutoreadRowIds,
|
|
107
|
-
refreshedFiles,
|
|
108
|
-
deferredFiles,
|
|
109
|
-
warnings,
|
|
110
|
-
};
|
|
111
|
-
const status =
|
|
112
|
-
warnings.length === 0
|
|
113
|
-
? "Context checkpoint applied. Continue with the next action stated before this call."
|
|
114
|
-
: `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.`;
|
|
115
|
-
return {
|
|
116
|
-
result: {
|
|
117
|
-
content: [
|
|
118
|
-
{ type: "text", text: status },
|
|
119
|
-
...(deferredFiles.length === 0 ? [] : [{ type: "text" as const, text: deferredFileText(deferredFiles) }]),
|
|
120
|
-
],
|
|
121
|
-
details,
|
|
122
|
-
},
|
|
123
|
-
autoreads: preparedSnapshots,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function collectPrunedRows(
|
|
128
|
-
branch: readonly SessionEntry[],
|
|
129
|
-
anchorToolCallId: string,
|
|
130
|
-
retainedToolCallIds: ReadonlySet<string>,
|
|
131
|
-
): { prunedToolCallIds: string[]; prunedAutoreadRowIds: string[] } {
|
|
132
|
-
let anchorIndex = -1;
|
|
133
|
-
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
134
|
-
const entry = branch[index];
|
|
135
|
-
if (
|
|
136
|
-
entry?.type === "message" &&
|
|
137
|
-
entry.message.role === "assistant" &&
|
|
138
|
-
entry.message.content.some(
|
|
139
|
-
(block) => block.type === "toolCall" && block.id === anchorToolCallId && block.name === "context_prune",
|
|
140
|
-
)
|
|
141
|
-
) {
|
|
142
|
-
anchorIndex = index;
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
if (anchorIndex < 0) return { prunedToolCallIds: [], prunedAutoreadRowIds: [] };
|
|
147
|
-
const prunedToolCallIds = new Set<string>();
|
|
148
|
-
const prunedAutoreadRowIds = new Set<string>();
|
|
149
|
-
for (let index = 0; index < anchorIndex; index += 1) {
|
|
150
|
-
const entry = branch[index];
|
|
151
|
-
if (entry?.type === "message" && entry.message.role === "assistant") {
|
|
152
|
-
for (const block of entry.message.content) {
|
|
153
|
-
if (block.type === "toolCall" && !retainedToolCallIds.has(block.id)) prunedToolCallIds.add(block.id);
|
|
154
|
-
}
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (entry?.type !== "custom_message" || entry.customType !== "tau.autoread") continue;
|
|
158
|
-
if (isRecord(entry.details) && typeof entry.details.rowId === "string") {
|
|
159
|
-
prunedAutoreadRowIds.add(entry.details.rowId);
|
|
160
|
-
}
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
for (let index = 0; index < anchorIndex; index += 1) {
|
|
164
|
-
const entry = branch[index];
|
|
165
|
-
if (entry?.type !== "message" || entry.message.role !== "toolResult") continue;
|
|
166
|
-
if (entry.message.toolName !== "context_prune" || !isRecord(entry.message.details)) continue;
|
|
167
|
-
const files = entry.message.details.refreshedFiles;
|
|
168
|
-
if (!Array.isArray(files)) continue;
|
|
169
|
-
for (const file of files)
|
|
170
|
-
if (isRecord(file) && typeof file.rowId === "string") prunedAutoreadRowIds.add(file.rowId);
|
|
171
|
-
}
|
|
172
|
-
return { prunedToolCallIds: [...prunedToolCallIds], prunedAutoreadRowIds: [...prunedAutoreadRowIds] };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function deferredFileText(files: readonly ContextPruneDeferredFileV2[]): string {
|
|
176
|
-
return [
|
|
177
|
-
"Deferred files are advisory. Reconsider them only when their condition applies:",
|
|
178
|
-
...files.map((file) => `- ${file.path}: ${file.reason} Relevant when: ${file.relevantWhen}`),
|
|
179
|
-
].join("\n");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function normalizePath(path: string): string {
|
|
183
|
-
return path.replace(/^@/, "");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function assertCurrent(options: ContextPruneExecutionOptions): void {
|
|
187
|
-
options.signal?.throwIfAborted();
|
|
188
|
-
if (options.generation !== options.currentGeneration()) {
|
|
189
|
-
throw new Error("Context checkpoint crossed a session lifecycle boundary");
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function errorMessage(error: unknown): string {
|
|
194
|
-
return error instanceof Error ? error.message : String(error);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
198
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
199
|
-
}
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import type { AgentToolResult, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import { Marker } from "@shanepadgett/tau-tui";
|
|
4
|
-
import { parseContextPruneDetailsV2 } from "../../shared/context-pruning-state.ts";
|
|
5
|
-
import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.ts";
|
|
6
|
-
import type { ContextPruneInput } from "./prune.ts";
|
|
7
|
-
|
|
8
|
-
const MAX_EXPANDED_ITEMS = 20;
|
|
9
|
-
const MAX_EXPANDED_LINE_CHARACTERS = 240;
|
|
10
|
-
const MAX_EXPANDED_TEXT_CHARACTERS = 3_000;
|
|
11
|
-
const MAX_WARNING_CHARACTERS = 1_000;
|
|
12
|
-
|
|
13
|
-
export type ContextPruningNudgeDetailsV3 =
|
|
14
|
-
| {
|
|
15
|
-
v: 3;
|
|
16
|
-
kind: "automatic";
|
|
17
|
-
tokens: number;
|
|
18
|
-
boundaryTokens: number;
|
|
19
|
-
reminder: number;
|
|
20
|
-
tier: number;
|
|
21
|
-
tierCount: number;
|
|
22
|
-
tierFloor: number;
|
|
23
|
-
anchorToolCallId: string | null;
|
|
24
|
-
suppressedThroughTokens: number;
|
|
25
|
-
}
|
|
26
|
-
| {
|
|
27
|
-
v: 3;
|
|
28
|
-
kind: "manual";
|
|
29
|
-
tokens: null;
|
|
30
|
-
boundaryTokens: null;
|
|
31
|
-
reminder: null;
|
|
32
|
-
tier: null;
|
|
33
|
-
tierCount: null;
|
|
34
|
-
tierFloor: null;
|
|
35
|
-
anchorToolCallId: string | null;
|
|
36
|
-
suppressedThroughTokens: null;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export function parseContextPruningNudgeDetailsV3(value: unknown): ContextPruningNudgeDetailsV3 | undefined {
|
|
40
|
-
if (!isRecord(value)) return undefined;
|
|
41
|
-
const keys = [
|
|
42
|
-
"v",
|
|
43
|
-
"kind",
|
|
44
|
-
"tokens",
|
|
45
|
-
"boundaryTokens",
|
|
46
|
-
"reminder",
|
|
47
|
-
"tier",
|
|
48
|
-
"tierCount",
|
|
49
|
-
"tierFloor",
|
|
50
|
-
"anchorToolCallId",
|
|
51
|
-
"suppressedThroughTokens",
|
|
52
|
-
];
|
|
53
|
-
if (Object.keys(value).length !== keys.length || !keys.every((key) => Object.hasOwn(value, key))) return undefined;
|
|
54
|
-
if (value.v !== 3 || (value.kind !== "automatic" && value.kind !== "manual")) return undefined;
|
|
55
|
-
if (
|
|
56
|
-
value.anchorToolCallId !== null &&
|
|
57
|
-
(typeof value.anchorToolCallId !== "string" || value.anchorToolCallId.length === 0)
|
|
58
|
-
) {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
if (value.kind === "manual") {
|
|
62
|
-
if (
|
|
63
|
-
value.tokens !== null ||
|
|
64
|
-
value.boundaryTokens !== null ||
|
|
65
|
-
value.reminder !== null ||
|
|
66
|
-
value.tier !== null ||
|
|
67
|
-
value.tierCount !== null ||
|
|
68
|
-
value.tierFloor !== null ||
|
|
69
|
-
value.suppressedThroughTokens !== null
|
|
70
|
-
)
|
|
71
|
-
return undefined;
|
|
72
|
-
return {
|
|
73
|
-
v: 3,
|
|
74
|
-
kind: "manual",
|
|
75
|
-
tokens: null,
|
|
76
|
-
boundaryTokens: null,
|
|
77
|
-
reminder: null,
|
|
78
|
-
tier: null,
|
|
79
|
-
tierCount: null,
|
|
80
|
-
tierFloor: null,
|
|
81
|
-
anchorToolCallId: value.anchorToolCallId,
|
|
82
|
-
suppressedThroughTokens: null,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
if (
|
|
86
|
-
!isTokenCount(value.tokens) ||
|
|
87
|
-
!isBoundary(value.boundaryTokens) ||
|
|
88
|
-
!isReminder(value.reminder) ||
|
|
89
|
-
!isTier(value.tier) ||
|
|
90
|
-
!isTier(value.tierCount) ||
|
|
91
|
-
!isTierFloor(value.tierFloor) ||
|
|
92
|
-
!isTokenCount(value.suppressedThroughTokens) ||
|
|
93
|
-
value.boundaryTokens > value.tokens ||
|
|
94
|
-
value.boundaryTokens <= value.suppressedThroughTokens ||
|
|
95
|
-
value.tier > value.tierCount ||
|
|
96
|
-
value.tierFloor > value.tierCount ||
|
|
97
|
-
value.tier !== Math.max(Math.min(value.reminder, value.tierCount), value.tierFloor) ||
|
|
98
|
-
(value.anchorToolCallId === null && value.suppressedThroughTokens !== 0)
|
|
99
|
-
) {
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
const interval = value.boundaryTokens / value.reminder;
|
|
103
|
-
if (
|
|
104
|
-
!Number.isSafeInteger(interval) ||
|
|
105
|
-
interval < 1 ||
|
|
106
|
-
value.tokens - value.boundaryTokens >= interval ||
|
|
107
|
-
value.suppressedThroughTokens % interval !== 0
|
|
108
|
-
)
|
|
109
|
-
return undefined;
|
|
110
|
-
return {
|
|
111
|
-
v: 3,
|
|
112
|
-
kind: "automatic",
|
|
113
|
-
tokens: value.tokens,
|
|
114
|
-
boundaryTokens: value.boundaryTokens,
|
|
115
|
-
reminder: value.reminder,
|
|
116
|
-
tier: value.tier,
|
|
117
|
-
tierCount: value.tierCount,
|
|
118
|
-
tierFloor: value.tierFloor,
|
|
119
|
-
anchorToolCallId: value.anchorToolCallId,
|
|
120
|
-
suppressedThroughTokens: value.suppressedThroughTokens,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function renderContextPruningNudge(details: unknown, theme: Theme): Marker | undefined {
|
|
125
|
-
const parsed = parseContextPruningNudgeDetailsV3(details);
|
|
126
|
-
if (!parsed) return undefined;
|
|
127
|
-
return new Marker({
|
|
128
|
-
theme,
|
|
129
|
-
state: "muted",
|
|
130
|
-
label: "Context:",
|
|
131
|
-
parts:
|
|
132
|
-
parsed.kind === "manual"
|
|
133
|
-
? ["Prune requested."]
|
|
134
|
-
: [
|
|
135
|
-
formatTokens(parsed.tokens),
|
|
136
|
-
...(parsed.tier === parsed.tierCount ? ["Prune now."] : parsed.tier > 1 ? ["Prune soon."] : []),
|
|
137
|
-
],
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function renderContextPruneCall(
|
|
142
|
-
args: ContextPruneInput,
|
|
143
|
-
theme: Theme,
|
|
144
|
-
context: {
|
|
145
|
-
rowState: ToolRowStateStore;
|
|
146
|
-
rowId: string;
|
|
147
|
-
invalidate: () => void;
|
|
148
|
-
lastComponent: unknown;
|
|
149
|
-
},
|
|
150
|
-
): Text {
|
|
151
|
-
context.rowState.watch(context.rowId, context.invalidate);
|
|
152
|
-
const component = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
153
|
-
const count =
|
|
154
|
-
(Array.isArray(args.keepFiles) ? args.keepFiles.length : 0) +
|
|
155
|
-
(Array.isArray(args.keepToolCalls) ? args.keepToolCalls.length : 0) +
|
|
156
|
-
(Array.isArray(args.deferFiles) ? args.deferFiles.length : 0);
|
|
157
|
-
component.setText(
|
|
158
|
-
`${formatToolRowTitle(context.rowState, context.rowId, "context_prune", theme)} ${theme.fg("muted", `${count} selection${count === 1 ? "" : "s"}`)}`,
|
|
159
|
-
);
|
|
160
|
-
return component;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function renderContextPruneResult(
|
|
164
|
-
result: AgentToolResult<unknown>,
|
|
165
|
-
expanded: boolean,
|
|
166
|
-
theme: Theme,
|
|
167
|
-
lastComponent: unknown,
|
|
168
|
-
): Text {
|
|
169
|
-
const component = lastComponent instanceof Text ? lastComponent : new Text("", 0, 0);
|
|
170
|
-
const details = parseContextPruneDetailsV2(result.details);
|
|
171
|
-
if (!details) {
|
|
172
|
-
component.setText(
|
|
173
|
-
theme.fg(
|
|
174
|
-
"warning",
|
|
175
|
-
boundedText(firstResultText(result) || "context_prune returned invalid details", MAX_WARNING_CHARACTERS),
|
|
176
|
-
),
|
|
177
|
-
);
|
|
178
|
-
return component;
|
|
179
|
-
}
|
|
180
|
-
const pruned = details.prunedToolCallIds.length + details.prunedAutoreadRowIds.length;
|
|
181
|
-
const retained = details.retainedToolCallIds.length + details.retainedAutoreadRowIds.length;
|
|
182
|
-
let text = theme.fg(
|
|
183
|
-
details.warnings.length === 0 ? "success" : "warning",
|
|
184
|
-
`Checkpoint · pruned ${pruned} · retained ${retained} · refreshed ${details.refreshedFiles.length} · deferred ${details.deferredFiles.length}${details.warnings.length === 0 ? "" : ` · warnings ${details.warnings.length}`}`,
|
|
185
|
-
);
|
|
186
|
-
if (expanded) {
|
|
187
|
-
const lines = [
|
|
188
|
-
...details.prunedToolCallIds.map((id) => `pruned tool: ${id}`),
|
|
189
|
-
...details.prunedAutoreadRowIds.map((id) => `pruned autoread: ${id}`),
|
|
190
|
-
...details.retainedToolCallIds.map((id) => `retained tool: ${id}`),
|
|
191
|
-
...details.retainedAutoreadRowIds.map((id) => `retained autoread: ${id}`),
|
|
192
|
-
...details.refreshedFiles.map((file) => `refreshed: ${file.path} (${file.rowId})`),
|
|
193
|
-
...details.deferredFiles.map((file) => `deferred: ${file.path} — ${file.reason}; when ${file.relevantWhen}`),
|
|
194
|
-
...details.warnings.map((warning) => `warning: ${warning}`),
|
|
195
|
-
];
|
|
196
|
-
const shown = lines.slice(0, MAX_EXPANDED_ITEMS).map((line) => boundedText(line, MAX_EXPANDED_LINE_CHARACTERS));
|
|
197
|
-
if (lines.length > shown.length) shown.push(`… ${lines.length - shown.length} more`);
|
|
198
|
-
let remainingWidth = Math.max(0, MAX_EXPANDED_TEXT_CHARACTERS - visibleWidth(text));
|
|
199
|
-
for (const line of shown) {
|
|
200
|
-
if (remainingWidth <= 1) break;
|
|
201
|
-
const styled = theme.fg("dim", line);
|
|
202
|
-
const availableWidth = remainingWidth - 1;
|
|
203
|
-
text += `\n${truncateToWidth(styled, availableWidth, "…")}`;
|
|
204
|
-
remainingWidth -= 1 + Math.min(visibleWidth(styled), availableWidth);
|
|
205
|
-
if (visibleWidth(styled) > availableWidth) break;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
component.setText(text);
|
|
209
|
-
return component;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function boundedText(text: string, maximumCharacters: number): string {
|
|
213
|
-
if (text.length <= maximumCharacters) return text;
|
|
214
|
-
return `${text.slice(0, Math.max(0, maximumCharacters - 1))}…`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function firstResultText(result: AgentToolResult<unknown>): string {
|
|
218
|
-
for (const part of result.content) if (part.type === "text") return part.text;
|
|
219
|
-
return "";
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function formatTokens(count: number): string {
|
|
223
|
-
if (count < 1_000) return `${count}`;
|
|
224
|
-
if (count < 10_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
225
|
-
if (count < 1_000_000) return `${Math.round(count / 1_000)}k`;
|
|
226
|
-
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function isTokenCount(value: unknown): value is number {
|
|
230
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function isBoundary(value: unknown): value is number {
|
|
234
|
-
return isTokenCount(value) && value > 0;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function isReminder(value: unknown): value is number {
|
|
238
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function isTier(value: unknown): value is number {
|
|
242
|
-
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 5;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function isTierFloor(value: unknown): value is number {
|
|
246
|
-
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 5;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
250
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
251
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Type } from "typebox";
|
|
2
|
-
import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
|
|
3
|
-
|
|
4
|
-
const DEFAULT_NUDGE_INSTRUCTIONS: [string, ...string[]] = [
|
|
5
|
-
"No prune is required yet unless broad exploration has converged or substantial evidence is already irrelevant. Continue coherent work.",
|
|
6
|
-
"Move toward a pruning point now. Finish the current coherent step, then prune before starting another broad exploration. Managed context is materially increasing model cost.",
|
|
7
|
-
"Prune now before further tool work. Continuing with stale managed context is wasting money.",
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
export default defineTauExtensionSettings({
|
|
11
|
-
key: "contextPruning",
|
|
12
|
-
defaults: {
|
|
13
|
-
enabled: true as boolean,
|
|
14
|
-
nudgeEveryTokens: 30_000 as number,
|
|
15
|
-
nudgeInstructions: DEFAULT_NUDGE_INSTRUCTIONS,
|
|
16
|
-
},
|
|
17
|
-
schema: Type.Object(
|
|
18
|
-
{
|
|
19
|
-
enabled: Type.Optional(Type.Boolean({ default: true, description: "Enable context pruning." })),
|
|
20
|
-
nudgeEveryTokens: Type.Optional(
|
|
21
|
-
Type.Integer({
|
|
22
|
-
default: 30_000,
|
|
23
|
-
minimum: 1,
|
|
24
|
-
description: "Active-context token interval between automatic pruning hints.",
|
|
25
|
-
}),
|
|
26
|
-
),
|
|
27
|
-
nudgeInstructions: Type.Optional(
|
|
28
|
-
Type.Array(Type.String({ minLength: 1, maxLength: 2_000 }), {
|
|
29
|
-
default: DEFAULT_NUDGE_INSTRUCTIONS,
|
|
30
|
-
minItems: 1,
|
|
31
|
-
maxItems: 5,
|
|
32
|
-
description:
|
|
33
|
-
"Ordered automatic pruning instructions. Later reminders repeat the final instruction, and the final tier requires an anchor before further tool work.",
|
|
34
|
-
}),
|
|
35
|
-
),
|
|
36
|
-
},
|
|
37
|
-
{ additionalProperties: false },
|
|
38
|
-
),
|
|
39
|
-
});
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
const CONTEXT_PRUNE_TOOL = "context_prune";
|
|
4
|
-
|
|
5
|
-
export interface ContextPruneRefreshedFileV2 {
|
|
6
|
-
path: string;
|
|
7
|
-
rowId: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ContextPruneDeferredFileV2 {
|
|
11
|
-
path: string;
|
|
12
|
-
reason: string;
|
|
13
|
-
relevantWhen: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ContextPruneDetailsV2 {
|
|
17
|
-
v: 2;
|
|
18
|
-
anchorToolCallId: string;
|
|
19
|
-
prunedToolCallIds: string[];
|
|
20
|
-
prunedAutoreadRowIds: string[];
|
|
21
|
-
retainedToolCallIds: string[];
|
|
22
|
-
retainedAutoreadRowIds: string[];
|
|
23
|
-
refreshedFiles: ContextPruneRefreshedFileV2[];
|
|
24
|
-
deferredFiles: ContextPruneDeferredFileV2[];
|
|
25
|
-
warnings: string[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface ActiveContextPruningState {
|
|
29
|
-
latestAnchorToolCallId: string | undefined;
|
|
30
|
-
retainedToolCallIds: ReadonlySet<string>;
|
|
31
|
-
prunedToolCallIds: ReadonlySet<string>;
|
|
32
|
-
prunedAutoreadRowIds: ReadonlySet<string>;
|
|
33
|
-
deferredFiles: readonly ContextPruneDeferredFileV2[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const RUNTIME_ENABLED_KEY = Symbol.for("@earendil-works/tau-agent/context-pruning-enabled");
|
|
37
|
-
const runtimeState = globalThis as typeof globalThis & { [key: symbol]: unknown };
|
|
38
|
-
|
|
39
|
-
export function setContextPruningEnabled(enabled: boolean): void {
|
|
40
|
-
runtimeState[RUNTIME_ENABLED_KEY] = enabled;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isContextPruningEnabled(): boolean {
|
|
44
|
-
return runtimeState[RUNTIME_ENABLED_KEY] === true;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function parseContextPruneDetailsV2(value: unknown): ContextPruneDetailsV2 | undefined {
|
|
48
|
-
if (!isRecord(value) || value.v !== 2 || !isNonEmptyString(value.anchorToolCallId)) return undefined;
|
|
49
|
-
const prunedToolCallIds = parseUniqueStrings(value.prunedToolCallIds);
|
|
50
|
-
const prunedAutoreadRowIds = parseUniqueStrings(value.prunedAutoreadRowIds);
|
|
51
|
-
const retainedToolCallIds = parseUniqueStrings(value.retainedToolCallIds, true);
|
|
52
|
-
const retainedAutoreadRowIds = parseUniqueStrings(value.retainedAutoreadRowIds);
|
|
53
|
-
const refreshedFiles = parseRefreshedFiles(value.refreshedFiles);
|
|
54
|
-
const deferredFiles = parseDeferredFiles(value.deferredFiles);
|
|
55
|
-
const warnings = parseStrings(value.warnings, true);
|
|
56
|
-
if (
|
|
57
|
-
!prunedToolCallIds ||
|
|
58
|
-
!prunedAutoreadRowIds ||
|
|
59
|
-
!retainedToolCallIds ||
|
|
60
|
-
!retainedAutoreadRowIds ||
|
|
61
|
-
!refreshedFiles ||
|
|
62
|
-
!deferredFiles ||
|
|
63
|
-
!warnings
|
|
64
|
-
)
|
|
65
|
-
return undefined;
|
|
66
|
-
return {
|
|
67
|
-
v: 2,
|
|
68
|
-
anchorToolCallId: value.anchorToolCallId,
|
|
69
|
-
prunedToolCallIds,
|
|
70
|
-
prunedAutoreadRowIds,
|
|
71
|
-
retainedToolCallIds,
|
|
72
|
-
retainedAutoreadRowIds,
|
|
73
|
-
refreshedFiles,
|
|
74
|
-
deferredFiles,
|
|
75
|
-
warnings,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function replayContextPruningState(
|
|
80
|
-
branch: readonly SessionEntry[],
|
|
81
|
-
enabled = isContextPruningEnabled(),
|
|
82
|
-
): ActiveContextPruningState {
|
|
83
|
-
const prunedToolCallIds = new Set<string>();
|
|
84
|
-
const prunedAutoreadRowIds = new Set<string>();
|
|
85
|
-
let latestAnchorToolCallId: string | undefined;
|
|
86
|
-
let retainedToolCallIds = new Set<string>();
|
|
87
|
-
let deferredFiles: readonly ContextPruneDeferredFileV2[] = [];
|
|
88
|
-
if (enabled) {
|
|
89
|
-
for (const entry of branch) {
|
|
90
|
-
if (entry.type !== "message" || entry.message.role !== "toolResult") continue;
|
|
91
|
-
const result = entry.message;
|
|
92
|
-
if (result.toolName !== CONTEXT_PRUNE_TOOL) continue;
|
|
93
|
-
const details = parseContextPruneDetailsV2(result.details);
|
|
94
|
-
if (!details || details.anchorToolCallId !== result.toolCallId) continue;
|
|
95
|
-
for (const id of details.prunedToolCallIds) prunedToolCallIds.add(id);
|
|
96
|
-
for (const id of details.prunedAutoreadRowIds) prunedAutoreadRowIds.add(id);
|
|
97
|
-
latestAnchorToolCallId = details.anchorToolCallId;
|
|
98
|
-
retainedToolCallIds = new Set(details.retainedToolCallIds);
|
|
99
|
-
deferredFiles = details.deferredFiles;
|
|
100
|
-
}
|
|
101
|
-
for (const id of retainedToolCallIds) prunedToolCallIds.delete(id);
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
latestAnchorToolCallId,
|
|
105
|
-
retainedToolCallIds,
|
|
106
|
-
prunedToolCallIds,
|
|
107
|
-
prunedAutoreadRowIds,
|
|
108
|
-
deferredFiles,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function parseRefreshedFiles(value: unknown): ContextPruneRefreshedFileV2[] | undefined {
|
|
113
|
-
if (!Array.isArray(value)) return undefined;
|
|
114
|
-
const files: ContextPruneRefreshedFileV2[] = [];
|
|
115
|
-
for (const item of value) {
|
|
116
|
-
if (!isRecord(item) || !isNonEmptyString(item.path) || !isNonEmptyString(item.rowId)) return undefined;
|
|
117
|
-
files.push({ path: item.path, rowId: item.rowId });
|
|
118
|
-
}
|
|
119
|
-
return files;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function parseDeferredFiles(value: unknown): ContextPruneDeferredFileV2[] | undefined {
|
|
123
|
-
if (!Array.isArray(value)) return undefined;
|
|
124
|
-
const files: ContextPruneDeferredFileV2[] = [];
|
|
125
|
-
for (const item of value) {
|
|
126
|
-
if (!isRecord(item) || typeof item.path !== "string" || typeof item.reason !== "string") return undefined;
|
|
127
|
-
if (typeof item.relevantWhen !== "string") return undefined;
|
|
128
|
-
files.push({ path: item.path, reason: item.reason, relevantWhen: item.relevantWhen });
|
|
129
|
-
}
|
|
130
|
-
return files;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function parseUniqueStrings(value: unknown, allowEmpty = false): string[] | undefined {
|
|
134
|
-
const strings = parseStrings(value, allowEmpty);
|
|
135
|
-
if (!strings || new Set(strings).size !== strings.length) return undefined;
|
|
136
|
-
return strings;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function parseStrings(value: unknown, allowEmpty = false): string[] | undefined {
|
|
140
|
-
if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && (allowEmpty || item.length > 0))) {
|
|
141
|
-
return undefined;
|
|
142
|
-
}
|
|
143
|
-
return [...value];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
147
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function isNonEmptyString(value: unknown): value is string {
|
|
151
|
-
return typeof value === "string" && value.length > 0;
|
|
152
|
-
}
|