@shanepadgett/tau-agent 0.33.1 → 0.34.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 (190) hide show
  1. package/docs/context.md +29 -20
  2. package/docs/extending-tau-agent.md +63 -24
  3. package/extensions/appshot/index.ts +112 -111
  4. package/extensions/aside/README.md +7 -0
  5. package/extensions/aside/index.ts +168 -0
  6. package/extensions/aside/panel.ts +83 -0
  7. package/extensions/branch/index.ts +38 -21
  8. package/extensions/cache-diagnostics/index.ts +70 -58
  9. package/extensions/checkpoint/README.md +9 -0
  10. package/extensions/checkpoint/checkpoint-budget.ts +79 -0
  11. package/extensions/checkpoint/checkpoint.ts +277 -0
  12. package/extensions/checkpoint/index.ts +102 -0
  13. package/extensions/checkpoint/messages.ts +169 -0
  14. package/extensions/checkpoint/prompt.ts +24 -0
  15. package/extensions/checkpoint/settings.ts +27 -0
  16. package/extensions/commit/commit-effort.ts +3 -3
  17. package/extensions/context/README.md +12 -8
  18. package/extensions/context/definitions.ts +178 -55
  19. package/extensions/context/index.ts +192 -160
  20. package/extensions/context/panel.ts +57 -37
  21. package/extensions/context/sync.ts +139 -89
  22. package/extensions/context/validation.ts +37 -25
  23. package/extensions/effort/README.md +1 -1
  24. package/extensions/effort/index.ts +3 -3
  25. package/extensions/effort/state.ts +3 -18
  26. package/extensions/explore/guidance.ts +6 -47
  27. package/extensions/explore/index.ts +38 -53
  28. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  29. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  30. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  31. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  32. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  33. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  34. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  35. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  36. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  37. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  38. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  39. package/extensions/explore/tools/target-params.ts +9 -0
  40. package/extensions/footer/index.ts +75 -56
  41. package/extensions/handoff/index.ts +8 -30
  42. package/extensions/image-gen/index.ts +46 -40
  43. package/extensions/patch/executor.ts +190 -135
  44. package/extensions/patch/matcher.ts +82 -48
  45. package/extensions/patch/parser.ts +149 -105
  46. package/extensions/patch/render.ts +32 -39
  47. package/extensions/patch/summary.ts +30 -26
  48. package/extensions/ready/README.md +7 -0
  49. package/extensions/ready/index.ts +58 -0
  50. package/extensions/ready/model.ts +128 -0
  51. package/extensions/ready/packs.ts +272 -0
  52. package/extensions/ready/render.ts +445 -0
  53. package/extensions/ready/scan.ts +760 -0
  54. package/extensions/reference/panel.ts +28 -16
  55. package/extensions/review/index.ts +63 -51
  56. package/extensions/review/panel.ts +5 -34
  57. package/extensions/runtime-context/context.ts +30 -26
  58. package/extensions/script-runner/README.md +2 -2
  59. package/extensions/script-runner/index.ts +122 -82
  60. package/extensions/silent-command-runner/index.ts +38 -23
  61. package/extensions/subagent/agents/context-sync.md +185 -33
  62. package/extensions/subagent/agents/scout.md +2 -2
  63. package/extensions/subagent/agents.ts +137 -84
  64. package/extensions/subagent/cmux-dashboard.ts +39 -30
  65. package/extensions/subagent/index.ts +79 -59
  66. package/extensions/subagent/resume.ts +1 -1
  67. package/extensions/subagent/run.ts +304 -193
  68. package/extensions/subagent/runtime.ts +781 -343
  69. package/extensions/tau/index.ts +47 -38
  70. package/extensions/tau-help/help.md +14 -6
  71. package/extensions/tool-loader/README.md +4 -2
  72. package/extensions/tool-loader/index.ts +77 -52
  73. package/extensions/web/index.ts +6 -3
  74. package/extensions/web/webfetch.ts +114 -86
  75. package/package.json +3 -3
  76. package/schemas/tau.schema.json +20 -33
  77. package/shared/bounded-text-result.ts +1 -0
  78. package/shared/checkpoint-visibility.ts +9 -0
  79. package/shared/events.ts +28 -10
  80. package/shared/isolated-session.ts +71 -29
  81. package/shared/model-effort.ts +9 -19
  82. package/shared/model-fallback/index.ts +66 -41
  83. package/shared/settings/load.ts +45 -38
  84. package/shared/temporary-output-store.ts +16 -10
  85. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  86. package/{extensions/explore → src}/ast/engine.ts +54 -38
  87. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  88. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  89. package/src/ast/format/deps.ts +94 -0
  90. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  91. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  92. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  93. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  94. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  95. package/src/ast/format/targeted-result.ts +20 -0
  96. package/src/ast/grammars/c_sharp.wasm +0 -0
  97. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  98. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  99. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  100. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  101. package/{extensions/explore → src}/ast/ir.ts +6 -0
  102. package/src/ast/languages/csharp-file-deps.ts +90 -0
  103. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  104. package/src/ast/languages/dotted-index.ts +131 -0
  105. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  106. package/src/ast/languages/fixtures/sample.cs +53 -0
  107. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  108. package/src/ast/languages/fixtures/sample.go +37 -0
  109. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  110. package/src/ast/languages/fixtures/sample.java +40 -0
  111. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  112. package/src/ast/languages/fixtures/sample.kt +31 -0
  113. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  114. package/src/ast/languages/fixtures/sample.md +23 -0
  115. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  116. package/src/ast/languages/fixtures/sample.rs +54 -0
  117. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  118. package/src/ast/languages/fixtures/sample.swift +45 -0
  119. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  120. package/src/ast/languages/fixtures/sample.ts +45 -0
  121. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  122. package/src/ast/languages/fixtures/sample.tsx +36 -0
  123. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  124. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  125. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  126. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  127. package/src/ast/languages/jvm-file-deps.ts +135 -0
  128. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  129. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  130. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  131. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  132. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  133. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  134. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  135. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  136. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  137. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  138. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  139. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  140. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  141. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  142. package/src/ast/queries/impact.ts +291 -0
  143. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  144. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  145. package/{extensions/explore → src}/ast/query.ts +1 -1
  146. package/{extensions/explore → src}/ast/registry.ts +1 -2
  147. package/{extensions/explore → src}/ast/scan.ts +1 -1
  148. package/src/ast/session.ts +50 -0
  149. package/{extensions/explore → src}/ast/slice.ts +8 -0
  150. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  151. package/src/file-injection/index.ts +547 -0
  152. package/src/image-generation/client.ts +50 -31
  153. package/src/image-generation/index.ts +177 -134
  154. package/src/index.ts +2 -0
  155. package/src/tool-loading/index.ts +58 -0
  156. package/extensions/context/evidence.ts +0 -517
  157. package/extensions/context/projection.ts +0 -141
  158. package/extensions/context/state.ts +0 -30
  159. package/extensions/context/write-scope.ts +0 -109
  160. package/extensions/explore/ast/format/deps.ts +0 -86
  161. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  162. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  163. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  164. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  165. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  166. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  167. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  168. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  169. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  170. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  171. package/extensions/explore/ast/languages/odin.ts +0 -407
  172. package/extensions/explore/ast/queries/impact.ts +0 -250
  173. package/extensions/explore/outline-injection.ts +0 -151
  174. package/extensions/explore/read/autoread.ts +0 -118
  175. package/extensions/working-memory/README.md +0 -17
  176. package/extensions/working-memory/checkpoint.ts +0 -242
  177. package/extensions/working-memory/index.ts +0 -341
  178. package/extensions/working-memory/memory.ts +0 -235
  179. package/extensions/working-memory/render.ts +0 -185
  180. package/extensions/working-memory/settings.ts +0 -38
  181. package/extensions/working-memory/state.ts +0 -158
  182. package/shared/autoread.ts +0 -234
  183. package/shared/context-messages.ts +0 -19
  184. package/shared/outline-injection.ts +0 -56
  185. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  186. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  187. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  188. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  189. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  190. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -64,6 +64,52 @@ async function init(ctx: ExtensionCommandContext, scope: "global" | "project"):
