@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
@@ -1,11 +1,21 @@
1
1
  ---
2
2
  name: scout
3
- description: Find local files, symbols, data flow, constraints, and unknowns without changing anything
3
+ description: Tiered, AST-first local discovery of files, declarations, data flow, constraints, and unknowns without changes
4
4
  tools:
5
5
  - read
6
- - grep
7
- - find
8
- - ls
6
+ - bash
7
+ - outline
8
+ - show
9
+ - discover
10
+ - ast_search
11
+ - deps
12
+ - reverse_deps
13
+ - callers
14
+ - callees
15
+ - references
16
+ - implementations
17
+ - impact
18
+ - context
9
19
  names:
10
20
  - Pathfinder
11
21
  - Trailblazer
@@ -16,36 +26,50 @@ model: openai-codex/gpt-5.6-luna
16
26
  thinking: high
17
27
  ---
18
28
 
19
- Stay inside delegated task. Answer exactly what was asked. No broader questions, background collection, unrequested recommendations, or mutations.
29
+ Stay inside task. Answer only what was asked. No mutations, side quests, background sweeps, or unasked advice.
20
30
 
21
- Delegating prompt is output contract. Requested shape wins. Otherwise use smallest matching shape below.
31
+ Delegating prompt controls output. Otherwise use smallest matching shape below.
22
32
 
23
- ## Inspection discipline
33
+ ## Evidence ladder
24
34
 
25
- 1. Extract exact target, question, and required output before searching.
26
- 2. Start with named paths and symbols. Use `grep` or `find` for specific evidence. Do not map repository.
27
- 3. Every read answers a pending question. Read smallest useful range. Follow imports, callers, or related files only when evidence requires it.
28
- 4. Use `lineNumbers: true` for text supporting findings. Cite exact `path:start-end` ranges from tool output. Never estimate line numbers.
29
- 5. Stop when every requested field has evidence. Put unresolved facts under `Unknowns`. Do not explore unrelated code for completeness.
35
+ Use cheapest source that proves each claim. Skip steps when task supplies exact path or declaration. Escalate only when current evidence cannot answer.
30
36
 
31
- Absolute paths may identify readable reference repositories outside current working directory.
37
+ 1. **Supplied context** — Treat current line-numbered task files as authoritative this turn.
38
+ 2. **Paths and literals** — Use read-only `bash` (`ls`, `find`, `rg`/`grep`) for narrow path discovery, exact text, registrations, and unsupported formats. Use ranged `read` for formatting or source without structural support.
39
+ 3. **Structure** — Default to `outline` for known files/packages and unfamiliar supported subtrees. Use `discover` when reuse intent is known but path or exact name is not. Use `ast_search` for source shapes.
40
+ 4. **Exact declarations** — Use `show` with path + name (+ line when needed). Prefer `signature`; add docs, body, imports, or context lines only when question requires them.
41
+ 5. **Focused relationships** — After resolving a declaration, use `callers`, `callees`, `references`, or `implementations` for one direct relationship question. Use `deps` and `reverse_deps` for file imports, not declaration calls.
42
+ 6. **Composition** — Use `impact` for full one-hop declaration plus transitive file blast radius. Use `context` for one budgeted declaration pack when nearby bodies and relationships answer faster than separate calls.
43
+
44
+ Structural results prove bounded syntax, not runtime dispatch. Preserve exact, inferred, and ambiguous labels. Do not turn ambiguous sites into claimed impact.
45
+
46
+ ## Search procedure
47
+
48
+ 1. Extract target, question, scope, and requested output shape.
49
+ 2. List required claims and select cheapest evidence for each.
50
+ 3. Start from supplied paths and names. Search outward only for required relationships.
51
+ 4. For reuse, run `discover`, then inspect selected candidates with `show`.
52
+ 5. For unknown source shape, run `ast_search`, then inspect only selected enclosing declarations.
53
+ 6. For behavior or data flow, orient target, follow focused relationships, then retrieve only declarations needed to explain flow.
54
+ 7. For impact, use `impact`; use focused relationship tools only when one section needs closer evidence.
55
+ 8. Stop when requested claims are supported. Put material gaps under `Unknowns`.
56
+
57
+ Absolute paths may point to read-only reference repositories outside cwd.
32
58
 
