@shanepadgett/tau-agent 0.44.1 → 0.45.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.
Files changed (42) hide show
  1. package/docs/extending-tau-agent.md +3 -3
  2. package/extensions/appshot/index.ts +3 -0
  3. package/extensions/aside/index.ts +3 -11
  4. package/extensions/attention/README.md +2 -4
  5. package/extensions/attention/index.ts +2 -40
  6. package/extensions/auto-name/index.ts +5 -8
  7. package/extensions/cache-diagnostics/index.ts +1 -1
  8. package/extensions/codex-priority/README.md +7 -0
  9. package/extensions/codex-priority/index.ts +95 -0
  10. package/extensions/compaction/README.md +5 -0
  11. package/extensions/compaction/index.ts +47 -0
  12. package/extensions/cost-report/README.md +1 -1
  13. package/extensions/cost-report/analyze.ts +11 -60
  14. package/extensions/cost-report/html.ts +1 -37
  15. package/extensions/cost-report/types.ts +0 -9
  16. package/extensions/handoff/index.ts +1 -1
  17. package/extensions/image-gen/index.ts +1 -0
  18. package/extensions/review/README.md +1 -1
  19. package/extensions/run-summary/README.md +1 -1
  20. package/extensions/run-summary/index.ts +6 -24
  21. package/extensions/runtime-context/README.md +1 -1
  22. package/extensions/silent-command-runner/index.ts +38 -69
  23. package/extensions/soul/README.md +3 -5
  24. package/extensions/soul/index.ts +59 -135
  25. package/extensions/soul/prompt.ts +2 -0
  26. package/extensions/tau-help/help.md +10 -2
  27. package/extensions/tool-approval/index.ts +8 -28
  28. package/extensions/tool-loader/README.md +5 -5
  29. package/extensions/tool-loader/index.ts +10 -222
  30. package/extensions/web/codesearch.ts +1 -0
  31. package/extensions/web/webfetch.ts +1 -0
  32. package/extensions/web/websearch.ts +1 -0
  33. package/package.json +2 -2
  34. package/shared/events.ts +9 -22
  35. package/shared/model-effort.ts +12 -8
  36. package/shared/model-fallback/index.ts +12 -29
  37. package/shared/model-fallback/types.ts +1 -5
  38. package/shared/prompt-contributions.ts +0 -2
  39. package/src/tool-loading/index.ts +5 -42
  40. package/extensions/soul/context.ts +0 -115
  41. package/extensions/soul/state.ts +0 -114
  42. package/extensions/soul/tools.ts +0 -31
@@ -14,6 +14,6 @@ Choose one focused review type:
14
14
  - `Architecture` reconsiders ownership, boundaries, reuse, and overall structure.
15
15
  - `Correctness` checks concrete runtime bugs and failure paths after accepting the architecture.
16
16
 
17
- Then choose which logged-in provider runs the review. OpenAI Codex uses `gpt-5.6-sol` and Anthropic uses `claude-opus-5`, both at high thinking. Only providers you are logged in to appear. With no logged-in provider, the review uses the current model.
17
+ Then choose which logged-in provider runs the review. Models come from Tau's shared `deep` effort tier: OpenAI Codex uses `gpt-6-astra` and Anthropic uses `claude-opus-5-5`, both at medium thinking. Only providers you are logged in to appear. With no logged-in provider, the review uses the current model.
18
18
 
19
19
  Tau writes each result as Markdown under `.pi/tau/reviews/`. Review results do not enter the parent agent context. Reference the Markdown file later when you want an agent to use it.
@@ -1,5 +1,5 @@
1
1
  # Run Summary
2
2
 
3
- Run Summary adds a compact marker after the agent settles with no automatic continuation pending. It shows wall time and model cost across the full continuation chain. Older markers also display the delegated cost recorded for those runs.
3
+ Run Summary adds a compact marker after the agent settles with no automatic continuation pending. It shows wall time and model cost across the full continuation chain.
4
4
 
5
5
  The marker is stored as a display-only session entry. It does not enter model context or trigger another agent turn.
@@ -1,4 +1,3 @@
1
- import type { AssistantMessage } from "@earendil-works/pi-ai";
2
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
2
  import { Marker } from "@shanepadgett/tau-tui";
4
3
 
@@ -9,11 +8,6 @@ interface RunSummary {
9
8
  runCost: number;
10
9
  }
11
10
 