64
64
  ctx.ui.notify(`tau init wrote ${path}`, "info");
65
65
  }
66
66
 
67
+ function pushExtensionFindings(
68
+ findings: Finding[],
69
+ level: Finding["level"],
70
+ path: string,
71
+ extensions: unknown,
72
+ specs: Awaited<ReturnType<typeof discoverTauSettingsSpecs>>,
73
+ ): void {
74
+ if (extensions !== undefined && (!extensions || typeof extensions !== "object" || Array.isArray(extensions))) {
75
+ findings.push({ level, message: `extensions must be an object: ${path}`, startup: true });
76
+ return;
77
+ }
78
+ const extensionRecord = (extensions ?? {}) as Record<string, unknown>;
79
+ for (const spec of specs) {
80
+ const section = extensionRecord[spec.key];
81
+ if (section === undefined) continue;
82
+ if (!Value.Check(spec.schema, section)) {
83
+ findings.push({ level, message: `invalid extensions.${spec.key}: ${path}`, startup: true });
84
+ }
85
+ }
86
+ }
87
+
88
+ async function inspectPath(
89
+ findings: Finding[],
90
+ item: { level: Finding["level"]; path: string; trusted: boolean },
91
+ specs: Awaited<ReturnType<typeof discoverTauSettingsSpecs>>,
92
+ ): Promise<void> {
93
+ const status = await readJsonStatus(item.path);
94
+ if (!status.exists) return;
95
+ if (!item.trusted) {
96
+ findings.push({
97
+ level: item.level,
98
+ message: `project Tau settings ignored because project is not trusted: ${item.path}. Run /trust, then /reload.`,
99
+ startup: true,
100
+ });
101
+ return;
102
+ }
103
+ if (!status.ok) {
104
+ findings.push({ level: item.level, message: `malformed JSON: ${item.path}: ${status.error}`, startup: true });
105
+ return;
106
+ }
107
+ if (typeof status.value.$schema !== "string") {
108
+ findings.push({ level: item.level, message: `missing $schema: ${item.path}`, startup: false });
109
+ }
110
+ pushExtensionFindings(findings, item.level, item.path, status.value.extensions, specs);
111
+ }
112
+
67
113
  async function inspect(ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">): Promise<Finding[]> {
68
114
  const findings: Finding[] = [];
69
115
  const specs = await discoverTauSettingsSpecs(ctx.cwd);
@@ -71,44 +117,7 @@ async function inspect(ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">):
71
117
  { level: "global" as const, path: globalTauSettingsPath(), trusted: true },
72
118
  { level: "project" as const, path: await projectTauSettingsPath(ctx.cwd), trusted: ctx.isProjectTrusted() },
73
119
  ];
74
-
75
- for (const item of paths) {
76
- const status = await readJsonStatus(item.path);
77
- if (!status.exists) continue;
78
- if (!item.trusted) {
79
- findings.push({
80
- level: item.level,
81
- message: `project Tau settings ignored because project is not trusted: ${item.path}. Run /trust, then /reload.`,
82
- startup: true,
83
- });
84
- continue;
85
- }
86
- if (!status.ok) {
87
- findings.push({ level: item.level, message: `malformed JSON: ${item.path}: ${status.error}`, startup: true });
88
- continue;
89
- }
90
- if (typeof status.value.$schema !== "string") {
91
- findings.push({ level: item.level, message: `missing $schema: ${item.path}`, startup: false });
92
- }
93
- const extensions = status.value.extensions;
94
- if (extensions !== undefined && (!extensions || typeof extensions !== "object" || Array.isArray(extensions))) {
95
- findings.push({ level: item.level, message: `extensions must be an object: ${item.path}`, startup: true });
96
- continue;
97
- }
98
- const extensionRecord = (extensions ?? {}) as Record<string, unknown>;
99
- for (const spec of specs) {
100
- const section = extensionRecord[spec.key];
101
- if (section === undefined) continue;
102
- if (!Value.Check(spec.schema, section)) {
103
- findings.push({
104
- level: item.level,
105
- message: `invalid extensions.${spec.key}: ${item.path}`,
106
- startup: true,
107
- });
108
- }
109
- }
110
- }
111
-
120
+ for (const item of paths) await inspectPath(findings, item, specs);
112
121
  return findings;
113
122
  }
