@shanepadgett/tau-agent 0.25.0 → 0.26.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.
@@ -6,7 +6,7 @@ It exists so agents can inspect paths, discover files, search text, inspect decl
6
6
 
7
7
  When the source baseline is no longer available, such as after compaction or a cold subagent resume, `read` safely returns the current full source.
8
8
 
9
- Agents invoke it with `ls`, `find`, `grep`, `outline`, `symbol`, and `read`. `outline` returns public declaration signatures and parenthesized numeric locators for TypeScript, TSX, Odin, Go, Rust, C#, Java, Kotlin, and Swift files. It also accepts a package directory, which inspects supported files directly inside it. Exact-name filters narrow the result, while `includePrivate` exposes internal declarations. `symbol` accepts those numbers to retrieve several exact declarations in one call, can add bounded surrounding lines, and rejects the whole batch when any locator is stale. Users run `/read-stats` to see estimated token and cost savings for the session.
9
+ Agents invoke it with `ls`, `find`, `grep`, `outline`, `symbol`, and `read`. `outline` returns public declaration signatures and parenthesized numeric locators for TypeScript, TSX, Odin, Go, Rust, C#, Java, Kotlin, Swift, and Markdown files. Markdown headings locate their complete sections. It also accepts a package directory, which inspects supported files directly inside it. Exact-name filters narrow the result, `includePrivate` exposes internal declarations, and `includeDocs` adds attached documentation comments when they are needed. Annotations and attributes remain in normal outlines. `symbol` accepts those numbers to retrieve several exact declarations in one call, can add bounded surrounding lines, and rejects the whole batch when any locator is stale. Users run `/read-stats` to see estimated token and cost savings for the session.
10
10
 
11
11
  Installed packages support `outline` and `symbol` on Apple Silicon Macs. They include the worker, so users do not need Rust or Cargo. On other platforms, the rest of Explore remains available and AST tools report the platform limit when invoked.
12
12
 
@@ -15,6 +15,7 @@ Use the cheapest useful step:
15
15
  1. Outline a package directory to discover its public API.
16
16
  2. Add exact names when likely declarations are known.
17
17
  3. Set `includePrivate` when implementation work needs internals.
18
- 4. Send several locators to `symbol` for complete declarations.
19
- 5. Add context lines when the edit needs nearby source.
20
- 6. Use ranged or whole-file `read` for cross-cutting logic, exact formatting, or parser gaps.
18
+ 4. Set `includeDocs` when declaration documentation affects the task.
19
+ 5. Send several locators to `symbol` for complete declarations.
20
+ 6. Add context lines when the edit needs nearby source.
21
+ 7. Use ranged or whole-file `read` for cross-cutting logic, exact formatting, or parser gaps.
@@ -7,6 +7,7 @@ import {
7
7
  truncateHead,
8
8
  type Theme,
9
9
  } from "@earendil-works/pi-coding-agent";
