@shanepadgett/tau-agent 0.26.0 → 0.28.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.
Files changed (121) hide show
  1. package/docs/subagents.md +1 -1
  2. package/extensions/appshot/README.md +1 -1
  3. package/extensions/appshot/index.ts +5 -11
  4. package/extensions/context/sync.ts +1 -9
  5. package/extensions/context-pruning/README.md +2 -2
  6. package/extensions/context-pruning/index.ts +58 -57
  7. package/extensions/context-pruning/prune.ts +2 -2
  8. package/extensions/context-pruning/render.ts +51 -39
  9. package/extensions/context-pruning/settings.ts +4 -5
  10. package/extensions/explore/README.md +13 -14
  11. package/extensions/explore/ast/adapter.ts +97 -0
  12. package/extensions/explore/ast/engine.ts +470 -0
  13. package/extensions/explore/ast/format/ast-search.ts +97 -0
  14. package/extensions/explore/ast/format/composite.ts +26 -0
  15. package/extensions/explore/ast/format/context.ts +66 -0
  16. package/extensions/explore/ast/format/deps.ts +86 -0
  17. package/extensions/explore/ast/format/discover.ts +85 -0
  18. package/extensions/explore/ast/format/impact.ts +105 -0
  19. package/extensions/explore/ast/format/outline.ts +66 -0
  20. package/extensions/explore/ast/format/relationships.ts +81 -0
  21. package/extensions/explore/ast/format/show.ts +38 -0
  22. package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
  23. package/extensions/explore/ast/grammars/manifest.json +41 -0
  24. package/extensions/explore/ast/grammars/manifest.ts +99 -0
  25. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  26. package/extensions/explore/ast/grammars/swift.wasm +0 -0
  27. package/extensions/explore/ast/graph/file-graph.ts +360 -0
  28. package/extensions/explore/ast/graph/relationships.ts +597 -0
  29. package/extensions/explore/ast/identity.ts +119 -0
  30. package/extensions/explore/ast/ir.ts +130 -0
  31. package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
  32. package/extensions/explore/ast/languages/csharp.ts +493 -0
  33. package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
  34. package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
  35. package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
  36. package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
  37. package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
  38. package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
  39. package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
  40. package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
  41. package/extensions/explore/ast/languages/go.ts +545 -0
  42. package/extensions/explore/ast/languages/java.ts +412 -0
  43. package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
  44. package/extensions/explore/ast/languages/kotlin.ts +611 -0
  45. package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
  46. package/extensions/explore/ast/languages/odin.ts +407 -0
  47. package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
  48. package/extensions/explore/ast/languages/rust.ts +506 -0
  49. package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
  50. package/extensions/explore/ast/languages/swift.ts +466 -0
  51. package/extensions/explore/ast/languages/tree.ts +220 -0
  52. package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
  53. package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
  54. package/extensions/explore/ast/languages/typescript.ts +716 -0
  55. package/extensions/explore/ast/markdown.ts +145 -0
  56. package/extensions/explore/ast/package-surface.ts +42 -0
  57. package/extensions/explore/ast/queries/ast-search.ts +306 -0
  58. package/extensions/explore/ast/queries/composite-target.ts +77 -0
  59. package/extensions/explore/ast/queries/context.ts +406 -0
  60. package/extensions/explore/ast/queries/discover.ts +427 -0
  61. package/extensions/explore/ast/queries/impact.ts +250 -0
  62. package/extensions/explore/ast/queries/outline.ts +203 -0
  63. package/extensions/explore/ast/queries/show.ts +262 -0
  64. package/extensions/explore/ast/query.ts +48 -0
  65. package/extensions/explore/ast/read/hook.ts +125 -0
  66. package/extensions/explore/ast/read/policy.ts +55 -0
  67. package/extensions/explore/ast/registry.ts +120 -0
  68. package/extensions/explore/ast/scan.ts +76 -0
  69. package/extensions/explore/ast/slice.ts +31 -0
  70. package/extensions/explore/ast/tools/ast-search.ts +138 -0
  71. package/extensions/explore/ast/tools/context.ts +126 -0
  72. package/extensions/explore/ast/tools/deps.ts +72 -0
  73. package/extensions/explore/ast/tools/discover.ts +208 -0
  74. package/extensions/explore/ast/tools/impact.ts +135 -0
  75. package/extensions/explore/ast/tools/outline.ts +200 -0
  76. package/extensions/explore/ast/tools/relationships.ts +160 -0
  77. package/extensions/explore/ast/tools/render.ts +121 -0
  78. package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
  79. package/extensions/explore/ast/tools/show.ts +116 -0
  80. package/extensions/explore/guidance.ts +52 -0
  81. package/extensions/explore/index.ts +91 -45
  82. package/extensions/explore/read/autoread.ts +118 -0
  83. package/extensions/explore/settings.ts +64 -0
  84. package/extensions/explore/traverse.ts +260 -86
  85. package/extensions/handoff/index.ts +1 -1
  86. package/extensions/patch/executor.ts +95 -1
  87. package/extensions/patch/index.ts +1 -0
  88. package/extensions/publish/index.ts +18 -85
  89. package/extensions/publish/publish-ui.ts +0 -4
  90. package/extensions/soul/README.md +7 -4
  91. package/extensions/soul/index.ts +11 -5
  92. package/extensions/soul/prompt.ts +30 -14
  93. package/extensions/soul/settings.ts +7 -2
  94. package/extensions/subagent/agents/context-sync.md +6 -10
  95. package/extensions/subagent/agents/review.md +44 -24
  96. package/extensions/subagent/agents/scout.md +53 -34
  97. package/extensions/subagent/run.ts +1 -1
  98. package/extensions/tau-help/help.md +2 -2
  99. package/package.json +6 -5
  100. package/schemas/tau.schema.json +51 -6
  101. package/{extensions/explore → shared}/autoread.ts +21 -7
  102. package/shared/bounded-text-result.ts +304 -0
  103. package/shared/events.ts +1 -0
  104. package/shared/full-file-knowledge.ts +49 -0
  105. package/shared/temporary-output-store.ts +275 -0
  106. package/extensions/explore/ast-tools.ts +0 -828
  107. package/extensions/explore/ast-worker.ts +0 -356
  108. package/extensions/explore/find.ts +0 -147
  109. package/extensions/explore/full-file-knowledge.ts +0 -234
  110. package/extensions/explore/grep.ts +0 -749
  111. package/extensions/explore/limits.ts +0 -22
  112. package/extensions/explore/ls.ts +0 -113
  113. package/extensions/explore/path-display.ts +0 -41
  114. package/extensions/explore/path-tree.ts +0 -150
  115. package/extensions/explore/read-cache.ts +0 -273
  116. package/extensions/explore/read-snapshots.ts +0 -57
  117. package/extensions/explore/read-stats-panel.ts +0 -154
  118. package/extensions/explore/read-stats.ts +0 -120
  119. package/extensions/explore/read.ts +0 -335
  120. package/extensions/explore/result.ts +0 -29
  121. package/native-bin/darwin-arm64/tau-ast +0 -0
