@shanepadgett/tau-agent 0.20.2 → 0.22.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/README.md +18 -0
- package/docs/extending-tau-agent.md +34 -2
- package/extensions/context-pruning/README.md +18 -114
- package/extensions/context-pruning/index.ts +72 -74
- package/extensions/context-pruning/projection.ts +38 -96
- package/extensions/context-pruning/prune.ts +135 -287
- package/extensions/context-pruning/render.ts +95 -35
- package/extensions/context-pruning/settings.ts +14 -15
- package/extensions/explore/read-cache.ts +31 -7
- package/extensions/handoff/README.md +7 -0
- package/extensions/handoff/index.ts +145 -0
- package/extensions/handoff/model.ts +71 -0
- package/extensions/image-gen/index.ts +24 -133
- package/extensions/subagent/index.ts +3 -1
- package/extensions/tau-help/help.md +5 -1
- package/package.json +9 -2
- package/schemas/tau.schema.json +15 -12
- package/shared/context-pruning-state.ts +69 -273
- package/shared/model-fallback/index.ts +1 -1
- package/{extensions/image-gen → src/image-generation}/client.ts +30 -20
- package/src/image-generation/index.ts +224 -0
- package/src/index.ts +7 -0
- package/extensions/context-pruning/file-evidence.ts +0 -265
- /package/{extensions/image-gen → src/image-generation}/constants.ts +0 -0
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { defineTool,
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
generateImage,
|
|
4
|
+
type GeneratedImageResult,
|
|
5
|
+
type GenerateImageRequest,
|
|
6
|
+
type ImageGenerationContext,
|
|
7
|
+
type ImageProvider,
|
|
8
|
+
} from "@shanepadgett/tau-agent";
|
|
6
9
|
import { type Static, Type } from "typebox";
|
|
7
|
-
import { detectImageMimeType, editImage, generateImage, type EditImage, type GeneratedImage } from "./client.ts";
|
|
8
|
-
import { OPENAI_IMAGE_MODEL, OPENAI_PROVIDER, XAI_IMAGE_MODEL, XAI_PROVIDER, type ImageProvider } from "./constants.ts";
|
|
9
10
|
|
|
10
|
-
const MAX_INPUT_BYTES = 50 * 1024 * 1024;
|
|
11
11
|
const MAX_INLINE_BYTES = 12 * 1024 * 1024;
|
|
12
12
|
|
|
13
13
|
const imageGenSchema = Type.Object(
|
|
14
14
|
{
|
|
15
15
|
prompt: Type.String({ minLength: 1 }),
|
|
16
16
|
provider: Type.Optional(
|
|
17
|
-
Type.Union([Type.Literal("openai"), Type.Literal("xai")], {
|
|
17
|
+
Type.Union([Type.Literal("openai" satisfies ImageProvider), Type.Literal("xai" satisfies ImageProvider)], {
|
|
18
18
|
description:
|
|
19
19
|
"Image provider override. Omit to follow the parent model, preferring OpenAI for GPT and xAI for Grok.",
|
|
20
20
|
}),
|
|
@@ -28,19 +28,7 @@ const imageGenSchema = Type.Object(
|
|
|
28
28
|
);
|
|
29
29
|
|
|
30
30
|
type ImageGenParams = Static<typeof imageGenSchema>;
|
|
31
|
-
|
|
32
|
-
interface ImageGenDetails {
|
|
33
|
-
path: string;
|
|
34
|
-
provider: ImageProvider;
|
|
35
|
-
model: typeof OPENAI_IMAGE_MODEL | typeof XAI_IMAGE_MODEL;
|
|
36
|
-
operation: "generate" | "edit";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function outputExtension(image: GeneratedImage): string {
|
|
40
|
-
if (image.mimeType === "image/png") return ".png";
|
|
41
|
-
if (image.mimeType === "image/webp") return ".webp";
|
|
42
|
-
return ".jpg";
|
|
43
|
-
}
|
|
31
|
+
type ImageGenDetails = Omit<GeneratedImageResult, "bytes">;
|
|
44
32
|
|
|
45
33
|
export default function imageGenExtension(pi: ExtensionAPI): void {
|
|
46
34
|
pi.registerTool(
|
|
@@ -51,127 +39,30 @@ export default function imageGenExtension(pi: ExtensionAPI): void {
|
|
|
51
39
|
"Generate a requested raster image or AI-edit existing images with OpenAI GPT Image or xAI Grok Imagine. Omit provider to follow the parent model; set it to openai or xai to override. Omit referenced_image_paths to generate; pass one to three local paths to edit or compose. Omit path to use Tau's external image store; pass path only when the user explicitly requests a repository file or other destination. Returns the image for inspection.",
|
|
52
40
|
parameters: imageGenSchema,
|
|
53
41
|
async execute(_toolCallId, params: ImageGenParams, signal, onUpdate, ctx) {
|
|
54
|
-
signal?.throwIfAborted();
|
|
55
|
-
const prompt = params.prompt.trim();
|
|
56
|
-
if (!prompt) throw new Error("Image prompt cannot be empty");
|
|
57
|
-
const requestedPath = params.path?.startsWith("@") ? params.path.slice(1) : params.path;
|
|
58
|
-
if (requestedPath !== undefined && !requestedPath.trim()) throw new Error("Image path cannot be empty");
|
|
59
|
-
const requestedAbsolutePath = requestedPath
|
|
60
|
-
? isAbsolute(requestedPath)
|
|
61
|
-
? requestedPath
|
|
62
|
-
: resolve(ctx.cwd, requestedPath)
|
|
63
|
-
: undefined;
|
|
64
|
-
if (
|
|
65
|
-
requestedAbsolutePath &&
|
|
66
|
-
![".jpg", ".jpeg", ".png", ".webp"].includes(extname(requestedAbsolutePath).toLowerCase())
|
|
67
|
-
) {
|
|
68
|
-
throw new Error("Image path must end in .jpg, .jpeg, .png, or .webp");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const parentUsesXai =
|
|
72
|
-
ctx.model?.provider.toLowerCase() === XAI_PROVIDER ||
|
|
73
|
-
ctx.model?.id.toLowerCase().includes("grok") === true;
|
|
74
|
-
const preferredProvider: ImageProvider = params.provider ?? (parentUsesXai ? "xai" : "openai");
|
|
75
|
-
const providers: readonly ImageProvider[] = params.provider
|
|
76
|
-
? [params.provider]
|
|
77
|
-
: preferredProvider === "xai"
|
|
78
|
-
? ["xai", "openai"]
|
|
79
|
-
: ["openai", "xai"];
|
|
80
|
-
let provider: ImageProvider | undefined;
|
|
81
|
-
let token: string | undefined;
|
|
82
|
-
for (const candidate of providers) {
|
|
83
|
-
const candidateToken = await ctx.modelRegistry.getApiKeyForProvider(
|
|
84
|
-
candidate === "openai" ? OPENAI_PROVIDER : XAI_PROVIDER,
|
|
85
|
-
);
|
|
86
|
-
if (candidateToken) {
|
|
87
|
-
provider = candidate;
|
|
88
|
-
token = candidateToken;
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (!provider || !token) {
|
|
93
|
-
if (params.provider === "openai") {
|
|
94
|
-
throw new Error("OpenAI Codex authentication is unavailable. Run /login openai-codex.");
|
|
95
|
-
}
|
|
96
|
-
if (params.provider === "xai") {
|
|
97
|
-
throw new Error("xAI authentication is unavailable. Run /login xai and choose a login method.");
|
|
98
|
-
}
|
|
99
|
-
throw new Error("Image generation authentication is unavailable. Run /login for OpenAI Codex or xAI.");
|
|
100
|
-
}
|
|
101
|
-
const model = provider === "openai" ? OPENAI_IMAGE_MODEL : XAI_IMAGE_MODEL;
|
|
102
|
-
const images: EditImage[] = [];
|
|
103
|
-
for (const path of params.referenced_image_paths ?? []) {
|
|
104
|
-
const rawPath = path.startsWith("@") ? path.slice(1) : path;
|
|
105
|
-
if (!rawPath.trim()) throw new Error("Referenced image path cannot be empty");
|
|
106
|
-
const absolutePath = isAbsolute(rawPath) ? rawPath : resolve(ctx.cwd, rawPath);
|
|
107
|
-
const metadata = await stat(absolutePath);
|
|
108
|
-
if (!metadata.isFile()) throw new Error(`Referenced image is not a file: ${absolutePath}`);
|
|
109
|
-
if (metadata.size > MAX_INPUT_BYTES) throw new Error(`Referenced image exceeds 50 MiB: ${absolutePath}`);
|
|
110
|
-
const bytes = await readFile(absolutePath);
|
|
111
|
-
if (bytes.length > MAX_INPUT_BYTES) throw new Error(`Referenced image exceeds 50 MiB: ${absolutePath}`);
|
|
112
|
-
const mimeType = detectImageMimeType(bytes);
|
|
113
|
-
if (!mimeType)
|
|
114
|
-
throw new Error(`Referenced image is not a supported PNG, JPEG, or WebP file: ${absolutePath}`);
|
|
115
|
-
images.push({ mimeType, data: bytes.toString("base64") });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const operation = images.length === 0 ? "generate" : "edit";
|
|
119
42
|
await onUpdate?.({
|
|
120
43
|
content: [
|
|
121
44
|
{
|
|
122
45
|
type: "text",
|
|
123
|
-
text:
|
|
124
|
-
operation === "generate"
|
|
125
|
-
? `Generating image with ${model}...`
|
|
126
|
-
: `Editing image with ${model}...`,
|
|
46
|
+
text: "Processing image request...",
|
|
127
47
|
},
|
|
128
48
|
],
|
|
129
49
|
details: undefined,
|
|
130
50
|
});
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
(generatedExtension === ".jpg" && requestedExtension === ".jpeg");
|
|
142
|
-
if (!matches)
|
|
143
|
-
throw new Error(
|
|
144
|
-
`${provider === "openai" ? "OpenAI Codex" : "xAI"} returned ${generated.mimeType}; destination must end in ${generatedExtension}`,
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
const absolutePath =
|
|
148
|
-
requestedAbsolutePath ??
|
|
149
|
-
join(homedir(), ".local", "share", "tau-agent", "images", `image-${randomUUID()}${generatedExtension}`);
|
|
150
|
-
|
|
151
|
-
const outputDirectory = dirname(absolutePath);
|
|
152
|
-
await withFileMutationQueue(absolutePath, async () => {
|
|
153
|
-
await mkdir(outputDirectory, { recursive: true });
|
|
154
|
-
const temporaryPath = join(
|
|
155
|
-
outputDirectory,
|
|
156
|
-
`.${basename(absolutePath)}.${randomUUID()}.tmp${generatedExtension}`,
|
|
157
|
-
);
|
|
158
|
-
try {
|
|
159
|
-
await writeFile(temporaryPath, generated.bytes, { flag: "wx" });
|
|
160
|
-
signal?.throwIfAborted();
|
|
161
|
-
await link(temporaryPath, absolutePath);
|
|
162
|
-
} finally {
|
|
163
|
-
await rm(temporaryPath, { force: true });
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const verb = operation === "generate" ? "Generated" : "Edited";
|
|
168
|
-
const details: ImageGenDetails = { path: absolutePath, provider, model, operation };
|
|
169
|
-
if (generated.bytes.length > MAX_INLINE_BYTES) {
|
|
51
|
+
const request = {
|
|
52
|
+
prompt: params.prompt,
|
|
53
|
+
provider: params.provider,
|
|
54
|
+
path: params.path,
|
|
55
|
+
referencedImagePaths: params.referenced_image_paths,
|
|
56
|
+
signal,
|
|
57
|
+
} satisfies GenerateImageRequest;
|
|
58
|
+
const { bytes, ...details } = await generateImage(ctx satisfies ImageGenerationContext, request);
|
|
59
|
+
const verb = details.operation === "generate" ? "Generated" : "Edited";
|
|
60
|
+
if (bytes.length > MAX_INLINE_BYTES) {
|
|
170
61
|
return {
|
|
171
62
|
content: [
|
|
172
63
|
{
|
|
173
64
|
type: "text",
|
|
174
|
-
text: `${verb} image saved to ${
|
|
65
|
+
text: `${verb} image saved to ${details.path}. The image exceeds the 12 MiB attachment limit, so it was not added to model context.`,
|
|
175
66
|
},
|
|
176
67
|
],
|
|
177
68
|
details,
|
|
@@ -179,8 +70,8 @@ export default function imageGenExtension(pi: ExtensionAPI): void {
|
|
|
179
70
|
}
|
|
180
71
|
return {
|
|
181
72
|
content: [
|
|
182
|
-
{ type: "text", text: `${verb} image saved to ${
|
|
183
|
-
{ type: "image", data:
|
|
73
|
+
{ type: "text", text: `${verb} image saved to ${details.path}` },
|
|
74
|
+
{ type: "image", data: bytes.toString("base64"), mimeType: details.mimeType },
|
|
184
75
|
],
|
|
185
76
|
details,
|
|
186
77
|
};
|
|
@@ -145,7 +145,9 @@ Start a fresh thread with \`agent\` and \`task\`. Continue an existing thread wi
|
|
|
145
145
|
|
|
146
146
|
Pass \`files\` when exact relevant files are already known. Tau autoreads current line-numbered snapshots into that child turn before it starts.
|
|
147
147
|
|
|
148
|
-
Delegate one focused task per call. Children do not inherit parent messages. Include exact absolute reference paths when a child must inspect a repository outside the current working directory
|
|
148
|
+
Delegate one focused task per call. Children do not inherit parent messages. Include exact absolute reference paths when a child must inspect a repository outside the current working directory.
|
|
149
|
+
|
|
150
|
+
Review limit: for one user task or coherent implementation batch, call the \`review\` agent at most twice. The first call is the initial review. The only permitted second call is a follow-up in the same retained thread to check fixes from that initial review. Never call \`review\` a third time for the same task. Do not call it for documentation-only work, trivial edits, small localized changes, or micro-adjustments after review; inspect and test those directly.`;
|
|
149
151
|
return { systemPrompt: `${event.systemPrompt}\n\n${prompt}` };
|
|
150
152
|
});
|
|
151
153
|
pi.registerTool(
|
|
@@ -36,7 +36,7 @@ Adds `/context` to select reusable repository work scopes from `.pi/contexts`, a
|
|
|
36
36
|
|
|
37
37
|
## context-pruning
|
|
38
38
|
|
|
39
|
-
Gives the agent `context_prune` for
|
|
39
|
+
Gives the agent `context_prune` for creating a hard context checkpoint after broad exploration converges. Everything before the checkpoint leaves future model input unless the agent retains an exact tool exchange or carries a file forward as a fresh snapshot. File snapshot failures are reported without blocking the checkpoint. Deferred files remain as a small conditional note. Context markers use an ordered instruction ladder that escalates from informational guidance to pruning before further tool work. Run `/prune` with no arguments to request a checkpoint manually. Session history remains unchanged, and checkpoints, markers, and pruned-row state follow the active branch.
|
|
40
40
|
|
|
41
41
|
## explore
|
|
42
42
|
|
|
@@ -46,6 +46,10 @@ Replaces Pi’s filesystem inspection tools with compact Tau versions: `ls`, `fi
|
|
|
46
46
|
|
|
47
47
|
Adds `/footer` to toggle and refresh Tau’s status footer.
|
|
48
48
|
|
|
49
|
+
## handoff
|
|
50
|
+
|
|
51
|
+
Adds `/handoff <goal>` to start a fresh linked session from the current conversation. Tau generates an opening prompt, autoreads the relevant project files it already knows about, and leaves the prompt in the editor for review instead of submitting it.
|
|
52
|
+
|
|
49
53
|
## ideas
|
|
50
54
|
|
|
51
55
|
Adds `/ideas` to log rough ideas or open the ideas browser.
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shanepadgett/tau-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Tau is a custom agentic harness built with pi extensions",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
6
11
|
"license": "MIT",
|
|
7
12
|
"keywords": [
|
|
8
13
|
"pi-package",
|
|
@@ -18,6 +23,7 @@
|
|
|
18
23
|
"access": "public"
|
|
19
24
|
},
|
|
20
25
|
"files": [
|
|
26
|
+
"src/**/*",
|
|
21
27
|
"extensions/**/*",
|
|
22
28
|
"shared/**/*",
|
|
23
29
|
"prompts/**/*",
|
|
@@ -28,8 +34,9 @@
|
|
|
28
34
|
"README.md"
|
|
29
35
|
],
|
|
30
36
|
"dependencies": {
|
|
31
|
-
"@shanepadgett/tau-tui": "0.
|
|
37
|
+
"@shanepadgett/tau-tui": "0.22.0",
|
|
32
38
|
"@toon-format/toon": "2.3.0",
|
|
39
|
+
"image-size": "2.0.2",
|
|
33
40
|
"smol-toml": "1.7.0"
|
|
34
41
|
},
|
|
35
42
|
"peerDependencies": {
|
package/schemas/tau.schema.json
CHANGED
|
@@ -68,18 +68,21 @@
|
|
|
68
68
|
"maximum": 100,
|
|
69
69
|
"description": "Context growth interval between automatic pruning hints."
|
|
70
70
|
},
|
|
71
|
-
"
|
|
72
|
-
"type": "
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
"nudgeInstructions": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"items": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"minLength": 1,
|
|
76
|
+
"maxLength": 2000
|
|
77
|
+
},
|
|
78
|
+
"default": [
|
|
79
|
+
"No prune is required yet unless broad exploration has converged or substantial evidence is already irrelevant. Continue coherent work.",
|
|
80
|
+
"Move toward a pruning point now. Finish the current coherent step, then prune before starting another broad exploration. Managed context is materially increasing model cost.",
|
|
81
|
+
"Prune now before further tool work. Continuing with stale managed context is wasting money."
|
|
82
|
+
],
|
|
83
|
+
"minItems": 1,
|
|
84
|
+
"maxItems": 5,
|
|
85
|
+
"description": "Ordered automatic pruning instructions. Later reminders repeat the final instruction, and the final tier requires an anchor before further tool work."
|
|
83
86
|
}
|
|
84
87
|
},
|
|
85
88
|
"additionalProperties": false
|