@shanepadgett/tau-agent 0.43.0 → 0.44.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.
|
@@ -169,7 +169,6 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
|
169
169
|
const langPhrase = formatLangList(detected);
|
|
170
170
|
|
|
171
171
|
const scripts = new Map<string, StoredScript>();
|
|
172
|
-
let tempDir: string | undefined;
|
|
173
172
|
|
|
174
173
|
function remember(scriptId: string, script: StoredScript): void {
|
|
175
174
|
scripts.set(scriptId, script);
|
|
@@ -190,12 +189,6 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
|
190
189
|
throw new Error("Deno is not available on this machine.");
|
|
191
190
|
}
|
|
192
191
|
|
|
193
|
-
async function ensureTempDir(): Promise<string> {
|
|
194
|
-
if (tempDir) return tempDir;
|
|
195
|
-
tempDir = await mkdtemp(join(tmpdir(), "tau-script-runner-"));
|
|
196
|
-
return tempDir;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
192
|
async function runScript(
|
|
200
193
|
language: Language,
|
|
201
194
|
command: string,
|
|
@@ -203,21 +196,27 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
|
203
196
|
cwd: string,
|
|
204
197
|
signal: AbortSignal | undefined,
|
|
205
198
|
): Promise<ExecResult> {
|
|
206
|
-
const dir = await
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
199
|
+
const dir = await mkdtemp(join(tmpdir(), "tau-script-runner-"));
|
|
200
|
+
try {
|
|
201
|
+
const file = join(dir, language === "python3" ? "_run.py" : "_run.ts");
|
|
202
|
+
await writeFile(file, source, "utf8");
|
|
203
|
+
const args =
|
|
204
|
+
language === "python3"
|
|
205
|
+
? [file]
|
|
206
|
+
: language === "node"
|
|
207
|
+
? ["--experimental-strip-types", file]
|
|
208
|
+
: ["run", "-A", file];
|
|
209
|
+
const result = await pi.exec(command, args, { cwd, signal, timeout: TIMEOUT_MS });
|
|
210
|
+
return {
|
|
211
|
+
...result,
|
|
212
|
+
stdout: scrubPath(result.stdout, file, dir),
|
|
213
|
+
stderr: scrubPath(result.stderr, file, dir),
|
|
214
|
+
};
|
|
215
|
+
} finally {
|
|
216
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
217
|
+
// best-effort cleanup
|
|
218
|
+
});
|
|
219
|
+
}
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
const paramsSchema = Type.Object(
|
|
@@ -315,12 +314,5 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
|
315
314
|
|
|
316
315
|
pi.on("session_shutdown", () => {
|
|
317
316
|
scripts.clear();
|
|
318
|
-
const dir = tempDir;
|
|
319
|
-
tempDir = undefined;
|
|
320
|
-
if (dir) {
|
|
321
|
-
void rm(dir, { recursive: true, force: true }).catch(() => {
|
|
322
|
-
// best-effort cleanup
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
317
|
});
|
|
326
318
|
}
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Reviews agent `bash` and `script_runner` requests before they run.
|
|
4
4
|
|
|
5
|
-
Common read-only bash commands skip review and run immediately. Other bash and every `script_runner` request go to a separate reviewer. Tau uses the reviewer model for the current provider, then the current chat model if that reviewer is unavailable or fails. If a reviewer model is unavailable or fails, Tau notifies and tries the next one. The reviewer returns a validated decision and one concise paragraph that explains the request.
|
|
5
|
+
Common read-only bash commands skip review and run immediately. Other bash and every `script_runner` request go to a separate reviewer. When the agent requests several tools at once, Tau reviews up to three requests concurrently, with one model review focused on each request. Tau uses the reviewer model for the current provider, then the current chat model if that reviewer is unavailable or fails. If a reviewer model is unavailable or fails, Tau notifies and tries the next one. The reviewer returns a validated decision and one concise paragraph that explains the request.
|
|
6
6
|
|
|
7
7
|
With `autoApprove` enabled, reviewer-approved requests run without another confirmation. Tau shows a user-only marker with the reviewer model after those auto-approvals. Common read-only bash that skips review does not get a marker. Routine local development work should be approved, including requests that modify project files or run scripts. The reviewer asks for human approval only when it finds a concrete destructive, system, production, privileged, or security-sensitive effect.
|
|
8
8
|
|
|
9
|
-
When approval is required, Tau shows one paragraph that explains the effect and risk without repeating the request. If the reviewer fails or returns a malformed decision, Tau asks for direct human approval instead of running it automatically. Tau also sends an attention notification when the approval window opens.
|
|
9
|
+
When approval is required, Tau shows one paragraph that explains the effect and risk without repeating the request. If several requests need approval, their confirmation windows open one at a time. If the reviewer fails or returns a malformed decision, Tau asks for direct human approval instead of running it automatically. Tau also sends an attention notification when the approval window opens.
|
|
10
10
|
|
|
11
11
|
In the terminal approval panel, move between Approve and Reject, press `n` to add a note to the highlighted choice, then press Enter to choose. Enter saves an edited note before choosing; Escape cancels note editing or blocks the request from the choice list. A rejection note tells the agent why the request was blocked. An approval note reaches the agent with the tool result; it does not change the request being approved. To ask for a different request, reject it with a note. Long notes are truncated. RPC clients use the standard confirmation dialog without notes.
|
|
12
12
|
|
|
@@ -17,6 +17,7 @@ import toolApprovalSettings from "./settings.ts";
|
|
|
17
17
|
|
|
18
18
|
const STATUS_KEY = "tool-approval";
|
|
19
19
|
const AUTO_APPROVED_TYPE = "tau.tool-approval.auto-approved";
|
|
20
|
+
const MAX_CONCURRENT_REVIEWS = 3;
|
|
20
21
|
|
|
21
22
|
const REVIEW_SCHEMA = Type.Object(
|
|
22
23
|
{
|
|
@@ -75,6 +76,13 @@ interface ToolApprovalRequest {
|
|
|
75
76
|
input: Record<string, unknown>;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
type ToolReviewResult = { review: ToolReview; provider: string; model: string };
|
|
80
|
+
|
|
81
|
+
interface CachedReview {
|
|
82
|
+
requestJson: string;
|
|
83
|
+
outcome: PromiseSettledResult<ToolReviewResult>;
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
interface AutoApprovedMarker {
|
|
79
87
|
toolName: ApprovalToolName;
|
|
80
88
|
provider: string;
|
|
@@ -83,6 +91,7 @@ interface AutoApprovedMarker {
|
|
|
83
91
|
|
|
84
92
|
export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
85
93
|
let settings = toolApprovalSettings.defaults;
|
|
94
|
+
let batchReviews: Map<string, CachedReview> | undefined;
|
|
86
95
|
const pendingNotes = new Map<string, string>();
|
|
87
96
|
|
|
88
97
|
pi.registerEntryRenderer<AutoApprovedMarker>(AUTO_APPROVED_TYPE, (entry, _options, theme) => {
|
|
@@ -136,6 +145,7 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
pi.on("session_start", async (_event, ctx) => {
|
|
148
|
+
batchReviews = undefined;
|
|
139
149
|
pendingNotes.clear();
|
|
140
150
|
await refreshSettings(ctx);
|
|
141
151
|
});
|
|
@@ -152,21 +162,32 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
152
162
|
}
|
|
153
163
|
if (!settings.enabled) return undefined;
|
|
154
164
|
|
|
155
|
-
let command: string | undefined;
|
|
156
165
|
if (request.toolName === "bash") {
|
|
157
|
-
const
|
|
158
|
-
if (typeof
|
|
159
|
-
if (!
|
|
166
|
+
const command = request.input.command;
|
|
167
|
+
if (typeof command !== "string") return block("bash command was malformed");
|
|
168
|
+
if (!command.trim()) {
|
|
160
169
|
ctx.ui.notify("Bash command blocked: command is empty", "warning");
|
|
161
170
|
return block("bash command is empty");
|
|
162
171
|
}
|
|
163
|
-
command = value;
|
|
164
172
|
if (isAllowlistedBash(command)) return undefined;
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
ctx.ui.setStatus(STATUS_KEY, `reviewing ${toolLabel(request.toolName)}`);
|
|
168
176
|
try {
|
|
169
|
-
|
|
177
|
+
batchReviews ??= await reviewAssistantRequests(ctx, event, request);
|
|
178
|
+
if (ctx.signal?.aborted) return block("Tool review cancelled");
|
|
179
|
+
const cached = batchReviews.get(event.toolCallId);
|
|
180
|
+
batchReviews.delete(event.toolCallId);
|
|
181
|
+
let result: ToolReviewResult;
|
|
182
|
+
if (cached && cached.requestJson === JSON.stringify(request)) {
|
|
183
|
+
if (cached.outcome.status === "rejected") throw cached.outcome.reason;
|
|
184
|
+
result = cached.outcome.value;
|
|
185
|
+
} else {
|
|
186
|
+
// A sibling's validated input may differ from its arguments in the assistant message.
|
|
187
|
+
result = await reviewToolRequest(ctx, request);
|
|
188
|
+
}
|
|
189
|
+
if (ctx.signal?.aborted) return block("Tool review cancelled");
|
|
190
|
+
const { review, provider, model } = result;
|
|
170
191
|
if (review.decision === "requires_user_approval") {
|
|
171
192
|
return requestToolApproval(
|
|
172
193
|
ctx,
|
|
@@ -192,6 +213,7 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
192
213
|
formatApproval(review.summary, "Automatic approval is disabled."),
|
|
193
214
|
);
|
|
194
215
|
} catch (error) {
|
|
216
|
+
if (ctx.signal?.aborted) return block("Tool review cancelled");
|
|
195
217
|
const message = singleLine(errorText(error));
|
|
196
218
|
ctx.ui.notify(`Tool review failed; manual approval required: ${truncAt(message, 600)}`, "warning");
|
|
197
219
|
return requestToolApproval(
|
|
@@ -206,6 +228,10 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
206
228
|
}
|
|
207
229
|
});
|
|
208
230
|
|
|
231
|
+
pi.on("turn_end", () => {
|
|
232
|
+
batchReviews = undefined;
|
|
233
|
+
});
|
|
234
|
+
|
|
209
235
|
pi.on("tool_result", (event) => {
|
|
210
236
|
const note = pendingNotes.get(event.toolCallId);
|
|
211
237
|
if (!note) return;
|
|
@@ -222,10 +248,12 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
222
248
|
});
|
|
223
249
|
|
|
224
250
|
pi.on("agent_end", () => {
|
|
251
|
+
batchReviews = undefined;
|
|
225
252
|
pendingNotes.clear();
|
|
226
253
|
});
|
|
227
254
|
|
|
228
255
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
256
|
+
batchReviews = undefined;
|
|
229
257
|
pendingNotes.clear();
|
|
230
258
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
231
259
|
});
|
|
@@ -252,10 +280,49 @@ function autoApprovedMarker(value: unknown): AutoApprovedMarker | undefined {
|
|
|
252
280
|
return { toolName: record.toolName, provider: record.provider, model: record.model };
|
|
253
281
|
}
|
|
254
282
|
|
|
255
|
-
async function
|
|
283
|
+
async function reviewAssistantRequests(
|
|
256
284
|
ctx: ExtensionContext,
|
|
257
|
-
|
|
258
|
-
|
|
285
|
+
event: ToolCallEvent,
|
|
286
|
+
currentRequest: ToolApprovalRequest,
|
|
287
|
+
): Promise<Map<string, CachedReview>> {
|
|
288
|
+
const reviews = new Map<string, CachedReview>();
|
|
289
|
+
const assistant = ctx.sessionManager
|
|
290
|
+
.getBranch()
|
|
291
|
+
.reverse()
|
|
292
|
+
.find((entry) => entry.type === "message" && entry.message.role === "assistant");
|
|
293
|
+
if (!assistant || assistant.type !== "message" || assistant.message.role !== "assistant") return reviews;
|
|
294
|
+
if (
|
|
295
|
+
!assistant.message.content.some(
|
|
296
|
+
(part) => part.type === "toolCall" && part.id === event.toolCallId && part.name === currentRequest.toolName,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
return reviews;
|
|
300
|
+
|
|
301
|
+
const requests = assistant.message.content.flatMap((part) => {
|
|
302
|
+
if (part.type !== "toolCall" || (part.name !== "bash" && part.name !== "script_runner")) return [];
|
|
303
|
+
const request: ToolApprovalRequest = {
|
|
304
|
+
toolName: part.name,
|
|
305
|
+
input: part.id === event.toolCallId ? currentRequest.input : part.arguments,
|
|
306
|
+
};
|
|
307
|
+
if (request.toolName === "bash") {
|
|
308
|
+
const command = request.input.command;
|
|
309
|
+
if (typeof command !== "string" || !command.trim() || isAllowlistedBash(command)) return [];
|
|
310
|
+
}
|
|
311
|
+
return [{ toolCallId: part.id, request, requestJson: JSON.stringify(request) }];
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
for (let index = 0; index < requests.length && !ctx.signal?.aborted; index += MAX_CONCURRENT_REVIEWS) {
|
|
315
|
+
const group = requests.slice(index, index + MAX_CONCURRENT_REVIEWS);
|
|
316
|
+
const outcomes = await Promise.allSettled(group.map((item) => reviewToolRequest(ctx, item.request)));
|
|
317
|
+
for (const [offset, item] of group.entries()) {
|
|
318
|
+
const outcome = outcomes[offset];
|
|
319
|
+
if (outcome) reviews.set(item.toolCallId, { requestJson: item.requestJson, outcome });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return reviews;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function reviewToolRequest(ctx: ExtensionContext, request: ToolApprovalRequest): Promise<ToolReviewResult> {
|
|
259
326
|
const requestJson = JSON.stringify(request);
|
|
260
327
|
const reviewer = REVIEW_MODELS.find((item) => item.provider === ctx.model?.provider);
|
|
261
328
|
const preferred = reviewer ? [reviewer] : [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shanepadgett/tau-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.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,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@ast-grep/wasm": "0.45.3",
|
|
38
|
-
"@shanepadgett/tau-tui": "0.
|
|
38
|
+
"@shanepadgett/tau-tui": "0.44.0",
|
|
39
39
|
"@vscode/tree-sitter-wasm": "0.3.1",
|
|
40
40
|
"image-size": "2.0.4",
|
|
41
41
|
"smol-toml": "1.8.0",
|