@shanepadgett/tau-agent 0.32.0 → 0.33.1
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/extensions/effort/README.md +7 -0
- package/extensions/effort/index.ts +134 -0
- package/extensions/effort/state.ts +33 -0
- package/extensions/footer/README.md +1 -1
- package/extensions/footer/index.ts +36 -6
- package/extensions/script-runner/README.md +2 -2
- package/extensions/script-runner/index.ts +17 -20
- package/extensions/tau-help/help.md +6 -2
- package/extensions/working-memory/README.md +1 -1
- package/extensions/working-memory/checkpoint.ts +4 -1
- package/extensions/working-memory/index.ts +2 -1
- package/extensions/working-memory/state.ts +1 -0
- package/package.json +2 -2
- package/shared/events.ts +5 -0
- package/shared/model-effort.ts +91 -13
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Effort Extension
|
|
2
|
+
|
|
3
|
+
Switches active chat model and thinking level as one effort tier. Available providers come from current logins, and each provider can fall back through its ranked models.
|
|
4
|
+
|
|
5
|
+
Run `/effort` to choose a tier and provider. `/effort low`, `/effort medium`, and `/effort high` skip the tier prompt. Press `Ctrl+Shift+E` to cycle tiers on current provider.
|
|
6
|
+
|
|
7
|
+
If selected provider has no usable model for tier, current model stays active. Footer shows an effort label whenever current provider, model, and thinking level match a configured tier; otherwise it shows no label.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Key, type AutocompleteItem } from "@earendil-works/pi-tui";
|
|
3
|
+
import { emitTauEvent, onTauEvent } from "../../shared/events.ts";
|
|
4
|
+
import {
|
|
5
|
+
effortForSelection,
|
|
6
|
+
type EffortProviderCandidates,
|
|
7
|
+
type ModelEffort,
|
|
8
|
+
resolveEffortProviders,
|
|
9
|
+
} from "../../shared/model-effort.ts";
|
|
10
|
+
import { EFFORT_STATE_TYPE, effortState, nextEffort } from "./state.ts";
|
|
11
|
+
|
|
12
|
+
const EFFORTS: readonly ModelEffort[] = ["low", "medium", "high"];
|
|
13
|
+
|
|
14
|
+
export default function effortExtension(pi: ExtensionAPI): void {
|
|
15
|
+
let activeEffort: ModelEffort | undefined;
|
|
16
|
+
let applying = false;
|
|
17
|
+
|
|
18
|
+
function publish(): void {
|
|
19
|
+
emitTauEvent(pi, "tau:model-effort.changed", { effort: activeEffort });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function setEffort(effort: ModelEffort | undefined): void {
|
|
23
|
+
if (activeEffort === effort) return;
|
|
24
|
+
activeEffort = effort;
|
|
25
|
+
pi.appendEntry(EFFORT_STATE_TYPE, effortState(effort));
|
|
26
|
+
publish();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function apply(
|
|
30
|
+
ctx: ExtensionContext,
|
|
31
|
+
effort: ModelEffort,
|
|
32
|
+
provider: EffortProviderCandidates,
|
|
33
|
+
): Promise<boolean> {
|
|
34
|
+
applying = true;
|
|
35
|
+
try {
|
|
36
|
+
for (const candidate of provider.candidates) {
|
|
37
|
+
if (!(await pi.setModel(candidate.model))) continue;
|
|
38
|
+
pi.setThinkingLevel(candidate.reasoning);
|
|
39
|
+
setEffort(effortForSelection(provider.provider, candidate.model.id, pi.getThinkingLevel()));
|
|
40
|
+
ctx.ui.notify(`Effort: ${effort} · ${provider.label}/${candidate.model.id}`, "info");
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
} finally {
|
|
45
|
+
applying = false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function chooseProvider(ctx: ExtensionContext, effort: ModelEffort): Promise<void> {
|
|
50
|
+
const providers = resolveEffortProviders(ctx, effort);
|
|
51
|
+
if (providers.length === 0) {
|
|
52
|
+
ctx.ui.notify(`No logged-in provider has a ${effort} effort model. Current model kept.`, "warning");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const labels = providers.map((provider) => `${provider.label} · ${provider.provider}`);
|
|
56
|
+
const selected = await ctx.ui.select(`Provider for ${effort} effort`, labels);
|
|
57
|
+
if (!selected) return;
|
|
58
|
+
const provider = providers[labels.indexOf(selected)];
|
|
59
|
+
if (!provider) return;
|
|
60
|
+
if (!(await apply(ctx, effort, provider))) {
|
|
61
|
+
ctx.ui.notify(`Could not select a ${effort} model from ${provider.label}. Current model kept.`, "warning");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pi.registerCommand("effort", {
|
|
66
|
+
description: "Select effort tier and logged-in provider",
|
|
67
|
+
getArgumentCompletions(prefix: string): AutocompleteItem[] | null {
|
|
68
|
+
const value = prefix.trimStart();
|
|
69
|
+
if (/\s/.test(value)) return null;
|
|
70
|
+
const items = EFFORTS.filter((effort) => effort.startsWith(value)).map((effort) => ({
|
|
71
|
+
value: effort,
|
|
72
|
+
label: effort,
|
|
73
|
+
}));
|
|
74
|
+
return items.length ? items : null;
|
|
75
|
+
},
|
|
76
|
+
handler: async (args, ctx) => {
|
|
77
|
+
await ctx.waitForIdle();
|
|
78
|
+
if (!ctx.hasUI) {
|
|
79
|
+
ctx.ui.notify("/effort requires interactive UI.", "error");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const arg = args.trim().toLowerCase();
|
|
83
|
+
if (arg && !isModelEffort(arg)) {
|
|
84
|
+
ctx.ui.notify("Usage: /effort [low|medium|high]", "error");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
let effort: ModelEffort | undefined = isModelEffort(arg) ? arg : undefined;
|
|
88
|
+
if (!arg) {
|
|
89
|
+
const selected = await ctx.ui.select("Effort", [...EFFORTS]);
|
|
90
|
+
if (selected && isModelEffort(selected)) effort = selected;
|
|
91
|
+
}
|
|
92
|
+
if (!effort) return;
|
|
93
|
+
await chooseProvider(ctx, effort);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
pi.registerShortcut(Key.ctrlShift("e"), {
|
|
98
|
+
description: "Cycle effort for current provider",
|
|
99
|
+
handler: async (ctx) => {
|
|
100
|
+
if (!ctx.isIdle()) {
|
|
101
|
+
ctx.ui.notify("Wait for the current run before changing effort.", "warning");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const effort = nextEffort(activeEffort);
|
|
105
|
+
const provider = resolveEffortProviders(ctx, effort).find((item) => item.provider === ctx.model?.provider);
|
|
106
|
+
if (!provider || !(await apply(ctx, effort, provider))) {
|
|
107
|
+
ctx.ui.notify(`Current provider has no available ${effort} effort model. Current model kept.`, "warning");
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
onTauEvent(pi, "model-effort.snapshot", "tau:model-effort.snapshot.requested", publish);
|
|
113
|
+
|
|
114
|
+
pi.on("session_start", (_event, ctx) => {
|
|
115
|
+
activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, pi.getThinkingLevel());
|
|
116
|
+
publish();
|
|
117
|
+
});
|
|
118
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
119
|
+
activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, pi.getThinkingLevel());
|
|
120
|
+
publish();
|
|
121
|
+
});
|
|
122
|
+
pi.on("model_select", (event) => {
|
|
123
|
+
if (!applying) setEffort(effortForSelection(event.model.provider, event.model.id, pi.getThinkingLevel()));
|
|
124
|
+
});
|
|
125
|
+
pi.on("thinking_level_select", (event, ctx) => {
|
|
126
|
+
if (!applying) {
|
|
127
|
+
setEffort(effortForSelection(ctx.model?.provider, ctx.model?.id, event.level));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isModelEffort(value: string): value is ModelEffort {
|
|
133
|
+
return value === "low" || value === "medium" || value === "high";
|
|
134
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ModelEffort } from "../../shared/model-effort.ts";
|
|
3
|
+
|
|
4
|
+
export const EFFORT_STATE_TYPE = "tau.model-effort.state";
|
|
5
|
+
|
|
6
|
+
export interface EffortStateV1 {
|
|
7
|
+
v: 1;
|
|
8
|
+
effort: ModelEffort | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function effortState(effort: ModelEffort | undefined): EffortStateV1 {
|
|
12
|
+
return { v: 1, effort: effort ?? null };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function replayEffortState(branch: readonly SessionEntry[]): ModelEffort | undefined {
|
|
16
|
+
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
17
|
+
const entry = branch[index];
|
|
18
|
+
if (entry?.type !== "custom" || entry.customType !== EFFORT_STATE_TYPE) continue;
|
|
19
|
+
const data = entry.data;
|
|
20
|
+
if (!data || typeof data !== "object") continue;
|
|
21
|
+
const value = data as Record<string, unknown>;
|
|
22
|
+
if (value.v !== 1) continue;
|
|
23
|
+
if (value.effort === null) return undefined;
|
|
24
|
+
if (value.effort === "low" || value.effort === "medium" || value.effort === "high") return value.effort;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function nextEffort(effort: ModelEffort | undefined): ModelEffort {
|
|
30
|
+
if (effort === "low") return "medium";
|
|
31
|
+
if (effort === "medium") return "high";
|
|
32
|
+
return "low";
|
|
33
|
+
}
|
|
@@ -6,7 +6,8 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import { join, relative } from "node:path";
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
|
-
import { onTauEvent } from "../../shared/events.ts";
|
|
9
|
+
import { emitTauEvent, onTauEvent } from "../../shared/events.ts";
|
|
10
|
+
import { effortForSelection, type ModelEffort } from "../../shared/model-effort.ts";
|
|
10
11
|
import { loadTauExtensionSettings, updateTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
11
12
|
import footerSettings from "./settings.ts";
|
|
12
13
|
|
|
@@ -39,6 +40,8 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
39
40
|
let footerInstalled = false;
|
|
40
41
|
let requestRender: (() => void) | undefined;
|
|
41
42
|
let unsubscribeFooterItems: (() => void) | undefined;
|
|
43
|
+
let unsubscribeModelEffort: (() => void) | undefined;
|
|
44
|
+
let activeEffort: ModelEffort | undefined;
|
|
42
45
|
const gitByCwd = new Map<string, GitSummary | undefined>();
|
|
43
46
|
let gitRefresh: Promise<void> | undefined;
|
|
44
47
|
let dailyCost: number | undefined;
|
|
@@ -76,7 +79,11 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
76
79
|
const git = gitByCwd.get(currentCtx.cwd);
|
|
77
80
|
const model = currentCtx.model ? `${currentCtx.model.provider}/${currentCtx.model.id}` : "no-model";
|
|
78
81
|
const thinking = pi.getThinkingLevel();
|
|
79
|
-
const
|
|
82
|
+
const separator = theme.fg("dim", " • ");
|
|
83
|
+
const effort = effortText(theme, activeEffort);
|
|
84
|
+
const topLeft = [theme.fg("dim", gitText(git)), effort, theme.fg("dim", `${model} (${thinking})`)]
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.join(separator);
|
|
80
87
|
const sessionUsage = sessionCost(currentCtx);
|
|
81
88
|
const session = formatCost(sessionUsage.cost);
|
|
82
89
|
const daily = dailyCost === undefined ? "$?" : formatCost(dailyCost);
|
|
@@ -87,7 +94,7 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
87
94
|
.join(" • ");
|
|
88
95
|
|
|
89
96
|
return [
|
|
90
|
-
renderSplit(width,
|
|
97
|
+
renderSplit(width, topLeft, topRight),
|
|
91
98
|
renderSplit(width, theme.fg("dim", bottomLeft), theme.fg("dim", bottomRight)),
|
|
92
99
|
];
|
|
93
100
|
},
|
|
@@ -165,6 +172,10 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
165
172
|
items.set(item.id, next);
|
|
166
173
|
render();
|
|
167
174
|
});
|
|
175
|
+
unsubscribeModelEffort = onTauEvent(pi, "footer.model-effort", "tau:model-effort.changed", (state) => {
|
|
176
|
+
activeEffort = state.effort;
|
|
177
|
+
render();
|
|
178
|
+
});
|
|
168
179
|
|
|
169
180
|
pi.registerCommand(COMMAND, {
|
|
170
181
|
description: "Toggle Tau footer",
|
|
@@ -202,11 +213,22 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
202
213
|
pi.on("session_start", async (_event, ctx) => {
|
|
203
214
|
const settings = await loadTauExtensionSettings(ctx, footerSettings);
|
|
204
215
|
setEnabled(ctx, settings.enabled);
|
|
216
|
+
activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, pi.getThinkingLevel());
|
|
205
217
|
onStateChange(ctx);
|
|
218
|
+
emitTauEvent(pi, "tau:model-effort.snapshot.requested", {});
|
|
219
|
+
});
|
|
220
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
221
|
+
activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, pi.getThinkingLevel());
|
|
222
|
+
onStateChange(ctx, false);
|
|
223
|
+
});
|
|
224
|
+
pi.on("model_select", (_event, ctx) => {
|
|
225
|
+
activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, pi.getThinkingLevel());
|
|
226
|
+
rerender(ctx);
|
|
227
|
+
});
|
|
228
|
+
pi.on("thinking_level_select", (event, ctx) => {
|
|
229
|
+
activeEffort = effortForSelection(ctx.model?.provider, ctx.model?.id, event.level);
|
|
230
|
+
rerender(ctx);
|
|
206
231
|
});
|
|
207
|
-
pi.on("session_tree", (_event, ctx) => onStateChange(ctx, false));
|
|
208
|
-
pi.on("model_select", (_event, ctx) => rerender(ctx));
|
|
209
|
-
pi.on("thinking_level_select", (_event, ctx) => rerender(ctx));
|
|
210
232
|
pi.on("agent_start", (_event, ctx) => onStateChange(ctx, false));
|
|
211
233
|
pi.on("turn_end", (_event, ctx) => onStateChange(ctx));
|
|
212
234
|
pi.on("agent_end", (_event, ctx) => onStateChange(ctx));
|
|
@@ -214,6 +236,9 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
214
236
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
215
237
|
unsubscribeFooterItems?.();
|
|
216
238
|
unsubscribeFooterItems = undefined;
|
|
239
|
+
unsubscribeModelEffort?.();
|
|
240
|
+
unsubscribeModelEffort = undefined;
|
|
241
|
+
activeEffort = undefined;
|
|
217
242
|
requestRender = undefined;
|
|
218
243
|
activeCtx = undefined;
|
|
219
244
|
footerInstalled = false;
|
|
@@ -226,6 +251,11 @@ export default function footerExtension(pi: ExtensionAPI): void {
|
|
|
226
251
|
}
|
|
227
252
|
}
|
|
228
253
|
|
|
254
|
+
function effortText(theme: Theme, effort: ModelEffort | undefined): string {
|
|
255
|
+
if (!effort) return "";
|
|
256
|
+
return theme.fg("dim", theme.bold(effort.toUpperCase()));
|
|
257
|
+
}
|
|
258
|
+
|
|
229
259
|
async function saveEnabled(ctx: ExtensionContext, enabled: boolean): Promise<void> {
|
|
230
260
|
await updateTauExtensionSettings("global", ctx, footerSettings, (current) => ({ ...current, enabled }));
|
|
231
261
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Script Runner
|
|
2
2
|
|
|
3
|
-
Gives the agent a first-class `script_runner` tool for running Python and TypeScript instead of falling back to bash. The agent picks whichever language is more efficient for the task.
|
|
3
|
+
Gives the agent a first-class `script_runner` tool for running Python 3 and TypeScript instead of falling back to bash. The agent picks whichever language is more efficient for the task.
|
|
4
4
|
|
|
5
5
|
When a run fails, the tool keeps the script and returns a `scriptId`. The agent retries with targeted `{oldText, newText}` edits against what it just wrote instead of resending the whole script, saving output tokens and keeping duplicate scripts out of context. Only the source the agent already sent is referenced; no file path is exposed.
|
|
6
6
|
|
|
7
|
-
Languages are detected from the environment: Python via `python3
|
|
7
|
+
Languages are detected from the environment: Python 3 via `python3`, TypeScript via Node with `--experimental-strip-types` (Node 22.6 or newer). The tool registers only the languages actually available and is hidden from the prompt entirely when neither is present.
|
|
@@ -16,10 +16,10 @@ import {
|
|
|
16
16
|
import { Text } from "@earendil-works/pi-tui";
|
|
17
17
|
import { Type } from "typebox";
|
|
18
18
|
|
|
19
|
-
type Language = "
|
|
19
|
+
type Language = "python3" | "typescript";
|
|
20
20
|
|
|
21
21
|
interface Runtimes {
|
|
22
|
-
|
|
22
|
+
python3: string | undefined;
|
|
23
23
|
typescript: string | undefined;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -32,26 +32,23 @@ const TIMEOUT_MS = 120_000;
|
|
|
32
32
|
const MAX_STORED = 8;
|
|
33
33
|
|
|
34
34
|
function detectRuntimes(): Runtimes {
|
|
35
|
-
let
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} catch {
|
|
42
|
-
// runtime not installed
|
|
43
|
-
}
|
|
35
|
+
let python3: string | undefined;
|
|
36
|
+
try {
|
|
37
|
+
execFileSync("python3", ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
38
|
+
python3 = "python3";
|
|
39
|
+
} catch {
|
|
40
|
+
// runtime not installed
|
|
44
41
|
}
|
|
45
42
|
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
46
43
|
let typescript: string | undefined;
|
|
47
44
|
if (major > 22 || (major === 22 && minor >= 6)) {
|
|
48
45
|
typescript = process.execPath;
|
|
49
46
|
}
|
|
50
|
-
return {
|
|
47
|
+
return { python3, typescript };
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
function capitalize(lang: Language): string {
|
|
54
|
-
return lang === "
|
|
51
|
+
return lang === "python3" ? "Python 3" : "TypeScript";
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
function newScriptId(): string {
|
|
@@ -95,8 +92,8 @@ function renderEditsPreview(edits: ReadonlyArray<{ oldText: string; newText: str
|
|
|
95
92
|
|
|
96
93
|
export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
97
94
|
const runtimes = detectRuntimes();
|
|
98
|
-
const detected = (["
|
|
99
|
-
(lang): lang is Language => (lang === "
|
|
95
|
+
const detected = (["python3", "typescript"] as const).filter(
|
|
96
|
+
(lang): lang is Language => (lang === "python3" ? runtimes.python3 : runtimes.typescript) !== undefined,
|
|
100
97
|
);
|
|
101
98
|
if (detected.length === 0) return;
|
|
102
99
|
|
|
@@ -115,9 +112,9 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
|
115
112
|
}
|
|
116
113
|
|
|
117
114
|
function resolveCommand(language: Language): string {
|
|
118
|
-
if (language === "
|
|
119
|
-
const cmd = runtimes.
|
|
120
|
-
if (!cmd) throw new Error("Python is not available on this machine.");
|
|
115
|
+
if (language === "python3") {
|
|
116
|
+
const cmd = runtimes.python3;
|
|
117
|
+
if (!cmd) throw new Error("Python 3 is not available on this machine.");
|
|
121
118
|
return cmd;
|
|
122
119
|
}
|
|
123
120
|
const cmd = runtimes.typescript;
|
|
@@ -139,9 +136,9 @@ export default function scriptRunnerExtension(pi: ExtensionAPI): void {
|
|
|
139
136
|
signal: AbortSignal | undefined,
|
|
140
137
|
): Promise<ExecResult> {
|
|
141
138
|
const dir = await ensureTempDir();
|
|
142
|
-
const file = join(dir, language === "
|
|
139
|
+
const file = join(dir, language === "python3" ? "_run.py" : "_run.ts");
|
|
143
140
|
await writeFile(file, source, "utf8");
|
|
144
|
-
const args = language === "
|
|
141
|
+
const args = language === "python3" ? [file] : ["--experimental-strip-types", file];
|
|
145
142
|
const result = await pi.exec(command, args, { cwd, signal, timeout: TIMEOUT_MS });
|
|
146
143
|
return {
|
|
147
144
|
...result,
|
|
@@ -36,7 +36,11 @@ Adds `/context` to set branch-local reusable repository work scopes from `.pi/co
|
|
|
36
36
|
|
|
37
37
|
## working-memory
|
|
38
38
|
|
|
39
|
-
Gives agent `working_memory` for selective hard checkpoints.
|
|
39
|
+
Gives agent `working_memory` for selective hard checkpoints. Every checkpoint retains at least one useful user message or visible assistant text. Requested source files return as structural outlines, deferred files remain cheap conditional reminders, and a continuation note carries conclusions extracted from exploration. Tool history and full file reads leave future model input without changing saved session. Advisory reminders begin at 40k active-context tokens. Run `/prune` to request reassessment manually.
|
|
40
|
+
|
|
41
|
+
## effort
|
|
42
|
+
|
|
43
|
+
Adds `/effort [low|medium|high]` 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.
|
|
40
44
|
|
|
41
45
|
## explore
|
|
42
46
|
|
|
@@ -88,7 +92,7 @@ Supplies the agent with the current local date and an initial root directory sna
|
|
|
88
92
|
|
|
89
93
|
## script-runner
|
|
90
94
|
|
|
91
|
-
Gives the agent a first-class `script_runner` tool to execute Python and TypeScript instead of bash. On failure it returns a `scriptId`; the agent retries with targeted `{oldText,newText}` edits against the script it already wrote rather than resending the whole script. Languages are detected from the environment (Python via `python3
|
|
95
|
+
Gives the agent a first-class `script_runner` tool to execute Python 3 and TypeScript instead of bash. On failure it returns a `scriptId`; the agent retries with targeted `{oldText,newText}` edits against the script it already wrote rather than resending the whole script. Languages are detected from the environment (Python 3 via `python3`; TypeScript via Node `--experimental-strip-types`, Node 22.6+). The tool registers only available languages and is hidden from the prompt if neither is present.
|
|
92
96
|
|
|
93
97
|
## silent-command-runner
|
|
94
98
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Working Memory gives agent selective checkpoints without changing saved conversation.
|
|
4
4
|
|
|
5
|
-
`working_memory`
|
|
5
|
+
`working_memory` retains at least one referenced user message or visible assistant text. Keep task framing, constraints, decisions, and active work chain; continuation and file tiers support that conversation context. It has three file tiers:
|
|
6
6
|
|
|
7
7
|
- Auto-read full source into next turn when agent needs its body to decide, edit, or debug.
|
|
8
8
|
- Carry a structural outline when symbols and locations will support a later scoped read.
|
|
@@ -23,7 +23,7 @@ export const workingMemoryParameters = Type.Object(
|
|
|
23
23
|
description:
|
|
24
24
|
"Working note for resuming mid-task. Carry durable decisions, concrete findings, live reasoning, unresolved questions, remaining work, and next action; include enough detail to continue without rereading discarded results.",
|
|
25
25
|
}),
|
|
26
|
-
keep: Type.Array(Type.String({ minLength: 3, maxLength: 100 }), { maxItems: 100 }),
|
|
26
|
+
keep: Type.Array(Type.String({ minLength: 3, maxLength: 100 }), { minItems: 1, maxItems: 100 }),
|
|
27
27
|
readFiles: Type.Array(PATH, {
|
|
28
28
|
maxItems: 12,
|
|
29
29
|
description:
|
|
@@ -91,6 +91,9 @@ export async function executeWorkingMemory(options: ExecuteWorkingMemoryOptions)
|
|
|
91
91
|
})
|
|
92
92
|
.sort((left, right) => left.order - right.order);
|
|
93
93
|
const retainedRefs = retained.map((unit) => unit.ref);
|
|
94
|
+
if (retained.length === 0) {
|
|
95
|
+
throw new Error("working_memory requires at least one valid keep reference");
|
|
96
|
+
}
|
|
94
97
|
const warnings = requestedRefs
|
|
95
98
|
.filter((ref) => !catalog.has(ref))
|
|
96
99
|
.map((ref) => `${ref}: memory reference is unavailable and was not retained`);
|
|
@@ -22,7 +22,7 @@ import { replayWorkingMemoryState, WORKING_MEMORY_TOOL, type WorkingMemoryCheckp
|
|
|
22
22
|
const NUDGE_TYPE = "tau.working-memory.nudge";
|
|
23
23
|
const BASELINE_TYPE = "tau.working-memory.nudge-baseline";
|
|
24
24
|
const TOOL_DESCRIPTION =
|
|
25
|
-
"Create a selective hard checkpoint for future model context.
|
|
25
|
+
"Create a selective hard checkpoint for future model context. Retain one or more valuable user or visible assistant messages, auto-read full source or carry file structure as needed, defer conditionally relevant files, and distill exploration findings into one compact continuation note.";
|
|
26
26
|
|
|
27
27
|
interface NudgeState {
|
|
28
28
|
anchorToolCallId: string | undefined;
|
|
@@ -99,6 +99,7 @@ export default function workingMemoryExtension(pi: ExtensionAPI): void {
|
|
|
99
99
|
promptGuidelines: [
|
|
100
100
|
"Use working_memory when stale evidence has accumulated or a memory reminder asks for reassessment; continue coherent exploration when current evidence remains useful.",
|
|
101
101
|
"A hidden working-memory reference catalog provides keep refs only for user messages and visible assistant text. Tool calls, tool results, hidden reasoning, and framework messages cannot be retained.",
|
|
102
|
+
"Every checkpoint must retain at least one relevant referenced message. Keep task framing, constraints, decisions, and immediate work chain when needed; continuation and file tiers supplement retained conversation and cannot replace it.",
|
|
102
103
|
"Choose one file tier: readFiles auto-reads source into the next turn when its body is needed; outlineFiles carries symbols and locations for later scoped inspection; deferFiles records inactive conditional paths. Do not read a file merely to decide whether to outline it.",
|
|
103
104
|
"Choose readFiles instead of outlineFiles when next work will require the complete file. Do not list a path in more than one file tier.",
|
|
104
105
|
"Use continuation as a working note for resuming mid-task. Carry durable decisions, concrete findings, live reasoning, unresolved questions, remaining work, and next action in as much detail as needed to continue without rereading discarded results. Do not make it a user-facing status update or narrate the checkpoint.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shanepadgett/tau-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.1",
|
|
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.0",
|
|
38
|
-
"@shanepadgett/tau-tui": "0.
|
|
38
|
+
"@shanepadgett/tau-tui": "0.33.1",
|
|
39
39
|
"@vscode/tree-sitter-wasm": "0.3.1",
|
|
40
40
|
"image-size": "2.0.2",
|
|
41
41
|
"smol-toml": "1.7.0",
|
package/shared/events.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ModelEffort } from "./model-effort.ts";
|
|
2
3
|
import type { OutlineInjectionRequest, OutlineInjectionResponse } from "./outline-injection.js";
|
|
3
4
|
import type { ToolRowVisualState } from "./tool-row-state.js";
|
|
4
5
|
|
|
@@ -46,6 +47,10 @@ export type TauAgentEvents = {
|
|
|
46
47
|
text?: string;
|
|
47
48
|
priority?: number;
|
|
48
49
|
};
|
|
50
|
+
"tau:model-effort.changed": {
|
|
51
|
+
effort: ModelEffort | undefined;
|
|
52
|
+
};
|
|
53
|
+
"tau:model-effort.snapshot.requested": Record<string, never>;
|
|
49
54
|
"tau:tool-row-state.set": {
|
|
50
55
|
rowId: string;
|
|
51
56
|
state?: ToolRowVisualState;
|
package/shared/model-effort.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
1
|
+
import type { Api, Model, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { resolveCandidates } from "./model-fallback/index.ts";
|
|
4
4
|
import type { ModelCandidate } from "./model-fallback/types.ts";
|
|
@@ -6,33 +6,111 @@ import type { ModelCandidate } from "./model-fallback/types.ts";
|
|
|
6
6
|
export type ModelEffort = "low" | "medium" | "high";
|
|
7
7
|
|
|
8
8
|
interface ModelPreference {
|
|
9
|
-
provider: string;
|
|
10
9
|
model: string;
|
|
11
10
|
reasoning: ThinkingLevel;
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
interface ProviderPreference {
|
|
14
|
+
provider: string;
|
|
15
|
+
models: readonly ModelPreference[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface EffortProviderCandidates {
|
|
19
|
+
provider: string;
|
|
20
|
+
label: string;
|
|
21
|
+
candidates: ReadonlyArray<{
|
|
22
|
+
model: Model<Api>;
|
|
23
|
+
reasoning: ThinkingLevel;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MODEL_PREFERENCES: Record<ModelEffort, readonly ProviderPreference[]> = {
|
|
15
28
|
low: [
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
{
|
|
30
|
+
provider: "openai-codex",
|
|
31
|
+
models: [
|
|
32
|
+
{ model: "gpt-5.6-luna", reasoning: "high" },
|
|
33
|
+
{ model: "gpt-5.5", reasoning: "low" },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{ provider: "xai", models: [{ model: "grok-4.5", reasoning: "medium" }] },
|
|
37
|
+
{ provider: "anthropic", models: [{ model: "claude-haiku-4-5", reasoning: "high" }] },
|
|
19
38
|
],
|
|
20
39
|
medium: [
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
{
|
|
41
|
+
provider: "openai-codex",
|
|
42
|
+
models: [
|
|
43
|
+
{ model: "gpt-5.6-terra", reasoning: "high" },
|
|
44
|
+
{ model: "gpt-5.5", reasoning: "medium" },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{ provider: "xai", models: [{ model: "grok-4.5", reasoning: "high" }] },
|
|
48
|
+
{ provider: "anthropic", models: [{ model: "claude-sonnet-5", reasoning: "high" }] },
|
|
24
49
|
],
|
|
25
50
|
high: [
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
51
|
+
{
|
|
52
|
+
provider: "openai-codex",
|
|
53
|
+
models: [
|
|
54
|
+
{ model: "gpt-5.6-sol", reasoning: "high" },
|
|
55
|
+
{ model: "gpt-5.5", reasoning: "high" },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{ provider: "xai", models: [{ model: "grok-4.5", reasoning: "high" }] },
|
|
59
|
+
{
|
|
60
|
+
provider: "anthropic",
|
|
61
|
+
models: [
|
|
62
|
+
{ model: "claude-opus-5", reasoning: "high" },
|
|
63
|
+
{ model: "claude-opus-4-8", reasoning: "high" },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
29
66
|
],
|
|
30
67
|
};
|
|
31
68
|
|
|
69
|
+
export function effortForSelection(
|
|
70
|
+
provider: string | undefined,
|
|
71
|
+
model: string | undefined,
|
|
72
|
+
reasoning: string | undefined,
|
|
73
|
+
): ModelEffort | undefined {
|
|
74
|
+
if (!provider || !model || !reasoning) return undefined;
|
|
75
|
+
for (const effort of ["high", "medium", "low"] as const) {
|
|
76
|
+
const preference = MODEL_PREFERENCES[effort].find((item) => item.provider === provider);
|
|
77
|
+
if (preference?.models.some((item) => item.model === model && item.reasoning === reasoning)) return effort;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resolveEffortProviders(
|
|
83
|
+
ctx: Pick<ExtensionContext, "modelRegistry">,
|
|
84
|
+
effort: ModelEffort,
|
|
85
|
+
): EffortProviderCandidates[] {
|
|
86
|
+
const available = new Map(ctx.modelRegistry.getAvailable().map((model) => [`${model.provider}/${model.id}`, model]));
|
|
87
|
+
return MODEL_PREFERENCES[effort].flatMap((preference) => {
|
|
88
|
+
const candidates = preference.models.flatMap(({ model, reasoning }) => {
|
|
89
|
+
const availableModel = available.get(`${preference.provider}/${model}`);
|
|
90
|
+
return availableModel ? [{ model: availableModel, reasoning }] : [];
|
|
91
|
+
});
|
|
92
|
+
return candidates.length
|
|
93
|
+
? [
|
|
94
|
+
{
|
|
95
|
+
provider: preference.provider,
|
|
96
|
+
label: ctx.modelRegistry.getProviderDisplayName(preference.provider),
|
|
97
|
+
candidates,
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
: [];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
32
104
|
export function resolveEffortCandidates(
|
|
33
105
|
ctx: Pick<ExtensionContext, "modelRegistry" | "model" | "cwd" | "isProjectTrusted">,
|
|
34
106
|
effort: ModelEffort,
|
|
35
107
|
includeParentModel: boolean,
|
|
36
108
|
): Promise<ModelCandidate[]> {
|
|
37
|
-
return resolveCandidates(
|
|
109
|
+
return resolveCandidates(
|
|
110
|
+
ctx,
|
|
111
|
+
MODEL_PREFERENCES[effort].flatMap((preference) =>
|
|
112
|
+
preference.models.map((model) => ({ provider: preference.provider, ...model })),
|
|
113
|
+
),
|
|
114
|
+
includeParentModel,
|
|
115
|
+
);
|
|
38
116
|
}
|