package/docs/subagents.md CHANGED
@@ -83,7 +83,7 @@ Tau assigns one display name to each fresh child and keeps it for follow-up turn
83
83
  ## Built-ins
84
84
 
85
85
  - `review` — adversarial, read-only review for correctness, runtime risks, duplication, and over- or under-engineering
86
- - `scout` — find local files, symbols, data flow, constraints, and unknowns without changing anything
86
+ - `scout` — tiered, AST-first local discovery of files, symbols, data flow, constraints, and unknowns without changing anything
87
87
  - `web-research` — `websearch`, `codesearch`, `webfetch`
88
88
  - `context-sync` — maps meaningful uncommitted work into `.pi/contexts`. Offered to the coding agent when `extensions.context.sync.enabled` and `sync.automation` are true. Manual `/context-sync` remains when sync is enabled with `automation` false. Validation can auto-run it when `validation.enabled` and `sync.enabled`
89
89
 
@@ -1,6 +1,6 @@
1
1
  # Appshot
2
2
 
3
- Appshot gives Tau direct discovery and capture of macOS application windows. Tau can list visible windows as compact TOON with their application identity, title, process ID, bounds, and stable window ID; capture an exact window as a PNG for visual inspection; and bring an application forward when needed. Captures preserve aspect ratio and fit within 1568×1568 pixels to keep image payloads bounded.
3
+ Appshot gives Tau direct discovery and capture of macOS application windows. Tau can list visible windows as compact JSON with their application identity, title, process ID, bounds, and stable window ID; capture an exact window as a PNG for visual inspection; and bring an application forward when needed. Captures preserve aspect ratio and fit within 1568×1568 pixels to keep image payloads bounded.
4
4
 
5
5
  The extension provides three tools:
6
6
 
@@ -6,7 +6,6 @@ import {
6
6
  type ExtensionAPI,
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
  import { Text } from "@earendil-works/pi-tui";
9
- import { encode } from "@toon-format/toon";
10
9
  import { randomUUID } from "node:crypto";
11
10
  import { mkdir, readFile, rename, rm, stat } from "node:fs/promises";
12
11
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
@@ -86,20 +85,15 @@ function encodeWindowList(windows: WindowInfo[]): string {
86
85
  height: window.bounds.height,
87
86
  }));
88
87
  const render = (count: number, omitted: number) =>
89
- encode(omitted === 0 ? { windows: rows.slice(0, count) } : { windows: rows.slice(0, count), omitted }, {
90
- indent: 1,
91
- });
88
+ JSON.stringify(omitted === 0 ? { windows: rows.slice(0, count) } : { windows: rows.slice(0, count), omitted });
92
89
  const fits = (value: string) =>
93
90
  Buffer.byteLength(value, "utf8") <= DEFAULT_MAX_BYTES && value.split("\n").length <= DEFAULT_MAX_LINES;
94
- const maximumRows = Math.max(0, DEFAULT_MAX_LINES - 2);
95
91
 
96
- if (rows.length <= maximumRows) {
97
- const full = render(rows.length, 0);
98
- if (fits(full)) return full;
99
- }
92
+ const full = render(rows.length, 0);
93
+ if (fits(full)) return full;
100
94
 
101
95
  let low = 0;
