@shanepadgett/tau-agent 0.44.0 → 0.45.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/extending-tau-agent.md +3 -3
- package/extensions/appshot/index.ts +3 -0
- package/extensions/aside/index.ts +3 -11
- package/extensions/attention/README.md +2 -4
- package/extensions/attention/index.ts +2 -40
- package/extensions/auto-name/index.ts +5 -8
- package/extensions/cache-diagnostics/index.ts +1 -1
- package/extensions/codex-priority/README.md +7 -0
- package/extensions/codex-priority/index.ts +95 -0
- package/extensions/compaction/README.md +5 -0
- package/extensions/compaction/index.ts +47 -0
- package/extensions/cost-report/README.md +1 -1
- package/extensions/cost-report/analyze.ts +11 -60
- package/extensions/cost-report/html.ts +1 -37
- package/extensions/cost-report/types.ts +0 -9
- package/extensions/handoff/index.ts +1 -1
- package/extensions/image-gen/index.ts +1 -0
- package/extensions/review/README.md +1 -1
- package/extensions/run-summary/README.md +1 -1
- package/extensions/run-summary/index.ts +6 -24
- package/extensions/runtime-context/README.md +1 -1
- package/extensions/script-runner/README.md +3 -1
- package/extensions/script-runner/index.ts +41 -96
- package/extensions/silent-command-runner/index.ts +38 -69
- package/extensions/soul/README.md +3 -5
- package/extensions/soul/index.ts +59 -135
- package/extensions/soul/prompt.ts +2 -0
- package/extensions/tau-help/help.md +10 -2
- package/extensions/tool-approval/README.md +3 -3
- package/extensions/tool-approval/index.ts +86 -47
- package/extensions/tool-approval/panel.ts +16 -2
- package/extensions/tool-loader/README.md +5 -5
- package/extensions/tool-loader/index.ts +10 -222
- package/extensions/web/codesearch.ts +1 -0
- package/extensions/web/webfetch.ts +1 -0
- package/extensions/web/websearch.ts +1 -0
- package/package.json +2 -2
- package/shared/events.ts +7 -20
- package/shared/model-effort.ts +12 -8
- package/shared/model-fallback/index.ts +12 -29
- package/shared/model-fallback/types.ts +1 -5
- package/shared/prompt-contributions.ts +0 -2
- package/shared/script-source.ts +94 -0
- package/src/tool-loading/index.ts +5 -42
- package/extensions/soul/context.ts +0 -115
- package/extensions/soul/state.ts +0 -114
- package/extensions/soul/tools.ts +0 -31
package/extensions/soul/index.ts
CHANGED
|
@@ -1,144 +1,68 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import { readPromptValues, renderBaseline, renderUpdate } from "./context.ts";
|
|
6
|
-
import {
|
|
7
|
-
admittedTools,
|
|
8
|
-
BASELINE_TYPE,
|
|
9
|
-
projectPrompt,
|
|
10
|
-
restorePrompt,
|
|
11
|
-
UPDATE_TYPE,
|
|
12
|
-
type SavedBaseline,
|
|
13
|
-
type SavedUpdate,
|
|
14
|
-
} from "./state.ts";
|
|
15
|
-
import { toolChangeReason } from "./tools.ts";
|
|
1
|
+
import { getDocsPath, getExamplesPath, getReadmePath, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { emitTauEvent } from "../../shared/events.ts";
|
|
3
|
+
import { collectPromptSources } from "../../shared/prompt-contributions.ts";
|
|
4
|
+
import { FIXED_INSTRUCTIONS } from "./prompt.ts";
|
|
16
5
|
|
|
17
|
-
const
|
|
18
|
-
interface PrefixCheckpoint {
|
|
19
|
-
baselineEntryId: string;
|
|
20
|
-
count: number;
|
|
21
|
-
hash: string;
|
|
22
|
-
}
|
|
6
|
+
const CAPTURE_TYPE = "tau.soul.capture";
|
|
23
7
|
|
|
24
|
-
|
|
25
|
-
|
|
8
|
+
const DOCUMENTATION = `Consult Pi or Tau documentation when the request concerns their usage or extension APIs.
|
|
9
|
+
Pi documentation:
|
|
10
|
+
- Main documentation: ${getReadmePath()}
|
|
11
|
+
- Additional docs: ${getDocsPath()}
|
|
12
|
+
- Examples: ${getExamplesPath()}
|
|
13
|
+
- Resolve docs/... and examples/... under those installed paths, not the working directory.
|
|
14
|
+
- Extensions: docs/extensions.md and examples/extensions/; themes: docs/themes.md; skills: docs/skills.md; prompt templates: docs/prompt-templates.md; TUI: docs/tui.md; keybindings: docs/keybindings.md; SDK: docs/sdk.md; providers: docs/custom-provider.md; models: docs/models.md; packages: docs/packages.md; environment: docs/environment-variables.md.
|
|
15
|
+
- Read the relevant documentation and follow related Markdown references before implementing Pi integrations.`;
|
|
26
16
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
pi.on("session_shutdown", () => {
|
|
31
|
-
inputs = null;
|
|
32
|
-
});
|
|
33
|
-
pi.on("before_agent_start", (event) => {
|
|
34
|
-
// Keep the shared options reference until all contributors have finished.
|
|
35
|
-
inputs = event.systemPromptOptions;
|
|
36
|
-
});
|
|
17
|
+
export default function soulExtension(pi: ExtensionAPI): void {
|
|
18
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
19
|
+
const options = event.systemPromptOptions;
|
|
37
20
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
reject(String(error));
|
|
21
|
+
// Sources that refresh on compaction are read once per compaction epoch and saved on the branch.
|
|
22
|
+
const branch = ctx.sessionManager.getBranch();
|
|
23
|
+
let epochStart = 0;
|
|
24
|
+
branch.forEach((entry, index) => {
|
|
25
|
+
if (entry.type === "compaction") epochStart = index + 1;
|
|
26
|
+
});
|
|
27
|
+
let captured: Record<string, string> = {};
|
|
28
|
+
for (const entry of branch.slice(epochStart)) {
|
|
29
|
+
if (entry.type === "custom" && entry.customType === CAPTURE_TYPE)
|
|
30
|
+
captured = entry.data as Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
const sources = collectPromptSources(pi);
|
|
33
|
+
const missing = sources.filter((source) => source.refresh === "compaction" && !(source.key in captured));
|
|
34
|
+
if (missing.length > 0) {
|
|
35
|
+
const read = await Promise.all(missing.map(async (source) => [source.key, await source.read(ctx)] as const));
|
|
36
|
+
captured = { ...captured, ...Object.fromEntries(read) };
|
|
37
|
+
pi.appendEntry(CAPTURE_TYPE, captured);
|
|
56
38
|
}
|
|
57
|
-
});
|
|
58
39
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
const values = await readPromptValues(pi, ctx, inputs, saved === null);
|
|
81
|
-
if (!saved) {
|
|
82
|
-
pi.appendEntry<SavedBaseline>(BASELINE_TYPE, {
|
|
83
|
-
version: 1,
|
|
84
|
-
compactionId: newestFirst.find((entry) => entry.type === "compaction")?.id ?? null,
|
|
85
|
-
afterEntryId: anchor.sourceEntry.id,
|
|
86
|
-
text: renderBaseline(inputs, values),
|
|
87
|
-
values,
|
|
88
|
-
initialTools: getCurrentTools(event.messages),
|
|
89
|
-
});
|
|
90
|
-
} else {
|
|
91
|
-
const previous = new Map(saved.baseline.values.map((value) => [value.key, value]));
|
|
92
|
-
for (const update of saved.updates) for (const value of update.data.values) previous.set(value.key, value);
|
|
93
|
-
const currentKeys = new Set(values.map((value) => value.key));
|
|
94
|
-
for (const value of previous.values()) {
|
|
95
|
-
if (value.refresh === "append" && !currentKeys.has(value.key)) values.push({ ...value, text: "" });
|
|
96
|
-
}
|
|
97
|
-
const changed = values.filter((value) => previous.get(value.key)?.text !== value.text);
|
|
98
|
-
const tools = new Map(admittedTools(saved).map((tool) => [tool.name, tool]));
|
|
99
|
-
const added = requested.some((tool) => !tools.has(tool.name));
|
|
100
|
-
for (const tool of requested) tools.set(tool.name, tool);
|
|
101
|
-
if (changed.length > 0 || added)
|
|
102
|
-
pi.appendEntry<SavedUpdate>(UPDATE_TYPE, {
|
|
103
|
-
version: 1,
|
|
104
|
-
baselineEntryId: saved.entryId,
|
|
105
|
-
afterEntryId: anchor.sourceEntry.id,
|
|
106
|
-
text: renderUpdate(changed),
|
|
107
|
-
values: changed,
|
|
108
|
-
tools: [...tools.values()],
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
const admitted = restorePrompt(ctx.sessionManager.getBranch());
|
|
112
|
-
if (!admitted) throw new Error("Soul failed to save its baseline.");
|
|
113
|
-
const messages = projectPrompt(event.messages, admitted, ctx);
|
|
114
|
-
const checkpointEntry = newestFirst.find(
|
|
115
|
-
(entry) => entry.type === "custom" && entry.customType === CHECKPOINT_TYPE,
|
|
116
|
-
);
|
|
117
|
-
const checkpoint = checkpointEntry?.type === "custom" ? (checkpointEntry.data as PrefixCheckpoint) : null;
|
|
118
|
-
if (checkpoint?.baselineEntryId === admitted.entryId) {
|
|
119
|
-
const prefix = createHash("sha256")
|
|
120
|
-
.update(JSON.stringify(messages.slice(0, checkpoint.count)))
|
|
121
|
-
.digest("hex");
|
|
122
|
-
if (prefix !== checkpoint.hash)
|
|
123
|
-
throw new Error("Previously sent conversation content changed. Compact before continuing.");
|
|
124
|
-
}
|
|
125
|
-
const hash = createHash("sha256").update(JSON.stringify(messages)).digest("hex");
|
|
126
|
-
if (hash !== checkpoint?.hash)
|
|
127
|
-
pi.appendEntry<PrefixCheckpoint>(CHECKPOINT_TYPE, {
|
|
128
|
-
baselineEntryId: admitted.entryId,
|
|
129
|
-
count: messages.length,
|
|
130
|
-
hash,
|
|
131
|
-
});
|
|
132
|
-
emitTauEvent(pi, "tau:prompt.snapshot", {
|
|
133
|
-
text: [admitted.baseline.text, ...admitted.updates.map((update) => update.data.text)].join("\n\n"),
|
|
134
|
-
});
|
|
135
|
-
return { messages };
|
|
136
|
-
} catch (error) {
|
|
137
|
-
ctx.ui.notify(`Soul stopped this request: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
138
|
-
ctx.abort();
|
|
139
|
-
// Pi currently enters the provider with an aborted signal; do not weaken that
|
|
140
|
-
// cancellation into a fallback prompt. See soul-request-check-findings.md.
|
|
141
|
-
return undefined;
|
|
40
|
+
const active = pi.getActiveTools();
|
|
41
|
+
const guidance = [
|
|
42
|
+
...new Set([
|
|
43
|
+
...(active.includes("bash") ? ["Use bash for file operations like ls, rg, find."] : []),
|
|
44
|
+
...active.flatMap((name) => options.toolGuidelines[name] ?? []),
|
|
45
|
+
...options.promptGuidelines,
|
|
46
|
+
]),
|
|
47
|
+
]
|
|
48
|
+
.map((rule) => `- ${rule}`)
|
|
49
|
+
.join("\n");
|
|
50
|
+
|
|
51
|
+
const sections = new Map<string, string[]>();
|
|
52
|
+
const add = (section: string, text: string) => {
|
|
53
|
+
if (text.trim()) sections.set(section, [...(sections.get(section) ?? []), text]);
|
|
54
|
+
};
|
|
55
|
+
add("documentation", DOCUMENTATION);
|
|
56
|
+
add("tool-guidance", guidance);
|
|
57
|
+
if (options.customPrompt) add("additional-instructions", options.customPrompt);
|
|
58
|
+
for (const source of sources) {
|
|
59
|
+
add(source.section, source.refresh === "append" ? await source.read(ctx) : (captured[source.key] ?? ""));
|
|
142
60
|
}
|
|
61
|
+
|
|
62
|
+
// Pi diffs these sections against the prompt already in the transcript and appends a patch only for
|
|
63
|
+
// sections whose text changed, so unchanged sections keep the cached prefix.
|
|
64
|
+
options.customPrompt = FIXED_INSTRUCTIONS;
|
|
65
|
+
for (const name of [...sections.keys()].sort()) options.sections[name] = (sections.get(name) ?? []).join("\n\n");
|
|
66
|
+
emitTauEvent(pi, "tau:prompt.snapshot", { text: event.systemPrompt });
|
|
143
67
|
});
|
|
144
68
|
}
|
|
@@ -9,6 +9,8 @@ Use headings or tables when they improve clarity. In conversational, personal, o
|
|
|
9
9
|
Use technical terms when they help. Keep paths, commands, API names, and errors exact.
|
|
10
10
|
State the intended action directly. Avoid adding what you won't do, what will remain unchanged, or how you'll separate or categorize results.
|
|
11
11
|
Give useful facts instead of praise, ceremony, or commentary about following instructions.
|
|
12
|
+
Talk about the user's work, not the machinery directing your behavior. Do not volunteer commentary about system prompts, tool prompts, internal instructions, the harness, or automatic validation. Discuss those mechanisms only when the user asks about them as the subject of the work.
|
|
13
|
+
Finish with the concrete result. Do not hedge completion because silent validation is pending, announce that checks will run or rerun, or explain what happens when you end the turn. When a failure is reported, fix it and describe the correction without narrating the validation process.
|
|
12
14
|
You are a partner, and the user expects you to act like one.
|
|
13
15
|
</communication>
|
|
14
16
|
|
|
@@ -32,6 +32,10 @@ Records private prompt-cache fingerprints without storing prompt content. Run `/
|
|
|
32
32
|
|
|
33
33
|
Adds `/clear-screen` to clear terminal output without changing the session.
|
|
34
34
|
|
|
35
|
+
## codex-priority
|
|
36
|
+
|
|
37
|
+
Requests Codex priority processing (Fast mode) for every `gpt-6-luna` request on the `openai-codex` provider, including the agent loop, compaction, tool approval, auto-naming, commit, and handoff. It has no command or setting. Cost uses OpenAI's 2.5x Fast mode rate for GPT-6 models. Do not run another extension that overlays `openai-codex` or sets `service_tier`.
|
|
38
|
+
|
|
35
39
|
## commit
|
|
36
40
|
|
|
37
41
|
Adds `/commit` for semantic commit grouping, review, and committing selected repository changes.
|
|
@@ -40,6 +44,10 @@ Adds `/commit` for semantic commit grouping, review, and committing selected rep
|
|
|
40
44
|
|
|
41
45
|
Adds `/cost-report` to build an HTML spend report from local session usage. Pick a time frame (past 7 days, current week, current month, year to date, or a specific month) and scope (current project or all sessions). Tau scans sessions, writes under `~/.pi/tau/cost-reports/`, opens the file, and notifies with the path. Empty windows warn without writing a file.
|
|
42
46
|
|
|
47
|
+
## compaction
|
|
48
|
+
|
|
49
|
+
Writes compaction summaries (automatic, `/compact`, and overflow recovery) with a cheaper model from the same provider as the session, so Opus, GPT-6.1 Sol, and Astra sessions do not pay their own rates to summarize themselves. Summaries never cross providers, and a notice names the model that wrote each one. When no cheaper model fits, Pi's default compaction runs.
|
|
50
|
+
|
|
43
51
|
## context
|
|
44
52
|
|
|
45
53
|
Adds `/context` to browse and inject reusable repository work scopes from `.pi/contexts`. Selecting entries injects them once into the conversation: `read` paths as complete files, `show` targets as current declaration slices, `outline` paths as Explore structures, and one hidden note listing `references` plus instructions to treat the injected material as current. Run `/context` again to inject more. Edit catalog files by hand when work scopes change. Domain folders are `NN_slug` tabs (ordered by the two-digit prefix; UI shows the slug), TOML files are concepts, and TOML sections are selectable entries.
|
|
@@ -106,7 +114,7 @@ Runs configured commands while keeping their output out of agent context when th
|
|
|
106
114
|
|
|
107
115
|
## soul
|
|
108
116
|
|
|
109
|
-
Supplies Tau's communication, discussion, planning, execution, and coding instructions
|
|
117
|
+
Supplies Tau's communication, discussion, planning, execution, and coding instructions, plus tool guidance and context from other Tau extensions. The date and directory snapshot stay fixed until successful compaction. Other changes, such as an edited `AGENTS.md` after `/reload`, arrive as appended updates without rewriting earlier instructions.
|
|
110
118
|
|
|
111
119
|
## stash
|
|
112
120
|
|
|
@@ -126,7 +134,7 @@ Reviews agent `bash` and `script_runner` requests before they run. Common read-o
|
|
|
126
134
|
|
|
127
135
|
## tool-loader
|
|
128
136
|
|
|
129
|
-
|
|
137
|
+
Keeps specialist tool groups out of every request as deferred tools and lets the agent load them with Pi's `tool_search`. Tau registers `web`, `image`, and `appshot`; project or global package extensions can add groups with `registerDeferredToolGroup()` from `@shanepadgett/tau-agent`. All models can load tools. Compatible models preserve the cached prefix; other models can incur a cache miss when tools are activated. Loading never triggers compaction.
|
|
130
138
|
|
|
131
139
|
## web
|
|
132
140
|
|
|
@@ -2,13 +2,13 @@
|
|
|
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. 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.
|
|
5
|
+
Common read-only bash commands skip review and run immediately. Other bash and every `script_runner` request go to a separate reviewer. For a `script_runner` retry with `scriptId` and edits, Tau reconstructs the full resulting script before review and runs that exact source after approval. A retry with missing or invalid stored source is blocked. 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 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.
|
|
9
|
+
When approval is required, Tau shows one paragraph that explains the effect and risk without repeating the request. For `script_runner`, human confirmation also shows the complete script that will run. 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
|
-
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.
|
|
11
|
+
In the terminal approval panel, move between Approve and Reject, press `j` or `k` to scroll a script, 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
|
|
|
13
13
|
Configure under `extensions.toolApproval`:
|
|
14
14
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Tool } from "@earendil-works/pi-ai";
|
|
2
2
|
import {
|
|
3
3
|
isToolCallEventType,
|
|
4
4
|
type ExtensionAPI,
|
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
import { Marker } from "@shanepadgett/tau-tui";
|
|
9
9
|
import { Type } from "typebox";
|
|
10
10
|
import { emitAgentBlocked } from "../../shared/agent-blocked.ts";
|
|
11
|
-
import {
|
|
11
|
+
import { emitTauEvent } from "../../shared/events.ts";
|
|
12
|
+
import { generateToolValidated } from "../../shared/model-fallback/index.ts";
|
|
13
|
+
import { resolveEffortCandidates } from "../../shared/model-effort.ts";
|
|
14
|
+
import type { ScriptSourceStore } from "../../shared/script-source.ts";
|
|
12
15
|
import { errorText, truncAt } from "../../shared/text.ts";
|
|
13
16
|
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
14
17
|
import { isAllowlistedBash } from "./allowlist.ts";
|
|
@@ -57,15 +60,6 @@ const REVIEW_TOOL = {
|
|
|
57
60
|
parameters: REVIEW_SCHEMA,
|
|
58
61
|
} satisfies Tool;
|
|
59
62
|
|
|
60
|
-
const REVIEW_MODELS: ReadonlyArray<{ provider: string; model: string; reasoning: ThinkingLevel }> = [
|
|
61
|
-
{ provider: "openai", model: "gpt-6-luna", reasoning: "medium" },
|
|
62
|
-
{ provider: "openai-codex", model: "gpt-6-luna", reasoning: "medium" },
|
|
63
|
-
{ provider: "anthropic", model: "claude-sonnet-5", reasoning: "medium" },
|
|
64
|
-
{ provider: "xai", model: "grok-4.5", reasoning: "low" },
|
|
65
|
-
{ provider: "openrouter", model: "deepseek/deepseek-v4.1-flash", reasoning: "high" },
|
|
66
|
-
{ provider: "opencode-go", model: "deepseek-v4.1-flash", reasoning: "high" },
|
|
67
|
-
];
|
|
68
|
-
|
|
69
63
|
type ToolReview =
|
|
70
64
|
| { decision: "approved"; summary: string }
|
|
71
65
|
| { decision: "requires_user_approval"; summary: string; reason: string };
|
|
@@ -112,10 +106,16 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
112
106
|
async function requestToolApproval(
|
|
113
107
|
ctx: ExtensionContext,
|
|
114
108
|
toolCallId: string,
|
|
115
|
-
|
|
109
|
+
request: ToolApprovalRequest,
|
|
116
110
|
title: string,
|
|
117
111
|
body: string,
|
|
118
112
|
): Promise<{ block: true; reason: string } | undefined> {
|
|
113
|
+
const toolName = request.toolName;
|
|
114
|
+
const source =
|
|
115
|
+
toolName === "script_runner" && typeof request.input.script === "string" ? request.input.script : undefined;
|
|
116
|
+
if (toolName === "script_runner" && source === undefined) {
|
|
117
|
+
return block("script_runner source is unavailable for manual approval");
|
|
118
|
+
}
|
|
119
119
|
if (!ctx.hasUI) return block(`${toolLabel(toolName)} needs confirmation, but interactive UI is unavailable`);
|
|
120
120
|
try {
|
|
121
121
|
emitAgentBlocked(pi, {
|
|
@@ -124,11 +124,14 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
124
124
|
source: "tool-approval.review",
|
|
125
125
|
});
|
|
126
126
|
if (ctx.mode !== "tui") {
|
|
127
|
-
const confirmed = await ctx.ui.confirm(
|
|
127
|
+
const confirmed = await ctx.ui.confirm(
|
|
128
|
+
title,
|
|
129
|
+
source === undefined ? body : `${body}\n\nFull script:\n${source}`,
|
|
130
|
+
);
|
|
128
131
|
return confirmed ? undefined : block(`${toolLabel(toolName)} rejected by user`);
|
|
129
132
|
}
|
|
130
133
|
const answer = await ctx.ui.custom<ApprovalAnswer | undefined>(
|
|
131
|
-
(tui, theme, keys, done) => new ToolApprovalPanel(tui, theme, keys, title, body, done),
|
|
134
|
+
(tui, theme, keys, done) => new ToolApprovalPanel(tui, theme, keys, title, body, source, done),
|
|
132
135
|
);
|
|
133
136
|
if (!answer) return block(`${toolLabel(toolName)} approval cancelled by user`);
|
|
134
137
|
const note = truncAt(answer.note, 800);
|
|
@@ -147,6 +150,7 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
147
150
|
pi.on("session_start", async (_event, ctx) => {
|
|
148
151
|
batchReviews = undefined;
|
|
149
152
|
pendingNotes.clear();
|
|
153
|
+
scriptSourceStoreFrom(pi)?.clearApprovals();
|
|
150
154
|
await refreshSettings(ctx);
|
|
151
155
|
});
|
|
152
156
|
|
|
@@ -161,6 +165,17 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
161
165
|
return block(`tool approval settings failed to load: ${truncAt(message, 600)}`);
|
|
162
166
|
}
|
|
163
167
|
if (!settings.enabled) return undefined;
|
|
168
|
+
const scriptStore = scriptSourceStoreFrom(pi);
|
|
169
|
+
if (request.toolName === "script_runner") {
|
|
170
|
+
try {
|
|
171
|
+
if (!scriptStore) return block("script_runner source store is unavailable for review");
|
|
172
|
+
const { source } = scriptStore.resolve(request.input);
|
|
173
|
+
request.input.script = source;
|
|
174
|
+
delete request.input.edits;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return block(`script_runner source could not be reviewed: ${errorText(error)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
164
179
|
|
|
165
180
|
if (request.toolName === "bash") {
|
|
166
181
|
const command = request.input.command;
|
|
@@ -174,7 +189,7 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
174
189
|
|
|
175
190
|
ctx.ui.setStatus(STATUS_KEY, `reviewing ${toolLabel(request.toolName)}`);
|
|
176
191
|
try {
|
|
177
|
-
batchReviews ??= await reviewAssistantRequests(ctx, event, request);
|
|
192
|
+
batchReviews ??= await reviewAssistantRequests(ctx, event, request, scriptStore);
|
|
178
193
|
if (ctx.signal?.aborted) return block("Tool review cancelled");
|
|
179
194
|
const cached = batchReviews.get(event.toolCallId);
|
|
180
195
|
batchReviews.delete(event.toolCallId);
|
|
@@ -188,41 +203,51 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
188
203
|
}
|
|
189
204
|
if (ctx.signal?.aborted) return block("Tool review cancelled");
|
|
190
205
|
const { review, provider, model } = result;
|
|
206
|
+
let rejected: { block: true; reason: string } | undefined;
|
|
191
207
|
if (review.decision === "requires_user_approval") {
|
|
192
|
-
|
|
208
|
+
rejected = await requestToolApproval(
|
|
193
209
|
ctx,
|
|
194
210
|
event.toolCallId,
|
|
195
|
-
request
|
|
211
|
+
request,
|
|
196
212
|
`Approve high-impact ${toolLabel(request.toolName)}?`,
|
|
197
213
|
formatApproval(review.summary, review.reason),
|
|
198
214
|
);
|
|
199
|
-
}
|
|
200
|
-
if (settings.autoApprove) {
|
|
215
|
+
} else if (settings.autoApprove) {
|
|
201
216
|
pi.appendEntry<AutoApprovedMarker>(AUTO_APPROVED_TYPE, {
|
|
202
217
|
toolName: request.toolName,
|
|
203
218
|
provider,
|
|
204
219
|
model,
|
|
205
220
|
});
|
|
206
|
-
|
|
221
|
+
} else {
|
|
222
|
+
rejected = await requestToolApproval(
|
|
223
|
+
ctx,
|
|
224
|
+
event.toolCallId,
|
|
225
|
+
request,
|
|
226
|
+
`Run reviewed ${toolLabel(request.toolName)}?`,
|
|
227
|
+
formatApproval(review.summary, "Automatic approval is disabled."),
|
|
228
|
+
);
|
|
207
229
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
event.toolCallId,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
formatApproval(review.summary, "Automatic approval is disabled."),
|
|
214
|
-
);
|
|
230
|
+
if (!rejected && request.toolName === "script_runner") {
|
|
231
|
+
if (!scriptStore) return block("script_runner source store is unavailable for approval");
|
|
232
|
+
scriptStore.approve(event.toolCallId, request.input);
|
|
233
|
+
}
|
|
234
|
+
return rejected;
|
|
215
235
|
} catch (error) {
|
|
216
236
|
if (ctx.signal?.aborted) return block("Tool review cancelled");
|
|
217
237
|
const message = singleLine(errorText(error));
|
|
218
238
|
ctx.ui.notify(`Tool review failed; manual approval required: ${truncAt(message, 600)}`, "warning");
|
|
219
|
-
|
|
239
|
+
const rejected = await requestToolApproval(
|
|
220
240
|
ctx,
|
|
221
241
|
event.toolCallId,
|
|
222
|
-
request
|
|
242
|
+
request,
|
|
223
243
|
`Automatic ${toolLabel(request.toolName)} review failed. Continue?`,
|
|
224
|
-
`The automatic review failed, so Tau could not summarize this ${toolLabel(request.toolName)}. Approve it only if you understand the request shown above.`,
|
|
244
|
+
`The automatic review failed, so Tau could not summarize this ${toolLabel(request.toolName)}. Approve it only if you understand ${request.toolName === "script_runner" ? "the full script below" : "the request shown above"}.`,
|
|
225
245
|
);
|
|
246
|
+
if (!rejected && request.toolName === "script_runner") {
|
|
247
|
+
if (!scriptStore) return block("script_runner source store is unavailable for approval");
|
|
248
|
+
scriptStore.approve(event.toolCallId, request.input);
|
|
249
|
+
}
|
|
250
|
+
return rejected;
|
|
226
251
|
} finally {
|
|
227
252
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
228
253
|
}
|
|
@@ -250,15 +275,29 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
250
275
|
pi.on("agent_end", () => {
|
|
251
276
|
batchReviews = undefined;
|
|
252
277
|
pendingNotes.clear();
|
|
278
|
+
scriptSourceStoreFrom(pi)?.clearApprovals();
|
|
253
279
|
});
|
|
254
280
|
|
|
255
281
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
256
282
|
batchReviews = undefined;
|
|
257
283
|
pendingNotes.clear();
|
|
284
|
+
scriptSourceStoreFrom(pi)?.clearApprovals();
|
|
258
285
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
259
286
|
});
|
|
260
287
|
}
|
|
261
288
|
|
|
289
|
+
function scriptSourceStoreFrom(pi: ExtensionAPI): ScriptSourceStore | undefined {
|
|
290
|
+
let store: ScriptSourceStore | undefined;
|
|
291
|
+
let count = 0;
|
|
292
|
+
emitTauEvent(pi, "tau:script-runner.source-store", {
|
|
293
|
+
accept(candidate) {
|
|
294
|
+
store = candidate;
|
|
295
|
+
count++;
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
return count === 1 ? store : undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
262
301
|
function approvalRequest(event: ToolCallEvent): ToolApprovalRequest | undefined {
|
|
263
302
|
if (isToolCallEventType("bash", event)) return { toolName: "bash", input: event.input };
|
|
264
303
|
if (isToolCallEventType<"script_runner", Record<string, unknown>>("script_runner", event)) {
|
|
@@ -284,6 +323,7 @@ async function reviewAssistantRequests(
|
|
|
284
323
|
ctx: ExtensionContext,
|
|
285
324
|
event: ToolCallEvent,
|
|
286
325
|
currentRequest: ToolApprovalRequest,
|
|
326
|
+
scriptStore: ScriptSourceStore | undefined,
|
|
287
327
|
): Promise<Map<string, CachedReview>> {
|
|
288
328
|
const reviews = new Map<string, CachedReview>();
|
|
289
329
|
const assistant = ctx.sessionManager
|
|
@@ -308,6 +348,17 @@ async function reviewAssistantRequests(
|
|
|
308
348
|
const command = request.input.command;
|
|
309
349
|
if (typeof command !== "string" || !command.trim() || isAllowlistedBash(command)) return [];
|
|
310
350
|
}
|
|
351
|
+
if (request.toolName === "script_runner" && part.id !== event.toolCallId) {
|
|
352
|
+
if (!scriptStore) return [];
|
|
353
|
+
try {
|
|
354
|
+
const { source } = scriptStore.resolve(request.input);
|
|
355
|
+
request.input = { ...request.input, script: source };
|
|
356
|
+
delete request.input.edits;
|
|
357
|
+
} catch {
|
|
358
|
+
// The sibling's own validated tool call will reject invalid or missing source.
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
311
362
|
return [{ toolCallId: part.id, request, requestJson: JSON.stringify(request) }];
|
|
312
363
|
});
|
|
313
364
|
|
|
@@ -324,23 +375,11 @@ async function reviewAssistantRequests(
|
|
|
324
375
|
|
|
325
376
|
async function reviewToolRequest(ctx: ExtensionContext, request: ToolApprovalRequest): Promise<ToolReviewResult> {
|
|
326
377
|
const requestJson = JSON.stringify(request);
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
model: ctx.model.id,
|
|
333
|
-
reasoning: "medium",
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
const candidates = await resolveCandidates(ctx, preferred, false);
|
|
337
|
-
const wanted = preferred[0];
|
|
338
|
-
if (
|
|
339
|
-
wanted &&
|
|
340
|
-
!candidates.some((item) => item.model.provider === wanted.provider && item.model.id === wanted.model)
|
|
341
|
-
) {
|
|
342
|
-
ctx.ui.notify(`Tool review skipped ${wanted.provider}/${wanted.model}; trying next model.`, "info");
|
|
343
|
-
}
|
|
378
|
+
// Requests contain shell commands and scripts, so only the session's own provider reviews them.
|
|
379
|
+
const provider = ctx.model?.provider;
|
|
380
|
+
const candidates = (
|
|
381
|
+
await resolveEffortCandidates(ctx, "quick", { includeParentModel: true, preferredProvider: provider })
|
|
382
|
+
).filter((candidate) => candidate.model.provider === provider);
|
|
344
383
|
const { value, candidate } = await generateToolValidated(
|
|
345
384
|
ctx,
|
|
346
385
|
candidates,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
pushSavedNote,
|
|
16
16
|
rawHint,
|
|
17
17
|
renderNoteEditor,
|
|
18
|
+
ScrollableMarkdown,
|
|
18
19
|
ToolPanel,
|
|
19
20
|
type ToolPanelConfig,
|
|
20
21
|
wrapWithPrefix,
|
|
@@ -34,6 +35,7 @@ export class ToolApprovalPanel implements Component, Focusable {
|
|
|
34
35
|
private readonly noteEditor: Editor;
|
|
35
36
|
private readonly panelConfig: ToolPanelConfig;
|
|
36
37
|
private readonly panel: ToolPanel;
|
|
38
|
+
private readonly sourceView: ScrollableMarkdown | undefined;
|
|
37
39
|
private readonly notes: Record<ApprovalChoice, string> = { approve: "", reject: "" };
|
|
38
40
|
private choice: ApprovalChoice = "approve";
|
|
39
41
|
private editing = false;
|
|
@@ -45,6 +47,7 @@ export class ToolApprovalPanel implements Component, Focusable {
|
|
|
45
47
|
keys: KeybindingsManager,
|
|
46
48
|
title: string,
|
|
47
49
|
body: string,
|
|
50
|
+
scriptSource: string | undefined,
|
|
48
51
|
done: (answer: ApprovalAnswer | undefined) => void,
|
|
49
52
|
) {
|
|
50
53
|
this.tui = tui;
|
|
@@ -56,11 +59,16 @@ export class ToolApprovalPanel implements Component, Focusable {
|
|
|
56
59
|
this.notes[this.choice] = value.trim();
|
|
57
60
|
this.closeNote();
|
|
58
61
|
};
|
|
62
|
+
if (scriptSource !== undefined) {
|
|
63
|
+
const fenceLength = (scriptSource.match(/`+/g) ?? []).reduce((max, run) => Math.max(max, run.length + 1), 3);
|
|
64
|
+
const fence = "`".repeat(fenceLength);
|
|
65
|
+
this.sourceView = new ScrollableMarkdown(tui, `${fence}\n${scriptSource}\n${fence}`, 14);
|
|
66
|
+
}
|
|
59
67
|
this.panelConfig = {
|
|
60
68
|
title,
|
|
61
69
|
secondary: "Approve runs this request as shown. To change it, reject with a note.",
|
|
62
70
|
header: [body],
|
|
63
|
-
body: { render: (width) => this.renderChoices(width), invalidate: () =>
|
|
71
|
+
body: { render: (width) => this.renderChoices(width), invalidate: () => this.sourceView?.invalidate() },
|
|
64
72
|
footer: { kind: "hints", hints: this.hints() },
|
|
65
73
|
};
|
|
66
74
|
this.panel = new ToolPanel(theme, this.panelConfig);
|
|
@@ -90,7 +98,9 @@ export class ToolApprovalPanel implements Component, Focusable {
|
|
|
90
98
|
this.done(undefined);
|
|
91
99
|
return;
|
|
92
100
|
}
|
|
93
|
-
if (this.
|
|
101
|
+
if (this.sourceView && (data === "j" || data === "k")) {
|
|
102
|
+
this.sourceView.scroll(data === "j" ? 1 : -1);
|
|
103
|
+
} else if (this.keys.matches(data, "tui.select.up") || this.keys.matches(data, "tui.select.down")) {
|
|
94
104
|
this.choice = this.choice === "approve" ? "reject" : "approve";
|
|
95
105
|
} else if (data === "n") {
|
|
96
106
|
this.editing = true;
|
|
@@ -113,6 +123,9 @@ export class ToolApprovalPanel implements Component, Focusable {
|
|
|
113
123
|
|
|
114
124
|
private renderChoices(width: number): string[] {
|
|
115
125
|
const lines: string[] = [];
|
|
126
|
+
if (this.sourceView) {
|
|
127
|
+
lines.push(this.theme.fg("muted", "Complete script:"), ...this.sourceView.render(width), "");
|
|
128
|
+
}
|
|
116
129
|
for (const choice of ["approve", "reject"] as const) {
|
|
117
130
|
const selected = choice === this.choice;
|
|
118
131
|
const prefix = selected ? this.theme.fg("accent", "→ ") : " ";
|
|
@@ -140,6 +153,7 @@ export class ToolApprovalPanel implements Component, Focusable {
|
|
|
140
153
|
? [bindingHint("tui.input.submit", "save"), bindingHint("tui.select.cancel", "cancel note")]
|
|
141
154
|
: [
|
|
142
155
|
bindingsHint(["tui.select.up", "tui.select.down"], "move"),
|
|
156
|
+
...(this.sourceView ? [rawHint("j/k", "scroll script")] : []),
|
|
143
157
|
bindingHint("tui.select.confirm", "choose"),
|
|
144
158
|
rawHint("n", "note"),
|
|
145
159
|
bindingHint("tui.select.cancel", "block"),
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Tool Loader
|
|
2
2
|
|
|
3
|
-
Tau
|
|
3
|
+
Tau registers specialist tool groups as deferred tools so their schemas stay out of every request. Most coding turns do not need web, image, macOS application, or application-specific schemas. The agent finds and loads them with Pi's built-in `tool_search` tool, which Tau keeps active whenever deferred tools exist.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Tau's built-in groups are:
|
|
6
6
|
|
|
7
7
|
- `web` for public web and implementation research
|
|
8
8
|
- `image` for raster image generation and editing
|
|
9
9
|
- `appshot` for macOS window discovery, capture, and activation
|
|
10
10
|
|
|
11
|
-
Project and global package extensions can add groups with `registerDeferredToolGroup()` from `@shanepadgett/tau-agent`. The group
|
|
11
|
+
Project and global package extensions can add groups with `registerDeferredToolGroup()` from `@shanepadgett/tau-agent`. The group id and description become the tool namespace shown to the agent.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Loaded tools are recorded in the session and stay available on that branch. All models can discover and load deferred tools. Supported models preserve the cached prompt prefix; other models, including Grok and opencode-go, can incur a cache miss when tools are activated. Tau does not compact automatically.
|
|
14
14
|
|
|
15
|
-
After changing this extension during development, run `/reload` before testing.
|
|
15
|
+
Pi's built-in `tool_search` extension must be enabled. After changing this extension during development, run `/reload` before testing.
|