@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.
- package/docs/context.md +29 -20
- package/docs/extending-tau-agent.md +63 -24
- package/extensions/appshot/index.ts +112 -111
- package/extensions/aside/README.md +7 -0
- package/extensions/aside/index.ts +168 -0
- package/extensions/aside/panel.ts +83 -0
- package/extensions/branch/index.ts +38 -21
- package/extensions/cache-diagnostics/index.ts +70 -58
- package/extensions/checkpoint/README.md +9 -0
- package/extensions/checkpoint/checkpoint-budget.ts +79 -0
- package/extensions/checkpoint/checkpoint.ts +277 -0
- package/extensions/checkpoint/index.ts +102 -0
- package/extensions/checkpoint/messages.ts +169 -0
- package/extensions/checkpoint/prompt.ts +24 -0
- package/extensions/checkpoint/settings.ts +27 -0
- package/extensions/commit/commit-effort.ts +3 -3
- package/extensions/context/README.md +12 -8
- package/extensions/context/definitions.ts +178 -55
- package/extensions/context/index.ts +192 -160
- package/extensions/context/panel.ts +57 -37
- package/extensions/context/sync.ts +139 -89
- package/extensions/context/validation.ts +37 -25
- package/extensions/effort/README.md +1 -1
- package/extensions/effort/index.ts +3 -3
- package/extensions/effort/state.ts +3 -18
- package/extensions/explore/guidance.ts +6 -47
- package/extensions/explore/index.ts +38 -53
- package/extensions/explore/{ast/read → read}/hook.ts +12 -7
- package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
- package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
- package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
- package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
- package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
- package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
- package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
- package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
- package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
- package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
- package/extensions/explore/tools/target-params.ts +9 -0
- package/extensions/footer/index.ts +75 -56
- package/extensions/handoff/index.ts +8 -30
- package/extensions/image-gen/index.ts +46 -40
- package/extensions/patch/executor.ts +190 -135
- package/extensions/patch/matcher.ts +82 -48
- package/extensions/patch/parser.ts +149 -105
- package/extensions/patch/render.ts +32 -39
- package/extensions/patch/summary.ts +30 -26
- package/extensions/ready/README.md +7 -0
- package/extensions/ready/index.ts +58 -0
- package/extensions/ready/model.ts +128 -0
- package/extensions/ready/packs.ts +272 -0
- package/extensions/ready/render.ts +445 -0
- package/extensions/ready/scan.ts +760 -0
- package/extensions/reference/panel.ts +28 -16
- package/extensions/review/index.ts +63 -51
- package/extensions/review/panel.ts +5 -34
- package/extensions/runtime-context/context.ts +30 -26
- package/extensions/script-runner/README.md +2 -2
- package/extensions/script-runner/index.ts +122 -82
- package/extensions/silent-command-runner/index.ts +38 -23
- package/extensions/subagent/agents/context-sync.md +185 -33
- package/extensions/subagent/agents/scout.md +2 -2
- package/extensions/subagent/agents.ts +137 -84
- package/extensions/subagent/cmux-dashboard.ts +39 -30
- package/extensions/subagent/index.ts +79 -59
- package/extensions/subagent/resume.ts +1 -1
- package/extensions/subagent/run.ts +304 -193
- package/extensions/subagent/runtime.ts +781 -343
- package/extensions/tau/index.ts +47 -38
- package/extensions/tau-help/help.md +14 -6
- package/extensions/tool-loader/README.md +4 -2
- package/extensions/tool-loader/index.ts +77 -52
- package/extensions/web/index.ts +6 -3
- package/extensions/web/webfetch.ts +114 -86
- package/package.json +3 -3
- package/schemas/tau.schema.json +20 -33
- package/shared/bounded-text-result.ts +1 -0
- package/shared/checkpoint-visibility.ts +9 -0
- package/shared/events.ts +28 -10
- package/shared/isolated-session.ts +71 -29
- package/shared/model-effort.ts +9 -19
- package/shared/model-fallback/index.ts +66 -41
- package/shared/settings/load.ts +45 -38
- package/shared/temporary-output-store.ts +16 -10
- package/{extensions/explore → src}/ast/adapter.ts +11 -2
- package/{extensions/explore → src}/ast/engine.ts +54 -38
- package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
- package/{extensions/explore → src}/ast/format/context.ts +3 -6
- package/src/ast/format/deps.ts +94 -0
- package/{extensions/explore → src}/ast/format/discover.ts +2 -2
- package/{extensions/explore → src}/ast/format/impact.ts +9 -11
- package/{extensions/explore → src}/ast/format/outline.ts +2 -2
- package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
- package/{extensions/explore → src}/ast/format/show.ts +1 -1
- package/src/ast/format/targeted-result.ts +20 -0
- package/src/ast/grammars/c_sharp.wasm +0 -0
- package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
- package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
- package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
- package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
- package/{extensions/explore → src}/ast/ir.ts +6 -0
- package/src/ast/languages/csharp-file-deps.ts +90 -0
- package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
- package/src/ast/languages/dotted-index.ts +131 -0
- package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
- package/src/ast/languages/fixtures/sample.cs +53 -0
- package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
- package/src/ast/languages/fixtures/sample.go +37 -0
- package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
- package/src/ast/languages/fixtures/sample.java +40 -0
- package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
- package/src/ast/languages/fixtures/sample.kt +31 -0
- package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
- package/src/ast/languages/fixtures/sample.md +23 -0
- package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
- package/src/ast/languages/fixtures/sample.rs +54 -0
- package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
- package/src/ast/languages/fixtures/sample.swift +45 -0
- package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
- package/src/ast/languages/fixtures/sample.ts +45 -0
- package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
- package/src/ast/languages/fixtures/sample.tsx +36 -0
- package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
- package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
- package/{extensions/explore → src}/ast/languages/go.ts +21 -3
- package/{extensions/explore → src}/ast/languages/java.ts +5 -38
- package/src/ast/languages/jvm-file-deps.ts +135 -0
- package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
- package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
- package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
- package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
- package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
- package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
- package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
- package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
- package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
- package/{extensions/explore → src}/ast/markdown.ts +72 -47
- package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
- package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
- package/{extensions/explore → src}/ast/queries/context.ts +116 -80
- package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
- package/src/ast/queries/impact.ts +291 -0
- package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
- package/{extensions/explore → src}/ast/queries/show.ts +46 -42
- package/{extensions/explore → src}/ast/query.ts +1 -1
- package/{extensions/explore → src}/ast/registry.ts +1 -2
- package/{extensions/explore → src}/ast/scan.ts +1 -1
- package/src/ast/session.ts +50 -0
- package/{extensions/explore → src}/ast/slice.ts +8 -0
- package/{extensions/explore → src/ast}/traverse.ts +32 -20
- package/src/file-injection/index.ts +547 -0
- package/src/image-generation/client.ts +50 -31
- package/src/image-generation/index.ts +177 -134
- package/src/index.ts +2 -0
- package/src/tool-loading/index.ts +58 -0
- package/extensions/context/evidence.ts +0 -517
- package/extensions/context/projection.ts +0 -141
- package/extensions/context/state.ts +0 -30
- package/extensions/context/write-scope.ts +0 -109
- package/extensions/explore/ast/format/deps.ts +0 -86
- package/extensions/explore/ast/grammars/odin.wasm +0 -0
- package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
- package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
- package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
- package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
- package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
- package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
- package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
- package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
- package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
- package/extensions/explore/ast/languages/odin.ts +0 -407
- package/extensions/explore/ast/queries/impact.ts +0 -250
- package/extensions/explore/outline-injection.ts +0 -151
- package/extensions/explore/read/autoread.ts +0 -118
- package/extensions/working-memory/README.md +0 -17
- package/extensions/working-memory/checkpoint.ts +0 -242
- package/extensions/working-memory/index.ts +0 -341
- package/extensions/working-memory/memory.ts +0 -235
- package/extensions/working-memory/render.ts +0 -185
- package/extensions/working-memory/settings.ts +0 -38
- package/extensions/working-memory/state.ts +0 -158
- package/shared/autoread.ts +0 -234
- package/shared/context-messages.ts +0 -19
- package/shared/outline-injection.ts +0 -56
- /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
- /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
- /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
- /package/{extensions/explore → src}/ast/identity.ts +0 -0
- /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
- /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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
160
|
-
: await editImage(
|
|
245
|
+
? await requestGeneratedImage(provider, prompt, token, signal)
|
|
246
|
+
: await editImage(provider, prompt, images, token, signal);
|
|
161
247
|
signal?.throwIfAborted();
|
|
162
|
-
const generatedExtension =
|
|
163
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|