33
59
  ## Result shapes
34
60
 
35
- Use only relevant sections. Omit empty sections.
61
+ Use relevant sections only. Omit empty sections.
36
62
 
37
63
  ### Locate
38
64
 
39
- `path:start-end` — symbol — match reason
65
+ `path:start-end` — declaration — match reason
40
66
 
41
67
  ### Explain behavior
42
68
 
43
- - `Entry:` `path:start-end` — symbol
44
- - `Flow:` ordered steps; one cited fact per step
69
+ - `Entry:` `path:start-end` — declaration
70
+ - `Flow:` ordered steps; one cited fact each
45
71
  - `Result:` observed outcome
46
72
 
47
- Only branches relevant to requested behavior.
48
-
49
73
  ### Trace data
50
74
 
51
75
  - `Source:` cited origin
@@ -54,12 +78,11 @@ Only branches relevant to requested behavior.
54
78
 
55
79
  ### Find references or impact
56
80
 
57
- - `Direct references:` cited relationships
81
+ - `Direct references:` cited relationships with certainty
82
+ - `Editable scopes:` declarations requiring inspection or change
58
83
  - `Behavior affected:` evidence-backed consequences
59
84
  - `Unknowns:` remaining uncertainty
60
85
 
61
- No speculative blast radius.
62
-
63
86
  ### Verify a claim
64
87
 
65
88
  - `Verdict:` `yes`, `no`, `partially`, or `unknown`
@@ -69,23 +92,19 @@ No speculative blast radius.
69
92
  ### Compare
70
93
 
71
94
  - `Shared:` cited similarities
72
- - `Differences:` cited differences by aspect
95
+ - `Differences:` cited by aspect
73
96
  - `Relevant consequence:` requested consequences only
74
97
 
75
98
  ### Inventory
76
99
 
77
- `path:start-end` — symbol — role
78
-
79
- When completeness matters, state searched scope. If completeness cannot be guaranteed, say why.
80
-
81
- ### Constraints and unknowns
100
+ `path:start-end` — declaration — role
82
101
 
83
- - `Constraints:` constraint — supporting citation
84
- - `Unknowns:` missing fact — evidence needed
102
+ When completeness matters, state searched scope. If uncertain, say why.
85
103
 
86
104
  ## Reporting rules
87
105
 
88
- - Every material code claim needs exact path, line range, and symbol when one exists.
89
- - Separate observed facts from inference. Label inference.
90
- - Quote smallest fragment needed to disambiguate. No whole functions or blocks when citation and concise description suffice.
91
- - No preamble, search log, generic repository summary, repeated evidence, or unrequested next steps.
106
+ - Every material code claim needs exact path, line range, and declaration when one exists.
107
+ - Cite ranges returned by tools. Never estimate line numbers.
108
+ - Separate fact from inference. Label inference.
109
+ - Quote smallest useful fragment.
110
+ - No preamble, search log, generic repository summary, repeated evidence, or unasked next steps.
@@ -12,7 +12,7 @@ import {
12
12
  type ExtensionAPI,
13
13
  type ExtensionContext,
14
14
  } from "@earendil-works/pi-coding-agent";
15
- import { createCompleteFileMeta } from "../explore/full-file-knowledge.ts";
15
+ import { createCompleteFileMeta } from "../../shared/full-file-knowledge.ts";
16
16
  import type { AgentDefinition } from "./agents.ts";
17
17
  import { emptySubagentResumeState, type RetainedTurnOutcome, type SubagentResumeState } from "./resume.ts";
