@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.
@@ -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
+ }
@@ -14,7 +14,7 @@ Replaces Pi’s default footer with Tau’s compact two-line footer.
14
14
  ## Layout
15
15
 
16
16
  ```text
17
- git • model • thinking S $0.18 · D $2.43
17
+ git • EFFORT • provider/model (thinking) S $0.18 · D $2.43
18
18
  cwd • session footer items
19
19
  ```
20
20
 
@@ -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 topLeft = [gitText(git), `${model} (${thinking})`].filter(Boolean).join(" • ");
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, theme.fg("dim", topLeft), topRight),
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`/`python`, 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.
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 = "python" | "typescript";
19
+ type Language = "python3" | "typescript";
20
20
 
21
21
  interface Runtimes {
22
- python: string | undefined;
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 python: string | undefined;
36
- for (const cmd of ["python3", "python"] as const) {
37
- try {
38
- execFileSync(cmd, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
39
- python = cmd;
40
- break;
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 { python, typescript };
47
+ return { python3, typescript };
51
48
  }
52
49
 
53
50
  function capitalize(lang: Language): string {
54
- return lang === "python" ? "Python" : "TypeScript";
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 = (["python", "typescript"] as const).filter(
99
- (lang): lang is Language => (lang === "python" ? runtimes.python : runtimes.typescript) !== undefined,
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 === "python") {
119
- const cmd = runtimes.python;
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 === "python" ? "_run.py" : "_run.ts");
139
+ const file = join(dir, language === "python3" ? "_run.py" : "_run.ts");
143
140
  await writeFile(file, source, "utf8");
144
- const args = language === "python" ? [file] : ["--experimental-strip-types", file];
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. Model-only references identify useful user messages and 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.
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`/`python`; 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.
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` keeps referenced user messages and visible assistant text. It has three file tiers:
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. Keep valuable user and 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.";
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.",
@@ -57,6 +57,7 @@ export function parseWorkingMemoryDetails(value: unknown): WorkingMemoryCheckpoi
57
57
  const deferredFiles = parseDeferredFiles(value.deferredFiles);
58
58
  if (
59
59
  !retainedRefs ||
60
+ retainedRefs.length === 0 ||
60
61
  !prunedRowIds ||
61
62
  !readFiles ||
62
63
  !warnings ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.32.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.32.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;
@@ -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
- const MODEL_PREFERENCES: Record<ModelEffort, readonly ModelPreference[]> = {
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
- { provider: "openai-codex", model: "gpt-5.6-luna", reasoning: "high" },
17
- { provider: "xai", model: "grok-4.5", reasoning: "medium" },
18
- { provider: "anthropic", model: "claude-haiku-4-5", reasoning: "high" },
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
- { provider: "openai-codex", model: "gpt-5.6-terra", reasoning: "high" },
22
- { provider: "xai", model: "grok-4.5", reasoning: "high" },
23
- { provider: "anthropic", model: "claude-sonnet-5", reasoning: "high" },
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
- { provider: "openai-codex", model: "gpt-5.6-sol", reasoning: "high" },
27
- { provider: "xai", model: "grok-4.5", reasoning: "high" },
28
- { provider: "anthropic", model: "claude-opus-5", reasoning: "high" },
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(ctx, MODEL_PREFERENCES[effort], includeParentModel);
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
  }