114
123
 
@@ -6,6 +6,10 @@ Tau is a coding agent built to be a reliable partner during software work. It ca
6
6
 
7
7
  Gives the agent macOS window discovery, screenshots, and app activation tools for visual validation. Requires macOS 14 and Screen & System Audio Recording permission.
8
8
 
9
+ ## aside
10
+
11
+ Adds `/aside <question>` for a one-off question to the current model without putting the question or answer in the conversation. Choose the current conversation branch or no context. A thinking widget clears when the answer opens. Run `/aside` to reopen the latest answer and `/aside clear` to cancel or clear it.
12
+
9
13
  ## attention
10
14
 
11
15
  Shows attention state when Tau needs the user to look at the chat, finishes compacting a session, or summarizes an abandoned branch.
@@ -32,15 +36,15 @@ Adds `/commit` for semantic commit grouping, review, and committing selected rep
32
36
 
33
37
  ## context
34
38
 
35
- Adds `/context` to set branch-local reusable repository work scopes from `.pi/contexts`, and `/context-sync` or `/context-sync <nudge>` for human-driven catalog sync. Active entries produce one ephemeral per-call projection instead of transcript messages. Entry `read` paths supply exact contents, `outline` paths use Explore, and `references` stay unloaded. Clear all selections and confirm to remove active context. Escape cancels a running manual sync. When `sync.automation` is on, coding agent can also run `context-sync` after meaningful uncommitted work. Sync catalogs durable code and long-lived documentation; recurring scratch, planning, interview, and rough-idea paths belong in `validation.ignoreGlobs`. `sync.enabled` is master switch for command, automation, and validation auto-run. Context validation is off by default; when on (and sync enabled), Tau auto-runs context-sync on failure. Folder names are tabs, TOML files are concepts, and TOML sections are selectable entries.
39
+ Adds `/context` to inject reusable repository work scopes from `.pi/contexts`, and `/context-sync` or `/context-sync <nudge>` for human-driven catalog sync. Selecting entries injects them once into the conversation: `read` paths as complete files, `show` targets as current declaration slices, `outline` paths as Explore structures, and one hidden note listing `references` plus instructions to treat the injected material as current. Run `/context` again to inject more. Manual sync replaces the editor with a status panel; Escape or Ctrl+C cancels. When `sync.automation` is on, coding agent can also run `context-sync` after meaningful uncommitted work. Sync catalogs durable code and long-lived documentation; recurring scratch, planning, interview, and rough-idea paths belong in `validation.ignoreGlobs`. `sync.enabled` is master switch for command, automation, and validation auto-run. Context validation is off by default; when on (and sync enabled), Tau auto-runs context-sync on failure. Domain folders are `NN_slug` tabs (ordered by the two-digit prefix; UI shows the slug), TOML files are concepts, and TOML sections are selectable entries.
36
40
 
37
- ## working-memory
41
+ ## checkpoint
38
42
 
39
- Gives agent `working_memory` for selective hard checkpoints. Every checkpoint retains at least one useful user message or visible assistant text. Requested source files return as structural outlines, deferred files remain cheap conditional reminders, and a continuation note carries conclusions extracted from exploration. Tool history and full file reads leave future model input without changing saved session. Advisory reminders begin at 40k active-context tokens. Run `/prune` to request reassessment manually.
43
+ Adds the `checkpoint` tool for retaining exact user and assistant messages, current file reads and outlines, durable work state, an agent-written resume directive, and deferred file paths as a rolling continuation context. Checkpoint rows are hidden by default. Set `extensions.checkpoint.showToolRows` to `true` before `/reload` to watch checkpoint and newly injected-file rows while working; existing injected-file rows keep their saved display state. The agent receives hidden checkpoint nudges at 50% and 75% of `extensions.checkpoint.checkpointTokenLimit` (150,000 by default); non-checkpoint tools are blocked at the limit until checkpoint succeeds.
40
44
 
41
45
  ## effort
42
46
 
