@shanepadgett/tau-agent 0.42.3 → 0.43.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/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 +2 -0
- package/extensions/tool-approval/index.ts +65 -44
- 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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Tool } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ContextWithSystemEvent, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { PromptValue } from "../../shared/prompt-contributions.ts";
|
|
4
|
+
|
|
5
|
+
export const BASELINE_TYPE = "tau.soul.baseline";
|
|
6
|
+
export const UPDATE_TYPE = "tau.soul.update";
|
|
7
|
+
|
|
8
|
+
export interface SavedBaseline {
|
|
9
|
+
version: 1;
|
|
10
|
+
compactionId: string | null;
|
|
11
|
+
afterEntryId: string;
|
|
12
|
+
text: string;
|
|
13
|
+
values: PromptValue[];
|
|
14
|
+
initialTools: Tool[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SavedUpdate {
|
|
18
|
+
version: 1;
|
|
19
|
+
baselineEntryId: string;
|
|
20
|
+
afterEntryId: string;
|
|
21
|
+
text: string;
|
|
22
|
+
values: PromptValue[];
|
|
23
|
+
tools: Tool[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ActivePrompt {
|
|
27
|
+
entryId: string;
|
|
28
|
+
baseline: SavedBaseline;
|
|
29
|
+
updates: Array<{ timestamp: number; data: SavedUpdate }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function restorePrompt(branch: readonly SessionEntry[]): ActivePrompt | null {
|
|
33
|
+
const newestFirst = [...branch].reverse();
|
|
34
|
+
const compaction = newestFirst.find((entry) => entry.type === "compaction");
|
|
35
|
+
const entry = newestFirst.find((item) => item.type === "custom" && item.customType === BASELINE_TYPE);
|
|
36
|
+
if (entry?.type !== "custom") return null;
|
|
37
|
+
const baseline = entry.data as SavedBaseline;
|
|
38
|
+
if (baseline?.version !== 1 || typeof baseline.text !== "string" || !Array.isArray(baseline.values)) {
|
|
39
|
+
throw new Error("Invalid saved Soul baseline.");
|
|
40
|
+
}
|
|
41
|
+
if (baseline.compactionId !== (compaction?.id ?? null)) return null;
|
|
42
|
+
const updates: ActivePrompt["updates"] = [];
|
|
43
|
+
for (const item of branch) {
|
|
44
|
+
if (item.type !== "custom" || item.customType !== UPDATE_TYPE) continue;
|
|
45
|
+
const data = item.data as SavedUpdate;
|
|
46
|
+
if (data?.version !== 1 || typeof data.text !== "string" || !Array.isArray(data.values)) {
|
|
47
|
+
throw new Error("Invalid saved Soul update.");
|
|
48
|
+
}
|
|
49
|
+
if (data.baselineEntryId === entry.id) updates.push({ timestamp: Date.parse(item.timestamp), data });
|
|
50
|
+
}
|
|
51
|
+
return { entryId: entry.id, baseline, updates };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Rebuild only Soul's text; tool declarations retain their original transcript positions. */
|
|
55
|
+
export function projectPrompt(
|
|
56
|
+
messages: ContextWithSystemEvent["messages"],
|
|
57
|
+
saved: ActivePrompt,
|
|
58
|
+
ctx: ExtensionContext,
|
|
59
|
+
): ContextWithSystemEvent["messages"] {
|
|
60
|
+
const projection = ctx.sessionManager.buildSessionProjection();
|
|
61
|
+
// Pi clones the hook input. Compare content, never object identity. A rewrite from
|
|
62
|
+
// another extension must not silently move a saved update or tool declaration.
|
|
63
|
+
if (JSON.stringify(messages) !== JSON.stringify(projection.messages)) {
|
|
64
|
+
throw new Error("Soul cannot preserve the prompt prefix: another extension changed request history.");
|
|
65
|
+
}
|
|
66
|
+
const output: ContextWithSystemEvent["messages"] = [
|
|
67
|
+
{
|
|
68
|
+
role: "system",
|
|
69
|
+
content: saved.baseline.text,
|
|
70
|
+
toolsAdded: saved.baseline.initialTools,
|
|
71
|
+
timestamp: 0,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
let pastBaseline = false;
|
|
75
|
+
const placed = new Set<SavedUpdate>();
|
|
76
|
+
const admitted = new Set(saved.baseline.initialTools.map((tool) => tool.name));
|
|
77
|
+
for (const entry of projection.entries) {
|
|
78
|
+
for (const message of entry.messages) {
|
|
79
|
+
if (message.role !== "system") {
|
|
80
|
+
output.push(message);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!pastBaseline) continue;
|
|
84
|
+
if (typeof message.content === "string" ? message.content.trim() : message.content.length > 0) {
|
|
85
|
+
throw new Error("A later system instruction bypassed Soul's prompt contributions.");
|
|
86
|
+
}
|
|
87
|
+
// Removed tools remain declared for cache stability; Pi still controls execution.
|
|
88
|
+
const added = (message.toolsAdded ?? []).filter((tool) => !admitted.has(tool.name));
|
|
89
|
+
for (const tool of added) admitted.add(tool.name);
|
|
90
|
+
if (added.length > 0)
|
|
91
|
+
output.push({ role: "system", content: "", toolsAdded: added, timestamp: message.timestamp });
|
|
92
|
+
}
|
|
93
|
+
if (entry.sourceEntry.id === saved.baseline.afterEntryId) pastBaseline = true;
|
|
94
|
+
for (const update of saved.updates) {
|
|
95
|
+
if (update.data.afterEntryId !== entry.sourceEntry.id) continue;
|
|
96
|
+
if (update.data.text)
|
|
97
|
+
output.push({
|
|
98
|
+
role: "custom",
|
|
99
|
+
customType: UPDATE_TYPE,
|
|
100
|
+
content: update.data.text,
|
|
101
|
+
display: false,
|
|
102
|
+
timestamp: update.timestamp,
|
|
103
|
+
});
|
|
104
|
+
placed.add(update.data);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!pastBaseline || placed.size !== saved.updates.length)
|
|
108
|
+
throw new Error("Soul prompt history lost a saved anchor.");
|
|
109
|
+
return output;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function admittedTools(saved: ActivePrompt): Tool[] {
|
|
113
|
+
return saved.updates.at(-1)?.data.tools ?? saved.baseline.initialTools;
|
|
114
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { declarationsEqual, type Api, type Model, type Tool } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
/** Removals only disable execution; changed declarations wait for compaction. */
|
|
4
|
+
export function toolChangeReason(
|
|
5
|
+
model: Model<Api>,
|
|
6
|
+
previous: readonly Tool[],
|
|
7
|
+
requested: readonly Tool[],
|
|
8
|
+
): string | null {
|
|
9
|
+
const existing = new Map(previous.map((tool) => [tool.name, tool]));
|
|
10
|
+
for (const tool of requested) {
|
|
11
|
+
const old = existing.get(tool.name);
|
|
12
|
+
if (old && !declarationsEqual(old, tool))
|
|
13
|
+
return `Tool ${tool.name} changed its schema or description. Compact before loading it.`;
|
|
14
|
+
}
|
|
15
|
+
if (requested.every((tool) => existing.has(tool.name))) return null;
|
|
16
|
+
const compat = model.compat;
|
|
17
|
+
const supported =
|
|
18
|
+
compat &&
|
|
19
|
+
"supportsMidConvoSystemMessages" in compat &&
|
|
20
|
+
compat.supportsMidConvoSystemMessages === true &&
|
|
21
|
+
((model.api === "anthropic-messages" &&
|
|
22
|
+
"supportsMidConvoToolChanges" in compat &&
|
|
23
|
+
compat.supportsMidConvoToolChanges === true &&
|
|
24
|
+
previous.length > 0) ||
|
|
25
|
+
((model.api === "openai-responses" || model.api === "openai-codex-responses") &&
|
|
26
|
+
(("supportsAdditionalTools" in compat && compat.supportsAdditionalTools === true) ||
|
|
27
|
+
("supportsToolSearch" in compat && compat.supportsToolSearch === true))));
|
|
28
|
+
return supported
|
|
29
|
+
? null
|
|
30
|
+
: "This model cannot add tools without changing the cached prefix. Compact before loading them.";
|
|
31
|
+
}
|
|
@@ -12,16 +12,12 @@ Adds `/aside <question>` for a one-off question to the current model without put
|
|
|
12
12
|
|
|
13
13
|
## attention
|
|
14
14
|
|
|
15
|
-
Shows attention state when Tau needs the user to look at the chat, finishes a
|
|
15
|
+
Shows attention state when Tau needs the user to look at the chat, finishes a compaction, or summarizes an abandoned branch.
|
|
16
16
|
|
|
17
17
|
## auto-name
|
|
18
18
|
|
|
19
19
|
Names sessions from their first request so saved sessions remain findable.
|
|
20
20
|
|
|
21
|
-
## auto-compact
|
|
22
|
-
|
|
23
|
-
Uses Pi's native compaction before a model turn when the current context reaches `extensions.autoCompact.tokenLimit`, which defaults to 175,000 tokens for every model. Set `extensions.autoCompact.enabled` to `false` to disable it. Interrupted work resumes through a hidden continuation message without an attention alert until the resumed work settles. Pi's native collapsed compaction entry remains visible in chat.
|
|
24
|
-
|
|
25
21
|
## branch
|
|
26
22
|
|
|
27
23
|
Adds `/branch` to create and switch Git branches from the TUI. Switching
|
|
@@ -46,11 +42,7 @@ Adds `/cost-report` to build an HTML spend report from local session usage. Pick
|
|
|
46
42
|
|
|
47
43
|
## context
|
|
48
44
|
|
|
49
|
-
Adds `/context` to inject reusable repository work scopes from `.pi/contexts
|
|
50
|
-
|
|
51
|
-
## effort
|
|
52
|
-
|
|
53
|
-
Adds `/effort [quick|standard|deep]` to select effort and a provider from current logins. Tau selects provider’s best available model for tier, then tries its configured model fallback. `Ctrl+Shift+E` cycles tiers on current provider. Footer derives effort from current provider, model, and thinking level, and hides it when no configured tier matches.
|
|
45
|
+
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.
|
|
54
46
|
|
|
55
47
|
## explore
|
|
56
48
|
|
|
@@ -102,7 +94,7 @@ Shows a compact display-only marker after each run with wall time and model cost
|
|
|
102
94
|
|
|
103
95
|
## runtime-context
|
|
104
96
|
|
|
105
|
-
Supplies
|
|
97
|
+
Supplies Soul with the local date and root directory snapshot. Both remain fixed across turns, reload, and resume, and refresh after successful compaction.
|
|
106
98
|
|
|
107
99
|
## script-runner
|
|
108
100
|
|
|
@@ -114,16 +106,12 @@ Runs configured commands while keeping their output out of agent context when th
|
|
|
114
106
|
|
|
115
107
|
## soul
|
|
116
108
|
|
|
117
|
-
|
|
109
|
+
Supplies Tau's communication, discussion, planning, execution, and coding instructions. Saves a prompt baseline across turns, reload, and resume; refreshes it after successful compaction. Operational changes arrive as saved context updates without rewriting earlier instructions. Tool groups that cannot load without changing the cached prefix wait for successful compaction.
|
|
118
110
|
|
|
119
111
|
## stash
|
|
120
112
|
|
|
121
113
|
Adds `Alt+S` to stash the current prompt draft and `/pop` to browse stashed drafts and put one back in the editor.
|
|
122
114
|
|
|
123
|
-
## subagent
|
|
124
|
-
|
|
125
|
-
Gives Tau a subagent delegation tool for isolated, focused work. Run `/agents` to enable or disable individual agents for the current session, or set `extensions.subagent.disabled` in Tau settings for a persistent choice. `scout` is substantial multi-hop local code lookup that would chew parent context; facts only, not small digs; `web-research` handles external research. Known files can be autoread as line-numbered snapshots into a fresh or retained child turn. Tau can continue a retained child thread when follow-up work depends on its prior reads and reasoning. You can also create your own subagents in supported subagent directories. Ask Tau how to do it and have it consult extension documentation; built-in agents show pattern. Each subagent can register its own model, tools, and pool of display names. Reused pool names get numeric suffixes. In interactive cmux sessions, Tau opens one temporary Markdown dashboard for live subagent progress; it does not change how children run and closes shortly after active cohort finishes.
|
|
126
|
-
|
|
127
115
|
## tau-help
|
|
128
116
|
|
|
129
117
|
Adds `/tau-help` to show this guide as rendered Markdown in the chat.
|
|
@@ -134,11 +122,11 @@ Adds `/tau`, `/tau init [--global|--project]`, and `/tau doctor` for Tau setup a
|
|
|
134
122
|
|
|
135
123
|
## tool-approval
|
|
136
124
|
|
|
137
|
-
Reviews agent `bash` and `script_runner` requests before they run. Common read-only bash commands skip review. Set `extensions.toolApproval.autoApprove` to run every reviewer-approved request without another confirmation. Those auto-approvals show a user-only marker. The reviewer approves routine local development work. Concrete destructive, system, production, privileged, or security-sensitive effects require human approval with one explanatory paragraph. Reviewer failures fall back to human approval and send an attention notification.
|
|
125
|
+
Reviews agent `bash` and `script_runner` requests before they run. Common read-only bash commands skip review. Set `extensions.toolApproval.autoApprove` to run every reviewer-approved request without another confirmation. Those auto-approvals show a user-only marker. The reviewer approves routine local development work. Concrete destructive, system, production, privileged, or security-sensitive effects require human approval with one explanatory paragraph. Reviewer failures fall back to human approval and send an attention notification. In the terminal approval panel, press `n` to add a note to Approve or Reject before choosing. Rejection notes tell the agent why the request was blocked; approval notes reach it with the tool result without changing the request. Reject with a note to ask for a revised request.
|
|
138
126
|
|
|
139
127
|
## tool-loader
|
|
140
128
|
|
|
141
|
-
Progressively exposes registered specialist tool groups through `load_tools`. Tau registers `web`, `image`, and `appshot`; project or global package extensions can add groups with `registerDeferredToolGroup()` from `@shanepadgett/tau-agent`.
|
|
129
|
+
Progressively exposes registered specialist tool groups through `load_tools`. Tau registers `web`, `image`, and `appshot`; project or global package extensions can add groups with `registerDeferredToolGroup()` from `@shanepadgett/tau-agent`. Compatible models load tools without replacing the cached prefix. Otherwise, requested groups are queued until successful compaction; loading never triggers compaction automatically.
|
|
142
130
|
|
|
143
131
|
## web
|
|
144
132
|
|
|
@@ -3,11 +3,12 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { Markdown, type Component, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
|
+
import { registerPromptSource } from "../../shared/prompt-contributions.ts";
|
|
6
7
|
|
|
7
8
|
const TAU_DOCS_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "docs");
|
|
8
9
|
const TAU_DOCS_GUIDANCE = `Tau Agent documentation (read only when the user asks about Tau Agent, Rok, Tau extensions, Tau event APIs, harness behavior, or extending Tau Agent):
|
|
9
10
|
- Tau Agent docs: ${TAU_DOCS_PATH}
|
|
10
|
-
- When asked about: context management / .pi/contexts taxonomy (docs/context.md), public events / external integration (docs/extending-tau-agent.md),
|
|
11
|
+
- When asked about: context management / .pi/contexts taxonomy (docs/context.md), public events / external integration (docs/extending-tau-agent.md), Tau TUI components (docs/tui.md)
|
|
11
12
|
- Resolve Tau docs/... under Tau Agent docs, not the current working directory
|
|
12
13
|
- When working on Tau topics, read the docs and follow .md cross-references before implementing
|
|
13
14
|
- Do not read Tau Agent docs for normal coding tasks`;
|
|
@@ -54,9 +55,14 @@ class TauHelpMessage implements Component {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export default function tauHelpExtension(pi: ExtensionAPI): void {
|
|
57
|
-
pi
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
registerPromptSource(pi, {
|
|
59
|
+
key: "tau/documentation",
|
|
60
|
+
section: "documentation",
|
|
61
|
+
refresh: "compaction",
|
|
62
|
+
async read() {
|
|
63
|
+
return TAU_DOCS_GUIDANCE;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
60
66
|
|
|
61
67
|
pi.registerMessageRenderer("tau-help", (message, _options, _theme) => {
|
|
62
68
|
if (typeof message.content !== "string") return undefined;
|
|
@@ -8,6 +8,8 @@ With `autoApprove` enabled, reviewer-approved requests run without another confi
|
|
|
8
8
|
|
|
9
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.
|
|
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.
|
|
12
|
+
|
|
11
13
|
Configure under `extensions.toolApproval`:
|
|
12
14
|
|
|
13
15
|
```json
|
|
@@ -12,6 +12,7 @@ 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";
|
|
@@ -56,12 +57,12 @@ const REVIEW_TOOL = {
|
|
|
56
57
|
} satisfies Tool;
|
|
57
58
|
|
|
58
59
|
const REVIEW_MODELS: ReadonlyArray<{ provider: string; model: string; reasoning: ThinkingLevel }> = [
|
|
59
|
-
{ provider: "openai", model: "gpt-
|
|
60
|
-
{ provider: "openai-codex", model: "gpt-
|
|
60
|
+
{ provider: "openai", model: "gpt-6-luna", reasoning: "medium" },
|
|
61
|
+
{ provider: "openai-codex", model: "gpt-6-luna", reasoning: "medium" },
|
|
61
62
|
{ provider: "anthropic", model: "claude-sonnet-5", reasoning: "medium" },
|
|
62
63
|
{ 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: "
|
|
64
|
+
{ provider: "openrouter", model: "deepseek/deepseek-v4.1-flash", reasoning: "high" },
|
|
65
|
+
{ provider: "opencode-go", model: "deepseek-v4.1-flash", reasoning: "high" },
|
|
65
66
|
];
|
|
66
67
|
|
|
67
68
|
type ToolReview =
|
|
@@ -82,6 +83,7 @@ interface AutoApprovedMarker {
|
|
|
82
83
|
|
|
83
84
|
export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
84
85
|
let settings = toolApprovalSettings.defaults;
|
|
86
|
+
const pendingNotes = new Map<string, string>();
|
|
85
87
|
|
|
86
88
|
pi.registerEntryRenderer<AutoApprovedMarker>(AUTO_APPROVED_TYPE, (entry, _options, theme) => {
|
|
87
89
|
const marker = autoApprovedMarker(entry.data);
|
|
@@ -98,22 +100,44 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
98
100
|
settings = await loadTauExtensionSettings(ctx, toolApprovalSettings);
|
|
99
101
|
}
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
async function requestToolApproval(
|
|
104
|
+
ctx: ExtensionContext,
|
|
105
|
+
toolCallId: string,
|
|
106
|
+
toolName: ApprovalToolName,
|
|
107
|
+
title: string,
|
|
108
|
+
body: string,
|
|
109
|
+
): Promise<{ block: true; reason: string } | undefined> {
|
|
110
|
+
if (!ctx.hasUI) return block(`${toolLabel(toolName)} needs confirmation, but interactive UI is unavailable`);
|
|
111
|
+
try {
|
|
112
|
+
emitAgentBlocked(pi, {
|
|
113
|
+
title: "Tool request review",
|
|
114
|
+
body: `Waiting for ${toolLabel(toolName)} approval`,
|
|
115
|
+
source: "tool-approval.review",
|
|
116
|
+
});
|
|
117
|
+
if (ctx.mode !== "tui") {
|
|
118
|
+
const confirmed = await ctx.ui.confirm(title, body);
|
|
119
|
+
return confirmed ? undefined : block(`${toolLabel(toolName)} rejected by user`);
|
|
120
|
+
}
|
|
121
|
+
const answer = await ctx.ui.custom<ApprovalAnswer | undefined>(
|
|
122
|
+
(tui, theme, keys, done) => new ToolApprovalPanel(tui, theme, keys, title, body, done),
|
|
123
|
+
);
|
|
124
|
+
if (!answer) return block(`${toolLabel(toolName)} approval cancelled by user`);
|
|
125
|
+
const note = truncAt(answer.note, 800);
|
|
126
|
+
if (answer.choice === "reject") {
|
|
127
|
+
return block(`${toolLabel(toolName)} rejected by user${note ? `. User note: ${note}` : ""}`);
|
|
128
|
+
}
|
|
129
|
+
if (note) pendingNotes.set(toolCallId, note);
|
|
130
|
+
return undefined;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const message = singleLine(errorText(error));
|
|
133
|
+
ctx.ui.notify(`Tool approval failed; request blocked: ${truncAt(message, 600)}`, "error");
|
|
134
|
+
return block(`tool approval failed: ${truncAt(message, 600)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
104
137
|
|
|
105
|
-
pi.on("
|
|
138
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
139
|
+
pendingNotes.clear();
|
|
106
140
|
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
141
|
});
|
|
118
142
|
|
|
119
143
|
pi.on("tool_call", async (event, ctx) => {
|
|
@@ -145,8 +169,8 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
145
169
|
const { review, provider, model } = await reviewToolRequest(ctx, request);
|
|
146
170
|
if (review.decision === "requires_user_approval") {
|
|
147
171
|
return requestToolApproval(
|
|
148
|
-
pi,
|
|
149
172
|
ctx,
|
|
173
|
+
event.toolCallId,
|
|
150
174
|
request.toolName,
|
|
151
175
|
`Approve high-impact ${toolLabel(request.toolName)}?`,
|
|
152
176
|
formatApproval(review.summary, review.reason),
|
|
@@ -161,8 +185,8 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
161
185
|
return undefined;
|
|
162
186
|
}
|
|
163
187
|
return requestToolApproval(
|
|
164
|
-
pi,
|
|
165
188
|
ctx,
|
|
189
|
+
event.toolCallId,
|
|
166
190
|
request.toolName,
|
|
167
191
|
`Run reviewed ${toolLabel(request.toolName)}?`,
|
|
168
192
|
formatApproval(review.summary, "Automatic approval is disabled."),
|
|
@@ -171,8 +195,8 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
171
195
|
const message = singleLine(errorText(error));
|
|
172
196
|
ctx.ui.notify(`Tool review failed; manual approval required: ${truncAt(message, 600)}`, "warning");
|
|
173
197
|
return requestToolApproval(
|
|
174
|
-
pi,
|
|
175
198
|
ctx,
|
|
199
|
+
event.toolCallId,
|
|
176
200
|
request.toolName,
|
|
177
201
|
`Automatic ${toolLabel(request.toolName)} review failed. Continue?`,
|
|
178
202
|
`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 +206,27 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
|
|
|
182
206
|
}
|
|
183
207
|
});
|
|
184
208
|
|
|
209
|
+
pi.on("tool_result", (event) => {
|
|
210
|
+
const note = pendingNotes.get(event.toolCallId);
|
|
211
|
+
if (!note) return;
|
|
212
|
+
pendingNotes.delete(event.toolCallId);
|
|
213
|
+
return {
|
|
214
|
+
content: [
|
|
215
|
+
...event.content,
|
|
216
|
+
{
|
|
217
|
+
type: "text" as const,
|
|
218
|
+
text: `User approval note (guidance for subsequent actions; does not change this tool request):\n${note}`,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
pi.on("agent_end", () => {
|
|
225
|
+
pendingNotes.clear();
|
|
226
|
+
});
|
|
227
|
+
|
|
185
228
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
229
|
+
pendingNotes.clear();
|
|
186
230
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
187
231
|
});
|
|
188
232
|
}
|
|
@@ -269,29 +313,6 @@ function formatApproval(summary: string, reason: string): string {
|
|
|
269
313
|
return singleLine(`${summary} ${reason}`);
|
|
270
314
|
}
|
|
271
315
|
|
|
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
316
|
function singleLine(text: string): string {
|
|
296
317
|
return text.replaceAll(/\s+/g, " ").trim();
|
|
297
318
|
}
|
|
@@ -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.
|