18
18
  import {
@@ -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, 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.
43
+ Structural source tools on in-process tree-sitter (WASM), available on every Node-supported platform. Registers `outline`, `show`, `discover`, `ast_search`, `deps`, `reverse_deps`, `callers`, `callees`, `references`, `implementations`, `impact`, and `context`; symbol targets use path + name (+ line when needed). Pi keeps `ls` / `find` / `grep` / `read`. Large full `read`/autoread of registered source (including Markdown) returns outline by default (`explore.read.*`); ranged `read` or `show` for bodies. Disable with `explore.read.enabled: false`.
44
44
 
45
45
  ## footer
46
46
 
@@ -92,7 +92,7 @@ Runs configured commands while keeping their output out of agent context when th
92
92
 
93
93
  ## soul
94
94
 
95
- Adds Rok’s persona and operating rules to Pi’s native assistant prompt.
95
+ Adds two independently toggleable sections to Pi’s native assistant prompt: `ponytail` (a lazy-senior-dev build ethos) and `caveman` (a terse communication style). Both on by default.
96
96
 
97
97
  ## stash
98
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Tau is a custom agentic harness built with pi extensions",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -31,14 +31,15 @@
31
31
  "themes/**/*",
32
32
  "docs/**/*",
33
33
  "schemas/**/*",
34
- "native-bin/**/*",
35
34
  "README.md"
36
35
  ],
37
36
  "dependencies": {
38
- "@shanepadgett/tau-tui": "0.26.0",
39
- "@toon-format/toon": "2.3.0",
37
+ "@ast-grep/wasm": "0.45.0",
38
+ "@shanepadgett/tau-tui": "0.28.0",
39
+ "@vscode/tree-sitter-wasm": "0.3.1",
40
40
  "image-size": "2.0.2",
41
- "smol-toml": "1.7.0"
41
+ "smol-toml": "1.7.0",
42
+ "web-tree-sitter": "0.26.11"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "@earendil-works/pi-ai": "*",
@@ -61,12 +61,11 @@
61
61
  "default": true,
62
62
  "description": "Enable context pruning."
63
63
  },
64
- "nudgeEveryPercent": {
64
+ "nudgeEveryTokens": {
65
65
  "type": "integer",
66
- "default": 20,
66
+ "default": 30000,
67
67
  "minimum": 1,
68
- "maximum": 100,
69
- "description": "Context growth interval between automatic pruning hints."
68
+ "description": "Active-context token interval between automatic pruning hints."
70
69
  },
71
70
  "nudgeInstructions": {
72
71
  "type": "array",
@@ -87,6 +86,47 @@
87
86
  },
88
87
  "additionalProperties": false
89
88
  },
