@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 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");
@@ -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
@@ -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,28 @@
1
1
  # Explore
2
2
 
3
+ Locator edit tools (`replace_declaration`, `replace_body`, `insert_declaration`, and `rename_declaration`) consume numeric declaration locators and reject stale or parser-recovered targets. They validate candidate syntax before writing, invalidate old locators, and return fresh locators after a clean reparse. For Markdown headings, `replace_declaration` replaces the complete section and `replace_body` preserves the heading while replacing its contents. Nested replacement headings must stay below the selected heading depth. Markdown insertion and rename remain unavailable. Code rename requires an explicit file or repository scope. Inferred references change only with approval; ambiguous references stay unchanged and are reported.
4
+
3
5
  Explore is Tau's first-party filesystem exploration extension.
4
6
 
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.
7
+ 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. For configured supported source, the official `read` tool requires a current structural attempt for that exact file fingerprint. Direct file outlines and searches qualify, along with files returned by `symbol`, `api_discover`, structural search, and relationship tools. Markdown is ungated by default. Unsupported files and unavailable workers keep the ordinary read path. 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. A successful Tau patch can return a trusted cached complete-file diff without another structural attempt when the current branch still contains the prior complete-file baseline and the resulting fingerprint matches.
6
8
 
7
9
  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
10
 
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.
11
+ Agents invoke it with `ls`, `find`, `grep`, `api_discover`, `ast_search`, `outline`, `symbol`, `references`, `callers`, `callees`, `implementations`, `tests`, and `read`. The five relationship tools expand a declaration locator across a repository or subtree. Every result reports exact source location, syntactic relationship, exact/inferred/ambiguous certainty, production/test/generated/re-export classification, declaration candidates, and a numeric locator for the nearest complete editable scope. Ambiguous results are marked non-actionable. Scans stay bounded, deterministic, ignore-aware, and cancellable. `ast_search` finds code shapes with ast-grep patterns across one supported source file, repository, package, or subtree. Directory searches require an explicit language; supported files can infer it. Results include exact previews, metavariable bindings, parser uncertainty, numeric locators for matches and enclosing scopes, complete scan counts, and explicit result or traversal limits. Output stays bounded and complete overflow is saved to the active session temporary store. `api_discover` searches declarations across a repository, package, or subtree by exact, prefix, substring, bounded fuzzy, declaration-kind, or documentation query. It filters public, private, source-exported, or package-surface declarations and reports defining files separately from caller access for TypeScript, TSX, Odin, Go, Rust, C#, Java, Kotlin, and Swift. Caller access includes the canonical module path, exact import statement, usable access expression, and resolution uncertainty; TypeScript results also include re-export chains. Candidates contain signatures without implementation bodies and numeric locators accepted by `symbol`. `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 accepts a package directory for one-level inspection, or `recursive: true` for ignore-aware mixed-language orientation of a repository or subtree. Recursive output stays bounded; when the complete outline is larger, Tau saves it to a temporary path for targeted `grep` and ranged `read` during the active session. 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 in four views: `signature` omits documentation and bodies, `signatureWithDocs` adds attached documentation without the body, `declaration` returns exact declaration source, and `declarationWithImports` adds required imports. It retrieves several locators in one call, can add bounded surrounding lines to exact declarations, and rejects the whole batch when any locator is stale. Users run `/read-stats` to see estimated token and cost savings for the session.
12
+
13
+ Explore scans a bounded, ignore-aware slice of the working root before each agent run. When the repository contains supported source and the selected native worker is usable, Explore adds language-specific AST-first guidance to the agent prompt. Unsupported repositories and hosts get the ordinary filesystem workflow. `api_discover`, `outline`, and `symbol` stay registered either way.
14
+
15
+ Installed packages support `api_discover`, `ast_search`, `outline`, `symbol`, and the relationship tools 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.
10
16
 
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.
17
+ For supported source, use the cheapest useful step:
12
18
 
13
- Use the cheapest useful step:
19
+ 1. Identify the current job: locate, reuse, edit, explain, or debug. Avoid loading implementation, callers, tests, or documentation needed only later.
20
+ 2. Use a recursive `outline` to orient an unfamiliar repository or subtree. Outline a known package or file directly. Outline large Markdown files before retrieving a relevant heading section.
21
+ 3. Use `api_discover` when reuse intent is known but the declaration path or exact name is not. Prefer package surfaces and public exports before private declarations.
22
+ 4. Narrow paths, exact names, query work, and result limits once the likely target is known. Enable `includePrivate` only for targeted implementation work.
23
+ 5. Stop at a clear outline or API candidate signature. Use `symbol(signature)` for closer contract inspection, `signatureWithDocs` only when attached documentation matters, and declaration views only for exact source.
24
+ 6. Use `references`, `callers`, `callees`, `implementations`, or `tests` after selecting a change target and when the result can affect the plan. Inspect tests earlier only when the task begins with test behavior or a failure.
25
+ 7. Use `ast_search` for code shapes and `grep` for literal text. Use targeted `read` for exact formatting, comments, parser gaps, unsupported files, or source outside declaration boundaries.
26
+ 8. Prefer locator edits when a complete declaration or body is the natural edit boundary. Use textual patching when the change crosses those boundaries or depends on surrounding text.
14
27
 
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.
28
+ Stop when the current question is answered. Expand exploration only for a specific unresolved question.