102
- let high = Math.min(rows.length, maximumRows);
96
+ let high = rows.length;
103
97
  while (low < high) {
104
98
  const middle = Math.ceil((low + high) / 2);
105
99
  if (fits(render(middle, rows.length - middle))) low = middle;
@@ -116,7 +110,7 @@ function registerAppshotTools(pi: ExtensionAPI, runHelper: RunHelper): void {
116
110
  name: "list_windows",
117
111
  label: "List Windows",
118
112
  description:
119
- "List visible normal macOS windows as compact TOON with window IDs, titles, application identity, process IDs, and bounds. Use list_windows to discover exact window IDs and application PIDs before screenshot_window or activate_app. Requires macOS 14 or newer and Screen & System Audio Recording permission.",
113
+ "List visible normal macOS windows as compact JSON with window IDs, titles, application identity, process IDs, and bounds. Use list_windows to discover exact window IDs and application PIDs before screenshot_window or activate_app. Requires macOS 14 or newer and Screen & System Audio Recording permission.",
120
114
  parameters: listWindowsSchema,
121
115
  async execute(_toolCallId, _params, signal) {
122
116
  if (process.platform !== "darwin") throw new Error("list_windows is only available on macOS");
@@ -223,12 +223,4 @@ function changedCatalogPaths(before: Map<string, string>, after: Map<string, str
223
223
  }
224
224
 
225
225
  // Ensure evidence tool name stays aligned with agent definition tooling.
226
- export const CONTEXT_SYNC_REQUIRED_TOOLS = [
227
- "read",
228
- "ls",
229
- "find",
230
- "grep",
231
- "bash",
232
- "patch",
233
- CONTEXT_SYNC_EVIDENCE_TOOL,
234
- ] as const;
226
+ export const CONTEXT_SYNC_REQUIRED_TOOLS = ["read", "bash", "patch", CONTEXT_SYNC_EVIDENCE_TOOL] as const;
@@ -20,7 +20,7 @@ Context Pruning does not impose a minimum token saving and does not reject a che
20
20
 
21
21
  ## Automatic and manual requests
22
22
 
23
- Tau checks context growth after tool-using turns and can send progressively stronger private instructions from `nudgeInstructions`. Growth is measured from the first tool-using turn after the latest checkpoint. Branch navigation and compaction reconstruct that baseline from the active branch.
23
+ Tau checks active context size after tool-using turns and can send progressively stronger private instructions from `nudgeInstructions`. Hints occur at fixed token intervals rather than percentages of the model's context window. After a checkpoint, Tau suppresses boundaries already crossed by the resulting context and resumes at the next boundary. Branch navigation and compaction reconstruct that state from the active branch.
24
24
 
25
25
  Run `/prune` with no arguments to ask the agent to create a checkpoint and continue its task immediately.
26
26
 
@@ -35,5 +35,5 @@ Normal Pi compaction remains independent. When a compaction no longer includes a
35
35
  Settings live under `extensions.contextPruning` in Tau settings.
36
36
 
37
37
  - `enabled`: enables the tool, `/prune`, projection, markers, and branch replay. Defaults to `true`.
38
- - `nudgeEveryPercent`: context-growth interval between automatic hints, from `1` through `100`. Defaults to `20`.
38
+ - `nudgeEveryTokens`: active-context interval between automatic hints. Defaults to `30000`, producing the default instruction ladder at 30k, 60k, and 90k tokens.
39
39
  - `nudgeInstructions`: ordered list of one through five nonempty instructions. Later reminders repeat the final instruction. Defaults to three escalating instructions.
@@ -15,11 +15,11 @@ import { createToolRowStateStore } from "../../shared/tool-row-state.ts";
15
15
  import { contextPruneParameters, executeContextPrune } from "./prune.ts";
16
16
  import { projectContext } from "./projection.ts";
17
17
  import {
18
- parseContextPruningNudgeDetailsV2,
18
+ parseContextPruningNudgeDetailsV3,
19
19
  renderContextPruneCall,
20
20
  renderContextPruneResult,
21
21
  renderContextPruningNudge,
22
- type ContextPruningNudgeDetailsV2,
22
+ type ContextPruningNudgeDetailsV3,
23
23
  } from "./render.ts";
24
24
  import contextPruningSettings from "./settings.ts";
25
25
 
@@ -30,8 +30,8 @@ const NUDGE_BASELINE_ENTRY_TYPE = "tau.context-pruning.nudge-baseline";
30
30
 
31
31
  interface NudgeState {
32
32
  anchorToolCallId: string | undefined;
33
- growthBaselinePercent: number | undefined;
34
- highestBoundary: number;
33
+ suppressedThroughTokens: number | undefined;
34
+ highestBoundaryTokens: number;
35
35
  highestTier: number;
36
36
  terminalTierReached: boolean;
37
37
  }
@@ -39,20 +39,20 @@ interface NudgeState {
39
39
  export default function contextPruningExtension(pi: ExtensionAPI): void {
40
40
  let enabled = false;
41
41
  let lifecycleGeneration = 0;
42
- let nudgeEveryPercent = contextPruningSettings.defaults.nudgeEveryPercent;
42
+ let nudgeEveryTokens = contextPruningSettings.defaults.nudgeEveryTokens;
43
43
  let nudgeInstructions = contextPruningSettings.defaults.nudgeInstructions;
44
44
  let toolRegistered = false;
45
45
  let commandRegistered = false;
46
46
  let visualRows = new Set<string>();
47
47
  let nudgeState: NudgeState = {
48
48
  anchorToolCallId: undefined,
49
- growthBaselinePercent: 0,
50
- highestBoundary: 0,
49
+ suppressedThroughTokens: 0,
50
+ highestBoundaryTokens: 0,
51
51
  highestTier: 0,
52
52
  terminalTierReached: false,
53
53
  };
54
54
  const rowState = createToolRowStateStore(pi, "context-pruning.tool-row-state");
55
- pi.registerMessageRenderer<ContextPruningNudgeDetailsV2>(NUDGE_MESSAGE_TYPE, (message, _options, theme) =>
55
+ pi.registerMessageRenderer<ContextPruningNudgeDetailsV3>(NUDGE_MESSAGE_TYPE, (message, _options, theme) =>
56
56
  renderContextPruningNudge(message.details, theme),
57
57
  );
58
58
 
@@ -98,7 +98,7 @@ export default function contextPruningExtension(pi: ExtensionAPI): void {
98
98
  const settings = await loadTauExtensionSettings(ctx, contextPruningSettings);
99
99
  if (generation !== lifecycleGeneration) return;
100
100
  enabled = settings.enabled;
101
- nudgeEveryPercent = settings.nudgeEveryPercent;
101
+ nudgeEveryTokens = settings.nudgeEveryTokens;
102
102
  nudgeInstructions = settings.nudgeInstructions;
103
103
  setContextPruningEnabled(enabled);
104
104
  if (enabled && !toolRegistered) {
@@ -162,22 +162,22 @@ export default function contextPruningExtension(pi: ExtensionAPI): void {
162
162
  commandContext.sessionManager.getBranch(),
163
163
  true,
164
164
  ).latestAnchorToolCallId;
165
- pi.sendMessage<ContextPruningNudgeDetailsV2>(
165
+ pi.sendMessage<ContextPruningNudgeDetailsV3>(
166
166
  {
167
167
  customType: NUDGE_MESSAGE_TYPE,
168
168
  content: manualPruneSteeringMessage(),
169
169
  display: true,
170
170
  details: {
171
- v: 2,
171
+ v: 3,
172
172
  kind: "manual",
173
- percent: null,
174
- boundary: null,
173
+ tokens: null,
174
+ boundaryTokens: null,
175
175
  reminder: null,
176
176
  tier: null,
177
177
  tierCount: null,
178
178
  tierFloor: null,
179
179
  anchorToolCallId: anchorToolCallId ?? null,
180
- growthBaselinePercent: null,
180
+ suppressedThroughTokens: null,
181
181
  },
182
182
  },
183
183
  { deliverAs: "steer", triggerTurn: true },
@@ -204,8 +204,8 @@ export default function contextPruningExtension(pi: ExtensionAPI): void {
204
204
  visualRows.clear();
205
205
  nudgeState = {
206
206
  anchorToolCallId: undefined,
207
- growthBaselinePercent: 0,
208
- highestBoundary: 0,
207
+ suppressedThroughTokens: 0,
208
+ highestBoundaryTokens: 0,
209
209
  highestTier: 0,
210
210
  terminalTierReached: false,
211
211
  };
@@ -216,45 +216,44 @@ export default function contextPruningExtension(pi: ExtensionAPI): void {
216
216
  pi.on("turn_end", (event, ctx) => {
217
217
  if (!enabled || event.toolResults.length === 0) return undefined;
218
218
  const usage = ctx.getContextUsage();
219
- if (!usage || usage.percent === null || !Number.isFinite(usage.percent)) return undefined;
220
- const rawPercent = Math.max(0, Math.min(100, usage.percent));
221
- const percent = Math.floor(rawPercent);
219
+ if (!usage || usage.tokens === null || !Number.isFinite(usage.tokens)) return undefined;
220
+ const tokens = Math.max(0, Math.floor(usage.tokens));
222
221
  const activeAnchor = replayContextPruningState(ctx.sessionManager.getBranch(), true).latestAnchorToolCallId;
223
222
  if (activeAnchor !== nudgeState.anchorToolCallId) {
224
223
  nudgeState = reconstructNudgeState(ctx.sessionManager.getBranch(), activeAnchor);
225
224
  }
226
- if (activeAnchor !== undefined && nudgeState.growthBaselinePercent === undefined) {
227
- const baselinePercent = Math.ceil(rawPercent);
225
+ if (activeAnchor !== undefined && nudgeState.suppressedThroughTokens === undefined) {
226
+ const suppressedThroughTokens = Math.floor(tokens / nudgeEveryTokens) * nudgeEveryTokens;
228
227
  pi.appendEntry(NUDGE_BASELINE_ENTRY_TYPE, {
229
- v: 1,
228
+ v: 2,
230
229
  anchorToolCallId: activeAnchor,
231
- baselinePercent,
230
+ suppressedThroughTokens,
232
231
  });
233
- nudgeState.growthBaselinePercent = baselinePercent;
232
+ nudgeState.suppressedThroughTokens = suppressedThroughTokens;
233
+ nudgeState.highestBoundaryTokens = suppressedThroughTokens;
234
234
  return undefined;
235
235
  }
236
- const baseline = nudgeState.growthBaselinePercent ?? 0;
237
- const reminder = Math.floor((percent - baseline) / nudgeEveryPercent);
236
+ const reminder = Math.floor(tokens / nudgeEveryTokens);
238
237
  if (reminder < 1) return undefined;
239
- const boundary = baseline + reminder * nudgeEveryPercent;
240
- if (boundary <= nudgeState.highestBoundary) return undefined;
238
+ const boundaryTokens = reminder * nudgeEveryTokens;
239
+ if (boundaryTokens <= nudgeState.highestBoundaryTokens) return undefined;
241
240
  const tierCount = nudgeInstructions.length;
242
241
  const tierFloor = nudgeState.terminalTierReached ? tierCount : Math.min(nudgeState.highestTier, tierCount);
243
242
  const tier = Math.max(Math.min(reminder, tierCount), tierFloor);
244
243
  const instruction = nudgeInstructions[tier - 1] ?? nudgeInstructions[0];
245
- const details: ContextPruningNudgeDetailsV2 = {
246
- v: 2,
244
+ const details: ContextPruningNudgeDetailsV3 = {
245
+ v: 3,
247
246
  kind: "automatic",
248
- percent,
249
- boundary,
247
+ tokens,
248
+ boundaryTokens,
250
249
  reminder,
251
250
  tier,
252
251
  tierCount,
253
252
  tierFloor,
254
253
  anchorToolCallId: activeAnchor ?? null,
255
- growthBaselinePercent: baseline,
254
+ suppressedThroughTokens: nudgeState.suppressedThroughTokens ?? 0,
256
255
  };
257
- pi.sendMessage<ContextPruningNudgeDetailsV2>(
256
+ pi.sendMessage<ContextPruningNudgeDetailsV3>(
258
257
  {
259
258
  customType: NUDGE_MESSAGE_TYPE,
260
259
  content: automaticPruneSteeringMessage(instruction, tier === tierCount),
@@ -263,7 +262,7 @@ export default function contextPruningExtension(pi: ExtensionAPI): void {
263
262
  },
264
263
  { deliverAs: "steer" },
265
264
  );
266
- nudgeState.highestBoundary = boundary;
265
+ nudgeState.highestBoundaryTokens = boundaryTokens;
267
266
  nudgeState.highestTier = Math.max(nudgeState.highestTier, tier);
268
267
  nudgeState.terminalTierReached ||= tier === tierCount;
269
268
  return undefined;
@@ -289,8 +288,8 @@ function setsEqual(left: ReadonlySet<string>, right: ReadonlySet<string>): boole
289
288
  }
290
289
 
291
290
  function reconstructNudgeState(branch: readonly SessionEntry[], anchorToolCallId: string | undefined): NudgeState {
292
- let growthBaselinePercent = anchorToolCallId === undefined ? 0 : undefined;
293
- let highestBoundary = 0;
291
+ let suppressedThroughTokens = anchorToolCallId === undefined ? 0 : undefined;
292
+ let highestBoundaryTokens = 0;
294
293
  let highestTier = 0;
295
294
  let terminalTierReached = false;
296
295
  let anchorResultIndex = -1;
@@ -310,65 +309,67 @@ function reconstructNudgeState(branch: readonly SessionEntry[], anchorToolCallId
310
309
  const baseline = parseNudgeBaseline(entry.data);
311
310
  if (
312
311
  baseline &&
313
- growthBaselinePercent === undefined &&
312
+ suppressedThroughTokens === undefined &&
314
313
  index > anchorResultIndex &&
315
314
  baseline.anchorToolCallId === anchorToolCallId
316
315
  ) {
317
- growthBaselinePercent = baseline.baselinePercent;
316
+ suppressedThroughTokens = baseline.suppressedThroughTokens;
317
+ highestBoundaryTokens = baseline.suppressedThroughTokens;
318
318
  }
319
319
  continue;
320
320
  }
321
321
  if (entry.type !== "custom_message" || entry.customType !== NUDGE_MESSAGE_TYPE) continue;
322
- const details = parseContextPruningNudgeDetailsV2(entry.details);
322
+ const details = parseContextPruningNudgeDetailsV3(entry.details);
323
323
  if (
324
324
  !details ||
325
325
  details.kind !== "automatic" ||
326
326
  index <= anchorResultIndex ||
327
327
  details.anchorToolCallId !== (anchorToolCallId ?? null) ||
328
- details.boundary === null ||
329
- details.growthBaselinePercent === null ||
330
- (growthBaselinePercent !== undefined && details.growthBaselinePercent !== growthBaselinePercent)
328
+ details.boundaryTokens === null ||
329
+ details.suppressedThroughTokens === null ||
330
+ (suppressedThroughTokens !== undefined && details.suppressedThroughTokens !== suppressedThroughTokens)
331
331
  )
332
332
  continue;
333
333
  const expectedTierFloor: number = terminalTierReached
334
334
  ? details.tierCount
335
335
  : Math.min(highestTier, details.tierCount);
336
- if (details.boundary <= highestBoundary || details.tierFloor !== expectedTierFloor) continue;
337
- highestBoundary = Math.max(highestBoundary, details.boundary);
336
+ if (details.boundaryTokens <= highestBoundaryTokens || details.tierFloor !== expectedTierFloor) continue;
337
+ highestBoundaryTokens = Math.max(highestBoundaryTokens, details.boundaryTokens);
338
338
  highestTier = Math.max(highestTier, details.tier);
339
339
  terminalTierReached ||= details.tier === details.tierCount;
340
- growthBaselinePercent = details.growthBaselinePercent;
340
+ suppressedThroughTokens = details.suppressedThroughTokens;
341
341
  }
342
- return { anchorToolCallId, growthBaselinePercent, highestBoundary, highestTier, terminalTierReached };
342
+ return { anchorToolCallId, suppressedThroughTokens, highestBoundaryTokens, highestTier, terminalTierReached };
343
343
  }
344
344
 
345
- function parseNudgeBaseline(value: unknown): { v: 1; anchorToolCallId: string; baselinePercent: number } | undefined {
345
+ function parseNudgeBaseline(
346
+ value: unknown,
347
+ ): { v: 2; anchorToolCallId: string; suppressedThroughTokens: number } | undefined {
346
348
  if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
347
349
  const record = value as Record<string, unknown>;
348
350
  if (
349
351
  Object.keys(record).length !== 3 ||
350
352
  !Object.hasOwn(record, "v") ||
351
353
  !Object.hasOwn(record, "anchorToolCallId") ||
352
- !Object.hasOwn(record, "baselinePercent") ||
353
- record.v !== 1 ||
354
+ !Object.hasOwn(record, "suppressedThroughTokens") ||
355
+ record.v !== 2 ||
354
356
  typeof record.anchorToolCallId !== "string" ||
355
357
  record.anchorToolCallId.length === 0 ||
356
- typeof record.baselinePercent !== "number" ||
357
- !Number.isInteger(record.baselinePercent) ||
358
- record.baselinePercent < 0 ||
359
- record.baselinePercent > 100
358
+ typeof record.suppressedThroughTokens !== "number" ||
359
+ !Number.isSafeInteger(record.suppressedThroughTokens) ||
360
+ record.suppressedThroughTokens < 0
360
361
  )
361
362
  return undefined;
362
363
  return {
363
- v: 1,
364
+ v: 2,
364
365
  anchorToolCallId: record.anchorToolCallId,
365
- baselinePercent: record.baselinePercent,
366
+ suppressedThroughTokens: record.suppressedThroughTokens,
366
367
  };
367
368
  }
368
369
 
369
370
  function automaticPruneSteeringMessage(instruction: string, finalTier: boolean): string {
370
371
  const silent =
371
- "Internal context-management instruction. Follow it silently. Do not mention or acknowledge context percentages, prune messages, or internal context management.";
372
+ "Internal context-management instruction. Follow it silently. Do not mention or acknowledge context-token counts, prune messages, or internal context management.";
372
373
  const protocol =
373
374
  "When pruning, first preserve durable conclusions, user constraints, conditional relevance, and the next action in visible prose, then call context_prune.";
374
375
  return finalTier
@@ -2,8 +2,8 @@ import { resolve } from "node:path";
2
2
  import { type ExtensionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
3
3
  import { type Static, Type } from "typebox";
4
4
  import type { ContextPruneDeferredFileV2, ContextPruneDetailsV2 } from "../../shared/context-pruning-state.ts";
5
- import { prepareAutoreadMessage, type PreparedAutoreadMessage } from "../explore/autoread.ts";
6
- import { MAX_COMPLETE_FILE_SNAPSHOT_BYTES } from "../explore/full-file-knowledge.ts";
5
+ import { prepareAutoreadMessage, type PreparedAutoreadMessage } from "../../shared/autoread.ts";
6
+ import { MAX_COMPLETE_FILE_SNAPSHOT_BYTES } from "../../shared/full-file-knowledge.ts";
7
7
 
8
8
  export const contextPruneParameters = Type.Object(
9
9
  {
@@ -10,48 +10,48 @@ const MAX_EXPANDED_LINE_CHARACTERS = 240;
10
10
  const MAX_EXPANDED_TEXT_CHARACTERS = 3_000;
11
11
  const MAX_WARNING_CHARACTERS = 1_000;
12
12
 
13
- export type ContextPruningNudgeDetailsV2 =
13
+ export type ContextPruningNudgeDetailsV3 =
14
14
  | {
15
- v: 2;
15
+ v: 3;
16
16
  kind: "automatic";
17
- percent: number;
18
- boundary: number;
17
+ tokens: number;
18
+ boundaryTokens: number;
19
19
  reminder: number;
20
20
  tier: number;
21
21
  tierCount: number;
22
22
  tierFloor: number;
23
23
  anchorToolCallId: string | null;
24
- growthBaselinePercent: number;
24
+ suppressedThroughTokens: number;
25
25
  }
26
26
  | {
27
- v: 2;
27
+ v: 3;
28
28
  kind: "manual";
29
- percent: null;
30
- boundary: null;
29
+ tokens: null;
30
+ boundaryTokens: null;
31
31
  reminder: null;
32
32
  tier: null;
33
33
  tierCount: null;
34
34
  tierFloor: null;
35
35
  anchorToolCallId: string | null;
36
- growthBaselinePercent: null;
36
+ suppressedThroughTokens: null;
37
37
  };
38
38
 
39
- export function parseContextPruningNudgeDetailsV2(value: unknown): ContextPruningNudgeDetailsV2 | undefined {
39
+ export function parseContextPruningNudgeDetailsV3(value: unknown): ContextPruningNudgeDetailsV3 | undefined {
40
40
  if (!isRecord(value)) return undefined;
41
41
  const keys = [
42
42
  "v",
43
43
  "kind",
44
- "percent",
45
- "boundary",
44
+ "tokens",
45
+ "boundaryTokens",
46
46
  "reminder",
47
47
  "tier",
48
48
  "tierCount",
49
49
  "tierFloor",
50
50
  "anchorToolCallId",
51
- "growthBaselinePercent",
51
+ "suppressedThroughTokens",
52
52
  ];
53
53
  if (Object.keys(value).length !== keys.length || !keys.every((key) => Object.hasOwn(value, key))) return undefined;
54
- if (value.v !== 2 || (value.kind !== "automatic" && value.kind !== "manual")) return undefined;
54
+ if (value.v !== 3 || (value.kind !== "automatic" && value.kind !== "manual")) return undefined;
55
55
  if (
56
56
  value.anchorToolCallId !== null &&
57
57
  (typeof value.anchorToolCallId !== "string" || value.anchorToolCallId.length === 0)
@@ -60,64 +60,69 @@ export function parseContextPruningNudgeDetailsV2(value: unknown): ContextPrunin
60
60
  }
61
61
  if (value.kind === "manual") {
62
62
  if (
63
- value.percent !== null ||
64
- value.boundary !== null ||
63
+ value.tokens !== null ||
64
+ value.boundaryTokens !== null ||
65
65
  value.reminder !== null ||
66
66
  value.tier !== null ||
67
67
  value.tierCount !== null ||
68
68
  value.tierFloor !== null ||
69
- value.growthBaselinePercent !== null
69
+ value.suppressedThroughTokens !== null
70
70
  )
71
71
  return undefined;
72
72
  return {
73
- v: 2,
73
+ v: 3,
74
74
  kind: "manual",
75
- percent: null,
76
- boundary: null,
75
+ tokens: null,
76
+ boundaryTokens: null,
77
77
  reminder: null,
78
78
  tier: null,
79
79
  tierCount: null,
80
80
  tierFloor: null,
81
81
  anchorToolCallId: value.anchorToolCallId,
82
- growthBaselinePercent: null,
82
+ suppressedThroughTokens: null,
83
83
  };
84
84
  }
85
85
  if (
86
- !isPercent(value.percent) ||
87
- !isBoundary(value.boundary) ||
86
+ !isTokenCount(value.tokens) ||
87
+ !isBoundary(value.boundaryTokens) ||
88
88
  !isReminder(value.reminder) ||
89
89
  !isTier(value.tier) ||
90
90
  !isTier(value.tierCount) ||
91
91
  !isTierFloor(value.tierFloor) ||
92
- !isPercent(value.growthBaselinePercent) ||
93
- value.boundary > value.percent ||
94
- value.boundary <= value.growthBaselinePercent ||
92
+ !isTokenCount(value.suppressedThroughTokens) ||
93
+ value.boundaryTokens > value.tokens ||
94
+ value.boundaryTokens <= value.suppressedThroughTokens ||
95
95
  value.tier > value.tierCount ||
96
96
  value.tierFloor > value.tierCount ||
97
97
  value.tier !== Math.max(Math.min(value.reminder, value.tierCount), value.tierFloor) ||
98
- (value.anchorToolCallId === null && value.growthBaselinePercent !== 0)
98
+ (value.anchorToolCallId === null && value.suppressedThroughTokens !== 0)
99
99
  ) {
100
100
  return undefined;
101
101
  }
102
- const interval = (value.boundary - value.growthBaselinePercent) / value.reminder;
103
- if (!Number.isInteger(interval) || interval < 1 || interval > 100 || value.percent - value.boundary >= interval)
102
+ const interval = value.boundaryTokens / value.reminder;
103
+ if (
104
+ !Number.isSafeInteger(interval) ||
105
+ interval < 1 ||
106
+ value.tokens - value.boundaryTokens >= interval ||
107
+ value.suppressedThroughTokens % interval !== 0
108
+ )
104
109
  return undefined;
105
110
  return {
106
- v: 2,
111
+ v: 3,
107
112
  kind: "automatic",
108
- percent: value.percent,
109
- boundary: value.boundary,
113
+ tokens: value.tokens,
114
+ boundaryTokens: value.boundaryTokens,
110
115
  reminder: value.reminder,
111
116
  tier: value.tier,
112
117
  tierCount: value.tierCount,
113
118
  tierFloor: value.tierFloor,
114
119
  anchorToolCallId: value.anchorToolCallId,
115
- growthBaselinePercent: value.growthBaselinePercent,
120
+ suppressedThroughTokens: value.suppressedThroughTokens,
116
121
  };
117
122
  }
118
123
 
119
124
  export function renderContextPruningNudge(details: unknown, theme: Theme): Marker | undefined {
120
- const parsed = parseContextPruningNudgeDetailsV2(details);
125
+ const parsed = parseContextPruningNudgeDetailsV3(details);
121
126
  if (!parsed) return undefined;
122
127
  return new Marker({
123
128
  theme,
@@ -127,7 +132,7 @@ export function renderContextPruningNudge(details: unknown, theme: Theme): Marke
127
132
  parsed.kind === "manual"
128
133
  ? ["Prune requested."]
129
134
  : [
130
- `${parsed.percent}%`,
135
+ formatTokens(parsed.tokens),
131
136
  ...(parsed.tier === parsed.tierCount ? ["Prune now."] : parsed.tier > 1 ? ["Prune soon."] : []),
132
137
  ],
133
138
  });
@@ -214,16 +219,23 @@ function firstResultText(result: AgentToolResult<unknown>): string {
214
219
  return "";
215
220
  }
216
221
 
217
- function isPercent(value: unknown): value is number {
218
- return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 100;
222
+ function formatTokens(count: number): string {
223
+ if (count < 1_000) return `${count}`;
224
+ if (count < 10_000) return `${(count / 1_000).toFixed(1)}k`;
225
+ if (count < 1_000_000) return `${Math.round(count / 1_000)}k`;
226
+ return `${(count / 1_000_000).toFixed(1)}M`;
227
+ }
228
+
229
+ function isTokenCount(value: unknown): value is number {
230
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
219
231
  }
220
232
 
221
233
  function isBoundary(value: unknown): value is number {
222
- return isPercent(value) && value > 0;
234
+ return isTokenCount(value) && value > 0;
223
235
  }
224
236
 
225
237
  function isReminder(value: unknown): value is number {
226
- return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 100;
238
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
227
239
  }
228
240
 
229
241
  function isTier(value: unknown): value is number {
@@ -11,18 +11,17 @@ export default defineTauExtensionSettings({
11
11
  key: "contextPruning",
12
12
  defaults: {
13
13
  enabled: true as boolean,
14
- nudgeEveryPercent: 20 as number,
14
+ nudgeEveryTokens: 30_000 as number,
15
15
  nudgeInstructions: DEFAULT_NUDGE_INSTRUCTIONS,
16
16
  },
17
17
  schema: Type.Object(
18
18
  {
19
19
  enabled: Type.Optional(Type.Boolean({ default: true, description: "Enable context pruning." })),
20
- nudgeEveryPercent: Type.Optional(
20
+ nudgeEveryTokens: Type.Optional(
21
21
  Type.Integer({
22
- default: 20,
22
+ default: 30_000,
23
23
  minimum: 1,
24
- maximum: 100,
25
- description: "Context growth interval between automatic pruning hints.",
24
+ description: "Active-context token interval between automatic pruning hints.",
26
25
  }),
27
26
  ),
28
27
  nudgeInstructions: Type.Optional(
@@ -1,21 +1,20 @@
1
1
  # Explore
2
2
 
3
- Explore is Tau's first-party filesystem exploration extension.
3
+ Explore gives Tau 12 structural source tools: outlines, declaration slices, discovery, structural search, graph and relationship queries, impact, and context packs. Targets use path + declaration name, with an optional line to disambiguate.
4
4
 
5
- It exists so agents can inspect paths, discover files, search text, inspect declarations in supported source languages, and read exact source with compact model payloads and readable tool rows. Autoread establishes complete-file knowledge while that source remains in active context. Later reads can return an unchanged marker or a smaller diff instead of resending the file.
5
+ Pi keeps ordinary filesystem tools (`ls`, `find`, `grep`, `read`). Explore adds structure on top of supported source languages via in-process tree-sitter (WASM) on every platform supported by the Node runtime. Registered languages share the same tools and exploration workflow.
6
6
 
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.
7
+ ## Large `read` / autoread
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, 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.
9
+ When `explore.read.enabled` is on (default), a full Pi `read` or autoread of a registered source file (including Markdown) above `explore.read.structureThresholdLines` (default 200) returns an **outline** — declarations or headings — not the full body. Use ranged `read` (`offset`/`limit`, capped by `explore.read.maxRangeLines`) or `show` for bodies and sections. Small files and unregistered paths stay ordinary Pi full text. Set `explore.read.enabled` to `false` to turn the overlay off.
10
10
 
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.
11
+ ## Tools
12
12
 
13
- Use the cheapest useful step:
14
-
15
- 1. Outline a package directory to discover its public API.
16
- 2. Add exact names when likely declarations are known.
17
- 3. Set `includePrivate` when implementation work needs internals.
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.
13
+ - `outline` — declarations and structure for a file, one-level directory, or recursive subtree (no bodies).
14
+ - `show` — exact signature / docs / declaration / declaration+imports for path+name targets.
15
+ - `discover` — find reusable declarations across a repo/package/subtree by name, kind, or docs (signatures only).
16
+ - `deps` / `reverse_deps` — file import graph forward and reverse.
17
+ - `callers` / `callees` / `references` / `implementations` — symbol relationship sites.
18
+ - `impact` — blast radius: symbol callees/callers plus file imports/importers/transitive dependents.
19
+ - `context` — budgeted pack of bodies/signatures around one symbol.
20
+ - `ast_search` — structural pattern search (ast-grep) over supported source.