@shanepadgett/tau-agent 0.32.0 → 0.33.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/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/tau-help/help.md +5 -1
- 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
|
}
|
|
@@ -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
|
|
|
@@ -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.0",
|
|
4
4
|
"description": "Tau is a custom agentic harness built with pi extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@ast-grep/wasm": "0.45.0",
|
|
38
|
-
"@shanepadgett/tau-tui": "0.
|
|
38
|
+
"@shanepadgett/tau-tui": "0.33.0",
|
|
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
|
}
|