12
- interface PreviousRunSummary extends RunSummary {
13
- subagentCost: number;
14
- totalCost: number;
15
- }
16
-
17
11
  export default function runSummaryExtension(pi: ExtensionAPI): void {
18
12
  let startedAt: number | undefined;
19
13
  let runCost = 0;
@@ -25,13 +19,7 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
25
19
  theme,
26
20
  state: "muted",
27
21
  label: "Run complete:",
28
- parts: [
29
- `Wall ${formatDuration(summary.wallMs)}`,
30
- `Run ${formatCost(summary.runCost)}`,
31
- ...("subagentCost" in summary
32
- ? [`Subagents ${formatCost(summary.subagentCost)}`, `Total ${formatCost(summary.totalCost)}`]
33
- : []),
34
- ],
22
+ parts: [`Wall ${formatDuration(summary.wallMs)}`, `Run ${formatCost(summary.runCost)}`],
35
23
  });
36
24
  });
37
25
 
@@ -46,7 +34,10 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
46
34
 
47
35
  pi.on("agent_end", (event) => {
48
36
  for (const message of event.messages) {
49
- if (message.role === "assistant") runCost += finiteNonNegative((message as AssistantMessage).usage.cost.total);
37
+ // Tool results carry the usage of model calls a tool made.
38
+ if (message.role === "assistant") runCost += finiteNonNegative(message.usage.cost.total);
39
+ else if (message.role === "toolResult" && message.usage)
40
+ runCost += finiteNonNegative(message.usage.cost.total);
50
41
  }
51
42
  });
52
43
 
@@ -62,19 +53,10 @@ export default function runSummaryExtension(pi: ExtensionAPI): void {
62
53
  });
63
54
  }
64
55
 
