@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 ensureTempDir();
207
- const file = join(dir, language === "python3" ? "_run.py" : "_run.ts");
208
- await writeFile(file, source, "utf8");
209
- const args =
210
- language === "python3"
211
- ? [file]
212
- : language === "node"
213
- ? ["--experimental-strip-types", file]
214
- : ["run", "-A", file];
215
- const result = await pi.exec(command, args, { cwd, signal, timeout: TIMEOUT_MS });
216
- return {
217
- ...result,
218
- stdout: scrubPath(result.stdout, file, dir),
219
- stderr: scrubPath(result.stderr, file, dir),
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 value = request.input.command;
158
- if (typeof value !== "string") return block("bash command was malformed");
159
- if (!value.trim()) {
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
- const { review, provider, model } = await reviewToolRequest(ctx, request);
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 reviewToolRequest(
283
+ async function reviewAssistantRequests(
256
284
  ctx: ExtensionContext,
257
- request: ToolApprovalRequest,
258
- ): Promise<{ review: ToolReview; provider: string; model: string }> {
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.43.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.43.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",