@shanepadgett/tau-agent 0.26.0 → 0.27.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/appshot/README.md +1 -1
- package/extensions/appshot/index.ts +5 -11
- package/extensions/context-pruning/README.md +2 -2
- package/extensions/context-pruning/index.ts +58 -57
- package/extensions/context-pruning/render.ts +51 -39
- package/extensions/context-pruning/settings.ts +4 -5
- package/extensions/explore/README.md +18 -11
- package/extensions/explore/ast-guidance.ts +67 -0
- package/extensions/explore/ast-languages.ts +61 -0
- package/extensions/explore/ast-tools.ts +1257 -91
- package/extensions/explore/ast-worker.ts +735 -66
- package/extensions/explore/index.ts +62 -6
- package/extensions/explore/orientation-state.ts +280 -0
- package/extensions/explore/read-stats-panel.ts +23 -1
- package/extensions/explore/read-stats.ts +21 -3
- package/extensions/explore/read.ts +58 -21
- package/extensions/explore/settings.ts +46 -0
- package/extensions/explore/traverse.ts +15 -4
- package/extensions/patch/executor.ts +95 -1
- package/extensions/patch/index.ts +1 -0
- package/extensions/soul/prompt.ts +1 -3
- package/extensions/subagent/agents/review.md +76 -25
- package/extensions/subagent/agents/scout.md +118 -22
- package/extensions/tau-help/help.md +3 -1
- package/native-bin/darwin-arm64/tau-ast +0 -0
- package/package.json +2 -3
- package/schemas/tau.schema.json +44 -4
- package/shared/bounded-text-result.ts +304 -0
- package/shared/events.ts +2 -1
- package/shared/temporary-output-store.ts +275 -0
|
@@ -1,35 +1,56 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_MAX_BYTES,
|
|
3
|
-
DEFAULT_MAX_LINES,
|
|
4
|
-
defineTool,
|
|
5
|
-
formatSize,
|
|
6
|
-
keyHint,
|
|
7
|
-
truncateHead,
|
|
8
|
-
type Theme,
|
|
9
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { defineTool, formatSize, keyHint, type Theme } from "@earendil-works/pi-coding-agent";
|
|
10
2
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
11
3
|
import { Text, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { realpath, stat } from "node:fs/promises";
|
|
6
|
+
import { basename, resolve } from "node:path";
|
|
14
7
|
import { Type } from "typebox";
|
|
8
|
+
import {
|
|
9
|
+
BoundedTextResultBuilder,
|
|
10
|
+
truncateBoundedHead,
|
|
11
|
+
type BoundedTextOverflowDetails,
|
|
12
|
+
} from "../../shared/bounded-text-result.ts";
|
|
13
|
+
import type { TemporaryOutputStore } from "../../shared/temporary-output-store.ts";
|
|
15
14
|
import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
15
|
+
import { applyExactSourceMutations, type ApplyPatchSummary } from "../patch/executor.ts";
|
|
16
|
+
import {
|
|
17
|
+
AST_LANGUAGE_REGISTRY,
|
|
18
|
+
astLanguageForPath,
|
|
19
|
+
formatAstLanguageLabels,
|
|
20
|
+
requireAstLanguageForPath,
|
|
21
|
+
type AstLanguage,
|
|
22
|
+
} from "./ast-languages.ts";
|
|
23
|
+
import type { OrientationState } from "./orientation-state.ts";
|
|
16
24
|
import { formatPathForDisplay, resolveExplorePath, stripLeadingAt } from "./path-display.ts";
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
import {
|
|
26
|
+
AstWorkerError,
|
|
27
|
+
type ApiCandidate,
|
|
28
|
+
type ApiDeclarationKind,
|
|
29
|
+
type ApiDiscoveryResult,
|
|
30
|
+
type ApiQuery,
|
|
31
|
+
type ApiSurfaceFilter,
|
|
32
|
+
type AstSearchResult,
|
|
33
|
+
type AstClient,
|
|
34
|
+
type EditOperation,
|
|
35
|
+
type EditPlanResult,
|
|
36
|
+
type OutlineEntry,
|
|
37
|
+
type OutlineFileResult,
|
|
38
|
+
type SourceRange,
|
|
39
|
+
type OutlineTarget,
|
|
40
|
+
type OutlineTargetResult,
|
|
41
|
+
type RecursiveOutlineSummary,
|
|
42
|
+
type RelationshipOperation,
|
|
43
|
+
type RelationshipResult,
|
|
44
|
+
type SymbolBatchResult,
|
|
45
|
+
type SymbolView,
|
|
27
46
|
} from "./ast-worker.ts";
|
|
28
47
|
|
|
48
|
+
const supportedLanguageLabels = formatAstLanguageLabels(AST_LANGUAGE_REGISTRY, "or");
|
|
49
|
+
|
|
29
50
|
const outlineParams = Type.Object(
|
|
30
51
|
{
|
|
31
52
|
path: Type.String({
|
|
32
|
-
description:
|
|
53
|
+
description: `${supportedLanguageLabels} source file or directory`,
|
|
33
54
|
}),
|
|
34
55
|
includePrivate: Type.Optional(Type.Boolean({ description: "Include private declarations and members" })),
|
|
35
56
|
includeDocs: Type.Optional(
|
|
@@ -41,6 +62,9 @@ const outlineParams = Type.Object(
|
|
|
41
62
|
description: "Exact top-level or member declaration names",
|
|
42
63
|
}),
|
|
43
64
|
),
|
|
65
|
+
recursive: Type.Optional(
|
|
66
|
+
Type.Boolean({ description: "Recursively outline every supported source file below a directory" }),
|
|
67
|
+
),
|
|
44
68
|
},
|
|
45
69
|
{ additionalProperties: false },
|
|
46
70
|
);
|
|
@@ -50,9 +74,8 @@ const symbolParams = Type.Object(
|
|
|
50
74
|
minItems: 1,
|
|
51
75
|
description: "Numeric locators shown in parentheses by outline",
|
|
52
76
|
}),
|
|
53
|
-
view: StringEnum(["signature", "declaration", "declarationWithImports"] as const, {
|
|
54
|
-
description:
|
|
55
|
-
"Source view to retrieve; TypeScript, TSX, Odin, Go, Rust, C#, Java, Kotlin, Swift, and Markdown support selective views",
|
|
77
|
+
view: StringEnum(["signature", "signatureWithDocs", "declaration", "declarationWithImports"] as const, {
|
|
78
|
+
description: `Source view to retrieve: signature omits docs and bodies; signatureWithDocs adds attached docs; declaration returns exact source; declarationWithImports adds required imports. ${supportedLanguageLabels} support selective views`,
|
|
56
79
|
}),
|
|
57
80
|
contextLines: Type.Optional(
|
|
58
81
|
Type.Integer({ minimum: 0, description: "Lines of source context before and after each declaration" }),
|
|
@@ -60,15 +83,169 @@ const symbolParams = Type.Object(
|
|
|
60
83
|
},
|
|
61
84
|
{ additionalProperties: false },
|
|
62
85
|
);
|
|
86
|
+
const apiDeclarationKinds = [
|
|
87
|
+
"module",
|
|
88
|
+
"namespace",
|
|
89
|
+
"package",
|
|
90
|
+
"class",
|
|
91
|
+
"method",
|
|
92
|
+
"property",
|
|
93
|
+
"field",
|
|
94
|
+
"constructor",
|
|
95
|
+
"enum",
|
|
96
|
+
"interface",
|
|
97
|
+
"function",
|
|
98
|
+
"variable",
|
|
99
|
+
"constant",
|
|
100
|
+
"object",
|
|
101
|
+
"enumMember",
|
|
102
|
+
"struct",
|
|
103
|
+
"event",
|
|
104
|
+
"operator",
|
|
105
|
+
"typeParameter",
|
|
106
|
+
"heading",
|
|
107
|
+
] as const satisfies readonly ApiDeclarationKind[];
|
|
108
|
+
const apiDiscoverParams = Type.Object(
|
|
109
|
+
{
|
|
110
|
+
path: Type.String({ description: "Repository, package, or subtree directory" }),
|
|
111
|
+
query: Type.Union([
|
|
112
|
+
Type.Object(
|
|
113
|
+
{ kind: Type.Literal("exactName"), name: Type.String({ minLength: 1 }) },
|
|
114
|
+
{ additionalProperties: false },
|
|
115
|
+
),
|
|
116
|
+
Type.Object(
|
|
117
|
+
{ kind: Type.Literal("prefixName"), name: Type.String({ minLength: 1 }) },
|
|
118
|
+
{ additionalProperties: false },
|
|
119
|
+
),
|
|
120
|
+
Type.Object(
|
|
121
|
+
{ kind: Type.Literal("substringName"), name: Type.String({ minLength: 1 }) },
|
|
122
|
+
{ additionalProperties: false },
|
|
123
|
+
),
|
|
124
|
+
Type.Object(
|
|
125
|
+
{
|
|
126
|
+
kind: Type.Literal("fuzzyName"),
|
|
127
|
+
name: Type.String({ minLength: 1 }),
|
|
128
|
+
maxCandidates: Type.Integer({ minimum: 1, maximum: 10_000 }),
|
|
129
|
+
maxWork: Type.Integer({ minimum: 1, maximum: 1_000_000 }),
|
|
130
|
+
},
|
|
131
|
+
{ additionalProperties: false },
|
|
132
|
+
),
|
|
133
|
+
Type.Object(
|
|
134
|
+
{
|
|
135
|
+
kind: Type.Literal("declarationKind"),
|
|
136
|
+
declarationKind: StringEnum(apiDeclarationKinds),
|
|
137
|
+
},
|
|
138
|
+
{ additionalProperties: false },
|
|
139
|
+
),
|
|
140
|
+
Type.Object(
|
|
141
|
+
{
|
|
142
|
+
kind: Type.Literal("documentation"),
|
|
143
|
+
terms: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
144
|
+
maxCandidates: Type.Integer({ minimum: 1, maximum: 10_000 }),
|
|
145
|
+
maxWork: Type.Integer({ minimum: 1, maximum: 1_000_000 }),
|
|
146
|
+
},
|
|
147
|
+
{ additionalProperties: false },
|
|
148
|
+
),
|
|
149
|
+
]),
|
|
150
|
+
surface: StringEnum(["all", "public", "private", "sourceExport", "packageSurface"] as const, {
|
|
151
|
+
description: "Declaration or export surface to search",
|
|
152
|
+
}),
|
|
153
|
+
resultLimit: Type.Integer({ minimum: 1, maximum: 100 }),
|
|
154
|
+
},
|
|
155
|
+
{ additionalProperties: false },
|
|
156
|
+
);
|
|
157
|
+
const astSearchParams = Type.Object(
|
|
158
|
+
{
|
|
159
|
+
path: Type.String({ description: "Supported source file, repository, package, or subtree" }),
|
|
160
|
+
pattern: Type.String({ minLength: 1, maxLength: 16 * 1024, description: "ast-grep code pattern" }),
|
|
161
|
+
language: Type.Optional(
|
|
162
|
+
StringEnum(
|
|
163
|
+
["typeScript", "tsx", "odin", "go", "rust", "cSharp", "java", "kotlin", "swift", "markdown"] as const,
|
|
164
|
+
{ description: "Explicit pattern and target language; required for directory targets" },
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
resultLimit: Type.Integer({ minimum: 1, maximum: 100 }),
|
|
168
|
+
},
|
|
169
|
+
{ additionalProperties: false },
|
|
170
|
+
);
|
|
171
|
+
const relationshipParams = Type.Object(
|
|
172
|
+
{
|
|
173
|
+
path: Type.String({ description: "Repository, package, or subtree directory" }),
|
|
174
|
+
locator: Type.Integer({ minimum: 1, description: "Numeric declaration locator" }),
|
|
175
|
+
resultLimit: Type.Integer({ minimum: 1, maximum: 100 }),
|
|
176
|
+
},
|
|
177
|
+
{ additionalProperties: false },
|
|
178
|
+
);
|
|
179
|
+
const replaceDeclarationParams = Type.Object(
|
|
180
|
+
{
|
|
181
|
+
locator: Type.Integer({ minimum: 1, description: "Numeric declaration locator" }),
|
|
182
|
+
source: Type.String({
|
|
183
|
+
minLength: 1,
|
|
184
|
+
description: "One complete replacement declaration or Markdown heading section",
|
|
185
|
+
}),
|
|
186
|
+
},
|
|
187
|
+
{ additionalProperties: false },
|
|
188
|
+
);
|
|
189
|
+
const replaceBodyParams = Type.Object(
|
|
190
|
+
{
|
|
191
|
+
locator: Type.Integer({ minimum: 1, description: "Numeric declaration locator" }),
|
|
192
|
+
body: Type.String({
|
|
193
|
+
minLength: 1,
|
|
194
|
+
description: "Complete replacement for the adapter-provided code body or Markdown section content",
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
{ additionalProperties: false },
|
|
198
|
+
);
|
|
199
|
+
const insertDeclarationParams = Type.Object(
|
|
200
|
+
{
|
|
201
|
+
locator: Type.Integer({ minimum: 1, description: "Numeric declaration locator" }),
|
|
202
|
+
position: StringEnum(["before", "after"] as const),
|
|
203
|
+
source: Type.String({ minLength: 1, description: "One complete declaration to insert" }),
|
|
204
|
+
},
|
|
205
|
+
{ additionalProperties: false },
|
|
206
|
+
);
|
|
207
|
+
const renameDeclarationParams = Type.Object(
|
|
208
|
+
{
|
|
209
|
+
locator: Type.Integer({ minimum: 1, description: "Numeric declaration locator" }),
|
|
210
|
+
newName: Type.String({ minLength: 1, description: "One identifier in the target language" }),
|
|
211
|
+
scope: Type.Union([
|
|
212
|
+
Type.Object({ kind: Type.Literal("file") }, { additionalProperties: false }),
|
|
213
|
+
Type.Object(
|
|
214
|
+
{ kind: Type.Literal("repository"), path: Type.String({ minLength: 1 }) },
|
|
215
|
+
{ additionalProperties: false },
|
|
216
|
+
),
|
|
217
|
+
]),
|
|
218
|
+
includeInferred: Type.Boolean({
|
|
219
|
+
description: "Update inferred references as well as exact references",
|
|
220
|
+
}),
|
|
221
|
+
},
|
|
222
|
+
{ additionalProperties: false },
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
interface LocatorEditResult {
|
|
226
|
+
plan: EditPlanResult;
|
|
227
|
+
mutation: ApplyPatchSummary;
|
|
228
|
+
invalidatedLocatorIds: number[];
|
|
229
|
+
freshLocatorIds: number[];
|
|
230
|
+
verificationDiagnostics: string[];
|
|
231
|
+
}
|
|
63
232
|
|
|
64
233
|
interface AstToolDetails {
|
|
65
|
-
kind: "outline" | "symbol";
|
|
66
|
-
result:
|
|
234
|
+
kind: "outline" | "symbol" | "apiDiscover" | "astSearch" | "relationship" | "locatorEdit";
|
|
235
|
+
result:
|
|
236
|
+
| OutlineTargetResult
|
|
237
|
+
| SymbolBatchResult
|
|
238
|
+
| ApiDiscoveryResult
|
|
239
|
+
| AstSearchResult
|
|
240
|
+
| RelationshipResult
|
|
241
|
+
| LocatorEditResult
|
|
242
|
+
| { path: string; summary: RecursiveOutlineSummary; visibleFiles: string[] };
|
|
67
243
|
declarationCount: number;
|
|
68
244
|
sourceBytes: number;
|
|
69
245
|
returnedBytes: number;
|
|
70
246
|
avoidedBytes: number;
|
|
71
247
|
truncated: boolean;
|
|
248
|
+
overflow: BoundedTextOverflowDetails | undefined;
|
|
72
249
|
}
|
|
73
250
|
|
|
74
251
|
interface LocatorRecord {
|
|
@@ -81,9 +258,21 @@ interface LocatorRecord {
|
|
|
81
258
|
generation: number;
|
|
82
259
|
}
|
|
83
260
|
|
|
84
|
-
type OutlineArgs = {
|
|
261
|
+
type OutlineArgs = {
|
|
262
|
+
path: string;
|
|
263
|
+
includePrivate?: boolean;
|
|
264
|
+
includeDocs?: boolean;
|
|
265
|
+
names?: string[];
|
|
266
|
+
recursive?: boolean;
|
|
267
|
+
};
|
|
85
268
|
|
|
86
|
-
export function createAstTools(
|
|
269
|
+
export function createAstTools(
|
|
270
|
+
client: AstClient,
|
|
271
|
+
rowState: ToolRowStateStore,
|
|
272
|
+
temporaryOutput: TemporaryOutputStore,
|
|
273
|
+
orientation: OrientationState,
|
|
274
|
+
emitMutation: (toolCallId: string, cwd: string, summary: ApplyPatchSummary) => void,
|
|
275
|
+
) {
|
|
87
276
|
const locators = new Map<number, LocatorRecord>();
|
|
88
277
|
let nextLocator = 1;
|
|
89
278
|
|
|
@@ -93,14 +282,15 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
93
282
|
declarationCount: number,
|
|
94
283
|
kind: AstToolDetails["kind"],
|
|
95
284
|
result: OutlineTargetResult | SymbolBatchResult,
|
|
96
|
-
): { text: string; details: AstToolDetails } {
|
|
97
|
-
const truncation =
|
|
285
|
+
): { text: string; visibleText: string; details: AstToolDetails } {
|
|
286
|
+
const truncation = truncateBoundedHead(text);
|
|
98
287
|
const returned = truncation.truncated
|
|
99
288
|
? `${truncation.content}\n\n[Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`
|
|
100
289
|
: truncation.content;
|
|
101
290
|
const returnedBytes = Buffer.byteLength(returned);
|
|
102
291
|
return {
|
|
103
292
|
text: returned,
|
|
293
|
+
visibleText: truncation.content,
|
|
104
294
|
details: {
|
|
105
295
|
kind,
|
|
106
296
|
result,
|
|
@@ -109,18 +299,23 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
109
299
|
returnedBytes,
|
|
110
300
|
avoidedBytes: Math.max(0, sourceBytes - returnedBytes),
|
|
111
301
|
truncated: truncation.truncated,
|
|
302
|
+
overflow: undefined,
|
|
112
303
|
},
|
|
113
304
|
};
|
|
114
305
|
}
|
|
115
306
|
|
|
116
307
|
function locator(entry: OutlineEntry, path: string): number {
|
|
117
308
|
if (!entry.locator) throw new Error(`Structural outline row ${entry.name} has no symbol locator`);
|
|
309
|
+
return registerLocator(entry.locator, path, entry.name);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function registerLocator(token: string, path: string, name: string): number {
|
|
118
313
|
const id = nextLocator++;
|
|
119
314
|
const record = {
|
|
120
315
|
id,
|
|
121
|
-
token
|
|
316
|
+
token,
|
|
122
317
|
path: resolve(path),
|
|
123
|
-
name
|
|
318
|
+
name,
|
|
124
319
|
stale: false,
|
|
125
320
|
declarationRetrieved: false,
|
|
126
321
|
generation: client.getGeneration(),
|
|
@@ -129,6 +324,44 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
129
324
|
return id;
|
|
130
325
|
}
|
|
131
326
|
|
|
327
|
+
function editableLocator(id: number): LocatorRecord {
|
|
328
|
+
const record = locators.get(id);
|
|
329
|
+
if (!record) throw new Error(`Unknown declaration locator: ${id}. Run outline or api_discover again.`);
|
|
330
|
+
if (record.stale || record.generation !== client.getGeneration()) {
|
|
331
|
+
throw new Error(`Declaration locator ${id} is stale. Run outline or api_discover again.`);
|
|
332
|
+
}
|
|
333
|
+
return record;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function renderApiCandidate(candidate: ApiCandidate, id: number, cwd: string): string {
|
|
337
|
+
const lines = [
|
|
338
|
+
`${formatPathForDisplay(candidate.definingFile, cwd)}:${displayLineRange(candidate.range)}(${id}): ${candidate.name} — ${candidate.symbolType}`,
|
|
339
|
+
...candidate.signature
|
|
340
|
+
.trim()
|
|
341
|
+
.split("\n")
|
|
342
|
+
.map((line) => ` ${line}`),
|
|
343
|
+
];
|
|
344
|
+
const surface: string[] = [];
|
|
345
|
+
if (candidate.visibility !== "public") surface.push(`visibility=${candidate.visibility}`);
|
|
346
|
+
if (candidate.sourceExport !== "yes") surface.push(`sourceExport=${candidate.sourceExport}`);
|
|
347
|
+
if (candidate.packageSurface !== "yes") surface.push(`packageSurface=${candidate.packageSurface}`);
|
|
348
|
+
if (candidate.internalOnly !== "no") surface.push(`internalOnly=${candidate.internalOnly}`);
|
|
349
|
+
if (surface.length > 0) lines.push(` surface: ${surface.join(", ")}`);
|
|
350
|
+
if (candidate.callerAccess) {
|
|
351
|
+
lines.push(
|
|
352
|
+
` caller: ${candidate.callerAccess.importStatement} use ${candidate.callerAccess.accessExpression}`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
if (candidate.reExportChain.length > 1) lines.push(` re-exports: ${candidate.reExportChain.join(" -> ")}`);
|
|
356
|
+
if (candidate.provenance !== "exact" || candidate.certainty !== "certain" || candidate.certaintyReason) {
|
|
357
|
+
const resolution = [`provenance=${candidate.provenance}`, `parse=${candidate.certainty}`];
|
|
358
|
+
if (candidate.certaintyReason) resolution.push(candidate.certaintyReason);
|
|
359
|
+
lines.push(` resolution: ${resolution.join(", ")}`);
|
|
360
|
+
}
|
|
361
|
+
if (candidate.uncertainty) lines.push(` uncertainty: ${candidate.uncertainty}`);
|
|
362
|
+
return lines.join("\n");
|
|
363
|
+
}
|
|
364
|
+
|
|
132
365
|
function renderEntry(
|
|
133
366
|
entry: OutlineEntry,
|
|
134
367
|
path: string,
|
|
@@ -313,11 +546,7 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
313
546
|
}
|
|
314
547
|
|
|
315
548
|
function renderOutlineFile(file: OutlineFileResult, cwd: string, includeHeader: boolean): string[] {
|
|
316
|
-
const lines = includeHeader
|
|
317
|
-
? [
|
|
318
|
-
`${formatPathForDisplay(file.path, cwd)} (${file.language}, ${file.lineCount} lines, ${formatSize(file.byteLength)})`,
|
|
319
|
-
]
|
|
320
|
-
: [];
|
|
549
|
+
const lines = includeHeader ? [formatPathForDisplay(file.path, cwd)] : [];
|
|
321
550
|
if (file.diagnostics.errorNodes > 0 || file.diagnostics.missingNodes > 0) {
|
|
322
551
|
lines.push(
|
|
323
552
|
`warning: parser recovered with ${file.diagnostics.errorNodes} ERROR and ${file.diagnostics.missingNodes} MISSING nodes`,
|
|
@@ -465,34 +694,195 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
465
694
|
name: "outline",
|
|
466
695
|
label: "outline",
|
|
467
696
|
description:
|
|
468
|
-
"Inspect
|
|
697
|
+
"Inspect declarations in one supported source file, one non-recursive package directory, or a recursive mixed-language subtree without returning implementation bodies. Parenthesized numbers are locators for symbol.",
|
|
469
698
|
promptSnippet: "Inspect public declarations and get symbol locators without reading implementation bodies",
|
|
470
699
|
promptGuidelines: [
|
|
471
|
-
"
|
|
472
|
-
"
|
|
473
|
-
"Leave includeDocs off
|
|
700
|
+
"Set recursive=true to orient an unfamiliar repository or subtree across supported languages.",
|
|
701
|
+
"Leave includePrivate off for surface discovery; enable it only for targeted internal implementation work.",
|
|
702
|
+
"Leave includeDocs off when names and signatures answer the question; enable it only when documentation comments matter.",
|
|
474
703
|
"Treat each parenthesized number after a line range as that declaration's symbol locator.",
|
|
704
|
+
"Use symbol(signatureWithDocs) only when one selected contract needs its attached documentation.",
|
|
475
705
|
"Use symbol with several locators when complete declaration source is needed.",
|
|
476
706
|
],
|
|
477
707
|
parameters: outlineParams,
|
|
478
|
-
async execute(
|
|
479
|
-
const path = resolveExplorePath(ctx.cwd, params.path);
|
|
708
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
709
|
+
const path = await realpath(resolveExplorePath(ctx.cwd, params.path));
|
|
480
710
|
const metadata = await stat(path);
|
|
711
|
+
if (params.recursive) {
|
|
712
|
+
if (!metadata.isDirectory()) throw new Error("recursive outline requires a directory target");
|
|
713
|
+
const names = params.names ?? [];
|
|
714
|
+
const builder = new BoundedTextResultBuilder(temporaryOutput, "completeBlocks");
|
|
715
|
+
const filesByPath = new Map<
|
|
716
|
+
string,
|
|
717
|
+
{ file: OutlineFileResult; locatorIds: number[]; renderedBytes: number }
|
|
718
|
+
>();
|
|
719
|
+
const allLocatorIds = new Set<number>();
|
|
720
|
+
const fatalFallbacks: Array<{
|
|
721
|
+
path: string;
|
|
722
|
+
fingerprint: string;
|
|
723
|
+
message: string;
|
|
724
|
+
code: "outlineFailed" | "resultFrameTooLarge";
|
|
725
|
+
}> = [];
|
|
726
|
+
let declarationCount = 0;
|
|
727
|
+
try {
|
|
728
|
+
const summary = await client.outlineRecursive(
|
|
729
|
+
path,
|
|
730
|
+
params.includePrivate ?? false,
|
|
731
|
+
params.includeDocs ?? false,
|
|
732
|
+
names,
|
|
733
|
+
{
|
|
734
|
+
async onFile(_relativePath, file) {
|
|
735
|
+
const firstLocator = nextLocator;
|
|
736
|
+
const block = renderOutlineFile(file, ctx.cwd, true).join("\n");
|
|
737
|
+
const fileLocatorIds = Array.from(
|
|
738
|
+
{ length: nextLocator - firstLocator },
|
|
739
|
+
(_, index) => firstLocator + index,
|
|
740
|
+
);
|
|
741
|
+
filesByPath.set(file.path, {
|
|
742
|
+
file,
|
|
743
|
+
locatorIds: fileLocatorIds,
|
|
744
|
+
renderedBytes: Buffer.byteLength(block),
|
|
745
|
+
});
|
|
746
|
+
for (const id of fileLocatorIds) allLocatorIds.add(id);
|
|
747
|
+
declarationCount += file.items.reduce(
|
|
748
|
+
(count, item) => count + (item.rowKind === "declaration" ? 1 + item.members.length : 0),
|
|
749
|
+
0,
|
|
750
|
+
);
|
|
751
|
+
await builder.appendBlock(file.path, formatPathForDisplay(file.path, ctx.cwd), block);
|
|
752
|
+
},
|
|
753
|
+
async onDiagnostic(diagnostic) {
|
|
754
|
+
if (
|
|
755
|
+
(diagnostic.code === "outlineFailed" || diagnostic.code === "resultFrameTooLarge") &&
|
|
756
|
+
diagnostic.sourceFingerprint
|
|
757
|
+
) {
|
|
758
|
+
fatalFallbacks.push({
|
|
759
|
+
path: resolve(path, diagnostic.relativePath),
|
|
760
|
+
fingerprint: diagnostic.sourceFingerprint,
|
|
761
|
+
message: diagnostic.message,
|
|
762
|
+
code: diagnostic.code,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
const language = diagnostic.language ? ` (${diagnostic.language})` : "";
|
|
766
|
+
await builder.appendBlock(
|
|
767
|
+
undefined,
|
|
768
|
+
diagnostic.relativePath,
|
|
769
|
+
`diagnostic: ${diagnostic.relativePath}${language} [${diagnostic.code}]: ${diagnostic.message}`,
|
|
770
|
+
);
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
signal,
|
|
774
|
+
);
|
|
775
|
+
const exceptionLines = exceptionSummaryLines([
|
|
776
|
+
...(summary.failedFiles > 0 ||
|
|
777
|
+
summary.unreadableFiles > 0 ||
|
|
778
|
+
summary.oversizedFiles > 0 ||
|
|
779
|
+
summary.parserDegradedFiles > 0
|
|
780
|
+
? [
|
|
781
|
+
`exceptions: ${summary.failedFiles} failed, ${summary.unreadableFiles} unreadable, ${summary.oversizedFiles} oversized, parser degraded ${summary.parserDegradedFiles}`,
|
|
782
|
+
]
|
|
783
|
+
: []),
|
|
784
|
+
...limitReachedLine([
|
|
785
|
+
...(summary.fileLimitReached ? ["file count"] : []),
|
|
786
|
+
...(summary.sourceByteLimitReached ? ["source bytes"] : []),
|
|
787
|
+
...(summary.depthLimitReached ? ["depth"] : []),
|
|
788
|
+
...(summary.elapsedLimitReached ? ["elapsed time"] : []),
|
|
789
|
+
]),
|
|
790
|
+
]);
|
|
791
|
+
if (exceptionLines) await builder.appendRequiredBlock("recursive outline exceptions", exceptionLines);
|
|
792
|
+
const bounded = await builder.finish();
|
|
793
|
+
if (!bounded.overflow.fullOutputComplete) {
|
|
794
|
+
const visible = new Set(bounded.visibleUnitIds);
|
|
795
|
+
for (const [file, { locatorIds: ids }] of filesByPath) {
|
|
796
|
+
if (visible.has(file)) continue;
|
|
797
|
+
for (const id of ids) locators.delete(id);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const returnedBytes = Buffer.byteLength(bounded.content);
|
|
801
|
+
orientation.recordAttempts(
|
|
802
|
+
[...filesByPath.values()].map((record) => ({
|
|
803
|
+
path: record.file.path,
|
|
804
|
+
toolCallId,
|
|
805
|
+
fingerprint: record.file.sourceFingerprint,
|
|
806
|
+
kind: "directOutline",
|
|
807
|
+
sourceBytesDeflected: Math.max(0, record.file.byteLength - record.renderedBytes),
|
|
808
|
+
})),
|
|
809
|
+
);
|
|
810
|
+
for (const fallback of fatalFallbacks) {
|
|
811
|
+
orientation.recordFatal({
|
|
812
|
+
...fallback,
|
|
813
|
+
includePrivate: params.includePrivate ?? false,
|
|
814
|
+
names,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
if (bounded.overflow.temporaryPath) {
|
|
818
|
+
orientation.recordTemporaryOutput(bounded.overflow.temporaryPath);
|
|
819
|
+
}
|
|
820
|
+
orientation.recordOutlineTelemetry(toolCallId, {
|
|
821
|
+
workerInputBytes: summary.totalByteLength,
|
|
822
|
+
completeRenderedBytes: bounded.overflow.totalBytes,
|
|
823
|
+
modelVisibleAstBytes: returnedBytes,
|
|
824
|
+
temporaryOutputBytes: bounded.overflow.temporaryPath ? bounded.overflow.totalBytes : 0,
|
|
825
|
+
});
|
|
826
|
+
return {
|
|
827
|
+
content: [{ type: "text", text: bounded.content }],
|
|
828
|
+
details: {
|
|
829
|
+
kind: "outline",
|
|
830
|
+
result: { path, summary, visibleFiles: bounded.visibleUnitIds },
|
|
831
|
+
declarationCount,
|
|
832
|
+
sourceBytes: summary.totalByteLength,
|
|
833
|
+
returnedBytes,
|
|
834
|
+
avoidedBytes: Math.max(0, summary.totalByteLength - returnedBytes),
|
|
835
|
+
truncated: bounded.overflow.truncated,
|
|
836
|
+
overflow: bounded.overflow,
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
} catch (error) {
|
|
840
|
+
await builder.abort();
|
|
841
|
+
for (const id of allLocatorIds) locators.delete(id);
|
|
842
|
+
throw error;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
481
845
|
const target: OutlineTarget = metadata.isDirectory()
|
|
482
846
|
? { kind: "directory", path }
|
|
483
|
-
: { kind: "file", path, language:
|
|
847
|
+
: { kind: "file", path, language: requireAstLanguageForPath(path) };
|
|
484
848
|
const names = params.names ?? [];
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
849
|
+
let result: OutlineTargetResult;
|
|
850
|
+
try {
|
|
851
|
+
result = await client.outline(
|
|
852
|
+
target,
|
|
853
|
+
params.includePrivate ?? false,
|
|
854
|
+
params.includeDocs ?? false,
|
|
855
|
+
names,
|
|
856
|
+
signal,
|
|
857
|
+
);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
if (
|
|
860
|
+
target.kind === "file" &&
|
|
861
|
+
error instanceof AstWorkerError &&
|
|
862
|
+
(error.code === "outline_failed" || error.code === "response_too_large") &&
|
|
863
|
+
error.sourceFingerprint
|
|
864
|
+
) {
|
|
865
|
+
orientation.recordFatal({
|
|
866
|
+
path,
|
|
867
|
+
fingerprint: error.sourceFingerprint,
|
|
868
|
+
includePrivate: params.includePrivate ?? false,
|
|
869
|
+
names,
|
|
870
|
+
code: error.code,
|
|
871
|
+
message: error.message,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
throw error;
|
|
875
|
+
}
|
|
876
|
+
const renderedFiles = result.files.map((file) => {
|
|
877
|
+
const firstLocator = nextLocator;
|
|
878
|
+
const text = renderOutlineFile(file, ctx.cwd, result.files.length > 1).join("\n");
|
|
879
|
+
return {
|
|
880
|
+
file,
|
|
881
|
+
text,
|
|
882
|
+
locatorIds: Array.from({ length: nextLocator - firstLocator }, (_, index) => firstLocator + index),
|
|
883
|
+
};
|
|
884
|
+
});
|
|
885
|
+
const lines = renderedFiles.flatMap((file, index) => [...(index === 0 ? [] : [""]), file.text]);
|
|
496
886
|
const declarationCount = result.files.reduce(
|
|
497
887
|
(count, file) =>
|
|
498
888
|
count +
|
|
@@ -503,7 +893,26 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
503
893
|
0,
|
|
504
894
|
);
|
|
505
895
|
if (declarationCount === 0) lines.push(names.length > 0 ? "No matching declarations" : "No declarations");
|
|
506
|
-
const
|
|
896
|
+
const completeText = lines.join("\n");
|
|
897
|
+
const output = compact(completeText, result.totalByteLength, declarationCount, "outline", result);
|
|
898
|
+
orientation.recordAttempts(
|
|
899
|
+
renderedFiles.map((rendered) => {
|
|
900
|
+
const renderedBytes = Buffer.byteLength(rendered.text);
|
|
901
|
+
return {
|
|
902
|
+
path: rendered.file.path,
|
|
903
|
+
toolCallId,
|
|
904
|
+
fingerprint: rendered.file.sourceFingerprint,
|
|
905
|
+
kind: "directOutline" as const,
|
|
906
|
+
sourceBytesDeflected: Math.max(0, rendered.file.byteLength - renderedBytes),
|
|
907
|
+
};
|
|
908
|
+
}),
|
|
909
|
+
);
|
|
910
|
+
orientation.recordOutlineTelemetry(toolCallId, {
|
|
911
|
+
workerInputBytes: result.totalByteLength,
|
|
912
|
+
completeRenderedBytes: Buffer.byteLength(completeText),
|
|
913
|
+
modelVisibleAstBytes: output.details.returnedBytes,
|
|
914
|
+
temporaryOutputBytes: 0,
|
|
915
|
+
});
|
|
507
916
|
return { content: [{ type: "text", text: output.text }], details: output.details };
|
|
508
917
|
},
|
|
509
918
|
renderCall(args, theme, context) {
|
|
@@ -531,10 +940,10 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
531
940
|
name: "symbol",
|
|
532
941
|
label: "symbol",
|
|
533
942
|
description:
|
|
534
|
-
"Return
|
|
535
|
-
promptSnippet: "Retrieve exact
|
|
943
|
+
"Return signatures, documented signatures, exact declarations, or declarations with required imports for numeric outline locators; stale locators fail atomically.",
|
|
944
|
+
promptSnippet: "Retrieve signatures or exact declarations for several outline locators",
|
|
536
945
|
parameters: symbolParams,
|
|
537
|
-
async execute(
|
|
946
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
538
947
|
if (params.contextLines !== undefined && params.view !== "declaration") {
|
|
539
948
|
throw new Error("contextLines is supported only with view=declaration");
|
|
540
949
|
}
|
|
@@ -553,6 +962,15 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
553
962
|
params.contextLines ?? 0,
|
|
554
963
|
signal,
|
|
555
964
|
);
|
|
965
|
+
orientation.recordAttempts(
|
|
966
|
+
result.declarations.map((declaration) => ({
|
|
967
|
+
path: declaration.path,
|
|
968
|
+
fingerprint: declaration.sourceFingerprint,
|
|
969
|
+
kind: "symbol" as const,
|
|
970
|
+
toolCallId,
|
|
971
|
+
sourceBytesDeflected: 0,
|
|
972
|
+
})),
|
|
973
|
+
);
|
|
556
974
|
const requestedByToken = new Map<string, LocatorRecord[]>();
|
|
557
975
|
for (const record of records) {
|
|
558
976
|
const requested = requestedByToken.get(record.token);
|
|
@@ -572,6 +990,12 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
572
990
|
lines.push(
|
|
573
991
|
...(includePaths ? [formatPathForDisplay(block.path, ctx.cwd)] : []),
|
|
574
992
|
`${lineRange}(${represented.map((record) => record.id).join(",")}): ${represented.map((record) => record.name).join(", ")}`,
|
|
993
|
+
...block.declarationIndexes.flatMap((index) => {
|
|
994
|
+
const declaration = result.declarations[index];
|
|
995
|
+
if (!declaration) return [];
|
|
996
|
+
const ids = (requestedByToken.get(declaration.locator) ?? []).map((record) => record.id).join(",");
|
|
997
|
+
return declaration.diagnostics.map((diagnostic) => `warning (${ids}): ${diagnostic}`);
|
|
998
|
+
}),
|
|
575
999
|
block.source,
|
|
576
1000
|
);
|
|
577
1001
|
}
|
|
@@ -605,22 +1029,747 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
605
1029
|
},
|
|
606
1030
|
});
|
|
607
1031
|
|
|
1032
|
+
async function executeLocatorEdit(
|
|
1033
|
+
toolCallId: string,
|
|
1034
|
+
locatorId: number,
|
|
1035
|
+
edit: EditOperation,
|
|
1036
|
+
signal: AbortSignal | undefined,
|
|
1037
|
+
cwd: string,
|
|
1038
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; details: AstToolDetails }> {
|
|
1039
|
+
const record = editableLocator(locatorId);
|
|
1040
|
+
const plan = await client.planEdit(record.token, edit, signal);
|
|
1041
|
+
const mutation = await applyExactSourceMutations(cwd, plan.files, signal);
|
|
1042
|
+
if (mutation.changes.length === 0) {
|
|
1043
|
+
const message = mutation.failures.map((failure) => failure.message).join("; ") || "No files changed.";
|
|
1044
|
+
throw new Error(`Locator edit failed: ${message}`);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const changedPaths = new Set(mutation.changes.map((change) => resolve(cwd, change.path)));
|
|
1048
|
+
const invalidatedLocatorIds: number[] = [];
|
|
1049
|
+
for (const candidate of locators.values()) {
|
|
1050
|
+
if (changedPaths.has(candidate.path) && !candidate.stale) {
|
|
1051
|
+
candidate.stale = true;
|
|
1052
|
+
invalidatedLocatorIds.push(candidate.id);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
orientation.invalidate([...changedPaths]);
|
|
1056
|
+
emitMutation(toolCallId, cwd, mutation);
|
|
1057
|
+
|
|
1058
|
+
const verificationDiagnostics: string[] = [];
|
|
1059
|
+
const verifiedTokens = new Set<string>();
|
|
1060
|
+
if (mutation.status === "completed") {
|
|
1061
|
+
for (const filePlan of plan.files) {
|
|
1062
|
+
try {
|
|
1063
|
+
const reparsed = await client.outline(
|
|
1064
|
+
{ kind: "file", path: filePlan.path, language: requireAstLanguageForPath(filePlan.path) },
|
|
1065
|
+
true,
|
|
1066
|
+
false,
|
|
1067
|
+
[],
|
|
1068
|
+
signal,
|
|
1069
|
+
);
|
|
1070
|
+
const file = reparsed.files[0];
|
|
1071
|
+
if (!file || file.sourceFingerprint !== sourceFingerprintForPlan(filePlan.source)) {
|
|
1072
|
+
verificationDiagnostics.push(`${filePlan.path}: post-mutation fingerprint did not match the plan`);
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
if (file.diagnostics.errorNodes > 0 || file.diagnostics.missingNodes > 0) {
|
|
1076
|
+
verificationDiagnostics.push(
|
|
1077
|
+
`${filePlan.path}: parser recovered with ${file.diagnostics.errorNodes} ERROR and ${file.diagnostics.missingNodes} MISSING nodes`,
|
|
1078
|
+
);
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
for (const item of file.items) {
|
|
1082
|
+
if (item.certainty === "certain" && item.locator) verifiedTokens.add(item.locator);
|
|
1083
|
+
for (const member of item.members) {
|
|
1084
|
+
if (member.certainty === "certain" && member.locator) verifiedTokens.add(member.locator);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
verificationDiagnostics.push(
|
|
1089
|
+
`${filePlan.path}: post-mutation reparse failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
const freshLocatorIds = plan.freshLocators
|
|
1095
|
+
.filter((fresh) => verifiedTokens.has(fresh.locator))
|
|
1096
|
+
.map((fresh) => registerLocator(fresh.locator, fresh.path, fresh.name));
|
|
1097
|
+
const freshTargetId = freshLocatorIds[0];
|
|
1098
|
+
|
|
1099
|
+
const lines = [
|
|
1100
|
+
`changed: ${mutation.changes.map((change) => change.path).join(", ")}`,
|
|
1101
|
+
`invalidated locators: ${invalidatedLocatorIds.join(", ") || "none"}; fresh locators: ${freshLocatorIds.map((id) => `(${id})`).join(", ") || "none"}; skipped impacts: ${plan.skippedImpacts.length}; status: ${mutation.status}`,
|
|
1102
|
+
];
|
|
1103
|
+
for (const skipped of plan.skippedImpacts) {
|
|
1104
|
+
const candidateIds = skipped.candidateLocators.flatMap((token, candidateIndex) => {
|
|
1105
|
+
if (token === record.token && freshTargetId !== undefined) return [freshTargetId];
|
|
1106
|
+
const existing = [...locators.values()].find((candidate) => candidate.token === token && !candidate.stale);
|
|
1107
|
+
if (existing) return [existing.id];
|
|
1108
|
+
const candidatePath = skipped.candidatePaths[candidateIndex];
|
|
1109
|
+
return candidatePath && !changedPaths.has(resolve(candidatePath))
|
|
1110
|
+
? [registerLocator(token, candidatePath, "rename candidate")]
|
|
1111
|
+
: [];
|
|
1112
|
+
});
|
|
1113
|
+
lines.push(
|
|
1114
|
+
`skipped: ${formatPathForDisplay(skipped.path, cwd)}:${displayLineRange(skipped.range)} [${skipped.reason}]${candidateIds.length > 0 ? ` candidates ${candidateIds.map((id) => `(${id})`).join(", ")}` : ""}`,
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
for (const diagnostic of verificationDiagnostics) lines.push(`warning: ${diagnostic}`);
|
|
1118
|
+
const text = lines.join("\n");
|
|
1119
|
+
const sourceBytes = plan.files.reduce((total, file) => total + Buffer.byteLength(file.source), 0);
|
|
1120
|
+
const returnedBytes = Buffer.byteLength(text);
|
|
1121
|
+
return {
|
|
1122
|
+
content: [{ type: "text", text }],
|
|
1123
|
+
details: {
|
|
1124
|
+
kind: "locatorEdit",
|
|
1125
|
+
result: { plan, mutation, invalidatedLocatorIds, freshLocatorIds, verificationDiagnostics },
|
|
1126
|
+
declarationCount: freshLocatorIds.length,
|
|
1127
|
+
sourceBytes,
|
|
1128
|
+
returnedBytes,
|
|
1129
|
+
avoidedBytes: Math.max(0, sourceBytes - returnedBytes),
|
|
1130
|
+
truncated: false,
|
|
1131
|
+
overflow: undefined,
|
|
1132
|
+
},
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const replaceDeclaration = defineTool<typeof replaceDeclarationParams, AstToolDetails>({
|
|
1137
|
+
name: "replace_declaration",
|
|
1138
|
+
label: "replace_declaration",
|
|
1139
|
+
description:
|
|
1140
|
+
"Replace one complete declaration through its stale-safe numeric locator. Code must parse as one declaration in the current parent container. A Markdown heading locator replaces the heading and its entire section, including deeper subsections, and requires one root heading at the same depth.",
|
|
1141
|
+
promptSnippet: "Replace one complete declaration by structural locator",
|
|
1142
|
+
parameters: replaceDeclarationParams,
|
|
1143
|
+
executionMode: "sequential",
|
|
1144
|
+
execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1145
|
+
return executeLocatorEdit(
|
|
1146
|
+
toolCallId,
|
|
1147
|
+
params.locator,
|
|
1148
|
+
{ kind: "replaceDeclaration", source: params.source },
|
|
1149
|
+
signal,
|
|
1150
|
+
ctx.cwd,
|
|
1151
|
+
);
|
|
1152
|
+
},
|
|
1153
|
+
renderCall(args, theme, context) {
|
|
1154
|
+
return locatorEditCall("replace_declaration", args.locator, locators, rowState, theme, context);
|
|
1155
|
+
},
|
|
1156
|
+
renderResult(result, options, theme, context) {
|
|
1157
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1158
|
+
},
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
const replaceBody = defineTool<typeof replaceBodyParams, AstToolDetails>({
|
|
1162
|
+
name: "replace_body",
|
|
1163
|
+
label: "replace_body",
|
|
1164
|
+
description:
|
|
1165
|
+
"Replace exactly the reliable adapter-provided body range while preserving its signature and attached metadata. For a Markdown heading locator, preserve the heading and replace its section content; nested headings must be deeper than the selected heading.",
|
|
1166
|
+
promptSnippet: "Replace one executable body by structural locator",
|
|
1167
|
+
parameters: replaceBodyParams,
|
|
1168
|
+
executionMode: "sequential",
|
|
1169
|
+
execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1170
|
+
return executeLocatorEdit(
|
|
1171
|
+
toolCallId,
|
|
1172
|
+
params.locator,
|
|
1173
|
+
{ kind: "replaceBody", body: params.body },
|
|
1174
|
+
signal,
|
|
1175
|
+
ctx.cwd,
|
|
1176
|
+
);
|
|
1177
|
+
},
|
|
1178
|
+
renderCall(args, theme, context) {
|
|
1179
|
+
return locatorEditCall("replace_body", args.locator, locators, rowState, theme, context);
|
|
1180
|
+
},
|
|
1181
|
+
renderResult(result, options, theme, context) {
|
|
1182
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1183
|
+
},
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
const insertDeclaration = defineTool<typeof insertDeclarationParams, AstToolDetails>({
|
|
1187
|
+
name: "insert_declaration",
|
|
1188
|
+
label: "insert_declaration",
|
|
1189
|
+
description:
|
|
1190
|
+
"Insert exactly one declaration immediately before or after a declaration locator in its current parent container.",
|
|
1191
|
+
promptSnippet: "Insert one declaration adjacent to a structural locator",
|
|
1192
|
+
parameters: insertDeclarationParams,
|
|
1193
|
+
executionMode: "sequential",
|
|
1194
|
+
execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1195
|
+
return executeLocatorEdit(
|
|
1196
|
+
toolCallId,
|
|
1197
|
+
params.locator,
|
|
1198
|
+
{ kind: "insertDeclaration", position: params.position, source: params.source },
|
|
1199
|
+
signal,
|
|
1200
|
+
ctx.cwd,
|
|
1201
|
+
);
|
|
1202
|
+
},
|
|
1203
|
+
renderCall(args, theme, context) {
|
|
1204
|
+
return locatorEditCall("insert_declaration", args.locator, locators, rowState, theme, context, args.position);
|
|
1205
|
+
},
|
|
1206
|
+
renderResult(result, options, theme, context) {
|
|
1207
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1208
|
+
},
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
const renameDeclaration = defineTool<typeof renameDeclarationParams, AstToolDetails>({
|
|
1212
|
+
name: "rename_declaration",
|
|
1213
|
+
label: "rename_declaration",
|
|
1214
|
+
description:
|
|
1215
|
+
"Rename one declaration and exact references within an explicit file or repository scope. Inferred references require includeInferred=true; ambiguous references remain unchanged.",
|
|
1216
|
+
promptSnippet: "Rename one declaration through stale-safe structural identity",
|
|
1217
|
+
parameters: renameDeclarationParams,
|
|
1218
|
+
executionMode: "sequential",
|
|
1219
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1220
|
+
const scope =
|
|
1221
|
+
params.scope.kind === "repository"
|
|
1222
|
+
? { kind: "repository" as const, path: await realpath(resolveExplorePath(ctx.cwd, params.scope.path)) }
|
|
1223
|
+
: { kind: "file" as const };
|
|
1224
|
+
return executeLocatorEdit(
|
|
1225
|
+
toolCallId,
|
|
1226
|
+
params.locator,
|
|
1227
|
+
{ kind: "renameDeclaration", newName: params.newName, scope, includeInferred: params.includeInferred },
|
|
1228
|
+
signal,
|
|
1229
|
+
ctx.cwd,
|
|
1230
|
+
);
|
|
1231
|
+
},
|
|
1232
|
+
renderCall(args, theme, context) {
|
|
1233
|
+
return locatorEditCall(
|
|
1234
|
+
"rename_declaration",
|
|
1235
|
+
args.locator,
|
|
1236
|
+
locators,
|
|
1237
|
+
rowState,
|
|
1238
|
+
theme,
|
|
1239
|
+
context,
|
|
1240
|
+
`${args.newName} ${args.scope.kind}${args.includeInferred ? " inferred" : ""}`,
|
|
1241
|
+
);
|
|
1242
|
+
},
|
|
1243
|
+
renderResult(result, options, theme, context) {
|
|
1244
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1245
|
+
},
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
const apiDiscover = defineTool<typeof apiDiscoverParams, AstToolDetails>({
|
|
1249
|
+
name: "api_discover",
|
|
1250
|
+
label: "api_discover",
|
|
1251
|
+
description:
|
|
1252
|
+
"Discover declarations and supported caller import paths across a canonical repository, package, or subtree without returning implementation bodies. Results include numeric symbol locators.",
|
|
1253
|
+
promptSnippet: "Find reusable declarations and caller import paths across a repository or subtree",
|
|
1254
|
+
promptGuidelines: [
|
|
1255
|
+
"Use api_discover when reuse intent is known but the declaration path or exact name is not.",
|
|
1256
|
+
"Choose exactly one query kind and keep fuzzy or documentation work limits narrow.",
|
|
1257
|
+
"Use packageSurface when the caller needs a supported public import path.",
|
|
1258
|
+
"Treat a clear candidate signature as sufficient; retrieve a selected locator with symbol only when its contract or implementation needs closer inspection.",
|
|
1259
|
+
],
|
|
1260
|
+
parameters: apiDiscoverParams,
|
|
1261
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1262
|
+
const path = await realpath(resolveExplorePath(ctx.cwd, params.path));
|
|
1263
|
+
if (!(await stat(path)).isDirectory()) throw new Error("api_discover requires a directory scope");
|
|
1264
|
+
const result = await client.discoverApi(
|
|
1265
|
+
path,
|
|
1266
|
+
params.query as ApiQuery,
|
|
1267
|
+
params.surface as ApiSurfaceFilter,
|
|
1268
|
+
params.resultLimit,
|
|
1269
|
+
signal,
|
|
1270
|
+
);
|
|
1271
|
+
orientation.recordAttempts(
|
|
1272
|
+
result.candidates.map((candidate) => ({
|
|
1273
|
+
path: candidate.definingFile,
|
|
1274
|
+
fingerprint: candidate.sourceFingerprint,
|
|
1275
|
+
kind: "apiCandidate" as const,
|
|
1276
|
+
toolCallId,
|
|
1277
|
+
sourceBytesDeflected: 0,
|
|
1278
|
+
})),
|
|
1279
|
+
);
|
|
1280
|
+
const builder = new BoundedTextResultBuilder(temporaryOutput, "completeBlocks");
|
|
1281
|
+
const locatorByUnit = new Map<string, number>();
|
|
1282
|
+
try {
|
|
1283
|
+
for (const [index, candidate] of result.candidates.entries()) {
|
|
1284
|
+
const id = registerLocator(candidate.locator, candidate.definingFile, candidate.name);
|
|
1285
|
+
const unit = `${candidate.definingFile}:${candidate.range.startByte}:${index}`;
|
|
1286
|
+
locatorByUnit.set(unit, id);
|
|
1287
|
+
await builder.appendBlock(unit, candidate.name, renderApiCandidate(candidate, id, ctx.cwd));
|
|
1288
|
+
}
|
|
1289
|
+
if (result.candidates.length === 0)
|
|
1290
|
+
await builder.appendRequiredBlock("no matches", "No matching declarations");
|
|
1291
|
+
const summary = result.summary;
|
|
1292
|
+
const exceptionLines = exceptionSummaryLines([
|
|
1293
|
+
...(summary.omittedCandidates > 0
|
|
1294
|
+
? [`${summary.omittedCandidates} candidates omitted (result limit ${summary.resultLimit})`]
|
|
1295
|
+
: []),
|
|
1296
|
+
...(summary.resolutionDiagnostics > 0
|
|
1297
|
+
? [`resolution diagnostics: ${summary.resolutionDiagnostics}`]
|
|
1298
|
+
: []),
|
|
1299
|
+
...limitReachedLine([
|
|
1300
|
+
...(summary.candidateLimitReached ? ["query candidates"] : []),
|
|
1301
|
+
...(summary.workLimitReached ? ["query work"] : []),
|
|
1302
|
+
...(summary.fileLimitReached ? ["files"] : []),
|
|
1303
|
+
...(summary.sourceByteLimitReached ? ["source bytes"] : []),
|
|
1304
|
+
...(summary.depthLimitReached ? ["depth"] : []),
|
|
1305
|
+
...(summary.elapsedLimitReached ? ["elapsed time"] : []),
|
|
1306
|
+
]),
|
|
1307
|
+
]);
|
|
1308
|
+
if (exceptionLines) await builder.appendRequiredBlock("API discovery exceptions", exceptionLines);
|
|
1309
|
+
const bounded = await builder.finish();
|
|
1310
|
+
if (!bounded.overflow.fullOutputComplete) {
|
|
1311
|
+
const visible = new Set(bounded.visibleUnitIds);
|
|
1312
|
+
for (const [unit, id] of locatorByUnit) if (!visible.has(unit)) locators.delete(id);
|
|
1313
|
+
}
|
|
1314
|
+
if (bounded.overflow.temporaryPath) orientation.recordTemporaryOutput(bounded.overflow.temporaryPath);
|
|
1315
|
+
const returnedBytes = Buffer.byteLength(bounded.content);
|
|
1316
|
+
return {
|
|
1317
|
+
content: [{ type: "text", text: bounded.content }],
|
|
1318
|
+
details: {
|
|
1319
|
+
kind: "apiDiscover",
|
|
1320
|
+
result,
|
|
1321
|
+
declarationCount: result.candidates.length,
|
|
1322
|
+
sourceBytes: summary.totalSourceBytes,
|
|
1323
|
+
returnedBytes,
|
|
1324
|
+
avoidedBytes: Math.max(0, summary.totalSourceBytes - returnedBytes),
|
|
1325
|
+
truncated: bounded.overflow.truncated,
|
|
1326
|
+
overflow: bounded.overflow,
|
|
1327
|
+
},
|
|
1328
|
+
};
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
await builder.abort();
|
|
1331
|
+
for (const id of locatorByUnit.values()) locators.delete(id);
|
|
1332
|
+
throw error;
|
|
1333
|
+
}
|
|
1334
|
+
},
|
|
1335
|
+
renderCall(args, theme, context) {
|
|
1336
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1337
|
+
const query = formatApiQuery(args.query as ApiQuery);
|
|
1338
|
+
const component =
|
|
1339
|
+
(context.lastComponent as AstCallComponent | undefined) ??
|
|
1340
|
+
new AstCallComponent(
|
|
1341
|
+
rowState,
|
|
1342
|
+
context.toolCallId,
|
|
1343
|
+
"api_discover",
|
|
1344
|
+
[stripLeadingAt(args.path)],
|
|
1345
|
+
[`[${query} ${args.surface} limit=${args.resultLimit}]`],
|
|
1346
|
+
theme,
|
|
1347
|
+
);
|
|
1348
|
+
component.set([stripLeadingAt(args.path)], [`[${query} ${args.surface} limit=${args.resultLimit}]`], theme);
|
|
1349
|
+
return component;
|
|
1350
|
+
},
|
|
1351
|
+
renderResult(result, options, theme, context) {
|
|
1352
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1353
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1354
|
+
},
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
const astSearch = defineTool<typeof astSearchParams, AstToolDetails>({
|
|
1358
|
+
name: "ast_search",
|
|
1359
|
+
label: "ast_search",
|
|
1360
|
+
description:
|
|
1361
|
+
"Search one canonical source file, repository, package, or subtree with an ast-grep code pattern. Returns bounded deterministic matches, metavariable bindings, parser certainty, and numeric locators for exact source or enclosing scopes.",
|
|
1362
|
+
promptSnippet: "Search repository code shapes with ast-grep patterns and retrievable locators",
|
|
1363
|
+
promptGuidelines: [
|
|
1364
|
+
"Use ast_search for code shapes; use grep for literal text.",
|
|
1365
|
+
"Pass language for repository, package, and subtree targets. Supported files can infer it.",
|
|
1366
|
+
"Use $NAME and $$$NAME metavariables. Keep resultLimit narrow, then retrieve selected numeric locators with symbol(declaration).",
|
|
1367
|
+
],
|
|
1368
|
+
parameters: astSearchParams,
|
|
1369
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1370
|
+
const path = await realpath(resolveExplorePath(ctx.cwd, params.path));
|
|
1371
|
+
const metadata = await stat(path);
|
|
1372
|
+
const inferred = metadata.isFile() ? astLanguageForPath(path) : undefined;
|
|
1373
|
+
if (metadata.isFile() && !inferred) throw new Error(`Unsupported ast_search file type: ${path}`);
|
|
1374
|
+
if (metadata.isDirectory() && !params.language) {
|
|
1375
|
+
throw new Error("ast_search requires language for repository, package, and subtree targets");
|
|
1376
|
+
}
|
|
1377
|
+
if (inferred && params.language && inferred !== params.language) {
|
|
1378
|
+
throw new Error(`ast_search language ${params.language} does not match ${inferred} target ${path}`);
|
|
1379
|
+
}
|
|
1380
|
+
const language = params.language ?? inferred;
|
|
1381
|
+
if (!language) throw new Error("ast_search could not infer a language; pass language explicitly");
|
|
1382
|
+
const result = await client.search(path, language, params.pattern, params.resultLimit, signal);
|
|
1383
|
+
orientation.recordAttempts([
|
|
1384
|
+
...(metadata.isFile() && result.targetSourceFingerprint
|
|
1385
|
+
? [
|
|
1386
|
+
{
|
|
1387
|
+
path,
|
|
1388
|
+
fingerprint: result.targetSourceFingerprint,
|
|
1389
|
+
kind: "structuralMatch" as const,
|
|
1390
|
+
toolCallId,
|
|
1391
|
+
sourceBytesDeflected: 0,
|
|
1392
|
+
},
|
|
1393
|
+
]
|
|
1394
|
+
: []),
|
|
1395
|
+
...result.matches.map((match) => ({
|
|
1396
|
+
path: metadata.isDirectory() ? resolve(path, match.relativePath) : path,
|
|
1397
|
+
fingerprint: match.sourceFingerprint,
|
|
1398
|
+
kind: "structuralMatch" as const,
|
|
1399
|
+
toolCallId,
|
|
1400
|
+
sourceBytesDeflected: 0,
|
|
1401
|
+
})),
|
|
1402
|
+
]);
|
|
1403
|
+
const builder = new BoundedTextResultBuilder(temporaryOutput, "completeBlocks");
|
|
1404
|
+
const locatorByUnit = new Map<string, number[]>();
|
|
1405
|
+
try {
|
|
1406
|
+
for (const [index, match] of result.matches.entries()) {
|
|
1407
|
+
const sourcePath = metadata.isDirectory() ? resolve(path, match.relativePath) : path;
|
|
1408
|
+
const matchId = registerLocator(match.locator, sourcePath, `match ${match.relativePath}`);
|
|
1409
|
+
const ids = [matchId];
|
|
1410
|
+
const unit = `${match.relativePath}:${match.range.startByte}:${index}`;
|
|
1411
|
+
const lines = [
|
|
1412
|
+
`${match.relativePath}:${displayLineRange(match.range)}(${matchId}) [${match.language}, ${match.certainty}]`,
|
|
1413
|
+
...match.preview.split("\n").map((line) => ` ${line}`),
|
|
1414
|
+
];
|
|
1415
|
+
if (match.previewTruncated) lines.push(" [preview truncated]");
|
|
1416
|
+
for (const binding of match.bindings) {
|
|
1417
|
+
const values = binding.values.map((value) => JSON.stringify(value.preview)).join(", ");
|
|
1418
|
+
lines.push(` $${binding.name} = ${values}${binding.valuesTruncated ? ", …" : ""}`);
|
|
1419
|
+
}
|
|
1420
|
+
if (match.bindingsTruncated) lines.push(" [bindings truncated]");
|
|
1421
|
+
if (match.certaintyReason) lines.push(` uncertainty: ${match.certaintyReason}`);
|
|
1422
|
+
if (match.enclosingScope) {
|
|
1423
|
+
const scopeId = registerLocator(
|
|
1424
|
+
match.enclosingScope.locator,
|
|
1425
|
+
sourcePath,
|
|
1426
|
+
match.enclosingScope.astKind,
|
|
1427
|
+
);
|
|
1428
|
+
ids.push(scopeId);
|
|
1429
|
+
lines.push(
|
|
1430
|
+
` enclosing ${displayLineRange(match.enclosingScope.range)}(${scopeId}): ${match.enclosingScope.astKind}`,
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
locatorByUnit.set(unit, ids);
|
|
1434
|
+
await builder.appendBlock(unit, match.relativePath, lines.join("\n"));
|
|
1435
|
+
}
|
|
1436
|
+
if (result.matches.length === 0) await builder.appendRequiredBlock("no matches", "No structural matches");
|
|
1437
|
+
for (const [index, diagnostic] of result.diagnostics.entries()) {
|
|
1438
|
+
await builder.appendBlock(
|
|
1439
|
+
undefined,
|
|
1440
|
+
`diagnostic-${index}`,
|
|
1441
|
+
`diagnostic: ${diagnostic.relativePath} [${diagnostic.code}]: ${diagnostic.message}`,
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
const summary = result.summary;
|
|
1445
|
+
const exceptionLines = exceptionSummaryLines([
|
|
1446
|
+
...(summary.matchesFound > summary.matchesReturned
|
|
1447
|
+
? [
|
|
1448
|
+
`${summary.matchesFound - summary.matchesReturned} matches omitted (result limit ${summary.resultLimit})`,
|
|
1449
|
+
]
|
|
1450
|
+
: []),
|
|
1451
|
+
...(summary.unreadableFiles > 0 ||
|
|
1452
|
+
summary.oversizedFiles > 0 ||
|
|
1453
|
+
summary.failedFiles > 0 ||
|
|
1454
|
+
summary.parserDegradedFiles > 0 ||
|
|
1455
|
+
summary.diagnosticsOmitted > 0
|
|
1456
|
+
? [
|
|
1457
|
+
`exceptions: ${summary.unreadableFiles} unreadable, ${summary.oversizedFiles} oversized, ${summary.failedFiles} failed, parser degraded ${summary.parserDegradedFiles}, diagnostics omitted ${summary.diagnosticsOmitted}`,
|
|
1458
|
+
]
|
|
1459
|
+
: []),
|
|
1460
|
+
...limitReachedLine([
|
|
1461
|
+
...(summary.resultLimitReached ? ["results"] : []),
|
|
1462
|
+
...(summary.fileLimitReached ? ["files"] : []),
|
|
1463
|
+
...(summary.sourceByteLimitReached ? ["source bytes"] : []),
|
|
1464
|
+
...(summary.depthLimitReached ? ["depth"] : []),
|
|
1465
|
+
...(summary.elapsedLimitReached ? ["elapsed time"] : []),
|
|
1466
|
+
]),
|
|
1467
|
+
]);
|
|
1468
|
+
if (exceptionLines) await builder.appendRequiredBlock("structural search exceptions", exceptionLines);
|
|
1469
|
+
const bounded = await builder.finish();
|
|
1470
|
+
if (!bounded.overflow.fullOutputComplete) {
|
|
1471
|
+
const visible = new Set(bounded.visibleUnitIds);
|
|
1472
|
+
for (const [unit, ids] of locatorByUnit) {
|
|
1473
|
+
if (!visible.has(unit)) for (const id of ids) locators.delete(id);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (bounded.overflow.temporaryPath) orientation.recordTemporaryOutput(bounded.overflow.temporaryPath);
|
|
1477
|
+
const returnedBytes = Buffer.byteLength(bounded.content);
|
|
1478
|
+
return {
|
|
1479
|
+
content: [{ type: "text", text: bounded.content }],
|
|
1480
|
+
details: {
|
|
1481
|
+
kind: "astSearch",
|
|
1482
|
+
result,
|
|
1483
|
+
declarationCount: summary.matchesReturned,
|
|
1484
|
+
sourceBytes: summary.sourceBytes,
|
|
1485
|
+
returnedBytes,
|
|
1486
|
+
avoidedBytes: Math.max(0, summary.sourceBytes - returnedBytes),
|
|
1487
|
+
truncated: bounded.overflow.truncated,
|
|
1488
|
+
overflow: bounded.overflow,
|
|
1489
|
+
},
|
|
1490
|
+
};
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
await builder.abort();
|
|
1493
|
+
for (const ids of locatorByUnit.values()) for (const id of ids) locators.delete(id);
|
|
1494
|
+
throw error;
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
renderCall(args, theme, context) {
|
|
1498
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1499
|
+
const options = `[${args.language ?? "infer"} limit=${args.resultLimit}]`;
|
|
1500
|
+
const component =
|
|
1501
|
+
(context.lastComponent as AstCallComponent | undefined) ??
|
|
1502
|
+
new AstCallComponent(
|
|
1503
|
+
rowState,
|
|
1504
|
+
context.toolCallId,
|
|
1505
|
+
"ast_search",
|
|
1506
|
+
[stripLeadingAt(args.path)],
|
|
1507
|
+
[options],
|
|
1508
|
+
theme,
|
|
1509
|
+
);
|
|
1510
|
+
component.set([stripLeadingAt(args.path)], [options], theme);
|
|
1511
|
+
return component;
|
|
1512
|
+
},
|
|
1513
|
+
renderResult(result, options, theme, context) {
|
|
1514
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1515
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1516
|
+
},
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
function relationshipTool(name: RelationshipOperation, description: string, guideline: string) {
|
|
1520
|
+
return defineTool<typeof relationshipParams, AstToolDetails>({
|
|
1521
|
+
name,
|
|
1522
|
+
label: name,
|
|
1523
|
+
description,
|
|
1524
|
+
promptSnippet: description,
|
|
1525
|
+
promptGuidelines: [guideline, "Inspect ambiguous candidate locators before selecting an edit target."],
|
|
1526
|
+
parameters: relationshipParams,
|
|
1527
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1528
|
+
const path = await realpath(resolveExplorePath(ctx.cwd, params.path));
|
|
1529
|
+
if (!(await stat(path)).isDirectory()) throw new Error(`${name} requires a directory scope`);
|
|
1530
|
+
const record = locators.get(params.locator);
|
|
1531
|
+
if (!record)
|
|
1532
|
+
throw new Error(`Unknown declaration locator: ${params.locator}. Run outline or api_discover again.`);
|
|
1533
|
+
if (record.stale || record.generation !== client.getGeneration()) {
|
|
1534
|
+
throw new Error(`Declaration locator ${params.locator} is stale. Run outline or api_discover again.`);
|
|
1535
|
+
}
|
|
1536
|
+
const result = await client.relationships(path, record.token, name, params.resultLimit, signal);
|
|
1537
|
+
orientation.recordAttempts(
|
|
1538
|
+
result.relationships.flatMap((relationship) => [
|
|
1539
|
+
{
|
|
1540
|
+
path: resolve(path, relationship.relativePath),
|
|
1541
|
+
fingerprint: relationship.sourceFingerprint,
|
|
1542
|
+
kind: "relationshipLocation" as const,
|
|
1543
|
+
toolCallId,
|
|
1544
|
+
sourceBytesDeflected: 0,
|
|
1545
|
+
},
|
|
1546
|
+
{
|
|
1547
|
+
path: relationship.targetPath,
|
|
1548
|
+
fingerprint: relationship.targetSourceFingerprint,
|
|
1549
|
+
kind: "relationshipLocation" as const,
|
|
1550
|
+
toolCallId,
|
|
1551
|
+
sourceBytesDeflected: 0,
|
|
1552
|
+
},
|
|
1553
|
+
...relationship.candidatePaths.flatMap((candidatePath, index) => {
|
|
1554
|
+
const fingerprint = relationship.candidateSourceFingerprints[index];
|
|
1555
|
+
return fingerprint
|
|
1556
|
+
? [
|
|
1557
|
+
{
|
|
1558
|
+
path: candidatePath,
|
|
1559
|
+
fingerprint,
|
|
1560
|
+
kind: "relationshipLocation" as const,
|
|
1561
|
+
toolCallId,
|
|
1562
|
+
sourceBytesDeflected: 0,
|
|
1563
|
+
},
|
|
1564
|
+
]
|
|
1565
|
+
: [];
|
|
1566
|
+
}),
|
|
1567
|
+
{
|
|
1568
|
+
path: resolve(path, relationship.relativePath),
|
|
1569
|
+
fingerprint: relationship.enclosingScope.sourceFingerprint,
|
|
1570
|
+
kind: "relationshipScope" as const,
|
|
1571
|
+
toolCallId,
|
|
1572
|
+
sourceBytesDeflected: 0,
|
|
1573
|
+
},
|
|
1574
|
+
]),
|
|
1575
|
+
);
|
|
1576
|
+
const builder = new BoundedTextResultBuilder(temporaryOutput, "completeBlocks");
|
|
1577
|
+
const locatorByUnit = new Map<string, number[]>();
|
|
1578
|
+
try {
|
|
1579
|
+
for (const [index, relationship] of result.relationships.entries()) {
|
|
1580
|
+
const sourcePath = resolve(path, relationship.relativePath);
|
|
1581
|
+
const targetId =
|
|
1582
|
+
relationship.targetLocator === record.token
|
|
1583
|
+
? record.id
|
|
1584
|
+
: registerLocator(
|
|
1585
|
+
relationship.targetLocator,
|
|
1586
|
+
relationship.targetPath,
|
|
1587
|
+
`${relationship.relationshipKind} target`,
|
|
1588
|
+
);
|
|
1589
|
+
const candidateIds = relationship.candidateLocators.map((token, candidateIndex) =>
|
|
1590
|
+
token === record.token
|
|
1591
|
+
? record.id
|
|
1592
|
+
: registerLocator(
|
|
1593
|
+
token,
|
|
1594
|
+
relationship.candidatePaths[candidateIndex] ?? relationship.targetPath,
|
|
1595
|
+
"relationship candidate",
|
|
1596
|
+
),
|
|
1597
|
+
);
|
|
1598
|
+
const scopeId = registerLocator(
|
|
1599
|
+
relationship.enclosingScope.locator,
|
|
1600
|
+
sourcePath,
|
|
1601
|
+
relationship.enclosingScope.qualifiedIdentity,
|
|
1602
|
+
);
|
|
1603
|
+
const ids = [targetId, ...candidateIds, scopeId];
|
|
1604
|
+
const unit = `${relationship.relativePath}:${relationship.range.startByte}:${index}`;
|
|
1605
|
+
locatorByUnit.set(unit, ids);
|
|
1606
|
+
const kind =
|
|
1607
|
+
relationship.classification === "production"
|
|
1608
|
+
? relationship.relationshipKind
|
|
1609
|
+
: `${relationship.relationshipKind}, ${relationship.classification}`;
|
|
1610
|
+
const certainty = relationship.certainty === "exact" ? "" : `, ${relationship.certainty}`;
|
|
1611
|
+
const lines = [
|
|
1612
|
+
`${relationship.relativePath}:${displayLineRange(relationship.range)}(${scopeId}) [${kind}${certainty}]`,
|
|
1613
|
+
...relationship.sitePreview.split("\n").map((line) => ` ${line}`),
|
|
1614
|
+
];
|
|
1615
|
+
if (relationship.sitePreviewTruncated) lines.push(" [preview truncated]");
|
|
1616
|
+
if (relationship.certainty === "ambiguous") {
|
|
1617
|
+
lines.push(
|
|
1618
|
+
` candidates: ${candidateIds.map((id) => `(${id})`).join(", ")}${relationship.competingCandidatesOmitted ? `; ${relationship.competingCandidatesOmitted} omitted` : ""}`,
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
if (relationship.certaintyReason) lines.push(` uncertainty: ${relationship.certaintyReason}`);
|
|
1622
|
+
if (targetId !== record.id) lines.push(` target (${targetId})`);
|
|
1623
|
+
await builder.appendBlock(unit, relationship.relativePath, lines.join("\n"));
|
|
1624
|
+
}
|
|
1625
|
+
if (result.relationships.length === 0)
|
|
1626
|
+
await builder.appendRequiredBlock("no relationships", `No direct ${name} found`);
|
|
1627
|
+
const summary = result.summary;
|
|
1628
|
+
const exceptionLines = exceptionSummaryLines([
|
|
1629
|
+
...(summary.relationshipsFound > summary.relationshipsReturned
|
|
1630
|
+
? [
|
|
1631
|
+
`${summary.relationshipsFound - summary.relationshipsReturned} relationships omitted (result limit ${summary.resultLimit})`,
|
|
1632
|
+
]
|
|
1633
|
+
: []),
|
|
1634
|
+
...(summary.ambiguousRelationships > 0 ? [`${summary.ambiguousRelationships} ambiguous`] : []),
|
|
1635
|
+
...(summary.parserDegradedFiles > 0 || summary.diagnostics > 0
|
|
1636
|
+
? [
|
|
1637
|
+
`exceptions: parser degraded ${summary.parserDegradedFiles}, diagnostics ${summary.diagnostics}`,
|
|
1638
|
+
]
|
|
1639
|
+
: []),
|
|
1640
|
+
...limitReachedLine([
|
|
1641
|
+
...(summary.resultLimitReached ? ["results"] : []),
|
|
1642
|
+
...(summary.fileLimitReached ? ["files"] : []),
|
|
1643
|
+
...(summary.sourceByteLimitReached ? ["source bytes"] : []),
|
|
1644
|
+
...(summary.depthLimitReached ? ["depth"] : []),
|
|
1645
|
+
...(summary.elapsedLimitReached ? ["elapsed time"] : []),
|
|
1646
|
+
]),
|
|
1647
|
+
]);
|
|
1648
|
+
if (exceptionLines) await builder.appendRequiredBlock("relationship exceptions", exceptionLines);
|
|
1649
|
+
const bounded = await builder.finish();
|
|
1650
|
+
if (!bounded.overflow.fullOutputComplete) {
|
|
1651
|
+
const visible = new Set(bounded.visibleUnitIds);
|
|
1652
|
+
for (const [unit, ids] of locatorByUnit)
|
|
1653
|
+
if (!visible.has(unit)) for (const id of ids) if (id !== record.id) locators.delete(id);
|
|
1654
|
+
}
|
|
1655
|
+
if (bounded.overflow.temporaryPath) orientation.recordTemporaryOutput(bounded.overflow.temporaryPath);
|
|
1656
|
+
const returnedBytes = Buffer.byteLength(bounded.content);
|
|
1657
|
+
return {
|
|
1658
|
+
content: [{ type: "text", text: bounded.content }],
|
|
1659
|
+
details: {
|
|
1660
|
+
kind: "relationship",
|
|
1661
|
+
result,
|
|
1662
|
+
declarationCount: summary.relationshipsReturned,
|
|
1663
|
+
sourceBytes: summary.sourceBytes,
|
|
1664
|
+
returnedBytes,
|
|
1665
|
+
avoidedBytes: Math.max(0, summary.sourceBytes - returnedBytes),
|
|
1666
|
+
truncated: bounded.overflow.truncated,
|
|
1667
|
+
overflow: bounded.overflow,
|
|
1668
|
+
},
|
|
1669
|
+
};
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
await builder.abort();
|
|
1672
|
+
for (const ids of locatorByUnit.values())
|
|
1673
|
+
for (const id of ids) if (id !== record.id) locators.delete(id);
|
|
1674
|
+
throw error;
|
|
1675
|
+
}
|
|
1676
|
+
},
|
|
1677
|
+
renderCall(args, theme, context) {
|
|
1678
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1679
|
+
const component =
|
|
1680
|
+
(context.lastComponent as AstCallComponent | undefined) ??
|
|
1681
|
+
new AstCallComponent(
|
|
1682
|
+
rowState,
|
|
1683
|
+
context.toolCallId,
|
|
1684
|
+
name,
|
|
1685
|
+
[stripLeadingAt(args.path)],
|
|
1686
|
+
[`[${args.locator} limit=${args.resultLimit}]`],
|
|
1687
|
+
theme,
|
|
1688
|
+
);
|
|
1689
|
+
component.set([stripLeadingAt(args.path)], [`[${args.locator} limit=${args.resultLimit}]`], theme);
|
|
1690
|
+
return component;
|
|
1691
|
+
},
|
|
1692
|
+
renderResult(result, options, theme, context) {
|
|
1693
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1694
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1695
|
+
},
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
const references = relationshipTool(
|
|
1700
|
+
"references",
|
|
1701
|
+
"Find direct references and type usages for a declaration locator.",
|
|
1702
|
+
"Use references to inspect direct repository usage and re-exports.",
|
|
1703
|
+
);
|
|
1704
|
+
const callers = relationshipTool(
|
|
1705
|
+
"callers",
|
|
1706
|
+
"Find direct callers for a declaration locator.",
|
|
1707
|
+
"Use callers for syntactic call sites; inferred dispatch is labelled.",
|
|
1708
|
+
);
|
|
1709
|
+
const callees = relationshipTool(
|
|
1710
|
+
"callees",
|
|
1711
|
+
"Find direct callees inside a declaration locator.",
|
|
1712
|
+
"Use callees to inspect direct dependencies of one executable scope.",
|
|
1713
|
+
);
|
|
1714
|
+
const implementations = relationshipTool(
|
|
1715
|
+
"implementations",
|
|
1716
|
+
"Find implementations and overrides for a declaration locator.",
|
|
1717
|
+
"Use implementations for syntactic inheritance and conservative same-name overrides.",
|
|
1718
|
+
);
|
|
1719
|
+
const tests = relationshipTool(
|
|
1720
|
+
"tests",
|
|
1721
|
+
"Find directly affected tests for a declaration locator.",
|
|
1722
|
+
"Use tests after selecting a change target, or earlier when the task starts from test behavior or a failure.",
|
|
1723
|
+
);
|
|
1724
|
+
|
|
608
1725
|
return {
|
|
609
1726
|
outline,
|
|
610
1727
|
symbol,
|
|
1728
|
+
replace_declaration: replaceDeclaration,
|
|
1729
|
+
replace_body: replaceBody,
|
|
1730
|
+
insert_declaration: insertDeclaration,
|
|
1731
|
+
rename_declaration: renameDeclaration,
|
|
1732
|
+
api_discover: apiDiscover,
|
|
1733
|
+
ast_search: astSearch,
|
|
1734
|
+
references,
|
|
1735
|
+
callers,
|
|
1736
|
+
callees,
|
|
1737
|
+
implementations,
|
|
1738
|
+
tests,
|
|
611
1739
|
clear() {
|
|
612
1740
|
locators.clear();
|
|
613
1741
|
nextLocator = 1;
|
|
1742
|
+
orientation.clear();
|
|
1743
|
+
},
|
|
1744
|
+
resetForTree() {
|
|
1745
|
+
locators.clear();
|
|
1746
|
+
nextLocator = 1;
|
|
1747
|
+
orientation.resetGate();
|
|
614
1748
|
},
|
|
615
1749
|
invalidate(paths: readonly string[]) {
|
|
616
1750
|
const changed = new Set(paths.map((path) => resolve(path)));
|
|
617
1751
|
for (const record of locators.values()) {
|
|
618
1752
|
if (changed.has(record.path)) record.stale = true;
|
|
619
1753
|
}
|
|
1754
|
+
orientation.invalidate(paths);
|
|
620
1755
|
},
|
|
621
1756
|
};
|
|
622
1757
|
}
|
|
623
1758
|
|
|
1759
|
+
function formatApiQuery(query: ApiQuery): string {
|
|
1760
|
+
switch (query.kind) {
|
|
1761
|
+
case "exactName":
|
|
1762
|
+
case "prefixName":
|
|
1763
|
+
case "substringName":
|
|
1764
|
+
case "fuzzyName":
|
|
1765
|
+
return `${query.kind}=${query.name}`;
|
|
1766
|
+
case "declarationKind":
|
|
1767
|
+
return `kind=${query.declarationKind}`;
|
|
1768
|
+
case "documentation":
|
|
1769
|
+
return `docs=${query.terms.join(",")}`;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
624
1773
|
function displayLineRange(range: SourceRange): string {
|
|
625
1774
|
const endLine =
|
|
626
1775
|
range.endByte > range.startByte && range.end.column === 0
|
|
@@ -643,7 +1792,14 @@ function renderAstResult(
|
|
|
643
1792
|
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
644
1793
|
if (!expanded && !context.isError) {
|
|
645
1794
|
const declarationCount = details?.declarationCount ?? 0;
|
|
646
|
-
const noun =
|
|
1795
|
+
const noun =
|
|
1796
|
+
details?.kind === "locatorEdit"
|
|
1797
|
+
? declarationCount === 1
|
|
1798
|
+
? "fresh locator"
|
|
1799
|
+
: "fresh locators"
|
|
1800
|
+
: declarationCount === 1
|
|
1801
|
+
? "declaration"
|
|
1802
|
+
: "declarations";
|
|
647
1803
|
const byteSummary = details
|
|
648
1804
|
? `, ${formatSize(details.returnedBytes)} returned, ${formatSize(details.avoidedBytes)} avoided`
|
|
649
1805
|
: "";
|
|
@@ -740,7 +1896,11 @@ function truncateLeft(text: string, width: number): string {
|
|
|
740
1896
|
}
|
|
741
1897
|
|
|
742
1898
|
function outlineOptionVariants(args: OutlineArgs): string[] {
|
|
743
|
-
const fixed = [
|
|
1899
|
+
const fixed = [
|
|
1900
|
+
...(args.recursive ? ["recursive"] : []),
|
|
1901
|
+
...(args.includePrivate ? ["private"] : []),
|
|
1902
|
+
...(args.includeDocs ? ["docs"] : []),
|
|
1903
|
+
];
|
|
744
1904
|
const names = args.names ?? [];
|
|
745
1905
|
if (names.length === 0) return [fixed.length > 0 ? `[${fixed.join(" ")}]` : ""];
|
|
746
1906
|
|
|
@@ -796,33 +1956,39 @@ function symbolTargetVariants(ids: readonly number[], locators: ReadonlyMap<numb
|
|
|
796
1956
|
return variants;
|
|
797
1957
|
}
|
|
798
1958
|
|
|
799
|
-
function
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1959
|
+
function sourceFingerprintForPlan(source: string): string {
|
|
1960
|
+
return `sha256:${createHash("sha256").update(source, "utf8").digest("hex")}`;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
function locatorEditCall(
|
|
1964
|
+
tool: string,
|
|
1965
|
+
id: number,
|
|
1966
|
+
locators: ReadonlyMap<number, LocatorRecord>,
|
|
1967
|
+
rowState: ToolRowStateStore,
|
|
1968
|
+
theme: Theme,
|
|
1969
|
+
context: {
|
|
1970
|
+
toolCallId: string;
|
|
1971
|
+
invalidate: () => void;
|
|
1972
|
+
lastComponent?: Component;
|
|
1973
|
+
},
|
|
1974
|
+
option = "",
|
|
1975
|
+
): Component {
|
|
1976
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1977
|
+
const record = locators.get(id);
|
|
1978
|
+
const targets = record ? [`${record.name}@${basename(record.path)}`, String(id)] : [String(id)];
|
|
1979
|
+
const options = [option ? `[${option}]` : ""];
|
|
1980
|
+
const component =
|
|
1981
|
+
(context.lastComponent as AstCallComponent | undefined) ??
|
|
1982
|
+
new AstCallComponent(rowState, context.toolCallId, tool, targets, options, theme);
|
|
1983
|
+
component.set(targets, options, theme);
|
|
1984
|
+
return component;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function exceptionSummaryLines(parts: string[]): string | undefined {
|
|
1988
|
+
const lines = parts.filter((part) => part.length > 0);
|
|
1989
|
+
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function limitReachedLine(limits: string[]): string[] {
|
|
1993
|
+
return limits.length > 0 ? [`limits reached: ${limits.join(", ")}`] : [];
|
|
828
1994
|
}
|