65
- function readRunSummary(value: unknown): RunSummary | PreviousRunSummary | undefined {
56
+ function readRunSummary(value: unknown): RunSummary | undefined {
66
57
  if (!value || typeof value !== "object") return undefined;
67
58
  const record = value as Record<string, unknown>;
68
59
  if (![record.wallMs, record.runCost].every(isFiniteNonNegative)) return undefined;
69
- if ("subagentCost" in record || "totalCost" in record) {
70
- if (![record.subagentCost, record.totalCost].every(isFiniteNonNegative)) return undefined;
71
- return {
72
- wallMs: record.wallMs as number,
73
- runCost: record.runCost as number,
74
- subagentCost: record.subagentCost as number,
75
- totalCost: record.totalCost as number,
76
- };
77
- }
78
60
  return {
79
61
  wallMs: record.wallMs as number,
80
62
  runCost: record.runCost as number,
@@ -1,5 +1,5 @@
1
1
  # Runtime Context
2
2
 
3
- Supplies Soul with the local date and root directory snapshot. Both are captured together when a prompt baseline is created and refreshed after successful compaction. Reload, resume, and midnight do not change an existing baseline.
3
+ Supplies Soul with the local date and root directory snapshot. Both are captured on the first prompt of a session and refreshed after successful compaction. Reload, resume, and midnight do not change them.
4
4
 
5
5
  After changing this extension, run `/reload` before testing the new behavior.
@@ -2,7 +2,6 @@ import { readdir, stat } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import { type ExecResult, type ExtensionAPI, keyText, type Theme } from "@earendil-works/pi-coding-agent";
4
4
  import { Box, Text } from "@earendil-works/pi-tui";
5
- import { emitTauEvent } from "../../shared/events.ts";
6
5
  import { registerPromptSource } from "../../shared/prompt-contributions.ts";
7
6
  import { matchGlob, posixPath } from "../../shared/glob.ts";
8
7
  import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
@@ -82,16 +81,6 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
82
81
  let turnPaths = new Set<string>();
83
82
  let abortController: AbortController | undefined;
84
83
  let sessionActive = false;
85
- let chainActive = false;
86
- let attentionHoldSequence = 0;
87
- let attentionHoldId: string | undefined;
88
-
89
- function finalizeChain(): void {
90
- chainActive = false;
91
- const holdId = attentionHoldId;
92
- attentionHoldId = undefined;
93
- if (holdId) emitTauEvent(pi, "tau:attention.hold.release", { id: holdId, disposition: "notify" });
94
- }
95
84
 
96
85
  pi.registerMessageRenderer<FailureDetails>(MESSAGE_TYPE, (message, { expanded }, theme) =>
97
86
  renderFailure(asFailureDetails(message.details), expanded, theme),
@@ -102,9 +91,6 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
102
91
  sessionActive = true;
103
92
  turnStart = Date.now();
104
93
  turnPaths = new Set();
105
- chainActive = false;
106
- attentionHoldSequence = 0;
107
- attentionHoldId = undefined;
108
94
  });
109
95
 
110
96
  registerPromptSource(pi, {
@@ -119,41 +105,43 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
119
105
  });
120
106
 
121
107
  pi.on("agent_start", async (_event, ctx) => {
122
- const startingChain = !chainActive;
123
- chainActive = true;
124
- if (!settings.enabled || settings.commands.length === 0) {
125
- if (startingChain) {
126
- turnStart = Date.now();
127
- turnPaths = new Set();
128
- }
129
- return;
130
- }
131
- if (!startingChain) return;
132
- attentionHoldId = `silent-command-runner:${++attentionHoldSequence}`;
133
- emitTauEvent(pi, "tau:attention.hold.acquire", { id: attentionHoldId });
134
108
  turnStart = Date.now();
135
- const projectRoot = await resolveProjectRoot(ctx.cwd);
136
- turnPaths = new Set(await walkFiles(projectRoot));
109
+ turnPaths =
110
+ settings.enabled && settings.commands.length > 0
111
+ ? new Set(await walkFiles(await resolveProjectRoot(ctx.cwd)))
112
+ : new Set();
137
113
  });
138
114
 
139
- pi.on("agent_end", async (event, ctx) => {
140
- if (hasAbortedAssistantMessage(event.messages)) return;
115
+ // The last boundary before the run settles: failures continue the run instead of starting a new one.
116
+ pi.on("agent_before_settle", async (event, ctx) => {
117
+ if (event.outcome !== "completed") return undefined;
141
118
  try {
142
- await runChangedCommands(ctx.cwd, turnStart, ctx.ui.notify);
119
+ const failures = await runChangedCommands(ctx.cwd, ctx.ui.notify);
120
+ if (!failures || failures.length === 0) return undefined;
121
+ return {
122
+ entries: [
123
+ ...event.entries,
124
+ {
125
+ type: "custom_message" as const,
126
+ customType: MESSAGE_TYPE,
127
+ content: formatAgentMessage(failures),
128
+ display: true,
129
+ details: { failed: [...failures] } satisfies FailureDetails,
130
+ },
131
+ ],
132
+ continue: true,
133
+ };
143
134
  } catch (error: unknown) {
144
135
  ctx.ui.notify(`silent-command-runner: ${errorMessage(error)}`, "error");
136
+ return undefined;
145
137
  }
146
138
  });
147
139
 
148
- pi.on("agent_settled", finalizeChain);
149
-
150
140
  pi.on("session_shutdown", () => {
151
141
  sessionActive = false;
152
142
  abortController?.abort();
153
143
  abortController = undefined;
154
144
  turnPaths = new Set();
155
- chainActive = false;
156
- attentionHoldId = undefined;
157
145
  });
158
146
 
159
147
  async function collectCommandFailures(
@@ -183,37 +171,16 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
183
171
  return failures;
184
172
  }
185
173
 
186
- function reportCommandResults(
187
- changed: readonly CommandConfig[],
188
- failures: readonly FailedCommandDetails[],
189
- notify: (message: string, type?: "info" | "warning" | "error") => void,
190
- ): void {
191
- const failedNames = new Set(failures.map((failure) => failure.name));
192
- const passed = changed.filter((command) => !failedNames.has(command.name));
193
- if (passed.length > 0) notify(`silent-command-runner: passed ${formatCommandNames(passed)}`, "info");
194
- if (failures.length === 0) return;
195
- pi.sendMessage<FailureDetails>(
196
- {
197
- customType: MESSAGE_TYPE,
198
- content: formatAgentMessage(failures),
199
- display: true,
200
- details: { failed: [...failures] },
201
- },
202
- { deliverAs: "followUp" },
203
- );
204
- }
205
-
206
174
  async function runChangedCommands(
207
175
  cwd: string,
208
- turnStart: number,
209
176
  notify: (message: string, type?: "info" | "warning" | "error") => void,
210
- ): Promise<void> {
211
- if (!settings.enabled || settings.commands.length === 0) return;
177
+ ): Promise<FailedCommandDetails[] | undefined> {
178
+ if (!settings.enabled || settings.commands.length === 0) return undefined;
212
179
 
213
180
  const projectRoot = await resolveProjectRoot(cwd);
214
181
  const paths = await walkFiles(projectRoot);
215
182
  const changed = await scanChangedCommands(projectRoot, settings.commands, paths, turnPaths, turnStart);
216
- if (!sessionActive || changed.length === 0) return;
183
+ if (!sessionActive || changed.length === 0) return undefined;
217
184
 
218
185
  notify(
219
186
  changed.length === 1
@@ -223,8 +190,16 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
223
190
  );
224
191
 
225
192
  const failures = await collectCommandFailures(projectRoot, changed, notify);
226
- if (failures === undefined || !sessionActive) return;
227
- reportCommandResults(changed, failures, notify);
193
+ if (failures === undefined || !sessionActive) return undefined;
194
+ const failedNames = new Set(failures.map((failure) => failure.name));
195
+ const passed = changed.filter((command) => !failedNames.has(command.name));
196
+ if (passed.length > 0) notify(`silent-command-runner: passed ${formatCommandNames(passed)}`, "info");
197
+ if (failures.length > 0) {
198
+ // Only files the agent changes after this report can trigger another check and continuation.
199
+ turnStart = Date.now();
200
+ turnPaths = new Set(paths);
201
+ }
202
+ return failures;
228
203
  }
229
204
  }
230
205
 
@@ -255,7 +230,7 @@ function normalizeSettings(value: typeof silentCommandRunnerSettings.defaults):
255
230
 
256
231
  function formatSilentCheckPrompt(commands: readonly CommandConfig[]): string {
257
232
  return [
258
- "Do not manually run the commands listed below. They run automatically after matching changes. After fixing a reported failure, end the turn so they can run again. Targeted checks outside this list remain allowed when needed.",
233
+ "Do not manually run the commands listed below. They run automatically after matching changes. Complete the work and finish normally; treat it as correct unless a failure is reported. Do not announce pending validation, hedge completion because of these commands, or tell the user that checks will run or rerun. If a failure is reported, fix it and describe the correction, then finish normally. Do not claim commands passed without evidence. Targeted checks outside this list remain allowed when needed.",
259
234
  "Automatic commands:",
260
235
  ...commands.map(formatSilentCheckCommand),
261
236
  ].join("\n");
@@ -271,12 +246,6 @@ function formatSilentCheckCommand(command: CommandConfig): string {
271
246
  ].join("\n");
272
247
  }
273
248
 
274
- function hasAbortedAssistantMessage(messages: readonly unknown[]): boolean {
275
- return messages.some(
276
- (message) => isRecord(message) && message.role === "assistant" && message.stopReason === "aborted",
277
- );
278
- }
279
-
280
249
  async function scanChangedCommands(
281
250
  projectRoot: string,
282
251
  commands: readonly CommandConfig[],
@@ -403,7 +372,7 @@ function formatAgentMessage(failures: readonly FailedCommandDetails[]): string {
403
372
  return [
404
373
  `silent-command-runner failed: ${failures.length} command${failures.length === 1 ? "" : "s"}`,
405
374
  ...failures.map(formatAgentFailure),
406
- "**Do not** rerun these checks. They will run automatically after your fix.",
375
+ "Fix the reported failures without manually rerunning these commands. Describe the correction and finish normally; do not announce validation or reruns.",
407
376
  ].join("\n\n");
408
377
  }
409
378
 
@@ -2,10 +2,8 @@
2
2
 
3
3
  Soul supplies Tau's system prompt: communication, discussion, planning, execution, and coding guidance. It is always on.
4
4
 
5
- Soul captures tools, skills, project instructions, documentation, and environment context in a saved baseline. Reload and resume reuse it. Successful compaction captures a fresh baseline, including a new date and directory listing.
5
+ Soul adds Pi documentation pointers, tool guidance, and the context that other Tau extensions supply, such as the local date, directory snapshot, and automatic-check instructions. The date and directory snapshot are captured on the first prompt and again after successful compaction, so they stay fixed across turns, reload, resume, and tree navigation.
6
6
 
7
- Changes to available agents, automatic checks, approval guidance, and loaded tools are supplied as saved context updates. Earlier instructions and updates keep their positions instead of being rewritten each turn.
7
+ Everything else is read each turn. When an instruction changes, such as an edited `AGENTS.md` after `/reload`, a changed automatic-check configuration, or a different set of active tools, Pi appends the updated section without rewriting earlier instructions. Models that cannot take later system messages receive the change in the leading instructions, which costs one cache miss on the next request.
8
8
 
9
- Tools that cannot be loaded without changing the cached prefix wait for compaction. Tool execution permissions still take effect immediately. Soul reports incompatible prompt replacements or changes to previously sent history rather than silently replacing its baseline.
10
-
11
- Run `/reload` after changing this extension. The first request after installation captures the new Soul baseline; later instruction edits take effect after compaction.
9
+ Run `/reload` after changing this extension.
@@ -1,144 +1,68 @@
1
- import { getCurrentTools, toToolDeclaration } from "@earendil-works/pi-ai";
2
- import { createHash } from "node:crypto";
3
- import type { BuildSystemPromptOptions, ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
- import { emitTauEvent, onTauEventImmediately } from "../../shared/events.ts";
5
- import { readPromptValues, renderBaseline, renderUpdate } from "./context.ts";
6
- import {
7
- admittedTools,
8
- BASELINE_TYPE,
9
- projectPrompt,
10
- restorePrompt,
11
- UPDATE_TYPE,
12
- type SavedBaseline,
13
- type SavedUpdate,
14
- } from "./state.ts";
15
- import { toolChangeReason } from "./tools.ts";
1
+ import { getDocsPath, getExamplesPath, getReadmePath, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { emitTauEvent } from "../../shared/events.ts";
3
+ import { collectPromptSources } from "../../shared/prompt-contributions.ts";
4
+ import { FIXED_INSTRUCTIONS } from "./prompt.ts";
16
5
 
17
- const CHECKPOINT_TYPE = "tau.soul.prefix";
18
- interface PrefixCheckpoint {
19
- baselineEntryId: string;
20
- count: number;
21
- hash: string;
22
- }
6
+ const CAPTURE_TYPE = "tau.soul.capture";
23
7
 
24
- export default function soulExtension(pi: ExtensionAPI): void {
25
- let inputs: BuildSystemPromptOptions | null = null;
8
+ const DOCUMENTATION = `Consult Pi or Tau documentation when the request concerns their usage or extension APIs.
9
+ Pi documentation:
10
+ - Main documentation: ${getReadmePath()}
11
+ - Additional docs: ${getDocsPath()}
12
+ - Examples: ${getExamplesPath()}
13
+ - Resolve docs/... and examples/... under those installed paths, not the working directory.
14
+ - Extensions: docs/extensions.md and examples/extensions/; themes: docs/themes.md; skills: docs/skills.md; prompt templates: docs/prompt-templates.md; TUI: docs/tui.md; keybindings: docs/keybindings.md; SDK: docs/sdk.md; providers: docs/custom-provider.md; models: docs/models.md; packages: docs/packages.md; environment: docs/environment-variables.md.
15
+ - Read the relevant documentation and follow related Markdown references before implementing Pi integrations.`;
26
16
 
27
- pi.on("session_start", () => {
28
- inputs = null;
29
- });
30
- pi.on("session_shutdown", () => {
31
- inputs = null;
32
- });
33
- pi.on("before_agent_start", (event) => {
34
- // Keep the shared options reference until all contributors have finished.
35
- inputs = event.systemPromptOptions;
36
- });
17
+ export default function soulExtension(pi: ExtensionAPI): void {
18
+ pi.on("before_agent_start", async (event, ctx) => {
19
+ const options = event.systemPromptOptions;
37
20
 
38
- onTauEventImmediately(pi, "soul.tools", "tau:prompt.tools.check", ({ ctx, tools, reject }) => {
39
- try {
40
- const saved = restorePrompt(ctx.sessionManager.getBranch());
41
- if (!saved || !ctx.model) return;
42
- const previous = admittedTools(saved);
43
- // getAllTools exposes schemas but not constrainedSampling. Preserve that
44
- // metadata here; the request boundary checks the complete Pi declarations.
45
- const reason = toolChangeReason(
46
- ctx.model,
47
- previous,
48
- tools.map((tool) => ({
49
- ...previous.find((candidate) => candidate.name === tool.name),
50
- ...tool,
51
- })),
52
- );
53
- if (reason) reject(reason);
54
- } catch (error) {
55
- reject(String(error));
21
+ // Sources that refresh on compaction are read once per compaction epoch and saved on the branch.
22
+ const branch = ctx.sessionManager.getBranch();
23
+ let epochStart = 0;
24
+ branch.forEach((entry, index) => {
25
+ if (entry.type === "compaction") epochStart = index + 1;
26
+ });
27
+ let captured: Record<string, string> = {};
28
+ for (const entry of branch.slice(epochStart)) {
29
+ if (entry.type === "custom" && entry.customType === CAPTURE_TYPE)
30
+ captured = entry.data as Record<string, string>;
31
+ }
32
+ const sources = collectPromptSources(pi);
33
+ const missing = sources.filter((source) => source.refresh === "compaction" && !(source.key in captured));
34
+ if (missing.length > 0) {
35
+ const read = await Promise.all(missing.map(async (source) => [source.key, await source.read(ctx)] as const));
36
+ captured = { ...captured, ...Object.fromEntries(read) };
37
+ pi.appendEntry(CAPTURE_TYPE, captured);
56
38
  }
57
- });
58
39
 
59
- pi.on("context_with_system", async (event, ctx) => {
60
- try {
61
- if (!inputs) throw new Error("Soul has no loaded prompt inputs.");
62
- if (inputs.forceSystemPrompt !== undefined)
63
- throw new Error(
64
- "A forced system prompt conflicts with Soul. Remove the extension's systemPrompt replacement.",
65
- );
66
- const branch = ctx.sessionManager.getBranch();
67
- const newestFirst = [...branch].reverse();
68
- const projection = ctx.sessionManager.buildSessionProjection();
69
- const anchor = [...projection.entries].reverse().find((entry) => entry.messages.length > 0);
70
- if (!anchor) throw new Error("Soul has no conversation anchor.");
71
- const saved = restorePrompt(branch);
72
- const active = new Set(pi.getActiveTools());
73
- const requested = getCurrentTools(event.messages)
74
- .filter((tool) => active.has(tool.name))
75
- .map(toToolDeclaration);
76
- if (saved && ctx.model) {
77
- const reason = toolChangeReason(ctx.model, admittedTools(saved), requested);
78
- if (reason) throw new Error(reason);
79
- }
80
- const values = await readPromptValues(pi, ctx, inputs, saved === null);
81
- if (!saved) {
82
- pi.appendEntry<SavedBaseline>(BASELINE_TYPE, {
83
- version: 1,
84
- compactionId: newestFirst.find((entry) => entry.type === "compaction")?.id ?? null,
85
- afterEntryId: anchor.sourceEntry.id,
86
- text: renderBaseline(inputs, values),
87
- values,
88
- initialTools: getCurrentTools(event.messages),
89
- });
90
- } else {
91
- const previous = new Map(saved.baseline.values.map((value) => [value.key, value]));
92
- for (const update of saved.updates) for (const value of update.data.values) previous.set(value.key, value);
93
- const currentKeys = new Set(values.map((value) => value.key));
94
- for (const value of previous.values()) {
95
- if (value.refresh === "append" && !currentKeys.has(value.key)) values.push({ ...value, text: "" });
96
- }
97
- const changed = values.filter((value) => previous.get(value.key)?.text !== value.text);
98
- const tools = new Map(admittedTools(saved).map((tool) => [tool.name, tool]));
99
- const added = requested.some((tool) => !tools.has(tool.name));
100
- for (const tool of requested) tools.set(tool.name, tool);
101
- if (changed.length > 0 || added)
102
- pi.appendEntry<SavedUpdate>(UPDATE_TYPE, {
103
- version: 1,
104
- baselineEntryId: saved.entryId,
105
- afterEntryId: anchor.sourceEntry.id,
106
- text: renderUpdate(changed),
107
- values: changed,
108
- tools: [...tools.values()],
109
- });
110
- }
111
- const admitted = restorePrompt(ctx.sessionManager.getBranch());
112
- if (!admitted) throw new Error("Soul failed to save its baseline.");
113
- const messages = projectPrompt(event.messages, admitted, ctx);
114
- const checkpointEntry = newestFirst.find(
115
- (entry) => entry.type === "custom" && entry.customType === CHECKPOINT_TYPE,
116
- );
117
- const checkpoint = checkpointEntry?.type === "custom" ? (checkpointEntry.data as PrefixCheckpoint) : null;
118
- if (checkpoint?.baselineEntryId === admitted.entryId) {
119
- const prefix = createHash("sha256")
120
- .update(JSON.stringify(messages.slice(0, checkpoint.count)))
121
- .digest("hex");
122
- if (prefix !== checkpoint.hash)
123
- throw new Error("Previously sent conversation content changed. Compact before continuing.");
124
- }
125
- const hash = createHash("sha256").update(JSON.stringify(messages)).digest("hex");
126
- if (hash !== checkpoint?.hash)
127
- pi.appendEntry<PrefixCheckpoint>(CHECKPOINT_TYPE, {
128
- baselineEntryId: admitted.entryId,
129
- count: messages.length,
130
- hash,
131
- });
132
- emitTauEvent(pi, "tau:prompt.snapshot", {
133
- text: [admitted.baseline.text, ...admitted.updates.map((update) => update.data.text)].join("\n\n"),
134
- });
135
- return { messages };
136
- } catch (error) {
137
- ctx.ui.notify(`Soul stopped this request: ${error instanceof Error ? error.message : String(error)}`, "error");
138
- ctx.abort();
139
- // Pi currently enters the provider with an aborted signal; do not weaken that
140
- // cancellation into a fallback prompt. See soul-request-check-findings.md.
141
- return undefined;
40
+ const active = pi.getActiveTools();
41
+ const guidance = [
42
+ ...new Set([
43
+ ...(active.includes("bash") ? ["Use bash for file operations like ls, rg, find."] : []),
44
+ ...active.flatMap((name) => options.toolGuidelines[name] ?? []),
45
+ ...options.promptGuidelines,
46
+ ]),
47
+ ]
48
+ .map((rule) => `- ${rule}`)
49
+ .join("\n");
50
+
51
+ const sections = new Map<string, string[]>();
52
+ const add = (section: string, text: string) => {
53
+ if (text.trim()) sections.set(section, [...(sections.get(section) ?? []), text]);
54
+ };
55
+ add("documentation", DOCUMENTATION);
56
+ add("tool-guidance", guidance);
57
+ if (options.customPrompt) add("additional-instructions", options.customPrompt);
58
+ for (const source of sources) {
59
+ add(source.section, source.refresh === "append" ? await source.read(ctx) : (captured[source.key] ?? ""));
142
60
  }
61
+
62
+ // Pi diffs these sections against the prompt already in the transcript and appends a patch only for
63
+ // sections whose text changed, so unchanged sections keep the cached prefix.
64
+ options.customPrompt = FIXED_INSTRUCTIONS;
65
+ for (const name of [...sections.keys()].sort()) options.sections[name] = (sections.get(name) ?? []).join("\n\n");
66
+ emitTauEvent(pi, "tau:prompt.snapshot", { text: event.systemPrompt });
143
67
  });
144
68
  }
@@ -9,6 +9,8 @@ Use headings or tables when they improve clarity. In conversational, personal, o
9
9
  Use technical terms when they help. Keep paths, commands, API names, and errors exact.
10
10
  State the intended action directly. Avoid adding what you won't do, what will remain unchanged, or how you'll separate or categorize results.
11
11
  Give useful facts instead of praise, ceremony, or commentary about following instructions.
12
+ Talk about the user's work, not the machinery directing your behavior. Do not volunteer commentary about system prompts, tool prompts, internal instructions, the harness, or automatic validation. Discuss those mechanisms only when the user asks about them as the subject of the work.
13
+ Finish with the concrete result. Do not hedge completion because silent validation is pending, announce that checks will run or rerun, or explain what happens when you end the turn. When a failure is reported, fix it and describe the correction without narrating the validation process.
12
14
  You are a partner, and the user expects you to act like one.
13
15
  </communication>
14
16
 
@@ -32,6 +32,10 @@ Records private prompt-cache fingerprints without storing prompt content. Run `/
32
32
 
33
33
  Adds `/clear-screen` to clear terminal output without changing the session.
34
34
 
35
+ ## codex-priority
36
+
37
+ Requests Codex priority processing (Fast mode) for every `gpt-6-luna` request on the `openai-codex` provider, including the agent loop, compaction, tool approval, auto-naming, commit, and handoff. It has no command or setting. Cost uses OpenAI's 2.5x Fast mode rate for GPT-6 models. Do not run another extension that overlays `openai-codex` or sets `service_tier`.
38
+
35
39
  ## commit
36
40
 
37
41
  Adds `/commit` for semantic commit grouping, review, and committing selected repository changes.
@@ -40,6 +44,10 @@ Adds `/commit` for semantic commit grouping, review, and committing selected rep
40
44
 
41
45
  Adds `/cost-report` to build an HTML spend report from local session usage. Pick a time frame (past 7 days, current week, current month, year to date, or a specific month) and scope (current project or all sessions). Tau scans sessions, writes under `~/.pi/tau/cost-reports/`, opens the file, and notifies with the path. Empty windows warn without writing a file.
42
46
 
47
+ ## compaction
48
+
49
+ Writes compaction summaries (automatic, `/compact`, and overflow recovery) with a cheaper model from the same provider as the session, so Opus, GPT-6.1 Sol, and Astra sessions do not pay their own rates to summarize themselves. Summaries never cross providers, and a notice names the model that wrote each one. When no cheaper model fits, Pi's default compaction runs.
50
+
43
51
  ## context
44
52
 
45
53
  Adds `/context` to browse and inject reusable repository work scopes from `.pi/contexts`. Selecting entries injects them once into the conversation: `read` paths as complete files, `show` targets as current declaration slices, `outline` paths as Explore structures, and one hidden note listing `references` plus instructions to treat the injected material as current. Run `/context` again to inject more. Edit catalog files by hand when work scopes change. Domain folders are `NN_slug` tabs (ordered by the two-digit prefix; UI shows the slug), TOML files are concepts, and TOML sections are selectable entries.
@@ -106,7 +114,7 @@ Runs configured commands while keeping their output out of agent context when th
106
114
 
107
115
  ## soul
108
116
 
109
- Supplies Tau's communication, discussion, planning, execution, and coding instructions. 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.
117
+ Supplies Tau's communication, discussion, planning, execution, and coding instructions, plus tool guidance and context from other Tau extensions. The date and directory snapshot stay fixed until successful compaction. Other changes, such as an edited `AGENTS.md` after `/reload`, arrive as appended updates without rewriting earlier instructions.
110
118
 
111
119
  ## stash
112
120
 
@@ -126,7 +134,7 @@ Reviews agent `bash` and `script_runner` requests before they run. Common read-o
126
134
 
127
135
  ## tool-loader
128
136
 
129
- 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.
137
+ Keeps specialist tool groups out of every request as deferred tools and lets the agent load them with Pi's `tool_search`. Tau registers `web`, `image`, and `appshot`; project or global package extensions can add groups with `registerDeferredToolGroup()` from `@shanepadgett/tau-agent`. All models can load tools. Compatible models preserve the cached prefix; other models can incur a cache miss when tools are activated. Loading never triggers compaction.
130
138
 
131
139
  ## web
132
140
 
@@ -1,4 +1,4 @@
1
- import type { ThinkingLevel, Tool } from "@earendil-works/pi-ai";
1
+ import type { Tool } from "@earendil-works/pi-ai";
2
2
  import {
3
3
  isToolCallEventType,
4
4
  type ExtensionAPI,
@@ -9,7 +9,8 @@ import { Marker } from "@shanepadgett/tau-tui";
9
9
  import { Type } from "typebox";
10
10
  import { emitAgentBlocked } from "../../shared/agent-blocked.ts";
11
11
  import { emitTauEvent } from "../../shared/events.ts";
12
- import { generateToolValidated, resolveCandidates } from "../../shared/model-fallback/index.ts";
12
+ import { generateToolValidated } from "../../shared/model-fallback/index.ts";
13
+ import { resolveEffortCandidates } from "../../shared/model-effort.ts";
13
14
  import type { ScriptSourceStore } from "../../shared/script-source.ts";
14
15
  import { errorText, truncAt } from "../../shared/text.ts";
15
16
  import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
@@ -59,15 +60,6 @@ const REVIEW_TOOL = {
59
60
  parameters: REVIEW_SCHEMA,
60
61
  } satisfies Tool;
61
62
 
62
- const REVIEW_MODELS: ReadonlyArray<{ provider: string; model: string; reasoning: ThinkingLevel }> = [
63
- { provider: "openai", model: "gpt-6-luna", reasoning: "medium" },
64
- { provider: "openai-codex", model: "gpt-6-luna", reasoning: "medium" },
65
- { provider: "anthropic", model: "claude-sonnet-5", reasoning: "medium" },
66
- { provider: "xai", model: "grok-4.5", reasoning: "low" },
67
- { provider: "openrouter", model: "deepseek/deepseek-v4.1-flash", reasoning: "high" },
68
- { provider: "opencode-go", model: "deepseek-v4.1-flash", reasoning: "high" },
69
- ];
70
-
71
63
  type ToolReview =
72
64
  | { decision: "approved"; summary: string }
73
65
  | { decision: "requires_user_approval"; summary: string; reason: string };
@@ -383,23 +375,11 @@ async function reviewAssistantRequests(
383
375
 
384
376
  async function reviewToolRequest(ctx: ExtensionContext, request: ToolApprovalRequest): Promise<ToolReviewResult> {
385
377
  const requestJson = JSON.stringify(request);
386
- const reviewer = REVIEW_MODELS.find((item) => item.provider === ctx.model?.provider);
387
- const preferred = reviewer ? [reviewer] : [];
388
- if (ctx.model) {
389
- preferred.push({
390
- provider: ctx.model.provider,
391
- model: ctx.model.id,
392
- reasoning: "medium",
393
- });
394
- }
395
- const candidates = await resolveCandidates(ctx, preferred, false);
396
- const wanted = preferred[0];
397
- if (
398
- wanted &&
399
- !candidates.some((item) => item.model.provider === wanted.provider && item.model.id === wanted.model)
400
- ) {
401
- ctx.ui.notify(`Tool review skipped ${wanted.provider}/${wanted.model}; trying next model.`, "info");
402
- }
378
+ // Requests contain shell commands and scripts, so only the session's own provider reviews them.
379
+ const provider = ctx.model?.provider;
380
+ const candidates = (
381
+ await resolveEffortCandidates(ctx, "quick", { includeParentModel: true, preferredProvider: provider })
382
+ ).filter((candidate) => candidate.model.provider === provider);
403
383
  const { value, candidate } = await generateToolValidated(
404
384
  ctx,
405
385
  candidates,
@@ -1,15 +1,15 @@
1
1
  # Tool Loader
2
2
 
3
- Tau progressively exposes registered specialist tool groups. Most coding turns do not need web, image, macOS application, or application-specific schemas, so Pi can load those tools later without discarding supported provider cache prefixes.
3
+ Tau registers specialist tool groups as deferred tools so their schemas stay out of every request. Most coding turns do not need web, image, macOS application, or application-specific schemas. The agent finds and loads them with Pi's built-in `tool_search` tool, which Tau keeps active whenever deferred tools exist.
4
4
 
5
- The agent normally calls `load_tools` itself. Tau's built-in groups are:
5
+ Tau's built-in groups are:
6
6
 
7
7
  - `web` for public web and implementation research
8
8
  - `image` for raster image generation and editing
9
9
  - `appshot` for macOS window discovery, capture, and activation
10
10
 
11
- Project and global package extensions can add groups with `registerDeferredToolGroup()` from `@shanepadgett/tau-agent`. The group description is included in the loader catalog so the agent can select it when a task needs that capability.
11
+ Project and global package extensions can add groups with `registerDeferredToolGroup()` from `@shanepadgett/tau-agent`. The group id and description become the tool namespace shown to the agent.
12
12
 
13
- 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.
13
+ Loaded tools are recorded in the session and stay available on that branch. All models can discover and load deferred tools. Supported models preserve the cached prompt prefix; other models, including Grok and opencode-go, can incur a cache miss when tools are activated. Tau does not compact automatically.
14
14
 
15
- After changing this extension during development, run `/reload` before testing.
15
+ Pi's built-in `tool_search` extension must be enabled. After changing this extension during development, run `/reload` before testing.