@shanepadgett/tau-agent 0.42.3 → 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.
- package/README.md +0 -1
- package/docs/context.md +1 -1
- package/docs/extending-tau-agent.md +1 -2
- package/extensions/attention/README.md +1 -1
- package/extensions/cache-diagnostics/index.ts +4 -0
- package/extensions/context/README.md +1 -24
- package/extensions/context/definitions.ts +1 -52
- package/extensions/context/index.ts +1 -150
- package/extensions/context/panel.ts +0 -72
- package/extensions/explore/guidance.ts +9 -1
- package/extensions/footer/README.md +1 -1
- package/extensions/footer/index.ts +3 -25
- package/extensions/qna/choice-question-body.ts +7 -2
- package/extensions/run-summary/README.md +1 -1
- package/extensions/run-summary/index.ts +18 -25
- package/extensions/runtime-context/README.md +1 -1
- package/extensions/runtime-context/context.ts +1 -1
- package/extensions/runtime-context/index.ts +10 -16
- package/extensions/script-runner/index.ts +21 -29
- package/extensions/silent-command-runner/README.md +0 -2
- package/extensions/silent-command-runner/index.ts +12 -41
- package/extensions/soul/README.md +5 -8
- package/extensions/soul/context.ts +115 -0
- package/extensions/soul/index.ts +141 -7
- package/extensions/soul/prompt.ts +47 -102
- package/extensions/soul/state.ts +114 -0
- package/extensions/soul/tools.ts +31 -0
- package/extensions/tau-help/help.md +6 -18
- package/extensions/tau-help/index.ts +10 -4
- package/extensions/tool-approval/README.md +4 -2
- package/extensions/tool-approval/index.ts +141 -53
- package/extensions/tool-approval/panel.ts +153 -0
- package/extensions/tool-loader/README.md +1 -1
- package/extensions/tool-loader/index.ts +91 -22
- package/package.json +2 -2
- package/schemas/tau.schema.json +0 -104
- package/shared/bounded-text-result.ts +0 -1
- package/shared/events.ts +19 -15
- package/shared/isolated-session.ts +1 -2
- package/shared/model-effort.ts +15 -21
- package/shared/prompt-contributions.ts +24 -0
- package/src/index.ts +1 -1
- package/docs/subagents.md +0 -92
- package/extensions/auto-compact/README.md +0 -9
- package/extensions/auto-compact/index.ts +0 -122
- package/extensions/auto-compact/settings.ts +0 -30
- package/extensions/context/settings.ts +0 -62
- package/extensions/context/sync.ts +0 -276
- package/extensions/context/validation.ts +0 -88
- package/extensions/effort/README.md +0 -7
- package/extensions/effort/index.ts +0 -134
- package/extensions/effort/state.ts +0 -18
- package/extensions/qna/inline-editor-row.ts +0 -56
- package/extensions/soul/overseer.ts +0 -273
- package/extensions/soul/settings.ts +0 -34
- package/extensions/subagent/README.md +0 -67
- package/extensions/subagent/agents/context-sync.md +0 -244
- package/extensions/subagent/agents/dormant/generalist.md +0 -33
- package/extensions/subagent/agents/scout.md +0 -104
- package/extensions/subagent/agents/web-research.md +0 -101
- package/extensions/subagent/agents.ts +0 -261
- package/extensions/subagent/cmux-dashboard.ts +0 -495
- package/extensions/subagent/index.ts +0 -425
- package/extensions/subagent/panel.ts +0 -124
- package/extensions/subagent/render.ts +0 -74
- package/extensions/subagent/resume.ts +0 -78
- package/extensions/subagent/run.ts +0 -641
- package/extensions/subagent/runtime.ts +0 -1296
- package/extensions/subagent/session-resource.ts +0 -61
- package/extensions/subagent/settings.ts +0 -18
|
@@ -12,10 +12,12 @@ import { generateToolValidated, resolveCandidates } from "../../shared/model-fal
|
|
|
12
12
|
import { errorText, truncAt } from "../../shared/text.ts";
|
|
13
13
|
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
14
14
|
import { isAllowlistedBash } from "./allowlist.ts";
|
|
15
|
+
import { ToolApprovalPanel, type ApprovalAnswer } from "./panel.ts";
|
|
15
16
|
import toolApprovalSettings from "./settings.ts";
|
|
16
17
|
|
|
17
18
|
const STATUS_KEY = "tool-approval";
|
|
18
19
|
const AUTO_APPROVED_TYPE = "tau.tool-approval.auto-approved";
|
|
20
|
+
const MAX_CONCURRENT_REVIEWS = 3;
|
|
19
21
|
|
|
20
22
|
const REVIEW_SCHEMA = Type.Object(
|
|
21
23
|
{
|
|
@@ -56,12 +58,12 @@ const REVIEW_TOOL = {
|
|
|
56
58
|
} satisfies Tool;
|
|
57
59
|
|
|
58
60
|
const REVIEW_MODELS: ReadonlyArray<{ provider: string; model: string; reasoning: ThinkingLevel }> = [
|
|
59
|
-
{ provider: "openai", model: "gpt-
|
|
60
|
-
{ provider: "openai-codex", model: "gpt-
|
|
61
|
+
{ provider: "openai", model: "gpt-6-luna", reasoning: "medium" },
|
|
62
|
+
{ provider: "openai-codex", model: "gpt-6-luna", reasoning: "medium" },
|
|
61
63
|
{ provider: "anthropic", model: "claude-sonnet-5", reasoning: "medium" },
|
|
62
64
|
{ provider: "xai", model: "grok-4.5", reasoning: "low" },
|
|
63
|
-
{ provider: "openrouter", model: "deepseek/deepseek-v4.1-flash", reasoning: "
|
|
64
|
-
{ provider: "opencode-go", model: "deepseek-v4.1-flash", reasoning: "
|
|
65
|
+
{ provider: "openrouter", model: "deepseek/deepseek-v4.1-flash", reasoning: "high" },
|
|
66
|
+
{ provider: "opencode-go", model: "deepseek-v4.1-flash", reasoning: "high" },
|
|
65
67
|
];
|
|
66
68
|
|
|
67
69
|
type ToolReview =
|
|
@@ -74,6 +76,13 @@ interface ToolApprovalRequest {
|
|
|
74
76
|
input: Record<string, unknown>;
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
type ToolReviewResult = { review: ToolReview; provider: string; model: string };
|
|
80
|
+
|
|
81
|
+
interface CachedReview {
|
|
82
|
+
requestJson: string;
|
|
83
|
+
outcome: PromiseSettledResult<ToolReviewResult>;
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
interface AutoApprovedMarker {
|
|
78
87
|
toolName: ApprovalToolName;
|
|
79
88
|
provider: string;
|
|
@@ -82,6 +91,8 @@ interface AutoApprovedMarker {
|
|
|
82
91
|
|
|
83
92
|
export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
84
93
|
let settings = toolApprovalSettings.defaults;
|
|
94
|
+
let batchReviews: Map<string, CachedReview> | undefined;
|
|
95
|
+
const pendingNotes = new Map<string, string>();
|
|
85
96
|
|
|
86
97
|
pi.registerEntryRenderer<AutoApprovedMarker>(AUTO_APPROVED_TYPE, (entry, _options, theme) => {
|
|
87
98
|
const marker = autoApprovedMarker(entry.data);
|
|
@@ -98,22 +109,45 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
98
109
|
settings = await loadTauExtensionSettings(ctx, toolApprovalSettings);
|
|
99
110
|
}
|
|
100
111
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
112
|
+
async function requestToolApproval(
|
|
113
|
+
ctx: ExtensionContext,
|
|
114
|
+
toolCallId: string,
|
|
115
|
+
toolName: ApprovalToolName,
|
|
116
|
+
title: string,
|
|
117
|
+
body: string,
|
|
118
|
+
): Promise<{ block: true; reason: string } | undefined> {
|
|
119
|
+
if (!ctx.hasUI) return block(`${toolLabel(toolName)} needs confirmation, but interactive UI is unavailable`);
|
|
120
|
+
try {
|
|
121
|
+
emitAgentBlocked(pi, {
|
|
122
|
+
title: "Tool request review",
|
|
123
|
+
body: `Waiting for ${toolLabel(toolName)} approval`,
|
|
124
|
+
source: "tool-approval.review",
|
|
125
|
+
});
|
|
126
|
+
if (ctx.mode !== "tui") {
|
|
127
|
+
const confirmed = await ctx.ui.confirm(title, body);
|
|
128
|
+
return confirmed ? undefined : block(`${toolLabel(toolName)} rejected by user`);
|
|
129
|
+
}
|
|
130
|
+
const answer = await ctx.ui.custom<ApprovalAnswer | undefined>(
|
|
131
|
+
(tui, theme, keys, done) => new ToolApprovalPanel(tui, theme, keys, title, body, done),
|
|
132
|
+
);
|
|
133
|
+
if (!answer) return block(`${toolLabel(toolName)} approval cancelled by user`);
|
|
134
|
+
const note = truncAt(answer.note, 800);
|
|
135
|
+
if (answer.choice === "reject") {
|
|
136
|
+
return block(`${toolLabel(toolName)} rejected by user${note ? `. User note: ${note}` : ""}`);
|
|
137
|
+
}
|
|
138
|
+
if (note) pendingNotes.set(toolCallId, note);
|
|
139
|
+
return undefined;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const message = singleLine(errorText(error));
|
|
142
|
+
ctx.ui.notify(`Tool approval failed; request blocked: ${truncAt(message, 600)}`, "error");
|
|
143
|
+
return block(`tool approval failed: ${truncAt(message, 600)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
104
146
|
|
|
105
|
-
pi.on("
|
|
147
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
148
|
+
batchReviews = undefined;
|
|
149
|
+
pendingNotes.clear();
|
|
106
150
|
await refreshSettings(ctx);
|
|
107
|
-
if (!settings.enabled) return undefined;
|
|
108
|
-
return {
|
|
109
|
-
systemPrompt: `${event.systemPrompt}\n\n${[
|
|
110
|
-
"Known-safe read-only bash commands skip review.",
|
|
111
|
-
"Other bash and every script_runner request are reviewed by a separate safety classifier before execution.",
|
|
112
|
-
"Treat classifier approval as a gate, not as permission to hide command intent from the user.",
|
|
113
|
-
"Routine local development requests can be approved automatically.",
|
|
114
|
-
"Requests with destructive, system, production, privileged, or security-sensitive effects require human confirmation.",
|
|
115
|
-
].join("\n")}`,
|
|
116
|
-
};
|
|
117
151
|
});
|
|
118
152
|
|
|
119
153
|
pi.on("tool_call", async (event, ctx) => {
|
|
@@ -128,25 +162,36 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
128
162
|
}
|
|
129
163
|
if (!settings.enabled) return undefined;
|
|
130
164
|
|
|
131
|
-
let command: string | undefined;
|
|
132
165
|
if (request.toolName === "bash") {
|
|
133
|
-
const
|
|
134
|
-
if (typeof
|
|
135
|
-
if (!
|
|
166
|
+
const command = request.input.command;
|
|
167
|
+
if (typeof command !== "string") return block("bash command was malformed");
|
|
168
|
+
if (!command.trim()) {
|
|
136
169
|
ctx.ui.notify("Bash command blocked: command is empty", "warning");
|
|
137
170
|
return block("bash command is empty");
|
|
138
171
|
}
|
|
139
|
-
command = value;
|
|
140
172
|
if (isAllowlistedBash(command)) return undefined;
|
|
141
173
|
}
|
|
142
174
|
|
|
143
175
|
ctx.ui.setStatus(STATUS_KEY, `reviewing ${toolLabel(request.toolName)}`);
|
|
144
176
|
try {
|
|
145
|
-
|
|
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;
|
|
146
191
|
if (review.decision === "requires_user_approval") {
|
|
147
192
|
return requestToolApproval(
|
|
148
|
-
pi,
|
|
149
193
|
ctx,
|
|
194
|
+
event.toolCallId,
|
|
150
195
|
request.toolName,
|
|
151
196
|
`Approve high-impact ${toolLabel(request.toolName)}?`,
|
|
152
197
|
formatApproval(review.summary, review.reason),
|
|
@@ -161,18 +206,19 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
161
206
|
return undefined;
|
|
162
207
|
}
|
|
163
208
|
return requestToolApproval(
|
|
164
|
-
pi,
|
|
165
209
|
ctx,
|
|
210
|
+
event.toolCallId,
|
|
166
211
|
request.toolName,
|
|
167
212
|
`Run reviewed ${toolLabel(request.toolName)}?`,
|
|
168
213
|
formatApproval(review.summary, "Automatic approval is disabled."),
|
|
169
214
|
);
|
|
170
215
|
} catch (error) {
|
|
216
|
+
if (ctx.signal?.aborted) return block("Tool review cancelled");
|
|
171
217
|
const message = singleLine(errorText(error));
|
|
172
218
|
ctx.ui.notify(`Tool review failed; manual approval required: ${truncAt(message, 600)}`, "warning");
|
|
173
219
|
return requestToolApproval(
|
|
174
|
-
pi,
|
|
175
220
|
ctx,
|
|
221
|
+
event.toolCallId,
|
|
176
222
|
request.toolName,
|
|
177
223
|
`Automatic ${toolLabel(request.toolName)} review failed. Continue?`,
|
|
178
224
|
`The automatic review failed, so Tau could not summarize this ${toolLabel(request.toolName)}. Approve it only if you understand the request shown above.`,
|
|
@@ -182,7 +228,33 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
182
228
|
}
|
|
183
229
|
});
|
|
184
230
|
|
|
231
|
+
pi.on("turn_end", () => {
|
|
232
|
+
batchReviews = undefined;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
pi.on("tool_result", (event) => {
|
|
236
|
+
const note = pendingNotes.get(event.toolCallId);
|
|
237
|
+
if (!note) return;
|
|
238
|
+
pendingNotes.delete(event.toolCallId);
|
|
239
|
+
return {
|
|
240
|
+
content: [
|
|
241
|
+
...event.content,
|
|
242
|
+
{
|
|
243
|
+
type: "text" as const,
|
|
244
|
+
text: `User approval note (guidance for subsequent actions; does not change this tool request):\n${note}`,
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
pi.on("agent_end", () => {
|
|
251
|
+
batchReviews = undefined;
|
|
252
|
+
pendingNotes.clear();
|
|
253
|
+
});
|
|
254
|
+
|
|
185
255
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
256
|
+
batchReviews = undefined;
|
|
257
|
+
pendingNotes.clear();
|
|
186
258
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
187
259
|
});
|
|
188
260
|
}
|
|
@@ -208,10 +280,49 @@ function autoApprovedMarker(value: unknown): AutoApprovedMarker | undefined {
|
|
|
208
280
|
return { toolName: record.toolName, provider: record.provider, model: record.model };
|
|
209
281
|
}
|
|
210
282
|
|
|
211
|
-
async function
|
|
283
|
+
async function reviewAssistantRequests(
|
|
212
284
|
ctx: ExtensionContext,
|
|
213
|
-
|
|
214
|
-
|
|
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> {
|
|
215
326
|
const requestJson = JSON.stringify(request);
|
|
216
327
|
const reviewer = REVIEW_MODELS.find((item) => item.provider === ctx.model?.provider);
|
|
217
328
|
const preferred = reviewer ? [reviewer] : [];
|
|
@@ -269,29 +380,6 @@ function formatApproval(summary: string, reason: string): string {
|
|
|
269
380
|
return singleLine(`${summary} ${reason}`);
|
|
270
381
|
}
|
|
271
382
|
|
|
272
|
-
async function requestToolApproval(
|
|
273
|
-
pi: Pick<ExtensionAPI, "events">,
|
|
274
|
-
ctx: ExtensionContext,
|
|
275
|
-
toolName: ApprovalToolName,
|
|
276
|
-
title: string,
|
|
277
|
-
body: string,
|
|
278
|
-
): Promise<{ block: true; reason: string } | undefined> {
|
|
279
|
-
if (!ctx.hasUI) return block(`${toolLabel(toolName)} needs confirmation, but interactive UI is unavailable`);
|
|
280
|
-
try {
|
|
281
|
-
emitAgentBlocked(pi, {
|
|
282
|
-
title: "Tool request review",
|
|
283
|
-
body: `Waiting for ${toolLabel(toolName)} approval`,
|
|
284
|
-
source: "tool-approval.review",
|
|
285
|
-
});
|
|
286
|
-
const confirmed = await ctx.ui.confirm(title, body);
|
|
287
|
-
return confirmed ? undefined : block(`${toolLabel(toolName)} rejected by user`);
|
|
288
|
-
} catch (error) {
|
|
289
|
-
const message = singleLine(errorText(error));
|
|
290
|
-
ctx.ui.notify(`Tool approval failed; request blocked: ${truncAt(message, 600)}`, "error");
|
|
291
|
-
return block(`tool approval failed: ${truncAt(message, 600)}`);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
383
|
function singleLine(text: string): string {
|
|
296
384
|
return text.replaceAll(/\s+/g, " ").trim();
|
|
297
385
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
type Component,
|
|
4
|
+
Editor,
|
|
5
|
+
type Focusable,
|
|
6
|
+
Key,
|
|
7
|
+
type KeybindingsManager,
|
|
8
|
+
matchesKey,
|
|
9
|
+
type TUI,
|
|
10
|
+
} from "@earendil-works/pi-tui";
|
|
11
|
+
import {
|
|
12
|
+
bindingHint,
|
|
13
|
+
bindingsHint,
|
|
14
|
+
editorTheme,
|
|
15
|
+
pushSavedNote,
|
|
16
|
+
rawHint,
|
|
17
|
+
renderNoteEditor,
|
|
18
|
+
ToolPanel,
|
|
19
|
+
type ToolPanelConfig,
|
|
20
|
+
wrapWithPrefix,
|
|
21
|
+
} from "@shanepadgett/tau-tui";
|
|
22
|
+
|
|
23
|
+
export type ApprovalChoice = "approve" | "reject";
|
|
24
|
+
export interface ApprovalAnswer {
|
|
25
|
+
choice: ApprovalChoice;
|
|
26
|
+
note: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ToolApprovalPanel implements Component, Focusable {
|
|
30
|
+
private readonly tui: TUI;
|
|
31
|
+
private readonly theme: Theme;
|
|
32
|
+
private readonly keys: KeybindingsManager;
|
|
33
|
+
private readonly done: (answer: ApprovalAnswer | undefined) => void;
|
|
34
|
+
private readonly noteEditor: Editor;
|
|
35
|
+
private readonly panelConfig: ToolPanelConfig;
|
|
36
|
+
private readonly panel: ToolPanel;
|
|
37
|
+
private readonly notes: Record<ApprovalChoice, string> = { approve: "", reject: "" };
|
|
38
|
+
private choice: ApprovalChoice = "approve";
|
|
39
|
+
private editing = false;
|
|
40
|
+
private _focused = false;
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
tui: TUI,
|
|
44
|
+
theme: Theme,
|
|
45
|
+
keys: KeybindingsManager,
|
|
46
|
+
title: string,
|
|
47
|
+
body: string,
|
|
48
|
+
done: (answer: ApprovalAnswer | undefined) => void,
|
|
49
|
+
) {
|
|
50
|
+
this.tui = tui;
|
|
51
|
+
this.theme = theme;
|
|
52
|
+
this.keys = keys;
|
|
53
|
+
this.done = done;
|
|
54
|
+
this.noteEditor = new Editor(tui, editorTheme(theme));
|
|
55
|
+
this.noteEditor.onSubmit = (value) => {
|
|
56
|
+
this.notes[this.choice] = value.trim();
|
|
57
|
+
this.closeNote();
|
|
58
|
+
};
|
|
59
|
+
this.panelConfig = {
|
|
60
|
+
title,
|
|
61
|
+
secondary: "Approve runs this request as shown. To change it, reject with a note.",
|
|
62
|
+
header: [body],
|
|
63
|
+
body: { render: (width) => this.renderChoices(width), invalidate: () => {} },
|
|
64
|
+
footer: { kind: "hints", hints: this.hints() },
|
|
65
|
+
};
|
|
66
|
+
this.panel = new ToolPanel(theme, this.panelConfig);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get focused(): boolean {
|
|
70
|
+
return this._focused;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
set focused(value: boolean) {
|
|
74
|
+
this._focused = value;
|
|
75
|
+
this.noteEditor.focused = value && this.editing;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
handleInput(data: string): void {
|
|
79
|
+
if (matchesKey(data, Key.ctrl("c"))) {
|
|
80
|
+
this.done(undefined);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (this.editing) {
|
|
84
|
+
if (this.keys.matches(data, "tui.select.cancel")) this.closeNote();
|
|
85
|
+
else this.noteEditor.handleInput(data);
|
|
86
|
+
this.refresh();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (this.keys.matches(data, "tui.select.cancel")) {
|
|
90
|
+
this.done(undefined);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (this.keys.matches(data, "tui.select.up") || this.keys.matches(data, "tui.select.down")) {
|
|
94
|
+
this.choice = this.choice === "approve" ? "reject" : "approve";
|
|
95
|
+
} else if (data === "n") {
|
|
96
|
+
this.editing = true;
|
|
97
|
+
this.noteEditor.setText(this.notes[this.choice]);
|
|
98
|
+
this.focused = this._focused;
|
|
99
|
+
} else if (this.keys.matches(data, "tui.select.confirm")) {
|
|
100
|
+
this.done({ choice: this.choice, note: this.notes[this.choice] });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.refresh();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
render(width: number): string[] {
|
|
107
|
+
return this.panel.render(width);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
invalidate(): void {
|
|
111
|
+
this.panel.invalidate();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private renderChoices(width: number): string[] {
|
|
115
|
+
const lines: string[] = [];
|
|
116
|
+
for (const choice of ["approve", "reject"] as const) {
|
|
117
|
+
const selected = choice === this.choice;
|
|
118
|
+
const prefix = selected ? this.theme.fg("accent", "→ ") : " ";
|
|
119
|
+
lines.push(
|
|
120
|
+
...wrapWithPrefix(
|
|
121
|
+
prefix,
|
|
122
|
+
this.theme.fg(selected ? "accent" : "text", choice === "approve" ? "Approve" : "Reject"),
|
|
123
|
+
width,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
if (this.editing && selected) renderNoteEditor(lines, this.noteEditor, width, this.theme, " ");
|
|
127
|
+
else if (this.notes[choice]) pushSavedNote(lines, this.notes[choice], width, this.theme, " ");
|
|
128
|
+
}
|
|
129
|
+
return lines;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private closeNote(): void {
|
|
133
|
+
this.editing = false;
|
|
134
|
+
this.noteEditor.setText("");
|
|
135
|
+
this.focused = this._focused;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private hints() {
|
|
139
|
+
return this.editing
|
|
140
|
+
? [bindingHint("tui.input.submit", "save"), bindingHint("tui.select.cancel", "cancel note")]
|
|
141
|
+
: [
|
|
142
|
+
bindingsHint(["tui.select.up", "tui.select.down"], "move"),
|
|
143
|
+
bindingHint("tui.select.confirm", "choose"),
|
|
144
|
+
rawHint("n", "note"),
|
|
145
|
+
bindingHint("tui.select.cancel", "block"),
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private refresh(): void {
|
|
150
|
+
this.panelConfig.footer = { kind: "hints", hints: this.hints() };
|
|
151
|
+
this.tui.requestRender();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -10,6 +10,6 @@ The agent normally calls `load_tools` itself. Tau's built-in groups are:
|
|
|
10
10
|
|
|
11
11
|
Project and global package extensions can add groups with `registerDeferredToolGroup()` from `@shanepadgett/tau-agent`. The group description is included in the loader catalog so the agent can select it when a task needs that capability.
|
|
12
12
|
|
|
13
|
-
Supported models
|
|
13
|
+
Supported models load new groups without replacing the cached prompt prefix. If the selected model cannot do that, the group is queued for activation after successful compaction. The loader reports this and does not compact automatically.
|
|
14
14
|
|
|
15
15
|
After changing this extension during development, run `/reload` before testing.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { defineTool, type ExtensionAPI, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type Static, Type } from "typebox";
|
|
3
3
|
import { registeredDeferredToolGroups, type DeferredToolGroupInfo } from "../../src/tool-loading/index.ts";
|
|
4
|
+
import { emitTauEvent } from "../../shared/events.ts";
|
|
5
|
+
import { BoundedTextResultBuilder } from "../../shared/bounded-text-result.ts";
|
|
6
|
+
import { createTemporaryOutputStore } from "../../shared/temporary-output-store.ts";
|
|
4
7
|
|
|
5
8
|
const loadToolsSchema = Type.Object(
|
|
6
9
|
{
|
|
@@ -21,14 +24,25 @@ interface LoadToolsDetails {
|
|
|
21
24
|
addedToolNames: string[];
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
const PENDING_TYPE = "tau.tool-loader.pending";
|
|
28
|
+
const APPLIED_TYPE = "tau.tool-loader.applied";
|
|
29
|
+
|
|
30
|
+
interface AppliedLoad {
|
|
31
|
+
pendingId: string;
|
|
32
|
+
names: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
export default function toolLoaderExtension(pi: ExtensionAPI): void {
|
|
36
|
+
let temporaryOutput = createTemporaryOutputStore();
|
|
25
37
|
let managed = false;
|
|
26
38
|
let allowedToolNames = new Map<string, ReadonlySet<string>>();
|
|
27
39
|
let managedToolNames = new Set<string>();
|
|
28
40
|
|
|
29
41
|
pi.registerTool(createLoadToolsTool(pi, []));
|
|
30
42
|
|
|
31
|
-
pi.on("session_start", (_event, ctx) => {
|
|
43
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
44
|
+
temporaryOutput = createTemporaryOutputStore();
|
|
45
|
+
await temporaryOutput.start();
|
|
32
46
|
const groups = registeredDeferredToolGroups(pi);
|
|
33
47
|
pi.registerTool(createLoadToolsTool(pi, groups));
|
|
34
48
|
|
|
@@ -39,20 +53,43 @@ export default function toolLoaderExtension(pi: ExtensionAPI): void {
|
|
|
39
53
|
);
|
|
40
54
|
managedToolNames = new Set(groups.flatMap((group) => group.toolNames));
|
|
41
55
|
managed = initialSet.has("load_tools") && groups.length > 0;
|
|
42
|
-
if (managed) restoreActiveTools(pi, initial, loadedCapabilities(ctx.sessionManager.getBranch()), groups);
|
|
56
|
+
if (managed) restoreActiveTools(pi, initial, loadedCapabilities(ctx.sessionManager.getBranch(), groups), groups);
|
|
43
57
|
});
|
|
58
|
+
pi.on("session_shutdown", () => temporaryOutput.shutdown());
|
|
44
59
|
|
|
45
60
|
pi.on("session_tree", (_event, ctx) => {
|
|
46
61
|
if (managed) {
|
|
47
62
|
restoreActiveTools(
|
|
48
63
|
pi,
|
|
49
64
|
pi.getActiveTools(),
|
|
50
|
-
loadedCapabilities(ctx.sessionManager.getBranch()),
|
|
65
|
+
loadedCapabilities(ctx.sessionManager.getBranch(), registeredDeferredToolGroups(pi)),
|
|
51
66
|
registeredDeferredToolGroups(pi),
|
|
52
67
|
);
|
|
53
68
|
}
|
|
54
69
|
});
|
|
55
70
|
|
|
71
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
72
|
+
const branch = ctx.sessionManager.getBranch();
|
|
73
|
+
const applied = new Set(
|
|
74
|
+
branch.flatMap((entry) =>
|
|
75
|
+
entry.type === "custom" && entry.customType === APPLIED_TYPE ? [(entry.data as AppliedLoad).pendingId] : [],
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
for (const entry of branch) {
|
|
79
|
+
if (
|
|
80
|
+
entry.type !== "custom" ||
|
|
81
|
+
entry.customType !== PENDING_TYPE ||
|
|
82
|
+
applied.has(entry.id) ||
|
|
83
|
+
!Array.isArray(entry.data)
|
|
84
|
+
)
|
|
85
|
+
continue;
|
|
86
|
+
const names = entry.data.filter((name): name is string => typeof name === "string");
|
|
87
|
+
const allowed = new Set([...allowedToolNames.values()].flatMap((group) => [...group]));
|
|
88
|
+
pi.setActiveTools([...new Set([...pi.getActiveTools(), ...names.filter((name) => allowed.has(name))])]);
|
|
89
|
+
pi.appendEntry(APPLIED_TYPE, { pendingId: entry.id, names });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
56
93
|
function createLoadToolsTool(pi: ExtensionAPI, groups: readonly DeferredToolGroupInfo[]) {
|
|
57
94
|
return defineTool<typeof loadToolsSchema, LoadToolsDetails>({
|
|
58
95
|
name: "load_tools",
|
|
@@ -63,7 +100,7 @@ export default function toolLoaderExtension(pi: ExtensionAPI): void {
|
|
|
63
100
|
"Use load_tools before attempting a registered specialist capability whose tools are not currently available.",
|
|
64
101
|
],
|
|
65
102
|
parameters: loadToolsSchema,
|
|
66
|
-
async execute(_toolCallId, params: LoadToolsParams) {
|
|
103
|
+
async execute(_toolCallId, params: LoadToolsParams, signal, _onUpdate, ctx) {
|
|
67
104
|
const group = registeredDeferredToolGroups(pi).find((candidate) => candidate.id === params.capability);
|
|
68
105
|
if (group === undefined) {
|
|
69
106
|
throw new Error(
|
|
@@ -81,7 +118,25 @@ export default function toolLoaderExtension(pi: ExtensionAPI): void {
|
|
|
81
118
|
}
|
|
82
119
|
|
|
83
120
|
const beforeSet = new Set(before);
|
|
84
|
-
|
|
121
|
+
const next = [...before, ...loadable.filter((name) => !beforeSet.has(name))];
|
|
122
|
+
const nextSet = new Set(next);
|
|
123
|
+
let blocked: string | null = null;
|
|
124
|
+
emitTauEvent(pi, "tau:prompt.tools.check", {
|
|
125
|
+
ctx,
|
|
126
|
+
tools: pi.getAllTools().filter((tool) => nextSet.has(tool.name)),
|
|
127
|
+
reject(reason) {
|
|
128
|
+
blocked = reason;
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
if (blocked) {
|
|
132
|
+
pi.appendEntry(PENDING_TYPE, loadable);
|
|
133
|
+
return boundedResult(
|
|
134
|
+
`${blocked} ${params.capability} is queued for activation after successful compaction.`,
|
|
135
|
+
{ version: 1, capability: params.capability, requestedToolNames: requested, addedToolNames: [] },
|
|
136
|
+
signal,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
pi.setActiveTools(next);
|
|
85
140
|
const after = pi.getActiveTools();
|
|
86
141
|
const addedToolNames = requested.filter((name) => !beforeSet.has(name) && after.includes(name));
|
|
87
142
|
const available = requested.filter((name) => after.includes(name));
|
|
@@ -91,24 +146,36 @@ export default function toolLoaderExtension(pi: ExtensionAPI): void {
|
|
|
91
146
|
? `Loaded ${params.capability} tools: ${addedToolNames.join(", ")}.`
|
|
92
147
|
: `${params.capability} tools are already loaded: ${available.join(", ")}.`;
|
|
93
148
|
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
type: "text" as const,
|
|
98
|
-
text: unavailable.length ? `${text} Unavailable: ${unavailable.join(", ")}.` : text,
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
details: {
|
|
149
|
+
return boundedResult(
|
|
150
|
+
unavailable.length ? `${text} Unavailable: ${unavailable.join(", ")}.` : text,
|
|
151
|
+
{
|
|
102
152
|
version: 1,
|
|
103
153
|
capability: params.capability,
|
|
104
154
|
requestedToolNames: requested,
|
|
105
155
|
addedToolNames,
|
|
106
156
|
},
|
|
107
|
-
|
|
157
|
+
signal,
|
|
158
|
+
);
|
|
108
159
|
},
|
|
109
160
|
});
|
|
110
161
|
}
|
|
111
162
|
|
|
163
|
+
async function boundedResult(text: string, details: LoadToolsDetails, signal: AbortSignal | undefined) {
|
|
164
|
+
const builder = new BoundedTextResultBuilder(temporaryOutput, "head");
|
|
165
|
+
try {
|
|
166
|
+
await builder.append(text);
|
|
167
|
+
signal?.throwIfAborted();
|
|
168
|
+
const result = await builder.finish();
|
|
169
|
+
return {
|
|
170
|
+
content: [{ type: "text" as const, text: result.content }],
|
|
171
|
+
details: { ...details, overflow: result.overflow },
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
await builder.abort();
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
112
179
|
function restoreActiveTools(
|
|
113
180
|
pi: ExtensionAPI,
|
|
114
181
|
current: readonly string[],
|
|
@@ -131,16 +198,18 @@ function formatGroupCatalog(groups: readonly DeferredToolGroupInfo[]): string {
|
|
|
131
198
|
return ` Registered groups: ${catalog}.`;
|
|
132
199
|
}
|
|
133
200
|
|
|
134
|
-
function loadedCapabilities(entries: readonly
|
|
201
|
+
function loadedCapabilities(entries: readonly SessionEntry[], groups: readonly DeferredToolGroupInfo[]): Set<string> {
|
|
135
202
|
const loaded = new Set<string>();
|
|
136
|
-
for (const
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
if (entry.type === "custom" && entry.customType === APPLIED_TYPE) {
|
|
205
|
+
const { names } = entry.data as AppliedLoad;
|
|
206
|
+
for (const group of groups) if (group.toolNames.some((name) => names.includes(name))) loaded.add(group.id);
|
|
207
|
+
}
|
|
208
|
+
if (entry.type !== "message") continue;
|
|
209
|
+
const message = entry.message;
|
|
141
210
|
if (message.role !== "toolResult" || message.toolName !== "load_tools" || message.isError === true) continue;
|
|
142
211
|
if (!isLoadToolsDetails(message.details)) continue;
|
|
143
|
-
loaded.add(message.details.capability);
|
|
212
|
+
if (message.details.addedToolNames.length > 0) loaded.add(message.details.capability);
|
|
144
213
|
}
|
|
145
214
|
return loaded;
|
|
146
215
|
}
|
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",
|