43
- Adds `/effort [low|medium|high]` to select effort and a provider from current logins. Tau selects provider’s best available model for tier, then tries its configured model fallback. `Ctrl+Shift+E` cycles tiers on current provider. Footer derives effort from current provider, model, and thinking level, and hides it when no configured tier matches.
47
+ Adds `/effort [quick|standard|deep]` to select effort and a provider from current logins. Tau selects provider’s best available model for tier, then tries its configured model fallback. `Ctrl+Shift+E` cycles tiers on current provider. Footer derives effort from current provider, model, and thinking level, and hides it when no configured tier matches.
44
48
 
45
49
  ## explore
46
50
 
@@ -74,6 +78,10 @@ Replaces separate edit/write operations with one multi-file `patch` tool. It can
74
78
 
75
79
  Adds `/qna` for when the agent has asked you several questions in chat and you want a friendly UI for answering them on your own terms. It is only active when you manually run the command.
76
80
 
81
+ ## ready
82
+
83
+ Adds `/ready` to scan agent-readiness rails (cold start, toolchain, verify, lint/entropy, policy, standards, context, and related signals). Choose Markdown or HTML; Tau writes a timestamped report under `.pi/tau/ready/` and notifies with the path. Scan-only in v1 — no model judgment and no scores.
84
+
77
85
  ## review
78
86
 
79
87
  Adds `/review` for explicit isolated review of current Git changes. Choose `simplify`, `architecture`, or `correctness`, or run a mode directly. Results stay outside agent context until you send them from result view, and can be exported under `.pi/tau/reviews/`. `/review show` reopens latest result on current session branch.
@@ -92,7 +100,7 @@ Supplies the agent with the current local date and an initial root directory sna
92
100
 
93
101
  ## script-runner
94
102
 
95
- Gives the agent a first-class `script_runner` tool to execute Python 3 and TypeScript instead of bash. On failure it returns a `scriptId`; the agent retries with targeted `{oldText,newText}` edits against the script it already wrote rather than resending the whole script. Languages are detected from the environment (Python 3 via `python3`; TypeScript via Node `--experimental-strip-types`, Node 22.6+). The tool registers only available languages and is hidden from the prompt if neither is present.
103
+ Gives the agent a first-class `script_runner` tool to execute Python 3, Node.js, and Deno scripts instead of bash. On failure it returns a `scriptId`; the agent retries with targeted `{oldText,newText}` edits against the script it already wrote rather than resending the whole script. Runtimes are detected from the environment (Python 3 via `python3`; `node` is the local Node.js runtime with `--experimental-strip-types`, Node 22.6+ — full Node APIs, TypeScript with erasable syntax or plain JavaScript; `deno` via `deno run -A` — full permissions, native TypeScript/JavaScript, Deno APIs). The tool registers only available runtimes and is hidden from the prompt if none are present.
96
104
 
97
105
  ## silent-command-runner
98
106
 
@@ -120,7 +128,7 @@ Adds `/tau`, `/tau init [--global|--project]`, and `/tau doctor` for Tau setup a
120
128
 
121
129
  ## tool-loader
122
130
 
123
- Progressively exposes specialist tools through `load_tools`. Tau normally loads the fixed `web`, `image`, and `appshot` groups itself when needed; supported providers can preserve more prompt-cache reuse.
131
+ Progressively exposes registered specialist tool groups through `load_tools`. Tau registers `web`, `image`, and `appshot`; project or global package extensions can add groups with `registerDeferredToolGroup()` from `@shanepadgett/tau-agent`. Supported providers can preserve more prompt-cache reuse.
124
132
 
125
133
  ## web
126
134
 
@@ -1,13 +1,15 @@
1
1
  # Tool Loader
2
2
 
3
- Tau progressively exposes specialist tools. Most coding turns do not need web, image, or macOS application schemas, so Pi can load those tools later without discarding supported provider cache prefixes.
3
+ Tau progressively exposes registered specialist tool groups. Most coding turns do not need web, image, macOS application, or application-specific schemas, so Pi can load those tools later without discarding supported provider cache prefixes.
4
4
 
5
- The agent normally calls `load_tools` itself. Users can also ask Tau to load one of these groups:
5
+ The agent normally calls `load_tools` itself. Tau's built-in groups are:
6
6
 
7
7
  - `web` for public web and implementation research
8
8
  - `image` for raster image generation and editing
9
9
  - `appshot` for macOS window discovery, capture, and activation
10
10
 
11
+ Project and global package extensions can add groups with `registerDeferredToolGroup()` from `@shanepadgett/tau-agent`. The group description is included in the loader catalog so the agent can select it when a task needs that capability.
12
+
11
13
  Supported models optimize prompt caching when a group loads. Other models keep the same functional behavior.
12
14
 
13
15
  After changing this extension during development, run `/reload` before testing.
@@ -1,21 +1,12 @@
1
- import { StringEnum } from "@earendil-works/pi-ai";
2
1
  import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
2
  import { type Static, Type } from "typebox";
4
-
5
- const CAPABILITIES = ["web", "image", "appshot"] as const;
6
- type Capability = (typeof CAPABILITIES)[number];
7
-
8
- const CAPABILITY_TOOLS: Record<Capability, readonly string[]> = {
9
- web: ["webfetch", "websearch", "codesearch"],
10
- image: ["image_gen"],
11
- appshot: ["list_windows", "screenshot_window", "activate_app"],
12
- };
13
- const SPECIALIST_TOOLS = CAPABILITIES.flatMap((capability) => CAPABILITY_TOOLS[capability]);
3
+ import { registeredDeferredToolGroups, type DeferredToolGroupInfo } from "../../src/tool-loading/index.ts";
14
4
 