10
+ import { StringEnum } from "@earendil-works/pi-ai";
10
11
  import { Text, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
11
12
  import { stat } from "node:fs/promises";
12
13
  import { basename, extname, resolve } from "node:path";
@@ -18,17 +19,22 @@ import type {
18
19
  AstLanguage,
19
20
  OutlineEntry,
20
21
  OutlineFileResult,
22
+ SourceRange,
21
23
  OutlineTarget,
22
24
  OutlineTargetResult,
23
25
  SymbolBatchResult,
26
+ SymbolView,
24
27
  } from "./ast-worker.ts";
25
28
 
26
29
  const outlineParams = Type.Object(
27
30
  {
28
31
  path: Type.String({
29
- description: "TypeScript, TSX, Odin, Go, Rust, C#, Java, Kotlin, or Swift source file or directory",
32
+ description: "TypeScript, TSX, Odin, Go, Rust, C#, Java, Kotlin, Swift, or Markdown source file or directory",
30
33
  }),
31
34
  includePrivate: Type.Optional(Type.Boolean({ description: "Include private declarations and members" })),
35
+ includeDocs: Type.Optional(
36
+ Type.Boolean({ description: "Include attached documentation comments in declaration signatures" }),
37
+ ),
32
38
  names: Type.Optional(
33
39
  Type.Array(Type.String(), {
34
40
  minItems: 1,
@@ -44,6 +50,10 @@ const symbolParams = Type.Object(
44
50
  minItems: 1,
45
51
  description: "Numeric locators shown in parentheses by outline",
46
52
  }),
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",
56
+ }),
47
57
  contextLines: Type.Optional(
48
58
  Type.Integer({ minimum: 0, description: "Lines of source context before and after each declaration" }),
49
59
  ),
@@ -67,9 +77,11 @@ interface LocatorRecord {
67
77
  path: string;
68
78
  name: string;
69
79
  stale: boolean;
80
+ declarationRetrieved: boolean;
81
+ generation: number;
70
82
  }
71
83
 
72
- type OutlineArgs = { path: string; includePrivate?: boolean; names?: string[] };
84
+ type OutlineArgs = { path: string; includePrivate?: boolean; includeDocs?: boolean; names?: string[] };
73
85
 
74
86
  export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
75
87
  const locators = new Map<number, LocatorRecord>();
@@ -102,29 +114,204 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
102
114
  }
103
115
 
104
116
  function locator(entry: OutlineEntry, path: string): number {
117
+ if (!entry.locator) throw new Error(`Structural outline row ${entry.name} has no symbol locator`);
105
118
  const id = nextLocator++;
106
- const record = { id, token: entry.locator, path: resolve(path), name: entry.name, stale: false };
119
+ const record = {
120
+ id,
121
+ token: entry.locator,
122
+ path: resolve(path),
123
+ name: entry.name,
124
+ stale: false,
125
+ declarationRetrieved: false,
126
+ generation: client.getGeneration(),
127
+ };
107
128
  locators.set(id, record);
108
129
  return id;
109
130
  }
110
131
 
111
- function renderEntry(entry: OutlineEntry, path: string, language: AstLanguage, indent: string): string {
112
- const lines =
113
- entry.range.start.line === entry.range.end.line
114
- ? `${entry.range.start.line + 1}`
115
- : `${entry.range.start.line + 1}-${entry.range.end.line + 1}`;
116
- let signature = entry.signature.replace(/\s+/g, " ").trim();
117
- if (language === "odin") {
118
- signature = signature
119
- .replace(/\s*([(),:])\s*/g, "$1")
120
- .replace(/\s*->\s*/g, "->")
121
- .replace(/\s*:=\s*/g, ":=")
122
- .replace(/\s*\{\s*$/, "");
132
+ function renderEntry(
133
+ entry: OutlineEntry,
134
+ path: string,
135
+ language: AstLanguage,
136
+ indent: string,
137
+ signatureOverride?: string,
138
+ ): string {
139
+ const lines = displayLineRange(entry.range);
140
+ let signature = (signatureOverride ?? entry.signature).trim();
141
+ if (
142
+ language === "typeScript" ||
143
+ language === "tsx" ||
144
+ language === "odin" ||
145
+ language === "go" ||
146
+ language === "rust" ||
147
+ language === "cSharp" ||
148
+ language === "java" ||
149
+ language === "kotlin" ||
150
+ language === "swift" ||
151
+ language === "markdown"
152
+ ) {
153
+ const signatureLines = signature.split("\n");
154
+ const relativeNameLine = entry.nameRange.start.line - entry.range.start.line;
155
+ const keywordNameLine =
156
+ language === "go"
157
+ ? signatureLines.findIndex(
158
+ (line) => /\b(?:func|type|const|var)\b/.test(line) && line.includes(entry.name),
159
+ )
160
+ : -1;
161
+ let inBlockComment = false;
162
+ let annotationDepth = 0;
163
+ let attributeDepth = 0;
164
+ const metadataLines = signatureLines.map((line) => {
165
+ const trimmed = line.trimStart();
166
+ if (inBlockComment) {
167
+ if (trimmed.includes("*/")) inBlockComment = false;
168
+ return true;
169
+ }
170
+ if (trimmed.startsWith("/*")) {
171
+ inBlockComment = !trimmed.includes("*/");
172
+ return true;
173
+ }
174
+ if (annotationDepth > 0) {
175
+ annotationDepth += (line.match(/\(/g) ?? []).length - (line.match(/\)/g) ?? []).length;
176
+ return true;
177
+ }
178
+ if (attributeDepth > 0) {
179
+ attributeDepth += (line.match(/\[/g) ?? []).length - (line.match(/\]/g) ?? []).length;
180
+ return true;
181
+ }
182
+ if (trimmed.startsWith("@") && !trimmed.startsWith(`@interface ${entry.name}`)) {
183
+ annotationDepth = (line.match(/\(/g) ?? []).length - (line.match(/\)/g) ?? []).length;
184
+ return true;
185
+ }
186
+ if (trimmed.startsWith("#[")) {
187
+ attributeDepth = (line.match(/\[/g) ?? []).length - (line.match(/\]/g) ?? []).length;
188
+ return true;
189
+ }
190
+ return trimmed.startsWith("//");
191
+ });
192
+ const declarationNameLine = signatureLines.findIndex(
193
+ (line, index) => !metadataLines[index] && line.includes(entry.name),
194
+ );
195
+ const nameLine =
196
+ relativeNameLine >= 0 && relativeNameLine === declarationNameLine
197
+ ? relativeNameLine
198
+ : keywordNameLine >= 0
199
+ ? keywordNameLine
200
+ : declarationNameLine;
201
+ const declarationLine = language === "markdown" ? Math.max(0, relativeNameLine) : nameLine >= 0 ? nameLine : 0;
202
+ const sourceLine = signatureLines[declarationLine] ?? entry.name;
203
+ const first =
204
+ language === "markdown" || sourceLine.includes(entry.name) ? sourceLine : `${entry.name} ${sourceLine}`;
205
+ const warning =
206
+ entry.certainty === "certain"
207
+ ? ""
208
+ : ` [${entry.certainty}${entry.certaintyReason ? `: ${entry.certaintyReason}` : ""}]`;
209
+ return [
210
+ ...signatureLines.slice(0, declarationLine).map((line) => `${indent}${line}`),
211
+ `${indent}${lines}(${locator(entry, path)}): ${first}${warning}`,
212
+ ...signatureLines.slice(declarationLine + 1).map((line) => `${indent}${line}`),
213
+ ].join("\n");
123
214
  }
215
+ signature = signature.replace(/\s+/g, " ");
124
216
  const label = signature && signature.includes(entry.name) ? signature : `${entry.name} ${signature}`.trim();
125
217
  return `${indent}${lines}(${locator(entry, path)}): ${label}`;
126
218
  }
127
219
 
220
+ function renderStructuralEntry(entry: OutlineEntry, indent: string): string {
221
+ const lines = displayLineRange(entry.range);
222
+ return `${indent}${lines}: ${entry.signature.trim().replace(/\s+/g, " ")}`;
223
+ }
224
+
225
+ function containerFrame(
226
+ item: OutlineFileResult["items"][number],
227
+ language: AstLanguage,
228
+ ): { header: string; footer: string } | undefined {
229
+ const firstMember = item.members[0];
230
+ if (!firstMember) return undefined;
231
+ if (
232
+ (language === "odin" ||
233
+ language === "cSharp" ||
234
+ language === "java" ||
235
+ language === "kotlin" ||
236
+ language === "swift") &&
237
+ item.bodyRange
238
+ ) {
239
+ const bodyOffset = item.bodyRange.startByte - item.range.startByte;
240
+ const signature = Buffer.from(item.signature);
241
+ if (bodyOffset >= 0 && bodyOffset < signature.byteLength) {
242
+ const header = signature
243
+ .subarray(0, bodyOffset + 1)
244
+ .toString("utf8")
245
+ .trimEnd();
246
+ if (header.endsWith("{")) return { header, footer: "}" };
247
+ }
248
+ }
249
+ if (
250
+ item.symbolType === "class" ||
251
+ item.symbolType === "struct" ||
252
+ item.symbolType === "interface" ||
253
+ item.symbolType === "enum" ||
254
+ item.symbolType === "object" ||
255
+ item.symbolType === "namespace"
256
+ ) {
257
+ const members = (
258
+ language === "typeScript" ||
259
+ language === "tsx" ||
260
+ language === "cSharp" ||
261
+ language === "java" ||
262
+ language === "kotlin" ||
263
+ language === "swift"
264
+ ? item.members.filter(
265
+ (member) => member.qualifiedName.split(".").slice(0, -1).join(".") === item.qualifiedName,
266
+ )
267
+ : item.members
268
+ )
269
+ .map((member) =>
270
+ (language === "java" &&
271
+ (member.symbolType === "class" ||
272
+ member.symbolType === "struct" ||
273
+ member.symbolType === "interface" ||
274
+ member.symbolType === "enum") &&
275
+ member.signature.endsWith("{")
276
+ ? `${member.signature} … }`
277
+ : member.signature
278
+ )
279
+ .split("\n")
280
+ .map((line) => ` ${line}`)
281
+ .join("\n"),
282
+ )
283
+ .join("\n");
284
+ const braceSuffix = `\n${members}\n}`;
285
+ if (item.signature.endsWith(braceSuffix))
286
+ return { header: item.signature.slice(0, -braceSuffix.length), footer: "}" };
287
+ const tupleMembers = item.members
288
+ .map((member) =>
289
+ `${member.signature},`
290
+ .split("\n")
291
+ .map((line) => ` ${line}`)
292
+ .join("\n"),
293
+ )
294
+ .join("\n");
295
+ const tupleSuffix = `\n${tupleMembers}\n);`;
296
+ if (item.signature.endsWith(tupleSuffix))
297
+ return { header: item.signature.slice(0, -tupleSuffix.length), footer: ");" };
298
+ const tupleMarker = `\n${tupleMembers}\n)`;
299
+ const tupleMarkerIndex = item.signature.lastIndexOf(tupleMarker);
300
+ if (tupleMarkerIndex >= 0)
301
+ return {
302
+ header: item.signature.slice(0, tupleMarkerIndex),
303
+ footer: item.signature.slice(tupleMarkerIndex + tupleMarker.length - 1),
304
+ };
305
+ if (item.symbolType === "class") return undefined;
306
+ }
307
+
308
+ const memberOffset = firstMember.range.startByte - item.range.startByte;
309
+ const signature = Buffer.from(item.signature);
310
+ if (memberOffset <= 0 || memberOffset > signature.byteLength) return undefined;
311
+ const header = signature.subarray(0, memberOffset).toString("utf8").trimEnd();
312
+ return header.endsWith("{") ? { header, footer: "}" } : undefined;
313
+ }
314
+
128
315
  function renderOutlineFile(file: OutlineFileResult, cwd: string, includeHeader: boolean): string[] {
129
316
  const lines = includeHeader
130
317
  ? [
@@ -136,7 +323,123 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
136
323
  `warning: parser recovered with ${file.diagnostics.errorNodes} ERROR and ${file.diagnostics.missingNodes} MISSING nodes`,
137
324
  );
138
325
  }
139
- const declarations = file.items.filter((item) => !item.isImport);
326
+ const declarations = file.items.filter((item) => item.rowKind === "declaration");
327
+ if (
328
+ file.language === "typeScript" ||
329
+ file.language === "tsx" ||
330
+ file.language === "odin" ||
331
+ file.language === "go" ||
332
+ file.language === "rust" ||
333
+ file.language === "cSharp" ||
334
+ file.language === "java" ||
335
+ file.language === "kotlin" ||
336
+ file.language === "swift" ||
337
+ file.language === "markdown"
338
+ ) {
339
+ let previousSection = "";
340
+ for (const item of file.items) {
341
+ const section =
342
+ item.rowKind === "package"
343
+ ? "package"
344
+ : item.rowKind === "import"
345
+ ? "imports"
346
+ : item.rowKind === "export"
347
+ ? "exports"
348
+ : item.rowKind === "sideEffect"
349
+ ? "side effects"
350
+ : "declarations";
351
+ if (section !== previousSection) {
352
+ if (lines.length > 0) lines.push("");
353
+ lines.push(section);
354
+ previousSection = section;
355
+ }
356
+ const frame = item.rowKind === "declaration" ? containerFrame(item, file.language) : undefined;
357
+ lines.push(
358
+ item.rowKind !== "declaration"
359
+ ? renderStructuralEntry(item, "")
360
+ : renderEntry(item, file.path, file.language, "", frame?.header),
361
+ );
362
+ const nestedContainers = new Map(
363
+ file.language === "typeScript" ||
364
+ file.language === "tsx" ||
365
+ file.language === "odin" ||
366
+ file.language === "cSharp" ||
367
+ file.language === "java" ||
368
+ file.language === "kotlin" ||
369
+ file.language === "swift"
370
+ ? [item, ...item.members]
371
+ .filter(
372
+ (entry) =>
373
+ entry.symbolType === "class" ||
374
+ entry.symbolType === "struct" ||
375
+ entry.symbolType === "interface" ||
376
+ entry.symbolType === "enum",
377
+ )
378
+ .map((entry) => [entry.qualifiedName, entry] as const)
379
+ : [],
380
+ );
381
+ const emittedEnumSeparators = new Set<string>();
382
+ const openNestedDepths: number[] = [];
383
+ for (const [memberIndex, member] of item.members.entries()) {
384
+ const depth =
385
+ file.language === "typeScript" ||
386
+ file.language === "tsx" ||
387
+ file.language === "odin" ||
388
+ file.language === "cSharp" ||
389
+ file.language === "java" ||
390
+ file.language === "kotlin" ||
391
+ file.language === "swift"
392
+ ? Math.max(1, member.qualifiedName.split(".").length - item.qualifiedName.split(".").length)
393
+ : 1;
394
+ const parent = member.qualifiedName.split(".").slice(0, -1).join(".");
395
+ const siblings =
396
+ file.language === "typeScript" ||
397
+ file.language === "tsx" ||
398
+ file.language === "odin" ||
399
+ file.language === "cSharp" ||
400
+ file.language === "java" ||
401
+ file.language === "kotlin" ||
402
+ file.language === "swift"
403
+ ? item.members.filter(
404
+ (candidate) => candidate.qualifiedName.split(".").slice(0, -1).join(".") === parent,
405
+ )
406
+ : [];
407
+ const parentIsEnum = file.language === "java" && nestedContainers.get(parent)?.symbolType === "enum";
408
+ while (openNestedDepths.length > 0 && (openNestedDepths.at(-1) ?? 0) >= depth) {
409
+ const closingDepth = openNestedDepths.pop();
410
+ if (closingDepth !== undefined) lines.push(`${" ".repeat(closingDepth)}}`);
411
+ }
412
+ if (
413
+ parentIsEnum &&
414
+ !siblings.some((candidate) => candidate.symbolType === "enumMember") &&
415
+ !emittedEnumSeparators.has(parent)
416
+ ) {
417
+ lines.push(`${" ".repeat(depth)};`);
418
+ emittedEnumSeparators.add(parent);
419
+ }
420
+ let memberSignature = member.signature;
421
+ const nextMember = item.members[memberIndex + 1];
422
+ const hasDescendants = nextMember?.qualifiedName.startsWith(`${member.qualifiedName}.`) ?? false;
423
+ if (hasDescendants && memberSignature.endsWith(" { … }")) {
424
+ memberSignature = `${memberSignature.slice(0, -" { … }".length)} {`;
425
+ openNestedDepths.push(depth);
426
+ }
427
+ if (parentIsEnum && member.symbolType === "enumMember") {
428
+ const constants = siblings.filter((candidate) => candidate.symbolType === "enumMember");
429
+ const constantIndex = constants.indexOf(member);
430
+ if (constantIndex + 1 < constants.length) memberSignature += ",";
431
+ else if (siblings.some((candidate) => candidate.symbolType !== "enumMember")) memberSignature += ";";
432
+ }
433
+ lines.push(renderEntry(member, file.path, file.language, " ".repeat(depth), memberSignature));
434
+ }
435
+ while (openNestedDepths.length > 0) {
436
+ const closingDepth = openNestedDepths.pop();
437
+ if (closingDepth !== undefined) lines.push(`${" ".repeat(closingDepth)}}`);
438
+ }
439
+ if (frame) lines.push(frame.footer);
440
+ }
441
+ return lines;
442
+ }
140
443
  const groups = new Map<string, typeof declarations>();
141
444
  for (const item of declarations) {
142
445
  const visibility = item.isExported ? "public" : "private";
@@ -167,6 +470,7 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
167
470
  promptGuidelines: [
168
471
  "Use a public package outline first to discover reusable APIs; add exact names when likely symbols are known.",
169
472
  "Set includePrivate when internal implementation discovery is needed.",
473
+ "Leave includeDocs off for routine exploration; enable it when documentation comments are needed.",
170
474
  "Treat each parenthesized number after a line range as that declaration's symbol locator.",
171
475
  "Use symbol with several locators when complete declaration source is needed.",
172
476
  ],
@@ -178,7 +482,13 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
178
482
  ? { kind: "directory", path }
179
483
  : { kind: "file", path, language: languageForPath(path) };
180
484
  const names = params.names ?? [];
181
- const result = await client.outline(target, params.includePrivate ?? false, names, signal);
485
+ const result = await client.outline(
486
+ target,
487
+ params.includePrivate ?? false,
488
+ params.includeDocs ?? false,
489
+ names,
490
+ signal,
491
+ );
182
492
  const lines = result.files.flatMap((file, index) => [
183
493
  ...(index === 0 ? [] : [""]),
184
494
  ...renderOutlineFile(file, ctx.cwd, result.files.length > 1),
@@ -186,7 +496,10 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
186
496
  const declarationCount = result.files.reduce(
187
497
  (count, file) =>
188
498
  count +
189
- file.items.reduce((fileCount, item) => fileCount + (item.isImport ? 0 : 1 + item.members.length), 0),
499
+ file.items.reduce(
500
+ (fileCount, item) => fileCount + (item.rowKind === "declaration" ? 1 + item.members.length : 0),
501
+ 0,
502
+ ),
190
503
  0,
191
504
  );
192
505
  if (declarationCount === 0) lines.push(names.length > 0 ? "No matching declarations" : "No declarations");
@@ -222,14 +535,21 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
222
535
  promptSnippet: "Retrieve exact declaration source for several outline locators",
223
536
  parameters: symbolParams,
224
537
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
538
+ if (params.contextLines !== undefined && params.view !== "declaration") {
539
+ throw new Error("contextLines is supported only with view=declaration");
540
+ }
225
541
  const records = [...new Set(params.locators)].map((id) => {
226
542
  const record = locators.get(id);
227
543
  if (!record) throw new Error(`Unknown symbol locator: ${id}. Run outline again.`);
228
544
  if (record.stale) throw new Error(`Symbol locator ${id} is stale. Run outline again.`);
545
+ if (record.generation !== client.getGeneration()) {
546
+ throw new Error(`Symbol locator ${id} is stale because the AST worker restarted. Run outline again.`);
547
+ }
229
548
  return record;
230
549
  });
231
550
  const result = await client.symbol(
232
551
  [...new Set(records.map((record) => record.token))],
552
+ params.view,
233
553
  params.contextLines ?? 0,
234
554
  signal,
235
555
  );
@@ -248,10 +568,7 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
248
568
  return declaration ? (requestedByToken.get(declaration.locator) ?? []) : [];
249
569
  });
250
570
  const range = block.returnedRange;
251
- const lineRange =
252
- range.start.line === range.end.line
253
- ? `${range.start.line + 1}`
254
- : `${range.start.line + 1}-${range.end.line + 1}`;
571
+ const lineRange = displayLineRange(range);
255
572
  lines.push(
256
573
  ...(includePaths ? [formatPathForDisplay(block.path, ctx.cwd)] : []),
257
574
  `${lineRange}(${represented.map((record) => record.id).join(",")}): ${represented.map((record) => record.name).join(", ")}`,
@@ -260,6 +577,10 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
260
577
  }
261
578
  const sourceBytes = result.blocks.reduce((count, block) => count + Buffer.byteLength(block.source), 0);
262
579
  const output = compact(lines.join("\n"), sourceBytes, result.declarations.length, "symbol", result);
580
+ if (output.details.truncated) {
581
+ throw new Error("Symbol result exceeded the output limit. Request fewer locators.");
582
+ }
583
+ if (params.view === "declaration") for (const record of records) record.declarationRetrieved = true;
263
584
  return { content: [{ type: "text", text: output.text }], details: output.details };
264
585
  },
265
586
  renderCall(args, theme, context) {
@@ -272,10 +593,10 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
272
593
  context.toolCallId,
273
594
  "symbol",
274
595
  targets,
275
- [args.contextLines === undefined ? "" : `[context=${args.contextLines}]`],
596
+ [symbolOptions(args.view, args.contextLines)],
276
597
  theme,
277
598
  );
278
- component.set(targets, [args.contextLines === undefined ? "" : `[context=${args.contextLines}]`], theme);
599
+ component.set(targets, [symbolOptions(args.view, args.contextLines)], theme);
279
600
  return component;
280
601
  },
281
602
  renderResult(result, options, theme, context) {
@@ -300,6 +621,18 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
300
621
  };
301
622
  }
302
623
 
624
+ function displayLineRange(range: SourceRange): string {
625
+ const endLine =
626
+ range.endByte > range.startByte && range.end.column === 0
627
+ ? Math.max(range.start.line, range.end.line - 1)
628
+ : range.end.line;
629
+ return range.start.line === endLine ? `${range.start.line + 1}` : `${range.start.line + 1}-${endLine + 1}`;
630
+ }
631
+
632
+ function symbolOptions(view: SymbolView, contextLines: number | undefined): string {
633
+ return `[${view}${contextLines === undefined ? "" : ` context=${contextLines}`}]`;
634
+ }
635
+
303
636
  function renderAstResult(
304
637
  result: { content: Array<{ type: string; text?: string }>; details?: AstToolDetails },
305
638
  expanded: boolean,
@@ -407,7 +740,7 @@ function truncateLeft(text: string, width: number): string {
407
740
  }
408
741
 
409
742
  function outlineOptionVariants(args: OutlineArgs): string[] {
410
- const fixed = args.includePrivate ? ["private"] : [];
743
+ const fixed = [...(args.includePrivate ? ["private"] : []), ...(args.includeDocs ? ["docs"] : [])];
411
744
  const names = args.names ?? [];
412
745
  if (names.length === 0) return [fixed.length > 0 ? `[${fixed.join(" ")}]` : ""];
413
746
 
@@ -485,6 +818,10 @@ function languageForPath(path: string): AstLanguage {
485
818
  return "kotlin";
486
819
  case ".swift":
487
820
  return "swift";
821
+ case ".md":
822
+ case ".markdown":
823
+ case ".mdown":
824
+ return "markdown";
488
825
  default:
489
826
  throw new Error(`Unsupported outline file type: ${extname(path) || "no extension"}`);
490
827
  }
@@ -3,7 +3,17 @@ import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
- export type AstLanguage = "typeScript" | "tsx" | "odin" | "go" | "rust" | "cSharp" | "java" | "kotlin" | "swift";
6
+ export type AstLanguage =
7
+ | "typeScript"
8
+ | "tsx"
9
+ | "odin"
10
+ | "go"
11
+ | "rust"
12
+ | "cSharp"
13
+ | "java"
14
+ | "kotlin"
15
+ | "swift"
16
+ | "markdown";
7
17
 
8
18
  export type OutlineTarget = { kind: "file"; path: string; language: AstLanguage } | { kind: "directory"; path: string };
9
19
 
@@ -23,13 +33,22 @@ export interface OutlineEntry {
23
33
  role: "item" | "member";
24
34
  symbolType: string;
25
35
  name: string;
36
+ qualifiedName: string;
26
37
  range: SourceRange;
38
+ nameRange: SourceRange;
39
+ receiverRange?: SourceRange;
40
+ bodyRange?: SourceRange;
27
41
  signature: string;
28
42
  astKind: string;
29
- locator: string;
43
+ certainty: "certain" | "recovered" | "nearRecovery";
44
+ certaintyReason?: string;
45
+ locator?: string;
30
46
  }
31
47
 
48
+ export type SymbolView = "signature" | "declaration" | "declarationWithImports";
49
+
32
50
  export interface OutlineItem extends OutlineEntry {
51
+ rowKind: "package" | "import" | "declaration" | "export" | "sideEffect";
33
52
  isImport: boolean;
34
53
  isExported: boolean;
35
54
  members: Array<OutlineEntry & { isPublic: boolean }>;
@@ -73,20 +92,27 @@ export interface SymbolBatchResult {
73
92
  }
74
93
 
75
94
  export interface AstClient {
95
+ getGeneration(): number;
76
96
  outline(
77
97
  target: OutlineTarget,
78
98
  includePrivate: boolean,
99
+ includeDocs: boolean,
79
100
  names: string[],
80
101
  signal: AbortSignal | undefined,
81
102
  ): Promise<OutlineTargetResult>;
82
- symbol(locators: string[], contextLines: number, signal: AbortSignal | undefined): Promise<SymbolBatchResult>;
103
+ symbol(
104
+ locators: string[],
105
+ view: SymbolView,
106
+ contextLines: number,
107
+ signal: AbortSignal | undefined,
108
+ ): Promise<SymbolBatchResult>;
83
109
  shutdown(): Promise<void>;
84
110
  }
85
111
 
86
112
  type WorkerRequestPayload =
87
113
  | { operation: "handshake" }
88
- | { operation: "outline"; target: OutlineTarget; includePrivate: boolean; names: string[] }
89
- | { operation: "symbol"; locators: string[]; contextLines: number };
114
+ | { operation: "outline"; target: OutlineTarget; includePrivate: boolean; includeDocs: boolean; names: string[] }
115
+ | { operation: "symbol"; locators: string[]; view: SymbolView; contextLines: number };
90
116
 
91
117
  interface WorkerResponse {
92
118
  requestId: number;
@@ -102,7 +128,7 @@ interface PendingRequest {
102
128
  removeAbortListener(): void;
103
129
  }
104
130
 
105
- const PROTOCOL_VERSION = 2;
131
+ const PROTOCOL_VERSION = 5;
106
132
  const MAX_FRAME_BYTES = 8 * 1024 * 1024;
107
133
  const STDERR_BYTES = 16 * 1024;
108
134
 
@@ -142,25 +168,36 @@ export class AstWorkerClient implements AstClient {
142
168
  private nextRequestId = 1;
143
169
  private incoming = Buffer.alloc(0);
144
170
  private stderr = "";
171
+ private generation = 0;
145
172
 
146
173
  constructor(command: string | undefined = undefined, args: readonly string[] = []) {
147
174
  this.command = command;
148
175
  this.args = args;
149
176
  }
150
177
 
178
+ getGeneration(): number {
179
+ return this.generation;
180
+ }
181
+
151
182
  async outline(
152
183
  target: OutlineTarget,
153
184
  includePrivate: boolean,
185
+ includeDocs: boolean,
154
186
  names: string[],
155
187
  signal: AbortSignal | undefined,
156
188
  ): Promise<OutlineTargetResult> {
157
- const result = await this.request({ operation: "outline", target, includePrivate, names }, signal);
189
+ const result = await this.request({ operation: "outline", target, includePrivate, includeDocs, names }, signal);
158
190
  if (result.kind !== "outline") throw new Error("tau-ast returned the wrong result for outline");
159
191
  return result as unknown as OutlineTargetResult;
160
192
  }
161
193
 
162
- async symbol(locators: string[], contextLines: number, signal: AbortSignal | undefined): Promise<SymbolBatchResult> {
163
- const result = await this.request({ operation: "symbol", locators, contextLines }, signal);
194
+ async symbol(
195
+ locators: string[],
196
+ view: SymbolView,
197
+ contextLines: number,
198
+ signal: AbortSignal | undefined,
199
+ ): Promise<SymbolBatchResult> {
200
+ const result = await this.request({ operation: "symbol", locators, view, contextLines }, signal);
164
201
  if (result.kind !== "symbol") throw new Error("tau-ast returned the wrong result for symbol");
165
202
  return result as unknown as SymbolBatchResult;
166
203
  }
@@ -206,6 +243,7 @@ export class AstWorkerClient implements AstClient {
206
243
  : resolveAstWorkerCommand(fileURLToPath(new URL("../../", import.meta.url)), process.platform, process.arch);
207
244
  if ("error" in resolution) throw resolution.error;
208
245
  const child = spawn(resolution.command, this.args, { stdio: ["pipe", "pipe", "pipe"] });
246
+ this.generation += 1;
209
247
  this.child = child;
210
248
  this.incoming = Buffer.alloc(0);
211
249
  this.stderr = "";
@@ -302,6 +340,7 @@ export class AstWorkerClient implements AstClient {
302
340
  private fail(child: ChildProcessWithoutNullStreams, error: Error, kill = true): void {
303
341
  if (this.child !== child) return;
304
342
  this.child = undefined;
343
+ this.generation += 1;
305
344
  this.incoming = Buffer.alloc(0);
306
345
  this.rejectPending(error);
307
346
  if (kill && child.exitCode === null) child.kill();
@@ -118,7 +118,7 @@ export async function createSubagentSessionResource(
118
118
  const modelRuntime = await ModelRuntime.create();
119
119
  modelRuntime.registerNativeProvider(inputs.provider);
120
120
  if (inputs.runtimeApiKey !== undefined)
121
- await modelRuntime.setRuntimeApiKey(inputs.model.provider, inputs.runtimeApiKey);
121
+ await modelRuntime.setRuntimeApiKey(inputs.model.provider, inputs.runtimeApiKey, { allowNetwork: false });
122
122
  if (signal.aborted) throw new Error(`Agent ${inputs.definition.name} startup aborted`);
123
123
  const resourceLoader = new DefaultResourceLoader({
124
124
  cwd: inputs.cwd,
@@ -40,7 +40,7 @@ Gives the agent `context_prune` for creating a hard context checkpoint after bro
40
40
 
41
41
  ## explore
42
42
 
43
- Replaces Pi’s filesystem inspection tools with compact Tau versions: `ls`, `find`, `grep`, and `read`. It also adds `outline` for public declarations in TypeScript, TSX, Odin, Go, Rust, C#, Java, Kotlin, and Swift files or package directories. Exact-name filters and `includePrivate` narrow or expand that surface. Parenthesized numbers identify declarations for `symbol`, which retrieves several complete declarations with optional surrounding lines and rejects stale batches. Installed packages support `outline` and `symbol` on Apple Silicon Macs without requiring Rust or Cargo; other platforms retain the rest of Explore and report the limit when an AST tool is invoked. These tools produce smaller model payloads and readable tool rows. Repeated `read` calls return unchanged markers or useful diffs when branch history proves the agent already saw the base content. A failed patch unlocks one normal reread of the affected path. `/read-stats` shows estimated token and cost savings for the current chat and whole session.
43
+ Replaces Pi’s filesystem inspection tools with compact Tau versions: `ls`, `find`, `grep`, and `read`. It also adds `outline` for public declarations in TypeScript, TSX, Odin, Go, Rust, C#, Java, Kotlin, Swift, and Markdown files or package directories. Markdown headings locate their complete sections. Exact-name filters and `includePrivate` narrow or expand that surface. Attached documentation comments are omitted by default; `includeDocs` adds them without hiding annotations or attributes. Parenthesized numbers identify declarations for `symbol`, which retrieves several complete declarations with optional surrounding lines and rejects stale batches. Installed packages support `outline` and `symbol` on Apple Silicon Macs without requiring Rust or Cargo; other platforms retain the rest of Explore and report the limit when an AST tool is invoked. These tools produce smaller model payloads and readable tool rows. Repeated `read` calls return unchanged markers or useful diffs when branch history proves the agent already saw the base content. A failed patch unlocks one normal reread of the affected path. `/read-stats` shows estimated token and cost savings for the current chat and whole session.
44
44
 
45
45
  ## footer
46
46
 
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Tau is a custom agentic harness built with pi extensions",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -35,7 +35,7 @@
35
35
  "README.md"
36
36
  ],
37
37
  "dependencies": {
38
- "@shanepadgett/tau-tui": "0.25.0",
38
+ "@shanepadgett/tau-tui": "0.26.0",
39
39
  "@toon-format/toon": "2.3.0",
40
40
  "image-size": "2.0.2",
41
41
  "smol-toml": "1.7.0"