@shanepadgett/tau-agent 0.16.0 → 0.18.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 +97 -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
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { replayContextPruningState } from "../../shared/context-pruning-state.ts";
|
|
4
|
+
import { applyCompleteFileDiff, COMPLETE_FILE_SCOPE, decodeCompleteFileSource } from "./full-file-knowledge.ts";
|
|
3
5
|
|
|
4
6
|
export type ReadCacheMode = "baseline" | "recovery" | "unchanged" | "diff";
|
|
7
|
+
export type ReadCachePresentation = "plain" | "line-numbered";
|
|
5
8
|
|
|
6
9
|
export interface ReadCacheMetaV1 {
|
|
7
10
|
v: 1;
|
|
8
11
|
pathKey: string;
|
|
9
12
|
scopeKey: string;
|
|
13
|
+
presentation: ReadCachePresentation;
|
|
10
14
|
servedHash: string;
|
|
11
15
|
baseHash?: string;
|
|
12
16
|
mode: ReadCacheMode;
|
|
@@ -16,70 +20,68 @@ export interface ReadCacheMetaV1 {
|
|
|
16
20
|
summary: string;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
interface
|
|
23
|
+
export interface ReadCacheScopeTrust {
|
|
20
24
|
hash: string;
|
|
21
|
-
|
|
25
|
+
text?: string;
|
|
26
|
+
rowIds: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AcceptedReadCacheRow {
|
|
30
|
+
rowId: string;
|
|
31
|
+
pathKey: string;
|
|
32
|
+
scopeKey: string;
|
|
33
|
+
meta: ReadCacheMetaV1;
|
|
34
|
+
dependencyRowIds: readonly string[];
|
|
22
35
|
}
|
|
23
36
|
|
|
24
|
-
interface
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
export interface CompleteFileDependencyChain {
|
|
38
|
+
pathKey: string;
|
|
39
|
+
servedHash: string;
|
|
40
|
+
rowIds: readonly string[];
|
|
41
|
+
sourceText: string;
|
|
28
42
|
}
|
|
29
43
|
|
|
30
|
-
interface
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
export interface ReadCacheReplayResult {
|
|
45
|
+
scopeTrust: ReadonlyMap<string, ReadonlyMap<string, ReadCacheScopeTrust>>;
|
|
46
|
+
acceptedRows: readonly AcceptedReadCacheRow[];
|
|
47
|
+
completeFileChains: ReadonlyMap<string, CompleteFileDependencyChain>;
|
|
48
|
+
failedPatchRecoveryPaths: ReadonlySet<string>;
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
export interface ReadCacheDecision {
|
|
36
52
|
baseHash?: string;
|
|
53
|
+
baselineText?: string;
|
|
37
54
|
recovery: boolean;
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
export interface ReadCacheStore {
|
|
41
58
|
decision(ctx: ExtensionContext, pathKey: string, scopeKey: string): ReadCacheDecision;
|
|
42
|
-
record(ctx: ExtensionContext, meta: ReadCacheMetaV1): void;
|
|
43
|
-
clear(): void;
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
export function createReadCacheStore(): ReadCacheStore {
|
|
47
|
-
const overlays = new Map<string, OverlayState>();
|
|
48
|
-
|
|
49
62
|
return {
|
|
50
63
|
decision(ctx, pathKey, scopeKey) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
const manager = ctx.sessionManager;
|
|
65
|
+
if (!manager) {
|
|
66
|
+
return { baseHash: undefined, baselineText: undefined, recovery: false };
|
|
67
|
+
}
|
|
68
|
+
const pruning = replayContextPruningState(manager.getBranch());
|
|
69
|
+
const ignoredRowIds = new Set([...pruning.prunedToolCallIds, ...pruning.prunedAutoreadRowIds]);
|
|
70
|
+
const state = replayReadCache(manager.buildContextEntries(), ctx.cwd, ignoredRowIds);
|
|
71
|
+
const trust = state.scopeTrust.get(pathKey)?.get(scopeKey);
|
|
54
72
|
return {
|
|
55
|
-
baseHash:
|
|
56
|
-
|
|
73
|
+
baseHash: trust?.hash,
|
|
74
|
+
baselineText: trust?.text,
|
|
75
|
+
recovery: state.failedPatchRecoveryPaths.has(pathKey),
|
|
57
76
|
};
|
|
58
77
|
},
|
|
59
|
-
record(ctx, meta) {
|
|
60
|
-
const key = overlayKey(ctx);
|
|
61
|
-
let overlay = overlays.get(key);
|
|
62
|
-
if (!overlay) {
|
|
63
|
-
overlay = { trust: new Map(), sequence: 1_000_000_000 };
|
|
64
|
-
overlays.set(key, overlay);
|
|
65
|
-
}
|
|
66
|
-
overlay.sequence += 1;
|
|
67
|
-
setTrust(overlay.trust, meta.pathKey, meta.scopeKey, meta.servedHash, overlay.sequence);
|
|
68
|
-
while (overlays.size > 32) {
|
|
69
|
-
const oldest = overlays.keys().next().value as string | undefined;
|
|
70
|
-
if (oldest === undefined) break;
|
|
71
|
-
overlays.delete(oldest);
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
clear() {
|
|
75
|
-
overlays.clear();
|
|
76
|
-
},
|
|
77
78
|
};
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
function parseReadCacheMeta(value: unknown): ReadCacheMetaV1 | undefined {
|
|
81
82
|
if (!isRecord(value) || value.v !== 1) return undefined;
|
|
82
83
|
if (typeof value.pathKey !== "string" || typeof value.scopeKey !== "string") return undefined;
|
|
84
|
+
if (value.presentation !== "plain" && value.presentation !== "line-numbered") return undefined;
|
|
83
85
|
if (typeof value.servedHash !== "string" || value.servedHash.length === 0) return undefined;
|
|
84
86
|
if (!isReadCacheMode(value.mode)) return undefined;
|
|
85
87
|
if (!isNonNegativeFinite(value.baselineTokens) || !isNonNegativeFinite(value.returnedTokens)) return undefined;
|
|
@@ -90,6 +92,7 @@ function parseReadCacheMeta(value: unknown): ReadCacheMetaV1 | undefined {
|
|
|
90
92
|
v: 1,
|
|
91
93
|
pathKey: value.pathKey,
|
|
92
94
|
scopeKey: value.scopeKey,
|
|
95
|
+
presentation: value.presentation,
|
|
93
96
|
servedHash: value.servedHash,
|
|
94
97
|
baseHash: value.baseHash,
|
|
95
98
|
mode: value.mode,
|
|
@@ -110,41 +113,120 @@ export function readMetaFromMessage(message: unknown): ReadCacheMetaV1 | undefin
|
|
|
110
113
|
return parseReadCacheMeta(message.details.readCache);
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
116
|
+
export function replayReadCache(
|
|
117
|
+
entries: readonly unknown[],
|
|
118
|
+
cwd: string,
|
|
119
|
+
ignoredRowIds: ReadonlySet<string> = new Set(),
|
|
120
|
+
): ReadCacheReplayResult {
|
|
121
|
+
const trust = new Map<string, Map<string, ReadCacheScopeTrust>>();
|
|
122
|
+
const acceptedRows: AcceptedReadCacheRow[] = [];
|
|
123
|
+
const failedPatchRecoveryPaths = new Set<string>();
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (!isRecord(entry)) continue;
|
|
126
|
+
let message: unknown;
|
|
127
|
+
if (entry.type === "message" && "message" in entry) message = entry.message;
|
|
128
|
+
else if (entry.type === "custom_message") message = entry;
|
|
129
|
+
else continue;
|
|
130
|
+
const parsedMeta = readMetaFromMessage(message);
|
|
131
|
+
if (parsedMeta) {
|
|
132
|
+
const rowId = readRowId(message);
|
|
133
|
+
if (rowId && ignoredRowIds.has(rowId)) continue;
|
|
134
|
+
const pathKey = resolve(cwd, parsedMeta.pathKey);
|
|
135
|
+
const meta = pathKey === parsedMeta.pathKey ? parsedMeta : { ...parsedMeta, pathKey };
|
|
136
|
+
if (rowId && applyReadMeta(trust, failedPatchRecoveryPaths, message, meta, rowId)) {
|
|
137
|
+
const acceptedTrust = trust.get(meta.pathKey)?.get(meta.scopeKey);
|
|
138
|
+
if (acceptedTrust) {
|
|
139
|
+
acceptedRows.push({
|
|
140
|
+
rowId,
|
|
141
|
+
pathKey: meta.pathKey,
|
|
142
|
+
scopeKey: meta.scopeKey,
|
|
143
|
+
meta,
|
|
144
|
+
dependencyRowIds: [...acceptedTrust.rowIds],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
131
148
|
continue;
|
|
132
149
|
}
|
|
133
|
-
for (const path of failedPatchPaths(message,
|
|
150
|
+
for (const path of failedPatchPaths(message, cwd)) failedPatchRecoveryPaths.add(path);
|
|
151
|
+
}
|
|
152
|
+
const completeFileChains = new Map<string, CompleteFileDependencyChain>();
|
|
153
|
+
for (const [pathKey, scopes] of trust) {
|
|
154
|
+
const complete = scopes.get(COMPLETE_FILE_SCOPE);
|
|
155
|
+
if (!complete || complete.text === undefined) continue;
|
|
156
|
+
completeFileChains.set(pathKey, {
|
|
157
|
+
pathKey,
|
|
158
|
+
servedHash: complete.hash,
|
|
159
|
+
rowIds: [...complete.rowIds],
|
|
160
|
+
sourceText: complete.text,
|
|
161
|
+
});
|
|
134
162
|
}
|
|
135
|
-
return
|
|
163
|
+
return { scopeTrust: trust, acceptedRows, completeFileChains, failedPatchRecoveryPaths };
|
|
136
164
|
}
|
|
137
165
|
|
|
138
|
-
function applyReadMeta(
|
|
139
|
-
|
|
166
|
+
function applyReadMeta(
|
|
167
|
+
trust: Map<string, Map<string, ReadCacheScopeTrust>>,
|
|
168
|
+
failedPatchRecoveryPaths: Set<string>,
|
|
169
|
+
message: unknown,
|
|
170
|
+
meta: ReadCacheMetaV1,
|
|
171
|
+
rowId: string,
|
|
172
|
+
): boolean {
|
|
173
|
+
const existing = trust.get(meta.pathKey)?.get(meta.scopeKey);
|
|
140
174
|
if (meta.mode === "baseline" || meta.mode === "recovery") {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
175
|
+
let text: string | undefined;
|
|
176
|
+
if (meta.scopeKey === COMPLETE_FILE_SCOPE) {
|
|
177
|
+
if (!isRecord(message)) return false;
|
|
178
|
+
let pathHeader: string | undefined;
|
|
179
|
+
if (message.customType === "tau.autoread") {
|
|
180
|
+
if (!isRecord(message.details)) return false;
|
|
181
|
+
if (typeof message.details.path !== "string" || typeof message.details.cwd !== "string") return false;
|
|
182
|
+
if (resolve(message.details.cwd, message.details.path) !== meta.pathKey) return false;
|
|
183
|
+
pathHeader = message.details.path;
|
|
184
|
+
}
|
|
185
|
+
const decoded = decodeCompleteFileSource({
|
|
186
|
+
content: message.content,
|
|
187
|
+
autoread: message.customType === "tau.autoread",
|
|
188
|
+
...(pathHeader === undefined ? {} : { pathHeader }),
|
|
189
|
+
presentation: meta.presentation,
|
|
190
|
+
servedHash: meta.servedHash,
|
|
191
|
+
});
|
|
192
|
+
if (!decoded.valid || decoded.text === undefined) return false;
|
|
193
|
+
text = decoded.text;
|
|
194
|
+
}
|
|
195
|
+
setTrust(trust, meta.pathKey, meta.scopeKey, { hash: meta.servedHash, text, rowIds: [rowId] });
|
|
196
|
+
failedPatchRecoveryPaths.delete(meta.pathKey);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
if (!meta.baseHash || existing?.hash !== meta.baseHash) return false;
|
|
200
|
+
if (meta.mode === "unchanged" && meta.servedHash !== meta.baseHash) return false;
|
|
201
|
+
let text = meta.mode === "unchanged" ? existing.text : undefined;
|
|
202
|
+
if (meta.scopeKey === COMPLETE_FILE_SCOPE && meta.mode === "diff") {
|
|
203
|
+
if (existing.text === undefined) return false;
|
|
204
|
+
const decoded = applyCompleteFileDiff({
|
|
205
|
+
content: isRecord(message) ? message.content : undefined,
|
|
206
|
+
baseText: existing.text,
|
|
207
|
+
baseHash: meta.baseHash,
|
|
208
|
+
servedHash: meta.servedHash,
|
|
209
|
+
});
|
|
210
|
+
if (!decoded.valid || decoded.text === undefined) return false;
|
|
211
|
+
text = decoded.text;
|
|
212
|
+
}
|
|
213
|
+
setTrust(trust, meta.pathKey, meta.scopeKey, {
|
|
214
|
+
hash: meta.servedHash,
|
|
215
|
+
text,
|
|
216
|
+
rowIds: [...existing.rowIds, rowId],
|
|
217
|
+
});
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readRowId(message: unknown): string | undefined {
|
|
222
|
+
if (!isRecord(message)) return undefined;
|
|
223
|
+
if (message.role === "toolResult" && message.toolName === "read") {
|
|
224
|
+
return typeof message.toolCallId === "string" && message.toolCallId.length > 0 ? message.toolCallId : undefined;
|
|
144
225
|
}
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
226
|
+
if (message.customType !== "tau.autoread" || !isRecord(message.details)) return undefined;
|
|
227
|
+
return typeof message.details.rowId === "string" && message.details.rowId.length > 0
|
|
228
|
+
? message.details.rowId
|
|
229
|
+
: undefined;
|
|
148
230
|
}
|
|
149
231
|
|
|
150
232
|
function failedPatchPaths(message: unknown, cwd: string): string[] {
|
|
@@ -165,35 +247,17 @@ function failedPatchPaths(message: unknown, cwd: string): string[] {
|
|
|
165
247
|
}
|
|
166
248
|
|
|
167
249
|
function setTrust(
|
|
168
|
-
trust: Map<string, Map<string,
|
|
250
|
+
trust: Map<string, Map<string, ReadCacheScopeTrust>>,
|
|
169
251
|
pathKey: string,
|
|
170
252
|
scopeKey: string,
|
|
171
|
-
|
|
172
|
-
sequence: number,
|
|
253
|
+
value: ReadCacheScopeTrust,
|
|
173
254
|
): void {
|
|
174
255
|
let scopes = trust.get(pathKey);
|
|
175
256
|
if (!scopes) {
|
|
176
257
|
scopes = new Map();
|
|
177
258
|
trust.set(pathKey, scopes);
|
|
178
259
|
}
|
|
179
|
-
scopes.set(scopeKey,
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function mergeTrust(state: ReplayState, overlay: Map<string, Map<string, ScopeTrust>>): void {
|
|
183
|
-
for (const [pathKey, scopes] of overlay) {
|
|
184
|
-
for (const [scopeKey, candidate] of scopes) {
|
|
185
|
-
const current = state.trust.get(pathKey)?.get(scopeKey);
|
|
186
|
-
if (!current || candidate.sequence > current.sequence) {
|
|
187
|
-
setTrust(state.trust, pathKey, scopeKey, candidate.hash, candidate.sequence);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function overlayKey(ctx: ExtensionContext): string {
|
|
194
|
-
const manager = ctx.sessionManager;
|
|
195
|
-
if (!manager) return `test:${ctx.cwd}`;
|
|
196
|
-
return `${manager.getSessionId()}:${manager.getLeafId() ?? "root"}`;
|
|
260
|
+
scopes.set(scopeKey, value);
|
|
197
261
|
}
|
|
198
262
|
|
|
199
263
|
function isReadCacheMode(value: unknown): value is ReadCacheMode {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { MAX_COMPLETE_FILE_SNAPSHOT_BYTES } from "./full-file-knowledge.ts";
|
|
2
|
+
|
|
2
3
|
const MAX_TOTAL_BYTES = 16 * 1024 * 1024;
|
|
3
4
|
|
|
4
5
|
interface Snapshot {
|
|
@@ -8,13 +9,16 @@ interface Snapshot {
|
|
|
8
9
|
|
|
9
10
|
export interface ReadSnapshotStore {
|
|
10
11
|
get(hash: string): string | undefined;
|
|
11
|
-
|
|
12
|
+
epoch(): number;
|
|
13
|
+
isCurrent(epoch: number): boolean;
|
|
14
|
+
set(hash: string, text: string, bytes: number, epoch: number): boolean;
|
|
12
15
|
clear(): void;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
export function createReadSnapshotStore(): ReadSnapshotStore {
|
|
16
19
|
const snapshots = new Map<string, Snapshot>();
|
|
17
20
|
let totalBytes = 0;
|
|
21
|
+
let epoch = 0;
|
|
18
22
|
|
|
19
23
|
return {
|
|
20
24
|
get(hash) {
|
|
@@ -24,8 +28,15 @@ export function createReadSnapshotStore(): ReadSnapshotStore {
|
|
|
24
28
|
snapshots.set(hash, snapshot);
|
|
25
29
|
return snapshot.text;
|
|
26
30
|
},
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
epoch() {
|
|
32
|
+
return epoch;
|
|
33
|
+
},
|
|
34
|
+
isCurrent(candidate) {
|
|
35
|
+
return candidate === epoch;
|
|
36
|
+
},
|
|
37
|
+
set(hash, text, bytes, candidateEpoch) {
|
|
38
|
+
if (candidateEpoch !== epoch || bytes > MAX_COMPLETE_FILE_SNAPSHOT_BYTES) return false;
|
|
39
|
+
if (snapshots.has(hash)) return true;
|
|
29
40
|
snapshots.set(hash, { text, bytes });
|
|
30
41
|
totalBytes += bytes;
|
|
31
42
|
while (totalBytes > MAX_TOTAL_BYTES) {
|
|
@@ -35,10 +46,12 @@ export function createReadSnapshotStore(): ReadSnapshotStore {
|
|
|
35
46
|
snapshots.delete(oldestHash);
|
|
36
47
|
totalBytes -= oldest?.bytes ?? 0;
|
|
37
48
|
}
|
|
49
|
+
return true;
|
|
38
50
|
},
|
|
39
51
|
clear() {
|
|
40
52
|
snapshots.clear();
|
|
41
53
|
totalBytes = 0;
|
|
54
|
+
epoch += 1;
|
|
42
55
|
},
|
|
43
56
|
};
|
|
44
57
|
}
|
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
createReadToolDefinition,
|
|
3
3
|
DEFAULT_MAX_BYTES,
|
|
4
4
|
formatSize,
|
|
5
|
-
generateUnifiedPatch,
|
|
6
5
|
truncateHead,
|
|
7
6
|
type ReadToolDetails,
|
|
8
7
|
type ToolDefinition,
|
|
@@ -13,9 +12,20 @@ import { readFile } from "node:fs/promises";
|
|
|
13
12
|
import { isAbsolute, resolve } from "node:path";
|
|
14
13
|
import { Type, type Static } from "typebox";
|
|
15
14
|
import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
15
|
+
import {
|
|
16
|
+
createCompleteFileMeta,
|
|
17
|
+
estimateTokens,
|
|
18
|
+
readScopeKey,
|
|
19
|
+
selectCompleteFileResponse,
|
|
20
|
+
} from "./full-file-knowledge.ts";
|
|
16
21
|
import { normalizeCountLimit } from "./limits.ts";
|
|
17
22
|
import { stripLeadingAt } from "./path-display.ts";
|
|
18
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
createReadCacheStore,
|
|
25
|
+
type ReadCacheMetaV1,
|
|
26
|
+
type ReadCachePresentation,
|
|
27
|
+
type ReadCacheStore,
|
|
28
|
+
} from "./read-cache.ts";
|
|
19
29
|
import { createReadSnapshotStore, type ReadSnapshotStore } from "./read-snapshots.ts";
|
|
20
30
|
|
|
21
31
|
const readSchema = Type.Object({
|
|
@@ -43,6 +53,7 @@ interface BaselineTextResult {
|
|
|
43
53
|
endLine: number;
|
|
44
54
|
completeFile: boolean;
|
|
45
55
|
scopeKey: string;
|
|
56
|
+
presentation: ReadCachePresentation;
|
|
46
57
|
summary: string;
|
|
47
58
|
cacheable: boolean;
|
|
48
59
|
}
|
|
@@ -86,10 +97,6 @@ function renderCallSummary(args: ReadToolInput | undefined): string {
|
|
|
86
97
|
return `${path}:${start}${end === "" ? "" : `-${end}`}`;
|
|
87
98
|
}
|
|
88
99
|
|
|
89
|
-
function estimateTokens(text: string): number {
|
|
90
|
-
return Math.ceil(text.length / 4);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
100
|
function baselineText(text: string, params: ReadToolInput): BaselineTextResult {
|
|
94
101
|
const allLines = text.split("\n");
|
|
95
102
|
const startIndex = params.offset ? Math.max(0, params.offset - 1) : 0;
|
|
@@ -134,7 +141,8 @@ function baselineText(text: string, params: ReadToolInput): BaselineTextResult {
|
|
|
134
141
|
|
|
135
142
|
const endLine = outputLines === 0 ? startLine : startLine + outputLines - 1;
|
|
136
143
|
const completeFile = startIndex === 0 && selectedEnd === allLines.length && !truncation.truncated;
|
|
137
|
-
const
|
|
144
|
+
const presentation: ReadCachePresentation = params.lineNumbers ? "line-numbered" : "plain";
|
|
145
|
+
const scopeKey = readScopeKey(completeFile, startLine, endLine, presentation);
|
|
138
146
|
const summary = completeFile ? `${allLines.length} lines` : `${outputLines} lines`;
|
|
139
147
|
return {
|
|
140
148
|
text: outputText,
|
|
@@ -144,6 +152,7 @@ function baselineText(text: string, params: ReadToolInput): BaselineTextResult {
|
|
|
144
152
|
endLine,
|
|
145
153
|
completeFile,
|
|
146
154
|
scopeKey,
|
|
155
|
+
presentation,
|
|
147
156
|
summary,
|
|
148
157
|
cacheable,
|
|
149
158
|
};
|
|
@@ -165,10 +174,24 @@ function createMeta(
|
|
|
165
174
|
baseHash?: string,
|
|
166
175
|
summary = baseline.summary,
|
|
167
176
|
): ReadCacheMetaV1 {
|
|
177
|
+
if (baseline.completeFile) {
|
|
178
|
+
return createCompleteFileMeta({
|
|
179
|
+
pathKey,
|
|
180
|
+
presentation: baseline.presentation,
|
|
181
|
+
servedHash: hash,
|
|
182
|
+
mode,
|
|
183
|
+
sourceText: baseline.text,
|
|
184
|
+
returnedText,
|
|
185
|
+
totalLines: baseline.totalLines,
|
|
186
|
+
summary,
|
|
187
|
+
baseHash,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
168
190
|
return {
|
|
169
191
|
v: 1,
|
|
170
192
|
pathKey,
|
|
171
193
|
scopeKey: baseline.scopeKey,
|
|
194
|
+
presentation: baseline.presentation,
|
|
172
195
|
servedHash: hash,
|
|
173
196
|
baseHash,
|
|
174
197
|
mode,
|
|
@@ -179,16 +202,6 @@ function createMeta(
|
|
|
179
202
|
};
|
|
180
203
|
}
|
|
181
204
|
|
|
182
|
-
function countDiffLines(patch: string): { added: number; removed: number } {
|
|
183
|
-
let added = 0;
|
|
184
|
-
let removed = 0;
|
|
185
|
-
for (const line of patch.split("\n")) {
|
|
186
|
-
if (line.startsWith("+") && !line.startsWith("+++")) added += 1;
|
|
187
|
-
else if (line.startsWith("-") && !line.startsWith("---")) removed += 1;
|
|
188
|
-
}
|
|
189
|
-
return { added, removed };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
205
|
export function createExploreReadTool(
|
|
193
206
|
rowState: ToolRowStateStore,
|
|
194
207
|
cache: ReadCacheStore = createReadCacheStore(),
|
|
@@ -197,6 +210,8 @@ export function createExploreReadTool(
|
|
|
197
210
|
const baseDefinition = readDefinitionForCwd(process.cwd());
|
|
198
211
|
return {
|
|
199
212
|
...baseDefinition,
|
|
213
|
+
description:
|
|
214
|
+
"Read a relevant file as a whole by omitting offset, limit, and lineNumbers. Later whole-file reads avoid repeating unchanged content and return useful diffs or current source after changes. Use ranges only for targeted peripheral inspection or to continue after truncation.",
|
|
200
215
|
parameters: readSchema,
|
|
201
216
|
async execute(
|
|
202
217
|
toolCallId: Parameters<ReadExecute>[0],
|
|
@@ -208,6 +223,7 @@ export function createExploreReadTool(
|
|
|
208
223
|
const definition = readDefinitionForCwd(ctx.cwd);
|
|
209
224
|
const normalized = normalizeReadParams(params);
|
|
210
225
|
const path = isAbsolute(normalized.path) ? resolve(normalized.path) : resolve(ctx.cwd, normalized.path);
|
|
226
|
+
const snapshotEpoch = snapshots.epoch();
|
|
211
227
|
const buffer = await readFile(path);
|
|
212
228
|
if (isSupportedImage(buffer)) {
|
|
213
229
|
return definition.execute(
|
|
@@ -222,7 +238,7 @@ export function createExploreReadTool(
|
|
|
222
238
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
223
239
|
let text: string;
|
|
224
240
|
try {
|
|
225
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
241
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer);
|
|
226
242
|
} catch {
|
|
227
243
|
return definition.execute(toolCallId, normalized, signal, onUpdate, ctx);
|
|
228
244
|
}
|
|
@@ -232,36 +248,39 @@ export function createExploreReadTool(
|
|
|
232
248
|
return { content: [{ type: "text", text: baseline.text }], details: baseline.details };
|
|
233
249
|
}
|
|
234
250
|
const hash = createHash("sha256").update(buffer).digest("hex");
|
|
235
|
-
const
|
|
251
|
+
const staleEpoch = !snapshots.isCurrent(snapshotEpoch);
|
|
252
|
+
const decision = staleEpoch
|
|
253
|
+
? { baseHash: undefined, baselineText: undefined, recovery: false }
|
|
254
|
+
: cache.decision(ctx, path, baseline.scopeKey);
|
|
236
255
|
let output = baseline.text;
|
|
237
256
|
let mode: ReadCacheMetaV1["mode"] = decision.recovery ? "recovery" : "baseline";
|
|
238
257
|
let summary = baseline.summary;
|
|
239
258
|
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
:
|
|
259
|
+
if (baseline.completeFile) {
|
|
260
|
+
const selected = selectCompleteFileResponse({
|
|
261
|
+
displayPath: normalized.path,
|
|
262
|
+
currentText: text,
|
|
263
|
+
currentHash: hash,
|
|
264
|
+
fullText: baseline.text,
|
|
265
|
+
totalLines: baseline.totalLines,
|
|
266
|
+
recovery: decision.recovery,
|
|
267
|
+
baseHash: decision.baseHash,
|
|
268
|
+
baselineText:
|
|
269
|
+
decision.baselineText ??
|
|
270
|
+
(decision.baseHash === undefined ? undefined : snapshots.get(decision.baseHash)),
|
|
271
|
+
});
|
|
272
|
+
output = selected.text;
|
|
273
|
+
mode = selected.mode;
|
|
274
|
+
summary = selected.summary;
|
|
275
|
+
} else if (!decision.recovery && decision.baseHash === hash) {
|
|
276
|
+
output = `unchanged, lines ${baseline.startLine}-${baseline.endLine} of ${baseline.totalLines}`;
|
|
244
277
|
mode = "unchanged";
|
|
245
278
|
summary = output;
|
|
246
|
-
} else if (!decision.recovery && decision.baseHash && baseline.completeFile && !normalized.lineNumbers) {
|
|
247
|
-
const baseText = snapshots.get(decision.baseHash);
|
|
248
|
-
if (baseText !== undefined) {
|
|
249
|
-
const patch = generateUnifiedPatch(normalized.path, baseText, text, 3);
|
|
250
|
-
const counts = countDiffLines(patch);
|
|
251
|
-
const candidate = `[read: ${counts.added} lines added, ${counts.removed} removed of ${baseline.totalLines}]\n${patch}`;
|
|
252
|
-
const candidateTruncation = truncateHead(candidate);
|
|
253
|
-
if (!candidateTruncation.truncated && estimateTokens(candidate) < estimateTokens(baseline.text)) {
|
|
254
|
-
output = candidate;
|
|
255
|
-
mode = "diff";
|
|
256
|
-
summary = `+${counts.added} -${counts.removed}`;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
279
|
}
|
|
260
280
|
|
|
261
281
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
262
|
-
snapshots.set(hash, text, buffer.byteLength);
|
|
282
|
+
snapshots.set(hash, text, buffer.byteLength, snapshotEpoch);
|
|
263
283
|
const meta = createMeta(baseline, path, hash, mode, output, decision.baseHash, summary);
|
|
264
|
-
cache.record(ctx, meta);
|
|
265
284
|
return withMeta(baseline, meta, output);
|
|
266
285
|
},
|
|
267
286
|
renderCall(
|