89
+ "explore": {
90
+ "type": "object",
91
+ "properties": {
92
+ "context": {
93
+ "type": "object",
94
+ "properties": {
95
+ "defaultBudgetTokens": {
96
+ "type": "integer",
97
+ "default": 8000,
98
+ "minimum": 1,
99
+ "description": "Default token budget for context when the caller omits budget."
100
+ }
101
+ },
102
+ "additionalProperties": false
103
+ },
104
+ "read": {
105
+ "type": "object",
106
+ "properties": {
107
+ "enabled": {
108
+ "type": "boolean",
109
+ "default": true,
110
+ "description": "Master switch for Explore structural read overlay and large-source autoread outline path. When false, Pi read and autoread keep ordinary full bodies."
111
+ },
112
+ "structureThresholdLines": {
113
+ "type": "integer",
114
+ "default": 200,
115
+ "minimum": 1,
116
+ "description": "Registered source at or under this line count may be full-read. Above it, full read model-visible result is outline only."
117
+ },
118
+ "maxRangeLines": {
119
+ "type": "integer",
120
+ "default": 200,
121
+ "minimum": 1,
122
+ "description": "Maximum lines returned by one ranged read on registered source."
123
+ }
124
+ },
125
+ "additionalProperties": false
126
+ }
127
+ },
128
+ "additionalProperties": false
129
+ },
90
130
  "footer": {
91
131
  "type": "object",
92
132
  "properties": {
@@ -234,10 +274,15 @@
234
274
  "soul": {
235
275
  "type": "object",
236
276
  "properties": {
237
- "enabled": {
277
+ "ponytail": {
278
+ "type": "boolean",
279
+ "default": true,
280
+ "description": "Add the lazy-senior-dev build ethos to Tau's system prompt."
281
+ },
282
+ "caveman": {
238
283
  "type": "boolean",
239
284
  "default": true,
240
- "description": "Enable Tau's Rok system prompt."
285
+ "description": "Add the terse communication style to Tau's system prompt."
241
286
  }
242
287
  },
243
288
  "additionalProperties": false
@@ -2,11 +2,10 @@ import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
4
  import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
5
- import { onTauEvent, type TauAgentEvents } from "../../shared/events.js";
6
- import type { ToolRowStateStore } from "../../shared/tool-row-state.js";
7
5
  import { Marker, type MarkerState } from "@shanepadgett/tau-tui";
8
- import { createCompleteFileMeta } from "./full-file-knowledge.ts";
9
- import type { ReadCacheMetaV1 } from "./read-cache.ts";
6
+ import { onTauEvent, type TauAgentEvents } from "./events.js";
7
+ import { createCompleteFileMeta, type ReadCacheMetaV1 } from "./full-file-knowledge.ts";
8
+ import type { ToolRowStateStore } from "./tool-row-state.js";
10
9
 
11
10
  const AUTOREAD_MESSAGE_TYPE = "tau.autoread";
12
11
 
@@ -30,6 +29,17 @@ export interface PreparedAutoreadMessage {
30
29
  details: AutoreadDetails & { status: "read"; readCache: ReadCacheMetaV1 };
31
30
  }
32
31
 
32
+ export type PrepareAutoreadMessage = (options: {
33
+ rowId: string;
34
+ path: string;
35
+ cwd: string;
36
+ source: string;
37
+ batchId: string;
38
+ signal: AbortSignal | undefined;
39
+ isLifecycleCurrent: () => boolean;
40
+ maximumBytes?: number;
41
+ }) => Promise<PreparedAutoreadMessage>;
42
+
33
43
  export async function prepareAutoreadMessage(options: {
34
44
  rowId: string;
35
45
  path: string;
@@ -76,7 +86,11 @@ export async function prepareAutoreadMessage(options: {
76
86
  };
77
87
  }
78
88
 
79
- export function registerAutoread(pi: ExtensionAPI, rowState: ToolRowStateStore): void {
89
+ export function registerAutoread(
90
+ pi: ExtensionAPI,
91
+ rowState: ToolRowStateStore,
92
+ prepare: PrepareAutoreadMessage = prepareAutoreadMessage,
93
+ ): void {
80
94
  let lifecycleGeneration = 0;
81
95
  pi.on("session_start", () => {
82
96
  lifecycleGeneration += 1;
@@ -90,7 +104,7 @@ export function registerAutoread(pi: ExtensionAPI, rowState: ToolRowStateStore):
90
104
  pi.on("session_shutdown", () => {
91
105
  lifecycleGeneration += 1;
92
106
  });
93
- onTauEvent(pi, "explore.autoread", "tau:autoread.requested", async (data) => {
107
+ onTauEvent(pi, "shared.autoread", "tau:autoread.requested", async (data) => {
94
108
  const event = readAutoreadRequestedEvent(data);
95
109
  if (!event) return;
96
110
  const generation = lifecycleGeneration;
@@ -105,7 +119,7 @@ export function registerAutoread(pi: ExtensionAPI, rowState: ToolRowStateStore):
105
119
  batchId: event.batchId,
106
120
  } satisfies Omit<AutoreadDetails, "status" | "error">;
107
121
  try {
108
- const message = await prepareAutoreadMessage({
122
+ const message = await prepare({
109
123
  ...details,
110
124
  signal: undefined,
111
125
  isLifecycleCurrent: () => generation === lifecycleGeneration,
@@ -0,0 +1,304 @@
1
+ import {
2
+ DEFAULT_MAX_BYTES,
3
+ DEFAULT_MAX_LINES,
4
+ formatSize,
5
+ truncateHead,
6
+ truncateTail,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ TemporaryOutputError,
10
+ type TemporaryOutputStore,
11
+ type TemporaryOutputWriter,
12
+ } from "./temporary-output-store.ts";
13
+
14
+ export type BoundedTextStrategy = "head" | "tail" | "completeBlocks";
15
+
16
+ export interface BoundedTextOverflowDetails {
17
+ strategy: BoundedTextStrategy;
18
+ truncated: boolean;
19
+ shownLines: number;
20
+ totalLines: number;
21
+ shownBytes: number;
22
+ totalBytes: number;
23
+ fullOutputComplete: boolean;
24
+ temporaryPath: string | undefined;
25
+ failure: "quota" | "write" | undefined;
26
+ }
27
+
28
+ export interface BoundedTextResult {
29
+ content: string;
30
+ overflow: BoundedTextOverflowDetails;
31
+ visibleUnitIds: string[];
32
+ }
33
+
34
+ export function truncateBoundedHead(content: string): ReturnType<typeof truncateHead> {
35
+ return truncateHead(content, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES });
36
+ }
37
+
38
+ interface VisibleBlock {
39
+ id: string | undefined;
40
+ label: string;
41
+ text: string;
42
+ }
43
+
44
+ export class BoundedTextResultBuilder {
45
+ private readonly store: TemporaryOutputStore;
46
+ private readonly strategy: BoundedTextStrategy;
47
+ private snapshot = "";
48
+ private blocks: VisibleBlock[] = [];
49
+ private requiredBlocks: VisibleBlock[] = [];
50
+ private totalBytes = 0;
51
+ private newlineCount = 0;
52
+ private hasContent = false;
53
+ private endsWithNewline = false;
54
+ private overflowed = false;
55
+ private modelClosed = false;
56
+ private oversizedBlockLabel: string | undefined;
57
+ private output: TemporaryOutputWriter | undefined;
58
+ private persistenceFailure: "quota" | "write" | undefined;
59
+ private finished = false;
60
+
61
+ constructor(store: TemporaryOutputStore, strategy: BoundedTextStrategy) {
62
+ this.store = store;
63
+ this.strategy = strategy;
64
+ }
65
+
66
+ async append(text: string): Promise<void> {
67
+ if (this.strategy === "completeBlocks") throw new Error("Complete-block results require appendBlock");
68
+ this.requireOpen();
69
+ this.count(text);
70
+ if (!this.overflowed) {
71
+ const candidate = `${this.snapshot}${text}`;
72
+ const truncation = this.strategy === "head" ? truncateHead(candidate) : truncateTail(candidate);
73
+ this.snapshot = truncation.content;
74
+ if (!truncation.truncated) return;
75
+ this.overflowed = true;
76
+ await this.persist(candidate);
77
+ return;
78
+ }
79
+ if (this.strategy === "tail") this.snapshot = truncateTail(`${this.snapshot}${text}`).content;
80
+ await this.persist(text);
81
+ }
82
+
83
+ async appendBlock(id: string | undefined, label: string, text: string): Promise<void> {
84
+ if (this.strategy !== "completeBlocks") throw new Error("Text results require append");
85
+ this.requireOpen();
86
+ const prefix = this.hasContent ? "\n\n" : "";
87
+ const chunk = `${prefix}${text}`;
88
+ this.count(chunk);
89
+
90
+ if (!this.modelClosed) {
91
+ const candidate = `${this.snapshot}${chunk}`;
92
+ if (!truncateHead(candidate).truncated) {
93
+ this.snapshot = candidate;
94
+ this.blocks.push({ id, label, text });
95
+ return;
96
+ }
97
+ this.modelClosed = true;
98
+ this.overflowed = true;
99
+ if (this.blocks.length === 0) this.oversizedBlockLabel = label.replaceAll(/[\r\n]/g, " ");
100
+ await this.persist(candidate);
101
+ return;
102
+ }
103
+ await this.persist(chunk);
104
+ }
105
+
106
+ async appendRequiredBlock(label: string, text: string): Promise<void> {
107
+ if (this.strategy !== "completeBlocks") throw new Error("Required blocks need complete-block retention");
108
+ this.requireOpen();
109
+ const block = { id: undefined, label, text };
110
+ this.requiredBlocks.push(block);
111
+ const prefix = this.hasContent ? "\n\n" : "";
112
+ const chunk = `${prefix}${text}`;
113
+ this.count(chunk);
114
+ if (!this.overflowed) {
115
+ const candidate = `${this.snapshot}${chunk}`;
116
+ if (!truncateHead(candidate).truncated) {
117
+ this.snapshot = candidate;
118
+ return;
119
+ }
120
+ this.overflowed = true;
121
+ this.modelClosed = true;
122
+ await this.persist(candidate);
123
+ return;
124
+ }
125
+ await this.persist(chunk);
126
+ }
127
+
128
+ async finish(): Promise<BoundedTextResult> {
129
+ this.requireOpen();
130
+ this.finished = true;
131
+ let temporaryPath: string | undefined;
132
+ if (this.output) {
133
+ try {
134
+ temporaryPath = await this.output.finish();
135
+ } catch (error) {
136
+ this.persistenceFailure = error instanceof TemporaryOutputError ? error.code : "write";
137
+ }
138
+ this.output = undefined;
139
+ }
140
+
141
+ const totalLines = lineCount(this.hasContent, this.newlineCount, this.endsWithNewline);
142
+ if (!this.overflowed) {
143
+ return {
144
+ content: this.snapshot,
145
+ overflow: {
146
+ strategy: this.strategy,
147
+ truncated: false,
148
+ shownLines: totalLines,
149
+ totalLines,
150
+ shownBytes: this.totalBytes,
151
+ totalBytes: this.totalBytes,
152
+ fullOutputComplete: true,
153
+ temporaryPath: undefined,
154
+ failure: undefined,
155
+ },
156
+ visibleUnitIds: this.blocks.flatMap((block) => (block.id === undefined ? [] : [block.id])),
157
+ };
158
+ }
159
+
160
+ if (this.strategy === "completeBlocks") {
161
+ return this.finishBlocks(totalLines, temporaryPath);
162
+ }
163
+ return this.finishText(totalLines, temporaryPath);
164
+ }
165
+
166
+ async abort(): Promise<void> {
167
+ this.finished = true;
168
+ const output = this.output;
169
+ this.output = undefined;
170
+ if (output) await output.abort();
171
+ }
172
+
173
+ private finishBlocks(totalLines: number, temporaryPath: string | undefined): BoundedTextResult {
174
+ const blocks = [...this.blocks];
175
+ const required = this.requiredBlocks.map((block) => block.text).join("\n\n");
176
+ let retained = [...blocks.map((block) => block.text), required].filter(Boolean).join("\n\n");
177
+ let shownBytes = Buffer.byteLength(retained);
178
+ let shownLines = textLineCount(retained);
179
+ let notice = this.notice(shownLines, shownBytes, totalLines, temporaryPath);
180
+ const oversized = this.oversizedBlockLabel
181
+ ? `[File block omitted because it exceeds the model-output budget: ${this.oversizedBlockLabel}. Run a non-recursive outline for that file.]`
182
+ : "";
183
+ while (blocks.length > 0 && truncateHead([retained, oversized, notice].filter(Boolean).join("\n\n")).truncated) {
184
+ blocks.pop();
185
+ retained = [...blocks.map((block) => block.text), required].filter(Boolean).join("\n\n");
186
+ shownBytes = Buffer.byteLength(retained);
187
+ shownLines = textLineCount(retained);
188
+ notice = this.notice(shownLines, shownBytes, totalLines, temporaryPath);
189
+ }
190
+ let content = [retained, oversized, notice].filter(Boolean).join("\n\n");
191
+ if (truncateHead(content).truncated) content = truncateHead(content).content;
192
+ return {
193
+ content,
194
+ overflow: this.overflowDetails(shownLines, shownBytes, totalLines, temporaryPath),
195
+ visibleUnitIds: blocks.flatMap((block) => (block.id === undefined ? [] : [block.id])),
196
+ };
197
+ }
198
+
199
+ private finishText(totalLines: number, temporaryPath: string | undefined): BoundedTextResult {
200
+ let retained = this.snapshot;
201
+ let shownBytes = Buffer.byteLength(retained);
202
+ let shownLines = textLineCount(retained);
203
+ let notice = this.notice(shownLines, shownBytes, totalLines, temporaryPath);
204
+ for (let iteration = 0; iteration < 3; iteration += 1) {
205
+ const noticeLines = textLineCount(notice) + 1;
206
+ const noticeBytes = Buffer.byteLength(notice) + 2;
207
+ const options = {
208
+ maxLines: Math.max(0, DEFAULT_MAX_LINES - noticeLines),
209
+ maxBytes: Math.max(0, DEFAULT_MAX_BYTES - noticeBytes),
210
+ };
211
+ retained = (
212
+ this.strategy === "head" ? truncateHead(this.snapshot, options) : truncateTail(this.snapshot, options)
213
+ ).content;
214
+ shownBytes = Buffer.byteLength(retained);
215
+ shownLines = textLineCount(retained);
216
+ notice = this.notice(shownLines, shownBytes, totalLines, temporaryPath);
217
+ }
218
+ return {
219
+ content: [retained, notice].filter(Boolean).join("\n\n"),
220
+ overflow: this.overflowDetails(shownLines, shownBytes, totalLines, temporaryPath),
221
+ visibleUnitIds: [],
222
+ };
223
+ }
224
+
225
+ private overflowDetails(
226
+ shownLines: number,
227
+ shownBytes: number,
228
+ totalLines: number,
229
+ temporaryPath: string | undefined,
230
+ ): BoundedTextOverflowDetails {
231
+ return {
232
+ strategy: this.strategy,
233
+ truncated: true,
234
+ shownLines,
235
+ totalLines,
236
+ shownBytes,
237
+ totalBytes: this.totalBytes,
238
+ fullOutputComplete: temporaryPath !== undefined && this.persistenceFailure === undefined,
239
+ temporaryPath,
240
+ failure: this.persistenceFailure,
241
+ };
242
+ }
243
+
244
+ private notice(
245
+ shownLines: number,
246
+ shownBytes: number,
247
+ totalLines: number,
248
+ temporaryPath: string | undefined,
249
+ ): string {
250
+ return formatBoundedTextOverflow({
251
+ strategy: this.strategy,
252
+ truncated: true,
253
+ shownLines,
254
+ totalLines,
255
+ shownBytes,
256
+ totalBytes: this.totalBytes,
257
+ fullOutputComplete: temporaryPath !== undefined && this.persistenceFailure === undefined,
258
+ temporaryPath,
259
+ failure: this.persistenceFailure,
260
+ });
261
+ }
262
+
263
+ private count(text: string): void {
264
+ if (text.length === 0) return;
265
+ this.totalBytes += Buffer.byteLength(text);
266
+ this.newlineCount += text.split("\n").length - 1;
267
+ this.hasContent = true;
268
+ this.endsWithNewline = text.endsWith("\n");
269
+ }
270
+
271
+ private async persist(text: string): Promise<void> {
272
+ if (this.persistenceFailure) return;
273
+ try {
274
+ this.output ??= await this.store.createOutput();
275
+ await this.output.write(text);
276
+ } catch (error) {
277
+ this.persistenceFailure = error instanceof TemporaryOutputError ? error.code : "write";
278
+ const output = this.output;
279
+ this.output = undefined;
280
+ if (output) await output.abort();
281
+ }
282
+ }
283
+
284
+ private requireOpen(): void {
285
+ if (this.finished) throw new Error("Bounded text result is already finished");
286
+ }
287
+ }
288
+
289
+ function formatBoundedTextOverflow(details: BoundedTextOverflowDetails): string {
290
+ const sizes = `${details.shownLines} of ${details.totalLines} lines (${formatSize(details.shownBytes)} of ${formatSize(details.totalBytes)})`;
291
+ if (details.fullOutputComplete && details.temporaryPath) {
292
+ return `[Output truncated: showing ${sizes}. Complete output: ${details.temporaryPath}\nTemporary output is valid only for the active session. Use targeted grep or ranged read; reading the whole file will hit the same output limits.]`;
293
+ }
294
+ const reason = details.failure === "quota" ? "disk quota exceeded" : "temporary-file write failed";
295
+ return `[Output truncated: showing ${sizes}. Complete temporary output unavailable: ${reason}.]`;
296
+ }
297
+
298
+ function textLineCount(text: string): number {
299
+ return lineCount(text.length > 0, text.split("\n").length - 1, text.endsWith("\n"));
300
+ }
301
+
302
+ function lineCount(hasContent: boolean, newlineCount: number, endsWithNewline: boolean): number {
303
+ return hasContent ? newlineCount + (endsWithNewline ? 0 : 1) : 0;
304
+ }
package/shared/events.ts CHANGED
@@ -25,6 +25,7 @@ export type TauAgentEvents = {
25
25
  move?: { from: string; to: string };
26
26
  linesAdded: number;
27
27
  linesRemoved: number;
28
+ resultingFingerprint: string | null;
28
29
  snapshotRanges?: Array<{ startLine: number; endLine: number }>;
29
30
  }>;
30
31
  };