15
5
  const loadToolsSchema = Type.Object(
16
6
  {
17
- capability: StringEnum(CAPABILITIES, {
18
- description: "Specialist group to load: web, image, or appshot",
7
+ capability: Type.String({
8
+ minLength: 1,
9
+ description: "Registered specialist group ID, such as web, image, appshot, or a package-provided group",
19
10
  }),
20
11
  },
21
12
  { additionalProperties: false },
@@ -25,50 +16,85 @@ type LoadToolsParams = Static<typeof loadToolsSchema>;
25
16
 
26
17
  interface LoadToolsDetails {
27
18
  version: 1;
28
- capability: Capability;
19
+ capability: string;
29
20
  requestedToolNames: string[];
30
21
  addedToolNames: string[];
31
22
  }
32
23
 
33
24
  export default function toolLoaderExtension(pi: ExtensionAPI): void {
34
25
  let managed = false;
35
- let allowedSpecialistNames = new Set<string>();
26
+ let allowedToolNames = new Map<string, ReadonlySet<string>>();
27
+ let managedToolNames = new Set<string>();
28
+
29
+ pi.registerTool(createLoadToolsTool(pi, []));
30
+
31
+ pi.on("session_start", (_event, ctx) => {
32
+ const groups = registeredDeferredToolGroups(pi);
33
+ pi.registerTool(createLoadToolsTool(pi, groups));
34
+
35
+ const initial = pi.getActiveTools();
36
+ const initialSet = new Set(initial);
37
+ allowedToolNames = new Map(
38
+ groups.map((group) => [group.id, new Set(group.toolNames.filter((name) => initialSet.has(name)))]),
39
+ );
40
+ managedToolNames = new Set(groups.flatMap((group) => group.toolNames));
41
+ managed = initialSet.has("load_tools") && groups.length > 0;
42
+ if (managed) restoreActiveTools(pi, initial, loadedCapabilities(ctx.sessionManager.getBranch()), groups);
43
+ });
36
44
 
37
- pi.registerTool(
38
- defineTool<typeof loadToolsSchema, LoadToolsDetails>({
45
+ pi.on("session_tree", (_event, ctx) => {
46
+ if (managed) {
47
+ restoreActiveTools(
48
+ pi,
49
+ pi.getActiveTools(),
50
+ loadedCapabilities(ctx.sessionManager.getBranch()),
51
+ registeredDeferredToolGroups(pi),
52
+ );
53
+ }
54
+ });
55
+
56
+ function createLoadToolsTool(pi: ExtensionAPI, groups: readonly DeferredToolGroupInfo[]) {
57
+ return defineTool<typeof loadToolsSchema, LoadToolsDetails>({
39
58
  name: "load_tools",
40
59
  label: "Load Tools",
41
- description:
42
- "Load one Tau specialist tool group for the current session. Groups: web for public web and implementation research; image for raster generation and editing; appshot for macOS window discovery, capture, and activation.",
43
- promptSnippet: "Load a specialist Tau tool group for web research, image generation, or macOS app inspection",
60
+ description: `Load one registered Tau specialist tool group for the current session.${formatGroupCatalog(groups)}`,
61
+ promptSnippet: "Load a registered specialist tool group when the current tools cannot perform the task",
44
62
  promptGuidelines: [
45
- "Use load_tools before attempting a specialist capability whose tools are not currently available.",
63
+ "Use load_tools before attempting a registered specialist capability whose tools are not currently available.",
46
64
  ],
47
65
  parameters: loadToolsSchema,
48
66
  async execute(_toolCallId, params: LoadToolsParams) {
67
+ const group = registeredDeferredToolGroups(pi).find((candidate) => candidate.id === params.capability);
68
+ if (group === undefined) {
69
+ throw new Error(
70
+ `Unknown specialist tool group: ${params.capability}.${formatGroupCatalog(registeredDeferredToolGroups(pi))}`,
71
+ );
72
+ }
73
+
49
74
  const before = pi.getActiveTools();
50
- const requested = [...CAPABILITY_TOOLS[params.capability]];
75
+ const requested = [...group.toolNames];
51
76
  const registered = new Set(pi.getAllTools().map((tool) => tool.name));
52
- const loadable = requested.filter((name) => registered.has(name) && allowedSpecialistNames.has(name));
77
+ const allowed = allowedToolNames.get(group.id) ?? new Set<string>();
78
+ const loadable = requested.filter((name) => registered.has(name) && allowed.has(name));
53
79
  if (loadable.length === 0) {
54
80
  throw new Error(`No ${params.capability} tools are available in this session's tool configuration.`);
55
81
  }
82
+
56
83
  const beforeSet = new Set(before);
57
- const next = [...before, ...loadable.filter((name) => !beforeSet.has(name))];
58
- pi.setActiveTools(next);
84
+ pi.setActiveTools([...before, ...loadable.filter((name) => !beforeSet.has(name))]);
59
85
  const after = pi.getActiveTools();
60
- const addedToolNames = after.filter((name) => !beforeSet.has(name));
86
+ const addedToolNames = requested.filter((name) => !beforeSet.has(name) && after.includes(name));
61
87
  const available = requested.filter((name) => after.includes(name));
62
88
  const unavailable = requested.filter((name) => !after.includes(name));
63
- const label = `${params.capability[0]?.toUpperCase()}${params.capability.slice(1)}`;
64
89
  const text =
65
90
  addedToolNames.length > 0
66
91
  ? `Loaded ${params.capability} tools: ${addedToolNames.join(", ")}.`
67
- : `${label} tools are already loaded: ${available.join(", ")}.`;
92
+ : `${params.capability} tools are already loaded: ${available.join(", ")}.`;
93
+
68
94
  return {
69
95
  content: [
70
96
  {
71
- type: "text",
97
+ type: "text" as const,
72
98
  text: unavailable.length ? `${text} Unavailable: ${unavailable.join(", ")}.` : text,
73
99
  },
74
100
  ],
@@ -80,33 +106,33 @@ export default function toolLoaderExtension(pi: ExtensionAPI): void {
80
106
  },
81
107
  };
82
108
  },
83
- }),
84
- );
85
-
86
- pi.on("session_start", (_event, ctx) => {
87
- const initial = pi.getActiveTools();
88
- const initialSet = new Set(initial);
89
- allowedSpecialistNames = new Set(SPECIALIST_TOOLS.filter((name) => initialSet.has(name)));
90
- managed = initialSet.has("load_tools") && SPECIALIST_TOOLS.every((name) => initialSet.has(name));
91
- if (managed) restoreActiveTools(pi, initial, loadedCapabilities(ctx.sessionManager.getBranch()));
92
- });
109
+ });
110
+ }
93
111
 
94
- pi.on("session_tree", (_event, ctx) => {
95
- if (managed) restoreActiveTools(pi, pi.getActiveTools(), loadedCapabilities(ctx.sessionManager.getBranch()));
96
- });
112
+ function restoreActiveTools(
113
+ pi: ExtensionAPI,
114
+ current: readonly string[],
115
+ loaded: ReadonlySet<string>,
116
+ groups: readonly DeferredToolGroupInfo[],
117
+ ): void {
118
+ const next = current.filter((name) => !managedToolNames.has(name));
119
+ for (const group of groups) {
120
+ if (!loaded.has(group.id)) continue;
121
+ const allowed = allowedToolNames.get(group.id) ?? new Set<string>();
122
+ next.push(...group.toolNames.filter((name) => allowed.has(name)));
123
+ }
124
+ pi.setActiveTools([...new Set(next)]);
125
+ }
97
126
  }
98
127
 
99
- function restoreActiveTools(pi: ExtensionAPI, current: readonly string[], loaded: ReadonlySet<Capability>): void {
100
- const specialist = new Set(SPECIALIST_TOOLS);
101
- const next = current.filter((name) => !specialist.has(name));
102
- for (const capability of CAPABILITIES) {
103
- if (loaded.has(capability)) next.push(...CAPABILITY_TOOLS[capability]);
104
- }
105
- pi.setActiveTools([...new Set(next)]);
128
+ function formatGroupCatalog(groups: readonly DeferredToolGroupInfo[]): string {
129
+ if (groups.length === 0) return " No specialist groups are registered.";
130
+ const catalog = groups.map((group) => `${group.id}: ${group.description}`).join("; ");
131
+ return ` Registered groups: ${catalog}.`;
106
132
  }
107
133
 
108
- function loadedCapabilities(entries: readonly unknown[]): Set<Capability> {
109
- const loaded = new Set<Capability>();
134
+ function loadedCapabilities(entries: readonly unknown[]): Set<string> {
135
+ const loaded = new Set<string>();
110
136
  for (const value of entries) {
111
137
  if (!value || typeof value !== "object") continue;
112
138
  const entry = value as Record<string, unknown>;
@@ -125,7 +151,6 @@ function isLoadToolsDetails(value: unknown): value is LoadToolsDetails {
125
151
  return (
126
152
  details.version === 1 &&
127
153
  typeof details.capability === "string" &&
128
- CAPABILITIES.includes(details.capability as Capability) &&
129
154
  Array.isArray(details.requestedToolNames) &&
130
155
  details.requestedToolNames.every((name) => typeof name === "string") &&
131
156
  Array.isArray(details.addedToolNames) &&
@@ -1,13 +1,16 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { createToolRowStateStore } from "../../shared/tool-row-state.js";
3
+ import { registerDeferredToolGroup } from "../../src/tool-loading/index.ts";
3
4
  import { createCodeSearchTool } from "./codesearch.ts";
4
5
  import { createWebFetchTool } from "./webfetch.ts";
5
6
  import { createWebSearchTool } from "./websearch.ts";
6
7
 
7
8
  export default function webExtension(pi: ExtensionAPI): void {
8
9
  const rowState = createToolRowStateStore(pi, "web.tool-row-state");
9
- pi.registerTool(createWebFetchTool(rowState));
10
- pi.registerTool(createWebSearchTool(rowState));
11
- pi.registerTool(createCodeSearchTool(rowState));
10
+ registerDeferredToolGroup(pi, {
11
+ id: "web",
12
+ description: "Public web and implementation research",
13
+ tools: [createWebFetchTool(rowState), createWebSearchTool(rowState), createCodeSearchTool(rowState)],
14
+ });
12
15
  pi.on("session_start", () => rowState.clear());
13
16
  }
@@ -27,14 +27,22 @@ const webFetchParams = Type.Object(
27
27
  );
28
28
 
29
29
  type WebFetchParams = Static<typeof webFetchParams>;
30
+ type WebFetchFormat = "markdown" | "text" | "html";
30
31
  interface WebFetchDetails {
31
32
  url: string;
32
- format: "markdown" | "text" | "html";
33
+ format: WebFetchFormat;
33
34
  mime: string;
34
35
  bytes: number;
35
36
  truncation?: TruncationResult;
36
37
  }
37
38
 
39
+ type WebFetchContent = { type: "text"; text: string } | { type: "image"; data: string; mimeType: string };
40
+
41
+ interface WebFetchResult {
42
+ content: WebFetchContent[];
43
+ details: WebFetchDetails;
44
+ }
45
+
38
46
  async function readResponseBody(response: FetchResponse): Promise<Uint8Array> {
39
47
  if (!response.body) return new Uint8Array();
40
48
  const reader = response.body.getReader();
@@ -61,6 +69,110 @@ function renderCallSummary(args: WebFetchParams): string {
61
69
  return truncateCallSummary((args.url ?? "").trim());
62
70
  }
63
71
 
72
+ function parseFetchUrl(raw: string): URL {
73
+ let url: URL;
74
+ try {
75
+ url = new URL(raw);
76
+ } catch {
77
+ throw new Error(`Invalid URL: ${raw}`);
78
+ }
79
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
80
+ throw new Error("URL must use http:// or https://");
81
+ }
82
+ return url;
83
+ }
84
+
85
+ function acceptHeaderForFormat(format: WebFetchFormat): string {
86
+ if (format === "markdown") return "text/markdown;q=1.0, text/plain;q=0.8, text/html;q=0.7, */*;q=0.1";
87
+ if (format === "text") return "text/plain;q=1.0, text/markdown;q=0.9, text/html;q=0.8, */*;q=0.1";
88
+ return "text/html;q=1.0, application/xhtml+xml;q=0.9, */*;q=0.1";
89
+ }
90
+
91
+ async function fetchWithChallengeRetry(
92
+ url: string,
93
+ headers: Record<string, string>,
94
+ signal: AbortSignal,
95
+ ): Promise<FetchResponse> {
96
+ const first = (await fetch(url, { method: "GET", headers, signal })) as FetchResponse;
97
+ if (first.status !== 403 || first.headers.get("cf-mitigated")?.toLowerCase() !== "challenge") return first;
98
+ await first.body?.cancel().catch(() => undefined);
99
+ return (await fetch(url, {
100
+ method: "GET",
101
+ headers: { ...headers, "User-Agent": "pi" },
102
+ signal,
103
+ })) as FetchResponse;
104
+ }
105
+
106
+ async function assertDeclaredBodySizeOk(response: FetchResponse): Promise<void> {
107
+ const declaredLength = response.headers.get("content-length");
108
+ if (declaredLength === null) return;
109
+ const bytes = Number.parseInt(declaredLength, 10);
110
+ if (!Number.isFinite(bytes) || bytes <= MAX_RESPONSE_BYTES) return;
111
+ await response.body?.cancel().catch(() => undefined);
112
+ throw new Error("Response too large (limit is 5MB)");
113
+ }
114
+
115
+ function formatFetchedText(raw: string, format: WebFetchFormat, mime: string): string {
116
+ if (format === "html") return raw;
117
+ const isHtml = mime === "text/html" || mime === "application/xhtml+xml";
118
+ if (!isHtml) return raw;
119
+ return format === "text" ? htmlToText(raw) : htmlToMarkdown(raw);
120
+ }
121
+
122
+ function buildWebFetchResult(url: string, format: WebFetchFormat, mime: string, body: Uint8Array): WebFetchResult {
123
+ const details = { url, format, mime, bytes: body.byteLength } satisfies WebFetchDetails;
124
+ if (mime.startsWith("image/") && mime !== "image/svg+xml") {
125
+ return {
126
+ content: [
127
+ { type: "text", text: `Fetched image from ${url} (${mime})` },
128
+ { type: "image", data: Buffer.from(body).toString("base64"), mimeType: mime },
129
+ ],
130
+ details,
131
+ };
132
+ }
133
+ const truncated = truncateToolOutput(formatFetchedText(new TextDecoder().decode(body), format, mime));
134
+ return {
135
+ content: [{ type: "text", text: truncated.text }],
136
+ details: {
137
+ ...details,
138
+ ...(truncated.truncation ? { truncation: truncated.truncation } : {}),
139
+ },
140
+ };
141
+ }
142
+
143
+ async function executeWebFetch(
144
+ params: WebFetchParams,
145
+ signal: AbortSignal | undefined,
146
+ onUpdate: ((update: { content: WebFetchContent[]; details: undefined }) => void | Promise<void>) | undefined,
147
+ ): Promise<WebFetchResult> {
148
+ const url = parseFetchUrl(params.url);
149
+ const format = params.format ?? "markdown";
150
+ const timeout = normalizeTimeout(params.timeout, 30);
151
+ await onUpdate?.({ content: [{ type: "text", text: "Fetching page..." }], details: undefined });
152
+ const timeoutSignal = AbortSignal.timeout(timeout * 1000);
153
+ const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
154
+
155
+ try {
156
+ const headers = {
157
+ "User-Agent":
158
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
159
+ Accept: acceptHeaderForFormat(format),
160
+ "Accept-Language": "en-US,en;q=0.9",
161
+ };
162
+ const response = await fetchWithChallengeRetry(url.toString(), headers, requestSignal);
163
+ if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
164
+ await assertDeclaredBodySizeOk(response);
165
+ const body = await readResponseBody(response);
166
+ const mime = (response.headers.get("content-type")?.split(";", 1)[0] ?? "").trim().toLowerCase();
167
+ return buildWebFetchResult(url.toString(), format, mime, body);
168
+ } catch (error) {
169
+ if (timeoutSignal.aborted && signal?.aborted !== true) {
170
+ throw new Error(`Web fetch timed out after ${timeout}s`);
171
+ }
172
+ throw error;
173
+ }
174
+ }
175
+
64
176
  export function createWebFetchTool(rowState: ToolRowStateStore) {
65
177
  return defineTool<typeof webFetchParams, WebFetchDetails | undefined>({
66
178
  name: "webfetch",
@@ -69,91 +181,7 @@ export function createWebFetchTool(rowState: ToolRowStateStore) {
69
181
  "Fetch a known HTTP(S) URL as Markdown, text, or HTML. Use webfetch when you already have a URL; use websearch for broad discovery and codesearch for implementation-oriented lookups. Use a separate research workflow when several searches, fetches, and synthesis are needed. Supports inline images, limits response bodies to 5 MB, and truncates text to 2,000 lines or 50 KB.",
70
182
  parameters: webFetchParams,
71
183
  async execute(_toolCallId, params, signal, onUpdate) {
72
- let url: URL;
73
- try {
74
- url = new URL(params.url);
75
- } catch {
76
- throw new Error(`Invalid URL: ${params.url}`);
77
- }
78
- if (url.protocol !== "http:" && url.protocol !== "https:") {
79
- throw new Error("URL must use http:// or https://");
80
- }
81
-
82
- const format = params.format ?? "markdown";
83
- const timeout = normalizeTimeout(params.timeout, 30);
84
- await onUpdate?.({ content: [{ type: "text", text: "Fetching page..." }], details: undefined });
85
- const timeoutSignal = AbortSignal.timeout(timeout * 1000);
86
- const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
87
-
88
- try {
89
- const accept =
90
- format === "markdown"
91
- ? "text/markdown;q=1.0, text/plain;q=0.8, text/html;q=0.7, */*;q=0.1"
92
- : format === "text"
93
- ? "text/plain;q=1.0, text/markdown;q=0.9, text/html;q=0.8, */*;q=0.1"
94
- : "text/html;q=1.0, application/xhtml+xml;q=0.9, */*;q=0.1";
95
- const headers = {
96
- "User-Agent":
97
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
98
- Accept: accept,
99
- "Accept-Language": "en-US,en;q=0.9",
100
- };
101
- const first = (await fetch(url.toString(), {
102
- method: "GET",
103
- headers,
104
- signal: requestSignal,
105
- })) as FetchResponse;
106
- let response = first;
107
- if (first.status === 403 && first.headers.get("cf-mitigated")?.toLowerCase() === "challenge") {
108
- await first.body?.cancel().catch(() => undefined);
109
- response = (await fetch(url.toString(), {
110
- method: "GET",
111
- headers: { ...headers, "User-Agent": "pi" },
112
- signal: requestSignal,
113
- })) as FetchResponse;
114
- }
115
- if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
116
-
117
- const declaredLength = response.headers.get("content-length");
118
- if (declaredLength !== null) {
119
- const bytes = Number.parseInt(declaredLength, 10);
120
- if (Number.isFinite(bytes) && bytes > MAX_RESPONSE_BYTES) {
121
- await response.body?.cancel().catch(() => undefined);
122
- throw new Error("Response too large (limit is 5MB)");
123
- }
124
- }
125
-
126
- const body = await readResponseBody(response);
127
- const mime = (response.headers.get("content-type")?.split(";", 1)[0] ?? "").trim().toLowerCase();
128
- const details = { url: url.toString(), format, mime, bytes: body.byteLength } satisfies WebFetchDetails;
129
- if (mime.startsWith("image/") && mime !== "image/svg+xml") {
130
- return {
131
- content: [
132
- { type: "text", text: `Fetched image from ${url.toString()} (${mime})` },
133
- { type: "image", data: Buffer.from(body).toString("base64"), mimeType: mime },
134
- ],
135
- details,
136
- };
137
- }
138
-
139
- const raw = new TextDecoder().decode(body);
140
- const isHtml = mime === "text/html" || mime === "application/xhtml+xml";
141
- const output =
142
- format === "html" ? raw : isHtml ? (format === "text" ? htmlToText(raw) : htmlToMarkdown(raw)) : raw;
143
- const truncated = truncateToolOutput(output);
144
- return {
145
- content: [{ type: "text", text: truncated.text }],
146
- details: {
147
- ...details,
148
- ...(truncated.truncation ? { truncation: truncated.truncation } : {}),
149
- },
150
- };
151
- } catch (error) {
152
- if (timeoutSignal.aborted && signal?.aborted !== true) {
153
- throw new Error(`Web fetch timed out after ${timeout}s`);
154
- }
155
- throw error;
156
- }
184
+ return executeWebFetch(params, signal, onUpdate);
157
185
  },
158
186
  renderCall(args, theme, context) {
159
187
  rowState.watch(context.toolCallId, context.invalidate);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.33.1",
3
+ "version": "0.34.0",
4
4
  "description": "Tau is a custom agentic harness built with pi extensions",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -35,10 +35,10 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@ast-grep/wasm": "0.45.0",
38
- "@shanepadgett/tau-tui": "0.33.1",
38
+ "@shanepadgett/tau-tui": "0.34.0",
39
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.1",
42
42
  "web-tree-sitter": "0.26.11"
43
43
  },
44
44
  "peerDependencies": {