@shanepadgett/tau-agent 0.33.0 → 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 +130 -93
  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
@@ -11,6 +11,7 @@ export type { ImageProvider } from "./constants.ts";
11
11
 
12
12
  const MAX_INPUT_BYTES = 50 * 1024 * 1024;
13
13
  const MAX_ERROR_MESSAGE_LENGTH = 2000;
14
+ const OUTPUT_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".webp"]);
14
15
 
15
16
  export type ImageGenerationContext = Pick<ExtensionContext, "cwd" | "model" | "modelRegistry">;
16
17
 
@@ -33,12 +34,21 @@ export interface GeneratedImageResult {
33
34
  height: number;
34
35
  }
35
36
 
37
+ type ReferencedImage = {
38
+ mimeType: "image/png" | "image/jpeg" | "image/webp";
39
+ data: string;
40
+ };
41
+
36
42
  function outputExtension(mimeType: GeneratedImageResult["mimeType"]): string {
37
43
  if (mimeType === "image/png") return ".png";
38
44
  if (mimeType === "image/webp") return ".webp";
39
45
  return ".jpg";
40
46
  }
41
47
 
48
+ function providerLabel(provider: ImageProvider): string {
49
+ return provider === "openai" ? "OpenAI Codex" : "xAI";
50
+ }
51
+
42
52
  function sanitizedError(error: unknown, signal: AbortSignal | undefined, secrets: readonly string[]): unknown {
43
53
  if (signal?.aborted) return signal.reason;
44
54
  let message = error instanceof Error ? error.message : "Image generation failed";
@@ -56,162 +66,195 @@ function sanitizedError(error: unknown, signal: AbortSignal | undefined, secrets
56
66
  return new Error(message || "Image generation failed");
57
67
  }
58
68
 
59
- export async function generateImage(
69
+ function parsePrompt(request: GenerateImageRequest): string {
70
+ if (typeof request.prompt !== "string") throw new Error("Image prompt must be a string");
71
+ if (request.provider !== undefined && request.provider !== "openai" && request.provider !== "xai") {
72
+ throw new Error("Image provider must be openai or xai");
73
+ }
74
+ if (request.path !== undefined && typeof request.path !== "string") throw new Error("Image path must be a string");
75
+ if (request.referencedImagePaths !== undefined && !Array.isArray(request.referencedImagePaths)) {
76
+ throw new Error("Referenced image paths must be an array");
77
+ }
78
+ const prompt = request.prompt.trim();
79
+ if (!prompt) throw new Error("Image prompt cannot be empty");
80
+ if (request.referencedImagePaths && request.referencedImagePaths.length > 3) {
81
+ throw new Error("Image generation accepts at most three referenced images");
82
+ }
83
+ return prompt;
84
+ }
85
+
86
+ function resolveRequestedAbsolutePath(cwd: string, path: string | undefined): string | undefined {
87
+ const requestedPath = path?.startsWith("@") ? path.slice(1) : path;
88
+ if (requestedPath !== undefined && !requestedPath.trim()) throw new Error("Image path cannot be empty");
89
+ if (!requestedPath) return undefined;
90
+ const absolutePath = isAbsolute(requestedPath) ? requestedPath : resolve(cwd, requestedPath);
91
+ if (!OUTPUT_EXTENSIONS.has(extname(absolutePath).toLowerCase())) {
92
+ throw new Error("Image path must end in .jpg, .jpeg, .png, or .webp");
93
+ }
94
+ return absolutePath;
95
+ }
96
+
97
+ function providerCandidates(
98
+ requestProvider: ImageProvider | undefined,
99
+ parentUsesXai: boolean,
100
+ ): readonly ImageProvider[] {
101
+ const preferred: ImageProvider = requestProvider ?? (parentUsesXai ? "xai" : "openai");
102
+ if (requestProvider) return [requestProvider];
103
+ return preferred === "xai" ? ["xai", "openai"] : ["openai", "xai"];
104
+ }
105
+
106
+ async function resolveProviderAuth(
60
107
  ctx: ImageGenerationContext,
61
- request: GenerateImageRequest,
62
- ): Promise<GeneratedImageResult> {
63
- const signal = request.signal;
64
- const secrets: string[] = [];
65
- try {
108
+ requestProvider: ImageProvider | undefined,
109
+ signal: AbortSignal | undefined,
110
+ secrets: string[],
111
+ ): Promise<{ provider: ImageProvider; token: string }> {
112
+ const parentUsesXai =
113
+ ctx.model?.provider.toLowerCase() === XAI_PROVIDER || ctx.model?.id.toLowerCase().includes("grok") === true;
114
+ for (const candidate of providerCandidates(requestProvider, parentUsesXai)) {
66
115
  signal?.throwIfAborted();
67
- if (typeof request.prompt !== "string") throw new Error("Image prompt must be a string");
68
- if (request.provider !== undefined && request.provider !== "openai" && request.provider !== "xai") {
69
- throw new Error("Image provider must be openai or xai");
70
- }
71
- if (request.path !== undefined && typeof request.path !== "string")
72
- throw new Error("Image path must be a string");
73
- if (request.referencedImagePaths !== undefined && !Array.isArray(request.referencedImagePaths)) {
74
- throw new Error("Referenced image paths must be an array");
75
- }
76
- const prompt = request.prompt.trim();
77
- if (!prompt) throw new Error("Image prompt cannot be empty");
78
- if (request.referencedImagePaths && request.referencedImagePaths.length > 3) {
79
- throw new Error("Image generation accepts at most three referenced images");
80
- }
116
+ const candidateToken = await ctx.modelRegistry.getApiKeyForProvider(
117
+ candidate === "openai" ? OPENAI_PROVIDER : XAI_PROVIDER,
118
+ );
119
+ signal?.throwIfAborted();
120
+ if (!candidateToken) continue;
121
+ secrets.push(candidateToken);
122
+ return { provider: candidate, token: candidateToken };
123
+ }
124
+ if (requestProvider === "openai") {
125
+ throw new Error("OpenAI Codex authentication is unavailable. Run /login openai-codex.");
126
+ }
127
+ if (requestProvider === "xai") {
128
+ throw new Error("xAI authentication is unavailable. Run /login xai and choose a login method.");
129
+ }
130
+ throw new Error("Image generation authentication is unavailable. Run /login for OpenAI Codex or xAI.");
131
+ }
81
132
 
82
- const requestedPath = request.path?.startsWith("@") ? request.path.slice(1) : request.path;
83
- if (requestedPath !== undefined && !requestedPath.trim()) throw new Error("Image path cannot be empty");
84
- const requestedAbsolutePath = requestedPath
85
- ? isAbsolute(requestedPath)
86
- ? requestedPath
87
- : resolve(ctx.cwd, requestedPath)
88
- : undefined;
89
- if (
90
- requestedAbsolutePath &&
91
- ![".jpg", ".jpeg", ".png", ".webp"].includes(extname(requestedAbsolutePath).toLowerCase())
92
- ) {
93
- throw new Error("Image path must end in .jpg, .jpeg, .png, or .webp");
94
- }
133
+ async function loadOneReferencedImage(
134
+ cwd: string,
135
+ path: string,
136
+ signal: AbortSignal | undefined,
137
+ ): Promise<ReferencedImage> {
138
+ signal?.throwIfAborted();
139
+ if (typeof path !== "string") throw new Error("Referenced image path must be a string");
140
+ const rawPath = path.startsWith("@") ? path.slice(1) : path;
141
+ if (!rawPath.trim()) throw new Error("Referenced image path cannot be empty");
142
+ const absolutePath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
143
+ const metadata = await stat(absolutePath);
144
+ signal?.throwIfAborted();
145
+ if (!metadata.isFile()) throw new Error(`Referenced image is not a file: ${absolutePath}`);
146
+ if (metadata.size > MAX_INPUT_BYTES) throw new Error(`Referenced image exceeds 50 MiB: ${absolutePath}`);
147
+ const bytes = await readFile(absolutePath, { signal });
148
+ if (bytes.length > MAX_INPUT_BYTES) throw new Error(`Referenced image exceeds 50 MiB: ${absolutePath}`);
149
+ const mimeType = detectImageMimeType(bytes);
150
+ if (!mimeType) {
151
+ throw new Error(`Referenced image is not a supported PNG, JPEG, or WebP file: ${absolutePath}`);
152
+ }
153
+ return { mimeType, data: bytes.toString("base64") };
154
+ }
95
155
 
96
- const parentUsesXai =
97
- ctx.model?.provider.toLowerCase() === XAI_PROVIDER || ctx.model?.id.toLowerCase().includes("grok") === true;
98
- const preferredProvider: ImageProvider = request.provider ?? (parentUsesXai ? "xai" : "openai");
99
- const providers: readonly ImageProvider[] = request.provider
100
- ? [request.provider]
101
- : preferredProvider === "xai"
102
- ? ["xai", "openai"]
103
- : ["openai", "xai"];
104
- let provider: ImageProvider | undefined;
105
- let token: string | undefined;
106
- for (const candidate of providers) {
107
- signal?.throwIfAborted();
108
- const candidateToken = await ctx.modelRegistry.getApiKeyForProvider(
109
- candidate === "openai" ? OPENAI_PROVIDER : XAI_PROVIDER,
156
+ async function loadReferencedImages(
157
+ cwd: string,
158
+ paths: readonly string[] | undefined,
159
+ signal: AbortSignal | undefined,
160
+ ): Promise<ReferencedImage[]> {
161
+ const images: ReferencedImage[] = [];
162
+ for (const path of paths ?? []) images.push(await loadOneReferencedImage(cwd, path, signal));
163
+ return images;
164
+ }
165
+
166
+ function readImageDimensions(bytes: Buffer, provider: ImageProvider): { width: number; height: number } {
167
+ let dimensions: ReturnType<typeof imageSize>;
168
+ try {
169
+ dimensions = imageSize(bytes);
170
+ } catch {
171
+ throw new Error(`${providerLabel(provider)} returned invalid image dimensions`);
172
+ }
173
+ if (
174
+ !Number.isInteger(dimensions.width) ||
175
+ dimensions.width <= 0 ||
176
+ !Number.isInteger(dimensions.height) ||
177
+ dimensions.height <= 0
178
+ ) {
179
+ throw new Error(`${providerLabel(provider)} returned invalid image dimensions`);
180
+ }
181
+ return { width: dimensions.width, height: dimensions.height };
182
+ }
183
+
184
+ function assertDestinationMatchesMime(
185
+ requestedAbsolutePath: string | undefined,
186
+ mimeType: GeneratedImageResult["mimeType"],
187
+ provider: ImageProvider,
188
+ ): string {
189
+ const generatedExtension = outputExtension(mimeType);
190
+ if (requestedAbsolutePath) {
191
+ const requestedExtension = extname(requestedAbsolutePath).toLowerCase();
192
+ const matches =
193
+ requestedExtension === generatedExtension || (generatedExtension === ".jpg" && requestedExtension === ".jpeg");
194
+ if (!matches) {
195
+ throw new Error(
196
+ `${providerLabel(provider)} returned ${mimeType}; destination must end in ${generatedExtension}`,
110
197
  );
111
- signal?.throwIfAborted();
112
- if (candidateToken) {
113
- provider = candidate;
114
- token = candidateToken;
115
- secrets.push(candidateToken);
116
- break;
117
- }
118
- }
119
- if (!provider || !token) {
120
- if (request.provider === "openai") {
121
- throw new Error("OpenAI Codex authentication is unavailable. Run /login openai-codex.");
122
- }
123
- if (request.provider === "xai") {
124
- throw new Error("xAI authentication is unavailable. Run /login xai and choose a login method.");
125
- }
126
- throw new Error("Image generation authentication is unavailable. Run /login for OpenAI Codex or xAI.");
127
198
  }
199
+ }
200
+ return generatedExtension;
201
+ }
128
202
 
129
- const images: Array<{
130
- mimeType: "image/png" | "image/jpeg" | "image/webp";
131
- data: string;
132
- }> = [];
133
- for (const path of request.referencedImagePaths ?? []) {
134
- signal?.throwIfAborted();
135
- if (typeof path !== "string") throw new Error("Referenced image path must be a string");
136
- const rawPath = path.startsWith("@") ? path.slice(1) : path;
137
- if (!rawPath.trim()) throw new Error("Referenced image path cannot be empty");
138
- const absolutePath = isAbsolute(rawPath) ? rawPath : resolve(ctx.cwd, rawPath);
139
- const metadata = await stat(absolutePath);
203
+ async function writeImageAtomically(
204
+ absolutePath: string,
205
+ bytes: Buffer,
206
+ generatedExtension: string,
207
+ signal: AbortSignal | undefined,
208
+ ): Promise<void> {
209
+ const outputDirectory = dirname(absolutePath);
210
+ await withFileMutationQueue(absolutePath, async () => {
211
+ await mkdir(outputDirectory, { recursive: true });
212
+ const temporaryPath = join(
213
+ outputDirectory,
214
+ `.${basename(absolutePath)}.${randomUUID()}.tmp${generatedExtension}`,
215
+ );
216
+ try {
217
+ await writeFile(temporaryPath, bytes, { flag: "wx", signal });
140
218
  signal?.throwIfAborted();
141
- if (!metadata.isFile()) throw new Error(`Referenced image is not a file: ${absolutePath}`);
142
- if (metadata.size > MAX_INPUT_BYTES) throw new Error(`Referenced image exceeds 50 MiB: ${absolutePath}`);
143
- const bytes = await readFile(absolutePath, { signal });
144
- if (bytes.length > MAX_INPUT_BYTES) throw new Error(`Referenced image exceeds 50 MiB: ${absolutePath}`);
145
- const mimeType = detectImageMimeType(bytes);
146
- if (!mimeType) {
147
- throw new Error(`Referenced image is not a supported PNG, JPEG, or WebP file: ${absolutePath}`);
148
- }
149
- images.push({ mimeType, data: bytes.toString("base64") });
219
+ await link(temporaryPath, absolutePath);
220
+ } catch (error) {
221
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
222
+ throw error;
150
223
  }
224
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
225
+ });
226
+ }
151
227
 
228
+ export async function generateImage(
229
+ ctx: ImageGenerationContext,
230
+ request: GenerateImageRequest,
231
+ ): Promise<GeneratedImageResult> {
232
+ const signal = request.signal;
233
+ const secrets: string[] = [];
234
+ try {
235
+ signal?.throwIfAborted();
236
+ const prompt = parsePrompt(request);
237
+ const requestedAbsolutePath = resolveRequestedAbsolutePath(ctx.cwd, request.path);
238
+ const { provider, token } = await resolveProviderAuth(ctx, request.provider, signal, secrets);
239
+ const images = await loadReferencedImages(ctx.cwd, request.referencedImagePaths, signal);
152
240
  const operation = images.length === 0 ? "generate" : "edit";
153
241
  const model = provider === "openai" ? OPENAI_IMAGE_MODEL : XAI_IMAGE_MODEL;
154
- const selectedProvider = provider;
155
- const selectedToken = token;
156
242
  signal?.throwIfAborted();
157
243
  const generated =
158
244
  operation === "generate"
159
- ? await requestGeneratedImage(selectedProvider, prompt, selectedToken, signal)
160
- : await editImage(selectedProvider, prompt, images, selectedToken, signal);
245
+ ? await requestGeneratedImage(provider, prompt, token, signal)
246
+ : await editImage(provider, prompt, images, token, signal);
161
247
  signal?.throwIfAborted();
162
- const generatedExtension = outputExtension(generated.mimeType);
163
- if (requestedAbsolutePath) {
164
- const requestedExtension = extname(requestedAbsolutePath).toLowerCase();
165
- const matches =
166
- requestedExtension === generatedExtension ||
167
- (generatedExtension === ".jpg" && requestedExtension === ".jpeg");
168
- if (!matches) {
169
- throw new Error(
170
- `${selectedProvider === "openai" ? "OpenAI Codex" : "xAI"} returned ${generated.mimeType}; destination must end in ${generatedExtension}`,
171
- );
172
- }
173
- }
174
-
175
- let dimensions: ReturnType<typeof imageSize>;
176
- try {
177
- dimensions = imageSize(generated.bytes);
178
- } catch {
179
- throw new Error(`${selectedProvider === "openai" ? "OpenAI Codex" : "xAI"} returned invalid image dimensions`);
180
- }
181
- if (
182
- !Number.isInteger(dimensions.width) ||
183
- dimensions.width <= 0 ||
184
- !Number.isInteger(dimensions.height) ||
185
- dimensions.height <= 0
186
- ) {
187
- throw new Error(`${selectedProvider === "openai" ? "OpenAI Codex" : "xAI"} returned invalid image dimensions`);
188
- }
189
-
248
+ const generatedExtension = assertDestinationMatchesMime(requestedAbsolutePath, generated.mimeType, provider);
249
+ const dimensions = readImageDimensions(generated.bytes, provider);
190
250
  const absolutePath =
191
251
  requestedAbsolutePath ??
192
252
  join(homedir(), ".local", "share", "tau-agent", "images", `image-${randomUUID()}${generatedExtension}`);
193
- const outputDirectory = dirname(absolutePath);
194
- await withFileMutationQueue(absolutePath, async () => {
195
- await mkdir(outputDirectory, { recursive: true });
196
- const temporaryPath = join(
197
- outputDirectory,
198
- `.${basename(absolutePath)}.${randomUUID()}.tmp${generatedExtension}`,
199
- );
200
- try {
201
- await writeFile(temporaryPath, generated.bytes, { flag: "wx", signal });
202
- signal?.throwIfAborted();
203
- await link(temporaryPath, absolutePath);
204
- } catch (error) {
205
- await rm(temporaryPath, { force: true }).catch(() => undefined);
206
- throw error;
207
- }
208
- await rm(temporaryPath, { force: true }).catch(() => undefined);
209
- });
210
-
253
+ await writeImageAtomically(absolutePath, generated.bytes, generatedExtension, signal);
211
254
  return {
212
255
  bytes: generated.bytes,
213
256
  path: absolutePath,
214
- provider: selectedProvider,
257
+ provider,
215
258
  model,
216
259
  operation,
217
260
  mimeType: generated.mimeType,
package/src/index.ts CHANGED
@@ -5,3 +5,5 @@ export {
5
5
  type ImageGenerationContext,
6
6
  type ImageProvider,
7
7
  } from "./image-generation/index.ts";
8
+ export { FILE_INJECTION_TYPE, prepareFileInjection } from "./file-injection/index.ts";
9
+ export { registerDeferredToolGroup, type DeferredToolGroup } from "./tool-loading/index.ts";
@@ -0,0 +1,58 @@
1
+ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import { emitTauEvent, onTauEventImmediately } from "../../shared/events.ts";
3
+
4
+ export interface DeferredToolGroup {
5
+ id: string;
6
+ description: string;
7
+ tools: readonly ToolDefinition[];
8
+ }
9
+
10
+ export type DeferredToolHost = Pick<ExtensionAPI, "events" | "on" | "registerTool">;
11
+
12
+ export interface DeferredToolGroupInfo {
13
+ id: string;
14
+ description: string;
15
+ toolNames: readonly string[];
16
+ }
17
+
18
+ // Pi evaluates each extension in an isolated module graph, so module-local registries are not shared.
19
+ const DEFERRED_TOOL_GROUP_REQUEST_EVENT = "tau:deferred-tool-group.request";
20
+
21
+ export function registerDeferredToolGroup(pi: DeferredToolHost, group: DeferredToolGroup): void {
22
+ const id = group.id.trim();
23
+ if (id.length === 0 || id !== group.id) throw new Error("Deferred tool group ID must be non-empty and trimmed");
24
+ if (group.description.trim().length === 0) throw new Error(`Deferred tool group ${id} needs a description`);
25
+ if (group.tools.length === 0) throw new Error(`Deferred tool group ${id} needs at least one tool`);
26
+
27
+ const toolNames = group.tools.map((tool) => tool.name);
28
+ if (new Set(toolNames).size !== toolNames.length) {
29
+ throw new Error(`Deferred tool group ${id} contains duplicate tool names`);
30
+ }
31
+ if (toolNames.includes("load_tools")) throw new Error("load_tools cannot be part of a deferred tool group");
32
+
33
+ const groups = registeredDeferredToolGroups(pi);
34
+ if (groups.some((existing) => existing.id === id)) {
35
+ throw new Error(`Deferred tool group is already registered: ${id}`);
36
+ }
37
+ for (const existing of groups) {
38
+ if (toolNames.some((name) => existing.toolNames.includes(name))) {
39
+ throw new Error(`Deferred tool group ${id} overlaps an existing tool group`);
40
+ }
41
+ }
42
+
43
+ for (const tool of group.tools) pi.registerTool(tool);
44
+ const info = { id, description: group.description, toolNames } satisfies DeferredToolGroupInfo;
45
+ onTauEventImmediately(pi, `deferred-tool-group.${id}`, DEFERRED_TOOL_GROUP_REQUEST_EVENT, ({ accept }) => {
46
+ accept(info);
47
+ });
48
+ }
49
+
50
+ export function registeredDeferredToolGroups(pi: Pick<ExtensionAPI, "events">): readonly DeferredToolGroupInfo[] {
51
+ const groups: DeferredToolGroupInfo[] = [];
52
+ emitTauEvent(pi, DEFERRED_TOOL_GROUP_REQUEST_EVENT, {
53
+ accept(group: DeferredToolGroupInfo) {
54
+ groups.push(group);
55
+ },
56
+ });
57
+ return groups;
58
+ }